当前位置: 首页 > news >正文

【C++】菱形继承深度解析+实际内存分布

这里写目录标题

  • 概述
  • 虚继承、虚函数涉及数据结构
  • 菱形继承
  • 虚继承接解决方案
  • C++ 中虚继承 + 多态下vptr、vbptr分布机制
    • MSVC(Microsoft ABI)对象布局
    • GCC

概述

在 C++ 中,虚继承用于解决菱形继承中重复基类的问题。通过 virtual 继承,编译器保证无论通过多少条路径继承,虚基类只会保留一份副本。为了实现这一点,编译器会在中间类(如 Derived1、Derived2)中插入一个 vbptr(虚基类表指针),它指向一张 vbtable,表中记录虚基类在最终派生类对象中的偏移。

当通过中间类指针访问虚基类成员(如 ((Derived1*)&f)->a)时,编译器不能确定虚基类 Base 在对象内的具体位置,必须通过 vbptr → vbtable → 偏移 查表获得最终地址,再加到 this 指针上完成访问。这是虚继承访问的“查表 + 加偏移”机制。

相比之下,若通过 Final* 直接访问虚基类成员,编译器可以直接写死偏移量,无需查表。这也是为什么虚继承的访问路径取决于你是通过哪条继承路径访问的。

虚继承还要求虚基类的构造只能由最派生类完成,确保共享子对象只被构造一次。虚继承对象除了可能包含 vptr(虚函数表指针)用于虚函数多态外,还一定包含 vbptr + vbtable,用于虚基类定位,即使没有虚函数,也依然存在虚继承的结构开销。

虚继承、虚函数涉及数据结构

名称中文叫法用于什么机制存在哪?存什么内容?何时出现?
vtable虚函数表虚函数调用(多态)独立在代码段中虚函数地址数组类有虚函数时生成
vptr虚函数表指针指向 vtable每个含虚函数对象内指向对应的 vtable类含虚函数 → 对象含 vptr
vbtable虚基类偏移表虚继承偏移管理独立在代码段中虚基类的偏移信息类虚继承时生成
vbptr虚基类表指针指向 vbtable每个虚继承对象内指向对应的 vbtable类虚继承 → 对象含 vbptr

菱形继承

🎯 一、虚继承为什么出现?(问题背景)
当一个类 Final 同时继承自 Derived1Derived2,而这两个类又继承自同一个基类 Base,就会形成菱形继承结构:

     Base/    \
Derived1  Derived2\    /Final

问题

  • 如果是普通继承,Final 对象中会包含两份 Base 的副本。
  • 导致:
    • 数据冗余:有两份 Base::value,哪个才是有效的?
    • 二义性:访问 value 时不明确该用哪一个。
    • 资源释放多次:如果 Base 管理资源,可能析构两次导致崩溃。

虚继承接解决方案

🚧 二、虚继承解决方案与代价

class Derived1 : virtual public Base {};
class Derived2 : virtual public Base {};
  • 虚拟继承保证 Base 子对象只存在一份,无论通过多少条路径继承过来。
  • “虚继承为了解决菱形继承重复基类问题,引入了 vbptr(虚基类表指针),通过它在运行时动态定位虚基类子对象。虚基类子对象通常放在派生类对象末尾,因此虚继承对象比普通继承对象更大,访问虚基类成员也多了一次间接寻址。”

🧠 三、内存布局详解(ASCII图)

