3.18 结构体与联合

结构体和联合体是 C 语言中构建复合数据类型的基本工具。它们在底层的实现方式截然不同:结构体成员按顺序排列,联合体成员共享存储空间。

结构体(struct)

内存布局规则

结构体成员按声明顺序依次在内存中排列,每个成员的偏移量必须满足其 要求。

struct S1 {
    char  c;    // 偏移 0,  大小 1
    int   i;    //偏移 4,大小 4 (填充 3字节:$1→4)$
    double d;   // 偏移 8,  大小 8
};  // 总大小 16 (8×2)
struct S1 的内存布局(16 字节):
偏移: 0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15
     [c] [pad][pad][pad][  i     ] [        d                    ]

结构体总大小必须是最大成员对齐要求的倍数,以便结构体数组的每个元素都能正确对齐。

结构体大小计算示例

struct S2 {
    char   c;      // 偏移 0, 对齐 1
    short  s;      // 偏移 2, 对齐 2 (填充 1 字节)
    int    i;      // 偏移 4, 对齐 4
    long   l;      // 偏移 8, 对齐 8 (填充 4 字节)
};  // 总大小 16 (8×2)
 
struct S3 {
    char   c1;     // 偏移 0, 对齐 1
    char   c2;     // 偏移 1, 对齐 1
    char   c3;     // 偏移 2, 对齐 1
};  // 总大小 3 (不需填充,最大对齐为 1)

节省内存的技巧

重新排列结构体成员可以减少填充开销:

// 低效排列(24 字节)
struct Bad {
    char   c;     // 偏移 0
    // 填充 3 字节
    int    i;     // 偏移 4
    double d;     // 偏移 8
    short  s;     // 偏移 16
    // 填充 6 字节 (结构体末尾)
};  // 总大小 24
 
// 高效排列(16 字节)
struct Good {
    double d;     // 偏移 0
    int    i;     // 偏移 8
    short  s;     // 偏移 12
    char   c;     // 偏移 14
    // 填充 1 字节 (结构体末尾)
};  // 总大小 16

原则:将较大尺寸的成员放在前面,较小尺寸的成员放在后面,可以减少填充。

结构体成员访问的汇编实现

struct point {
    int x;
    int y;
};
 
int get_y(struct point *p) {
    return p->y;
}
get_y:
    # %rdi = p
    movl    4(%rdi), %eax       # y 在偏移 4 处
    ret

结构体作为函数参数

结构体作为参数时,整个结构体会被复制:

// 小结构体通过寄存器传递
struct small {
    long a;
    long b;
};
 
long sum_small(struct small s) {
    return s.a + s.b;
}
 
// 大结构体通过指针传递(隐式)
struct big {
    long data[10];
};
 
long sum_big(struct big s) {
    long sum = 0;
    for (int i = 0; i < 10; i++) sum += s.data[i];
    return sum;
}
# 小结构体:成员分别在 %rdi 和 %rsi 中
sum_small:
    leaq    (%rdi, %rsi), %rax  # rax = s.a + s.b
    ret
 
# 大结构体:通过指针传递
sum_big:
    # %rdi 指向结构体的指针
    # 编译器在调用处分配副本
    movq    $0, %rax
    movl    $0, %ecx
.Lloop:
    addq    (%rdi, %rcx, 8), %rax
    addq    $1, %rcx
    cmpq    $10, %rcx
    jl      .Lloop
    ret

联合体(union)

基本特性

联合体的所有成员共享同一块存储空间,大小等于最大成员的大小。

union U1 {
    char   c;     // 1 字节
    short  s;     // 2 字节
    int    i;     // 4 字节
    long   l;     // 8 字节
};  // 大小 8 (最大成员为 long)
 
union U2 {
    char   c[5];  // 5 字节
    int    i;     // 4 字节
};  //大小 8 (5对齐到 4的倍数 $→ 8)$
union U1 的内存布局(同一块内存的不同解读):
地址: 0  1  2  3  4  5  6  7
     [l                          ]  ← 作为 long 读取
     [    i    ]                    ← 作为 int 读取
     [  s  ]                        ← 作为 short 读取
     [c]                             ← 作为 char 读取

联合体的核心用途

1. 节省内存(多选一的字段)

// 节点可以是整数或浮点数,通过 type 区分
struct Node {
    int type;       // 0: int, 1: float
    union {
        int   i;
        float f;
    } data;
};  // 大小 8 (4 + 4),而不是 12 (4 + 4 + 4)

2. 数据重新解释(type punning)

union DoubleInt {
    double d;
    unsigned long long i;
};
 
void inspect_double(double x) {
    union DoubleInt u;
    u.d = x;
    // 以 64 位整数的形式查看 double 的位表示
    printf("0x%016llx\n", u.i);
}

位段(Bit Fields)

结构体中的位段允许精确指定成员占用的位数:

struct Flags {
    unsigned int read   : 1;  // 1 位
    unsigned int write  : 1;  // 1 位
    unsigned int exec   : 1;  // 1 位
    unsigned int owner  : 5;  // 5 位
};  // 大小 4 (底层 unsigned int)

结构体与联合的对比

特性structunion
存储方式成员依次排列成员共享同一空间
总大小所有成员大小之和 + 填充最大成员的大小(可能含填充)
同时有效值所有成员同时有效只有最后一个赋值有效
典型用途复合对象节省空间 / 类型重解释

相关笔记

    • 结构体填充的原因与规则
    • 结构体数组的布局
    • 结构体成员的寻址模式

链接到