C++ 枚举

在实际开发中,有些数据只能在几个固定的选项中取值。比如一年四季只有春、夏、秋、冬,一个订单的状态只有待付款、已付款、已发货、已完成。

如果我们直接使用整数 0、1、2、3 来表示这些状态,代码里就会到处出现类似 if (status == 2) 的判断。过几天再回头看,往往很难记清 2 到底代表 “已付款” 还是 “已发货”。借助 C++ 中的 “枚举(enum)”,我们就能给这些固定选项起上清晰好懂的名字。

C++ 枚举的定义与使用

在 C++ 中,所谓的枚举,就是把一组彼此相关、数量有限的固定选项一一列举出来,归到同一种类型中。我们可以使用 enum 关键字来定义枚举类型,其中 enum 是英文 “enumeration(枚举)” 的缩写。

语法:

enum 枚举类型名 {
    枚举值1,
    枚举值2,
    ...
    枚举值n
};

说明:

enum 后面是我们自己定义的枚举类型名,习惯上首字母大写,这一点和前面学过的结构体一样。花括号 {} 内部的每一项称为 “枚举值”,各项之间用英文逗号(,)隔开,最后一项后面的逗号可以省略。

需要注意的是,右花括号 } 后面的英文分号(;)不能漏掉。定义好枚举类型后,我们就可以使用它来定义枚举变量,格式为 枚举类型名 变量名 = 枚举值;。

在实际开发中,枚举变量经常配合 switch 语句一起使用。因为状态、方向、季节这类数据本来就是 “从几个固定选项里选一个”,正好对应多分支匹配。

示例 1:使用枚举表示订单状态

#include <iostream>

// 定义订单状态枚举
enum OrderStatus {
    Pending,     // 待付款
    Paid,        // 已付款
    Shipped,     // 已发货
    Completed    // 已完成
};

int main() {
    OrderStatus status = Shipped;

    switch (status) {
        case Pending:
            std::cout << "订单待付款\n";
            break;
        case Paid:
            std::cout << "订单已付款\n";
            break;
        case Shipped:
            std::cout << "订单已发货\n";
            break;
        case Completed:
            std::cout << "订单已完成\n";
            break;
    }
    return 0;
}

运行结果如下。

订单已发货

分析:

在这个例子中,我们先使用 enum OrderStatus { ... }; 定义了一个包含 4 个固定选项的枚举类型。接着在 main() 函数中定义了枚举变量 status,并将其初始值设为 Shipped。相比于写 int status = 2;,写成 OrderStatus status = Shipped; 一眼就能看出当前订单处于 “已发货” 状态。

当 switch (status) 进行判断时,会直接匹配到 case Shipped: 分支并输出提示文字。在程序运行过程中,枚举变量的值也是可以修改的,比如后面继续写 status = Completed;,但只能赋值为该枚举类型中已经列举出来的选项。

这里小伙伴们还要特别注意:Pending、Paid、Shipped 这些枚举值本身就是标识符,并不是字符串,因此使用时千万不要给它们加上双引号。下面这种写法就是错误的。

// 错误方式
OrderStatus status = "Shipped";

枚举的适用场景

枚举适合用来表示 “可选值数量有限,且每个选项含义明确” 的分类数据,比如星期一到星期日、春夏秋冬、上下左右方向、订单状态等。

如果一个数据本身会在很大范围内连续变化,或者需要参与加减乘除计算,就不适合定义成枚举。像年龄、价格、成绩这类数据,我们直接使用 int 或 double 即可。

C++ 枚举值的整数编号

看完上面的例子,可能有些小伙伴会好奇:“如果不通过 switch 判断,直接使用 std::cout 打印枚举变量,屏幕上会输出英文名字吗?” 答案是不会,屏幕上输出的是一个整数。

