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)结构体与联合的对比
| 特性 | struct | union |
|---|---|---|
| 存储方式 | 成员依次排列 | 成员共享同一空间 |
| 总大小 | 所有成员大小之和 + 填充 | 最大成员的大小(可能含填充) |
| 同时有效值 | 所有成员同时有效 | 只有最后一个赋值有效 |
| 典型用途 | 复合对象 | 节省空间 / 类型重解释 |
相关笔记
-
- 结构体填充的原因与规则
-
- 结构体数组的布局
-
- 结构体成员的寻址模式