【问题标题】:Macros and postincrement宏和后增量
【发布时间】:2011-09-15 14:01:58
【问题描述】:

这里有一些更奇怪的宏行为,我希望有人能阐明:

#define MAX(a,b) (a>b?a:b)

void main(void)
{
  int a = 3, b=4;

  printf("%d %d %d\n",a,b,MAX(a++,b++));
}

输出为 4 6 5。b 的值增加了两次,但在 MAX 显示其值之前不会增加。谁能告诉我为什么会发生这种情况以及如何预测这种行为? (另一个为什么应该避免使用宏的例子!)

【问题讨论】:

  • 有趣,如果你做 MAX(++a, ++b) 会发生什么?我在一个论坛上找到了这个信息:永远不要使用前置或后置增量作为宏的参数,因为宏可能会多次使用该参数。因此,第 2 项应该是“永远不要对函数的参数使用递增或递减”,因为您可能不知道这是否是一个真正的函数,或者它是否作为宏实现。来源:bytes.com/topic/c/answers/…
  • 这可能被视为反对使用前置或后置增量的论据。
  • @peko:也就是说,当标准函数被实现为宏时,它们必须对每个参数进行一次准确的评估(7.1.4/1)。此外,标准库总是提供函数以及宏。所以strlen(++s) 只增加一次s,但是如果你想避免宏,你可以写(strlen)(++s),这不是宏信息。第三方库的编写者可能会或可能不会提供这些有用的保证,如果他们提供了他们不会告诉您它们是函数还是宏的“东西”。

标签: c macros undefined-behavior


【解决方案1】:

宏进行文本替换。您的代码相当于:

printf("%d %d %d\n",a,b, a++ > b++ ? a++ : b++);

这具有未定义的行为,因为b 可能会递增(在第三个参数的末尾),然后在没有中间序列点的情况下使用(在第二个参数中)。

但是与任何 UB 一样,如果您盯着它看一会儿,您可能会想出一个解释,说明您的实现实际上做了什么来产生您看到的结果。参数的评估顺序未指定,但在我看来,参数似乎是按从右到左的顺序进行评估的。所以首先,ab 递增一次。 a不大于b,所以b再次递增,条件表达式的结果为5(即b在第一次递增后第二次递增)。

这种行为是不可靠的 - 另一个实现或另一天的相同实现可能会由于以不同的顺序评估参数而给出不同的结果,或者理论上甚至可能由于序列点问题而崩溃。

【讨论】:

  • 我有点理解一旦我得到输出会发生什么,但我如何预测这种行为?
  • @Appster:你不能(不使用特定编译器的大量未记录细节),这是未定义的行为。永远不要写这段代码。如果你的意思是,“我怎么能预测它是未定义的”,那么一个简化的版本是你永远不应该在同一个表达式中使用变量并增加它。如果面试问题是“这个程序的输出是什么?”,那么正确的答案是“我不在乎,如果你这样做了,那么你就是一个糟糕的 C 程序员”。这可能不是最有可能让你得到这份工作的答案,但它是正确的:-)
  • +1 。所以基本上,a++ > b++ 比较 3 > 4?事实并非如此,因此编译器移动到 : 的右侧,将 a 和 b 分别增加到 4 和 5 之后。所以显示时MAX的结果是b=5,然后b第二次递增。我想我明白了!
  • @Appster:这是正确的,MAX(a++,b++) 本身已经定义了(如果令人惊讶的话)行为,因为?: 条件运算符在条件和其他两个表达式中的任何一个之间有一个序列点实际被评估。它是 MAXab 在同一未定义表达式中的附加用途的组合。
  • 当您使用 C 时,您会注意到很多未记录的行为,但仔细观察(以及大量测试样本)总是会发现一种模式。这就是面试官的成功之处,告诉面试官我几乎不在乎会让我被淘汰:)
【解决方案2】:

在宏中,参数只是被实参替换;因此,如果参数在宏中出现多次,则可以对其进行多次评估。

你的例子:

MAX(a++,b++)

扩展为:

a++>b++?a++:b++

我认为您不需要更多解释:)

您可以通过将每个参数分配给一个临时变量来防止这种情况:

#define MAX(a,b) ({   \
    typeof(a) _a = a; \
    typeof(b) _b = b; \
    a > b ? a : b;    \
})

(不过,这个使用了几个 GCC 扩展)

或者使用内联函数:

int MAX(int a, int b) {
    return a > b ? a : b;
}

这将与运行时的宏一样好。

或者不要在宏参数中做增量:

a++;
b++;
MAX(a, b)

【讨论】:

  • 同意。在这个答案中避免使用宏通常会更安全,因为它们是类型安全的。此外,因为它们是内联的,它们实际上不会像普通函数那样导致创建和销毁堆栈帧。
  • 类型安全,只计算一次参数,被更多的调试器看到......请注意,在这里使用函数而不是宏不会删除 UB。
【解决方案3】:

