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 enum class 隐式转整数,作用域污染 强类型,作用域隔离,可指定底层类型 union 联合体 原始 union std::variant 未定义行为风险,需手动管理 类型安全,自动管理,支持 visit 核心原则:用 enum class 替代 enum,用 variant 替代 union

总结对比表

特性 enum enum class union std::variant
类型安全
内存占用 4字节(默认int) 可指定 最大成员 最大成员+标签
作用域 全局污染 隔离
现代C++推荐

好了,枚举和联合体就聊到这儿。记住一句话:能用 enum class 就别用 enum,能用 std::variant 就别用原始 union。这样你的代码会更安全,也更容易维护。嗯,这些经验都是我在实际项目中踩坑踩出来的,希望对你有帮助。

公众号:蓝海资料掘金营,微信deep3321