当前位置: 首页 > news >正文

【C++特殊工具与技术】联合:节省空间的类

在 C++ 的类体系中,除了我们熟悉的structclass,还有一种特殊的类类型 ——联合(Union)。它就像内存中的 “共享公寓”,所有成员共用同一块存储空间,这种特性让它在需要节省内存的场景中大放异彩。

目录

一、联合的定义:共享内存的类

1.1 基础语法与内存模型

1.2 与结构体的本质区别

二、联合的 “不能与必须”:成员的限制

2.1 不能有静态数据成员

2.2 不能有引用成员

2.3 成员的构造与析构限制(C++11 前)

三、联合的使用:同一时间只有一个 “有效成员”

3.1 成员的访问与赋值

3.2 典型应用场景:状态与数据的绑定

四、嵌套联合:联合中的联合

4.1 基础嵌套示例

4.2 嵌套联合的访问

五、匿名联合:没有名字的 “透明共享”

5.1 语法与特性

5.2 与命名联合的对比

5.3 应用场景:简化代码结构

六、联合的进阶:位域与类型双关

6.1 位域(Bit Field)与联合的结合

6.2 类型双关:安全与风险并存

七、联合的局限与现代替代方案

7.1 主要局限

7.2 现代替代方案:std::variant

八、总结 


一、联合的定义:共享内存的类

1.1 基础语法与内存模型

联合的定义语法与结构体(struct)非常相似,区别在于关键字union的使用。其核心规则是:所有数据成员共享同一块内存空间,因此联合的大小等于其最大成员的大小。

// 定义一个联合:存储整数、浮点数或字符
union ValueStorage {int int_val;    // 4字节(假设32位系统)float float_val;// 4字节char char_val;  // 1字节
};

可以通过sizeof验证其内存大小: 

#include <iostream>
using namespace std;int main() {ValueStorage vs;cout << "联合大小:" << sizeof(vs) << "字节" << endl; // 输出4(最大成员是int/float)return 0;
}

内存布局示意图

+------------------+
|     Token        |
| +--------------+ |
| | 内存空间      | | 大小为 max(sizeof(char), sizeof(int), sizeof(double))
| | (共享区域)    | |
| +--------------+ |
+------------------+

关键特性:所有成员从相同地址开始存储,修改任一成员都会影响整个内存空间 

对齐规则:联合的大小必须是其最大成员对齐值的整数倍

union Alignment {char c;      // 大小1,对齐要求1int i;       // 大小4,对齐要求4double d;    // 大小8,对齐要求8
};
// sizeof(Alignment) = 8

内存布局示例:  

0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| d (占据全部8字节)              |
+---+---+---+---+---+---+---+---+
| i (占据0-3字节)   | 空闲空间   |
+---+---+---+---+---+---+---+---+
| c (占据0字节)     | 空闲空间   |
+---+---+---+---+---+---+---+---+

1.2 与结构体的本质区别

结构体(struct)的成员是顺序存储的,总大小是各成员大小之和(考虑内存对齐);而联合的成员是重叠存储的,总大小等于最大成员的大小。通过一个对比示例理解:

// 结构体:成员顺序存储
struct StructExample {int a;     // 4字节float b;   // 4字节(总大小8字节,无填充)char c;    // 1字节(总大小9字节,实际因对齐可能填充到12字节)
};// 联合:成员共享存储
union UnionExample {int a;     // 4字节float b;   // 4字节(总大小4字节)char c;    // 1字节(总大小4字节)
};

二、联合的 “不能与必须”:成员的限制

联合的共享内存特性,决定了它在成员类型上有严格限制。理解这些限制是正确使用联合的关键。

2.1 不能有静态数据成员

静态数据成员(static成员)属于类本身,不占用实例的内存空间。但联合的设计目标是 “实例级别的内存共享”,静态成员与这一目标矛盾。因此,联合中不能声明静态数据成员。 

union InvalidUnion {static int static_val; // 编译错误:联合不能有静态成员int normal_val;
};

2.2 不能有引用成员

