深度探索C++对象模型(五)构造、析构、拷贝语意学

考虑下面这个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将无法决定初值,

class Concrete_derived:public Abstract_base{
public:
    Concrete_derived();
    //...
};
void foo()
{
    //Abstract_base::_mumble 未被初始化
    Concrete_derived trouble;
    //...
}

derived class的唯一要求就是Abstract_base必须提供一个带有唯一参数的protected constructor:

Abstract_base::
Abstract_base(char *mumble_value=0)
:_mumble(mumble_value)
{   
}

一般而言,class的data member应该被初始化,并且只在constructor中或是在class的其他member functions中指定初值。其他任何操作都将破坏封装性质,使class得维护和修改更加困难。

纯虚函数的存在(Presence of a Pure Virtual Function)
一个人可以定义和调用(invoke)一个pure virtual function;不过它只能被静态地调用,不能经由虚拟机制调用。
例如,

//ok:定义pure virtual function
// 但只可能被静态地调用(invoked statically)
inline void Abstract_base::interface()const
{
    function
    //....
}

inline void
Concreate_derived::interface() const
{
    //ok:静态调用(static invocation)
    Abstract_base::interface();
    //....
}

class设计者一定要定义pure virtual destructor。因为每一个derived class destructor会被编译器加以扩张,以静态调用的方式调用其“每一个virtual base class”以及“上一层base class”的destructor。因此,只要缺乏任何一个base class destructors的定义,就会导致链接失败。
你可能会争辩说,难道对一个pure virtual destructor的调用操作,不应该在“编译器扩张derived class的destructor”时压抑下来吗?不!class设计者可能已经真的定义了一个pure virtual destructor。这样的设计时以C++语言的一个保证为前提:继承体系中每一个class object的destructor都会被调用。所以编译器不能够压抑这一调用操作。
编译器没有足够的知识“合成”一个pure virtual destructor的函数定义,因为编译器对一个可执行文件采取“分离编译模型”之故。是的,开发环境可以提供一个设备,在链接时找出pure virtual destructor不存在的事实,然后重新激活编译器,赋予一个特殊指令,以合成一个必要的函数实例;但是我不知道目前是否有任何编译器这么做。
一个比较好的替代方案就是,不要把virtual destructor声明为pure。

虚拟规格的存在(Presence of a Virtual Specification)

如果你决定把函数定义内容并不与类型有关设计为一个virtual function,那将是一个槽糕的选择,因而几乎不会被后继的derived class改写。此外,由于它的non-virtual函数实例是个inline函数,如果常常被调用的话,效率上的报应不可谓不轻。
一般而言,把所有的成员函数都声明为virtual function,然后再靠编译器的优化操作把非必要的virtual invocation去除,并不是好的设计理念。

虚拟规格中const的存在
决定一个virtual function是否需要const,似乎是件琐碎的事情。但当你真正面对一个abstract base class时,却不容易做决定。做这件事情,意味着得预期subclass实例可能被无穷次数地使用。不把函数声明为const,意味着此函数不能够获得一个const reference或const pointer。比较令人头大的是,声明一个函数为const,然后才发现实际上其derived instance必须修改一个data member。我不知道有没有一致的解决方法,我的想法很简单,不再用const就是了。

重新考虑class的声明
下面定义的Abstract_base才是比较适当的一种设计:

class Abstract_base{
public:
    virtual ~Abstract_base();
    virtual void interface()=0;
    const char* mumble() const{return _mumble;}
protected:
    Abstract_base(char *pc=0);
    char *_mumble;
};

(一)“无继承”情况下的对象构造
在C中,global被视为一个“临时性的定义”,因为它没有显式的初始化操作。一个“临时性的定义”可以在程序中发生多次。那些实例会被链接器折叠起来,只留下单独一个实例,被放在程序data segment中一个“特别保留给未初始化之global objects使用”的空间。这块空间被称为BSS,这是Block Started by Symbol的缩写。
C++并不支持“临时性的定义”,这是因为class构造函数的隐式引用之故。C和C++的一个差异就在于,BSS data segment在C++中相对地不重要。C++的所有全局对象都被以“初始化过的数据”来对待。

抽象数据类型(Abstract Data Type)
如果要将class中的所有成员都设定常量初值,那么给予一个explicit initialization list会比较有效率些(比起意义相同的constructor的inline expansion而言)。

