前言
前几天说了要总结下c++构造函数的问题,于是看了看<<深度探索c++对象模型>>,来这里总结一下,不得不说,如果我们不看这本书,我们对c++构造函数可能还有一点误解.
介绍
C++ Annotated Reference Manual(下面都简称ARM)中告诉我们”default constructor在需要的时候被创建出来”,关键字眼是”在需要的时候”,被谁需要?做什么事情?下面举个例子:
1 | class Foo{public: int val; Foo *pnext;}; |
在这个例子里,程序员希望Foo有一个default constructor将它的两个成员都初始化为0.但是这里符合ARM所说的”在需要的时候”?答案是no,其间的差别在于一个是程序员的需要,一个是编译器的需要.如果是程序有需要,那么这就是程序员的责任.本例子中承担责任的就是设计class Foo的人.也就是说上述程序片段不会合成一个default constructor.
default constructor
那么什么时候会合成一个default constructor呢?答案是编译器需要的时候!此外,被合成出来的default constructor只会执行编译器所需要的行为
也就说,就算有需要为class Foo合成一个default constructor,那个constructor也不会将两个成员val和pnext初始化为0.为了让上面的代码正确执行,class Foo的设计者必须提供一个显式的default constructor,将两个成员初始化为0.
nontrivial default constructor
- 带有default constructor的member class Object
如果一个class没有任何constructor,但是它内含一个member object,而后者有一个default constructor,那么这个class的implicit default constructor就是”nontrivial”,编译器需要为此class合成出一个default constructor.不过这个合成操作只有在constructor真正被需要的时候才会被合成.举个例子:
1 | class Foo{ public: Foo();Foo(int);...}; |
被合成的Bar default constructor内含必要的代码,能够调用class Foo的default constructor来处理member object Bar::foo,但是它并不产生任何代码来初始化Bar::str.
被合成的default constructor可能看起来像下面这样:
1 | inline Bar::Bar() |
- 带有default constructor的Base class
如果一个没有任何constructor的class派生自一个”带有default constructor”的base class,那么这个derived class的default constructor会被视为nontrivial, 并因此需要而被合成出来.它将调用上一层Base class的default constructor(根据它们的声明次序).
如果设计者提供多个constructors, 但是其中都没有default constructor呢?编译器会扩张现有的每一个constructor, 将”用以调用所有必要之default constructor”的程序代码添加进去.它不会合成一个新的default constructor,这是因为用户已经显式的提供了构造函数了.如果同时也存在”带有default constructor”的member class object, 那些default constructor也会被调用–在所有base class constructor都被调用了之后.
- 带有一个virtual function的class
举例子如下:
1 | class Widget |
下面两个扩张操作会在编译期间发生:
- 一个virtual function table(在cfront中被称为vtbl)会被编译器产生出来,内放class的virtual functions地址.
- 在每一个class object内,一个额外的pointer member(也就是vptr)会被编译器合成出来,内含相关的class vtbl地址.
此外, widget.flip()的虚拟引发操作会被改写,以使用widget的vptr和vtbl中的flip()条目:
//widget.flip()的虚拟引发操作的转变
(*widget.vptr[1])(&widget).
其中:
- 1表示flip()在virtual table中的索引
- &widget代表要交给”被调用的某个flip()函数实体”的指针.
为了让这个机制发挥作用,编译器必须为每个Widget(或其派生类之)object 的vptr设定初值,放置适当的virtual table地址.对于class定义的每个constructor,编译器会安插一些代码来做这些事情,对于那些没有声明任何constructors的classes,编译器会为它们合成一个default constructor,以便正确的初始化每一个class object的vptr.
- 带有一个virtual Base class的class
virtual base class的实现在不同的编译器之间有很大的差异.但是实现的共同点在于必须使virtual base class 在其每一个derived class object中的位置,能够于执行器准备妥当.举例如下:
1 | class X {public: int i;}; |
编译器无法确定foo()之中”经由pa而存取的X::i”的实际偏移位置,因为pa的真正类型可以变化.编译器必须改变”执行存取操作”的那些代码,使得X::i可以延迟至执行期才决定下来.原先cfront的做法是靠”在derived class object的每一个virtual base class中安插一个指针”完成.所有”经由reference或者指针来存取一个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没有声明任何constructors, 编译器必须为它合成一个default constructor.
总结
- 从上面我们知道了在上面的四种情况下编译器会为我们合成一个default constructor如果我们没有声明任何constructor.至于那些不属于上述四种情况而又没有声明任何constructor的classes, 我们说它们拥有的是implicit trivial default constructor,它们实际上不会被合成出来.
- 在合成的default constructor中, 只有base class subobjects和member class objects会被初始化,所有其他的nonstatic data member, 如整数,整数指针,整数数组等等都不会被初始化.如果有需要的话,那么应该是我们手动初始化,而不能寄希望于编译器合成,因为那并不是编译器所需要的.