【问题标题】:Preprocessor reserved keywords预处理器保留关键字
【发布时间】:2021-03-10 19:03:18
【问题描述】:

对于宏,除了必须是标识符之外,还有其他名称限制吗?例如,以下内容是否有效?

#define assert getchar
#include <stdio.h>

int main(void)
{
    assert();
}

代码链接:https://godbolt.org/z/ra63na.

main:
        push    rbp
        mov     rbp, rsp
        mov     eax, 0
        call    getchar
        mov     eax, 0
        pop     rbp
        ret

预处理器是否了解 C 语言?还是更像是一个查找和替换程序?

【问题讨论】:

    标签: c c-preprocessor


    【解决方案1】:

    对于宏,除了必须是标识符之外,还有其他名称限制吗?

    是的,它们受语言规范(“保留标识符”)第 7.1.3 节的规定的约束,特别是:

    • 以下划线和大写字母或另一个下划线开头的所有标识符始终保留用于任何用途 [包括宏名称]。

    [...]

    • [标准库规范]子条款(包括未来的库方向)中的每个宏名称都保留给 如果包含任何关联的标头,则按指定使用;除非 另有明确说明

    [...]

    • 在任何 [标准库规范] 子条款中列出的具有文件范围的每个标识符(包括未来的库 方向)保留用作宏名称和标识符 具有相同名称空间中的文件范围(如果有任何关联) 包含标题。

    [...] 如果程序在上下文中声明或定义标识符 它被保留(7.1.4 允许的除外),或定义一个 保留标识符作为宏名称,行为未定义。

    如果第二个要点还包含assert.h 标头,则它尤其与您的示例代码相关。然后,标识符assert 将保留用作宏名称。您将其用作一个会触发未定义的行为。这并没有对实现提出任何特殊要求——事实上,这正是“未定义行为”的含义。它不需要实现接受代码,也不需要拒绝它,也不需要在任何一种情况下发出任何类型的诊断。如果它确实接受它,则不需要预处理器对assert 执行宏替换,也不会禁止这样做,事实上,它也不需要以任何方式看起来合理的行为或可预测的。

    如果您在包含stdio.h 的代码中将getchar 定义为宏名称,则类似示例将适用于第三个要点。但是,实际呈现的代码是可以的。

    你也问,

    预处理器是否了解 C 语言?或者是 它更像是一个查找和替换程序?

    一点。 C 预处理器不是一种通用的宏语言,并且尝试将其用作一种通常会很糟糕。预处理器的输入是一系列标记,根据与 C 语法一致的规则确定,它使用与 C 相同的标识符语法。条件包含指令识别 C 的算术表达式的子集,并且它们根据主机实现的整数数据类型之一工作。预处理器(或至少在它之前的标记化阶段)理解 C 字符串文字和字符常量,因此宏替换不会影响这些内容。

    【讨论】:

      【解决方案2】:

      标准 (C11) 的第 7.1.2 和 7.1.3 节对此进行了介绍。以下是一些与宏有关的规则:

      • 如果使用,标头应包含在任何外部声明或定义之外,并且应首先包含在对其声明的任何函数或对象或它定义的任何类型或宏的第一次引用之前。
      • 程序不得有任何名称与当前定义的关键字在包含标头之前或在标头中定义的任何宏展开时名称相同的宏。
      • 以下任何子条款中的每个宏名称(包括未来库 如果包含任何关联的标头,则保留用于指定用途; 除非另有明确说明。
      • 以下任何子条款中列出的具有文件范围的每个标识符(包括 未来的库方向)保留用作宏名称和标识符 如果包含任何关联的标头,则文件范围在同一名称空间中。

      所以您发布的确切程序是正确的,因为 &lt;assert.h&gt; 尚未包含在内。但是,如果您确实包含该标头,那将是未定义的行为。

      【讨论】:

        【解决方案3】:

        真的很蠢。它理解到足以进行令牌替换,但仅此而已。

        例如:#define test fail 将替换 test(...) 中的 test,但不会替换 tested"test"

        由于 C 具有非常基本的语法,因此编写一个可以处理和识别类似标记的解析器实际上并不难。让它理解 C 语法的全部内容超出了该工具的范围。

        换句话说,对于像这样的输入程序:

        #define test fail
        
        int main() {
          test(9, "test", tested());
        
          return 0;
        }
        

        C 预处理器将其分解为标记,最终类似于:

        [ "#", "define", "test", "fail" ]
        
        [ "int", "main", "(", ")", "{" ]
        [ "test", "(", "9", "\"test\"", "tested", "(", ")", ")", ";" ]
        ...
        

        其中每一个都使用简单的预处理器语法进行处理。

        这稍微复杂一些,因为宏可以包含参数,但你明白了。使用的文法是整个 C 文法的一个简单子集。

        【讨论】:

          【解决方案4】:

          是的,它是有效的。不,预处理器不支持语言。预处理器完全按照它的指示执行 - 包含内容,替换宏 - 如果这导致语法无效,编译器必须检测到。

          除 C 符号命名规则外,没有 C 语言依赖或保留字。所有预处理器指令都以 # 开头,这不是有效的 C 符号名称,因此不需要保留字。

          预处理器可以自行运行——通过编译器驱动程序的命令行选项或在 GUN 工具链中它是一个独立的可执行文件cpp——使其可用于除 C 和C++ 源代码预处理。

          【讨论】:

          • 预处理器能够自行运行并不是标准的要求——这只是一种常见的工具设置。预处理器的行为是由语言标准定义的,所以说它“不知道语言”是什么意思并不清楚。 C预处理器与C++预处理器不同,预处理器宏中的保留标识符有几条规则
          • @Clifford -- 我想不出一个例子来说明cpp 在C 之外如何有用。在c 预处理之外使用cpp 的用例示例是什么?
          • @David542,C 预处理器历来被用作其他一些具有足够句法相似性的语言的预处理器。尤其是 Fortran。
          • @David542:真的吗?您想在另一个文件中插入一个文件的内容 (#include) 或用其他一些文本 (#define) 以参数方式替换宏或有条件地包含或执行文本部分 (#if / #elif/ @987654329 @):makefile,汇编代码,应用很多。它只是一个文本预处理器,其中没有特定于 C 的内容。在某些情况下,sedAWK 之类的工具可能更合适,但这是一个学习曲线。
          • @JohnBollinger 我不确定“句法相似性”是否必要。它甚至不需要是一种编程语言。它几乎必须是文本。需要注意的是,在行首的 # 前面有零个或多个空格是保留的,所以没有任何东西已经以这种方式使用 #
          猜你喜欢
          • 1970-01-01
          • 2011-05-06
          • 1970-01-01
          • 2014-09-07
          • 2012-10-22
          • 2016-07-11
          • 1970-01-01
          • 2013-11-13
          • 1970-01-01
          相关资源
          最近更新 更多