C++进阶—继承(下)菱形(虚拟)继承分析虚拟继承存储对象模型

news2024/9/21 3:03:57

 

目录

0. 前言

1. 普通多继承下,基类和派生类复制转换底层细节(切片)

2. 多继承下的复杂菱形继承

3. 菱形虚拟继承(虚基类)重点

3.1 菱形非虚拟继承对象存储模型

3.2 菱形虚拟继承对象存储模型

3.3 虚拟继承对象存储模型

3.4 多对象继承关系分析其虚基类&虚拟化继承位置

5. 继承的总结和反思


0. 前言

这篇文章主要接上篇文章,从更深层次理解普通继承切片切割以及虚拟继承切片切割,从底部虚拟内存分析,以及分析C++多继承带来的一些问题,和C++解决多继承带来问题采取的方式,并从底层内存观察其逐步实现及原理,最终更深层次感受多继承!并从软件工程分析继承和组合两个概念!!!

1. 普通多继承下,基类和派生类复制转换底层细节(切片)

派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。

那么编译器在普通继承下,处理时如何切片,由上面一题目,看内存分析如下:

可知在编译阶段,对象实例化时,实例化对象只存储成员变量,而成员函数会根据其模板参数、所属类域存储在公共代码段,以便进行调用!

在对象实例化时,通过调试观察其虚拟内存得出实例化对象会提前在栈区或者堆区开辟好空间,其成员变量在栈区先使用低地址在使用高地址,(如结构体,便于通过偏移量计算成员位置),因此可以绘制出对象d实例化时,内存存储数据模型:

        而通过上篇文章可知,默认构造,先构造其基类,在构造子类,而对于多继承其根据继承顺序依次构造,因此先实例化_b1,在实例化_b2,其次实例化_d,因此可以看出由低地址到高地址使用实例化!!!

使用调试,观察其切割切片方式:

  1.  将&d派生类Derive地址赋值给Base* p1基类指针,此时便会进行切片,切割使用_b1,所以此时p1指向的地址便是原类Derive实例化对象d的地址,但是由于其进行切片,向后只能访问其基类大小个字节,只能访问_b1
  2. 将&d派生类Derive地址赋值给Base* p2基类指针,此时便会进行切片,切割使用_b2,由于Base2实例化在中间,因此切片时从_b2地址进行切片赋值,向后只能访问其基类大小个字节,只能访问_b2
  3. 将&d派生类Derive地址赋值给其所属类型的指针变量,此时未发生切片,p3所指向的地址便是整个实例化对象的地址,所以p3的地址便是最开始的地址!!!

最终结果:

 p3和p1虽然向后访问数据的偏移量不同,但是所指向同一空间的起始地址&d,_d1,而p2指向同一空间基于Base2实例化的地址,即_b2地址,再根据派生类成员变量内存分布,即可以得出上图结果!!!

总结:

  1. 对于派生类引用赋值给基类,底层是对指针和解引用的封装,含义不同,内存操作相同!!!
  2. 对于派生类直接赋值给基类,会直接进行切割赋值

2. 多继承下的复杂菱形继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

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

什么是二义性:(多继承和菱形继承都会导致二义性)

 如上图:在多继承中class A和class B若是有多个相同数据成员,此时对于class C而言同名的数据成员会产生二义性的问题,需要通过类域对其进行区分,如下代码:

	class A {
	public:
		A() :_a(1), _same(10) {

		}

		int _a;
		int _same;
	};
	class B {
	public:
		B() :_b(1), _same(1) {

		}
		int _b;
		int _same;
	};
	class C : public A,public B{
	public:
		void Print() {
			//cout << _same << endl;//err _same无法确定是属于哪个类,二义性
		}
		int _c;
	};
	void test() {
		C c;
		//cout << c._same << endl;//err _same无法确定是属于哪个类,二义性
	}

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。 在Assistant的对象中Person成员会有两份,除了二义性,当间接相同基类成员变量如果占用空间过大,也会浪费内存空间。

	class Person
	{
	public:
		string _name; // 姓名
	};

	class Student : public Person
	{
	public:
		int _num; //学号
	};

	class Teacher : public Person
	{
	public:
		int _id; // 职工编号
	};

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

	void Test()
	{
		// 这样会有二义性无法明确知道访问的是哪一个
		Assistant a;
		//a._name = "peter";
		// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
		a.Student::_name = "xxx";
		a.Teacher::_name = "yyy";
	}

