C++虚继承中的虚基类表

本文深入探讨C++中的虚继承机制,特别是虚基类表的作用和内存布局。通过实例展示了虚继承如何解决多重继承时的二义性和空间浪费问题。虚基类表存储了虚基类相对于实例对象的偏移,使得多个子类能共享同一份虚基类实例,从而优化内存使用。同时,文章提供了访问虚基类成员的代码示例,进一步阐述了虚继承的实现原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

虚继承主要解决多重继承会在子类中存在多份拷贝的问题,这不仅浪费空间,而且存在二义性。

在之前的 C++ 继承中已经说过虚继承基本概念,这里不再赘述。这篇文章主要探究虚继承的原理。文章中多处给出了类实例对象的内存布局,查看其内存布局时,使用 VS 工具 /d1 reportAllClassLayout 进行查看,关于这个工具的详细介绍,请点击这里

虚继承的实现原理

虚继承的底层实现一般与编译器相关,一般会通过虚基类表指针虚基类表实现,先看如下这个程序:

#pragma pack(1)

class A
{
public:
    int a;
};

class B : virtual public A
{
public:
    int b;
};

class C : virtual public A
{
public:
    int c;
};

其内存布局图如下所示:

> cl /d1 reportSingleClassLayoutB CppTest.cpp
class B size(12):
        +---
 0      | {vbptr}
 4      | b
        +---
        +--- (virtual base A)
 8      | a
        +---

B::$vbtable@:
 0      | 0
 1      | 8 (Bd(B+0)A)
vbi:       class  offset o.vbptr  o.vbte fVtorDisp
               A       8       0       4 0
----------------------------------------------------
> cl /d1 reportSingleClassLayoutC CppTest.cpp
class C size(12):
        +---
 0      | {vbptr}
 4      | c
        +---
        +--- (virtual base A)
 8      | a
        +---

C::$vbtable@:
 0      | 0
 1      | 8 (Cd(C+0)A)
vbi:       class  offset o.vbptr  o.vbte fVtorDisp
               A       8       0       4 0

从中可以看出,每个虚继承的子类都有一个虚基类表指针 vbptr(virtual base table pointer,占用一个指针的大小,32 位下为4字节,64 位为 8 字节)和该指针指向一个虚基类表(额外的空间,不占用实例对象的空间),上面的程序中 B 虚继承 A,那么 B 类中将会有一个虚基类表指针 vbptr,C 类虚继承 A 类,也是一样。该 vbptr 指针指向一个虚基类表,关于虚基类表下面再详细说。

现在我们再看一个例子:

#pragma pack(1)

class A
{
public:
    int a;
};

class B : virtual public A
{
public:
    int b;
};

class C : virtual public A
{
public:
    int c;
};

class D : public B, public C
{
public:
    int d;
};

这次我们使用类 D 多重继承类 B 和类 C(B 和 C 都是虚继承 A),看一下内存布局图:

> cl /d1 reportSingleClassLayoutD CppTest.cpp
class D size(24):
        +---
 0      | +--- (base class B)
 0      | | {vbptr}
 4      | | b
        | +---
 8      | +--- (base class C)
 8      | | {vbptr}
12      | | c
        | +---
16      | d
        +---
        +--- (virtual base A)
20      | a
        +---

D::$vbtable@B@:
 0      | 0
 1      | 20 (Dd(B+0)A)

D::$vbtable@C@:
 0      | 0
 1      | 12 (Dd(C+0)A)
vbi:       class  offset o.vbptr  o.vbte fVtorDisp
               A      20       0       4 0

需要强调的是,虚基类依旧会在子类中存在拷贝,但仅仅只存在一份;我们看到虚基类 A 仅仅在类 D 的实例对象中存在一份,也就是上面内存布局里的 virtual base A。当继承的子类被当作父类继承时,虚基类表指针也会被继承;我们上面已经测试过 B 虚继承 A,C 虚继承 A 都会在各自的实例对象中多出一个虚基类表指针 vbptr 指向一个虚基类表,现在 D 类继承与 B 类和 C 类,其类中的虚基类表指针也会被继承。我们也可以从上面的内存布局中看到 D 中类 B 和类 C 中都没有类 A 的成员而是都多了一个虚基类表指针 vbptr,而且类 D 中只有一份虚基类 A 的成员,所以奥秘就在 类 B 和类 C 中的虚基类表指针 vbptr 上。

我们再看类 B 和类 C 的虚基类表的结构,该表中记录了虚基类 A 与本类的偏移地址,因为现在虚基类 A 在类 D 的实例对象中只能存在一份,所以类 B 和类 C 中只要通过虚基类表中给的偏移量访问就能访问到虚基类 A。上面的内存布局中,仅存的一份虚基类 A 的地址相较于类 D 的实例对象的首地址偏移量为 20,而类 B 的 vbptr 相较于类 D 的实例对象的首地址偏移量为 0,所以类 B 中想要访问虚基类 A 需要偏移 20 个字节,所以虚基类表中记录的与虚基类 A 的偏移量为 20,而类 C 的访问虚基类 A 的偏移量为 12,也是同样可以这样计算出来。这样就可以找到虚基类 A 的数据成员,而且类 B 和类 C 对虚基类 A 的数据的修改都是在同一地址,这样就不用像普通菱形继承那样维持着公共基类(也就是这里说的虚基类)的两份同样的拷贝了,同时也能节省一点空间。

