【C++】多态详细讲解

本篇来聊聊C++面向对象的第三大特性-多态。
1.多态的概念
多态通俗来说就是多种形态。多态分为编译时多态(静态多态)和运⾏时多态(动态多态)。
- 编译时多态:主要就是我们前⾯讲的函数重载和函数模板,他们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态。
- 运⾏时多态:具体点就是去完成某个⾏为(函数),可以传不同的对象就会完成不同的⾏为,就达到多种形态。
我们重点要说的是运行时多态。运行时多态举个例子:买票的行为,学生买票是学生票,普通人买票是全价票,军人买票又是优先票...这就是买票行为的多种形态。
2.多态的定义及实现
2.1 多态的构成条件
实现多态还有 两个必须 重要条件:
- 必须是基类的指针或者引⽤调⽤虚函数
- 被调⽤的函数必须是虚函数,并且完成了虚函数重写/覆盖。
2.2 虚函数
class Adult //基类
{
public:virtual void BuyTicket(){cout << "全价票" << endl;}
}; virtual在继承里也遇到过,在继承里出现是为了解决菱形继承问题,跟多态里virtual意义不同。
2.3 虚函数的重写/覆盖
class Student : public Adult //派生类
{
public:virtual void BuyTicket(){cout << "学生票" << endl;}
}; 2.3.1 多态的实例
void Ticket(Adult* pa) //基类的指针
{pa->BuyTicket();
} 前面说过,多态的第一个条件,必须是基类的指针或者引⽤调⽤,这里的pa满足;第二个条件,被调用函数必须是虚函数而且进行了虚函数的重写/覆盖,这里的BuyTicket函数满足条件。
int main()
{Adult a; //基类对象Student s; //派生类对象Ticket(&a);Ticket(&s);return 0;
}
传不同的对象过去,得到的结果不同。

void Ticket(Adult& pa) //基类的引用
{pa.BuyTicket();
}int main()
{Adult a; //基类对象Student s; //派生类对象Ticket(a);Ticket(s);return 0;
}

class Soldier : public Adult //派生类
{
public:virtual void BuyTicket(){cout << "军人票" << endl;}
};
然后只用Student和Soldier这两个派生类。
int main()
{Student st; //派生类对象Soldier so; //派生类对象Ticket(st);Ticket(so);return 0;
}

2.3.2 对实例进一步解释
1.为什么一定是基类的指针或引用?在继承中我们说过派生类和基类的切片/切割问题,参数类型是基类可以保证实参传基类的对象也可以,传派生类的对象也可以。如果是派生类的指针或引用,基类对象根本传不过去。
2.对于Ticket函数的参数列表,按照我们在继承中的说法,不管传什么过去,调用的都是Adult的成员函数,因为此时pa是由参数类型决定的,与传过来的对象无关,但是,在多态中,pa与自己的类型无关,只与传过来的对象有关,指向谁,调用谁。
3.构成多态的两个条件必须都满足。
如果不满足其中一个条件,比如基类没有虚函数。
class Adult //基类
{
public:void BuyTicket() //没有虚函数{cout << "全价票" << endl;}
};class Student : public Adult //派生类
{
public:virtual void BuyTicket(){cout << "学生票" << endl;}
};void Ticket(Adult* pa) //基类的指针或引用
{pa->BuyTicket();
}int main()
{Adult a; //基类对象Student s; //派生类对象Ticket(&a);Ticket(&s);return 0;
} 此时不构成多态,pa就走继承那套逻辑,不管传什么都是调用基类的。

如果都是虚函数并且进行了虚函数的重写/覆盖,但是Ticket参数类型不是基类的指针或引用,依旧是不构成多态的。
class Adult //基类
{
public:virtual void BuyTicket(){cout << "全价票" << endl;}
};class Student : public Adult //派生类
{
public:virtual void BuyTicket(){cout << "学生票" << endl;}
};void Ticket(Adult pa) //不是基类的指针或引用
{pa.BuyTicket();
}int main()
{Adult a; //基类对象Student s; //派生类对象Ticket(a);Ticket(s);return 0;
}

