C++菱形继承_多态

💓博主CSDN主页:麻辣韭菜-CSDN博客💓

⏩专栏分类:http://t.csdnimg.cn/362T6⏪

🚚代码仓库:要相信光/C++高阶🚚

🌹关注我🫵带你学习更多C++知识
  🔝🔝

目录

前言

一.菱形继承的解决方法

1.1 virtual

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

二.继承的总结和反思

三.多态的概念

1.1 概念

1.2 多态的定义及实现

(1)多态的构成条件

 (2)虚函数

(3)虚函数的重写

(4) C++11 override 和 final

四.抽象类

五.多态的原理

5.1虚函数表

5.2多态的原理  

5.3 动态绑定与静态绑定

六.多继承关系的虚函数表


 

前言

继承还有菱形继承没有解决,会在这篇解决菱形继承。本篇重点多态,多态在面试和笔试经常考,重点中的重点。

一.菱形继承的解决方法

1.1 virtual

在继承讲到了菱形继承会产生数据冗余和二义性,虚拟继承可以解决菱形继承的数据冗余,和二义性 。 请看下图

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

 我们先看多继承的内存地址

从上面地址我们可以看到是连续存放的而且数据冗余了。⬇

我们再看虚拟继承内存地址红色框起来的⬇

 怎么会多了两个地址 00ad7b40 和 00ad7b48 然后我们把这两个地址在内存监视窗口输入一下就得到下图

这两个地址里面分别放着两个值 14 和 12 我们这时再看看这个两个地址分别加14 和 12等于多少

38+14 = 52 按照 16进制换算 等于4c 而40+c =4c 怎么这么巧?这两个地址存放的值相加都指向同一个地址A的值。

通过上面的演示我们看到: 这里是通过了 B C 的两个指针,指
向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量 可以找到下面的 A
这时我们再看下面这张图

二.继承的总结和反思

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

区分什么是继承什么是组合 请看下面代码

// 公有继承 -- is-a
class A
{
public:
	void func(){}
protected:
	int _a1;
	int _a2;
};

class B : public A
{};

 组合    -- has-a
class C
{
public:
	void func(){}
protected:
	int _c1;
	int _c2;};

class D
{
private:
	C _cc;
};

如果还是有点不明白,记住万能公式 我是你的什么就用继承, 我有你什么就组合。

比如 我是你的学生,那就继承这个老师的属性。 我有你房子的钥匙,你就用这个套

如果我是你的什么明显语句不通顺。那组合,如果我是你什么 也有你的什么,两个都行那还是组合。

三.多态的概念

1.1 概念

多态的概念:通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态
举个栗子:比如 买票这个行为 ,当 普通人 买票时,是全价买票; 学生 买票时,是半价买票; 军人 买票时是优先买票。
再比如拼多多新人注册时,得红包更多 那么就你扫码金额 = random()%99,而老用户就只有几角钱 那么就你扫码金额 = random()%1。 

1.2 多态的定义及实现

(1)多态的构成条件

1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

 (2)虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数。

virtual void BuyTicket() { cout << "买票-全价" << endl; }

(3)虚函数的重写

虚函数的重写 ( 覆盖 ) 派生类中有一个跟基类完全相同的虚函数 ( 即派生类虚函数与基类虚函数的
返回值类型、函数名字、参数列表完全相同 ) ,称子类的虚函数重写了基类的虚函数。
虚函数重写的两个例外:
1. 协变 ( 基类与派生类虚函数返回值类型不同 )
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指
针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。↓
class A{};
class B : public A {};
class Person {
public:
 virtual A* f() {return new A;}
};
class Student : public Person {
public:
 virtual B* f() {return new B;}
};
析构函数的重写 ( 基类与派生类析构函数的名字不同 )
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加 virtual 关键字,
都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,
看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处
理,编译后析构函数的名称统一处理成 destructor
class Person {
public:
 virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
 virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
//数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
 Person* p1 = new Person;
 Person* p2 = new Student;

 delete p1;
 delete p2;
 return 0;
}

(4) C++11 override 和 final

从上面可以看出, C++ 对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数
名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有
得到预期结果才来 debug 会得不偿失,因此: C++11 提供了 override final 两个关键字,可以帮
助用户检测是否重写。
1. final :修饰虚函数,表示该虚函数不能再被重写
2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
(5)  重载、覆盖 ( 重写 ) 、隐藏 ( 重定义 ) 的对比

