循环

It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures. [^1]

目录


[TOC]

循环


本章将介绍 C 语言的重复语句(迭代语句),这种语句允许用户设置循环。

循环(loop)时重复的执行其他语句(循环体)的一种语句。在 C 语言中,每个循环都有自己的控制表达式(controlling expression)。循环每执行一次,都要对控制表达式求值。如果表达式为真(表达式的值非 0),那么循环继续执行;否则,退出循环。

C 提供了三种迭代语句:

  • while 语句
  • do 语句(do while 语句)
  • for 语句

其中以 for 语句最为常用。

本节后面还会讨论循环相关的 C 语言特性。如 break,continue,goto语句

一 while 语句

1. while 的基本用法

while 的基本格式如下:

while(控制表达式){
循环体
}

执行 while 语句时,首先计算控制表达式的值。如果值不为零,那么执行循环体,接着再次判定 控制表达式的真值,如果为真,再次执行循环体。直到控制表达式的真值为假,才会结束 while 语句。

**例1-1:**倒计数程序

int i = 10;
while(i > 0){
printf("%d\n", i);
i--;
}

关于这个例子,我们可以对 while 进行深度思考:

  • while 循环终止时,控制表达式的值一定为假

    int i = 10;
    while(i > 0){
    printf("%d\n", i);
    i--;
    }
    printf("%d", i); // i is now 0
  • 可能根本不执行 while 循环体

    int i = 0;
    while(i > 0){
    printf("%d\n", i);
    i--;
    }
    // Nothing is printed.
  • while 语句常有多种写法

    int i = 10;
    while(i > 0){
    printf("%d\n", i--); // 将 i-- 写在 printf 内,简化循环
    }

2. 无限循环

如果控制表达式的值始终非零,while 循环将无法终止。

while(1){
printf("Hello World\n");
}

除非循环体内有控制循环的语句(break,return,goto)或者调用了导致程序终止的函数,非则上面的循环永远不会结束。

程序 1:显示平方表

现编写一个程序来显示平方表。首先程序提示用户输入一个数 n,然后显示出 n 行的输出,每行包含 一个 1 ~ n 的数及其平方值。

This program prints a table of squares.
Enter number of entries in table: 5
1 1
2 4
3 9
4 16
5 25

参考答案:

#include<stdio.h>

int main(void){

int n;
int i = 1;

printf("This program prints a table of squares.\n");
printf("Enter number of entries in table: ");
scanf("%d", &n);

while(i <= n){
printf("\t%d\t%d\n", i, i * i);
i++;
}

return 0;
}

程序 2:数列求和

This program sums a series of integers.
Enter integers(0 to terminate):8 23 71 5 0
The sum is:107

参考答案:

#include<stdio.h>

int main(void){

int n, sum = 0;

printf("This program sums a series of integers.\n");
printf("Enter integers(0 to terminate): ");

scanf("%d", &n);
while(n != 0){
sum += n;
scanf("%d", &n);
}

printf("The sum is:%d\n", sum);

return 0;
}

我写的,仅供参考:

#include<stdio.h>

int main(void){

int n, sum = 0;

printf("This program sums a series of integers.\n");
printf("Enter integers(0 to terminate): ");

while(scanf("%d", &n) && n){
sum += n;
}

printf("The sum is:%d\n", sum);

return 0;
}

或者 while 循环可以这样写:

int n = 1, sum = 0;

while(n != 0){
scanf("%d", &n);
sum += n;
}

二 do 语句

1. do while 基本用法

do 语句 和 while 语句其实本质上是相同的。只不过 do 语句至少会执行一次循环体。

基本形式:

do{
循环体
}while(控制表达式);

例2-1倒计数程序

int i = 10;
do{
printf("%d\n", i);
i--;
}while(i > 0);

顺便一提,do 语句最好都加上花括号

虽然 do while 没有 while 语句使用的那么多,但是前者对于至少需要执行一次的循环来说是十分方便的。

程序 3:编写一个程序计算用户输入的整数的位数

Enter a integer: 60
The number has 2 digit(s).

参考答案:

#include<stdio.h>

int main(void){

int n, count = 0;

printf("Enter a nonnegative integer:");
scanf("%d", &n);

do{
count++;
n /= 10; // 除法 10 的次数就是 n 的位数
}while(n != 0);

printf("The number has %d digit(s).\n", count);

return 0;
}

如果我们用 while 循环:

while(n != 0){
count++;
n /= 10;
}

如果你输入的是 0 ,这个循环会直接退出。这不符合我们的预期,0 也是整数啊,而且它有 1 位数。

