【C++进阶】多态

在这里插入图片描述

👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨


目录

  • 一、 多态的概念
  • 二、多态的定义及实现
      • 2.1 虚函数
      • 2.2 虚函数的重写(覆盖)
      • 2.3 多态的构成条件(重点)
      • 2.4 多态构成条件的两个例外
      • 2.5 析构函数的重写(面试常考)
  • 三、override和final(支持C++11)
      • 3.1 override
      • 3.2 final
  • 四、重载、覆盖(重写)、隐藏(重定义)的对比
  • 五、多态原理
      • 5.1 虚函数表

一、 多态的概念

多态是面向对象三大基本特征中的最后一个。概念:通俗来说,就是多种形态,具体点就是当不同的对象去完成某个行为,就会产生出不同的状态。

比如在购买高铁票时,成人原价,学生半价,而军人可以优先购票,对于购票这一相同的动作,需要根据不同的对象提供不同的方法

#include <iostream>
using namespace std;class Adult // 成人
{
public:virtual void Buyticket(){cout << "成人-原价" << endl;}
};class Student : public Adult
{
public:virtual void Buyticket(){cout << "学生-半价" << endl;}
};class Soldier : public Adult
{
public:virtual void Buyticket(){cout << "军人-优先" << endl;}
};void Buyticket(Adult& At)
{At.Buyticket();
}int main()
{Adult at;Student s;Soldier sd;Buyticket(at); // 成人Buyticket(s); // 学生Buyticket(sd); // 军人return 0;
}

【输出结果】

可以看到,不同对象调用同一函数,执行结果是不同

在这里插入图片描述

二、多态的定义及实现

2.1 虚函数

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

在这里插入图片描述

2.2 虚函数的重写(覆盖)

虚函数的重写(覆盖)子类中有一个跟父类完全相同的虚函数,即子类虚函数与父类虚函数的返回值类型、函数名字、参数列表完全相同,称子类的虚函数重写了父类的虚函数。

// 父类
class Adult // 成人
{
public:virtual void Buyticket(){cout << "成人-原价" << endl;}
};// 子类
class Student : public Adult
{
public:virtual void Buyticket(){cout << "学生-半价" << endl;}
};

2.3 多态的构成条件(重点)

在继承中要构成多态还有两个条件

  1. 必须通过父类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且子类必须对父类的虚函数进行重写(返回值类型、函数名字、参数列表的类型完全相同)

注意:上述两个构成多态的条件缺一不可!缺少其中任意一个条件,都不构成多态!

// 父类
class Adult // 成人
{
public:virtual void Buyticket(){cout << "成人-原价" << endl;}
};// 子类
class Student : public Adult
{
public:// 子类必须对父类的虚函数进行重写virtual void Buyticket(){cout << "学生-半价" << endl;}
};// 1. 通过父类的指针或者引用调用虚函数
// void Buyticket(Adult* At) - 指针
void Buyticket(Adult& At) // 引用
{// 2. 被调用的函数必须是虚函数At.Buyticket();
}

2.4 多态构成条件的两个例外

  • 例外一:子类虚函数可以不使用virtual修饰

在这里插入图片描述

虽然这个例外在语法上是支持的,但是建议不要省略,因为会破坏代码的可阅读性,可能无法让别人一眼看出多态。

  • 例外二:协变(父类与子类虚函数返回值类型可以不同)

子类重写基类虚函数时,与父类虚函数返回值类型不同。即 父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用时,称为协变。

第一种:返回各对象的指针

class Adult // 成人
{
public:// 父类虚函数返回父类对象的指针virtual Adult* Buyticket(){cout << "成人-原价" << endl;return 0;}
};
// 子类
class Student : public Adult
{
public:virtual Student* Buyticket(){cout << "学生-半价" << endl;return 0;}
};

第二种:返回各对象的引用

class Adult // 成人
{
public:// 父类虚函数返回父类对象的引用virtual const Adult& Buyticket(){cout << "成人-原价" << endl;return Adult(); // 返回匿名对象 }
};
// 子类
class Student : public Adult
{
public:virtual const Student& Buyticket(){cout << "学生-半价" << endl;return Student();}
};

