18 第1章 2) 一个块包含了一个声明的序列,然后再跟着一个语句序列。这些声明和语句用一对括号 包围起来。 注意,这个语法允许一个块嵌套在另一个块内。这个嵌套特性称为块结枸(block structure)。 C族语言都具有块结构,但是不能在一个函数内部定义另一个函数 如果块B是包含声明D的最内层的块,那么我们说D属于B。也就是说,D在B中,且不在 嵌套于B中的任何其他块中。 在一个块结构语言中,关于变量声明的静态作用域规则如下。如果名字x的声明D属于块 B,那么D的作用城包括整个B,但是以任意深度嵌套在B中、重新声明了x的所有块B不在此 作用域中。这里,x在B'中重新声明是指存在另一个属于B'的对相同名字x的声明D'。 另一个等价的表达这个规则的方法着眼于名字x的一次使用。设B,B,…,B:是所有的 句令了x的该次使用的块。其中,B:嵌套在B:,中,B,,嵌套在B:,中,·,依此类推。寻找 最大的满足下面条件的:存在一个属于B,的x的声明。本次对x的使用就是指向B:中对x的声 明。换句话说,x的本次使用在B,中的这个声明的作用域内。 例16在图1-0中的C+程序有四个块,其中包含了变量a和b的几个定义。为了帮助记 忆,每个声明把其变量初始化为它 所属于的那个块的编号。 比如,考虑块B,中的南明inta量1 B 它的作用域包括整个B,当然那些(可能很 B 深地)按套在B,中并且有它自已的对ā的 声明的块除外。直接嵌套在B,中的B,没 int a 3 cout cc a cc bi 有a的声明,而B就有。B,没有a的声 明。因此块B是整个程序中唯一位于名字 B a在B1中的声明的作用城之外的地方。也 就是说,这个作用域包括8和,中除了 couta<bi B1之外的所有部分。关于程序中的全部五 cout<abt 个声明的作用域的总结见图111。 从另一个角度看,让我们考虑块B4中 图1-10一个C+程序中的块结构 的输出语句,并把那里使用的变量a和b和 声明 作用域 适当的声明绑定。包含该语句的块的列表 int a=1: 从小到大是B。、B,、B,。请注意,B、没若 int b=1; B,-B 句含问题中所提到的点。B。有一个b的声 int b=2: B2 -Ba 明,因此该语句中对的使用被绑定到这个 int a3 B 声明,因此打印出来的6的值是4。然而 int b-4 B. B,没有的声明,因此我们接着看B2。这 个块也没有a的声明,因此我们继续看B:。 图111例1,6中的声明的作用域 幸运的是,这个块有一个声明inta=1。因此,打印出来的a的值是1。如果没有这个声明, 程序就是错误的。 1.6.4显式访问控制 类和结构为它们的成员引人了新的作用域。如果刀是一个具有字段(成员)x的类的对象,那 么在卫.x中对x的使用指的是这个类定义中的字段x。和块结构类似,类C中的一个成员声明x
引 论 19 的作用域可以扩展到所有的子类C',除非C有一个本地的对同一名字x的声明。 private和protected这样的关键字的使用,像C+或Java这样的面向对象语言 提供了对超类中的成员名字的显式访问控制。这些关键字通过限制访问来支持封装(encapsula ion)。因此,私有(private)名字被有意地限定了作用战,这个作用域仅仅句含了该类和“友类 (C+的术语)相关的方法声明和定义。被保护的()名字可以由子类访问,而公共的 (public)名字可以从类外访问。 在C++中,一个类的定义可能和它的部分或全部方法的定义分离。因此对于一个和类C相 关联的名字x,可能存在一个在它作用城之外的代码区域,然后又跟着一个在它作用域内的代码 区域(一个方法定义)。实际上,在这个作用域之内和之外的代码区域可能相互交替,直到所有 的方法都被定义完毕。 声阴和定义 程序设计语言概念中的两个看起来相似的术语“声明”和“定义”"实际上有着很大的不同。声 明告诉我们事物的类型,而定义告诉我们它们的值。因此.i北1是一个的声明.而1=1是 的一个定义(定值) 当我们处理方法成者其他过程时,这个区别就更加明显。在C++中,通过给出了方法 的参数及结果的类型(通常称为该方法的范型),在类的定义中声明议个方法。然后,这个 方法在另一个地方被定义,即在另一个地方给出了执行这个方法的代码。类似地,我们会编 常看到在一个文件中定义了一个C语言的函数,然后在其他使用这个函数的文件中声明这 个函数。 1.6.5动态作用域 从技术上讲,如果一个作用域策略依赖于一个或多个只有在程序执行时刻才能知道的因素 它就是动态的。然而,术语动态作用城通常指的是下面的策略:对一个名字x的使用指向的是最 近被调用但还没有终止且声明了x的过程中的这个声明。这种类型的动态作用域仅仅在一些特 殊情况下才会出现。我们将考虑两个动态作用域的例子:C预处理器中的宏扩展,以及面向对象 编程中的方法解析 例1.7在图112给出的C程序中,标识符a是一个代表了表达式(x+1)的宏。但x到底是什 么呢?我们不能够静态地(也就是说通过程序文本)解析x。 实际上,为了解析x,我们必须使用前面提 #define a (x+1) 到的普通的动态作用域规则。我们检查所有当 int x2: 前活跃的函数调用,然后选择最近调用的且具 有一个对x的声明的函数口。对x的使用就是指 void c()print("Zd\n",a); 向这个声明。 void main()b();c(); 在图1-12的例子中,函数main首先调用函 数b。当b执行时打印宏a的值。因为首先必须 图1-12 一个其名字的作用域必须动态确定的宏 日这个规则可能只对当前的例子成立。如果将图1-l2的例子中的函数b改成void b()(int×=1:printf (~%da",al:c0:.那么当ma1n函数阀用函数&,函数b又调用e的时候,c中的printf(%dn,a)语句 依然打印值2。即此时对x的使用对应的仍然是全局的:,而不是按飘规则确定的函数,即“最近调用的且有 个对x的声明的函数”。 一译者注
20 第1章 用(x+1)替换掉a,所以我们把本次对x的使用解析为对函数b中的声明it×=1。原因是6有 一个x的声明,因此b中的printf中的(x+1)指向这个x。因此,打印出的值是2 在b运行结束之后,函数c被调用.我们依日需要打印宏的值。然而,难-可以被c访问 的x是全局变量x。函数c中的printf语句指向x的这个声明,且被打印的值是3。 动态作用域解析对多态时程是必不可少的。所调多态过程是指对于同一个名字根据参数 类型具有两个或多个定义的过程。在有些语言中,比如ML(见7,3.3节),人们可以静态地确 定名字所有使用的类型。在这种情况下,编译器可以把每个名字为的过程簪换为对相应的 过程代码的引用。但是,在其他语言中,比如在Jav和C++中,编译器有时不能够做出这样 的决定。 静态作用域和动态作用城的类比 虽然可以有各种各样的静态或者动态作用域策略,在通常的(块结构的)静态作用域规则 和通常的动态策略之间有一个有趣的关系。从某种意义上说,动态规则处理时闻的方式举 于静态作用域处理空闻的方式。静态规则让我们寻找的声明位于最内层的、包含变量使用位 置的单元(块)中;而动态规则让我们寻找的声明位于最内层的、包含了变量使用时间的单元 (过程调用)中。 例1.8面向对象语言的一个突出特征就是每个对象能够对一个消息做出适当反应,调用相应 的方法。换句话说,执行x,m()时调用哪个过程要由当时x所指向的对象的类来决定。一个典型 的例子如下: 1)有个类C.它有一个名字为m()的方法 2)D是C的一个子类,而D有一个它自已的名字为m()的方法。 3)有一个形如x,m()的对x的使用,其中x是类C的一个对象。 正常情况下,在编译时刻不可能指出x指向的是类C的对象还是其子类D的对象。如果这 个方法被多次应用,那么很可能某些调用作用在由x指向的类C的对像,而不是类D的对象,而 其他调用作用于类D的对象之上。只有到了运行时刻才可能决定应当调用m的哪个定义。因 此,编译器生成的代码必须决定对象x的类,并调用其中的某一个名字为m的方法 1.6.6参数传递机制 所有的程序设计语言都有关于过程的概念,但是在这些过程如何获取它们的参数方面,不同 的语言之间有所不同。在本节,我们将考虑实在参数(在调用过程时使用的参数)是如何与形式 参数(在过程定义中使用的参数)关联起来的。使用哪一种传递机制决定了调用代码序列如何处 理参数。大多数语言要么使用“值调用”,要么使用“引用调用”,或者两者都用。我们将解释这 些术语以及另一个被称为“名调用”的方法,解释后者主要是基于对历史的兴趣。 值调用 在值调用(call-by-valu©)中,会对实在参数求值(如果它是表达式)或拷贝(如果它是变量)。 这些值被放在属于被调用过程的相应形式参数的内存位置上。这种方法在C和Jav中使用,也 是C+语言及大部分其他语言的一个常用选项。值调用的效果是,被调用过程所做的所有有关 形式参数的计算都局限于这个过程,相应的实在参数本身不会被改变。 然而请注意,在C语言中我们可以传递变量的一个指针,使得该变量的值能够被被调用 者修改。同样,C、C+和Ja中作为参数传递的数组名字实际上向被调用过程传递了一 指向该数组本身的指针或引用。因此,如果ā是调用过程的一个数组的名字,且它被以值调用
引论 3 的方式传递给相应的形式参数x,那么像x(2]=i这样的赋值语句实际上改变了数组元素)」 原因是虽然x是:的值的一个拷贝,但这个值实际上是一个指针,指向被分配给数组的存储区 域的开始处。 类似地,J中的很多变量实际上是对它们所代表的事物的引用,或者说指针。这个结论对 数组、字符串和所有类的对象都有效。虽然Ja贝使用值调用,但只要我们把一个对象的名字传 递给一个被调用过程,那个过程收到的值实际上是这个对象的指针。因此,被调用过程是可以改 变这个对象本身的值的。 引用调用 在引用调用(cal-by-reference)中,实在参数的地址作为相应的形式参数的值被传递给被调用 者。在被调用者的代码中使用形式参数时,实现方法是沿着这个指针找到调用者指明的内存位 置。因此,改变形式参数看起来就像是改变了实在参数一样。 但是,如果实在参数是一个表达式,那么在调用之前首先会对表达式求值,然后它的值被 存放在一个该值自己的位置上。改变形式参数会改变这个位置上的值,但对调用者的数据没 有影响。 C++中的“参数使用的是引用调用。而在很多其他语言中,引用调用也是一种选项。当 形式参数是一个大型的对象、数组或结构时,引用调用几乎是必不可少的。原因是严格的值调用 要求调用者把整个实在参数拷贝到属于相应形式参数的空间上。当参数很大时,这种拷贝可能 代价高昂。正如我们在讨论值调用时所指出的,像a这样的语言解决数组、字符串和其他对象 的参数传递问题的方法是仅仅复制这些对象的引用。结果是,J运行时就好像它对所有不是基 本类型(比如整数、实数等)的参数都使用了引用调用。 名调用 第三种机制一名调用一被早期的程序设计语言Agl60使用。它要求被调用者的运行 方式好像是用实在参数以字面方式替换了被调用者的代码中的形式参数一样。这么做就好像形 式参数是一个代表了实在参数的宏。当然被调用过程的局部名字需要进行重命名,以便把它价 和调用者中的名字区别开来。当实在参数是一个表达式而不是一个变量时,会发生一些和直觉 不符的问顺。这也悬今天不鹿采用这种机制的原因之一 1.6.7别名 引用调用或者其他类似的方法,比如像Jaa中那样把对象的引用当作值传递,会引起一个有 趣的结果。有可能两个形式参数指向同一个位置,这样的变量称为另一个变量的别名(lias)。 结果是,任意两个看起来从两个不同的形式参数中获得值的变量也可能变成对方的别名。 例19假设a是一个属于某个过程p的数组,且p通过调用语句g(a,a)调用了另一个过程 9(x,y)。再假设像C语言或类似的语言那样,参数是通过值传递的,但数组名实际上是指向数 组存放位置的引用。现在,x和y变成了对方的别名。要点在于,如果9中有一个赋值语句 x[10]=2,那么y[10]的值也是2 事实上,如果编译器要优化一个程序,就要理解别名现象以及产生这一现象的机制。正如我 们从第9章看到的,在很多情况下我们必须在确认某些变量相互之间不是别名之后才可以优化程 序。比如,我们可能确定×=2是变量x唯一被赋值的地方。如果是这样,那么我们可以把对 的使用替换为对2的使用。比如,把a=×+3替换为较简单的ā=5。但是,假设有另一个变量) 是x的别名。那么,一个赋值语句y=4可能具有意想不到的改变x的值的效果。这可能也意味 着把a=×+3替换为a=5是一个错误,此时,a的正确值可能是7
22 第1章 1.6.81.6节的练习 练习1.6.1:对图1-13a中的块结构的C代码,指出赋给w、x、y和:的值。 名1+j: )练习161的代码 b)练习1.62的代到 图113块结构代码 练习1.6.2:对图1-13站中的代码重复练习1.6.1。 练习1.6.3:对于图1-14中的块结构代码,假设使 用常见的声明的静态作用域规则,给出其中12个声明 int v.x: 中的每一个的作用域。 练习1.6.4:下面的C代码的打印结果是什么? )) 图1-14练习1.63的块结构代码 o14aia0《b0:c0: 1.7第1章总结 ·语言处理器 一个集成的软件开发环境,其中包括很多种类的语言处理器,比如编译器 解释器、汇编器、连接器、加载器、调试器以及程序概要提取工具。 ·编译器的步骤:一个编译器的运作需要一系列的步骤,每个步骤把源程序从一个中间表 示转换成为另一个中间表示。 ·机器语言和江编语言:机器语言是第一代程序设计语言,然后是汇编语言。使用这些语 言进行编程既费时,又容易出错 ·编译器设计中的建模:编译器设计是理论对实践有很大影响的领域之一。已知在编译器 设计中有用的模型包括自动机、文法、正则表达式、树型结构和很多其他理论概念。 ·代马优化:虽然代码不能真正达到最优化,但提高代码效率的科学既复杂又非常重要 它是编译技术研究的一个主要部分。 。高领语★。随着时间的流浙,程序设计语言相负了越来越多的原先由程序员负责的任务 比如内存管理、类型一致性检查或代码的并发执行。 ·编译器和计算机体系结构:编译器技术影响了计算机的体系结构,同时也受到体系结构 发展的影响。体系结构中的很多现代创新都依赖于编译器能够从源程序中抽取出有效利 用硬件能力的机会。 。软件生产率和软件安全性:使得编译器能够优化代码的技术同样能够用于多种不同的程 序分析任务。这些任务既包括探测常见的程序错误,也包括发现程序可能会受到已被黑