扫盲类的内存布局情况

本文详细解析了C++中类实例在内存中的布局方式,包括成员变量的对齐规则、虚函数表的作用及布局、继承与多重继承情况下类实例的具体布局等。

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


类的实例在内存中的布局?

我们在高级语言上写了一个类,它代表了我们对一事物的描述;如果我们不了解它的实例在内存在如何布局,这也不要紧,这与使面向对象的一大特色之一封装不蒙而合,对上层屏蔽了细节,你不了解它也可以,它能好好工作;但是随着对上层的了解,我想揭开它的面孔 ..

 

扫盲之一:内存对齐 ,如果不知道他说的内容可以去网上搜一搜;

 

看了网上的东东,给了我们一些概念上的东西,那我们在 VC 上动动手(我使用的是 VS2005, 中文)

 

1 VC 在哪里设置对齐

VS2005中文版,对齐方式设置.bmp

 

2. 使用 #pragma pack 来设置

以下来自 MSDN

Specifies packing alignment for structure, union, and class members.

#pragma pack( [show ]| [ push |pop ][, identifier ], n )

 

例子:查看一个简单的类在内存中的情况

1.       我们选择的对齐方式是默认,(在我的机器上为 4 字节对齐方式)

如下一个类他会是什么一个情况呢?

class Test

{

public :

         int               GetBuy () const { return m_iBuy ; }

 

private :

         int               m_iBuy ;     

         char          m_cType ;

         int               m_iSell ;

};

先给出答案,再分析,它表现的将会如下所示

1>class Test size(12):

1> +---

1> 0 | m_iBuy

1> 4 | m_cType

1>   | <alignment member> (size=3)

1> 8 | m_iSell

1>   +---

 

看见没,中间的 char 后面被填充了 3 个字节,这其实就是根据对齐方式,依次布局在内存中,不够得被填充了,

注:上面那段其实是 VC 生成的,要输出这个额外的信息,需要使用命令:

/d1 reportAllClassLayout

用法之一:

VS2005中文版,输出布局方式命令使用.bmp

加入之后你在编译时, output 窗口变可以产生。

 

如何 Test 被修改成如下方式时呢?

class Test

{

public :

         int               GetBuy () const { return m_iBuy ; }

 

private :

         int               m_iBuy ;     

         char          m_cType ;

         char          m_cType1 ;

         int               m_iSell ;

};

 

更进一步下面呢?

#include <string>

 

class Test

{

public :

         int               GetBuy () const { return m_iBuy ; }

 

private :

         int               m_iBuy ;     

         char          m_cType ;

         short          m_cType1 ;

         std ::string str ;

         char          m_cType2 ;

         int               m_iSell ;

};

不要认为这些不重要,也许在面试中遇到了,你正确的回答,那一定能给你加分,当然正确的理解,在某些时候对你也很有帮助, 1. 合理的布局能帮你省掉空间,尤其在嵌入式中,内存很紧张的情况,当然这是一个优化的策略,一般的 C++ 程序中,合理的表达描述或许更加重要一些, 2. 理解这些在逆向中也很重要

1>class Test size(48):

1> +---

1> 0 | m_iBuy

1> 4 | m_cType

1>   | <alignment member> (size=1)

1> 6 | m_sType1

1> 8 | ?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@ str

1>40 | m_cType2

1>   | <alignment member> (size=3)

1>44 | m_iSell

看到没,如果我们队std::string 没有好好的理解的话,那又需要看看文档,了解它的实现方式,呵呵撤远了

 

也许我们需要更加的深入一些,如果有了虚函数呢

class Test

{

public :

         int               GetBuy () const { return m_iBuy ; }

        

         virtual bool GetInfo () const { return 0; }

private :

         int               m_iBuy ;     

         char          m_cType ;

         int               m_iSell ;

};

 

1>class Test size(16):

1> +---

1> 0 | {vfptr}

1> 4 | m_iBuy

1> 8 | m_cType

1>   | <alignment member> (size=3)

1>12 | m_iSell

1>   +---

多出一个虚表 vftable ,里面有 vfptr (virture function pointer)

含有虚函数时,快速监视.bmp

 

既然知道了怎么多我们看看他在内存中的情况

Test 修改如下:

class Test

{

public :

         Test () : m_iBuy (99), m_cType (100),m_iSell (101)

         {

 

         }

 

         int               GetBuy () const { return m_iBuy ; }

 

