跳轉到內容

Pascal 程式設計/檔案

來自華夏公益教科書,開放的書籍,開放的世界

您是否曾經想過如何處理大量資料?檔案是 Pascal 中的解決方案。您已經在輸入和輸出一章中瞭解了一些基礎知識。在這裡,我們將詳細介紹 ISO 標準 7185 “Pascal” 中定義的更多細節。“擴充套件 Pascal” ISO 標準 10206 定義了更多功能,但這些將在本華夏公益教科書第二部分中介紹。

檔案資料型別

[編輯 | 編輯原始碼]

到目前為止,我們只處理文字檔案, 具有資料型別 text 的檔案,但還有更多檔案型別。

從數學角度講,檔案是有界的有限序列。這意味著,

  • 元件沿軸(序列)方向排列,
  • 元件值從一個域(有界)中選擇,並且
  • 存在一定數量的元件(有限)。

用數學符號表示

在 Pascal 中,我們可以透過指定 file of recordType 來宣告檔案資料型別,其中 recordType 需要是有效的記錄資料型別。允許的記錄資料型別可以是任何資料型別,除了另一個檔案資料型別(包括 text)或包含這種資料型別的型別。這意味著 array 檔案資料型別,或者 record 具有 file 作為元件是不允許的。讓我們看一個例子

program fileDemo(output);

type
	integerFile = file of integer;

使用資料型別為 integerFile 的變數,我們可以訪問僅包含一種型別資料的檔案,即 integer 值(域限制)。

var
	temperatures: integerFile;
	i: integer;

注意,變數 temperatures 本身不是檔案。這個 Pascal 變數只是為我們提供了抽象的“控制代碼”,它允許我們(即 program)獲得實際檔案的控制權(如 § 概念 中所述)。

所有檔案都有當前模式。在宣告檔案變數時,此模式通常是未定義的。在 ISO 標準 7185 定義的標準 Pascal 中,您可以從生成模式或檢查模式中選擇。

生成模式

[編輯 | 編輯原始碼]

為了寫入檔案,您需要呼叫名為 rewrite 的標準內建 procedureRewrite 將嘗試從頭開始開啟檔案以供寫入。

begin
	rewrite(temperatures);

file 立即變為空,因此得名rewrite。擴充套件 Pascal 還有非破壞性的 procedure extend

只有在成功開啟檔案以供寫入後,所有寫入例程才合法。嘗試寫入未開啟以供寫入的檔案將構成致命錯誤。

	write(temperatures, 70);
	write(temperatures, 74);

