Skip to content

虚函数表 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; 含虚函数164(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 逐步更新

  1. 进入基类构造 → vptr 指向基类 vtable;
  2. 基类构造完成,进入派生类构造 → 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」即可。

五、对象布局类问题套路

  1. 先数出:几个 vptr(几条含虚的基类链 / 继承支数)+ 各数据成员 + 是否数组特化。
  2. 再按对齐规则(最大成员对齐单位)计算总和并向上取整。
  3. 空类特殊(1 字节);**空基类优化(EBO)**让 struct Empty{}; struct X:Empty{int n;}; 大小=4 而非 5(编译器把空基类占位与成员合并)——std::tuple/unique_ptr 删除器常利用 EBO 省内存。

C++ 面试八股 · VitePress 版