Cpp多态机制的深入理解(20)
文章目录
- 前言
- 一、多态的概念
- 二、多态的定义与实现
- 两个必要条件
- 虚函数
- 虚函数的重写
- 重写的三个例外
- override 和 final
- 重载、重写(覆盖)、重定义(隐藏)
- 三、抽象类
- 概念
- 接口继承和实现继承
- 四、多态的原理
- 虚表和虚表指针
- 虚函数调用过程
- 动态绑定与静态绑定
- 五、那...那单继承甚至多继承呢?
- 总结
前言
多态也是三大面向对象语言的特性之一,同时我也觉得他也蛮有意思的
与封装“一个方法,多个接口”不同的是,多态可以实现 “一个接口,多种方法”
调用同名函数时,可以根据不同的对象(父类对象或子类对象)调用属于自己的函数,实现不同的方法,因此 多态 的实现依赖于 继承
一、多态的概念
在使用多态的代码中,不同对象完成同一件事会产生不同的结果
比如在购买高铁票时,普通人原价,学生半价,而军人可以优先购票,对于 购票 这一相同的动作,需要 根据不同的对象提供不同的方法
二、多态的定义与实现
两个必要条件
- virtual 修饰后形成的虚函数,与其他类中的虚函数形成 重写(三同:返回值、函数名、参数均相同)
- 必须通过【父类指针】或【父类引用】进行虚函数调用
虚函数
被virtual修饰的类成员函数称为虚函数
全局虚函数没有意义,因为虚函数是为多态而用的
虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同(类型相同即可)),称子类的虚函数重写了基类的虚函数
// 基类
class Person
{
public:// 虚函数virtual void BuyTicket() { cout << "买票-全价" << endl; }
};// 派生类
class Student : public Person
{
public:// 虚函数重写virtual void BuyTicket() { cout << "买票-半价" << endl; }
};// 三种函数实现
// 引用
void Func(Person& p)
{p.BuyTicket();
}// 指针
//void Func(Person* p)
//{
// p->BuyTicket();
//}// 非引用指针,调用父类
//void Func(Person p)
//{
// p.BuyTicket();
//}
测试结果:
重写的三个例外
- 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变
这个了解一下就行,实际我感觉挺没啥用处的
如果你也有这种感觉,鼓励你致电老本,去好好批斗他!
class A {};
class B : public A {};class Person
{
public:// 协变 返回值可以是父子类对象指针或引用//virtual A* BuyTicket() // 返回值是父类指针virtual Person* BuyTicket(){cout << "Person-> 买票-全价" << endl;return nullptr;}
};class Student : public Person
{
public://virtual B* BuyTicket()// 返回值是子类指针virtual Student* BuyTicket(){cout << "Student-> 买票-半价" << endl;return nullptr;}
};
- 析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
class Person
{
public:// 析构函数名不同,构成重写,编译器将析构函数名字统一处理成destructorvirtual ~Person(){cout << "~Person()" << endl;}
};class Student : public Person
{
public:virtual ~Student(){cout << "delete[]" << _ptr << endl;delete[] _ptr;cout << "~Student()" << endl;}
private:int* _ptr = new int[10];
};void Func(Person& p)
{p.BuyTicket();
}int main()
{// 正常情况调用析构没有问题//Person p;//Student s;//Func(p);//Func(s);// 派生类有动态开辟的内存,需要调用多态// 指向谁调用谁Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}
- 派生类重写虚函数virtual关键字可以省略
class Person
{
public:virtual ~Person(){cout << "~Person()" << endl;}
};class Student : public Person
{
public:// 派生类virtual关键字省略~Student(){cout << "~Student()" << endl;}
};
override 和 final
C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载
- final:修饰虚函数,表示该虚函数不能再被重写
- override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
// final 修饰虚函数,不能重写
class Car
{
public:// 加了final关键字,虚函数不能被重写virtual void Drive() final {}
};class Benz :public Car
{
public:virtual void Drive() { cout << "Benz-舒适" << endl; }
};int main()
{Benz b;return 0;
}
重载、重写(覆盖)、重定义(隐藏)
三、抽象类
概念
在虚函数的后面写上 = 0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
class Car
{
public:// 纯虚函数 强制派生类重写虚函数 virtual void Drive() = 0;
};int main()
{Car c;return 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 c;Benz b1;BMW b2;// 基类可以定义指针 指向谁调用谁Car* ptr1 = &b1;Car* ptr2 = &b2;ptr1->Drive();ptr2->Drive();return 0;
}
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数
四、多态的原理
在讲解原理之前,不如我们先来看这么一段神奇代码
#include <iostream>
using namespace std;class Test
{virtual void func() {};
};int main()
{Test t; //创建一个对象cout << "Test sizeof(): " << sizeof(t) << endl;return 0;
}
可能你会觉得没有对象,会觉得是0,但是你突然想起了之前讲过的空类也占内存空间,你可能会想是不是1
但是其实都错了,真相是4/8(取决于你的系统是32位还是64位),可能我这么一说,你也猜到了其实有一个隐藏变量,且类型是指针类型
其实,就是靠着这个虚表指针和虚表实现了多态
虚表和虚表指针
在 vs 的监视窗口中,可以看到涉及虚函数类的对象中都有属性 __vfptr(虚表指针),可以通过虚表指针所指向的地址,找到对应的虚表
虚函数表中存储的是虚函数指针,可以在调用函数时根据不同的地址调用不同的方法
可能有点混,有三个“虚”,大家别被整虚了!
虚表指针指向虚表,虚表里面存放着虚函数指针,所以虚表的本质其实是个函数指针数组
接下来我会给出一段代码,在该代码中父类 Person 有两个虚函数(func3 不是虚函数),子类 Student 重写了 func1 这个虚函数,同时新增了一个 func4 虚函数
#include <iostream>using namespace std;class Person
{
public:virtual void func1() { cout << "Person::fun1()" << endl; };virtual void func2() { cout << "Person::fun2()" << endl; };void func3() { cout << "Person::fun3()" << endl; }; //fun3 不是虚函数
};class Student : public Person
{
public:virtual void func1() { cout << "Student::fun1()" << endl; };virtual void func4() { cout << "Student::fun4()" << endl; };
};int main()
{Person p;Student s;return 0;
}
//打印虚表
typedef void(*VF_T)();void PrintVFTable(VF_T table[]) //也可以将参数类型设为 VF_T*
{//vs中在虚表的结尾处添加了 nullptr//如果运行失败,可以尝试清理解决方案重新编译int i = 0;while (table[i]){printf("[%d]:%p->", i, table[i]);VF_T f = table[i];f(); //调用函数,相当于 func()i++;}cout << endl;
}int main()
{//提取出虚表指针,传递给打印函数Person p;Student s;//第一种方式:强转为虚函数地址(4字节)PrintVFTable((VF_T*)(*(int*)&p));PrintVFTable((VF_T*)(*(int*)&s));return 0;
}
子类重写后的虚函数地址与父类不同
因为平台不同指针大小不同,因此上述传递参数的方式(VF_T * )( * (int * )&p 具有一定的局限性
假设在 64 位平台下,需要更改为 (VF_T * )( * (long long * )&p
//64 位平台下指针大小为 8字节
PrintVFTable((VF_T*)(*(long long*)&p));
PrintVFTable((VF_T*)(*(long long*)&s));
除此之外还可以间接将虚表指针转为 VF_T* 类型进行参数传递
//同时适用于 32位 和 64位 平台
PrintVFTable(*(VF_T**)&p);
PrintVFTable(*(VF_T**)&s);
传递参数时的类型转换路径
不能直接写成 PrintVFTable((VF_T*)&p);,因为此时取的是整个虚表区域的首地址地址,无法定位我们所需要虚表的首地址,打印时会出错
综上所述,虚表是真实存在的,只要当前类中涉及了虚函数,那么编译器就会为其构建相应的虚表体系
虚表是在 编译 阶段生成的
虚表指针是在构造函数的 初始化列表 中初始化的
虚表一般存储在 常量区(代码段),有的平台中可能存储在 静态区(数据段)
int main()
{//验证虚表的存储位置Person p;Student s;int a = 10; //栈int* b = new int; //堆static int c = 0; //静态区(数据段)const char* d = "xxx"; //常量区(代码段)printf("a-栈地址:%p\n", &a);printf("b-堆地址:%p\n", b);printf("c-静态区地址:%p\n", &c);printf("d-常量区地址:%p\n", d);printf("p 对象虚表地址:%p\n", *(VF_T**)&p);printf("s 对象虚表地址:%p\n", *(VF_T**)&s);return 0;
}
显然,虚表地址与常量区的地址十分接近,因此可以推测 虚表 位于常量区中,因为它需要被同一类中的不同对象共享,同时不能被修改(如同代码一样)
虚函数调用过程
综上,我们可以大概想象出多态的原理了:
- 首先确保存在虚函数且构成重写
- 其次使用【父类指针】或【父类引用】指向对象,其中包含切片行为
- 切片后,将子类中不属于父类的切掉,只保留父类指针可调用到的部分函数
- 实际调用时,父类指针的调用逻辑是一致的:比如虚表第一个位置调用第一个函数,虚表第二个位置调用第二个函数,但是因为此时的虚表是切片得到的,所以 同一位置 可以调用到不同的函数,这就是多态
也就是说,父类和子类的虚表其实是不一样的,在构成重写的前提下!
这就是多态!
int main()
{Person* p1 = new Person();Person* p2 = new Student();p1->func1();p2->func1();delete p1;delete p2;return 0;
}
通过汇编代码来看的话:
动态绑定与静态绑定
其实我们想一想,函数重载某种程度上也是一种多态,也是一个函数面对不同对象的时候有不同的效果,但是不同的是,重载在编译的时候就确定了待调用函数的地址,而动态绑定的代码,待调用地址存放在 eax 中,不确定
五、那…那单继承甚至多继承呢?
坦白说,这很麻烦,我也不敢说我很懂,于是我在这里贴两篇文章,大家自行参阅吧!
《C++虚函数表解析》
《C++对象的内存布局》
总结
我们终于学完三大面向对象特性了,坦白说,多态还是蛮困难的,但是,我们难度的最高峰再过几篇就要来了,怕不怕!
相关文章:

Cpp多态机制的深入理解(20)
文章目录 前言一、多态的概念二、多态的定义与实现两个必要条件虚函数虚函数的重写重写的三个例外override 和 final重载、重写(覆盖)、重定义(隐藏) 三、抽象类概念接口继承和实现继承 四、多态的原理虚表和虚表指针虚函数调用过程动态绑定与静态绑定 五、那...那单继承甚至多…...

(六)Python结构数据类型
一、集合类型(Sets) Sets(集合)是一个无序不重复的元素集。主要功能是自动清除重复的元素。创建集合时使用大括号{}包含其中元素。 Food{西瓜,南瓜,冬瓜,北瓜} print(Food) 输出结果: 增加重复元素,则会…...

C++进阶-->多态(Polymorphism)
1. 多态的概念 多态,顾名思义多种形态;多态分为编译时多态(静态多态)和运行时多态(动态多态),静态多态就是就是我们前面讲的函数重载和函数模板,可以通过传不同类型,然后…...

python实战项目51:selenium结合requests获取某众点评评论
python实战项目51:selenium结合requests获取某众点评评论 一、selenium获取cookies二、利用requests发送请求三、注意事项四、完整代码一、selenium获取cookies 首先,初始化selenium的webdriver,然后使用webdriver打开某众点评主页,之后手动扫码登录,利用selenium的get_c…...
面试准备第一版ssm spring-springmvc
请写出spring中常用的依赖注入方法: 1、setter 2、构造方法注入 3、字段注入 Setter 注入: 通过公共的 setter 方法进行依赖注入。优点:可选依赖,能更清晰地看到依赖关系。缺点:依赖在构造时不可用,可能导…...

Ubuntu学习笔记 - Day1
文章目录 学习目标:学习内容:学习笔记:Linux简介基于Linux内核的系统 Ubuntu简介GNU简介 远程连接Ubuntu查看Ubuntu的IP地址Mac连接Ubuntu此时可能显示报错,连接被拒绝解决办法连接成功连接退出 学习目标: 一周掌握 Li…...
挑战Java面试题复习第4天,坚持就是胜利
挑战第 4 天 Excption与Error包结构OOM 知识点SOF 知识点线程程序进程知识点有些字段不想序列化,怎么办?说说 IO 流Java IO与 NIO的区别 Excption与Error包结构 运行时异常(RuntimeException): 包括RuntimeException…...

Android 虚拟化框架(AVF)指南
Android 虚拟化框架(AVF)指南 一、项目介绍二、项目特色三、如何使用AVF四、总结 随着移动设备的普及和应用场景的多样化,安全性和隐私保护成为了移动操作系统的重要课题。Android作为全球最广泛使用的移动操作系统之一,一直在不断…...

day-77 超级饮料的最大强化能量
思路 动态规划:因为每一步要么选A,要么选B,所以问题可以转换为求最后一步从A选或从B选中的较大值 解题过程 定义而二维数组dp,dp[i][0]表示最后一步从A取能获得的最大能量,dp[i][1]表示最后一步从B取能获得的最大能量状态转换方程…...

有道小P 1.0.8 | 完全免费的AI全科学习助手,家长的好帮手
有道小P是一款由网易有道出品的完全免费的AI全科学习助手,专为中小学生设计。它支持多种输入方式,包括文字、语音和拍照识别,能够覆盖十个科目的所有题型,提供详细的解析和逐步解答,帮助孩子们理解和吸收知识。此外&am…...

vue项目中如何在路由变化时增加一个进度条
在 Vue.js 项目中,使用路由(如 Vue Router)时,为了提升用户体验,你可能会想要在路由变化时显示一个进度条。这可以通过多种方式实现,其中一种流行的做法是使用第三方库,如 vue-loading-bar 或 n…...

如何解决mingw64安装后配置完环境变量仍然执行不了gcc命令以及Vscode中的环境路径配置中找不到gcc
配置环境变量教程很多,就不多说,说下耗费一小时解决的问题:mingw64安装后配置完环境变量仍然执行不了gcc命令 配置 了N次了,都还是在终端找不到指令,然后,将路径放到第一个,然后再看下…...

3-petalinux2018.3 摸索记录 - 命令驱动 _ 交叉编译链
一、命令行控制GPIO 对于ps端设备,在板卡的linux系统中,切换到/sys/class/gpio路径下可以看到目前挂载的gpio设备。 export: 导入用户空间 gpiochip: 系统中gpio寄存器信息 unexport: 移除用户空间 以MIO40和MIO42…...
【二分查找】——模板
二分查找模板题 一、题目要求 给定一个长度为n的非递减数组和一个数字target,要求找到数组中第一个大于等于target的位置pos,数组下标从 0 开始。如果不存在大于等于target的数字,则输出 -1。 二、输入格式 第一行:为两个正整…...
从可逆计算看DSL的设计要点
低代码平台的可视化设计器本质上是DSL(Domain Specific Language)的结构化编辑器。可视化设计器将编辑的结果序列化成文本格式时所采用的规范就是一种DSL语法定义。 Nop平台基于可逆计算原理,提出了一整套系统化的构建机制来简化DSL的设计和…...
axios竟态问题
竟态问题 在我们日常开发经常遇到一些竟态问题 例子1 现象1 表格分页,如果设置请求loading, 先切换到分页第99页,迅速在又切换到第1页,最后列表显示的是第99页数据。 原因 由于第99页请求数据花费时间可能500ms,第1页数据只需要100ms,第1页…...

如何批量注册多个Outlook邮箱账号并避免关联
批量注册多个Outlook邮箱账号时,如何避免账号之间的关联性是一个重要的考量因素。会在此文一起探讨如何高效且安全地批量注册多个Outlook邮箱账号,并提供一些实用的建议来确保这些账号不会被关联。 一、Outlook邮箱批量注册机制 在深入注册流程之前&…...
如何在安卓設備上設置全局代理?
對於安卓用戶來說,設置全局代理是維護網路隱私一種有效的方法,可以幫助在所有應用中使用同一個代理伺服器。本文將詳細介紹如何在安卓設備上進行全局代理設置。全局代理指的是通過一個代理伺服器來轉發設備上所有應用程式的網路請求。這樣,所…...

操作系统实验记录
实验零:虚拟机安装 一、安装vmware虚拟机 与vmware匹配搜索结果 - 考拉软件 (rjctx.com),下载17.5.1版本即可下载后对照教程安装 二、下载iso虚拟驱动 搜索清华大学镜像网站,点击再搜ubuntu,下载这个4.1GB的iso文件安装后打开vmware虚拟机 三、配置vmware虚拟机 右键管…...
FastAPI 路径参数详解:动态路径与数据校验的灵活实现
FastAPI 路径参数详解:动态路径与数据校验的灵活实现 本文全面介绍了在 FastAPI 中使用路径参数的技巧和实现方式。路径参数允许 API 动态响应不同路径中的请求信息,结合 URL(Uniform Resource Locator)和 URI(Unifor…...
CVPR 2025 MIMO: 支持视觉指代和像素grounding 的医学视觉语言模型
CVPR 2025 | MIMO:支持视觉指代和像素对齐的医学视觉语言模型 论文信息 标题:MIMO: A medical vision language model with visual referring multimodal input and pixel grounding multimodal output作者:Yanyuan Chen, Dexuan Xu, Yu Hu…...
在HarmonyOS ArkTS ArkUI-X 5.0及以上版本中,手势开发全攻略:
在 HarmonyOS 应用开发中,手势交互是连接用户与设备的核心纽带。ArkTS 框架提供了丰富的手势处理能力,既支持点击、长按、拖拽等基础单一手势的精细控制,也能通过多种绑定策略解决父子组件的手势竞争问题。本文将结合官方开发文档,…...

React19源码系列之 事件插件系统
事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...

微服务商城-商品微服务
数据表 CREATE TABLE product (id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 商品id,cateid smallint(6) UNSIGNED NOT NULL DEFAULT 0 COMMENT 类别Id,name varchar(100) NOT NULL DEFAULT COMMENT 商品名称,subtitle varchar(200) NOT NULL DEFAULT COMMENT 商…...
【学习笔记】深入理解Java虚拟机学习笔记——第4章 虚拟机性能监控,故障处理工具
第2章 虚拟机性能监控,故障处理工具 4.1 概述 略 4.2 基础故障处理工具 4.2.1 jps:虚拟机进程状况工具 命令:jps [options] [hostid] 功能:本地虚拟机进程显示进程ID(与ps相同),可同时显示主类&#x…...
C++.OpenGL (14/64)多光源(Multiple Lights)
多光源(Multiple Lights) 多光源渲染技术概览 #mermaid-svg-3L5e5gGn76TNh7Lq {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-3L5e5gGn76TNh7Lq .error-icon{fill:#552222;}#mermaid-svg-3L5e5gGn76TNh7Lq .erro…...

【C++进阶篇】智能指针
C内存管理终极指南:智能指针从入门到源码剖析 一. 智能指针1.1 auto_ptr1.2 unique_ptr1.3 shared_ptr1.4 make_shared 二. 原理三. shared_ptr循环引用问题三. 线程安全问题四. 内存泄漏4.1 什么是内存泄漏4.2 危害4.3 避免内存泄漏 五. 最后 一. 智能指针 智能指…...

Golang——6、指针和结构体
指针和结构体 1、指针1.1、指针地址和指针类型1.2、指针取值1.3、new和make 2、结构体2.1、type关键字的使用2.2、结构体的定义和初始化2.3、结构体方法和接收者2.4、给任意类型添加方法2.5、结构体的匿名字段2.6、嵌套结构体2.7、嵌套匿名结构体2.8、结构体的继承 3、结构体与…...
MinIO Docker 部署:仅开放一个端口
MinIO Docker 部署:仅开放一个端口 在实际的服务器部署中,出于安全和管理的考虑,我们可能只能开放一个端口。MinIO 是一个高性能的对象存储服务,支持 Docker 部署,但默认情况下它需要两个端口:一个是 API 端口(用于存储和访问数据),另一个是控制台端口(用于管理界面…...
uniapp 实现腾讯云IM群文件上传下载功能
UniApp 集成腾讯云IM实现群文件上传下载功能全攻略 一、功能背景与技术选型 在团队协作场景中,群文件共享是核心需求之一。本文将介绍如何基于腾讯云IMCOS,在uniapp中实现: 群内文件上传/下载文件元数据管理下载进度追踪跨平台文件预览 二…...