【C++】面向对象编程的三大特性:深入解析继承机制(二)https://developer.aliyun.com/article/1617389
三、菱形继承及菱形虚拟继承
3.1 继承分类
单继承:当一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:属于多继承的一种特殊情况
3.2 菱形继承问题
从上面可以看出来,菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。
class Person { public : string _name ; // 姓名 }; class Student : public Person { protected : int _num ; //学号 }; class Teacher : public Person { protected : int _id ; // 职工编号 }; class Assistant : public Student, public Teacher { protected : string _majorCourse ; // 主修课程 }; void Test () { // 这样会有二义性无法明确知道访问的是哪一个 Assistant a ; a._name = "peter"; }
Assistant的对象中Person成员会有两份,那么如果单纯a._name = "peter"
会产生二义性,编译器无法明确知道访问的是哪一个直接父类的成员。
解决办法:使用指定类域限定符访问
a.Student::_name = "xxx"; a.Teacher::_name = "yyy";
不足之处:需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
3.3 虚继承(菱形虚拟继承)
3.3.1 虚继承概念
对此引入虚继承这个概念,可以用于解决菱形继承的二义性和数据冗余的问题。
class Person { public : string _name ; // 姓名 }; class Student : virtual public Person { protected : int _num ; //学号 }; class Teacher : virtual public Person { protected : int _id ; // 职工编号 }; class Assistant : public Student, public Teacher { protected : string _majorCourse ; // 主修课程 }; void Test () { Assistant a ; a._name = "peter"; }
我们需要在腰部位置上将继承改为虚继承(使用virtual)
3.3.2 虚拟继承解决数据冗余和二义性的原理
这里需要借助内存窗口观察对象成员的模型,调试窗口有时为了方便观察进行了调整,导致结果不准确。
class A { public: int _a; }; // class B : public A class B : virtual public A { public: int _b; }; // class C : public A class C : virtual public A { public: int _c; }; class D : public B, public C { public: int _d; }; int main() { D d; d.B::_a = 1; d.C::_a = 2; d._b = 3; d._c = 4; d._d = 5; return 0; }
从下列两个模拟来看,一个是没有虚继承导致数据冗余和二义性,一个是完成虚继承解决了数据冗余和二义性
具体说明:
- 第一种,这里B和C类都存储了一个变量_a,导致数据冗余和二义性。
- 第二种,这里可以看出D对象中将A放到了对象组成的最下面,这个_a同时属于B和C,修改这个 _a同样会影响到其他类的 _a。
- 这里我们需要知道B和C如何去找到公共的A,这里是通过B和C的两个指针(就是属于x类内存中第一个存储的地址),去指向一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量(十六进制)可以找到下面的_a。
注意:
- 内存存储的顺序是按照声明顺序或者继承顺序存储的。
3.3.3 多继承指针偏移特性
继承的指针偏移特性,为了支持动态绑定和多态性。多继承的情况下,由于不同的父类可能有不用的虚函数表,因此需要通过指针偏移来正确访问不同父类的虚函数表。这种指针偏移通常由编译器自动生成的,确保访问父类的成员或者调用父类的虚函数时,能够正确地定位到父类对象内存位置。
四、继承总结和反思
4.1 设计继承建议
一般不建议设计出多继承,一定不要设计出菱形继承,不然在复杂度及性能上都有问题。实践中可以设计多继承,但是切记不要设计菱形继承,因为太复杂,容易出现各种问题
4.2 继承和组合
- public继承:是一种is-a的关系,也就是说每个派生类对象都是一个基类对象
- 组合:是一种has-a的关系,假设B组合了A,每个B对象中都有一个A对象
继承允许你根据基类的实现派生类的实现。这种通过生成派生类的复用通常被称为为白箱复用(white-box reuse),术语"白箱"是相对可视性而言:在继承方式中,基类的内部细节对子类可见。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合性度高
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组合或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以"黑箱"的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先实现对象组合有助于你保持每个类被封装
实践尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合
组合比如:
class A { private: int _ a; } //组合 class B { private: A _aa; int _b; }
4.3 低耦和与高内聚
- 低耦和:类和类之间、模块与模块之间关系不那么紧密,关联不高
- 高耦合:类和类之间、模块与模块之间关系很紧密,关联很高
五、笔试面试题
问题:
- 什么是菱形继承?菱形继承的问题是什么?
- 什么是菱形虚拟继承、解决数据冗余和二义性的?
- 继承和组合的区别?什么时候用继承?什么时候用组合?
回答:
- 菱形继承是指在面向对象编程中的一种继承关系,其中一个列同时继承两个不同的类,而这两个类又都继承自同一个父类,形成了一个菱形的继承结构。
- 数据冗余和二义性问题,并且还有代码维护困难、不稳定性
- 菱形虚拟继承是C++中用来解决菱形继承问题的一种机制,派生类通过虚拟继承来自共同基类,被虚继承的基类只会在继承层次结构中存在一份实例,而不会出现多次复制
- 继承和组合是面向对象编程中两种不同的关系模式,继承是一种"is-a"关系,表示的是类之间的一种分类关系,即子类是父类的一种特殊形式,而组合是一种"has-a"关系,表示一个类包含另一个类作为基一部分,但是它们之间不具有层次关系。
- 当存在明确的"是一个is-a"关系,且子类需要继承父类的行为和属性时,使用继承,当需要在不同的类之间建立层次关系时,使用继承。
- 当对象之间存在包含关系,一个对象包含另一个对象作为其一部分时,使用组合。当对象之间的关系更加动态,或者不需要建立明确的层次关系时,使用组合。
六、继承是子类拷贝一份父类数据吗?(重点)
在继承中,子类和父类之间是一种逻辑关系,子类没有真正地拥有一份父类代码的副本,而是通过继承机制在需要时访问父类的功能。继承的这种特性使得子类不仅可以直接使用父类已有的功能,还可以通过重写(override)或扩展(extend)来修改或增加功能,而这一切都是通过共享父类的代码实现的,而不是通过复制代码实现的。
继承的实现是在运行时,通过类之间的关系来实现的。子类通过引用父类的成员,而不是复制父类的代码。因此,子类并没有物理上拥有父类的代码,而是通过继承关系“共享”父类的代码。
因此,继承不是拷贝,它是一种更高级的代码复用机制,通过类的层次结构来共享行为,而不是简单地复制粘贴代码。这样不仅可以减少代码重复,还可以通过多态和重写机制来增强代码的灵活性和可扩展性。
以上就是本篇文章的所有内容,在此感谢大家的观看!这里是店小二呀C++笔记,希望对你在学习C++语言旅途中有所帮助!