我们发现在类Assistant中存在两份的基类Person,分别Assistant存在类Student和类Teacher中,如果数据多则严重浪费空间,也不利于维护, 我们引用基类Person中的数据还需要通过域运算符进行区分。

为了解决以上问题,C++提供了虚基类,也叫做虚拟继承的概念

3. 菱形虚拟继承(虚基类)重点

为了解决上述菱形继承带来的问题,C++中引入了虚基类,其作用是在间接继承共同基类时只保留一份基类成员,虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。

	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; // 主修课程
	};
	void Test()
	{
		Assistant a;
		a._name = "peter";
	}

虚拟继承解决数据冗余和二义性的原理分析

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

	void Test() {
		D d;
		d.B::_a = 1;
		d.C::_a = 2;
		d._b = 3;
		d._c = 4;
		d._d = 5;
	}

分析上述菱形非虚拟继承对象存储模型,如下:

实例化对象d进行了多继承,其基类B、C为非虚拟继承:

  1. 首先根据其继承的第一个类调用基类的构造,即调用B的构造,B继承了A,因此B再次调用基类A的构造
  2. 其实根据其继承的第二个类调用基类的构造,即调用C的构造,C继承了A,因此C再次调用基类A的构造
  3. 再根据vs内存监视窗口,其虚拟内存,观察其更改顺序,即可的到其非虚拟菱形继承对象存储模型
  4. 从对象存储模型可观察到非虚拟菱形继承中类A,分别在B类C类各有一份,因此造成二义性和数据冗余

3.2 菱形虚拟继承对象存储模型

为了研究虚拟继承原理,再给出一个简化的菱形虚拟继承体系,再借助内存窗口观察对象成员的模型:

	class A {
	public:
		int _a;
	};

	class B : virtual public A{
	public:
		int _b;
	};
	class C : virtual public A{
	public:
		int _c;
	};

	class D :public B, public C {
	public:
		int _d;
	};

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

分析上述菱形虚拟继承对象存储模型,如下:

 实例化对象d进行了多继承,其基类B、C为虚拟继承,类A为虚基类:

1. 首先根据其继承的第一个类调用其基类的构造,即调用B的构造,B虚拟继承了A,B中指向A的数据就变成了虚基类表的指针,该指针指向一个虚基类表,虚基类表中存储了该指针到公共数据所在内存的偏移量,然后构造B的成员

2. 其次根据其继承的第二个类调用其基类的构造,即调用C的构造,C虚拟继承了A,C中指向A的数据就变成了虚基类表的指针,该指针指向一个虚基类表,虚基类表中存储了该指针到公共数据所在内存的偏移量,然后构造C的成员

3. 最后构造D类的成员变量,此时,实例化对象d,d对象中只有一份数据A,以及其两个基类的虚基类表指针

 

简单来说:

  • 如果使用非虚拟继承,那么D会从B、C那里继承两份相同的数据
  • 如果使用虚拟继承,那么那两份相同的数据在D类对象中只会存在一份。而D从B、C那里继承的是它们独有的数据以及B和C的虚基类表指针。通过它们各自的虚基类表指针,就可以获取该指针与那份公共数据存储位置的偏移量,进而可以访问它。

3.3 虚拟继承对象存储模型

当不是菱形状态传递时,有关继承virtual,其虚拟继承的对象存储模型:

	class A {
	public:
		int _a;
	};

	class B : virtual public A{
	public:
		int _b;
	};
	class C : virtual public A{
	public:
		int _c;
	};

	class D :public B, public C {
	public:
		int _d;
	};

	void func(B* bb) {
		cout << bb->_a << endl;
	}
	void Test() {
		D d;
		d.B::_a = 1;
		d.C::_a = 2;
		d._a = 0;
		d._b = 3;
		d._c = 4;
		d._d = 5;
		B b;
		func(&d);
		func(&b);
	}

只要是虚拟继承,编译器都会按照虚基类表指针进行编译,才能保证其子类的传递正确性。

3.4 多对象继承关系分析其虚基类&虚拟化继承位置

当存在多边形对象继承时,其虚基类,一般为最终继承关系中会重复的成员变量所属的类!!!

 从上图继承关系可知,构成菱形继承,E类中会造成A类成员变量二义性,因此,需要将类B、C定义虚拟继承解决其二义性和数据冗余。

