【C++第十六章】多态

【C++第十六章】多态

定义🧐

  多态通俗来说,就是多种形态,我们去完成某个行为时,不同对象完成时会产生出不同的状态。比如游乐园中,1.2米以上买票就需要买成人票,1.2米以下就可以买儿童票。

  多态是在不同继承关系的类对象,去调用同一函数,产生不同行为。我们通过下面代码来学习:

#include <iostream>
using namespace std;class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person 
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
//多态条件
//1.虚函数重写
//2.父类的指针或者引用去调用虚函数//虚函数重写要求
//父子继承关系的两个虚函数,要求同函数名、参数、返回//virtual只能修饰成员
//三同的例外:协变->返回类型可以不同,但必须是父子类关系的指针或者引用
//派生类重写的虚函数可以不加virtual
void Func(Person& p)
{p.BuyTicket();
}
int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}

  student类继承了person类,并拥有同一函数BuyTicket,此时我们使用**父类的指针或者引用去调用该函数**就可以形成多态。

  总结一下,多态形成的条件为:1.虚函数的重写(由于一些特性,子类虚重写的虚函数可以不加virtual) 2.需要父类指针或者引用去调用该函数。而虚函数重写需要满足三同——函数名、参数、返回类型(除协变)都要相同。普通函数的继承是实现继承,虚函数的继承是接口继承

Pasted image 20240814120002

协变🔎

  协变是三同的例外,协变的返回类型可以不同,但必须是父子类关系的指针或者引用

Pasted image 20240813205432

子类virtual🔎

  子类重写的虚函数可以不加virtual,但父类必须加上virtual

Pasted image 20240813205612

  原因在于,父类指针只会调用父类析构,但是我们可能将父类指针指向子类,而delete由两部分构成——destructor(析构的统一处理,继承章节提到过)和operator delete,析构函数的名称由于多态被统一处理了,所以delete时会先调用析构再调用operator delete,在该代码中,我们想要p指向谁就调用谁的析构,此时就需要用到虚函数,而person和student满足父子关系,也有统一函数名destructor,此时只缺少virtual关键字,所以我们在析构加上virtual就可以变为多态,实现指向谁析构谁。

  不过在设计时,为了不让我们忘记给子类加上virtual而导致内存泄漏,所以统一设计,即使子类不写virtual也可以重写。

#include <iostream>
using namespace std;class Person {
public:~Person(){cout << "~Person()" << endl;}
};
class Student : public Person
{
public:~Student(){cout << "~Student()" << endl;}
};int main()
{Person* p = new Person;delete p;p = new Student;delete p;return 0;
}

Pasted image 20240813214552

Pasted image 20240813214616

重载、重写(覆盖)、重定义(隐藏)🔎

Pasted image 20240814125718

小试牛刀🔎

Pasted image 20240814124514

  B类创建了一个指针,该指针指向test函数,而test是A类的成员,所以test的参数为A* this,内部为this->fucn(),而this是B,B与A是父子关系,满足虚函数重写,所以是多态调用,但虚函数重写是父类的实现,用的是父类的接口,所以val还是父类的值,则选B

final和override🧐

  final用于修饰虚函数,被修饰的虚函数不能被重写

Pasted image 20240814130212

  override修饰派生类的虚函数,可以检查是否完成重写,没有重写则会报错。

Pasted image 20240814130450

抽象类🧐

  在虚函数后面加上"=0",则这个函数为纯虚函数,含有纯虚函数的类叫做抽象类(接口类),抽象类不能实例出对象派生类继承后必须要重写纯虚函数,才能实例化对象。

  纯虚函数规范了抽象类必须要重写,也体现出接口继承。抽象类一般用于不需要实例化的对象,比如person类,我们没有赋予属性前这个类就可以看做是抽象的,当我们继承person类后,重写它的各种属性(身高年龄职业等),让其变为一个具体的对象再进行实例化。

Pasted image 20240814132812

虚函数表🧐

  C++会把虚函数存到虚函数表(_vfptr)中,所以会多开一个指针指向该表(本篇博客代码在32位环境下运行),虚函数编译后也存在代码段中,只不过会把虚函数的地址单独拿出来放在表中。

Pasted image 20240814174804

Pasted image 20240814175304

  虚函数的重写,也叫虚函数的覆盖,虚函数表也会进行覆盖。所以当我们传子类对象时,实际上是父类对子类的切片,通过子类虚函数表找到虚函数地址。

#include <iostream>
using namespace std;
class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }virtual void func(){}private:int a = 0;
};class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }private:int b = 1;
};void Func(Person ptr)
{ptr.BuyTicket();
}int main()
{Person p;Student s;Func(p);Func(s);return 0;
}

