跳轉到內容

Pascal 程式設計/記錄

來自華夏公益教科書

成功程式設計的關鍵是找到資料的“正確”結構和程式。

—尼克勞斯·維爾特[1]

在你學會使用 array 之後,本章介紹了另一種資料型別結構概念,稱為 record。像 array 一樣,使用 record 的主要目的是允許你編寫乾淨的、結構化的 程式。否則它是可選的。

概念

[edit | edit source]

你在 第一章 中簡要地看到了 record。雖然 array同質的資料聚合,這意味著所有成員都必須具有相同的基本資料型別,但 record 可能,但並非一定是具有各種不同資料型別的聚合。 [2]

宣告

[edit | edit source]

一個 record 資料型別宣告看起來很像一個集合的變數宣告

program recordDemo;
type
	(* a standard line on a text console *)
	line = string(80);
	(* 1st grade through 12th grade *)
	grade = 1..12;
	
	(* encapsulate all administrative data in one structure *)
	student = record
			firstname: line;
			lastname: line;
			level: grade;
		end;

宣告以 record 一詞開始,並以 end 結束。在兩者之間,你聲明瞭欄位,或成員,整個 record 的成員元素。

這裡分號的作用是分隔成員。關鍵字 end 實際上將終止一個 record 宣告。請注意,在以下正確示例中,最後一個成員聲明後面沒有分號
program recordSemicolonDemo;
type
	sphere = record
			radius: real;
			volume: real;
			surface: real
		end;
儘管如此,在最後一行新增分號仍然是一種常見的做法,即使它不是必需的。否則你將太頻繁地簡單地在最後一行新增一個新成員,而忘記在前面一行新增分號,從而引發語法錯誤。

所有 record 成員必須在 record 宣告本身具有不同的名稱。例如,在上面的示例中,宣告兩個名為 level 的“變數”,成員元素將被拒絕。

對要宣告的欄位數量沒有要求。一個“空” record 也是可能的:[fn 1]

type
	emptyRecord = record
		end;

多個相同資料型別的欄位

[edit | edit source]

變數宣告 類似,你可以透過用逗號分隔識別符號來定義多個相同資料型別的欄位。之前的 sphere 宣告也可以寫成

type
	sphere = record
			radius, volume, surface: real;
		end;

然而,大多數 Pascal 老手和風格指南不鼓勵使用這種簡寫符號(用於變數和 record 宣告,以及在形式引數列表中)。它只有在所有宣告的識別符號絕對始終具有相同資料型別時才合理;幾乎可以保證你永遠不想改變逗號分隔列表中僅一個欄位的資料型別。如有疑問,請使用長格式。在程式設計中,便利起著間接的作用。

使用

[edit | edit source]

透過宣告一個 record 變數,你立即獲得了整個集合的“子”變數。訪問它們是透過指定record 變數的名稱,加上一個點 (.),然後是 record 欄位的名稱

var
	posterStudent: student;
begin
	posterStudent.firstname := 'Holden';
	posterStudent.lastname := 'Caulfield';
	posterStudent.level := 10;
end.

你已經在上一章關於 字串 中看到了點符號,其中在 .capacity 上附加一個 string() 變數的名稱指的是相應變數的字元容量。這不是巧合。

然而,尤其是初學者有時會混淆資料型別名稱與變數的名稱。以下程式碼突出了區別。請記住,資料型別 宣告 不會保留任何記憶體,並且主要是為編譯器提供資訊,而變數宣告實際上會分配一塊記憶體。
program dotNoGo(output); { This program does not compile. }
type
	line = string(80);
	quizItem = record
			question: line;
			answer: line;
		end;
var
	response: line;
	challenge: quizItem;
begin
	writeLn(line.capacity); { ↯ `line` is not a variable }
	writeLn(response.capacity); { ✔ correct }
	
	writeLn(quizItem.question); { ↯ `quizItem` refers to a data type }
	{ Data type declarations (as per definition) do not reserve any memory }
	{ thus you cannot “read/write” from/to a data type. }
	writeLn(challenge.question); { ✔ correct }
end.
而且,與往常一樣,你需要先為變數分配一個值,然後才能讀取它。上面的原始碼忽略了這一點,以關注主要問題。關鍵是,點符號 (.) 只有在記憶體的情況下才有效。 [fn 2]


優點

[edit | edit source]

但是為什麼以及何時我們想要使用 record?乍一看,在給出的示例中,它似乎是宣告和使用多個變數的麻煩方式。然而, record 被作為一個單位處理這一事實包含一個很大的優勢

  • 您可以透過簡單的賦值(:=)來複製整個record的值。
  • 這意味著您可以一次傳遞大量資料:一個record可以作為例程的引數,在EP函式中也可以作為返回值。[fn 3]

顯然,您希望將始終一起出現的資料分組。將無關的資料分組沒有意義,僅僅因為我們可以這樣做。另一個非常有用的優點將在下面關於變體記錄部分中介紹。

路由重寫

[編輯 | 編輯原始碼]

正如您之前看到的,引用record的成員可能會有點繁瑣,因為我們一遍又一遍地重複變數名。幸運的是,Pascal 允許我們稍微縮寫一下。

With-子句

[編輯 | 編輯原始碼]

with-子句允許我們消除重複公共字首,特別是record變數的名稱。[3]

begin
	with posterStudent do
	begin
		firstname := 'Holden';
		lastname := 'Caulfield';
		level := 10;
	end;
end.

