C++ - 多态语法 - 虚函数使用介绍
多态简单介绍
多态就是多种形态,是不同的对象去完成同一个动作所产生的结果可能有多种。这种多种的形态我们称之为多态。
比如:我们在买票的时候的时候,可能有成人全价,儿童半价,军人免票等等。对于成人,儿童,军人这三个不同的对象,在买票同一动作当中,就产生了不同的结果。
多态的定义 和 实现
多态出现在同一继承关系当中的不同类对象,比如上述说的 Person对象买票全价,Student对象买票半价。
在多态的组成方面,有两大必须的条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
虚函数
在知道多态是如何构成之前,我们先来认识一种特殊的成员函数---虚函数。
注意:
- 其中的 virtual 虽然可以用来修饰虚函数,和虚继承,但是此时的虚函数和虚继承没有任何关系,可以理解为 virtual 修饰函数就是虚函数;修饰继承关系及时虚继承。
- 关于虚函数 virtual 的修饰,只要在 函数的返回值之前加上 vitual 修饰的函数就是虚函数了。
- 只要类当中的成员函数可以加 virtual 修饰 变成虚函数,普通的全局函数是不能加 virtual 变成虚函数的。
虚函数定义如:
class Person {
public:virtual void buy() { cout << "全价" << endl;}
};
全局函数不能加 virtual 修饰变成虚函数:

虚函数的重写
虚函数和其他成员函数一样,但是虚函数有一个特征,虚函数支持重写(覆盖)。
如果在派生类当中,有一个和基类当中相同的虚函数(两者之间返回值,函数名,参数列表完全相同),我们认为,此时派生类重写了基类当中的虚函数。
class Person
{
public:virtual void buy() {cout << "Perosn:全价" << endl;}
};class Student : public Person
{
public:virtual void buy() {cout << "Student:半价" << endl;}
};
上述子类(student)就重写了 父类(Perosn)当中的 buy() 这个虚函数。
对于上述 这种虚函数的使用场景(通过指针或者引用来调用虚函数):
void func(Person& people)
{people.buy();
}int main()
{func(Person());func(Student());return 0;
}
输出:
Perosn:全价
Student:半价
这样的话,我们就可以做到类似于,自动识别对象,然后去购买不同的票了。
请注意,我们在调用虚函数的时候,一定是使用 引用或者指针的方式来调用虚函数,而且子类父类当中的函数都应该是 virtual 修饰的,子类重写过的虚函数,否则无法实现多态(如下,我们把func()函数当中的 Person& 参数类型 改为 Person):
void func(Person people)
{people.buy();
}
输出:
Perosn:全价
Perosn:全价
我们发现结果都是 “全价”。没有多态现象出现。
同样,如果父类的函数没有加 virtual 修饰,输出结果和上述一样,但是如果父类虚函数加了 virtual 修饰,子类函数没有加 virtual 修饰,是可以实现多态的。----但是就算能够实现多态,建议还是把子类和父类的虚函数都加上 virtual 修饰。
编译器在这里支持,派生类不用加 virtual ,是因为,编译器对于派生类的检查只是检查,派生类符不符合 “三同”的 多态条件。不同,可能看该函数和父类当中的虚函数函数名相同,就是别成隐藏了;相同才会去认为该函数是虚函数的重写。
因为 派生类 继承了 父类的 virtual 修饰的虚函数,而子类当中的 重写只是对 父类当中虚函数的实现部分进行 重写。
class Person
{
public:virtual void buy() {cout << "Perosn:全价" << endl;}
};class Student : public Person
{
public:void buy() {cout << "Student:半价" << endl;}
};void func(Person& people)
{people.buy();
}int main()
{Person perosn;func(perosn);Student student;func(student);return 0;
}
输出:
Perosn:全价
Student:半价
像上述的实现多态例子中的 Student 类型 对象传参到 Person& 类型参数接收,这里发生了 子类 到 父类的 切割。
有了切割,当传入参数就是父类的时候,不需要切割,这类直接就是调用父类对象的引用来调用buy()这个函数;如果传入的是子类的话,就会发生切割,指向子类,此时就是子类的引用,所以调用的是子类的buy()函数。
具体切割是如何切割法,可以看以下博客:C++ - 继承_chihiro1122的博客-CSDN博客
但是这里就有一个问题,我们知道,对象当中只存储成员变量,不存储成员函数;而且就算是子类的引用,只是访问的是父类当中子类的那一部分成员,编译器在此处究竟是如何做到区分两个虚函数的呢?
虚函数重写的两个特殊情况
协变
这种情况是 -- 基类和派生类虚函数的返回值类型不同。
但是,这里虚函数的返回值类型是有规定的,如果是只是普通类型的返回值类型不同,是会报错的:

