您的位置:首页 > 编程语言 > C语言/C++

C++虚拟继承__对象内存的分布_虚继承会多余分配虚表v-tab的指

2013-05-21 14:38 537 查看


C++虚拟继承__对象内存的分布_虚继承会多余分配虚表v-tab的指针vptr_图1-1清楚的描述了虚继承类对象内存的分布__转载(淘宝共享数据平台和陈皓博客)

2012-05-27 17:11:44| 分类: C/C++开发|字号 订阅

更详细的例子,可以参看:

/article/2551888.html

虚拟继承

为了避免上述Top类的多次继承,我们必须虚拟继承类Top(Top类成为虚基类,当在多条继承路径上有一个公共的基类,在这些路径中的某几条汇合处,这个公共的基类就会产生多个实例(或多个副本),若只想保存这个基类的一个实例,可以将这个公共基类说明为虚基类

虚基类的主要作用: 虚基类主要解决多重继承时,基类被多次继承的问题(即:使用virtual方式继承,为了保证继承后父类的内存布局只会存在一份) 。

参考博客 多重继承_内存的分布:: 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对象来使用了。而且,我们这种布局的设计不可能还有改进的余地了。

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



上图有两点值得大家注意。
第一点,就是类中成员分布顺序是完全不一样的(实际上可以说是正好相反)。
第二点,类中增加了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;
}

运行结果:



再提一个问题如果是:

class Bottom:virtual public Left,virtual public Right
{
public:
int lra;
};

那么sizeof(Bottom)占用多大内存呢?   是28个字节,又引入了vptr。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: