5.1 造句:当……就……

5.1.1 语法要素

关键字:while

用法:while(表达式)语句

执行过程:

(1)求表达式的值

(2)如果表达式的值不为0,执行语句,然后转至(0)再次求表达式的值;如果表达式的值为0,while语句结束。

总的来看,while语句表达的意思用自然语言来说就是:当表达式的值不为0时就执行语句,否则while语句结束。

while语句的流程图如图5-1所示。

5.1 造句:当……就…… - 图1

图5-1 while语句的流程图

循环语句中的语句部分也叫循环体(Loop body),由于这部分可能是由多个语句组成的一个复合语句,而初学者往往容易忘记写复合语句的{}而造成逻辑上的错误,因此在不影响语意的前提下,本书一律把while语句写成下面形式:

5.1 造句:当……就…… - 图2

这种写法可以使初学者避免许多不必要的麻烦,而一旦日后彻底掌握了while语句,选择风格就完全是个人的一种自由了。

while语句长于不定数循环,但也可以做定数循环。在描述循环次数一定的循环时,通常要一个整数类型的变量用来记录已经循环了多少次。术语把这个变量叫做循环变量或“记数器”,计数器一定要有个初始值,这个值一般是0。然后在循环体内的语句每执行一次就加1。下面是一个示意性的演示。

程序代码5-1

5.1 造句:当……就…… - 图3

代码jsq += 1;中的“+=”也是C语言的一个运算符,表示的含义是求运算符两边表达式的和的值(类型与jsq相同),副效应是把这个值写到运算符左面表达式所代表的内存中。效果上和jsq = jsq + 1是一样的,但毫无疑问写起来更为简洁。这个运算符和赋值运算的优先级、结合性一样,且遵守同样的类型转换规则。类似的运算符还有-=、*=、/=、%=,这些运算统称为复合赋值(Compound Assignment)运算。

输出为:

5.1 造句:当……就…… - 图4

计数器的初始值和每次循环的增量并不是绝对的为0和1,这要看具体问题,视如何表达解决问题的方法更方便。

使用循环语句容易出错的地方在于写循环条件,不少初学者经常轻率地把“>”写成“>=”或犯类似的错误,往往使循环次数多一次或少一次。更严重的问题是,有些错误的循环条件会使循环永远进行,这种情况叫无限循环或死循环,多数情况下应该绝对避免。一旦在编译器中运行代码时出现了死循环,可以试试按[Ctrl-C]或[Ctrl-Break]组合键,看能否把程序停下来。

练习

下面代码的输出结果是什么?

5.1 造句:当……就…… - 图5

5.1.2 猴子吃桃问题更简洁的写法

程序代码3-5中的许多语句是反复出现的,使用while语句可以把这段代码写得更为简洁。

程序代码5-2

5.1 造句:当……就…… - 图6

程序运行结果:

5.1 造句:当……就…… - 图7

程序执行至while语句时,由于djt的初值为5,表达式djt>0的值为1,故执行{}内各语句,之后djt的值变成了4。再次求表达式djt>0的值,依然为1。所以再次执行{}内各语句……最后,当djt的值变为0时,再求表达式djt>0的值,为0,while语句结束。

练习

用while语句重写3.6.3后面练习的代码。

5.1.3 错误的循环变量

例题:求1.00007-2.00007+3.00007+……-10.00007的值。

程序代码5-3

5.1 造句:当……就…… - 图8

这段代码中值得学习的一个技巧是通过循环语句中的“fh = -fh;”实现交替改变数列中各项的符号。然而它的运行结果却是:

5.1 造句:当……就…… - 图9

很显然结果是错误的。错误的根本原因在于对于数据类型的认识不够清楚,具体体现在fds <= 10.00007。由于fds是double类型,在许多情况下只能近似地表示一个带小数点的实数,因此“<=”这个运算在许多情况下也不会得到精确的结果。

结论:在定数循环中,作为计数器的变量一般用整数类型。请自己重新编写程序代码5-3并改正其中的错误。

5.1.4 次数不定的循环

题目:编程,从键盘上输入若干整数,要求程序统计整数的个数并计算它们的平均值。

分析:编程的前提是我们自己能够用纸笔解决这个问题。我们自己解决这个问题的过程可能如下。

得到一个整数,记录个数(1),求和

得到一个整数,记录个数(2),求和

……

(报数停止)

这个过程明显是一个不断重复,然后在某种条件下停止的过程。而且这是一个事先(编写代码时)不知道需要循环多少次的典型的不定数循环。

报数可以通过调用scanf()函数模拟,“重复”可以用循环语句描述,问题的难点在于循环终止条件的设计。

唯一可能的突破口在scanf()函数调用。由于函数调用是一个表达式,而这个表达式的值是把键盘输入的字符序列成功地转变成内存中的变量值的个数。由于“%d”格式要求键盘键入的字符序列必须符合十进制整数格式,这样当在键盘输入的字符序列不符合十进制整数格式要求的情况下,由于无法把键盘输入的字符序列转化为整数类型的数据,对scanf()的函数调用表达式的值将为0。举例来说,假如有:

5.1 造句:当……就…… - 图10

在调用scanf("%d", &s)时,如果在键盘上键入“123[CR]”,那么scanf("%d", &s)的值为1。如果在键盘键入“a[CR]”,那么由于这个a字符序列不符合十进制整数格式要求,无法把a这个字符序列转换成一个int类型的数据,更谈不上把值写入s内存,scanf("%d", &s)的值将为0。这样,由于scanf("%d", &s)本身也是一个有值的表达式,显然可以写在while语句的()之内。下面是程序代码。

程序代码5-4

