首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
【 C++ 多态】虚函数、重写、虚析构、抽象类与虚表原理
📅 2026/10/27 18:58:29
✍️ 爱科研究院
👁 阅读 3,247
C 多态虚函数、重写、虚析构、抽象类与虚表原理哈喽大家好面向对象系列上一篇讲完了继承这篇接着啃面向对象三大特性里最核心的一块——多态。本篇从什么是多态讲起落到实现多态的两个条件、虚函数与重写、默认参数的坑、析构函数为什么要设成虚函数、override/final、纯虚函数与抽象类最后再钻进内存看看虚表指针和动态绑定到底怎么运转。口语化讲解代码均可直接编译放心食用~目录一、先分清静态多态与动态多态二、运行时多态同一个动作不同对象不同表现三、实现多态的两个条件四、虚函数用 virtual 标记出来的函数五、重写覆盖虚函数在子类里重新实现5.1 重写要满足什么条件5.2 重写和继承里的隐藏有什么区别5.3 派生类里可以省略 virtual六、完整多态演示买票七、专题绝不在虚函数重写时使用默认参数八、为什么基类析构函数一定要设为虚函数九、C11 两个关键字override 与 final十、纯虚函数与抽象类十一、多态底层原理虚表指针、虚表与绑定11.1 对象内存布局里多了一个 vfptr11.2 sizeof 为什么是 16 字节11.3 虚表到底存在哪里11.4 静态绑定与动态绑定十二、总结一、先分清静态多态与动态多态多态字面意思就是多种形态。C 里多态分两大类1. 静态多态编译时多态就是我们之前学过的函数重载和函数模板传不同类型、不同个数的参数就调用到不同的函数版本。参数与函数的匹配是在编译期完成的编译完调用关系就定死了所以归为静态。2. 动态多态运行时多态到了运行阶段传不同的对象就表现出不同的行为形成多种形态。这才是本篇要讲的主角下文提到多态默认都指运行时多态。区分要点静态多态靠参数匹配编译期定死动态多态靠对象类型运行期才决定调谁。二、运行时多态同一个行为不同对象不同表现生活里到处都是多态买票学生优惠、普通人全价、军人优先购票——都是买票这个动作身份不同结果不同动物叫小狗汪汪、小猫喵喵、小鸭嘎嘎——都是叫物种不同叫声不同。放到代码里多态是指一组存在继承关系的类对象调用同一个函数却产生了不同的行为。比如 Student 继承 PersonPerson 对象买票全价Student 对象优惠买票——同一个BuyTicket对象不同表现不同。三、实现多态的两个条件实现动态多态的本质就是调用虚函数。要跑出多态效果必须同时满足两个条件必须用基类的指针或引用去调用虚函数被调用的虚函数在派生类中完成了重写覆盖。为什么第一条要求必须是基类指针或引用还是上篇讲的切片原理子类能赋值给父类父类不能赋值给子类。只有基类指针/引用才能既指向基类对象、又指向派生类对象从而在同一条调用路径上接纳不同对象反过来用派生类指针指向父类对象是不成立的。从解释中也能看出来如果仅仅使用子类的指针或引用去调用该子类中的虚函数是能行的通的但是为了能一键接受不同对象必须使用基类指针或引用这个规则其实是我的私活这也是应用时的推荐写法。使用子类的指针或引用去调用该子类的虚函数书写十分难受简直是丧失了多态的意义。为什么第二条要求重写派生类必须对基类虚函数给出自己的实现基类和派生类之间才有不同的函数体不同形态的效果才出得来。两个条件缺一不可用基类指针但没重写调的还是基类函数重写了但没用基类指针/引用触发不了动态绑定。四、虚函数用 virtual 标记出来的函数在类成员函数前面加virtual修饰这个函数就叫虚函数classPerson{public:virtualvoidBuyTicket(){cout买票-全价endl;}};注意virtual只能修饰类的普通成员函数。下面这些都不能加virtual全局普通函数静态成员函数静态函数不属于对象没有 this 指针无法进虚表友元函数友元不是成员函数构造函数对象还没构造完虚表指针尚未建立。析构函数可以而且常常需要是虚函数这在第八节专门讲。五、重写覆盖虚函数在子类里重新实现5.1 重写要满足什么条件重写也叫覆盖override继承关系中派生类对基类的某个虚函数给出全新的实现。重写要求派生类虚函数与基类虚函数的下列要素一致函数名相同参数列表相同const属性相同是否 const 成员函数要一致返回值类型一般相同。函数体功能不同这就是重写。5.2 重写和继承里的隐藏有什么区别上篇讲过隐藏派生类与基类出现同名成员函数时派生类函数自动屏蔽基类同名函数。它和重写是两回事对比项隐藏name hiding重写override适用函数普通成员函数即可不要求是虚函数必须是基类虚函数匹配要求只看函数名相同就屏蔽参数不同也隐藏要求函数名、参数、const 全部一致意图编译器的名称遮蔽规则避免意外重载干扰开发者有意识地为不同对象提供不同实现调用方式调被隐藏的基类函数要显式写基类::函数名靠基类指针/引用 虚表动态绑定自动调对应版本工程上如果靠显式写类域去调被隐藏的函数代码会又难写又难维护基类指针很多场景下还不支持所以上篇才建议尽量不要在派生类里定义和基类同名的非虚函数。而多态要的同名是建立在虚函数和重写之上的、主动设计的同名。5.3 派生类里可以省略 virtual重写时派生类的虚函数不加virtual关键字也算重写——virtual属性会随基类成员函数一起继承下来视为函数声明的一部分。反过来基类不加virtual就构不成虚函数自然谈不上重写。不过规范写法仍然建议派生类和基类都写上virtual再配合下一节的override关键字一眼能看出这是重写关系。六、完整多态演示买票下面用 Person、Student、Soldier 三个类演示多态基类指针分别指向不同对象调用同一个BuyTicket各自表现不同。#includeiostreamusingnamespacestd;classPerson{public:virtualvoidBuyTicket(){cout买票-全价endl;}};classStudent:publicPerson{public:virtualvoidBuyTicket(){cout买票-优惠价endl;}};classSoldier:publicPerson{public:virtualvoidBuyTicket(){cout买票-优先购票endl;}};intmain(){Person*p;Student s;Soldier so;ps;p-BuyTicket();// 指向学生优惠价pso;p-BuyTicket();// 指向军人优先购票Personrefs;// 引用同样能触发多态ref.BuyTicket();return0;}运行结果买票-优惠价 买票-优先购票 买票-优惠价同一条p-BuyTicket()p 指向谁就调谁的版本这就是运行时多态。七、专题绝不在虚函数重写时使用默认参数先看一个经典示例#includeiostreamusingnamespacestd;classA{public:virtualvoidfunc(intval1){std::coutA-valstd::endl;}virtualvoidtest(){func();}};classB:publicA{public:voidfunc(intval0)override{std::coutB-valstd::endl;}};intmain(){B*pnewB;p-test();deletep;return0;}按直觉分析p 指向 B 对象test()自己没写、继承自 Atest()内部调func()func()被 B 重写了应该走 B 的版本B 的默认参数是 0于是输出B-0。但是下面是运行结果答案是B-0。—图 1运行结果实际运行结果是B-1。 为什么虚函数的声明部分是继承自基类函数也就是声明和基类函数一模一样。继承首要的就是把声明部分全搬过来然后有实现就搬实现是虚函数就自己实现但是声明部分不变还是搬过来而不是自己实现那搬的什么呢 virtual void func(int val 1) 这是基类A中func的全部声明B类中结合上我们的实现B中的func函数到底是什么呢virtualvoidfunc(intval1){std::coutB-valstd::endl;}这才是B的func函数的真身所以答案是B-1 。所以函数体跟着对象走动态缺省参数跟着基类定义类型走静态两者错开才出现B 的函数体、A 的默认值这种割裂结果。**结论绝不在虚函数重写时给缺省默认参数。给也要给相同值。八、为什么基类析构函数一定要设为虚函数8.1 父子析构函数的关系上篇讲过基类和派生类的析构函数构成隐藏关系底层编译器把两个析构函数的名字统一处理成destructor名字相同在名称查找上构成隐藏。如果基类析构不是虚函数就构不成多态用基类指针delete一个派生类对象时只能调用到基类析构派生类析构被跳过派生类里申请的资源就没人释放造成内存泄漏。8.2 基类析构设为 virtual 后的规则当基类析构函数是虚函数时派生类析构函数只要定义出来无论有没有写virtual、也无论两者名字不同都与基类析构构成重写。虽然~A()和~B()看起来名字不一样但编译器对析构函数名字做了特殊处理编译后统一成destructor所以仍然满足重写规则。8.3 正确写法与反例对比正确写法基类析构加 virtual#includeiostreamusingnamespacestd;classA{public:virtual~A(){cout~A()endl;}};classB:publicA{public:~B()override{cout~B()-delete[] _pendl;delete[]_p;}protected:int*_pnewint[10];};intmain(){A*p1newA;A*p2newB;deletep1;// 只调 ~A()deletep2;// 多态析构先调 ~B() 释放资源再调 ~A()return0;}delete p2时构成多态先正确调用派生类~B()释放数组资源再调用基类~A()顺序为先子后父。反例把virtual去掉classA{public:~A(){cout~A()endl;}// 不是虚函数};此时A* p2 new B; delete p2;只会调用~A()~B()被跳过_p指向的数组得不到释放直接泄漏。工程准则只要一个类可能被当作基类、并且派生类自己申请了资源就把基类析构函数声明为virtual。九、C11 两个关键字override 与 finalC11 提供了两个辅助重写与继承控制的关键字。9.1 override帮你检查是否真的重写成功C 对虚函数重写的签名要求很严格但手写时偶尔会因为笔误参数写错、const 漏写、名字拼错以为自己重写了实际只是形成了隐藏编译器又不一定报错。在派生类函数后面加override编译器就会去检查这个函数是否真的重写了基类的某个虚函数签名对不上就直接报错classBase{public:virtualvoidShow(){}};classDerived:publicBase{public:voidShow()override{}// 确认重写 Base::Show签名写错这里就会报错};9.2 final禁止继续重写或禁止被继承final有两种用法修饰虚函数派生类不能再重写这个虚函数修饰类该类成为最终类不能被继承上一篇继承讲过。classBase{public:virtualvoidShow(){}virtualvoidHelper()final{}// 派生类不能再重写 Helper};classDerivedfinal:publicBase// Derived 不能再被别的类继承{public:voidShow()override{}};小提示final写在类名后面、override写在函数参数列表后面位置别搞混。十、纯虚函数与抽象类10.1 纯虚函数与抽象类的规则在虚函数后面加 0这个函数就叫纯虚函数。纯虚函数可以只声明、不给出实现基类确实没必要实现它。包含纯虚函数的类叫抽象类抽象类不允许被实例化派生类继承后如果没有重写这个纯虚函数那么派生类仍然是抽象类照样不能实例化纯虚函数相当于强制派生类去重写——不重写就造不出对象。10.2 完整示例车辆#includeiostreamusingnamespacestd;classCar{public:virtualvoidDrive()0;// 纯虚函数};classBenz:publicCar{public:voidDrive()override{coutBenz-舒适endl;}};classTesla:publicCar{public:voidDrive()override{coutTesla-操控endl;}};intmain(){// Car car; // error C2259: 无法实例化抽象类 CarCar*pBenznewBenz;pBenz-Drive();// Benz-舒适Car*pTeslanewTesla;pTesla-Drive();// Tesla-操控deletepBenz;deletepTesla;return0;}Car作为抽象类只负责定义车都能开这个接口规范具体怎么开由 Benz、Tesla 各自实现。这正是接口设计思路的基础。十一、多态底层原理虚表指针、虚表与绑定前面是使用层面的多态掌握前面的知识使用多态就不会有太大问题了但多态的内存也十分吸引人现在我们开始看内存看看运行时凭什么能对象不同调不同版本。11.1 对象内存布局里多了一个 vfptr#includeiostreamusingnamespacestd;classA{public:virtualvoidfunc(){}protected:int_num11;char_ch1c;};intmain(){A a;coutsizeof(a)endl;return0;}一个类只要含有虚函数对象里就会多出一个成员虚函数表指针v 代表 virtualf 代表 function简称 vfptr。它指向一张虚函数表虚表vtable——本质是一个函数指针数组存放该类各个虚函数的地址。分清两个概念vfptr属于对象是对象内存里的一个指针vtable虚表属于类是一张函数指针数组同一个类的所有对象共享这一张表。11.2 sizeof 为什么是 16 字节在 64 位环境下A对象的布局大致是成员字节vfptr 虚表指针8 字节_num1int4 字节_ch1char1 字节对齐填充3 字节合计16 字节_num1和_ch1按对齐规则凑成 8 字节加上 8 字节的 vfptr正好 16。如果只看普通成员怎么对齐都凑不到 16多出来的 8 字节正是这个 vfptr。补充vfptr 在对象中的位置与平台有关多数平台放在对象最前面有些平台放在最后面32 位环境下指针只有 4 字节算出来的sizeof会不一样以实际调试窗口为准。11.3 虚表到底存在哪里虚表不属于某个对象而是类级别的一份静态数据存放在程序的只读常量区.rdata属于全局静态存储区的只读部分。同一个类的所有对象vfptr 都指向这同一张虚表派生类重写虚函数后自己的虚表中对应位置存放重写后的函数地址。11.4 静态绑定与动态绑定把第一节和底层原理串起来静态绑定编译期绑定函数调用在编译阶段就确定好普通函数、函数重载、普通成员函数以及前面讲的默认参数都属于这一类动态绑定运行期绑定通过对象内的 vfptr 找到虚表再按虚表中的函数地址决定调用谁虚函数 多态走的就是这条路径。调用虚函数时大致过程是先从对象里拿到 vfptr → 找到对应虚表 → 按函数在表中的位置取出地址 → 跳转执行。基类对象和派生类对象的虚表内容不同于是动态绑定到了不同版本多态就此实现。底层汇编和虚表布局可以在调试器VS 的内存/监视窗口里自己观察一遍印象会深很多但是基础学习还是不建议先看这个没必要本篇先把主干原理交代清楚其实我也没太研究细致。总之关于底层原理我有机会再来扩充看到这句话的时候就代表我还没扩充呢。十二、总结多态分两类静态多态重载、模板编译期匹配动态多态本篇主角运行期按对象表现不同行为。两个条件基类指针或引用调用虚函数派生类完成重写。缺一不可。虚函数virtual修饰的类成员函数全局函数、静态成员函数、友元函数、构造函数都不能是虚函数。重写函数名、参数、const 属性一致返回值一般相同和隐藏不是一回事隐藏不需要虚函数、只看名字相同。默认参数静态绑定函数体动态绑定、默认值静态绑定重写时两边默认参数必须保持一致否则出隐蔽 bug。析构虚函数基类析构声明为virtual派生类析构自动构成重写否则基类指针delete派生类对象会跳过派生析构、泄漏资源。override检查签名是否真的重写final禁止继续重写虚函数或禁止类被继承。纯虚函数 0含纯虚函数的类是抽象类不能实例化派生类不重写仍是抽象类相当于强制接口规范。底层原理含虚函数的对象多一个 vfptr 指向类共享的虚表虚表存只读常量区函数体经虚表动态绑定默认参数走静态绑定。把买票示例和车辆抽象类示例自己敲一遍再在调试窗口观察一次虚表多态这关就吃透了。如果这篇对你有帮助别忘了点赞收藏关注三连~ 面向对象系列下一篇咱们接着深挖参考C 运行时多态、虚函数与重写、默认参数静态绑定、虚析构、override/final、纯虚函数与抽象类、虚表指针与动态绑定
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/27 18:58:29
手撕Kmeans
2026/10/27 18:58:29
万字深潜:向量数据库 Milvus 的分布式内核到底强在哪?从「单一事实来源」WAL 到万亿级毫秒检索
2026/10/27 18:53:26
ReAct大模型学习指南:小白也能掌握的推理+行动Agent模式(收藏版)
2026/10/27 19:38:47
Vue与TailwindCSS组合开发实战指南
2026/10/27 19:38:47
分布式系统高并发解决方案:Redis锁与幂等设计实战
2026/10/27 19:38:47
MySQL学生成绩管理系统设计:从表结构到权限的完整实验方案
2026/10/27 19:38:47
轻量级知识竞赛系统:Flask+SQLite+Vue实时答题与排名
2026/10/27 19:38:47
AI代理如何重塑消费决策与商业生态
2026/10/27 19:33:46
二维数组与函数:编程基础与性能优化实战
2026/10/27 0:04:20
WorkBuddy_新建任务与工作模式19_“模式选错”的典型代价
2026/10/27 0:04:20
WorkBuddy_新建任务与工作模式20_新手推荐路径
2026/10/27 0:04:20
ZerolanLiveRobot开发者指南:如何扩展一个新服务并注册自定义事件(附完整实战示例)
2026/10/27 11:41:45
全国计算机二级C语言真题PDF高效刷题指南
2026/10/27 9:24:40
requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南
2026/10/27 12:03:45
递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现
2026/10/25 22:15:16
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/26 11:29:48
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/25 12:42:56
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)