分类: C/C++
2009-05-19 08:54:38
关于内存对齐的探讨
内存对齐的问题主要存在于理解struct等复合结构在内存中的分布。
首先要明白内存对齐的概念。
许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐。
这个k在不同的cpu平台下,不同的编译器下表现也有所不同。比如32位字长的计算机与16位字长的计算机。这个离我们有些远了。我们的开发主要涉及两大平台,windows和linux(unix),涉及的编译器也主要是microsoft编译器(如cl),和gcc。
内存对齐的目的是使各个基本数据类型的首地址为对应k的倍数,这是理解内存对齐方式的终极法宝。另外还要区分编译器的分别。明白了这两点基本上就能搞定所有内存对齐方面的问题。
不同编译器中的k:
1、对于microsoft的编译器,每种基本类型的大小即为这个k。大体上char类型为8,int为32,long为32,double为64。
2、对于linux下的gcc编译器,规定大小小于等于2的,k值为其大小,大于等于4的为4。
明白了以上的说明对struct等复合结构的内存分布就应该很清楚了。
下面看一下最简单的一个类型:struct中成员都为基本数据类型,例如:
struct test1
{
char a;
short b;
int c;
long d;
double e;
};
在windows平台,microsoft编译器下:
假设从0地址开始,首先a的k值为1,它的首地址可以使任意位置,所以a占用第一个字节,即地址0;然后b的k值为2,他的首地址必须是2的倍数,不能是1,所以地址1那个字节被填充,b首地址为地址2,占用地址2,3;然后到c,c的k值为4,他的首地址为4的倍数,所以首地址为4,占用地址4,5,6,7;再然后到d,d的k值也为4,所以他的首地址为8,占用地址8,9,10,11。最后到e,他的k值为8,首地址为8的倍数,所以地址12,13,14,15被填充,他的首地址应为16,占用地址16-23。显然其大小为24。
这就是 test1在内存中的分布情况。我们建立一个test1类型的变量,a、b、c、d、e分别赋值2、4、8、16、32。然后从低地址依次打印出内存中每个字节对应的16进制数为:
2 0 4 0 8 0 0 0 10 0 0 0 0 0 0 0 0 0 0 0 0 0 40 40
验证:
显然推断是正确的。
在linux平台,gcc编译器下:
假设从0地址开始,首先a的k值为1,它的首地址可以使任意位置,所以a占用第一个字节,即地址0;然后b的k值为2,他的首地址必须是2的倍数,不能是1,所以地址1那个字节被填充,b首地址为地址2,占用地址2,3;然后到c,c的k值为4,他的首地址为4的倍数,所以首地址为4,占用地址4,5,6,7;再然后到d,d的k值也为4,所以他的首地址为8,占用地址8,9,10,11。最后到e,从这里开始与microsoft的编译器开始有所差异,他的k值为不是8,仍然是4,所以其首地址是12,占用地址12-19。显然其大小为20。
验证:
我们建立一个test1类型的变量,a、b、c、d、e分别赋值2、4、8、16、32。然后从低地址依次打印出内存中每个字节对应的16进制数为:
2 0 4 0 8 0 0 0 10 0 0 0 0 0 0 0 0 0 40 40
显然推断也是正确的。
接下来,看一看几类特殊的情况,为了避免麻烦,不再描述内存分布,只计算结构大小。
第一种:嵌套的结构
struct test2
{
char f;
struct test1 g;
};
在windows平台,microsoft编译器下:
这种情况下如果把test2的第二个成员拆开来,研究内存分布,那么可以知道,test2的成员f占用地址0,g.a占用地址1,以后的内存分布不变,仍然满足所有基本数据成员的首地址都为其对应k的倍数这一原则,那么test2的大小就还是24了。但是实际上test2的大小为32,这是因为:不能因为test2的结构而改变test1的内存分布情况,所以为了使test1种各个成员仍然满足对齐的要求,f成员后面需要填充一定数量的字节,不难发现,这个数量应为7个,才能保证test1的对齐。所以test2相对于test1来说增加了8个字节,所以test2的大小为32。
在linux平台,gcc编译器下:
同样,这种情况下如果把test2的第二个成员拆开来,研究内存分布,那么可以知道,test2的成员f占用地址0,g.a占用地址1,以后的内存分布不变,仍然满足所有基本数据成员的首地址都为其对应k的倍数这一原则,那么test2的大小就还是20了。但是实际上test2的大小为24,同样这是因为:不能因为test2的结构而改变test1的内存分布情况,所以为了使test1种各个成员仍然满足对齐的要求,f成员后面需要填充一定数量的字节,不难发现,这个数量应为3个,才能保证test1的对齐。所以test2相对于test1来说增加了4个字节,所以test2的大小为24。
第二种:位段对齐
struct test3
{
unsigned int a:4;
unsigned int b:4;
char c;
};
或者
struct test3
{
unsigned int a:4;
int b:4;
char c;
};
在windows平台,microsoft编译器下:
相邻的多个同类型的数(带符号的与不带符号的,只要基本类型相同,也为相同的数),如果他们占用的位数不超过基本类型的大小,那么他们可作为一个整体来看待。不同类型的数要遵循各自的对齐方式。
如:test3中,a、b可作为一个整体,他们作为一个int型数据来看待,所以test3的大小为8字节。并且a与b的值在内存中从低位开始依次排列,位于4字节区域中的前0-3位和4-7位
如果test4位以下格式
struct test4
{
unsigned int a:30;
unsigned int b:4;
char c;
};
那么test4的大小就为12个字节,并且a与b的值分别分布在第一个4字节的前30位,和第二个4字节的前4位。
如过test5是以下形式
struct test5
{
unsigned int a:4;
unsigned char b:4;
char c;
};
那么由于int和char不同类型,他们分别以各自的方式对齐,所以test5的大小应为8字节,a与b的值分别位于第一个4字节的前4位和第5个字节的前4位。
在linux平台,gcc编译器下:
struct test3
{
unsigned int a:4;
unsigned int b:4;
char c;
};
gcc下,相邻各成员,不管类型是否相同,占的位数之和超过这些成员中第一个的大小的时候,在结构中以k值为1对齐,在结构外k值为其基本类型的值。不超过的情况下在内存中依次排列。
如test3,其大小为4。a,b的值在内存中依次排列分别为第一个四字节中的0-3和4-7位。
如果test4位以下格式
struct test4
{
unsigned int a:20;
unsigned char b:4;
char c;
};
test4的大小为4个字节,并且a与b的值分别分布在第一个4字节的0-19位,和20-23位,c存放在第4个字节中。
如过test5是以下形式
struct test5
{
unsigned int a:10;
unsigned char b:4;
short c;
};
那么test5的大小应为4字节,a,b的值为0-9位和10-13位。c存放在后两个字节中。如果a的大小变成了20
那么test5的大小应为8字节。即
struct test6
{
unsigned int a:20;
unsigned char b:4;
short c;
};
此时,test6的a、b共占用0,1,2共3字节,c的k值为2,其实可以4位首位置,但是在结构外,a要以int的方式对齐。也就是说连续两个test6对象在内存中存放的话,a的首位置要保证为4的倍数,那么c后面必须多填充2位。所以test6的大小为8个字节。
关于位段结构的部分是比较复杂的。暂时我就知道这么多。