Pasted image 20240814211250

  那么为什么可以用指针和引用,而不能用对象呢?首先引用的底层也是指针,所以它俩都是能指向子类对象中切割出来的父类。用对象也是子类切割出来的父类,成员拷贝给父类,但是不会拷贝虚函数表的指针,原因在于当出现父类=子类的情况时,不一定能调用到父类的虚函数。

Pasted image 20240814205451

  如果父类写了虚函数,子类没写虚函数,虚函数表的内容一样,但是存储在不同的位置,因为多开一个虚函数表不会耗费太多资源,从安全性考虑新开一个表更为保险。

Pasted image 20240814210906

  同一个类可以共用一张虚表。

Pasted image 20240814210537

  我们将虚表地址打印出来,发现虚表更靠近代码段,所以我们认为虚表是存在代码段中的。

Pasted image 20240816134602

  并且,所有的虚函数一定会被放进类的虚函数表中,我们以下面代码来说明:

#include <iostream>
using namespace std;class Base
{
public:virtual void func1(){cout << "Base::func1" << endl;}virtual void func2(){cout << "Base::func1" << endl;}
private:int _a;
};class Derive : public Base
{
public:virtual void func1(){cout << "Derive::func1" << endl;}virtual void func3(){cout << "Derive::func3" << endl;}virtual void func4(){cout << "Derive::func4" << endl;}void func5(){cout << "Derive::func5" << endl;}
private:int _b;
};
class X : public Derive
{
public:virtual void func3(){cout << "X::func3" << endl;}
};
//打印虚函数
typedef void (*VFUNC)();
void PrintVFT(VFUNC* a)
{for (size_t i = 0; a[i] != 0; i++){printf("[%d]:%p->", i, a[i]);VFUNC f = a[i];(*f)();}printf("\n");
}int main()
{Base b;PrintVFT((VFUNC*)(*((int*)&b)));Derive d;PrintVFT((VFUNC*)(*((int*)&d)));X x;PrintVFT((VFUNC*)(*((int*)&x)));return 0;
}

  我们用监视窗口发现只有两个虚函数。

Pasted image 20240816144909

  但实际上用函数指针数组打印出虚函数地址,发现实际有四个,不过监视窗口给我们隐藏了。

Pasted image 20240816144939

  我们打印出虚函数地址的原理为,先取到Derive对象的地址,然后强转成int*只取前4个字节,也就是取到虚函数表,解引用拿到虚函数的地址,最后强转一下传过去。

Pasted image 20240816150251

  在多继承下,会有多个虚表存在。

#include <iostream>
using namespace std;class Base1
{
public:virtual void func1(){cout << "Base1::func1" << endl;}virtual void func2(){cout << "Base1::func2" << endl;}
private:int _a1;
};class Base2
{
public:virtual void func1(){cout << "Base2::func1" << endl;}virtual void func2(){cout << "Base2::func2" << endl;}
private:int _a2;
};class Derive : public Base1, public Base2
{
public:virtual void func1(){cout << "Derive::func1" << endl;}virtual void func3(){cout << "Derive::func3" << endl;}private:int _b;
};
typedef void (*VFUNC)();
void PrintVFT(VFUNC* a)
{for (size_t i = 0; a[i] != 0; i++){printf("[%d]:%p->", i, a[i]);VFUNC f = a[i];(*f)();}printf("\n");
}int main()
{Derive d;PrintVFT((VFUNC*)(*((int*)&d)));Base2* ptr = &d; //自动切片PrintVFT((VFUNC*)*(int*)ptr);return 0;
}

Pasted image 20240817142309

  我们打印发现,重写的两个fun1地址不一样。

Pasted image 20240817160641

  我们用下面代码讲解:

#include <iostream>
using namespace std;class Base1
{
public:virtual void func1(){cout << "Base1::func1" << endl;}virtual void func2(){cout << "Base1::func2" << endl;}
private:int _a1;
};class Base2
{
public:virtual void func1(){cout << "Base2::func1" << endl;}virtual void func2(){cout << "Base2::func2" << endl;}
private:int _a2;
};class Derive : public Base1, public Base2
{
public:virtual void func1(){cout << "Derive::func1" << endl;}virtual void func3(){cout << "Derive::func3" << endl;}private:int _b;
};
typedef void (*VFUNC)();
void PrintVFT(VFUNC* a)
{for (size_t i = 0; a[i] != 0; i++){printf("[%d]:%p->", i, a[i]);VFUNC f = a[i];(*f)();}printf("\n");
}int main()
{Derive d;Base1* p1 = &d;p1->func1();Base1* p2 = &d;p2->func1();return 0;
}

  我们从p1的汇编指令来看,首先p1所call的不是真正的地址,而是call到jmp,再由jmp跳到真正的地址,开始建立fun1的栈帧,而p2发现call和jmp以及func1的地址也不一样,并且会进行多段跳。