引用(&)是对象的别名,必须在初始化时绑定到具体对象,且无法重新绑定。但联合的成员会共享内存,当切换成员时(如从int切换为float),引用可能指向无效的内存地址。因此,联合中不能声明引用成员。 

union InvalidUnion {int& ref_val; // 编译错误:联合不能有引用成员int normal_val;
};

2.3 成员的构造与析构限制(C++11 前)

在 C++11 标准之前,联合的成员不能是非平凡构造 / 析构类型(即需要编译器生成默认构造 / 析构函数的类型)。例如std::string(需要管理动态内存)、自定义类(含虚函数)等。这是因为联合无法保证成员的构造 / 析构顺序 —— 当一个成员被销毁时,其他成员可能正在使用同一块内存。 

// C++11前无效,C++11后需显式管理构造/析构
union StringUnion {std::string str; // C++11前编译错误int num;
};

 C++11 的改进:允许联合包含非平凡类型成员,但必须手动调用构造 / 析构函数。例如:

#include <string>
union ModernUnion {std::string str;int num;ModernUnion() { /* 需显式初始化其中一个成员 */ }~ModernUnion() { /* 需显式析构当前活动成员 */ }
};

三、联合的使用:同一时间只有一个 “有效成员”

联合的核心规则是:同一时间只能有一个成员是有效的。当写入一个成员时,之前写入的其他成员会被覆盖。

3.1 成员的访问与赋值

通过点运算符(.)或箭头运算符(->)访问联合成员,赋值时需明确当前使用的成员类型。 

ValueStorage vs;// 存储整数
vs.int_val = 1024;
cout << "整数:" << vs.int_val << endl; // 有效(输出1024)// 存储浮点数(覆盖之前的int_val)
vs.float_val = 3.14f;
cout << "浮点数:" << vs.float_val << endl; // 有效(输出3.14)
cout << "整数(无效):" << vs.int_val << endl; // 未定义行为!// 存储字符(覆盖之前的float_val)
vs.char_val = 'A';
cout << "字符:" << vs.char_val << endl; // 有效(输出A)

注意:读取非当前写入的成员属于未定义行为(Undefined Behavior, UB)。编译器可能优化掉无效的读取操作,导致不可预测的结果。

3.2 典型应用场景:状态与数据的绑定

联合常用于 “状态枚举 + 关联数据” 的场景。例如,游戏中的角色可能有不同状态(移动、攻击、死亡),每种状态需要不同的附加数据。使用联合可以避免为每种状态单独分配内存。 

// 定义状态枚举
enum class CharacterState {Moving,   // 移动时需要目标坐标Attacking,// 攻击时需要攻击对象IDDead      // 死亡时不需要额外数据
};// 联合:存储不同状态的关联数据
union StateData {struct { int x; int y; } target_pos; // 移动目标坐标int attack_id;                        // 攻击对象ID
};// 角色状态结构体
struct Character {CharacterState state;StateData data;
};// 使用示例
Character hero;// 移动状态
hero.state = CharacterState::Moving;
hero.data.target_pos.x = 100;
hero.data.target_pos.y = 200;// 攻击状态(覆盖data的存储)
hero.state = CharacterState::Attacking;
hero.data.attack_id = 9527;

通过这种设计,StateData的大小等于最大成员的大小(target_pos是 8 字节,attack_id是 4 字节,因此StateData占 8 字节),相比为每种状态单独定义结构体(如MovingDataAttackingData)节省了内存。 

四、嵌套联合:联合中的联合

联合可以作为其他类或联合的成员,形成嵌套结构。这种设计能更灵活地组织复杂数据。

4.1 基础嵌套示例

// 定义一个嵌套联合
union NestedUnion {struct { // 结构体作为联合成员int year;int month;int day;} date;struct {int hour;int minute;int second;} time;long long timestamp; // 64位时间戳
};

这个联合可以存储日期(年 / 月 / 日)、时间(时 / 分 / 秒)或 64 位时间戳(如 Unix 时间)。其内存大小等于最大成员的大小:

  • datetime各占 12 字节(假设int为 4 字节,无内存对齐);
  • timestamp占 8 字节(64 位系统);
    因此联合总大小为 12 字节(受内存对齐影响可能更大)。

4.2 嵌套联合的访问

访问嵌套联合的成员时,需要逐层指定路径: 

NestedUnion nu;// 存储日期
nu.date.year = 2024;
nu.date.month = 5;
nu.date.day = 20;
cout << "日期:" << nu.date.year << "-" << nu.date.month << "-" << nu.date.day << endl;// 存储时间(覆盖date的存储)
nu.time.hour = 12;
nu.time.minute = 30;
nu.time.second = 45;
cout << "时间:" << nu.time.hour << ":" << nu.time.minute << ":" << nu.time.second << endl;

五、匿名联合:没有名字的 “透明共享”

C++ 支持匿名联合(Anonymous Union),即没有名称的联合。其成员直接成为外层作用域的成员,无需通过联合变量名访问。

5.1 语法与特性

匿名联合的声明不需要变量名,且成员必须是公有的(public),不能有成员函数。它通常用于类或结构体内部,隐藏实现细节。

struct Employee {enum { Salary, Hourly } payment_type;union { // 匿名联合double monthly_salary;  // 月薪(当payment_type为Salary时有效)double hourly_wage;     // 时薪(当payment_type为Hourly时有效)};
};

访问匿名联合的成员时,直接使用成员名:

Employee e;// 月薪制员工
e.payment_type = Employee::Salary;
e.monthly_salary = 15000.0;// 时薪制员工(覆盖monthly_salary)
e.payment_type = Employee::Hourly;
e.hourly_wage = 200.0;

5.2 与命名联合的对比

特性命名联合匿名联合
声明方式union Name { ... };union { ... };
成员访问通过联合变量名(如u.member直接使用成员名(如member
作用域独立作用域外层作用域
成员访问控制支持public/private只能是public

5.3 应用场景:简化代码结构

匿名联合常用于需要 “透明共享内存” 的场景,例如在类中封装不同类型的属性,同时保持接口简洁。例如,一个存储 “数值或字符串” 的容器类: 

class Variant {
public:enum Type { Int, String };Variant(int val) : type(Int) { int_val = val; }Variant(const char* str) : type(String) { // 注意:C++11前不能直接存储std::string,需手动管理内存strcpy(string_val, str); }int get_int() const { return int_val; }const char* get_string() const { return string_val; }private:Type type;union { // 匿名联合int int_val;char string_val[256]; // 假设字符串不超过255字符};
};

通过匿名联合,int_valstring_val直接成为Variant类的成员,外部调用get_int()get_string()时无需关心内部存储细节。

六、联合的进阶:位域与类型双关

联合的共享内存特性使其在位操作和 类型双关(Type Punning)中非常有用。

6.1 位域(Bit Field)与联合的结合

位域用于指定成员占用的二进制位数,与联合结合可以高效操作二进制位。例如,一个存储 RGB 颜色值的结构: 

union Color {struct { // 位域结构体unsigned char r : 8; // 红色(8位)unsigned char g : 8; // 绿色(8位)unsigned char b : 8; // 蓝色(8位)} components;int rgb_value; // 32位整数(假设int为32位)
};

通过联合,可以同时以 “颜色分量” 或 “整数值” 的方式访问颜色数据:

Color c;
c.components.r = 255; // 红色全亮
c.components.g = 160; // 绿色中等
c.components.b = 0;   // 蓝色关闭
cout << "RGB整数值:" << c.rgb_value << endl; // 输出0xFFA000(假设小端序)

6.2 类型双关:安全与风险并存

类型双关指通过联合将同一块内存解释为不同类型。例如,将float的二进制位解释为int

union FloatInt {float f;int i;
};FloatInt fi;
fi.f = 3.14f;
cout << "浮点数3.14的二进制表示(整数):" << fi.i << endl; // 输出1078523331(具体值依赖浮点数编码)

注意:类型双关在 C++ 中属于未定义行为(违反严格别名规则)。虽然大多数编译器支持这种写法,但更安全的方式是使用reinterpret_castmemcpy。 

七、联合的局限与现代替代方案

尽管联合在内存效率上表现优异,但它也有明显的局限性:

7.1 主要局限

  • 类型安全:联合不跟踪当前有效成员的类型,需要开发者手动管理(如配合枚举)。
  • 成员限制:C++11 前无法存储非平凡类型成员,C++11 后需手动管理构造 / 析构。
  • 可维护性:过度使用联合会导致代码可读性下降,尤其是嵌套联合和匿名联合。

7.2 现代替代方案:std::variant

C++17 引入的std::variant是联合的 “类型安全增强版”。它可以存储多种类型中的一种,并自动跟踪当前类型,避免未定义行为。 

#include <variant>
#include <string>
#include <iostream>using namespace std;int main() {variant<int, float, string> v; // 可存储int/float/stringv = 1024;       // 当前类型为intv = 3.14f;      // 当前类型为floatv = "hello";    // 当前类型为string// 安全访问当前类型if (auto p = get_if<string>(&v)) {cout << "字符串内容:" << *p << endl; // 输出hello}return 0;
}

std::variant的优势在于:

  • 自动跟踪当前类型,避免未定义行为;
  • 支持任意可拷贝构造的类型(包括非平凡类型);
  • 提供安全的访问接口(如get<T>()visit())。 

八、总结

联合的核心价值是内存效率,适合以下场景:

  1. 内存敏感的嵌入式系统:在资源受限的环境中,联合可以显著减少内存占用。
  2. 与 C 语言兼容:处理 C 接口时,联合是保持数据结构一致的常用手段。
  3. 状态与关联数据的绑定:如游戏状态、通信协议解析(不同消息类型需要不同字段)。

尽管std::variant在类型安全上更优,但联合在性能和内存效率上仍不可替代。掌握联合的使用,是 C++ 程序员进阶的重要一步。

最后的思考:当你需要设计一个 “多类型数据容器” 时,会选择联合还是std::variant?欢迎在评论区分享你的经验!


附:联合关键知识点表格

特性说明
内存共享所有成员共用同一块内存,大小等于最大成员大小
成员限制无静态成员、无引用成员;C++11 前无复杂类型成员
有效成员规则同一时间只有一个成员有效,需手动管理状态
匿名联合成员直接作为外层作用域成员,无名称,只能公有
现代替代方案std::variant(类型安全,自动跟踪当前类型)

相关文章:

  • QT6与VS下实现没有CMD窗口的C++控制台程序
  • vue3-ts-qrcode :安装及使用记录 / 配置项 / 效果展示
  • 编辑器及脚本案例
  • Android图形系统框架解析
  • 探秘阿里云EBS存储:云计算的存储基石
  • Qt蓝图式技能编辑器状态机模块设计与实现
  • day09——Java基础项目(ATM系统)
  • ARINC653系统架构
  • Stroke-based Cyclic Amplifier (SbCA方法):实现图像任意尺度超清放大
  • 聊聊GPIO 工作模式
  • 英飞凌推出ASIL-B等级并具唤醒功能的第三代3D霍尔传感器TLE493D-x3系列
  • 服务器如何从http升级到https(nginx)
  • day02-Docker
  • B站 XMCVE Pwn入门课程学习笔记(4)(不断更新)
  • 计算机网络:(四)物理层的基本概念,数据通信的基础知识,物理层下面的传输媒体
  • 不同系统修改 Docker Desktop 存储路径(从C盘修改到D盘)
  • FPGA基础 -- Verilog 行为级建模之过程性结构
  • 【深度学习】卷积神经网络(CNN):计算机视觉的革命性引擎
  • 从0开始学习R语言--Day27--空间自相关
  • Vue.js 按键修饰符详解:提升键盘事件处理效率
  • 柳州专业做网站/网络营销薪酬公司
  • 搭建网站费用/精准营销系统
  • 做网站 找风投/凡科建站登录
  • nb-iot网站开发/seo搜索引擎优化课后答案
  • 怎样能创建一个网站/疫情优化调整
  • 个人网站建设需要备案吗/合肥网站seo公司