C++:深入理解多态,多态实现原理及拓展

news2024/11/25 16:55:00

文章目录

  • 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/608675.html

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

相关文章

[论文阅读] (30)李沐老师视频学习——3.研究的艺术·讲好故事和论点

《娜璋带你读论文》系列主要是督促自己阅读优秀论文及听取学术讲座&#xff0c;并分享给大家&#xff0c;希望您喜欢。由于作者的英文水平和学术能力不高&#xff0c;需要不断提升&#xff0c;所以还请大家批评指正&#xff0c;非常欢迎大家给我留言评论&#xff0c;学术路上期…

SpringMVC第十一阶段:SpringMVC 拦截器执行源码解析

SpringMVC 拦截器执行源码解析&#xff1a; 1、执行doDispatcher做请求分发处理 1.1、调用getHandler()获取请求处理器&#xff0c;处理器中包含请求的方法和拦截器信息 getHandlerInternal() 根据请求地址获取对应的目标方法getHandlerExecutionChain() 获取请求地址对…

(转载)基于鱼群算法的函数寻优算法(matlab实现)

1 理论基础 1.1 人工鱼群算法概述 人工鱼群算法是李晓磊等人于2002年提出的一类基于动物行为的群体智能优化算法。该算法是通过模拟鱼类的觅食、聚群、追尾、随机等行为在搜索域中进行寻优&#xff0c;是集群体智能思想的一个具体应用。生物的视觉是极其复杂的&#xff0c;它…

Java006——对第一个Java程序HelloWorld的简单介绍

