C++ 默认构造函数


C++默认构造函数是可以不用实参进行调用的构造函数,它包括了以下两种情况:

  • 没有带明显形参的构造函数。
  • 提供了默认实参的构造函数。

我们知道,在类设计者没有提供默认构造函数的情况下,编译器会帮我们自动合成默认构造函数。但是这里面的理解,很多人包括我曾经有两点误解

  • 类如果没有定义任何的构造函数,那么编译器一定会为类定义一个合成的默认构造函数。(错误!)
  • 合成默认构造函数会初始化类中所有的数据成员。(错误!)

一些书中对这里面的解释也是语焉不详,或者翻译的不够准确。事实上需要强调的是:

  • C++编译器只有在必要时才给类合成默认构造函数
  • 合成默认构造函数总是不会初始化类的内置类型及复合类型的数据成员
    (关于内置类型和复合类型见:C++ 类型

编译器在这么几种情况下会给类添加默认构造函数:

1. 含有类对象成员,且该成员类型有默认构造函数。

如果一个类没有任何构造函数,但是它含有一个类对象成员,且该成员类型有默认构造函数,那么编译器就会为该类合成一个默认构造函数,不过这个合成操作只有在构造函数真正需要被调用的时候才会发生。

举个例子,编译器将为类A合成一个默认构造函数:

1
2
3
4
5
6
7
8
9
class A {
public:
string s; //string有默认构造函数
int v;
};
int main() {
A a; //编译至此时,编译器将为A合成默认构造函数
return 0;
}

合成的默认构造函数代码大概如下:

1
A::A(): s() {}

被合成的默认构造函数内只含必要的代码,它完成了对数据成员s的初始化,但不产生任何代码来初始化A::v。正如上面所述,初始化类的内置类型或复合类型成员不是编译器的责任。
如果类中有多种类对象成员,则编译器按照这些类对象成员声明的顺序,在构造函数按顺序插入调用各个类默认构造函数的代码。

2. 基类带有默认构造函数的派生类。

当一个类派生自一个含有默认构造函数的基类时,该类也符合编译器需要合成默认构造函数的条件。编译器合成的默认构造函数将根据基类声明顺序调用上层的基类默认构造函数。如果设计者定义了多个构造函数,编译器将不会重新定义一个合成默认构造函数,而是把合成默认构造函数的内容插入到每一个构造函数中去。

3. 带有虚函数的类

类带有虚函数可以分为两种情况:

  • 类本身定义了自己的虚函数。
  • 类从基类中继承了虚函数。

带有虚函数的类也满足编译器需要合成默认构造函数的条件。原因是含有虚函数的类对象都含有一个虚表指针vptr,编译器需要对vptr设置初值以满足虚函数机制的正确运行,编译器会把这个设置初值的操作放在默认构造函数中。如果类设计者没有定义任何一个默认构造函数,则编译器会合成一个默认构造函数完成上述操作,否则,编译器将在每一个构造函数中插入代码来完成相同的事情。

4. 虚继承的派生类

虚继承是为了解决多重继承下确保子类对象中继承自每个父类的成员只存在一个副本的问题,考虑典型的菱形继承,如下图:

菱形继承

上图中菱形继承的虚继承代码如下:

1
2
3
4
class A  { public: int a; };
class B1 : public virtual A { public:int b1; };
class B2 : public virtual A { public:int b2; };
class C : public B1, public B2 { public: int c; };

对于虚继承,编译器将给每个虚继承的子类产生一个指向虚基类的指针;当虚继承的类被当做父类继承时,虚基类指针也会被继承。这个指针的安插,编译器将会在合成默认构造函数中完成。同样的,如果类设计者已经定义了多个构造函数,那么编译器不会重新写默认构造函数,而是把虚基类指针的安插代码插入已有的构造函数中。

总结

  • C++编译器只有在必要时才给类合成默认构造函数
  • 合成默认构造函数总是不会初始化类的内置类型及复合类型的数据成员
  • 编译器添加默认构造函数的必要情形可以概括为:
    a) 调用对象成员或基类的默认构造函数
    b) 为对象初始化虚表指针与虚基类指针