在 C++ 中,联合体(union)也常称为 “共用体”,它是一种语法形式与结构体相似、但底层内存分配方式完全不同的自定义类型。
如果把结构体比作 “多居室”,每个成员都有自己独立的房间,可以同时保存各自的数据。那么联合体就好比 “单间”,所有成员共用同一块内存空间,同一时刻只能保存其中一个成员的值,如下图所示。

union 的定义与使用
在 C++ 中,我们可以使用 union 关键字来定义联合体类型,并在花括号里声明多个不同数据类型的成员。定义好联合体变量后,同样通过点运算符(.)来读写成员。
语法:
union 联合体类型名 {
类型1 成员名1;
类型2 成员名2;
...
类型n 成员名n;
};说明:
花括号 {} 内部用来声明各个成员,右花括号 } 后面的英文分号(;)同样不能漏掉。
需要特别注意的是,联合体里的各个成员并不是各自占一块独立内存,而是共用同一块内存区域。同一时刻只有最后一次赋值的那个成员才是真正有效的,我们通常称之为 “活跃成员”。一旦给另一个成员赋了新值,原来的成员数据就会失效。
示例 1:定义并使用 union
#include <iostream>
union Number {
int integer; // 整数成员
double decimal; // 小数成员
};
int main() {
Number value = {42};
std::cout << "整数值:" << value.integer << '\n';
value.decimal = 3.14; // 改写同一块内存
std::cout << "小数值:" << value.decimal << '\n';
return 0;
}运行结果如下。
整数值:42
小数值:3.14分析:
在这个例子中,Number value = {42}; 定义了一个联合体变量,并把初值 42 赋给第 1 个成员 integer,此时读取 value.integer 可以正常输出 42。接着执行 value.decimal = 3.14; 时,同一块内存改存了小数 3.14,原来的整数 42 随之失效。
为什么上面代码能把 42 和 3.14 都正常打印出来呢?其实很简单,因为我们在给 value.decimal 赋新值之前,就已经先把 value.integer 打印出来了。一旦执行了 value.decimal = 3.14;,我们就不能再去读取已经失效的 value.integer 了。
Number value = {42};
value.decimal = 3.14;
// 正确方式
std::cout << value.decimal << '\n';
// 错误方式
std::cout << value.integer << '\n';给 decimal 赋新值后,这块内存里的二进制位就已经按照 double 小数的规则重新编码了。如果这时再去读取失效的 integer,程序会把小数的一部分二进制位当成整数解析,读出来的是一串毫无意义的乱码数字。
这种读取非活跃成员的做法在 C++ 中属于未定义行为,程序运行结果不再可控,小伙伴们平时写代码时一定要避免。
注意 直接按顺序使用花括号初始化 union 变量时(如 Number value = {42};),花括号里只能提供 1 个初始值,默认用来初始化联合体的第 1 个成员。
union 与 struct 的区别
为了让小伙伴们更直观地理解 “共用同一块内存空间” 是什么意思,下面我们借助 sizeof 运算符和取地址运算符(&),来看看 union 与 struct 在内存中的实际表现。
1. 内存大小对比
既然 struct 的每个成员各自占一块空间,而 union 的所有成员共用同一块空间,它们占用的内存字节数自然就不一样。
示例 2:对比 struct 与 union 的大小
#include <iostream>
struct Record {
int count; // 占 4 字节
double average; // 占 8 字节
};
union SharedValue {
int count; // 占 4 字节
double average; // 占 8 字节
};
int main() {
std::cout << "sizeof(Record) = " << sizeof(Record) << '\n';
std::cout << "sizeof(SharedValue) = " << sizeof(SharedValue) << '\n';
return 0;
}运行结果如下。
sizeof(Record) = 16
sizeof(SharedValue) = 8分析:
在上面代码中,Record 和 SharedValue 内部声明的成员完全相同,都是一个占 4 字节的 int 和一个占 8 字节的 double。但两者测出来的字节数却相差了一半,原因主要有以下 2 点:
- union 的大小取决于最大成员:
SharedValue的所有成员轮流共用同一块空间。只要这块空间能装下占 8 字节的最大成员double,就一定装得下占 4 字节的int,所以整个联合体只占 8 个字节。 - struct 的大小需要容纳所有成员:
Record必须同时存下 4 字节的count和 8 字节的average。为了提升硬件读写速度,编译器还会按最大成员 8 字节的整数倍做 “内存对齐”,在 4 字节后面补齐 4 个空白字节,因此总共占用了 16 个字节。
关于内存对齐的具体计算规则,这里简单了解一下即可,不需要过多深究。
2. 成员地址对比
前面学习指针时我们知道,使用取地址运算符 & 可以获取变量在内存中的起始地址。接下来我们直接把 struct 和 union 各个成员的内存地址打印出来对比一下。
示例 3:对比 struct 与 union 的成员地址
#include <iostream>
struct Record {
int count;
double average;
};
union SharedValue {
int count;
double average;
};
int main() {
Record rec = {10, 95.5};
SharedValue sv = {10};
std::cout << "rec.count 地址:" << &rec.count << '\n';
std::cout << "rec.average 地址:" << &rec.average << '\n';
std::cout << "sv.count 地址:" << &sv.count << '\n';
std::cout << "sv.average 地址:" << &sv.average << '\n';
return 0;
}运行结果如下。
rec.count 地址:0x7ffee3b4b8a0
rec.average 地址:0x7ffee3b4b8a8
sv.count 地址:0x7ffee3b4b898
sv.average 地址:0x7ffee3b4b898分析:
每次运行程序时,系统分配的具体十六进制地址可能会不一样,我们重点观察两个成员地址之间的关系。以本次运行结果为例,&rec.count 与 &rec.average 的地址相差了 8 个字节,说明 struct 里的两个成员存放在不同的内存位置。
而 &sv.count 和 &sv.average 打印出来的内存地址完全相同。无论运行多少次,这两个地址始终一模一样,直观证实了 union 的所有成员确实是从同一个内存起点开始共用空间的。
union 的使用注意事项
在早期内存非常紧张的年代,使用 union 可以省下几个字节的空间,如今在嵌入式硬件开发和底层协议解析中有时仍会用到它。不过在日常软件开发中,组织学生信息、商品订单这类业务数据时,我们直接使用 struct 或后面要学的类(class)会更安全清晰。
如果在实际项目中确实需要用到 union,小伙伴们要注意以下 2 点限制:
- 不要直接存放复杂类型:普通的
union通常只适合存放int、double、char这类基本数据类型,不要直接放入std::string、std::vector等复杂类型。因为这些类型带有自动申请和释放内存的逻辑,共用同一块空间时编译器无法判断当前应该销毁哪一个成员,编译时就会报错。 - 联合体不会记录当前活跃成员:
union本身并不知道当前有效的是哪一个成员,全靠写代码的人自己记住。如果记错了活跃成员去读取数据,编译器不会报错,但程序会直接读出乱码。
提示 如果在现代 C++ 开发中确实需要表示 “一个变量有时存整数、有时存字符串” 这种多选一的数据,C++17 标准在 <variant> 头文件中提供了类型安全的 std::variant 来替代传统 union,后面在现代特性章节中会详细介绍。
