C++——多态

目录

前言:需要声明的,本节课件中的代码及解释都是在vs2013下的x86程序中,涉及的指针都是4bytes。 如果要其他平台下,部分代码需要改动。比如:如果是x64程序,则需要考虑指针是8bytes问题 等等

1.多态的概念

2.多态的定义及实现

2.1多态的构成条件

2.2虚函数

2.3虚函数的重写

2.4 C++11 override 和 final

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

3. 抽象类

3.1 概念

3.2 接口继承和实现继承

4.多态的原理

4.1虚函数表

4.2多态的原理

4.3 动态绑定与静态绑定

5.单继承和多继承关系的虚函数表

5.1 单继承中的虚函数表

5.2 多继承中的虚函数表

5.3. 菱形继承、菱形虚拟继承


前言:需要声明的,本节课件中的代码及解释都是在vs2013下的x86程序中,涉及的指针都是4bytes。 如果要其他平台下,部分代码需要改动。比如:如果是x64程序,则需要考虑指针是8bytes问题 等等

1.多态的概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。

再举个栗子: 最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块...,而有人扫的红包都是1毛,5 毛....。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 = random()%99;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你去使用支付宝,那么就你扫码金额 = random()%1;总结一下:同样是扫码动作,不同的用户扫得到的不一样的红包,这也是一种多态行为。ps:支付宝红包问题纯属瞎编,大家仅供娱乐。

2.多态的定义及实现

2.1多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象买票半价。

那么在继承中要构成多态还有两个条件: 1. 必须通过基类的指针或者引用调用虚函数 2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

如上图所示,函数的参数得是基类,使用引用或指针都可以,这里我们使用引用,调用的时候,基类指向基类的虚函数,子类指向子类的虚函数。

2.2虚函数

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

2.3虚函数的重写

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

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

运行结果如下图所示:

虚函数重写的两个例外:

1. 协变(基类与派生类虚函数返回值类型不同)

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

2. 析构函数的重写(基类与派生类析构函数的名字不同)

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

2.4 C++11 override 和 final

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

1. final:修饰虚函数,表示该虚函数不能再被重写

如图所示,在Drive()后面添上final修饰就不能再被重写。

2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

(不加override)

(加override)

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

3. 抽象类

3.1 概念

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

运行结果:

3.2 接口继承和实现继承

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

4.多态的原理

4.1虚函数表

通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?我们接着往下分析

通过观察和测试,我们发现了以下几点问题:

1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分,另一部分是自己的成员。

2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表 中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函 数,所以不会放进虚表。

4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

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

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

4.2多态的原理

上面分析了这个半天了那么多态的原理到底是什么?还记得这里Func函数传Person调用的 Person::BuyTicket,传Student调用的是Student::BuyTicket

1. 观察下图的红色箭头我们看到,p是指向mike对象的,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。

2. 观察下图的蓝色箭头我们看到,p是指向johnson对象的,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。

3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

4. 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。

5. 再通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。

(对比多态调用虚函数与调用普通函数)

// 以下汇编代码中跟你这个问题不相关的都被去掉了
void Func(Person* p)
{...p->BuyTicket();// p中存的是mike对象的指针,将p移动到eax中//001940DE  mov         eax, dword ptr[p]// [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx//001940E1  mov         edx, dword ptr[eax]// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax//00B823EE  mov         eax, dword ptr[edx]// call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来//以后到对象的中取找的。//001940EA  call        eax//00头1940EC  cmp       esi, esp
}
int main()
{//...// 首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,所以这里是普通函数的调//用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址//mike.BuyTicket();//00195182  lea         ecx, [mike]//00195185  call        Person::BuyTicket(01914F6h)//...
}

4.3 动态绑定与静态绑定

1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载

2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

3. 本小节之前(4.2小节)买票的汇编代码很好的解释了什么是静态(编译器)绑定和动态(运行时)绑定。

5.单继承和多继承关系的虚函数表

需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类的虚表模型前面我们已经看过了,没什么需要特别研究的

5.1 单继承中的虚函数表

