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


    这篇文章主要讲解虚继承的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 类实际上被继承了两次, ( 这种机制在 Eif fel中被称作 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 中相应的部分。

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

    1 Top* top = bottom;

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

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

      这两个赋值语句执行之后,topL left 指针将指向同一个地址,同样 topR right 也将指向同一个地址

    虚拟继承

       为了避免上述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 部分是完全不同的,所以其实我们不可能再把这个 upcasted bottom 对象当作一个真正的 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 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 对象中相应的位置。

     

          这里对于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 <Left*>(top1);

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

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

    1 Left* left = dynamic_cast <Left*>(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 类中包含针对 top vptr ,它认为这完全没有必要。为了强制编译器在 Bottom 中包含 top vptr ,我们可以在 top 类里面添加一个虚析构函数。

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

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

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

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

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

     

    总结

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

       

    二级指针

       这里的问题初看摸不着头脑,但是细细想来有些问题还是显而易见的。这里我们考虑一个问题,还是以上节的Downcasting 中的类继承结构图作为例子。

    1 Bottom* b = new  Bottom();
    2 Right* r = b;

      (在把 b 指针的值赋值给指针 r 时, b 指针将加上 8 字节,这样 r 指针才指向 Bottom 对象中 Right 部分 ) 。因此我们可以把 Bottom* 类型的值赋值给 Right* 对象。但是 Bottom** Right** 两种类型的指针之间赋值呢?

    1 Bottom** bb = &b;
    2 Right** rr = bb;

       编译器能通过这两条语句吗?实际上编译器会报错: error: invalid conversion from `Bottom**' to `Right**'
      为什么 不妨反过来想想,如果能够将 bb 赋值给 rr ,如下图所示。所以这里 bb rr 两个指针都指向了 b b r 都指向了 Bottom 对象的相应部分。那么现在考虑考虑如果给 *rr 赋值将会发生什么。

    1 *rr = b;  

      注意 *rr Right* 类型 ( 一级 ) 的指针,所以这个赋值是有效的!

    doublepointers

        这个就和我们上面给r 指针赋值一样 (*rr 是一级的 Right* 类型指针,而 r 同样是一级 Right* 指针 ) 。所以,编译器将采用相同的方式实现对 *rr 的赋值操作。实际上,我们又要调整 b 的值,加上 8 字节,然后赋值给 *rr ,但是现在 **rr 其实是指向 b ! 如下图

        呃,如果我们通过rr 访问 Bottom 对象,那么按照上图结构我们能够完成对 Bottom 对象的访问,但是如果是用 b 来访问 Bottom 对象呢,所有的对象引用实际上都偏移了 8 字节——明显是错误的!

       总而言之,尽管*a *b 之间能依靠类继承关系相互转化,而 **a **b 不能有这种推论。

    虚基类的构造函数

       编译器必须要保证所有的虚函数指针要被正确的初始化。特别是要保证类中所有虚基类的构造函数都要被调用,而且还只能调用一次。 如果你写代码时自己不显示调用构造函数,编译器会自动插入一段构造函数调用代码。这将会导致一些奇怪的结果,同样考虑下上面的类继承结构图,不过要加入构造函数。

     1 class  Top
     2 {
     3 public :
     4    Top() { a = -1 ; }
     5    Top(int  _a) { a = _a; }
     6    int  a;
     7 };
     8
     9 class  Left : public  Top
    10 {
    11 public :
    12    Left() { b = -2 ; }
    13    Left(int  _a, int  _b) : Top(_a) { b = _b; }
    14    int  b;
    15 };
    16
    17 class  Right : public  Top
    18 {
    19 public :
    20    Right() { c = -3 ; }
    21    Right(int  _a, int  _c) : Top(_a) { c = _c; }
    22    int  c;
    23 };
    24
    25 class  Bottom : public  Left, public  Right
    26 {
    27 public :
    28    Bottom() { d = -4 ; }
    29    Bottom(int  _a, int  _b, int  _c, int  _d) : Left(_a, _b), Right(_a, _c)
    30     {
    31       d = _d;
    32     }
    33    int  d;
    34 };
    35 

       先来考虑下不包含虚函数的情况,下面这段代码输出什么?

    1 Bottom bottom(1 ,2 ,3 ,4 );
    2 printf(" %d   %d   %d   %d   %d /n " , bottom.Left::a, bottom.Right::a, bottom.b, bottom.c, bottom.d);

       你可能猜想会有这样结果:

    1 1 2 3 4
       但是,如果我们考虑下包含虚函数的情况呢,如果我们从Top 虚继承派生出子类,那么我们将得到如下结果:

    -1 -1 2 3 4
       如本节开头所讲,编译器在Bottom 中插入了一个 Top 的默认构造函数,而且这个默认构造函数安排在其他的构造函数之前,当 Left 开始调用它的基类构造函数时,我们发现 Top 已经构造初始化好了,所以相应的构造函数不会被调用。如果跟踪构造函数,我们将会看到

    Top::Top()
    Left::Left(1,2)
    Right::Right(1,3)
    Bottom::Bottom(1,2,3,4)
       为了避免这种情况,我们应该显示地调用虚基类的构造函数

    1 Bottom(int  _a, int  _b, int  _c, int  _d): Top(_a), Left(_a,_b), Right(_a,_c)
    2 {
    3    d = _d;
    4 }

     

    void*  的转换

     1 dynamic_cast <void *>(b);

        最后我们来考虑下把一个指针转换到void * 。编译器会把指针调整到对象的开始地址。通过查 vtable ,这个应该是很容易实现。看看上面的 vtable 结构图,其中 offset to top 就是 vptr 到对象开始地址。另外因为要查阅 vtable ,所以需要使用 dynamic_cast

    指针的比较

       再以上面Bottom类继承关系为例讨论,下面这段代码会打印Equal吗?

    1 Bottom* b = new  Bottom();
    2 Right* r = b;
    3       
    4 if (r == b)
    5    printf("Equal! /n " );

       先明确下这两个指针实际上是指向不同地址的,r指针实际上在b指针所指地址上偏移8字节 ,但是,这些C++内部细节不能告诉C++程序员,所以C++编译器在比较r和b时,会把r减去8字节,然后再来比较,所以打印出的值是"Equal".

     

    参考文献   

    [1] CodeSourcery , in particular the C++ ABI Summary , the Itanium C++ ABI (despite the name, this document is referenced in a platform-independent context; in particular, the structure of the vtables is detailed here). The libstdc++ implementation of dynamic casts, as well RTTI and name unmangling/demangling, is defined in tinfo.cc .

    [2] The libstdc++ website, in particular the section on the C++ Standard Library API .

    [3] C++: Under the Hood by Jan Gray.

    [4] Chapter 9, “Multiple Inheritance” of Thinking in C++ (volume 2) by Bruce Eckel . The author has made this book available for download .

  • 相关阅读:
    数据可视化 —— 数据流图(Data Flow Diagram)
    TensorFlow 实战(四)—— tensor 的认识
    数据集(benchmark)、常用数据集的解析(cifar-10、)
    HDU--杭电--4504--威威猫系列故事——篮球梦--DP
    android打包apk时混淆遇到的问题
    C语言数据结构----递归的应用(斐波拉契数列、汉诺塔、strlen的递归算法)
    按 Eclipse 开发喜好重新布置 cocos2dx 目录层次
    HDU--杭电--4502--吉哥系列故事——临时工计划--背包--01背包
    (step6.3.2)hdu 1068(Girls and Boys——二分图的最大独立集)
    flashcache中应用device mapper机制
  • 原文地址:https://www.cnblogs.com/kex1n/p/2286468.html
Copyright © 2020-2023  润新知