虚函数调用能跨类型执行不同实现是因为编译器为含虚函数的类插入vptr指向vtable,对象构造时vptr自动初始化为指向本类vtable;若基类函数未声明virtual则静态绑定,导致多态失效。

虚函数调用为什么能跨类型执行不同实现
因为编译器在类中插入了隐藏的 vptr(虚函数表指针),它指向一块只读内存区域——vtable。每个含虚函数的类有唯一一份 vtable,里面存的是该类所有虚函数的地址。对象构造时,vptr 自动被初始化为指向自己类的 vtable。
常见错误现象:Base* p = new Derived(); p->func(); 却调用了 Base::func —— 很可能 func 在 Base 中没声明为 virtual,导致静态绑定。
-
virtual必须显式写在基类函数声明前,派生类重写时可加可不加(但建议加上,提高可读性) - 纯虚函数如
virtual void func() = 0;会让vtable对应项为nullptr,此时类不可实例化 - 构造/析构函数里调用虚函数,不会动态分发——因为
vptr正在被修改中,此时指向的是当前正在构造/析构的那个类的vtable
怎么验证一个类有没有虚函数表
最直接的办法是看 sizeof:只要类含虚函数(或继承自含虚函数的类),哪怕只有一个,sizeof 至少为指针大小(通常 8 字节 on x64)。空类原本是 1 字节,加 virtual 后立刻变成 8。
使用场景:调试 ABI 兼容性、序列化时跳过 vptr、或做内存布局分析。
立即学习“C++免费学习笔记(深入)”;
- 用
offsetof查不到vptr的确切偏移——它是编译器实现细节,标准未规定位置(通常在对象开头) - GCC/Clang 下可用
__builtin_object_size或调试信息反推,但更可靠的是用gdb打印对象内存:p/x *(void**)obj看首地址是否为有效函数指针 - 注意:启用
-fno-rtti不影响vtable,但会删掉type_info指针(如果存在的话)
虚函数表内容怎么查(GCC/Clang 实操)
用 nm -C 或 objdump -t 可看到符号 vtable for ClassName;更直观的是用 g++ -fdump-class-hierarchy 输出类布局文本。
示例命令:g++ -fdump-class-hierarchy test.cpp && cat test.cpp.000t.class
- 输出中会列出每个类的
vtable条目,格式类似:0 (int) 0(offset)、8 (int) 8(RTTI 指针)、然后是函数地址行 - 多重继承下,子类可能有多个
vptr,对应不同父类子对象起始位置——这时sizeof会更大,且static_cast到不同基类指针时,地址值可能变化 - 虚继承会额外引入
vbtable(虚基类表),和主vtable分开,别混淆
哪些操作会破坏虚函数动态分发
不是所有“看起来像多态”的写法都真能动态调用。核心原则:必须通过指针或引用,且静态类型是基类,而实际对象是派生类。
典型翻车现场:Base b; Derived d; b = d;——这是对象切片,b 就只是个 Base,vptr 指向 Base 的表,再调虚函数也只会进 Base 版本。
- 值传递参数(
void foo(Base b))→ 发生切片,失去多态 - 用
memcpy或std::bit_cast复制对象内存 →vptr被原样复制,但若目标内存未正确构造,行为未定义 - 把对象放在
std::vector<base>里 → 同样切片;要用std::vector<:unique_ptr>></:unique_ptr>或std::vector<:shared_ptr>></:shared_ptr> - 用
reinterpret_cast强转指针类型 → 绕过类型系统,vptr还在,但调用时可能访问错位的vtable条目,崩溃或静默错误
真正难搞的从来不是“怎么写虚函数”,而是“什么时候它其实没生效”——尤其在对象生命周期、内存布局、类型转换交叉的边界上。









