【C语言-自定义数据类型】3. 枚举——给整数起个有意义的名字

开发技术 作者: 精灵星空 SKY315.COM 更新于: 浏览: 7
码字发文不易,转载请注明出处【精灵星空 SKY315.COM】

前面咱们把结构体和共用体都讲完了。今天说个相对轻松点的东西——枚举。

枚举这玩意儿,说白了就一句话:给一堆整数起名字,让代码好读。

你可能会想:整数就整数呗,起啥名字?我写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函数把枚举转成字符串输出,逻辑判断用的也是枚举的名字。整段代码读起来特别清晰。

枚举的遍历——小心踩坑

你可能会想:枚举既然是一串连续的数字,那能不能遍历?

c
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——给类型起别名,让代码更简洁。