【问题标题】:Understanding macros in C [duplicate]理解 C 中的宏 [重复]
【发布时间】:2023-03-16 22:55:01
【问题描述】:

为什么下面代码的输出值是 5?

#include<stdio.h>

#define A -B
#define B -C
#define C 5

int main()
{
  printf("The value of A is %d\n", A);
  return 0;
}

【问题讨论】:

  • 试试看。我会说 5
  • 你尝试了之后得到了什么?你为什么不试试?输出让您感到困惑的是什么?
  • 嗯...它在 macOS 上使用 clang 编译,输出为 5
  • 请记住,在预处理器处理宏扩展之前,输入已经被标记化。因此,代码就像你写的是- - 5 而不是--5,这将是一个错误(你不能减少一个常数)。
  • @Marged:我不同意:这与懒惰无关,编译源代码并不能证明太多,解释额外空间来自哪里是技巧问题的核心。告诉面试官令牌粘贴和编译器错误将是申请人的绝佳答案。

标签: c++ c c-preprocessor


【解决方案1】:

这是一个棘手的问题,因为它是对编译器预处理器的压力测试。

取决于预处理器是编译器的集成阶段还是通过文件或管道将其输出传递给编译器的单独程序,在这种情况下,它是否足够小心以不执行错误的令牌粘贴,您可能会得到预期输出:5 否则您可能会遇到编译错误。

经过stdio.h的预处理内容,源码展开为:

int main()
{
  printf("The value of A is %d\n", --5);
  return 0;
}

但是这两个- 是单独的标记,因此取决于预处理器是否在其输出中将它们分开,您可能会得到一个输出5 的程序或一个由于-- 不能应用于一个文字5

gccclang 预处理器行为正确,并在使用 -E 命令行选项生成预处理器输出时用额外的空格分隔 - 以防止标记粘贴。他们在&lt;stdio.h&gt;扩展后将其输出为预处理的源代码:

int main()
{
  printf("The value of A is %d\n", - -5);
  return 0;
}

尝试使用您自己的编译器来检查它如何扩展源代码。似乎 Visual Studio 2013 和 2015 未能通过测试并以错误拒绝该程序。

为了清楚起见,我并不是说程序的行为应该取决于编译器架构。我希望至少有一个常见的 C 编译器会错误地处理这个一致性测试。 MS Visual Studio 2013 和 2015 未能通过此测试,我并不感到惊讶。

仅在预处理器的文本输出中需要额外的空间。 Visual Studio 是否使用多个单独的阶段无关紧要,源程序完全有效,并且无法编译它是一个 BUG。

【讨论】:

  • 空白从何而来?如果clang和gcc是对的,为什么不把B扩展成- 5
  • @axiac:它们不需要扩展到- 5,因为-5 是正确的。
  • 标准说明了必须发生的事情(并且 GCC 和 Clang 正确地实现了它);一些编译器可能存在错误,可能是因为它们使用了单独的预处理程序。该标准并没有说“根据预处理器是否为单独的程序,您可以获得不同的行为”。
  • @axiac,空间是在预处理器将其结果作为文本发出时合成的,而不是作为直接传递给编译器的标记序列。这允许它确保如果输出被重新读取为 C 源,它将表示与原始源相同的标记序列。这是实施质量方面的考虑,因为标准没有说明这种转换回文本的内容。
  • @JonathanLeffler:我完全同意。此代码是预处理器的确认测试,VS 未通过它。我澄清了这方面的答案。
【解决方案2】:

无需编译此代码,只需在其上使用gcc -E(预处理器),看看会发生什么:

<lots of output expanding stdio.h> ...

int main()
{
  printf("The value of A is %d\n", - -5);
  return 0;
}

很明显,结果是5(可以通过查看嵌套宏来猜到,但是一个小的预处理器测试并没有什么坏处)。

(其他答案指出,某些编译器可能会处理减号的预处理,这会导致编译器错误。gcc 处理得很好。)

