C预处理器先剥离注释还是先展开宏?

64

考虑这个(可怕的,糟糕的,没有好东西的)代码结构:

#define foo(x) // commented out debugging code

// Misformatted to not obscure the point
if (a)
foo(a);
bar(a);

我看到两个编译器的预处理器在这段代码上生成了不同的结果:

if (a)
bar(a);

if (a)
;
bar(a);

显然,这对于可移植的代码库来说是一件不好的事情。

我的问题是:预处理器应该怎么处理它?首先省略注释还是先展开宏?


5
好问题 - 让我费了些力气查找实际的真实信息 :) - Reed Copsey
使用“#define foo(x)##”来创建一个更安全的空白宏。...(或者是###? : /) - Pod
顺便问一下,你在第一个例子中使用的编译器是什么?我很确定它会破坏很多代码——即使只在#define中使用/* */注释可能是明智的,但我的印象是我看到了大量使用'//'注释的情况。 - Michael Burr
1
预处理器不理解 // 注释,但编译器理解吗?请记住,最初 C 只能理解 /* */ 注释,而 // 是 C++ 的扩展。我认为 C 只在 C99 中引入了 //。 (我的历史记忆是否正确?)实际上,无论您使用哪个编译器,我都很想看看它如何处理 /* - Aaron McDaid
6个回答

40
很遗憾,原始的ANSI C规范在第4节中明确排除了任何预处理器功能。(“此规范仅描述C语言。它不提供库或预处理器。”) C99规范明确处理了这个问题。在“翻译阶段”中,注释被替换为一个空格,这发生在预处理指令解析之前。(详见第6.10节) VC ++GNU C编译器都遵循这个范例 - 如果旧编译器不符合要求,但如果它符合C99规范,则应该是安全的。

10
抱歉,但是你链接的不是 ANSI C 规范;实际的规范在 2.1.1.2 节中描述了翻译阶段;我不久前发布了一个关于这些阶段的概述:https://dev59.com/yXM_5IYBdhLWcg3wNgOZ#1479972 - Christoph
是的 - 不太确定。我总是使用(大多数)符合C99标准的编译器。不过看起来OP正在使用C99编译器,因为在C89中//作为注释是不存在的。 - Reed Copsey
我遇到的许多C编译器即使不支持C99的其他任何功能,也支持C ++ / C99的“//”注释。 - Michael Burr
@Novelocrat:就像我说的,如果你使用的是C99,那么你是安全的。不过,从技术上讲,有很少几个完全兼容C99的编译器(例如,MS和GNU都不是100%兼容)。 - Reed Copsey

12

此处复制粘贴的C99标准翻译阶段描述所述,消除注释(它们被替换为单个空格)发生在第3个翻译阶段,而预处理指令在第4个阶段处理和宏扩展。

在C90标准中(我只有纸质版,因此无法复制粘贴),这两个阶段按相同顺序发生,尽管翻译阶段的描述在某些细节上与C99标准略有不同-注释被删除并替换为单个空格字符,然后再处理预处理指令和宏扩展。

同样,C++标准将这2个阶段按相同顺序发生。

至于如何处理'//'注释,C99标准如下(6.4.9/2):

除了在字符常量、字符串文字或注释内部外,字符//引入一个注释,其中包括所有多字节字符,但不包括下一个新行字符。

C++标准如下(2.7):

字符//开始一个注释,该注释以下一个新行字符结束。

所以你的第一个例子显然是该翻译者的错误 - 在展开foo()宏时,应该保留foo(a)后面的';'字符 - 注释字符不应成为the foo()宏的“内容”的一部分。
但是,由于你面对的是有缺陷的翻译器,你可能需要更改宏定义为:
#define foo(x) /* junk */

为了解决这个 bug,可以采用以下方法。

然而(我有些跑题了……),由于行拼接(即在换行符前加上反斜杠)会在注释处理之前进行,你可能会遇到以下这段不好的代码:

#define evil( x) printf( "hello "); // hi there, \
                 printf( "%s\n", x); // you!



int main( int argc, char** argv)
{
    evil( "bastard");

    return 0;
}

这可能会让写这段代码的人感到惊讶。

或者更好的方法是尝试下面这个,由某个喜欢盒式注释的人(肯定不是我!)编写:

int main( int argc, char** argv)
{
                            //----------------/
    printf( "hello ");      // Hey, what the??/
    printf( "%s\n", "you"); // heck??         /
                            //----------------/
    return 0;
}

根据您的编译器默认是否处理三字符序列(编译器应该处理,但由于三字符序列会让几乎所有遇到它们的人感到惊讶,因此一些编译器决定默认关闭它们),您可能会得到或者不会得到所需的行为 - 当然,无论那种行为是什么。


或者将注释移到#define行之外。 - jmucchiello
2
问题的要点是 junk 是实际的代码,在调试时被注释掉了,现在不再使用。 - Phil Miller
这个答案非常好,包含了真正帮助我的真理宝石 - 在宏中使用 /* */ 风格的注释,我就安全了。 - Phil Miller

5
根据MSDN,在令牌化阶段,注释被替换为单个空格,这发生在宏展开之前的预处理阶段。

4

不要在宏定义中使用//注释。如果必须添加注释,请使用/* */。此外,您的宏定义中存在错误:

#define foo(x) do { } while(0) /* junk */

这样,foo始终可以安全使用。例如:
if (some condition)
    foo(x);

无论foo是否定义为某个表达式,都不会抛出编译器错误。

“junk” 是有时作为宏体的代码。我的前任并不是那么小心。 - Phil Miller
你能否澄清一下你的意思?没有具体的例子很难给出建议。 - Vitali

2
#ifdef _TEST_
#define _cerr cerr
#else
#define _cerr / ## / cerr
#endif
  • 将适用于某些编译器(VC++)。当未定义_TEST_时,

    _cerr ...

    将被替换为注释行

    // cerr ...


1

我记得符合要求需要三个步骤:

  1. 去除
  2. 扩展宏
  3. 再次去除

这样做的原因是编译器能够直接接受 .i 文件。


非常好的观点 - 对于多阶段预处理可能会变得复杂(大多数人完全避免使用“包含保护”,但在依赖关系解析方面具有一些有趣的优势)。 - John P

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接