5.2 返回命令
从第 4 章我们了解到,if 语句、while 语句、for 语句全部都可以借助 goto 语句实现。但是从源代码再利用的角度来看,仅仅依靠 goto 语句是不够的。
goto 语句无法将程序返回原来的位置。我们期望的运行是,执行跳转语句时记住这一位置,之后碰到返回语句时又能跳转回到该位置后面的语句。
有了返回原来的位置这样的命令,代码的再利用成为可能。一个程序中有几处执行相同的操作时,就可以把这些操作封装在一个地方了(图 5.1)。

图 5.1 返回命令使代码再利用成为可能
函数的诞生
把反复使用的命令封装在一起再利用,这种需求在很早以前就有了。1949 年的 EDSAC 就使用了带有这一功能的技术 4。
4当时还没有为之取名以便于理解。1949 年编写的“The Square Program”中,92 处和 114 处的指令将原先放置在 75 处的跳转命令的跳转目的地改写了。这里仅对该技术作简单说明,详细的源代码和解释可以参考该 PDF 文件链接:http://www.cl.cam.ac.uk/~mr10/edsacposter.pdf。
当时,程序的命令和数据完全都存储在内存中,修改程序就如同把数值代入变量中一样简单。通过修改程序中跳转命令的跳转目的地,就能使函数调用后返回原来的位置 5。
5也许有人说,这个因为没有返回值,所以不是函数而是子程序(subroutine)。这么说没错,但本书将子程序也归为函数进行说明。
1: 将 110 处跳转命令的跳转目的地改写为 3 处
2: 调用函数(跳转至 100 处)
3: 下一个命令
……
51: 将 110 处跳转命令的跳转目的地改写为 53 处
52: 调用函数(跳转至 100 处)
53: 下一个命令
……
100: 函数操作
……
110: 返回(跳转至 0 处) 就这样,函数诞生了 6。
6其他领域也在使用函数这个概念。比如,作为数学用语的函数(function)是莱布尼茨于 1673 年最早使用的。在理论计算机科学领域,1930 年发明的 λ 演算也使用了函数这个概念,通过建模说明计算的本质。
记录跳转目的地的专用内存
在函数调用前修改返回命令的跳转目的地时,函数调用者必须同时知道跳转目的地在哪里和返回命令所在地在哪里。这是很难办到的。假如在函数中增加几行代码,返回命令的位置就会相应地往后挪一些。这样一来,就不得不修改调用这一函数的全部代码。
后来出现了稍微改良过的方法,即创建用来事先记录返回目的地的内存空间,并设计能跳转到该内存空间里记录的地址的命令。这样,即使函数调用前不知道返回命令所在地也没关系了 7。
7这里的返回目的地内存通常位于寄存器的高速存储装置中。
1: 将 3 写入返回目的地内存
2: 调用函数(跳转至 100 处)
3: 下一个命令
……
100: 函数操作
……
101: 返回至返回目的地内存所记录的地址
然而,这种方法也有一个问题。当调用函数 X 期间又调用了函数 Y 时,返回目的地内存被写覆盖,函数 X 执行之后应该返回的目的地地址就找不到了。这时该如何处理呢?
专栏
函数命名
说到函数为操作命名的好处和实现方法,其实和函数之外的其他因素也有关系。
使用函数给操作命名的做法,就是用便于理解的字符串取代数值,来表示操作开始时内存的位置。这和变量一样。变量的诞生,就是为了用字符串替代数值来表示存储了某个值的内存的位置。
关于名字和作用域的相关内容,我们将在第 7 章详细论述。
栈
栈终于登场了 8。栈是一种存储有多个值的数据结构,实现最后被存入的值最先被读取。在第 3 章介绍 FORTH 语言时我们也有提到。
8此外还有其他方法,比如为函数分别准备记录返回位置的场所。早期的 FORTRAN 语言就采用了这种方法。但在调用函数 X 期间再次调用函数 X 时,这种方法就无法知道返回目的地了,无法实现下一节中讲的递归调用。
那么栈具体是怎么实现的呢?首先,决定记录栈顶位置(即最后被存入的数据的地址)的内存地址。图 5.2 中这一地址就是 42。之后每当存入新数据时将按步骤执行,42 处数值加 1 后把数据存入该数值指向的地址 9。
9此处的 42 没有什么特殊含义。
最初的状态(图 5.2 ❶)如下:
42: 栈顶在哪(当前值:100)
100:
将函数 X 的返回目的地写入该栈后的状态(图 5.2 ❷)如下:
42: 栈顶在哪(当前值:101)
100:
101: 函数 X 的返回目的地
然后把函数 Y 的返回目的地写入栈。42 处的值加 1 后变成 102,数 据被写入 102 处(图 5.2 ❸)。
42: 栈顶在哪(当前值:102)
100:
101: 函数 X 的返回目的地
102: 函数 Y 的返回目的地

图 5.2 在栈中写入值
接下来,我们来看一下数据的读取过程。数据读取时按照步骤,先读 42 处数值指向的地址上存储的数据,再将 42 处数值减 1(图 5.3)。
42: 栈顶在哪(当前值:101)
100:
101: 函数 X 的返回目的地
102: 函数 Y 的返回目的地

图 5.3 从栈中读取值
这样一来,即使在调用函数 X 期间又调用了函数 Y,也不至于把函数 X 的返回目的地写覆盖,程序可以顺利地返回。
