继承:复杂的菱形继承与虚继承

news2024/11/13 19:44:12

目录

前言

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

继承方式

virtual关键字

虚拟继承的原理

原理:

额外消耗:

构造顺序为什么是ABCD

不允许使用间接非虚拟基类原理

假设只有A B

为什么virtual加在B C中而不是D中?

如何实现一个不能被继承的类(继承无意义的类)

​编辑继承的总结和反思


前言

上文已经完成了前六种继承知识的介绍,下面是继承的复杂情况介绍。本文将着重介绍复杂的菱形继承与虚继承

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

继承方式

单继承:一个子类只有一个直接父类时称这个继承关系为单继承(箭头指向基类)
多继承:在第一个基类的后面加一个,即可连接
class D : public C, public B,public A这就是一种多继承

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

菱形继承的缺陷:存在数据的冗余和二义性的问题。那如何解决这几个问题呢?

virtual关键字

virtual关键字可以解决菱形继承中基类对象冗余的问题。

在C++中,virtual 关键字主要用于两个方面:定义虚函数和实现虚拟继承。以下是关于虚拟继承用途的详细介绍:

#include<iostream>
using namespace std;
class A {
public:
	A(const char* s) { cout << s << endl; }
	~A() {}
};

//class B :virtual public A
class B : public A
{
public:
	B(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};

class C : public A
{
public:
	C(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};

class D : public C, public B,public A
{
public:

	D(const char* s1, const char* s2, const char* s3, const char* s4)
		:B(s1, s2)
		, C(s1, s3)	
		,A(s1)		
		cout << s4 << endl;
	}
};


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

这就是一个菱形继承的典型实例。我们在初始化D时,A一共被调用了三次。因此存在大量的数据冗余与二义性问题。

由于A的冗余,我们需要把继承A的部分加上virtual,变成虚继承。使得最终只有一个A副本。

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

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

class D : public C, public B, virtual public A

继承A的部分都得加上virtual,这样只有一个A,共享一个A副本。

不允许一部分加一部分不加。原因是加上virtual之后,A变成虚拟基类(只允许存在一个A),但是后面有不是virtual的,这样就又产生了A对象,这就出现了矛盾

虚拟继承的原理

虚拟继承是C++中用于解决多继承时可能出现的菱形继承问题(即一个类多次继承自同一个基类)的一种机制。虚拟继承允许在继承层次中共享基类的一个实例,而不是为每个直接或间接的基类副本都保留一份基类实例。以下是虚拟继承产生额外消耗的原因及其原理:

原理:

  1. 共享基类实例: 在虚拟继承中,无论基类在继承层次中被继承多少次,都只存在一个共享的基类实例。这意味着所有继承了这个虚拟基类(被virtual继承的类)的派生类都会引用同一个基类部分

  2. 虚基类表: 为了实现这种共享,编译器会为每个含有虚基类的对象插入一个额外的指针,这个指针指向一个虚基类表(vtable)。虚基类表包含了虚基类的偏移量信息,用于在运行时调整指针,使得能够正确地访问共享的基类实例。

  3. 调整: 当通过指针或引用访问虚基类成员时,编译器生成的代码会使用虚基类表来调整指针,确保指向正确的共享基类实例。

这个 A 同时属于 B C ,那么 B C 如何去找到公共的 A 呢? 这里是通过了 B C 的两个指针,指
向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量
可以找到下面的 A

额外消耗:

  1. 空间开销

    • 虚基类指针:每个含有虚基类的对象都需要额外的空间来存储指向虚基类表的指针。
    • 虚基类表:需要额外的存储空间来维护每个对象的虚基类偏移信息。
  2. 时间开销

    • 访问调整:每次访问虚基类的成员时,都需要进行指针调整,这增加了访问时间。
    • 构造和析构:在构造和析构过程中,需要确保虚基类部分只被初始化和清理一次,这可能导致更复杂的构造函数和析构函数调用序列,从而增加时间开销。
  3. 复杂性开销: 虚拟继承增加了编译器实现的复杂性,可能导致生成的代码更加复杂,这可能会间接影响程序的性能。

总之,虚拟继承虽然解决了菱形继承问题,但其机制带来的额外空间和时间开销,以及在复杂性和性能上的潜在影响,使得程序员在考虑使用虚拟继承时需要权衡其利弊。在设计继承体系时,如果可以避免,通常推荐不使用虚拟继承。

构造顺序为什么是ABCD

当进行虚拟继承之后,虚拟基类必须被显式初始化(最终派生类也是如此),当进行虚拟继承之后,构造顺序变成ABCD这是为什么呢?

构造顺序:在虚继承中,虚拟基类的构造函数必须在所有其他基类之前被调用。这是因为在派生类中只有一个虚拟基类的实例,所以需要首先构建它
单一共享实例:虚拟继承确保 class A 在 class D 中只有一个共享的实例。因此,无论 class B 和 class C 如何继承 class A,class D 中只有一个 class A 的副本。
阻止重复初始化:如果不直接在 class D 的构造函数初始化列表中初始化 class A,那么 class B 和 class C 的构造函数可能会尝试初始化 class A,这会导致重复初始化错误。所以虚拟继承之后,必须在D中显式初始化A
明确初始化:由于 class B 和 class C 都虚拟继承自 class A,class D 需要确保 class A 的构造函数只被调用一次,并且是在正确的顺序下。因此,class D 必须在它的构造函数初始化列表中直接初始化 class A

