为什么 i = ++i 会引起未定义行为?

5
我知道C语言使用序列点的概念来识别模糊计算,而赋值操作符“=”不是序列点。然而,我无法看出在执行语句i=++i时的任何歧义。按照我的理解,这只是将位于&i的值求值,对其进行递增并将其存回相同位置。然而,GCC将其标记为如下警告:[警告]操作“i”可能未定义[-Wsequence-point]。我是否对=操作的功能存在误解?编辑:在标记为重复之前,请注意我已浏览其他帖子,涉及序列点和未定义行为。其中没有一个特别涉及表达式i=++i(请注意pre-increment)。通常提到的表达式是i=i++a=b++ + ++b等等。我对它们中的任何一个都没有疑问。

1
返回的结果将是“i”的递增值。但是,在赋值之后,“i”可能会被递增。标准规定,副作用将在下一个序列点之前发生,但这可能在赋值之前或之后。 - Fred Larson
1
你的困惑可能是因为你不了解序列点。请参见这里和查看这个问题:https://dev59.com/Zm855IYBdhLWcg3wuG-W - ldog
2
不,你误解了。我并不是说++i返回的值可能会不同。我是说它总是返回i+1。但是增量副作用可能发生在赋值之前或之后。在a = ++b;中副作用发生的位置没有关系,因为没有自我赋值。 - Fred Larson
1
@FredLarson,你的意思是返回i+1的值和修改&i上的内容是独立的事件吗?所以返回计算出的值是实际操作,而增加i内存位置上的值是使用++运算符的副作用? - Qurious
现在完全明白了。谢谢!!我一直认为增量是一个修改变量并返回值的原子操作。所以混淆主要是关于“副作用”的问题。 - Qurious
显示剩余6条评论
2个回答

7
您对未定义行为的理解有所缺失。未定义行为只是指编译器可以任意处理。它可以抛出错误,可以(像GCC一样)显示警告,也可以让恶魔从您的鼻子里飞出来。最重要的是,它不会表现得很好,并且在编译器之间不会保持一致,因此请勿这样做!
在本例中,编译器不必保证运算符左侧的副作用在返回语句的右侧之前已经完成。这对您来说可能很有趣,但您不会像计算机那样思考。如果它愿意,它可以计算返回值并将其返回到寄存器中,在将其分配给i之前对实际值进行增量操作。因此,它看起来更像是:
register=i+1;
i=register;
i=i+1;

标准并不能保证这种情况不会发生,所以最好不要这样做!

3
问题不在于什么是UB,而在于为什么它是UB? - didierc
1
好的回答,但是在我看来,副作用严格意义上只是更新内存单元。同时进行增量操作也是执行计算(依我之见)。我认为混淆来自于这一点。 - didierc
1
是的,副作用是“更新内存单元”。但是,“更新内存单元”的方法在标准中没有被定义/规定;只要具有相同效果,就允许使用任何方法来完成它(大多数实现将需要将值提取到寄存器中,增加寄存器中的值并将其写回“单元”中)。而“写回”的时刻并不固定;唯一的要求是它应该在下一个序列点(即“;”)之前完成,并且这就是为什么在序列点之间多次修改值是被禁止的(或导致未定义行为)的原因。 - wildplasser
@Qurious 因为计算机很奇怪。并不是说它一定会,只是标准没有规定它不能,这是非常重要的区别。 - IdeaHat
现在明白了。混淆是由 ++ 操作的 副作用 部分引起的。我一直认为增量意味着“更新内存位置然后返回值”。这种 更新 是一个副作用,对我来说不太清楚。感谢您的所有努力! - Qurious
显示剩余4条评论

3
未定义的行为是由于变量i在两个序列点之间被多次修改。 序列点是之后所有先前评估的副作用都可见,但没有未来副作用可见的点。 标准规定:

在上一个和下一个序列点之间,一个对象应该通过表达式的评估最多只能修改其存储值一次。 此外,应仅读取先前的值以确定要存储的值。

那么,我们关心的副作用是什么?

  • ++i,它将i赋值为i + 1的值
  • i = ++i,它将i赋值为表达式++i的值,该值为i + 1
所以,我们将得到两个(可等效的)副作用:将i+1分配给变量i。我们关心的是,在哪两个序列点之间发生这些副作用?
哪些操作构成序列点?有多个,但只有一个实际上与此相关:
在完整表达式的末尾(在本例中,i = ++i是完整表达式)
即,前置增量++i不是序列点。这意味着两个副作用(增量和赋值)都将在相同的两个序列点之间发生,修改相同的变量i。因此,它是未定义的行为;两个修改恰好具有相同的值是无关紧要的。
但是为什么在序列点之间多次修改变量是不好的呢?为了防止像这样的事情发生:
i = ++i + 1;

这里,i被递增,但由于前缀递增的语义,它也被赋值为(i+1) + 1。由于副作用具有模糊的顺序,行为是未定义的。
现在,理论上可以在标准中特殊规定两个序列点之间的多次修改是可以的,只要值相同即可,但这可能会不必要地使编译器实现变得复杂,而收益却不大。

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