同一行代码,根据对象的实际类型在运行时决定调用哪个函数——这就是多态。virtual 关键字是实现它的核心。
上一节(12.8)提到:子类覆盖父类的方法是"静态绑定"——编译器在编译时就根据变量的声明类型决定调用哪个函数,而不是对象的实际类型。
这会导致一个让人困惑的现象:
| 1 | class Hero { |
| 2 | public: |
| 3 | void Attack() { cout << "普通攻击" << endl; } |
| 4 | }; |
| 5 | class Mage : public Hero { |
| 6 | public: |
| 7 | void Attack() { cout << "释放火球术" << endl; } |
| 8 | }; |
| 9 | |
| 10 | int main() { |
| 11 | Mage m; |
| 12 | Hero* p = &m; // 父类指针指向子类对象(合法) |
| 13 | p->Attack(); // ⚠️ 输出:普通攻击 ← 调用了父类版本! |
| 14 | // p 的类型是 Hero*,编译器只看声明类型,不看实际对象 |
| 15 | } |
明明 p 指向的是一个 Mage 对象,调用 Attack() 却还是执行了父类的版本——这显然不是我们想要的。我们希望:不管指针的类型是什么,都根据它实际指向的对象来调用函数。
这就是多态,而实现它只需要一个关键字:virtual。
在父类的函数声明前加 virtual,这个函数就变成了虚函数。此后,通过父类指针或引用调用这个函数时,C++ 会在运行时查找对象的实际类型,调用对应的版本。
| 1 | class Hero { |
| 2 | public: |
| 3 | virtual void Attack() { cout << "普通攻击" << endl; } |
| 4 | }; |
| 5 | class Mage : public Hero { |
| 6 | public: |
| 7 | void Attack() { cout << "释放火球术" << endl; } |
| 8 | }; |
| 9 | class Warrior : public Hero { |
| 10 | public: |
| 11 | void Attack() { cout << "挥剑斩击" << endl; } |
| 12 | }; |
| 13 | |
| 14 | void DoAttack(Hero* hero) { // 接受父类指针 |
| 15 | hero->Attack(); // 运行时决定调用哪个版本 |
| 16 | } |
| 17 | |
| 18 | int main() { |
| 19 | Mage m; |
| 20 | Warrior w; |
| 21 | DoAttack(&m); // 输出:释放火球术 |
| 22 | DoAttack(&w); // 输出:挥剑斩击 |
| 23 | // 同一行代码,根据实际对象类型执行不同行为——这就是多态 |
| 24 | } |
Hero* p 不管指向什么,都调用 Hero::Attack()。
Hero* p 指向 Mage 就调用 Mage::Attack()。
C++ 实现动态绑定靠的是虚函数表(vtable)。每个含有虚函数的类都有一张这样的表,记录着每个虚函数的实际地址。每个对象内部有一个隐藏的指针(vptr)指向自己类的 vtable。
调用虚函数时,程序先通过 vptr 找到 vtable,再从表里查到函数的真实地址来调用——这就是"运行时查表"。
vptr,指向自己类的虚函数表(vtable)。Mage 的表里是火球术,Warrior 的表里是斩击。如果你用父类指针管理子类对象,析构函数必须声明为 virtual,否则 delete 时只会调用父类的析构函数,子类的资源无法正确释放,导致内存泄漏。
| 1 | class Hero { |
| 2 | public: |
| 3 | // ❌ 非虚析构——用父类指针 delete 子类对象时,只调用 Hero::~Hero |
| 4 | ~Hero() { cout << "Hero 析构" << endl; } |
| 5 | |
| 6 | // ✅ 正确做法:父类析构函数加 virtual |
| 7 | virtual ~Hero() { cout << "Hero 析构" << endl; } |
| 8 | }; |
| 9 | |
| 10 | class Mage : public Hero { |
| 11 | public: |
| 12 | ~Mage() { cout << "Mage 析构" << endl; } |
| 13 | }; |
| 14 | |
| 15 | int main() { |
| 16 | Hero* p = new Mage(); |
| 17 | delete p; // virtual 析构:先 Mage 析构,再 Hero 析构 ✅ |
| 18 | // 非 virtual 析构:只调用 Hero 析构,Mage 的资源泄漏 ❌ |
| 19 | } |
delete 子类对象,就必须把父类的析构函数声明为 virtual。这是 C++ 多态使用中最常见的 bug 来源之一。
C++11 引入了 override 关键字,写在子类覆盖函数的末尾,告诉编译器"这个函数是故意覆盖父类的虚函数"。如果函数签名写错了(比如参数类型不对),编译器会立刻报错提醒你。
| 1 | class Hero { |
| 2 | public: |
| 3 | virtual void Attack(int damage) {} |
| 4 | }; |
| 5 | |
| 6 | class Mage : public Hero { |
| 7 | public: |
| 8 | // 不写 override:参数写错了变成 float,变成新函数而非覆盖,没有警告 |
| 9 | void Attack(float damage) {} |
| 10 | |
| 11 | // 写 override:参数写错,编译器立刻报错 ✅ |
| 12 | void Attack(int damage) override {} |
| 13 | }; |
virtual,调用时根据对象实际类型动态决定版本——这就是多态。override,让编译器帮你检查签名是否匹配。