write 之後的所有引數都 destination(這裡為 temperatures必須destination 檔案的 recordType。必須至少有一個。只有當 destination 是一個 text 檔案時,才允許使用各種內建資料型別。

請注意,過程 writeLn(和 readLn)只能應用於 text 檔案。其他檔案不“瞭解”行的概念,因此 Ln 過程不能應用於它們。

檢查模式

[編輯 | 編輯原始碼]

為了讀取檔案,您需要呼叫名為 reset 的標準內建 procedureReset 將嘗試從頭開始開啟檔案以供讀取。

	reset(temperatures);
	while not EOF(temperatures) do
	begin
		read(temperatures, i);
		writeLn(i);
	end;
end.

請注意,在 reset(temperatures) 之後,您不能再向該檔案寫入任何內容。模式是排他的:您要麼寫入,要麼讀取。 [fn 1]

一個file最明顯、最顯著的“優勢”可能是:與array不同,我們不需要在原始碼中預先指定大小。file可以根據需要變得任意大。然而,array可以使用:=賦值來進行複製。整個檔案無法透過這種方式複製。

一個file的主要“劣勢”可能是:訪問只能是順序的。我們必須從開始讀取和寫入file。如果我們想要獲得,比如第 94 個記錄,我們需要提前 93 次,並且還要考慮到可能存在少於 94 個記錄的情況。[fn 2]

詞語“優勢”和“劣勢”用引號括起來,因為程式語言無法判斷/評定什麼是“更好”或“更差”。評估任務屬於程式設計師。檔案特別適合於長度不可預測的I/O,例如使用者輸入。

原始例程

[edit | edit source]

到目前為止,我們只使用過read/readLnwrite/writeLn。這些過程很方便,非常適合日常使用。但是,Pascal 也允許您對檔案進行相對“低階”訪問,getput

緩衝區

[edit | edit source]

每個檔案變數都與一個緩衝區相關聯。緩衝區是一個臨時儲存空間。您從file中讀取和寫入的所有內容都會透過此儲存空間,然後實際的讀取或寫入操作才會傳達給OS[fn 3] 緩衝I/O是為了效能原因而選擇的。

在 Pascal 中,我們可以透過將附加到變數名來訪問緩衝區的一個“當前”元件,就像它是一個指標一樣。這個解引用值的 資料型別是recordType,就像我們在宣告中一樣。所以,如果我們有

var
	foobar: file of Boolean;

表示式foobar的 資料型別是Boolean

為了將所有內容聯絡起來,讓我們看看一個圖表。這個圖表是為了理解,並且展示了一個非常具體的情況。關注關係

上半部分屬於OS的管轄範圍。下半部分屬於(我們)的program的管轄範圍。檔案的資料,這裡是一系列總共 16 個integer值,OS管理。任何對資料的訪問都透過OS進行。直接讀取或寫入是不可能的。我們請求OS將前 4 個integer資料值複製到我們的緩衝區中。我們這樣做是因為,與單獨複製 4 個整數相比,一次性將它們複製在一起會更快。[fn 4]

滑動視窗

[edit | edit source]

三個不同的儲存位置——實際資料檔案、內部緩衝區和緩衝區變數——共同為我們提供了檔案的“檢視”。如果我們將所有包含相同資訊的內容疊加在一起,我們將得到以下影像

這裡,第二組整數被載入到內部緩衝區(綠色背景)。檔案緩衝區指向內部緩衝區的第二個元件。這用整個檔案第六個元件上的藍灰色陰影來表示。其他所有內容都被陰影覆蓋,這意味著我們只能檢視和操作第六個元件。

前進視窗

[edit | edit source]

這個滑動視窗可以使用getput例程(向右方向,即EOF方向)前進。這兩個例程都會將檔案緩衝區向前移動,使其指向內部緩衝區的下一個專案。一旦內部緩衝區被完全處理,下一批元件就會被載入或儲存。呼叫get只有在檔案處於檢查模式時才合法;類似地,put只有在檔案處於生成模式時才合法。

使用視窗

[edit | edit source]

Getput接受一個非可選引數,一個file(或text)變數。Put獲取緩衝區變數的當前內容,並確保將其寫入實際檔案。讓我們看看它是如何運作的。考慮以下program

program getPutDemo(output);
type
	realFile = file of real;
var
	score: realFile;
begin

下表右欄顯示了score的狀態,包括內容以及滑動視窗所在的位置(藍色背景)。

原始碼 成功操作後的狀態
	rewrite(score);
N/A
🠅
	score^ := 97.75;
97.75
🠅
	put(score);
97.75 N/A
🠅
	score^ := 98.38;
97.75 98.38
🠅
	put(score);
97.75 98.38 N/A
🠅
	score^ := 100.00
97.75 98.38 100.00
🠅
	{ For demonstration purposes: no `put(score)` here. }
97.75 98.38 100.00
🠅

現在,讓我們列印我們剛用一些real值填充的檔案score。為了改變一下,我們使用get。與read/readLn一樣,get只允許在不為EOF時使用

	reset(score);
	while not EOF(score) do
	begin
		writeLn(score^);
		get(score);
	end;
end.

請注意,這隻會列印兩個real

 9.775000000000000E+01
 9.838000000000000E+01

第三個real值,儘管已定義,但沒有透過相應的put(score)寫入

要求

[edit | edit source]

如上所述,get 只能在指定檔案處於檢查模式時呼叫,而 put 只能在檔案處於生成模式時呼叫。更具體地說,呼叫 get(F) 僅在 EOF(F)false 時才允許,而呼叫 put(F) 僅在 EOF(F)true 時才允許。換句話說,禁止讀取超過 EOF 的內容,而寫入必須發生在 EOF 處。

成功呼叫 rewrite(F)(或 EP procedure extend(F))後,EOF(F) 的值變為 true。任何隨後的 put(F) 不會改變此值。呼叫 reset(F) 後,EOF(F) 的值取決於給定檔案是否為空。任何隨後的 get(F) 可能會將此值從 false 更改為 true(永遠不會反過來)。

Text 緩衝區

[edit | edit source]

text 的緩衝區值具有一些特殊行為。text 檔案本質上file of char。本章介紹的所有內容都可以應用於 text 檔案,就好像它是一個 file of char 一樣。但是,正如反覆強調的那樣,text 檔案被結構化為,每行由一個(可能為空)char 值序列組成。

EOLn(input) 變為 true 時,緩衝區變數 input 返回一個空格字元 (' ')。因此,在使用緩衝區變數時,區分空格字元作為行的一部分以及空格字元作為行終止符的唯一方法是呼叫函式 EOLn

原因:各種作業系統使用不同的方法來標記行尾。它必須以某種方式標記,因為這些資訊無法憑空憑空推斷出來。但是,那裡有多種策略。這對程式設計師來說很不方便,因為他們無法考慮所有內容。因此,Pascal 選擇了,無論使用哪種具體的 EOL 標記,緩衝區變數都在行尾包含一個簡單的空格字元。這是可預測的,而可預測的行為是好的。

目的

[edit | edit source]

值得注意的是,read/readLnwrite/writeLn 的所有功能本質上都可以基於 getput。以下是一些基本關係

如果 f 指向 file of recordType 變數,而 x 是一個 recordType 變數,read(f, x) 等效於

	x := f^;
	get(f);

類似地,write(f, x) 等效於

	f^ := x;
	put(f);

對於 text 變數,關係並不那麼直接。行為取決於各種目標/源變數的資料型別。但是,一個簡單的關係是,如果 f 指向 text 變數,則 readLn(f) 等效於

	while not EOLn(f) do
	begin
		get(f);
	end;
	get(f);

後者的 get(f) 實際上“消耗”了換行符。

支援

[edit | edit source]

不幸的是,在 開篇 中介紹的編譯器中,Delphi 和 FPC 不支援所有 ISO 7185 功能。

  • Delphi 和 FPC 要求在執行任何操作之前將檔案明確地與檔名關聯。需要通過後臺記憶體(例如,磁碟)中的檔案來支援任何型別的 file。這將在這本書的第二部分中解釋,因為 ISO 標準 10206“擴充套件 Pascal”也為此定義了一些方法。
  • FPC{$mode ISO}{$mode extendedPascal} 中提供了過程 getput,以及檔案變數緩衝區。Delphi 完全不支援這一點。

請放心,如果您使用的是 GPC,一切都會正常工作。作者無法就 Pascal‑P 編譯器做出任何宣告,因為他們沒有對其進行測試。

任務

[edit | edit source]
當相應檔案處於檢查模式時,您可以向緩衝區變數寫入嗎?換句話說,當檔案處於檢查模式時,緩衝區變量出現在賦值語句的LHS上是否合法?
緩衝區變數顧名思義是一個變數。您可以讀取和寫入它,而與當前模式無關。但是,只有在file變數初始化後才會建立緩衝區。這意味著必須透過呼叫resetrewrite來選擇模式。將reset/rewrite視為一種特殊的new,並將檔案變數視為指標。您只能在先前定義了指標的情況下對其進行解引用(=附加
緩衝區變數顧名思義是一個變數。您可以讀取和寫入它,而與當前模式無關。但是,只有在file變數初始化後才會建立緩衝區。這意味著必須透過呼叫resetrewrite來選擇模式。將reset/rewrite視為一種特殊的new,並將檔案變數視為指標。您只能在先前定義了指標的情況下對其進行解引用(=附加


編寫一個過濾器program,將重複的空格字元' '合併為單個空格字元。(過濾器程式的意思是,根據給定輸入處理input並寫入output,應用指定的規則。)額外獎勵:編寫一個不宣告任何額外變數的解決方案(即沒有var部分)。
一個可接受的解決方案是
program mergeRepeatingSpace(input, output);
const
	{ Choose any character, but ' ' (a single space). }
	nonSpaceCharacter = 'X';
begin
	output^ := nonSpaceCharacter;
	
	while not EOF do
	begin

由於當我們處於EOL時,input包含空格字元,因此發出新行的唯一正確方法是使用writeLnWriteLn不使用緩衝區變數。換句話說,output現在可能包含任何值。

		if EOLn then
		begin
			writeLn;

在這個if語句的分支中,input包含空格字元。但是,此空格字元例項不應觸發重複空格字元檢測。因此,我們將非空格字元分配給output(現在充當“前一個字元變數”)。

			output^ := nonSpaceCharacter;
		end
		else
		begin
			if [output^, input^] <> [' '] then

在使用string/char連線運算子+的擴充套件 Pascal 中,您可以編寫

			if output^ + input^ <> '' then

請記住,簡單的=比較使用空格字元將兩個運算元填充到相同的長度。

			begin
				write(input^);
			end;
			
			output^ := input^;
			{ The buffer variable (`output↑`) now contains the previous character. }
		end;
		
		get(input);
	end;
end.
一個更容易的實現可能使用一個Boolean變數作為標誌,指示前一個字元是否是非換行空格字元。
一個可接受的解決方案是
program mergeRepeatingSpace(input, output);
const
	{ Choose any character, but ' ' (a single space). }
	nonSpaceCharacter = 'X';
begin
	output^ := nonSpaceCharacter;
	
	while not EOF do
	begin

由於當我們處於EOL時,input包含空格字元,因此發出新行的唯一正確方法是使用writeLnWriteLn不使用緩衝區變數。換句話說,output現在可能包含任何值。

		if EOLn then
		begin
			writeLn;

在這個if語句的分支中,input包含空格字元。但是,此空格字元例項不應觸發重複空格字元檢測。因此,我們將非空格字元分配給output(現在充當“前一個字元變數”)。

			output^ := nonSpaceCharacter;
		end
		else
		begin
			if [output^, input^] <> [' '] then

在使用string/char連線運算子+的擴充套件 Pascal 中,您可以編寫

			if output^ + input^ <> '' then

請記住,簡單的=比較使用空格字元將兩個運算元填充到相同的長度。

			begin
				write(input^);
			end;
			
			output^ := input^;
			{ The buffer variable (`output↑`) now contains the previous character. }
		end;
		
		get(input);
	end;
end.
一個更容易的實現可能使用一個Boolean變數作為標誌,指示前一個字元是否是非換行空格字元。


編寫一個program,從input讀取資料,並最後一個輸入char值寫入output。在標準的 Linux 或 FreeBSD 系統上,您可以使用命令列echo -n '123H' | ./printLastCharacter測試您的program‑n選項標誌很重要。否則,您的program可能只會顯示單個空格(' ')字元。或者,您可以使用printf '123H' | ./printLastCharacter。無論哪種變體,您的program都應該寫入一行,包含單個字元H
一個可接受的解決方案可能如下所示
program printLastCharacter(input, output);
begin
	{ We cannot output anything, unless there is at least one character. }
	if not EOF(input) then
	begin
		while not EOF(input) do
		begin
			{ After `get(input)`, `input↑` becomes undefined once
			  we reach `EOF(input)`. Therefore copy it beforehand. }
			output^ := input^;
			get(input);
		end;
		put(output);
		writeLn(output);
	end;
end.

透過在program引數列表中指定inputreset的後斷言變為真。這意味著,在我們的第二行僅在那之後,begin中存在隱式(=不可見)的get(input)EOF(input)的值才會被定義。如果您碰巧擁有支援擴充套件 Pascal 的haltprocedure的編譯器,您就可以消除一個縮排級別

	{ We cannot output anything, unless there is at least one character. }
	if EOF(input) then
	begin
		halt;
	end;
	
	while not EOF(input) do
一般來說,程式設計師喜歡避免縮排級別,因為它可能表明複雜性。另一方面,如果您認為這種編碼風格“更復雜”也是完全合法的。
一個可接受的解決方案可能如下所示
program printLastCharacter(input, output);
begin
	{ We cannot output anything, unless there is at least one character. }
	if not EOF(input) then
	begin
		while not EOF(input) do
		begin
			{ After `get(input)`, `input↑` becomes undefined once
			  we reach `EOF(input)`. Therefore copy it beforehand. }
			output^ := input^;
			get(input);
		end;
		put(output);
		writeLn(output);
	end;
end.

透過在program引數列表中指定inputreset的後斷言變為真。這意味著,在我們的第二行僅在那之後,begin中存在隱式(=不可見)的get(input)EOF(input)的值才會被定義。如果您碰巧擁有支援擴充套件 Pascal 的haltprocedure的編譯器,您就可以消除一個縮排級別

	{ We cannot output anything, unless there is at least one character. }
	if EOF(input) then
	begin
		halt;
	end;
	
	while not EOF(input) do
一般來說,程式設計師喜歡避免縮排級別,因為它可能表明複雜性。另一方面,如果您認為這種編碼風格“更復雜”也是完全合法的。

註釋

  1. ISO標準 10206 定義的擴充套件 Pascal 還允許更新模式,即同時讀寫,但這隻適用於“直接訪問檔案”(索引檔案)。
  2. 擴充套件 Pascal,ISO知道“直接訪問檔案”。這種檔案型別允許以簡單快捷的方式訪問第 94 個記錄,但它不能根據需要“增長”。
  3. 這是一個實現細節,而不是程式語言強加的要求。即使僅僅存在一個OS也超出了 Pascal 的範圍。儘管如此,這種描述是一個常見的方案。
  4. 當然,這是在假設我們要使用它們的情況下。不必要地複製以後不會用到的資料是浪費計算時間。
下一頁: 範圍 | 上一頁: 指標
主頁: Pascal 程式設計
華夏公益教科書