Pasted image 20240817153059

Pasted image 20240817153107

  原因在于func1所接受的是Derive* this,而this应该能够访问整个对象,所以我们需要修正p2让它指向Derive,如图2的ecx-8就是在修正p2,让其指向Derive对象,而p1恰好与this重叠,所以没有多段跳

结尾👍

  以上便是多态的全部内容,如果有疑问或者建议都可以私信笔者交流,大家互相学习,互相进步!🌹

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

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

相关文章

n*n矩阵,输出矩阵中任意两点之间所有路径

题目1&#xff1a;给你一个正整数n&#xff0c; 构造一个n*n的四项链表矩阵。 要求&#xff1a; 1.使用四项链表 2.矩阵从左到右&#xff0c;从上到下值依次为1,2,3,4,......n*n 题目2&#xff1a;基于题目1&#xff0c; 在n*n链表矩阵中&#xff0c;输出矩阵中任意两点之间所有…

VK11之BADI增强SD_COND_SAVE_A

T-code se19 例如ZSD_COND_SAVE_A 若不知道增强位置&#xff0c;可以再CLASS CL_EXITHANDLER 的方法GET_INSTANCE设置外部断点&#xff0c;来查看Exit的位置。 此需求是当用户用VK11新增时&#xff0c;检查不能出现在process的值来进行错误反馈 UPDKZ ‘I’ 指新增 METHOD …

《黑神话:悟空》与游戏经济学的深度剖析

《黑神话&#xff1a;悟空》作为近年来备受瞩目的国产3A游戏大作&#xff0c;自其发布以来&#xff0c;不仅在游戏界内引起了轰动&#xff0c;更在多个消费领域产生了深远的影响。这款游戏不仅以其卓越的品质和深刻的文化内涵吸引了大量玩家的关注&#xff0c;还通过一系列连锁…

7-8月月报 | Apache SeaTunnel社区进展一览

各位热爱 Apache SeaTunnel 的小伙伴们&#xff0c;社区 7-8 月份月报来啦&#xff01;这两个月项目有了哪些进展&#xff1f;又有谁登上了我们社区的贡献者榜单呢&#xff1f;快来一睹为快吧。 Merge Stars 感谢以下小伙伴上两个月为 Apache SeaTunnel 项目和社区发展所做的…

在window环境下编译和安装DeepSpeed

DeepSpeed 是一个由 Microsoft 开发的深度学习优化库&#xff0c;旨在提高大规模深度学习模型的训练速度和效率。本文将介绍如何下载、安装和使用 DeepSpeed。 环境准备 需要安装 Git 以便从 GitHub 下载 DeepSpeed 源代码。可以从 Git 官方网站 下载并安装最新版本的 Git。 …

毕业设计选题系统

一、项目概述 Hi&#xff0c;大家好&#xff0c;今天分享的项目是《毕业设计选题系统》。 毕业论文选题是大学教学管理中的重要环节&#xff0c;关系到高校的教学质量。传统的手工管理方式工作效率低下、管理繁琐&#xff0c;浪费教师和学生的时间与精力的问题。本系统以提高…

c++内存管理和模板

C语言malloc calloc和realloc的弊端 C语言的这三个函数对于内置类型的处理是还可以的&#xff0c;但是对自定义类型就无法处理了&#xff0c;因为c自定义类型在初始化的时候是自动调用构造函数的&#xff0c;而C语言的这三个函数是无法初始化内置类型的&#xff0c;于是我们下…

