深度探索C++对象模型 2构造函数语意学(二)

2014-11-24 07:38:40 · 作者: · 浏览: 4
取的X::i”的实际偏移位置,因为pa的真正类型可以改变。编译器必须改变执行存取操作的那些代码,是X::i可以延迟至执行期才决定下来。原先cfront的做法是靠“在derived class object的每一个virtual baseclasses中安插一个指针”完成。所有“经由reference或pointer来存取一个virtualbase class”的操纵都可以通过相关指针完成。在我的例子中,foo()可以被改写如下,以符合这样的策略:

//可能的编译器转变操作

void foo( const A* pa ){ pa->_vbcX->I= 1024; }

其中,_vbcX表示编译器所产生的指针,指向virtualbase class X。

_vbcX(或编译器所作出的某个东西)是在class object构造期间被完成的。对于class所定义的每一个constructor,编译器会安插那些“允许每一个virtual base class的执行期存取操作”的代码。如果class没有声明任何constructors,编译器必须为它合成一个defaultconstructor。

总结:有四种情况编译器必须为未声明constructor的class合成一个default constructor。其他情况,我们说class拥有implicit trivaldefault constructor,他们实际上并不会被合成出来。

在合成的default constructor中,只有base classsubobject和member class object会被初始化。所有其他的nonstatic data member(如整数、整数指针、整数数组)都不会被初始化。这些nonstatic data member初始化操作队程序而言或许有需要,但对编译器而言非必要。

C++新手常见的两个误解:

1. 任何class如果没有定义default constructor,就会合成一个

2. 编译合成出来的defaultconstructor会显式设定“class 内每一个data member的默认值”。

如你所见,没有一个是真的!

2.2 CopyConstructor的构造操作