三 for 循环

现在开始介绍 C 语言最后一种循环,也是功能最强大的一种循环:for 语句。它是我们用的最多的一种循环,一定要熟练掌握。

1. for 语句的基本用法

for(表达式1; 表达式2; 表达式3){
循环体
}

例3-1 倒计数程序

for(i = 10; i > 0; i--){
printf("%d\n", i);
}

在执行上面这个 for 语句时,i 先初始化为 10;然后判定 i 是否大于 0 ;因为结果为真,执行循环体;然后对变量 i 进行自减操作;然后再次判断 i 是否大于 0 … 直到最后一次 i 自减后,i > 0 不成立了,退出循环。

for 循环如果我们用 while 语句 也可以模拟:

i = 10;
while(i > 0){
printf("%d\n", i);
i--;
}

抽象一下即为:

表达式1;
while(表达式2){
循环体;
表达式3;
}

for 循环中的 i-- 可以写成 --i 吗?

即:

for(i = 10; i > 0; --i){
printf("%d\n", i);
}

这种做法对循环没有任何影响。i++i-- 在 for 循环的 表达式3 中是完全等价的。

2. 在 for 语句中省略表达式

1.省略第一个表达式

i = 10;
for(; i > 0; --i){
printf("%d\n", i);
}

注意:

  • 省略第一个表达式相当于不初始化 i
  • 即便 第一个表达式省略了,也不能省略后面的分号

2. 省略第三个表达式

for(i = 10; i > 0;){
printf("%d\n", i);
--i
}

3. 省略第一个和第三个表达式

i = 10;
for(; i > 0; ){
printf("%d\n", i);
--i
}

是不是很像我们的 while 语句?

4. 省略第二个表达式

for (; ;) {
printf("Hello\n");
}

省去控制表达式,默认为真,因此 for 语句会不断循环下去。

它相当于:

while (1) {
printf("Hello\n");
}

3. C99 中的 for 语句

C99 中第一个表达式可以替换成一个声明:

for(int i = 0; i < 10; i++){

}

变量 i 不需要在该句之前进行声明。事实上,如果变量 i 在之前已经声明了,这个语句会创建一个新的 i 且该值仅用于循环内。(新的 i 会覆盖之前的 i 。for 语句的花括号相当于一个块,新 i 的作用域仅在这个块内。)

例如:

int main(void) {

int i = 20;

for (int i = 0; i < 10; i++) {
printf("%d\n", i);
}
// 循环结束新的 i 应该等于 10

printf("%d\n", i);

return 0;
}

输出:

0
1
2
3
4
5
6
7
8
9
20

最后输出的 20 不符合我们的预期,这说明了一个问题:

旧的 i 始终存在,只不过在 for 语句内,新的 i 相对旧的 i 显“显性”(可以用高中生物的基因来理解一下;或者你就理解为覆盖,但是新的 i 开辟的是新的内存,它不会真的覆盖旧的 i )。

for 语句声明的变量不可以在循环外访问(生存期仅在 for 语句内)。例如:

for (int i = 0; i < 10; i++) {
printf("%d\n", i);
}

printf("%d\n", i);// 在这里编译会报错,"未定义标识符 i"

让 for 语句声明自己的循环控制变量通常是一个好办法:这样方便且程序的可读性更强,但是如果在 for 循环退出后还要使用该变量,则只能使用以前的 for 语句格式。

另外,for 语句可以声明多个变量,只要它们的类型相同

for(int i = 0, j = 10; i < j; i++, j--){

}

4. 逗号表达式

逗号表达式(comma expression)

基本用法
表达式1,表达式2,表达式3,...,表达式n;
  • 逗号表达式的优先级低于所有其他运算符
  • 从左向右依次执行:先计算表达式1,然后是表达式2 …
  • 最终整个逗号表达式的值为最后一个表达式的值

例如:

i = 1, j = 2, k = i + j;

相当于是:

((i = 1), (j = 2), (k = i + j));

整个逗号表达式的值为 k = i + j 的值,也就是 3

应用

假如在进入 for 语句时希望初始化两个变量

sum = 0;
for(i = 1; i <= N; i++){
sum += i;
}

改写为:

for(sum = 0, i = 1; i <= N; i++){
sum += i;
}

程序 4:显示平方表(for)

#include<stdio.h>

int main(void){

int n;

printf("This program prints a table of squares.\n");
printf("Enter number of entries in table: ");
scanf("%d", &n);

for(int i = 1; i <= n; i++){
printf("\t%d\t%d\n", i, i * i);
}

return 0;
}

