C++ 深入理解多态及拓展

news2024/11/27 23:48:53

文章目录

  • 1. 理解虚表
    • 1.1 虚表
    • 1.2 验证
    • 1.3 子类虚表
    • 1.4 相同类不同对象的虚表
  • 2. 静态绑定和动态绑定
    • 2.1 静态绑定
    • 2.2 动态绑定
  • 3. 多态的实现原理
    • 3.1 向上转型
    • 3.2 多继承
    • 3.3 原理
  • 4. 拓展
    • 4.1 构造函数能不能是虚函数
    • 4.2 父类和子类的析构函数在底层的命名问题
    • 4.3 对象之间无法实现多态的原因

1. 理解虚表

多态:简单来说就是执行一种行为,不同的对象会表现出不同的执行过程

今天分享一下 C++ 中 多态的实现原理:

1.1 虚表

首先看一下下面这个简单的例子:

在这里插入图片描述

很显然,这里分别打印 4 8 非常合理,但是如果我们在 Father 类让这两个函数变成虚函数 ,这时候打印结果是多少?

在这里插入图片描述

可以看出:Father 类的空间大小变成了 8 字节,但是有没有可能是函数的大小?并不会,因为C++的类成员函数会存放在内存中的代码区(将 virtual 删掉之后,打印分别是 48),所以导致空间变大了的原因就是 virtual

这里通过调试就可以看到这个指针 __vfptr,并且可以看出这个指针子类也有,但是和父类一样但是不完全一样:指针的地址不一致,但是指针的内容一致,这个现象后面再讨论在这里插入图片描述

1.2 验证

实际上,当一个类中有虚函数的时候,这个函数就会存储一个指针 —— 虚表指针,也就是这里的 __vfptr,顾名思义,指向虚表,也就是虚函数表

而虚函数表中存储的就是虚函数的地址,并且大部分情况下,这个虚表指针__vfptr 在对象模型中会被放在第一位

就拿这个 Father 来说,上述讲的内容可以总结如下:

在这里插入图片描述

接下来是验证过程:

① 首先定义一个函数指针,类型就是Father 成员函数的类型void function(),并且类型起名为function_t

typedef void(*function_t)();

② 那么由于这个虚函数表一般放在内存模型的第一位,那么我们只需要取出前 4 个字节的数据,就可以得到虚表指针了

但是由于毫不相干的指针没法互相转化,所以我们需要做点特出处理
Firstly:获取father对象的地址

&father

Secondly:然后强转成 int* 就可以获取前 4 个字节的 int* 指针

(int*)(&Father)

Thirtly:然后解引用,就可以获得这 4 个字节的真实数据了对吧

*(int*)(&Father)

Finally: 这 4 个字节也就是虚表指针的地址,也就是虚函数数组首地址,所以再转化成函数指针,再接收

function_t* ptr = (function_t*) (*(int*)(&father));

③于是成功得到虚表指针,然后我们再对 ptr 解引用,就可以得到第一个虚函数,再调用,就可以成功调用里面的第一个函数了!

主要代码如下:
在这里插入图片描述

执行结果如下:

在这里插入图片描述
所以我们就成功证明了以上的结论,虚表里面存放的也确实是该类的虚函数,再简单总结一下:

  • 如果类中有虚函数,那么这个类对象的第一个成员变量(一般是放在第一位)就是虚表指针,虚表指针指向虚表,里面存放该类的虚函数地址,有多少个虚函数,这个虚表就会有多大

1.3 子类虚表

前面的截图中可以看到,子类继承父类后,子类也有虚表指针,内容一样,但是虚表指针的值不一样

在这里插入图片描述
这时候思考一下:子类继承父类之后是直接继承父类的虚表指针咩?如果是直接继承,那么 __vfptr为何不一样

这时候再修改一下代码:
在这里插入图片描述
在子类,对父类的 function1函数进行重写,这时候调式情况如下:
在这里插入图片描述
看出:子类虚表指针的其中第一个虚函数地址变化了

