第16章 共用体与枚举:C语言里的“多面手”与“命名师”
说实话,C语言里有些特性,平时用得不多,但一旦遇到合适的场景,那真是“一招鲜,吃遍天”。共用体(union)和枚举(enum)就是这样的存在。我刚开始学的时候也觉得它们有点鸡肋,直到在项目中踩过坑、省过内存,才真正体会到它们的妙处。今天咱们就好好聊聊这两个家伙,顺带把位域也捎上。
16.1 共用体union:同一块内存,不同的“面孔”
共用体,说白了就是让多个变量共享同一块内存空间。你想想看,如果有一个数据,有时候是整数,有时候是浮点数,但不会同时出现,那何必为它准备两份独立的内存呢?共用体就是干这个的。
16.1.1 定义与基本用法
定义共用体的语法和结构体很像,只是关键字换成了 union:
union Data {
int i;
float f;
char str[20];
};
这里 union Data 的三个成员共享同一块内存。这块内存的大小,由最大的成员决定——也就是 char str[20] 的20个字节。我习惯把共用体看作一个“多用途容器”,你往里面放什么,它就变成什么。
使用的时候,和结构体一样用点号或箭头:
union Data data;
data.i = 10;
printf("data.i = %d\n", data.i);
data.f = 3.14;
printf("data.f = %f\n", data.f);
注意!当你给 data.f 赋值后,之前存的 data.i 就被覆盖了。这就是共用体的核心特性——同一时刻只能保存一个成员的值。
16.1.2 我在项目中用过的场景
我记得有一次做嵌入式通信协议解析,数据包里的某个字段,根据类型不同,可能是8位整数、16位整数,或者是一个浮点数。如果用结构体,得定义三个独立变量,浪费内存不说,代码还啰嗦。用共用体就清爽多了:
union PacketPayload {
uint8_t byteVal;
uint16_t wordVal;
float floatVal;
};
解析时根据类型标签,往对应的成员里写数据,然后读取。内存只占最大成员的大小,在资源紧张的MCU上,这省下来的每一字节都很珍贵。
16.1.3 共用体与结构体的嵌套
共用体和结构体可以组合使用,这在协议解析中非常常见。比如一个数据帧,头部固定,负载可变:
struct Frame {
uint8_t type;
uint8_t length;
union {
uint8_t raw[8];
struct {
int16_t x;
int16_t y;
} point;
float value;
} payload;
};
这样,根据 type 字段,你可以用不同的方式解读 payload。我个人觉得,这种设计比用一堆指针强制转换要安全得多,至少编译器会帮你做类型检查。
16.2 枚举enum:给数字起个“人话”名字
枚举,说白了就是给整数常量起个名字。你写 #define RED 0 也能达到类似效果,但枚举更整洁,而且有作用域限制。
16.2.1 定义与基本用法
enum Color {
RED, // 默认0
GREEN, // 默认1
BLUE // 默认2
};
默认情况下,枚举值从0开始递增。你也可以手动指定:
enum Status {
OK = 0,
ERROR = -1,
TIMEOUT = 1
};
使用枚举变量时,代码的可读性会大幅提升:
enum Color c = RED;
if (c == GREEN) {
printf("绿色\n");
}
你想想看,如果直接写 if (c == 1),过两个月你自己都看不懂这1代表什么。枚举就是帮你消除这种“魔法数字”的。
16.2.2 枚举在状态机中的应用
我在做嵌入式状态机时,特别喜欢用枚举。比如一个简单的按键扫描状态机:
enum KeyState {
KEY_IDLE,
KEY_PRESSED,
KEY_RELEASED
};
enum KeyState state = KEY_IDLE;
void key_scan(void) {
switch (state) {
case KEY_IDLE:
if (key_pin_read() == 0) {
state = KEY_PRESSED;
}
break;
case KEY_PRESSED:
if (key_pin_read() == 1) {
state = KEY_RELEASED;
}
break;
case KEY_RELEASED:
// 处理按键释放事件
state = KEY_IDLE;
break;
}
}
用枚举定义状态,switch-case 结构一目了然。我曾经见过有人用 #define 定义状态,结果因为宏没有作用域,不小心和别的模块冲突了,查了半天bug。枚举就没有这个问题。
MAX_STATE,用来做边界检查。
16.3 位域:精确控制每一位
位域是C语言里一个比较“底层”的特性,它允许你以位为单位定义结构体成员。这在硬件寄存器操作中特别有用。
16.3.1 定义与语法
struct BitField {
unsigned int a : 1; // 占1位
unsigned int b : 3; // 占3位
unsigned int c : 4; // 占4位
};
这里 a 只占1位,b 占3位,c 占4位。整个结构体通常只占一个 unsigned int 的空间(具体取决于编译器)。
16.3.2 实际应用:硬件寄存器操作
我记得在做STM32驱动时,控制寄存器的各个位就是通过位域来操作的。比如一个控制寄存器的定义:
struct ControlReg {
unsigned int enable : 1;
unsigned int mode : 2;
unsigned int irq_en : 1;
unsigned int : 2; // 保留位,不命名
unsigned int speed : 2;
};
然后你可以直接操作这些位域,就像操作普通结构体成员一样:
volatile struct ControlReg* reg = (volatile struct ControlReg*)0x40021000;
reg->enable = 1;
reg->mode = 2;
编译器会自动生成移位和掩码操作,比你手动写 REG |= (1 << 0) 要直观得多。
16.4 知识体系总览
下面这张图帮你理清本章的核心脉络:
16.5 避坑指南与经验总结
讲到这里,我得分享几个我亲身踩过的坑:
- 共用体赋值后立即读取其他成员:我曾经在调试一个通信协议时,往共用体的
float成员写了值,然后去读int成员,想当然地以为能得到整数部分。结果当然是乱码。记住,共用体不是“类型转换器”,它只是让你用不同的视角看同一块内存。 - 枚举值重复:如果手动指定枚举值时不小心重复了,编译器不会报错(至少默认情况下不会)。我建议在枚举定义后加一个静态断言来检查唯一性。
- 位域的跨平台问题:位域的顺序和填充方式,不同编译器可能不同。如果你写的是库代码,要提供给不同平台使用,最好别用位域,改用移位宏。
📌 核心要点回顾:
- 共用体:多个成员共享内存,同一时刻只存一个值。适合协议解析、变体数据。
- 枚举:给整数起名字,提升代码可读性。状态机的好搭档。
- 位域:按位定义结构体成员,硬件驱动开发利器。注意跨平台兼容性。
嗯,这一章的内容就到这里。共用体、枚举和位域,虽然不像指针、结构体那样天天用,但用对地方,能让你的代码更优雅、更高效。下次写代码时,不妨想想:这里能不能用枚举代替魔法数字?这个数据结构能不能用共用体省点内存?
公众号:蓝海资料掘金营,微信deep3321