Percy

未定义的行为和顺序点

c

什么是“序列点”?

未定义行为与序列点之间有什么关系?

我经常使用诸如的有趣且令人费解的表情a[++i] = i;来使自己感觉更好。为什么我应该停止使用它们?


阅读 447

收藏
2020-11-24

共1个答案

一尘不染

C 98和C 03
此答案适用于C 标准的较旧版本。该标准的C 11和C ++ 14版本没有正式包含“序列点”。操作是“先于”或“未排序”或“不确定地排序”。最终效果基本相同,但是术语不同。

免责声明:好的。这个答案有点长。因此阅读时要有耐心。如果您已经知道这些事情,那么再次阅读它们不会使您发疯。

先决条件:C ++标准的基础知识

什么是序列点?
标准说

在执行序列中某些特定的点(称为顺序点)上,以前评估的所有副作用都应完整,并且以后评估的副作用都不应发生。(第1.9 / 7节)

副作用?有什么副作用?
对表达式的求值会产生某些结果,并且如果执行环境的状态另外发生变化,则可以说表达式(对其求值)会产生一些副作用。

例如:

int x = y++; //where y is also an int

除了初始化操作外,y由于++运算符的副作用,get的值也会更改。

到现在为止还挺好。继续到序列点。comp.lang.c作者提供的seq点的替代定义Steve Summit:

顺序点是粉尘沉淀的时间点,可以保证到目前为止已经看到的所有副作用都是完整的。

C ++标准中列出了哪些通用序列点?
那些是:

在完整表达式(§1.9/16)评估结束时(完整表达式是一个表达式,而不是另一个表达式的子表达式。)1

范例:

int a = 5; // ; is a sequence point here

在对第一个表达式(§1.9/18)2求值之后对以下每个表达式的求值

a && b (§5.14)
a || b (§5.15)
a ? b : c (§5.16)
a , b (§5.18)(这里a,b是逗号运算符; infunc(a,a) ,不是逗号运算符,它只是参数a和之间的分隔符a。因此,在这种情况下,行为是不确定的(如果a认为是原始类型))
在对所有函数参数(如果有)进行求值之后(在函数主体中执行任何表达式或语句之前),在函数调用时(函数是否为内联§1.9/17)。

1:注意:对完整表达式的评估可以包括对不属于完整表达式的词汇的子表达式的评估。例如,评估默认参数表达式(8.3.6)所涉及的子表达式被认为是在调用函数的表达式中创建的,而不是在定义默认参数的表达式中创建的

2:所指示的运算符是内置运算符,如第5节中所述。当这些运算符之一在有效上下文中被重载(第13节),从而指定了用户定义的运算符函数时,该表达式指定函数调用,并且操作数形成一个参数列表,它们之间没有隐含的序列点。

什么是未定义行为?
该标准在本节§1.3.12中将未定义行为定义为

行为,例如在使用错误的程序构造或错误的数据时可能发生的行为,对此本国际标准不施加任何要求3。

当本国际标准省略对行为的任何明确定义的描述时,也可能会出现未定义的行为。

3:允许的不确定行为,范围从完全忽略情况以无法预测的结果,到在翻译或程序执行期间以环境特征的书面方式记录的行为(有无诊断消息),到终止翻译或执行(发出诊断消息)。

简而言之,未定义的行为意味着从守护程序从鼻子飞出到女友怀孕都可能发生任何事情。

未定义行为和序列点之间有什么关系?
在开始讨论之前,您必须了解“未定义行为”,“未指定行为”和“实现已定义行为”之间的区别。

您还必须知道这一点the order of evaluation of operands of individual operators and subexpressions of individual expressions, and the order in which side effects take place, is unspecified。

例如:

int x = 5, y = 6;

int z = x++ + y++; //it is unspecified whether x++ or y++ will be evaluated first.

这里的另一个例子。

现在标准中§5/4说

1)在上一个序列点与下一个序列点之间,标量对象最多应通过表达式的计算修改其存储值。
这是什么意思?

非正式地,它意味着两个序列点之间的变量不得被多次修改。在表达式语句中,next sequence point通常位于终止分号,而通常位于前一条语句previous sequence point的末尾。表达式也可以包含intermediate sequence points。

从上面的句子中,以下表达式调用未定义的行为:

i++ * ++i;   // UB, i is modified more than once btw two SPs
i = ++i;     // UB, same as above
++i = 2;     // UB, same as above
i = ++i + 1; // UB, same as above
++++++i;     // UB, parsed as (++(++(++i)))

i = (i, ++i, ++i); // UB, there's no SP between `++i` (right most) and assignment to `i` (`i` is modified more than once btw two SPs)

但是以下表达式很好:

i = (i, ++i, 1) + 1; // well defined (AFAIK)
i = (++i, i++, i);   // well defined 
int j = i;
j = (++i, i++, j*i); // well defined

2)此外,应仅访问先验值以确定要存储的值。
这是什么意思?这意味着,如果将对象写入完整表达式内,则在同一表达式内对该对象的任何和所有访问都必须直接参与要写入的值的计算。

例如,在i = i + 1所有访问中i(在LHS和RHS中)直接涉及要写入的值的计算。这样很好

该规则有效地限制了法律表达方式,使其具有明显可在修改之前进行的访问权限。

范例1:

std::printf("%d %d", i,++i); // invokes Undefined Behaviour because of Rule no 2

范例2:

a[i] = i++ // or a[++i] = i or a[i++] = ++i etc

之所以被禁止,是因为i(的in的a[i])访问之一与最终存储在i中的值(发生在i++)没有任何关系,因此没有很好的定义方式-无论是出于我们的理解还是编译器的访问是否应该在存储增量值之前或之后进行。因此,行为是不确定的。

例子3:

int x = i + i++ ;// Similar to above
2020-11-24