【C++】多态(上)超详细

封装,继承,多态不只是C++的三大特性,而是面向对象编程的三大特性。

什么是多态:

不同的对象做同一件事情,结果会出现多种形态。

1.满足多态的几个条件

1.父子类完成虚函数重写(需要满足三同:函数名,参数,返回值都需要相同)。

2.父类的指针或引用去调用虚函数。(指向谁就调用谁的虚函数)

既然有了父类和子类,那么说明多态发生的前提是继承。

例子:

不同身份的人买票的价格是不同的。

但如果不满足多态的条件,就达不到我们想要的结果:

2.多态的坑

2.1虚函数重写的两个例外

强调一下:

1.返回值只能是父类返回父类的指针或引用,子类返回子类的指针或引用,顺序不能反过来。

2.返回值可以是其他不相关的父子类,也可以是自己的父子类。

关于析构函数的重写

如果我们正常的写析构函数,看看它们调用的情况:

可见,两次析构都调用的是基类的,这是正常现象,两个Person的指针自然调用Person的析构。但这并不是我们的目的,我们想要的是指针指向谁就应该调用谁的析构,也就是说:我们想让P2调用Student的析构,只有这样才满足多态的规则。

但如果我们用virtual修饰这两个析构函数呢:

加上virtual就达到了我们的目的,也就是说父子类的析构函数构成虚函数重写。

但有些奇怪,这两个析构函数的名字明明不同,不符合多态的语法,为什么依然可以构成重写呢?

其实这里编译器会把析构函数的名字全部换成destructor,这样就满足多态的语法了!

那C++的语法为什么不把析构函数的名字直接定义成destructor,而是私下换名字呢?这样不麻烦嘛?

其实这也是无奈之举,因为析构函数的概念早于多态,在多态之前已经把析构函数的名字设计好了,祖师爷也没想到后面设计多态的语法时会在这个地方有坑,只能自己私下改名字了。

结论:建议把析构函数写成虚函数,防止内存泄漏的发生。

其实还有一个例外,就是派生类可以不写virtual。

这也是C++语法常常被吐槽的点,但还是建议写上,不然容易被人吐槽。

2.2一道杀人诛心的面试题

这道题目曾被多家大型公司(百度,腾讯等)当作面试题。

先说答案:B。是不是有点匪夷所思?

思路:

继承只是一个形象的说法,实际上在继承时并没有把父类的成员拷贝到子类中,而是用了一套查找规则:在P指针调用父类函数时,编译器会先在子类中找,没找到再去父类去找,所以父子类的同名函数会构成隐藏。

所以在给test函数传this指针时,this的类型是A*。但用this调用func时依然构成多态调用,因为this依然指向的是B类型,所以会直接调B中的func。

下面就到这个题目特别坑的点:多态的虚函数重写,重写的只是函数体的实现。意思就是:

子类的func是对父类func的重写,但只是重写了函数体的实现,函数的结构部分依然用父类的。所以val的值是1。

那咱们回过头来想,既然子类的函数体结构部分没有调用,那可以省略virtual好像也有些道理。

3.关键字override和final

3.1final

如果让你设计一个不能被继承的类,其实有两种方法。

方法一:

把基类的构造函数定义为私有。(C++98)

原因是:基类的构造函数在派生类中不可访问,那么派生类就无法对象实例化。

方法二:

利用关键字final。(C++11)

方法二很简单,就是用final修饰基类之后就无法继承了。

3.2override

override是加到派生类的重写虚函数中,用于检查是否完成重写。

成功重写时,是没有任何报错的。

没有成功重写就会报错,所以我们在写代码时尽量把这个关键字加上。

4.对比重载/重写/隐藏

5.多态的底层

5.1虚函数表指针

这道题的答案是12。因为Base类中有一个虚函数,所以在成员对象中就会多出一个指针,叫虚函数表指针,简称虚表指针。

这个指针指向了一张表,这个表里存放了虚函数的指针。

在x86平台下:

通过对比监视窗口和内存窗口可以看到在地址0x00E17B34位置存放了00e112df指针,在地址0x00E17B38位置存放了00e1124e指针。

5.2虚函数表指针的作用

下面我们来看一看编译器是如何通过虚表指针实现多态的。

这是Mike的对象模型和监视窗口:

这是John的对象模型和监视窗口:

通过对比Mike和John的对象模型发现:

在John的对象模型中004a9b54和下面的1是继承Mike的,1下面的2是John的成员变量。

虽然是继承下来的但有些不一样:

继承下来的虚表指针和Mike的虚表指针不一样,一个是004a9b34一个是004a9b54。

既然虚表指针不一样,那虚表指针里面的函数指针也应该不一样,观察监视窗口,我们发现Mike的虚表指针中存放的是Person的BuyTickt函数,John的虚表中存放的是Student的BuyTickt函数。

