★ C++进阶篇 ★ 多态
Ciallo~(∠・ω< )⌒☆ ~ 今天,我将继续和大家一起学习C++进阶篇第二章----多态 ~
❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️
澄岚主页:椎名澄嵐-CSDN博客
C++基础篇专栏:★ C++基础篇 ★_椎名澄嵐的博客-CSDN博客
C++进阶篇专栏:★ C++进阶篇 ★_椎名澄嵐的博客-CSDN博客
❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️
目录
一 多态的概念
二 多态的定义及实现
2.1 多态的构成条件
2.2 虚函数
2.3 虚函数的重写
2.4 多态经典选择题
2.5 虚函数重写的其他问题
2.6 override & final
2.1.7 重载/重写/隐藏的对比
三 纯虚函数和抽象类
四 多态的原理
4.1 虚函数表指针
4.2 多态的原理
4.2.1 多态是如何实现的
4.2.2 动态绑定与静态绑定
4.2.3 虚函数表
一 多态的概念
多态(polymorphism)的概念:通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运行时多态(动态多态),这里我们重点讲运行时多态,编译时多态(静态多态)和运行时多态(动态多态)。编译时多态(静态多态)主要就是我们前面讲的函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态,运行时归为动态。
运行时多态,具体就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种形态。⽐如买票这个行为,当普通⼈买票时,是全价买票;学⽣买票时,是优惠买票(5折或75折);军⼈买票时是优先买票。
二 多态的定义及实现
2.1 多态的构成条件
多态是一个继承关系的下的类对象,去调用同一函数,产生了不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象优惠买票。
实现多态的两个条件:
- 必须指针或者引用调用虚函数:只有基类的指针或引用才能指向派生类对象
- 被调用的函数必须是虚函数:派生类必须对基类的虚函数重写/覆盖,重写或者覆盖了,派生类才能有不同的函数,多态的不同形态效果才能达到。
2.2 虚函数
类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修 饰。
class Person
{
public:virtual void BuyTicket(){cout << "买票-全价" << endl;}
};
2.3 虚函数的重写
虚函数的重写:派生类中有⼀个跟基类完全相同的虚函数(子类和父类的虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。(三同)
以下是一个关于买票的多态实验~
class Person // 父类对象
{
public:virtual void BuyTicket() // 必须是虚函数~{cout << "买票-全价" << endl;}
};class Student : public Person // 子类对象
{
public:virtual void BuyTicket() // 最好虚函数~对父类的虚函数重写{cout << "买票-半价" << endl;}
};void Func(Person* ptr) // 传指针或引用~ 既可以传父类对象指针又可以传子类指针~
{ptr->BuyTicket();
}int main()
{Person ps;Student st;Func(&ps); // 买票-全价Func(&st); // 买票-半价return 0;
}
传不同类型的指针调用不同的函数。
派生类的虚函数在不加virtual关键字,也可以构成重写,但该写法不是很规范。(会出选择题)
我们再来写一个动物叫声的多态情况~
class Animal
{
public:virtual void talk() const{}
};
class Dog : public Animal
{
public:virtual void talk() const{std::cout << "汪" << std::endl;}
};
class Cat : public Animal
{
public:virtual void talk() const{std::cout << "喵₍˄·͈༝·͈˄*₎◞ ̑̑" << std::endl;}
};
void letsHear(const Animal& animal)
{animal.talk();
}
2.4 多态经典选择题
class A
{
public:virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }virtual void test() { func(); }
};class B : public A
{
public:void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};int main(int argc, char* argv[])
{B* p = new B;p->test();return 0;
}
重写的本质是重写虚函数的实现,在继承中绝不重新定义继承下来的缺省值。
2.5 虚函数重写的其他问题
- 协变
派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。协变的实际意义并不大,了解⼀下即可。
class A {};
class B : public A {};
class Person {
public:virtual A* BuyTicket(){cout << "买票-全价" << endl;return nullptr;}
};
class Student : public Person {
public:virtual B* BuyTicket(){cout << "买票-打折" << endl;return nullptr;}
};
void Func(Person* ptr)
{ptr->BuyTicket();
}
int main()
{Person ps;Student st;Func(&ps); //买票 - 全价Func(&st); //买票 - 打折return 0;
}
- 析构函数的重写
基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了vialtual修饰,派⽣类的析构函数就构成重写。
以上代码我们可以看到,如果~A(),不加virtual,那么delete p2时只调用的A的析构函数,没有调⽤B的析构函数,就会导致内存泄漏问题,因为~B()中在释放资源。
2.6 override & final
从上面可以看出,C++对虚函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错参数写错等导致无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此C++11提供了override,可以帮助用户检测是否重写。如果我们不想让派⽣类重写这个虚函数,那么可以⽤final去修饰。
加了override,程序就会在函数名写错参数写错时,用红线标出。
2.1.7 重载/重写/隐藏的对比
- 重载
1. 两个函数在同一作用域。
2. 函数名相同,参数不同,参数类型或参数个数不同,返回值可同可不同。
- 重写
1. 两个函数分别在继承体系的父类和子类不同作用域。
2. 除协变外,函数名参数返回值都相同。
3. 两个函数都是虚函数。
- 隐藏
1. 两个函数分别在继承体系的父类和子类不同作用域。
2. 函数名相同。
3. 两个函数只要不构成重写就是隐藏。
4.父子类的成员变量相同也叫隐藏。
三 纯虚函数和抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派生类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。
class Car
{
public:virtual void Drive() = 0;
};
class Benz :public Car
{
public:virtual void Drive(){cout << "Benz-舒适" << endl;}
};
class BMW :public Car
{
public:virtual void Drive(){cout << "BMW-操控" << endl;}
};
int main()
{Car car;// 编译报错:error C2259: “Car”: 无法实例化抽象类Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();return 0;
}
四 多态的原理
4.1 虚函数表指针
我们先来看下面这个代码~
class Base
{
public:virtual void Func1(){ cout << "Func1()" << endl; }virtual void Func2(){ cout << "Func2()" << endl; }protected:int _b = 1;//4char _ch = 'x';//1->4
};
int main()
{Base b;cout << sizeof(b) << endl; //12return 0;
}
在32位下程序的运行结果为12,除了_b和_ch成员,还多⼀个__vfptr放在对象的前⾯,对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。⼀个含有虚函数的类中都⾄少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。
4.2 多态的原理
4.2.1 多态是如何实现的
从底层的⻆度Func函数中ptr->BuyTicket(),是如何作为ptr指向Person对象调用Person::BuyTicket,ptr指向Student对象调⽤Student::BuyTicket的呢?通过下图我们可以看到,满⾜多态条件后,底层不再是编译时通过调⽤对象确定函数的地址,⽽是运⾏时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引⽤指向基类就调⽤基类的虚函数,指向派⽣类就调⽤派⽣类对应的虚函数。第⼀张图,ptr指向的Person对象,调⽤的是Person的虚函数;第⼆张图,ptr指向的Student对象,调⽤的是Student的虚函数。
class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
protected:string _name;
};class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-打折" << endl; }
protected:int _id;
};class Soldier : public Person {
public:virtual void BuyTicket() { cout << "买票-优先" << endl; }
protected:string _codename;
};void Func(Person* ptr)
{// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。ptr->BuyTicket();
}int main()
{// 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后// 多态也会发⽣在多个派⽣类之间。Person ps;Student st;Soldier sr;Func(&ps);Func(&st);Func(&sr);return 0;
}
指向哪个对象,运行时,到指定对象的虚函数表中找到对应的虚函数地址,进行调用。
4.2.2 动态绑定与静态绑定
- 对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。
- 满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就做动态绑定。
// ptr是指针+BuyTicket是虚函数满⾜多态条件。
// 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址
ptr->BuyTicket();
00EF2001 mov eax,dword ptr [ptr]
00EF2004 mov edx,dword ptr [eax]
00EF2006 mov esi,esp
00EF2008 mov ecx,dword ptr [ptr]
00EF200B mov eax,dword ptr [edx]
00EF200D call eax
// BuyTicket不是虚函数,不满⾜多态条件。
// 这⾥就是静态绑定,编译器直接确定调⽤函数地址
ptr->BuyTicket();
00EA2C91 mov ecx,dword ptr [ptr]
00EA2C94 call Student::Student (0EA153Ch)
4.2.3 虚函数表
- 基类对象的虚函数表中存放基类所有虚函数的地址。
- 派⽣类由两部分构成,继承下来的基类和自己的成员,⼀般情况下,继承下来的基类中有虚函数表指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个,就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴的。
- 派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。
- 派⽣类的虚函数表中包含,基类的虚函数地址,派生类重写的虚函数地址,派生类自己的虚函数地址三个部分。
- 虚函数表本质是一个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标记。(这个C++并没有进⾏规定,各个编译器⾃⾏定义的,vs系列编译器会再后⾯放个0x00000000 标记,g++系列编译不会放)
- 虚函数存在哪的?虚函数和普通函数一样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址又存到了虚表中。
- 虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下⾯的代码可以对⽐验证⼀下。vs下是存在代码段(常量区)
int main()
{int i = 0;static int j = 1;int* p1 = new int;const char* p2 = "xxxxxxxx";printf("栈:%p\n", &i);printf("静态区:%p\n", &j);printf("堆:%p\n", p1);printf("常量区:%p\n", p2);Base b;Derive d;Base* p3 = &b;Derive* p4 = &d;printf("Person虚表地址:%p\n", *(int*)p3);printf("Student虚表地址:%p\n", *(int*)p4);printf("虚函数地址:%p\n", &Base::func1);printf("普通函数地址:%p\n", &Base::func5);return 0;
}
栈:010FF954
静态区:0071D000
堆:0126D740
常量区:0071ABA4
Person虚表地址:0071AB44
Student虚表地址:0071AB84
虚函数地址:00711488
普通函数地址:007114BF
~完~
相关文章:

