为什么预处理器没有展开这个C或C++宏?

32

有人能为我指出使用gcc 4.1.0编译时代码中的问题吗。

#define X 10
int main()
{
  double a = 1e-X;
  return 0;
}

我遇到了错误:指数没有数字。

当我用10替换X时,它可以正常工作。我使用了g ++ -E命令检查应用了预处理器的文件,但它没有将X替换为10。 我原本认为预处理器会自动将文件中定义的每个宏替换为其对应的文本,而不考虑上下文。这样理解对吗?

我知道这可能是一个非常愚蠢的问题,但我很困惑,我宁愿变得愚蠢也不愿困惑 :)。

是否有任何评论或建议?


4
实际上这是一个非常好的问题 - 底层问题需要深入了解编译器的工作原理。 - sharptooth
有点遗憾的是关于这个问题的标题,肯定有人可以编辑它吧? - James Morris
1
@james.. 其实当我开始写这个问题时,我认为它是一个相当愚蠢的问题,因此无法想出一个合适的标题。感谢编辑问题标题的Jonathan。 - Atul
1
宁愿傻一点也不要感到困惑,这是程序员的信条。 - René Nyffenegger
4个回答

19

预处理器不是文本处理器,它在令牌级别上工作。在您的代码中,在定义之后,每个出现的令牌X将被替换为令牌10。但是,您的代码中其余部分中没有X令牌。

1e-X在语法上无效,不能转换为令牌,这基本上就是错误提示所说的(它说要使其成为有效的令牌 - 在此情况下为浮点字面量 - 您必须提供有效的指数)。


16
当你写成1e-X这样的形式时,X并不是一个独立的符号,供预处理器替换 - 必须在两侧加上空格(或某些其他符号)。仔细思考一下,你就会明白为什么了.. :)

编辑: "12-X"是有效的,因为它被解析为三个单独的标记:"12", "-", "X"。 "1e-X"不能像那样分割,因为"1e-"本身不形成一个有效的标记,正如Jonathan在他的答案中提到的那样。

至于解决问题的方法,您可以使用标记连接:

#define E(X) 1e-##X
int main()
{
  double a = E(10); // expands to 1e-10
  return 0;
}

2
这并没有真正解释为什么在这种情况下“-”不被视为“某些其他符号”之一,特别是当“int y = 12-X;”可以正常工作时。 - jamesdlin
jamesdlin提出的观点是有效的。有人知道为什么y=12-X有效,但1e-X无效吗? - Atul
感谢您的解释。我选择了您的答案作为采纳答案 :)。 - Atul
当涉及到像0x1E-FOO这样的数量时,情况变得非常有趣,其中0x1E将形成一个有效的标记(不像上面的裸1e),但编译器仍然会出错。 - supercat

11

有几个人说1e-X被词法分析为单个标记,这部分正确。简单解释一下:

在转换期间,有两类token:预处理token和token。源文件最初被分解为预处理token;这些token随后用于所有预处理任务,包括宏替换。在预处理之后,每个预处理token都转换为token;这些生成的token在实际编译过程中使用。

预处理token的类型比token的类型少。例如,关键字(如forwhileif)在预处理阶段不重要,因此没有关键字预处理token。关键字仅作为标识符词法分析。当从预处理token转换为token时,检查每个标识符预处理token;如果它与关键字匹配,则转换为关键字token,否则转换为标识符token。

在预处理期间只有一种数字token:预处理数字。这种预处理token对应于两种不同类型的token:整型字面量浮点字面量

预处理数字预处理token的定义非常广泛。实际上,它匹配任何以数字或小数点开头后跟任意数量的数字、非数字(例如字母)和 e+e-的字符序列。因此,以下所有内容都是有效的预处理数字预处理token:

1.0e-10
.78
42
1e-X
1helloworld

前两个可以转换为浮点字面量;第三个可以转换为整型字面量。后两个不是有效的整型字面量或浮点字面量;这些预处理标记不能转换成标记。这就是为什么你可以在没有错误的情况下预处理源代码,但不能编译它:错误发生在从预处理标记到标记的转换中。


1
事实上,正是因为1e-是一个pp-number(虽然不是浮点字面量),所以在被接受的答案中#define E(X) 1e-##X起作用。 - avakar

6

GCC 4.5.0同样不改变X。

答案在于预处理器如何解释预处理标记以及“最大匹配”规则。 “最大匹配”规则是决定将“x+++++y”视为“x ++ ++ + y”(因而是错误的),而不是视为合法的“x ++ + ++ y”的规则。

问题是为什么预处理器会将“1e-X”解释为一个单独的预处理标记。显然,它将把“1e-10”视为单个标记。除非“1e-”后面跟随数字,否则它通过预处理器后就不存在有效的解释了。所以,我猜测预处理器将“1e-X”视为单个标记(实际上是错误的)。但我没有分析标准中的正确条款来确定是否需要这么做。但标准中“预处理数”或“pp-number”的定义(见下文)与整数或浮点常量的定义略有不同,并允许许多作为整数或浮点常量无效的“pp-number”。

如果有帮助的话,“cc-E-v soq.c”的Sun C编译器的输出结果如下:

# 1 "soq.c"
# 2
int main()
{
"soq.c", line 4: invalid input token: 1e-X
  double a =  1e-X ;
  return 0;
}
#ident "acomp: Sun C 5.9 SunOS_sparc Patch 124867-09 2008/11/25"
cc: acomp failed for soq.c

所以,至少有一个C编译器在预处理器中拒绝该代码 - 可能是因为GCC预处理器有点松散(我尝试使用gcc -Wall -pedantic -std=c89 -Wextra -E soq.c来引起它的抱怨,但它没有发出任何声音)。而在宏和“1e-XXX”符号表示法中使用3个X表明,所有三个X都被GCC和Sun C编译器消耗了。

C标准定义的预处理数字

从C标准 - ISO/IEC 9899:1999 §6.4.8预处理数字:

pp-number:
    digit
    . digit
    pp-number digit
    pp-number identifier-nondigit
    pp-number e sign
    pp-number E sign
    pp-number p sign
    pp-number P sign
    pp-number .

鉴于此,'1e-X' 是一个有效的 'pp-number',因此 X 不是单独的标记('1e-XXX' 中的 'XXX' 也不是单独的标记)。因此,预处理器无法扩展 X;它不是可扩展的单独标记。

1
它不把 1e-X 视为单个标记。实际上,到达 X 时它就停止编译,并意识到无法形成有效的标记。 - avakar
根据经验(由于GCC预处理器不将X视为宏而是将其视为“1e-X”的一部分,而不报告错误),这可能是GCC预处理器中的一个错误。Sun预处理器确定1e-X是无效的标记,仍然不会将X翻译成10-但会报告错误。如果预处理器没有将X视为“1e-X”的一部分,则它错误地未能将X翻译成10,这就是问题的起源。是的,这是格式错误的代码-我认为这是不可否认的。但在我看来,预处理器必须将“1e-X”视为一个标记。 - Jonathan Leffler
@avakar @Jonathan:在预处理期间,1e-X是一个有效的_预处理记号_。它只是不能被转换成一个_记号_。我已经发布了一篇带有解释的答案。 - James McNellis

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