C 语言重拾【九】联合类型 union

在 C 语言中,union 常被翻译为“联合”或者“联合体”。它和 struct 一样都可以声明多个成员,但两者的内存模型并不相同。union 的关键不在于“同时拥有多个成员”,而在于多个成员共用同一块内存

因此,union 更适合用在内存复用底层数据解释这类场景中。

什么是 union

先看一个最简单的声明:

1
2
3
4
union Example {
int i;
float f;
};

这个 union 看起来有两个成员:if。但与 struct 不同的是,它们并不会分别占据独立空间,而是共享同一块内存

如果是结构体:

1
2
3
4
struct Example {
int i;
float f;
};

那么 if 都有各自独立的空间;而在 union 中,编译器只会分配一块足够容纳“最大成员”的内存。

可以总结为:

  • struct:每个成员都有自己的存储空间
  • union:所有成员共享一份存储空间

union 和 struct 的核心区别

两者的区别可以概括为:

  • struct:把多个字段组合在一起
  • union:给同一块内存提供多种解释方式

假设 intfloat 都占 4 个字节,那么:

1
2
3
4
struct Example {
int i;
float f;
};

一般需要 8 个字节左右的空间(实际大小还可能受对齐影响)。

而:

1
2
3
4
union Example {
int i;
float f;
};

一般只需要 4 个字节,因为 if 复用了同一块内存。

给一个成员赋值,另一个成员会发生什么

这里需要明确一个问题:既然 if 使用的是同一块内存,那么给 i 赋值之后,f 会发生什么?

更准确地说:

  • 你给 i 赋值时,是把这块内存按 int 的格式写了一遍
  • 之后去读 f,是把同一串比特位float 的格式解释

举个例子:

1
2
union Example e;
e.i = 1;

此时内存里保存的是“整数 1”的二进制表示。如果再去读:

1
printf("%f\n", e.f);

你读到的不是 1.0,而是“把整数 1 的位模式当作浮点数解释后的结果”。

所以,union 里并不是“两个成员同时各自拥有一个值”,而是:

只有一份原始内存内容,你选择用哪种类型的视角去看它。

一个更直观的例子

下面看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

union Example {
int i;
float f;
};

int main(void) {
union Example e;

e.i = 1;
e.f = 3.0f;

printf("i = %d\n", e.i);
printf("f = %f\n", e.f);

return 0;
}

执行顺序是:

  1. 先把内存写成整数 1
  2. 再把同一块内存改写成浮点数 3.0f

因此,最后留下来的实际上是 3.0f 的位模式。此时:

  • e.f 一般会打印 3.000000
  • e.i 打印的是“把 3.0f 的位模式当成整数解释后的值”

也就是说,最后写入哪个成员,这块内存通常就更接近按哪种类型来解释

union 最常见的误区

union 在使用时有两个常见误区。

误区一:它适合拿来同时保存多个字段

这种理解并不准确。因为它只有一份内存,任一时刻通常只应把它当成一种成员类型来使用。

误区二:读另一个成员,就是“自动转换类型”

这也不对。union 不会帮你做数值转换,它只是让你用另一种方式解释相同的位模式。比如:

  • int 1float 1.0
  • 它们的内存表示并不相同

因此,把一个成员写进去,再从另一个成员读出来,通常不会得到直观意义上的数值结果。

union 在什么场景下适用

既然 union 这么容易误用,它真正适合什么场景?

1. 互斥数据的内存复用

如果一组字段在任意时刻只会有一个有效值,那么 union 很适合节省内存。

例如:

1
2
3
4
5
union Value {
int i;
float f;
char str[32];
};

如果这个值对象在某一时刻只可能表示整数、浮点数或字符串中的一种,那么没必要为三种情况都分配独立空间。

2. 二进制数据、协议、寄存器解析

这是 union 非常经典的用途。

比如我们有一段 4 字节数据,有时想按整数看,有时想按字节数组看:

1
2
3
4
union Data {
unsigned int value;
unsigned char bytes[4];
};

这类写法在以下场景很常见:

  • 网络协议
  • 文件格式解析
  • 嵌入式开发
  • 设备寄存器访问

这些场景的共同特点是:程序员非常在意底层字节布局。

3. 配合标签字段表示“当前有效值”

这是在 C 里使用 union 的最稳妥姿势之一。

因为 union 自己并不知道当前哪个成员有效,所以实际开发里常常会额外配一个标记字段:

1
2
3
4
5
6
7
8
9
10
11
12
enum ValueType {
TYPE_INT,
TYPE_FLOAT
};

struct Variant {
enum ValueType type;
union {
int i;
float f;
} data;
};

使用时先判断 type,再决定读 data.i 还是 data.f

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>

enum ValueType {
TYPE_INT,
TYPE_FLOAT
};

struct Variant {
enum ValueType type;
union {
int i;
float f;
} data;
};

void printVariant(struct Variant v) {
switch (v.type) {
case TYPE_INT:
printf("int: %d\n", v.data.i);
break;
case TYPE_FLOAT:
printf("float: %f\n", v.data.f);
break;
default:
printf("unknown type\n");
break;
}
}

这里的 type 就是在告诉我们:当前这块 union 内存应该按什么方式解释。

使用 union 时的注意点

union 虽然强大,但也意味着程序员需要自己承担更多责任。

1. 要明确“当前有效成员”

如果没有配套的标记字段,就很容易写错。例如:

1
2
u.i = 42;
printf("%f\n", u.f);

这类代码往往较难维护,也不利于从阅读层面判断作者的真实意图。

2. 它更适合底层场景,而不是普通业务逻辑

普通逻辑代码里,struct 往往比 union 更直观、更安全。只有当你真的需要:

  • 节省内存
  • 复用同一块存储
  • 解析底层位模式

再考虑使用 union

3. 不要把它当作“自动类型转换工具”

union 的本质不是转换,而是“重新解释内存”。如果只是想做普通数值转换,应该使用显式转换,而不是依赖 union

总结

理解 union,最重要的是记住下面这句话:

union 不是同时保存多个值,而是给同一块内存提供多种解释方式。

因此,union 最适合的场景主要有三类:

  1. 互斥数据的内存复用
  2. 底层二进制、协议和寄存器解析
  3. 配合标签字段实现变体数据结构

如果只是编写普通业务代码,union 往往不是首选;但在处理底层内存布局、位模式或者资源受限场景时,它仍然是 C 语言中非常有价值的一种工具。