C语言中的序列点

37

在命令式编程中,序列点指的是程序执行过程中的任何一个点,在该点之前的所有运算已经产生了所有副作用,并且在该点之后的所有运算尚未产生任何副作用。

这意味着什么?请有人用简单的语言解释一下吗?


可能是序列点和部分顺序的重复问题。 - jev
4个回答

61

当发生序列点时,这基本上意味着你可以保证所有之前的操作都已经完成。

在没有介入序列点的情况下两次更改变量的值是未定义行为的一个例子。

例如,i = i++;是未定义的,因为两个对i的更改之间没有序列点。

需要注意的是,不仅是两次改变变量的值可能会导致问题。任何其他使用中涉及到的值的更改同样有可能会产生问题。在讨论如何排序事物时,标准使用“值计算和副作用”这个术语。例如,在表达式a = i + i++中,i(值计算)和i++(副作用)可能以任意顺序执行。

维基百科有一份C和C++标准规定的序列点列表,但应该始终从ISO标准中获取最权威的列表。以下是C11附录C中列出的序列点(解释经过改编):


以下是标准描述的序列点:

  • 在函数调用中,函数指示符和实际参数的评估之间以及实际调用之间;
  • 在逻辑运算符&&||和逗号的第一个和第二个操作数的评估之间;
  • 在条件运算符?:的第一个操作数和第二个和第三个操作数中的任何一个被评估之间;
  • 完整声明符号的结束;
  • 在完整表达式和下一个要评估的完整表达式之间。以下是完整表达式:
    • 初始化表达式;
    • 表达式语句中的表达式;
    • 选择语句(ifswitch)的控制表达式;
    • whiledo语句的控制表达式;
    • for语句中的每一个表达式;
    • return语句中的表达式。
  • 在库函数返回之前;
  • 在与每个格式化的输入/输出函数转换说明符相关联的操作之后;
  • 在每次调用比较函数之前和之后,以及在调用比较函数并移动传递给该调用的对象之间。

我期待着您更多的信息,Pax。我对 C 语言有基本的了解。您对我之前的问题给出了非常好的解释。 - Jagan
3
如果在没有中间序列点的情况下修改变量的值并将该值用于除确定要存储的值之外的其他任何方式,也是未定义行为。例如,a[i++] = i 是未定义的,因为虽然它只修改了 i 的值一次,但 i 的值用于除确定存储在 i 中的值之外的其他目的。 - Robert Gamble
@Matt:这是一层抽象。只要你编写正确的C代码,无论你的平台是否执行超标乱序执行(Out-of-Order Execution),结果都将是相同的。实际上,即使你编写的代码有错误,由于二进制可执行文件在每种情况下都是相同的,你应该得到相同的结果。 - Oliver Charlesworth
@Matt Joiner 我们需要在上面的优秀答案中加入一个警告:从线程执行的上下文角度来看。OOE通常对指令流是不可见的 - CPU的指令调度器必须确保指令之间的数据依赖关系得到满足。但是,当涉及到内存和缓存时,情况就完全不同了,C和C++标准都非常明确地指出,如果内存完成的顺序很重要,则需要使用内存屏障。 - marko
我理解变量在一个序列点内不能被更新超过一次,因此i=i++是未定义的。但是为什么b= i++ + i也是未定义的?我也收到了编译器的警告。 - Rajesh
显示剩余2条评论

13

关于序列点需要注意的重要事项是它们不是全局的,而应被视为一组本地约束条件。例如,在语句

a = f1(x++) + f2(y++);

在对x++进行评估和调用f1之间存在一个序列点,并且在对y++进行评估和调用f2之间存在另一个序列点。然而,无法保证x在调用f2之前还是之后被递增,也无法保证y在调用x之前还是之后被递增。如果f1更改了y或f2更改了x,则结果将是未定义的(编译器生成的代码可以合法地读取x和y、递增x、调用f1、检查y与先前读取的值是否相同,如果已更改,则继续寻找并销毁所有巴尼视频和商品;不幸的是,我不认为任何真正的编译器会生成实际执行此操作的代码,但这在标准下是允许的)。


如果任何函数修改了x或y,则这是在序列点之后完成的(即在实际调用函数之前的那个点)。行为未指定。 - 2501

7
扩展paxdiablo的答案,提供一个例子。
假设有以下语句:
x = i++ * ++j;

有三个副作用:将 i * (j+1) 的结果赋值给 x,将 i 加 1,将 j 加 1。这些副作用的应用顺序是未指定的;在被评估后 i 和 j 可能会立即增加,或者它们可能在 x 被分配之前都已经被评估但尚未递增,或者它们可能在 x 被分配之后才会被递增。

序列点是所有副作用都被应用的点(x、i 和 j 都已被更新),无论它们被应用的顺序如何。


11
然而,我们应该指出x = i++ * ++j的结果是明确定义的,不像paxdiablo的i=i++的例子。 - Oliver Charlesworth

4
这意味着编译器可能会执行花哨的优化、技巧和魔法,但必须在所谓的序列点达到一个明确定义的状态。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接