6.3 非局部名字的访问
在编程语言中,处理在当前作用域之外声明的变量或名字的能力是理解作用域规则的核心。这些名字被称为非局部名字。本节将详细探讨在不同作用域规则下,如何访问这些非局部名字。
6.3.1 无过程嵌套的静态作用域
静态作用域,也称为词法作用域,根据程序代码的字面结构来确定名字的作用域。在不支持过程(函数)嵌套的语言中,如C语言,这种作用域规则的实现相对简单直接。
C语言的静态作用域
C语言的设计不允许函数内部再定义函数,这意味着所有的非局部名字引用都必须指向在函数外部定义的全局变量。这些全局变量的作用域从它们声明的位置开始,延伸至文件末尾,除非在某个函数内部有相同名字的局部变量被声明,这时局部变量会隐藏同名的全局变量。
全局变量的存储位置在编译时确定,因此,在任何函数内对这些全局变量的引用都可以直接通过它们的静态地址来实现。这种方法简化了编译器的设计,同时也提高了程序的运行效率,因为访问全局变量的地址不需要在运行时解析。
静态作用域的好处
在无过程嵌套的静态作用域下,程序中的过程(或函数)可以作为参数传递给其他函数,或作为结果返回。这是因为在这种作用域规则下,过程中引用的任何非局部名字都具有全局作用域,其地址在所有过程中都是可用的。这种设计在C语言中表现为函数指针的使用,允许高度的模块化和函数间的灵活交互。
通过探讨C语言中的静态作用域实现,我们可以深入理解作用域规则对程序设计和语言特性的影响。在下一节中,我们将讨论支持过程嵌套的语言如何处理非局部名字的访问,并比较其与无过程嵌套语言的不同之处。
6.3.2 有过程嵌套的静态作用域
在支持过程嵌套的语言中,静态作用域规则允许在一个过程内部定义另一个过程,形成了一个嵌套的作用域层次。这种结构对于理解如何访问非局部名字至关重要。Pascal语言是支持过程嵌套并采用静态作用域规则的一个典型例子。
Pascal语言中的静态作用域
在Pascal中,如果一个过程p
中没有对名字a
的声明,那么适用于p
中a
出现的声明位于p
的某个外围过程q
中。这种情况下,q
中对a
的声明从静态代码结构上看比任何其他a
声明更靠近p
中的这个a
出现。
为了具体说明,考虑一个将快速排序算法用Pascal语言重写的例子。在这个例子中,partition
函数嵌套在quickSort
过程内部,并且引入了一个exchange
过程。这种过程声明的嵌套结构通过一个阶梯图表示,其中partition
函数中的某个变量a
的最近的外部声明位于程序的全局作用域中,即使该变量在partition
函数内部被引用。
实现有过程嵌套的静态作用域
在实现静态作用域时,引入了“过程嵌套深度”的概念,以及一种称为“访问链”的机制来处理非局部名字的访问。访问链是每个活动记录中的一个指针,它指向当前过程的直接外围过程的活动记录。这种机制允许运行时系统沿着访问链向上查找,以定位非局部变量的声明所在的活动记录。
例如,如果一个过程p
的嵌套深度为n_p
,并且它引用了一个嵌套深度为n_a
的变量a
(n_a <= n_p
),那么可以通过追踪访问链n_p - n_a
次来找到包含变量a
的活动记录。这种查找机制确保了即使在深层嵌套的情况下,也能正确地访问到非局部名字。
访问链的建立
当一个过程被调用时,其活动记录的访问链需要被正确设置以反映嵌套结构。如果被调用的过程嵌套在调用过程之内,则其访问链直接指向调用过程的活动记录。如果嵌套深度大于或等于调用过程,根据静态嵌套关系,通过追踪调用过程的访问链可以找到正确的活动记录,进而建立被调用过程的访问链。
过程作为参数传递
当过程作为参数传递时,其访问链也需要一并传递,以确保在新的调用上下文中仍能正确访问非局部名字。这种机制允许在Pascal等语言中灵活地使用高阶函数,同时保持静态作用域规则的一致性和正确性。
通过这种复杂的机制,支持过程嵌套的静态作用域语言能够处理非常复杂的作用域和命名问题,同时为程序员提供了强大的编程工具。这种作用域规则在程序设计中提供了额外的灵活性,允许更紧密地封装功能和数据,促进了模块化和重用。
*6.3.3 动态作用域
动态作用域是一种与静态作用域截然不同的名字解析规则。在动态作用域下,名字的绑定取决于程序的调用顺序而非其在代码中的位置。这意味着一个变量的值由最近的活动调用上下文决定。
动态作用域的特性
在动态作用域规则下,当一个过程q
被另一个过程p
调用时,q
中的非局部名字a
会绑定到p
中对应名字a
的存储单元。这种绑定方式确保了q
在执行时,对a
的引用与p
中对a
的绑定是一致的。换句话说,q
对a
的访问实际上访问的是p
中a
的值。
动态作用域的这一特性导致了其与静态作用域的主要区别:名字的作用域是在运行时动态决定的,而非在编译时静态确定。这种机制允许函数根据当前的调用栈来解析非局部变量,从而能够访问到最近调用过程中的变量。
动态作用域的实例
考虑一个简单的示例程序,该程序展示了动态作用域如何影响变量的解析。假设有两个过程show
和small
,其中show
用于输出变量r
的值,而small
则设置r
的值并调用show
。根据动态作用域的规则,show
输出的r
值将取决于调用show
时r
的最近一次绑定。
在静态作用域下,变量r
的值总是由它在代码中的声明决定,因此无论如何调用show
,输出的都是同一个r
的值。而在动态作用域下,show
的输出会根据最近的r
值变化而变化,即显示调用show
时r
的当前值。
动态作用域的实现
动态作用域可以通过两种主要方式实现:
-
深访问(Deep Access):这种方法通过搜索运行时的调用栈来实现动态作用域。搜索从当前活动记录开始,沿着调用链向上查找,直到找到包含所需非局部名字的第一个活动记录。
-
浅访问(Shallow Access):在这种方法中,程序中的每个名字都在静态数据区分配一个空间,用于保存其当前值。当一个新的过程被调用时,它的局部变量使用这些预分配的存储单元。过程结束时,原先的值会被恢复。
比较深访问和浅访问
- 深访问优点是它直接映射了程序的调用结构,但可能需要较长的时间来访问非局部名字,因为需要遍历调用栈。
- 浅访问则提供了更快的非局部名字访问时间,但在过程的入口和出口处需要额外的工作来保存和恢复变量值。
动态作用域提供了一种强大的机制,允许函数根据其被调用的上下文动态解析非局部变量。这种灵活性在某些编程场景中非常有用,尤其是在需要根据调用历史动态改变行为的情况下。然而,它也可能导致代码难以理解和维护,因为变量的作用域不再是由其在代码中的位置静态决定的。