小编典典

未定义的行为和序列点

all

什么是“序列点”?

未定义的行为和序列点之间的关系是什么?

我经常使用有趣而复杂的表达方式,例如a[++i] = i;,让自己感觉更好。为什么我要停止使用它们?

(注意:这是一个条目。如果您想批评以这种形式提供 FAQ的想法,那么开始这一切的 meta上的帖子就是这样做的地方。该问题在C++聊天室中进行监控,FAQ想法最初是从那里开始的,所以你的答案很可能会被提出这个想法的人阅读。)


阅读 119

收藏
2022-02-28

共1个答案

小编典典

C98 和 C03

此答案适用于旧版本的 C 标准。标准的 C11 和 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 DefinedBehavior之间的区别。

你也必须知道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

  • 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(the one in a[i]) 的访问之一与最终存储在 i 中的值无关(这发生在 in
i++),因此没有好的方法来定义——无论是为了我们的理解还是为了编译器的——访问是在存储增量值之前还是之后进行。所以行为是不确定的。

示例 3:

int x = i + i++ ;// Similar to above

2022-02-28