现在再通过同样的方法来调用子类虚表中的第一个虚函数

int main()
{
	Father father;
	A a;
	function_t* ptr = (function_t*) (*(int*)&a);
	(*ptr)();
	return 0;
}

打印结果如下,得出:调用的就是子类重写之后的函数,子类虚表中改变的那一项就是重写父类虚函数function1 的地址

在这里插入图片描述
⭐结论:
Ⅰ 如果父类有虚函数,那么子类会拷贝父类的虚表
Ⅱ 并且如果子类重写了父类的虚函数,则会在虚表中修改同位置的被重写的父类虚函数
Ⅲ 如果子类有自己定义的虚函数,那么也会放到自己的虚表中

结合下面草图理解理解

在这里插入图片描述

1.4 相同类不同对象的虚表

那么如果Father 类中有多个实现类,虚表的情况如何 0.o?

对代码稍作修改,调试如下:

在这里插入图片描述总结:可以看出所有Father对象的虚表内容都是一样的

  • 同一个类的所有对象都共用同一份虚表

2. 静态绑定和动态绑定

至此还需要补充一点知识:静态绑定和动态绑定

2.1 静态绑定

概念:程序在编译时期就能确定程序中需要调用的函数地址,即确定程序的行为

2.2 动态绑定

编译阶段无法确定对象调用函数的地址,具体在程序运行的期间,再根据对象或者指针的实际类型,动态地决定使用程序所调用的函数。(运行时在虚函数表中寻找要调用的函数地址)

这一部分大伙可以看这篇文章,作者写的很好,我不多嗦

3. 多态的实现原理

而多态就是基于动态绑定所实现的,如果发生了多态,编译时期无法得知具体程序会调用哪个函数,于是就会进行动态绑定在运行中确定具体需要调用的函数

然后回顾一下多态发生的两个前提条件:

  • 重写
    子类需要对父类的虚函数进行重写。重写之后,子类和父类有着不同的虚表
  • 父类引用/指针 接收 子类引用/指针
    例如Father* father = new Son()

在原理之前,看完向上转型可能可以更好地理解
重写父类的虚函数,这个没什么好说的,这里具体看看向上转型:

3.1 向上转型

为了方便讲解,以下的场景,都拿指针来举例子
当父类引用 / 指针接收子类对象的时候,那么这个指针指向的区域是个什么样子?也就是这块内存具体长什么样?

这涉及到了切片
⭐切片的本质就是:舍弃子类成员,但是不是真正意义上的舍弃,只是无法访问
sizeof关键字也不会计算子类成员)

如下,还是类似的代码,子类继承了 Father 并重写了 function1 函数

class Father
{
public:
	int father;
	virtual void function1() {
		cout << "this is function1()" << endl;
	}
	virtual void function2() {}
};

// A B 都是子类
class A : public Father
{
public:
	int a;
	void function1() {  // 重写父类 function1 函数
		cout << "son A : this is function1" << endl;
	}
	
};

现在有如下代码:终点是代码中的这两个指针

int main() 
{
	A a;
	Father* ptr1 = &a;
	A* ptr2 = &a;
	return 0;
}

A* ptr2 = &a
先分析一下这个代码,这个就是典型的子类指针接收子类对象

首先父类有两个成员,一个虚表指针,一个自己的成员变量 father,子类 A 会继承父类的属性,并且拷贝虚表并覆盖虚表的内容,如果 A 类中有自己独有的虚函数,也会添加到虚表中

所以 A 指针表示如下
在这里插入图片描述Father* ptr1 = &a
这里就涉及到了向上转型,会对 a 对象进行切片
所以这和上面那个基本一样

Father* 指针表示如下,也就是粉色部分,子类的特有成员无法被访问
在这里插入图片描述Father* ptr3 = new Father
强调一下:需要区分,这个和前面两个是不一样的,这里创建的是父类对象,所以虚表自然也就是父类的虚表

在这里插入图片描述

3.2 多继承

如果是多继承的情况,情况又是怎样的
现在对代码稍加需改,让子类 A 多继承一个类:Mother


class Father
{
public:
	int father;
	virtual void function1() {
		cout << "this is function1()" << endl;
	}
	virtual void function2() {}
};

class Mother
{
public:
	int mother;
	virtual void function3() {}
};

class A : public Father, public Mother
{
public:
	int a;
	void function1() {  // 重写父类 function1 函数
		cout << "son A : this is function1" << endl;
	}
	
};

如果是上面这种继承关系,那么如下指针需要如何表示

int main()
{
	A a;
	Father* ptr1 = &a;
	Mother* ptr2 = &a;
	A* ptr3 = &a;
	return 0;
}

① 首先第一个问题是创建好的 a 对象模型是什么样的,它继承了FatherMother,而FatherMother 都有虚函数,也就都有虚表,那么 a 类也就都会拷贝虚表并修改。

在这里插入图片描述
② 然后就只需要和上面一样进行切片就好了,最终表示如下

在这里插入图片描述

3.3 原理

上面那部分看懂之后,多态的原理可以拿下了,这里做个陈述和总结:

  • C++ 的多态依赖于动态绑定,需要在程序运行过程中确定被调用的函数地址,具体就是查询虚函数表,确定调用的是哪个函数,因此,被调用的时候是在运行的时候才会被确定的。
  • 当满足多态的条件之后,父类和子类都会有虚表指针,分别指向各自的虚表,不同的是,子类会拷贝父类的虚表,并将 重写的虚函数地址 覆盖掉原虚表中对应的虚函数,所有的子类都会这样
  • 所以当发生向上转型的时候,会创建子类对象,并且父类指针指向属于父类的那部分(切片)。因此在调用函数的时候,由于不同的子类有不同的虚表,就直接去虚表中调用对应的虚函数最终就可以实现多态。

如果还是有点懵,可以看一下我画的这份草图

在这里插入图片描述
所以,就可以根据这样,一个父类接收不同的子类,当调用子类重写函数的时候,就可以实现调用一个父类指针的一个函数,因为接收子类对象的不同,来表现出不同的函数,即多态

4. 拓展

4.1 构造函数能不能是虚函数

虚函数表会在编译阶段完成构建,但是虚函数表中的虚函数需要依靠虚表指针才能实现,然而,虚表指针的初始化发生在对象的构建期间,也就是构造函数中(也就是将虚表的地址赋值给虚表指针)。

这就尴尬了,如果构造函数是虚函数,那么虚函数的调用需要虚表指针才可以完成,然而虚表指针需要在构造函数中初始化

所以 不行。

4.2 父类和子类的析构函数在底层的命名问题

提前说一个结论:在 C++ 中,父类和子类的析构函数在底层的命名都是 destructor

其实目的就是为了形成多态,假设现在有这样的代码Father* ptr = new Son(),那么当这个对象需要被回收的时候,由于这个指针是 Father 类的,所以就会去调用 Father 类的析构函数,但是子类的成员却没有被释放(虽然是切片,但是子类成员在内存空间中仍然存在)。

因此,如果子类和父类的析构函数同名,那么上述情形就可以发生多态,调用子类的析构函数,并且编译器会在子类的析构函数执行完成之后自动调用父类的析构函数(特性),因此,原因如上。

这也就是为什么父类的析构函数一般都要加上 virtual 修饰的原因

4.3 对象之间无法实现多态的原因

父类对象接收子类对象,不管是赋值操作还是构造操作,都只会处理普通成员变量。一个类中的不同对象的虚表一样,所以父类对象的虚表不会受子类的影响,与子类无关,调用的时候只会调用父类虚表中的虚函数。

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

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

相关文章

c++实现smtp发送邮件,支持ssl的465端口发送,支持附件、一次发送多人、抄送等

