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

C++的类中的虚拟继承【底层剖析(配图解)】

文章目录

  • 前言
  • 1. 继承的形式
    • 1.1 单继承
    • 1.2 多继承
  • 2. 菱形继承
    • 2.1 菱形继承的产生
    • 2.2 virtual虚拟继承现象和语法
    • 2.3 虚拟继承的底层
      • 2.3.1 单虚拟继承
      • 2.3.2 虚拟继承 — 虚基表、虚基表指针
      • 2.3.3 菱形虚拟继承的底层分析
    • 2.4 小结

前言

关于菱形继承虚拟继承这个话题是C++学习者必须经历的一条路。小编的这篇文章将从:

  1. 继承的类型
  2. 菱形继承的问题
  3. 虚拟继承是如何解决菱形继承的问题的?
  4. ……

角度出发,带大家对虚拟继承底层实现方式有一定的了解!

相信各位读者在阅读之后一定会大有收获!

1. 继承的形式

在之前小编和大家探讨基础语法的是时候列出了许多的示例,但是这些示例都是一些单子单父的情况。小编也在语法的时候提到一句:一个派生类可以继承多个基类。所以这也让我们的继承方式变得多样起来。

1.1 单继承

  • 单继承:一个子类只有一个直接父类的继承关系为单继承

如下图:

在这里插入图片描述

1.2 多继承

  • 多继承:一个子类有两个及以上的直接父类的继承关系被称为多继承。

如下图:

在这里插入图片描述

不管继承方式是单继承还是多继承,我们都应该注意到:我们对待这个子类为一个新的类型,一定要注意好如何设计好这个类的构造、析构等函数,以及各种接口

2. 菱形继承

2.1 菱形继承的产生

刚刚了解到单继承和多继承的形式,我们可以想到这样一个场景:我现在有一个person类来描述一个学校的人,那么我用student类继承这个person类,再由teacher类继承这个person类。我们学校有助教(assistant类),助教既是一个学生,也是一个老师……我们可以采用assistant继承personteacher……

大概继承关系如下图:

class person
{};class student : public person
{};class teacher : public person
{};class assistant : public student , public teacher
{};

在这里插入图片描述

观察这个图像的形状——一个菱形。这就是菱形继承名字的由来,这样的继承体系的结构十分地像一个菱形,所以被命名为菱形继承

我们现在对这个菱形继承的一些成员和方法进行完善

例1:

下面我们为这个继承关系中的类,进行一些字段的填写:

#include<iostream>
#include<string>
using namespace std;
class person
{
public://……
protected:string _name = "zhangsan"; //姓名int _ID = 1; //身份证
};class student : public person
{
public:void study() //学习方法{}
protected:int _sid = 2; //学生id
};class teacher : public person
{
public:void teach() //教学方法{}
protected:int _tid = 3; //教师id
};class assistant : public student, public teacher
{
public:void check() //助教的方法{}
protected:int _age = 4; //年龄—这里是故意将年龄在这里充当一个字段的
};int main()
{assistant a;cout << "DEBUG" << endl;return 0;
}

来看这个继承体系的继承模型
在这里插入图片描述

很显然,我们可以得到如下结论:

  1. person的字段在assistant中有两份。person类被student类和teacher类继承下来,对于student类和teacher类都有一份person类的字段,那么再由assistant继承他们,那么就导致了assistant就有了两份person类的字段!

    这就导致了一个来自菱形继承的问题

    • 数据冗余二义性

      • 数据冗余:两份来自于基类person的字段。
      • 二义性:派生类在访问基类字段的时候,是访问的基类student还是基类teacher呢?

我们从内存角度看看这个存储结构

在这里插入图片描述

完全符合我们的模型和预期!!!

我们从观察内存存储角度出发,发现这个菱形继承的问题所在

2.2 virtual虚拟继承现象和语法

那么我们应该如何去解决这个问题呢?

我们现在需要解决的问题:

  1. 菱形继承的二义性
  2. 菱形继承的数据冗余

菱形继承的二义性我们可以通过指明类域的方式来进行分别

如果是我们设计这样的一个方法来解决这样的问题,我们大概会怎么样去解决呢?

既然这个样的一个assistant中有两份的person对象,那么我们想办法只保留一份即可。

C++处理二义性和数据冗余的核心也是将这样的数据保留一份即可!

  • 关键字virtual
  • 使用:用于在分支子类的地方上。例如:上面的例1的studentteacher
  • 下图标红的类
    在这里插入图片描述

