跳转至内容

Pascal 编程/例程

来自 Wikibooks,开放世界的开放书籍

在第一章已经提到了例程。例程,如之前所述,是可以重复使用的代码片段,可以反复使用。例程的示例包括read / readLnwrite / writeLn。您可以根据需要调用这些例程任意多次。在本章中,您将学习

  • 如何定义您自己的例程,
  • 定义和声明之间的区别,以及
  • 函数和过程之间的区别。

不同场合的不同例程

[编辑 | 编辑源代码]

例程有两种形式。在 Pascal 中,例程可以替换语句,也可以替换(子)表达式。可以在允许使用语句的地方使用的例程称为procedure(过程)。作为表达式的一部分调用的例程是function(函数)。

function(函数)是一种返回值的例程。Pascal 定义了包括odd在内的多个函数。函数odd将一个integer(整数)表达式作为参数,并根据提供的参数的奇偶性返回falsetrue(通俗地说,就是它是否能被 2 整除)。让我们看看函数odd 的实际应用

program functionDemo(input, output);
var
	x: integer;
begin
	write('Enter an integer: ');
	readLn(x);
	
	if odd(x) then
	begin
		writeLn('Now this is an odd number.');
	end
	else
	begin
		writeLn('Boring!');
	end;
end.

Odd(x) 读作“x 的奇偶性”。首先,评估括号中的表达式。这里它仅仅是x,更确切地说,是变量的值,但只要最终计算结果为integer(整数)表达式,也允许更复杂的表达式。然后,此表达式的(即实际参数)被传递到一个(在本例中不可见的)代码块,该代码块处理输入,对其执行一些计算,并根据计算结果返回falsetrue。函数的返回值最终将填充到函数调用的位置。在您的脑海中,您可以将false / true 替换为odd(x),尽管这取决于给定的输入而动态变化。

另一方面,过程不能用作表达式的部分。您只能在允许使用语句的地方调用过程。

procedure(过程)可以使用函数,反之亦然。不要将function(函数)仅仅理解为表达式的替代品。在下一节中,我们将学习原因。

基本原理

[编辑 | 编辑源代码]

例程的二分法,区分procedure(过程)和function(函数),旨在温和地推动程序员编写“干净”的程序。这样做,例程不会隐藏它只是一个语句序列的替代品,还是一个复杂、难以写出的表达式的简写。这种表示法在不引入讨厌的伪类型(例如,C 编程语言中的void,其中每个例程都是一个函数,但“无效”数据类型void 将允许您使其(部分)表现得像procedure)的情况下起作用。

定义例程遵循您从第一个program 开始就熟悉的模式。program 在某些方面类似于特殊的例程:您可以通过 OS 定义的方式运行它任意多次。program 的定义几乎与例程的定义完全一样。

例程由以下部分定义:

  1. 一个头部,以及
  2. 一个块

按照这个顺序。例程头部根据它是function还是procedure,显示出一些差异。我们首先看看块,因为它们对这两种类型的例程都是一样的。

一个是生产性部分(语句)和(可选的)声明和定义的综合。在标准 Pascal(如 ISO 标准 7185 所述)中,块具有固定的顺序:[fn 2]

  1. 常量定义(const 部分)
  2. 类型定义(type 部分)
  3. 变量声明(var 部分)
  4. 例程声明和定义
  5. 序列(begin end,可能为空)

除了最后一个生产性部分之外,所有项目都是可选的。

Note 部分(consttypevar 部分)不能为空。一旦您指定了部分标题,您就必须在刚刚开始的部分中至少定义/声明一个符号。

EP 中,已取消固定顺序限制。在那里,部分和例程声明以及定义可以根据需要出现多次,并且不必遵循特定顺序。结果在“范围”一章中详细介绍。本书其余部分将参考 EP的定义,因为所有主要的编译器都支持这一点。但是,标准 Pascal 定义的顺序是一个很好的指导方针:在可能使用这些类型(即 var 部分)的部分之前定义类型是有意义的。

例程头部由以下部分组成:

  1. 单词 functionprocedure
  2. 标识此例程的标识符,
  3. 可能的参数列表,以及,
  4. 最后,对于函数,调用此函数产生的表达式的类型,即结果数据类型

例程的参数列表还定义每个参数的数据类型。因此,函数 odd 的头部可能如下所示

function odd(x: integer): Boolean;

请注意参数列表之后的分号 (:),它分隔函数的结果数据类型。您可以将函数视为一种特殊的变量声明,它也用分号分隔标识符,但对于函数,"变量"的值是动态计算的。