观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数。

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << 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; }
private:int b;
};typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{// 依次取虚表中的虚函数指针打印并调用。//调用就可以看出存的是哪个函数cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}
int main()
{Base b;Derive d;// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数//指针的指针数组,这个数组最后面放了一个nullptr// 1.先取b的地址,强转成一个int*的指针// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。// 4.虚表指针传递给PrintVTable进行打印虚表// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最//后面没有放nullptr,导致越界,这是编译器的问题。// 我们只需要点目录栏的 - 生成 - 清理解决方案,再//编译就好了。VFPTR * vTableb = (VFPTR*)(*(int*)&b);PrintVTable(vTableb);VFPTR* vTabled = (VFPTR*)(*(int*)&d);PrintVTable(vTabled);return 0;
}

运行结果:

5.2 多继承中的虚函数表

class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1;
};
class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2;
};
class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}
int main()
{Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb1);VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));PrintVTable(vTableb2);return 0;
}

运行结果如下:

观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

5.3. 菱形继承、菱形虚拟继承

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。如果好奇心比较强的宝宝,可以去看下面的两篇文章。

1. C++ 虚函数表解析

2. C++ 对象的内存布局

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

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

相关文章

【黑马】MyBatis

目录 MyBatis简介JDBC缺点&#xff1a;MyBatis针对于JDBC进行简化&#xff0c;简化思路&#xff1a; MyBatis快速入门具体构建步骤解决SQL映射文件的警告提示 Mapper代理开发案例&#xff1a;使用Mapper代理方式完成案例具体步骤详解&#xff1a;Mapper代理方式 Mapper核心配置…

C++ SQL ORM

测试代码 // // Created by www on 2024/8/7. // #include "sqlitepp/database.h" #include "sqlitepp/condition.h"#include <iostream> using namespace sqlitepp; using namespace sqlitepp::literals;enum class test_enum {hello };void test…

掌握这项技能,用Python爬虫定制你的私人电影推荐库

思路&#xff1a; 1、先选取你要获取的视频 2、用vip解析工具解析&#xff0c;获取地址 3、复制粘贴&#xff0c;获取视频 以下是效果图 需要的python模块&#xff1a; ##第一个模块不要安装&#xff0c;第二个模块需要安装 1、from multiprocessing import Pool 2、imp…

守护网站心脏:HAProxy负载均衡技术详解(全网最详细)

守护网站心脏&#xff1a;HAProxy负载均衡技术详解 文章目录 **守护网站心脏&#xff1a;HAProxy负载均衡技术详解**haproxy介绍特点功能配置结论 haproxy负载均衡测试haproxy主机操作日志重定向操作 webserver1主机操作webserver2主机操作 haporxy相关配置测试backup参数测速h…

【Redis】Redis 数据类型

文章目录 前言1 通用知识1.1 基本全局命令1.2 数据结构和内部编码 2 String2.1 类型介绍2.2 相关命令2.2.1 SET/GET 系列命令2.2.2 INCR/DECR 计数系列命令2.2.3 其他命令2.2.4 命令小结 2.3 内部编码2.4 应用场景2.4.1 缓存功能2.4.2 计数功能2.4.3 共享会话2.4.4 验证码功能 …

如何在linux上部署zabbix监控工具

<1>搭建服务机 1&#xff09;首先我们先执行 sed -i s/SELINUXenforcing/SELINUXdisabled/ /etc/selinux/config ​ #然后我们再把防火墙开机自启关掉 马上生效 systemctl disable --now firewalld 2&#xff09;我们获得rpm包 rpm -Uvh https://mirrors.aliyun.com/…

Java设计模式-享元设计模式

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 1. 享元设计模式介绍1.1 定义1.2 原理1.3 使用场景1.4 注意事项 2. 享元设计模式角色2.1 抽象享元角色&#xff08;‌AbstractFlyWeight&#xff09;‌2.2 具体享元…

【MySQL】视图——视图、视图的概念、为什么要使用视图、视图的基本使用、视图规则和限制

文章目录 MySQL1. 视图1.1 视图的概念1.2 为什么要使用视图1.3 视图的基本使用1.4 视图规则和限制 MySQL 1. 视图 1.1 视图的概念 视图是一个虚拟表&#xff0c;其内容由查询定义。同真实的表一样&#xff0c;视图包含一系列带有名称的列和行数据。视图的数据变化会影响到基表…

【动态规划】1、不同路径II+2、三角形最小路径和

1、不同路径II&#xff08;难度中等&#xff09; 该题对应力扣网址 AC代码 只会写简单的if-else class Solution { public:int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {//1、定义子问题//2、子问题递推关系//3、确定dp数组的计算顺序…

