构造函数提供了一种机制,通过它有机会完成必要的初始化工作,从而使对象成为有意义的存在物,而不仅仅只是一块原始的空间。
但是,我们逐渐了解到,构造函数具有的地位,不仅对于用户(程序员),对于编译器履行职责也极为重要。通过这个机制,它让C++的一些基本的特性,如继承、多态得到了正确的贯彻和表现。
首先不难理解的一点是在构造函数中,要确保基类对象的正确构造,如果是从基类继承的话。因为继承类对象至少可以被“低”看为一个基类对象,具有后者的所有行为和表现,所以基类的构造函数首先被调用。如果所谓的基类也是从其他类继承过来的,这就形成了一个调用链。最后的情况是,最基础的类的构造函数首先被执行,然后才是上一层的构造函数,如此到最外层的继承类。这个过程必须是严格有序的。如果没有这个次序保证,继承类就有机会在基类还没构建好的情况下就访问基类的数据或函数,这将导致不可预料的、灾难性的后果。
只有首先确保基类的正确构造,接下来才能进行继承类本身的构造。因为类中可能含有成员对象,必须保证这些对象也被构造,按照一定的次序(一般就是变量声明的次序)。可能其中一个或一些成员对象的构造需要参数,这需要在类的构造函数中提供所有需要的参数(构成所谓显式的“初始化列表”)。
看起来这就是构造函数的全部隐含(或半隐含)工作?不是的。我们忽视了一个极为重要的东西,我称之为类关联信息(表)。类关联信息是编译时生成的、为运行时所需的类的附加数据(可以认为这些数据放在全局数据区中)。我们熟悉的虚函数表(v-table),我把它归在类关联信息的范畴之中(最初,我以为虚函数表就是全部,但这个理解有点狭义)。此外,还包含运行时类型信息(RTTI)。此外,不排除我们还不清楚的其它辅助数据(总之,类关联信息是一种广义的、统一的称呼)。如果编译器为类生成了类关联信息,那么毫无疑问,必须在构造函数中将它与当前对象(类的实例)关联起来(也许简单到只需要设置一个指针即可)。
例如,如果类中含有虚函数,或者,它覆盖了基类中的虚函数(两种情况下都意味着类有自己的虚函数表)。那么,设置正确的关联后,将存在一个指针(v-ptr),它正确地指向了该虚函数表(v-table)。此后,多态才能表现出所期望的正确行为。
再次指出(次序的重要性),设置关联,或者狭义地说,设置v-ptr必须发生在对基类构造函数的调用之后。因为,继承类如果有自己的虚函数表,那么v-ptr会被改写,以指向该表,即使此前v-ptr已经被基类所设置。这是合法的,也正是所期望的。但是语义上我们绝不允许基类可以改写继承类所设置的v-ptr.如果v-ptr设置发生在基类构造调用之前,那么这种非法的一幕就会发生。
所有上述的事情完成之后(对于用户来说它们几乎是隐含的),才真正开始执行用户的初始化代码。
在构造函数中调用虚函数,会发生什么?发生的情况也许是始料未及的。当前类的对象正在构建,v-ptr指向的是当前类的虚函数表。此时,还没到继承类执行它自己的代码(如设置v-ptr,执行初始化代码)的时候,那一切发生在当前类的构造函数执行完毕之后。所以,将要执行的是本类的虚函数版本,而不是可能被覆盖的继承类的版本。这里有一个反面的证据。假如v-ptr设置发生在基类构造函数调用之前,让我们有机会调用继承类的虚函数版本,这意味着什么?继承类还没有完成初始化(因而对象还没有构建好),我们企图在一个没有构建好的对象上执行它的成员函数,可以想见后果是灾难性的。
类的构造函数何时被调用?在对象被创建的时候。对象可能位于栈上,全局数据区,或堆上。对象可能会在声明的地方创建,这样的对象位于栈上或全局数据区。对象也可以使用new操作符动态地创建,这样的对象将位于堆上。