形式参数,即例程头部上下文中的参数,用分号分隔。考虑以下过程头部

procedure printAligned(x: integer; tabstop: integer);

请注意,每个例程头部都以分号结尾。

虽然例程头部告诉处理器(通常是编译器),“嘿,有一个具有以下属性的例程:[…]”,但这还不够。您必须“充实”,给例程一个主体。这是在随后的块中完成的。

在块内,所有参数都可以像变量一样读取。

函数结果

[编辑 | 编辑源代码]

在定义函数的块的序列中,自动存在一个函数名称的变量。您必须恰好分配一次值,以便函数在数学上定义。参考此示例

function getRandomNumber(): integer;
begin
	// chosen by fair dice roll,
	// guaranteed to be random
	getRandomNumber := 4;
end;

请注意,该块不包含声明变量 getRandomNumbervar 部分,但它已由函数的头部隐式声明:名称和数据类型都是函数头部的一部分。

例程声明大多数情况下是隐式的。声明例程或一般任何标识符是指向处理器(即通常是编译器)提供信息以正确解释您的程序源代码的过程。此信息不会直接编码在您的可执行程序中,但它隐式存在。示例包括:

  • 变量声明告诉处理器安装适当的规定以预留一些内存空间。这块内存将根据其关联的数据类型进行解释。但是,变量的名称或数据类型都不会以任何方式存储在您的程序中。只有处理器在读取您的源代码文件时才知道此信息。
  • 例程头部构成例程声明(通常紧随其定义之后[fn 3])。同样,例程头部中提供的信息不会直接存储在可执行文件中,但它们可以确保处理器(编译器)正确转换您的源代码。
  • 同样,type 声明仅用于干净和抽象的编程,但这些声明不会最终出现在可执行程序文件中。[fn 4]

声明使标识符能够表示某个对象(从数学上讲是“对象”)。另一方面,定义将根据其名称定义此对象的确切含义。无论是常量的值、变量的值还是例程中采取的步骤(语句序列),通过定义定义的数据都将在您的可执行文件中生成特定代码,这可能会根据相关声明中提供的信息而有所不同;编写具有数据类型 integer 的变量与编写类型 real 的值根本不同。正确存储、计算和检索 integerreal 值的代码不同,但计算机并不知道这一点。它只是执行给定的指令,例如,某些指令类似于 Pascal 数据类型 real 上的操作,从某种意义上说,这是一个“巧合”。

调用例程

[编辑 | 编辑源代码]

例程根据其签名进行选择。例程签名由以下部分组成:

  1. 例程的名称,
  2. 所有参数的数据类型,以及
  3. (隐式)它们的正确顺序。

因此,函数 odd 的签名读取为 odd(integer)。名为 odd 的函数接受一个 integer 值作为第一个(也是唯一一个)参数。

Note 在其他一些编程语言中,返回值的数据类型也属于例程的签名。如果您在编程语言之间切换,请记住术语签名的不同定义。

Pascal 允许你声明和定义同名的过程,但它们的形式参数不同。这通常称为重载。当调用过程时,必须正好有一个同名过程接受参数及其对应的数据类型。

预定义过程

[编辑 | 编辑源代码]
Pascal 的预定义函数(节选)
签名 描述 返回值类型
abs(integer) 参数的绝对值 整数
odd(integer) 奇偶性(给定值是否能被二整除) 布尔值
sqr(integer) 值的平方 整数

持久变量

[编辑 | 编辑源代码]

一些编译器,例如 FPC,允许你使用常量就像变量一样,但它们的生命周期不同。在下面的示例中,“常量” numberOfInvocations 在整个程序执行期间都存在,但只能在其声明的范围内访问。

program persistentVariableDemo(output);
{$ifDef FPC}
	// allow assignments to _typed_ “constants”
	{$writeableConst on}
{$endIf}

procedure foo;
const
	numberOfInvocations: integer = 0;
begin
	numberOfInvocations := numberOfInvocations + 1;
	writeLn(numberOfInvocations);
end;

begin
	foo;
	foo;
	foo;
end.

程序将为每次调用打印 123。第 2、4 和 5 行包含精心设计的注释,这些注释指示编译器支持持久变量。这些注释是非标准的,但其中一些在附录中进行了说明,“预处理器功能”章节

请注意,类型化“常量”的概念并非标准化。一些面向对象编程扩展将提供更强大的工具来实现如上所示的行为。我们主要向您解释了持久变量的概念,以便您可以阅读和理解其他人的源代码。