uniapp获取swiper中子组件的内容高度

swiper有默认高度,如果不单独设置一个具体高度&#xff0c;swiper后面的内容将不会展示 这里展示的例子是: swiper中放有一个子组件,想要完整展示子组件的内容&#xff0c;swiper就需要获取到子组件的内容高度并设置 <!-- 注意: 这里的单位是 px,不是rpx --><swiper…

Element UI导航菜单刷新就复原问题解决方法~

1、首先要知道为什么一刷新就复原了&#xff0c;是因为default-active属性设置的是默认值&#xff0c;是一个死值&#xff0c;一旦刷新就会复原&#xff0c;造成高亮不能保持&#xff0c;那么怎么解决呢&#xff1f; 2、很简单&#xff0c;无需像一些博主一样绑定path。思路&a…

linux中信号的相关概念

信号 内核层和用户层通信的一种方式 信号类型 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18…

【限流与Sentinel超详细分析】

Sentinel 随着微服务的流行&#xff0c;服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式服务架构的流量控制组件&#xff0c;主要以流量为切入点&#xff0c;从流量控制、熔断降级、系统自适应保护等多个维度来保障微服务的稳定性。 1 Sentinel 基本概念 资源…

MySQL —— 聚合查询,分组查询 与 联合查询

聚合函数 常见的统计总数、计算平局值等操作&#xff0c;可以使用聚合函数来实现&#xff0c;常见的聚合函数有&#xff1a; 函数说明count()统计数据总数sum()求和avg()求平均值max()求最大值min()求最小值 注意凡是涉及运算的&#xff0c;数据库会自动掉 NULL 值 注意NULL …

本地安Stable Diffusion全记录

这里写自定义目录标题 资料 资料 AI绘图软件Stable Diffusion 之本地安装 手把手教你在本机安装Stable Diffusion秋叶整合包 让 stable diffusion 局域网访问&#xff1a;详细解析配置步骤【Stable Diffusion 实战教程】 局域网多设备访问stable diffusion Stable Diffusion 老…

Lua语言基础学习:安装Lua和Lua库管理工具

Lua语言简介 Lua是一种轻量、高效、可嵌入的脚本语言&#xff0c;由巴西里约热内卢天主教大学的研究小组于1993年开发&#xff0c;Lua的解释器非常小巧&#xff0c;编译后的体积很小&#xff08;如完整解释器不过200KB&#xff09;&#xff0c;这使得它非常适合嵌入到其他应用程…

谷粒商城实战笔记-126-全文检索-ElasticSearch-整合-测试保存

文章目录 一&#xff0c;谷粒商城实战笔记-126-全文检索-ElasticSearch-整合-测试保存1&#xff0c;在Elasticsearch的配置类中增加通用设置2&#xff0c;索引数据3&#xff0c;验证 一&#xff0c;谷粒商城实战笔记-126-全文检索-ElasticSearch-整合-测试保存 1&#xff0c;在…

汇编语言基础及常见汇编指令

一、实验原理 x64dbg 是一款开源且免费的 Ring 3 级动态调试器&#xff0c;采用 QT 编写&#xff0c;支持 32 / 64 位程序。其反汇编引擎 BeaEngine 和 Capstone 功能极其强大&#xff0c;也有丰富的插件和脚本功能&#xff0c;且并保持更新&#xff0c;目前已经基本替代了 Ol…

数字医学影像系统PACS源码,三甲以下医院都能满足,C#语言开发,C/S架构系统成熟稳定,支持二次开发项目使用。

数字医学影像系统&#xff08;RIS/PACS&#xff09;源码&#xff0c;三甲以下的医院都能满足。开发技术&#xff1a;C/S架构&#xff0c;C#开发语言&#xff0c;数据库服务器采用Oracle数据库。 PACS系统模块组成 &#xff1a; 工作站&#xff1a; 分诊工作站、超声工作站、放…

学习鸿蒙-应用市场申请签名

1.需要的文件概念 .cer / .p7b / .p12 / .csr HarmonyOS应用/服务通过数字证书&#xff08;.cer文件&#xff09;和Profile文件&#xff08;.p7b文件&#xff09;来保证应用/服务的完整性。在申请数字证书和Profile文件前&#xff0c;首先需要通过DevEco Studio来生成密钥&am…