每个含虚函数的类在编译时生成唯一虚函数表(vtable),存于只读数据段,所有该类对象共享;每个对象仅含一个指向该表的指针(vptr),通常位于对象内存起始处。

不会。每个对象不保存虚函数表,只保存一个指向虚函数表的指针(vptr);虚函数表本身是类级别的,所有该类对象共享同一张表。
虚函数表(vtable)是类的,不是对象的
编译器为每个含虚函数的类生成**唯一一张** vtable,它在程序启动时就初始化完成,存放在只读数据段(.rodata),生命周期与程序一致。你用 Base 创建 1000 个对象,它们的 vptr 都指向同一张 Base::vtable;同理,Derived 对象的 vptr 指向的是 Derived::vtable,这张表也只有一份。
常见误解是“每个对象带一份函数指针数组”,实际对象里只有:一个 vptr(通常 4 或 8 字节)+ 成员变量 + 填充字节。你可以用 sizeof 验证:class Base { virtual void f(){}; int x; }; 的大小至少是 sizeof(void*) + sizeof(int)(对齐后可能更大),但绝不会大到容纳多个函数指针。
vptr 在对象内存中的位置和初始化时机
vptr 是编译器自动插入的隐式成员,绝大多数编译器(如 GCC、MSVC)把它放在对象内存布局的**最开头**(少数平台可能放末尾,但行为一致)。它的值不是对象构造时“复制”来的,而是在构造函数执行**过程中由编译器注入**的:
- 进入
Base构造函数时,vptr被设为指向Base::vtable - 进入
Derived构造函数时(且基类构造已完成),vptr被覆写为指向Derived::vtable - 这意味着:在
Derived构造函数体执行前,哪怕你调用虚函数,也会走到Base版本(因为此时vptr还没更新)
为什么 memset(&obj, 0, sizeof(obj)) 会导致虚函数调用崩溃
这是最典型的实操陷阱。一旦你对含虚函数的对象做 memset,vptr 就被清零,变成空指针。后续任何虚函数调用(比如 obj.func() 或通过指针/引用调用)都会触发间接跳转到地址 0,结果就是段错误(SEGFAULT)或未定义行为。
容易被忽略的点:
-
std::memcpy、std::fill、序列化反序列化时裸拷贝内存,同样会破坏vptr - 聚合类型(
struct)若不含虚函数,memset安全;但只要加了一个virtual,立刻失效 - 调试时若看到
__vfptr字段为0x00000000,基本可断定是这类内存误操作导致的
多继承下 vptr 数量可能不止一个
单继承时,对象只有一个 vptr;但多继承(尤其虚继承)可能导致一个对象含多个 vptr —— 每个父类子对象区域头部都可能有自己的 vptr,分别指向对应类的 vtable。这不是“每个对象多份表”,而是“一个对象可能有多个指针,各指不同类的唯一表”。这种布局由 ABI(如 Itanium C++ ABI)规定,手动计算偏移极易出错,应依赖 offsetof 或调试器查看实际内存。
真正需要警惕的从来不是“表有多少份”,而是“vptr 是否有效、是否被意外覆盖、是否在构造/析构中途被误用”。这些才是线上 crash 和 undefined behavior 的高发区。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











