# C++多态的实现机制
面试官问"C++多态是怎么实现的",很多人只答"虚函数+继承"就停了。但面试官真正想听的是底层怎么走的——vtable 在哪、vptr 什么时候初始化、调用链是怎样的。只说表面概念,面试官一定会追问到你答不上来。
把编译时多态和运行时多态分清楚,再把运行时多态的调用链讲明白,这道题就稳了。
# 简要回答
C++ 多态分两种:编译时多态靠函数重载和模板,编译期就确定调用谁;运行时多态靠 virtual 函数 + 继承 + 基类指针/引用,运行时通过 vptr → vtable → 函数地址 动态绑定到正确的实现。
# 详细回答
| 维度 | 编译时多态 | 运行时多态 |
|---|---|---|
| 实现手段 | 函数重载、模板 | 虚函数 + 继承 |
| 绑定时机 | 编译期(静态绑定) | 运行期(动态绑定) |
| 关键机制 | 编译器根据参数类型/模板参数选择 | vptr → vtable → 函数地址 |
| 触发条件 | 直接调用即可 | 必须通过基类指针或引用调用 |
| 性能 | 无额外开销,可内联 | 多一次间接寻址,无法内联 |
| 典型场景 | sort 的比较函数、max<T> | 基类接口统一调度派生类行为 |
运行时多态的三个必要条件:基类声明 virtual 函数、派生类重写(override)该函数、通过基类指针或引用调用。三者缺一不可——如果用派生类对象直接调用,编译器会静态绑定,不走 vtable。
虚函数调用的底层流程(以 Animal* p = new Dog(); p->speak(); 为例):
- 编译器为每个含虚函数的类生成一张 vtable,表中按声明顺序存放虚函数地址。Dog 的 vtable 中
speak槽位存的是Dog::speak的地址 - 每个对象的内存布局最前面藏一个 vptr,构造时指向本类的 vtable
- 调用
p->speak()时:通过 p 找到 Dog 对象 → 取 vptr → 在 vtable 中查speak的偏移 → 跳转执行Dog::speak()
这就是为什么基类析构函数必须声明为 virtual——否则 delete p 时只调基类析构,派生类资源泄漏。

# 知识拓展
Q:抽象类和纯虚函数是什么关系?
含有纯虚函数(virtual void run() = 0;)的类就是抽象类,不能实例化,只能被继承。派生类必须实现所有纯虚函数才能实例化,相当于强制定义接口规范。
Q:override 和 final 分别干什么?
override 让编译器检查你是否真的重写了基类虚函数,写错函数签名会直接报错,防止"以为重写了其实没有"的 bug。final 禁止派生类继续重写某个虚函数,或者禁止某个类被继承。
Q:虚函数调用比普通函数慢多少?
多一次指针间接寻址(取 vptr + 查 vtable),在现代 CPU 上通常只差几纳秒。真正的性能损失不在寻址本身,而在于虚函数调用无法被内联优化。热路径上如果频繁调用小虚函数,可以考虑 CRTP(静态多态)替代。
Q:构造函数里能调虚函数吗?
能调,但不会多态。构造函数执行时 vptr 指向的是当前正在构造的类的 vtable,不是最终派生类的。所以在基类构造函数里调虚函数,调的是基类版本,不是派生类版本。
评论
验证登录状态...