为什么大多数C开发人员使用define而不是const?

109

在许多程序中,#define 的作用与常量相同。例如:

#define FIELD_WIDTH 10
const int fieldWidth = 10;

我经常看到第一种形式比另一种更受欢迎,依赖于预处理器来处理基本上是应用程序决策的内容。这个传统有什么原因吗?


4
请注意,对于整数常量,您也可以像使用“enum {FIELD_WIDTH = 10};”一样使用枚举。如果您需要在函数作用域中使用编译时常量,则这可能很有用。 - Jochen Walter
2
你也可以在函数作用域内使用 #define,但是你必须记得在函数结束时取消定义。 - CashCow
3
@CashCow:没错,但是这样你就取消定义了一个可能在文件范围内的 #define(它可能已经在你没有编写的包含文件中定义)。 - Jochen Walter
6
@Jochen:当与另一个宏定义发生冲突时,您的#define已经覆盖了先前的定义,并忽略了编译器警告,因此您可以将其#undef并在代码中进一步使用该宏时产生编译器错误,以提醒您应更改函数范围宏的名称。 - Secure
1
@Secure:当然可以,但是你必须处理名称冲突。使用局部作用域的优点是避免了这种情况。当然,使用局部枚举也不是绝对安全的:如果全局 #define 将 FIELD_WIDTH 重写为其他值,比如 20,那么枚举将被重写为 \enum {20 = 10};``。但是,如果 FIELD_WIDTH 的全局和局部定义都是枚举类型,则两个定义都可以共存(局部定义会掩盖全局定义)。 (有关此主题的更多信息,请参见 http://www.eetimes.com/discussion/programming-pointers/4023879/Enumeration-Constants-vs-Constant-Objects。) - Jochen Walter
显示剩余3条评论
9个回答

179

这其中有一个非常充分的理由:const在C语言中并不意味着某个东西是常量。它仅仅表示一个变量是只读的。

在编译器需要真正常量(例如用于非VLA数组的数组大小)的地方,使用一个const变量,如fieldWidth就不可能实现。


49
在不懂C语言的C++程序员中,如果回答正确,请点赞。 - R.. GitHub STOP HELPING ICE
3
@C. Ross: 一致性。所有的显式常量通常都是用 #define 来定义的。 const 只用于表示只读(访问路径)变量。我知道,在 C 中,const 实现得并不好 :-) - Bart van Ingen Schenau
3
在C89中不允许这样做。在C99中,您可以为具有自动存储期限的变量这样做,但从技术上讲它是VLA。 - caf
6
@异常情况:在 const 的处理上,C 和 C++ 是非常不同的语言。在 C++ 中,const 可以用于正确的常量,但在 C 中则不行。 - Bart van Ingen Schenau
2
不仅在非VLA数组大小中不能使用const,也不能用于创建其他const。例如:const uint8_t SECONDS_PER_MINUTE = 60U; const uint16_t SECONDS_PER_HOUR = 60U * SECONDS_PER_MINUTE;。我想要一种有类型的常量(正如@Bart所写,const并不是真正的常量)。 - Gauthier
显示剩余9条评论

25
它们是不同的。 const只是一个修饰符,它表示变量在运行时不能被更改。但是变量的所有其他特性都保持不变:它具有分配的存储空间,并且可以访问此存储空间。因此,代码不仅将其视为文字,而是通过访问指定的内存位置(除非它是static const,否则可以进行优化)引用变量,并在运行时加载其值。由于const变量具有分配的存储空间,如果将其添加到标头并在多个C源中包含它,则会出现“多重符号定义”链接错误,除非将其标记为extern。在这种情况下,编译器无法根据其实际值对代码进行优化(除非打开全局优化)。 #define仅将名称替换为其值。此外,预处理器中可以使用#define定义的常量:您可以将它与#ifdef一起使用,根据其值进行条件编译,或使用字符串化运算符#获取带有其值的字符串。由于编译器在编译时知道它的值,因此它可以基于该值优化代码。
例如:
#define SCALE 1

...

