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

Mas*_*ara 56 c c99 undefined-behavior language-lawyer

这个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;
}
Run Code Online (Sandbox Code Playgroud)

在声明中a[a[0]] = 1;,a[0]都是读取和修改.

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

在前一个和下一个序列点之间,对象的存储值最多只能通过表达式的计算来修改一次.此外,先前的值应该只读以确定要存储的值.

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

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

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

M.M*_*M.M 52

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

这有点模糊,引起了混乱,这也是C11将其抛弃并引入新的测序模型的部分原因.

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

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

例如x = x + 5是正确的,因为没有x + 5事先知道就无法解决问题x.然而a[i] = i++是错误的,因为i为了计算出存储的新值,不需要在左侧读取i.(两个读数i分别考虑).


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

在我们确定在哪里写之前,我们不能写.在我们阅读之后,我们不知道在哪里写a[0].因此,读取必须在写入之前进行,因此没有UB.

有人评论了序列点.在C99中,此表达式中没有序列点,因此序列点不会进入此讨论.

  • @MasakiHara; ISO C99对此没有任何错误,除非声明有点模糊. (5认同)
  • 国际海事组织如果C99中的陈述旨在表示你所说的意图,那么它比"有点模糊"更糟糕,它有缺陷,因为它没有授权它想要的一切."确定要存储的值"对于它是否包括"确定存储值的位置"并不含糊:它不包括它.C11的作者似乎普遍认为C99是错误的.OTOH,如果编译器编写者普遍按照你的意思解释它,那么我们至少有一个事实上的保证比C99的作者设法实际写下来更强:-) (5认同)
  • 在我的脑海中 - 在C11中,在执行赋值之前评估操作数是*排序,所以它不是UB. (2认同)
  • 我觉得你错了.从引文中可以清楚地看出,`a [a [0]] = 1`会调用未定义的行为.如果假定严格按顺序执行CPU指令,其中指令的所有副作用(包括电子电路上的瞬态过程)在开始执行下一条指令之前完成,则这似乎毫无意义.这适用于现代主流架构.但是,也有人试图开发超标量体系结构,但可能并非如此. (2认同)

hac*_*cks 16

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

不会.它不会产生不确定的行为.a[0]在两个序列点之间仅修改一次(第一个序列点位于初始化程序的末尾,int a[3] = {0, 0, 0};第二个序列点位于完整表达式之后a[a[0]] = 1).

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

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

int x = 10;
x = x*x + 2*x + x%5;   
Run Code Online (Sandbox Code Playgroud)

报价的第二个陈述说:

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

x读取上述表达式中的所有内容以确定对象x本身的值.


注意:请注意,问题中提到的引用分为两部分.第一部分说:在上一个和下一个序列点之间,一个对象的表达式的评估最多只能修改一次.,
因此表达就像

i = i++;
Run Code Online (Sandbox Code Playgroud)

来自UB(前一个和下一个序列点之间的两个修改).

第二部分说:此外,先前的值应该是只读的,以确定要存储的值.,因此表达式如

a[i++] = i;
j = (i = 2) + i;  
Run Code Online (Sandbox Code Playgroud)

调用UB.在两个表达式i中,只在前一个和下一个序列点之间修改一次,但最右边的读数i不确定要存储的值i.


在C11标准中,这已经改为

6.5表达式:

如果相对于对同一标量对象的不同副作用或使用相同标量对象的值进行值计算,对标量对象的副作用未被排序,则行为未定义.[...]

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

  • 标准说:读取先前值**以确定要存储的值**是可以的.但是,没有提到读取先前值**以确定对象本身**. (4认同)
  • 这是最好的答案,因为它是唯一一个甚至提到序列点的人.我觉得其他人没有意识到"只有一个逻辑顺序可以评估"和"它只在两个序列点之间被修改一次,因此不是UB"之间存在差异.我已经看到很多序列点违规(当然是UB)似乎只有一个合理的数学解释 (2认同)
  • @haccks,是的,这就是你的示例表达式定义行为的原因,正如你在答案中提到的那样.但OP的表达方式也是如此. (2认同)

Joh*_*ger 13

C99列出了附件C中所有序列点的列举.最后有一个

a[a[0]] = 1;
Run Code Online (Sandbox Code Playgroud)

因为它是一个完整的表达式语句,但里面没有序列点.尽管逻辑规定a[0]必须首先计算子表达式,并且结果用于确定赋值的数组元素,但排序规则并不能确保它.当初始值为a[0]is时0,a[0]在两个序列点之间读取和写入,并且读取不是为了确定要写入的值.根据C99 6.5/2,评估表达式的行为因此未定义,但在实践中我认为您不必担心它.

在这方面,C11更好.第6.5节第(1)段说

表达式是操作符和操作数的序列,其指定值的计算,或指定对象或函数,或者生成副作用,或执行其组合.在运算符的结果的值计算之前,对运算符的操作数的值计算进行排序.

请特别注意第二句,它在C99中没有类似物.你可能认为这就足够了,但事实并非如此.它适用于值计算,但它没有说明相对于值计算的副作用的排序.更新左操作数的值是副作用,因此不会直接应用额外的句子.

尽管如此,C11仍然为我们提供了,因为赋值运算符的规范提供了所需的排序(C11 6.5.16(3)):

[...]在左右操作数的值计算之后,对更新左操作数的存储值的副作用进行排序.对操作数的评估是不确定的.

(相比之下,C99只是说更新左操作数的存储值发生在前一个和下一个序列点之间.)将6.5和6.5.16组合在一起,然后,C11给出一个明确定义的序列:内部[]在之前被评估外部[],在更新存储值之前进行评估.这满足C11的6.5(2)版本,所以在C11中,定义了评估表达式的行为.


Pet*_*ter 5

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

 index = a[0];
 a[index] = 1;    /* still UB if index < 0 || index >= 3 */
Run Code Online (Sandbox Code Playgroud)

在表达式中a[a[0]] = 1,首先需要进行评估a[0].如果a[0]恰好为零,那么a[0]将被修改.但是,a[0]在尝试读取其值之前,编译器(不遵守标准)无法改变评估顺序和修改.

  • "没有其他方法可以评估"并不意味着代码没有被定义.未定义的内容在标准中单独说明.引号中的"shall"一词(参见上面的问题)意味着如果约束未定义,则行为未定义.我的问题是为什么代码可以根据标准有效. (4认同)
  • 它是.给定`a [b]`形式的任何有效表达式,有必要在a [b]`被评估之前评估表达式`a`和表达式`b`.这个逻辑是递归的. (3认同)
  • @Peter,具有未定义的行为在这种情况下是代码的特征,而不是以任何方式执行它的环境的函数.实际上,您可以期望编译器生成的代码可以实现与预期无关的代码.一个符合标准的编译器*可以*生成几乎任何东西的代码,例如打印"你的耻辱!" 到`stderr`,作为评估表达式的行为.它不会因此而不符合,尽管它可能不受欢迎. (2认同)