构造、析构、拷贝语意学

纯虚函数的存在

在C++中,可以定义和调用一个pure virtual function(纯虚函数)。不过它只能被静态地调用(这里是指通过类名::进行的调用),不能经由虚拟机制调用。
class A
{
public:
	A() {}
	//virtual A() = 0;构造函数不能声明为虚函数,更不能声明为纯虚函数
	virtual void interface() = 0;
	void fun();
private:
	int a;
};
void A::interface() {
	cout << "我在声明的时候是个纯虚函数" << endl;
}
void A::fun() {
	cout << "我是fun函数" << endl;
}
class B :public  A {
private:
	int b;
public:
	B() {}
	void interface();
};
void B::interface() {
	A::interface();
	A::fun();
}
int main()
{
	//A a;错误,凡是包含纯虚函数的类都是抽象类,而抽象类是不能实例化出对象的
	B b;
	b.interface();
	system("pause");
	return 0;
}

注意:要不要定义一个纯虚函数由设计者自己确定。但pure virtual destructor一定需要定义,因为每一个derived class destructor会被编译器加以扩张,以静态调用的方式调用其每一个virtual base class以及上一层base class的destructor。因此,只要缺乏任何一个base class destructors的定义,就会导致链接失败。但如果一个类不是base class,则无所谓。比如:
class C 
{
	C() {}
	virtual ~C() = 0;//能正常编译通过
};

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

Point global;
Point foobar()
{
	Point local;
	Point *heap = new point;
	*heap = local;
	delete heap;
	return local;
}
L1,L5,L6表现出三种不同的对象产生方式:global内存配置、local内存配置和heap内存配置。L7把一个class object指定给另一个,L10设定返回值,L9显式地以delete运算符删除heap object。

下面是Point的第一次声明形式:

typedef struct
{
	float x, y, z;
}Point;
当定义:

Point global;

观念上Point的trival constructor和destructor都会被产生出来并被调用,constructor在程序起始处被调用而destructor在程序的exit()处被调用(exit()是由系统产生的,放在main()结束之前)。然而,事实上那些trival members要不是没被定义,就是没被调用。
注意:global在C++中被视为完全定义(它会阻止第二个或更多个定义)。C和C++的一个差异就在于:C++的所有全局对象都被以“初始化过的数据”来对待。

比如:

int Global;
int main()
{
	cout << Global << endl;
	system("pause");
	return 0;
}


foobar()函数中的L5,有一个Point object local,同样也是既没有被构造也没有被析构。当然,Point object loacl如果没有先经过初始化,可能会成为一个潜在的程序“臭虫”,这种情况就是第一次使用它就需要其初值。像L7

扫描二维码关注公众号,回复: 1049950 查看本文章

至于heap在L6的初始化操作:

Point *heap=new Point;
会被转换为对new运算符(由library提供)的调用:

Point *heap=__new(sizeof(Point));

并没有default constructor施行于new运算符所传回的Point object身上。

L7对此object有个赋值操作,如果local曾被适当地初始化过,就没有问题了

*heap=local;

如果没有初始化,则会报错


这样的指定操作只是像C那样纯粹的位搬移操作,不会触发trivial copy assignment operator。

L9执行一个delete操作:

delete heap;

会被转换位对delete运算符(由library)的调用:

__delete(heap);

此时destructor要不就是没被产生要么就是没有被调用。

最后,函数以传值的方式将local当作返回值传回,这在观念上会触发trivial copy constructor,不过实际上return操作只是一个简单的位拷贝操作,因为对象是个Plain OI Data。

抽象数据类型

class Point {
pubblic:
	Point(float x = 0.0, float y = 0.0, float z = 0.0)
		: _x(x), _y(y), _z(z) {}
private:
	float _x, _y, _z;
};
这个经过封装的Point class,其大小并没有改变,还是三个连续的float。不论private或public存取层,或是member function的声明,都不会占用额外的对象空间。
我们并没有为Point定义一个copy constructor或copy operator,因为默认的位语意已经足够,我们也不需要提供一个destructor,因为程序默认的内存管理方法也已足够。
对于一个global实例:
Point global;//实施Point::Point(0.0,0.0,0.0);
现在有了default constructor作用其上。由于global被定义在全局范畴中,其初始化操作将延迟到程序启动时才开始。
如果要将class中的所有成员都设定常量初值,那么给予一个explicit initialization会比较有效率。比如:
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的有效率
于是,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;
L6配置出一个heap Point object:
Point *heap=new Point;
现在则被附加一个对default Point constructor的有条件调用操作:
Point *heap = __new(sizeof(Point));
if (heap != 0)
heap->Point::Point();
然后才又被编译器进行inline expansion操作。
把heap指针指向local object:
*heap=local;
则保持着简单的位拷贝操作。以传值方式传回local object,情况也是一样的:
returnlocal;
该操作并不会导致destructor被调用,因为我们并没有显式地提供一个destructor函数实例。

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

