读书笔记《Inside the C++ Object Model》:Default Constructor的构造操作

来源:互联网 发布:柠檬tv网络电视 编辑:程序博客网 时间:2024/06/10 01:28

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

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

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

实际上,没有一个是真的!


C++ Annotated Reference Manual(ARM)告诉我们“default constructors在需要的时候被编译器产生出来”。关键字眼是“在需要的时候”。被谁需要?做什么事情?看看下面这段程序代码:

class Foo {public:int val;Foo *pnext;};void foo_bar(){Foo bar;if (bar.val || bar.pnext) {cout << "no init bar members" << endl;} else {cout << "init bar members" << endl;}}

在VC2013中测试结果编译报错:使用了未初始化的局部变量“bar”。

在这个例子中,正确的程序语义是要求Foo有一个default constructor,可以将它的两个members初始化为0。上面这段代码可曾符合所说的“在需要的时候”?答案是NO。

其间的差别在于一个是程序的需要,一个是编译器的需要。程序如果有需要,那是程序员的责任。本例中要承担责任的是设计class Foo的人。所以上述代码编译器并不会合成出一个default constructor。

但如果把bar定义为global,VC2013中测试正常编译通过,运行结果:init bar members

Global objects的内存保证会在程序启动的时候被清为0。Local objects配置于程序的堆栈中,heap objects配置于自由空间中,都不一定会被清0,它们的内容将是内存上次被使用后的遗迹。这点和C语言的机制相同。


什么时候编译器才会合成一个default constructor呢?

当编译器需要它的时候!此外,被合成出来的constructor只执行编译器所需要的行动。也就是说,即使有需要为class Foo合成一个default constructor,那个constructor也不会将两个data members val和pnext初始化为0。为了让上一段代码正确编译执行,class Foo的设计者必须提供一个显示的default constructor,将两个members适当地初始化。

C++ Standard已经修改了ARM中的说法,虽然其行为事实上仍然是相同的。C++ Standard[ISO-C++95]的Section 12.1这么说:

对于class X,如果没有任何user-declared constructor,那么会有一个default constructor被隐式声明出来……一个隐式声明出来的default constructor将是一个trivial(浅薄而无能,没啥用的)constructor……

C++ Standard然后开始一一叙述什么情况下这个implicit default constructor会被视为trivial。一个nontrivial default constructor在ARM的术语中就是编译器所需要的那种,必要的话由编译器合成出来。

nontrivial default constructor有下面4种情况。

1.“带有Default Constructor”的Member Class Object

如果一个class没有任何constructor,但它内含一个member object,而后者有default constructor,那么这个class的implicit default constructor就是“nontrivial”,编译器需要为该class合成出一个default constructor。不过这个合成操作只有在constructor真正需要被调用时才会发生。

看个例子,在下面程序中,编译器为class Bar合成一个default constructor:

