🚀个人主页:BabyZZの秘密日记 📖收入专栏:C语言
🌍文章目入
0. 开场白1. 什么是 union?1.1 最小可运行示例
2. 内存布局与对齐规则2.1 大小怎么算?2.2 对齐与填充
3. 访问与生命周期3.1 “写 A 读 B”合法吗?3.2 匿名 union
4. 典型应用场景5. 进阶话题5.1 初始化规则5.2 与位域(bit-field)混用5.3 与 `_Alignas` 控制对齐
6. 常见“踩坑”清单7. 完整 Demo:协议解析器8. 小结 & 思维导图9. 参考资料
0. 开场白
“结构体(struct)像一间大房子,每个成员都有自己的房间;联合体(union)像一间胶囊旅馆,所有成员轮流住同一张床。” 如果你刚学完结构体,现在轮到“更省内存、更危险”的联合体了。本文将带你一次性吃透 union 的语法、内存模型、典型用法、常见误区以及 C11 标准的新玩法。
1. 什么是 union?
一句话: union 是一种特殊的数据类型,允许在同一内存位置存储不同的数据类型,但同一时间只能使用其中一个。
1.1 最小可运行示例
#include
union Data {
int i;
float f;
char str[20];
};
int main(void) {
union Data d;
d.i = 10;
printf("as int : %d\n", d.i);
d.f = 220.5;
printf("as float : %.1f\n", d.f); // 覆盖 int 的值
/* str 又覆盖了前面的 4 字节 float */
sprintf(d.str, "hello");
printf("as string: %s\n", d.str);
return 0;
}
输出:
as int : 10
as float : 220.5
as string: hello
内存里只有一块区域,大小等于 最大成员的大小(这里是 20 字节)。
2. 内存布局与对齐规则
2.1 大小怎么算?
union U {
char c;
int i;
double d;
};
printf("%zu\n", sizeof(union U)); // 8(对齐到 double)而非 1+4+8
2.2 对齐与填充
对齐要求 = 成员中最大对齐值。末尾填充保证数组元素也能对齐。 用 offsetof、alignof 可观察:
#include
#include
printf("alignof(U) = %zu\n", alignof(union U)); // 8
3. 访问与生命周期
3.1 “写 A 读 B”合法吗?
C 标准规定:写入成员 A,再读取成员 B 属于未定义行为(UB),除非 B 是 char 数组或 unsigned char 用来查看原始字节。 示例:类型双关(type punning)的正确姿势
union Pun { uint32_t u; float f; };
float f2u(float x) {
union Pun p = {.f = x}; // 初始化为 float
p.u |= 0x80000000u; // 修改最高位(符号位)
return p.f; // 读取 float → OK
}
3.2 匿名 union
C11 允许在结构体里嵌套匿名 union,实现“变体字段”:
struct Node {
enum { INT, FLT, STR } type;
union {
int i;
float f;
char *s;
}; // 匿名
};
void print(const struct Node *n) {
switch (n->type) {
case INT: printf("%d\n", n->i); break;
case FLT: printf("%.2f\n", n->f); break;
case STR: printf("%s\n", n->s); break;
}
}
4. 典型应用场景
场景示例原因节省内存网络协议包头各字段不会同时出现类型双关快速 float↔int 转换避免 memcpy变体记录JSON AST 节点同一结构体表达多种值寄存器映射MCU 外设寄存器不同位域视图共用地址
5. 进阶话题
5.1 初始化规则
C89:只能初始化第一个成员C99/C11:指定初始化器
union U u = {.f = 3.14f};
5.2 与位域(bit-field)混用
union Flags {
struct {
unsigned a:1;
unsigned b:2;
unsigned c:5;
};
uint8_t raw;
};
5.3 与 _Alignas 控制对齐
union alignas(16) Vec {
float v[4];
};
6. 常见“踩坑”清单
写入后忘记当前活跃成员 → 读错数据。把 union 放进数组却按错误成员写循环。strlen(union.str) 前未确保 str 以 \0 结束。在 C++ 中混用 union 和非平凡构造类型(需 std::variant)。认为 union { int i; short s; } 中 i 高 16 位一定保留 → UB。
7. 完整 Demo:协议解析器
需求:解析一条 TLV(Tag-Length-Value)消息,其中 Value 可能是整数、浮点或字符串。
#include
#include
#include
typedef enum { TYPE_INT = 1, TYPE_FLT = 2, TYPE_STR = 3 } type_e;
typedef struct {
type_e type;
union {
int32_t i;
float f;
char str[16];
} value;
} tlv_t;
void decode(const uint8_t buf[], tlv_t *out) {
out->type = (type_e)buf[0];
switch (out->type) {
case TYPE_INT:
memcpy(&out->value.i, buf+1, 4);
break;
case TYPE_FLT:
memcpy(&out->value.f, buf+1, 4);
break;
case TYPE_STR:
memcpy(out->value.str, buf+1, sizeof(out->value.str)-1);
out->value.str[15] = '\0';
break;
}
}
int main(void) {
uint8_t raw[] = {TYPE_FLT, 0x00, 0x00, 0x58, 0x40}; // 3.25
tlv_t pkt;
decode(raw, &pkt);
printf("decoded float = %.2f\n", pkt.value.f);
return 0;
}
8. 小结 & 思维导图
记住一句话:union 省内存,但“当前是谁”靠程序员自己记账。工具箱:sizeof、alignof、offsetof、memcpy、char alias。下一步:
C++ 用 std::variant + std::visit 替代裸 union。嵌入式里结合 volatile 访问硬件寄存器。
9. 参考资料
ISO/IEC 9899:2018 §6.5.2.3, §6.7.2.1 《C 专家编程》第 4 章 《深入理解计算机系统》2.1.7
如果本文帮到你,欢迎点个 Star;踩到新坑,欢迎留言一起补全!