【C++】——多态
一.多态的概念
1.多态
多态(polymorphism)的概念:通俗的来说,就是多种形态。多态分为静态多态(编译时多态)和动态多态(运行时多态),而我们讲的多态大部分都是动态多态。
静态多态主要就是我们前面了解过的函数模板和函数重载,它们传不同的参数就可以调用不同的函数,通过参数不同达到多种形态,这所以叫编译时多态,是因为实参传递给实参的参数匹配过程是在编译时完成的,我们一般把编译时归为静态,运行时归为动态。
运行时多态,简单来说,就是对于同一个函数,不同的对象去调用的会完成不同的行为,以此来达到多种形态。比如买火车票这个行为,普通人买票就是全价;学生买票就买学生票(有优惠);军人买票可以优先买票。
这里先演示一下多态的运行结果:
class person
{
public:virtual void buyticket(){cout << "全价" << endl;}
};class student : public person
{
public:virtual void buyticket(){cout << "半价" << endl;}
};void func(person* p)
{p->buyticket();
}int main()
{person p;student s;func(&p);func(&s);return 0;
}
对这段程序来说,person和student都有买票这个行为,但是不同的对象行为是不同的,我们用一个基类的指针去调用buyticket这个函数,其运行结果会根据对象的不同而不同,当基类指针接收的是一个基类对象的地址时,调用的就是基类的buyticket;当基类指针接受的是一个派生类的对象的地址时,调用的就是派生类的buyticket。
2.虚函数
类的成员函数前面加上virtual来修饰,那么这个成员就被称为虚函数。注意:非成员函数不能被virtual修饰。
class A
{virtual bool max(int a, int b){return a > b;}protected:int _a;int _b;
};
当该类作为基类被派生类继承后,在派生类中该虚函数依旧还是虚函数。
3.多态实现的前提
多态是在继承体系中,基类对象和派生类对象去调用同一函数,产生了不同的行为。
要实现多态的效果必须满足下面的两个条件:
1、必须是基类的指针或者引用调用该成员函数:因为基类的指针或者引用既可以表示基类对象也可以表示派生类对象。
2、被调用的成员函数必须是虚函数,且在派生类中已经完成了重写/覆盖。
3.1虚函数的重写/覆盖
派生类中有一个与基类三同(虚函数返回值、函数名、形参列表完全相同,ps:形参列表相同只要求形参的类型相同,不要求形参名相同)的虚函数,即派生类的虚函数重写了基类的虚函数。
注意:在重写基类的虚函数时,派生类的虚函数加不加virtual都可以(因为继承后基类的虚函数被派生类继承下来了,该函数在派生类中依旧保持虚函数属性),但是这样写并不规范,建议在重写时也加上virtual。
我们现在会看前面给出的那段程序,首先虚函数在派生类中完成了重写,且在调用该函数时是利用基类的指针调用的(引用也可以),不同的是基类的指针指向的对象不同,第一次指向基类对象,第二次指向派生类对象。
多态面试题1
以下程序输出的结果是什么?
A.A->0 B.B->1 C.A->1 D.B->0 E.编译出错 F.以上答案都不对
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;
}
答案是:B
首先创建了一个B类对象的指针,然后调用了test函数,B类对象的指针之所以可以调用test函数,是因为B从A继承了它;然后在test函数中又调用了func函数,要注意的是,这里调用func函数时其实满足了多态的条件,因为在成员函数中,调用其他成员函数时默认有一个this指针。这个this指针就是基类对象的指针,然后func完成了重写,且这里的this指向的是一个B类对象,所以这里应该调用B类的func函数,所以应该打印B->。
那按道理来说应该是B->0,为什么是B->1呢?
注意,重写的本质是重写虚函数的函数体,所以在我们调用重写后的虚函数时,其返回值、函数名、形参表都与基类的相同,不同的只有函数体。所以这里本质上val还是1.
而当我们直接调用B类中的func时,此时与多态无关,val用的就是0.
注意:不要修改重写后的函数缺省值。
4.虚函数重写——协变
先前说,虚函数重写时要求三同。
但是其返回值可以不同,但要求返回值是具有父子关系的类型的指针/引用,这个规则称为协变。
class A{};
class B:public A{};class person
{
public://virtual person* buyticket()virtual A* buyticket(){cout << "成人票" << endl;return nullptr;}
};class student : public person
{
public://virtual student* buyticket()virtual B* buyticket(){cout << "学生票" << endl;return nullptr;}
};
我们既可以直接用person/student类的指针作为返回值,也可以用另一组具有父子关系的类作为返回值。这也是一种协变。协变也满足多态的条件,也可以实现多态。
5.析构函数的重写
基类的析构函数为虚函数,只要派生类定义了析构函数,无论是否加virtual都与基类的析构函数构成重写。虽然这两个析构函数的名字不同,但其实在编译阶段,所有的析构函数的函数名都被处理成了destructor,所以也满足了三同。
所以,只要基类的析构函数是虚函数,派生类只要显式定义析构函数,就构成了重写。
面试题2
为什么基类中的析构函数建议设计成虚函数?
为了避免基类指针指向派生类对象时,调用析构函数不能完全清理派生类对象的全部数据,造成的内存泄露问题。
我们可以借助下面这段程序来理解:
class A
{
public:virtual ~A(){cout << "~A()" << endl;}
};class B :public A
{
public:~B(){cout << "~B()" << endl;}protected:int* _ptr;
};int main()
{A* pa = new A;A* pb = new B;delete pa;delete pb;return 0;
}
当我们分别用基类的指针指向了基类对象和派生类对象时,析构pa时,就是很普通的析构,但是当析构pb时,此时满足了多态,会先调用B的析构函数,然后再调用A的析构函数(派生类析构函数调用结束时会自动调用基类的析构函数),就可以将pb指向的内容全部销毁。
但是如果没有将A类的析构函数定义为虚函数的话,此时析构pb就不会产生多态效果,直接调用了A的析构,但是pb指向的是B类对象,等于它只析构了B类对象中的A类部分,还有B类自己的部分没有析构,这就导致了内存泄漏。
6.override、final
override是C++11中新增的一个关键字,用来检测派生类中的指定函数是否完成了重写。也就是说,用override修饰的函数,必须是基类虚函数的重写,否则就会报错。
我们用override修饰了派生类的这个函数,编译阶段,编译器就会向上搜索,判断这个函数是否为基类的虚函数的重写,如果是,不报错;不是,就报错。
我们看到,这里的函数名写错了,所以不构成重写。
所以,我们可以利用这个关键字来替我们检查是否完成了重写。
如果我们不想这个虚函数被派生类重写,那么我们可以用final来修饰它。
![]()
7.重载/重写/隐藏的对比
这是三个研究的都是同名函数之间的关系。
8.纯虚函数和抽象类
当一个虚函数以=0为结尾时,这个虚函数就是纯虚函数
class A
{
public:virtual void func() = 0{}
};
纯虚函数可以有函数体,也可以没有。
包含纯虚函数的类,叫做抽象类。抽象类不能用来定义对象,但是可以作为基类
如果继承该抽象类的派生类没有重写该纯虚函数的话,该派生类也是一个抽象类。
二.多态的原理
1.虚函数表指针
下面这段程序在32位下运行结果是什么?
A.编译报错 B.运行报错 C.8 D.12
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
答案不是8而是12的原因是,在Base类对象的头部还有一个虚函数表指针__vfptr(注意有些平台可能会放到对象的最后面,这个跟平台有关),这个指针指向一个函数指针数组,该函数指针数组里面存储的都是Base类的虚函数的指针。而在32位下,指针的大小是4字节,加上int和char,在进行内存对齐的话,刚好是12字节。
一个含有虚函数的类中至少都有一个虚函数表指针,因为一个类所有的虚函数的地址都要被放到这个类对象的虚函数表中,虚函数表也简称为虚表。
同一个类的不同对象公用同一个虚表
当派生类没有重写该虚函数时,此时派生类和基类的虚表指针的内容是一样的,但是虚函数表指针不同。
重写后,派生类就会将之前的那个地址给覆盖掉
2.多态的原理
针对下面的程序,在func中当p指向的是person类的对象时就调用的是person的buyticket,p指向的是student类的对象时就调用student的buyticket,这是为什么呢?
我们在前面说了,一个有虚函数的类的对象都会有一个虚函数表指针,里面存放着该类所有虚函数的地址。在满足多态的前提下,在运行时,当p指向的是person对象时,p就会到person类的虚表里面去找对应虚函数的地址,然后去调用;当p指向的是student对象时,p就会到student类的虚表里面去找对应虚函数的地址,然后调用。
class person
{
public:virtual void buyticket(){cout << "person" << endl;}protected:string _name;int _age;
};class student : public person
{
public:virtual void buyticket() override{cout << "student" << endl;}protected:int _id;
};void func(person* p)
{p->buyticket();
}int main()
{person p;student s;func(&p);func(&s);return 0;
}
3.动态绑定和静态绑定
对不满足多态条件(指针/引用+调用虚函数)的函数调用是在编译时绑定的,也就是在编译时确定调用函数的地址,叫做静态绑定
满足多态条件的函数调用是在运行时绑定的,也就是在运行时到指定对象的虚表中找到调用函数的地址,叫做动态绑定。
4.虚函数表
1、基类对象的虚函数表中存放基类所有的虚函数的地址。非虚函数的地址是不会存在里面的。
2、派生类由两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个,它们只是里面存储的地址相同。这就像基类对象和派生类对象中的基类部分。
这里调式窗口看不到B类自己的func4虚函数,可以借助内存窗口观察
3、派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函 数地址
4、派⽣类的虚函数表中包含,基类的虚函数地址,派⽣类重写的虚函数地址,派⽣类⾃⼰的虚函数地 址三个部分。
虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标 记。(这个C++并没有进⾏规定,各个编译器⾃⾏定义的,vs系列编译器会再后⾯放个0x00000000 标记,g++系列编译不会放)
5、虚函数存在哪的?
虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址⼜存到了虚表中
6、虚函数表存在哪里?
为了确认,我们可以写一段程序,用来判断其在那块空间上存储
我们可以将其和每个空间上的变量的地址进行比较,如果存储在同一个空间的话,地址应该比较接近,为了获取虚表指针存储的地址,因为该指针在对象的头部,其大小是四个字节,所以我们可以先将其强转成int*,然后在对其进行解引用,只拿到前四个字节的内容,就可以拿到里面的地址。
class Base { public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func5() { cout << "Base::func5" << endl; } protected:int a = 1; };class Derive : public Base { public:// 重写基类的func1virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func1" << endl; }void func4() { cout << "Derive::func4" << endl; }protected:int b = 2; };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; }
我们对比可以发现,虚表地址和常量区的地址接近,所以可以认为,虚表就存储在常量区中。
多态部分的细节:
1、被virtual修饰的成员函数称为虚函数
2、virtual关键字只在声明时加上,在类外实现时不能加
3、static和virtual是不能同时使用的
4、实现多态是要付出代价的,如虚表,虚表指针等,所以不实现多态就不要有虚函数了
5、抽象类可以定义指针,而且经常这样做,其目的就是用父类指针指向子类从而实现多态
6、基类有几张虚表,派生类就有几张,派生类自己的虚函数不会开一个新的虚表存储自己虚函数的地址,其地址会存放到第一张虚表的末尾。
下面程序的运行结果:
答案 0 1 2;
new B时,会调用B类的构造函数,但是调用之前会先调用基类的构造函数,然后执行test(),此时多态还没有形成,所以调用的就是A类的func(),打印0;
然后到B类的构造函数,执行test(),由于基类已经创建成功,虚表已经存在了,所以此时构成了多态,调用B类的func,打印1;
最后执行p->test();满足多态,++后打印,2;
class A
{
public:A() :m_iVal(0) { test(); }virtual void func() { std::cout << m_iVal << ' '; }void test() { func(); }
public:int m_iVal;
};class B : public A
{
public:B() { test(); }virtual void func(){++m_iVal;std::cout << m_iVal << ' ';}
};int main(int argc, char* argv[])
{A* p = new B;p->test();return 0;
}
完~
相关文章:

【C++】——多态
一.多态的概念 1.多态 多态(polymorphism)的概念:通俗的来说,就是多种形态。多态分为静态多态(编译时多态)和动态多态(运行时多态),而我们讲的多态大部分都是动态多态。 静态多态主要就是我们前面了解过的函数模板和函数重载,它…...

Web前端开发--HTML语言
文章目录 前言1.介绍2.组成3.基本框架4.常见标签4.1双标签4.1.1.标题标签4.2.2段落标签4.1.3文本格式化标签4.1.4超链接标签4.1.5视频标签4.1.6 音频标签 4.2单标签4.2.1换行标签和水平线标签4.2.2 图像标签 5.表单控件结语 前言 生活中处处都有网站,无论你是学习爬…...

AI驱动的网络空间智能对抗;无人集群系统,多体协同算法创新和故障智能预警
目录 AI驱动的网络空间智能对抗 认知与认知域安全 认知攻击-杀伤链 PPDR主动安全框架 短视频内容分析 不良视频鉴别:人工+智能 舆情监测 非介入式监测 大模型对新闻内容审查与播报 无人集群系统,多体协同算法创新和故障智能预警 一、无人集群系统概述 二、多体协…...

推荐一款SSD硬盘优化器:Auslogics SSD Optimizer Pro
SSD Optimizer Pro 是一款专为优化固态硬盘 (SSD) 性能而设计的专业工具,旨在最大化 SSD 的效率,延长硬盘使用寿命。凭借简便的操作界面和强大的优化功能,SSD Optimizer Pro 可以让用户充分利用 SSD 的优势,从而获得更高的系统性能…...

k8s-service、endpoints、pod之间是怎么进行网络互通的
k8s-service、endpoints、pod之间是怎么进行网络互通的 1、service2、endpoints3、service、endpoints、pod通信图4、不通服务pod内部间访问 1、service 在K8S中,Service是一种抽象,定义了一组Pod的逻辑集合和访问这些Pod的策略。首先,我们需…...

Go语言开发商城管理后台-GoFly框架商城插件已发布 需要Go开发商城的朋友可以来看看哦!
温馨提示:我们分享的文章是给需要的人,不需要的人请绕过,文明浏览,误恶语伤人! 前言 虽然现在做商城的需求不多,但有很多项目中带有商城功能,如社区医院系统有上服务套餐、理疗产品需求、宠物…...

【51单片机】UART串口通信原理 + 使用
学习使用的开发板:STC89C52RC/LE52RC 编程软件:Keil5 烧录软件:stc-isp 开发板实图: 文章目录 串口硬件电路UART串口相关寄存器 编码单片机通过串口发送数据电脑通过串口发送数据控制LED灯 串口 串口是一种应用十分广泛的通讯接…...

高性能分布式缓存Redis-高可用部署
一、主从架构搭建 为什么要进行主从架构搭建,一台redis不行吗? ①、持久化后的数据只在一台机器上,因此当硬件发生故障时,比如主板或CPU坏了,这时候无法重启服务器,有什么办法可以保证服务器发生故障时数…...
如何使用XSL-FO生成PDF格式的电子发票的技术博文示例
目录 使用 XSL-FO 生成电子发票 PDF:从布局设计到优化为什么选择 XSL-FO?1. 初始设置2. 标题区块3. 买卖方信息4. 商品明细表格5. 合计信息6. 优化代码结构与布局7. 生成 PDF 文件8. 示例总结 使用 XSL-FO 生成电子发票 PDF:从布局设计到优化…...
TDengine 签约山东港,赋能港口数字化转型
随着全球港口物流数字化进程的加速,港口运营面临日益复杂的数据管理挑战,从能源管理、设备监控到运营安全保障,各类数据需要及时存储并高效分析。山东港在信息化建设过程中,数字化综合管理平台的性能和查询功能一度受到瓶颈制约。…...

基于YOLO11/v10/v8/v5深度学习的煤矿传送带异物检测系统设计与实现【python源码+Pyqt5界面+数据集+训练代码】
《------往期经典推荐------》 一、AI应用软件开发实战专栏【链接】 项目名称项目名称1.【人脸识别与管理系统开发】2.【车牌识别与自动收费管理系统开发】3.【手势识别系统开发】4.【人脸面部活体检测系统开发】5.【图片风格快速迁移软件开发】6.【人脸表表情识别系统】7.【…...

mysql-workbench 导入csv格式数据报错:Unhandled exception: Could not determine delimiter
xlsx文件中第二行某个单元格有换行符,csv文件中用双引号包起来了,但是python 在采样的时候,只读了前两行,readline可不认识csv的规则。csv文件可以识别双引号包起来的换行符是单元格内部的换行,python的readline识别不…...
使用Python简单实现客户端界面
服务端实现 import threading import timeimport wx from socket import socket, AF_INET, SOCK_STREAMclass LServer(wx.Frame):def __init__(self):wx.Frame.__init__(self, None, id1002, titleL服务器端界面, poswx.DefaultPosition, size(400, 450))# 窗口中添加面板pl …...
15分钟学 Go 第 43 天:前端与Go的结合
第43天:前端与Go的结合 目标:了解Go如何与前端交互,前端使用Vue.js 在现代Web开发中,Go语言常用于后端开发,而Vue.js是一个流行的前端框架,用于构建用户界面。结合二者,可以构建高效、可维护的…...
解决SRS推送webrtc流卡顿问题
目录 1.问题描述2.原因分析3.ffmpeg去掉B帧的方法3.1 命令行推流3.2 ffmpeg源码推流 1.问题描述 使用ffmpeg通过rtmp协议推流给SRS,然后浏览器通过webrtc拉取播放流,经多次测试发现webrtc播放流总是卡顿,而拉取rtmp流是正常的。 2.原因分析…...

GDPU Andriod移动应用 Broadcast Receiver
聆听广播,跟着节拍吧。 计时器 新建一个名为PhoneStateMonitor的工程; 实现一个应用运行时长的计时器,并在界面上刷新计数器,要求包括: (1)在Layout中包含两个TextView控件,横向分…...

CSP/信奥赛C++刷题训练:经典例题 - 栈(1):洛谷P3056 :[USACO12NOV] Clumsy Cows S
CSP/信奥赛C刷题训练:经典例题 - 栈(1):洛谷P3056 :[USACO12NOV] Clumsy Cows S 题目描述 Bessie the cow is trying to type a balanced string of parentheses into her new laptop, but she is sufficiently clums…...

WiFi一直获取不到IP地址是怎么回事?
在当今这个信息化时代,WiFi已成为我们日常生活中不可或缺的一部分。无论是家庭、办公室还是公共场所,WiFi都为我们提供了便捷的无线互联网接入。然而,有时我们可能会遇到WiFi连接后无法获取IP地址的问题,这不仅影响了我们的网络使…...
蓝牙BLE开发——iOS 每次写入数据超过200字节报错?
iOS 写入数据超过200字节报错 文章目录 iOS 写入数据超过200字节报错官方建议:报错问题解决 writeblecharacteristicvalue 官方建议: 并行调用多次会存在写失败的可能性。APP不会对写入数据包大小做限制,但系统与蓝牙设备会限制蓝牙4.0单次…...

Ascend Extension for PyTorch是个what?
1 Ascend Extension for PyTorch Ascend Extension for PyTorch 插件是基于昇腾的深度学习适配框架,使昇腾NPU可以支持PyTorch框架,为PyTorch框架的使用者提供昇腾AI处理器的超强算力。 项目源码地址请参见Ascend/Pytorch。 昇腾为基于昇腾处理器和软…...

手游刚开服就被攻击怎么办?如何防御DDoS?
开服初期是手游最脆弱的阶段,极易成为DDoS攻击的目标。一旦遭遇攻击,可能导致服务器瘫痪、玩家流失,甚至造成巨大经济损失。本文为开发者提供一套简洁有效的应急与防御方案,帮助快速应对并构建长期防护体系。 一、遭遇攻击的紧急应…...

【OSG学习笔记】Day 18: 碰撞检测与物理交互
物理引擎(Physics Engine) 物理引擎 是一种通过计算机模拟物理规律(如力学、碰撞、重力、流体动力学等)的软件工具或库。 它的核心目标是在虚拟环境中逼真地模拟物体的运动和交互,广泛应用于 游戏开发、动画制作、虚…...

工业安全零事故的智能守护者:一体化AI智能安防平台
前言: 通过AI视觉技术,为船厂提供全面的安全监控解决方案,涵盖交通违规检测、起重机轨道安全、非法入侵检测、盗窃防范、安全规范执行监控等多个方面,能够实现对应负责人反馈机制,并最终实现数据的统计报表。提升船厂…...
在四层代理中还原真实客户端ngx_stream_realip_module
一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡(如 HAProxy、AWS NLB、阿里 SLB)发起上游连接时,将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后,ngx_stream_realip_module 从中提取原始信息…...

srs linux
下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935,SRS管理页面端口是8080,可…...
C++八股 —— 单例模式
文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全(Thread Safety) 线程安全是指在多线程环境下,某个函数、类或代码片段能够被多个线程同时调用时,仍能保证数据的一致性和逻辑的正确性…...

Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

mac:大模型系列测试
0 MAC 前几天经过学生优惠以及国补17K入手了mac studio,然后这两天亲自测试其模型行运用能力如何,是否支持微调、推理速度等能力。下面进入正文。 1 mac 与 unsloth 按照下面的进行安装以及测试,是可以跑通文章里面的代码。训练速度也是很快的。 注意…...

leetcode73-矩阵置零
leetcode 73 思路 记录 0 元素的位置:遍历整个矩阵,找出所有值为 0 的元素,并将它们的坐标记录在数组zeroPosition中置零操作:遍历记录的所有 0 元素位置,将每个位置对应的行和列的所有元素置为 0 具体步骤 初始化…...

【图片转AR场景】Tripo + Blender + Kivicube 实现图片转 AR 建模
总览 1.将 2D 图片转为立体建模 2. 3. 一、将 2D 图片转为立体建模 1.工具介绍 Tripo 网站 2.找图片 找的图片必须是看起来能够让 AI 有能力识别和推理的,因为现在的AI虽然可以补全但是能力还没有像人的想象力那么丰富。 比如上面这张图片,看起来虽…...