字节对齐
字节对齐的作用
计算机中内存大小的基本单位是字节(byte),理论上来讲,可以从任意地址访问某种基本数据类型。
但是实际上,计算机并非逐字节大小读写内存,现代计算机系统通常以字(word)为单位来读取和写入内存。一个字的大小通常是4字节(32位cpu)或8字节(64位cpu)。
字节对齐的目的就是提高CPU对数据的访问效率。
编译器在对齐结构体成员时,会遵循以下原则:
- 结构体的起始地址必须是其最大对齐值的整数倍。
- 结构体每个成员相对第一个成员的偏移都是其size的整数倍,如不满足,在当前成员和前一个成员之间填充字节以满足。
- 结构体的总size为结构体中最大成员的size的整数倍,如不满足,在结构体最后一个成员之后填充字节以满足。
- 结构体嵌套时,递归的按照结构体中最大的那个成员对齐。(注意不是最大的结构体,而是结构体中最大的那个成员)
#include <cstdio>
#include <cstdint>
// 空结构体和空类所占内存大小为1
// 编译器会给空类/结构体隐式的加一个字节,确保每个实例在内存中有唯一的地址
struct DemoStruct {
};
struct DemoStruct0 {
    char c1;     // 1B
    int i1;      // 4B
    uint64_t n1; // 8B
};
struct DemoStruct1 {
    char c1;     // 1B
    uint64_t n1; // 8B
    int i1;      // 4B
};
int main()
{
    // 1
    printf("sizeof(DemoStruct)=%lu\n", sizeof(DemoStruct));
    // 16
    printf("sizeof(DemoStruct0)=%lu\n", sizeof(DemoStruct0));
    // 24
    printf("sizeof(DemoStruct1)=%lu\n", sizeof(DemoStruct1));
    return 0;
}
再看嵌套struct的例子:
#include <cstdio>
#include <cstdint>
#include <cstddef>
struct DemoStruct2 {
    short s1;                  // 2B
    short s2;                  // 2B
                               // Padding 4B(因为sdb1结构体是8字节对齐的, 所以sdb1.sc1相对于首地址的偏移必须是8的倍数,所以在sdb1.sc1之前填充4B)
    struct SubDemoStruct1 {
        char sc1;              // 1B
                               // Padding 7B
        long long int slli1;   // 8B
    } sdb1;
    struct SubDemoStruct2 {
        char sc1;              // 1B
    } sdb2;
                               // Padding 3B(因为sdb3结构体是4字节对齐的, 所以sdb3.si1相对于首地址的偏移必须是4的倍数,所以在sdb3.si1之前填充3B)
    struct SubDemoStruct3 {
        int si1;               // 4B
    } sdb3;
    char c1;                   // 1B
                               // Padding 7B(因为结构体中最大成员的size是8, 所以结构体的总大小必须是8的倍数,所以在结构体最后填充7B)
};
int main()
{
    DemoStruct2 ds2;
    printf("sizeof(DemoStruct2)=%lu\n", sizeof(ds2));
    printf("DemoStruct2 结构中的 s1 偏移 = %lu 字节。\n",  offsetof(struct DemoStruct2, s1));
    printf("DemoStruct2 结构中的 s2 偏移 = %lu 字节。\n", offsetof(struct DemoStruct2, s2));
    printf("DemoStruct2 结构中的 sdb1.sc1 偏移 = %lu 字节。\n", offsetof(struct DemoStruct2, sdb1.sc1));
    printf("DemoStruct2 结构中的 sdb1.slli1 偏移 = %lu 字节。\n", offsetof(struct DemoStruct2, sdb1.slli1));
    printf("DemoStruct2 结构中的 sdb2.sc1 偏移 = %lu 字节。\n", offsetof(struct DemoStruct2, sdb2.sc1));
    printf("DemoStruct2 结构中的 sdb3.si1 偏移 = %lu 字节。\n", offsetof(struct DemoStruct2, sdb3.si1));
    printf("DemoStruct2 结构中的 phone 偏移 = %lu 字节。\n",   offsetof(struct DemoStruct2, c1));
    return 0;
}
输出:
sizeof(DemoStruct2)=40
DemoStruct2 结构中的 s1 偏移 = 0 字节。
DemoStruct2 结构中的 s2 偏移 = 2 字节。
DemoStruct2 结构中的 sdb1.sc1 偏移 = 8 字节。
DemoStruct2 结构中的 sdb1.slli1 偏移 = 16 字节。
DemoStruct2 结构中的 sdb2.sc1 偏移 = 24 字节。
DemoStruct2 结构中的 sdb3.si1 偏移 = 28 字节。
DemoStruct2 结构中的 phone 偏移 = 32 字节。
C/C++编译器会自动处理struct的内对齐,同时提供了一些机制让程序员手动控制内存对齐(#pragma pack directive)。
#include <stdio.h>
#include <stdint.h>
struct DemoStruct {
  char c1;     // 1B
  uint64_t n1; // 8B
};
#pragma pack(1)  // 显式指定字节对齐规则
struct DemoPackStruct {
  char c1;     // 1B
  uint64_t n1; // 8B
};
#pragma pack(2)
struct DemoPack2Struct {
  char c1;     // 1B
  uint64_t n1; // 8B
};
#pragma pack()  // 设置为默认字节对齐规则
struct DemoPack3Struct {
  char c1;     // 1B
  uint64_t n1; // 8B
};
int main() {
    // 16
    printf("sizeof(DemoStruct)=%d\n", sizeof(DemoStruct));
    // 9
    printf("sizeof(DemoPackStruct)=%d\n", sizeof(DemoPackStruct));
    // 10
    printf("sizeof(DemoPack2Struct)=%d\n", sizeof(DemoPack2Struct));
    // 16
    printf("sizeof(DemoPack3Struct)=%d\n", sizeof(DemoPack3Struct));
    return 0;
}
pragma pack用法:
#pragma pack(show) // 显示当前内存对齐的字节数,编辑器默认4字节对齐
#pragma pack(n)    // 设置编译器按照n个字节对齐,n可以取值1,2,4,8,16
#pragma pack(0)    // 恢复编译器默认对齐方式
#pragma pack()     // 恢复编译器默认对齐方式
#pragma pack(push)      // 将当前的对齐字节数压入栈顶,不改变对齐字节数
#pragma pack(push, n)   // 将当前的对齐字节数压入栈顶,并按照n字节对齐
#pragma pack(pop)       // 弹出栈顶对齐字节数,不改变对齐字节数
#pragma pack(pop, n)    // 弹出栈顶并直接丢弃,按照n字节对齐
说明:
- #pragma pack(push): push是“压入”的意思。编译器编译到此处时将保存对齐状态(保存的是push指令之前的对齐状态)。
- #pragma pack(pop): pop是”弹出“的意思。编译器编译到此处时将恢复push指令前保存的对齐状态(请在使用该预处理命令之前使用#pragma pack(push))。
push和pop是一对应该同时出现的名词,只有pop没有push不起作用,只有push没有pop可以保持之前对齐状态(但是这样就没有使用push的必要了)。
#pragma pack()是取消自定义对齐方式,恢复默认方式,而push之后pop是回到push指令之前的对齐方式。