C++关于编译器合成的默认构造函数
有两个常见的误解:
1.任何类如果没有定义默认构造函数,就会被合成出一个来。
2.编译器合成的默认构造函数会显式地设定类内每一个数据成员的默认值。
对于第一个误解,并不是任何类在没有显式定义默认构造函数时都会被编译器合成一个默认构造函数。
在以下4种情况下,编译器才会合成默认构造函数,以满足编译器自己的需求(并不是为了满足程序的需求)。
一、父类有默认构造函数(default constructor)
如果一个没有任何构造函数的类派生自 “一个有默认构造函数的” 父类,那么这个派生类的默认构造函数被认为是 ”有用的(被编译器所需求)“,因此需要被合成出来。它会调用父类的默认构造函数。
如果派生类含有多个构造函数,但其中不含默认构造函数,编译器并不会为他合成新的默认构造函数,而是会扩展每一个构造函数,将所有需要调用的默认构造函数的代码安插进去。
二、类中带有类类型成员
如果类A中带有类类型成员,并且这个类类型是有默认构造函数的,那么类A的默认构造函数被认为是 ”有用的“,需要合成。
//X,Y,Z都带有显式默认构造函数 class X { public: X(); }; class Y { public: Y(); }; class Z { public: Z(); }; class A { public: X x; //三个类类型 Y y; Z z; A(int a); //带有一个构造函数 private: int val; }; //程序员对A的构造函数的实现(你所看到的) A::A(int a) { val = a; } //编译器扩展合成后(编译器认为应该这样) A::A(int a) { //按声明顺序安插代码,调用构造 x.X::X(); y.Y::Y(); z.Z::Z(); //显式的用户代码 val = a; }
三、带有虚函数的类
在编译期间会发生两个扩展行动:
1.编译器会产生一个虚表(存放着类内虚函数的地址)。
2.在每一个类对象中,会有一个额外的虚表指针被编译器合成出来,用来指向相关虚表。
四、带有虚基类的类
虚基类的实现必须满足虚基类在其 ”每一个派生子类的对象中的位置“ 能够于执行期准备妥当。
class A { public: int a; }; class X :public virtual A { public: int x; }; class Y :public virtual A { public: int y; }; class Z :public X, public Y { public: int z; }; //编译时无法确定p->A::a的位置 void fun(X *p){ p->a = 1; } //编译时无法确定p的真正类型(基类指针可以指向派生类对象,此时真正类型为派生类类型) int main() { fun(new X); //真正类型为X fun(new Z); //真正类型为Z return 0; }
编译器无法确定fun()中 ”经由p存取的A::a “的实际偏移位置,因为p的真正类型是可变的,如在main()中既可以是X类型,也可以是Z类型。编译器必须改变存取操作的代码,使A::a延迟到执行时才确定下来。
fun()可以被编译器改写成如下:
//编译器转变操作,其中vcbA是编译器产生的指针,指向虚基类A void fun(X *p){ p->vcbA->a = 1; } //vcbA是在类对象构造期间被完成的
除此四种情况之外,如果类没有声明任何构造函数,他们就会有一个隐式而”无用“的默认构造函数,他们实际上并不会被合成构造出来。
对于第二个误解,在合成的默认构造函数中,只有基类子对象和类类型对象会被初始化,而其他所有的非静态成员(如整数,指针,数组等),都不会初始化,对他们进行初始化的应该是程序员,而非编译器。
注意:值类型的默认值并不是默认构造的初始化。