联合体
困难10联合体:一间房多种用途,一次只能用一种
什么是联合体?
联合体(union)和结构体(struct)长得很像,但本质完全不同:结构体的每个成员都有自己独立的内存空间,而联合体的所有成员都共享同一块内存。就像你的房间,你可以把它布置成卧室(放床、衣柜),也可以布置成书房(放书桌、书架),但同一时间只能有一种用途,不可能既是卧室又是书房。
联合体的大小等于它最大成员的大小,因为所有成员共用那一块空间。比如一个联合体包含 int(4字节)、float(4字节)、char(1字节),那么它的大小就是 4 字节 —— 存放 int 或 float 时正好,放 char 时也只占那 4 字节中的第一个字节。
如何定义和使用联合体?
用关键字 union,定义方式跟结构体一模一样。
#include <iostream>
using namespace std;
union Data {
int i; // 整型,占4字节
float f; // 浮点型,占4字节
char c; // 字符型,占1字节
// 联合体大小 = max(4,4,1) = 4 字节
};
int main() {
Data d; // 定义一个联合体变量
d.i = 42; // 当前有效的是 int,内存中存着 42
cout << "i = " << d.i << endl; // 输出 42
d.f = 3.14f; // 现在写入了 float 值,原来的 int 数据被覆盖了
cout << "f = " << d.f << endl; // 输出 3.14
// 下面这行非常危险:读取已经被覆盖的成员
cout << "i 现在是:" << d.i << endl; // 输出乱码(把浮点数的二进制当整数读)
return 0;
}
关键点:修改联合体的任何一个成员,都会改变共享内存中的内容,其他成员的值就“失效”了。就像你刚把卧室改成书房,就别再去找床了。
什么时候用联合体?
-
节省内存:当你在不同时间需要使用不同类型的值,但不会同时使用时。比如一个游戏中的人物状态——有时是“健康值(整数)”,有时是“名字(字符串)”,但同一时刻只需要一种。联合体比结构体省内存。
-
类型转换的快捷方式:联合体可以把同一段内存按照不同的类型来解读。例如,想查看一个
float的二进制表示(即四个字节的具体值),可以用联合体同时包含float和unsigned char[4],写进float后读取数组就能得到每个字节。union FloatBytes { float f; // 浮点数 unsigned char bytes[4]; // 4个字节 }; int main() { FloatBytes fb; fb.f = 3.14f; for(int i = 0; i < 4; i++) { cout << (int)fb.bytes[i] << " "; // 输出浮点数的每个字节(十进制) } return 0; }这个技巧在解析网络数据包或文件格式时很有用。
-
在C语言中用于实现“可选的成员”:配合结构体使用,比如一个结构体包含一个枚举“类型”和一个联合体,根据类型选择联合体中有效的成员。这在C++中用
variant更安全,但竞赛中联合体仍常见。
新手容易犯的错误
-
读取了已经被覆盖的成员:如上例,先给
d.i赋值,再给d.f赋值,然后去读d.i,得到的是垃圾值。记住:联合体同时只有一种成员是有效的,你需要用某种方式记录当前存的是什么类型(比如加一个额外的枚举变量)。 -
对联合体整体赋值时误解:
Data d = {42};这样初始化是合法的,但只对第一个成员(i)赋值。如果想初始化其他成员,需要用指定成员初始化(C++11起):Data d = {.f=3.14};但竞赛中不太常用,最好只靠写成员来赋值。 -
忘记联合体的大小是最大成员:当你用
sizeof计算时,得到的总是最大成员的大小,而不是当前有效成员的大小。如果你需要知道当前有效成员的字节数,必须自己记录。 -
结构体中的联合体成员对齐问题:如果联合体在结构体里,结构体的大小可能受对齐影响,此时联合体的大小可能会被填充到对齐的整数倍。这些细节在 C++ 中需留意。
完整示例:用联合体管理多种数据类型
假设你要记录一位同学的成绩,可能是百分制整数(如 95),也可能是等级('A'、'B'、'C'),还可能是带小数点的绩点(如 3.8)。这三种类型不会同时使用,用联合体能节省内存。
#include <iostream>
#include <cstring> // 为了更完整的例子,此处实际上不需要
using namespace std;
struct Score {
int type; // 0=整数,1=等级,2=绩点
union {
int num; // 百分制成绩,如 95
char grade; // 等级,如 'A'
double gpa; // 绩点,如 3.8
} value; // 联合体成员
};
int main() {
Score s1, s2, s3;
// 第一个学生:百分制 95分
s1.type = 0;
s1.value.num = 95;
cout << "学生1: " << s1.value.num << "分" << endl;
// 第二个学生:等级 A
s2.type = 1;
s2.value.grade = 'A';
cout << "学生2: " << s2.value.grade << "等级" << endl;
// 第三个学生:绩点 3.8
s3.type = 2;
s3.value.gpa = 3.8;
cout << "学生3: " << s3.value.gpa << "绩点" << endl;
// 错误演示:如果忘记记录 type,直接读取错误成员
// s2.value.gpa = 2.5; // 此时 type 仍然是 1,但值变成了绩点
// cout << s2.value.grade; // 错误!等级被覆盖了
return 0;
}
运行结果:
学生1: 95分
学生2: A等级
学生3: 3.8绩点
这个例子清晰地展示了联合体的典型用法:用一个额外的 type 字段标记当前联合体中存储的是什么类型。每次读写时先检查 type,确保你正在操作的是当前有效的成员。
相关知识点
- 结构体(struct):联合体的“兄弟”,每个成员有独立内存,适合同时存储多个值。如果还不了解结构体,应先学习它。
- 内存对齐与大小:联合体大小是最大成员的大小,但结构体对齐可能使联合体在结构体中被填充,了解
sizeof可以让你更清楚实际内存布局。 - 指针与联合体:可以用指针访问联合体的成员,效果一样。但要注意,联合体的地址与所有成员的起始地址相同。
- C++17 的
std::variant:标准库中安全的联合体替代品,能自动记录当前存储的类型,避免误读错误成员。竞赛中允许 C++17 时可以使用,但更常见的是手写联合体加枚举。 - 位域(bit-field):另一种节省内存的方式,可以把数据压缩到几个比特位上,适合存储开关、标记等。与联合体配合使用时更灵活。
例题精讲
已知 union U { int i; char c; double d; }; 在32位系统上(int占4字节,char占1字节,double占8字节,对齐要求为8),sizeof(U) 的值是?
执行以下代码后,输出结果为? #include <iostream> using namespace std; union U { int i; float f; }; int main() { U u; u.f = 3.14f; cout << u.i << endl; return 0; }
联合体的所有成员变量的起始地址相同,即都从同一个内存地址开始存储。
完成下面的程序,定义一个联合体类型 Data,包含一个整型成员 i 和一个双精度浮点型成员 d,使得 sizeof(Data) 的值为 8。
#include <iostream>
using namespace std;
union Data {
int i;
___ d;
};
int main() {
cout << sizeof(Data) << endl;
return 0;
}定义联合体变量时,可以使用花括号同时初始化多个成员,例如:union U { int a; double b; }; U u = {10, 3.14}; 这种写法是正确的。