learn C++ NO.18——多态

什么是多态?

多态是C++面向对象编程中的一个核心概念,它允许程序在执行过程中,根据对象的实际类型来调用适当的函数。多态性主要通过继承和虚函数来实现,这使得代码更加灵活和可扩展。多态的条件如下:1、调用函数是重写的虚函数。2、基类指针或者引用。

虚函数的概念

被virtual修饰的成员函数就是虚函数。
在这里插入图片描述

虚函数的重写

虚函数重写是指派生类重新定义(或称为覆盖)了基类中的虚函数。当派生类中存在一个与基类中虚函数具有三同,即相同名称、参数列表和 返回值(在C++11及以后版本中,还包括const属性和volatile属性)的函数时,该函数就重写了基类中的虚函数。

下面通过一个代码样例来简单看一看
在这里插入图片描述
在这里插入图片描述

在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样写。

在这里插入图片描述

协变是虚函数重写的一个特例。协变是指派生类重写虚函数时,与基类虚函数返回值类型不同。但是基类虚函数返回值类型是基类的指针或引用。派生类虚函数的返回值类型是派生类的指针或引用。

在这里插入图片描述
析构函数的重写也是一个特殊的例子。如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

为什么需要重写虚函数呢?通过下面场景便可以明白。

在这里插入图片描述
可以看到在释放Student的切片时,编译器没有去调用派生类的析构函数释放派生类对象部分,这导致了内存泄露问题。因为delete底层是会去让p对象去调用它的析构函数,然后调用operator delete来释放空间。在这个场景中用户期望调用析构函数的行为是一个多态调用。所以,我们需要重写析构函数以达到正常释放派生类对象的目的。

在这里插入图片描述
C++11标准提供了两个关键字,override 和 final。可以用于帮助用户检测重写情况。

final:修饰虚函数,表示该虚函数不能再被重写。
在这里插入图片描述

final修饰的类不能被继承。
在这里插入图片描述

想让一个类不能被继承不仅仅可以用final修饰这个类,还可以通过私有构造函数来实现。不过私有构造函数后需要对外提供一个静态函数以实例化类对象。私有化析构函数也可以做到让类不能被继承。但是需要提供一个对外的清理资源的接口以供用户释放资源。下面以私有化构造函数为例。
在这里插入图片描述

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
在这里插入图片描述

多态的原理

虚函数表

在C++中,虚函数表是一种用于支持多态性的机制。当一个类中含有虚函数时,编译器会为该类生成一个虚函数表,这个表存储了该类的虚函数在代码段中的地址。在运行时,通过虚函数表,程序能够正确地调用指向派生类对象的基类指针或引用所调用的虚函数。下面通过样例简单看一看。

在这里插入图片描述
sizeof(Base)的值是多少呢?答案是8,因为Func1是一个虚函数,编译器会生成一个虚函数表来保存虚函数的地址。_b为一个字节,虚函数表占四个字节。根据内存对齐的原则,所以sizeof(Base)为8字节。

多态的原理

先通过调试窗口简单看一看基类和派生类究竟干了些啥。
在这里插入图片描述
在这里插入图片描述
通过调试窗口可以发现派生类对象Johnson也有一份虚函数表。并且虚函数表的内容与基类的内容不一样。这是因为派生类的虚函数完成了重写,将原本基类的虚函数进行了覆盖。所以虚函数的重写也称为覆盖。重写通常是语言层面的叫法,覆盖是底层实现层面的叫法。

虚函数表本质是一个存虚函数指针的指针数组,在VS平台下,一般情况这个数组最后面放了一个nullptr。g++平台不会这样处理。
在这里插入图片描述

派生类的虚表生成:首先,将基类中的虚表内容拷贝一份到派生类虚表中。其次,如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚函数表中基类的虚函数。最后,派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

vftptr也许会因为成员变量被定义在栈区或事堆区上,而存储在栈区或堆区上,但是vftptr所指向的虚函数表是存储在代码段上的。在VS平台下,虚函数存储和普通函数一样存储在代码段中。虚函数表也是存储在代码段中。需要注意的是虚函数表存储的不是虚函数,而是虚函数指针,即虚函数在代码段(常量区)中的地址。g++平台下虚函数表和虚函数都是存放代码段(常量区)中。
在这里插入图片描述
在这里插入图片描述

