C++中的虚函数(virtual)是如何实现的?(虚函数表和虚表指针)

8次阅读

虚函数调用需查表因编译期无法确定具体调用版本,故运行时通过 vptr 和 vtable 实现多态;每个含虚函数的类有静态 vtable,对象头含隐式 vptr 指向对应 vtable,vptr 不参与 sizeof 且不可修改。

C++ 中的虚函数(virtual)是如何实现的?(虚函数表和虚表指针)

虚函数调用 为什么 需要查表?

因为编译期无法确定 ptr->func() 到底该调用哪个版本的函数——ptr 可能指向 BaseDerived1Derived2 对象。C++ 选择在运行时通过间接跳转实现多态,而不是生成所有可能的分支(那会爆炸式膨胀代码)。这个“间接跳转”的载体就是虚函数表(vtable)和每个对象头里的虚表指针(vptr)。

vptr 和 vtable 在内存里长什么样?

每个含虚函数的类(包括派生类)在编译期生成一张静态的 vtable,里面按声明顺序存放函数指针;每个该类的对象在内存布局最前面隐式插入一个 vptr,初始化时指向其所属类的 vtable。注意:vptr 不是成员变量,不参与 sizeof 计算(但影响对象实际大小),也不可被取地址或修改。

struct Base {virtual void f() {}     virtual void g() {}}; struct Derived : Base {void f() override {}  // 覆盖 Base::f     void h() {}           // 新增非虚函数}; // 编译后:// Base::vtable = {&Base::f, &Base::g} // Derived::vtable = {&Derived::f, &Base::g}  ← g 未重写,复用基类地址 // new Base → 内存:[vptr→Base::vtable] + 成员数据 // new Derived → 内存:[vptr→Derived::vtable] + Base 成员 + Derived 成员

多重继承下 vptr 怎么处理?

一个类若从多个含虚函数的基类继承,它会为每个这样的基类子对象维护独立的 vptr。例如 class D : public A, public B(A/B 都有虚函数),则 D 对象内存中会有两个 vptr:一个紧贴 A 子对象起始处,指向 A 的 vtable;另一个在 B 子对象起始处,指向 B 的 vtable。强制转型(如 static_cast(d_ptr))本质就是调整指针值,使其对齐到 B 子对象的起始地址(即第二个 vptr 所在位置)。

  • 单继承:通常只有一个 vptr,位于对象开头
  • 多重继承:可能有多个 vptr,位置取决于基类声明顺序和 ABI(如 Itanium C++ ABI)
  • 虚继承:还会引入额外的偏移量字段,用于运行时修正 this 指针,vtable 条目也可能带 offset 数据

哪些操作会破坏 vptr 的有效性?

vptr 是构造 / 析构过程中由编译器自动管理的,手动干预极易出错:

立即学习C++ 免费学习笔记(深入)”;

  • memcpy 复制含虚函数的对象 → 只复制了 vptr 值,但新对象的生命周期未触发构造函数,vptr 可能指向已销毁的 vtable 或不匹配的类
  • 把派生类对象赋给基类数组(切片)→ 基类数组元素只有基类大小,vptr 被截断或覆盖,后续虚调用 UB
  • 在构造函数里调用虚函数 → 此时 vptr 指向当前正在构造的类的 vtable,不会动态绑定到最终派生类(即使对象是派生类实例)

虚函数机制本身很轻量,但依赖严格的对象生命周期和内存布局。一旦绕过构造 / 析构流程,vptr 就成了悬空指针。

text=ZqhQzanResources