前言
一般而言,constructor与destructor的安插恰如其分:1
2
3
4
5
6{
Point point;
//point.Point::Point() 插入构造
...
//point.Point::~Point() 插入析构
}
如果一个区段(以{}括起来的区域)或函数中有一个以上的离开点,情况又稍微混乱一些。Destructor必须被放在每一个离开点(当时object还处于存活状态)之前,例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15{
Point point;
switch(int(point.x())){
case -1:
//插入destructor
return;
case 0:
//插入destructor
return;
default:
//插入destructor
return;
}
//destructor在这里行动
}
在上述实例中会生成几个destructor,甚至会在区段结束前生成一个destructor——即使程序本身根本不可能执行到那里。那么同样地,goto指令可能也需要多个destructor调用操作。
我们倾向于把object放置在使用它的程序区段附近,这样通常可以节约不必要的构造与析构操作。
全局对象
假设当前存在如下程序段:1
2
3
4
5
6
7
8Matrix identity;
main(){
//identity必须被初始化
Matrix m1 = identity;
...
return 0;
}
C++保证,一定会在main()函数中第一次使用identity之前,将identity构造完毕,并且在main()函数结束之前把identity析构。全局对象如果存在constructor与destructor的话,我们一般称其需要静态的初始化操作与内存释放操作。
C++程序中所有的global objects都被置于程序的data segment中。如果明确指定给global object一个值,那么object将以该值为初始值,否则object所配置到的内存内容为0。这里和C语言存在一定的分歧,C语言不会给任何全局变量设定为0的初值。
我们可以认为,C++将global object在编译时期置于data segment中且内容为0,其constructor一直到程序startup时才会实施。我们必须对一个放置于program data segment中的object的初始化表达式做evaluate,这也是为什么一个object需要静态初始化的原因。
munch策略
cfront有一个执行静态初始化(以及内存释放)的可移植方法munch。其策略如下:
- 为每一个需要静态初始化的档案产生一个_sti()函数(static initialization),内带必要的constrcutor调用操作或inline expansions。例如前文所说的identity对象会在matrix.c中产生下面的_sit()函数:
1
2
3_sti_matrix_c_identity(){
identity.Matrix::Matrix();
} - 类似地,在每一个需要静态的内存释放操作文件中产生一个_std()函数(static deallocation),该函数内带必要的destructor调用操作,或其inline expansions。
- 提供一组runtime library “munch”函数:一个_main()函数(用以调用所有可执行文件中的_sti()函数),以及一个_exit函数(用以调用所有的_std()函数)。
其执行策略如图所示:
(接下来作者以较多篇幅描述了cfont如何在众多可执行文件中找出_sti函数与_std函数,其内容有兴趣的读者可自行参阅,以及静态初始化的一些局限性)
局部静态对象
假设当前有程序片段如下:1
2
3
4
5const Matrix& identity(){
static Matrix mat_identity;
//...
return mat_identity;
}
Local static class object有语意如下:
- 其constrcutor只能执行一次,尽管上述函数可能会被调用多次。
- 其destructor只能执行一次,尽管上述函数可能会被调用多次。
编译器曾经的策略是无条件地在startup时构造对象,然而这会导致所有的local static class objects初始化,即使它们所在的那个函数从未被使用过。因此,应当仅在identity()被调用时构造mat_identity才是最佳选择。
对于上述机制,cfront有实现策略如下:首先,导入一个临时bool量以保护mat_identity的初始化操作,当第一次处理identity()时,该bool量为false,于是调用constructor,随后将临时bool量置为true。同样地,destructor也使用类似的策略。
对象数组
假设当前有对象数组定义式如下:1
Point knots[10];
如果Point既没有定义一个constructor也没有定义一个destructor,那么编译器行为十分简单:只需要分配足够多的内存以容纳10个Point object。
但如果Point定义了一个default destructor,那么这个destructor必须轮流施行于每一个元素之上,一般而言,这是经由一个或多个runtime library函数达成。我们暂且将该此类函数命名为vec_new(),其基本形式如下:1
2
3
4
5
6
7void* vec_new(
void* array,//数组起始地址
size_t elem_size,//object大小
int elem_count,
void (*constructor)(void*),
void (*destructor)(void*,char)//析构操作,在有异常抛出时释放资源
)
对于上述数组定义表达式,编译器真正处理操作如下:1
vec_new(&knots,sizeof(Point),10,&Point::Point(),0);
类似地,对于destructor,也有vec_delete()函数如下:1
2
3
4
5
6void vec_delete(
void *array,
size_t elem_size,
int elem_count,
void (*destructor)(void*,char)
)
如果开发者提供了一个或多个明确初值给对象数组,例如:1
Point knots[10]={Point(),Point(1.0,1.0,0.5),0.5};
那么编译器将会将其转为:1
2
3
4
5
6
7Point knots[10];
//明确初始化前三个
Point::Point(&knots[0]);
Point::Point(&knots[1],1.0,1.0,0.5);
Point::Point(&knots[2],0.5,0.0,0.0);
vec_new(&knots+3,sizeof(Point),7,&Point::Point,0);
Default Constructors和数组
(本小结似乎在描述语言陋规,没看明白)