前面我们用结构体把不同数据打包在一起,一个 struct Student 里同时有姓名、学号、成绩,各占各的空间,互不干扰。
但有时候,我们需要的恰恰相反:同一个存储空间,在不同时刻存放不同类型的数据。 比如一个变量有时存整数,有时存浮点数,但从来不同时存在;或者我们要用一种紧凑的方式去“拆解”一个整数的各个字节。这时候,结构体的“孪生兄弟”——共用体(union) 就登场了。
同时,我们还会认识一个让代码更优雅的工具:枚举(enum)。它能把一堆有关系的整数常量组织起来,让代码的可读性上一个台阶。
一、共用体是什么?
共用体(union) 是一种特殊的复合类型,它所有的成员共享同一块内存空间。同一时刻,只有一个成员是“活跃”的,修改一个成员会覆盖其他成员的值。
定义共用体的语法和结构体几乎一样,只是把 struct 换成 union:
union Data {
int i;
double d;
char c;
};
union Data 的大小不是三个成员大小之和,而是最大成员的大小(加上可能的对齐填充)。因为所有成员都用同一个地址,存不同的类型时就是“变脸”。
#include <stdio.h>
union Data {
int i;
double d;
char c;
};
int main(void) {
union Data u;
printf("sizeof(union Data) = %zu\n", sizeof(u)); // 通常是 8(double 大小)
u.i = 42;
printf("u.i = %d\n", u.i); // 42
printf("u.d = %f\n", u.d); // 未定义行为,输出垃圾值
printf("u.c = %c\n", u.c); // 未定义行为
u.d = 3.14;
printf("u.i = %d\n", u.i); // 未定义行为,被覆盖了
printf("u.d = %f\n", u.d); // 3.14
return 0;
}
要点:
- 同一时刻,共用体只能保存一个成员的值。
- 读取一个非最后一次写入的成员,结果是未定义行为(虽然多数实现只是把位模式重新解释)。
- 共用体的大小等于其最宽成员的大小。
共用体与结构体的核心区别:
| 结构体(struct) | 共用体(union) | |
|---|---|---|
| 成员存储 | 各自独立,同时存在 | 共享同一空间 |
| 大小 | ≥ 所有成员大小之和(对齐后) | ≥ 最大成员大小 |
| 同时有效成员数 | 全部 | 一个 |
| 典型用途 | 打包不同类型数据 | 类型多态、节省内存、拆解数据 |
二、共用体的典型应用场景
1. 节省内存:多种类型不同时使用
比如你正在开发一个绘图程序,图形属性里有一个“填充色”,但填充色可能是 RGB 值(三个整数),也可能是灰度值(一个整数)。它们不会同时使用。
struct Shape {
int type; // 0=灰度, 1=RGB
union {
int gray; // 灰度值 0~255
struct {
int r, g, b;
} rgb; // RGB 三色
} color;
};
int main(void) {
struct Shape s;
s.type = 1;
s.color.rgb.r = 255;
s.color.rgb.g = 128;
s.color.rgb.b = 64;
// 现在 s.color.gray 是无意义的
return 0;
}
2. 判断大小端(字节序)
大小端(Endianness)是指多字节数据在内存中的存储顺序。大端模式将高字节存低地址,小端模式反之。利用共用体可以轻易检测当前平台:
#include <stdio.h>
int is_little_endian(void) {
union {
int i;
char c[sizeof(int)];
} u;
u.i = 1;
return u.c[0] == 1; // 小端:低字节(1)在最低地址
}
int main(void) {
if (is_little_endian()) {
printf("小端模式\n");
} else {
printf("大端模式\n");
}
return 0;
}
写入 int 值为 1(四字节:01 00 00 00 或 00 00 00 01),然后读第一个字节,若是 1,说明低地址存低字节,即小端。
3. 拆解数据:按类型和按字节访问
网络编程、协议解析中,常需要一个 32 位整数,但又要能单独操作每个字节。共用体非常方便:
union IPAddress {
uint32_t addr;
uint8_t bytes[4];
};
int main(void) {
union IPAddress ip;
ip.bytes[0] = 192;
ip.bytes[1] = 168;
ip.bytes[2] = 1;
ip.bytes[3] = 100;
printf("IP: %u.%u.%u.%u\n", ip.bytes[0], ip.bytes[1], ip.bytes[2], ip.bytes[3]);
printf("作为 32 位整数: 0x%08X\n", ip.addr);
return 0;
}
注意:此类用法依赖平台的字节序,不同平台结果可能不同。
三、枚举:给整数常量起个有意义的名字
有时程序中需要一组相关的整数常量,比如星期(1~7)、颜色(红黄蓝绿)、状态码(成功、失败、超时)。直接用数字写满代码,可读性差又容易出错:
int color = 1; // 1 是红色?还是蓝色?得翻文档
枚举(enum) 就是为这个场景设计的。它定义一组命名整数常量,让代码自解释。
enum Weekday {
MONDAY = 1,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
};
规则:
- 如果不手动赋值,枚举值默认从 0 开始,依次递增。
- 可以手动指定某个值,后续值自动递增(
MONDAY = 1,则TUESDAY自动为 2)。 - 多个枚举常量可以有相同的值(但不推荐)。
声明枚举变量:
enum Weekday today = WEDNESDAY;
if (today == SATURDAY || today == SUNDAY) {
printf("周末愉快!\n");
}
C 语言中,枚举类型底层是 int,所以枚举变量可以和整数混用(虽然编译器可能不警告),但为了类型清晰,应该避免把裸整数赋给枚举变量。
四、枚举与 switch 是好搭档
枚举配合 switch 非常自然,某些编译器(如 GCC/Clang)在开启特定警告选项时,可以检测 switch 是否覆盖了所有枚举值。
#include <stdio.h>
enum Direction {
NORTH,
SOUTH,
EAST,
WEST
};
void move(enum Direction dir) {
switch (dir) {
case NORTH: printf("向北走\n"); break;
case SOUTH: printf("向南走\n"); break;
case EAST: printf("向东走\n"); break;
case WEST: printf("向西走\n"); break;
default: printf("未知方向\n"); break;
}
}
五、枚举 vs 宏:为什么优先用枚举?
用 #define 也能定义常量:
#define RED 0
#define GREEN 1
#define BLUE 2
但枚举有不可替代的优势:
#define 宏 | enum 枚举 | |
|---|---|---|
| 类型检查 | 无(纯文本替换) | 有,枚举是独立类型 |
| 调试信息 | 调试器只能看到数字 | 可以看到枚举常量名 |
| 作用域 | 宏全局有效(除非 #undef) | 受作用域控制,可放在结构体/函数内 |
| 自增赋值 | 需手动指定 | 自动递增 |
因此,能用枚举的地方,尽量不要用宏定义一堆零散的整数常量。
六、常见错误与陷阱
1. 读取共用体非活跃成员
union Data u;
u.i = 10;
printf("%f\n", u.d); // 未定义行为!值是垃圾
严格来说这是 UB,尽管常被用于类型双关。若确实需要类型双关,C99 起可以使用 union 进行类型双关(在 GCC/Clang 等编译器中是支持的扩展,但不是严格标准行为),更安全的做法是使用 memcpy。。
2. 枚举值当成字符串
enum Color { RED, GREEN, BLUE };
printf("%s\n", RED); // 错误!打印出数字或崩溃
枚举值是整数,不能直接当字符串输出。如果需要在调试中输出名称,通常手工写转换函数。
3. 枚举类型混用导致意外赋值
enum Color { RED=0, GREEN=1, BLUE=2 };
enum Direction { NORTH=0, SOUTH=1 };
enum Color c = NORTH; // 编译可能不报错,但逻辑上是错的
虽然都是 int,但不同枚举类型混用会让代码混乱。尽量保持类型一致。
4. 对共用体使用 sizeof 误当成成员之和
union U {
int a;
double b;
};
printf("%zu\n", sizeof(union U)); // 通常是 8,不是 12
共用体大小只需容下最大的那个。
七、小结
今天你认识了结构体的两个“亲戚”:
- 共用体 让多个成员共享同一块内存,用于节省空间、检查字节序、协议解析等。使用时务必清楚当前活跃的成员是谁。
- 枚举 给整数常量赋予了有意义的名字,让代码更可读、更安全。配合
switch使用优雅高效。
共用体与结构体有时会联合使用,比如前面看到的带标记的 Shape(type 字段指示当前共用体的含义),这其实是 C 语言实现“变体记录”或“tagged union”的经典手法。
现在,你已经可以自由组合结构体、共用体、枚举来构建复杂的数据模型。但数据只在程序运行时存在——一旦程序退出,变量就消失了。怎么把数据长久保存?下一篇,我们进入文件操作的世界:fopen、fclose、fprintf、fscanf,让你的程序能读写磁盘上的文件,真正“留下痕迹”。
课后小练习
- 定义一个共用体
Number,包含int、float、double三个成员。写一个函数print_number(union Number n, int type),根据 type 的值打印对应的成员。在main中测试。 - 利用共用体写一个函数,输入一个
unsigned int,交换它的高低 16 位并返回(例如0x12345678变成0x56781234)。提示:用共用体配合unsigned short数组。 - 定义一个枚举
HttpStatus,包含常见的 HTTP 状态码(200, 301, 404, 500 等),并手动指定值。写一个函数get_status_message(enum HttpStatus code),返回对应的字符串描述。 - (小挑战)设计一个“配置文件解析器”的数据模型:配置项的值可能是整数、浮点数或字符串。用共用体和枚举结合实现一个
ConfigValue类型,并编写设置和打印函数,根据类型打印不同格式的值。
我们下期见!
453

被折叠的 条评论
为什么被折叠?



