跳转到内容

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 编程
华夏公益教科书