例2:

#include<iostream>
#include<string>
using namespace std;
class person
{
public://……
protected:string _name = "zhangsan";int _ID = 1;
};class student : virtual public person
{
public:void study(){}
protected:int _sid = 2;
};class teacher : virtual public person //虚拟继承
{
public:void teach(){}
protected:int _tid = 3;
};class assistant : public student, public teacher 
{
public:void check(){}
protected:int _age = 4;
};int main()
{assistant a;cout << "DEBUG" << endl;return 0;
}

上面的代码,使用了virtual关键字。由studentteacher虚拟继承person,这就是“虚拟继承
在这里插入图片描述
这里,我们从内存存储角度来看,菱形继承的问题是否得到了解决呢?

如下图:

在这里插入图片描述

  • 现象和问题
  1. 这样的结果完美符合我们想要的预期—最后的assistant类只保留了一份来自于person的字段。
  2. 有两个字段:002df1d0002dec14是什么呢?
  3. person字段被保存到了高地址处。整个对象的最高地址处。这是为什么呢?

接下来,小编和大家一起分析这个虚拟继承的底层

2.3 虚拟继承的底层

在分析上面问题之前,我们先来谈一个单独继承的模型!

  • 注意到上面的studentteacher字段:他们的首地址的四字节(X86环境下)处都是一个“看起来的乱码。这就是C++解决虚拟继承的关键

问题又来了:当我们这样处理的时候,对于这样的场景我们应该如何去查找对应的分支处基类的字段呢?

例3:

#include<iostream>
using namespace std;class A
{
public:int _a = 1;
};class B : virtual public A
{
public:int _b = 2;
};class C : virtual public A
{
public:int _c = 3;
};class D : public B, public C
{
public:int _d = 4;
};int main()
{D d;B* ptr1 = &d;ptr1->_a; //语句一C* ptr2 = &d;ptr2->_a; //语句二return 0;
}

上面的代码中,语句一和语句二是如何找到这个共同的_a呢?
我们稍后解决……

2.3.1 单虚拟继承

虚拟继承都是用于多继承的情况下,解决数据冗余问题。不过小编为了从简单了解,所以直接采用单继承的虚拟继承原理开始谈起。

我们从简单的一个单继承的虚拟继承模型开始谈起。

例4:

#include<iostream>
using namespace std;
class A
{
public:int _a = 1;
};class B : virtual public A
{
public:int _b = 2;
};int main()
{B b;cout << "DEBUG" << endl;return 0;
}

来看内存存储

在这里插入图片描述

  • 这就是一个指针字段。地址为:0x00299b30

  • 来看这个字段包含的东西:
    在这里插入图片描述

  • 进一步思考:0x012FFA34 - 0x012FFA2C == 8十六进制

  • 答案呼之欲出了:

    这个字段就是一个指针。这个指针指向了一个表,这个表存放着所继承下来的父类存放在当前指针位置的偏移量

2.3.2 虚拟继承 — 虚基表、虚基表指针

接下来为上面的答案揭秘

  1. 实际上,通过virtual虚拟继承,我们将分支的父类的成员变量放在了一个公共的区域
  2. 对于原来的对象,我们需要找到这个公共的区域是需要通过偏移量的。而这个偏移量被存储在了一个公共的区域形成了一张表,该表被称为:虚基表。而这个首字段的指针指向这个表,被称为:虚基表指针
  3. 这个存储模型如下:

在这里插入图片描述

这就是上面的结果!!

现在我们了解到了单继承下的虚拟继承行为,那么多继承就是利用了这样的方式来解决问题!我们迁移到多继承的条件下

2.3.3 菱形虚拟继承的底层分析

现在回到例3的场景

#include<iostream>
using namespace std;class A
{
public:int _a = 1;
};class B : virtual public A
{
public:int _b = 2;
};class C : virtual public A
{
public:int _c = 3;
};class D : public B, public C
{
public:int _d = 4;
};int main()
{D d;B* ptr1 = &d;ptr1->_a; //语句一C* ptr2 = &d;ptr2->_a; //语句二return 0;
}

