“关于C++,最常听到的一个抱怨就是,编译器背着程序员做了太多事情。”
C++ Annotated Reference Manual(ARM)告诉我们:“默认构造函数在需要的时候被编译器产生出来”。什么是“被需要”?被谁需要?干什么?
下面这段代码来看一下:
class Base{ public: int val; Base *pnext; };void Test(){ Base base; if(base.val && base.pnext){......}}
这段程序要求在两个members都不为0时,做某些事情。正确的调用应该是在Base的构造函数里将其初始化为0。上述代码是不是ARM曾说的“在需要的时候”?并不是。要求两个members都不为0才执行任务是程序的需要,实现它是程序员的责任,但编译器却没有这样的要求,上述程序片段并不会为Base合成一个默认构造函数。
那么,到底什么时候才需要合成默认构造函数呢?在编译器需要的时候,此外,被合成出来的构造函数也只会执行编译器所需要的动作,换句话说,合成的默认构造函数函数并不会把两个members初始化为0.
C++ Standard[ISO-C++95] 的Section 12.1如是说:
对于 class X,如果没有任何的用户声明的构造函数,那么会有一个默认构造函数被隐式(implicit)声明出来......一个被隐式声明的默认构造函数将是一个浅薄无用的(trivial)构造函数
什么时候需要non-trivial的默认构造函数?下面分四种情况来说明下。
01
—
内含[带有默认构造函数的类]的类
内含指的是一个类的实例化对象,作为另一个类的数据成员,这种情况就叫做内含。不直观?来看下图:
对应的代码形式为:
class Foo { public: int a; };class Bar { public: Foo foo; };
如果是多重内含的话,代码可以长这样:
class B1 { public: B1() { cout << "B1() Called" << endl; } };class B2 { public: B2() { cout << "B2() Called" << endl; } };class B3 { public: B3() { cout << "B3() Called" << endl; } };class Derived {public: B1 b1; B2 b2; B3 b3; Derived() { cout << "Derived() Called" << endl; }};
就拿上面的代码为例,如果Derived类内含member class object(即b1、b2、b3),那么Derived类将在每一个版本的构造函数中,调用每一个member class object的默认构造函数。编译器将扩张已经存在的,即显式的(explicit)构造函数,在用户代码前,插入隐式的(implicit)member class默认构造函数。
编译器扩张后的效果是什么样子的?
// 伪代码,不一定能编译通过class Derived {public: B1 b1; B2 b2; B3 b3; Derived() { /* Compiler's implicit code begin*/ b1.B1::Dopey(); b2.B2::Sneezy(); b3.B3::Bashful(); /* Compiler's implicit code end*/ cout << "Derived() Called" << endl; }};
Derived类的显式默认构造函数将隐式调用三个成员的显式默认构造函数。来看一下运行效果是不是这样:
void Test(){ cout <"**********Test() Called**********" <endl; Derived d; cout << "**********Test() Ended**********" << endl;}int main(){ Test(); return 0;}
实验证实了上述的结论。
02
—
带有默认构造函数的基类
基类与内含就是两个不同的概念了,基类说明两个类之间的关系是继承。它又分为四种情况:
一、若子类和基类都没有任何显式构造函数,则编译器将为子类和基类自动生成隐式构造函数
class B1 {};class B2 {};class B3 {};class Derived : public B1, public B2, public B3 {};
来进行一下调用:
void Test() { cout << "**********Test() Called**********" << endl; Derived d; cout << "**********Test() Ended**********" << endl;}int main() { Test(); return 0;}
OK,什么也不会输出。这是因为,编译器在这种情况下,隐式地为四个类生成了默认构造函数。但没有东西输出,这是因为像上个程序一样输出函数调用信息,那是程序员的需求,却不是编译器成功实例化对象的需要。
二、若子类没有显式构造函数,派生自含有显式默认构造函数的基类,编译器将为子类合成隐式默认构造函数,并按照声明顺序调用基类显式默认构造函数。
如果类之间的关系满足上述约束,那么这个Derived类的默认构造函数会被视为nontrivial,因此需要由编译器进行合成。
class B1 { public: B1() { cout << "B1() Called" << endl; } };class B2 { public: B2() { cout << "B2() Called" << endl; } };class B3 { public: B3() { cout << "B3() Called" << endl; } };class Derived : public B1, public B2, public B3 {};void Test() { cout << "**********Test() Called**********" << endl; Derived d; cout << "**********Test() Ended**********" << endl;}int main() { Test(); return 0;}
来看一下运行结果:
为什么要强调“按照声明顺序”?因为C++类里初始化顺序只与声明顺序有关
看一下我把继承顺序调换一下:
class Derived : public B3, public B1, public B2 {};void Test() { cout << "**********Test() Called**********" << endl; Derived d; cout << "**********Test() Ended**********" << endl;}
看一下运行结果:
很好,按照继承的声明顺序调用基类们的默认构造函数。
三、若子类有显式构造函数,但没有默认构造函数,派生自含有默认构造函数的基类,编译器将为子类的每一版构造函数隐式插入基类的默认构造函数。
class B1 { public: B1() { cout << "B1() Called" << endl; } };class B2 { public: B2() { cout << "B2() Called" << endl; } };class B3 { public: B3() { cout << "B3() Called" << endl; } };class Derived : public B1, public B2, public B3 {public: Derived(int) { cout << "Derived(int) Called" << endl; }};void Test() { cout << "**********Test() Called**********" << endl; Derived d(12); cout << "**********Test() Ended**********" << endl;}
同样道理,如果设计者提供了多个构造函数,但是其中没有默认构造函数的话,编译器会扩张每一个显式构造函数,来调用所有必要的基类默认构造函数。
但是有一点:编译器不会再为子类合成一个新的隐式默认构造函数,因为设计者定义的构造函数已经可以满足需要。
来运行一下看看:
很好,满足结论。
四、若子类有显式构造函数,但是基类没有默认构造函数,则不能通过编译
代码如下:
class B1 { public: B1(int a) { cout << "B1(int) Called" << endl; } };class B2 { public: B2(int a) { cout << "B2(int) Called" << endl; } };class B3 { public: B3(int a) { cout << "B3(int) Called" << endl; } };class Derived : public B1, public B2, public B3 {public: Derived() { cout << "Derived() Called" << endl; } // no default constructor exists for class "Dopey"};
结合第三点来看,这是因为,基类里面设计者已经提供了显式的带有int参数的构造函数,虽然没有默认构造函数,但是完全足够对基类进行实例化。这不满足手册提到的“在必要的时候生成隐式构造函数”的约束。
看一下编译结果:
编译器说没有和B3::B3()匹配的构造函数,这也侧面印证了如果不显式调用基类的构造函数,那么将默认自动隐式调用基类的默认构造函数。
但是在这个例子里,基类没有默认构造函数,编译器也没有为基类进行自动合成隐式构造函数。所以编译不通过。
编译错误提示我们,应该传入含有一个参数的基类构造函数进行初始化,再次印证了我们说的“不满足'在必要的时候生成隐式构造函数'的约束”。
03
—
带有虚函数的类
若类含有显式构造函数,编译器为每个构造函数隐式安插vptr向虚函数表
若没有显式构造函数,编译器将隐式合成默认构造函数对vptr行初始化。
上述两个扩张行动,将在编译期间发生。
04
—
带有虚基类的类
编译器必须使virtual base class其每一个派生类对象中的位置能够在执行期准备妥当。例如下列代码中:
class X { public: int i; };class A : virtual public X { public: int j; };class B : virtual public X { public: double D; };class C : public A, public B { public: int k; };// 编译时期无法reslove出 pa->X::i 的位置void foo(A *pa) { pa->i = 1024;}
上述代码中,真正指向的类型是可变的。因为C类也是一个A类。
编译器可能会进行如下转变:
void foo(A *pa) { pa->__vbcX->i = 1024; }
__vbcX是编译时期产生的指向virtual base class X的指针,在class object构造时期被完成。如果class没有显式声明构造函数,编译器要为其合成一个隐式默认构造函数,安插进允许每一个virtual base class得执行期存取操作的代码。
05
—
总结
有上述四种情况,会造成编译器必须为未声明构造函数的类合成一个隐式构造函数。。C++ Standard把这些合成出来的称为implicit nontrivial default constructors。被合成的构造函数只能满足编译器,而非程序的要求。至于不在这四种情况中又没有声明任何构造函数的类,它拥有的是implicit trivial default constructors,实际上并不会被合成出来。在合成的default constructors中,只有base class subobjects和member class objects会被初始化,其他的非静态数据成员均不会被初始化。这也导致了常有的两个误区:任何class如果没有定义显式默认构造函数,都会被合成出来一个。
编译器合成的隐式默认构造函数会显式设定class里每一个数据成员的默认值。
上面的两个说法都是错的。