C和C ++中arr[i] = i ++和i = i + 1语句的行为

18

在C和C++语言中,arr[i] = i++;语句会导致未定义的行为。为什么i = i + 1;语句不会导致未定义的行为?


5
可能是未定义行为和序列点的重复问题。 - underscore_d
5
询问此类问题时,您需要专注于一种语言,就像您不会询问“C#和Java”一样,不要过多关注“C和C ++”。 - Cody Gray
5个回答

36
由于最初标记为而不是任何具体版本,下面的答案是针对该问题的通用答案。但是,请注意,对于,从C ++ 17 开始,行为已更改。请参见此 Barry的答案以了解更多信息。

对于该语句

arr[i] = i++;
i的值在RHS(右操作数)和LHS(左操作数)中都被使用,在其中一种情况下,该值正在被修改(作为post ++ 的副作用),而在这两个操作之间没有序列点来确定应考虑哪个i的值。您还可以查看this canonical answer以了解更多信息。
另一方面,对于i = i + 1i的值仅在RHS中使用,计算结果存储在LHS中,换句话说,没有歧义。我们可以将相同的语句写成i ++ ,其中
  • 读取i的值
  • 将其增加1
  • 将其存回i
按照明确定义的顺序执行。因此,没有问题。

4
这是一个关于语言方面的非重复变体问题,你的回答不错。 - Bathsheba
7
几年前,C++放弃了“顺序点”,转而采用更加复杂的“顺序之后”描述方式,使得例如i=++i;合法化。 - 6502
5
i = i + 1 不等同于 i++,它的效果等同于 ++i。通过比较 j = i = i+1j = ++i 以及 j = i++ 的影响来理解区别(应记住赋值运算符从右至左结合,因此 j = i = i+1 等同于 j = (i = i+1))。 - Peter
1
@Bathsheba 谢谢您先生的赞美。 :) - Sourav Ghosh
4
我的问题是,你在第二段中的写法暗示了i=i+1i++是等价的,但它们并不等价。这种建议对回答问题是不必要的,也会产生误导。 - Peter
显示剩余3条评论

13
请注意,这将在C++17中发生变化。 在C ++17中,arr [i] = i ++不会引发未定义的行为。 这是由于[expr.ass]中以下更改造成的:
在所有情况下,分配都在右操作数和左操作数的计算值之后、分配表达式的计算值之前进行排序。 右操作数先于左操作数排序。 也就是说,我们先执行i ++,然后执行arr [i],然后执行分配。现在明确定义的顺序是:
auto src = i++;
auto& dst = arr[i];
dst = src;

2
尽管现在这个代码可以工作,但它仍然是一个非常脆弱的表达式。幸运的是,如果你使用GCC编译器,你可以通过使用“-fno-strong-eval-order”选项来保持这个糟糕的表达式未定义。 - KevinZ
4
@KevinZ 为什么说“幸运”?在这种情况下有额外未定义行为有什么好处吗? - Ruslan
2
是的,编写这样的表达式很糟糕,纯粹是因为它看起来模糊且虚伪聪明 - 但更糟糕的是,在代码中留下定时炸弹并强制编译器避免遵守新标准。通过推理来鼓励编写良好的代码,而不是受 UB 的限制。如果 UB 在太晚之前被检测到,希望能如此。实际上,坦率地说,建议人们依赖某些东西是 UB,而且可以观察到,这是极其愚蠢的。需要向编写此代码的人解释其原因;他们很可能不会运行 UBsan 或任何其他工具。 - underscore_d
@KevinZ 除非还存在未提及的别名问题,否则 arr[i] = i++ 怎么会是双重存储呢? - Daniel H
@Barry先生,我没有复制内容,而是在我的回答中提到了您的答案链接,并给予了应有的信用(我相信如有需要请告知)。希望这样做对您没有问题。谢谢! - Sourav Ghosh
显示剩余2条评论

9
对于C99,我们有:
“6.5表达式” 在前一个和下一个序列点之间,一个对象在表达式的评估中最多只能被修改一次。此外,应该只读取先前的值以确定要存储的值。
在“arr [i] = i ++”中,i的值只被修改了一次。但是“arr [i]”也从i中读取,而这个值不用于确定i的新值。这就是为什么它具有未定义的行为。
另一方面,在“i = i + 1”中,我们读取i以计算“i + 1”,这被用作i的新值。因此,这个表达式没问题。

8
arr[i] = i++;

这意味着:

  • 在赋值之前会先计算右侧表达式
  • 在赋值之前会先计算下标操作符

但是对于右侧表达式的计算顺序和下标操作符的计算顺序存在歧义,编译器可以将其视为:

auto & val{arr[i]};
i++;
auto const rval{i};
val = rval;

或作为
i++;
auto & val{arr[i]};
auto const rval{i};
val = rval;

或者(与上面相同的结果)

i++;
auto const rval{i};
auto & val{arr[i]};
val = rval;

可能会产生不可预测的结果,而
i = i + 1;

没有任何歧义,右手表达式在赋值之前被求值:

auto const rval{i + 1};
auto & val{i};
val = rval;

或(与上述结果相同)

auto & val{i};
auto const rval{i + 1};
val = rval;

或者:undefined(); - Deduplicator

2
在你的例子中,如果 a [i] = i++,例如 i = 3,你认为 a [i] 是先被计算还是 i++?在一个情况下,值 3 将存储在 a [3] 中,在另一个情况下,它将存储在 a [4] 中。很明显,我们在这里遇到了问题。除非他们找到了一个保证会发生什么的方法,否则没有理智的人敢写出这样的代码。(Java 给出了这个保证)。
那么 i = i + 1 有什么问题呢?语言必须首先读取 i 来计算 i+1,然后存储该结果。这里没有任何可能出错的地方。a [i] = i+1 也是一样的道理。与 i++ 不同,计算 i+1 不会改变 i 的值。因此,如果 i = 3,则必须将数字 4 存储在 a [3] 中。
各种语言都有不同的规则来解决 a [i] = i++ 的问题。Java 定义了发生的情况:表达式从左到右进行评估,包括它们的副作用。C 将其定义为未定义行为。C++ 并没有使其成为未定义行为,而只是未指定。它说要么先评估 a[i],然后再评估 i++,要么反过来,但它没有说哪一个先。因此,与 C 不同,C++ 定义了只有两种情况中的一种会发生。显然,在您的代码中,这还是太多了。

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