是 a[i]=y++;和 a[i++]=y;未定义的行为或未指定的 C 语言?

Are a[i]=y++; and a[i++]=y; undefined behavior or unspecified in C language?

提问人:三六九 提问时间:1/13/2023 最后编辑:Eric Postpischil三六九 更新时间:1/13/2023 访问量:171

问:

当我在寻找表达式为什么要定义行为时,我突然看到了一个解释,因为表达式存在于程序中的两个序列点之间,而 c 标准规定在两个序列点中副作用的发生顺序是不确定的,所以当表达式在程序中运行时, 不确定是先操作运算符还是先操作 = 运算符。我对此感到困惑。在评估表达式的过程里,是不是应该先用优先级来判断,然后再引入序列点来判断哪个子表达式先执行?我错过了什么吗?v[i++]=i;++

用户AnT站在俄罗斯的立场这样解释时,是不是意味着在代码中编写或在程序中不能确定操作员和操作员无法确定谁先运行。a[i]=y++;a[i++]=y;++=

C 未定义行为 赋值运算符 量后 副作用

评论

1赞 Robert Harvey 1/13/2023
这就是为什么您总是在单独的代码行中编写增量和赋值操作的原因。这样一来,你和你之后的程序员就不必进行这些心理体操了。
6赞 Some programmer dude 1/13/2023
由于两者在赋值的两端都使用不同的变量,因此没有问题。这一切都是明确定义的。a[i]=y++a[i++]=y
0赞 Eric Postpischil 1/13/2023
@Someprogrammerdude:在断言这些值之前,我们需要知道 的值。 如果指向 和 为零,则不修改不同的对象,如果指向 和 为零,则不修改不同的对象。aa[i] = y++ayia[i++] = yaii

答:

5赞 dbush 1/13/2023 #1

原因是未定义行为是因为变量在同一个表达式中读取和写入,而没有排序。v[i++]=i;i

表达式(如 和 )不会表现出未定义的行为,因为如果没有排序,表达式中不会同时读取和写入任何变量。a[i]=y++a[i++]=y

但是,运算符确实确保在分配到左侧的副作用之前对其两个操作数进行全面评估。具体来说,计算结果为指定数组第 i个元素的左值,并被计算为 的当前值。=a[i]ay++y

评论

0赞 Lundin 1/13/2023
“因为变量 i 在同一个表达式中既被读取又被写入”,呃,好吧,也可以这样说。i++
1赞 Lundin 1/13/2023
标准的说法是“因为对对象有一种副作用,而这种副作用与同一对象的值计算有关,是未排序的”。诚然,这是一个让任何人在阅读后都更聪明的措辞......
0赞 Andrew Henle 1/13/2023
@Lundin 也许“在没有排序的情况下以相同的表达方式读取、写入和修改”?
0赞 Lundin 1/13/2023
@AndrewHenle 就是这样,是吗?:)i++
0赞 dbush 1/14/2023
@三六九 唯一的保证是增量的副作用发生在下一个序列点。
0赞 nielsen 1/13/2023 #2

C 标准(C11 草案)对后缀 ++ 运算符有如下说明:

(6.5.2.4.2) 后缀++运算符的结果是操作数的值。作为副作用,操作数对象的值递增(即,将相应类型的值 1 添加到其中)。[...]

序列点由代码中的一个点定义,在该点之前,可以保证该点之前的所有副作用都已生效,并且在该点生效后没有副作用。

表达式中没有中间序列点。因此,没有定义表达式(递增)的副作用是在评估右侧之前还是之后生效。因此,此表达式中未定义的是右侧的值。v[i++] = i;i++iii

表达式中不存在此问题,因为右侧的值不受 的副作用的影响。a[i++] = y;yi++

0赞 Useless 1/13/2023 #3

当表达式被计算时 在进程中

哪种表达方式?

v[i++]=i;

是一个语句。它由一个顶级赋值表达式组成,其中 ab 本身都是表达式。a = b

左手表达式 a 本身是形式,其中 d 是形式的另一个子表达式,是另一个表达式,最终解析为 。c[d]d ++di

如果有帮助,我们可以用伪函数调用风格写出整个内容,比如

assign(array_index(v, increment_and_return_old_value(i)), i);

现在,问题是标准没有告诉我们最终值参数是在 (或 )突变之前还是之后获得的。iiincrement_and_return_old_value(i)i++

...然后引入序列点来判断哪个子表达式先执行?

函数调用中的参数列表不是序列点。未定义函数参数的计算相对顺序(仅在输入函数体之前必须对所有参数进行计算)。,

同样的逻辑也适用于原始代码——标准说没有序列点,所以没有序列点。


这是否意味着在代码中编写或在程序中不能确定操作员和操作员无法确定谁先运行。a[i]=y++;a[i++]=y;++=

问题不在于赋值,而在于评估要赋值的右侧操作数。

而且,在这些情况下,被分配的左侧事物与被分配的右侧之间没有关系。因此,尽管我们仍然无法确定首先评估哪个,但这并不重要

如果我明确地写出来

int *lhs = &a[i];
int rhs = y++;
*lhs = rhs;

那么反转前两条线就没有区别了。它们的相对顺序无关紧要,因此缺乏定义的相对顺序也无关紧要。

相反,为了完整起见,

int *lhs = v[i++];
int rhs = i;
*lhs = rhs;

是前两行的顺序确实很重要的原始情况,并且未指定的事实是一个问题。

3赞 Eric Postpischil 1/13/2023 #4

C标准中的具体规则是C 2018 6.5 2:

如果标量对象上的副作用相对于同一标量对象上的不同副作用或使用同一标量对象的值进行值计算未排序,则行为未定义。如果表达式的子表达式有多个允许的排序,则如果在任何排序中出现这种未排序的副作用,则行为是未定义的。

第一句话是这里的关键一句话。首先,考虑 .这里,in 计算 的值,两者都计算 的值并递增 的存储值。计算 的值是 的值计算。递增的存储值是副作用。为了确定 的行为是否未定义,我们询问副作用是否相对于 上的任何其他副作用或值计算是未排序的。v[i] = i++;iv[i]ii++iiiiiv[i] = i++;ii

没有其他副作用,因此相对于任何其他副作用,它不是未排序的。i

中有一个值计算,但副作用和这个值计算是由后缀运算符的规范排序的。C 2018 6.5.2.4 2说道:i++++

...结果的值计算在更新操作数的存储值的副作用之前进行排序......

因此,我们知道 in 值的计算是在递增存储值的副作用之前排序的。ii++

现在我们考虑 in 的值计算。规范没有告诉我们这一点,所以让我们考虑赋值运算符 .在 C 2018 6.5.16 3 中,赋值规范确实说明了一些关于排序的内容:iv[i]++=

...更新左操作数存储值的副作用是在左操作数和右操作数的值计算之后排序的。操作数的评估是未排序的。

第一句话告诉我们,在左操作数和右操作数的值计算之后,对 的更新进行了排序。但它并没有告诉我们任何关于 in 的值计算的副作用。v[i]++iv[i]

因此,in 的值计算相对于 in 的副作用是未排序的,因此语句的行为不是由 C 标准定义的。iv[i]ii++

在我们有:a[i] = y++;

  • 中的值计算。ia[i]
  • 中的值计算。yy++
  • 更新了 in 中的存储值。yy++
  • 中的值计算。aa[i]
  • 更新了 in 中的存储值。a[i]a[i] = …

唯一更新两次或同时更新和计算的对象是 ,我们从上面知道 in 上的值计算是在更新 之前排序的。因此,此语句不包含相对于同一对象上的另一个副作用或值计算未排序的任何副作用。因此,它的行为并非未被 C 2018 6.5 2 中的规则定义。yyy++y

类似地,在 中,我们有:a[i++] = y;

  • 中的值计算。ia[i++]
  • 更新了 in 中的存储值。ii++
  • 上的值计算。y
  • 中的值计算。aa[i]
  • 更新了 in 中的存储值。a[i]a[i++] = …

同样,只有一个对象上有两个操作,并且这些操作是有序的。C 2018 6.5 2 中的规则并非未定义该行为。

注意

在上面,我们假设指针既不是指针,也不是指针,或者将是或。相反,如果我们考虑以下代码:ava[i]v[i]iy

int y = 3;
int *a = &y;
int i = 0;
a[i] = y++;

然后行为是未定义的,因为 是 ,因此代码更新两次,一次用于赋值,一次用于 ,并且这些更新是未排序的。赋值规范说,左操作数的更新是在结果的值计算(即赋值右侧的值)之后排序的,但增量是副作用,而不是值计算的一部分。因此,这两个更新是未排序的,并且行为不是由 C 标准定义的。a[i]yya[i] = …y++++

3赞 Lundin 1/13/2023 #5

试图清楚地解释“标准”术语:

该标准指出(C17 6.5),在表达式中,变量的副作用可能不会以与同一对象的值计算相关的未排序顺序发生。

要理解这些奇怪的术语:

  • 副作用 = 写入变量或对变量执行读取或写入访问。volatile
  • 值计算 = 从内存中读取值。
  • 未排序 = 访问/评估之间的顺序未指定,也未明确定义。C 具有序列点的概念,序列点是程序中的某些点,当达到这些点时,必须评估以前的副作用。例如,a 引入了一个序列点。当每个部分的计算顺序在下一个序列点之前未明确定义时,表达式的两个部分相对于彼此未排序。(所有序列点的完整列表可在 C17 附录 C 中找到。;

因此,当从标准语翻译成英语时,具有未定义的行为,因为以未指定的顺序写入与同一表达式中的其他读取相关的顺序。我们怎么知道呢?v[i++]=i;ii

  • 赋值运算符说 (6.5.16) “操作数的计算是未排序的”,指的是 的左操作数和右操作数。==
  • 后缀运算符说 (6.5.2.4) “作为副作用, 操作数对象的值递增“和”结果的值计算在更新操作数的存储值的副作用之前进行排序”。在实践中,这意味着首先读取,然后应用,尽管在下一个序列点之前,在本例中为 .++i++;

如果万一或所有事情都发生在不同的变量上。有两个副作用,更新(或)和更新,但它们是在不同的对象上完成的,因此这两个示例都是明确定义的。a[i]=y++;a[i++]=y;iya[i]

评论

0赞 John Bollinger 1/13/2023
“值计算”是指确定(子)表达式在程序执行过程中的某个时间点所表示的值。这通常涉及从内存(或至少从寄存器)读取,但它的定义很差,从内存中读取值。事实上,该规范有一个特定的、完全不同的术语来从内存中读取对象的值:“左值转换”。
0赞 Lundin 1/13/2023
@JohnBollinger 不幸的是,尽管 C11 一直在使用这个术语,但“价值计算”没有正式的定义。从 C99 到 C11 的整个重写是 IMO 的一次大惨败,它没有改变多处理上下文之外的任何具体内容,但使所有内容都更难阅读。
0赞 John Bollinger 1/13/2023
事实上,该规范没有提供“价值计算”的正式定义。因此,我们应该尝试根据普通英语来理解该术语的含义。我认为,我在之前的评论中提出的定义非常匹配,并且与所有规范对该术语的用法一致。您给出的含义也不合适,并且规范中有一个不同的、明确定义的术语来表达该含义。
0赞 Lundin 1/13/2023
@JohnBollinger 好吧,我不打算争论。因为这个答案的目的不是写一个能满足对 C 有深入了解的语言律师的解释,而是写一个普通 C 程序员可能理解的解释。可能存在简化。
0赞 supercat 1/15/2023
@JohnBollinger:如果该标准定义了“解析”左值以产生“参考”的概念,则该标准的许多方面可以大大简化。代码,左值没有被计算,但代码显然在用它做一些事情。如果步骤序列“解析左值以形成指针;“在指针处访问存储”被视为一种操作,通常,对于这两个步骤之间发生的任何事情,该操作都是不确定的顺序。int *p = &structArray[i++].someMember;structArray[i++]