运算符、表达式与控制流:C 程序如何做决定
程序不仅要保存数据,还要计算和做决定。 C 用表达式产生值,用语句产生动作,用控制流选择下一步执行路径。 初学时容易把这些内容当成语法表。 但在 C 里,表达式求值顺序、整数运算、短路逻辑和循环边界都会直接影响内存安全和并发安全。
表达式产生值
表达式是能被求值的代码片段。
1 + 2
x * 10
count > 0
func(3)
表达式可能只是计算,也可能有副作用。 赋值、函数调用、自增自减都会产生副作用。
x = x + 1;
printf("%d\n", x);
副作用意味着程序状态发生变化。 理解副作用,是理解求值顺序的前提。
语句执行动作
语句通常以分号结束。
int x = 0;
x = x + 1;
return x;
控制语句不一定只是一行。
if (x > 0) {
x = x - 1;
}
建议始终写花括号。 这能避免后续添加代码时制造控制流错误。
算术运算符
常见算术运算:
| 运算符 | 含义 |
|---|---|
+ |
加 |
- |
减 |
* |
乘 |
/ |
除 |
% |
取余 |
整数除法会截断。
int x = 5 / 2; // 2
除零是严重错误。 整数除零是未定义行为。 浮点除零有自己的浮点规则,但也不应随意依赖。
赋值不是数学等号
= 表示把右侧值写入左侧对象。
int x = 1;
x = x + 2;
第二行的含义是:
- 读取当前
x。 - 加
2。 - 把结果写回
x。
它不是数学里的“x 等于 x 加 2”。
复合赋值:
x += 2;
x *= 3;
表达更短,但不要牺牲可读性。
比较运算符
比较产生真假结果。
a == b
a != b
a < b
a <= b
a > b
a >= b
注意 == 和 = 不同。
if (x = 1) {
/* 高风险:这是赋值,不是比较 */
}
编译器 warning 通常能发现这类问题。 所以 warning 要从第一天打开。
逻辑运算符和短路
&& // 逻辑与
|| // 逻辑或
! // 逻辑非
&& 和 || 有短路行为。
if (p != NULL && p->value > 0) {
use(p);
}
如果 p == NULL,右侧不会求值。
这让空指针检查可以保护后续解引用。
顺序写反就会先解引用再检查,风险完全不同。
自增自减要简单使用
++i;
i++;
--i;
i--;
单独作为语句时清晰。 混进复杂表达式时容易出错。
a[i++] = i; // 不推荐
不要在同一表达式里多次修改同一个对象。 这类代码会触发求值顺序和未定义行为风险。
位运算符
位运算直接操作二进制位。
| 运算符 | 含义 |
|---|---|
& |
按位与 |
| ` | ` |
^ |
按位异或 |
~ |
按位取反 |
<< |
左移 |
>> |
右移 |
常用于 flags:
enum {
READ = 1 << 0,
WRITE = 1 << 1,
EXEC = 1 << 2
};
int mask = READ | WRITE;
位运算要优先使用无符号类型。 有符号移位和溢出有更多风险。
if 选择路径
if (score >= 60) {
puts("pass");
} else {
puts("fail");
}
if 的条件表达式为真时执行第一个块,否则执行 else。
多分支:
if (score >= 90) {
grade = 'A';
} else if (score >= 80) {
grade = 'B';
} else {
grade = 'C';
}
边界要清晰。
>= 和 > 的差异会直接影响分类结果。
switch 适合离散分支
switch (op) {
case '+':
result = a + b;
break;
case '-':
result = a - b;
break;
default:
return -1;
}
break 很重要。
没有 break 会继续落入下一个 case。
如果故意 fallthrough,要写清楚注释或属性。
while 循环
while 先判断条件,再执行循环体。
while (i < n) {
sum += values[i];
++i;
}
循环必须保证条件最终可能变假。 否则会死循环。 生产代码中的循环还要考虑超时、取消和资源释放。
do while 循环
do while 至少执行一次。
do {
read_next();
} while (has_more());
它适合“先做一次,再判断是否继续”的场景。
不要为了少写一行把普通循环改成 do while。
for 循环
for 适合计数循环。
for (size_t i = 0; i < n; ++i) {
sum += values[i];
}
三个部分分别是初始化、条件和迭代表达式。
数组循环中要格外注意边界。
i <= n 和 i < n 差一个越界。
break 和 continue
break 退出当前循环。
continue 跳过本轮剩余部分,进入下一轮。
for (size_t i = 0; i < n; ++i) {
if (values[i] < 0) {
continue;
}
if (values[i] == target) {
break;
}
}
它们能简化控制流。 但过多跳转会让资源释放路径变难读。
goto 的合理位置
入门阶段不要用 goto 写普通业务流程。
但在 C 的资源清理里,goto cleanup 很常见。
int rc = -1;
FILE* f = fopen(path, "rb");
if (f == NULL) goto cleanup;
rc = 0;
cleanup:
if (f != NULL) fclose(f);
return rc;
它的价值是集中释放资源。 不是替代结构化控制流。
求值顺序要保守
C 的某些子表达式求值顺序不是你想象的从左到右。 不要写依赖复杂求值顺序的代码。
int x = 1;
int y = x++ + x++; // 不要这样写
把复杂表达式拆成多行。 编译器优化会处理性能。 人类审计需要清晰。
工程风险
基础控制流常见风险:
- 循环边界越界。
switch漏写break。- 除零。
- signed/unsigned 混合比较。
- 复杂表达式多次修改同一对象。
- 错把
=写成==。 - 资源释放路径被
return或break绕过。 - 死循环缺少超时和降级。
这些风险会从小程序一路带到系统工程。
小结
运算符让程序计算。 表达式产生值和副作用。 控制流决定代码路径。 C 的难点不在于记住符号,而在于知道每个符号背后的求值、转换、边界和资源影响。 把表达式写简单,把控制流写清楚,是避免底层错误的第一层防线。