i = (i, ++i, 1) + 1; 这行代码是做什么用的?

175

阅读了这篇回答关于未定义行为和序列点,我写了一个小程序:

#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;
                        ^
为什么?但很可能它会被我的第一个问题的答案自动回答。

293
不要做奇怪的事情,否则你就没有朋友了 :( - Maroun
9
警告消息是对你第一个问题的答案。 - Yu Hao
2
@gsamaras:不是的。生成的被丢弃,而不是修改。真正的答案是:逗号运算符创建一个序列点。 - Karoly Horvath
3
当你的分数是正数甚至有10个以上的问题时,你不需要在意。 - LyingOnTheSky
9
注意:一种优化编译器可能会简单地执行 printf("2\n"); - chux - Reinstate Monica
显示剩余23条评论
7个回答

256

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

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

因为它丢弃第一个操作数,所以通常仅在第一个操作数有期望副作用的情况下才有用。如果第一个操作数没有发生副作用,则编译器可能会生成关于无效表达式的警告。

因此,在上述表达式中,最左边的i将被计算并丢弃其值。然后计算++i将使i增加1,并再次丢弃++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;  

请注意,上述表达式是完全有效的,不会引发未定义的行为,因为在逗号运算符的左右操作数评估之间存在序列点


1
虽然最终表达式是有效的,但第二个表达式++i不是未定义行为吗?它被评估并且未初始化变量的值被预增加,这不正确吧?还是我漏掉了什么? - Koushik Shetty
2
@Koushik; i 被初始化为 5。请看声明语句 int i = 5; - haccks
1
哦,我的错。抱歉,我真的没看到那个。 - Koushik Shetty
这里有一个错误:++i会先将i加1,然后再对其求值,而i++会先对i求值,然后再将其加1。 - Quentin Hayot
1
@QuentinHayot; 什么?在表达式求值后会发生任何副作用。对于 ++i,将评估此表达式,i 将被递增,并且此递增值将是表达式的值。对于 i++,将评估此表达式,i 的旧值将是表达式的值,在表达式的前一个和下一个序列点之间的任何时间都将递增 i - haccks

62

引用自 逗号运算符,C11,第6.5.17章:

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

因此,在您的情况下,

(i, ++i, 1)

被评估为

  1. i,作为一个void表达式进行评估,其值被丢弃
  2. ++i,作为一个void表达式进行评估,其值被丢弃
  3. 最后,返回1

因此,最终语句如下所示

i = 1 + 1;

并且 i 被赋值为 2。我想这回答了你两个问题:

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

注意:值得一提的是,在左操作数计算后存在一个序列点,因此像 (i, ++i, 1) 这样的表达式将不会引发未定义行为(UB),这可能是人们错误地普遍认为的。


+1 Sourav,因为这解释了为什么i的初始化明显没有效果!然而,我认为对于不知道逗号运算符的人来说并不是那么明显(我也不知道如何寻求帮助,除了提问)。可惜我得到了那么多的踩!我会检查其他答案,然后决定接受哪一个。谢谢!顺便说一句,很好的顶级答案。 - gsamaras
我觉得我需要解释一下为什么我接受了haccks的答案。我本来准备接受你的答案,因为它确实回答了我的两个问题。然而,如果你查看一下我的问题的评论,你会发现有些人一开始看不出为什么这不会引起UB。haccks的答案提供了一些相关信息。当然,我在我的问题中链接了关于UB的答案,但有些人可能会错过。希望你能同意我的决定,如果不同意,请让我知道。 :) - gsamaras

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被覆盖掉之前的内容无论是什么都已经不存在了。


您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - gsamaras
非常抱歉解释不足,我只在那里写了一个注释(*...但被忽略了,还有...*)。我主要想解释为什么++i对结果没有贡献。 - dlask
现在我的for循环将始终像这样:int i = 0; for( ;(++i, i<max); ) - CoffeDeveloper

19

的结果

(i, ++i, 1)

是什么

1

对于

(i,++i,1) 
, 运算符会丢弃其左侧所有值的计算结果,只保留最右侧的值,即 1
因此,
i = 1 + 1 = 2

1
是的,我也想过,但我不知道为什么! - gsamaras
@gsamaras 因为逗号运算符会计算前面的表达式但不使用它(即不用于赋值或类似操作)。 - Marco A.

14

逗号运算符的维基页面上你将会找到一些好的阅读材料。

基本上,它

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

这意味着

(i, i++, 1)

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


天啊,那个语法在C++中有效吗?我记得我有几个地方需要那个语法(基本上我写了:(void)exp; a= exp2;而我只需要a = exp,exp2; - CoffeDeveloper

13

你需要了解逗号运算符在这里的作用:

你的表达式:

(i, ++i, 1)

第一个表达式 i 被评估,第二个表达式 ++i 被评估,并且整个表达式返回第三个表达式 1

所以结果是:i = 1 + 1

对于你的奖励问题,正如你所看到的,第一个表达式 i 根本没有影响,因此编译器会发出警告。


5

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

在现代C语言中,它的用法很奇怪,但在旧版的ANSI C中非常有趣:

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

虽然所有操作(函数)都是从左到右调用的,但只有最后一个表达式会被用作条件'while'的结果。

这可以防止处理'goto'以保持唯一的命令块在条件检查之前运行。

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


Luciano,你也可以通过以下链接查看这个答案:https://dev59.com/hGMm5IYBdhLWcg3wDrtR#17903036。 - gsamaras
在90年代初期,当内联函数还没有出现时,我经常使用它来优化并保持代码的组织。 - Luciano

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