给定一个表达式 expression,evaluation 的过程包括:
- 求值(value computation)
- 确定副作用(Initiation of Side Effect)
求值的过程需要确定返回值 Value 和返回值类型 Value_Type,记为 <V, V_T> ;而 Side Effect 的判断标准则是程序执行环境状态的改变。
了解 evaluation 的动机是显然的。一方面,可以从值的角度帮助我们理解语法,另一方面,写过编译器中端优化的应该清楚副作用分析的重要性,这可以帮助我们理解优化。
表达式 evaluation 里一个重要的议题是求值和副作用执行顺序的问题。比如被人诟病已久的 thq C 语言中那堆毫无意义的 ++i++
之类的题目应该得到怎样的结果,a = b++
之类的式子又应该是先确定副作用还是先求值,等等。通过一个新定义 Sequenced Before ,可以给出这些问题的答案。
Sequenced Before
Sequenced Before 是一种 非对称 、 可传递 的 成对 Evaluation 之间的关系。A Sequenced Before B 的含义是 A 的 evaluation 执行在 B 的 evaluation 之前。也即:与 A 相关的求值和副作用确定 全部 在与 B 相关的求值和副作用确定之前。
C 语言定义了一系列的 sequenced point 来规范 sequenced before 这种行为,A sequenced point B 保证了 A sequenced before B。
实际上,C 语言标准中的 sequenced point 就是 C 语言程序分析中【程序点】的严格定义。
下面给出能标记出 Sequenced Point 的位置:
- 函数名和实参 evaluation 和实际函数调用执行之间
- 在 &&、||、逗号运算符分隔的前后两个表达式之间
- ? : 三目运算表达式中,? 之前表达式以及之后执行的表达式之间
- 两个 full expression 之间
- full exp:表达式语句(分号)、if、while、do、for、return 等控制条件表达式等
- 需要注意的是一个表达式语句(分号)的前后应该都可以标记出一个 sequenced point
- 库函数调用之前
- printf / scanf、fprintf / fscanf、sprint / sscanf 等一系列输入输出中按转换说明符执行完转换动作之后
- bsearch、qsort 等比较函数调用之前和之后以及调用比较函数和对象移动之间
Sequenced point 前后的表达式执行顺序是确定的,前一定先于后,举例而言:
1 | int i = 1; |
而对于夹在两个 sequenced points 之间的表达式的表达式内执行顺序,除了显式的语法规定,表达式内子表达式的 evaluation 之间顺序关系没有约定,纯看编译器的喜好。举例而言:
1 | a = b + c; |
这一点也就反驳了 ++i 那种破题存在的必要性:
为了处理这种不确定现象的结果,C 语言标准规定,对于一个标量对象(Scalar Object),如果:
- 产生两次副作用且两次副作用没有先后顺序要求,或
- 产生的副作用和同样标量对象取值之间没有先后顺序要求
则其结果是 undefined behavior。
这给了我们一个新的启示,就是要慎用自增 / 自减运算符,因为我们对它的 90% 的使用场景都涉及了第二条规定(试想我们是否经常写出类似 a[k++] = i
或者 a = i++
这样的语句?)。国军标禁止在运算表达式中或函数调用参数中使用 ++ 或 – 操作符。
在抽象机场景下使用自增自减运算符不会产生什么问题,因为不存在优化的需求,运算顺序是比较好确定的。但到了编译场景下(存在优化)就需要慎重考虑了。