struct字节对齐以及使用中的问题

396 阅读5分钟

stuct字节对齐

在C和C++中,struct的成员在内存中的排序方式受到字节对齐的影响,struct的字节对齐通常以结构体的成员中具有最大对齐需求的成员为基准。例如,如果一个struct中包含intchar类型的成员,那么由于int的字节对齐需求通常较大,因此整个结构体将按照int的字节大小进行字节对齐。

字节对齐规则

  1. 结构体第一个成员的偏移量为0。
  2. 结构体其他成员的偏移量为该成员大小与对齐值中较小的那个的整数倍。若成员为结构体类型,则按照结构体中最大成员大小与对齐值进行比较。
  3. 结构体的总大小为对齐值的整数倍。

对齐值默认为结构体成员中最大的那个,当然也可以通过伪指令或attribute关键字手动设置(优先级高)。

验证实验

通过具体的实验验证这一特性:

  1. 假设由如下三个结构体:
struct struct_1 {
    uint8_t a;   // uint8_t占用一个字节
    uint16_t b;  // uint16_t占用两个字节
    uint32_t c;  // uint32_t占用四个字节
};
​
struct struct_2 {
    uint16_t a;
    uint16_t b;
    uint32_t c;
};
​
struct struct_3 {
    uint16_t a;
    uint32_t b;
    uint16_t c;
};
  1. 分别输出三个结构体占用字节数
printf("struct_1 size: %d\n", sizeof(struct struct_1));  // struct_1 size: 8
printf("struct_2 size: %d\n", sizeof(struct struct_2));  // struct_2 size: 8
printf("struct_3 size: %d\n", sizeof(struct struct_3));  // struct_3 size: 12

受到字节对齐的影响,结构体中成员的类型与顺序都会影响到最终整个结构体占用的内存大小。

  1. 结构体在内存中实际的分布

image.drawio

  • struct1uint32_t的字节最大,因此struct1以4字节对齐。
  • uint8_t 占用1个字节,且为结构体中的第一个成员,因此偏移量为0.
  • uint16_t 占用2个字节,比对齐值小,因此uint16_t成员的偏移量为2的整数倍,在此处偏移量为2。
  • uint32_t大小为4字节,与对齐值相同,因此uint16_t成员的偏移量为4的整数倍,在此处偏移量为4。
  • struct2struct3同理。

struct字节对齐引发的问题

  • 不合理的成员顺序,可能会造成大量的内存空间浪费。

  • 若使用内存拷贝对结构体进行赋值,若结构体内存不是紧凑的,即有部分填充字节,则可能无法获得正确的拷贝结果。

    • sturct1中想要赋值的结果为a = 0x00, b = 0x0102, c = 0x03040506,而实际结果为a = 0x00, b = 0x0203, c = 0x04050607
  • 注意同一内存块中的不同的成员变量并不一定是紧凑的顺序。(参考struct1的内存分布情况)

  • 使用内存拷贝的方式赋值时,需要注意大小端存储的问题。

    • struct1中从内存角度上看为a = 0x00, b = 0x0203, c = 0x04050607,而若在程序中使用printf输出的结果可能为a = 0x00, b = 0x0302, c = 0x07060504

验证实验

uint8_t data[] = {0x00, 0x01,0x02, 0x03, 0x04, 0x05, 0x06 , 0x07, 0x08, 0x09, 0x0A, 0x0B};
struct_1 s1;
​
memcpy(&s1, data, sizeof(struct_1));
​
printf("struct1 uint8_t address: 0x%x\n",(void*)(&(s1.a)));  // struct1 uint8_t address: 0xc87ff62c
printf("struct1 uint16_t address: 0x%x\n",(void*)(&(s1.b))); // struct1 uint16_t address: 0xc87ff62e
printf("struct1 uint32_t address: 0x%x\n",(void*)(&(s1.c))); // struct1 uint32_t address: 0xc87ff630printf("struct1 uint8_t value: 0x%0.2x\n",s1.a);  // struct1 uint8_t value: 0x00
printf("struct1 uint16_t value: 0x%0.4x\n",s1.b); // struct1 uint16_t value: 0x0302
printf("struct1 uint32_t value: 0x%0.8x\n",s1.c); // struct1 uint32_t value: 0x07060504

struct字节对齐的优势

  • 两个同类型的结构体变量可以直接赋值。
  • 提高访问速度。
  • 减少内存浪费。

手动设置字节对齐值

伪指令

  • 使用伪指令#pragma pack(n),C编译器将按照n个字节对齐。
  • 使用伪指令#pragma pack(),C编译器将取消自定义字节对齐方式。

此方法会影响到后续的字节对齐值。

验证实验

struct struct_4 {
        uint8_t a;
        uint16_t b;
        uint32_t c;
    };
#pragma pack(1)
struct struct_5 {
    uint8_t a;
    uint16_t b;
    uint32_t c;
};
​
struct struct_6 {
    uint8_t a;
    uint16_t b;
    uint32_t c;
};
#pragma pack(8)
struct struct_7 {
    uint8_t a;
    uint16_t b;
    uint32_t c;
};
​
printf("struct_4 size: %d\n", sizeof(struct struct_4));  // struct_4 size: 8
printf("struct_5 size: %d\n", sizeof(struct struct_5));  // struct_5 size: 7
printf("struct_6 size: %d\n", sizeof(struct struct_6));  // struct_6 size: 7
printf("struct_7 size: %d\n", sizeof(struct struct_7));  // struct_7 size: 8
  • 按照默认的对齐规则,结构体的大小为4字节。
  • 使用伪指令设置对齐值为1后,结构体的大小为4字节,且影响了后续所有结构体的对齐值。
  • 若手动设置的对齐值比结构体中所有的成员都大,则根据字节对齐原则,相当于无效设置。

使用C关键字

  • 使用__attribute((aligned(n))),可以指定单个结构体的对齐值(不确保一定生效)。
  • 使用__attribute((packed)),可以取消单个结构体在编译过程中的优化对齐,即取消字节对齐。