5.1 “无继承”情况下的对象构造

考虑如下的程序片段:

Point global; //1

Point foobar()
{
	Point local; //2
	Point* head = new Point; //3
	*head = local;
	//stuff
	delete heap;  //4
	return local;
}

上述表现出三种不用的对象产生方式:global对象、local对象和heap对象。

一个object的生命,是该object的一个执行期属性。local object的声明在foobar()函数内,global object的生命和整个程序的声明相同,heap object的生命从它被new运算出来,到它delete运算符摧毁为止。

下面是Point的第一次的声明,可以写成C程序。C++ Standard 说这是一种所谓的Plain OI‘ Data(纯输入输出数据)声明:

typedef struct
{
	float x,y,z;
}Point; 

如果用C++编译这段代码,观念上,编译器会为Point产生一个trivial default constructor、一个trivial destructor、一个trivial copy constructor和trivial copy assignment operator。但事实上,编译器会分析这个声明,并为它贴上Plain OI’ Data标签。

当编译器遇到这样的定义:

Point global;

观念上Point的trivial constructor和destructor都会被调用,constructor在程序的起始处被调用,而destructor在程序的exit()处被调用。然而,事实上那些trivial member要不是没被定义,就是没被调用,程序的行为一如它C中的表现。

在C中,global被视为一个“临时性的定义”,因为它没有显示的初始化操作,这样的定义可以在程序中发生多次,那些实例会被链接器则叠起来,只留下一个单独一个实例,被放在data segment中,这块空间成为BSS(Block Started by Symbol)。

C++并不支持“临时性的定义”,global在C++中被视为完全定义(它会阻止第二个或更多的定义)。C和C++的一个差异在于,BSS data segment在C++中相对的不重要。C++的所有全局对象都被以“初始化过的数据”来对待。

对于Point object local,同样也是没有被构造也没被析构。对于heap object的初始化操作:

Point* heap = new Point;

//被编译器转化
Point* heap = _new(sizeof(Point));	

并没有default constructor施行于new运算符所传回的Point object身上。如果对此object赋值ca偶作,如果local曾被适当地初始化,一切就没问题:

*heap = local;	

观念上,这样的操作会触发trivial copy assignment operator,然而实际上该object 是一个Plain OI‘ Data,所有的赋值操作将是像C那样的纯粹位搬移操作。

对于delete操作一样:

delete heap;

//被编译器转化
_delete(heap);

观念上,这样操作会触发trivial destructor,但一如我们所见,destructor要不是没被产生就是没被调用。最后函数传值将local 当做返回值传回,这样观念上会触发trivial copy constructor,不过实际上return操作只是一个简单的位拷贝操作,因为对象是个Plain OI’ Data。

抽象数据类型(Abstract Data Type)

以下是Point第二次声明,在public接口之下多了private数据,提供完整的封装性,但没提供任何virtual function:

class Point
{
public:
	Point(float x = 0.0,float y = 0.0,float z = 0.0)
		:_x(x),_y(y),_z(z){}
	//no copy constructor,copy operator
	//or destructor defined
private;
	float _x,_y,_z;
};

这个经过封装的Point class,其大小并没有改变,还是三个连续的float,不论private或public存取层,或是member function的声明都不会占用额外的对象空间。

我们并没有为Point定义一个copy constructor或copy operator,因为默认的位语意(default bitwise semantics)已经足够。我们也不需要提供一个destructor,程序默认的内存管理方法也已足够。

如果将class中的所有成员设定常量初值,那么explicit initialization list效率高,甚至在local scope中也是。

在编译层面,会有一个优化机制来sh别inline constructors,后者简单的地提供一个member-by-member的常量自定操作。然后编译器会抽取那些值,并对待它们好像是explicit initialization list所提供一样,而不会把constructor扩展成一系列的assignment指令。

于是,local Point object定义:

{
	Point local;
	//...
}	

