C 标准没有指定这种行为,因为预处理阶段的输出只是一个标记和空格流。将令牌流序列化回字符串,这就是gcc -E
标准没有要求,甚至没有提及,并且不构成标准指定的翻译过程的一部分。
在第 3 阶段,程序“被分解为预处理标记和空白字符序列”。除了忽略空格的连接运算符和保留空格的字符串化运算符的结果之外,标记也被固定,并且不再需要空格来分隔它们。但是,需要空格才能:
流中的空白元素直到第 7 阶段才会被消除,尽管它们在第 4 阶段结束后不再相关。
Gcc 能够生成对程序员有用的各种信息,但与标准中的任何内容都不对应。例如,翻译的预处理器阶段还可以使用以下之一生成可用于插入 Makefile 的依赖信息:-M
选项。或者,可以使用以下命令输出编译代码的人类可读版本-S
选项。并且可以使用以下命令输出预处理程序的可编译版本,大致对应于第 4 阶段生成的令牌流-E
选项。这些输出格式都不受 C 标准控制,C 标准只关心实际执行程序。
为了生产-E
输出时,gcc 必须以不改变程序语义的格式序列化标记和空格流。在某些情况下,如果流中的两个连续标记没有彼此分开,它们将被错误地粘合在一起成为单个标记,因此 gcc 必须采取一些预防措施。它实际上无法将空格插入正在处理的流中,但是当它响应于呈现流时,没有什么可以阻止它添加空格gcc -E
.
例如,如果您的示例中的宏调用被修改为
A(A(0x40E))
那么令牌流的天真输出将导致
(10+(10+0x40E+20)+20)
无法编译,因为0x40E+20
是单个 pp-number 标记,无法转换为数字标记。之前的空间+
防止这种情况发生。
如果您尝试将预处理器实现为某种字符串转换,那么毫无疑问您将在极端情况下遇到严重的问题。正确的实现策略是首先按照标准中的指示进行标记化,然后将第 4 阶段作为标记和空白流上的函数执行。
字符串化是一个特别有趣的情况,其中空格影响语义,它可以用来查看实际的令牌流是什么样子。如果将以下扩展字符串化A(A(40))
,您可以看到实际上没有插入空格:
$ gcc -E -x c - <<<'
#define Y 20
#define A(x) (10+x+Y)
#define Q_(x) #x
#define Q(x) Q_(x)
Q(A(A(40)))'
"(10+(10+40+20)+20)"
字符串化中空格的处理由标准精确指定:(§6.10.3.2,第 2 段,非常感谢 John Bollinger 找到了规范。)
参数的预处理标记之间每次出现的空格
成为字符串文字中的单个空格字符。构成参数的第一个预处理标记之前和最后一个预处理标记之后的空白将被删除。
这是一个更微妙的示例,其中需要额外的空格gcc -E
输出,但实际上并未插入到令牌流中(再次通过使用字符串化来生成真正的令牌流来显示。)I
(identify) 宏用于允许将两个令牌插入到令牌流中,而无需插入空格;如果你想使用宏来组成参数,这是一个有用的技巧#include
指令(不推荐,但可以做到)。
也许这对于您的预处理器来说可能是一个有用的测试用例:
#define Q_(x) #x
#define Q(x) Q_(x)
#define I(x) x
#define C(x,...) x(__VA_ARGS__)
// Uncomment the following line to run the program
//#include <stdio.h>
char*quoted=Q(C(I(int)I(main),void){I(return)I(C(puts,quoted));});
C(I(int)I(main),void){I(return)I(C(puts,quoted));}
这是 gcc -E 的输出(只是最后的好东西):
$ gcc -E squish.c | tail -n2
char*quoted="intmain(void){returnputs(quoted);}";
int main(void){return puts(quoted);}
在从阶段 4 传出的令牌流中,令牌int
and main
不被空格分隔(也不是return
and puts
)。字符串化清楚地表明了这一点,其中没有空格分隔标记。但是,即使显式传递,程序也可以正常编译和执行gcc -E
:
$ gcc -E squish.c | gcc -x c - && ./a.out
intmain(void){returnputs(quoted);}
并编译输出gcc -E
.
不同的编译器和同一编译器的不同版本可能会产生预处理程序的不同序列化。所以我认为你不会找到任何可以通过逐个字符比较来测试的算法-E
给定编译器的输出。
最简单的序列化算法是无条件输出两个连续标记之间的空格。显然,这会输出不必要的空格,但它永远不会在语法上改变程序。
我认为最小空间算法是在令牌中最后一个字符的末尾记录 DFA 状态,以便以后如果存在从第一个令牌末尾的状态开始的转换,则可以在两个连续令牌之间输出空格在以下标记的第一个字符上。 (将 DFA 状态保留为令牌的一部分与将令牌类型保留为令牌的一部分并没有本质上的不同,因为您可以从 DFA 状态的简单查找中派生出令牌类型。)该算法不会在后面插入空格40
在你原来的测试用例中,但它会在后面插入一个空格0x40E
。所以它不是您的 gcc 版本使用的算法。
如果使用上述算法,则需要重新扫描由令牌串联创建的令牌。但是,无论如何这是必要的,因为如果串联结果不是有效的预处理标记,则需要标记错误。
如果您不想记录状态(尽管,正如我所说,这样做基本上没有任何成本)并且您不想通过在输出令牌时重新扫描令牌来重新生成状态(这也非常便宜) ),您可以预先计算一个由标记类型和后续字符键入的二维布尔数组。计算本质上与上面相同:对于返回特定令牌类型的每个接受 DFA 状态,在数组中输入该令牌类型的真值以及任何从 DFA 状态转换的字符。然后,您可以查找令牌的令牌类型和后续令牌的第一个字符,以查看是否需要空格。该算法不会产生最小间距的输出:例如,它会在40
在你的例子中,因为40
is a pp-number
对于某些人来说这是可能的pp-number
可以扩展为+
(即使你不能延长40
以这种方式)。所以 gcc 可能使用该算法的某个版本。