【问题标题】:Using Backslash-Newline outside of a macro in C在 C 中的宏之外使用反斜杠换行符
【发布时间】:2021-05-25 01:07:22
【问题描述】:
int main(){\
 int a = 5;\
 return a;\
}

以上编译正常。我假设 C 预处理器在编译之前删除了反斜杠?

output of gcc -E:

int main(){
 int a = 5;
 return a;}

似乎并非所有\n(新行)字符都像使用宏一样被删除,它只是主要删除了反斜杠。

我已经看到这用于多行宏,例如:

#define TEST(in)\
 int a = in;    \
 int b = 6;

int main(){
 TEST(5)
 return 0;
}

output of gcc -E:

int main(){
 int a = 5; int b = 6;
 return 0;
}

预处理将删除上面示例中的反斜杠以及\n 字符,但为什么在我的第一个示例中它没有删除所有换行符?

【问题讨论】:

  • 我认为 c 预处理器没有任何实际要求来确保在输出中包含或删除换行符。
  • 请注意,gcc -E 的行为没有被标准指定,它是 gcc 实现的产物。该标准仅根据原始源(预处理之前)指定二进制文件的行为
  • 我相信它正在尝试以保留错误消息的行数的方式扩展反斜杠换行符。它发出换行符,以便解析器在编译第 n 行时看到 n-1 个换行符。
  • 如果您使用gcc -P -E 而不仅仅是-E,您将在一行中获得所有预处理输出。 -P 表示“不保留有关令牌原始源位置的信息”;当您运行 C 预处理器而不打算将输出提供给 C“适当的编译器”(又名“翻译阶段 7”)时,这通常是您想要的。

标签: c c-preprocessor


【解决方案1】:

“拼接”——反斜杠换行序列——在预处理器处理程序文本之前被删除。至少理论上是这样,记住 C 标准实际上并没有定义一个称为“预处理器”的过程。

它确实定义了一个过程,用于将程序文本转换为可以解析的标记流,然后将其转换为可执行文件。该过程由八个翻译阶段组成,编译器必须产生与每次执行一个阶段所产生的结果相同的结果,每个阶段都将前一个阶段的输出作为输入。 (大多数输入和输出是标记流,而不是字符串。因此,使用 -E 标志运行时 GCC 产生的输出与标准中的任何内容都不对应,从而允许 GCC 基本上产生它认为方便的任何输出. 或者它的作者认为你会觉得方便。)

“as if”子句意味着特定编译器可以组合阶段或分段执行它们,只要它不改变结果。因此,您实际上只能将过程视为算法的抽象描述。不过,理解还是有用的。全文见标准§5.1.1.2

对阶段的高度浓缩和注释描述,不完整且在细节上有些不精确,希望它比标准中的语言更容易消化。但请阅读原文。

  1. 删除三元组(现在已弃用,因此如果您不知道它们是什么,请不要担心),如有必要,将程序文本转换为编译器所需的任何字符编码。

  2. 去除接头。所有的反斜杠换行序列都被简单地从程序文本中删除,没有留下任何东西。 (好吧,理论上是这样。实际上,大多数编译器仍然知道每一位文本的原始源代码行号。但这些信息仅用于产生诊断。)

  3. 将文本拆分为标记和空白序列,并将所有 cmets 替换为单个空格字符。

  4. “执行预处理指令,扩展宏调用,并执行_Pragma 一元运算符表达式”。这与定义预处理器的标准一样接近,因此可以合理地说“预处理器”是阶段 4 的执行。#include 指令是预处理器指令,处理包含指令从传递包含的文件开始在将其插入到令牌流中进行进一步预处理之前的阶段 1-3。

  5. 将字符和字符串文字中的所有转义序列替换为将在执行期间使用的实际字符(可能是宽字符)。

  6. 连接相邻的字符串文字。

  7. 删除所有空格,只留下标记。将预处理标记转换为句法标记。解析生成的令牌流并将其转换为“翻译单元”。或者,换句话说,将程序编译成一个目标文件(尽管这比标准中的语言更具体)。

  8. 将所有翻译单元和必要的库模块组合到一个可执行映像中。通俗地说,这是链接阶段,结果可以交给操作系统执行。