如果不是协变引起的虚函数返回值类型不同,编译器是会报编译错误的。
只允许 基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用的情况,而我们把这种称为 协变。(而且,父类虚函数 和 子类虚函数 的返回值类型 必须同时是 指针 或者 引用,如果是像 指针 和 引用 岔着用是不行的,编译器会报错)
如下代码所示:
class Person
{
public:virtual Person* buy() {cout << "Perosn:全价" << endl;return 0;}
};class Student : public Person
{
public:Student* buy() {cout << "Student:半价" << endl;return 0;}
};
虽然协变指定是父类虚函数返回值是父类的指针或引用,紫烈虚函数返回值是子类的之怎或引用;但是,只要是满足继承关系的类,按照上述的方式去使用协变,也是可以的(就是说上述返回值不一定是 Person 和 Student,也可以是其他父子关系)。
如下代码所示(在 Person 和 Student 的虚函数返回值类型使用 A 和 B 其他继承关系):
class A
{
public:};class B : public A
{
};class Person
{
public:virtual A* buy() {cout << "Perosn:全价" << endl;return 0;}
};class Student : public Person
{
public:B* buy() {cout << "Student:半价" << endl;return 0;}
};
但是协变是一个 坑,由上面说的种种细节可以看出来,细节很多,不好记。而且协变在日常当中的使用频率也很少。不如不支持这个语法。但是在学校考试 和 面试当中经常考。
析构函数的重写
class Person {
public:virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:virtual ~Student() { cout << "~Student()" << endl; }
};
虽然上述的 Person 和 Student 两个类的析构函数名字看上去不同,但是实际上,继承当中的 父类 和子类的 析构函数是可以 构成虚函数的。
如上述例子, ~Perosn()和 ~Student()两个函数,子类可以重写。
之所以支持,是因为,类的虚构函数都被处理为了destructor 这个统一的名字。这样处理的目的也是为了让 子类和父类的析构函数构成重写。
如果不这样处理,会出现一些问题,子类重写的话,会出现一些问题:
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;
}
如上所示,我们希望输出的结果是 :
~Person()
~Student()
~Person()
但,实际输出却是:
~Person()
~Person()
出现这个问题的原因是也 p 指针的类型。我们知道,普通对象 看当前调用的类型来决定调用 哪一个对象的析构函数,当前调用者 (p) 的类型是 Person*,所以自然只会调用 Person 对象的析构函数,(对于 delete p ,释放顺序是 p->destructor + operator delete(p) ),这里调用的是 Person的析构函数,但是这里我们不希望调用 Perosn的析构函数。
这里我们希望 p 指向那个对象就调用哪一个对象的析构函数,而不是看 p 指针的类型来决定调用哪一个对象的 析构函数。如果看类型的话,一直调用的就是 p 的类型的析构函数。但是 p 这个指针有可能指向父类,也有可能指向子类。
我们希望 p->destructor()调用的析构函数,是一个多态调用,而不是一个普通调用。
所以这里我们要使用多态来实现,在 detele 底层实现当中,就是使用 指针来调用 析构函数的,指针已经实现了,现在还差重写,所以才有了上述的 析构函数重写。
final 和 override
上述我们也介绍了,如果实现函数重写,我们也发现,C++当中对于重写函数的规定还不少,缺一样都会导致重写失败。有些错误甚至在编译器时期是不会报错的,只有在程序运行之后才能发现问题,此时在发现问题就只能去debug,在代码量很多的场景当中,特别麻烦。
所以在C++11 当中新增了 两个关键词 final 和 override ,来帮助我们检查是否重写。
final:
final 关键字是用来阻止某一虚函数被子类重写:

