C/C++ 的结构,就像是一个将几种数据结构打包的功能。在使用的时候,可能会注意到,结构体的大小不等于结构体所有成员的大小之和,原因是编译器进行了字节对齐。

字节对齐的目的

如果一个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐。比如在 32 位 CPU 下,假设一个整型变量的地址为 0x00000004(下面简写为 0x04),那它就是自然对齐的。

需要字节对齐的根本原因在于 CPU 访问数据的效率问题。32 位 CPU 把内存的每 4 个字节分为一组,一次可以同时访问一组。比如,可以同时访问 0x00 - 0x03,也可以同时访问 0x04 - 0x07,但不能同时访问 0x03 - 0x06

若不进行字节对齐,假如一个 int 存储在了 0x03 - 0x06,那么读取这个 int 需要 CPU 读取两次内存,降低了效率。而进行了字节对齐,效率会有一定的提升。

这段话也解释了,64 位系统相比 32 位系统更大(由于字节对齐,需要更多的空位),但是运行速度会快一些(对于 8 字节的数据类型,如 long longdouble 的读取会快一些)。

C 语言编译器对字节对齐的要求

  1. 标准数据类型:只要地址是它的长度的整数倍就行了;
  2. 数组:按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了。
  3. 联合:按其包含的长度最大的数据类型对齐。
  4. 结构体:结构体中每个数据类型都要按其包含的长度最大的数据类型对齐。

1
2
3
4
5
6
7
8
9
10
11
struct foo
{
char c10[10];
long long ll;
char c;
};

int main()
{
std::cout << sizeof(foo); //输出 32
}

该结构中,c10 虽然只占 10 个字节,但是由于它要对齐 8 字节的 long long,因此会在 c10 后填充到 16 个字节(填充了 6 字节)。同样的, c 后面也会填充到 8 字节。
整个结构占了 16 + 8 + 8 = 32 字节。

把上述代码的 long long 改为 int,整个结构占了 12 + 4 + 4 = 20 字节。

1
2
3
4
5
6
7
8
9
10
11
struct foo
{
char c10[10];
int i;
char c;
};

int main()
{
std::cout << sizeof(foo); //输出 20
}

手动设置字节对齐

在设计不同CPU下的通信协议时,或者编写硬件驱动程序时,寄存器的结构这两个地方都需要按统一字节对齐,所以需要手动设定字节对齐。

可以使用 #pragma pack() 语句设置字节对齐,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma pack (1) /*指定按2字节对齐*/
struct foo
{
char c10[10];
int i;
char c;
}bar;
#pragma pack () /*取消指定对齐,恢复缺省对齐*/

int main()
{
std::cout << sizeof(foo); //输出 15
}

结构体占了 10 + 4 + 1 = 15 字节。