【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理

文章目录

  • 1. 单继承
  • 2. 多继承
  • 3. 菱形继承
    • 3.1 菱形继承的问题——数据冗余和二义性
    • 3.2 解决方法——虚拟继承
    • 3.3 虚拟继承的原理
  • 4. 继承和组合
  • 5. 继承的反思和总结

1. 单继承

在上一篇文章中,我们给大家演示的其实都是单继承。

单继承的概念:

单继承:一个子类只有一个直接父类的继承关系为单继承
在这里插入图片描述

2. 多继承

然后呢C++里面还支持多继承,那什么是多继承呢?

一个子类有两个或以上直接父类时称这个继承关系为多继承
在这里插入图片描述
比如一个类表示汽车,另一个类表示飞机。现在你希望创建一个新的类,使得它既可以像汽车一样在地上跑,又可以像飞机一样在天上飞,即这个新的类继承这两个基类的属性和行为,同时拥有汽车和飞机的特性。那这就是一个多继承。

3. 菱形继承

多继承也不难理解,但是有时候可能会引发一些难搞的情况。

比如,多继承就有可能导致菱形继承的出现:

菱形继承是多继承的一种特殊情况。

那顾名思义,菱形继承就是继承关系近似呈一个菱形形状,比如像这样:

在这里插入图片描述
简单解释一下,首先这里有一个Person类,然后Student继承了Person,Teacher也继承了Person。
然后,又有一个Assistant(助教)类即继承了Student,又继承了Teacher。
那此时它们的继承关系就呈一个菱形状。

那菱形继承会导致什么问题呢?

3.1 菱形继承的问题——数据冗余和二义性

菱形继承就会存在一个数据冗余和二义性的问题

从下面的对象成员模型构造可以看出,在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; // 主修课程
};

在这样一个菱形继承体系里面,就会存在如下一些问题:

首先呢,由于Student和Teacher都是继承Person,所以它们都拥有Person的_name属性,然后呢,Assistant又同时继承了Student和Teacher,所以就会导致在Assistant里面有两份_name,那这就导致了一个数据冗余的问题。
在这里插入图片描述
由于Assistant里面有两个_name,一个继承自Student,一个继承自Teacher,所以在访问的时候就会发生歧义,我们把它叫做数据二义性
在这里插入图片描述
我们现在想给Assistant的类对象a的_name成员赋值,那这里就无法确定你访问的是哪一个。

那有办法解决这个问题吗?

当然也有办法,我们可以通过显式指定访问哪个父类的成员来一定程度的解决二义性的问题
在这里插入图片描述
但是数据冗余的问题依然存在。
而且我们这里方便演示只给Person搞了一个成员_name,如果再多一些属性,比如住址、电话、年龄,性别等,那这样是不是都出现两份了。

那为了更好的解决菱形继承导致的数据冗余和二义性的问题,C++就引入了虚拟继承…

3.2 解决方法——虚拟继承

C++引入了虚拟继承可以解决菱形继承的二义性和数据冗余的问题

那虚拟继承是怎样的呢?

虚拟继承要用到一个新的关键字——virtual(虚拟的)
那怎么做呢?
在这里插入图片描述
在这里插入图片描述
给继承关系中第二层的类增加一个关键字virtual就行了。

然后就可以了吗?我们来看一下:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这里调式窗口看起来还是多个,但其实它们是同一个(这里只是调试窗口展示出来的效果)
在这里插入图片描述
我们后面也会给大家讲一下底层的原理。

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。
如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。

3.3 虚拟继承的原理

为了研究虚拟继承的原理,我们下面给出一个简化的菱形继承的继承体系,再借助内存窗口(因为监视窗口已经看不出来底层真实的样子了)观察对象成员的模型

现在我们给出这样一个继承体系:

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

在这里插入图片描述

那下面我们就一起来通过内存窗口分析一下虚拟继承的原理。

首先我们先来观察一下不使用虚继承时菱形继承底层是什么样子的:

在这里插入图片描述
在这里插入图片描述
现在我创建一个D类的对象d,把它所有的成员(自己的包括继承下来的),这里我们给的都是整型(方便观察),将他们置成1到5的数值。
在这里插入图片描述
下图是菱形继承的内存对象成员模型:这里可以看到数据冗余在这里插入图片描述

那我们接下来看看虚继承是怎么解决这个问题的:

