一、共用体(union)——让不同变量共享同一块内存
共用体,说白了就是让几个不同类型的变量共用同一块内存空间。我刚开始学C语言时,总觉得这东西有点鸡肋——既然有结构体了,为什么还要搞个共用体?直到后来做嵌入式开发,才真正体会到它的妙处。
1.1 共用体的定义
共用体的定义方式和结构体非常像,只是关键字从 struct 换成了 union:
union Data {
int i;
float f;
char str[20];
};
这里定义了一个叫 Data 的共用体类型,它包含三个成员:一个整型、一个浮点型、一个字符数组。但有意思的是,这三个成员在内存中是从同一个地址开始存放的。
关键区别:结构体的每个成员都有独立的存储空间,而共用体的所有成员共享同一块存储空间。
1.2 共用体的特点
共用体有几个特点,我当年踩过不少坑,这里给你总结一下:
- 所有成员共享同一块内存——共用体的大小等于它最大成员的大小。上面那个
union Data,最大成员是char str[20](20字节),所以整个共用体就是20字节。 - 同一时刻只能存放一个成员的值——你给
i赋了值,再去读f,得到的就是一堆乱码。嗯,这个我吃过亏。 - 修改一个成员会影响其他成员——因为它们共用内存,改了一个,其他的自然也跟着变。
我曾经在项目里用共用体做数据解析,结果忘了初始化就直接读取浮点成员,调试了一下午才发现是垃圾值在作怪。所以,使用共用体前一定要先赋值!
1.3 共用体的实际应用
共用体在嵌入式开发中特别常用。我举个例子,比如你要解析一个串口数据包,数据可能是整数也可能是浮点数:
union PacketData {
uint8_t bytes[4];
uint32_t integer;
float decimal;
};
// 接收数据
union PacketData rxData;
rxData.bytes[0] = 0x41;
rxData.bytes[1] = 0xA0;
rxData.bytes[2] = 0x00;
rxData.bytes[3] = 0x00;
// 直接按浮点数读取
printf("浮点数值: %f\n", rxData.decimal); // 输出 20.0
你看,通过共用体,我们可以用字节数组接收原始数据,然后直接按浮点数解读,省去了手动转换的麻烦。
二、枚举(enum)——给整数起个名字
枚举,说白了就是给一组整数常量起个有意义的名字。我个人习惯用枚举来替代宏定义,因为枚举有类型检查,更安全。
2.1 枚举的定义
enum Weekday {
MON, // 默认 0
TUE, // 默认 1
WED, // 默认 2
THU, // 默认 3
FRI, // 默认 4
SAT, // 默认 5
SUN // 默认 6
};
默认情况下,枚举常量从0开始依次递增。你也可以手动指定值:
enum Status {
OK = 0,
ERROR = -1,
TIMEOUT = -2,
BUSY = 1
};
2.2 枚举的使用
枚举用起来很直观,你想想看:
enum Weekday today = WED;
if (today == SAT || today == SUN) {
printf("周末啦!\n");
} else {
printf("继续搬砖...\n");
}
比起直接用数字0~6,用 MON、TUE 这样的名字,代码可读性一下子就上去了。
我建议在 switch-case 语句中尽量用枚举,这样编译器能帮你检查是否遗漏了某个分支。比如你定义了7天的枚举,如果 switch 里只写了5个 case,编译器会给出警告。
2.3 枚举的注意事项
- 枚举本质上是整数——你可以把一个整数值赋给枚举变量,但最好别这么干,容易出问题。
- 枚举常量是全局可见的——不同枚举类型中不能有同名的常量。比如你不能在
enum A和enum B里都定义OK。 - C语言中枚举的大小——通常是
int大小,但具体取决于编译器。
三、typedef——给类型起个外号
typedef 的作用就是给已有的类型起个新名字。我刚开始觉得这功能没啥用,后来写复杂项目才发现,它能让代码简洁不少。
3.1 typedef 的基本用法
typedef unsigned long long uint64_t;
typedef struct {
int x;
int y;
} Point;
uint64_t bigNumber = 1234567890ULL;
Point p1 = {10, 20};
你看,用 typedef 定义后,每次声明变量就不用写 struct 关键字了,代码清爽很多。
3.2 typedef 与函数指针
函数指针的声明本来就够绕的,用 typedef 能简化不少:
// 定义一个函数指针类型
typedef int (*Operation)(int, int);
// 使用
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
Operation op = add;
printf("结果: %d\n", op(5, 3)); // 输出 8
op = sub;
printf("结果: %d\n", op(5, 3)); // 输出 2
我个人习惯在嵌入式驱动开发中,用 typedef 定义寄存器地址类型、回调函数类型等,这样代码移植起来也方便。
3.3 typedef 与 #define 的区别
| 对比项 | typedef | #define |
|---|---|---|
| 处理时机 | 编译阶段 | 预处理阶段(文本替换) |
| 类型检查 | 有编译器类型检查 | 无类型检查 |
| 作用域 | 遵循作用域规则 | 全局有效(直到 #undef) |
| 指针声明 | typedef int* PINT; PINT a, b; // a和b都是指针 | #define PINT int* PINT a, b; // a是指针,b是int |
嗯,这里要注意,#define 只是简单的文本替换,容易出一些隐蔽的bug。我建议能用 typedef 的地方就别用 #define。
四、知识体系总览
下面这张图帮你梳理了本章的核心内容:
五、综合示例
最后,我把这三个知识点串起来,写一个实际点的例子:
#include <stdio.h>
// 定义数据类型枚举
typedef enum {
TYPE_INT,
TYPE_FLOAT,
TYPE_STRING
} DataType;
// 定义共用体存放数据
typedef union {
int i;
float f;
char str[32];
} DataValue;
// 定义结构体包装数据和类型
typedef struct {
DataType type;
DataValue value;
} DataPacket;
void printData(DataPacket *pkt) {
switch (pkt->type) {
case TYPE_INT:
printf("整数: %d\n", pkt->value.i);
break;
case TYPE_FLOAT:
printf("浮点数: %f\n", pkt->value.f);
break;
case TYPE_STRING:
printf("字符串: %s\n", pkt->value.str);
break;
default:
printf("未知类型\n");
}
}
int main() {
DataPacket pkt1 = {TYPE_INT, .value.i = 42};
DataPacket pkt2 = {TYPE_FLOAT, .value.f = 3.14f};
DataPacket pkt3 = {TYPE_STRING, .value.str = "Hello"};
printData(&pkt1);
printData(&pkt2);
printData(&pkt3);
return 0;
}
这个例子把枚举、共用体和typedef结合在了一起,实现了一个可以存放不同类型数据的通用数据包。我在做物联网项目时,经常用这种模式来处理不同传感器的数据。
避坑指南:使用共用体时,一定要通过类型标记(比如上面的 DataType)来记录当前存放的是哪种类型的数据。我曾经在项目中忘了做这个标记,结果读取数据时类型对不上,排查了好久才发现问题。
公众号:蓝海资料掘金营,微信deep3321