zoukankan      html  css  js  c++  java
  • 转载:C++ 多继承和虚继承的内存布局

    C++ 多继承和虚继承的内存布局 【已翻译100%】

    run_mei 推荐于 4年前 (共 14 段, 翻译完成于 10-17) 评论 46
    收藏 
    198
     

    警告. 本文有点技术难度,需要读者了解C++和一些汇编语言知识。

    在本文中,我们解释由gcc编译器实现多继承和虚继承的对象的布局。虽然在理想的C++程序中不需要知道这些编译器内部细节,但不幸的是多重继承(特别是虚拟继承)的实现方式有各种各样的不太明确的结论(尤其是,关于向下转型指针,使用指向指针的指针,还有虚拟基类的构造方法的调用命令)。 如果你了解多重继承是如何实现的,你就能预见到这些结论并运用到你的代码中。而且,如果你关心性能,理解虚拟继承的开销也是非常有用的。最后,这很有趣。 :-)

    zaobao
    zaobao
    翻译于 4年前
    1人顶
     翻译得不错哦!
     

    多重继承

    首先我们考虑一个(非虚拟)多重继承的相对简单的例子。看看下面的C++类层次结构。

    class Top
    { public: int a;
    }; class Left : public Top
    { public: int b;
    }; class Right : public Top
    { public: int c;
    }; class Bottom : public Left, public Right
    { public: int d;
    };
    使用UML图,我们可以把这个层次结构表示为:

    注意Top被继承了两次(在Eiffel语言中这被称作重复继承)。这意味着类型Bottom的一个实例bottom将有两个叫做a的元素(分别为bottom.Left::a和bottom.Right::a)。

    Ley
    Ley
    翻译于 4年前
    1人顶
     翻译得不错哦!
     

    Left、Right和Bottom在内存中是如何布局的?让我们先看一个简单的例子。Left和Right拥有如下的结构:

    Left
    Top::a
    Left::b
       Right
        Top::a
        Right::c

    请注意第一个属性是从Top继承下来的。这意味着在下面两条语句后

    Left* left = new Left();
    Top* top = left;
    left和top指向了同一地址,我们可以把Left Object当成Top Object来使用(很明显,Right与此也类似)。那Buttom呢?GCC的建议如下:
    Bottom        
    Left::Top::a
    Left::b
    Right::Top::a
    Right::c
    Bottom::d

    如果我们提升Bottom指针,会发生什么事呢?
    Bottom* bottom = new Bottom();
    Left* left = bottom;
    

    这段代码工作正常。我们可以把一个Bottom的对象当作一个Left对象来使用,因为两个类的内存部局是一样的。那么,如果将其提升为Right呢?会发生什么事?

    Right* right = bottom;
    为了执行这条语句,我们需要判断指针的值以便让它指向Bottom中对应的段。
      Bottom
      Left::Top::a
      Left::b
    rightpoints to  Right::Top::a
      Right::c
      Bottom::d

    经过这一步,我们可以像操作正常Right对象一样使用right指针访问bottom。虽然,bottom与right现在指向两个不同的内存地址。出于完整性的缘故,思考一下执行下面这条语句时会出现什么状况。
    Top* top = bottom;
    是的,什么也没有。这条语句是有歧义的:编译器将会报错。
    error: `Top' is an ambiguous base of `Bottom'
    两种方式可以避免这样的歧义
    Top* topL = (Left*) bottom;
    Top* topR = (Right*) bottom;
    执行这两条语句后,topL和left会指向同样的地址,topR和right也会指向同样的地址。
    开源中国吹牛第一
    开源中国吹牛第一
    翻译于 4年前
    4人顶
     翻译得不错哦!
     

    虚拟继承

    为了避免重复继承Top,我们必须虚拟继承Top:

    class Top
    { public: int a;
    }; class Left : virtual public Top
    { public: int b;
    }; class Right : virtual public Top
    { public: int c;
    }; class Bottom : public Left, public Right
    { public: int d;
    };
    这就得到了如下的层次结构(也许是你一开始就想得到的)

    虽然从程序员的角度看,这也许更加的明显和简便,但从编译器的角度看,这就变得非常的复杂。重新考虑下Bottom的布局,其中的一个(也许没有)可能是:

    Bottom

    Left::Top::a

    Left::b

    Right::c

    Bottom::d

    Ley
    Ley
    翻译于 4年前
    2人顶
     翻译得不错哦!
     

    这个布局的优点是,布局的第一部分与Left的布局重叠了,这样我们就可以很容易的通过一个Left指针访问 Bottom类。可是我们怎么处理

    Right* right = bottom;
    

    我们将哪个地址赋给right呢? 经过这个赋值,如果right是指向一个普通的Right对象,我们应该就能使用 right了。但是这是不可能的!Right本身的内存布局是完全不同的,这样我们就无法像访问一个"真正的"Right对象一样,来访问升级的Bottom对象。而且,也没有其它(简单的)可以正常运作的Bottom布局。

    解决办法是复杂的。我们先给出解决方案,之后再来解释它。

    layout of Bottom

    你应该注意到了这个图中的两个地方。第一,字段的顺序是完全不同的(事实上,差不多是相反的)。第二,有几个vptr指针。这些属性是由编译器根据需要自动插入的(使用虚拟继承,或者使用虚拟函数的时候)。编译器也在构造器中插入了代码,来初始化这些指针。

    super0555
    super0555
    翻译于 4年前
    1人顶
     翻译得不错哦!
     

    vptr (virtual pointers)指向一个 “虚拟表”。类的每个虚拟基类都有一个vptr指针。要想知道这个虚拟表 (vtable)是怎样运用的,看看下面的C++ 代码。

    Bottom* bottom = new Bottom();
    Left* left = bottom; int p = left->a;
    

    第二个赋值使left指向了bottom的所在地址(即,它指向了Bottom对象的“顶部”)。我们想想最后一条赋值语句的编译情况(稍微简化了):

    movl left, %eax # %eax = left 
    movl (%eax), %eax # %eax = left.vptr.Left 
    movl (%eax), %eax # %eax = virtual base offset 
    addl left, %eax # %eax = left + virtual base offset 
    movl (%eax), %eax # %eax = left.a 
    movl %eax, p # p = left.a 

    用语言来描述的话,就是我们用left指向虚拟表,并且由它获得了“虚拟基类偏移”(vbase)。这个偏移之后就加到了left,然后left就用来指向Bottom对象的Top部分。从这张图你可以看到Left的虚拟基类偏移是20;如果假设Bottom中的所有字段都是4个字节,那么给left加上20字节将会确实指向a字段。

    super0555
    super0555
    翻译于 4年前
    1人顶
     翻译得不错哦!
     

    经过这个设置,我们就可以同样的方法访问Right部分。按这样

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

    之后right将指向Bottom对象的合适的部位:

      Bottom
      vptr.Left
      Left::b
    rightpoints to  vptr.Right
      Right::c
      Bottom::d
      Top::a

    对top的赋值现在可以编译成像前面Left同样的方式。唯一的不同就是现在的vptr是指向了虚拟表的不同部位:取得的虚拟表偏移是12,这完全正确(确定!)。我们可以将其图示概括:virtual table

    当然,这个例子的目的就是要像访问真正Right对象一样访问升级的Bottom对象。因此,我们必须也要给Right(和Left)布局引入vptrs:

    layout of Left and Right

    现在我们就可以通过一个Right指针,一点也不费事的访问Bottom对象了。不过,这是付出了相当大的代价:我们要引入虚拟表,类需要扩展一个或更多个虚拟指针,对一个对象的一个简单属性的查询现在需要两次间接的通过虚拟表(即使编译器某种程度上可以减小这个代价)。

    super0555
    super0555
    翻译于 4年前
    1人顶
     翻译得不错哦!
     

    向下转换

    如我们所见,将一个派生类的指针转换为一个父类的指针(或者说,向上转换)可能涉及到给指针增添一个偏移。有人可能会想了,这样向下转换(反方向的)就可以简单的通过减去同样的偏移来实现。确实,对非虚拟继承来说是这样的。可是,虚拟继承(毫不奇怪的!)带来了另一种复杂性。

    假设我们像下面这个类这样扩展继承层次。
    class AnotherBottom : public Left, public Right
    { public: int e; int f;
    };

    继承层次现在看起来是这样

    class hierarchy

    现在考虑一下下面的代码。

    Bottom* bottom1 = new Bottom();
    AnotherBottom* bottom2 = new AnotherBottom();
    Top* top1 = bottom1;
    Top* top2 = bottom2;
    Left* left = static_cast<Left*>(top1);

    下图显示了Bottom和AnotherBottom的布局,而且在最后一个赋值后面显示了指向top的指针。

      Bottom
      vptr.Left
      Left::b
      vptr.Right
      Right::c
      Bottom::d
    top1points to  Top::a
      AnotherBottom
      vptr.Left
      Left::b
      vptr.Right
      Right::c
      AnotherBottom::e
      AnotherBottom::f
    top2points to  Top::a
    super0555
    super0555
    翻译于 4年前
    3人顶
     翻译得不错哦!
     

    现在考虑一下怎么去实现从top1到left的静态转换,同时要想到,我们并不知道top1是否指向一个Bottom类型的对象,或者是指向一个AnotherBottom类型的对象。所以这办不到!这个重要的偏移依赖于top1运行时的类型(Bottom则20,AnotherBottom则24)。编译器将报错:

    error: cannot convert from base `Top' to derived type `Left' 
    via virtual base `Top'
    

    因为我们需要运行时的信息,所以应该用一个动态转换来替代实现:

    Left* left = dynamic_cast<Left*>(top1);
    

    可是,编译器仍然不满意:

    error: cannot dynamic_cast `top' (of type `class Top*') to type 
       `class Left*' (source type is not polymorphic)
    

    (注:polymorphic多态的)

    问题在于,动态转换(转换中使用到typeid)需要top1所指向对象的运行时类型信息。但是,如果你看看这张图,你就会发现,在top1指向的位置,我们仅仅只有一个integer (a)而已。编译器没有包含指向Top的虚拟指针,因为它不认为这是必需的。为了强制编译器包含进这个vptr指针,我们可以给Top增加一个虚拟的析构器:

    class Top
    { public: virtual ~Top() {} int a;
    };
    

    这个修改需要指向Top的vptr指针。Bottom的新布局是

    layout of Bottom

    (当然类似的其它类也有一个新的指向Top的vptr指针)。现在编译器为动态转换插进了一个库调用:

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

    这个函数__dynamic_cast定义在stdc++库中(相应的头文件是cxxabi.h);参数为Top的类型信息,Left和Bottom(通过vptr.Top),这个转换可以执行。 (参数 -1 标示出Left和Top之间的关系现在还是未知)。更多详细资料,请参考tinfo.cc 的具体实现 。

    super0555
    super0555
    翻译于 4年前
    1人顶
     翻译得不错哦!
     

    总结语

    最后,我们来看看一些没了结的部分。

    指针的指针

    这里出现了一点令人迷惑的问题,但是如果你仔细思考下一的话它其实很简单。我们来看一个例子。假设使用上一节用到的类层次结构(向下类型转换).在前面的小节我们已经看到了它的结果:

    Bottom* b = new Bottom();
    Right* r = b;
    (在将b的值赋给r之前,需要将它调整8个字节,从而让它指向Bottom对象的Right部分).因此,我们可以合法地将一个Bottom* 赋值给一个Right*的指针。但是Bottom**和Right**又会怎样呢? 
    Bottom** bb = &b;
    Right** rr = bb;

    编译器会接受这样的形式吗?我们快速测试一下,编译器会报错:

    error: invalid conversion from `Bottom**' to `Right**'
    为什么呢?假设编译器可以接受从bb到rr的赋值。我们可以只管的看到结果如下: 
    double pointers

    因此,bb和rr都指向b,并且b和r指向Bottom对象的正确的章节。现在考虑当我们赋值给*rr时会发生什么(注意*rr的类型时Right*,因此这个赋值是有效的):

    *rr = b;	
    polarisxxm
    polarisxxm
    翻译于 4年前
    1人顶
     翻译得不错哦!
     
  • 相关阅读:
    LeetCode Missing Number (简单题)
    LeetCode Valid Anagram (简单题)
    LeetCode Single Number III (xor)
    LeetCode Best Time to Buy and Sell Stock II (简单题)
    LeetCode Move Zeroes (简单题)
    LeetCode Add Digits (规律题)
    DependencyProperty深入浅出
    SQL Server存储机制二
    WPF自定义RoutedEvent事件示例代码
    ViewModel命令ICommand对象定义
  • 原文地址:https://www.cnblogs.com/sanghai/p/6728723.html
Copyright © 2011-2022 走看看