为继承做准备

我们 的第三个Point声明,将为继承性质以及某些操作的动态决议做准备。
class Point {
public:
	Point(float x = 0.0, float y = 0.0)
		: _x(x), _y(y){}
	virtual float z();
protected:
	float _x, _y;
};
除了每一个class object多负担一个vptr之外,virtual function的导入也引发编译器对于我们的Point class产生膨胀作用:
1.我们所定义的constructor被附加了一些代码,以便将vptr初始化。这些代码必须被附加在任何base class constructors的调用之后,但必须在任何的使用者供应的代码之前。
2.合成一个copy constructor和一个copy assignment operator(原因是一个class声明了任何virtual functions,就会导致这两个函数的合成,而且是nontrivial), 而且其操作不再是trivial(但implicit destructor仍然是trivial)。如果一个Point object被初始化或以一个derived class object赋值,那么以位为基础的操作可能对vptr带来非法设定。

L1的global初始化操作、L6的heap初始化操作以及L9的heap删除操作,都还是和稍早的Point版本相同,然而L7的memberwise赋值操作:
*heap=local;很有可能触发copy assignment operator的合成,及其调用操作的一个inline expansion(行内扩张):以this取代heap,而以rhs取代local。

最戏剧性的冲击发生在以传值方式传回local的那一行。由于copy constructor的出现,fooba()转换如下:
Point foobar(Point &_result) {
	Point local;
	local.Point::Point(0.0, 0.0);
	//heap的部分与前面的相同
	...
	//copy constructor的应用
	__result.Point::Point(local);
	//local对象的destructor将在这里执行
	//调用Point定义的destructor:
	local.Point::~Point();
	return;
}
如果支持NRV优化,这个函数会被进一步转化如下:
Point foobar(Point &_result) {
	__result.Point::Point(0.0, 0.0);
	return;
}

5.2 继承体系下的对象构造

当我们定义一个object如下:
T object;
如果T有一个constructor(不论是由user提供或是编译器合成),它会被调用,这是明显的。不明显的是,constructor的调用伴随了什么?
Constructor可能内含大量的隐藏码,因为编译器会扩充每一个constructor,扩充程度视class T的继承体系而定。一般而言编译器所做的扩充操作如下:
1.记录在membebr initialization list中的data membebrs初始化操作会被放进constructor的函数实体,并以member的声明顺序为顺序。
2.如果有一个member并没有出现在member initialization list之中,但它有一个default constructor,那么该default constructor必须被调用。
3.如果class object有virtual table pointer,它必须被设定初值,指向适当的virtual table。
4.所有上一层的base class constructors必须被调用,以base class的声明顺序为顺序。
5.所有virtual base class constructors必须被调用,从左到右,从深到浅。
 
class Point {
	Point(float x = 0.0, float y = 0.0);
	Point(const Point&);
	Point& operator=(const Point&);
	virtual ~Point();
	virtual float z() { return 0.0; }
protected:
	float _x, _y;
};
class Line {
	Point _begin, _end;
public:
	Line(float = 0.0, float = 0.0, float = 0.0, float = 0.0);
	Line(const Point&, const Point&);
	void draw();
};
在class Line中,每一个exolicit constructor都会被扩充以调用其两个member class objects的constructors。
比如:
Line::Line(const Point &begin, const Point &end)
	:_end(end), _begin(begin)
{}
会被编译器扩充并转换为;
Line* Line::Line(Line *this, const Point &bebgin, const Point &end)
{
	this->_begin.Point::Point(begin);
	this->_end.Point::Point(end);
	return this;
}
由于Point声明了一个copy constructor、一个copy operator,以及一个destructor,所以Line class的implicit copy constructor、copy operator和destructor都将是nontrivial。