5. 继承的总结和反思

  1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱 形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设 计出菱形继承。否则在复杂度及性能上都有问题。
  2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
  3. 继承和组合
  •         public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
  •         组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
  •         优先使用对象组合,而不是类继承 。
  •         继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称 为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
  •         对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象 来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复 用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

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

	// Car和BMW Car和Benz构成is-a的关系
	class Car {
	protected:
		string _colour = "白色"; // 颜色
		string _num = "陕ABIT00"; // 车牌号
	};

	class BMW : public Car {
	public:
		void Drive() { cout << "好开-操控" << endl; }
	};

	class Benz : public Car {
	public:
		void Drive() { cout << "好坐-舒适" << endl; }
	};

	// Tire和Car构成has-a的关系

	class Tire {
	protected:
		string _brand = "Michelin";  // 品牌
		size_t _size = 17;         // 尺寸

	};

	class Car {
	protected:
		string _colour = "白色"; // 颜色
		string _num = "陕ABIT00"; // 车牌号
		Tire _t; // 轮胎
	};

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Redis原理 - 内存策略

原文首更地址&#xff0c;阅读效果更佳&#xff01; Redis 本身是一个典型的 key-value 内存存储数据库&#xff0c;因此所有的 key、value 都保存在之前学习过的 Dict 结构中。不过在其 database 结构体中&#xff0c;有两个 Dict &#xff1a;一个用来记录 key-value&#xf…

【计算机网络详解】——软件定义网络SDN(学习笔记)

目录 &#x1f552; 1. 概念&#x1f552; 2. OpenFlow 协议 &#x1f552; 1. 概念 软件定义网络&#xff08;Software Defined Network&#xff0c;SDN&#xff09;的概念最早由斯坦福大学的Nick McKeown教授于2009年提出。SDN最初只是学术界讨论的一种新型网络体系结构。SD…

基于JAVA实现的简易学生信息管理系统(附源码)

一、前言 最近在学习JAVA&#xff0c;这几天跟着网上的视频学完基础知识之后&#xff0c;做了一个学生信息管理系统&#xff0c;写的比较普通&#xff0c;没太大亮点&#xff0c;希望可以给初学者一些参考经验&#xff0c;另外&#xff0c;如有不恰当的地方还请各位指正&am…

论文解读:End-to-End Object Detection with Transformers

发表时间&#xff1a;2020 论文地址&#xff1a;https://arxiv.org/pdf/2005.12872.pdf 项目地址&#xff1a;https://github.com/facebookresearch/detr 提出了一种将对象检测视为集合预测问题的新方法。我们的方法简化了检测流程&#xff0c;有效地消除了许多手工设计的组件…

解决跨域问题的两种方案

说明&#xff1a;跨域是指&#xff0c;在A向B发送请求时&#xff0c;如果A和B的协议、端口号和域名有一个不相同。跨域问题是指&#xff0c;浏览器出于安全&#xff0c;会阻止跨域的异步请求&#xff08;如Ajax&#xff09;&#xff0c;而在分布式的开发环境下&#xff0c;跨域…

ChatGPT在媒体与娱乐领域的沉浸式场景:虚拟主持人和创意助手的新应用探索

第一章&#xff1a;引言 在当今数字化时代&#xff0c;人工智能技术在媒体与娱乐领域的应用日益广泛。ChatGPT作为一种先进的自然语言处理模型&#xff0c;具备强大的对话生成能力和创造力&#xff0c;为媒体与娱乐产业带来了新的创意和可能性。本文将探讨ChatGPT在媒体与娱乐…

学了那么长时间的编程,C语言的各种操作符都搞不懂?点开这里有详细的介绍—>

目录 前言 一、原码、反码、补码的基础概念 1.原码 2.反码 3.补码 二、原码、反码、补码的计算方法 1.原码 2.反码 3.补码 三、算术操作符 四、移位操作符 1. 左移操作符 移位规则&#xff1a; 2. 右移操作符 移位规则&#xff1a; &#xff08;1&#xff09; …

MySQL 中Relay Log打满磁盘问题的排查方案

MySQL 中Relay Log打满磁盘问题的排查方案 引言&#xff1a; MySQL Relay Log&#xff08;中继日志&#xff09;是MySQL复制过程中的一个重要组件&#xff0c;它用于将主数据库的二进制日志事件传递给从数据库。然而&#xff0c;当中继日志不断增长并最终占满磁盘空间时&…

【openGauss数据库审计项配置审计日志维护】--略有小成

【openGauss数据库审计项配置&审计日志维护】--略有小成 &#x1f53b; 一、openGauss数据库审计&#x1f530; 1.1 关于openGauss审计功能&#x1f530; 1.2 openGauss审计功能开启&#x1f530; 1.3 配置具体的审计项 &#x1f53b; 二、查看审计结果&#x1f53b; 三、维…