所以一定要同时满足构成多态的两个条件。
2.3.3 多态的经典例题
下面程序运行的结果是什么?
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()
{B* p = new B;p->test();return 0;
}
答案:B -> 1
有两个类,A是基类,B是派生类。A和B里有返回值、函数名、参数类型都相同的函数func,虽然在派生类B里面没有加virtual,在这里依然构成虚函数的重写(原因前面说过)。
p是B类的指针,因为B继承了A,所以B类的指针p可以调用test函数。
在类里,成员函数都有隐含的this指针,这个this指针类型是A* 不是B*,虽然继承到B来了,但这只是一种说法,并不是真的把A里的内容全拷贝一份给B,所以A继承下来成员函数this指针类型依然是A*。
所以,调用func函数的就是基类的指针。
而func又构成虚函数的重写,所以这里同时满足了构成多态的两个条件。既然构成了多态,p指向派生类B,所以func应该是B类重写的func。
但是虚函数的重写只会重写函数体部分,重写后的样子如下。
所以结果是 B -> 1 。
2.3.4 协变
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;
} 返回类型也可以是自己类型的指针或引用。

2.3.5 析构函数的重写
基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了vialtual修饰,派⽣类的析构函数就构成重写。
class A
{
public:virtual ~A(){cout << "~A()" << endl;}
};
class B : public A {
public:~B(){cout << "~B()->delete:" << _p << endl;delete _p;}
protected:int* _p = new int[10];
};
// 只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能
//构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。
int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}
2.3.6 override 和 final关键字
class Car
{
public:virtual void Dirve(){}
};class Benz :public Car
{
public:virtual void Drive() override { cout << "Benz" << endl; }
};int main()
{return 0;
} 如果不加override这个代码是检查不出错误的。
class Car
{
public:virtual void Drive() final //不能被重写{}
};
class Benz :public Car
{
public:virtual void Drive() { cout << "Benz" << endl; }
};
int main()
{return 0;
} 不想成员函数被重写,就加上final。
2.4 重载/重写/隐藏的对⽐

3.纯虚函数和抽象类
class Car //抽象类
{
public:virtual void Drive() = 0; //纯虚函数
}; 纯虚函数不需要定义实现,只需要声明,但并不代表纯虚函数不能定义实现。抽象类不能实例化出对象。如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。
class Benz :public Car
{
public:virtual void Drive() //重写纯虚函数{cout << "Benz" << endl;}
};
此时就没有父类的对象,只有派生类的,但是父类的指针和引用还是有的。
int main()
{Car* pBenz = new Benz;pBenz->Drive();return 0;
}


4.多态的原理
4.1 虚函数表指针
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
protected:int _b = 1;char _ch = 'x';
};
int main()
{Base b;cout << sizeof(b) << endl;return 0;
} 答案:D.12
类的大小如何计算,在【C++】类和对象(上):初识类和对象 中的 2.2 对象大小 有详细解释。
按照计算对象大小的规则,这里的结果应该是8,但为什么是12呢?因为除了_b和_ch成员,还多⼀个_vfptr放在对象的前⾯(注意有些平台可能会放到对象的最后⾯,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(简称虚表指针)。
vf就是virtual function。这个指针在32位程序下大小为4,所以按照内存对齐规则,大小为12.
⼀个含有虚函数的类中都⾄少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。这里的表其实就是一个数组,虚函数表也就是一个函数指针数组,虚函数表指针就是指向这个数组的指针。

比如我们现在再加一个虚函数,这个_vfptr就会多一个内容。
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}virtual void Func2(){cout << "Func2()" << endl;}void Func3() //不是虚函数{cout << "Func3()" << endl;}
protected:int _b = 1;char _ch = 'x';
};

这里只会存放虚函数的指针,不是虚函数不会放在里面,func3就不再这个里面。

4.2 多态的原理
以下面这段代码为例。
class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:string _name;
};class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-打折" << endl; }
private:string _id;
};class Soldier : public Person {
public:virtual void BuyTicket() { cout << "买票-优先" << endl; }
private:string _codename;
};void Func(Person* ptr)
{ptr->BuyTicket();
}int main()
{Person ps;Student st;Soldier sr;Func(&ps);Func(&st);Func(&sr);return 0;
}
4.2.1 从理论上分析
有ps、st、sr三个对象,就有三个虚函数表,这三个虚表里存放着自己的虚函数。


多态中Func函数应该是指向谁调用谁。

从内存角度,这里的ptr“看到的”都是父类,因为传子类过去会切片。

在满足多态的条件下,运行时编译器会找_vfptr这个指针,然后通过这个指针去对应的虚函数表里去找虚函数的地址。不管传的ptr类型是什么,与类型无关,只与指向的对象有关。
4.2.2 从汇编角度观察
满足多态条件下的汇编,最终调的是 call eax 。

不满足多态时,把前面的代码父类中的virtual删除,就不满足了。
class Person //父类
{
public:void BuyTicket() { cout << "买票-全价" << endl; }
private:string _name;
};
来看一下汇编。

不管传的ptr是什么,与对象无关,调用的都是父类的函数。
4.3 静态绑定与动态绑定
- 对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定。
- 满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定。
// ptr是指针+BuyTicket是虚函数满⾜多态条件。
// 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址ptr->BuyTicket();
00CD3C72 mov eax,dword ptr [ptr]
00CD3C75 mov edx,dword ptr [eax]
00CD3C77 mov esi,esp
00CD3C79 mov ecx,dword ptr [ptr]
00CD3C7C mov eax,dword ptr [edx]
00CD3C7E call eax
// BuyTicket不是虚函数,不满⾜多态条件。
// 这⾥就是静态绑定,编译器直接确定调⽤函数地址ptr->BuyTicket();
00603C72 mov ecx,dword ptr [ptr]
00603C75 call Person::BuyTicket (06015FFh)

5.虚函数表更深入的讲解
- 基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共⽤同⼀张虚表,不同类型的对象各⾃有独⽴的虚表,所以基类和派⽣类有各⾃独⽴的虚表。
- 派⽣类由两部分构成,继承下来的基类和⾃⼰的成员,⼀般情况下,继承下来的基类中有虚函数表指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴的。
派⽣类中 重写的基类的虚函数 ,派⽣类的虚函数表中对应的虚函数就会 被覆盖 成派⽣类重写的虚函数 地址 。 派⽣类的虚函数表中包含, (1)基类的虚函数地址,(2)派⽣类重写的虚函数地址完成覆盖,(3)派⽣类⾃⼰的虚函数地址 三个部分。 虚函数表本质是⼀个存虚函数指针的 指针数组 ,⼀般情况这个数组最后⾯放了⼀个0x00000000标记。(这个C++并没有进⾏规定,各个编译器⾃⾏定义的)。 虚函数表 存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,vs下是存在代码段(常量区) 虚函数 存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在 代码段 的,只是虚函数的地址⼜存到了虚表中。
本次分享见到这里,我们下篇见~

