虚函数表只存储虚函数的地址,每个含虚函数的类有唯一静态vtable,对象通过vptr指向它;基类指针调用虚函数时,通过vptr查vtable实现动态绑定。

虚函数表(vtable)到底存了什么
虚函数表不是语法概念,而是编译器生成的、每个含虚函数的类对应的一张函数指针数组。它不存储类名、对象数据或类型信息,只存 vptr 能跳转到的虚函数地址——比如 Base::func()、Derived::func() 这类被声明为 virtual 的函数入口地址。
关键点:一个类只有一个 vtable(静态分配),所有该类对象共享;但每个对象开头会隐式插入一个 vptr 成员,指向其所属类的 vtable。继承关系越深,vtable 可能越长(覆盖/新增虚函数都会影响布局)。
为什么基类指针调用虚函数能执行子类逻辑
根本原因在于运行时通过 vptr 间接寻址:编译器把 ptr->func() 编译成「取 ptr 所指对象的 vptr → 查 vtable 第 N 项 → 跳转执行」三步操作。只要 ptr 实际指向的是 Derived 对象,它的 vptr 就指向 Derived 的 vtable,里面第 N 项已经是 Derived::func() 的地址。
- 非虚函数调用在编译期绑定,直接硬编码地址,和
vptr无关 - 构造函数里调用虚函数不会多态——此时子类 vtable 尚未就绪,
vptr指向当前正在构造的类的 vtable - 析构函数必须是虚函数,否则
delete base_ptr只会调用基类析构,子类部分内存泄漏
如何验证 vtable 的存在和结构
不能直接访问 vtable(无标准接口),但可通过调试器或内存观察间接确认:
立即学习“C++免费学习笔记(深入)”;
在支持 -fdump-class-hierarchy 的 GCC 下编译:g++ -fdump-class-hierarchy test.cpp,会生成文本描述各类型的 vtable 布局,包括偏移、函数签名、是否被覆盖等。
更直观的方式是打印对象内存(仅用于学习):
struct Base { virtual void f() {} };
struct Derived : Base { void f() override {} };
Base b; Derived d;
std::cout << "b size: " << sizeof(b) << ", d size: " << sizeof(d) << "\n";
// 通常都是 8(64位下 vptr 占 8 字节),说明 vptr 是唯一额外开销
注意:sizeof 不体现 vtable 本身大小(它是全局只读数据段里的常量数组),只体现对象内 vptr 成员的大小。
多重继承下 vtable 为什么会变复杂
单继承时,子类对象内存布局是「基类部分 + 新增成员」,vptr 放在最前面;但多重继承(如 class D : public A, public B)会导致对象内出现多个 vptr——每个父类子对象都需要自己的 vtable 指针。
这时 static_cast 和 dynamic_cast 就不只是地址加减:它们要修正指针值以对齐目标子对象起始地址。例如 static_cast(&d) 可能返回比 &d 大 8 字节的地址,因为 B 子对象不在对象开头。
常见陷阱:
- 用
reinterpret_cast强转多继承指针几乎必然出错 -
offsetof对虚继承或含虚函数的类行为未定义 - 虚继承引入额外指针(vbptr),进一步打乱布局,vtable 条目也会增加 vbtable 偏移项
vtable 是实现细节,标准不规定其格式,不同编译器(GCC / Clang / MSVC)布局可能不同,跨 ABI 传递含虚函数的对象指针需格外谨慎。








