一、什么是字节对齐,为什么要对齐?
现代计算机中内存空间都是按照byte 划分的,从理论上讲似乎对任何类型的
变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特
定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺
序的一个接一个的排放,这就是对齐。
对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平
台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU 在访问
一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.
其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放
进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果
一个int 型(假设为32 位系统)如果存放在偶地址开始的地方,那 么一个读周期就
可以读出这32bit,而如果存放在奇地址开始的地方,就需要2 个读周期,并对两次
读出的结果的高低字节进行拼凑才能得到该32bit 数据。显然在读取效率上下降很多。
二、请看下面的结构:
struct struct1
{
double dda;
char cda;
int ida;
};
sizeof(struct1) = ?
错误的求法:
sizeof(struct1)=sizeof(double)+sizeof(char)+sizeof(int)=13
但是当你在VC 上运行如下测试代码:
C 代码
运行结果为:16
其实,这是VC 对变量存储的一个特殊处理。为了提高CPU 的存储速度,VC 对一些变
量的起始地址做了“对齐”处理。在默认情况下,VC 规定各成员变量存放的起始地址
相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数。下面列
出常用类型的对齐方式(vc6.0,32 位系统)。
类型 对齐方式(变量存放的起始地址相对于结构的起始地址
的偏移量)
char 偏移量必须为sizeof(char)即1 的倍数
int 偏移量必须为sizeof(int)即4 的倍数
float 偏移量必须为sizeof(float)即4 的倍数
double 偏移量必须为sizeof(double)即8 的倍数
Short 偏移量必须为sizeof(short)即2 的倍数
各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对
齐方式调整位置,空缺的字节VC 会自动填充。同时VC 为了确保结构的大小为结构的
字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最
后一个成员变量申请空间后,还会根据需要自动填充空缺的字节。
现在来分析VC 是怎样来存放结构的:
struct struct1
{
double dda;
char cda;
int ida;
};
第一个成员dda 分配空间,其起始地址跟结构的起始地址相同(刚好偏移量0 刚好为
sizeof(double)的倍数),该成员变量占用sizeof(double)=8 个字节;接下来为第二
个成员cda 分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为8,
是sizeof(char)的倍数,所以把cda 存放在偏移量为8 的地方满足对齐方式,该成员
变量占用 sizeof(char)=1 个字节;接下来为第三个成员ida 分配空间,这时下一个
可以分配的地址对于结构的起始地址的偏移量为9,不是sizeof (int)=4 的倍数,为
了满足对齐方式对偏移量的约束问题,VC 自动填充3 个字节(这三个字节没有放什么
东西),这时下一个可以分配的地址对于结构的起始地址的偏移量为12,刚好是
sizeof(int)=4 的倍数,所以把ida 存放在偏移量为12 的地方,该成员变量占用
sizeof(int)=4 个字节;这时整个结构的成员变量已经都分配了空间,总的占用的空
间大小为:8+1+3+4=16,刚好为结构的字节边界数(即结构中占用最大空间的类型所
占用的字节数sizeof(double)=8)的倍数,所以没有空缺的字节需要填充。所以整个
结构的大小为:sizeof(struct1)=8+1+ 3+4=16,其中有3 个字节是VC 自动填充的,
没有放任何有意义的东西。
下面再举个例子,交换一下上面的struct1 的成员变量的位置,使它变成下面的情况:
struct mystruct2
{
char cda;
double dda;
int ida;
};
在VC 环境下,运行结果为:24
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 的倍数。
};
所以该结构总的大小为:sizeof(struct2)为1+7+8+4+4=24。其中总的有7+4=11 个字
节是VC 自动填充的,没有放任何有意义的东西。
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 个字节。接着开始为 m4 分配
空间,这时其偏移量为1,需要补足3 个字节,这样使偏移量满足为n=4 的倍数(因
为sizeof(double)大于n),m4 占用8 个字节。接着为m3 分配空间,这时其偏移量为
12,满足为4 的倍数,m3 占用4 个字节。这时已经为所有成员变量分配了空间,共分
配了4+8+4=16 个字节,满足为n 的倍数。如果把上面的#pragma pack(4)改为#pragma
pack(16),那么我们可以得到结构的大小为24。
(首先为m1 分配空间,其偏移量为0,占用1 个字节。接着开始为m4 分配空间,这时
其偏移量为1,需要补足7 个字节,这样使偏移量为8 满足默认对齐方sizeof(double)
的倍数,m4 占用8 个字节。接着为m3 分配空间,这时其偏移量为16,这样使偏移量
满足默认对齐方式sizeof(int)倍数,m4 占用4 个字节,结构边界必须是最大的变量
sizeof(double)占用的空间数的倍数,所以需要填充四个字节.8+8+4+4=24)
再看下面这个例子:
#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 文件中,定义了如下一个宏:
/* Amount of space required in an argument list for an arg of type TYPE.
* TYPE may alternatively be an expression whose type is used.
* */
#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 = 1
为了将上述两种情况统一,偏移量 = ((sizeof(TYPE) + 3) / 4) * 4
在有的源代码中,将内存对齐宏__va_rounded_size 通过位操作来实现,代码如下:
#define __va_rounded_size(TYPE) \
((sizeof(TYPE)+sizeof(int)-1)&~(sizeof(int)-1))
由于 ~(sizeof(int) – 1) ) = ~(4-1)=~(00000011B)=11111100B
(sizeof(TYPE) + sizeof(int) – 1)就是将大于4m 但小于等于4(m+1)的数提高到大于等于4(m+1)
但小于4(m+2),这样再& ~(sizeof(int) – 1) )后就正好将原长度补齐到4 的倍数了。