10.5.4 虚基派生类的构造函数和析构函数

    前文提及,为了对包含在派生类对象中的基类对象进行复制和初始化,派生类的构造函数要调用基类的构造函数,虚基派生构造函数的调用规则同前面单基派生和多基派生的情况略有不同,为直观起见,结合图10.6来进行说明。

    首先引入“最派生类”的概念,实际应用中,继承结构的层次可能很多,为了便于说明问题,规定在建立对象时指定的类为“最派生类”。对普通的多层继承而言,构造函数的调用多少有点嵌套的意味,如由C1类派生C2类,C2类又派生C3类时,表示为以下形式。


    C2(总参数表):C1(参数表) C3(总参数表):C2(参数表)

    即对C3中C1类成员的初始化是通过调用C2类构造函数,进而在C2类构造函数中嵌套调用C1类构造函数来实现的。

    对虚基派生来说,这有些行不通,类B和类C也可以创建对象,因此,必须在类B和类C的构造函数中调用虚基类A的构造函数,如果只是简单地利用类层次构造函数的嵌套调用,则在最派生类D时,虚基类A的构造函数会被执行两次。

    根据虚基派生的性质,最派生类D中只有一份虚基类(共同基类)A的备份,因此,虚基类A的构造函数在D的构造函数中只能被调用一次。

    所以,从虚基类A中直接派生(类B和类C)或间接派生(类D)的类中,其构造函数的初始化列表中都要列出对虚基类A构造函数的调用,但只有用于创建对象的那个派生类的构造函数才能够真正调用虚基类A的构造函数,而该派生类的基类构造函数的初始化表中列出的对虚基类A的构造函数调用在执行时被忽略,这样便保证了对虚基类的子对象只初始化一次。

    举例说明其正确的用法,如代码10.7所示。

    代码10.7 虚基派生类的构造函数和析构函数VirtualConstructorDestructor


    <——————————-文件名:example1007.cpp————————————————> 01 #include<iostream> 02 using namespace std; 03 class A//类A定义 04 { 05 private://private成员列表 06 int x; 07 public://public成员列表 08 A(int xp=0)//构造函数,带默认参数 09 { 10 x=xp; 11 cout<<"A的构造函数被执行"<<endl; 12 } 13 ~A()//析构函数 14 { 15 cout<<"A的析构函数被执行"<<endl; 16 } 17 }; 18 class B:virtual public A//类B由类A虚基派生而来 19 { 20 public: 21 B(int xp):A(xp)//在初始化表中调用基类构造函数 22 { 23 cout<<"B的构造函数被执行"<<endl; 24 } 25 ~B()//析构函数 26 { 27 cout<<"B的析构函数被执行"<<endl; 28 } 29 }; 30 class C:virtual public A//类C由类A虚基派生而来 31 { 32 public: 33 C(int xp):A(xp)//在初始化表中调用基类构造函数 34 { 35 cout<<"C的构造函数被执行"<<endl; 36 } 37 ~C()//析构函数 38 { 39 cout<<"C的析构函数被执行"<<endl; 40 } 41 }; 42 class D:public B,public C//类D由类B和类C共同派生而来 43 { 44 public: 45 D(int xp):B(xp),C(xp),A(xp) 46 //初始化表中不仅要调用类B和类C的构造函数,还应调用共同虚基类的构造函数 47 { 48 cout<<"D的构造函数被执行"<<endl; 49 } 50 ~D()//析构函数 51 { 52 cout<<"D的析构函数被执行"<<endl; 53 } 54 }; 55 int main() 56 { 57 D expD(2);//声明D类对象expD 58 return 0;//main函数执行完毕退出后,expD撤销,析构函数触发执行 59 }

    输出结果如下所示。

    A的构造函数被执行

    B的构造函数被执行

    C的构造函数被执行

    D的构造函数被执行

    D的析构函数被执行

    C的析构函数被执行

    B的析构函数被执行

    A的析构函数被执行

    【代码解析】代码10.7中,代码第57行,在创建D类对象expD时,D类的两个基类B和C的构造函数都被调用,但代码第21行的类B和代码第33行的类C构造函数初始化表中的对虚基类A构造函数的调用被忽略了,执行的是类D构造函数初始化表中列出的虚基类A的构造函数。

    如果在程序中使用诸如“B expB(1);”的语句创建类B对象时,则在类B构造函数初始化表中列出的对虚基类A构造函数的调用便会被执行,这解释了为什么要在虚基类派生路径上的每个类构造函数的初始化表都要列出对虚基类构造函数的调用,这样便很好地保证了不管路径多深,虚基类的构造函数必须且只能被调用一次。

    注意

    C++规定,虚基类构造函数的执行要先于非虚基类构造函数的执行,同类构造函数的执行顺序仍遵循类定义时的派生顺序,而不是出现在初始化表中的顺序。

    析构函数的执行顺序与构造函数的执行顺序相反。

    特别要说明的是一类特殊情况,即如图10.7表示的多虚基类的情况。

    将图10.7各个类的定义简写如下所示(假设采用public派生方式)。

    10.5.4 虚基派生类的构造函数和析构函数 - 图1

    图 10.7 多虚基类的情况


    class C:virtual public A,virtual public B class D:virtual public A,virtual public B class E:public C,public D各个类的构造函数简写如下所示。 C():A(),B() D():A(),B() E():C(),D(),A(),B();

    注意,定义时在前的类在图的左边,在创建E类对象时,按从左到右深度优先遍历算法来调用各个类的构造函数,如下所述。

    (1)初始化A类复制

    (2)初始化B类复制

    (3)初始化C类复制

    (4)初始化D类复制

    (5)初始化E类复制