学习笔记——《C Prime Plus》
第6章 C控制语句:循环
- 6.1 再探 while 循环
- 6.1.1 程序注释
- 6.1.2 C风格读取循环
- 6.2 while 语句
- 6.2.1 终止 while 循环
- 6.2.2 何时终止循环
- 6.2.3 while:入口循环条件
- 6.2.4 语法要点
- 6.3 _Bool 类型
- 6.4 不确定循环和计数循环
- 6.5 for 循环
- 6.5.1 利用 for 的灵活性
- 6.6 其他运算符:+=、-=、*=、/=、%=
- 6.7 逗号运算符
- 6.8 出口条件循环:do while
- 6.9 如何选择循环
- 6.10 循环嵌套
- 6.10.1 程序分析
- 6.10.2 循环变式
- 6.11 数组简介
- 6.11.1 在 for 循环中使用数组
- 6.12 使用函数返回值的循环实例
- 6.12.1 程序分析
对于计算机科学而言,一门语言应该提供以下 3 种形式的程序流:
- 执行语句序列;
- 如果满足某些条件就重复执行语句序列(循环);
- 通过测试选择执行哪一个语句序列(分支)。
6.1 再探 while 循环
#include <stdio.h>
int main(void)
{long num;long sum = 0L; //把 sum 初始化为0,用long类型储存更大的数int status;printf("Please enter an integer to be summed");printf("(q to quit):");status = scanf("%ld", &num);while(status == 1){sum = sum + num;printf("Please enter next integer (q to quit):");status = scanf("%ld", &num);}printf("Those integers sum to %ld. \n", sum);return 0;
}
6.1.1 程序注释
首先:
根据测试条件 status == 1,只要 status 等于 1 ,循环就会重复。每次循环,num 的当前值都被加到 sum 上,这样 sum 的值始终是当前整数之和。当 status 的值不为 1 时,循环结束。
然后:
要让程序正常运行,每次循环都要获取 num 的一个新值,并重置 status。利用 scanf() 的两个不同的特性来完成。
- 使用 scanf() 读取 num 的一个新值;然后检查 scanf() 的返回值判断是否成功获取。如果 scanf() 成功读取一个整数,就把该数存入 num 并返回 1,随后返回值将被赋给 status(注意,用户输入的值储存在 num 中,不是 status 中)。这样做同时更新了 num 和 status的值,while 循环进入下一次迭代。
- 如果用户输入的不是数字(如,q ),scanf() 会读取失败并返回 0 。此时,status 的值就是 0 ,循环结束。
该程序利用 scanf() 的双重特性避免了在循环中交互输入时的这个棘手的问题。。例如,假设 scanf() 没有返回值,那么每次循环只会改变 num 的值。
该程序的结构:
把 sum 初始化为0
提示用户输入数据
读取用户输入的数据当输入的数据为整数时
{输入添加给 sum提示用户进行输入然后读取下一个输入
}输入完成后,打印 sum 的值
顺便一提,这叫伪代码(pseudocode),是一种用简单的句子表示程序思路的方法,它与计算机语言的形式相对应。
伪代码有助于设计程序的逻辑。确定程序的逻辑无误之后,再把伪代码翻译成实际的编程代码。使用伪代码的好处之一是,可以把注意力集中在程序的组织和逻辑上,不用再设计程序时还要分心如何用编程语言来表达自己的想法。
6.1.2 C风格读取循环
总之,因为 while 循环是入口条件循环,程序在进入循环体之前必须获取输入的数据并检查 status 的值, 所以在 while 前面要有一个 scanf() 。要让循环继续执行,在循环内需要一个读取数据的语句,这样程序才能获取下一个 status 的值,所以在 while 循环末尾还要有一个 scanf() ,它为下一次迭代做好了准备。
while 循环伪代码标准格式:
获取第 1 个用于测试的值
当测试为真时处理值获取下一个值
根据伪代码的设计思路,编写代码:
status = scanf("%ld", &num);
while(status == 1)
{//循环行为status = scanf("%ld", &num); //让循环继续执行,为下一次迭代
}
也可写为:
while(scanf("%ld", &num) == 1)
{//循环行为
}
6.2 while 语句
while 循环的通用形式如下:
while( expression )
{statement;
}
statement 部分可以是以分号结尾的简单语句,也可以是用花括号括起来的复合语句。
到目前为止,程序示例中的 expression 部分都使用关系表达式。也就是说,expression 是值之间的比较,可以使用任何表达式。如expression 为真(或者更一般地说,非零),执行 statement 部分一次,然后再次判断 expression。在 expression 为假(0)之前,循环的判断和执行一直重复进行。每次循环都被称为一次迭代(iteration)。
6.2.1 终止 while 循环
while 循环有一点非常重要:在构建 while 循环时,必须让测试表达式的值有变化,表达式最终要为假。否则,循环就不会终止。(可以使用 break 和 if 语句来终止循环)。
index = 1;
while (index < 5)
{printf("Good morning!\n");
}
上面程序段将打印无数次 Good morning!。因为循环中 index 的值一直都是原来的值 1,不曾改变。
6.2.2 何时终止循环
要明确一点:只有在对测试条件求值时,才决定是终止还是继续循环。
#include <stdio.h>
int main(void)
{int n = 5;while(n < 7) //第7行{ printf("n = %d\n", n);n++; //第10行 printf("Now n = %d\n", n); //第11行 }printf("The loop has finished.\n"); return 0;
}
运行结果:
在第 2 次循环时,变量 n 在第 10 行首次获得值 7 。但是,此时程序并未退出,它结束本次循环(第 11 行),并在对第 7 行的测试条件求值时才退出循环(变量 n 在第 1 次判断时为 5 ,第 2 次判断时为 6 )。
6.2.3 while:入口循环条件
while 循环是使用入口条件的有条件循环。所谓“有条件”指的是语句部分的执行取决于测试表达式描述的条件,如(index < 5)。该表达式是一个入口条件(entry condition),因为必须满足条件才能进入循环体。
6.2.4 语法要点
//糟糕的代码创建了一个无限循环
#include <stdio.h>
int main(void)
{int n = 0;while(n<3)printf("n is %d\n", n);n++;printf("That's all this program does\n'");return 0;}
运行结果:
屏幕上回一直输出以上内容,除非强行关闭这个程序。
虽然程序中缩进了 n++;这句话,但是并未把它和上一条语句括在花括号内。因此,只有直接跟在测试条件后面的一条语句是循环的一部分。变量 n 的值不会改变,条件 n < 3 一直为真。该循环体会一直打印 n is 0 ,这是一个无限循环(infinite loop),没有外部干涉不会退出。
切记:
即使 while 语句本身使用复合语句,在语句构成上,它也是一条单独的语句。该语句从while 开始执行,到第 1 个分号结束。在使用了复合语句的情况下,到右花括号结束。
6.3 _Bool 类型
在编程中,表示真或假的变量被称为布尔变量(Boolean variable),所以_Bool 是 C 语言中布尔变量的类型名,_Bool 类型的变量只能储存 1(真)或 0(假)。如果把其他非零数值赋值非 _Bool 类型的变量,该变量会被设置为 1 。这反映了 C 把所有的非零值都视为真。给布尔变量取一个能表示真或假值的变量名是一种常见的做法。
6.4 不确定循环和计数循环
一些 while 循环是不确定循环(indefinite loop)。所谓不确定循环,指在测试表达式为假之前,预先不知道要执行多少次循环。例如,开头的程序,通过与用户交互获得数据来计算整数之和,我们事先并不知道用户会输入什么整数。
另外,还有一类是计数循环(counting loop)。这类循环在执行循环之前就知道要重复执行多少次。
#include <stdio.h>
int main(void)
{const int NUMBER = 22;int count = 1; //初始化 while(count <= NUMBER) //测试 {printf("Be my Valentine!\n"); //行为count++; //更新计数 }return 0;
}
在创建一个重复执行固定次数的循环中涉及了3个行为:
- 必须初始化计数器;
- 计数器与有限的值作比较;
- 每次循环时递增计数器。
while 循环的测试条件执行比较,递增运算符执行递增,递增发生在循环的末尾,这可以防止不小心漏掉递增。但是计数器的初始化放在循环外,就有可能忘记初始化。实践告诉我们可能发生的事情终究会发生,所以,我们可以用另一种控制语句,可以避免这些问题—— for 循环。
6.5 for 循环
for 循环把上述 3 个行为(初始化、测试和更新)组合在一处。
for 语句是一种入口条件循环,即在执行循环之前就决定了是否执行虚幻。因此,for 循环可能一次都不执行。
#include <stdio.h>
int main(void)
{const int NUMBER = 22;int count;for(count=1; count<=NUMBER; count++){printf("Be my Valentine!\n");} return 0;
}
关键字 for 后面的圆括号中有 3 个表达式,分别用两个分号隔开。
- 第 1 个表达式是初始化,只会在 for 循环开始时执行一次。
- 第 2 个表达式是测试条件,在执行循环之前对表达式求值。如果表达式为假,就结束循环。
- 第 3 个表达式执行更新,在每次循环结束时求值。
完整的 for 循环还包括后面的简单语句或复合语句。for 圆括号中的表达式也叫做控制表达式。
打印整数 1 ~ 6 及其对应的立方。
#include <stdio.h>
int main(void)
{int num;printf(" n n'scubed\n");for(num=1; num<=6; num++){printf("%5d %5d\n",num ,num*num*num);}return 0;
}
运行结果:
for 循环的第 1 行包含了循环所需的所有信息:num 的初始值,num 的种植和每次循环 num 的增量。
注意:num 的终值不是 6 ,而是 7 。虽然最后一次循环打印的 num 的值是6, 但随后 num++ 使num 的值为 7,然后 num<=6 为假,for 循环结束。
6.5.1 利用 for 的灵活性
for 的灵活性源于如何使用 for 循环中的 3 个表达式。第 1 个表达式给计数器赋初值,第 2 个表达式表示计数器的范围,第 3 个表达式递增计数器。
此外,for 循环还有其他 9 种用法。
(1)可以使用递减运算符来递减计数器:
#include <stdio.h>
int main(void)
{int secs;for(secs = 5; secs > 0; secs--){printf("%d seconds!\n", secs);}printf("We have ignition!\n");return 0;
}
(2)可以让计数器递增 2、10 等:
#include <stdio.h>
int main(void)
{int n; for(n=2; n < 60; n = n + 13) //从2开始,每次递增13,在60内 printf("%d \n",n);return 0;
}
(3)可以用字符代替数字计数:
#include <stdio.h>
int main(void)
{char ch;for(ch = 'a'; ch <= 'z'; ch++)printf("The ASCII value for %c is %d.\n", ch, ch);return 0;
}
(4)处理测试迭代次数外,还可以测试其他条件:
for (num = 1; num <= 6; num++)
替换成:
for (num = 1; num*num*num <= 216; num++)
如果与控制次数相比,你更关心限制立方的大小,就可以使用这样的测试条件。
(5)可以让递增的量几何增长,而不是算术增长。也就是说,每次都乘上一个固定量:
#include <stdio.h>
int main(void)
{double debt;for (debt = 100.0; debt < 150; debt = debt * 1.1)printf("Your debt si now %.2lf.\n", debt);return 0;
}
(6)第 3 个表达式可以使用任何合法的表达式。无论是什么表达式,每次迭代都会更新该表达式的值:
#include <stdio.h>
int main(void)
{int x;int y = 55;for (x = 1; y <= 75; y= (++x * 5) + 50)printf("%10d %10d\n", x, y);return 0;
}
运行结果:
该循环打印 x 的值和表达式 ++x * 5 + 50 的值。
注意:测试涉及 y,而不是 x。for 循环中的 3 个表达式可以是不同的变量;虽然该例可以正常运行,但是编程风格不太好。
(7)可以省略一个或多个表达式(但是不能省略分号),只要在循环中包含能结束循环的语句即可:
#include <stdio.h>
int main(void)
{int ans, n;ans = 2;for (n = 3; ans <= 25;)ans = ans * n;printf("n = %d; ans = %d.\n", n, ans);return 0;
}
该循环保持 n 的值为 3 。变量 anx 开始的值为 2 ,然后递增到 6 和 18,最终是 54。
注意:省略第 2 个表达式会被视为真,程序会一直运行。
(8)第 1 个表达式不一定是给变量赋初值,也可以使用 printf() 。记住,在执行循环的其他部分之前,只对第 1 个表达式求值一次或执行一次。
#include <stdio.h>
int main(void)
{int num = 0;for (printf("Keep entering numbers!\n"); num != 6; )scanf("%d", &num);printf("That's the one I want!\n'");return 0;
}
程序打印第 1 行的句子一次,在用户输入 6 之前不断接受数字。
(9)循环体的行为可以改变循环头中的表达式:
for (n = 1; n < 10000; n = n + data)
如果程序经过几次迭代后发现 delta 太大或太小,循环中的 if 语句可以改变 delta 的大小。
6.6 其他运算符:+=、-=、*=、/=、%=
//一下两两一组写法相互等价:
socore += 20;
socore = socore + 20;dimes -= 2;
dimes = dimes - 2;bunnies *= 2;
bunnies = bunnies * 2;time /= 2.73;
time = time / 2.73;reduce %= 3;
reduce = reduce % 3;
6.7 逗号运算符
逗号运算符扩展了 for 循环的灵活性,以便在循环头中包含更多的表达式。
例如,打印一类邮件资费,邮资为首重 40 元/千克,续重20 元/千克:
#include <stdio.h>
int main(void)
{const int FIRST_OZ = 46;const int NEXT_OZ = 20;int ounces, cost; //ounces:盎司(重量单位)printf("ounces cost"); for(ounces = 1, cost = FIRST_OZ; ounces <= 16; ounces++, cost += NEXT_OZ)printf("%3d $%4.2f\n", ounces, cost/100.0);return 0;
}
该程序在初始化表达式和更新表达式中使用了逗号运算符。
初始化表达式中的逗号使 ounces 和 cost 都进行了初始化,更新表达式中的逗号每次都迭代 ounces 递增1、cost 递增 20(NEXT_Z 的值是20)。
逗号运算符并不局限于 for 循环中使用,但这是它最常使用的地方。
逗号运算符有两个其他性质:
- 首先,它保证了被它分隔的表达式从左往右求值(换言之,逗号使一个序列点,所以逗号左侧项的所有副作用都在程序执行逗号右侧之前发生)。因此,ounces 在 cost 之前被初始化。在该例中,顺序并不重要,但是如果 cost 的表达式中包含了 ounces 时,顺序就很重要。
- 其次,整个逗号表达式的值是右侧项的值。如:
x = ( y = 3, ( z = ++y + 2 ) + 5 );
先把 3 赋给 y,递增 y 为 4,然后把 4 加 2 之和 6 赋给 z,接着加上 5,最后把结果 11 赋给 x。
6.8 出口条件循环:do while
while 循环和 for 循环都是入口条件循环,即在循环的每次迭代之前检查测试条件,所以有可能根本不执行循环体中的内容。
C 语言还有出口条件循环(exit-condition loop),即在循环的每次迭代之后检查测试条件,这保证了至少执行循环体中的内容一次。
#include <stdio.h>
int main(void)
{const int secret_code = 13;int code_entered;do{printf("To enter the triskaidekaphobia therapy club,\n");printf("please enter the secret code number:");scanf("%d", &code_entered);}while (code_entered != secret_code);printf("Congratulations! You are cured!\n");return 0;
}
在用户输入 13 之前不断提示用户输入数字。
也可使用 while 循环语句(入口条件):
#include <stdio.h>
int main(void)
{const int secret_code = 13;int code_entered;printf("To enter the triskaidekaphobia therapy club,\n");printf("please enter the secret code number:");scanf("%d", &code_entered);while (code_entered != secret_code){printf("To enter the triskaidekaphobia therapy club,\n");printf("please enter the secret code number:");scanf("%d", &code_entered);}printf("Congratulations! You are cured!\n");return 0;
}
需满足循环条件,才会进入循环;所有在 while 循环前面先实现一遍循环体的内容。
do while 循环的通用形式:
do
{statement;
}while( expression );
statement 可以是一条简单语句或复合语句。do while 循环以分号结尾。
do while 循环在执行完循环体后才执行测试条件,所以至少执行循环体一次;而 for 循环或 while 循环都是在执行循环体之前先执行测试条件。
do while 循环使用于那些至少迭代一次的循环。例如,下面是一个包含 do while 循环的密码程序伪代码:
do
{提示用户输入密码读取用户输入的密码
}while (用户输入的密码不等于正确密码);
6.9 如何选择循环
首先,确定是入口条件循环还是出口条件循环。
通常,入口条件循环用得比较多,原因是:
- 一般原则是在执行循环之前测试条件比较好。
- 测试放在循环的开头,程序的可读性更高。
- 在许多应用中,要求在一开始不满足测试条件时就直接跳过整个循环。
一般而言,当循环涉及初始化和更新变量时,用 for 循环比较合适,而在其他情况下用 while 循环更好。
对于下面这种条件,用 while 循环就很好:
while (scanf("%ld", &num) == 1)
对于涉及索引计数的循环,用 for 循环更合适:
for (count = 1; count <= 100; count++)
6.10 循环嵌套
循环嵌套(nested loop)指在一个循环内包含另一个循环。嵌套循环常用于按行和列显示数据,也就是说,一个循环处理一行中的所有列,另一个循环处理所有的行。
#include <stdio.h>
#define ROWS 6
#define CHARS 10int main(void)
{int row;char ch;//内层循环一行打印 10 个字符,外层循环创建 6 行for(row = 0; row < ROWS; row++) //第10行 { for(ch = 'A'; ch < ('A'+ CHARS); ch++) //第12行{printf("%c", ch);}printf("\n");}return 0;
}
运行结果:
6.10.1 程序分析
(1)代码第 10 行开始的 for 循环被称为外层循环(outer loop),第 12 行开始的 for 循环被称为内层循环(inner loop)。
(2)外层循环从 row 为 0 开始循环, 到 row 为 6 时结束;因此,外层循环要执行 6 次,row 的值从 0 变为 5。
(3)每次迭代要执行的第 1 条语句是内层的 for 循环,该循环要执行 10 次,在同一行打印字符 A~J;第二句是外层循环的 printf("\n");
(4)嵌套循环中的内层循环在每次外层循环迭代是都要执行完所有的循环;比如:此代码中的内层循环每次都要执行 10 次才结束,然后执行 printf("\n"); 然后再进行外层循环的下一次循环。
6.10.2 循环变式
可以通过外层循环控制内层循环,在每次外层循环迭代时内层循环完成不同的任务。比如:内层循环开始打印的字符取决于外层循环的迭代次数。
#include <stdio.h>
int main(void)
{const int ROWS = 6;const int CHARS = 6; //用const关键字代替#define int row;char ch;//依赖外部循环的嵌套循环 for(row = 0; row < ROWS; row++){for(ch = ('A' + row); ch <('A'+ CHARS); ch++){printf("%c", ch);}printf("\n");}return 0;
}
运行结果:
6.11 数组简介
数组(array)是按顺序储存的一系列类型相同的值,如 10 个 char 类型的字符或 15 个 int 类型的值,整个数组有一个数组名,通过整数下标访问数组中单独的项或元素(element)。
float debts[20];
debts[5] = 32.54;
debts[6] = 1.2e+21;
声明 debts 是一个内含 20 个元素的数组,每个元素都可以储存 float 类型的值。数组的第 1 个元素是 debts[0],第 2 个元素是 debts[1],以此类推,直到 debts[19]。
注意,数组元素的编号从 0 开始,而不是从 1 开始。
把值读入指定的元素中:
scanf("%f", &debts[4]); //把一个值读入数组的第5个元素
用于识别数组元素的数字被称为下标(subscript)、索引(indice)、或偏移量(offset)。下标必须是整数,而且要从 0 开始计数。数组的元素被依次储存在内存中相邻的位置。
6.11.1 在 for 循环中使用数组
该程序读取 10 个高尔夫分数,用 for 循环来读取数据。
程序打印总分、平均分、差点(handicap,是平均分与标准分的差值)
//使用循环处理数组
#include <stdio.h>
#define SIZE 10
#define PAR 72int main(void)
{int index, score[SIZE];int sum = 0;float average;printf("Enter %d golf scores:\n", SIZE);for(index = 0; index < SIZE; index++){scanf("%d", &score[index]); //取出 10 个分数 }printf("The scores read in are as follows:\n");for(index = 0; index < SIZE; index++){printf("%5d", score[index]); //验证输入 }printf("\n"); for(index = 0; index < SIZE; index++){sum += score[index]; //求总分数 }average = (float)sum/SIZE; //求平均分printf("Sum of scores = %d, average = %.2f\n", sum, average);printf("That's a handicap of %.0f.\n", average - PAR);return 0;
}
运行结果:
(1)首先,注意程序实例虽然打印了 11 个数字,但是只读入了 10 个数字,因为循环只读了 10 个值。
(2)由于scanf() 会跳过空白字符,所以可以在一行输入 10 个数字,也可以每行只输入一个数字,或者像本例这样混合使用空格和换行符隔开每个数字(因为输入时缓冲的,只有当用户键入 Enter 键后数字才会被发送给程序)。
要读取 int 类型变量 fue,应该写成:
scanf("%d", &fue);
要读取 int 类型的元素 score[index],应该写成:
scanf("%d",&score[index]);
较好的编程风格:
(1)使用 #define 指令创建的明示常量(SIZE)来指定数组的大小。如果以后要扩展程序处理 20 个分数,只需简单地把 SIZE 重新定义为 20 即可,不用逐一修改程序中使用了数组大小的每一处。
(2)使用 3 个独立的 for 循环,遵循了模块化(modularity)的原则。模块化的思想是:应该把程序划分为一些独立的单元,每个单元执行一个任务。这样做提高了程序的可读性,也方便后续更新或修改程序。
6.12 使用函数返回值的循环实例
编写一个有返回值的函数,要完成以下内容:
- 定义函数时,确定函数的返回类型;
- 使用关键字 return 表明待返回的值。
例如:
double power(double n, int p) //返回一个 double 类型的值
{double pow = 1;int i;for (i = 1; i<=p; i++)pow *= n;return pow; //返回 pow 的值
}
要声明函数的返回类型,在函数名前写出类型即可,就像声明一个变量那样。
关键字 return 表明该函数将把它后面的值返回给主调函数。
返回值也可以是表达式的值:
return 2 * x + b;
//计算数的整数幂
#include <stdio.h>
double power(double n, int p); //ANSI 函数原型
int main(void)
{double x, xpow;int exp;printf("Enter a number and the positive integer power");printf(" to which\nthe number will be raised. Enter q");printf(" to quit.\n");while (scanf("%lf%d", &x, &exp) == 2){xpow = power(x, exp); //函数调用printf("%.3g to the power %d is %.5g\n", x, exp, xpow);printf("Enter next pair of numbers or q to quit.\n");}printf("Hope you enjoyed this power trip -- bye!\n");return 0;
}double power(double n, int p) //函数定义
{double pow = 1;int i;for(i = 1; i<=p; i++){pow *= n;}return pow;}
运行结果
6.12.1 程序分析
main()
实例中的 main() 是一个驱动程序(driver),即被设计用来测试函数的小程序。
while 循环
(1)输入1.2 12,scanf() 成功读取两值,并返回2,循环继续。因为 scanf() 跳过空白,多页可以多行输入。
(2)输入 q 会使 scanf() 的返回值为 0,因为 q 与 scanf() 中的转换说明 %lf 不匹配。scanf() 将返回 0,循环结束。类似地,输入 2.8 q 会使 scanf() 的返回值为 1,循环也会结束。
power() 函数
(1)第一次出现:double power(double n, int p); //ANSI函数原型
这是 power() 函数的原型,它声明程序将使用一个名为 power() 的函数。开头的关键字 double 表明 power() 函数返回一个 double 类型的值。编译器要知道 power() 函数返回值的类型,才能知道有多少字节的数据,以及如何解释它们,这就是为什么必须声明函数的原因。圆括号中的 double n, int p 表示power() 函数的两个参数。第 1 个参数应该是 double 类型的值,第 2 个参数应该是 int 类型的值。
----------------------------------------------------------------------------------------------------------------------------
(2)第二次出现:xpow = power(x, exp); //函数调用
程序调用 power() 函数,把两个值传递给它。该函数计算 x 的 exp 次幂,并把计算结果返回给主调函数。在主调函数中,返回值将被赋给变量 xpow。
----------------------------------------------------------------------------------------------------------------------------
(3)第三次出现:double power(double n, int p) //函数定义
power() 函数有两个形参,一个是 double 类型,一个是 int 类型,分别由变量 n 和变量 p 表示。
注意:函数定义的末尾没有分号,而函数原型的末尾有分号。
power() 函数用 for 循环计算 n 的 p 次幂,并把计算结果赋给 pow,然后返回 pow 的值,return pow;