a ] = 1是否会产生未定义的行为?

这个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.5expression式中):

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

它没有提到读取对象来确定要修改的对象本身。 因此这个陈述可能会产生未定义的行为。

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

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

先前的值应该只读取以确定要存储的值。

这有点模糊,造成混乱,这就是C11为什么抛出它并引入新的测序模型的部分原因。

它试图说的是:如果读取旧值保证发生的时间早于写入新值,那就没有问题了。 否则它是UB。 当然,这是一个新的价值在计算之前被计算的要求。

(当然,我刚刚写的描述会被一些人发现比标准文本更模糊!)

例如x = x + 5是正确的,因为在不知道x情况下不可能计算x + 5 。 然而a[i] = i++是错误的,因为i不需要读左边的i来计算在i存储的新值。 (我的两个读数分开考虑)。


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

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

有人评论序列点。 在C99中,这个expression式中没有序列点,所以序列点不会进入这个讨论。

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

不会。这不会产生不确定的行为。 a[0]在两个序列点之间只修改一次(第一个序列点在初始化器int a[3] = {0, 0, 0};第二个序列在完整expression式a[a[0]] = 1 )。

它没有提到读取对象来确定要修改的对象本身。 因此这个陈述可能会产生未定义的行为。

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

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

报价的第二个陈述说:

此外, 先前的值应该是只读的,以确定要存储的值。

读取上述expression式中的所有x来确定对象x本身的值。


注:请注意,问题中提到的引号有两个部分。 第一部分说: 在前一个和下一个序列点之间,一个对象应该通过评估一个expression式来最多修改一次对象的存储值。 ,和
所以expression就像

 i = i++; 

属于UB(在前一个和下一个序列点之间的两个修改)。

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

 a[i++] = i; j = (i = 2) + i; 

调用UB。 在这两个expression式中, i只在前一个和下一个序列点之间修改一次,但是最右侧的读数不能确定要存储在i的值。


在C11标准中,这已经改变为

6.5expression式:

如果对标量对象的副作用不是相对于同一个标量对象的不同副作用或使用相同标量对象的值进行值计算而言是不确定的,则行为是未定义的。 […]

在expression式a[a[0]] = 1a[a[0]] = 1只有一个副作用,在a[0]的值计算之前,索引a[0]的值计算被sorting。

C99列举了附录C中所有序列点的列举。在附录C中有一个序列点

 a[a[0]] = 1; 

因为它是一个完整的expression式语句,但里面没有序列点。 尽pipe逻辑规定必须首先评估子expression式a[0] ,并且用于确定赋值给哪个数组元素的结果,但是sorting规则不能确保它。 当a[0]的初始值为0 ,在两个序列点之间读取和写入a[0] ,读取不是为了确定要写入什么值。 根据C99 6.5 / 2,评估expression的行为因此是不确定的,但在实践中,我不认为你需要担心。

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

expression式是一系列运算符和操作数,它们指定值的计算,或者指定对象或函数,或者产生副作用,或者执行其组合。 运算符操作数的值计算在运算符结果的值计算之前被sorting。

特别要注意第二句,在C99中没有类似的意思。 你可能认为这样就足够了,但事实并非如此。 它适用于数值计算 ,但没有提到与计算值有关的副作用的sorting。 更新左操作数的值是一个副作用,所以额外的句子不会直接应用。

不过,C11在这方面还是值得的,因为赋值运算符的规范提供了所需的sorting(C11 6.5.16(3)):

[…]更新左操作数的存储值的副作用在左和右操作数的值计算之后被sorting。 操作数的评估是不确定的。

(相反,C99只是说更新左操作数的存储值发生在前一个序列点和下一个序列点之间)。6.5节和6.5.16节一起,C11给出了一个定义良好的序列:inner []是在外部[]之前进行评估,在存储值更新之前对其进行评估。 这符合6.5(2)的C11版本,所以在C11中,定义了评估expression式的行为。

除非a[0]包含的值不是一个有效的数组索引(即,在你的代码中不是负数且不超过3 ),否则该值已被很好地定义。 您可以将代码更改为更具可读性和等效性

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

在expression式a[a[0]] = 1 ,有必要先评估a[0] 。 如果a[0]恰好为零,那么a[0]将被修改。 但是编译器(没有遵守标准)没有办法改变评估顺序,并在尝试读取它的值之前修改a[0]

副作用包括对象1的修改。

C标准规定,如果对同一个对象的副作用或者使用同一对象2的值进行计算,对象的副作用是不确定的,那么行为是不确定的。

该expression式中的对象a[0]被修改(副作用),并且它的值(值计算)被用于确定索引。 看起来这个expression式产生未定义的行为:

 a[a[0]] = 1 

但标准中赋值运算符中的文本解释了运算符左右操作数的值计算在左操作数被修改之前被sorting3

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


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

2 (引自ISO / IEC 9899:201x 6.5expression式2):
如果对标量对象的副作用不是相对于同一个标量对象的不同副作用或使用相同标量对象的值进行值计算而言是不确定的,则行为是未定义的。

3 (引自ISO / IEC 9899:201x 6.5.16作业人员3):
更新左操作数的存储值的副作用在左和右操作数的值计算之后被sorting。 操作数的评估是不确定的。