final 关键词修饰位置 和 之前 const 修饰 this 指针一样,是在 参数列表括号的右边。(而且只能放在父类的虚函数上)
当父类的 虚函数被 final 修饰之后,子类就不能再重写父类的这个虚函数了。
override:
override用于帮助派生类检查是否完成重写,如果没有,会报错:

这样就方式我们因为,派生类没有重写完成,而导致后序debug的麻烦了。
虚函数的指针 与 虚函数表 (多态的一些底层原理)
下面这个例子,应该输出什么?
class Bass
{
public:virtual void func(){}private:char _b;
};int main()
{cout << sizeof(Bass) << endl;Bass b;return 0;
}
上述输出不是1,而是8。我们知道,类的大小只计算成员大小,不计算函数。
我们打开调试发现,在 b 这个对象当中多了一个 _vfptr 指针(virtual function)。

这指针是 虚函数表 指针,
这就是为什么,没有实现多态,不要把虚函数搞到类当中去;因为虚函数会被放进虚函数表当中;其实严格来说,虚函数还是存储在代码段当中的,而虚函数表当中存储的是各个虚函数的地址。
这个虚函数表,在重写之后,会发生变化,我们来看下面这个例子:
class Person {
public:virtual void BuyTicket() {cout << "买票-全价" << endl;}int _a = 1;
};
class Student : public Person {
public:virtual void BuyTicket() {cout << "买票-半价" << endl;}int _b = 2;
};void Func(Person& p)
{p.BuyTicket();
}int main()
{Person Mike;Student jason;Func(Mike);Func(jason);return 0;
}
在上述这个代码当中,Mike 对象(父类对象)当中有下面这两个部分:
_vfptr 是虚函数表的指针,此时的虚函数表当中存储的是 Person(父类)当中虚函数的地址,此时只有一个地址,因为只写了一个虚函数,如果有多个虚函数的话,有几个虚函数,虚函数表当中就有几个地址。
Jason对象(子类对象)当中有下面两个部分:
我们发现,在子类对象 jason 当中有一个父类对象,父类对象当中也有一个虚函数表指针,此时虚函数表当中也只有一个地址,这个地址已经发生了改变,指向了子类重写的虚函数。
总结:重写也可以叫覆盖,重写是我们写代码层面所看到的,覆盖是底层逻辑当中,子类重写的虚函数地址覆盖了父类虚函数的地址。
此时我们就明白下面这个函数是如果实现,传入父类就调用父类的函数,传入子类就调用子类的函数了:

- 传入父类,看到就是父类,直接调用父类的函数;传入子类,切片之后看到的还是父类;
- 如果是普通调用,在编译的时候就确定了地址,编译器判断是不是普通的调用很简单,看符不符合多态,不符合就是普通调用。
- 如果是普通调用,就直接看p的类型,p的类型是Person,那么就直接在Person当中找到这个函数的地址,所以就不能实现多态。
- 符合多态,就和上述说的一样,运行时到指向的对象的虚函数表当中,找调用。
重载,重写(覆盖),重定义(隐藏)的对比

虚函数和多态的例题

