这个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表达式中):
在前一个和下一个序列点之间,对象的存储值最多只能通过表达式的计算修改一次。此外,先前的值应该是只读的,以确定要存储的值。
没有提到读取对象来确定要修改的对象本身。因此,此语句可能会产生未定义的行为。
然而,我觉得很奇怪。这实际上是否会产生未定义的行为?
(我也想知道其他ISO C版本中的这个问题。)
答案 0 :(得分:51)
先前的值应该是只读的,以确定要存储的值。
这有点含糊不清并引起混淆,这也是C11将其抛弃并引入新的测序模型的部分原因。
它试图说的是:如果保证读取旧值的时间早于写入新值,那么这很好。否则就是UB。当然,要求在编写新值之前计算新值。
(当然,我刚才写的描述会比标准文本更模糊!)
例如x = x + 5
是正确的,因为如果不事先了解x + 5
,就无法计算x
。但是a[i] = i++
是错误的,因为为了计算要存储在i
中的新值,不需要在左侧读取i
。 (i
的两个读数分别考虑。)
立即返回您的代码。我认为这是明确定义的行为,因为为了确定数组索引而读取a[0]
保证在写入之前发生。
在我们确定要写的地方之前,我们不能写。在我们阅读a[0]
之后,我们才知道在哪里写。因此,读取必须在写入之前进行,因此没有UB。
有人评论了序列点。在C99中,此表达式中没有序列点,因此序列点不会进入此讨论。
答案 1 :(得分:16)
此C99代码是否会产生未定义的行为?
没有。它不会产生未定义的行为。 a[0]
仅在两个sequence points之间修改一次(第一个序列点位于初始化程序int a[3] = {0, 0, 0};
的末尾,第二个序列点位于完整表达式a[a[0]] = 1
之后)。
没有提到读取对象来确定要修改的对象本身。因此,此语句可能会产生未定义的行为。
可以多次读取一个对象来修改自身及其完美定义的行为。看看这个例子
int x = 10;
x = x*x + 2*x + x%5;
引言的第二个陈述说:
此外,先前值应为只读以确定要存储的值。
读取上述表达式中的所有x
以确定对象x
本身的值。
注意:请注意,问题中提到的报价分为两部分。第一部分说:在上一个和下一个序列点之间,一个对象的存储值最多只能通过表达式的评估修改一次。和
因此表达式如
i = i++;
属于UB(前一个和下一个序列点之间的两次修改)。
第二部分说:此外,先前的值应该是只读的,以确定要存储的值。,因此表达式如
a[i++] = i;
j = (i = 2) + i;
调用UB。在两个表达式中i
仅在前一个和下一个序列点之间修改一次,但最右侧i
的读数不确定要存储在i
中的值。
在C11标准中,这已改为
如果相对于同一标量对象的不同副作用或使用相同标量对象的值进行值计算,对标量对象的副作用是未排序的,则行为未定义。 [...]
在表达式a[a[0]] = 1
中,a[0]
只有一个副作用,并且在a[0]
的值计算之前对索引a[a[0]]
的值计算进行了排序。
答案 2 :(得分:13)
C99列出附件C中所有序列点的列举。
末尾有一个a[a[0]] = 1;
因为它是一个完整的表达式语句,但里面没有序列点。虽然逻辑规定必须首先计算子表达式a[0]
,并且结果用于确定赋值的数组元素,但排序规则不能确保它。当a[0]
的初始值为0
时,a[0]
在两个序列点之间读取和写入,并且读取不以确定什么要写的价值。因此,根据C99 6.5 / 2,评估表达式的行为是不确定的,但实际上我并不认为你需要担心它。
表达式是一系列运算符和操作数,用于指定值的计算,或指定对象或函数,或生成副作用或执行其组合的操作和操作数。在运算符结果的值计算之前,对运算符的操作数的值计算进行排序。
特别注意第二句,它在C99中没有类似物。你可能认为这就足够了,但它不是。它适用于值计算,但它没有说明相对于值计算的副作用的排序。更新左操作数的值是副作用,因此额外的句子不会直接应用。
尽管如此,C11仍然为我们提供了这个,因为赋值运算符的规范提供了所需的排序(C11 6.5.16(3)):[...]更新左操作数的存储值的副作用是 在左右操作数的值计算之后排序。对操作数的评估是不合理的。
(相比之下,C99只是说更新左操作数的存储值发生在前一个和下一个序列点之间。)对于6.5和6.5.16节,然后,C11给出了明确定义的序列:内部在[]
外部评估[]
,在更新存储值之前对其进行评估。这满足C11的6.5(2)版本,所以在C11中,定义了表达式的评估行为。
答案 3 :(得分:5)
除非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]
。
答案 4 :(得分:1)
副作用包括修改对象 1 。
C标准表示,如果对对象的副作用未对同一对象产生副作用或使用同一对象 2 的值进行值计算,则行为未定义。
此表达式中的对象a[0]
被修改(副作用),它的值(值计算)用于确定索引。看起来这个表达式会产生未定义的行为:
a[a[0]] = 1
然而,标准中赋值运算符中的文本解释了运算符=
的左右操作数的值计算在左操作数被修改 3 之前被排序。
因此定义了行为,因为第一个规则 1 未被违反,因为修改(副作用)在同一对象的值计算之后被排序。
1 (引自ISO / IEC 9899:201x 5.1.2.3程序Exectution 2):
访问易失性对象,修改对象,修改文件或调用函数
那些操作中的任何一个都是副作用,这些都是状态的变化
执行环境。
2 (引自ISO / IEC 9899:201x 6.5表达式2):
如果相对于不同的副作用,对标量对象的副作用是无效的
在相同的标量对象上或使用相同标量的值进行值计算
对象,行为未定义。
3 (引自ISO / IEC 9899:201x 6.5.16分配操作员3):
更新左操作数的存储值的副作用是
在左右操作数的值计算之后排序。评估
操作数没有排序。