跳转到内容

C 编程/副作用和顺序点

来自 Wikibooks,开放世界中的开放书籍
上一个:常见实践 C 编程 下一个:序列化

在 C 中,更普遍地说,在计算机科学中,如果函数或表达式修改了其范围之外的状态,或者与其调用函数或外部世界有可观察的交互,则该函数或表达式被称为具有副作用。按照惯例,返回值会影响调用函数,但这通常不被认为是副作用。

一些副作用是

  • 修改全局变量或静态变量
  • 修改函数参数
  • 将数据写入显示器或文件
  • 读取数据
  • 调用其他具有副作用的函数

在存在副作用的情况下,程序的行为可能取决于历史;也就是说,求值的顺序很重要。了解和调试具有副作用的函数需要了解上下文及其可能的历史。[1][2]

顺序点定义了计算机程序执行中的任何一点,在该点,保证所有先前求值的副作用都已执行,并且尚未执行后续求值的任何副作用。它们在 C 的引用中经常被提及,因为它们是确定表达式的有效性以及如果有效,其可能结果的核心概念。有时需要添加更多顺序点来使表达式定义,并确保唯一的有效求值顺序。

  1. 一个表达式的求值可以先于另一个表达式的求值,或者等效地,另一个表达式的求值后于第一个表达式的求值。
  2. 表达式的求值是不确定顺序的,这意味着一个是先于另一个,但哪个是未指定的。
  3. 表达式的求值是无序的

无序求值的执行可能重叠,如果它们共享状态,则会导致灾难性的未定义行为。这种情况可能出现在并行计算中,导致竞争条件。

歧义示例

[编辑 | 编辑源代码]

考虑两个函数f()g()。在 C 中,+运算符没有与顺序点相关联,因此在表达式f()+g()中,f()g()有可能先执行。逗号运算符引入了顺序点,因此在代码f(),g()中,求值的顺序是定义的:首先调用f(),然后调用g()

当在单个表达式中多次修改同一个变量时,顺序点也会发挥作用。一个经常被引用的例子是 C 表达式i=i++,它明显地将i分配给它之前的值,并递增ii的最终值是不明确的,因为根据表达式求值的顺序,递增可能发生在赋值之前、之后或与赋值交织在一起。特定语言的定义可能指定一种可能的行为,或者只是说行为是未定义的。在 C 中,求值此类表达式会导致未定义行为。[3]

在 C[4]中,顺序点出现在以下位置。

  1. &&(逻辑与)、||(逻辑或)(作为短路求值的一部分)和逗号运算符的左侧和右侧操作数的求值之间。例如,在表达式*p++ != 0 && *q++ != 0中,*p++ != 0子表达式的所有副作用在尝试访问q之前完成。
  2. 在三元“问号”运算符的第一个操作数和第二个或第三个操作数的求值之间。例如,在表达式a = (*p++) ? (*p++) : 0中,在第一个*p++之后有一个顺序点,这意味着它在执行第二个实例之前已经被递增。
  3. 在完整表达式结束时。此类别包括表达式语句(例如赋值a=b;)、return 语句、ifswitchwhiledo-while语句的控制表达式,以及for语句中的所有三个表达式。
  4. 在函数调用中,函数被进入之前。参数求值的顺序没有指定,但此顺序点意味着所有参数的副作用在函数被进入之前都已完成。在表达式f(i++) + g(j++) + h(k++)中,f被调用,参数为i的原始值,但在进入f的主体之前,i被递增。类似地,在进入gh之前,分别更新jk。但是,没有指定f()g()h()执行的顺序,也没有指定ijk递增的顺序。如果f的主体访问变量jk,它可能会发现这两个变量都被递增了,或者都没有被递增,或者只有一个变量被递增。(函数调用f(a,b,c)不是逗号运算符的用法;abc的求值顺序是未指定的。)
  5. 在函数返回时,返回值被复制到调用上下文之后。(此顺序点仅在 C++ 标准中指定;它仅在 C 中隐式存在。)
  6. 在初始化程序结束时;例如,在声明int a = 5;中,在对5求值之后。
  7. 在每个声明序列中的每个声明符之间;例如,在int x = a++, y = a++中,在对a++的两次求值之间。(这不是逗号运算符的例子。)
  8. 在与输入/输出格式说明符关联的每个转换之后。例如,在表达式printf("foo %n %d", &a, 42)中,在%n被求值之后,并在打印42之前,有一个顺序点。

参考文献

[编辑 | 编辑源代码]
  1. “函数式编程研究主题” D. Turner 编著,Addison-Wesley,1990 年,第 17-42 页。检索自:Hughes, John, 为什么函数式编程很重要 (PDF)
  2. Collberg, CSc 520 编程语言原理,亚利桑那大学计算机科学系
  3. C99 规范的第 6.5 章 #2 条:“在先前和下一个顺序点之间,对象在其存储值最多被表达式求值修改一次。此外,仅访问先前值以确定要存储的值。
  4. C99 规范的附录 C 列出了可以假设顺序点的情况。
[编辑 | 编辑源代码]


上一个:常见实践 C 编程 下一个:序列化
华夏公益教科书