很多人,看到满足多态的条件,以为输出的是 B->0 ;但是实际输出却是 B->1。
我们发现,上述的func()函数,满足 虚函数重写,子类父类的虚函数函数名,返回值,参数类型和个数都是相同的(注意,不要看val 的缺省参数不同就认为这里不满足多态,参数列表相同只要求 参数个数 和 参数类型相同即可);
而且,在 test()函数当中调用的 func ()函数,使用指针调用的 ,因为 func()函数是本类当中的成员函数,本类当中的成员是需要用 this->func() 这样的形式来访问的;而这里的this指针是父类还是子类的 指针呢?
答案是父类的。因为,子类继承父类当中的成员,不是直接进行拷贝赋值,而是调用父类的构造函数,在子类当中构造出一个父类的子对象,这个子对象我们可以理解为子类当中父类对象成员。然而,test()函数是存在于代码段的,他也不是在子类和父类当中都有存在,也就是说,test()函数只在代码段当中存储了一份,而不是在子类和父类当中都存储了一份。
因为,父类对象是直接在子类当中存储的,子类不会单独的看test()函数,而是把父类对象看做是一个整体,test()就在这个整体当中,所以,test()对象当中的 调用 func()函数使用的this指针是 A*(父类指针)。
而在主函数当中的指针p,指向的是 B (子类对象),又满足多态,所以此时肯定是调用子类当中的 func()函数,所以输出 B-> 是正确的。
但是,要注意的是,重写只是重写函数当中 实现部分,对于函数名,返回值,参数列表还是使用的是父类的。所以,此处的 val 的缺省参数才是 父类当中的1,而不是子类当中的0。
可以理解为,重写是,父类 的 函数名,返回值,参数列表 + 子类函数实现。

现在我们把上述例题修改一下,把 test()函数挪到 B 函数当中,其他不变:

此时输出结果就是 B->0 了 。因为此时的 test()函数不满足多态的条件,此时的test()函数当中调用的 func()函数的 this 指针不是A*(父类指针)了,而是 B* (子类指针)。
所以,此时 test()当中的 调用 func()函数,就只是一个简单的 在本类当中调用本类的其他函数的情况。
相关文章:
C++ - 多态语法 - 虚函数使用介绍
多态简单介绍 多态就是多种形态,是不同的对象去完成同一个动作所产生的结果可能有多种。这种多种的形态我们称之为多态。 比如:我们在买票的时候的时候,可能有成人全价,儿童半价,军人免票等等。对于成人,儿…...
php获取客户端ip地址及ip所在国家、省份、城市、县区
摘要 获取客户端ip地址,然后使用这个ip地址获取所在的国家、省份、城市,可以在网站中实现IP属地,发布地等功能。 本文的获取IP地址信息均采自网络上免费的IP查询网站,通过其API或者网页HTML解析出的ip地址信息。 代码 <?p…...
Error: Port Library failed to initialize: -86
最近遇到一个很奇怪的错误,这里记录一下,以备以后再次遇到 Error: Port Library failed to initialize: -86 Error: Could not create the Java Virtual Machine. Error: A fatal exception has occurred. Program will exit.背景是,就是一普…...
SOME/IP 支持两种序列化方式:TLV 和 TV
SOME/IP 是一种基于 IP 的可扩展面向服务的中间件协议,它可以在车载以太网中实现 ECU 之间的高效通信和互操作性。 SOME/IP 的序列化方式是指将数据结构或对象按照一定的规则转换成字节序列的过程,以便在网络中传输和解析。 SOME/IP 支持两种序列化方式:TLV 和 TV。 TLV是…...
Unity之3D物理导航系统
一 介绍 Unity自带寻路(导航)系统是unity官方自带的一种寻路系统。我们可以通过它来制作简单的寻路,比如可以制作点击某个位置,让角色自动的绕开障碍走到目标点的效果,比如可以制作敌人AI,让它可以通过NavMesh绕开障碍追击我方单…...
9.4黄金行情是否反转?今日多空如何布局?
近期有哪些消息面影响黄金走势?今日黄金多空该如何研判? 黄金消息面解析:周一(9月4日)亚市盘中,现货黄金震荡走高,延续上周涨势,一度刷新日内高点至1946.16美元/盎司。周三,ISM将发布服务业P…...
Win10下使用vim9
作为一个经常与文字打交道的Writer,你在学会Vim的基本操作之后,就一定会爱上Vim的。 以下是Windows10_64位(专业版)环境中安装、使用Vim9的全过程,分享一下: 一、下载、安装Vim9 去Vim官网去下载最新的Vi…...
Flink+Flink CDC版本升级的依赖问题总结
之前使用Flink1.13Flink CDC2.0同步MySQL数据,想测试一下最新的几个版本。但是各种依赖冲突的报错,经过一段时间的调试,终于解决,现在总结一下。 1、flink1.15前后jar包名称不一样 flink-streaming-java、flink-clients、flink-…...
Matlab论文插图绘制模板第112期—带阴影标记的图
之前的文章中,分享了Matlab带线标记的图: 进一步,本期分享的是带阴影标记的图。 先来看一下成品效果: 特别提示:本期内容『数据代码』已上传资源群中,加群的朋友请自行下载。有需要的朋友可以关注同名公号…...
专业运动耳机哪个牌子好、专业运动耳机推荐
在进行运动时,倾听音乐实际上是一种放松大脑、放松身体的小技巧。毕竟运动是一个耗费体力最多的活动,整个过程也往往令人感到乏味。如果有音乐作伴,你的运动就会变得更加轻松愉快。那么,哪种耳机适合运动呢?我正好对此…...
【SQL应知应会】索引 • Oracle版:B-树索引;位图索引;函数索引;单列与复合索引;分区索引
欢迎来到爱书不爱输的程序猿的博客, 本博客致力于知识分享,与更多的人进行学习交流 本文免费学习,自发文起3天后,会收录于SQL应知应会专栏,本专栏主要用于记录对于数据库的一些学习,有基础也有进阶,有MySQL也有Oracle 索引 • MySQL版 前言一、Oracle索引1.索引概述及分类…...
用ChatGPT做一个Chrome扩展 | 京东云技术团队
用ChatGPT做了个Chrome Extension 最近科技圈儿最火的话题莫过于ChatGPT了。 最近又发布了GPT-4,发布会上的Demo着实吸睛。 笔记本上手画个网页原型,直接生成网页。网友直呼:前端失业了! 但我觉着啊,真就外行看热闹…...
动态库的制作与使用及 动态库加载失败解决
加载动态库时有时会出现error while loading shared libraries:libcalc.so:可以通过lld命令查看动态库的依赖关系,发现libcalc.so时not found 原因 查找的优先级是DT_RPATH->LD_LIBRARY_PATH->/etc/ld.so.cache->/lib/,/usr/lib 找不到一个优…...
404 not found nginx(dist打包后,刷新和跳转都是404 not found nginx的问题) 解决方案(打包发布在服务器)
当我们执行了yarn run build之后,生成dist文件 我们将代码放入nginx-1.24.0下面的html中 然后我们就配置conf文件下的nginx.conf 配置方面不介绍了,主要问题是因为没有加这句话 问题分析 index index.htm index.html; index 就是根目录,也就…...
《Chain-of-Thought Prompting Elicits Reasoning in Large Language Models》全文翻译
《Chain-of-Thought Prompting Elicits Reasoning in Large Language Models》- Chain-of-Thought Prompting Elicits Reasoning in Large Language Models 论文信息摘要1. 介绍2. 思维链提示3. 算术推理3.1 实验设置3.2 结果3.3 消融研究3.4 思想链的稳健性 4. 常识推理5. 符号…...
MySQL——笔试测试题
解析: 要查询各科目的最大分数,可以使用如下的SQL语句: SELECT coursename, MAX(score) FROM t_stuscore GROUP BY coursename; 这条SQL语句使用了MAX()聚合函数来获取每个科目的最大分数,并使用GROUP BY子句按照科目进行分组…...
WangEditor在Vue前端的应用
1、在Vue项目中安装WangEditor 对于Vue2: npm install wangeditor/editor-for-vue --save 或者 yarn add wangeditor/editor-for-vue 对于Vue3: npm install wangeditor/editor-for-vuenext --save 或者 yarn add wangeditor/editor-for-vuenext 2、将Wa…...
初学python的感受
目录 初学感受学习计划学习目标 初学感受 刚学python的我惊讶的发现编程语言之间竟有如此多的相似之处,因此在学python的时候相对于学C语言时要轻松的多,虽然二者也有一些不同之处,但是我想只要对二者稍微区分的话应该不会搞混的,并且在学习的过程中也可以借鉴学C语言的方法去…...
SpringSecurity中注解讲解
文章目录 1 EnableGlobalMethodSecurity1.1 PreAuthorize1.1.1 开启注解1.1.2 使用注解原生方法1.1.3 使用注解自定义方法 1.2 PostAuthorize1.3 Secured 2 其他注解2.1 PostFilter2.2 PreFilter 3 权限表达式 1 EnableGlobalMethodSecurity EnableGlobalMethodSecurity 是 Sp…...
朔雪流量复制器的前端
朔雪流量复制器的前端 1. 功能需求简介 本流量复制器使用端口映射模式实现流量复制,可以实现一对一,一对多,和多对一的流量复制模式。 映射网口的多少取决于设备的硬件,最多可以支持36端口。 使用模式包括** 1)从1…...
django filter 统计数量 按属性去重
在Django中,如果你想要根据某个属性对查询集进行去重并统计数量,你可以使用values()方法配合annotate()方法来实现。这里有两种常见的方法来完成这个需求: 方法1:使用annotate()和Count 假设你有一个模型Item,并且你想…...
2.Vue编写一个app
1.src中重要的组成 1.1main.ts // 引入createApp用于创建应用 import { createApp } from "vue"; // 引用App根组件 import App from ./App.vue;createApp(App).mount(#app)1.2 App.vue 其中要写三种标签 <template> <!--html--> </template>…...
Vue2 第一节_Vue2上手_插值表达式{{}}_访问数据和修改数据_Vue开发者工具
文章目录 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染2. 插值表达式{{}}3. 访问数据和修改数据4. vue响应式5. Vue开发者工具--方便调试 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染 准备容器引包创建Vue实例 new Vue()指定配置项 ->渲染数据 准备一个容器,例如: …...
GruntJS-前端自动化任务运行器从入门到实战
Grunt 完全指南:从入门到实战 一、Grunt 是什么? Grunt是一个基于 Node.js 的前端自动化任务运行器,主要用于自动化执行项目开发中重复性高的任务,例如文件压缩、代码编译、语法检查、单元测试、文件合并等。通过配置简洁的任务…...
搭建DNS域名解析服务器(正向解析资源文件)
正向解析资源文件 1)准备工作 服务端及客户端都关闭安全软件 [rootlocalhost ~]# systemctl stop firewalld [rootlocalhost ~]# setenforce 0 2)服务端安装软件:bind 1.配置yum源 [rootlocalhost ~]# cat /etc/yum.repos.d/base.repo [Base…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...
云原生周刊:k0s 成为 CNCF 沙箱项目
开源项目推荐 HAMi HAMi(原名 k8s‑vGPU‑scheduler)是一款 CNCF Sandbox 级别的开源 K8s 中间件,通过虚拟化 GPU/NPU 等异构设备并支持内存、计算核心时间片隔离及共享调度,为容器提供统一接口,实现细粒度资源配额…...
大模型——基于Docker+DeepSeek+Dify :搭建企业级本地私有化知识库超详细教程
基于Docker+DeepSeek+Dify :搭建企业级本地私有化知识库超详细教程 下载安装Docker Docker官网:https://www.docker.com/ 自定义Docker安装路径 Docker默认安装在C盘,大小大概2.9G,做这行最忌讳的就是安装软件全装C盘,所以我调整了下安装路径。 新建安装目录:E:\MyS…...
精益数据分析(98/126):电商转化率优化与网站性能的底层逻辑
精益数据分析(98/126):电商转化率优化与网站性能的底层逻辑 在电子商务领域,转化率与网站性能是决定商业成败的核心指标。今天,我们将深入解析不同类型电商平台的转化率基准,探讨页面加载速度对用户行为的…...
mcts蒙特卡洛模拟树思想
您这个观察非常敏锐,而且在很大程度上是正确的!您已经洞察到了MCTS算法在不同阶段的两种不同行为模式。我们来把这个关系理得更清楚一些,您的理解其实离真相只有一步之遥。 您说的“select是在二次选择的时候起作用”,这个观察非…...
