当我在寻找表达式v[i++]=i;
为什么要定义行为时,我突然看到了一个解释,因为该表达式存在于程序中的两个序列点之间,而c标准规定在这两个序列点中副作用的发生顺序是不确定的,所以当该表达式在程序中运行时,不确定是首先操作CCD_ 2运算符还是首先操作=运算符。我对此感到困惑。当表达式被求值时,在这个过程中,不应该先用优先级来判断,然后引入序列点来判断哪个子表达式首先被执行吗?我是不是错过了什么?
当用户AnT站在俄罗斯一边这样解释时,是否意味着在程序中写入a[i]=y++;
或a[i++]=y;
等代码不能确定++
运算符和=
运算符不能确定谁先运行。
v[i++]=i;
是未定义行为的原因是,变量i
在同一表达式中读取和写入,而不进行排序。
像a[i]=y++
和a[i++]=y
这样的表达式不会表现出未定义的行为,因为在没有排序的情况下,没有变量可以在表达式中同时读取和写入。
然而,=
运算符确实确保在赋值到左侧的副作用之前,对其两个操作数进行完全求值。具体地,a[i]
被评估为指定阵列a
的第i个元素的左值,并且y++
被评估为y
的当前值。
C标准中的具体规则是C 2018 6.5 2:
如果标量对象上的副作用相对于同一标量对象上不同的副作用或使用同一标量的值进行的值计算是未排序的,则行为是未定义的。如果一个表达式的子表达式有多个可允许的顺序,那么如果在任何顺序中出现这种未排序的副作用,则行为是未定义的。
这里的第一句话很关键。首先,考虑v[i] = i++;
。这里,v[i]
中的i
计算i
的值,而++
0两者都计算i
的值并递增存储的i
的值。计算CCD_ 23的值是CCD_ 24的值计算。增加i
的存储值是一个副作用。为了确定v[i] = i++;
的行为是否未定义,我们询问副作用是否相对于i
上的任何其他副作用或相对于i
上的值计算是未排序的。
i
没有其他副作用,因此相对于任何其他副作用而言,它不是无序列的。
i++
中有一个值计算,但副作用和这个值计算是按照后缀++
运算符的规范排序的。C 2018 6.5.2.4 2说:
…在更新操作数存储值的副作用之前,对结果的值计算进行排序…
因此,我们知道i++
中i
的值的计算是在增加存储值的副作用之前进行的。
现在我们考虑CCD_ 35中CCD_ 34的值计算。++
规范没有告诉我们这一点,所以让我们考虑赋值运算符=
。分配的规范确实说明了一些关于测序的内容,在C 2018 6.5.16 3:中
…更新左操作数存储值的副作用在左操作数和右操作数的值计算之后排序。操作数的求值是无序列的。
第一句告诉我们v[i]
的更新是在左和右操作数的值计算之后排序的。但相对于v[i]
中i
的值计算,它并没有告诉我们++
中的任何副作用。
因此,相对于i++
中i
的副作用,v[i]
中i
的值计算是无序列的,因此该语句的行为不是由C标准定义的。
在a[i] = y++;
中,我们有:
a[i]
中i
的一个值计算- 在CCD_ 50中对CCD_
- 对CCD_ 52中的CCD_ 51的存储值的更新
- CCD_ 54中CCD_
- 对CCD_ 56中的CCD_ 55的存储值的更新
唯一更新两次或同时更新和评估的对象是y
,从上面我们知道,y++
中对y
的值计算是在y
更新之前排序的。因此,相对于同一对象上的另一个副作用或值计算,此语句不包含任何未排序的副作用。因此,它的行为并没有被C 2018 6.5 2中的规则所定义。
类似地,在a[i++] = y;
中,我们有:
a[i++]
中i
的一个值计算- 在
i++
中对i
的存储值进行更新 - CCD_ 66
- 在CCD_ 68中对CCD_
- 对CCD_ 70中存储的CCD_ 69的值进行更新
同样,只有一个对象有两个操作,并且这些操作是按顺序进行的。该行为未被C 2018 6.5 2中的规则所定义。
备注
在上文中,我们假设a
和v
都不是指针,使得a[i]
或v[i]
将是i
或y
。如果我们考虑这个代码:
int y = 3;
int *a = &y;
int i = 0;
a[i] = y++;
然后行为是未定义的,因为a[i]
是y
,所以代码更新y
两次,一次用于分配a[i] = …
,一次为y++
,并且这些更新是未排序的。赋值规范规定,对左操作数的更新是在结果的值计算(即赋值右侧的值)之后排序的,但++
的增量是副作用,而不是值计算的一部分。因此,这两个更新是无序列的,并且行为不是由C标准定义的。
试图解释;标准语";术语简单明了:
该标准规定(C17 6.5),在一个表达式中,变量的副作用相对于同一对象的值计算,可能不会以未排序的顺序出现。
理解这些奇怪的术语:
- 副作用=写入变量或执行对
volatile
变量的读或写访问 - 值计算=从内存中读取值
- Unsequenced=未指定也未定义访问/求值之间的顺序。C有序列点的概念,序列点是程序中的某些点,当达到这些点时,必须评估以前的副作用。例如,
;
引入了一个序列点。当每个部分的求值顺序在下一个序列点之前没有明确定义时,表达式的两个部分相对于彼此是不排序的。(所有序列点的完整列表可在C17附录C中找到。)
因此,当从标准语翻译成英语时,v[i++]=i;
具有未定义的行为,因为i
是以与同一表达式中i
的另一个读取相关的未指定顺序写入的。我们怎么知道的?
- 赋值运算符
=
表示(6.5.16)";操作数的求值是无序列的";,参考CCD_ 89的左操作数和右操作数 - 后缀CCD_ 90运算符表示(6.5.2.4)";作为副作用操作对象的值被递增";以及";在更新操作数"的存储值的副作用之前对结果的值计算进行排序;。在实践中,这意味着首先读取
i
,然后应用++
,尽管在下一个序列点之前,在这种情况下是;
在a[i]=y++;
或a[i++]=y;
的情况下,所有事情都发生在不同的变量上。有两个副作用,更新i
(或y
)和更新a[i]
,但它们是在不同的对象上进行的,因此这两个例子都是明确定义的。
C标准(C11草案)对后缀++运算符有如下规定:
(6.5.2.4.2)后缀++运算符的结果是操作数的值。作为副作用,操作对象的值会递增(即,将适当类型的值1添加到其中)。[…]
序列点由代码中的一个点定义,在该点中,保证该点之前的所有副作用都已生效,而该点之后没有副作用。
在表达v[i++] = i;
中不存在中间序列点。因此,没有定义表达式i++
(递增i
)的副作用是在评估右侧i
之前还是之后生效。因此,在该表达式中未定义的是右侧CCD_ 103的值。
该问题在表达式a[i++] = y;
中不存在,因为右侧y
的值不受i++
的副作用的影响。
在过程中评估表达式时
哪个表达式?
v[i++]=i;
是语句。它由顶级赋值表达式a = b
组成,其中a和b本身都是表达式。
左边的表达式a本身具有形式c[d]
,其中d是形式d ++
的另一个子表达式,而d
是另一个表达式,最终解析为i
。
如果它有帮助的话,我们可以用伪函数调用的风格写出来,比如
assign(array_index(v, increment_and_return_old_value(i)), i);
现在的问题是,标准没有告诉我们最终值参数i
是在i
被increment_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;
是最初的情况,前两行的顺序确实很重要,而未指定的事实是一个问题。