		:B(s1, s2)
		, C(s1, s3)
		, A(s1)

在初始化列表中,虽然A在最后,但是列出来的顺序,并不是初始化的顺序!!!

不允许使用间接非虚拟基类原理

在菱形继承中,不允许使用间接非虚拟基类(这个基类是间接的、非虚拟的)

class D : public C, public B
{
public:

	D(const char* s1, const char* s2, const char* s3, const char* s4)
		:B(s1, s2)
		, C(s1, s3)
		, A(s1)
	{
		cout << s4 << endl;
	}
};

当我们去掉全部的虚拟继承之后,该构造函数会报错。因为A是间接、非虚拟继承的基类,不允许直接使用

如下就可以:

class D : public C, public B,public A
{
public:

	D(const char* s1, const char* s2, const char* s3, const char* s4)
		:B(s1, s2)
		, C(s1, s3)
		, A(s1)
	{
		cout << s4 << endl;
	}
};

原因是A是直接继承的类,虽然全都不是虚拟继承 ,结果就是多了几个A的副本。

满足了:不允许使用间接非虚拟基类(这个基类是间接继承的、非虚拟的)原理

假设只有A B

假设只有A B,B虚拟继承A之后,那么可以保证使用B时,正常编译,并保证存在A吗?


答案是是的,如果只有 class A 和 class B,且 class B 虚拟继承自 class A,那么可以保证在使用 class B 时,代码可以正常编译,并且 class B 的实例中将存在一个 class A 的实例。虚拟继承确保了即使在多继承的情况下,也只有一个共享的基类实例。
 


以下是代码示例:
class A {
public:
	A() { std::cout << "A constructor called" << std::endl; }
	~A() { std::cout << "A destructor called" << std::endl; }
};

class B : virtual public A {
public:
	B() { std::cout << "B constructor called" << std::endl; }
	~B() { std::cout << "B destructor called" << std::endl; }
};

int main() {
	B b; // 创建B的实例,这将自动创建一个A的实例
	return 0;
}

需要注意的是,虚拟继承会引入一些额外的开销,因为它需要额外的机制来保证只有一个基类实例,并且需要处理虚继承层次中的指针调整。因此,虚拟继承通常只在必要时使用

为什么virtual加在B C中而不是D中?
 

答案:

在C++中,虚拟继承关键字 virtual 应用于那些想要共享其基类实例的派生类而不是在最终的派生类上。这是因为虚拟继承的目的是确保在多重继承的情况下,最终的派生类只拥有一个共享的基类实例,而不管这个基类被继承了多少次

以下是为什么 virtual 关键字加在 B 和 C 中而不是 D 中的原因:

共享基类实例:当我们使用虚拟继承时,我们的目的是确保在最终的派生类中只有一个基类实例。为了达到这个目的,必须要在那些直接继承基类并且想要共享基类实例的派生类上使用 virtual 关键字。
控制共享:通过在 B 和 C 上使用 virtual 关键字,我们告诉编译器,无论 B 和 C 被如何继承,它们都应当共享它们共同的基类 A 的单一实例。如果我们在 D 上使用 virtual,这不会产生任何效果,因为 D 是最终的派生类,它并不继承其他基类来共享实例。
构造函数的调用:由于 B 和 C 虚拟继承自 A,它们的构造函数不会尝试创建 A 的实例。相反,它们会依赖最终的派生类(在这个例子中是 D)来初始化共享的 A 实例。因此,D 的构造函数负责初始化 A,并且必须确保只初始化一次
菱形继承问题:虚拟继承解决了菱形继承问题,即一个类通过多个路径继承自同一个基类。在这个例子中,D 通过 B 和 C 继承 A,形成一个菱形结构。为了确保 D 只有一个 A 的实例,B 和 C 必须虚拟继承 A。

如何实现一个不能被继承的类(继承无意义的类)

可以考虑,把基类构造函数私有即可(1.显式调用父类调不动 2.默认调用父类也调不动)因此建立派生类对象时,建立不成功。

当然C++11引入了final关键字,不允许该类被继承(继承时就会报错)。


继承的总结和反思

1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱
形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设
计出菱形继承。否则在复杂度及性能上都有问题。
2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
3. 继承和组合
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
// 组合
class C
{
public:
	void func()
	{}
protected:
	int _c;
};

class D
{
public:
	void f()
	{
		_c.func();
		//_c._a++;
	}
protected:
	C _c;
	int _d;
};

在D中包含C类。此时D只能访问C类的public成员(前提是C对D可见)

继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称
白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的
内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很
大的影响。派生类和基类间的依赖关系很强,耦合度高
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象
来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复
用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。
组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被
封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有
些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用
继承,可以用组合,就用组合。( 具体怎么用,就看是has-a还是is-a

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

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

相关文章

AtCoder ABC 359 F 题解

本题要看出性质并进行验证&#xff0c;程序难度低。&#xff08;官方 Editorial 似乎没有写证明过程&#xff1f;难道是过于显而易见了吗…&#xff09; 题意 给你一个数组 a a a&#xff0c;对于一棵 n n n 个节点的树 T T T&#xff0c; d i d_i di​ 为每个节点的度&am…

基于Java+SpringBoot+Vue的校园社团信息管理

基于JavaSpringBootVue的校园社团信息管理 前言 ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN[新星计划]导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;&公&粽&号 查找《智能编…

C++ 栈和队列的简单封装(9.3)

1.栈的封装 代码 #include <iostream>using namespace std;typedef int datatype; class Stack { private:datatype *data;int max_size; //栈的大小int the_top; //栈顶 public:Stack(){data new int[50];max_size 50;the_top -1;}Stack(int a){data n…

一个好用的Maven依赖冲突解决插件:Maven Helper

在项目开发&#xff0c;或项目Maven需要新增依赖、项目依赖组件升级时&#xff0c;经常会出现添加后&#xff0c;因为各个模块中有相同的依赖、不同的版本而导致依赖冲突&#xff0c;从而导致项目启动不起来&#xff0c;这种冲突非常恶心&#xff0c;因为是传递依赖所以会看不出…

【数据推荐】我国省市县三级的人口受教育状况数据(分年龄\性别\户籍)

人口数据是我们在各项研究中都经常使用的数据。之前我们为大家分享过基于《2020中国人口普查分县资料》整理的全国范围的第七次人口普查人口数据&#xff0c;具体包括如下8个分表&#xff08;均可查看之前的文章获悉详情&#xff09;&#xff1a; 表1&#xff1a;我国省市县三…

大二暑假去龙旗科技(上海)做了两个月软件测试实习生,讲讲我的经历和感受

目录 1.为什么选择软件测试 2.入职&#xff0c;辞职流程以及实习工作内容 3.行业选择和个人感悟 新的学期开始了兄弟们&#xff0c;我也已经断更几个月了&#xff0c;这几个月我并没有摆烂&#xff0c;我选择了备考蓝桥杯&#xff0c;复习期末&#xff0c;暑假出去实习。结果…

SpringDataJPA系列(5)@Query应该怎么用?

SpringDataJPA系列(5)Query应该怎么用&#xff1f; 之前说到过&#xff0c;DMQ查询策略有两种&#xff1a;方法命令和Query注解的方式。为什么需要两种呢&#xff1f;它们分别适用的场景是怎么样的&#xff1f; Query使用 定义一个通过名字查询用户的方法 以下是测试方法&…

将泛型和函数式编程结合,竟然会让代码这么优雅!

但这种方式却太表象了&#xff0c;没有灵魂和深度&#xff0c;过去的那些日子&#xff0c;我感觉自己的编程水平也就限于把重复的代码抽一抽&#xff0c;&#xff08;如下图所示一样&#xff09;&#xff0c;甚至觉得代码优化不就是这样吗&#xff0c;这样的状态一直维持很久。…

Linux多线程——利用C++模板对pthread线程库封装

文章目录 线程封装主要框架线程启动线程等待其他信息 测试函数 线程封装 我们之前介绍过pthread的线程库&#xff0c;这个线程库主要是基于C语言的void*指针来进行传参和返回 我们使用C的模板对其封装可以让他的使用更加方便&#xff0c;并且经过测试可以让我们更加直观的了解…

DPDK基础入门(二):Cache与大页优化

Cache简介 目前Cache主要由三级组成: L1 Cache, L2 Cache和Last Level Cache(LLC)。 L1最快&#xff0c;但容量小&#xff0c;可能只有几十KB。LLC慢&#xff0c;但容量大&#xff0c;可能多达几十MB。 L1和L2 Cache一般集成在CPU内部。另外,&#xff0c;L1和L2 Cache是每个处…

【2024】Datawhale X 李宏毅苹果书 AI夏令营 Task3

本文是关于李宏毅苹果书”第2章 实践方法论“学习内容的记录。 模型在测试集上表现不佳&#xff0c;可能是因为模型没有充分学习训练集。模型不能充分学习训练集的原因&#xff1a; 模型偏差优化问题过拟合不匹配 一、模型偏差 模型偏差是指&#xff1a;由于模型过于简单&a…

网站如何针对不同的DDOS进行防御?

建设网站租用服务器是多数企业及个人的选择&#xff0c;一个安全稳定的服务器对网站的重要性无需再赘述。要保证服务器租用的安全和稳定&#xff0c;除了需要服务器自身有强大的硬、软件基础之外&#xff0c;还需要防范外部的一些因素&#xff0c;常见的就是各种网络攻击&#…

Linux 上如何做MySQL数据备份

目录 SQL备份脚本创建crontabcrontab命令总结查看特定目录中的周期性任务 crontab&#xff08;cron table 的缩写&#xff09;是 Unix/Linux 系统上用于设置周期性被执行的任务的工具。它允许用户定义需要在特定时间&#xff08;比如每天凌晨、每周的某个时间等&#xff09;自动…

驭势科技研究成果入选学术顶会IROS 2024

近日&#xff0c;驭势科技团队关于自动驾驶车辆定位算法的最新研究成果《LiDAR-based HD Map Localization using Semantic Generalized ICP with Road Marking Detection》&#xff0c;创造性地解决了基于LiDAR的实时路标检测和高精地图配准所带来的挑战&#xff0c;成功入选国…

汇川技术|KingIOServer与AC810PLC通过ModbusTCP通讯测试

哈喽&#xff0c;你好啊&#xff0c;我是雷工&#xff01; 最近有个项目用亚控的KingSCADA软件开发SCADA系统&#xff0c;需要和汇川的AC810PLC进行通讯&#xff1b; 本节测试亚控的采集软件KingIOServer与汇川的AC810PLC的通讯测试。 以下为测试笔记。 01 效果演示 测试过程…

3个苹果锁屏密码解锁方法,帮你快速解决密码忘记的烦恼!

苹果手机锁屏密码忘记了是一件很常见的问题&#xff0c;但也是一件让人头疼的事情。如果你遇到了这样的问题&#xff0c;不要着急&#xff0c;因为有很多方法可以帮助你解锁iPhone。下面我们将介绍四种简单的方法来解锁iPhone。 一、使用密码解锁工具 iphone忘记了密码怎么解锁…

iconfont图标字体库详细介绍

概述 图标库在前端开发中应用十分广泛&#xff0c;图标库不仅会丰富美化界面的展示&#xff0c;语义化的图标库更能简洁明了地向用户传达某些信息&#xff0c;比如功能的特性和作用&#xff0c;引导用户&#xff0c;极大提高系统的易用性。在没有 UI 设计师的情况下&#xff0…

【C++】手动实现队列的封装(C++)

目录 源代码&#xff1a; 输出结果如下&#xff1a; 实现以下封装 源代码&#xff1a; #include <iostream>using namespace std;class Queue { private:int* arr; // 队列的动态数组int front; // 队列头部元素的索引int rear; // 队列尾部元素的索引in…

新版某数字壳脱壳,过frida检测,及重打包

目录 脱壳 寻找特征& frida hook 过frida检测 修复dex 重打包 修改smail 去签名校验 正文 大家好&#xff0c;我是小生&#xff0c;这次的app是一个国内某计划app, 功能相当全&#xff0c;界面也很美观&#xff0c;很实用&#xff0c;这个app我很欣赏。总共花了有…

【SQL】Delete使用

目录 语法 需求 示例 分析 代码 语法 DELETE删除表中所需内容 删除表中满足特点条件的行&#xff1a;DELETE FROM 表名 WHERE 条件; 删除表中所有行&#xff1a;DELETE FROM 表名; WHERE子句 WHERE子句用于指定从表中选取记录的条件。允许筛选数据&#xff0c;只返回满足…