5.2 do while
do while 是“先执行、后判断”的循环语句。无论条件是否成立,循环体都会先执行一次,因此它适合“至少执行一次”的流程。
1. 基本语法
do {
statement;
} while (condition);2
3
注意结尾分号属于语法的一部分,不能省略。
2. 示例
#include <stdio.h>
int main(void) {
int n = 3;
do {
printf("n = %d\n", n);
n--;
} while (n > 0);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
输出:
n = 3 n = 2 n = 1
即使把初值改成 n = 0,循环体仍会先执行一次,然后才根据条件决定是否继续。
3. 适用场合
当流程需要先执行一次操作,再根据结果决定是否重复时,例如菜单交互、失败重试和输入校验,宜使用 do while。如果循环体可能一次也不需要执行,优先考虑 while,让读者一眼看出“可能不进循环”。
4. 与 break / continue 的配合
在 do while 中,break 会直接跳出循环;continue 会立刻跳到条件判断处,再决定是否进入下一轮。理解这点可以避免把 continue 误解为“回到循环体开头”。
5. 一个典型输入循环
#include <stdio.h>
int main(void) {
int x;
do {
if (scanf("%d", &x) != 1) {
break;
}
printf("read: %d\n", x);
} while (x != 0);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
例如依次输入 7 和 0,交互结果为:
7 read: 7 0 read: 0
这个模式表达的是“至少尝试读取一次,读到 0 结束;读取失败也结束”,很适合简单交互流程。
6. 与 while 的改写关系
do while 往往可以改写为“先执行一次循环体,再进入 while”的形式。
/* do while 版本 */
do {
step();
} while (cond());
/* 等价思路 */
step();
while (cond()) {
step();
}2
3
4
5
6
7
8
9
10
当循环体较长或包含多条退出路径时,直接保留 do while 通常更易读。
7. do { ... } while (0)
宏定义中常见 do { ... } while (0) 的写法。它的条件恒为 0,循环体只执行一次,作用是把多条语句包装成一条语句,使宏调用后加上分号,在 if/else 中也能像普通语句一样使用。宏的定义方法见 17.2 define。
8. 习题
8.1 巩固
先不编译,写出下面程序的输出,再编译运行核对。
#include <stdio.h>
int main(void) {
int n = 5;
do {
printf("%d ", n);
n -= 2;
} while (n > 0);
printf("\n");
int k = 10;
do {
printf("%d ", k);
--k;
} while (k > 10);
printf("\n%d\n", k);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
回答下面的问题。
- 两段循环分别打印什么?最后一行输出的
k是多少? - 第二段的循环体执行了几次?条件第一次求值时
k是多少?据此说明“先执行、后判断”在这里意味着什么。 - 把第二段的条件改成
while (k > 8),重新运行,输出有什么变化? - 删掉第一段
} while (n > 0);末尾的分号再编译,记录诊断信息,并说明这个分号属于哪条语句。
下面的程序想累加 1 到 10 之间的奇数,实际却停不下来。
#include <stdio.h>
int main(void) {
int i = 1;
int sum = 0;
do {
if (i % 2 == 0) {
continue;
}
sum += i;
++i;
} while (i <= 10);
printf("%d\n", sum);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 说明为什么会一直循环:
continue执行之后,i、sum和随后的条件判断分别处于什么状态。 - 有人把
++i;整体挪到if之前,循环能停了,输出却是 35 而不是 25。先预测再运行,列出实际被累加的各个数,指出比原来多算了哪个数、又漏掉了哪个数。 - 写出两种都能得到 25 的修改:一种保留
continue但补上推进,一种去掉continue改用条件判断。 - 用
gcc -std=c17 -Wall -Wextra编译原程序,有没有警告?据此说明单靠编译选项能不能发现这类问题。
do { ... } while (0) 的作用要在宏被放进 if 时才看得出来。下面三个宏展开后都是两条 printf。
#define A(x) printf("%d\n", (x)); printf("%d\n", (x) * 2)
#define B(x) { printf("%d\n", (x)); printf("%d\n", (x) * 2); }
#define C(x) \
do { \
printf("%d\n", (x)); \
printf("%d\n", (x) * 2); \
} while (0)2
3
4
5
6
7
- 先把三处调用都放进带复合语句的
if (a > 0) { A(a); } else { printf("negative\n"); },令a = -1,用gcc -std=c17 -Wall -Wextra编译运行,三条输出是否相同? - 去掉三处
if的花括号,改成if (a > 0) A(a); else ...,仍令a = -1。哪个版本编不过?抄下诊断,并说明展开后的第二条printf挂到了哪里,为什么else找不到自己的if。 - 再去掉
else分支,只留if (a > 0) A(a);,令a = -1运行。A版本打印了什么?编译警告原文是什么?B版本又有什么不同? C在去掉花括号之后仍然正确。说明宏定义末尾为什么不写分号,而调用处为什么必须写。- 在
C的定义末尾故意加上一个分号,再放回if/else中编译,结果如何?据此写出这类宏在定义和调用两方面的规则。
8.2 编程
写一个程序,读入一个非负整数 n(不超过 9 位),用 do while 逐位拆开,输出三个整数:位数、各位数字之和、把各位数字倒序排列得到的数。
样例输入与输出:
12044 7 402101 0 010004 1 1要求:
- 拆位循环用
do while,每轮取n % 10,再执行n /= 10,循环条件是n != 0。 n为 0 时输出1 0 0:0 也算一位,这正是do while与while在这里的分界。- 倒序结果用
int保存,输入不超过 9 位时不会溢出。 - 完成后把循环改成
while (n != 0),重新运行0这一组输入,记录差别,并说明哪种写法与题意一致。
用 do while 包住一个 switch,写一个温度换算菜单。每轮先输出一行 请选择:,再读一个整数作为选项。
- 选项 1:再读一个
double,按摄氏度转华氏度,输出华氏度:和保留一位小数的结果,公式为 。 - 选项 2:再读一个
double,按华氏度转摄氏度,输出摄氏度:和保留一位小数的结果,公式为 。 - 选项 0:输出
再见,结束程序。 - 其他整数:输出
无效选择,回到菜单。 - 读选项或读数失败时,输出
输入结束,返回 1。 - 循环条件写成
while (choice != 0)。
验收一:依次输入 1、100、2、212、9、0(每行一个),输出为:
请选择:
华氏度:212.0
请选择:
摄氏度:100.0
请选择:
无效选择
请选择:
再见2
3
4
5
6
7
8
验收二:输入 1 之后再输入 abc,输出 请选择: 和 输入结束,退出状态为 1。
验收三:不给任何输入直接运行,同样先输出 请选择:,再输出 输入结束,退出状态为 1。
提示:case 标签后面只能跟语句,声明不是语句。GCC 在默认选项下容忍 case 1: double c; 这种写法,但加上 -pedantic 会报 a label can only be part of a statement。把整个分支放进花括号既解决这个问题,也让各个 case 里的对象各有作用域。
8.3 综合与挑战
本节的输入循环先读一次、再判断,改写成 while 时容易丢掉第一次读到的值或最后一行输出。原始版本:
int x;
do {
if (scanf("%d", &x) != 1) {
break;
}
printf("read: %d\n", x);
} while (x != 0);2
3
4
5
6
7
- 输入
7和0(各占一行),程序输出什么?不给任何输入时呢? - 改成
while (scanf("%d", &x) == 1 && x != 0) { printf("read: %d\n", x); },同样的输入少输出哪一行?从判断与输出谁先谁后解释原因。 - 改成
while (x != 0 && scanf("%d", &x) == 1) { ... },第一次判断时x处于什么状态?由此说明这个改写为什么不可靠。 - 写出与
do while版本行为完全一致的while版本。允许读入语句出现两次,并指出重复的是哪一步、为什么必须重复。 - 如果循环体里有多处
break,继续改写成while反而更绕。说明这时应当保留do while,还是改用for (;;)加break,并给出理由。