枚举类型在C语言中存在感不高,语法也不严谨,属于可有可无的语言功能。但它在实际编码中能带来一些方便,还是有必要了解一下的。
C语言的枚举类型在定义和使用上往往让人感到困惑,因为它和结构体外观很像,但实质差别很大,不熟练时很容易出错。
为什么需要枚举类型
实际编写程序的时候,我们经常使用一系列整数来表示一些属性。比如我要写一个打开文件的函数,有一个参数名为mode,它为0
表示“只读”,1
表示“只写”,2
表示“读写”。这样的设计对机器来说既清楚又优雅,但对人来说就不那么友好了。代码一多,谁还能记得哪个数代表哪个意思,很容易写混。而且这样的代码可读性也不高,别人看起来到处都是“012”,不知所云。
于是聪明的人们想到了预处理指令,给每个数字起个名字就好了。解决方案来了:
#define READ_ONLY 0
#define WRITE_ONLY 1
#define READ_WRITE 2
这种方法被广泛使用,在各种C语言库里都能发现它的身影。但这种方法还是不够简洁,不够优雅。当属性很多的时候,写这么多#define
就很难受了。我从socket.h中截一小部分来感受一下:
/* Protocol families. */
#define PF_UNSPEC 0 /* Unspecified. */
#define PF_LOCAL 1 /* Local to host (pipes and file-domain). */
#define PF_UNIX PF_LOCAL /* POSIX name for PF_LOCAL. */
#define PF_FILE PF_LOCAL /* Another non-standard name for PF_LOCAL. */
#define PF_INET 2 /* IP protocol family. */
#define PF_AX25 3 /* Amateur Radio AX.25. */
#define PF_IPX 4 /* Novell Internet Protocol. */
#define PF_APPLETALK 5 /* Appletalk DDP. */
#define PF_NETROM 6 /* Amateur radio NetROM. */
#define PF_BRIDGE 7 /* Multiprotocol bridge. */
实际上,在这种情况下我们通常不关心这个常量的具体数值是多少,只要它是固定的并且和其他常量区分开就行。于是枚举就诞生了。使用枚举类型的另一个好处是,#define
对指令后的整个文件有效,而枚举类型有申明作用域。
定义
枚举类型的定义和结构体很像,但有一些差别。成员不必(也不能)指定类型,用逗号分隔成员,可以初始化。结尾要加分号。一个完整的定义如下:
enum Label {
ev1,
ev2 = 5,
ev3
} var, *prt;
其中Label
、var
、ptr
可以省略。
上面的代码定义了一个枚举类型enum Label
;三个类型为enum Label
的常量ev1
、ev2
、ev3
,其值分别为0、5、6;一个类型为enum Label
的变量var
和一个类型为enum Label *
的指针ptr
。
从类型上就能看出与结构体的不同,每个成员都是刚刚定义的枚举类型。实际上不管怎么定义,枚举类型都是整数。
C23之前,枚举的实际类型都是int
。C23中枚举类型变得相当复杂。
C23可以手动指定枚举的实际类型,如下:
enum Label : unsigned char { a, b };
如果没有手动指定,则会进行复杂的类型推导。简单概括就是,默认int
,但会根据初始化情况调整,保证枚举类型的取值范围能覆盖所有成员的值。
成员的值从0开始,依次递增,每次加1,如果有初始化,则可以跳跃到指定值,然后再递增。
所有枚举成员都是常量,定义以后,它们的值就不能修改了。
跟结构体一样,枚举类型的名称Label
属于另一个命名空间,所以可以在此处申明一个名为Label
的变量而不会有冲突。但是,枚举成员却不在一个单独的命名空间内,而是和变量等共处于一个命名空间。而且,枚举成员跟它的外部申明处于同一作用域,所以在这里不能申明同名的变量。
enum Label { a, b, c };
int Label; // OK
int a; // error
枚举类型不允许像结构体那样在定义前申明,如果还没有定义,下面两行申明都是不合法的:
enum Label;
typedef enum Label tp;
使用
首先用上面定义的枚举类型申明一个枚举变量。
enum Label var;
随后就可以给这个变量赋值了。
var = ev1; // var = 0
var = ev2; // var = 5
var = ev1 + ev2; // var = 0 + 5
因为枚举常量的作用域与它的类型申明相同,所以可以直接用,不必(也不能)涉及Label
。
上面的代码和注释里的代码等价。
C语言的枚举类型不严谨的地方就在于,枚举变量并不是只能使用对应的枚举常量,而是一切整数值都可以;而且也不是只有对应的枚举变量才能使用枚举常量,而是一切整数变量都可以。下面的代码都是合理合法的,连警告都不会有。
enum Label var1 = 10;
int var2 = ev3;
var2 = var1;
这就使得枚举类型的名字Label
毫无用处,不同的枚举类型之间没有界限。
C++从C++11开始就通过enum class
解决了这个问题,但C语言在这方面毫无进展。C23从C++引入了复杂的枚举类型推导,但对于真正的语言痛点丝毫没有触及。
枚举常量是“真”常量,而不是const变量。这意味着一切可以使用整数字面量的地方都可以使用枚举常量。
初始化全局或static变量:
enum {
zero,
one,
two,
};
int glob = one;
static int stat = two;
作为数组长度:
enum { len = 5 };
int arr[len] = { 0 };
作为case
标签:
enum {
zero,
one,
two,
};
switch (num) {
case zero:
case one:
case two:
;
}
回到问题
虽然C语言的枚举类型问题不少,但还是为我们带来了大大的方便。对于文章开头的3个模式的问题,使用枚举类型的解决方案如下:
enum { read_only, write_only, read_write };
显然简洁不少。