一、什么是字对齐?为什么要进行字对齐?
现代计算机的内存空间都是按照byte划分的,从理论上讲似乎对任何类型变量的访问都可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址上访问,这就是字节对齐。
字节对齐的原因,主要有如下两条:
平台原因:不是所有的硬件平台都能访问任意地址上的任意数据的;某些arm cpu上只能在某些地址处取某些特定类型的数据,否则会抛出硬件异常。
性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
二、对齐规则
每个编译器都有自己的默认对齐字节数,可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这个值,其中n就是你要指定的对齐字节数。具体的对齐规则有两条:
数据成员首地址对齐规则:结构体或联合体中的每一个数据成员,它的首地址必须是X的倍数,其中X等于这个数据成员自身的长度,或#pragma pack指定的数值n,选其中比较小的那一个。
结构体或联合体的整体对齐规则:在数据成员完成各自对齐之后,结构体或联合体本身也要进行对齐,它的大小必须是Y的倍数,其中Y等于最大数据成员长度,或#pragma pack指定的数值n,也是选其中比较小的那一个。
三、编译器默认对齐字节数
xcode编译器,64位程序,默认8字节对齐,32位程序,默认4字节对齐。
vs编译器,64为程序,默认8字节对齐,32位程序,默认8字节对齐。
四、字节对齐实验
#pragma pack(4) typedef struct tag_T_MSG { double ParaA; char ParaB; int ParaC; short ParaD; float ParaE; } T_MSG; #pragma pack() int main(int argc, const char * argv[]) { T_MSG msg; printf("ParaA address: %p\n", &msg.ParaA); printf("ParaB address: %p\n", &msg.ParaB); printf("ParaC address: %p\n", &msg.ParaC); printf("ParaD address: %p\n", &msg.ParaD); printf("ParaE address: %p\n", &msg.ParaE); int nSize =sizeof(T_MSG); printf("%d\n",nSize); return 0; }
T_MSG结构体的n=4,其中结构体的第一个数据成员,它的首地址对齐由编译器保证,我们无需关注。这里假设ParaA的首地址为0,那么:
成员ParaB的大小是1,并且n=4,选择最小的一个值,则它的首地址必须是1的倍数,ParaA后面的空地址是8,8是1的倍数,所以ParaB的首地址为8。
成员ParaC的大小是4,并且n=4,选择最小的一个值,则它的首地址必须是4的倍数,ParaB后面的空地址是9,但是9不是4的倍数,所必须空出3个字节,将ParaC的首地址移到12。
成员ParaD的大小是2,并且n=4,选择最小的一个值,则它的首地址必须是2的倍数,ParaC后面的空地址是16,16是2的倍数,所以ParaD的首地址为16。
成员ParaE的大小是4,并且n=4,选择最小的一个值,则它的首地址必须是4的倍数,ParaD后面的空地址是18,但是18不是4的倍数,所以ParaD的首地址必须向后移2个字节,移到20。
经过以上的分析,此时计算出来的结构体大小是24,它们的内存布局,如下图所示 :
结构体每一个数据成员的首地址对齐后,那么结构体本身的大小也必须对齐。从上面的分析知道,结构体最大的数据成员长度是8,并且n=4,所以结构本身的大小必须是4字节对齐,而24刚好是4的倍数,无需在结构体后面填充空字节。因此,整个结构体的最终大小就是24byte。
在xcode中运行上面的代码,最终的输出结果如下所示,和上面的分析结果一致。
ParaA address: 0x7fff5fbff800 ParaB address: 0x7fff5fbff808 ParaC address: 0x7fff5fbff80c ParaD address: 0x7fff5fbff810 ParaE address: 0x7fff5fbff814 24
五、嵌入式平台上字节对齐问题
在普通的PC设备上,字节对齐参数无论如何设置,最终生成的可执行文件,在运行时都不出现致命的问题。但是在某些嵌入式平台上,下面的代码:
typedef struct tag_T_HEAD { int magic; char data[64]; }T_HEAD; int main(int argc, const char * argv[]) { T_HEAD hdr; int* pValue = (int*)(hdr.data+1); printf("dValue = %d\n", *pValue); return 0; }
生成的可执行程序在运行时,会直接崩溃。因为int变量的数据地址必须是4的倍数,而pValue指针明显不是4的的倍数,打印pValue指针存储的int变量时,硬件会抛出异常。