求值
求值 (Evaluation) 讨论的是:一个表达式在运行时如何计算出结果值,以及如何产生副作用。
本节将围绕三个关键词建立正确的直觉:
- 值计算 (Value computation):得到一个结果值;
- 副作用 (Side effect):修改对象、执行 I/O、调用函数等;
- 顺序关系:哪些求值之间有确定先后,哪些没有。
1. 未求值上下文(不求值表达式)
有些语境中,表达式不会被求值,因此其中的副作用不会发生。最典型的是 sizeof:
#include <stdio.h>
int main(void) {
int i = 0;
(void)sizeof(i++); /* i++ 不会被执行 */
printf("%d\n", i);
return 0;
}2
3
4
5
6
7
8
可能的输出(示例):
<输出与输入或平台相关,请以实际运行为准>
本章只需要记住:不要依赖“未求值上下文”中表达式的副作用。
2. 先后关系与未定义行为
很多初学者写出 UB 的根源是:在同一个表达式里,对同一个标量对象既读又改,并且两次操作之间没有标准保证的先后关系。
例如:
int i = 0;
int x = i++ + ++i; /* UB */2
运行结果:该代码块主要用于语法或结构说明,单独运行通常无终端输出。
要修复它,最直接的方法是:拆开写。
int i = 0;
int a = i++;
int b = ++i;
int x = a + b;2
3
4
运行结果:该代码块主要用于语法或结构说明,单独运行通常无终端输出。
3. 与子章节的对应关系
如果你在判断“某段表达式有没有副作用”,重点看 4.3.1 求值和副作用。如果你在判断“多个子表达式谁先谁后”,重点看 4.3.2 求值顺序。把这两部分分开阅读,通常比在一个大表达式里一次性判断更稳妥。
4. 一个实用准则
当你需要停下来思考“这条表达式到底先做哪一步”时,通常就已经到了应当拆语句的时机。把更新动作和计算动作拆开,不只是为了规避 UB,也是在给后续维护者降低理解成本。
5. 求值问题的排查顺序
排查表达式问题时,可以先看“是否存在副作用”,再看“副作用之间是否有标准保证的先后关系”,最后看“是否跨越完整表达式边界”。这三个问题按顺序回答,通常就能快速判断代码是安全、未指定顺序,还是已经落入未定义行为。
6. 把顺序关系写成结构
与其依赖读者记住复杂运算符规则,不如把先后关系写成语句结构。例如先完成状态更新,再进入判定,再执行输出。结构化后的代码在重构和调试中更稳定,也更便于和后续的并发、错误处理章节衔接。
7. 习题
判断下面每段代码是否可能产生未定义行为 (Undefined Behavior, UB);如果是,请写出一个等价且无 UB 的版本(要求:用拆分语句的方式修复)。
int i = 0;
int x = i++ + i;2
运行结果:该代码块主要用于语法或结构说明,单独运行通常无终端输出。
int i = 0;
int x = ++i + 1;2
运行结果:该代码块主要用于语法或结构说明,单独运行通常无终端输出。