所以,多态的实现过程就是通过虚表指针!当Student对象传给Person对象时,通过切片把虚表指针切过去,然后通过Student的虚表指针调用Student的虚函数。当Person对象传给Person对象时,通过Person的虚表指针调用Person的虚函数。

但是当不满足多态语法时,编译器先检查,如果不满足多态语法,编译器就直接通过对象类型去调用成员函数,就不会通过虚表指针调用了。

补充一下:

每个对象都有一张虚表,同类型的对象共用一张虚表,不同类型的对象虚表不同。

6.单继承中的虚表

Derive继承Base后,通过监视窗口查看它们的虚表发现:Base的虚表是正常的里面有两个函数指针Func1和Func2。但是Derive的虚表有问题,有Derive的Func1和Base的Func2。

其实在继承后,派生类的虚表可以形象的说:把基类的虚表拷贝下来,如果构成重写,那么派生类的重写的函数把基类的覆盖掉。所以Func1是Derive的,Func2是Base的。

但问题是Func3和Func4哪里去了呢?

这也是VS的bug,其实VS的监视窗口有些时候并不准确,还要去内存窗口看一下!

通过对比监视窗口和内存窗口,我们发现Derive的虚表中好像存了4个函数指针:

前两个002b1410和002b1389和监视窗口中的一致,但后两个还不能确定,只能说比较像,所以我们需要写一个程序来验证我们的猜想。

验证结果:Derive的虚表中存了4个函数指针!

解释一下这个程序,写这个程序要求对指针的理解程度极高,如果你能看懂这个程序那么你在C语言指针方面的掌握非常好,如果能写出这个程序,那么你对指针的理解已经达到优秀了!

首先,虚表本质上是一个函数指针数组。我们平时传参传数组时,C语言考虑到效率问题往往传的是首元素的地址,比如一个int型的数组传参时传int*的指针。那么虚表中存放的全是函数指针,所以我们传参的时候应该传函数指针的地址,也就是二级指针。

那这个二级指针如何获得呢?在C语言部分,大家都知道,数组名就是首元素的地址,所以我们只需要得到虚表的名字(是d的成员变量)就可以了,也就是Derive d中的前4个字节。

难道将d强制类型转换成int就可以了嘛?这是不可以的,两个完全不想关的两个类型是不能强转的。这里给大家总结一下:1.int和float可以互相强转(char本质上也属于int型)2.任何类型的指针都可以相互强转 3.任何类型的指针可以和int相互强转。

知道这个知识后就可以取d的地址,前强转成int*,再解引用,这样就拿到了d的前4个字节,但这4个字节的类型是int,它真正的类型应该是虚表中首元素的地址,也就是Func1的指针的地址,强转过去后直接函数传参,然后采用数组下标的方式变量整个虚表就可以访问到虚表中所有的函数指针,然后再通过函数指针调用对应的函数就可以确认猜想!

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

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

相关文章

GD32用ST-Link出现internal command error的原因及解决方法

一、GD32 F407烧录时出现can not reset target shutting down debug session 搜寻网上资料,发现解决方式多种多样,做一个简单的总结: 1.工程路径包含中文名 2.需更改debug选项 3.引脚冲突 4.杜邦线太长 而先前我的工程路径包含中文名也仍…

面试集中营—Seata分布式事务

一、分布式事务 本地事务 在计算机系统中,更多的是通过关系型数据库来控制事务,这是利用数据库本身的事务特性来实现的, 因此叫数据库事务,由于应用主要靠关系数据库来控制事务,而数据库通常和应用在同一个服务器&am…

搭建Springboot的基础开发框架-02

本系列专题虽然是按教学的深度来定稿的,但在项目结构和代码组织方面是按公司系统的要求来书定的。在本章中主要介绍下基础开发框架的功能。后续所有章节的项目全是在本基础框架的基础上演进的。 工程结构介绍 SpringbootSeries:父工程,定义一…

贪吃蛇(c实现)

目录 游戏说明: 第一个是又是封面,第二个为提示信息,第三个是游戏运行界面 游戏效果展示: 游戏代码展示: snack.c test.c snack.h 控制台程序的准备: 控制台程序名字修改: 参考&#xff1a…

vs2019 cpp20 规范的线程头文件 <thread> 注释并探讨两个问题

(1)学习线程,与学习其它容器一样,要多读 STL 库的源码。很多知识就显然而然的明白了。也不用死记硬背一些结论。上面上传了一份注释了一下的 源码。主要是补充泛型推导与函数调用链。基于注释后的源码探讨几个知识点。 STL 库的多…

上位机图像处理和嵌入式模块部署(树莓派4b的软件源)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing 163.com】 很多文章都建议替换一下树莓派4b的软件源,不过我自己实际使用下来,官方的软件下载速度其实还可以。这里下载的时候&#xf…

