跳转到内容

Pascal 编程/数组

来自维基教科书,开放的书籍,为开放的世界

数组是用于自定义数据类型的结构概念。它将相同数据类型的元素分组在一起。如果您处理大量相同数据类型的数据,您将经常使用数组。

一般来说,数组是有限的且排列好的对象集合,所有对象都具有相同的数据类型,称为基本类型组件类型[1] 数组至少有一个离散的、有界的维度,连续地枚举所有其对象。每个对象都可以通过一个或多个标量值(称为索引)沿这些维度唯一标识。

在 Pascal 中,数组数据类型使用保留字 array 结合辅助保留字 of 来声明,后跟数组的基本类型

program arrayDemo(output);
type
	dimension = 1..5;
	integerList = array[dimension] of integer;

保留字 array 后面跟着一个非空的用逗号分隔的维度列表,用方括号括起来。[fn 1] 所有数组的维度必须是序数数据类型,但基本类型可以是任何类型。如果数组只有一个维度,比如上面的数组,我们也可以称它为列表

如上所述,数据类型 integerList 的变量包含五个独立的 integer 值。访问它们遵循特定的方案

var
	powerN: integerList;
begin
	powerN[1] := 5;
	powerN[2] := 25;
	powerN[3] := 125;
	powerN[4] := 625;
	powerN[5] := 3125;
	
	writeLn(powerN[3]);
end.

这个 program 将打印 125,因为它是 powerN 的值,其索引值为 3。数组就像一系列“桶”,每个桶都包含一个基本数据类型的值。每个桶都可以通过根据维度规范的值来标识。当引用其中一个桶时,我们必须命名该组,即变量名(在本例中为 powerN),以及用方括号括起来的适当索引。

字符数组

[编辑 | 编辑源代码]

字符列表通常在 I/O 和操作方面具有并曾经具有特殊支持。本节主要是关于理解,因为在下一章中,我们将了解一个更复杂的数据类型,称为 string

直接赋值

[编辑 | 编辑源代码]

可以使用赋值语句将字符串字面量分配给 array[] of char 变量,因此,您可以使用

program stringAssignmentDemo;
type
	fileReferenceDimension = 1..4;
	fileReference = array[fileReferenceDimension] of char;
var
	currentFileReference: fileReference;
begin
	currentFileReference[1] := 'A';
	currentFileReference[2] := 'Z';
	currentFileReference[3] := '0';
	currentFileReference[4] := '9';

您可以简单地写

	currentFileReference := 'AZ09';
end.