★ C++进阶篇 ★ 多态
Ciallo~(∠・ω< )⌒☆ ~ 今天,我将继续和大家一起学习C进阶篇第二章----多态 ~ ❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️ 澄岚主页:椎名澄嵐-CSDN博客 C基础篇专栏:★ C基础篇 ★_椎名澄嵐的博客-CSDN博客 …...

pg入门3—详解tablespaces2
pg默认的tablespace的location为空,那么如果表设置了默认的tablespace,数据实际上是存哪个目录的呢? 在 PostgreSQL 中,如果你创建了一个表并且没有显式指定表空间(tablespace),或者表空间的 location 为…...

python 爬虫 selenium 笔记
todo 阅读并熟悉 Xpath, 这个与 Selenium 密切相关、 selenium selenium 加入无图模式,速度快很多。 from selenium import webdriver from selenium.webdriver.chrome.options import Options# selenium 无图模式,速度快很多。 option Options() o…...

git分支管理的一些常用规范
一、分支命名规范 1.通常项目经理或者需求方会给需求开发做计划,约定一些编码,例如FN-01。此时这个需求指派给你,这个时候你可以在现有代码仓库的maser分支或者其他约定的开发分支checkout到本地,命名这个需求的开发分支为feat/F…...

GPT-4论文阅读
GPT-4 Technical Report论文阅读 文章目录 GPT-4 Technical Report论文阅读 Abstract训练的稳定性Training processPredictable scaling训练的稳定性多么难能可贵 Capabilities考试成绩传统的benchmark语言方面的能力Visual inputsSteerability LimitationsRisks & mitigat…...

