编译原理 6:编译器后端
作用域
所谓「x
的作用域」是这样的一段区域,在该区域中,x
的引用均指向 x
的这一声明。如果一种程序设计语言,在编译时就能确定某个声明的作用域,称此语言使用静态作用域;反之,作用域在运行时才能确定的语言,称为使用动态作用域。
过程的传参方式
这里的「过程」包含「函数」「过程」和「方法」。出现在过程定义中的某些标识符具有特殊的意义,称为该过程的形式参数(形参)。在调用过程时,真正传入过程的称为实在参数(实参)。根据过程获得实参方式的不同,分为下面几种方式:
传值
将实参的值计算出来后单向、确定地传入。例如下面的 swap()
函数
void swap(int a, int b) {
int t = a;
a = b;
b = t;
}
并不能起到交换变量的作用:因为 a
和 b
的值是单向传入 swap()
函数体的,交换它们并没有交换函数调用外的实际变量。
传地址
将实参的地址以传值的方式传入。例如 C 语言中使用指针实现的 swap()
函数
void swap(int *a, int *b) {
int t = *a;
*a = *b;
*b = t;
}
就能实现函数外变量的交换,因为函数内能通过地址访问到实际的参数。
传值结果
将上面两种方式结合,比较少见。具体来说,调用前将实参计算并以传值方式传入,但同时也把地址传入。过程结束时,再将形参复制回实参位置。此种文法的效果上和传地址类似,都能使得过程外变量的值改变。
传名
传名是一种特殊的传递参数的方法,它将过程体内对形参的引用「替换」为实参。例如,在 Scala 中定义一个传名引用的函数:
def func(x: => Int): Unit = {
println(x)
println(x)
}
现在调用 func(a + b + 2)
,这就相当于执行了两次 println(a + b + 2)
。如果定义一个函数将某全局变量自增再返回:
def increaseAndReturn(): Int = {
inc = inc + 1
inc
}
调用 func(increaseAndReturn())
,会等价于执行 println(increaseAndReturn())
两次,造成 inc
自增 2 次。
运行时存储组织
内存分段
现代操作系统使用虚拟内存技术管理进程的内存使用。因此,对于程序来说,其可见的是一整段连续地址空间。在典型的操作系统上,程序的内存空间分为这些段:
其中:
目标代码段(
.text
)是编译后的程序码所在的区域。静态数据段包含大小确定的数据对象。在 UNIX 系统上,它可以进一步分为只读已初始化段
.rodata
(如常量)、可变已初始化段.data
(如已初始化的全局变量)、未初始化段.bss
(如未初始化全局变量)等。栈区:管理过程的活动。
堆区:可自由申请和释放的空间。
局部数据的组织
多数时候,机器更愿意进行自然访问——即,对于占用 \(n\) 个字节的变量,在 \(n\) 字节对齐的地址上访问它。为了实现自然访问,机器会在排布内存时留下一些「空洞」,这就是局部数据的组织。
存储分配策略
静态存储分配(Fortran):编译时就确定好所有存储空间的分配,不允许递归调用,不允许动态建立数据结构。
栈式存储分配(C):通过运行栈记录活动,使用调用序列和返回序列维护调用状态,在栈中存储局部数据和临时变量。
指令开销
x86 体系结构的特色产品。指令本身开销是 1,多一个立即数再多 1 份开销。
本节笔记到此结束。