Appearance
虚函数表 vtable 与对象内存布局
一、对象模型:vptr + vtable
单个含虚函数的类
Dog 对象内存
┌───────────────┐
Dog dog; │ vptr ─────────┼──► Dog::vtable
│ ...数据成员 │ ┌────────────────────┐
└───────────────┘ │ RTTI(倾斜到typeinfo)│
│ Dog::speak() │
│ Animal::eat() │
│ ... │
└────────────────────┘- vptr 放在对象最前面(libstdc++/MSVC 单继承均如此),一个字大小(8 字节 64 位)。
- vtable 是只读的、进程内该类仅一份;不同类不同表,继承通过表内容表达。
多继承下的对象
多继承时对象内部有多个基类子对象,通常有多个 vptr(每支基类一个),指向不同 vtable。
class X : public A, public B { 覆写 A::f, B::g };
┌──────────────────────────┐
│ A 部分: [vptr→表A][A字段] │ ← A 的 vtable
│ B 部分: [vptr→表B][B字段] │ ← B 的 vtable(this 指针需偏移)
│ X 自己的字段 │
└──────────────────────────┘(这是「为什么把 A 转成 B* 时指针地址会变化/偏移」的根源。)*
二、对象大小
| 类 | 大小(64位) | 说明 |
|---|---|---|
空类 class A{}; | 1 | 需占位使不同对象地址不同 |
| 空类但含虚函数 | 8(1 个 vptr) | vptr 占 8 字节;实际可能因对齐仍是 8 |
int x; 无虚 | 4 | — |
int x; 含虚函数 | 16 | 4(data)+8(vptr)+对齐到 8 边界 → 16 |
| 多继承各有虚 | 通常 = 基类子对象之和 | vptr×基类数 + 各自数据 + 对齐 |
关键点
- 空类 size=1:为了
A a1,a2; &a1 != &a2,需要最小非零存储。 - 空类加虚函数后 size 跳到 8(对齐整 8),不是 1+vptr 简单相加,而是按成员对齐规则取对齐后的整数。
- 值纯虚函数/抽象类不额外占位(只影响能否实例化)。
- 同一翻译单元不包含 vtable 需注意 weak(见链接性),但这是实现细节,一般没必要展开。
三、vtable 存在哪里?
- 逻辑上 vtable 属于「只读数据段(.rodata)」,是静态、进程内共享的数据。
- 注意与「类数据成员」区分:每个对象自己的 vptr 在对象内;vtable 本身是全局唯一的。
- (实现细节:libstdc++ 大 ODR、'key function' 决定 vtable 放哪个翻译单元——
vague linkage/weak symbol。可答「实现相关,标准不规定」,提供者通常答 .rodata。)
四、高频问答
Q1. 派生类对象里 vptr 指向何处?构造过程如何变 vptr?
对象构造时 vptr 逐步更新:
- 进入基类构造 → vptr 指向基类 vtable;
- 基类构造完成,进入派生类构造 → vptr 再指向派生类 vtable。 这就是构造期间不能触发多态的原因(vptr 尚未指向最终类)。
Q2. 继承覆盖、隐藏虚函数有什么坑?
- 只要签名完全一致且基类虚,就覆写;否则名字隐藏(派生类同名不同参会把基类同名虚整体隐藏)。
- 覆写时建议加
override,让编译器捕捉「本以为是覆写其实签名不符」的 bug。
Q3. 纯虚函数 / 抽象类?
virtual void f() = 0;纯虚函数 → 类为抽象类,不可实例化;- 派生类必须实现所有纯虚函数才能实例化;
- 可作「接口 / 公共契约」,也常见「纯虚析构」需给实现
~Base()=0;的{}(否则派生析构链找不到实现)。
Q4. vptr 放开头有什么优缺点?
- 放开头:
A*与派生类对象开头一致,单继承下 this 指针无须偏移,类型转换高效。此即 C++ ABI 通用选择。 - 多重继承因要多个 vptr、不同子对象偏移,才会出现地址偏移与 thunk(跳转适配)技术——一句「需要 second base 的 this 调整,编译器生成调整 thunk」即可。
五、对象布局类问题套路
- 先数出:几个 vptr(几条含虚的基类链 / 继承支数)+ 各数据成员 + 是否数组特化。
- 再按对齐规则(最大成员对齐单位)计算总和并向上取整。
- 空类特殊(1 字节);**空基类优化(EBO)**让
struct Empty{}; struct X:Empty{int n;};大小=4 而非 5(编译器把空基类占位与成员合并)——std::tuple/unique_ptr删除器常利用 EBO 省内存。