Menu

C 语言枚举 enum:命名常量、自动编号和转字符串

如何在 C 语言中定义枚举:自动编号,显式赋值,在 switch 中使用枚举让编译器发现遗漏的分支,把枚举转换为字符串,以及让代码保持易读的命名规范。

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

满是裸数字的代码,没人看得懂。if (state == 2) 什么也没告诉你;if (state == STATE_RUNNING) 则一目了然。enum 是 C 语言给一小组固定整数起正式名字的方式,而且和一堆 #define 不同,这些名字被归入同一个编译器知道的类型中。

定义枚举

声明中给出一个标签名,并在大括号内列出各个枚举常量:

有两点需要马上注意。第一,编号从 0 开始,每次加一,所以 RED 是 0,GREEN 是 1,BLUE 是 2。第二,枚举常量的名字在外层作用域中处处可见:你写的是 GREEN,而不是 Color.GREEN。它们和你的变量处于同一个命名空间,所以 C 代码习惯把它们写成全大写,或者加上类型前缀(COLOR_RED),以避免冲突。

打印枚举用 %d,因为枚举值本身就是整数值。

显式赋值

可以给任何一个枚举常量指定一个常量值,它后面的常量会从这里接着编号:

这些值不必唯一,也不必有序:两个枚举常量可以共用同一个数字,偶尔可以用来定义别名(COLOR_DEFAULT = COLOR_BLACK)。它们必须是编译时的整数常量,所以不能用变量来计算。

一个常见的惯用写法用它来记录数量:

enum Suit {
    CLUBS, DIAMONDS, HEARTS, SPADES,
    SUIT_COUNT           /* automatically 4 - stays correct if you add a suit */
};

SUIT_COUNT 并不是真正的花色,而是花色的数量,由自动编号免费维护。循环和数组大小都可以使用它,永远不会过时。

typedef enum

和结构体一样,C 语言中完整的类型名是 enum Color,用 typedef 可以省掉这个关键字:

因为枚举常量就是从 0 开始的普通整数,它们还可以直接当作数组下标:dx[DIR_EAST] 无需任何转换就能用。这是从 0 开始编号的枚举最有用的特性。

在 switch 中使用枚举

把枚举和 switch 搭配使用时,这个类型才真正体现出价值。用 -Wall 编译,编译器会在你遗漏某个 case 时提醒你。

在枚举中加一个 STATE_FAILED,GCC 就会警告:enumeration value 'STATE_FAILED' not handled in switch。这个警告是 C 语言中最接近“穷尽性检查”的东西,也是这类 switch 不写 default: 分支的有力理由:default 会让警告消失,让新增的状态悄悄漏过、无人处理。应该像 describe 那样,把兜底的返回值放在 switch 之后。

把枚举转换为字符串

C 语言在运行时不保留名字;printf("%s", GREEN) 行不通,因为 GREEN 就是 1。有两种写法可以解决。

switch 写法,编译器会帮你检查:

以及查找表写法,更短,但没有检查:如果数组和枚举对不上了,不会有任何警告:

指定下标的数组初始化([RED] = "RED")至少能让名字和对应位置绑定在一起,所以调整枚举顺序时不会悄悄打乱字符串。边界检查也很重要,因为没有什么能阻止调用者传入枚举范围之外的值,见下一节。

枚举并非滴水不漏

枚举变量底层就是整数,C 语言不会检查它的范围:

对枚举常量做算术运算可以,和普通 int 比较可以,越界的强制转换也会被接受。所以,把枚举当作极好的文档和有用的编译器提示,而不是一种保证。任何来自程序外部的值,比如文件、命令行参数、网络消息,都要先校验,再拿去做 switch。

大小和存储

在 C17 中,每个枚举常量都必须能放进 int:enum Huge { BIG = 3000000000 }; 违反了约束,严格模式的编译器(-pedantic-errors)会拒绝它,尽管 gcc 和 clang 在默认模式下会作为扩展接受它。枚举常量本身的类型是 int,而枚举类型的大小由实现定义(实际上就是 int):

(C23 取消了这个限制:枚举常量可以超出 int 的范围,你甚至可以用 enum E : unsigned long { ... } 指定底层类型。在你的工具链以 C23 为目标之前,让枚举常量保持在 int 范围内。)

因为具体选择由实现定义,永远不要把枚举直接写进二进制文件或网络数据包,并指望另一台机器能正确读回。在边界处把它转换为 <stdint.h> 中的定宽类型,比如 uint8_t 或 uint32_t。

enum 与 #define

两者都能给数字起名字。一般来说枚举更好:

enum#define
由谁处理编译器预处理器
分组相关的值共享一个类型每个常量各自独立
自动编号有没有,需手动编号
在调试器中可见通常能看到名字看不到,只有数字
switch 穷尽性警告有没有
作用域遵守块作用域文本替换,直到 #undef

当常量不是整数(字符串、浮点数),或者必须能被预处理器本身使用时(比如 条件编译),就用 #define。对于一组相关的整数状态、标志或种类,用枚举。

命名规范

枚举的名字位于普通作用域中,很容易冲突。下面这些规范能让大型 C 代码库保持清晰:

  • 给每个枚举常量加上类型前缀:COLOR_RED、STATE_IDLE、HTTP_OK。图形程序中没有前缀的 RED,迟早会碰上别人的 RED。
  • 枚举常量用全大写加下划线(SHOUT_CASE),和其他常量的写法保持一致。
  • 类型名用单数的 PascalCase 或 snake_case_t:Color、State 或 color_t,整个项目保持一致。
  • 当你要用枚举做循环或下标时,在最后加一个 _COUNT 哨兵值。

常见问题

C 语言中怎么定义枚举?

enum Color { RED, GREEN, BLUE }; 定义了一个类型,它的取值就是这三个命名常量。默认从 0 开始编号,所以 RED 是 0,GREEN 是 1,BLUE 是 2。用 enum Color c = GREEN; 声明变量,或者加上 typedef,这样就可以只写 Color c。

C 语言中可以给枚举指定具体的值吗?

可以:enum Status { OK = 200, NOT_FOUND = 404, ERROR = 500 };。你可以只给一部分赋值,其余的自动顺延:没有显式赋值的名字会接着前一个的值继续,所以 enum { A = 10, B, C }; 中 B = 11,C = 12。

C 语言中怎么把枚举转换为字符串?

C 语言没有内置的办法,枚举常量的名字在运行时并不存在。常见做法是写一个小函数,用 switch 为每个值返回一个字符串字面量,或者用一个以枚举为下标的字符串数组。switch 写法更安全,因为新增枚举常量却没有对应的 case 时,-Wall 会发出警告。

C 语言中枚举的大小是多少?

由实现定义,但实际上通常是 sizeof(int),在大多数系统上是 4 字节。编译器会选择一个能容纳所有枚举常量的整数类型。在设计二进制文件或网络数据包的格式时,不要假设它有固定大小,应改用 <stdint.h> 中的定宽类型。

Coddy 编程语言插图

和 Coddy 一起学编程

开始