第八章 结构、联合与枚举
8.4 枚举
枚举(enumeration)类型用于存放用户指定的一组整数值(§ iso.7.2)。枚举类型的每种取值各自对应一个名字,我们把这些值叫做枚举值(enumerator)。例如:
enum class Color{red, green, blue};
上述代码定义了一个名为Color的枚举类型,它的枚举值是red、green和blue。“一个枚举类型”简称“一个enum”。
枚举类型分为两种:
- enum class,它的枚举值名字(比如red)位于enum的局部作用域内,枚举值不会隐式地转换成其他类型。
- “普通的enum”,它的枚举值名字与枚举类型本身位于同一个作用域中,枚举值隐式地转换成整数。
通常情况下,建议程序员使用enum class,它很少会产生我们意想不到的结果。
8.4.1 enum class
enum class是一种限定了作用域的强类型枚举,例如:
enum class Traffic_light{red, yellow, green};
enum class Warning{green, yellow, orange, red}; //火警等级
Warning a1 = 7; //错误:不存在int向Warning的类型转换
int a2 = green; //错误:green位于它的作用域之外
int a3 = Warning::green; //错误:不存在Warning向int的类型转换
Warning a4 = Warning::green; //OK
void f(Traffic_light x)
{
if(x == 9){/*...*/} //错误:9不是一个Traffic_light
if(x == red){/*...*/} //错误:当前作用域中没有red
if(x == Warning::red){/*...*/} //错误:x不是一个Warning
if(x == Traffic_light::red){/*...*/} //OK
}
两个enum的枚举值不会互相冲突,它们位于各自enum class的作用域中。
枚举常用一些整数类型表示,每个枚举值是一个整数。我们把用于表示某个枚举的类型称为它的基础类型(underlying type)。基础类型必须是一种带符号或无符号的整数类型(见6.2.4节),默认是int。我们可以显式地指定:
enum class Warning:int{green, yellow, orange, red}; //sizeof(Warning)==sizeof (int)
如果你认为上述定义太浪费空间,可以用char代替int:
enum class Warning : char{green, yellow, orange, red}; //sizeof(Warning)==1
默认情况下,枚举值从0开始,依次递增。因此,我们可以得到:
static_cast<int>(Warning::green)==0
static_cast<int>(Warning::yellow)==1
static_cast<int>(Warning::orange)==2
static_cast<int>(Warning::red)==3
在有了Warning之后,用Warning变量代替普通的int变量使得用户和编译器都能更好地理解该变量的真正用途。例如:
void f(Warning key)
{
switch(key){
case Warning::green:
//...相应的操作...
break;
case Warning::orange:
//...相应的操作...
break;
case Warning::red:
//...相应的操作...
break;
}
}
用户很容易发现程序缺少了对yellow的处理,编译器也能发现这一点。因为Warning的四个值只处理了三个,所以编译器会发出一条警告信息。
我们可以用整型(见6.2.1节)常量表达式(见10.4节)初始化枚举值,例如:
enum class Printer_flags{
acknowledge = 1,
paper_empty = 2,
busy = 4,
out_of_black = 8,
out_of_color = 16,
//
};
我们特意为Printer_flags选取了一些特殊的枚举值,以便能用位运算符把它们组合在一起。enum属于用户自定义的类型,因此我们可以为它定义 | 和 & 运算符(见3.2.1.1节和第18章)。例如:
constexpr Printer_flags operator|(Printer_flags a, Printer_flags b)
{
return static_cast<Printer_flags>(static_cast<int>(a)|static_cast<int>(b));
}
constexpr Printer_flags operator&(Printer_flags a, Printer_flags b)
{
return static_cast<Printer_flags>(static_cast<int>(a)&static_cast<int>(b));
}
因为enum class不支持隐式类型转换,所以我们必须在这里使用显式的类型转换。在为Printer_flags定义了 | 和 & 之后:
void try_to_print(Printer_flags x)
{
if(x&Printer_flags::acknowledge){
//...
}
else if(x&Printer_flags::busy){
//...
}
else if(x&(Printer_flags::out_of_black | Printer_flags::out_of_color)){
//缺墨:黑白或彩色
//...
}
//...
}
我把operator|()和operator&()定义成了constexpr函数(见10.4节和12.1.6节)。这样就能把它们用于常量表达式了。例如:
void g(Printer_flags x)
{
switch(x){
case Printer_flags::acknowledge:
//...
break;
case Printer_flags::busy:
//...
break;
case Printer_flags::out_of_black:
//...
break;
case Printer_flags::out_of_color:
//...
break;
case Printer_flags::out_of_black & Printer_flags::out_of_color:
//缺墨:黑白或彩色
//...
break;
}
//...
}
C++允许先声明一个enum class,稍后再给出它的定义(见6.3节)。例如:
enum class Color_code : char; //声明
void foobar(Color_code* p); //使用声明
//...
enum class Color_code : char{ //定义
red, yellow, green, blue
};
一个整数类型的值可以显式地转换成枚举类型。如果这个值属于枚举的基础类型的取值范围,则转换是有效的;否则,如果超出了合理的表示范围,则转换的结果是未定义的。例如:
enum class Flag : char{x = 1, y = 2, z = 4, e = 8};
Flag f0{}; //f0的默认值是0
Flag f1 = 5; //类型错误:5不属于Flag类型
Flag f2 = Flag{5}; //错误:不允许窄化转换成enum class类型
Flag f3 = static_cast<Flag>(5); //“不近人情”的转换
Flag f4 = static_cast<Flag>(999); //错误:999不是一个char类型的值(也许根本捕获不到)
最后一条赋值语句很好地展示了为什么不允许从整数到枚举类型地隐式转换,因为绝大多数整数值根本不在某一枚举类型的合理表示范围之内。
每个枚举值对应一个整数,我们可以显式地把这个整数抽取出来。例如:
int i = static_cast<int>(Flag::y); //i的值变为2
char c = static_cast<char>(Flag::e); //c的值变为8
我们在这里提到的枚举的取值概念与Pascal语言家族中的枚举概念不同。然而,像Printer_flags这样的位操作枚举类型在C和C++的历史中已经存在很长时间了,它要求枚举值之外的其他值也应该是定义良好的。
对enum class执行sizeof的结果是对其基础类型执行sizeof的结果。如果没有显式指定基础类型,则枚举类型的尺寸等于sizeof(int)。
8.4.2 普通的enum
“普通的enum”是指C++在提出enum class之前提供的枚举类型,在很多C和C++98的代码中都存在普通的enum。普通的enum的枚举值位于enum本身所在的作用域中,它们隐式地转换成某些整数类型的值。我们把8.4.1节的例子去掉class关键字后变成下面的形式:
enum Traffic_light{red, yellow, green};
enum Warning{green, yellow, orange, red}; //火警等级
//错误:yellow被重复定义(取值相同)
//错误:red被重复定义(取值不同)
Warning a1 = 7; //错误:不存在int向Warning的类型转换
int a2 = green; //OK:green位于其作用域中,隐式地转换成int类型
int a3 = Warning::green; //OK:Warning向int的类型转换
Warning a4 = Warning::green; //OK
void f(Traffic_light x)
{
if(x == 9){/*...*/} //OK(但是Traffic_light并不包含枚举值9)
if(x == red){/*...*/} //错误:作用域中有两个red
if(x == Warning::red){/*...*/} //OK(哎哟!)
if(x == Traffic_light::red){/*...*/} //OK
}
我们在同一个作用域的两个普通枚举中都定义了red,从而“很幸运地”避免了一个难以发现的错误。我们可以人为地消除枚举值的二义性,以实现对于普通enum的“清理”(在小规模程序中很容易做到,但是在规模较大的程序中就很难做到了):
enum Traffic_light{tl_red, tl_yellow, tl_green};
enum Warning{green, yellow, orange, red}; //火警等级
void f(Traffic_light x)
{
if(x == red){/*...*/} //OK(哎哟!)
if(x == Warning::red){/*...*/} //OK(哎哟!)
if(x == Traffic_light::red){/*...*/} //错误:red不是一个Traffic_light类型的值
}
从编译器的角度来看,x==red是合法的,但它几乎肯定是一个程序缺陷。把名字注入外层作用域(当使用enum时会发生这种情况,但是使用enum class和class不会)的行为称为名字空间污染(namespace pollution),在规模较大的程序中应该尽量避免这样做(第14章)。
你可以为普通的枚举指定基础类型,就像你对enum class所做的一样。此时,允许类声明枚举类型,稍后再给出它的定义。例如:
enum Traffic_light : char{tl_red, tl_yellow, tl_green}; //基础类型是char
enum Color_code : char; //声明
void foobar(Color_code* p); //使用声明
//...
enum Color_code : char{red, yellow, green, blue}; //定义
如果没有指定枚举的基础类型,则不能把它的声明和定义分开。此时,枚举的基础类型需要通过一个相对复杂的算法推算出来:如果所有枚举值都是非负值,则该枚举类型的范围是[0 : 2k - 1],其中2k是2的最小整数次幂并且保证所有枚举值都位于该范围内。如果存在负值,则范围是[-2k : 2k - 1]。该算法定义了能够存放所有枚举值的最小位域,其中,枚举值是用传统的二进制补码表示的。例如:
enum E1{dark, light}; //范围0:1
enum E2{a = 3, b = 9}; //范围0:15
enum E3{min=-10, max=1000000}; //范围 -1048576:1048575
整数到普通enum的显式类型转换规则与转换为enum class的规则一样。稍有的一点区别是,当没有显式地指定基础类型时,除非该值位于枚举类型的范围之内,否则转换的结果是未定义的。例如:
enum Flag{x=1, y=2, z=4, e=8}; //范围 0:15
Flag f0(); //f0的默认值是0
Flag f1 = 5; //类型错误:5不是一个Flag
Flag f2 = Flag{5}; //错误:不存在int向Flag的显式类型转换
Flag f2 = static_cast<Flag>(5); //OK:5在Flag的取值范围之内
Flag f3 = static_cast<Flag>(z|e); //OK:12在Flag的取值范围之内
Flag f4 = static_cast<Flag>(99); //未定义的:99不在Flag的取值范围之内
因为普通的enum和其基础类型之间存在隐式类型转换,所以我们不需要为它专门定义运算符|:z和e会自动转换成int,因此z|e能够正常求值。对枚举类型求sizeof的结果等价于对其基础类型求sizeof的结果。如果没有显式地指定基础类型,则除非其中的枚举值不能表示成int或者unsigned int,否则该枚举将取某种既能保存其范围内的值又不超过sizeof(int)的整数类型作为其类型。例如在sizeof(int)==4的机器上,sizeof(Flags)可能是1,也可能是4,但不会是8。
8.4.3 未命名的enum
一个普通的enum可以是未命名的,例如:
enum {arrow_up = 1, arrow_down, arrow_sideways};
如果我们需要的只是一组整型常量,而不是用于声明变量的类型,则可以使用未命名的enum。