3.10.1 求值和副作用
表达式求值可以拆成两部分:一部分是“算出结果值”,另一部分是“改变程序状态”。这两个部分经常同时出现,但需要分开分析。
1. 值计算
值计算 (Value computation) 指的是为了得到表达式结果而进行的计算过程。例如 3 + 2 的结果是 5,这里仅发生值计算,不会改变外部状态。
2. 副作用
副作用 (Side effect) 是求值过程中对程序可观察状态的改变,典型包括修改对象内容、执行输入输出、调用可能修改全局状态的函数。下面这段代码里,++ 会修改对象,printf 会产生 I/O,这两类都属于副作用。
c
#include <stdio.h>
int main(void) {
int x = 0;
++x;
printf("%d\n", x);
return 0;
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
输出:
bash
1
3. 与完整表达式的关系
C 标准保证:前一个完整表达式的副作用先于后一个完整表达式发生。因此,把读写拆到不同的语句中,就能由语句的先后确定它们的顺序。
4. 把副作用和计算分层
当一个表达式既要更新对象又要参与复杂计算时,建议先把更新动作拆出来,再写纯计算表达式。这样不仅更容易确认顺序关系,也更方便调试器逐步观察状态变化。
c
int i = 0;
/* 未定义行为:对 i 的修改与 (i + 3) 中对 i 的读取之间没有先后保证 */
/* int x = (i += 2) * (i + 3); */
/* 推荐:先更新,再计算 */
i += 2;
int x = i * (i + 3);1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
5. 函数调用里的副作用
即使返回值被丢弃,函数调用本身也可能产生副作用:只要函数修改了对象或执行了输入输出,这次调用就产生了副作用。
c
tick(); /* 可能更新时间戳、计数器或日志 */1
因此,判断一个表达式是否有副作用,不仅要看表达式本身,还要看被调用函数做了什么。
6. 读写同一对象要先建立顺序关系
无先后保证的读写导致未定义行为
对同一标量对象的一次修改,如果与对它的另一次修改或读取之间没有标准保证的先后关系,行为未定义。
一个典型例子是在同一表达式中对同一标量对象执行两次修改:
c
int i = 1;
int x = i++ + i++; /* 未定义行为 */1
2
2
这里,i++ 既是值计算(取 i 当前值),又是副作用(将 i 加 1)。两个 i++ 的副作用之间没有序列点,标准不保证哪个先完成——编译器可以选择任意顺序甚至在寄存器中同时处理。这是未定义行为:没有保证的不只是 x 和 i 的值,而是整个程序的行为,因此不能把某一次运行结果当成规则。修复的方法同样是拆分语句。
7. 习题
#10315
⚡3⏳2
判断下列表达式是否一定产生副作用,并说明副作用是什么:
1 + 2i += 1i++