`
xitonga
  • 浏览: 579482 次
文章分类
社区版块
存档分类
最新评论

深度探索C++对象模型:5.构造、析构、拷贝语意学

 
阅读更多

第五章:构造、析构、拷贝语意学

考虑下面这个abstract base class声明:

class Abstract_base{

public:

virtual ~Abstract_base() = 0;

virtual void interface() const =0;

virtual const char*

mumble() const {return_mumble;}

protected:

char* _mumble;

};

虽然这个class被设计为一个抽象的base class(其中有pure virtual function,使得Abstract_base不可能拥有实例),但它仍然需要一个显示的构造函数初始化其data member _mumble。如果没有这个操作,derived class的局部对象_ mumble将无法决定初值。

纯虚函数的存在

C++新手会惊讶的发现,一个人可以定义和调用一个纯虚函数;不过只能被静态的调用,不能经由虚拟机制调用。

例如:可以合法的写下下面的代码:

inline void

Abstract_base::interface1() const{

//...

}

inline Concrete_derived::interface1() const{

Abstract_base::interface1();//静态调用纯虚函数

//...

}

要不要定义和调用纯虚函数,由class设计者决定。唯一的例外就是pure virtual destructor:class设计者一定得定义它。因为每一个derived class destructor会在编译期间被编译器加以扩张,以静态调用的方式调用其“每一个 virtual base class”以及“上一层普通base class”的destructor。因此,只要却反任何一个base class destructor的定义,就会导致连接失败。

虚拟规格的存在:

如果把Abstract_base::mumble()设计为一个virtual function,那将是一个糟糕的选择,因为其函数定义内容并不与类型有关,因而几乎不会被后继的derived class改写。

虚拟规格中的const的存在:

声明一个函数为const,然后发现实际上其derived instance必须修改某一个data member,那么就建议一开始不要使用const。

重新考虑class的声明

class Abstract_base{

public:

virtual ~Abstract_base(); //不再是pure virtualfunction

virtual voidinterface1()= 0; //不再是const

const char*

mumble() const {return_mumble;} //不再是virtual

protected:

Abstract_base(char* pc = 0);//新增一个constructor以方便初始化_mumble

char* _mumble;

};

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

1.Point global;

2.

3.Point foobar()

4.{

5. Point local;

6. Point* heap = new Point;

7. *heap = local;

8. //...

9. delete heap;

10. return local;

11.}

L1、L5、L6表现出三种不同对象的产生方式:global内存配置、local内存配置和heap内存配置。L7把一个class object指定给另一个,L10设定返回值,L9则显式的以delete运算符删除heap object。

Plain Ol’ Data声明

下面是Point的第一次声明:

typedef struct{

float x, y, z;

}Point;

这是一种所谓的Plain Ol’ Data(POD)声明,指的是含有数据成员类型如:基本数据类型、指针、union、数组、构造函数是trivial的struct或者class。用来表明C++中与C相兼容的数据类型,可以按照C的方式来处理(运算、拷贝等)。非POD数据类型与C不兼容,只能按照C++特有的方式进行使用。

观念上,编译器会为Point声明一个trival default constructor、一个trivial destructor、一个trivial copy constructor以及一个trival copy assignment operator。但实际上这些trival member要不是没被定义就是没被调用,程序的行为一如它在C中一样,因为对象是一个Plain Ol’ Data。

抽象数据包类型:

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

class Point{

public:

Point(float x = 0.0, floaty = 0.0, float z = 0.0)

:_x(x), _y(y),_z(z){}

private:

float _x, _y, _z;

};

这个封装的 class相对于上一个Point class大小没有改变,还是三个连续的float。

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

对于一个global实例:

Pointglobal; //实施Point::Point(0.0, 0.0, 0.0);

现在有了default constructor作用于其上。由于global被定义在全局范畴中,其初始化操作将延迟到程序启动时才开始。(6.1节对此有讨论)

对于local实例:

{

Pointlocal;

//…

}

现在编译器会为它加上default constructor的inline expansion:

{

Pointlocal;

local._x= 0.0; local._y = 0.0; local._y = 0.0;

//…

}

L6配置出一个heap Point object:

(6)Point* heap = new Point;

现在则被附加一个“对default Point constructor”的有条件调用操作:

//C++伪码

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

if (heap != 0)

heap->Point::Point();

然后才被编译器进行inline expansion操作。至于把heap指针指向local object:

(7) *heap= local;

则保持简单的为拷贝操作。以传值方式传回local object,情况也是一样:

(10)return local;

L9删除heap搜值对象:

(9)delete heap;

该操作不会导致destructor被调用,因为我们并没有显式地提供一个destructor函数实例。

观念是,我们的Point class有一个相关的default copy constructor、copy operator、和destructor。然而这里根本没有产生它们。

为继承做准备

第三个Point声明,将为“继承性质”以及某些操作的动态决议做准备。目前我们限制对z成员作存取操作:

class Point{

public:

Point(float x = 0.0, floaty = 0.0)

:_x(x), _y(y){}

virtual float z();

private:

float _x, _y;

};

因为virtual function的导入,会发生以下改变:

1.我们所定义的constructor被附加了一些代码,一边捡vptr初始化。这些代码必须被附加在任何base class constructor的调用之后,但必须在任何由使用者供应的代码之前。如,下面就是可能的附加结果:

Point*Point::Point(Point* this, float x, float y)

:_x(x), _y(y){

this->_vptr_Point = _vtbl_Point; //设定vptr

this->_x = x;

this->_y = y;

return this;

}

2.合成一个copy constructor和copy assignment operator,而且其操作不再是trival(但implicit destructor仍然是trival )。

//copyconstructor的内部合成

inlinePoint*

Point::Point(Point* this, const Point &rhs ){

this->_vptr_Point = _vtbl_Point;

//将rhs坐标中的连续位拷贝到this对象

return this;

}

L1的global初始化操作,L6的heap初始化操作以及L9的heap删除操作,都还是和稍早的Point版本相同,然而L7的memberwise赋值操作:

*heap =local;

很有可能触发copy assignment operator的合成,以及调用操作的一个inline expansion:以this取代heap,以rhs取代local。

在L10中,由于copy constructor的出现,foobar()很有可能被转化为下面这样:

//C++伪码

Pointfoobar(Point &_result){

Pointlocal;

local.Point::Point(0.0,0.0);

//heap的部分与前面相同…

_result.Point::Point(local); //copy constructor的应用

local.Point::~Point();

return;

}

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

//C++伪码:foobar()的转化

Pointfoobar(Point& _result){

_result.Point::Point(0.0,0.0);

return;

}

一般而言,如果你的设计中,有许多函数都需要以传值方式传回一个local class object,例如像这样形式的一个算术运算:

Toperator+ (const T&, const T&){

T result;

//真正的工作在此

Return result;

}

那么提供一个copy constructor就比较合理。他的出现会促发NVR优化,经过NVR优化后的代码不会调用copy constructor。

5.2继承体系下的对象构造

constructor的调用可能内含大量的隐藏码,因为编译器会扩充每一个constructor,扩充程度视class T的继承体系而定。一般而言编译器所做的扩充操作大约如下:

1. 记录在member initialization list中的data member初始化操作会被放进constructor的函数本体,并以member的声明顺序为顺序。

2. 如果有一个member并没有出现在member initialization list之中,但它有一个default constructor,那么该default constructor必须被调用。

3. 在那之前,如果class object有virtual table pointer(s),它们必须被设定初值,指向适当的virtual tables。

4. 在那之前,所有上一层的base class constructor必须被调用,以base class的声明顺序为顺序(与member initialization list中的顺序没关联):

a)如果base class被列于member initialization list中,那么任何显式指定的参数都应该传递进去。

b)如果base class没有被列于member initialization list中,而它有default constructor(或default memberwise copy constructor),那么就调用。

c)如果base class是多重继承下的第二或后继的base class,那么this指针必须有所调整。

5. 在那之前,所有的virtual base class constructor必须被调用,从左到右,从最深到最浅。(不论virtual base class constructor是否在上一次都必须被先调用)

a)如果class被列于member initialization list中,那么如果有任何显式指定的参数,都应该传递过去。若没有列于list中,而class有一个default constructor,亦应该调用。

b)此外,class中的每一个virtual base class subobject的偏移位置必须在执行期可被存取。

c)如果class object是最底层(most-derived)的class,其constructors可能被调用;某些用以支持这一行为的机制必须被放进来.

在这一节,从“C++语言对class所保证的语意”这个角度,探讨constructors扩充的必要性。我们仍然以Point为例子,并为它增加一个copy constructor、一个copy operator、一个virtual destructor,如下所示:

class Point{

public:

Point(float x = 0.0, floaty = 0.0);

Point(const Point&);

Point& operator=(constPoint&);

virtual ~Point();

virtual float z(){return 0.0;}

//…

protected:

float _x, _y;

};

如果有class Line,由_begin和_end两个点构成:

class Line{

public:

Line(float x1 = 0.0, floaty1 = 0.0, float x2 = 0.0, float y2 = 0.0);

Line(const Point&, constPoint&);

draw();

private:

Point _begin, _end;

};

每一个explicit constructor都会扩充以调用其两个member class objects的constructor。如果我们定义constructor如下:

Line::Line(const Point&begin, const Point& end)

:_end(end),_begin(begin){}

它会被编译器扩充并转换为:

//C++伪码

Line* Line::Line(Line* this,

const Point& begin, const Point&end){

this->_begin.Point::Point(begin);

this->_end.Point::Point(end);

return this;

}

由于Point声明了一个copy constructor、一个copy operator,以及一个destructor(本例为virtual),所以Line class的implicit copy constructor、copy operator和destructor都将有具体效用(nontrvial)。

当程序员写下:

Line a;

时,implicit Line destructor会被合成出来(如果Line派生自Point,那么合成出来的destructor将会是virtual。然而由于Line只是内含Pointobject而非继承自Point,所以被合成出来的destructor只是nonvirtual而已,但不是virtual)。其中,它的member class objects的destructor会被调用(以构造的相反顺序):

inline void Line::~Line(Line* this){

this->_end.Point::~Point();

this->_begin().Point::~Point();

}

虽然Point destructor是virtual,但其调用操作(在containing class destructor之中)会被静态地决议出来。

类似的道理,当一个程序员写下:

Line b =a;

时,implicit Line copy constructor会被合成出来,成为一个inline public member。

最后,当程序员写下:

a = b;

时,implicit copy assignment operator会被合成出来,成为一个inline public member。

虚拟继承:

考虑下面这个虚拟继承

class Point3d:public virtual Point{

public:

Point3d(float x=0.0,floaty=0.0,float z=3)

:Point(x,y),_z(z){}

Point3d(const Point3d& rhs)

:Point(rhs),_z(rhs._z){}

~Point3d();

Point3d& operator=(constPoint3d& );

virtual float z( ){return _z;}

protected:

float _z;

};

传统的constructor扩充现象并没有用,这是因为virtual base class的共享性之故。

试着想象一下三种类的派生情况:

ClassVertex : virtual public Point{…};

ClassVertex3d : public Point3d, public Vertex{…};

ClasspVertex : public Vertex 3d {…};

Vertex的constructor必须也调用Point的constructor。然而,当Point3d和Vertex同为Vertex的subobjects时,它们对Point constructor的调用操作一定不可以发生;取而代之的是,作为一个最底层的class, Vertex3d有责任将Point初始化。而更往后的继承,则由PVertex来负责完成被共享的Point subobject的构造。所以,constructor的函数本体因而必须条件式的测试传进来的参数,然后决定调用或不调用相关的virtual base class constructor。下面就是Point3d的constructor扩充内容:

//C++伪码:在virtual base class情况下的constructor扩充内容

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的调用操作。

//C++伪码:

Vertex3d*Vertex3d::Vertex3d(Vertex3d* this, bool _most_derived,

float x, float y, float z){

if(_most_derived != false)

this->Point::Point(x, y);

this->Point3d::Point3d(false, x, y,z);

this->Vertex::Vertex(false, x, y);

//设定vptrs

//安插user code

return this;

}

当我们定义

Vertex3dcv;

时,Vertex constructor正确的调用Point constructor。Point3d和Vertex的constructor会做每一件该做的事情---对Point的调用操作除外。所以,“virtual base class constructor”的被调用有着明确的定义:只有当一个完整的class object被定义出来时,他才会被调用;如果object只是某个完整object的subobject,它就不会被调用。

某些新的编译器会把每一个constructor分裂为二,一个针对完整的object,另一个针对subobject。完整object版本无条件调用virtual base constructor,设定所有的vptr等。Subobject版本则不调用virtual base constructor,也不可能设定vptr。

Vptr初始化语意学

当我们定义一个PVertex object时,constructors的调用顺序是:

Point(x,y);

Point3d(x,y, z);

Vertex(x,y, z);

Vertex3d(x,y, z);

PVertex(x,y, z);

假设这个继承类体系中的每一个class都定义了一个virtual function size(),此函数负责传回class的大小。如果我们写:

PVertexpv;

Point3dp3d;

Point* pt= &pv;

那么这个调用操作:

pt->size();

将传回PVertex的大小,而:

pt =&p3d;

pt->size();

将传回Point3d的大小。

更进一步,我们假设这个继承体系中的每一个constructors内含一个调用操作,像这样:

Point3d::Point3d(floatx, float y, float z)

:_x(x), _y(y), _z(z){

if(spyOn){

cerr << “size ofPoint3d” << size() << endl;

}

}

当我们在定义PVertex的时候,在调用Point3d constructor时,会间接调用的是Point3d::size(),而不是PVertex::size()。这是因为当base class constructor执行时,derived实例还没有被构造起来。

总而言之,当派生类在构造(或析构)时,如果调用基类的构造函数(或析构函数),而基类的构造函数会调用虚函数,即便这个虚函数也在派生类中重新定义,此时调用的虚函数仍然是基类中的虚函数。

那么如何才能做到这一点?

根本的解决之道是:在执行一个constructor时,必须限制一组virtual function候选名单,也就是控制vptr的初始化和设定操作即可。

vptr初始化操作的规则:必须在base class constructor调用操作之后,但是在程序员供应的代码或是member initialization list中所列的members初始化操作之前!

所以constructor的执行算法通常如下:

1. 在derived class constructor中,所有的virtual base classes 以及上一层base class 的constructors会被调用

2. 上述完成后,对象的vptrs被初始化,指向相关的virtual tables.

3. 如果有member initialization list的话,将在constructor体内扩展开来。这必须在vptr被设定之后才做,以免有一个virtual member function在member initialization list中被调用。

4. 最后,执行程序员所提供的代码

例如:一直由程序员所定义的PVertex constructor:

PVertex::PVertex(floatx, float y, float z)

:_next(0),Vertex(x, y, z),Point(x,y){

if(spyOn)

cerr << “size ofPVertex” << size() << endl;

}

它很有可能被扩展为:
PVertex* PVertex::PVertex(PVertex* this, bool _most_derived,

float x, float y, float z){

if(_most_derived != false) //条件调用virtual base constructor

this->Point::Point(x, y);

this->Vertex3d::Vertex3d(x, y, z); //无条件调用上一层base

this->_vptr_PVertex =_vtbl_PVertex;//将相关的vptr初始化

this->_vptr_Point_PVertex =_verbl_Point_PVertex; //将相关的vptr初始化

if(spyOn) //程序员所写代码

cerr << “size ofPVertex” <<

<<(*this->_vptr_PVertex[3].faddr)(this) //经由虚拟机制调用

<< endl;

return this;

}

在class的constructor的member initialization list中调用该class的一个虚拟函数,从vptr的角度来看是安全的,这是因为vptr保证能够在member initialization list被扩展之前,由编译器正确设定好。但是在语意上可能是不安全的,因为函数本身可能还得依赖未被设立初值的members,所以这种做法不推荐。

5.3 对象复制语意学

当我们设计一个class,并以一个class object指定给另一个class object时,我们有三种选择:

1. 什么都不做,因此得以实施默认行为

2. 提供一个explicit copy assignment operator

3. 显式地拒绝一个class object指定给另一个class object

如果选择第三点,那么只要将copy assignment operator声明为private,并且不提供其定义即可。把它设定为private,我们就不在允许于任何地点(除了member function和该class的friend之中)做赋值操作。不提供其函数定义,则一旦某个member function或friend企图影响一份拷贝,程序在链接是失败。

对于第二点,只有在默认行为所导致的语意不安全或不正确时,我们才需要设计一个copy 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 function(我们不一定要拷贝右端class object的vptr,因为它可能是一个derived class object)时。

4. 当class继承自一个virtual base class(不论此base class有没有copy operator)时。

在虚拟继承情况下,copy assignment opertator会遇到一个不可避免的问题, virtual base class subobject的复制行为会发生多次,与前面说到的在虚拟继承 情况下虚基类被构造多次是一个意思,不同的是在这里不能抑制非most-derived class 对virtual base class 的赋值行为。

5.4析构语意学

如果class没有定义destructor,那么只有在class内含的member object(抑或class自己的base class)拥有destructor的情况下,编译器才会自动合成一个出来。否则,destructor被视为不需要,也就不需要被合成。

如果一个派生类派生于两个基类,这两个基类一个有destructor(设计者定义或合成),另一个没有destructor,如果这个派生类没有显式定义destructor,那么编译器会为这个派生类合成一个destructor,它的唯一作用是调用那个有destructor的基类的destructor。

像constructor一样,目前对于destructor的一种最佳实现策略就是维护两份destructor实例:

1. 一个complete object实例,总是设定还vptrs,并调用virtual base class destructors

2. 一个base class subobject实例;除非在destructor函数调用一个virtual function,否则它绝不会调用base class destructor并设定vptr.

一个object的声明结束于destructor开始执行时,由于每一个base class destructor都轮番被调用,所以derived object实际上变成了一个完整的object。例如一个PVertex对象归还内存空间之前,会依次变成一个Vertex3d对象,一个Vertex对象,一个Point3d对象,最后变成一个Point对象。当我们的destructor中调用member function时,对象的蜕变会因为vptr的重新设定而受到影响。在程序施行destructors的真正语意将在第6章中详述。

一个由程序员定义的destructor被扩展的方式类似constructor被扩展的方式,但顺序相反:

  1. destructor的函数本体首先执行
  2. 如果class拥有member class object,而后者拥有destructor,那么它们会以其声明顺序的相反顺序被调用
  3. 如果object内含一个vptr,现在被重新设定,指向适当的base class的virtual table
  4. 如果有任何直接的(上一层)nonvirtual base classes拥有destructor,它们会以其声明顺序相反顺序被调用。
  5. 如果有任何virtual base classes拥有destructor,而目前套路的这个class是最尾端(most-derived)的class,那么它们会以原来的构造顺序的相反顺序被调用。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics