18、枚举与联合:enum枚举类型、union联合体、类型安全
说实话,枚举和联合体这两个东西,在C++里属于那种「看着简单,用好了很香,用砸了很坑」的特性。我最早接触它们是在一个嵌入式项目里,那时候为了省内存,把各种状态和数据类型塞进联合体,结果调试的时候差点把自己绕晕。后来慢慢摸透了它们的脾气,才真正体会到什么叫「类型安全」和「内存复用」的平衡。
今天咱们就把这两个老朋友好好捋一遍。你想想看,枚举帮我们把散落的常量变成有名字的类型,联合体则是在同一块内存上玩变脸。但怎么用才不翻车?这就是咱们要聊的重点。
enum枚举:从数字到名字
先说说枚举。说白了,枚举就是给整数起个名字。比如你写一个状态机,用0、1、2表示空闲、忙碌、错误,过两周你自己都记不住哪个是哪个。用枚举就清楚多了。
enum Status {
Idle, // 0
Busy, // 1
Error // 2
};
Status current = Busy;
if (current == Idle) {
// 做点什么
}
我个人习惯,枚举值都用大写开头,这样一眼就能看出来是枚举常量。不过这只是风格问题,你爱用小写也行,团队统一就好。
核心要点:枚举让代码自文档化。你看到 Status::Busy 就知道是忙碌状态,比看到 1 强太多了。
enum class:强类型枚举
传统C++的枚举有个问题——它就是个整数,可以跟其他整数乱比较。比如:
enum Color { Red, Green };
enum Fruit { Apple, Banana };
if (Red == Apple) { // 这居然能编译!而且true!
// 太可怕了
}
我在项目中遇到过这种bug,排查了半天才发现是两个枚举值撞了。后来C++11引入了 enum class,这个问题就解决了。
enum class Color { Red, Green };
enum class Fruit { Apple, Banana };
// if (Color::Red == Fruit::Apple) { // 编译错误!类型不同
// }
嗯,这里要注意:enum class 默认底层类型是 int,但你可以指定其他类型:
enum class Flag : unsigned char {
None = 0,
Read = 1,
Write = 2,
Execute = 4
};
这样既安全又省内存。我建议你在新代码里一律用 enum class,除非你要跟C代码交互。
小技巧:可以用 static_cast<int>(Color::Red) 把强类型枚举转成整数,但别滥用。
union联合体:内存里的变脸大师
联合体就更有意思了。它让多个成员共享同一块内存,同一时间只有一个成员是「活的」。说白了,就是给你一块内存,你想怎么解释都行。
union Data {
int i;
float f;
char c[4];
};
Data d;
d.i = 42;
// 此时 d.f 和 d.c 的内容取决于内存布局,别乱读
我曾经在一个网络协议解析里用过联合体,把收到的字节流直接映射成结构体,效率确实高。但坑也大——你读错成员,数据就全乱了。
警告:永远不要读取联合体中「非活跃」的成员。这是未定义行为!C++标准说只有最后一个写入的成员才是活跃的。
类型安全的联合体:variant
既然原始联合体这么危险,C++17给我们带来了 std::variant。它本质上是一个类型安全的联合体,会记住当前存的是哪种类型。
#include <variant>
#include <iostream>
std::variant<int, float, std::string> v;
v = 42;
std::cout << std::get<int>(v) << '\n'; // 正确
v = 3.14f;
std::cout << std::get<float>(v) << '\n'; // 正确
// std::cout << std::get<int>(v); // 抛出 std::bad_variant_access
我个人特别喜欢 std::visit 这个模式,可以优雅地处理所有可能类型:
std::visit([](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int>) {
std::cout << "int: " << arg << '\n';
} else if constexpr (std::is_same_v<T, float>) {
std::cout << "float: " << arg << '\n';
} else if constexpr (std::is_same_v<T, std::string>) {
std::cout << "string: " << arg << '\n';
}
}, v);
你看,这样就不会有未定义行为,编译器帮你检查所有分支。我建议你在需要「同一位置存不同类型」的场景下,优先考虑 std::variant,而不是原始联合体。
枚举与联合体的搭配使用
在实际项目中,枚举和联合体经常一起出现。比如一个协议包,类型用枚举表示,数据用联合体表示:
enum class PacketType : uint8_t {
Login,
Logout,
Data
};
struct Packet {
PacketType type;
union {
struct { char user[32]; char pass[32]; } login;
struct { uint32_t session_id; } logout;
struct { uint8_t payload[256]; uint32_t len; } data;
};
};
嗯,这里要注意:C++11以后,联合体里可以放有非平凡构造函数的成员(比如 std::string),但需要你自己管理构造和析构。我个人觉得,除非性能要求极高,否则还是用 std::variant 省心。
避坑指南:我曾经在一个项目里用原始联合体存 std::string,结果忘记手动调用析构函数,内存泄漏了。后来全部改成 std::variant,世界清净了。
知识体系图
下面这张图帮你理清枚举和联合体的核心脉络:
总结对比表
| 特性 | enum | enum class | union | std::variant |
|---|---|---|---|---|
| 类型安全 | 弱 | 强 | 弱 | 强 |
| 内存占用 | 4字节(默认int) | 可指定 | 最大成员 | 最大成员+标签 |
| 作用域 | 全局污染 | 隔离 | — | — |
| 现代C++推荐 | ❌ | ✅ | ❌ | ✅ |
好了,枚举和联合体就聊到这儿。记住一句话:能用 enum class 就别用 enum,能用 std::variant 就别用原始 union。这样你的代码会更安全,也更容易维护。嗯,这些经验都是我在实际项目中踩坑踩出来的,希望对你有帮助。