Object Lifetime and Storage Management

在考虑标识符和绑定(bindings)的时候, 关键在于区分标识符和它们所引用的对象, 以及以下事件: 对象的创建 绑定的创建 所有使用绑定的情况, 诸如引用变量, 子程序, 类型等 停用和重用那些暂时没有用的绑定 绑定的析构(destruction) 对象的析构 绑定的生命周期指的是这个绑定从创建到析构的整个过程, 类似的可以定义对象的生命周期. 通常情况下, 一个对象的生命周期可能会大于对应的绑定的生命周期, 即当 标识符不再引用该对象时, 该对象依然存在(例如在子程序中传入某个对象的引用, 如C++中的&参数). 当然, 一个绑定的生命周期也有可能大于对应对象的生命周期, 虽然这通常被认为是一个BUG. 对象的生命周期通常与以下内存分配(storage allocation)机制有关: 静态(static)对象会在程序的整个运行过程中被分配一个绝对地址. 栈(stack)对象随着子程序的调用和返回而被创建以及按照LIFO的顺序析构. 堆(heap)对象可以在任何时候创建和析构, 其额外要求更加通用和昂贵的内存分配算法. 静态分配(static allocation) 静态分配最明显的例子就是全局对象, 当然全局对象不是唯一的例子. 构成程序机器语言翻译的指令也可以认为是静态分配的变量; 数字和字符串的常量当然也是静态分配 的; 另外很多编译器会产生一系列的表用于支持运行时的debug, 动态类型检查, 垃圾回收, 异常处理等, 这些表也都是静态分配的. 静态分配的对象通常希望它们的值 不在变化, 因此经常被分配在被保护的只读的内存中以方便在试图修改其值产生中断并抛出运行时错误. 在很多语言中,一个具名常量通常要求有一个能够在编译期确定的初始值. 通常这些初始值都被限制在那些已知的常量以及内置的函数. 这些具名常量加上字面常量通常被 叫做表现常量(manifest constants)或者编译期常量(compile-time constants). 在某些语言中(C, Ada), 常量仅仅只是那些无法在elaboration time之后改变的值, 这些值可能依赖与其他在运行时才能确定的值. 这些elaboration-time的常量在作为递归函数的局部变量时必须要分配在栈上. C#显示提供了 声明两种常量的方法, 即const和readonly关键字. 另外编译器通常对于子程序的某些值采用特定的分配策略: 参数和返回值, 编译器通常会尽可能的将这些值存在寄存器中. 临时变量, 通常是那些复杂计算过程的中间值, 一个优秀的编译器也会将它们保存在寄存器内. bookkeeping information, 这些通常包含子程序的返回地址. 基于栈的分配(stack-based allocation) 一门语言如果想要支持递归, 那么在局部变量上采用静态分配的策略将不再适用(Fortran90之前不支持递归), 因为需要变量的个数是未知的. 所幸的是递归天然地适用 于栈结构的分配策略. 每一个子程序在运行时都有一个栈帧(frame, 或者称为活动记录, activation record), 包含了传入参数, 局部变量, 临时变量, 以及 bookkeeping信息. 通常传入参数位于帧的顶部, 方便被调用者定位参数, 而其他的布局则依赖于实现. 栈的维护是子程序调用序列的责任, 即调用者在调用前(序言, prologue)和调用后(尾声, epilogue)执行的代码. 通常有一个帧指针(frame pointer)来保存当前帧的地址, 在大多数语言的实现中, 栈都是往地址减 小的方向增长的. 在这样的实现方式下, 局部变量, 临时变量, bookkeeping信息对于帧指针有一个负的偏移, 而传入参数和返回对于帧指针则有一个正的偏移, 因为 这些都保存在调用者的帧上. ...