14. 联合体与结构体的混合使用:结构体中包含联合体、联合体中包含结构体
各位同学,咱们今天聊一个在实际工程中非常常见、但也容易让人绕晕的话题——联合体和结构体怎么混着用。
说白了,就是两种组合方式:结构体里套联合体,以及联合体里套结构体。这两种模式在嵌入式开发、协议解析、内存复用这些场景下,几乎是天天见。我个人习惯把这种用法叫做“数据结构的套娃”,但别怕,套娃也有套娃的规矩。
14.1 为什么需要混合使用?
先问一个问题:你写一个数据包,里面有一个字段是“类型”,根据类型不同,后面的数据格式完全不一样。你会怎么设计?
用纯结构体?那你会定义出好几个不同的结构体,每次都要判断类型再强转,代码又臭又长。用纯联合体?联合体本身没有“类型标识”,你没法知道当前存的是哪种数据。
嗯,这里就是混合使用的用武之地了。用结构体来承载“公共信息”(比如类型标识),用联合体来承载“可变数据”。这样既保留了类型信息,又做到了内存复用。
核心思想:结构体负责“组织”,联合体负责“复用”。两者结合,就能用最小的内存表达最灵活的数据结构。
14.2 结构体中包含联合体
这是最常见的用法。我在项目中遇到过好几次,比如解析不同版本的传感器数据帧,帧头一样,但数据体长度和含义不同。用结构体套联合体,一行代码都不用改,就能兼容多个版本。
// 结构体中包含联合体:根据类型选择不同的数据解释方式
#include <stdio.h>
#include <string.h>
// 定义几种不同的数据格式
typedef struct {
int x;
int y;
} Point2D;
typedef struct {
int x;
int y;
int z;
} Point3D;
typedef struct {
float latitude;
float longitude;
} GPSData;
// 主结构体:公共头部 + 联合体数据区
typedef struct {
int dataType; // 0: 2D点, 1: 3D点, 2: GPS
union {
Point2D p2d;
Point3D p3d;
GPSData gps;
} data; // 联合体成员,根据dataType选择解释方式
} DataPacket;
int main() {
DataPacket packet;
// 存储一个2D点
packet.dataType = 0;
packet.data.p2d.x = 10;
packet.data.p2d.y = 20;
printf("2D Point: (%d, %d)\n", packet.data.p2d.x, packet.data.p2d.y);
// 存储一个GPS数据(会覆盖之前的2D点数据)
packet.dataType = 2;
packet.data.gps.latitude = 39.9042f;
packet.data.gps.longitude = 116.4074f;
printf("GPS: (%.4f, %.4f)\n", packet.data.gps.latitude, packet.data.gps.longitude);
return 0;
}
你看,DataPacket 这个结构体里,dataType 是公共的,data 是联合体。不管存哪种数据,整个结构体的大小只取决于最大的那个联合体成员。我算过,这个结构体的大小是 sizeof(int) + max(sizeof(Point2D), sizeof(Point3D), sizeof(GPSData)),也就是 4 + 12 = 16 字节(假设 int 是 4 字节)。
我的习惯:在结构体里放联合体时,我会把联合体成员放在结构体的最后,这样前面的公共字段可以快速访问,不会因为联合体的大小变化而偏移。
14.3 联合体中包含结构体
这种用法稍微少见一点,但一旦用上,往往是为了解决“同一块内存,多种解读方式”的问题。说白了,就是你想用不同的结构体去解析同一段内存。
举个例子,你从串口收到一个 4 字节的数据,它可能是一个 int32,也可能是四个 uint8 组成的字节数组,还可能是一个 RGB 颜色值(三个字节加一个保留字节)。用联合体套结构体,就能轻松切换视角。
// 联合体中包含结构体:同一块内存,多种解读方式
#include <stdio.h>
#include <stdint.h>
// 定义一个联合体,里面包含多个结构体
typedef union {
uint32_t rawValue; // 原始32位整数值
// 结构体1:拆分成4个字节
struct {
uint8_t byte0;
uint8_t byte1;
uint8_t byte2;
uint8_t byte3;
} bytes;
// 结构体2:拆分成两个16位值
struct {
uint16_t lowWord;
uint16_t highWord;
} words;
// 结构体3:RGB颜色 + 保留字节
struct {
uint8_t red;
uint8_t green;
uint8_t blue;
uint8_t alpha; // 保留或透明度
} color;
} MultiViewData;
int main() {
MultiViewData data;
// 写入一个32位值
data.rawValue = 0x12345678;
printf("原始值: 0x%08X\n", data.rawValue);
printf("字节视角: 0x%02X 0x%02X 0x%02X 0x%02X\n",
data.bytes.byte0, data.bytes.byte1,
data.bytes.byte2, data.bytes.byte3);
printf("16位视角: 低16位=0x%04X, 高16位=0x%04X\n",
data.words.lowWord, data.words.highWord);
printf("颜色视角: R=0x%02X, G=0x%02X, B=0x%02X, A=0x%02X\n",
data.color.red, data.color.green,
data.color.blue, data.color.alpha);
return 0;
}
这里要注意一个细节:字节序问题。在小端模式下(x86、ARM 默认),byte0 对应的是最低地址的字节,也就是 0x78。如果你在通信协议里用这种方式,一定要确认双方字节序一致。我曾经因为没注意这个,调试了整整一个下午,最后发现是大小端搞反了。
避坑指南:联合体中的结构体成员,其内存布局是实现定义的。不同编译器、不同优化选项,可能会对结构体进行对齐填充。如果你要跨平台使用,建议用 #pragma pack(1) 或 __attribute__((packed)) 强制紧凑对齐。
14.4 更复杂的嵌套:结构体套联合体再套结构体
有时候,一层嵌套不够用。比如你要解析一个复杂的协议帧,帧头里有多个可变字段,每个字段又可能有多种子格式。这时候就需要多层嵌套。
我画了一张图,帮你理清这种嵌套关系:
你看,外层是一个结构体,里面有一个联合体,联合体里又包含了两个不同的结构体。这种设计在工业协议、网络协议里非常常见。比如 Modbus 协议、CAN 总线协议,底层都是这种思路。
14.5 实际工程中的注意事项
说了这么多,最后总结几条我踩过的坑,你写代码时多留个心眼:
- 内存对齐问题:结构体套联合体时,整个结构体的对齐方式由最严格的那个成员决定。如果你不确定,用
sizeof()打印一下,别靠猜。 - 初始化要小心:如果你用
= {0}初始化一个包含联合体的结构体,它只会把联合体的第一个成员清零。如果你第一个成员不是最大的,其他成员可能残留垃圾数据。 - 不要直接 memcpy 整个结构体:尤其是跨平台传输时,结构体内部的填充字节可能不同。我建议你序列化时,只拷贝有效字段,或者用
#pragma pack(1)强制紧凑。 - 联合体里的结构体成员顺序:C 标准保证结构体成员按声明顺序排列,但联合体里的多个结构体共享同一块内存,它们的起始地址相同。这意味着,如果你在联合体里放了两个结构体,它们的第一个成员会重叠。
我的建议:在嵌入式开发中,如果你要定义“结构体套联合体”的数据结构,最好在注释里写明每个字段的字节偏移和大小。这样半年后你回头看代码,不用重新数偏移量。
好了,关于联合体和结构体的混合使用,就聊到这里。这两种组合方式,说白了就是“用结构体搭骨架,用联合体填血肉”。你只要记住:结构体负责组织逻辑,联合体负责复用内存,两者结合就能写出既灵活又高效的数据结构。
下次写协议解析或者数据包处理时,不妨试试这种设计。你会发现,代码量少了一半,可读性却翻了一倍。
公众号:蓝海资料掘金营,微信deep3321