5.5 跳转语句
跳转语句会改变通常的控制流。break 和 continue 作用于循环或 switch,return 结束当前函数,goto 跳到同一函数中的标号。
1. 本节内容
先根据希望离开的语法范围选择对应语句。只有确实需要跳到标号时才使用 goto,并检查跳转后的清理和作用域约束。
2. 习题
先不编译,写出下面程序的输出,再编译运行核对。
#include <stdio.h>
int main(void) {
int count = 0;
int i;
for (i = 0; i < 10; ++i) {
if (i > 7) {
break;
}
if (i % 4 == 0) {
continue;
}
++count;
printf("%d ", i);
}
printf("\n%d %d\n", count, i);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 写出全部输出。
count和i最后各是多少? - 把
break换成continue,打印的那行数字有变化吗?第二行的i变成多少?说明break与continue分别让循环在哪里结束。 - 调换两处判断的顺序,先判断
i % 4 == 0再判断i > 7,第二行的i变成多少?从i = 8那一轮说明原因。 i为什么声明在for外面?改成for (int i = 0; ...)后,循环之后还能打印它吗?编译结果是什么?
下面的程序想找出乘积为 6 的第一对整数,找到就停止搜索。它现在只退出了内层循环。
#include <stdio.h>
int main(void) {
for (int i = 0; i < 5; ++i) {
for (int j = 0; j < 5; ++j) {
if (i * j == 6) {
printf("found %d %d\n", i, j);
break;
}
}
printf("row %d done\n", i);
}
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
- 写出完整输出,说明为什么会出现两组
found。 - 用标志对象的办法改造:外层循环条件加上
&& !found,内层找到后置位标志。改造后只出现一组found 2 3,row最多打印到第 2 行。说明最后这一行row 2 done为什么仍然出现。 - 用把搜索抽成函数、找到后直接
return的办法改造。函数写static int find_pair(int *pi, int *pj),找到返回 1,找不到返回 0。 - 在内层
break之后用goto跳到外层循环之后的标签,使row 2 done不再打印。比较标志版与goto版:两者打印的row行数相差多少?循环条件分别需要怎么写? - 把内层找到后的
break换成i = 5; j = 5;(直接改循环变量让循环结束)。本题上界为 5,程序确实停下来了。再把两个上界都改成 100,在找到后打印一对下标、计数到 5 次就返回,观察下标在 5 与 6 之间来回跳的现象。说明为什么第一次找到的1 6与之后反复出现的6 1不是同一对,以及这种写法为什么在评审中不被接受。
goto 在工程里最常见的用途是统一错误清理。下面的函数有缺陷:有的分支提前返回,跳过了释放动作。
#include <stdio.h>
static int cleanups;
static void release(void) {
++cleanups;
}
static int run(int stage) {
if (stage < 0) {
return -1;
}
if (stage == 1) {
return -1;
}
if (stage == 2) {
release();
return -1;
}
release();
return 0;
}
int main(void) {
for (int s = -1; s <= 3; ++s) {
int rc = run(s);
printf("stage %d rc=%d cleanups=%d\n", s, rc, cleanups);
}
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- 写出输出。
stage为 -1 和 1 的那两次调用,各自执行了release()没有?打印出来的cleanups为什么在stage 1那一行仍是 1?缺陷在哪几行? - 改成单出口:函数末尾只留一个
done:标签,标签之后统一调用release(),前面所有失败分支都写goto done;,返回值统一由rc对象带出。改写后五次调用的cleanups依次是多少?rc是否仍与原来一致? - 为什么返回值必须在跳转之前写好,或者用一个
rc对象贯穿?如果在每个失败分支里直接return -1;,会跳过什么? - 把
release换成真实的free与fclose两个调用,写出两资源版本的函数骨架。说明清理顺序为什么与申请顺序相反,以及某个指针为NULL时free为什么仍然安全(fclose则不同)。 - 有人主张把清理写进一个函数、在每个失败分支各调用一次,而不用
goto。以本题的三个失败分支为例,列出这种写法的具体代价,并说明正文为什么说goto在这里“最常见、也最合理”。
本页的三个跳转语句分别解决不同层次的问题。下面三问把它们放进实际程序,每问单独验收。
(1) [4.3] 写一个统计工具:从标准输入读整数。读到 0 结束,输出 个数 总和 并返回 0;负数跳过;读到非整数或 EOF 时输出 bad input after N values,返回 1,N 是此前已计入的个数。
样例输入:
3
-4
5
0
72
3
4
5
样例输出:
2 8要求:循环写成 for (;;),0 用 break,负数用 continue,读失败用 goto 跳到函数末尾的错误处理,不要在分支里重复 printf 和 return 1。验收另两组:输入 3 后跟 abc 得 bad input after 1 values,退出状态 1;空输入得 bad input after 0 values,退出状态 1。最后去掉 goto,只用标志对象表达读失败,重写一遍,记录需要新增哪些对象、条件怎么改,并说明原版在“读到 0 之后不再读取”这一点上为什么更直接。
(2) [4.3] 把 (1) 失败后的策略改成“跳过该行继续读”,用 getchar 丢弃当前行剩余的字符直到换行,然后 continue。说明为什么只写 continue 会陷入死循环:scanf 读到错误字符后有没有把它取走?
(3) [6.4] 下面的状态机程序想依次走完三个阶段,实际行为与作者的预期不一致:
#include <stdio.h>
int main(void) {
int state = 0;
int steps = 0;
for (;;) {
switch (state) {
case 0:
printf("state 0 -> 1\n");
state = 1;
break;
case 1:
printf("state 1 -> 2\n");
state = 2;
continue;
case 2:
printf("state 2 -> done\n");
goto done;
default:
printf("unknown\n");
goto done;
}
printf("after switch, state=%d\n", state);
if (++steps > 10) {
goto done;
}
}
done:
printf("finished after %d steps\n", steps);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
写出输出,逐条说明程序里的 break、continue、goto 各自把控制送到哪里,并解释 steps 为什么只有 1。把 case 1 的 continue 改成 break,steps 变成多少?再把 case 0 的 break 也改成 continue:先预测不加任何保护时程序会怎样,再在 for 循环体开头加一个自增的轮次计数与上限,确认它为什么会一直跑下去。最后从“continue 跳过的是本轮剩余语句”这一条解释原因。