一、为什么要内存对齐

平台原因

不是所有的CPU都能访问任意地址上的任意数据的,有些CPU只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

性能原因

尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的,它一般会以双字节、四字节、8字节、16字节甚至32字节为单位来存取内存,我们将上述这些存取单位称为内存存取粒度

如果数据存储时是按照内存存取粒度对齐的,那么处理器就可以在一个内存访问周期内完成数据的读取;反之,如果数据没有对齐,可能需要多个内存访问周期才能读取完整的数据。

二、内存对齐规则

对齐系数

在 C/C++ 中,编译器会在结构体(联合体、类)成员之间插入填充字节,以确保每个成员都按照特定的对齐要求进行存储,以提高内存访问效率。

每个编译器都有自己的默认“对齐系数”(也叫对齐模数)。GCC默认对齐系数为4,MSVC 32位默认对齐系数为8,MSVC 64位默认对齐系数位16。

我们可以在代码中通过预编译命令#pragma pack(n)来指定对齐系数,n 可以为 (1, 2, 4, 8, 16)中的任意一个。

下面是 #pragma pack 命令的基本用法:

1
2
3
#pragma pack(n) // 使用自定义n字节对齐  n可以为1,2,4,8,16
#pragma pack() // 使用缺省字节对齐
#pragma pack(show) // 在编译输出窗口以警告的形式显示出当前的内存以几个字节对齐

对齐规则

假设在一个结构体(联合体、类)中,最大数据类型长度为 m,编译器对齐系数是 n,则将 min(m, n) 称做对齐单位,也叫有效对齐值,这里记为 s。

对齐分为成员对齐和结构体(联合体、类)整体对齐。

  • 成员对齐规则:第一个成员相对于结构体首地址的偏移量始终为 0,以后每个成员(数据类型长度记为 k)相对于结构体首地址的偏移量都为 min(k, s) 的整数倍。
  • 整体对齐:结构体的总大小必须是有效对齐值 s 的整数倍。

从内存对齐的规则可以看出:`

  • 除第一个成员外,每个成员的偏移量:min(k, min(m, n))
  • 如果设置的对齐系数 n 大于类中最大数据类型长度 m 时,则该设置实际是不起作用的。
  • 当对齐系数 n == 1 时,整个结构体的大小为所有成员长度之和。

三、结构体内存对齐示例

在 64 位系统上编译下面的测试程序,已知在 64 位系统上各类型占用字节数如下:

1
2
3
4
5
6
7
char     1字节
short 2字节
int 4字节
long 4字节
double 8字节
long long 8字节
char* 8字节

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#pragma pack(8)  // 强制设置对齐系数为8

struct A {
char s[5];
short c;
int a;
};

int main()
{
int i = sizeof(A);
printf("%d", i);

return 0;
}

按照第二节所讲的内存对齐规则,分析如下:
因为结构体中最大的数据成员长度为 int(即 4 字节),而且#pragma pack(8)指定的对齐系数为 8 ,所以有效对齐值 s 为 min(4, 8) = 4

下图是结构体 A 按照 4 字节对齐的内存布局(需要注意的是“内存不是填充在 s5 后面,而是填充在 c 后面”):

从图我们很容易知道sizeof(A) = 12.

文章图片带有“CSDN”水印的说明:
由于该文章和图片最初发表在我的CSDN 博客中,因此图片被 CSDN 自动添加了水印。