C++对象模型——Default Constructor的建构操作(第二章)

第2章    构造函数语意学 (The Semantics of Constructor)

    关于C++,最常听到的一个抱怨就是,编译器背着程序员做了太多事情.Conversion运算符就是最常被引用的一个例子.

2.1    Default Constructor的建构操作

    C++ Annotated Reference Manual (ARM)指出"default constructors ...在需要的时候被编译器产生出来".关键字眼是"在需要的时候".被谁需要?做什么事情?看看下面这段程序代码:
class Foo {
public:
    int val;
    Foo *pnext;
};

void foo_bar() {
    Foo bar;
    if (bar.val || bar.pnext)
        // ... do something
}
    在这个例子中,正确的程序语意是要求Foo有一个default constructor,可以将它的两个members初始化为0,上面这段代码不符合ARM所述的"在需要的时候".其间的差别在于一个是程序的需要,一个是编译器的需要.上述代码不会合成出一个default constructor.
    那么 什么时候才会合成出一个default constructor呢?当编译器需要它的时候!此外,被合成出来的constructor只执行编译器所需的行动,也就是说,即使有需要为class Foo合成一个default constructor,那个constructor也不会将两个data members val和pnext初始化为0.为了让上一段代码正确执行,class Foo的设计者必须提供一个显式的default constructor,将两个members适当地初始化.
    C++ Standard已经修改了ARM的说法,虽然其行为事实上仍然相同.C++ Standard指出"对于class X,如果没有任何user-declared constructor,那么会有一个default constructor被隐式声明出来....一个被隐式声明的default constructor将是一个trivial constructor..."

"带有 Default constructor"的Member Class Object

    如果一个class没有任何constructor,但它内含一个member object,而后者有default constructor,那么这个class的implicit default constructor就是"nontrivial",编译器需要为此class合成出一个default constructor,不过这个合成操作只有在constructor真正需要被调用时才会发生.
    于是出现一个有趣的问题: 在C++各个不同的编译模块中,编译器如何避免合成出多个default constructor(譬如说一个是为A.C档合成,另一个为B.C档合成)?解决的办法是把合成的default constructor,copy constructor,destructor,assignment copy operator都以inline方式完成.一个inline函数有静态链接(static linkage),不会被档案以外者看到.如果函数太复杂,不适合做成inline,就会合成出一个explicit non-inline static实体.
    例如,下面的程序片段中,编译器为class Bar合成一个default constructor:
class Foo {
public:
    Foo();
    Foo(int)
    ...
};
class Bar {
public:
    Foo foo;
    char *str;
};
void foo_bar() {
    Bar bar;    //Bar::foo必须在此处初始化
                //Bar::foo是一个member object.而其class Foo拥有defautl constructor
    if (str) {
        ...
    }
};
    被合成的Bar default constructor内含必要的代码,能够调用class Foo的default constructor来处理member object Bar::foo,但它并不产生任何代码来初始化Bar::str.将Bar::foo初始化是编译器的责任,将Bar::str初始化是程序员的责任,被合成的default constructor可能如下:
// Bar的default constructor可能被这样合成
// 被member foo调用class Foo的default constructor
inline Bar::Bar() {
    // C++伪代码
    foo.Foo::Foo();
}

    注意,被合成的default constructor只满足编译器的需要,而不是程序的需要,为了让这个程序片段能够正确执行,字符指针str也需要被初始化.假设程序员经由下面的default constructor提供了str的初始化操作:
// 程序员定义的default constructor
Bar::Bar() { str = 0; }
    现在程序的需求获得满足了,但是编译器还需要初始化member object foo.由于default constructor已经被显式定义,编译器没有办法合成第二个.
    编译器采取行动:如果 class A内含一个或一个以上的member class objects,那么 class A的每一个constructor必须调用每一个member classes的default constructor,编译器会扩张已存在的constructors,在其中安插一些码,使得user code在被执行之前,先调用必要的default constructors.沿续前一个例子,扩张后的constructors可能像这样:
// 扩张后的default constructor
// C++伪代码
Bar::Bar() {
    foo.Foo::Foo();    // 附加上complier code
    str = 0;        // 显式user code
}
    如果有多个class member objects都要求constructor初始化操作,将如何呢? C++语言要求以"member objects在class中的声明次序"来调用各个constructors.这一点由编译器完成,它为每一个constructor安插程序代码,以"member声明次序"调用每一个member所关联的default constructor.这些码被安插在显式user code之前.如果有如下所示三个classes:
class Dopey {
public:
    Dopey();
};
class Sneezy {
public:
    Sneezy(int);
    Sneezy();
};
class Bashful {
public:
    Bashful();
};
    以及一个 class Snow_White:
class Snow_White {
public:
    Dopey dopey;
    Sneezy sneezy;
    Bashful bashful;
private:
    int number;
};
    如果Snow_White没有定义default constructor,就会有一个nontrivial constructor被合成出来,依次序调用Dopey、Sneezy、Bashful的default constructor。然而如果Snow_White定义下面这样的default constructor:
// 程序员所写的default constructor
Snow_White::Snow_White() : Sneezy(1024) {
    member = 2048;
}
    它会被扩张为:
// 编译器扩张后的default constructor
// C++伪代码
Snow_White::Snow_White() : Sneezy(1024) {
    // 插入member class object
    // 调用其constructor
    dopey.Dopey::Dopey();
    sneezy.Sneezy::Sneezy(1024);
    bashful.Bashful::Bashful();
    // 显式user mode
    member = 2048;
}
    顺序依次为类的成员对象,类的构造函数。

