构造、析构、拷贝语义学——"无继承“情况下的对象构造

(由于学校琐事不断,本月更新过少,下个月需要参加电赛,可能会再次影响更新)

问题实例

 
考虑下述程序:

1
2
3
4
5
6
7
8
9
10
Point global;

Point foobar(){
Point local;
Point *heap = new Point;
*heap = local;
...//do sth
delete heap;
return local;
}

其中第一行、第五行、第六行体现出了完全不同的对象产生方式:global内存配置、local内存配置和heap内存配置。第七行将一个class object赋值给另一个,第九行,明确地delete heap object,第十行则设定返回值(此处再次发生了构造)。

一个object的生命是该object的一个执行期属性,local object的生命从第五行定义开始直至第十行为止。global object的生命与整个程序的生命相同,heap object的生命从它被new出来开始,直至被delete。


问题剖析

 
我们假定Point有声明如下,C++ standrad称这种声明为所谓的Plain OI’s Data形式:

1
2
3
typedef struct{
float x,y,z;
} Point;

如果以C++来编译这段代码,那么编译器理论上会为Point 声明一个trivial default constructor、一个trival destructor、一个trival copy constructor、以及一个trival copy assignment operator。但是实际上,编译器会分析该声明,并为其贴上Plain OI’s Data卷标。

当编译器遇到定义如下:

1
Point global;

观念上Point的trival constructor与destructor都会被产生与调用,constructor在程序起始处被调用而destructor在程序的exit()处被调用(exit()由编译器产生,置于main()函数结束之前)。然而,那些constructor与destructor要么是没定义,要么是没调用,程序的行为一如在C中的表现一样。

除了一处例外:在C中,global被视为一个“临时性定义”,因为它没有一个明确的初始化操作。一个临时性定义会在程序中发生多次,它们的实例会被链接器折叠,只留下一个单独的实体,被放在程序data segment中一个“特别保留给未初始化之global objects使用”的空间。

C++并不支持临时性的定义,这是因为class构造函数的隐含应用之故。虽然大家公认C++能够判定出目前对象是一个class object或是一个Plain OI’s Data,但似乎我们没有必要去分辨它们。因此,global在C++中被视为完全定义(阻止其他的定义)。C与C++的一大差异在于,BSS data segment在C++中不甚重要,C++中所有的全局对象都被当做初始化过的数据来对待。

foobar()中的第五行,有一个Point object local,同样既没有被构造也没有被析构,如果其在未经初始化的情况下就试图去使用,那么会引发bug。至于heap object在第六行中的初始化操作:

1
Point* heap = new Point;

会被转换为对new操作符的调用:
1
Point *heap = _new(sizeof(Point));

需要注意的是,并没有任何default constructor施行于new运算符所传回的Point object身上,第七行对此object有一个赋值操作,我们可能会认为object是一个Plain OI’s Data,所以赋值操作将只是简单地纯粹位搬移操作,第九行执行的delete将被转换为对delete运算符的调用:
1
_delete(heap);

而非触发Point的trivial destructor。最后,函数以pass by value的方式将local当做返回值使用,这在观念上会触发trivial copy constructor,但实际上return操作只是执行了一个简单的位拷贝操作,因为对象是一个Plain OI’s Data。


抽象数据类型

 
现在我们考虑Point加入了private数据之后的情况:

1
2
3
4
5
6
7
class Point{
public:
Point(float x = 0.0,float y = 0.0,float z = 0.0):
_x(x),_y(y),_z(z) {}
private:
float _x,_y,_z;
};

众所周知,封装并未给其带来空间上的额外开销,另外,我们并没有声明copy constructor或copy assignment operator,因为default bitwise已经足够,出于类似的原因,我们也不需要为其声明destructor。

对于一个global实体,现有default constructor作用于其上:

1
Point global;//执行Point::Point(0.0,0.0,0.0);

由于global被定义在全局范畴,因此其初始化操作将会延迟到程序startup时才开始。