一、HelloWorld.java程序整体认识 public class HelloWorld { //创建一个名字叫HelloWorld的类&#xff08;Java中的类叫class&#xff09;public static void main(String[] args) {//主程序入口&#xff0c;类似C语言main函数System.out.println("He…

python之scipy.signal重采样

前言 在复现LiftingNet过程中&#xff0c;了解到作者对于不同转速设备的机械信号进行重采样来矫正转速&#xff0c;也就是固定长度的样本包含了相同旋转周期的设备信息&#xff0c;而非相同时间长度。po一下原文&#xff1a; 这里其实用到了阶次分析的原理&#xff0c;该样本…

【vue】生命周期,组件,插槽,依赖注入,实现分页器组件,异步组件,keep-alive

❤️ Author&#xff1a; 老九 ☕️ 个人博客&#xff1a;老九的CSDN博客 &#x1f64f; 个人名言&#xff1a;不可控之事 乐观面对 &#x1f60d; 系列专栏&#xff1a; 文章目录 生命周期总结组件例子轮播图组件 在组件中使用v-modelMixin插槽具名插槽实现一个分页器 依赖注入…

【软件测试】软件测试总结笔记(1)

软件测试理论总结 1.Introduction1.1 What is Software Bug1.2 Tester的职责和目标其他概念软件测试的分类 2.软件开发生命周期Software Development ProcessSoftware Development Lifecycle ModelsTDD - Test-Driven Development测试驱动开发&#xff08;一种敏捷开发&#x…

城市微博签到数据分享地址解码与纠偏教程

收录于合集 #开源4个 #GIS34个 #社交媒体2个 #大数据4个 哈喽大家好&#xff0c;我又来啦&#xff01;最近一直有小伙伴私戳问我要 签到数据&#xff0c;本着 开源共享的精神&#xff0c;我这次给大家分享中国多个城市2022年9月的匿名签到数据&#xff0c;欢迎大家点击在看…

用Python做兼职,轻松赚取零花钱,分享Python兼职经验

文章目录 前言一、技术方案二、接单流程三、注意事项四、总结 前言 某家电商公司需要从竞争对手的网站上获取商品信息&#xff0c;以便更好地了解市场情况和竞争对手的策略。由于该公司没有专门的技术团队&#xff0c;因此他们需要找一家专业的爬虫服务公司来帮助他们完成这项…

冈萨雷斯DIP第4章知识点

文章目录 4.1 背景4.3 取样和取样函数的傅里叶变换4.5 二变量函数的傅里叶变换4.6 二维 DFT 和 IDFT 的一些性质4.6.6 二维离散卷积定理 4.7 频率域滤波基础4.7.3 频率域滤波步骤小结4.7.4 空间域和频率域滤波之间的对应关系 4.8 使用低通频率域滤波器平滑图像4.9 使用高通滤波…

Nacos作为服务注册中心简单示例

一、服务注册与发现场景 主要包含两个服务&#xff1a; zhshl-order服务: 作为服务消费者zhsl-stock服务: 作为服务提供者 当我们启用服务发现的时候,需要进行的操作主要有三步 0、前置条件,需要先搭建好一个nacas服务&#xff0c;可以是一个集群或者是单个nacos服务。可以…

MySQL 数据定义语言 DDL

文章目录 数据定义语言 DDL表的设计范式第一范式&#xff08;1NF&#xff09;第二范式&#xff08;2NF&#xff09;第三范式&#xff08;3NF&#xff09; 创建表修改表删除表截断表 数据定义语言 DDL 前面我们学习的 DML 语言&#xff0c;主要实现对数据的增、删、改等基本操作…

零基础入门网络安全必看的5本书籍(附书单pdf)

作为一个Java转行网络安全的过来人&#xff0c;我深知自学时的种种不易&#xff0c;同时也经常有粉丝朋友问我&#xff1a;刚入门应该怎么学、有哪些书籍推荐等问题&#xff0c;今天我就把我自己的学习书单分享给大家&#xff0c;希望对大家有帮助&#xff01; 一、5本必读书籍…

JVM垃圾回收——对象进入老年代

目录 1、什么是大对象以及大对象对垃圾回收的影响 2、什么情况下对象会进入老年代 2.1 当创建对象的大小超过-XX:PretenureSizeThreshold的设定值 2.2 长期存活的对象将进入老年代 2.3 动态年龄判定 2.4 空间担保分配 什么是空间分配担保&#xff1f; 为什么要…

uploads靶场通关(1-11关)

Pass-01&#xff08;JS校验&#xff09; 看题目我们准备好我们的php脚本文件&#xff0c;命名为1.php 上传该php文件&#xff0c;发现上传失败 方法一&#xff1a;将浏览器的JavaScript禁用 然后就能上传了 方法二&#xff1a; 查看源码&#xff0c;发现只能上传以下形式的文…

【Docker】LXC所实现的隔离性、Linux Namespace等讲解

前言 Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux或Windows操作系统的机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。 &#x1f4d5;作者简介&#xff1a;热…

域名解析异常有哪些办法?如何实现动态域名解析?

什么是域名解析&#xff1f; 域名解析就是把域名解析成一个ip地址&#xff0c;我们大多数人都喜欢记忆域名&#xff0c;但是机器只认识IP地址&#xff0c;只要这个IP地址对应相关域名&#xff0c;这就叫域名解析。 工作中常会遇到域名解析故障&#xff0c;比如访问站点对应的…

FPGA问答系列--Vivado Schematic中的实线和虚线有什么区别?

FPGA问答系列–Vivado Schematic中的实线和虚线有什么区别&#xff1f; 前言&#xff1a;本文章为FPGA问答系列&#xff0c;我们会定期整理FPGA交流群&#xff08;包括其他FPGA博主的群&#xff09;里面有价值的问题&#xff0c;并汇总成文章&#xff0c;如果问题多的话就每周…

ChatPPT一键制作PPT,效果拉满!

&#x1f4a7; C h a t P P T 一键制作 P P T &#xff0c;效果拉满&#xff01; \color{#FF1493}{ChatPPT一键制作PPT&#xff0c;效果拉满&#xff01;} ChatPPT一键制作PPT&#xff0c;效果拉满&#xff01;&#x1f4a7; &#x1f337; 仰望天空&#xff0c;妳我…

阿里云nginx配置https踩坑(配置完后访问显示无法访问此网站)

本人小前端一枚&#xff0c;最近在玩服务器部署自己的东西时踩了个坑&#xff01;&#xff01;&#xff01; server {listen 443 ssl;server_name localhost;ssl_certificate 证书.com.pem;ssl_certificate_key 证书.com.key;#后台管理静态资源存放location / { #文件目…