四.抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。 包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象 。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生
类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
 virtual void Drive()
 {
 cout << "Benz-舒适" << endl;
 }
};
class BMW :public Car
{
public:
 virtual void Drive()
 {
 cout << "BMW-操控" << endl;
 }
};
void Test()
{
Car* pBenz = new Benz;
 pBenz->Drive();
 Car* pBMW = new BMW;
 pBMW->Drive();
}

五.多态的原理

5.1虚函数表

这里常考一道笔试题: sizeof(Base) 是多少?
class Base
{
public:
 virtual void Func1()
 {
 cout << "Func1()" << endl;
 }
private:
 int _b = 1;
};

结果是8 下面我们通过监视来看看 发现里面多了一个_vfptr这个指针。

对象中的这个指针我们叫做虚函数表指针 (v virtual f 代表 function) 。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数
的地址要被放到虚函数表中,虚函数表也简称虚表,。那么派生类中这个表放了些什么呢?我们接着往下分析↓
通过观察和测试,我们发现了以下几点问题:
1. 派生类对象 d 中也有一个虚表指针, d 对象由两部分构成,一部分是父类继承下来的成员,虚 表指针也就是存在部分的另一部分是自己的成员。
2. 基类 b 对象和派生类 d 对象虚表是不一样的,这里我们发现 Func1 完成了重写,所以 d 的虚表 中存的是重写的 Derive::Func1 ,所以虚函数的重写也叫作覆盖 ,覆盖就是指虚表中虚函数 的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3. 另外 Func2 继承下来后是虚函数,所以放进了虚表, Func3 也继承下来了,但是不是虚函
数,所以不会放进虚表。
4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个 nullptr
5. 总结一下派生类的虚表生成:
a. 先将基类中的虚表内容拷贝一份到派生类虚表中
b. 如果派生 类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

5.2多态的原理  

灵魂问题:Func这个函数是怎么知道指向谁调用谁?我们利用汇编接着分析

 

我们再看看不满足多态的调用什么样的 

首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,所以这里是普通函数的调 用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址

5.3 动态绑定与静态绑定

1. 静态绑定又称为前期绑定 ( 早绑定 ) 在程序编译期间确定了程序的行为 也称为静态多态
比如:函数重载
2. 动态绑定又称后期绑定 ( 晚绑定 ) ,是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态
比如:我们前面分析的基类的指针或引用调用虚函数 ,

六.多继承关系的虚函数表

前面的单继承虚函数表已经演示过了,这里重点讲解多继承的虚函数表

先来一个多继承的代码

class Base1
{
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};
class Base2
{
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
		int b2;
};
class Derive : public Base1, public Base2
{
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};

通过前面的单继承的演示,我们知道不管子类的对象有多少个,都会共用一张虚表。

那多继承的子类会有几张虚表?

通过监视窗口子类有两张虚表。而且先初始化的是Base1的虚表指针。这个根据谁先声明先初始化谁,在多态中谁先继承先初始化谁。

仔细看 窗口监视里面没有func3,编译器做了处理 我们看不到 那我们如何查看d对象的虚表?那这个func3是放在哪里的?如何打印这个d对象的虚表? 

第一张虚表是在对象的什么位置?32位下是头4个字节。那我们可以取d对象的前4个字节 通过这个虚表指针指向的是个vector的数组,里面的放的是虚函数的指针。通过下标访问就可以打印 了

这里对于指针要求比较高,看不懂可以先理解下面这段代码

typedef void(*VF_PTR)();
void PrintVFTable(VF_PTR table[])
{
	for (int i = 0; table[i] != nullptr; ++i)
	{
		printf("[%d]:%p->", i, table[i]);
		VF_PTR f = table[i];
		f();
	}
	cout << endl;
}
PrintVFTable((VF_PTR*)(*(int*)&d));

从上图我们可以得出 多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

 虚表是什么阶段生成的? -- 编译

对象中虚表指针什么时候初始化的? -- 构造函数的初始化列表

虚表存在哪里?--代码区

那如何打印第二张虚表? 想想之前的继承切片 子类赋值给父类,通过自动偏移就能找到父类的起始地址。👇

