结构体占用的内存往往大于各成员大小之和,原因是编译器在成员之间插入填充字节以满足对齐要求。本节说明对齐规则、sizeof 结果、减小填充的方法,以及在必须与外部布局一致时使用打包。

structa_t { char c; short s; } 的典型布局(32/64 位常见平台) 低地址 高地址 c 1 字节 填充 1 字节 s 2 字节 sizeof(structa_t) = 4 字节 成员大小相加:1 + 2 = 3 short 须从 2 的倍数 地址开始,故 c 后插入 1 字节填充。 偏移 1 偏移 2(s 对齐) 偏移 0
成员按声明顺序从低地址向高地址排布;实线框为数据成员,黄色虚线框为编译器插入的填充。后文将用更多结构体说明不同成员顺序下 sizeof 的差异。

数据对齐(Data Alignment)

各标量类型具有实现相关的对齐要求;在常见 32/64 位平台上可参照下表(具体以目标平台为准):

类型典型对齐
char1 字节
short2 字节
int4 字节
float4 字节
double8 字节

CPU 从内存读取数据时,往往按固定宽度的一次传输(与机器字长、总线宽度等相关)搬运若干字节。若变量地址满足其对齐要求,硬件通常可用较少次数的访存读到完整数据;地址未对齐时,可能需要多次访问或额外处理,性能会下降。在部分架构上,硬件甚至不允许非对齐访问,会触发总线错误等异常,程序直接终止。因此各类型具有对齐单位,对象起始地址宜为该单位的整数倍;编译器在结构体中插入填充,正是为了满足上述硬件约束。

结构体填充

为使各成员满足对齐,编译器可能在成员之间及结构体末尾插入填充字节。下列四个结构体成员类型相同、顺序不同:

typedef struct structa_tag {
    char c;
    short int s;
} structa_t;

typedef struct structb_tag {
    short int s;
    char c;
    int i;
} structb_t;

typedef struct structc_tag {
    char c;
    double d;
    int s;
} structc_t;

typedef struct structd_tag {
    double d;
    int s;
    char c;
} structd_t;

若简单相加成员大小,会得到 3、7、13、13 等数值,但 sizeof 结果不同:

#include <stdio.h>

int main(void) {
    printf("sizeof(structa_t) = %lu\n", sizeof(structa_t));
    printf("sizeof(structb_t) = %lu\n", sizeof(structb_t));
    printf("sizeof(structc_t) = %lu\n", sizeof(structc_t));
    printf("sizeof(structd_t) = %lu\n", sizeof(structd_t));
    return 0;
}

输出:

sizeof(structa_t) = 4
sizeof(structb_t) = 8
sizeof(structc_t) = 24
sizeof(structd_t) = 16

填充位置说明

结构体 A:char + short

char (1) + padding (1) + short (2) = 4 字节

short 需 2 字节对齐,故在 char 后插入 1 字节填充。

结构体 B:short + char + int

short (2) + char (1) + padding (1) + int (4) = 8 字节

结构体 C:char + double + int

char (1) + padding (7) + double (8) + int (4) + padding (4) = 24 字节

末尾填充使结构体数组中每个元素仍满足最严格成员的对齐。

结构体 D:double + int + char

double (8) + int (4) + char (1) + padding (3) = 16 字节

将较大成员前置,可减少与 C 相比的浪费。

减小填充的建议

  • 将占用字节较多的成员放在前面。
  • 将大小相近的成员相邻排列。
  • 避免在小成员之间插入大对齐要求的成员。

结构体数组场景下,节省的内存会随元素个数放大。定义结构体类型时亦可结合typedef 提高可读性。

结构体打包(Packing)

读写二进制协议、硬件寄存器映射等场合,有时要求布局与外部规范逐字节一致,此时可减小或取消填充(可能影响访问效率,宜谨慎使用)。GCC 示例:

#pragma pack(1)

/* 或单个结构体: */
struct name {
    ...
} __attribute__((packed));
#include <stdio.h>
#pragma pack(1)

typedef struct structa_tag { char c; short int s; } structa_t;
typedef struct structb_tag { short int s; char c; int i; } structb_t;
typedef struct structc_tag { char c; double d; int s; } structc_t;
typedef struct structd_tag { double d; int s; char c; } structd_t;

int main(void) {
    printf("sizeof(structa_t) = %lu\n", sizeof(structa_t));
    printf("sizeof(structb_t) = %lu\n", sizeof(structb_t));
    printf("sizeof(structc_t) = %lu\n", sizeof(structc_t));
    printf("sizeof(structd_t) = %lu\n", sizeof(structd_t));
    return 0;
}

输出:

sizeof(structa_t) = 3
sizeof(structb_t) = 7
sizeof(structc_t) = 13
sizeof(structd_t) = 13

打包后 sizeof 等于成员大小之和;与二进制文件读写配合时须保证读写双方布局一致。

本节说明了默认对齐、填充与打包的取舍。下一节讨论堆内存泄漏的成因与预防。