class Foo {public://class Foo拥有default constructorFoo() {cout << "Foo default constructor" << endl;}Foo(int) {cout << "Foo constructor" << endl;}};class Bar {public:Foo foo;//foo是一个member objectchar *str;};void foo_bar(){Bar  bar;//Bar::foo是一个member object,而其class Foo拥有default constructorif (bar.str) {cout << "bar.str is not nullptr" << endl;}}
被合成的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 constructorinline Bar::bar(){    //C++伪代码    foo.Foo::Foo();}
再一次要注意,被合成的default constructor之满足编译的需要,而不是程序的需要。为了让这个程序能够正确执行,字符指针str也需要被初始化。假设程序员经由下面的default constructor提供了str的初始化操作:

//程序员定义的default constructorBar::Bar(){    str = 0;}
现在程序的需求获得满足了,但是编译器还需要初始化member object foo。由于default constructor已经被显示地定义出来,编译器就没有办法合成第二个。这种情况下,编译器会采取什么措施呢?

编译器采取的措施是:“如果class A内含一个或一个以上的member class objects,那么class A的每一个constructor必须调用每一个member class的default constructor”。编译器会扩张已存在的constructor,在其中安插一些代码,使得user code被执行之前,先调用必要的default constructor。扩张后的constructor可能像这样:

//扩张后的default constructor//C++伪代码Bar::Bar(){    foo.Foo::Foo();//附加上的compiler code    str = 0;//explicit user code }
如果有多个class member objects都要求constructor初始化擦做,C++要求以“member objects在class中的声明顺序”来调用各个constructors。这一点由编译器完成,它为每一个constructor安插程序代码,以“member声明顺序”调用每一个member所管理的default constructor。这些代码将被安插在explicit user code之前。

举例:

class Dopey {public:Dopey();};class Sneezy {public:Sneezy(int);Sneezy();};class Bashful {public:Bashful();};class Snow_White {public://dopey、sneezy和bashful是三个member objectsDopey dopey;Sneezy sneezy;Bashful bashful;private:int mumble;};
如果Snow_White没有定义default constructor,就会有一个nontrivial constructor被合成出来,依次调用Dopey、Sneezy、Bashful的default constructors。然后如果Snow_White定义了下面这样的default constructor:

//程序员定义的explicit default constructorSnow_White::Snow_White() : sneezy(1024){    mumble = 2048;}
它会被扩张为:

//编译器扩张后的default constructor//C++伪代码Snow_White::Snow_White() : sneezy(1024){    //编译器插入调用member class objects的constructor    dopey.Dopey::Dopey();    sneezy.Sneezy::Sneezy(1024);    bashful.Bashful::Bashful();    //explicit user code    mumble = 2048;}
2.“带有Default Constructor”的Base Class

如果一个没有任何constructors的class派生自一个“带有default constructor”的base class,那么这个derived class的default constructor会被视为nontrivial,并因此需要被合成出来。它将调用上一层base classes的default constructor(根据它们的声明顺序)。对于一个后继派生的class而言,这个合成的constructor和一个“被显示提供的default constructor”么有什么差异。

如果设计者提供多个constructors,但其中都没有default constructor呢?编译器不会再合成一个新的default constructor,但编译器会扩张现有的每一个constructors,将“用以调用所有必要之default constructors”的程序代码加进去。如果同时也存在着“带有default constructor”的member class objects,那些default constructor也会被调用——在所有base class constructor都被调用之后。

3.“带有一个Virtual Function”的Class

另有两种情况,也需要合成出default constructor:

1.class声明(或继承)一个virtual function。

2.class派生自一个继承串链,其中有一个或更多的virtual base classes。

下面两个扩张行动会在编译期间发生:

1.一个virtual function table(vtbl)会被编译器产生出来,内放class的virtual functions地址。

2.在每一个class object中,一个额外的pointer member(也就是vptr)会被编译器合成出来,内含相关之class vtbl的地址。

为了让多态机制发挥功效,编译器必须为每一个Base(或其派生类的)object的vptr设定初值,放置适当的virtual table地址。对于class所定义的每一个constructor,编译器会安插一些代码来做这样的事情。对于那些未声明任何constructor的classes,编译器会为它们合成一个default constructor,以便正确地初始化每一个class object的vptr。

4.“带有一个Virtual Base Class”的Class

virtual base class的实现法在不同的编译器之间有极大的差异。然而,每一种实现法的共同点在于必须使virtual base class在其每一个derived class object中的位置,能够于执行期准备妥当。

对于class所定义的每一个constructor,编译器会安插那些“允许每一个virtual base class的执行期存取操作”的代码。如果class没有声明任何constructor,编译器必须为它合成一个default constructor。


总结:

有4种情况,会造成“编译器必须为未声明constructor的class合成一个default constructor”。C++ Standard把那些合成物成为implicit nontrivial default constructor。但,被合成出来的default constructor只能够满足编译器(而非程序)的需要。至于没有存在那4种情况而又没有声明任何constructor的classes,我们说它们拥有的是implicit trivial default constructor,它们实际上并不会被合成出来。

在合成的default constructor中,只有base class subobjects和member class objects会被初始化。所有其他的nonstatic data members(如整数、整数指针、整数数组等等)都不会被初始化。这些初始化操作对程序而言或许有需要,但对编译器则非必要。如果程序需要一个“把某指针设为0”的default constructor,那么提供它的人应该是程序员。

阅读全文
0 0