本节摘要:共用体的全体成员共享同一块内存(大小取最大成员),是"同一数据的多种解读"的语言化表达——解析硬件报文、看浮点的字节、实现变体记录都靠它。枚举给整数常量起名,本质仍是 int。typedef 给现有类型起新名,让函数指针与复杂声明变得可读。三件工具合起来,就是 C 的"自定义类型系统"。
#include <stdio.h> union View { float f; unsigned int u; unsigned char byte[4]; }; int main(void) { union View v; v.f = 1.0f; /* 按 float 写入 */ printf("按无符号整数读 %08X\n", v.u); /* 3F800000:IEEE 754 的 1.0 */ for (int i = 0; i < 4; i++) printf("字节%d = %02X\n", i, v.byte[i]); return 0; }
输出 3F800000——1.0f 的 IEEE 754 位模式(符号 0、指数 127、尾数 0)。第 2 章用 char 指针转换看字节,共用体是同一件事的类型安全版:不用指针转换,语言层面声明"这 4 字节有两种合法解读"。sizeof(union View) 是 4——所有成员叠在同一地址,大小取决于最大的那个(同样要对齐补齐)。
核心纪律:写一个成员后读另一个,读出的是同一批字节的重新解读;同一时间只有一个成员是"活跃"的,程序自己负责记住上次写的是谁。C 的类型系统不跟踪这件事,读错了成员就是合法但荒谬的结果——这不是编译器能救的,是设计契约问题(C23 才引入了带标签的变体提案雏形)。
共用体最经典的实战是报文双视图:
/* 一个 4 字节的硬件状态寄存器:整体是数值,拆开是标志位与字段 */ union Status { unsigned int raw; /* 整体读写,用于总线传输 */ struct { unsigned ready : 1; /* 位段:第 0 位 */ unsigned error : 1; /* 第 1 位 */ unsigned mode : 3; /* 第 2-4 位,共 3 位 */ unsigned reserved: 27; } bits; /* 按字段读写,用于逻辑判断 */ }; int main(void) { union Status s; s.raw = 0x00000005; /* 从硬件总线读到的原始值 */ printf("ready=%d error=%d mode=%u\n", s.bits.ready, s.bits.error, s.bits.mode); /* 1 0 1 */ s.bits.error = 1; /* 改一个标志位 */ printf("raw = 0x%X\n", s.raw); /* 0x7:位改动直接反映在整体值上 */ return 0; }
一个 union 同时提供"整体视图"(raw)与"字段视图"(bits)——读寄存器、配寄存器、解析网络协议头,全用这一招。位段(冒号数字)指定每个成员占几个二进制位;位段的具体排布依赖实现,跨平台协议解析要慎用,但对接"本机硬件寄存器"场景它天生合适。
共用体还有个省内存的冷知识:含指针的共用体在主流 64 位平台上,任何非数组成员撑不过 8 字节,多个变体共用一块,节点结构体可以瘦身——早期内存紧张的系统里是正经优化手段。
enum Color { RED, GREEN, BLUE }; /* 0 1 2 */ enum Week { MON = 1, TUE, WED }; /* 1 2 3:可指定起点 */ enum Level { LOW = 10, MID = 20, HIGH = 30 }; /* 可任意指定 */ enum Color c = GREEN; if (c == 1) { /* 能跑但不该这么写 */ }
枚举的本质是 int:底层类型就是整数,不带类型检查——把 Color 赋给 Week 编译器不拦。它的价值全在可读性:switch 分支、状态机状态、错误码表,用名字代替魔数,编辑器还能补全。工程建议:switch 处理枚举时开"未覆盖分支"警告,改枚举忘改 switch 是高频事故;另外 C 不允许在枚举名上加方法(那是 C++ 与其他语言的事),需要行为函数就配一个函数表(第 5 章函数指针)。
typedef unsigned char byte; typedef struct Node Node; /* 前置声明配合 typedef,链表标准开局 */ typedef int (*Compare)(const void *, const void *); /* 函数指针别名 */ struct Node { int value; Node *next; /* 有了 typedef 不用写 struct Node */ }; /* 用别名声明 qsort 风格的比较器,可读性天壤之别 */ int by_value(const void *a, const void *b) { /* ... */ return 0; } Compare cmp = by_value;
typedef 的价值分三层:结构体免 struct 前缀(Go 风格的简洁);函数指针从天书变人话(Compare cmp 对比 int (*cmp)(const void*, const void*));表达意图(typedef int errno_t; 说明这是错误码而不是普通计数)。它和宏定义类型(#define byte unsigned char)的区别:typedef 是编译器认识的类型别名,参与语法分析,声明多个变量不会出错——预处理那条路只是文本替换,尽量别用于类型。
typedef 与结构体的配合是大型 C 项目的基础设施:标准库的 size_t、FILE、time_t 全是 typedef——FILE 尤其值得回味,它是"内容不透明"的类型,你只能拿 FILE * 指针使用它,永远不该解引用看内部——这就是不透明指针(opaque pointer)模式,C 版的封装与信息隐藏,Linux 内核与多数 C 库的标准做法。

⚠️ 常见坑:共用体读错活跃成员不报错。写完 float 读 unsigned 是"合法的重新解读",写出错的字段视图解析寄存器,查起来没有崩溃只有错误的数字。共用体旁边必须配文档或标签变量说明当前活跃成员。
💡 关键直觉:struct 是"分房间",union 是"合租同一间"。数据天然只有一种解读就用 struct;同一批字节需要多视角(原始值与字段、整数与字节),union 是唯一优雅解。
第 7 章结束。最后一章靠近硬件:位运算、文件 IO、预处理与调试规范。