【C++】继承(三):破解C++菱形继承之谜:为什么我们需要虚拟继承?

一、复杂的菱形继承及菱形虚拟继承

1.单继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承
在这里插入图片描述
Student类的直接父类是Person类,PostGraduate类大家可能会误会成多继承,其实不是的,该类只有一个直接父类StudentPerson类只是间接的继承

2.多继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
在这里插入图片描述
上面这个图就充分体现了多继承的特性,Assistant类既继承了Student类又继承了Teacher类,并且都是直接继承,没有间接继承,这个就是所谓的多继承

3.菱形继承

有了多继承,自然而然的就产生了菱形继承,菱形继承是一种特殊的多继承,他是多继承的一种形态

在这里插入图片描述
根据上面这个继承关系我们可以得到下面的这个图

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余二义性的问题。在Assistant的对象中Person成员会有两份。
在这里插入图片描述
从这个图中不难看出,由于Assistant类既继承了Student又继承了Teacher,再加上StudentTeacher都继承了Person,所以StudentTeacher当中都会有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,该对象继承了两个类,分别是BCBC又都继承了A,这里就构成了菱形继承,但是我们这里并没有使用虚拟继承来解决,只是为了借助内存窗口看一下菱形继承的对象成员分布情况,由于我们先继承的B再继承的C,所以B的成员在前,C的成员在后,B里面有自己的成员_b也有从A哪里继承过来的成员_aC也是这样,D里面既有B类,也有C类,并且他们两个里面都有_a,上面我们就可以看到B里面的_a=1C里面的_a=2,这里也就会有数据冗余和二义性的问题,我们必须指定初始化那个类里面的_a,不然编译器不知道要怎么初始化

我们再来看看加了虚拟继承之后成员又会如何分布呢?

这里我们就直接跳过代码板块,直接用图说话:
在这里插入图片描述


我们这里是用B类的指针和C类的指针来接收这个d对象,这里会有天然的切片,每个类的指针就直接是指向了该类的成员所在的那块位置,会有指针偏移

在这里插入图片描述

二、继承的总结和反思

  1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
  2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java
  3. 继承和组合
    先来讲一下什么是组合?
    举个例子:
class Person
{
public:
	string _name;
};
class Student
{
private:
	Person _p;
	int _age;
};

这就是组合,他和继承很像,都可以使用到Person里面的成员,但是组合只能使用它的public成员,而继承既可以使用public又可以使用protected成员,除此之外还能间接使用它的私有成员,那既然这样我们为什么要使用继承呢?直接用组合不就完了,还没有菱形继承的问题

其实这两个各有千秋,但是如果不是非得用继承的情况下我们一般推荐使用组合

什么情况下使用继承,什么情况使用组合?
答:继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象,比如说PersonStudent,学生是人(is-a的关系),所以用继承,而组合是一种has-a的关系,如果是PersonStudent这种情况,就不能用组合,你不能说学生里面有一个人吧,这不符合逻辑,但是如果是汽车和轮胎,就可以用组合,汽车里面有轮胎,这非常合理

优先使用组合而不是继承还有一个原因

说之前我们先讲解一下四个名词,低耦合,高内聚高耦合,低内聚
什么是高/低耦合
低耦合就是类与类之间的关联很小,不会牵一发而动全身,反之则是高耦合
什么是高/低内聚
高内聚就是类里面全是与该类紧密相关的函数或者成员,这就交高内聚,如果一个类的成员和其函数与该的关联度不高,反正就是什么都沾点的那种,这就叫低内聚

在继承方式中,基类的内部细节对子类可见。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。而对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

文字太过粗糙,我们画个图来解说一下
在这里插入图片描述


最后再唠唠我们面试时面试官可能会问的问题

  1. 什么是菱形继承?菱形继承的问题是什么?

菱形继承(Diamond Inheritance)是多继承(Multiple Inheritance)的一种特殊情况,指的是在面向对象编程中,有一个基类被两个不同的类所继承,并且存在一个类继承于这两个类而形成的一种菱形关系。具体来说,菱形继承的定义可以表述为:两个子类继承同一个父类,而又有子类同时继承这两个子类,这就导致菱形继承存在数据冗余和二义性的问题

  1. 什么是菱形虚拟继承?如何解决数据冗余和二义性的

菱形虚拟继承通过使用virtual关键字来修饰共同的基类,确保了子类在继承多个拥有共同基类的父类时,只会在内存中保留一份基类数据,并且使用唯一的基类指针来访问基类成员,从而解决了数据冗余和二义性的问题。

  1. 继承和组合的区别?什么时候用继承?什么时候用组合?

这个问题我们在上面有讲解,我就不再说一遍了


继承篇章到这里就结束了,我们下期接着聊C++中的其他知识✨

  • 19
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值