所有標識值的識別符號首先在recordposterStudent範圍內進行查詢。如果找不到匹配項,則也會考慮給定record外部的所有變數識別符號。

當然,仍然可以使用完整名稱來表示record成員。例如,在上面的原始碼中,在with-子句仍然可以完全合法地寫posterStudent.level。誠然,這會違背with-子句的目的,但有時為了文件目的,強調特定的record變數可能仍然是有益的。但是,重要的是要理解,FQI(完全限定識別符號),即帶有一個點的識別符號,不會失去其“有效性”。

原則上,所有包含“點”的結構化值元件都可以用with來縮寫。這也適用於你在上一章學過的資料型別string

program withDemo(input, output);
type
	{ Deutsche Post „Maxi-Telegramm“ }
	telegram = string(480);
var
	post: telegram;
begin
	with post do
	begin
		writeLn('Enter your telegram. ',
			'Maximum length = ',
			capacity, ' characters.');
		readLn(post);
		{ … }
	end;
end.

這裡,在with-子句capacity內,同樣在post.capacity內,都指的是post.capacity

多個級別

[編輯 | 編輯原始碼]

如果需要巢狀多個with-子句,可以使用簡短的表示法

	with snakeOil, sharpTools do
	begin
		
	end;

它等效於

	with snakeOil do
	begin
		with sharpTools do
		begin
			
		end;
	end;

重要的是要牢記,首先在sharpTools中搜索識別符號,如果找不到匹配項,其次,考慮snakeOil中的識別符號。

變體記錄

[編輯 | 編輯原始碼]

在 Pascal 中,record是唯一允許您在執行時(program執行時)改變其結構的資料型別結構概念。這種record的超實用屬性允許我們編寫涵蓋許多情況的通用程式碼。

讓我們看一個例子

type
	centimeter = 10..199;
	
	// order of female, male has been chosen, so `ord(sex)`
	// returns the [minimum] number of non-defective Y chromosomes
	sex = (female, male)
	
	// measurements according EN 13402 size designation of clothes [incomplete]
	clothingSize = record
			shoulderWidth: centimeter;
			armLength: centimeter;
			bustGirth: centimeter;
			waistSize: centimeter;
			hipMeasurement: centimeter;
			case body: sex of
				female: (
					underbustMeasure: centimeter;
				);
				male: (
				);
		end;

record變體部分以關鍵字case開頭,您已經從選擇中瞭解過。之後跟著一個record成員宣告,變體選擇器,但您不要使用分號,而是使用關鍵字of。在此之後,所有可能的變體都在下面。每個變體都用變體選擇器域中的一個值來標記,這裡分別是femalemale。用冒號(:)分隔,之後跟著用括號括起來的變體指示符。在這裡,您可以列出只有在某個變體“啟用”時才可用的其他record成員。請注意,所有變體中的所有識別符號都必須是唯一的。各個變體用分號分隔,最多可以有一個變體部分,它必須出現在最後。因為您需要能夠列出所有可能的變體,所以變體選擇器必須是序數資料型別。

使用變體記錄要求您首先選擇一個變體。透過將一個值賦給變體選擇器來“啟用”變體。請注意,變體不是“建立”的;它們都在program啟動時就已存在。您只需要做出選擇。

	boobarella.body := female;
	boobarella.underbustMeasure := 69;

只有在將值賦給變體選擇器之後,並且只要該值保持不變,您才能訪問相應變體的任何欄位。反轉前兩行程式碼並嘗試訪問underbustMeasure欄位是非法的,即使body尚未定義,更重要的是,它不具有值female

在您的program中稍後更改變體選擇器是完全可以的,然後使用不同的變體,但是變體部分中所有先前儲存的值都將失效,您無法恢復它們。如果您將變體切換回先前,原始的值,則需要重新分配該變體中的所有值。

這個概念開闢了新的視野:您可以以簡潔的方式更互動地設計您的程式。現在,您可以根據執行時資料(program執行時讀取的資料)來選擇變體。因為在任何時候(在第一次將值賦給變體選擇器之後),只有一個變體是“啟用”的,所以如果您的program嘗試讀取/寫入“啟用”變體的值,它就會崩潰。這是期望的行為,因為這就是擁有不同變體的目的。它保證了您的程式整體完整性。

匿名變體

[編輯 | 編輯原始碼]

Pascal 還允許使用匿名變體選擇器,即不帶任何名稱的選擇器。其含義是

  • 您無法顯式選擇(或查詢)任何變體,因此
  • 反過來,所有變體都被認為是同時“啟用”的。

“但是,這不是練習的目的嗎?”你可能會問。是的,確實,因為沒有命名選擇器,你的 program 無法跟蹤哪個變體應該工作,哪個變體是“有缺陷的”。 有責任確定目前你可以合理地讀/寫哪個變體。

匿名變體經常被濫用來實現“型別轉換”。如果你有一個匿名變體部分,你可以宣告帶有不同資料型別的成員,這些成員反過來決定底層資料的解釋方法。然後,你可以利用這樣一個事實,即許多(但不一定是所有)編譯器將所有變體放在相同的記憶體塊中。

程式碼:

program anonymousVariantsDemo(output);
type
	bitIndex = 0..(sizeOf(integer) * 8 - 1);
	
	exposedInteger = record
			case Boolean of
				false: (
						value: integer;
					);
				true: (
						bit: set of bitIndex;
					);
		end;

var
	i: exposedInteger;
begin
	i.bit := [4];
	writeLn(i.value);
end.

