当前位置: 首页 > article >正文

C++大坑之——多继承(菱形继承)

在这里插入图片描述

文章目录

  • 前言
  • 一、多继承是什么?
    • 1. 多继承概念
    • 2. 多继承语法
  • 二、菱形继承
    • 1. 为什么会有菱形继承问题?
    • 2. 代码感受菱形继承
    • 3. 虚拟继承
      • 1)虚拟继承概念及语法
      • 2)虚拟继承的原理
    • 4. 为什么要有虚基表?
    • 5. 为什么要有偏移量?
    • 6. 关于解决数据冗余
  • 三、小试牛刀
  • 四、库里的菱形继承
  • ❤️继承的总结和反思


前言

前面学习了继承的概念与语法,今天我们一起来看看C++中的大坑——菱形继承🥰


一、多继承是什么?

1. 多继承概念

多继承是指一个类可以同时继承多个父类的特性。在这种情况下,子类能够访问和使用其所有父类的方法和属性。

这样理解:现实生活中,子类可能会继承多个父类,比如骡子是由马和驴所生的,他同时继承了马和驴的一些特征。
在这里插入图片描述

这种特性在一些面向对象编程语言(如C++)中是允许的,但在其他语言(如Java)中则被限制为单继承。

我们再通过下面这个例子区分一下单继承和多继承:

这是单继承,一个子类只有一个直接的父类,他也只有这一个直接父类的成员
在这里插入图片描述

这是多继承,及子类同时具有两个及以上的直接父类,他有所有直接父类的成员
在这里插入图片描述


2. 多继承语法

多继承的基本语法是:class 子类 : 继承方式 父类1,继承方式 父类2…

现在有这样一种情况:

#include<iostream>
using namespace std;


class Student
{
public:
	int _num; //学号
	int _age; //年龄
};

class Teacher
{
public:
	int _id; // 职工编号
	int _age; //年龄
};

class Assistant : public Student, public Teacher
{
public:
	string _majorCourse; // 主修课程
	int _age; //年龄
};

int main()
{
	Assistant as;
	as.Student::_age = 18;
	as.Teacher::_age = 30;
	as._age = 19;

	return 0;
}

他是这样继承的,如下图所示:
在这里插入图片描述
也可以很清楚的看到,这里有三份年龄都不一样,这就是多继承


二、菱形继承

1. 为什么会有菱形继承问题?

假设有这样一个继承的样子:
在这里插入图片描述
两个类同时继承一个父类,他们呢又有同样一个子类,就会形成菱形继承

我们来看一下菱形继承的对象模型:

这会导致什么现象呢?其实刚刚多继承的那个例子已经有所铺垫了,作为子类,他有两个基类的成员,就造成了(1)数据冗余,(2)二义性

在Assistant的对象中Person成员会有两份。
在这里插入图片描述

这三种形式都属于菱形继承,
也就是说继承只要形成了闭环,就是菱形继承!
在这里插入图片描述


2. 代码感受菱形继承

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; // 主修课程
};

直观的感受,这里有一个很大的问题:
在这里插入图片描述

假设我要使用a._name,就会有二义性,无法具体确定_name访问的是哪个父类成员的_name,但是二义性好解决,指定作用域就可以。但是,对于数据冗余的问题依然解决不了。


3. 虚拟继承

1)虚拟继承概念及语法

因此,就出现了虚拟继承!

虚拟继承是一种在C++中解决菱形继承问题的机制。当一个子类通过多个父类继承同一个祖先类时,会导致潜在的二义性(即“钻石问题”)。虚拟继承通过确保只有一份祖先类的实例存在,来避免这种问题。

主要特点:

  1. 语法:在继承时使用关键字virtual来声明父类。例如:

    class A {};
    class B : virtual public A {};
    class C : virtual public A {};
    class D : public B, public C {};
    

    注意,这里是在腰部进行virtual关键字,最下面的儿子以及祖先都不写!

  2. 共享实例:虚拟继承确保无论通过哪个路径继承,只有一个A的实例存在于D中。

  3. 构造顺序:虚拟基类的构造函数在所有派生类构造之前被调用,确保它的成员被初始化。

  4. 访问:在虚拟继承中,派生类可以通过虚拟基类来访问祖先类的成员,避免了命名冲突。

