15.10 虚函数和构造函数
当创建一个包含有虚函数的对象时,必须初始化它的VPTR以指向相应的VTABLE。这必须在对虚函数进行任何调用之前完成。正如我们可能猜到的,因为生成一个对象是构造函数的工作,所以设置VPTR也是构造函数的工作。编译器在构造函数的开头部分秘密地插入能初始化VPTR的代码。正如第14章所述,如果我们没有为一个类显式创建构造函数,则编译器会为我们生成构造函数。如果该类含有虚函数,则生成的构造函数将会包含相应的VPTR初始化代码。这有几个含义。
首先,这涉及效率。内联(inline)函数的作用是对小函数减少调用代价。如果C++不提供内联函数,则预处理器就可能被用来创建这些“宏”。然而,预处理器没有访问或类的概念,因此不能被用来创建成员函数宏。另外,有了由编译器插入的隐藏代码的构造函数,预处理宏根本不能工作。
当寻找效率漏洞时,我们必须明白,编译器正在插入隐藏代码到我们的构造函数中。这些隐藏代码不仅必须初始化VPTR,而且还必须检查this的值(以免operator new返回零)和调用基类构造函数。放在一起,这些代码可以影响我们认为是一个小内联函数的调用。特别是,构造函数的规模会抵消函数调用代价的减少。如果做大量的内联构造函数调用,代码长度就会增长,而在速度上没有任何好处。
当然,也许并不会立即把所有这些小构造函数都变成非内联,因为它们更容易写为内联构造函数。但是,当我们正在调整我们的代码时,记住,务必去掉这些内联构造函数。
15.10.1 构造函数调用次序
构造函数和虚函数的第二个有趣的方面涉及构造函数的调用顺序和在构造函数中虚函数调用的方法。
所有基类构造函数总是在继承类构造函数中被调用。这是有意义的,因为构造函数有一项专门的工作:确保对象被正确地建立。派生类只访问它自己的成员,而不访问基类的成员。只有基类构造函数能正确地初始化它自己的成员。因此,确保所有的构造函数被调用是很关键的,否则整个对象不会适当地被构造。这就是为什么编译器强制为派生类的每个部分调用构造函数的原因。如果不在构造函数初始化表达式表中显式地调用基类构造函数,它就调用默认构造函数。如果没有默认构造函数,编译器将报告出错。
构造函数调用的顺序是重要的。当继承时,必须知道基类的全部成员并能访问基类的任何public和protected成员。这意味着,当在派生类中时,必须能肯定基类的所有成员都是有效的。在通常的成员函数中,构造已经发生,所以这个对象的所有部分的成员都已经建立。然而,在构造函数内,必须想办法保证所有成员都已经建立。保证它的惟一方法是让基类构造函数首先被调用。这样,当在派生类构造函数中时,在基类中能访问的所有成员都已经被初始化。在构造函数中,“知道所有成员对象是有效的”也是下面做法的原因:只要可能,我们应当在这个构造函数初始化表达式表中初始化所有的成员对象(即对象通过组合被置于类中)。只要遵从这个做法,我们就能保证初始化所有基类成员和当前对象的成员对象。