C 编程/副作用和顺序点
外观
< C 编程
在 C 中,更普遍地说,在计算机科学中,如果函数或表达式修改了其范围之外的状态,或者与其调用函数或外部世界有可观察的交互,则该函数或表达式被称为具有副作用。按照惯例,返回值会影响调用函数,但这通常不被认为是副作用。
一些副作用是
- 修改全局变量或静态变量
- 修改函数参数
- 将数据写入显示器或文件
- 读取数据
- 调用其他具有副作用的函数
在存在副作用的情况下,程序的行为可能取决于历史;也就是说,求值的顺序很重要。了解和调试具有副作用的函数需要了解上下文及其可能的历史。[1][2]
顺序点定义了计算机程序执行中的任何一点,在该点,保证所有先前求值的副作用都已执行,并且尚未执行后续求值的任何副作用。它们在 C 的引用中经常被提及,因为它们是确定表达式的有效性以及如果有效,其可能结果的核心概念。有时需要添加更多顺序点来使表达式定义,并确保唯一的有效求值顺序。
- 一个表达式的求值可以先于另一个表达式的求值,或者等效地,另一个表达式的求值后于第一个表达式的求值。
- 表达式的求值是不确定顺序的,这意味着一个是先于另一个,但哪个是未指定的。
- 表达式的求值是无序的。
无序求值的执行可能重叠,如果它们共享状态,则会导致灾难性的未定义行为。这种情况可能出现在并行计算中,导致竞争条件。
考虑两个函数f()和g()。在 C 中,+运算符没有与顺序点相关联,因此在表达式f()+g()中,f()或g()有可能先执行。逗号运算符引入了顺序点,因此在代码f(),g()中,求值的顺序是定义的:首先调用f(),然后调用g()。
当在单个表达式中多次修改同一个变量时,顺序点也会发挥作用。一个经常被引用的例子是 C 表达式i=i++,它明显地将i分配给它之前的值,并递增i。i的最终值是不明确的,因为根据表达式求值的顺序,递增可能发生在赋值之前、之后或与赋值交织在一起。特定语言的定义可能指定一种可能的行为,或者只是说行为是未定义的。在 C 中,求值此类表达式会导致未定义行为。[3]
在 C[4]中,顺序点出现在以下位置。
- 在
&&(逻辑与)、||(逻辑或)(作为短路求值的一部分)和逗号运算符的左侧和右侧操作数的求值之间。例如,在表达式*p++ != 0 && *q++ != 0中,*p++ != 0子表达式的所有副作用在尝试访问q之前完成。 - 在三元“问号”运算符的第一个操作数和第二个或第三个操作数的求值之间。例如,在表达式
a = (*p++) ? (*p++) : 0中,在第一个*p++之后有一个顺序点,这意味着它在执行第二个实例之前已经被递增。 - 在完整表达式结束时。此类别包括表达式语句(例如赋值
a=b;)、return 语句、if、switch、while或do-while语句的控制表达式,以及for语句中的所有三个表达式。 - 在函数调用中,函数被进入之前。参数求值的顺序没有指定,但此顺序点意味着所有参数的副作用在函数被进入之前都已完成。在表达式
f(i++) + g(j++) + h(k++)中,f被调用,参数为i的原始值,但在进入f的主体之前,i被递增。类似地,在进入g和h之前,分别更新j和k。但是,没有指定f()、g()、h()执行的顺序,也没有指定i、j、k递增的顺序。如果f的主体访问变量j和k,它可能会发现这两个变量都被递增了,或者都没有被递增,或者只有一个变量被递增。(函数调用f(a,b,c)不是逗号运算符的用法;a、b和c的求值顺序是未指定的。) - 在函数返回时,返回值被复制到调用上下文之后。(此顺序点仅在 C++ 标准中指定;它仅在 C 中隐式存在。)
- 在初始化程序结束时;例如,在声明
int a = 5;中,在对5求值之后。 - 在每个声明序列中的每个声明符之间;例如,在
int x = a++, y = a++中,在对a++的两次求值之间。(这不是逗号运算符的例子。) - 在与输入/输出格式说明符关联的每个转换之后。例如,在表达式
printf("foo %n %d", &a, 42)中,在%n被求值之后,并在打印42之前,有一个顺序点。
- ↑ “函数式编程研究主题” D. Turner 编著,Addison-Wesley,1990 年,第 17-42 页。检索自:Hughes, John, 为什么函数式编程很重要 (PDF)
- ↑ Collberg, CSc 520 编程语言原理,亚利桑那大学计算机科学系
- ↑ C99 规范的第 6.5 章 #2 条:“在先前和下一个顺序点之间,对象在其存储值最多被表达式求值修改一次。此外,仅访问先前值以确定要存储的值。”
- ↑ C99 规范的附录 C 列出了可以假设顺序点的情况。
- 问题 3.8 的 comp.lang.c 常见问题解答