优点:

  • 消除了菱形继承带来的二义性,以及数据冗余
  • 提高了代码的可维护性和可读性。

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;
};

这个继承方式是这样的:A里有_a,B里有_b,C、D同理:
在这里插入图片描述

对于它内部内存的管理:

先来进行初始化:

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

对于普通菱形继承,不使用virtual关键字是这样的:

可以看到,它没有什么不同,就是按照顺序连续存储的,有两个A就造成了数据冗余与二义性。
在这里插入图片描述


对于带virtual关键字的菱形继承:

在这里插入图片描述

首先,我们可以发现,A只有一份了,然后这个公有的A被放到了最下面,这样就解决了二义性的问题

其次,在B和C中,多了一串奇奇怪怪的东西,如下图所示:
在这里插入图片描述
我们分别进入这两个地址,0x002B7B480x002B7B53,如下如所示:
在这里插入图片描述

这个地方红色框框出来的实际上就分别是B到A与C到A的偏移量!
我们叫它虚基表!!!

在这里插入图片描述

虚基表中红框部分存了偏移量,第一行是预留的,目前第二行是有效的。使用白框中的地址就可以找到偏移量,最终可以定位到A类中去!


4. 为什么要有虚基表?

为什么要有一个虚基表呢?下面这里白框的部分难道不能直接存A的地址吗?

在这里插入图片描述

原因有一下两个场景:

场景一:我们这里共同的只有一个A类,因此对于这里来说看不出差别,但是假设我还有其他的值要存呢?假设我还有EFG…要存在这里呢?
因此我们引入了虚基表,这些偏移量全部存到一个虚基表里边去,子类对象里只存虚基表的地址,利用偏移量来寻找所需的A。

场景二:我们这里只定义了一个d对象,假设我还有一个d1呢?这两个对象是一模一样的结构,它们相对偏移量的关系也是相等的,有了虚基表就可以传同一份虚基表的地址,通过相同的偏移量来找到A。
如下图:我们可以看到d与d1虚基表的地址是一样的!!!
在这里插入图片描述
在这里插入图片描述


5. 为什么要有偏移量?

那为什么又需要偏移量来找呢?
请问下面这段代码需不需要用到偏移量?

D d;
d._a = 1;

答案:不需要。
作为虚拟继承的它,编译器直到它的A在最下面,找到时候就直接去最下面找就可以了。

那什么时候会用到偏移量呢?
下面我也给出两个场景:

场景一:切片。假如有这样一段代码。
D d;
B b = d;
那么这个b作为父类就要去找d中相应的部分进行切片,但是d中是这样存的:
在这里插入图片描述
B的部分除了最上面蓝色的框还有最下面的A,因此找A就需要进行偏移量来找到。

场景2:

假设有这样一串代码:

D d;
d._a = 1;

B b;
b._a = 2;
b._b = 3;

B* ptr = &b;
ptr->_a++;

ptr = &d;
ptr->_a++;

首先我们要知道,作为虚拟继承,不只是D的模型,连B的模型结构都变了,他变得与C保持一致。
如图:
在这里插入图片描述
b的模型已经不再是纯粹的,他也有虚基表,它的A也在最下面。

那么就会引发出一个问题,假设有这样的代码:
在这里插入图片描述
单看这里两个蓝色框里的代码,从表面看没有任何差异,对于编译器来说他并不知道实在调用b还是在调用d,因此只要我们取出偏移量,就可以根据偏移量来计算找到A。

我们可以来看一下汇编,这里是一模一样的,唯独偏移量不同:
在这里插入图片描述

因此,虚基表和偏移量都是必须的!!!

6. 关于解决数据冗余

但从下面这张图来说,好像没有解决数据冗余的问题。
在这里插入图片描述

