Menu

C++ 枚举:enum class 与普通 enum 的区别

学习 C++ 枚举:如何声明枚举,为什么作用域枚举 enum class 比普通 enum 更安全,自定义底层值,在 switch 中使用枚举值,以及与整数之间的相互转换。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

枚举是用来做什么的

结构体把几个相关的值组合成一个对象。enum 解决的是另一个问题:它给一小组固定的选项起名字。你不必再记住 0 表示“红色”、1 表示“绿色”、2 表示“蓝色”,而是直接写 Color::Red,由编译器来帮你把关。

只要一个变量只能是少数几种有名字的状态之一,比如红绿灯的颜色、扑克牌的花色、连接状态,就用 enum。这样代码能自己说明含义,编译器也能发现拼写错误和漏掉的情况,而这些是裸整数永远做不到的。

声明枚举

现代 C++ 有两种枚举。先从你几乎总是应该使用的那种开始:作用域枚举 enum class。你列出名字,然后通过枚举名加 :: 访问每个枚举值:

注意你写的是 Color::Green,而不是单独的 Green。这些值本身只是标签:你可以比较它们、给变量赋值、把它们传来传去,但很少关心它们背后的数字。默认情况下,Red 是 0,Green 是 1,Blue 是 2,从零开始往上数。

普通 enum 与 enum class

较老的无作用域 enum(不带 class 关键字)会把它的名字直接放进外围作用域,并且会自行转换成 int。听起来很方便,但它会带来两个实际问题:

enum Color { Red, Green, Blue };
enum Fruit { Apple, Banana, Red };  // error: 'Red' already declared

enum Status { Active, Inactive };
int x = Active;          // compiles silently - is this what you meant?
if (Active == Banana) {  // compares unrelated enums via int - allowed!
}

因为普通枚举值是全局名字,两个枚举只要有一个相同的标签就会冲突。又因为它们会退化成 int,编译器会毫无怨言地比较两个毫不相干的枚举的值。作用域枚举 enum class 把这两个问题都解决了:名字位于类型内部,而且这个类型不会悄悄变成 int:

经验法则:默认使用 enum class。只有当你确实需要隐式转换成 int 时,比如旧的 C 风格标志常量,才退回到普通 enum。

自定义值与底层类型

你可以给枚举值指定明确的数字。没有指定的会从前一个值继续往上数,这对 HTTP 状态码或位标志这类东西很方便:

每个枚举背后都有一个整数类型,默认是 int。当你在意大小时,可以把它固定为更小的类型,比如在紧凑的结构体中存放大量枚举,或者要匹配某种传输格式:

选定底层类型也就确定了值必须落在的范围:底层类型为 uint8_t 的枚举无法保存大于 255 的值。

枚举与 int 之间的转换

作用域枚举 enum class 从不隐式转换,这正是它的意义所在。当你确实需要那个数字时,比如打印它、用它作为数组下标,或者从文件中读取它,就使用 static_cast。从 enum 转成 int 总是安全的:

把 int 转回枚举才是危险的方向。这个转换不会检查数字是否对应一个真实的枚举值,它会给你一个严格来说落在枚举命名集合之外的值:

Suit s = static_cast<Suit>(2);   // fine - that's Clubs
Suit bad = static_cast<Suit>(99); // compiles, but 99 is not a valid Suit
// using `bad` in a switch or as an array index is a lurking bug

如果整数来自用户输入或文件,在转换之前要自己校验范围,否则你会得到一个没有任何 case 能处理的值,这会在后面埋下未定义行为的隐患。

在 switch 中使用枚举

因为枚举表示“固定集合中的一个”,所以它和 switch 是完美的搭配。当你覆盖了每个枚举值后,如果以后新增了一个值却忘了处理,很多编译器会给出警告。这是裸整数无法免费提供的安全保障:

一个坑:C++ 没有内置的方法来打印枚举值的名字。对于作用域枚举,cout << TrafficLight::Red 无法编译;即使是普通枚举,它打印出来的也是数字,而不是“Red”。用一个小小的 switch,或者像上面那样的查找表,是把枚举转换成人能读懂的字符串的常用方法。

下一步:异常

枚举和结构体让你描述数据长什么样。但真实的程序还必须处理出错的情况:打不开的文件、解析不了的数字、超出范围的值。C++ 用异常来处理这些失败路径,这就是下一页的内容。

常见问题

C++ 中 enum 和 enum class 有什么区别?

普通 enum 会把它的名字泄漏到外围作用域,并且会隐式转换成 int,这会导致命名冲突和意外的比较。作用域枚举 enum class 把名字留在枚举内部(Color::Red),没有显式转换就拒绝转成 int。在现代 C++ 中优先使用 enum class:它类型安全,能避开那些经典的坑。

怎样把 C++ 枚举转换成 int?

普通 enum 会隐式转换,所以 int n = Red; 直接就能用。作用域枚举 enum class 需要显式转换:int n = static_cast<int>(Color::Red);。反过来,把 int 转回枚举:Color c = static_cast<Color>(2);。但要小心,运行时并不会检查这个值是否是一个有效的枚举值。

C++ 枚举的第一个值从几开始?

默认情况下,第一个枚举值是 0,之后每一个都比前一个大 1。所以在 enum class Level { Low, Mid, High }; 中,Low 是 0,Mid 是 1,High 是 2。你可以给其中任何一个指定明确的值来覆盖这个规则。

Coddy 编程语言插图

和 Coddy 一起学编程

开始