一. 虚基类的声明:
1. 虚继承:
在以下类的定义层次中,Derived1与Derived2的对象将各自拥有一个基类子对象Base,类A作为Derived1与Derived2的派生类,将拥有2个基类子对象Base,一个存储在基类子对象Derived1中,另一个存储在基类子对象Derived2中,如果类A想使用两个基类子对象Base,这没问题,但是如果想共享同一个Base子对象,比如使用Base记录同一个状态,而不需要记录多个状态,上述常规继承就不能满足需求;
class Base {};
class Derived1 : public Base {};
class Derived2 : public Base {};
class A : public Derived1, public Derived2 {};
为了满足Derived1与Derived2共享同一个状态,使用虚继承机制:
虚继承是一种机制,类通过虚继承指出它希望共享其虚基类的状态,对于一个给定的虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象,共享的基类子对象称为虚基类;
在派生列表的访问标号后使用关键字virtual可以指定通过虚继承派生,修改上述代码后,如下:
class Base {};
class Derived1 : public virtual Base {};
class Derived2 : public virtual Base {};
class A : public Derived1, public Derived2 {};
现在Derived1与Derived2共享一个基类子对象Base,类A将拥有一个Derived1对象,一个Derived2对象,和一个Base对象,他们都是基类子对象;
2. 虚基类的常规转换:支持派生类到基类的常规转换
即使基类是虚基类,也照常可以通过基类类型的指针或者引用操作派生类的对象;例如:
Base *pb = new A;
Base *pb2 = new Derived2;
Derived1 *pd1 = new A;
3. 虚基类成员名字的可见性:假定通过多个派生路径继承名为x的成员,(x 就是一个普通的类的成员变量或者函数,被多次继承)
a:如果在每个路径中x表示同一虚基类成员,则没有二义性,因为共享该成员的单个实例;
解释:就是说在派生层次中,自始至终只有虚基类定义了成员x,所有派生类中没有定义成员x,这当然不会产生二义性,这时使用x时使用的就是虚基类中的x;
b:如果在某个路径中x是虚基类的成员,而在另一路径中x是后代派生类的成员,也没有二义性,因为特定派生类实例的优先级高于共享虚基类的实例;
解释:就是说在派生层次中其中的一条派生路径中,虚基类定义了成员x,而在另一条派生路径中,某个派生类也定义了成员x,但是,由于特定派生类实例的优先级高于共享虚基类的实例,所以也不会产生二义性,将优先使用派生类中的实例x;
c:如果沿每个继承路径,x表示后代派生类的不同成员,则该成员的直接访问是二义性的,就像非虚多重继承层次一样,这时可以在派生类中定义一个同名成员函数,在该函数中指定限定名访问成员x;
解释:就是说在派生层次中,其中一个派生路径中的一个派生类定义了成员x,而另一个派生路径中也有一个派生类也定义了成员x,那么这对x的使用就会产生二义性;
以下代码说明了多重继承包括虚继承层次中二义性的产生情况:
class Base { public: Base() : dval(0.0) {} Base(double d) : dval(d) {} void fun1(); protected: double dval; char ch; }; class D { public: void fun2(); protected: double dval; }; class Derived1 : public virtual Base { public: Derived1() : dval(0.0) {} Derived1(double d) : dval(d), Base(d) {} //如果不显式调用虚基类的任何构造函数初始化虚基类,编译器就自动调用虚基类的默认构造函数,如果虚基类没有默认构造函数就出错; private: void foo(int); protected: double dval; int ival; private: int n; }; class Derived2 : public virtual Base { public: void foo(); void foo(char); protected: int ival; int n; };
class Final : public Derived1, public Derived2//, public D { public: double fun3() { return dval; //不产生二义性,访问的是Derived1中的dval,而不是Base中的dval,因为Deverid1中的特定实例优先于虚基类中的实例; //如果把类D加入到Final的派生列表中,此处对dval的使用就会在类D与类Derived1之间产生二义性; } char fun4() { ch = 'a'; return ch; //这两个语句对ch的使用不产生二义性,访问的是虚基类Base中的ch,因为派生层次共享一个虚基类子对象,即共享一个实例; fun1(); //访问的是虚基类中的fun1,不产生二义性,原因同上; } int fun5() { //n = 0; //产生二义性,虽然n在Derived1中定义为private,在Derived2中定义为protected,仍然产生二义性; ival = 100; return ival;//这两个语句对ival的使用会产生二义性,因为基类Derived1与Derived2中都定义了ival,此时不知道应该使用哪一个, 只要成员变量名字相同,不管类型是否相同,不管访问标号是private,public或public,都将产生二义性; //注意:允许定义具有二义性的派生层次,只有在使用了具有二义性的成员时才会编译错误,否则编译通过; } void fun6() { //foo('a'); //同样,基类Derived1与Derived2中都定义了成员函数foo,虽然一个为public,一个为private,但是仍将产生二义性; //注意:多重继承中,不管成员函数的型参是否相同,数目是否一致,甚至访问标号也不相同,只要成员函数名相同就会产生二义性; } private: int ival; //对于fun5中对ival的使用,如果Final类定义了自己的ival,如此处ival的定义,就会使用此处定义的ival,这时不会产二义性; };
二. 虚基类的初始化与构造
1. 虚基类初始化:在虚派生层次中,由最底层派生类的构造函数初始化虚基类;例如在上述例子中,就由Final类的构造函数负责对虚基类Base的成员初始化,如果又有从Final派生的类如下代码:
class FinalSub : public Final
{
public:
FinalSub() : Base() {}; //默认构造函数,用虚基类的默认构造函数显式初始化虚基类
FinalSub(int v) : Base(v) {}; //如果不显式调用虚基类的任何构造函数初始化虚基类,编译器就自动调用虚基类的默认构造函数,如果虚基类没有默认构造函数就出错;
};
因为FinalSub类属于派生层次中的最底层派生类,所以由该类负责对虚基类Base成员的初始化;任何直接或者间接继承虚基类的类一般也必须为该虚基类提供初始化式,只要可以创建虚基类派生类类型的独立对象,该类就必须初始化自己的虚基类,这些初始化式仅仅在创建继承层次中间的类对象时被使用;
例如:当我们生成FinalSub类的对象时,就使用FinalSub为虚基类提供的初始化式初始化虚基类,尽管此时继承层次的中间类Derived1或者Derived2提供了自己的虚基类初始化式,但他们不会被调用,调用Derived1或者Derived2的构造函数时会忽略他们的虚基类初始化式,只会调用FinalSub提供的虚基类初始化式,因为此时Derived1或者Derived2是作为基类子对象被创建的;
当为继承层次的中间类比如Derived1生成独立的对象(而不是作为基类子对象)时,才会调用Derived1提供的虚基类初始化式,如果在最底层或继承层次的中间类的构造函数没有显示初始化虚基类,那么编译器就自动调用虚基类的默认构造函数,如果虚基类没有默认构造函数就出错;
本例中的上述代码,为Base定义了默认构造函数;
2. 构造函数与析构函数的次序:无论虚基类出现在继承层次的任何地方,总是在构造非虚基类之前构造虚基类;
假如FinalSub从Final, A, B 派生,如下:
class FinalSub : public Final, public A, public B, 则首先检查Final的继承子树,然后检查A的继承子树,最后检查B的继承子树,然后从每个子树根类开始向下到最底层派生类进行检查;同样在复制构造函数与赋值操作符中也按照相同次序给基类赋值;
例如上述代码的构造次序为:
虚基类:Base->Derived1->Derived2->Final->A->B->FinalSub;
析构次序与构造逆序:
FinalSub->B->A->Final->Derived2->Derived1->Base;