但是,假设 _a是个数组呢?_a[10086],那么普通继承会继承很多份_a[10086],但是虚拟继承只继承一份,所以还是解决了数据冗余的问题。


三、小试牛刀

  1. 请问p1, p2, p3的关系是什么?
class Base1 { public:  int _b1; };
class Base2 { public:  int _b2; };
class Derive : public Base2, public Base1 { public: int _d; };

int main() {
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	Derive* p3 = &d;
	return 0;
}

分析如图:
在这里插入图片描述
谁先继承谁在上面L:
p1与p3所指向位置是一样的,但是p1与p3含义不同,p2在它们下面。
因此p1 = p3 != p2


  1. 请问下面代码打印顺序是什么?
#include<iostream>
using namespace std;
class A {
public:
	A(const char* s) { cout << s << endl; }
	~A() {}
};

class B :virtual public A
{
public:
	B(const char* sa, const char* sb) :A(sa) { cout << sb << endl; }
};

class C :virtual public A
{
public:
	C(const char* sa, const char* sb) :A(sa) { cout << sb << endl; }
};

class D :public B, public C
{
public:
	D(const char* sa, const char* sb, const char* sc, const char* sd) 
		:B(sa, sb), C(sa, sc), A(sa)
	{
		cout << sd << endl;
	}
};

int main() {
	D* p = new D("class A", "class B", "class C", "class D");
	delete p;

	return 0;
}

答案是:在这里插入图片描述

这里考了两个点:

  • 虚拟继承只继承一份
  • 初始化列表顺序与构造顺序无关,谁先声明谁先构造。

四、库里的菱形继承

其实我们iostream就是一种菱形继承,库里的大佬驾驭得住,我们在实战中还是要尽量避免使用。
在这里插入图片描述


❤️继承的总结和反思

组合与继承的关系
在这里插入图片描述

  1. 多继承的复杂性
    很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。

  2. 多继承的缺陷
    多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。

  3. 继承和组合

    • 继承
      public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。

    • 组合
      组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。

    • 优先使用对象组合,而不是类继承。

    • 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。

    • 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。

    • 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

    • 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。


到这里就结束啦,创作不易,佬们三连支持一波🤩🤩🤩🥰🥰🥰<( ̄︶ ̄)↗[GO!]
在这里插入图片描述


http://www.kler.cn/news/362416.html

相关文章:

  • uniapp,获取头部高度
  • 复旦大学全球供应链研究中心揭牌,合合信息共话大数据赋能
  • pikachu靶场CSRF-post测试报告
  • 【从零开始的LeetCode-算法】3185. 构成整天的下标对数目 II
  • 数据结构与算法:贪心算法与应用场景
  • js---三元表达式详解
  • Java的方法传参机制
  • Nvidia在Jetson NX上模型量化
  • 关于武汉芯景科技有限公司的限流开关芯片XJ6241开发指南(兼容LTC4411)
  • 学习笔记每日一题——缺失的第一个正数
  • 【原创】java+ssm+mysql计算机等级考试网系统设计与实现
  • Android compose 重建流程1
  • opencv 图像BGR三通道分离 split 与 合并 merge -python 实现
  • LeetCode15 三数之和 - “贪心+双指针: 基于”两数之和“的拓展题“
  • 小O睡眠省电调研
  • Linux基础知识和常用基础命令
  • 【Next.js 项目实战系列】07-分配 Issue 给用户
  • 智能电池与ROS通信让机器人获取电池电流电压电量信息
  • SpringBoot框架下的桂林旅游信息中心
  • 微积分复习笔记 Calculus Volume 1 - 2.5 Precise Definition of a Limit
  • Apache Cordova学习计划
  • 基于单片机的 OLED 显示终端设计分析与研究
  • ARM/Linux嵌入式面经(五二):华为
  • Web开发:ABP框架5——入门级别的常见问题和报错解析
  • 信息安全工程师(64)其他恶意代码分析与防护
  • 【Qt】控件——Qt多元素控件、常见的多元素控件、多元素控件的使用、List Widget、Table Widget、Tree Widget