輸出:

16
16 是(這應該被認為是“巧合”). 我們強調,所有 Pascal 標準都沒有對內部記憶體結構做出任何宣告。高階程式語言不關心資料如何儲存,它甚至不知道“位”,“高電壓”/“低電壓”的概念。
Warning 因此,如果你(有意地)使用這裡演示的任何行為,你不能再說是“我在用 Pascal 程式設計”,而是在專門針對編譯器如此這般進行程式設計。資料結構的記憶體佈局在 Pascal 實現之間有所不同
例如,上面的示例是為 GPCFPC 在其預設配置下設計的,並且可以使用它們。不要把它看作是“Pascal”,而是一個它的後代。使用不同的編譯器很可能會產生不同的結果。

這個概念也存在於許多其他程式語言中。例如,在程式語言 C 中,它被稱為 聯合

條件迴圈

[edit | edit source]

到目前為止,我們一直只使用計數 迴圈。如果你可以提前預測迭代次數,迴圈體需要執行多少次,這將非常棒。但很多時候,無法事先制定一個適當的表示式來確定迭代次數。

條件迴圈 允許你讓下一次迭代的執行取決於一個Boolean 表示式。它們有兩種形式

  • 頭部控制迴圈,以及
  • 尾部控制迴圈。

區別在於,尾部控制迴圈的迴圈體至少執行一次,而頭部控制迴圈可能根本不執行迴圈體。在任何情況下,都會反覆評估一個條件,並且必須保持該條件才能使迴圈繼續。

頭部控制迴圈

[edit | edit source]

頭部控制迴圈通常被稱為 while 迴圈,因為它的語法。

“控制”條件出現在迴圈體上方,即頭部

程式碼:

program characterCount(input, output);
type
	integerNonNegative = 0..maxInt;
var
	c: char;
	n: integerNonNegative;
begin
	n := 0;
	
	while not EOF do
	begin
		read(c);
		n := n + 1;
	end;
	
	writeLn('There are ', n:1, ' characters.');
end.

輸出:

$ cat ./characterCount.pas | ./characterCount
There are 240 characters.
$ printf '' '' | ./characterCount
There are 0 characters.
迴圈的條件是一個 Boolean 表示式,用 whiledo 兩個詞框起來。對於任何(後續)迭代,該條件必須評估為 true。從輸出中可以看出,在第二種情況下,它甚至可能為零次:顯然,對於輸入,n := n + 1 從未執行

EOFEOF(input) 的簡寫。這個標準 function 如果沒有更多資料可供讀取,則返回 true,通常稱為檔案結束。如果相應的 EOF 函式呼叫返回 true,則從檔案中 read是非法的,並且會非常糟糕地失敗。

與計數迴圈不同,你可以修改條件迴圈的條件所依賴的資料。

const
	(* instead of a hard-coded length `64` *)
	(* you can write `sizeOf(integer) * 8` in Delphi, FPC, GPC *)
	wordWidth = 64;
type
	integerNonNegative = 0..maxInt;
	wordStringIndex = 1..wordWidth;
	wordString = array[wordStringIndex] of char;

function binaryString(n: integerNonNegative): wordString;
var
	(* temporary result *)
	binary: wordString;
	i: wordStringIndex;
