C++虚拟继承中对象内存的分布

虚拟继承

为了避免上述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

上述代码将产生如下的类层次图(其实这可能正好是你最开始想要的继承方式):

C++虚拟继承中对象内存的分布_第1张图片      
 

 

对于程序员来说,这种类层次图显得更加简单和清晰,不过对于一个编译器来说,这就复杂得多了

 

我们再用Bottom的内存布局作为例子考虑,它可能是这样的:

      

按照下面的分析,实际应该是:

C++虚拟继承中对象内存的分布_第2张图片

这种内存布局的优势在于它的开头部分(Left部分)和Left的布局正好相同,我们可以很轻易地通过一个Left指针访问一个Bottom对象。但这并不是我们讨论的重点我们主要来考虑考虑Right

1 Right* right = bottom;

这里我们应该把什么地址赋值给right指针呢?

        理论上说,通过这个赋值语句,我们可以把这个right指针当作真正指向一个Right对象的指针(现在指向的是Bottom)来使用。但实际上这是不现实的!一个真正的Right对象内存布局和Bottom对象Right部分是完全不同的,所以其实我们不可能再把这个upcasted的bottom对象当作一个真正的right对象来使用了。而且,我们这种布局的设计不可能还有改进的余地了。

这里我们先看看实际上内存是怎么分布的,然后再解释下为什么这么设计

C++虚拟继承中对象内存的分布_第3张图片
上图有两点值得大家注意。
第一点,就是类中成员分布顺序是完全不一样的(实际上可以说是正好相反)。
第二点,类中增加了vptr指针,这些是被编译器在编译过程中插入到类中的( 在设计类时如果使用了虚继承,虚函数都会产生相关vptr)。
同时,在类的构造函数中会对相关指针做初始化,这些也是编译器完成的工作
其中的  offset  to  top: int = 0和 offset to top:int = 8 分别指Left::vptr和Right::vptr在Bottom对象中,到对象起始地址的偏移量。
Vptr指针指向了一个“virtual table”,在类中每个虚基类都会存在与之对应的一个vptr指针(下面的图比较清晰的表明了问题)。
为了给大家展示virtual table作用,考虑下如下代码::

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; }

运行结果:
   
   
   
   
C++虚拟继承中对象内存的分布_第4张图片
  再提一个问题如果是:
class Bottom:virtual public Left,virtual public Right
{
  public:
    int lra;
};
那么sizeof(Bottom)占用多大内存呢?   是28个字节,又引入了vptr
   
   
   
   
C++虚拟继承中对象内存的分布_第5张图片
 

你可能感兴趣的:(C++虚拟继承中对象内存的分布)