什么是 union (联合体)?
union 是一种特殊的自定义数据类型,它允许你在同一个内存位置存储不同类型的数据。

与 struct(结构体)不同,struct 会为每个成员分配独立的内存空间,而 union 的所有成员共享同一块内存,这块内存的大小等于其成员中占用空间最大的那个成员的大小。
union 的核心特点:
- 内存共享:所有成员共用一块内存。
- 大小由最大成员决定:
sizeof(union)等于其最大成员的sizeof。 - 同时只能使用一个成员:你在任何时刻只能安全地使用其中一个成员,如果你写入了一个成员,然后以另一个成员的类型去读取,得到的是未定义的行为或毫无意义的乱码。
union 的基本语法和示例
#include <stdio.h>
#include <string.h>
// 定义一个联合体
DataUnion {
int i;
float f;
char str[20];
};
int main() {
DataUnion data;
// data.i, data.f, data.str 都指向同一块内存
printf("Size of DataUnion: %zu bytes\n", sizeof(DataUnion)); // 输出通常是 20 (由 str[20] 决定)
// 使用成员 i
data.i = 10;
printf("data.i = %d\n", data.i); // 输出 10
// 使用成员 f
// 写入 data.f 会覆盖 data.i 在内存中的内容
data.f = 220.5;
printf("data.f = %.2f\n", data.f); // 输出 220.50
// 如果现在再读取 data.i,得到的是垃圾值
printf("data.i (after f) = %d\n", data.i); // 输出一个无意义的数字
// 使用成员 str
// 写入 data.str 会覆盖 data.f 在内存中的内容
strcpy(data.str, "Hello C");
printf("data.str = %s\n", data.str); // 输出 Hello C
// 如果现在再读取 data.f,得到的是垃圾值
printf("data.f (after str) = %.2f\n", data.f); // 输出一个无意义的数字
return 0;
}
这个例子清晰地展示了 union 的“覆盖”特性,当你使用一个成员时,其他成员的数据就会被破坏。
union 指针
就像你可以有指向 struct 的指针一样,你也可以有指向 union 的指针,使用指针通常是为了:
- 通过函数传递
union(避免大结构体的值拷贝开销)。 - 动态地管理
union对象。
声明和初始化 union 指针
DataUnion data; // 定义一个联合体变量 DataUnion *ptr; // 定义一个指向 DataUnion 类型的指针 ptr = &data; // 让指针 ptr 指向 data 的内存地址
通过指针访问 union 成员
有两种方式通过指针访问 union 的成员:

-
使用
->(间接成员访问运算符):这是最常见、最推荐的方式。ptr->i = 100; // 等价于 (*ptr).i = 100; printf("Value via pointer: %d\n", ptr->i); -
*使用 `
(解引用) 和.` (成员访问运算符)**:(*ptr).f = 99.99; printf("Value via dereferenced pointer: %.2f\n", (*ptr).f);由于 的优先级高于 ,所以必须用括号将
*ptr括起来。
union 指针的实际应用场景
union 指针最常见的用途是实现多态或类型双关,即用一个数据表示多种不同的含义,一个经典的例子是网络编程中的 IP 地址。
示例:处理 IPv4 和 IPv6 地址
一个 IPv4 地址是一个 4 字节的整数,而一个 IPv6 地址是一个 16 字节的数组,我们可以用 union 来优雅地处理这两种情况。
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h> // 用于 inet_pton 和 ntohl
// 定义一个联合体来存储 IP 地址
// 它可以存储 IPv4 (4字节) 或 IPv6 (16字节)
union IPAddr {
struct {
uint32_t addr; // IPv4 地址,存储为网络字节序的无符号长整型
} v4;
struct {
uint8_t addr[16]; // IPv6 地址,存储为16字节的数组
} v6;
};
// 函数通过指针接收 IP 地址并打印
void printIP(const char *name, union IPAddr *ip) {
printf("Printing IP for %s:\n", name);
// 在实际应用中,你需要一个标志位来区分是 v4 还是 v6
// 这里我们简化处理,假设调用者知道类型
// 检查 addr[0] 是否为0来判断是否可能是IPv4(这是一个简化的启发式方法)
if (ip->v6.addr[0] == 0 && ip->v6.addr[1] == 0 && ip->v6.addr[2] == 0 && ip->v6.addr[3] == 0 && ip->v6.addr[4] == 0 && ip->v6.addr[5] == 0 && ip->v6.addr[6] == 0 && ip->v6.addr[7] == 0 && ip->v6.addr[8] == 0 && ip->v6.addr[9] == 0 && ip->v6.addr[10] == 0 && ip->v6.addr[11] == 0) {
// 简单判断,可能是IPv4映射的IPv6地址
// 更好的方法是在 union 外面加一个 type 字段
uint32_t v4_addr = ntohl(ip->v4.addr); // 从网络字节序转换为主机字节序
printf(" IPv4 Address: %u.%u.%u.%u\n",
(v4_addr >> 24) & 0xFF,
(v4_addr >> 16) & 0xFF,
(v4_addr >> 8) & 0xFF,
v4_addr & 0xFF);
} else {
printf(" IPv6 Address: ");
for (int i = 0; i < 16; i += 2) {
printf("%02x%02x", ip->v6.addr[i], ip->v6.addr[i+1]);
if (i < 14) printf(":");
}
printf("\n");
}
}
int main() {
// --- 示例 1: IPv4 地址 ---
union IPAddr ipv4_addr;
// 将 "192.168.1.1" 转换为网络字节序的32位整数
inet_pton(AF_INET, "192.168.1.1", &(ipv4_addr.v4.addr));
// 通过指针传递给函数
printIP("My Router", &ipv4_addr);
// --- 示例 2: IPv6 地址 ---
union IPAddr ipv6_addr;
// 将 "2001:0db8:85a3:0000:0000:8a2e:0370:7334" 转换为16字节数组
inet_pton(AF_INET6, "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ipv6_addr.v6.addr);
// 通过指针传递给函数
printIP("Server", &ipv6_addr);
return 0;
}
分析这个例子:
- 我们定义了一个
union IPAddr,它既能容纳 4 字节的 IPv4 地址,也能容纳 16 字节的 IPv6 地址。 - 我们使用
struct来封装union的成员,使代码更清晰(v4.addrvsip.addr)。 printIP函数接收一个union IPAddr的指针,这使得无论传入的是 IPv4 还是 IPv6 地址,函数都能用同一种方式处理。- 重要提示:上面的代码为了简化,没有包含一个类型标识符,一个更健壮的设计是在
union外面加一个enum成员来明确当前存储的是哪种类型。
最佳实践和注意事项
-
永远不要同时读写多个成员:
union的核心就是共享内存,写入一个成员后,再读取另一个成员是危险的,除非你完全清楚内存布局并且有意为之(如类型双关)。 -
使用类型标志:当一个
union可能存储多种类型时,最好在union定义中加入一个enum成员来记录当前存储的是哪种类型。enum DataType { TYPE_INT, TYPE_FLOAT, TYPE_STRING }; typedef struct { enum DataType type; union { int i; float f; char str[20]; } data; } SafeData; // 使用时 SafeData myData; myData.type = TYPE_INT; myData.data.i = 123;这样,在读取
union之前,你可以先检查type成员,确保你读取的是正确的类型,从而避免错误。 -
注意字节序和对齐:当
union用于处理二进制数据(如网络协议、文件格式)时,必须注意字节序(大端/小端)和对齐问题,跨平台使用时,要确保数据转换正确。 -
指针运算:可以对
union指针进行指针运算(如ptr++),它会移动到下一个union对象的起始地址,移动的sizeof(union)个字节。
| 特性 | struct |
union |
|---|---|---|
| 内存分配 | 为每个成员分配独立的内存空间。 | 所有成员共享同一块内存空间。 |
| 大小 | sizeof(struct) = 所有成员大小之和(考虑对齐)。 |
sizeof(union) = 最大成员的大小。 |
| 成员使用 | 可以同时使用所有成员。 | 任何时刻只能安全地使用一个成员。 |
| 指针 | 可以有指向 struct 的指针,用 -> 或 (*ptr). 访问成员。 |
可以有指向 union 的指针,用 -> 或 (*ptr). 访问成员。 |
| 主要用途 | 将不同类型的数据组合成一个逻辑实体。 | 在同一内存位置存储不同类型的数据,用于多态、类型转换或节省内存。 |
union 指针是 C 语言中一个强大但需要谨慎使用的工具,它让你能够以非常灵活的方式处理内存,但也要求你对内存管理有深刻的理解。
