目录
- 一、复杂的菱形继承及菱形虚拟继承
- 1.单继承
- 2.多继承
- 3.菱形继承
- 3.1虚拟继承解决数据冗余和二义性的原理
- 二、继承的总结和反思
一、复杂的菱形继承及菱形虚拟继承
1.单继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
Student类的直接父类是Person类,PostGraduate类大家可能会误会成多继承,其实不是的,该类只有一个直接父类Student,Person类只是间接的继承
2.多继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
上面这个图就充分体现了多继承的特性,Assistant类既继承了Student类又继承了Teacher类,并且都是直接继承,没有间接继承,这个就是所谓的多继承
3.菱形继承
有了多继承,自然而然的就产生了菱形继承,菱形继承是一种特殊的多继承,他是多继承的一种形态
根据上面这个继承关系我们可以得到下面的这个图
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在Assistant的对象中Person成员会有两份。
从这个图中不难看出,由于Assistant类既继承了Student又继承了Teacher,再加上Student和Teacher都继承了Person,所以Student和Teacher当中都会有Person的成员,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::_name不明确 //因为你存在两份这个_name,编译器不知道你访问的是Student里面的还是Teacher里面的 // 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决 a.Student::_name = "xxx"; a.Teacher::_name = "yyy"; }
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用,有菱形继承关系的时候再使用
在腰部的两个类上加上关键字virtual使用虚拟继承即可解决上面菱形继承出现的问题,代码如下:
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; // 主修课程 };
3.1虚拟继承解决数据冗余和二义性的原理
为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型。
我们先看一下不加虚拟继承的菱形继承
class A { public: int _a; }; class B : public A { public: int _b; }; class C : 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; }
有一个D的对象d,该对象继承了两个类,分别是B和C,B和C又都继承了A,这里就构成了菱形继承,但是我们这里并没有使用虚拟继承来解决,只是为了借助内存窗口看一下菱形继承的对象成员分布情况,由于我们先继承的B再继承的C,所以B的成员在前,C的成员在后,B里面有自己的成员_b也有从A哪里继承过来的成员_a,C也是这样,D里面既有B类,也有C类,并且他们两个里面都有_a,上面我们就可以看到B里面的_a=1,C里面的_a=2,这里也就会有数据冗余和二义性的问题,我们必须指定初始化那个类里面的_a,不然编译器不知道要怎么初始化
我们再来看看加了虚拟继承之后成员又会如何分布呢?
这里我们就直接跳过代码板块,直接用图说话:
我们这里是用B类的指针和C类的指针来接收这个d对象,这里会有天然的切片,每个类的指针就直接是指向了该类的成员所在的那块位置,会有指针偏移
二、继承的总结和反思
- 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
- 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java
- 继承和组合
先来讲一下什么是组合?
举个例子:
class Person { public: string _name; }; class Student { private: Person _p; int _age; };
这就是组合,他和继承很像,都可以使用到Person里面的成员,但是组合只能使用它的public成员,而继承既可以使用public又可以使用protected成员,除此之外还能间接使用它的私有成员,那既然这样我们为什么要使用继承呢?直接用组合不就完了,还没有菱形继承的问题
其实这两个各有千秋,但是如果不是非得用继承的情况下我们一般推荐使用组合
什么情况下使用继承,什么情况使用组合?
答:继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象,比如说Person和Student,学生是人(is-a的关系),所以用继承,而组合是一种has-a的关系,如果是Person和Student这种情况,就不能用组合,你不能说学生里面有一个人吧,这不符合逻辑,但是如果是汽车和轮胎,就可以用组合,汽车里面有轮胎,这非常合理
优先使用组合而不是继承还有一个原因
说之前我们先讲解一下四个名词,低耦合,高内聚和高耦合,低内聚
什么是高/低耦合?
低耦合就是类与类之间的关联很小,不会牵一发而动全身,反之则是高耦合
什么是高/低内聚?
高内聚就是类里面全是与该类紧密相关的函数或者成员,这就交高内聚,如果一个类的成员和其函数与该的关联度不高,反正就是什么都沾点的那种,这就叫低内聚
在继承方式中,基类的内部细节对子类可见。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。而对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
文字太过粗糙,我们画个图来解说一下
最后再唠唠我们面试时面试官可能会问的问题
- 什么是菱形继承?菱形继承的问题是什么?
菱形继承(Diamond Inheritance)是多继承(Multiple Inheritance)的一种特殊情况,指的是在面向对象编程中,有一个基类被两个不同的类所继承,并且存在一个类继承于这两个类而形成的一种菱形关系。具体来说,菱形继承的定义可以表述为:两个子类继承同一个父类,而又有子类同时继承这两个子类,这就导致菱形继承存在数据冗余和二义性的问题
- 什么是菱形虚拟继承?如何解决数据冗余和二义性的
菱形虚拟继承通过使用virtual关键字来修饰共同的基类,确保了子类在继承多个拥有共同基类的父类时,只会在内存中保留一份基类数据,并且使用唯一的基类指针来访问基类成员,从而解决了数据冗余和二义性的问题。
- 继承和组合的区别?什么时候用继承?什么时候用组合?
这个问题我们在上面有讲解,我就不再说一遍了
继承篇章到这里就结束了,我们下期接着聊C++中的其他知识✨
还没有评论,来说两句吧...