联合体 (union)
索引
1. 什么是联合体
联合体和结构体很像,但有一个关键区别:
// 结构体 - 每个成员都有自己的内存
struct MyStruct {
int a; // 4字节
float b; // 4字节
char c; // 1字节
}; // 总共 9字节(加对齐可能更多)
// 联合体 - 所有成员共享同一块内存
union MyUnion {
int a; // 4字节
float b; // 4字节
char c; // 1字节
}; // 总共只有 4字节(最大成员的大小)核心特点:所有成员从同一个地址开始,同时只有一个成员的值是”有效”的。
2. 内存示意图
结构体(各成员独立占用空间):
┌────┬────┬────┬────┬────┬────┬────┬────┬────┐
│ a (int) │ b (float) │ c │...│...│
└────┴────┴────┴────┴────┴────┴────┴────┴────┘
联合体(所有成员共享同一块内存):
┌────┬────┬────┬────┐
│ a (int) │
│ b (float) │ ← 同一块内存!
│ c │ │
└────┴────┴────┴────┘
3. 基本用法
#include <stdio.h>
union Data {
int i;
float f;
char str[4];
};
int main(void) {
union Data d;
d.i = 0x41424344;
printf("int: %d\n", d.i); // 1094861636
printf("float: %f\n", d.f); // 12.141...(同一块内存解读为float)
printf("str: %c%c%c%c\n", d.str[0], d.str[1], d.str[2], d.str[3]);
// // DCBA(小端序)
// 写float会覆盖之前的int!
d.f = 3.14f;
printf("int now: %d\n", d.i); // 值变了!因为内存被覆盖
return 0;
}4. 实际用途1:协议解析(最常用!)
这正是嵌入式项目中最常见的场景——把字节数组和结构体绑在一起,解析超方便:
// 协议格式:[0x5A][0xA5][Length][SenderID 4-bytes][Command]...
// 用联合体把字节数组和结构体绑在一起
typedef union {
// 原始字节视图
unsigned char raw[10];
// 结构化视图
struct {
unsigned char head1; // 0x5A
unsigned char head2; // 0xA5
unsigned char length;
unsigned long sender_id; // 4字节ID
unsigned char command;
unsigned char data;
} fields;
} Protocol_t;
void parse_packet(unsigned char *buf) {
Protocol_t pkt;
// 直接把收到的字节塞进去
for (int i = 0; i < 10; i++)
pkt.raw[i] = buf[i];
// 用结构体成员读取,清晰!
if (pkt.fields.head1 == 0x5A && pkt.fields.head2 == 0xA5) {
printf("命令: 0x%02X\n", pkt.fields.command);
printf("发送方ID: 0x%08lX\n", pkt.fields.sender_id);
}
}5. 实际用途2:大小端 / 字节拆分
嵌入式里超常用——把一个 int 拆成4个字节分别发送:
union U32_Bytes {
unsigned long value; // 32位整数
unsigned char byte[4]; // 4个独立字节
};
void send_id(unsigned long device_id) {
union U32_Bytes u;
u.value = device_id; // 比如 0x12345678
// 直接取每个字节发送,不用位运算!
UART_Send(u.byte[0]); // 0x78(小端)
UART_Send(u.byte[1]); // 0x56
UART_Send(u.byte[2]); // 0x34
UART_Send(u.byte[3]); // 0x12
}
// 对比传统写法(繁琐):
// UART_Send((id >> 0) & 0xFF);
// UART_Send((id >> 8) & 0xFF);
// UART_Send((id >> 16) & 0xFF);
// UART_Send((id >> 24) & 0xFF);6. 实际用途3:节省内存(8位MCU必备)
RAM 紧张时,不同状态下用不同的数据,但不会同时用到:
union StateData {
struct { // 配对状态时用
unsigned char target_id[4];
unsigned char retry_count;
} pairing;
struct { // 工作状态时用
unsigned char last_cmd;
unsigned char valve_status;
unsigned int timer_ms;
} working;
};
// 只占 max(5, 4) = 5字节,而不是 5+4 = 9字节7. 常见误区
union U {
int a;
float b;
};
union U u;
u.a = 42; // 写入 a
u.b = 3.14f; // 写入 b,此时 a 的值已经无意义!
printf("%d\n", u.a); // ❌ 未定义行为!b覆盖了a的内存
printf("%f\n", u.b); // ✅ 最后写的是b,读b有效记住:写哪个成员,就只能读哪个成员(除非故意用于类型转换)。
8. 总结对比
| 特性 | struct | union |
|---|---|---|
| 内存 | 各成员独立 | 共享最大成员的空间 |
| 同时有效 | 所有成员 | 只有最后写入的 |
| 典型用途 | 组合数据 | 协议解析、类型转换、省内存 |