我们可以联系到多态中虚函数表,这两者有一定的相似之处,不过虚基类表存储的时虚基类相对当前实例对象的偏移,而虚函数表则存储的是虚函数地址。

到这里,关于虚基类表的探究就已经结束了,我们对虚继承的原理也有了一定的认识,还可以自己在程序中访问虚基类表,其基本思路如下:

D* d = new D;
std::cout << "[0] ->" << *(int*)(*(int*)d) << std::endl;  
std::cout << "[1] ->" << *(((int*)(*(int*)d)) + 1)<< std::endl; // 偏移量20
std::cout << "b value  : " << *((int*)d + 1) << std::endl;
std::cout << "[0] ->" << *(int*)(*((int*)d + 2)) << std::endl;  
std::cout << "[1] ->" << *((int*)(*((int*)d + 2)) + 1) << std::endl; // 偏移量12  
std::cout << "c value  : " << *((int*)d + 3) << std::endl;  
std::cout << "d value  : " << *((int*)d + 4) << std::endl;
std::cout << "a value  : " << *((int*)d + 5) << std::endl;

参考:
C++ 虚继承实现原理(虚基类表指针与虚基类表)
【C++拾遗】 从内存布局看C++虚继承的实现原理

<think>好的,用户遇到了Git合并时本地更改会被覆盖的问题,需要解决方案。根据之前的引用资料,解决方法主要有commit、stash和revert三种。首先,我得确认用户的具体情况,比如他们的本地更改是否已经完成,是否需要保留,或者是否可以丢弃。用户可能不太清楚这三种方法的适用场景,需要详细解释每个选项的步骤和优缺点。比如,commit适用于需要保留更改的情况,stash适合临时保存以便后续恢复,而revert则是直接撤销更改,适用于不需要保留的情况。需要结构清晰,分点说明,每个方法都给出操作步骤和适用场景,帮助用户理解哪种情况最适合他们。还要注意使用中文,避免技术术语过于复杂,让用户容易理解。同时,生成相关问题,可能涉及如何恢复stash的内容,或者如何避免频繁冲突等,扩展用户的知识点。另外,要确保LaTeX格式正确,但在这个问题中可能用不上数学公式,所以主要关注代码块和引用标识。检查引用是否正确,比如引用[^1]和[^2]的来源是否相关,用户提供的引用内容是否准确对应解决方案。 最后,确保回答真实可靠,操作步骤准确无误,避免误导用户。可能还需要提醒用户根据具体情况选择方法,比如如果有未完成的实验性代码,stash可能是更好的选择,而如果是确定要保留的修改,则应该commit。</think>要解决Git合并时本地更改被覆盖的问题,可根据不同场景选择以下三种方法: ### 一、提交本地更改(commit) **适用场景**:当本地更改已测试通过且需要保留到版本历史中。 **操作步骤**: 1. 将改动添加到暂存区: ```bash git add . # 添加所有文件 # 或指定文件 git add file1.txt file2.py ``` 2. 提交更改并添加描述: ```bash git commit -m "完成用户登录模块优化" ``` 3. 执行合并操作: ```bash git merge origin/main ``` **优点**:保留完整的开发记录,便于后续追溯[^1]。 ### 二、暂存更改(stash) **适用场景**:需要临时保存未完成的修改(如调试代码),优先处理合并操作。 **操作步骤**: 1. 保存当前工作状态: ```bash git stash # 可选添加备注 git stash save "用户登录模块调试代码" ``` 2. 查看暂存记录列表: ```bash git stash list ``` 3. 合并远程分支: ```bash git merge origin/main ``` 4. 恢复暂存内容: ```bash git stash pop # 恢复并删除最近一次暂存 # 或指定特定暂存 git stash apply stash@{1} ``` **优点**:保持工作目录整洁,特别适合处理紧急合并任务[^2]。 ### 三、撤销本地更改(revert) **适用场景**:本地修改可丢弃(如实验性代码或错误修改)。 **操作步骤**: 1. 撤销所有未提交的修改: ```bash git checkout -- . # 全部文件 # 或指定文件 git checkout -- file1.txt ``` 2. 执行合并操作: ```bash git merge origin/main ``` **风险提示**:此操作不可逆,需确认文件无需保留。 --- ### 对比决策表 | 方法 | 保留记录 | 可恢复性 | 适用阶段 | |---------|----------|----------|------------------| | commit | ✅ | ✅ | 功能开发完成时 | | stash | ❌ | ✅ | 开发中途临时切换 | | revert | ❌ | ❌ | 放弃无效修改 | ### 扩展应用场景 - 当存在多个暂存时,可用`git stash list`查看记录,通过`git stash apply stash@{n}`精准恢复 - 合并后出现冲突时,可使用`git mergetool`可视化工具解决冲突 - 长期分支开发推荐使用`git rebase`保持提交历史线性
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

code_peak

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值