我们如何来分析,编译器是如何通过语句一、语句二来找到_a的呢?

  • 小编直接给出这个问题的内存存储结构抽象模型

    • 内存存储结构:
      在这里插入图片描述
    • 抽象模型
      在这里插入图片描述

    箭头代表从指针开始找到虚基表,再找到基类成员

	D d;B* ptr1 = &d;ptr1->_a; //语句一C* ptr2 = &d;ptr2->_a; //语句二
  • 再来回答上面的问题:语句一(ptr1->_a;)和语句二(ptr2->_a;)是如何找到对应的_a成员的呢?

    • 由于ptr1,ptr2B类和C类的指针。访问_a的时候,识别到虚基表指针,找到虚基表指针指向的虚基表,得到A类对象的偏移量,最后通过偏移量找到A类对象距离当前指针位置的地址,最后成功访问到A类对象。

现在,我们再来看之前的例2

#include<iostream>
#include<string>
using namespace std;
class person
{
public://……
protected:string _name = "zhangsan";int _ID = 1;
};class student : virtual public person
{
public:void study(){}
protected:int _sid = 2;
};class teacher : virtual public person //虚拟继承
{
public:void teach(){}
protected:int _tid = 3;
};class assistant : public student, public teacher 
{
public:void check(){}
protected:int _age = 4;
};int main()
{assistant a;cout << "DEBUG" << endl;return 0;
}
你能说清楚这个例子的存储模型是如何存储的吗?

在这里插入图片描述

2.4 小结

  • 首先来小结一下菱形虚拟继承到底是如何进行的:

    1. 将公共继承的父类,存放到一个共同的区域
    2. 将原有的父类内容改作一个虚基表指针
    3. 这个虚基表指针指向这个共同区域,虚基表
    4. 这个虚基表存放在当前对象到父类对象的偏移量
    5. 通过偏移量找到父类对象。
    6. 访问父类对象。

小编再次提出一个问题:

  • 这个虚基表和偏移量的意义是什么呢?

    1. 对于不同的同类对象,可以共用一张虚基表
    2. 来看下面的输出:
      在这里插入图片描述
      但是如果你细心的话,你会发现:在D这个对象中BC对象都是分裂的!即所占用的空间不是连续的空间了
      所以如果不通过偏移量这样的一个统一的模型,编译器是无法找到对应的A类对象是在那个位置的,也就是无法访问成功。
      结论:偏移量为访问基类成员提供了统一的方法:先找偏移量再找成员

完。

希望这篇文章能够帮助你!

http://www.dtcms.com/a/273082.html

相关文章:

  • Java多线程:核心技术与实战指南
  • 鸿蒙智行6月交付新车52747辆 单日交付量3651辆
  • 如何设计一个登录管理系统:单点登录系统架构设计
  • 无法识别的USB设备怎么解决 一键修复
  • JAVA JVM对象的实现
  • [2025CVPR]CCFS:高IPC数据集蒸馏的课程式粗细筛选技术解析
  • OkHttp 的拦截器有哪些
  • 苍穹外卖—day1
  • 树莓派5+Ubuntu24.04 LTS ROS2 N10P镭神激光雷达 保姆级教程
  • Linux Ubuntu 安装 AnythingLLM
  • STM32中DMA(直接存储器访问)详解
  • [Meetily后端框架] AI摘要结构化 | `SummaryResponse`模型 | Pydantic库 | vs marshmallow库
  • Spring Boot 与 Docker 的完美结合:容器化你的应用
  • 时序数据库InfluxDB
  • Flink 2.0 DataStream算子全景
  • MBSE工具+架构建模:从效率提升到质量赋能
  • 智能Agent场景实战指南 Day 9:市场营销Agent构建策略
  • 粗排样本架构升级:融合LTR特征提升模型性能的技术实践
  • 车载诊断架构 --- DTC深层次参数信息(e.g. ComfirmDTCLimit unconfirmDTCLimit)
  • 第10章 语句 笔记
  • 轻松使用格式工厂中的分离器功能来分离视频和音频文件
  • 噪音到10µVRMS 以下的DC-DC:TPS62913
  • 实现一个点击输入框可以弹出的数字软键盘控件 qt 5.12
  • Java 单例类详解:从基础到高级,掌握线程安全与高效设计
  • wpf使用webview2显示网页内容(最低兼容.net framework4.5.2)
  • C Primer Plus 第6版 编程练习——第8章
  • python语言编程文件删除后的恢复方法
  • ARM环境上 openEuler扩展根盘并扩展到根分区中
  • 小架构step系列10:日志热更新
  • HTTP核心基础详解(附实战要点)