下图的Func函数如何做到多态调用呢?如果传递的是基类对象,它会直接在运行时通过基类的虚函数表找到对应的虚函数进行调用。派生类也是同理。而普通调用则是在编译时,编译器确定地址调用。
在这里插入图片描述
下面通过反汇编简单看一看多态调用与普通调用的区别。

在这里插入图片描述
在这里插入图片描述
通过上图可以看到,无论传递的是基类对象还是派生类对象,多态调用的指令都是一样的。基类多态调用,运行时编译器会去基类的虚函数表中找到虚函数地址,然后调用。而派生类传递的是基类的切片(切割),所以派生类中基类的部分,虚函数表会进行重写。编译器会去重写后的虚函数表调用派生类的虚函数。

动态绑定与静态绑定

静态绑定指的是程序在编译阶段就确定了程序的动作。这也称之为前期绑定。一般函数重载就是一种静态的多态。
在这里插入图片描述
动态绑定又称为后期绑定,指的是程序在运行后才能确定程序的动作。虚函数的重写就是一种动态绑定的行为。

关于继承多态的试题

下面做一个题来提升一下对于多态的理解。
在这里插入图片描述

看代码依次来进行分析,首先,主函数中new了一个B对象。然后通过这个对象调用test函数。test()函数一定是由this指针进行调用,所以可以理解成A* ->func()。但是func是一个重写的虚函数。所以应该调用的是B ::func()。这里最具迷惑性的坑来了,就是基类和派生类的func函数都给了默认参数。而这个默认参数是取基类的默认参数。因为虚函数的重写的是实现。所以这里的返回值、函数名、参数列表部分都是用的基类。最终输出的结果是 B->1

为什么基类对象不能构成多态呢?

若出现派生类对象赋值给基类对象切片时,并不会拷贝虚函数表。这是因为若基类对象构成多态势必拷贝虚函数表,那基类对象调用虚函数时,就会调用派生类的虚函数。那就乱套了。

基类对象不能构成多态是因为方法调用在编译时就已经确定,且不会通过虚函数表进行动态查找,所以无法调用派生类中的覆盖方法。要实现多态,必须使用基类指针或引用指向派生类对象。

在这里插入图片描述

打印虚函数表的程序

在这里插入图片描述
简单说明一下,上图的情况,基类Person有一个虚函数BuyTicket,两个成员函数Func1和Func2。派生类Student重写了BuyTicket()和一个虚函数Func3。通过监视窗口可以看到,Student对象的虚函数表没有Func3。下面,写一个程序来验证一下我们的猜想。
在这里插入图片描述
这里利用了VS平台的虚函数表的结尾时nullptr的特性对虚函数表进行了打印遍历。通过程序可以发现,Func3就是紧跟在Student类对象的虚函数表后面的。

关于多继承多态的问题

这里以下面的多继承为例。
在这里插入图片描述
这里有两个基类分别是Base1和Base2,分别有一个整型成员变量和两个虚函数func1和func2,有一个派生类Derive继承了Base1和Base2,也有一个整型成员变量,和一个重写的func1和虚函数func3。下面简单来看一下Derive定义的对象模型。

在这里插入图片描述
所以Derive对象的大小是20字节。下面通过一段代码,看看VS平台对于多继承的派生类的多态调用是如何进行处理的。

在这里插入图片描述
乍一看好像没有什么特别的,但是实际在底层汇编处理时,别有洞天。下面通过监视窗口简单瞅一瞅。

