如果我有
#define NUM (30 * 60)
预处理器是复制粘贴(30 * 60)
到代码中,还是在代码中出现的任何地方写入 1800 NUM
?
( 30 * 60 )
(可能)在编译时计算正如其他人所说,预处理器只是将定义的标记替换为其定义的文本。30*60
然后完全取决于编译器本身来注意可以在编译时执行的任何算术(例如)。
在您的示例中,NUM
源文件中所有符合条件的实例都将简单地替换为 text ( 30 * 60 )
。
这就是对您提出的直接问题的简单回答。但还有几个问题值得探索。
预处理器执行的文本替换是文字的。也就是说,预处理器几乎不理解 C 语言的任何语法。因此,结果可能并不意味着您所期望的。例如,如果你有
#define N 30 + 60
int a = N * 2;
预处理后的文本不会int a = 30 + 60 * 2;
像a
预期的那样为 180 N*2
。相反,由于优先级a
变为 150。
解决方案是在扩展文本中始终使用足够的括号,您的写作( 30 * 60 )
就是这种最佳实践的一个例子。当您开始使用包含形式参数的宏时,您会发现明智地使用括号对于避免意外非常重要。
但我想超越简单的答案,并尝试向您展示如何自己探索预处理器的行为。
对于像您的问题一样简单的代码,知道它是一个简单的文本替换应该足以预测会发生什么。但是当您开始使用预处理器的更复杂的功能(从带有参数的宏开始)时,您有时会想要调试您的预处理器的使用。要做到这一点,通常最容易运行预处理器而不编译和运行任何代码。
(类似地,有时您会想知道编译器本身做了什么,为此编译成汇编语言很有用,而无需创建二进制文件和执行代码。我们将在下一节中了解这一点。 )
预处理器在历史上是一个单独的程序,由编译器驱动程序命令在源文件上运行,并且在编译器本身的第一次传递之前运行。在现代编译器实现中,预处理器通常不作为单独的可执行文件实现,但由于历史原因,它仍然可以在不编译的情况下调用。
预处理器的通常名称是cpp
. 在极其常用的 GCC 编译器套件中,它也可以作为gcc -E
. 在这两种情况下,cpp
都将读取在其命令行上命名的文件,或者stdin
如果没有命名文件则读取,并将其输出写入stdout
. 该输出通常会用#line
指令修饰,以便编译器可以归咎于正确的源文件。您通常可以使用命令行选项将其关闭,该选项-P
用于 GCC 实现。
鉴于此源代码:
#define NUM (30 * 60)
int n = NUM * 42;
char *str = "NUM";
我们可以像这样通过预处理器输入它并立即查看输出:
C:\...>cpp -P q19987548.c 整数 n = (30 * 60) * 42; 字符 *str = "NUM"; C:\...>
另请注意,替换不会发生在字符串文字内。一些更老的编译器确实替换了字符串中的文本,但自从第一个 C 标准被采用以来就没有了。
(30 * 60) * 42
计算的?更进一步,我们可以要求编译器向我们展示它生成的汇编代码,并使用它来发现它是否在编译时计算了表达式。在许多 C 编译器中,这是使用-S
选项完成的,就像在 GCC 中一样。以 Windows 上针对 x86 的 GCC 为例,我们可以编译上述代码片段,得到如下汇编输出:
.file "q19987548.c"
.globl _n
.data
.align 4
_n:
.long 75600
.globl _str
.section .rdata,"dr"
LC0:
.ascii "NUM\0"
.data
.align 4
_str:
.long LC0
可以看出,这个代码片段声明了一个名为的位置_n
,并用常量 75600 填充它,这正是30 * 60 * 42
. 因此,在这种情况下,它显然是由编译器计算的。
一般来说,当对目标使用通常的优化时,您应该假设编译器知道它在做什么,而不用太担心这个级别的细节。
尽管不是一门完整的编程语言,但 C 预处理器可用于一些相当惊人的技巧。
由于它与 C 语言本身如此解耦,因此可以用于处理其他语言的源代码。我已经看到它用于在 和 中生成手册页和其他nroff
文档troff
。任何与其标记化规则兼容并接受其注入空白和删除 C 注释的源文本都可以使用它进行处理。
预处理是根据令牌替换定义的。找到令牌NUM
后,将其替换为 5 个令牌序列(
, 30
, *
, 60
, )
。实际规则稍微复杂一些,因为您可能NUM
也出现在替换中(不会再次替换),但这是大局。
至于乘法,大多数(如果不是全部)编译器都会为您进行乘法运算并在生成的代码中使用 1800。
NUM
预处理器用您指定的内容替换代码中的实例- (30 * 60)
。编译器可能(几乎肯定会)稍后将其优化为1800
,从而为您节省运行时计算。
#define NUM (30 * 60)
将NUM
完全替换为(30 * 60)
.
这就是为什么这样的事情有时会导致可怕且愚蠢的不稳定行为的原因:
#define H 0.1f
#define H2 2.f*H
现在在实际代码中应该计算f
O(h^2) 阶函数的导数:
float num_dev = (f(x+H)-f(x-H))/H2
如果预处理器会计算2.f*H
替换前的结果,那么一切都会井井有条。
但是由于预处理器只是替换H2
,因为2.f*H
这会给出错误的结果。
float num_dev = ((f(x+H)-f(x-H))/2.f)*H
(我添加了新()
的以明确我的观点,编译器不会这样做。)
因此,就像您所做的那样,在表达式周围加上括号总是一个很好的主意。
#define H2 (2.f*H)
这是一个实际的例子:
#include <stdio.h>
#define H 0.1f
#define H2 2.f*H
int main(void) {
float a = (4.f-2.f)/H2;
float b = (4.f-2.f)/(H2);
printf("%f %f\n", a, b);
return 0;
}
输出:
0.100000 10.000000
假设您使用的是gcc,您可以使用 -E 标志来显示预处理器输出。您可以使用以下单行代码测试您自己的编译器的行为:
$ { echo "#define NUM (30 * 60)"; echo "int a = NUM;" ; } | gcc -E -
# 1 "<stdin>"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "<stdin>"
int a = (30 * 60);
$