虚函数调用实际被编译器改写为通过vptr和vtable的运行时跳转,如(*p->vptr[2])(p),依赖对象内存中的虚表指针而非指针静态类型;vptr在构造中分阶段更新,未完成构造时虚调用仍走基类版本。

因为基类指针背后实际携带了虚函数表指针(vptr),运行时通过它查到当前对象所属类的虚函数表(vtable),再根据函数在表中的固定偏移位置跳转到真正的实现——不是靠指针类型,而是靠对象内存里藏着的那张“函数地图”。
虚函数调用实际被编译器改写成什么
你写的 p->show() 看似简单,编译器在生成汇编时会把它替换成类似这样的逻辑:
(*p->vptr[2])(p)
其中 vptr 是对象开头隐式插入的指针,[2] 是 show 在虚函数表里的索引(从 0 开始),最后把 p 当作 this 传进去。这个过程完全发生在运行时,和 p 的静态类型(比如 Base*)无关。
常见错误现象:
- 忘记在基类声明中加
virtual→ 编译器不生成vtable,也不插vptr,调用永远走静态绑定 - 派生类重写函数时漏写
override或签名不一致(比如 const 修饰不同)→ 实际没覆盖,vtable对应位置仍是基类函数地址
为什么子类对象赋值给基类指针后还能找到自己的函数
关键在于:子类对象的内存布局开头也是 vptr,但它指向的是子类自己的 vtable,而该表中已将基类虚函数的条目替换为子类版本的地址。
使用场景:
- 容器存基类指针(如
std::vector<animal></animal>),遍历时统一调用speak()却各自输出不同声音 - 工厂函数返回
Base*,但实际构造的是Derived对象,上层无需知道具体类型
注意:如果基类没有虚函数,整个机制不启动;即使有虚函数,若子类没重写,vtable 中对应项仍指向基类实现。
容易被忽略的初始化细节
vptr 不是天生就位的,它在构造函数中被分阶段设置:
- 进入
Base构造函数时,vptr指向Base::vtable - 执行完
Base构造后,进入Derived构造函数时,vptr被更新为指向Derived::vtable - 这意味着:在
Base构造函数体内调用虚函数,哪怕子类已重写,也只会调用Base版本——此时vptr还没切过去
这个时间差是很多段错误或未定义行为的源头,尤其当虚函数里访问子类才初始化的数据成员时。
真正起作用的从来不是指针类型,而是对象内存里那个悄悄存在的 vptr —— 它只在构造完成、对象真正“活过来”之后,才指向正确的函数地图。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











