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
的标准内置 procedure
。 Rewrite
将尝试从头开始打开文件以供写入。
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
的标准内置 procedure
。 Reset
将尝试从头开始打开文件以供读取。
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
/readLn
和write
/writeLn
。这些过程很方便,非常适合日常使用。但是,Pascal 也允许您对文件进行相对“低级”访问,get
和put
。
缓冲区
[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]这个滑动窗口可以使用get
和put
例程(向右方向,即EOF方向)前进。这两个例程都会将文件缓冲区向前移动,使其指向内部缓冲区的下一个项目。一旦内部缓冲区被完全处理,下一批组件就会被加载或存储。调用get
只有在文件处于检查模式时才合法;类似地,put
只有在文件处于生成模式时才合法。
使用窗口
[edit | edit source]Get
和put
接受一个非可选参数,一个file
(或text
)变量。Put
获取缓冲区变量的当前内容,并确保将其写入实际文件。让我们看看它是如何运作的。考虑以下program
program getPutDemo(output);
type
realFile = file of real;
var
score: realFile;
begin
下表右栏显示了score
的状态,包括内容以及滑动窗口所在的位置(蓝色背景)。
源代码 | 成功操作后的状态 | ||||||
---|---|---|---|---|---|---|---|
rewrite(score);
|
| ||||||
score^ := 97.75;
|
| ||||||
put(score);
|
| ||||||
score^ := 98.38;
|
| ||||||
put(score);
|
| ||||||
score^ := 100.00
|
| ||||||
{ For demonstration purposes: no `put(score)` here. }
|
|
现在,让我们打印我们刚用一些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
(永远不会反过来)。
众所周知,禁止读取之前未定义的变量(即,必须事先分配一个值)。由于涉及读取缓冲区值,因此只有在缓冲区先前已定义的情况下才允许写入缓冲区。考虑以下错误代码片段 temperatures^ := 88;
put(temperatures); { ✔ Good. Will successfully write 88. }
put(temperatures); { ↯ Bad. temperatures^ is not defined. }
put(temperatures); { ↯ temperatures^ still not defined. }
get 和 put 会推进滑动窗口。只有第一个 put(temperatures) 读取已定义的值 temperatures^ 。但是,下一个和后续的 put(temperatures) 将读取未定义的 temperatures↑ 。 |
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
/readLn
和 write
/writeLn
的所有功能本质上都可以基于 get
和 put
。以下是一些基本关系
如果 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}
中提供了过程get
和put
,以及文件变量缓冲区。Delphi 完全不支持这一点。
请放心,如果您使用的是 GPC,一切都会正常工作。作者无法就 Pascal‑P 编译器做出任何声明,因为他们没有对其进行测试。
任务
[edit | edit source]file
变量初始化后才会创建缓冲区。这意味着必须通过调用reset
或rewrite
来选择模式。将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↑
包含空格字符,因此发出新行的唯一正确方法是使用writeLn
。WriteLn
不使用缓冲区变量。换句话说,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
参数列表中指定input
,reset
的后断言变为真。这意味着,在我们的第二行和仅在那之后,begin
中存在隐式(=不可见)的get(input)
,EOF(input)
的值才会被定义。如果您碰巧拥有支持扩展 Pascal 的halt
procedure
的编译器,您就可以消除一个缩进级别
{ We cannot output anything, unless there is at least one character. }
if EOF(input) then
begin
halt;
end;
while not EOF(input) do