this 指向
this 指向谁? 多数情况下,this 指向调用它所在方法的那个对象。 说得更通俗点,谁调的函数,this 就归谁。当调用方法没有明确对象时,this 就指向全局对象。在浏览器中,指向 window;在 Node 中,指向 Global。(严格模式下,指向 undefined) this 的指向是在调用时决定的…...

【贪心算法】贪心算法一
贪心算法一 1.柠檬水找零2.将数组和减半的最少操作次数3.最大数4.摆动序列 点赞👍👍收藏🌟🌟关注💖💖 你的支持是对我最大的鼓励,我们一起努力吧!😃😃 1.柠檬水找零 题目…...

windnd.hook_dropfiles中的create_buffer值太小无法拖放长文件名
今天在使用我之前的Python识别拖放的PDF文件再转成文本文件-CSDN博客发现,文件藏在路径太深入的地方,不能打开,因为拖放文件只能读取260个字节的文件名(b’路径),再查看windnd.hook_dropfiles函数ÿ…...

Gitlab runner的使用示例(二):Maven + Docker 自动化构建与部署
Gitlab runner的使用示例(二):Maven Docker 自动化构建与部署 在本篇文章中,我们将详细解析一个典型的 GitLab CI/CD 配置文件(gitlab-ci.yml),该文件主要用于通过 Maven 构建 Java 应用&…...

