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是逗号运算符; in func(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
这是什么意思?这意味着,如果将对象写入完整表达式内,则在同一表达式内对其的所有访问都必须直接参与要写入的值的计算。
例如,在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
(其中的一个a[i]
)与最终存储在i中的值无关(它发生在中i++
),因此没有很好的定义方式-无论是出于我们的理解还是编译器的访问是否应该在存储增量值之前或之后进行。因此,行为是不确定的。
例子3:
int x = i + i++ ;// Similar to above
在此处跟踪C ++ 11的答案。
*p++ = 4
不是未定义的行为。*p++
被解释为*(p++)
。p++
返回p
(副本),并将值存储在先前的地址中。为什么要调用UB?很好。