内存对齐
一、字节对齐基本概念
请看下面的结构:
sizeof(struct1) = ?
错误的求法:
sizeof(struct1)=sizeof(double)+sizeof(char)+sizeof(int)=13
但是当你运行如下测试代码:
#include struct mystruct { double dda; char cda; int ida; }; int main() { struct mystruct ss; printf("%d\n",sizeof(ss)); return 0; }
运行结果为:16
其实,这是编译器对变量存储的一个特殊处理。为了提高CPU的存储速度,编译器对一些变量的起始地址做了“对齐”处理。在默认情况下,编译器规定各成员变量存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数。下面列出常用类型的对齐方式:
类型
char
int
float
double
Short
各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节编译器会自动填充。同时编译器为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节
现在来分析编译器是怎样来存放结构的:
struct struct1 { double dda; char cda; int ida; };
第一个成员dda分配空间,其起始地址跟结构的起始地址相同(偏移量0刚好为sizeof(double)的倍数),该成员变量占用sizeof(double)=8个字节;接下来为第二个成员cda分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为8,是sizeof(char)的倍数,所以把cda存放在偏移量为8的地方满足对齐方式,该成员变量占用
下面再举个例子,交换一下上面的struct1的成员变量的位置,使它变成下面的情况:
struct mystruct2 { char cda; double dda; int ida; };
struct mystruct2 { char cda; //偏移量为0,满足对齐方式,cda占用1个字节; double dda; //下一个可用的地址的偏移量为1,不是sizeof(double)=8 //的倍数,需要补足7个字节才能使偏移量变为8(满足对齐 //方式),因此VC自动填充7个字节,dda存放在偏移量为8 //的地址上,它占用8个字节。 int ida; //下一个可用的地址的偏移量为16,是sizeof(int)=4的倍 //数,满足int的对齐方式,所以不需要VC自动填充,type存 //放在偏移量为16的地址上,它占用4个字节。 //所有成员变量都分配了空间,空间总的大小为1+7+8+4=20,不是结构 //的节边界数(即结构中占用最大空间的类型所占用的字节数sizeof //(double)=8)的倍数,所以需要填充4个字节,以满足结构的大小为 //sizeof(double)=8的倍数。 };
二、#pragma pack(n)来设定变量以n字节对齐方式
VC对结构的存储的特殊处理确实提高CPU存储变量的速度,但是有时候也带来了一些麻烦,我们也屏蔽掉变量默认的对齐方式,自己可以设定变量的对齐 方式。VC 中提供了#pragma pack(n)来设定变量以n字节对齐方式。n字节对齐就是说变量存放的起始地址的偏移量有两种情况:
第一、如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式;
第二、如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。
结构的总大小也有个约束条件,分下面两种情况:如果n大于所有成员变量类型所占用的字节数,那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数;否则必须为n的倍数。下面举例说明其用法:
#pragma pack(push) //保存对齐状态 #pragma pack(4)//设定为4字节对齐 struct test { char m1; double m4; int m3; }; #pragma pack(pop)//恢复对齐状态
以上结构的大小为16,下面分析其存储情况,首先为m1分配空间,其偏移量为0,满足我们自己设定的对齐方式(4字节对齐),m1占用1个字节。接着开始为
再看下面这个例子:
#pragma pack(8) struct S1{ char a; long b; }; struct S2 { char c; struct S1 d; long long e; }; #pragma pack()
成员对齐有一个重要的条件,即每个成员分别对齐.即每个成员按自己的方式对齐.
也就是说上面虽然指定了按8字节对齐,但并不是所有的成员都是以8字节对齐.其对齐的规则是,每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是8字节)中较小的一个对齐.并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节.
S1中,成员a是1字节默认按1字节对齐,指定对齐参数为8,这两个值中取1,a按1字节对齐;成员b是4个字节,默认是按4字节对齐,这时就按4字节对齐,所以sizeof(S1)应该为8;
S2 中,c和S1中的a一样,按1字节对齐,而d 是个结构,它是8个字节,它按什么对齐呢?对于结构来说,它的默认对齐方式就是它的所有成员使用的对齐参数中最大的一个,S1的就是4.所以,成员d就是 按4字节对齐.成员e是8个字节,它是默认按8字节对齐,和指定的一样,所以它对到8字节的边界上,这时,已经使用了12个字节了,所以又添加了4个字节 的空,从第16个字节开始放置成员e.这时,长度为24,已经可以被8(成员e按8字节对齐)整除.这样,sizeof(S2)为24个字节.
这里有三点很重要:
1.每个成员分别按自己的方式对齐,并能最小化长度。
2.复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度。
3.对齐后的长度必须是成员中最大的对齐参数的整数倍,这样在处理数组时可以保证每一项都边界对齐。
三、minix的stdarg.h文件中对齐方式
在minix的stdarg.h文件中,定义了如下一个宏:
#define __va_rounded_size(TYPE) \ (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int))
从注释以及宏的名字可以看出是有关内存对齐方面的作用。根据前面关于C语言内存对齐方面的理论可知
n字节对齐就是说变量存放的起始地址的偏移量有两种情况:
第一、如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式(各成员变量存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数);
第二、如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。
此时n = 4,对于sizeof(TYPE)一定为自然数,sizeof(int) - 1 = 3
sizeof(TYPE)只可能出现如下两种情况:
(1) 当sizeof(TYPE) >= 4,偏移量 = (sizeof(TYPE)/4)*4
(2) 当sizeof(TYPE) < 4,偏移量 = 4
此时sizeof(TYPE) = 1 or 2 or 3,而(sizeof(TYPE) + 3) / 4
为了将上述两种情况统一,偏移量 = ((sizeof(TYPE) + 3) / 4) * 4
#define __va_rounded_size(TYPE) \ ((sizeof(TYPE)+sizeof(int)-1)&~(sizeof(int)-1))
(sizeof(TYPE) + sizeof(int) – 1)就是将大于4m但小于等于4(m+1)的数提高到大于等于4(m+1)但小于4(m+2),这样再& ~(sizeof(int) – 1) )后就正好将原长度补齐到4的倍数了。