当预处理器读取该行时,它将 printf 中的 MAX(a++,b++) 替换为 (a++>b++?a++;b++)

所以你的函数变成了

    printf(a,b,(a++>b++?a++;b++));

这里的评估顺序是“依赖于编译器的”。

要了解这些情况何时会发生,您必须了解序列点。

在每个序列点,将完成之前所有表达式的副作用(将完成所有变量计算)。这就是为什么您不能依赖以下表达式:

    a[i] = i++;

因为没有为赋值、自增或索引操作符指定序列点,所以你不知道自增对 i 的影响何时发生。 “在前一个和下一个序列点之间,对象的存储值最多只能通过表达式的评估修改一次。此外,应仅读取先验值以确定要存储的值。”。如果一个程序违反了这些规则,任何特定实现的结果都是完全不可预测的(未定义)。

--标准中规定的顺序点如下:

1) 在评估其参数之后调用函数的点。

2) && 运算符的第一个操作数的结尾。

3)|| 的第一个操作数的结尾运算符。

4) ?: 条件运算符的第一个操作数的结尾。

5)逗号运算符的每个操作数的结尾。

6) 完成完整表达式的评估。它们是:

评估自动对象的初始化器。

“普通”语句中的表达式——后跟分号的表达式。

do、while、if、switch 或 for 语句中的控制表达式。

for 语句中的其他两个表达式。

return 语句中的表达式。

【讨论】:

    【解决方案4】:

    宏由预处理器评估,根据宏定义愚蠢地替换所有宏。在您的情况下,MAX(a++, b++) 变为 (a++>b++) ? a++ : b++

    【讨论】:

      【解决方案5】:

      如果我是正确的,这是正在发生的:

      将 MAX 替换为 (a>b...) 你有 printf("%d %d %d\n",a,b,(a++ > b++ ? a++ : b++ ) );

      首先,检查 a++ > b++,然后两个值都增加(a = 4,b = 5)。 然后第二个 b++ 被激活,但是因为它是后增量,所以在打印第二个值 b = 5 后它会增加。

      对不起,我的英语不好,但我希望你能理解?! :D

      来自德国的问候 ;-)

      拉尔夫

      【讨论】:

        【解决方案6】:

        所以你的扩展给出了(为清楚起见进行了调整):

        (a++ > b++) ? a++ : b++
        

        ...所以首先评估(a++ > b++),每个增加一个并根据ab 的尚未增加的值选择一个分支。选择了“else”表达式b++,它在b 上进行第二次递增,而b 已经在测试表达式中递增。由于是后自增,所以将第二次自增之前的b 的值赋予printf()

        【讨论】:

          【解决方案7】:


          您得到这里的结果有两个原因:

          1. 宏不过是编译时展开和粘贴的代码。所以你的宏

            MAX(a,b) (a>b?a:b)
            

            变成这样

            a++>b++?a++:b++
            

            你得到的函数是这样的:

            printf("%d %d %d\n",a,b, a++>b++?a++:b++);
            

            现在应该清楚为什么 b 会增加两次:第一次是比较,第二次是返回它。这不是未定义的行为,它是明确定义的,只需分析代码,您就会看到究竟会发生什么。 (在某种程度上是可以预测的)

          2. 这里的第二个问题是,在 C 语言中,参数从最后一个到第一个传递到堆栈,因此所有的递增都将在您打印 a 和 b 的原始值之前完成,即使它们被列在最前面。试试这行代码,你就会明白我的意思:

            int main(void)
            {
                int a = 3, b=4;
                printf("%d %d %d\n",a,b, b++);
                return 0;
            }
            

          输出将是 3 5 4。 我希望这可以解释这种行为并帮助您预测结果。
          但是,最后一点取决于您的编译器

          【讨论】:

          • “在 C 中的参数从最后一个传递到第一个到堆栈”可能在您的实现使用的 ABI 中,但标准不保证这一点。它甚至没有实现定义参数的评估顺序,它是未指定的。
          【解决方案8】:

          我认为提问者希望输出开始:

          3 4 ...
          

          代替:

          4 6 ...
          

          这是因为参数被推入堆栈时是从右到左计算的,也就是说,最后一个参数首先被计算并被推入,然后是倒数第二个,然后是第二个参数,最后第一个参数。

          我认为(如果有错误,有人会发表评论)这是在 C 标准(和 C++)中定义的。

          更新

          评估顺序已定义,但未定义(感谢 Steve)。您的编译器恰好是这样做的。我想我对评估顺序和传递参数的顺序感到困惑。

          【讨论】:

          • 6.5.2.2/10 of C99:“函数指示符、实际参数和实际参数中的子表达式的求值顺序未指定,但在实际调用之前有一个序列点。 "
          猜你喜欢
          • 2017-04-30
          • 1970-01-01
          • 2012-09-15
          • 1970-01-01
          • 2012-03-07
          • 1970-01-01
          • 2014-12-09
          • 2012-06-01
          相关资源
          最近更新 更多