为了避免上述Top类的多次继承,我们必须虚拟继承类Top(Top类成为虚基类,当在多条继承路径上有一个公共的基类,在这些路径中的某几条汇合处,这个公共的基类就会产生多个实例(或多个副本),若只想保存这个基类的一个实例,可以将这个公共基类说明为虚基类
虚基类的主要作用: 虚基类主要解决多重继承时,基类被多次进程的问题 )。
参考博客 多重继承_内存的分布:: http://blog.163.com/xychenbaihu@yeah/blog/static/132229655201242703331596/
1 class Top
2 {
3 public:
4 int a;
5 };
6
7 class Left : virtual public Top
8 {
9 public:
10 int b;
11 };
12
13 class Right : virtual public Top
14 {
15 public:
16 int c;
17 };
18
19 class Bottom : public Left, public Right
20 {
21 public:
22 int d;
23 };
24
上述代码将产生如下的类层次图(其实这可能正好是你最开始想要的继承方式):
对于程序员来说,这种类层次图显得更加简单和清晰,不过对于一个编译器来说,这就复杂得多了。
我们再用Bottom的内存布局作为例子考虑,它可能是这样的:
按照下面的分析,实际应该是:
这种内存布局的优势在于它的开头部分(Left部分)和Left的布局正好相同,我们可以很轻易地通过一个Left指针访问一个Bottom对象。但这并不是我们讨论的重点,我们主要来考虑考虑Right:
1 Right* right = bottom;
这里我们应该把什么地址赋值给right指针呢?
理论上说,通过这个赋值语句,我们可以把这个right指针当作真正指向一个Right对象的指针(现在指向的是Bottom)来使用。但实际上这是不现实的!一个真正的Right对象内存布局和Bottom对象Right部分是完全不同的,所以其实我们不可能再把这个upcasted的bottom对象当作一个真正的right对象来使用了。而且,我们这种布局的设计不可能还有改进的余地了。
这里我们先看看实际上内存是怎么分布的,然后再解释下为什么这么设计。
1 Bottom* bottom = new Bottom();
2 Left* left = bottom;
3 int p = left->a;
第二条的赋值语句让left指针指向和bottom同样的起始地址(即它指向Bottom对象的“顶部”)。我们来考虑下第三条的赋值语句。下面是它汇编结果:
1 movl left, %eax # %eax = left
2 movl (%eax), %eax # %eax = left.vptr.Left
3 movl (%eax), %eax # %eax = virtual base offset
4 addl left, %eax # %eax = left + virtual base offset
5 movl (%eax), %eax # %eax = left.a
6 movl %eax, p # p = left.a
总结下,我们用left指针去索引(找到)virtual table,然后在virtual table中获取到虚基类的偏移(virtual base offset, vbase),然后在left指针上加上这个偏移量,这样我们就获取到了Bottom类中Top类的开始地址。
从上图中,我们可以看到对于Left指针,它的virtual base offset是20,如果我们假设Bottom中每个成员都是4字节大小,那么Left指针加上20字节正好是成员a的地址。
我们同样可以用相同的方式访问Bottom中Right部分。
1 Bottom* bottom = new Bottom();
2 Right* right = bottom;
3 int p = right->a;
right指针就会指向在Bottom对象中相应的位置(该图非常清晰的表明了虚继承时,类对象的内存分布,vptr.Left和vptr.Right这两个虚表指针中存放的v-tab的地址,虚表中存放的是成员函数的地址,虚基类成员变量的在对象中的偏移量)。
(图 1 - 1 最为清楚的表示了虚继承类对象的内存分布,引入了虚函数表)
这里对于p的赋值语句最终会被编译成和上述left相同的方式访问a。唯一的不同是就是vptr,我们访问的vptr现在指向了virtual table另一个地址,我们得到的virtual base offset也变为12。
需要提醒的是以上设计需要承担一个相当大的代价:我们需要引入虚函数表,对象底层也必须扩展以支持一个或多个虚函数指针,原来一个简单的成员访问现在需要通过虚函数表两次间接寻址(编译器优化可以在一定程度上减轻性能损失)。
测试代码:
#include <stdio.h>
class Top { public: int a; };
class Left:virtual public Top { public: int la; };
class Right:virtual public Top { public: int ra; };
class Bottom:public Left,public Right { public: int lra; };
int main() { //引入了vptr,虚函数表指针,所以是12个字节 printf("sizeof(Left)=%d\n",sizeof(Left));
//由于引入了Left的vptr,Right的vptr,所以是24个字节 printf("sizeof(Bottom=%d)\n",sizeof(Bottom));
Bottom obj; obj.a = 10; obj.la = 20; obj.ra = 30; obj.lra = 40; //由于Top只被继承了一次(而之前的多重继承却继承了多次) printf("obj.a=%d\tobj.la=%d\tobj.ra=%d\tobj.lra=%d\n", obj.a,obj.la,obj.ra,obj.lra);
Left *left = &obj; Right *right = &obj; //left的指针和&obj的相同,而right和&obj是不同的 printf("left=%p\tright=%p\t&obj=%p\n",left,right,&obj); //由于虚函数表指针的存在,不再需要类名来区分,a只有一个 printf("left->a=%d,right->a=%d\n",left->a,right->a); //虚函数表解决了歧义问题,而且Top只被继承了一次,所以不会产生歧义 Top *top = &obj; printf("top->a=%d\n",top->a); return 0; }
运行结果:
再提一个问题如果是:
class Bottom:virtual public Left,virtual public Right { public: int lra; };
那么sizeof(Bottom)占用多大内存呢? 是28个字节,又引入了vptr。