C++对象模型剖析(十五)一一执行期语义学(一)

执行期语义学

对象的构造和析构 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 segmentbss segment的区别:

    • 什么是data segment?程序的data段用来存放已初始化的全局变量,在编译器编译的时候描绘将已初始化的数据分配内存空间,数据保存在目标文件中。
    • 什么是bss segmentBlock Start by Symbol存放未初始化的全局变量,在编译器编译的时候,不会给该段的数据分配空间,只是记录数据所需空间的大小,程序执行的时候在分配内存并将内存清零。
    • 为什么要分data段和bss段?在程序编译的时候,不会给bss段中断的数据分配空间,只是记录数据所需空间的大小,在程序执行的时候才会给bss段中的数据分配内存,通过这种方式可以节省部分空间,进一步缩减可执行程序的大小。
    int v1 = 1;
    int v2;		// v2 = 0;
    

    上面的代码中v1v2的值都被配置在程序的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_identityconstructor必须只能执行一次,虽然上述的函数可能被调用多次。
      • mat_identitydestructor必须只能被执行一次,虽然上述的函数可能被执行多次。

      编译器一般有两个不同的方法来实现保证这个语意:

      • 无条件地在程序起始时构造出对象来。但是这回导致所有的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进行偏移就好了。

  • 14
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值