C++语言编译过程中,编译器会背着程序员做了很多工作,其中默认构造函数就是其中一件工作。编译器对代码的干涉动作比较可能发生在“Member initialization”或者“named return value optimization”(NRV)身上,这些干涉动作会带来“程序形式”和“程序效率”上的冲击。
1.名词解释
implicit:暗中的,隐式的(通常指并非在源代码中出现的)explicit:显示的(通常意指成员源码中所出现的)trivial:没有用的nontrivial:有用的memberwise:对每一个member施加以...bitwise:对每一个bit施加以...2. Default Constructor的构造操作
只有在编译器需要的时候才会合成一个default constructor,且被合成的default constructor只执行编译器所需的行动。C++ Standard中:
“对于class X,如果没有任何user-declared constructor,那么会有一个default constructor被暗中(implicitly)声明出来......一个被暗中声明出来的default constructor将是一个trivial constructor.......”
nontrivial default constructor生成的四种情况:
(1)带有Default Constructor的Member Class Object
如果一个class没有任何constructor,但它内含一个member object,而后者有default constructor,那么这个class的implicit default constructor 就是“nontrivial”,编译器需要为该class 合成一个default constructor。不过这个合成操作只有在constructor真正需要被调用的时候才会发生。
被合成的default constructor内含必要的代码,能够调用member object的default constructor来处理构造操作,除此之外,不会合成其他的代码。
举个例子,在下面的程序片段中,编译器为class Bar合成了一个default constructor。
class Foo { public: Foo(), Foo(int) ...};class Bar { public: Foo foo; char* str};void foo_bar(){Bar bar; // 这里需要调用Bar的构造函数,编译器会合成一个// default constructor,如果没有任何地方调用// Bar的构造函数,编译器不会为其合成,即使Bar// 没有默认的构造函数if (bar.str) {}...}
被合成的Bar default constructor内含必要的代码,能够调用class Foo的default constructor来处理member object Bar::far, 但是它并不会产生代码来初始化Bar::str。被合成的default constructor的伪代码如下:
inline Bar::Bar(){foo.Foo::Foo();}
再一次请注意,合成的代码只是满足编译器的需要,而不是满足程序员的需要,因此,对str的初始化是程序员的责任。为了让这段代码顺利执行,字符指针str也必须被初始化,假设程序员提供了下面的default constructor,对str进行了初始化。
Bar::Bar() { str = 0; }
现在程序员的要求满足了,但是编译器还需要初始化member object foo,由于default constructor已经显示定义出来,编译器没法合成第二个。这种情况编译器会如何处理?
如果class有default constructor。编译器就不会合成default constructor,而是扩充已有的default constructor。如果class A内含一个或一个以上的member class objects,那么class A的每一个constructor必须调用每一个member classes的default constructor。编译器会扩张已存在的constructors,在其中安插一些代码,使得user code在被执行之前,先调用必要的default constructor。如果有多个class member objects都要求constructor初始化操作,C++语言将要求以member objects在class中的声明次序来调用每个constructor。这一点由编译器完成。请看下面的例子,假设有三个class定义如下:
class Dopey { public: Dopey();...};class Sneezy { pyblic: Sneezy(int); Sneezy();...}class Bashful {public: Bashful();...}
以及一个class Snow_White:
class Snow_White{public: Dopey dopey;Sneezy sneezy;Bashful bashful;private:int mumble;};
如果Snow_White 没有定义default constructor,就会有一个nontrivial constructor被合成出来,依序调用Dopey,Sneezy,Bashful的default constructor,然而,如果Snow_White定义了下面这样的default constructor:
Snow_White::Snow_White : sneezy(1024){mumble = 2048;}
它会扩张为:
// 编译器扩张后的default constructor// C++ 伪代码Snow_White::Snow_White() : sneezy(1024){// 插入 member class object// 调用其constructordopey.Dopey::Dopey();sneezy.Sneezy::Sneezy(1024);bashful.Bashful::Bashful();// explicit user codemumble = 2048;}
(2)带有Default Constructor的Base Class
类似的道理,如果一个没有任何constructor的class派生自一个“带有default constructor”的base constructor,那么这个derived class的default constructor会被视为nontrivial,并因此需要被合成出来。它将会调用上一层的base class的default constructor(依据它们声明的顺序)。对一个后继派生的class而言,这个合成的constructor和一个“被显式提供的default constructor”没有什么差异。
如果设计者提供多个constructor,但其中都没有default constructor,那么编译器会扩张现有的每一个constructor,将“用以调用所有必要之default constructor”的程序代码加进去,它不会合成一个新的default constructor,因为其他“有user所提供的constructor”存在的缘故。如果同时存在着“带有default constructor”的member class objects,那些default constructor也会在base class constructor被调用之后,依次被调用。
(3)带有一个Visual Function的Class
另有两种情况,也需要合成出Default constructor:
class声明(或继承)一个visual function。
class派生自一个继承串链,其中有一个或更多的visual base class。
不管是哪一个情况,由于缺乏由user声明的constructor,编译器会详细记录合成一个default constructor的必要信息,以下面代码为例:
class Widget { public: virtual void flip() = 0;}void flip(const Widget& widget) { widget.flip();}// 假设Bell 和 Whistle 都派生自Widgetvoid foo(){Bell b;Whistle w;flip(b);flip(w);}
下面的两个扩张操作会在编译期间发生:
一个virtual function table(vtbl)会被编译出来,内放class的virtual functions地址在每一个class objects中,一个额外的pointer member(也就是vptr)会被编译器合成出来,内含相关的class vtbl的地址。
此外,widget的flip的虚拟调用,会被重写改写,以使用widget的vptr和vtbl中的flip()条目:
// widget.flip的虚拟调用操作的转变(*widget.vptr[1])(&widget)
1 表示flip在virtual table中的固定索引&widget代表要交给“被调用的某个flip函数实例”的this指针
为了让此机制发挥功效,编译器必须为每一个Widget(或其派生类) object的vptr设置初值,放在恰当的virtual table 地址,对于class所定义的每一个constructor,编译器会安插一些代码做这样的事情。
(4)带有一个visual base class的class
对于class 所定义的每一个constructor,编译器会安插那些“允许每一个virtual base class的执行期存取操作”的代码,如果class 没有声明任何constructor,编译器必须为它合成一个default constructor。
小结:
C++有两个误解:
1)任何class如果没有定义Default constructor,就会被合成出一个来;
2)编译器合成出来的Default constructor会明确设定“class 内每一个data member的默认值”。
上面的两个都是错误的。
然而,事实上,C++合成默认构造函数只是在编译器需要的情况下才会合成,并且只有四种情况,编译器会为未声明constructor的classes合成一个default constructor,这些合成的为成为 implicit nontrivial default constructor。至于没有存在那4种情况而又没有声明任何constructor的classes,我们说它们拥有implicit trivial default constructor,它们实际不会被合成出来。
3.Copy Constructor的构造操作
有三种情况,会以一个object的内容作为另一个class object的初值。
对一个object做明确的初始化操作。当object被当做参数交给某个函数。当函数返回一个class object。
(1)Default Memberwise Initialization
当class object以“相同class的另一个object”作为初值时,其内部是以所谓的default member initialization手法完成的,也就是把每一个内建的或派生的data member的值,从某个object拷贝一份到另一个object身上。不过它并不会拷贝其中的member class object,而是以递归的方式实施memberwise initialization。 决定一个copy Constructor是否为trivial的标准在于class是否展现出所谓的“bitwise copy semantics”。bitwise copy semantics(位逐次拷贝)意思就是:使用bitwise copy semantics时就不会使用default Constructor或copy Constructor。
(2)bitwise copy semantics
有四种情况不需要bitwise copy semantics:
当class 内含一个member object而后者的class声明有一个copy Constructor时。当class继承自一个base class而后者存在有一个copy Constructor时。当class声明了一个或多个virtual functions时。当class派生自一个继承串链,其中有一个或多个virtual base classes时。
前两种情况中,编译器必须将member或base class的“copy Constructor调用操作”安插到被合成的“copy constructor"中。后两种情况中,存在virtual functions和virtual base classes,如果使用位逐次拷贝,其vptr就会出错。
后两种情况会出现重新设定virtual table的指针,只要有一个class声明了一个或多个virtual functions就会如此。
(3)扩张操作
增加一个virtual function table(vtbl),内含每一个有作用的virtual function的地址。将一个指向virtual function table的指针(vptr),安插在每一个class object内。当一个编译器导入一个vptr到class之中时,该class就不再展现bitwise semantics了,故编译器需要合成一个copy constructor,以求将vptr适当地初始化。
4. 程序转化语意学
(1)明确的初始化操作
必要的程序转化包括两个阶段:(1)重写每一个定义,其中的初始化操作会被剥夺;(2)class的copy constructor调用操作会被安插进去。
(2)参数的初始化
其中一种策略:导入所谓的暂时性object,并调用copy constructor将它初始化,然后将该暂时性object交给函数,函数调用完成后,临时对象将会被析构。还有一种策略”拷贝构建“,将实际参数直接建造在其应该的位置上。
(3)返回值的初始化
named return value(NRV)优化机制。有时候看似不需要拷贝构造函数,但是为了NRV效率问题,也可以提供一个默认拷贝构造函数。
小结:
拷贝构造函数的使用,迫使编译器对程序代码做部分优化,尤其当一个函数以传值(by value)的方式传回一个class object,而该class有一个copy constructor时,这将导致深奥的程序转化——不论在函数的定义或使用上。此外编译器也将copy constructor的调用操作优化,以一个额外的第一参数取代NRV。
5.成员们的初始化队伍(Member Initialization List)
当写一个constructor时,有机会设定class members的初值。要不是经由member Initialization list就是在构造函数内部进行数据成员的初始化。
下面四种情况必须使用成员初始化列表:
当初始化一个reference member时;当初始化一个const member时;当调用一个base class的constructor,而它拥有一组参数时;当调用一个member class的constructor,而它拥有一组参数时。
编译器会对Initialization list一一处理并可能重新排序,以反映出member的声明次序。它会安插一些代码到constructor体内,并置于任何explicit user code之前。
总结:
主要介绍了default constructor、copy constructor还有何种情况使用到copy constructor以及成员初始化列表。其中default constructor只是在四种情况下才会被编译器构造出来,copy constructor和位逐次拷贝,在一些含有copy constructor的类中或者有virtual中,编译器需要合成copy constructor而不能使用位逐次拷贝,成员初始化列表在四种情况下必须使用。