Chinaunix首页 | 论坛 | 博客
  • 博客访问: 156777
  • 博文数量: 53
  • 博客积分: 2059
  • 博客等级: 大尉
  • 技术积分: 490
  • 用 户 组: 普通用户
  • 注册时间: 2009-09-14 20:25
文章分类
文章存档

2012年(1)

2011年(25)

2010年(16)

2009年(11)

我的朋友

分类: LINUX

2011-09-13 14:58:37

多重继承和虚继承的内存布局

刺猬@http://blog.csdn.net/littlehedgehog

 

 

 

    这篇文章主要讲解虚继承的C 对象内存分布问题,从中也引出了dynamic_cast和static_cast本质区别、虚函数表的格式等一些大部分C 程序员都似是而非的概念。原文见(By Edsko de Vries, January 2006)

 

 

 

      敬告本文是介绍C 的技术文章,假定读者对于C 有比较深入的认识,同时也需要一些汇编知识。

   本文我们将阐释GCC编译器针对多重继承和虚拟继承下的对象内存布局。尽管在理想的使用环境中,一个C 程序员并不需要了解这些编译器内部实现细节,实际上,编译器针对多重继承(特别是虚拟继承)的各种实现细节对于我们编写C 代码都或多或少产生一些影响(比如downcasting pointerpointers 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访问) 。

    那么LeftRightBottom在内存中如何分布的呢?我们先来看看简单的LeftRight内存分布:

       [Right 类的布局和Left是一样的,因此我这里就没再画图了。刺猬]

       注意到上面类各自的第一个属性都是继承自Top类,这就意味着下面两个赋值语句:

1 Left* left = new Left();
2 Top* top = left;

       lefttop实际上是指向两个相同的地址,我们可以把Left对象当作一个Top对象(同样也可以把Right对象当Top对象来使用)。但是Botom对象呢?GCC是这样处理的:

     但是现在如果我们upcast 一个Bottom指针将会有什么结果 

1 Bottom* bottom = new Bottom();
2 Left* left = bottom;
 

       这段代码运行正确。这是因为GCC选择的这种内存布局使得我们可以把Bottom对象当作Left对象,它们两者(Left部分)正好相同。但是,如果我们把Bottom对象指针upcastRight对象呢?

1 Right* right = bottom;

      如果我们要使这段代码正常工作的话,我们需要调整指针指向Bottom中相应的部分。

     通过调整,我们可以用right指针访问Bottom对象,这时Bottom对象表现得就如Right对象。但是bottomright指针指向了不同的内存地址。最后,我们考虑下:

1 Top* top = bottom;

     恩,什么结果也没有,这条语句实际上是有歧义(ambiguous)的,编译器会报错: error: `Top' is an ambiguous base of `Bottom'。其实这两种带有歧义的可能性可以用如下语句加以区分:

1 Top* topL = (Left*) bottom;
2 Top* topR = (Right*) bottom;
 
 

  这两个赋值语句执行之后,topLleft指针将指向同一个地址,同样topRright也将指向同一个地址

 

 

虚拟继承

   为了避免上述Top类的多次继承,我们必须虚拟继承类Top

 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 

   上述代码将产生如下的类层次图(其实这可能正好是你最开始想要的继承方式)

virtualinheritance

 

     对于程序员来说,这种类层次图显得更加简单和清晰,不过对于一个编译器来说,这就复杂得多了。我们再用Bottom的内存布局作为例子考虑,它可能是这样的:

       

      这种内存布局的优势在于它的开头部分(Left部分)Left的布局正好相同,我们可以很轻易地通过一个Left指针访问一个Bottom对象。不过,我们再来考虑考虑Right:

1 Right* right = bottom;

  这里我们应该把什么地址赋值给right指针呢?理论上说,通过这个赋值语句,我们可以把这个right指针当作真正指向一个Right对象的指针(现在指向的是Bottom)来使用。但实际上这是不现实的!一个真正的Right对象内存布局和Bottom对象Right部分是完全不同的,所以其实我们不可能再把这个upcastedbottom对象当作一个真正的right对象来使用了。而且,我们这种布局的设计不可能还有改进的余地了。这里我们先看看实际上内存是怎么分布的,然后再解释下为什么这么设计。

