C++ 多态:从概念到实践,吃透面向对象核心特性
在面向对象编程(OOP)中,多态是三大核心特性(封装、继承、多态)的压轴戏。它解决了 “同一行为在不同对象上产生不同结果” 的问题,是代码解耦、提升扩展性的关键。本文将从通俗概念入手,逐步深入多态的实现细节、原理机制,最后结合实际场景带你落地实践,帮你彻底掌握 C++ 多态。
一、什么是多态?—— 通俗理解
多态的核心是 “一个接口,多种实现”。通俗来说:完成同一个行为,不同对象去做会产生不同的效果。
举两个生活中的例子,帮你快速 get:
- 买票行为:
普通人买票 → 全价;
学生买票 → 半价;
军人买票 → 优先通道。
同样是 “买票”,不同身份的人(对象)执行,结果不同。
- 支付宝扫红包:
新用户扫码 → 红包金额大(8-10 元);
老用户扫码 → 红包金额小(0.1-0.5 元)。
同样是 “扫码”,不同用户(对象)执行,红包结果不同。
在 C++ 中,多态的官方定义是:在不同继承关系的类对象上,调用同一函数,产生不同行为。比如Student
继承Person
,Person
对象调用BuyTicket()
是全价,Student
对象调用则是半价。
二、多态的定义与实现 —— 关键条件 + 代码示例
要实现多态,必须满足两个核心条件,缺一不可。我们先拆解条件,再通过代码落地。
2.1 多态的两个核心条件
调用方式:必须通过基类的指针或引用调用虚函数;
函数要求:被调用的函数必须是虚函数,且派生类必须重写基类的虚函数。
2.2 关键概念:虚函数与重写
在讲代码前,先明确两个基础概念:
1. 虚函数(virtual function)
被virtual
关键字修饰的类成员函数,就是虚函数。它是多态的 “开关”。
class Person {
public:// 虚函数:买票virtual void BuyTicket() {cout << "Person: 买票-全价" << endl;}
};
2. 虚函数的重写(覆盖)
派生类中定义一个与基类完全相同的虚函数(返回值、函数名、参数列表完全一致),称为 “重写”(也叫 “覆盖”)。
⚠️ 注意:派生类虚函数可省略virtual
(因继承后基类虚函数属性保留),但不规范,建议显式加virtual
,或用 C++11 的override
关键字强制检查重写。
2.3 多态实现代码示例
下面用 “买票” 场景实现多态,代码带详细注释:
#include <iostream>
using namespace std;// 基类:Person
class Person {
public:// 1. 定义虚函数virtual void BuyTicket() {cout << "Person: 买票-全价" << endl;}
};// 派生类:Student(继承Person)
class Student : public Person {
public:// 2. 重写基类虚函数(用override确保重写,编译期检查错误)virtual void BuyTicket() override {cout << "Student: 买票-半价" << endl;}
};// 派生类:Soldier(继承Person)
class Soldier : public Person {
public:// 3. 重写基类虚函数virtual void BuyTicket() override {cout << "Soldier: 买票-优先通道" << endl;}
};// 4. 通过基类引用调用虚函数(满足多态条件1)
void DoBuyTicket(Person& people) {people.BuyTicket(); // 多态调用:根据实际对象类型决定执行哪个函数
}int main() {Person p; // 基类对象Student s; // 派生类1对象Soldier sol; // 派生类2对象DoBuyTicket(p); // 输出:Person: 买票-全价DoBuyTicket(s); // 输出:Student: 买票-半价DoBuyTicket(sol); // 输出:Soldier: 买票-优先通道return 0;
}
运行结果:
Person: 买票-全价Student: 买票-半价Soldier: 买票-优先通道
可见:同一函数DoBuyTicket
,传入不同对象,执行结果完全不同 —— 这就是多态!
2.4 重写的两个例外情况
严格来说,重写要求 “返回值、函数名、参数列表完全一致”,但有两个合法例外:
例外 1:协变(返回值不同,但符合规则)
基类虚函数返回基类对象的指针 / 引用,派生类虚函数返回派生类对象的指针 / 引用,称为 “协变”,仍算重写。
class A {};
class B : public A {}; // B继承Aclass Person {
public:// 基类虚函数:返回A*virtual A* CreateObj() {cout << "Create A" << endl;return new A();}
};class Student : public Person {
public:// 派生类虚函数:返回B*(协变,算重写)virtual B* CreateObj() override {cout << "Create B" << endl;return new B();}
};
例外 2:析构函数的重写(函数名不同)
基类析构函数若为虚函数,派生类析构函数无论是否加virtual
,都算重写。
原因:编译器会将所有析构函数统一处理为destructor
(隐藏的统一函数名)。
为什么要重写析构函数?
避免内存泄漏!当用基类指针指向派生类对象时,若基类析构不是虚函数,delete
时只会调用基类析构,派生类成员内存无法释放。
class Person {
public:virtual ~Person() { // 基类析构设为虚函数cout << "~Person()" << endl;}
};class Student : public Person {
public:~Student() override { // 重写析构cout << "~Student()" << endl;}
};int main() {Person* p1 = new Person();Person* p2 = new Student(); // 基类指针指向派生类对象delete p1; // 输出:~Person()delete p2; // 输出:~Student() → ~Person()(先析构派生类,再析构基类)return 0;
}
2.5 C++11:override 与 final 关键字
重写要求严格,若不小心写错函数名(如BuyTicket
写成BuyTicker
),编译器不会报错,只会导致多态失效,debug 困难。C++11 提供两个关键字解决这个问题:
1. override:强制检查重写
加在派生类虚函数后,若该函数未重写基类虚函数,编译期报错。
class Student : public Person {
public:// 错误示例:函数名写错(BuyTicker≠BuyTicket),编译器直接报错virtual void BuyTicker() override { cout << "Student: 买票-半价" << endl;}
};
2. final:禁止虚函数被重写
加在基类虚函数后,该函数不能被任何派生类重写,否则编译报错。
class Person {
public:// final:禁止派生类重写BuyTicketvirtual void BuyTicket() final {cout << "Person: 买票-全价" << endl;}
};class Student : public Person {
public:// 错误:无法重写被final修饰的虚函数virtual void BuyTicket() override {cout << "Student: 买票-半价" << endl;}
};
2.6 易混淆概念:重载、重写、重定义(隐藏)
很多人会把这三个概念搞混,用一张表清晰对比:
特性 | 重载(Overload) | 重写(Override) | 重定义(隐藏,Redefine) |
---|---|---|---|
作用域 | 同一类 | 基类与派生类 | 基类与派生类 |
函数名 | 相同 | 相同 | 相同 |
参数列表 | 必须不同 | 必须相同 | 可相同可不同 |
返回值 | 可相同可不同 | 相同(协变例外) | 可相同可不同 |
虚函数 | 无要求 | 基类必须是虚函数,派生类重写 | 无要求(若基类是虚函数且参数相同,仍算隐藏) |
调用方式 | 编译期静态绑定 | 运行期动态绑定(多态) | 编译期静态绑定 |
三、抽象类 —— 多态的 “接口规范”
抽象类是包含纯虚函数的类,它的核心作用是 “强制派生类实现接口”,无法直接实例化对象。
3.1 纯虚函数的定义
在虚函数后加=0
,就是纯虚函数:
class Car {
public:// 纯虚函数:驾驶接口virtual void Drive() = 0;
};
3.2 抽象类的特性
不能实例化对象:
Car car;
会编译报错;派生类必须重写纯虚函数:否则派生类也是抽象类,无法实例化;
体现接口继承:抽象类只定义 “接口”(如
Drive
),不关心 “实现”,派生类负责具体实现。
3.3 抽象类代码示例
用 “汽车驾驶” 场景,抽象类规范不同车型的实现:
#include <iostream>
using namespace std;// 抽象类:Car(定义驾驶接口)
class Car {
public:virtual void Drive() = 0; // 纯虚函数:接口virtual ~Car() {} // 虚析构,避免内存泄漏
};// 派生类:Benz(实现驾驶接口)
class Benz : public Car {
public:void Drive() override {cout << "Benz: 舒适驾驶,平稳如床" << endl;}
};// 派生类:BMW(实现驾驶接口)
class BMW : public Car {
public:void Drive() override {cout << "BMW: 操控为王,贴地飞行" << endl;}
};// 派生类:Audi(实现驾驶接口)
class Audi : public Car {
public:void Drive() override {cout << "Audi: 科技感拉满,灯光秀走起" << endl;}
};// 测试函数:通过基类指针调用接口
void TestDrive(Car* car) {car->Drive(); // 多态调用
}int main() {// Car car; // 错误:抽象类不能实例化Car* benz = new Benz();Car* bmw = new BMW();Car* audi = new Audi();TestDrive(benz); // 输出:Benz: 舒适驾驶...TestDrive(bmw); // 输出:BMW: 操控为王...TestDrive(audi); // 输出:Audi: 科技感拉满...delete benz;delete bmw;delete audi;return 0;
}
3.4 接口继承 vs 实现继承
普通函数继承:是 “实现继承”—— 派生类继承基类函数的实现,直接用;
虚函数继承:是 “接口继承”—— 派生类继承基类函数的接口,必须自己实现(抽象类强制)。
比如Car
的Drive()
是接口继承,Benz
必须自己写Drive()
的实现;而普通函数void Func() { ... }
被继承后,派生类可直接调用,无需重写。
四、多态的原理 —— 虚函数表(核心)
很多人会疑惑:为什么通过基类指针 / 引用调用虚函数,就能自动找到派生类的实现?这背后的核心是 “虚函数表”(简称 “虚表”)。
4.1 虚表的本质
含有虚函数的类,其对象会额外存储一个 “虚表指针”(_vfptr
),指向一张 “虚函数表”。
虚表:本质是一个存储虚函数指针的数组,数组末尾通常有一个
nullptr
作为结束标志;虚函数:和普通函数一样存在代码段,虚表里存的是虚函数的地址(不是函数本身);
虚表指针:存在对象的内存中(通常在对象头部,x86 下占 4 字节,x64 下占 8 字节)。
4.2 基类的虚表与对象内存
先看一个基类示例,分析其内存布局:
class Base {
public:virtual void Func1() { cout << "Base::Func1()" << endl; }virtual void Func2() { cout << "Base::Func2()" << endl; }void Func3() { cout << "Base::Func3()" << endl; } // 普通函数,不进虚表
private:int _b = 1; // 成员变量,4字节(x86)
};
在 x86 环境下,sizeof(Base)
是8 字节—— 原因:
4 字节:虚表指针(
_vfptr
);4 字节:成员变量
_b
。
对象内存布局如下:
Base对象:
+------------+
| _vfptr | // 虚表指针,指向Base的虚表
+------------+
| _b = 1 | // 成员变量
+------------+Base的虚表:
+------------+
| &Base::Func1 | // 虚函数1地址
+------------+
| &Base::Func2 | // 虚函数2地址
+------------+
| nullptr | // 结束标志
+------------+
4.3 派生类的虚表生成规则
派生类继承基类后,其虚表的生成遵循 3 步规则:
拷贝:先将基类的虚表内容完整拷贝到派生类的虚表中;
覆盖:若派生类重写了基类的某个虚函数,用派生类的虚函数地址覆盖虚表中对应基类虚函数的地址;
新增:若派生类有自己的新虚函数,按声明顺序将其地址添加到虚表末尾。
4.4 派生类的虚表与对象内存
基于上面的Base
类,定义派生类Derive
:
class Derive : public Base {
public:virtual void Func1() override { cout << "Derive::Func1()" << endl; } // 重写Func1virtual void Func4() { cout << "Derive::Func4()" << endl; } // 新虚函数
private:int _d = 2; // 派生类成员变量,4字节
};
x86 环境下,sizeof(Derive)
是12 字节—— 原因:
4 字节:继承的虚表指针;
4 字节:继承的
_b
;4 字节:自己的
_d
。
派生类对象内存布局与虚表:
Derive对象:
+------------+
| _vfptr | // 虚表指针,指向Derive的虚表
+------------+
| _b = 1 | // 继承的成员变量
+------------+
| _d = 2 | // 自己的成员变量
+------------+Derive的虚表:
+------------+
| &Derive::Func1 | // 覆盖:派生类Func1地址
+------------+
| &Base::Func2 | // 拷贝:基类Func2地址
+------------+
| &Derive::Func4 | // 新增:派生类Func4地址
+------------+
| nullptr | // 结束标志
+------------+
4.5 用代码打印虚表(验证原理)
编译器的监视窗口可能隐藏部分虚函数(如派生类新增的Func4
),我们可以写一个工具函数打印虚表,直观看到虚表内容:
#include <iostream>
using namespace std;// 定义虚函数指针类型
typedef void(*VFPTR)();// 打印虚表:参数是虚表指针
void PrintVTable(VFPTR vTable[]) {cout << "虚表地址:" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i) {printf(" 第%d个虚函数:地址=0X%p,调用结果:", i, vTable[i]);VFPTR func = vTable[i];func(); // 调用虚函数,验证内容}cout << endl;
}// 基类Base
class Base {
public:virtual void Func1() { cout << "Base::Func1()" << endl; }virtual void Func2() { cout << "Base::Func2()" << endl; }void Func3() { cout << "Base::Func3()" << endl; }
private:int _b = 1;
};// 派生类Derive
class Derive : public Base {
public:virtual void Func1() override { cout << "Derive::Func1()" << endl; }virtual void Func4() { cout << "Derive::Func4()" << endl; }
private:int _d = 2;
};int main() {Base b;Derive d;// 1. 取出Base对象的虚表指针(x86下对象首4字节是虚表指针)VFPTR* baseVTable = (VFPTR*)(*(int*)&b);PrintVTable(baseVTable);// 2. 取出Derive对象的虚表指针VFPTR* deriveVTable = (VFPTR*)(*(int*)&d);PrintVTable(deriveVTable);// 打印对象大小cout << "sizeof(Base) = " << sizeof(Base) << endl; // 8字节cout << "sizeof(Derive) = " << sizeof(Derive) << endl;// 12字节return 0;
}
运行结果:
虚表地址:00F1D730第0个虚函数:地址=0X00F110B4,调用结果:Base::Func1()第1个虚函数:地址=0X00F11118,调用结果:Base::Func2()虚表地址:00F1D748第0个虚函数:地址=0X00F1117C,调用结果:Derive::Func1()第1个虚函数:地址=0X00F11118,调用结果:Base::Func2()第2个虚函数:地址=0X00F111E0,调用结果:Derive::Func4()sizeof(Base) = 8
sizeof(Derive) = 12
结果完全符合我们的分析:派生类虚表覆盖了Func1
,保留了Func2
,新增了Func4
。
4.6 多态的实现流程(终于懂了!)
回到最开始的 “买票” 示例,当执行DoBuyTicket(s)
(s
是Student
对象)时,多态的执行流程是:
传入
Student
对象的引用people
(本质是基类引用指向派生类对象);访问
people
的虚表指针_vfptr
,找到Student
的虚表;在虚表中找到
BuyTicket
的地址(此时是Student::BuyTicket
);调用该地址对应的函数,输出 “Student: 买票 - 半价”。
而若传入Person
对象,步骤 2 会找到Person
的虚表,调用Person::BuyTicket
—— 这就是多态的本质!
4.7 静态绑定 vs 动态绑定
静态绑定(早绑定):编译期确定函数调用地址,如普通函数、重载函数;
动态绑定(晚绑定):运行期通过虚表找到函数地址,如多态的虚函数调用。
比如:
Person p;p.BuyTicket(); // 静态绑定:编译期确定调用Person::BuyTicketPerson& ref = Student();ref.BuyTicket(); // 动态绑定:运行期通过虚表找到Student::BuyTicket
五、单继承与多继承的虚表差异
实际开发中,继承关系分单继承和多继承,两者的虚表结构不同,我们重点看派生类的虚表特点。
5.1 单继承的虚表(一个虚表)
单继承下,派生类只有一个虚表,虚表内容按 “拷贝→覆盖→新增” 规则生成,如 4.4 节的Derive
类,这里不再赘述。
5.2 多继承的虚表(多个虚表)
多继承下,派生类会有多个虚表(每个基类对应一个虚表),未重写的虚函数会放入第一个基类的虚表中。
多继承代码示例
#include <iostream>
using namespace std;typedef void(*VFPTR)();
void PrintVTable(VFPTR vTable[]) {cout << "虚表地址:" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i) {printf(" 第%d个虚函数:0X%p → ", i, vTable[i]);VFPTR func = vTable[i];func();}cout << endl;
}// 基类1
class Base1 {
public:virtual void Func1() { cout << "Base1::Func1()" << endl; }virtual void Func2() { cout << "Base1::Func2()" << endl; }
private:int _b1 = 1;
};// 基类2
class Base2 {
public:virtual void Func1() { cout << "Base2::Func1()" << endl; }virtual void Func2() { cout << "Base2::Func2()" << endl; }
private:int _b2 = 2;
};// 派生类:多继承Base1和Base2
class Derive : public Base1, public Base2 {
public:// 重写两个基类的Func1virtual void Func1() override { cout << "Derive::Func1()" << endl; }// 新增虚函数Func3virtual void Func3() { cout << "Derive::Func3()" << endl; }
private:int _d = 3;
};int main() {Derive d;// 1. 取出Base1对应的虚表(对象首4字节)VFPTR* vTable1 = (VFPTR*)(*(int*)&d);PrintVTable(vTable1);// 2. 取出Base2对应的虚表(跳过Base1的大小:4字节虚表指针 + 4字节_b1 = 8字节)VFPTR* vTable2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));PrintVTable(vTable2);return 0;
}
运行结果:
虚表地址:009ED730第0个虚函数:0X009E117C → Derive::Func1() // 重写的Func1第1个虚函数:0X009E1118 → Base1::Func2() // Base1的Func2(未重写)第2个虚函数:0X009E11E0 → Derive::Func3() // 新增的Func3(放入第一个虚表)虚表地址:009ED748第0个虚函数:0X009E117C → Derive::Func1() // 重写的Func1第1个虚函数:0X009E11A0 → Base2::Func2() // Base2的Func2(未重写)
结论:
多继承派生类有多个虚表(Base1 和 Base2 各一个);
重写的虚函数(
Func1
)在两个虚表中都被覆盖;派生类新增的虚函数(
Func3
)放入第一个基类(Base1)的虚表中。
六、多态的实际应用场景
学完理论,更重要的是知道 “什么时候用多态”。以下是 3 个高频场景,带代码示例。
场景 1:框架设计(插件机制)
很多框架通过多态实现 “插件扩展”,比如日志框架:核心逻辑不变,新增日志类型(文件日志、控制台日志、数据库日志)时,无需修改原有代码。
// 抽象日志接口(抽象类)
class ILogger {
public:virtual void Log(const string& msg) = 0; // 日志接口virtual ~ILogger() {}
};// 控制台日志(插件1)
class ConsoleLogger : public ILogger {
public:void Log(const string& msg) override {cout << "[Console] " << msg << endl;}
};// 文件日志(插件2)
class FileLogger : public ILogger {
public:void Log(const string& msg) override {// 实际项目中会写入文件cout << "[File] " << msg << endl;}
};// 日志管理器(核心框架,无需修改)
class LogManager {
public:// 注册日志插件void AddLogger(ILogger* logger) {_loggers.push_back(logger);}// 统一打印日志(多态调用)void LogAll(const string& msg) {for (auto logger : _loggers) {logger->Log(msg);}}
private:vector<ILogger*> _loggers;
};int main() {LogManager manager;// 注册插件manager.AddLogger(new ConsoleLogger());manager.AddLogger(new FileLogger());// 打印日志:自动调用所有插件的Log方法manager.LogAll("系统启动成功!");return 0;
}
运行结果:
[Console] 系统启动成功!
[File] 系统启动成功!
好处:新增 “数据库日志” 时,只需继承ILogger
实现Log
,无需改LogManager
—— 符合 “开闭原则”(对扩展开放,对修改关闭)。
场景 2:图形绘制(不同形状渲染)
图形库中,不同形状(圆形、矩形、三角形)的绘制逻辑不同,用多态统一调用。
// 抽象形状(抽象类)
class Shape {
public:virtual void Draw() = 0; // 绘制接口virtual ~Shape() {}
};// 圆形
class Circle : public Shape {
public:void Draw() override {cout << "绘制圆形:○" << endl;}
};// 矩形
class Rectangle : public Shape {
public:void Draw() override {cout << "绘制矩形:□" << endl;}
};// 三角形
class Triangle : public Shape {
public:void Draw() override {cout << "绘制三角形:△" << endl;}
};// 绘制管理器
void DrawAllShapes(const vector<Shape*>& shapes) {for (auto shape : shapes) {shape->Draw(); // 多态调用:不同形状自动执行对应绘制逻辑}
}int main() {vector<Shape*> shapes;shapes.push_back(new Circle());shapes.push_back(new Rectangle());shapes.push_back(new Triangle());DrawAllShapes(shapes); // 统一绘制所有形状return 0;
}
场景 3:业务逻辑(支付方式选择)
电商平台的支付模块,不同支付方式(支付宝、微信、银联)的逻辑不同,用多态解耦。
// 抽象支付接口
class IPayment {
public:virtual bool Pay(double amount) = 0; // 支付接口:返回是否成功virtual ~IPayment() {}
};// 支付宝支付
class Alipay : public IPayment {
public:bool Pay(double amount) override {cout << "支付宝支付 " << amount << " 元:";// 实际调用支付宝APIcout << "支付成功!" << endl;return true;}
};// 微信支付
class WeChatPay : public IPayment {
public:bool Pay(double amount) override {cout << "微信支付 " << amount << " 元:";// 实际调用微信APIcout << "支付成功!" << endl;return true;}
};// 订单类(依赖支付接口,不依赖具体支付方式)
class Order {
public:Order(double amount) : _amount(amount) {}// 支付:传入任意支付方式bool PayOrder(IPayment* payment) {if (_amount <= 0) {cout << "订单金额无效!" << endl;return false;}return payment->Pay(_amount); // 多态调用}
private:double _amount;
};int main() {Order order(299.9); // 299.9元的订单// 用支付宝支付order.PayOrder(new Alipay());// 用微信支付order.PayOrder(new WeChatPay());return 0;
}
七、常见问题与面试考点
多态是 C++ 面试的高频题,整理了 10 个核心问题,帮你避坑。
1. inline 函数可以是虚函数吗?
答:可以,但编译器会忽略 inline 属性。因为虚函数需要放入虚表,而 inline 函数是编译期展开,两者冲突,最终函数会按普通虚函数处理。
2. 静态成员函数可以是虚函数吗?
答:不能。静态成员函数属于类,没有this
指针,无法访问对象的虚表指针,因此无法放入虚表,自然不能是虚函数。
3. 构造函数可以是虚函数吗?
答:不能。对象的虚表指针是在构造函数初始化列表阶段才初始化的,若构造函数是虚函数,此时虚表指针尚未就绪,无法找到虚函数地址,逻辑矛盾。
4. 析构函数为什么建议设为虚函数?
答:避免内存泄漏!当用基类指针指向派生类对象时,若基类析构不是虚函数,delete
时会静态绑定调用基类析构,派生类成员内存无法释放;设为虚函数后,会动态绑定调用派生类析构(再调用基类析构),确保内存完全释放。
5. 虚表是在什么时候生成的?存在哪里?
答:虚表在编译阶段生成,存储在代码段(常量区),不是在运行时动态生成。对象中存储的是虚表指针,不是虚表本身。
6. 一个类有多少张虚表?
答:一个类(无论有多少对象)只有一张虚表,所有对象共享这张虚表,对象中只存虚表指针。
7. 多继承中,派生类有几张虚表?
答:派生类有N 张虚表(N 是直接继承的基类个数),每个基类对应一张虚表,派生类新增的虚函数放入第一个基类的虚表中。
8. 虚函数和普通函数的调用效率哪个高?
答:分情况:
普通对象调用:效率相同,都是静态绑定;
基类指针 / 引用调用:普通函数效率高(静态绑定),虚函数需要运行时查虚表(动态绑定),有轻微开销。
9. 什么是抽象类?抽象类的作用是什么?
答:包含纯虚函数的类是抽象类。作用:
强制派生类实现接口(不实现则派生类也是抽象类,无法实例化);
体现接口继承,解耦接口与实现。
10. 多态的实现原理是什么?
答:核心是 “虚函数表 + 虚表指针”:
含有虚函数的类,其对象会存储一个虚表指针(
_vfptr
);类的虚函数地址存储在虚表中,虚表在编译期生成;
派生类虚表按 “拷贝→覆盖→新增” 规则生成;
通过基类指针 / 引用调用虚函数时,运行期通过虚表指针找到对应虚表,再找到虚函数地址,实现动态绑定(多态)。
八、总结
多态是 C++ 面向对象的灵魂,它通过 “虚函数表” 机制,实现了 “同一接口,多种实现”,让代码更灵活、更易扩展。
核心要点回顾:
多态的两个条件:基类指针 / 引用调用虚函数 + 派生类重写虚函数;
虚函数重写的例外:协变、析构函数重写;
抽象类:含纯虚函数,强制派生类实现接口;
原理:虚表指针指向虚表,运行期动态绑定;
应用:框架设计、插件机制、业务逻辑解耦。
掌握多态,不仅能写出更优雅的代码,更能理解面向对象的设计思想(如开闭原则)。建议结合本文代码动手实践,感受多态的魅力!