这是因为在传统 enum 中,每一个枚举值在底层都对应着一个整数编号,具体遵循以下 2 条规则:

  • 没有手动指定数值时,第 1 个枚举值默认对应整数 0,后面的枚举值依次加 1。
  • 使用 = 手动指定某个枚举值的整数后,后面未指定的枚举值会在前一项的基础上继续加 1。

示例 2:默认编号与手动指定编号

#include <iostream>

// 默认从 0 开始编号
enum Direction {
    Up,
    Down,
    Left,
    Right
};

// 手动指定部分编号
enum Season {
    Spring = 1,
    Summer,
    Autumn = 10,
    Winter
};

int main() {
    Direction dir = Left;
    std::cout << "dir = " << dir << '\n';
    std::cout << "Up = " << Up << ", Right = " << Right << '\n';

    std::cout << "Spring = " << Spring << ", Summer = " << Summer << '\n';
    std::cout << "Autumn = " << Autumn << ", Winter = " << Winter << '\n';
    return 0;
}

运行结果如下。

dir = 2
Up = 0, Right = 3
Spring = 1, Summer = 2
Autumn = 10, Winter = 11

分析:

从运行结果可以看到,直接使用 std::cout 打印枚举变量 dir 时,输出的是 Left 对应的底层整数 2,而不是 "Left" 这串文字。在 Direction 枚举中,我们没有手动指定数值,所以第 1 项 Up 默认是 0。后面的枚举值依次加 1,因此 Left 是 2,Right 是 3。

而在 Season 枚举中,我们将 Spring 手动指定为 1,紧跟在后面的 Summer 就会在 1 的基础上加 1 变成 2。接着 Autumn 又指定为 10,后面的 Winter 则在 10 的基础上加 1 变成 11。

按照这个规则,下面代码中枚举值 Blue 对应的整数是 6,小伙伴们可以思考一下为什么。

enum Color {
    Red,
    Green = 5,
    Blue
};

此外,传统 enum 的枚举值会自动发生类型转换,可以直接当成 int 整数参与输出或比较。但反过来,我们不能把一个普通整数直接赋给枚举变量。

// 正确方式
int num = Spring;

// 错误方式
Season s = 1;

在上面代码中,int num = Spring; 是合法的,枚举值 Spring 会自动转换为整数 1。而 Season s = 1; 会直接编译报错,因为编译器不允许把普通整数直接赋给 Season 类型的变量。在实际开发中,既然定义了枚举,我们就应该始终使用 Spring、Summer 这些有意义的名字,不要再把数字直接写回条件判断里。

传统 enum 的局限性

传统 enum 用起来虽然简单,但存在两个明显的短板:① 枚举值会自动转换为普通整数;② 枚举值的名字会直接暴露在外层作用域中。

拿前面的 Direction 枚举来说,定义好之后,我们在代码里可以直接写 Left,不需要指明它来自 Direction。这样写看似省事,但其实暗藏隐患。一旦同一个作用域里定义了两个不同的枚举,并且恰好包含同名的枚举值,就会直接引发命名冲突。

示例 3:不同枚举包含同名枚举值

#include <iostream>

// 水平方向
enum HorizontalDirection {
    Left,
    Right
};

// 鼠标按键
enum MouseButton {
    Left,    // 与上面的 Left 重名
    Right    // 与上面的 Right 重名
};

int main() {
    HorizontalDirection dir = Left;
    std::cout << dir << '\n';
    return 0;
}

运行结果如下。

(报错)error: redefinition of 'Left'

分析:

从报错信息中的 redefinition(重复定义)可以看出,编译器提示 Left 这个名字重复定义了。“水平方向的左” 和 “鼠标左键” 本来是两件不相干的事。可传统 enum 的枚举值会直接暴露在同一个外层作用域里,编译器读到第 2 个 Left 时发现名字已被占用,就会直接报错。

因此在实际开发中,传统 enum 多见于早期的旧项目或 C 语言兼容接口。编写现代 C++ 新代码时,我们通常会改用更安全的强类型枚举(enum class),把枚举值严格限制在各自的作用域内,从根源上避免重名冲突。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号