本文从定义、底层原理、使用方法、典型案例四个层面系统梳理 C 语言的结构体(struct)与联合体(union),并通过内存布局图、代码示例与对比表格帮助建立清晰、可落地的认知。
目录
一、一句话先记住本质区别
- 结构体(struct):把不同类型的数据打包在一起,每个成员各自拥有独立内存,总大小是各成员(含对齐填充)的累加。
- 联合体(union):让多个成员共用同一段内存,同一时刻只能保存其中一个成员的值,总大小等于最大成员(含对齐)的大小。
二、结构体 struct:定义、原理与使用
2.1 定义
结构体是一种用户自定义的聚合类型,将若干可能不同类型的变量组合成一个整体,每个变量称为一个成员(member)。
struct Student {
char name[20]; // 数组成员
int age; // 整型成员
float score; // 浮点成员
};
定义方式有三种:
// 1. 直接定义变量
struct Student s1;
// 2. 定义类型的同时定义变量
struct Student { char name[20]; int age; } s2;
// 3. 用 typedef 起别名(最常用,省去 struct 关键字)
typedef struct {
char name[20];
int age;
float score;
} Student;
Student s3; // 直接使用别名
2.2 原理
编译器为结构体分配连续内存,每个成员在内存中按声明顺序依次排列,并通过 &s.成员 取得各自的地址。成员之间可能存在**填充字节(padding)**以满足对齐要求(详见第五节)。
下图展示结构体成员的独立存储与对齐填充:

2.3 使用方法
typedef struct { int x; int y; } Point;
Point p;
p.x = 3; // 点运算符访问成员
p.y = 4;
Point *pp = &p;
pp->x = 10; // 指针用箭头运算符访问
printf("(%d, %d)\\n", p.x, p.y);
结构体还支持整体赋值与作为函数参数/返回值:
Point a = {1, 2};
Point b = a; // 合法:结构体可整体拷贝
void move(Point q) { q.x += 1; q.y += 1; } // 值传递会拷贝整个结构体
三、联合体 union:定义、原理与使用
3.1 定义
联合体的语法与结构体几乎相同,唯一关键字差异是把 struct 换成 union:
union Data {
int i;
float f;
char c;
};
3.2 原理
联合体的所有成员起始于同一地址,共享同一块内存。这块内存的大小取最大成员所需的大小。因此:
- 同一时刻只有一个成员是"有效"的,写入新成员会覆盖旧成员的二进制内容;
- sizeof(union) 由最大成员和对齐共同决定,不随成员个数累加。

3.3 使用方法
union Data d;
d.i = 65; // 写入整型成员
printf("%d\\n", d.i); // 65
d.f = 3.14f; // 之后写浮点,i 的值已被破坏
printf("%.2f\\n", d.f); // 3.14(此时读 d.i 是无意义的)
关键纪律:用哪个成员写入,就用哪个成员读出,不要"交叉读取"未写入的成员(除少数刻意利用二进制表示的技巧外)。
四、核心区别与内在联系(对比表)
| 关键字 | struct | union |
| 成员内存 | 各自独立、依次排列 | 共享同一块内存 |
| 总大小 | 各成员(含填充)之和 | 最大成员大小(含对齐) |
| 同时有效成员 | 全部成员可同时存在 | 同一时刻仅 1 个 |
| 成员地址 | 互不相同 | 完全相同 |
| 写入影响 | 互不影响 | 后写覆盖先写 |
| 典型用途 | 描述"一个事物的多个属性" | 节省内存 / 同一数据的多种解释 |
| 初始化 | 可同时初始化多个成员 | 一般只能初始化首个(C99 起可指定) |
内在联系
五、内存对齐:二者共用的底层规则
CPU 按"块"读取内存,成员若落在对齐边界上访问更快。常见规则:
- 每个成员的偏移量必须是其自身对齐值(通常为 sizeof(类型))的整数倍,否则编译器插入 padding;
- 结构体总大小必须是最大成员对齐值的整数倍,否则在末尾补 padding;
- 联合体大小 = 最大成员大小,并向上取整到最大对齐值的整数倍。
经典示例:成员顺序影响 sizeof
struct A { char c; int i; char d; }; // 1+3(pad)+4+1+3(pad) = 12
struct B { char c; char d; int i; }; // 1+1+2(pad)+4 = 8
经验法则:声明结构体时把成员按占用字节从大到小排列,可减少填充、节省内存。
六、典型案例实战
案例 1:结构体描述"多属性实体"
#include <stdio.h>
#include <string.h>
typedef struct {
char name[32];
int year;
float rating;
} Movie;
int main(void) {
Movie m = {"Inception", 2010, 8.8f};
strcpy(m.name, "Interstellar"); // 数组成员用函数修改
printf("%s (%d) score=%.1f\\n", m.name, m.year, m.rating);
return 0;
}
// 输出:Interstellar (2010) score=8.8
案例 2:联合体做"省内存的变体"
需求:一个变量在不同场景下分别表示整数、浮点或字符,但任意时刻只用一种,用联合体避免为每种类型都分配空间。
#include <stdio.h>
typedef union {
unsigned char bytes[4];
int value;
} IntBytes; // sizeof = 4
int main(void) {
IntBytes ib;
ib.value = 0x41424344;
// 通过 bytes 成员观察同一块内存的逐字节表示(与大小端相关)
for (int k = 0; k < 4; k++)
printf("%02X ", ib.bytes[k]);
printf("\\n");
return 0;
}
// 小端机器输出:44 43 42 41
案例 3:联合体 + 结构体实现"类型标签联合"(tagged union)
结构体与联合体配合的经典模式,联合体负责"多态地存",结构体里的标签负责"标明当前是哪种":
#include <stdio.h>
typedef struct {
enum { INT_T, FLOAT_T } type; // 标签:当前有效的是哪个成员
union {
int i;
float f;
} data; // 联合体:共享存储
} Variant;
int main(void) {
Variant v;
v.type = INT_T; v.data.i = 42; // 写入并打标签
if (v.type == INT_T) printf("int = %d\\n", v.data.i); // 按标签读取
v.type = FLOAT_T; v.data.f = 3.5f; // 切换成员
if (v.type == FLOAT_T) printf("float = %.1f\\n", v.data.f);
return 0;
}
实际工程里更稳妥的做法是封装 set_int()/set_float() 函数,在写成员的同时设置标签,保证二者永远一致,杜绝"读错解释方式"。
案例 4:结构体"柔性数组"实现变长对象
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int len;
int data[]; // 柔性数组成员(必须是最后一个成员,C99)
} Buffer;
int main(void) {
int n = 5;
Buffer *b = malloc(sizeof(Buffer) + n * sizeof(int));
b->len = n;
for (int i = 0; i < n; i++) b->data[i] = i * i;
printf("last = %d\\n", b->data[n-1]); // last = 16
free(b);
return 0;
}
七、易错点与最佳实践
八、小结
- 结构体是"加法":为每个成员分配独立空间,用来描述一个对象的全部属性。
- 联合体是"重叠":让多个成员共用同一块空间,用来在互斥的多种解释/表示之间切换、节省内存。
- 二者语法与访问方式一致,共享同一套内存对齐规则,且可互相嵌套,组合出如 tagged union、柔性数组等实用结构。
理解差异的钥匙只有一句话:结构体让成员"并存",联合体让成员"互斥"。把握了这一点,再复杂的嵌套与 sizeof 计算都能从容应对。



