跳到主要内容

条件、循环与跳转

本节目标

查询 C23 条件、分支、switch、循环以及 break、continue、goto 的核心规则。

本章用于查询 C23 中选择下一条语句的规则。阅读控制流时,先找控制表达式,再确认每个跳转的精确目标,最后检查所有路径是否都会推进状态或终止。

条件表达式与真假规则

查询规则: ifwhiledo whilefor 的条件必须具有标量类型;它与 0 比较不相等时为真,与 0 比较相等时为假。标量类型包括算术类型和指针类型,因此非零数值与非空指针都为真。

条件结果推荐写法
整数为 0count != 0 可突出判断意图
整数非 0比较范围时直接写 count > 0
空指针pointer != NULL 最清楚
非空指针先检查,再解引用

逻辑运算符 !&&|| 的结果是 int 类型的 01&&|| 会短路,但不要把普通算术或函数调用误当成具有同样的求值顺序保证;相关规则可回查运算符、表达式、求值与类型转换

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);
}
省略位置含义
初始化进入循环前不执行初始化动作
条件按永真条件处理;必须靠 breakreturn 等方式终止
迭代表达式每轮末尾不自动执行更新动作

初始化部分声明的标识符,其作用域覆盖循环的其余控制部分和循环体。把计数器、上界和更新方向放在一起,能让“是否有界”更容易检查。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,或使用显式状态让外层也结束。continuewhile 中转到条件检查,在 do while 中转到循环尾部的条件检查,在 for 中转到迭代表达式。

goto 的目标必须是同一函数内的标签。它只适合经过审查的局部跳转,例如在同一函数末尾汇合错误清理路径;不要用它模拟普通循环或跨越不清楚的初始化流程。更窄且必须遵守的边界是:goto 不能从作用域外跳入含有可变修改类型标识符的作用域。除此之外,是否能跨过某个声明要依据具体约束和对象使用情况判断,不能概括成“goto 绝不能跨声明或进入任何作用域”。

下面的代表性程序以 -28 为循环边界:负数用 continue 跳过,零由 if 处理,正数 15switch 分类,遇到 6 后用 break 结束最内层循环。程序刻意不使用数组或 goto

control_flow_report.c
#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 条件却没有可达的终止跳转形成无限循环明确并审查 breakreturn 条件
在多层嵌套中复用相似的条件边界和终止路径难以确认提取有名字的条件或拆分函数
goto 从外部进入可变修改类型标识符的作用域违反语言约束调整作用域或改为结构化控制流

排查复杂控制流时,可以为每条路径标注“进入条件 → 状态变化 → 跳转目标 → 终止条件”。只要其中一条可重复路径没有状态推进,就应把它当作潜在无限循环继续检查。