啃书系列持续更新ing,点赞和关注是对萌新博主莫大的鼓励。
系列文章:
啃书《C++ Primer Plus》之 C++ 函数指针
啃书《C++ Primer Plus》之 C++ 名称空间1
啃书《C++ Primer Plus》之 C++ 名称空间2
啃书《C++ Primer Plus》之 C++ 引用
啃书《C++ Primer Plus》之 const修饰符修饰 类对象 指针 变量 函数 引用
枚举是C++语言基础知识点,提供了一种创建符号常量的方式,这种方式定义符号常量更集中,也更清晰明了,但也有它的弊端。下面就来梳理一下有关枚举的一些概念。包括两种常见的枚举创建形式以及有关枚举局限的一些讨论,内容导图如下:
枚举类型
在介绍写法和用法之前,我们需要先来认识一下枚举需要的关键字: enum
这个长得很像 emm (博主在学习它之前一直把它当做emm来看待) 的关键字的全拼是 enumeration ,它的意思就是枚举。代表着列举某些常用量的意思,这和它批量定义常量的作用是很搭的。
在知道了枚举的关键字之后,下面我们来说说它的经典用法:枚举类型
创建枚举类型
要想使用枚举类型,必须先创建它 (这好像是句废话) 。创建它的语法规则如下:
enum 枚举类型名{常量名1,常量名2...};
当然,这里的类型名是可以省略的,那样,我们就仅仅使用枚举的规则创建了几个常量。
写成代码,举个粟子:
enum numbers{ZERO,ONE,TEN};
enum {SPRING,SUMMER};
那么,我们不禁要问:使用枚举定义常量,它的值如何定义呢?
需要介绍的是,枚举在创建时会默认隐式的为每一个常量赋值,它遵循下面两条规律:
- 第一个常量默认为0
- 非首个常量的值为前一个常量的值+1
所以,根据这两条规律,我们可以知道,当没有显式的赋值时,常量的值将会被默认的定为:0,1,2…
另外,除了隐式的赋予默认值,枚举还允许我们进行显式的赋值(不然定义常量可太麻烦了),即在定义枚举时使用 “=” 给常量赋值。这也是为什么上面的两条规律被拆开描述而不合在一起。
总结下上面说过的赋值规则,看下面的例子:
enum numbers{ZERO,TWO = 2,TEN};
/*
ZERO = 0
TWO = 2
TEN = 3
*/
enum numbers{ONE = 1,TWO = 2,TEN = 10};
/*
ONE = 1
TOW = 2
TEN = 10
*/
最后,需要强调三点:
- 枚举中的常量值必须为整型
- 枚举允许多个常量对应同一个数值
- 枚举常量的取值可以是long long
使用枚举类型
使用枚举类型集中定义了一些常量,在定义以后,我们便可以使用这些常量了。方便的是,可以直接使用这些常量进行赋值或是输出:
enum numbers{ONE = 1,TEN = 10};
int ten = TEN;
std::cout << "the value of constant ONE is : " << ONE << std::endl;
然而先不要急着使用他们进行运算,马上我们就会说到枚举类型运算的问题。
另外,从上面定义规则中需要命名可以看出,在创建枚举的时候,创建了一个新的类型,这个类型就是枚举类型。既然创建了一个类型,那我们就可以使用这个类型创建枚举变量。
例如:
enum numbers{ONE = 1,TWO = 2,TEN = 10};
numbers num = ONE;
在这个例子中,我们创建了枚举类型 numbers 的变量 num ,那么对于 num 来说,它的取值范围值得我们进行讨论。
我们想知道,既然枚举类型的常量必须是整数,那么可以将整数赋给 num 吗?
答案是:可以,但需要两个条件;
- 类型转换
- 整数的范围在枚举类型的取值范围内
例如在上面的例子中,想要赋值给 num:
num = num(2); //合法
num = num(13); //合法
num = num(16); //不合法,超出取值范围
可以看到,只要满足两个条件,可以将常数列表是存在的整型赋值给枚举变量,甚至可以将常数列表里不存在的整型赋值给枚举变量。
但仍然有一部分整数会超出取值范围,破。坏第二个条件
取值范围
那我们又不禁会问:枚举类型的取值范围是如何确定的呢?
再说取值范围的上下界前,我们需要先知道一点:位于上下界之间的整数都可以通过类型转化的方式赋值给枚举变量。
好,知道了这一点,下面来说上下界的确定规则。
- 上界确定:首先找出枚举常量中的最大值,接着找出所有比它大的2的幂中的最小值减一得到。例如,最大值为12,那么上界就应该是16-1=15
- 下界确定:首先找出枚举常量中的最小值,如果最小值小于0,则取其相反数按照类似于确定上界的方法找出负的2的幂,否则,设置为0。
不过,需要指出的是,这个规则来源于《C++ primer plus》,书中提到了这个规则主要是考虑到枚举类型的占位,且具体实现取决于编译器。所以不同的编译器有不同的范围设置,博主使用的 codeblocks 的取值范围于 int 相同,而非取决于其中的常量。
类型转换
最后在这里还需要接着说说有关类型转换的问题。
刚刚我们说到使用类型转化可以将整型变为枚举类型,前面我们还提到可以给整型赋予枚举类型的值会发生隐式的类型转化。
这样一来,我们就可以来理解前面提出的枚举常量参与运算的问题了。
枚举作为右值会隐式的转换成整型,所以枚举常量是可以参与运算的,运算的结果将是整型。但是由于整型不能直接的赋值给枚举类型,因此枚举是不可以接受运算结果的直接赋值的。
有点绕?没关系,我们看下面代码中的几个例子:
enum numbers{ZERO,ONE,TWO,THREE,FORE}
// 0 1 2 3 4
int a = ZERO;
numbers num = ZERO;
a += ONE; //合法
num += ONE; //不合法
a = TWO + THREE; //合法
num = TWO + THREE //不合法,不可以将整型直接赋给枚举类型
num = numbers(TWO + THREE); //合法,整型可以通过类型转赋值给枚举类型
枚举类
好的,看到这里,相信你已经学会了枚举类型的使用方法。
但是相信你会注意到一个BUG:枚举不能解决名称冲突的问题!!
下面的代码就包含这个问题:
enum numbers1{ONE = 1,TWO = 2};
enum numbers2{ZERO,ONE};
(需要注意的是,这里的问题只于名称有关,与常量取值无关)
这个问题是很致命的,因为你不知道在一个工程项目中由他人负责开发的代码中会不会占用某些名称用来命名枚举常量。也许我们可以通过名称空间来解决这个问题(有关名称空间的内容,可以参考系列文章:啃书《C++ Primer Plus》之 C++ 名称空间1)。
但是本文则是使用另一种作用域解决这个问题,介绍另外一种枚举的使用方法:枚举类。它保留了枚举的使用,并且解决了名称冲突的问题。
类作用域
在说枚举类之前,需要先来说一下类作用域的概念。
在C++中,作用域有很多种,全局的、局部的、名称空间内的等等。在类中定义的成员,无论是变量还是常量或是函数,他们的作用域为类。这意味着:他们的名称只在类中是可知的,而类外是不可知的。 不同类中可以包括同名的成员,访问的时候使用作用域解析符 “::” 来访问不同类种的成员。
所以,这就为解决枚举常量的名称冲突问题提供了一种解决方案。可以利用这种方式,将枚举封装在一个类中,进而通过类作用域的限制达成解决名称冲突的问题。
在类中定义的枚举类型
按照上面的思路,我们将枚举类型分别定义在不同类中,并使用作用域解析符避免名称冲突。
class A
{
public:
enum {ONE = 1,TWO = 2};
};
class B
{
public:
enum {ZERO,ONE};
};
int main()
{
int a = A::ONE; //合法
int b = B::ONE; //合法
int c = ONE; //不合法,不存在名称 ONE
}
可以看到,这样的处理很好的避免了名称冲突的问题,每一个 ONE 名称都属于其各自的类,而直接访问 ONE 的做法将会失败。
这样的操作虽然解决了问题,但是却浪费了类的定义。一个类的功能有很多,若是仅用来给枚举提供名称保护,着实有些大材小用,还不如使用名称空间来得实在。、
所以,语言开发者秉持着 既然要追求刺激,那就贯彻到底 完善并优化语言的心态,提出了一种写法,直接将枚举类型升级。将 enum 的修饰提升至类修饰 class 之前,创造一个专用于枚举的类。
枚举类
好,现在我们就要说这个枚举类型的升级版了。不过枚举类的名称在书上并没有直接出现(没有得到官方显式的命名,那博主就自己隐式的默认命名吧)。
它的写法如下:
enum class 类名{常量1,常量2...};
考虑到结构是类在C语言中的早期版本,因此此处的 class 关键字也可以换成 struct 关键字。
有关定义部分,规则同上文枚举类型部分相同,这里不再赘述,我们主要来介绍它的使用。
首先,在定义枚举变量方面,语法没有特别的变化。
接着,结合上文说过的类作用域的概念,在类外是不能访问这些常量的,因此,想要访问这些常量,使用他们或是给变量赋值,就必须使用域名解析符 “::” 。 这样,就可以解决名称冲突的问题了。
最后,需要特别强调一点:
使用枚举类定义的常量是不能进行隐式类型转化的。
使用枚举类定义的常量是不能进行隐式类型转化的。
使用枚举类定义的常量是不能进行隐式类型转化的。
(重要的事情说三遍)因此在使用这些值时需要进行显式的类型转化。
可参考如下代码:
enum class numbers1{ONE = 1,TWO = 2};
enum class numbers2{ZERO,ONE};
int main()
{
numbers1 num1 = numbers1::ONE;
numbers2 num2 = numbers2::ONE;
//这里需要显式转化,否则是非法的
int a = int(numbers1::TWO);
int b = int(numbers2::ZERO);
}
枚举的局限
最后,我们来总结一下枚举的一些局限所在。
- 首先,就是容易产生名称的问题。这点是最初的枚举类型中遇到的最致命的问题,枚举类型仅规定常量和类型名,对于作用域确实全局的。因此很容易就会出现名称冲突的问题,在实际设计程序过程中,我们会选择使用像类作用域或是名称空间这样的作用域限制来解决这个问题。
- 接着就是枚举对类型是有要求的,他只能接受整数,像 π 或者 e 这样的常数我们无法使用枚举定义。对于那些常数,只能选择传统的常量声明方法。
- 最后就是类型转化的问题,可以发现在类型间的相互转化这方面,枚举中的关系还是有些复杂的。一方面,对于一般枚举类型,可以隐式的转化为整型,但是整型不能隐式的转化为枚举型;另一方面,对于枚举类,从枚举常量到整型必须经过显示转换的步骤,否则不可使用,这也带来了些许不便。