C++ 自定义类型进阶:联合(Union)与枚举(Enum)
联合和枚举是 C++ 中两种重要的自定义类型,它们在内存布局、类型安全和表达能力上各有特色。现代 C++(C++11 及以后)对两者都进行了显著增强。
1. 枚举(Enumeration)
枚举用于定义一组具名整数常量,提高代码可读性和类型安全性。
1.1 传统 enum vs enum class(强烈推荐后者)
// 传统 enum(作用域渗漏)
enum Color { RED, GREEN, BLUE };
// enum class(作用域枚举,C++11 起)
enum class Status : int { // 可显式指定底层类型
Success = 0,
Failure = 1,
Timeout = 2
};
主要区别:
| 特性 | 传统 enum | enum class (scoped enum) |
|---|---|---|
| 作用域 | 渗漏到外围作用域 | 严格限定在枚举名内 |
| 隐式转换 | 可隐式转为 int | 不允许隐式转换 |
| 底层类型 | 默认 int,可指定 | 可指定(int、char、uint64_t 等) |
| 前向声明 | 困难 | 容易(需指定底层类型) |
与 std::hash 等配合 | 较差 | 优秀 |
推荐写法(现代 C++):
enum class LogLevel : uint8_t {
Debug = 0,
Info,
Warning,
Error,
Fatal
};
// 使用
LogLevel level = LogLevel::Info;
// int i = level; // 错误!不允许隐式转换
int i = static_cast<int>(level); // 显式转换
// 枚举类与 switch 配合完美
switch (level) {
case LogLevel::Debug: ... break;
// case 必须使用 LogLevel::XXX,不会有名字冲突
}
1.2 进阶用法
- 底层类型控制:
std::underlying_type_t<LogLevel> - 枚举类 +
using别名 提升可读性 - 位标志(Bit Flags):使用
enum class+ 运算符重载实现类型安全的位操作
enum class Permission : unsigned {
Read = 1 << 0,
Write = 1 << 1,
Exec = 1 << 2,
All = Read | Write | Exec
};
// 运算符重载(示例)
constexpr Permission operator|(Permission a, Permission b) {
return static_cast<Permission>(
static_cast<unsigned>(a) | static_cast<unsigned>(b));
}
- 反射支持(C++26 实验性或第三方):枚举名字符串转换等。
2. 联合(Union)
联合是一种节省内存的类型,所有成员共享同一块内存(大小等于最大的非静态成员)。
2.1 基本用法与限制
union Data {
int i;
float f;
char str[8];
}; // sizeof(Data) == 8(取决于平台)
重要规则(C++ 核心):
- 同一时刻只能有一个成员是“活跃的”(active)。
- 读非活跃成员属于未定义行为(UB)。
- C++11 前:不能包含拥有非平凡构造函数/析构函数的成员。
- C++11 后:允许,但需要手动管理生命周期(placement new / 显式析构)。
2.2 匿名联合(Anonymous Union)
常用于结构体内部节省内存:
struct Variant {
int type;
union { // 匿名联合
int i;
double d;
std::string s; // C++11 后允许(需手动管理)
};
};
2.3 进阶:带非平凡成员的 Union
union U {
int i;
std::string s; // 有构造函数
U() : i(0) {} // 默认构造 int
U(const std::string& str) : s(str) {}
~U() { if (active == 2) s.~basic_string(); } // 必须手动析构
private:
int active = 1; // 跟踪活跃成员
};
强烈建议:不要手动写这种联合,使用 std::variant 替代。
3. std::variant —— 现代安全的“类型安全的联合”
C++17 引入,是联合的类型安全升级版。
#include <variant>
#include <string>
using Value = std::variant<int, double, std::string>;
Value v = 42;
v = 3.14;
v = "hello";
// 访问方式
if (auto* p = std::get_if<int>(&v)) {
std::cout << *p;
}
// 使用 std::visit(推荐,最优雅)
std::visit([](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int>) { ... }
else if constexpr (...) { ... }
}, v);
优点:
- 类型安全,无 UB
- 自动管理生命周期
- 支持
std::monostate处理无值情况 - 可持有任意类型(包括非平凡)
性能:与手工 union 几乎无差别,但更安全。
4. 最佳实践总结
- 枚举:
- 永远优先使用
enum class。 - 需要与 C 代码交互时才使用传统
enum。 - 给底层类型指定合适的大小(
uint8_t、uint32_t等)。
- 联合:
- 简单 POD 类型(如数值、指针)可直接用
union。 - 复杂类型 → 使用
std::variant。 - 需要极致性能 + 手动控制时,才考虑带生命周期管理的 union(极少见)。
- 组合使用:
enum class Type { Int, Double, String };
struct Any {
Type t;
std::variant<int, double, std::string> v;
};
- 内存对齐与
[[no_unique_address]](C++20)在更复杂的自定义类型中也很重要。
5. 常见陷阱
- 读了非活跃的 union 成员 → UB(可能崩溃、返回垃圾值)。
enum class忘记static_cast导致编译错误(这是特性,不是 bug)。- 在
std::variant中持有引用类型(不允许,必须用std::reference_wrapper)。 - 跨平台时注意枚举底层类型和 union 对齐。
掌握 enum class + std::variant 后,你的自定义类型会变得更加类型安全、表达力强且现代。传统 union 和 enum 主要在与遗留代码、嵌入式、驱动等场景仍有价值。
如果你想深入以下任一主题,告诉我:
std::variant的完整实现技巧与std::visit进阶- 位字段(Bit-field)与联合的结合
- 枚举的反射 / 序列化
- Tagged Union 手写模式 vs
variant - 在性能敏感代码中的优化案例
继续下一部分吗?(如 std::optional、std::any、CRTP、Type Erasure 等)