【问题标题】:Is it reasonable to use enums instead of #defines for compile-time constants in C?在C中使用枚举而不是#defines作为编译时常量是否合理?
【发布时间】:2016-01-18 20:13:10
【问题描述】:

在使用 C++ 工作一段时间后,我将回到一些 C 开发领域。我已经意识到,在不必要的时候应该避免使用宏,以便让编译器在编译时为你做更多的工作。因此,对于常量值,在 C++ 中我会使用静态 const 变量或 C++11 枚举类来获得良好的作用域。在 C 中,静态常量并不是真正的编译时常量,枚举的行为可能(?或可能不是?)略有不同。

那么,对于常量使用枚举而不是#defines 是否合理?

作为参考,这里有一个excellent list of pros and cons of enums, #defines and static consts in C++

【问题讨论】:

  • 正如你所说,C 和 C++ 在这个话题上存在着深刻的差异。因此,可能没有一个对两种语言都有效的答案。
  • @NikolaiRuhe 我相信这个问题只问如何在 C 中做到这一点(见标题)。 C++ 部分仅供参考。
  • 该标签应该被删除,然后。
  • @NikolaiRuhe:好的,为了清楚起见将其删除。
  • @Lundin:我在考虑更多关于 SFINAE 类型的事情,以及让编译器运行我的 constexpr 函数、类型检查等的可能性。

标签: c enums constants c-preprocessor


【解决方案1】:

如今,在 C++ 中,没有真正的充分理由使用 #define 编译时常量。另一方面,有充分的理由使用enums 或enum classes。首先也是最重要的 - 它们在调试期间更具可读性。

C 中,您可能希望显式选择底层类型,而enums 则不可能。这可能是使用defines 或consts 的原因。但是应该强烈推荐枚举。

运行时开销不是问题 - 在现代编译器中,生成的机器代码不会有任何差异(只要sizeof(the_enum)=sizeof(the_type_used_by_define_based_values))。