子类完成虚函数的重写,怎么两个func1和func2的地址怎么不一样?

从汇编演示我们可以看到这里是对Base2的虚函数进行了封装,所以打印出来的地址两个不同。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Excel PDF 系列】EasyExcel + iText 库实现 Excel 转换 PDF

你知道的越多&#xff0c;你不知道的越多 点赞再看&#xff0c;养成习惯 如果您有疑问或者见解&#xff0c;欢迎指教&#xff1a; 企鹅&#xff1a;869192208 文章目录 前言转换前后效果引入 pom 配置代码实现定义 ExcelDataVo 对象主方法EasyExcel 监听器 前言 最近遇到生成 …

2024第二次培训:win11系统下使用nginx、JDK、mysql搭建基于vue2、java前后端分离的web应用运行环境

一.背景 公司安排了带徒弟的任务&#xff0c;给培训写点材料。前面分开介绍了mysql、jdk、nginx的安装&#xff0c;都只是零星的介绍&#xff0c;只能算零散的学习。学习了有什么用呢&#xff1f;能解决什么问题&#xff1f;能完成什么工作&#xff1f; 今天我们要用之前的几篇…

TCP/UDP,HTTP、HTTPS存在什么风险会影响到网络安全吗

近年来&#xff0c;随着网络技术的飞速发展&#xff0c;互联网影响人们的方方面面&#xff0c;我们平时也接触到许多以前从未听过的东西&#xff0c;今天德迅云安全就来分享下一些互联网安全知识&#xff0c;讲解一些关于常看到的关于IP, TCP/UDP&#xff0c;HTTP、HTTPS这些名…

Docker自定义JDK镜像并拉取至阿里云镜像仓库全攻略

前言 随着容器技术的日益成熟&#xff0c;Docker已经成为现代软件开发和部署的标配工具。其中&#xff0c;自定义Docker镜像是满足特定项目需求的关键步骤。特别是在Java开发环境中&#xff0c;我们可能需要为不同的项目配置不同版本的JDK。这时&#xff0c;通过Docker自定义J…

venv、pip、conda、anaconda、miniconda的区别和优缺点,和彻底清除python多余的环境

virtualenv(venv) 这是一个虚拟环境管理器&#xff0c;它可以让你每个项目甚至每个脚本配置一个自定义的Python解释器环境&#xff0c;这最大的好处是我可以不污染开发环境。​ pip pip 是 Python 最常用的包管理器&#xff0c;它能自动处理依赖 。 conda 如果说venv是虚拟…

langchain学习笔记(九)

RunnableBranch: Dynamically route logic based on input | &#x1f99c;️&#x1f517; Langchain 基于输入的动态路由逻辑&#xff0c;通过上一步的输出选择下一步操作&#xff0c;允许创建非确定性链。路由保证路由间的结构和连贯。 有以下两种方法执行路由 1、通过Ru…

S2---FPGA-A7板级原理图硬件实战

视频链接 FPGA-A7板级系统硬件实战01_哔哩哔哩_bilibili FPGA-A7板级原理图硬件实战 基于XC7A100TFGG484的FPGA硬件设计流程图 A7核心板&#xff0c;是基于XILINX公司的ARTIX-7系列100T的XC7A100T,2FGG484I这款芯片开发的高性能核心板&#xff0c;具有高速&#xff0c;高带宽&a…

wpa_supplicant交叉编译

文章目录 源码编译openssl编译libnl交叉编译WPA 开发板测试使用 源码 wpa_supplicant官网&#xff1a;http://w1.fi/wpa_supplicant/ GIT源&#xff1a;git://w1.fi/hostap.git openssl 源码&#xff1a; https://www.openssl.org/ libnl 源码&#xff1a; https://github.c…

QT之液晶电子时钟

根据qt的<QLDNumber>做了一个qt液晶电子时钟. 结果 实时显示当前时间,左键可以拖动时钟在屏幕的位置,右键点击关闭显示. 实现过程 新建一个class文件,让这个文件的父类是QLCDNumber 相关功能变量定义和函数实现 .c文件代码 这里需要注意的一点是event->button是获取的…

力扣SQL50 产品销售分析 I 查询

