一.内存对齐的初步讲解
内存对齐可以用一句话来概括:
“数据项只能存储在地址是数据项大小的整数倍的内存位置上”
例如int类型占用4个字节,地址只能在0,4,8等位置上。
例1:
int main()
{
struct xx bb;
printf("&a = %p/n", &bb.a);
printf("&b = %p/n", &bb.b);
printf("&c = %p/n", &bb.c);
printf("&d = %p/n", &bb.d);
printf("sizeof(xx) = %d/n", sizeof(struct xx));
return 0;
}
可以简单的修改结构体的结构,来降低内存的使用,例如可以将结构体定义为:
二.操作系统的默认对齐系数
每 个操作系统都有自己的默认内存对齐系数,如果是新版本的操作系统,默认对齐系数一般都是8,因为操作系统定义的最大类型存储单元就是8个字节,例如 long long(为什么一定要这样,在第三节会讲解),不存在超过8个字节的类型(例如int是4,char是1,long在32位编译时是4,64位编译时是 8)。当操作系统的默认对齐系数与第一节所讲的内存对齐的理论产生冲突时,以操作系统的对齐系数为基准。
例如:
假设操作系统的默认对齐系数是4,那么对与long long这个类型的变量就不满足第一节所说的,也就是说long long这种结构,可以存储在被4整除的位置上,也可以存储在被8整除的位置上。
可以通过#pragma pack()语句修改操作系统的默认对齐系数,编写程序的时候不建议修改默认对齐系数,在第三节会讲解原因
例2:
int main()
{
struct xx bb;
printf("&a = %p/n", &bb.a);
printf("&b = %p/n", &bb.b);
printf("&c = %p/n", &bb.c);
printf("&d = %p/n", &bb.d);
printf("sizeof(xx) = %d/n", sizeof(struct xx));
return 0;
}
三.内存对齐产生的原因
内存对齐是操作系统为了快速访问内存而采取的一种策略,简单来说,就是为了放置变量的二次访问。操作系统在访问内存 时,每次读取一定的长度(这个长度就是操作系统的默认对齐系数,或者是默认对齐系数的整数倍)。如果没有内存对齐时,为了读取一个变量是,会产生总线的二 次访问。
例如假设没有内存对齐,结构体xx的变量位置会出现如下情况:
这样大家就能理解为什么结构体的第一个变量,不管类型如何,都是能被8整除的吧(因为访问内存是从8的整数倍开始的,为了增加读取的效率)!
内存对齐的问题主要存在于理解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中成员都为基本数据类型,例如:
假设从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
显然推断也是正确的。
接下来,看一看几类特殊的情况,为了避免麻烦,不再描述内存分布,只计算结构大小。
第一种:嵌套的结构
这种情况下如果把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。
第二种:位段对齐
相邻的多个同类型的数(带符号的与不带符号的,只要基本类型相同,也为相同的数),如果他们占用的位数不超过基本类型的大小,那么他们可作为一个整体来看待。不同类型的数要遵循各自的对齐方式。
如:test3中,a、b可作为一个整体,他们作为一个int型数据来看待,所以test3的大小为8字节。并且a与b的值在内存中从低位开始依次排列,位于4字节区域中的前0-3位和4-7位
如果test4位以下格式
如过test5是以下形式
在linux平台,gcc编译器下:
如果test4位以下格式
关于位段结构的部分是比较复杂的。暂时我就知道这么多。