C++ 虚继承和虚函数同时存在的对象模型

本文详细探讨了C++中虚继承和虚函数共存时的对象内存布局。通过实例分析了不同情况下的内存分布,包括基类无虚函数、虚基类有无虚函数以及派生类有无虚函数等场景。文章深入讲解了虚函数表、虚基类表以及内存对齐等关键概念,揭示了C++内存管理的复杂性。
摘要由CSDN通过智能技术生成

如果说没有虚函数的虚继承只是一个噩梦的话,那么这里就是真正的炼狱。这个C++中最复杂的继承层次在VS上的实现其实我没有完全理解,摸爬滚打了一番也算得出了微软的实现方法吧,至于一些刁钻的实现方式我也想不到什么理由来解释它,也只算是知其然不知其所以然吧。

 

分2个阶段来探讨:

 

1.      有虚函数的派生类虚继承了没有虚函数的基类;

 

2.      有虚函数的派生类虚继承了有虚函数的基类;

 

1.  基类无虚函数

 

1.1 虚、实基类都没有虚函数

 

这种情况也还算比较简单。因为虚函数表指针一定是会放在最开始的,所以根据猜测也可以知道其大概布局情况。看下面一个简单的例子。

 

#include "stdafx.h"
#pragma pack(8)
class F1{
public:
int f1;
F1():f1(0xf1f1f1f1){}
};
 
class A : virtual F1{
public:
         inta;
         A():a(0xaaaaaaaa){}
         virtual void funA2(){}
         virtual ~A(){}
         virtual void funA(){}
};
 
 
int _tmain(int argc, _TCHAR* argv[])
{
         A aa;
         return 0;
} 


 

这是只有一个虚继承的简单情况,可以看到类A的前4个字节就是虚函数表指针,虚函数表里面有3个项,当然就是分别指向类A的那3个虚函数的指针了(注意是小端序):


 

 

可以看到 funA2() 函数的入口地址是0x00f81028;~A() 函数的入口地址是 0x00f811bd;

funA() 函数的入口地址是 0x00f810aa。

 

为了验证 funA2() 函数的入口地址是否正确(~A()和 funA() 可以用相同的方法验证),打开反汇编窗口:

 

找到地址为 0x00f81028 的地方:

 

 

可以看到 0x00f81028 存放着一条 jmp 指令,跳转到A::funA2() 函数的真正地址:0x00f815d0,我们接着到 0x00f815d0 处看看是什么情况:

 

 

可以看到 0x00f815d0 处的确是A::funA2() 函数开始执行的地方,懂汇编的童鞋应该知道前两句指令就是建立A::funA2() 函数的堆栈~

 

虚函数表指针是最大的,要放在第一位,接下来我们看虚基类表指针:

 

 

可以看到虚基类表里面的两个偏移,一个是 -4,说明类A的起始地址位于虚基类表指针偏移-4 个字节的地方,也就是虚函数表指针的地址了。虚基类表里面的第二项就是第一个虚基类( F1 )相对于虚基类表指针的偏移,是8 字节,可以看到刚好也是 f1f1f1f1 的位置。

 

看了一个简单的情况我们再来看一个稍微复杂的继承情况,虚实多继承,所有的基类仍然都没有虚函数。

#include "stdafx.h"
#pragma pack(8)
class F0{ public: char f0; F0() :f0(0xf0){}};
class F1{ public: int f1; F1():f1(0xf1f1f1f1){} };
class F2{ public: int f2; double f22; F2():f22(0), f2(0xf2f2f2f2){} };
class F3 :virtual F2{ public: int f3; F3(): f3(0xf3f3f3f3){} };
class F4 :virtual F2{ public: int f4; F4(): f4(0xf4f4f4f4){} };
 
class A : virtual F0, F1, virtual F3, F4{
public:
         inta;
         A():a(0xaaaaaaaa){}
         virtual void funA2(){}
         virtual ~A(){}
         virtual void funA(){}
};
 
int _tmain(int argc, _TCHAR* argv[])
{
         A aa;
         return 0;
} 


 

我们知道,只要出现虚继承,那么派生类的内存布局就分为实部和虚部,我们来仔细分析一下这 64 个字节:

1.      派生类的实部:

 

1.1、74  68  ad  00:派生类 A 的虚函数表指针;(距派生类 C 的虚基类表指针 -16 个字节)

 

1.2、cc  cc  cc  cc:为了字节对齐而填充的 4个字节;(距派生类 C 的虚基类表指针 -12 个字节)

 

1.3、f1  f1  f1  f1:实基类 F1 的成员变量,int  f1 = 0xf1f1f1f1;(距派生类 C 的虚基类表指针 -8 个字节)

 

1.4、cc  cc  cc  cc:为了字节对齐而填充的 4个字节;(距派生类 C 的虚基类表指针 -4 个字节)

 

1.5、84  68  ad  00:实基类 F4 和派生类 A 共用的虚基类表指针;(距派生类 C 的虚基类表指针 0 个字节)

 

1.6、f4  f4  f4  f4:实基类 F4 的成员变量,int  f4 = 0xf4f4f4f4;(距派生类 C 的虚基类表指针 +4 个字节)

 

1.7、aa  aa  aa  aa:派生类 A 的成员变量,int  a = 0xaaaaaaaa;(距派生类 C 的虚基类表指针 +8 个字节)

 

实部的内存布局图如下:

 

        

2.      派生类实部和虚部的结合处:

 

cc  cc  cc  cc:这四个字节是为了字节对齐而填充的,同时,这四个字节之前的内容是派生类的实部,之后的内容就是派生类的虚部;(距派生类 C 的虚基类表指针 +12 个字节)

 

3.      派生类的虚部:

 

3.1、f

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值