C++98 和 C++03
此答案适用于旧版本的 C++ 标准。标准的 C++11 和 C++14 版本不正式包含“序列点”;操作是“先排序”或“未排序”或“不确定排序”。最终效果基本相同,但术语不同。
免责声明:好的。这个答案有点长。所以阅读时要有耐心。如果你已经知道这些东西,再读一遍也不会让你发疯。
先决条件:C++ Standard的基本知识
什么是序列点?
标准说
在称为序列点的执行序列中的某些指定点,之前评估的所有副作用
应是完整的,并且后续评估不会发生副作用。 (§1.9/7)
副作用?什么是副作用?
表达式的求值会产生一些东西,如果另外执行环境的状态发生变化,则表示该表达式(其求值)有一些副作用。
例如:
int x = y++; //where y is also an int
除了初始化操作之外,y 的值也会因为++ 操作符的副作用而改变。
到目前为止一切顺利。继续到序列点。 comp.lang.c作者Steve Summit给出的seq-points的替代定义:
序列点是尘埃落定的时间点,到目前为止已经看到的所有副作用都保证是完整的。
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 是逗号运算符;在 func(a,a++) , 不是逗号运算符,它只是参数 a 和 a++ 之间的分隔符。因此在这种情况下行为是未定义的(如果a 被认为是原始类型))
在函数调用时(无论函数是否内联),在评估所有函数参数(如果有)之后
在函数体 (§1.9/17) 中执行任何表达式或语句之前发生。
1:注意:完整表达式的评估可以包括非词法上的子表达式的评估
完整表达的一部分。例如,评估默认参数表达式(8.3.6)所涉及的子表达式被认为是在调用函数的表达式中创建的,而不是定义默认参数的表达式
2 : 指示的操作符是内置操作符,如第 5 节所述。当这些操作符之一在有效上下文中被重载(第 13 条),从而指定用户定义的操作符函数时,表达式指定一个函数调用,操作数形成一个参数列表,它们之间没有隐含的序列点。
什么是未定义行为?
标准将§1.3.12 部分中的未定义行为定义为
行为,例如在使用错误程序结构或错误数据时可能出现的行为,对此国际标准没有要求3。
当这
国际标准省略了对行为的任何明确定义的描述。
3 :允许的未定义行为的范围从完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以环境特征的记录方式表现(有或有-
发出诊断消息),终止翻译或执行(发出诊断消息)。
简而言之,未定义的行为意味着任何事情都可能发生,从从你的鼻子飞出的守护进程到你的女朋友怀孕。
未定义行为与序列点之间的关系是什么?
在我开始之前,您必须知道Undefined Behaviour, Unspecified Behaviour and Implementation Defined Behaviour 之间的区别。
您还必须知道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.
另一个例子here。
现在§5/4 中的标准说
- 1) 在前一个序列点和下一个序列点之间,一个标量对象的存储值最多只能通过表达式的计算修改一次。
什么意思?
通俗地说,这意味着在两个序列点之间,一个变量不能被多次修改。
在表达式语句中,next sequence point 通常位于分号结束处,previous sequence point 位于前一条语句的末尾。表达式还可能包含中间 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 的所有访问(在 L.H.S 和 R.H.S 中)直接参与计算要写入的值。所以没关系。
此规则有效地将合法表达限制为那些访问明显先于修改的表达。
示例 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 的答案here。