begin
	(* initialize `binary` with blanks *)
	for i := 1 to wordWidth do
	begin
		binary[i] := ' ';
	end;
	(* if n _is_ zero, the loop's body won't be executed *)
	binary[i] := '0';
	
	(* reverse Horner's scheme *)
	while n >= 1 do
	begin
		binary[i] := chr(ord('0') + n mod 2);
		n := n div 2;
		i := i - 1;
	end;
	
	binaryString := binary;
end;

迴圈條件所依賴的 n 將被反覆除以二。由於除法運算子是整數除法 (div),因此在某個時候,值 1 將被除以二,並且算術上正確的結果 0.5 被截斷 (trunc) 到零。但是,值 0 不再滿足迴圈的條件,因此將不會有任何後續迭代。

尾部控制迴圈

[edit | edit source]

在尾部控制迴圈中,條件出現在迴圈體下方,在尾部。迴圈體始終在條件評估之前被執行一次。

program repeatDemo(input, output);
var
	i: integer;
begin
	repeat
	begin
		write('Enter a positive number: ');
		readLn(i);
	end
	until i > 0;
	
	writeLn('Wow! ', i:1, ' is a quite positive number.');
end.

迴圈體被 repeatuntil 關鍵字封裝起來。[fn 4]until 後面跟一個 Boolean 表示式。與 while 迴圈相反,尾部控制迴圈始終繼續,始終保持執行,until 指定的條件變為 true。一個 true 條件表示結束。在上面的示例中,使用者將被反覆提示,直到他最終服從並輸入一個正數。

日期和時間

[edit | edit source]

本節向你介紹 ISO 標準 10206 中定義的擴充套件 Pascal 的功能。你需要一個符合 EP 的編譯器才能使用這些功能。

時間戳

[edit | edit source]

EP 中,有一個名為 timeStamp標準資料型別。它被宣告如下:[fn 5]

type
	timeStamp = record
			dateValid: Boolean;
			timeValid: Boolean;
			year: integer;
			month: 1..12;
			day: 1..31;
			hour: 0..23;
			minute: 0..59;
			second: 0..59;
		end;

從宣告中可以看出,timeStamp 還包含用於日曆日期的資料欄位,而不僅僅是標準時鍾指示的時間。

Note 處理器(即通常是編譯器)可能會提供額外的(因此是非標準的)欄位。例如,GPC 提供了包括 timeZone 在內的其他欄位,該欄位指示相對於 UTC(“世界時間”)的秒數偏移量。

獲取時間戳

[edit | edit source]

EP 還定義了一個一元的 procedure,它將值填充到 timeStamp 變數中。 GetTimeStamp 將值分配給傳遞到第一個(也是唯一)引數中的 timeStamp record 的所有成員。這些值代表呼叫此過程時的“當前日期”和“當前時間”。但是,在 1980 年代,並非所有(個人/家庭)計算機都具有內建的“即時”時鐘。因此,ISO 標準 10206 在 21 世紀之前制定,指出“當前”一詞是“實現定義的”。dateValidtimeValid 欄位專門用於解決某些計算機根本不知道當前日期和/或時間的問題。從 timeStamp 變數中讀取值時,在讓 getTimeStamp 填充它們之後,仍建議先檢查其有效性。

如果 getTimeStamp 無法獲得“有效”值,它將設定

  • daymonthyear 為表示 公元 1 年 1 月 1 日 的值,但同時也將 dateValid 設定為 false
  • 對於時間,hourminutesecond 都會變為 0,表示午夜的值。 timeValid 欄位變為 false

兩者相互獨立,因此完全有可能只確定時間,但日期無效。

請注意,公曆是在公元 1582 年引入的,因此 timeStamp 資料型別通常對公元 1583 年之前的任何日期都無用。

可列印的日期和時間

[編輯 | 編輯原始碼]

獲得 timeStamp 後,EP 還會提供兩個一元函式

  • date 返回 daymonthyear 的人類可讀的 string 表示形式,以及
  • time 返回 hourminutesecond 的人類可讀的 string 表示形式。

如果 dateValidtimeValid 分別指示資料無效,則這兩個函式都將失敗並終止 program。請注意,string 表示形式的確切格式未由 ISO 標準 10206 定義。

將這些內容放在一起,考慮以下 program

程式碼:

program dateAndTimeFun(output);
var
	ts: timeStamp;
begin
	getTimeStamp(ts);
	
	if ts.dateValid then
	begin
		writeLn('Today is ', date(ts), '.');
	end;
	
	if ts.timeValid then
	begin
		writeLn('Now it is ', time(ts), '.');
	end;
end.

輸出:

Today is 10 Oct 2024.
Now it is 13:42:42.
輸出可能會有所不同。這裡,使用的是 GPC,硬體有一個 RTC。不言而喻,如果你看到的是,那就是 dateValidtimeValid 都為 false

迴圈總結

[編輯 | 編輯原始碼]

現在是盤點並重申各種迴圈的好時機。

條件迴圈

[編輯 | 編輯原始碼]

如果你無法預測總迭代次數,條件迴圈是首選工具。

頭部控制迴圈 尾部控制迴圈
while condition do
begin
	
end;
 
repeat
begin
	
end
until condition;
condition 必須評估為 true 才能發生任何(包括後續)迭代。 condition 必須為 false 才能發生任何後續迭代。
Pascal 中條件迴圈的比較

可以將任一迴圈表示為另一個迴圈,但通常其中一個更合適。尾部控制迴圈特別適合在還沒有任何資料可以判斷的情況下使用,以便在第一次迭代之前評估合適的 condition

計數迴圈

[編輯 | 編輯原始碼]

如果你可以在進入迴圈之前預測總迭代次數,則計數迴圈很合適。

向上計數迴圈 向下計數迴圈
for controlVariable := initialValue to finalValue do
begin
	
end;
for controlVariable := initialValue downto finalValue do
begin
	
end;
在每次非最終迭代之後,controlVariable 都會變為 succ(controlVariable)controlVariable 必須小於或等於 finalValue 才能發生另一次迭代。 在每次非最終迭代之後,controlVariable 都會變為 pred(controlVariable)controlVariable 必須大於或等於 finalValue 才能發生另一次迭代。
Pascal 中計數迴圈方向的比較
Note initialValuefinalValue 表示式都會被精確評估一次[4] 這與條件迴圈有很大不同。

在計數迴圈的迴圈體內,你不能修改計數變數,只能讀取它。這可以防止任何意外操作,並確保計算的預測總迭代次數確實會發生。

Note 不能保證 controlVariable 在迴圈“之後”為 finalValue。如果有正好零次迭代,則對 controlVariable 不會進行任何賦值。因此,通常假定 controlVariablefor 迴圈之後無效/未初始化,除非你絕對確定至少進行了一次迭代。

對聚合進行迴圈

[編輯 | 編輯原始碼]

如果你使用的是支援 EP 的編譯器,還可以選擇對集合使用 for in 迴圈

program forInDemo(output);
type
	characters = set of char;
var
	c: char;
	parties: characters;
begin
	parties := ['R', 'D'];
	for c in parties do
	begin
		write(c:2);
	end;
	writeLn;
end.

你已經走到這一步了,你所知道的知識已經相當令人印象深刻。由於本章關於 record 的概念應該不難理解,以下練習主要側重於訓練。一個專業的計算機程式設計師大部分時間都花在思考什麼樣的實現,使用哪些工具(例如 array “vs.” set),是最有用/合理的。鼓勵你在開始輸入任何內容之前先思考。儘管如此,有時(尤其是由於你缺乏經驗)你需要嘗試一下,如果這是有意的,那就沒關係。漫無目的地找到解決方案並不能體現真正的程式設計師。

一個 record 可以包含另一個 record 嗎?
就像 array 可以包含另一個 array 一樣,record 也完全可以做到。寫一個測試 program 來驗證這一點。重要的是要注意,點符號可以無限擴充套件(myRecordVariable.topRecordFieldName.nestedRecordFieldName.doubleNestedRecordFieldName)。顯然,在某個時候它變得難以閱讀,因此請明智地使用它。
就像 array 可以包含另一個 array 一樣,record 也完全可以做到。寫一個測試 program 來驗證這一點。重要的是要注意,點符號可以無限擴充套件(myRecordVariable.topRecordFieldName.nestedRecordFieldName.doubleNestedRecordFieldName)。顯然,在某個時候它變得難以閱讀,因此請明智地使用它。


編寫一個永不結束的迴圈,這意味著該迴圈不可能終止。如果你的測試程式沒有終止,你很可能完成了這項任務。在標準的 Linux 終端上,你可以按下 Ctrl+C 來強制殺死該程式。
有兩種無限迴圈
while true do
begin
	
end;

repeat until 迴圈 中需要否定條件

repeat
begin
	
end
until false;
無限迴圈是非常不可取的。雖然像這些示例中這樣的常量表達式很容易識別,但永真式,總是計算為 true 的表示式,或者永遠無法滿足的表示式(在 repeat until 迴圈 的情況下),則不然。例如,假設 i 是一個 integer,則迴圈 while i <= maxInt do 將無限期地執行,因為 i 永遠不會超過 maxInt[fn 6],從而破壞迴圈條件。因此,請記住仔細為條件迴圈制定表示式,並確保它最終會達到終止狀態。否則,這可能會讓你的 program 的使用者感到沮喪。
有兩種無限迴圈
while true do
begin
	
end;

repeat until 迴圈 中需要否定條件

repeat
begin
	
end
until false;
無限迴圈是非常不可取的。雖然像這些示例中這樣的常量表達式很容易識別,但永真式,總是計算為 true 的表示式,或者永遠無法滿足的表示式(在 repeat until 迴圈 的情況下),則不然。例如,假設 i 是一個 integer,則迴圈 while i <= maxInt do 將無限期地執行,因為 i 永遠不會超過 maxInt[fn 6],從而破壞迴圈條件。因此,請記住仔細為條件迴圈制定表示式,並確保它最終會達到終止狀態。否則,這可能會讓你的 program 的使用者感到沮喪。


將以下迴圈重寫為 while 迴圈
repeat
begin
	imagineJumpingSheep;
	sheepCount := sheepCount + 1;
	waitTwoSeconds;
end
until asleep;
重要的是要意識到,整個迴圈體在 while 迴圈開始之前就被重複了
imagineJumpingSheep;
sheepCount := sheepCount + 1;
waitTwoSeconds;

while not asleep do
begin
	imagineJumpingSheep;
	sheepCount := sheepCount + 1;
	waitTwoSeconds;
end;
不要忘記在將一個條件迴圈轉換為另一個型別時否定條件。顯然,repeat until 迴圈 在這種情況下更合適。
重要的是要意識到,整個迴圈體在 while 迴圈開始之前就被重複了
imagineJumpingSheep;
sheepCount := sheepCount + 1;
waitTwoSeconds;

while not asleep do
begin
	imagineJumpingSheep;
	sheepCount := sheepCount + 1;
	waitTwoSeconds;
end;
不要忘記在將一個條件迴圈轉換為另一個型別時否定條件。顯然,repeat until 迴圈 在這種情況下更合適。


如果你使用的是 Linux 或 FreeBSD OS 以及符合 EP 的編譯器:編寫一個 program,它將命令 getent passwd 的輸出作為輸入,並且只打印每行中的第一個欄位/列。在 passwd(5) 檔案中,欄位用冒號 (:) 分隔。你的 program 將列出所有已知的使用者名稱。
你可以使用命令 getent passwd | ./cut1 執行以下程式(你的可執行程式的檔名可能不同)。
program cut1(input, output);
const
	separator = ':';
var
	line: string(80);
begin
	while not EOF(input) do
	begin
		{ This reads the _complete_ line, but at most}
		{ line.capacity characters are actually saved. }
		readLn(line);
		writeLn(line[1..index(line, separator)-1]);
	end;
end.
請記住,index 將返回冒號字元的索引,你不想列印它,因此你需要從它的結果中減去 1。如果一行不包含冒號,則此 program 顯然會失敗。
你可以使用命令 getent passwd | ./cut1 執行以下程式(你的可執行程式的檔名可能不同)。
program cut1(input, output);
const
	separator = ':';
var
	line: string(80);
begin
	while not EOF(input) do
	begin
		{ This reads the _complete_ line, but at most}
		{ line.capacity characters are actually saved. }
		readLn(line);
		writeLn(line[1..index(line, separator)-1]);
	end;
end.
請記住,index 將返回冒號字元的索引,你不想列印它,因此你需要從它的結果中減去 1。如果一行不包含冒號,則此 program 顯然會失敗。


根據你之前的解決方案,擴充套件你的 program,以便僅列印 UID 大於或等於 1000 的使用者名稱。UID 儲存在第三個欄位中。
已突出顯示更改的行。上一個原始碼中的註釋已省略。
program cut2(input, output);
const
	separator = ':';
	minimumID = 1000;
var
	line: string(80);
	nameFinalCharacter: integer;
	uid: integer;
begin
	while not EOF do
	begin
		readLn(line);
		
		nameFinalCharacter := index(line, separator) - 1;
		
		{ username:encryptedpassword:usernumber:… }
		{         ↑ `nameFinalCharacter + 1` }
		{          ↑ `… + 2` is the index of the 1st password character }
		uid := index(subStr(line, nameFinalCharacter + 2), separator);
		
		{ Note that the preceding `index` did not operate on `line` }
		{ but an altered/different/independent “copy” of it. }
		{ This means, we’ll need to offset the returned index once again. }
		readStr(subStr(line, nameFinalCharacter + 2 + uid), uid);
		{ Read/readLn/readStr automatically terminate reading an integer }
		{ number from the source if a non-digit character is encountered. }
		{ (Preceding blanks/space characters are ignored and }
		{ the _first_ character still may be a sign, that is `+` or `-`.)} 
		
		if uid >= minimumID then
		begin
			writeLn(line[1..nameFinalCharacter]);
		end;
	end;
end.
回想一下,在上一章中,subStr 中的第三個引數可以省略,這實際上意味著“給我一個 string剩餘部分”。注意,此程式設計任務模擬了 cut(1) 的(部分)行為。使用已經為你程式設計的程式/原始碼,只要有可能。沒有必要重新發明輪子。儘管如此,這個基本任務是一個很好的練習。在 RHEL 系統上,你可能更希望將 minimumID 設定為 500
已突出顯示更改的行。上一個原始碼中的註釋已省略。
program cut2(input, output);
const
	separator = ':';
	minimumID = 1000;
var
	line: string(80);
	nameFinalCharacter: integer;
	uid: integer;
begin
	while not EOF do
	begin
		readLn(line);
		
		nameFinalCharacter := index(line, separator) - 1;
		
		{ username:encryptedpassword:usernumber:… }
		{         ↑ `nameFinalCharacter + 1` }
		{          ↑ `… + 2` is the index of the 1st password character }
		uid := index(subStr(line, nameFinalCharacter + 2), separator);
		
		{ Note that the preceding `index` did not operate on `line` }
		{ but an altered/different/independent “copy” of it. }
		{ This means, we’ll need to offset the returned index once again. }
		readStr(subStr(line, nameFinalCharacter + 2 + uid), uid);
		{ Read/readLn/readStr automatically terminate reading an integer }
		{ number from the source if a non-digit character is encountered. }
		{ (Preceding blanks/space characters are ignored and }
		{ the _first_ character still may be a sign, that is `+` or `-`.)} 
		
		if uid >= minimumID then
		begin
			writeLn(line[1..nameFinalCharacter]);
		end;
	end;
end.
回想一下,在上一章中,subStr 中的第三個引數可以省略,這實際上意味著“給我一個 string剩餘部分”。注意,此程式設計任務模擬了 cut(1) 的(部分)行為。使用已經為你程式設計的程式/原始碼,只要有可能。沒有必要重新發明輪子。儘管如此,這個基本任務是一個很好的練習。在 RHEL 系統上,你可能更希望將 minimumID 設定為 500


編寫一個素數篩。一個例程進行計算,另一個例程列印它們。本練習的目標是讓你有機會輸入,編寫一個合適的程式。如有必要,你可以檢視現有的 實現,但仍然自己編寫,在原始碼中新增自己的註釋。
以下 program 滿足所有要求。注意,使用 array[1..limit] of Boolean 的實現也完全沒問題,儘管所示的 set of natural 實現原則上是首選的。
program eratosthenes(output);

type
	{ in Delphi or FPC you will need to write 1..255 }
	natural = 1..4095;
	{$setLimit 4096}{ only in GPC }
	naturals = set of natural;

const
	{ `high` is a Borland Pascal (BP) extension. }
	{ It is available in Delphi, FPC and GPC. }
	limit = high(natural);

{ Note: It is important that `primes` is declared }
{ in front of `sieve` and `list`, so both of these }
{ routines can access the _same_ variable. }
var
	primes: naturals;

{ This procedure sieves the `primes` set. }
{ The `primes` set needs to be fully populated }
{ _before_ calling this routine. }
procedure sieve;
var
	n: natural;
	i: integer;
	multiples: naturals;
begin
	{ `1` is by definition not a prime number }
	primes := primes - [1];
	
	{ find the next non-crossed number }
	for n := 2 to limit do
	begin
		if n in primes then
		begin
			multiples := [];
			{ We do _not_ want to remove 1 * n. }
			i := 2 * n;
			while i in [n..limit] do
			begin
				multiples := multiples + [i];
				i := i + n;
			end;
			
			primes := primes - multiples;
		end;
	end;
end;

{ This procedures lists all numbers in `primes` }
{ and enumerates them. }
procedure list;
var
	count, n: natural;
begin
	count := 1;
	
	for n := 2 to limit do
	begin
		if n in primes then
		begin
			writeLn(count:8, '.:', n:22);
			count := count + 1;
		end;
	end;
end;

{ === MAIN program === }
begin
	primes := [1..limit];
	sieve;
	list;
end.
欣賞一下,由於你將 sieve 任務與 list 任務分離,因此例程定義和 program 底部的主要部分都保持相當短,因此更容易理解。
以下 program 滿足所有要求。注意,使用 array[1..limit] of Boolean 的實現也完全沒問題,儘管所示的 set of natural 實現原則上是首選的。
program eratosthenes(output);

type
	{ in Delphi or FPC you will need to write 1..255 }
	natural = 1..4095;
	{$setLimit 4096}{ only in GPC }
	naturals = set of natural;

const
	{ `high` is a Borland Pascal (BP) extension. }
	{ It is available in Delphi, FPC and GPC. }
	limit = high(natural);

{ Note: It is important that `primes` is declared }
{ in front of `sieve` and `list`, so both of these }
{ routines can access the _same_ variable. }
var
	primes: naturals;

{ This procedure sieves the `primes` set. }
{ The `primes` set needs to be fully populated }
{ _before_ calling this routine. }
procedure sieve;
var
	n: natural;
	i: integer;
	multiples: naturals;
begin
	{ `1` is by definition not a prime number }
	primes := primes - [1];
	
	{ find the next non-crossed number }
	for n := 2 to limit do
	begin
		if n in primes then
		begin
			multiples := [];
			{ We do _not_ want to remove 1 * n. }
			i := 2 * n;
			while i in [n..limit] do
			begin
				multiples := multiples + [i];
				i := i + n;
			end;
			
			primes := primes - multiples;
		end;
	end;
end;

{ This procedures lists all numbers in `primes` }
{ and enumerates them. }
procedure list;
var
	count, n: natural;
begin
	count := 1;
	
	for n := 2 to limit do
	begin
		if n in primes then
		begin
			writeLn(count:8, '.:', n:22);
			count := count + 1;
		end;
	end;
end;

{ === MAIN program === }
begin
	primes := [1..limit];
	sieve;
	list;
end.
欣賞一下,由於你將 sieve 任務與 list 任務分離,因此例程定義和 program 底部的主要部分都保持相當短,因此更容易理解。


編寫一個 program,它從 input 讀取無限數量的數值,並在最後將算術平均值列印到 output
program arithmeticMean(input, output);
type
	integerNonNegative = 0..maxInt;
var
	i, sum: real;
	count: integerNonNegative;
begin
	sum := 0.0;
	count := 0;
	
	while not eof(input) do
	begin
		readLn(i);
		sum := sum + i;
		count := count + 1;
	end;
	
	{ count > 0: do not do division by zero. }
	if count > 0 then
	begin
		writeLn(sum / count);
	end;
end.

請注意,使用不包含負數的資料type(這裡我們將其命名為integerNonNegative)可以減輕count可能翻轉符號的問題,這種情況被稱為溢位。如果count := count + 1變得太大,就會導致program失敗,實際上超出了範圍0..maxInt

儘管存在maxReal,但沒有程式設計方法可以判斷sum是否變得太大或太小,使其變得極不準確,因為無論如何任何sum的值都可能是合法的。
program arithmeticMean(input, output);
type
	integerNonNegative = 0..maxInt;
var
	i, sum: real;
	count: integerNonNegative;
begin
	sum := 0.0;
	count := 0;
	
	while not eof(input) do
	begin
		readLn(i);
		sum := sum + i;
		count := count + 1;
	end;
	
	{ count > 0: do not do division by zero. }
	if count > 0 then
	begin
		writeLn(sum / count);
	end;
end.

請注意,使用不包含負數的資料type(這裡我們將其命名為integerNonNegative)可以減輕count可能翻轉符號的問題,這種情況被稱為溢位。如果count := count + 1變得太大,就會導致program失敗,實際上超出了範圍0..maxInt

儘管存在maxReal,但沒有程式設計方法可以判斷sum是否變得太大或太小,使其變得極不準確,因為無論如何任何sum的值都可能是合法的。


這個任務對於使用符合EP的編譯器的使用者來說是一個很好的練習:編寫一個time function,它返回一個string,以“美國”時間格式9:04 PM。乍一看這似乎很簡單,但它會變得非常具有挑戰性。玩得開心!
一個聰明的人會嘗試重用time本身。但是,time本身的輸出沒有標準化,所以我們需要自己定義所有內容。
type
	timePrint = string(8);

function timeAmerican(ts: timeStamp): timePrint;
const
	hourMinuteSeparator = ':';
	anteMeridiemAbbreviation = 'AM';
	postMeridiemAbbreviation = 'PM';
type
	noonRelation = (beforeNoon, afterNoon);
	letterPair = string(2);
var
	{ contains 'AM' and 'PM' accessible via an index }
	m: array[noonRelation] of letterPair;
	{ contains a leading zero accessible via a Boolean expression }
	z: array[Boolean] of letterPair;
	{ holds temporary result }
	t: timePrint;
begin
	{ fill `t` with spaces }
	writeStr(t, '':t.capacity);

此回退值(在ts.timeValidfalse的情況下)允許此function的程式設計師/“使用者”“盲目”地列印其返回值。輸出中將會有一個明顯的空白。另一個合理的“回退”值是一個空的string

	with ts do
	begin
		if timeValid then
		begin
			m[beforeNoon] := anteMeridiemAbbreviation;
			m[afterNoon] := postMeridiemAbbreviation;
			z[false] := '';
			z[true] := '0';
			
			writeStr(t,
				((hour + 12 * ord(hour = 0) - 12 * ord(hour > 12)) mod 13):1,
				hourMinuteSeparator,
				z[minute < 10], minute:1, ' ',
				m[succ(beforeNoon, hour div 12)]);

這是這個問題中最複雜的部分。首先,所有傳遞給writeStr的數字引數都明確地:1作為最小寬度規範字尾,因為有一些編譯器會假設,例如,:20作為預設值。由於我們知道timeStamp.hour的範圍是0..23,我們可以使用divmod操作,如示例所示。但是,我們需要考慮hour值為0的情況,通常表示為 12:00 AM(而不是零)。使用所示的Boolean表示式和ord進行條件“偏移” 12 可以解決這個問題。此外,這裡簡要提醒一下,在EP 中,succ 函式接受第二個引數

		end;
	end;
	
	timeAmerican := t;
end;
最後,我們需要將臨時結果複製到函式結果變數中。請記住,必須只有一個賦值,儘管並非所有編譯器都強制執行此規則。
一個聰明的人會嘗試重用time本身。但是,time本身的輸出沒有標準化,所以我們需要自己定義所有內容。
type
	timePrint = string(8);

function timeAmerican(ts: timeStamp): timePrint;
const
	hourMinuteSeparator = ':';
	anteMeridiemAbbreviation = 'AM';
	postMeridiemAbbreviation = 'PM';
type
	noonRelation = (beforeNoon, afterNoon);
	letterPair = string(2);
var
	{ contains 'AM' and 'PM' accessible via an index }
	m: array[noonRelation] of letterPair;
	{ contains a leading zero accessible via a Boolean expression }
	z: array[Boolean] of letterPair;
	{ holds temporary result }
	t: timePrint;
begin
	{ fill `t` with spaces }
	writeStr(t, '':t.capacity);

此回退值(在ts.timeValidfalse的情況下)允許此function的程式設計師/“使用者”“盲目”地列印其返回值。輸出中將會有一個明顯的空白。另一個合理的“回退”值是一個空的string

	with ts do
	begin
		if timeValid then
		begin
			m[beforeNoon] := anteMeridiemAbbreviation;
			m[afterNoon] := postMeridiemAbbreviation;
			z[false] := '';
			z[true] := '0';
			
			writeStr(t,
				((hour + 12 * ord(hour = 0) - 12 * ord(hour > 12)) mod 13):1,
				hourMinuteSeparator,
				z[minute < 10], minute:1, ' ',
				m[succ(beforeNoon, hour div 12)]);

這是這個問題中最複雜的部分。首先,所有傳遞給writeStr的數字引數都明確地:1作為最小寬度規範字尾,因為有一些編譯器會假設,例如,:20作為預設值。由於我們知道timeStamp.hour的範圍是0..23,我們可以使用divmod操作,如示例所示。但是,我們需要考慮hour值為0的情況,通常表示為 12:00 AM(而不是零)。使用所示的Boolean表示式和ord進行條件“偏移” 12 可以解決這個問題。此外,這裡簡要提醒一下,在EP 中,succ 函式接受第二個引數

		end;
	end;
	
	timeAmerican := t;
end;
最後,我們需要將臨時結果複製到函式結果變數中。請記住,必須只有一個賦值,儘管並非所有編譯器都強制執行此規則。

來源

  1. Wirth, Niklaus (1979). "The Module: a system structuring facility in high-level programming languages". proceedings of the symposium on language design and programming methodology. Berlin, Heidelberg: Springer. Abstract. doi:10.1007/3-540-09745-7_1. ISBN 978-3-540-09745-7. https://link.springer.com/content/pdf/10.1007%2F3-540-09745-7_1.pdf. Retrieved 2021-10-26. 
  2. Cooper, Doug. "Chapter 11. The record Type". Oh! Pascal! (third edition ed.). p. 374. ISBN 0-393-96077-3. […] records have two unique aspects:
    First, the stored values can have different types. This makes records potentially heterogeneous—composed of values of different kinds. Arrays, in contrast, hold values of just one type, so they're said to be homogeneous.
    […]
    {{cite book}}: |edition= has extra text (help); line feed character in |quote= at position 269 (help); syntaxhighlight stripmarker in |chapter= at position 17 (help)
  3. Wirth, Niklaus (1973-07-00). The Programming Language Pascal (Revised Report ed.). p. 30. Within the component statement of the with statement, the components (fields) of the record variable specified by the with clause can be denoted by their field identifier only, i.e. without preceding them with the denotation of the entire record variable. {{cite book}}: Check date values in: |date= (help)
  4. Jensen, Kathleen; Wirth, Niklaus. Pascal – user manual and report (4th revised ed.). p. 39. doi:10.1007/978-1-4612-4450-9. ISBN 978-0-387-97649-5. The initial and final values are evaluated only once.

筆記

  1. 這種record將無法儲存任何內容。在下一章中,您將學習它可能在唯一一個例項中有用。
  2. 實際上大多數編譯器將點視為解引用指示符,並且欄位名稱表示從基本記憶體地址的靜態偏移量。
  3. 在標準(“未擴充套件”)Pascal 中,ISO 標準 7185 中,function只能返回“簡單資料型別”和“指標資料型別”的值。
  4. 實際上,顯示的 begin end 是多餘的,因為 repeat until 本身就構成了一個框架。出於教學目的,我們建議您在通常出現語句序列的地方始終使用 begin end。否則,您可能會將 repeat until 迴圈 更改為 while do 迴圈忘記用適當的 begin end 框架包圍迴圈體語句。
  5. 為了簡便起見,省略了 packed 指示符。
  6. 根據大多數編譯器對 maxInt 的定義。ISO 標準只要求所有在 -maxInt..maxInt 範圍內的算術運算都能完全正確地工作,但理論上(雖然不太可能)支援更多值。
下一頁: 指標 | 上一頁: 字串
主頁: Pascal 程式設計
華夏公益教科書