过程可以根据需要使用多次。它们不是简单的“文本替换”工具:过程的定义不会“复制”到其被调用的位置,即调用站点。可执行程序文件的大小基本保持不变。

利用过程也有利于,并且通常有利于程序的开发进度。通过将编程项目分解成更小的可理解的问题,你可以专注于解决作为大任务一部分的孤立问题。这种方法被称为分而治之。我们现在要求你逐渐转向在开始输入任何内容之前更多地思考你的编程任务。你可能需要花更多时间思考,例如,如何构建过程的参数列表。这个过程需要什么信息,什么参数?在哪里以及如何通过过程定义来概括重复出现的模式?识别这些问题需要时间和专业知识,因此如果你没有看到任务示例答案中显示的所有内容,请不要气馁。你会从错误中学习。

但是,请记住,过程并不是万能药。在某些非常特定的情况下,你可能不希望使用过程。然而,识别这些情况超出了本书的范围。为了本书的目的,以及在 99% 的所有编程项目中,如果可能,你都希望使用过程。现代编译器甚至可以识别某些“不必要”使用过程的情况,但唯一的好处是你的源代码结构更加清晰,因此更易读,尽管是以牺牲抽象度增加复杂度为代价的。[脚注 5]

编写一个(现在臭名昭著的)程序,以大写字母(至少跨三行)输出单词“Mississippi”。应该清楚的是,编写四个过程,printMprintIprintSprintP,将大大加快开发速度。
可接受的答案可能如下所示
program mississippi(output);

const
	width = 8;

procedure printI;
begin
	writeLn( '#   ':width);
	writeLn( '#   ':width);
	writeLn( '#   ':width);
	writeLn( '#   ':width);
	writeLn( '#   ':width);
	writeLn;
end;

procedure printM;
begin
	writeLn('#    #':width);
	writeLn('##  ##':width);
	writeLn('# ## #':width);
	writeLn('#    #':width);
	writeLn('#    #':width);
	writeLn;
end;

procedure printP;
begin
	writeLn( '###  ':width);
	writeLn( '#  # ':width);
	writeLn( '###  ':width);
	writeLn( '#    ':width);
	writeLn( '#    ':width);
	writeLn;
end;

procedure printS;
begin
	writeLn('  ###  ':width);
	writeLn(' #   # ':width);
	writeLn('  ##   ':width);
	writeLn('#   #  ':width);
	writeLn(' ###   ':width);
	writeLn;
end;

begin
	printM;
	printI;
	printS;
	printS;
	printI;
	printS;
	printS;
	printI;
	printP;
	printP;
	printI;
end.
可接受的答案可能如下所示
program mississippi(output);

const
	width = 8;

procedure printI;
begin
	writeLn( '#   ':width);
	writeLn( '#   ':width);
	writeLn( '#   ':width);
	writeLn( '#   ':width);
	writeLn( '#   ':width);
	writeLn;
end;

procedure printM;
begin
	writeLn('#    #':width);
	writeLn('##  ##':width);
	writeLn('# ## #':width);
	writeLn('#    #':width);
	writeLn('#    #':width);
	writeLn;
end;

procedure printP;
begin
	writeLn( '###  ':width);
	writeLn( '#  # ':width);
	writeLn( '###  ':width);
	writeLn( '#    ':width);
	writeLn( '#    ':width);
	writeLn;
end;

procedure printS;
begin
	writeLn('  ###  ':width);
	writeLn(' #   # ':width);
	writeLn('  ##   ':width);
	writeLn('#   #  ':width);
	writeLn(' ###   ':width);
	writeLn;
end;

begin
	printM;
	printI;
	printS;
	printS;
	printI;
	printS;
	printS;
	printI;
	printP;
	printP;
	printI;
end.

注释

  1. 某些 Pascal 方言在这方面并不那么严格:FPC 有一个选项 {$extendedSyntax on},它将允许上述程序继续编译。
  2. 已有意省略label 部分。
  3. 扩展 Pascal 标准允许所谓的“前向声明”[远程指令]。过程的前向声明只是声明,没有定义。
  4. 一些编译器支持生成非标准的“运行时类型信息”(RTTI)。通过启用 RTTItype 声明确实会生成存储在程序中的数据。
  5. 一种这样的编译器优化称为内联。这将有效地将过程定义复制到调用站点。纯函数 甚至可以通过将其定义为独立函数而受益,前提是编译器支持适当的优化。


下一页: 枚举 | 上一页: 表达式和分支
首页: Pascal 编程
华夏公益教科书