vtable

      上图有两点值得大家注意。第一点就是类中成员分布顺序是完全不一样的(实际上可以说是正好相反)。第二点,类中增加了vptr指针,这些是被编译器在编译过程中插入到类中的(在设计类时如果使用了虚继承,虚函数都会产生相关vptr)。同时,在类的构造函数中会对相关指针做初始化,这些也是编译器完成的工作。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 offset20,如果我们假设Bottom中每个成员都是4字节大小,那么Left指针加上20字节正好是成员a的地址。

 

    我们同样可以用相同的方式访问BottomRight部分。

1 Bottom* bottom = new Bottom();
2 Right* right = bottom;
3 int p = right->a;

   right指针就会指向在Bottom对象中相应的位置。

 

      这里对于p的赋值语句最终会被编译成和上述left相同的方式访问a。唯一的不同是就是vptr,我们访问的vptr现在指向了virtual table另一个地址,我们得到的virtual base offset也变为12。我们画图总结下:

 

virtualinheritance

   当然,关键点在于我们希望能够让访问一个真正单独的Right对象也如同访问一个经过upcasted(到Right对象)的Bottom对象一样。这里我们也在Right对象中引入vptrs

vtable2

    OK,现在这样的设计终于让我们可以通过一个Right指针访问Bottom对象了。不过,需要提醒的是以上设计需要承担一个相当大的代价:我们需要引入虚函数表,对象底层也必须扩展以支持一个或多个虚函数指针,原来一个简单的成员访问现在需要通过虚函数表两次间接寻址(编译器优化可以在一定程度上减轻性能损失)

 

Downcasting

   如我们猜想,将一个指针从一个派生类到一个基类的转换(casting)会涉及到在指针上添加偏移量。可能有朋友猜想,downcasting一个指针仅仅减去一些偏移量就行了吧。实际上,非虚继承情况下确实是这样,但是,对于虚继承来说,又不得不引入其它的复杂问题。这里我们在上面的例子中添加一些继承关系:

1 class AnotherBottom : public Left, public Right
2 {
3     public:
4         int e;
5         int f;
6 };

 

   这个继承关系如下图所示:

virtual2

   那么现在考虑如下代码

1 Bottom* bottom1 = new Bottom();
2 AnotherBottom* bottom2 = new AnotherBottom();
3 Top* top1 = bottom1;
4 Top* top2 = bottom2;
5 Left* left = static_cast(top1);

   下面这图展示了BottomAnotherBottom的内存布局,同时也展示了各自top指针所指向的位置。

      现在我们来考虑考虑从top1leftstatic_cast,注意这里我们并不清楚对于top1指针指向的对象是Bottom还是AnotherBottom。这里是根本不能编译通过的!因为根本不能确认top1运行时需要调整的偏移量(对于Bottom20,对于AnotherBottom24)。所以编译器将会提出错误: error: cannot convert from base `Top' to derived type `Left' via virtual base `Top'。这里我们需要知道运行时信息,所以我们需要使用dynamic_cast:

1 Left* left = dynamic_cast(top1);

    不过,编译器仍然会报错的 error: cannot dynamic_cast `top' (of type `class Top*') to type `class Left*' (source type is not polymorphic)。关键问题在于使用dynamic_cast(和使用typeid一样)需要知道指针所指对象的运行时信息。但是,回头看看上面的结构图,我们就会发现top1指针所指的仅仅是一个整数成员a。编译器没有在Bottom类中包含针对topvptr,它认为这完全没有必要。为了强制编译器在Bottom中包含topvptr,我们可以在top类里面添加一个虚析构函数。

1 class Top
2 {
3     public:
4         virtual ~Top() {} 
5         int a;
6 };

 

    这就迫使编译器为Top类添加了一个vptr。下面来看看Bottom新的内存布局:

   是的,其它派生类(LeftRight)都会添加一个vptr.top,编译器为dynamic_cast生成了一个库函数调用。

1 left = __dynamic_cast(top1, typeinfo_for_Top, typeinfo_for_Left, -1);

   __dynamic_cast定义在libstdc (对应的头文件是cxxabi.h),有了TopLeftBottom的类型信息,转换得以执行。其中,参数-1代表的是类Left和类Top之间的关系未明。如果想详细了解,请参看的实现。

 

 

 

总结

    最后,我们再聊聊一些相关内容。

   

二级指针

阅读(413) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~