注意:父子类关系的指针/引用,不是必须是自己的,也可以是其他类的,但是要对应匹配子类和父类。

class A  // 父类
{};class B : public A // 子类
{};class Adult // 成人
{
public:// 父类虚函数返回父类对象的引用virtual const A& Buyticket(){cout << "成人-原价" << endl;return A(); // 返回匿名对象 }
};
// 子类
class Student : public Adult
{
public:virtual const B& Buyticket(){cout << "学生-半价" << endl;return B();}
};

还有一点要注意的是,不可以一个是指针,一个是引用,必须同时是指针,或者同时是引用

2.5 析构函数的重写(面试常考)

有个问题:析构函数加上virtual是不是虚函数重写?

答案:是。虽然父类与子类析构函数名字不同(不满足三重),看起来违背了重写的规,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

#include <iostream>
using namespace std;class A
{
public:virtual ~A(){cout << "~A()" << endl;}
};class B : public A
{
public:virtual ~B(){cout << "~B()" << endl;}
};int  main()
{A a;B b;return 0;
}

【输出结果】

在这里插入图片描述

接下来就是面试官的连续“攻击”:为什么要这样处理呢?— 因为要构成重写

那么为什么要让它们构成重写呢?其实不加virtual关键字也是可以的

在这里插入图片描述

但如果不对析构重写的话,那么下面有一个场景是过不了的(记住此场景)

#include <iostream>
using namespace std;class A // 父类
{
public:~A(){cout << "~A()" << endl;}
};class B : public A // 子类
{
public:~B(){cout << "~B()" << endl;delete[] ptr;}
protected:int* ptr = new int[3];
};int  main()
{A* a = new A;delete a;a = new B;delete a;return 0;
}

【输出结果】

在这里插入图片描述

我们发现,不加virtual没有调用子类的析构函数,发生了内存泄漏。那为什么没有调到子类的析构呢?第一次释放了a指向的空间,然后又改变了指向

在前面说过,类的析构函数都被处理成了destructor这个函数。而delete对于自定义类型的原理是:

  1. 在空间上执行析构函数,完成对象中资源的清理工作
  2. 调用operator delete函数释放对象的空间(operator delete本质就是调用free函数)

即对于delete a,先调用了析构函数a->destructor(),然后再调用operator delete函数释放对象的空间。

但由于编译器将析构函数名处理成一样的函数名destructor,因此构成了隐藏/重定义了。而且a刚好是A类型的指针,是一个普通的调用,不是多态调用。对于普通调用,看的是当前者的类型。因此delete a就会再次调用A类的析构函数。

但我们想的是指向什么类型,就去调用对应的析构函数,因此这是就得用到多态了。多态调用:看的是其指向的类型,指向什么类型,调用什么类型。

  • 因此,为什么要在 父类/基类 的析构函数中加上virtual修饰?

答案:为了构成多态,确保不同对象的析构函数能被成功调用,避免内存泄漏。

三、override和final(支持C++11)

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重写,而这种错误在编译期间是不会报错的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了overridefinal两个关键字,可以帮助用户检测是否重写。

3.1 override

作用:修饰子类的虚函数,检查是否构成重写(是否满足重写的必要条件),若不满足,则报错

先以正确代码为例,override要写在子类函数括号的后面

#include <iostream>
using namespace std;class A
{
public:virtual void Print(){cout << "class A" << endl;}
};class B : public A
{
public:virtual void Print() override{cout << "class B : public A" << endl; }
};void Print(A& a)
{a.Print();
}int main()
{A a;B b;Print(a);Print(b);return 0;
}

【输出结果】

在这里插入图片描述

以下故意在子类的虚函数加个参数(不构成三重:子类虚函数与父类虚函数的返回值类型、函数名字、参数列表的类型完全相同),看看是否会报错

在这里插入图片描述

