【问题标题】:When space (or parentheses) are required in C during compilation?编译期间 C 中何时需要空格(或括号)?
【发布时间】:2019-03-31 10:29:23
【问题描述】:

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

【问题讨论】:

  • 是的,例如你不能写intmainreturn0。它必须是 int main / return 0int(main) / return(0)
  • 我同意这些明显的例子。

标签: c parsing compiler-construction flex-lexer


【解决方案1】:

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

#define ALT_7      (0xfe+OFFSET)

#define ALT_7      (0xfe +OFFSET)

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

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

【讨论】:

  • 这通常不能回答问题。它给出了一个有限的实例,这不足以解释一般的 C 解析,它没有解释实例(如解释它如何适合 C 解析或由标准指定)。
【解决方案2】:

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

  • 宏定义:

    #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> >
    

【讨论】:

    【解决方案3】:

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

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

    1. 物理源文件多字节字符映射到源字符集。三元组序列被单字符表示替换。

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

    3. 源文件从字符转换为预处理标记和空白字符 - 每个可以作为预处理标记的字符序列都转换为预处理标记,每个注释变成一个空格。

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

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

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

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

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

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

    如果输入流已被解析为预处理标记直到给定字符,则下一个预处理标记是可以构成预处理标记的最长字符序列。此规则有一个例外:标头名称预处理标记仅在#include 预处理指令内和#pragma 指令内的实现定义位置中被识别。在这种情况下,可以是标题名称或字符串文字的字符序列被识别为前者。

    第 1 段告诉我们预处理标记是 header-nameidentifierpp-numbercharacter- 之一常量string-literal标点符号,或不是上述各项之一的非空白字符。

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

    第 3 阶段针对您需要空间的地方引入了两条规则:

    • 如果将根据上述规则将源代码解析为一个预处理标记,您需要两个标记,那么您必须在第一个标记结束的位置插入一个空格。
    • 如果使用/* 而非/* 来引入评论,请在它们之间留一个空格。

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

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

    【讨论】:

      【解决方案4】:

      大多数语言中的词法分析器都基于贪婪的正则表达式——一个标记只要它可以。

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

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

      【讨论】:

      • 这不是一个有用的答案,因为它使用行话而不解释它:“Lexers”、“regexes”、“hungry”、“token”。此外,它没有提供任何可以解决++“可以解释”为++的信息。
      • 感谢您的回答,但这不完全是问题。我写了我理解为什么会这样解释。这个问题涉及我应该注意的另一个例子的存在。
      • @EricPostpischil 这个问题被标记为flex-lexer,所以说需要解释术语“词法分析器”就像是说Java问题的答案需要首先解释Java是什么。我认为正则表达式也是如此(因为它们是flex 的基本概念)。并且“饥饿”在使用它的同一句子中进行了解释:“a token is as long as it can be”(尽管我从未听说过这个词,并认为它可能是对“贪婪”的误译)。
      【解决方案5】:

      C 编译器有几个部分,问题是:你在实现哪一个?

      C 预处理器实际上为空格生成一个标记,并使用它来确定事物。如果您正在实现组合的预处理器/编译器,您可能希望只进行一次标记化,然后在将标记流交给适当的编译器之前丢弃空白标记。

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

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

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

      这样就证明了算子是贪婪匹配的。写入 i - -4 OTOH 有效,因为贪心匹配将空间视为第一个 - 令牌的结束,并开始一个新的,然后产生第二个减号。

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-09-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-24
        相关资源
        最近更新 更多