在这里插入图片描述
同样的程序,我们再来观察内存空间:
在这里插入图片描述
我们发现此时整个对象的模型已经发生改变了。
我们看到,原本BC里面都存有一份_a,但是现在_a只有一个,而且单独放在最后面,那此时d对象中就只有一个_a成员了,就不存在数据冗余了,访问的时候也没有二义性了。
但是,此时BC里面原本应该存_a的位置存的是个什么东西呢?
是随机值吗?
看着也不像啊。
🆗,那告诉大家这里面存的其实是两个地址或者说指针,那它们指向的空间又存的是什么,我们可以再开两个内存窗口观察一下(注意vs上是小端存储)
在这里插入图片描述
我们观察这两个指针指向的内存空间,发现它们指向的当前位置那一个字节都是0,但是下一个字节都是一个确定的数值,一个是20(窗口显示的16进制,我们转成10进制),一个是12

那存的这些数字又分别代表什么呢?

其实仔细观察可以发现原本BC中应该存_a的位置和现在_a所在的位置,它们之间的偏移量(相对距离)就是20和12!!!
因为我们现在设置的一行刚好4个字节
在这里插入图片描述

所以,它底层原理是这样的:

原来B里面有一个_a,C里面有一个_a,这就造成了数据冗余和访问时的二义性。
所以要解决这个问题,就要只存一个_a,那就不能存到原来的位置了。
怎么办?
它放到了一个公共的位置(这个位置的_a同时属于BC两个类),那怎么找到这个_a存放的位置呢?
原来存放_a的位置就存了两个指针(叫做虚基表指针),它们分别指向一块区域(我们把它叫做虚基表),这里面就存储了原来_a在BC中的存储位置到现在_a位置的一个偏移量,通过这个偏移量就可以找到现在_a所在的位置。

那大家可能有这样的疑问,在这里也提一下:

那大家可能会想,为什么不直接存_a的地址呢?为什么还要存一个指针,通过指针去找偏移量,再通过偏移量找_a。
🆗,我们上面也说了,指针其实指向一张表,它其实指向的并不是一个位置,而是一块区域,这里面可能存了多个有用的值,一般这种我们把它叫做表(在这里名字为虚基表),另外我们其实也发现这个偏移量并没有存在指向的第一个位置
在这里插入图片描述
第一个位置是0,偏移量在后面放。
那第一个位置其实是空出来有其他用处,跟后面的多态有关系。

另外呢:

其实这里D的上一层比如说B就也是这种结构
在这里插入图片描述
因为他这里为了保持一个统一处理,正常情况下B只继承A,是不会出现数据冗余的,但这里做了统一处理。

那什么情况下会去使用偏移量找这个公共的_a呢?

在这里插入图片描述
大家看这种场景
这个是我们上一篇文章讲过的赋值转换嘛,正常情况下我们可以认为它进行一个切片嘛,把d里面属于B类的那一部分直接切出来赋给b就可以了。
但是现在虚拟继承这种情况,b里面还有从A类继承下来的_a成员是不是不在B里面啊,而是单独放到了外面,那此时要找这个_a是不是的通过虚基表指针指向的虚基表里面的偏移量找啊。

当然对于我们当前举的这个例子来说

在这里插入图片描述
我们的A这个类搞的比较小,这样一看虽然解决了数据冗余的问题,但反而多费了4个字节的空间。
但是如果A这个类比较大的话,这样处理的优势就出来了,解决了数据冗余的问题,而且节省了很多空间。

那这里对于上面的那个Person的菱形虚拟继承体系我们也给了一个原理解释图,大家可以看一下:

在这里插入图片描述
在这里插入图片描述

4. 继承和组合

这是继承

在这里插入图片描述
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。基类的内部细节对子类可见。

除了继承还有一种关系叫做组合。

组合呢是这样的:

在这里插入图片描述
其实就是一个类用另一个类的类对象作为其成员。
组合其实也是一种复用
组合是一种has-a的关系。假设D组合了C,每个D对象中都有一个C对象。C对象的内部细节对D是不可见的。

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

对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

5. 继承的反思和总结

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

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

  3. 优先使用对象组合,而不是类继承 。
    实际中尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。
    类之间的关系如果可以用继承,也可以用组合,那就用组合。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/8818.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Flutter如何获取屏幕的分辨率和实际画布的分辨率

