news 2026/10/17 11:02:13

C++:多态详解(从概念本质、语法规则到底层实现,结合实战代码的全方位指南)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++:多态详解(从概念本质、语法规则到底层实现,结合实战代码的全方位指南)

一. 多态的概念:从“多种形态说起”

1.1 多态的概念解析

多态通俗来说就是“多种形态”,在C++中分为两类

本文代码示例所需头文件:

#include <iostream> using namespace std;

1.2 生活中的多态示例

最经典的场景就是“买票行为”:

用样是“买票”,不同对象执行不同逻辑,这就是多态的本质。我们接下来会在代码中通过继承和多态来实现“调用同一函数”产生不同结果。

二. 多态的构成条件和核心语法

多态是一个继承关系下的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象优惠买票(当然再来个军人对象继承Person的话,这个对象去调用也会由不同的结果)

要实现多态,除了要有继承关系,还必须满足下面这两个强制条件,缺一不可:

说明:要实现多态的效果,第一必须是基类的指针或者引用,因为只有基类的指针或引用才能既指向基类对象又指向派生类对象;第二派生类必须对基类的虚函数完成重写/覆盖,重写了,基类和派生类之间才能有不同的函数,多态的不形态效果才能达到。

2.1 条件 1:虚函数的定义

类成员函数前加virtual关键字,该函数即为虚函数(非成员函数和静态成员函数不能加virtual)。虚函数的作用是 “标记” 该函数需要参与多态,让编译器为其生成动态绑定逻辑。

// 基类:Person class Person { public: // 虚函数:标记为需要参与多态 virtual void BuyTicket() { cout << "买票-全价" << endl; } };

2.2 条件 2:虚函数的重写(覆盖)

派生类中定义一个 “与基类虚函数完全一致” 的函数,即为重写。这里 “完全一致” 指:

代码示例(注意看注释):

// 基类:Person class Person { public: // 虚函数:标记为需要参与多态 virtual void BuyTicket() { cout << "买票-全价" << endl; } }; // 派生类:Student(继承Person) class Student : public Person { public: // 重写基类虚函数:函数名、参数、返回值完全一致 // 派生类中virtual也可以省略 virtual void BuyTicket() { cout << "买票-打折" << endl; } }; // 派生类:Soldier(继承Person) class Soldier : public Person { public: // 重写基类虚函数 // 派生类中virtual也可以省略 virtual void BuyTicket() { cout << "买票-优先" << endl; } }; // 关键:用基类指针调用虚函数(满足多态条件1) // 这里也可以用基类引用 void Func(Person* ptr) { // 运行时根据ptr指向的对象类型,调用对应类的BuyTicket // Person* ptr ptr->BuyTicket(); // 如果是基类引用(Person& ptr) //ptr.BuyTicket(); } int main() { Person ps; // 基类对象 Student st; // 派生类对象(学生) Soldier sr; // 派生类对象(军人) Func(&ps); // 指向基类对象 → 调用Person::BuyTicket → 输出“买票-全价” Func(&st); // 指向学生对象 → 调用Student::BuyTicket → 输出“买票-打折” Func(&sr); // 指向军人对象 → 调用Soldier::BuyTicket → 输出“买票-优先” return 0; }

注意:派生类重写时,即使不加virtual,也能构成重写(因为基类虚函数的 “虚属性” 会被继承),但不建议这么写,可读性差且易出错。

坑点(下面的笔试题会有体现):在 C++ 中,虚函数重写时若基类和派生类的虚函数都指定了缺省参数,调用时的缺省值只由“基类的函数声明”决定,与派生类的重写实现无关。

2.3 多态场景的一个笔试选择题(重要):

以下程序输出的结果是什么(B)
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; }

图解如下:

改编扩展:以下程序输出的结果是什么(D)
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* q = new B; q->func(); return 0; }

图解如下:

三、虚函数重写的特殊情况

虚函数重写并非只有 “完全一致” 一种情况,还有两种特殊场景需要注意:协变和析构函数重写,这也是面试高频考点。

