6.1 对象的构造和析构
一般而言,constructor和destructor的插入如预期所示:
{
Point point;
// point.Point::Point() 一般而言会被插入在这里
...
// point.Point:;~Point() 一般而言会被插入在这里
}
如果一个区段(以{}括起来的区域)或函数中有大于一个离开点,destructor 必须被放在每一个离开点之前(当时object还存活),例如:
{
Point point;
// constructor 在这里行动
switch (int(point.x())) {
case -1:
// mumble
// destructor 在这里行动
return;
case 1:
// mumble
// destructor 在这里行动
return;
default:
// mumble
// destructor 在这里行动
return;
}
// destructor在这里行动
}
在这个例子中,point的destructor必须在 switch 指令四个出口的 return 操作前被生成出来,另外 也很可能在这个区段的结束符号(右大括号)之前被生成出来--即使程序的分析的结果发现绝对不会进行到那里。
同理,goto指令也可能需要许多个destructor调用操作.例如下面的程序片段:
{
if (cache2)
// 检查cache;如果吻合就传回1
return 1;
Point xx;
// xx的constructor 在这里行动
while (cvs.iter(xx))
if (xx == value)
goto found;
// xx的destructor 在这里行动
return 0;
found:
// cache item
// xx的destructor 在这里行动
return 1;
Destructor调用操作必须放在最后两个 return 指令之前,但是却不必被放在最初的 return 之前,因为那时object尚未被定义出来。
一般而言会把 object 尽可能放置在使用它的那个程序区段附近,这样做可以节省不必要的对象产生操作和销毁操作。以本例而言,如果在检查cache之前就定义了Point object,那就不够理想,这个道理非常明显。
全局对象(Global Objects)
有以下程序片段:
Matrix identity;
main() {
// identity 必须在此处被初始化; //bzf:未初始化的对象不太能被操作
Matrix m1 = identity;
...
return 0;
}
C++保证,一定会在main()函数中第一次用到identity之前,把identity构造出来,在main()函数结束之前把identity销毁。像identity这样的 global object 如果有constructor或destructor的话,它需要静态的初始化操作和内存释放操作。
C++程序中所有的global objects都被放置在程序的 data segment 中.如果明确指定给它一个值,object将以该值为初值.否则object所配置到的内存内容为0。下面:
int v1 = 10;
int v2;
v1和v2都被配置于程序的data segment,v1值为10,v2值为0(C不自动设定初值)。C中的global object只能够被一个常量表达式(可在编译时期求值)设定初值。虽然 class object在编译期可以被放置于data segment中并且内容为0,但constructor一直要到程序启动(startup)时才会实施。必须对"放置于program data segment中的object的初始化表达式"做评估,这正是为什么一个object需要静态初始化的原因。
当cfront还是唯一的C++编译器,而且跨平台移植性比效率的更重要的时候,有一个可移植但成本颇高的静态初始化(以及内存释放)方法。cfront的束缚是,它的解决方案必须在每一个UNIX平台上都有效。因此不论是相关的linker或object-file format,都不能预先做任何假设.由于有这样的限制,下面的策略就浮现出来:
1.为每一个需要静态初始化的档案产生一个_sti()函数,内带必要的constructor调用操作或 inline expansions。前面所说的identity对象会在matrix.c中产生出下面的_sti()函数(sti可能是 static initialization的缩写):
__sti__matrix_c__identity()
{
identity.Matrix::Matrix(); // 这是 static initialization
}
其中matrix_c是文件名编码,_identity表示文件中所定义的第一个 static object.在 _sti之后附加上这两个名称,为可执行文件提供一个独一无二的识别符号.
2.类似情况,在每一个需要静态的内存释放操作(static deallocation)的文件组宏,产生出一个 _std()函数(static deallocation),内带必要的destructor调用操作,或是其 inline expansions.在例子中会有一个_std()函数被产生出来,针对identity对象调用Matrix destructor.
3.提供一组runtime library "munch"函数:一个_main()函数(用以调用可执行文件中的所有的__sti()函数),以及一个exit()函数(以类似方式调用所有的__std()函数).
如图:

cfront在程序中插入一个_main()函数调用操作,main()函数的第一个指令.这里的exit()和C library的exit()不同,为了链接前者,在cfront的CC命令中必须先指定C++ standard library.
最后一个需要解决的问题是,如何收集一个程序中各个object files的__sti()函数和__std()函数.它必须是可移植的.在当时,cfront(也代表C++)若要成功地流行于UNIX各平台,必须依靠它.
解决方法是使用nm命令.nm会倾印出object file的符号表格项目(symbol table entries).一个可执行文件系统由.o文件产生出来,nm将施行于可执行文件身上,其输出被导入munch程序中.munch程序会搜寻以__sti或__std开头的名称,然而把函数名称加到一个sti()函数和std()函数的跳转表格(jump table)中,接下来它把这个表格写到一个小的program text文件中,将这个内含表格的文件加以编译,然后整个可执行文件被重新链接,_main()和exit()于是在各个表格上访问一遍,轮流调用每一个项目(代表一个函数地址).
局部静态对象
const Matrix &identity()
{
static Matrix mat_identity;
// ...
return mat_identity;
}
Local static class object保证了什么样的语意?
mat_identity 的 constructor 必须只能执行一次,虽然上述函数可能会被调用多次.
mat_identity 的 destructor 必须只能执行一次,虽然上述函数可能会被调用多次.
编译器的策略之一就是,无条件地在程序起始(startup)时构造出对象,然而这会导致所有的 local static class objects 都在程序起始时被初始化,即使它们所在的那个函数从不曾被调用过。因此,只在identity()被调用时才把mat_identity构造出来,是比较好的做法。
首先导入一个临时性对象以保护 mat_identity 的初始化操作。第一次处理identity()时,这个临时对象被评估为 false,于是constructor会被调用,然后临时对象被改为 true,这样就解决了构造的问题。
在相反的那一端,destructor也需要有条件地施行与mat_identity身上,但只有在mat_identity已经被构造起来时才算数.要判断mat_identity是否被构造出来,很简单, 如果临时对象为 true,就表示构造好了。
困难是,由于cfront产生C码,mat_identity 对函数而言仍然是local,因此没办法在静态的内存释放函数(static deallocation function)中存取它。解决的方式是: 取出local object的地址(由于object是 static,其地址在 downstream component 中将转换到程序内用来放置 global object 的 data segment 中),下面是cfront的输出:
static struct Matrix *__0__F3 = 0; // 临时对象, 作为戒护之用
// C++的reference在C中以pointer来代替,且identity()的名称会被mangled
struct Matrix *identity_Fv()
{
static struct Matrix __lmat_identity;
// 如果临时性的保护对象已经被设立,就什么也不做,否则
// a.调用constructor:__ct__6MatrixFv
// b.设定保护对象,使它指向目标对象
__0__F3 ? 0:(__ct__6MatrixFv(&__lmat_identity), (__0__F3 = (&__lmat__identity)));
}
最后,destructor必须在"与 text program file 有关联的静态内存释放函数(static deallocation function)"中被有条件地调用。
char __std__stat_0_c_j()
{
__0__F3 ? __dt__6MatrixFv( __0__F3, 2 ) : 0;
// ...
}
对象数组
假设有下列的数组定义:
Point knots[10];
如果Point既没有定义一个 constructor 也没有定义一个 destructor,只需配置足够的内存以储存10个连续的Point元素即可。
然而Point的确定义了一个 default destructor,所以这个destructor必须轮流施行于每一个元素上。一般而言这是经由一个或多个runtime library函数达成的。在cfront中,使用一个被命名为vec_new()的函数,产生出由 class objects 构成的数组。新近的编译器则提供两个函数一个用来处理"没有virtual base class"的 class,另一个用来处理"内带virtual base class"的 class。后一个函数通常被称为 vec_vnect.函数类型通常如下:
void *vec_new
(
void *array, // 数组起始地址
size_t elem_size, // 每一个class object的大小
int elem_count, // 数组中的元素数目
void (*constructor)(void *),
void (*destruction)(void *, char)
)
constructor 和 destructor 参数是该 class 的 default construct 和 default destructor 的函数指针.
参数 array 带有的若么是具名数组(本例为knots) 的地址,要么0。如果是0,那么数组由应用程序的 new 运算符,被动态配置于heap中。
参数 elem_size 表示数组中的元素大小。在vec_new中,constructor施行于elem_count个元素上。对于支持exception handling的编译器而言,destructor的提供是必要的。下面是编译器针对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()--如果class拥有virtual base classes的话)的 runtime library 函数完成,其函数类型如下:
void*
vec_delete(
void *array, //数组起始地址
size_t elem_size, //每一个class object的大小
int elem_count, //数组中的元素数目
void (*destructor)(void*,char)
)
有的编译器会另外增加一些参数,用以传递其他数值,以便能够有条件地导引vec_delete()的逻辑。在vec_delete()中,destructor被施行于elem_count个元素身上。
如果提供一个或多个明显初值给一个由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 的地址,是不可以的。由指针启动 constructor,无法存取 default argument values。那么,如何支持以下的语句:
complex::complex(double=0.0, double=0.0);
当程序员写出:
complex c_array[10];
时,编译器最终需要调用:
vec_new(&c_array, sizeof(complex), 10, &complex::complex, 0);
为了解决这个问题,可由编译器产生一个内部的 stub constructor,没有参数,在其函数内调用由程序员提供的constructor,并将default参数值明确地指定过去:
complex::complex() // 内部产生的 stub constructor 函数(存根构造函数)
{
complex(0.0, 0.0);
}
违反了C++语言规则:class 只支持一个没有带参数的 constructor。当然,
// bzf:
C++标准明确规定,不能获取构造函数和析构函数的地址。
指向其他成员函数的指针可以,指向构造函数析构函数的不行。
因为构造函数和析构函数都是没有返回值的,无法声明一个没有返回值的成员函数指针。
但是通过汇编代码,有可能获得它。

浙公网安备 33010602011771号