总结:对象的内存布局,如果类含有虚函数,则对象的前4个字节是虚表指针,接下来是类中依次定义的数据成员。对象中不包含成员函数的内容。
在C++中,结构体和类都具有构造函数、析构函数和成员函数,两者只有一个区别:结构体的访问控制默认为public,而类的默认访问控制是private。因此,在反汇编中,C++中的结构体与类没有分别,两者的原理相同,只是类型名称不同。
对象的内存布局
先放上示例代码:
class CNumber
{
public:
CNumber()
{
m_nOne = 1;
m_nTwo = 2;
}
int GetNumberOne()
{
return m_nOne;
}
int GetNumberTwo()
{
return m_nTwo;
}
private:
int m_nOne;
int m_nTwo;
};
void main()
{
CNumber Number;
}
在32位下,整型变量的数据大小为4字节, Number对象的内存布局如下:
Number对象在内存中地址为0x0012FF78,该地址处定义了对象Number的各个数据成员,它们分别放在0x0012FF78与0x0012FF7C处。对象Number中先定义的数据成员在低地址处,后定义的数据成员在高地址处,依次排列。对象的大小只包含数据成员,类成员函数属于执行代码,不属于类对象的数据。
根据图9-1可知,凡是属于CNumuber类型的变量,在内存中都会占据8字节的空间,这8字节由两个int类型的数据成员组成,各自占4个字节。从内存布局上看,类与数组非常类似,都是由多个数据元素构成,但类的能力要远远大于数组,任何数据类型(该类的对象自身除外,因为在该类中定义自身对象时形成递归定义且无递归出口,无法确定占用空间大小,但是却可以定义自身类型的指针,因为指针永远占用4字节(限32位机器))都可以在类中定义。
对象长度 = sizeof(数据成员1) + sizeof(数据成员2) + ... + sizeof(数据成员n)
上述公式不完全准确,在三中情况下会出现例外:
- 空类。空类没有数据成员,其长度为1字节。(否则对象会完全不占用内存空间,则空类就无法取得实例对象的地址,this指针失效,因此会不能实例化。而类的定义是由成员数据和成员函数组成,在没有成员数据的情况下还可以有成员函数,因此仍然需要实例化,因此分配了1字节的空间用于实例化。)
- 内存对齐。在VC++6.0中,类和结构体中的数据成员是根据它们在类或结构体中出现的顺序来依次申请内存空间的,由于内存对齐的原因,它们并不一定会像数组那样连续第排列。在为结构体和类中的数据成员分配内存时,结构体中的最大的数据成员类型长度为M,指定的对齐值为N,那么实际对齐值为q = min(M, N),其成员的地址安排在q的倍数上,填充部分采用0xCC进行填充。
例1:
class tagTEST
{
public:
short sShort; //应占2字节内存空间,假设所在地址为0x0012FF74
int nInt; //应占4字节内存空间
};
数据成员sShort的地址为0x0012FF74,类型为short,占2字节内存空间。VC++ 6.0指定的对齐值默认为8,short的长度为2,于是实际的对齐值取较小者2。所以,short被分配在地址0x0012FF74处,此地址是2的倍数,可分配。此时,轮到为第二个数据成员分配内存了,如果分配在sShort后,应在地址 0x0012FF76处,但第二个数据成员为int类型,占4字节内存空间,与指定的对齐值比较后,实际对齐值取int类型的长度4,而地址0x0012FF76不是4的倍数,需要插入两个字节填充,以满足对齐条件,因此第二个数据成员被定义在地址0x0012FF78处,如图9-2所示。
上述原著引用片段,是对象内存布局的具体分析思路和分析过程,总结起来实质上还是q = min(M, N)这个公式,直接以4位对齐值,其实不存在以2为对齐值的过程。
例2:
struct
{
double dDouble; //所在地址:0x0012FF00~0x0012FF08之间,占8字节
int nInt; //所在地址:0x0012FF08~0x0012FF0C之间,占4字节
short sShort; //所在地址:0x0012FF0c~0x0012FF10之间,占2字节
};
上例中结构体成员的总长度为8+4+2=14,按默认的对齐值设置要求,结构体的整体大小要能被8整除,于是编译器在最后一个成员sShort所占内存之后加入2字节空间填补到整个结构体重,使总大小为8+4+2+2=16,这样就满足了对齐的要求。
例3:
struct
{
char cChar; //应占1字节内存空间,如所在地址为0x0012FF00
int nInt; //应占4字节内存空间
short sShort; //应占2字节内存空间
};
以上结构如果还是按照8字节的对齐方式,其布局格式如下所示:
cChar 所在地址:0x0012FF00~0x0012FF04之间,占4字节,对齐nInt
nInt 所在地址:0x0012FF04~0x0012FF08之间,占4字节
sShort 所在地址:0x0012FF08~0x0012FF0C之间,占2字节,另外填充2字节
随后定义的数据成员sShort应该使用6字节的空间数据对齐。VC++ 6.0通过检查发现,结构中最大的类型为nInt数据占4字节空间,于是将对齐值由8调整为4,重新调整后,sShort只需要填充2字节的空白数据就可以实现对其。再次体现了q=min(M, N)工时的使用,对齐用的是最大数据类型和默认对齐值两者中的最小值。
默认对齐值可以在定义结构体时进行调整,VC++ 6.0中可使用预编译指令#pragma pack(N)来调整对齐大小。修改以上示例,调整对齐值为1,如以下代码所示:
例4:
#pragma pack(1)
struct
{
char cChar; //应占1字节内存空间
int nInt; //应占4字节内存空间
short sShort; //应占2字节内存空间
};
调整期对齐值为1后,q=min(M, N)=1,在分配nInt时无需插入空白数据,结构体总长度为7。
当结构体中以数组作为成员时,将根据数组元素的长度计算对齐值,而不是按数组的整体大小去计算,如以下代码所示:
例5:
struct
{
char cChar; //应占1字节内存空间,如所在地址为0x0012FF00
char cArray[4]; //应占4字节内存空间
short sShort; //应占2字节内存空间
}
由于cArray数组内的元素是char类型,所以cChar和cArray的对齐没有缝隙,无需插入空白数据,cChar和cArray在内存中将会占5字节内存,最大类型为short,故对齐值为2,所以在分配sShort时,需要在cChar/cArray的5字节后面插入1字节的数据,其结构布局如下所示:
cChar 所在地址:0x0012FF00~0X0012FF01之间,占1字节
cArray[4] 所在地址:0x0012FF01~0x0012FF06之间,占5字节(这里有1字节的填充)
sShort 所在地址:0x0012FF06~0x0012FF08之间,占2字节
当结构体中出现结构体类型的数据成员时,不会将嵌套的结构体类型的整体长度参与到对齐值的确定中,而是选取内嵌结构体中最大的数据类型的长度参与确定。
这里还有一种情况未予以考虑,可能会对q = min(M, N)构成挑战,后期再补吧,那就是:
struct
{
char cChar;
short sShort;
int nInt;
//这种情况下,cChar和sShort的内存分配,是暂时按照2字节对齐值还是考虑到后续的nInt统一使用4字节,需要用编译器进行验证一下。
}
- 静态数据成员。当类中的数据成员被修饰为静态时,它与静态局部变量类似,存放的位置和全局变量一致。
如果类中定义了虚函数或者类为派生类,对象的内存布局中将含有虚函数表和父类数据成员等数据信息,后期再补。
本总结是对原著内容的凝缩,知识内容非我原创,如需详细知识请购买原著。