新概念c语言案例教程__何勤

新概念c语言案例教程__何勤

ID:82889629

大小:580.81 KB

页数:199页

时间:2023-09-24

上传者:灯火阑珊2019
新概念c语言案例教程__何勤_第1页
新概念c语言案例教程__何勤_第2页
新概念c语言案例教程__何勤_第3页
新概念c语言案例教程__何勤_第4页
新概念c语言案例教程__何勤_第5页
新概念c语言案例教程__何勤_第6页
新概念c语言案例教程__何勤_第7页
新概念c语言案例教程__何勤_第8页
新概念c语言案例教程__何勤_第9页
新概念c语言案例教程__何勤_第10页
资源描述:

《新概念c语言案例教程__何勤》由会员上传分享,免费在线阅读,更多相关内容在行业资料-天天文库

新概念C程序设计教程上册版权所有

1第一章C语言的基本概念1.1c语言程序的构成成分:例题1.11)ttinclude/・编译预处理命令,将标准输入输出函数2)作为头文件包扩到用户源程序文件中・/3)^include/・编译预处理命令,将系统提供的数学函数4)作为头文件包扩到用户源文件中・/5)intmain()〃函数名为main的主函数的函数首部6){〃函数体开始7)floatx,y:〃定义部分.定义变量及其类型8)x=45.2;〃语句部分.赋值语句9)y=sqrt(3*x+5.6);〃语句部分.赋值语句10)printf("%f

2”,y);〃语句部分.输出语句11)return0J〃/语句部分.返回语句12)}〃函数体结束注意:最左边的行号是为了说明方便而加上的,实际C语言程序中是不能有的。1.C语言的源程序的主要构成成分是函数定义(从第5到第12行)。ー个C语言源程序文件一般由多个函数定义顺序组成(直到第5章オ讨论由两个以上函数构成的C语言源程序),但是其中必须有一个main函数。程序的运行都是从系统调用main函数开始的。2.函数定义由函数首部(第5行)和函数体(第6到第12行)组成。函数体必须用大括号“ド和“}”括住。3.C语言源程序的次要构成成分是:编译预处理命令(第1、3两行的左边部分)、注释(每一行右半部以〃开始的内容,或以/・开始,以・/结束的内容)和声明(见第5章)。编译预处理命令和注释是不以分号结束的。4.C语言程序中,(任何函数的)函数体的主要构成成分是:定义序列(第7行的左半部)和语句序列(第8行到第11行的左半部)。这两种成分都要以分号结束(复合语句除外)。C语言函数体中,所有定义必须位于语句序列之前(・注)。・注:这是C89标准,其它高级编程语言不一定有此先后顺序要求。C99标准放宽了要求,可以在复合语句的前半部定义仅在复合语句中有效的局部变量,但目前不少编译器并不支持C99标准。所以,为了获得较好的程序可移植性,本书的论述以较老的C89标准为主。深入ー步:定义序列被编译程序翻译成了内存中的数据序列(包括对存放数据的内存空间的事先安排),而语句序列则被翻译成了指示如何加工这些数据序列的机器语言的指令序列。将要运行的程序的指令序列也是位于内存中的。所以在运行某个程序前,程序用户必须借助于操作系统将位于外存文件中的这两部分内容加载到内存中。*5.在正式进行编译之前,编译预处理程序将根据源程序中的编译预处理命令対源程序文件

3进行一些辅助性的文本插入(include命令)、替换(#define命令)和编辑工作。编译预处理命令都是以“#”开始,不以分号结束。每条编译预处理命令必须书写在同一行上。6.注释以“/*”开始,以“ウ”结束。注释会被编译程序首先删除掉。但良好的注释使程序更易被人们读懂。不要忘记书写注释的结束符号“*/"。注释有两种常用方式:一ー种是注释内容独自占据多行,对注释以下的一段程序或整个源程序文件进行说明;另一种是出现在一行的右边,对该行左边的内容进行说明。(注释部分在VC++6.0中是以绿色字体显示出来的。)单行注释以“〃”开始,它用起来比较方便,但它不是C语言标准规定的。1.2C语言程序的语法要素1.正文部分:除了格式控制串(见1.4节printf()和scanf()函数)和注释部分以外的C语言源程序的其它部分,称为C语言的源程序的正文部分。(正文部分在VC++6.0中是以黑色字体显示出来的。)2.C语言的字符集,26个大小写英文字母:A——Z,a——z;阿拉伯数字字符:〇——9;特殊字符:!#%,&*_+=ー〜く>丨/ヽ’":.,()[]{}?:空格字符(对应键盘上的最长键Space)换行符(对应Enter键)制表符(对应Tab键)以上所有这些字符构成了C语言的字符集。书写C语言的源程序的正文部分,只能使用C语言字符集中的字符,其它任何字符不得使用。字符集中的任何字符,只能用英文半角方式来进行输入。3.标识符是以26个大小写英文字母、阿拉伯数字。到9和下划线构成的字符序列;其中从左边开始的第一个字符不能是数字。在C语言中,只能使用符合标识符规定的名字,来对变量、符号常量、函数、语句标号命名。C语言对标识符的大小写敏感,也就是说main与Main,printf与Printf都是不同的标识符(main和printf都是C语言中的已经规定的函数名,所以,我们不能用来作为自定义的标识符)。4.关键字是被编译程序保留下来的特殊标识符;主要分为两大类:用于定义(或声明)的关键字和用于构成语句的关键字。(C89标准中的)关键字都是由小写英文字母组成的。关键字(还包括预处理明令)在VC++6.0中是以蓝色字体显示出来的。所有关键字列表如下:在ANSIC89标准中共有32个关键字autobreakcasecharconstcontinuedefaultdodoubleelseenumextxrnfloatforgotoifintlongregisterreturnshortsignedsizeofstaticstructswitchtypedefunionunsignedvoidvolatilewhile在ANSIC99标准中增加了5个关键字Bool_Complex_Imaginaryinlinerestrict5.C语言源程序中可使用的分隔符有三个:空格、回车/换行、逗号。同类项之间要用逗号分隔,关键字和标识符之间要用空格或回车/换行来分隔,不能用逗号(比如intm,n;)。在C语言中,合理地使用多余的空格和空行,可以使得程序更清晰、易懂、优美。6.C语言中常量分为数值常量(又称为字面常量)和符号常量两种。应当多使用符号常量,尽量少用数值常量。・最常用的数值常量有以下儿种:1),整型常量567,-425,0等,是没有小数分量的数值。2).实型(浮点型)常量:

4由正负号、数字、小数点构成。0.543、543.、543.0、0.0都是十进制的小数形式。在数的左边还可以加上正负号。比如•543.0。3),字符型常量匕,,‘K''85,'

5,等,是用单引号括起来的ASCH码表中的字符,包括键盘上的可显示(或可打印)的字符和不可显示的ー些控制字符(字符‘

6’就是表示ー个控制字符——Enter键,详见附录ASCH码表)。深入ー步:字符型量的本质字符常量在编写程序时,要用两个单引号括住它。它属于ー种从表面上看来是非数值型的量.但其实在计算机的内存中,它就是以ASCH码的形式存储的,占用ー个字节内存空间的一个二进制正整数(取值在〇一一127之间).字符型量是用计算机进行文字处理的基础。・符号常量一般由大写英文字母组成的标识符构成,用#define来进行定义,比如:#defincPI3.1416。源程序中所有出现的记号PI都会被编译预处理程序用3.1416替代。修改常量的值变得非常方便,程序的可读性也更好。7.变量定义变量的作用:要求系统为存放(其数值可以变化的)数据事先分配内存单元,变量名作为内存单元的得号展・。定义变量的形式是:〈类型名〉く变局列表〉;定义变量只能使用标识符。多个变量之间用逗号隔开,构成变量列表。C语言中各种常用的变量类型是int(整型)、float(单精度浮点型)、char(字符型)、double(双精度浮点型)、long(长整型)。通过变量名score,可以找到(即映射为)相应的内存单元地址。假设地址为2000,语句:seore=38.0;的含义和作用是:将数值38.0存放在地址为2000的内存单元中。变量的值是可以变化的。而变量名所对应的内存地址是不变的。8.变量一定要先进行定义,定义后一定要进行初始化(即存入一个初始值在变量对应的内存单元中,参见P页),然后才能在表达式中使用或进行输出。否则,变量所对应的内存单元中的坟圾藝理就会在程序中(的表达式或输出语句)被误用。9.运算符是用来告诉编译程序对运算量要做何种运算的抽象形式。由编译程序转化成具体的运算类指令。最常用的运算符分为三大类:算术运算符、关系运算符(见下一章)、逻辑运算符(见下一章)。各种算术运算符见下表表2.x各种算术运算符运算符运算举例结果+正号+88-负号-a(a的值为3)-3+加11+516-减11-56*乘法11*555/除法11/52(这是整数除法)/除法11.0/52.2(这是实数除法)%取模(取整除的余数)11%51(11除以5的余数)乘法运算符最容易漏写(比如:将2*x*y,误写为2xy),不要把实数除法运算符误用为整数除法(比如:错把1.0/3写成1/3。1/3的值是〇)。

77.所谓表达式,是用运算符将运算量(常量、变量、函数调用)连接起来的有意义的式子。其中还可使用圆括号来改变运算符固有的先后运算顺序——圆括号内的部分优先进行运算。单个常量、变量和函数调用是最筒単的表达式。在初学编程时,表达式极易出现多种书写错误。在命令型高级语言中,实际上只能用表达式和语句这种抽象的、形式化的方式,来命令计算机为你进行数据处理工作(由编译程序转化为ー系列的取数指令和运算指令)。单个常量、单个变量和单个函数调用是最简单的表达式(比如x,可以看成是表达式x+0)。深入一步:普通的(指没有副作用的,见本章提高部分)表达式中出现变量的含义表达式x+y中,出现变量的含义是:根据变量名(这对应着ー个内存单元的地址)到内存中去取该变量的值(取到CPU的某个寄存器中去),然后(在运算器中)进行指定的运算.该变量的值依然不变。8.赋值语句的形式是:<变量>=<お达式>:。赋值语句先计算出赋值号“=”右边表达式的值,然后将此值存放在赋值号左边的变量(所对应的内存单元)之中。注意:赋值号的左边不能是表达式,只能是单个变量。赋值语句中变量值的变与不变:“左边改变,右边不变”赋值号右边表达式中的变量(如果有的话)仅仅是取出它的值来参与表达式规定的运算,变量的值仍然未变.而赋值号左边变量(所对应的内存单元中)的原来值,将被表达式算出的新值存入而“覆盖“掉。9.形式如・二包含方至すx的ス港父,•(x可以是任何简单类型的变量)的赋值语句,大量的出现在程序中。它表示的是ー种邙住去不:即指明了如何由变量x的ー个老值(取出来参与表达式所规定的运算),最终得到了变量x的ー个新值(最终存入到赋值号右边的变量x中)。专题讨论:库函数和库函数调用•库函数:编译程序(又称为编译器)附带的,ー批可以被任何别的C语言程序“调用”的,通用的、命名了的子程序。所有这些库函数组成了一个标准函数库,与编译程序一道打包发行。安装了C编译程序后,我们就能在编写程序时,调用这些库函数.・函数调用(functioncal1):在我们编写的C语言源程序中,可以调用库函数为我们的程序做ー些复杂的(通常是辅助性的)运算和处理工作,函数的调用方式为:库函数名(参数列息)其中参数列表中的参数如果多于ー个,参数之间就要用逗号隔开.比如要求出5.6的4次方是多少,就要调用库函数pow(5.6,4);将2开平方,就要调用库函数:sqrt(2)。调用库函数时千万不要忘记包含相应的头文件。参数可以是常量、变量、表达式。参数必须要用圆括号括住。比加pow(x,3.7)、sin(alfa*3.1416/180);C语言中的各种常用库函数的调用方法说明,请见附录。编程时调用了库函数,这些事先编译好的、命名了的子程序,就可以通过“编译”步骤之后的“链接”这ー步骤,“链入”到你所编写的程序之中,共同组成一个可执行的机器语言程序(可执行程序文件的扩展名是.exe).这样就可以大大减少编写程序的工作量。以下仅列出常用的数学库函数(要包含头文件math,h):sin(x)x为弧度,正铉函数

8cos(x)X为弧度,余铉函数exp(x)ex以e为底的指数函数log(x)logcX以e为底的对数函数loglO(x)logioX以10为底的对数函数fabs(x)X求X的绝对值函数fmod(x,y)整除x/y的余数floor(x)求不大于X的最大整数pow(x,y)xy幕函数,sqrt(x)xI/2开平方函数1.3数据输出与输入:1.3.1数据(变量或表达式的值)输出可调用格式化输出库函数:printf〇函数。调用的一般形式为printf(参数1,参数2,参数3,……,参数n)其中参数1是格式控制串。参数2,参数3,……,参数n是输出项列表例题1.2#ncludemain()(intnum;floatx;num=123:x=34.678printf(*%d%f”,num,x):}程序运行结果为:12334.678000格式控制串是用双引号括起来的字符串。%d是第一个输出项num的格式说明,以有符号的卜进制形式输出整数(正数不输出符号)%f是第二个输出项x的格式说明,以小数形式输出单、双精度实数,默认输出6位小数显然,%d是控制整数num的,%f是控制实数x的。为了清晰起见,格式控制串中除了格式说明之外,还可以有按照原样输出的普通字符例如:printf(“num=%dx=%f”,num,x):输出结果为:num=123x=34.678000如果num是车牌号,而x是车速的话,则以下的语句更为优秀:printf("车牌为%d号,车速是%f千米/小时、n”,num,x);输出结果为:车牌为123号,车速是34.678000千米/小时在格式控制串中,还可以有换行符号'

9’(这是ー种转义字符)例:printf("num=%d

10x=%f'',num,x);输出结果为:

11num=123x=34.678000域宽和精度%m.nf:对于实数,要想改变默认的小数点后面显示6位。可改为%,nf例如:printf("x=%.2f”,x);输出结果为:x=34.68//4舍5入printf("x=%7.2f”,x);输出结果为:x=34.68说明:34.68左边多出两个空格,整个变量x的输出域宽(包括小数点)占了7位。如果指定域宽小于输出项的实际宽度,则指定的域宽不起作用。L4.2数据(变量)输入,可调用格式化输出库函数:scanf()函数格式化输入库函数scanf的调用,可以使得程序运行暂停下来,等待用户的输入,程序用户可以通过键盘输入一个数值到变量(所对应的内存单元)中。不过在函数调用时,第一个参数是相应的占位符%d,%f或%c,它们与要输入的变量类型的对应关系如下表:占位符输入变量的类型%dint%ffloat%cchar库函数scanf()的调用通过键盘输入ー个变量值的最简单形式是:scan或"占位符",&变量名);变量左边的符号"&“是ー个取地址符号。这个取地址符号,在scanf()函数调用时,初学者最容易漏写。注意:占位符中的转换说明一定要与后面存储输入数据的变量类型相匹配。深入ー步:格式化库输入函数scanf()的工作原理简述等待程序用户从键盘上榆入字符串,在用户按下回车键后,按照占位符中的转换说明,对输入字符串进行数值数制转换,变成二进制的整数、实数或ASCH码,然后将此二进制的整数、实数或ASCII码,存入到由后ー个参数(即“&变量”)指定了地址的变量所对应的内存单元中。不必感到吃惊,你在PC机或笔记本电脑的键盘上的任何多次(或一次)击键产生的榆入,系统其实都是当作一串(或ー个)字符来对待的。功能强大、复杂而又容易用错的格式化输入库函数scanf()的比较详细的讲解请参见本章提高部分。

121.5用计算机求解问题的一般步骤:到目前为止,我们学了不少有关c语言的基础语法知识。但是在进行编程工作时,其实只有变量、表达式、赋值语句、输入数据到变量和输出变量或表达式的结果,这些要素オ是我们需要重点考虑的。其中的变量,成为我们编程解决实际问题时的核心和主线。要考虑的关键问题是:需要定义哪些变量;需要输入哪些己知的变量;如何根据己知的变量构造出合适的表达式,从而用赋值语句来求出未知变量的值;最后将求出的变量(或表达式)的值输出。用计算机求解问题,解决问题的一般过程是;1.用普通语言简要并尽可能精确地叙述问题;2.问题中已知的量有几个,其中有几个量随求解的具体应用场合会发生变化?(这些量应当定义为变量)有哪几个不会发生变化(这些量可用数值常量或符号常量来表示)?其中会变化的已知量一般应当在程序中用到此数据前,用输入函数调用(有的高级语言用输入语句)进行数据输入。3.问题中要求出的有几个量?4.从已知的量如何得到要求出的量?有何公式可以利用?有何方程式可以利用?如果是公式,就可以直接将其转换为赋值语句,只需把公式右边的数学表达式转换为高级语言的算术表达式即可。如果是方程式,则需你自己亲自将方程式求解得到最后的公式,然后将其转换为赋值语句,只有到了这ー步,オ可以将工作交给计算机做。5.如果从已知的到要求的最终结果需要一些中间变量,则需要在程序中说明这些中间变量,并且得到怎么从已知的量到中间变量的值的公式,最终山已知量,中间变量得到所要求的结果的公式,将所有这些公式转换为赋值语句。6.将结果输出。其中最为困难的往往是第4步和第5步。这两步通常又被称为寻找求解问题的算法。1.6逐步求精的伪代码表示算法:表示问题求解的算法有多种方法,其中最为常用的是伪代码和流程图。但由于流程图只适合解决规模较小的、比较简单的问题。虽然初学者易于理解和掌握,但在程序员和编程高手中并不流行ーー因为它不利于对算法的构思、修改和调整(仅适用于比较清晰地表达算法)。所以木书不作介绍(养成了用流程图来表示算法的习惯后,人们很难再转变成用其它更好的方式去构造算法,所以还不如一步到位,采用伪代码)。本书使用的是用逐步求精的伪代码来表示算法。所谓伪代码没有严格的规范(所以也比较灵活),但其中有一些结构要素,可以用ー些比较规范的方式来表述对问题的计算和处理流程。ー级算法只是解决问题的ー个轮廓。有些复杂问题,只根据ー级算法还难以直接写出C语言的源程序。这时可对ー级算法进ー步求精(称为二级求精),将它其中的某些步骤,扩展成更详细的步骤,...直到可以很容易写出c语言程序的语句时为止。对于某些人是很显然的算法描述,对于其他人可能并不显而易见。因此,算法求精的过程和步骤是因人而异的。你的编程经验越丰富,算法求精的步骤就可能越少。不过,算法求精的步骤太少也不一定是好事。因为程序员的ー个良好习惯,就是把伪代码表示的ー级算法(最多到二级求精),转变为源程序中的注释。注释太过简洁,会加大别人和自己阅读程序的困难。学习体会:学习用伪代码来表达自己的编程思路和算法,笔者的体会是没有什么捷径让你能够迅速

13掌握它,开始只能是去模仿,去领悟.时间长了,看的、模仿的、写的算法多了,慢慢就会了。类似于学游泳和学骑自行车.在大量地模仿和大量地实践过程中,你就会不知不觉地掌握它。第1讲大小写字符之间的转换编写ー个程序,运行时接受用户输入的小写英文字母,程序将其转换为大写英文字母并显示在屏幕上。类型必修题趣味性・难度・分析:此程序主要是要考虑如何由输入的任意ー个小写的英文字母,得到所对应的大写英文字母。查看ASCII码表,发现所有小写英文字母的ASCII码值都比相应的大写字母ASCII码值大32。即存在以下等式关系:大写英文字母的ASCII码=小写英文字母的ASCI!码ー32于是编写C语言程序如下:1#include2main()3{4charchi,ch2;5printf(“请键入ー个小写英文字母并按回车键

14”);6scanfC*%c”,&chl);7ch2=chl-32;8printf(“相对应的大写英文字母为枇、n”,ch2);9)程序说明:在C语言中,字符变量其实就是占有一个字节内存的ー种小整型变量,它的取值一般就是在0——127之间的整数值,对应着标准ASCI!表上的128个字符。细节问题请参考本章提高部分:对字符变量的进ー步说明。问题1:是否可以将第4行与第6行互相调换?答:不可以。在C语言中,定义(序列)必须放在语句(序列)的前面。问题2:是否可以漏掉第4行到第8行中的任意一行最后面的分号?答:不可以。在C语言中,任何定义或语句,都必须以分号作为结束符。只有一个例外一ー那就是复合语句,参见下一章。问题3:如果将第8行改写为:printf(“相对应的大写英文字母为うd

15”,ch2);程序将会出现什么问题?答:这时输出的是该字符所对应的ASCII码(其实也是字符变量在内存中的真正值转换成的十进制数)。

16第2讲求三个数的和及平均值类型必修题趣味性・难度・变量安排:3个已知的数要用3个变量al,a2,a3:三个数的和用1个变量sum,平均值用1个变量ave,所有变量都为实型变量。ー级算法:1.输入这三个数al,a2,a3;2.求这三个数的的和sum及平均值ave;3.输出sum,ave;其中第二步由于不能直接转化成语句而需要进一步求精根据求和及求平均值的公式sum=al+a2+a3ave=sum4-3只需将其转化为赋值语句即可第2步的二级求精:2.1sum=al+a2+a3;/*求这三个数的和2.2ave=sum/3;/*求这三个数的平均值转化成C语言程序:/・求三个数的和及平均值・/1#include2intmain()3(4floatal,a2,a3;/・三个已知数,说明为实型变量・/5floatsum,ave;/・两个未知数,说明为实型变量・/6/・输入这三个数al,a2,a3*/7printf(“请输入3个数,3个数之间用空格隔开

17");8scanR"%f%f%ド,&al,&a2,&a3);910/・求这三个数的的和sum及平均值ave*/11sum=al+a2+a3;12ave=sum/3;1314/・输出sum,ave*/15printfi(ttsum=%f,ave=%f,,sum,ave);16return0;17}

18注意:运行此程序时,要用键盘输入三个数值给变量al,a2,a3,三个数之间要用空格隔开。输完后,按下回车键,程序オ会继续运行下去(其中的道理参见9.5节)。ヽ点评将算法的伪代码,转化为程序中的注释:注意,伪代码形式的ー级算法,在程序中转变为注释,使得程序的可读性比较好。这是・种良好的编程习惯。问题:如果将第8行改为scanfi[ll%f;%f,%f',&al,&a2,&a3);数据应当如何输入?答:输入的数据之间现在不能用逗号隔开,而要用逗号隔开了。练习:请将程序第7行删掉再编译运行,感受一下程序是否不友好了?第3讲已知三角形的两边夹角,求对边长度及面积类型必修题趣味性・难度・ー级算法1.输入三角形的两边x,y及夹角alfa2.求三角形对边长度—length3.求三角形面积ーarea4.输出三角形的对边长度及面积其中第2、第3步需要进ー步求精C语言的库函数中,有一批数学库函数,其中包含正弦函数sin()及余弦函数cos()(注意,库函数名都是小写的)。不过对这些库函数的调用,首先要包含头文件:#include〇其次,这两个三角函数所要用到的角度是以弧度作为单位的,而不是度,因此,如果输入的角度是以度为单位的,还必须转化位弧度为单位的角度。然后,才能利用公式来求三角形的对边长度和面积。sqrt函数,是用来求一个数的平方根的数学库函数。length=sqrt(x*x+y*y-2*x*y*cos(alfa*PI/180))area=x*y*sin(alfa*180/3.1416)*1/2转化成C语言的程序1#include2#include3#definePI3.14164intmain()5(6floatx,y,alfa;7floatlength,area;

1912/・输入三角形的两边x,y及夹角alfa*/3printf(“请输入三角形的两边及夹角

20");4scanR"%f%f%F',&x,&y,&alfa);56/・求三角形对边长度・/7length=sqrt(x*x+y*y-2*x*y*cos(alfa*PI/l80));89/・求三角形面积・/10area=x*y*sin(alfa*PI/l80)/2;1112/・输出三角形的对边长度及面积・/2021printf("三角形的对边长为%f,面积为%f

21”,length,area);2223return0;24}问题1:第2行是否可以不要?第不行,只要调用库函数中的数学函数,必须包含此头文件。小结:对变量进行初始化的三种方式1.在变量定义时,立即给它ー个初值(比如inti=0;);2.用赋值语句,将表达式计算出来的值,赋给变量;i=0;3.采用输入函数,将数据由外部(键盘或文件等输入设备)存入变量所代表的内存中(比如scanH"%d",&i);):〇任何变量,在定义后,你都必须采用上述三种方式之一,对其进行初始化。没有进行初始化的变量,是决对不能出现在表达式中的。第4讲将一个三位整数反向后输出类型必修题趣味性・难度**ー级算法1.输入ー个三位整数-»n2.通过利用学过的算术运算符整除、和取余数%进行分解,分别求出此三位整数n的百位数n3,十位数n2和个位数nl3.反向后的三位整数为num=n1*100+n2*10+n34.输出此三位数其中第2步由于不能立即写出C语言的语句,需要进ー步求精:5.1求出n的百位数:n3=n/100

221.2求出n的十位数n2=(n-n3*100)/102.3求出n的个位数nl=n%10算法验证:假设将一个任意指定的三位整数比如315fn(即存入变量口中),用于验证上述算法过程的正确性;由2.1可得到百位数n3的值为:315/100即等于3:由2.2可得到十位数n2的取值为:(315-3X100)/10等于1;由2.3可得到个位数nl的取值为:315%10等于5。再通过算法的第3步,反向后的值num为:n1*100+n2*10+n3=5*100+1*10(H3=5130最终值num确实等于513〇算法验证是正确的(验证算法的其他重要方法请看第讲洗扑克牌算法P页)。转化为C语言程序:1#include2intmain()3(4intn;/・输入变量・/5intn3,n2,nl;/・中间变量・/6intnum;/・结果变量・/7/・输入ー个三位整数n*/8printf(“请输入ー个三位整数、n");9scanfT%d'‘,&n);1011/・分别求出它的百位数n3,十位数n2,个位数nl*/12n3=n/100;13n2=(n—n3*100)/10;14nl=n%10;1516/・反向后的三位整数为・/17num=n1*100+n2*10+n3;1819/・输出此三位数・/20printfC'num=%d”,num);21return0;22}问题1:第8行起什么作用?是否可以删掉这句?答,这一句起到提示的作用。可以删掉,但程序对使用者不友好(不好用),使得用户要面对黑屏进行数据输入,很容易出现输入错误。问题2:是否可以将第17行和第20句合并为下面这一句并删掉第6行?printf("num=%d",nl*l00+n2*10+n3);答:可以。printflf)函数调用时,格式串后的输出项参数,既可以是常量或变量,也可以是ー个表达式。直接输出值时可以不定义变量,直接将表达式(的计算结果)作为输出参数。问题3:是否可以用赋值语句n=315,取代输入变量n的第9句scanf("%d",&n);?答:可以,赋值语句n=315,和输入函数调用语句scanfT%d”,&n);都可以使变量n能够得到初始化(即得到有用的初值)。但用前者,程序失去了灵活性,它只能将一个固定的三位整数315反向输出。如果想要将别的三位整数反向输出,则需要编程者重新修改程序。输入函数调用语句scanf("%d”

23,&n),则可以使得程序用户输入任何的三位整数。只需将程序重新运行ー遍即可,不需要修改程序。问题4:将第12行和第13行颠倒次序是否可以?答:不可以,变量n3会出现初始化错误。因为颠倒次序以后,赋值语句n2=(n-n3*100)/10;中,的表达式:(n-n3*100)/10中的变量n和n3,是要到相应变量的内存空间中去取出数值来,参与表达式所给定的运算的。变量n的值没有问题,但变量n3因为没有得到初始化,其内存中是垃圾数据。也就是说,13行的赋值语句:n2=(n—n3*100)/10J的执行,依赖于12行的赋值语句:n3=n/100;的执行,变量n3的值オ得以初始化。问题5:将13行与14行互换是否可以?答:可以,因为两条赋值语句之间的变量并没有依赖关系。问题6:第10条语句是否可改为printf("num=%d”,&num);答:不可以,这样打印出来的一般是变量num的内存地址,而不是在这个地址的存储单元中存放的(变量的)数据。也就是说,如果变量num的内存地址是123456,而变量num的值是546,那么,此句运行后,得到的是:num=123456这显然是错的。问题7:是否可以将所有变量n,n3,n2,nl都用实型float来定义?答:不可以;程序12行、13行的赋值语句的表达式,都要求是整除运算。14行的n%10,运算符%也要求变量n是整型,而不是实型。问题8:是否可以将4,5,6行,用一行来表示?答:可以。但不好,最好是将已知变量(要初始化的)、结果变量(求出的结果)和中间(临时)变量分开写,这样有利于检查程序:已知变量是否初始化了,结果变量是否输出了。习题1.将一个两位整数反向输出;习题2.将一个4位整数反向输出;第5讲鸡兔同笼问题类型必修题趣味性**难度・题目,已知鸡和兔的总头数和总脚数,求鸡有多少只,兔有多少只?ー级算法1.输入总头数heads,总脚数fbet2.求鸡的只数chicken和兔的只数rabbit3.输出变量chicken和rabbit的值其中只有第二步需要求精

24根据二元・次方程组可知四个变量heads,feet,chicken,rabbit之间的关系为heads=chicken+rabbitfeet=2Xchicken+4Xrabbit解此方程组可得到如下两个公式:chicken=(4Xheads-feet)/2rabbit=(feet-2Xheads)/2将此公式转化为赋值语句,考虑到输入的总头数和总脚数不一定能得到整数解,因此将变量chicken(表示鸡的只数),rabbit(表示兔的只数),定义为实型变量。二级求精2.1chicken=(4*heads-feet)/2.02.2rabbit=(feet-2*heads)/2.0转化为C语言程序1#include2intmain()3(4intheads,feet;5doublechicken,rabbit;6/・输入总头数heads,总脚数feet*/78printfT请输入总头数和总脚数,两数间用空格隔开

25");9scanft佻d%^',&heads,&feet);1011/・求鸡的只数chicken和兔的只数rabbit*/1213chicken=(4.0*heads-feet)/2.0;14rabbit=(feet-2.0*heads)/2.0;15/・输出变量chicken和rabbit的值1617printfi[“鸡的只数%f只,兔的只数%f只'',chicken,rabbit);18return0;19)问题1:如果不解方程组,而直接将其转变为赋值语句,也就是用heads=chicken+rabbit;feet=2*chicken+4*rabbit;取代第5和第6句,结果会如何?答:赋值语句右边的表达式中出现的变量,必须是已经是经过初始化的变量(参见P页)。赋值语句是用赋值号右边的表达式求出的值,存放到赋值号左边的变量表示的存储单元中。所以,这两句是不正确的,但大多编译程序不会报错。程序运行时会用内存存储单元中不确定的变量chicken,和rabbit的值(垃圾数据),经过运算得到两个错误数值,去改变变量heads和feet的原来值。换言之,方程式不能直接转变为赋值语句,只有公式オ可以直接转变为赋值语句(公式是等号左边为ー个要求值的变量,等号右边是ー个数学代数式,数学代数式中出现的变量的值都是已知的)。'点评:所谓计算机的智能

26此题程序一旦编好,由另一个用户来使用计算机运行这个程序,他会觉得计算机很聪明,竟然会求解鸡兔同笼的方程式,得到正确答案。“深蓝”计算机为什么能够战胜人类的国际象棋世界冠军卡斯帕罗夫,其道理是类似的。人类的智能转变成了程序存存放在了计算机中,与计算机的强大计算能力(普通pc机目前每秒可运算几亿次)、“记忆能力”(存储容量巨大)结合起来,使得运行着程序的计算机让外行看起来非常聪明能干。1.3ー个小型C程序的开发过程一个小型c程序的整个开发过程包括如下几个步骤:1.分析问题并进行构思,安排所需变量和符号常量:2.用逐步求精的伪代码或流程图构造算法(并验证算法,参见第讲);3.(用编辑软件)编辑输入源程序,并保存在扩展名为.c的文件中;(参见附录A)4.进行编译预处理;(参见&2节)5.用编译器进行编译;(在集成开发环境下,第4和第5步是一次性完成的)6.根据编译错误和警告回到第3步修改语法错误;7.编译无错后,生程目标文件(扩展名为.obj);8.将用户程序与库函数的程序进行连接,并生成可执行的程序(扩展名为.exe);9.运行程序,发现链接或运行时错误则回到第3步修改;10.结束编程过程,保存并保管好无错误的源程序文件(II后进行软件维护这是必须的);11.将可执行程序交给用户使用。

272.13用C语言编写科学计算类小型程序的一般常用格式(在只有・个main函数的情况下):#include/・头文件包含部分・/#definePI3.1416/・符号常量宏定义部分・/intmain()/・主函数首部・/{/・定义序列部分・/int整型变量列表;float单精度浮点型变量列表;char字符型变量列表;/・变量初始化部分,主要是输入库函数的调用语句,在程序运行时从键盘上得到由用户输入的变量的初值,或者由赋值语句给变量赋初值・/printfp");〃输入的提示信息scanfT”,地址列表);/・计算、处理流程部分,用来得到中间变量和最终结果的值,由计算型的赋值语句和流程控制型语句(选择结构语句和循环结构语句)构成・//・程序结果输出部分,主要是输出函数的调用,将变量的值输出在显示屏上,*/printf();return0;/・返回到操作系统・/说明:程序输出部分,完全可以与其他语句混合在一起,变量的值的输入也可以出现在程序的说明序列后的任何部分,这里只是列出ー个一般的科学计算类程序的编程格式,读者在后面的编程题中可以看到,不少非科学计算类的程序(比如游戏程序,管理程序),与此格式有不小区别。此处列出来仅仅是给初学者提供ー个基本编程格式的框架。只有定义序列部分是必须放在函数体的最前面的。提高部分:(建议初学者快速浏览一遍即可,编程需要用到时再来査阅。)二、变量类型本书的第二章中,简单变量类型只使用了•整型int,・单精度浮点型float,

28•字符型char这3种类型。这是最基本的最常用的(不过单精度浮点型使用的频度并不如双精度浮点型double)。但还有很多由基本类型派生出的类型。在使用变量和常量的类型时有一个原则,那就是:只要基本类型够用,就不用其它的派生类型。其实在C89标准(ANSIC即美国在1989年颁布的C语言标准,在!999年还出了一个新版的标准,称为C99标准)中,C语言中基本数据类型不止上述3种,还有双精度浮点型即double型和无值型void。这些类型构成了其它几种派生类型的基础。双精度浮点型变量,在VC++6.0中,占用8个字节的内存空间。注意:对于双精度浮点型变量,在调用格式化输入函数scanf()输入该类型变量的值时,必须使用的占位(格式)符是%Lf或%lf(在%与£之间的字符是英文字符L的小写,而不是数字1。所以,最好不要用小写英文字母1,而用大写的L),不能用%f格式。但是,在调用格式化输出函数printR)输出双精度变量的值时,却可以使用%f格式。问题:如果变量x的类型是double型的,请问1、输入此变量时是否可用占位符%f?2、输入此变量时是否可用占位符%Lf?3、输出此变量时是否可用占位符%f?4、输出此变量时是否可用占位符%Lf?除了字符型在任何情况下都是占用一个字节的内存空间外,对其它类型的变量,ANSIC89标准只规定了每种数据类型的最小取值范围。在应用中如有必要,则应当查找相关手册,确定变量的真正取值范围。在不同编译环境ド,变量的取值范围会有不同。void类型既可以用来表示无返回值的函数,又可生成适于各种元素的指针。基本类型的修饰符:除void类型外,基本类型之前都可以加各种修饰符。修饰符改变基本类型的含义,以更加精确的适合特定需要。修饰符有如下几种:signed(有符号)unsigned(无符号)long(长型)short(短型)有符号和无符号整数间的区别在于,计算机怎样解释二进制位串形式的整数的最高位(即最左边的一位)。如果定义ー个有符号整数,则C编译程序生成的代码(即机器指令)

29认为该数最髙位是符号标志:最高位是0,表示正数,最高位是1,则表示的是负数。(参见后面)如果用这些修饰符来修饰整型int,int本身可以省略不写。例如signed等价于signedintlong等价于!ongintunsigned等价于unsignedintshort等价于shortint不过定义signedint这种类型是多余的,因为int型本身就是默认为有符号的。对大多数程序而言,常常使用有符号数,但有符号整数的最大值,只有无符号数的一半。例如如果系统用两个字节来存放整数,那么int型的最大值是32767。而unsigned型的整数最大值是65535。至于什么时候要用修饰符,还是那个简单原则:可不用时就不用。问题:十进制32767用2进制来表示是多少?十进制65535用2进制来表示是多少?・浮点型变量的有效位:由程序运行得到的结果,有多少位是有效的?单精度浮点型变量的取值有效位一般是7位。超出7位的值是无效的。问题:ー个很大的实型数,如果加上1个数值比较小的数,结果会如何?答:比较小的那个数,有可能被忽略不计了。如果哪个比较大的单精度浮点数,其值超出了7位的话。比如12345678.9+7.8,这个加法对于使用单精度变量来编程计算,就变得毫无意义(因为前ー个数的最后一位很可能是无效的)。如果你想要得到精度比较高的实数运算结果,可以采用双精度实型double来说明变量。在C语言中,双精度变量取值的有效位一般可确保十进制的13位(如果两个双精度的浮点数相加,此结论仍然有效,但对于乘法运算后的结果,有效位将大大减少)。正因如此,所以在银行等金融行业,我们一般不能用浮点型变量去进行利息等的计算,哪怕是双精度浮点型。因为帐目最终可能不平衡。解决的办法是使用长整型变量。长整型变量的取值范围如果还不够大,则采用纯数字构成的字符串来解决问题(参见本书后面相关的讨论)。2.常量类型本书的基本语法要素中,常量只使用了・整型常量567,-425・实型常量567.17,・字符型常量V,‘、。,,‘8,等•字符串常量“567","hello!”等.这些类型。

30这是最基本的最常用的。在使用变量和常量的类型时也有一个原则,那就是:只要基本类型够用,就不用其它的类型。・除基本整型外,整型常量还包括:长整型,要以字母L或1结尾如:123456789L;无符号整型常量,以u或U结尾如12345U,无符号长整型123456789UL。此外,除了十进制常量外,在C语言中还可以使用八进制、十六进制形式的整型常量。ハ进制数以〇开始,比如054,表示的是8进制数(54)8而不是(54)io;十六进制数以Ox开始,比如0x54A7,表示的是十六进制数(54A7)16(请参见第讲,将十进制数转化为任意进制数)。・浮点常量的两种机外表现形式:高级语言中所谓的浮点数,指的是ー种小数点位置可以移动的实数(与之相对应的是:早期计算机所采用的定点数所表示的实数。但定点数表示法存在ー个很大的问题:对于绝对值极大的数或极小的数,其数字的有效位都远离固定小数点附近;而采用定点数表示法,计算机表示ー个数的有限存储单元・般只能表示在小数点附近左右的有效数字位)。浮点常量的两种机外(即在程序中的)表现形式是:(1)十进制小数形式:由正负号、数字、小数点构成。〇.543、543.、543.0、0.0都是十进制的小数形式。在其左边还可以加上正负号。比如•543.0。(2)指数形式:如543e2或543E2都代表543X10、注意:e(或E)表示以!0为底的暴;字母e(或者E)的左边必须有数字,e(或者E)右边的指数必须是整数。ー个浮点数,其指数表现形式可以有多种。比如543e2和5.43e4,但表示的其实都是同一个实数;但我们把小数点前只有一位数(5.43e4)的这种表示方式,称为‘‘规范化的指数形式”。两种机外表现形式的浮点数,在编译(或输入)时都被编译程序(或标准输入库函数)转变为浮点数的机内表现形式(参见提高部分第11点)。输出时则正好相反。2.类型间的转换:不像有些高级语言(比如Pasca!语言),不同类型的数据之间进行转换受到了严格限制。在C语言中,不同类型间的数据都是可以相互转换的。・在混合类型的算术表达式中的类型转换:表达式中的类型转换,包括自动转换和强制转换两种:自动转换:自动转换发生在不同数据类型的量混合运算时,由编译系统自动完成。自动转换遵循以下规则:1.若参与运算量的类型不同,则先转换成同一类型,然后进行运算。2.转换按内存存储数据的长度(即字节数)增加的方向进行,以保证精度不降低。如int型和long型数据进行运算时,先把int型量转成!ong型后再进行运算。3.所有的浮点运算都是以双精度形式进行运算的,即使仅含float单精度量运算的表达式,也要先转换成double型,再作运算。4.char型和short型参与运算时,必须先转换成int型。

31强制类型转换如果自动类型转换满足不了你的要求,则你可以要求编译程序进行强制类型转换。强制类型转换是通过类型转换运算来实现的。其一般形式为:(类型说明符)(表达式)其功能是把表达式的运算结果强制转换成类型说明符所规定的类型。例如:(float)m把整型变量m强制转换为单精度实型。(int)(3*x-y)把表达式3*x-y的结果强制转换为整型。在使用强制转换时应注意以ド问题:1.类型说明符和表达式都必须加括号(单个变量可以不加括号),如把(int)(x-y)写成(in)x-y则成了仅仅把x转换成int型之后再与y相加了。注意:无论是强制转换或是自动转换,都只是为了本次运算的需要而对变量的取值进行的临时性的转换,而不改变变量说明时对该变量定义的类型,也不会改变变量相对应内存中所存放的数值。•赋值语句中的类型转换:在ー个赋值语句中类型转换的规则非常简单:先计算出来表达式的值(如果表达式中本身有类型转换,按照上述表达式中类型转换规定进行),然后再按照赋值号左边的变量类型进行转换。右边量的数据类型长度(该类型数据的字节数)如果比左边变量的数据类型长度长时,将会丢失一部分数据,这样会降低数值的精度。(有关赋值语句中的类型转换的细节问题,请参见《C程序设计》谭浩强著,清华大学出版社2005年第三版P59——62页,该书这ー点阐述得很清楚,本书不再赘述)。・练习:请查看别的C程序设计教材(书末参考文献前4本),大概了解一下,本书所没有讲到的格式化输入输出函数调用的细节问题。问题4:以下赋值语句分别执行后,赋值号左边变量的值是多少?假设ch是字符型,初值是字符‘*'、x是实型,初值是23.5、n是整型,初值是34:1.ch='a'+6;2.n=2*x+n-12.45-ch;3.x=n*ch;4.赋值表达式的概念:赋值语句去掉分号,就成了赋值表达式。它也完成了与赋值语句ー样的运算操作。不同点在于,赋值表达式是本身具有一个计算值的,这个值就等于存放在赋值号左边变量中的值。例如:赋值表达式n=3.6如果变量n为整型,则n的值为3,赋值表达式本身的值也是3。我们自己也可以验证ー下这个说法是否正确:#includeintmain()intn;

32printfT%d”,n=3.6);}运行此程序,打印出的结果确实为3。1.自增与自减运算符:升与一++是自加1运算符,使得变量的值加1;ーー是自减1运算符,使得变量的值减1。它们只能作用于变量上,不能作用在常量和表达式上。在i++,i-,++i,・・i独自成为一条语句的情况下,iや、Hi;都相当于i=i+l;i・・;、-i;都相当于i=i一1;但如果i++或iーー出现在表达式或别的语句中,读者要注意:printfC'a=%d”,a++);等价于以下两句:printfTa=%d”,a);a=a+l;printfC4a=%d,,,a-);等价于以下两句:printf(ua=%d,\a);a=a-1;printf(4ta=%d,,,-H-a);等价于以下两句:a=a+l;prin氓“a=%d”,a);printf(ua=%dM,-a);等价于以下两句:a=a-l;printfC'a=%d",a);也就是说:++或一出现在某变量的左边,那么就要先改变该变量的值,然后再用变量的新值去做语句指定的其他事(比如参与表达式的计算或输出);如果:-H-或ー出现在某变量的右边,那就是先要用该变量的老值去做语句指定的其他事,然后再改变该变量的值(加1或减Do问题.执行以下语句后,变量m、n的最终值是多少?(Dm=4;n=--m;(2)m=4;n=m";

33、点评:关于自增和自减运算符自加运算符X++;虽然与赋值语句x=x+l:等价,但执行速度却要快ー些。原因在于X++的执行中所加的1,不必到内存中去取,而赋值语句x=x+l;中的1却需要通过总线到内存中的常量区去取。CPU执行一条不需访问内存的指令,比执行一条需要访问内存的指令大约要快10倍左右。对自减运算符也是如此。所以,在循环中,尤其是在内层循环中,能够使用自加自减运算符的,就不要用对应的赋值语句。注意:自增自减运算符使用会带来很多意想不到的问题,初学者要慎用——最好单独使用,而不要用它与其他运算符来构成具有副作用的表达式(见下面的讨论)。・表达式的副作用:在ー个表达式(赋值表达式除外)中,如果不出现++、ー运算符,以及不出现具有副作用的函数调用(参见第6章函数),则称此表达式为没有副作用的表达式。没有副作用的表达式,在计算出结果后,并不会改变表达式中变量的原来取值,提倡使用没有副作用的表达式(细节问题请参见谭浩强〈C程序设计〉P58-P59页)。问题1:j的初值为5,有以下表达式(j--)+(j-)请问该表达式的值是多少?答:这个式子是具有副作用的表达式,在不同的编译环境下,得到的结果可能不一样。ー些系统把5作为表达式中所有j的值,表达式的结果是10,j的最终值是3;而另ー些系统按从左到右计算,即第一个(j-)表示参与加法运算的是5,然后j立即自减1,这样第二个参与加法运算的(j-)的值就是4了。表达式的计算结果为9〇j的最终值仍然是3。6.逗号运算符:逗号运算符在形式上就是ー个或多个逗号,它是C语言里优先级最低的运算符(逗号运算符也采用从左向右的结合方式),其形式是:表达式1,表达式2,表达式3在对逗号表达式求值时,首先求出表达式1的值,然后再去求表达式2的值,再求表达式3的值。以最右边的表达式(这里是表达式3)的值作为整个逗号表达式的值。表达式的个数并不限于2个或3个,可以是任意多个。逗号表达式主要用在循环语句for语句的头部的表达式1(循环前的变量初始化)和表达式3(循环变量的更新)中。在其他地方尽量不要用,因为容易出现误用。例如fbr(sum=0,i=l;i<=100;i++)sum=sum+i;是在for循环的表达式一中使用逗号表达式的例子(参见第4章)。7.多重赋值:比如:变量1=变量2=变量3=表达式;(1)

34上式相当于:变量1=(变量2=(变量3=表达式));(2)所起的作用是为多个变量赋予同一个数值(表达式计算出来的值)。6.复合赋值运算符:有一种特殊形式的赋值表达式,称为复合赋值表达式。一般形式如下:变量二元运算符=表达式(1)这种表达式其实是ー种普通赋值表达式的简写形式。即与(1)式等价的普通赋值表达式是:变量=变量二元运算符(表达式)(2)可以这样使用的二元运算符有两大类:算术运算符、位运算符。即+=-=*=/=%=等习题:求以下复合赋值语句的值:设x=14.6,y=3.71.x/=y-3;提示:此赋值语句等价于:x=x/(y-3);而不是x=x/y-3;问题:y*=2*x+5.3;等价于什么赋值语句?答:等价于y=y*(2*x+5.3);7.运算符的优先级与结合性:各类运算符都有明确的优先级。但表达式中同一优先级的运算,到底哪ー个运算先做呢?记得在学算术和代数时,老师所教的吗?是从左到右,例如6-2+13,相当于(6-2)+13,即运算是从左到右来进行的,在C语言中,大多数运算符基本上也是类似的从左到右进行的。不过,在C语言中,还有少量一元(或称单目)运算符是遵循从右到左的优先顺序的:例如x=y=z=3.6i这是由一个多重赋值的表达式构成的语句,这个语句并不等同于:((x=y)=z)=3.6;(此式不正确)而是等同于:x=(y=(z=3.6));也就是说,对出现在同一表达式中的多个赋值运算符,其运顺序是从右向左的。3.6先被赋给了z,整个赋值表达式z=3.6的值也是3.6,此值又被赋给了y,…。因此,运算符的缙令性实际上也是一种对运算符的优先顺序的规定,只不过是针对属于同・优先级的运算符而言的。关于全部运算符的优先级与结合性,请参见附录D10.有关输入输出函数的更多问题:•对于字符的输入输出,除了可以用格式化输入输出库函数外,还可以使用以下几个库函数:输出单个ASCH字符的库函数putchar()

35用法举例1:putchar(kT);/・这会导致将字符d在屏幕的当前光标位置显示出来・/用法举例2:putchar(ch);/・这会导致将字符变量ch所取的字符在屏幕上显示・/用法举例3:putchar(''ガ);/・此句运行相当于按下了“enter”键,起的是“回车换行”的作用(“回车换行”这个术语是从电传打字机的运行借用过来的,早期的计算机不使用屏幕显示输出信息,而是用电传打字机)。输入单个字符到某个字符变量的库函数getchar"用法举例:ch=getchar();程序运行到此句时,程序会被系统暂时中断(中断概念的解说可见本书9.4节),等待程序用户从键盘输入ー个字符。如果您在运行此程序时敲击了字符键,h、并随即按下回车键“enter”,则小写字母f的ASCI!码值就被getchar〇函数的运行存放到了字符变量ch所对应的内存中。注意:输入字符函数getchar〇调用时不需要参数。输入字符到某个字符变量getche。用法举例:ch=getche();此库函数与getchar。的最大区别是:getche()只需耍按下ー个键,不必在输入-一个字符后再按下回车键,程序就可立即从被中断处继续往下运行。10.计算机内部如何表示有符号整数?ーー整数用补码(即编译程序是如何将我们源程序中的整型变量或常量值,通过变换存储在计算机的内存中的):在计算机的内部要表示一个有符号数,按照约定是用二进制位串的最高位(即最左边的位)来表示符号的,即:最高位如果是。,表示的是正数;最高位为1,表示的是负数。比如01001001表示的数值为1*26+1*23+1*2°=(73)10.而对于11001001如果用的是原码表示法的话,表示的是十进制的ー73。・补码:不过,在现代计算机中,往往不用原码表示法,而用的是补码表示法。・原码转换成补码的规则很简单:正数的补码是和原码・样的;负数的原码转换成补码,只要不管(即将符号位排除在转换过程外外)符号位,将其它各位取反(即将所有1变为〇,所有〇变为1)后,再加上1即可。比如,原码

3611001000去掉最高符号位变成了!001000,将每一位取反,变成了0110111,加1后变成011100〇,再添加上符号位,最后变成为:10111000。即原码11001000的补码是10111000・补码转换成原码的规则:现在,我们将刚刚得到的补码10111000变成原码:去掉符号位变成0111000,将其取反变成1000111,再加1变成1001000,最后加上符号位变成11001000。即补码10111000的原码是11001000。由此可见:对补码再次求补就可得到原码。・对字符变量的进ー步说明:(A)由于对字符变量机内取值的二进制最高位的解释不一样,ー些编译器把字符变量作为取值范围为〇——255的无符号整型变量对待,而另ー些编译器将其作为取值范围为ー128——127的有符号整型变量对待。(B)字符变量的复杂输入输出转换工作(字符在机内的ASCH码与显示器上显示出的该字符的点阵图形之间的转换),都是由输入输出库函数与操作系统密切配合来完成的,请参见第8章9.5节和第8章8.3节。问题:为何用补码而不用原码?答:这是因为,如果按补码规则进行算术运算,所需制造的计算机的电路结构最简单。只要有加法电路就可以做(以补码形示表示的数的)加减法。而目.符号位可以不作任何其它特殊考虑,只要把数——包括符号位,用补码表示即可,此补码的所有位ー并参与运算。只要把运算结果中有向更高位的进位丢弃即可。《计算机科学概论》(机械工业出版社,NellDale等著,张欣等译,原书第2版)ー书中,对二进制补码有一段精彩的论述,参见此书的中文版P38——P40页。11.计算机中如何表示有符号的二进制的实数?在现代计算机中,一般用浮点计数法(即编译程序是如何将我们源程序中的实型变量或常量值,通过变换存储在计算机的内存中的)

37浮点记数法:就是用一个二进制位串表示ー个小数点位置浮动(如果是固定小数点位置的数,就称为定点数)的实型数。这里仅以ー个8位二进制浮点数举例(一般情况下是要用4或8个字节的位组合来表示一个浮点数)。我们可以用最高位第7位表示符号位,用第4,5,6位表示指数位,用0,1,2,3位来表示尾数位。76543210符号位指数位尾数位现在假设,存储了一个位组合00111101在ー个字节中,我们来分析一下,它表示一个什么样的十进制实数。分析:因为符号位为0,所以表明它是正数,指数位是101,尾数是1101。我们先看尾数部分,在左边加上ー个小数点成为.1101,然后再看指数部分011,表示该尾数的小数点要向右移3位,于是,该数最终成为二进制的实数:(110.1)2。它实际上等于(1101)2再除以2。如果ー个实数值很小,比如x=0.126X103,此数在转化为实数的二进制浮点数的机内表示时,即使其指数位是能够表示的最大的负数,而尾数位仍然可能是全都为〇〇结果此数最终被计算机作为〇来对待。小知识:数据的外部(即在源程序中的)表示与机内表示:计算机内部的整数或实数的数据形式,实际上都是二进制形式的(参见本章的提高部分);编译程序会把人们在编写源程序中使用的所有十进制(或其他进制)形式的常量数据,转换成机内的二进制形式的数据。不必我们操心。・格式化输出库函数调用scanfO:1)printfO函数调用的一般形式是:printf(“格式串”,输出项列表):printfO函数被设计用来物出由格式串所指示构成的ぎ,学。这就要通过在格式串中占位符所指定的位置插入输出项的值,来构成输出字符串。调用printf()函数时的第一个参数必须是格式串——这是构成输出字符串的“模板”。后面的参数——输出项——给出要插入到格式串中的任意值。格式串中的转义字符也要按转义后的意义输出;格式串中的其它普通字符,则按照原样进入到输出字符串中。2)格式串的构成形式是:・占位符1・占位符2*…・号表示:此处可以是任意多个转义字符或者(按原样输出的)普通字符。3)输出项列表的构成形式是:表达式1,表达式2,…表达式也可以是单个变量、单个常量(不常见)或者单个的函数调用(因为这些也都是特殊类型的表达式)。因此,printf()函数调用的详细格式是:

38printf("・占位符1・占位符2*…”,表达式1,表达式2,…);tt格式串输出项列表4)格式串中占位符的个数,要与输出项列表中的表达式的个数一致。既不能多,也不能少。输出项列表可以没有,但此时格式串中也不能有占位符。5)占位符的类型要与输出项的类型ー一匹配。即占位符1的类型要与表达式1匹配、占位符2的类型要与表达式2匹配…。否则,输出项的数值转换工作就可能出现意想不到的错误。6)占位符的组成形式:格式化输出库函数printfO中的占位符的组成形式如下:%〈格式符》转换说明格式符受到转换说明的影响,有的转换说明不能含有格式符。printfO函数调用的转换说明有以下几种:表3.xprintfO中的转换说明d,i将有符号整数转换为十进制形式0将无符号整数转换为8进制u将无符号整数转换为10进制x(或X)将无符号整数转换为16进制.(x表示用小写字母a——f来显示十六进制数,X表示用大写字母A——F来显示十六进制数))f将double型值转换为十进制形式,并且把小数点放在正确位置上。如果没有用格式符说明精度,小数点后面显示6位。e(或E)将机内浮点数转换为科学记数法形式表示的double型值,如果没有用格式符说明精度,小数点后面显示6位。e(或E)用来表示以10为底的基。出现在e(或E)右边的整数是指数。g(或G)读者初学时最易犯的错误是,将占位符机与整型变量的输出项匹配,将占位符刎与float变量的输出项匹配.printf()函数的调用方法(一)、变量值的输出・单个变量值的输出格式化输出库函数printf(),是被设计用来在显示屏(或打印机)上输出双引号中称本中的内容的。但是,它不仅可以将格式串按照原样输出,而且还可以通过格式串,输出ー个(或者多个)变量的值。不过为了做此事,必须在格式串的适当位置,犢へー个(或多个)“占位符”〇并且在格式串这个参数后面,书写需要输出的变量,作为另外一个(或几个)参数。“占位符”其实只是为了后面要输出的变量值,在格式串中般生点烟一个适当的位置(这就象在ー个预排的队列中用ー个物品,为某个尚未到来的人,在亚デ队列中预先占据一

39个位置)。格式串中的占位符被变量的值替代后,オ生成了要在输出设备上输出的字符串。占位符由符号“%”和转换说明符构成。最常用的占位符有:%d,%f或%c,它们与要输出的变量的类型之间的简单对应关系如下表:占位符输出的变量类型%dint%ffloat%cchar因此,用库函数printf来输出一个变量值的简单形式就是:printf("・占位符・",变量);此时的格式串就成为了:”占位符・的形式。位于占位符左右两边的“*”号表示:在此处可以出现0个或任意多个按原样输出的普通字符(和转义字符)。格式化输出库函数printf()输出变量值的基本工作原理:将变量的二进制形式的值从存储单元中取出,按照占位符中的转换说明的规定,转换成(通常是十进制数字形式的)字符串后。用该(数字)子字符串,背桃隼格式串中的相应点位得,与格式串中的其他原样输出的普通字符连接成一个新的字符串,然后将其输出。例如:假设内存屮的int型变量i的取值是0000000011111111,如果此时执行的printfO语句形式是:prints“该树的树龄是%d岁Xn”,i);那么变量i的二进制数值将会在转换说明d的规定下,转换成“255”这个由纯卜进制数字字符组成的子字符串,插入到格式串中占位符%d所占据的位置上。所以,最终你在屏幕上看到的并不是:该树的树龄是:履I岁。这个格式串,而是:该树的树龄是255岁。这个由格式串转化而来的输出字符串。•对于float类型的量来说,用占位符“%f”来“规定”输出,默认情况下小数点后面会显示6位数值。若需要显示小数点后的n位数字,占位符的形式要进行调整。必须把格ギ得.n(注意:整数值n的ん边还有•个小数点,不要漏写它!)放置在%和转换说明f之间。比如,例题2.2中表示圆球体积的变量v值如果仅需四舍五入保留小数点后的3位数值,我们就可将其改写为:printf(“圆球的体积是:%.3f立方米Xn”,v);

40・占位符的一般格式:占位符是由%<格式符>转换说明这三个要素顺序组成的,其中格式符是可选项;转换说明用来指定数据的转换方式;格式符用来规定数据的输出宽度、対齐方向和精度(更详细一些的讨论,请参见下一章)。问题1:如果变量v的类型是noat,是否可以将printf()函数调用语句改写成:printf(“圆球的体积是:%d立方米、n”,v);答:不可以,格式说明符必须与变量类型相匹配。所以,此题必须将%d改为%f。•占位符与变量类型的匹配问题:占位符通常与要输出的变量类型匹配。决不能用占位符%d来匹配float类型的输出变量、也决不能用占位符%f来匹配int类型的输出变量。这是常见的编程错误。但是在C语言中也存在例外:可用占位符%d来匹配待输出的字符类型变量,但此时输出的不是字符,而是该字符所对应的ASCII码。也可用占位符%c来匹配待输出的整型变量,但此时输出的不是ー个整数值,而是将该整数值当成ASCI!码,输ル该ASCII码所对应的字符。・多个变量值的输出如果要用一次printf函数调用输出两个变量的值,printf函数的调用形式就应当是:prin里“*占位符I*占位符2*”,变量1,变量2);这样的形式。其中,变量1的输出转换格式山占位符1指定;变量2的输出转换格式山占位符2指定。对于输出更多的变量,情况完全是类似的:pHn甘ド・占位符1・占位符2*.....・占位符n*”,变量1,变量2,......变量n);在格式串中出现的占位符的个数,要与待输出的变量个数相等,既不能多,也不能少。占位符中的转换说明也要与待输出的变量类型ー一匹配。•切记:多个占位符一般不要互相紧靠在一起。比如(假设变量x和y都已经定义并进行了初始化floatx=103.456,y=85.678):printfC%.2f%.2f

41"x,y);这样ー来,将导致几个变量的值在输出时区分不开:103.4585.68这很不好。比较好的方法可以象如下这样:printf("x的值是%.2f,y的值是%.2f

42"x,y);显示屏上的输出是:x的值是!03.46,y的值是85.68

43或者printf("x=%.2f,y=%.2f

44"x,y);显示屏上的输出是:x=J03.46,y=85.68这样的输出都比较好看。如果变量X的值表示的是数学成绩,变量x的值表示的是英语成绩,那么用如下printfO调用语句更好:printf(“数学成绩%.If分,英语成绩%.If分'n”x,y);显示屏上的输出是:数学成绩103.5分,英语成绩85.7分格式化输出库函数printf()调用小结:格式化输出库函数printfO,是设计用来将格式串的内容输出的,格式串中的普通字符按照原样输出;格式串中的转义字符将按照转义后的形式输出:格式串中的占位符,仅仅是为后面的输出项(输出项可以是变量,但也可以是表达式)占据ー个位置。占位符由%、格式符、转换说明二者构成。输出项按照转换说明的要求进行转换,按照格式符的要求进行调整,然后插入到占位符所占据的位置之处,形成最终的输出字符串。格式串必须作为第一个参数,需要输出的变量(或表达式)必须作为其余的参数。在格式串中出现的占位符的个数,要与需要输出(参数表中的)变量(或表达式)的个数相等。既不能多,也不能少。占位符中的转换说明,也必须与需要输出的变量(或表达式)类型ー一匹配。格式化输出库函数printf()的功能其实很强大:不仅可用它来输出多个变量的值、也可用它来输出多个表达式的值。但是,其实它的用法很复杂,也很容易用错。比较详细的用法请参见本章(下)。常用的转义字符列表如下:・转义字符对于可打印字符,可以在程序中用単引号括起来的方法表示。但是ー些控制字符(如回车符、换行符、制表符、退格等)却是无法采用这种方式在程序中表示出来。这就要用转义字符的方式来表示:即,一般用ー个反斜杠加上一个字符的方式,来表示一个控制字符;控制字符在程序正文部分出现,仍然要用单引号括住,比如‘、0’、‘

45,等。转义字符也可出现在字符串中,比如格式控制串中。转义字符转义字符的意义

46回车换行\t横向跳到下一制表位置\v竖向跳格\b退格\r回车\f走纸换页\\反斜线符"'''V单引号符

47\a鸣铃

48\ddd\xhh1〜3位八进制数所代表的字符1~2位十六进制数所代表的字符常见错误:1.语句书写的先后顺序不正确。参见P页;2.关于输入函数scanf()使用中的常见错误参见P页;3.关于输出函数printfO使用中的常见错误参见P页;2.13编程错误分类:・语法错误:可以在编译期间由编译程序找出的错误;・运行时错误:程序在运行时才能发现的错误;・算法错误:在编译和运行时都不能发现的,只有通过事先(编译前)或事后(程序试运行后)分析、检查结果才能发现的错误;、点评:编程时,首先要想方设法避免的是算法错误,因为ー个规模较大的程序,如果在算法上有根本性的错误,有可能造成前功尽弃的严重后果。这就要求程序员在编写算法时仔细慎重,并掌握ー些验证算法是否正确的方法(参见洗牌程序中,画变量内存取值变化图来验证算法是否正确的方法)。214最常见的编程错误列表(此表包括了同学们极易犯的几乎全部初级错误,在上机前要认真看一看;上机出现错误时,也可借助此表来排除错误):1.使用非法的标识符或错误的库函数名;比如main。写成mian();printfO写成了print():或写了sin(2a)、Cos(①)ヽn*r*r等;2.变量未经定义就在语句中使用;3.变量类型定义不当(取值范围不够大、该用整型而用了实型、精度不够等等);4.变量未经初始化就使用在表达式中;5.语句或定义结束时漏写了分号;(或误用分号;复合语句结朿后面不需要分号)6.表达式中漏写了必要的乘号・。例如;将3*x*y+5错写成3xy+5;7.表达式中缺少必要的圆括号,或圆括号不匹配,或者用花括号・、方括号取代了圆括号;8.注意将数学中的代数式转变为C语言的算术表达式中的问题。参见P页。9.忘记了注释的结束符・/;正确的应当是以/・开始,以・/结束,两个符号之间不能用空格隔开;10.在该用小写字母的地方,却用了大写字母(例如,把main写成了Main、scanf写成了Scanf:定义变量名是小写,但在程序中却用了大写的变量名,其中s,c,x,k,z最易用错,比如si写成了SI、ch写成了Ch);11.在语句之间对变量进行了定义。正确方法是在函数体中,将所有的定义放在所有语句之前(但是在ANSIC99标准中,允许在块语句中的最前部定义变量);12.编写代码(程序)时,就特别要注意:避免程序在运行时,用了〇作为除数;绝对值很小的实型量也不能用作为除数;13.在程序的正文部分(参见第2章),用了非法的标点符号(除了英文半角输入法,其他输入方式下的标点符号都是不对的);14.漏写函数体结束时的花括号,或者花括号不配对;15.分隔符使用不正确;比如inta,bc.d:应为inta,b,c,d;;

491.程序中调用了库函数,但却忘记包含相应的头文件(比如要包含头文件:math.h);2.标准输入输出头文件包含时出错;正确的是:#include,或者#include“stdio.h"。但有不少同学会出现拼写错误。3.语句之间的变量有先后依赖关系的,不可随意颠倒语句的先后次序。4.C语言本身不负责数据的输入输出工作。而由输入输出库函数的调用来进行。关于输入函数调用请参见P页,关于输出函数调用请参见P页如何对变量进行基本操作?存数score=38.0;取数3*scoreヽsqrt(score)输入scanff%r',&csore);输出printf「所得分数:%f,score);加、减、乘、除习题:编写ー个程序,运行时输入一个当天的华氏温度值,程序将其转换为摄氏温度并将其在显示屏上输出。已知转换公式为C=5/9(f-32)«(提示:注意5/9在程序中应该如何表示)。

50第二章选择结构程序设计在最简单的情况下,计算机按程序中书写的先后顺序,一条一条地执行由语句翻译成的机器指令。然而,在ー些情况下,语句执行的先后顺序依赖于程序运行时输入的数据或中间运算的结果。此时,必须根据某个(布尔)表达式的值做出判定,以决定选择哪条(或哪些)语句执行,并且跳过哪条(或哪些)语句不执行。通常称这种语句为选择结构语句。选择结构语句,是ー种容器性的语句(又称为构造型语句),它只是用来提供安放其他申句的位置。这样的位置分别有一个、两个或多个。然后通过计算ー个(布尔)表达式的值,来对处于不同位置的语句进行选择。其中,通常只有一条被嵌入的语句可以被选中执行。嵌入在其他位置的语句则被跳过不执行。注意:除了所嵌入的语句之外,选择结构语句本身(的框架)一般不会改变任何变量的取值,除非(布尔)表达式本身是具有副作用的(表达式的副作用,参见第2章的提髙部分)。分句:我们把嵌入在选择结构语句之中的语句称为分句。C语言中,选择结构语句包括两种if语句和一ー种switch语句。(1)提供安放ー个分句位置的是无else的if语句。一般用布尔表达式的值,作为分句是否选择执行的依据)。(2)提供安放两个分句位置的是有else的if语句。一般用布尔表达式的值,作为两个分句选择哪ー个执行的依据。(3)提供安放多个分句(或处理流程)位置的是switch语句。用整型表达式或字符型表达式的多个可能的不同取值,作为在多个分句(或处理流程)中选择其ー执行的依据;或者是if…elseif形式的标准多层嵌套语句(见第讲求建筑物的高度)。2.1两种if语句if语句与赋值语句有所不同。赋值语句先计算ー个表达式,然后把表达式计算出来的值赋给赋值号左边的ー个变量。if语句则先计算ー个表达式的值。这个表达式一般是ー个布尔表达式,布尔表达式的计算结果是逻辑值“真”或逻辑值"假”,if语句根据这个取值的不同,来选择执行不同的分句。if语句同时又是ー种容器型(也称为构造型)语句,也就是说,首先必须把if语句从何处开始,到何处结束弄清楚。其次,在if语句中还能嵌入一到两条分语句(简称为分句),其中有else结构的if语句中,能够嵌入两条分句,无else结构的if语句只能嵌入一条分句。点评:对if语句的深入理解ー个原来在语法上独立的语句被嵌入到if语句中之后,就变成了语法上不独立的ー个

51分句(语法上独立的句子现在是if语句),这似乎很难理解。这就象原来每一个都是独立作战单元的几个士兵,进入了一辆坦克中。他们现在每个人都不再是ー个独立的作战单元,而成了坦克这ー个独立作战单元的ー个组成部分。如果不把if语句看成是一条(容器型)语句,在学习后面的if语句嵌套时,就有可能会产生理解程序运行流程上的困难。被嵌入的分句,可以是一•条简单语句,也可以是一条空语句(一),还可以是一条复合语句(一)。(2)第一种if语句(没有else分支)的语法结构及用法如下:if(布尔表达式)if语句开始处分句;被嵌入的分句,if语句结束处语句2;if语句后的下一条语句若布尔表达式的取值为“真”,则执行被嵌入的分句,然后执行if语句后的下一条语句。若布尔表达式的取值为“假”,则被嵌入的语句1被跳过不执行,转而执行if语句后的下一条语句。第一种if语句的执行流程如图3..1所示。分句达式为假if语句后的下一条语句图2.1第一种if语句的执行流程例题2.1进入旅游景点,对游客身髙进行区分,如果身高高于1.2米要买票。程序如下:1#include2intmain()3{4floatx;printf(“请输入游客身高曲“);scanf("%F',&x);5678if(x>1.2)

521printf(“请购买门票

53”);/・嵌入在if语句中的语句1*/23printf("程序结束

54”);/*if语句后的下一条语句・/4)请分别两次运行此程序:第一次:输入身高1.5米,表达式x>1.2为真,程序运行结果是:请购买门票程序结束嵌入在if语句中的分句得到了执行第二次运行,输入身高1.1米,表达式x>1.2为假,程序运行结果是:程序结束显然,嵌入在if语句中的分句此时没有得到执行。问题:例题2.1中的if语句是否一定会得到运行?所嵌入的分句是否一定会得到运行?(1)第二种if语句(有else分支)的结构及用法如下:if(布尔表达式)if语句开始处分句1;被嵌入的第一条分句else分句2;被嵌入的第二条分句,if语句结束处语句2;if语句后的下一条语句布尔表达式的取值只有两个,“真”(在C语言中用整数值1来表示真)或者“假”(在C语言中用整数值。来表示假)。如果布尔表达式的取值为“真”,则执行被嵌入的分句1,被嵌入的分句2跳过不执行,然后执行if语句后的下一条语句。如果布尔表达式的取值为“假”,则执行被嵌入的分句2,被嵌入的分句1跳过不执行,然后执行if语句后的下一条语句。第二种if语句的执行流程如图2.2所示。图3.2第二种if语句的执行流程由此可见,有else的if语句是根据布尔表达式的不同取值,在嵌入的两条分句之间选择一条执行。

55例题2.2进入旅游景点,对游客身高要进行区分,如果身高等于或低于1.2米,不需购买门票,高于1.2米要购买门票。程序如下:1#include2intmain()3(4floatx;56printf("请输入游客身高

56”);7scanf("%ド,&x);8if(x<=1.2)9printf("不必购买门票

57");/*嵌入的语句1*/10else11printf(”请购买门票

58");/*嵌入的语句2*/1213printf("程序结束

59”);/*if语句后的下一条语句・/14return0;15}请分别两次运行此程序:第一次运行时,输入身高1.1米,if后面的表达式x<=1.2成立(表达式的结果为真),选择嵌入的分句1执行:程序运行结果是:不必购买门票程序结束显然,嵌入在else后面的分句2没有得到执行。第二次运行:输入身高1.5米,表达式x<=1.2不成立(表达式的结果为假),选择嵌入的分句2执行;程序运行结果是:请购买门票程序结束显然,这一次,嵌入在else前面的分句1没有得到执行。问题1:第9行,第11行为何耍缩格(即在前面加了一些空格)?是否可以不缩格?答:为的是强调这并不是两条独立的语句,面仅仅是嵌入在if语句中的分句。可以不缩格,但是不缩格,程序的结构就看不太清楚。习题1.编程题:已知自变量x的值,求函数y的值,已知函数式为:3x4-6当x<2y=\5x2-5.4当x22(提示:当条件x<2不成立时,x>=2一定成立)

60注意:在C语言中,if后面的表达式一定要用圆括号括住,布尔表达式后面一般不能有分号(除非你想把一条空语句作为嵌入在if语句中的分句)。另外,一定要把if语句从整体上看成是二条语句,虽然在它的内部甚至可以嵌入ー个或两个处理流程(即复合语句)。2.2布尔表达式之一:关系表达式那么什么是布尔表达式呢?布尔表达式是对关系表达式和逻辑表达式的统ー称呼。由于这两种表达式运算得到的结果都是布尔值。布尔值只能取两个值之中的一个,即“真”或“假”。在c语言中,“真”用整数值1来表示,而“假"用整数值0来表示。定义:关系表达式由操作数(常量、变量、函数调用、算术表达式等)、圆括号、关系运算符(大于〉、大于等于>=、小于<、小于等于<=、等于=、不等于!=)构成的有意义的式子称为关系表达式。其中,关系运算符是必须有的,操作数有两个,因为关系运算符是二元运算符。操作数可以是常量、变量、函数调用、算术表达式中的任何ー个。关系运算符的优先级比算术运算符低。•常用的算术、关系运算符的相对优先级如下:1.・、/、%2.+-3.大于:>大于等于:>=小于:<小于等于:<=4.等于:=不等于:!=关系表达式用来比较两个量(或算术表达式)的大小或相等不等。这两个量可以是整型量或实型量,这时比较的是数值本身的大小,比如:x>=3.7、n==100,(m%7)!=0等。关系表达式也可以用来比较两个字符量的大小,比如,,a>,4,或ch!=ヘ〇,,其中ch是字符变量,此时比较的是字符所对应的ASCH码的大小。注意对于用字符变量或常量来进行比较大小(即进行关系运算),通常是取用它所对应的ASCII码值来进行大小比较的(例外情况是:这台计算机不是用ASCH码来表示字符,而是用别的字符编码表)。C语言中,关系表达式的运算结果取值为逻辑值,逻辑值只有两个,“真”或者“假”。数学中的不等式与(比较大小的)关系表达式的区别关系表达式x>=3并不是中学学过的不等式x23。在中学学过的不等式中,x,3意味着x的值不能比3小。也就是说,不等式限制了变量的取值。而关系表达式x>=3并不能限定变量x的取值范围。关系表达式x>=3在x的值大于等于3时取值为“真”(即为1),在x的值小于3时取值为“假"(即为〇)。

61对于x!=3.情况也是类似的。关系表达式x!=3并不限制x的取值不可以是3。此本法オ在x的值等于3时取值为“假”,x的其他取值是表达式的值都为真。点评,要想改变ー个变量的取值,一般只能用赋值语句、输入函数调用来直接改变;或通过指针变量来间接改变。没有副作用的关系表达式,改变不了变量的取值。不要以为我们写了x<=3.6,x的值就会比3.6小(类似的,并不是写了ch!='C',字符变量ch的取值就不会是大写英文字母C)。中学学过的不等式和方程式(即等式)限制了自变量或因变量的取值范围;而高级语言中的没有副作用的关系表达式不会对(出现在表达式中的)变量的取值有任何限制和影响。关系表达式只是将变量的值从存储单元取出来进行关系运算符所给定的关系运算,并得到ー个“真”或者“假”的结果值。归纳起来,可以通俗地说:对于任何关系表达式,如果变量的取值使得表达式所表示的大小(或相等、不相等)关系不成立,表达式的取值为“假(0)”;如果变量的取值使得表达式所表示的大小(或相等、不相等)关系成立,表达式的取值为“真(1)”。深入ー步基本的机器指令中并没有比较两个数大小的指令(参见第10章),那么为什么高级语言中可以比较两个数的大小或者是否相等呢?这是由于机器语言级有减法指令,并且一般计算机的CPU中都有一个状态寄存器(参见第10章),其中有一个二进制位专门用来表示上一条指令的运算结果是否为〇,因此,CPU通过读取这个状态寄存器的值,就可以知道两数相减的结果是否为0,也就间接地知道两数是否相等,这就是在高级语言中为何可以用等于“==”,不等于"!="的原因.对于大于、大于等于,小于、小于等于,关键在于状态寄存器中有一个二进制位,可以表示上一条指令的运算结果是否大于0。基本的三条跳转指令如下:(1)无条件跳转.(2)如果上一条指令的运算结果大于零则跳转.(3)如果上一条指令的运算结果大于等于零则跳转.该二进制位与这三条指令结合起来,就可在高级语言中使用关系表达式,并根据表达式的值选择不同的处理流程(参见第10章).注意:不要对两个大小非常接近的实型量进行相等比较。因为它们的结果可能为〇。这是因为实型量的机内表示是由:符号位+尾数+指数构成的,如果两个实数大小很接近,它的指数和尾数就可能完全ー样,相减的结果尾数就全为0,指数不变。系统就会认为比较的两数是相等的(参见第2章提高部分).点评/通过表达式可以要求计算机做多个数值的数学运算,在程序中能否要求计算机同时做三个数或更多数的大小比较呢?这是不行的。其实计算机本身的CPU的硬件结构决定了它在任意时刻只能做两个数的运算以及比较两个数的大小(参见第章算术逻辑单元的编程结构图),而不能做多个数的运算和比较大小.在表达式中虽然可以写a+b*c+d等多个数的算术运算,但翻译成机器指令

62时则变成了多次两个数的加减乘除。对于比较大小,只能用关系运算符,而ー个关系运算符只能用来比较两个数。所以要用计算机来比较多个数的大小,则只能用关系表达式明确地告诉计算机,如何通过两两比较大小,来达到最终实现多个数的比较大小的目的(可参见本章后面讨论)。注意:不仅不能直接命令计算机比较多个数的大小,就如a>b>c或者4<=x<9.6这样的比较形式,也不符合高级语言的数据比较要求(参见本章后面讨论).问题1.当整型变量n的取值分别为1和4时,下列关系表达式的值是多少?(1)n>1ヽ(〃ー3)>1(2)n>=1ヽ(n-3)>=1(3)n'C';(2)ch>=tC,;(3)ch

63该程序段无任何语法错误,但所写的if语句形同虚设,根本不起任何作用。语句1的执行与布尔表达式的取值无关。2.3复合语句复合语句(也称为块语句、分程序)是由花括号括起来的多条语句。其格式为:(语句1:语句2:语句n;}在一般的高级语言中,都可以把复合语句作为一条语句,用于任何只能出现一条简单语句的任何位置。因此,复合语句当然可以作为嵌入在if语句中的分句。深入ー步:在本ス中,经常把复合语句称为・个处理流程。复合语句中如果只有简单句(即不包含选择、循环、无条件跳转语句、函数调用),则称为ー个基本处理流程。这是因为这样的语句转换成机器指令后,•定也是顺序执行的・组指令,不包含任何跳转指令。注意1:复合语句是C语言中唯ーー种不以分号结束的语句。注意2:在C99标准中,可以在块语句(复合语句)的前部定义变量,例如:{inti;i=3;j=j+i;}这样的变量只在块语句中起作用,在这个语句块之外,这样的变量不能使用。第6讲读入三个数,找岀并打印其中最小的数类型必修题趣味性・难度・ー级算法:1.读入三个数xl、x2、X3«2.假设xl为最小数,即将xl的值赋给min。3.如果x22intmain()3{4floatxl>x2,x3,min;5/・读入三个数xl、x2、x3*/67printf(”请输入三个数、n");8scanf(''%f%f%fw,&xl,&x2,&x3);9/・设xl为最小数,即将xl的值赋给min*/10min=xl;

641/・如果x2intmain()(floatxl,x2,x3,min;printf(”请输入三个整数、n");scanf(''%f%f%fz,,&xl,&x2,&x3);if(xlintmain()(floatxl»x2,x3»min;pintf(ヽ、请输入三个实数、n〃);scanf(''%f%f%f,&xl,&x2,&x3);if(xl

651.输出x、y的值。其中第2步需要二级求精。二级求精:(temp=x;x=y;y=temp;)转换为c语言程序如下:1#include2intmain()3{4floatx,y,temp;5printf(”请输入两个数・,y

66w);6scanf(、、%£,%f",&x,&y);7if(x

67x=y;/・复制变量y的值,将它存到变量x所表示的存储空间中・/y=x;/・复制变量x的值,将它存到变量y所表示的存储空间中・/答:不可以。如果用以上语句,则两个变量中的值都是变量y原来的值。因为计算机在执行完第一条赋值语句后,变量x中的值是变量y的值,x原来的值已经不存在了。2.5if语句的嵌套及其用法在if语句中所嵌入的分语句,还可以是if语句,这就是if语句的嵌套。注意:if语句所嵌入的分句,除了简单句、复合语句和空语句外,还可以是一条容器型语句。多条if语句同时出现时,一定要搞清楚哪些if语句之间是并列关系,哪些是嵌套关系;哪条if语句从何处开始到何处结束;是哪种类型的if语句。否则,在阅读和编写有多个处理流程的选择结构的程序段(而且不能简单地使用多分支的switch语句)时,就有可能出现理解错误或编程错误。•else与if的配对问题在以下的问题1和问题2中,讨论了else与if的即対问题。问题1.如果有如下的嵌套if语句:if(布尔表达式1)if(布尔表达式2)语句1:else语句2;此程序段的流程结构是怎样的?答:从整体上看,是一条无else的if语句(其条件为布尔表达式1),其中嵌入了一条有else的if语句(其条件为布尔表达式2)。嵌入的if语句在(布尔表达式1的值)为“真”时执行。此问题的程序语句没有使用缩进方式书写,如果将其写成以下的形式,虽然流程没变,但程序的可读性更好。if(布尔表达式1)if(布尔表达式2)语句1:else语句2;问题2.如果有如下的嵌套if语句:if(布尔表达式1){if(布尔表达式2)语句1:)else语句2;此程序段的流程结构是怎样的?答:外层是一条有else的if语句,在外层的if语句中布尔表达式1的值为“真”时,嵌入另一条if语句。这条if语句是无else的if语句。语句1属于内层if语句;语句2属于外层if语句。此问题的程序语句没有使用缩进方式书写,如果将其写成以下的形式,虽然流程没变,但程序可读性更好。if(布尔表达式1)(if(布尔表达式2)语句1;)else语句2;

68结论:else一般是与上面最靠近的关键字if配对,构成有else的if语句。除非有花括号出现,才能改变与该else配对的if。2.5布尔表达式之ニ:逻辑表达式由常量、变量、圆括号、函数调用、算术表达式、关系表达式、逻辑运算符构成的有意义的式子,称为逻辑表达式。其中,逻辑运算符是必需有的,其他为可选项。在C语言中,逻辑运算符“与"(英文用and)用符号“&&”来表示,“或”(英文用or)用符号“『’来表示,“非”(英文用not)用符号“!”来表示。基本逻辑运算的真值表见表3.1。表3.1基本逻辑运算的真值表ABA&&B(与)A||B(或)•A(非)真真真真假真假假真假假真假真真假假假假真•逻辑运算符的优先级,非运算符!最高、其次是与运算符&&、最低的是或运算||。・常用运算符(算术、关系、逻辑)的相对优先级为:1.!2.・、/、%3.+、・4.>、>=、V、<=5.=、!=6.&&7.II逻辑运算符,主要是用来把两个关系表达式结合起来,构成一个逻辑表达式。・“与”运算符&&的用法:比如:如果我们想要关系表达式1和关系表达式2都为真时オ做分句1(即严格做分句1的条件),只要有一个不成立就做分句2,此时就要用逻辑与‘'&&"运算符,表示为if(关系表达式1&&关系表达式2)分句1:else分句2;

69•或运算符|!的用法:如果我们想要关系表达式1和关系表达式2其中之一为真时就做分句1(即放寛做分句!的条件),其他情况下都做分句2.此时就要用逻辑或''||"运算符,表示为if(关系表达式1||关系表达式2)分句1;else分句2;・非运算符!的用法:非运算符!是单目运算符,它作用在出现在它右边的逻辑量上,将原来的逻辑值“真”变成“假”、原来的逻辑值“假”变成“真”。比如:!(3>5)3>5的运算结果为假,经过非运算!最终值为真(即为1)。问题1.如果要表示变量x的取值只有在满足2くxV5时オ执行分句1,否则执行分句2,能否将其表示为:if(2<=x<5)分句1;else分句2;答:不可以。假设x的取值为1(x的取值不满足条件2Wx<5,本应该执行分句2,但是对于表达式2<=x<5,由于关系运算符的结合性是由左到右,因此相当于(2<=x)<5,此表达式中圆括号中的关系表达式2<=x就是2<=1,关系表达式的值显然是“假”(因此取值为0)。整个表达式变成了〇<5。所以整个布尔表达式的取值为“真”。现在要执行的分句,竟然是分句1了!这显然是不对的。造成错误的关键在于:用表达式2<=x计算得到的举曾值0与擎製值5进行了比较:这是完全不对的。将一个逻辑值与整数值比较大小在这里毫无意义。而这是初学时极易犯的错误(不过在判断四大湖大小问题的第讲中,我们却使用了几个逻辑值之和与整数值进行大小比较,这是使用C语言编程的灵活之处。在Pascal、Java等髙级语言中,逻辑值是不能与一个数值比较大小的)。问题2.在程序中,如何表示变量x的取值要求满足以下条件?(1)2《x<5答:x>=2&&x<5〇(2)xW1.3或者x>7答:x<=1,3||x>7〇习题1.请将以下x、y的取值条件用逻辑表达式表示出来。(1)3Wx<(y+7)(2)y27或者y<2.4习题2.x=3,y=-l,求出以下逻辑表达式的值。(1)(x>3)&&(y+7)<2⑵(y>6)||(y<2.4)习题3.请将字符变量ch的取值,用逻辑表达式表示出来。

70(1)ch是数字字符(2)ch不是数字字符(3)ch是小写英文字符(4)ch是英文字符(包括大写和小写)(5)ch不是英文字符(6)ch既不是英文字符也不是数字字符第8讲输入三个数并由大到小输出这三个数类型必修题趣味性・难度**ー级算法:

711.输入三个数x、y、z2.如果x>y并且x>z(这说明x最大)那么2.1如果y>z那么2.1.1输出x、y、z(这说明y第二大,z最小)否则2.1.2输出x、z、y(这说明z第二大,y最小)3.如果y>x并且y>z(这说明y最大)那么3.1如果x>z那么3.1.1输出y、x、z否则3.1.2输出y、z、x4.如果z>x并且z>y(这说明z最大)那么4.1如果x>y那么4.1.I输出z、x、y否则4.1.2输出z、y、x本例算法的主要结构为2、3、4这三条并列的if语句,其中分别都嵌入了一条if语句,嵌入的位置都是在表达式为“真”时要执行的分句。注意嵌入的分句用的标号2.1、3.1、4.1表示此三条语句并不是独立的语句,而是嵌入到2、3、4这三条if语句中的分句。注意:2.1、3.1和4.1这三个分句相对于2、3、4这三条if语句都向右缩进了几格,使程序的可读性更强。转换为C语言程序如下:1#include2intmain()3456789if1011121314if1516171819if2021floatx,y,z;/・输入三个数x、v、z;*/printf(”请输入三个数,用空格隔开、n〃);scanf(''%f%f%fwz&x,&y,&z);(x>y&&x>z)if(y>z)printf(w%f,elseprintf(''%f,(y>x&&y>z)if(x>z)printf(w%f,elseprintf(''%f,(z>x&&z>y)if(x>y)/・说明X最大・/%f»%f

72w,x,y,z);/・说明y第二大,z最小*/%f,%f

73,r,x,z,y);/・说明z第二大,y最小・//・说明y最大*/%f,%f

74”,y,x,z);%f,%f

75w,y,z,x);/・说明z最大・/printf(''%f,%f,%f

76w,z,x,y,);22else(''%f,%f,%f

77w,z,y,x);23pri2425return0;26)问题1.以上程序中哪些if语句是并列关系,哪些是嵌套关系?每个if语句是什么类型的?其中嵌入了什么语句?if语句从哪里开始,到哪里结束?答:9、14、19行开始的这三条if语句是并列关系,并且都是没有else的if语句,这三条if语句中又都分别嵌入了一条带有else的if语句。第9行开始的if语句到第!3行结束;第14行开始的if语句到第!8

78行结束:第19行开始的if语句到第23行结束:第10行开始的if语句也在第13行结束;第15行开始的if语句也在第18行结束;第20行开始的if语句也在第23行结束。问题2.此题如果不用逻辑表达式,而只用关系表达式,是否可以?答:可以,不过流程会复杂很多。读者不妨自己试ー试。习题1.请读者把以上程序中语句前的数字号去掉,并把所有句子左对齐,然后判断每个if语句间的关系是嵌套关系,还是并列关系,还是没有关系?哪条if语句到哪里结束?习题2.输入三个字符,按照ASCII码的顺序,由小到大输出这三个字符。第9讲求建筑物的高度题目类型选修开拓思路题趣味性**难度**已知;平地上建有4个圆塔,每个圆塔的高度都是10米,塔的半径都为1米。4个塔的中心坐标x,y分别为;(1)塔1中心坐标为2,2=(2)塔2中心坐标为ー2,2。(3)塔3中心坐标为ー2,-2。(4)塔4中心坐标为2,-2。求任意给定一点的建筑物的高度。ー级算法:1.输入坐标点x,y的值。2.如果(x,y在塔1内)或者(x,y在塔2内)或者(x,y在塔3内)或者(x,y在塔4内)那么打印’’建筑物的高度为10米”。否则打印“建筑物的高度为0米”。其中布尔表达式“x,y在塔1内”需要进ー步求精,如下:(x-2)*(x-2)+(y-2)*(y-2)<1其他几个表达式类似。布尔表达式“x,y在塔2内”如下:(x+2)*(x+2)+(y-2)♦(y-2)<1布尔表达式“X,y在塔3内”如下:(x+2)*(x+2)+(y+2)*(y+2)<1布尔表达式“X,y在塔4内”如下:(x-2)*(x-2)+(y+2)*(y+2)<1转换为C语言程序如下:1#include2intmain()3{4floatx,y;5/・输入坐标点x,y的值・/6printf(“请输入坐标x,y的值、n");7scanf(%f%fw,&x.,&y);8if(((x-2)*(x-2)+(y-2)*(y-2)<1)||((x+2)*(x+2)+(y-2)*(y-2)<1)||9((x+2)*(x+2)+(y+2)*(y+2)<1)||((x-2)*(x-2)+(y+2)*(y+2)<1))10printf(“建筑物的高度为!0米、n”);11else12printf(“建筑物的高度为0米'n");

7912return0;3}另外一种简洁的算法如下:(1)输入坐标点x,y的值。(2)如果(x,y不在塔1内)并且(x,y不在塔2内)并且(x,y不在塔3内)并且(x,y不在塔4内)那么,打印“建筑物的高度为0米二否则,打印“建筑物的高度为10米”。点评タ点评:此题也可以不用逻辑运算符,但流程结构就复杂多了,通过对比可知逻辑运算符的运用,在简化if语句嵌套时的巨大威力。下而介绍不用逻辑表达式的算法。第・种复杂的算法如下:ー级算法:(1)输入坐标点X,y的值;(2)if(X,y在塔1内)(2.(1)“建筑物的高度为10米”;else(2.(2)坐标点x,y不在塔1内的情况:/・还有可能在塔2、3、4内・/其中(2.2)需要进ー步求精。二级求精:(2.(2)坐标点x,y不在塔1内的情况:/*还有可能在塔2、3、4内・/if(x,y在塔2内)(2.2.(1)打印“建筑物的髙度为10米”;else(2.2.(2)坐标点x,y不在塔2内的情况;/・但还有可能在塔3、4内・/其中(2.2.2)还需进ー步求精。三级求精:处理坐标点x,y不在塔2内的情况;/・但还有可能在塔3、4内・/if(x,y在塔3内)(2.2.2.(1)“建筑物的髙度为10米”:else(2.2.2.(2)坐标点X,y不在塔3内的情况;/・但还有可能在塔4内・/其中(2.2.2.2)还需进ー步求精。四级求精:处理坐标点x,y不在塔3内的情况:/・但还有可能在塔4内・/if(x»y在塔4内)打印“建筑物的髙度为10米”;else打印“建筑物的高度为0米";/・程序流程到此,x,y点一定不在任何塔内・/通过以上考虑,可得到全部算法如ド:(1)输入坐标点x,y的值;(2)if(x,y在塔1内)

80(2.2.2.(1)打印“建筑物的高度为10米”;(4)else(5)if(x,y在塔2内)(6)打印“建筑物的高度为10米”;(7)else(8)if(x,y在塔3内)(9)打印“建筑物的髙度为10米”;(10)else(11)if(x,y在塔4内)(12)打印“建筑物的髙度为10米”;(13)else(14)打印“建筑物的高度为0米”;问题1.输入的坐标点x,y满足什么条件jf,オ会执行算法第柏14行的处理?答:只有输入的坐标点x,y不在任何塔内并,算法流程オ会到达第的14行的处理。对于这种所有嵌入的if语句都出现在else后面的情况(这些条件都是互斥的,即只有一个条件会得到满足),可以把算法写成如下不用缩格的形式,并且把else与嵌入在它后面的if写在同一行:输入坐标点x,y的值;if(x,y在塔1内)打印“建筑物的高度为10米”;elseif(x,y在塔2内)打印“建筑物的髙度为10米”;elseif(x,y在塔3内)打印“建筑物的高度为10米”;elseif(x,y在塔4内)打印“建筑物的高度为10米”;else打印“建筑物的高度为〇米”;只有这ー种特殊的多重嵌套的if语句可以不需要缩格。if(表达式1)语句1;elseif(表达式2)语句2;elseif(表达式3)语句3;elseif(表达式n)语句n;else

81语句n+1;问题2.以上程序中“else语句“+1;”是与哪个if语句配对的?答:它是与ifl:表达式n)配对的;但是,只有在n个表达式的值都为假时,オ会执行到嵌入的这ー语句。这种语句的程序流程如图3.3所示。图3.3一条多重:嵌套if…elseif语句的执行流程注意:在程序的任何一次运行时,在多重嵌套if语句(从整体上来看,它就是一条if语句)中,只有嵌入其中的一条分句会被执行。其他被嵌入的分句都会被跳过。当所有布尔表达式的取值均为“假”时,オ会执行嵌入的分句n+1。这就是一般情况下的多分支选择结构语句。本章后面将所要介绍的switch语句也是多分支选择结构语句,但由于要求每个分支的执行条件都是一个整数值,所以使用起来比较方便:但是从应用范围来说,只有以上这种形式,オ是适用于任何在多个完全相互排斥的条件下,选择一条被嵌入的语句执行的普遍形式。在很多C语言教科书中,把这种特殊的多重嵌套形式的if…elseif语句,当作if语句的第三种形式。对于这种特殊结构,可以用缩格的方式来编写如下:if(条件1)语句1:elseif(条件2)语句2;elseif(条件3)语句3;else语句n+1;但如果嵌套次数太深的话(所缩的格接近或超出一行的宽度),可能不如用不缩格的方法淸楚。点评タ

82如果不得已要使用多层嵌套的if语句,最好使用以上这种形式的嵌套げ语句。在所有else后面都嵌入・条if语句。第二种复杂的算法如下:ー级算法:1.输入坐标点x,y的值;2.如果(x,y不在塔1内)(2.(1)x,y不在塔1内的情况;否则(2.(2)“建筑物的高度为10米”:/・在塔!内*/其中2.1需要进ー步求精。二级求精:处理x,y不在塔1内的情况2.1如果(x,y不在塔2内)(2.1.(1)x,y不在塔1和塔2内的情况;否则(2.1.(2)“建筑物的高度为10米”;/・在塔2内・/其中(2.1.1)需要进ー步求精。三级求精:处理x,y不在塔1和塔2内的情况(2.1.1)如果(x,y不在塔3内)(2.1.1.(1)x,y不在塔1、塔2和塔3内的情况:否则(2.1.1.(2)“建筑物的高度为10米”:/・在塔3内*/其中(2.1.1.1)需要进ー步求精。四级求精:处理x,y不在塔1、塔2和塔3内的情况(2.1.1.(1)(x,y不在塔4内)打印“建筑物的高度为0米”:/・此时点x,y一定不在任何ー个塔内・/否则打印”建筑物的高度为10米”;/・在塔4内・/由上述讨论可以得到第二种复杂的完整算法如下:(1)输入坐标点x,y的值;(2)if(x,y不在塔1内)(3)if(X,y不在塔2内)(4)if(X,y不在塔3内)(5)if(X,y不在塔4内)(6)打印“建筑物的高度为〇米”;(7)else打印“建筑物的高度为10米”(8)else打印“建筑物的高度为10米”;(9)else打印“建筑物的高度为10米”;(10)else打印“建筑物的高度为10米”;第二种复杂算法中,使用了缩进格式,此算法如果不用缩进形式,程序将非常难以读懂。读

83者不妨试ー试。问题3.将上述算法中各if语句从哪一行开始到哪一行结束(从上往下分别称为ifl、if2,if3和if4)标出来,各if语句的条件为“真”时要执行的分句从何处开始到何处结束?条件为“假”时要执行的语句是哪些?各if语句是并列关系还是嵌套关系?答:各个if语句间都是嵌套关系,没有并列关系。整个来看是一条if语句;其中if2是嵌入在if1中的条件为“真”时要执行的分句;if3又是嵌入在if2中的条件为“真”时要执行的分句;if4又是嵌入在if3中的条件为“真”时要执行的分句。输入坐标点x,y的值;if(x,y不在塔1内)/*ifl开始处・/if(x,y不在塔2内)/*if2开始处,if1为真・/if(x,y不在塔3内)/*if3开始处,if2为真・/if(x,y不在塔4内)/*if4开始处,if3为真・/打印’’建筑物的高度为0米”;/*if4为“真”*/else打印“建筑物的高度为!0米”/*if4为“假”if4结束处・/else打印“建筑物的高度为10米”;/*if3为“假”,if3结束处・/else打印“建筑物的高度为10米”;/*if2为“假”if2结束处・/else打印“建筑物的高度为10米”;/*ifl为“假”ifl结束处・/・问题4.如果塔不是4个,而是N个,塔心坐标也没有规律,请考虑怎么做?(参见第讲射击问题)点评タ在阅读包含有多个if语句的程序段时,一定要弄清楚各条if语句之间的相互关系是嵌套关系还是并列关系?也要弄清楚各条if语句从哪里开始到哪里结束,哪个else是与哪个if配对的。但最为重要的是自己在平时编程时,要养成使用缩格的好习惯(除了一种特例外)。这样即使在if语句间的关系较为复杂时,也比较容易看清楚。小知识:德.摩根律德.摩根律和布尔代数在变换和简化比较复杂的逻辑表达式时,起着重要的作用.2.7switch语句c语言中,在多分支的情况下,如果用if语句嵌套的层数太多,往往会使读懂程序和编写程序的难度大大增加,并且容易出错(标准的if…elseif形式的多重嵌套语句除外),由此出现了一种新的多分支语句,即switch语句。其格式为:switch(表达式){/・注意,此花括号不能少・/case常量1:语句1或空<break>;case常量2:语句2或空<break>;case常量n:语句n或空<break>;default:语句n+1或空;)/・注意,此花括号不能少・/执行switch开关语句时,将表达式计算出的值逐个与case后的常量进行比较,若表达式的取值与其中的一个常量相匹配,则执行该常量后的若干条语句,若不与任何ー个列出的常

84量相匹配,则执行default后面的语句。注意:(1)关键字switch后的表达式可以是整型变量,也可以是字符变量,还可以是整型表达式,但不能是实数或实型表达式。(2)可以省略关键字default以及default后面的分句(但不提倡省略default后面的分句,因为程序的健壮性可能会不好)。(3)每个case或default后都可以有多条语句,但可以不用花括号“『与"ド将其括起来。(4)每个ease分支后面,都加了一个语句,表示语句break是可选的,如果有break,会通过执行break语句而跳出switch语句,switch语句中的其他语句部分就不会得到执行。如果没有break语句,则以下所有“嵌入”的语句都会顺序依次执行,这一点与同属于选择结构的if语句有所不同。小知识:什么是程序的健壮性?程序的健壮性(又称鲁棒性robustness)是指程序在各种不正确的操作或输入不符合要求的数据时,能够比较好的应对,而不会出现致命错误,导致程序不能继续运行下去。健壮性不好的程序比较脆弱,在错误操作或不符合要求的输入数据下,程序容易崩溃。第10讲输入两个运算量及ー个算术运算符,输出运算结果类型必修题趣味性**难度**此题是构成一个简单计算器程序的基础。ー级算法:1.输入第一个运算量X。2.输入运算符eh。3.输入第二个运算量y。4.根据ch的取值执行以下的一条语句。{ch取值为、+,:result=x+y;ch取值为、-,:result=x-y;ch取值为、*,:result=x*y;ch取值为、/,:result=x/y;ch的其他取值:输出”输入的运算符有错")5.输出运算结果result〇转换成C语言程序如下:1#include2intmain()3{4floatx,y,result;5charch;

8523scanf(w%f%c%fw,&x,&ch,&y);410switch(ch)11(12case'+':resultsx+y;break;13case:result=x-y;break;14case'*1:result=x*y;break;15case'/':result=x/y;break;16default:printf("输入的运算符有错ゝn");break;17)18printf("result=%f”,result);1920return0;21)注意:此题运行时,要将第一个数、运算符、第二个数连起来输入,数据之间不要用空格隔开。否则,scanf函数会把空格作为运算符存入到字符变量ch中.程序不能正常运行.问题1.如果把每一行最后的break语句都漏掉了,结果是否正确?答:流程会发生错误,当输入的ch的值为,一,号时,会执行result=x-y;,然后依次执行第14行、第15行、第16行的句子。result=x*y;result=x/y;printf(、、输入的运算符有错'n");所以减法运算没有作用,即使输入的是“3.6-1.8”,得到的结果也是2.0(最后做除法,并且还在屏幕上输出“输入的运算符有错”信息)。问题2.程序第6行中的switch(ch)右边多加了分号会怎样?答:会出现语法错误。switch(ch)右边不允许有分号,因为switch(ch)这一行并不是ー条switch语句。问题3.如果漏掉了第!I行和第17行的花括号,结果会怎样?答:会出现语法错误。switch语句的结构要求用这ー对花括号来括住所有的分句。问题4.关键字switch的后面一般是ー个用圆括号括住的表达式,这个表达式是否可以是ー个实型表达式?答:不可以,只能是整型(但不能是longint型)和字符型。问题5.case此处的单引号是否可用双引号?或者用中文输入法下的单引号?此处的冒号是否可用逗号或者分号取代?答:都不可以,字符常量只能用单引号括住。问题6.default:以及在它后面嵌入的语句是否可以省略?答:可以,但非常不好。程序会对用户输入错误的数据不知如何处理。问题7.case后面所嵌入的分句是否可以是if语句?答:所嵌入的可以是任何类型的语句,也包括if语句。问题8.如果在ー个case后面耍嵌入多条分句,这多条语句是否要用花括号括住,让它们变成一・条复合语句?答:可以不必用花括号。因为根据switch后面表达式的取值,其取值与之匹配的某个case

86后面的语句会一条・条执行,直到遇到break时オ跳出switch语句。问题9.是否可用switch语句来构造一切多分支的流程结构?答:不可以,因为只有在可以列举出整数(或字符)取值的情况下,オ可以用switch语句。一般情况尽量使用标准的if-elseif形式的多重嵌套语句。练习:以上例题还未考虑到除数为〇的情况,请修改程序,把除数为〇的情况考虑进去。第11讲输入年月,输出该月有几天类型必修题趣味性**难度**ー级算法:(1)输入年year和月month〇(2)判断该月有几天。(3)输出年、月和该月的天数。其中第二步要进一步求精。根据month的值执行以下一条语句。(1)当month的值为1、3、5、7、8、10、12月:days=31天。(2)当month的值为4、6、9、11月:days=30天。(3)当month的值为2月:if(year为闰年)thendays=29天elsedays=28天其中year为闰年需进一步求精。一年365天是地球围绕太阳运行一周的大致时间,但实际运行一周平均用365.2422天,于是每4年(选定了年号被4整除的年份)加一天,称该年为闰年,但这样又多加了(365.25为一年),于是每100年(选定了年号能被100整除的年份)再减去ー个闰年(365.24为一年),但这样又减多了,于是每400年(年号能被400整除的年份)再加上一个闰年(365.2425为一年)。这样每400年中,实际的公元H历天数是365*400+100-4+1=365*400+97天。每年的日历平均天数是(365*400+97)/400=365.2425天。注意这样实际上每过ー百万天(约2740年),仍然会有三天的误差。所以,闰年的条件可表达为满足以下两个条件之一的年份。(1)能被4整除但不能被100整除的年份,用逻辑表达式可表示为:year%4==0&&year%100!=0(2)能被400整除的年份,用逻辑表达式可表示为:year%400=0山于两个条件只要其中之一满足就是闰年,可用逻辑或的关系将两个表达式合并为ー个逻辑表达式。(year%4=0&&year%100!=0)||year%400=0三级求精:year为闰年时表示为以下的逻辑表达式。((year%4=0&&year%100!=0)||year%400==0)转换为c语言程序如下:1#include2intmain()3{4intyear,month;

871intdays;2print—'、请输入年月,两个值之间用空格隔开、n");3scanf(w%d%dw,&year,&month);4switch(month)9{10case1:case3:case5:case7:case8:case10:case12:days=31;break;11case4:case6:case9:case11:days=30;break;12case2:13if((year%4==0&&year%100!=0)||year%400==0)14days=29;15else16days=28;17break;18default:printf("你输入的月份不对、n”);1920)21printf("もd年,%d月,有吿d天",year,month,days);22return0;23}问题1.删去第18行是否可以?好不好?为什么?问题2.从第12行到第!7行是一个什么样的结构和执行流程?答:总体是ー个case下嵌入的处理流程,其中包含一条带else的if语句,if语句后是一条break语句,可跳到switch语句后的下一条语句。习题.不用逻辑表达式而仅用关系表达式,用以上的两种方法来做此题(参见求建筑物的高度,可以参看李学武编著的《中学生C语言入门演练100例》中的相关例题)。2.8提高部分:・注意:对于C语言来说,其实并不要求if后面的表达式为布尔表达式。而可以是任何其它类型的表达式。但对任何其它类型的表达式,在此处也是作为布尔表达式来对待(或看待)的:表达式的计算结果如果为非〇则表示逻辑值‘真’,表达式的计算结果如果为〇则表示逻辑值‘假‘。换句话说,在C语言中,任何表达式都可以出现在流程控制语句中(在所有选择结构和下一章要学的循环结构中),作为布尔表达式来使用。任何其它表达式被作为布尔表达式使用时的情况请看下表:任何其イ也表达式原来结果:非。原来结果:。表示的逻辑值:真表示的逻辑值:假表达式的最终取值:1表达式的最终取值:。例如算术表达式x+3,假设x的值为ー6,则表达式的原来结果为ー3,这是非〇,则表示逻辑值‘真’,因此将表达式x+3作为布尔表达式使用,它的最终取值是1而不是ー3。再例如x=3.7是赋值表达式,赋值表达式原来的取值就是保存在赋值号左边变量的值3.7(参见上一章提髙部分:赋值表达式),由于值3.7是“非0”,表示的是逻辑值'真,,最后该表达式的取值为1而不是3.7了。注意:千万不要把if(x=3.7)

88写成为if(x=3.7)因为后ー个式子是永远为其的。比较好的办法是把相等判断的关系表达式写成:if(3=x)〃即把常量写在相等判断运算符的左边这样ー来,如果你不小心写错了的话,成为:if(3=x)这种错误编译程序会帮助我们找出来。注意:在书写相等关系表达式时,最好要写成常量==变量的形式。点评:虽然C语言允许用任何表达式来代替实现流程选择的布尔表达式,但本书不提倡这种替代用法。•条件运算符:二选一的选择结构,除了可以用有else的if语句外,还可以用条件运算符来实现。它的形式为:表达式1?表达式2:表达式3执行步骤如下:先计算表达式1:如果为表达式1真,则将表达式2的值作为整个条件表达式的值,否则,将表达式3的值作为整个条件表达式的值。问题:请问以下赋值语句执行后,变量min的值是多少?假设x=7.4y=5.6min=x<=y?x:y;答:查找附录D可知:条件运算符的优先级比赋值运算符高,比关系运算符低,所以此句相当于:min=((x<=y)?x:y;)由于x的值比y的值大,所以关系运算(x<=y)的值为假,所以条件表达式3的取值(即y的取值)5.6就作为了整个条件表达式的值。语句变成为min=5.6;这样一条赋值语句。«逻辑表达式的短路运算:逻辑表达式如果是以“与”运算&&为主构成的,在算出其中之一为假时,由于已经可以得到整个表达式的值为假,不再往下进行运算;如果是以“或”运算||为主构成的,在算出其中之一为真时,由于已经可以得到整个表达式的值为真,不再往下进行运算。这样ー来,表达式中有些有副作用的部分,可能会得不到执行。从而影响计算结果。这也是本书不提倡使用具有副作用的表达式的重要原因。2.9选择结构的常见错误1.if语句的常见错误(1)if语句的布尔表达式后面加了分号,变成:if(布尔表达式);,多了一条空语句,导致程序流程完全错误(无else时)或通不过编译(有else时)。(2)if语句的布尔表达式整体上没用括号括住,例如将if((x<=l)&&(x>3))写成了if(x<=l)&&(x>3)o(3)关系表达式用错,将if(x=3)错写成if(x=3)(最好的形式是if(3==x)),if(y<=6)错写成if(y<=6)(在<和=之间多了一个空格)。(4)if语句的关键字用错,将if写成IF,或将else写成else。

89(5)在if语句中的ー个位置嵌入了多条语句,但没有用花括号括住以构成复合语句。例如:if(表达式)语句1;语句2;else语句3;语句4;语句5;正确的应当是:if(表达式)(语句1;语句2;}else(语句3;语句4;}语句5;(6)将嵌套关系的if语句错用成并列关系的if语句,或将并列关系错用成嵌套关系。(7)else分句与哪个if配对的关系搞错。(8)逻辑表达式用错,例如将if((x<=3)&&(x>l))错写成if(3>=x>l)。(9)在应该用有else的if语句时,用了无else的if语句,或者正好相反。1.switch语句的常见错误(1)漏写了本来应当有的break,导致流程错误。(2)在switch(整型或字符型表达式)右边多加了分号。(3)漏写了switch语句本该有的ー对花括号。(4)switch后面的表达式用了实型表达式。(5)在“case具体取值:”中漏掉了冒号,或错用逗号、分号带替冒号。(6)在不应该省略default分句时,省略了该分句,导致程序的健壮性差。

90第三章循环结构程序设计让每件事尽可能简单,但又不简单过度。——爱因斯坦3.1while语句:在许多复杂的问题中,常常需要做大量类似的计算处理,如果要把这些大量类似的计算处理的每一步都写成语句,并输入到计算机中,其工作量是相当大的,有时甚至是不可能完成的。while语句用来告诉编译程序,根据某个布尔表达式的运算结果进行下ー步的处理流程选择,是执行新的处理流程还是再一次执行刚刚运行过的处理流程。高级程序设计语言中三大基本结构的最后一种是循环结构(前两种是顺序结构和选择结构),包括while循环、d〇…while循环和fbr循环三种。下面先介绍while循环。只要把while循环彻底掌握好了,其他两种循环都很容易掌握和运用。类似于无else的if语句,while循环语句也是ー种构造型(即容器型)语句。计算机执行这种语句时,首先要计算ー个布尔表达式,然后根据表达式的不同取值进行处理流程的选择。如果表达式取值为“真”,则选择执行循环语句中嵌入的一条分句1,这条被嵌入的分句又被称为循环体。如果表达式取值为“假”,则跳过循环体不做,而是执行循环语句后的下一条语句2。其形式如下:while(布尔表达式)while语句开始处分句;while语句结束处语句2;while语句结束后的下一条语句while语句与无else的if语句的不同之处在于,循环体一旦执行完,计算机又会市新回过头去,继续计算布尔表达式的值,用以决定是否还要再次执行循环体的分句。通常情况下,被嵌入的分句往往也是一条(包含多条分句的)复合语句,这多条语句也被称为循环体。因此,while循环语句的ー•般形式为while(布尔表达式)//循环语句开始处(循环体}//循环语句结朿处循环后的ド一条语句;当布尔表达式的值为“真”时,循环体中的语句将被反复执行。while语句的执行流程如图4.1所示。注意while后的布尔表达式要用圆括号括住,布尔表达式后不能有分号。此外,循环体如果是一条语句,最好也用花括号括住,构成复合语句。虽然在循环体是一条语句的时候可以不用花括号。此外,要把循环语句从整体上看成是1条语句,虽然在循环语句中,可以嵌入一条简单句或一条复合语句作为循环体。

91点评タ如果不把循环语句整体上看成是一条(构造型)语句,在学习后面的多重循环时,就有可能会产生理解程序运行流程上的困难。循环语句的使用,可使编写程序的工作量在某些情况下极大地减少。只需构造出ー些可以反复执行的相同的语句组,把这个语句组作为循环体,放到while语句中,并在关键字while后面加上循环执行的条件(注意,这个条件要用圆括号括住,并且这个条件一般要转换成布尔表达式)即可。问题.循环语句本身是否会改变变量的值?答:循环语句只是先计算ー个布尔表达式(除非这个表达式是具有副作用的表达式),然后根据表达式的值为“真”还是为“假”,来决定是否执行循环体(for循环和while循环)。一般情况下,只有循环体中嵌入的语句,オ有可能改变变量的值。第12讲求整数1到100之和类型必修题开拓思路题趣味性・难度**算法分析如下:此问题如果简单地用以下的赋值语句表示。sum=l+2+3+4+,,,+99+100;注意,不能用省略号,每个加数都要出现在表达式中。此算法确实能解决这个问题,但此算法是非常愚笨的。首先,如果此问题变成1到10万之和时还用此类方法,那编程就变成一项使人绝望的体力劳动。那么,能否用100个变量呢?这100个变量分别用标识符〃1,〃2,"3,”4,…,nlOO来表示,然后通过输入函数调用,从键盘上依次输入这100个变量的值,最后通过赋值语句sum=nl+〃2+"3+…+”100;来解决此问题呢?这种方法更不可取。这种问题最好用刚学过的while循环语句来做。先看如下的算法。(1)sum=0;/*sum是存放前i项数值之和的变量・/(2)i=l;(3)sum=sum4-i;(4)i=2;(5)sum=sum+i;(6)i=3;(7))sum=sum+i;(200)i=100;(201)sum=sum+i;(202)输出变量sum的值;通过这100组语句(从2到201句),一定能求出1到!00之和。因为任何ー组语句执行完的时候,变量sum都是数列1、2、3、100的前i项之和。通过分析,可以把从第4句开始的所有偶数句(除了第202句外)改为》=1+1;,并在第201句之后再插入一句:i=i+l:。注意,此句并不会影响变量sum的最后取值,但对构成循环体是很重要的ー个环节,可使外观形式相似的语句组,变为外观形式完全相同的语句组。这样除了第1句、第2句和第203句外,所有语句组(两条构成一组)的外观形式都相同了。

92程序变为:(1)sum=0;/・前i项和的变量・/(2)i=l;/・变量初始化*/(3)sum=sum4-i;(4)i=i+l;(5)sum=sum+i;(6)i=i+l:(7)sum=sum4-i:(8)i=i+l;(199)sum=sum+i(200)i=i+l;(201)sum=sum+i:(202)i=i+l;(203)输出变量sum的值;经过上述改造后,就很容易将其改为用循环语句来表达的形式。注意:执行第201行的语句sum=sum+i;时,i的值为100。由此可以得到循环体(即所有相同语句组)执行的条件是i<=100»ー级算法:sum=0;/・前i项之和的变量sum清零・/ェ=1;while(i<=100){sum=sum+i;1=1+1;)输出变量sum的值;这个算法的执行流程与前面不使用循环的顺序执行流程不同(因为包含了跳转),但除了计算布尔表达式和根据布尔表达式的值来决定是否继续执行循环体(还是跳转到循环后的下一条语句)外,所执行的赋值语句组其实是完全ー样的,得到的变量sum的结果也是ー样的。但程序的书写方式,由于使用了while循环结构而得到了最大程度的简化。本题体现出了循环语句的巨大威力和使用技巧。点评タ编写循环语句的思路要点就是:使形式略微不同的语句组,转变成形式完全相同的语句组;然后把其中的ー个语句组作为循环体,再加上语句组(循环体)执行的条件(布尔表达式),这样即可构成循环。转换为C语言的程序如下:1#include2intmain()3{

931inti,sum;2sum=O;3i=l;4while(i<=100)5{6sum=sum+i;7i=i+l;8)13printf(',和为%(1",sum);14return0;15)问题1.漏掉第5行,结果会怎样?答:初始化sum错,最终的sum值不对。问题2.漏掉第6行,结果会怎样?答:初始化i错,循环次数不对。问题3.将第7行写成While(i<=100)结果会怎样?答:关键字while用错,所有关键字的字母都必须小写。问题4.将第7行写成whilei<=100结果会怎样?答:在C语言中,while后面的布尔表达式必须用圆括号括住。问题5.将第7行写成while(i<100)结果会怎样?答:循环次数少了一次,这种错误很容易犯,所以在写布尔表达式时,特别要仔细,最好进行ー些验证。问题6.将第7行写成while(i<=100):结果会怎样?答:语法检查无问题,但这是无限循环。循环体是一条以分号结尾的空语句,第8行到第1I行的复合语句则度成循环后的下一条语句。问题7.将第7行写成while(i<=100)结果会怎样?答:错。关系运算符‘‘<="之间不能有空格,也不能反过来写成“=グ’,所有的关系运算符都是如此。问题8.可以将第7行称为第7句吗?答:不可以。因为while循环语句是ー种构造型的语句,虽然在while循环语句中可以(也必须)嵌入一条称为循环体的复合语句,但本题中的一条循环语句是从第7行开始,到第11行结束的。问题9.漏掉了第10行的句子,结果会怎样?

94答:无限循环。因为不论重复多少次,i的值永远为1。问题10.将第9行的语句错写为sum=sum+l!结果会如何?答:结果为100,因为循环执行了100次,每次sum的值都增加1。问题11.漏掉了第8行和第!1行的花括号,结果会如何?答:无限循环。因为语句i=i+l;成为循环后的下一条语句。点评タ由上述问题可以发现,初学者编写循环程序时,需要特别细心,稍有不慎就会出错。需细心体会上述各个问题,避免犯上述错误。也可以在编完循环语句后,查看章最后介绍的常见错误,以排除错误。3.2数列求和算法知识简介本节将介绍数列求和问题的算法(其中包括while循环语句与if语句加往前跳转的goto语句构成的循环的关系讨论)。任何数列求和问题,都可归结为ー个学生团队郊游到果园摘橘子的ー类问题。可以要求所有的学生摘一定数量的橘子,每个学生都有一个编号,这个编号是从1开始的,如果有100个学生,那么学生的编号就是从1到100。现在要求每个学生摘橘子的数量为其编号数,即第,・个学生摘i个橘子。为了收获并统计全班学生所摘橘子的总数,可采用如下方法:在果园门口放ー个大筐,然后由班主任要求学生按自己的编号顺序,依次把橘ア扔进大筐中。为此,写出ー个统计橘子总数的方法L如下:(1)清空大筐(sum=0;)o(2)班主任的手指指向第i(,=1;)个学生。(3)被指的学生将橘子扔进大筐(sum=sum+i;)。(4)班主任指向下ー个学生(,=,+1;).(5)如果班主任指的学生还有的话(沁=100),跳转到第(3)步。(6)到此为止,筐中的橘子数就是全班学生所摘橘子的总数。用更精确的算法表示如下:1.sum=0;2.i=l;3.sum=sum+i;4.i=i+l;5.如果iv=100那么跳转到36.输出sum;注意除了第(5)步在满足条件的情况下会跳转到第(3)步外,这6步都是按照顺序依次执行的。/・清空大筐(sum表示前i个学生扔下的橘子数)*//・班主任的手指指向第一个学生・//・被指的学生将橘子扔进大筐・//・班主任指向下ー个学生*//・如果班主任指向的学生还有的话,跳到第3步・//・至此筐中橘子数就是全班学生所摘橘子的总数・/转换成C语言的程序如下:1#include2intmain()3(

952intsum,i;34sum=O;5i=l;6loop:sum=sum+i;7i=i+l;8if(i<=100)gotoloop;9printf(”所摘橘子总数是%d”,sum);1011return0;12)if语句执行时,只要关系表达式i<=100为“真”,就会跳转到语句标号为1。”的那条语句处执行。注意:此程序和第11讲求1到100之和的问题,其实是同一个问题的不同编程解决方法,而且这两个程序的流程基本上是ー样的。唯一不同的是,前ー个程序While循环是先判断(先执行布尔表达式),然后决定是否执行循环体;而后一个例子中的if语句加无条件跳转,是先执行循环体,然后进行判断.以决定是否继续循环。if语句加上往回跳转的goto语句所构成的循环,与d〇…while循环在翻译成机器指令时,两者的指令流程是完全相同的,并没有区别(参见后面d。…while循环)»在C语言中,goto语句是一种跳转语句,这种语句有对应的机器指令,要与语句标号结合起来用,格式如下:loop:语句1;语句2;语句n;gotoloop;语句n4-2;语句n+3;这些语句的执行流程为:从语句1开始,顺序执行到语句〃(这”条语句是一个基本处理流程1,goto语句后的语句是另ー个基本处理流程2),然后执行goto语句,又无条件地跳回到语句1«注意:语句1的前面使用了标号!oop(标号要求是一个标识符),标号后面一定要有冒号。这样就构成了一个无限循环,也称为死循环。基本处理流程2(语句n+2及后面的语句)不会得到执行。要想上述结构不成为死循环,只需在无条件的goto语句前加上一个if语句,将上述语句段改为:loop:语句1:语句2;语句n;if(布尔表达式)gotoloop;语句h+2;语句h+3;

96这样,goto语句便成为嵌入在if语句中的分句。只有在布尔表达式为“真”的情况下,goto语句オ会得以执行。这种结构中,在语句1到语句"之间,如果至少有一条语句(在循环执行若干次后)能够使得布尔表达式为“假”。这就构成了一种正常的循环结构。所谓循环结构就是这样的一种结构,它可以在某个条件为“真”的情况下,重复执行ー个基本处理流程(即循环体),在这个条件为“假”时,才执行下一个基本处理流程。这种用if语句加上无条件的往回跳转语句所构成的循环结构,其实与d〇…while循环的流程结构是完全一样的。do{语句1;语句2;语句3;语句n;}while(布尔表达式);语句n+2:语句n+3;d〇…while循环的流程为:先执行循环体(这是ー个由语句1、语句2、…、语句"构成的复合语句),然后计算布尔表达式,其值如果为“真”,继续执行循环体,其值如果为“假”,则执行循环语句后的下一条语句。・习题,对于求I至U100之和的问题,用if语句加上goto语句,构造出与while循环语句的流程完全一致的循环结构。答:i=l;if(i>100)gotoloop2loopl:sum=sum+i;i=i+l;if(i<=100)gotoloopl;loop2:printf("所摘橘子总数是之d",sum);点评タ虽然if语句加上无条件跳转语句可以构成任何类型的循环,但任何程序都可以只由顺序结构、选择结构和循环结构语句组成,完全可以不使用goto语句。过多地使用goto语句是造成程序流程混乱的根源所在。此处讨论有两个目的:一是可以加深对循环语句本质的了解;二是在很难写出标准循环语句的不得已的时侯,用这种循环来救急。第13讲求数列1,3,5,99各项之和类型必修题开拓思路题趣味性・难度**数列的求和问题一般都要设三个变量:前,・项之和的变量为sum,第i项用ー个变量i来表示,第i项的数值用变量"来表示,见表4.1。表4.1各变量的取值

97用变量i来表示当前项123450用变量〃来表示当前项的值135799用变量sum表示前i项之和14916?注意,任意第i项的〃值是前ー项的〃值加2。ー级算法:(1)前i项之和的变量sum初始化为零;/・前i项之和初始化・/(2)当前项i为第・项;/・循环变量初始化・/(3)数列中第工项目前的值为れ=1;/*第エ项的数值れ初始化・/(4)while(i<=50)(sum=sum+n;/・将当前项的值加到变量sum屮*/i=i+l;/・当前项变为下ー项・/n=n+2;/・下ー项的数值为前ー项的数值加2»为下一次求和作准备*/}(5)输出sum的值:转换为C语言的程序如下:1#include2intmain()3(4intsum;/・前工项之和*/56inti,n;7sum=0;8i=l;9n=l;10while(i<=50)11(12sum=sum+n;/・更新前•£项之和・/13i=i+l;/・更新i的值・/14n=n+2;/・更新n值,用新老n之间的关系・/15)16printf(''sum=%d

98w»sum);17return0;18}为了验证循环程序的正确性,可以在循环体中再加上一句,在每一次循环时都把各个变量的值打印出来,看看是否和表4.1中的值一致,程序变为:1#include2intmain()3{4intsum;/・前i项之和・/5inti,n;67sum=O;8i=l;9n=l;10while(i<=50)11{12sum=sum+n;/・更新前工项之和・/

9911printf(wi=%d,n=%d,sum=%d

100w»i,n,sum);12i=i+l;/*更新上的值・/13n=n+2;/・更新ハ值,用新老れ之间的关系・/ェ6}1718printf(wsum=%d

101w,sum);19return0;20)程序的运行结果为:i=l,n=l,sum=li=2,n=3,sum=4i=3,n=5,sum=9i=50,n=99,sum=点评タ此题主要用的是通项变量〃的新值和老值之间的关系来进行计算,任何数列求和问题,循环体中的语句sum=sum+n;/・新的前i项和与老的前i项和之间的关系・/i=i+l;/・求和进入下ー项・/这两条语句都是ー样的。只有求出通项变量n的新值的赋值语句,不同的应用问题是不ー样的。点评.タ脚手架代码在循环体中第13行加入的打印语句,可把每次执行循环后的值,在显示屏上显示出来,这对于调试循环语句很有帮助,极力向同学们推荐这种调试程序方法。这一行代码又可称为“脚手架代码”:由于此代码在程序编写中的主要作用,是在编写程序期间,帮助我们验证程序是否正确无误。此行代码在程序编好以后,可以删除掉(用编译预处理给出的方式,可以自动让这种代码行不起作用,参见第8章)。这类代码的名称由此而来。不过,这种调试程序的数据,有时会与程序运行时的正常数据输出混合在ー起,引起不便。在这种情况下,我们可以考虑将脚手架代码修改一下,令调试的数据不是在屏幕上输出,而是输出到某ー个文本文件中(参见第8章8.4节)。(相关参考:参见第8章中编译预处理的内容;画内存变量取值变化图参见第34讲。)小结:在数列求和的编程问题中,求通项变量〃的新值有两种方法。ー种是利用变量〃的新值和老值之间的关系,另ー种方法是利用变量〃与数列项变量i之间的关系。对于此题,通过仔细观察表4.1,可以看出,当前项i与当前项的值〃之间存在以下的等值关系。〃=2サノ所以,本题的数列求和也可以用如下的程序实现。1#include

1021intmain()2{3intsum;4inti,n;56sum=O;7i=l;8n=l;9while(i<=50)10{11sum=sum+n;12printf(wi=%d»n=%d,sum=%d

103w,i,n,sum);13i=i+l;15n=2*i-l;ェ6)1718printf(''sum=%d

104w,sum);19return0;20)程序的运行结果为:i=l,n=l,sum=li=2,n=3,sum=4i=3»n=5,sum=9i=50>n=99,sum=问题.如果把第13行和第14行的句子顺序颠倒ー下,结果是否正确?答:错误。第14讲求17/3+1/5-1/7+1/9…共50项之和类型选修题趣味性・*难度**算法分析:此数列求和问题的关键,是如何求出通项〃。可设置ー些辅助变量,辅助变量k用来表示第i项的正负号な的初值为1,在循环体中新的k值与老的k值之间的关系为た二けた(也可写为れ/,这是因为C语言的运算符中有取负数运算符即负号‘'-")。辅助变量m用来表示第i项的分母值,可以看出m的新值和老值之间的关系为机ラn+2;,的初始值为1。有了这两个辅助变量k和m,就可以很容易求出第i项的通项变量的值"=1.〇・A/m;于是,程序就很容易写出。ー级算法:(1)sum=O;(2)i=l;(3)k=l;(4)m=l;(5)while(i<=50)5.1n=l.O*k/m;5.2sum=sum-i-n;5.3k=-k;5.4m=m+2;5.5i=i+l;}(6)打印sum:转换为c语言程序如下:

1051#include2intmain()3{4floatsum,n;5intk,m,i;6sum=0;7i=l;8k=l;9m=l;10while(i<=50)11{12n=l.0*k/m;13sum=sum+n;14k=-k;15m=m+2;16i=i+l;17)18printf(x'sum=%fw,sum);19return0;20)问题1.如果变量sum和〃定义为整型,是否正确?答:错误。问题2.如果将赋值语句〃=1.0*k/m;改为n=k/m是否正确?答:错误,原因是&和,"都是整型变量,那么,幼”的运算就是整除运算,而不是实数除法运算,所以,在"I比1大时得到的运算结果就是〇。习题1.编程求2/34/4+4/5-5/6+6/7-共50项之和。习题2.编程求1+1/4-1/7+1/10+1/13-1/16+共20项之和。(提示:将数列每ー项的正负号、分子、分母分开考虑,考虑正负号k与第i项之间的关系。注意:只要i能被3整除,k就应当是ー1;否则k应当是1)。第15讲输出1〜999中能被3整除,且至少有一位是5的所有整数类型必修题算法类型穷举法趣味性・难度**ー级算法:i=l;while(i<=999)/*遍历!〜999中的所有数・/(分解出i的百位n3、十位n2、个位nl;①如果(i满足条件)②打印此数i;i++;}其中①和②步需要进ー步求精。①的二级求精:n3=i/100;n2=(i-n3*100)/IO;nl=i%10;②的二级求精:i能被3整除;i%3==0«

106i中至少有一位是5:(n3==511112==5||nl==5)〇由于这两个条件要同时满足,所以只需将以上的两个表达式用逻辑运算符“与”运算符连接起来即可。(i吿3==0)&&(n3==5||n2==5||nl==5)转换为c语言程序如下:1#include2intmain()3{4inti,n3,n2,nl;5i=l;6while(i<=999)7(8/・分解出i的百位n3、十位n2、个位nl;*/9n3=i/100;10n2=(i-n3*100)/10;11nl=i%10;12if((i%3==0)&&(n3==5||n2==5||nl==5))13printf(''%drw,i);14i++;15)16return0;17)问题.将第!2行的逻辑表达式改为:(i%3==0)&&(n3==5||n2=5||nl=5)»是否可以?答:不可以,在C语言中,if后面的布尔表达式从整体上一定要用圆括号全部括住。习题1.输出1〜999中能被3整除,或者至少有一位是5的所有整数。习题2.求1-999之间用5除余3并且用7除余4的所有数。第16讲用蒙特卡罗法(即概率法)求圆周率类型必修题开拓思路题算法类型概率算法(又称为随机投点算法)趣味性**难度**算法分析:利用随机函数求兀的值。将随机数的前后两个值(均在0〜1之间)作为点的x,y坐标,统计1万个点中,落入半径为1的四分之一圆内的点数〃,从而计算出四分之一圆的面积6。

107s/边长为1的正方形面积2”ハ0000(4.1)随机落点越多,式(4.1)越接近于相等。又知由于四分之一圆的面积为:s=(it/4)rXr=7t/4(由于!=l)(4.2)由式(4.1)和式可以算出兀的值,如下:兀=(4Xn)/10000图4.2用蒙特卜罗法求圆周率此方法又称为蒙特卡罗法,即概率算法,如图4.2所示。ー级算法:n=s=0;while(s<=10000){2.1得到两个(均在〇〜1之间)随机数x,y;2.2如果x,y在四分之一圆内那么园内的落点数n=n+l;2.3s=s+l:/・累计总落点数・/}pi=(4*n)/10000;输出pi的值:转换为C语言的程序如下:1#include2#include3intmain()4(5doublex,yzpi;6intn,s;7n=s=0;8while(s<=10000000)9{10x=rand()/32767.0;11y=rand()/32767.0;12if((x*x+y*y)<1)13n++;14s++;15}16pi=(4.0*n)/10000000;17printf("圆周率约等于%If”,pi);18return0;19)C语言中有一个库函数rand(),每次调用此库函数,都可以得到取值在〇〜RANDMAX(在VC++6.0中,这个系统定义的符号常量RANDMAX的值为32767)之间的ー个整数,而且所有这些整数出现的几率都是1/(RANDMAX+1),所以把这个函数称为(伪)随机函数。如果我们要得到〇〜机之间的随机数,可以将山此函数得到的数再取除以,”的余数,即rand()%m。要注意的是,在调用此函数时要包含头文件math.h。・习题.求定积分:设イ(x)是[0,1]上的连续函数,且OWf(x)W1,需要计算积分值六/f(x)dr,并计算积分/的面积G。利用随机投点法来做此题。(没学过定积分的读者不必做此题。)心点评タ从以上的例题和习题可以看出,随机投点法的应用范围很广泛。可以通过在两个图形(其中一

108个简单封闭图形包含一个复杂封闭图形)面积上的随机投点,求其中一个复杂图形的面积。求解方程式为:简单图形面积:复杂图形面积=总落点数:复杂图形中落点数所以:复杂图形面积纟简单图形面积X(复杂图形中落点数/总落点数)落点数越多越接近。第17讲输入10个数,求其中的最大数、最小数及平均值类型必修题趣味性・难度**算法分析:首先输入第一个数到变量x,将它作为比较基准,并假设它既是最大数max,又是最小数min。然后在循环中重复用变量x依次输入第2,第3,第4,…第10个数的值,将此x的值与max比较,如果任何ー次比较中x>max,说明假设的最大值是不对的,要用新的x值作为当前已比较过的数的最大值max,对于min变量也是类似的。在进行每一次比较后,都要确保max是当前,•个数中的最大数,确保min是当前i个数中的最小数。通过9次比较后,变量max就是10个数中的最大数,变量就是10个数中的最小数。ー级算法:输入第•个数到X假设第一个数为最大数max假设第一个数为最小数min求和变量初始化为sum=x;循环变量i=l;while(i<=9){读入第i+1个数到变量xsum=sum+x;if(x>max)max=x;if(x2intmain()3(4floatx,max,min,ave,sum;

1091inti;2/・输入第一个数x*/3printf("请输入第一个数、n");4scanf(''%fw,&x);5/・假设第一个数为最大数max*/6max=x;7/・假设第一个数为最小数min*/8min=x;9/・求和变量初始化为x*/10sum=x;11/・循环变量初始化・/12i=l;13whilemax)20max=x;21if(x

110错误与while完全类似,这里不再赘述。问题:以下的dO…while循环语句:do循环体;)while(布尔表达式):是否可以省略花括号写成:循环体:while(布尔表达式);答:一般不可以,除非循环体中只有一条语句。练习1.请你将用while循环的求1到100之和,按上述步骤改写成do~while循环。练习2.请你将前面用while循环做的例题选择至少两题,改为do-while循环。第18讲将一个十进制正整数d转换为〃(〃く9)进制数类型必修题趣味性**难度**算法分析:参考第1章十进制数转化为2进制数的方法,将10进制转化为任意n进制的算法也是类似的。将十进制数转换成n进制数的短除法:(见第1章P页,只需将其中的2换成n)短除法把要转换的数不断的除以n然后取余数,商作为新的待转换的数:当商为〇的时候结束。然后把余强倒着写出来。ー级算法:1.读入一个10进制数d2.读入要转换的数制n3.while(d>0){求d除以n的余数,将其打出;/・得到ー个转换后的低位n进制数*/d/n->d;/・得到新的d,为求下ー个余数作准备。*/)4.1.束1#include2main()3{4intd;/*原十进制整数・/

1111intn;/*n进制数,nW9*/2printfT请输入ー个大于0的十进制整数

112");3scanff'%d”,&d);4printfT请输入要转换成几进制数?输入的整数要小于10

113M);5scanf(,,%d,\&n);6while(d>0)H{12printf("%d",d%n);13d=d/n;14}/*endwhile*/15}・习题1:此题转换后的n进制的数值在一行上要倒过来读;你能否修改程序,让其按自然顺序由高到低显示出来?(提示,在循环前,可以先输出ー些个空格,然后在循环中,每输出一个值就退两格)参考答案:程序改为:1#include2main()3{4intd;/・原十进制整数・/5intn;/*n进制数,nW9*/6printf("请输入一个大于〇的十进制整数

114");7scanf("%d",&d);8printf("请输入耍转换成几进制数?输入的整数要小于10

115");9scanf("%d",&n);10printf("ヽt\t\t\t\t");/・注意这一行是新加的,将光标右移5个水平制表符的位置・/11while(d>0)12{13printf(',%d\b\b,,,d%n);/*每打印ー个值,退两格。退一格不对。*/14d=d/n;15}/*endwhile*/16printfTXn");/・换一行留给系统打印信息,否则,pressany…会将山效数据复盖掉*/17)此题用到的转义字符\t和也,请看第二章提高部分。・习题2:在学完数组后,请将此题修改为把转换后n进制的各位数存放到ー个整数数组中并输出。点评タ将10进制数转换为任意其他进制数,在程序设计中,是ー种必须掌握的常用编程技巧。因为在将多重循环转换为二重循环时,这是必要的知识。参见第63讲盒子里的气球。习题1.本讲假设了d是正整数,请将一个十进制整数d转换为〃(〃ぐ9)进制数,d可以是负数。・习题2.请将一个十进制整数d>0转换为十六进制数,利用英文字母A、B、C、D、E、F来分别表示十六进制中数值相当于十进制的10、11、12、13、14、15。在十六进制中,这

116些都是个位数,不需进位。表4.2十六进制和十进制数的对照表16进制的数相当于10进制的数16进制的数相当于10进制的数16进制的数相当于10进制的数II77C122288D133399E1444A10F1555B11101666*・习题.将一个4位的"W9进制数转换为十进制数。提示:这个n进制数的数制就是变量n的输入值。而这个n进制数的各位上的数是以十进制整数格式输入的,即如果从键盘输入的n为7,那么输入的这个整数值的每一位都不能比6大(七进制数每位最大的数值是6)«也就是说,如果输入3026,就把它当作七进制数(3026)7I必须将此数的每一位都利用整数除法“/”或整数取余“%”分解出来,然后利用以下的公式m=3/73+0x72+2x71+6x70这样才能得到这个十进制的数。・程序阅读题:以下程序是否能完成上述任务?谁优谁劣?(此题请在学完数组后再做)1#include2main()3{4intd;/・原十进制整数・/5intn;/*n进制数,nW9*/6inti;7do8(9printf(”请输入一个大于0的十进制整数、n");10scanf(•'%dHz&d);11)12while(d>0);1314printf(”请输入要转换成几进制数?输入的整数要小于10

117”);15do16{17printf("你输入的转换数制不符合要求,请重新输入、n”);18scanf("%dn,&n);19}

11820while((n<=0)||(n>9));21i=0;22while(d>n)23{24mun[i]=d%n;25d=d/n;26i++;27}/*endwhile*/2829if(d>0)30num[i]=d;3132while(i>=0)33{34printf(“十进制数をd转换成了%d进制数:

119”,d,n);35printf(”%d“,num[i]);36i--;37)3839)点评タ数值的进制转换在计算机科学、编程和软件中应用广泛,读者务必熟悉。在将任意多重循环转换成二重循环时也要用到,参见例题:盒子里的气球。・程序阅读题:以下程序是否能完成上述任务?谁优谁劣?(此题在学完数组后再做)#includemain(){intd;/・原十进制整数・/intn;/*n进制数,nW9*/inti;intnum[10];do(printf("请输入一个大于〇的十进制整数、n");scanf("%d",&d);)while(d<=0);printf(”请输入耍转换成几进制数?输入的整数耍小于10

120”);scanf("%d",&n);while((n<=0)||(n>9))(printf("你输入的转换数制不符合要求,请重新输入'n");scanf("%d",&n);)i=0;printf(”卜进制数・d转换成了%d进制数:

121",d,n);while(d>=n)(num[i]=d%n;d=d/n;i++;}/*endwhile*/if(d>0)

122num[i]=d;while(i>=0)printf(n%d",num[i]);i--;))第19讲不用整除运算符“/”和取模运算符“短’,求m除以"的商和余数,其中m和〃均为整数类型选修题趣味性・难度**算法分析:设置ー个记数器变量i,,・的初值为0。如果相比〃大,就将m的值减去〃,得到新的m,并将i加1,然后继续上述过程,直到Z〃小于〃为止。ー级算法:读入m,nif(m=n)(m=m-n;i++;)输出,商为・£,余数为m结束转换为C语言程序如下:1#include2intmain()3{4intm,n,i;56/・读入m,n*/7printf(”请输入两个整数,两数之间用空格隔开、n”);8scanf(M%d%dM,&m,&n);9if(m=n)17(18m=m-n;

1231i++;2)34/・输出商为i,余数为m*/5printf(、、输出商是セd,余数是セd”,i,m);6)习题.将此题改为对负整数的转换也适用(简单的计算机可以没有乘除法指令,原因就在此题中揭密)。第20讲求任意ー个正整数〃(〃>=2)是否为素数(解法一)类型必修题趣味性**难度**算法分析:用2、3、…、n-1作为除数去除〃,只要其中有一个数能够除尽",〃就不是素数。可以用一个标志变量来跟踪,先假设〃是素数,令标志变量flag的初值为1,然后用2、3ヽ…、〃ー1(用变量i来作除数)去除〃,只要有一次〃能被,•除尽,就让flag=0。ー级算法:输入nflag=l;/・假设n是索数・/i=2;while(i<=n-l){如果n除以上的余数为零那么flag=0;/・因为n不是素数*/i=i+l;)如果flag等于1那么打印n是素数转换为C语言程序如下:1#include2intmain()3{4intn,i,flag;5/・输入n*/6printf(”请输入ー个整数、n“);7scanf(n%dn,&n);8flag=l;/・假设n是素数・/9i=2;10while(i<=n-l)11{12if(n%i==0)13flag=0;/・因为n不是素数・/14i=i+l;15)16if(flag==l)17printf("吿d是素数、n”,n);18else

1241printf("吿d不是素数、n”,n);2return0;3}问题.是否可以缩小试除的范围?答:其实是可以到sqrt(n)结束的,不必到n-1。请参考其他教材。常用的编程技巧:设置标志变量时,先假设它为“真”,当某条件成立时,再假设它为“假”,如此例中的变量flag〇点评タ此程序的效率不高,因为即使某个数〃已经被i除尽,〃已经不是素数,但只要沁=〃」为“真”,循环仍会继续。改进的方法是,可将循环的布尔表达式改为((/<=«-l)&&(flag!=0))o这样循环体执行的条件更苛刻,程序的运行效率就提高了。编程技巧:要放宽循环执行条件,用“或”运算符,要严格循环执行条件,用“与”运算符。3.4break语句和continue语句break语句不仅可以用来中断switch语句的执行,也可以用来中断ー一个循环语句的执行。continue语句只可以用在三种循环语句的循环体中,用来结束本次循环(即循环体中,continue语句后的所有语句都不执行),进入下一次循环。第21讲求任意一个正整数n(n>=2)是否为素数(解法二)类型必修题趣味性**难度**算法分析:仍然用变量i作为除数来除〃,只要〃能被i除尽,就立即用break语句跳出循环。循环结束后,可根据i的当前值来判断循环是正常结束的,还是中途被break语句中断的。如果循环结束后i的值等于〃,说明循环是正常结束的,任何i值都不能除尽n,所以〃是素数。一•级算法:输入n将2—iwhile(i<=n-l)(如果n除以エ的余数为零那么break:)如果エ等于れパ这说明if语句后的表达式从未为真,break从未得到执行^那么打印n是素数否则,打印刀不是素数转换为C语言程序如下:1#include2intmain()3{4intn,i,flag;5/*输入n*/6printf("请输入ー个整数、n");

1251scanf(w%dw,&n);8i=2;9while(i<=n-l)10{11if(n%i==0)12break;13i=i+l;14)15if(i==n)16printf(”吿d是素数、n”,n);17else18printf(、、吿d不是素数、n”,n);19return0;20)问题1.如果将第15行的if语句if(i=n)改为if(i=n)»结果会如何?问题2.如果不小心把第8行的语句改为i=l»结果会如何?问题3.如果把循环体中第“、12、13行的语句改为:if(n%i==0)break;elsei=i+l;结果会如何?问题4.如果12行错用continue语句代替了break语句,结果会如何?运行ー下,看看你的判断是否正确。第22讲模拟ー个简单计算器的程序类型选修题开拓思路题趣味性***难度***讨论:3+4*5这个式子等于多少?以前的简单电子计算器不管先乘除后加减的运算规则,而是先执行加法3+4得到7,然后再做乘法7*5,得到35,而不是23。本题就是要做一个不按照先乘除后加减的规则来进行运算的简单计算器。算术表达式中也不能使用圆括号。算法分析:本题可在第12讲的基础上更进ー步,通过编程来实现ー个最简单的计算器。由于问题较复杂,可以先考虑不用循环应该如何去做。ー级算法:读入表达式中的第一个数一X读入第一个运算符~ch如果ch不是等号继续往下,否则跳到!oop处读入表达式中的ドー个数~y根据运算符得到x与y运算的结果一result读入下ー个运算符fch如果ch不是等号继续往下,否则跳到loop处读入下一个数一y根据运算符得到result与y运算的结果一result读入下ー个运算符fch如果ch不是等号继续往ド,否则跳到loop处

126读入下ー个数一y根据运算符得到result与y运算的结果一resultloop:打印result;可以看出,只要读入的算术表达式中的下ー个运算符Ch不是等号,这个过程会一直进行下去。而且除了第一句以外,剩下的每4句ー组,每组的算法都相同,只有第一组的4句例外。但只耍把变量x改为result,那么所有的组都相同(标为黑体字的部分,进行了变换,但这种变换不会影响程序运行的结果)。读入表达式中的第•个数一result读入ー个运算符~ch如果ch不是等号继续往下,否则跳到loop处读入下一个数~y根据运算符,得到result与y运算的结果一result读入下ー个运算符fch如果ch不是等号继续往下,否则跳到loop处读入下一个数一y根据运算符,得到result与y运算的结果-result读入下ー个运算符fch如果ch不是等号继续往ド,否则跳到loop处读入下一个数一y根据运算符,得到result与y运算的结果一resultloop:打印result这样就为构成循环创造了良好的条件。但此题与前面讲过的循环题有所不同。第一,因为循环的次数不是固定的,应当在读入的运算符是等号“=”时结朿循环。第二,循环变量不是整型变量,而是字符变量ch。因此,在构成的循环体前,对循环变量ch的初始化不能少。为此,耍将第一个分组中的第一句取出来,使之成为循环前的循环变量初始化语句。要重新修改语句分组,并在最后一个分组的最后加上一句:'‘读入表达式中的下ー个运算符fch”,但增加此句并不会改变result的最终取值,最终形式如下:读入表达式中的第一个数•result读入第一个运算符~ch如果ch不是等号继续往ド,否则跳到loop处读入表达式中的ドー个数一y根据运算符,得至リresult与y运算的结果—result读入表达式中的ドー个运算符一ch如果ch不是等号继续往下,否则跳到loop处读入表达式中的下•个数~y根据运算符,得到result与y运算的结果一result读入表达式中的下•个运算符jch如果ch不是等号继续往下,否则跳到loop处读入表达式中的下ー个数fy根据运算符,得至リresult与y运算的结果一result读入表达式中的ドー个运算符chloop:打印result至此,准备工作已经全部完成。使用循环后的ー级算法:读入表达式中的第•个数--result

127读入表达式中的第一个运算符fchwhile(ch不是等号,=()(读入表达式中的下ー个数一y根据运算符,得到result与y运算的结果一result①读入表达式中的下ー个运算符fch}打印result的值其中只有①步需要进一步求精(参见第12讲)。根据运算符得到result与y运算的结果一result根据ch的取值执行以下的深语句(case*+,:result=result+y;break;case'-*:result=result-y;break;case'*1:result=result*y;break;case1/':result=result/y;break;)转换为c语言的程序如下:1#include2voidmain()3{4floatresult,y;5charch;6/・读入表达式中第,个数一result、第•个运算符・/7scanf(*'%f%c",&result,&ch);89while(ch!='=*)10{11/・读入表达式中的下ー个数fy*/12scanf(n%fn,&y);13/・根据运算符得到result与y运算的结果-result*/14switchch)15(16caseresult=result+y;break;17case।_•:result=result-y;break;18caset*।:result=result*y;break;19case7':result=result/y;break;20}/*endswitch*/21/・读入表达式中的下ー个运算符fch*/22scanf(n%c",&ch);23}/*endwhile*/2425printf(,,result=%f*',result);26)注意在运行此题程序输入数据时,要连着输入,其间不要按空格键,也不要按回车键。算术表达式和等号全部输完后,再按回车键即可(其原因参考下面问题2).

128问题1.如果不小心漏掉了第22行,结果会怎样?答:循环变量ch的值得不到更新,造成死循环。点评タ此题的循环变量用的是字符变量,但仍要注意对循环变量的初始化(第97行),以及在循环体中对循环变量进行更新(第22行)。*・问题2.此程序运行时,只要在运行到第7行时,一次性地在一行中输入ー个完整的不带圆括号的算术表达式,然后按回车键即可。不需要在每输入ー、两个数据后,就按一下回车键(而且这样还会出错),为什么?答:这是由于格式化输入函数scanf()是ー种带有缓冲区的输入函数(参见第8章),按回车键前输入的所有数据(字符序列)都被一次性地存放在输入缓冲区中,并形成一个标准输入流stdin,此流中的数据并没有立即被送到变量的内存区。在按回车键之后,第一个数和第一个运算符被从流中取出来。通过ー个scanf()函数,送给表达式中的第一个数和第一个运算符。但其余一次性输入的数据和运算符仍然在标准流中,等待程序中后面的scanf()输入函数按先后顺序依次读取使用,直到输入表达式的等号オ完全结束。要进ー步了解缓冲区和流的概念,可参见第8章。3.5for循环语句不论是while循环,还是d〇…while循环,最常犯的两类错误如下:(1)循环前忘记了循环变量的初始化,造成循环次数错。(2)循环体中漏掉了对循环变量进行更新的语句,造成无限循环。为此高级程序设计语言的设计者们提供了一种更好用的循环语句,即for语句。for语句的形式为:for(表达式:L;表达式2:表达式3)(循环体;}/*for循环语句结束处・/循环后的ド一条语句;fbr语句的执行流程如下:(1)先执行表达式1(表达式1用来进行循环前的初始化操作)。(2)然后计算表达式2(表达式2就是布尔表达式)(3)如果表达式2的值为“真”,那么执行循环体;否则(结束循环语句的执行)跳转到循环后的下一条语句(即第(6)步)。

129(4)执行完循环体后,执行表达式3。(5)跳转到第(2)步。(6)执行循环后的下一条语句。for循环的流程如图4.4所示。初看起来,for循环的流程似乎十分复杂,难以记住和运用。其实,如果找到了窍门,它是很容易记住和运用的。为此,先回顾一下while循环语句的结构,如下:循环变量初始化的语句;while(布尔表达式)(循环体;循环变量更新语句;}循环后的下条语句{ド,条语句图4.4fbr循环的流程下面再写出while循环语句的执行流程。(1)先执行循环变量初始化的语句(虽然这个语句不属于while语句,但while少了这个句子不行)。(2)计算布尔表达式。(3)如果布尔表达式的值为“真”,那么执行循环体;否则(结束循环语句的执行),跳转到循环后的下一条语句(即第(6)步)。(4)执行完循环体后,执行循环变量更新语句。(5)跳转到第(2)步。(6)执行循环后的下一条语句。仔细对照上述两个流程可以发现,for循环语句从执行流程上看,实质上就是一・种while循环语句,只不过是ー种更不易出错的while循环语句(因为表达式1就是为循环变量赋初值:而表达式3就是对循环变量进行更新)。初学时,如果不习惯for循环,可以先写出问题的while循环语句,然后进行如ド的改造エ作。循环变量初始化的语句;/・此句移到for后面的圆括号中作为表达式・/iwhile(布尔表达式)/・此部分改为for(表达式!.;表达式2;表达式3)*/(循环体;循环变量更新语句;/・此句移到for后面的圆括号中作为表达式3*/)循环后的下条语句;注意・(1)原来while循环语句前的循环变量初始化的语句变成了for语句中的表达式1.(2)原来while语句中的布尔表达式变成了for语句中的表达式2。(3)原来while语句循环体中的循环变量更新语句(但要求是while循环体中的最后一句)变成了for语句中的表达式3.点评タ由于在for循环中强制性地要求编程者给出循环变量的初始化语句和循环变量的更新语句,所以for循环语句其实就是•种更不容易出错的循环语句。fbr循环的使用,在循环前的初始化语句是多条,而且循环变量更新语句也是多条的场合下,还能使程序更为紧凑(参见后面)。注意:如果while循环中,循环变量的更新语句不是循环体中的最后一句,在改成fbr循环

130时要小心,否则可能会出错。练习1.请你将用while循环的求1至リ100之和,按上述步骤改写成fbr循环。练习2.请你将前面用while循环做的例题选择至少两题,改为for循环。第23讲求1+22+333+4444+55555+666666…共9项之和类型选修题开拓思路题趣味性****难度***算法分析:此题初看起来,好像没有思路。根据波利亚的如何求解列表,可以考虑一个更为简单而又有些类似的问题,即求1+11+111+1111+11111+111111…共9项之和。此问题通过简化求解起来就容易多了,因为此题中通项变量n的新值与老值之间的关系非常简单,〃的初值是1,则有:”="*10+1;所以此题很容易编出如下程序:n=l;sum=0;for(i=l;i<=9;i++)(sum=sum+n;n=n*10+l;}在此题的基础上,再来考虑原题就容易多了。这里引入辅助变量m来取代简化题中的“(因为本书有个约定,在数列求和问题中,用变量〃来表示通项值)。首先写出数列的各项值,包括辅助变量m的值,见表4.3。表4.3数列的各项值数列项号变量Z123辅助变量m111111数列通项变量〃122333由表4.3很容易得到通项变量"的表达式,如下:n=m*i;通过引入ー个辅助变量,",可使求和问题很容易得到解决。算法如下:sum=0;//Im=l;//2i=l;//3n=m*i;//4while(i<=9)//5sum=sum+n;//6

131//7//8〃9m=m*10+l;i++;n=m*i;输出sum;习题1.此题请读者自己根据算法,写出C语言的程序。问题1.算法第8句和第9句是否可以颠倒次序?答:不可以。这两句之间存在依赖关系——第9句依赖第8句,如果颠倒次序,通项变量n的取值不对。注意:凡是存在依赖关系的语句,是不能语句颠倒次序的。问题2.如果不小心漏掉了第4句,结果会怎样?答:第一次循环时,执行第6句时加到变量sum上的n值是垃圾数据,n没有得到正确的初始化。问题3.此题的算法是否可以改成如下形式?sum=O;m=l;i=l;while(i<=9)(n=m*i;sum=sum+n;m=m*10+l;i++;)输出sum;答:可以,这种形式更为简洁。因为在原来的方案中,最后一次执行循环的第9句没有用上。而且,此题的形式改用fbr循环也更容易。点评,编程思路之一如果解某个题没有思路,可以考虑将其简化,转换成一个更为简单的题。更为简单的题一旦获得解决,对解决原题可能有很大帮助。本题求1+22+333+4444+55555+666666…共9项之和,由于被转换成求1+11+111+1111+11111+111111…共9项之和,问题变得容易多了。把ー个难题分解成了两个较易解决的问题。・习题2.求n+nn+nnn+nnn共10项之和,nW9,n的值由用户在程序运行时输入。*・习题3.求xl/3!+x5/5!-//7!+…直到所求项的绝对值小于10的负6次方。要求读者不用pow()函数来做。提示将分子、分母、系数分开来考虑。分母可先考虑如何求1!、2!、3!、4!......之和。点评.タ关于多项式的求和在高等数学中证明了许多函数可以用多项式的无穷级数来展开,因此,能解此题的读者可以利用此题来求许多更杂函数的进似值。

1323.6多重循环ー个循环语句的循环体中,如果还包含有循环语句,那么这就是多重循环或称为循环的嵌套。外部的循环称为外层循环,内部的循环称为内层循环。外层循环可以是fbr、while、d〇…while中的任何ー种,内层循环也可以是for,while、do--while中的任何,种。多重循环使用比较多的是二重循环,其次是三重循环。第24讲求10万以内的所有素数类型选修题趣味性…*难度***参见第19讲ー级算法:n=2当n<=100000{求n是否为素数,如果是则打印此数①n=n+l;)其中的①步需要进一步求精(其中求任意的”是否为素数的算法见第19讲)。二级求精:flag=l;i=2;while(i<=n-l)(如果n除以i的余数为零那么flag=0;i=i+l;}如果flag等于1那么打印!!是素数转换成C语言程序如下:1#include2intmain()3{4intn,i,flag;5n=2;6while(n<=100000)7{/・以下求n是否为素数・/8flag=l;9i=2;10while(i<=n-l)11{12if(n%i==0)13flag=0;

1337i=i+l;8}/*endwhilei*/9if(l==flag)10printf(''%d»

134”,n);1112n=n+l;八更新循环变量・/20}/*endwhilen*/2122return0;23)问题.此题得到的结果大多数在屏幕上一闪而过,能否将结果保存在ー个文件中,或者把它用打印机打出来?注意下面的打印图形系列是专门为提高读者运用二重或多重循环编程的能力而设置的,希望读者循序渐进、按部就班地学习这些例题。第25讲打印图形类型必修题开拓思路题趣味性**难度**112123123412345算法分析:首先考虑任意的第i行怎么打印。要打印的数字个数,第1行是1个数,第2行是2个数,第i行就是[•个数。每行要打印的数字都是从1开始,后面要打印的数都是前面那个数加1,所以可以用一个循环变量,来表示第i行要打印的所有值,,的初值为1,每次打印后都更新为下一次循环要打印的数:月+1,循环变量,的终值为i(因为共要打印i个)。打印任意第i行的算法如下:j=i;while(j<=i)(打印j,打印空格;j=j+l;)下面考虑所有行的打印方案。ー级算法:i=l;while(i<=5)(打印任意第i行;换行;1++;}山于打印任意第i行的二级求精算法已经给出,将其结合起来则成为:

135i=l;while(i<=5)(/・打印任意第1行・/j=l;while(j<=i)(打印j,打印」•个空格j=j+l;)换行;转换成C语言程序如下:1#include2main()3(4inti,j;56i=l;7while(i<=5)8{9/・打印任意第i行*/10j=l;11while(j<=i)12{13/・打印j,并打印ー个空格・/14printf("%d",j);15j=j+l;16}/*endwhilej*/1718printf("

136H);19i=i+l;20}/*endwhilei*/2122)问题1.请问,17行的空行有何好处?问题2.第18行的printf函数调用语句如果漏写,结果会如何?习题1.编程打印如卜.图形1135135713579习题2.编程打印如下图形135791357135131

137第26讲打印图形类型必修题开拓思路题趣味性**难度**aababcabedabode算法分析:此题的算法可借鉴第24讲,只不过要打印的数据要另外设置ー个字符变量。首先考虑任意的第i行怎么打印。要打印的字符个数,第1行是1个,第2行是2个,第i行就是i个字符。每行要打印的字符都是从、’开始,后面要打印的字符都是前面那个字符的ASCH码加1;所以可以用ー个字符变量ch来表示要打印的值,ch的初值为匕’,每次打印后都更新为下一次循环要打印的字符ch=ch+l,每一行要印i个字符。控制一行打印的循环变量,的初值为!,终值为i。打印任意第i行的算法如下:3=1;ch=•a';while(j<=i)(打印ch,打印空格;ch=ch+l;3=3+1;)下面考虑所有行的打印方案。ー级算法:i=l;while(i<=5)(打印任意第i行;换行:i++;)由于打印任意第,•行的二级求精算法已经给出,将其结合起来则成为:i=l;while(i<=5){/・打印任意第1行・/3=1;ch='a';while(j<=i)打印ch,打印空格;ch=ch+l;/*ch变成下一次j循环要打印的字符・/j=j+l;)换行;i=i+l;)转换成C语言程序如下:

1381#include2intmain()3{4inti,j;5charch;67i=l;8while(i<=5)9(10/・打印任意第i行*/11j=l;12ch='a';13while(j<=i)14{15printf("%c",ch);/・打印ch,打印空格・/16ch=ch+l;/*ch变成下一次j循环要打印的字符・/17j=j+l;18}/*endwhilej*/1920printf(M

139M);/・换行・/21i=i+l;22}/*endwhilei*/2324return0;25)问题.将此题的程序改为如下的形式是否可以?1#include2intmain()3{4inti,j;5charch;67i=l;8while(i<=5)9(10/・打印任意第i行・/11/*j=l;*/12/*ch='a*;♦/13for(j=l,ch=zaz;j<=i;ch++,j++)14/・打印Ch,打印空格・/printf(H%c”,ch);1516/*ch=ch+l;*/17/*j=j+l;*/18}/*endfor*/1920printf("

140");/・换行・/21i=i+l;22}/*endwhile♦/23

14124return0;25}注意:以上11、12、16、17行已经被删掉了。答:可以,这是ー种形式更为紧凑的for循环,在内层循环中,表达式1是ー个逗号表达式:jゴ,ch='a',表达式3也是ー个逗号表达式:M++J++;这样程序更为紧凑。关于逗号表达式的详细说明,参见第2章的提高部分(此处逗号表达式的作用,只是使逗号表达式中出现的几个表达式能够按顺序依次执行)。习题1.你是否可以只用两个字符变量来重做此题?习题2.打印以下的图形。abodeabedabcaba第27讲打印图形类型必修题开拓思路题趣味性**难度***abacbadcbaedcbafedcba算法分析:先解决任意第,・行如何打印。循环变量i的初值为1,任意第i行要输出i个字符。控制第i行输出的循环变量可以用字符变量ch,ch的初值是多少暂时不管,终值一定是字符a。由于第i行一共要输出i个字符,可以由此反过来推出ch的初值应该是多少;对于第1行,猜想有如下等式:ch初值的ASCI!码vh终值的ASCII码=1但这个式子不对,将此式应用于第2行(i=2)就可以发现错误,字符b的ASCII码一字符a的ASCII码的值等于!,而不是2。所以,适用于任何一行的正确的等式如下:ch的初值的ASCII码ーch终值的ASCI!码=レ1因此,对于任意的第1行,则有:ch的初值的ASCII码づ一I+ch终值的ASCII码ch的初值的ASCH码ヰ1+字符‘a'的ASCII码现在,可以很容易地写出在屏幕上输出任意第i行的算法。输出任意第i行的算法如下:ch=i-l+•a';while(ch>='a•)(输出字符变量Ch的值,输出ー个空格;ch--;}现在来考虑输出所有N行的算法如下:i=l;while(i<=N)(输出任意第1行①换行;i++;)其中对①步进行进ー步求精的算法已如上所述。C语言的程序如下:1#include2#defineN63main()4(5inti,j;6charch;7

1421i=l;2while(i<=N)io(11/・输出任意第i行・/12ch=i-l+'a';13while(ch>=zaz)14{15/・输出字符变量ch的值・/16printf(n%cH,ch);/*输出字符变量ch的值,输出ー个空格;*/17ch--;18)1920printf(M

143");21i++;22)2324)此题还有一种解法:仔细查看所有行的第一个字符,可以发现每一行的第一个字符都是前ー行的第一个字符在ASCII码表中的后面一个,我们用ー个字符变量M0,来跟踪表示每一行的第一个字符。任何一行上的字符也很有规律,若任何一行上要打印的字符用字符变量ch来表示,ch的新值和老值存在如卜的关系:ch=ch-1ー级算法:i=l;ch0='a•while(i<=n)输出任意第i行chO=chO+li/・为下ー个新行准备第一个字符・/换行;i++;)其中3.1输出任意第i行需要进ー步求精。ch=chO;/・初始化每一行的字符变量ch*/while(ch>=1a')(打印ch和一个空格;ch=ch-l;)将上述算法结合在ー起,如下:i=l;chO='a•while(i<=n)(/・输出任意第i行*/ch=chO;while(ch>='a')(打印ch和一个空格;ch=ch-l;}chO=chO+l;/・为下,个新行准备第・个字符*/换行;i++;}问题1.ch=chO这一句起到了什么作用?・问题2.请在学第二遍时,用第34讲中介绍的画内存变量取值变化图的方法,验证这两种算法是否

144正确。习题1.请读者自己将本题算法转换成C语言的程序。习题2.编程打印九九乘法表。1X1=11X2=22X2=41X3=32X3=63X3=9・习题3.打印以卜.图形:12345678910第28讲打印图形类型必修题开拓思路题趣味性**难度***121123211234321123454321算法分析:首先将注意力集中到任意的第i行如何打印上,先不考虑要打印的空格。也就是说,先将此题转化为更简单的另・题。1121123211234321123454321先将此题搞定,原题就容易多了,这就是ー种简化问题的常用方法。如果一个问题较难,是否可以编出与原题类似但又更简单的问题的程序。可以看出,如果令,•的初值为1,第,.行要打印的数据有2サー1个(如果i从1开始取值的话),用ー个循环变量,来表示打印给定一行时的循环,贝叮的取值应当是从1到2・ム1。每一行耍打印的数值用ー个整型变量〃来跟踪表示,则此变量的初值应为1。当六,・时,下ー个要打的〃值比当前〃值大1,即〃=〃+1;而当,之以后,下ー个要打印的〃值比当前"值小1。打印(任意)第,・行的ー级算法:j=l;n=l;while(j<=2*i-l)(打印n的值,打印空格;如果(j

145(打印第1行;打印换行;i=i+l;}前面打印任意第[•行的ー级算法,在这里就成为打印第i行的二级求精。i=l;while(i<=5)(j=l;n=l;while(j<=2*i-l)打印n的值;如果(jvi)那么n=n+l;否则n=n-l;j=j+l;}打印换行;i=i+l;)转换成c语言的程序如下:1#include2intmain()3{4inti,j,n;5i=l;6while(i<=5)7{8j=l;9n=l;10while(j<=2*i-l)11{12printf(''%2d",n);13if(j

146");21i=i+l;22}/*endi*/23return0;24)现在考虑任意第,・行数字前要打印的空格,关键要考虑在任意的第,行,首先要打印几个空格,,•越小,打印的空格越多。这很容易实现。比如,用10-,.来表示第i行要打印的空格数,,•越小,10-,・的值就越大。下面给出其伪代码。ー级算法:l.i=l;25while(i<=5)

147{打印第i行的空格;①打印第i行的数字;②换行;i++;)②的二级求精已经介绍过,ド面给出①的二级求精。for(k=l;k<=10-i;k++)printf("H;/・打印ー个空格・/总的算法为:i=l;while(i<=5)(/*2.1打印第i行的空格・/for(k=l;k<=10-i;k++)printf(w1');/・打印ー个空格・//*2.2打印第i行的数字・/j=l;n=l;while(j<=2*i-l)(打印n的值;如果(j2main()3{4inti,j,k,n;5i=l;6while{i<=5)7{8/*2.1打印第i行的空格・/9for(k=l;k<=10-i;k++)10printf(•'n);/*打印ー个空格・/1112/*2.2打印第i行的数字・/13j=l;14n=l;15while(j<=2*i-l)16{

1481printf("%d",n);2if(j

149");10i++;11}/*endwhilei*/1213}习题1.请将此题的内层循环改为fbr循环。习题2.打印以下图形。abaabcbaabcdcba以上图形ー共7行。*习题3.打印以下图形。abcdcbaabcbaabaa*・习题4.打印以下图形。acegecaacecaacaacacecacegecaceg*・习题5.打印以下图形:BC456GHIJ1112131415第29讲四大湖问题类型必修题开拓思路题趣味性**难度***上地理课时,四个学生回答我国四大淡水湖的大小时说:A学生:洞庭湖最大,洪泽湖最小,鄱阳湖第三。B学生:洪泽湖最大,洞庭湖最小,鄱阳湖第二,太湖第三。C学生:洪泽湖最小,洞庭湖第三。D学生:鄱阳湖最大,太湖最小,洪泽湖第二,洞庭湖第三。对于每个湖的大小,每大仅答对ー个,求出各湖的大小顺序。算法分析:此题可用蛮力法或称为穷举法,将・切的可能排序都遍历,然后在所有的各湖大小排

150序中,找出满足每个学生只答对ー个的答案。以变量の表示洞庭湖的大小,所有可能的取值是1、2、3、4。以变量油表示洪泽湖的大小,所有可能的取值是1、2、3、4。以变量力表示太湖的大小,所有可能的取值是1、2、3、40以变量pル表示鄱阳湖的大小,所有可能的取值是1、2,3、4。这4个变量的取值满足以下的条件(四个湖的大小各不相同,且有一个第一,ー个第二,一个第三,ー个第四)。dh+hh+th+ph-10下面来看如何表述才能使学生的回答中只有一个是正确的。A学生的三个回答,用变量的取值大小表述如下:

151(用取值1表示此湖最大,即排名第一.)洞庭湖最大,dh==l.洪泽湖最小,hh==4o鄱阳湖第三,ph==3.这三个关系表达式中只有一个为“真”(取值为1),其他两个为“假"(取值为0),所以A学生的叙述可以表示为:((dh=1)+(hh=4)+(ph=3))==1同理,B学生的叙述:“洪泽湖最大,洞庭湖最小,鄱阳湖第二,太湖第三。”可以表示为:((hh==1)+(dh=4)+(ph=2)+(th=3))=1同理,C学生的叙述:“洪泽湖最小,洞庭湖第三。”可以表示为:((hh=4)+(dh=3))=l同理,D学生的叙述:“鄱阳湖最大,太湖最小,洪泽湖第二,洞庭湖第三。”可以表示为:((ph=1)+(th==4)+(hh=2)+(dh==3))=1下面用四重循环来表示此题的ー级算法。for(ph=l;ph<=4;ph++)/*遍历鄱阳湖的所有排名・/for(th=l;th<=4;th++)/・遍历太湖的所有排名・/for(hh=1;hh<=4;hh++)/*遍历洪湖的所有排名・/for(dh=l;dh<=4;dh++)/・遍历洞庭湖的所有排名・/如果(各湖排名互不相等)(如果(A学生只答对了一个并且B学生只答对了一个并且C学生只答对了・个并且D学生只答对了一个)打印ph,th.hh>dh的值)此算法的效率比较低,而且“各湖大小互不相等”的二级求精比较麻烦。ph!=th&&ph!=hh&&ph!=dh&&th!=hh&&th!=dh&&hh!=dh此算法的最大优点是整体结构非常简洁,清晰。缺点是浪费时间,做了很多次无用的循环遍历。第一步改进:去掉最内层的循环,只要前三个湖的排名取值已经通过遍历确定下来,对第四个湖的排名可以不必用循环去遍历,只要通过“各湖排名互不相等”这个四个变量之间的约束条件,计算即可得到。第四个湖的排名=10ー第三个湖的排名ー第二个湖的排名ー第一个湖的排名即:dh=10-ph-th-hh第一步改进后的算法如下:/・遍历鄱阳湖的所有排名・//・遍历太湖的所有排名・//・遍历洪湖的所有排名*/for(ph=l;ph<=4;ph++)for(th=l;th<=4;th++)for(hh=l;hh<=4;hh++)如果(三个湖排名的取值互不相等)(dh=10-ph-th-hh;/*确保四个湖排名互不相等・/如果(A学生只答对了一个并且B学生只答对了一个并且C学生只答对了一个并且D学生只答对了一个)打印ph,th,hh,dh的值}/*end如果(各湖大小互不相等)♦/}/*endforhh*/可以看出,经过改进后的算法效率更高,但程序稍稍难懂了一些。各湖排名的取值互不相等也得到了简化,成为判别三个湖大小互不相等。

152ph!=th&&ph!=hh&&th!=hh第二步改进:第三重循环可以在更小的范围内进行,不需要在鄱阳湖ph和太湖th的所有取值的遍历下去考虑,而只需在满足鄱阳湖排名不等于太湖排名的条件下进行循环。所以算法可以进行进一步改进。第二步改进后的算法如下:for(ph=l;ph<=4;ph++)/・遍历鄱阳湖的所有排名・/for(th=l;th<=4;th++)/*遍历太湖的所有排名・/如果(ph!=th)/・如果鄱湖排名不等于太湖排名・/{for(hh=l;hh<=4;hh++)/・遍历洪湖的所有排名・/(如果(各湖排名的取值互不相等)(dh=10-ph-th-hh;如果(A学生只答对了一个并且B学生只答对了一个并且C学生只答对了一个并且D学生只答对了一个)打印ph,th,hh,dh的值}/*end如果(各湖大小互不相等)*/}/*endforhh*/}/*end如果(ph!=th)*/在以上的算法中,各湖排名的取值互不相等也进ー步得到了简化,最终成为:ph!=hh&&th!=hh1#include2intmain()3{4intph,th,hh,dh;5for(ph=l;ph<=4;ph++)/・遍历鄱阳湖的所有排名・/6for(th=l;th<=4;th++)/*遍历太湖的所有排名・/7{8if(ph!=th)/・在ph不等于th的情况下考虑洪湖排名・/9{10for(hh=l;hh<=4;hh++)/・遍历洪湖的所有排名・/11if((hh!=ph)&&(hh!=th))/・如果三湖排名各不相同・/12(13dh=10-ph-th-hh;/・四湖排名各不相同・/14if(((dh==l)+(hh==4)+(ph==3))==1&&15((hh==1)+(dh==4)+(ph==2)+(th==3))==1&&16((hh==4)+(dh==3))==1&&17((ph==1)+(th==4)+(hh==2)+(dh==3))==1)18/・打印ph、th、hh、dh的值・/19printf("鄱阳湖排名%d,太湖排名%d,洪湖排名・d,洞庭湖排名%d”,ph,th,hh,dh);20)21)2223)问题1.请问程序的哪条语句确保了dh与其他各湖排名不相等?问题2.请问各条fbr语句从哪里开始,到哪里结束?相互之间是何种(嵌套还是并列)关系?每个for循环的循环体包括什么语句?各条if语句从哪里开始,到哪里结束?答:从最内层的fbr循环来看,循环变量是hh,从第10行开始,到第20行结束,其中的循环体只包含一条if语句,此if语句是ー种无else形式的,布尔表达式为:((hh!=ph)&&(hh!=th)),其值为“真‘’时,if

153语句执行嵌入其中的两条语句,分别是第13行的赋值语句和第14行,到第19行的另一条if语句。由于是两条语句,所以要用第12行和第20行的花括号括住,使之成为一条え合语句。中间ー层的fbr循环是从第6行开始,到第22行结束。循环变量是th,循环体中只有一条if语句,此if语句是从第8行开始,到第21行结束。最外层的for循环中的循环变量是ph,循环体中只有一条for循环语句。问题3.第4行和第22行的ー对花括号是否可以省略?第9行和第2!行的ー对花括号是否可以省略?第12行和第20行的ー对花括号是否可以省略?答:可以看花括号中是否只有一条语句,如果只有一条语句,就可以将其省略,如果有两条以上的语句(包括两条),构成复合语句的花括号就不可以省略。所以,只有第12行和第20行的ー对花括号是不可以省略的。习题1、(摘自《程序算法与技巧精选》郭继展等机械工业出版社P221)有四种不同的果树,栽成了一排。生物刘老师叫四个学剩猜出树名。张说:“第一棵是梨树,第二棵是枣树,第三棵是杏树。第四棵是桃树”。王说:“第一棵是梨树,第二棵是桃树,第三棵是枣树。第四棵是杏树”。李说:“第一棵是杏树,第二棵是枣树,第三棵是梨树。第四棵是桃树”。赵说:“第一棵是桃树,第二棵是梨树,第三棵是杏树。第四棵是枣树”。刘老师听了,摇摇头说:“只有三个人每人说对了两种,有一个人四种说的全不对。你们的基本常识还需要加强啊!”请问这四棵树的树名依次应该是什么?・习题2.某教练接到ー项比赛任务,要他在代号为A、B、C、D、E、F的6个队员中选出若干人去参加ー项比赛。人选的配备必须注意下列各点。(1)A、B两人中至少去一人。(2)A、D不能一起去。(3)A、E、F三人中要派两人去。(4)B、C两人都去或都不去。(5)C、D两人中去一人。(6)若D不去,则E也不去。请问应该让谁去?(提示:用ー个整型变量a表示A去(a为1)还是不去(a为0),其他人也类似。)点评:虽然在很多场合中,将关系表达式运算得到的逻辑值(或几个关系表达式之和)与整数值比较大小常常是错误的和无意义的。但在此题中却是ー种例外的巧妙应用。小知识:读懂程序和编写程序的诀窍之ー仔细看清楚算法或程序的结构,有多个循环和选择结构交织在ー起时,一定要搞清楚每个循环或选择结构是从何处开始,到何处结束;相互之间是嵌套关系还是并列关系;每个循环或选择结构嵌入了几条语句。特别要注意空语句和复合语句.3.7本章常见错误小结while循环语句经常容易出错的地方有以下几处。(1)while后面的布尔表达式忘了用括号括住,变为whilei<=100o还有while(i<=n)&&(j!=O)也少了括号,正确的应为while((i<=n)&&(j!=O))或者while(i<=n&&j!=0)o(2)布尔表达式的后面多了分号,变成while(布尔表达式);,导致循环体是一条空语句,而

154且循环是无限循环(因为在每次执行循环体后,循环的条件永远不变)。(3)循环前忘记对变量进行初始化,包括循环变量和循环中用到的变量。(4)在循环体中忘记写循环变量的更新语句,导致循环执行的条件(即布尔表达式的值)永远为“真",产生了无限循环。在循环变量为字符变量时最容易犯此错误。(5)漏写了括住循环体的花括号,导致循环体只有一句,这样也常常造成死循环。因为漏写花括号后,对循环变量的更新成为循环后的下一条语句。(6)循环的终止条件发生错误,即循环多了一次或少了一次。比如将while(循环变量<=常量)错写成while(循环变量〈常量),造成循环体中的语句少执行了一次,导致结果不对。(7)循环体的花括号后多加了一个分号。(8)在循环体中,出现改变循环变量终值取值的语句(如果终值是用・个变量来表示的话)。d〇…while语句与while语句类似,只不过dO…while语句的while(布尔表达式)后面一定要有分号。for循环语句的常见错误除了while的常见错误外(仅有忘记循环前的初始化和对循环变量更新这两个错误不易犯),还容易犯以下错误。(1)三个表达式之间的两个分号被省略掉了。比如正确的应当是for(;i<10;),可能会被省略掉前后的任意一个或两个分号。(2)三个表达式之间用逗号隔开。比如for(i=l,i<10,i++)或者for(i=lJ=M,i

155第四章数组辩证法认为,一般性寓于特殊性之中.特殊与一般反映了世界联系和发展的客观规律,它同时也成为人们的思维规律,成为人们认识世界的重要的思维形式.__佚名到ヒー章为止,共介绍了三大基本结构,采用这三种基本结构,可以编出各种程序。但是程序编写的效率有时很可能非常低。从本章开始,将要介绍ー些新类型的变量和新的编程技法,用以提高编程效率。4.1ー维数组引论在现实生活中,为了管理或工作上的方便,人们通常不会给每个同一类型的个体都起ー个独特的名字,比如エ厂生产的产品。通常,人们仅会给任何同一类型的产品起一个商品名称。然后加上一个唯一的编号,来对同类中的每ー个产品进行标识(区别)。对于班级和学生管理与此类似。虽然实际上每个学生都有一个通常是家长起的名字,但在进行管理时,要求任何一位管理人员或老师记住每位学生的名字,在很多情况下都是不必要的。学校管理人员更喜欢给每个班起一个唯一的名字,然后再给此班的任何ー个学生指定一个唯一的编号——即学号,用来标识ー个学生。比如2005计软1班4号学生,指的是张-0编号可以从〇开始(但通常情况下,人们习惯于从1开始编号),如果某班中有50个学生,则编号依次是:2005计软1班0号学生,2005计软1班2号学生,2005计软1班3号学生,…,2005计软1班49号学生。在很多情况下对每个学生采用这种新的名称,往往比用其姓名更易于管理。比如,某个系的年级辅导员可以很方便地以这种对学生个体的新的命名来进行任务的分工:如05软件3班单号学生挖坑,双号学生种树。这种任务分派比用全班学生的花名册来进行分エ要方便快捷得多。点评.タ命名方式的启示可以给ー批同类型的n个个体的集合起一个总的名字(集体名词),用以命名这个集体,然后给这个集体中的每个个体指定一个唯一的编号(这个唯一的编号可以从0开始,依照自然数的顺序为每个个体进行编号,比如〇,2,3,…,〃ー1)。用这种命名方式对这个集体进行管理,往往比给其中的每个个体起一个特殊的名字要方便得多。这种方便主要体现在以ド三个方面:(1)看到2005计软2班3号,就知道此个体属于哪个集体。(2)方便管理人员利用编号易于分类的性质(比如:编号是偶数还是奇数,是大号还是小号等),进行任务的分配。(3)将一个班的全体学生在进行集体活动时安排在相邻的位置进行管理调度,比让全校学生混杂在ー起要好管得多。越是不注重对个体进行详细区分的应用场合,这种命名方式就显得越为方便快捷。在编写程序时,情况也是类似的。在大多数情况下,要编写程序对一批用在同•场合的

156回券粤的数据进行处理时,给每个要处理的数据都起一个独立的变量名,往往会得不偿失,不利于编写简洁高效的程序。可以使用更为简洁的方法来定义一批同类型的变量。对这ー批变量从整体上起一个名字,这个名称被称为数组名(这是ー种集体变量名,相当于班级名)。然后用数组名加上方括号中的序号(这相当于班级中学生的学号)的方式,来区分(并使用)这ー批变量中的不同变量,详见以下介绍。4.2ー维数组•ー维数组的定义:c语言中,定义ー个ー维数组的语法为:类型名数组名[整型常量表达式】;类型名可以是任何类型。既可以是简单类型(比如float、int、char等),也可以是后面章节将要介绍的构造类型(比如第8章的结构类型)。类型名用以表示该数组中所包含的全部变量都属于何种类型。裂级多是ー个用标识符起的名字。方括号中的“整型常量表达式”在最常见的情况下是ー个正整数(不能是实数、负整数和〇〇可以是ー个表示整数值的符号常量,但决不可以是ー个整型变量),这个整型常量表达式的值用来表示该数组中所包含的(同种类型的)变量的总个数。例如:有一数组定义:inta[10];这就定义了一个数组名为a的数组,数组中一共有a[0],a[l],-,a[9]这10个整型变量(类似于用班级名和学号来标识班中的任何一个学生)。«数组元素(又称为下标变量):在这里用的是数组名加上放在方括号中的一个序号来标识这ー批变量中的某ー个特定变量。a[0],a[l],-,a[9]这10个变量全都被称为数组元素,又被称为下标变量。注意:下标变量的序号是从〇开始而不是从1开始的。因此在此处序号最大的下标变量是a[9]而不是a[10]o问题:如果定义了一个数组floatscore[N];N是符号常量,它的值是20。该数组中是否有score[20]这个下标变量?该数组中一共有多少个可以使用的下标变量?分别是哪几个?这些下标变量中可以存放何种类型的数据?小知识:数组元素为何又称为下标变量?原因在于,在早期用计算机编程时主要用于科学计算,ー个数组经常用于表示一个向量,ー个数组元素用来表示此向量的ー个分量,而同一个向量的不同分量之间只有下标不同。例如同一个向量a的分量a1和a3就是下标1和3的不同。对计算机中的变量命名时,不能用下标,只好用ー种变通的方式,把和下标ー样大小的数值放在方括号中,称为数组的下标。因此,在计算机中,用数组元素或1]、a[3]来分别表示向量的分量期和a3.这大概就是下标变量这个称呼的由来。

157•数组的下标、下标表达式:对于下标变量a[3],其中方括号内的序号值(在此处是3)又被称为此数组的下标。数组的下标既可以是整数值,也可以是ー个整型表达式。这个整型表达式被称为下标本さオ。例如a[2*i+l],a[3*i-2*j](其中的变量i和上都是整型变量)中的2*i+l和3*i-2*j就是下标表达式。•动态下标变量:随着下标表达式中的整型变量取值的不同,a[下标表达式]这同一个式子可以表示该数组中的多个不同的下标变量。因此,我们把语句中(表达式中或赋值语句的左边)出现的如下形式的下标变量:数组名I含有变量的下标表达式]称为动态下标变量。例如:对于动态下标变量a[2*i-l],当i等于1时,a[2や1]其实就是a[l]:当i等于2时a[2*i-l]实际上就是a[3]……。点评:程序中对于下标变量的存取采用两种方式:一种是采用数组名[常量或常量表达式]的方式(比如a[7]),这种方式和普通变量的使用方式本质上没有任何区别;另ー种是采用动态下标变量的方式.采用这种方式,同一个式子随着下标表达式的取值不同,可以表示不同的下标变量.这就给编程带来了更大的自由度和灵活性。灵活地使用这种自由度(往往要与循环结合起来用),将会使编出来的程序简洁而又高效.这是采用数组方式进行编程的要点和精华所在.•下标和下标表达式的允许取值范围下标表达式是由整型(或字符型)常量、整型(或字符型)变量、算术运算符构成的,表示某个数组下标值的整型表达式。比如a[i]、a[3*i-l]o因此,此ド标表达式的取值,只有在该数组的下标允许的取值范围内オ是有效的。也就是说,对于如上所定义的a数组(inta[10];),在a]下标表达式]的下标表达式中,该下标表达式的取值范围一定耍满足:〇《下标表达式W9下标表达式的取值如果超出此上下边界,所要表示的下标变量不存在,这就会出现数组访问越界错误。这种错误C编译程序一般不做检查,要求程序员编程时自己注意。但这种错误常常会带来严重的后果。注意下标标达式允许的取值范围:一般情况下,如果定义数组中用的是符号常量N:类型名数组名[N];那么,存取该数组元素的卜.标表达式的值必须满足如ド式子:〇<下标表达式〈N-1・数组元素在内存中的相对位置:数组的概念是高级语言学习中所遇到的第一种结构化的数据。同一数组名下的所有数组元素(或称为下标变量)都被编译程序安排在一片连续的内存空间中。这片连续的内存空

158间最低的内存(首)地址的ー个存储单元中,存放着第一个数组元素的值(例如a[〇],在C语言中,第一个数组元素的下标一定是0),如图5.1所示。04812162024283236a[0]a[l]a[2]a[3]a[4]a[5]a[6]a[7]a[8]a[9]120-443542-764262698285图5」内存中的数组其中第一行是各数组元素相对于数组首地址的偏移地址,第二行是各数组元素,第三行オ是数组元素(下标变量)所对应的内存单元中存放的数据(假设每个血型整型变量占据4个字节的基本存储单元)。问题:有如下数组定义intnum[3O];请问下标变量num[18]和下标变量num[3]的内存首地址之差是多少?假设每个血型变量占4个字节的内存空间。•数组元素的初始化:可以在定义数组时,对数组中的各数组元素进行赋初值,其形式如下:intnum[l0]={2,3,4,5,6,7,8,9,10,11);这样导致的结果如图所示:a[0]a[l]a[2]a[3]a[4]a[5]a[6]a[7]a[8]a[9]234567891011还可以只对部分数组元素赋初值,比如:intnum[10]={7,5,2,8};没有得到赋值的所有后面的数组元素,编译程序会(通过编写的ー些指令)安放数值0在相应的内存中。这样导致程序被编译后(但是是在程序运行之前)内存中数组元素取值如下图所示:a[0]a[l]a[2]a[3]a[4]a[5]a[6]a[7]a[8]a[9]7528000000所以,如果想对所有的数组元素都赋初值O就很简单:intnum[10]={0};即可。注意int型整型数据在VisualC++的编译环境下占4个字节的内存,在TurboC2.0的编译环境下占2个字节的内存.在ー些高级语言中,数组的下标可以从任何ー个整数值开始,到任何ー个整数值结束,比如Pascal语言中定义ー个整型数组可以为:num:array[-10,10]ofinteger;,这个数组ー共有21个数组元素,分别是num[-10],num[-9],...,num[9],num[10].C语言中数组元素的下标值规定从0开始,是为了方便使用指针变量来遍历并存取数组元素。定义并使用数组(中一一批同类型的变量)带来的方便,主要体现在三个方面。(1)看到下标变量a[3],就知道此下标变量属于哪个集体变量(即数组名),大大方便了编程和读懂程序。(2)方便程序员利用下标表达式并结合循环进行灵活高效的编程,大大提高编程的效率。(3)编译程序会为同属一个数组的所有下标变量安排一片连续的内存空间,加快数据的存取

159速度和程序的运行速度(但如果要插入或删除数组中的某个数据,效率则会很低,参见数组元素的插入与删除。在这种情况下,应当采用链表)。下面先来看几个例子,通过这几个例子对・维数组中数组元素的存取有一个系统的把握。・下标变量中数值的存放(下标变量出现在赋值语句的左边):例题5.!将0,1,2,3,4,,49存放到数组元素num[0],num[l],num[2],,num[49]中,并将它们打印出来ー级算法:(1)将〇,1,2,3,-,49依次存放在数组元素num[〇],num[l],num⑵…,num[49]中。(2)打印所有数组元素的值。转换成C语言程序如下:1#include2intmain()3{4inti,num[50];/・注意,此处不可以是num[49]*/5/・通过赋值语句,将数组元素初始化・/61=0;7while(i<=49)8(9num[2]=2;11)13/・顺序打印各数组元素・/142=0;15while(i<=49)16{17printf(''%d

160w,num[2]);182=2+1;19)20return0;21}问题1.将第4行的变量定义改为:int1,num[49]:是否可以?答:错,这样ー来数组中只有49个下标变量,下标最大的数组元素是num[48]o这使得从第7行到第11行的循环语句在执行最后一次循环体时,数组下标会出现越界错误。对于这种错误,大部分C编译器是不会去检查的。问题2.在i等于8时,循环体中的句子numm=i;其实是对于哪个下标变量赋初值?num[i]被称为什么?答:此时该式相当于赋值语句num[8]=8;,num[i]被称动态下标变量。问题3.以下程序是否能起到相同的作用?#includevoidmain(){intnum[50];/*赋值・/num[0]=0;num[1]=1;num[2]=2;num[3]=3;

161num[49]=49;printf(''%d

162wnum[1]);printf(''%d

163wnum[2]);printf(''%d

164wnum[3]);printf(''%d

165wnum[49]);其中的省略号在真正的程序中不能有。要有两组50句类似的句子。答:对各个下标变量赋值所起的作用和本讲中的使用循环语句的例题ー样,输出的结果也ー样。但这样编程会非常累,很不可取。既然有高效的编程方法,就不要用麻烦的方法了。这种麻烦的编程方法,就是由于没有采用在循环中使用一条语句:numU戸:的聚陵方去来分别为多个下标变量赋值。问题4.程序中,两个while是嵌套关系还是并列关系?答:是并列关系。问题5.将!7行的语句:printf(''%d

166w,num[1]);改为:printf(''num[%d]=%d

167w,i,num[i]);是否可以?哪种效果更好?答:后ー种更清楚。请读者自己亲自偿试一下。问题6.第14行的语句i=0;漏掉了,结果会如何?答:第2个循环根本不会执行,因为i的值在执行第二个循环时是50。问题7。将第6行和第14行都改为i=l;是否可以?答:语法上没错,但是这样一来下标变量num[0]就没有得到初始化。num[0]中将是垃圾数据。

168习题1:请编程定义ー个数组intnum[50]J不用定义时就直接赋初值的初始化方法,将整数3,4,5,-51,52分别存放在数组元素num[0]、num[1]、num[2]、-num[48]ヽnum[49]中。习题2:请编程定义ー个数组intnum[50];不用定义时就直接赋初值的初始化方法,将整数3,整5,.一48,49分别存放在数组元素num[3]、num[4],num[5]、,._..num[48]、num[49]中。三个下标变量num[〇]ヽnum[1],num[2]不在程序中进行赋值(这些下标变量中将是垃圾数据)。习题3:请编程定义ー个数组intnum[20];不用定义时就直接赋初值的初始化方法,将3,6,9,...…57,61分别依次存放在下标变量num[〇]、num[1]-.num[2]、.....num[18]num[19]中。・下标变量的变量值的取用(下标变量事现在表达式中)例题5.2请编程定义ー个数组并直接将其初始化为:intnum[10]={2,4,6,8,10,12,14,16,18,20}Z。求出该数组中所有下标为奇数的数组元素之和sum程序如下:1#include2voidmain()3{4intnum[10]={2,4,6,8,10,12,14,16,18,20};5intsum,i;67sum=0;8i=l;9while(i<=9)10{11sum=sum+num[i];12i=i+2;13}14printf("所有下标为奇数的数组元素之和为之d

169",sum);15}问题:将程序中的11和12行替换为:lf(i%2!=〇)sum=sum+num[i];i++是否可以?习题4:已知实型数组floattemp[365]中的365个下标变量中依次存放着某一年中每天的平均温度(不考虑闰年),而且已知一月一日是星期二。编程求:1、该年7月份的平均气温。2、该年国庆7天长假的平均气温。3、该年所有星期天的平均气温。・思考题:如何将保存在硬盘某文件中的任意一年的气温数据读到习题4的数组temp中(此题在学了第8章的文件输入输出后再做)。请编程定义ー个数组intnum[10];然后编程将1,1,2,3,5,8,13,21,34,55(

170这是著名的菲波那契数列,除了最初两个数外,每ー个数都是前两数之和)分别依次存放在下标变量num[〇]、num[1]、num[2],num[8],num[9]中。算法分析:首先将1赋给num[〇]和然后使用循环,用前两个下标变量得到下ー个下标变量的值:num[i]=num[i-2]+num[iT];循环变量的初值是2,终值是9,伪代码的算法如下:intnum[10]={l,1};1i=22while(i<10){4.1num[i]=num[i-2]+num[i-l];4.2输出卡标变量num[i]的值;4.3i++;}请读者自己将以上算法转变成C语言程序点评./有规律的数据是否要求用户输入?程序运行时往往要用到两大类数据,・类是非常有规律变化的数据,・类是(现实情况下采集到的)无规律的数据。在编程时,如果编写要求用户从键盘输入有规律的数据的程序段,那是非常愚蠢的行为。解决的办法是采用循环,有时还要结合数组,通过变量自身取值的变化得到那些有规律变化的数据。编程的原则之一是,让用户输入的数据量最少。即使是无规律的数据,如果不是从现实中采集到的,也常常利用(伪)随机函数rand()(或更高质量的自己编写的随机函数)自动生成(参见第讲)。习题4:定义ー个数组floatscore[20],并在定义时将所有数组元素初始化,定义另ー个实型数组floatsum[5];要求:1、求数组score的各数组元素之和并将其放在sum[1]中2、求下标为偶数的数组score的各数组元素之和并将其放在sum[2]中3、求下标为0、1、3、6、10、15的数组score的各下标变量之和并将其放在sum[3]中4,求score[0]+2*score[1]+3*score⑵+…+20*score[19]之和第31讲利用程序来选举班上的班长。全班有50人,全班每个同学都可通过在讲台上的一台电脑的键盘上输入ー个学号来投班上某同学一票。算法分析:1、使用的数据结构:定义ー个数组intnum[50];下标对应着全班同学的(班内相对)学号,下标变量mun[n]中存放的数值,就是学号为n的同学的选票数。2、intnum[50];中所有下标变量的值初始化为0;3、i=0;4、while(i<50)/*根据学号遍历每个学生・/printf("请选择ー个学号投票、n");第i个学生输入所选同学的学号ー》n,如果(n<50并且n>0)num[n]++;/・被选同学的选票数加1*/

171否则{显示“您选择的学号不对,请重选”,continue;}i++;}5、将全班每个同学所得的选票数按学号顺序打出来。转变成C语言程序:1#include2main()3(4intnum[50]={0};5inti,n;67i=0;8while(i<50)9{10printf(“您的学号为%d,请选择ー个同学的学号投票、n",i);11scanf("%d",&n);12if(n>0&&n<50)13num[n]++;/*第i号同学投了学号为n的同学一票・/14else15{printf("您选的学号不对,请重选、n");continue;}16i++;17)18for(i=0;i<50;i++)19{20print(“学号为%d的同学所得票数为%d

172",i,num[i]);21)22}问题1、第4行起了什么作用?问题2ヽ15行中的continue语句是否可以换成break语句?问题3ヽ15行中的continue语句是否可以换成别的语句?・习题1、此题会将所有同学的选票依次打印出来,这很不好。请将此题改进为只将得票最多同学的学号和所得票数打印出来。・习题2、在学了对全部数组元素进行排序后(参见第讲),请将上题改为选举班上5名班委。得票最多的5人当选。

173类型开拓思路题趣味性**难度**输入al,a2,a3a99,alOO共100个数,要求计算bl=(al+a2)/2,b2=(a3+a4)/2,…,b50=(a99+a100)/2;cl=alXalOO,c2=a2Xa99,c3=a3Xa98,...,c50=a50Xa51。如果用以前学过的知识,则首先要定义200个变量。其中al,用,a3,-,alOO共100个,bl,b2,b3,…,b50共50个,cl,c2,c3,—,c50共50个。程序如下:1#include2intmain()3{4floatal>a2,a3»a99,alOO;5floatbl,b2,b3,…,b49,b50;6floatcl,c2,c3>…,c49,c50;78scanf(''%f%f%f***%f%fw,&al,&a2,&a3,•••,&a99,&a100)j9bl=(al+a2)/2;10b2=(a3+a4)/2;11-12b50=(a99+al00)/2;13cl=al*al00;14c2=a2*a99;15...16c50=a50*a51;1718}注意所有的省略号在实际程序中都是不允许有的,必须全部编写出来。可见,程序将非常冗长。如果数据不是200个,而是20000个,用这种方法编程就是对程序编写者的一种折磨。那么,有没有简洁的编程方法做此题呢?这种方法是存在的,但要用到C语言中一种刚介绍的新类型的变量,即数组。这样,使用ー维数组的定义,上述程序可以简化为:#includeintmain()(floata[101];floatb[51];c[51];scanf(''%f%f%f,,^f%fw,&a[1],&a[2],&a[3],,,,,&a[99],&a[100]);b[l]=(a[l]+a[2])/2;b[2]=(a[3]+a[4])/2;

174b[50]=(a[99]+a[100])/2;c[1]=a[1]*a[100];c[2]=a[2]*a[99];c[50]=a[50]*a[51];)其中a[〇]、b[〇]和c[0]这三个下标变量在程序中并没有使用到。程序虽然得到了一定的简化,但还是不够。分析50个关于50个下标变量bu的赋值语句,会发现赋值语句的左边,都可以很简单地用b[i]来表示,只要让i从1变到50,i的值每次加1即可。关键在于(是否可以,并且)怎样用ー个式子来统一表示50个赋值语句右边的表达式。先来看第二个关于b的赋值语句b[2]=(a[3]+a[4])/2;,左边应当变成b[i],(因为i=2),再看右边的表达式,如何变成与变量i相关的下标表达式。(1)下标3可看成是2打ー1,下标4可看成2カ。这是ー种尝试。(2)下标3还可看成是i+1,下标4可看成是i+2。这是另ー种尝试(还可以有其他尝试)。上述这两种尝试在i=2的式子上看不出到底哪ー个正确,因为b[i]=(a[2*i-l]+a[2*i])/2(5.1)和b[i]=(a[i+l]+a[i+2])/2(5.2)在i=2时都等价于b[2]=(a[3]+a[4])/2。所以,通过i=2时的b[2]=(a[3]+a[4])/2;这个赋值语句,得到的尝试性的两个一般性的表达式(5.1)和(5.2),必须经受i等于任何值的检验。通过检验可以发现,只有(5.1)式是正确的,它能在i等于1到50的任意ー个值时,代表其中任何一个关于b[i]的赋值语句右边的表达式。所以,可以用以下的赋值语句:b[i]=(a[2*i—l]+a[2*i])/2;来统一表示这50个关于b[i]的赋值语句,只要让i从1变到50,每次i的值加1即可。类似的,分析50个关于50个下标变量c[i]的赋值语句,会发现也可以用以下的式子:c[i]=(a[i]*a[101-i]);来统一表示这50个赋值语句,只要让i从1变到50,每次i的值加1即可。将变量i从1变到50I表达式b[i]=(a[2*i-l]+a[2*i+l])/2!正好就是50个具体的关于b[i]的表达式。i=l时,b[i]=(a[2*i-l]+a[2*i])/2;就等于b[l]=(a[l]+a[2])/2;«i=2时,b[i]=(a[2*i-l]+a[2*i])/2:就等于b[2]=(a[3]+a[4])/2;。i=50时,b[i]=(a[2*i-l]+a[2*i])/2;就等于b[50]=(a[99]+a[100])/2;。对于c[i],情况是类似的。于是,再利用循环,程序可以得到极大的筒化。类似的,以下的输入函数调用语句:scanf(w%f%f%f%fw,&a[l],&a[2],&a[3],,•,>&a[99],&a[100]);也可以利用循环筒化为:i=l;while(i<=100)printf("请输入a[吿d]的值、n”,i);scanf(''%fw,&a[i]);i=i+l;

175最后程序变为:1#include2intmain()3{4floata[101];5floatb[51],c[51];6inti;7i=l;8while(i<=100)9(10printf("请输入a[i]的值、n”,i);11scanf(''%fw,&a[i]);12i=i+l;13}1415i=l;16while(i<=50)17{18b[i]=(a[2*i-l]+a[2*i])/2;19c[i]=(a[i]*a[101-i]);20i=i+l;21)22for(i=l;i<=50;i++)23printf(''b[%d]=%f,c[%d]=%f

176w,i,b[i],i,c[i]);24)可以把最终这个利用数组定义和下标变量再加上循环的程序与开始的程序加以比较,会发现程序得到了很大的简化。此题如果不用数组(主要用了含有下标表达式的动态下标变量)并结合循环来进行编程,将是非常麻烦的。点评.タ数组如果不与循环结合起来使用,将发挥不出它在简化编程工作上的巨大威力。将循环用于大量数据存放到数组中的问题,关键是要找到ー个或几个普遍适用于任何一次循环的下标表达式和由动态下标变量为要素构成的赋值语句(比如本题中的b[i]=(矶2*i-l]+a[2*i])/2;和c[i]=(fl[/]*a[101-/]);这两个与动态下标变量有关的表达式和赋值语句)。点评ク使用动态下标变量的最大好处是可以把下标变量中的下标用一个下标表达式来统一表示,并与循环结合起来使用(数组的ド标表达式变成一个与循环变量有关的整型表达式),使程序得到最大程度的简化。带来的坏处是数组元素的下标容易上下越界,C语言的编译程序对数组的越界错误并不检查,所以这样的程序在运行时会带来ー些潜在的重大危险。解决办法是:在必要时编写ー些预防越界错误发生的程序段。问题1.本题中的三个循环是嵌套关系还是并列关系?问题2.第15行的句子如果漏掉了,结果会如何?问题3.在第!3行和第15行之间插入了一个空行,请你谈谈这个空行有没有好处?问题4.第5行的数组说明是否可以改为:doubleb[n];?

177答:不可以。数组说明中只能是常量或常量表达式,不能是变量。习题1.编程将49,…,3,2,1,〇依次分别赋给num[〇],numn],…,num[47],num[48],num[49],即49—num[〇],48-num[l],…,0-num[49]。・习题2.巧分饼干。某寄宿学校老师拿出一大桶饼干,共2520块,准备分给班上的6个小学生。老师按事先写在ー张纸上的数字把这桶饼干分完,每个人分到的饼干数都不相同。然后他说:“1号小朋友,把你分到的饼干分1/8给2号;2号拿到后,连同原来的饼干分1/7给3号;3号拿到后,连同原来的饼干分1/6给4号;依此类推,最后6号拿到后,连同原来的饼干分1/3给1号。这样,你们每个人分到的饼干就一样多了。”问6个小学生原先各分到多少饼干?提示设mi(i=l,2,…,6)依次为6个小朋友原先分到的饼干数,ni(i=l,2,...,6)依次为除1号小朋友外,其他5个小朋友得到了别人的饼干,但还没有分给别人时的饼干数。对于1号小朋友,ml-nl,由于2520/6=420,可以得到如下等式(对于2到6号小朋友).n2=m2+niX(l/8),n2X(6/7)=420n3=m3+n2X(l/7),n3X(5/6)=420店=叫+明X(l/4),n6X(2/3)=420对于1号小朋友,有:mi=n”ri]X(7/8)+n6X(1/3)=420点评.タ如果用ー批同类型的普通变量来命名一批要处理的数据而不用数组,不仅会使程序难以简化,也会使程序更容易出错。程序的可读性、可维护性都会大大降低。因为(如果文档或注释不全)不知道到底哪些变量是相关的,属于同一批的本次要进行处理的变量。第33讲掷骰子游戏ー个骰子或称色(shai)子,是一个正方体,共有六个面,每个面上分别标有1、2、3、4、5、6这6个数字。在大富翁、麻将、飞行棋等电脑游戏中都会出现它的身影。本题用计算机模拟掷骰子游戏,并统计1000次掷骰子中分别出现数字1、2、3、4、5、6的次数。趣味性**难度**算法分析:随机函数rand()的每一次调用,都可得到ー个很可能不同于上一次调用时的值,其取值一般在0到32767这32768个整数之间。这32768个整数值每ー个值出现的几率都几乎相同。要得到6个不同的随机值,只需将此函数调用取除以6的余数即可,即:rand()%6:但得到的是在〇—5之间的6个随机数,再将其加1,即:rand()%6+l,就可得到1一―6之间的一个随机数。注意rand函数调用的两个特点:第一,调用时不需要参数,即函数调用时用rand(),圆括号中不必填入实际参数;

178第二,每次函数调用返回的值(从表面上看起来)是不确定的(即是伪随机的,但实际上是有周期性的,只不过这个周期比较大罢了)。问题L一般情况下,如果想通过rand〇函数调用,得到介于整数a——b之间的ー个随机整数,应当如何表示?答:rand()%(b-a+l)+a〇请你回答为什么?ー级算法:inttotal[7]={〇};/・分别用数组total的元素total[1]、total[2],total[6]来统计取到随机数1、2、……6的次数。*/(1)for(i=l;i<=1000;i++)(face=rand()%6+l;/・得到ー个16之间的随机数:*/打印此随机数face;/・根据此次生成的随机数face,将与这个随机数相应的total数组元素加1。・/switch(face)(case1:total[l]=total[1]+1;break;case2:total[2]=total[2]+l;break;case3:total[3]=total[3]+l;break;case4:total[4]=total[4]+1;break;case5:total[5]=total[5]+1;break;case6:total[6]=total[6]+1;break;}/*endswitch*/(2)在屏幕上显示各随机数出现的次数转变成C语言程序:1#include

1792#include3main()4{5inttotal[7]={0};/*将数组total的所有数组元素置为0*/6inti,face;7for(i=l;i<=1000;i++)8(9face=rand()%6+l;/・得到ー个16之间的随机数;*/ioprintf("%d,w,face);/・打印此随机数:*/11/・根据此次生成的随机数face,将与这个随机数相应的total数组元素加1〇・/1213141516171819202122switch(face)(case1:total[1]++;break;case2:total[2]++;break;case3:total[3]++;break;case4:total[4]++;break;case5:total[5]++;break;case6:total[6]++;break;}/*endswitch*/}/*endfor*/23for(i=l;i<=6;i++)24printf(”随机数吿d出现的次数是%d

180”,i#total[i]);25)问题1.你能否不用switch语句,使得程序更简洁紧凑吗?答:可以。用一条简单赋值语句,即可取代繁琐的占据了从第12到第21行的switch语句,即:total[face]=total[face]+1;或者更为简洁的total[face]++;这是结合循环使用动态下标变量所带来的极大优越性。问题2.将此程序运行两遍后,你发现了什么问题?答:前后两次运行时,第一次各数组元素的取值看起来好像是随机的,但与第二次重新运行时比较发现,第二次运行时各个随机数的取值顺序(即程序第10行打出的随机值)与第一次其实完全相同。请你自己查看两次运行的输出结果是否相同。例如:第一次是5、5、4、2、3、1、6、4、2、5;关闭程序后,第二次运行程序得到的仍然是5、5、4、2、3、1、6,4、2、5这些完全ー样的数。解决的办法是:在第8行和第10行之间加上函数调用srand(time(NULL));,但此srand()函数还需要ー个整数值作为实际参数(此数称为随机种子)。函数time用来读取系统时间(任何PC机在主板上都安装有一个时钟,它由主板上的ー个小电池供电,即使关机了,这个时钟仍然在计时)值。这一般是ー个从某年某月某日开始计数的总秒数,调用time。时的参数NULL是ー个符号常量,其值为0。在第2行和第3行之间还要插入ー个头文件包含即#include,程序变为:1#include2#include3#include/・这一行是新加的・/4main()5{6inttotal[7]={0};7inti,face;8

1811srand(time(NULL));/*这一行是新加的・/2for(i=l;i<=1000;i++)3{4face=rand()%6+1;5total[face]++;6)7for(i=l;i<=6;i++)8printf('、随机数之d出现的次数是吿d

182”,i,total[i]);9)此程序解决了生成不重复的随机数的问题。程序阅读题:请问以下程序做了什么事?请你为程序加上注释。1#include2#include3#include4main()5{6intnum[1001],total[7]={0};7inti,face;89srand(time(NULL));10for(i=l;i<=1000;i++)11{12face=rand()%6+l;13num[i]=face;14total[num[i]]++;14)15for(i=l;i<=6;i++)16printf('、随机数ぎd出现的次数是"d

183",i,total[i]);17)答:所做的事与本讲的主程序基本上一样。只是把第i次生成的随机数face,保存到了另ー个数组num的下标变量num[i]中而已。问题3.以上的程序阅读题中,是否可以用一句:num[i]=rand()%6+l;代替程序中的11行和12行的两句?答:可以。但对初学者不太好懂。习题1.修改此题,将其改为随机生成n个随机数,随机数取值在整数a——b之间,其中n、a、b都在程序运行时由用户输入。点评:“计算机的重要用途之一就是利用随机函数生成随机数(瞬间就可生成千万个),对社会上和自然界中的随机现象进行定量的描述、模拟和研究,可大大降低成本、提髙效益”(摘自《程序算法与技巧精选》郭继展,机械工业出版社P8页).然而,计算机模拟用的其实是ー种伪随机数,是按某种算法计算产生的,是ー个固定的数值序列(具有周期性),每个“随机数”,箕实都是事前可知的,但这是计算机模拟自然界和社会真实随机事件的基石。“各种髙级语言大多提供随机数函数,但质量都不高。如果用来给小学生做四则运算练习或考试模拟足够了,但是如果用于重要复杂的模拟,很可能会失真”。在需要更髙质量的随机数函数的情况下,请你查找ー些资料自己动手编写这种随机数生成函数.在附录G中是郭继展书中提供的ー个比较均匀分布的更高质量的随机数函数的源程序(细节请参考《程序算法与技巧精选》郭继展,机械工业出版社P8-P12)。第34讲生成n个(nくM)下車學的1到M之间的随机整数类型必修题

184趣味性***难度**讨论:此题的应用范围很广。比如,如果将此题变成一个特例:n=M=54,即生成54个不重复的1〜54之间的随机数,这其实就是ー副扑克牌的洗牌程序(可对所有54张牌的每ー张指定一个唯一一的1到54的编号。例如:可以规定草花A为1号、草花2为2号等等)。为了找到算法,先举ー个贴近生活的特例(n等于5,M等于6的情况)来理解本讲中的发牌算法,大家就比较容易明白:有6个编号分别为1,2,3,4,5,6的盒子。每个盒里面分别只能存放ー张红绿卡。起初每个盒子中放的都是绿卡,用来表示该盒子的编号是可以取的(用盒子的编号i进ー步表示编号为i那张扑克牌是可以发出的)。现在我手里有一个色子(即骰子)。通过扔色子,每次都可得到1ーー6之间的一个随机整数。发出5张不同的扑克牌的算法(这是•种广义的算法,因为这种算法还没有完全数量化)如下:1、在编号分别为1,2,3,4,5,6的盒子中全部放好绿卡,所发出的牌数count为0。2、扔一次色子,得到ー个1到6之间的随机整数一〉num。3、如果编号为num的盒子中是绿卡/・而不是红卡・/4,那么(4.1发出编号为num的扑克牌;4.2将发牌数count加14.3将编号为num盒子中的绿卡换为红卡;/・再想发一次编号为num的牌不可能了・/5,如果(n<5)那么跳转到第2步。6、结束(到此count一定等于5)。下面将算法完全数值化:将整型数组intpk[7]:代替放红绿卡的所有盒子(下标变量pk⑼不使用)。编号为i的盒中是红卡,用pk[i]的值为0来表示;编号为i的盒中是绿卡,则用pk[i]等于1来表示。则上述广义等落就可翻译成计算机可执行的如下算法:1、定义整型数组pk并初始化intpk[7]={0};所发出的牌数变量n初值也为0。2、num=rand〇%6+1;/・得到一个(1到6之间的随机整数)今num。・/3、如果(pk[num=0])/・表示编号为num的盒子中是绿卡,而不是红卡・/4、那么(4.1打印num;/・相当于发出编号为num的扑克牌・/4.2count++;/*将发牌数count加1*/4.3pk[num]=1;/・表示将编号为num盒子中的绿卡换为红卡;*/}5,如果(count<5)那么goto第2步。6、结束(到此count一定等于5)。第2步到第5步其实是•个用无条件跳转语句加上if语句构成的循环。根据第4章4.2节对while(包括dowhile循环)与if加无条件跳转goto的关系的讨论,可知以上算法可以转变为包含while循环的如下算法:

1851、定义整型数组pk并初始化intpk[7]={0};所发出的牌数变量count初值也为0。2、while(count<5){num=rand()%6+l;/・得到ー个(1到6之间的随机整数)今num。*/如果(pk[num]==0)/*用“0"表示编号为num的盒子中是绿卡,而不是红卡・/那么{打印num;/・发出编号为num的扑克牌・/count++;/*将发牌数count加1*/pk[num]=1;/・表示将编号为num盒子中的绿卡换为红卡;*/3、结束现在,再把上述n和M取值都为特殊值的算法一般化如下:1、定义整型数组pk并初始化:intpk[M+l]={0》;〃注意M是符号常量,不能是变量2、所发出的牌数变量count初值也为〇〇3、输入要生成的随机数个数n;/*n必须满足条件n〈M*/4、while(count

186输出此随机数num;③变量count加1;④pk[num]=1;/・装明此随机数已经取过了・/⑤}}可以用手动的方法,在纸面上运行此伪代码表示的算法,来验证算法是否正确(用你的大脑作为“CPU”,画了以下表格的纸张作为内存)。首先在纸上画出执行完算法第(2)步后变量的内存取值分布图,如图5.2所示。pk[O]pkロ]pk[2]pk[3]pk[4]pk[5]countnum0000000垃圾图5.2第2步后的内存取值执行(3)的第一次循环时count等于0,循环条件成立,假设①步取到的随机值num为3,变量的内存取值如图5.3所示。pk[0]pk[l]pk[2]pk[3]pk[4]Pk[5]countnum00000003图5.3ran为4时的内存取值执行②的if语句时,先计算布尔表达式“pk[num]==0"的值,取值为“真”,所以嵌入在if中的③、④、⑤都会执行,这3句执行完后,内存变量的取值情况如图5.4所示。(由于执行了:变量count加1;和可[ran]=l;这两句)pk[O]pk[l]pk[2]pk[3]pk[4]pk[5]countnum00010013图5.4执行完后的内存取值if语句执行结束后,第一次循环也结束了。算法又回到了第(3)步循环语句的开始处:不同之处在于:现在变量count的值是1,pk[3]的值也变为1了。要决定是否继续循环前,仍然要计算关系表达式(count<2)是否为“真”;由于内存中count的值现在是1,所以表达式count<2仍然为“真”,所以循环还要进行第二次。假设现在取到的随机数还是3,由于if后的关系表达式sj[3]=0现在为“假”(因为sj[3]的值现在是1),if语句中嵌入的3条语句都不会执行,内存取值分布没有发生任何变化,如图5.5所示。pk[O]pk[l]pk[2]Pk[3]pk[4]pk[5]countnum00010013图5.5语句未执行时的内存取值if语句执行结束后,第二次循环也结束了。算法又回到了第(3)步循环语句的开始处。要决定是否继续循环前,仍然要计算关系表达式(count<2)是否为“真”,由于内存中count的值现在还是1,所以表达式count<2仍然为“真”,所以循环还要进行第三次。现在,假设①步取到的随机值为1,内存取值分布如图5.6所示。pk[0]pk[l]pk[2]pk[3]pk[4]pk[5]countnum00010011图5.6第三次循环执行完第①步的内存取值执行②步的if语句时,先计算布尔表达式sj[l]=O.取值为“真”,所以③、④、⑤都会得到执行。这3句执行完后,内存变量的取值情况如图5.7所示。

187pk[0]pk[l]pk[2]pk[3]pk[4]pk[5]countnum010J0021图5.7语句执行后的内存取值if语句执行结束后,第三次循环也就结束了。算法又回到第(3)步循环语句的开始处,不同之处在于:现在count是2,pk口]的值也变为1了。接下来进行是否还要执行循环体的计算工作,即计算关系表达式count<2是否为'‘真”,根据内存取值情况可以看出,关系表达式count<2现在为“假”,循环语句的执行到此结束了。算法“运行”到此结束。由此可以看出,在n=2和M等于5时,算法的循环次数并没有出现边界错误。可以由此断定,算法在n(nWM)等于其他一般值时,也不会出现边界错误。此算法通过上述验证,可以发现确实不存在逻辑错误和循环次数错误。确实输出了取到的两个随机数1和3(而不是得到了不希望的1个或3个随机数。假设如此,则说明循环变量的终值出现了边界错误)。点评.ク读者可仿照此处介绍的方法来读懂、验证算法或源程序。但完全不必像本书中介绍的ー样,画出这么多的内存变量取值变化图。只需在纸上用铅笔画出ー个内存变量取值变化图,然后用ー些比较简单的输入数据或参数,跟踪算法的实际执行流程和数值的变化,一步ー步地用铅笔修改图中的变量取值。在整个执行流程中,仔细按照算法修改内存中各变量的取值。小知识:读懂、验证算法的诀窍之一就是画内存变量取值变化图。学会在纸面上画内存变量取值变化图,然后用大脑作为CPU,用ー些假设的、比较简单的、尽量小和少的输入值(如果有循环,则循环次数要设置的比较少,比如上述例子中n是2,M是5),去执行用伪代码写出来的算法。用这样的方法,一般能够很容易地看懂ー些别人写出的、比较难懂的算法或程序;也可以用此方法来验证自己编写的算法或程序是否存在逻辑上的错误或者边界错误等等.点评:笔者认为,上述这种技能是想成为程序员或编程高手的・种重要的、基本的技能。使用这种技能有两方面的好处。第一,在写出算法而尚未写出程序时就可以检查算法是否存在逻辑错误或边界错误(数组边界错误,或者循环次数多一次或少ー次)、程序的流程是否构造正确等,大大减少了不必要的编程工作量。这类似于装修前画出立体效果闇,让用户感受ー下在其中生活是否会感到舒服一样(如果用户感觉不好,可重新修改设计,大大减少了实际装修后オ返工的工作量)。第二,可使读者读懂比较难的程序或算法的能力得到极大的提高。希望读者把这ー重要的技巧至少用到分析和阅读几十个你认为比较难懂而又比较有趣的算法或程序上,使之成为你的编程工具箱中的一件“利器”。并且养成一种良好的编程习惯,在用伪代码写出算法后,不要急于把它转换成高级语言的程序,而是用此方法去验证算法,找出可能的早期错误。这也使写出伪代码算法的效益最大化。当然,即使通过这种验证,也不能保证这个算法是完全正确和完善的。但这种验证的重要价值,也绝对不能否认。这种验证的方法用于流程比较复杂的算法时,效果不一定好。如果不用此法来验证,而仅仅依赖于编译程序,很可能最终会得到ー个没有任何语法错

188误和运行时错误的程序。然而,此程序得到的结果要么可能完全是不正确的(逻辑错),要么此程序存在严重的边界错误。如果是逻辑错,则整个编程和调试工作完全是白费カ气。转换成C语言的程序如下:/・生成n个不重复的取值在1-M之间的随机整数・/1#include2#include3#include4#defineM545intmain()6(7inti,n,count,num;8intpk[M+l]={0};9printf(“请输入需要生成的不多于%d的随机数的个数:

189",M);10scanf("%d",&n);11count=0;12srand(time(NULL));13while(count

190这个程序加上注释。/・生成n个不相同的取值在1--M之间的随机数・/1#include2#include3#include4#defineM545intmain()6(7inti,n,count=0»num;8intpk[M+l];9labell:printf(”请输入需要生成的不多于吿d的随机数的个数:

191",M);10scanf("%dM,&n);11if(n>MIIn<=0)12{printf("输入的生成个数不符合要求、n”);gotolabell;}13for(i=0;i<=M;i++)14pk[i]=1;1516171819202122232425262728srand(time(NULL));while(count0)(printf(''%d,w,num);count++:pk[num]--;))答:可以完成同样功能。这里是用下标变量pk[num]的取值1来表示:随机数num是可以取一次的;而用下标变量pk[num]的取值。来表示:随机数num不能取了。这个程序更好,因为很容易将其改为每个随机数都可以取同样几次(而不仅仅是一次)的一般情况。习题1.编写一个程序,洗两副扑克牌(为玩扑克游戏“拖拉机”做准备)。也就是说,生成108个1到54之间的随机数,每个随机数都恰好取两次。习题2.随机生成并按生成顺序打印20个小写的英文字母,每个字母都不可重复。习题3.随机生成52个小写的英文字母,每个字母恰好重复两次。第35讲数据模拟——豆子落下问题类型必修题趣味性**难度**如图5.8所示的ー个容器中有500粒豆子,从入口处一个ー个地落下,通过三层隔板落入下面的4个格子中,豆子经过隔板向左右两方落下的机会相等,编写ー个程序,统计每ー个格子中的豆子数目。

192图5.8豆子落下算法分析:先来看看豆子如何才能掉入0号格子中,一粒豆子每遇到隔板都向左边掉下,就会落到0号格中。要掉到1号格中,有三条下落路径可以进入(从左到右,依次是在第一层、第二层、第三层豆子下落的方向)。1.左,左,右2.左,右,左3.右,左,左要掉到2号格中,也有三条下落路径可以进入。4.左,右,右5.右,左,右6.右,右,左通过仔细观察可以发现,要掉到1号格中,只需在下落的过程中有一次是向右掉下的:要掉到2号格中,只需在下落的过程中有两次是向右掉下的。可总结出一般规律:要掉到i号格中,只需在下落的过程中总共有i次是向右掉下的。用数组intnum[4];来分别记录掉在每格中的豆子数:用变量right来累计下落过程中向右掉下的次数。ー级算法:(1)初始化numD的数组元素都为〇(2)j=O;(3)while(j<=499)(right=O;②③④⑤⑥⑦/・只要随机值d等于1就向右掉下・/处理第一层落下;处理第二层落下;处理第三层落下;num[right]-H-;)(4)输出数组num中各元素的值其中②〜④需要进ー步求精。②的二级求精:取一个值为0或1的随机数d=rand()%2if(l==d)right++;③和④的二级求精与②的二级求精算法完全相同。转换成C语言程序如下:

1931#include2#include3#include4#defineN500/・豆子数・/5#defineM3/・隔板数・/6main()7{8intjtright»d;9intnum[M+l]={0};/*初始化num口数组元素都为〇・/1011srand(time(NULL));12j=0;13while[j

194并大大加快科研和实验的时间,就象此讲的例子。要做ー个非常精确的三层格板,使得豆子向左右两边落下的概率是ー样的,极为困难(还要精选圆豆子)。而用计算机来模拟和仿真,只要rand()%2所取到的数是完全随机的,这个仿真就比真实的三层隔板要好得多,既省时省又钱(还环保)。而且还很容易将此程序,快速转变为任意层隔板的豆子落下问题。同学们在毕业后的科学研究或技术革新中,在要进行实验或试验时,就应该首先认真想ー想,是否可以通过编程,用计算机来做这个仿真或模拟实验.编写程序相当于建实验室和购买实验设备;运行程序相当于做实验。得到的数据相当于实验的结果.不过,做此类事情时要特别注意:C语言库函数提供的伪随机函数rand()或random()的质量其实并不高,质量更高的随机函数参见附录G(参见第讲的点评)。习题1.请将此题的程序改为对任何多层隔板都适用的形式,隔板数由用户输入。・习题2.你能否采用一个二维数组,把每粒豆子落下来的路径都保存下来?・习题3.如果将盒子编号的顺序反过来即3、2、1、0(而不是0、1、2、3),你能否做出此题?第36讲在数组中对某个整数的查找和删除(程序阅读)类型选修题实用型开拓思路题趣味性**难度**对于ー个无序的数组,要在里面查找ー个数有两种方式:ー种方法是直接使用顺序查找法进行查找;另ー种是先对无序的数组元素进行排序(参见第讲,如果对该数组中的数据要频繁进行查找的话,就必须对该数组中的所有元素进行排序),在排好序的数组上就可以使用效率高得多的查找方法(比如二分法查找,参见第讲)。本讲中是ー组无序的数据,我们采用顺序查找法。顺序查找法的基本思想是从头到尾依次比较,宜到找到所要的数为止。由于算法和程序都比较简单(但却很重耍、很典型,本书不能不提到),我们直接给出C语言的源程序如下。本书不作讲解,请读者自己认真看懂它。1#include2#defineN103voidmain()4intnum[N],i,j=-1,k;5printf("请输入%d个数、n”,N);6for(i=0;i<=N-1;i++)7scanf("%d",&num[i]);8printf('*

195");9printf(“请输入要查找的ー个数值:'n");10scanf("%d",&k);11/・在数组中按顺序查找・/12for()

19620/・删除此数组元素num刈;/・流程到此,说明num,"]就等于k*/21(22printf("查到值为%d的数组元素是num[%d]

197",k,);23for(j=i;j<=N-2;j++)/・丛数组中删除该值。24num[j]=num[j+1];25}23for(i=0;i<=9;i++)/・打印删除ー个值后的所有数组元素24num[OInumillnuml21numlilnum[i+11num(i+2)137202328//num[0]nuni/Unum/21num(Hnum[i+H1372328图59删除值为k=20数据的数组前后变化图点评:此题中数组元素的查找,用的是从前往后逐个查找的办法,此方法的效率很低,更高效率的方法有二分法查找(请参见第56讲)等。此外,在排好序的数组中,删除ー个数据的效率也是很低的,需要把后面所有有效数组元素的值,逐个向前复制:算法效率很低。插入ー个数据,也是类似的。采用链表方式(请参看《数据结构》等其他教科书)存储・批数据,可以避免插入和删除数据时的极低效率;但往往在链表结构上査找ー个数据,却又比采用数组存储方式(采用二分法查找)慢得多。真是各有利弊呀!所以,往往需要程序员根据不同应用,进行综合考虑,决定到底采用何种存储结构。采用的存储结构不同,解决同一问题的算法可能也会有很大的不同。对此问题,可以借鉴的方法参见云风所著的《游戏之旅——我的编程感悟》ー书P19页,电子工业出版社。在C语言中采用数组方式,还有一个很大的麻烦,这就是:由于数组的存储空间大小,都是在编译期间确定的——而不是在程序运行期间动态决定的。因此,往往程序员要预留好很大的内存空间,来应对可能是很少出现的插入大量数据到数组中的情况。所以,采用数组方式,大量的内存空间可能会得不到充分的利用。这也是使用静态数组的另一大弊端。使用动态申请内存的链表,则可避免这一浪费内存空间的情况。在ー个排好序后的数组(一般是结构数组)或链表中,进行数据的查找、删除和插入,这是大量非数值计算类程序中经常要做的事(操作系统这个系统软件也不例外,参见9.4、9.5节)。

198小知识:C语言中,花括号、方括号、圆括号各有不同的用途,不要乱用.(1)花括号:用于构成分程序(复合语句),括住整个函数体,可在结构定义和在switch语句中使用,还可在对数组、结构变量初始化时使用。(2)圆括号:在表达式中表示优先运算(可嵌套使用);在函数调用、函数定义和函数声明时使用.(3)方括号:在数组定义时使用,在存取数组元素时使用。其他各种符号的使用,参见第7章的7.3节.4.3ー维字符数组和字符串ー维字符数组定义的格式为:char数组名[常量表达式];比如charstr[100];〇它用来存放ー个ASCII文本或ー个字符串,用以进行进ー步的处理。除了具有一般ー维数组的共同特点外,还有以下几点特殊之处:(1)不必通过循环来遍历每个数组元素,就可以用%s格式整体输入一个字符串到字符数组中,例如用以下的语句:scanf(w%sw,str);注意:此处用的是%S格式,而不是输入单个字符时用的%C格式。注意采用%s格式调用scanf函数,在程序运行过程中输入字符串时,字符串中不能包含有空格符、跳格符(Tab)或回车符,这些字符都会被系统认为字符串的输入已经结束.要输入包含有空格等字符的字符串,要调用其他的字符串输入函数,比如gets或fjgets.与之相对应的输出函数是puts和母uts。但不提倡用gets,而提倡用fgets函数.(2)注意,上述的scanf。函数调用语句中,数组名前面并没有用取地址符号。其原因是在C语言中,数组名所表示的,就是ー块连续内存空间的首地址(这块内存空间是系统分配给此数组,用来存放该数组所有数组元素的数据的)。所以,不像在普通变量名前要用取地址符号。字符数组名出现在scanf的输入参数中,不必使用取地址符号。(3)字符串以%s格式输入后,系统会自动在字符串末尾加上一个字符串结束标志ヘ〇’(这是ー个转义字符,对转义字符的介绍可参看第2章的提高部分),也就是ASCH码值为〇的这ーー特殊字符。这个字符也被系统的某些头文件定义为NUL这个符号常量(也就是说,NUL与へO’其实是ー样的)。(4)任何字符串都是以へ0,作为结束标志,但字符数组可以不用''(T作为最后ー个数组元素。也就是说,只有当ー维字符数组的最后ー个有效字符为'、0'时,它才能构成一个字符串(注意有些输出函数只能用于输出字符串——不能用于输出字符数组,比如输出函数puts和fputs)。例如:如果定义了一yF"一・维字符数组charstr[10];假设开始时,该数组所对应的连续内存中存放的字符(其实是对相应的ASCH码)如下:str[O]str[l]str[2]str[3]str[4]str[5]str[6]str[7]str[8]str[9]HELL0WORLD这就仅仅是ー个字符数组ーー在这个数组中没有保存任何字符串(此时,如果使用puts(str)进行输出就会出错)。

199而在运行程序的ー些语句后,以上同一个字符数组的内存中假设变为:str[O]str[l]str[2]Str[3]str[4]str[5]str[6]str[7]str[8]str[9]HELLO!\0RLD或str[O]str[l]str[2]Str[3]str[4]str[5]str[6]str[7]str[8]str[9]HELLOj\0\0\0\0在这个数组中就存放了一个字符串:“HELLO!”。此时,如果使用puts(str)进行输出就不会有任何问题。请读者区分清楚:字符算组与存放在某字符数组中的字符学这两个容易混淆的概念。注意:字符串的长度与字符数组的长度是不同的概念。在该例中,字符数组的长度是10,而HELOLO!这个字符串的长度是6(而不是7,它不包含字符串结束标志''0')。(5)在对字符数组初进行始化时,既可以按ー维数组通常的方式进行:charstring[20]={'H','e',T,T,'o','w',’0','r',T,'d',〇'};也可以按更为简洁的方式进行:charstring[20]={•,Helloworld!"};或者,还可以省略花括号(但双引号不能省略):charstring[20]=4'Helloworld!";对于后两种情况,编译程序会自动加上字符串结束标志''0'。注意:以如下这种方式定义和初始化字符数组也是允许的:charstringロ="HeUoworld!";这时,编译程序会自动加上字符串结束标志,并为此字符数组分配刚好够用的字节数:13,每个字符占用ー个字节。(6)在程序中需要对字符串(或字符型数组)进行操作时,通常最好调用库函数来进行。最常用的几个库函数如下:①字符串连接:strcat()。②字符串复制:strcpy。。③求字符串的长度:strlen()o④比较两个字符串的大小:strcmp()。注意在使用这些库函数时,要包含<string.h>头文件,常用的字符串处理函数的使用可参见附录C中的库函数简介.编写对字符串进行基本处理的程序,也是当代程序员和编程高手必须熟练掌握的基本功。这ー类问题比较枯燥,但读者应当引起重视,认真学好这一部分内容。

200第37讲输入一行字符串,判断这行字符串是否回文类型必修题趣味性**难度**所谓的回文,比如level,就是从左到右的字符序列与从右到左是ー样的。算法分析:首先这个字符串可以放入ー维字符数组中。由于系统会自动加一个ASCII码为0的空字符(用‘、〇’来表示)作为字符串结束标志,调用库函数strlen()(或根据该结束标志)可求得此字符串的长度L(注意:此长度不包含字符串结束符へ(T)。然后,只要将str[O]与str[L-l]相比,s叩]与str[L-2]相比…不难推出,在一般情况下是将st巾]与str[L-(»+l)]相比。现在还要求出数组中字符串的中位数,用来得到对比结束的条件。因为整个数组的长度有L个字符,中位数就是L/2(整除)。如果L是奇数,表达式L/2的值正好是中位数(比如,长度为5的字符串,各字符分别存放在str[0]、s叩]、str[2]、str[3]、str[4]中,str[5]中存放的是NULL(へ0)L/2的整除商就是2,正好是中位数)。此数只有一个,不必比较。如果L是偶数,表达式L/2的值就是比中位数大1的数(比如,长度为4的字符串,各字符分别存放在str[0]、str[l]、str[2]、str[3]中,str[4]中存放的是字符へ(F,L/2的整除商就是2,所以也不必比到し/2).ー级算法:(1)以%s格式读入一字符串到字符数组str中;(2)求得该字符串的长度length;(3)i=0;(4)flag=O;/・假设是回文,即用标志变量flag=O来表示此事・/(5)while(i<length/2)/・将每对字符进行比较・/{if(str[i]!=str[length-(i+1)])①flag=l;/・不是回文,将标志变量flag设为1*/5.2i++;)(6)if(O=flag)打印是回文;②else不是回文;③转换为C语言程序如下:1#include2#include3main()4(5inti,length,flag;6charstr[100];78scanf(H%sH,str);9length=strlen(str);10i=0;11flag=0;12while(i

2011{2if(str[i]!=str[length-(i+1)])3flag=l;4i++;5)6if(0==flag)7printf("打印是回文:

202”);8else9printf("打印不是回文;

203”);1011}问题1.用strlen函数得到的字符串的长度是否包括字符串结束标志NUL(或へ0,)?答:不包括。问题2.在程序运行中,要存储ー个由用户输入的长度最长为L的字符串,字符数组str至少要定义成多大?答:charstr[L+l];,一定要给字符串结束标志’\0‘预留一个存放的位置。问题3.第8行的语句是否可用以下语句代替?问题4.以下程序段是否正确?intmain()charss[30];{scanf(M%c,,,ss);答:错。问题5.以下程序段是否正确?intmain()charss[30];{scanf(w%sw,&ss);答:错。习题1.本例题采用了一个变量,请采用两个变量i和j,令i从1开始变化,每次比较后i加1,j从strlen(str)-1开始变化,每次比较后j减1,当i〈j一直做。重做本例题。习题2.反向显示字符串。第38讲统计一行字符串中每个小写英文字符出现的次数类型必修题趣味性***难度**讨论:通过统计ー篇长篇文章中每个汉字使用的频率,基本上可以断定此篇文章是不是某个著名作家写的。也就是说,每个人都有他特定的用字频率谱。每个人之间的用字频率谱与他的指纹、笔迹类似,也是确认文章作者身份的ー个标志。此题是解决这ー类问题的基础。准备工作:可以事先将26个英文字符依次存放在ー个字符数组中,即将:'arch[0]3-*ch[1]'z,—ch[25]

204然后用num[0]来统计(累加)小写字符匕,出现的次数。用num"]来统计(累加)小写字符b出现的次数。用num[25]来统计(累加)小写字符セ出现的次数。ー级算法:(1)将26个英文字符依次存放在ー个字符数组中(2)从键盘输入一行字符存到字符串str中(3)i=0;(4)while(字符串未完)(让j从〇变化到25,如果str[i]与某个ch[j]匹配(即相等),那么num[j]++;①i++;②)(5)输出每个字符出现的次数。其中①和(5)需要进ー步求精。①的二级求精:让j从〇变化到25,如果st币]与某个ch[j]匹配,那么num[j]++;for(j=0;j<=25;j++)(if(ch[j]==str[i])num[j]++;}第(5)步的二级求精:输出每个字符出现的次数for(i=0;i<=25;i++)(printf("小写字符%c出现的次数是・d

205",ch[i],num[i]);转换成c语言的程序如下:1#include23intmain()4{5charch[26];6intnum[26];7inti,j;8charstr[100];910/・事先将26个英文字符依次存放在ー个字符数组ch口中,将numn清。*/11chr=a';12i=0;13while(i<=25)14{15ch[i]=chr;16num[i]=0;17chr=chr+l;18i++;

20619)20scanf("%s",str);21i=0;22while(str[i]!=/\0/)23(24for(j=0;j<=25;j++)25{26if(str[i]==ch[j])27num[j]++;28)29i++;3031)32/・输出每个字符出现的次数・/33for(i=0;i<=25;i++)34(35printf("字符+c出现的次数是+d

207",ch[i],numは]);36}37)点评,查表法的使用此题的解法中,实际上用两个数组联合起来,构成了一张字符出现次数表(更为简洁的方式是用ー个结构数组来构成一张查找表,而不是用两个存放简单变量的数组,参见第8章的8.!节),见表5.1。表5.1各字符出现的次数数组下标ch[]中依次存放英文字母num"中存放字母出现的次数0七’num[0I存放的字母‘a"出现的次数1力’num[l]存放的字母へ,出现的次数2'C'num[2]存放的字母セ’出现的次数3て’num[3]存放的字母出现的次数24ッ,num[24]存放的字母‘y'出现的次数25num[25]存放的字母,z'出现的次数每读入文章中的一个字符,即可查找此字符与下标值为多少的ch5的数组元素相等(用来确定到底是出现了哪个字符),然后把对应i值的num国的值加1——即出现的字符数加!«利用査表法,可以把一些多分支的选择结构转换为循环结构。问题1.是否可用查表法来实现所有的多分支结构?如果不能实现所有的多分支结构,那么指出何种情况下可用查表法来实现多分支结构(请参考《代码大全》一书对此问题的讨论)?问题2.是否可以删掉第11行、第!5行和第17行,而把第5行改为:charch[26]=Mabcdefghijklmnopqrstuvwxyzw;答:这很不好,字符数组的长度不够,字符串结束标志へ0',在数组中没有位置可以存放,如果改成:charch[27]="abcdefghijklmnopqrstuvwxyz”;或者

208charch[]=**abcdefghijklmnopqrstuvwxyz";都可以。习题!.用多分支的选择结构(switch语句)重新做此题,比较一下两种解法各自的特点。习题2.请模仿本题的查表法改做以前做过的某个习题。・习题3.在学了第8章后,请将整个ー篇英文文章从文件中读入,统计该文章中各英文字符出现的次数。・习题4.在学了第8章的结构数组后,请将此题改为用结构数组来实现。第39讲统计一个字符串中字母和数字的个数类型必修题趣味性**难度**ー级算法:(1)输入字符串到str;(2)字母个数chajnum清〇;(3)数字个数digitnum清〇;(4)读入字符串str中的第一个字符到ch;(5)while(ch不是‘、〇‘)(if(ch是数字)数字个数加1;elsei出ch是字母)then字母个数加1;读入字符串str中的下ー个字符到ch;(6)打印字符串中的数字个数和字母个数;“ch是数字''的C语言表示为:(ch>=’0'&&ch<='9')。“ch是字母'’的C语言表示为:((ch>='a'&&ch<=4z')||(ch>=*A,&&ch<=*Z'))请读者自己把此题的算法转换成C语言的程序,并上机调试第40讲模拟输入库函数,将数字字符串转换为十进制整数类型开拓思路题趣味性**难度**小知识:理解PC机中的键盘工作原理:(摘自《PC技术内幕》第8版,清华大学出版社PeterNorton著张琦等译P245页)由键盘(按下或松开某个键后)送入PC机的信息经过特别分配的键盘串行输入口进入PC机的主机。从那儿,信息就就到了我们所定义的“键盘控制器”(何勤注,即键盘的输入输出接口电路)。PC机系统单元的键盘控制器可以是一个微小的嵌入式计算机,它有自己的微处理器、

209带有固化程序的ROM和一些RAM,“键盘控制器”也可由主板芯片组的一部分来完成。通过键盘,键盘控制器获得了有关键被按下或释放的信息。每当它得到这样的信息,键盘控制器都会发出两个动作。第一个动作是将代表了击键信息的扫描码放入缓冲区中(一小块RAM,它通常处于主存的低地址区)。第二个动作是引发ー个硬件中断,中断号为9(何勤注:有关中断的简要介绍,请读者参见《轻松学习C程序设计》一书的第9章,9.5节)。必须理解这样ー个十分重要的事实:扫描码对应的是你所按下的键,而不是你所想输入的字符。于是a和A的扫描码是相同的。扫描码比须根据三种击键和换档(字母、数字、Ctrl键和Alt键)中的每一种以及三种换档锁定键(caps键、数字键和scroll键)的当前状态来确定使用者的击键动作的含义是什么。(何勤注:这就是为何按下ー个键有时候程序(这可能是一个字处理程序)在屏幕上显示ー个字符,有时侯代表的却是ー个游戏程序中按下的一个快捷键的根本原因。)(在PC机上此过程实际上比较复杂:先由键盘接口电路将此按键的扌I描号存放到输入缓冲区中,然后键盘接口电路向CPU发出一个中断信号,此中断信号如果最后是调用DOS中的9号中断处理程序进行处理的话,则会将此按键所对应的ASCII码(但还要判断上档键shift是否按下)存放在键盘输入缓冲区中。不过很多DOS下的游戏程序会用自己的9号中断处理程序去取代DOS中的9号中断处理程序。这样ー来,你在运行游戏程序时按ド的键就很可能被当作游戏中的某个快捷键了)讨论:人们在PC机的键盘上连续敲击数字键时,键盘输入输出接口电路(即键盘控制器)会把相应数字字符按键的扫描码(此扫描码与ASCII码有着对应关系,参见上述的小知识)存放到内存中的ー个固定区域——即键盘输入缓冲区中(并发出9号中断信号,参见第8章9.5节)。那么,C语言中的输入函数最后是如何把这些ー个ー个输入的ASCII码形式(至于如何由键盘扫描码得到对应按键的ASCII码本书不作深入讨论,读者可参考Peter.Norton著张琦等译《PC技术内幕》第8版,清华大学出版社。在DOS操作系统控制PC机的情况下,这是由9号中断处理程序来处理此事的)的数字字符串转换为整数或实数(实际上最终应转换成二进制形式的整数或实数)的呢?这就是本讲所要研究的内容。算法分析:首先来研究如何把字符串中的单个数字字符转换成一个十进制的数值。这个数字字符用str[i]来表示(开始时i=0),str被定义为字符数组,转换后的十进制数值用整型变量n来表示。因为字符在C语言的程序中出现的形式虽然是用单引号括住的单个字符,但是,这种字符变量或字符常量可以出现在算术表达式或布尔表达式中,这个字符变量或常量的值就是相应字符的ASCI!码值。因此整型变量n的值可按如下的赋值语句求出(查看ASCH码表可知,10个数字字符在表中是连续在一起的,’〇'的ASCH码值最小,’9’的最大)。n=str[i]-lO';(5.3)再进ー步讨论,如果要转换的位数为两个的话(即几十几),因为字符串(即字符数组)中下标号由小到大表示的是由高位到低位的数字(即str[0]如果表示的是百位数,那么str[l]表示的就是十位数,str⑵表示的就是个位数),那么只要将式(5.3)改为:n=str[i]-'0'+n*10;(5.4)即可,右边表达式中的n是变量的老值(十位数上的值),新读入的st巾ド(F是个位数的值。如果是多位数,仍然可以采用式(5.4)»也就是说,式(5.4)就是对于此问题通用的式子。ー级算法:(1)读入待转换的数字字符串fstr(2)i=0;

210(2)n=0;(3)while(str[/]!=l\O,)n=str[i]-'0'+”*10;H+;)(5)打印n的值转换成C语言程序如下:1#include2intmain()3{4charstr[30];5intn,i;6printf(”请输入ー串纯数字形式的字符串、ね”);7scanf(w%s",str);8n=0;91=0;10while(str[1]!='\0')11{12n=str[i]-'0-+n*10;/・由n的老值和当前读入的数字字符求出n的新值・/131++;14)1516printf('、转换后的数字为+d”,n);17)问题.此程序有什么地方有待改进?答:如果输入的不是数字字符,此程序就会运行不正常。此外,如果位数太多,超出整数的范围,此程序也会出问题。习题1.请将存在隐患的上述程序改写成更为健壮的程序。所谓健壮的程序是指程序对不正确的操作和输入不会无所适从,程序运行不会混乱,甚至会指出用户操作的错误。习题2.将带有一个小数点(即英文或半角输入法下的句点)的数字字符串转换成十进制实数。提示在有了例题的基础后,此习题就比较容易做出来了.关键点在于读字符串中的字符时,要注意小数点的出现,在出现小数点后,开始求一个系数k的值,k的初值为1,在出现小数点后,每出现ー个数字字符,此系数k的新值就要乘以10.这样,再以上述类似的方法求出ー个整数后,将此数最后用k来除,得到的就是这个实数。注意对于字符串的处理,在实际编程工作中,最好尽量多用库函数。但对于学习编程知识来说,自己动手编ー些基本的字符串处理程序还是很有必要的。第41讲类型开拓思路题趣味性**难度**

211检查表达式中的左右圆括号是否配对,并判断表达式中圆括号的使用是否正确(这是编译程序要做的一部分工作)算法分析:表达式中的左右括号数不仅要相等,而且不能先出现右括号,比如3*(x+3.7)是ー个正确的表达式,而3*)x+3.7(则不是一个正确的,因为在表达式中先出现了右括号。此外,(3*4.2)-)x+3.7(/2.6也是错误的表达式。在此表达式中,虽然没有先出现右括号,但是如果从左到右依次读入表达式中的字符,并统计此表达式中的左、右括号数时,会发现右括号出现的次数比左括号多(在读到第三个圆括号时)。结论:在按上述(从左到右的)顺序读入表达式的字符时,任何时右括号数都不能比左括号数多。ー级算法:(0)读入字符串到str:(1)左、右括号数left、right清零(2)i=O(3)当(str[i]k\(T){统计左右括号数;①i++;②)(4)如果(left等于right)那么打印“左右括号相等”(5)如果(left>right)那么打印’‘左括号多’’其中①需要进一步求精。二级求精:统计左右括号数;如果(str[i]是左括号)那么左括号数left加1;如果(str[i]是右括号)那么{右括号数right加!;如果(right>left)那么{输出“先出现了右括号”;程序中止:}转换成C语言程序如下;(1)#include(2)#include(3)intmain()(4){(5)charstr[100];

212(6)intleft=0,right=0,i;(7)(8)scanf("をs",str);(9)i=0;(10)while(str[i]!=,\0/)(11){(12)/*3.1统计左右括号数・/(13)if('(,==str[i])(14)left++;(15)if(')/==str[i])(16){(17)right++;(18)if(right>left)(19){(20)printf("先出现了右括号、n");(21)exit(0);(22))(23))(24)i++;(25)}/*endwhile*/(26)(27)if(left>right)(28)printf(”左括号大于右括号、n");(29)if(right==left)(30)printf(、,左括号等于右括号'n");(31)}习题.统计ー篇英文文章中的单词数。提示:注意每个新单词开始时(连续两个字符,前ー个字符是空格,后ー个是单词中的非空格字符)的特点。4.4二维数组既然编译程序允许人们在高级语言中用一个数组名加上一个下标的方式,来存取一片连续的内存空间中存放的一批同类型数据中的任何ー个,那么在需要时,编译程序也允许人们用一个数组名加上两个(或更多个)下标的方式,来存取一片连续的内存空间中存放的一批同类型数据中的任何ー个。这种以两个下标来定义(说明)数组,并存取ー批同类型变量中的数据方式称为二维数组。逻辑上或现实生活中有很多这样的情况,要处理的数据呈现ー种线性结构,比如排队、列表、数学中的向量、数据的有限序列。对这种线性结构的数据,用ー维数组(或后面要学到的链表)来处理往往是比较合适的。这种合适主要体现为,在逻辑或位置上相邻的数据在计算机中也存储在相邻的内存位置上,这就为按顺序批量快速处理数据和编写高效程序带来很大方便。对ー维数组来说,高级语言其实是用数据在内存存储空间中的相邻对应数组元素中下标值的相邻,进而用数组元素下标值的相邻来体现数据之间在逻辑或位置上的相邻关系。但在实际编程中,有时需要处理更复杂的结构。例如编写棋类游戏的程序时,耍处理的是放在二维棋盘中的棋子;在计算机的数值计算应用方面,经常需要表示和处理矩阵。棋盘和矩阵不是线性结构,它们有两个维。其数据所在的位置需要通过两个下标(可以用这两个下标来表示坐标X和y)来指定。而对于宇宙航行、飞机导航、三维游戏、虚拟现实等,其数据所在的位置需要三个下标来确定。C语言中也能定义二维(具有两个下标)和更多维(更多下标)的数组。二维数组在C

213语言中的定义如下:类型名数组名[常量或常量表达式1H常量或常量表达式2);从ー个角度看,inta[3][2]:可以看成是具有两个下标的数组。任何二维数组中的每ー个数组元素,都要由两个下标值才能唯一确定其数据在(系统分配给该二维数组的)这片连续内存中存放的位置:数组元素a[0][0]a[0][l]a[l][0]a[l][l]a[2][0]a[2][l]内存偏移地址048121620(假设ー个数组元素要4个字节存放)从另一个角度看,在C语言中,还可以把二维数组看成是ー维数组的数组,即有这样的ー个ー维数组,这个数组中的每ー个数组元素还是ー个ー维数组。比如二维数组inta[3][2]:其实可以看成有三个ー维数组:它们分别是doublea[0][2],doublea[l][2],doublea[2][2](数组名分别是a[〇]、a[l]、a⑵)。这三个ー维数组,每个都有2个数组元素。名为a[0]的ー维数组的数组元素分别是a[〇][0]、a[0][l]o这对应上表中的前两个。名为a[l]的一维数组的数组元素分别是a[l][0]、a[l][1]。这对应上表中的中间两个。名为a[2]的一维数组的数组元素分别是a[2][0]、a[2][l]。这对应上表中的后两个。在C语言中,虽然二维数组实际上是按照行优先(即左边的下标优先)的顺序,依次存放在呈现出维编程结构的内存中。但人们还是按照日常生活中的习惯来称呼它,即把数组inta[3][2]:称为3行2列的整型数组。对于数组charstr[4][20];,我们把它称为4行20列的字符数组(或者把它称为4个长度为20的ー维字符数组)。对于二维数组来说,高级语言其实是用数组元素下标的相邻关系来体现数据之间在逻辑或位置上的相邻关系。例如数组元素与a[2][/],不论i和j的值为何,两个数组元素中所存储的数据(在地理或逻辑上)的位置一定相隔两行(a[〇]国在上,a⑵[/]在下);而数组元素与a[n][i]中所存储的数据(在地理或逻辑上)一定是在相邻的两列上在左边,a[〃]口在右边),而不论m、n和i的值是多少。点评,ター维数组和二维数组的ー个重要不同点对ー维数组来说,高级语言其实是用数据的内存空间上的相邻对应数组元素下标值的相邻,用数组元素下标的相邻来体现数据之间在逻辑或位置上的相邻关系。对二维数组来说,高级语言其实是用数组元素卜标的相邻来体现数据之间在逻辑或位置上的相邻关系。但数组元素某ー个下标的相邻不再表示在内存中的存储位置相邻了。也就是说,逻辑或位置上相邻的线性数据应存储在下标相邻的ー维数组元素中,这样就确保数据在内存中的相邻;而对于逻辑或位置上相邻的平面数据,应存储在下标相邻的二维数组元素中,但这样并不能确保数据在内存中的相邻。这个不同使得在C语言中遍历二维数组的所有数组元素时,要注意应当以先按行后按列的次序来进行(如果按先列后行的次序,数据存取的速度就会稍慢一点)。第42讲把数值1〜12存放到ー个3行4列的二维数组中,然后按行打印类型必修题趣味性・难度・

214算法分析:要求程序运行的结果如下:num[0][0]1num[0][l]2num[0][2]3num[0][3]4num[l][0]5num[l][l]6nim[l][2]7num[l][3]8num[2][0]9num[2][l]10num[2][2]11num[2][3]12可以看出,如果用i作为第一个下标的循环变量,它的取值应为从0〜2,j作为第二个下标的循环变量,它的取值应为从0〜3。中所要存放的数值与下标之间的关系也不难得出。先看第一行(此时第一个下标i=O),很容易确定任何数组元素的取值与第二个下标j的取值之间的关系为(i)+j+l。再看第一列(此时第二个下标j=O),很容易确定的取值与第一个下标i之间的关系为a[i][jH*4+l+g(j),把这两个式子综合起来就可以得到a[i][j]=i*4+j+l»这是ー个普遍适用的式子。由于程序比较简单,可以直接用C语言写出来,如下:(1)#include(2)intmain()<3)((4)inti,j,num[3][4];(5)(6)for(i=0;i<3;i++)(7)for(j=0;j<4;j++)(8){num[i][j]=(i*4)+j+l;}(9)(10)/・将数据打印出来・/(11)for(i=0;i<3;i++)(12){(13)for(j=0;j<4;j++)(14)printf(''num[%d][%d]=%dnum[i][j]);(15)printf(w

215w);(16))(17)(18))问题1.把输入数据的二重循环(第6〜8行)改写成以下的语句是否可以?for(i=0;i<4;i++)for(j=0;j<3;j++){num[j][i]=(j*4)+i+l;)答:可以。不过这是按照列优先的情况进行赋值,即首先依次赋值给num[0][0]=l,num[l][0]=5、num[2][0]=9,这三个第〇列的数组元素,然后是num⑼ロ]、numロ]ロ]、num[2][l],等等。这两种方式的结果是ー样的。不过,因为在C语言中,数据是按行优先的方式顺序存放在内存中的(但fbrtran语言却是按照列优先来存储的),所以后一种方式的程序运行速度不如前ー种方式快。问题2.如果第!2行和第!6行的花括号漏掉没写,情况会怎样?答:这样原本属于外层循环的第!5行的句子print(C

216");现在不属于外层循环的循环体,所以不会换行。所有数据全放在同一行上,一行放不下的话,系统会自动换行。问题3.删掉第6、7、8行,把第4行的数组定义改为:intnum[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};是否可以?答:可以,数组可以在定义时进行初始化。问题4.删掉第6、7、8行,把第4行的数组定义改为:intnum[3][4]={l,2,3,4,5,6,7,8,9,10,11,12};是否可以?答:可以,问题3是把二维数组看成是ー维数组的数组进行初始化,而本问题是把二维数组看成是有两个下标的二维数组,由于在内存中二维数组的数组元素是按行优先的方式存放

217的,所以,1、2、3、4依次被安排给了num[〇][〇]、num[〇]ロ]、num[〇][2]、num[〇][3]、4、5、6、7依次被安排给了numロ][〇]、numロ]口]、numロ][2]、numロ][3],等等。问题5.请问是采用问题3或4的方式给二维数组赋初值,还是采用例题中的程序好?答:没有确切的答案,要看使用场合而定。语句方式赋初值方式灵活,可以很容易改成更一般的形式(参见习题1)。不过,如果需求不会有任何变化,数组又较小,则直接在定义时赋初值更简洁、明了。问题6.是否一定要将逻辑或地理上位于第一行的数据存放在二维数组的num[0][i](在本题中i从〇变化到32)之中?答:最好这样存放,但不是一定要这样存放不可。此方式存储最大的好处在于,编程序时比较方便。习题1.请将此题改写为给m行n列的二维整型数组分别赋初值0,1,2,-,mXn-1,将m和n定义为符号常量。*・习题2.请将习题1改写为ー个独立的函数,以便于其他的程序调用,m和n作为形参,二维数组也作为形参(此题在学完函数一章后可做)。第43讲编写ー个程序,将一个3行3列的矩阵转置类型必修题趣味性・难度**以下的矩阵:123456789经过转置(即原来的第i行变成了现在的第i歹リ)后成为:147258369讨论:首先将此矩阵存放在二维数组中,其形式如下:n[0][0]1n[0][l]2n[0][2]3n[l][0]4n[l][l]5n[l][2]6n[2][0]7n[2][l]8n[2][2]9现在要通过转置变成如下的形式:n[0][0]1n[0][I]4n[0][2]7n[l][0]2n[l][l]5n[l][2]8n[2][0]3n[2][l]6n[2][2]9由此可见,主对角线上的数组元素n[0][0]、n[l][l],n[2][2]并没有变化,只需将主对角线右上的数组元素n[0]ロ]、n[0][2]、n[l][2]分别与主对角线左下的数组元素n[l][0]、n[2][0]、n[2][l]的值通过一个临时变量进行交换即可。总共只要进行三次交换。最初想到的算法是这样的,即遍历所有的数组元素n[i][j],利用临时变量temp,将它和与之对应的

218n[j][i]进行交换。更精确ー些,表示如下:for(i=0;i<3;i++)/*1*/for(j=0;j<3;j++)/*2*/{/*3*/if(i!=j)/*4*/{1*5,Itemp=n[i][j];/*6*/n[i][j]=n[j][i];パフ*/n[j][i]=temp;/*8*/}/*9*/}/*!.0・/但这样做是否正确呢?先来看外层循环ー共有多少次,循环变量i的值可取。、1、2,共三次循环。对于每一次外层循环,都嵌入了一个变量为j的内层循环,内存循环也有三次,所以内层循环总共会运行3X3=9次。除去(i=0,j=0)、(i=l,j=l)、(i=2,j=2)的三次外,算法第4行的if语句中的布尔表达式(i!=j)会在其中的6次循环中成立,所以ー共进行了6次形如n[i][j]与n[j][i]的数组元素值之间的交换。因此这个算法是错误的(正确的应该

219是进行三次交换)。错误的原因是没有限定只能将主对角线右上的数组元素(源数)与主对角线左下的数组元素(目的数)进行单方向交换;而不能将主对角线左下的数组元素(源数)与主对角线右上的数组元素(目的数)交换。所以改进的方法很简単,只需将if语句的条件修改一下,使之变成0>i)即可。算法改为:for(i=0;i<3;i++)/*1*/for(j=0;j<3;j++)/*2*/{/*3*/if(j>i)/*4*/{/*5*/temp=n[i][j];/*6*/n[i][j]=n[j][i];/*7*/n[j][i]=temp;/*8*/}/*9*/)点评.タ程序中的错误在编程时很容易犯错误,所以在构思ー个问题的算法时首先要尽量认真仔细思考,确保它是正确的。这ー类算法错误有时是很难发现的。这也是为何程序编写完并通过编译后,还要进行测试的原因所在。测试方法有很多种,其中传统的有黑盒测试和白盒测试两种,比较新颖的是通过事先编写测试程序来驱动软件开发的过程(参见《测试驱动开发》)。目前较受关注的是单元测试(参见《单元测试》)。转换成C语言的程序如下:(1)#include(2)main()(3)((4)intn[3][3]»{1,2,3,4,5,6,7,8,9};(5)inti,j,temp;(6)(7)for(i=0:i<3;i++)(8)for(j=0;j<3;j++)(9)((10)if(j>i)(11){(12)temp=n[i][j];(13)n[i][j]=n[j][i];(14)n[j][i]=temp;(15)}(16)}(17))问题1.是否可以去掉第10行,同时将第8行改为:for(j>i;j<3;j++)答:可以。可以用画内存变量取值变化图来验证这ー点。问题2.第4行是否可以改为其他的形式?

220习题1.利用学过的二维数组打印杨辉三角形(打5行)。11121133114641(提示:把它们看作二维矩阵中的系数,除了第一列和主对角线外,其余数的左下三角的系数都满足:任何ー个系数是它上一行的两个系数之和,即a[i]D]=a[i-l]U]+a[i-l]U-l]第44讲游戏中炸弹爆炸对精灵的伤害平面坐标系(共有9X12格)中有多个精灵,精灵所在的位置都由它们的坐标i和j决定(假设坐标值都为整数,战棋类游戏大多是这样的),精灵的生命值为ー个炸弹在坐标为(iOJO)(iO和jO也都是整数)处爆炸,对精灵的伤害h(即失去的生命值)由精灵到爆炸处的距离d而定。d<2h=302

221①计算精灵与炸弹间的距离d;②根据d求出精灵生命值的损失h;③更新精灵的生命值e[i]U];④如果精灵的小于等于0,那么,输岀“位于坐标点ij的精灵已经死了'并将印]5清零;否则输出“位于坐标点ij的精灵的生命值;其中②需要三级求精,此三级求精算法请读者自己做。此算法的示意图如图5.9所示。第JUHP(1W)-?幘具2HP(±*<1)-♦鞫13-I4t<4HP(l^a)图5.9程序的示意图转换成C语言的程序如下:(1)#include(2)main()(3){(4)inte[12][9]={0};(5)inti,j,io,jo;(6)floatd;(7)e[1][4]=90;(8)e[5][1]=110;(9)e[8][6]=150;(10)e[10][0]=130;(11)printf("请输入炸弹的落点、n");(12)scanf(w%d%dw,iO,j0);(13)for(i=0;i<=ll;i++)(14)for(j=0;j<=8;j++)(15){(16)if(e[i][j]!=0)/*startif*/(17){(18)d=sqrt((iO-i)*(iO-i)+(jO-j)*(jO-j));(19)if(d<2)(20)e[i][j]=e[i][j]-30;(21)elseif(d<3)(22)e[i][j]=e[i][j]-25;(23)elseif(d<4)(24)e[i][j]=e[i][j]-20;(25)elseif(d<5)

222(1)e[i][j]=e[i][j]-10;(2)else(3);(4)if(e[i][j]<=0)(5){printf(”位于坐标点をd,%d的精灵已经死了",i,j);(6)e[i][j]=0;(7)}(8)else(9)printf(”位于坐标*d,上d的精灵现在的生命值为%d”,i,j,e[i][j]);(10)}/*endif*/(11)}/*endfor*/(12))问题1.如果水平方向上的任何坐标点i(不管j的值是多少)与任何坐标点i+1的垂直距离都是2个单位;任何坐标点j(不管i的值是多少)与任何坐标点j+1的水平距离都是3个单位。请问上述程序要如何改动?问题2.第27、28行是否可以删掉?答:可以,这是标准的多重嵌套if语句所要求的格式,第28行是一条空语句。问题3本题没对用户输入的炸弹坐标进行检测是否合符要求,请您修改代码。习题1.请将此题改写为可以多次扔下炸弹。・习题2.在学习了第9章的结构后,将此题改写为:将一个精灵的坐标值和生命值分开来,并都存放在ー个结构变量的成员中;所有精灵的数据全都放在ー个结构数组中,重做此题。・习题3.在学习了文件输入输出后,请将全部精灵的数据(生命值和坐标)从文件中读取出来,并在关闭程序前保存到文件中。*・习题4.本题中7、8、9、10行将精灵的初始生命值和坐标值都限制死了,你能否修改这个程序,使得程序运行时,精灵是随机产生的:产生的位置是随机的,每隔两分种产生两个精灵,精灵初始生命值都是200。用户一直都可以输入它投下的炸弹坐标直到用户输入的炸弹坐标为ー1,-1时,程序オ结束。第45讲统计全班学生的期末考试成绩输入全班同学的期末考试各科成绩(ー科一科地输入),ー共n门,求每个学生的平均成绩以及全班各科的平均成绩,假定班上有m人类型必修题趣味性・难度**讨论:定义二维数组:intstu[50][5],对于stu[i][j]来说,第・维(表示行的,即靠左边的)下标i用来表示学生的编号,第二维下标j用来表示是哪门课。现规定次序分别是数学、语文、英语、物理、生物。也就是说,数组元素stu[3]⑵用来存放第4个学生的英语成绩。50个学生每人的平均成绩要定义一个ー维数组floataverage[50]来存放。全班各科平均成绩也要用一个数组class_average。]来存放。ー级算法:(1)50个学生每人的平均成绩数组average的所有元素清0(2)全班各科平均成绩数组class_average的所有元素清0(3)for(i=0;i<50;i++)for(j=0j<5;j++)(3.1提示:请输入第i个学生的第j门课的成绩3.2输入第i个学生的第j门课的成绩)(4)求每个学生的平均成绩

223(5)求全班各科的平均成绩(6)显示所有学生的成绩和平均成绩(7)显示全班各科的平均成绩其中第4步和第5步需要二级求精。第4步的二级求精:分析:先求第i个学生的平均成绩,然后令i从0逐次遍历至49即可。先求第i个学生的平均成绩,如下:(1)j=O;(2)sum=O;(3)while(j<5)/・累加第i个学生的各门课成绩・/(3){(4)sum=sum+stu[i][j];(5)j++;(6))(7)aerage[i]=sum/5.0;下面再求所有学生的平均成绩。只要在求第i个学生的平均成绩的算法外面再加上一个外层循环即可。(4.(1)i=O;(4.(2)do(求第i个学生的平均成绩;)while(i<50)把上述两步展开就成为求所有同学平均成绩的二级、三级求精算法,如下:(4.(1)1=0;(4.(2)do(j=0;sum=0;while(j<5)/*累加第i个学生的各门课成绩・/(sum=sum+stu[i][j];j++;)aerage[i]=sum/5.0;/・第i个学生的平均成绩・/)while(i<50)下面来分析求全班各科平均成绩的二级算法:第一步,求全班任意第j科的平均成绩。(1)sutn=0;(2)for(i=0;i<50;i++)sum=sum+stu[i][j];)(3)class_average[j]=sum/50;

224第二步,令j从0变化到4,即可求出各科平均成绩。for(j=0;j<=4;j++)(sum=0;for(i=0;i<50;i++)(sum=sum+stu[i][j];}class_average[j]=sum/50;)转换成C语言的程序如下:(1)#include(2)#defineSTUDENT_NUM50/・学生数・/(3)(4)#defineCLASSNUM5(5)main()(6)((7)/*50个学生每人的平均成绩数组average的所有元素清0*/(8)/*全班各科平均成绩数组class_average的所有元素清0*/(9)floataverage[STUDENT_NUM]={〇};(10)floatclass_average[CLASS_NUM]={0};(11)floatStu[STUDENT_NUM][CLASS_NUM]={0};(12)inti,j;(13)(14)floatsum;(15)for(i=0;i

225(38)sum=sum+stu[i][j];(39))(40)class_average[j]=sum/STUDENT_NUM;(41))(42)/・显示所有学生的成绩和平均成绩・/(43)(44)for(i=0;i

226",i,j,stu[i][j]);(49))(50)printf(”第・d个学生的平均成绩为吿f

227“,i,average[i]);(51))(52)/・显示全班各科平均成绩・/(53)for(j=0;j

228排序算法有很多种,首先介绍一种选择排序算法。选择排序算法的思想是:首先从要排序的数中选择最大的数,将它放在第一个位置,然后在剩下的数中选择最大的数放在第二个位置,如此继续,直到最后从剩下的两个数中选择最大的数放在倒数第二个位置,而剩下的ー个数放在最后的ー个位置(ー共要重复n-1遍),完成排序工作。算法思路的分析:为了找到思路,下面先看一个较简单的例子。举例来说,如果n=4,共有4个数要由大到小排好序,假设开始的顺序如下:数组元素a[O]a[l]a[2]a[3]其中的值3947第一ー趟比较要得到所有n个数中的最大数,将a⑼分别与a[l],a[2],a[3]比较(即a[0]与a[j]比较,j依次取值1、2、3),只要a[〇]

229在a[i-l],aロ],…,a[N-l]中,找出第i趟比较的最大数,并把它放在a[i-l]的位置上的算法:for(j=i;;j++)(if(a[i-l](2)#include(3)#include(4)#defineN20(5)main()(6){(7)inti,j,temp;(8)inta[N];(9)(10)srand(timd(NULL));(11)for(i=0;i

230(17)for(i=0;i

231盒子中存放ー张不同的扑克牌(例如,分别依次是大王、小王、草花A、草花2、草花3、草花4)。盒子编号123456扑克牌丿ゝ卜:小王草花A草花2草花3草花4我们现在摇动ー个签桶(签桶中有编号分别为1,2,3,4,5,6的6支签)。假设得到的一个随机数是2(即抽出了一支2号签)。这表明第一次应当发出2号盒中的“小王”。如果此时我们把2号盒中的“小王”与6号盒(当前最大号的盒)中的草花4对换一下,(将此次要发出的牌放在编号最大的盒子中)。那么,现在应当只剩下前5个盒子包含未发出的牌了。第一次发牌后,(对于下一次的随机发牌)6号盒无效了,发牌后盒子中的扑克牌如下图所示:盒子编号123456扑克牌大王草花4草花A草花2草花3小王签桶中的6号签已经没用了,将其取出扔掉。我们,现在再次摇动签桶,抽出一支(1到5号之间的)签来。现在假设抽出的是4号签。这表示4号盒中的草花2是第2张要发出的牌。我们现在将4号盒中的扑克与当前最大号(5号)盒中的扑克对调ー下。第二次发牌后,5号盒也无效了,发牌后盒子中的扑克牌如1、.图所示:盒子编号123456扑克牌大王草花4草花A草花3草花2小王依此类推。采用上述方式,只要从逐渐减少(每次减少ー根编号最大的签)的签桶中一共抽5次签,就能以完全随机的方式确定先后要发出的6张牌。现在来考虑按类似于上述方式的方法来随机发54张牌。(1)安排好54个盒子,盒子按1,2,3,…,53,54的顺序依次编好号。(2)将54张也牌依次编好号,并将1号牌放在1号盒子中,将2号牌放在2号盒子中……将53号牌放在53号盒子中,将54号牌放在54号盒子中(这样放牌仅仅是出于方便)。(3)取ー个1〜54(函数调用的式子为rand()%54+l)之间的随机数机(代替从签桶中的抽签),然后将编号为,〃的盒子中的牌与编号为54的盒子中的牌交换一下。此时,编号为54的盒中的牌就是第•张要发出的牌。(剩下要发的牌依次在编号为1,2,3,…,53的盒子中。)(i=0)(4)取ー个1〜53(rand()%53+l)之间的随机数m,然后将编号为m的盒子中的牌与编号为53的盒子中的牌交换一下。此时,编号为53的盒中的牌就是第二张要发出的牌。(剩下要发的牌依次在编号为1,2,-,51,52的盒子中。)(z=l)(5)取ー个1〜52(rand()%52+l)之间的随机数/n,然后将编号为5的盒子中的牌与编号为52的盒子中的牌交换一下,此时,编号为52的盒中的牌就是第三张要发出的牌。(剩下要发的牌依次在编号为1,2,…,50,51的盒子中。)(i=2)可以发现从第(3)步开始的处理流程非常相似,每步构成一个独立的处理流程,重复执行53次这种独立的处理流程,就可以将要发出的牌重新打乱顺序依然存放在这54个盒子中。

232以i作为循环变量,i的初值为0,终值为52。卜.面根据以上叙述经过仔细改造,得到任意的第i次要执行的处理流程:取ー个rand()%(54-i)+l之间的随机数m,然后将编号为机的盒子中的牌与编号为(54-/)的盒子中的牌交换一下,此时,编号为(54一ル盒中的牌就是第i+1张要发出的牌。(剩下要发的牌依次在编号为1,2,-,(54-(i+1))中。)借助变量i归纳出的这个处理流程可以适合任何一次发牌的情况。因此,可以很容易写出发牌问题的计算机算法:数据结构为:用具有55个数组元素的数组pkp来模拟这些盒子,数组元素的下标作为盒子的编号(数组元素pkp[O]不用),数组元素中存放的整数值表示此扑克牌的编号。点评:此题巧妙地将同一数组的前后两部分别做不同的用处。前一部分存放尚未发出的牌号,后一部分存放已经发出的牌号(但要通过对换才能达到)。前・部分越来越小,与此同时后一部分则越来越大。由于所有未发出的牌数(n)永远等于本次要所产生的随机数的个数(1,2,3n),所以本算法不会有取出的随机数是无效的情况出现,大大提高了算法的时间效率。ー级算法:(1)说明数组pkp;/*将54张牌依次编好号,并将1号牌放在1号盒子中,将2号牌放在2号盒子中将53号牌放在53号盒子中,将54号牌放在54号盒子中・/(2)for(i=l;i<=54;j++)pkp[/]=«;(3)i=0;(4)while(i<53)取ー个rand%(54-/)+l之间的随机数m,然后将下标为m的元素中的数值与下标为54—i的数组元素中的数值交换一下,此时,下标为54T的数组元素中的值就是第i+\张要发出的牌号。i=i+\-/・依次将54,53,52,-,2,1盒中的牌取出发放・/(5)按由大到小的次序,逆序打印pkp数组中的所有值(pkp[0]除外)转换为C语言程序如下:(1)#include(2)#include(3)#include(4)#defineSIZE54(5)intmain()(6){(7)intpkp[SIZE+1],itmttemp;(8)(9)for(1=0;i<=SIZE;2++)(10){pkp[1]=1;}(11)(12)srand(time(NULL));

233(2)1=0;(3)while(1=l;i--)(30)printf(''%d,",pkp[l]);(31)return0;(32)}问题1.如果初始化时(第9行和第10行),将牌号1存入pkp[54]中,将牌号2存入pkp[53]中……将牌号54存入pkp[l]中是否可以?答:可以,初始时54张牌的任何ー张在任意ー个位置都可以,只要确保每个盒子中的牌号不同即可。问题2.如果将赋值语句krand()%(SIZE-j)+l;不小心写成加=rand()%(SIZE)+l—i;是否可以?问题3.比较两种不同的发牌程序,说明数组pk和pkp在两例题中起到了什么作用?分别讨论数组的下标值(即数组元素的序号)和存放在数组元素中的值所表示的含义。问题4.定义符号常量SIZE有何好处?答:便于修改程序,如果以数值常量出现在程序中,需要修改的地方就多达6处,而用符号常量仅需修改ー处。如果某ー常量频繁地出现在程序中,用符号常量在将来修改时就会有很大的便利,此外程序的可读性会更好。习题1.设置发牌数SIZE为4,请用在第讲洗牌程序1中刚刚学到的验证算法的画内存变量取值变化图的方法,来理解和验证本题的算法是否正确?是否有逻辑错误或边界错误?习题2.模仿此题,编写一个洗麻将牌的程序。习题3.请用与例题类似的方法洗两付扑克牌。・习题4.能否把具体要发哪张牌的完整信息打印出来,而不仅仅是打印ー个扑克牌的编号序列?此题在学完第8章的结构再来做。・习题5.在完成习题I的情况下,把52张牌分发给4个人,每人13张,大小王除外(桥牌的洗发牌程序)。此题在学完第8章的结构再来做。第48讲用筛法求素数类型开拓思路题趣味性…难度****前面已经介绍了用蛮力法(或称为穷举法)求出10万以内的所有素数的方法,但此方法花费的时间较多。那么,是否存在更为优越的算法来求出所有素数呢?这种方法是存在的。素数(即质数)是大于I,并且除了I和它本身外,不能被其他任何数所整除的整数。

234用筛法求素数是基于这样的想法。首先将整数2,3,4,5,6,-,99999,100000依次存放于某数组中的a[2],a[3],a[4],-,a[99999],a[100000]中(此数组用作筛子),然后从该数组(筛)中选出当前下标值最小的数组元素值不为〇的数组元素,将它作为素数输出,再从数组中筛掉该数及其所有倍数。具体的算法如下(1)先把10万个数放到a数组中,a[I]中存放I,a[2]中存放2……a[99999]中存放99999,a[100000]中存放100000(2)设置ー个变量i,初值为2(3)将a[i]打出来(此数一定是素数)(4)将所有下标值为i的倍数的数组元素的值变为0,即aドザ=0(5)将i的值加1.如果4100000,则跳转到第7步(6)如果aロ不为0(说明此数是素数),那么跳转到第3步,否则,(此数不是素数,它ー定是前面某个i的倍数而且已经被筛掉即清0了)跳转到第5步。(7)结束。其中第1步和第4步需要进ー步求精。这种算法描述不规范,是ー种自然语言描述算法的方式,把它改造成比较规范的伪代码的表述方式如下:(1)先把10万个数放到a数组中,a[I]中存放I,a[2]中存放2……a[99999]中存放99999,a口00000]中存放10万(2)设置ー个变量厶初值为2(3)将下标为i的数组元素值a[i]打出来(此数一定是素数)(4)将所有下标值为i的倍数的数组元素的值变为〇,即aドザ=0/*即将其筛掉・/(5)将i的值加1(6)如果/=100000,那么跳转到第8步(否则顺序执行第7步)(7)如果a口不为O/・说明此数是素数,因为没被筛掉・/那么,跳转到第3步;否则,跳转到第5步/・此数已被筛掉了・/(8)结束此算法理解起来并不太难,也可以直接用if加无条件跳转语句来实现。代码如下:#includeintmain(){inta[100001],i,j;/・将。万个数放到a数组中,a[1]中存放1,a[2]中存放2a[99999]中存放99999,a[100000]中存放100000*/for(i=0;i<=100000;i++){a[i]=i;}i=2;loopl:printf(w%d",a[エ]);/・将所有下标值为i的倍数的数组元素的值变为0,即a[i*j]=0*/for(J=1;i*j<=100000;j++){a[i*j]«0;}/・将エ的值加1,如果エ>100000,则跳转到第フ步・/loop2:2++;if(i>100000)gotoloop3;/・判断此时a[i]是否为〇,如果不为〇(说明此数是素数),则跳转到第3步,否则,(此数不是素数,它•定是前面某个i的倍数而且已经被筛掉即清〇了),跳转到第5步。*/if(a[1]!=0)gotoloopl;

235elsegotoloop2;loop3:;)点评.ク此程序可以求出10万以内的所有素数,所花费的时间确实比以前少了很多。但此题的算法中用了三条(无条件跳转)goto语句,程序的流程结构看起来比较复杂混乱,这违反了结构化程序设计思想。结构化程序设计思想不提倡用goto语句,更不提倡大量使用goto语句。不过,由于人们在考虑算法时,跳转语句更贴近人们的自然思路,如果实在没有别的办法想出不用跳转语句构成的循环结构,则上述的算法和程序也不失为一种临时应急的解决办法。这也是本书为何要讨论循环语句与跳转语句关系的根本原因所在。那么,是否能把此题改为不用goto语句的循环呢?先将上述算法复制于此,便于讨论。(1)先把10万个数放到a数组中,a口]中存放1,a⑵中存放2……a[99999]中存放99999,aロ0000〇]中存放100000(2)设置ー个变量厶初值为2(3)将a[i]打出来(此数一定是素数)(4)将所有下标值为i的倍数的数组元素的值变为〇,即aロ・刃=0即将其筛掉(5)将i的值加1(6)如果4100000,则跳转到第8步(否则将顺序执行第7步)(7)如果a[i]不为0/・说明此数是素数,因为没被筛掉・/那么,跳转到第3步;否则,跳转到第5步/・此数已被筛掉了・/(8)结束仔细分析上述算法,可以将其改为如下算法:将原算法中的第7步的if语句,提前到第2步的后面;这样,条件为“真”时执行原来的第3步和第4步,将其组成复合语句(即现在的(3.1)和(3.2)步);条件为“假”本来要跳转到原来的第5步去执行,现在可以直接将这ー步变为if语句else后面的分句。原来的第6步现在被改为第4步了。注意if语句本身一般不会改变变量的取值(只要布尔表达式本身是没有副作用的)(1)先把10万个数放到a数组中,a臼中存放],a[2]中存放2……a[99999]中存放99999,a[100000]中存放100000,(2)设置一个变量i,初值为2(3)if(a同不为〇)/・说明此数是素数,因为没被筛掉・/(3.(1)・打出来/・此数一定是素数・//・将下标值为其任何倍数的数组元素,从数组中筛掉,即将其值变为〇・/(3.(2)有下标值为i的倍数的数组元素的值变为〇,即a[i*/]=0else将i的值加1;/*a国==0,说明此数已经被筛掉了・/(4)if(z>100000)

236跳转到第5步;else跳转到第3步;(5)结束第二步改进:此算法还可以继续改进,原来是>100000则跳到结束处,否则继续执行“如果a国不为〇”的判断。现在将条件4100000改为i<=100000,所以条件为“真”的跳转就被改为跳转到第3步;而条件为“假”则不必跳转(采用无else结构的if语句)。第二步改进后的算法如下:(1)先把10万个数放到a数组中,aロ]中存放1,a[2]中存放2……a[99999]中存放99999,a[100000]中存放100000(2)设置ー个变量厶初值为2(3)if(a[i]不为〇)/*说明此数是素数,因为没被筛掉・/{将a[i]打出来/・此数一定是素数・//・将下标值为其任何倍数的数组元素,从数组中筛掉・/将所有下标值为i的倍数的数组元素的值变为0,即a[i*j]=Oelse将i的值加1/*a[i]=O,说明此数已经被筛掉了・/(4)如果!<=100000,那么跳转到第3步/・条件不成立自然会执行下ー步,不必用跳转・/(5)结束根据前面4.2节的讨论,可以知道第二步改进后的算法中的3、4两步就是ー个do-while循环结构。所以,最终得到的算法如下:(1)先把10万个数放到a数组中,a口]中存放1,a[2]中存放2,……a[99999]中存放99999,aロ00000]中存放100000,(2)设置一个变量i,初值为2do{(3)如果(a[i]不为〇)/・说明此数是素数,因为没被筛掉・/(①将a【i]打出来/・此数一定是素数・//・将下标值为其任何倍数的数组元素,从数组中筛掉・/②将所有下标值为i的倍数的数组元素的值变为0,即a[i*j]=0)否则将i的值加1/*aは]==0,说明此数已经被筛掉了・/}(4)while(z<=100000)/・条件成立自然会跳到第3步・/(5)结束:点评./如果读者不了解循环与跳转之间的关系,就无法把最初用跳转实现的算法改为用循环实现的算法。由此可见,理解循环语句与跳转语句之间的相互转换关系还是很有必要的。

237习题1.用在洗牌程序1中学到的验证算法的画内存变量取值变化图的方法,来理解和验证本题的算法是否正确?还可将上述算法进ー步改进成以下while循环的算法。(1)先把10万个数放到a数组中,aロ]中存放1,a[2]中存放2……a[99999]中存放99999,a[100000]中存放100000(2)设置ー个变量i,初值为1while(1<=100000)(if(a[i]!=0)((2.(1)标为i的数组元素值a[i]打出来(此数一定是素数)(2.(2)有下标值为i的倍数的下标值的数组元素的值变为〇,即a[i*/]=0)}(3)结束点评,此题的算法,如果在写伪代码的时候,就排除使用跳转指令,坚持用标准的结构化的流程控制结构,可能会加大写出正确算法的难度。笔者在写此题算法的过程中,原本也打算直接用标准的循环结构来编写,可是发现很难写出来。于是就用无条件跳转来描述算法。一旦写出算法后,将此算法改造成不包含无条件跳转指令的算法就容易多了。标准的循环控制结构虽然可以使程序的流程更清晰,但它似乎与人类天性中的思维结构相距较远。无条件跳转指令的滥用虽然会造成程序流程混乱不堪,但在无法用标准的流程控制结构写出算法时,适当使用无条件跳转指令(或语句)来构造解决问题的算法还是很有必要的。只不过在写出正确的算法后,最好还是将其再转换成标准的结构化的流程控制结构,就像本书中的此题ー样。(在有必要时)充分利用无条件跳转语句带来的自由度,可在寻求(解决某困难问题的)算法的思路时不必带上(使用)标准控制流的这把枷锁。当然,在得到正确的算法后,将得到的包含无条件跳转指令的算法改造成仅包含标准的流程控制结构的算法,使算法的流程更为清晰,这也是非常有必要的。在多重循环和多重选择结构中,结构化的循环结构和选择结构要比用无条件跳转语句构成的清晰得多。不必去争论goto语句是否有用,一切从实际的需要出发。其中第(1)步和(2.2)步需要进一步求精。转换成C语言的程序如下:(1)#include(2)intmain()(3)((4)inta[100001],i,j;(5)/・将!0万个数放到a数组中,a[l]中存放1,a[2]中存放2,a[99999]中(6)存放99999,a[100000]中存放10000〇・/(7)for(i=0;i<=100000;i++)(8){a[i]=1;}(9)(10)1=1;(11)while(i<=100000)(12){

238(3))1++;(4)if(a[i]!=0)(5){(6)printf(H%d,a[1]);(7)/・将所有下标值为i的倍数的数组元素的值变为〇,即a[i・ゴ]=0ソ(8)for(j=l;i*j<=100000;j++)(9){a[i*j]=0;}(10)(11)}/*endif*/(12)(13)}/*endwhile*/(14))习题2.请将此题用for循环来实现。第49讲求100以内所有数的质因子类型选修题开拓思路题趣味性***难度****质因子举例如ド:12=2X2X321=3X7先求出100以内的所有素数,并将其放在ss[1],ss[2],ss[3]等中。ー级算法:1.算法分析:首先求任意给定的ー个数〃W100的质因子,方法为先求出100以内的所有质因子,把它们依次存放在数组ss口中,即ss[l]=2,ss[2]=3,ss[3]=5,…,ss口。如果问题很难,分析问题可以考虑从特殊到一般,即在一个特殊情况下比如〃=45,看看如何求出45这个数的所有质因子。步骤如下:(1)先打印"45=”。(2)令〃=45,用第一个质数2来试除45,余数不等于0,说明2不是45的质因子。(3)用下ー个质因子3来试除45,余数为〇,说明3是45的质因子。(4)将3打印出来,再打印ー个乘号"45=3*”。(5)用45/3=15来作为新的〃,因为〃不等于1。(6)用质因子3来试除15,余数为0,说明3是15的质因子(3是45的第二个质因子)。(7)将3打印出来,再打印ー个乘号"45=3*3*”。(8)用15/3=5来作为新的〃,因为〃不等于1,过程没有结束。(9)用质因子3来试除5,余数不为0,说明3不是5的质因子(因此45的质因子中只有两个3)。(10)用下ー个质因子5来试除5,余数为0,说明5是45的ー个质因子。(11)将5打印出来,再打印ー个乘号"45=3*3*5*”。(12)因为5/5的商为1,至此求质因子的过程结束。通过对上述特例的讨论和仔细分析,可以得到ー一般情况下此题算法的特点如下:(1)〃能被ss口除尽,是用同一个质因子ss[i](即i不变)继续求〃的质因子的条件。〃不能被ssm除尽时,オ改用下ー个质因子作为除数(即ss[i]中的下标,要加1)。(2)〃不等于1是循环执行的条件。

239(3)如果ss[i]是〃的质因子,不仅要打出此数,还要用〃/ss[i]作为新的〃值,并继续用同ー个ss团来求此n的质因子。i不能加1。考虑此算法要用两个循环。内层用ー个循环,专门用来求在,不变时,〃有几个形如ss[i]的质因子。算法的伪代码如下:/・求n有儿个形如ss[1]的质因子的算法・/while(n除以ss[i]的余数等于〇并且n不等于1时)(打印此ss"]*/*因为ss[i]是n的质因子・/n=n/ss[i]:/*得到新的n*/对于ー个数n外部还有一个循环,用来求〃的所有质因子。(1)i=l;(2)当(〃不等于1时)((2.(1)有几个形如ss[i]的质因子:2++;)由此可见,前面的求〃有几个形如ss[i]的质因子的算法,在此处成为(2.1)步的二级求精算法。将一级算法和二级求精综合到ー起成为如下的算法:(1)求出100以内的所有素数,将其放在ss[l],ss[2],ss[3]等中(2)n=l;(3)while(n<=100)((4)打印〃=(5)z=l;(6)k=n;(7)while(〃不等于1时){(7.(1)有几个形如ss[工]的质因子while(n除以ss[目的余数等于〇并且n不等于1时){....打印此ss[i]*/*因为ss[刀是n的质因子・/n=n/ss[1];/*得到新的n*/)(7.(2)1++;)(8)换行;(9)n++;(10)}转变为C语言程序:

240#includeintmain(){1inti,j,n,k,flag,ss[30]={0};2/・求出100以内的所有素数,将其放在ss[l],ss[2],ss[3]等中;*/3n=2;4j=l;5while(n<=100)6{/・以下求n是否为素数・/7flag=l;8i=2;9while((i<=n-l)&&(flag!=0))10{11if(n%i==O)12flag=O;13i++;14}/*endwhilei*/1516if(l==flag)17(18ss[j]=n;19j++;20)20n++;/・更新循环变量・/21}/*endwhilen*/22/・以下求1到100的质因子・/22n=l;23while(n<=100)24(25printf(''%d=w,n);11i=l;12k=n;25while(%!=1)26{27/*(4.1)求k有几个形如ss[i]的质因子・/28while((k%ss[1])==0&&(k!=l))29{30printf("%d*w,ss[i]);/・因为ss[i]是n的质因子・/31k=k/ss[i];/*得到新的n*/32)33/*(4.2)i++:*/34i++;35)36printf(n

241");37n++;38)39)习题.用在洗牌程序中学到的验证算法的画内存变量取值变化图的方法,来理解和验证本题的算法是否正确。第50讲农夫过河类型开拓思路型算法类型随机穷举法

242趣味性****难度****ー个农夫在河边带了一只狼、ー只羊和一颗白菜,他需要把这三样东西用船带到河的对岸。然而,这艘船只能容下农夫本人和另外一样东西。如果农夫不在场的话,狼会吃掉羊,羊也会吃掉白菜,如图5.10所示。请编程为农夫解决这个过河问题。图5.10农夫、狼、羊和白菜讨论:1.需要的数据结构分别用数组元素A[0]、A[l],A[2]的取值1和0来表示狼、羊、白菜是否在A岸,如果在,则为1;如果不在则为〇。例如A臼等于1表示羊在A岸,A[0]等于〇表示狼不在A岸。分别用数组元素B[0]、B[l],B[2]的取值1和〇来表示狼、羊、白菜是否在B岸,如果在,则为1;如果不在则为0。农夫从A岸到B岸一定要带ー样东西过河,农夫从B岸回到A岸,既可以带ー样东西,也可不带东西。记录来回过河行动所带的东西,可以用两个整型数组。intriver_ab[100]用来记录农夫从A岸带到B岸的物品;intriver_ba[100]用来记录农夫从B岸带回到A岸的物品。比如,如果下标变量river_ab[l]的值等于!,表示农夫第一次从A岸带到B岸的物品是羊;rivejbaロ]的值等于ー1,表示农夫第一次从B岸带回到A岸的物品为空(用ー1表示没带东西)。2.算法分析可以采用随机穷举法来解决这个问题(用回溯法也可以),即如果从A岸到B岸时,随机生成〇〜2的ー个数take,表示农夫想选择ーー个什么东西带过河。例如如果take等于0,表示农夫想把狼带过河,此时首先要判断A[take]是否为0,如果为0(表示狼已经过河了)就要重新取随机数。在随机数可取的情况下,还要判断农夫此行是否可以进行,即剩下在A岸的东西是否会被吃掉。如果会发生被吃事件,则此次取的随机数仍然是无用的,需要重新取。如果不会发生被吃事件,则农夫可将此东西带过河。ー级算法:(1)初始化A、B数组,A[0]=1,A[1]=1,A[2]=1;B[O]=O,B[1]=0,B[2]=0;river_ab口和river一ba口数组的所有元素都赋值为ー1;(2)<=1;(3)while(1){(3.1)处理第i次由A岸到B岸的过河问题;(3.2)如果过河完毕,那么break;(3.3)处理第i次由B岸回到A岸的过河问题;(3.4)/++;

243(4)输出过河的全过程;算法进ー步细化后的二级求精(1)初始化A、B数组,A[0]=1,A[1]=1,A[2]=1;B[O]=O,B[1]=0,B[2]=0;river_ab口和river_ba口数组的所有元素都赋值为-1;(2)i=l;(3)while(i<=100)(3.1)处理第i次由A岸到B岸的过河问题;/・以下处理由A岸到B岸的过河问题・/(3.1.1)随机取ー个〇〜2之间的随机数take/・表示想带何物品过河・/(3.1.2)如果此数不可取,那么跳转到3.1.1重新取;(3.1.3)A[take]=O;B[take]=l;/・带过河・/(3.1.4)river_ab[z]=take;/・记载第i次从A到B的过河物品・/(3.2)如果所有东西都过河了,那么break;(3.3)处理第i次由B岸回到A岸的过河问题;/・以下处理由B岸到A岸的过河问题・/(3.3.1)随机取ー个〇〜1之间的随机数yes(3.3.2)如果(yes等于0)/・说明农夫不必带东西回到A岸・/那么{river_ba[i]=-l;/・记载第i次从B到A的过河物品・//++;continue;)否则/*yes等于1,说明需要带东西过河・/((3.3.2.(1)取一个0-2之间的一个随机数take;(3.3.2.(2)此数不可取,那么跳转到(3.3.2.1)重新取;(3.3.2.(3)B[take]=O;A[take]=l;/・带过河・/(3.3.2.(4)riverba[/]=take;/・记载第i次从B到A的过河物品・/}3.4汁+;}(4)如果300)那么打印过河过程;否则打印此题可能无解:(5)结束;此算法的第(3.1.2)步(包括类似的(3.3.2.2)步)和第4步需要进ー步求精。第(3.1.2)步的三级求精(比较复杂,读者要详细领会):如果(此数不可取)那么goto(3.1.2)ー个数take不可取,可以表示如下:2==(A[0]+A[l])或者2=(A[1]+A[2])(狼和羊共处A岸或者羊和白菜共处A岸)但是,单纯用此式来进行判断是不对的,应將打算带过河的东西除去后(即A[take]=O),再做上述判断。若不成功,还要将A[take]的值恢复原样(即A[take]=l)。(3.1.2.(1)A[take]=O;/*将打算带过河的东西除去・/(3.1.2.(2)(2==A[0]+A[l])或者(2==A[1]+A[2])/・判断是否可带・/那么/・不可带・/

244A[take]=l;/*将A[take]的值恢复原样・/goto(3.1.2)/・跳转到(3.1.2)重新取;*/}改成上述的算法是否完全正确呢?不是,因为还没有判断当前是否还有下标值为take的东西,可以让农夫带过河去。所以,第(3.1.2)步完全正确的三级求精算法如下:(3.1.2.(1)如果(0=A[take])/・是否有此东西可带・/那么goto(3.1.2);/・没有此东西可带,跳到(3.1.2)重新取随机数・//・到下ー步时,一定有这个东西可带・/(3.1.2.(2)A[take]=0;如果2==(A[0]+A[l])或者2=(A[1]+A[2])/・判断是否可带过河・/那么/・此物不可带・/(A[take]=l;/・恢复原状・/goto(3.1.2)/・跳至リ(3.1.2)重新取随机数・/否则goto(3.2)/・表示此物可带,其实这个else分句完全可以不要・/打印过河过程第4步的二级求精这里不作介绍,(3.322)的三级求精也不作介绍,留给读者练习。习题1.请将(3.3.2.2)步算法的二级求精算法写出来。注意,要避免将刚刚从A岸带到B岸的东西马上又从B岸带回到A岸去。参考答案:/・如果(0==B[take]或者take是刚带过河的)*/(1)如果(0==B[take]或者river_ab[i]等于take)那么goto(3.3.2.2);/・此东西不可带,重新取随机数・//・到达下ー步时,一定有这个东西可带・/(3.1.2.(3)B[take]=0;如果2=(B[0]+B[l])或者2==(B[1]+B[2])/・判断是否可带过河・/那么/・此物不可带・/(B[take]=l;/・恢复原状・/goto3.3.2.2/・重新取随机数・/)习题2.将第4步的二级求精算法写出来。习题3.用洗牌程序1中学到的验证算法的画内存变量取值变化图的方法,来理解和验证本题的算法是否正确?习题4.将所有的算法结合起来,写出此题完整的C语言的程序。*・习题5.商人过河问题:有三个商人带着三个随从和货物过河,船每次最多只能载两个人,山他们自己划行,并且如何乘船渡河的大权山商人们掌握。要求保证在过河期间的任一岸上,商人的人数要大于或等于随从的人数,否则随从会杀死商人抢走货物。编程找到至少ー个方案,帮助商人过河。可模仿农夫过河问题解此题。此题还可以有其他的解法(参见《C语言实例解析精粹》)。

245第五章函数在人类社会的发展过程中,分エ起到了决定性的作用。可以想像一下,即使一个非洲原始部落的人,若要像鲁宾逊那样,在一个荒无人烟的地方完全自力更生地活下来,几乎也要耗尽他的绝大部分精力和体力。分エ是人类建立现代文明社会,过上安定和美好生活的重要基础。类似的,如果ー个大型软件项目没有分エ,几十万、几百万行甚至上千万行的语句,靠ー个程序员是根本完成不了的。那么,在程序设计领域是否有实现分エ的语法结构呢?这种语法结构是存在的。在C语言中,实现分エ的语法结构就是函数。5.1引言在讨论函数之前,先来讨论一下顾客向家具生产厂家定做家具——床的过程。定做家具床的全过程如下:1.家具厂商的工作。首先,家具厂要建造一条家具床的封闭型的自动生产流水线,这条流水线可生产规格参数有所不同的各种不同木制家具床。生产线建成后,家具厂商要提供一条定做床的订货参数规格要求如下:实木类家具床(宽度,木材种类,颜色)其中“家具床”是要求定制的服务名,目的是告诉潜在的所有顾客:要以书写此名字来告诉家具厂商,要求厂家生产的是ー张床而不是别的家具。服务名后的圆括号内是参数规格说明,这里给出了三个形式参数。其中宽度要求顾客提供ー个实数值来填写订货单中床的宽度;木材种类要求顾客提供ー个枚举值(比如,顾客只能从花梨木、红木、杉木这三个列举出来的值中选取ー个来填写订货单);颜色也要求顾客提供ー个枚举值(朱红、灰、本色)来填写订货单。“家具床”左边的“实木类”用于告诉顾客:此服务得到的结果是一件实木类的家具床,而不是金属类的家具床。圆括号中的参数是抽象的,并没有具体的值。只是向顾客提供了一个要求此服务时,填写订货单的数据规格要求,即:要填写几个数据?数据是何种类型的?按什么顺序填写?所以,把这些参数称为形式参数(简称形参)。用圆括号括住的部分,称为形式参数表,或简称为ヰ参表。1.顾客的工作顾客可以是在进行ー套房屋装修的大,也可以是需要订制购买床的任何大。他的主要エ作是负责房屋装修的调度管理、购买相应的物品和服务。他在装修的・定阶段发现要定做床了,而他看到了家具厂在广告中附上的订货单规格要求。根据自己的需要,他需要的床的宽度是1.4m,杉木,朱红色。于是,他按照家具厂商提出的订货单规格要求填写了如下的一张订货单。家具床(1.4m)杉木,朱红色)然后将此订货单发送给厂家后进行等待。3.自动化流水线的生产工作厂家收到订単后,根据订货单中名称知道要启动哪一条自动化生产流水线。根据顾客提供的券,季算,立即启动自动流水线进行生产。把家具床生产出来后,生产线立即停工,并马上将床运送给顾客。4.顾客一旦收到自动化流水线运来的定做的家具床后,就可以进行下ー步的装修工作了。可以看出,厂家给出的订货规格填写说明清单和顾客严格按照清单规格去填写订货单,是完

246成这件委托加工定做工作的关键“接口”。注意有如下的ー些特点。顾客根本不需要知道床生产的任何知识和实际生产过程,他只要按照厂家提供的订货参数规格要求去填写订货单,然后寄给厂家,就可以等待厂家把生产好的床送来.而家具厂的生产过程,是完全按照顾客提供的订单中指定的规格要求进行的.厂家建立了自动生产流水线,顾客通过给它发送订单,要求自动生产流水线为他生产所需要的家具.顾客通过填写定做单申请服务;而流水线通过接收定做单为顾客提供服务.5.2函数概念的概述在较大型的程序开发工作中,也实现了类似于人类社会定做床的这种分工模式。在c语言中这种分工模式的语法机制就是囲裂。什么是函数(function):函数是被封装起来并具有一个名称的一段程序,是程序中具有逻辑独立性的动作实体(类似于生活例子中的家具床自动化生产流水线)。程序员A编写了一个函数,提供特定的(由函数的代码所给定的)运算(或输入输出)服务,相当于家具生产厂家制造了一条生产家具床的自动化生产线。调用指定函数的程序员B,相当于定做床的顾客,通过调用函数来使用函数所提供的运算服务。1.编写函数的程序员A的工作程序员A首先要编写提供特定服务的代码段,这一代码段被称为国契俘(相当于家具厂商建立一条封闭的自动生产流水线)。函数体编写好后,程序员A要向任何一位潜在客户(别的程序员,但也可以包括他本人)编写一条耍求进行此指定服务的参数规格说明,称为函数首部,其格式为:返回值类型函数名(形式参数表)例如密函数pow,其函数首部为:doublepow(doublea,doubleb)其中形式参数a为底,形式参数b为指数;最左边的double,是告诉程序员B,调用该函数得到的ー个返回值是何类型的,pow是函数名。注意:每个形式参数的类型名都不可省略,多个形式参数之间要用逗号隔开。形式参数乂简称为形参。圆括号中的形式参数是抽象的,并没有具体的值。只是规定了调用此函数时,(实际)参数填写的顺序、类型和个数。此函数首部相当于家具厂家给出的订货单参数规格要求:实木类家具床(宽度,木材种类,颜色)函数首部要书写在函数体的上部,函数体要用花括号括住。函数首部和函数体ー起构成了所谓的电数定义

247小知识函数定义的格式:返回值类型函数名(形式参数表){函数体与main函数类似,普通函数(无论是库函数还是由其他程序员编写的函数)的函数体主要也是由说明系列和语句系列构成的。1.程序员B调用函数的方式程序员B为了得到函数所给定的运算服务,只需要写出函数名,在函数名的右边写出ー对圆括号,在圆括号中,依照形式参数表给定的顺序要求填写各个实际参里的值。例如要求出x的3.6次方,程序员B需要这样书写:pow(x,3.6)〇这被称为函数的调用。一般的函数调用格式如下:函数名(实际参数表);实际参数可以是常量、变量和表达式。多个实际参数之间也要用逗号隔开。实际参数的个数要与形式参数相同,类型也要匹配,顺序也不能颠倒。实际参数又简称为实参。在程序中需要调用该函数求一个变量X的3.6次方,并把得到的返回结果,赋给变量z的时候,只需这样写:z=pow(x,3.6);在程序中需要调用该函数求一个变量X的3.6次方,并把它打印出来的时候,只需这样书写pow〇的函数调用:printf(“变量x的3.6次方=%f

248",pow(x,3.6));(这相当于顾客根据自己的要求,他需要的床的材料是杉木,宽度是1.4m,颜色是朱红色。于是,他填写了一张订货单:家具床(1.4m,杉木,朱红色),然后将此订货单发送给自动化生产线后进行等待。)2.程序员A所编写的函数体(相当于家具床的自动化生产流水线)的工作程序员A所编写的函数体一旦接收到程序员B编写的申藜颯用传递给它的实际参数,根据程序员B提供的实际参数,计算机立即(跳转去)“运行”由程序员A所编写函数体的语句(这些语句事先已经被翻译成了机器指令,实际上运行的是指令)。把结果计算出来后,通过return语句(参见下面说明)把结果值返回到程序员B调用该函数的地方(这就像要把生产出的床用浄车运送到发出订货单的地方ー样)。也就是说,如果调用前(程序员B所书写)的语句如下:

249z=pow(x,3.6);那么,在调用函数后的(return语句的)返回值是12.78的情况下,该值就被返回到程序员B编写的语句z=pow(x,3.6);之处:也就是说:这条语句在经过函数调用并目.返回值以后,已经变成了z=12.78;这样的一条赋值语句了。即调用函数后的返回值,出现在程序员B写出的调用该函数的程序语句之处。函数调用前的语句形式函数调用后的语句实际上的形式z=pow(x,3.6)z=12.78在调用过程中,发生了两次很重要的数值的传递过程:•次是发生在函数调用开始时,所有实际参数的值,被ーー对应地传递给了相对应的函数首部中的形式参数(这是被调用的函数体的语句可以被计算机执行的先决条件);另一次是被调用的函数体在运行时遇到了return语句,由return语句将返回值传递到函数调用之处(这就类似于家具厂商用汽车将生产好的家具床运到顾客发出订货单之处)。关于return语句,请参见下一节的讨论。注意有如下ー些特点。程序员B根本不需要知道编写赛函数程序段的任何知识和看到程序实际运行的代码,他只要按照程序员A提供的形式参数表的要求,去写函数调用语句,然后系统会自动将此调用要求(的参数)传送给程序员A写出的解函数程序段,此时程序员B编写的程序段,就可以等待幕函数把按照他的要求算出来的结果送回来。而函数体语句的执行过程是完全按照程序员B提供的实际参数值的大小运行的。程序员A建立了函数pow(),任何程序员B都可以通过写函数调用语句(相当于例子中给厂家发订货单ヒ要求函数为他计算所需要的结果。以上就是对函数问题的概述。问题:函数体中的return语句起了什么作用?对于函数的调用、返回、形参值与实参值之间的数值传递的深层细节感兴趣的读者,可以参看本书第9章的提高部分。下面以ー个表格对比的形式,对以上所叙述的内容进行小结:定做家具的全过程程序调用得到服务的全过程1、发布家具订货参数规格清单(事先做)1、书写函数首部(事先做)2、建造生产流水线(事先做)2、书写函数体(事先做)3、客户填写实际参数并发送订货单3、在程序中填写实际参数进行函数调用4、家具厂收到顾客订货单,将订货单中实际参数传给流水线4、被调用的函数首部接收到实参值,将所有雪率值舉给対座的や筝变ぎ

2505、流水线进行生产家具床5、被调用的函数体进行数据处理6、用汽车将家具床送给发订单的客户6、由return语句将返回值带回到函数调用之处注意:在表中完成1、2、4、5、6步的,是ー个提供服务的角色,而完成第3步的是要求提供服务的另一个角色。在最后ー步整个过程圆满结束。5.3函数的编写现在的问题是,我们应该如何编写ー个独立的、可由别的程序员(当然也可以包括我们自己)调用的函数呢?编写独立函数有何原则呢?我们首先来编写ー个(独立)函数,然后找出一些编写(独立)函数的原则。例题6.1编写ー个函数,该函数得到ー个三位整数,然后返回一个反向后的三位整数。程序如下:intreverse(intn){intn3,n2,nl;/・中间变量・/intnum;/*结果变量*//・分别求出它的百位数れ3、十位数n2、个位数れl*/n3=n/100;n2=(n—n3*100)/IO;nl=n%10;/・反向后的三位整数为:*/num=nl*100+n2*10+n3;returnnum;/*返回此三位整数给调用者*/这个函数是如何编写出来的呢?为了讨论方便,把第2讲的例题复制于此,并在每一行后面用/**作为开始,然后再加上说明。#include/*・止匕行不要・/0intmain()/**此行改为intrevers(intn),称为函数首部・/intれ;/・输入变量/**删除,因为n现在要作为形参表中的形式参数・/2intn3,n2,nl;/・中间变量・//*・此行不变・/3intnum;/・结果变量・//*・此行不变・/4printf(“请输入ー个三位整数、n");/**此行删除・/5scanf&n);/**删除,该值现在由调用者通过实参传进来・//・分别求出它的百位数n3、十位数n2、个位数nl*/6n3=n/100;/**此行不变・/7n2=(n-n3*100)/IO;/**此行不变・/8nl=n%10;/**此行不变・//・反向后的三位整数・/9num=nl*100+n2*10+n3;/**此行不变・/

251/・输出此三位数・/6printf(''num=%d,',num);/**此句耍改成returnnum;这样才能将该值带回给此函数的词用者编写的调用语句处。*/说明如下:(1)给该函数起个符合标识符要求的名字reverse,并根据返回值的类型写出函数首部的返回类型。因为返回值仍然是三位整数,所以在reverse函数名的左边加上int,表明该函数调用后的返回值是ー个整型数据,形式如下:intreverse()(2)圆括号中是形式参数表,现在要明确形式参数表中要有几个参数,它们分别是什么类型?这很简单,只要看第2讲中编写的main〇函数中,输入了几个变量的值。显然,只有一个变量的值需要输入,那就是要被反向的三位整数〃。这就决定了形参表中只有一个参数,需要调用者提供实际数据。这样函数首部就完全确定好了。intreverse(intn)(3)由于“已经作为形参,它的值由调用者传入,不需要在此函数运行时由程序员A编写的代码输入,因此,第4句和第5句都要删去。第1句int〃;也要删去,因为形式参数n的类型说明要出现在函数苜部的形式参数表中,所以不能重复出现在函数体的说明系列中。(4)第10句,原来的main。函数中仅仅将反向后的三位整数打印出来即可。但现在不行,因为函数的编写者(即程序员A)并不知道调用此反向函数的程序员B要用这个反向后的三位整数做什么用处。他(程序员B)很可能不只是打印出来就完事;程序员A不能代替他做决定,而只能将此结果(此处是反向后的三位整数)传递给他。在C语言中,函数将结果传递到调用者的程序段中,要使用return语句。本题中用的是:returnnum;〇由以上论述和分析可以归纳出,将主函数形式的程序转换成独立函数的一些原则:将主函数形式的程序转换成独立函数的一些原则:(1)将main。函数的所有必须的输入变量,全部都作为独立函数的形式参数。(2)将main。函数中能够山输入变量得到值的中间变量和临时变量,作为独立函数的局部变量。(3)将main。函数中printf。函数的输出项(目前只能是一个结果变量或一个表达式)的值,作为独立函数return语句的返回值。(4)用刚刚得到的返回值的类型来决定函数首部的函数类型。有了上述原则后,如果我们觉得一个独立的函数难以编写,我们不妨先用main。函数的形式先编写出来,再根据上述原则去进行转换。习题.依照以上变换原则,将前几章(数组一章除外)的例题中你感兴趣的习题或例题改写成独立函数。问题1.如果将函数首部reverse左边的返回类型漏掉,是否可以?答:对于此题来说是可以的。因为只要返回值的类型是整型int,就可省略掉这个int不写。其他的返回类型不可省略。如果函数没有返回值,则返回类型要用void。问题2.此题是否可以多设几个形式参数?比如将百位数〃3、十位数〃2和个位数”1都设为形式参数?答:只要在局部变量定义中去掉〃3、“2、〃1,并将相应的代码段删掉,程序变成:intreverse(intn,intn3,intn2,intnl){

252intnum;/・结果变量・/num=nl*100+n2*10+n3;/・返回此三位数给调用者・/returnnum;)这个函数在语法上是没有任何错误的,但实际上非常不可取。这样会把本来应由编写独立函数的程序员A所做的工作强制转移给调用函数的程序员B来做。因为调用函数的程序员B在书写函数调用的表达式时,必须给出任何形式参数的实际数值(即实参值)。因此,调用函数的程序员B为了调用此函数,就不得不自己编写求这个三位整数的百位〃3、十位〃2和个位〃1的程序代码,然后才能调用此函数。此函数在编程分エ意义上,已经失去了存在的价值,任何一位程序员都不会去调用这个函数。编程原则:编写函数的原则之ー形式参数最少原则。能用函数内部局部变量来定义的,决不定义成形式参数。只有形参个数最少,编写的函数オ不会把本应该由函数体做的运算工作强制转移给调用者去做;应该由函数完成的工作,绝对不应当要求函数的调用者去做。问题3.是否可以将返回语句returnnum;改写成如下的形式?(1)return〃1*100+〃2*10+〃3;(2)return(〃l*100+〃2*10+〃3);(3)return(〃l*100+〃2*10+〃3;)答:return的语法格式为:return表达式;任何变量只是表达式的特例,所以(1)和(2)是可以的。(3)是错误的,因为它把分号放在了圆括号中,return语句要求以分号结束。问题4.此题是否可以改成不要return语句的形式?答:不可以,因为调用者将得不到此结果。只有在函数不必返回值的情况下オ可以不要return语句。不过,如果你非要这么做的话,就必须用输岀库函数直接将反向后的三位整数在显示屏上输出,并将函数的返回类型改为void,修改后的程序为:voidreverse(intn){intn3,n2,nl;//临时变量・/intnum;/*结果变量*//・分别求出它的百位数れ3、十位数れ2、个位数れ1*/n3=n/100;n2=(n—n3*100)/IQ;nl=n%10;/・反向后的三位整数为*/num=nl*100+n2*10+n3;printf(''%dw,num);

253此程序没有语法错误,但是这样修改在此题中也很是不可取的。因为作为编写函数的程序员A,绝对不能越俎代庖。不能想当然地以为,其他的程序员调用编写的这个函数,一定仅仅只是把这个反向的三位数在屏幕上显示出来。因为调用者很可能要用这个反向的三位数做进ー步的运算。而且,即使在显示屏上输出,难道你知道别人要将此数放在屏幕的什么位置吗?编程原则:编写函数的原则之ニ不要越俎代庖。对别人调用此函数要做什么,在编写函数时,不要做任何其他的假设,这样编写的函数才能应用广泛。不要做本应由调用者做的工作.・问题5.在有返回值的函数体中,是否可以有多个return语句?答:完全可以。但是要记住,在进行函数调用后(将实际参数值传递给相对应的形式参数后),运行函数体的程序段时,只要在流程中一遇到return语句,就立即会结束函数体的运行,程序的运行(带着函数的返回值)将立刻返回到调用函数处(函数体中该retuen语句后面的所有语句不会得到执行)。返回值为整型时还要区分两种情况。(1)逻辑型返回值:返回值1表示“真”,返回值〇则表示“假”,因此,逻辑型返回值只有这两种可能的取值。(2)非逻辑型返回值。点评.タ封装的含义函数概念中的“封装”这个词不要简单地理解成“看不到别人编写的函数体代码”这样的概念。封装是指编写函数的程序员要努力去实现ー些原则,这些原则的一部分前面己经介绍过。一旦按照这些原则去做,就可以说,这个函数已经封装好了。封装本身指的是ー种良好的编程行为——即函数要做自己应该做的事、不做自己不该做的事。是否可以看到函数体的代码是不重要的。正是由于封装好了,这个函数オ是具有逻辑独立性的动作实体。现在,我们来编写ー个主调函数(在这里我们用的主调函数是main。形式的,但实际上可以在任何函数的函数体中编写调用别的函数的语句),来调用这个刚刚编出来的求一个反向的三位整数的函数。你可以看到,这种程序员自己定义的函数调用的方式,与以前调用ー般的库函数并没有什么区别:1intreverse(intn)2{3intn3,n2,nl;/*中间变量・/4intnum;/・结果变量・/5/・分别求出它的百位数n3、十位数n2、个位数成//6n3=n/100;7n2=(n—n3*100)/IO;8nl=n%10;9/・反向后的三位整数为・/10num=nl*100+n2*10+n3;11/・返回此三位数・/12returnnum;13}/*endreverse()*/14#include15intmain()

2541{2intk,;3inti,ren;/*ren用来得到反向后的三位整数。*/45i=l;21while(i<=10)22{23printfC请输入第%d一个三位整数

255”,i);24scanfT%cT,&k);25ren=reverse(k);26printfT反向后的三位整数为%d

256[en);27i++;28}29)小知识:抽象和具体人类社会每出现一次分エ,就会出现ー对抽象和具体的实例。从事这ー具体工作的人,对工作流程和工作有关的知识都要懂得;而消费这种工作生产出的产品或享受这种工作所提供的服务的顾客,可以对这些知识ー无所知,他所需知道的仅仅是产品(或服务)的名称、功能、价格(有时还有体积、重量等参数)这样ー些相对抽象的概念。所以对顾客来说,产品的生产过程被隐蔽起来了,顾客不必了解具体的生产过程的知识。因此可以说,产品生产对顾客来说就是ー种抽象.这种由分工产生的抽象,可以使现代人仅仅需要掌握的基本谋生技能和知识降到很低的水平,从而使人们抽出大量时间去学ー些专门领域内的知识和技能。如果没有分エ,没有分工带来的抽象,人类现在应和几万年前的古人类差不多。在软件工程领域,情况也是类似的。如果没有函数这种编程机制,实现程序员之间的编程工作的分エ是不可想象的。例题6.2:已知三角形的两边夹角,编写ー个函数,求此三角形的面积。此题算法和main函数形式的源程序请参见第4讲,根据上述的转换原则,转变成独立函数形式的源程序如下:1doublearea(doublea,doubleb,doublealfa)2(3doublealfaa;/・转换成弧度后的角度・/4doubles;5alfaa=(3.1416/180)*alfa;6s=(1.0/2)*a*b*sin(alfaa);7returns;8}问题L第1行的末尾是否可以加分号?答:不可以。函数首部结束处不用分号。

257问题2.是否可以不定义局部变量s,即删除第4、第6行,同时把第8行改为:return(1.0/2)*a*b*sin(alfal);答:可以。问题3.调用此函数时,填写实际参数时,是否一定要把角度作为最后ー个参数?答:是的。函数调用时,实际参数要与形式参数ーー对应,次序不可颠倒。问题4.函数调用时的实际参数表(简称实参表)中的实际参数,是否可以是变量、常量或表达式?答:实际参数是变量、常量或表达式都可以,只要类型匹配。问题5.函数头部形参表中的3个double是否都要写?答:是的,一个都不能少,每ー个形参前都必须有一个类型名。习题1:编写一个函数,求ー个整数n是否素数。习题2.请编写ー个函数intdigit(intd;intk),求一个整数d从低位数过来的第k位的值。即:digit(3278,2)等于7digit(3278,5)等于〇。第51讲发放工资问题类型开拓思路题趣味性**难度**编写程序,要求该程序在输入某人的编号,确实实发エ资数额后,自动确定发多少张100元、50元、10元、5元、5角、1角的钞票,并输岀实发エ资总额和所需各种钞票的总数目(要求用函数)。要求发的总张数最少。算法分析:先不考虑用函数解此题的算法。实发エ资额用实型变量sum,“100为整型。(1)输入实发エ资总额sum(2)得到要发多少张100元的:n100=sum/100;(3)打印要发多少张100元的;(4)得到要发的剩余额sum=sum-n100*100;(5)得到要发多少张50元的:〃50=sum/50;(6)打印要发多少张50元的;(7)得到要发的剩余额sum=sum-〃50*50;(8)得到要发多少张10元的;〃10=sum/10;(9)打印要发多少张10元的;(10)得到要发的剩余额sum=sum-nl0*10;(11)得到要发多少张5元的:"5=sum/5;(12)打印要发多少张5元的:

258(13)得到要发的剩余额sum=sum-n5*5;(14)得到要发多少张5角的:n05=sum/0.5;(15)打印要发多少张5角的;(16)得到要发的剩余额sum=sum-〃05*0.5;(17)得到要发多少张ー角的;n01=sum/0.1;(18)打印要发多少张一角的;这个算法很简单,但书写起来比较麻烦。可以考虑将其中的类似段编写成函数来减少编程的工作量,前面已经将类似的语句,每三句分成一组,这里把其中的第二组复制到下面。得到要发多少张50元的;”50=sum/50;打印要发多少张50元的;得到要发的剩余额sum=sum-"50*50;考虑这ー组语句需要的已知变量是什么,最终要求出什么值,还做了什么事?如果要把这三句变成逻辑上独立的程序段(即函数),从第一句赋值语句可以看出,需要的已知变量有两个,一个是剩余要发的钱sum,另ー个是本次要发的钞票数额50,因为这两个值出现在赋值语句的表达式中。这两个变量要作为函数的形式参数。要求出的数有两个,ー个是本次要发的钞票张数,另ー个是新的剩余额。但是用C语言编写函数,如果不用后面介绍的指针,也不用前面学过的数组作为参数,则ー个C语言的函数只能带ー个返回值,而这里要求有两个变量的值。但通过分析可以发现,只有新的剩余额是真正必需返回的,本次要发的钞票的张数可以看成是局部变量,一旦求出后,在函数体中打印本次要发的此面额的钞票张数即可,可以不必把本次要发的此面额的钞票张数作为返回值。由于打印信息对于元和角有所不同,可以用一个选择结构来进行判断。通过上述分析,可以很容易地写出这个函数的伪代码算法。doublefgz(doublesum,doublevalue)/*value是钞票面值*/{'intnum;/・要发的张数・/inttemp;doublenewsum;/・尚要发放的新的剩余额・/num=sum/value;/・变为整数・/如果(value>l)那么printf('、要发%^1元的张数为%d”,value,num);否则printf('、要发セf角的张数为%d”,value*10,num);/・得到尚要发放的剩余额*/newsum=sum-value*num;returnnewsum;)下面将其转换为C语言的函数。doublefgz(doublesum,doublevalue)(intnum;/・要发的张数・/inttemp;doublenewsum;/・尚要发放的新的剩余额・/

259num=sum/value;if(value>l)printf("要发彩d元的张数为をd”,(int)value,num);elseprintf("要发吿f角的张数为%d”,value*10,num);/・得到尚要发放的剩余额・/newsum=sum-value*num;returnnewsum;}下面编写主函数,并把函数fgz的定义放在主函数的前面。#includedoublefgz(doublesum,doublevalue)(intnum;/・要发的张数・/inttemp;doublenewsum;/・尚要发放的新的剩余额・/num=sum/value;if(value>l)printf(w^S%d7ClW^^!(>^%d

260w,(int)value,num);elseprintf('、要发セf角的张数为%d\口“,value*10,num);/・得到尚要发放的剩余额・/newsum=sum-value*num;returnnewsum;)intmain(){doublesum;/・实发エ资总额、剩余エ资总额・/doublemoney[7]={100,50,10,5,0.5,0.1}/・钞票面值数组・/printf(、、请输入该职エ的实发エ资总额");scanf(''%fw,sum);for(i=0;i<=6;i++)(sum=fgz(sum,money[i]);))说明:由于把钞票面值放到了数组中,程序再一次得到简化。如果不把钞票面值放到数组中,程序就不便用循环,函数调用必须写6次。intmain。函数的程序如下:intmain(){doublesum;/・实发エ资总额、剩余エ资总额:*/printf(、、请输入该职エ的实发エ资总额");scanf(''%f,sum);sum=fgz(sum,100);sum=fgz(sum,50);sum=fgz(sum,10);sum=fgz(sum,5);

261sum=fgz(sum,0.5);sum=fgz(sum,0.1);)显然将发放钞票的各种面额放在ー个数组中,这样编出来的程序更为简洁。问题1.是否可以把函数的定义放在main函数的后面?答:可以把函数定义放在main函数的后面,但要在main函数的前面加上函数的声明。例如在本题中,应进行如下的声明。#includedoublefgz(double,double);/・函数声明・/main(){)doublefgz(doublesum,doublevalue)/*函数头部*/(函数体;)注意函数声明和函数头部(有的书中称为首部)有以下几点不同。(1)函数声明最后有分号,函数头部最后没有。(2)函数声明的形参表中只需要参数类型,不需要参数名.(3)函数声明不一定是必须的;而函数头部一定要有,后面一定跟函数体。问题2.此题是否可以不用函数,而只用循环来编写呢?习题.将此题改造成可以发放多个职エ工资的程序。编程原则:编写函数的原则之三如果想把一段连续的功能上比较独立的程序段改写成函数,那么此程序段中所有需要外界提供的已知量(包括两部分,一部分是由前面的程序段通过输入或赋值语句得到值的变量,另ー类是此程序段中要通过输入语句得到值的变量)一定要作为函数的形式参数(使用全局变量一般是不可取的,因为全局变量破坏了函数的封装)。第52讲游戏中的射击问题类型趣味性***难度***已知笛卡尔平面坐标系中有n个精灵,第i个精灵的坐标值为x[i]和y[/]o玩家从某ー坐标点x,y射出的子弹的轨迹是一条直线,角度是alfa度,且具有极强的穿透力(可穿透任何精灵),请计算出玩家这ー枪击中了哪几个精灵。此题初看起来似乎比较麻烦,没有思路。为此将此题简化成子弹某一瞬间在某ー点的问题。已知笛卡尔平面坐标系中有〃个精灵,第i个精灵的坐标值为x[i]和y[/]o玩家射出的子弹现在在x,y坐标点处,请计算此点是否在某精灵的身体中。此题与前面介绍求建筑物的高度类似(参见第8讲)。

262预备题:求任意x,y点处建筑物的高度。已知在笛卡尔坐标系中有〃个圆塔,第i个圆塔的坐标值为x[i]和yド]。圆塔的半径均为r。所有圆塔的高度均为八米,求任意给定一点x,y处建筑物的高度。下面首先写出这ー题的算法。算法讨论:此题中比较麻烦的是,塔的数量是ー个不确定的值。首先将所有塔的塔心坐标的x值存放在N”+1]数组中,所有塔心坐标的y值存放在数组中,所有塔的半径都为r(其实本题最好的方法是用结构数组来做,参见第8章)。用i作为循环变量。ー级算法:(0)输入圆塔个数”(1)将各圆塔的圆心坐标值输入到各个数组元素中(2)输入任意待求点的坐标值x和y(3)i=l:(4)flag=0;/・标志变量,事先假设x,y不在任何塔内・/(5)当(沁=〃)((5.(1)x,y在塔i内那么(flag=lj打印坐标点在塔i内:)1++;(6)如果(flag等于0)那么打印x,y点建筑物的高度是Om;否则打印x,y点建筑物的高度是/?m;其中第I步需要进ー步求精。二级求精:(1)1)i=l;(2)2)当i<=n{输入ー个数到中输入ー个数到y口中<++;)可根据此算法转换成C语言的程序。在解出预备题的基础上ド面来考虑原题,可以发现题目此时就容易多了。原题中的精灵可以看成是圆塔,只不过现在要解决的问题不是某ー个点x,y是否在圆塔内,而是一条射线是否穿过了某个圆。ー级算法:(0)输入精灵个数〃(1)将精灵坐标值输入到数组x和y中(假设精灵)(2)输入生成射线方程式的参数(这条射线表示的就是子弹的穿透路径)(3)/=1;

263(1)flag=O;(5)当(i<=〃)((5.(1)射线与(表示精灵,的)圆i相交那么|flag=l;打印精灵i被击中;)(5.(2)i++;)(6)如果(flag等于。)打印没有精灵被击中;(7)结束现在解出此题的关键已经变成一个纯粹的平面解析几何的问题,某射线是否和某个圆相交(精灵的横截面可以简化成一个圆)。这个问题又可以简化为求某条线(子弹的路径)到点(即圆心)的距离问题。如果此距离比半径小,说明精灵被击中。某直线到某点的距离是否小于半径r,这显然是ー个命题,答案一定是非“真”即"假”。现在的问题是,某直线到某点的距离是否小于半径R,要出现在选择结构的后面作为条件。而选择结构if的后面要求是ー个布尔表达式,如果某直线到某点的距离是否小于半径Z"不能简单地转换为ー个布尔表达式,怎么办呢?可以用本章中介绍的函数来做。编写ー个函数relation,某直线到某点的距离是否小于半径R,如果为“真”,则函数返回值!!否则返回值0。返回值虽然是整数,但可以被当作逻辑值来对待(原因是在C语言中没有逻辑型变量)。在函数relation中,与直线有关的参数有:起点x,y和角度alfh;与圆有关的参数有:圆心坐标","和圆的半径な函数的返回值有:〇(表示不相交)和1(表示相交)。下面得出最后此问题的算法。ー级算法:(0)输入精灵个数〃(1)将精灵坐标值输入到数组x和y中(2)输入生成射线方程式的参数(这条射线表示的就是子弹的穿透路径)(3)/—1;(4)flag=l;(5)当(/<=«){(5.(1)(relation、y,alfa,x[z],y[j])==l)那么(flag=O;打印精灵i被击中;)(5.(2)I-H-;}

264(6)如果(l=flag)打印没有精灵被击中;(7)结束・习题1.请读者两人ー组,把此题变成C语言的程序,一位编写函数relation(),另一位负责调用此函数,来完成剩下的任务。点评.タ对于if选择结构和循环结构的布尔表达式,如果在编程工作中还仅仅是ー个命题,无法立即写出布尔表达式;或者表达式太复杂以至于很难写出来的话,可以把要做的这件事(处理求证命题的真假)编写成一个函数,此函数的返回值用逻辑类型,即命题成立,返回值是1,命题不成立,则返回值是0(山于C语言的C89标准中没有逻辑类型变量,所以只能由返回值仅取0或1这两个值的函数来取代)。点评.タ如果函数的返回值只能是〇和1,最好查看一下库函数的使用手册,弄清楚此函数调用是不是仅仅起・个判断命题真假的作用。习题2.如果子弹的穿透カ不是很强,而只能击中第一个遇到的精灵,程序将如何改动?5.4用数组作为函数参数函数的形式参数不仅可以是简单变量,还可以是数组这样的构造型(即容器型)变量。用简单变量作为形参和实参时,实参的值在调用时被传给形参。正因为如此,简单形参变量值的改变オ不会影响到实参变量的取值——它们是处在不同内存空间的变量(即便是形参变

265量与实参变量同名,请参见第8章9.5节)。但是,如果用整个数组作为形式参数(形参)和实际参数(实参),在函数调用时,实参数组并不是将每个数组元素的值传递给相应的各形参数组元素,而只是将实参数组的首地址传给形参数组——形参数组与实参数组其实是同一个数组。这样做的好处在于可以节省复制数据的时间。数组名本身表示的就是此数组的首地址。第53讲以数组作为函数参数,重做选择排序问题,将第45讲中选择排序的程序转换成函数类型必修题趣味性**难度**根据第45讲讨论的原则,将以下main〇形式的程序修改成函数。1234567891011121314151617181920212223242526272829303132333435#include/・删去・/#defineN20/・册リ去・/main()/Z改为voidintitj;floata[N];/*删去・/floattemp;for(i=0;i

266调用者传入实际要排序的数组的首地址给此形参数组),第2行也被删去了,是由于此数N也要求函数调用者传入实际的参数值给函数。第29行到第33行被删去了,这是由于排好序的数组就是(被传送进首地址到函数中的)主调函数中的同一个数组,所以,根本不必打印,也不必用return语句返回值。最终函数形式的选择排序程序如下:(1)(2)voidselect_sort(float((3)(4)inti,j;(5)1=1;(6)while(i<-N-2)(7)((8)/・求第i个位置的最大数・/(9)for(j=i;j<=W-l;j++)(10)((11)if(a[i]

267逗号隔开,每个形参的类型说明也不能少;而实参表可以有多个,出现在任何函数调用的场合,实参之间要用逗号隔开,实参前不能加上类型名。实参不仅可以是变量,还可以是常量、表达式。(5)实参和形参在数量、顺序和类型上应当•・致,否则会发生类型不匹配的错误。注意在赋值语句中,会进行自动类型转换的,则不受此类型要求一致的限制。小结:形参和实参的类型可以不一致,但只有通过验证,满足以下条件的自动类型转换オ允许类型不一致。类型验证公式如下:形参类型实参类型II变量类型=表达式类型;ー这是一条赋值语句的形式例如;实参是实型,形参是整型,这是否是允许的参数传递形式?答;把此两种类型代入以上的公式,公式就变为:形参类型实参类型II整型=实型;由于这种类型的赋值语句是不允许的,系统不会进行自动类型转换,所以不可以用实型作为实参来传给整型的形参。但是反过来,实参是整型,形参是实型,则是公式所允许的。请读者思考并代入公式验证。注意虽然可以把下划线用在自定义的函数命名中,但希望大家不要用在作为函数名的标识符的第一个字符,以免和库函数重名。5.5函数的嵌套与递归深入ー步;c语言源程序中的每个函数,都会被编译程序翻译成一段(机器语言的)子程序。除了main函数外,其他函数之间都可以互相调用。也支持嵌套调用。C语言中,不允许在一个函数(定义)的函数体中出现另ー个函数定义。换言之,C语言不支持函数的嵌套定义。不仅main函数可以调用函数,在任何ー个函数的函数体中都可以调用其他的函数。在ー个函数的函数体中,如果出现调用它本身的函数调用语句,则称为递归调用。递归在解决某些问题时是ー个十分有用的方法,它可以使某些看起来不易解决的问题变得容易解决。递归其实是比较难以理解和运用的一大类算法。编写递归函数就是要编写•种特殊的函数体,该函数体中有一个(或多个)语句,这个语句调用函数自身(称为递归调用)。不同之处在于,实际参数比上一次调用耍小。用这种方式写出的程序比较简短。但是,递归通常要花费较多的机器时间并占用较多的存储空间,所以•般效率不太高。第54讲用递归计算n!讨论:在前面曾用循环的方法实现了n\的计算,现在用递归的方法计算。〃!可以用以下的

268递推公式计算。此式有错1"=0nx(n-l)!n>0按照这个公式,可以将求〃!的问题变成求(〃ー1)!的问题[因为当(〃ー1)!求出以后,再乘以〃就可以得到〃!];而求(〃ー1)!的问题可简化成求(〃ー2)!的问题[因为当(〃ー2)!求出以后,再乘以〃ー1就是(〃ー1)!的值]……求1!的问题可简化成求0!的问题(因为当〇!求出以后,再乘以1就是1!的值)。而根据递推公式〇!可以直接得出数值,那就是1,至此由大到小的回推过程结束。但是还必须由0!计算出1!:由1!计算出2!最后由(〃ー1)!计算出〃!。这样ー步ー步由小到大递推过去,计算工作才算结束。计算过程非常麻烦。但最大的好处是,可以很容易地根据这个递推公式编出递归调用的函数。下面给出用递归计算〃!的程序,形式参数的设置显然只要一个参数(用来决定到底是求什么数的阶乘数)就足够了。函数返回值的类型用整型、长整型longint、双精度实型都可以。函数名取为たc。intfac(intn)j/*(1.1)函数首部・/Iif(0==n)return1;else/*(1.2)*//*(1.3)*/returnn*fac(n-1);/*(1.4)*/)为了调用这个函数求出某个整数的阶乘,还要编写ー个intmain。函数来调用它。程序如下:#includeintmain()intk,ff;scanf(”吿d”,&k);ff=fac(k);/*(2.1)*//*(2.2)*//*(2.3)*/printf("*d的阶乘是%d

269",k,ff);/*(2.4)*/将这两个函数输入并保存到同一个文件中,编译并运行它们。下面讨论ー下程序是如何运行的(这段论述很繁琐,不想看的读者可以跳过)。(1)程序从intmain。函数开始运行,在运行到2.2句(相对应的机器指令)时,从键盘输入ー个数值1到变量&,表示要求1的阶乘是多少。(2)执行2.3句,此句是一个赋值语句,先执行函数调用fac(l)(因为k的值是1),这个数值1被传给fhc()函数的形式参数〃,此时"等于1,函数体开始运行,因为“不等于〇。程序运行到1.4句returnドfhc(0);处,在此处进入下一次fac(O)函数的调用。注意第一次函数调用运行到此句时,还无法返回主函数intmain。,因为返回语句return中的表达式returnl*fhc(O)无法立即得到ー个具体的数值(因此,第一次函数调用fac(1)并

270没有结束并返回到main()函数),还要再次调用函数fac().幸运的是,此次调用的实际参数值是比较简单的值。。(3)这个数值〇又被传给fhc()函数的形式参数”,函数体再次得以运行,但由于此次(即第二次函数调用)的参数为〇,因此它会执行(1.3)句return1,函数调用fac(0)的值就求出来了,结果是1。(4)此时应将第二次函数调用的结果1返回到调用处。即返回到第一次以1为实参调用fac()函数的返回语句之处:returnl*fac(O);,此句现在变成了return1*1;即return1:。(5)现在第一次函数调用的return语句才可以执行,程序流程现在回到主函数的2.3句,不过,现在这条语句在函数调用执行完后,由ffMhc(l);变成AM;这样的一条赋值语句。(6)因此,运行2.4句,在屏幕上显示出来的信息是:1的阶乘是1。由此可见,递归过程的函数虽然不一定难编(只要能找出递推关系),但程序的执行流程是很复杂的。最大的特点是上一次的函数调用尚未结束,就要以新的比较小的实参值再一次调用这个函数本身。计算机是不会把两次(甚至多次的递归)调用搞混的,但如果用语言来描述,人类的大脑就会有些受不了,更不要说"的值比较大的多次递归了。所以,这里仅列举了k=l时的递归调用。读者要注意的是,每次调用的局部变量看起来用的是同一个变量名,实际上这些同名的局部变量在内存中占用的是不同的存储空间(参见第8章中函数调用时参数传递过程中的栈结构)。但是对于依3的调用,用如图6.1所示的方式来表示,程序的运行流程就比较清楚。第一次调用第二次调用第三次调用第四次调用n=3〃=2n=ln=0kk=fac(3)73*fhc⑵/2*fac(l)/l*fac(O)/1'返回值6、返回值2、返回值1、返回值1图6.1鱼c(3)的递归调用所有调用结束后,最终得到的赋值语句是kk=6;。第55讲汉诺塔类型经典题开拓思路题趣味性***难度***本讲讨论非常著名的汉诺塔问题,几乎大部分编程书中都会介绍的就是汉诺塔。该问题用递归解决起来很简单。既然是经典例题,本书自然也不能例外。据说这个问题来自古印度,某个神庙里有三根细细的圆柱,共有64个大小不等,中心有圆孔的金盘套在柱子上。这就是汉诺塔。最初64个盘子是由大到小自下而上地全部在A柱上。目标是要利用B柱作为中间过渡柱,把这64个盘子全部移到C柱上移动过程中要遵守的规则是:每次只能移动ー个盘子;大盘在任何时候都不能压在小盘之上。僧侣们遵照这个规则日夜不息地把圆盘从ー个柱子搬到另ー个柱子上,每次只能搬动ー个盘子。由于需要移动的次数太多,据说当所有盘都搬到C柱上时,世界就要毁灭。汉诺塔如图6.2所示。

271ill图6.2hanoi塔首先,用数学归纳法证明此题对任何〃(即〃可以是任何正整数)有解。(1)当盘子数〃=1时,只需直接将此盘从A柱搬到C柱即可。(2)现假设片k时有解,即可以将k个盘子(在不违反规则的情况下)从ー个源柱,通过ー个中间柱移到目的柱上。(3)现在证明n=k+1时也有解。开始时A柱上的k+1个盘子可以看成由k个盘和最底下的ー个最大盘组成。根据归纳假设这k个盘可以(在不违反规则的情况ド)通过C柱移到B柱上(在这k个盘的移动过程中,最大盘可以看成不存在)。完成这一大步后,只要将A柱上的最大盘直接搬到C柱上。再根据归纳假设B柱上的这え个盘可以(在不违反规则的情况下)通过A柱移到C柱上。至此证明结束。通过上述证明,也为用递归方法编写函数找到了思路。任何ー个规模为た+1的汉诺塔问题,都可以分解成三步。开始时,A柱上的k+1个盘子可以看成由え个盘和最底下的ー个最大盘组成。然后进行以下三步。第一步:将A柱上的这え个盘(在不违反规则的情况下)通过C柱移到B柱上。第二步:将A柱上的最大盘直接搬到C柱上。第三步:将这k个盘(在不违反规则的情况下)通过A柱移到C柱上。为编写函数方便,把k+1变成〃,上述论述成为:任何ー个规模为〃(当n>l)的汉诺塔问题,都可以分解成下面的三步。开始时,A柱上的〃个盘子可以看成是由〃ー1个盘和最底下的ー个最大盘组成。然后进行以下三步。第一步:将A柱上的这〃ー1个盘(在不违反规则的情况下)通过C柱移到B柱上。第二步:将A柱上的最大盘直接搬到C柱上。第三步:将这〃ー1个盘(在不违反规则的情况下)通过A柱移到C柱上。至此,通过编写递归函数把这个搬动过程在显示屏上打印出来就没有太大的问题了。而且,如果巴经制造出了一个可以搬动盘子的机器人,那这个递归函数就可以用来控制这个机器大去(不违反规则地)搬动盘子。现在考虑如何编写解决这ー问题的递归函数。即使是递归函数的编写,也应当遵守一般函数的编写原则。根据形参数最少原则和上述分析,这个函数要指明有多少个盘子在源柱上,要把哪个柱作为中间柱,哪个柱作为目的柱。由此可见,函数至少应当有4个形式参数,分别是盘子数(n)、源柱(fixim)、所借助的中间

272柱(by)、目的柱(to)。表示三个柱子的形式参数类型用字符类型。每个盘子的大小尺寸可以不作为形式参数。因为只要按照规则去搬动,不违反大盘不能压在小盘之上即可。由于此函数只需打印盘子的搬动次序(实际上,只需打印出正确的搬动顺序,而盘子的大小完全可以不考虑),所以此递归函数可以不返回任何值。函数名可取为hanoio这样,函数的头部就确定了。voidhanoi(intnfcharfrom,charby,charto)下面给出将n个在fix>m柱(源柱)的盘子通过by柱(中间柱)移动到to柱(目的柱)上的递归算法ー级算法:如果n等于1打印from—to;/・递归进入最简单的边界情况・/否则/・将一个n规模的问题转换为完全等价的n-!规模的下述三步・/(hanoi(n-1,from,to,by);/*对应于从n到れー:].的第一步・/打印从from柱移至リto柱;/*对应于从n到n・l的第二步・/hanoi(n-1,by,from,to);/・对应于从n到n・1的第三步*/}转换成C语言的程序如下:/・将n个初始在from柱的盘子通过by柱移动到to柱的递归函数1voidhanoi(intn,charfrom,charby,charto)2(3if(l==n)4printf(w%c->%c

273w,from,to);/・打E卩from-*t。;5else6{7hanoi(n-1,from,to,by);8printf(M%c->%c

274w,from,to);9hanoi(n-1,by,from,to);10)11)下面,再编写ー个main()函数来调用此函数。12#include13intmain()14{1516intm;17printf(、、请输入要搬动的盘子数"");18scanf(w%d",&m);19hanoi(m,'A-B"C);20return0;21)问题.如果不小心漏掉了第3、4句,会怎样?答:递归不会终止。习题.给每个盘子由小到大指定一个整数编号,能否修改此程序,把耍搬动的盘子的编号ー并打印出来?(提示:比如有4个盘子,由小到大分别为1、2、3、4号盘,仔细分析上述代码,主要观

275察输出函数打印时,移动的到底是几号盘。)参考答案如下:voidhanoi(intntcharfrom,charby,charto)(if(n==l)printf("l号盘%c->%c

276w,from,to);/・打E卩1号盘fromTto;else(hanoi(n-1,from,to,by);printf('、告d号盘宾->%c

277w,n,from,to);hanoi(n-1,by,from,to);小知识:数学归纳法与递归函数两者间的密切关系以及对比用递归方法编写函数,苜先要给出,在n等于1(或者是〇或者是一个比较小的值时)时问题的具体解决方案(使得递归过程能够中止,比如上一题中的代码段:if(n==l)printf("l号盘%cf%c

278",a,c););其次,要能将规模n的问题,转化为等价的规模为n-l的问题。把这和数学归纳法的证明比较一下,恰好是ー个相反的过程(ー个由大到小,而另ー个由小到大)。在可以运用数学归纳法证明的应用场合下,我们常常可以先用数学归纳法来证明问题有解,在归纳法证明过程中找到的规模为n的问题与规模为n-1的问题之间的相互关系,往往也是解决用递归函数编程的关键所在。因为用递归函数编程解决问题的关键所在就是:如何将规模为n的问题,转化为等价的规模为n—1(有时不一定够,如fibonacci序列问题)的问题,就如上述汉诺塔问题所体现的那样。数学归纳法的证明,就如同推倒多米诺骨牌的编号为1的第一张牌,其他各张大号牌会依由小到大的牌号顺序自动依次倒下直到编号任意大的ー张牌,特点是做起来非常容易(自动化,ー张牌倒下的后果,成了下ー张牌自动倒下的原因,紧密相连);而函数的递归调用类似于是从某ー张大号的牌向编号较小的牌方向推倒这ー系列的多米诺骨牌,特点也是非常容易(自动化,由规模为n的递归函数调用的,自动地进入规模为n-1的函数调用)。不过两者之间还是有两点不同:1.递归过程必须在n等于ー个比较小的值的时候能够中止。因此在编写此类递归函数的程序时,一定不要漏掉递归中止的代码段。2.调用进入到最简单的递归函数后,・般还要逐次返回上•级主调函数,最终返回到递归函数外的主调函数。小结:编写规模为n的递归函数的函数体的统ー模式:if(n满足递归中止条件)处理递归中止的代码;else

279通过调用递归函数本身,将规模为n的问题转化为更小规模(通常是n-1,但也有例外)的问题的代码;第56讲用递归求整数m与n的最大公约数类型必修题趣味性**难度**讨论:从数学上可以知道,求m与〃的最大公约数等价于求〃与机%〃的最大公约数。这时,可以把〃当作新的m把机%〃当作新的n,问题又变成求新的机与〃的最大公约数。如此继续下去,直到新的〃=0时,(最初两数m和〃的)最大公约数就是此时最新的Z〃。如果令gcd(Z〃,〃)为求/〃与〃的最大公约数的函数,则通过上述讨论,可得到以下的式子。请编辑注意:下图中的ged要改成gcd:tnn=0gcd(/n,n)=、、れged(n,加%〃)n>0据此公式,可以很容易按照编写递归函数的统一模式写出此递归函数。求整数m与〃的最大公约数的程序如下:(1)intgcd(intm,intn)(2){(3)if(0==n)(4)returnm;(5)else(6)returngcd(n,m%n(7)});问题1.此程序的健壮性如何?答:健壮性不好,完全没有考虑到用户输入的Z〃值可能比n小的问题。问题2.此递归函数的调用有何特点?答:一般的递归函数调用过程中,在后半阶段(当取值达到最小临界值后)都有一个逐层返回上一级函数的回归过程。但此例中没有这个过程,原因很简单,这是因为第6句的返回语句,不是在函数调用返回后还需要进ー步运算的一个表达式(求”的阶乘是这种情况);此外else分句也只有一个简单的返回语句,而不是ー个复合句(汉诺塔属于这种情况)。习题1.用递归方法求fibonacci序列的值。所谓fibonacci序列是1,1,2,3,5,8,13即除了前两项外,以后的每ー项都是前两项之和。第57讲在ー个由小到大排好序的整型数组中进行二分法查找类型开拓思路题实用题趣味性**难度***算法讨论:在计算机的应用程序中,对ー批数据进行排序和查找是极为重要的最基本且常用的操作,有很多种实现算法。二分法查找又称为折半查找,它是一种效率比较高的查找方法,很有实用价值。折半查找要求数组是有序数组,即数组中的元素值是有序的,不妨设有序数组是递增有序的。

280下面介绍折半查找的基本思想。设ロow..high]是当前的査找区间,查找的过程如下:(1)首先确定该区间的中点位置mid。(2)将待查的k值与arr[mid]比较,若相等,则查找成功并返回此位置,否则须确定新的查找区间,继续二分查找,具体方法如下。1)若arr[mid]>k,则由数组的递增有序性可知,arr[mid]arr[high]均大于匕因此若数组中存在等于k的数组元素,则该元素必定是在位置mid左边的子数组arr[low]arr[mid-l]中,故新的查找区间是左子数组arr[low]....arr[mid-l]»2)类似地,若arr[mid]<&,则要查找的:必在mid的右子数组arr[mid+l]...arr[high]中,即新的查找区间是右子数组arr[mid+l]...arr[/iig/i]中,下一次査找是针对新的査找区间进行的。因此,从初始的查找区间[low…high]开始,每经过一次与当前查找区间的中点位置上的结点关键字的比较,就可确定查找是否成功,不成功则当前的查找区间缩小一半。这一过程重复进行,直至找到关键字为k的结点,或者直至当前的查找区间为空(说明数组中并无此数)。ー级算法:/・在有序数组arr[0..n-1]中进行二分査找,是否有一个值为k的元素。成功时返回元素的下标值,失败时返冋ー1*/(1)low=0;/・置当前査找区间上、下界的初值・/(2)high=n-l;(3)while(low<=high){/・当前査找区间arr[low..high]是非空的*/(mid=(low+high)/2;if(arr[mid]==K)returnmid;/・查找成功返回・/if(khigh时表示査找区间为空,査找失败・/如果从第3步开始编写递归函数,首先要考虑设置几个形式参数。数组本身肯定要作为参数,查找值k也要作为参数,还有查找的上下界low和high,ー共4个参数,返回值是整型。函数本身需要一个局部整型变量mid,表示中间元素的序号。编写递归函数如下:1intbinarysearch(intarr[],intk,intlow,inthigh)2{3intmid;4while(low<=high)5(6mid=(low+high)/2;7if(k==arr[mid])8returnmid;9if(k

2811)2return-1;34)现在,还要编写ー个主函数,用来调用此函数:5#nclude18main()19{intarray[15]={1,3,7,8,10,14,24,25,27,32,36,46,67,89,99)20intkey,search,n=15;21printf(''inputanumberforsearch:

282w);22scanf(、、%d”,&key);23search=binsearch(array,key,0,n-1,);24if(-l==search)25printf("no'ハ");26else27printf(''OK!,array[%d]=%dw,mid,search);)问题.第1行是否可改为:intbinarysearch(constintarr[],intk,intlow,inthigh)1即在数组名的类型前面加上修饰符const?如果可以,有何作用?答:可以,而且很有用处,值得向读者大力推荐。提示二分法查找在编程时非常容易出错,错误集中在对新的查找上下界的调整上.据说世界上第一个没有任何Bug(即错误)的程序,在算法本身提出的16年后(1962年)オ出现・各种错误的二分法查找程序,可参见《算法设计与实验题解》中的相关内容。・习题1.不调用库函数,利用递归的二分法求”的平方根。精确到0.01。点评タ二分法在算法中属于分治法,它的应用范围极其广泛,算法的效率也比较高。二分法不仅可以求〃的平方根,也可以求任意次的开方。•般,还可以用来求X的(〃》/〃)次方,其中"1和”为整数。*・习题2.参考其他书籍,用递归的方式编写ー个快速排序算法的程序。点评.タ只要找到了递推公式和递推结束时该做的工作,利用编写递归函数的统ー模式是很容易编写出递归函数的。5.7提高部分C语言的源程序在经过编译形成二进制的可执行文件,并被操作系统(参见9.4节)载入到内存(安排了代码和数据的存储空间)中后,其在内存中的分布形成了几个相对独立的空间,如图6.3所示。代码区(可执行程序)静态数据区

283动态数据区(栈区stack)臼由空间区(堆区heap)图6.3ー个C程序的内存分布堆区的使用只能采用指针的方式向操作系统申请空间(比如调用malbc函数),使用完后,要将空间归还给系统(比如调用f?ee函数)。下面介绍变量的其他分类性质。ー个变量定义实际上包含多重意义,包括:给出变量的类型;规定变量的名称;分配给此变量的存储空间;限定变量可进行的运算。但实际上,这个定义还同时确定了该变量的其他两个性质(但有时还要在类型名前加上修饰符,比如staticint/”;),即作用域和生存期。(1)按变量的作用范围(即有效范围)来分,可分为局部变量和全局变量。所谓全局变量是指定义在任何函数之外的变量,它的存储空间是静态数据区。这种变量的有效范围是整个C程序的文件。也就是说,在同一个文件中的任何函数都可存取这种变量的值。比如;intmax;#includemain()()这个max就是全局变量。而在函数形参表中的变量和函数体内定义的变量都是局部变量。编程原则之一;全局变量要尽可能少用。因为它破坏了函数的封装性,任何一个函数对它的取值进行修改,都会作用于全局。因此,依赖全局变量的任何函数也会受到全局变量被修改的影响。问题1.主函数main。中定义的变量是全局变量还是局部变量?答:是局部变量,它的存储空间在动态数据区获得。这些变量在其他的函数中一・般都不能存取(除非将这些变量的内存首地址作为函数的指针变量的实际参数,这样オ可以在调用的函数中,间接存取这些变量的值)。不过,因为程序的运行是从main()开始,到main。函数的最后一条语句结束的(除非遇到了exit。函数调用),因此main。函数中的局部变量的生存期与全局变量相同。问题2.main()函数中定义的局部变量与一般函数中定义的局部变量有何区别?答:所谓生存期是指变量拥有系统分配的存储空间的时间。普通函数中的局部变量和形参变量在函数调用时获得内存空间(形参和局部动态变量都在栈中获得存储空间即进栈,参见第10章),在函数返回到主调函数时,所有的形参变量和(动态的)局部变量都失去内存空间(除非是后面将耍介绍的静态局部变量),这些内存空间都被归还给系统(采用的方法就是出栈,参见第10章)。但由于主函数在程序运行的开始就开始,在程序运行的最后オ结束,所以主函数中的(动态)局部变量的生存期是最长的。注意在任何的复合语句的ー开始也可以定义变量,则该变量的作用域和生成期就仅限于此复合语句中。问题3.函数的局部变量名是否可以和全局变量同名?答:可以,在这个函数调用期间,此变量名表示的是局部变量,函数调用结束后,此变量名表示的仍然是全局变量。这是两个同名但属于不同存储地址的变量。

284(2)按生存期来分,可分为静态变量(static)和动态变量。静态变量获得的内存空间不是在动态数据区中,而是在静态数据区中。所以即使该函数调用已经结束,但属于该函数的局部静态变量的值仍然被保存在内存中。直到下一次该函数被调用时,此静态变量上一次调用后的值仍然是可以使用的。点评.タ各种类型变量的特点如下(1)全局变量的作用范围最广,在程序运行的全部代码段中起作用,但是在某些函数中如有同名的局部变量,则全局变量在此函数中不起作用。它的生存期最长,从程序运行的开始到结束。(2)main函数中局部变量的作用范围小,只在main函数中起作用。但由于main函数在整个程序运行期间・直占用内存,所以main函数的局部变量的生存期最长,与全局变量的生存期ー样长。(3)所有局部静态变量的特点与main函数中的局部变量类似,其作用范围小,但生存期长。在某个函数中定义的静态变量,即使此函数调用结束,此静态变量的值仍被保留下来(但其他函数不能使用此变量的值),在下次进行该函数的调用时,还可以得到该变量在上一次调用结束时的值。(4)其他函数中的动态局部变量的作用范围最小,生存期也可能最短(要看此函数究竟ー共调用了几次)。问题4.ー个函数的局部变量是否可以与另ー个函数的局部变量同名?答:可以。不过它们是两个在不同作用域中的不同变量,在内存空间中占据的位置也不同。问题5.在ー个递归函数的调用过程中,多次不同的调用同一个动态形参变量或局部变量(比如autointm;,但所有没有auto修饰符的局部变量都被认为是动态的,只有静态局部变量ー定要在类型名前加修饰符static),所占用的内存是否是同一个?答:不是,每递归调用一次,所有的动态形参变量或局部变量(通过进栈操作,参见第!0章)都会获得新的内存空间。最外层调用的(动态)局部变量和形参的生存期最长;而最内层调用的(动态)局部变量和形参的生存期最短。问题6.在ー个递归函数的调用过程中,多次不同的调用同一个静态形参变量或静态局部变量占用的内存是否是同一个?答:是同一个。问题7.局部静态变量有何用处?答:局部静态变量最大的用处是,在两次函数调用之间传递属于函数本身的私有数据。这比用全局变量好得多,因为全局变量的值可能会被文件中的任何函数体中的语句改变。而静态变量的私密性要好得多,因为只有这个函数本身可使用它,不必担心会被别的程序员编写的其他函数中的语句修改它的取值。大力提倡用静态变量,而不用全局变量在同一个函数的两次调用之间保留或传递数据。比如要编写ー个函数,还要统计此函数的调用次数,就可以在函数体中定义ー个静态局部变量来做此事。这比用全局变量或主函数中的局部变量要好。

当前文档最多预览五页,下载文档查看全文

此文档下载收益归作者所有

当前文档最多预览五页,下载文档查看全文
温馨提示:
1. 部分包含数学公式或PPT动画的文件,查看预览时可能会显示错乱或异常,文件下载后无此问题,请放心下载。
2. 本文档由用户上传,版权归属用户,天天文库负责整理代发布。如果您对本文档版权有争议请及时联系客服。
3. 下载前请仔细阅读文档内容,确认文档内容符合您的需求后进行下载,若出现内容与标题不符可向本站投诉处理。
4. 下载文档时可能由于网络波动等原因无法下载或下载错误,付费完成后未能成功下载的用户请联系客服处理。
最近更新
更多
大家都在看
近期热门
关闭