QNX Hypervisor(十)Linux Guest IPC 二
上文还遗留了一个问题,就是在测试ipc的时候挂死了。相关原理我写在了另外一篇文章。 内存管理 所以导致挂死的问题就是因为没有进行地址映射,mmu无法转换。从kernel代码看,只有ram区域才会进行映射。我们的qvmconf文件也确实没有配置0xb8000000,只配置了pass。 pass loc …...

怎样把PPT上顽固的图标删了
例如: 解决: 首先打开下载好的PPT模板,然后在视图选项卡里面找到幻灯片母版。 进入幻灯片母版后,找到第一页母版页就会看到LOGO了,这时使用鼠标就可以选中删除啦。...

【论文阅读】Slim Fly: A Cost Effective Low-Diameter Network Topology 一种经济高效的小直径网络拓扑
文章目录 Slim Fly: A Cost Effective Low-Diameter Network Topology文章总结1. 摘要2. indroduction3. 主要工作 主要思想references Slim Fly: A Cost Effective Low-Diameter Network Topology Slim Fly:一种经济高效的小直径网络拓扑 SC’14 Maciej Besta 苏…...

Prometheus使用Pushgateway推送数据
Pushgateway简介 Prometheus 的 Pushgateway 是一个简单的 HTTP 服务器,它允许数据被推送到该服务器,而不是通过拉取的方式获取。它的存在是为了让临时和批处理作业能够将其指标暴露给 Prometheus。由于这类作业可能存在的时长不足以被主动抓取…...

【Oracle】调优与oracle最大连接数配置
博主介绍: 大家好,我是想成为Super的Yuperman,互联网宇宙厂经验,17年医疗健康行业的码拉松奔跑者,曾担任技术专家、架构师、研发总监负责和主导多个应用架构。 技术范围: 目前专注java体系,DDD&…...

Unity教程(十六)敌人攻击状态的实现
Unity开发2D类银河恶魔城游戏学习笔记 Unity教程(零)Unity和VS的使用相关内容 Unity教程(一)开始学习状态机 Unity教程(二)角色移动的实现 Unity教程(三)角色跳跃的实现 Unity教程&…...

图像超分辨率(ISR)
图像超分辨率(Image Super-Resolution, ISR)是一种图像处理技术,旨在通过软件算法从低分辨率的图像中重建出高分辨率的图像。这种技术对于改善图像质量、增加细节清晰度等方面非常重要,特别是在图像放大、卫星成像、医学成像和视频…...

园区网基础组网保姆级(mstp,vrrp,irf,eth-trunk,route-policy,ospf,bgp,rbm,nat,mlag等等)
本文实验使用模拟器:H3C HCL 5.10.2版本 一、园区核心/接入架构1.1.三层架构1.2.二层架构二、园区核心 To 接入实践2.1.MSTP+VRRP派系2.1.1.MSTP+VRRP配置2.1.2.MSTP+VRRP验证2.2.IRF+Eth-Trunk派系2.2.1.IRF+Eth-Trunk配置2.3.两种派系的对比2.4.VXLAN结构三、园区核心/出口架…...

大数据技术原理与应用
第一章、大数据概述 1、大数据时代的特征,并结合生活实例谈谈带来的影响。 (一)特征 1、Volume 规模性:数据量大。 2、Velocity高速性:处理速度快。数据的生成和响应快 摩尔定律:每两年,数…...

《黑神话悟空》开发框架与战斗系统解析
本文主要围绕《黑神话悟空》的开发框架与战斗系统解析展开 主要内容 《黑神话悟空》采用的技术栈 《黑神话悟空》战斗系统的实现方式 四种攻击模式 连招系统的创建 如何实现高扩展性的战斗系统 包括角色属性系统、技能配置文件和逻辑节点的抽象等关键技术点 版权声明 本…...