day13_类中成员之一:构造器

由来 我们发现我们new完对象时&#xff0c;所有成员变量都是默认值&#xff0c;如果我们需要赋别的值&#xff0c;需要挨个为它们再赋值&#xff0c;太麻烦了。我们能不能在new对象时&#xff0c;直接为当前对象的某个或所有成员变量直接赋值呢。可以&#xff0c;Java给我们提…

详解c++---map和set的封装

目录标题 前言红黑树的基本代码map和set的封装红黑树迭代器红黑树迭代器- -begin和end函数代码测试const迭代器方括号的实现 前言 通过之前的学习我们知道set容器中存储的数据是k&#xff0c;map容器中存储的数据是k和v&#xff0c;但是这两个容器底层都是通过红黑树来进行实现…

blfs:为lfs虚拟机增加桌面01

vmware中克隆一份lfs&#xff0c;扩大硬盘分区再操作。 1、硬盘拓展容量&#xff0c;分区挂载到/home&#xff08;已有的大小在后面编译桌面系统会捉襟见肘&#xff09; 使用fdisk进行分区 fdisk /dev/sda 执行p w分区并保存 mkfs -v -t ext4 /dev/sda4 转ext4格式 让/…

uniapp中使用mixins(混入)

mixins 选项接收一个混入对象的数组。这些混入对象可以像正常的实例对象一样包含实例选项&#xff0c;这些选项将会被合并到最终的选项中&#xff0c;使用的是和 Vue.extend() 一样的选项合并逻辑。也就是说&#xff0c;如果你的混入包含一个 created 钩子&#xff0c;而创建组…

如何使用ffmpeg将BDMV(m2ts)转换成MKV、MP4等其他格式的文件

BDMV 是蓝光碟使用的格式。这种格式没有办法使用播放软件播放&#xff0c;必须要用硬盘播放器&#xff0c;也就是专门的设备。但是最经典的 ffmpeg 可以将其转换成其他格式&#xff0c;并且保持相同的码率和清晰度&#xff0c;这样就可以很方便的查看了。 本文使用 macOS 进行…

加速度计的原理与应用

什么是加速度计 加速度计是一种传感器&#xff0c;可以测量物体所受加速的大小和方向。 加速度计的工作原理 传统加速度计利用质量和弹簧的相互作用来感应加速度&#xff0c;当物体收到加速度时&#xff0c;弹簧会发生变形&#xff0c;通过衡量这种变形来测量加速度的大小。 …

postgresql | 数据库| 生成2000W条的简单测试表

前言&#xff1a; 数据库学习的过程中&#xff0c;很可能需要数据量比较大的表来进行模拟测试&#xff0c;那么&#xff0c;测试表的创建需要遵循的是贴近实际的生产环境&#xff0c;尽量的模仿实际的生产环境。 因此&#xff0c;学习数据库的时候&#xff0c;快速的创建一个…

chatgpt赋能python:Python求单词长度:基于字符串操作的简单实现

Python求单词长度&#xff1a;基于字符串操作的简单实现 Python作为一种广泛应用于各个领域的编程语言&#xff0c;其强大的字符串操作功能在文本处理中经常被使用。本篇文章将介绍基于Python的字符串操作实现单词长度的方法。 什么是单词长度 在文本处理中&#xff0c;单词…

【Redis】1、学习 Redis 的五大基本数据类型【String、Hash、List、Set、SortedSet】

目录 一、NoSQL 和 SQL 区别二、认识 Redis三、Redis 的数据结构介绍四、Redis 通用命令五、String 类型六、key 的格式七、Hash&#xff08;散列&#xff09;类型八、List 类型九、Set 类型十、SortedSet 类型&#xff08;可排序&#xff09; 一、NoSQL 和 SQL 区别 二、认识 …

macOS Sonoma编译OpenCV源码输出IOS平台库

1.macOS下载并编译OpenCV源码: 克隆源码: 主仓: git clone https://github.com/opencv/opencv.git 扩展仓: git clone https://github.com/opencv/opencv_contrib.git 编译xcode源码需要CMake与XCode命令行工具 确认已安装CMake 确认已安装XCode 安装xcode command l…

UNZIP

目录 搭建环境 做题开始 通过ln直接创建 通过mkdir 后进行ln 搭建环境 这次是在自己的靶机环境里面搭建 cd /var/www/html index.html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</ti…