条件、循环与跳转
本节目标
查询 C23 条件、分支、switch、循环以及 break、continue、goto 的核心规则。
本章用于查询 C23 中选择下一条语句的规则。阅读控制流时,先找控制表达式,再确认每个跳转的精确目标,最后检查所有路径是否都会推进状态或终止。
条件表达式与真假规则
查询规则: if、while、do while 和 for 的条件必须具有标量类型;它与 0 比较不相等时为真,与 0 比较相等时为假。标量类型包括算术类型和指针类型,因此非零数值与非空指针都为真。
| 条件 | 结果 | 推荐写法 |
|---|---|---|
整数为 0 | 假 | count != 0 可突出判断意图 |
整数非 0 | 真 | 比较范围时直接写 count > 0 |
| 空指针 | 假 | pointer != NULL 最清楚 |
| 非空指针 | 真 | 先检查,再解引用 |
逻辑运算符 !、&& 和 || 的结果是 int 类型的 0 或 1。&& 与 || 会短路,但不要把普通算术或函数调用误当成具有同样的求值顺序保证;相关规则可回查运算符、表达式、求值与类型转换。
if 与 else
查询规则: if 根据条件选择是否执行一条语句,else 提供条件为假时的另一条语句。即使分支当前只有一条语句,也建议使用花括号,避免后续增补语句时改变控制范围。
if (temperature < 0) {
state = FROZEN;
} else if (temperature == 0) {
state = AT_BOUNDARY;
} else {
state = ABOVE_BOUNDARY;
}
else 与最近一个尚未匹配 else 的 if 配对,这就是 dangling else(悬空 else)规则。缩进不参与语法;嵌套分支必须依靠花括号明确结构。
if (ready) {
if (has_data) {
process_data();
} else {
wait_for_data();
}
}
switch、case 与 default
查询规则: switch 的控制表达式必须具有整数类型。switch 控制表达式会先进行整数提升;每个 case 的整数常量表达式都会转换为提升后的控制表达式类型,转换后值相同的 case 标签不允许重复,同一个 switch 中最多只能有一个 default。匹配时控制流直接跳到对应的 case 标签;若没有 case 匹配且不存在 default,switch 体中的任何部分都不会执行。
switch (status) {
case 0:
handle_idle();
break;
case 1:
handle_ready();
break;
default:
handle_unknown();
break;
}
匹配某个 case 后,执行会继续穿过后续标签,直到遇到跳转或 switch 结束。每个互斥分支通常显式写 break;C23 应使用 [[fallthrough]]; 显式标记有意贯穿。这个属性作用于一条空语句,其后遇到的下一个块项必须是与同一最内层 switch 关联的 case 或 default 标签:
switch (phase) {
case PHASE_PREPARE:
prepare();
[[fallthrough]];
case PHASE_RUN:
run();
break;
default:
recover();
break;
}
若必须兼容尚不识别 C23 属性的编译器,可以按该编译器约定保留明确的贯穿注释,但注释不是 C23 的标准贯穿属性。default 可以放在任意标签位置,但统一放在末尾通常更易读。
for 循环
for (初始化; 条件; 迭代表达式) 把循环的三个阶段集中在一行:初始化只执行一次;每轮先检查条件;循环体正常结束后执行迭代表达式,再检查下一轮条件。
for (int index = 0; index < limit; ++index) {
visit(index);
}
| 省略位置 | 含义 |
|---|---|
| 初始化 | 进入循环前不执行初始化动作 |
| 条件 | 按永真条件处理;必须靠 break、return 等方式终止 |
| 迭代表达式 | 每轮末尾不自动执行更新动作 |
初始化部分声明的标识符,其作用域覆盖循环的其余控制部分和循环体。把计数器、上界和更新方向放在一起,能让“是否有界”更容易检查。for 循环中的 continue 会转到本轮的迭代表达式,然后才重新检查条件,并不是直接跳过更新进入下一轮循环体。
while 与 do while
while 是前测循环:第一次执行循环体前就检查条件,循环体可能一次也不执行。do while 是后测循环:先执行一次循环体,再检查条件,因此循环体至少执行一次。
while (remaining > 0) {
consume_one();
--remaining;
}
do {
response = read_response();
} while (response == RETRY);
当循环次数由进入前已知的计数边界决定时,优先考虑 for;当终止依赖循环体内不断更新的状态时,while 往往更直接;只有业务确实要求“至少执行一次”时再选 do while。无论选择哪一种,都要能指出哪条路径推进了终止条件。
break、continue 与 goto
查询规则: 先确定跳转属于哪一层结构,再确认跳转后的下一条语句。
| 语句 | 精确目标 | 常见误解 |
|---|---|---|
break | 结束最内层包围它的循环或 switch | 不会一次退出多层嵌套 |
continue | 开始最内层包围它的循环的下一轮 | 在 for 中仍先执行迭代表达式 |
goto label; | 跳到同一函数内名为 label 的标签 | 不能跨函数跳转 |
break 只退出最内层的循环或 switch;若要退出多层结构,可以把逻辑提取到函数并 return,或使用显式状态让外层也结束。continue 在 while 中转到条件检查,在 do while 中转到循环尾部的条件检查,在 for 中转到迭代表达式。
goto 的目标必须是同一函数内的标签。它只适合经过审查的局部跳转,例如在同一函数末尾汇合错误清理路径;不要用它模拟普通循环或跨越不清楚的初始化流程。更窄且必须遵守的边界是:goto 不能从作用域外跳入含有可变修改类型标识符的作用域。除此之外,是否能跨过某个声明要依据具体约束和对象使用情况判断,不能概括成“goto 绝不能跨声明或进入任何作用域”。
下面的代表性程序以 -2 到 8 为循环边界:负数用 continue 跳过,零由 if 处理,正数 1 到 5 由 switch 分类,遇到 6 后用 break 结束最内层循环。程序刻意不使用数组或 goto。
#include <stdio.h>
int main(void) {
int processed = 0;
for (int value = -2; value <= 8; ++value) {
if (value < 0) {
continue;
}
if (value == 0) {
puts("zero");
++processed;
continue;
}
if (value == 6) {
printf("stop=%d\n", value);
break;
}
switch (value % 3) {
case 0:
printf("%d: multiple-of-3\n", value);
break;
case 1:
printf("%d: remainder-1\n", value);
break;
default:
printf("%d: remainder-2\n", value);
break;
}
++processed;
}
printf("processed=%d\n", processed);
return 0;
}
输出:
zero
1: remainder-1
2: remainder-2
3: multiple-of-3
4: remainder-1
5: remainder-2
stop=6
processed=6
嵌套控制流与常见陷阱
| 陷阱 | 实际后果 | 修复 |
|---|---|---|
只靠缩进判断 else 归属 | else 仍与最近的未匹配 if 配对 | 为每层分支加花括号 |
case 末尾遗漏 break | 执行贯穿后续标签 | 互斥分支显式 break;有意贯穿使用 [[fallthrough]]; |
以为内层 break 会退出外层循环 | 外层循环继续执行 | 提取函数并 return,或显式传递终止状态 |
continue 跳过了手写的状态更新 | while 条件可能永远不变 | 把更新放在所有路径都能执行的位置 |
省略 for 条件却没有可达的终止跳转 | 形成无限循环 | 明确并审查 break 或 return 条件 |
| 在多层嵌套中复用相似的条件 | 边界和终止路径难以确认 | 提取有名字的条件或拆分函数 |
goto 从外部进入可变修改类型标识符的作用域 | 违反语言约束 | 调整作用域或改为结构化控制流 |
排查复杂控制流时,可以为每条路径标注“进入条件 → 状态变化 → 跳转目标 → 终止条件”。只要其中一条可重复路径没有状态推进,就应把它当作潜在无限循环继续检查。