在这里插入图片描述
通过监视窗口可以看到,Base1的虚函数表中func1的地址和Base2中的func1的地址不一样。那为什么上面程序的结果是Base1对象的切片和Base2对象的切片调用的是同一个func1呢?下面通过反汇编一探究竟。
在这里插入图片描述
通过反汇编可以清晰的看到,VS平台编译器对Base2的虚函数表做了特殊处理,让派生类继承Base2的成员部分的虚函数表存的是对于this偏移量的处理的地址。本质上也是和Base1和d对象同一个func1的地址。至于为什么要这样处理?我推测根对象模型有关。由于先继承的基类会在后继承的基类的上面。而VS平台下,这里的Base1的虚函数表刚好就是在d对象的前四个字节。而Base2虚标则存储在d对象中的第8-11个字节的位置上。编译器会先去Base2虚函数表地址中存储的this指针中 - 8的位置找func1。而Base1和d的虚函数表恰好处于首地址。所以不需要想Base2那样处理。

抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

接口继承和实现继承

普通的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。**虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。**所以如果不实现多态,不要把函数定义成虚函数。这样会浪费资源。

inline函数可以是虚函数吗?

答案是可以,但是,在VS平台下,虚函数用inline修饰,这时候编译器会忽略inline属性,那这个函数就不再是内联函数。因为虚函数要存放在虚函数表中。
在这里插入图片描述

静态成员函数可以是虚函数吗?

答案是不行。有以下几种原因。
从对象与虚函数表层面看,静态成员没有this指针。它不与类对象关联,只与类关联。因此它不能被存放在虚函数表中。
从多态性和动态绑定层面上看,静态成员函数,在编译时即确定函数地址。这与虚函数在执行时确认函数地址来说是相违背的。因此静态成员函数与动态绑定即只相违背。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Golang | Leetcode Golang题解之第420题强密码检验器