Explicit initialization list带来三项缺点:
1、只有当class members都是public,此法才奏效。
2、只能指定常量,因为它们在编译时期就可以被评估求值。
3、由于编译器并没有自动施行之,所以初始化行为的失败可能性会高一些。

为继承做准备
virtual functions的导入促使每一个Point object拥有一个virtual table pointer。这个指针给我们提供virtual接口的弹性,其成本是:每一个object需要额外的一个word空间。
除了每一个class object多负担一个vptr之外,virtual function的导入也引发编译器对于我们的Point class产生膨胀作用:
1、我们所定义的constructor被附加了一些代码,以便将vptr初始化。这些代码必须被附加在任何base class constructors的调用之后,但必须在任何由使用者(程序员)供应的代码之前。
2、合成一个copy constructor和一个copy assignment operator,而且其操作不再是trivial(但implicit destructor仍然是trivial)。如果一个类对象被初始化或以一个derived class object赋值,那么以位为基础(bitwise)的操作可能对vptr带来非法设定。
编译器在优化状态下可能会把object的连续内容拷贝到另一个object身上,而不会实现一个精确地“以成员为基础(memberwise)的赋值操作”。C++ standard要求编译器尽量延迟nontrivial members的实际合成操作,直到真正遇到其使用场合为止。

(二)继承体系下的对象构造
Constructor可能内含大量的隐藏码,因为编译器会扩充每一个constructor,扩充程度视class T的继承体系而定。一般而言编译器所做的扩充操作大约如下:
1、记录在member initialization list中的data members初始化操作会被放进constructor的函数本体,并以members的声明顺序为顺序。
2、如果有一个member并没有出现在member initialization list之中,但它有一个default constructor,那么该default constructor必须被调用。
3、在那之前,如果class object有virtual table pointer(s),它(们)必须被设定初值,指向适当的virtual table(s)。
4、在那之前,所有上一层的base class constructors必须被调用,以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 constructors必须被调用,从左到右,从最深到最浅:

a、如果class被列于member initialization list中,那么如果有任何显示指定的参数,都应该传递过去。若没有列于list之中,而class有一个default constructor,亦应该调用之。
b、此外,class中的每一个virtual base class subobject的偏移位置(offset)必须在执行期可被存取。
c、如果class object是最底层(most-derived)的class,其constructors可能被调用;某些用以支持这一行为的机制必须被放出来。

虚拟继承(Virtual Inheritance)
考虑下面这个虚拟继承(继承自我们的Point):

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扩充现象”并没有用,这是因为virtual base class的“共享性”之故:

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

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的调用操作一定不可以发生;取而代之的是,作为一个最底层的class,Vertex3d有责任将Point初始化。而更往后(往下)的继承,则由PVertex(不再是Vertex3d)来负责完成“被共享之Point subobject”的构造。
“virtual base class constructors的被调用”有着明确的定义:只有当一个完整的class object被定义出来时,它才会被调用;如果object只是某个完整object的subobject,它就不会被调用。
某些新进的编译器把每一个constructor分裂为二,一个针对完整的object,另一个针对subobject。“完整object”版无条件地调用virtual base constructor,设定所有的vptrs等。“subobject”版则不调用virtual base constructors,也可能不设定vptrs等。

vptr初始化语意学(The Semantics of the vptr Initialization)

Constructors的调用顺序是:由根源而末端(bottom up)、由内而外(inside out)。当base class constructor执行时,derived实例还没有被构造起来。在PVertex constructor执行完毕之前,PVertex并不是一个完整的对象;Point3d constructor执行之后,只有Point3d subobject构造完毕。

vptr初始化操作应该如何处理?本质而言,这得视vptr在constructor之中“应该在何时被初始化”而定。我们有三种选择:
1、在任何操作之前;
2、在base class constructors调用操作之后,但是在程序员供应的代码或是“member initialization list中所列的members初始化操作”之前。
3、在每一件事情发生之后。

策略2解决了“在class中限制一组virtual functions名单”的问题。如果每一个constructor都一直等待到其base class constructors执行完毕之后才设定其对象的vptr,那么每次它都能够调用正确的virtual function实例。

令每一个base class constructor设定其对象的vptr,使它指向相关的virtual table之后,构造中的对象就可以严格而正确地变成“构造过程中所幻化出来的每一个class”的对象。也就是说,一个PVertex对象会先形成一个Point对象、一个Point3d对象、一个Vertex对象、一个Vertex3d对象,然后才成为一个PVertex对象。在每一个base class constructor中,对象可以与constructor’s class的完整对象做比较。对于对象而言,“个体发生学”概括了“系统发生学”。constructor的执行算法通常如下:

