[C++] 菱形继承和虚拟菱形继承 原理分析
单继承 与 多继承
-
单继承:一个子类只拥有一个直接父类,继承关系是直上直下的
想这样的关系:
Student
继承Person
,Doctor
继承Student
。就属于 直上直下的继承关系 -
多继承:一个子类拥有多个直接父类
想这样,
Assistant(助教)
分别继承了Student
和Teacher
,就是一种多继承
菱形继承 和 菱形虚拟继承
什么是菱形继承
Student
和 Teacher
都继承了 Person
Assistant
多继承了 Student
和 Teacher
并不是只有图示这样的才叫菱形继承,像这样
只要体系中 有相同父类的两个或多个子类被另一个子类继承了,就会形成菱形继承
- 二义性,即 最下面的子类的对象中 存在多个相同的成员
- 数据冗余,即 最下面子类的对象中 存储了多个相同的内容
好像数据冗余也没有太大的影响?
然而并不是,对于小占用的数据是如此,如果是大占用的呢?
这样的双倍,就不会影响小了
菱形虚拟继承及其原理
virtual
Person
可以使用,简单的类来 查看菱形虚拟继承的对象模型,对象在内存中的存储 就是对象模型
PS:博主的 VS执行环境是 64位 的,所以查看地址需要 8字节 的查看
32位环境,请 4 字节查看
A 类成员变量 _a 给缺省值 10
非虚拟继承的对象模型:
为了 方便观察,给 对象的成员赋了值,且 A对象的成员 _a 给了缺省参数 10:
对比来看 对象模型
可以发现,内存中,从低到高 每8个字节 分别存储的是:
再对比 对象d1 中的数据,会发现,对象模型中存储的是:
B::A::_a = 10
B::_b = 4
C::A::_a = 10
C::_c = 16
D::_d = 20
所以 非虚拟菱形继承 的对象模型,在内存中存储的就是 B、C 和 自己的成员,且是紧挨着存储的
即:
菱形虚拟继承的对象模型:
同样是给 对象的成员赋了值,且 A对象的成员 _a 给了缺省参数 10:
观察 其对象模型,可以发现存储的是地址和数值:
对象中 存储的数据 确实都在呢:_b = 4, _c = 16, _d = 20, _a = 10 4、16、20、10 都存储在对象模型中,但是除此之外还有两个不知道是什么的地址
观察那两个地址:
按照 4 字节查看,可以看到 那两个地址下面各自存放了一个数值:40 和 24
其实,这两个数值 表示 偏移量,而 存放那两个地址的位置是指针,被称为虚基表指针,存放偏移量的这个地方,被称为 虚基表:
这两个偏移量,其实是 虚基表指针 相对于 _a 存储位置的偏移量:
所以其实,B 和 C类还在 对象模型中
只不过相对于 非模拟继承,直接存储
_a
变成了使用两个指针指向虚基表,从表中找到相对偏移量,再由偏移量找到_a
这种方法非常的麻烦,但是很有效的解决了 数据冗余和二义性 问题
因为 最高层父类的数据只存储了一份,如果想访问 就使用指针和偏移量去找,访问的都是同一个位置
可能存在的问题:
虽然可能存在二义性的数据只留下了一个且被存放在了对象空间的下面,但是 数据依旧都在同一个对象中,为什么还需要存储偏移量,使用时还需要用偏移量来找到原本应该属于自己的数据呢?
这个问题的原因或许有许多的解答,但是这里我只解释一个地方
先分析另一个问题:
还是以 A,B,C,D 这个继承体系为例:
如果不存储偏移量去找数据,怎么完成这个赋值的操作
子类对象赋值给父类对象是会发生切割的,会只保留 对应类的部分 再赋值给 对应的类.
如果,不存储偏移量不去找数据,那么 切割过去的就是一个指针 而不是数据,那就说明赋值失败了.
对象 b 和 c 需要的是数据,而不是指针,也不会去找偏移量,更不会通过偏移量去找数据.
所以,既然 子类对象中的数据被挪走了,就需要记录位置 以便使用时可以找得到
菱形继承的关键源头,其实不在于菱形继承,而在于多继承
继承总结
-
C++多继承的设计 是有许多人说 C++ 语法复杂的原因之一,因为太复杂,太繁琐。有了多继承就肯定会出现菱形继承,为了解决菱形继承出现的问题,又有了菱形虚拟继承,底层实现过于复杂。这其实也可以看作 C++ 语法设计的缺陷之一,作为吸取了不少C++语法的经验,Java 就直接把多继承给舍弃了。
-
关于 组合 和 继承
-
继承实现的是,将 其他类 融入到 我的类之中,实现了 子类对象可以看作就是父类对象的现象,即 A对象就是B对象。从 子类对象可以无类型转化赋值给父类对象 就可以说明此现象
-
组合则实现的是,在我的类中 定义一个其他类的对象作为我的成员,实现了 A对象有B对象的现象。
-
组合 和 继承 是C++中代码复用的两种方式,不过在实际的使用中 优先考虑使用对象组合,为什么呢?
组合 和 继承都是代码复用的手段,但是 组合 相对于 继承 是属于 高内聚,低耦合 的。
什么是耦合?在代码中 耦合 可以看作代码之间的关联度,在程序设计中,需要遵循的一个规则就是:尽量的高内聚,低耦合,进而降低不同代码之间的关联度。
低耦合的代码,可以做到 一方修改代码 几乎不影响 另一方,组合就是这样的。使用对象组合,类实现的底层是不会暴露出来的,这样的复用 可以称为 黑箱(黑盒)复用(black-box reuse) ,因为不给使用者提供底层细节,所以使用时就只能调用接口来执行操作,一方的代码变动,但是接口不变的话,另一方依旧可以正常使用
而继承,相对耦合就高了。因为 父类融入了子类中,修改父类的代码对子类的影响有时是非常大的。使用继承 子类可以直接访问父类的成员,即底层,这样的复用 可以称为 白箱(白盒)复用(white -box reuse)。因为父类的底层细节是直接暴露给子类的, 所以子类成员实现某些功能的时候,很可能会直接使用父类成员进行操作 而不是使用接口。所以当父类代码修改的时候,很可能对子类影响很大,导致子类也需要修改代码
所以 优先考虑使用对象组合,继承的维护成本比组合的维护成本要高很多!!
-
组合一般在类似这种情况下使用。比如:车有4个轮子,人有一双手等,不能说 车是4个轮子 或 4个轮子是车
-
继承一般在类似这种情况下使用。比如:狗是动物,猫是动物,同样的不能说 动物是猫,动物是狗
-
-
继承 是在一定程度上 破坏类的封装,所以 当组合 和继承都可以使用的时候,优先使用组合
作者: 哈米d1ch 发表日期:2022 年 7 月 23 日