这篇文章主要讲解虚继承的C++对象内存分布问题,从中也引出了dynamic_cast和static_cast本质区别、虚函数表的格式等一些大部分C++程序员都似是而非的概念。原文见这里(By Edsko de Vries, January 2006)
敬告: 本文是介绍C++的技术文章,假定读者对于C++有比较深入的认识,同时也需要一些汇编知识。
本文我们将阐释GCC编译器针对多重继承和虚拟继承下的对象内存布局。尽管在理想的使用环境中,一个C++程序员并不需要了解这些编译器内部实现细节,实际上,编译器针对多重继承(特别是虚拟继承)的各种实现细节对于我们编写C++代码都或多或少产生一些影响(比如downcasting pointer、pointers to pointers 以及虚基类构造函数的调用顺序)。如果你能明白多重继承是如何实现的,那么你自己就能够预见到这些影响,进而能够在你的代码中很好地应对它们。再者,如果你十分在意的代码的运行效率,正确地理解虚继承也是很有帮助的。最后嘛,这个hack的过程是很有趣的哦:)
多重继承
首先我们先来考虑一个很简单(non-virtual)的多重继承。看看下面这个C++类层次结构。
1 class Top
2 {
3 public:
4 int a;
5 };
6
7 class Left : public Top
8 {
9 public:
10 int b;
11 };
12
13 class Right : 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
用UML表述如下:
注意到Top类实际上被继承了两次,(这种机制在Eiffel中被称作repeated inheritance),这就意味着在一个bottom对象中实际上有两个a属性(attributes,可以通过bottom.Left::a和 bottom.Right::a访问) 。
那么Left、Right、Bottom在内存中如何分布的呢?我们先来看看简单的Left和Right内存分布:
[Right 类的布局和Left是一样的,因此我这里就没再画图了。刺猬]
注意到上面类各自的第一个属性都是继承自Top类,这就意味着下面两个赋值语句:
1 Left* left = new Left();
2 Top* top = left;
left和top实际上是指向两个相同的地址,我们可以把Left对象当作一个Top对象(同样也可以把Right对象当Top对象来使用)。但是Botom对象呢?GCC是这样处理的:
但是现在如果我们upcast 一个Bottom指针将会有什么结果?
1 Bottom* bottom = new Bottom();
2 Left* left = bottom;
这段代码运行正确。这是因为GCC选择的这种内存布局使得我们可以把Bottom对象当作Left对象,它们两者(Left部分)正好相同。但是,如果我们把Bottom对象指针upcast到Right对象呢?
1 Right* right = bottom;
如果我们要使这段代码正常工作的话,我们需要调整指针指向Bottom中相应的部分。