前言 c实现smtp发送邮件&#xff0c;支持ssl的465端口发送&#xff0c;支持附件、一次发送多人、抄送等。 这里只使用了openssl库&#xff08;用来支持ssl的465端口&#xff09;&#xff0c;其他部分是原生c,支持在win/linux运行。 网上很多都是原始的支持25端口&#xff0c;明…

Fiddler抓包工具之高级工具栏中的重定向AutoResponder的用法

重定向AutoResponder的用法 关于Fiddler的AutoResponder重定向功能&#xff0c;主要是时进行会话的拦截&#xff0c;然后替换原始资源的功能。 它与手动修该reponse是一样的&#xff0c;只是更加方便了&#xff0c;可以创建相应的rules&#xff0c;适合批处理的重定向功能。 …

[SQL Server]数据库入门之多表查询

&#x1f3ac; 博客主页&#xff1a;博主链接 &#x1f3a5; 本文由 M malloc 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f384; 学习专栏推荐&#xff1a;LeetCode刷题集&#xff01; &#x1f3c5; 欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指…

使用vant,实现密码输入框右边提供可视按钮(最简单)

在实际项目开发中&#xff0c;要实现密码输入框带密码可见切换按钮&#xff08;右侧的眼睛&#xff09;&#xff0c;点眼睛可以显示或隐藏密码。 实现原理&#xff1a;动态绑定输入框类型 1.绑定密码框的type属性&#xff0c;在密码框使用插槽 ps&#xff1a;由于icon标签不…

美股怎么交易?有哪些美股交易基础知识?

美股市场相对成熟&#xff0c;投资回报率也更高一些&#xff0c;受到投资者喜爱。美股怎么交易&#xff1f;首先就需要了解美股交易基础知识。 美股交易基础知识一、美股交易市场 美股主要交易市场有NYSE纽约证券交易所、NASDAQ纳斯达克证券市场、AMEX美国证券交易所。 美股交…

GitOps 最佳实践(上)| 基于 Amazon EKS 构建 CI/CD 流水线

GitOps 是目前比较理想的方法来实现基于 Kuberentes 集群的持续部署。 了解了 GitOps 的概念以及 CI/CD 流水线的架构&#xff0c;接下来我们将通过以下四个模块逐步完成构建 CI/CD 流水线的最佳实践&#xff1a; 通过 IaC 部署云基础架构&#xff1b;在 Amazon EKS 集群上部…

2023年新课标I卷作文,5位人工智能考生(ChatGPT,文心一言,GPT4, ChatGLM-6b, ChatT5)来写作,看谁写得最好

大家好&#xff0c;我是微学AI&#xff0c;今天是2023年6月7日&#xff0c;一年一度的高考又来了&#xff0c;今年的高考作文题也新鲜出炉。今年是特殊的一年&#xff0c;有人说2023是AI的元年&#xff0c;这一年里有大语言模型的爆发&#xff0c;每天都有大模型的公布&#xf…

23年测试岗,测试工程师从初级到中高级进阶,测试晋升之路...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 调查显示&#xf…

【旋转摆正验证码】移动积分兑换影视会员活动旋转摆正验证码识别——识别解绝方法

移动积分兑换影视会员活动旋转验证码的0~200ms级小模型识别思路 具体讲解识别思路 移动积分兑换影视会员活动拖动旋转验证码被破解&#xff1f;当代流行的人机验证到底安不安全&#xff1f; 提示&#xff1a;以下是皆为学习交流之&#xff0c;如有侵权 &#xff0c;望通知删帖…

年内BEV落地之战:华为遥遥领先,还是蔚小理登上王座?

作者 | 张祥威 编辑 | 德新 落地城市NOA&#xff0c;是今年最重磅的自动驾驶大战。而BEV感知&#xff0c;目前看来是 通往城市NOA的必经之路。 年内落地BEV&#xff0c;已经是国内自动驾驶头部玩家的共识。 其实&#xff0c;BEV是很早就提出的算法&#xff0c;又称鸟瞰图或上帝…

R730调整风扇转速

