跳转到内容

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