2.1 Default Constructor的建构操作
常见的两个误解:
1、任何类如果没有定义默认构造函数,编译器就会合成一个出来(错误)
2、编译器合成出来的默认构造函数会明确设定类里面的每一个数据成员的默认值,也就是会帮类里所有数据成员都初始化(错误)
编译器会合成默认构造函数的四种情况
情况一、类A的数据成员里有类B的对象,且类B有构造函数
如果类A没有构造函数,那么编译器就会合成一个默认构造函数。例子:
class Foo{
public:
Foo(){
};
Foo(int a);
//...
};
class Bar{
public:
Foo foo; //注意是组合(内含),不是继承
char *str;
};
Bar bar; //Bar::foo必须在此处初始化
这种情况下编译器会为类Bar合成一个默认构造函数,这个默认构造函数含有必要的代码,能够调用类Foo的构造函数来初始化Bar::foo,但它并不产生任何代码来初始化Bar::str。因为将Bar::foo初始化是编译器的责任,而将Bar::str初始化时程序员的责任。被合成的默认构造函数可能是这样的(为了简化讨论,下面的这些例子都省略掉了隐含的this指针):
//Bar默认构造函数可能是这样的:
inline
Bar::Bar(){
//c++伪代码
foo.Foo::Foo();
}
被合成的默认构造函数只满足编译器的需要,而不是程序的需要,为了让这个程序片段能够正确执行,字符指针str也需要被初始化。
假设程序员提供了构造函数,初始化了字符指针str:
Bar::Bar(){
str=0;} //程序员提供了显式的构造函数
str初始化了,但是对象foo还未初始化,但是构造函数已经被明确定义出来了,编译器没法合成第二个,此时编译器会扩展这个构造函数,在程序员写的代码前调用类Foo的构造函数初始化foo:
//扩展后的构造函数可能是这样的
//c++伪代码
Bar::Bar(){
foo.Foo()::Foo(); //附加的代码
str=0; //程序员的代码
}
假如现在有多个类Foo,Foo1,Foo2,而类Bar中依次含有这些类的对象,那么在合成Bar的默认构造函数或者扩展构造函数都是与上面的类似,按照这些对象的声明顺序依次调用他们的构造函数。
情况二、类A继承类B,且类B有构造函数
这种情况与第一种情况类似。
如果类A没有构造函数,那么编译器就会合成一个默认构造函数,这个默认构造函数会调用类B的构造函数。如果是多重继承,则根据声明的次序依次调用基类的构造函数。
如果类A有构造函数(一个或者多个),但是这些构造函数里面没有调用类B的构造函数,那么编译器会扩展这些构造函数,在程序员的代码前调用类B的构造函数。如果是多重继承,则根据声明的次序依次调用基类的构造函数。如果同时存在着情况一,会先调用基类的构造函数,再调用数据成员里面其他类的对象的构造函数(即构造函数调用优先情况二,其次情况一)。
情况三、带有一个虚函数的类
分两种情况,都需要合成默认构造函数:
1、类声明(或继承)一个虚函数
2、类派生自一个继承串链,其中有一个或以上的虚基类
不管哪种情况,由于缺乏显式的构造函数,编译器会详细记录合成一个默认构造函数的必要信息,例子:
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); //调用的是Bell::flip()
flip(w); //调用的是Whistle::flip()
}
编译期间会发生下面两个扩展操作:
1、一个虚函数表(vtbl)会被编译器产生出来,里面放置类的虚函数的地址
2、在每一个对象中,一个额外的虚函数表指针(vptr)会被编译器合成出来,里面存储着类的虚函数表(vtbl)的地址。
此外,widget.flip()的虚拟引发操作会被重新改写:
//原来:widget.flip(),现在转换成:
(*widget.vptr[1])(&widget) //第一章提到索引0