a[a[0]] = 1 会产生未定义的行为吗?

Does a[a[0]] = 1 produce undefined behavior?

此 C99 代码是否产生未定义的行为?

#include <stdio.h>

int main() {
  int a[3] = {0, 0, 0};
  a[a[0]] = 1;
  printf("a[0] = %d\n", a[0]);
  return 0;
}

在语句 a[a[0]] = 1; 中,a[0] 被读取和修改。

我看了 ISO/IEC 9899 的 n1124 草稿。它说(在 6.5 表达式中):

Between the previous and next sequence point an object shall have its stored value modified at most once by the evaluation of an expression. Furthermore, the prior value shall be read only to determine the value to be stored.

没有提到读取一个对象来确定要修改的对象本身。因此,此语句可能会产生未定义的行为。

不过,我觉得很奇怪。这实际上会产生未定义的行为吗?

(我也想知道其他ISO C版本有这个问题。)

该值定义明确,除非 a[0] 包含的值不是有效的数组索引(即在您的代码中不是负数且不超过 3)。您可以将代码更改为更具可读性和等效性

 index = a[0];
 a[index] = 1;    /* still UB if index < 0 || index >= 3 */

在表达式 a[a[0]] = 1 中,需要先计算 a[0]。如果 a[0] 恰好为零,则 a[0] 将被修改。但是编译器(除非不符合标准)无法在尝试读取其值之前更改评估顺序并修改 a[0]

Does this C99 code produce undefined behavior?

没有。它不会产生未定义的行为。 a[0] 在两个 sequence points 之间只被修改一次(第一个序列点在初始化器 int a[3] = {0, 0, 0}; 的末尾,第二个在完整表达式 a[a[0]] = 1 之后)。

It does not mention reading an object to determine the object itself to be modified. Thus this statement might produce undefined behavior.

一个对象可以被多次读取以修改自身及其完美定义的行为。看这个例子

int x = 10;
x = x*x + 2*x + x%5;   

引用的第二个陈述说:

Furthermore, the prior value shall be read only to determine the value to be stored.

读取上面表达式中的所有x来确定对象x本身的值。


注意: 请注意,问题中提到的引文分为两部分。第一部分说:在前一个和下一个序列点之间,对象的存储值最多应通过表达式的计算修改一次。,和
因此表达式 like

i = i++;

属于 UB(前后序列点之间的两次修改)。

第二部分说:此外,只读先前的值以确定要存储的值。,因此像

这样的表达式
a[i++] = i;
j = (i = 2) + i;  

调用 UB。在这两个表达式中,i 仅在前后序列点之间修改一次,但最右边的 i 的读数并不能确定要存储在 i.

中的值

在 C11 标准中,这已更改为

6.5 表达式:

If a side effect on a scalar object is unsequenced relative to either a different side effect on the same scalar object or a value computation using the value of the same scalar object, the behavior is undefined. [...]

在表达式a[a[0]] = 1中,a[0]只有一个副作用,索引a[0]的值计算顺序在a[a[0]]的值计算之前。

the prior value shall be read only to determine the value to be stored.

这有点含糊并引起混淆,这也是 C11 将其丢弃并引入新的排序模型的部分原因。

它想说的是:如果保证读取旧值的时间早于写入新值,那很好。否则就是UB。当然,要求在写入新值之前计算新值。

(当然有些人会觉得我刚才写的描述比标准文本更含糊!)

例如 x = x + 5 是正确的,因为在不知道 x 的情况下不可能计算出 x + 5。但是 a[i] = i++ 是错误的,因为不需要读取左侧的 i 来计算出要存储在 i 中的新值。 (i的两次读取分别考虑)


现在回到你的代码。我认为这是明确定义的行为,因为 a[0] 的读取以确定数组索引保证在写入之前发生。

在确定写在哪里之前,我们不能写。直到我们阅读 a[0] 之后,我们才知道在哪里写。因此读必须先于写,所以没有UB。

有人评论了序列点。在C99中这个表达式中没有序列点,所以序列点不在讨论之列。

C99列举了附件C中所有的序列点,

末尾有一个
a[a[0]] = 1;

因为是一个完整的表达式语句,但是里面没有序列点。虽然逻辑上规定子表达式 a[0] 必须首先求值,并且结果用于确定将值分配给哪个数组元素,但排序规则并不能确保这一点。当a[0]的初始值为0时,a[0]在两个序列点之间既读又写,读的目的是而不是确定要写入的值。因此,根据 C99 6.5/2,计算表达式的行为是未定义的,但实际上我认为您不需要担心它。

C11在这方面比较好。第 6.5 节第 (1) 段说

An expression is a sequence of operators and operands that specifies computation of a value, or that designates an object or a function, or that generates side effects, or that performs a combination thereof. The value computations of the operands of an operator are sequenced before the value computation of the result of the operator.

特别注意第二句,它在 C99 中没有类似的东西。您可能认为这就足够了,但事实并非如此。它适用于 值计算 ,但它没有说明副作用相对于值计算的顺序。更新左操作数的值是一个副作用,所以额外的句子不直接适用。

尽管如此,C11 在这一点上为我们提供了帮助,因为赋值运算符的规范提供了所需的顺序 (C11 6.5.16(3)):

[...] The side effect of updating the stored value of the left operand is sequenced after the value computations of the left and right operands. The evaluations of the operands are unsequenced.

(相比之下,C99 只是说更新左操作数的存储值发生在前一个和下一个序列点之间。)结合 6.5 和 6.5.16 节,那么,C11 给出了一个明确定义的序列:内部 [] 在外部 [] 之前计算,外部 [] 在更新存储值之前计算。这满足了C11版本的6.5(2),所以在C11中,定义了表达式求值的行为。

副作用包括修改对象1

C 标准规定,如果对对象的副作用未排序且对同一对象有副作用或使用同一对象的值进行值计算,则行为未定义2.

此表达式中的对象 a[0] 已修改(副作用),其值(值计算)用于确定索引。这个表达式似乎会产生未定义的行为:

a[a[0]] = 1

然而,标准中赋值运算符的文本解释了运算符 = 左右操作数的值计算在修改左操作数之前排序3.

因此定义了行为,因为没有违反第一条规则1,因为修改(副作用)是在同一对象的值计算之后排序的。


1(引自ISO/IEC 9899:201x 5.1.2.3 程序执行2):
访问 volatile 对象、修改对象、修改文件或调用函数 执行这些操作中的任何一个都是副作用,即状态的变化 执行环境。

2(引自ISO/IEC 9899:201x 6.5 表达式2):
如果一个标量对象的副作用相对于另一个不同的副作用是无序的 在同一标量对象上或使用同一标量的值进行值计算 对象,行为未定义。

3(引自ISO/IEC 9899:201x 6.5.16赋值运算符3):
更新左操作数的存储值的副作用是 在左右操作数的值计算之后排序。的评价 操作数未排序。