3.2 final

作用:修饰父类的虚函数,不让子类的虚函数与其构成重写,即不构成多态

对父类的虚函数加上final:无法构成重写

在这里插入图片描述

除此之外,final在某些场景下很实用:final还可以修饰父类,修饰后,父类不可被继承。

在这里插入图片描述

注:final可以修饰子类的虚函数,因为子类也有可能成为父类;但override无法修饰父类的虚函数,因为父类之上没有父类了,自然无法构成重写。

四、重载、覆盖(重写)、隐藏(重定义)的对比

面试题中也喜欢考这三者的区别

  • 重载:即函数重载。在同一个作用域中,通过参数的类型、个数或顺序不同来定义多个具有相同函数名但不同参数列表的方法。重载方法在编译时根据调用的参数匹配最合适的方法。

  • 重写(覆盖):发生在类中,当出现虚函数且符合重写的三同原则(函数名、参数列表的类型和返回类型必须与父类中的方法一致)时,则会发生重写(覆盖)行为

  • 重定义(隐藏):发生在类中,当子类中的函数名与父类中的函数名起冲突时,会隐藏父类同名函数,默认调用子类的函数。如果想使用父类的同名成员,可以通过::指定调用。

重写和重定义比较容易记混,简言之 先看看是否为虚函数,如果是虚函数且三同,则为重写;若不是虚函数且函数名相同,则为重定义。注:在类中,仅仅是函数名相同(未构成重写的情况下),就能触发 重定义(隐藏)

在这里插入图片描述

五、多态原理

5.1 虚函数表

多态究竟是如何实现的?先来看一段简单的代码,同时也是一道笔试题。

#include <iostream>
using namespace std;class A
{
public:virtual void Test(){cout << "class A" << endl;}
};int main()
{A a; cout << "A sizeof:" << sizeof(a) << endl;return 0;
}

【输出结果】

在这里插入图片描述

上述代码仅仅只是一个空类,只有一个虚函数,而我们知道对象是不存储函数的。而这里连成员变量也没有,结果为什么是4呢?当把环境改为64位平台(x64),输出结果却是8

在这里插入图片描述

这样下来,大小随平台而变的只能是指针了,因此可以推测当前类中藏着一个虚表指针

在这里插入图片描述

