宏展开出现奇怪结果

3
考虑以下代码片段。
#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;
}

输出

The value of A is 5

但是这段代码本不应该编译通过,因为展开后它应该像这样:printf("The value of A is %d\n", --5);,然后会提示编译错误,说需要lvalue。难道不是这样吗?


1
@KeineLust,这并不是因为--绑定更强,只能应用于lvalue。否则,使用--将是不可能的,因为它总是可以被解释为- - - Amin Negm-Awad
@KeineLust 试着把这个交给编译器,看看会发生什么。试试吧。 - Art
@Art,Amin,你们说得对,在我的电脑上,这个宏展开为“- - 5”,符号之间有一个空格。OP,如果你使用的是gcc编译器,可以使用“cpp app.c”命令来验证。 - David Ranieri
它展开为- -5(注意空格)。运行预处理器并查看输出。 - Petr Skocik
1
@KeineLust 不要测试它,请记住它是“递减”运算符。 - Suraj Jain
@SurajJain 是的,我在之前的评论中被 Amin 和 Art 告知了 ;) - David Ranieri
4个回答

4

我不这么认为。即使宏展开是文本处理,也不可能从宏边界创建一个标记。因此它是-(-5),而不是--5,因为--是一个单独的标记。


4
预处理器在展开 BC 之间加入了一个空格:
#define A -B
#define B -C
#define C 5
A

使用以下命令生成输出结果 (cpp < test.c)

# 1 "test.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 329 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "test.c" 2



- -5

这真的很好,但前7行实际上是什么意思,以及cpp < test.c如何输出预处理文本? - Abhisek
@Abhisek,C编译器通常会在你看不见的情况下调用它。额外的行是由编译器用来跟踪原始源代码行号和类似的东西的。 - Alnitak

4

将-E选项传递给它(例如:gcc -E a.c)。这将输出预处理后的源代码。

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

所以它会在“-”和“-5”之间引入一个空格,因此它不会被视为递减运算符“--”,因此“printf”将打印5。
GCC文档关于标记间距提供了有关为什么会产生额外空格的信息:
首先,考虑一个仅涉及独立预处理器的问题:需要保证重新读取其预处理输出会产生相同的标记流。如果不采取特殊措施,由于宏替换,这可能不是这种情况。例如:
 #define PLUS +
 #define EMPTY
 #define f(x) =x=
 +PLUS -EMPTY- PLUS+ f(=)
         ==> + + - - + + = = =
 not
         ==> ++ -- ++ ===

一个解决方案是在所有相邻的标记之间简单地插入一个空格。然而,我们希望将空格插入到最少,既出于美学原因,也因为它会给那些仍然试图滥用预处理器来进行Fortran源代码和Makefiles等操作的人带来问题。现在,只需要注意当标记被添加(或删除,如EMPTY示例所示)到原始词法标记流中时,我们需要检查意外的标记粘贴。我们称这个过程为粘贴避免。标记添加和删除只能发生在宏扩展时,但意外的粘贴可以发生在许多地方:在每个宏替换之前和之后,在每个参数替换之前和之后,以及在由#和##运算符创建的每个标记之前和之后。

2
我也看过这个,但我没有看到空格被引入或者结果字符串没有宏间标记的规则。 - Amin Negm-Awad
@AminNegm-Awad 对不起,我发布了错误的内容,请重新检查。 - Suraj Jain
区别在于,我不认为标准本身需要“空格粘贴”。这是一项实现细节。然而,这对于问题并没有任何影响。 - Amin Negm-Awad
你能否编辑答案,包括这个实现细节的事情?非常感谢。 - Suraj Jain

2
在C语言中,程序源代码在翻译的早期阶段(第3阶段)被分成所谓的“预处理标记”,在宏替换发生之前(第4阶段)。稍后(在第7阶段),“预处理标记”将转换为常规的“标记”,并被馈入编译器本身的语法和语义分析器中(请参见语言规范中的“5.1.1.2翻译阶段”)。
第3阶段是未来C语言运算符和其他词汇元素的预处理标记形成的阶段(标识符、数字、标点符号、字符串文字等)。像--、>>=等多字符标点符号就是在那个早期阶段形成的。为了最终在第7阶段获得--运算符的标记,您需要在第3阶段早期将其作为完整的标点符号。从预处理标记到常规标记的过渡时不会发生额外的标点符号连接,这意味着在第3阶段检测到的两个相邻的-标点符号不会在第7阶段变成一个单独的--标记。编译器本身永远不会有机会看到这两个相邻的-和一个单一的--标记。
换句话说,在C语言中,您不能使用预处理器将东西连接在一起。这就是为什么预处理器有专门的功能,如##来方便连接。而##就是您必须使用的将两个标记合并成一个标记的方法。
顺便说一下,通过声称预处理器会在您的-字符之间放置空格字符来解释这种行为是不正确的。语言规范中没有这样的内容。真正发生的是,在编译器的内部结构中,您的-标记永远保持为两个单独的标记。预处理器和编译器如何实现这一点是它们的内部实现细节。在预处理器和编译器本身松散耦合的实现中(例如完全独立的模块通过中间文本表示进行通信),在相邻的标点符号之间注入空格是实现所需标记分离的自然方式。

GCC文档中提到了以下内容:https://gcc.gnu.org/onlinedocs/cppinternals/Token-Spacing.html - Suraj Jain
@Suraj Jain:这很好,但正如我上面所说的,这只是GCC编译器的一个实现细节。C语言本身并没有类似的东西。 - AnT stands with Russia
哦,那解决了OP的问题吗?实际上我搜索过了,我并没有真正地密集使用宏。你可以编辑我的答案来支持你所说的。 - Suraj Jain

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