//被扩展后,附加上default Point constructor的inline expansion
{
	//inline expansion of default constructor
	Point local;
	local._x = 0.0;local._y = 0.0;local._z = 0.0;
	//...
}

对于heap Point object:

Point* heap = new Point;

//被扩展后
Point* heap = _new(sizeof(Point));
if(heap != 0)
	heap->Point::Point();

然后才被编译器进行inline expansion。

对于heap指针指向local object和以传值方式传回local object都保持着简单的位拷贝操作:

*heap = local;

return local;

观念上,我们的Point class有一个相关的default copy constructor、copy operator和destructor。然而它们是无关痛痒的(trivial),而且编译器实际上根本没产生它们。

为继承做准备

这次第三次声明Point,实现多态性:

class Point
{
public:
	Point(float x = 0.0,float y = 0.0,float z = 0.0)
		:_x(x),_y(y),_z(z){}
	//no copy constructor,copy operator
	//or destructor defined
	
	virtual float z();
	//...
protected:
	float _x,_y,_z;
};

virtual functions的导入促使每一个Point object拥有一个virtual table pointer,这个指针给我们提供virtual接口的弹性,其成本是每一个object额外的一个指针。

除了vptr外,编译器对于我们的Point class产生膨胀作用:

  • 我们定义的constructor被附加了一些代码,以便将vptr初始化。这些代码必须被附加在任何base class constructors的调用之后,单必须在任何由使用者供应的代码之前:
//C++伪代码:内部膨胀
Point*
Point::Point(Point* this,float x,float y)
				:_x(x),_y(y)
{
	//设定object的vptr
	this->vptr_Point = _vtlb_Point;
	
	//扩展member initialization list
	this->_x = x;
	this->_y = y;
	
	//传回this对象
	return this;
}
  • 合成一个copy constructor和一个copy assignment operator,不再是trivial(但是implicit destructor 仍然是trivial)。如果一个Point object被初始化或以一个derived class object赋值,那么以位为基础(bitwise)的操作可能对vptr带来非法设定。
//C++伪代码
//copy constructord 内部合成
inline Point*
Point::Point(Point* this,const Point& rhs)
{
	//设定object的vptr
	this->vptr_Point = _vtlb_Point;
	
	//将rhs坐标中的连续位拷贝到this对象中,
    //或是经由member assignment提供一个member...
	
	return this;
}

编译器优化状态下可能把object的连续内容拷贝到另一个object身上,而不是实现一个精确的“以成员为基础memberwise”。C++ Standard要求编译器尽量延迟nontrivial members的实际合成操作,直到真正遇到其使用场合为止。

对于前面foobar()函数:

Point global;

Point foobar()
{
	Point local;
	Point* heap = new Point;
	*heap = local;
	//stuff
	delete heap;
	return local;
}

global、heap以及heap delete删除操作和稍早的Point版本相同,对于赋值操作触发copy assignment operator的合成,以及调用操作的inline expansion:以this取代heap,而以rhs取代local。

对于return那一行,由于copy constructor的出现,foobar()会被转换成如下这样:

Point foobar(Point& _result)
{
	Point local;
	local.Point::Point(0.0,0.0);
	
	//heap部分与前相同
	
	//copy constructor的应用
	_result.Point::Point(local);
	
	//local对象的destructor在这里执行
	//local.Point::~Point();
	
	return;
}

如果支持NRV(named return value)优化,这个函数会被进一步转化:

//NRV优化
Point foobar(Point& _result)
{
	_result.Point::Point(0.0,0.0);
	
	//heap与前相同
	
	return;
}

一般而言,如果你的设计中,有许多函数都需要以传值方式(by value)传回一个local class object:

T operator+(const T&,const T&)
{
	T result;
	//...真正的工作在此
	return result;
}

那么提供一个copy constructor比较合理,它会触发NRV优化。NRV优化不再需要调用copy constructor,因为运算结果已经被直接计算于“将值传回的object”体内了。

猜你喜欢

转载自blog.csdn.net/weixin_28712713/article/details/84274313
5.1