C++ - 枚举 vs. 常量 vs. #define

32

我编辑了标题并添加了一个标签,希望你不介意。 :-) - Nawaz
@Nawaz。完全没问题,不用担心。 - Simplicity
可能是["static const" vs "#define" in c]的重复问题 (https://dev59.com/onI-5IYBdhLWcg3wxruQ) - Matthieu M.
@MatthieuM。当然不是。C++的规则已经足够不同,它应该有一个不同的答案。 - Pharap
7个回答

37

考虑以下代码:

#define WIDTH 300

enum econst
{
   eWidth=300
};

const int Width=300;

struct sample{};

int main() 
{
        sample s;
        int x = eWidth * s; //error 1
        int y = WIDTH * s;  //error 2
        int z = Width * s;  //error 3
        return 0;
}

很明显,每个乘法都导致编译错误,但是看看GCC如何为每个乘法错误生成消息:

  

prog.cpp:19:错误:在“eWidth * s”中没有匹配的operator*
  prog.cpp:20:错误:在“300 * s”中没有匹配的operator*
  prog.cpp:21:错误:在“Width * s”中没有匹配的operator*

在错误消息中,您看不到您已经#defined的宏WIDTH,对吗?这是因为当GCC尝试编译与第二个错误对应的行时,它看不到WIDTH,它只看到300,在GCC编译该行之前,预处理器已经用300替换了WIDTH。另一方面,对于枚举eWidth和常量Width,没有发生任何这样的事情。

在此处查看错误:http://www.ideone.com/naZ3P


此外,请阅读Scott Meyers的《Effective C++》中Item 2: Prefer consts, enums, and inlines to #defines


1
不可否认,Clang可能会通过显示导致错误的行并因此包括宏(以及其扩展)来生成更好的错误消息。虽然我同意宏不应该用于常量,但我认为更好的处理问题的方法不应该将gcc的错误消息作为推理的基础。 - Matthieu M.
我知道这个问题和答案都很老了,但我一直有一个关于它的问题。内存使用。这三种方法中哪一种会导致在编译程序中创建一个占用空间的变量? - RedX
@RedX:宏(例如#define X 40)不占用任何内存。但是...请不要担心这个问题。即使const变量占用了一些字节,也请使用const变量。在许多情况下,编译器可以优化代码,因此即使是const变量,也可以节省内存(如果它们确实是常量)。或者使用枚举,它们可以为您节省字节。 - Nawaz
@Nawaz 我开发嵌入式系统的代码,所以我自然会担心全局变量的数量和它们占用的空间。我已经寻找了相当长的时间来减少对#define的使用,但一直担心内存使用情况。 - RedX
@RedX:在这种情况下,enum 应该可以帮助你在 C 中(或者在 C++ 中,你也可以使用 constconstexpr 定义的常量表达式)。试一试吧。 - Nawaz
显示剩余8条评论

16

enum是编译时常量,具有调试信息但没有存储分配。

const分配了一个存储位置,取决于编译器是否通过常量传播进行优化。

#define没有存储分配。


“enum”在程序中声明一个该类型的变量时会占用存储空间。不是吗?我认为你提到“enum”的情况是指在程序中从未声明该类型的变量。但是,为什么有人会声明枚举而不使用它呢? - RBT
1
如果您能澄清“无存储分配”并不意味着“不使用内存”的事实,我会点赞。所有这些都需要将数据存储在某个地方,但“存储分配”和“无存储分配”的区别在于存储的位置。 - Pharap

6

#define值会被预处理器替换为它们声明的值,因此在调试器中,只能看到值,而不是#defined名称。例如,如果您有 #define NUMBER_OF_CATS 10,在调试器中,你只会看到10(因为预处理器已经将代码中NUMBER_OF_CATS的每个实例替换为10)。

枚举类型是一种类型本身,其值是该类型的常量实例,因此预处理器不会对其进行更改,您将在调试器中看到该值的符号描述。


3
编译器在使用特定选项编译程序时,会将枚举信息存储在二进制文件中。
当变量是枚举类型时,调试器可以显示枚举名称。以下是一个示例:
enum E {
    ONE_E = 1,
};

int main(void)
{
    enum E e = 1;

    return 0;
}

如果你使用gcc -g编译,你可以在gdb中尝试以下操作:

Reading symbols from test...done.
(gdb) b main
Breakpoint 1 at 0x804839a: file test.c, line 8.
(gdb) run
Starting program: test 

Breakpoint 1, main () at test.c:7
7               enum E e = 1;
(gdb) next
9               return 0;
(gdb) print e
$1 = ONE_E
(gdb) 

如果使用宏定义,你将无法为e提供正确的类型,并且必须使用整数。在这种情况下,编译器将打印1而不是ONE_E-g标志要求gdb向二进制文件中添加调试信息。你可以通过输入以下命令查看其是否存在:
xxd test | grep ONE_E

虽然我不认为这在所有架构上都可以实现。


谢谢您的回复。您所说的“编译器在二进制文件中存储枚举信息”是什么意思?谢谢。 - Simplicity
从gcc手册中可以得知:-g选项会以操作系统本地格式生成调试信息,而二进制文件中也包含了这些信息,且其格式是gdb所能理解的。 - Penz

1

至少对于我目前手头上的Visual Studio 2008来说,这个句子是正确的。如果你有

#define X 3
enum MyEnum
{
    MyX = 3
};

int main(int argc, char* argv[])
{
    int i = X;
    int j = (int)MyX;
    return 0;
}

如果你在main中设置了断点,你可以将鼠标悬停在"MyX"上,看到它的值为3。如果你将鼠标悬停在X上,则不会看到任何有用的信息。

但这并不是语言属性,而是IDE的行为。下一个版本可能会有所不同,其他IDE也可能如此。因此,请检查一下你的IDE,看看这个句子是否适用于你的情况。


2
这是一种语言属性,因为编译器甚至不会看到 X(只有预处理后的值 3),因此调试器绝对无法使用 X 作为符号名称,因此它将仅显示纯值 3。这对所有编译器和 IDE 都是正确的。另一方面,在调试器中显示像 MyX 这样的符号确实是一个 IDE 特定的功能,但它是如此常见,我怀疑现代 IDE 中是否有任何不提供此功能的。 - Péter Török
@PéterTörök 没有什么可以阻止某人创建一个同时进行预处理的编译器或者创建一种将 X 放置在符号表中的方法。 - Pharap

0

0

我回答得太晚了,但我觉得我可以补充一些内容 - 枚举 vs. 常量 vs. #define

枚举 -

  1. 不需要分配值(如果只想要连续的值0、1、2..),而在#defines的情况下,您需要手动管理值,这可能会导致人为错误
  2. 它只是作为变量,在在线调试期间,可以在监视窗口中查看枚举的值
  3. 您可以有一个枚举类型的变量,可以将枚举分配给该变量

    typedef enum numbers { DFAULT, CASE_TRUE, CASE_OTHER, };

    int main(void) { numbers number = CASE_TRUE; }

常量 -

  1. 它是存储在只读内存区域中的常量,但可以使用地址访问,这在#define的情况下是不可能的。
    1. 如果使用const而不是#define,则可以进行类型检查
  2. 定义是预处理指令,但const是编译时的 例如

    const char *name = "vikas";

你可以访问变量名并使用其基地址,例如vikas [3]可读取'a'等。

#define - 是愚蠢的预处理器指令,只做文本替换。


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