虚拟继承在一般的应用中很少用到,但是,在多重继承中,有一个很经典的应用场合。例如:
include <iostream.h>
#include <memory.h>
class CA
{
int k; //为了便于说明后面的内存结构特别添加
public:
void f() {cout << "CA::f" << endl;}
};
class CB : public CA
{
};
class CC : public CA
{
};
class CD : public CB, public CC
{
};
void main()
{
CD d;
d.f();
}
当编译上述代码时,我们会收到如下的错误提示:
error C2385: 'CD::f' is ambiguous
即编译器无法确定你在d.f()中要调用的函数f到底是哪一个。这里可能会让人觉得有些奇怪,命名只定义了一个CA::f,既然大家都派生自CA,那自然就是调用的CA::f,为什么还无法确定呢?
这是因为编译器在进行编译的时候,需要确定子类的函数定义,如CA::f是确定的,那么在编译CB、CC时还需要在编译器的语法树中生成CB::f,CC::f等标识,那么,在编译CD的时候,由于CB、CC都有一个函数f,此时,编译器将试图生成两个CD::f标识,显然这时就要报错了。(当我们不使用CD::f的时候,以上标识都不会生成,所以,如果去掉d.f()一句,程序将顺利通过编译)
要解决这个问题,有两个方法:
1、重载函数f():此时由于我们明确定义了CD::f,编译器检查到CD::f()调用时就无需再像上面一样去逐级生成CD::f标识了;
此时CD的元素结构如下:
--------
|CB(CA)|
|CC(CA)|
--------
故此时的sizeof(CD) = 8;(CB、CC各有一个元素k)
2、使用虚拟继承:虚拟继承又称作共享继承,这种共享其实也是编译期间实现的,当使用虚拟继承时,上面的程序将变成下面的形式:
#include <iostream.h>
#include <memory.h>
class CA
{
int k;
public:
void f() {cout << "CA::f" << endl;}
};
class CB : virtual public CA
{
};
class CC : virtual public CA
{
};
class CD : public CB, public CC
{
};
void main()
{
CD d;
d.f();
}
此时,当编译器确定d.f()调用的具体含义时,将生成如下的CD结构:
----
|CB|
|CC|
|CA|
----
同时,在CB、CC中都分别包含了一个指向CA的vbptr(virtual base table pointer),其中记录的是从CB、CC的元素到CA的元素之间的偏移量。此时,不会生成各子类的函数f标识,除非子类重载了该函数,从而达到“共享”的目的。
也正因此,此时的sizeof(CD) = 12(两个vbptr + sizoef(int));
所有这一切都是编译期间决定的,只是编译器为了提供这样一个新的语法功能为我们多作了一些事情而已。
一些考虑点:
(1)多重继承用得少固然是适合其施展拳脚的场合少,但对多重继承的畏惧也是一个原因,Java中鼓吹的AOP我觉得在C++中就应该使用多重继承来实现最完美,当然我对AOP并不热衷,不想去了解它,考虑在一个业务类中加入一个运行控制类,使用聚合/组合固然是可以实现,但唯有使用多重继承来表达才最接近现实模型,这是经我验证过的,使用多重继承不但表达自然,而且代码量可以减少一个数量级。]
(2)怎样避免多重继承。如果某个设计使得需要用到虚拟继承,我一般把CA设计成另外一个概念,不用于继承:
class CB{
private:
CA m_a;
};
这样做的好处是再设计CA时,可以为后来的用户带来方便,(因为不是设计为继承的,所以用户不会被多重继承和虚拟继承所烦恼)。