当程序员写下:
Line a;时
implicit Line destructor会被合成出来(如果Line派生自Point,那么合成出来的destructor将会是virtual。在这里,不是继承关系,所以被合成出来的destructor只是nontrivial),其中,它的member class objects的destructors会被调用:
inline void Line::Line(Line *this)
{
	this->_end.~Point::Point();
	this->_begin.~Point::Point();
}
请注意,索然Point destructor是virtual,但其调用操作会被静态地决议出来(应该是没有继承关系的缘故)。
类似的道理,
Line b=a;时,implicit Line copy constructor会被合成出来,成为一个inline public member。
a=b;时,
implicit copy assignment constructor会被合成出来,成为一个inline public member。

虚拟继承

考虑下面这个虚拟继承:
class Point3d :public virtual Point {
public:
	Point3d(float x = 0.0, float y = 0.0, float z = 0.0)
		:Point(x, y), _z(z) {}
	Point3d(const Point3d& rhs)
		:Point(rhs), _z(rhs._z) {}
	~Point3d();
	Point3d& operator=(const Point3d&);
	virtual float z() { return _z; }
protected:
	float _z;
};
传统的“constructor扩充现象”并没有用,否则会导致二义性,而虚拟继承正是用于解决二义性。
Point3d* Point3d::Point3d(Point3d *this,
	float x, float y, float z) {
		this->Point::Point(x, y);
	this->__vptr_Point3d = __vtbl_Point3d;
	this->__vptr_Point3d__Point =
		__vtbl_Point3d__Point;
	this->_z = rhs._z;
	return this;
}
上面的Point3d constructor扩充内容是错误的。比如在下面的派生情况中:
class Vertex :virtual public Point {};
class Vertex3d :public Point3d, public Vertex {};
class PVertex :public Vertex3d {};

在上述派生关系中,按理说,Vertex的constructor必须也调用Point的constructor。 然而,当Point3d和Vertex同为Vertex3d的subobjects时,它们对Point constructor的调用操作一定不可以同时发生,因为这样就会导致二义性。所以Vertex3d有责任将Point初始化。而更往后的继承,则由PVertex(不再是Vertex3d)来负责完成“被共享之Point subobject”的构造。
+
下面才是Point3d的constructor扩充内容:
Point3d* Point3d::Point3d(Point3d *this, bool __most_derived,
	float x, float y, float z) {
	if (__most_derived != false)
		this->Point::Point(x, y);
	this->__vptr_Point3d = __vtbl_Point3d;
	this->__vptr_Point3d__Point =
		__vtbl_Point3d__Point;
	this->_z = rhs._z;
	return this;
}
在更深的继承情况下,例如Vertex3d,调用Point3d和Vertex的constructor时,总是会把__most_derived参数设为false, 于是就压制了两个constructors中对Point constructor的调用操作
Vertex3d* Point3d::Vertex3d(Vertex3d *this,
	float x, float y, float z) {
	if (__most_derived != false)
		this->Point::point(x, y);
	//调用上一层base classes
	this->Point3d::Point3d(fal se, x, y, z);
	this->Vertex::Vertex(false, x, y);
	//...设定vptrs的操作
	//...安插user code
}
这样的策略得以保持语意的正确无误。

vptr初始化语意学