请注意,您不再需要指定任何索引。您正在引用赋值符号(:=LHS 上的整个数组变量。这仅适用于覆盖整个数组的值。确实有一些扩展允许您仅覆盖 char 数组的一部分,但关于这些将在下一章中详细介绍。

大多数将字符串字面量分配给 char 数组的实现将填充给定的字符串字面量,如果它小于变量的容量,则使用不重要的 char 值。填充字符串意味着用其他字符填充剩余的位置以满足特定的大小。GPC 使用空格字符(' '),而FPC 使用 char 值,其 ord 数值为零(#0)。

读取和打印

[编辑 | 编辑源代码]

虽然没有标准化,[fn 2] read/readLnwrite/writeLn 通常支持写入和读取 array[] of char 变量。

非常早期的 Pascal 编译器支持这种人类可读的 IO

代码:

program interactiveGreeting(input, output);
type
	nameCharacterIndex = 1..20;
	name = array[nameCharacterIndex] of char;
var
	host, user: name;
begin
	host := 'linuxnotebook'; { in lieu of getHostName }
	writeLn('Hello! What is your name?');
	readLn(user);
	
	write('Hello, ', user, '. ');
	writeLn('My name is ', host, '.');
end.

输出:

Hello! What is your name?
Aïssata
Hello, Aïssata. My name is linuxnotebook.
注意,显示将根据上面解释的填充算法 解释 以及较长的输入将被静默地裁剪为 user 的最大容量。用户输入已突出显示,并且 program 是使用 FPC 编译的。

这是因为 text 文件,如标准文件 inputoutput,被理解为无限的 char 值序列。[fn 3] 由于我们的 array[] of char 也是有限的 char 值序列,因此单个值可以非常直接地复制到和从 text 文件,不需要任何类型的转换。

基本比较

[编辑 | 编辑源代码]

与其他数组不同,array[] of char 变量可以使用 =<> 进行比较。

	if user <> host then
	begin
		write('Hello, ', user, '. ');
		writeLn('My name is ', host, '.');
	end
	else
	begin
		writeLn('No. That is my name.');	
	end;

这种比较只对相同的数据类型有效。它是一种基本的逐字符比较。如果任何一个数组更长或更短,则 = 比较将始终失败,因为并非所有字符都可以相互比较。相应地,<> 比较对于长度不同的 array[] of char 值将始终成功。

注意,EP 标准还定义了 EQNE 函数,以及更多其他函数。与 =<> 的区别在于,空白填充(即 FPC 中的 #0 或 GPC 中的 ' ')在 EQNE 中没有意义。因此,这意味着使用这些函数,您可以比较字符串和 char 数组,无论其各自的最大容量如何,仍然可以获得自然预期的结果。另一方面,=<> 比较会查看内存的内部表示。

数组的基本类型可以是任何数据类型,[fn 4] 甚至是另一个数组。如果我们想声明一个“数组的数组”,有一个简短的语法:

program matrixDemo(output);
const
	columnMinimum = -30;
	columnMaximum =  30;
	rowMaximum =  10;
	rowMinimum = -10;
type
	columnIndex = columnMinimum..columnMaximum;
	rowIndex = rowMinimum..rowMaximum;
	plot = array[rowIndex, columnIndex] of char;

这已经在上面描述了 描述。最后一行与以下相同:

	plot = array[rowIndex] of array[columnIndex] of char;

它可以扩展为两个独立的声明,使我们能够“重复使用” “内部” 数组数据类型

	row = array[columnIndex] of char;
	plot = array[rowIndex] of row;

注意,在后一种情况下,plot 使用 row 作为基本类型,它本身就是一个数组。然而,在简短的符号中,我们指定 char 作为基本类型,而不是 row,而是基本类型。

当一个数组被声明为包含另一个数组时,也有一个简短的符号来访问单个数组项。

var
	curve: plot;
	x: columnIndex;
	y: rowIndex;
	v: integer;
begin
	// initialize
	for y := rowMinimum to rowMaximum do
	begin
		for x := columnMinimum to columnMaximum do
		begin
			curve[y, x] := ' ';
		end;
	end;
	
	// graph something
	for x := columnMinimum to columnMaximum do
	begin
		v := abs(x) - rowMaximum;
		
		if (v >= rowMinimum) and (v <= rowMaximum) then
		begin
			curve[v, x] := '*';
		end;
	end;

这对应于数组的声明。务必确保您指定的索引确实是有效的。在后一个循环中,if 分支会检查这一点。尝试访问不存在的数组值,即通过提供非法索引,可能会导致程序崩溃,或者更糟糕的是,无法检测到错误,从而导致难以发现的错误。

Note 使用 GPC 编译的程序将以以下方式终止:
./a.out: value out of range (error #300 at 402a76)
如果您尝试访问不存在的数组项(最终的十六进制数可能会有所不同)。但是,FPC 默认情况下会忽略这一点。您需要专门请求检测错误,方法是指定 ‑Cr 命令行开关,或者在源代码中放置编译器指令注释:
{$rangeChecks on}
在您的源代码中(在您访问任何数组之前,在您的源代码中写一次就足够了)。

另请参阅 第“枚举”章,小节“限制”

注意,xy 的“不寻常”顺序是为了方便绘制直立的图形。

	// print graph, note reverse `downto` direction
	for y := rowMaximum downto rowMinimum do
	begin
		writeLn(curve[y]);
	end;
end.

这意味着,仍然可以将整个“子”数组作为一个整体引用。不必写出数组值的所有维度,只要有意义就行。

具有正好两个维度的数组数据类型也称为矩阵,单数为矩阵

Note 在数学中,一个矩阵不必一定是同构的,但可以包含不同的“数据类型”。

第一章中介绍的,数据类型 real 是 Pascal 编程语言的一部分。它用于存储整数,并可选择包含小数部分。

实数字面量

[编辑 | 编辑源代码]

为了区分 integer 字面量和 real 字面量,在源代码中指定 real 值(以及对于 read/readLn)略有不同。以下源代码片段显示了一些 real 字面量

program realDemo;
var
	x: real;
begin
	x := 1.61803398;
	x := 1E9;
	x := 500e-12
	x := -1.7320508;
	x := +0.0;
end.

总结规则

  • 一个 real 值字面量始终包含一个 . 作为基数标记,或者一个 E/e 来分隔指数( in ),或者两者都有。
  • .(如果有)之前至少有一个西阿拉伯数字,之后也有一个西阿拉伯数字。
  • 整个数字和指数之前都有符号,但是符号是可选的。
Note 0.0 接受符号,但实际上(在数学上)是一个无符号数。 -0.0+0.0 表示相同的值。

和以往一样,所有数字值都不能包含空格。

有效使用real 数据类型时,您需要了解它的一些限制。首先,我们要再次强调一个在介绍数据类型时提到的问题:在计算中,real 变量只能存储有理数 (ℚ) 的一个子集[2] 这意味着,例如,您无法存储(数学意义上的)实数 (ℝ) 。这个数字是一个无理数(即不是有理数)。如果您无法将一个数字写成一个有限的real 文本,那么在使用有限内存的系统中,例如计算机系统,无法存储它。

幸运的是,在 EP 中,三个常量可以帮助您使用real 值。 minReal 是最小的正real 值。它与常量maxReal一起,保证了 中的所有算术运算都会产生“合理的”近似值。[3] 并没有明确规定“合理”近似值的具体含义,因此它可能意味着,例如,maxReal - 1 作为“近似值”会产生 maxReal[fn 5] 此外,real 变量可能存储大于maxReal的值。

epsReal 是“epsilon real”的缩写。希腊字母 ε(epsilon)在数学中通常表示一个无穷小的(正)值,但为零。根据 ISO 标准 10206(“扩展 Pascal”),epsReal 是从大于1.0最小值中减去 1.0的结果。[3] 在这个值和 1.0 之间,没有其他值可以表示,因此 epsReal 代表可用的最高精度,但仅限于点。[fn 6] real 数据类型的大多数实现将显示出显著变化的精度。最值得注意的是,符合 IEEE 标准 754 格式的 real 数据类型实现的精度会向极端值衰减,当接近(并超过)-maxRealmaxReal 时。因此,通常情况下,您使用,如果有的话,一个合理的 epsReal 倍数,以适应给定的情况。

转换函数

[编辑 | 编辑源代码]

Pascal 的强类型系统阻止您将real 值分配给integer 变量。real 值可能包含integer 变量无法存储的小数部分。

Pascal 作为语言的一部分,定义了两个标准函数,以一种定义明确的方式解决这个问题。

  • 函数trunc,是“截断”的缩写,它会简单地丢弃任何小数部分,并作为integer 返回整数部分。因此,这实际上是将一个数字向零取整。 trunc(-1.999) 将返回 -1 的值。
  • 如果您觉得这“不自然”,round 函数会将real 数字四舍五入到最接近的integer 值。 round(x) (关于结果值)等效于trunc(x + 0.5)(对于非负值),并等效于trunc(x - 0.5)(对于负 x 值)。[fn 7] 换句话说,这就是您可能在小学或家庭教育中学习到的第一个取整方法。它通常被称为“商业取整”。

如果不存在满足函数各自定义的integer 值,这两个函数都将失败。

如果您想(明确地)将一个integer 值“转换”为一个real 值,没有函数可以实现。相反,人们使用算术中性运算

  • integerValue * 1.0(使用乘法中性元素),或
  • integerValue + 0.0(使用加法中性元素)

这些表达式利用了这样一个事实,正如之前在关于表达式的一章中作为旁注提到的那样,表达式的整体数据类型将在涉及到一个real 值时变为real[4]

Note 保证所有可能的integer 值都可以存储为real 变量。[fn 8] 这主要涉及非小值,但重要的是要理解,integer 数据类型存储整数、整数值的最佳选择。

打印real

[编辑 | 编辑源代码]

默认情况下,write/writeLn 使用“科学计数法”打印real 值。

Borland Pascal 定义了一个 real 值常量 pi。它被许多编译器采用。

代码:

program printPi(output);
begin
	writeLn(pi);
end.

输出:

 3.141592653589793e+00
此输出是由使用 GPC 编译的程序生成的(不强制 ISO 标准兼容性)。宽度可能会有所不同,但总体样式
  1. 符号,其中正号用空格代替
  2. 一位数字
  3. 一个点
  4. 一个正数的十进制后数字
  5. 一个 E(大写或小写)
  6. 指数的符号,但这次始终写正号,零指数前也加正号
  7. 指数值,具有固定的最小宽度(此处为 2)和前导零
始终相同。

虽然这种样式非常通用,但对于某些读者来说也可能不寻常。特别是 E 表示法现在相当古老,通常只在袖珍计算器上看到,即缺少足够显示空间的设备。

幸运的是,write/writeLn 允许我们自定义显示样式。

首先,指定最小总宽度real 参数也是合法的,但这也会显示更多数字

代码:

program printPiDigits(output);
begin
	writeLn(pi:40);
end.

输出:

 3.141592653589793238512808959406186e+00
请注意,40 指的是整个宽度,包括符号、基数标记、e 和指数表示。

对于 real 类型的值(对于 real 值),过程 write/writeLn 接受另一个冒号分隔的格式说明符。第二个数字确定十进制后数字(精确)的数量,“小数部分”。提供两个格式说明符也会禁用科学计数法。所有 real 值都使用常规位置表示法打印。这可能意味着对于诸如 1e100 的“大”数字,打印一个后跟一百个零(仅针对整数部分)。

让我们看一个例子

代码:

program realFormat(output);
var
	x: real;
begin
	x := 248e9 + 500e-6;
	writeLn(x:32:8);
end.

输出:

           248000000000.00048828
请注意,整个宽度,包括 . 和负数的情况下的 -,是 32 个字符。在 . 之后有 8 个数字。请记住精确的数字,尤其是小数部分,可能会有所不同。

在某些地区和语言中,习惯上使用 ,(逗号)或其他字符而不是点作为基数标记。Pascal 的内置 write/writeLn 过程,另一方面,始终会打印一个点,并且为此事 – read/readLn 始终只接受点作为基数标记。然而,所有当前的 Pascal 编程套件,Delphi、FPCGPC 都提供适当的实用程序来克服此限制。有关更多详细信息,请参阅其手册。此问题不应阻止我们继续学习 Pascal。

输出 write/writeLn(以及在 EP 中的 writeStr)生成的将相对于最后一个打印的数字进行舍入

代码:

program roundedWriteDemo(output);
begin
	writeLn(3.75:4:1);
end.

输出:

 3.8
请注意,这种舍入必须与 real 的限制 产生的近似值区分开来。经验证实,用于此演示的计算机确实可以精确存储指定的数值。您在此特定情况下看到的舍入不是由于任何技术原因。

比较

[edit | edit source]

首先,所有(算术)比较运算符都适用于 real 值。然而,运算符 =<> 处理起来尤其棘手。

在大多数应用程序中,在检查相等性或不等性时,您real 值相互比较。问题是,诸如 ⅓ 之类的数字无法用有限的一系列二进制数字精确存储,只能近似,但对于 ⅓ 来说并非只有一个有效的近似值,而是许多合法的近似值。但是,=<> 运算符进行比较 – 可以这么说 – 特定位模式。这通常不是期望的(对于无法精确表示的值)。相反,您希望确保要比较的值在某个范围内,例如

function equal(x, y, delta: real): Boolean;
begin
	equal := abs(x - y) <= delta;
end;

Delphi 和 FPC 的标准 RTS 将函数 sameValue 作为 math 单元 的一部分提供。您不希望编写其他人已经编写的东西,即使用这些资源。

除法

[edit | edit source]

既然我们知道用于存储(有理数的子集)的数据类型,在 Pascal 中称为 real,我们就可以执行并使用另一个算术运算的结果:除法。

风格

[edit | edit source]

Pascal 定义了两个不同的除法运算符

  • div 运算符执行整数除法,并丢弃(如果适用)任何余数。表达式的结果数据类型始终为 integerdiv 运算符仅在两个操作数都是 integer 表达式时才有效。
  • 可能更熟悉的运算符 /(斜杠),也将 LHS 数字(被除数)除以 RHS 数字(除数),但 / 除法始终产生 real 类型的值。[4] 即使小数部分为零也是如此。“余数”对于 / 运算不存在。

div 运算可以用 / 表示

divisor div dividend = trunc(divisor / dividend)

但是,这只是一个语义等效物,[fn 9]不是实际计算的方式。原因是,/ 运算符将首先将两个操作数转换为 real 值,并且由于,如上所述,并非所有 integer 值都能精确地表示为 real 值,这将产生可能遭受舍入误差的结果。另一方面,div 运算符是一个纯 integer 运算符,并使用“整数精度”,这意味着实际上计算 div 结果时没有舍入参与。

除数限制

[edit | edit source]

注意,由于没有普遍接受的零除定义,零除算式是非法的,会导致程序中止。如果除数不是常数,而是依赖于运行时数据(如从用户输入读取的变量),则应该在进行除法之前检查它是否为非零值。

if divisor <> 0 then
begin
	x := dividend / divisor;
	// ...
end
else
begin
	writeLn('Error: zero divisor encountered when calculating rainbow');
end;

或者,您可以声明不包含零的数据类型,这样任何对零值的赋值都会被检测到。

type
	natural = 1..maxInt;
var
	divisor: natural;
begin
	write('Enter a positive integer: ');
	readLn(divisor); // entering a non-positive value will fail

一些 Pascal 方言引入了“异常”的概念,可以用来识别此类问题。异常可能在本书“扩展”部分再次提及。

数组作为参数

[edit | edit source]

数组可以通过一个简单的赋值语句 dataOut := dataIn; 进行复制。但是,正如 Pascal 的强类型安全所要求的那样,两个数组必须是赋值兼容的:这意味着它们的基类型和维度规格必须相同。[fn 10]

由于调用例程涉及隐式赋值,如果整个程序只能使用一种数组类型,那么编写处理大量不同情况的通用代码将几乎不可能。为了缓解这种情况,一致数组类型参数允许编写接受不同数组维度长度的例程。数组维度数据类型仍然必须匹配

让我们看一个使用此功能的示例程序。

program tableDemo(output);

procedure printTableRows(data: array[minimum..maximum: integer] of integer);
var
	i: integer; // or in EP `i: type of minimum;` [preferred alternative]
begin
	for i := minimum to maximum do
	begin
		writeLn(i:20, ' | ', data[i]:20);
	end;
end;

一致数组参数看起来与常规数组变量声明非常相似,但维度的指定方式不同。通常,在声明新数组时,您会提供常量值作为维度限制。但是,由于我们不需要常量,因此我们为任何数组的实际维度限制命名占位符标识符 printTableRows 将接收。在本例中,它们被命名为 minimummaximum,通过中间的 .. 连接,表示一个范围

minimummaximumprintTableRows 的定义中成为变量,但您不允许为它们赋值。[fn 11]不允许声明与数组边界变量具有相同名称的新标识符。

在 Pascal 中,所有常量隐式地具有一个明确可确定的数据类型。由于我们的数组限制标识符实际上是变量,因此它们需要数据类型。: integer 表示 minimummaximum 都有数据类型 integer

Note 在一致数组参数中,嵌套数组的简短表示法使用 ; 来分隔多个维度,因此类似于常规参数列表。

一旦我们声明并定义了 printTableRows,我们就可以对许多不同大小的数组使用它。

var
	table: array[0..3] of integer;
	nein: array[9..9] of integer;
begin
	table[0] := 1;
	table[1] := 2;
	table[2] := 4;
	table[3] := 8;
	printTableRows(table);
	
	nein[9] := 9;
	printTableRows(nein);
end.

Delphi 和 FPC(截至 2020 年发布的 3.2.0 版本)不支持一致数组参数,但 GPC 支持。

对数

[edit | edit source]

特殊支持

[edit | edit source]

在 21 世纪之前,对数表和计算尺在手工计算中被广泛使用,以至于导致 Pascal 中包含了两个基本函数。

  • 函数 exp 将数字以 (欧拉常数)为底进行指数运算。 exp(x) 的值等效于数学术语 .
  • 函数 ln(“logaritmus naturalis”的缩写)求取数的自然对数。“自然”是指

这两个函数始终返回 real 值。

介绍

[edit | edit source]

由于并非所有课程都教授对数的使用,或者您可能只是需要复习一下,这里简要介绍一下:对数的基本思想是所有运算都提升一个级别。

对数级别
真实级别
基本对数规则

在提升的层面上,许多操作变得更简单,尤其是当数字很大时。例如,如果你实际上要取两个数字的乘积,那么你可以执行一个相当简单的 *加法*。为此,你需要将所有操作数向上提升一级:这可以通过取对数来完成。特别注意 : 在对数层面上, 是一个非“对数化”的因子,你只需要取 的对数。

完成后,你需要再次下降一级以获得 *预期* 操作(在底层)的实际“真实”结果。 ln 的反向操作是 exp[fn 12] 要用 Pascal 术语来表达这个原则,

x * y = exp(ln(x) + ln(y))

请记住,xy 必须为正数才能成为 ln 的有效参数。

应用

[edit | edit source]

取对数然后再次对值进行指数运算被认为是“缓慢”的操作,会引入一定程度的开销。在编程中,*开销* 指的是执行与实际底层问题没有直接关系的步骤,而只是为了解决问题而采取的步骤。一般来说,应该避免开销,因为(首先)它会让我们离解决方案更远。

尽管如此,如果 *中间* 结果超出了 real 数据类型的范围,但 *已知* 最终结果 *肯定* 会再次在 real 的范围内,那么就可以使用对数来 *克服* real 数据类型的范围限制。

代码:

program logarithmApplicationDemo(output);
const
	operand = maxReal;
var
	result: real;
begin
	// for comparison
	writeLn(maxReal:40);
	
	result := sqr(operand);
	result := sqrt(result);
	writeLn(result:40);
	
	// “lift” one level
	result := ln(operand);
	
	result := 2.0 * result; // corresponds to sqr(…)
	result := 0.5 * result; // corresponds to sqrt(…)
	
	// reverse `ln`: bring `result` “back down”
	result := exp(result);
	
	writeLn(result:40);
end.

输出:

 1.7976930000000000495189307440746532950903200318892038e+308
                                                         Inf
 1.7976929999999315921963138504476453672053533033977331e+308
第 10 行中 sqr 的中间结果超出了 real 的范围,导致任何后续结果无效。在这个特定的实现中,这种情况显示为 Inf(无穷大)。由于我们 *知道* 通过取主平方根来逆转这个操作会再次得到可存储的结果,所以我们可以使用对数来执行相同的操作。 显示的输出是由使用 GPC 编译的 program 生成的。该程序在使用 80 位数字的具有 FPU 的 64 位平台上执行。

如您所见,这会损害精度。这是在快速操作和“足够精确”的结果之间的 *妥协*。

当然,最好的解决方案是找到一个 *更好的算法*。上面的演示 实际上是 ,即 abs(x)(请记住,对数字进行平方运算始终会产生一个非负数)。此操作的结果将保留在 real 的范围内。

任务

[edit | edit source]

所有任务,包括以下章节中的任务, *都可以* 在 *没有* 符合数组参数的情况下解决。这考虑到了并非所有主要编译器都支持符合数组参数这一事实。[fn 13] 尽管如此,使用带有符合数组参数的例程通常是更优雅的解决方案。

编写一个 program,打印以下乘法表
    1    2    3    4    5    6    7    8    9   10
    2    4    6    8   10   12   14   16   18   20
    3    6    9   12   15   18   21   24   27   30
    4    8   12   16   20   24   28   32   36   40
    5   10   15   20   25   30   35   40   45   50
    6   12   18   24   30   36   42   48   54   60
    7   14   21   28   35   42   49   56   63   70
    8   16   24   32   40   48   56   64   72   80
    9   18   27   36   45   54   63   72   81   90
   10   20   30   40   50   60   70   80   90  100
该程序可能不包含任何字符串文字(即你不能只使用十个 writeLn)。数据 *生成* 和数据 *打印* 应由 *单独* 的例程实现(这些例程 *不能* 互相调用)。
以下是一个直接的实现。
program multiplicationTable(output);

const
	xMinimum = abs(1);
	xMaximum = abs(10);
	yMinimum = abs(1);
	yMaximum = abs(10);
	// NB: Only Extended Pascal allows constant definitions
	//     based on expressions. The following two definitions
	//     are illegal in Standard Pascal (ISO 7185).
	zMinimum = xMinimum * yMinimum;
	zMaximum = xMaximum * yMaximum;

现在(作为 *常量*)计算最大和最小预期值,这样如果任何值超过了 maxInt,编译器会在 *编译* 期间发出警告。

插入 abs 是作为一种文档方式:该程序仅对 *非* 负数有效。

type
	x = xMinimum..xMaximum;
	y = yMinimum..yMaximum;
	z = zMinimum..zMaximum;
	table = array[x, y] of z;

使用 z 作为 table 数组的基本类型(而不仅仅是 integer)的优点是,如果我们意外地 *错误地* 实现了乘法,分配超出范围的值将失败。对于像这样的简单任务,这当然无关紧要,但对于更复杂的情况,*故意限制* 允许的范围可以 *阻止* 编程错误。如果您只是使用了一个简单的 integer,请不要担心。

var
	product: table;

注意,product 变量必须在定义 populateTableprintTable *之前* 和 *外部* 声明。这样,这两个例程都引用了 *同一个* product 变量。[fn 14]

procedure populateTable;
var
	factorX: x;
	factorY: y;
begin
	for factorX := xMinimum to xMaximum do
	begin
		for factorY := yMinimum to yMaximum do
		begin
			product[factorX, factorY] := factorX * factorY;
		end;
	end;
end;

也可以重复使用先前计算的值,利用表格可以沿着对角线轴镜像的事实,或进行其他“优化技巧”。不过,对于 *此任务* 来说,重要的是正确地嵌套 for 循环。

procedure printTable;
var
	factorX: x;
	factorY: y;
begin
	for factorY := yMinimum to yMaximum do
	begin
		for factorX := xMinimum to xMaximum do
		begin
			write(product[factorX, factorY]:5);
		end;
		writeLn;
	end;
end;

当然,高级实现首先会确定最大预期长度并将其存储为一个变量,而不是使用硬编码格式说明符 :5。不过,这超出了此任务的范围。[fn 15] 只是应该提到,像这样硬编码的值被认为是不好的风格。

begin
	populateTable;
	printTable;
end.
以下是一个直接的实现。
program multiplicationTable(output);

const
	xMinimum = abs(1);
	xMaximum = abs(10);
	yMinimum = abs(1);
	yMaximum = abs(10);
	// NB: Only Extended Pascal allows constant definitions
	//     based on expressions. The following two definitions
	//     are illegal in Standard Pascal (ISO 7185).
	zMinimum = xMinimum * yMinimum;
	zMaximum = xMaximum * yMaximum;

现在(作为 *常量*)计算最大和最小预期值,这样如果任何值超过了 maxInt,编译器会在 *编译* 期间发出警告。

插入 abs 是作为一种文档方式:该程序仅对 *非* 负数有效。

type
	x = xMinimum..xMaximum;
	y = yMinimum..yMaximum;
	z = zMinimum..zMaximum;
	table = array[x, y] of z;

使用 z 作为 table 数组的基本类型(而不仅仅是 integer)的优点是,如果我们意外地 *错误地* 实现了乘法,分配超出范围的值将失败。对于像这样的简单任务,这当然无关紧要,但对于更复杂的情况,*故意限制* 允许的范围可以 *阻止* 编程错误。如果您只是使用了一个简单的 integer,请不要担心。

var
	product: table;

注意,product 变量必须在定义 populateTableprintTable *之前* 和 *外部* 声明。这样,这两个例程都引用了 *同一个* product 变量。[fn 14]

procedure populateTable;
var
	factorX: x;
	factorY: y;
begin
	for factorX := xMinimum to xMaximum do
	begin
		for factorY := yMinimum to yMaximum do
		begin
			product[factorX, factorY] := factorX * factorY;
		end;
	end;
end;

也可以重复使用先前计算的值,利用表格可以沿着对角线轴镜像的事实,或进行其他“优化技巧”。不过,对于 *此任务* 来说,重要的是正确地嵌套 for 循环。

procedure printTable;
var
	factorX: x;
	factorY: y;
begin
	for factorY := yMinimum to yMaximum do
	begin
		for factorX := xMinimum to xMaximum do
		begin
			write(product[factorX, factorY]:5);
		end;
		writeLn;
	end;
end;

当然,高级实现首先会确定最大预期长度并将其存储为一个变量,而不是使用硬编码格式说明符 :5。不过,这超出了此任务的范围。[fn 15] 只是应该提到,像这样硬编码的值被认为是不好的风格。

begin
	populateTable;
	printTable;
end.


想一下用 *少于* 五个字符来指定 real 值文字的多种不同方式,它们都表示值“正一”。它们是什么?
无论它们的可使用性如何,以下所有都是表示值“正一”的合法 real 值文字
  1. 1.0
  2. +1.0
  3. 1.00
  4. 1E0
  5. 1E+0
  6. 1E-0
  7. 1E00
在现实生活中,你主要使用的是 1.0。本练习旨在让你意识到,与 integer 值不同,real 数值可以有多种有效的表示形式。注意,一些编译器也会接受诸如 1. 的字面量,但这是非标准的。GPC 仅在非 ISO 兼容模式下接受它,但仍会发出警告。
无论它们的可使用性如何,以下所有都是表示值“正一”的合法 real 值文字
  1. 1.0
  2. +1.0
  3. 1.00
  4. 1E0
  5. 1E+0
  6. 1E-0
  7. 1E00
在现实生活中,你主要使用的是 1.0。本练习旨在让你意识到,与 integer 值不同,real 数值可以有多种有效的表示形式。注意,一些编译器也会接受诸如 1. 的字面量,但这是非标准的。GPC 仅在非 ISO 兼容模式下接受它,但仍会发出警告。


编写一个二元(= 接受/取两个参数)整数 function,计算正数的 n 次方。尽可能限制参数的数据类型。如果结果无效(即超出范围),函数应返回 0
“计算”意味着我们想要精确的结果。虽然你已经在这节课中学过对数,但最好始终保持在整数范围内。对数可能会产生近似值。以下是一种可接受的实现
type
	naturalNumber = 1..maxInt;
	wholeNumber = 0..maxInt;

{**
	\brief iteratively calculates the integer power of a number
	\param base the (positive) base in x^n
	\param exponent the (non-negative) exponent in x^n
	\return \param base to the power of \param exponent,
		or zero in the case of an error
**}
function power(base: naturalNumber; exponent: wholeNumber): wholeNumber;
var
	accumulator: wholeNumber;
begin
	{ anything [except zero] to the power of zero is defined as one }
	accumulator := 1;
	
	for exponent := exponent downto 1 do
	begin
		{ if another “times `base`” would exceed the limits of `integer` }
		{ we invalidate the entire result }
		if accumulator > maxInt div base then
		begin
			accumulator := 0;
		end;
		
		accumulator := accumulator * base;
	end;
	
	power := accumulator;
end;
正如你所见,编写诸如 exponent := exponent 之类的空语句完全有效,以满足 Pascal 语法的要求。一个好的编译器会对其进行优化。请注意,EP 标准提供了 integer 次方运算符 pow[fn 16]
“计算”意味着我们想要精确的结果。虽然你已经在这节课中学过对数,但最好始终保持在整数范围内。对数可能会产生近似值。以下是一种可接受的实现
type
	naturalNumber = 1..maxInt;
	wholeNumber = 0..maxInt;

{**
	\brief iteratively calculates the integer power of a number
	\param base the (positive) base in x^n
	\param exponent the (non-negative) exponent in x^n
	\return \param base to the power of \param exponent,
		or zero in the case of an error
**}
function power(base: naturalNumber; exponent: wholeNumber): wholeNumber;
var
	accumulator: wholeNumber;
begin
	{ anything [except zero] to the power of zero is defined as one }
	accumulator := 1;
	
	for exponent := exponent downto 1 do
	begin
		{ if another “times `base`” would exceed the limits of `integer` }
		{ we invalidate the entire result }
		if accumulator > maxInt div base then
		begin
			accumulator := 0;
		end;
		
		accumulator := accumulator * base;
	end;
	
	power := accumulator;
end;
正如你所见,编写诸如 exponent := exponent 之类的空语句完全有效,以满足 Pascal 语法的要求。一个好的编译器会对其进行优化。请注意,EP 标准提供了 integer 次方运算符 pow[fn 16]


改进你之前的解决方案,使其也适用于负 base 值。如果你的编译器支持 EP procedure halt,你的 function 应该打印一条错误消息并终止 program,如果 ,因为没有普遍认可的 定义
已突出显示更改的行。
{**
	\brief iteratively calculates the integer power of a number
	\param base the non-zero base in x^n
	\param exponent the (non-negative) exponent in x^n
	\return \param base to the power of \param exponent,
		or zero in the case of an error
	
	The program aborts if base = 0 = exponent.
**}
function power(base: integer; exponent: wholeNumber): integer;
var
	accumulator: integer;
	negativeResult: Boolean;
请记住更新所有数据类型,并保持源代码文档同步。
begin
	if [base, exponent] = [0] then
	begin
		writeLn('Error in `power`: base = exponent = 0, but 0^0 is undefined');
		halt;
	end;
根据任务说明,这部分是可选的。这里,选择了一个 set 来让你对这种可能性有所了解。但是,你通常并且最有可能编写 (base = 0) and (base = exponent) 或类似的语句,这同样有效。
	{ anything [except zero] to the power of zero is defined as one }
	accumulator := 1;
	
	negativeResult := (base < 0) and odd(exponent);
	{ calculating the _positive_ power of base^exponent }
	{ simplifies the invalidation condition in the loop below }
	base := abs(base);
	
	if base > 1 then
	begin
		for exponent := exponent downto 1 do
		begin
			{ if another “times `base`” would exceed the limits of `integer` }
			{ we invalidate the entire result }
			if accumulator > maxInt div base then
			begin
				accumulator := 0;
			end;
			
			accumulator := accumulator * base;
		end;
	end;
这个新的 if 分支的必要性可能不像它应该的那样明显:因为我们之前将可能的 base 值的范围扩展到所有 integer 值,因此也可以指定 0。但是,请记住 除以零 是非法的。由于我们的无效条件依赖于 div base,因此我们需要采取预防措施。
	if negativeResult then
	begin
		accumulator := -1 * accumulator;
	end;
	
	power := accumulator;
end;
已突出显示更改的行。
{**
	\brief iteratively calculates the integer power of a number
	\param base the non-zero base in x^n
	\param exponent the (non-negative) exponent in x^n
	\return \param base to the power of \param exponent,
		or zero in the case of an error
	
	The program aborts if base = 0 = exponent.
**}
function power(base: integer; exponent: wholeNumber): integer;
var
	accumulator: integer;
	negativeResult: Boolean;
请记住更新所有数据类型,并保持源代码文档同步。
begin
	if [base, exponent] = [0] then
	begin
		writeLn('Error in `power`: base = exponent = 0, but 0^0 is undefined');
		halt;
	end;
根据任务说明,这部分是可选的。这里,选择了一个 set 来让你对这种可能性有所了解。但是,你通常并且最有可能编写 (base = 0) and (base = exponent) 或类似的语句,这同样有效。
	{ anything [except zero] to the power of zero is defined as one }
	accumulator := 1;
	
	negativeResult := (base < 0) and odd(exponent);
	{ calculating the _positive_ power of base^exponent }
	{ simplifies the invalidation condition in the loop below }
	base := abs(base);
	
	if base > 1 then
	begin
		for exponent := exponent downto 1 do
		begin
			{ if another “times `base`” would exceed the limits of `integer` }
			{ we invalidate the entire result }
			if accumulator > maxInt div base then
			begin
				accumulator := 0;
			end;
			
			accumulator := accumulator * base;
		end;
	end;
这个新的 if 分支的必要性可能不像它应该的那样明显:因为我们之前将可能的 base 值的范围扩展到所有 integer 值,因此也可以指定 0。但是,请记住 除以零 是非法的。由于我们的无效条件依赖于 div base,因此我们需要采取预防措施。
	if negativeResult then
	begin
		accumulator := -1 * accumulator;
	end;
	
	power := accumulator;
end;


编写一个程序,接受“chirps”,即由最多 320 个字符组成的微博消息。
  • 用户将用空行终止其输入。事先打印此说明。
  • 完成后,打印消息。
  • 打印时,一行最多可以有 80 个字符(或对你来说合理的长度)。你可以假设用户的输入行最多为 80 个字符。
  • 确保你只在空格字符处换行(除非一行中没有空格字符)。
提示:为了测试目的,你可能希望缩小你的输入大小。


你可以在以下 Wikibook 页面找到更多可以解决的任务


来源

  1. Jensen, Kathleen; Wirth, Niklaus. Pascal – user manual and report (4th revised ed.). p. 56. doi:10.1007/978-1-4612-4450-9. ISBN 978-0-387-97649-5. An array type consists of a fixed number of components (defined when the array type is introduced) all having the same type, called the component type. {{cite book}}: no-break space character in |title= at position 7 (help)
  2. 这个限制来自 Pascal: ISO 7185:1990 (报告). ISO/IEC. 1991. p. 16. "real-type. 所需的类型标识符 real 应表示 real-type。 […] 这些值应为实数的实现定义子集,如 6.1.5 中所述,用带符号实数表示。"  最后一句话的末尾暗示,只有可以写入的那些值,即你在源代码中可以指定的那些值,才是合法的 real 值。例如,值 不同,后者的小数表示将无限长(也称为无理数),因此 的实际正确值不能在源代码中以 “real” 值的形式出现。
  3. a b Joslin, David A. (1989-06-01). "扩展 Pascal – 数值特性". Sigplan Notices. 24 (6): 77–80. doi:10.1145/71052.71063. 程序员可以通过实现定义的常量 MINREALMAXREALEPSREAL (均为正数)了解实数范围和精度。在 [-maxreal,-minreal]0[minreal,maxreal] 这些范围内,“可以预期算术运算将以合理的近似值进行”,而在这些范围之外则不能。然而,由于什么是“合理的近似值”是一个见仁见智的问题,并且在标准中没有定义,因此此声明可能不如乍一看那么有用。精度的衡量标准则更加明确:EPSREAL 是常用的精度度量(通常是浮点精度),即最小的值,使得 1.0 + epsreal > 1.0 {{cite journal}}: 换行符在 |quote= 位置 259 (帮助)
  4. a b Jensen, Kathleen; Wirth, Niklaus. Pascal – 用户手册和报告 (第 4 版修订版). p. 20. doi:10.1007/978-1-4612-4450-9. ISBN 978-0-387-97649-5. 只要至少一个操作数的类型为 Real (另一个操作数可能是 Integer 类型),以下运算符将生成一个实数值:
    * 乘法
    / 除法(两个操作数都可以是整数,但结果总是实数)
    + 加法
    - 减法
    {{cite book}}: 换行符在 |quote= 位置 218 (帮助); 不间断空格字符在 |title= 位置 7 (帮助)

注释

  1. 一些(旧的)计算机不知道方括号字符。说真的,这不是个玩笑。相反,使用了一个替代的二字母组:var signCharacter: array(.Boolean.) of char;,以及 signCharacter(.true.) := '-';。你可能在一些(旧的)Pascal 教材中遇到过这种表示法。无论如何,使用方括号仍然是首选方法。
  2. 只有与 packed array[1..n] of componentType 相关的I/O 是标准化的,其中 n 大于 1componentType 是或是一个 char 的子范围。但是,在本书的这部分,你还没有学习打包的概念,即 packed 关键字。因此,所展示的行为是非标准的。
  3. 更准确地说,text 文件是(可能为空的)序列,每行由一个(可能为空的)char 值序列组成。
  4. 一些编译器(例如 FPC)允许零大小的数据类型 [在任何 ISO 标准中都不允许]。如果是这种情况,则具有零大小基类型的数组将变得无效,实际上会失去所有数组的特性。
  5. 现代 real 算术处理器可以指示精度损失,即当算术运算的结果必须“近似”时。但是,没有标准化的方法可以从 Pascal 源代码中访问此类信息,并且通常这种信号也不利,因为最小的精度损失都会触发警报,从而降低程序速度。相反,如果重要的话,人们会使用允许任意精度算术的软件,例如 GNU 多精度算术库
  6. 这个数字并不完全是任意的。最流行的 real 数字实现 IEEE 754 使用了一个“隐藏位”,使值 1.0 变得特殊。
  7. 并非所有编译器都符合标准的这个定义。 FPC 的标准 round 实现将在等距情况下向偶数舍入。了解这一点对于统计应用很重要。 GPC 针对其 round 实现使用硬件提供的功能。因此,实现依赖于硬件,依赖于其特定配置,并且可能偏离 ISO 7185 标准定义。
  8. 鉴于最流行的实现补码 用于integer 值,以及IEEE 754 用于real 值,你必须考虑这样一个事实,即(实际上)integer 中的所有 位都对它的(数学)值有贡献,而一个real 数存储表达式 mantissa * base pow exponent 的值。用非常简单的术语来说,mantissa 存储值的integer 部分,但问题是它占用的位数少于integer 会使用的位数,因此(对于需要更多 位的值)会造成信息的损失(即精度损失)。
  9. 精确的技术定义是这样的:dividend div divisor 的值应使得
    abs(dividend) - abs(divisor) < abs((dividend div divisor) * divisor) <= abs(dividend) 
    其中,如果abs(dividend) < abs(divisor),则该值为零;否则,[…]
  10. 此外,两个数组都必须是packed 或“未打包”的。
  11. EP 标准将此特性称为protected
  12. 重要的是,两个函数都使用一个共同的基数,在本例中是 .
  13. ISO 标准 7185(“标准 Pascal”)将这种不符合数组参数的现象称为“Level 0 兼容性”。“Level 1 兼容性”包括对符合数组参数的支持。在入门中介绍的编译器中,只有GPC 是“Level 1”兼容的编译器。
  14. 这种编程风格略微不受欢迎,关键字“全局变量”,但目前我们还不知道合适的语法 (var 参数) 来避免这样做。
  15. 额外说明:你可以利用这样一个事实,即(假设zMaximum 是正数)。你可以使用这个值来找出所需的最小位数。
  16. EP 中,还存在一个real 幂运算符**。区别类似于除法运算符pow 仅接受integer 值作为操作数并返回一个integer 值,而** 始终返回一个real 值。你应该根据所需的精度选择其中一个,再次强调,你的选择应该基于所需的精度。
下一页: 字符串 | 上一页: 集合
首页: Pascal 编程
华夏公益教科书