相关文章:
【C++】多态详细讲解
本篇来聊聊C面向对象的第三大特性-多态。 1.多态的概念 多态通俗来说就是多种形态。多态分为编译时多态(静态多态)和运⾏时多态(动态多态)。 编译时多态:主要就是我们前⾯讲的函数重载和函数模板,他们传不同类型的参数就可以调⽤不同的函数,通…...
4. k8s二进制集群之ETCD集群证书生成
安装cfssl工具配置CA证书请求文件创建CA证书创建CA证书策略配置etcd证书请求文件生成etcd证书 继续上一篇文章《负载均衡器高可用部署》下面介绍一下etcd证书生成配置。其中涉及到的ip地址和证书基本信息请替换成你自己的信息。 安装cfssl工具 下载cfssl安装包 https://github…...
Drools规则引擎初体验
前言 假设有这样一个场景,订单管理系统需要根据用户的消费情况,来为每个用户发放不同程度的优惠券,这个发放规则复杂且多变,我们该怎么办?在代码中写死显然是不可取的,规则一变就要修改代码,频…...
Day36【AI思考】-表达式知识体系总览
文章目录 **表达式知识体系总览**回答1:**表达式知识体系****一、三种表达式形式对比****二、表达式转换核心方法****1. 中缀转后缀(重点)****2. 中缀转前缀** **三、表达式计算方法****1. 后缀表达式计算(栈实现)****…...
Tailwind CSS v4.0 升级与 Astro 5.2 项目迁移记录
本文博客链接 https://ysx.cosine.ren/tailwind-update-v4-migrate 自用小记。 Tailwind CSS v4.0 - Tailwind CSS 新的高性能引擎 - 完整构建的速度速度快 5 倍,增量构建的速度快于 100 倍以上 —— 以微秒为单位进行测量。为现代 Web 设计 - 建立在前沿的 CSS 特…...
K8S ReplicaSet 控制器
一、理论介绍 今天我们来实验 ReplicaSet 控制器(也叫工作负载)。官网描述如下: 1、是什么? ReplicaSet 副本集, 维护一组稳定的副本 Pod 集合。 2、为什么需要? 解决 pod 被删除了,不能自我恢…...
基于springboot校园点歌系统
基于Spring Boot的校园点歌系统是一种专为校园场景设计的音乐点播平台,它能够丰富学生的校园生活,提升学生的娱乐体验。以下是对该系统的详细介绍: 一、系统背景与意义 在校园环境中,学生们对于音乐有着浓厚的兴趣,传…...
【R语言】数据操作
一、查看和编辑数据 1、查看数据 直接打印到控制台 x <- data.frame(a1:20, b21:30) x View()函数 此函数可以将数据以电子表格的形式进行展示。 用reshape2包中的tips进行举例: library("reshape2") View(tips) head()函数 查看前几行数据&…...
【C++】2.高并发内存池 -- 如何设计一个定长内存池
博客主题:如何设计一个定长内存池 个人主页:https://blog.csdn.net/sobercq CSDN专栏:https://blog.csdn.net/sobercq/category_12884309.html Gitee链接:https://gitee.com/yunshan-ruo/high-concurrency-memory-pool 文章目录 前…...
Redis --- 使用Feed流实现社交平台的新闻流
要实现一个 Feed 流(类似于社交媒体中的新闻流),通常涉及以下几个要素: 内容发布:用户发布内容(例如文章、状态更新、图片等)。内容订阅:用户可以订阅其他用户的内容,获…...
React中key值的正确使用指南:为什么需要它以及如何选择
React中key值的正确使用指南:为什么需要它以及如何选择 一、key值的基本概念二、如何选择合适的key值1. 数据来源决定key策略2. key值的三大核心要求 三、React为何需要key值?1. 虚拟DOM优化机制2. 状态维护机制 四、常见误区及解决方案1. 索引作为key的…...
在Debian 12上安装VNC服务器
不知道什么标题 可以看到这个文章是通过豆包从国外网站copy的,先这样写着好了,具体的我有时间再补充,基本内容都在这里了。 在Debian 12上安装VNC服务器 简介 VNC(Virtual Network Computing,虚拟网络计算…...
游戏引擎学习第88天
仓库:https://gitee.com/mrxiao_com/2d_game_2 调查碰撞检测器中的可能错误 在今天的目标是解决一个可能存在的碰撞检测器中的错误。之前有人提到在检测器中可能有一个拼写错误,具体来说是在测试某个变量时,由于引入了一个新的变量而没有正确地使用它&…...
c++中priority_queue的应用及模拟实现
1.介绍 priority_queue 是一种数据结构,它允许你以特定的顺序存储和访问元素。在 C 标准模板库(STL)中,priority_queue 是一个基于容器适配器的类模板,它默认使用 std::vector 作为底层容器,并且默认使用最…...
深度学习篇---计算机视觉任务模型的剪裁、量化、蒸馏
文章目录 前言第一部分:计算机视觉任务图像分类特点 图像识别特点 目标检测特点 图像分割子任务特点 第二部分:模型剪裁、量化、蒸馏模型剪裁1.权重剪裁2.结构剪裁3.迭代剪裁 模型量化1.对称量化2.非对称量化3.动态量化4.静态量化 知识蒸馏1.训练教师网络…...
java-关键字(final,static)
关键字 final 和 static 是两个常用的关键字,它们分别用于不同的场景,具有不同的作用。 final final 关键字用于表示某个实体是不可变的。它可以应用于变量、方法和类。 final 变量 当 final 用于变量时,表示该变量一旦被初始化后&#…...
游戏引擎 Unity - Unity 设置为简体中文、Unity 创建项目
Unity Unity 首次发布于 2005 年,属于 Unity Technologies Unity 使用的开发技术有:C# Unity 的适用平台:PC、主机、移动设备、VR / AR、Web 等 Unity 的适用领域:开发中等画质中小型项目 Unity 适合初学者或需要快速上手的开…...
JDK17主要特性
JDK 17,也被称为Java 17或Java Platform, Standard Edition 17,是Java编程语言的第十七个主要版本,由Oracle公司在2021年9月发布。Java 17是一个长期支持(LTS,Long-Term Support)版本,这意味着它…...
将OneDrive上的文件定期备份到移动硬盘
背景: 我在oneDrive上存了很多文件,分布在多个文件夹中,也有套了好几层文件夹的情况。我希望每隔一段时间,将oneDrive上的所有文件向移动硬盘上拷贝一份,但是我只想将距离上一次向移动硬盘拷贝的文件相比,发…...
【Elasticsearch】geohex grid聚合
在 Elasticsearch 中,地理边界过滤是一种用于筛选地理数据的技术,它可以根据指定的地理边界形状(如矩形、多边形等)来过滤符合条件的文档。这种方法在地理空间数据分析中非常有用,尤其是在需要将数据限制在特定地理区域…...
crewai框架第三方API使用官方RAG工具(pdf,csv,json)
最近在研究调用官方的工具,但官方文档的说明是在是太少了,后来在一个视频里看到了如何配置,记录一下 以PDF RAG Search工具举例,官方文档对于自定义模型的说明如下: 默认情况下,该工具使用 OpenAI 进行嵌…...
算法 哈夫曼树和哈夫曼编码
目录 前言 一,二进制转码 二,哈夫曼编码和哈夫曼树 三,蓝桥杯 16 哈夫曼树 总结 前言 这个文章需要有一定的树的基础,没学过树的伙伴可以去看我博客树的文章 当我们要编码一个字符串转成二进制的时候,我们要怎么…...
TCP 丢包恢复策略:代价权衡与优化迷局
网络物理层丢包是一种需要偿还的债务,可以容忍低劣的传输质量,这为 UDP 类服务提供了空间,而对于 TCP 类服务,可以用另外两类代价来支付: 主机端采用轻率的 GBN 策略恢复丢包,节省 CPU 资源,但…...
Sumatra PDF:小巧免费,满足多样阅读需求
Sumatra PDF是一款完全免费的本地阅读器软件,以小巧的体积和全面的功能受到用户青睐。如今,它已经更新到3.3版本,带来了更多实用功能,尤其是新增的注释功能,值得我们再次关注。 软件特色 轻量级体积:压缩…...
vue2-给data动态添加属性
vue2-给data动态添加属性 1. 问题的来源 在VUe2中(VUE3中使用了proxy,及时动态添加也能实现响应式),如果我们动态给data添加一个属性,会发现视图没有同步更新举个例子我们通过v-for遍历data中的一个属性list…...
TiDB 分布式数据库多业务资源隔离应用实践
导读 随着 TiDB 在各行业客户中的广泛应用 ,特别是在多个业务融合到一套 TiDB 集群中的场景,各企业对集群内多业务隔离的需求日益增加。与此同时,TiDB 在多业务融合场景下的资源隔离方案日趋完善,详情可参考文章 《你需要什么样的…...
105,【5】buuctf web [BJDCTF2020]Easy MD5
进入靶场 先输入试试回显 输入的值成了password的内容 查看源码,尝试得到信息 什么也没得到 抓包,看看请求与响应里有什么信息 响应里得到信息 hint: select * from admin where passwordmd5($pass,true) 此时需要绕过MD5()函…...
BFS(广度优先搜索)——搜索算法
BFS,也就是广度(宽度)优先搜索,二叉树的层序遍历就是一个BFS的过程。而前、中、后序遍历则是DFS(深度优先搜索)。从字面意思也很好理解,DFS就是一条路走到黑,BFS则是一层一层地展开。…...
33.Word:国家中长期人才发展规划纲要【33】
目录 NO1.2样式 NO3 图表 NO4.5.6 开始→段落标记视图→导航窗格→检查有无遗漏 NO1.2样式 F12/另存为:Word.docx:考生文件夹样式的复制样式的修改 样式的应用(没有相似/超级多的情况下)——替换 [ ]通配符&#x…...
gym-anytrading
参考:https://github.com/upb-lea/gym-electric-motor AnyTrading 是一组基于 reinforcement learning (RL) 的 trading algorithms(交易算法)的 OpenAI Gym 环境集合。 该项目主要用于foreign exchange (FOREX) 和 stock markets (股票市场)…...