这就是标准的要求。但是现实世界的编译器会很多做其他事情,比如生成或多或少可读的错误消息;以可能使其执行更快和/或占用更少空间的方式重新排列代码;将调试信息插入可执行文件;并生成用户要求的任何附加分析和报告(没有一个是标准化的)。例如,这包括-E 和/或-S 输出。编译器做这些事情是为了帮助你,它们有助于理解你的程序的编译方式。但你不应该把它们当真,因为编译过程的官方结果是实际的可执行文件。

大多数编译工具链也可以生成库,因此并非所有程序都立即完全处理成可执行映像。但这是唯一标准化的结果。尽管该标准提到了库,尤其是标准库,但它并没有对库是如何存在的做出任何假设。

标准库(和头文件)甚至不必存在于文件系统中;编译器识别它们的名字并做出适当的响应就足够了。标准库必须实现的一些东西不能用可移植的 C 语言编写,因此标准库源代码(如果存在)很可能并非全部采用标准 C 程序的形式。标准库头文件可能包含接受编译器特殊处理的结构,因此不能被其他编译器使用或直接复制到您的程序中。

这似乎太悬而未决,但其目的是让 C 实现在极其有限的处理器上运行成为可能,包括根本没有任何外部存储的处理器。 (而且针对嵌入式系统仍然很常见,因为它可能会遗漏很多您通常认为是理所当然的事情。)而且,总的来说,这些年来它为我们提供了很好的服务。

【讨论】:

    【解决方案2】:

    C 标准没有像gcc -E 那样指定如何使用文本输出执行预处理。编译器尝试生成文本输出,这些输出可以反馈给编译器以生成相同的程序。只要产生相同的标记,空白细节在很大程度上与此无关。在您的示例中, gcc 输出可读文本,其中转义换行符只要被空白包围,就会作为换行符输出。这个可选的:转义的换行符实际上应该在早期阶段从输入中完全删除,从而允许重构在单独的行上断开的标记。

    这是一个病态的例子:

    #inc\
    lude\
     <st\
    dio.\
    h>
    int \
    main\
    () {\
    retu\
    rn 0\
    ; }
    

    关于定义跨越多行且带有转义换行符的宏,它们的扩展由 C 标准精确描述:空格和 cmets 序列必须由单个空格替换。

    这是一个说明性示例:

    int main(){\
        int a = 5;\
        return a;\
    }
    
    #define TEST() int main(){\
        int a = 5;\
        return a;\
    }
    
    #define XSTR(x) #x
    #define STR(x) XSTR(x)
    
    TEST()
    
    STR(TEST())
    

    gcc -E的输出:

    # 1 "prepmain.c"
    # 1 "<built-in>"                                                                                                                  # 1 "<command-line>"
    # 1 "/usr/include/stdc-predef.h" 1 3 4
    # 1 "<command-line>" 2
    # 1 "prepmain.c"                                                                                                                  int main(){
        int a = 5;
        return a;}                                                                                                                    # 14 "prepmain.c"
    int main(){ int a = 5; return a;}                                                                                                                                                                                                                                   "int main(){ int a = 5; return a;}"
    

    gcc -E -P的输出:

    int main(){ int a = 5; return a;}
    int main(){ int a = 5; return a;}
    "int main(){ int a = 5; return a;}"
    

    【讨论】:

    • 不完全。 “除换行符之外的每个非空空白字符序列是否被保留或替换为一个空格字符是实现定义的。” (5.1.1.2)。单个空格规则仅适用于字符串化运算符。
    • 供参考:6.10.3.2/2:“参数的预处理标记之间的每次出现的空格都成为字符串文字中的单个空格字符。第一个预处理标记之前和之后的空格构成参数的最后一个预处理标记被删除。"
    • 没关系。除了字符串化之外,没有其他标准机制可以让您观察空白。
    猜你喜欢
    • 2013-03-16
    • 2018-02-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-11-04
    相关资源
    最近更新 更多