第26章:共用体与枚举——让数据更灵活,让代码更可读

说实话,C语言里有些特性,你平时可能用得不多,但一旦遇到合适的场景,它们就像一把瑞士军刀,特别管用。共用体(union)和枚举(enum)就是这样的存在。再加上typedef这个“起外号”的工具,今天这一章,我们就把这三兄弟彻底搞明白。

一、共用体(union)——同一块内存,不同的解读方式

先问大家一个问题:你有没有遇到过这种情况——同一个数据,有时候你想把它当整数读,有时候又想把它拆成字节看?比如在通信协议里,一个4字节的整型数据,你需要按字节去打包、拆包。

共用体就是干这个的。它让多个变量共享同一块内存空间。说白了,就是给同一块内存起了好几个名字,每个名字有不同的类型。

1. union的定义与基本用法

union Data {
    int i;
    float f;
    char str[4];
};

你看,这个union里有三个成员:int、float、char数组。它们从同一个地址开始存放。也就是说,你往i里写数据,然后读f,得到的是同一块内存按浮点数格式解释出来的值。

大小怎么算? 共用体的大小,等于它最大成员的大小。上面这个例子,int和float都是4字节,char数组也是4字节,所以union Data的大小就是4字节。

核心要点: 共用体所有成员共享同一块内存,修改一个成员会影响其他成员。

2. 我在项目中遇到的union实战

我记得有一次做嵌入式通信协议解析,需要把一个32位的整型数据拆成4个字节发送。当时我用了这样的结构:

union Packet {
    uint32_t value;
    uint8_t bytes[4];
};

这样我往value里写入数据,然后直接读bytes[0]bytes[3]就能拿到每个字节。省去了移位和掩码操作,代码干净多了。

小技巧: 用union做字节序转换时,要注意大小端问题。不同平台下,bytes[0]可能对应的是高位字节还是低位字节,这个得心里有数。

3. 共用体的典型应用场景

  • 通信协议解析: 同一个数据包,按不同字段解读
  • 寄存器映射: 既可以按位访问,也可以按字节访问
  • 节省内存: 多个变量不会同时使用,共用一块内存
  • 类型转换: 在不违反严格别名规则的前提下,做数据格式转换

注意: 共用体不能直接用于类型双关(type punning)——就是通过一个成员写入,通过另一个不同类型的成员读取。这在C标准里是未定义行为。虽然很多编译器支持,但写可移植代码时要小心。

二、枚举(enum)——给数字起个名字

你写代码的时候,有没有用过一堆数字常量来表示状态?比如0表示空闲,1表示忙碌,2表示错误。时间一长,你自己都忘了0代表什么。枚举就是来解决这个问题的。

1. enum的定义与使用

enum State {
    IDLE,      // 默认0
    BUSY,      // 默认1
    ERROR      // 默认2
};

默认情况下,枚举常量从0开始,依次递增。你也可以手动指定值:

enum Color {
    RED = 1,
    GREEN = 2,
    BLUE = 4
};

我个人习惯在定义状态机时,用枚举来替代宏定义。为什么呢?因为枚举有类型检查,调试器也能直接显示枚举名,而不是一堆数字。

2. 枚举在状态机中的应用

我曾经写过一个简单的按键扫描程序,状态机用枚举实现,代码可读性直接拉满:

enum KeyState {
    KEY_RELEASED,
    KEY_PRESSED,
    KEY_HOLD
};

enum KeyState currentState = KEY_RELEASED;

void key_scan(void) {
    switch(currentState) {
        case KEY_RELEASED:
            if (key_pin_read() == 0) {
                currentState = KEY_PRESSED;
            }
            break;
        case KEY_PRESSED:
            if (key_pin_read() == 0) {
                currentState = KEY_HOLD;
            } else {
                currentState = KEY_RELEASED;
            }
            break;
        case KEY_HOLD:
            if (key_pin_read() != 0) {
                currentState = KEY_RELEASED;
            }
            break;
    }
}

你看,每个状态的名字都清清楚楚。要是用数字0、1、2,过两个月你自己都看不懂。

避坑指南: 我曾经在项目里用枚举定义了一堆错误码,结果后来新增了一个枚举值,不小心插在了中间,导致后面所有枚举值都变了。从那以后,我养成了一个习惯——显式指定枚举值,或者把新增的枚举值加在最后。

3. 枚举与宏定义的对比

对比项 枚举 宏定义
类型安全 有类型检查 无类型检查
调试支持 调试器显示枚举名 只显示数字
作用域 遵循块作用域 全局替换
占用空间 通常为int大小 无存储空间
适用场景 有限的状态、选项 常量、条件编译

三、typedef——给类型起个外号

typedef说白了就是给已有的类型起个新名字。它不创造新类型,只是别名。但别小看这个功能,用好了能让代码简洁很多。

1. typedef的基本用法

typedef unsigned char uint8_t;
typedef unsigned int uint16_t;

// 现在可以这样用
uint8_t data = 0xFF;
uint16_t counter = 1000;

在嵌入式开发里,我几乎每个项目都会用typedef重定义整型类型。这样写出来的代码,跨平台移植时只需要改typedef那一行,不用满世界找int、long去改。

2. typedef与结构体、共用体、枚举的结合

这是最常用的组合拳:

typedef struct {
    int x;
    int y;
} Point;

typedef enum {
    MONDAY,
    TUESDAY,
    WEDNESDAY
} Weekday;

typedef union {
    uint32_t word;
    uint8_t byte[4];
} Register;

这样定义之后,声明变量就不用每次都写structenumunion关键字了:

Point p1;
Weekday today = MONDAY;
Register reg;

我个人习惯在定义复杂类型时,总是用typedef包一层。这样代码更干净,别人看你的代码也更舒服。

小建议: 函数指针类型也适合用typedef。比如:typedef void (*Callback)(int); 这样声明回调函数时,代码会清晰很多。

四、知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以把它当作一个快速回顾的索引。

共用体、枚举与typedef知识体系 共用体 union 枚举 enum typedef 核心特性 • 所有成员共享同一块内存 • 大小为最大成员的大小 • 修改一个成员影响其他成员 核心特性 • 给整数常量起名字 • 默认从0开始递增 • 可手动指定枚举值 核心特性 • 给已有类型起别名 • 不创建新类型 • 提高代码可移植性 典型应用场景 通信协议解析 状态机实现 寄存器映射 跨平台类型定义

五、总结与避坑

好了,这一章的内容就这些。我最后再唠叨几句:

  • 共用体: 适合做数据格式转换,但要注意大小端和严格别名规则。别用它做类型双关,除非你很清楚自己在干什么。
  • 枚举: 比宏定义更安全、更可读。状态机、错误码、选项列表,用枚举准没错。记得显式指定值,避免插入新值时打乱顺序。
  • typedef: 让复杂类型变简单,让代码更可移植。结构体、枚举、函数指针,都值得用typedef包一层。

嗯,这一章就到这里。代码写多了你就会发现,这些看似不起眼的语言特性,用好了能让你的代码质量上一个台阶。


专注资料整理