【讨论】:

    【解决方案2】:

    我从事嵌入式系统工作已有十几年,主要使用 C。我的 cmets 专门针对这个领域。可以通过三种方法创建对这些类型的应用程序具有特定含义的常量。

    1) #define:宏在代码呈现给 C 编译器之前由 C 预处理器解析。当您查看处理器供应商提供的头文件时,它们通常有数以千计的宏定义对处理器寄存器的访问。您在代码中调用它们的子集,它们成为 C 源代码中的内存访问。其余的会消失,不会呈现给 C 编译器。

    定义为宏的值在 C 中变为文字。因此,它们不会导致任何数据存储。没有与定义关联的数据存储位置。

    宏可用于条件编译。如果要根据功能配置剥离代码,则必须使用宏定义。例如:

    #if HEARTBEAT_TIMER_MS > 0
        StartHeartBeatTimer(HEARTBEAT_TIMER_MS);
    #endif
    

    2) 枚举:与宏定义一样,枚举不会导致数据存储。它们变成文字。与宏定义不同,它们不会被预处理器剥离。它们是 C 语言结构,将出现在预处理的源代码中。它们不能用于通过条件编译剥离代码。无法在编译时或运行时测试它们是否存在。值只能作为文字参与运行时条件。

    未引用的枚举在编译后的代码中根本不存在。另一方面,如果枚举值未在 switch 语句中处理,编译器可能会提供警告。如果常量的目的是产生一个必须在逻辑上处理的值,那么只有枚举才能提供使用 switch 语句所带来的安全程度。

    枚举还具有自动增量功能,因此如果常量的目的是用作数组的常量索引,那么我总是会使用枚举来避免未使用的插槽。事实上,枚举本身可以产生一个常量,表示可以在数组声明中使用的多个项目。

    由于枚举是 C 语言结构,它们肯定会在编译器时进行评估。例如:

    #define CONFIG_BIT_POS 0
    #define CONFIG_BIT_MASK (1 << CONFIG_BIT_POS)
    

    CONFIG_BIT_MASK 是 (1

    enum {
        CONFIG_BIT_POS = 0,
        CONFIG_BIT_MASK = (1 << CONFIG_BIT_POS)
    };
    

    在这种情况下,CONFIG_BIT_MASK 被评估并成为文字值 1。

    最后,我要补充一点,宏定义可以组合生成其他代码符号,但不能用于创建其他宏定义。这意味着如果必须派生常量名称,那么它只能是由宏符号或宏扩展的组合创建的枚举,例如使用列表宏(X 宏)。

    3) const:这是一种使数据值只读的 C 语言结构。在嵌入式应用程序中,当应用于静态或全局数据时,这具有重要作用:它将数据从 RAM 移动到 ROM(通常是闪存)。 (它对局部变量或自动变量没有这种影响,因为它们是在运行时在堆栈或寄存器中创建的。)C 编译器可以优化它,但当然可以防止这种情况发生,所以除了这个警告之外, const data 实际上需要在运行时增加只读存储器中的存储。这意味着它具有类型,该类型定义了在已知位置的存储。它可以是 sizeof() 的参数。它可以在运行时被外部应用程序或调试器读取。

    这些 cmets 以嵌入式应用程序为目标。显然,对于桌面应用程序,一切都在 RAM 中,而其中大部分并不真正适用。在这种情况下,const 更有意义。

    【讨论】:

      【解决方案3】:

      我会坚持将这些功能用于他们的目的。

      一个符号参数,在一组备选方案中取一个离散值,应该表示为一个枚举成员。

      数值参数,例如数组大小或数值容差,应表示为 const 变量。不幸的是,C 没有适当的构造来声明编译时常量(就像 Pascal 那样),我倾向于说定义的符号同样可以接受。我现在甚至非正统地选择使用与其他标识符相同的大小写方案定义的符号。

      具有显式赋值的枚举,例如二进制掩码,更加有趣。冒着看起来很挑剔的风险,我会考虑使用声明的常量,比如

      #define IdleMask 1
      #define WaitingMask 2
      #define BusyMask (IdleMask | WaitingMask)
      enum Modes { Idle= IdleMask, Waiting= WaitingMask, Busy= BusyMask };
      

      也就是说,当您看到编译器如何轻松处理每天收到的大量代码时,我不会太在意简化编译器的任务。

      【讨论】:

      • BusyMask 是等待发生的意外。始终用括号括住非平凡的预处理器常量。
      • 另外,我会使用IdleMask 0x1 &lt;&lt; 0WaitingMask 0x1 &lt;&lt; 1(或至少0x010x02)来强调这些的按位读取。
      • @user694733: 不,常量BusyMask 不打算在其他任何地方使用;只有Busy可以。
      • 我认为在使用后#undef 这样的临时宏是一种很好的做法。特别是如果这是在标题中。然而,这只会进一步膨胀这个冗长的初始化,所以不知何故我看不到这正在流行......
      【解决方案4】:

      对于常量使用枚举而不是#define 是否合理?

      如果你喜欢。枚举的行为类似于整数。

      但我仍然更喜欢常量,而不是枚举和宏。常量提供类型安全,它们可以是任何类型。枚举只能是整数,宏不尊重类型安全。

      例如:

      const int MY_CONSTANT = 7;
      

      而不是

      #define MY_CONSTANT 7
      

      enum
      {
        MY_CONSTANT = 7
      };
      

      顺便说一句,我的回答与 C++ 有关。我不确定它是否适用于 C。

      【讨论】:

      • 能否请您提供一些理由为什么您更喜欢常量。这可能不会立即显而易见。
      • 在 C 中,某些结构需要常量(开关标签、静态数组等),并且 const 限定的变量不被视为常量。所以你的答案只适用于 C++。
      【解决方案5】:

      使用enum { FOO=34 }; 优于#define FOO 34 的优势在于宏是经过预处理的,因此原则上编译器不会真正看到它们(实际上,编译器确实看到它们;最近的@ 987654321@ 有一个复杂的基础设施,可以从宏扩展中给出一些内部抽象语法树。

      特别是,调试器从enum { FOO=34 }; 比从#define FOO 34 更可能知道FOO(但同样,在实践中并非总是如此;有时,调试器足够聪明,能够展开宏...)。

      因此,比起#define FOO 34,我更喜欢enum { FOO=34 };

      而且还有打字优势。与使用 bool isblack; 相比,使用 enum color_en { WHITE, BLACK }; enum color_en color; 可以从编译器获得更多警告

      顺便说一句,static const int FOO=37; 通常为调试器所知,但编译器可能会对其进行优化(因此不使用内存位置;它可能只是machine code 中某些指令中的立即操作数)。

      【讨论】:

      • 当然,调试器更有可能知道const,所以如果适用的话,这通常是最好的选择。
      【解决方案6】:

      const int MY_CONSTANT = 7; 将占用存储空间;枚举或#define 没有。

      使用#define,您可以使用任何(整数)值,例如#define IO_PORT 0xb3

      使用枚举,您可以让编译器分配数字,如果值不那么重要,这会容易得多:

      enum {
         MENU_CHOICE_START = 1,
         MENU_CHOICE_NEXT,
         ...
      };
      

      【讨论】:

      • 一切都占用“存储空间”,程序无法凭空分配数字。不同之处在于定义或枚举与代码合并在一起,增加了.text 段的大小,而不是增加了.rodata 段。 const 总是首选,如果它是一个选项,因为您可以本地化 const 变量的范围,也可以在调试器中查看它们。
      • 编译器可能会优化,使const int MY_CONSTANT=7; 根本没有内存位置...
      • @Lundin,`const` 占用了一个内存位置并需要一条获取指令。 #define 和枚举值作为操作数放置在指令中。代码大小会更小,因为额外的 fetch 需要 32 位地址(操作数)。
      • @BasileStarynkevitch:无论如何,常量值将作为操作数出现在汇编代码中。我同意中间常量(仅用于定义其他常量)可以完全优化掉。
      • @Paul Ogilvie:这是一个特定于实现的细节。如今,全世界似乎都在 ARM 非平凡常量上运行,或者在编译时未知的常量,往往会作为数据放在文字池中。无论如何,枚举相对于 C 和 #define 中的常量“变量”的主要好处在于能够在编译时表达式中使用 then,例如数组大小。
      【解决方案7】:

      TL;DR 的答案是,使用 #defineenum 通常并不重要。

      不过还是有一些细微的差别。

      枚举的主要问题是你不能改变类型。如果您使用枚举常量,例如enum { FALSE, TRUE };,那么这些常量将始终是int 类型。

      如果您需要无符号常量或大小与sizeof(int) 不同的常量,这可能会出现问题。如果您需要进行按位运算,有符号整数可能会导致细微的错误,因为在 99% 的情况下,将它们与负数混合没有任何意义。

      但是,使用宏,您可以指定任何类型:

      #define X 0                 // int type
      #define X 0u                // unsigned int type
      #define X 0ul               // unsigned long type
      #define X ((uint8_t)0)      // uint8_t type
      

      缺点是您不必选择使用宏来实际定义类型,而您可以使用枚举来执行此操作。枚举提供了更多的类型安全性,但前提是您对它们进行 typedef:typedef enum {FALSE, TRUE} BOOL;。 C 根本没有太多的类型安全性,但是好的编译器或外部静态分析工具可以在尝试意外转换为枚举类型时检测并警告类型问题。

      不过,另一个奇怪的地方是“BOOL”是一个枚举变量。与枚举常量不同,枚举变量不能保证它们对应的整数类型。您只知道它将是某种整数类型,足以容纳相应枚举常量的所有值。如果枚举的大小很重要,这可能是一件非常糟糕的事情。

      当然,枚举的优势在于您可以在本地范围内声明它们,因此您不必在不需要时弄乱全局命名空间。

      【讨论】:

      • 是的,松散的类型控制和浮点常量的缺乏导致我在 C 中主要使用枚举来进行实际枚举。另外,#define 常量当然可以在预处理器表达式中使用。好吧,这已经被一个特别糟糕的嵌入式编译器咬了,它总是使用 16 位 int 枚举,同时默默地截断 32 位常量。
      • @doynax 确实主要是在嵌入式系统中枚举会造成最大的麻烦。
      猜你喜欢
      • 2011-10-22
      • 1970-01-01
      • 2016-11-18
      • 2020-01-26
      • 1970-01-01
      • 1970-01-01
      • 2012-07-31
      • 2010-11-25
      • 2022-01-25
      相关资源
      最近更新 更多