JetBrains`s IntelliJ IDEA springboot项目 gradle-bin安装 国内加速

gradle wapper加速 可以看到&#xff0c;一般我们直接init的springboot项目会默认使用wapper来安装不同版本的gradle&#xff0c;但services.gradle.org网速过慢&#xff0c;我们选择切换为国内源 发现一篇同样的文档,并在此补充多个源 源名urlgradle(原本)https\://service…

网站如何针对不同的DDOS进行防御?

建设网站租用服务器是多数企业及个人的选择&#xff0c;一个安全稳定的服务器对网站的重要性无需再赘述。要保证服务器租用的安全和稳定&#xff0c;除了需要服务器自身有强大的硬、软件基础之外&#xff0c;还需要防范外部的一些因素&#xff0c;常见的就是各种网络攻击&#…

接口测试 —— 如何设计高效的测试用例!

摘要&#xff1a; 随着互联网应用的日益复杂化&#xff0c;接口测试已成为保证软件质量不可或缺的一部分。本文将探讨如何有效地设计接口测试用例&#xff0c;并提供实用的建议和示例。 一、引言 接口测试&#xff08;API测试&#xff09;是确保系统各部分之间交互正确性的关键…

信息安全--(四)网络安全体系与安全模型(二)

其他安全模型 ■纵深防御模型&#xff1a;①安全保护②安全监测③实时响应④恢复 ■分层防护模型&#xff1a;参考OSI模型&#xff0c;对保护对象进行层次化保护。 ■等级保护模型&#xff1a;将信息系统划分成不同安全保护等级&#xff0c;采取相 应的保护措施。 ■网络生…

【MySQL-24】万字全面解析<索引>——【介绍&语法&性能分析&使用规则】

前言 大家好吖&#xff0c;欢迎来到 YY 滴MySQL系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; YY的《C》专栏YY的《C11》专栏YY的《Lin…

视频智能分析打手机检测算法安防监控打手机检测算法应用场景、算法源码、算法模型介绍

随着智能手机的普及&#xff0c;手机已成为人们生活中不可或缺的一部分。然而&#xff0c;在某些场合&#xff0c;如驾驶、会议、学校课堂等&#xff0c;不当使用手机可能会导致安全隐患或干扰他人。因此&#xff0c;开发出一种能够准确识别并阻止不当使用手机的行为检测算法显…

网吧业务安全对抗(有源码)

网吧业务竞争激烈&#xff0c;网吧都会有以下系统软件。 无盘: 无盘是指没有硬盘。好处是统一维护管理和节约成本。本人研究无盘好几年&#xff0c;后面会专门发帖介绍。 计费: 是指收费系统。 营销软件: 包括销售饮品、‌零食和向客户发送电子邮件营销和短信营销等。产品如…

springboot,maven多模块开发,子模块获取不到父模块添加的依赖,有多个root模块问题解决

错误示范 我以为放进去然后重载一下就是子模块了 导致后续在外层加的依赖&#xff0c;其article都接收不到 解决方案 需要在父模块的modules注册子模块 修改前后对比 此时子模块也能获取父模块的依赖

在Ubuntu/Linux下重温FC游戏——超级玛丽奥

文章目录 在Ubuntu/Linux下重温FC游戏——超级玛丽奥1 概述2 安装 FCEUX 模拟器3 下载 FC ROMS4 重温时光 在Ubuntu/Linux下重温FC游戏——超级玛丽奥 1 概述 FC 游戏机&#xff0c;是任天堂生产、发行和销售的 8 位第三世代家用游戏机&#xff0c;日本版官方名称为家庭电脑&…

Java源码学习之高并发编程基础——AQS源码剖析之线程间通信之条件等待队列

1.前言&目录 前言&#xff1a; 在Java中&#xff0c;使用synchronized关键字构建的锁&#xff0c;线程间通信可以使用某对象实例的wait/notify机制完成。AQS同样也提供了一套线程间通信的解决方案——条件等待队列。 在AQS源码分析的两篇文章AQS源码分析&#xff08;上&am…

逻辑器件输出高阻态时,输出端口的电平是什么状态呢?

高阻态是逻辑器件输出端口的一种状态&#xff0c;当端口处于高阻态时&#xff0c;输入端口的电平变化不会引起输出端口变化&#xff0c;不会对与之相连的后级输入端口或总线产生影响&#xff0c;对于总线架构的电路极为重要。   输出端口处于高阻态时&#xff0c;输出端口处于…

优秀软件工程师的工作思维

引言 在快速迭代的软件开发领域&#xff0c;软件工程师不仅需要精通编程技术&#xff0c;还需要具备产品思维、技术思维和工程思维&#xff0c;这三种思维相辅相成&#xff0c;共同推动产品的成功。本文将借鉴陈春花等管理学者的思考方式&#xff0c;深入剖析软件工程师如何在…

数据恢复工具,电脑+手机双端,十分好用!

哈喽&#xff0c;各位小伙伴们好&#xff0c;我是给大家带来各类黑科技与前沿资讯的小武。 今天给大家安利两款数据恢复工具&#xff0c;分别为电脑手机双端&#xff0c;无论是因为格式化误操作、设备损坏还是其他意外情况&#xff0c;都能轻松找回重要的文件、照片、视频等数…