同样几个成员,排列顺序不同,结构体占的内存能差出一大截。原因是编译器在结构体成员之间悄悄塞了填充字节(padding)。下面讲清四件事:为什么会填充、怎么算大小、怎么压缩、什么时候要关掉它。

数据对齐(Data Alignment)

每种数据类型都有一个自然对齐要求,跟处理器架构有关。在大多数机器上是这样:

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

对齐的意思是:这个变量最好放在地址是它大小的整数倍的位置上。放对了,CPU 通常一次内存操作就能取到;放歪了,可能要额外读几次,慢一截。

什么是结构体填充

为了让每个成员都落在自己的对齐边界上,编译器会在成员之间、结构体末尾插入一些用不到的字节,这就叫填充(padding)。

看四个结构体,成员一样只是顺序不同:

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;

如果直接加成员大小:

  • A = char + short = 1 + 2 = 3
  • B = short + char + int = 2 + 1 + 4 = 7
  • C = char + double + int = 1 + 8 + 4 = 13
  • D = double + int + char = 8 + 4 + 1 = 13

但实际用 sizeof 一量,全不一样:

#include <stdio.h>

int main() {
    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 字节

char 占 1 字节,short 需要 2 字节对齐,所以 char 后面补 1 字节,让 short 落在偶数地址上。

结构体 B:short + char + int

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

short 本来就在 2 字节边界,char 紧随其后占 1 字节,int 需要 4 字节对齐,char 后面得垫 1 个字节。

结构体 C:char + double + int

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

double 要 8 字节对齐,char 后得连补 7 字节。int 结束后又补 4 字节收尾,这是为了这个结构体的数组,下一个元素也要对齐。

结构体 D:double + int + char

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

从大到小排,前面成员都天然对齐,只需要末尾补 3 字节。同样的内容,比 C 省了 8 字节。

减少填充的技巧

填充不一定能完全避免,但能大幅压缩。几条经验:

  • 大的类型放前面:double、int 先写,char 这类小的后写。
  • 把大小相近的成员放一起
  • 别把小成员夹在大成员中间

上面 D 就是把 C 重排顺序,从 24 字节压到 16 字节的典型例子。结构体一多、一数组,省下的内存非常可观。

结构体打包(Packing):关掉填充

有些场景结构体的内存布局必须和外部数据完全一致,比如读写二进制文件头、网络协议包、硬件寄存器。这时填充反而是灾难,得关掉。

GCC 下两种写法:

#pragma pack(1)   /* 从此以后 1 字节对齐,不填充 */

/* 或单个结构体: */
struct name {
    ...
} __attribute__((packed));

打包后 sizeof 直接等于成员大小之和:

#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() {
    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

注意打包是拿省内存换访问速度,一般不要随便开打包,只在必须匹配外部格式时才需要。

默认对齐让成员各就其位、访问高效但占内存;排好成员顺序能省不少;真要和外部格式对上,再用打包关掉填充。