"static const"与"#define"与"enum"的区别

665

在C语言中,以下哪个语句更好使用?

static const int var = 5;
或者
#define var 5
或者
enum { var = 5 };

51
有趣的是,这个问题与https://dev59.com/DnI-5IYBdhLWcg3w3ck8几乎完全相同。唯一的区别在于那个问题是关于C++的,而这个问题是关于C的。由于我的答案是特定于C ++的,我认为这使它们不同,但其他人可能不同意。 - T.E.D.
69
肯定不完全相同。出于兼容性原因,C++ 允许使用 C 语法的很多领域。在这些情况下,“如何最好地实现 X”这样的问题在 C++ 中将有不同的答案,比如对象初始化。 - MSalters
1
另外,https://dev59.com/4XI-5IYBdhLWcg3wQV-c - jamesdlin
1
这怎么不是基于观点的?它们各自有不同的目的。 - Sam Hammamy
17个回答

796
这取决于你需要该值的用途。你(以及到目前为止的所有人)忽略了第三种选择:
  1. static const int var = 5;
  2. #define var 5
  3. enum { var = 5 };
如果不考虑名称的选择问题,那么:
  • 如果你需要传递一个指针,你必须使用(1)。
  • 既然(2)是可选项,你就不需要传递指针。
  • (1)和(3)都有符号在调试器的符号表中 - 这使得调试更容易。而(2)更有可能没有符号,让你想知道它是什么。
  • (1)不能用作全局作用域的数组维度;(2)和(3)都可以。
  • (1)不能用作函数作用域的静态数组维度;(2)和(3)都可以。
  • 根据C99规范,所有这些都可以用于本地数组。从技术上讲,使用(1)将意味着使用VLA(可变长度数组),尽管“var”引用的维度当然是固定大小5。
  • (1)不能在像switch语句这样的位置使用;(2)和(3)都可以。
  • (1)不能用于初始化静态变量;(2)和(3)都可以。
  • (2)可以更改你不想更改的代码,因为它被预处理器使用;(1)和(3)都不会像那样具有意外的副作用。
  • 你可以检测出预处理器是否设置了(2);(1)和(3)都不允许这样做。
因此,在大多数情况下,优先选择“枚举”而不是其他替代方法。否则,第一个和最后一个要点可能是控制因素 - 如果您需要同时满足这两个要点,则需要更加深入地思考。

如果您在问有关C ++的问题,那么每次都应使用选项(1) - 静态常量。


132
列表很棒!“枚举”(enum)的一个缺点是它们被实现为“整型”([C99] 6.7.2.2/3)。使用 #define 可以指定无符号和长整型,使用 UL 后缀,而使用 const 则可以指定类型。enum 可能会导致常规类型转换方面的问题。 - Gauthier
52
人们总是抱怨类型安全。我不明白为什么不只需使用“#define var ((int)5)”就可以获得定义的类型安全了。 - Ingo Blackman
12
@RedX: 如果空间不是一个问题,那么你需要处于非常特殊的环境中。话虽如此,enum#define本身并没有使用额外的空间。该值将出现在目标代码中作为指令的一部分,而不是分配存储在数据段、堆或栈上。对于static const int,你会分配一些空间,但如果你不获取地址,编译器可能会对其进行优化。 - Jonathan Leffler
20
再次支持使用enum(枚举类型)和static const(静态常量):它们的值不能被修改。使用#define定义的值可以通过#undefine取消定义,而enumstatic const的值则固定为给定的值。 - Daan Timmer
22
@QED: 不,谢谢。在括号外使用简单常数是安全的。或者,展示一个可能被合法地期望编译的程序,如果没有在5周围加括号会如何被改变。如果它是函数风格宏的参数,或者表达式中有任何运算符,那么如果我没有包括括号,你责怪我是正确的。但这里不是这种情况。 - Jonathan Leffler
显示剩余19条评论

285

一般来说:

static const

因为它尊重作用域且类型安全。

唯一需要注意的是:如果要使变量可能在命令行上定义,还有一种替代方法:

#ifdef VAR // Very bad name, not long enough, too general, etc..
  static int const var = VAR;
#else
  static int const var = 5; // default value
#endif

尽可能使用类型安全的替代方案,而不是宏/省略号。

如果您真的需要使用宏(例如,您想要__FILE____LINE__),那么您最好非常小心地命名您的宏:在其命名约定中,Boost推荐全部大写,并以项目名称(此处为BOOST_)开头,然后在浏览库时,您会注意到这通常后跟着特定领域(库)的名称,然后是一个有意义的名称。

一般来说,它会导致名称很长 :)


2
同意 - 此外,使用 #define 也存在一般性的危险,因为预处理器不了解语法,可能会破坏代码。 - NeilDurant
10
我同意你的看法,更好的做法是使用#if而不是#ifdef。+1 - Tim Post
65
这是标准的C++宣传。下面的答案更清楚地解释了选项的真正含义。特别是:我最近遇到了一个"static const"的问题。有人使用它在头文件中定义了大约2000个"常量"。然后这个头文件被包含在大约100个“.c”和“.cpp”文件中。=>“const”占用了8M字节。真是太大了。是的,我知道你可以使用链接器来删除未引用的常量,但这仍然会留下那些被引用的"常量"。空间不足,这个答案有很多问题。 - Ingo Blackman
2
仅那些被取地址的静态变量应该保留在一个好的编译器中; 如果已经取了地址,就不能使用#define或枚举(没有地址)...所以我真的看不出还有什么其他可用的方法。如果你可以不使用“编译时评估”,可能会寻找extern const来代替。 - Matthieu M.
15
对于布尔标志,#if 可能比 #ifdef 更可取,但在这种情况下,如果要从命令行定义 var0,则使用 #if 将无法实现。因此,在这种情况下,只要 0var 的合法值,使用 #ifdef 更有意义。 - Maarten
显示剩余13条评论

113
在C中,正确的答案是:使用#define(或者如果适当的话,使用enum)。
虽然具有const对象的作用域和类型属性是有益的,但实际上,在C中(与C++不同),const对象不是真正的常量,因此在大多数实际情况下通常是无用的。
因此,在C中,选择应由您计划如何使用常量来确定。例如,您不能将const int对象用作case标签(而宏将起作用)。您不能将const int对象用作位字段宽度(而宏将起作用)。在C89/90中,您不能使用const对象指定数组大小(而宏将起作用)。即使在C99中,当您需要一个非VLA数组时,也不能使用const对象来指定数组大小。
如果这对您很重要,那么它将决定您的选择。大多数时候,您别无选择,只能在C中使用#define。不要忘记另一种替代方案,它在C中产生真正的常量-枚举。
在C ++中,const对象是真正的常量,因此在C ++中,几乎总是更好地首选const变量(在C ++中不需要显式静态)。

7
关于这个陈述,它指出在switch语句中不能使用const int对象作为case标签(而宏定义可以),但您测试了一下,在C语言中使用const int变量作为case标签是可以工作的。 - john
9
@john:好的,你需要提供你测试过的代码并指明具体的编译器。在所有版本的C语言中,在case标签中使用const int对象是非法的。(当然,你的编译器可以支持它作为非标准的类似于C++的语言扩展)。 - AnT stands with Russia
12
“...因此在大多数实际情况下通常是无用的。” 我不同意。只要您不需要将名称用作常量表达式,它们就是完全有用的。C语言中,“常量”指可以在编译时计算的内容;const则表示只读。 const int r = rand(); 是合法的。 - Keith Thompson
在C++中,与const相比,特别是与像arraybitset这样的stl容器一起使用,最好使用constexpr - Mayukh Sarkar
1
@john 你应该在 switch() 语句中进行测试,而不是在 case 中。我也刚刚被这个问题卡住了 ☺ - Hi-Angel

37
static const#define的区别在于前者使用内存,而后者不使用内存进行存储。其次,您不能传递#define的地址,但可以传递static const的地址。实际上,这取决于我们所处的情况,我们需要在这两者中选择一个。在不同的情况下,两者都能发挥最佳作用。请不要假设其中一个比另一个更好... :-)

如果是这种情况,Dennis Ritchie 就会保留最好的那个了... 哈哈哈... :-)


6
提到内存+1,尽管一些嵌入式系统仍然没有太多内存,但我可能会从使用静态常量开始,并仅在需要时更改为#define。 - fluffyben
3
我刚刚进行了测试。实际上,与#define或枚举相比,const int会使用额外的内存。由于我们编写的是嵌入式系统程序,我们无法承受额外的内存使用。因此,我们将回归使用#define或枚举。 - Davide Andrea
5
实际上,使用 const 不再会占用内存。如果使用 -O3 选项编译代码,GCC(测试版本为4.5.3和一些较新版本)可以轻松地将 const int 优化为直接字面量。因此,如果您进行低RAM嵌入式开发(例如AVR),则可以安全地使用C中的常量,只要您使用GCC或其他兼容编译器即可。我没有测试过,但是我也预计Clang会做同样的事情。 - Raphael

19

C语言中,#define 更为流行。您可以使用这些值来声明数组大小,例如:

#define MAXLEN 5

void foo(void) {
   int bar[MAXLEN];
}

据我所知,ANSI C不允许在这种情况下使用static const,在C++中你应该避免使用宏。你可以写成:

const int maxlen = 5;

void foo() {
   int bar[maxlen];
}

在C++中,由于const已经隐含了内部链接,因此甚至可以省略static


1
“internal linkage”是什么意思?我可以在一个文件中定义const int MY_CONSTANT = 5;,然后在另一个文件中使用extern const int MY_CONSTANT;进行访问。我在标准(至少是C99)中没有找到有关于const修改默认行为的任何信息,“6.2.2:5 如果对象的标识符的声明具有文件作用域且没有存储类说明符,则它的链接性是外部的”。 - Gauthier
@Gauthier:抱歉,我的意思是“const已经在C++语言中隐含了”。这是特定于C++的。 - sellibitze
@sellibitze,看到你提出了一些真正的论点,而不是大量的观点,这很好。如果有真正论据的奖金,你就赢了! - Paul
4
从C99开始,你的第二段代码是合法的。bar是可变长度数组(VLA);编译器可能会生成代码,就好像它的长度是常量一样。 - Keith Thompson

15

C中使用const的另一个缺点是无法在初始化另一个const时使用该值。

static int const NUMBER_OF_FINGERS_PER_HAND = 5;
static int const NUMBER_OF_HANDS = 2;

// initializer element is not constant, this does not work.
static int const NUMBER_OF_FINGERS = NUMBER_OF_FINGERS_PER_HAND 
                                     * NUMBER_OF_HANDS;

即使使用const也不起作用,因为编译器不认为它是一个常量:

static uint8_t const ARRAY_SIZE = 16;
static int8_t const lookup_table[ARRAY_SIZE] = {
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; // ARRAY_SIZE not a constant!

我很乐意在这些情况下使用类型为const的变量,否则...


5
有点晚了,但这个问题在另一个问题中出现了。寻找原因,为什么你的static uint8_t const ARRAY_SIZE = 16;突然不再编译可能有些具有挑战性,尤其是当#define ARRAY_SIZE 256深埋在错综复杂的头文件之中时。那个全大写的名字ARRAY_SIZE很容易引起麻烦。请将全部大写字母留给宏,并且永远不要定义不符合全部大写字母形式的宏。 - David Hammen
@David:明智的建议,我会遵循。 - Gauthier
这是由于“静态”存储,而不是“const”限定符引起的。 - tstanisl
@tstanisl 嗯,是的,因为这些静态变量需要初始化,我认为这是在编译时进行的。我只是指的是这种定义“常量”的方式。令人奇怪的是,我的第一个代码示例现在可以使用gcc 9.3.0工作,即使使用 --stc=c89。第二个则不行,编译器会报错“可变数组”,因为ARRAY_SIZE是一个(只读)变量,而不是常量。 - Gauthier
@Gauthier,第一个示例可以在GCC和CLANG的严格模式下与所有C标准(c89、c99、c11、c18)一起使用。有趣的是,似乎static变量可以用于初始化其他static变量。 - tstanisl
@tstanisl 确实看起来是这样。我想我知道当时使用的奇怪嵌入式编译器是什么(已经超过10年了!),但在双重检查之前不会指责他们 ;) - Gauthier