题目: 题解: func strongPasswordChecker(password string) int {hasLower, hasUpper, hasDigit : 0, 0, 0for _, ch : range password {if unicode.IsLower(ch) {hasLower 1} else if unicode.IsUpper(ch) {hasUpper 1} else if unicode.IsDigit(ch)…

DataX实战:从MongoDB到MySQL的数据迁移--修改源码并测试打包

在现代数据驱动的业务环境中,数据迁移和集成是常见的需求。DataX,作为阿里云开源的数据集成工具,提供了强大的数据同步能力,支持多种数据源和目标端。本文将介绍如何使用DataX将数据从MongoDB迁移到MySQL。 环境准备 安装MongoDB…

【亿美软通-注册/登录安全分析报告】

前言 由于网站注册入口容易被黑客攻击,存在如下安全问题: 暴力破解密码,造成用户信息泄露短信盗刷的安全问题,影响业务及导致用户投诉带来经济损失,尤其是后付费客户,风险巨大,造成亏损无底洞…

ehcarts生成彩虹图,半圆饼图,蚊香图等

其实这三种图我都认为是一种图,都是饼图的变形,需求长这样子 我的效果图这样子 想要一模一样自己改改颜色就行了 我们先生成完整的蚊香图,众所周知,我们正常的饼图只是把seriesData配置一次,然后把数据全部塞进去&a…

9/24作业

1. 分文件编译 分什么要分文件编译? 防止主文件过大,不好修改,简化编译流程 1) 分那些文件 头文件:所有需要提前导入的库文件,函数声明 功能函数:所有功能函数的定义 主函数:main函数&…

【AI视频】Runway:Gen-2 图文生视频与运动模式详解

博客主页: [小ᶻZ࿆] 本文专栏: AI视频 | Runway 文章目录 💯前言💯仅图片生成视频方法一:通过Midjourney生成图片方法二:通过Runway预览生成图片注意点 💯图加文生成视频方式一:Midjourney…

中年被裁,记录下这段时间的心路历程,内含前端面试题和面经

前言 真正的转变都是痛苦且无声的。 大家好啊,好久不见,停更了一个月了,最近确实没时间更新我的公益服游戏,这段时间我经历了工作被裁员,学习复习,面试找工作,到最终找到工作。想把这段时间我的…

为什么 ECB 模式不安全

我们先来简单了解下 ECB 模式是如何工作的 ECB 模式不涉及链接模式,所以也就用不着初始化向量,那么相同的明文分组就会被加密成相同的密文分组,而且每个分组运算都是独立的,这也就意味着可以并行提高运算效率,但也正是…

电脑ip地址怎么换地区:操作步骤与利弊分析

在当今全球化的信息时代,人们经常需要访问不同地区的网络资源。然而,由于地理位置的限制,某些内容或服务可能只对特定地区的用户开放。这时,更换电脑IP地址的地区就成为了一个实用的解决方案。本文将详细介绍两种更换电脑IP地址地…

WebRTC关键技术及应用场景:EasyCVR视频汇聚平台高效低延迟视频监控解决方案

众所周知,WebRTC是一项开源的实时通信技术,它通过集成音频、视频和数据传输到Web浏览器中,使得实时通信变得简单且无需任何插件或第三方软件。WebRTC不仅是一个API,也是一系列关键技术和协议的集合,它的出现改变了传统…

羽毛球场馆预约系统,便捷管理预约

全国羽毛球运动的热度不断上升,在健身行业中掀起了一股羽毛球热潮。同时羽毛球运动的风靡,也吸引了不少人入局,各种大大小小的羽毛球馆不断出现,为大众的羽毛球喜好提供了场地。 随着互联网的发展,羽毛球馆也开始向线…

共享单车轨迹数据分析:以厦门市共享单车数据为例(六)

副标题:.基于POI数据的站点功能混合度探究——以厦门市为例 为了保证数据时间尺度上的一致性,我们从互联网上下载了2020年的POI数据,POI数据来源于高德地图 API平台,包括名称、大小类、地理坐标等。并将高德地图 POI数据的火星坐标 系 GCJ-0…

idea 创建多模块项目

一、新建项目,创建父工程 新建项目,选择 spring initializr 填写相关信息后提交 删除不相关的目录,如下 修改打包方式为 pom,在 pom.xml 文件中新增一行,如下 二、创建子模块 新增子模块 填写子模块信息&#x…

《机器学习》周志华-CH8(集成学习)

8.1个体与集成 集成学习(ensemble learning)通过构建并结合多个学习器来完成学习任务,有时也被称为多分类器系统,基于委员会的学习。 同质”集成“:只包含同种类型的个体学习器,同质集成中的个体学习器亦称“基学习器”&#xff0…

C# winforms 使用菜单和右键菜单

初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github:codetoys,所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的,可以在任何平台上使用。 源码指引:github源…

累加求和-C语言

1.问题: 计算123……100的和,要求分别用while、do while、for循环实现。 2.解答: 累加问题,先后将100个数相加。要重复进行100次加法运算,可以用循环结构来实现。重复执行循环体100次,每次加一个数。 3.代…

【C++掌中宝】C++ 中的空指针救世主——nullptr

文章目录 1. 什么是 NULL?2. NULL 在 C 和 C 中的区别3. C11 引入 nullptr 的原因4. nullptr 与 NULL 的区别5. nullptr 的应用场景6. 模拟 nullptr 的实现7. 总结结语 1. 什么是 NULL? 在 C 和 C 编程中,NULL 常用于表示空指针,…

如何修改音频的音量增益

一、前言 在开发音频相关的功能(比如说语音通话、播放音乐)时,经常会遇到音量太小的问题,这时候就需要我们对原始数据进行处理。本文将介绍如何通过修改原始音频数据来增加增益,本文包含如下内容: 1.音频数…

【Java】虚拟机(JVM)内存模型全解析

目录 一、运行时数据区域划分 版本的差异: 二、程序计数器 程序计数器主要作用 三、Java虚拟机 1. 虚拟机运行原理 2. 活动栈被弹出的方式 3. 虚拟机栈可能产生的错误 4. 虚拟机栈的大小 四、本地方法栈 五、堆 1. 堆区的组成:新生代老生代 …

速通LLaMA3:《The Llama 3 Herd of Models》全文解读

文章目录 概览论文开篇IntroductionGeneral OverviewPre-TrainingPre-Training DataModel ArchitectureInfrastructure, Scaling, and EfficiencyTraining Recipe Post-TrainingResultsVision ExperimentsSpeech Experiments⭐Related WorkConclusionLlama 3 模型中的数学原理1…