整整一个月没有写文章了&#xff0c;一是因为最近太忙&#xff0c;有点休息的时间就想躺着&#xff1b;二是买了Tesla P40显卡&#xff0c;想写个安装教程&#xff0c;结果快一个月了&#xff0c;安装还是失败。 大家如果谁懂在R730的ESXi上&#xff0c;用直通方式安装Tesla&am…

MMPretrain代码课

安装注意事项 训练时需要基于算法库源码进行开发&#xff0c;所以需要git clone mmpretrain仓库。如果只调用&#xff0c;则pip install 即可。 from mmpretrain import get_model, list_models,inference_model分别用于模型的获取、例举、推理 此时还没加载预训练权重 tor…

Redis-Cluster集群架构

Redis-Cluster 1.哨兵模式和redis-cluster模式的区别 哨兵模式的问题&#xff1a;1.只有一个master节点可以提供写的操作&#xff0c;qps 最多10w&#xff0c;对于高并发特别高的大型互联网系统 ​ 2.单节点不会内存太大&#xff0c;内存很大会给主节点造成压力&#xff0c;…

如何用数据资产管理,解锁数据新价值

数字经济和数字化转型的发展有什么共通点吗&#xff1f;这个问题的答案也很明显&#xff0c;数据就是数字经济数字化转型的基础&#xff0c;也是推动两者快速发展的核心要素。数字化时代&#xff0c;数据已经成为了个人、机构、企业乃至国家的重要战略资产&#xff0c;所以如何…

CnOpenData数字经济专利及引用被引用数据

一、数据简介 自人类社会进入信息时代以来&#xff0c;数字技术的快速发展和广泛应用衍生出数字经济。与农耕时代的农业经济、工业时代的工业经济大有不同&#xff0c;数字经济是一种新的经济、新的动能、新的业态&#xff0c;并引发了社会和经济的整体性深刻变革。现阶段&…

Nginx网络服务——页面优化与安全

Nginx网络服务——优化与防盗链 一、Nginx的网页优化1.Nginx的网页压缩2.Nginx的图片缓存3.Nginx的连接超时设置4.Nginx的并发设置 二、Nginx的页面安全1.查看Nginx版本的方式2.隐藏版本号 三、Nginx的日志分割1.编写日志分割脚本2. 执行脚本进行测试3. 将日志脚本添加至计划性…

InnoDB - 行格式

文章目录 InnoDB - 行格式1. 什么是行格式2. 四种行格式3. Compact行格式 InnoDB - 行格式 1. 什么是行格式 我们平时是以行记录为单位向表中插入数据的&#xff0c;这些数据在磁盘上的存放方式被称为行格式或者记录格式。 InnoDB引擎中支持四种行格式&#xff1a;Compact、…

Java8 Stream详解及中间操作方法使用示例(一)

Java 8 引入了 Stream API&#xff0c;提供了一种新的处理集合和数组的方式。Stream API 可以让我们更加便捷、灵活地处理数据&#xff0c;尤其是大规模数据。在这里&#xff0c;我将详细介绍 Java 8 中的 Stream API。 什么是 Stream Stream 是 Java 8 中引入的一个新的概念&…

vs2022配置pcl1.13.1

下载 下载PCL预编译安装程序PCL-1.13.1-AllInOne-msvc2022-win64.exe 和要安装的PCL组件&#xff08;例如pcl-1.13.1-pdb-msvc2022-win64.zip&#xff09; 安装 双击 PCL-1.13.1-AllInOne-msvc2022-win64.exe进行安装。到图1的步骤时&#xff0c;选择第二项。 图1 下一步&am…

串口助手(布局,图标,串口号,隐藏界面,显示实时时间)

文章目录 前言一、串口助手布局二、设置软件的标题&#xff0c;图标三、显示可用串口号四、隐藏&#xff0c;显示面板五、显示实时时间总结 前言 从这篇文章开始 教大家自己制作一个串口助手软件&#xff0c;并实现基本的功能。学做一个 串口助手可以一边回顾复习 QT 的相关知…