(1)什么是字节对齐[1]
对齐规则:
结构体中一个变量占用 n 个字节,则该变量的起始地址必须能够被 n 整除,即:存放起始地址% n = 0,对于结构体而言,这个 n 取其成员中的数据类型占空间的值最大的那个。(这里n可以称为对齐值)
补充:结构体的长度必须为结构体的实际对齐值(自身对齐值和编译器默认对齐值中最小的一个)的整数倍,不够就补空字节。
(2)为什么要字节对齐[1]
内存空间是按照字节来划分的,从理论上说对内存空间的访问可以从任何地址开始,但是在实际上不同架构的 CPU 为了提高访问内存的速度,就规定了对于某些类型的数据只能从特定的起始位置开始访问。这样就决定了各种数据类型只能按照相应的规则在内存空间中存放,而不能一个接一个的顺序排列。
举个例子,比如有些平台访问内存地址都从偶数地址开始,对于一个 int 型(假设 32 位系统),如果从偶数地址开始的地方存放,这样一个读周期就可以读出这个 int 数据,但是如果从奇数地址开始的地址存放,就需要两个读周期,并对两次读出的结果的高低字节进行拼凑才能得到这个int 数据,这样明显降低了读取的效率。
(3)如何进行字节对齐[1,2]
参考[2],每个数据类型的变量(或称数据对象data object)都有一个对齐值(alignment-requirement),即sizeof(data type)。例如char类型变量的对齐值是1,int、long、float的对齐值是4,long long、double的对齐值则是8,单位都是字节。而结构体的对齐值则是成员变量中占用空间最大的那个变量的大小。
根据上述结论,“一个变量占用 n 个字节,则该变量的起始地址必须能够被 n 整除,即:存放起始地址% n = 0”,那么char类型变量的地址必须被1整除,int、long、float的地址必须被4整除,而long long、double必须被8整除。
由于结构体里成员变量的地址是按照顺序存储的,因此不能保证每个成员变量满足上述规则,所以有些成员变量的地址之前或之后需要填充一些字节。
至于要填充多少字节,跟每个变量实际的对齐值有关。确定实际的对齐值,有2条规则:
1、每个编译器有默认的对齐值,VS2010中默认是8,可以在代码中加入#pragma pack(n)指令,告诉编译器默认对齐值的大小,这里的n必须是1,2,4,8,16之一。(命令行编译中可以使用/Zp[n]选项)
2、实际的对齐值,取编译器默认的对齐值(即n的值)和数据对象的自身对齐值(即sizeof(data type))中最小的一个,即
min( n,sizeof( item ) )。
记住这2条规则,下面的示例代码,不用运行,就可以手动计算出结果了。
示例代码:
#include <stdio.h>
// #pragma pack(8)
struct A {
char a;
long b;
};
struct B {
char a;
struct A b;
long c;
};
struct C {
char a;
struct A b;
double c; // Change “long” to “double” in struct B
};
struct D {
char a;
struct A b;
double c;
int d; // Add “int” in struct C
};
struct E {
char a;
int b;
struct A c;
double d;
};
// Error: C语言中不允许空的结构体,而C++中则允许,将.c后缀换成.cpp试试?
struct F {
};
void main(void)
{
printf("A's size: %d\n", sizeof(struct A));
printf("B's size: %d\n", sizeof(struct B));
printf("C's size: %d\n", sizeof(struct C));
printf("D's size: %d\n", sizeof(struct D));
printf("E's size: %d\n", sizeof(struct E));
printf("F's size: %d\n", sizeof(struct F));
}
分析如下:
I. struct A,假设a的地址从0开始,则a占1个字节,顺序下来,b地址则是1,不满足对齐规则,在b的地址之前填充3个字节(取最小的对齐值4),b的地址现在是4(1+3),接着b占4个字节。所以A的大小是8个字节(1+3+4)。
等价代码,如下
struct A {
char a;
char padding[3]; // 填充3个字节
long b;
};
II. struct B,假设a的地址从0开始,则a占1个字节,顺序下来,结构体b地址则是1,由于结构体b自身对齐值是4(取成员变量中最大的sizeof(item)),不满足对齐规则,所以在其地址之前填充3个字节(取对齐值4),b的地址是4(1+3),接着结构体b占8个字节(由I可知b的大小),顺序下来,c的地址是12(1+3+8),满足c的对齐规则,不用填充,接着就是c占用的4个字节。所以B的大小是16个字节(1+3+8+4)。
等价代码,如下
struct B {
char a;
char padding[3]; // 填充3个字节
struct A b;
long c;
};
III. struct C,由II可知,a和b变量已经占了12个字节,顺序下来,c的地址是12,不满足对齐规则(地址12不能整除double的8个字节),在c地址之前填充4个字节,这时c的地址是16(1+3+8+4),满足对齐规则,下面就是c占用的8个字节。所以C的大小是24个字节(1+3+8+4+8)
等价代码,如下
struct C {
char a;
char padding1[3]; // 填充3个字节
struct A b;
char padding2[4]; // 填充4个字节
double c;
};
IV. struct D,由III可知,a、b和c变量已经占了24个字节,顺序下来,d的地址是24(1+3+8+4+8),似乎满足对齐规则,于是接着d占用的4个字节,这时D的大小是28(1+3+8+4+8+4)。但是不满足上面提到的“结构体的长度必须为结构体的实际对齐值(自身对齐值和编译器默认对齐值中最小的一个)的整数倍,不够就补空字节。”(28不能被对齐值8整除),所以d占用4个字节后,后面还要填充4个字节。所以D的大小是32个字节。(1+3+8+4+8+4+4)
等价代码,如下
struct D {
char a;
char padding1[3]; // 填充3个字节
struct A b;
char padding2[4]; // 填充4个字节
double c;
int d;
char padding3[4]; // 填充4个字节
};
V. struct E,假设a的地址从0开始,则a占1个字节,顺序下来,b地址则是1,不满足对齐规则,在b的地址之前填充3个字节(取最小的对齐值4),b的地址现在是4(1+3),接着b占4个字节。顺序下来,c的地址是8(1+3+4),c的对齐值是8,满足对齐规则,接着c占用的8个字节。顺序下来,d的地址是16(1+3+4+8),d的对齐值是8,满足对齐规则,接着d占用的8个字节。所以D的大小是24个字节。(1+3+4+8+8)
等价代码,如下
struct E {
char a;
char padding[3]; // 填充3个字节
int b;
struct A c;
double d;
};
VI. struct F,是空结构体,在C语言中不允许。在C++则允许,而且空结构体(或空类)的大小是1。
在头文件下面,加入#pragma pack(n)指令,即指定编译器的默认的对齐值。作为一个小练习,下面的表格中都应该填多少?
答案是