3.1 协变(了解)

派生类重写基类虚函数时,返回值类型可以不同,但必须满足:

这种情况称为“协变”,实际开发中使用较少,了解即可。

代码示例(注意看注释):

#include <iostream> using namespace std; // 基类A class A {}; // 派生类B(继承A) class B : public A {}; // 基类Person class Person { public: // 虚函数:返回基类A的指针 virtual A* BuyTicket() { cout << "买票-全价" << endl; return nullptr; } }; // 派生类Student class Student : public Person { public: // 重写:返回派生类B的指针(协变) 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; }

3.2 析构函数的重写(重点)

基类的析构函数为虚函数:基类析构函数加virtual后,派生类析构函数无论是否加virtual,都构成重写。这是因为编译器会将所有析构函数的名称统一处理为destructor,看似名称不同,实则一致。

注意:这个在面试题中经常考到,问基类中的析构函数建不建议写成虚函数?大家可以结合下面的代码示例和为什么去进行回答,这样才能讲清楚

为什么需要析构函数构成重写?
如果基类析构函数不是虚函数,用基类指针指向派生类对象并delete时,只会调用基类析构函数,导致派生类中动态申请的资源无法释放,引发内存泄漏。

代码示例(注意看注释,其中额外测试部分是补充了解的和这里的重点不同):

