《Hello C++》4.3 共用体

共用体(union)是一种特殊的数据结构,它允许不同的数据成员共享同一块内存区域。这意味着,尽管共用体可以包含多个不同类型的成员,但在任何一个时刻,它只能存储其中一个成员的值。这种设计使得共用体在特定场景下非常有用,尤其是在需要考虑内存节省的情况下。

共用体的定义方式与结构体(struct)相似,但需要把关键字换成 union。例如,你可以定义一个包含整型、字符型和浮点型成员的共用体。

程序代码4-3-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <cstring>

using namespace std;

union BasicData
{
int integer_value;
float float_value;
char string_value[20];
};

int main()
{
BasicData data;

// 存储整数
data.integer_value = 42;
cout << "整数值: " << data.integer_value << endl;

// 存储浮点数(会覆盖之前的整数)
data.float_value = 3.14f;
cout << "浮点数值: " << data.float_value << endl;

// 存储字符串(会覆盖之前的浮点数)
strcpy(data.string_value, "Hello, Union!");
cout << "字符串值: " << data.string_value << endl;

// 演示数据覆盖效果
cout << "覆盖后尝试读取整数: " << data.integer_value << endl; // 无意义的值

return 0;
}

然而,与结构体所有成员独立占用内存空间不同,共用体的所有成员都从相同的内存地址开始存储。因此,一个共用体变量所占用的总内存大小,由其最长的成员决定。例如,如果一个共用体包含一个 int(通常占 4 字节)和一个 double(通常占 8 字节),那么该共用体的大小就是 8 字节。这种内存共享机制也带来了一个关键特性:当你给一个成员赋值后,再给另一个成员赋值,就会覆盖之前的值,因为它们共享同一块内存。所以,在某一时刻,只有最后一次被赋值的成员是有效的。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 存储整数
data.integer_value = 42;
cout << "整数值: " << data.integer_value << endl;

// 存储浮点数(会覆盖之前的整数)
data.float_value = 3.14f;
cout << "浮点数值: " << data.float_value << endl;

// 存储字符串(会覆盖之前的浮点数)
strcpy(data.string_value, "Hello, Union!");
cout << "字符串值: " << data.string_value << endl;

cout << "覆盖后尝试读取整数: " << data.integer_value << endl; // 无意义的值

由于共用体的这种特性,在使用时有几点需要特别注意。你不能直接对共用体中定义的变量名进行赋值或初始化,也不能用它作为函数参数或返回值,而只能通过其成员来访问数据。同时,共用体变量的地址和它各个成员的地址都是相同的。在 C++ 中,共用体可以像类一样拥有成员函数(如构造函数),但不能有虚函数、静态数据成员或引用成员,也不能作为基类使用(面向对象及类的知识我们会在第七章类与对象中讲到)。此外,如果共用体的成员是自定义类型(如某个类的对象),那么该类不能有构造函数、析构函数、重载的赋值运算符或虚函数。

共用体一个典型的应用场景是当某个数据项可能具有多种类型,但在一段时间内只会使用其中一种类型时,可以用来节省内存。例如,在一个商品管理结构中,有些商品的 ID 是整数,有些则是字符串,就可以使用共用体来存储 ID。共用体也常用于低级编程,如操作系统数据结构、硬件寄存器访问或需要直接处理数据字节的场景(例如类型转换或分离高低字节)。还有一种特殊的匿名共用体(anonymous union),它没有名称,其成员可以直接被外层结构体或类的成员访问,使得代码更加简洁。

程序代码4-3-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
using namespace std;

struct ConfigValue
{
enum ValueType { INT_TYPE, FLOAT_TYPE } type; // 标签,用于指示当前存储的是哪种类型的数据
union
{ // 匿名共用体
int int_val; // 当type为INT_TYPE时,使用此成员
float float_val; // 当type为FLOAT_TYPE时,使用此成员
}; // 匿名共用体结束,其成员int_val和float_val直接成为ConfigValue的成员
};

int main()
{
ConfigValue cv;

cv.type = ConfigValue::INT_TYPE;
cv.int_val = 100; // 直接访问匿名共用体的成员,无需中间变量名
cout << "Integer value: " << cv.int_val << endl;

cv.type = ConfigValue::FLOAT_TYPE;
cv.float_val = 3.14f; // 给float_val赋值会覆盖int_val的内存
cout << "Float value: " << cv.float_val << endl;

// 错误演示:由于内存共享,此时再读取cv.int_val将得到无意义的数据
// cout << cv.int_val << endl;

return 0;
}

在实际编程中,为了确保能正确解读共用体中存储的数据,通常需要另一个额外的变量,例如:enum ValueType { INT_TYPE, FLOAT_TYPE } type;(通常称为"标签"或"判别式")来记录当前共用体中存储的是哪个成员。例如,在一个结构体中,可以有一个 type 字段指明当前应该使用共用体中的哪个成员。共用体的内存布局还会受到内存对齐规则的影响。编译器可能会在成员之间或共用体末尾添加填充字节,以确保数据在内存中的地址符合特定要求,从而优化访问速度。因此,共用体的实际大小可能不等于其最大成员的大小,而是其对齐要求的整数倍。