         virtual bool GetInfo () const { return 0; }

         virtual bool GetType () const { return 0; }

private :

         int               m_iBuy ;     

         char          m_cType ;

         int               m_iSell ;

};

我们在构造时初始化,在 C++ 中,变量并没被初始化,由上图可以看出,因此我们必须将这些值进行初始化,养成好的习惯,否则是指针之类的会很麻烦了,也许你的程序很多时候是正常运行的,有时确崩溃了。

99  =  0x63,         100  = 0x64 101 = 0x65

打开 VC 内存,如下图

含有虚函数时,内存中的布局.bmp

 

看到没,我们通过地址找到了 Test 实例在内存中的位置,看到了 63 64 65 。前面有两个数字,不用想,肯定是用来描述虚表的, /(^o^)/~ 但是为啥是 63 00 00 00 呢,占四字节我们可以理解,咋跑到头上去了, 99 不是 0x 00 00 00 63 么?

 

字节 的存储顺序:原理时间情况和 CPU 有关

Intel 体系的芯片使用的编码方式属于 Little-Endian, 而另外一些使用 Big-Endian

 

Big-Endian       高位字节存入低地址,低位字节存入高地址,依次排列

Little-Endian   低位字节存入低地址,高位字节存入高地址,反序排列

 

呵呵知道地址了,我小试一下改改值看看。代码如下:

int _tmain (int argc , _TCHAR * argv [])

{

         Test kTest ;

         int iBuy = kTest .GetBuy ();

 

         int * pHack = NULL ;

         pHack = reinterpret_cast <int *>(0x0012FF58);

                  

         *pHack = 0x00006789;

        

         return 0;

}

呵呵,在 return 0 时下断点,值果然被修改,内存中果然变成了 89 67 00 00 ,并被显示成红色

 

继承的相应的内存布局呢

class Test

{

public :

         Test () : m_iBuy (99), m_cType (100),m_iSell (101)

         {

 

         }

 

         int               GetBuy () const { return m_iBuy ; }

 

         virtual bool GetInfo () const { return 0; }

         virtual bool GetType () const { return 0; }

private :

         int               m_iBuy ;     

         char m_cType ;

         int               m_iSell ;

};

 

class TestBoy : public Test

{

public :

         TestBoy (): m_iAge (15)

         {

 

         }

 

protected :

         int m_iAge ;

};

 

顾使用前面的方法,看一下

1>class Test size(16):

1> +---

1> 0 | {vfptr}

1> 4 | m_iBuy

1> 8 | m_cType

1>   | <alignment member> (size=3)

1>12 | m_iSell

1> +---

1>Test::$vftable@:

1> | &Test_meta

1> |  0

1> 0 | &Test::GetInfo

1> 1 | &Test::GetType

1>Test::GetInfo this adjustor: 0

1>Test::GetType this adjustor: 0

1>class TestBoy size(20):

1> +---

1> | +--- (base class Test)

1> 0 | | {vfptr}

1> 4 | | m_iBuy

1> 8 | | m_cType

1>   | | <alignment member> (size=3)

1>12 | | m_iSell

1> | +---

1>16 | m_iAge

1> +---

1>TestBoy::$vftable@:

1> | &TestBoy_meta

1> |  0

1> 0 | &Test::GetInfo

1>   1 | &Test::GetType

 

由此可以看出先加父类放入前边,后面再加自己的数据

如果自己也有虚函数呢?那又是怎么处理的?

 

lass Test

{

public :

         Test () : m_iBuy (99), m_cType (100),m_iSell (101)

         {

 

         }

 

         int               GetBuy () const { return m_iBuy ; }

 

         virtual bool GetInfo () const { return 0; }

         virtual bool GetType () const { return 0; }

private :

         int               m_iBuy ;     

         char m_cType ;

         int               m_iSell ;

};

 

class TestBoy : public Test

{

public :

         TestBoy (): m_iAge (15)

         {

 

         }

 

         virtual bool GetInfo () const { return 1; }

         virtual int   GetAge () const { return m_iAge ; }

 

protected :

         int      m_iAge ;

         short m_sInfo ;

};

 

我们输出其结果看一看,它究竟会采用什么样的策略来处理

1>class Test size(16):

1> +---

1> 0 | {vfptr}

1> 4 | m_iBuy

1> 8 | m_cType

1>   | <alignment member> (size=3)

1>12 | m_iSell