以一个object的内容作为另一个class object的初值的三种情况:

    对一个object做显示初始化操作 当object被当做参数交给某个函数时 当函数传回一个class object时

    假如class设计者显示定义了一个copyconstructor,大部分情况下,一个object的内容作为另一个同类object的初值时,上述copy constructor会调用。
    Default Memberwise Initialization

    当class object以“相同class的另一个object”作为初值,其内部是以所谓的defaultmemberwise initialization手法完成的,也就是把每一个内建的或派生的data member的值,从某个object拷贝一份到另一个object身上。不过它并不会拷贝其中的memberclass object,而是以递归的方式施行memberwiseinitialization。

    C++Standard上说,如果class没有声明一个copy constructor,就会有隐式的声明或隐式的定义出现。和以前一样,C++把copy constructor区分为trivial和nontrivial。只有nontrivial的实例才会被合成与程序中。决定一个copy constructor是否为trivial的标准在于class是否展现出所谓的bitwise copysemantics。

    bitwisecopy semantics(位逐次拷贝)

    classWord{

    public:

    Word(const char* );

    ~Word(){ delete [] str; }

    private:

    intcntl

    char*str;

    };

    上面这个类的一个实例作为另一个该类的实例的初值时,不会合成一个default copy constructor,因为上述声明展现了bitwise copy semantics。

    classWord{

    public:

    Word(const String& );

    ~Word();

    private:

    intcnt;

    Stringstr;

    };

    上面这个类中的string有一个explicit copyconstructor,当该类一个实例作为另一个该类的实例的初值时,会合成一个default copy constructor,以便调用member class String object的copy constructor。不展现bitwise copysemantics。被合成出来的copy constructor的伪代码如下:

    inlineWord::Word( const Word& wd )

    {

    str.String::String( wr.str );

    cnt = wd.cnt;

    }

    在合成出来的copy constructor中,如整数,指针,数组等的nonclassmembers也都会被复制。

    不展现Bitwise Copy Semantics!

    1.当一个class内含一个member object而后者的class声明有一个copy constructor时(不论是显示声明还是编译器合成)

    2.当class继承自一个base class而后者存在一个copy constructor时

    3.当class声明一个活多个virtual functions时

    4.当class派生自一个继承串链,其中有一个或多个virtualbase classes时

    前两种情况,编译器必须将member或base class的copy constructor调用操作安插到被合成的copyconstructor中。后两种情况接下来讨论。

    重新设定virtual table的指针

    当一个class声明一个或多个virtualfunctions,编译器会进行两个扩张操作:

    1. 增加一个virtualfunction table(vtbl),内含每一个有作用的virtualfunction的地址。

    2. 一个指向virtualfunction table的指针,安插在每一个class object内。

    如果编译器对于每一个新产生的class object的vptr不能成功而正确地设好其初值,将导致可怕的后果。因此,当编译器导入一个vptr到class之中时,该class就不再展现bitwise semantics了,过需要合成一个copy constructor以将vptrs适当地初始化。如:

    \

    当父类对象一另一个父类对象作为初值或子类对象以另一个子类对象作为初值时,都可以直接依靠bitwise copy semantics完成。如< http://www.2cto.com/kf/ware/vc/" target="_blank" class="keylink">vcD4KPHA+ICAgICAgICAgQmVhcnlvZ2k7PC9wPgo8cD4gICAgICAgICBCZWFyd2lubmllID0geW9naTsgICAgICAgICAgIC8vsNF5b2dptcR2cHRy1rG907+9sbS4+Hdpbm5pZbXEdnB0csrHsLLIq7XEoaM8L3A+CjxwPjxpbWcgc3JjPQ=="https://www.cppentry.com/upload_files/article/49/1_r7woa__.jpg" alt="\">

    当父类对象以子类对象作为初始化时,其vptr复制操作也必须保证安全。如

    ZooAnimalfranny = yogi; //这里会发生切割行为

    不能将franny 的vpt指向Bear claass的virtual table。也就是说合成出来的ZooAnimal copy constructor会显示设定object的vptr指向ZooAnimal Class的virtual。

    处理VirtualBase Class Subobject

    一个class object如果以另一个object作为初值,而后者有一个virtual base class subject,那么也会使“bitwise copy semantics”失效,所以编译器必须合成一个copyconstructor,安插一些代码以设定virtual baseclass pointer/offset的初值,对每一个members执行必要的memberwise初始化操作,以及执行其他的内存相关工作。如果是两个同一级别的同类对象(也就是说两个对象都是一个类的实例,不分别是子类和父类的实例),那么bitwise copy就绰绰有余了。

    2.3程序转化语意学

    #include “X.h”

    X foo()

    {

    Xxx;

    Returnxx;

    }

    已知上述程序,一个人可能会做出如下假设:

    1. 每次foo()被调用,就传回xx的值

    2. 如果class X定义了一个copy constructor,那么当foo()被调用时,保证该copy constructor也会被调用。

    实际上,上诉两个假设都不一定正确。第一个假设必须看class X如何定义而定,见本节如下的返回值初始化部分。第二个假设可能会被NVR优化,见本节在编译器层面做优化部分

    显示的初始化操作:

    X x0;

    Void foo_bar(){

    Xx1(x0);

    Xx2 = x0;

    Xx3=X(x0);

    }

    必要的程序转化有两个阶段:

    1. 重写每一个定义,其中的初始化操作会被剥除。(这里的定义指“占用内存的行为”)

    2. class的copy constructor调用操作会被安插进去。

    //可能的程程序转化,伪码

    Void foo_bar(){

    Xx1; //定义被重写,初始化操作被剥除,也就是没有调用默认构造函数,下同。

    Xx2; //定义被重写,初始化操作被剥除

    X x3; //定义被重写,初始化操作被剥除

    //安插X copy constructor调用操作。

    x1.X::X(x0);

    x1.X::X( x0);

    x1.X::X( x0); //其实这里会x0不正确。会首先通过x0产生一个临时变量,临时变量通//过copy constructor赋值给x1.

    }

    X xx0(1024);

    X xx1=X(1024);

    X xx2=(X)1024;

    第二行和第三行语法明显提供了两个步骤的初始化操作:

    1. 将一个临时性的object设以初值1024

    2. 将临时性的object以拷贝构造的方式作为explicit object的初值

    换句话说xx0是被单一的constructor设定初值

    xx0.X::X(1024);

    而xx1和xx2的初始化转化如下:

    X _temp0;

    _temp0.X::X(1024);

    xx1.X::X(_temp0);

    _temp.X::~X();

    参数的初始化:

    已知函数

    Void foo( X x0);

    下面这样调用函数

    X xx;

    foo(xx);

    则其中一种实现策略是导入临时性object,并调用copy constructor将它初始化,然后将此临时性object交给函数,如下:

    X _temp0;

    _temp0.X::X( xx );

    foo( _temp0 );

    但这样的转换还不到位,foo()的声明也需要被转化,形式参数必须从原先的一个class X object改变为class X 引用。如下:

    void foo( X& x0);

    在foo函数完成后,会掉用析构函数对付临时性的object。

    另一宗实现策略是拷贝建构,把实际参数直接建构在相应位置上,记录于程序堆栈中。

    返回值的初始化:

    X bar(){

    X xx;

    //处理xx

    return xx;

    }

    返回值如何从局部对象拷贝过来,Stroustrup解决办法是一个双阶段转化

    1.首先加上一个额外参数,类型是class object的一个reference,用来存放拷贝建构而得的返回值。

    2.在return指令之前安插一个copy constructor调用操作,以便将欲传回的object的内容当做上述新增参数的初值。

    则上述代码转化如下:

    void bar(X&_result){//加上一个额外参数

    X xx;

    //编译器所产生的default constructor调用操作

    xx.X::X();

    //…处理xx;

    //编译器所产生的copy constructor调用擦做

    _result.X::X( xx );

    Return;

    }

    现在编译器必须转化每一个bar()调用操纵,以反映其新定义。例如:

    X xx = bar();

    将被转化为下列两个指令语句:

    X xx; //注意不实行default constructo

    bar( xx );

    在使用者层面做优化:

    程序优化的观念:定义一个“计算用”的constructor,换句话说程序员不再写

    X bar( const T&y,const T &z){

    X xx;

    //…以y和z来处理xx

    return xx;

    }

    那会要求xx被“memberwise”的拷贝到编译器所产生的_resul中。Jonathan定义另一个constructor,可以直接计算xx的值:

    X bar( const T &y, const T &z){

    returnX ( y, z );

    }

    于是当bar被转化后效率比较高:

    void bar( X &_result, const T &y,const T &z) ){

    _result.X::X(y, z );

    return;

    }

    _result被直接计算出来,而不是经由拷贝构造函数拷贝而得!但有时定义这样的构造函数没有实际意义。

    在编译器层面优化:

    X bar(){

    X xx;

    //处理xx

    return xx;

    }

    Named Return Value (NRV) optimization,具名返回值优化,实现这种优化有个前提,就是必须提供copy constructor,因为NRV优化的目的就是为了剔除copy constructor的使用。只有有了才能被剔除,否则谈不上剔除。一般的如果不优化NRV,上诉实现就是类似于返回值的初始化中的过程,而实现了优化的过程则如下所示,避免了copy constructor的使用。

    void bar( X &_result ){

    //defaultconstructor被调用

    _result.X::X();

    //直接处理_result

    return;

    }

    CopyConstructor:要还是不要?

    如果对象面临大量的拷贝操作[ 比如这个class的object需要经常以传值的方式返回],有必要实现一个拷贝构造函数以支持NRV优化。但是如果想使用底层的memcpy之类的直接进行bit wise copy,注意是否真的是bit wise copy拷贝,比如如果是virtual,这样可能破坏调vptr,如下:

    class Shape{

    public:

    Shape(){memset(this,0,sizeof( Shape )); }

    Virtual~Shape();

    }

    编译器会为此constructor扩张的内容看起来像这样:

    Shape::Shape(){

    _vptr_Shape= vtbl_Shape;

    Memset(this, 0, sizeof( Shape )); //memset会将vptr清为0,出错

    };

    2.4成员们的初始化队伍(MemberInitialization List)

    当你写下一个constructor时,就有机会设定class members的初值,要不是经由memberinitialization list就是在constructor函数本体之内。但有4中情况必须用memberinitialization list来初始化:

    1. 当初始化一个referencemember时

    2. 当初始化一个constmember时

    3. 当调用一个base class的constructor,而它拥有一组参数时

    4. 当调用一个memberclass的constructor,而它拥有一组参数时

    有些情况可以在constructor函数本体之内设定classmembers的初值,但效率不高,如:

    class Word{

    String _name;

    int _cnt;

    public:

    Word(){

    _name = 0;

    _cnt = 0;

    }

    }

    将会被转化成:

    Word::Word(){

    _name.String::String();

    Stringtemp = String(0);

    _name.String::operator=(temp);

    temp.String::~String();

    _cnt = 0;

    }

    而如下两种方式:

    Word::Word : _name(0){

    _cnt= 0;

    }

    或Word::Word: _cnt (0),_name(0){

    }

    都将被转化为

    Word::Word(){

    _name.String::String(0); //编译器会一一处理initialization list,顺序按class中的

    _cnt = 0; //members的声明顺序决定,而不是由initializationlist中排列顺序决定。

    }

    另外需说明的一点是编译器对initializationlist处理所安插在constructor内中的代码都置于用户用户定义的代码之前。