• C++对象模型剖析(六)一一Data语义学(三)


    Data 语义学(三)

    “继承” 与 Data member

    上期的这个继承的模块我们还剩下一个虚拟继承(virtual inheritance)没有讲,现在我们就来看看吧。

    • 虚拟继承(Virtual Inheritance)

      虚拟继承本质就是:通过某种形式来实现共享继承,使被继承的类在继承体系中只存在一个实例。最常用的就是:解决菱形继承

      下面我们看一个熟悉的例子:

      在这里插入图片描述

      这样我们就能够很清楚的知道多重继承和虚拟继承的区别,而且我们也能看出在多重继承的体系下,我们需要维护两个 ios base class object ,这就造成了空间和效率上的浪费,我们不仅要为这两个 ios object 分配空间,我们还要同步对他们的修改操作,来保证两个 object 是一样的。所以,解决这个问题的关键就是导入虚拟继承(virtual inheritance)。

      class ios { ... }
      class istream : public virtual ios { ... }
      class ostream : public virtual ios { ... }
      class iostream : public istream, public ostream { ... }
      
      • 1
      • 2
      • 3
      • 4

      但是在编译器中实现虚拟继承难度很高:编译器需要一个足够有效的方法,将 istream 和 ostream 各自维护的一个 ios subobject,折叠成为一个由 iostream 维护的单一的 ios subobject,并且还可以保存 base class 和 derived class 的指针(以及 引用)之间的多态指定的操作。(polymorphism assignments)。

      一般的实现方法是这样的:**Class 如果内含一个或多个 virtual base class subobjects,像 istream 那样,将被分割成两部分,一个不变区域和一个共享区域。**不变区中的数据,不管后继如何衍化,总是拥有固定的 offset,所以这一部分数据可以被直接存取。至于共享区域,所表现的就是 virtual base class object。这一部分的数据,其位置会因为每一个的派生操作而发生变化,所以它们只可以被间接存取。各家编译器实现技术之间的差异就在于间接存取的方法不同。下面就为大家介绍这三种方法。

      首先看看Vertex3d虚拟继承的层次结构。

      class Point2d {
      public:
          ...
      protected:
          float _x, _y;
      };
      
      class Vertex : public virtual Point2d {
      public:
          ....
      protected:
          Vertex *next;
      };
      
      class Point3d : public virtual Point2d {
      public:
          ...
      protected:
          float _z;
      };
      
      class Vertetx3d : public Vertex, public Point3d {
      public:
          ...
      protected:
          float mumble;
      };
      
      // 继承关系
      //				Point2d(_x, _y)
      //				    |
      //				____|____
      //			    |        | 
      //		Vertex(next)	Point3d(_z)
      //		    	|        |
      //			    |________|
      //					|
      //				  Vertex3d(mumble)
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38

      **一般的布局策略是先安排好 derived class 的不变部分,然后再建立其共享部分。**不同的编译器对 virtual inheritance 的实现的不同就体现在共享部分的实现上。

      • 第一个方法:在 derived class 种添加指向 virtual base class 的指针

        直接上书上的例子

        void Point3d::operator+=(const Point3d &rhs)
        {
            _x += rhs._x;
            _y += rhs._y;
            _z == rhs._z;
        };
        // 在这种策略下,这个运算符会被内部转换为
        _vbcPoint2d->_x += rhs._vbcPoint2d->_x;
        _vbcPoint2d->_y += rhs._vbcPoint2d->_y;
        _z += rhs._z;
        
        // 现在我们考虑另一种情况
        Point2d *p2d = pv3d;
        // 同样在这种策略下,这个转换也会被内部转换为
        Point2d *p2d = pv3d ? pv3d->_vbcPoint2d : 0;
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15

        这个实现模型有两个主要的缺点:

        • 每一个对象必须针对其每一个 virtual base class 背负一个额外的指针。然而理想上我们却希望 class object 有固定的负担,不因为其 virtual base class 的个数而变化。
        • 由于虚拟继承串链的加长,导致间接存取层次的增加。这里的意思是,如果我有三层虚拟派生,我就需要三次间接存取(经由三个 virtual base class 指针)。然而理想上我们却希望有固定的存取时间,不因为虚拟派生的深度而改变。

        对于第二缺点,有些编译器会选择通过拷贝的操作取得所有的 nested virtual base class 指针,放到 derived class object 之中。这就解决了“固定存取时间”的问题,但是同时也付出一些空间上的代价。所以一般这些编译会提供一个选项——询问程序员是否要产生双重指针。

        看看模型的布局
        在这里插入图片描述

        对于第一个缺点,就引出了剩余的两个解决方案。

      • Microsoft 编译器引入了 virtual base class table。

        每一个class object 如果有一个或多个 virtual class table,就会由编译器安插一个指针,指向 virtual base class table。至于正真的 vitual base class pointer 将会被放在该表格中。

      • 在 virtual function table 中放置 virtual base class 的 offset(而不是地址)。

        以上面的继承体系为例,我们看看在这种策略下,每一个类(class)的布局

        image-20240302102506928

        上面的图很直观的呈现的这种将 virtual base class offset 和 virtual function table 结合的方法,virtual function table 可经由正值或负值来索引。如果是正值,很显然就是索引到了 virtual function table;如果是负值,则是索引到了 virtual base class offsets。

        // 再来看看这个 operator
        void Point3d::operator+=(const Point3d &rhs)
        {
            _x += rhs._x;
            _y += rhs._y;
            _z += rhs._z;
        }
        
        // 在这种策略下,编译器在内部做的转换如下
        void Point3d::operator+=(const Point3d &rhs)
        {
            (this + _vptr_Point3d[-1])->_x += (&rhs + rhs._vptr_Point3d[-1])->_x;
            (this + _vptr_Point3d[-1])->_y += (&rhs + rhs._vptr_Point3d[-1])->_y;
            _z += rhs._z;
        }
        
        // 转换操作
        Point2d *p2d = pv3d;
        Point3d *p2d = pv3d ? pv3d + pv3d->_vptr_Point3d[-1] : 0;
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19

      上面的每一种方法都是一种实现模型,而不是一种标准。每一种模型都是用来解决 “存取 shared subobject 内的数据(其位置会因每次派生操作而变化)”所引发的问题。

      一般而言,virtual base class 最有效的运用形式就是:一个抽象的 virtual base class,没有任何 data member。

      也就是我们所说的抽象类,在该类中定义纯虚函数(pure virtual function),也称为接口(interface)。

      还有一小节讲的是类成员指针(data member pointer),但是有点奇怪的是实验的结果跟书上显式的不一样,这个等我弄明白了再更吧,如果你们知道为什么求求出个文章吧。

  • 相关阅读:
    个人习惯阅读源码的方式以及IDEA查看源码常用快捷键(小技巧完善中。。。)
    idea使用Alibaba Cloud Toolkit实现自动部署
    上周热点回顾(7.10-7.16)
    c++学习--第二部分
    windows中MySQL主从配置【第一篇】
    汽车工业生产线数字孪生可视化管理平台,赋予工厂车间数字化智慧化管理
    win10 自带虚拟机软件 虚拟CentOS系统
    MySQl表的增删查改(CRUD)
    Java常用配置项和命令行
    【补题日记】[2022杭电暑期多校3]B-Boss Rush
  • 原文地址:https://blog.csdn.net/m0_73170116/article/details/136412354