编译期间 C 中何时需要 space(或括号)?

When space (or parentheses) are required in C during compilation?

我正在学习编译的工作原理,我的最终目标是编写一个迷你 C 编译器。我还处于这个项目的开始阶段。当我在扫描器和解析器部分工作以构建 AST 时,我意识到 space 是(或圆括号)在 i+ +4i+(+4)、[=13= 这样的表达式中是必需的],或i-(-4)。否则,在 i--4 表达式(例如)中,-- 被解释为一元运算符 -- 并引发错误。我完全理解原因。这不是问题。
问题是下面的,之前,我天真地认为 spaces 在 C 中并不是那么重要,如果只是为了代码的可读性。但是现在,我想知道是否还有其他类似上述这些的例子?

大多数语言中的词法分析器都基于贪婪的正则表达式——一个标记是尽可能长的。

如果 ++ 可以解释为 ++ 运算符(从左到右),则它不会被解释为两个加号。如果 inta 可以解释为标识符,则不会将其解释为 int 后跟 a,等等

i+ +4 需要空格、括号或介于 ++ 之间的东西,否则词法分析器将贪婪地从左到右使用它,如 ++.

我不得不修复一些旧代码并进行更改

#define ALT_7      (0xfe+OFFSET)

#define ALT_7      (0xfe +OFFSET)

原因是 0xfe+OFFSET 是一个 预处理数字 标记,而不是人们可能天真地认为的三个标记。旧编译器将其解析为三个,但新编译器将其解析为一个无效数字常量,因此失败。

预处理器方面的内容可能更多,但更晦涩(作为 C/C++ 预处理的整个主题)。

C 中何时需要 space 的规则并未明确指定,而是 C 解析方式的结果。这方面的规则相当复杂,因为它们涉及多个分析阶段和各种情况的一些例外情况。如果您正在编写 C 编译器,则需要使用 C 标准作为参考。

C 2018 5.1.1.2 指定翻译阶段(重新措辞和总结,不是精确引用):

  1. 物理源文件多字节字符映射到源字符集。三字母序列被单字符表示取代。

  2. 合并以反斜杠继续的行。

  3. 源文件由字符转换为预处理标记和white-space字符——每个可以作为预处理标记的字符序列都转换为预处理标记,并且每个注释变成一个 space.

  4. 执行预处理(执行指令和扩展宏)。

  5. 字符常量和字符串文字中的源字符被转换为执行字符集的成员。

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

  7. 白色-space字符被丢弃。 “每个预处理令牌都被转换成一个令牌。生成的标记在句法和语义上进行分析,并作为翻译单元进行翻译。” (引用的文字是我们认为的 C 编译的主要部分!)

  8. 程序链接成为可执行文件。

首先,C 源代码中需要 spaces 的地方由阶段 3 控制,即预处理标记的形成。这在 C 2018 6.4 中指定。第 1 段给出了预处理标记的语法(更多内容见下文),第 4 段告诉我们:

If the input stream has been parsed into preprocessing tokens up to a given character, the next preprocessing token is the longest sequence of characters that could constitute a preprocessing token. There is one exception to this rule: header name preprocessing tokens are recognized only within #include preprocessing directives and in implementation-defined locations within #pragma directives. In such contexts, a sequence of characters that could be either a header name or a string literal is recognized as the former.

第 1 段告诉我们预处理标记是 header-nameidentifierpp-number 之一字符常量字符串文字标点符号,或非- white-space 不属于前面各项的字符。

然后 6.4 中的进一步子条款告诉我们这些标记是什么样的。

第 3 阶段针对需要 space 的地方引入了两个规则,它们本质上是:

  • 如果根据上述规则将源代码解析为一个预处理标记,您需要两个,那么您必须在您希望第一个标记结束的地方插入一个space。
  • 如果使用 /* 而不是 /* 来引入评论,请在它们之间添加一个 space。

阶段 4 引入了另一个规则。因为 6.10.3 3 说“在类对象宏的定义中标识符和替换列表之间应该有白色 space”,所以你需要一个 space 来区分类函数宏:

#define foo(x) (3*(x)) // Macro that acts on argument x.
#define foo (x)        // Macro that expands to `(x)`.

很多情况下确实需要空格:

  • 宏定义:

    #define MACRO (a)      // defines a simple macro, it expands to (a)
    #define MACRO(a)       // defines a function-like macro with a single parameter a
    
  • 注释语法陷阱:

    a/*b    // starts a comment */
    a/ *b   // a divided by the value pointed to by b
    
  • 预处理数字文字:

    0x2e+1  <>  0x2e +1
    
  • 三字母的类似问题:

    "??/??/????"  <>  "??" "/??" "/????"  // "??/??/????" is parsed as "\????"   
    
  • 令牌分离:

    a+ +b   <>  a++b   // a++b would be a syntax error
    a- -b   <>  a--b   // a--b would be a syntax error
    a& &b   <>  a&&b   // but &b is unlikely to be a valid operand for &
    
  • C++嵌套模板问题:

    template a<b<c> >
    

C 编译器有几个部分,问题是:您要实现哪个部分?

C 预处理器实际上为白色生成一个标记space 并使用它来确定事物。如果您要实现组合 preprocessor/compiler,您可能只想进行一次标记化,然后在将标记流交给编译器之前丢弃白色 space 标记。

C 本身似乎主要关注 spaces、制表符和换行符作为标记结束的指示符。

除此之外,它还有一个或两个字符运算符的概念,并且似乎在贪婪地匹配它们。也就是说,- - 会变成 MINUS_TOKEN, MINUS_TOKEN,而 -- 无论在哪里,总是会变成 DECREMENT

也就是说,您的 i--4 示例给出了解析器错误,因为在后缀递减运算符之后有一个无关的 4

所以证明运算符是贪婪匹配的。写 i - -4 OTOH 是可行的,因为贪婪匹配将 space 视为第一个 - 标记的结尾,并开始一个新的标记,然后产生第二个负号。

总而言之,C 本身忽略了标记化阶段之外的 whitespace,而预处理器则不会。