非限定枚举

C++ 提供了许多有用的基本数据类型(4.1 课《基本数据类型简介》)和复合数据类型(12.1 课《复合数据类型简介》),但这些类型并不总是能满足我们的需求。

例如,假设你正在编写一个程序,需要记录苹果是红色、黄色还是绿色,或者衬衫的颜色(从预设颜色列表中选择)。如果只有基本类型可用,你会怎么做?

你可能会用整数值存储颜色,通过某种隐式映射(0 = 红色,1 = 绿色,2 = 蓝色):

int main() {
    int appleColor{ 0 }; // 我的苹果是红色的
    int shirtColor{ 1 }; // 我的衬衫是绿色的

    return 0;
}

这并不直观,我们已经讨论过为什么“魔法数字”不好(5.2 课《字面量》)。我们可以通过使用符号常量来消除魔法数字:

constexpr int red{ 0 };
constexpr int green{ 1 };
constexpr int blue{ 2 };

int main() {
    int appleColor{ red };
    int shirtColor{ green };

    return 0;
}

虽然这在阅读上稍好一些,但程序员仍然需要推断出 appleColorshirtColor(它们是 int 类型)被设计为存储颜色符号常量集合中定义的值之一(这些常量可能在其他地方定义,可能在单独的文件中)。

我们可以通过使用类型别名让这个程序更清晰一些:

using Color = int; // 定义一个名为 Color 的类型别名

// 下列颜色值应用于 Color
constexpr Color red{ 0 };
constexpr Color green{ 1 };
constexpr Color blue{ 2 };

int main() {
    Color appleColor{ red };
    Color shirtColor{ green };

    return 0;
}

我们离目标更近了一步。阅读这段代码的人仍然需要理解这些颜色符号常量是与 Color 类型的变量一起使用的,但至少现在类型有了一个独特的名称,搜索 Color 的人能够找到相关的符号常量集合。

然而,由于 Color 只是 int 的别名,我们仍然存在正确使用这些颜色符号常量的问题没有得到解决。我们仍然可以像这样操作:

Color eyeColor{ 8 }; // 语法上有效,语义上无意义

此外,如果我们用调试器调试这些变量,我们只会看到颜色的整数值(例如 0),而不是符号意义(红色),这可能会使判断程序是否正确更加困难。

幸运的是,我们可以做得更好。

bool 类型为例,bool 类型特别有趣,因为它只有两个定义值:truefalse。我们可以直接使用 truefalse(作为字面量),也可以实例化一个 bool 对象并让它持有这两个值之一。此外,编译器能够区分 bool 与其他类型。这意味着我们可以重载函数,并自定义当传递 bool 值时这些函数的行为。

如果我们能够定义自己的自定义类型,并定义与该类型关联的命名值集合,那么我们就有了完美解决上述挑战的工具……

枚举类型

枚举类型(也称为枚举或 enum)是一种复合数据类型,其值被限制为一组命名的符号常量(称为枚举器)。

C++ 支持两种枚举类型:非限定枚举(我们现在将介绍)和限定枚举(我们将在本章后面介绍)。

由于枚举类型是程序定义类型(13.1 课《程序定义类型简介》),因此每个枚举类型都需要在使用之前完整定义(前向声明是不够的)。

非限定枚举

非限定枚举通过 enum 关键字定义。通过示例学习枚举类型最为直观,因此让我们定义一个非限定枚举,它可以存储一些颜色值。我们将在下面解释它是如何工作的。

// 定义一个名为 Color 的新非限定枚举
enum Color {
    // 这里是枚举器
    // 这些符号常量定义了该类型可以持有的所有可能值
    // 每个枚举器用逗号分隔,而不是分号
    // 最后一个枚举器后面的尾随逗号是可选的,但推荐使用
    red,
    green,
    blue, // 推荐使用尾随逗号
}; // 枚举定义必须以分号结尾

main() 中,我们实例化了三个 Color 类型的变量:apple 初始化为红色,shirt 初始化为绿色,cup 初始化为蓝色。为每个对象分配了内存。请注意,枚举类型的初始化器必须是该类型定义的枚举器之一。变量 sockshat 会导致编译错误,因为初始化器 white2 不是 Color 的枚举器。

枚举器隐式为 constexpr

术语回顾

  • 枚举类型:程序定义的类型本身(例如 Color)。
  • 枚举器:属于枚举类型的特定命名值(例如 red)。

命名枚举类型和枚举器

按照惯例,枚举类型的名称以大写字母开头(就像所有程序定义类型一样)。

警告
枚举类型可以不命名,但在现代 C++ 中应避免使用无名枚举。

枚举器必须有名称。不幸的是,枚举器名称没有通用的命名约定。常见的选择包括以小写字母开头(例如 red)、以大写字母开头(Red)、全大写(RED)、全大写带前缀(COLOR_RED)或以“k”开头并使用驼峰命名法(kColorRed)。

现代 C++ 指南通常建议避免全大写命名约定,因为全大写通常用于预处理器宏,可能会产生冲突。我们还建议避免以大写字母开头的命名约定,因为以大写字母开头的名称通常保留给程序定义类型。

最佳实践
枚举类型的名称以大写字母开头。枚举器的名称以小写字母开头。

枚举类型是独立类型

你创建的每个枚举类型都被视为一个独立的类型,这意味着编译器能够将其与其他类型区分开来(与 typedef 或类型别名不同,它们被视为与其别名类型不可区分)。

因为枚举类型是独立的,所以一个枚举类型中定义的枚举器不能与其他枚举类型的对象一起使用:

enum Pet {
    cat,
    dog,
    pig,
    whale,
};

enum Color {
    black,
    red,
    blue,
};

int main() {
    Pet myPet{ black }; // 编译错误:black 不是 Pet 的枚举器
    Color shirt{ pig }; // 编译错误:pig 不是 Color 的枚举器

    return 0;
}

你可能本来就不想要一件“猪”衬衫。

使用枚举类型

由于枚举器具有描述性,因此它们对于增强代码文档和可读性非常有用。当你有一组相关常量,并且对象一次只需要持有这些值中的一个时,枚举类型最适合使用。

常见的枚举类型包括一周的天数、基本方向和一副牌的花色:

enum DaysOfWeek {
    sunday,
    monday,
    tuesday,
    wednesday,
    thursday,
    friday,
    saturday,
};

enum CardinalDirections {
    north,
    east,
    south,
    west,
};

enum CardSuits {
    clubs,
    diamonds,
    hearts,
    spades,
};

有时,函数会向调用者返回一个状态码,以指示函数是成功执行还是遇到了错误。传统上,使用小的负整数来表示不同的可能错误码。例如:

int readFileContents() {
    if (!openFile())
        return -1;
    if (!readFile())
        return -2;
    if (!parseFile())
        return -3;

    return 0; // 成功
}

然而,使用这样的魔法数字并不具有描述性。更好的方法是使用枚举类型:

enum FileReadResult {
    readResultSuccess,
    readResultErrorFileOpen,
    readResultErrorFileRead,
    readResultErrorFileParse,
};

FileReadResult readFileContents() {
    if (!openFile())
        return readResultErrorFileOpen;
    if (!readFile())
        return readResultErrorFileRead;
    if (!parseFile())
        return readResultErrorFileParse;

    return readResultSuccess;
}

然后,调用者可以将函数的返回值与适当的枚举器进行比较,这比测试返回结果是否为特定整数值更容易理解。

if (readFileContents() == readResultSuccess) {
    // 做一些事情
} else {
    // 打印错误消息
}

枚举

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"