174

在阅读了有关未定义行为和序列点的答案后,我编写了一个小程序:

#include <stdio.h>

int main(void) {
  int i = 5;
  i = (i, ++i, 1) + 1;
  printf("%d\n", i);
  return 0;
}

输出是2。天哪,我没有看到递减的到来!这里发生了什么?

此外,在编译上述代码时,我收到一条警告:

px.c:5:8: 警告:逗号表达式的左操作数无效

  [-Wunused-value]   i = (i, ++i, 1) + 1;
                        ^

为什么?但可能我的第一个问题的答案会自动回答它。

4

7 回答 7

256

在表达式(i, ++i, 1)中,使用的逗号是逗号运算符

逗号运算符(由 token 表示,)是一个二元运算符,它计算其第一个操作数并丢弃结果,然后计算第二个操作数并返回此值(和类型)。

因为它丢弃了它的第一个操作数,所以它通常只在第一个操作数具有理想的副作用时才有用。如果第一个操作数的副作用没有发生,那么编译器可能会生成有关表达式无效的警告。

因此,在上面的表达式中,最左边的i将被评估,其值将被丢弃。然后++i将被评估并增加i1 并且表达式的值++i将再次被丢弃,但副作用i是永久的。然后1将被评估并且表达式的值将是1

它相当于

i;          // Evaluate i and discard its value. This has no effect.
++i;        // Evaluate i and increment it by 1 and discard the value of expression ++i
i = 1 + 1;  

请注意,上面的表达式是完全有效的,并且不会调用未定义的行为,因为在逗号运算符的左右操作数的计算之间存在一个序列点。

于 2015-06-03T08:15:01.973 回答
62

引文C11, 章节6.5.17,逗号 运算符

逗号运算符的左操作数被评估为 void 表达式;在它的求值和右操作数的求值之间有一个序列点。然后对右操作数求值;结果有它的类型和值。

所以,在你的情况下,

(i, ++i, 1)

被评估为

  1. i, 被评估为 void 表达式,值被丢弃
  2. ++i, 被评估为 void 表达式,值被丢弃
  3. 最后,1返回值。

所以,最后的陈述看起来像

i = 1 + 1;

i到达2。我想这回答了你的两个问题,

  • 如何i获得值 2?
  • 为什么会有警告信息?

注意:FWIW,因为在左侧操作数的求值之后存在一个序列点(i, ++i, 1),一个表达式 like不会调用 UB,因为人们通常会误以为是。

于 2015-06-03T08:17:12.857 回答
30
i = (i, ++i, 1) + 1;

让我们一步一步来分析。

(i,   // is evaluated but ignored, there are other expressions after comma
++i,  // i is updated but the resulting value is ignored too
1)    // this value is finally used
+ 1   // 1 is added to the previous value 1

所以我们得到 2。现在的最终分配:

i = 2;

之前在i中的任何内容现在都被覆盖了。

于 2015-06-03T08:18:21.020 回答
19

结果

(i, ++i, 1)

1

为了

(i,++i,1) 

评估发生使得,操作员丢弃评估值并保留最正确的值,即1

所以

i = 1 + 1 = 2
于 2015-06-03T08:14:17.193 回答
14

您会在 wiki 页面上找到一些关于Comma operator的好读物。

基本上,它

... 计算其第一个操作数并丢弃结果,然后计算第二个操作数并返回此值(和类型)。

这意味着

(i, i++, 1)

将依次评估i、丢弃结果、评估i++、丢弃结果,然后评估并返回1

于 2015-06-03T08:16:24.487 回答
13

您需要知道逗号运算符在这里做什么:

你的表情:

(i, ++i, 1)

计算第一个表达式 ,i计算第二个表达式 ,并为整个表达式返回++i第三个表达式 , 。1

所以结果是:i = 1 + 1.

如您所见,对于您的附加问题,第一个表达式i根本没有效果,因此编译器会抱怨。

于 2015-06-03T08:18:22.043 回答
5

逗号具有“逆”优先级。这是您将从 IBM(70 年代/80 年代)的旧书和 C 手册中获得的信息。所以最后一个“命令”是父表达式中使用的。

在现代 C 中,它的使用很奇怪,但在旧 C (ANSI) 中却很有趣:

do { 
    /* bla bla bla, consider conditional flow with several continue's */
} while ( prepAnything(), doSomethingElse(), logic_operation);

虽然所有操作(函数)都是从左到右调用的,但只有最后一个表达式将用作条件“while”的结果。这可以防止处理“goto”以在条件检查之前保持唯一的命令块运行。

编辑:这也避免了对处理函数的调用,该函数可以处理左操作数的所有逻辑,从而返回逻辑结果。请记住,在过去的 C 语言中,我们没有内联函数。因此,这可以避免调用开销。

于 2015-06-11T05:18:36.297 回答