在C语言中选择使用枚举(enum)还是宏定义(define)?

7
我注意到libc中大多数预定义值都是使用#define指令编写的。例如,在fseek中,whence参数需要一个int,而(据我所知)枚举类型会更好。有很多类似的例子,这些例子显然存在某种原因(除了向后兼容问题)。
因此,我想知道在哪种情况下最好使用#define,而枚举是一种更易于发现的类型安全替代方案。
作为一个实际的例子,考虑一个typedef表示输入/输出通道,就像在内核中可能发生的情况一样。GPIO可以配置为输入或输出。在这里使用enum指令是否值得?
typedef struct gpio {
    size_t port; 
    size_t bit;
    enum { DIR_INPUT, DIR_OUTPUT } direction; // or `bool`?
    bool value;
} gpio;

请注意,enum可以有三种不同的实现方式:
i) 类型:
typedef enum gpio_direction {   
   DIR_OUTPUT
   DIR_INPUT
} gpio_direction;

ii) 全局枚举

enum gpio_direction {   
   DIR_OUTPUT
   DIR_INPUT
} gpio_direction;

iii) 匿名枚举(如我的示例所示)。


1
enum 的一个问题是枚举常量的类型总是 int,不能用于超出 int 范围的值。你代码中另一个可能存在的问题是编译器(或者在理论上甚至是编译过程中)enum 大小可能会有所不同,从而影响结构体的布局。 - M.M
1
我个人认为,当值不重要时(例如在状态机或条件字段中),我总是会使用 Enum。在这种情况下,我永远不会将 ENUM 与 int 进行比较,我总是将 ENUM 与 ENUM 进行比较。在实际值很重要的情况下,我总是使用 #define,例如硬件配置寄存器值或位域。 - Schafwolle
1
你正在询问嵌入式系统中的最佳实践。libc是由桌面程序员编写的。在嵌入式系统中,有更多需要考虑的因素,其中类型显式表达更为重要。如果您不是特别关注嵌入式系统,那么这个就是重复的问题。总的来说,这个话题已经在SO上被无休止地辩论过了,您可以找到大量相关信息。 - Lundin
你后面的建议稍微好一些,因为在嵌入式设备上你不会真正使用fseek,但我仍然不同意你的选择。我想问的是:为什么在大多数情况下libc使用#defines时最好使用枚举?如果你愿意,我可以重命名这个问题。 - nowox
2
有趣(且非重复)的问题是:为什么#define经常被使用? - Hans Lub
显示剩余6条评论
2个回答

5
有很多类似的例子,这些显然都有存在的理由。
其中一个原因是:在汇编代码中,没有一种便携式的方法可以引用C语言的枚举类型。低级代码通常是(现在仍然如此),使用宏定义而不是枚举类型来指定低级代码使用的常量。
另一个原因是习惯用法if(verbosity & WAKE_THE_NEIGHBOURS),其中一个#define值用于指定位位置。
所以我想知道,在哪种情况下应该使用 #define,而枚举是一种类型安全的替代方案,更容易被发现。
在所有其他情况下,我会(现在 - 使用 #define 也算是一种传统)使用 enum,这样 if (verbosity == RED) 就会引发一个警告(如果你使用例如 gcc 和-Wall)。

那么,是否也可以使用 #ifndef _LANGUAGE_ASM \n enum {...} \n #else \n #define ... \n #endif - nowox
if (verbosity & WAKE_THE_NEIGHBOURS)是一个非常好的观点。 - nowox
如果(verbosity&WAKE_THE_NEIGHBOURS)可以与枚举一起使用...但是,您将获得有符号类型。粗略编写的#define将执行相同操作,因此没有实际区别,除非# define经过小心编写。 - Lundin
@Lundin:不,WAKE_THE_NEIGHBOURS 需要来自序列 1,2,4,8,16,.. 的值,而 enum 将分配 0,1,2,3,..。你可以通过使用 << 来解决这个问题,但这有什么意义呢? - Hans Lub
@HansLub 没有任何限制阻止您为枚举类型赋任何值。typedef enum { WAKE_THE_NEIGHBOURS = 16; .. } wake_t。这就是为什么您的回答目前看起来没有太多意义的原因。 - Lundin
1
是的,但您也会失去(相对较弱的)类型安全性:if (verbosity & RED)if (verbosity & WAKE_THE_NEIGHBOURS) 都需要将 enum 转换为 int,从而擦除有关原始 enum 类型的所有信息。 - Hans Lub

3

虽然在其他可用的情况下应该使用其他解决方案,但仍有一些情况需要使用宏。有些是历史原因,但今天仍然有一些有效的原因。

例如,您提到fseekwhence参数(即SEEK_SETSEEK_CUR和`SEEK_END)。由于历史原因,这些被指定为标准中的宏 - 因此实现者必须将它们定义为宏才能完全符合要求,一些恶意程序员可能会编写以下代码(并归咎于库造成后果):

#include <stdio.h>

#ifndef SEEK_CUR
int evil = *(int*)0;
#endif

但就我所见,他们没有理由不能这样写:

enum __WHENCE_T {
    __SEEK_CUR,
    __SEEK_END,
    __SEEK_SET
};

#define SEEK_CUR __SEEK_CUR
#define SEEK_END __SEEK_END
#define SEEK_SET __SEEK_SET

另一个历史原因是,当使用宏时,编译器可能会生成更有效的代码。那时编写的代码可能仍然存在,并且包含在当时效果更好的结构(现在仍然很好用)。
现代的原因是,如果您需要在C编译器之外使用这些值。例如,如上所述,如果您想在汇编代码(或其他语言)中使用该值。您只需要确保头文件不会扩展为某些东西(其中包含C代码),除非它是使用C编译器编译的。例如:
#define NUMBER 42

#ifdef __STDC__
extern int variable;

int function(void);
#endif

如果从汇编语言中包含,宏NUMBER仍将是可扩展的(并且会扩展为与C程序相同的内容)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接