汇编学习之路之对象的内存布局

结构体和类都是抽象的,在真实的世界中他们只可以表示某个群体,无法确定这个群体中的某个独立个体,而对象则是群体中独立存在的个体。

由于类是抽象概念,当两个类的特征相同时,他们之间应该是相等的关系。而对象是实际存在的,即使他们之间所包含的数据不同,也不能视为同一个对象,这就如同人类中的两个实体对象,即使他们是双胞胎,也不能因为他们的外貌等各方面的特征都相同就将他们描述成同一个人。看看下面的代码:

class  Number
{
public:
	Number()
	{
		num_one = 1;
		num_two = 2;
	}

	int GetNumberOne()
	{
		return num_one;
	}

	int GetNumberTwo()
	{
		return num_two;
	}

private:
	int num_one;
	int num_two;
};

///----主函数
int main()
{
	Number vNumber;
	return 0;
}
在32位下,整型变量的数据大小为4字节。 在Number类中,先定义的数据成员在低地址处,后定义的数据成员在高地址处,依次排列,对象的大小只包含数据成员,类成员函数属于执行代码,不属于类对象的数据

看看监视窗口数据:


Number类型的数据都将占有8字节的空间,这8字节的空间由两个数据成员组成,他们都是int类型,各自的数据长度为4字节。

从内存布局上看, 类与数组非常相似,都是由多个元素组成的,但类的能力远远大于数组。类的成员数据非常广泛,除本身的对象外,任何已知的数据类型都可以在类中定义。

为什么在类中不能定义自身的对象? 因为类需要在申请内存的过程中计算出自身的实际大小,以用于实例化。如果在类中定义了自身的对象, 在计算各数据成员的长度时,又会回到自身,这样就形成了递归,而这个递归定义并没有出口,是一个无限的循环递归定义,所以不能以自身对象作为类的成员,但是自身类型的指针除外,因为任何类型的指针在32位下所占用的内存大小始终为4字节,等同于一个常量值,因此将其作为类的数据成员不会影响长度的计算。因此,我们可以得出下面的计算公式来对象的长度:

对象长度 = sizeof(数据成员1) + sizeof(数据成员2) +…+sizeof(数据成员n)


上面的这个公式,看起来是没有问题的,但是,对象的大小计算远远没有这么简单的。即使类中没有继承和函数定义,仍然有3中特殊情况能推翻该公式:空类,内存对齐,静态数据成员。当出现这三种情况时,使用此公式计算得到的对象长度与对象的实际长度不相符。简单介绍下这三种情况:

◆空类。空类中没有数据成员,按照上面的公式计算得出的对象长度为0。类型长度为0.则此类的对象不占据内存空间。而实际情况是空类的长度是1字节。如果对象完全布展内存空间,那么空类就无法获得实例对象的地址,this指针失效,因此不能被实例化。而类的定义是有成员数据和成员函数组成的,在没有成员数据的情况下,还可以有成员函数,因此仍然需要实例化,分配了1字节的空间用于类的实例化,这1字节的数据并没有被使用。

◆内存对齐。由于内存对齐的原因,对象的数据成员不一定会像数组那样连续排列。由于数据;类型的不同,因此在内存空间的大小也不相同,在申请内存时,会遵循一定的规则。

在为结构体或类的数据成员分配内存时,结构体中的当前数据成员类型长度为M,指定的对齐值是 N,那么实际对齐值q = min(M,N),其成员的地址安排在 q 的倍数上。

例如下面的代码:

struct
{
	short sShort;	///---应占2字节的内存空间,假设所在地址为: 0x0012FF74
	int nInt;			///---应占4字节内存空间
};
数据成员sShort的地址为:0x0012FF74,类型为short ,占2字节内存空间。VC++6.0指定的对齐值默认为8,short 的长度为2,于是实际的对齐值取较小者2.所以,short被分配在地址0x0012FF74处,此地址是2的倍数,可分配。此时,轮到第二个数据成员分配内存空间了,如果分配在short后,应在地址0x0012FF76处,但是第二个数据成员占4字节内存空间,与指定的对齐值比较后,实际对齐值去int 类型的长度4,而地址0x0012FF76不是4的倍数,需要插入2字节填充,以满足对齐条件。因此,第二个数据成员被定义在地址为: 0x 0012FF78处。

上面说到了VC++6.0默认的对齐值是8,那么结构体的整体大小要能被8整除,比如:

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 + 4 = 16,这样就能满足要求了。

但是,并非设定了默认的对齐值就将结构特点的对齐值锁定,如果结构体中的数据成员类型最大值为M,指定的对齐值为N,那么实际对齐值为: min(M,N);如下面的代码:

struct
{
	char cChar;			///---应占1字节内存空间,假设地址为: 0x 0012FF00;
	int nInt;					///---应占4字节内存空间
	short sShort;			///---占2字节内存空间
};
上述结构体还是按照8字节的对齐方式对齐,在sShort后面将会填充2字节以满足对齐要求。

既然有默认的对齐值,就可以在定义结构体时进行调整,VC++6.0中可以使用预编译指令 : #pragma pack(M)  来调整对齐大小。使用pack修改对齐值也非一定生效,与默认对齐值一样,都需要参考结构体中的数据成员类型,当设定的对齐值大于结构体中的数据成员类型大小时,此对齐值同样是无效的。对齐值的就按流程换个说法是:

将设定的对齐值与结构体中最大值的基本类型数据成员的长度进行比较,取 两者之间的较小者。

当结构体中以数组作为成员时,将根据数组元素的长度计算对齐值,而不是按照数组的整体大小去计算对齐值。

由于存在内存对齐,数据的布局变化多端,因此在分析结构体中和类的数据成员布局时,不能单纯地参考各数据成员的类型长度,按照顺序进行排列,而应该按照上诉方法仔细观察和分析。另外,各编译器厂商的实现也有所不同,应该详细阅读相关文档。

◆静态数据成员。当类中的数据成员被修饰为静态时,对象的长度计算又会发生变化,虽然静态数据成员在类中被定义,但它与静态局部变量类似。存放的位置和全局变量一致。只是在编译器增加了作用域的检查,在作用域外不可见,同类对象将共同享有静态数据成员的空间。

你可能感兴趣的:(汇编学习之路之对象的内存布局)