
多态
3.1 什么是多态
指同一个接口(函数)作用于不同的对象时,表现出不同的行为。
3.2 动态多态
✅ 条件:
基类中的函数必须是虚函数(
virtual
关键字)子类必须重写这个虚函数
必须通过基类的指针或引用来调用
✅ 示例代码
#include <iostream>
using namespace std;
class Animal {
public:
virtual void speak() const {
cout << "Animal speaks" << endl;
}
};
class Dog : public Animal {
public:
void speak() const override {
cout << "Dog barks" << endl;
}
};
void makeSound(const Animal& a) {
a.speak(); // 运行时决定调用哪个 speak
}
int main() {
Dog d;
makeSound(d); // 输出:Dog barks
}
3.3 多态的底层机制
在 C++ 中,运行时多态依赖于虚函数机制,它能让你在运行时根据对象的真实类型调用正确的函数。
实现的关键就是两个隐藏机制:
虚函数表(vtable)
虚指针(vptr)
📌 1. vtable(虚函数表)
每个定义了虚函数的类,编译器都会生成一张“虚函数表”(vtable)。
这张表是一个函数指针数组,表中记录的是该类虚函数的地址。
如果类中有继承关系,则子类会继承并可能重写(覆盖)父类的虚函数地址。
📌 2. vptr(虚指针)
每个含有虚函数的对象,内部会自动带有一个隐藏的指针变量
vptr
。vptr
指向该对象所属类的 vtable。编译器会在对象构造时自动初始化
vptr
,在派生类构造阶段会重新指向正确的 vtable。
示例:
class Base {
public:
virtual void show() { cout << "Base\n"; }
};
class Derived : public Base {
public:
void show() override { cout << "Derived\n"; }
};
Base* b = new Derived();
b->show(); // 输出:Derived
调用过程如下:
(1)b 是指向 Derived 对象的 Base*
(2)b->show()
实际会变成:
b->vptr->vtable[0]()
(3)因为 vptr
指向的是 Derived
的 vtable,所以调用的是 Derived::show
🔍vtable 的构造细节
编译器为每个类创建 vtable,是一个只读的全局数组。
如果类没有虚函数,则没有 vtable。
如果类继承了虚函数但没有重写,vtable 中该位置仍指向父类函数。
一旦重写,vtable 中该项会被替换成子类重写版本。
🔍构造 & 析构中的特殊行为
构造函数中调用虚函数不会多态!
因为构造函数阶段,vptr 还没有被初始化为子类的 vtable。
析构函数必须设为 virtual! 否则使用
Base*
删除Derived
对象时,只会调用 Base 析构,资源可能泄露。
Derived 对象:
[对象内存]
+---------+-------------------------+
| vptr → | Derived 的 vtable 地址 |
+---------+-------------------------+
| 成员1 | ... |
| 成员2 | ... |
+---------+-------------------------+
vtable 内容(函数指针数组):
[0] → Derived::show
[1] → Derived::destructor
3.4常见的面试问题
问题1:虚函数怎么实现多态?
✔答:通过 vtable + vptr 实现运行时查表。
问题2:一个对象能有多个 vptr 吗?
✔答:多重继承时可能有多个 vptr。
问题3:构造函数中调用虚函数能多态吗?
✔答:不能,此时 vptr 还指向当前构造类的 vtable。
问题4:为什么要用虚析构函数?
✔答:保证基类指针 delete 派生类对象时调用完整析构链。
问题5:虚函数增加后影响 ABI 吗?
✔答:是的,vtable 的结构可能变化,影响二进制兼容性。