通常很多C++程序员存在两种误解:
- 没有定义默认构造函数的类都会被编译器生成一个默认构造函数。
- 编译器生成的默认构造函数会明确初始化类中每一个数据成员。
C++标准规定:如果类的设计者并未为类定义任何构造函数,那么会有一个默认 构造函数被暗中生成,而这个暗中生成的默认构造函数通常是不做什么事的(无 用的),trivial下面四种情况除外。
换句话说,有以下四种情况编译器必须为未声明构造函数的类生成一个会做点事 的默认构造函数。(nontrivial constructor)我们会看到这些默认构造函数仅“忠于编译器”,而可能不会按 照程序员的意愿效命。
1.包含有带默认构造函数的对象成员的类(带有default constructor的member class object)
如果一个class没有任何constructor,但他含有一个member object,而后者有defautl constructor,那么这个class的implicit
defautl constructor就是“nontrivial”,编译器需要为此class合成出一个default constructor,不过这个合成操作只有在constructor真正需要被调用时发生。
class Foo {public: Foo(); Foo(int);...};
class Bar {public: Foo foo; char str;};
void foo_bar(){
Bar bar; //Bar::foo应在此处被初始化
if(str){...}
}
被合成的Bar default ctor内含必要的代码,能够调用class Foo的default ctor来处理member object Bar::foo,但并不处理Bar::str。即被合成的default ctor只是为了满足编译器的需要(编译器需要有个地方来初始化Bar::Foo,因为它有自己的default ctor),而不是程序的需要(初始化Bar::str是程序员的动作)。
如果程序员在ctor只显式初始化了Bar::str,但对编译器而言,它还需要初始化member object foo,由于无参构造函数已经被明确定义出来,编译器没办法合成第二个。那么这时候编译器会怎么做呢?
编译器的行动是“如果class A内含一个或以上的member class objects,那么classA的每一个constructor必须调用每一个member classs的defautl constructor,编译器会扩张已存在的constructor。在其中安插一些代码。使得user code在被执行之前,先调用必要的default constructor,)
如果有多个class member object都要求constructor的初始化操作,将如何呢?c++要求以“member objects在class中的声明次序”来调用各个constructor。这一点有编译器完成。
例如
Bar::Bar(){ str=0; }
会被编译器扩展为
Bar::Bar(){
foo.Foo::Foo(); //附加上的编译器代码
str=0;//显式的程序员代码
}
2.带有defautl constructor“的base class。
如果一个没有定义任何构造函数的类派生自带有默认构造函数的基类,那么编译器为它定义的默认构造函数,将按照声明顺序为之依次调用其基类的默认构造函 数。若该类没有定义默认构造函数而定义了多个其他构造函数,那么编译器扩充它的所有构造函数——加入必要的基类默认构造函数。另外,编译器会将基类的默认构造函数代码加在对象成员的默认构造函数代码之前。(注:对于基类有默认构造函数的,而在子类中无显式构造函数时,编译器对于基类部分的数据以及数据成员不会视而不见的,而且是先基类后对象成员,对于有多个基数的,按照它们在继承列表中的顺序进行调用;当然即使你定义了其他类型的构造函数,编译器也会在后面帮你完成那些你没有显式完成的工作,它会扩充所有你编写的构造函数,当然只是在你没有显式调用基类的哪个构造函数时)。
3.带有一个virtual function的class
另有2中情况,也需要合成出defautl constructor。
1. 这个类自己声明(或者继承)了Virtual Function。
2. 这个类继承自一个继承串链,其中有一个或多个virtual base class。
不管哪一种情况,由于缺乏有user声明的constructor,编译器会详细记录合成一个defautl constructor的必要信息,以下面的程序片段为例;
class Widget{
public:
virtual void flip()=0;
//...
};
void flip(const Wideget& widget) { widget.fiip();}
//假设Bell和whistle都派生子widget
void foo()
{
Bell b;
Whistle w;
flip(b);
flip(w);
}
这种情况下,编译时,要做如下工作:
1.编译器需要生成一个virtual function table(vtbl)并填充。
2.在class object中,一个额外的pointer member(就是vptr,指向vtbl)会被编译器合成出来。此外虚拟调用会被替换(w.vf() => w.vprt[1])。
为了支持这种功能,编译器必须为每个w对象设置它的vptr(这是成员变量,此时需要指向合适的vtbl),因此编译器需要在default ctor中安插一些代码来完成这种工作。
4.带有一个virtaul Base class的class
virtual base class的实现法在不同编译器之间有极大的差异,然而,每一种实现法的共同点在于必须使virtaul base classs在其每一个derived class object中的位置,能够于执行期准备妥当,例如下面这段程序代码中:
考虑这样的代码:
classX { public: inti; };
classA : publicvirtualX { public: in tj; };
classB : publicvirtualX { public: double d; };
classC : publicA, publicB { public: int k; };
//无法在编译期间解析出 pa->X::i 的位置(给一个pa无法确定i的地址)。
void foo( constA* pa ) { pa->i = 1024; }
main() {
foo( new A );
foo( new C );
// ...
}
由于pa的真正类型不确定,所以某些编译器会记录一个指针例(如 __vbcX)来记录X,然后通过这个指针来定位pa指向的i。上述
void foo( constA* pa ) { pa->i = 1024; }
变成了:
void foo( constA* pa ) { pa-> __vbcX ->i = 1024; }
因此,__vbcX这个指针(执行virtual base class)需要在object构造期间设置好。于是编译器需要一个default ctor来完成这个工作。