虚拟继承在一般的应用中很少用到,所以也往往被忽视,这也主要是因为在C
++中,多重继承是不推荐的,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要(因为这样只会降低效率和占用更多的空间,实在是一无是处)。
以下面的一个例子为例:
#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 )) ;
所有这一切都是编译期间决定的,只是编译器为了提供这样一个新的语法功能为我们多作了一些事情而已。
注:以上讨论限MS Visual C ++编译器。
以下面的一个例子为例:
#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 )) ;
所有这一切都是编译期间决定的,只是编译器为了提供这样一个新的语法功能为我们多作了一些事情而已。
注:以上讨论限MS Visual C ++编译器。