5.1 造句:当……就…… - 图11

下面是程序的测试,首先预备一组数据“123t”(t是作为非法的%d输入表示输入结束的),输出结果应该是“共3个整数,平均值为2.000000”。

5.1 造句:当……就…… - 图12

与事先预测不同的是,平均值的结果。这是由于he/gs是一个int类型,与输出格式%f的要求不匹配的缘故。这是初学者很容易犯的一个错误。正确的写法应该是,把he/gs改写为(double)he/(double)gs。改写后程序运行的结果是:

5.1 造句:当……就…… - 图13

与预期结果一致。

然而只有一组测试的数据,说服力是很成问题的。在测试程序时应该尽量考虑到多种可能,至少要考虑极端的可能性。在本程序中不输入任何数据(程序开始运行时就直接输入一个非整数字符)或只输入一个数据的情况应该进行测试。测试结果表明,输入0整数时,程序的运行是有问题的。

5.1 造句:当……就…… - 图14

请自行解决这个bug。

5.1.5 逗号表达式及其应用

题目同前小节,但希望在每次从键盘输入数据之前程序能显示一行提示信息:“请输入一个整数,输入整数之外的任一字符表示结束”。也就是说希望程序应该表现出下面的行为:

5.1 造句:当……就…… - 图15

很明显,在每次调用scanf()函数读去数据之前应该调用printf()函数输出相应的提示信息。然而令人尴尬的是,如果把printf()函数调用写在while语句之前的话,提示信息不会循环输出,如果把printf()函数调用写在while语句的循环体内的话,那么在第一次调用scanf()数之前不会输出给用户的提示信息。一种比较蹩脚的做法(不过蹩脚总比没实现好)是下面的写法:

5.1 造句:当……就…… - 图16

可以证实,这种写法能实现对程序功能的要求。然而在代码中写两句完全相同的、重复的语句,对于稍微专业一点的程序员来说通常是难以容忍的。这样的写法实在是太不优雅了。

问题的矛盾体现在while语句的()内只能写表达式不能写语句。逗号运算符可以摆脱这种尴尬:

5.1 造句:当……就…… - 图17

不难注意到,printf()函数调用表达式和scanf()函数调用表达式之间有一个“,”,这是一个运算符,其运算过程是首先求printf()这个函数调用表达式的值,然后求scanf()这个函数调用表.达式的值,而最后()内的表达式的值是scanf()函数调用表达式的值。这保证了表达式的值和前小节中代码的写法一致,因而依然可以使用前面的方式控制循环的继续或停止,同时实现了printf()函数调用和scanf()函数调用同步循环。

逗号运算符(Comma operator)是C语言中优先级最低的运算符(优先级为1),它是一个二元运算符,结合性为从左到右。其语法格式是:

表达式1,表达式2

逗号表达式的求值必须按照次序进行,首先求表达式1的值,求出的这个值被放弃,然后再求表达式2的值,这个值就是逗号表达式的值,这个值的类型也是表达式2的类型,这里可能存在隐式类型转换(例如short或char转换为int),但并不存在算术运算中那种二元类型转换问题。举例来说:

5.1 造句:当……就…… - 图18

的值是97而不是97.0。

有多个“,”的表达式可以根据结合性分析,例如:

5.1 造句:当……就…… - 图19

由于逗号表达式结合性是从左到右,所以第一个“,”的运算对象是ele2,第二个“,”的运算对象是(e1, e2)e3。也就是说前一表达式等价于:

5.1 造句:当……就…… - 图20

又由于逗号的序点性质,求el的值一定是首先进行的,这个值被抛弃。然后求e2的值。表达式(e1, e2)的值为e2的值。同理,((e1, e2), e3)值最后为e3的值。不难看出表达式ele2e3的求值过程是:

el的值,放弃;

e2的值,放弃;

e3的值,得到了整个表达式的值。

逗号运算符和复合语句中的{}在功能上有一定程度的类似。复合语句:

5.1 造句:当……就…… - 图21

其中的{}把顺序执行的语句1语句2语句3;合成了一条语句。而逗号运算符:

5.1 造句:当……就…… - 图22

其中的“,”把顺序求值el、e2、e3合成了一个表达式。这也恰恰是其用途——在只能写表达式的地方完成一系列的顺序求值,哪怕求void值也可以。这不能不让人惊叹赞赏C语言的设计者对程序设计语言深刻、全面而独到的理解。在我们还没有写代码的时候他们就已经知道我们可能需要什么了。

但是,并非所有的“,”都是逗号运算符。一个例外就是,在函数调用时()内部的“,”不是逗号运算符,函数调用时()内部的“,”是分隔开各个表达式的分隔符,对这一点请务必始终保持清醒,否则函数调用就乱套了。紧接着而产生的问题是在函数调用时被分隔符分隔的表达式可不可以是个逗号表达式,答案是可以的。但是这样就产生了一个“安能辨我是雌雄”的悖论——函数调用f(1, 2, 3)中()内部的,究竟哪个是逗号运算符哪个是分隔符呢?C语言规定函数调用()内部被分隔符“,”分隔开的表达式如果是逗号表达式应该写成初级表达式,也就是用()括起来。如f(1, (2, 3)),这样就不存在二义性的矛盾了。但说实话,我实在想不出什么样的程序中会有f(1, (2, 3))这样变态的需要。所以还是老话,C语言给了你充分的自由,但不要滥用这种自由,应该用得恰倒好处才好。

练习

编写一统计班级学习成绩(求出最高分、最低分、平均分)的程序。班级人数事先不知道,输入一小于0或大于100的分数时表示输入结束(提示:while(scanf("%d", &fs), fs<=100&&fs >= 0))。