13-2 非作用域枚举

C++ 包含许多有用的基本数据类型和复合数据类型(我们在4.1 课——基本数据类型简介和12.1 课——复合数据类型简介中介绍过)。但是,这些类型并不总是足以满足我们想要做的事情。

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

您可以将颜色存储为整数值,使用某种隐式映射(0 = 红色,1 = 绿色,2 = 蓝色):

int main()
{
    int appleColor{ 0 }; // my apple is red
    int shirtColor{ 1 }; // my shirt is green

    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;
}

虽然这样对阅读更有利一些,但程序员仍然需要推断出appleColor和shirtColor(类型为int)是用来保存颜色符号常量集中定义的值之一的(这些常量很可能是在其他地方定义的,可能是在一个单独的文件中)。

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

using Color = int; // define a type alias named Color

// The following color values should be used for a 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 }; // syntactically valid, semantically meaningless

此外,如果我们在调试器中调试这些变量中的任何一个,我们将只看到颜色的整数值(例如0),而不是符号含义(red),这可能会使我们更难判断我们的程序是否正确。

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

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

如果我们能够定义自己的自定义类型,并定义与该类型关联的一组命名值,那么我们将拥有完美的工具来优雅地解决上述挑战……

枚举 Enumerations

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

C++ 支持两种枚举:非作用域枚举(我们现在将介绍)和作用域枚举(我们将在本章后面介绍)。

因为枚举是程序定义的类型13.1 -- 程序定义(用户定义)类型的简介,所以每个枚举都需要在我们可以使用它之前完全定义(前向声明是不够的)。

无作用域枚举

无作用域枚举是通过enum关键字定义的。

枚举类型最好通过示例来讲解,所以让我们定义一个无作用域的枚举,它可以保存一些颜色值。下面我们将解释它的工作原理。

// Define a new unscoped enumeration named Color
enum Color
{
    // Here are the enumerators
    // These symbolic constants define all the possible values this type can hold
    // Each enumerator is separated by a comma, not a semicolon
    red,
    green,
    blue, // trailing comma optional but recommended
}; // the enum definition must end with a semicolon

int main()
{
    // Define a few variables of enumerated type Color
    Color apple { red };   // my apple is red
    Color shirt { green }; // my shirt is green
    Color cup { blue };    // my cup is blue

    Color socks { white }; // error: white is not an enumerator of Color
    Color hat { 2 };       // error: 2 is not an enumerator of Color

    return 0;
}

我们首先使用enum关键字告诉编译器,我们正在定义一个未定义作用域的枚举,我们将其命名为Color。

在一对花括号内,我们定义该Color类型的枚举器:redgreenblue。这些枚举器定义了该类型Color所限定的特定值。每个枚举器之间必须用逗号(而非分号)分隔——最后一个枚举器后面的逗号是可选的,但为了保持一致性,建议添加。

最常见的做法是每行定义一个枚举器,但在简单的情况下(枚举器数量较少且不需要注释),它们可以全部定义在一行中。

枚举类型Color的定义以分号结尾。至此,我们已经完整定义了枚举类型Color!

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

枚举器隐式地是 constexpr。

提醒:
快速回顾一下术语:

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

命名枚举和枚举者

按照惯例,枚举类型的名称以大写字母开头(所有程序定义的类型也是如此)。

警告:
枚举类型不必命名,但在现代 C++ 中应该避免使用未命名的枚举类型。

枚举器必须命名。遗憾的是,枚举器名称没有通用的命名规则。常见的选择包括:以小写字母开头(例如 red)、以大写字母开头(Red)、全部大写(RED)、全部大写加前缀(COLOR_RED),或者以“k”为前缀并首字母大写(kColorRed)。

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

最佳实践:
枚举类型名称以大写字母开头,枚举者名称以小写字母开头。

枚举类型是不同的类型

您创建的每个枚举类型都被视为一个独特类型distinct type,这意味着编译器可以将其与其他类型区分开来(与类型定义typedefs 或类型别名type aliases不同,它们被认为与它们所别名的类型没有区别)。

由于枚举类型是不同的,因此定义为一个枚举类型一部分的枚举器不能用于另一个枚举类型的对象:

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

enum Color
{
    black,
    red,
    blue,
};

