【C++】在编译期沉默,在运行期发声:多态的一生
浮世尘弦个人主页⭐个人专栏《C语言》、《数据结构与算法》、《C》非淡薄无以明志非宁静无以致远前言学完继承很多人会觉得自己已经摸到了 C 面向对象的门槛。直到遇见多态。多态值得单独写一篇不只是因为它被面试反复提起更因为它是继承和虚函数真正活过来的地方。在这里你会撞见一些看上去自相矛盾的现象同样是virtual写进构造函数就失去意义基类析构函数少写一个virtual程序可能在退出时悄悄出错还有三个长得几乎一样的词——重载、重写、隐藏——它们决定了那次调用究竟落到谁身上。这篇会从虚函数讲起顺着重写规则、多态成立的两个条件、析构函数的重写一路走到纯虚函数和抽象类最后一章把视角沉进内存看看虚函数表指针与虚函数表做了什么。我想让你读完能自己回答一个问题在编译器沉默的那段时间里它究竟把什么决定留给了运行期。让我们开始今天的学习吧目录前言一多态的定义和实现1认识虚函数2虚函数的重写/覆盖3多态(动态)的两个条件⭐4多态练习题易错5,协变了解二析构函数的重写⭐1重写析构函数2override和final关键字3重载重写隐藏的对比三纯虚函数和抽象类四多态的原理⭐1,虚函数表指针2多态的原理3动态绑定和静态绑定4虚函数表结尾一多态的定义和实现1认识虚函数在认识多态前我们先要知道虚函数以及虚函数重写。虚函数类成员函数前面加virtual修饰那么这个成员函数被称为虚函数。注意非成员函数不能加virtual修饰。核心功能实现运行时多态。声明虚函数和虚继承使用的同样的关键字但是两者没有什么关系。2虚函数的重写/覆盖虚函数的重写/覆盖派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表类型和个数完全相同(三同))称派⽣类的虚函数重写了基类的虚函数。注意点只要基类写了virtual即使子类不写也是虚函数。一般建议写上3多态(动态)的两个条件⭐先简单了解一下多态的概念多态就是一个继承关系下的类对象调用同一个接口函数名在对象上表现出不同的行为。通俗来说就是“一个接口多种实现”。多态分为两大类编译时多态(静态多态)和运行时多态(动态多态。编译时多态(静态多态)就是函数模版和函数重载。特点在编译期间就已经确定了具体调用哪个函数。效率极高因为地址在编译期就绑定好了静态绑定运行时多态(动态多态)就是虚函数继承特点在运行期间根据指针/引用所指向的实际对象类型来决定调用哪个类的函数动态绑定。效率比静态多态稍慢因为需要在运行时查表虚函数表接下来结合一个代码样例来了解一下多态吧class Person { public: virtual void BuyTicket() { cout 旅游-全价 endl; } }; class Student :public Person { public: void BuyTicket()//子类不加由于继承也是虚函数 { cout 旅游-半价 endl; } }; //这里使用Person是因为只有父类指针才能同时接收父类和子类 void Func(Person ptr) { ptr.BuyTicket();//实际调用时和类型无关和ptr指向的对象有关 } int main() { Person p1; Student s1; Func(p1); Func(s1); return 0; }运行结果上面一个继承关系下的类对象子类继承父类的虚函数。通过父类对象的指针调用了不同对象中的虚函数。由此我们引出构成多态的两个条件1,必须是基类的指针或者引用调用了虚函数。原因只有基类的指针和引用才能即指向基类对象又指向派生类对象。2派生类必须完成了对父类虚函数的重写。原因只有完成了重写才能有不同的函数形态用来进行调用。4多态练习题易错下面这段代码的运行结果是什么A-0 B: B-1 C: A-1 D: B-0 E: 编译出错 F: 以上都不正确分析这里B完成了对test()函数的继承但是没有完成对其的重写。B中实际继承的A::test()因此p-test会去调用父类的test()在里面发现func()函数就根据this的类型A*去查找在A中找到了func()。发现func()实际是一个虚函数实际的对象是B。this-func()就构成了多态。去B中找到func()进行调用。最后一个坑子类虚函数的默认参数取决于“调用时指针/引用的静态类型”。这里编译器在编译A::test()时只知道A::func的默认参数是1。所以不会用到下面的参数。最终打印的结果就是B-1直接进行调用时B::func()或者p-func()这种场景下才会使用val 0,打印出B-0C 标准委员会早就发现了这个坑所以极力推荐绝对不要在子类重写虚函数时重新定义默认参数结果B:B-15,协变了解派⽣类重写基类的虚函数时与基类的虚函数的返回值类型不同。即基类虚函数返回基类对象的指针或者引用不一定是自己的派⽣类虚函数返回派⽣类对象的指针或者引用时称为协变。协变也是多态。////协变 class A{}; class B : public A {}; class Person { public: //virtual A* BuyTicket() //可以不是自己的 virtual Person* BuyTicket() { cout 旅游-原价 endl; return nullptr; } }; class Student : public Person { public: //virtual B* BuyTicket() virtual Student* BuyTicket() { cout 旅游-半价 endl; return nullptr; } }; void Func2(Person* p1) { p1-BuyTicket(); } int main() { Person p1; Student s1; Func2(p1); Func2(s1); return 0; }运行结果二析构函数的重写⭐1重写析构函数只要基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加virtual关键字都与基类的析构函数构成重写。虽然看起来名字不同不能构成重写但是实际上编译器对析构函数的名称做了特殊处理编译后析构函数的名称统⼀处理成destructor//析构函数的重写 class A { public: virtual ~A() { cout ~A() endl; } }; class B : public A { public: //名字被处理成destructor,构成隐藏 ~B() //不加virtual,名字不同也构成虚函数重写 { cout ~B() p1 endl;//子类会自动调用父类的析构 delete p1; } protected: int* p1 new int[10]; }; int main() { A* p1 new A; A* p2 new B; delete p1; //p2-dectructor()operator delete //先调用子类的析构后调用父类的析构 delete p2; return 0; }运行结果为什么基类中的析构函数建议设计为虚函数因为要构成多态如果基类指针指向子类对象如p2当delete p2时如果基类析构不是虚函数就只会调用基类的析构导致子类申请的额外资源无法被释放从而造成内存泄漏。所以这里析构的名字被处理成了destrctor构成了多态关系 同时基类不加virtual两者构成了隐藏关系。2override和final关键字override关键字上面的虚函数重写要三个相同如果写错了就无法构成重写这种错误无法在编译期间报错。在运行时得不到想要的结果时才会进行更正。因此C11提供了override关键字可以帮助我们检查是否构成了重写。final关键字如果不想让子类重写基类的虚函数可以在基类当中的虚函数后加上一个final演示一下3重载重写隐藏的对比三纯虚函数和抽象类纯虚函数的定义在虚函数的声明后面写上 0这个函数就成为了纯虚函数。纯虚函数的实现纯虚函数通常不需要定义实现因为它的意义在于被派生类重写父类给出实现通常没有实际意义只要声明即可。不过C 语法上是允许纯虚函数拥有实现的。抽象类的定义包含纯虚函数的类被称为抽象类。抽象类的限制抽象类不能实例化出对象。派生类的约束如果一个派生类继承自抽象类但没有重写父类的纯虚函数那么这个派生类依然也是抽象类依然无法实例化。设计意义纯虚函数某种程度上强制了派生类必须重写该虚函数因为不重写就无法实例化出对象。代码演示//纯虚函数和抽象类 class fruit //抽象类 { public: virtual void Diver() 0; //纯虚函数声明即可 }; class apple : public fruit { public: virtual void Diver()//重写纯虚函数不属于抽象类 { cout apple-sweet endl; } }; class orange : public fruit { public: virtual void Diver() { cout orange-sour endl; } }; int main() { //父类不能实例化出对象但是可以使用父类的指针和引用 fruit* a1 new apple; fruit* o1 new orange; a1-Diver(); o1-Diver(); return 0; }运行结果四多态的原理✨1,虚函数表指针先来看一个例题下面这段代码在32位下的运行结果是什么A8 B12 C编译报错 D运行报错分析运行结果为12字节明明只有两个变量应该为 8个字节才对这是为什么呢打开监视窗口就可以发现端倪这里的b有三个部分构成除去两个成员新增加了一个_vfptr放在对象的前面这是一个指针32位下刚好是4个字节因此结果为12字节称为虚函数表指针。一个含有虚函数的类中都至少含有一个虚函数表指针。因为一个类当中的虚函数的地址都要放到一个函数指针数组当中这个函数指针数组就是虚函数表虚函数表又可以称为虚表。结果B2多态的原理下面结合一组案例来理解多态的原理class Person { public: virtual void BuyTicket() { cout 旅游-全价 endl; } string _name; }; class Student :public Person { public: virtual void BuyTicket()//子类不加由于继承也是虚函数 { cout 旅游-半价 endl; } int id; }; class OldMan : public Person { public: virtual void BuyTicket() { cout 旅游-优先 endl; } int age; }; void Func3(Person* p1) { p1-BuyTicket(); } int main() { Person* p new Person; Person* s new Student; Person* o new OldMan; Func3(p); Func3(s); Func3(o); return 0; }从底层调用角度Func3()函数中的p1-BuyTicket()是如何作为p1指向Person对象调用Person::BuyTicket()?p1是如何调用Student::BuyTicket的呢可以通过下图得知满足多态条件后编译器不再是编译时通过调用对象确定函数地址而是运行时去调用对象的虚表中确定虚函数的地址。这样就实现了指针或者引用指向基类的对象就调用基类的虚函数指向派生类就调用派生类的虚函数。3动态绑定和静态绑定静态绑定不满足多态条件的函数调用是在编译时绑定的也就是编译期确定调用函数的地址动态绑定满足多态条件的函数调用是在运行时绑定的也就是在运行时到指向对象的虚函数表中确定调用函数的地址下面从汇编层面了解一下两种绑定的差异从上图可知静态绑定只需要少量指令运行速度比动态绑定要快的多。4虚函数表1基类对象的虚函数表中存着基类所有虚函数的地址。同类型的对象共用一张虚表不同类型的对象各用各的虚表。基类和子类各用各的虚表。代码测试class Base { public: virtual void Func1() { cout Func1 endl; } virtual void Func2() { cout Func2 endl; } void Func3() { cout Func3 endl; } protected: int a 1; }; int main() { Base b1; Base b2; return 0; }监视窗口2派生类有两部分构成继承父类的部分和自己成员部分。一般只要父类当中有虚表指针那么子类继承下来的就不需要再生成虚表指针。注意这个继承下来的虚表指针和父类当中的虚表指针不同。3子类重写父类的虚函数那么子类中从父类继承下来的的虚表中对应的虚函数地址也要更换成重写函数的地址。4派生类的虚表中的函数地址可以分成三部分(1)父类的虚函数地址。(2)子类重写覆盖父类的虚函数地址。(3)子类自己的虚函数地址。5虚函数存放在哪里虚函数代码存放的位置和普通函数一样编译后都是一段指令都存在代码段中。6虚函数表指针(vptr)存放对象的内存空间中栈堆或全局区。下面这段代码可以检测出存放的位置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; /*cout 和上面的各个区域的地址进行对比,找到接近的位于同区域 endl;*/ printf(Person虚表地址:%p\n, *(int*)p3); printf(Student虚表地址:%p\n, *(int*)p4); //成员函数取地址加上一个 printf(虚函数地址:%p\n, Base::Func1);//代码段 printf(普通函数地址:%p\n, Base::Func3); return 0; }结尾本篇文章中所涉及到的代码gitee链接多态以上就是本文的全部内容了如果对你有所帮助的话请大佬不妨给博主来个“一键三连”这对我是大大的支持也能让博主产出更优质的内容。往期回顾【C】从基类到派生类关于C继承的务实梳理总结本文系统讲解C多态机制涵盖虚函数、虚函数重写、多态的两个条件基类指针/引用调用虚函数、派生类重写基类虚函数并通过代码实例演示多态实现原理。深入剖析虚函数表vtable与虚函数表指针vptr的作用阐明动态绑定与静态绑定的区别强调析构函数设为虚函数的重要性以避免内存泄漏。同时介绍协变、纯虚函数、抽象类、override与final关键字等进阶特性帮助读者全面掌握运行时多态的核心机制。⚡把上面的内容吃透就休息一下吧⚡

相关新闻

Ace前台框架完整版下载与静态工程化实践指南

Ace前台框架完整版下载与静态工程化实践指南

简介:本资源为ACE前端框架完整版开发套件,面向Web前端开发者及中高级前端学习者,旨在提供开箱即用的现代化UI解决方案,显著降低响应式管理后台、数据看板等B端应用的搭建门槛。压缩包共3353个文件,涵盖2223个JS脚本&am…

2026/10/14 19:00:15 阅读更多 →
S19文件填充、格式互转与CRC校验一站式工具详解

S19文件填充、格式互转与CRC校验一站式工具详解

简介:这份资源是Vector官方HexView工具的完整安装包,面向嵌入式开发与单片机编程人员,用于处理S19、HEX、BIN三类常见固件文件。工具支持格式互相转换、数据填充、CRC校验和地址偏移,适合在固件更新、调试分析、适应不同烧录器或项…

2026/10/14 18:59:14 阅读更多 →
合成数据能否持续带来收益:质量、覆盖度与分布退化

合成数据能否持续带来收益:质量、覆盖度与分布退化

合成数据能否持续带来收益:质量、覆盖度与分布退化 系列:大模型研究争议:从论文结论到证据边界日期:2026-10-13 摘要:数据更多,为什么仍可能越学越窄? 模型能不断生成新题目、新答案&#xff…

2026/10/14 18:59:14 阅读更多 →

最新新闻

304与316不锈钢紧固件怎么选?成分差异、耐腐蚀对比与采购验收全解

304与316不锈钢紧固件怎么选?成分差异、耐腐蚀对比与采购验收全解

看到这个标题,我猜很多人第一反应是"不就是螺丝嘛,304还是316,哪个贵买哪个"。但实际接触过工业采购、设备维护或者出海项目的人,都知道这个选择经常让人头疼——选贵了,成本上去了;选错了&#…

2026/10/14 23:48:05 阅读更多 →
揭秘AI生成视频的真相:代码驱动Canvas逐帧渲染

揭秘AI生成视频的真相:代码驱动Canvas逐帧渲染

朋友给我发来一段很特别的视频,说这是某个叫 Opus 5.5 的AI工具“生成”的。我盯了几秒钟,第一反应是不对劲——画面太干净了。没有常见视频生成模型那种隐隐约约的噪点,边缘稳定,线条利落,几何结构严丝合缝&#xff0…

2026/10/14 23:48:05 阅读更多 →
本2硕9小厂975写SQL怕被AI替,拿到老家国企早9晚5Offer,该求稳吗?

本2硕9小厂975写SQL怕被AI替,拿到老家国企早9晚5Offer,该求稳吗?

本2硕9小厂975写SQL怕被AI替,拿到老家国企早9晚5Offer,该求稳吗? 本2硕9高学历与在小厂975写SQL的低技术现状形成反差,该开发者在脉脉表达了被AI替代的焦虑,手握十八线老家早9晚5国企Offer陷入去留困境。原帖用户讨论称…

2026/10/14 23:48:05 阅读更多 →
Mask R-CNN在显微矿物图像实例分割中的落地实践

Mask R-CNN在显微矿物图像实例分割中的落地实践

简介:本资源是一套基于TensorFlow 2.x与Keras实现的Mask R-CNN显微矿物图像实例分割完整项目,面向深度学习初学者、地质图像分析研究者及计算机视觉实践者,解决矿物显微图像中目标检测与像素级分割的实际问题,适用于地质勘探、矿产…

2026/10/14 23:48:05 阅读更多 →
ISACA数字化转型风险指南:治理与管理域框架及落地实操

ISACA数字化转型风险指南:治理与管理域框架及落地实操

简介:《风险视角下中国企业数字化转型应对指南》是ISACA中国2020年发布的知识性研究成果,面向企业管理者、风控与安全从业者及数字化转型相关人员,旨在提供识别、评估和应对数字IT风险的可行方法。指南分为“框架和指导原则”与“与ISACA资源…

2026/10/14 23:48:05 阅读更多 →
行政区划代码SQL脚本:GB/T 2260维度表设计与查询实践

行政区划代码SQL脚本:GB/T 2260维度表设计与查询实践

简介:依据GB/T 2260标准整理的全国行政区划代码资源,内含截至2017年的四级行政区划数据共46462条,覆盖省/直辖市、市、县与乡/镇/街道/兵团,适合开发者、数据分析师及GIS项目人员用于建表存储、地址匹配与区域维度分析。压缩包共4…

2026/10/14 23:47:02 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →