ChatGPT解决这个技术问题 Extra ChatGPT

预处理器如何处理循环依赖?

我想知道 C 预处理器如何处理循环依赖(#defines)。这是我的程序:

#define ONE TWO 
#define TWO THREE
#define THREE ONE

int main()
{
    int ONE, TWO, THREE;
    ONE = 1;
    TWO = 2;
    THREE = 3;
    printf ("ONE, TWO, THREE = %d,  %d, %d \n",ONE,  TWO, THREE);
}

这是预处理器的输出。我无法弄清楚为什么输出是这样的。我想知道预处理器在这种情况下采取的各种步骤以提供以下输出。

# 1 "check_macro.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "check_macro.c"

int main()
{
 int ONE, TWO, THREE;
 ONE = 1;
 TWO = 2;
 THREE = 3;
 printf ("ONE, TWO, THREE = %d,  %d, %d \n",ONE, TWO, THREE);
}

我在 linux 3.2.0-49-generic-pae 上运行这个程序并在 gcc 版本 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5) 中编译。


r
rici

在扩展预处理器宏时,不会扩展该宏的名称。因此,您的所有三个符号都被定义为它们自己:

ONE -> TWO -> THREE -> ONE (not expanded because expansion of ONE is in progress)
TWO -> THREE -> ONE -> TWO (        "                         TWO      "        )
THREE -> ONE -> TWO -> THREE (      "                         THREE    "        )

此行为由 C 标准的 §6.10.3.4 设置(C11 草案中的节号,尽管据我所知,该节的措辞和编号自 C89 以来没有变化)。当遇到宏名称时,将其替换为其定义(并处理 ### 预处理器运算符,以及类似函数的宏的参数)。然后重新扫描结果以获取更多宏(在文件其余部分的上下文中):

2/ 如果在替换列表的扫描过程中找到被替换的宏的名称(不包括源文件的其余预处理标记),则不会替换它。此外,如果任何嵌套替换遇到被替换的宏的名称,它不会被替换……

该子句继续说,由于递归调用而未被替换的任何标记都被有效地“冻结”:它永远不会被替换:

…这些未替换的宏名称预处理标记不再可用于进一步替换,即使它们稍后(重新)在宏名称预处理标记将被替换的上下文中进行检查。

最后一句所指的情况在实践中很少出现,但这是我能想到的最简单的情况:

#define two one,two
#define a(x) b(x)
#define b(x,y) x,y
a(two)

结果是 one, twotwo 在替换 a 的过程中扩展为 one,two,扩展后的 two 被标记为完全扩展。随后,b(one,two) 被展开。这不再是在替换 two 的上下文中,而是 b 的第二个参数 two 已被冻结,因此不再展开。


+1,很好的答案。 Here's an example,我觉得很好地展示了这种行为(但是,唉,评论有点太长了)。
@IlmariKaronen:我为第 2 段的最后一句添加了一个示例,否则有点难以理解。但是重新阅读您的评论/答案,我认为这不是您的目标,因此无需说您的示例与 OP 大致相同,尽管结果可能更直观一些。
E
Eric Lippert

出版物 ISO/IEC 9899:TC2 第 6.10.3.4 节“重新扫描和进一步替换”第 2 段回答了您的问题,为方便起见,我在此引用;以后,如果您对规范有任何疑问,请考虑阅读规范

如果在替换列表的扫描过程中找到被替换的宏的名称(不包括源文件的其余预处理标记),则不会替换它。此外,如果任何嵌套替换遇到被替换的宏的名称,它不会被替换。这些未替换的宏名称预处理标记不再可用于进一步替换,即使它们稍后在该宏名称预处理标记将被替换的上下文中进行(重新)检查。


公平地说,在 C 标准中找到并理解答案并非易事。使用您的“阅读标准”逻辑,我们可以用 RTFM 回答与 C 相关的每一个问题。
@Lundin:规范以目录开头,清楚地确定规范的哪一部分是关于宏扩展的;我花了 30 秒才找到正确的段落,而且我不是 C 规范方面的专家。是的,我建议人们在对标准化语言有疑问时实际阅读标准,这个标签中的大多数不好的问题都会消失。这是好事。
除了这不是一个坏问题。 OP已经做了一些研究,包括一个编译和预处理器输出的例子,指定了编译器和系统等。而且似乎没有明显的问题重复。同样,阅读 C 标准并非易事。例如,您没有设法这样做。您出于某种原因引用了 ISO 9899:1999 TC2 的 N1124 草案,此后该草案已被 C99+TC2、C99+TC3 草案 N1256、C99+TC3、C11、C11+TC1 取代。虽然我相信你通过这些修订知道宏重新扫描的所有变化......
@Lundin:就不好的问题而言,我每天在这里看到 100 倍更糟糕的问题,所以是的,这非常好。我选择了该版本的标准,因为它很容易找到——它是从 Wikipedia 链接的——而且它是免费的,而且大多数编译器都遵守它。正如我所说,我根本不是 C 规范的历史或内容方面的专家。我的观点是,我通过一次网络搜索和一眼目录就找到了问题的答案;对于普通程序员来说,这并不是不可能的。我鼓励规范阅读成为所有程序员的工具箱。
@Alice:虽然 cHao 或许可以更优雅地表达自己,但这一点很好。我基本上不关心有明确规范的库代码中的百行方法的正确性证明;我担心整个操作系统、整个数据库等的正确性,在一个内存模型弱、内存不安全的语言等的世界中运行。您用来证明 STL 集合正确性的技术无法扩展到整个 Windows 操作系统。
R
R Sahu

https://gcc.gnu.org/onlinedocs/cpp/Self-Referential-Macros.html#Self-Referential-Macros 回答了关于自引用宏的问题。

答案的关键是,当预处理器找到自引用宏时,它根本不会扩展它们。

我怀疑,相同的逻辑用于防止循环定义的宏的扩展。否则,预处理器将处于无限扩展中。


M
M.M

在您的示例中,您在定义同名变量之前进行宏处理,因此无论宏处理的结果是什么,您总是打印 1, 2, 3

这是首先定义变量的示例:

#include <stdio.h>
int main()
{
    int A = 1, B = 2, C = 3;
#define A B
#define B C
//#define C A
    printf("%d\n", A);
    printf("%d\n", B);
    printf("%d\n", C);
}

这将打印 3 3 3。有点阴险的是,取消注释 #define C A 会改变行 printf("%d\n", B); 的行为


2
2 revs

这是 rici'sEric Lippert's 答案中描述的行为的一个很好的演示,即如果宏名称在已经扩展同一个宏时再次遇到,则不会重新扩展。

test.c 的内容:

#define ONE 1, TWO
#define TWO 2, THREE
#define THREE 3, ONE

int foo[] = {
  ONE,
  TWO,
  THREE
};

gcc -E test.c 的输出(不包括初始 # 1 ... 行):

int foo[] = {
  1, 2, 3, ONE,
  2, 3, 1, TWO,
  3, 1, 2, THREE
};

(我会将此作为评论发布,但在评论中包含大量代码块有点尴尬,因此我将其作为社区 Wiki 答案。如果您觉得将其作为现有答案的一部分会更好,请随意复制它并要求我删除这个CW版本。)


关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