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

【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.xdnf.cn/news/18858.html

相关文章:

  • 2025.8.27链表_链表逆置
  • 科技赋能生态,智慧守护农林,汇岭生态开启农林产业现代化新篇章
  • TensorFlow 面试题及详细答案 120道(21-30)-- 模型构建与神经网络
  • 斯塔克工业技术日志:用基础模型打造 “战甲级” 结构化 AI 功能
  • uniapp H5禁止微信浏览器长按出菜单,只针对图片
  • 全球首款Al勒索软件PromptLock:跨平台攻击新威胁, Windows/macOs/Linux均受影响
  • 【生产事故处理--kafka日志策略保留】
  • 深入解析达梦数据库:模式分类、状态管理与实操指南
  • 【数据分享】安徽省安庆市地理基础数据(道路、水系、铁路、行政边界(含乡镇)、DEM等)
  • 如何用Renix实现网络测试自动化: 从配置分离到多厂商设备支持
  • WebConfig的登录与放行
  • 对比视频处理单元(VPU)、图形处理器(GPU)与中央处理器(CPU)
  • 前端-从零开始在本机部署一个前端项目
  • 流程控制语句(1)
  • Dify 从入门到精通(第 59/100 篇):Dify 的自动化测试(进阶篇)
  • 野火STM32Modbus主机读取寄存器/线圈失败(一)-解决接收中断不触发的问题
  • 嵌入式-定时器的时基单元,自制延迟函数-Day21
  • AI驱动的前端性能优化:从监控到自动化修复
  • C# 字符和字符串
  • 《信息检索与论文写作》实验报告三 中文期刊文献检索
  • 【算法速成课1 | 题解】洛谷P3366 【模板】最小生成树 MST(Prim Kruskal)
  • GitHub 宕机自救指南:保障开发工作连续性
  • Android中点击链接跳转到对应App页面的底层原理
  • 信号线串扰仿真
  • 【C++】类和对象 --- 类中的6个默认成员函数
  • 达梦数据库-控制文件 (二)
  • 如何在开发工具中使用钉钉MCP
  • 数据结构:在堆中插入元素(Inserting In a Heap)
  • 深度学习-----详解MNIST手写数字数据集的神经网络实现过程
  • Magicodes.IE.Pdf 生成导出PDF文件 bytes Stream FileStreamResult 下载