假如在上面的那张继承图中,这个继承体系中的每一个constructors内含一个调用操作:
Point3d::Point3d(float x, float y, float z)
	:_x(x), _y(y), _z(z)
{
	if (spyOn)
		cerr << "Within Point3d::Point3d()"
		<< "size:" << size() << endl;
}
此时问题来了,当定义PVertex object时,前述的5个constructors会如何?每一次size()调用会被决议为PVertex::size()吗还是目前正在执行的constructor所对应的class的size()函数实例?
C++语言规则告诉我们,在Point3dconstructor中调用size()函数,必须被决议为Point3d::size()而不是PVertex::size()。更一般性的规律:在一个class(本例为Point3d)的constructor(和destructor)中,经由构造中的对象(本例为PVertex对象)来调用一个virtual function,其函数实例应该是在此class(本例为Point3d)中有作用的那个。当base class constructor执行时,derived实例还没有被构造起来。在PVertex constructor执行完毕之前,PVertex并不是一个完整的对象;Point3d constructor执行之后,只有Point3d subobject构造完毕。
为解决这个问题,方法就是在执行一个constructor时,必须限制一组virtual functions候选名单。那么什么是决定一个class的virtual functions名单的关键?答案是virtual table。而virtual table又是通过vptr进行处理。所以为了控制一个class中有所作用的函数,编译系统只要简单地控制住vptr的初始化和设定操作即可。
那么vptr初始化操作应该何时处理?本质而言,这得视vptr在constructor之中“应该在何时被初始化”而定。答案是:在base class constructors调用操作之后,但是在程序员供应的代码或是"member initialization list中所列的members初始化操作"之前。
constructor的执行算法通常如下:
1.在derived class constructor中,所有virtual base classes及上一层base class的constructors会被调用。
2.上述完成之后,对象的vptr(s)会被初始化,指向相关的virtual table。
3.如果有member initialization list的话,将在constructor体内扩展开来。这必须在vptr被设定之后才做,以免有一个virtual membber function被调用。
4.最后,执行程序员所提供的代码。
例如,已知下面这个由程序员定义的PVertex constructor:
PVertex:PVertex(float x, float y, float z)
	: _next(0), Vertex3d(x, y, z), Point(x, y)//前面提到过,由PVertex负责完成“被共享之Point subobject”的构造
{
	if (spyOn)
		cerr << "Within PVertex::PVertex()" << "size:" << size() << endl;
}
它可能被扩展为:
PVertex* PVertex::PVertex(PVertex* this, bbool __most__derived,
		float x, float y, float z)
{
	//条件式地调用virtual base constructor
	if (__most__derived != false)
		this->Point::Point(x, y);
	//无条件地调用上一层base
	this->Vertex3d::Vertex3d(x, y, z);
	//将相关的vptr初始化
	this->__vptr_PVertex = __vtbl_PVertex;
	this->_vptr_Point_PVertex = __vtbl_Point__PVertex;
	//程序员所写的代码
	if (spyOn)
		cerr << "Within PVertex::PVertex()"
		<< "szie:"
		//经由虚拟机制调用
		//这就是巧妙之处,this指针是PVertex型的,在构造函数中,size()
		//肯定就是PVertex::size(),因为PVertex派生类的实例还没构造起来
		//(因为这是在构造函数之中)。但若不是在构造函数中调用size(),
		//这时PVertex派生类的实例已经构造起来,size()就不知道是PVertex的
		//还是PVertex派生类的
		<< (*this->__vptr__PVertex[3].faddr)(this)
		<< endl;
	//传回被构造的对象
	return this;
}

5.3 对象复制语意学

如果不对一个类供应一个copy assignment operator,而光是依赖默认的memberwise copy,编译器不会产生出一个实例。也就是说,如果一个class已经有了bitwise copy语意,那么implicit assignment operator被视为毫无用处,也就不会被合成出来。
一个class对于默认的copy assignment operator,在以下情况下,不会表现出bitwise copy语意:
1.当class内含一个member object,而其class有一个copy assignment operator时。
2.当一个class的base class有一个copy assignment operator时。
3.当一个class声明了任何virtual functions(我们一定不要拷贝右端class object的vptr地址,因为它可能是一个derived class obbject)时。
4.当class继承自一个virtual base class(此时,不论babse class有没有copy operator)时。

比如,对于class Point,
Point a,b;
a=b;
上述操作由bitwise copy完成,把Point b拷贝到Point a,其间并没有copy assignment operator被调用。从语意或从效率上考虑,这都是我们需要的。注意,我们还是可能提供一个copy constructor,为的是把NRV优化打开。copy constructor的出现不应该让我们以为也一定要提供一个copy assignment operator。

现在导入一个copy assignment operator,用以说明operator在继承之下的行为:
inline Point& Point::operator=(const Point &p)
{
	_x = p._x;
	_y = p._y;
	return *this;
}
现在派生一个Point3d class(虚拟继承)
class Point3d :virtual public Point {
public:
	Point3d(float x = 0.0, float y = 0.0, float z = 0.0);
protected:
	float _z;
};
如果没有为Point3d定义一个copy assignment operator,编译器就必须合成一个,如下所示:
inline Point3d& Point3d::operator=(Point3d* const this, const Point3d &p)
{
	//调用base class的函数实例
	this->Point::operator=(p);
	_z = p._z;
	return *this;
}