程序 5:不用乘法,实现程序“显示平方表”

#include<stdio.h>

int main(void){

int n;

printf("This program prints a table of squares.\n");
printf("Enter number of entries in table: ");
scanf("%d", &n);

for(int i = 1, square = 1, odd = 3; i <= n; i++){
printf("\t%d\t%d\n", i, square);
square += odd;
odd += 2;
}

return 0;
}

四 退出循环

有时,循环还没有结束,但我们需要在循环的中间设置退出点,甚至可能需要对循环设置多个退出点。break 语句可以满足这种需求。

continue 语句用来跳过某次迭代的部分内容,但是不会跳过整个循环。

goto 语句允许程序从一条语句直接跳到另一条语句。

1. break 语句

break 语句不但可以把程序控制从 switch 语句中转移出来;还可以用于跳出 while,do while,和 for 循环。

程序 6:判断素数

假如要编写一个程序判断 n 是否为素数。我们计划是编写一个 for 语句用 n 除以 2 到 n - 1 之间的所有数。一旦发现有约数就跳出循环,没不需要继续尝试下去。循环终止后,可以用 if 判断循环是提前终止(不是素数)还是正常终止(是素数)。

#include<stdio.h>

int main(void){

int n, i;

printf("Enter a number:");
scanf("%d", &n);

for(i = 2; i < n; i++){
if(n % i == 0){
break;
}
}

if(i < n){
printf("%d is not a prime\n", n);
}else{
printf("%d is a prime\n", n);
}

return 0;
}

程序 7:程序2 重写

程序 2 中,要求我们当用户输入 0 时,结束求和。我们用很多中方法去实现了,我们可以用 break 语句让程序的意图更加直观。

#include<stdio.h>

int main(void){

int n, sum = 0;

printf("This program sums a series of integers.\n");
printf("Enter integers(0 to terminate): ");


while(1){
scanf("%d", &n);
if(n == 0){
break;
}else{
sum += n;
}
}

printf("The sum is:%d\n", sum);

return 0;
}

一个 break 只能跳出一个循环,如果循环嵌套,那么可能需要多个 break 才能从里层的循环跳出。

int i;
while(1){
for(i = 1; i < 10; i++){
if(i == 5){
printf("%d\n", i);
break;
}
}
}

运行一下这个程序,发现,屏幕上一直在输出 5,说明外层的循环没有跳出。

2. continue 语句

break 语句刚好将程序控制转移到循环体的末尾之后;而 continue 语句刚好将程序控制转移到循环体末尾之前

break 语句会跳出循环;而 continue 语句会将程序控制留在循环体之内。

其实 continue 的作用就是跳过本次循环点剩下内容,重新开始下一轮循环。

程序 8:计算奇数和

编写一个程序。先提示用户输入一个数 n,然后将 1 ~ n 的所有奇数相加,然后输出最终的和

不使用 continue

#include<stdio.h>

int main(void){

int n, sum = 0;

printf("Enter a number:");
scanf("%d", &n);

for(int i = 1; i <= n; i++){
if(i % 2 != 0){
sum += i;
}
}

printf("Sum of odds is :%d\n", sum);

return 0;
}

使用 continue

只需要改变 for 循环即可

for(int i = 1; i <=n; i++){
if(i % 2 == 0){
continue;
}
sum += i;
}

3. goto 语句

break 和 continue 语句都是跳转语句:它们可以把控制从程序的一个位置转移到另一个位置。然而,这两者都是受限制的。

goto 语句则可以跳转到函数中任何有标号的语句处。(C99 增加了一条限制:goto 不能用于绕过变长数组(后面的章节会讲)的声明。)

标识符:语句;
goto 标识符;

程序:程序 6 重写

#include<stdio.h>

int main(void){

int n, i;

printf("Enter a number:");
scanf("%d", &n);

for(i = 2; i < n; i++){
if(n % i == 0){
goto done;
}
}
done:
if(i < n){
printf("%d is not a prime\n", n);
}else{
printf("%d is a prime\n", n);
}

return 0;
}

goto 语句在早期的编程语言中很常见,但是日常 C 语言编程却很少使用它了。break,continue,return 语句(本质上都是受限制的goto 语句)和 exit 函数足以应付大多数需要使用 goto 的情况。

而且 goto 语句不建议滥用,能不用就不用,因为 goto 语句可以打乱程序原本的执行顺序,这大大降低了程序的可读性,提高了改错成本。(这里程序圆是有切身体会的,大一 做C语言的课设的时候,那时候对 C 掌握的并不是很好。我在一个函数内大量的使用 goto 语句,导致我最后都不知道我的 goto 跳到哪里了。)

