zoukankan      html  css  js  c++  java
  • 多重继承及虚继承中对象内存的分布

    http://www.uml.org.cn/c++/201305163.asp

    个人总结:多重继承时,对象中保存多个虚函数表指针,

    虚拟继承时,对象中保存多个虚函数表指针,但被虚拟继承的基类对象在被继承的对象中只有一份,这个是靠共享其内容实现的。且共享的内容在子类对象的最后。

    虚拟继承

    为了避免上述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

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

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

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

    1 Right* right = bottom;

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

    上图有两点值得大家注意。第一点就是类中成员分布顺序是完全不一样的(实际上可以说是正好相反)。第二点,类中增加了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。我们画图总结下:

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

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

  • 相关阅读:
    P3834 【模板】可持久化线段树 (静态主席树)
    P3834 【模板】可持久化线段树 (静态主席树)
    2019 南昌网络赛 I. Max answer
    Linux从入门到精通——系统的进程
    Linux从入门到精通——文件权限
    Linux从入门到精通——命令行使用技巧
    Linux从入门到精通——Linux系统的文件及其管理
    Linux从入门到精通——vim及管理输入输出
    Linux从入门到精通——用户管理
    thiny mission 2021 10 15
  • 原文地址:https://www.cnblogs.com/diegodu/p/4011381.html
Copyright © 2011-2022 走看看