《何勤:《C语言程序设计问题以及求解方法》》由会员上传分享,免费在线阅读,更多相关内容在行业资料-天天文库。
C语言程序设计问题与求解方法——编程高手修炼捷径何勤著
1代序目前,人们要真正学会编程,大多都要花费五年以上的时间,悬梁刺股、卧薪尝胆。真正原因何在?其实只需到真正会编程的人身上,就能找到根本原因。经过认真仔细分析,我发现根本原因在于:每个真正会编程者,都必须具备计算机科学的大局观。也就是说,每个真正会编程者都必须懂得和掌握:I、c语言的基本语法(主要是各种命令型语言的公共部分,其他语言目前还无法取代)2、大量阅读和调试经典的、基本的、由易到难的各种类型的C语言程序(至少100题以上)。消化和积累各种基本问题的编程思路,并能用逐步求精的伪代码构造常见问题的算法;3、学习算法和数据结构的基本知识。4、整体上把握计算机到底是如何工作的;5、整体上把握操作系统是如何在硬件的密切配合下通过查找各种表格,管理调度计算机的所有软硬件资源为多道应用程序运行(和计算机用户)提供服务的。6、编译程序大体上是如何对源程序进行编译工作的。以上六项是必须具备的。此外,如果想成为程序员,还应当学习和掌握:汇编语言程序设计、数据库原理及数据库编程、计算机网络及网络编程、面向对象程序设计等课程知识、计算机原理和操作系统更深入的知识。由此可见,这条成才之路确实非常漫长和艰苦!尤其是第4到第6项知识的掌握,更是一个极为痛苦的心路历程上的万里长征!因为读者不仅要认真学习这些知识,而且还要做大量的提炼精华、融会贯通的艰巨功课。这是由于各门课程之间的内容衔接这项无比艰巨的工作,通常必须由学生亲自去完成,目前没有任何一本书在这方面做得比较好。为了大大减轻这个成才过程中的痛苦,明显缩短读者真正学会编程的周期。笔者花费了十余年时间广泛收集、筛选素材,并且进行了长时间地、艰苦地探索。终于很幸运地找到了一个绝好的比喻——理想厨房系统,恰好可以把以上几个方面的知识在一个比较初级的层次上,比较完美地串联起来。构成一幅计算机科学中(与真正学会编程有关的)最重要的、最精华的基本知识的“联络图”。为初学者在短时间内把握计算机科学的大局观并学会编程,开辟了一条相对比较轻松的捷径。而且,这也为后续更高级编程技术课的学习奠定了扎实的知识基础。本书虽然比较系统地讲解了C语言语法,但是,读者别指望从这本书中找到很多高深语法细节问题的详细讲解。因为笔者认为:过早学习太多高深语法知识,是很多读者学不会编程的罪魁祸首!这将导致很多读者觉得程序设计课相当枯燥乏味。浪费了读者本可以用来学习生动有趣的编程思路和技巧的、有限和宝贵的精力。大脑里留存下了一大堆未经消化的细节语法规则,反而束缚了最需灵活自由的编程思路的展开和翱翔。编程语言的高级语法知识的学习和掌握,比大量编程思路的领会、消化和积累要容易得多!读者在学习编程过程中最需要得到的是编程思路上的引导和启发。读者应当在真正学会编程后再决定是否要比较全面深入自学哪一种程序设计语言的语法细节知识。本书中的大多数例题值得你认真钻研,因为其中蕴含了大量比较经典的基本编程思想和编程技巧。本书不可能系统讲解各种编程思路(这是算法、数据结构这两门课程的任务),然而,本书却教给了读者很多有效的举一反三的编程方法,这种方法强调从特殊到一般,来
2探索问题的编程思路和编程技巧。在循环和数组这两章中的很多例题中,展现了如何利用这种方法来做各类编程题。本书也很重视培养读者用逐步求精的伪代码来构思算法的能力。读者要注意,学习钻研本书,一定要配套做100—200道各种类型的由易到难的编程题。这个亲自动手编写和调试程序的实践性修炼环节,是任何编程书籍和老师都无法替代的。这是学会编程决不能省略的最重要环节。再困难也要坚持,挺过开始的困难阶段,变成一种习惯后,你就能够逐渐享受到编程带来的极大乐趣。读者还要特别注意:千万不要被某些教科书误导,从而陷入钻研一门高级语言语法细节知识的痴迷和狂热之中,误以为学好一门语言的高深语法,就可以轻松步入编程高手的行列。这就象一位想学会写作文的学生,只热衷于冷僻汉字和高深语法,而置更为重要的通过认真学习消化课文来积累写作思路和技巧(包括大量造句和写作文)于不顾那么可笑!必须将编程思路的学习领会和积累,放在编程学习中的首要位置。以理想厨房为“纲”,以程序如何运行、如何构思、如何编写为“目”,把计算机科学中为了真正学会编程必须掌握的、几乎所有的、基础的、精华的知识有机的串联起来。在本书中仅仅做了这样一件事。一书在手,“软(指软件)硬(指硬件)兼施、内(指编程思想)外(指语言语法)兼修”,让读者可以真正全方位学习编程,并且真正学会编程,这是本书的写作目标。所以,本书中的知识讲解比较密集、浓缩,尤其是第一章和第二章。读者要作好心理上的准备,你是否愿意付出艰苦努力,去认真学习本书,出版社和作者是否值得信任。读者不要以为真正学会编程只是计算机专业学生的事。任何一位当代社会的理工类大学生,都必须具备一定的编程能力,能够在未来的科技工作中把计算机做为自己的得力助手和亲密伙伴,否则你就不是当代社会的合格科技人材。当代科技工作者不会编程,就象古代战士不会射箭一样。何勤初学者阅读本书,建议从第41页开始
3第一部分:计算机原理(初)第0章"理想厨房”的工作原理0.1理想厨房系统0.2理想厨房系统的一个炒菜实例0.3“理想厨房”工作的重要特点0.4理想厨房系统与计算机系统术语对照表第1章计算机的基本工作原理1.1二进制简介1.1.1二进制与二进制数的基本概念1.1.2与二进制相关的术语:位、位串、字节1.1.3数和码的含义与区别1.2计算机系统1.2.1计算机系统中的硬件1.2.2计算机系统中的软件1.2.3计算机指令能做的工作1.3提高部分131程序(指令序列)在硬件上的运行过程1.3.1结构化、规范化的机器语言程序1.3.2各种数制之间的转换第二部分(C语言基础)第2章C语言程序结构和基本语法要素第3章顺序结构程序设计3.1语句执行的顺序性3.2用计算机求解问题的步骤3.3逐步求精的伪代码3.4验证算法的方法3.5赋值表达式和多重赋值3.6变量类型的进一步讨论3.7各种类型的常量3.8不同类型数据之间的类型转换3.9常见编程错误3.10提高部分3.10.1机内形式的整数3.10.2二进制浮点数在计算机中的表示方法第4章选择结构程序设计
44.1两种基本的if语句4.2布尔表达式之一:关系表达式4.3空语句4.4复合语句4.5if语句的嵌套及其用法4.6布尔表达式之二:逻辑表达式4.7一种特殊的多重嵌套if语句——多分支选择结构语句4.8switch语句4.9选择结构常见错误4.10提高部分4.10.1其他表达式作为布尔表达式使用4.10.2条件运算符4.10.3逻辑表达式的短路运算第5章循环结构程序设计5.1while语句5.2自增、自减运算符和表达式的副作用5.3do-while循环语句5.4for语句5.5复合赋值运算符和逗号表达式5.6break语句和continue语句5.7循环语句的嵌套5.8常见错误5.9提高部分第6章数组6.1引言6.2一维数组6.2.1一维数组的定义6.2.2下标变量(数组元素)6.2.3下标和下标表达式6.2.4动态下标变量6.2.5下标和下标表达式的允许取值范围6.2.6数组元素在内存中的相对位置6.2.7数组元素的初始化6.2.8下标变量的存取6.3一维字符数组和字符串6.3.1一维字符数组的定义6.3.2单个字符的输入输出库函数6.4二维数组6.5编程综合练习第7章函数7.1引言7.2函数的概念
57.1函数编写的一些重要原则7.2使用数组(或数组元素)作为函数参数7.3函数的嵌套与递归7.4有关函数定义、返回、声明、调用的进一步说明7.4.1函数定义7.4.2return语句与函数类型7.4.3函数声明与函数原型7.4.4函数调用7.4.5函数的形式参数与实际参数7.5提高部分第8章指针8.1引言8.2指针变量的定义、初始化和应用8.2.1指针变量的定义8.2.2指针变量的初始化8.2.3指针赋值8.2.4间接寻址运算符8.2.5指针变量作为函数的形式参数和实际参数8.2.6指针作为函数调用的返回值8.3指向数组的指针以及相关的运算8.3.1指针变量指向数组元素8.3.2数组名用作指针(常量)8.4提高部分第9章C语言进阶9.1结构9.1.1结构类型的定义9.1.2定义结构变量9.1.3结构变量的初始化和访问(输入/输出和存取)9.1.4结构数组和结构指针的定义、初始化以及访问方式9.1.5用typcdef定义类型的别名9.2编译预处理9.2.1#include命令9.2.2#define命令9.2.3条件编译指令9.3文件、流和输入/输出9.3.1概述9.3.2文件和流的概念9.3.3文件的两种形式9.3.4文件的输入和输出
69.1提高部分9.1.1链表(单链表)9.1.2位运算9.1.3枚举类型9.1.4文件流的本质第三部分算法与数据结构简介第四部分利用ege图形库函数的游戏编程案例第五部分计算机原理和操作系统简介第11章编程原理进阶11.1引言11.2输入/输出设备和输入/输出接口电路11.3内存与外存11.4中断和操作系统11.4.1操作系统工作的机制11.4.2操作系统的特点11.5提高部分H.5..1计算机为何使用二进制数字信号?11.5.2模拟图像和声音的数字化编码过程附录Aege图形库函数使用说明和库函数列表附录B常用字符与ASCII码对照表附录B常用C语言库函数附录C运算符的优先级和结合性附录D格式化输入输出库函数的用法小结参考文献
7第零章理想厨房的工作原理一种有着神奇的“魔力”和“智能”的人造设备,正在迅速地、彻底地、默默无闻或者令人震惊地改变和丰富我们所生活的大千世界。这个看起来很不起眼的,在少数场合被称为“电脑”的电器设备,是如何具有如此神奇的“魔力”和“智能”的?本章和下一章将带你开始解开这个与我们的生活和工作息息相关的当代社会最大的谜。0.1节介绍理想厨房系统,0.2节通过一个炒菜实例,讲解理想厨房各部件是如何密切配合工作的。0.3是一张理想厨房系统与计算机系统的对照表。计算机从发明到现在不过70年左右的时间,然而计算机的发明、改进和普及,把人类带进了智能时代。计算机本身也变得越来越复杂、快速、小巧、种类繁多。但大多数计算机都遵循冯.诺伊曼体系结构,这为我们理解计算机的基本工作原理提供了方便。从某种角度来看,计算机就是一种人造智能生命。想要真正学会编程,通过编写的程序命令计算机工作,就必须懂得计算机的基本工作原理。就像人们要与某种具有智能的其他物种个体进行交流通信,就必须对那个物种的习性有一个基本了解一样。本章和下一章是全书的重要基础。通过这两章,读者可以了解计算机的工作过程。这些知识对学习程序设计非常有帮助。直接学习计算机工作原理是极其枯燥乏味、很困难的,因为有大量的新名词。为此,笔者付出了极大的努力,找到了一种比较好的类比方法——理想厨房系统,通过这个例子就可以初步了解计算机的基本工作原理。0.1理想厨房系统:理想厨房系统,是一个通过顺序执行菜谱中的各个加工步骤,把原材料加工成菜肴的系统。它由硬件和软件(菜谱)组成。I)软件部分:菜谱是理想厨房系统中的一个无重量、无体积、不会损坏、但可以经常更换的极为重要的“软件”部件。菜谱由一个个的加工步骤顺序组成。每个加工步骤命令理想厨房系统完成一个基本操作(比如炒、蒸、煮、输入一种原材料等)。注意:为了解说简洁起见,在以下叙述中,我们经常把菜谱中的一个“加工步骤”称为一条“指令:。因为一个加工步骤就是一条指导理想厨房如何工作的命令。2)硬件部分:理想厨房系统,主要由以下四个“硬件”(即实物)部件构成——理想厨房、自动冰箱、输入输出设备(即配菜员和传菜生)和三条传送带。需要注意的是,理想厨房仅仅只是理想厨房系统中的一个重要组成部分。理想厨房系统的构成简图如图0」所示:
8自动冰箱理想国房0.1理想厨房系统运行示意图:0.1理想厨房系统示意图:・自动冰箱:自动冰箱负责临时保存菜谱、原材料和菜肴。它由非常多的(比如几百个)大小一样的格子组成,每个格子都有一个唯一固定编号,这个编号称为地址。地址是从0开始逐一递增的。是不是感到很奇怪:菜谱也要保存在冰箱中,这样做的道理请看本章后面。每份原材料和菜谱中的每个加工步骤,都占据冰箱中的一个格子。・理想厨房:功能:负责根据从冰箱的菜谱中取到的加工步骤,进行炒菜以及进行相关的控制工作。构成:理想厨房主要由厨房管理员、厨师、炊具和一些碟子组成,参见图0.1。•理想厨房中的各种碟子理想厨房中有一些起着重要作用的碟子:一个PC碟(又称为指令地址存放碟):此碟中存放一个非负整数值,这个值是一个地址;它指明将要执行的指令位于自动冰箱的哪一格中。一个IR碟(又称为指令存放碟):用来存放刚刚从冰箱中取过来的一条立即要执行的指令。理想厨房中还有若干个通用碟(图0」中标有名称RO、RI、R2的碟):用来临时存放从冰箱中取来的原材料或经过加工了的半成品或成品。这是由于到冰箱格子中存取物品,要比到通用碟慢得多的缘故。•指令执行的全过程理想厨房每次只能按顺序执行菜谱中的一条指令。理想厨房执行指令的流程完全是周期性的,即任意一条指令都是按照“取指令——阅读分析指令——执行指令”这三个阶段进行的。
9厨房管理员首先根据PC碟中的值,通过三条传送带的协调工作(三条传送带如何协调工作的细节,请参见下一节),到自动冰箱的指定格中去取菜谱中的一条指令。取到理想厨房并把它存放到IR碟中之后,PC碟中的值将会加上1——这是为取下一条指令预先做好准备。然后,厨房管理员阅读并分析瓜碟中刚取到的这一条指令,根据该指令的指示,去做以下六类工作中的一种:1.取物品:通过三套传送带,畲令自动冰箱把指定地址格子中的(炒菜加工步躲马上要用到的)原材料(通过材料传送带)传送到理想厨房中来;2.加工:命令厨师按照指令的要求,对原材料作一个基本加工操作(做‘炒",“蒸”,“煮”等基本操作步骤中的某一个动作);3.存物品:通过向三套传送带向自动冰箱发命令,把某个碟子或炊具中的成品(或半成品)送回到冰箱指定的格子中存放;4.在厨房内部进行物品传送:在厨房的各个碟子和炊具之间传送原料或半成品;5.输入:命令配菜员为某道菜临时配备原材料;(在本章不作讨论)。6.输出:命令传菜生将炒好的菜送给顾客;(在本章不作讨论)。一条指令执行完后,理想厨房立即自动进行下一个完全类似的、新的“取指令——阅读分析指令——执行指令”的工作。下面我们通过一个实例来讲述理想厨房系统的工作机制。这是本章的一个重点,因为计算机的工作原理,与之极其相似。0.2理想厨房系统的一个炒菜实例在本节中我们通过炒制一道青菜的例子,来说明理想厨房系统的工作全过程。首先,把青菜放在冰箱地址为5的格子中,冰箱地址为6号的格子,预留给炒好的菜使用。地址。的格子中:地址1的格子中:地址2的格子中:菜谱的所有加工步骤(又称为指令)从冰箱地址0号格开始依次按照顺序存放,编写炒青菜的菜谱如下:取地址5(中的物品)到RO碟;将RO(倒入炒锅中)炒好后装到R1碟;存R1碟(中的物品)到地址6中;可见,此菜谱一共有3个加工步骤。开始时理想厨房系统状态如下图0.3(注意:冰箱格子以及理想厨房碟子中存放的物品都用了斜体字)理想厨房自动冰箱碟名碟中物品地址冰箱格子中物品
10R0R1厨具R2厨师PC0厨房管理员IR材料传送带地址传送带01234567取地址5至!!R0碟将RO炒好后装到R1碟;存R1碟到地址6中;青菜0控制传送带取图0.3菜谱和原料安放完毕后,启动理想厨房系统,开始自动化的工作。I)厨房管理员根据PC碟子中的数字知道要到地址为0的格子中取第一条指令(即加工步骤)。于是,厨房管理员向控制传送带上发出一个“取”信号,然后马上将PC碟中的数字复制后放到地址传送带上。这两个信号都会到达冰箱。冰箱收到这两个信号后(知道理想厨房想要得到第0格中的物品,于是自动冰箱)将0号格的内容“取地址5到ROit复制一份,将其放到材料传送带上,送往理想厨房。理想厨房收到后,将这条指令放到IR碟中。然后,厨房管理员将PC碟中的原来值增加1,以便为取下一条指令做好准备。取指令工作完成后,理想厨房系统处于如下图0.4状态:碟名理想厨房自动冰箱冰箱格子中物品碟中物品地址R0材料传送带0取地址5至!]R0碟R1炊具1将RO炒好后装到R1碟;R2厨师2存R1碟到地址6中;地址传送带3PC7厨房管理员45青菜IR取地址5到RO碟控制传送带67图0.4指令执行阶段:厨房管理员读到指令存放碟(即小碟)中的加工步骤后,知道要到冰箱地址为5的格子中去取物品,并且要放到R0碟中。因此,管理员向控制传送带上送出一个“取"信号,然后马上将5这个数字放到地址传送带上。冰箱知道理想厨房要取5格中的物品,于是冰箱将地址为5的格子中的物品“膏若取出来,放到材料传送带上。材料传送带上的物品“春若,传到理想厨房后,按照指令的要求(通过理想厨房内部的传送带)送到了R0碟中。理想厨房碟名碟中物品第一条指令执行完后,理想厨房系统处于如下图0.5所示的状态:自动冰箱地址冰箱格子中物品
11R0青菜R1炊具R2厨师PC1厨房管理员IR取地址5到R0碟材料传送带地址传送带01234567取地址5到R0碟将R0炒好后装到R1碟;存R1碟到地址6中;青菜5控制传送带取图0.52)接下来,开始下一条指令的取指令阶段。完全类似于前一条指令,在取指令阶段完成后,理想厨房系统处于如下图0.6状态:理想厨房自动冰箱碟名碟中物品地址冰箱格子中物品R0青菜(原料)R1炊具R2厨师PC2厨房管理员IR将R0炒好后装到R1碟;材料传送带01234567取地址5到R0碟将RO炒好后装到R1碟;存R1碟到地址6中;青菜地址传送带1控制传送带取图0.6指令执行阶段:管理员阅读并分析指令存放碟中的指令后,命令厨师将R0碟中的物品倒入锅中炒好后装到R1碟。第二条指令执行完后,理想厨房系统处于如下图0.7状态:理想厨房自动冰箱碟名碟中物品地址冰箱格子中物品R0青菜R1熟青菜炊具R2厨师PC2厨房管理员IR将RO炒好后装到R1碟;材料传送带01234567取地址5到R0碟将R0炒好后装到R1碟;存R1碟到地址6中;青菜地址传送带控制传送带图0.73)同理,在第3条指令的取指令阶段完成后,理想厨房系统处于如下图0.8状态:自动冰箱理想厨房
12碟名碟中物品地址冰箱格子中物品加)生青菜材料传送带0取地址5到R0碟R1黑膏兼炊具1将RO炒好后装到R1碟;R2厨师2存R1碟到地址6中;地址传送带345青菜PC3厨房管理员控制传送带6IR存火/碟到地址o中7图0.8指令执行阶段:下面开始执行“存R1碟到地址6中”送条埼令。厨房管理员分析指令存放碟中的加工步骤后,知道要将R1碟中的物品,送到冰箱地址为6的格子中去存放。于是,管理员向控制传送带上发一个“存”信号,然后马上将6这个数放到地址传送带上;最后,将R1碟中的物品“熟青菜”放到材料传送带上,送往冰箱。冰箱收到两个来自理想厨房的信号后,知道理想厨房要存放物品到6格中;于是自动冰箱(的机械手)在材料传送带旁,等待从理想厨房R1碟传来物品——“熟青菜”、一旦到达,自动冰箱就将其取下,并将其存放到地址为6的格子中。完成后系统状态如图0.9:理想厨房自动冰箱01234567取地址5至0R0碟将R0炒好后装到R1碟;存R1碟到地址6中;青菜熟青菜地址冰箱格子中物品碟名碟中物品R0青菜R1黑点乘炊具R2厨师材料传送带地址传送带PC3厨房管理员IR存R7碟到地址6中;6控制传送带图0.9到此为止,炒青菜这道菜终于大功告成。0.3理想厨房工作的重要特点现在,我们对刚学到的重点知识,做一个小结:1、顺序性和周期性顺序性:理想厨房每次都只能取得和执行一条指令;一条地址为i中的指令执行完毕后,才能顺序执行地址为i+1中的指令。周期性:厨房管理员的工作完全是周期性的,即他永远在做:(命令各部件)取指令T阅读分析指令T发出控制命令要求各部件执行指令(简称为取指一译码一执行)这一周期性的动作。只要一启动,理想厨房就永远按照这个周期性的动作,一条一条的顺序地取指令并且执行指令,…这样不停地、不知疲倦地快速运行着,直到执行了一条“停止运行"指令或发生严重故障时为止。
132、有限和无限有限:厨师会做的各种不同基本加工操作所构成的集合是固定有限的(炒、煎、蒸、煮、烤等几十种),也就是说厨师学不会任何一种新的基本加工操作。厨房管理员能看懂的各种不同种类加工步骤所构成的集合也是固定有限的(从冰箱取物品、存物品到冰箱、厨师的各种不同加工方式、配菜员输入原材料到冰箱或厨房、传菜生输出菜肴给顾客等)。无限:然而,人们可以为理想厨房编写出来的菜谱数量是无限制的。因此,理想厨房可以炒出菜的品种总数也是没有限制的。3、智者与白痴理想厨房中的厨师和厨房管理员都是不知疲倦的、机械化的“白痴”,在厨师或厨房管理员的“大脑”中没有任何一道菜的(全部或一部分)加工步骤。加工制作各种菜肴的智慧(即蕴含在加工步骤中的智慧)都是来自于存放在自动冰箱中的菜谱,也就是来自于菜谱的编写者。正是由人们编写出来的可以让理想厨房执行的各种各样的菜谱,才使得原本白痴般的、能力极为有限然而速度却极快的理想厨房系统在炒菜方面显得似乎无所不能!4、主动与被动:在指令执行的三个阶段中,取指令和分析阅读指令是硬件主功进行的,而在指令的执行阶段,硬件是在软件(即菜谱中的加工步骤)的命令下褛动进行的。5、不变与可变:一个理想厨房系统的硬件组成和结构是不变的,而存放在它的冰箱中的软件(菜谱)却是可以随着客户需要而随时加以改变的;同一个菜谱(菜谱不变)随着加工处理的原材料的种类的不同(原材料可变),可以得到不同的菜肴(菜肴可变)。比如:情炒青菜的菜谱同样可以用来清炒韭菜,只要把生韭菜放到原来放生青菜的指定格中。一条指令执行的前两个阶段(取指令、阅读分析指令),参与工作的硬件部件是不变的;而在指令的执行阶段,随着指令类型的不同参与工作的硬件部件是可变的。6、两个中心厨房管理员是执行指令的控制中心;厨师(加上炊具)是原材料的加工中心。理想厨房系统的工作原理,到此已经全部介绍完毕。在下一章你将看到:理想厨房的工作原理,与计算机的工作原理是极为类似的。因此在本书中,从整体上把握计算机的基本工作原理,就变成为一个比较轻松的名词替换的小游戏。0.4理想厨房系统与计算机系统术语对照表下面给出两个系统之间的术语对照表,见表0.1。表0.1术语对照表理想厨房系统电子数字计算机系统(简称计算机系统)1.硬件设备
14自动冰箱(包含很多大小相等的格子)(冰箱中的)一个格子内存(又称为主存,包含很多容量相等的基本存储单元)(内存中的)一个基本存储单元材料传送带数据总线地址传送带地址总线控制信号传送带控制总线理想厨房(包含以下设备)CPU或称为微处理器(包含以下部件)厨师及炊具算术逻辑单元ALU(又称为运算器)厨房管理员控制单元(又称为控制器)通用碟通用寄存器(或数据寄存器)指令地址存放碟PC指令地址寄存器PC(又称为程序计数器)指令存放碟IR指令寄存器限状态存放碟程序状态字寄存器PSW(又称为标志寄存器)采购员及配菜员输入设备(键盘、鼠标、网卡、U盘等)传菜生输出设备(显示器、打印机、网卡、U盘等)自动仓库外存(硬盘、U盘)2.软件与硬件之间的接口(编写菜谱或程序的基本要素)冰箱地址(即格子的编号)内存地址(即基本存储单元的编号)厨师可做的各种炒菜基本动作算术逻辑单元可进行的各种基本运算碟子的名称寄存器的编号理想厨房可以执行的所有不同加工动作该类型计算机的指令集3.软件特殊菜谱(机器语言形式的)程序加工步骤指令原材料数据炒好的菜信息(或称为结果)精确的普通菜谱高级语言源程序(又称为源代码)简要的普通菜谱伪代码4.系统的使用者编写特殊菜谱者用机器语言编程的程序员编写精确的普通菜谱者用高级程序设计语言编程的程序员理想厨房系统的大堂经理和顾客计算机的用户以上表中这些与计算机相关的术语,将在1.2节进行讲解。与在理想厨房系统上运行一个菜谱极为类似,在电子计算机上运行一个程序时,上表中列出的计算机的各个部件也会协同工作,完成程序给定的任务(参见1.2节)。学习了理想
15厨房这个例子,理解计算机原理就变得非常容易了。0.5本章习题1、在取一条指令到理想厨房的过程中,哪些部件会依次参与工作?哪个部件在此过程中起着核心控制作用?2、取一份原材料的工作过程与取一条指令的工作过程有什么区别?3、考虑一下为理想厨房编写的菜谱与给普通人看的菜谱有何不同点?4、编写一个香菇炒青菜的菜谱。5、写出三条传送带各自的职责。哪传送带是可以双向传递的?在取指令时,材料传送带是双向的还是单向的?
16第一章二进制的数和码——捷径有时候是一条弯路。——万事开头难。计算机能够“理解和懂得”的语言是二进制机器语言,计算机能够直接加工处理的,都是二进制的数和码。本章1.1对二进制进行了简介,其中最重要的概念是:字节、二进制的数和码;世界上的各种事物如何通过编码用二进制位串来表示(或近似表示)。12对计算机的基本构成成分、机器语言、计算机的基本工作原理进行了简介。L1二进制简介为了从整体上把握计算机的基本工作原理,并为后面的编程学习奠定扎实的基础,读者必须事先对数字信号、二进制及其相关知识有一个比较清晰的、整体的简明了解。以下进行简要介绍。1.1.1二进制与二进制数的基本概念十进制数所对应的二进制数(所对应的十六进制数)0001112102311341004510156110671117810008910019101010A(或a)111011B(或b)121100C(或c)131101D(或d)141110E(或e)151111F(或f)161000010表1.1部分十进制数与二进制数(和十六进制数)数值对照表二进制就是只能用数字“0”和“1”来进行计数的数字系统。二进制加法运算的重要规则是:1+1=10,即两个1相加,就产生了向高位的进位——即“逢二进一”(做减法时则是“借一当二”),类似于十进制中的“逢十进一”(做减法时则是“借一当十”)o其它二进制加法规则更简单:0+0=0、0+1=1、1+0=1o
17与十进制数类似,在一个二进制数中,靠左边的数字是高位数,靠右边的数字是低位数,其中最左边的位称为最高位。我们经常用一对圆括号括住一个数值,并在圆括号外的右下角加一个整数下标,用此下标来表示该数值是几进制的(但是对于十进制数一般不用圆括号和下标)。比如:(1011)16是一个十六进制数;而(1011)2是一个二进制数。1.1.1二进制数的多项式展开表示一个十进制整数,其数值可用以下多项式展开来表示:比如37853785=3X103+7X102+8X10'+5X10°(1)我们把(1)式中10的几次方称为权重,权重左边的乘数称为系数。(1)式中共有4个系数,从左到右依次是:"3"、“7”、“8”、“5”,权重依次分别是103、102、10:10%可见,用这种记数法表示数值时,越左边的系数,所对应的权重越大,所以就越重要。权重中的基数(即底)与表示该数的进制是一样大的,在十进制数中都是10。类似的,任意一个二进制整数,其数值也可用多项展开式来表示:比如二进制整数1011(1011)2=1X23+0X22+1X2如果完全用二进制展开表示,则应当是:(1011)2=1X10"+0X10lo+lX10'+1X100,系数和权重都用二进制表示,但人们一般仅将系数用二进制表示。+1X2°(2)1(2)式中系数从左到右依次是:“1”、“0”、“1"、"1",而权重依次分别是23、22、2,、2。(注意:这里的权重是用十进制来表示的。如果权重也用二进制,则应当分别是二进制的10"、10,°,10\100)。展开式中系数的最大值一定比进制数小1,比如:在十进制记数系统中,系数的最大值是9,而在在二进制记数系统中,系数的最大值是1。1.2二进制相关术语简介:位、位串、字节下面,我们来熟悉一些与书写、存储或传输一串二进制数字有关的术语。121位(bit):书写、存储或传输单个二进制数字,我们将其称为"位"(bit)或“比特”。单个“位”中的二进制数字不是0就是1,再没有别的可能数字。任何一个只能处在两种不同稳定状态的电子元件(触发器、电容等)或者某种均匀介质(比如覆盖着磁性颗粒的金属圆盘)表面上的一个小点,都可以用来(间接)表示和存储二进制的一个“位:如果用电容充满电状态表示二进制数"1",就可以用电容放完电的状态来表示二进制数“0”。122位串及其长度:多个二进制数字顺序排列在一起,称之为“位串”(有些教科书称为位模式)。位串中含有的数字总个数称为位串的长度。比如:位串100110的长度是6。处于位串左边的位称为高位,处于位串右边的位是低位。位于位串最左边的位,称为最高位。123度量位串长度的基本单位——字节(Byte)“位”这个二进制最小度量单位太小了,通常用起来很不方便。现代的绝大多数计算机和一些数字处理设备,大多数是以长度为8的位串——字节——来作为度量(部件或设备的)数据存储容量大小的一种基本单位。把长度为8的一个位串称作为一个字节,长度为16的一个位串称为2个字节等等。长度为n的位串,一共有n/8个字节。也就是说,一个位串的长度,既可以用位串中的总位数来度量,也可以用位串所具有的字节数来度量。
181.2.4二进制数据存储和传输的一些其它常用单位KB、MB和GB字节(Byte)这个基本单位虽然大小是位(bit)这个最小二进制单位的8倍,但是在很多场合仍然还是显得太小。更大的常用单位有(在以下叙述和各种资料、书籍中,经常用字符B来代表术语字节——Byte):千字节:1KB=1024B=210B兆字节:1MB=1024KB=2"'KB=Z'B=1048576B(约为一百零五万字节)吉字节:168=1024\^=2"忖8=2叼=1073742814B(约十亿七千多万字节)需要引起注意的是:相邻单位之间,都是1024倍的关系,而不是1000倍的关系。125位串的通常传输方式——并行或串行:一个长度为n的位串,既可以用n根并排导线同时进行传输,每根导线传输一个位——即并行传输(这种传输方式速度很快,但要用多根导线);也可以只用一根导线,分为n个相等时间段,一位一位地依次先后进行传输——即串行传输(这种传输方式速度较慢,但只要用一根导线)。在导线中如果用直流电的高电平(即有电流通过)来传输1,就可以用低电平(即无电流通过)来传输0。在计算机内部全部使用二进制的数或码,但由于二进制通常太长不好书写,人们在编程和将数据输入计算机时,还是喜欢用十进制(或者十六进制和八进制)。因此,我们必须对用各种进制表示的数如何转化成别的进制数有一个基本了解。1.3各种进制数之间的转换1.3.1二进制整数转化成十进制整数任意一个二进制整数,其数值可用以下展开式来表示:比如二进制数整1011(1011)2=1X23+0X22+lX2'+1X20(2)此二进制数的值,等于十进制的1X8+0X4+1X2+1X1=8+2+1=11(3)由此可以得到二进制整数转化成十进制整数的一般方法:只要将一个二进制整数(比如1011)展开后的(2)式中的每一位的系数值(采用十进制乘法规则)乘以这一位转化成十进制数后的权重(即2的几次方),然后再将乘积项的数值(采用十进制加法规则)逐个相加起来即可。132任意R进制实数(或整数)的表示法一般情况下,任何一个R进制去软,都可以紧凑地表示为:(±rnT^'--rtro.rlr.2...r.m)R(1.3)(在计算科学中,R通常是2,8,10,16中的某一个正整数)其中的任何一个(系数)r,都是位于0到R-1之间的一个正整数,r°是个位数,口是小数点后的第一位数,rn是最高位数。是小数点后的最低位数。如果小数点后的所有(系数)r.nr.2、…、r*等都为0,则(1.3)表示一个整数。将任意一个R进制数扩大R倍(即乘以R),只需将小数点右移一位即可;类似地,将任意一个R进制数缩小R倍(即除以R),只需将小数点左移一位即可。其中R等于10是人们最为熟悉的十进制数,这种(可以带小数点的)十进制数的简洁紧凑表示方法,是由古代印度人发明的(来源于人有十个手指)。任何一个R进制实数,也都可以用多项式展开表示为:+(rnXR+rn.tXR1-'+■■■+nXR'+r0XR1+r^XR'+r.2XR2+---+r.mXRm)代称为系数,K称为权重)在R进制数值的多项式展开表示法中,不使用小数点。人们最为熟悉的仍然是R等于10时的多项式展开式。
19133将十进制整数转化成二进制数把一个十进制转整数(比如89)换成二进制,只需要用新的基数2(采用十进制除法规则)除以这个十进制数(89),余数(1)是结果左边的下一位数字,商(44)是新的被除数,…然后重复这个过程,直到商为0时终止。短除法就是按照这个转换原理,把要转换的十进制整数不断的除以2,然后取余数,商作为新的被除数,直到商为0的时候结束。然后把余数倒着写出来。例如:把89转换成二进制数2|891-2440222021112512202]01即:89=02延伸与拓展:转换规则的技术内幕要想明白这种转换规则的道理,请考虑十进制整数与它等值的二进制整数的方程式:89=(rnX2T+---+r2X^+r,X?+r0X^)2其中的二进制系数r”〃等等应该如何求得?可将该方程式的左边的十进制整数整除2,得到商44和余数1;与此同时将方程式的右边的二进制整数的小数点左移一位(等价于整除2)。这样方程式右边将得到rnX2°-,4---+r2X2,+riX2°+r()X2T;显然,商44应当与二进制整数为X2-1+…+0X0+riX2°相等;而89整除2所得到的余数1(除以2的余数不是0就是1)就等于方程右边的系数力(系数功不是。就是1)。同理,从方程式44=rnX2'l+-+r2X2'+rlX2f,,我们可以用完全相同的方式得到系数”的值,……,转换工作直到方程左边的商为0时结束。注意:短除法也完全适用于将一个十进制整数转换为一个任意R(R>2)进制的整数,只需将除数由2替换为R即可。1.3.4将十进制小数转化成二进制小数10进制小数转换为2进制小数的转换过程,与整数的进制转换过程有些类似而计算方
20法又恰恰相反:不是用新的基数2除这个数,而是用新基数2去乘它。乘法的进位(进到个位的数字)将成为答案右边的下一位数字,乘法结果中的小数部分将成为新的被乘数,整个过程直到乘法结果中的小数部分为0时终止。=0.750=1.50=1.0〃进位0,小数点后的第一位〃进位1,小数点后的第二位〃进位1,小数点后的第三位例如:把十进制小数0.375转换成二进制小数:0.375X20.75X20.5X2所以,(O.375),o=(O.O11)2延伸与拓展:转换规则的技术内幕要想明白这种转换规则的道理,请考虑十进制小数与它等值二进制小数的方程式:0.735=(0.jQ…其中的二进制系数口、Q等等应该如何求得。可将该方程式的左边乘以十进制的2,与此同时将方程式的右边的小数点右移一位。这样方程式左边将得到一个整数值1,这个整数值1与二进制小数最左边的系数格相等。去掉整数部分后,方程式两边剩下的小数部分的数值仍然是相等的,即0.47=0.q幻…Q.。注意:这个规则也完全适用于将一个十进制小数转换为一个任意R(R>2)进制的小数,只需将乘数由2替换为R即可。1.3.4将十进制实数转化成二进制实数一个十进制实数,只要将它的整数部分和小数部分分别转化成二进制数,然后将其合起来即可。1.3.5二进制整数转换成十六进制整数虽然计算机用二进制存储和传输各种数据,但是它并不适合在计算机外部(比如在源程序中)表示数据。因为与十进制数据相比,二进制数据(即位串)过于长了。然而,十进制又不像二进制那样能够直接显示存储在计算机中的是什么。在二进制位串和十进制数字之间不存在明显的关系,它们之间的转换很不方便。为此,人们想到了使用十六进制(或八进制)。在计算机外部用十六进制来表示计算机内部的二进制数据。由于二进制和十六进制数之间的转换非常快捷,这样就能比较好的解决以上难题。十六进制数用16个符号来表示一个数。字符集是{0,l,2,3,4,5,6,7,8,9,A,B,C,D»E,F}.注意符号A,B,C,D,E,F(也可用小写)的值分别等于十进制的10,11,12,13,14,15。表1.4是二进制、八进制、十六进制整数对照表。利用此表就能很快实现二进制和十六进制数之间的转换。二进制八进制十六进制二进制八进制十六进制000100010811110011191022101012A(或a)1133101113B(或b)10044110014c(或C)10155110115D(或d)11066111016E(或e)11177111117F(或f)表1.4二进制、八进制、十六进制整数对照表下面介绍二进制整数与十六进制整数(或八进制)之间是如何转换的。一个二进制数,比如:
21(1011110)2=1X26+OX25+1X24+1X23+1X22+lX21+0X2°〃展开=(1X22+0X2'+1X2°)X24+(1X23+lX22+lX2'+0X20)X2。〃从低位开始每4个分为一组=(1O1)2X24+(111O)2X2°=5X16'+EX16°〃将圆括号中的二进制数经过查表1.4转换为16进制数=(5E)16也就是说,只要将一个二进制数,从最低位开始,每4个二进制的位直接通过查表1.4,就可以快速转变成十六进制的1位数。1011110〃从最低位开始每4位二进制分为1组II5E〃查找1.4表直接得到十六进制数由此可见,二进制整数转换成十六进制整数是很方便快捷的,只要直接查找表14即可,不需任何转换计算。由此也可见,一个较长的二进制数转换成十六进制数后确实简短多了。二进制整数转换成八进制整数与此完全类似,二进制与八进制之间的转换只需使用表1.4的前两列。1.3.7十六进制整数转换成二进制整数由于上述二进制整数转换成十六进制整数的整个过程完全是可逆的。所以,完全可以通过查表1.4,将一个十六进制转化成二进制。例如十六进制(E5):通过查表十六进制的E对应1110,十六进制的5对应0101。注意:一个十六进制数字要转化成四位二进制数字,不足四位要补成四位;比如:十六进制5对应着二进制0101而不是对应着二进制101。所以,十六进制数:E51I二进制数:11100101结果为:(E5)16=(11100101)21.4二进制的数和码如果计算机仅仅只能够对一些数值进行运算——在计算机刚发明的早期年代,确实就是如此——那么它的应用范围就必然很狭窄。然而,现代计算机的应用范围却是极其广泛的,几乎深入到生产生活的方方面面。其根本原因在于:现代计算机不仅能够对“数值”进行运算,还能够对间接表示世界上各种各样事物(或状态)的“码”进行不同的处理。也就是说,不仅可以直接用一个二进制位串表示一个无符号的整数,通过各种各样的编码规则还可以用二进制位串表示(或近似表示)文字、图像、声音等世界上的几乎各种事物。所以,我们想要真正懂得计算机并且学好编程,就不仅要熟悉二进制的“数”,还必须对一些常见的二进制的“码”有一个比较清晰的整体了解。以下这些内容虽然比较繁琐,然而理解起来却并不困难。・十进制的数和码:我们先来看一看十进制数字系统中数与码的区别。如果3785用于表示数,则越高位的数字越重要(因为权重越大,在十进制数3785中,“3”的权重是103,而“5”的权重是10°);而3785用来表示非数值的码,则每一位数字都同样事要。码值仅相差一,
22所代表的意义就可以有巨大的区别。比如:3785可以代表汉字“大”,而3786可以代表汉字“小二对数值进行运算是很有必要的,然而对间接表示事物的码进行运算通常都是毫无意义的(除非码被用来间接表示数值)。虽然用十进制3785数字串只能直接表示摩:一个非负整数,这个数的值是三千七百八十五;然而同样一个十进制数字串“3785”,通过某种编码,可以表示的事物种类却是不眼制的:既可以表示码为3785的一个汉字,又可以表示任何别的什么一万个(编码从0000到9999)同类型事物中的码值为3785的一个特定事物。与十进制一样,二进制数与二进制码也有完全类似的区别。只不过在二进制中,只能用数字。和1组成的位串,来表达任何大小的数值或者表示具有任何含义的码。・一位二进制数和码如果用单个位来表示整数值,只能直接表示0和1这两个值中的一个。如果用单个"位”来表示码,则能够用来对任何(同属一种类型的)两种不同事物(或状态)制定编码规则。比如:用。表示"假",用1表示“真”;用。表示状态“关”,用1表示状态"开";用。表示“否”,用1表示"是"等等。{存,取}、{黑,白}、{读,写}、{阴,阳}、{上,下}、{好,坏}、{喜,怒}、{男,女}、{大,小}等等,都可用1位二进制码来表示。・两位二进制数和码如果用长度为2的一个位串来直接表示整数值,则只能够表示00(其值等于0)、01(其值等于1)、10、11这4个二进制非负整数值中的某一个。如果用长度为2的位串来进行编码,由于一共有00、01、10、11这4(即22)个码值可以使用,则能够用来对属于同一类型的4个不同的事物(或状态)制定编码规则。比如:{煎、炒、蒸、煮}、{加,减,乘,除}、{A,B,C,D}、{-2,-1,0,1}、{宿舍、教学楼、食堂、图书馆}、{前、后、左、右}等等,都可用长度为2的二进制码来间接表不O编码和解码的一个实例通过制定一个编码规则,比如:可以规定用00表示"D"、01表示"C"、10表示“B”、11表示"A",这就可以构成一张用4个码来表示4个字符的编码解码表【注1】,见表1.2o【注1】:如果把码00、01、10、11看成电视速配节目中4个未婚男士的代号,字符A、B、C、D看成4个未婚女士的代号。所谓的“编码解码表”,只不过是所有男士与所有女士之间的一张特定的、"皆大欢喜”的快速配对表而已。用严格的数学术语来讲,所谓制定编码规则,无非是制定了一张两个集合{00,01,10,11}与{A^CQ}之间的所有元素的一对一的映射表而已。二进制码字符00D01C10B11A表1.24个字符的一张编码解码表有了这张编码解码表,先通过对字符串"CAB”进行编号,就可以用一些码值构成的二进制位串
23“011110”,在二进制的数字信号处理设备中间接地存储和传输这个字符串。到达目的地后,接收方也要具有同样的一张“编码解码表”,才能将这种接收到的二进制位串,翻译成它的本来意义。比如将二进制位串011110翻译成字符串"CAB",这个过程称为解码。编码发送接收解码CAB>011110>011110>CAB图1.1字符串的编码、发送、接收、解码全过程・长度为n的二进制数和码长度为n的位串,可以表示的最大二进制非负整数111……1(一共n个1)究竟是多大呢?这很简单,将由n个1构成的此二进制数加上1,可得到1000(1的后面一共有n个0)o由这个数的多项式展开可知,它的大小就是2"。因此二进制整数111……1(一共n个1)的大小为21Mo因此,如果用长度为n的位串来直接表示一个非负整数,则可以表示的二进制数值从小到大依次是0、1、10、11、……、直到1111…11(一共n个1,其值等于2"-1),一共有2"个数。用长度为n的位串来进行编码,由于一共有2"个码值可以使用,则能够用来对任意的2"个同类事物所构成的集合制定一对一的编码规则。•世上的绝大多数事物都可通过编码来间接表示不添加任何别的非数字符号(正负号和小数点),用一个数字固定的位串,只能够直接表示一个非负整数(在计算机科学中,经常把非负整数称为无符号整数),有符号整数和实数都不能直接用位串来表示。但是,用位串作为码,可以间接表示的事物种类却是无限的。世上一切可数的事物(或状态)都可以用各种码来间接表示。幸运的是,世上大多数人类通常可以感知到的不可数的事物(比如图像、声音、实数等)都可以通过编码来近似表示。♦无效码(或称为非法码)在可使用的码比(要进行编码的)集合中的元素多的情况下,就会存在一些无效码,这些无效码不代表集合中的任何一个元素。究竟哪些是无效码,是由具体编码规则决定的。比如用长度为3的位串来对某个集合中的所有元素进行编码,可用的码值一共有8(即23)个,集合中如果只有5个元素,那么必然有(&5=)3个码是无效码。一般情况下,长度为n的位串一共有2。个码,如果用来对m个元素构成的集合进行编码,而且2n大于m,则总共有2Jm个无效码。♦不定长度编码还有一类码的长度不一样的编码方式,常常用比较短的码表示集合中经常出现的元素;比如哈夫曼编码就是一种不定长度的编码(关于吟夫曼编码请参见其它书籍或资料)。1.6表示字符的编码1.6.1ASCII码“目前计算机中用得最广泛的字符集及其编码,是由美国国家标准局(ANSI)制定的ASCII码(AmericanStandardCodeforInformationInterchange,美国标准信息交换码),它已被国际标准化组织(ISO)定为国际标准,称为ISO646标准。适用于所有拉丁文字字母,ASCII码有7位码和8位码(即扩展ASCII码)两种形式。7位ASCII码是用七位二进制数进行编码的,可以表示128个字符。
24第。〜32号以及第127号(一共34个)是控制字符或通讯专用字符,如控制符:LF(换行)、CR(回车)、FF(换页)、DEL(删除)、BEL(振铃)等;通讯专用字符:SOH(文头)、EOT(文尾)、ACK(确认)等;第33〜126号(共94个)是可打印的普通字符,其中第48〜57号为0〜9十个阿拉伯数字;65〜90号为26个大写英文字母,97〜122号为26个小写英文字母,其余为一些标点符号、运算符号等。注意:在计算机中,1个长度为7的ASCII码值通常采用1个字节来存储,其最高位可以用来作奇偶校验位。对于Unicode码、对于绝大计算机中用来表示有符号数的补码、表示有符号实数的编码(IEEE745标准)的讲解,对于在计算机中如何用码来表示(或近似表示)图像和声音,请参见章
25第二章计算机基本工作原理本章只介绍计算机在整体上是如何工作的,即讲解各部件是如何紧密配合完成执行指令这一工作的。更进一步的讲解请参见第章。2.1计算机系统2.1.1定义:计算机系统是一个通过运行程序,把数据加工成信息的二进制数字信号处理设备。这个定义有两点值得注意:第一、计算机内部使用的都是二进制数字信号;第二、除了运行程序把数据加工成信息外,计算机不能做任何其他事。对相关名词的解释:(1)程序一个程序2(相当于菜谱)是由完成某一任务的很多条指令(相当于加工步骤)顺序组成的。对程序和指令的进一步讲解参见本章后面。(2)数据和信息“数据”和“信息”这两个术语太基础了,两者都没有公认的明确定义。一般认为“数据”是对某个事物的真实记录(即对事物某个或某些方面的一种号去抽象);而“信息”则是对数据进行某种处理(比如计算、排序、查找、分析、综合、解题、思考等)后得到的正确结果。比如:到医院体检,只要体检设备正常、操作流程规范就可得到一批体检数据;这批数据经过医生(或者经过一个运行着专家诊断系统程序的计算机)的正确解读分析后,就可得到某人身体状况的各种信息。2此处所讲的••程序”实际上指的是可以直接命令计算机工作的机器语营程序:而不是本书其他各章将要讲解的高级语点源程序.机普语言程序将在本章后面进行讲解.对于同一批数据,由于处理目的不同,可以得到不同的信息。数据不够精确或不充足、
26对数据的处理方法不正确都可能得不到想要的信息。用不同数据运行同一个程序通常可以得到不同的信息。延伸与拓展:数据与信息关系的进一步讨论从一个数据处理任务中得到的信息又可以成为另一个更高级的数据任务中的数据。数据和信息都可以用数值、文字、图像、声音和视频的形式来记载和传送。因此,我们也可以说:信息是原始数据经过加工而得到的更有用的数据。“信息战”指的是用虚假数据来欺骗敌人,让敌人通过解析虚假数据得到自以为正确的信息;也可以直接用虚假信息来欺骗敌人,让敌人作出错误的决策。“数据挖掘”指的是从一批(或一大批)数据之中,挖掘出各种各样的有用信息。2.1.1组成:类似于理想厨房系统,计算机系统也是由(有重量、有体积的实物)里住和(无重量、无体积的抽像)软件这两大部分组成的。软件主要由程序和数据组成。程序(即指令序列)必须在硬件上得到运行,才能将(输入的)数据加工成信息(输出)。程序、数据和信息在计算机内部都是以二进制位串形式呈现的。计算机系统各要素之间的相互关系请看以下示意图2.1o数据(输入)-硬件:运行程序(指令序列)-(输出)信息图2.1计算机系统各要素之间相互关系示意图以下我们先粗线条地讲解计算机的硬件结构和功能,然后介绍指令和程序,最后讲述程序(指令序列)是如何在硬件上得到运行的。2.2计算机系统中的硬件计算机硬件主要由以下三种实物部件构成:内存——功能相当于自动冰箱;CPU——功能相当于理想厨房;外围设备——功能相当于配菜员和传菜生;计算机的组成结构简图请参见图2.2o键盘控制器打印机控制器监视器控制器磁盘控制器图2.2计算机系统组成结构示意图(此图要替换)在图2.2中,左上是CPU,右上是内存,最下部的一排都是外围设备。计算机这三大部件之间通常是通过总线连接起来的,每一条总线都是由多根并排导线组成。总线负责CPU、内存、外围设备之间的通信及数据传送。
27电流在导线中是以光速运动的(不过电信号每通过一个逻辑门要花费大约零点几纳秒时间),加上在计算机内部大量采用并行传输方式,这是计算机具有超快运行速度的根本原因。总线共有三条(其作用相当于理想厨房系统中的三条传送带),分别是数据总线(相当于材料传送带)、地址总线(相当于地址传送带)和控制总线(相当于控制传送带)。2.2.1内存一又称为主存储器(相当于自动冰箱):内存负责存放型序和程序运行所需要加工的数好(还可以临时存放运行程序所得到的信息)。内存主要由非常多的基本存储单元组成(随着计算机类型的不同,内存大约有几百到几十亿个基本存储单元)。每个基本存储单元通常既可以用来存放指令,又可以用来存放程序要加工的数据%每个基本存储单元都有一个唯一固定的编号,这个编号称为地址(或内存地址)。对于绝大多数现代计算机,每个基本存储单元中都只能存放长度为1个字节的位串,(极少数现代计算机和大多数早期计算机的一个基本存储单元的存储容量不是一个字节,而是一个字),参见图2.3。地址……)多个基本存储单元……10000000001000100001100100-TTj(图2.3内存片段示意图:由大量基本存储单元构成的内存)一个数据(或者一条指令)如果太长,一个基本存储单元存放不下,通常就要用内存地址连续的相邻几个基本存储单元——组成一个更大的内存单元——来存放。・多个内存芯片中的所有基本存储单元统一进行编址,所有基本存储单元构成了一个逻辑上统一的“内存空间”与单片计算机3不同,通用计算机的内存实际上是由多个处于不同位置的硅芯片组成的(对于个人计算机来说,有的内存芯片位于主板上,有的位于显卡、声卡和网卡上),所有这些内存芯片中的每个基本存储单元,不论它位于哪块内存芯片上,都只有一个唯一编号一一即地址。也就是说,如果一台计算机的所有内存芯片总共有n个基本存储单元,那么它们的内存地址通常就是从。到n-l。一台通用计算机中各个不同内存芯片上的所有基本存储单元由此构成了二个形式上(或3什么单片机,什么是通用计算机,请参见本章后面介绍称为逻辑上)统一的单一的内存室画。这使得计算机的CPU(和其他部件)能够以一种形式上完全一致的——即对各个内存芯片不加以区分的——方式“读取”这个内存空间中的任何一个内存单元中的数据(或者向这个内存空间中的任何一个内存单元中“写入”数据)。延伸与拓展:Intel公司上世纪八十年代生产的8086CPU所对应的内存空间:
28・两种不同类型的内存——ROM和RAM一台通用计算机中各个不同内存芯片在存储类型上还可以分为两大类:一类是只读存储器ROM。ROM中存放的位串是永久的、不可改变的,即使在电源关闭后,存储在其中的所有位串仍然保持不变。只能“读”出(即复制出)ROM内存的任何内存单元中的数据,而不能将数据写入ROM的任何内存单元中。ROM用来保存标准化的数据和基本输入输出程序(即BIOS),用于启动计算机。另一类是随机存储器RAM,其内存单元中存放的位串可以被(运行的程序)改变,它被用作为临时存储器。在电源关闭后,RAM中的数据都将消失。内存并不将存储在它之中的指令和数据区别对待。一个基本存储单元中的存放的位串究竟是什么,是由程序(或者说是由软件)负责而不是由硬件来负责的。一个基本存储单元中的二个隹申既可能是一条指令(或一条指令中的一部分),也可能是一个任何类型的数据(或一个数据中的一部分)。计算机内部存储和传输数据的另一个常用单位“字”在计算机内部,还有一个与计算机硬件性能密切相关的存取和传输数据的常用单位:“字”(Word)o一个“字”的长度到底是等于1个字节、2个字节、4个字节还是8个字节,与该计算机的数据总线的宽度——即组成一条数据总线中的并排导线的根数——有关。比如:在某一类数据总线宽度为32的计算机上,一个字的字长就是32位,它包含4(32/8=4)个字节。计算机执行一条指令,就能够存取和传输一个字长度的位串。在现代计算机中,数据总线宽度通常等于8、16、32或64。并分别把这些计算机称为8位、16位、32位或64位计算机。这是由于:数据总线的宽度是衡量一台计算机性能的极为重要的一个指标。2.2.1CPU——又称为中央处理单元(相当于理想厨房):CPU负责通过三条总线到内存指定的存储单元中去读取(程序中的)(相当于取菜谱中的加工步骤),对取到的指令进行译码得到操作命令,并根据命令的要求对数据进行运算或通过总线命令其他部件执行指令。CPU中的主要部件有:控制单元(相当于厨房管理员)、算术逻辑单元ALU(相当于厨师加炊具)和一些用来临时存放数据的寄存器(相当于理想厨房中的碟子)。通常的CPU中随着类型的不同有十几个到几百个寄存器不等。
29(1)控制单元又称为控制器:控制单元负责通过总线从内存中读取指令、对读取到的指令进行译码得到命令,并负责通过总线向相关部件发布(执行指令的)命令,控制单元是指令执行的控制中心。(2)算术逻辑单元(ALU)又称为运算器:运算器用来对数据进行基本运算。它负责根据来自于控制单元的命令,得到所要加工的数据、对数据进行运算、保存运算结果。ALU是计算机中的数据处理中心。(3)寄存器:在CPU中通常都有一些专用寄存器和通用寄存器。专用寄存器通常用来存储一些与程序运行相关的特定数据或信息。CPU中最重要的专用寄存器有三个:指令地址寄存器——又称为PC寄存器或程序计数器。PC寄存器中存放下一条将要执行的指令的(内存)地址。没有PC寄存器中的值,控制单元无法知道要执行的下一条指令位于内存的何处。指令寄存器——又称为IR寄存器。IR寄存器是用来存放刚刚从内存中取来的立即要执行的指令,便于控制单元对指令进行译码。程序状态字寄存器——又称为PSW寄存器。任何种类的CPU中,通常都有一个程序状态字寄存器,它是用来自动跟踪和记录一条指令执行后,计算机当前所处的各种状态信息。比如本次计算结果是否为零、计算结果是否溢出等等。这类“是否”数据有多条,每一条信息使用程序状态字寄存器中的某一个固定位(bit)来保存。有些计算机中,把程序状态字寄存器称为桥志奇有番。延伸与拓展:一个标志寄存器的实例:Intel公司生产的个人计算机的CPU中的标志寄存器中,存放的数据如下图所示:15141312111098765432100FDFIFTFSFZFAFPFCF对其中一些常用位的介绍如下:CF是进位标志:用于反映运算是否产生了进位或借位。有进位或借位CF置1,否则置0。PF奇偶标志:用于反映运算结果低8位中T»的个数。T”的个数为偶数,则PF置1,否则置0.ZF(ZeroFlag)零标志:用于判断结果是否为0。运算结果0,ZF置1,否则置0。SF(SignFlag)符号标志:用于反映运算结果的符号,运算结果为负,SF置1,否则置0。OF溢出标志:反映有符号数加减运算是否溢出。如果运算结果超过了8位或者16位有符号数的表示范围,则OF置1,否则置0。
30任何类型的CPU中都还有少量通用寄存器可供程序使用,通用寄存器的功能与可读写的RAM内存单元差不多,都是用来临时存放一些运算器要加工处理的数据。但是,运算器到通用寄存器中读写数据比到内存单元中读写数据要快得多。因此,在编程时把程序经常要用到的少量数据临时存放在一些通用寄存器中,这样可以加快程序的运行速度。一个寄存器中通常存放长度为一个字的位串。2.2.1外围设备外围设备负责将数据输入到计算机内部和将信息输出到计算机外部,外围设备可分为以下三类:⑴输入设备负责将数据输入到计算机内部。典型输入设备有:键盘、鼠标、扫描仪、话筒、数码相机、网线、传感器、摄像头等:⑵输出设备负责输出计算机运行程序得到的信息。典型的输出设备有:显示器、打印机、音箱、网线等:(3)外存(或称为辅助存储器)通用计算机通常都还有一些速度比内存慢得多,而容量又比内存大得多的价格比较便宜的外存(或称为辅助存储器),比如硬盘、光盘和U盘等。由于关机后RAM内存中的所有数据都将消失,可以将RAM内存中临时存放的程序、数据和信息(以文件的形式)长久保存到外存中——即进行“存盘"操作。外存可以在关机状态下长期保存大量程序和数据,但是外存中的程序和批量数据必须加载到内存中才能得到CPU的运行和处理。对外存和其他输入输出设备的进一步介绍请参见第十章。由图2.1可见,通用计算机的CPU、内存是直接与总线相连的:而所有外围设备通常都是通过输入输出控制器(或称为输入输出接口电路)回段连接到总线上的。比如:与显示器相连的输入输出控制器称为“显卡”,与音箱、话筒相连的输入输出控制器称为“声卡”,与网线相连的输入输出控制器称为“网卡”。包含了机械部件的外围设备的运行速度比由纯电子器件组成的计算机内部的运行速度要慢得多,速度相差大约103——107倍。计算机内部由CPU、内存、输入输出控制器和连接它们的总线组成,这是一个由纯电子部件构成的使用二进制数字信号的高速运行的内部世界。现代通用计算机在执行一条输入输出指令时,通常都是由CPU中的控制单元将执行指令所需的相关命令发送给输入输出控制器,由输入输出控制器来控制输入输出设备进行工作。由于计算机内部使用的数据与外围设备使用的数据格式通常都不一样,计算机内部与外围设备的运行速度也不一样,输入输出控制器通常都要完成数据格式转换和数据缓冲等工作,对这些内容的进一步讲解请参见本书最后一章。不同计算机配置的外围设备品种和数量可以大不相同。过关测试:I、计算机是由0和0两大部分组成的。2、计算机通过运行(),将()加工成()。3、计算机硬件由()三大部件构成,三者之间通常用0连接起来。4、总线分为0总线、0总线和()总线,总线的功能是()。5、内存由很多同样大小的基本存储单元构成。每个基本存储单元都有一个固定不变的唯一编号,这个编号称为()。6、内存用来存放0和()。7,内存既可以根据从()总线上接收到的来自CPU的读取数据命令,将某个指定地
31址的内存单元中的数据读取出来,放到()上送到CPU;而到底要读写内存中的哪一个存储单元,是由CPU通过()总线传送到内存的地址来确定的。8、内存分为()和0两大类型。两种类型内存的特点是()o9、CPU由()、()和()组成。10、控制单元的任务是()o11、算术逻辑单元的主要任务是()。12、CPU中最重要的专用寄存器有三个,它们分别是()、()和()。14、()寄存器,是用来存放将要执行的指令的内存地址的。15、IR寄存器,是用来存放()。16、程序状态字寄存器(或称为标志寄存器),是用来存放0的。17、通用计算机的外围设备都是通过()间接连接到总线上的。18、计算机的外围设备分为()、()和()这三大类。19、CPU能否直接运行外存中的程序?2.2计算机系统中的软件计算机软件是由程序(相当于菜谱)和数据组成的。程序是计算机系统中的一个无重量、无体积、不会损坏、但通常可以经常更换的极为重要的软件部件。(机器语言)程序是由完成某一任务的很多条指令顺序组成的。那么,什么是指令呢?2.2.1指令:指令命令计算机进行一个基本操作。任何类型计算机的任何一条指令,都是由两部分组成:携作写和操作教。在书写一条指令时,操作码位于一行的左边,操作数要位于同一行的右边。操作码指示计算机做一件什么事(计算机所能够做的、固定n件事中的哪一件);操作数告知计算机对什么数据(或指出对位于哪个内存单元或寄存器中的数据)做这件事,或者还要告诉计算机将计算结果存放到哪里去。一条指令的操作数通常有。到3个。有些指令的操作数规定必须位于某个(或某些)特定的寄存器中。一条指令所能够做的事通常都是极为低级的、简单的;一般只涉及两个(或一个)数据的基本运算【注入存数、取数、输入、输出和跳转等等(参见本章后面:计算机常用指令的分类)。【注】:基本运算包括:算术运算(加、减、乘、除)、逻辑运算(与、或、非)和位运算,请参见附录。比如在一种计算机中,指令110010110111表示:将内存地址为10110111中的内存单元的数读取出来,与累加器(这是大多数CPU中具有的一个常用寄存器)中的数相加,将运算结果仍然存放在累加器中,操作码1100表示要做加法。2.2.2指令集:一台计算机硬件能够执行的所有不同指令的(操作码的)集合,称为指令集。通常计算机的指令集中——随着计算机(CPU)的类型不同——有几十到几百条指令不等。不同类型计算机的指令集通常都是不相同的。比如:在一台计算机上用操作码110。表示做加法;而在另一台不同类型计算机上可能用操作码1011011表示做加法。对一个模型计
32算机的指令集的介绍,请见第章。指令集的本质一台计算机的指令集,本质上是计算机CPU的设计者为计算机硬件设计制定的一张“编码解码表”(这其实是一张在计算机内部看不到的、广义上的“表”)。编码的对象是一种类型计算机能够执行的、可以由人们通过编程来命令计算机做的所有各种不同拳本操作。每一种基本操作都由设计者赋予了一个编码,这个编码就是指令的操作码。计算机的硬件(CPU中的控制器)“懂得”这个操作码要求它做什么。2.2.1计算机常用指令分类虽然每种类型计算机的指令集都是不一样的;但是,所有不同种类的计算机指令集中的常用指令娄犁却都曷很相似的,这源自于任何类型计算机能够做的事本质上都是类似的(所有计算机都是图灵机的一种具体实现)。任何类型计算机指令集中的所有指令,通常都可分为以下七大常用类型:1.数值计算类指令:命令算术逻辑单元(ALU)对数据作一次基本运算。2.读数据指令:把指定内存地址的存储单元中的数据,读到CPU的某个寄存器中。此类指令的执行过程与硬件自动进行的取指令的操作过程很类似,只不过目的寄存器不一样。从某个内存单元中“读"数据(到CPU中)又称为“取出"数据,“取之不尽”:内存单元中的数据是“取之不尽”的,从某个内存单元中“取出”数据(到CPU),只是复制出了这个位串而已,内存单元中的该位串依然不变。3.写数据指令:把某个寄存器中的运算结果(或中间运算结果)写入到某个内存单元中。向某个内存单元“写”数据又称为“存放"数据。存就变”:数据是“一存就变”的,CPU向一个内存单元存放一个数据,就是用一个新的位串替换了内存单元中原来老的位串,内存单元中存放的原来值当然被新值取代了。延伸与拓展:读数据指令和写数据指令的执行过程此过程与理想厨房到自动冰箱中取物品(或存物品)完全类似。内存既可以根据从桂制昌线上接收到的来自CPU的读数撮命令,将某个指定地址的内存单元中的数据读出来,放到数据与线上(送往CPU的某个寄存器中);也可以根据从革制总线上接收到的来自CPU的写数卷命令,将CPU中通过藜赠总、线传送过来的数据写到指定的内存单元中。而到底要读写内存中的哪一个存储单元,是由通过地址图•线传送到内存的地址来确定的。4.数据移动指令:将一个寄存器中的数移动(即复制)到另一个寄存器中。5.数据输入指令:命令输入设备输入一个数据。输入指令的执行,将覆盖掉内存中某个存储单元(或CPU中的某个寄存器)中的原来数据。6.数据输出指令:命令输出设备输出某个内存单元(或CPU中某个寄存器)中的一个数据。7.跳转指令:这种指令的执行结果就是改变PC寄存器中的当前值,这使得计算机可以跳转到内存别的地方去开始顺序往下执行指令。跳转指令分为有条件跳转指令和无条件跳转指令两大类。跳转指令使得计算机具有了根据不同情况运行不同程序段的能力,跳转指令的进一步说明请参见下一章。
33更高级一些的计算机指令的类型可能还更多,但这些更高级的指令通常都能够由这基本的七大类指令中的多条指令来替代。高级指令通常只是使得编程更高效或运行速度更快而已。7.3.5机器语言程序:用某一类计算机指令集中的指令编写出来的,完成某一任务的指令序列,称为机器语言程序。与理想厨房菜谱中的各个加工步骤类似,机器语言程序中的指令序列必须按照执行的先后顺序,按地址由小到大连续存放在内存的各存储单元中。比如如下图2.4所示:由4条指令顺序组成的程序段在内存中的存放情况。第1条指令占据了地址为0000和0001的连续两个基本存储单元,…,第3条指令占据了地址为0011、0100和0101和0110的连续4个基本存储单元。内存地址基本存储单元中存放的指令序列0000第1条指令0001第1条指令0010第2条指令0011第3条指令0100第3条指令0101第3条指令0110第3条指令0111第4条指令1000第4条指令(图2.4指令序列在内存中必须按照运行的先后顺序,按地址由小到大连续存放)虽然指令集中的指令数量是有限的、固定的,但是使用指令集中的有限种指令,可以编写出来的机器语言程序的数量却是不限制的。机器语言程序,是可以直接在计算机上运行的唯一一种程序。因为只有这种程序中的指令,才能够(通过控制单元的译码)直接转变为计算机运行时所需要的驱动各条数字电路中的高电平或低电平。7.1程序(指令序列)在硬件上的运行过程任何一条指令的执行过程都可以分为以下三个阶段:1、取指令阶段(完全类似于0.2节理想厨房通过三条传送带到冰箱中取一个加工步骤):控制单元负责根据PC寄存器中的值,通过三条总线的密切协作,从内存中取得一条指令,并将其保存到1R寄存器中;同时将PC寄存器中的原来值加上刚取到的这条指令的字节数,将结果重新存入PC寄存器一•这是为顺序取得内存中的下一条指令作准备。2、译码阶段:控制单元对取到IR寄存器中的指令进行译码。将指令分解转换成控制信号和地址信号(有时还包括进行操作的数据)。3、执行阶段:控制单元根据指令译码后得到的控制信号和地址信号,通过总线要求相关部件执行这条指令。一条指令到了执行阶段,才发挥出了命令计算机硬件进行相关操作的作用。任何一条指令在取指令阶段和译码阶段都是完全类似的、由硬件主功进行的。但是在指令的执行阶段,硬件是在由程序员编写的指令(注意:这是由人编写出来的软件)的命令下祺劲进行的工作的。而且,由于指令类型的不同,参与执行指令操作的部件和任务可以是不同的。一条指令执行完毕后,CPU立即自动地按顺序取得并执行下一条指令。这样形成了一个永不停息的(只要不出现严重故障、断电或停机)、自动化的(通常按照顺序)读取指令——译码——
34执行指令的硬件与软件密切协作、交替配合的周期性动作。计算机的重要性和独特性人类(常常要借助仪表或测量器具)通过五官获得数据(或称为对事物的感性认识);用大脑的思考将数据加工成信息(或称为对事物的理性认识);而计算机通过输入设备得到数据,通过运行程序将数据加工成信息。计算机是目前唯一一种能够帮助人们将各种海量数据快速加工处理成信息的人造设备。目前,一台普通的计算机用一秒钟(甚至用更短得多的时间)就可以计算出一个人需要几十年才能手工计算出的结果,而且通常不会出现任何错误。而任何一个人在将数据加工成信息时,是很容易出现错误的。在将数据加工成信息时,计算机比人更值得信赖、有效,速度也快得多。7.1计算机的分类对计算机进行分类的方法有多种。按照用途来分:计算机可分为通用计算机和单片计算机两大类;1、通用计算机是指各行业、各种工作环境都能使用的计算机。通用计算机功能齐全,适合于科学计算、数据处理、过程控制、娱乐和上网等多方面的应用。具有较高的运算速度、较大的存储容量、配备较齐全的外围设备(鼠标、键盘、显示器、硬盘、U盘和光驱、打印机等)。可以随着用户的需要,随时安装(或者卸载)各种不同的程序。但与下述的单片计算机相比,其结构比较复杂、价格比较贵。个人使用的台式微型计算机(简称为PC机)和笔记本电脑都属于通用计算机。2、单片计算机是指将一个计算机的主要部件(输入输出设备除外)集成在一个单一的硅芯片上的微型计算机。单片计算机又称为单片机或微控制器。由于单片机的集成度高,所以单片计算机具有体积小(大约一个指甲盖大小)、功耗低、微型化等优点,被广泛应用于智能仪器仪表的制造和对机电设备的自动控制等。单片计算机通常只运行一个或少数几个固定不变的存放在ROM中的程序,用途比较单一,因此又称为专用计算机。单片计算机配置的输入输出设备通常都很少。嵌入在各种机电设备和家用电器中的单片机,又常常称为嵌入式计算机。延伸与拓展:计算机对当代社会的重要性从人类的认识层次上来看,数据和信息对人的作用是不一样的。数据属于对事物的感性认识而信息则属于对事物的理性认识。人类能够用五官接收数据,用大脑的思考将数据加工成信息;而计算机用输入设备接收数据,通过运行程序将数据加工成信息。由于计算机将数据加工成信息的速度比人类快得多(在很多情况下快得人们几乎难以想象),因此,计算机可以帮助人们从海量数据中快速提取出大量的信息,这也大大减轻了人们的从数据中获取信息的难度。当代社会为何称为“信息社会二根本原因就是难以计数的大量计算机(至少几十亿)已经广泛使用在人类生产生活的各个方面。计算机使人类社会进入到智能信息时代。7.2计算机的重要特点7.2.1计算机系统与理想厨房系统之间的相同点计算机的重要特点与理想厨房几乎完全一样,即:1、(指令执行的)顺序性和周期性;
35顺序性:计算机一条一条地按照指令在内存中的存放顺序,串行执行指令;如果正在执行的指令所在的内存地址是m的话,下一条要执行的指令,一定是存放在内存地址为m+L的内存单元中(L是正在执行的指令的字节数)——除非刚刚执行的是一条跳转指令。周期性:任何指令的执行都是按照“取指令——译码——要求相关部件执行指令”这三个阶段进行的。2、有限和无限:指令集中指令数量的有限和由这些指令可以编写出来的程序数量的无限;3、智者(计算机的智慧,来自于计算机外部的程序编写者)和白痴(计算机的硬件是白痴);人类的智能通过所编写的程序成了计算机的一部分。这是计算机具有智能的根本原因。4、两个中心:控制单元是指令执行的控制中心,算术逻辑单元是数据运算的处理中心。5、主动与被动:6、相同与不同7.1.1计算机系统与理想厨房系统之间的重要不同点1、理想厨房是将有重量的实物构成的原材料加工成菜肴的,而计算机是将无重量的抽象数据加工成无重量的抽象信息的。原材料和菜肴都是消耗品——只能使用一次;而数据和信息与程序一样都是非消耗品——可以使用无数次。2、在执行一个炒菜的基本加工步骤时,可以将多种原材料同时放入炊具中进行加工;而在执行一条指令时,由于受到算术逻辑单元的硬件结构限制,一次只能对画个(或一个)软进行基本运算,每次运算不能多于两个数。3、数据、信息和程序(指令序列)都是二进制位串,因而相互之间都是可以转化的;而原材料只能转变成菜肴,原材料和菜肴都不可能转变为菜谱(菜谱也不可能转变为原材料和菜肴)。4、厨师只能加工原材料,决定了理想厨房系统只能用于炒菜;算术逻辑单元只能对数据进行处理决定了计算机只能用于将数据加工成信息,计算机不能加工实物。延伸与拓展:智能机器人与计算机之间的区别智能机器人并不等同于计算机,智能机器人的"大脑"是计算机。所以,即使智能机器人能够加工实物,我们也决不能说计算机能够加工实物——计算机只能通过运行程序加工处理数据得到信息。这些信息经过转换和放大后可以用来控制机器人的行动。7.2操作系统简介现代通用计算机都是在开机后首先启动运行一个称为“操作系统”的特殊的大型管理程序。通过操作系统的运行,方便用户对外存中的存放的各种文件进行操作和管理;方便用户运行应用程序。用户对应用程序的运行要求通过数鼠标或键盘操作(形成一条命令)传给操作系统后,操作系统就知道用户想要做什么事。于是,操作系统将用户想要运行的应用程序和相关数据从外存加载到内存,然后通过执行一条跳转指令(其实就是将该程序的第一条指令的地址存入到PC寄存器中),跳转到该应用程序的第一条指令处,应用程序将自动顺序往下运行;(在单核CPU构成的计算机上,应用程序运行完毕后,又执行一条跳转指令,跳转回到操作系统的程序段中运行,操作系统又重新获得对计算机的控制权)。操作系统不仅能够方便用户运行程序和以文件的形式对程序和数据进行各种操作,它还提供了很多公共程序段供程序员编写程序时调用,大大方便了程序员编写程序的工作。操作系统还要负责管理计算机的所有软硬件资源,为多道程序的运行进行调度管理和提供服务。对于操作系统的进一步讲解,请参见第章。目前通用计算机经常使用的操作系统是Windows、Unix和linux0
36延伸与拓展:个人计算机(PC机)和笔记本电脑的操作系统如何启动PC计算机在开机后先运行一个位于ROM中的自检程序对硬件进行检查,发现无问题后就转而运行同样位于ROM中的一个引导程序,由这个引导程序到硬盘上将操作系统程序和所需数据加载到RAM内存中。最后,引导程序通过执行一条跳转指令,跳转到刚刚加载到RAM内存的操作系统的第一条指令处。操作系统程序就开始在个人计算机上得到了运行。2.8现代计算机硬件方面的一些重大技术改进现代计算机的硬件为了提高性能,普遍采取了一些先进技术,比如中断技术、流水线技术、高速缓存、各种数据缓冲区、DMA输入输出方式和多核CPU等等。以上这些技术将在第十章进行简介。现在读者只需知道,所有这些先进技术,都没有造成基本编程模式的重大改动。用高级编程语言(参见下一章)编写程序的基本原理,仍然是以本章和下一章所介绍的知识为基础的。根本原因在于软件的价值与硬件相比越来越高。如果一种硬件技术的革新使得以前的程序和编程模式在这种新型的先进计算机上毫无用处,那么这种新型计算机多半就会被市场毫不留情的淘汰掉。2.9提高部分2.9.1跳转指令・无条件跳转指令:一种简单的无条件跳转指令的格式是跳转到偏移地址“跳转到"代表无条件跳转指令的操作码。执行带偏移地址的跳转指令很简单,只需将此偏移地址与PC寄存器中的原来值相加,得到一个新的正整数,再把此数重新放到PC寄存器中,这条跳转指令就执行完了。举例来说:PC寄存器中现在是15,要执行的无条件跳转指令是:跳转到-4,则执行此指令后,PC寄存器中的值变为(15-4=)11了。下一条要执行的指令现在是在内存地址为11的内存单元中,而不再是在地址为15的内存单元中了。・有条件跳转指令有条件跳转指令的格式是:条件成立跳转到偏移地址有条件跳转指令是在某个条件成立时才进行跳转(跳转操作与无条件跳转指令完全一样),条件不成立时就不进行跳转——即不改变PC寄存器中的当前值。所有这些条件值都是存放在CPU的一个程序状态字寄存器(或标志寄存器)中的。在条件不成立时,有条件跳转指令不做任何事。举例来说:溢出则跳转指令的操作码假设是1110,那么指令111011010111就是一条条件跳转指令。这条跳转指令在程序状态字寄存器中的溢出位为1时将执行跳转(PC+11010111-*PC),溢出位为0时将不进行跳转。跳转指令的操作数还可以是绝对地址,这种操作数只需直接将其存到PC寄存器即可。2.9.2规范化、结构化的机器语言程序•主程序和子程序在编写机器语言程序时如果滥用跳转指令,程序将会象一团乱麻一样极难读懂和理清头
37缙。因此,提倡编写规范的、结构化的机器语言程序。规范的、结构化的机器语言程序在内存中通常都是分段连续的,每一个连续存放在内存中的程序段都是功能相对独立的,这种程序段称为子程序。这些程序段中还有一个唯一的、特殊的程序段——主程序。任何规范化、结构化的机器语言程序,都是从主程序的第一条指令开始运行的。2.8.1结构化的机器语言程序各程序段之间的连接方式各段程序之间并不是用跳转指令来尾首相连的(即:不是从前一个程序段的最末尾处跳转到下一个程序段的最开头处…),而是通过执行主程序中的第i条指令——这是一条跳转指令——跳转到某个子程序的第1条指令之处,顺序执行到该子程序的最后一条跳转指令之后,又跳回到主程序的第仁!条指令。任何一个子程序都是用这样的方式与主程序相“链接”的,只是非负整数i的值不同而已,参见图3.1。主程序(主调程序)地址111213141516171819指令******跳转到204******************一个子程序(被调用程序)/204205206207208(出口)************跳转到地址14图3.1主程序与子程序之间的连接方式(假设每一条指令都占内存的一个基本存储单元)2.8.2子程序的“调用"与"返回"通过执行一条跳转指令去运行一个子程序称为“调用"(call)子程序;子程序通过执行跳转指令回到主程序称为“返回”(return)。调用子程序的程序段又称为“主调程序”。不仅主程序能够“调用"子程序,任意一个子程序也可以用完全相同的方式“调用"任意另一个子程序。这种模块化、层次化(主程序属于第一层,由主程序调用的子程序属于第二层,…等等)的机器语言的程序组成结构,有利于大型程序编写的分工。功能不同的子程序,可以是由不同的程序员在不同时期编写出来。因此,各程序段之间在"调用"和“返回”时,通常都存在着数据传递和保存现场、恢复现场的问题,参见下面的延伸与拓展。延伸与拓展:子程序“调用”与“返回”中的数据传递和保存恢复现场如果主调程序需要传递一些待处理的数据给被调用的子程序(这是极为常见的情况),被调用的子程序也需要将一个处理结果回传给主调程序(这也是很常见的情况),仅仅依靠两条跳转指令,就想完成两个程序段之间的“调用"和“返回”是次对不行的。首先,被调用子程序不仅要能够接收主调程序传递给它的数据,还要能够将运算处理结果回传给主调程序。使得主调程序和被调子程序之间能够交换数据的最直接最常用的方式是:让主调程序和被调用子程序共享一段存放数据的内存空间(这段共享的内存空间通常位于“栈”中)。其次,被调用子程序返回到主调程序后,还要不能够影响主调程序的继续正常运行。这里存在一个“保存现场”和“恢复现场”的任务。也就是说,由于运行子程序中的指令,很可能把CPU某些寄存器中的原来值覆堇将J,而这些值恰恰是主溯程序运行一些指令后暂
38时存放在其中的,主调程序后面的指令需要用到的数据。这就称为主调程序的“现场信息”遭到了破坏。现场信息一旦遭到破坏,返回到主调程序后,主调程序就无法正常运行下去。例如:调用子程序前,主调程序中的某条取数指令将数据16存到了寄存器R0中;但寄存器R0中的值又被子程序中的某条指令改变成了25。这样即使返回到主调程序后,主调程序后续指令需要使用的R0寄存器中的值就不是原来的数据16To这样一来,主调程序运行下去将得
39不到正确结果。我们在初学时,对于子程序调用时数据的双向传递、保存和恢复现场这两个比较深入的课题不必刨根问底。对于子程序的调用和返回,我们只要知道以下两点就足够了:在调用子程序时,不仅要执行一条跳转指令,还必须执行一些附加的指令;从子程序返回到主调程序时,不仅要执行一条跳转指令,也还必须执行一些附加的指令——由这些附加的指令完成数据的双向传递和保存现场、恢复现场的工作。本书将在第七章比较深入地探讨这两个问题。幸运的是,在我们用C语言(或别的高级语言)编写可以翻译成机器语言子程序的函数时,参数传递的细节问题、保留和恢复主调程序的现场信息,都不需要编程者来考虑。这些麻烦的然而是例行的事务,编译程序都为我们包办代替了。本章习题:一、问答题I.指令地址(PC)寄存器起了什么作用?它位于哪里?取完一条指令后,它的值一定加I吗?指令(IR)寄存器是用来干什么的?它位于哪里?2.3.4.5.6.7.控制单元的主要功能是什么?它位于哪里?算术逻辑单元的主要功能是什么?它位于哪里?总线在计算机中起何作用?数据总线在传输数据时是双向的还是单向的?在传送指令时呢?现代绝大部分计算机是以字作为基本存储单元还是以字节作为基本存储单元?即将要运行的程序的指令序列存放在哪里?程序要加工的数据存放在哪里?二、不看术语对照表,请将两边的术语连线:冰箱格子的编号冰箱的一个格子编写普通菜谱的人特殊菜谱加工步骤理想厨房厨师及炒菜设备厨房管理员指令存放碟IR普通菜谱专用加工容器编写特殊菜谱的人指令地址存放碟PC原材料采购员及配菜员传菜生炒好的菜厨师可做的各种炒菜基本动作理想厨房状态存放碟内存的基本存储单元数据状态寄存器输出设备用机器语言编程的程序员信息输入设备指令地址寄存器指令寄存器CPU专用寄存器基本运算内存地址高级语言程序算术逻辑单元控制单元算术逻辑单元可进行的各种运算用高级程序设计语言编程的程序员计算机系统机器语言程序指令
40三、1.请你用计算机的术语,详细叙述控制单元通过三套总线到内存取指令的步骤。2.将十进制整数134转化成二进制整数。3.将二进制整数110分别转化成八进制和十六进制整数。4.将十六进制整数(D34A)转化成二进制整数。5.将八进制整数623转化成二进制整数。(注意:一个八进制数字转化成多位二进制数字时,不足三位要补成三位,八进制2对应着二进制010而不是对应着二进制10)6.能否将内存中的任意一段数据当成指令来执行?(非法指令问题、乱序指令问题)7.内存单元的地址是二进制位串,内存单元中存放的数据也是二进制位串,请尽量列出两者之间的不同点。08.请将十进制数123.45转化成二进制数。(提示:将整数部分和小数部分分别进行转化)。9.现代计算机的可执行程序在程序运行期间是不可修改的,但在计算机发明的早期却没有这种限制,请考虑一下这种限制的有利和不利之处。*10.一个机器语言程序中的任意一条指令的操作码发生一位错误,会出现什么问题(可能是一条非法指令,或者变成另一种指令)?一个机器语言程序中的任意一条指令的操作数发生一位错误,会出现什么问题(特别考虑跳转指令)?如果是表示一个文字的码发生一位错误,会出现什么问题?11、假设有一种采用固定长度操作码的CPU,指令集中共有43条指令,请问操作码的长度最少应当是多少位?其中有多少个是无效码?12、对于4个元素构成的集合,用长度为2的位串进行编码,一共有多少种不同的编码方式?13、对于数据压缩这个任务来说,什么是数据?什么是信息?对于将压缩了的数据解压这个任务来说,什么是数据?什么是信息?14、地址总线的宽度(即并排导线的总的根数)决定了一台计算机内存的基本存储单元的总数的上限。请问一台计算机地址总线的宽度为32时,这台计算机内存的基本存储单元总数的上限是多少?如果这台计算机的一个基本存储单元只能存放一个字节,它的内存容量的上限是多少字节?15、一个程序要如何才能输入到计算机中?16、信息在某些特定场合也可以转化为程序,你能举出这样的例子吗?四、是非判断题1、某些数据处理后得到的信息,对于另一种更高级的任务来说又可以是数据。2、每个基本存储单元的地址是固定不变的,而每个基本存储单元中存放的位串对于RAM内存来说又是可变的。从某一数据处理任务中得到的信息又可以作为另一个数据处理任务中的数据。3、对于一台计算机,每个基本存储单元可存储的位串都是一样长的。4、计算机程序运行出现错误时,有可能将程序作为数据来对待或者将数据作为程序(这将是一堆乱码)来运行。
41第三章C语言源程序的构成成分3.1高级程序设计语言和编译程序简介计算机发明的早期,人们都是用一条条的指令来编写机器语言程序的。各种指令的操作码和书写格式很难记忆;而且程序一旦出现错误,将很难从0和1组成的海洋中查找出来!由于不同类型计算机使用的机器语言不同,为一种类型计算机编写的程序难以移植到另一台不同类型的计算机上运行。此外,使用机器语言编程如果滥用跳转指令,程序的流程将会混乱不堪,极难读懂。一条指令所能够做的事微不足道。这就意味着:用机器语言来编写程序的程序员,必须将他的整体编程思路一直细化、直到分解成为计算机能够执行的、微观的、一条条的指令为止。这使得用机器语言来编写大型程序变得极祭四举。人们能否使用一种宏观的、与具体计算机类型无关的、人类比较易于掌握和理解的由字符序列组成的更高级的“程序”,来具体“指导”计算机自动生成机器语言程序,变成一件极为重要和迫切的任务。高级程序设计语言在这种迫切的需求下诞生了。高级程序设计语言,是一种人们比较容易掌握的、无二义性的、”将代数式和英语符号组合在一起的独立于机器的编程语言”。本书以后经常将其简称为高级语言或高级编程语言。用高级语言编写的程序——称为源程序——是不能直接在计算机上得以运行的。所谓编译程序,是一种由计算机专家编写出来的特别有用的一种(机器语言)程序,它可以把人们用某种高级语言编写出来的、本质上是一串字符序列的源程序,翻译加工成一个由指令序列组成的二进制机器语言程序。换言之,对于编译程序而言,高级语言源程序是数据,这些数据经过编译程序的运行处理而得到的信息就是一个机器语言程序。这个机器语言程序能够完成源程序所要求完成的任务。数据程序高级语言源程序(一长串字符)图3.2编译程序的作用C语言、C++语言、Java语言、C#语言、Pascal语言和Basic语言都属于这一类的当前比较流行的高级编程语言。据统计,使用高级语言编程比用低级语言编程的效率大约高出5到20倍。用问题:一个用C语言编写的源程序,能否使用java语言的编译程序去进行编译?答:决对不能!采用哪种语言规范进行编程,就必须使用那种语言的编译程序进行编译。但是有一个例外:使用C语言编写的源程序,除了可使用C语言的编译程序进行编译之外,通常都还可以使用C++语言的编译程序进行编译。原因在于C++语言实际上是C语言的一个超集。思考题:在不同类型计算机上运行的C语言编译程序是否相同?从功能和形式两方面来考虑。3.2C语言历史概述本书选定用C语言这种高级语言来进行讲解,首先是由于当前最流行的高级语言比如C++,Java,C#都是由C语言衍生出来的。因而,所有这些语言的基础语法部分都与C语言大同小异。此外,目前很多重要的系统软件(比如操作系统)中的相当多的代码段(即程序段),也是用C语言编写的;在嵌入式系统开发中也大量使用C语言。通过对C语言的学习,有利于加深对计算机基本工作原理的认识。在当代,C语言已成为计算机领域内的一门通用语言。
42练习1:上网查找资料并阅读参考书:《C语言程序设计——现代方法》[美]KN.King著、吕秀锋译人民邮电出版社,归纳出C语言的主要优点和缺点。*练习2:高级程序设计语言的范式主要有四种类型:命令型(或称为过程型)、函数型、面向对象型、逻辑型。上网查找资料并翻阅参考书《计算机科学概论》,大致了解各种范式语言的特点。C语言是一种命令型语言。问题1.C程序设计语言是谁设计出来的?答:是由美国贝尔实验室的D.M.Ritchie于1972——1973年间在B语言的基础上设计出来的。C语言自从由D.M.Ritchie设计出来以后,得到了迅速普及,自从D.M.Ritchie与他的同事BrianW.Kerni加an合写了一本名著《TheCProgrammingLanguage》之后,这本书的第一版,实际上成了早期C语言的标准。这个标准称为"K&R的经典C”或简称为“经典c\很多早期开发出来的C程序,都是遵守这个事实上的标准的。然而这个标准尚有一些不足。为了C语言的标准化和健康发展,美国国家标准协会经过长期努力,于1989年制定颁布了一个C语言标准,被称为ANSIC89标准,简称“C89标准”。对经典C进行了一些改进和完善。随后于1999年又颁布了C语言的一个新标准。简称“C99标准”。3.5C语言源程序的构成成分:我们通过以下演示程序3.1来探讨C语言源程序的构成成分演示程序3.11#include
43和每条语句都要以分号结束(复合语句除外)。定义:定义主要是用来向编译程序申请若干个存放某种类型数据的内存单元的。语句:语句主要是用来告诉编译程序:对(用定义申请到的)内存单元中的数据进行怎样的运算处理,请编译程序翻译成适当的机器指令。演示程序3.1的函数体中只有两条语句(第4、第5行),没有定义。L函激背部:返回值类型函数名(参数列表)函数体:以/开始,以,结束Y语句图3.1C语言函数的构成3.5.2C语言源程序的次要组成成分:编译预处理命令和注释•编译预处理命令简介编译预处理程序按照编译预处理命令对源程序进行文本插入和替换等初加工。4参见第十一章与其他许多高级程序设计语言不同,C语言的源程序在正式运行编译程序号前,必须事先运行一个编译预处理程序。编译预处理程序根据源程序中出现的编译预处理命令对源程序这个文本文件进行文本插入(#include命令)和文本替换(#dcfine命令)等初加工。每一条编译预处理命令都以字符开始,并且不能以分号结束。一条编译预处理命令通常书写在同一行上。编译预处理命令通常放在C语言源程序文件的开始处(但有些编译预处理命令——比如条件编译命令——可以出现在函数体中,参见第章),本例题第1行就是一条编译预处理命令。#include命令,是头文件包含命令,编译预处理程序根据这种命令,将相应头文件(头文件的扩展名为,h)中的全部内容进行“复制”,然后“粘贴到"(也就是插入到)你的源程序中头文件包含命令所在的位置上。编译预处理程序运行完毕后,源程序中所有的编译预处理命令都不复存在,但源程序文件的内容得到了编译预处理程序的编辑加工。编译预处理命令的详细解说将在第章进行。•注释注释通是以“/*”开始,以“*/”结束的字符序列。注释被用来说明整个程序或某段程序的功能、程序的作者和编写(或修改)日期等的。注释是给人看的,而不是给编译程序“看”的。注释的常用方式有两种:一种是注释内容独自占据多行,对注释以下的一段程序或者整
44个源程序文件进行说明(例题2.2的第1一5行);另一种是出现在一行语句或定义的右边,对同一行左边的内容进行说明解释(比如本例程中的第1、2,3、6行)。在将源程序翻译成机器语言程序之前,源程序中的注释都会被编译程序删除掉。但良好的注释将使得尊犁序更易被人们读懂,这使源程序更容易理解和修改。还有一种仅仅用于单个一行的注释。单行注释只需以作为开始,比如:#definePI3.1416〃指定符号常量Pl的值为3.1416它的使用比较方便、不易出错。但早期版本的一些C语言编译器不一定支持这种方式的注释。延伸与拓展:不要忘记书写注释完毕时的结束符号“*/:最好首先输入一对"/*""*/",然后再在两者之间插入注释文本。两个用作为注释的符号*和/之间不能有空格。修改源程序的语句时,一定不要忘了修改相应的注释。不然的话,这段未经修改的注释很可能使以后的程序维护者或阅读者无所适从、深受其害。注释“/*”和《*/”不支持嵌套使用,比如:/*123/*abcd*/ef*/是错误的。1、函数2、编译预处理命令3、注释4、声明(在第章讲解)C语言源程序的构成成分图3.3一个C语言源程序的构成成分•对演示程序3.1的说明演示程序3.1的第4行是一条格式化输出库函数printf()的库函数调用语句。这条语句的最基本的功能是:将输出库函数printf()中的一个调用参数——双引号括住的一串字符,按照原样输出在显示屏上。・格式控制串:输出库函数printf()中用双引号括住的这一串字符HeUoWorld!
45,称为格式控制串(有时简称为格式串)。这是调用printf()库函数时必须要填写的一个最重要的参数。格式控制串中末尾的两个字符‘
46'不是按照原样在显示屏上输出的普通字符,而只是一个所谓的“转义字符”(以、开始的转义字符的进一步说明请参见下一章),所以没有显示在屏幕上,其作用是通知输出设备(显示器或打印机)换行。格式化输出库函数的进一步说明请参见3..x节。演示程序3.1的第5行是一条返回语句return0这条返回语句的作用是在main函数运行完以后,正常返回到操作系统。过关检测:1、一个C语言源程序是由一个或多个顺序组成的。其中必须有并且只能有一个o2、任意一个函数由和函数体这两部分组成;函数体必须以开始,以结束。3、C语言源程序的函数体的主要组成成分是和;4、定义主要是用来向编译程序申请若干个存放某种类型数据的一^的。5、语句主要是用来告诉:如何对内存单元中的数据进行;请编译程序翻译成适当的。6、除了复合语句外,每条定义和每条语句都要以结束。7、编译预处理程序根据源程序中出现的对源程序进行等初加工。
478、注释的主要作用是o9、注释以一开始,以—结束;一种仅用于单行的注释只需以一开始。注释的作用主要是。
48演示程序3.21/*2功能函数调用与返回的演示3日期:2012年11月28日星期三4作者:何勤5*/6#include
49w);10sayO;/*函数调用语句*/>main函数11printf("GoodBye!
50");12return0;/*返回到操作系统*/13}J14voidsay015{16printf("Mynameiszhangsan
51"»say函数17return;/*返回到主调函数*/18}」程序运行结果:HelloWorld!MynameiszhangsanGoodBye!•程序说明:1、C语言程序是由一个或多个函数定义顺序构成的。本程序由两个函数构成:一个函数名为main的主函数。一个函数名为say的函数。2、C语言程序都是从main函数的第一条语句开始运行的(即本程序中的第9行),该句打印出:HelloWorld!o第10行是对say函数的调用语句,say函数中的语句16行开始运行,打印出:mynameis
52zhangsan。然后通过运行17行的return语句返回到main函数的第11行,打印出:GoodBye!3、printf函数调用中
53的作用・操作题:1、把程序中的第9行和第10行掉换次序,看看运行结果如何?2、把程序中的第10行和第11行掉换次序,看看运行结果如何?3、把printf()函数调用语句中的所有
54删除掉,重新进行编译,看看运行结果如何?3.5.3标准库函数及函数调用语句标准库函数是指编译程序(又称为编译器)附带的,一批事先编写好了的、命名了的子程序。在我们编写的C语言源程序的语句中可以调用库函数,为我们的程序做一些复杂的(然而通常是辅助性的)运算处理或输入输出工作。函数调用语句的格式为:函数名(参数列表)参数列表必须要用目括号括住,其中参数列表中的参数如果多于一个,参数之间要用逗号隔开。比如:要求出5.6的4次方是多少,就要以这种方式调用pow库函数:pow(5.6,4);将数值2开平方,就要调用库函数:sqrt(2)o编程时调用了库函数,这些事先编译好了的子程序,就可以通过“编译”步骤之后的“链接”这一步骤,链接到你所编写的程序之中,共同组成一个可执行的机器语言程序。调用库函数时不要忘记包含相应的头文件。C语言中的各种常用库函数的调用方法说明,请参见附录C。库函数名通常用的都是小写英文字符构成的标识符。・C语言中,常用的数学库函数如下:(注意:调用以下这些数学库函数时,不要忘记加上头文件包含命令:#include
55floor(x)求不大于X的最大整数pow(x,y)求X的y次塞表1.3常用数学库函数表3.5.3C语言源程序的编辑、编译、连接和调试过程:一个源程序从编写、编译到运行的整个过程简述如下:(1)通过运行编辑程序编辑输入源程序;一个C语言源程序编写好了后,通常必须保存在一个扩展命为.c的文件中,否则,VC++6.0编译器会把你的源程序当成C++源程序来进行编译。(2)运行编译预处理程序,对源程序进行编译前的编译预处理工作;(3)用编译器对(经过编译预处理后的)源程序进行编译工作;(4)根据编译程序(或称为编译器)给出的错误信息和警告回到第(1)步修改程序中的语法错误;(5).编译无错误后,生程目标文件(目标文件的扩展名为.obj);(6)运行链接程序,将用户程序与所调用的库函数的子程序段进行链接,生成可执行程序(文件的扩展名为.exe);(7)试运行此可执行程序,发现链接或运行时的错误则回到第1步进行修改;(8)结束调试过程,注意保存并保管好无错误的源程序文件(有必要的话可将其刻在光盘上),日后进行软件维护这是必须的。四、过关测试1、C语言程序是从函数的第一条语句开始运行的。C语言源程序文件的扩展名为2、函数调用语句的格式为。3、函数体中有调用库函数的语句,则必须用#include编译预处理命令包含相应的»4、编译无错误后,生程"一^件,文件的扩展名为.5链接程序将用户程序与所调用的库函数的子程序段进行链接,生成程序文件,文件的扩展名为。在有了以上对C语言构成成分的大致的整体了解后,就让我们正式踏上漫长的、充满陷阱、知识、挑战,开始时有些枯燥乏味,但以后有着无穷乐趣的系统学习编程的“捷径”之路吧!不走弯路,通过一本书就能真正掌握和积累大量编程的基本思路和技巧,就是最短最快的捷径。第四章C语言基本语法要素——枯燥乏味乃生动有趣之母。——欲速则不达。下面,我们将具体讨论C语言的基本语法要素。4.1C语言字符集C语言字符集包括以下字符:1)26个大写和小写英文字母:A〜Z,a~z;2)阿拉伯数字字符:0〜9;3)特殊字符:!#%-&*_+=-~<>|/\'";.,()[]{)?:空格字符(对应键盘上的最长键Space,)
56C字符集中的任何字符,都只能在英文半角方式下进行输入。常见的编程错误很多学生会误用中文输入法下输入的标点符号。最容易用错的是逗号、分号“;”、圆括号、“)”和双引号在C语言源程序的正文部分(即除了注释、字符串和格式串之外的其他部分),只能使用C语言字符集中的字符来构成。4.1标识符标识符是以C语言字符集中的26个大小写英文字母、阿拉伯数字0到9和下划线构成的字符序列;其中,最左边的第一个字符不能是数字(比如2stu是错误的)。标识符是用来作为编程者在程序中使用的名字。在C语言中,只能使用标识符来对变量、符号常量、函数、语句标号、自定义类型(参见第9章)进行命名。下面字符序列都是合法的标识符:name,_total,student,sum_l而下面是不合法的标识符:
572ab,sum-2,123,,number7,x+y定义标识符还必须注意以下几点1.c语言对标识符中英文字符的大小写敏感,也就是说main与Main,printf与Printf都是不同的标识符。main和printf都是C语言中已经规定了的函数名。所以,我们只能用main来作为主函数名,而不能用Main或MAIN等来作为主函数名。2.在用C语言编程时,可识别标识符的长度受到各种不同版本的C编译器的实现限制。例如在某一版本的编译器中,标识符的有效长度只有8位,那么所有前8位(即靠左边的8位)相同的标识符将被该编译器当作是同一个标识符(比如student」和student_2)。3.在满足标识符规定的前提下,标识符虽然可由编程者随意给定,但还是用“见其名而知其意”的标识符来为变量、符号常量、函数等命名更好。这样的程序更容易读懂和维护。4.编程时,不要将系统定义好的标识符(库函数名、编译预处理命令等)用来定义你自己想用的变量名。所以scanf,printf,define,include等标识符,最好不要另作他用。4.3关键字“关键字”有的教科书又称为“保留字”,指的是有少量的特定的标识符(英语单词)被编译程序保留下来,表示特殊的规定意义。编译程序一旦“读到"你在源程序中写下的关键字,就会明白你要它做何事。因此,我们在编程时不能把关键字定义为我们自己的程序中想要用的名字。延伸与拓展T编译程序如何“阅读”源程序编译程序“阅读”源程序的方式,其实是通过在源程序这一个长字符串中,"识别"(即查找或匹配)出一些特定的子字符串——即关键字、标识符和运算符等——来进行“阅读”工作的。关键字分为4类。・数据类型用的关键字(12个),主要是用来告诉编译程序:编程者要定义或使用什么类型的变量(参见2.5节)。比如:int,float,char,double,等・流程控制语句用的关键字(12个):是用来告诉编译程序:我们要计算机如何有选择地运行程序中的哪些语句。比如:if、while、return、break、goto等。・存储类型关键字(4个):auto,extern,register,static用来指明变量的存储类型。・其他关键字(4个):const,sizeof,npedef,volatile在ANSIC89标准中共有32个关键字,所有关键字列表如下:breakautocasecharconstcontinuedefaultdoelsedoubleenumexternfloatforgot。iflongintregisterreturnshortsignedsizeofstaticswitchstructtypedefunionunsignedvoidvolatilewhile表2.2C89标准的关键字C99标准又增加了少量关键字,请读者自行查找C99标准增加的关键字。注意:关键字不要误用大写字符,比如旺、While、Switch、Float都不是关键字。不要将编译预处理命令、库函数名与关键字混为一谈。在程序中这三者起的作用有明显区别。4.4分隔符(和“多余的”空格、空行)C语言源程序中可使用的分隔符有三个:空格、回车/换行、逗号。同类项之间要用逗号分隔;(但有例外:for语句的三个表达式之间要用分号隔开,参见第5章)关键字和标识符之间、关键字和常量之间都要用空格或回车/换行来分隔,不能用逗号。比如:intnum,age;和return0;在C语言中,合理地使用“多余的”空格和空行——这里指的是不起语法分隔作用的
58空格和空行,可以使得程序更清晰、易懂、优美。比如:在加法运算符+的两边,分别加一个空格:a+b就比不加空格:a+b要好看些;两个同类项之间,除了必要的逗号分隔符外,还可以加一个(或多个)空格;在功能不同的语句段之间,最好加上一个空行。程序写得太紧凑(一行中书写多个语句),并不是好事。这样的程序既不好懂,也难以调试。除非你想参加国际C语言混乱代码大赛。注意:注释可以书写在源程序中任何可以插入空格的地方。4.4常量C语言源程序中,可以使用的运算量有两大类:常量和变量。在程序运行过程中,其值不能被改变的量称为常量。C语言中常量又分为数值常量和符号常量两大类。程序中应当多使用含义比较清楚的符号常量,尽量不要用意义不太明确的数值常量。在C语言源程序中,数值常量可以直接使用,但数值常量的书写规则与我们在日常工作中书写常量的规则很类似而又有些小的区别。你必须严格按照C语言的规范要求来书写,否则,编译程序将无法识别,不能将其转换成机器指令能够处理的二进制机内形式的常量。各种常用的数值常量汇总请看下表:类型举例使用规则整型567>-425、0、+23各数字之间不能出现空格或逗号,不要用。开头(0开头的整数值是八进制整数)实型:小数表示法0.543、.543、-543.、+543.21、0.0小数点的两边至少一边要有数字实型:指数表示法7.16E+15(表示7.16X1015)-3.456e-ll(表示-3.456X10')字母e(或者E)之前和之后都必须有数字,这些数字之前都可以带有正负号;e之后的指数必须是一个整数值;在字母e的前后以及数字之间不得插入空格字符型‘a'、'+‘、’8,、用一对单引号括起来的英文常用字符集中的单个字符字符串型“567"、"hello!""h"用一对双引号括起来的一串字符表4.1各种常用的数值常量•对字符常量的说明英文字母和标点符号等称为字符常量。在源程序的正文部分书写字符常量都必须用单引号括住,为的是与单个字符的标识符或运算符区别开来。比如在源程序正文部分,a是标识符,+是加法运算符,然而,用单引号括住的‘a'和'+'都是字符常量。字符常量存放在内存中的值通常是这个字符所对应的ASCII码值5。字符型量是用计算机进行文字处理的基础。注意:整型常量6和字符常量’6'是不一样的。整型常量6在计算机内部表示为二进制的110;而字符常量'6'经过查找附录中的ASCII码表可知,它的ASCH码值为54,等于8位二进制的00110110;也就是说,字符常量’6'在计算机内部的通常形式就是一个8位二进制码001101100
59在C语言源程序中,可以使用以反斜杠开头的转义字符。比如'
60'表示"回车换行”字符,对转义字符的进一步说明参见本章提高部分。4.4.2符号常量前面所讲的常量都是数值常量。C语言中字符常量也是数值常量中的一种。而符号常量是用符号在源程序中表示一个数值常量。符号常量一般是由大写英文字符组成的标识符构成,用编译预处理命令#define将符号常量与某个数值常量关联起来。比如对于实型符号常量PI,可以用以下方式定义:^definePI3.14这样一来,在进行编译之前,源程序中所有出现的符号常量PI,都会被编译预处理程序用3.14替换掉。使用符号常量,使得修改常量的值变得非常方便。比如只要将#definePI3.14修改为:#definePI3.1416,则源程序中多处审理的PI现在全都会被编译预处理程序替换成3.1416了,程序的可读性也更好。对于字符常量和整型常量,也都可以用符号常量来表示,比如:#defineADD'+'#dcfinedays314.4.3数据的机内形式和机外形式源程序中(或输入输出时)常量(或变量)值的形式——比如十进制形式——我们称为数据的机外形式;而计算机内部存储(和传输)的二进制数据形式我们称为数据的机内形式(参见第章提高部分)。一个机内形式的数据常常可以表示为多种等值的机外形式的数据。我们在编写C语言源程序和运行程序输入数据时,只能使用数据的机外形式,将机外形式数据转换成机内形式数据是由系统(编译程序、输入输出库函数等)自动进行的。但是在遇到了一些疑难编程问题(溢出、类型转换或进行嵌入式编程和系统编程时)时,往往需要了解和掌握数据的机内形式,才能找到较好的解决方案。4.5变量一些动态变化的量比如车速、温度、股票价格等,在源程序中用常量无法表示。在C语言源程序中,向编译程序申请一个(或几个)存放某种类型数据的、值的大小可以变化的内存单元,称为定义变量。4.6量不使用ASCII码的计算机上,字符常量存放在内存中的值不是这个字符所对应的ASCII码值4.6.1变量的定义:定义一个简单变量的格式为:类型名变量名;类型名要使用关键字(比如int、float,char等,参见2.10.3),变量名必须使用标识符。通过这种方式,让编译程序为我们在定义中所起的一个变量名,在内存中分配一个合适大小的内存单元,用来存放一个此类型的数据。例如:在定义变量五rnum;之后,C编译程序通常会为变量num在内存中分配他毋旌续的2个(或4个)字节作为一个内存单元,用来存放变量num的数据。编译程序通过这种方式,把一个变量num
61与一个可以存放int型数据的内存单元联系了起来。参见图2.1。内存地址(不变)…0010110••0010111••0011000•••0011001-OOllOlO内存单元中的数据(可变)存放num的值存放num的值num图4.1定义变量的作用:请求系统分配内存单元,通过变量名来存取该内存单元中的数据延伸与拓展:编译程序为变量分配内存单元的技术内幕:编译程序通过扫描每个函数中的所有变量定义,建立起一张表:变量名——内存单元地址对照表。接下来在扫描所有的语句时,编译程序就可以通过查找这张对照表,将源程序语句中出现的任何变量名,转变为机器语言指令中的存放某个数据的内存单元的地址——即指令中的操作数。编译程序通过把变量名映射为内存地址的方式,达到了为变量分配相应内存单元的目的(类似于导游通过将游客名对应为房间号,达到了为每个游客分配一个房间的目的)。如何编程并通过运行程序在内存中建立一张"表格",请参见例题6.x1,定义多个简单变量的格式为:类型名变量名列表;例如:intage,num,sum;在变量名列表中的多个变量名之间,要用逗号隔开,最后要以分号结束。编译程序会为每一个同类型的变量分配同样字节数的内存单元,用来存放各变量的值。最好使用意义明确的标识符来定义变量,比如对于兔的只数,使用标识符rabbit.count作为变量名就比用标识符n作变量名要好得多。4.6.2各种基本类型的变量定义・基本型整型变量(int):在程序运行时,需要内存单元存放(数值可以变化的)可正可负的整数,要用此类型来定义。一般C编译器分配2个(16位系统)或4个字节(32位系统)的内存空间,给一个int型变量。占用两个字节的int型变量的取值范围,在-32768到32767之间,这个范围比较小。・单精度浮点型变量(float):在程序运行时,需要内存单元存放以实数形式(即有小数分量)出现的量(比如34.1,-678.34,0.368等),要用此类型来定义。比如floatx,方就定义了两个单精度浮点型变量x和y。一般C编译器分配4个字节的内存空间给一个float型变量。单精度浮点型变量的精度是十进制的7位。也就是说,只有数值中的高7位数字肯定是正确无误的。float型变量的优点是:取值范围远比int型变量大(大约正数是在1.17X10-38到34义10M之间,负数取值范围与正数是关于原点对称的)。然而与int型变量相比,float型变量的缺点是:运算速度不如int型变量快,所存入的数据通常也只是一个近似值,细节请参见第十章。・字符型变量用类型关键字char来定义在程序运行时,需要用内存单元存储单个可变化的字符(通常是ASCII字符),要用此类型来定义。C编译器分配1个字节的内存空间给一个char类型的变量。比如charchi,ch2;定义了两个字符变量chi和ch2。
62char类型用于存储字母和标点符号之类的字符,但是在C语言的技术实现上char却是一种整数类型,也就是说char类型实际存储的是整数而不是字符。为了处理字符计算机使用一种编码,对字符的最常用编码就是ASCII码。在ASCII码中整数值98代表小写字母b;因此要存储字母b计算机只需存储数值98。标准ASCII码值的范围从0到127,在高级编程语言中,任何类型变量的值都有一定的限定范围(这是由于存储变量值的字节数很有限而造成的),程序运行时超出了变量允许的取值范围,称为发生了溢出(关于溢出的讨论,请参见下一章)。这是在编程时就要注意避免和进行处理的。变量允许的取值范围与所分配到的字节数有关,不同编译器对同一种类型变量分配的字节数很可能不一样,C语言标准只是规定了各种类型变量所占用内存的最少字节数(参见下一章)。延伸与拓展:“变量类型”的技术内幕对于不同类型的变量,编译程序分配给变量的内存单元字节数很可能不一样、数据的外部形式、机内形式不一样(参见第三章提高部分)、运算时选用的运算指令类型不一样(比如对于实型量加法,编译程序选用浮点数加法指令;而对于整型量加法,则选择整数加法指令)、输入输出变量值的转换工作不一样、变量允许的取值范围可能不一样、允许进行的运算也不一样;但上述所有这些不一样,除了最后两项需要编程者注意外,大多都不需要编程者来具体操心。这些原本极为琐碎的基础性的编程工作,只要我们恰当地定义了变量的类型,并在程序语句中合理地使用变量,编译程序(包括标准输入输出库函数)基本上都为我们代劳了。4.6.3变量名和变量的值变量的内存单元一旦由编译程序分配完毕,在源程序的语句中,就可以通过书写变量名来表示要“访问”(即存或取)变量所对应的内存单元中的数据了,这个数据称为变量的值。比如语句中的num+2表示:把变量名num所对应的内存单元中的值取出来——简称为把变量num的值取出来再加上2(num+2其实是表达式,细节参见本章2.14);而语句num=21;表示:要把数值21有△到变量名num所对应的内存单元中一一简称为:把数值21存入到变量num中(num=21;其实是赋值语句,细节参见本章2.16)。“取之不尽”和“一存就变”变量的值是“取名不冬”的。从内存单元得到一个变量的值,其实只是从这个内存单元中复审审了这个值而已,该变量的值没有发生任何变化,仍然可以再次取用;但是,变量的值又是“二存就变”的,只要运行了一条与某变量相关的存数操作语句,(在内存单元中的)变量的“老值”就被变量的新值覆盖掉了,变量的“老值”将不复存在。延伸与拓展:机器语言程序与高级语言源程序最重要的不同点之一
63机器语言程序中的指令,经常使用内存地址(作为操作数)来指明要存取(access)哪个内存单元中的数据;而高级语言源程序中的语句,使用变量多来指明要存取哪个内存单元中的数据。这是机器语言程序与高级语言源程序最重要的不同点之一。内存地址是长长的、难以记忆的二进制位串;而变量名则是程序员自己所起的,好记和易懂的(但要符合标识符规定的)名字。通过这种方式,首先使得源程序更为简明而又可读性好。其次,高级语言源程序所要加工的数据获得了极为宝贵的内存绝对位置无关性。这使得源程序具有了良好的移植性,也为多道程序同时放入到内存中创造了先决条件。但使用高级语言编程也存在一些缺点,在源程序的语句(或表达式)中,究竟何处是取变量的值(变量值不变),何处是将一个值存到变量之中(变量值将改变),变得有些模糊不清。这给初学者阅读和理解程序的运行带来一定的困难。这个问题读者要给予充分的注意,否则你就很可能读不懂很多程序。464变量的初始化变量的初始化,就是存入一个有效数据到变量对应的内存单元中。在C语言中,变量定义后,通常还要进行初始化,也就是事先要将一个数据存放在变量对应的内存单元中。然后该变量——由于有了一个有效的确定值——才能在语句和表达式中用来参与运算或进行输出。否则,变量所对应的内存单元中(以前运行别的程序遗留下)的无效数据——我们将其称为垃圾数据,就会在程序语句的执行运算或输出时被误用,造成程序运行错误。最简单的初始化变量的方法,是在定义变量时,在变量右边用一个等号“=”给予它一个初始值。初始值必须是常量(或常量表达式)。比如以下变量定义:intnumber,sum=100;charchi=a',ch2;floatarea=65.432;这三条变量定义的"变量内存取值示意图”如下:变量名numbersumchich2area变量的值垃圾数据100a垃圾数据65.432
64i图4.1变量内存取值示意图这样一来,除了变量number和ch2中的值是垃圾数据外,int型变量sum、char型变量chi和floatt型变量area都得以初始化了。初始化变量有三种常用方法。初始化变量的其他两种方法,是使用scanfQ输入库函数和赋值语句(请参见4.8和4.16)。4.7数据输出——格式化输出库函数printf()的用法变量的值如果不从内存单元中取出来(通常还要经过数制转换或解码),通过输出设备送到计算机的外部,这个值对外界就不起任何作用,程序本身的运行也就失去了意义。・变量值的输出格式化输出库函数printf(),是被设计用来在某些常用的输出设备(PC机的显示器或打印机)上输出双引号括住的格式控制串中的字符序列的。但是,printf()不仅可以像例题1.3那样,将格式串中的字符序列按照原样输出。而且还可以通过格式串的设置和控制,输出n个(n>l)变量的值,其格式为:printf("*占位符1*占位符2";*占位符n*",变量1,变量2,……变量n);占位符1的类型要与变量1的类型匹配,占位符2的类型要与变量2的类型匹配,……占位符n的类型要与变量n的类型匹配。在上式中使用了位于占位符左右两边的“*”号表示:在此处可以出现。个或任意多个按照原样输出的普通字符或者转义字符。占位符又常常称为格式符,由符号"%”和转换说明符构成。最常用的占位符有:%d,%f或%c,它们与要输出的变量类型之间的简单对应关系如下表:占位符(格式符)控制输出的变量类型转换说明符的含义%dintd表示:转换成十进制数%ffloatf表示:转换成十进制小数小数点后默认输出6位%ccharC表示:转换成字符表4.2占位符与输出量类型之间的匹配表演示程序4.11#include
65M,num,x,ch);〃将三个变量的值从对应内存单元中取出来输出8return0;9)程序运行结果为:12334.678000+此题的格式控制串是:"%d%f%c
66"。注意:在格式串中的三个占位符之间,都加了一个空格字符。如果将格式串中的占位符
67连在一起书写“%d%f%c",则几个输出项的数据就会紧密地连在一起:12334.678000+这是决对不可取的。本题中由于三个输出变量顺序已定(num,x,ch),所以格式串中的三个占位符之间的顺序决不能调换。但是,可以在此题的格式串中加入任意多个普通字符(即按照原样输出的非空白字符)和转义字符。如果变量num代表车牌号,x表示车速,而ch表示车辆是否要加油的话,那么,使用以下的prints函数调用语句来替换以上程序的第7行,对于程序用户是国人来说,或许更为优秀:printf("车牌号为:%d,车速是:%f千米/小时,加油标志:%c
68",num,x,ch);输出结果为:车牌号为:123,车速是:34.678000千米/小时,加油标志:+由此可见,“占位符”所起的作用:为所对应的、要输出的变量的值,在格式串中事先占据一个适当位置。换句话说,在调用printf。函数时,所有输出变量的值都必须插入到格式控制串中去,插入的位置是所对应的占位符事先为它占据的,与原样输出的普通字符(还包括必须转义输出的转义字符)结合起来,最终形成输出到显示(或打印机)上的信息。请看以下示意图:・对浮点型数据更精细的输出控制对于小数形式的实型量,格式符%f默认规定小数点后面一共显示6位。要想改变默认规定的显示位数,可将占位符书写为:%.nf,n是你想要显示的小数点后经过4舍5入的位数。例如:以下程序:1#include
69住宅面积要用一个float型的变量tenement_area来表示,这是因为程序要处理的每套住宅的面积都可能不一样,而且不一定都是整数。该住宅每月应交的物管费用也还要用一个变量。显然还是应该用float类型来定义,该变量不妨用标识符managpmcnjcost来命名。一套住宅每月的物管费用应当是:1.3Xtenement_area。经过上述分析,我们可以编出如下程序来:0#include
70",management_cost);7.return0;8.1问题1.是否可以把第6行语句中的分号、双引号和圆括号改为中文输入法下的分号,双引号和圆括号?请你亲自试一试。运行以上程序,得到的结果是:每月物管费用为:112.45元程序说明:在C语言程序中,我们要使用C字符集中的替换“X”表示要做乘法运算。这样一来,就可通过第5行的语句management_cost=1.3*tenement_area;求出每月要交的物管费用来,并且存放在变量mana/msjc。5t中。(这其实是一条本章后面将要详细讲解的赋值语句)。但是,此程序还存在一个亟待解决的问题:这个程序不经过修改,只能计算面积为86.5平方米的住宅每月的物管费用。如果要计算其它面积住宅的每月物管费用,必须在每次运行之前修改程序的第3行并且重新编译。这对于程序的使用者来说非常麻烦——万一程序的编写者不在怎么办?如果在程序每次运行时,能够通过键盘将实际的住宅面积输入到变量tenement_arca中,那么这个程序每一次运行就可以算出一套实际住宅的每月物管费用,这样就可以不必每次都修改程序。下面一节将讨论:如何调用格式化输入库scanfO函数,在程序运行时,通过键盘随时输入一个或多个变量的值,解决上述至关重要的问题。4.8格式化输入库函数scanf。的用法(一)
71格式化输入库函数scanf()的调用,可以使得程序运行暂停下来,等待用户从键盘输入数据。程序用户可以通过键盘输入一个(或多个)数值;输完数据后,还要按下回车键(Enter键或Return键),scanf()函数就会将这个(或多个)输入值(经过转换)存放到变量所对应的内存单元中。从scanf()函数返回后,接着运行程序后面的语句。不过在scanf。函数调用时,第一个参数是相应的格式符%d,%f或%c,它们与要输入的变量类型的简单对应关系如下表:格式符输入变量的类型%dint%ffloat%cchar表4.3格式符与输入变量类型的匹配调用库函数scanf(),通过键盘输入一个变量值的最简单形式是:scanf(“格式符”,&变量);例如:scanf("%f,&tcnemcnt_area);//假设tcncmcnt_area是float型变量;canf(M%dM,&age);//假设agp是int型变量scanf("%c",&ch);〃假设ch是char型变量,在%c之前,加了一个空格字符。原因请参见本书附录E。请看以下演示程序4.3#include
72);scanfd%d",&num);printf(”请输入现在的气温
73");scanfC'%/,&x);printff请输入等级(A、B、C)
74,r);scanff1%c",&ch);printf("总人数是:%d
75气温是:%.2f
76等级是:%c
77",num,x,ch);〃输出变量值return0;程序运行后,人机之间的交互过程如下:请输入总人数18/请输入现在气温23.6/请输入等级(A、B、B/C)〃计算机显示的输入提示〃程序用户在键盘上输入的数据。表示按下回车键〃计算机显示的输入提示〃用户在键盘上输入的数据〃计算机显示的输入提示〃用户在键盘上输入的数据总人数是:18〃计算机显示的输出结果气温是:23.60〃计算机显示的输出结果
78等级是:B〃计算机显示的输出结果在本例程序中,交替使用了库函数prints和scanfO调用,这使得程序可以在运行时与用户进行实时交互,实现了“人机对话”(更准确地说,是计算机用户与运行着程序的计算机对话)。这样编写的程序用户比较喜欢。常见编程错误:漏写输入项中变量左边的取地址运算符是scanf()函数调用时最为常见的严重编程错误。这个错误很可能使得程序在运行时崩溃。例如:scanf("%f,tenement_area);X错误!scanff'%f,&tenetnentarea);M正确!・多个变量值的输入:多个变量值的输入scanf。函数的调用方式如下:scanf(“格式符1格式符2格式符3",&变量1,&变量2,&变量3);对于scanfQ函数调用,格式串中格式符的个数要与输入项一样多,类型也要一一匹配。即格式符1要与变量1的类型匹配、格式符2要与变量2的类型匹配,等等。注意:用一次scanfO函数调用,输入多个非字符类型的变量,程序运行时几个输入数据之间要用空格隔开,输完最后一个数据之后,再按下回车键。但字符变量是例外,几个字符变量的值要连着用键盘输入,输入的字符之间不要用空格隔矛,因为空格本身也是一个字符。字符变量与其他类型数值变量最好不要用同一个scanfO函数调用输入,这样容易在输入时出错。与printR类似,在scanfO函数调用时,整型量不能用格式符%f与之匹配,实型量也不能用格式符%d与之匹配。这也是初学者经常犯的严重编程错误。•对演示程序4.2的进一步完善有了上述这些知识,我们现在可以来完善例题4.2了。请读者先不看答案,自己独立做出来,然后与本书所给的以下答案比较一下。经过修改完善的演示程序4.2如下所示:1#includc
79");8scanf(M%f*,&tenement_arca);9management_cost=UNIT_PRICE*tenement_area;/*求出每月物管费用*/10printff,住宅面积%6.2f平方米,物管费%.2f元
80”,tenement_arca,management_cost);
811return0;2}编译通过后,请重复两次运行此程序,每次输入的面积不同,看看程序是否输出了相应不同的物管费用?延伸与拓展:printf。函数与scanfO函数调用时的一些重要区别虽然格式化输入库函数scanf()在用法上与printf()有不少类似之处,但是,两者之间在使用上,也有以下所列的重要不同之处:第一、在printf()的格式串中可以使用转义字符
82来换行,但在scanf()的格式串中,却决对不能使用转义字符
83;(还包括其他一些转义字符也不要使用,原因参见第三章提高部分)第二、scanf()的格式串后面的参数,是输入数据的存放地址,所以在通常情况下,变量名左边的取地址符号是决不能漏写的。例如:如果age是int型变量,那么scanf("%d”,&age);决不能写成scanf("%d",age);然而,对于printf()函数来说,以下语句却常常是一种误写:printf("%d”,&age);在VC++6.0编译环境下,这只是输出了变量age的内存地址。正确形式是:printf("%d”,age);这样才是输出变量agp的值。第三、对于printf。函数来说:printf("%d%d%d",nl^n21n3);很不好,三个变量的值在输出时根本区分不开;而printf("nl=%d^2=%d^3=%d,>,nl,n2,n3);却是比较好的形式。但是对于scanf。来说:scanf("%d%d%d”,&nl,&n2,&n3);通常却比较好,在输入三个数据之间只需输入一个空格将其分开(注);而scanf("nl=%djn2=%d»n3=%d”,&nl,&n2,&n3);却非常不好。你必须先从健盘键入这些字符之后,才能输入变量m的值…,整个输入过程非常麻烦,完全是自讨苦吃。不信你可以亲自试一试。第四、printf。的输出项既可以是表达式和变量,也可以是常量和函数调用(注意:其实这些本质上都是表达式);而scanf。的输入项通常只能是变量的地址,决不能是通常的表达式。4.9运算符在机器语言中是使用操作码来命令计算机做什么事。在C语言源程序中,怎样才能表达,我们想对以变量或常量形式出现的数据进行何种运算呢?这就要使用运算符。用运算符把各种运算量(变量、常量、函数调用等)结合起来,构成表达式(一),用这种形式化、抽象化的方法来告诉编译程序,我们想要计算机对哪些数据、依照什么样的顺序、执行哪些种类的运算,请编译程序将其翻译成(与某类具体机器有关的)机器语言指令。
84C语言中的各种运算符参见附录。C语言中,最常用的运算符分为三大类:1、算术运算符(见下表4.4)2、关系运算符:有大于>、大于等于>=、小于<、小与等于<=、等于==、不等于!=一共六个关系运算符,用法见第4章3、逻辑运算符:有与&&、或||、非!一共三个逻辑运算符,用法见第4章各种算术运算符见下表运算符运算举例结果-(一元运算符)负号-a(a的值为3)-3+加11+516-减11-56♦乘法11*555/整数除法(右边不能是0)11/52(这是整数除法)/实数除法(右边不能是0)11.0/5.02.2(这是实数除法)%取模(右边不能是0)11%51(11除以5的余数)表4.4各种算术运算符一元运算符只需要一个运算量;而二元运算符需要左右两个运算量参与运算,才能得到运算结果。使用算术运算符,有以下几点需要注意:1.初学者最容易漏写乘法运算符(比如:将2*x*y,误写为2xy)。2.不要把实数除法误用为整数除法(比如:错把1.0/3.0写成1/3»1/3的值是0)。-般的,如果m和n都是正整型量,则结果是舍弃了小数部分的整数商。3.不要将数值。作为除数,这将导致程序无法运行下去而崩溃。4.取模运算符%两边的运算量,都要求是整型量,不能是实型量。注意1:整除运算符和取模运算符的运算量不要使用负整数。因为C89标准将负整数的商和取模值的大小交给编译器自行决定,因此在不同系统中,使用负整数进行整除和取模运算得到的结果可能不一样。注意2:要确保程序运行时的运算结果不会溢出(即不超过数值的取值范围)。取模运算符“%”得到的是整数除法的余数。取模运算符“%”的作用非常大,在后面章节你能看到,在处理一些周期性问题或者将一个多位数值分解成一个个的单个数字时,都离不开它。延伸与拓展:运算符的技术内幕C语言中的运算符共有四十三种(见附录:运算符的优先级和结合性)。运算符规定的运算,最终都将由编译程序翻译成的机器指令来具体执行。运算符与运算类的机器指令之间并不是一一对应的。有些运算符规定的运算,用一条机器指令即可实现(比如第章要学到的i++、T等);另一些运算符指定的运算,需要多条机器指令来实现(比如第4章的条件运算符和第5章的逗号运算符等)。一种运算符随着运算量的类型不同,可以转换成不同的机器指令(比如加法运算符+在用于整数加法和实数加法时转换成的机器指令是不同的)。4.10表达式
85所谓表达式(或子表达式),是用一个或多个运算符将运算量连接起来的可以计算出一个确定值的式子。运算量包括常量、变量、函数调用和子表达式。在表达式中,还可以使用圆括号来改变运算符固有的先后运算顺序——圆括号内的子表达式优先进行运算。所有命令型高级程序设计语言中,最常用的表达式分为3类,算术表达式,关系表达式和逻辑表达式。算术表达式将在下一节讲解,后两类表达式将在第4章介绍,其他一些表达式将陆续在后面章节进行介绍。一些类型的表达式举例如下:(5.0/9.0)*(f-32)〃算术表达式a*a+b*b-2*a*b*cos(alfa)〃算术表达式(-b-sqrt(b*b-4*a*c))/(2*a)〃算术表达式注意:2*a加上了括号i<=20〃关系表达式ch!='\0'〃关系表达式(x>0)&&(x<=20)〃逻辑表达式单个常量、单个变量和单个(有返回值的)函数调用,其实都是最简单形式的表达式(比如变量X,可以看成是表达式x+0)O需要特别强调的是,出现在表达式中出现的所有变量,都应当是已经被初始化了的。否则,通过表达式计算出来的值,就一定是垃圾数据。在C语言程序中,在一个表达式中最好使用同一种类型的运算量(比如:同为int型的变量或常量),但一个表达式所使用的运算量的类型也可以不同,就涉及到了类型之间的转换问题。关于表达式中的类型转换这个比较复杂而又重要的问题,请参见下一章。・调用printfQ函数输出表达式的值:printfO函数不仅可以输出变量的值,还可以输出表达式的值。调用方式如下:printf(参数1,参数2,参费3,……,参数n);其中参教/是用双引号括住的格式控制串,参费2参戏,……,参数n是输出项列表。输出项通常情况下是表达式,变量只不过是一种最简单的表达式而已。例如:假设有变量定义:inta=l,b=2,c=3,d=4;printf("a+b=%d,c*d=%d,c%%b=%d
86",a+b,c*d,c%b);在屏幕上的输出为:a+b=3,c*d=12,c%b=l注意:在格式字符串中表示要输出一个字符%,必须用两个%连着写。・表达式用作为函数调用时的参数比如:sqrt(x+2.5),表示以x+2.5这个表达式的计算结果作为参数,进行开平方库函数的调用。
87延伸与拓展:表达式的技术内幕例如:C语言源程序中的整型表达式m+n,被翻译成机器指令后,可能就是由以下3条指令序列构成的:1.从内存中取变量m的值到CPU的某个寄存器;(取数指令)2.从内存中取变量n的值到CPU的另一个寄存器;(取数指令)3.命令算术逻辑单元将两个整数相加,将和存到某个寄存器中;(整型算术加法运算指令)也就是说,在源程序语句被翻译成机器指令时,一个表达式中常常蕴含了许多条机器语言的取数指令和运算类指令(有副作用的表达式还包含存数指令)。表达式是使用命令型高级语言的编程效率远远高于用机器语言(或汇编语言)编程效率的重要原因。4.11算术表达式所谓算术表达式,是用一个或多个算术运算符将运算量(包括常量、变量、函数调用)连接起来的,可以计算出明确值的式子。比如:b*b-4*a*c、a*b*sin(alfa)/2.0算术表达式是用得最多的一种表达式,凡是要计算一个数值的工作,大多都离不开它。4.11.1算术表达式中的运算符的优先级:如果算术表达式中有两个以上算术运算符,各运算符按照什么先后顺序进行运算?这要分以下三种情况来讨论:(1)由运算符固有的优先级来确定。在C语言的全部算术运算符中,取负数-这个一元运算符优先级最高;乘*、除/、取模%这些运算符的优先级其次;加+、减-运算符的优先级最低:也就是说,C语言中的算术表达式中的运算顺序,除了增加了一个与乘除运算优先级同样的取模运算符%之外,还是完全遵守我们在中小学在算术式和代数式中就用过的“先乘除、后加减”的运算顺序。比如:3.7+4.1*-12.0,即先做取负数运算-12.0,再做乘法运算得到-49.2,最后再做加法运算,得到-45.5。(2)在一个算术表达式中,如果出现了多个同一优先级的二元算术运算符,则是按丛左到有的顺序进行运算的。比如:45/2%6*3因为该式中从左到右依次出现的算术运算符是/、%、*都是同一优先级的,所以它的运算是从左到右依次进行的:点评:术语简—运算符的“结合性”所谓运算符的“结合性”其实指的是运算符与运算量的结合。是对出现在表达式中的具有同一优先级的连续多个运算符而言的,用来规定这些运算符之间与运算量的先后结合次序。先与运算符结合的运算量——构成了一个子表达式——先进行运算。所以我们可以说,三不算术本算符的结合性建丛左到有的(或称为具有左结合性)。在下一章将看到,有一些运算符的结合性是从右到左的(比如复合赋值运算符)。
88(3)如果以上两种方式构成的算术表达式都不能满足你对运算顺序的要求,你可以使用圆括号来改变运算符固有的运算顺序。比如,我们想求三个变量之和的平均值,我们不能用nl+n2+n3/3.0来表示,而必须加上圆括号:(nl+n2+n3)/3.0在C语言中,可以使用圆括号括住表达式中的一个子表达式,用来强制改变运算符固有的运算次序。比如:(3.7+4.1)*12.1那就是要先做子表达式3.7+4.1的加法运算,然后再用子表达式得到的运算结果7.8再做乘法运算:7.8*12.1。但是,在C语言中,千万不能用中小学学过的方括号“广、"]”和花括号来表示要优先进行运算的(子表达式)部分。因此,以下式子在C语言中是错误的:2.3-{b*炉(3.7+b)]}/6.3o应当用多重圆括号0来取代D和{}。正确的表达式应当是:2.3-(b*(y-(3.7+b)))/6.3在表达式中,可以使用多重嵌套的圆括号,来强制改变运算符的固有的优先次序,运算顺序审内到处。在以上表达式中,最先做的是3.7+b这个子表达式的运算,然后依次做外层括号的运算。在编程时,不记得表达式中运算符的优先级时,最直观而又最省事的办法不是去查看运算符的优先级表,而是直接在表达式中添加圆括号。适当增加“多余的”圆括号,将使得表达式中的先后运算顺序更容易看清楚。4.12赋值语句通常的表达式不会改变变量的值。那么,如何改变变量的原来值呢?如何通过已知的变量求出未知变量的值呢?这就要用到赋值运算符“="(以后将其简称为赋值号)构成的赋值语句。赋值运算符是具有副作用的运算符,所有可以改变变量值的运算符,都称为具有副作用的邙算符(第5章将要介绍的自增运算符++和自减-运算符也是具有副作用的运算符)。赋值语句的格式是:变量-表达式;赋值语句的工作流程是:1.计算出赋值号“=”右边表达式的值;2.将此值存放在赋值号左边的变量中。比如sales=3693.89;其作用就是把3693.89存入变量到sales中。注意1:赋值号“=”的左边只能是单个变量,不能是常量(3693.89=sales;是错误的),也不能是函数调用(sin(x)=a/2.0;也是错误的)。注意2:赋值号右边表达式中的变量——如果有的话——仅仅是取出它们的值来参与表达式规定的运算(前提是:右边的表达式中不出现具有副作用的运算符),变量的值通常不会改变。而赋值号左边变量的原来值,将会被表达式计算出来的新值的有△而“覆盖”掉。问题1.已知华氏温度的数据在变量f中,如何求出相应的摄氏温度并把它存放在变量c中?
89已知转换公式是:C=(5/9)X(F-32.0),C表示摄氏温度,F表示华氏温度。问题2.已知变量x的当前值是36,变量y的当前值是72。请问执行赋值语句x=y;之后,x和y的值分别是多少?请问:如果执行的赋值语句是y=x;(而不是*=齐)之后,x和y的值分别又是多少?点评:一类极为重要的、常用的赋值语句形式如:x=也含有变量x的芨送式,•(x可以是任何基本类型的变量)的赋值语句,大量地、频繁地出现在程序中。它表示的是一种迭代关系:即指明了如何由变量x的一个老值取出来参与表达式所规定的运算,(将运算结果又存入到赋值号右边的变量x之中)最终得到变量x的一个新僖。这一点与大家在数学中学过的方程式有着很大的区别。比如:i=i+l;和sum=sum+k;(k可以是任意值),在数学中是无解的方程式,但是在命令型高级语言中,我们却经替帚要使用这种类型的赋值语句。问题3、已知某人的工资额保存在float型变量salary中,如何将其增加百分之30,并且仍然保存在变量salary中?答:可采用赋值语句salary=salary*1.3;(注意:不能用salary=salary*130%;)o此处删除了三行请读者注意:数学中的方程式是不能直接转变为赋值语句的,只有公式(等式左边是单个的未知量,等式右边是仅仅包含已知量的代数式)才可直接转变为赋值语句。解方程通常必须由编程者亲自做。参见下一章例题3.2鸡兔同笼问题。例题4.1已知三角形的两边及其夹角,求三角形的面积类型必修题趣味性*难度*一级算法1.输入三角形的两边X,y及其夹角alfa2.根据已知量求三角形面积,赋给变量area〃以后用符号-area表示赋值给area3.输出三角形的面积其中第2步需要进一步求精:第2步的进一步求精:根据求三角形求面积的公式,可转化为以下赋值语句:area=x*y*sin(alfa*3.1416/180)/2.0转化成C语言的程序:1#includc
90”);11scanf("%f%f%f1,&x,&y,&alfa);1213/*求三角形面积*/14area=x*y*sin(alfa*PI/l80)/2;
911516/*输出三角形的面积*/17printf("三角形的面积为:%f
92n,area);1819return0;20)说明:c语言中三角函数的库函数都使用弧度度量角度,而在日常生活中使用度数度量角度;因此在调用sin()时,必须将度数转化为弧度。头文件math.h不能漏写。问题1.使用第3行符号常量有何好处?问题2.第2行是否可以省略?为什么?问题3.漏写第7行会如何?问题4.第7行是否可以下移到第12行?为什么?问题5.是否可将第10行和第11行互相颠倒位置?问题6.sin(alfa*PI/180)能否写成Sin(alfa*PI/180)?或sin[alfa*PI/180J?为什么?习题1.编程题:已知三角形的两边及其夹角,求对边长度。编程练习:已知三角形的两边及其夹角,求对边长度。例题4.2安全密码生成器人们平常喜欢用纯数字序列来作为密码,比如用生日、特别的日子、电话号码等,这种密码很不安全,容易破解。请编一个程序将6位纯数字的原来码转变为由小写英文字符组成的比较安全的密码。查找ASCII表可知:数字字符的ASCH码'0'是48——'9'是57英文字符的ASCH码'a'是97——'z'是122分析:数字字符‘0‘到’9'的ASCII码值在48——57的范围,26个小写英文字符中随意任取连着的10个字符就可与之匹配。例如我们取‘d‘到‘m'。这10个字符的ASCII码的值是100——109之间。如果把’0‘到‘9‘顺序与‘d'到'm'一一对应,那么只要把每个输入的值加上52即可。编写程序如下:#include
93H,ch6,ch5,ch4,ch3,ch2,chl);return0;)
94程序运行后,人机对话过程如下:请输入6位数的纯数字123456/建议使用密码:jihgfe有了这个程序,你就可以用你的生日或熟悉的电话号码等纯数字来生成比较安全的密码To这个程序有很大的改进余地。请读者自己进一步完善它,轻松生成比较好记(你只要记住你熟悉的纯数字)又比较不易破解的密码。4.13提高部分:4.13.1对字符常量的进一步说明在c语言中,字符型量其实就是一种小的整型量。因此,ch='a';和ch=97;这两条赋值语句通常运行效果是一样的,因为字符公的ASCII值就是97。由于英文常用字符集中的字符数(比如ASCII字符集中有128个字符)通常比C语言字符集中的字符数(共有92个)多,C语言中允许使用一种特殊形式的字符常量来表示英文常用字符,这就是以反斜杠开头的转义字符,该形式将反斜杠后面的字符转变成一种另外的意义。转义字符的名称由此而来。最常用的转义字符是'
95',用来表示回车换行。其他常用转义字符见表2.3。转义字符形式功能
96回车并换行(相当于按下enter键)\t横向跳格\v竖向跳格\b退格(相当于按下Backspace键)\r回车(即回到本行的最左边)\f走纸换页w反斜杠字符\,单引号字符双引号字符\a响铃或扬声器蜂鸣\o字符串结束符,其ASCII编码值为零\ddd1〜3位八进制所代表的字符\xhh1〜2位十六进制所代表的字符表2.3常用的转义字符表在源程序正文部分书写转义字符与普通字符类似,都必须用单引号括住,比如‘
97'。但是在格式化输出库函数printfQ的格式控制串或字符中书写转义字符,不要用单引号括住,比如printf("hello
98”)。输入一个字符时也不用单引号括住。有少量计算机不是使用ASCII码来表示英文字符的。而是使用别的编码标准。为使你编出来的程序也能够在这一类的计算机上运行,你就不能在程序中将用到的字符常量与一个具体的ASCII码值关联起来。4.13.2变量地址的运算符&和变量占用的内存字节数的运算符sizeof问题:已知在VC++6.0编译环境下,int型整型变量num的内存单元占据了4字节大小的
99内存空间。假设所对应的内存字节的地址分别是二进制的10001100、10001101.10001110>lOOOllllo请问变量num的地址是多少?变量的值与变量的地址有关吗?答:变量的地址,就是所有这些连续字节地址中的最小地址——100001100o而变量的值就是存放在这四个连续字节中的,总长度为32位的二进制位串所构成的数(参见本章提高部分:有符号整数的二进制补码表示)。如何得到一个变量的地址呢?运算符&就是取变量地址的运算符。在例题2.2第行14行格式化输入函数scanf的调用中,我们用到了此运算符&来得到变量的地址。我们也可用此运算符使用printf函数来输出变量的内存地址,不过通常这样做没什么意义。如何知道一个变量在内存空间中占用的字节数?可以使用运算符sizeof(变量名)。如何知道一种类型的变量在内存空间占用的字节数?可以使用运算符sizeof(类型名),例如:通过sizeof(int)可得到int型变量在内存中占用的字节数。例题2.4#include
100M,sizeof(year));printf("变量last的值二%d,H,last);printf("变量last的地址二%pJ,&last);printf("变量last的字节数二%d
101",sizcofQast));printf("变量chi的值二%c,chl);printf("变量chi的地址=%p,n,&chl);printf("变量chi的字节数二%d
102",sizeof(chl));return0;}说明:要输出变量的(十六进制的)地址,必须要用格式符:%p。练习:根据此题运行结果,请你制作并填写一张类似表3.2的表格——即把每个变量占用的字节数、变量的地址、变量的值,都填写在此表中。变量名及占用的字节数变量的地址(十六进制)变量的值year(int)40012FF7C26last(int)40012FF78(不可预知的垃圾数据)chi(char)10012FF74'd'的ASCII码表4.变量的地址和变量取值图注意:变量的地址(类似于自动冰箱格子的编号)与变量所代表的内存中所存放的数值(类似于自动冰箱这一格中所存放的原材料)之间的重要区别。
103对于以上这些十六进制的地址,结论是:变量year所对应的连续4个字节的内存地址分别是0012FF7c到0012FF81;变量last所对应的连续4个字节的内存地址分别是0012FF78到0012FF7B;变量chi虽然只占用一个字节,但由于内存对齐的原因,从0012FF75开始有三个字节的内存空间浪费了。练习2:修改例题2.4中变量的初始值,看看打印出来的各种数据有何变化。习题是非判断题1.两个整型量(包括常量和变量)m和n相除m/n,所得的结果是截去了小数部分的整数商。2.在调用数学库函数时,可以不包含头文件math.ho3.在C89标准中规定:定义可以出现在函数体中的任意位置。4.表达式中出现的变量,可以是未经过初始化的变量。5.两个运算量之间的乘号有时可以省略不写,有时可以用号代替"*”号。6.在赋值语句中,赋值号的左边一定是一个单个变量,不能是常量,也不能是一个函数调用。7.C语言字符集中的同一个字符,出现在源程序的不同位置,其含义可能不同。(考虑圆括号和%这两个字符)8.C语言程序中,凡是可以出现常量的地方,都可以用一个表达式来替代。9.赋值语句使得我们,既可以通过已知变量求得未知变量的值,又可以通过变量的老值求得该变量的新值。10.语句x=x+l;是错误的,因为运算符的两边不相等。11.没有副作用的表达式,永远不会改变变量的值。12.内存中变量的值可以重复取出任意多次来使用,变量的值都不会变。一旦存入(或输入)一个新值到该变量中,变量的老值将不复存在。13.库函数名通常都是由小写英文组成的标识符,而C89中的关键字可以用大写字母构成。14.单个常量、变量、有返回值的函数调用都是表达式。15.printf调用时,格式控制串中转换说明的数量要与输出项的数量一样多,类型也要一一匹配。16.“程序"又可称为"代码",“源程序”又可称为“源代码”。17.源程序就是数据,完成同一任务的机器语言程序就是计算机运行编译程序后得到的信息。二选择题4.不是C语言提供的合法关键字是()。A.switchB.printfC.caseD.default5.以下选项中合法的用户标识符是A)longB)_2TestC)3DmaxD)A.dat6.下列各项字符序列中,合法的变量名是oA)2e3B)youC)*yD)float7、下列可以正确表示字符型常量的是()。A、'\tB、"a"C、"
104"D、2978.已定义c为字符型变量,则下列赋值语句中正确的是()A)c='97'B)c="97"C)c='a'D)c="a"
1058.C语言中运算对象必须是整型的运算符是()A)%B)/C)=D)*9.在C语言程序中,表达式8/5的结果是()A)1.6B)1C)3D)010.在C语言程序中,表达式5%2的结果是oA)2.5B)2C)1D)3三、输入输出库函数的调用1.设float型变量radius表示圆球的半径,float型变量volumn表示圆球的体积。请指出以下printf函数调用语句中的错误。1)printf("圆球的体积是:%f立方米,圆球的半径是:%f立方米
106”,radius);2)printf("圆球的体积是:%d立方米
107”,volumn);3)printf("圆球的体积是:%.2f立方米
108,volumn");4)print("圆球的体积是:%f立方米
109”,&volumn);5)填空题1)如果要用一次printf。函数调用,依次输出n个变量(或表达式)的值,那么在函数调用的格式串中应当有一个格式符,并且格式符的中的转换说明要与输出项的——一匹配。2)如果三个待输出的变量的类型依次分别是int、char、float,那么格式串的形式通常应当是:"*%_*%_%_*”。其中的星号代表任意多个普通字符或转义字符。6)已知age是int型变量,ch是char型变量,heigh是float型变量,请用适当的转换说明符d、f、c填空:1)scanf("%_",&age);2)scanf("%_”,&ch);3)scanf("%_",&heigh);4)四、将下列代数式转换成相应的c语言表达式1.6y/5x-3a22.|y+lg(V+l)+3e'+ln5|+sin(25)*五、能力挑战题改错:先阅读,然后通过上机调试,请找出以下程序中的所有错误来。/*作者:何勤编写日期:2009-12-25功能:输入圆球的半径,求圆球的体积#include(stido.h)#dcfinedPI3.1416;/*符号常量*/Intmian<>;(print(“请输入圆球的半径
110")Float,r;v;scanf("%f
111”,r)V=(4/3).Pl.r3;Print(“圆球的体积是:%d立方米
112,"&v);
113return0;)六、阅读、编译并运行例题2.1七.编程题1.从健盘输入圆锥体的半径radius和高度height,计算其体积volumn.(其中圆周率要求用符号常量表示)。2.编程:输入年利息、存钱的年数和金额,计算到期本金和利息之和(不计复利)。3.编程:已知一元二次方程ax2+bx+c=0的系数a,b,c(a,b,c由键盘数入),并且假设b2-4ac>0,求方程的两个实数根。*4.编写一个程序,让用户输入一个字符,程序显示此字符所对应的ASCII码。要求显示格式如下:输入的字符是:对应的ASCH码是:*5.编写一个程序,要求用户输入一个ASCII码,程序显示此ASCH码所对应的字符。要求显示格式如下:输入的ASCII码是:对应的字符字符是:6.参照例题2.2,更安全的密码。请将6位纯数字好记的明码转变为比较安全的混合密码。要求6位密码由两位大写英文字符、两位小写英文字符和两位数字字符构成。7.编程:学生个人生活小管家。输入本月总收入,输入预算支出项,显示输出本月预算情况。支出分为:就餐费、手机费、日常生活用品费、交友娱乐费。练习1:将例题2.3中第7行的语句printf("%d%f%c
114",num,x,ch);分别改为以下的形式,重新编译运行,看看会怎样?错在哪里?(1)printf(M%d%f
115M,num,x,ch);(2)printf(M%d%f%c
116H,num,x)(3)printf(n%d%f%c
117,num,x,chH);(4)printf(M%d%d%c
118M,num,x,ch);练习2:把例题2.1中第4和第6行删掉,看看是否会输出垃圾数据?问题1.第8行的语句能否写成:(a)scanf("%F',tenement_area);(b)scanf(,,%dH,&tenement_arca);(c)scanff1%f,&tenement_areaM);为什么?问题2:如果漏写了例题2.1中第8行的scanfO调用语句,结果将如何?问题3:能否将变量tenement_arca定义为int型?为什么?
119第五章顺序结构程序设计——概念比细节更重要,但细节有时决定成败。本章5.5节之后(包括5.5节)的内容,初学者都可在学完第8章之后再来学习。把这些内容安排在此处,只是为了C语言基础语法知识讲解的系统和连贯;有利于学过C语言的读者,也有利于将来在编程时通过本书查找相关知识点。在例题5.3中,介绍了一种在纸面上运行算法的算法走查方法,这是本章的一个重点。5.2用计算机求解问题的步骤、5.3逐步求精的伪代码表示算法、这两节也是本章的重点。本章的难点是:各种数据类型之间的转换5.1语句执行的顺序性机器指令通常是按照存储在内存中的先后顺序依次执行的。高级语言的每一条语句都将翻译成若干条机器指令。第一条语句翻译成的机器指令一定位于第二条语句翻译成的机器指令之前,比如:第1条语句第1条指令第2条指令第2条语句第3条指令第4条指令第5条指令第3条语句第6条指令第7条指令因此我们可以说,高级语言程序的语句通常也是按照书写的先后顺序在计算机上依次执行的,即:按照先后顺序依次执行第1条语句、执行第2条语句、执行第3条语句,……等等。后面将会看到,很多时候语句的先后书写顺序不一样,程序运行的结果将大不相同。5.2用计算机求解问题的步骤:到目前为止,我们学习了很多的有关C语言的基础语法知识。让人感觉千头万绪、杂乱无章、不知从何处下手编程。其实在进行编程工作时,只需重点关注变量、表达式、赋值语句(等少数几类语句)、输入数据到变量和输出变量(或者表达式)的值。其中,变量成为我们解决实际编程问题时的核心和主线。遇到编程问题,思考的要点是:1、对此问题需要定义哪些类型的几个变量;2、哪些变量的值需要输入;〃即已知的变量需要初始化3、如何根据已知的变量构造出合适的表达式,从而用赋值语句来求出未知变量的值;4、将求出的变量(或表达式的)值输出。如果需要对一些语句进行有选择地执行或者是重复执行,还需要用到第4、第5章所学的知识。我们在前几章所学过其他知识,主要是起着一个背景作用,不必死记硬背。这些知识可以使得我们在编程时少犯错误,出现错误时使我们有能力尽快将其查找出来。
120用计算机求解问题,解决问题的一般过程是:1.用普通语言简要并尽可能精确地叙述问题;2.问题中已知的量有几个,其中有几个量随求解的具体应用场合会发生变化?——这些量应当定义为变量。有哪几个量不会发生变化?——这些量可用符号常量或数值常量来表示。其中会变化的已知量一般应当在程序中用到此数据之前,通过输入库函数调用(有的高级语言使用输入语句,)进行数据输入。3.问题中需要求解出的有几个量?——这些量也应当定义为变量。4.从已知的量如何得到需要求解出的量?有何公式可以利用?有何方程式可以利用?如果是公式,就可以直接将其转换为赋值语句,只需把公式右边的数学表达式转换为高级语言的算术表达式即可。如果是方程式,通常则需要你自己亲自将方程式求解,得到最后的公式,然后将其转换为赋值语句,只有到了这一步,才可以将工作交给计算机做。5.如果从已知的到要求的最终结果需要一些中间变量,则需要在程序中定义这些中间变量,并且得到怎么从已知的量到中间变量的值的公式,最终由已知量,中间变量得到所要求的最终结果的公式,将所有这些公式转换为赋值语句。6.将结果输出。其中最为困难的往往是第4步和第5步——即如何从已知的求出未知的。这两步通常又称为寻找求解问题的算法。5.3逐步求精的伪代码表示问题求解的算法有多种方法,其中最为常用的是伪代码和流程图。但由于流程图只适合解决规模较小的、比较简单的问题,虽然初学者易于理解和掌握,表达算法也比较清晰,但在程序员和真正会编程者中并不流行——因为它不利于对算法的构思、修改和调整(仅适用于表达比较简单的算法),所以本书不作介绍。养成了用流程图来表示算法的习惯后,人们很难再转变成用其它更好的方式去构造算法,所以还不如一步到位。本书使用的是用逐步求精的伪代码来表示算法。所谓伪代码没有严格的规范(所以也比较灵活),但其中有一些结构要素,可以用一些比较规范的方式来表述对问题的计算和处理流程。一级算法只是解决问题的一个轮廓。有些复杂问题,只根据一级算法还难以直接写出(C语言的)源程序。这时可对一级算法进一步细化(称为二级求精),将它其中的某些步骤,扩展成更详细的步骤,...直到可以很容易写出(C语言)程序的语句时为止。对于某些人是很显然的算法描述,对于其他人可能并不显而易见。因此,算法求精的过程和步骤是因人而异的。你的编程经验越丰富,算法求精的步骤就可能越少。不过,算法求精的步骤太少也不一定是好事。因为程序员的一个良好习惯,就是把伪代码表示的一级算法(算法复杂,还可以到二级求精),转变为源程序中的注释。注释太过简洁,会加大自己和别人阅读程序的困难。逐步求精的伪代码,其实质精神,就是模仿人们在做一个大的任务时,习惯于首先将其粗分为几个一级任务,然后再来分别考虑,每个一级任务如何完成?不太明确的一级任务还要再细分为若干个二级任务,……等等。学习用伪代码来表达自己的编程思路和算法,笔者的体会是没有什么捷径让你能够迅速掌握它,开始只能是去模仿,去领悟,时间长了,看的和模仿的算法多了,慢慢自然就会用了。例题5.1求三个数的和及平均值类型必修题趣味性*难度*变量安排:3个要输入的数用3个变量al,a2,a3;三个数的和用1个变量sum,
121平均值用1个变量average,所有变量都用浮点类型定义。伪代码的一级算法:1.输入这三个数al,a2,a3;2.求这三个数的的和sum及平均值averagp;3.输出sum,average;其中第二步由于不能直接转化成语句而需要进一步求精。根据求和及求平均值的以下两个代数式:sum=al+a2+a3average=sum4-3只需将其转化为赋值语句即可。第2步的二级求精:2.1sum=al+a2+a3;/*求这三个数的和2.2average=sum/3.0;/*求这三个数的平均值转化成C语言程序:/*求三个数的和及平均值*/1#include
122");8scanf(,&al,&a2,&a3);910/*求这三个数的的和sum及平均值average*/11sum=al+a2+a3;12average=sum/3.0;1314/*输出sum,average*/15printf("三数之和二%f,平均值二%f
123”,sum,average);16return0;17)注意:运行此程序时,要用键盘输入三个数据给变量al,a2,a3时,三个数据之间要用空格隔开。输完后,按下回车键,程序才会继续运行下去。编程练习1.输入两个数,求两数的平方和。点评:将表示算法的伪代码,转化为程序中的注释伪代码形式的一级算法(有时还包括二级求精),在程序中转变为注释,使得程序的可
124读性比较好。这是一种良好的编程习惯。例题5.2鸡兔同笼问题已知鸡和兔的总头数和总脚数,求鸡有多少只,兔有多少只?类型必修题趣味性**难度*伪代码的一级算法1.输入总头数heads,总脚数feet2.求鸡的只数chicken和兔的只数rabbit3.输出变量chicken和rabbit的值其中只有第二步需要求精根据二元一次方程组,可知四个变量heads,feet,chicken,rabbit之间的关系为heads=chicken+rabbitfeet=2Xchicken+4Xrabbit解此方程组,可得到以下两个公式:chicken=(4Xheads-feet)/2rabbit=(fcct-2Xheads)/2将此公式转化为赋值语句,考虑到输入的总头数和总脚数不一定能得到整数解,因此将变量chicken(表示鸡的只数),rabbit(表示兔的只数),定义为float型变量。二级求精2.1chicken=(4*hcads-fcet)/2.02.2rabbit=(feet-2*heads)/2.0转化为C语言程序1#include
125”);9scanf("%d%d”,&heads,&feet);1011/*求鸡的只数chicken和兔的只数rabbit*/1213chicken=(4.0*heads-feet)/2.0;14rabbit=(feet-2.0*hcads)/2.0;15/*输出变量chicken和rabbit的值1617printf—鸡的只数为:%d只,兔的只数为:%d只
126",chicken,rabbit);18return0;19)问题1:如果不解方程组,而直接将其转变为赋值语句,也就是用heads=chicken+rabbit;feet=2*chicken+4*rabbit;
127取代第13和第14句,结果会如何?答:赋值语句右边的表达式中出现的变量,必须是已经初始化了的变量。赋值语句是用赋值号右边的表达式求出的值,存放到赋值号左边的变量表示的存储单元中。所以,这两句是不正确的,但大多数编译程序不会报告发现了错误。程序运行时,会取出内存单元中变量chicken,和rabbit中的垃圾数据参与运算。经过运算得到两个错误数值,去改变变量heads和feet的原来正确值。换言之,数学中的方程式不能直接转变为赋值语句,只有公式才可以直接转变为赋值语句习题:在学了if语句后,重做此题,把输入数据可能不符合要求的情况解决掉。5.4睑证算法的方法以及重要性笔者认为,采用画变量(内存单元)取值变化图验证算法(参见对例题3.3的算法验证)的技能,是想成为程序员或编程高手的一种极为重要的、最基本的技能。使用这种技能有两方面的重大好处。第一,大大减少不必要的编程工作量。在写出算法而尚未编出程序时,就可以检查算法是否存在逻辑错误或边界错误(数组的边界错误,或者循环次数多一次或少一次,请参见第五章循环、第六章数组)、程序的流程是否构造正确等,大大减少了不必要的编程工作量。这类似于在装修前画出立体效果图,让用户感受一下在其中生活是否会感到舒服一样。如果用户感觉不好,可重新修改设计,大大减少了实际装修完后才返工的极大损失。毕竟重写和修改伪代码要比重写或修改程序省事多了——尤其是对一个大型程序。第二,可使读者读懂较难程序或算法的能力得到极大的提高。而强大的阅读程序和算法的能力,是一个人能够否成为编程高手的最重要的、决定性因素之一。希望读者把这一重要的技巧至少用到分析和阅读几十个你认为比较难懂而又比较有趣的算法或程序上,使之成为你的编程工具箱中的一件“利器”。并且养成一种良好的编程习惯,在用伪代码写出算法后,不要急于把它转换成高级语言的程序,而是用这种在纸面上运行、走查的方法去验证算法,尽可能在早期找出并修改算法中的逻辑错误或边界错误。当然,即使通过这种验证,也不能保证这个算法是完全正确和完善的。对于语法错误更是无能为力。但这种验证的重要价值,我们绝对不能否认。如果不用此方法来进行验证,在写出算法后就立即编程。仅仅依赖于运行编译程序来进行简单调试,你很可能最终会得到一个没有任何语法错误和运行时错误的程序。然而,此程序得到的结果要么可能是完全不正确的(逻辑错),要么此程序存在边界错误。如果是逻辑错误,则整个编程和调试工作完全是白费力气。在编写大型程序时,这种时间的浪费是一个很严重的问题。例题5.3将一个三位整数反向后输出类型必修题趣味性*难度**一级算法1.输入一个三位整数一》n2.通过利用学过的算术运算符整除/和取余数%进行分解,分别求出此三位整数n的百位数n3,十位数n2和个位数nl3.反向后的三位整数为num=nl*100+n2*10+n34.输出此三位数其中第2步由于不能立即写出C语言的语句,需要进一步求精:2.1求出n的百位数:n3=n/1002.2求出n的十位数n2=(n-n3*100)/10
1282.1求出n的个位数nl=n%10算法验证:执行算法第一步,假设将一个任意指定的三位整数,比如:将315输入到变量n中,用于验证上述算法过程的正确性。执行算法第1步后,变量的内存单元取值将如图3.1所示:nn3n2nlnum315不确定不确定不确定不确定图3.1执行算法2.1步的赋值语句:n3=n/100;先计算表达式n/100(即315/100),可得到3,将其存入变量n3中。算法2.1步执行完后,变量的内存单元取值将如图3.2所示(变量n3的值发生了变化):nn3n2nlnum3153不确定不确定不确定图3.2执行2.2步的赋值语句:n2=(n-n3*100)/10;先计算表达式(n—n3*100)/10,将n和n3的当前值从内存中取出代入到表达式,进行表达规定的运算:(315—3*100)/10,得到结果1,存入到变量n2中。算法2.2步执行完后,变量的内存单元取值将如图3.3所示(变量n2的值发生了变化):nn3n2nlnum31531不确定不确定图3.3执行2.3步的赋值语句:nl=n%10;后,变量的内存单元取值将如图3.4所示(变量nl的值发生了变化):nn3n2nlnum315315不确定图3.4执行算法第3步规定的赋值语句的运算:num=nl*100+n2*10+n3;之后,将得到的结果513存入num中,变量的内存单元取值将如图3.5所示(变量num的值发生了变化):nn3n2nlnum315315513图3.5经过以上验证,315确实变成了513,算法逻辑上的确是正确的。按照以上方法真正在纸面上验证算法是否正确,只需用铅笔画一张图,随着语句的一条一条执行逐次用铅笔修改内存单元中变量的值即可。将上述经过纸面运行走查验证的算法,转化为C语言程序:1#include
1291intnum;/*结果变量*/2/*输入一个三位整数n*/3printf(“请输入一个三位整数
130");4scanf("%d”,&n);511/*分别求出它的百位数n3,十位数n2,个位数nl*/12n3=n/100;13n2=(n-n3*100)/10;14nl=n%10;
1311516/*反向后的三位整数为*/17num=nl*100+n2*10+n3;1819/*输出此三位数*/20printf("num=%d”,num);21return0;22)问题1:是否可以将第17行和第20句合并为下面这一句并删掉第6行?printf("num=%d",nl*100+n2*10+n3);答:可以,printf。函数调用时,格式串后的输出项可以是一个表达式。问题2:将第12行和第13行颠倒语句次序是否可以?你可用画变量内存单元取值变化图的方法得到你的结论。答:不可以,变量n3会出现初始化错误。第13行表达式中的n3依赖12行的语句初始化。问题3:将13行与14行互换,是否可以?答:可以,因为两条赋值语句之间的变量并没有依赖关系。问题4:是否可以将所有变量n,n3,n2,nl都用float型来定义?答:不可以;程序12行、13行的赋值语句的表达式,都要求是整除运算。14行的n%10,运算符%也要求变量n是整型,而不是实型。习题:输入一个四位整数,将其前两位和后两位互换后输出。比如:输入2568,输出68255.5自加运算符++和自减运算符我们经常会要用到i=i+1;、尸j-1;这样的赋值语句。但这样书写不太简洁。我们可以用更为简洁的方式:i++(或++i);、j一(或--j);oi++;或++i;都使变量i的值增加了1,相当于赋值语句i=i+l;j-;或--j;都使变量j的值减少了1,相当于赋值语句i++和i一称为后缀形式;而++i和--i称为前缀形式。两者都称为自加或自减运算符。例如:inti=7;|inti=7;i++;j++i;j=i;!i=i;左右两段程序执行的结果i值都为8,j的值也都为8。但是,把i++与++i组合在表达式或语句中就会出现区别。例如,放在表达式中:inti=7;x=i++;/*注意:这一句相当于:x=i;i=i+1;这两句*/y=i;的执行结果为:x为7,y为8。即后缀形式(i++等)是“先引用后加1(或减1)"o而
132/*注意:这一句相当于:i=i+l;x=i;这两句*/inti=7;x=++i;y=i;的执行结果为:x为8,y为8。即前缀形式(++i等)是“先加1(或减1)后引用"。如果i++或i一一出现在别的语句中,读者要注意:printf("a=%d”,a++);等价于以下两条语句:printf("a=%d",a);a=a+l;而:printf("a=%d”,++a);等价于以下两条语句:a=a+l;printf("a=%d”,a);也就是说:++或一出现在某个变量的左边,那么就要先改变该变量的值,然后再用变量的新值去做语句指定的其他事(比如参与表达式的计算或输出);如果:++或一出现在某个变量的右边,那就是先要用该变量的老值去做语句(或表达式)指定的其他事,然后再改变该变量的值(加1或减1)。自加和自减运算符的结合性是“从右到左"。比如:表达式++i-,相当于++(i-)-自加和自减运算符的运算对象只能是变量(而且常常是整型变量),而不能是表达式或常数。例如:7-或++(x-y)都是错误的。注意:自增自减运算符组合在表达式或语句中,会带来很多意想不到的问题,初学者要慎用—最好作为语句单独使用(如i++;或-i;}o点评:具有副作用的运算符和表达式与赋值运算符=类似,自增++和自减-这两个运算符也会改变变量的取值,因此也是属于具有副作用的运算符。凡是会改变变量取值的运算符称为具有副作用的运算符。具有副作用的运算符出现在表达式中,就可能形成具有副作用的表达式。这种类型的表达式在编程时一定要慎用,很容易出现意想不到的问题。参见本章提高部分。5.6赋值表达式・赋值表达式的概念:赋值语句去掉分号,就成了赋值表达式。赋值表达式也完成了与赋值语句一样的运算和存取数操作。不同之处在于,赋值表达式本身是具有一个计算值的,这个值与存放在赋值号左边变量中的值相等。我们可以通过以下程序亲自验证一下这个说法是否正确:#include
133运行此程序后,打印出的两个值是一样的。5.6多重赋值语句多重赋值语句所起的作用是为多个变量赋予同一个数值(最右边表达式计算出来的值)。例如:变量1=变量2=变量3=表达式;⑴上式相当于:变量1=(变量2=(变量3=表达式));(2)由以上论述还可得知,赋值运算符的结合性是从右到左的。但是,读者要注意:以下的用法却是错误的:inta=b=c=39;这并不是多重赋值语句。例题5.4用九行九列的字符和空格字符的组合,来构成并输出一个汉字:”王"。思路:用九个printfO语句,每个printfO语句输出这一行中的字符和空格字符。C语言源程序如下:1#include
134H);6printfC*
135》;7printfC*
136)8printfC*********
137M);9printfC*
138,f);10printf("*
139M);11printf(,,*********
140,*);12printf(”
141");13return0;)说明:此程序可在显示屏的最左边显示一个由“户号(包括和空格和空行)组成的汉字:“王但这个字太靠近屏幕左边,不太好看。请你修改此程序,让这个“王"字比较居中。延伸与拓展:计算机显示屏显示各种文字(包括图像)的内在奥秘计算机在屏幕上能够显示任何的文字,其道理与例题3.4是完全类似的。只不过在习题3.4中,用来组成汉字“王”的基本成份,是由字符(用作为黑点)和空格字符(用作为白点)组成的9行X9列的“点阵”组成,每个长和宽均为几毫米的字符(“*”或空格"”),构成一个“点,而计算机用来显示一个ASCII字符,一般用的是由8行X8列个像素——这种更小的点来构成字符的点阵。每个可显示的ASCII字符的这种点阵信息,通常全部存储在称为"ASCII字符集的点阵码”的字库中,它位于称作为“显卡”(显卡就是显示器的输出控制器)上的BIOS只读内存ROM中。可以根据字符的ASCII码值到这个"ASCII字符库”中查找要显示的点阵信息位于字符库的何处。
142类似于活字印刷到活字库中取一个活字字模放到印刷版面的某个位置上,如果把屏幕比作为印刷用的“白纸”,那么计算机的某一帧显示内存(显示内存通常也是在显示卡上)就可比作为由多个活字组成的“印刷版面:只要把一个字符的点阵信号(相当于一个活字字模)从内存的“字库”中取(即复制)出来,写到显示内存的某个位置上,覆盖掉原来8行X8列的像素值(这个动作相当于在印刷中放置一个活字字模到印刷版面上)。这个字符就会自动由计算机的硬件快速显示在屏幕的某个位置上。显示屏是一张经久耐用的唯一“张纸”,某一帧显示内存中的整屏点阵信息就是“印刷版面”,每隔十几毫秒,它就会被计算机的硬件自动“印刷”(即映射)到显示屏上,刷新整个屏幕(大约每秒刷新60—80次)。(注):像素到底有多大?这与显示屏的当前分辨率有关。如果显示器的当前分辨率是1024X7680那么显示屏上每一行都有1024个像素,每一列都有768个像素。整个显示屏由1024X768个像素构成。每个像素长和宽大约0.2毫米。也就是说,你在显示器屏幕上看到的一个五号字体的字符,大约由一百个左右像素构成。延伸与拓展:显示器的两种工作模式——图形方式和文本方式的技术内幕在VC++6.0集成开发环境(即编译程序等)运行时,显示器是运行在困步方式下的。然而,运行C语言源程序编译得到的可执行程序,显示器默认是工作在文本方式下的。在文本工作方式下,屏幕被分割成用来显示输出80列、25行的文本(即字符)。显示器?熊以单个字符作为最小输出单位。在这种文本方式下工作,不能用鼠标器,只能使用键盘。要在程序运行时将显示器改变到图形方式下(比如屏幕被设置成1024X768个像素)工作,需要调用相应的图形库函数(参见TC2.0环境下的例题:贪吃蛇游戏)。在图形方式下,显示器以单个像素作为最小输出单位。游戏程序和窗体应用程序,通常要在图形方式下运行,这样才能由程序控制在屏幕上画出点(即单个像素)、线和各种颜色的图形和画面来。在图形方式下,在鼠标的移动过程中,屏幕也在频繁地刷新(即被显示内存“印刷”出新的图像),有必要的话,系统程序(操作系统、鼠标驱动程序等)可以将代表鼠标的点阵图像,频繁地填充到显示内存中的与鼠标当前位置对应的适当位置上。这样你就能在屏幕上看到由你的手“移动着”的鼠标图象。18最常见的编程错误列表(此表包括了同学们极易犯的几乎全部初级错误,在上机前要认真看一看;上机出现错误时,也可借助此表来排除错误):1.使用非法的或错误的标识符;比如main写成mian;printf。写成了print。;或写了sin(2a)、cos①、n*r*r等;2.变量未经定义就使用;3.变量类型使用不当(取值范围不够大、本该用整型而用了实型、精度不够等等);4.变量未经初始化就使用在表达式中;5.语句或定义结束缺少分号;(或误用分号:复合语句结束后面不需要分号)6.表达式中漏写了必要的乘号*。例如:将3*x*y+5错写成3xy+5;7.表达式中缺少必要的圆括号,或圆括号不匹配,或者用花括号{卜方括号取代了圆括号;8.忘记了注释的结束符*/;正确的应当是以/*开始,以*/结束,所用的两个符号和之间不能用空格隔开;9.在该用小写字母的地方,却用了大写字母(例如,把main写成了Main、scanf写成了Scanf;
143定义变量名是小写,但在程序中却用了大写的变量名,其中s,c,X,k,z最易用错,比如si写成了SI、ch写成了Ch);1.在语句之间对变量进行了定义。正确方法是在函数体中将所有定义放在所有语句之前;2.编写代码(程序)时就特别要注意:避免程序在运行时,能够用0作为除数;3.在字符串或输入输出格式控制串外的其他地方,用了非法的标点符号(除了英文半角输入法,其他输入方式下的标点符号都是不对的);4.漏写函数体结束时的花括号,或者花括号不配对;5.分隔符使用不正确;比如inta,bc.d;应为inta,b,c,d;;6.程序中调用了库函数,但忘记包含相应的头文件(比如要包含头文件:math.h);7.标准输入输出头文件包含时出错;正确的是:#includc
144",z);
145第六章选择结构程序设计宇宙花费了一百多亿年的时间(通过无数次选择)“刹费苦心”地创造了智能生物一人类——来认识自己。然而对于人类的未来命运,宇宙却毫不关心。选择结构就是根据某个表达式计算的结果,选择不同的语句执行。本章系统讲解了各种选择结构语句。本章还讲解了构成选择依据的关系表达式和逻辑表达式、条件运算符、复合语句和空语句。选择结构的嵌套是本章的一个难点;构造出合适的关系表达式和逻辑表达式是编程的一项重要基本功。在最简单的情况下,计算机按程序中书写语句的先后顺序,一条一条地执行所有的语句(翻译成的机器指令)。然而在实际情况下,某些语句是否执行,要依赖于(程序运行时)输入的数据或某个表达式的计算结果。在C语言中,存在着一类可以嵌入一些其它语句的容器型语句,必须根据某个(布尔)表达式的值做出判定,以决定选择哪条(或哪些)被嵌入的语句执行,并且跳过哪条(或哪些)被嵌入的语句(不执行)。通常称这一类容器型语句为选择结构语句。我们把嵌入在一条选择结构语句之中的语句称为分句。注意:除了所嵌入的分句之外,选择结构语句本身一般不会改变任何变量的值,除非(布尔)表达式本身是具有副作用的(关于表达式的副作用参见第7章)。6.1两种基本的if语句if语句的工作流程是:先计算一个表达式的值。这个表达式一般是一个布尔表达式(-*),布尔表达式的计算结果是逻辑值“真”或逻辑值"假”,if语句根据这个取值的不同,来选择是否执行分句(或者选择执行两条分句中的哪一条分句)。被嵌入的分句,可以是一条简单语句、一条空语句(一),还可以是一条复合语句(一)。由于if语句同时又是一种容器型语句,因此,还必须把if语句从何处开始,到何处结束辨别清楚。(1)第一种if语句(没有else分支)的语法结构及用法如下:if(布尔表达式)if语句开始处分句;被嵌入的分句,if语句结束处语句2;if语句后的下一条语句若布尔表达式的取值为"真",则执行被嵌入的分句,然后执行if语句后的下一条语句。若布尔表达式的取值为"假”,则嵌入的分句被跳过不执行,转而执行if语句后的下一条语句。第一种if语句的执行流程如图4.1所示。
146式为真表达式/1表达式为假分句if语句后的下一条语句图4.1第一种if语句的执行流程演示程序6.1进入旅游景点,对游客身高进行区分,如果身高高于L2米要购买门票。程序如下:1#includc
147”);7scanf("%f",&x);8if(x>1.2)9printf("请购买门票
148”);/*嵌入在if语句中的语句1*/1011printf("程序结束、n");/*if语句后的下一条语句*/12}请分别两次运行此程序:第一次:输入身高1.5米,表达式x>1.2为真,程序运行结果是:请购买门票程序结束嵌入在if语句中的分句得到了执行第二次运行,输入身高1.1米,表达式x>1.2为假,程序运行结果是:程序结束显然,在这一次运行中,嵌入在if语句中的分句没有得到执行。问题:例题4.1中的if语句是否一定会得到运行?所嵌入的分句是否一定会得到运行?答:一定。不一定。对于演示程序6.1还有另一种编程方法,请看如下:
1491#include
150");7scanf("%f",&x);8if(x<=1.2)9printf("不必购买门票
151”);/*嵌入在第一条if语句中的分句*/1011if(x>1.2)12printf("请购买门票
152”);/*嵌入在第二条if语句中的分句*/13return0;14}在此,用了两条if语句来处理两种不同然而又是相互排斥(即非此即彼)的条件,但这样其实并不好。对于这种相互排斥的条件,最好采用一条带else分支的if语句,而不是使用两条不带else的if语句,请参见例题4.2。(2)第二种if语句(有else分支)的结构及用法如下:if(布尔表达式)if语句开始处分句1;被嵌入的第一条分句else分句2;被嵌入的第二条分句,if语句结束处语句2;if语句后的下一条语句如果布尔表达式的取值为"真",则执行被嵌入的分句1,被嵌入的分句2跳过不执行,然后执行if语句后的下一条语句。如果布尔表达式的取值为"假”,则执行被嵌入的分句2,被嵌入的分句1跳过不执行,然后执行if语句后的下一条语句。第二种if语句的执行流程如图4.2所示。
153表达式为假表达式为真表达式if语句后的下一条语句图4.2第二种if语句的执行流程由此可见,有else的if语句是根据布尔表达式的不同取值,在嵌入的两条分句之间选择一条执行。演示程序6.2进入旅游景点,对游客身高要进行区分,如果身高等于或低于1.2米,不需购买门票,高于1.2米要购买门票。程序如下:1#include
154”);7scanf("%>,&x);8if(x<=1.2)9printfC不必购买门票
155");/*嵌入的分句1*/10else11printfC请购买门票
156");/*嵌入的分句2*/1213printf("程序结束
157");/*if语句后的下一条语句*/14return0;15}请分别两次运行此程序:第一次运行时,输入身高1」米,if后面的表达式x<=1.2成立(表达式的结果为真),选择嵌入的分句1执行;程序运行结果是:不必购买门票程序结束显然,嵌入在else后面的分句2没有得到执行。第二次运行:输入身高1.5米,表达式x<=1.2不成立,,此时x的值必然大于1.2,选择嵌入的分句2执行;程序运行结果是:请购买门票程序结束显然,这一次,嵌入在else前面的分句1没有得到执行。
158问题1:第9行,第11行为何要缩格(即在前面加了一些空格)?是否可以不缩格?答:为的是强调这并不是两条独立的语句,而仅仅是嵌入在if语句中的分句。书写时可以不缩格,但是不缩格,程序的结构就看不太清楚。一个原来在语法上独立的语句被嵌入到if语句中之后,就变成了语法上不独立的一个分句。语法上独立的语句现在只是一条if语句了。如果不知道if语句只是一条语句,在学习后面的if语句嵌套时,就有可能会产生理解程序运行流程上的困难。习题1.编程题:已知自变量x的值,求函数y的值,已知函数式为:3x+6当x<2y={x2-5.4当x>2(提示:当条件x<2不成立时,x>=2一定成立)提示:在C语言中,if后面的表达式一定要用圆括号括住,布尔表达式后面一般不能有分号。4.2布尔表达式之一:关系表达式那么,什么是布尔表达式呢?布尔表达式是对关系表达式和逻辑表达式的统一称呼。这是由于:这两种表达式运算得到的结果都是布尔值。布尔值只能取两个值之中的一个,即“真”或者“假工在C语言中,“真”用整数值1来表示,而“假”用整数值0来表示(其它高级语言不一定如此)。定义:由运算量、圆括号、关系运算符(大于〉、大于等于>=、小于v、小于等于<=、等于==、不等于!=)构成的有意义的式子称为关系表达式。>=、<=、==、!=必须连着书写,不允许在两个字符之间插入空格。在关系表达式中,关系运算符是必须有的,运算量要分别位于运算符的两边,因为关系运算符是二元运算符。运算量可以是常量、变量、函数调用、算术表达式中的任何一个。常用的算术、关系运算符的相对优先级(由高到低依次)如下:1.*、/、%2.+-3.大于:>大于等于:>=小于:<小于等于:<=4.等于:==不等于:!-可见,关系运算符的优先级比算术运算符低。关系表达式用来比较两个量(或算术表达式)的大小,或者相等还是不等。这两个量可以是整型量或实型量,这时比较的是数值本身的大小,比如:x>=3.7、n==100、(m%7)!=0等。关系表达式也可以用来比较两个字符量(或者比较一个字符量和一个数值量)的大小,比如,‘a'>'4'或ch!='\0',其中ch是字符变量。此时比较的通常是字符所对应的ASCII码的大小(例外情况是:这台计算机不是用ASCII码来表示字符,而是采用别的字符编码表)。只要是字符常量和变量,都是取它所对应的码值(通常是ASCH码)进行关系运算。C语言中,关系表达式的运算结果取值为逻辑值,逻辑值只有两个,“真”或者“假工数学中的不等式与(比较大小的)关系表达式有着重要区别。关系表达式X>=3并不是中学
159学过的不等式x》3。在中学学过的不等式中,x>3意味着x的值不能比3小。也就是说,不等式限制了变量的取值。而在关系表达式中出现的变量,只是取出变量的值进行关系运算,变量的值不会改变。没有副作用的关系表达式,改变不了变量的值。关系表达式x>=3在x的值大于等于3时取值为"真"(即为1),在x的值小于3时取值为“假”(即为0)。对于x!=3,情况也是类似的。此表达式在x的值等于3时取值为“假二x的其他取值表达式的值都为真。可以通俗地说:对于任何关系表达式,如果(变量的取值使得)关系表达式所表示的大小(或相等、不相等)关系不成立(比如3>5),则表达式的取值为假(0);如果(变量的取值使得)关系表达式所表示的大小(或相等、不相等)关系成立(比如3<5),则表达式的取值为真(Do延伸与拓展:简单计算机的机器指令中,并没有比较两个数大小的指令(高级一些的计算机是有数据比较指令的),那么为什么高级语言中可以比较两个数的大小或者是否相等呢?这是由于机器语言中有减法指令,并且一般计算机的CPU中都有一个程序状态字寄存器(或称为标志寄存器),其中有一个二进制位专门用来表示上一条指令的运算结果是否为0,因此,CPU通过读取程序状态字寄存器中的该位的值,就可以知道上一条两数相减指令执行的结果是否为0,也就间接地知道两数是否相等。这就是在高级语言中为何可以用等于"==",不等于“!=”的原因。对于大于、小于,关键在于程序状态字寄存器中还有一个二进制位,可以表示上一条指令的运算结果是否大于0。基本的三条跳转指令如下:(1)无条件跳转。(2)如果上一条指令的运算结果大于零则跳转。(3)如果上一条指令的运算结果等于零则跳转。程序状态字寄存器中的这两个二进制位与这三条指令结合起来,就可在高级语言中使用关系表达式,并根据表达式的值选择不同的处理流程。注意:不要对两个大小非常接近的实型量进行相等比较。因为它们的结果可能为0=这是因为实型量在机内通常是由:符号位+指数位+尾数位(总共只用32位或64位)来近似表达的,如果两个实数大小很接近(对于float型,如果数值的前7位都一样),它的有限的指数位和尾数位就可能完全一样,相减的结果尾数就全为0,指数不变。系统就会认为比较的两数是相等的。问题1.当整型变量n的取值分别为1和4时,下列关系表达式的值是真(1)还是假(0)?(1)n>1、(n-3)>1(2)n>=1、(n-3)>=1(3)n<1、(n-3)<1(4)nv=1、(n-3)<=1(5)n==1、(n-3)==1(6)n!=l、(n-3)!=1问题2.当字符变量ch的取值分别为‘a'和'2'时,下列关系表达式的值是真还是假?
160(1)ch>'C';(2)ch>='C';(3)ch<'a'-3;(4)ch<='a'+3;(5)ch=='a';(6)ch!=’a';通过表达式可以要求计算机做多个数值的数学运算,在程序中能否要求计算机同时做三个数或更多数的大小比较呢?这是不行的。其实计算机本身的算术逻辑单元(ALU)的硬件结构决定了,它在任意时刻只能做两个数的运算以及比较两个数的大小(只有两个数能同时进入到算术逻辑单元中),而不能做多个数的运算和比较大小。因此,只能用关系运算符来一次比较两个数的大小。要用计算机来比较多个数的大小,则只能用关系表达式来明确地告诉计算机,如何通过两两比较大小,来达到最终实现多个数的比较大小的目的(可参见本章后面的多个例题)。不仅不能直接命令计算机比较多个数的大小,就如a>b>c或者4v=x<9.6这样的比较形式,也不符合高级编程语言的数据比较要求(参见本章后面的讨论)。6.3空语句仅由分号构成的语句称为空语句。空语句的主要作用如下:1.纯粹消耗CPU时间,起到延时的作用,2.为了程序的结构清楚,可读性好,以后扩充新功能方便或者自动化测试的需要;对于if/else语句等,如果分支不配对的话,需要用空语句进行配对。比如:if(条件){语句1;}else{;}〃这是一个空语句此外,空语句在程序中还可以暂时代替还没有写出来的语句组或函数体。注意:在if语句中,很可能不小心错用了空语句,比如:if(布尔表达式);分句1;else分句2;这个if语句是错误的。当布尔表达式的值为“真”时,执行一条空语句(因为布尔表达式的后面有一个分号,此分号代表一个空语句)。因此编译程序把此if语句当作无else的if语句对待;这样一来,语句1成为if语句后的下一条语句。因此,当编译程序遇到语句1后面的else时,就会报错。问题,以下if语句有何错误?if(布尔表达式);语句1;答:当布尔表达式的值为"真"时,执行一条空语句,因为布尔表达式的后面有一个分号,代表一个空语句。这样,语句1成为if语句后的下一条语句。语句1的执行与布尔表达式的取值无关,所写的if语句形同虚设,根本不起任何作用
161例题6.3读入三个数,找出并打印出其中最小的数类型必修题趣味性*难度*一级算法:1.读入三个数xl、x2、x3o2.假设xl为最小数,即将xl的值赋给min。3.如果x2
162");8scanf(,&xl,&x2,&x3);9/*设xl为最小数,即将xl的值赋给min*/10min=xl;11/*如果x2vmin,那么将x2的值赋给min*/12if(x2
163”);scanf(&xl,&x2,&x3);if(xl 164elsemin=x2;if(x3 165");scanf(rt%f%f%fw,&xl,&x2,&x3);if(xl 166语句n;在一般的高级语言中,都可以把复合语句作为一条语句,用于任何只能出现一条简单语句的位置上。因此,复合语句当然可以作为嵌入在if语句中的一条分句。除了简单句外,复合语句中也可以包含选择结构语句、循环结构语句和复合语句等容器型语句。注意1:复合语句是C语言中唯一一种不以分号结束的语句。注意2:在C99标准中,可以在块语句(复合语句)的前部定义变量,例如:{inti;i=3;j=j+i;}这样的变量只在块语句中起作用,在这个语句块之外,这样的变量不能使用。例题6.4读入两个数x、y,将大数存于x,小数存于y类型:必修题趣味性*难度*一级算法:1.读入两个数x、yo2.如果x小于y,那么交换x和y的值。3.输出x、y的值。其中第2步需要二级求精。二级求精:temp=x;x=y;y=temp;}转换为C语言程序如下:1#includc 167");6scanf("%£%>,&x,&y);7if(x 16813printf("大数等于%f,小数等于%f",x,y);14return0;15)问题1.如果第7行的if(x 169分句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)分句;)else分句2;可见,else一般是与上面最靠近的关键字if配对,构成有else的if语句。除非有花括号出现,才能改变与else配对的ifo根据以上讨论:请读者分析以下两个程序段,并体会else与if配对时带来的不同执行效果:(1)if(scorel>80)if(score2>80)分句1;〃注意:执行分句1时,scorel和score2都大于80else分句2;〃注意:执行分句2时,scorel大于80、score2小于等与80⑵if(scorel>80){if(score2>80)分句;〃注意:执行分句时,scorel和score2都大于80)else分句2;〃注意:执行分句2时,scorel小于等于80、但与$core2的大小无关 1706.6布尔表达式之二:逻辑表达式定义:由常量、变量、圆括号、函数调用、算术表达式、关系表达式、逻辑运算符构成的有意义的式子,称为逻辑表达式。其中,逻辑运算符是必需有的,其他为可选项。在C语言中,逻辑“与”(英文用and)运算用符号"&&”来表示,逻辑“或”(英文用。r)运算用符号“||”来表示,逻辑“非”(英文用not)运算用符号来表示。基本逻辑运算的真值表见表4.1„表4.1基本逻辑运算的真值表ABA&&B(与)A||B(或)!A(非)真真真真假真假假真假假真假真真假假假假真延伸与拓展:如何用电路实现逻辑运算高级编程语言中的逻辑运算是有对应的逻辑运算机器指令的,这些指令是由算术逻辑单元中的电路来负责执行的。在电路中,用两个串联的开关kl和k2(作为输入端)来控制一个灯泡(作为输出端)的亮和不亮,其实这就实现了逻辑“与"运算,只要将开关打开定义为“假”,开关合上定义为“真】灯泡不亮定义为“假二灯泡亮定义为“真"只有两个开关都合上(真)灯泡才能亮(真)。这就是逻辑“与”运算。在数字逻辑电路中,用逻辑门电路实现“与”运算的道理是和这个用两个串联开关控制灯的例子其实是一样的,只不过是用了两个晶体三极管作为两个开关(这种非机械的电子开关在开和关的两种状态之间切换的速度极快,体积又极其微小);用输出端的高电压(代替灯泡亮)表示"真",用输出端的低电压(代替灯泡不亮)表示“假工用开关电路实现逻辑“或”运算也非常简单,只要用两个并联的开关(作为输入端)来控制一个灯泡(作为输出端)的亮与不亮即可。只要有一个开关打开,灯泡就一定亮,这就是逻辑“或"运算。在数字逻辑电路中,用三极管等元件来实现“与”运算的电路称为“与门”;用三极管等元件来实现“或”运算的电路称为“或门”;实现逻辑“非”运算的电路称为“非门,计算机的算术运算电路也是由多个以上这样的最基本的“门电路”通过某种方式组合而成的,请参见书未参考文献HL常用运算符(算术、关系、逻辑)的相对优先级为:1.!2.*、/、%3.+、-4.>、>=、<、<=5.==、!=6.&& 171逻辑运算符(&&和II),主要是用来把两个关系表达式结合起来,构成一个逻辑表达式。如果我们想要关系表达式1和关系表达式2都为真时才执行分句1(即严格执行分句1的条件),只要有一个不成立就执行分句2,此时就要用逻辑与"&&”运算符,表示为:if(关系表达式1&&关系表达式2)分句1;如果我们想要关系表达式1和关系表达式2其中之一为真时就执行分句1(即放宽执行分句1的条件),其他情况下都执行分句2,此时就要用逻辑或"运算符,表示为:if(关系表达式1II关系表达式2)分句1;非运算符!是一元运算符,作用在它右边的逻辑量上,将原来的逻辑值“真”变成“假”、原来的逻辑值“假"变成"真,比如:!(3>5)3>5的运算结果为假,经过非运算!最终值为真(即为1)。问题1.如果要表示变量x的取值只有在满足24x<5时才执行分句1,否则执行分句2,能否将其表示为:if(2<=x<5)分句1;else分句2;答:不可以。假设x的值为1(x的取值不满足条件2 172习题2请将以下x、y的取值条件用逻辑表达式表示出来。(1)3 173");8scanf(,&x,&y,&z);9if(a#include 174intmain(void){inta,b,c;scanff,%d%d%d*,,&a,&b,&c);if(a<=b&&b<=c)printf(H%d%d%d”,a,b,c);elseif(a<=c&&c<=b)printf(H%d%d%dM,a,c,b);elseif(b<=a&&a<=c)printf(M%d%d%dn,b,a,c);elseif(b<=c&&c<=a)printf('*%d%d%dH,b,c,a);elseif(c<=a&&a<=b)printff^d%d%dn,c,a,b);elseif(c<=b&&b<=a)printf("%d%d%dM,c,b,a);return0;〃最后一行的if可以省略,想想为什么?例题6.4求建筑物的高度已知:平地上建有4个圆塔,每个圆塔的高度都是10米,塔的半径都为1米。4个塔的中心坐标x,y分别为:(1)塔1中心坐标为2,2„(2)塔2中心坐标为-2,2o(3)塔3中心坐标为-2,-2。(4)塔4中心坐标为2,-2o求任意给定一点的建筑物的高度。题目类型选修开拓思路题趣味性**难度**一级算法:1.输入坐标点x,y的值。2.如果(x,y在塔1内)或者(x,y在塔2内)或者(x,y在塔3内)或者(x,y在塔4内)那么打印"建筑物的高度为10米,否则打印“建筑物的高度为。米二其中布尔表达式“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) 175");7scanf("%£%/,&x.,&y);8if(((x-2)*(x-2)+(y-2)*(y-2)vl)||((x+2)*(x+2)+(y-2)*(y・2)vl)||9((x+2)*(x+2)+(y+2)*fy+2) 1761printf(“建筑物的高度为10米 177”);2else3printf(“建筑物的高度为0米 178");45return0;6)另外一种简洁的算法如下:(1)输入坐标点x,y的值。(2)如果(x,y不在塔1内)并且(x,y不在塔2内)并且(x,y不在塔3内)并且(x,y不在塔4内)那么打印“建筑物的高度为。米”。否则打印“建筑物的高度为10米”。点评:此题也可以不用逻辑运算符,但程序流程结构就复杂多了,参见下面的延伸与拓展。通过对比可以得知:逻辑运算符在简化if语句嵌套时的巨大威力。延伸与拓展:下面介绍例题4.6不用逻辑表达式的算法。第一种攵杂的算法如下:一级算法:1.输入坐标点x,y的值;2.如果(x,y不在塔1内)(2.(1)x,y不在塔1内的情况;否则(2.(2)“建筑物的高度为10米,;/*在塔1内*/其中2.1需要进一步求精。二级求精:处理x,y不在塔1内的情况2.1如果(x,y不在塔2内)(2.1.(1)x,y不在塔1和塔2内的情况;否则(2.1.(2)建筑物的高度为10米.;/*在塔2内*/其中(21.1)需要进一步求精。三级求精:处理x,y不在塔1和塔2内的情况(2.1.1)如果(X,y不在塔3内)(21.1.(1)理x,y不在塔1、塔2和塔3内的情况;否则(21.1.(2)“建筑物的高度为10米.;/*在塔3内*/其中(2.1.1.1)需要进一步求精。四级求精:处理x,y不在塔1、塔2和塔3内的情况(1)1.1.1)如果(X,y不在塔4内)打印"建筑物的高度为。米.;/*到此处点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内) 179(1)打印“建筑物的高度为0米”;(2)else(3)打印“莫筑物的高度为10米”(4)else(5)打印“建筑物的高度为10米.;(6)else(7)打印“建筑物的高度为10米.;(8)else(9)打印“建筑物的高度为10米.;第一种复杂算法中,使用了缩进格式,此算法如果不用缩进形式,程序的流程将非常难以看清,读者不妨亲自试一试。问题3.请将上述算法中各正语句从哪一行开始到哪一行结束(从上往下分别称为道、迤、汨和if4语句)标出来,各if语句的条件为“真”时要执行的分句从何处开始到何处结束?条件为“假”时要执行的语句是哪些?各正语句是并列关系还是嵌套关系?答:各个正语句之间都是嵌套关系,没有并列关系。整个是一条if!语句;其中i£2是嵌入在ifl中的条件为“真”时要执行的分句;的又是嵌入在池中的条件为“真.时要执行的分句;闻又是嵌入在ifl中的条件为“真”时要执行的分句。输入坐标点x,y的值;正(x,y不在塔1内)/9开始处*/if(x,y不在塔2内)/*道开始处,ifl为真*/if(x,y不在塔3内)/*i£3开始处,迪为真*/if(x,y不在塔4内)/*if4开始处,if3为真*/打印"莫筑物的高度为0米“;/«为"真"*/else打印'■建筑物的高度为10米"/*讯为"假"正4结束处*/else打印”建筑物的高度为10米";/*if3为"假",if3结束处*/else打印“建筑物的高度为10米”;/*边为“假”边结束处*/else打印"建筑物的高度为10米.;/*ifl为"假"ifl结束处*/问题4.如果塔不是4个,而是N个,塔心坐标也没有规律,请考虑怎么做?(参见例题射击问题)第二种复杂的算法如下:一级算法:(1)输入坐标点x,y的值;(1)if(x,y在塔1内)(2.(1)"建筑物的高度为10米.;else(2.(2)x,y不在塔1内的情况;/*还有可能在塔2、3、4内*/其中(2.2)需要进一步求精。二级求精:(22)处理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)还需进一步求精。 180三级求精:处理x,y不在塔2内的情况;/*但还有可能在塔3、4内*/if(x,y在塔3内)(1)2.2.1)打印"建筑物的高度为10米.;else(2)2.2)处理坐标点x,y不在塔3内的情况;/*但还有可能在塔4内*/其中(2.2.2.2)还需进一步求精。四级求精:处理坐标点x,y不在塔3内的情况;/*但还有可能在塔4内*/if(x,y在塔4内)打印"建筑物的高度为10米.;else打印“建筑物的高度为0米.;/*程序流程到此zy点一定不在任何塔内*/通过以上考虑,可得到全部算法如下:⑴输入坐标点X,y的值;⑵if(x,y在塔1内)(3)打印“建筑物的高度为10米.;(4)eke(5)if(x,y在塔2内)(6)打印“建筑物的高度为10米”;(7)else(8)if(x,y在塔3内)(9)打印“建筑物的高度为10米.;(10)else(11)if(x,y在塔4内)(1劣打印"建筑物的高度为10米.; 181(13)else(14)打印“建筑物的高度为0米";/*程序流程到此x,y点一定不在任何塔内*/问题1.输入的坐标点x,y满足什么条件,才会执行算法第14行的处理?答:只有输入的坐标点sy不在任何塔内,算法流程才会到达第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打印“建筑物的高度为0米.;这就是一种下一节将要讲解的一种特殊的多重嵌套的if语句一多分支选择结杓谙旬。.在阅读包含有多个if语句的程序段时,一定要弄清楚各条if语句之间的相互关系是嵌套关系还是并列关系?也要弄清楚各条if语句从哪里开始到哪里结束,哪个else是与哪个if配对的。但最为重要的是自己在平时编程时,要养成使用缩格的好习惯。这样一来,即使if语句之间的相互关系较为复杂,也比较容易看清楚。6.7一种特殊的多重嵌套if语句——多分支选择结构语句在if语句的多重嵌套结构中,有一种特殊的多重嵌套if语句所有else(最后一个else除外)后面的嵌入的分句都是一个if语句。这种多重嵌套if语句,可以不需要缩格:if(表达式1)分句1;elseif(表达式2)分句2;elseif(表达式3)分句3;elseif(表达式a)分句n-,else分句n+1;这种特殊的多重嵌套if语句,不少教科书将其称为if语句的第三种形式——多分支选择结档谙旬。.这种语句的程序流程如图4.3所示。图4.3多分支载梯结构语句的执行流程表达式•''''注意:在程序的任何一次运行时,福备重嵌套if语句(从整体上来看,它就是一条if真非(0)表达式2语句)中,只有嵌入在其中的一条分句会得到执行——哪一个表达式为真,就执行哪一条分句;其他被嵌入的分句都会被跳过。当所有布尔表达式的取值均为“假”时,才会执行嵌入的分句n+1。这就是一般情况下的多分支选择结构语句。 182真非(0)表达式3假(0)假(0)其非(0)表达式假(0)本章后面4.7节将要介绍的switch语句也是多分支选择结构语句,但是由于switch语句要求每个分支的执行条件都是一个整数值,所以使用起来比较方便。但是从应用范围来说,只有以上这种形式,才是适用于任何在多个完全相互排斥的条件下,选择一条被嵌入的语句执行的普遍形式。例题6.5输入一门百分制的成绩score,输出该成绩的等级当score<60输出不及格当60《score<70输出及格70 183");scanf("%f”,&score);if(score<60)printf("不及格 184");elseif(score<70)printf(“及格 185");elseif(score<80)printf(“中 186”);elseif(score<90)printf("良好 187");elseprintf(“优秀 188”);return0;)问题1.程序中的elseif(score<70)是否能够写成:elseif(60= 189");if(score>=60&&score<70)printf("及格 190"); 191if(score>=70&&score<80)printf("中 192");6.8switch语句C语言中,在多分支的情况下(标准的if-elseif形式的多分支语句除外),如果使用if语句嵌套的重数太多,往往会使读懂和编写程序的难度大大增加,并且容易出错。下面介绍一种新的多分支语句,即switch语句。其格式为:switch(表达式){/*注意,此处花括号不能少*/case常量表达式1:语句1或空<break;>case常量表达式2:语句2或空<break;>case常量表达式n:语句n或空<break;>default:语句n+1或空;/*default分支可省略*/}/*注意,此处花括号不能少*/•switch语句的执行过程:将switch后面表达式计算出的值,逐个与case后面的常量进行比较。若表达式的值与某个case后面的“常量表达式”的值相同,则顺序执行该“常量表达式”后的语句(组);若不与任何一个列出的常量相匹配,则执行default后面的语句。说明:(1)关键字switch后的表达式可以是各类整型变量(但不能是longint型),字符型变量,还可以是整型表达式,但不能是实型量或实型表达式。(2)每个case分支后面常量表达式的值必须各不相同。(3)每个case或default之后都可以有多条语句,但是,都可以不用花括号"/与将其括起来。(4)每个case分支后面,都可以加一个语句<break,表示语句break;是可选用的;如果有break语句,则会通过执行break语句而跳出switch语句,switch语句中剩下的语句部分就不会得到执行。如果没有break语句,则以下所有“嵌入”的语句都会顺序依次执行,这一点与同属于选择结构的if语句有所不同。(5)可以省略关健字default以及default后面的分句,但不提倡省略default后面的分句,因为程序的健壮性可能会不好。(6)case后面所列常量表达式的值没有顺序要求。什么是程序的健壮性?程序的健壮性(又称鲁棒性robustness)是指程序在各种不正确的操作或输入不符合要求的数据时,能够比较好的应对,而不会出现致命错误,导致程序不能继续运行下去。健壮性不好的程序比较脆弱,在错误操作或不符合要求的输入数据下,程序容易崩溃。例题6.6输入一门百分制的成绩score,输出该成绩的等级当score<60输出不及格当60 193当90 194”);scanf("%d”,&score);switch(score/10)/*score/10得到的是整数*/{case10:case9:printf(u优秀 195");break;case8:printf("良好 196");break;case7:printf(“中 197");break;case6:printf("及格 198");break;default:printf("不及格 199”);}/*endscore*/return0;)问题L缺失所有brack;语句,会造成什么后果?请你亲自删掉所有brack;语句,输入成绩85分试一试。练习.在switch语句之前增加一条if语句,处理分数小于0和大于100的错误情况。例题6.7输入两个运算量及一个算术运算符,输出运算结果类型必修题语法讲解题趣味性**难度**此题是构成一个简单计算器程序的基础。一级算法:1.输入第一个运算量X。2.输入运算符ch。3.输入第二个运算量y。4.根据ch的取值执行以下的一条语句。(ch取值为'+':result=x+y;ch取值为:result=x-y;ch取值为:result=x*y;ch取值为'/':result=x/y;ch的其他取值:输出“输入的运算符有错”,并结束运行;}5.输出运算结果rcsulto转换成C语言程序如下:1#include 200"); 20189scanf("%f%c%r,&x,&ch,&y);switch(ch)10{11case'+:result=x+y;break;12case-’:result=x-y;break;13case'*':result=x*y;break;14case*T:result=x/y;break;15default:{printf("输入的运算符有错、n");exit(0);}16}/*endswitch*/17printf(w%.2f%c%.2f=%f 202”,x,ch,y,result);1819return0;20}注意:此题运行时,要将第一个数、运算符、第二个数连起来输入,数据之间不要用空格隔开。否则,scanf函数会把空格作为运算符存入到字符变量ch中。程序不能正常运行。点评:exit()函数的用法exit()是一个库函数,是用来中断程序运行(返到操作系统)的。在程序中运行了函数调用语句“exit©;”之后,该语句之后的所有语句都不会得到执行。问题1.case'+':此处的单引号是否可用双引号?或者用中文输入法下的单引号?此处的冒号是否可用逗号或者分号取代?问题2.如果在一个case后面要嵌入多条分句,这多条语句是否要用花括号括住,让它们变成一条复合语句?答:可以不必用花括号。因为根据(switch右边的)表达式的值,其值与某个case所列举的值匹配,此case后面的语句就会一条一条顺序执行,直到遇到break时才跳出switch语句。问题3.是否可用switch语句来构造一切多分支的流程结构?答:不可以,因为只有在可以列举出整数(或字符)值的情况下,才可以用switch语句。一般情况尽量使用标准的if---elseif形式的多分支语句。练习:以上例题还未考虑到除数为。的情况,请修改程序,把除数为0的情况也考虑进去。(提示:插入一条if语句到ch取值为'/’的情况)。例题6.8输入年月,输出该月有几天类型必修题趣味性**难度**一级算法:(1)输入年year和月montho(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天 203elsedays=28天其中year为闰年需进一步求精。一年365天只是地球围绕太阳运行一周的大致时间,但实际上运行一周平均要用365.2422天,于是每4年(选定了年号被4整除的年份)加上一天,称该年为闰年,但这样又多加了(365.25为一年),于是每100年(选定了年号能被100整除的年份)再减去一个闰年(365.24为一年),但这样又减多了,于是每400年(年号能被40。整除的年份)再加上一个闰年(365.2425为一年)。这样一来,每40。年中,实际的公元日历天数是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由于两个条件只要其中之一满足就是闰年,可用逻辑或运算符将两个表达式合并为一个逻辑表达式。fyear%4==0&&year%100!=0)||year%400==0三级求精:year为闰年时表示为以下的逻辑表达式。((year%4==0&&year%100!=0)||year%400==0)转换为C语言程序如下:1#include 204”);7scanf("%d%d”,&year,&month);8switch(month)9{10case1:case3:case7:case8:case10: 205case12:days=31;break;11case4:case6:case9:case11:days=30;break;12case2:if(()rear%4==0&&year%100!=0)||year%400==0)days=29;elsedays=28;break;13default:{printf("你输入的月份不对 206");exit(0);}1415}16printf("%d年,%d月,有%d天\。”,year,month,days);17return0;18)问题1.从第12行到第13行是一个什么样的结构和执行流程?答:总体是一个case下嵌入的处理流程,其中包含一条带else的if语句,if语句后是一条break语句,可跳到switch语句后的下一条语句。6.9选择结构的常见错误1.if语句的常见错误(1)错将赋值语句用作关系表达式,将if(x==3)错写成if(x=3),(最好的形式是if(3==x))。(2)逻辑表达式用错,例如将if((x〈=3)&&(x>l))错写成if(3>=x>l)。(3)if语句的布尔表达式后面加了分号,变成:if(布尔表达式);,多了一条空语句,导致程序流程完全错误(无else时)、或者通不过编译(在有else分支时)。(4)在if语句中的一个嵌入位置嵌入了多条语句,但没有用花括号括住以构成一条复合语句。例如:茅^'2;,虫虫1句句。句句句if语语U1S语语语正确的应当是:if(表达式)|语句1;语句2;}else|语句3;语句4;)语句5;(5)将嵌套关系的if语句错用成并列关系的if语句,或将并列关系错用成嵌套关系。(6)else分句与哪个if配对的关系搞错。(7)if(y<=6)错写成if(y<=6)(在〈和=之间多了一个空格),>=、==、和!=都不能分开写。(8)if语句的布尔表达式整体上没用括号括住,例如将if((x<=l)&&(x>3))写成了if(x<=l)&&(x>3)o 207(6)在应该用有else的if语句时,用了无else的if语句,或者正好相反。(10)if语句的关键字用错,将if写成IF,或将else写错成eles。2.switch语句的常见错误(1)漏写了本来应当有的break,导致流程错误。(2)在switch(整型或字符型表达式)右边多加了分号。(3)漏写了switch语句本该有的一对花括号。(4)switch后面的表达式用了实型表达式。(5)在“case具体取值:"中漏掉了冒号,或错用逗号、分号带替冒号。(6)在不应该省略default分句时,省略了该分句,导致程序的健壮性差。6.10提高部分:6.1.1其它表达式作为布尔表达式使用对于C语言来说,其实并不要求if后面的表达式为布尔表达式。而可以是任何其它类型的表达式。但对任何其它类型的表达式,在此处也是作为布尔表达式来对待(或看待)的:表达式的计算结果如果为非0则表示逻辑值‘真',表达式的计算结果如果为0则表示逻辑惇侬。换句话说,在C语言中,任何表达式都可以出现在流程控制语句中(在所有选择结构和下一章要学的循环结构中),作为布尔表达式来使用。任何其它表达式被作为布尔表达式使用时的情况请看下表:任何其他表达式作为布尔表达式使用表达式原来结果:非0表达式原来结果:0表示的逻辑值:真表示的逻辑值:假表达式的最终取值:1表达式的最终取值:0例如算术表达式X+3,假设X的值为-6,则表达式的原来结果为-3,这是非0,则表示逻辑值‘真',因此将表达式x+3作为布尔表达式使用,它的最终取值是1而不是-3。再例如x=3.7是赋值表达式,赋值表达式原来的取值就是保存在赋值号左边变量的值3.7(参见上一章提高部分:赋值表达式),由于值3.7是“非0",表示的是逻辑值‘真’,最后该表达式的取值为1而不是3.7了。注意:千万不要把if(x==3.7)写成为if(x=3.7)因为后一个式子是豕迹为事的。比较好的办法是把相等判断的关系表达式写成:if(3==x)〃即把常量写在相等判断运算符的左边这样一来,如果你不小心写错了的话,成为:if(3=x)这种错误编译程序会帮助我们找出来。注意:在书写相等关系表达式时,最好要写成常量==变量的形式。 208虽然C语言允许用任何表达式来代替进行流程选择的布尔表达式,但本书不提倡这种替代用法。6.1.1条件运算符:二选一的选择结构,除了可以用有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;这样一条赋值语句。6.1.2逻辑表达式的短路运算逻辑表达式如果是以“与”运算&&为主构成的,在算出其中之一为假时,由于已经可以得到整个表达式的值为假,不再往下进行运算;如果是以“或"运算||为主构成的,在算出其中之一为真时,由于已经可以得到整个表达式的值为真,不再往下进行运算。这样一来,表达式中有些有副作用的部分,可能会得不到执行。从而影响计算结果。这也是本书不提倡大量使用具有副作用的表达式(除了作为赋值语句的赋值表达式)的重要原因。比如:6.11习题二、填空题1.能正确表达逻辑关系0«a<10的C语言表达式是»2.写出以下各逻辑表达式的值,设a=l,Q2,c=4。(提示:注意算术表达式被作为布尔表达式时的取值:非0表示"真",所以变成了1;注意运算符的优先级。)(1)a||b+c&&b-c(2)a+b>c&&b==c(3)!(a>b)&&!c|11(4)!(a+b)+c-l&&b+c/2三、编程题1.用户输入出生日期和当前日期,编程计算出实际年龄。2、编程输入整数a和b,若两数之和大于100,则输出百位以上的数字,否则输出两数之和。3、假如a、b、c、d是4个逻辑变量,其值只能是真(1)或假(0),由用户输入,编程判断它们是否均相等。 2094、某书店卖书时有以下规定:购书在2000元以上者打7.5折;购书在1000元以上者打8折;购书在200元以上者打8.5折;购书在100元以上者打9折;100元以下不打折。计算顾客购买书的实际应付货款。此题要求用两种方式做:1.用多条并列的if语句来做;2用.一条标准的多重嵌套的if…elseif语句来做。(提示1:请看以下伪代码if(moncy>=2000)按75折处理;if(money<2000&&money>=1000)按8折处理;if(money<1000&&money>=200)按85折处理;***,以上所有的if语句之间,是并列关系。请读者按此提示去做。再请看以下伪代码:if(money>=2000)按75折处理;elseif(money>1(X)0)按8折处理;elseif(money>200)按85折处理;以上所有的if语句之间,是标准的多重嵌套if…elseif语句。请读者按此提示去做。*5、你能否用switch语句来重做第4题?6、输入一个学生的5分制的成绩,然后根据成绩打印出:5分——优秀、4分良好、3分及格、2分、1分和。分——不及格。用if语句和用switch两种语句来做。7、报数游戏。A、B、C、D,E、F、G、H共8人站成一排,按以下方法开始从1报数,问谁先报到123456?ABCDEFGH1—2f3f4-*5-6f7f8I1—14—13—12—11—10—9-15f6f7f8f9-20—21—22 210第七章循环结构程序设计让每件事尽可能简单,但又不简单过度。——爱因斯坦循环语句就是在条件成立时反复执行嵌入在它内部的一条分句。本章主要讲解三种循环语的用法,即while语句、for语句和do~while语句。此外,本章还介绍了自增自减运算符++、一,复合赋值语句+=、-=、*=、和/=,循环结构的嵌套(多重循环)、逗号运算符。在编程学习中,循环语句的嵌套(多重循环)是大多数读者学习中的难点,在多重循环中又嵌入选择结构语句读者觉得更难。本章用大量循序渐进的例题讲解了使用循环语句的编程思路和技巧。尤其是数列求和打印各种图形的系列例题,读者应当认真阅读、消化,训练自己的坚实编程基本功。7.1while语句在许多复杂的问题中,常常需要做大量类似的计算处理,如果要把这些大量类似的计算处理的每一步都写成语句,并输入到计算机中,其工作量是相当大的,有时甚至是不可能完成的。高级程序设计语言中三大基本结构的最后一种是循环结构(前两种是顺序结构和选择结构),包括while循环、do…while循环和for循环三种。下面先介绍while循环。只要把while循环彻底掌握好了,其他两种循环都很容易掌握和运用。while循环语句用来根据某个布尔表达式的运算结果进行下一步的处理流程选择:是执行新的语句序列,还是再一次重新执行刚刚执行过的语句(或语句序列)。与if语句类似,while循环语句也是一种容野掣谙句。计算机执行这种语句时,首先要计算一个布尔表达式,然后根据表达式的不同取值进行处理流程的选择;如果表达式取值为“真”,则选择执行嵌入在while中的一条分句(这条被嵌入的分句又被称为彳营邛俘);如果表达式取值为"假”,则跳过该分句不做,执行循环语句后的下一条语句。while语句的形式如下:while(布尔表达式)while语句开始处分句;分句(又称为循环体)〃while语句结束处下一条语句;while语句结束后的下一条语句不过,当分句执行完毕后,计算机又会重新回过头去,继续计算布尔表达式的值,用以决定是否还要再次执行该分句。也就是说:当布尔表达式的值为“真”时,循环语句中嵌入的分句——循环体——将被反复执行多次;直到布尔表达式的值为"假”时,才执行循环语句后的下一条语句。通常情况下,被嵌入的分句往往是一条本身包含多条分句的复合语句,因此,while循环语句的一般形式为:while(布尔表达式)循环语句开始处{循环体;由一条或多条语句构成}循环语句结束处 211while锯环后的下一条语句;while语句的执行流程如图5.1所示。注意:while后的布尔表达式要用圆括号括住,布尔表达式后不能有分号。注意:虽然在循环语句中,可以嵌入一条分句,但是,循环语句整体上就是二条谙包。在这一点上,循环语句与同是容器型语句的选择结构语句是一样的。如果不把循环语句整体上看成是一条(构造型)语句,在学习后面的多重循环时,就有可能会产生理解程序运行流程上的困难。问题:当遇到while语句时,while语句被计算机执行了几次?嵌入在while语句中的分句被计算机执行了几次?答:while语句计算机只执行了一次,而且一定执行了一次;但是,是嵌入在while语句中的分句根据不同的表达式的计算结果,可以执行了0次、一次或者多次。循环语句的使用,可使编写程序的工作量在某些情况下极大地减少。只需构造出一些可以反复执行的、相同的语句组,把这个语句组作为循环体,放到while语句中,并在关键字while后面加上循环执行的条件即可(注意,这个条件要用圆括号括住,并且要写成表达式)。例题7.1求整数1到100之和类型必修题语法讲解题开拓思路题趣味性*难度**算法分析如下:此问题如果简单地用以下的赋值语句表示。sum=1+2+3+4+…+99+100;注意,表达式中不能用省略号,每个加数都要出现在表达式中。此算法肯定能解决这个问题,但此算法是非常愚笨的。如果此问题变成1到10万之和,还用此类方法,那编程就变成一项使人绝望的体力劳动。 212用100个变量来解此题更不可取。这种问题最好用刚学过的while循环语句来做。请先看以下的算法。(1)sum=0;/*sum是存放前1项数值之和的变量*/(2)i=l;(3)sum=sum-Fi\(4)i=2;(5)sum—sum+/,;(6)i=3;(7)sum=sum+/;(1)(200)i=100;(201)sum=sum4-/;(202)输出变量sum的值;通过这100组语句(从2到201句),一定能求出1到100之和,因为任何一组语句执行完的时候,变量sum都是数列1、2、3、……100的前i项之和。通过分析,可以把从第4句开始的所有偶数句i=2、i=3等等(除了第202句外)改写为i=i+l;,并且在第201句之后再插入一条语句:i=i+l;。注意,此句并不会影响变量sum的最后取值,但此句对构成循环体是很重要的一个环节,可使形式相似的语句组,变为形式完全相同的语句组。这样除了第1句、第2句和第203句外,所有语句组(3和4、5和6……201、202等等每两条构成一组)的形式都完全相同了,程序变为:(1)sum=0;/*前i项和的变量*/(2)i=l;/*度量初始化*/(3)sum=sum+i;(4)i=i+l;(5)sum=sum+i;(6)i=i+l;(7)sum=sum+i;(8)i=i+l;(199)sum=sum+i(200)i=i+l;(201)sum—sum+i;(202)i=i+l;〃这条语句使i的新值为101(203)输出变量sum的值;经过上述改造后,就很容易将其改为用循环语句来表达的形式。注意:执行第201行语句sum=sum+”时,/的值为100。由此可以得到所有相同语句组(即循环体)执行的条件是当i<=100为真。一级算法:sum=0;/*前/项之和的变量sum清零*/ 213/=1;while(/<=100)(sum二sum+片z=/+-l;}输出变量sum的值;这个算法的执行流程与前面不使用循环的顺序执行流程不同(因为包含了跳转),但除了计算关系表达式(/<=100)和根据关系表达式的值来决定是否继续执行循环体之外,所执行的赋值语句组其实是完全一样的,变量sum得到的结果也是一样的。但程序的书写方式,由于使用了while循环结构而得到了最大程度的简化。本题体现出了循环语句的巨大威力和使用技巧。编写循环语句的思路要点就是:把形式相似的语句组,转变成形式完全相同的语句组;然后把其中的一个语句组作为循环体,再加上语句组——循环体——执行的条件(布尔表达式),这样即可构成循环。转换为C语言的程序如下:1#include 214答:无限循环。因为语句/=/+1;成为循环语句之后的下一条语句。在循环语句中,用来控制循环次数的变量称为循环变量。循环变量的初始化语句(i=l;决定了循环体执行的次数)和循环体中对循环变量值更新的语句(i=i+l;决定了循环体在执行有限次后最终能够结束),是形成正确的while语句流程的重要部分。由上述问题可以发现,初学者编写循环程序时,需要特别细心,稍有不慎循环语句就会出错。需细心体会上述各个问题,避免犯上述错误。练习:请将例题5.1改写为求1到n之和。n是自然数,n由程序用户在运行时输入。延伸与拓展:对循环语句的深入理解•用if语句加goto语句构成的循环本节将深入讲解数列求和问题的算法,其中包括while循环语句与if语句加往前跳转的got。语句构成的循环的关系讨论。为的是加深读者对循环语句的理解。任何数列求和问题,都可归结为一个学生团队郊游到果园摘橘子的一类问题。可以要求所有的学生摘一定数量的橘子,每个学生都有一个编号,这个编号是从1开始的,如果有100个学生,那么学生的编号就是从1到100。现在要求每个学生摘橘子的数量为其编号数,即第i个学生摘i个橘子(与编号相关的其它数值完全可以,这对应着不同的数列求和问题)。为了收获并统计全班学生所摘橘子的总数,可采用如下方法:在果园门口放一个大筐,然后由班主任要求学生按自己的编号顺序,依次把橘子扔进大筐中。为此,写出一个统计橘子总数的方法如下:1.sum=0;2.i=l;3・sum=sum+i;4.i=i+1;5.如果i<=10036.输出sum;转换成c语言的程序如下:/*清空大筐(sum表示前i个学生扔下的橘子数)*//*班主任的手指指向第一个学生*//*被指的学生将橘子扔进大箧*//*班主任指向下一个学生*//*如果班主任指向的学生还有的话,跳转到第3步*//*至此筐中橘子数就是全班学生所摘橘子的总数*/1#include 215if语句执行时,只要关系表达式i<=100为“真”,就会跳转到语句标号为1。呼的那条语句处执行。注意:此程序和例题5.1中求1到100之和的问题,其实是同一个问题的不同编程解决方法,而且这两个程序的流程基本上是一样的。唯一不同的是,前一个程序while循环是先判断(先执行布尔表达式),然后决定是否执行循环体;而后一个例子中的if语句加无条件跳转,是先执行循环体,然后进行判断。以决定是否继续循环。if语句加上往回跳转的got。语句所构成的循环,与do…while循环在翻译成机器指令时,两者的指令流程是完全相同的,并没有任何区别(请参见本章后面do…while循环语句)□在C语言中,got。语句是一种跳转语句,这种语句有对应的机器指令——即无条件跳转指令。got。语句要与语句标号(标号必须符合标识符的规定)结合起来使用,格式如下:loop:语句1;语句2;语句n;gotoloop;语句n+2;语句n+3;这些语句的执行流程为:从语句1开始,顺序执行到语句n(这n条语句是一个基本处理流程1,got。语句后的语句是另一个基本处理流程2),然后执行got。语句,又无条件地跳回到语句lo注意:在语句1的前面使用了标号loop(标号要求是一个标识符),标号后面一定要有冒号。这样就构成了一个无限循环,也称为死循环。基本处理流程2(语句n+2及后面的语句)不会得到执行。要想上述结构不成为死循环,只需在无条件的got。语句前加上一个if语句,将上述语句段改为:loop:语句1;语句2;语句n;if(布尔表达式)gotoloop;〃这对应着一条有条件跳转机器指令语句n+2;语句n+3;这样一来,gPt。语句便成为嵌入在if语句中的分句。只有在布尔表达式为“真”的情况下,gpto语句才会得以执行(但这条包含gpto分句的if语句,恰好对应着一条有条件跳转机器指令)。这种结构中,在语句1到语句n之间,如果至少有一条语句(在语句1到语句n所构成的循环执行若干次后)能够使得布尔表达式为“假”,这就构成了一种正常的循环结构。所谓循环结构,本质上就是这样的一种结构,它可以在某个条件为“真”的情况下,重复执行一个基本处理流程(即循环体),在这个条件为“假”时,才执行下一个基本处理流程。 216这种用if语句加上无条件的往回跳转语句所构成的循环结构,其实与do…while循环语句的流程结构是完全一样的。do123句句4^语语语语句叫while(布尔表达式);语句n+2;语句n+3;do…while循环的流程为:先执行循环体(这是一个由语句1、语句2、…、语句n构成的复合语句),然后计算布尔表达式,其值如果为"真",就继续执行循环体,其值如果为"假”,则执行循环语句后的下一条语句。*习题,对于求1到100之和的问题,用if语句加上gpt。语句,构造出与while循环语句的流程完全一致的循环结构。答:i=l;ifQ>100)gotoloop2loopl:sum=sum+i;i=i+l;if。<=100)gotoloopl;loop2:printf("所摘橘子总数是泡",sum);点评:虽然if语句加上无条件跳转语句可以构成任何类型的循环,但任何源程序都可以只由顺序结构、选择结构和循环结构语句组成,完全可以不使用goto语句。过多地使用goto语句是造成程序流程混乱的根源所在。此处讨论goto语句构成循环有两个目的:一是可以加深对循环语句本质的了解;二是在很难写出标准循环语句的不得已的时侯,可以用这种循环来救急。重要结论:循环语句本身(不包括嵌入其中的分句)在机器语言层面上看,其实主要是由跳转指令构成的。例题7.2求数列1,3,5,…,99各项之和类型必修题开拓思路题趣味性*难度**数列的求和问题,一般都要设置三个变量:第/项用一个变量,来表示,第/项的数值用变量。来表示,前,项之和的变量为sum,参见表4.1。表5.1数列1,3,5,…99中各变量的取值用变量)来表示当前项123450用变量〃来表示当前第i项的值135799用变量sum表示前/项之和14916 217注意:任意第/项的〃值是前一项的n值加上2。一级算法:(1)前/项之和的变量sum初始化为零;/*前/项之和初始化*/(2)设置当前项/为第1项;/*循环变量初始化*/(3)设置第/项当前值为n=l;/*第,项的值n初始化1*/(4)while(i<=50)(sum=sum+n;/*将当前项的值n累加到变量sum中*/1=i+1;/*累计求和次数*/n=n+2;/*下一项的值为前一项的值加2,为下一次求和作准备*/)(5)输出sum的值;转换为C语言的程序如下:1#include 218w,i,n,sum);/*注意此句的书写位置*/14i++;/*更新/的值*/15n=n+2;/*更新〃值,用新老〃之间的关系*/16}1718printf("sum二%d 219”,sum);19return0;20)为了验证循环程序的正确性,在循环体中的13行增加了一个用来调试程序是否正确的语句,在每一次循环时都把各个变量的值打印出来,使读者能够看到是否与表4.1中所列的值一致。正式运行时可以将此句变为注释。点评:脚手架代码在循环体中第13行加入的打印语句,可把每次执行循环后的值,在显示屏上显示出来,这对于调试循环语句很有帮助。向同学们推荐这种调试程序方法。这一行代码又可称为“脚手架代码”:由于此代码在程序编写中的主要作用,是在编写程序期间,帮助我们验证程序是否正确无误。此行代码在程序编好以后,可以删除掉(使用编译预处理中的条件编译命令,可以自动让这种代码行不起作用,参见第9章)。这类代码的名称由此而来。不过,这种调试程序的数据,有时会与程序运行时的正常数据输出混合在一起,引起不便。在这种情况下,我们可以考虑将脚手架代码修改一下,令调试的数据不是在屏幕上输出,而是输出到某一个文本文件中(参见第9章9.4节)。程序的运行结果为:i=l,n=l,sum=li=2fn—3,sum—4i=3,n=5,sum~9i=50,n=99,sum= 220此题主要用的是通项变量n的新值和老值之间的关系来进行编程。点评:在任何数列求和问题,循环体中的语句sum=sum+n;/*新的前i项和与老的前i项和之间的关系*/i=i+l;/*求和进入下一项*/这两条语句都是一样的。只有求出通项变量n的新值的赋值语句,不同的应用问题是不一样的。对于此题,通过仔细观察表4.1,可以看出,当前项,与当前项的值a之间存在以下的等值关系。n=2*/-1所以,本题也可以用如下的程序实现。1#include 221",i,n,sum);14i=i+l;15n=2*i-l;〃注意只有此句不同16)1718printf("sum二%d 222”,sum);19return0;20}程序的运行结果为:i=l,n=l,sum=li=2,n=3,sum=4i=3,n=5,sum=9i=50,n=99,sum=点评:在数列求和的编程问题中,求通项变量n的新值有两种方法。一种是利用变量n的新值和老值之间的关系,另一种方法是利用变量n与数列项变量i之间的关系。问题.如果把第14行和第15行的句子顺序颠倒一下,结果是否正确?答:错误。例题7.3求1-1/3+1/5-1/7+1/9…共50项之和类型选修题趣味性**难度**算法分析:此数列求和问题的关键,是如何求出每一个通项〃的值。可以设置一些辅助变量。辅助变量上用来表示第,项的正负号,上的初值为lo在循环体中,新的4值与老的k值之间的关系为左=-1*4(也可写为k=-k)Q辅助变量m用来表示第,项的分母值,可以看出m的新值和老值之间的关系为m=m+2;,的初始值为1。有了这两个辅助变量左和m,就可以很容易求出第i项的通项变量的值 223n=1.0*Ar//n;于是,程序就很容易写出来。一级算法:(1)sum=0;(2)i=1;(3)k=l;(4)m=1;(5)while(i<=50)(5.1n=1.0*k/m;5.2sum=sum+n;5.3k=-k;5.4m=m+2;5.5i=i+1;}(6)打印sum;转换为c语言程序如下:1#include 22418printf("sum:%/,sum);19return0;20}问题1.如果变量sum和〃定义为整型,是否正确?答:错误。问题2.如果将赋值语句"=1.0*A/m;改为〃=4/m是否正确?答:错误。A和m都是整型变量,A/m的运算就是整除运算,而不是实数除法运算。习题1.编程求2/3-3/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就应当是否则k应当是1)o点评:遇到难一点的问题,思路常常是:能否先做一个类似的、更简单一些的题?或者能否设置一个或几个辅助变量来解题?例题7.4输出1〜999中能被3整除,且至少有一位是5的所有整数类型必修题算法类型穷举法趣味性*难度**一级算法:i=1;whiJe(i<=999)/*遍历1〜999中的所有数*/(分解出i的百位n3、十位n2、个位nl;①如果(i满足条件)②打印此数i;i++;}其中①和②步需要进一步求精。①的二级求精:n3=i/100;n2=i%100/10;nl=i%10;②的二级求精:/能被3整除:i%3==0./中至少有一位是5:(n3==5||n2==511n1==5)。由于这两个条件要同时满足,所以只需将以上的两个表达式用逻辑“与”运算符&&连接起来即可。(i%3==0)&&(n3==5||n2==5||nl==5)转换为C语言程序如下:1#include 2251i=l;2while(i<=999)3{4/*分解出i的百位n3、十位n2、个位nl;*/5n3=i/100;6n2=i%100/10;7nl=i%10;8if((i%3==0)&&(n3==5||n2==5||nl==5))9printf("%d,"j);10i++;11)12return0;13)问题.将第12行的逻辑表达式改为:(i%3==0)&&(n3==5||n2==5||nl==5),是否可以?答:不可以,在C语言中,if后面的布尔表达式从整体上一定要用圆括号全部括住。习题1.输出1〜999中能被3整除,或者至少有一位是5的所有整数。习题2.输出1〜999之间用5除余3并且用7除余4的所有数。例题7.4用蒙特卡罗法(即概率法)求圆周率类型:必修题开拓思路题算法类型:概率算法趣味性**难度**算法分析:利用随机函数求兀的值。将随机数的前后两个值(均在0〜1之间)作为点的x,y坐标,统计1万个点中,落入半径为1的四分之一圆内的点数“,从而计算出四分之一圆的面积s。s/边长为1的正方形面积-”/10000(4.1)随机落点越多,式(4.1)越接近于相等。又知由于四分之一圆的面积为:s=(n/4)rXz=jt/4(由于r=l)(4.2)由式(4.1)和式可以算出兀的值,如下:n=(4Xn)/10000图7.2用蒙特卡罗法求圆周率此方法又称为蒙特卡罗法,即概率算法,如图5.2所示。一级算法:n=s=0;while(s<=10000)(13.1到两个(均在0〜1之间)随机数x,y;13.2果x,y“落在"四分之一圆内那么圆内的落点数n=n+l;13.3s=s+l;/*累计总落点数*/pi=(4*n)/10000;输出pi的值;转换为C语言的程序如下:1#include 2269{10x=rand0/32767.0;11y=rand0/32767.0;12if((x*x+y*y)vl)13n++;〃有效落点数累加14s++;15}16pi=(4.0*n)/10000000;17printf("圆周率约等于%F,pi);18return0;19}C语言中有一个库函数rand。,每次调用此库函数,都可以得到取值在。〜RANDMAX(在VC++6.0中,这个系统定义的符号常量RANDMAX的值为32767)之间的一个整数,而且所有这些整数出现的几率都是1/(RANDMAX+1),所以把这个函数称为(伪)随机函数。如果我们要得到0〜m之间的随机数,可以将由此函数得到的数再取除以m的余数,即rand。%)。要注意的是,在调用此函数时要包含头文件math.h。*习题.求定积分:设/'(x)是[0,1]上的连续函数,且0 227}ave=sum/count;输出max、min>ave转换为c语言的程序如下:说明scanf()函数的返回值问题:scanfO函数返回成功赋值的数据项数,读到文件末尾出错时则返回EOFo如:scanff%d,%d",&a,&b);如果a和b都被成功读入,那么scanf的返回值就是2如果只有a被成功读入,返回值为1如果a和b都未被成功读入,返回值为0如果遇至I]错误或遇到Iendoffile,返回值为EOF。且返回值为inti.转换为C语言的程序如下:#include 228");scanf(n%f,,&x);/*假设第一个数为最大数max*/max=x;/*假设第一个数为最小数min*/min=x;/*求和变量初始化为x,不能初始化为。*/sum=x;/*循环变量初始化*/count=1; 229while(scanf("%F,&x)==l)(sum=sum+x;if(x>max)max=x;if(x 230”,max,min,ave);systcm(Mpausc,f);return0;问题1.是否可以把所有的实型变量改为双精度double类型?答:可以,但所有输入的scanf调用语句要修改输入格式为:%lf,输出变量的格式符不变。7.3do…while循环语句图7.3do…while循环的流程do---while循环语句的格式如下:do(循环体;)while(布尔表达式);/*循环语句的结束处*/循环后的下一条语句;do--while循环语句首先执行循环体,然后再计算布尔表达式的值是否为“真'如果为“真,则继续执行循环体;如果为“假”,则结束循环语句,执行下一条语句。do---while循环与while循环大同小异,最大的区别在于,do…while循环的循环体至少会被执行一次。其流程如图7.3所示。注意:对于do…while循环语句,不要漏写while(布尔表达式);中最后的分号,因为do…while循环语句是在此处结束的,必须要有一个分号。除此以外,使用do…while循环语句时容易出现的错误与while完全类似,这里不再赘述。问题:以下的do…while循环语句:do循环体;while(布尔表达式); 231是否可以省略花括号写成:do循环体;while(布尔表达式);答:一般不可以,除非循环体中只有一条语句。练习1.请你将用while循环的求1到100之和,按上述步骤改写成do-while循环。练习2.请你将前面用while循环做的例题选择至少两题,改为do〜while循环。例题7.6将一个十进制正整数d转换为〃(n<9)进制数类型必修题趣味性**难度**算法分析:参考第1章提高部分十进制数转化为2进制数的方法,将10进制转化为任意n进制的算法也是类似的。将十进制数转换成n进制数的短除法:(见第1章P页,只需将其中的2换成n即可)短除法把要转换的数不断的除以n然后取余数,商作为新的待转换的数;当商为0的时候结束。然后把余数倒着写出来。一级算法:1.读入一个10进制数d2.读入要转换的数制n3.while(d>0)(求d除以n的余数,将其打出;/*得到一个转换后的低位n进制数*/d/n->d;/*得到新的d,为求下一个余数作准备。*/)4.1.束1#include 232");9scanfC,%dH,&n);10while(d>0)11{12printf("%d”,d%n);13d=d/n;14}/*endwhile*/ 23314return0;151*习题1:此题转换后的n进制的数值在一行上要倒过来读;你能否修改程序,让其按自然顺序由高到低显示出来?(提示,在循环前,可以先输出一些个空格,然后在循环中,每输出一个数字就后退两格,退一格不对)参考答案:程序改为:1#include 234");7scanfC%dn,&d);8printf("请输入要转换成几进制数?输入的整数要小于10 235");9scanfC%dM,&n);10printfC\t\t\t\t\t");/*注意这一行是新加的,将光标右移5个水平制表符的位置*/11whilc(d>0)12{13printf("%d\b\b",d%n);/*\b是退格字符,每打印一个数字退两格*/14d=d/n;15}//endwhile16printf(“ 236”);/*换一行留给系统打印信息,否则,pressany…会将有效数据复盖掉*/17return0;18}此题用到的转义字符\t和\b,请看第二章提高部分。*习题2:在学完数组后,请将此题修改为把转换后n进制的各位数存放到一个整数数组中并输出。点评:将10进制数转换为任意其他进制数,在程序设计中,是一种必须掌握的常用编程技巧。因为在将多重循环转换为二重循环时,这是必要的知识。参见第63讲盒子里的气球。习题1.本讲假设了d是正整数,请将一个十进制整数d转换为n(n<9)进制数,d可以是负数。*习题2.请将一个十进制整数d>0转换为十六进制数,利用英文字母A、B、C、D、E、F来分别表示十六进制中数值相当于十进制的10、11、12、13、14、15。在十六进制中,这些都是个位数,不需进位。表5.2十六进制和十进制数的对照表16进制的数相当于10进制的数16进制的数相当于10进制的数16进制的数相当于10进制的数1177C122288D133399E14 23744A10F1555B11101666**习题.将一个4位的”W9进制数转换为十进制数。提示:这个n进制数的数制就是变量n的输入值。而这个n进制数的各位上的数是以十进制整数格式输入的,即如果从键盘输入的n为7,那么输入的这个整数值的每一位都不能比6大(七进制数每位数最大的值是6)。也就是说,如果输入3026,就把它当作七进制数(3026)7,必须将此数的每一位都利用整数除法或整数取余“%”分解出来,然后利用以下的公式m=3X73+OX72+2X71+6X70这样才能得到这个十进制的数。点评:数值的进制转换在计算机科学、编程和软件中应用广泛,读者务必熟悉。在将任意多重循环转换成二重循环时也要用到,参见例题:盒子里的气球。例题7.7不用整除运算符和取模运算符"%”,求m除以〃的商和余数,其中m和〃均为整数类型选修题趣味性*难度**算法分析:设置一个记数器变量八/的初值为0。如果m比〃大,就将m的值减去〃,得到新的m,并将/,加1,然后继续上述过程,直到m小于〃为止。一级算法:读入m,nif(m 238');8scanf(M%d%dM,&m,&n); 2391if(m 240");7scanfC%dH,&n);8flag=l;/*假设n是素数*/9i=2;10whileG<=n-1)11{12if(n%i==0)13flag=0;/*因为n不是素数*/ 2412i=i+l;3}4if(flag==1)5printf("%d是素数 242”,n);6else7printfC%d不是素数 243" 244・逗号运算符和逗号表达式:逗号运算符在形式上就是一个或多个逗号,它是C语言中优先级最低的运算符,其形式是:表送式7,法送式2,表达式3所有这些用逗号隔开的子表达式统称为逗号表达式。在对逗号表达式求值时,首先求出表达式1的值,然后再去求表达式2的值,再求表达式3的值。以最右边的表达式(这里是表达式3)的值,作为整个逗号表达式的值。逗号表达式中子表达式的个数并不限于2个或3个,可以是任意多个。逗号表达式主要用在循环语句for语句的头部的表达式1(循环前的变量初始化)和表达式3(循环变量的更新)中,请参见下一节,在其他地方尽量不要用,因为容易出现误用。例如:for(sum=0,i=l;i<=100;i++)sum=sum+i;是在for循环的表达式1中使用逗号表达式的例子(参见下一节)。1.6for语句不论是while循环,还是do--while循环,初学者最常犯的两类错误如下:(1)循环前忘记了循环变量的初始化,造成循环次数错。(2)循环体中漏掉了对循环变量进行更新的语句,导致无限循环。为此高级程序设计语言的设计者们提供了一种更好用的循环语句,即for语句。for语句的形式为:for(表达式1;表达式2;表达式3)(循环体;}/*for循环语句结束处*/循环后的下一条语句;for语句的执行流程如下:(1)先执行表达式1(表达式1用来进行循环前的初始化操作)。(2)然后计算表达式2(表达式2就是判断循环体是否执行的布尔表达式)(3)如果表达式2的值为“真”,那么执行循环体;否则跳转到循环后的下一条语句(即第(6)步)。(4)执行完循环体后,执行表达式3。(5)跳转到第(2)步。(6)执行循环后的下一条语句。for循环的流程如图4.4所示。比如,求1到100之和的程序,可用for循环来改写为:1#include 2451{2sum=sum+i;3)4printf(-1到100之和为%d”,sum);5return0;6}注意:(1)原来while循环语句前的循环变量初始化的语句变成了fdr语句中的表达式1。(2)原来while语句中的布尔表达式变成了for语句中的表达式2。(3)原来while语句循环体中的循环变量更新语句,变成了fbr语句中的表达式3。在fbr循环语句中的三个表达式中,表达式1仅仅执行一次,其他两个表达式都可能执行多次。在循环体执行之后,先执行表达式3,然后再执行表达式2。表达式2的值决定了是否再次执行循环体。点评由于在fbr循环中强制性地要求编程者给出循环变量的初始化语句和循环变量的更新语句,所以fbr循环语句其实就是一种更不容易出错的循环语句。注意:for循环中的三个表达式都是可以省略的。比如在以下例子中,省略了表达式1和表达式3(但是,分隔三个表达式之间的分号不能省略):1#include 246例题7.11求1+22+333+4444+55555+666666…共9项之和类型选修题开拓思路题趣味性***难度**算法分析:此题初看起来,好像没有思路。我们可以考虑一个更为简单而又有些类似的问题,即求1+11+111+1111+11111+111111…共9项之和。此问题通过简化求解起来就容易多了,因为此题中通项变量n的新值与老值之间的关系非常简单,〃的初值是1,则有:n=n*10+1;所以此题很容易编出如下程序:n=1;sum—0;for(i=l;i<=9;i++){sum+=n;n=n*10+1;在此题的基础上,再来考虑原题就容易多了。这里引入辅助变量m来取代简化题中的〃(因为本书有个约定,在数列求和问题中,用变量”来表示通项值)。首先写出数列的各项值,包括辅助变量m的值,见表4.3。表7.3数列的各项值数列项号变量i123辅助变量m111111数列通项变量n122333由表4.3很容易得到通项变量〃的表达式,如下:/7=/77*/;通过引入一个辅助变量m,可使求和问题很容易得到解决。算法如下:sum=0;//Im=l;//2i=l;〃3n=m*i;〃4while(i<=9)//5(sum+=n;//6m=m*10+l;//7i+十;〃8n=m*i;〃9)输出sum;习题1.此题请读者自己根据此算法,写出C语言的源程序。习题2.用for循环来做此题,并在for(表达式1;表达式2;表达式3)中使用逗号表达式。问题1.如果不小心漏掉了第4句,结果会怎样?答:第一次循环时,执行第6句时加到变量sum上的n值是垃圾数据,n没有得到正确的初始化。问题3.此题的算法是否可以改成如下形式?sum=O;m=l;i=l;while(i<=9)(n=m*i; 247sum+=n;m=m*10+l;i++;}输出sum;答:可以,这种形式更为简洁。因为在原来的方案中,最后一次执行循环的第9句没有用上。而且,此题的形式改用for循环也更容易。点评:编程思路之一如果解某个题没有思路,可以考虑将其简化,转换成一个更为简单的题。更为简单的题一旦获得解决,对解决原题可能有很大帮助。*习题2.求n+nn+nnn+nnn共10项之和,n49,n的值由用户在程序运行时输入。**习题3.求x-x73!+^/5!-7/7!+…直到所求项的绝对值小于10的负6次方。要求读者不用pow。函数来做。(提示:将分子、分母和系数分开来考虑)点评:关于多项式的求和在高等数学中证明了许多函数可以用多项式的无穷级数来展开,因此,能解此题的读者可以利用此题来求许多复杂函数的近似值。7.7break语句和continue语句为了使循环控制更加灵活,C语言允许在循环体中(使用选择结构语句或条件运算符来)测试某个条件是否成立;条件成立,则执行break语句,或者执行continue语句。break语句用来中断循环语句的执行,流程转向执行循环语句之后的下一条语句;continue语句用来跳过循环体中尚未执行的其余语句。但用在for循环中与用在while或do-while循环中的continue语句有些区别:continue语句用在for循环中,跳过循环体中尚未执行的其余语句,执行对循环变量进行更新的表达式3(即执行for(表达式1;表达式2;表达式3)中的表达式3),然后计算表达式2,根据表达式2的值来决定是否还要执行循环体continue语句用在while或do-while循环中,跳过循环体中尚未执行的语句,转向是否还要继续执行循环体的条件判定(即计算while(表达式)中的表达式)。•break语句和continue语句对循环控制流程的影响:break语句使得循环语句终止了,程序流程自然转向执行循环后的下一条语句;而continue语句仅仅使得本次循环体的执行终止了,但循环语句是否继续执行,取决于对循环条件的判定注意:1、break既能用于循环语句又能用于switch语句,而continue只能用于循环语句。2、break语句和continue语句用于多重循环(一》)时,只影响包含这些语句的那一层循环,与外层循环无关。比如:for(i=0;i<10;i++){语句1;for(j=5;j>=l;j-) 248(if()break;语句2;}语句3;}语句4;执行break语句之后,程序流程转向执行语句3,决不是转向执行语句4。再比如:for(i=0;i<10;i++){语句1;for(j=5;j>=l;j-){if()continue;语句2;}语句3;)语句4;执行continue;语句之后,程序流程转向执行语句j-,然后执行j>=l;而不是转向执行i++o例题7.9求任意一个正整数n(n>=2)是否为素数(break语句的用法)类型必修题趣味性**难度**算法分析:仍然用变量/作为除数来除只要〃能被/除尽,就立即用break语句跳出循环。循环结束后,可根据/的当前值来判断循环是正常结束的,还是中途被break语句中断的。如果循环结束后/的值等于〃,说明循环是正常结束的,任何/值都不能除尽〃,所以“是素数。一级算法:输入n将2-iwhile(i<=n-l)(如果(c除以/的余数为零)那么break;〃条件成立,则n不是素数,不必再循环"1一,〃循环变量更新}如果(i==n)〃这说明循环正常结束,if语句的表达式从未为真,分句break没有执行那么打印〃是素数否则,打印。不是素数转换为C语言程序如下:1#include 2491intmain(void)2(3intn,i,flag4/*输入n*/5printf(“请输入一个整数、n");6scanf("%d",&n);7i=2;8while(i<=n-1)9{10if(n%i==0)11break;12i=i+1;13}14if(i==n)15printf("%d是素数 250”,n);16else17printf("%d不是素数 251",n);18return0;19}问题1.如果将第15行的if语句if(i二二n)改为if(i=n),判断结果将会如何?问题2.如果不小心把第8行的语句改为i=1,结果将会如何?问题3.如果把循环体中第11、12、13行的语句改为:if(n%i==0)break;elsei=i+l;结果会如何?问题4.如果12行错用continue语句代替了break语句,结果将会如何?运行一下程序,看看你的判断是否正确。例题7.10continue语句的用法输出10到1000之间不能被2或5或7整除的整数#includc 252讨论:3+4*5这个式子等于多少?以前的简单电子计算器不管先乘除后加减的运算规则,而是先执行加法3+4得到7,然后再做乘法7*5,得到35,而不是23。本题就是要做一个不按照先乘除后加减的规则来进行运算的简单计算器。算术表达式中也不能使用圆括号。算法分析:本题可在第12讲的基础上更进一步,通过编程来实现一个最简单的计算器。由于问题较复杂,可以先考虑不用循环应该如何去做。一级算法:读入表达式中的第一个数一X读入第一个运算符-ch如果(ch是等号)跳转到loop处;读入表达式中的下一个数一»y根据ch得到x与y运算的结果—result读入下一个运算符—ch如果(ch是等号)跳转到loop处读入表达式中的下一个数-*y根据ch得到result与y运算的结果一resultloop:打印result的值;可以看出,只要读入的算术表达式中的下一个运算符ch不是等号,这个过程会一直进行下去。除了第一句以外,剩下的每4句一组,每组的算法都相同,只有第一组的4句有些不同。但是,只要把变量x改为result,那么所有的语句组都相同。以下是修改后的算法:读入表达式中的第一个数fresult〃此句进行了修改读入下一个运算符—ch如果(ch是等号)跳转到loop处读入表达式中的下一个数一y根据ch得到result与y运算的结果—result//此句进行了修改读入下一个运算符-ch如果(ch是等号)跳转到loop处读入表达式中的下一个数一*y根据ch得到result与y运算的结果-resultloop:打印result的值;这样就为构成循环语句创造了良好的条件。但是,此题与前面讲过的循环有所不同。第一,因为循环的次数不是固定的,应当在读入的运算符是等号"="时结束循环。第二,控制循环的变量不是整型变量,而是字符变量ch。因此,在构成的循环体前,对循环变量ch的初始化不能少。为此,将第一个分组中的第一句取出来,使之成为循环前的循环变量初始化语句。这就要重新修改语句分组,并在最后一个分组的最后加上一句:"读入表达式中的下一个运算符一ch”,但增加此句并不会改变result的最终取值。最终形式如下: 253读入表达式中的第一个数—result读入第一个运算符-ch如果(ch是等号)跳转到loop处读入表达式中的下一个数根据ch得到result与y运算的结果—result读入表达式中的下一个运算符一ch如果(ch是等号)跳转到loop处读入表达式中的下一个数一y根据ch得到result与y运算的结果—result读入表达式中的下一个运算符一chloop:打印result的值;至此,准备工作已经全部完成。使用循环后的一级算法如下:读入表达式中的第一个数-result读入表达式中的第一个运算符一chwhile(ch不是等号'='){读入表达式中的下一■个数一y根据ch得到result与y运算的结果—result①读入表达式中的下一个运算符一ch)打印result的值其中只有①步需要进一步求精。根据ch得到result与y运算的结果—result根据ch的取值执行以下的一条语句case:result二result+y;break;case*-*:result=result-y;break;case:result=result*y;break;case*/*:result=result/yjbreak;转换为c语言的程序如下:1#include 254")8scanf(M%f%c,1,&result,&ch);9while(ch!='=*)10{11scanf(”%F,&y);12switch(ch)13{14caseresult=result+y;break;15case*-*:result=result-y;break;16caseresult=result*y;break; 2554case7':result=result/y;break;5}6scanff'%cM,&ch);/*更新循环变量*/7}8printf(,,result=%f 256,*,result);9gptcheQ;/*回到屏幕看结果*/10return0;11}注意:在运行此题程序输入数据时,要连着输入,其间不要按空格键,也不要按回车键。算术表达式和等号全部输完后,再按回车键即可(其原因参考下面问题2)。问题1.如果不小心漏掉了第22行,结果会怎样?答:循环变量ch的值得不到更新,造成死循环。点评此题的循环变量用的是字符变量,但仍要注意对循环变量的初始化(第97行),以及在循环体中对循环变量进行更新(第22行)。**问题2.此程序运行时,只要在运行到第7行时,一次性地在一行中输入一个完整的、不带圆括号的算术表达式,然后按回车键即可。不需要在每输入一、两个数据后就按一下回车键(而且这样还会出错),为什么?答:这是由于格式化输入函数scanf()是一种带有缓冲区的输入函数(参见第8章),按回车键前输入的所有数据(字符序列)都被一次性地存放在输入缓冲区中,并形成一个标准输入流stdin,此流中的数据并没有立即被送到变量的内存区。在按回车键之后,第一个数和第一个运算符被从流中取出来。通过一个scanf()函数,送给表达式中的第一个数和第一个运算符。但其余一次性输入的数据和运算符仍然在标准流中,等待程序中后面的scanf()输入函数按先后顺序依次读取使用,直到输入表达式的等号才完全结束。要进一步了解缓冲区和流的概念,可参见第8章。*习题:你能否把此题改为像电子计算器那样,输完一个数和运算符后就得到一个当前的结果,输完等号后得到最终结果?7.8循环语句的嵌套循环语句中的循环体中,还可以包含一个或多个循环语句,这种情况称为循环语句的嵌套。内嵌的循环语句中还可以再嵌入循环语句,这就构成了多重循环结构的嵌套。已经介绍的三种循环语句都可以嵌套和相互嵌套内层循环语句,必须完全包含在外层循环语句的循环体中,不能发生交叉,每一层循环必须在逻辑上是完整的。例如:语句1;while(表达式1){分句1;while(表达式2){循环体;}/*内层while循环结束*/分句3; 257}/*外层while循环结束*/就是一个二层循环嵌套(又称为二重循环)的结构。外层循环语句的循环体由分句1,内层循环语句和分句3构成。其中分句2和分句3不是必须有的。while也可以是for或者do〜while。while、for和do〜while之间,也都可以互相嵌套。例题7.12求10万以内的所有素数类型选修题趣味性***难度***一级算法:n-2当〃<=100000(求”是否为素数,如果是,则打印此数①/7=/7+1;}其中的①步需要进一步求精(其中求任意的〃是否为素数的算法,参见例题5.8)。二级求精:flag=l;i=2;while(i<=n-l)(如果(n除以i的余数为零)那么flag=0;i=i+l;如果flag等于1那么打印"是素数转换成C语言程序如下:1#include 258”,n);1819n=n+l;/*更新循环变量*/20}/*endwhilen*/2122return0;23}*问题.此题得到的结果大多数在屏幕上一闪而过,能否将结果保存在一个文件中,或者把它用打印机打出来?・二重循环中语句的五个书写位置 259如以上例题所示,二重循环中共有五个位置可以书写语句(以while为例,其他循环也都类似):①〃循环之前while//startwhile外部循环②〃外部循环内部、内部循环之前while//startwhile内部循环{③〃内部循环内部}//endwhile内部循环④〃外部循环内部、内部循环之后}//endwhile外部循环⑤〃循环之后到底应当将哪些语句放在上述5个位置的哪一个位置,这是需要编程者认真考虑的,如果位置放错了,肯定得不到正确结果。注意:下面的打印图形系列是专门为提高读者运用二重或多重循环编程的能力而设置的,希望读者循序渐进、按部就班地学习这些例题。例题7.13打印图形12123123412345类型必修题开拓思路题趣味性**难度**i=l1i=212i=3123i=41234i=512345算法分析:设一个变量来控制打印第几行,首先考虑任意的第,行怎么打印。要打印的数字个数,第1行是1个数,第2行是2个数,总结出一般规律:第/行打印,.个数。再来观察每一行要打印的数字,发现都是从1开始打,后面要打印的数都是前面那个数加1,所以可以用一个变量/来表示第/行要打印的每个数字,/的初值为1,每次打印后都更新为下一次循环要打印的数:/=#1,循环变量/的终值为/(因为第/行一共要打印,个数)。得到打印任意第/行的算法如下: 260F1;while(j<=i){打印j,打印空格;H+1;〃为打印下一个数作准备;)下面考虑所有行的打印方案。一级算法:for(i=l;i<=5;i++)〃一共打印5行(打印任意第i行;一行;由于打印任意第i行的二级求精算法已经给出,将其结合起来则成为:for(i=l;i<=5;i++)/*打印任意第i行7j=l;whileG<=i)打印i,打印一个空格i=i+i;}换行;}转换成c语言程序如下:1#include 261例题7.14打印图形ababcabedabcde类型必修题开拓思路题趣味性**难度**j=12345i=lai=2abi=3abci=4abedi=5abcde算法分析:此题的算法可借鉴例题5.13,只不过要打印的数据要另外设置一个字符变量。首先考虑任意的第7行怎么打印。要打印的字符个数,第1行是1个,第2行是2个,第/行就要打印了个字符。每行要打印的字符都是从‘a'开始,后面要打印的字符都是前面那个字符的ASCII码加1;所以可以用一个字符变量ch来表示要打印的字符,ch的初值为‘a',每次打印后都更新为下一次循环要打印的字符ch=ch+1。由于每一行要打印i个字符,控制每一行打印次数的循环变量/的初值为1,终值为A打印任意第"亍的算法如下:j=1;ch='a';whilefl<=i){打印ch,打印空格;ch=ch+1;i=i+i;}下面考虑所有行的打印方案。一级算法:for(i=l;i<=5;i++)(打印任意第i行;换行;}由于打印任意第,行的二级求精算法已经给出,将其结合起来则成为:for(i=l;i<=5;i++)/*打印任意第i行*/1=1;ch='a';while。<=i)打印ch,打印空格;ch=ch+l;/*ch变成下一次内部循环要打印的字符*/j=j+i;/*内部列循环变量j更新*/ 262}换行;}转换成c语言程序如下:1#include 263113135135713579(提示:另设一个整型变量k来打印第i行的数字,每一行k的初值是1,每次打印完后增加2)习题2.打印以下图形。abcdeabed例题7.15打印图形bacbadcbacdcba类型必修题开拓思路题趣味性**难度**算法分析:先解决任意第/行如何打印。循环变量,的初值为1,任意第,行要输出了个字符。控制第,行输出的循环变量可以用字符变量"的初值是多少暂时不管,终值一定是字符a。由于第/行一共要输出,•个字符,可以由此反过来推出ch的初值应该是多少;对于第i行,猜想有如下等式:ch初值的ASCII码-ch终值的ASCII码=i但这个式子不对,将此式应用于第2行(i=2)就可以发现错误,字符b的ASCII码-字符a的ASCH码的值等于1,而不是2。所以,适用于任何一行的正确的等式如下:ch的初值的ASCII码-ch终值的ASCH码=i-l因此,对于任意的第i行,则有:ch的初值的ASCII码=i-1+ch终值的ASCII码也就是:ch的初值=i-1+'a'现在,可以很容易地写出在屏幕上输出任意第i行的算法。输出任意第i行的算法如下:ch=i-1+*;while(ch>=,a,)(输出字符变量ch的值,输出一个空格;ch—;}现在来考虑输出所有5行的算法如下:i=l;while(i<=5)(输出任意第i行换行;i++;其中对①步进行进一步求精的算法已如上所述。C语言的程序如下:1#include 2641inti,j;2charch;34i=l;5while(i<=N)6{H/*输出任意第i行*/12ch=i-1+W;13while(ch>='a')14{15/*输出字符变量ch的值*/16printff,%cM,ch);/*输出字符变量ch的值,输出一个空格;*/17ch—;18}1920printfC'n');21i++;22}23return0;24}・此题还有另一种解法:仔细查看所有行的第一个字符,可以发现:每一行的第一个字符都是前一行的第一个字符的ASCII码加lo我们用一个字符变量H。来跟踪表示每一行的第一个字符。任何一行上的字符也很有规律,若任何一行上要打印的字符用字符变量/来表示,ch的新值和老值存在着如下的关系:ch=ch-1一级算法:i=l;chO='a'while(i<=5)(输出任意第i行;chO=chO+l;/*为下一个新行准备第一个字符*/换行;i++;}其中输出任意第i行算法需要进一步求精:ch=chO;/*初始化第i行的要打印的第一个字符ch*/while(ch>=*a*)(打印ch和一个空格;ch=ch-1;}将上述算法结合在一起,如下:i=l;chO=,a,while(i<=n){/*输出任意第i行*/ch=chO;whilc(ch>=')(打印ch和一个空格;ch=ch-1;chO=chO+1;/*为下一个新行准备第一个字符*/换行;i++;问题l.ch=chO这一句起到了什么作用? 265*问题2.请用例题3.2中介绍的画内存变量取值变化图的方法,验证这种算法是否正确(可以仅验证i为1和2的前两行打印的情况是否正确)。习题1.请读者自己将本题算法转换成C语言的程序。习题2.编程打印九九乘法表。1X1=1,1X2=2,2X2=41X3=3,2X3=6,3X3=9例题7.16打印图形1121123211234321123454321类型必修题开拓思路题趣味性**难度科*算法分析:首先将注意力集中到任意的第/行如何打印上,先不考虑要打印的空格。也就是说,先将此题转化为更简单的另一题。1121123211234321123454321先将此题搞定,原题就容易多了,这就是一种简化问题的常用方法。如果一个问题较难,是否可以编出与原题类似但又更简单的问题的程序。可以看出,如果令/的初值为1,第/行要打印的数据有2"1个(如果/.从1开始取值的话),用一个循环变量/来表示打印给定一行时的循环,则/的取值应当是从1到2*/1。每一行要打印的数值用一个整型变量〃来跟踪表示,则此变量的初值应为1。当六,时,下一个要打的A值比当前A值大1,即A=A+1;而当以后,下一个要打印的A值比当前A值小lo打印(任意)第,行的一级算法:尸1;n=l;while(j<=2*i-l)(打印n的值,打印空格;如果(j 266由此,可以写出打印5行数字的一级算法:i=l;while(i<=5){打印第i行;打印换行;i=i+l;)前面打印任意第,行的一级算法,在这里就成为打印第/行的二级求精。while(i<=5){i=Un=l;whde(j<=2*i-l)(打印n的值;如果(jvi)那么n=n+l;否则n=n-1;}打印换行;i=i+l;)转换成c语言的程序如下:1#include 267”);21i=i+l;22}/*endi*/23return0; 268现在考虑任意第/行数字前要打印的空格,关键要考虑在任意的第7行,首先要打印几个空格,/越小,打印的空格越多。这很容易实现。比如,用10-,来表示第/行要打印的空格数,/越小,10-7的值就越大。下面给出其伪代码。一级算法:l.i=l;2.while(i<=5){打印第i行的空格;①打印第i行的数字;②换行;i++;}②的二级求精已经介绍过,下面给出①的二级求精。for(k=l;k<=10-i;k++)printfC");/*打印一个空格*/总的算法为:i=l;while(i<=5){/*2.1打印第i行的空格*/fbr(k=l;k<=l0-i;k++)printff1n);/*打印一个空格*//*2.2打印第i行的数字*/尸1;n=l;while(j<=2*i-l)|打印n的值;如果g)那么n=n+l;否则n=n-l;尸j+1;)换行;i++;}转换成C语言的程序如下:1#include 2691/*2.2打印第i行的数字*/ 2701j=l;2n=l;3while。v=2*i-l)4(5printf(”%d”,n);67n=n+l;8else9n=n-l;10j=j+l;11}/*endwhilej*/12/*2.3换行*/13printfO14i++;15}/*endwhilei*/16return0;17}习题1.请将此题的内层循环改为for循环。习题2.打印以下图形。aacaacecaacegeca以上图形一共7行。*习题3.打印以下图形。abcdcbaabcbaabaa**习题4.打印以下图形。acegecagecaceg*习题5.打印以下图形:a23def78910(提示:可以先考虑简单一些的题——打印以下图形:123 271例题7.17四大湖问题上地理课时,四个学生回答我国四大淡水湖的大小时说:A学生:洞庭湖最大,洪泽湖最小,鄱阳湖第三。B学生:洪泽湖最大,洞庭湖最小,鄱阳湖第二,太湖第三。C学生:洪泽湖最小,洞庭湖第三。D学生:鄱阳湖最大,太湖最小,洪泽湖第二,洞庭湖第三。对于每个湖的大小,每人仅答对一个,求出各湖的大小顺序。类型必修题开拓思路题趣味性**难度***算法分析:此题可用蛮力法或称为穷举法,将一切的可能排序都遍历,然后在所有的各湖大小排序中,找出满足每个学生只答对一个的答案。以变量而表示洞庭湖的大小,所有可能的取值是1、2、3、4o以变量人力表示洪泽湖的大小,所有可能的取值是1、2、3、4o以变量由表示太湖的大小,所有可能的取值是1、2,3、4o以变量侬表示鄱阳湖的大小,所有可能的取值是1、2、3、4o这4个变量的取值满足以下的条件(四个湖的大小各不相同,且有一个第一,一个第二,一个第三,一个第四)。dh+hh+th+ph=10下面来看如何表述才能使学生的回答中只有一个是正确的。A学生的三个回答,用变量的取值大小表述如下:洞庭湖最大,dh==l,(用取值1表示此湖最大,即排名第一。)洪泽湖最小,hh==4。鄱阳湖第三,ph==3。这三个关系表达式中只有一个为"真"(取值为1),其他两个为"假”(取值为0),所以A学生的叙述可以表示为:((dh==l)+(hh==4)+(ph==3))==l同理,B学生的叙述:“洪泽湖最大,洞庭湖最小,鄱阳湖第二,太湖第三。”可以表示为:((hh==1)+(dh==4)+(ph==2)+(th==3))==l同理,C学生的叙述:“洪泽湖最小,洞庭湖第三。”可以表示为:((hh==4)+(dh==3))==l同理,D学生的叙述:“鄱阳湖最大,太湖最小,洪泽湖第二,洞庭湖第三。”可以表示为:((ph==l)+(th==4)+(hh==2)+(dh==3))==l下面用四重循环来表示此题的一级算法。for(ph=l;ph<=4;ph++)/*遍历鄱阳湖的所有排名*/for(th=l;th<=4;th++)/*遍历太湖的所有排名*/for(hh=l;hh<=4;hh++)/*遍历洪湖的所有排名*/for(dh=l;dh<=4;dh++)/*遍历洞庭湖的所有排名*/如果(各湖排名互不相等)如果(A学生只答对了一个并且B学生只答对了一个并且C学生只答对了一个并且 272D学生只答对了一个)打印ph,th,hh,dh的值 273此算法的效率比较低,而且"各湖大小互不相等”的二级求精比较麻烦。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=1;hh<=4;hh++)/*遍历洪湖的所有排名*/如果(三个湖排名的取值互不相等)(dh=10-ph-th-hh;/*确保四个湖排名互不相等*/如果(A学生只答对了一个并且B学生只答对了一个并且C学生只答对了一个并且D学生只答对了一个)打印ph,th,hh,dh的值}/*end如果(各湖大小互不相等)*/}/*endforhh*/可以看出,经过改进后的算法效率更高,但程序稍稍难懂了一些。各湖排名的取值互不相等也得到了简化,成为判别三个湖大小互不相等。ph!=th&&ph!=hh&&th!=hh第二步改进:第三重循环可以在更小的范围内进行,不需要在鄱阳湖ph和太湖th的所有取值的遍历下去考虑,而只需在满足鄱阳湖排名不等于太湖排名的条件下进行循环。所以算法可以进行进一步改进。第二步改进后的算法如下:for(ph=l;ph<=4;ph++)for(th=l;th<=4;th++)如果(ph!=th)/*遍历鄱阳湖的所有排名*//*遍历太湖的所有排名*/for(hh=1;hh<=4;hh++)/*遍历洪湖的所有排名*//*如果鄱湖排名不等于太湖排名*/如果(各湖排名的取值互不相等)dh=10-ph-th-hh;如果(A学生只答对了一个并且B学生只答对了一个并且C学生只答对了一个并且D学生只答对了一个)打印ph,th,hh,dh的值}/*end如果(各湖大小互不相等)*/ 274}/习题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也不去。endforhh♦/}/*end如果(ph!=th)*/在以上的算法中,各湖排名的取值互不相等也进一步得到了简化,最终成为:ph!=hh&&th!=hh1#include 27578910111213141516171819202122if(ph仁th)/*在ph不等于th的情况下考虑洪湖排名*/{for(hh=l岫v=4;hh++)/*遍历洪湖的所有排名*/if((hh!=ph)&&(hh!=th))/*如果三湖排名各不相同*/|dh=10-ph-th-hh;/*四湖排名各不相同7if(((dh==l)+(hh==4)+(ph==3))==l&&((hh==l)+(dh==4)+(ph==2)+(th==3))==l&&((hh==4)+(dh==3))==1&&((ph==l)+(th==4)+(hh==2)+(dh==3))==l)/*打印ph、th、hh、dh的值*/primf("鄱阳湖排名%d,太湖排名%d,洪湖排名%d,洞庭湖排名%dH,ph,th,hh,dh);23return0;24}问题1.请问程序的哪条语句确保了dh与其他各湖排名不相等?习题1、(摘自《程序算法与技巧精选》郭继展等机械工业出版社P221)有四种不同的果树,栽成了一排。生物刘老师叫四个学生猜出树名。张说:“第一棵是梨树,第二棵是枣树,第三棵是杏树。第四棵是桃树”。王说:“第一棵是梨树,第二棵是桃树,第三棵是枣树。第四棵是杏树”。李说:“第一棵是杏树,第二棵是枣树,第三棵是梨树。第四棵是桃树”。赵说:"第一棵是桃树,第二棵是梨树,第三棵是杏树。第四棵是枣树”。刘老师听了,摇摇头说:“只有三个人每人说对了两种,有一个人四种说的全不对。你们的基本常识还需要加强啊!”请问这四棵树的树名依次应该是什么?请问应该让谁去?(提示:用一个整型变量a表示A去(a为1)还是不去(a为0),其他人也类似。)点评:虽然在很多场合中,将关系表达式运算得到的逻辑值(或几个关系表达式之和)与整数值比较大小常常是错误的和无意义的。但在此题中却是一种例外的巧妙应用。点评:读懂程序和编写程序的诀窍之一仔细看清楚算法或程序的结构,有多个循环和选择结构交织在一起时,一定要搞清楚每个循环或选择结构是从何处开始,到何处结束;相互之间是嵌套关系还是并列关系;每个循环或选择结构嵌入了几条语句。特别要注意空语句和复合语句。7.9常见错误小结while循环语句经常容易出错的地方有以下几处。(1)while后面的布尔表达式忘了用括号括住,变为whilei<=100»还有while(i<=n)&&(j!=0)也少了括号,正确的应为while((iv=n)&&(j!=0))或者while(i<=n&&j!=0)。(2)布尔表达式的后面多了分号,变成while(布尔表达式);,导致循环体是一条空语句,而且循环是无限循环(因为在每次执行循环体后,循环的条件永远不变)。(3)循环前忘记对变量进行初始化,包括循环变量和循环中用到的变量。(4)在循环体中忘记写循环变量的更新语句,导致循环执行的条件(即布尔表达式的值)永远为 276"真",产生了无限循环。在循环变量为字符变量时最容易犯此错误。(5)漏写了括住循环体的花括号,导致循环体只有一句,这样也常常造成死循环。因为漏写花括号后,对循环变量的更新成为循环后的下一条语句。(6)循环的终止条件发生错误,即循环多了一次或少了一次。比如将while(循环变量<=常量)错写成while(循环变量〈常量),造成循环体中的语句少执行了一次,导致结果不对。(7)循环体的花括号后多加了一个分号。(8)在循环体中,出现改变循环变量终值取值的语句(如果终值是用一个变量来表示的话)。do…while语句与while语句类似,只不过do…while语句的while(布尔表达式)后面一定要有分号。for循环语句的常见错误除了while的常见错误外(仅有忘记循环前的初始化和对循环变量更新这两个错误不易犯),还容易犯以下错误。(1)三个表达式之间的两个分号被省略掉了。比如正确的应当是for(可能会被省略掉前后的任意一个或两个分号。(2)三个表达式之间用逗号隔开。比如for(i=l,i<10,i++)或者for(i=l,j=M,i 2770111第4条指令请问该表达式的值是多少?答:这个式子是具有副作用的表达式,在不同的编译环境下,得到的结果可能不一样。一些系统把5作为表达式中所有j的值,表达式的结果是10,j的最终值是3;而另一些系统按从左到右计算,即第一个(j-)表示参与加法运算的是5,然后j立即自减1,这样第二个参与加法运算的(j-)的值就是4了。表达式的计算结果为9。j的最终值仍然是3。1.无限循环,要构造无限循环语句,只需要用以下三种方式之一即可:while(l){循环体}或者:for(;;){循环体}或者:do{循环体}while(1);这些无限循环语句,往往在循环体中使用break语句或goto语句来结束。2.11习题一、选择题1.以下程序运行后的输出结果是()。main(void)(inti=l,sum=0;while(i<=4)sum二sum+i;i=i+l;printf("%d 278”,sum);1A.4B.5C.10D,死循环2.写出以下程序的运行结果。main(void){inty,a;y=2;a=l;while(y-!=-l){do{a*=y;a++;(while(yr—);}printf("%d,%d 279"ay);}3.写出以下程序的运行结果()omain(void){inty=10;for(;y>0;y-) 280if(y%3==0){printf(“%d”,~y);continue;}1.以下程序的输出结果是()=main(void)(){inta--1,b—0,k;if((++a<0)&&!(b-<=0))printf(n%d%d 281n,a,b);elseprintff%d%d 282H,b,a);}A.-11B.01C.10D.002.以下程序运行后的输出结果是()0main(void)(inta=5,b=4,c=3,d=2;if(a>b)if(b>c)printf("%d”,d+++1);elseprintf(M%d0,+4-d+1);printf(u%d 283u,d);}A.2B.3C.33D.433.以下程序执行后输出的结果是()。main(void){inti=0;switch®{case0:printf(,*%dH,i++);case1:printf(M%dM,i++);default:printf(,*%d,,j++);A.012B.OC.123D.1二、填空题1.执行语句for(i=5Q>0;i—);i—;后,变量i的值是。2.以下程序的功能是求s=l+(1+2)+(l+2+3)+…+(1+2+3+…+n)的值,请填空。main(void)(){inti,j=O,s=O,n;scanf("%d",&n);fbr(i=ly<=n;i++)printf(*s=%d 284*,s);J3.以下的程序片断:intn=0,p;do(scanf("%d",&p);n++;)while(p!=123&&n<3);其中do---while循环的结束条件是o三、编程题1.编程实现累加器功能,把用户输入的数据累加,直到输入0为止,输出累加结果。2.某人摘梨,卖掉一半,又吃了一个;第二天卖掉剩下的一半,又吃了一个;第三天、第四天、第五天都是如此,第六天一看,发现就剩下一个梨了。试求此人共摘了多少个梨。3.请编写一个程序,程序的功能是打印以下图案。************** 285****4.偷盗者问题:甲、乙、丙、丁4个人为嫌疑犯,只有一个是偷盗者。在审讯中,4人都有可能说真话或假话。编程推断谁是偷盗者。甲:乙没偷,丁偷的。乙:我没偷,丙偷的。丙:甲没偷,乙偷的。T:我没偷。*5.编程求一个十进制整数m的从低位开始数的第k位数是几。比如m=123456,它的从右数过来的第2位是5,第7位是0。其中n和k都由键盘输入。
此文档下载收益归作者所有