我从零开始学习C语言(12)- 循环语句 PART1
六、循环
开篇提到:没有循环和结构化变量的程序不值得编写。
这句话体现了C语言作为结构化程序设计语言的核心思想。原因至少有5条:
(1)它是结构化编程的基本要求
循环结构和结构化变量是结构化编程的三大基本结构(顺序、选择、循环)中的核心组成部分。缺少它们,程序将无法实现复杂的逻辑控制和重复操作,仅能完成简单的线性流程。
(2)它具有解决问题的必要性
现实中的计算任务通常需要重复处理(如批量数据操作)或条件分支(如错误处理)。循环结构(如for
/while
)和结构化变量(如数组、结构体)是高效组织代码的基础工具。例如,累加1到100的数值必须依赖循环实现。
(3)它使代码具有可维护性与可读性
结构化变量(如结构体)能将分散的数据整合为逻辑单元,而循环能避免代码冗余。例如,用循环输出九九乘法表比硬编码81行printf
更易维护36。非结构化的代码(如大量goto
)会导致"面条式代码",难以调试和扩展。
(4)它是计算机思维的本质体现
循环是计算机"重复执行"能力的直接映射,结构化变量则是对现实数据的抽象。例如,for
循环的三段式(初始化、条件、迭代)集中体现了计算机的流程控制逻辑。
(5)它是历史教训的总结
早期非结构化编程(如汇编语言)因流程混乱难以维护,而结构化编程通过限制goto
的使用,强制采用循环等控制结构,显著提升了代码质量。
循环是重复执行其他语句(循环体)的一种语句。在C语言中,每个循环都有一个控制表达式 。每次执行循环体(循环重复一次)时都要对控制表达式求值。如果表达式为真(即值不为零),那么继续执行循环。
C语言提供了3种重复语句,即while 语句、do 语句和for 语句,我们后续分别介绍。while 循环在循环体执行之前测试控制表达式,do循环在循环体执行之后测试控制表达式,for 语句则非常适合那些递增或递减计数变量的循环。后面还会介绍主要用于for 语句的逗号运算符。
最后两节讨论与循环相关的C语言特性。
6.4描述break 语句、continue 语句和goto 语句。
break 语句用来跳出循环并把程序控制传递到循环后的下一条语句,continue 语句用来跳过本次循环的剩余部分,而goto 语句则可以跳到函数内的任何语句上。
6.5介绍空语句,它可以用于构造循环体为空的循环。
6.1 while语句
在C语言所有设置循环的方法中,while 语句是最简单也是最基本的。while 语句的格式如下所示:
[while 语句] while (表达式) 语句
圆括号内的表达式是控制表达式,圆括号后边的语句是循环体。下面举例:
while(i < n) //控制表达式i = i * 2; //循环体
注意,这里的圆括号是强制要求的,而且在右括号和循环体之间没有任何内容。
执行while 语句时,首先计算控制表达式的值。如果值不为零(即真值),那么执行循环体,接着再次判定表达式。这个过程(先判定控制表达式,再执行循环体)持续进行直到控制表达式的值变为零才停止。
下面的例子使用while 语句计算大于或等于数n的最小的2的幂:
i = 1;
while (i < n)i = i * 2;
假设n的值为10。下面跟踪显示了while 语句执行时的情况:
- i = 1; i现在值为1
- i < n成立? 是的,继续执行。
- i = i * 2; i现在值为2
- i < n成立?是的,继续执行。
- i = i * 2; i现在值为4
- i < n成立?是的,继续执行。
- i = i * 2; i现在值为8
- i < n成立?是的,继续执行。
- i = i * 2; i现在值为16
- i < n成立?不成立,退出循环。
注意,只有在控制表达式 i < n 为真的情况下循环才会继续。当表达式值为假时,循环终止,而且就像描述的那样,此时i 的值是大于或等于n 的。
虽然循环体必须是单独的一条语句,但这只是个技术问题;如果需要多条语句,那么只要用一对花括号构造成一条复合语句就行:
while (i > 0) {printf("T减%d并在计数\n", i);i--;
}
即使在没有严格要求的时候,一些程序员也总是使用花括号:
while (i < n) { //可以加括号,但这里不是必须加i = i * 2;
}
这是一个跟踪语句执行的示例。下面的语句显示一串“倒计数”信息。
i = 10;
while (i > 0) {printf("T减%d并在计数\n", i);i--;
}
在while 语句执行前,把变量i 赋值为10。因为10大于0,所以执行循环体,这导致显示出信息T减10并在计数,同时变量i进行自减。然后再次判定条件i > 0 。因为9大于0,所以再次执行循环体。整个过程持续到显示信息T减1并在计数,并且变量i的值变为0时停止。然后判定条件i > 0的结果为假,导致循环终止。
“倒计数”的例子引发出对while 语句的讨论。
- 在while 循环终止时,控制表达式的值为假。因此,由表达式i> 0控制的循环终止时,i 一定是小于或等于0的。(否则还将执行循环)
- 可能根本不执行while 循环体。因为控制表达式在循环体执行之前进行判定,所以循环体有可能一次也不执行。第一次进入倒计数循环时,如果变量i的值是负数或零,那么将不会执行循环。
- while 语句常常可以有多种写法。例如,我们可以在printf函数调用的内部进行变量i的自减操作,这样可以使倒计数循环更加简洁:
i = 10; while (i > 0) {printf("T减%d并在计数\n", i--); }
6.1.1 无限循环
如果控制表达式的值始终非零,while 语句将无法终止。事实上,我们有时故意用非零常量作为控制表达式来构造无限循环:
[惯用法] while (1)...
除非循环体中含有跳出循环控制的语句(break 、goto 、return)或者调用了导致程序终止的函数,否则上述形式的while 语句将永远执行下去。
程序08 显示平方表
现在编写一个程序来显示平方表。首先程序提示用户输入一个数 ,然后显示出行的输出,每行包含一个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把期望的平方数个数存储在变量n中。程序需要用一个循环来重复显示数i和它的平方值,循环从i等于1开始。如果i小于或等于n,那么循环将反复进行。需要保证的是每次执行循环时对i 值加1。可以使用while 语句编写循环。(坦白地说,现在没有其他更多的选择,因为while 语句是目前为止我们唯一掌握的循环类型。)下面是完成的程序。
square.c
/* Prints a table of squares using a while statement */ #include <stdio.h> int main(void) {int i, n;printf("This program prints a table of squares.\n");printf("Enter number of entries in table: ");scanf("%d", &n);i = 1;while (i <= n) {printf("%10d%10d\n", i, i * i);i++;}return 0; }
留意一下程序square.c 是如何把输出整齐地排成两列的。窍门是使用类似%10d 这样的转换说明代替%d ,并利用了printf 函数在指定宽度内将输出右对齐的特性。
程序09 数列求和
在下面这个用到while 语句的示例中,我们编写了一个程序对用户输入的整数数列进行求和计算。下面显示的是用户能看到的内容:
This program sums a series of integers.
Enter integers (0 to terminate): 8 23 71 5 0
The sum is: 107很明显,程序需要一个循环来读入数(用scanf 函数)并将其累加。用n表示当前读入的数,而sum表示所有先前读入的数的总和,得到如下程序:
sum.c
/* Sums a series of numbers */ #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; }
注意,条件n != 0 在数被读入后立即进行判断,这样可以尽快终止循环。此外,程序中用到了两个完全一样的scanf 函数调用,在使用while 循环时往往很难避免这种现象。
6.2 do语句
do 语句和while 语句关系紧密。事实上,do 语句本质上就是while 语句,只不过其控制表达式是在每次执行完循环体之后进行判定的。do 语句的格式如下所示:
[do 语句] do 语句 while (表达式);
和处理while 语句一样,do 语句的循环体也必须是一条语句(当然可以用复合语句),并且控制表达式的外面也必须有圆括号。执行do 语句时,先执行循环体,再计算控制表达式的值。如果表达式的值是非零的,那么再次执行循环体,然后再次计算表达式的值。在循环体执行后 ,若控制表达式的值变为0,则终止do 语句的执行。
下面使用do 语句重写前面的“倒计数”程序:
i = 10;
do {printf("T减%d并在计数\n", i);--i;
} while (i > 0);
执行do 语句时,先执行循环体,这导致显示出 信息T减10并在计数 ,并且i 自减。接着对条件i > 0 进行判定。因为9大于0,所以再次执行循环体。这个过程持续到显示出信息 T减1并在计数 并且i 的值变为0。此时判定表达式i > 0 的值为假,所以循环终止。
正如此例中显示的一样,do 语句和while 语句往往没有什么区别。
两种语句的区别是,do 语句的循环体至少要执行一次,而while 语句在控制表达式初始为0时会完全跳过循环体。
顺便提一下,无论需要与否,最好给所有的 do 语句都加上花括号,这是因为没有花括号的do 语句很容易被误认为是while 语句:粗心的程序员可能会把单词while 误认为是while 语句的开始。
程序010 计算整数的位数
虽然C程序中while 语句的出现次数远远多于do语句,但是后者对于至少需要执行一次的循环来说是非常方便的。为了说明这一点,现在编写一个程序计算用户输入的整数的位数:
Enter a nonnegative integer: 60
The number has 2 digit(s).方法是把输入的整数反复除以10,直到结果变为0停止;除法的次数就是所求的位数。因为不知道到底需要多少次除法运算才能达到0,所以很明显程序需要某种循环。但是应该用while 语句还是do 语句呢?do 语句显然更合适,因为每个整数(包括0)都至少有一位数字。下面是程序。
numdigit.c
/* Calculates the number of digits in an integer */ #include <stdio.h> int main(void) {int digits = 0, n;printf("Enter a nonnegative integer: ");scanf("%d", &n);do {n /= 10;digits++;} while (n > 0);printf("The number has %d digit(s).\n", digits);return 0; }
为了说明do 语句是正确的选择,下面观察一下如果用相似的while循环替换do 循环会发生什么:
while (n > 0) {n /= 10;digits++; }
如果n初始值为0,上述循环根本不会执行,程序将打印出
The number has 0 digit(s).
6.3 for语句
现在介绍C语言循环中最后一种循环,也是功能最强大的一种循环:for 语句。不要因为for 语句表面上的复杂性而灰心;实际上,它是编写许多循环的最佳方法。for 语句非常适合应用在使用“计数”变量的循环中,当然它也可以灵活地用于许多其他类型的循环中。
for 语句的格式如下所示:
[for 语句格式] for (表达式1; 表达式2; 表达式3) 语句
其中表达式1 、表达式2 和表达式3 全都是表达式。下面举例:
for (i = 10; i > 0; i--)printf("T减%d并在计数\n",i);
在执行for 语句时,变量i 先初始化为10,接着判定i 是否大于0。因为判定的结果为真,所以打印信息T减10并在计数,然后变量i 进行自减操作。随后再次对条件i > 0 进行判定。循环体总共执行10次,在这一过程中变量i 从10变化到1。
for 语句和while 语句关系紧密。 事实上,除了一些极少数的情况以外,for 循环总可以用等价的while 循环替换:
表达式1;
while (表达式2) {
语句
表达式3;
}
就像这个模式显示的那样,表达式1 是循环开始执行前的初始化步骤,只执行一次;表达式2 用来控制循环的终止(只要表达式2 的值不为零,循环持续执行);而表达式3 是每次循环中最后被执行的一个操作。把这种模式用于先前的for 循环示例中,可以得到:
i = 10;
while (i > 0) {printf("T减%d并在计数\n", i);i--;
}
研究等价的while 语句有助于更好地理解for 语句。例如,假设把先前的for 循环示例中的i-- 用--i 来替换:
for (i = 10; i > 0; --i)printf("T减%d并在计数\n", i);
这样做会对循环产生什么样的影响呢?看看等价的while 循环就会发现,这种做法对循环没有任何影响:
i = 10;
while (i > 0) {printf("T减%d并在计数\n", i);--i;
}
因为for 语句中第一个表达式和第三个表达式都是以语句的方式执行的,所以它们的值互不相关——它们有用仅仅是因为有副作用。结果是,这两个表达式常常作为赋值表达式或自增/自减表达式。
6.3.1 for语句惯用方法
对于“向上加”(变量自增)或“向下减”(变量自减)的循环来说,for 语句通常是最好的选择。对于向上加或向下减共n 次的情况,for 语句经常会采用下列形式中的一种。
从0向上加到n-1 :
[惯用法] for ( i = 0; i < n; i++) ...
从1向上加到n :
[惯用法] for ( i = 1; i <= n; i++) ...
从n-1 向下减到0:
[惯用法] for ( i = n - 1; i >= 0; i--) ...
从n 向下减到1:
[惯用法] for ( i = n; i > 0; i--) ...
模仿这些书写格式将有助于避免初学者常犯的下面一些错误。
在控制表达式中把> 写成< (或者相反)。注意,“向上加”的循环使用运算符<或运算符<= ,而“向下减”的循环则依赖运算符>或运算符>= 。
在控制表达式中把< 、<= 、> 或>= 写成== 。控制表达式的值在循环开始时应该为真,以后会变为假以便能终止循环。类似i==n 这样的判定没什么意义,因为它的初始值不为真。
编写的控制表达式中把i < n写成i <=n ,这会犯“循环次数差一”错误。
6.3.2 在for语句中省略表达式
for 语句远比目前看到的更加灵活。通常for 语句用三个表达式控制循环,但是有一些for循环可能不需要这么多,因此C语言允许省略任意或全部的表达式。
如果省略 第一个 表达式,那么在执行循环前没有初始化的操作:
i = 10;
for (; i > 0; --i)printf("T减%d并在计数\n", i);
在这个例子中,变量i 由一条单独的赋值语句实现了初始化,所以在for 语句中省略了第一个表达式。(注意,保留第一个表达式和第二个表达式之间的分号。即使省略掉某些表达式,控制表达式也必须始终有两个分号。)
如果省略了for 语句中的第三个 表达式,循环体需要确保第二个表达式的值最终会变为假。我们的for 语句示例可以这样写:
for (i = 10; i > 0;)printf("T减%d并在计数\n", i--);
为了补偿省略第三个表达式产生的后果,我们使变量i 在循环体中进行自减。
当for 语句同时省略掉第一个 和第三个 表达式时,它和while 语句没有任何分别。例如,循环
for (; i > 0;)printf("T减%d并在计数\n", i--);
等价于
while (i > 0)printf("T减%d并在计数\n", i--);
这里while 语句的形式更清楚,也因此更可取。如果省略第二个 表达式,那么它默认为真值,因此for 语句不会终止(除非以某种其他形式停止)。
例如,某些程序员用下列的for语句建立无限循环:
[惯用法] for ( ; ; )...
6.3.3 C99中的for语句
在C99中,for 语句的第一个表达式可以替换为一个声明,这一特性使得程序员可以声明一个用于循环的变量:
for (int i = 0; i < n; i++)
...
变量i不需要在该语句前进行声明。事实上,如果变量i在之前已经进行了声明,这个语句将创建一个新的i且该值仅用于循环内。
for 语句声明的变量不可以在循环外访问(在循环外不可见):
for (int i = 0; i < n; i++){...printf("%d", i); //对的,内部循环中i可见...
}
printf("%d", i); //错了,循环外部i不可见
让for 语句声明自己的循环控制变量通常是一个好办法:这样很方便且程序的可读性更强,但是如果在for 循环退出之后还要使用该变量,则只能使用以前的for 语句格式。顺便提一下,for 语句可以声明多个变量,只要它们的类型相同:
for (int i = 0, j = 0; i < n; i++)
...
6.3.4 逗号运算符
有些时候,我们可能喜欢编写有两个(或更多个)初始表达式的for语句,或者希望在每次循环时一次对几个变量进行自增操作。使用逗号表达式作为for 语句中第一个或第三个表达式可以实现这些想法。
逗号表达式的格式如下所示:
[逗号表达式] 表达式1, 表达式2
这里的表达式1 和表达式2 是两个任意的表达式。
逗号表达式的计算要通过两步来实现:
第一步,计算表达式1并且扔掉计算出的值。
第二步,计算表达式2 ,把这个值作为整个表达式的值。对表达式1 的计算应该始终会有副作用;如果没有,那么表达式1 就没有了存在的意义。
例如,假设变量 i 和变量 j 的值分别为1和5,当计算逗号表达式++i, i+j 时,变量 i 先进行自增,然后计算 i+j ,所以表达式的值为7。(而且,显然现在变量 i 的值为2。)顺便说一句,逗号运算
符的优先级低于所有其他运算符,所以不需要在++i 和i+j 外面加圆括号。
有时需要把一串逗号表达式串联在一起,就如同某些时候把赋值表达式串联在一起一样。逗号运算符是左结合的,所以编译器把表达式
i = 1, j = 2, k = i + j
解释为
((i = 1), (j = 2)), (k = (i + j))
因为逗号表达式的左操作数在右操作数之前求值,所以赋值运算i =1 、j = 2 和k = i + j 是从左向右进行的。
提供逗号运算符是为了在C语言要求只能有一个表达式的情况下可以使用两个或多个表达式。
换句话说,逗号运算符允许将两个表达式“粘贴”在一起构成一个表达式。(注意与复合语句的相似之处,后者允许我们把一组语句当作一条语句来使用。)需要把多个表达式粘在一起的情况不是很多。
正如后面的某一章将介绍的那样,某些宏定义可以从逗号运算符中受益。除此之外,for 语句是唯一可以发现逗号运算符的地方。例如,假设在进入for 语句时希望初始化两个变量。可以把原来的程序:
sum = 0;
for (i =1; i <= N; i++)sum += i;
改写为
for (sum = 0, i = 1; i <= N; i++)sum += i;
表达式sum = 0, i = 1 首先把0赋值给sum ,然后把1赋值给i 。利用附加的逗号运算符,for语句可以初始化更多的变量。
程序011 显示平方表(改进版)
程序square.c (6.1节)可以通过将while 循环转化为for循环的方式进行改进。
square2.c
/* Prints a table of squares using a for statement */ #include <stdio.h> int main(void) {int i, n;printf("This program prints a table of squares.\n");printf("Enter number of entries in table: ");scanf("%d", &n);for (i = 1; i <= n; i++)printf("%10d%10d\n", i, i * i);return 0; }
利用这个程序可以说明关于for 语句的一个要点:C语言对控制循环行为的三个表达式没有加任何限制。虽然这些表达式通常对同一个变量进行初始化、判定和更新,但是没有要求它们之间以任何方式进行相互关联。看一下同一个程序的另一个版本。
square3.c
/* Prints a table of squares using an odd method */ #include <stdio.h> int main(void) {int i, n, odd, square;printf("This program prints a table of squares.\n");printf("Enter number of entries in table: ");scanf("%d", &n);i = 1;odd = 3;for (square = 1; i <= n; odd += 2) {printf("%10d%10d\n", i, square);++i;square += odd;}return 0; }
这个程序中的for 语句初始化一个变量(square ),对另一个变量(i )进行判定,并且对第三个变量(odd )进行自增操作。变量i是要计算平方值的数,变量square 是变量i 的平方值,而变量odd是一个奇数,需要用它来和当前平方值相加以获得下一个平方值(允许程序不执行任何乘法操作而计算连续的平方值)。
for 语句这种极大的灵活性有时是十分有用的。后面我们将会发现for 语句在处理链表时非常有用。但是,for 语句很容易误用,所以请不要走极端。如果重新安排程序square3.c 的各部分内容,可以清楚地表明循环是由变量i来控制的,这样程序中的for循环就清楚多了。
做一些OJ习题
1881、循环输出1~100之间的每个数(点击链接跳转)
while写法:
#include <stdio.h>int main(void)
{int i = 1;while(i <= 100){printf("%d\n", i++);}return 0;
}
for写法:
#include <stdio.h>int main(void)
{for(int i = 1; i <= 100; i++){printf("%d\n", i); } return 0;
}
1882. 循环输出100~1之间的每个数(点击链接跳转)
while写法:
#include <stdio.h>int main(void)
{int i = 100;while(i >= 1){printf("%d\n", i--);}return 0;
}
for写法:
#include <stdio.h>int main(void)
{for(int i = 100; i >= 1; i--){printf("%d\n", i); } return 0;
}
1696. 请输出1~n之间所有的整数 (点击链接跳转)
while写法:
#include <stdio.h>int main(void)
{int i = 1, n;scanf("%d", &n);while(i <= n){printf("%d\n", i++);}return 0;
}
for写法:
#include <stdio.h>int main(void)
{int n;scanf("%d", &n);for(int i = 1; i <= n; i++){printf("%d\n", i); } return 0;
}
1697. 请输出n~1之间所有的整数 (点击链接跳转)
#include <stdio.h>int main(void)
{int i, n;scanf("%d", &n);i = n;while(i >= 1){printf("%d\n", i--);}return 0;
}
#include <stdio.h>int main(void)
{int n;scanf("%d", &n);for(int i = n; i >= 1; i--){printf("%d\n", i); } return 0;
}
1698. 请输出带有特殊尾数的数 (点击链接跳转)
#include <stdio.h>int main(void)
{int i = 1, n, g;scanf("%d", &n);while(i <= n){g = i % 10;if(g == 1 || g == 3 || g == 5 || g == 7){printf("%d\n", i); }i++;}return 0;
}
#include <stdio.h>int main(void)
{int n, g;scanf("%d", &n);for(int i = 1; i <= n; i++){g = i % 10;if(g == 1 || g == 3 || g == 5 || g == 7){printf("%d\n", i); }} return 0;
}
1699. 输出是2的倍数,但非3的倍数的数 (点击链接跳转)
#include <stdio.h>int main(void)
{int n;scanf("%d", &n);for(int i = 1; i <= n; i++){if(i % 2 == 0 && i % 3 != 0){printf("%d\n", i); }} return 0;
}
1700. 请输出所有的2位数中,含有数字2的整数 (点击链接跳转)
#include <stdio.h>int main(void)
{int s, g;for(int i = 10; i <= 99; i++){s = i / 10;g = i % 10;if(s == 2 || g == 2){printf("%d\n", i); }} return 0;
}
1701. 请输出所有的3位对称数 (点击链接跳转)
#include <stdio.h>int main(void)
{int s, g, b;for(int i = 100; i <= 999; i++){b = i / 100;g = i % 10;if(b == g){printf("%d\n", i); }} return 0;
}
1711. 输出满足条件的整数1 (点击链接跳转)
#include <stdio.h>int main(void)
{int s, g, sum;for(int i = 10; i <= 99; i++){s = i / 10;g = i % 10;sum = s + g;if(s > g && sum % 2 == 0){printf("%d\n", i); }} return 0;
}
1712. 输出满足条件的整数2 (点击链接跳转)
#include <stdio.h>int main(void)
{int s, g, b, sum;for(int i = 100; i <= 999; i++){b = i / 100;s = i / 10 % 10;g = i % 10;sum = s + g + b;if(b > s && s > g && sum % 2 == 0){printf("%d\n", i); }} return 0;
}
1713. 输出满足条件的整数3 (点击链接跳转)
#include <stdio.h>int main(void)
{int n;scanf("%d", &n);for(int i = 1; i <= n; i+=3){printf("%d\n", i); } return 0;
}
1714. 输出满足条件的整数4 (点击链接跳转)
#include <stdio.h>int main(void)
{int n, g, s, b;scanf("%d", &n);for(int i = 1; i <= n; i++){g = i % 10;s = i / 10 % 10;b = i / 100;if((g == 3 || g == 5 || s == 3 || s == 5 || b == 3 || b == 5) && i % 2 == 0){printf("%d\n", i); }} return 0;
}
1715. 输出满足条件的整数5 (