C++ 虚继承对象内存布局

C++ 虚继承对象内存布局

一、先区分:普通继承 vs 虚继承

1. 普通公有继承(非虚)

structBase{inta;};structDerived:Base{intb;};

布局:Base部分 + Derived自身成员

Derived 对象内存: [ int a ] // Base [ int b ] // Derived

多继承普通继承会多份基类副本,菱形继承产生二义性,所以引入虚继承 virtual,让所有派生类共享同一份公共基类(虚基类)。

2. 虚继承核心目的

解决菱形继承(钻石继承)的数据冗余、成员二义性问题:公共父类只存一份,派生类通过**虚基表指针(vbase ptr)**间接访问虚基类。

二、核心概念

  1. 虚基类(virtual base class):被virtual继承的上层父类
  2. 虚基表指针(vbptr, virtual base pointer):每个含有虚继承的派生类对象会带一个 vbptr,指向虚基表 vbtable
  3. 虚基表 vbtable:存储「当前对象 → 虚基类子对象」的内存偏移量
  4. 虚基类子对象统一放在派生类内存末尾,所有派生类共享同一份虚基类数据

区分:

  • vptr(虚表指针):虚函数用,存虚函数地址;
  • vbptr(虚基指针):虚继承用,存虚基类偏移。
    一个类可同时有 vptr + vbptr。

三、最简单路虚继承示例

#include<iostream>usingnamespacestd;structVBase{// 虚基类intv_a=10;};structMid:virtualVBase{// 虚继承 VBaseintm_b=20;};

Mid 对象内存布局(32/64位通用逻辑,以64位举例)

Mid包含:

  1. vbptr(虚基指针,8字节64位)
  2. 自身成员m_b(int 4,对齐补4)
  3. 末尾:共享虚基类VBase子对象v_a

内存排布:

Offset 内容 0x00 vbptr → 指向 Mid 的虚基表 vbtable 0x08 m_b (int) 0x0C 填充对齐 4字节 0x10 VBase::v_a (虚基类,放在最后)

Mid 的虚基表 vbtable(vbptr指向这里)

虚基表存储从 vbptr 地址到虚基类子对象首地址的偏移

  • 偏移值:0x10,代表从vbptr位置往后加 0x10 才能找到 VBase。

访问mid.v_a的底层流程:

  1. 取对象首地址,拿到 vbptr;
  2. 查虚基表得到偏移 offset;
  3. 对象首地址 + offset = VBase子对象地址;
  4. 访问v_a

四、经典菱形虚继承(最常考布局)

钻石结构:

Top (虚基类,仅一份) / \ virtual/ \virtual Left Right \ / \ / Down

代码:

structTop{intt=1;};structLeft:virtualTop{intl=2;};structRight:virtualTop{intr=3;};structDown:Left,Right{intd=4;};

Down 对象完整内存布局(64位系统)

规则:

  1. 先放 Left 派生部分:vbptr_Left+ Left::l
  2. 再放 Right 派生部分:vbptr_Right+ Right::r
  3. 再放 Down 自身成员 d
  4. 内存最末尾:唯一一份 Top 虚基类 t
Offset 内容 0x00 vbptr_Left // Left 的虚基指针 0x08 Left::l // int 4 0x0C 对齐填充4 0x10 vbptr_Right // Right 的虚基指针 0x18 Right::r // int4 0x1C 对齐填充4 0x20 Down::d // int4 0x24 对齐填充4 0x28 Top::t // 全局唯一虚基类,所有类共享

两张虚基表

  1. Left 的 vbtable:存 Left 对象首地址到 Top 的偏移0x28
  2. Right 的 vbtable:存 Right 对象首地址到 Top 的偏移0x18
访问逻辑举例:

Down d; d.t;

  • 通过 Left 分支:d首地址 + Left虚基表偏移 → Top地址
  • 通过 Right 分支:d首地址 + Right虚基表偏移 → 同一个Top地址
    完美实现只有一份Top,无冗余、无二义。

五、同时含虚函数 + 虚继承(vptr + vbptr 共存)

structVBase{virtualvoidfunc(){}// 虚函数,带vptrinta;};structDer:virtualVBase{intb;};

Der 对象布局:

  1. vbptr(虚基指针,虚继承用)
  2. Der::b
  3. 末尾:VBase子对象(内含 VBase 的 vptr + int a)
0x00 vbptr 0x08 b 0x10 VBase子对象: 0x10 vptr(虚函数表指针) 0x18 a

区分两个指针:

  • vbptr:属于派生类 Der,用于找虚基类 VBase;
  • vptr:属于虚基类 VBase 内部,用于多态虚函数调用。

六、关键底层规则总结(面试高频)

1. vbptr 生成规则

只要类使用virtual继承任意基类,该类对象就会增加一个vbptr
多虚继承则对应多个 vbptr(菱形中Left、Right各一个)。

2. 虚基类存放固定规则

所有虚基类子对象统一放在派生类内存布局的最后,不会穿插在派生类成员中间。

3. 虚基表 vbtable 存储内容

虚基表里只存偏移量 offset:当前vbptr所在位置 → 虚基类子对象首地址的差值。
不同派生类、不同继承分支,偏移值不同。

4. 大小对比:普通多继承 vs 虚继承

以上面菱形为例:

  • 普通非虚多继承:Down 包含两份Top,内存更大,数据冗余;
  • 虚继承:仅一份Top,代价是每个中间类多一个 vbptr(指针开销)。

5. 构造/析构特殊规则(和布局配套考点)

  1. 虚基类构造最先调用:无论继承层级多深,虚基类构造函数第一个执行;
  2. 只有最底层派生类 Down 能直接初始化虚基类 Top,Left/Right 构造函数对Top的初始化会被忽略;
  3. 析构顺序相反:派生自身 → 中间类 → 虚基类。

七、32位 / 64位系统差异

  • 32位:指针4字节,vbptr/vptr 都是4字节,对齐按4;
  • 64位:指针8字节,对齐按8;
    布局结构逻辑完全一致,仅指针宽度、填充对齐不同。

八、常见面试问题简答

  1. 虚继承为什么能解决菱形二义性?
    虚基类只存一份,派生类通过虚基表偏移间接访问唯一副本,不存在多份数据冲突。
  2. vbptr 和 vptr 区别?
    vptr 对应虚函数表,存函数地址,实现多态;vbptr对应虚基表,存内存偏移,实现共享虚基类。
  3. 虚继承的开销是什么?
    每个含虚继承的子类多一个vbptr,间接访问虚基类成员需要查表计算偏移,性能略低于普通继承。
  4. 虚基类放在对象哪个位置?
    永远在派生类布局末尾,不会在前面。



布局规则,将基类成员变量放到最下面,然后用vbptr代替;
vbptr指向vbtable虚基类表 , 第一行是vbptr相对本身子对象的偏移