1、在derived class constructor中,“所有virtual base classes”及“上一层base class”的constructors会被调用。
2、上述完成之后,对象的vptr(s)被初始化,指向相关的virtual table(s)。
3、如果有member initialization list的话,将在constructor体内扩展开来。这必须在vptr被设定之后才做,以免有一个virtual member function被调用。
4、最后,执行程序员所提供的代码。

下面是vptr必须被设定的两种情况:
1、当一个完整的对象被构造起来时。如果我们声明一个Point对象,则Point Constructor必须设定其vptr。
2、当一个subobject constructor调用了一个virtual function(不论是直接调用或间接调用)时。

在class的constructor的member initialization list中调用该class的一个虚拟函数,安全吗?
就实际而言,将此函数施行于其class’s data member的初始化行动中,总是安全的。这是因为,正如我们所见,vptr保证能够在member initialization list被扩展之前,由编译器正确设定好。但是在语意上这可能是不安全的,因为函数本身可能还得依赖未被设立初值的members。所以这种做法不推荐。然而,从vptr的整体角度来看,这是安全的。

何时需要供应参数给一个base class constructor?这种情况下在“class的constructor的member initialization list中”调用该class的虚拟函数,仍然安全吗? 不!此时vptr若不是尚未被设定好,就是被设定指向错误的class。更进一步地说,该函数所存取的任何class’s data members一定还没有被初始化。

(三)对象复制语意学(Object Copy Semantics)
当我们设计一个class,并以一个class object指定给另一个class object时,我们有三种选择:
1、什么都不做,因此得以实施默认行为。
2、提供一个explicit copy assignment operator。
3、显示地拒绝把一个class object指定给另一个class object。
如果要选择第3点,不准将一个class object指定给另一个class object,那么只要将copy assignment operator声明为private,并且不提供其定义即可。把它设为private,我们就不再允许于任何地点(除了在member functions以及该class的friends之中)做赋值(assign)操作。不提供其函数定义,则一旦某个member function或friend企图影响一份拷贝,程序在链接时就会失败。一般认为这和链接器的性质有关(也就是说并不属于语言本身的性质),所以不是很令人满意。

只有在默认行为所导致的语意不安全或不正确时,我们才需要设计一个copy assignment operator(关于memberwise copy及其潜在陷阱)。当不会发生“别名化”或“内存泄漏”时可认为memberwise copy 行为是安全的。

一个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 object)时。
4、当class继承自一个virtual base class(不论此base class有没有copy operator)时。

C++ Standard上说copy assignment operators并不表示bitwise copy semantics是nontrivial。实际上,只有nontrivial instances才会被合成出来。

(五)析构语意学(Semantics of Destruction)
如果class没有定义destructor,那么只有在class内含的member object(抑或class自己的base class)拥有destructor的情况下,编译器才会自动合成出一个来。否则,destructor被视为不需要,也就不需被合成(当然更不需要被调用)。
我们应该拒绝这种奇怪的“对称策略”:“已经定义了一个constructor,所以提供一个destructor也是天经地义的事”。事实上,我们应该因为“需要”而非“感觉”来提供destructor,更不要因为你不确定是否需要一个destructor,于是就提供它。

一个由程序员定义的destructor被扩展的方式类似constructors被扩展的方式,但顺序相反:
1、如果object内含一个vptr,那么首先重设(reset)相关的virtual table。
2、destructor的函数本体现在被执行,也就是说vptr会在程序员的代码执行前被重设(reset)。
3、如果class拥有member class objects,而后者拥有destructors,那么它们会以声明顺序的相反顺序被调用。
4、如果有任何直接的(上一层)nonvirtual base classes拥有destructor,它们会以其声明顺序的相反顺序被调用。
5、如果有任何virtual base classes拥有destructor,到目前讨论的这个class的最尾端(most-derived)的class,那么它们会以其原来的构造顺序的相反顺序被调用。

就像constructor一样,目前对于destructor的一种最佳实现策略就是维护两份destructor实例:
1、一个complete object实例,总是设定好vptr(s),并调用virtual base class destructors。
2、一个base class subobject实例;除非在destructor函数中调用一个virtual function,否则它绝不会调用virtual base class destructors并设定vptr。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值