class A { public: // 基类析构函数加virtual,支持重写 virtual ~A() { cout << "~A()" << endl; } }; class B : public A { public: // 派生类析构函数:自动构成重写(加不加virtual都可以) ~B() { cout << "~B()->delete:" << _p << endl; delete _p; // 释放派生类动态申请的资源 } protected: int* _p = new int[10]; // 派生类动态申请的数组 }; void test() { cout << "--------额外测试结果--------" << endl; //额外测试,这个是正常场景,加不加都行 //只是为了让大家了解一下这个析构顺序 //析构顺序:~B(),~A(),~A() //其中第一个~A()是因为子类B析构完后调用基类的(先子后父),后面一个是a对象析构 A a; B b; } // 基类只要保障了析构函数是虚函数,下面场景就不会存在内存泄漏 int main() { // 基类指针指向派生类对象 A* ptr1 = new B; delete ptr1; // 多态调用:先调用~B(),再先子后父自动调用~A(),无内存泄漏 // 基类指针指向基类对象 A* ptr2 = new A; delete ptr2; // 调用~A() test(); return 0; }

主要测试版块:如果基类析构不加virtual:delete ptr1只会调用~A(),B类中_p指向的数组未释放,导致内存泄漏。额外测试那里加不加都行。

四. C++11:override 与 final 关键字

虚函数重写对语法要求严格(如函数名写错、参数类型不匹配),这些错误编译时不会报错,只会在运行时出现非预期结果。C++11 提供override和final两个关键字,帮我们在编译阶段检测错误。

4.1 override:检测是否重写

在派生类虚函数后加override,编译器会检查该函数是否真的重写了基类虚函数。若未重写(如函数名错、参数错),直接编译报错。
代码示例(注意看注释):

class Car { public: // 基类虚函数:Drive(注意拼写是Drive,不是Dirve) virtual void Drive() { cout << "Car-行驶" << endl; } }; class Benz : public Car { public: // 错误示例:函数名写成Dirve,加override后编译报错 // virtual void Dirve() override { cout << "Benz-舒适" << endl; } // 正确示例:函数名正确,override检测通过 virtual void Drive() override { cout << "Benz-舒适" << endl; } }; int main() { Car* p = new Benz; p->Drive(); // 多态调用:输出“Benz-舒适” return 0; }

4.2 final:禁止重写

在基类虚函数后加final,表示该虚函数不允许任何派生类重写。若派生类强行重写,编译报错。
代码示例(注意看注释):

class Car { public: // 基类虚函数加final:禁止派生类重写 virtual void Drive() final { cout << "Car-行驶" << endl; } }; class Benz : public Car { public: // 错误:Drive()被final修饰,无法重写,编译报错 // virtual void Drive() override { cout << "Benz-舒适" << endl; } }; int main() { return 0; }

五. 易混淆概念:重载、重写、隐藏的对比(常考)

多态相关的三个概念(重载、重写(覆盖)、隐藏(重定义))极易混淆,我们通过下面的图片,代码示例和表格来加强一下对它们的区分:

代码示例(借鉴-注意看注释):

特性重载(Overload)重写(Override)隐藏(Hide)
作用域同一类(同一作用域)基类与派生类(不同作用域)基类与派生类(不同作用域)
函数名必须相同必须相同必须相同
参数列表必须不同(类型/个数/顺序)必须相同可相同可不同
返回值类型无要求必须相同(协变除外)无要求
虚函数要求无必须都是虚函数无
核心场景同一类中同名函数的不同实现多态的核心,动态绑定派生类屏蔽基类同名成员(非重写)
底层机制编译期静态绑定,通过参数列表区分函数运行期动态绑定,依赖虚函数表编译期静态绑定,通过作用域区分
示例class A { void func(int); void func(double); }class A { virtual void func(); }; class B : public A { void func() override; }class A { void func(); }; class B : public A { void func(int); }
注意事项仅在同一类中生效,派生类中若与基类函数同名且参数不同,会隐藏基类函数重写时函数签名(函数名+参数+返回值)必须严格一致,析构函数重写有特殊性若派生类函数与基类虚函数同名但参数不同,会隐藏基类虚函数,导致多态失效
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 5:32:49

Conda-forge提交Qwen-Image-Edit-2509包以简化安装流程

Conda-forge提交Qwen-Image-Edit-2509包以简化安装流程 在电商运营、社交媒体内容生成和广告设计等高度依赖视觉表达的领域&#xff0c;图像编辑正面临前所未有的效率挑战。传统方式需要设计师手动使用Photoshop完成每一张图的修改&#xff0c;面对成百上千张商品图时&#xf…

作者头像 李华
网站建设 2026/10/17 11:45:10

什么是RoCE网络

转自微信号&#xff1a;AI Long Cloud 一、什么是RoCE网络&#xff1f; RoCE&#xff08;英文&#xff1a;RDMA over Converged Ethernet&#xff09;是一种基于以太网的远程直接内存访问&#xff08;RDMA&#xff09;技术&#xff0c;旨在通过无损以太网实现低延迟和高吞吐量…

作者头像 李华
网站建设 2026/10/17 17:20:41

百度网盘智能助手:告别繁琐提取码,开启极速下载新时代

还在为百度网盘的提取码而烦恼吗&#xff1f;每次看到心仪的资源&#xff0c;却要在页面间来回切换寻找那串神秘代码&#xff1f;现在&#xff0c;BaiduPanKey将彻底改变这一现状&#xff0c;为你带来前所未有的便捷体验。 【免费下载链接】baidupankey 项目地址: https://g…

作者头像 李华
网站建设 2026/10/17 10:32:32

Wan2.2-T2V-5B模型部署指南:快速搭建本地视频生成服务

Wan2.2-T2V-5B模型部署指南&#xff1a;快速搭建本地视频生成服务 在短视频内容爆炸式增长的今天&#xff0c;创作者对高效、低成本的自动化视频生成工具需求日益迫切。然而&#xff0c;主流文本到视频&#xff08;Text-to-Video, T2V&#xff09;模型往往依赖多卡A100集群运行…

作者头像 李华
网站建设 2026/10/17 7:48:37

音乐爱好者必备!Melody+管理多平台音源,远程听歌难题cpolar轻松解决

文章目录前言1. 添加镜像源2. 本地部署Melody3. 本地访问与使用演示4. 安装内网穿透5. 配置Melody公网地址6. 配置固定公网地址前言 Melody 是一款个人在线音乐平台&#xff0c;能聚合多个音乐网站的资源&#xff0c;支持歌曲搜索、播放、下载和上传到云盘&#xff0c;还能同步…

作者头像 李华