scaled_x = x * SCALE;
当定义SCALE1时,编译器可以消除乘法,因为它知道x * 1 == x,但如果SCALE是一个(externconst,则需要生成代码来获取值并执行乘法,因为在链接阶段之前不知道该值。(使用extern可从多个源文件中使用常量。)
使用枚举类型更接近于使用#define
enum dummy_enum {
   constant_value = 10010
};

但这仅限于整数值,并且不具有#define的优点,因此并不常用。

const在需要从编译库中导入常量值时很有用,或者与指针一起使用时。又或者是一个由变量索引值访问的常量值数组。否则,const没有比#define更大的优势。


1
+1 提到“枚举”作为替代方案 - 应该更经常使用。一个主要的优点是符号可用于调试器。 - Jonathan Leffler
1
我更倾向于认为不能使用定义是调试器的劣势,而枚举可在调试器中使用是其优势。 - Vovanium
3
#defineenum 之间的优势在于你可以使用 #ifdef 来测试其存在性或在 #if 中使用其值,这使得它更适用于控制构建时的选择或提供可能存在于某些平台上但不存在于其他平台上的功能。 - R.. GitHub STOP HELPING ICE
1
较新版本的gdb / gcc确实支持在调试器中使用#define常量(我认为必须在编译时向gcc传递-gdb3标志以启用此功能)。 - psmears
2
关于优化的部分对于任何执行链接时优化的编译器都不是正确的。 - Étienne
显示剩余2条评论

14

这是因为大多数时候,您需要一个常量,而不是一个带有const限定符的变量。在C语言中,它们并不完全相同。例如,变量无法作为static存储期对象的初始化器的一部分,也不能用作非变长数组维度的值(例如结构体中数组的大小或任何C99之前的数组)。


除了“结构体中的数组”之外,C99中是否还有其他非VLA数组的情况? - PleaseHelp
3
具有静态存储期的数组。复合字面量数组。在文件作用域中使用typedef的数组类型。可能还有其他的。 - R.. GitHub STOP HELPING ICE

7
扩展R的答复:fieldWidth不是一个常量表达式,它是一个带const限定符的变量。它的值直到运行时才确定,因此不能在需要编译时常量表达式的地方使用它(如在数组声明中,或在switch语句的case标签中等)。
与宏FIELD_WIDTH进行比较,预处理后展开为常量表达式10;这个值在编译时是已知的,因此可以用于数组维度、case标签等。

2
所以 const 的操作更像 Java 中的 final,只允许一次赋值? - C. Ross
@C. Ross:就实际目的而言,是的,尽管我认为语义上可能存在一些非平凡的差异(在C代码中我并不经常使用const,而且我还处于Java学习曲线的底部)。 - John Bode

6

补充R.和Bart的回答:在C语言中,只有一种方法可以定义符号的编译时常量:枚举类型常量。标准规定这些类型为int。我个人会将您的示例写成:

enum { fieldWidth = 10 };

但我猜在C程序员中,对此的看法可能有很大的差异。

4

尽管const int并不总是合适的选择,但如果你要定义某个东西为一个整数值,枚举通常可以作为#define的替代品。在这种情况下,这实际上是我的首选。

enum { FIELD_WIDTH = 16384 };
char buf[FIELD_WIDTH];

在C++中,这是一个巨大的优势,因为你可以将枚举限定在类或命名空间中,而不能将#define限定在其中。
在C语言中,你没有命名空间,并且不能将枚举限定在结构体中,我甚至不确定你是否获得了类型安全性,所以我实际上看不到任何主要的优势,虽然也许有些C程序员会指出给我。

在C++中,有没有一种“好的”方法来定义枚举,使其表现得像一个“int”,并包括使用算术运算符?我知道可以定义运算符重载,并且这样的事情可以通过宏来完成,以避免太过邪恶,但我不知道是否存在类似的简单语言关键字。我是否不知道其中有一个?如果没有,那么它似乎是语言中奇怪的遗漏,因为C++应该允许对C程序进行轻松改编。 - supercat

2
根据K&R(第二版,211页),“const和volatile属性是ANSI标准的新内容”。这可能意味着真正古老的ANSI代码根本没有这些关键字,这只是一个传统问题。 此外,它说编译器应该检测试图更改const变量的尝试,但除此之外,它可以忽略这些限定符。我认为它的意思是一些编译器可能不会优化包含const变量的代码以表示为机器码中的中间值(就像#define所做的那样),这可能导致访问远程内存的额外时间成本,并影响性能。

1
+1 引用 K&R,但回答它是关于优化而不是语言要求的话则-1。净值为0。 - R.. GitHub STOP HELPING ICE
不,这意味着const和volatile是由ANSI标准C引入的。K&R第二版实际上是在标准获得批准之前出版的。 - JeremyP
许多C程序员使用strchr来搜索字符,而不是使用strtof,因为strtof直到C99才被添加。 - R.. GitHub STOP HELPING ICE

1
在C语言中,定义数字常量的最佳方式是使用enum。请阅读K&R的《C程序设计语言》第39页相应章节。

1

一些C编译器会将所有的const变量存储在二进制文件中,如果准备大量系数列表,这将在嵌入式世界中占用大量空间。

相反地:使用const允许在现有程序上刷写以更改特定参数。


2
关于你提到的“相反”,这将高度依赖于具体实现。一个好的实现会内联所有的const变量,并在它们的地址从未被使用时将其优化掉,就像内联函数一样。 - R.. GitHub STOP HELPING ICE
1
我看过几个在嵌入式系统中使用/滥用的实现,跨越了几个平台(AVR、HCS08)。它们被定义为“const”,但是通过一些编译器指令,导致它们被存储在一个定义好的内存地址。它们似乎就像一个“* const”一样工作。 - Nick T
如果不是在二进制文件中,你认为 #define 的值存储在哪里? - David Heffernan
1
@David,预处理器的内存? - Nick T
那么它们如何进入可执行文件呢? - David Heffernan
显示剩余2条评论

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