explicit initialization list

如果需要对class中的所有成员均设定常量初值,那么给予一个explicit initialization list会比较高效。有实例如下:

1
2
3
4
5
6
7
void mumble(){
Point local1 = {1.0,1.0,1.0};
Point local2;
local2._x=1.0;
local2._y=1.0;
local2._z=1.0;
}

local1的初始化操作比local2的更加高效,因为当函数的activation record被放进程序堆栈时,上述initialization list中的常量就可以被放入local1内存中。

Explicit initialization list存在3项缺陷:

  1. 仅能作用于class member均为public的情况
  2. 只能指定常量,因为仅有常量能在编译时期就可以被evaluate
  3. 由于不存在自动施行,因此初始化行为的失败可能性会比较高

一般来说,explicit initialization list带来的优势不足以弥补其弊端,但对于全局对象而言,explicit initialization list的效率要高于inline constructor。

在编译器层面,会有一个特殊的机制来识别inline constructor,然后将其转变为explicit initialization list那种格式(如果可以转变的话),如果不能转变,那么将提供member-by-member的常量指定操作:

1
2
3
4
5
{
Point local;
//inline expansion
local._x=0.0;local._y=0.0;local._z=0.0;
}


为继承做准备

 
我们的第三个Point声明,将为“继承性质”以及某些操作的dynamic resolution做准备:

1
2
3
4
5
6
7
class Point{
public:
Point(float x=0.0,y=0.0):_x(x),_y(y) {}
virtual float z();
protected:
float _x,_y;
};

virtual function的引入使得每一个Point object拥有一个vptr,这可能会引起空间性能的降低。另外,virtual function的引入也将引发编译器对Point class产生膨胀作用:

  • 我们所定义的constructor被附加了一些代码,以便将vptr初始化。这些代码必须附加于任何base class constructors的调用之后,但必须在任何由开发者撰写的代码之前。
    1
    2
    3
    4
    5
    6
    7
    Point* Point(Point *this,float x,float y):_x(x),_y(y){
    //以下为附加部分
    this->_vptr_Point = _vtbl_Point;
    this->_x = x;
    this->-y = y;
    return this;
    }
  • 合成一个constructor与一个copy assignment operator,而且其操作不再为trivial(但implicit destructor依然是trivial)。bitwise操作可能会导致vptr非法:
    1
    2
    3
    4
    5
    6
     inline Point*
    Point::Point(Point *this,const Point &rhs){
    this->_vptr_Point = _vtbl_Point;
    //执行原计划的复制操作
    return this;
    }

编译器在优化状态下可能会把object的连续内容拷贝到另一个object上,而不是考虑memberwise的赋值操作。C++ standard要求编译器尽量延迟nontrivial members的实际合成操作,直到其真正使用。

我们再一次分析如下程序:

1
2
3
4
5
6
7
8
9
10
Point global;

Point foobar(){
Point local;
Point *heap = new Point;
*heap = local;
...//do sth
delete heap;
return local;
}

*heap=localmemberwise赋值操作,可能会触发copy assignment operator的合成,及其调用操作的一个inline expansion:以this代替heao而以rhs代替local。

传值返回的那一行出现了更大的冲击,由于copy constructor的出现,foobar很有可能被转化成了下面的形式:

1
2
3
4
5
6
7
8
Point foobar(Point &_result){
Point local;
local.Point::Point(0.0,0.0);
//heap与之前相同
_result.Point::Point(local);//应用copy constructor
local.Point::~Point();//destroy local
return;
}

如果支持NRV优化,那么这个函数将会进一步被转化为:
1
2
3
4
5
Point foobar(Point &_result){
_result.Point::Point(0.0,0.0);
//heap部分相同
return;
}

一般而言,如果你的程序中充斥着大量需要return value的函数(例如数值计算函数),那么提供一个copy constructor比较合理,因为这会触发NRV优化。