尽管在派生列表中同一个基类只能出现一次,但实际上派生类可以多次继承同一个类。派生类可以通过它的两个直接基类分别继承同一个间接基类,也可以直接继承某个基类,然后通过另一个基类再一次间接继承该类。
举个例子,IO标准库的istream和ostream分别继承了一个共同的名为base_ios的抽象基类。该抽象基类负责保存流的缓冲内容并管理流的条件状态。iostream是另外一个类,它从istream和ostream直接继承而来,可以同时读写流的内容。因为istream和ostream都继承自base_ios,所以iostream继承了base_is两次,一次是通过istream,另一次是通过ostream。
在默认情况下,派生类中含有继承链上每个类对应的子部分。如果某个类在派生过程中出现了多次,则派生类中将包含该类的多个子对象。
这种默认的情况对某些形如iostream的类显然是行不通的。一个iostream对象肯定是希望在同一个缓冲区进行读写操作,也会要求条件状态能同时反映输入和输出操作的情况。假如在iostram对象中真的包含了base_ios的两份拷贝,则上述的共享行为就无法实现而了。
在C++语言中我们通过虚继承(virtual inheritance)的机制解决上述问题。虚继承的目的是令某个类做出声明,承诺愿意共享它的基类。其中,共享的基类子对象成为虚基类(virtual base class)。在这种机制下,无论虚基类在继承体系中出现了多少次,在派生类中只包含唯一一个共享的虚基类子对象。
使用虚基类
我们指定虚基类的方式是在派生列表中添加关键字virtual:
//关键字public和virtual的顺序随意 class Raccon : public virtual ZooAnimal {/*...*/}; class Bear : virtual public ZooAnimal {/*...*/};
通过上面的代码我们将ZooAnimal定义为Raccon和Bear的虚基类。
virtual说明符表明了一种愿望,即在后续的派生类中共享虚基类的同一份实例。至于什么样的类能够作为虚基类并没有特殊规定。
如果某个类指定了虚基类,则该类的派生仍按常规方式进行:
class Panda : public Bear, public Raccon, public Endangered{};
Panda通过Raccon和Bear继承了ZooAnimal,因为Raccon和Bear继承了ZooAnimal的方式是虚继承,所以在Panda中只有一个ZooAnimal基类部分。
支持基类的常规类型转换
无论基类是不是虚基类,派生类对象都能被可访问基类的指针或引用操作。例如,下面这些从Panda向基类的类型转换都是合法的:
void dance(const Bear&); void rummage(const Raccoon&); ostream& operator<<(ostream&, const ZooAnimal&); Panda ying_yang; dance(ying_yang); //正确:把一个Panda对象当成Bear对象传递 dance(ying_yang); //正确:把一个Panda对象当成Raccoon传递 cout << yingyang; //正确:把一个Panda对象当成ZooAnimal传递
虚基类成员的可见性
因为在每个共享的虚基类中只有唯一一个共享的子对象,所以该基类的成员可以被直接访问,并不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,则我们仍然可以直接访问这个被覆盖的成员。但是如果被多余一个基类覆盖,则一般情况下派生类必须为该成员自定义一个新的版本。
例如,假定B定义了一个名为x的成员,D1和D2都是通过B虚继承得到的,D继承了D1和D2,则在D的作用域中,x通过D的两个基类都是可见的。如果我们通过D的对象使用x,有三种可能性:
- 如果D1和D2中都没有x的定义,则x将被解析成B的成员,此时不存在二义性,一个D对象只含有一个x的实例。
- 如果x是B的成员,同时是D1和D2中某一个的成员,则同样没有二义性,派生类的x比共享虚基类B的x优先级更高。
- 如果在D1和D2中都有x的定义,则直接访问x将产生二义性问题。
与非虚的多重继承体系一样,解决这种二义性问题最好的方法是在派生类中为成员自定义新的实例。