4. 联合体(union)深度解析:联合体的定义、内存共享特性、大小端检测实战
联合体这个东西,刚学C语言的时候我其实挺困惑的。结构体好歹每个成员都有自己的窝,联合体倒好,所有成员挤在一个房间里。说白了,联合体就是一种所有成员共享同一块内存的数据类型。今天咱们就把它的底裤扒干净。
4.1 联合体的定义与基本语法
联合体的定义和结构体长得像亲兄弟,但内核完全不同。看个例子:
union Data {
int i;
float f;
char str[4];
};
这里定义了一个叫 Data 的联合体类型。它有三个成员,但这三个成员共用同一段内存空间。你想想看,如果这是个结构体,那大小会是 sizeof(int) + sizeof(float) + sizeof(char[4]),差不多12个字节。但联合体呢?它只取最大成员的大小,也就是4个字节。
核心要点:联合体的大小等于其最大成员的大小。所有成员共享同一块内存的起始地址。
我在项目中遇到过有人把联合体和结构体搞混,结果数据解析全乱套了。嗯,这里要记住:联合体同一时刻只能保存一个成员的值。你给 i 赋值,再去读 f,拿到的就是一堆垃圾。
4.2 内存共享特性——联合体的灵魂
联合体最迷人的地方就是内存共享。咱们用代码说话:
#include <stdio.h>
union Test {
int a;
char b[4];
};
int main() {
union Test t;
t.a = 0x12345678;
printf("a = 0x%x\n", t.a);
printf("b[0] = 0x%x\n", (unsigned char)t.b[0]);
printf("b[1] = 0x%x\n", (unsigned char)t.b[1]);
printf("b[2] = 0x%x\n", (unsigned char)t.b[2]);
printf("b[3] = 0x%x\n", (unsigned char)t.b[3]);
return 0;
}
这段代码在小端机器上跑,输出会是:
a = 0x12345678
b[0] = 0x78
b[1] = 0x56
b[2] = 0x34
b[3] = 0x12
为什么会这样?因为 a 和 b 共享同一块4字节内存。你写 a 的时候,实际上就是在写那4个字节。然后你通过 b 数组去读,读到的就是内存里的原始字节。
个人习惯:我经常用联合体来做数据类型的"强制转换",比指针强转安全得多。尤其是在处理通信协议的时候,一个联合体就能搞定数据包解析。
4.3 大小端检测实战
大小端问题,说白了就是数据在内存里的摆放顺序。大端模式是高位字节在低地址,小端模式是低位字节在低地址。咱们用联合体一行代码就能检测出来:
#include <stdio.h>
int check_endian() {
union {
int i;
char c;
} u = { .i = 1 };
return u.c; // 小端返回1,大端返回0
}
int main() {
if (check_endian()) {
printf("小端模式\n");
} else {
printf("大端模式\n");
}
return 0;
}
这段代码的原理很简单:int i = 1 在内存中,小端模式下低地址存的是 0x01,大端模式下低地址存的是 0x00。联合体让 char c 和 int i 共享低地址字节,所以读 c 就能知道大小端。
我曾经踩过的坑:有一次做嵌入式网络协议栈,没考虑大小端问题,直接把收到的字节流当整数用。结果在x86开发板上跑得好好的,部署到ARM板子上全乱套了。后来用联合体写了个大小端转换函数,一劳永逸。
4.4 联合体的高级用法——协议解析
在实际项目中,联合体最常见的用途就是解析通信协议。比如一个简单的数据包:
#include <stdio.h>
#include <stdint.h>
// 数据包结构
typedef struct {
uint8_t header; // 帧头
uint8_t len; // 数据长度
uint16_t crc; // 校验
uint8_t data[8]; // 数据
} Packet;
// 联合体:方便按字节访问
union PacketView {
Packet pkt;
uint8_t raw[sizeof(Packet)];
};
int main() {
union PacketView pv;
// 模拟接收到的原始数据
pv.raw[0] = 0xAA; // 帧头
pv.raw[1] = 0x04; // 长度
pv.raw[2] = 0x12; // CRC低字节
pv.raw[3] = 0x34; // CRC高字节
pv.raw[4] = 0x01; // 数据
pv.raw[5] = 0x02;
pv.raw[6] = 0x03;
pv.raw[7] = 0x04;
printf("帧头: 0x%02X\n", pv.pkt.header);
printf("长度: %d\n", pv.pkt.len);
printf("CRC: 0x%04X\n", pv.pkt.crc);
return 0;
}
你看,通过联合体,我们可以把原始字节流和结构体无缝对接。这在嵌入式开发里太常见了——串口收到一堆字节,用联合体一映射,直接按字段访问,省去了手动解析的麻烦。
4.5 联合体与结构体的嵌套
联合体和结构体可以嵌套使用,这在处理复杂协议时特别有用。比如一个数据包可能有多种类型:
#include <stdio.h>
// 不同类型的消息体
struct MsgA {
int x;
int y;
};
struct MsgB {
float value;
char name[8];
};
// 消息联合体
union MessageBody {
struct MsgA a;
struct MsgB b;
};
// 完整消息结构
struct Message {
int type; // 消息类型
union MessageBody body; // 消息体
};
int main() {
struct Message msg;
msg.type = 1; // 类型A
// 访问类型A的数据
msg.body.a.x = 100;
msg.body.a.y = 200;
printf("Type A: x=%d, y=%d\n", msg.body.a.x, msg.body.a.y);
// 切换类型
msg.type = 2;
msg.body.b.value = 3.14f;
printf("Type B: value=%.2f\n", msg.body.b.value);
return 0;
}
我建议:用联合体做协议解析时,一定要加一个类型字段来标记当前联合体里存的是什么。不然你读出来的数据可能完全是另一回事。这就像你打开一个箱子,得先知道里面装的是衣服还是工具,不然瞎摸会出问题。
4.6 联合体的注意事项
| 注意事项 | 说明 |
|---|---|
| 初始化 | C99支持指定初始化器,如 union U u = { .member = value }; |
| 对齐问题 | 联合体的对齐方式由其最严格对齐的成员决定 |
| 匿名联合体 | C11支持匿名联合体,成员可以直接访问 |
| 位域联合体 | 联合体成员可以是位域,用于精确控制比特位 |
警告:不要用联合体去访问同一个成员的不同类型,除非你清楚知道自己在做什么。比如你写了一个 float,然后去读 int,这种行为是未定义的。联合体只保证存储共享,不保证解释正确。
4.7 联合体知识体系总览
下面这张图把联合体的核心知识点串起来了,你可以对照着复习:
联合体这东西,说白了就是C语言给开发者的一把瑞士军刀。用好了,代码简洁高效;用不好,bug藏得你找不到。我个人建议你在做底层开发、协议解析、数据转换的时候多想想联合体,它往往能给你一个优雅的解决方案。
公众号:蓝海资料掘金营,微信deep3321