是因为 Pascal 就是这样设计的,还是有任何取舍?
或者禁止或不禁止修改 for 块内的计数器的利弊是什么?恕我直言,修改 for 块内的计数器几乎没有用处。
编辑:
您能否提供一个示例,我们需要修改 for 块内的计数器?
很难在wallyk的答案和cartoonfox的答案之间做出选择,因为两个答案都很好。cartoonfox从语言方面分析问题,而wallyk从历史和现实世界方面分析问题。无论如何,感谢您的所有回答我要特别感谢 wallyk。
是因为 Pascal 就是这样设计的,还是有任何取舍?
或者禁止或不禁止修改 for 块内的计数器的利弊是什么?恕我直言,修改 for 块内的计数器几乎没有用处。
编辑:
您能否提供一个示例,我们需要修改 for 块内的计数器?
很难在wallyk的答案和cartoonfox的答案之间做出选择,因为两个答案都很好。cartoonfox从语言方面分析问题,而wallyk从历史和现实世界方面分析问题。无论如何,感谢您的所有回答我要特别感谢 wallyk。
在编程语言理论(和可计算性理论)中,WHILE 和 FOR 循环具有不同的理论属性:
C 中存在的 FOR 循环在技术上不算作 FOR 循环,因为您不一定知道循环在执行之前将迭代多少次。(即你可以破解循环计数器永远运行)
您可以使用 WHILE 循环解决的问题类比使用 Pascal 中的严格 FOR 循环可以解决的问题更强大。
Pascal 是这样设计的,以便学生有两个具有不同计算属性的不同循环结构。(如果您使用 C 方式实现 FOR,则 FOR 循环只是 while... 的替代语法)
在严格的理论术语中,您永远不需要在 for 循环中修改计数器。如果你能侥幸逃脱,你就会有一个 WHILE 循环的替代语法。
您可以在这些 CS 讲义中找到有关“while 循环可计算性”和“for 循环可计算性”的更多信息:http ://www-compsci.swan.ac.uk/~csjvt/JVTTeaching/TPL.html
顺便说一句,另一个这样的属性是循环变量在 for 循环之后未定义。这也使优化更容易
Pascal 最初是为 CDC Cyber(一个 1960 和 1970 年代的大型机)实现的,它与当今的许多 CPU 一样,具有出色的顺序指令执行性能,但对分支也有显着的性能损失。Cyber 架构的这个和其他特征可能严重影响了 Pascal 的for
循环设计。
简短的回答是,允许分配循环变量将需要额外的保护代码和对循环变量的优化优化,这些循环变量通常可以在 18 位索引寄存器中很好地处理。在那些日子里,由于硬件成本高且无法以任何其他方式加速软件性能,因此软件性能受到高度重视。
长答案
包含 Cyber 的 Control Data Corporation 6600 系列是一种 RISC 架构,使用 18 位地址引用的 60 位中央存储器字。一些模型有一个(昂贵的,因此不常见的)选项,比较移动单元(CMU),用于直接寻址 6 位字符字段,但除此之外不支持任何类型的“字节”。由于 CMU 通常不能指望,因此大多数网络代码都是因为没有它而生成的。每个单词十个字符是通常的数据格式,直到对小写字符的支持让位于暂定的 12 位字符表示。
指令为 15 位或 30 位长,但 CMU 指令实际上为 60 位长。因此,每个字最多包含 4 条指令,或者两个 30 位,或者一对 15 位和一个 30 位。30 位指令不能跨越字。由于分支目标可能只引用字,因此跳转目标是字对齐的。
该架构没有堆栈。事实上,过程调用指令RJ
本质上是不可重入的。 通过在 RJ 指令所在的位置RJ
写入跳转到下一条指令来修改被调用过程的第一个字。被调用的过程通过跳转到它们的开头返回给调用者,该开头是为返回链接保留的。程序从第二个单词开始。为了实现递归,大多数编译器都使用了辅助函数。
寄存器文件有 8 个实例,三种寄存器各有 8 个实例,A0..A7 用于地址操作,B0..B7 用于索引,X0..X7 用于一般算术。A 和 B 寄存器为 18 位;X 寄存器为 60 位。设置 A1 到 A5 的副作用是使用加载地址的内容加载相应的 X1 到 X5 寄存器。设置 A6 或 A7 会将相应的 X6 或 X7 内容写入加载到 A 寄存器中的地址。A0 和 X0 未连接。B 寄存器几乎可以在每条指令中用作与任何其他 A、B 或 X 寄存器相加或相减的值。因此,它们非常适合小型柜台。
对于高效的代码,B 寄存器用于循环变量,因为可以对它们使用直接比较指令(B2 < 100 等);与 X 寄存器的比较仅限于与零的关系,因此将 X 寄存器与 100 进行比较,例如,需要减去 100 并测试结果是否小于零等。如果允许对循环变量进行赋值,则为 60 位值在分配给 B 寄存器之前必须进行范围检查。这是一个真正的麻烦。Herr Wirth 可能认为麻烦和低效率都不值得使用实用程序——程序员总是可以在这种情况下使用while
or repeat
...until
循环。
额外的怪异
一些 Pascal 语言独有的特性与 Cyber 的各个方面直接相关:
pack
关键字:单个“字符”使用 60 位字,或者每个字包含十个字符。alfa
类型:packed array [1..10] of char
pack()
和unpack()
处理压缩字符。这些对现代架构不执行任何转换,仅进行类型转换。text
文件与文件的怪异file of char
writeln
set of char
在 CDC 上非常有用,但由于内存使用过多(8 位 ASCII 的 32 字节变量/常量),它在许多后续的 8 位机器上不受支持。相比之下,单个 Cyber 单词可以通过省略换行符和其他内容来管理本机 62 个字符集。Pascal 最初被设计为一种教学语言,以鼓励块结构编程。Kernighan(K&R 的 K)写了一篇关于 Pascal 局限性的(可以理解的有偏见的)文章,为什么 Pascal 不是我最喜欢的编程语言。
禁止修改 Pascal 所说的循环控制变量for
,再加上缺少break
语句,这意味着无需研究循环体的内容就可以知道循环体执行了多少次。
没有break
语句,并且在循环终止后不能使用控制变量比不能修改循环内的控制变量更多的是限制,因为它会阻止一些字符串和数组处理算法被写入“显而易见的“ 方法。
Pascal 和 C 之间的这些和其他差异反映了它们最初设计时的不同哲学:Pascal 强制执行“正确”设计的概念,C 或多或少允许任何事情,无论多么危险。
(注意:Delphi 确实有一个Break
声明,以及Continue
, 和CExit
中的一样return
。)
显然,我们永远不需要能够在for
循环中修改控制变量,因为我们总是可以使用while
循环重写。在 K&R 第 7.3 节中可以找到使用这种行为的 C 语言示例,其中printf()
介绍了 的简单版本。处理'%'
格式字符串中的序列的代码fmt
是:
for (p = fmt; *p; p++) {
if (*p != '%') {
putchar(*p);
continue;
}
switch (*++p) {
case 'd':
/* handle integers */
break;
case 'f':
/* handle floats */
break;
case 's':
/* handle strings */
break;
default:
putchar(*p);
break;
}
}
虽然这使用了一个指针作为循环变量,但它同样可以用一个整数索引写入字符串:
for (i = 0; i < strlen(fmt); i++) {
if (fmt[i] != '%') {
putchar(fmt[i]);
continue;
}
switch (fmt[++i]) {
case 'd':
/* handle integers */
break;
case 'f':
/* handle floats */
break;
case 's':
/* handle strings */
break;
default:
putchar(fmt[i]);
break;
}
}
它可以使一些优化(例如循环展开)更容易:不需要复杂的静态分析来确定循环行为是否可预测。
可以肯定地得出结论,Pascal 旨在防止修改循环内的 for 循环索引。值得注意的是,Pascal 绝不是唯一阻止程序员这样做的语言,Fortran 是另一个例子。
以这种方式设计语言有两个令人信服的理由:
对于许多算法,这种行为是必需的行为;例如,更新数组中的所有元素。如果内存服务于 Pascal 还提供 do-while 循环和 repeat-until 循环。大多数,我猜,用 C 风格语言实现的算法,修改循环索引变量或跳出循环,可以很容易地用这些替代形式的循环来实现。
我摸不着头脑,没有找到一个令人信服的理由来允许在循环内修改循环索引变量,但后来我一直认为这样做是糟糕的设计,并且选择了正确的循环构造作为元素好的设计。
问候
标记
免责声明:自从我上次做 PASCAL 以来已经有几十年了,所以我的语法可能并不完全正确。
您必须记住,PASCAL 是 Nicklaus Wirth 的孩子,Wirth 在设计 PASCAL(及其所有继任者)时非常关心可靠性和可理解性。
考虑以下代码片段:
FOR I := 1 TO 42 (* THE UNIVERSAL ANSWER *) DO FOO(I);
不看过程 FOO,回答以下问题:这个循环是否结束?你怎么知道的?过程 FOO 在循环中调用了多少次?你怎么知道的?
PASCAL 禁止修改循环体中的索引变量,以便可以知道这些问题的答案,并且知道当过程 FOO 改变时答案不会改变。
在某些语言(不是 C 或 C++)中,循环变量在循环体范围内是不可变的,任何修改其值的尝试都被视为语义错误。此类修改有时是程序员错误的结果,一旦做出就很难识别。然而,编译器可能只检测到明显的变化。将循环变量的地址作为参数传递给子例程的情况使得检查变得非常困难,因为编译器通常不知道例程的行为。
所以这似乎是为了帮助你以后不要烫手。