C++ 多继承和虚继承的内存布局
2016-01-27 12:23
537 查看
在本文中,我们解释由gcc编译器实现多继承和虚继承的对象的布局。虽然在理想的C++程序中不需要知道这些编译器内部细节,但不幸的是多重继承(特别是虚拟继承)的实现方式有各种各样的不太明确的结论(尤其是,关于向下转型指针,使用指向指针的指针,还有虚拟基类的构造方法的调用命令)。 如果你了解多重继承是如何实现的,你就能预见到这些结论并运用到你的代码中。而且,如果你关心性能,理解虚拟继承的开销也是非常有用的。最后,这很有趣。 :-) | zaobao 翻译于 2年前 0人顶 顶 翻译的不错哦! |
首先我们考虑一个(非虚拟)多重继承的相对简单的例子。看看下面的C++类层次结构。 |
注意Top被继承了两次(在Eiffel语言中这被称作重复继承)。这意味着类型Bottom的一个实例bottom将有两个叫做a的元素(分别为bottom.Left::a和bottom.Right::a)。
Ley
翻译于 2年前
0人顶
顶 翻译的不错哦!
Left、Right和Bottom在内存中是如何布局的?让我们先看一个简单的例子。Left和Right拥有如下的结构:
?
? ?
? ? ? | Aaron_Lim 翻译于 2年前 2人顶 顶 翻译的不错哦! |
为了避免重复继承Top,我们必须虚拟继承Top: |
虽然从程序员的角度看,这也许更加的明显和简便,但从编译器的角度看,这就变得非常的复杂。重新考虑下Bottom的布局,其中的一个(也许没有)可能是:
Bottom
Left::Top::a
Left::b
Right::c
Bottom::d
Ley
翻译于 2年前
1人顶
顶 翻译的不错哦!
这个布局的优点是,布局的第一部分与Left的布局重叠了,这样我们就可以很容易的通过一个Left指针访问 Bottom类。可是我们怎么处理 ? 解决办法是复杂的。我们先给出解决方案,之后再来解释它。 你应该注意到了这个图中的两个地方。第一,字段的顺序是完全不同的(事实上,差不多是相反的)。第二,有几个vptr指针。这些属性是由编译器根据需要自动插入的(使用虚拟继承,或者使用虚拟函数的时候)。编译器也在构造器中插入了代码,来初始化这些指针。 | super0555 翻译于 2年前 0人顶 顶 翻译的不错哦! |
vptr (virtual pointers)指向一个 “虚拟表”。类的每个虚拟基类都有一个vptr指针。要想知道这个虚拟表 (vtable)是怎样运用的,看看下面的C++ 代码。 ? ? | super0555 翻译于 2年前 0人顶 顶 翻译的不错哦! |
经过这个设置,我们就可以同样的方法访问Right部分。按这样 ?
当然,这个例子的目的就是要像访问真正Right对象一样访问升级的Bottom对象。因此,我们必须也要给Right(和Left)布局引入vptrs: 现在我们就可以通过一个Right指针,一点也不费事的访问Bottom对象了。不过,这是付出了相当大的代价:我们要引入虚拟表,类需要扩展一个或更多个虚拟指针,对一个对象的一个简单属性的查询现在需要两次间接的通过虚拟表(即使编译器某种程度上可以减小这个代价)。 | super0555 翻译于 2年前 0人顶 顶 翻译的不错哦! |
如我们所见,将一个派生类的指针转换为一个父类的指针(或者说,向上转换)可能涉及到给指针增添一个偏移。有人可能会想了,这样向下转换(反方向的)就可以简单的通过减去同样的偏移来实现。确实,对非虚拟继承来说是这样的。可是,虚拟继承(毫不奇怪的!)带来了另一种复杂性。 |
现在考虑一下下面的代码。
?
|
|
super0555
翻译于 2年前
1人顶
顶 翻译的不错哦!
现在考虑一下怎么去实现从top1到left的静态转换,同时要想到,我们并不知道top1是否指向一个Bottom类型的对象,或者是指向一个AnotherBottom类型的对象。所以这办不到!这个重要的偏移依赖于top1运行时的类型(Bottom则20,AnotherBottom则24)。编译器将报错: ? ? ? 问题在于,动态转换(转换中使用到typeid)需要top1所指向对象的运行时类型信息。但是,如果你看看这张图,你就会发现,在top1指向的位置,我们仅仅只有一个integer (a)而已。编译器没有包含指向Top的虚拟指针,因为它不认为这是必需的。为了强制编译器包含进这个vptr指针,我们可以给Top增加一个虚拟的析构器: ? (当然类似的其它类也有一个新的指向Top的vptr指针)。现在编译器为动态转换插进了一个库调用: ? 。 | super0555 翻译于 2年前 0人顶 顶 翻译的不错哦! |
最后,我们来看看一些没了结的部分。 |
?
?
?
?
polarisxxm
翻译于 2年前
0人顶
顶 翻译的不错哦!
这样的赋值和上面的赋值给r在根本上是一致的。因此,编译器会用同样的方式实现它!特别地,它会在赋值给*rr之前将b的值调整8个字节。办事*rr指向的是b!我们再一次图示化这个结果: 只要我们通过*rr来访问Bottom对象这都是正确的,但是只要我们通过b自身来访问它,所有的内存引用都会有8个字节的偏移---明显这是个不理想的情况。 因此,总的来说,及时*a 和*b通过一些子类型相关,**aa和**bb却是不相关的。 | polarisxxm 翻译于 2年前 0人顶 顶 翻译的不错哦! |
编译器必须确保对象的所有虚指针都被正确的初始化。特别是,编译器确保了类的所有虚基类都被调用,并且只被调用一次。如果你不显示地调用虚拟超类(不管他们在继承层次结构中的距离有多远),编译器都会自动地插入调用他们缺省构造函数。 |
?
?
?
?
为了避免这种情况,你应该显示的调用虚基类的构造函数:
?
polarisxxm
翻译于 2年前
1人顶
顶 翻译的不错哦!
再假设同样的(虚拟)类继承等级,你希望这样就打印“相等”吗? |
super0555
翻译于 2年前
0人顶
顶 翻译的不错哦!
最后,我们来思考一下当将一个对象转换为void类型的指针时会发生什么事情。编译器必须保证一个指针转换为void类型的指针时指向对象的顶部。使用虚函数表这很容易实现。你可能已经想到了指向top域的偏移量是什么。它是虚函数指针到对象顶部的偏移量。因此,转化为void类型的指针操作可以使用查询虚函数表的方式来实现。然而一定要确保使用动态类型转换,如下: |
相关文章推荐
- Google C++ style guide——命名约定
- C++虚函数和虚继承浅析
- C++【常见面试题】String类的实现,以及深拷贝、浅拷贝问题
- USACO:Factorials
- 0723-0802 C语言笔记(李明杰前8天)
- C++【String类】String头插单个字符,头删单个字符的函数实现
- C++调用C#dll类库中的方法(非显性COM)
- C语言中常见排序算法汇总
- C++【String类】String插入单个字符,插入字符串的函数实现
- c++中优化内存分配:new/delete操作符;allocator类对象的使用;operator new/operator delete函数及定位new表达式
- C++【String类】String删除单个字符,删除字符串的函数实现
- C++【String类】String查找单个字符,查找字符串的函数实现
- C/C++宏定义中#与##区别
- C语言 二维数组复制、清零及打印显示
- C语言char s[] 和 char *s的差别
- 深度学习(七)caffe源码c++学习笔记
- C语言 动态创建二维数组
- [土狗之路]coursera C语言基础12周(期末考试)作业
- C语言实现对bmp格式图片打码
- C++中的容器类详解