执行期语义学
对象的构造和析构 object construction and destruction
我们通过几个例子来看看一般的构造函数和析构函数是怎么安插的吧
class Point {};
void test1()
{
int x = 0;
Point point;
// Point::Point();
switch (x)
{
case 1:
// Point::~point();
return;
case 2:
// Point::~point();
return;
default:
// Point::~point();
return;
}
// Point::~point()
return;
}
void test2()
{
if (0) return;
Point point;
// Point::Point();
if (1) goto find;
//Point::~Point();
return;
find:
// Point::~Point();
return;
}
如上所示,每当一个对象被定义的时候,编译器就会显式插入一个该对象的构造函数,当该对象所在的作用为即将结束的时候,编译器就会自动为它安插一个析构函数。
全局对象(global object)
Matrix identity; int main() { Matrix m1 = identity; return 0; }
看到上面的程序片段,C++保证,一定会在
main()
函数中第一次用到identity
之前,把identity
构造出来,而在main()
函数结束之前将identity
销毁掉。C++程序中所有的global objects都放置在 data segment中。如果显式指定给它一个值,此object将以该值为初值。否则object所配置的内存的内容是0。
这里讲点题外话:程序的
data segment
和bss segment
的区别:- 什么是
data segment
?程序的data段用来存放已初始化的全局变量,在编译器编译的时候描绘将已初始化的数据分配内存空间,数据保存在目标文件中。 - 什么是
bss segment
?Block Start by Symbol
存放未初始化的全局变量,在编译器编译的时候,不会给该段的数据分配空间,只是记录数据所需空间的大小,程序执行的时候在分配内存并将内存清零。 - 为什么要分data段和bss段?在程序编译的时候,不会给bss段中断的数据分配空间,只是记录数据所需空间的大小,在程序执行的时候才会给bss段中的数据分配内存,通过这种方式可以节省部分空间,进一步缩减可执行程序的大小。
int v1 = 1; int v2; // v2 = 0;
上面的代码中
v1
和v2
的值都被配置在程序的data segment中。(在C语言中,编译器并不会自动设定初值)。在C语言中一个global object只能够被一个常量表达式设定初值。当然,constructor并不是常量表达式,所以,虽然class object可以被设置为0并放置在data segment中,但是constructor一直要等到程序启动才会实施。局部静态对象 Local Static Objects
const Matrix & identity() { static Matrix mat_identity; return mat_identity; }
局部静态对象保证了什么样的语意?(以上面为例)
mat_identity
的constructor
必须只能执行一次,虽然上述的函数可能被调用多次。mat_identity
的destructor
必须只能被执行一次,虽然上述的函数可能被执行多次。
编译器一般有两个不同的方法来实现保证这个语意:
- 无条件地在程序起始时构造出对象来。但是这回导致所有的
local class objects
都在程序起始时被初始化,即使它们所在的那个函数从不曾被调用过。 - 另一种方式就是在需要的时候才进行构造(这也是比较符合我们c++的风格的)。作者在
cfront
中的做法是:先导入一个临时性对象保护mat_identity
的初始化操作。第一次处理identity()
时,这个临时对象被评估为false
,于是constructor就会被调用,然后临时对象被改为true
。这样就解决了构造的问题。析构的时候我们需要判断对象是否被构造起来了,我们检查一下这个临时对象即可。但是困难是,这个object对于函数来说是局部的,无法在静态的内存释放函数中存取该对象。作者的做法是:直接将这个局部静态对象的地址取出来了。
看看下面的实现
static struct Matrix *__0__F3 = 0; struct Matrix* identity_Fv() { static struct Matrix __lmat_identity; __0__F3 ? 0 // do nothing : (__ct__6MatrixFv(&__lmat_identity ), // constructor (__0__F3 = ( &__lmat_identity ))); // 指向设定好的对象 ... } // 最后destructor必须被有条件的调用 char __std__stat_0_c_j() { __0__F3 ? __dt__6MatrixFv( __0__F3, 2 ) : 0 ; ... }
对象数组(array of object)
看看下面数组的定义
Point knots[10];
编译器是直接为这个数组分配足够的空间还是分配空间并调用这个
Point
的构造函数呢?如果这个
Point
并没有定义一个constructor或者一个destructor,那么我们的编译器确实是直接为其分配一个足够的空间即可。但是当这个类中定义了一个
default constructor
,这个default constructor
必须轮流实行于每一个元素之上。这是由多个runtime library函数组成的。在cfront中,我们使用一个被命名为vel_new()
的函数,产生出以class object构造而成的数组。在比较新的编译器中,则是提供两个,一个用来处理没有virtual base class的class,另一个用来处理内含virtual base class 的class。后一个函数通常被称为vec_new()
。函数的类型通常如下:void* vec_new() { void* array; size_t elem_size; int elem_count; void (*constructor)(void*), void (*destructor)(void*, char) }
如果对部分元素进行了初始化,那么就将
void* array
进行偏移就好了。
- 什么是