C 结构体内存对齐

Xilong Yang
2021-08-11

疏理一下结构体的内存对齐规则

内存对齐是什么?为什么要进行内存对齐?

现代计算机以字节为单位划分内存空间,但大多不是以字节为单位存取内存的。一次存取往往涉及多个字节,这个大小称为存取粒度。存取粒度通常与系统有关,如32位系统存取粒度大多为4字节,而64位系统的存取粒度大多为8字节。

对于这个结构体:

struct foo {
    char c;
    int  a; // 假定int占用4字节
};

可以看出,它的成员大小总和为5字节。但通常这个结构体的大小会被填充到8字节。原因如下:

考虑不对齐字节的情况,如果结构体地址为0x00,则c的地址为0x00,a的地址为0x01。此时一个存取粒度为4字节的机器要取出a,需要以下几步。

┌─┬────┐        1.取地址0x00开始的4字节,并保留需要的数据0x01、0x02、0x03
│c│a   │        2.取地址0x04开始的4字节,并保留需要的数据0x04
└─┴────┘        3.将上两步获取的数据合并,计算出a的值
 0|1234

这样的过程对计算机来说是很麻烦的,而如果进行字节对齐,则结构体地址和c的地址依然是0x00,而a的地址移到了0x04,此时一个存取粒度为4字节的机器可以直接取出a的值。

┌───────┬───────┐    直接取地址0x04开始的4字节即可取出a的值。
│c      │a      │
└───────┴───────┘
0 1 2 3 4 5 6 7

由此不难看出,所谓的内存对齐就是为了便于机器存取而根据存储粒度对内存布局的调整。

内存对齐的规则

规则1:结构体内部成员的地址一定是自身大小的整数倍,否则就进行对齐。

比如上面的例子中,int类型大小为4字节,而0x01并不是4字节的整数倍,因此对齐到0x04。

考虑下面结构体:

struct foo1 {
    int  a; // 假定int为4字节
    char c;
};

根据规则1:a的地址为0x00,不需对齐;c的地址为0x04,是char类型大小的整数倍,也不需对齐。于是该结构体的大小应当是5字节。

但此时,对于数组:

foo1 arr[2];

其内存布局为如下,此时若想取出arr[1].a,则有:

┌───────┬─┬──────┬─┐    1.由于arr[1].a的地址为0x05,则需要先取出0x04开始的4个字节,
│a      │c│a     │c│      并保留0x05、0x06、0x07。
└───────┴─┴──────┴─┘    2.然后取出0x08开始的4字节,并保留0x08
0 1 2 3 4 5 6 7 8 9     3.将上两步数据合并计算出arr[1].a的值。

这又回到了没有对齐的情况了,因此为了避免这种情况,引入了规则2:

规则2:结构体的大小为其最大成员大小的整数倍,若基最大成员大小大于存取粒度,则结构体的大小为存取粒度的整数倍,否则就在结构体末尾补齐。

在此规则下,结构体foo1的大小应为8字节。需要注意,结构体中最大成员并不包括结构体成员,如:

// 假定int占4字节
struct byte8 {
    int a;
    int b;
};

struct foo2 {
    byte8 a;
    char  b;
};

struct foo3 {
    long long a;
    char b;
};

此时foo2的最大成员并不是看做整体的byte8的8字节,而是byte8与foo2中的基础成员中的最大成员,此处为int。因此foo2对4字节对齐,大小为12字节。而foo3的最大成员为long long,对8字节对齐,大小为16字节。

© 2019- Xilong Yang | CC BY-NC 4.0 | Powered by LaTeX.css, Prism, MathJax