一个类,如果没有任何的用户声明的的构造函数,那么会有一个默认的构造函数被隐式地声明出来。这个被隐式声明的构造函数,究竟什么时候被合成、被编译器合成的默认构造函数究竟执行怎么样的操作,编译器如何处理用户定义的构造函数,就是本文要探讨的问题。
1、默认构造函数何时被合成
如果一个类没有任何的用户声明的构造函数,那么在当编译器需要的时候,编译器会为类合成一个默认的构造函数,它只用于执行编译器所需要的操作。注意,默认的构造函数是在编译器需要的时候被合成出来,而不是程序需要的时候,如果程序需要,则默认的构造函数应该由程序员实现。
那么编译器需要的时候是什么时候呢?正确来说,编译器需要的时候是遇到如下四种情况的时候,它需要为以下四种类型的类合成一个默认的构造函数:
1)类的成员变量带有默认构造函数
2)类的基类带有默认构造函数
3)类带有virtual函数
4)类带有一个virtual基类
且合成操作只有在构造函数真正需要被调用时才会被合成。
现在还有一个问题就是,在C++中各个不同的编译模块中,编译器如何避免合成多个默认呢?解决方法是把合成的默认构造函数、复制构造函数、析构函数、赋值操作运算符等都以inline的方式完成。如果函数太复杂,不适合做成inline,就会成成一个显式非inline的static函数。无论是inline还是非inline的static函数,其作用都是为了不被文件以外者访问。
对于下面的这段程序:
class X { public: int mData; int *mPtr; };
类X没有声明任何的构造函数,但是它并不属于上述所说的4种类型的类,所以编译器并不会为类X合成一个默认的构造函数。
下面详细分析编译器为上述4种类型的类合成的默认构造函数的行为。
2、 类的成员变量带有默认构造函数 (即类含有成员类对象(member class objects ))
这种情况是指:一个类没有任何构造函数,但它的成员变量是一个有默认构造函数的类的变量。
例如,如下代码所示:
class X { public: X(){mData = 0; cout << "X::X()" << endl;} int mData; }; class Xs { public: X mX; int mN; }; int main() { Xs xs; cout << xs.mX.mData << endl; cout << xs.mN << endl; return 0; }
类Xs没有定义任何构造函数,但是其成员变量mX是类型是X,且类X拥有一个默认的构造函数,其运行结果如下:
从运行的结果可以看出,在编译器为类Xs合成的默认构造函数中,调用了X的构造函数为其成员变量mX进行初始化,但是该合成的默认构造函数却没有对类Xs的成员变量mN进行初始化。可见,编译器为类Xs合成的构造函数如下伪代码所示:
inline Xs::Xs() { mX.X::X(); }
有时候我们会为类定义一个默认构造函数,但是在该构造函数中,只初始化部分的成员变量,那么会发生什么样的行为和效果呢?保持类X和main函数的测试代码不变,修改类Xs的代码为如下:
class Xs { public: Xs() { cout << "Xs::Xs()" << endl; mN = 1; } X mX; int mN; };
可以看到,我们为类Xs添加了一个默认的构造函数,但在该构造函数中,我们只为成员变量mN进行初始化,其运行结果如下:
由运行结果可以看出,虽然在类的Xs的默认构造函数中,我们没有显示地对mX进行初始化,但是类X的默认构造函数还是被调用了,且其调用顺序还在类Xs的默认构造函数函数体代码的前面。
由此可见,编译器的行为是:如果类内含有一个或多个类成员对象(member class object),那么该类的每一个构造函数必须调用每一个类成员的默认构造函数(按照成员声明顺序)。编译器会扩张已存在的构造函数,在其中安插一些代码,使得用户的代码被执行之前,先调用必要类成员的默认构造函数。
对于此时类Xs的构造函数可用以下伪代码表示:
inline Xs::Xs() { mX.X::X(); cout << "Xs::Xs()" << endl; mN = 1; }
对于此类情况,编译器合成默认的构造函数或向已有的默认构造函数中插入代码的意义在于:使每个类成员变量都得到初始化。
3、类的基类带有默认构造函数
这种情况是指:一个没有任何构造函数的类派生自一个带有默认构造函数的类。
例如,如下代码所示:
class X { public: X(){mData = 0; cout << "X::X()" << endl;} int mData; }; class XX : public X { public: int mN; }; class XXX : public XX { }; int main() { XX xx; cout << xx.mData << endl; cout << xx.mN << endl; XXX xxx; return 0; }
类XX没有任何构造函数,但是其基类X存在一个默认的构造函数,其运行结果如下:
从运行结果可以看出,编译器合成的构造函数调用上一层基类的默认构造函数。对于一个后继派生的class而言,这个合成的默认构造函数与一个被显式提供的默认构造函数无异。
如果类的设计者提供了一个或多个构造函数(包括默认构造函数),但是在其提供的构造函数中并不显式地调用其基类的构造函数,编译器会如何处理呢?
为类XX加上两个构造函数,修改main函数的测试代码,如下所示:
class XX : public X { public: XX() { mN = 1; } XX(int n) { mN = n; } int mN; }; int main() { XX xx1; XX xx2(2); return 0; }
类XX的构造函数都没有显式地调用其基类的构造函数。其运行结果如下:
从运行结果可以证明:编译器会扩张派生类的每一个现在的构造函数,将要调用的所有必要的默认构造的程序代码加插进去。注意:由于已经存在用户自定义的构造函数,所以编译器不再合成新的构造函数。
对于此类情况,编译器合成默认的构造函数或向已有的默认构造函数中插入代码的意义在于:确保类的基类子对象得到初始化。
4、类带有virtual函数
这种情况是指:类声明或继承了一个或多个virtual函数。
例如,对于以下的代码:
class X { public: virtual ~X() { cout << "X::~X()" << endl; } virtual void print() { cout << "X::print()" << endl; } int mData; }; class XX : public X { public: virtual ~XX() { cout << "XX::~XX()" << endl; } virtual void print() { cout << "XX::print()" << endl; } int mN; }; int main() { X *x = new XX; x->print(); delete x; return 0; }
类X有一个virtual函数print和一个virtual析构函数,其运行结果如下:
从运行结果可以看出,利用X的指针调用print函数,调用的是类XX的print函数,且在delete析构时也是调用了类XX的析构函数,先析构派生类再析构基类。
所以,由于虚函数的加入,编译器会在编译期间发生如下操作,以使虚函数机制发挥作用:
1)虚函数表(vtbl)会被编译器产生而来,用于存放为类的virtual函数地址。
2)在每一个类的对象内,一个额外的指针成员会被编译器合成出来,这个指针就是指向虚函数表的指针(vptr)。
此外,编译器还会改写虚函数的调用。例如,上述的main函数可能会被改写成如下的伪代码
int main() { X *x = malloc(sizeof(XX)); x->vptr = XX::vtbl; (x->vptr[1])(x); // 1为print函数在vtbl中的索引 (x->vptr[0](x); // 0为析构函数在vtbl中的索引 free(x); return 0; }
所以,编译器合成的默认构造函数会为类安插一个vptr,并指定其初值,使其指向该类的虚函数表。若该类已经定义了构造函数,编译器会为每个构造函数安插代码来做同样的事情(即安插vptr,并设定初值)。
对于此类情况,编译器合成默认的构造函数或向已有的默认构造函数中插入代码的意义在于:使virtual函数机制(多态)可以正确地生效。
5、类带有一个virtual基类
这种情况是指类派生自一个继承串链,其中有一个或更多的virtual基类。
virtual基类的实现方法 在不同的编译器之间有极大的差异,但是每一种实现的共同点是必须使virtual基类在其每一个派生类对象中的位置,能够在执行期间准备妥当。
例如,对于以下的代码(并不是设计良好的代码):
class X { public: X() {mData = 100;} int mData; }; class X1 : virtual public X { public: X1() {mX1 = 101;} int mX1; }; class X2 : virtual public X { public: X2() {mX2 = 102;} int mX2; }; class XX : public X1, public X2 { public: XX() {mXX = 103;} int mXX; }; int main() { cout << "sizeof(XX): " << sizeof(XX) << endl; XX xx; int *p = (int*) &xx; for (int i = 0; i < sizeof(XX) / sizeof(int); ++i, ++p) { cout << *p << endl; } return 0; }
在main函数中遍历并输出对象的内容,其运行结果如下:
从结果可以看到,即像在所有的类中都没有定义virtual函数,编译器还是会为子类XX插入了两个vptr,其中第一个vptr属于X1,第二个vptr属于X2。至于它有什么作用,我还不清楚,但是可以肯定的是编译器会为我们定义的构造函数安插必要的代码来实现virtual基类的机制。若类没有声明任何的构造函数,编译器必须合成一个默认构造函数来完成相同的操作。
注:若基类X中有virtual函数,则还会安插一个X::vptr,它的位置在100之前,即在基类X的成员变量之前。
6、总结
满足上述4种情况之一或以上的类,若没有声明任何的构造函数,编译器会为其合成一个默认的构造函数;若已声明一个或多个构造函数,则编译器会为每个构造函数安插一定的代码来完成编译必要的工作。这些被合成的构造函数称为implicit nontrivial default constructors(隐式有效的默认构造函数)。
不满足上述4种情况的类,而又没有声明任何构造函数,则该类拥有的是implicit trivial default constructors(隐式无效的默认构造函数),实际上不会被合成出来。
合成出来的默认构造函数,只会初化化基类子对象和成员类对象,其他的非static成员变量并不会被初始化。
此外,C++有两个常见的误解:
1)任何class如果没有定义默认构造函数,就会被合成出来。
2)编译器合成的默认构造函数,会显式设定class内每一个成员数据的默认值。