理解C语言中的宏

7
为什么以下代码的输出值为5?
#include<stdio.h>

#define A -B
#define B -C
#define C 5

int main()
{
  printf("The value of A is %d\n", A);
  return 0;
}

1
尝试一下。我会说5。 - Jean-François Fabre
1
你尝试它的时候得到了什么? 为什么你没有尝试呢? 输出有什么让你感到困惑的地方? - Jonathan Leffler
5
请记住,在预处理器扩展宏之前,输入已经进行了分词。因此,代码会像你写的- - 5一样,而不是--5,后者会出错(无法对常量进行减法操作)。 - Jonathan Leffler
1
@Marged:我不同意,这与懒惰无关,编译源代码并不能证明什么,解释额外空间来自何处才是这个问题的核心。告诉面试官有关标记粘贴和编译器错误的信息将是应聘者的绝佳答案。 - chqrlie
我认为这是一个很棒的问题 :) 正如@chqrlie所指出的那样,仅仅编译并不能帮助我们理解为什么这样可以工作。 - Jean-François Fabre
显示剩余7条评论
5个回答

8
这是一个棘手的问题,因为它是编译器预处理器的压力测试。
这取决于预处理器是编译器的集成阶段还是通过文件或管道传递其输出到编译器的单独程序。在这种情况下,无论它是否足够小心以避免错误令牌粘贴,您可能会得到预期的输出:5或者您可能会得到编译错误。
在经过stdio.h的预处理内容之后,源代码扩展为:
int main()
{
  printf("The value of A is %d\n", --5);
  return 0;
}

但是这两个-是独立的标记,因此根据预处理器是否在其输出中将它们分开,您可能会得到一个输出5的程序或一个无法编译的程序,因为--不能应用于文字5
无论是gcc还是clang预处理器,在使用-E命令行选项生成预处理器输出时,都会正确地将-与额外的空格分离,以防止标记粘合。他们在展开<stdio.h>后将其作为预处理源代码输出。
int main()
{
  printf("The value of A is %d\n", - -5);
  return 0;
}

尝试使用自己的编译器检查源代码如何扩展。看起来Visual Studio 2013和2015未通过测试,并以错误拒绝程序。
为了使事情清楚,我并不是说程序的行为应该取决于编译器架构。我希望至少有一个通用的C编译器会处理不当的一致性测试。我对MS Visual Studio 2013和2015未能通过此测试并不感到惊讶。
在预处理器的文本输出中只需要额外的空格。无论Visual Studio是否使用多个单独的阶段,源程序都是完全有效的,他们无法编译它是一个BUG。

白空格从哪里来?如果clang和gcc是正确的,为什么它们不将B扩展为-5 - axiac
@axiac:他们不需要扩展到“-5”,因为“-5”是正确的。 - Jean-François Fabre
1
标准规定了必须发生的事情(GCC和Clang正确实现);一些编译器可能存在错误,可能是因为它们使用单独的预处理程序。标准没有说“您可以根据预处理程序是否为单独的程序而获得不同的行为”。 - Jonathan Leffler
4
@axiac,当预处理器将其结果作为文本而不是作为直接传递给编译器的令牌序列发出时,空格会被合成。这使得它能够确保,如果输出作为C源代码重新读取,它将表示与原始源相同的令牌序列。这是一个实现方面的考虑,因为标准对于这种从文本到令牌的转换没有做出任何规定。 - John Bollinger
@JonathanLeffler:我完全同意。这段代码是预处理器的确认测试,但VS无法通过它。我已经澄清了相关答案。 - chqrlie
真的很有趣。我以前从未考虑过这个区别! - Brett Hale

5

不需要编译此代码,只需在其上使用gcc -E(预处理器)并观察结果:

<lots of output expanding stdio.h> ...

int main()
{
  printf("The value of A is %d\n", - -5);
  return 0;
}

显然的结果是5(通过查看嵌套的宏可能已经猜到,但小型预处理器测试并不会有坏处)。
(其他答案指出,一些编译器可能处理减号的预处理,这将导致编译器错误。gcc 处理得很好。)