如上图所示,对象中的这个指针我们叫做虚函数表指针(v代表virtualf代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

另外,从这里我们也可以知道:一般不使用多态的话,最好还是不要加上virtual,因为是有开销的。


那么派生类中这个表放了些什么呢?我们接着往下分析

针对上面的代码我们做出以下改造

class A
{
public:virtual void Func1(){cout << "A::Func1()" << endl;}virtual void Func2(){cout << "A::Func2()" << endl;}void Func3(){cout << "A::Func3()" << endl;}protected:int _a = 1;
};class B : public A
{
public:virtual void Func1() override{cout << "B::Func1()" << endl;}
};int main()
{A a;B b;return 0;
}

在这里插入图片描述

vs的监视窗口中,可以看到只要涉及虚函数类的对象中都有属性__vfptr(虚表指针),可以通过虚表指针所指向的地址,找到对应的虚表。

  1. 子类对象b中也有一个虚表指针,b对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
  2. 父类a对象和子类b对象虚表是不一样的,这里我们发现Test完成了重写,所以b的虚表中存的是重写的B::Test,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func继承下来后是虚函数,所以放进了虚表,Print也继承下来了,但是不是虚函数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr
  5. 总结一下子类的虚表生成:
    ①先将父类中的虚表内容拷贝一份到子类虚表中
    ②如果派生类重写了父类中某个虚函数,用子类自己的虚函数覆盖虚表中父类的虚函数。
    ③子类自己新增加的虚函数按其在子类中的声明次序增加到子类虚表的最后。

这里还有很容易混淆的问题:虚函数存在哪的?虚表存在哪的?
答:很多人都认为虚函数存在虚表,虚表存在对象中。但是这是错的。
注意:虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是它的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。
那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的。

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

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

相关文章

Unity实现2D游戏跟随摄像机(平滑移动)

文章目录 玩家角色脚本字段跟随逻辑 完整代码其他相关文章连接 玩家角色 首先创建一个可用的玩家角色&#xff0c;写好移动逻辑&#xff0c;如果要使用在Unity商店中购买的资源&#xff0c;可以点击Window菜单栏> Package Manager选项&#xff0c;来打开Package Manager窗口…

应急响应-Windows挖矿实战

0x00 主机表现 windows主机cpu拉满&#xff0c;主机卡顿&#xff0c;初步判断为中了挖矿病毒 0x00 处置 通过cpu拉满状态&#xff0c;定位初步的进程文件&#xff0c; 通过进程得到的文件上传沙箱&#xff0c;结果显示为恶意文件&#xff0c; 定位到文件夹&#xff0c; 存…

【C++ Core Guidelines解析】深入理解现代C++的特性和原理

文章目录 &#x1f468;‍⚖️《C Core Guidelines解析》的主要观点&#x1f468;‍&#x1f3eb;《C Core Guidelines解析》的主要内容&#x1f468;‍&#x1f4bb;作者介绍 &#x1f338;&#x1f338;&#x1f338;&#x1f337;&#x1f337;&#x1f337;&#x1f490;&a…

蚂蚁开源编程大模型,提高开发效率

据悉&#xff0c;日前蚂蚁集团首次开源了代码大模型 CodeFuse&#xff0c;而这是蚂蚁自研的代码生成专属大模型&#xff0c;可以根据开发者的输入提供智能建议和实时支持&#xff0c;帮助开发者自动生成代码、自动增加注释、自动生成测试用例、修复和优化代码等kslouitusrtdf。…

3D封装技术发展

长期以来&#xff0c;芯片制程微缩技术一直驱动着摩尔定律的延续。从1987年的1um制程到2015年的14nm制程&#xff0c;芯片制程迭代速度一直遵循摩尔定律的规律&#xff0c;即芯片上可以容纳的晶体管数目在大约每经过18个月到24个月便会增加一倍。但2015年以后&#xff0c;芯片制…

ffmpeg-android studio创建jni项目

一、创建native项目 1.1、选择Native C 1.2、命名项目名称 1.3、选择C标准 1.4、项目结构 1.5、app的build.gradle plugins {id com.android.application }android {compileSdk 32defaultConfig {applicationId "com.anniljing.ffmpegnative"minSdk 25targetSdk 32…

使用Vue + axios实现图片上传,轻松又简单

目录 一、Vue框架介绍 二、Axios 介绍 三、实现图片上传 四、Java接收前端图片 一、Vue框架介绍 Vue是一款流行的用于构建用户界面的开源JavaScript框架。它被设计用于简化Web应用程序的开发&#xff0c;特别是单页面应用程序。 Vue具有轻量级、灵活和易学的特点&#xf…

AI系统论文阅读:SmartMoE

提出稀疏架构是为了打破具有密集架构的DNN模型中模型大小和计算成本之间的连贯关系的——最著名的MoE。 MoE模型将传统训练模型中的layer换成了多个expert sub-networks&#xff0c;对每个输入&#xff0c;都有一层special gating network 来将其分配到最适合它的expert中&…

【C#实战】控制台游戏 勇士斗恶龙(3)——营救公主以及结束界面

君兮_的个人主页 即使走的再远&#xff0c;也勿忘启程时的初心 C/C 游戏开发 Hello,米娜桑们&#xff0c;这里是君兮_&#xff0c;最近开始正式的步入学习游戏开发的正轨&#xff0c;想要通过写博客的方式来分享自己学到的知识和经验&#xff0c;这就是开设本专栏的目的。希望…

中国电信研究院发布《5G+数字孪生赋能城市数字化应用研究报告》

9月5日&#xff0c;中国电信研究院战略发展研究所联合中关村智慧城市产业技术创新战略联盟在2023年中国国际服务贸易交易会数字孪生专题论坛正式对外发布《5G数字孪生赋能城市数字化应用研究报告》。 会上&#xff0c;中国电信研究院战略发展研究所副所长季鸿从数字中国…

【Spring Boot】JPA — JPA入门

JPA简介 1. JPA是什么 JPA是Sun官方提出的Java持久化规范&#xff0c;它为Java开发人员提供了一种对象/关联映射工具来管理Java应用中的关系数据&#xff0c;通过注解或者XML描述“对象-关系表”之间的映射关系&#xff0c;并将实体对象持久化到数据库中&#xff0c;极大地简…

centos7更新podman

实验环境&#xff1a;centos7.7.1908 1.安装podman并查看版本 yum install podman podman -v 当前podman版本信息是1.6.4 2.更新podman版本 通过查看资料显示centos 7 支持最高版本为 3.4.4&#xff0c;更新podman大致有以下四步&#xff1a; golang 安装(本次使用版本: 1.…

实时测试工具 Visual Studio 扩展 NCrunch 4.18 Crack

NCrunch Visual Studio 扩展 .NET 的终极实时测试工具 在编码时查看实时测试结果和内联指标。 下载v4.18 发布于 2023 年 7 月 17 日 跳过视频至&#xff1a; 代码覆盖率 指标 分布式处理 配置 发动机模式 Visual Studio 自动并发测试 NCrunch 是一个完全自动化的测试扩展&a…

[machine Learning]强化学习

强化学习和前面提到的几种预测模型都不一样,reinforcement learning更多时候使用在控制一些东西上,在算法的本质上很接近我们曾经学过的DFS求最短路径. 强化学习经常用在一些游戏ai的训练,以及一些比如火星登陆器,月球登陆器等等工程领域,强化学习的内容很简单,本质就是获取状…

网络编程套接字,Linux下实现echo服务器和客户端

目录 1、一些网络中的名词 1.1 IP地址 1.2 端口号port 1.3 "端口号" 和 "进程ID" 1.4 初始TCP协议 1.5 UDP协议 2、socket编程接口 2.1 socket 常见API 2.2 sockaddr结构 3、简单的网络程序 3.1 udp实现echo服务器和客户端 3.1.1 echo服务器实…

Arrays.copyOf 和System.arraycopy?深拷贝和浅拷贝?

Arrays.copyOf 和 System.arraycopy 1&#xff09;二者有何不同&#xff1f; System.arraycopy()方法 System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length); 需主动创建目标对象dest可定义起始元素&#xff0c;灵活拷贝元素比较重要的一点&…

华为三层交换机与路由器对接上网

华为三层交换机与路由器对接上网

昇腾Ascend TIK自定义算子开发教程(概念版)

一、参考资料 【2023 CANN训练营第一季】Ascend C算子开发入门&#xff08;中&#xff09; 二、重要说明 TIK2编程范式把算子核内的处理程序&#xff0c;分成多个流水任务&#xff0c;任务之间通过队列&#xff08;Queue&#xff09;进行通信和同步&#xff0c;并通过统一的…

目标检测笔记(十三): 使用YOLOv5-7.0版本对图像进行目标检测完整版(从自定义数据集到测试验证的完整流程))

文章目录 一、目标检测介绍二、YOLOv5介绍2.1 和以往版本的区别 三、代码获取3.1 视频代码介绍 四、环境搭建五、数据集准备5.1 数据集转换5.2 数据集验证 六、模型训练七、模型验证八、模型测试九、评价指标 一、目标检测介绍 目标检测&#xff08;Object Detection&#xff…

2023国赛高教社杯数学建模C题思路分析

1 赛题 在生鲜商超中&#xff0c;一般蔬菜类商品的保鲜期都比较短&#xff0c;且品相随销售时间的增加而变差&#xff0c; 大部分品种如当日未售出&#xff0c;隔日就无法再售。因此&#xff0c; 商超通常会根据各商品的历史销售和需 求情况每天进行补货。 由于商超销售的蔬菜…