C++虚拟继承中对象内存的分布
来源:互联网 发布:明道软件怎么样 编辑:程序博客网 时间:2024/06/05 16:37
虚拟继承
为了避免上述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。
- C++虚拟继承中对象内存的分布
- 多重继承及虚继承中对象内存的分布
- 多重继承及虚继承中对象内存的分布
- 多重继承及虚继承中对象内存的分布
- 多重继承及虚继承中对象内存的分布
- 多重继承及虚继承中对象内存的分布
- 多重继承及虚继承中对象内存的分布
- 【c++】深入剖析虚拟继承与各种继承关系中派生类内成员内存分布情况及虚基类表的内容
- 虚继承下对象的内存分布
- C++虚拟继承__对象内存的分布_虚继承会多余分配虚表v-tab的指
- [经验总结]多重继承及虚继承中对象内存的分布(转载)
- 【转载】多重继承及虚继承中对象内存的分布
- C++中继承的内存分布
- C++中继承的内存分布
- C++中对象的内存分布
- 【C++】虚函数和虚继承的内存分布情况
- C 的内存分布
- 虚拟继承和多重继承中类对象的大小
- android 设置Button或者ImageButton的背景透明 半透明 透明
- asp.net c#中去掉最后一个字符和去掉第一个字符
- java接口
- 使用Maven管理Eclipse Java项目(多modules编译)
- 关于excel的导入
- C++虚拟继承中对象内存的分布
- 嵌入式笔试1
- surfaceflinger-files.sh
- 重要提示:微软8月份MCITP考试几乎全部变题!
- html5学习笔记3-新增表单元素
- asp.net 延迟执行
- Android开发入门之路(续篇)
- 使用SQLIO评估数据库磁盘性能
- ios培训之IOS NSInvocation用法