11
如果可以这样做,static const有很多优点。它遵循正常的作用域原则,在调试器中可见,并且通常遵守变量所遵守的规则。
然而,在最初的C标准中,它实际上并不是一个常量。如果您使用#define var 5,您可以编写int foo[var];作为声明,但是您不能这样做(除非使用编译器扩展)static const int var = 5;。在C++中不是这种情况,static const版本可以在任何#define版本可以使用的地方使用,我相信在C99中也是如此。
然而,永远不要用小写字母命名#define常量。它将覆盖该名称的任何可能使用,直到翻译单元结束。宏常量应该在实际上是它们自己的命名空间中,这通常是所有大写字母,也许带有前缀。

6
不幸的是,C99中的 const 仍不是真正的常数。你可以在C99中使用 const 声明数组大小,但这只是因为C99支持可变长度数组(VLA)。因此,它只能在允许使用VLA的情况下工作。例如,即使在C99中,您仍然无法使用 const 声明 struct 中成员数组的大小。 - AnT stands with Russia
1
虽然 C99 不允许你这样做,但是 GCC(测试版本为 4.5.3)可以完美地让你使用 const int 大小来初始化数组,就像 C++ const 或宏一样。当然,你是否想依赖于 GCC 对标准的这种偏差取决于你自己,我个人会选择这种方式,除非你真的预见到将来会使用其他编译器,而 Clang 在这里也有相同的功能(测试版本为 Clang 3.7)。 - Raphael

9

使用const比使用#define更好。这是因为编译器处理const,而预处理器处理#define。就像#define本身不是代码的一部分(粗略地说)。

例如:

#define PI 3.1416

符号名称PI可能永远不会被编译器看到;它可能会在源代码到达编译器之前被预处理器移除。因此,名称PI可能不会进入符号表。如果您在编译过程中使用常量时出现错误,这可能会让人感到困惑,因为错误消息可能指的是3.1416而不是PI。如果PI在您没有编写的头文件中被定义,您将不知道那个3.1416来自哪里。
此问题也可能在符号调试器中出现,因为您编程使用的名称可能不在符号表中。
解决方案:
const double PI = 3.1416; //or static const...

8

#define var 5会在存在类似于mystruct.var的情况下引起问题。

例如,

struct mystruct {
    int var;
};

#define var 5

int main() {
    struct mystruct foo;
    foo.var = 1;
    return 0;
}

预处理器会替换它,代码将无法编译。因此,传统的编码风格建议所有常量#define使用大写字母以避免冲突。

6
我写了一个快速测试程序来证明其中一个区别:
#include <stdio.h>

enum {ENUM_DEFINED=16};
enum {ENUM_DEFINED=32};

#define DEFINED_DEFINED 16
#define DEFINED_DEFINED 32

int main(int argc, char *argv[]) {

   printf("%d, %d\n", DEFINED_DEFINED, ENUM_DEFINED);

   return(0);
}

这个代码编译出现以下错误和警告:

main.c:6:7: error: redefinition of enumerator 'ENUM_DEFINED'
enum {ENUM_DEFINED=32};
      ^
main.c:5:7: note: previous definition is here
enum {ENUM_DEFINED=16};
      ^
main.c:9:9: warning: 'DEFINED_DEFINED' macro redefined [-Wmacro-redefined]
#define DEFINED_DEFINED 32
        ^
main.c:8:9: note: previous definition is here
#define DEFINED_DEFINED 16
        ^

请注意,使用 enum 关键字定义时会产生错误,而使用 define 定义则会产生警告。

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