Flutter如何获取分辨率 在Flutter中,你可以使用MediaQuery来获取屏幕的分辨率和实际画布的分辨率。 要获取屏幕的分辨率,你可以使用MediaQuery.of(context).size属性,它返回一个Size对象,其中包含屏幕的宽度和高度。下面是一个获…

POSTGRESQL SQL 执行用 IN 还是 EXISTS 还是 ANY

开头还是介绍一下群,如果感兴趣polardb ,mongodb ,mysql ,postgresql ,redis 等有问题,有需求都可以加群群内有各大数据库行业大咖,CTO,可以解决你的问题。加群请联系 liuaustin3 ,在新加的朋友会分到3群(共…

SQL频率低但笔试会遇到: 触发器、索引、外键约束

一. 前言 在SQL面笔试中,对于表的连接方式,过滤条件,窗口函数等肯定是考察的重中之重,但是有一些偶尔会出现,频率比较低但是至少几乎会遇见一两次的题目,就比如触发器,索引和外键约束&#xff0…

Spring源码解析(二):bean容器的创建、默认后置处理器、扫描包路径bean

Spring源码系列文章 Spring源码解析(一):环境搭建 Spring源码解析(二): 目录 一、Spring源码基础组件1、bean定义接口体系2、bean工厂接口体系3、ApplicationContext上下文体系 二、AnnotationConfigApplicationContext注解容器1、创建bean工厂-beanFa…

【Nginx05】Nginx学习:HTTP核心模块(二)Server

Nginx学习:HTTP核心模块(二)Server 第一个重要的子模块就是这个 Server 相关的模块。Server 代表服务的意思,其实就是这个 Nginx 的 HTTP 服务端所能提供的服务。或者更直白点说,就是虚拟主机的配置。通过 Server &…

iview切换Select时选项丢失,重置Seletc时选项丢失

分析原因 在旧版本的iview中如果和filterable一起使用时,当值清空选项或者使用重置按钮清空时选项会丢失。 解决方式一 把去掉filterable 解决方式二 使用ref,调用clearSingleSelect()方法清空 ref"perfSelect" this.$refs.perfSelect.c…

Java链式编程

一、链式编程 1.1.释义 链式编程,也叫级联式编程,调用对象的函数时返回一个this对象指向对象本身,达到链式效果,可以级联调用。 1.2.特点 可以通过一个方法调用多个方法,将多个方法调用链接起来,形成一…

UE4中创建的瞄准偏移或者混合空间无法拖入动画

UE4系列文章目录 文章目录 UE4系列文章目录前言一、解决办法 前言 UE4 AimOffset(瞄准偏移)动画融合时,AimOffse动画拖入不了融合框的解决办法,你会发现动画无法拖入到融合框,ue4编辑器提示“Invalid Additive animation Type”,…

C#核心知识回顾——10.List、Dictionary、数据结构

1.List List<int> list new List<int>(); List<String> strings new List<String>();//增list.Add(0);list.Add(1);List<int> ints new List<int>();ints.Add(0);list.AddRange(ints);//插入list.Insert(0, 1);// 位置0插入1//删//1.移…

使用GPIO来模拟UART

前言 最近在看一些秋招的笔试和面试题&#xff0c;刚好看到一个老哥的经验贴&#xff0c;他面试的时候被问到了如果芯片串口资源不够了该怎么办&#xff1f;其实可以用IO口来模拟串口&#xff0c;但我之前也没有具体用代码实现过&#xff0c;借此机会用32开发板上的两个IO口来…

从0开始,手写MySQL数据管理器DM

说在前面 从0开始&#xff0c;手写一个MySQL的学习价值在于&#xff1a; 可以深入地理解MySQL的内部机制和原理&#xff0c;MySQL可谓是面试的绝对重点和难点&#xff0c; 尼恩曾经指导过的一个7年经验小伙&#xff0c;凭借精通MySQL 搞定月薪40K。 从而更好地掌握MySQL的使…

前端实现pdf,图片,word文件预览

前端实现文件预览功能 需求&#xff1a;实现一个在线预览pdf、excel、word、图片等文件的功能。 介绍&#xff1a;支持pdf、xlsx、docx、jpg、png、jpeg。 以下使用Vue3代码实现所有功能&#xff0c;建议以下的预览文件标签可以在外层包裹一层弹窗。 图片预览 iframe标签能够将…