Performanced C++ 经验规则(1):你不知道的构造函数(上)
Performanced C++ 经验规则
前言:Performanced C++,意为“高性能C++“编程,是笔者和所在团队多年C++编程总结的经验规则,按条款方式讲述(参考了《Effective C++》的方式),希望能对初入C++的程序员提供帮助,少走弯路,站在前人的肩膀上,看得更高走的更远。我们也同样是脚踩许许多多大牛的经典著作,还有无数默默付出的程序员的辛劳,以及自己许许多多惨痛的编程体验,才有了这些“规则”。
第一条:你不知道的构造函数(上)
首先来看,我们“知道”的构造函数,C++构造函数究竟做了哪些事情?
1、创建一个类的对象时,编译器为对象分配内存空间,然后调用该类的构造函数;
2、构造函数的目的,是完成对象非静态成员的初始化工作(静态成员如何初始化?记住以下要点:在类外进行、默认值为0、在程序开始时、在主函数之前、单线程方式、主线程完成),记住:C++类非静态成员是没有默认值的(可对比Java)。
3、如果构造函数有初始化列表,则先按照成员声明顺序(非初始化列表中的顺序)执行初始化列表中的内容,然后再进入构造函数体。这里又有疑问了,如果类本身没有非虚拟的基类,应显式地调用直接基类的某个构造函数,否则,将会自动其直接基类的默认构造函数(如果此时直接基类没有默认构造函数,得到编译错误);如果类本身有虚拟基类,也应显式地调用虚拟基类的某个构造函数,否则,将会自动调用虚拟基类的默认构造函数;如果成员有其它类的对象,则应显式地调用成员所属类的相应构造函数,否则对于没有在初始化列表中出现的类成员,也会自动调用其默认的构造函数。
注意上述调用顺序,编程时应按照“先祖再客最后自己”的原则进行,即,首先完成自身包含的“祖先对象”的初始化,之后,完成自身包含的成员是其它类型(客人)的初始化,最后才是自身非类类型成员的初始化工作。
再注意,上面多次提到了术语“默认构造函数”,默认构造函数是指:无参构造函数或每个参数均有默认值的构造函数。当且仅当,一个类没有声明任何构造函数时,可认为编译器会自动为该类创建一个默认构造函数(无参的,注意“可认为”,即实际情况并非如此,编译器并不一定总是会自动创建默认构造函数,除非必要,这涉及到更深的汇编层面。当然,在写代码的时候,这个“可认为”是正确的)。
这一小部分内容可能信息量过大,让我们看一段代码以加深理解。
#include <iostream> using namespace std; class Base { private: int _x; public: Base(int x) : _x(x) { cout << "Base(x) _x=" << _x << endl; } Base() {} }; class DerivedA :virtual public Base { int _y; public: DerivedA(int x = 0, int y = 1) : Base(x), _y(y) { cout << "DerivedA(x,y) _y=" << _y << endl; } }; class DerivedB :virtual public Base { int _z; public: DerivedB(int x = 0, int z = 2) : Base(x), _z(z) { cout << "DerivedB(x,z) _z=" << _z << endl; } }; class Other { int _o; public: Other() : _o(3) { cout << "Other() _o=" << _o << endl; } }; class DerivedFinal : public DerivedB, public DerivedA { int _xyz; Other _other; public: DerivedFinal(int x = 10, int y = 20, int z = 30, int o = 50) : DerivedA(x,y), DerivedB(x,z), Base(x), _xyz(x * y * z) { cout << "DerivedFinal(x,y,z,o) _xyz=" << _xyz << endl; } }; int main(int argc, char** argv) { DerivedFinal df; return 0; }
输出结果(Ubuntu 12.04 + gcc 4.6.3):
Base(x) _x=10 DerivedB(x,z) _z=30 DerivedA(x,y) _y=20 Other() _o=3 DerivedFinal(x,y,z,o) _xyz=6000
和你心中的答案是否一致呢?
一切从DerivedFinal的调用顺序说起,首先,这是虚继承,故虚基类Base的构造函数将首先被调用,尽管它在DerivedFinal构造函数的初始化列表顺序中排在后面的位置(再次记住,调用顺序与初始化列表中的顺序无关),接下来是DerivedB(x,z),因为它先被继承;之后是DerivedA(x,z),再之后,DerivedFinal自身非类类型成员_xyz被初始化,最后是Other(),other成员并没有出现在DerivedFinal的初始化列表中,所以它的默认构造函数将被自动调用。另外,如果不是虚继承,调用间接基类Base的构造函数将是非法的,但此处是虚继承,必须这样做。
接下来继续讨论,上面提到,编译器不一定总是会产生默认构造函数,虽然在编写代码时,你“可以这么认为”,这听起来太玄乎了,那么,到底什么时候,编译器才会真正在你没有定义任何构造函数时,为你产生一个默认构造函数呢?有以下三种情况,编译器一定会产生默认构造函数:
(1)该类、该类的基类或该类中定义的类类型成员对象中,有虚函数存在。
发生这种情况时,由于必须要完成对象的虚表初始化工作(关于虚函数的原理,笔者建议参考陈皓的《C++虚函数表解析》),所以编译器在没有任何构造函数的时候,会产生一个默认构造函数来完成这部分工作;然而,如果已经有任何构造函数,编译器则把初始化虚表这部分工作“合成”到你已定义的构造函数之中(用心良苦)。
让我们稍稍进入汇编领域(笔者强烈建议,要精通C/C++,一定的汇编和反汇编能力是必须的,能精通更好)看一下,一个有虚函数的类,构造函数的x86反汇编代码:
class VirtualTest { public: virtual void foo(int x) { cout << x << endl; } }; int main(int argc, char** argv) { VirtualTest vt; lea ecx, [ebp-4] ;获取对象首地址 call @ILT+15(VitrualTest::VirtualTest) (0048A500) ;调用构造函数,由于该类没有定义任何构造函数又包含虚函数,编译器产生了一个默认构造函数并调用 return 0; } //下面是默认构造函数反汇编 004013D0 55 push ebp 004013D1 8B EC mov ebp,esp 004013D3 51 push ecx ;头三句,初始化函数调用过程,详见汇编知识 004013D4 89 4D FC mov dword ptr [ebp-4],ecx ;获取对象首地址,即this指针 004013D7 8B 45 FC mov eax,dword ptr [this] ;取出this指针,这个地址将会作为指针保存到虚表首地址 004013DA C7 00 60 68 40 00 mov dword ptr [eax],offset VirtualTest::`vftable' (0042201c) ;取虚表首地址,保存到虚表指针中(即对象头4字节) 004013E0 8B 45 FC mov eax,dword ptr [this] ;再次取出this指针地址,返回函数调用,即得到对象 004013E3 8B E5 mov esp,ebp 004013E5 5D pop ebp 004013E6 C3 ret
由该汇编代码还可以看出,虚表指针初始化,在构造函数初始化列表之后,进入构造函数体代码之前。
(2)该类、该类的基类中所定义的类类型成员对象中,带有构造函数。
发生这种情况时,由于需要显式地调用这些类类型成员的构造函数,编译器在没有任何构造函数的时候,也会产生一个默认构造函数来完成这个过程;同样,如果你已经定义一个构造函数但没有对这些类类型成员显式调用构造函数,编译器则把这部分工作“合成”到你定义的构造函数中(调用它们的默认构造函数,再次用心良苦)。
(3)该类拥有虚基类。
发生这种情况,需要维护“独此一份”的虚基类继承而来的对象,所以也需要通过构造函数完成。方式同(1)(2)。
除上述3种情况外,“可认为在没有任何构造函数时候,编译器产生一个默认构造函数”是不对的,因为这样的默认构造函数是“无用”的,编译器也就不会再用心良苦去做没用的工作。这部分涉及汇编较多,如果想详细了解,建议阅读钱林松所著的《C++反汇编与逆向分析技术揭秘》,机械工业出版社,2012.5。
这里只要记住结论就可以了。
终于讲述完了,进入构造函数体之前的奥秘,你是否觉得不过瘾呢?不着急,下一篇将讲述C++进入构造函数体之后,那些你不知道的内容。