class Base    { int a; };
class Derived1 : virtual public Base { int b; };
class Derived2 : virtual public Base { int c; };
class Final    : public Derived1, public Derived2 { int d; };
Final 对象 f 内存结构(简化布局):+----------------------+  ← Final对象起始地址 (f)
| Derived1::vbptr      |  ← Derived1 的虚基指针
+----------------------+
| b (Derived1 成员)     |
+----------------------+
| Derived2::vbptr      |  ← Derived2 的虚基指针
+----------------------+
| c (Derived2 成员)     |
+----------------------+
| d (Final 自身成员)    |
+----------------------+
| a (Base::a)          |  ← ✅ 唯一的 Base 子对象
+----------------------+
  • 中间派生类(如 Derived1并不拥有基类(如 Base)的成员变量,也不真正包含 Base 子对象,只有最派生类(如 Final)才真正拥有 Base 子对象的内存空间。
  • 中间派生类的内存布局里没有基类的成员函数吗
    • 虽然 Derived1 声明了继承 Base,但由于是虚继承,它并不会在自己的对象中嵌入 Base 子对象
    • 取而代之的是编译器插入一个 vbptr(虚基类表指针),用于在最终派生类里共享唯一一份 Base 子对象
  • ✅ 2. 那 Base 的成员函数去哪了?
    • 虽然 Derived1 不直接拥有 Base 的数据成员(如 int a)或函数成员(如 f()),但只要 Base::f() 是虚函数,那么:
      • Derived1虚函数表(vtable)仍然会保留 Base::f() 的指针
      • 真正的 Base 数据成员和虚表指针,只有在 最终派生类(如 Final 中才会被实际分配并共享。
      • 只要 Base 的函数是虚函数,它会在 Derived1 的虚表中出现,支持多态调用;但 Base 的实际数据和对象布局,只会在最终类(如 Final)中统一存在一份,由 vbptr 指向。

在虚继承中,由于 Base 只存在一份,它的位置由最派生类决定,不能提前写死偏移。所以中间类会插入一个 vbptr,指向虚基类表 vbtable,表中记录虚基类在最终对象中的偏移。访问时,先通过 vbptr 查出 Base 的偏移,加到 this 指针上,最终定位到 Base 成员。这就是虚继承访问路径的“查表 + 加偏移”机制。


🏗️ 五、构造函数执行顺序图(虚继承)

class Base    { Base()    { cout << "Base\n"; } };
class D1 : virtual public Base { D1() { cout << "D1\n"; } };
class D2 : virtual public Base { D2() { cout << "D2\n"; } };
class Final : public D1, public D2 { Final() { cout << "Final\n"; } };int main() { Final f; }Base
D1
D2
Final 

C++ 中虚继承 + 多态下vptr、vbptr分布机制

#include <iostream>
using namespace std;class A {
public:virtual void f();  // ✅ 虚函数 ⇒ vptrint a = 1;
};class B : virtual public A {
public:virtual void g();  // ✅ 虚函数 ⇒ vptrint b = 2;
};class C : virtual public A {
public:virtual void h();  // ✅ 虚函数 ⇒ vptrint c = 3;
};class D : public B, public C {
public:virtual void i();  // ✅ 虚函数 ⇒ vptrint d = 4;
};int main() {A a;B b;C c;D d;cout << "A: " << sizeof(A) << endl;cout << "B: " << sizeof(B) << endl;cout << "C: " << sizeof(C) << endl;cout << "D: " << sizeof(D) << endl;return 0;
}// ✅ 实现部分(必须加上)
void A::f() {}
void B::g() {}
void C::h() {}
void D::i() {}

实验发现,不同编译器下优化策略不一样

MSVC(Microsoft ABI)对象布局

A对象; 16字节

A a;
// 内存布局
8字节:a的虚表指针
4字节:成员变量
4字节:padding

image.png

B对象:40字节

B b;
// 内存布局
8字节:b的虚表指针
8字节:b的虚基表指针
4字节:b的成员变量
4字节:padding
8字节:a的虚表指针
4字节:a的成员变量
4字节:padding

image.png

C对象和B对象一样;

D:72字节 = b:24+c:24+d:8 + a:16

D d;
// 内存布局
8字节:b的虚表指针
8字节:b的虚基表指针
4字节:b的成员变量
4字节:padding
8字节:a的虚表指针
4字节:a的成员变量
4字节:padding

image.png

发现MVSC下c d成员竟然没优化到一起(这点不知道为啥???);

  • d直接访问abcd的成员,全是通过首地址+偏移访问;
  • D 自身通常不会额外持有一个独立的虚表指针(vptr)。其虚函数 D::i() 会被编译器优化地插入到继承路径中的某个已有虚表中(如 BC 的虚表)。因此,在内存布局中,你会看到 D 对象拥有 BCA 各自的 vptr 和 vbptr,但没有单独的 D::vptr,这体现了 C++ 编译器在虚函数表布局上的重用与优化策略。
  • ✅ D 如何访问 f()
    • D → B (vbptr_B) → offset_to_A → A 子对象 → vptr_A → f()
    • B 自己并不会再“拷贝” A::f() 进自己的虚表中。
  • D 直接访问 A::a
    • 编译器知道 A 在 D 中的偏移(比如 0x38),会直接用偏移访问;
  • D 通过 B 或 C 的方式访问 A
    • image.png
    • image.png

GCC

🌿 1. 类 A(普通虚函数类)

offset 0x00: vptr_A
offset 0x08: int a
offset 0x0C: padding
sizeof(A) = 16

B对象 (32字节) - 虚拟继承A:> 虽然 B 虚继承 A,在多继承中只保留一份 A,但 B 单独存在时还是附带一份自己的 A 虚基副本

B 对象(大小:32 bytes)
┌──────────── offset 0x00[0x00..0x07] vptr_B       ← B 的虚函数表指针
│ [0x08..0x0B] int b        ← 自己的成员
│ [0x0C..0x0F] padding      ← 对齐填充
├──────────── offset 0x10
│ A 虚基子对象(附带)      ← vptr_A + a
│   [0x10..0x17] vptr_A     ← 虚基类 A 的虚表指针
│   [0x18..0x1B] int a
│   [0x1C..0x1F] padding
└──────────── sizeof(B) = 32

C对象 (32字节) - 虚拟继承A:和 B 一样,虚继承 A;布局、大小、vptr 结构都完全一样,只需将 b 改为 cvptr_B 改为 vptr_C

C 对象(大小:32 bytes)
┌──────────── offset 0x00[0x00..0x07] vptr_C       ← B 的虚函数表指针
│ [0x08..0x0B] int c        ← 自己的成员
│ [0x0C..0x0F] padding      ← 对齐填充
├──────────── offset 0x10
│ A 虚基子对象(附带)      ← vptr_A + a
│   [0x10..0x17] vptr_A     ← 虚基类 A 的虚表指针
│   [0x18..0x1B] int a
│   [0x1C..0x1F] padding
└──────────── sizeof(C) = 32

🧩 D 对象内存布局(按字节)
假设在 64 位系统下,指针大小为 8 字节。
🎯 关键点(GCC Itanium):

  • 没有 vbptr
  • A 虚基类在 D 的尾部(只保留一份)
  • 虚基偏移存在于 vtable 的负索引槽中
  • 所有虚函数表指针放在子对象头部
  • D::d 会被尽可能塞入空隙区(如 C 的 padding)

在 GCC(Itanium ABI)下,B 即使虚继承了 A,它的对象中不包含显式的“虚基表指针(vbptr)”字段。

D 对象(大小:48 bytes)
┌──────────── offset 0x00
│ B 子对象(16 bytes)
│   [0x00..0x07] vptr_B-in-D   ← 指向 D 的 vtable(B 视图)
│   [0x08..0x0B] int b = 2[0x0C..0x0F] padding
├──────────── offset 0x10
│ C 子对象(16 bytes)
│   [0x10..0x17] vptr_C-in-D   ← 指向 D 的 vtable(C 视图)
│   [0x18..0x1B] int c = 3[0x1C..0x1F] int d = 4     ← D 的成员变量被紧贴在此处(复用 C 尾部空间)
├──────────── offset 0x20
│ A 虚基子对象(16 bytes)
│   [0x20..0x27] vptr_A-in-D   ← 指向 D 的 vtable(A 视图)
│   [0x28..0x2B] int a = 1[0x2C..0x2F] padding
└──────────── sizeof(D) = 48
http://www.dtcms.com/a/353678.html

相关文章:

  • 【题解】Codeforces Round 1045 (Div. 2) Problem.B ~ Problem.E
  • KANO 模型:功能不是“加一分”,而是“分五类”
  • YOLO-yaml/pt模型文件的差异
  • GitFlow工作流
  • 铠德科技为您详解静电与温冲背后的隐形损失
  • 下一代防火墙
  • nestjs 导出excel
  • JoyAgent-JDGenie开源多智能体系统详解:架构、部署与企业级应用案例
  • day22 回溯算法part01
  • Day6--HOT100--238. 除自身以外数组的乘积,41. 缺失的第一个正数,73. 矩阵置零
  • 前端漏洞(下)- ClickJacking漏洞
  • 指针 (六):sizeof和strlen细节强化之“做题篇”
  • stl--std::map
  • Java 使用Jna 调用 C# dll文件踩到的坑
  • SpringBoot整合Redis:从入门到实战的完整指南
  • 【Linux 小实战】自定义 Shell 的编写
  • LCD 上显示字符
  • zookeeper-集群扩缩容
  • 稳敏双态融合架构--架构师的练就
  • banner这个文件是怎么请求到后端数据的
  • Rust:引用
  • Vue-24-利用Vue3的element-plus库实现树形结构数据展示
  • Autodesk Maya 2026.2 全新功能详解:MotionMaker AI 动画、LookdevX 材质增强、USD 工作流优化
  • 在MiniOB源码中学习使用Flex与Bison解析SQL语句-第二节
  • 【Linux】正则表达式学习记录
  • FFMPEG api使用
  • 从disable_cost到disabled_nodes,最小代价预估质的飞跃
  • nestjs日志(nest-winston)
  • pyecharts可视化图表-tree:从入门到精通
  • Linux 系统调优与CPU-IO-网络内核参数调优