第6章 执行期语意学
6.1 对象的构造和析构
constructor和destructor的安排
{
if (cache)
// 检查cache; 如果温和就传回1
return 1;
Point point;
// constructor在这里行动
switch(int(point.x())) {
case -1:
// numble;
// destructor在这里行动
return;
case 0:
// numble;
// destructor在这里行动
return;
case 1:
// numble;
// destructor在这里行动
default:
// numble;
// destructor在这里行动
return;
}
}
另外也很有可能在这个区段的结束符号(右大括号)之前被生出来, 即使程序分析的结构发现绝不会进行到那里, 一般而言会把object尽可能放置在使用它的那个程序区段附近, 这么做可以节省非必要的对象产生操作或摧毁操作
全局对象
Matrix identity;
main() {
// identity必须在此处被初始化
matrix m1 = identity;
//...
return 0;
}
C++保证, 一定会在main()函数第一次用到identify之前, 把identify构造出来, 而在main()函数结束之前把identify摧毁掉. 像identify这样的所谓的global object如果有constructor和destructor的话, 我们说他需要静态的初始化操作和内存释放操作
C++程序中所有的global objects都被放置在程序的data segment中. 如果显式指定给它一个值, 此object将以该值为初值. 否则object所配置到的内存内容为0(这和C略有不同, C并不自动设定初值). 在C语言中一个global object只能够被一个常量表达式(可在编译时期求其值的那种)设定初值. 当然constructor并不是常量表达式. 虽然class object在编译时期可以被放置于data segment中并且内容为0, 但constructor一直要到程序启动(startup)时才会实施. 必须对一个"放置于program data segment中的object的初始化表达式"做评估(evaluate), 这正是为什么object需要静态初始化的原因.
我的理解是data segment(包括object)中的值全为0, 只是在main函数执行时, 设定了值(object 执行constructor操作)
当cfront还是唯一的C++编译器, 而且跨平台移植性比效率的考虑更重要的时候,有一个可移植但成本颇高的静态初始化(以及内存释放)方法, 称为munch策略:
- 为每一个需要静态初始化的文件产生一个_sti()函数, 内含必要的constructor调用操作或是inline expansions. 例如起前面所说的identify对象会在matrix.c中产生出下面的_sti()函数(可能是static initialization的缩写):
__sti__matrix_c__identity() {
// C++伪码
identify.Matrix::Matrix; // 这就是static initialization
}
- 在每一个需要静态的内存释放操作的文件中, 产生一个__std()函数(可能是static deallocation的缩写), 内含必要的destructor调用操作, 或是其inline expansions
- 提供一组runtime library "munch"函数: 一个_main()函数(用以调用可执行文件中的所有__sti()函数), 以及一个exit()函数(以类似方式调用所有的_std()函数)
cfront 2.0版之前并不支持nonclass object的静态初始化操作; 也就是说C语言的限制仍然残留着. 所以下面的每一个初始化操作都被标记为不合法:
extern int i;
// 全部都要求静态初始化(static initialization)
// 在2.0版之前的C和C++中, 这些都是不合法的
int j = i;
int *pi = new int(i);
double sal = compute_sal(get_employee(i));
使用被静态初始化的objects, 有下列缺点:
(1) 如果exception handling被支持, 那些objects将不能够被放置于try区段之内. 这对于被静态调用的constructors可能是特别无法接受的, 因为任何的throw操作将必然触发exception handling library默认的terminate()函数
(2) 为了控制"需要跨越模块做静态初始化"的objects的相依顺序, 而扯出来的复杂度
作者建议根本就不要用那些需要静态初始化的global objects(虽然这项建议几乎普遍不为C程序员所接收)
局部静态对象
const Matrix& identity() {
static Matrix mat_identity;
// ...
return mat_identity;
}
- mat_identity的constructor必须只能实施一次, 虽然上述函数可能被调用多次
- mat_identify的destructor必须只能实施一次, 虽然上述函数可能会被调用多次
编译器的策略之一就是, 无条件地在程序起始(startup)时构造出对象来. 然而这会导致所有的local static class objects都在程序起始时被初始化, 即使它们所在的那个函数从不曾被调用过
实际上identify()被调用时才把mat_identity构造起来是一种更好的做法, 现在的C+标准已经强制要求这一点
cfront的做法: 首先导入一个临时性对象以保护mat_identity的初始化操作. 第一次处理identify()时, 这个临时对象被评估为false, 于是constructor会被调用, 然后临时对象被改为true. 这样就解决了构造的问题. 而在相反的那一端, destructor也需要有条件地施行于mat_identity身上, 但只有mat_identity已经被构造起来才算数, 可以通过那个临时对象是否为true来判断mat_identity是否已经构造
对象数组
Point knots[10]; // 没有明显初值
如果Point没有定义一个constructor也没有定义一个destructor, 那么上面代码所执行的工作不会比"内建(build-in)类型所组成的数组"更多(即不会调用下面所要讲到的vec_new()), 也就是说我们只要配置足够内存以存在10个连续的Point元素即可
然而Point的确定义了一个default destructor, 所以这个destructor必须轮流施行于每一个元素之上. 一般而言这是经由一个或多个runtime library函数达成的. 在cfront中, 使用一个被命名为vec_new()的函数, 产生出以class objects构造而成的数组. (比较新近的编译器, 则是提供两个函数, 一个用来处理"没有virtual base class"的class, 另一个用来处理"内含virtual base class"的class, 后一个函数通常被称为vec_vnew), vec_new()类型通常如下:
void* vec_new(
void *array, // 数组起始地址
size_t elem_size; // 每一个class object的大小
int elem_count; // 数组中的元素个数
void (*constructor)(void *),
void (*destructor)(void *, char)
)
- constructor是class的default constructor的函数指针
- destructor是class的default destructor的函数指针
- array持有的若不是具名数组(本例中为knots)的地址, 就是0. 如果是0, 那么数组将经由应用程序的new运算符, 被动态配置于heap中
- 在vec_new()中, constructor施行于elem_cout个元素之上
下面是编译器可能对10个Point元素所做的vec_new()调用操作:
Point knots[10];
vec_new(&knots, sizeof(Point), 10, &Point::Point, 0);
如果Point也定义了一个destructor, 当knots的生命结束时, 该destructor也必须是施行于那10个Point元素身上. 这是经由一个类似的vec_delete()(或是一个vec_vdelete(), 如果classes拥有virtual base classes的话)的runtime library函数完成, 函数类型如下:
void* vec_delete(
void *array,
size_t elem_size,
int elem_count,
void (*destructor)(void *, char)
)
如果程序员提供一个或多个明显初值给一个由class objects组成的数组, 像下面这样:
Point knots[10] = {
Point(),
Point(1.0, 1.0, 0.5),
-1.0
};
对于那些明显获得初值的元素, vec_new()不再有必要,对于那些尚未被初始化的元素, vec_new()的施行方式就行面对"由class elements组成的数组, 而该数组没有explicit initialization list"一样. 因此上一个定义很可能被转换为:
Point knots[10];
// C++伪码
// 显示地初始化前3个元素
Point::Point(&knots[0]);
Point::Point(&knots[1], 1.0, 1.0, 0.5);
Point::Point(&knots[2], -1.0, 0.0, 0.0);
// 以vec_new初始化后7个元素
vec_new(&knots+3, sizeof(Point), 7, &Point::Point, 0);
Default Constructors和数组
如果想要在程序中取出一个constructor的地址, 是不可以的. 当然, 这是在编译器支持vec_new()时该做的事情. 然而, 经由一个指针来启动constructor, 将无法(不被允许)存取default argument values
举个例子, 在cfront2.0之前, 声明一个由class objects所组成的数组, 意味着这个class必须没有声明constructs或一个default constructor(没有参数那种)----> 有还是没有一个default constructor(没有参数那种)???. 一个constructor不可以取一个或一个以上的默认参数值. 这违反直觉的, 会导致以下的大错
class complex {
complex(double = 0.0, double = 0.0);
};
在当时的语言规则下, 此复数函数库的使用者没办法声明一个由complex class objects组成的数组.
我的理解是在2.0版本之前, 这样带有默认参数的构造函数无法区分无参构造函数
然而在2.0版, 修改了语言本身, 为支持句子:complex::complex(double = 0.0, double = 0.0)
, 当程序员写出complex c_array[10]
时, 而编译器最终需要调用vec_new(&c_array, sizeof(complex), 10, &complex::complex, 0);
, 默认的参数如何能够对vec_new()而言有用?
cfront所采用的方法是产生一个内部的stub construct, 没有参数. 在其函数内调用由程序员提供的constructor, 并将default参数值显式地指定过去(由于construct的地址已经被取得, 所以它不能够成为一个inline):
// 内部产生的stub constructor
// 用以支持数组的构造
complex::complex() {
complex(0.0, 0.0);
}
编译器自己又一次违反了一个明显的语言规则: class如今支持了两个没有带参数的constructs. 当然, 只有class objects数组真正被产生出来时, stub实例才会被产生以及被使用