C语言篇:枚举类型

323 阅读5分钟

枚举类型在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;

其中Labelvarptr可以省略。

上面的代码定义了一个枚举类型enum Label;三个类型为enum Label的常量ev1ev2ev3,其值分别为056;一个类型为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 };

显然简洁不少。