1> +---

1>Test::$vftable@:

1> | &Test_meta

1> |  0

1> 0 | &Test::GetInfo

1> 1 | &Test::GetType

1>Test::GetInfo this adjustor: 0

1>Test::GetType this adjustor: 0

1>class TestBoy size(24):

1> +---

1> | +--- (base class Test)

1> 0 | | {vfptr}

1> 4 | | m_iBuy

1> 8 | | m_cType

1>   | | <alignment member> (size=3)

1>12 | | m_iSell

1> | +---

1>16 | m_iAge

1>20 | m_sInfo

1>   | <alignment member> (size=2)

1> +---

1>TestBoy::$vftable@:

1> | &TestBoy_meta

1> |  0

1> 0 | &TestBoy::GetInfo

1> 1 | &Test::GetType

1> 2 | &TestBoy::GetAge

1>TestBoy::GetInfo this adjustor: 0

1>   TestBoy::GetAge this adjustor: 0

由此可以虚表还是使用父类的,但是虚表的内容变化了

这样我就明白了继承关系,是如何处理的

但是。。很少用的。。一些语言没有的。。多重继承呢?

多重继承又是很容易出错的,那我们也用同样的方法来分析一下

 

class TestGirl

{

public :

         TestGirl () : m_iBuy (99), m_cType (100),m_iSell (101)

         {

 

         }

 

         int               GetBuy () const { return m_iBuy ; }

 

         virtual bool GetInfo () const { return 0; }

         virtual bool GetType () const { return 0; }

private :

         int               m_iBuy ;     

         char m_cType ;

         int               m_iSell ;

};

 

class TestBoy

{

public :

         TestBoy (): m_iAge (15)

         {

 

         }

 

         virtual bool GetInfo () const { return 1; }

         virtual int   GetAge () const { return m_iAge ; }

 

protected :

         int      m_iAge ;

         short m_sInfo ;

};

 

class Test : public TestBoy , public TestGirl

{

public :

         virtual int GetMoney () { return 100; }

 

private :

         char m_cMoney ;

};

 

1>class TestGirl size(16):

1> +---

1> 0 | {vfptr}

1> 4 | m_iBuy

1> 8 | m_cType

1>   | <alignment member> (size=3)

1>12 | m_iSell

1> +---

1>TestGirl::$vftable@:

1> | &TestGirl_meta

1> |  0

1> 0 | &TestGirl::GetInfo

1> 1 | &TestGirl::GetType

1>TestGirl::GetInfo this adjustor: 0

1>TestGirl::GetType this adjustor: 0

1>class TestBoy size(12):

1> +---

1> 0 | {vfptr}

1> 4 | m_iAge

1> 8 | m_sInfo

1>   | <alignment member> (size=2)

1> +---

1>TestBoy::$vftable@:

1> | &TestBoy_meta

1> |  0

1> 0 | &TestBoy::GetInfo

1> 1 | &TestBoy::GetAge

1>TestBoy::GetInfo this adjustor: 0

1>TestBoy::GetAge this adjustor: 0

1>class Test size(32):

1> +---

1> | +--- (base class TestBoy)

1> 0 | | {vfptr}

1> 4 | | m_iAge

1> 8 | | m_sInfo

1>   | | <alignment member> (size=2)

1> | +---

1> | +--- (base class TestGirl)

1>12 | | {vfptr}

1>16 | | m_iBuy

1>20 | | m_cType

1>    | | <alignment member> (size=3)

1>24 | | m_iSell

1> | +---

1>28 | m_cMoney

1>   | <alignment member> (size=3)

1> +---

1>Test::$vftable@TestBoy@:

1> | &Test_meta

1> |  0

1> 0 | &TestBoy::GetInfo

1> 1 | &TestBoy::GetAge

1> 2 | &Test::GetMoney

1>Test::$vftable@TestGirl@:

1> | -12

1> 0 | &TestGirl::GetInfo

1> 1 | &TestGirl::GetType

 

由此可见,基类的还是被顺序的放在前面,单虚表的内容有所替换,从 Test_meta 中可以看到。

 

反过来我们可以理解继承的调度关系;

 

在实际应用中,也发现过这样的问题: 1. 对齐方式不一样,导致函数后面栈平衡不了, 2. 数据清零,拿到指针然后一个 memset ,全部为零,结果破坏虚表; 这种问题在编译的时候发现不了,都是运行时错误

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值