CC++ & Algorithm

联合体

困难10
语言版本:C++
概述:联合体让多个成员共享同一块内存,就像一间房间可以当卧室也可以当书房,但同一时间只能有一种用途。

联合体:一间房多种用途,一次只能用一种

什么是联合体?

联合体(union)和结构体(struct)长得很像,但本质完全不同:结构体的每个成员都有自己独立的内存空间,而联合体的所有成员都共享同一块内存。就像你的房间,你可以把它布置成卧室(放床、衣柜),也可以布置成书房(放书桌、书架),但同一时间只能有一种用途,不可能既是卧室又是书房。

联合体的大小等于它最大成员的大小,因为所有成员共用那一块空间。比如一个联合体包含 int(4字节)、float(4字节)、char(1字节),那么它的大小就是 4 字节 —— 存放 intfloat 时正好,放 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;
}

关键点:修改联合体的任何一个成员,都会改变共享内存中的内容,其他成员的值就“失效”了。就像你刚把卧室改成书房,就别再去找床了。

什么时候用联合体?

  1. 节省内存:当你在不同时间需要使用不同类型的值,但不会同时使用时。比如一个游戏中的人物状态——有时是“健康值(整数)”,有时是“名字(字符串)”,但同一时刻只需要一种。联合体比结构体省内存。

  2. 类型转换的快捷方式:联合体可以把同一段内存按照不同的类型来解读。例如,想查看一个 float 的二进制表示(即四个字节的具体值),可以用联合体同时包含 floatunsigned 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;
    }
    

    这个技巧在解析网络数据包或文件格式时很有用。

  3. 在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):另一种节省内存的方式,可以把数据压缩到几个比特位上,适合存储开关、标记等。与联合体配合使用时更灵活。

例题精讲

1单选题

已知 union U { int i; char c; double d; }; 在32位系统上(int占4字节,char占1字节,double占8字节,对齐要求为8),sizeof(U) 的值是?

A4
B8
C12
D16
2单选题

执行以下代码后,输出结果为? #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; }

A3
B3.14
C一个很大的整数(具体值取决于浮点数的二进制表示)
D编译错误
3判断题

联合体的所有成员变量的起始地址相同,即都从同一个内存地址开始存储。

4填空题
完成下面的程序,定义一个联合体类型 Data,包含一个整型成员 i 和一个双精度浮点型成员 d,使得 sizeof(Data) 的值为 8。

#include <iostream>
using namespace std;
union Data {
    int i;
    ___ d;
};
int main() {
    cout << sizeof(Data) << endl;
    return 0;
}
5判断题

定义联合体变量时,可以使用花括号同时初始化多个成员,例如:union U { int a; double b; }; U u = {10, 3.14}; 这种写法是正确的。