mybatis-plus使用指南(1)

快速开始 首先 我们 在创建了一个基本的springboot的基础框架以后&#xff0c;在 pom文件中 引入 mybatisplus的相关依赖 <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5…

猫头虎分享已解决Bug || 已解决ERROR: Ruby Gems安装中断 ⚠️ Bug 报告:Gem::RemoteFetcher::FetchError

猫头虎分享已解决Bug || 已解决ERROR: Ruby Gems安装中断 ⚠️ Bug 报告&#xff1a;Gem::RemoteFetcher::FetchError 博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; …

树莓派安装opencv

安装opencv 上述步骤完成后&#xff0c;输入以下代码(基于python3) sudo apt-get install python3-opencv -y不行的话&#xff0c;试试换源&#xff0c;然后 sudo apt-get update成功&#xff01; 测试opencv是否安装成功 输入 python3 然后再输入 import cv2 没有报错就…

java jdk1.8下载与安装

一、下载 1.下载jdk1.8安装包 官网下载地址&#xff1a;Java Downloads | Oracle 打开官网链接&#xff0c;下滑至Java 8模块&#xff0c;选取自己电脑适合的版本点击下载 二、安装 1.找到我们下载的安装包&#xff0c;双击运行 2.点击下一步 3.点击更改&#xff0c;修改安…

字典是如何实现的?Rehash 了解吗?

字典是 Redis 服务器中出现最为频繁的复合型数据结构。除了 hash 结构的数据会用到字典外&#xff0c;整个 Redis 数据库的所有 key 和 value 也组成了一个 全局字典&#xff0c;还有带过期时间的 key 也是一个字典。(存储在 RedisDb 数据结构中) 字典结构是什么样的呢&#xf…

Linux重定向及缓冲区理解

重定向&#xff1a; 在上一期虚拟文件系统中讲到了每个进程在打开后&#xff0c;都会默认打开3个文件&#xff0c;如下&#xff1a; stdin 标准输入&#xff08;键盘&#xff09; 文件描述符&#xff1a;0 stdout 标准输出&#xff08;显示器&#xff09;文件描述符&a…

IT项目管理-小题计算【太原理工大学】

1.合同总价问题 问承包商的利润是&#xff1f; 实际利润目标利润&#xff08;目标成本-实际成本&#xff09;*卖方分担比例 解&#xff1a;10 000&#xff08;100 000 - 90 000&#xff09;* 0.2 12 000&#xff08;元&#xff09; 实际成本有时也写作最终成本&#xff0c;问承…

XYCTF - web

目录 warm up ezMake ezhttp ezmd5 牢牢记住&#xff0c;逝者为大 ezPOP 我是一个复读机 ezSerialize 第一关 第二关 第三关 第一种方法&#xff1a; 第二种方法&#xff1a; ez?Make 方法一&#xff1a;利用反弹shell 方法二&#xff1a;通过进制编码绕过 ε…

logback日志持久化

1、问题描述 使用logback持久化记录日志。 2、我的代码 logback是Springboot框架里自带的&#xff0c;所以只要引入“spring-boot-starter”就行了。无需额外引入logback依赖。 pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns&…

班级综合测评|基于Springboot+vue的班级综合测评管理系统(源码+数据库+文档)

目录 基于Springbootvue的“智慧食堂”系统 一、前言 二、系统设计 三、系统功能设计 1 管理员功能模块 2学生功能模块 3教师功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大…

(Java)心得:LeetCode——18.四数之和

一、原题 给你一个由 n 个整数组成的数组 nums &#xff0c;和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] &#xff08;若两个四元组元素一一对应&#xff0c;则认为两个四元组重复&#xff09;&#xff1a; …

环境变量(全)

概念 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数 如&#xff1a;我们在编写C/C代码的时候&#xff0c;在链接的时候&#xff0c;从来不知道我们的所链接的动态静态库在哪里&#xff0c;但是照样可以链接成功&#xff0c;生成可执…

韩顺平0基础学Java——第10天

p202-233 类与对象&#xff08;第七章&#xff09; 成员方法 person类中的speak方法&#xff1a; 1.public表示方法是公开的 2.void表示方法没有返回值 3.speak&#xff08;&#xff09;中&#xff0c;speak表示方法名&#xff0c;括号是形参列表。 4.大括号为方法体&am…

Oracle 流stream数据的复制

Oracle 流stream数据的复制 --实验的目的是捕获scott.emp1表的变化&#xff0c;将变化应用到远程数据库scott.emp1表中。 --设置初始化参数 AQ_TM_PROCESSES1 COMPATIBLE9.2.0 LOG_PARALLELISM1 GLOBAL_NAMEStrue JOB_QUEUE_PROCESSES2 --查看数据库的名称&#xff0c;我的为o…