"带有Default Constructor"的Base Class

    如果一个没有任何的constructors的class派生自一个"带有default constructor"的base class ,那么这个derived class 的default constructor会被视为nontrivial,并因此需要被合成出来,它将调用上一层的base classes的default constructor(根据它们的声明次序),对一个后继派生的 class 而言,这个合成的constructor和一个"被明确提供的default constructor"没有差异。
    如果设计者提供多个constructor,但其中都没有default constructor呢?编译器会扩张现有的每一个constructor,将"用以调用所有必要的default constructors"的程序代码加进去,它不会合成一个新的default constructor,这是因为其他"由user所提供的constructor"存在的缘故。如果同时存在"带有default constructors"的member class objects,那些default constructor也会被调用在——在所有base class constructor都被调用之后。可以看出, 构造是从类层次的最根处开始的,在每一层,首先调用基类构造函数,然后调用成员对象的构造函数

"带有一个Virtual Function"的Class

    另有两种情况,也需要合成出default constructor:
    1.    class 声明(或继承)一个virtual function
    2.    class 派生自一个继承串链,其中有一个或更多的virtual base classes.
    不管哪一种情况,由于缺乏由user声明的constructors,编译器会详细记录合成一个default constructor的必须信息,以下面这个程序片段为例:
class Widget {
public:
    virtual void flip() = 0;
    // ...
};
void flip(const Widget &widget) {
    widget.flip();
}
// 假设Bell和Whistle都派生自Widget
void foo() {
    Bell b;
    Whistle w;
    flip(b);
    flip(w);
}
    下面两个扩张会在编译期间发生:
    1.    一个virtual function table(在cfront中被称为vtbl)会被编译器产生出来,内放 class 的 virtual functions 地址。
    2.    在每一个 class object中,一个额外的pointer member(也就是vptr)会被编译器合成出来,内含相关的 class vtbl的地址。
    此外,widget.flip()的虚拟引发操作(virtual invocation)会被重新改写,以使用widget的vptr和vtbl中的flip()条目:
// widget.flip()的虚拟引发操作(virtual invocation)的转变
(*widget.vptr[1])(&widget)
    其中:
    1表示flip()在virtual table中的固定索引
    &widget代表要交给"被调用的某个flip()函数实体"的this指针。
    为了让这机制发挥功效, 编译器必须为每一个Widget(或其派生类的)object的vptr设定初始值,放置适当的virtual table地址。对于 class 所定义的每一个constructor,编译器会安插一些代码做这样的事情(看5.2节)。对于那些未声明任何 constructor的classes,编译器会为它们合成一个default constructor,以便正确地初始化每一个 class object的vptr.

"带有一个Virtual Base Class"的Class

    Virtual base class 的实现法在不同的编译器之间有极大的差异,然而, 每一种实现法的共通点在于必须使virtual base class 在其每一个derived class object 中的位置,能够于执行期准备妥当,例如下面这段代码中:
class X {
public:
    int i;
};
class A : public virtual X {
public:
    int j;
};
class B : public virtual X {
public:
    double d;
};
class C : public A, public B {
public:
    int k;
};
// 无法在编译时期决定(resolve) pa->X::i的位置
void foo(const A *pa) {
    pa->i = 1024;
}
main()
{
    foo(new A);
    foo(new C);
    // ...
}
    编译器无法确定foo()中"经由pa而存取的X::i"的实际偏移位置,因为pa的真正类型可以改变。 编译器必须改变"执行存取操作"的那些码,使X::i可以延迟至执行期才决定。原先cfront的做法是靠"在derived class object的每一个virtual base classes中安插一个指针"完成。所有"经由reference或pointer来存取一个virtual base class"的操作都可以通过相关指针完成。foo可以被改写如下,以符合这样的实现策略:
// 可能的编译器转换操作
void foo(const A *pa) {
    pa->_vbcX->i = 1024;
}
    其中,_vbcX表示编译器所产生的指针,指向virtual base class X.
    _vbcX(或编译器所做出的某个东西)是在 class object 建构期间被完成的。对于 class 所定义的每一个constructor,编译器会安插那些"允许每一个virtual base class的执行期存取操作"的码。如果 class 没有声明任何constructor,编译器必须为它合成一个default constructor.

总结

    有四种情况,会导致"编译器必须为未声明constructor的classes合成一个default constructor‘",C++ Stardard把那些合成物称为implicit nontrivial default constructor。被合成出来的constructor只能满足编译器(而非程序)的需要。它之所以能够完成任务,是借着"调用member object或者base class的default constructor"或者"为每一个object初始化其virtual function机制或virtual base class机制"而完成。至于没有存在那四种情况而又没有声明任何constructor的classes,它们拥有的是implicit trivial default constructor,它们实际上并不会被合成出来.
    在合成的default constructor中,只有base class subjects和member class objects会被初始化,所有其它的nonstatic data member,如整数,整数指针,整数数据等都不会被初始化,这些初始化操作对程序而言或许需要,但对编译器则并非必要.如果程序需要一个"把某指针设为0"的default constructor,那么提供它的应该是程序员.
    C++新手一般有两个常见的误解:
    1.    任何class如果没有定义default constructor,就会被合成出来
    2.    编译器合成出来的default constructor会明确设定"class 内有每一个data member的默认值"
    这两个都是错误的.


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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值