C++ 联合体(union)

在 C++ 中,联合体(union)也常称为 “共用体”,它是一种语法形式与结构体相似、但底层内存分配方式完全不同的自定义类型。

如果把结构体比作 “多居室”,每个成员都有自己独立的房间,可以同时保存各自的数据。那么联合体就好比 “单间”,所有成员共用同一块内存空间,同一时刻只能保存其中一个成员的值,如下图所示。

struct 与 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,后面在现代特性章节中会详细介绍。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号