Problem: 1068. 产品销售分析 I 思路 left join on&#xff1a;左连接 Code select p.product_name, s.year, s.price from Sales s left join Product p on s.product_id p.product_id

深入剖析k8s-控制器思想

引言 本文是《深入剖析Kubernetes》学习笔记——《深入剖析Kubernetes》 正文 控制器都遵循K8s的项目中一个通用的编排模式——控制循环 for {实际状态 : 获取集群中对象X的实际状态期望状态 : 获取集群中对象X的期望状态if 实际状态 期望状态 {// do nothing} else {执行…

【nmap工具介绍及常用命令】从零基础入门到精通,看完这一篇就够了。

1.功能介绍 nmap&#xff08;network mapper&#xff09;,网络映射器&#xff0c;是kali内置的一款工具&#xff0c;是网络连扫描软件&#xff0c;用来扫描网上设备开放的网络连接端。确定哪些服务运行在哪些连接端&#xff0c;并且&#xff0c;推断设备使用什么系统。 nmap的…

2024年腾讯云优惠代金券一键领取页面,太划算!

腾讯云代金券领取渠道有哪些&#xff1f;腾讯云官网可以领取、官方媒体账号可以领取代金券、完成任务可以领取代金券&#xff0c;大家也可以在腾讯云百科蹲守代金券&#xff0c;因为腾讯云代金券领取渠道比较分散&#xff0c;腾讯云百科txybk.com专注汇总优惠代金券领取页面&am…

详细介绍如何用windows自带Hyper-V安装虚拟机(windows11和ubuntu22)

通过系统自带的hyper-v安装windows11&#xff0c;舒服又惬意&#xff0c;相比用第三方虚拟机软件速度快很多。 硬件准备 准备 系统需要符合能安装 Hyper-V 的最低要求windows版本含Hyper-V的功能 电脑空间 电脑要有足够的空间来安装你这个虚拟机。根据自己的磁盘容量情况来规…

如何知道当前ubuntu的版本

查看版本&#xff1a; cat /etc/lsb-release 查看内核&#xff1a; uname -a

新算法转让(一种基于改进欧拉法开发的元启发式算法)

新算法转让&#xff08;一种基于改进欧拉法开发的元启发式算法&#xff09; 新的群智能算法转让&#xff0c;新的元启发式算法转让 1.开发完成的完整代码 2.灵感部分已完成&#xff0c;有word版本说明 3.测试结果17/30个排名第一&#xff08;算法开发完毕&#xff09;与AO、A…

面部SDF阴影锯齿问题的探索

近期做的一些工作涉及到面部SDF阴影&#xff0c;网上普遍做法是不做插值&#xff0c;直接Step硬性裁剪&#xff0c;我通过SmoothStep做了简单修改&#xff0c;看下效果。 看上去还可以是因为gif有压缩&#xff0c;但面部SDF阴影做插值有个很严重的问题&#xff1a; 插值处理后…

LangFlow——一款可轻松实验和原型化 LangChain流水线的AI项目

LangFlow——一款可轻松实验和原型化 LangChain流水线的AI项目。 前言 在人工智能兴起的当下&#xff0c;AI正在重塑着很多行业。今天介绍的是一款近期登上github热门的一款可轻松实验和原型化 LangChain[1] 流水线的AI项目—LangFlow。 Flowise——通过拖放界面构建定制的LLM…

音视频开发项目:H.265播放器:视频解码篇

视频演示 如下将演示新版播放器播放 1分钟1080p/25fps/H.265 MP4视频&#xff0c;具体视频参数如下&#xff1a; 粉丝福利&#xff0c; 免费领取C音视频学习资料包学习路线大纲、技术视频/代码&#xff0c;内容包括&#xff08;音视频开发&#xff0c;面试题&#xff0c;FFmpe…

微软自带的便笺(jian)无法连接到服务器。错误代码 0x80072EFD,同时如何在手机使用便笺

便笺的内容无法在各个设备同步&#xff0c;错误如图所示 一、问题的解决 参考自微软社区&#xff1a;Redirecting 原话&#xff1a; 此错误一般都是由于网络问题导致的。建议您首先确认您有没有开启代理&#xff0c;您可以打开设置&#xff0c;网络和Internet&#xff0c;找…
最新文章