zoukankan      html  css  js  c++  java
  • 浅析GCC下C++多重继承 & 虚拟继承的对象内存布局

    继承是C++作为OOD程序设计语言的三大特征(封装,继承,多态)之一,单一非多态继承是比较好理解的,本文主要讲解GCC环境下的多重继承和虚拟继承的对象内存布局。

    一、多重继承

          先看几个类的定义:

     

    01 class Top
    02  {
    03 public:
    04       int a;
    05  };
    06  
    07  class Left : public Top
    08  {
    09 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  };

     

              不难想象,Left和Right类的内存布局如下图所示:

     

                     

              我们如下进行验证:

     

    1 Left *left = new Left();
    2 Top *top = left;
    3 cout << left <<  ' ' << top << endl;//输出:0x902c008       0x902c008
    4 Right *right = new Right();
    5 top = right;
    6 cout << right << ' ' << top << endl;//输出:0x902c018       0x902c018

     

             从输出结果可以看出,父类指针top指向子类对象left和right的起始地址,与上述内存布局吻合。

     

             在非虚拟多重继承的情况下,子类的内存布局是什么样子的呢?如下所示:

               

              可以看出,Bottom类由于继承了Left和Right,而Left和Right又分别继承了Top。因此,Bottom包含了Top两次!

              下面进行验证:

     

    1 Bottom *bottom = new Bottom();   
    1 //  top = bottom;     //error: ‘Top’ is an ambiguous base of ‘Bottom’   
    1 top = (Left *)bottom;
    1 left = bottom;
    2 cout << bottom << ' ' << top << ' ' << left << endl;//输出:0x9930028 0x9930028 0x9930028
    3 top = (Right *)bottom;
    4 right = bottom;
    5 cout << bottom << ' ' << top << ' ' << right << endl;//输出:0x9930028 0x9930030 0x9930030
             从输出结果可以看出,left指针和right指针分别指向了bottom对象中它们所处的位置: 
                  

            由于bottom对象中存在两部分top对象,因此不能直接用top指针指向bottom对象,因为编译器不知道你的意图到底是指向left中的 bottom部分,还是right中的bottom部分。需要进行转换才可以。如果需要通过bottom指针分别访问left和right中的top部 分,可以如下:  bottom->Left::a,  bottom->Right::a。

     

             好了,到这里讲完了非虚拟继承下的多重继承的内存布局情况,相信大家应该有一个比较清晰的认识了。最重要的一点是: 多重继承时,父类共同继承的祖父类会在子类中有多份存在。

     

     

    二、虚拟继承

         平时讨论的最多的是虚函数,很少涉及到虚拟继承的情况。那么,虚拟继承到底是一个什么概念呢?

          先来看一个例子:    

     

    01 #include <iostream>
    02 using namespace std;
    03  
    04  class Father
    05  {
    06 public:
    07     int a;
    08  };
    09  
    10  class Child : virtual public Father
    11  {
    12  public:
    13     int b;
    14  };
    15  
    16  int main()
    17  {
    18     cout << sizeof(Father) << ' ' << sizeof(Child) << endl;//输出:4   12
    19     Child child;
    20     cout << &child << ' ' << &child.b << ' ' << &child.a << endl;//输出:0xbfc08124 0xbfc08128 0xbfc0812c
    21     return 0;
    22  }

     

     

     

          对,你没有看错,类的大小输出不是4   8,而是4   12。虚拟继承时,编译器会在子类中安插上一个虚表指针。

          从输出的对象成员地址来看,我们可以得到Child类的如下内存布局:

             

          现在我们对多重继承的例子进行改造:

     

    01 class Top
    02 {
    03 public:
    04      int a;
    05 };
    06  
    07 class Left : virtual public Top
    08 {
    09 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 };

     

         把Left和Right改成了虚拟继承Top。

     

          从上面验证简单虚拟继承时,编译器安插虚表指针的例子,我们可以想象出此时Bottom类的对象内存布局如下:

            

            对,你没有看错!虚拟继承时,子类只有父类共同继承的祖父类的一份存在。这其实也就是虚拟继承的最大用途。此时,Top,Left,Right和Bottom对象的大小分别为:4  ,12  ,12 ,24。

             既然有虚表指针了,那么Bottom的虚表是什么样的呢?请看:

            

            有了虚表,内存布局情况一目了然。下面我们进行验证:

     

    1 Bottom *bottom = new Bottom();
    2 top = bottom;
    3 cout << bottom << ' ' << top << endl;//输出:0x9fa5028       0x9fa503c
    4 Left *left = bottom;
    5 cout << bottom << ' ' << left << endl;//输出:0x9fa5028       0x9fa5028
    6 Right *right = bottom;
    7 cout << bottom << ' ' << right << endl;//输出:0x9fa5028       0x9fa5030

      根据输出结果,我们可以知道指针的指向情况:

        

    由于引入了虚指针和虚表,left指针和right指针可以根据虚表提供的偏移量信息,轻松访问到Top::a。


    到此为止,已经讨论清楚了多重继承和虚拟继承下的对象内存布局情况。总结下:非虚拟多重继承时,子类会有父类

    共同继承祖父类的多份存在;虚拟继承时,子类会被安插一个虚拟指针;多重虚拟继承时,子类只有父类共同继承祖父类的一

    份存在。通过父类的虚拟指针,可以正确地访问祖父类中的成员。

  • 相关阅读:
    Java面向对象(02)--封装
    Java面向对象(01)--初识
    Java基础(10)--数组
    Java基础(09)--方法
    python中format输出常用的3种格式
    python 查找列表中重复元素以及重复元素的次数
    HttpRunner六:创建run.py文件,执行套件并生成测试报告
    HttpRunner五:关联参数的应用,获取上一个接口的返回值,用于当前接口的请求值
    HttpRunner四:testcases、testsuites以及参数化的使用
    HttpRunner中在case2中,使用作为请求参数和预期结果,预期结果中值显示是:LazyString($变量key)
  • 原文地址:https://www.cnblogs.com/sideny/p/3301926.html
Copyright © 2011-2022 走看看