程序设计语言基础
动态和静态的区别
- 在为一个语言设计一个编译器时,面对的重要问题是编译器能够对一个程序作出哪些判断
- 静态(static)策略,或者编译时刻(compile time)决定,使用的策略支持编译器静态决定某个问题
- 动态(dynamic)策略,或者运行时刻(run time)决定,一个只允许在运行程序的时候做出决定的策略
- 静态作用域(dynamic scope),或者词法作用域(lexical scope),通过阅读程序就可以确定一个声明的作用域
- 动态作用域(dynamic scope),当程序运行时,同一个对x的使用会指向x的几个声明中的某一个
例1.3
Java中的,一个变量是用于存放数据值的某个 内存位置 的名字
"static"指的并不是变量的作用域,编译器确定用于存放被声明变量的内存位置的 能力
public static int x;
- 使得x成为一个类变量(class variable),也就是说不管创建了多少个这个类的对象,只存在一个x的拷贝
- 编译器可以确定内存中的被用于存放整数x的位置
- 如果这个声明忽略了"static",那么这个类的每个对象都会有它子集的用于存放x的位置,编译器无法运行程序之前预先确定所有这些位置
环境与状态
- 程序运行时的改变是否影响数据元素的值
x = y + 1
这样赋值语句会改变x所指的值,这个赋值改变了x所指向的内存位置上的值
- 如果x不是一个静态变量,那么这个类的每一个对象都有它自己的分配给变量x的实例的位置
- 对x的赋值可能会改变那些"实例"变量中的某一个变量的值,取决于包含这个赋值的方法作用于哪个对象
名字和内存(存储)位置的关联,及之后和值的关联可以用两个映射来描述
环境(environment)
- 从一个名字到存储位置的映射
- 变量就是指内存位置,环境定义为从名字到变量的映射
状态(state)
- 从内存位置到它们的值的映射
- 以C语言的术语来说,状态把左值映射为它们的相应右值
名字到位置的绑定和从位置到值得绑定
-
名字到位置静态绑定与动态绑定,大部分从名字到位置的绑定是动态的
- 例题1.4中声明可以在编译器生成目标代码时一劳永逸地分配一个存储位置
- 从位置到值得静态绑定与动态绑定,一般来说位置到值的绑定也是动态的,我们无法在运行一个程序之前指出一个位置上的值
例1.4
... int i; // 全局 i ... void f(...){ // 局部 i int i; ... i = 3; // 对局部 i 的使用 ... } ... x = i + 1; // 对全局 i 的使用
- 在上述程序片段中,整数 i 被声明为一个全局变量,同时被声明为局部于函数 f 的变量
- 执行f 时,环境相应地调整,使得名字 i 指向那个为局部于f 的那个i所保留存储位置,且 i 的所有使用都指向这个位置
- 局部的i 通常被赋予一个运行时刻栈中的位置
静态作用域和块结构
块(block),是声明和语句的一个组合
例1.5
- 一个C程序由一个顶层的变量和函数声明的序列组成
- 函数内部可以声明变量,包括局部变量和参数,每个这样的声明的作用域被限制在它们出现的那个函数内
- 名字x的一个顶层声明的作用域包括其后的所有程序
- 但是如果一个函数中也有一个x的声明,那么函数中的那些语句就不在这个顶层声明的作用域内
C语言中的块
- 块是一种语句,可以出现在其他类型的语句所能够出现的任何地方
- 一个块包含了一个声明的序列,然后再跟着一个语句序列
- 这个语法允许一个块嵌套在另一块内,这个嵌套特性称为块结构(block structure)
例1.6
main(){ int a = 1; int b = 1; { int a = 3; cout << a << b; } { int b = 4; cout << a << b; } cout << a << b; }
显式访问控制
- 类和机构为它们的成员引入了新的作用域
- 若p是一个具有字段x的类的对象,那么p.x 中对x的使用指的是这个类定义中的字段x
- 和块结构类似,类C中的一个成员声明x的作用域可以扩展到所有的子类C',除非C'有一个本地的对同一名字x的声明
通过public, private 和 protected 这样的关键字的使用,像C++或Java这样的面向对象语言提供了对超类中的成员名字的显式访问控制
这些关键字通过限制访问来支持封装(encapsulation)
- 私有(private)名字被有意地限定了作用域,这个作用域仅仅包含了该类和"友类"
- 被保护(protected)名字可以由子类访问
- 公共(public)名字可以从类外访问
动态作用域
- 如果一个作用域策略依赖于一个或多个只有在程序执行时刻才知道的因素,它就是动态的
- 动态作用域指的是,对一个名字x的使用指向的是最近被调用但还没有终止且声明的x的过程中的这个声明
- 这种类型的动态作用域仅仅在一些特殊情况下才会出现
C预处理器中的宏拓展
#define a (x + 1) int x = 2; void b() {int x = 1; printf("%d ", a);} void c() {printf("%d ", a);} void main(){b(); c();}
- 标识符a是一个代表了表达式(x + 1)的宏
- x是什么呢,我们不能静态地解析x
- 选择最近调用的且具有一个对x的声明的函数
- 函数 void b(),打印的值是2
- 函数 void c(),打印的值是3
- 动态作用域解析对堕胎过程是必不可少的
- 所谓多态过程是指对同一个名字根据参数类型具有两个或多个定义的过程
例1.8
面向对象语言的一个突出特征就是每个对象能够对一个消息做出适当反映,调用相应的方法
换句话说,执行 x.m()时调用哪个过程要由当时x所指向的对象的类来决定,一个典型的例子如下
- 有一个类C,它有一个名字为m()的方法
- D是C的一个子类,而D有一个它自己的名字为m()的方法
- 有一个形如x.m()的对x的使用,其中x是类C的一个对象
正常情况下,编译时刻不可能指出x指向的是类C的对象还是其子类D的对象
- 只有到了运行时刻才可能决定应当调用m的哪个定义,编译器生成的代码必须决定对象x的类,并调用其中的某一个名字为m的方法
参数传递机制
- 实在参数如何与形参关联起来
- 使用哪一种传递机制决定了调用代码序列如何处理参数
- 大多数语言要么使用"值调用",要么使用"引用调用",或者二者都用
值调用
- 在值调用(call-by-value)中,会对实在参数求值(如果它是表达式)或拷贝(如果它是变量)
- 这些值被放在属于被调用过程的相应形式参数的内存位置上
- C、C++和Java中作为参数传递的数组名字实际上向被调用过程传递了一个指向该数组本身的指针或引用
- 若a是调用过程的一个数组的名字,且它被以值调用的方式传递给相应的形式参数x,像 x[2]=i,这样的赋值语句实际上改变了数组元素a[i]
- 原因是虽然x是a的值的一个拷贝,但这个值实际上是一个指针,指向被分配给数组a的存储区域的开始处
- Java中的很多变量实际上是对它们所代表的事物的引用,或者说指针
- 这个结论对数组、字符串和所有类的对象都有效,虽然Java只使用值调用,但只要我们把一个对象的名字传递给一个被调用过程,那个过程的值实际上是这个对象的指针
- 因此被调用过程是可以改变这个对象本身的值的
引用调用
- 在引用调用中(call-by-reference)中,实在参数的地址作为相应的形式参数的值被传递给被调用者
- 使用形参时,实现方法是沿着这个指针找到调用者指明的内存位置,因此改变形式参数就像是改变了实在参数一样
- 如果实在参数是一个表达式,那么在调用之前首先会对表达式求值,然后它的值被存放在一个该值自己的位置上
- 改变形式参数会改变这个位置上的值,但对调用者的数据没有影响
- C++中的"ref"参数使用的是引用调用
- 当形式参数是一个大型的对象、数组或结构时,引用调用几乎必不可少,原因是严格的值调用要求调用者把整个实在参数拷贝到属于相应形式参数的空间上
- 当参数很大时,这种拷贝可能代价高昂
- Java运行时,对所有不是基本类型的参数都用了引用调用
名调用(已经抛弃)
别名
引用调用或者其他类似的方法,比如像Java中那样把对象的引用当作值传递,可能两个形式参数指向同一个位置,这样的变量称为另一个变量的别名(alias)
例1.9
- 假设a是一个属于某个过程p的数组,且p通过调用语句q(a, a)调用了另一个过程q(x, y)。再假设像C语言或类似的语言那样,参数是通过值传递的,但数组名实际上是指向数组存放位置的引用
- 现在x和y变成了对方的别名,要点在于,如果q中有一个赋值语句x[10]=2,那么y[10]的值也是2
如果编译器要优化一个程序,要理解别名显现以及产生这一现象的机制