5.5 析构语意学

如果class没有定义destructor,那么只有在class内含的member obbject(或者是class自己的base class)拥有destructor的情况下,编译器才会合成出一个来。否则,destructor被视为不需要,也就不需要被合成(当然更不需要被调用)。例如,类Point,默认情况下并没有被编译器合成出一个destructor,甚至虽然在Point中有一个虚函数
class Point {
public:
	Point(float x = 0.0, float y = 0.0);
	Point(const Point&);
	virtual float z();
private:
	float _x, _y;
};
类似的道理,如果把两个Point对象组合成一个Line class:
class Line {
public:Line(const Point&, const Point&);
	   virtual draw();
protected:
	Point _begin, _end;
};
Line也不会拥有一个被合成出来的destructor,因为Point并没有destructor。
当我们从Point派生出Point3d(即使是一种虚拟派生关系)时,如果恶魔没有声明一个destructor,编译器也就没必要合成一个destructor。

不论Point还是Point3d,都不需要destructor,为它们提供一个destructor反而是低效率的。你应该拒绝那种“对称策略”的奇怪想法:你已经定义了一个constructor,所以你需要提供一个destructor也是天经地义的事情。事实上,程序员应该因为需要而非感觉来提供destructor,更不要因为你不确定是否需要一个destructor,于是就提供它。
{
	Point pt;
	Point *p = new Point3d;
	foo(&pt, p);
	delete p;
}
我们看到,pt和p在作为foo()函数之前,都必须先初始化为某些坐标值。这时候需要一个constructor,否则使用者必须显式地提供坐标值。一般而言,class的使用者没有办法检验一个local变量或heap变量以知道它们是否被初始化。把constructor想象为程序的一个额外负担是错误的,因为它们的工作有其必要性。
当我们显式地delete掉p,是否需要在delte之前这么做:
p->x(0); p->y(0);
当然不需要。没有任何理由说在delete一个对象之前先得将其内容清除干净。在结束pt和p的生命之前,没有任何"class使用者层面"的程序操作是绝对必要的,因此,也就不一定需要一个destructor。
然而考虑Vertex class,它维护了一个由紧邻的顶点所形成的链表,并且当一个顶点的生命结束时,在链表上来回移动以完成删除操作。如果这正是程序员所需要的,那么这就是Vertex destructor的工作。
//class Vertex:virtual public Point
inline Vertex& Vertex::operator=(const Vertex &v)
{
	this->Point::Operator = (v);
	//Vertex通过_next维护着一个链表
	_next = v._next;
	return *this;
}
当我们从Point3d和Vertex派生出Vertex3d时,如果我们不供应一个explicit Vertex3d destructor,那么我们还是希望Vertex destructor被调用,以结束一个Vertex3d object。因此编译器必须合成一个Vertex3d destructor,其唯一任务就是调用Vertex destructor。如果我们提供一个Vertex3d destructor,编译器会扩展它,使它调用Vertex destructor(在我们所供应的程序代码之后)。一个由程序员定义的destructor被扩展的方式类似constructors被扩展的方式,但顺序相反:
1.destructor的函数体首先被执行。
2.如果class拥有member class objects,而后者拥有destructors,那么它们会以其声明顺序的相反顺序被调用。
3.如果object内含一个vptr,现在被重新设定,指向适当之base class的virtual table。
4.如果有任何直接的(上一层)nonvirtual base classes拥有destructor,它们会以其声明顺序的相反顺序被调用。
5.如果有任何virtual base classes拥有destructor,而目前讨论的这个class是最尾端的class,那么它们会以其原来的构造顺序的相反顺序被调用。

一个object的生命结束于其destructor开始执行之时。由于每一个base class destructor都轮番被调用,所以derived object实际上变成了一个Vertex3d对象、一个Vertex对象,一个Point3d对象,最后成为一个Point对象。当我们在destructor中调用member function时,对象的蜕变会因为vptr的重新设定(在每一个destructor中,在程序员所供应的代码执行之前)而受到影响。

















猜你喜欢

转载自blog.csdn.net/wk_bjut_edu_cn/article/details/80186504
今日推荐