基类指针调用虚函数时执行派生类版本,因编译器生成vtable并由对象vptr在运行时动态绑定;须通过指针或引用调用且函数声明为virtual,否则静态绑定。

为什么基类指针调用函数时,实际执行的是派生类的版本
因为编译器在遇到 virtual 声明的函数时,会为该类生成虚函数表(vtable),每个对象头部隐式存储一个指向该表的指针(vptr)。运行时通过 vptr 找到对应函数地址,完成动态绑定。
关键前提是:必须通过指针或引用调用,且函数声明为 virtual。直接用对象值传递会触发静态绑定(即切片 + 编译期决议)。
- 非
virtual函数:编译期根据变量静态类型决定调用哪个版本 -
virtual函数:运行期根据对象实际类型查 vtable 决定调用哪个版本 - 构造函数不能是
virtual;析构函数建议声明为virtual(尤其基类有指针成员时)
虚函数表(vtable)在内存中长什么样
vtable 是编译器生成的静态数组,每个元素是函数指针,顺序与类中 virtual 函数声明顺序一致。单继承下,派生类 vtable 会复制基类部分,并覆盖被重写的函数地址;多继承则可能有多个 vptr,布局更复杂。
注意:vtable 不是对象的一部分,而是类级别的只读数据;vptr 才是每个对象开头的指针(通常 8 字节,在 64 位系统上)。
立即学习“C++免费学习笔记(深入)”;
- 可以用
sizeof验证:带虚函数的类,即使无成员变量,sizeof也至少为 8 - gdb 中可打印
*((void**)obj)查看 vptr 指向的首项(即第一个虚函数地址) - 纯虚函数在 vtable 中对应 nullptr,强制子类实现
override 和 final 关键字到底防什么
override 不是语法糖,它让编译器检查:当前函数是否真的重写了基类的 virtual 函数。拼错函数名、参数不匹配、const 修饰不一致都会报错。
final 则禁止后续派生类再重写该函数,或禁止整个类被继承。两者都用于把本应在运行时暴露的问题(如意外未重写、误覆写)提前到编译期捕获。
- 没加
override时,看似重写成功,实则定义了一个新函数,基类虚函数仍按原逻辑走 - 基类函数加了
final,子类里再写同签名函数并加override,编译直接失败 - 函数参数类型用引用/指针时,顶层 const 不影响重写判断;但底层 const(如
int* const)会影响
动态绑定失效的典型场景
最常见的是在构造函数和析构函数内部调用虚函数——此时动态绑定不生效,调用的是当前正在构造/析构的那个类的版本,而非最终派生类的版本。
原因:对象的 vptr 在构造函数执行过程中逐步被修改(先设为基类 vtable 地址,再逐层更新),析构时则逆向还原。所以中间状态无法反映完整类型。
- 构造函数中调用
virtual函数,等价于调用当前类的函数(哪怕派生类已重写) - 析构函数中同理,不会跳转到派生类实现
- 避免在构造/析构中做依赖多态的行为;如需初始化逻辑,可提取为独立的
init()并由用户显式调用
虚函数机制本身开销很小(一次指针解引用 + 偏移寻址),但真正容易出问题的地方,往往不是“怎么写”,而是“在哪调”和“谁来管生命周期”。尤其是跨模块传递对象、用智能指针管理多态对象时,vptr 的存在和析构顺序会直接影响行为是否符合预期。










