正如该答案所建议的,这是硬件支持的问题,尽管语言设计中的传统也起着作用。
当函数返回时,它将指针指向特定寄存器中的返回对象
在Fortran,Lisp和COBOL这三种第一语言中,第一种使用的返回值是基于数学建模的。第二个返回任意数量的参数,就像接收它们一样:作为一个列表(也可以说它仅传递并返回一个参数:列表的地址)。第三个返回零或一个值。
这些最早的语言对跟随它们的语言的设计产生了很大的影响,尽管唯一返回多个值的语言Lisp从未受到广泛的欢迎。
当C语言问世时,虽然受到了之前的语言的影响,但它非常注重有效利用硬件资源,并在C语言所做的工作与实现它的机器代码之间保持着紧密的联系。它的一些最古老的功能,例如“自动”与“注册”变量,是这种设计理念的结果。
还必须指出的是,汇编语言直到80年代才开始广泛流行,直到80年代才逐渐被主流开发所淘汰。编写编译器和创建语言的人都熟悉汇编语言,并且在大多数情况下,保持最佳状态。
与该规范背道而驰的大多数语言都从未受到太大欢迎,因此也从未在影响语言设计师的决策方面发挥过强大的作用(当然,他们的灵感来自于他们所知道的)。
因此,让我们来看一下汇编语言。首先让我们看一下6502,这是1975年的微处理器,在Apple II和VIC-20微型计算机中广为使用。与当时的大型机和小型计算机相比,它非常脆弱,尽管与20年前30年代编程语言兴起时的第一台计算机相比,它具有强大的功能。
如果您看一下技术说明,它有5个寄存器和几个1位标志。唯一的“满”寄存器是程序计数器(PC)-该寄存器指向要执行的下一条指令。其他寄存器是累加器(A),两个“索引”寄存器(X和Y)以及堆栈指针(SP)。
调用子例程会将PC放入SP指向的内存中,然后递减SP。从子例程返回则相反。一个人可以在栈中推入和拉出其他值,但是相对于SP而言,它很难引用到内存,因此编写重入子例程非常困难。我们认为这是理所当然的,它在我们认为随时都可以调用子例程的情况下,在这种体系结构上并不常见。通常,将创建一个单独的“堆栈”,以便将参数和子例程返回地址保持分开。
如果您看一下激发6502的处理器6800,它还有一个与SP一样宽的寄存器,即索引寄存器(IX),可以从SP接收值。
在机器上,调用重入子例程包括将参数压入堆栈,压入PC,将PC更改为新地址,然后该子例程会将其局部变量压入堆栈。由于局部变量和参数的数量是已知的,因此可以相对于堆栈进行寻址。例如,一个接收两个参数并具有两个局部变量的函数如下所示:
SP + 8: param 2
SP + 6: param 1
SP + 4: return address
SP + 2: local 2
SP + 0: local 1
可以调用任意次,因为所有临时空间都在堆栈上。
在TRS-80和许多基于CP / M的微型计算机上使用的8080,可以通过将SP压入堆栈,然后将其弹出到其间接寄存器HL中来实现与6800类似的功能。
这是一种非常常见的实现方式,它在更现代的处理器上获得了更多的支持,它的基本指针使返回之前的所有局部变量都易于转储。
问题是,您如何退还任何东西?处理器寄存器在早期并不是很多,经常需要使用其中一些来找出要寻址的内存。返回堆栈上的东西会很复杂:您必须弹出所有内容,保存PC,然后推回返回的参数(该参数将同时存储在哪里?),然后再次推PC并返回。
因此,通常要做的是为返回值保留一个寄存器。调用代码知道返回值将在特定的寄存器中,必须保留该值,直到可以保存或使用它为止。
让我们看一下一种允许多个返回值的语言:Forth。Forth所做的是保持单独的返回堆栈(RP)和数据堆栈(SP),以便函数必须要做的就是弹出其所有参数,并将返回值保留在堆栈上。由于返回堆栈是单独的,因此不会妨碍您的操作。
作为在使用计算机的前六个月中学习汇编语言和Forth的人,多个返回值对我来说似乎完全正常。像Forth这样的运算符/mod
返回整数除法,其余运算符似乎很明显。另一方面,我可以很容易地看到一个有C思维的早期经验的人如何发现这个概念很奇怪:它违背了他们对“功能”的根深蒂固的期望。
至于数学……好吧,在我上数学课之前,我正在用计算机编程。还有就是 CS和由数学影响编程语言的一个整体部分,但是,话又说回来,有一整节是不是。
因此,在数学影响早期语言设计,硬件限制决定了易于实现的因素以及流行语言影响硬件演变方式的因素方面,我们综合了一些因素(Lisp机器和Forth机器处理器是此过程中的杀手kill)。