虚函数与多态
更新时间:2026-08-27。本文是
languages/cpp/主题入门层第 28 篇(存量class-object拆解迁入)。虚函数让"调用哪个函数"从编译期推迟到运行期:一个Shape*指针,运行时才知道指向的是Circle还是Square,然后调对应的area。这就是多态。本篇讲透virtual的用法和它背后的虚表。
本文要回答的问题
- 多态是什么?没有
virtual会怎样? - 虚表(vtable)是什么?多态的开销在哪?
- 为什么基类析构函数必须是虚的?
一、没有 virtual 的继承:按"指针类型"调
class Shape {
public:
double area() const { return 0; } // 普通成员函数
};
class Circle : public Shape {
double r_;
public:
Circle(double r) : r_(r) {}
double area() const { return 3.14159 * r_ * r_; }
};
Shape* s = new Circle(5.0);
std::cout << s->area(); // 输出 0 —— 调的是 Shape::area!s 是 Shape*,编译器编译这行时只看得到指针类型:调 Shape::area,输出 0。虽然 s 实际指向 Circle,但普通成员函数的调用在编译期就定死了——这叫静态绑定。
二、加了 virtual:运行时才决定
class Shape {
public:
virtual double area() const { return 0; } // 虚函数
};
class Circle : public Shape {
double r_;
public:
Circle(double r) : r_(r) {}
double area() const override { return 3.14159 * r_ * r_; } // 重写
};
class Square : public Shape {
double s_;
public:
Square(double s) : s_(s) {}
double area() const override { return s_ * s_; }
};
Shape* shapes[] = { new Circle(5.0), new Square(3.0) };
for (auto* s : shapes) {
std::cout << s->area(); // 分别调 Circle::area 和 Square::area
}virtual 把调用从"编译期按指针类型"改成"运行期按实际对象"——动态绑定,这就是多态。s->area() 执行时,通过 s 指向的对象找到它真正的类,调那个类的 area。
配套关键字 override(C++11):声明"我在重写基类的虚函数",写错签名编译器立刻报错——防止把 area() 写成 aree() 这种静默错误。
三、虚表:多态的底层
动态绑定靠**虚表(vtable)**实现。带虚函数的类,每个对象里藏一个隐藏指针 vptr,指向一张"函数指针表":

每个具体类一张虚表,表里是"这个类各个虚函数的真实地址"。调用虚函数 = 一次间接跳转(取 vptr → 查表 → 跳转)。代价:
- 每个带虚函数的对象多一个指针(8 字节)的大小;
- 每次虚调用多一次间接寻址(比直接调用略慢,分支预测也难命中——L4 篇有讲);
- 虚函数基本无法内联。
所以"该用多态时用多态,别为没用处的 virtual 白付开销"。
四、虚析构函数:多态下的安全退出
class Shape {
public:
virtual ~Shape() {} // 虚析构!必须的
};
Shape* s = new Circle(5.0);
delete s; // 不虚的话:只调 Shape::~Shape,Circle 的资源全漏基类析构函数必须是虚的。否则 delete s(s 是 Shape*)只调用 Shape::~Shape,Circle 自己的清理逻辑(释放它的成员资源)根本不执行。这是多态最常见的资源泄漏源。规则一句话:有虚函数的类,析构函数写成虚的。
五、和 C 对比
| 对比项 | C | C++ |
|---|---|---|
| 多态 | 函数指针结构体手搓 | virtual 语言级 |
| 动态分发 | 手动 if/else 判断类型 | 虚表自动 |
| 开销 | 函数指针间接调用 | 同样有间接调用 |
| 抽象接口 | 手搓 vtable 结构体 | virtual + 接口类(30 篇) |
C 也能做多态——struct 里塞函数指针、自己实现 vtable(内核/驱动这么干),但全是手工活,错一处就崩。C++ 把这件事标准化了,代价是每个虚对象多 8 字节和一个间接跳转。
六、与本站主线衔接
override/final的完整规则,见 override 与 final;- 抽象类与纯虚函数(接口),见 抽象类与纯虚函数;
- 虚表在对象布局里的位置、多重继承的虚表,见专家层虚函数与虚表;
- 多态的间接跳转对分支预测的影响,见 L4 分支预测。
一句话总结
virtual 把成员函数的调用从编译期静态绑定变成运行期动态绑定,基类指针/引用调用的虚函数会在运行时按实际对象分发——这就是多态;底层是对象里藏 vptr 指向虚表,代价是每个对象 +8 字节、每次虚调用多一次间接跳转且难以内联;有虚函数的类,析构函数务必写 virtual,否则 delete 基类指针会跳过派生类的清理。
上一篇:派生类构造与析构顺序 下一篇:override 与 final