PL真有意思(六):子程序和控制抽象

在之前我们把抽象定义为一种过程,程序员可以通过它将一个名字与一段可能很复杂的程序片段关联起来。抽象最大的意义就在于,我们可以从功能和用途的角度来考虑它,而不是实现。

在大多数程序设计语言中,子程序是最主要的控制抽象的方法。大多数子程序都是参数化的,即通过传递一些参数来影响子程序的行为。

回顾栈的布局

当一个子程序被调用的时候,在栈的顶部将给它一个新的栈帧或称为活动记录。这个栈帧可能包含实际参数和/或返回值、簿记信息(包含返回地址和保存的寄存器)、局部变量和/或各种临时量。当子程序返回时,栈帧从栈中弹出。

如果某个对象的大小在编译时位置,那么就将它放在栈帧的顶部大小可变的区域,并将它的地址和内情向量保存在栈帧的某个部分,放在相对于栈指针的一个静态可知的偏移处。

在那些允许嵌套子程序和静态作用域的语言,对象有可能出现在外围的子程序中,通过维护一个静态链就可以找到这些既非局部也非全局的对象。每个栈帧都包含一个对词法上位于其外围的帧的引用

调用序列

维护子程序调用栈是调用序列的责任。所谓调用序列就是由调用方紧接着子程序子程序调用和之后执行的代码。

在进入自称的过程中需要完成很多工作,包括出艾迪参数,保存返回地址,修改程序计数器,修改栈指针以分配空间,保存那些维护着重要的值但是可能被子程序改写的寄存器等等等

寄存器的保存和恢复

许多处理器调用序列都是将并非为特殊用途而保留的寄存器分为数目差不多的两组,其中一组由调用方负责,另一组由被调用方负责。

静态链的维护

在有嵌套子程序的语言中,至少有一部分静态链维护工作必须由调用方完成,而不能由被调用方完成

被调用直接嵌套在调用方内,在这种情况下,被调用方的静态链应该直接引用调用方的栈帧

被调用方在k>=0作用域之外,更接近词法嵌套的外层,在这种情况下,所有围绕着被调用方的作用也围绕着调用方。这时候调用方就对静态链做k次间接引用,将结果送给被调用方做静态链

典型的调用序列

一般的调用序列调用方可以按如下的方式操作:

保护起那些由调用方保存、其值在调用之后还需要的寄存器

计算出参数的值,并将它们移入栈或者寄存器中

计算出静态链,将它作为一个隐含的参数传递

执行一条特殊的子程序调用指令跳进子程序,同时将返回地址放入栈或某个寄存器中

被调用方的前序操作则是:

分配一个帧,也就是将sp指针减去某个适当的常数

将原来的栈指针保存在栈中,并给帧指针赋以适当的新值

保存那些由被调用方负责,而且在当前子程序中可能被复写的寄存器

在子程序完成之后的后序操作:

如果有返回值,则将返回值移入某个寄存器或栈中的某个保留位置

根据需要恢复被调用方保存的寄存器

恢复fp和sp

跳回到返回地址

最后调用方则可以:

将返回值移入需要它的位置

根据需要恢复调用方保存的寄存器

内联展开

作为基于栈的调用方式的一种替代,许多语言实现中还允许将特定子程序在调用的位置内联展开。被调用子程序的副本成为调用方的一部分;没有任何实际子程序调用发生。

在C中可以由程序员来指示是否建议将某些子程序内联化

inline int max(int a, int b) { return a > b ? a : b;}

但是与真正的子程序调用相比,内联展开的一个明显缺点就是增加了代码量

参数传递

大多数子程序都是参数化的,它们将得到一些参数,这些参数或控制着子程序行为的某些特定方面,或指定子程序需要来操作的数据。

参数模式

之前提了一下实参传递,以及明确实参与形参关系的语义规则。有些语言定义了唯一一组规则,适用于所有参数,这样的语言包括了C 、Fortran和Lisp,其它一些语言则提供了两组或更多组不同的规则

对于f(x)我们有两种实现方式,

可以为f提供一个x的副本

直接将x的地址传递给f

这两种最基本的参数传递模式分别称为值调用和引用调用,它们的设计反映了它们的实现方式

值调用和引用调用在使用值模型的语言的语言中最有意义。在使用引用模型的语言中,变量本身已经是对象的引用,这两种模型实际上都没有意义。

在Java中,内部类型使用值调用,而用户定义类型使用引用模型。相对的是C#中使用的是值调用,但是可以通过显式的关键字来使用引用传递。

闭包作为参数

闭包(对一个子程序的引用,再加上该子程序的引用环境)也会因为某些原因需要作为参数传递。最明显的原因就是当参数被声明为子程序时。

在子函数式语言中,子程序往往是作为参数传递的,并作为结果返回

在面向对象语言中,虽然没有嵌套子程序,但是也可以模仿子程序闭包的行为,方法是将与一个方法和它的环境打包在一个显示的对象里,

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zgyzff.html