int main()
{
    Pet myPet { black }; // compile error: black is not an enumerator of Pet
    Color shirt { pig }; // compile error: pig is not an enumerator of Color

    return 0;
}

image

image

你大概本来就不想要一件猪图案的衬衫。

枚举的运用

由于枚举类型具有描述性,因此有助于增强代码文档的清晰度和可读性。枚举类型最适合用于处理少量相关的常量,并且对象一次只需要保存其中一个值的情况。

常见的枚举包括一周中的日子、基本方位和一副扑克牌中的花色:

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; // success
}

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

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)
{
    // do something
}
else
{
    // print error message
}

枚举类型在游戏中也能派上用场,用来识别不同类型的物品、怪物或地形。基本上,任何由一小群相关对象组成的事物都可以用枚举类型来表示。

例如:

enum ItemType
{
	sword,
	torch,
	potion,
};

int main()
{
	ItemType holding{ torch };

	return 0;
}

当用户需要在两个或多个选项之间进行选择时,枚举类型也可以用作有用的函数参数:

enum SortOrder
{
    alphabetical,
    alphabeticalReverse,
    numerical,
};

void sortData(SortOrder order)
{
    switch (order)
    {
        case alphabetical:
            // sort data in forwards alphabetical order
            break;
        case alphabeticalReverse:
            // sort data in backwards alphabetical order
            break;
        case numerical:
            // sort data numerically
            break;
    }
}

许多语言使用枚举来定义布尔值——毕竟,布尔值本质上就是一个带有两个枚举器的枚举:truefalse !然而,在 C++ 中,truefalse被定义为关键字而不是枚举器。

因为枚举值很小,复制成本也很低,所以按值传递(和返回)枚举值是没问题的。

在O.1 课——使用 std::bitset 进行位标志和位操作中,我们讨论了位标志。枚举也可用于定义一组相关的位标志位置,以便与以下代码一起使用std::bitset:

#include <bitset>
#include <iostream>

namespace Flags
{
    enum State
    {
        isHungry,
        isSad,
        isMad,
        isHappy,
        isLaughing,
        isAsleep,
        isDead,
        isCrying,
    };
}

int main()
{
    std::bitset<8> me{};
    me.set(Flags::isHappy);
    me.set(Flags::isLaughing);

    std::cout << std::boolalpha; // print bool as true/false

    // Query a few states (we use the any() function to see if any bits remain set)
    std::cout << "I am happy? " << me.test(Flags::isHappy) << '\n';
    std::cout << "I am laughing? " << me.test(Flags::isLaughing) << '\n';

    return 0;
}

image

如果您想知道如何在需要整数值的地方使用枚举器,非作用域枚举器会隐式地转换为整数值。我们将在下一课(13.3——非作用域枚举器的整数转换)中进一步探讨这一点。

无作用域枚举的范围

无作用域枚举之所以这样命名,是因为它们将其枚举器名称放在与枚举定义本身相同的作用域中(而不是像命名空间那样创建一个新的作用域区域)。

例如,给定以下程序:

enum Color // this enum is defined in the global namespace
{
    red, // so red is put into the global namespace
    green,
    blue,
};

int main()
{
    Color apple { red }; // my apple is red

    return 0;
}

枚举Color定义在全局作用域中。因此,所有枚举名称(redgreenblue)也都位于全局作用域中。这会污染全局作用域,并显著增加命名冲突的概率。

由此产生的一个后果是,枚举器名称不能在同一作用域内的多个枚举中使用:

enum Color
{
    red,
    green,
    blue, // blue is put into the global namespace
};

enum Feeling
{
    happy,
    tired,
    blue, // error: naming collision with the above blue
};

int main()
{
    Color apple { red }; // my apple is red
    Feeling me { happy }; // I'm happy right now (even though my program doesn't compile)

    return 0;
}

image

在上面的例子中,两个非作用域枚举(ColorFeeling)都将同名枚举器放入blue了全局作用域。这会导致命名冲突,进而引发编译错误。

无作用域枚举也为其枚举器提供了一个命名作用域区域(很像命名空间为其中声明的名称提供命名作用域区域)。这意味着我们可以按如下方式访问无作用域枚举的枚举器:

enum Color
{
    red,
    green,
    blue, // blue is put into the global namespace
};

int main()
{
    Color apple { red }; // okay, accessing enumerator from global namespace
    Color raspberry { Color::red }; // also okay, accessing enumerator from scope of Color

    return 0;
}

大多数情况下,无需使用作用域解析运算符即可访问无作用域枚举器。

避免枚举器命名冲突

防止无作用域枚举器命名冲突的方法有很多种。

一种方法是在每个枚举器前加上枚举本身的名称:

enum Color
{
    color_red,
    color_blue,
    color_green,
};

enum Feeling
{
    feeling_happy,
    feeling_tired,
    feeling_blue, // no longer has a naming collision with color_blue
};

int main()
{
    Color paint { color_blue };
    Feeling me { feeling_blue };

    return 0;
}

这仍然会污染命名空间,但通过使名称更长、更唯一,减少了命名冲突的可能性。

更好的选择是将枚举类型放在提供独立作用域的区域内,例如命名空间:

namespace Color
{
    // The names Color, red, blue, and green are defined inside namespace Color
    enum Color
    {
        red,
        green,
        blue,
    };
}

namespace Feeling
{
    enum Feeling
    {
        happy,
        tired,
        blue, // Feeling::blue doesn't collide with Color::blue
    };
}

int main()
{
    Color::Color paint{ Color::blue };
    Feeling::Feeling me{ Feeling::blue };

    return 0;
}

这意味着我们现在必须将枚举名称和枚举器名称加上作用域区域的名称作为前缀。

适合高级读者:
类也提供了一个作用域,通常将与类相关的枚举类型放在该类的作用域内。我们将在第15.3 课——嵌套类型(成员类型)中讨论这一点。

另一种相关方法是使用作用域枚举(它定义了自己的作用域)。我们稍后会讨论作用域枚举(13.6 -- 作用域枚举(枚举类))。

最佳实践:
最好将枚举放在命名作用域区域(例如命名空间或类)内,这样枚举器就不会污染全局命名空间。

或者,如果枚举仅在单个函数体内使用,则应在该函数内部定义枚举。这样可以将枚举及其枚举器的作用域限制在该函数内。此类枚举的枚举器将覆盖全局作用域中定义的同名枚举器。

与枚举器比较

我们可以使用相等运算符(operator==&& operator!=)来测试枚举是否具有特定枚举值。

#include <iostream>

enum Color
{
    red,
    green,
    blue,
};

int main()
{
    Color shirt{ blue };

    if (shirt == blue) // if the shirt is blue
        std::cout << "Your shirt is blue!";
    else
        std::cout << "Your shirt is not blue!";

    return 0;
}

在上面的例子中,我们使用 if 语句来测试是否shirt等于枚举值blue。这使我们能够根据枚举中持有的枚举值来控制程序的行为。

我们将在下一节课中更多地运用它。

测验时间

问题 1

定义一个名为 MonsterType 的无作用域枚举类型,用于在以下怪物种族中进行选择:兽人orc 、哥布林goblin、巨魔troll、食人魔ogre和骷髅skeleton。

显示解决方案

enum MonsterType
{
    orc,
    goblin,
    troll,
    ogre,
    skeleton,
};

问题 2

将 MonsterType 枚举放在一个命名空间中。然后,创建一个 main() 函数并实例化一个巨魔角色。程序应该可以编译通过。

显示解决方案

namespace Monster
{
    enum MonsterType
    {
        orc,
        goblin,
        troll,
        ogre,
        skeleton,
    };
}

int main()
{
    // We use [[maybe_unused]] to avoid warnings about unused variables
    // You could also output the monster instead
    [[maybe_unused]] Monster::MonsterType monster{ Monster::troll };

    return 0;
}

因为MonsterType 是一个无作用域枚举,它的枚举器(例如 troll)被放置在枚举本身的命名空间中(在本例中为 namespace Monster)。因此,我们可以这样Monster::troll访问它 troll

由于无作用域枚举也会将其枚举器放入自己的命名空间中,因此 troll 也可以通过 Monster::MonsterType::troll 访问。然而,这样做并没有什么实际好处。

posted @ 2025-12-20 16:04  游翔  阅读(12)  评论(0)    收藏  举报