联合体的典型应用:数据拆分与合并
联合体(union)在嵌入式开发中,最实用的场景就是数据拆分与合并。说白了,就是把一个多字节的数据,拆成单个字节来处理;或者反过来,把几个字节拼成一个完整的数据。我做了这么多年嵌入式,这个技巧几乎每天都在用。
为什么需要拆分与合并?
你想想看,我们写程序时经常要处理 int、float 这些类型。但硬件通信时,很多协议是按字节传输的。比如串口、I2C、SPI,一次只能发一个字节。那怎么把一个 4 字节的 int 发出去?
有人会说:「用移位和掩码操作啊。」没错,位运算确实可以。但代码写起来又长又容易出错。我个人习惯用联合体,代码简洁,可读性也强。
核心思想:联合体的所有成员共享同一块内存。你写一个成员,读另一个成员,就能实现数据格式的转换。
经典案例:将 int 拆成 4 个 byte
假设我们有一个 32 位的整数,要拆成 4 个字节发送出去。用联合体怎么做?
union IntToBytes {
int value; // 4字节整数
unsigned char bytes[4]; // 4个字节
};
就这么简单。当你给 value 赋值后,bytes[0] 到 bytes[3] 就是它的四个字节。反过来,你填好 bytes 数组,读 value 就能得到组合后的整数。
来看个完整例子:
#include <stdio.h>
#include <stdint.h>
union IntToBytes {
int32_t value;
uint8_t bytes[4];
};
int main() {
union IntToBytes data;
data.value = 0x12345678;
printf("原始值: 0x%X\n", data.value);
printf("字节拆分:\n");
printf("bytes[0] = 0x%X\n", data.bytes[0]);
printf("bytes[1] = 0x%X\n", data.bytes[1]);
printf("bytes[2] = 0x%X\n", data.bytes[2]);
printf("bytes[3] = 0x%X\n", data.bytes[3]);
// 反过来:合并
data.bytes[0] = 0xAB;
data.bytes[1] = 0xCD;
data.bytes[2] = 0xEF;
data.bytes[3] = 0x12;
printf("合并后: 0x%X\n", data.value);
return 0;
}
注意大小端问题! 我在项目中就吃过这个亏。不同处理器的大小端不一样。x86 是小端,ARM 可以配置。上面的例子在小端机器上,bytes[0] 是低字节(0x78),bytes[3] 是高字节(0x12)。如果你要跨平台通信,一定要明确协议的大小端。
IP 地址解析
IP 地址本质上就是一个 32 位整数。我们平时看到的 192.168.1.1 这种点分十进制格式,其实就是把 32 位拆成 4 个 8 位。用联合体处理再合适不过。
union IPAddress {
uint32_t addr; // 32位IP地址
uint8_t octets[4]; // 4个字节
};
void printIP(union IPAddress ip) {
printf("%d.%d.%d.%d\n",
ip.octets[0],
ip.octets[1],
ip.octets[2],
ip.octets[3]);
}
int main() {
union IPAddress myIP;
myIP.addr = 0xC0A80101; // 192.168.1.1
printf("IP地址: ");
printIP(myIP);
// 修改最后一个字节
myIP.octets[3] = 100;
printf("修改后: ");
printIP(myIP);
return 0;
}
嗯,这里要注意:网络字节序是大端。如果你从网络上收到一个 IP 地址,它可能是大端存储的。而 x86 机器是小端,直接读会有问题。我建议用 ntohl() 和 htonl() 做转换。
更复杂的应用:浮点数拆解
浮点数的拆解更有意思。有时候你要把 float 通过无线模块发出去,但模块只支持字节流。联合体就能派上用场。
union FloatToBytes {
float value;
uint8_t bytes[4];
};
union FloatToBytes sensorData;
sensorData.value = 3.14159f;
// 现在 bytes[0..3] 就是浮点数的 IEEE 754 表示
// 可以直接通过串口发送
我曾经在一个传感器项目中,需要把采集到的温度值(float)通过 2.4G 无线模块发送。接收端是另一个单片机。两边都用同样的联合体定义,数据收发完全没问题。省去了写序列化反序列化的麻烦。
数据拆分与合并的流程图
实用技巧与避坑指南
- 对齐问题:联合体的大小由最大成员决定。但编译器可能会填充字节对齐。如果你要精确控制内存布局,可以用
__attribute__((packed))或#pragma pack(1)。 - 类型安全:C 语言不会检查你读的是哪个成员。写了一个 int,读成 float,结果就是乱码。你自己要保证读写的一致性。
- 我曾经在一个项目中,用联合体解析协议帧。协议里有个 2 字节的字段,我定义成 short。结果在 32 位和 16 位平台上跑,short 大小不一样,导致解析错误。后来统一用
uint16_t才解决。
我的建议:在嵌入式开发中,联合体配合 stdint.h 里的定长类型(uint8_t、uint16_t、uint32_t)使用,可以避免平台差异带来的问题。另外,如果你要处理的数据结构比较复杂,可以考虑用联合体嵌套结构体,这样代码更清晰。
总结一下
联合体的数据拆分与合并,说白了就是利用内存共享的特性,在不同视角下看同一块数据。这个技巧在嵌入式通信、协议解析、数据存储中非常实用。你想想看,不用写一堆移位和掩码操作,代码简洁多了。
不过要记住:大小端、对齐、类型安全,这三个坑一定要避开。我刚开始用联合体时也踩过几次,后来养成了用定长类型和显式指定对齐的习惯,就再也没出过问题。
公众号:蓝海资料掘金营,微信deep3321