不知道次数的重复
当你知道或者能算出需要循环多少轮时,for 循环是合适的工具。但很多工作并没有现成的次数。逐行读取文件,直到结束。不断要求输入密码,直到正确为止。把一个数不断减半,直到它变成 1。对这些情况,问题不是“多少次”,而是“到什么时候”,而这正是 while 所表达的。
没有人事先算出“七”这个数。只要条件成立,循环就一直进行。
语法和先判断条件的规则
while (condition) {
// body - runs while the condition is nonzero
}
条件在每一轮之前都会被判断,包括第一轮。这带来三个结果:
- 如果条件一开始就为假,循环会执行零次。
while (0) { ... }永远不会执行循环体。 - 条件所判断的东西必须在循环之前初始化,否则你判断的是一个垃圾值。
- 循环体中必须有东西最终让条件变为假,否则循环永远不会结束。
和 if 一样,括号是必需的,括号后面没有分号。多出来的那个分号就是同一个 bug 在循环里的版本:
/* BUG: the empty statement is the body - this spins forever */
while (i < 10);
{
printf("%d\n", i);
i++;
}
i 永远不会递增,所以条件永远不会改变。gcc -Wall 会标出这个空的循环体。
C 语言的真假规则在这里和在 if/else 中完全一样:只要表达式非零,循环就继续;为零时停止。
标准结构:初始化、判断、更新
几乎每个正确的 while 循环都由分布在三个地方的三部分组成:
这和 for 头部包含的三个部分完全相同,只是没有集中在一行里。这也是“用 for 还是 while?”的实在答案:如果三部分都围绕同一个计数器,for 会把它们放在一起,方便读者一并检查。如果更新散落在很长的循环体中,或者条件判断的不是计数,while 是更清晰的写法。
忘记第 3 部分是 while 循环最典型的 bug。如果循环卡住了,首先要检查循环体是否真的改变了条件所读取的东西。
哨兵循环
哨兵是一个表示“停止”的特殊值。对数字求和,直到用户输入 0:
0 之后的 99 永远不会被读到,哨兵已经结束了循环。注意,数字的个数事先从来都不知道,这正是这里要用 while 的原因。
C 字符串也是同样的道理:它们以 '\0' 哨兵结尾,所以遍历一个字符串是教科书式的 while:
读取输入直到 EOF
实际中最常见的 while 是读取输入,直到没有输入为止。规则是判断输入函数的返回值,而不是它存下来的值。
getchar 返回下一个字符,输入结束时返回常量 EOF:
有两个细节很重要。c 是 int,因为 EOF 是一个负值,在很多系统上放不进 char:把它声明成 char 可能导致比较永远不成立。另外,赋值写在条件里面:(c = getchar()) != EOF 一步完成读取字符、存储和比较。里面那层括号是必需的,因为 != 的优先级比 = 高;不加括号的话,存进 c 的会是比较的结果。这是少数几个在条件中赋值属于惯用写法而不是 bug 的地方之一。
同样的结构也适用于 scanf,它的返回值是成功转换的项数:
int n;
while (scanf("%d", &n) == 1) {
/* n holds a valid number */
}
循环会在文件末尾以及遇到第一个不是数字的输入时结束,这通常正是你想要的。
不应该写的是 while (!feof(f))。feof 报告的是某次读取是否已经失败,所以当你停在最后一项上时它仍然为假,循环体会再执行一次,而这次读取失败,处理的是过时的数据。应该直接判断读取操作本身。
while (1) 和 break
有时退出条件只有在循环体中间才能知道:读取之后、处理之前。惯用的做法是写一个有意为之的无限循环,再配一个明确的出口:
while (1) 永远为真,所以唯一的出路是 break(或者 return、exit)。你也会看到用 for (;;) 达到同样目的;两者编译出来完全一样,代码库偏好哪种只是风格问题。
当退出判断确实属于循环体中间时,就用这种结构。如果判断很自然地放在开头,就把它写进条件里,让读者一眼看到:一个唯一的 break 就是第一条语句的 while (1),只是一个伪装起来的条件。
for 和 while 的相互转换
任何 for 循环都能写成 while 循环,反过来也一样。对应关系是机械式的:
/* for */ /* equivalent while */
for (init; cond; update) { init;
body; while (cond) {
} body;
update;
}
所以下面两段代码打印的内容相同:
唯一不完全等价的地方是 continue。在 for 循环中,continue 会跳到头部的更新部分,所以计数器仍然会前进。在 while 版本中,continue 直接跳回条件判断,跳过了循环体末尾的 i++,立刻变成死循环:
/* BUG: continue skips i++, so the loop spins forever on the first even number */
int i = 0;
while (i < 10) {
if (i % 2 == 0) {
continue;
}
printf("%d ", i);
i++;
}
如果 while 循环中用到了 continue,要确保更新发生在它之前,或者改用 for,由头部来保证更新。
常见陷阱
- 没有东西更新条件。 循环体必须改变条件所读取的东西。程序卡住时,先检查这里。
- 头部后面多了分号。
while (cond);会让空语句成为循环体。 - 判断一个从未初始化的变量。 条件在循环体之前执行,所以它读取的值必须已经存在。
getchar的结果用char而不是int。EOF放不进char;这样比较可能永远不成立,或者一个合法的字节被误认为EOF。while (!feof(f))。 会多循环一次。应该判断读取操作。- 以为循环体至少会执行一次。 并不会。当你需要保证至少执行一次时,比如先打印菜单再检查选择,那就该用 do-while 循环。
常见问题
C 语言的 while 循环怎么写?
while (condition) { body }。每一轮之前都会判断条件,所以只有条件非零时循环体才会执行;如果一开始就为假,循环体一次也不执行。循环体中必须有东西最终改变条件,否则循环永远不会结束。
C 语言中 for 和 while 有什么区别?
两者能力相同,任何 for 都能改写成 while。当计数器、它的上限和步长适合放在同一个头部时用 for;当停止条件不是计数时用 while,比如读到文件末尾、重试直到成功,或者循环到某个标志改变为止。
C 语言怎么读取输入直到 EOF?
判断输入函数的返回值,而不是它读到的值:while (scanf("%d", &n) == 1) { ... } 会在文件末尾或遇到第一个非数字输入时停止,while ((c = getchar()) != EOF) { ... } 则逐个字符读取。永远不要用 while (!feof(f)) 来循环,它会在末尾之后多执行一次。
为什么我的 C 语言 while 循环变成了死循环?
通常是因为循环体中没有任何东西改变条件所判断的值,或者头部后面多了一个分号(while (i < 10);),让空语句成了循环体。这两种写法都能顺利编译,所以要检查循环体是否真的更新了条件中的变量。