C++98 和 C++03
此答案适用于旧版本的 C++ 标准。标准的 C++11 和 C++14 版本不正式包含“序列点”;操作是“先排序”或“未排序”或“不确定排序”。净效果基本相同,但术语不同。
免责声明:好的。这个答案有点长。所以阅读时要有耐心。如果你已经知道这些东西,再读一遍也不会让你发疯。
先决条件: C++标准的基本知识
什么是序列点?
标准说
在称为序列点的执行序列中的某些指定点处,之前评估的所有副作用都应该是完整的,并且后续评估的副作用应该没有发生。(§1.9/7)
副作用?什么是副作用?
表达式的求值会产生一些东西,如果另外执行环境的状态发生变化,则表示表达式(其求值)有一些副作用。
例如:
int x = y++; //where y is also an int
除了初始化操作之外,y
由于操作符的副作用,值也会发生变化++
。
到现在为止还挺好。继续到序列点。comp.lang.c 作者给出的 seq-points 的替代定义Steve Summit
:
序列点是尘埃落定的时间点,到目前为止已经看到的所有副作用都可以保证是完整的。
C++ 标准中列出的常见序列点是什么?
那些是:
在完整表达式 ( §1.9/16
) 的计算结束时(完整表达式是不是另一个表达式的子表达式的表达式。)1
例子 :
int a = 5; // ; is a sequence point here
§1.9/18
在对第一个表达式 ( )的求值之后的以下每个表达式的求值中
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:允许的未定义行为范围从完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以环境特征的记录方式表现(有或没有发出诊断消息),到终止翻译或执行(伴随诊断信息的发布)。
简而言之,未定义的行为意味着从你的鼻子飞出的守护进程到你的女朋友怀孕,任何事情都可能发生。
未定义行为和序列点之间有什么关系?
在我开始之前,您必须知道Undefined Behaviour、Unspecified Behavior 和 Implementation Defined Behavior之间的区别。
你也必须知道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)在前一个和下一个序列点之间,一个标量对象的存储值最多只能通过表达式的评估修改一次。
这是什么意思?
非正式地,这意味着在两个序列点之间,一个变量不能被多次修改。在表达式语句中,thenext sequence point
通常位于终止分号处,而 theprevious sequence point
位于前一条语句的末尾。一个表达式也可能包含 middle 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
(the one in a[i]
) 的访问之一与最终存储在 i 中的值无关(这发生在 in i++
),因此没有好的方法来定义 - 无论是为了我们的理解还是为了编译器的——访问是在存储增量值之前还是之后进行。所以行为是不确定的。
示例 3:
int x = i + i++ ;// Similar to above
在此处跟进 C++11 的答案。