网络资源模板--Android Studio 通讯录App
目录 一、项目演示 二、项目测试环境 三、项目详情 四、完整的项目源码 一、项目演示 网络资源模板--基于Android studio 通讯录 二、项目测试环境 三、项目详情 首页 MainActivity 类是一个 Android 地址簿应用的核心部分,负责管理联系人列表的显示、搜索和添…...

Spring 出现 No qualifying bean of type ‘com.xxx‘ available 解决方法
目录 1. 问题所示2. 原理分析3. 解决方法4. 彩蛋4.1 bug彩蛋4.2 完整Demo4.3 补充Springboot1. 问题所示 出现如下问题: 19:58:23.476 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean o…...

C# 批量更改文件后缀名称
解决问题思路 解决固定文件夹下更改文件后缀名,采用轮询的方式, 流程如下: 获取当前文件名(带后缀的文件名)截取文件名称,去掉后缀另存为带更改后的后缀文件 注意:采用第三方插件࿰…...

KIC算法介绍及pyrosetta示例代码
Kinetic Loop Closure (KIC) 是 Rosetta 中一种重要的环区(loop region)建模算法,主要用于解决蛋白质中的柔性区域(特别是环区)的重构问题。环区是蛋白质中非常灵活的部分,通常结构不确定。KIC 算法采用基于运动学的解决方案,通过设置特定的几何约束,能够在给定的两端锚…...

【论文串烧】多媒体推荐中的模态平衡学习 | 音视频语音识别中丢失导致的模态偏差对丢失视频帧鲁棒性的影响
文章目录 一、多媒体推荐中的模态平衡学习1.1 研究背景1.2 解决问题1.3 实施方案1.4 文章摘要1.5 文章重点1.6 文章图示图 1:不同模型变体在 AmazonClothing 数据集上的初步研究图 2:CKD模型架构的说明图 3:在 Amazon-Clothing 数据集上训练过…...

【C语言二级考试】循环结构设计
C语言二级考试——循环结构程序设计 五.循环结构程序设计 1.for循环结构 2.while和do-while循环结构 3.continue语句和break语句 4.循环的嵌套 知识点参考【C语言】循环-CSDN博客 文章目录 1.for循环2.while和do-while循环结构3.continue语句和break语句4.循环的嵌套 1.for循环…...

诗文发布模板(python代码打造键盘录入诗文自动排版,MarkDown源码文本)
python最好用的f-string,少量代码打造键盘录入诗文自动排版。 (笔记模板由python脚本于2024年09月19日 19:11:50创建,本篇笔记适合喜欢写诗的pythoner的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网:https://www.python.org/ Free&am…...

GO主流开源框架
GO主流开源框架 Go 语言有着丰富的开源框架生态,涵盖了多种应用场景,如 Web 开发、数据库操作、微服务、日志处理等。以下是一些常见的 Go 框架及其典型作用场景: 1. Web 框架 Gin: 作用:一个高性能的轻量级 Web 框架ÿ…...

LeetCode:2398. 预算内的最多机器人数目 双指针+单调队列,时间复杂度O(n)
2398. 预算内的最多机器人数目 today 2398. 预算内的最多机器人数目 题目描述 你有 n 个机器人,给你两个下标从0开始的整数数组 chargeTimes 和 runningCosts ,两者长度都为 n 。第 i 个机器人充电时间为 chargeTimes[i] 单位时间,花费 ru…...

oracle 插入date日期类型的数据、插入从表中查出的数据,使用表中的默认数据
date sysdate to_date 插入从表中查出的数据 方式一 方式二 或者指定列名称 下边这个案例的前提是指定列插入,如果不指定,则也是默认的...

物流系统打单软件 佳易王物流运单怎么打印教程
一、前言 物流系统打单软件 佳易王物流运单怎么打印教程 1、佳易王物流管理系统可同时打印物流单和标签 2、如果一台电脑上有多台打印机,软件可以设置物流或标签对应的打印机,系统自动识别打印机。 二、软件程序图文说明 1、上图为 物流单在空白单上打…...