但是 goto 语句偶尔还是有用的,比如上面的例子:循环嵌套的情况,使用goto 语句就很好跳出多重循环了:

int i;
while(1){
for(i = 1; i < 10; i++){
if(i == 5){
printf("%d\n", i);
goto find;
}
}
}
find:

程序:账薄结算

这个程序帮你理解一种简单的交互式程序设计,我们可以通过这种方式设计菜单。

题目:

开发一个程序用来维护账簿的余额。程序将为用户提供选择菜单:清空余额账户,向账户存钱,从账户取钱,显示当前余额,退出程序。选项用 0,1,2,3,4表示。程序的会话类似这样:

**** ACME checkbook-balancing program ****
Comands: 0 = clear, 1 = credit, 2 = debit, 3 = balance, 4 = exit

Enter command: 1
Enter amount of credit: 1042.56
Enter command: 2
Enter amount of debit : 133.56
Enter command: 3
Current balance: 909
Ener command: 4
Goodbye~

参考程序:

#include<stdio.h>

int main(void) {

double balance = 0; // 余额
double credit, debit;

// 菜单,形式可以自己设计,尽量美观一点嘛,不过不用纠结这种界面,不要舍本逐末。

printf("**** ACME checkbook-balancing program ****\n");
printf(" Comands: \n");
printf(" 0 = clear \n");
printf(" 1 = credit \n");
printf(" 2 = debit \n");
printf(" 3 = balance \n");
printf(" 4 = exit \n");

// 题目中已经规定了这些功能用 0,1,2,3,4 代替,其实是想让我们用 switch
// 如果你想用 if else 也完全 ok

// 死循环让用户可以重复选择
while (1) {
int choice;
printf("Enter command: ");
scanf("%d", &choice);

switch (choice) {

// 清除账户是一种很“危险”的举动,可以让用户确认一次

case 0: printf("Are you sure to clear your balance?\n");
printf("1 = yes, 0 = no\n");
int isClear;
scanf("%d", &isClear);
if (isClear == 1) {
balance = 0;
printf("clear successfully!\n");
}
break;

case 1: printf("Enter amount of credit: ");
scanf("%lf", &credit);
balance += credit;
break;

case 2: printf("Enter amount of debit : ");
scanf("%lf", &debit);
balance -= debit;
break;

case 3: printf("Current balance: %.2f\n", balance);
break;

case 4: printf("Are you sure to quit?\n");
printf("1 = yes, 0 = no\n");
int isQuit;
scanf("%d", &isQuit);
if (isQuit == 1) {
printf("Goodbye~\n");
return 0;
}
else {
break;
}
default: printf("Illeagl option!\n");
break;
}
}
}

五 空语句

i = 0; ; j = 1;

第二个语句就是一个空语句:除了末尾的分号,什么符号也没有。

空语句主要有一个好处:编写空循环体的循环。

判断素数的循环我们可以这样改写:

for(d = 2; d <= n && n % d != 0; d++) 
;

注意不要写成:

for(d = 2; d <= n && n % d != 0; d++) ;

程序员习惯将空语句单独放在一行,否则,一般人阅读程序时可能会混淆后面的语句是否是其循环体。

for(d = 2; d <= n && n % d != 0; d++) ;
if(d < n)
printf("%d is divisible by %d", n, d);

一般情况下,将普通循环转化为带空循环体的语句不会提高效率,但是会让程序更加简洁。

但是在一些情况下,可能带空循环体的循环会更加高效。如:读取字符数据时(后面会讲)。

空语句可能会造成的情况

1.if 语句

if(d == 0);
printf("Error: division by zero\n");

如果输入的 d 不是 0,后面的这条消息一样会被打印。

2.while 语句

i = 10;
while(i > 0);{
printf("%d\n", i);
i--;
}

程序会进入死循环,因为 while 没有循环体,而 i 的的值不会减小。

i = 11;
while(--i > 0);{
printf("%d\n", i);
i--;
}

循环终止,但是花括号内的语句只被执行一次。

输出:0

3. for 语句

for(i = 10; i > 0; i--);
printf("%d\n", i);

printf 语句被执行一次

输出:0

参考资料:《C Primer Plus》《C语言程序设计:现代方法》

[^1]: 用100个函数操作一个数据结构比仅用10个函数但是操作10个不同的数据结构要好。Epigrams on Programming 编程警句