默认构造函数
? ? ? ? 在C++ Annotated Reference Manual中有提到,当有需要时,编译器应该位类合成默认构造函数。这里的关键是“有需要”,什么时候需要?被谁需要?都要看具体情况。编译器不会为所有的类提供默认构造函数,哪怕这个类不包含任何构造函数。对于以下代码片段,编译器不会为类Node合成默认构造函数。(真的不会吗?我一直以为会的,哪怕是将成员变量初始为垃圾值,他也是应该有构造函数的。)
class Node
{
public:
int val;
Node* next;
};
? ? ? ? C++(ISO-C++95)标准里也提到,当一个类没有构造函数时,应该隐式的声明一个默认构造函数,隐式的默认构造函数是不必要的(trivial)。
合成默认构造函数的条件
? ? ? ? 只有为了实现(implementation)需要时,才需要一个必要(nontrivial)的默认构造函数, 才会给类合成默认构造函数。什么时候默认构造函数才是必要的?只有在下面四个条件下,默认构造函数才是必要的。
1. 类的数据成员具有默认构造函数时
? ? ? ? 如下所示,类List中有一个Node成员,且该成员具有默认构造函数,那么此时编译器会为List合成一个默认构造函数,而且这种合成只有当该默认构造函数需要被调用时才会合成。而为了防止在多个源文件中为一个类合成多个默认构造函数,编译器会将合成的构造函数(拷贝构造函数,西析构函数,赋值运算符)等函数内联化,如果函数过于复杂无法内联,则会显式的合成一个非内联的静态函数。
class Node
{
public:
Node() {}
int val;
Node* next;
};
class List
{
public:
Node n;
int len;
};
? ? ? ? 即使编译器为List合成类一个默认构造函数,该构造函数只会调用Node的默认构造函数以初始化成员n,而不会去初始化成员len。初始化len应该是程序员的责任,调用Node的默认构造函数则是编译器的实现需要。
? ? ? ? 如果List有一个默认构造函数List::List() {len = 0;}。由于显示声明了默认构造函数,所以编译器不会给List合成默认构造函数。但是显示声明的默认构造函数又没有去初始化成员n。此时,为了实现需要,编译器会修改显示默认构造函数,以使其能够初始化成员n。修改后的默认构造函数看起来的形式就成了List::List() { n.Node::Node(); len = 0;}。如果类List有多个没有显式调用构造函数类似n的成员呢?编译器会按照成员的声明顺序去调用各个成员的默认构造函数。
2. 基类具有默认构造函数
? ? ? ? 如果基类具有默认构造函数,则编译器会为派生类合成一个默认构造函数,该合成的默认构造函数会按照基类的继承顺序依次调用其默认构造函数。
? ? ? ? 如果该派生类具有有参构造函数,但是没有默认构造函数怎么办?编译器会修改所有的有参构造函数,使其调用基类的构造函数。如果该派生类中的数据成员同样具有默认构造函数,则编译器会在调用所有基类的构造函数之后,调用数据成员的默认构造函数。
3. 类具有虚函数
? ? ? ? 当类具有虚函数的时候,就意味着在该类对象的内存空间中存在虚标指针。为该虚表指针赋值就是默认构造函数需要做的,即编译器的“实现”需要。此时,编译器会为类合成默认构造函数。如果该类中具有有参构造函数,编译器不会合成默认构造函数。但是编译器同样会在有参构造函数中插入代码,以实现对虚表指针的赋值。
4. 类具有虚基类
? ? ? ? 在具有虚继承的类结构中,编译器无法在编译时确定一个基类成员的位置。如一下的继承层次:
class A {public: int x};
class B : virtual A {int y;};
class C : virtual A {double z;};
class D : public B, public C {int k;};
? ? ? ? 那么对于函数void func(A* a) {a->x = 1;},对于调用func(new A); func(new D);,编译器没办法固定x与指针a的偏移量,因此这一实现需要在运行期进行。在早起的虚继承实现中,是通过在派生类中插入一个只想基类对象的指针来实现虚继承的。不论什么实现,编译器都需要添加一些信息来在运行时找到x,那么添加的这个信息就需要默认构造函数来完成了。因此在虚继承中需要合成默认构造函数。(虚基类的具体实现原理还需要再探讨)
总结
? ? ? ? 总的来说,编译器并不会为任何没有构造函数的类去合成构造函数。编译器只有在必要时才会去为一个类合成默认构造函数,这里的必要体现在实现的必要性上,如调用基类的默认构造函数;调用类成员的默认构造函数;或者是为了实现多态、虚继承而需要预先做一些初始化工作。另外需要注意的是,编译器合成的构造函数仅仅去做编译器为了实现需要而需要做的事情,合成的默认构造函数不会去将一个指针变量初始化为空指针这样的工作,这样的初始化工作应该是程序员的责任。
|