【讨论】:

    【解决方案3】:

    这个问题真的没有意义,但我还是决定试一试。

    Visual Studio 2013 和 2015:error C2105: '--' needs l-value

    原因是下面这行:

    printf("The value of A is %d\n", A);
    

    先翻译成(A变成-B):

    printf("The value of A is %d\n", -B);
    

    然后变成(B变成-C);

    printf("The value of A is %d\n", --C);
    

    然后变成(C变成5):

    printf("The value of A is %d\n", --5);
    

    并且由于 5 不是左值,因此您不能将其递减,因此会出现错误。 看起来很合乎逻辑,知道预处理器只会做一个简单的字符串替换。

    【讨论】:

    • 恭喜。您可以向 Microsoft 提交错误报告。这是他们的编译器的错误行为。
    • 在扩展宏之前对预处理器的输入进行标记。令牌不会被取消标记。来自-B- 是一个令牌;它不受进一步宏观扩张的影响。下一个令牌是B;它受宏扩展为-C(这是两个令牌)。 - 再次不受宏扩展的影响;它继续下去。 C 不受宏扩展;它毫发无损。预处理器的输出是 3 个标记 — --C。将它们混为--C,然后将两个破折号解释为减量是不正确的。标准非常明确。
    • 仅在预处理器的文本输出中需要的额外空间。 VS是否使用多个单独的阶段无关紧要,源程序完全有效,编译失败是一个BUG。
    • @JonathanLeffler 如果我没听错的话,不可能#define A str#define B uct 然后使用AB 生成关键字struct,不是吗?无论如何,它都会被解析为两个标记,对吧? (struct
    • 如图所示,没错。有## 标记粘贴操作符,当然:#define C(x, y) C1(x, y) 加上#define C1(x, y) x ## y 可以用在C(A, B) apoplexy { … }; 等中,导致不幸的代码读者中风(更不用说编译器编写者了)。需要两个级别的宏来将参数扩展为struct。标记粘贴仅适用于“冗长”的标记,而不是标点符号。
    【解决方案4】:

    这是一个很好的例子,如何不使用预处理器。为避免混淆,应使用括号(不仅在这种情况下)

    #define A (-B)
    #define B (-C)
    #define C (5)
    

    【讨论】:

    • 这是关于如何使用预处理器的有效观察;它不是“为什么”问题的直接答案。因此,+1 表示有效性,-1 表示相关性。
    • 是的,但最初的问题无法回答。这是一个例子,如果程序实际编译时甚至不是 UB 的未定义行为,但结果在 C 标准含义中是不可预测的,但在这种情况下,编译过程是未定义的。所以我的另一个答案是编译器错误,如果预处理器没有添加任何空格,或者如果它添加了 5。
    • 否;它是具有有效答案的完美定义的代码——尽管至少有一个主要的编译器错误地处理了有效的 C 代码。代码中没有UB。 (我并不是说它是好的代码;它不是。使用宏而不是问题中的内容可以防止损坏的编译器破坏有效代码。不过,这些都是切题的问题。)
    • #define C (5) 中的括号似乎没有必要。你认为他们需要什么背景?
    • 为了避免 C 标准疯子(我不是其中之一)的讨论,一般规则(恕我直言)是:*最好写多个括号而不是缺少一对* :)
    【解决方案5】:

    每个#define 预处理指令将在预处理器环境中插入一个变量,该变量分配给由预处理指令列表组成的值。

    {A -> -B; B->-C; C->5}
    

    是评估A 时的环境。现在,制作A的评估过程,我们有

      A  ->  -B   (the identifier `A` is transformed in the stream of preprocessing tokens `-B`)
     -B  ->  --C
    --C  ->  --5
    ->  5
    

    Prosser 的算法将不再评估这个,因为它没有更多的标识符。

    所以,减少,

     A->5
    

    A 流在5 流中转换,这个流将从 C 令牌中的预处理令牌转换并进一步发送到 C 编译器。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-01-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-01-27
      • 1970-01-01
      • 2012-11-30
      相关资源
      最近更新 更多