C++ 多继承和虚继承的内存布局(转)
2018-03-18 19:55
197 查看
转自:http://www.oschina.net/translate/cpp-virtual-inheritance
警告. 本文有点技术难度,需要读者了解C++和一些汇编语言知识。在本文中,我们解释由gcc编译器实现多继承和虚继承的对象的布局。虽然在理想的C++程序中不需要知道这些编译器内部细节,但不幸的是多重继承(特别是虚拟继承)的实现方式有各种各样的不太明确的结论(尤其是,关于向下转型指针,使用指向指针的指针,还有虚拟基类的构造方法的调用命令)。 如果你了解多重继承是如何实现的,你就能预见到这些结论并运用到你的代码中。而且,如果你关心性能,理解虚拟继承的开销也是非常有用的。最后,这很有趣。 :-)
警告. 本文有点技术难度,需要读者了解C++和一些汇编语言知识。在本文中,我们解释由gcc编译器实现多继承和虚继承的对象的布局。虽然在理想的C++程序中不需要知道这些编译器内部细节,但不幸的是多重继承(特别是虚拟继承)的实现方式有各种各样的不太明确的结论(尤其是,关于向下转型指针,使用指向指针的指针,还有虚拟基类的构造方法的调用命令)。 如果你了解多重继承是如何实现的,你就能预见到这些结论并运用到你的代码中。而且,如果你关心性能,理解虚拟继承的开销也是非常有用的。最后,这很有趣。 :-)
多重继承首先我们考虑一个(非虚拟)多重继承的相对简单的例子。看看下面的C++类层次结构。
注意Top被继承了两次(在Eiffel语言中这被称作重复继承)。这意味着类型Bottom的一个实例bottom将有两个叫做a的元素(分别为bottom.Left::a和bottom.Right::a)。 |
Left、Right和Bottom在内存中是如何布局的?让我们先看一个简单的例子。Left和Right拥有如下的结构:
|
虚拟继承为了避免重复继承Top,我们必须虚拟继承Top:虽然从程序员的角度看,这也许更加的明显和简便,但从编译器的角度看,这就变得非常的复杂。重新考虑下Bottom的布局,其中的一个(也许没有)可能是:BottomLeft::Top::aLeft::bRight::cBottom::d |
这个布局的优点是,布局的第一部分与Left的布局重叠了,这样我们就可以很容易的通过一个Left指针访问 Bottom类。可是我们怎么处理 你应该注意到了这个图中的两个地方。第一,字段的顺序是完全不同的(事实上,差不多是相反的)。第二,有几个vptr指针。这些属性是由编译器根据需要自动插入的(使用虚拟继承,或者使用虚拟函数的时候)。编译器也在构造器中插入了代码,来初始化这些指针。 |
经过这个设置,我们就可以同样的方法访问Right部分。按这样
当然,这个例子的目的就是要像访问真正Right对象一样访问升级的Bottom对象。因此,我们必须也要给Right(和Left)布局引入vptrs: 现在我们就可以通过一个Right指针,一点也不费事的访问Bottom对象了。不过,这是付出了相当大的代价:我们要引入虚拟表,类需要扩展一个或更多个虚拟指针,对一个对象的一个简单属性的查询现在需要两次间接的通过虚拟表(即使编译器某种程度上可以减小这个代价)。 |
向下转换如我们所见,将一个派生类的指针转换为一个父类的指针(或者说,向上转换)可能涉及到给指针增添一个偏移。有人可能会想了,这样向下转换(反方向的)就可以简单的通过减去同样的偏移来实现。确实,对非虚拟继承来说是这样的。可是,虚拟继承(毫不奇怪的!)带来了另一种复杂性。假设我们像下面这个类这样扩展继承层次。现在考虑一下下面的代码。
|
现在考虑一下怎么去实现从top1到left的静态转换,同时要想到,我们并不知道top1是否指向一个Bottom类型的对象,或者是指向一个AnotherBottom类型的对象。所以这办不到!这个重要的偏移依赖于top1运行时的类型(Bottom则20,AnotherBottom则24)。编译器将报错: (当然类似的其它类也有一个新的指向Top的vptr指针)。现在编译器为动态转换插进了一个库调用: |
总结语最后,我们来看看一些没了结的部分。指针的指针这里出现了一点令人迷惑的问题,但是如果你仔细思考下一的话它其实很简单。我们来看一个例子。假设使用上一节用到的类层次结构(向下类型转换).在前面的小节我们已经看到了它的结果: |
这样的赋值和上面的赋值给r在根本上是一致的。因此,编译器会用同样的方式实现它!特别地,它会在赋值给*rr之前将b的值调整8个字节。办事*rr指向的是b!我们再一次图示化这个结果: 只要我们通过*rr来访问Bottom对象这都是正确的,但是只要我们通过b自身来访问它,所有的内存引用都会有8个字节的偏移---明显这是个不理想的情况。因此,总的来说,及时*a 和*b通过一些子类型相关,**aa和**bb却是不相关的。 |
虚拟基类的构造函数编译器必须确保对象的所有虚指针都被正确的初始化。特别是,编译器确保了类的所有虚基类都被调用,并且只被调用一次。如果你不显示地调用虚拟超类(不管他们在继承层次结构中的距离有多远),编译器都会自动地插入调用他们缺省构造函数。这样也会引来一些不可以预期的错误。以上面给出的类层次结构作为示例,并添加上构造函数的部分: |
指针等价再假设同样的(虚拟)类继承等级,你希望这样就打印“相等”吗? |
转换为void类型的指针最后,我们来思考一下当将一个对象转换为void类型的指针时会发生什么事情。编译器必须保证一个指针转换为void类型的指针时指向对象的顶部。使用虚函数表这很容易实现。你可能已经想到了指向top域的偏移量是什么。它是虚函数指针到对象顶部的偏移量。因此,转化为void类型的指针操作可以使用查询虚函数表的方式来实现。然而一定要确保使用动态类型转换,如下:dynamic_cast<void*>(b);参考文献[1] CodeSourcery, 特别是C++ ABI Summary, Itanium C++ ABI(不考虑名字,这些文档是在平台无关的上下文中引用的;特别低,structure of the vtables给出了虚函数表的详细信息)。libstdc++实现的动态类型转化,和同RTTI和命名调整定义在 tinfo.cc中。[2]libstdc++ 网站,特别是 C++ Standard Library API这一章节。[3]Jan Gray 写的C++: Under the Hood [4]Bruce Eckel的Thinking in C++(第二卷) 第9章"多重继承"。 作者允许下载这本书download. |
相关文章推荐
- C++对象内存布局--⑨VS编译器--虚拟继承--菱形继承
- c++继承中的内存布局
- C++ 对象的内存布局—— 虚继承下的虚函数
- c++继承中的内存布局 .
- C++继承 派生类中的内存布局(单继承、多继承、虚拟继承)
- c++继承中的内存布局(转)
- c++单继承、多继承、菱形继承的内存布局(虚函数表结构)
- C++对象的内存布局---单继承
- C++继承中的内存布局
- C++对象内存布局--⑩GCC编译器--虚拟继承--菱形继承
- c++继承中的内存布局
- c++继承中的内存布局
- c++继承中的内存布局
- c++继承中的内存布局【转】
- c++虚继承对象的内存布局(修改版)
- c++继承中的内存布局(转)
- C++ 多继承和虚继承的内存布局
- c++继承中的内存布局(一)
- c++涉及继承和虚继承时的内存布局
- 【C++】c++单继承、多继承、菱形继承内存布局(虚函数表结构)