3

这个问题并不是很清晰,但我还是决定尝试回答。

Visual Studio 2013和2015:error C2105: '--'需要左值

原因是以下代码行:

printf("The value of A is %d\n", A);

首先进行翻译(A 变为 -B):

printf("The value of A is %d\n", -B);

然后进入(B变为-C);
printf("The value of A is %d\n", --C);

然后变成(C 变成 5):

printf("The value of A is %d\n", --5);

由于5不是左值,您无法对其进行减量操作,因此出现错误。这看起来很合理,因为预处理器只会进行简单的字符串替换。


6
恭喜您。您可以向 Microsoft 提交错误报告。他们的编译器表现不正确。 - Jonathan Leffler
2
预处理器的输入在宏展开之前被标记化。标记不会被还原。-B 中的 - 是一个标记;它不会再次被宏展开。下一个标记是 B;它将作为 -C 进行宏展开(这是两个标记)。- 再次不会被宏展开;它继续向下传递。C 不会被宏展开;它保持不变地传递。预处理器的输出是 3 个标记——--C。将它们混淆为 --C,然后将两个破折号解释为减法是不正确的。标准非常清晰。 - Jonathan Leffler
2
额外的空格只在预处理器的文本输出中需要。无论VS是否使用多个独立阶段,源程序都是完全有效的,他们无法编译它是一个BUG。 - chqrlie
1
@JonathanLeffler 如果我理解正确的话,不可能将 #define A str#define B uct,然后使用 AB 生成关键字 struct,是吗?无论如何,它都将被解析为两个标记,对吗?(struct) - axiac
3
如所示,是正确的。当然有一个##标记粘贴运算符:#define C(x, y) C1(x, y) 以及 #define C1(x, y) x ## y,可以在 C(A, B) apoplexy { … }; 等中使用,导致代码不幸的读者(更不用说编译器作者)中风。需要两级宏来使参数扩展到 struct。标记粘贴只适用于“词语”标记,而不是标点符号。 - Jonathan Leffler
显示剩余5条评论

0
这是一个很好的例子,说明不应该使用预处理器。为了避免混淆,应该使用括号(不仅在这种情况下)。
#define A (-B)
#define B (-C)
#define C (5)

这是关于如何使用预处理器的有效观察,它并不直接回答“为什么”的问题。因此,对于有效性加1分,对于相关性减1分。 - Jonathan Leffler
是的,但最初的问题是无法回答的。它只是一个示例,而不是“未定义行为”(UB),因为在C标准意义下,UB是指程序实际编译,但结果是不可预测的,但在这种情况下,编译过程是未定义的。因此,如果预处理器不添加任何空格,则我的另一个答案是编译器错误;如果添加了空格,则输出5。 - 0___________
2
不,这是完全定义良好且具有有效答案的代码 - 尽管至少有一个主要编译器会处理错误的C代码。 代码中没有未定义行为。 (我并不是在声称这是好代码;它不是。使用您的宏而不是问题中的内容可以防止损坏的编译器在有效代码上出现故障。尽管这些都是次要问题。) - Jonathan Leffler
#define C (5) 中的括号似乎是不必要的。你认为它们在什么情况下是必需的? - chqrlie
你在不到4秒的时间内回复了我的问题吗?多余的括号并不会有影响,我只是想问你是否考虑了我没有考虑到的东西。 - chqrlie
显示剩余3条评论

0
每个 #define 预处理指令都会将一个变量插入到预处理器的环境中,该变量被分配为由一系列预处理指令组成的值。
{A -> -B; B->-C; C->5}

是在评估A时的环境。现在,对于A的评估过程,我们有:

  A  ->  -B   (the identifier `A` is transformed in the stream of preprocessing tokens `-B`)
 -B  ->  --C
--C  ->  --5
->  5

这个代码段不再被Prosser算法评估,因为它没有更多的标识符。

因此,进行简化处理,

 A->5

A 被转换成流 5,这个流将被转换成 C 令牌并发送到 C 编译器。


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