前面咱们把结构体和共用体都讲完了。今天说个相对轻松点的东西——枚举。
枚举这玩意儿,说白了就一句话:给一堆整数起名字,让代码好读。
你可能会想:整数就整数呗,起啥名字?我写int status = 1不是挺清楚的吗?
清楚啥呀。你过两天再看这段代码,你还记得1代表啥吗?是“成功”还是“失败”?是“未开始”还是“进行中”?你自己都忘了。别人看你代码更是两眼一抹黑,全靠猜。
所以枚举就是来解决这个问题的。它让你用有意义的名字来代替那些“光秃秃的数字”。
先看个反例:一堆莫名其妙的数字
假设你写个程序,处理订单状态。订单有几种状态:待付款、已付款、已发货、已收货、已取消。
你不用枚举,代码长这样:
int status = 0; // 0代表待付款 // ... if (status == 0) { printf("请尽快付款\n"); } else if (status == 1) { printf("已付款,等待发货\n"); } else if (status == 2) { printf("已发货,请注意查收\n"); } else if (status == 3) { printf("已收货,交易完成\n"); } else if (status == 4) { printf("订单已取消\n"); }
代码能跑,但问题在哪儿?你满屏都是0、1、2、3、4,你自己写的时候都得翻回去看注释,才知道哪个数字代表哪个状态。 万一你手一抖,把status == 2写成了status == 3,程序也不报错,就是逻辑错了,你找半天找不出来。
更惨的是,如果订单状态增加了,你加了个“退款中”,然后得把所有用到数字的地方全改一遍,哪个地方漏改了,Bug就埋下了。
用枚举改写一下
咱们用枚举重新写:
enum OrderStatus { UNPAID, // 待付款 PAID, // 已付款 SHIPPED, // 已发货 RECEIVED, // 已收货 CANCELLED // 已取消 }; int status = UNPAID; if (status == UNPAID) { printf("请尽快付款\n"); } else if (status == PAID) { printf("已付款,等待发货\n"); } else if (status == SHIPPED) { printf("已发货,请注意查收\n"); } else if (status == RECEIVED) { printf("已收货,交易完成\n"); } else if (status == CANCELLED) { printf("订单已取消\n"); }
你看,status == SHIPPED是不是比status == 2清楚多了?你一眼就能看出这是在判断“已发货”。别人读你代码也不用猜。
这就是枚举的核心价值:让代码自己说话。
枚举的定义和使用
定义枚举的格式:
enum 枚举名 { 名字1, 名字2, 名字3, ... };
每个名字之间用逗号隔开,最后可以加个逗号也可以不加。
定义完了之后,这些名字就可以当成整数常量来用了。你写enum OrderStatus status;就是定义了一个枚举变量,它的值只能是那堆名字之一。
但其实枚举变量本质就是整数。你直接写int status;也行,然后给它赋UNPAID,效果一样。不过写enum OrderStatus status;更规范,更清楚你想表达什么。
枚举的值到底是啥?
你定义了:
enum Weekday { MON, TUE, WED, THU, FRI, SAT, SUN };
编译器默认给它们从0开始编号:MON=0,TUE=1,WED=2,THU=3,FRI=4,SAT=5,SUN=6。
你可以打印出来看看:
printf("%d\n", MON); // 输出0 printf("%d\n", WED); // 输出2 printf("%d\n", SUN); // 输出6
你也可以自己指定值:
enum Color { RED = 10, GREEN = 20, BLUE = 30 };
那RED就是10,GREEN是20,BLUE是30。
如果只给一部分指定值,后面的会跟着递增:
enum Month { JAN = 1, FEB, // 2 MAR, // 3 APR, // 4 MAY = 10, JUN, // 11 JUL // 12 };
你看,JAN指定了1,后面FEB自动2,MAR自动3。MAY指定了10,后面JUN自动11,JUL自动12。
所以枚举其实就是给整数常量起名字。你指定不指定值都行,编译器都有规则。
枚举配合switch——绝配
枚举跟switch语句简直是天生一对:
enum OrderStatus status = SHIPPED; switch (status) { case UNPAID: printf("请尽快付款\n"); break; case PAID: printf("已付款,等待发货\n"); break; case SHIPPED: printf("已发货,请注意查收\n"); break; case RECEIVED: printf("已收货,交易完成\n"); break; case CANCELLED: printf("订单已取消\n"); break; default: printf("未知状态\n"); break; }
用枚举配switch,可读性直接拉满。谁看谁明白。
枚举跟#define的区别
你可能会问:我用#define也能给整数起名字啊,为啥非得用枚举?
#define UNPAID 0 #define PAID 1 #define SHIPPED 2
这也能用。但枚举比#define好在几个地方:
第一,枚举有类型。 enum OrderStatus是一个真正的类型,编译器会做类型检查。而#define只是文本替换,没有类型,容易写错。
第二,枚举有作用域。 枚举的名字遵循作用域规则,可以在函数里定义,出了函数就失效。#define从定义处到文件末尾都有效,容易污染全局命名空间。
第三,枚举调试方便。 你在调试器里看枚举变量,可能能直接显示名字。#define就只是数字,调试的时候跟没定义一样。
第四,枚举是连续的定义。 你写enum {A, B, C},编译器自动帮你编号。#define你得一个个手动写。
所以能用枚举的地方,尽量用枚举,别用#define去定义一连串相关的整数常量。
一个完整的例子:石头剪刀布
咱们写个小游戏,用枚举表示出拳类型:
#include <stdio.h> #include <stdlib.h> #include <time.h> enum Choice { ROCK, // 石头 PAPER, // 布 SCISSORS // 剪刀 }; const char* choice_name(enum Choice c) { switch (c) { case ROCK: return "石头"; case PAPER: return "布"; case SCISSORS: return "剪刀"; default: return "未知"; } } int main() { enum Choice player, computer; srand(time(NULL)); printf("请输入你的选择(0=石头,1=布,2=剪刀):"); int input; scanf("%d", &input); player = (enum Choice)input; computer = (enum Choice)(rand() % 3); printf("你出了:%s\n", choice_name(player)); printf("电脑出了:%s\n", choice_name(computer)); if (player == computer) { printf("平局!\n"); } else if ((player == ROCK && computer == SCISSORS) || (player == PAPER && computer == ROCK) || (player == SCISSORS && computer == PAPER)) { printf("你赢了!\n"); } else { printf("你输了!\n"); } return 0; }
这个例子里,enum Choice定义了三种出拳,choice_name函数把枚举转成字符串输出,逻辑判断用的也是枚举的名字。整段代码读起来特别清晰。
枚举的遍历——小心踩坑
你可能会想:枚举既然是一串连续的数字,那能不能遍历?
for (enum Weekday d = MON; d <= SUN; d++) { printf("%d\n", d); }
这在C语言里能编译,但不推荐。因为C语言里枚举变量本质上就是整数,你写d++编译器不会拦着你,但这不是好习惯。枚举的设计意图是让你用“名字”,不是让你当数字来算。而且如果枚举值不是连续的,遍历就会出问题。
你如果想遍历所有枚举值,建议单独定义一个数组:
enum Weekday { MON, TUE, WED, THU, FRI, SAT, SUN, WEEKDAY_COUNT }; const char *weekday_names[] = { "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日" }; for (int i = 0; i < WEEKDAY_COUNT; i++) { printf("%s\n", weekday_names[i]); }
末尾那个WEEKDAY_COUNT是个小技巧——它自动等于前面所有枚举值的个数,你拿它当数组长度和循环边界,完美。
几个常见的坑
坑1:把枚举当字符串打印
enum Weekday d = MON; printf("%s\n", d); // 错误!枚举是整数,不能%s打印 printf("%d\n", d); // 正确,输出0
想打印枚举的名字,得自己写个函数把枚举转成字符串,就像上面那个choice_name函数一样。
坑2:枚举值可以重复
enum Test { A = 1, B = 1, // 跟A一样的值,合法但容易混淆 C = 2 };
编译器不报错,但A和B值一样,容易出逻辑问题。别这么干。
坑3:枚举变量可以赋任意整数
enum Weekday d = MON; d = 100; // 语法上合法,但语义上不对
C语言对枚举的类型检查比较宽松,你给它赋一个不在枚举范围内的整数,编译器最多给个警告,不一定报错。所以你自己得注意,别乱赋。
坑4:不同枚举的名字不能重复
enum A { X, Y }; enum B { X, Z }; // 错误!X已经被A用了
枚举的名字在同一个作用域里不能重复,因为它们在编译器眼里就是整数常量。
总结一下
枚举就一句话:给整数起名字,让代码好读。
核心要点:
-
enum 名字 { 值1, 值2, ... }; -
默认从0开始自动编号,也可以手动指定
-
枚举值本质就是整数,但可读性比裸数字强太多
-
跟switch配合使用效果最佳
-
能代替
#define定义一连串相关常量
枚举在实际开发中用得特别多——状态机、选项、错误码、类型标记,全都用它。你今天把枚举学会了,以后看别人的代码,看到满屏的enum就不会发怵了。
回去自己动手,把上面那个石头剪刀布的游戏敲一遍。再写一个“星期几”的枚举,输入1到7输出对应的星期名。练熟了,枚举就是你的了。下节课咱们讲typedef——给类型起别名,让代码更简洁。