首页 信息学奥赛培训教程C++版

信息学奥赛培训教程C++版

举报
开通vip

信息学奥赛培训教程C++版目录5青少年信息学奥林匹克竞赛情况简介7第一章计算机基础知识71.1计算机的基本常识71.1.1计算机的产生与发展71.1.2计算机系统及工作原理81.1.3计算机中有关数及编码的知识101.1.4原码、反码与补码101.1.5逻辑运算111.2操作系统111.2.1DOS(DiskOperatingSystem)的组成111.2.2DOS的文件和目录121.2.3DOS命令121.2.4Windows简介131.3计算机网络常识131.3.1网络基础知识141.3.2Internet简介161.4计算机信息安全基...

信息学奥赛培训教程C++版
目录5青少年信息学奥林匹克竞赛情况简介7第一章计算机基础知识71.1计算机的基本常识71.1.1计算机的产生与发展71.1.2计算机系统及工作原理81.1.3计算机中有关数及编码的知识101.1.4原码、反码与补码101.1.5逻辑运算111.2操作系统111.2.1DOS(DiskOperatingSystem)的组成111.2.2DOS的文件和目录121.2.3DOS命令121.2.4Windows简介131.3计算机网络常识131.3.1网络基础知识141.3.2Internet简介161.4计算机信息安全基础知识161.4.1计算机的网络安全171.4.2计算机病毒171.4.3病毒的分类19第2章C++编程简介192.1机器语言、汇编语言和高级语言202.2C语言与C++的历史202.3C++ 标准 excel标准偏差excel标准偏差函数exl标准差函数国标检验抽样标准表免费下载红头文件格式标准下载 库212.4结构化编程222.5简单程序252.6简单程序:两个整数相加272.7算术运算292.8判断:相等与关系运算符312.9新型头文件与名字空间33第3章C++输入/输出流333.1简介333.2流343.2.1iostream类库的头文件343.2.2输入/输出流类和对象353.3输出流353.3.1流插入运算符373.3.2连续使用流插入/流读取运算符373.3.3输出char*类型的变量383.3.4用成员函数put输出字符和put函数的连续调393.4输入流393.4.1流读取运算符413.4.2成员函数get和getline443.5成员函数read、gcount和write的无格式输入/输出453.6流操纵算子453.6.1整数流的基数:流操纵算子dec、oct、hex和setbase463.6.2设置浮点数精度(precision、setprecision)473.6.3设置域宽(setw、width)483.6.4用户自定义的流操纵算子493.7流格式状态503.7.1格式状态标志503.7.2尾数零和十进制小数点(ios::showpoint)513.7.3对齐(ios::left、ios::right、ios::internal)533.7.4设置填充字符(fill、setfill)543.7.5整数流的基数:(ios::dec、ios::oct、ios::hex、ios::showbase)553.7.6浮点数和科学记数法(ios::scientific、ios::fixed)563.7.7大/小写控制(ios::upercase)573.7.8设置及清除格式标志(flags、setiosflags、resetosflags)583.8流错误状态61第4章文件处理614.1简介614.2文件和流614.3建立并写入文件654.4读取文件中的数据674.5更新访问文件68第5章C++的字符串流685.1流的继承关系685.2字串流的输入操作695.3字串流的输出操作705.4字串流在数据类型转换中的应用715.5输入/输出的状态标志74第6章控制结构746.1简介746.2算法746.3控制结构756.4if选择结构766.5if/else选择结构786.6while重复结构786.7构造算法:实例研究1(计数器控制重复)806.8构造算法与自上而下逐步完善:实例研究2(标记控制重复)856.9构造算法与自上而下逐步完善:实例研究3(嵌套控制结构)886.10赋值运算符886.11自增和自减运算符916.12计数器控制循环的要点926.13for重复结构946.14for结构使用举例976.15switch多项选择结构1016.16do/while重复结构1026.17break和continue语句1046.18逻辑运算符1056.19混淆相等(==)与赋值(=)运算符1066.20结构化编程小结108第7章函数1087.1简介1087.2数学函数库1097.3函数1097.4函数定义1127.5头文件1137.6作用域规则1167.7递归1187.8使用递归举例,Fibonacci数列1207.9递归与迭代1217.10带空参数表的函数1227.11内联函数1237.12函数重载125第8章数组1258.1简介1258.2数组1268.3声明数组1268.4使用数组的举例1378.5将数组传递给函数1418.6排序数组1428.7查找数组:线性查找与折半查找1478.8多维数组153第9章指针与字符串1539.1简介1539.2指针变量的声明与初始化1549.3指针运算符1569.4按引用调用函数1589.5指针与常量限定符1639.6按引用调用的冒泡排序1679.7指针表达式与指针算法1699.8指针与数组的关系1729.9指针数组1739.10函数指针1779.11字符与字符串处理简介1779.11.1字符与字符串基础1799.11.2字符串处理库的字符串操作函数185第10章信息学奥赛中的常用算法18510.1算法简介18710.2枚举算法19110.3回溯算法19310.4递归算法19610.5递推算法20010.6分治算法20210.7贪心算法20510.8搜索算法一(深度优先)20910.9搜索算法二(广度优先)21210.10动态规划法21510.11高精度计算228附录228ASCII表青少年信息学奥林匹克竞赛情况简介   信息学奥林匹克竞赛是一项旨在推动计算机普及的学科竞赛活动,重在培养学生能力,使得有潜质有才华的学生在竞赛活动中锻炼和发展。近年来,信息学竞赛活动组织逐步趋于 规范 编程规范下载gsp规范下载钢格栅规范下载警徽规范下载建设厅规范下载 和完善,基本上形成了“地级市——省(直辖市)——全国——国际”四级相互接轨的竞赛网络。现把有关赛事情况简介如下:全国青少年信息学(计算机)奥林匹克分区联赛:  在举办1995年NOI活动之前,为了扩大普及的面,并考虑到多数省、直辖市、自治区已经开展了多年省级竞赛,举办了首届全国青少年信息学(计算机)奥林匹克分区联赛。考虑到不同年级学生的知识层次,也为了鼓励更多的学生积极参与,竞赛设提高组、普及组,并分初、复赛进行,这样可以形成一个梯队,确保每年的竞赛活动有比较广泛扎实的基础。  从1995年起,至2001年共举办了七届全国青少年信息学奥林匹克分区联赛,每年举办一次(下半年十月左右),有选手个人奖项(省、国家级)、选手等级证书、优秀参赛学校奖项。安徽省青少年信息学(计算机)奥林匹克复决赛(简称AHOI):  省级信息学奥赛是一个水平较高的、有较大影响力的学科竞赛。由各市组织代表队参赛,参赛名额实行动态分配制度,每年举办一次(上半年五月左右)。从1984年起安徽省奥林匹克竞赛活动得到了蓬勃发展。奖项有个人一、二、三等奖,女选手第一、二、三名,奖励学校团体总分1-8名、市团体总分1-8名。全国青少年信息学(计算机)奥林匹克竞赛(简称NOI):  由中国算机学会主办的、并与国际信息学奥林匹克接轨的一项全国性青少年学科竞赛活动。1984年举办首届全国计算机竞赛。由各省市组织参赛,每年举办一次。奖项有个人一、二、三等奖,女选手第一、二、三名,各省队团体总分名次排队。国际青少年信息学(计算机)奥林匹克竞赛(简称IOI):  每年举办一次,由各参赛国家组队参赛。全国青少年信息学(计算机)奥林匹克分区联赛竞赛大纲一、初赛内容与要求:(#表示普及组不涉及,以下同) 计基算本机常的识 *诞生与发展    *特点    *在现代社会中的应用*计算机系统的基本组成*计算机的工作原理#       *计算机中的数的表示*计算机信息安全基础知识    *计算机网络 计基算本机操的作 *MSDOS与Windows的使用基础*常用输入/输出设备的种类、功能、使用*汉字输入/输出方法*常用计算机屏示信息 程序 设计 领导形象设计圆作业设计ao工艺污水处理厂设计附属工程施工组织设计清扫机器人结构设计 基本知识 程序的表示 *自然语言的描述*PASCAL或BASIC语言 数据结构的类型 *简单数据的类型*构造类型:数组、字符串*了解基本数据结构(线性表、队列与栈) 程序设计 *结构化程序的基本概念*阅读理解程序的基本能力*具有完成下列过程的能力:现实世界(指知识范畴的问题)—>信息世界(表达解法)—>计算机世界(将解法用计算机能实现的数据结构和算法描述出来) 基本算法处理 *简单搜索 *字串处理*排序     *查找*统计     *分类   *合并*简单的回溯算法*简单的递归算法二、复赛内容与要求:在初赛的内容上增加以下内容(2008年修改稿): 计算机软件 *操作系统的使用知识*编程语言的使用 数据结构 *结构类型中的记录类型*指针类型*文件(提高组必须会使用文本文件输入)*链表*树*图# 程序设计 *程序设计能力*设计测试数据的能力*运行时间和占用空间的估算能力# 算法处理 *排列组合的应用*进一步加深回溯算法、递归算法*分治法*搜索算法:宽度、深度优先算法*表达式处理:计算、展开、化简等#*动态规划#三、初赛试题类型:注:试题语言两者选一  (程序设计语言:FREEPASCAL、C、C++)  *判断*填空*完善程序*读程序写运行结果*问答四、推荐读物:*分区联赛辅导丛书*学生计算机世界报及少年电世界杂志第一章计算机基础知识1.1计算机的基本常识1.1.1计算机的产生与发展计算机的产生是20世纪最重要的科学技术大事件之一。世界上的第一台计算机(ENIAC)于1946年诞生在美国宾夕法尼亚大学,到目前为止,计算机的发展大致经历了四代:①      第一代电子管计算机,始于1946年,结构上以CPU为中心,使用计算机语言,速度慢,存储量小,主要用于数值计算;②      第二代晶体管计算机,始于1958年,结构上以存储器为中心,使用高级语言,应用范围扩大到数据处理和工业控制;③      第三代中小规模集成电路计算机,始于1964年,结构上仍以存储器为中心,增加了多种外部设备,软件得到了一定的发展,文字图象处理功能加强;④      第四代大规模和超大规模集成电路计算机,始于1971年,应用更广泛,很多核心部件可集成在一个或多个芯片上,从而出现了微型计算机。我国从1956年开始电子计算机的科研和教学工作,1983年研制成功1亿/秒运算速度的“银河”巨型计算机,1992年11月研制成功10亿/秒运算速度的“银河II”巨型计算机,1997年研制了每秒130亿运算速度的“银河III”巨型计算机。目前计算机的发展向微型化和巨型化、多媒体化和网络化方向发展。计算机的通信产业已经成为新型的高科技产业。计算机网络的出现,改变了人们的工作方式、学习方式、思维方式和生活方式。        1.1.2计算机系统及工作原理1.计算机的系统组成计算机系统由软件和硬件两部分组成。硬件即构成计算机的电子元器件;软件即程序和有关文档资料。(1) 计算机的主要硬件输入设备:键盘、鼠标、扫描仪等。输出设备:显示器、打印机、绘图仪等。中央处理器(CPU):包括控制器和运算器运算器,可以进行算术运算和逻辑运算;控制器是计算机的指挥系统,它的操作过程是取指令——分析指令——执行指令。存储器:具有记忆功能的物理器件,用于存储信息。存储器分为内存和外存 ①内存是半导体存储器(主存):它分为只读存储器(ROM)和随机存储器(RAM)和高速缓冲存储器(Cache); ROM:只能读,不能用普通方法写入,通常由厂家生产时写入,写入后数据不容易丢失,也可以用特殊方法(如紫外线擦除(EPROM)或电擦除(EEPROM_)存储器); RAM:可读可写,断电后内容全部丢失; Cache:因为CPU读写RAM的时间需要等待,为了减少等待时间,在RAM和CPU间需要设置高速缓存Cache,断电后其内容丢失。 ②外存:磁性存储器——软盘和硬盘;光电存储器——光盘,它们可以作为永久存器;   ③存储器的两个重要技术指标:存取速度和存储容量。内存的存取速度最快(与CPU速度相匹配),软盘存取速度最慢。存储容量是指存储的信息量,它用字节(Byte)作为基本单位,1字节用8位二进制数表示,1KB=1024B,1MB=1024KB,lGB=1024MB(2)计算机的软件计算机的软件主要分为系统软件和应用软件两类: ①系统软件:为了使用和管理计算机的软件,主要有操作系统软件如,WINDOWS95/98/2000/NT4.0、DOS6.0、UNIX等;WINDOWS95/98/2000/NT4.0是多任务可视化图形界面,而DOS是字符命令形式的单任务的操作系统。 ②应用软件:为了某个应用目的而编写的软件,主要有辅助教学软件(CAI)、辅助设计软件(CAD)、文字处理软件、工具软件以及其他的应用软件。2.计算机的工作原理到目前为止,电子计算机的工作原理均采用冯.若依曼的存储程序方式,即把程序存储在计算机内,由计算机自动存取指令(计算机可执行的命令=操作码+操作数)并执行它。工作原理图如下: 1.1.3计算机中有关数及编码的知识1.计算机是智能化的电器设备计算机就其本身来说是一个电器设备,为了能够快速存储、处理、传递信息,其内部采用了大量的电子元件,在这些电子元件中,电路的通和断、电压高低,这两种状态最容易实现,也最稳定、也最容易实现对电路本身的控制。我们将计算机所能表示这样的状态,用0,1来表示、即用二进制数表示计算机内部的所有运算和操作。2.二进制数的运算法则二进制数运算非常简单,计算机很容易实现,其主要法则是:0+0=00+1=11+0=11+1=0  0*0=00*1=01*0=01*1=1由于运算简单,电器元件容易实现,所以计算机内部都用二进制编码进行数据的传送和计算。3.十进制与二进制、八进制、十六进制数之间的相互转换(1)数的进制与基数计数的进制不同,则它们的基数也不相同,如表1-1所示。 进制 基数 特点 二进制 0,1 逢二进一 八进制 0,1,2,3,4,5,6,7 逢八进一 十六进制 0,1,2,...,9,A,B,C,D,E,F 逢十六进一(2)数的权不同进制的数,基数不同,每位上代表的值的大小(权)也不相同。如:(219)10=2*102+1*101+9*100  (11010)2=1*24+1*23+0*22+1*21+1*20   (273)8=2*82+7*81+3*80    (27AF)16=2*163+7*162+10*161+15*160 (3)十进制数转换任意进制  1)将十进制整数除以所定的进制数,取余逆序。 (39)10=(100111)2                       (245)10=(365)8 INCLUDEPICTURE"../../../DOCUME~1/sad/LOCALS~1/Temp/4_files/jc1.ht2%5b1%5d.gif"\*MERGEFORMAT  2)将十进制小数的小数部分乘以进制数取整,作为转换后的小数部分,直到为零或精确到小数点后几位。如:(0.35)10=(0.01011)2       (0.125)10=(0.001)2(4)任意进制的数转换十进制按权值展开:如:(219)10=2*102+1*101+9*100  (11010)2=1*24+1*23+0*22+1*21+1*20=26   (273)8=2*82+7*81+3*80=187    (7AF)16=7*162+10*161+15*160=18674.定点数与浮点数定点数是指数据中的小数点位置固定不变。由于它受到字长范围的限制,所能表示的数的范围有限,计算结果容易溢出。浮点数的形式可写成:N=M*2E(其中M代表尾数,E代表阶码)其形式如下: 阶码 尾数(包括符号位)5.ASCII编码由于计算机是电器设备,计算机内部用二进制数,这样对于从外部输入给计算机的所有信息必须用二进制数表示,并且对于各种命令、字符等都需要转换二进制数,这样就牵涉到信息符号转换成二进制数所采用的编码的问题,国际上统一用美国标准信息编码(ASCII)它可用7位二进制数表示,存储时用一个字节,它的最高位为0。因此基本的ASCII字符集有128个如:0-9:48-57:00110000-...A-Z:65-90:01000001-...a-z:97-122:01100000-...6.汉字编码与汉字输入法(1)机内码ASCII码不能表示汉字,因此要有汉字信息交换码,我国国家标准是gb2312,它也被称作国际码。它由两个字节组成,两个字节的最高位都为1。gb2312共收纳6763个汉字,其中,一级汉字(常用字)3755个按汉字拼音字母顺序排列,二级汉字3008个按部首笔画次序排列。(2)汉字输入码(外码)目前,汉字输入法主要有键盘输入、文字识别和语音识别。键盘输入法是当前汉字输入的主要方法。它大体可以分为:  流水码:如区位码、电报码、通信密码,优点重码律少,缺点难于记忆;  音码:以汉语拼音为基准输入汉字,优点是容易掌握,但重码律高;  形码:根据汉字的字型进行编码,优点重码少,但不容易掌握;  音形码:将音码和形码结合起来,能减少重码律同时提高汉字输入速度。(3)汉字字模供计算机输出汉字(显示和打印)用的二进制信息叫汉字字形信息也称字模。通用汉字字模点阵规格有16*16,24*24,32*32,48*48,64*64,每个点在存储器中用一个二进制位((bit)存储,如一个16*16点阵汉字需要32个字节的存储空间。1.1.4原码、反码与补码在计算机中,数据是以补码的形式存储的:在n位的机器数中,最高位为符号位,该位为零表示为正,为1表示为负;其余n-1位为数值位,各位的值可为0或1。当真值为正时:原码、反码、补码数值位完全相同;当真值为负时:  原码的数值位保持原样,  反码的数值位是原码数值位的各位取反,  补码则是反码的最低位加一。注意符号位不变。 如:若机器数是16位:十进制数17的原码、反码与补码均为: 0000000000010001十进制数-17的原码、反码与补码分别为:1000000000010001、1111111111101110、11111111111011111.1.5逻辑运算1.逻辑运算 逻辑与:同真则真 逻辑或:有真就真 逻辑非:你真我假 逻辑异或:不同则真2.按位运算 按位与∩:同1则1如10010101∩10110111=10010101 按位或∪:有1则1如10010101∪10110111=101101113.逻辑化简 化简定律:    (1)交换律:A+B=B+A,A·B=B·A  (2)结合律:(A+B)+C=A+(B+C),(A·B)·C=A·(B·C)  (3)幂等律:A·A=A,A+A=A  (4)吸收律:A·(A+B)=A,A+(A·B)=A  (5)分配律:A·(B+C)=A·B+A·C,A+(B·C)=(A+B)·(A+C)  (6)互补律:A+A=1,A·A=0  (7)非深入:A+B=A·B,A·B=A+B  (8)0-1律:A+0=A,A+1=1,A·1=A,A·0=0  例:化简函数Q=AD+AD+AB+ACEF。这个函数有5个自变量,化简过程如下:   Q=AD+AD+AB+ACEF    =A+AB+ACEF    =A+ACEF    =A练习:求证:(A+B)(A+C)=AB+AC1.2操作系统1.2.1DOS(DiskOperatingSystem)的组成MS—DOS采用模块结构,它由五部分组成:ROM中的BIOS模块、IO.SYS模块、MSDOS.SYS模块、COMMAND.COM模块和引导程序。(1)BIOS模块:在PC机主板上有一个ROM芯片,该芯片中存有系统自测试程序,           CMOS设置程序和基本输入输出程序(BIOS)。BIOS是一组程序和参           表,其中程序部份是可以通过中断方式调用的一组驱动程序,参数           给出外设的地址和参数。BIOS是计算机硬件和操作系统之间的接口           通过它操作系统管理计算机硬件资源。(2)IO.SYS模块:IO.SYS是MS—DOS和ROMBIOS之间的接口程序。它和RON               BIOS一起完成系统设备的管理。(3)MSDOS.SYS模块:MSDOS.SYS用于实现文件管理,包括文件管理、目录管理、内存管理等功能。它以功能调用的形式实现用户和MS—DOS之间的程序级接口。(4)COMMAND.COM模块:COMMAND.COM的主要功能是负责接收、识别、解释和执行用户从键盘输入的MS—DOS命令。(5)引导程序:引导程序又叫“引导记录”,其作用是检查当前盘上是否有两个系统文件,若有系统文件则把DOS系统从磁盘装人内存。一张系统盘上应该包含有:引导记录、IO.SYS、MSDOS.SYS和COMMAND.COM等模块。      1.2.2DOS的文件和目录1)文件概念:文件是指记录在存储介质(如磁盘、光盘)上的一组相关信息的集合。2)文件标识:驱动器号+路径+文件名(1到8各字符)+扩展名(1到3个字符代表文件的类型)3)通配符:*代表从该位置起的一个或多个合法字符;?代表所在位置的任一个合法字符。4)树形目录:DOS采用树形目录结构。由一个根目录和若干层子目录组成。这种目录结构一是能够解决文件重名问题,即不同的目录可以包含相同的文件名或目录名;二是能够解决文件多而根目录容量有限带来的问题。在查找某个子目录下的一个文件时,要使用目录路径。指定路径有两种方法:绝对路径和相对路径。绝对路径是从根目录开始到文件所在目录的路径。例如要查找UCDOS子目录下的二级子目录DATA下的README.TXT文件,绝对路径为:\UCDOS\DATA。路径中第一个“\”符号代表根目录。相对路径是从当前目录开始到文件所在目录的路径。当前目录指在不特意指定路径情况下DOS命令所处理的目录。例如系统提示符为:“C:\UCDOS\DATA>”,则DATA是当前目录。1.2.3DOS命令1.内部命令 1)内部命令:当启动DOS系统时,计算机引导程序将系统以及常用的命令处理模块驻留在计算机的内存中,我们称之为内部命令。 2)常用的内部命令: (1)目录命令: DIR(显示文件目录) MD、CD、RD(子目录的建立、进入、删除命令)  (2)文件操作命令: COPY(复制命令)、DEL(删除命令)、REN(更改文件名) TYPE(显示文本文件内容) (3)其他内部命令 DATA、TIME、VER、CLS等   · 2.外部命令 1)外部命令:存储在外存储器上的DOS可执行的文件,这些文件程序所占的存储容量比较大,当用户使用外部命令时,计算机从外存调入内存,当执行完外部命令,就自动从内存中退出。 2)常用的外部命令 (1)磁盘格式化命令: FORMAT 盘符 [/S)I/V] 其作用,能够清除原盘中所有信息,并将磁盘规范成计算机所能接受的格式,以便有效存储信息。 (2)软盘复制命令: DISKCOPY [盘符1:][盘符2:]   其作用,能够进行软盘之间的全盘复制(以磁道方式),不仅可以复制系统文件而且可以复制隐含文件。1.2.4Windows简介Windows是一个多任务图形用户界面,该环境可以在基于MS-DOS的计算机上运行,在多任务图形用户环境下,Windows提供了一个基于下拉菜单、屏幕窗口和鼠标的界面,在该环境下运行的应用程序必须进行专门的设计才能发挥这些特征的优点。1.Windows的特点Windows能够充分发挥计算机的作用,其图形接口能够组织用户程序和文件、同时运行几个用户程序、在文档之间移动和复制信息、在平台上进行应用程序的切换等。为了提高效率,Windows还提供了一些辅助程序,如字处理器、画笔及其他标准应用程序等。Windows具有以下主要特点。(1)图形化的用户界面Windows提供了一种不同于DOS系统下命令行的工作方式,它通过对窗口、图标、选单、对话框、命令按钮、滚动框等图形符号与画面的操作来实现对计算机的各种操作。(2)标准化的操作界面在Windows中,所有的操作都是通过窗口中的图形界面进行的。(3)多任务机制和执行性能在Windows中,平稳的多任务机制可以同时运行多道程序以及执行多项任务,各程序与各任务之间不仅转换容易,而且还可以方便地交换数据。(4)充分利用内存Winddws利用虚拟内存技术,允许应用程序超过640阳常规内存的运行空间,从而最大限度地利用了计算机系统的所有内存资源,从而使内存较小的微机也能运行大型的应用程序。(5)强大的联网功能在Windows中,可以简单直观地实现网络的安装、配置、浏览,从而可以更加方便地实现网络管理和资源共享。(6)丰富的多媒体功能Windows提供大量辅助程序,用以实现文字、图形、图像、声音、视频等多媒体功能,同时还支持其他厂商基于Windows标准开发的各种相应软件。(7)TryType技术TryType(真实字体)属于内建式比例字体,可以任意平滑放大与缩小。这种字体能使屏幕上显示的效果与实际打印机输出的信息完全一致,这就是所谓的“所见即所得”。[例]在Windows95中,“任务栏”的作用是____。A)显示系统的所有功能B)只显示当前活动窗口名C)只显示正在后台工作的窗口名D)实现窗口之间的切换解答:在任务栏中,显示了所有打开的程序的图标。本题正确答案为D。1.3计算机网络常识1.3.1网络基础知识1.网络的概念计算机网络是将地理位置不同的计算机,用通信链路连接起来,共同遵守一定的 协议 离婚协议模板下载合伙人协议 下载渠道分销协议免费下载敬业协议下载授课协议下载 ,以实现计算机软硬件资源共享为目标的通信系统。2.网络的组成计算机网络由网络硬件和网络软件组成。网络软件包括网络操作系统、通信软件、通信协议(计算机之间实现数据通信共同遵守的相关规定)。网络硬件包括网络的拓扑结构、网络服务器、网络工作站、传输介质和设备。3.网络的分类(1)按通信距离分:局域网(LAN):局限于某个范围(10公里左右)的网络连接情(校园网)。广域网(WAN):跨地区的局域网,Internet是覆盖全球的广域网。(2)按网络的使用目的分:共享资源网:使用者可分享网络的各种资源(如Internet)。数据处理网:用于数据处理(企业经营管理用的网络)。数据传输网:用于数据的收集、交换和传输(情报检索网络)。(3)按网络的拓扑结构分:星形网:以一台计算机为中心,以放射状连接若干台计算机。环形网:传输线路构成一个封闭的环,入网的计算机连到这个环形线路上。总线网:用一条通信线路作主干,入网的计算机通过相应接口连到线路上。4. 开放系统互联模型 (OSI模型)   OSI模型分7层:各层功能如下:            1.物理层  物理层与移动二进制数和维护物理连接有关。  2.数据链路层  数据链路层通过帧在一个给定的物理链路传输分组(报文),保持帧的有序以及发现检测到的各种错误,包括传输错误,但是数据链路层只了解在链路另一端的对等实体。数据链路层的地址是为了将网络中一点的数据帧送到另一点。  3.网络层  网络层知道每个数据链路的对等进程,并负责在链路间移动分组,把它送到目的地。网络层地址是为了把单一分组从网络的一端送到目的地。  4.传输层  传输层注意的是整个网络,该层是第一个端到端层。其对等实体位于分组的最终目的地。传输层依靠网络层经过中间节点移动分组。传输层地址是为了把网络一端进程的完整信息送到最终目的地的对等进程。  5-7.会话层、表示层和应用层提供了如下功能: 处理计算机间数据表示的差别。确保数据在网络传输中不被窃取和泄露,并且确保网络不允许未经授权就访问数据。最高效地使用网络资源通过应用程序及活动同步来管理对话和活动。在网络节点间共享数据。1.3.2Internet简介Internet英文直译为“互联网”,中文名为“因特网”。是世界上众多计算机网络的集合起源于20世纪80年代。1.Internet的IP地址、IP地址类型和主机域名(1)在Internet网上采用统一的网络协议TCP/IP,与Internet相连的计算机必须具有唯一的主机地址,称IP地址。IP地址采用分段地址方式,使用数字表示;如:207.46.130.14,其中由三个点隔开的四个数是十进制,其大小是0-255,每个数对应一个8位二进制数,所以IP地址用32位二进制位存放站4个字节。(2)IP地址类型:最初设计互联网络时,为了便于寻址以及层次化构造网络,每个IP地址包括两个标识码(ID),即网络ID和主机ID。同一个物理网络上的所有主机都使用同一个网络ID,网络上的一个主机(包括网络上工作站,服务器和路由器等)有一个主机ID与其对应。IP地址根据网络ID的不同分为5种类型,A类地址、B类地址、C类地址、D类地址和E类地址。 A类IP地址 一个A类IP地址由1字节的网络地址和3字节主机地址组成,网络地址的最高位必须是“0”,地址范围从     1.0.0.0到126.0.0.0。可用的A类网络有126个,每个网络能容纳1亿多个主机。B类IP地址一个B类IP地址由2个字节的网络地址和2个字节的主机地址组成,网络地址的最高位必须是“10”,地址范围从128.0.0.0到191.255.255.255。可用的B类网络有16382个,每个网络能容纳6万多个主机。 C类IP地址一个C类IP地址由3字节的网络地址和1字节的主机地址组成,网络地址的最高位必须是“110”。范围从192.0.0.0到223.255.255.255。C类网络可达209万余个,每个网络能容纳254个主机。 D类地址用于多点广播(Multicast)。D类IP地址第一个字节以“lll0”开始,它是一个专门保留的地址。它并不指向特定的网络,目前这一类地址被用在多点广播(Multicast)中。多点广播地址用来一次寻址一组计算机,它标识共享同一协议的一组计算机。 E类IP地址以“llll0”开始,为将来使用保留。全零(“0.0.0.0”)地址对应于当前主机。全“1”的IP地址(“255.255.255.255”)是当前子网的广播地址。在IP地址3种主要类型里,各保留了3个区域作为私有地址,其地址范围如下:A类地址:10.0.0.0~10.255.255.255B类地址:172.16.0.0~172.31.255.255C类地址:192.168.0.0~192.168.255.255(3)为了使用方便,在访问Internet上的主机时,通常使用主机域名而不是IP地址,但主机域名和IP地址一一对应,它由圆点分隔的一序列单词组成如“Public.bta.net.cn"。IP地址如同电脑的身份证号码,而域名相当电脑的姓名。2.Internet的功能 (1)信息浏览(WWW)WWW(WorldWideWeb),中文名为”万维网“,是基于超文本的、方便用户信息浏览和信息搜索的信息服务系统。用户在浏览器中输入网址即可得到需要的信息。人们常用的浏览器有网景公司的Netscape浏览器和Microsoft公司的InternetExplorer浏览器。网址的输入是使用协议提供的服务+服务器地址(IP地址或主机域名)如http://198.105.232.1;ftp://zsqz.com (2)文件传输(FTP)FTP(FileTransferProtocol)是Internet的一种标准协议,这一协议使用户能在联网的计算机之间传送文件如上载(UPLOAD把本地计算机上地文件复制到远程计算机上)和下载(DOWNLOAD把远程计算机上的文件复制到本地计算机上)。 (3)传送电子邮件(E-mail)电子邮件地址=用户名+@+主机域名;如:zhangming@yahoo.com(4)电子公告牌(BBS)(5)远程登录(telnet)(6)电子商务等3.TCP/IP参考模型TCP/IP协议的开发研制人员将Internet分为五个层次,以便于理解,它也称为互联网分层模型或互联网分层参考模型,如下表:应用层(第五层)传输层(第四层)互联网层(第三层)网络接口层(第二层)物理层(第一层)各层简要说明如下:      物理层:对应于网络的基本硬件,这也是Internet物理构成,即我们可以看得见的硬件设备,如PC机、互连网服务器、网络设备等,必须对这些硬件设备的电气特性作一个规范,使这些设备都能够互相连接并兼容使用。  网络接口层:它定义了将数据组成正确帧的规程和在网络中传输帧的规程,帧是指一串数据,它是数据在网络中传输的单位。  互联网层:本层定义了互联网中传输的“信息包”格式,以及从一个用户通过一个或多个路由器到最终目标的""信息包""转发机制。  传输层:为两个用户进程之间建立、管理和拆除可靠而又有效的端到端连接。  应用层:它定义了应用程序使用互联网的规程。1.4计算机信息安全基础知识1.4.1计算机的网络安全1、不同环境和应用中的网络安全  运行系统安全,即保证信息处理和传输系统的安全。它侧重于保证系统正常运行,避免因为系统的崩溃和损坏而对系统存贮、处理和传输的信息造成破坏和损失,避免由于电磁泄漏,产生信息泄露,干扰他人,受他人干扰。  网络上系统信息的安全。包括用户口令鉴别,用户存取权限控制,数据存取权限、方式控制,安全审计,安全问题跟踪,计算机病毒防治,数据加密。  网络上信息传播安全,即信息传播后果的安全。包括信息过滤等。它侧重于防止和控制非法、有害的信息进行传播后的后果。避免公用网络上大量自由传输的信息失控。  网络上信息内容的安全。它侧重于保护信息的保密性、真实性和完整性。避免攻击者利用系统的安全漏洞进行窃听、冒充、诈骗等有损于合法用户的行为。本质上是保护用户的利益和隐私。网络安全的特征2、网络安全应具有以下四个方面的特征:  保密性:信息不泄露给非授权用户、实体或过程,或供其利用的特性。  完整性:数据未经授权不能进行改变的特性。即信息在存储或传输过程中保持不被修改、不被破坏和丢失的特性。  可用性:可被授权实体访问并按需求使用的特性。即当需要时能否存取所需的信息。例如网络环境下拒绝服务、破坏网络和有关系统的正常运行等都属于对可用性的攻击;  可控性:对信息的传播及内容具有控制能力。3、主要的网络安全威胁  自然灾害、意外事故;  计算机犯罪;  人为行为,比如使用不当,安全意识差等;  “黑客”行为:由于黑客的入侵或侵扰,比如非法访问、拒绝服务计算机病毒、非法连接等;  内部泄密;  外部泄密;  信息丢失;  电子谍报,比如信息流量分析、信息窃取等;  信息战;  网络协议中的缺陷,例如TCP/IP协议的安全问题等等。4、黑客常用的信息收集工具信息收集是突破网络系统的第一步。黑客可以使用下面几种工具来收集所需信息:SNMP协议,用来查阅非安全路由器的路由表,从而了解目标机构网络拓扑的内部细节。TraceRoute程序,得出到达目标主机所经过的网络数和路由器数。Whois协议,它是一种信息服务,能够提供有关所有DNS域和负责各个域的系统管理员数据。(不过这些数据常常是过时的)。DNS服务器,可以访问主机的IP地址表和它们对应的主机名。Finger协议,能够提供特定主机上用户们的详细信息(注册名、电话号码、最后一次注册的时间等)。Ping实用程序,可以用来确定一个指定的主机的位置并确定其是否可达。把这个简单的工具用在扫描程序中,可以Ping网络上每个可能的主机地址,从而可以构造出实际驻留在网络上的主机清单。1.4.2计算机病毒计算机病毒是一种程序,是人为设计的具有破坏性的程序。计算机病毒具有破坏性、传播性、可激发性、潜伏性、隐蔽性等特点。1.4.3病毒的分类    (1)按病毒设计者的意图和破坏性大小,可将计算机病毒分为良性病毒和恶性病毒。    ①良性病毒:这种病毒的目的不是为了破坏计算机系统,而只是为了编制者表现自己。此类病毒破坏性较小,只是造成系统运行速度降低,干扰用户正常工作。    ②恶性病毒:这类病毒的目的是人为的破坏计算机系统的数据。具有明显破坏目标,其破坏和危害性都很大,可能删除文件或对硬盘进行非法的格式化。    (2)计算机病毒按照寄生方式可以分为下列四类:    ①源码病毒:在源程序被编译之前,就插入到用高级语言编写的源程序当中。编写这种病毒程序较困难。但是,一旦插入,其破坏性和危害性都很大。    ②入侵病毒:是把病毒程序的一部分插入到主程序中。这种病毒程序也难编写,一旦入侵,难以清除。    ②操作系统病毒:是把病毒程序加入或替代部分操作系统进行工作的病毒。这种病毒攻击力强、常见、破坏性和危害性最大。    ④外壳病毒:是把病毒程序置放在主程序周围,一般不修改源程序的一种病毒。它大多是感染DOS下的可执行程序。这种病毒占一半以上,易编制,也易于检测和消除。在日常维护中应隔离计算机病毒的来源,经常要用杀毒软件检查计算机系统和存储器。[例]设一张软盘已染上病毒,能清除病毒的措施是____。    A)删除该软盘上的所有文件    B)格式化该软盘    C)删除该软盘上的所有可执行文件    D)删除该软盘上的所有批处理文件    解答:软盘染毒后,病毒隐藏在磁盘内部,并感染磁盘上的文件,而且可能通过磁盘的使用进而扩散到其他磁盘,造成更大的破坏。为了清除病毒,必须格式化软盘,从而彻底清除染毒文件和病毒本身。    本题正确答案为B。第2章C++编程简介2.1机器语言、汇编语言和高级语言程序员用各种编程语言编写指令,有些是计算机直接理解的,有些则需要中间翻译(tranlation)的步骤。如今使用的计算机语言有几百种,可以分为三大类:1.机器语言2.汇编语言3.高级语言任何计算机只能直接理解本身酌机器语言(machinelanguage)。机器语言是特定计算机的自然语言,由计算机的硬件设计定义。机器语言通常由一系列数字组成(最终简化0和1),让计算机一次一个地执行最基本的操作。机器语言非常繁琐,下面的机器语言程序将工龄工资和基础工资相加,并把结果保存在工资总额中:+1300042774+1400593419+1200274027随着计算机越来越普及,机器语言编程对大多数程序员显然太慢、太繁琐。程序员不用计算机直接理解的一系列数字,而是用类似英文缩写的助记将来表示计算机的基本操作,这些助记符构成了汇编语言(assemblylanguage)。称为汇编器(assembler)的翻译程序以计算机的速度将汇编语言程序转换为机器语言。下列汇编程序也是工龄工资和基础工资相加,并将结果保存在总工资中,但要比相应的机器语言清晰得多:LOADBASEPAYADDOVERPAYSTOREGROSSPAY尽管这种代码对于人们一目了然,但计算机却无法理解,必须先翻译为相应的机器语言。随着汇编语言的出现,计算机的使用迅速增加,然而即使是最简单的任务.也需要许多条指令才能完成。为了加速编程过程,人们开发了高级语言(high-levellanguage),用一条语句完成大量任务。称为编译器(compiler)的翻译程序将高级语言程序变为相应的机器语言。高级语言使程序员能够编写更像英语的指令,可以包含常用的数学符号。将工龄工资和基础工资相加.并把结果保存在总工资中,可以用下列高级语言程序:grossPay=basePay+overTimePay显然,从程序员角度看,高级语言比机器语言和汇编语言都要强得多。C和C++是最强大最广泛使用的高级语言。将高级语言程序编译为相应的机器语言的过程可能需要大量时间。解释器(interpreter)程序可以直接执行高级语言程序,而不必先将这些程序编译成相应的机器语言。尽管编译程序的执行速度比解释程序更快,但解释器在程序开发环境中更常用,因为增加新特性和纠正错误时经常需要重新编译程序。一旦程序开发完成,编译版本的运行最有效。2.2C语言与C++的历史C++是从C语言演变而来的,而C语言又是从两个编程语言BCPL和B演变而来的、BCPL是MartinRichards于1967年开发的,用于编写操作系统软件和编译器。KenThompson在他的B语言(1970年在贝尔实验室)。DCPL和B都是“无类型”语言,每个数据项在内存中占一个“字”(word)长、如果要将数据项作为整数或实数处理,编程的工作量会很大。C语言是从D语言演变而成的,由贝尔实验室的DennisRitchie开发,最初于1972年在DECPDP—11计算机上实现。C语言使用了BCPL和B的许多重要概念,同时增加了数据类型和其他特性。C语言最初作为UNIX操作系统的开发语言而闻名于世。如今,大多数操作系统都是用C/C++写成的。二十多年来,C语言已经遍布在大多数计算机上。C语言是硬件无关的,只要仔细设计。就可以编写能移植列大多数计算机上的C语言程序。到20世纪70年代未期,C语言演变成现在所谓的“传统C”、“经典CPP或“Kernighan/RitchieC"。1978年PrenticeHall公司出版了Kernighan和Ritchie合作的著作《TheCProgrmmmingLanguage》,引起了人们对C语言的广泛关注(见参考文献Ke78)。C语言在各种不同类型计算机(有时称为硬件平台)上的普及导致了许多变形。它们虽然相似,但通常互不兼容。对需要为不同平台编写可移植程序的开发人员.这是个严重问题,显然需要有个标准的C语言版本。1983年,美国国家计算机与信息处理标准委员会(x3)成立了x3JII技术分会,目的是提供无歧义性且与机器无关的语言定义。1989年推出了这种语言标准。AN5I与国际标准化组织(IS0)合作.在全球范围内将C语言标淮化,1990年推出了联合标准文档。称为ANSI/IS09899:1990。这个文档可以从ANSI获得副本。1988年推出的Kernighan和Ritchie著作的第二版体现了该版本(称为ANSIC),这也是目前全世界使用的版本。C++是C语言的扩展,是20世纪80年代初由贝尔实验室的Bjarnestroustrup开发的。C++的许多特性是从c语言中派生的,但更重要的是,它提供了面向对象编程(object-orientedprogramming)的功能。软件业正在酝酿一场革命,最终日标是更快、更正确、更经济地建立软件,新的、更强大的软件需求迫在眉睫。对象(object)实际上是模拟实际项目的可复用软件组件(component)。软件开发人员发现,利用模块化、面t向对象的设计和实现方法与过去结构化编程方法相比较,可以使软件开发小组的生产率更高。面向对象编程的优势在于更容易理解、纠正和修改。许多面向对象的语言也纷纷涌现,包括最著名的由Xerox的PaloAlto研究中心(PARC)开发的smalltalk。Smalltalk是纯粹的面向对象的语言,其所有的编程元素都是“对象”。C++则是一种“混合型语言“,可以用C语言方式、面向对象方式或兼用两种方式进行编程。2.9节将介绍基于C/C++的新语言——Java。2.3C++标准库C++程序由类(class)和函数(function)组成。可以用多个小的软件模块构成C++程序,但大多数C++程序员会利用C++标准库中已有的类和函数来编程。这样,C++“世界”中实际要学习两方面的知识,第一是学习C++语言本身,第二是学习如何利用C++标准库中现有的类和函数(本教程将介绍许多类和函数)。Plauger的著作是程序员必读的.可以帮助程序员深入了解C++中包括的ANSIC语言库函数,了解如何实现这些库函数,还可以了解如何用库函数编写可移植代码。标准库函数通常由编译器厂家提供。许多独立软件供应商(indepandentsofterwarevender)也提供各种专用类库。常用数据类型与Pascal数据类型的比较: Pascal 数据类型 C/C++ ShorInt 8位有符号整数 char Byte 8位无符号整数 BYTE,unsignedshort SmallInt 16位有符号整数 short Word 16位无符号整数 unsignedshort Integer,LongInt 32位有符号整数 int,long Cardinal,LongWord/DWORD 32位无符号整数 unsignedlong Int64 64位有符号整数  _int64 Single 4字节浮点数 float *Real48 6字节浮点数   Double 8字节浮点数 double *Extended 10字节浮点数 longdouble Currency 64位货币类型   TDate/TDateTime 8字节日期/时间   Variant,OleVariant 16字节可变类型 VARIANT,^Variant,^OleVariant Char,AnsiChar 1字节字符 char WideChar 2字节字符 WCHAR *ShortString 短字符串 string AnsiString/String 长字符串 ^AnsiString WideString 宽字符串 ^WideString PChar,PAnsiChar NULL结束的字符串 char* PWideChar NULL结束的宽字符串 LPCWSTR Boolean,ByteBool 1字节布尔类型 任何1字节 WordBool 2字节布尔类型 任何2字节 BOOL,LongBool 4字节布尔类型 BOOL 注:有*前缀的是向前兼容类型;有^前缀的是C++Builder特有类型。2.4结构化编程20世纪60年代,许多大型软件的开发遇到了严重困难。常常推迟软件 计划 项目进度计划表范例计划下载计划下载计划下载课程教学计划下载 ,因而使成本大大超过预算,而且最终产品也不可靠。人们开始认识到,软件开发是项复杂的活动,比原来所预想的要复杂得多。20世纪60年代的研究结果是结构化编程(structuredprogramming)的出现,用规定的方法编写程序比非结构化编程能产生更清晰、更容易测试/调试以及更容易修改的程序。本教程的第2章将介绍结构化编程原理。第3章到第5章则会开发多种结构化程序。结构化编程研究的一个更具体结果是1971年NiklausWirth教授推出了Pascal语言。Pascal语言是用17世纪著名数学家和哲学家巴雷斯·帕斯卡(BlaisePascal)的名字命名的,常用于教学中讲解结构化编程.因而很快成为了大学中受欢迎的语言。但是这个语言缺乏在商业、工业和政府应用程序中所需要的许多特性,因此没有被大学以外的环境所接受。Ada语言是在20世纪70年代和80年代初由美国国防部资助开发的。在此之前,国防部的导弹命令与控制软件系统是由几百种不同语言生成的,国防部要求用一种语言来完成大多数工作。Ada以Pascal为基础.但最终结构与Pascal大相径庭。这个语言是根据著名诗人LordByron的女儿(AdaLovelace)的名字命名的。AdaLovelace在19世纪初编写了世界上第一个计算机程序,用于charlesBabbage设计的分析机引擎的计算设备。Ada的一个最重要功能是多任务(multiasking).程序员可以使多个活动任务并行发生。我们要介绍的其他常用高级语言(包括C/C++)通常只让程序员编写一次只有一个活动任务的程序。2.5简单程序C++使用非程序员可能感到奇怪的符号。我们首先介绍一个简单程序:打印一行文本。程序及其屏输出如图2.2。这段程序演示了C++语言的几个重要特性。我们详细介绍程序的每一行。//Fig.2.2:fig1_02.cpp//AfirstprograminC++以//开头,表示该选项其余部分是注释语句(comment)。程序员手稿注释语句用来说明和提高程序的可读性。注释语句还可以帮助其它人阅读和理解程序。在运行程序时注释语句并不使计算机采用任何操作。C++编译器忽略注释误句,不产生任何机器目标码。注释语句"firstprograminC++"只是描述该程序的用途。以//开头的说明语句称为单行貹注释语句(singned-linecomment),因为该注释语句在行尾结束(注意:C++程序员也可以用C语言注释语句样式,即注释语句以/*开头,以*/结束)。1//Fig.2.2:fig01_02.cpp2//AfirstprograminC++3#include<iostream>4usingnamespacestd;5intmain()6{7cout<<"WelcomtoC++!\n";89rerturn0;//indicatethatprogramendedsucessfully10}输出结果:WelcomtoC++!下列语句:#include<iostream>是条预处理指令(preprocessordirective),是发给C++预处理器的消息。预处理器先处理以#开头的一行语句之后再编译该程序。为一行预处理指令告诉预处理器要在程序中包括输入、输出的头文件iostream.h的内容。应该在任何使用C++式输入、输出泫从键盘输入数据或向屏幕输出数据的程序中包括这个头文件。注意,最新ANSI、ISOC++杂标准实际上指定iostream.h和其它标准头文件不需要后缀.h,如iostream。我们在本教程中有时继续使用旧式头文件,因为有些编译器还不支持最新的ANSI/ISOC++草案标准。下列语句:intmain()是每个C++程序都包的语句。main后面的括号表示main是个程序基本组件,称为函数(function)。C++程序包含一个或几个函数,其中有且只有一个main函数即使main不是程序中的第一个函数,C++程序通常都从main函数开始执行。main左边的关键字int表示main返回一个整数值。左花括号({)应放在每个函数体(body)开头,对应右花括号(})应放在每个函数的结尾。下列语句:cout<<"WelcomtoC++!\n";让计算机在屏幕上打印引号之间的字符串(string)。整个行称为语句(statement),包括cout<<运算符、字衔串"WelcomtoC++!\n"和分号(;)。每条语句应以分号(又称为语句终止符)结束。C++中的输出和输入是用字符流(stream)完成的,这样,执行上述语句时,将字符流"WelcomtoC++!"发送到标准输出流对象(standardoutputstreamobject)cout,通常cout将其输出到屏幕。第3章“C++输入/输出流”中将详细介绍cout。运算符<<称为流插入符(streminsertionoperator)。执行这个程序时,运算符右边的值(历操作数)插入输出流中。历操作数通常按引号中的原样直接打印。但注意字符\n不在屏幕中打印。反斜杠(\)称为转义符(escapcharacter),表示要输出特殊字符。字符串中遇到反斜杠时,下一个字符与反斜杠组合,形成转义序列(escapesequence)。转义序列\n表示换行符(newline)。使光标(即当前屏幕位置的指示符)移到下一行开头。表2.3列出了常用转义序列。下列语句:return0;//indicatethatprogramendedsucessfully放在每个main函数的末尾。C++的关键字return是退出函数的几种方式之一。main函数末尾使用return语句时,数值0表示顺利结束。第3章将详细介绍和解释包括这个语句的原因。目前只要记住在每个程序中都要包括这个语句。否则在某些程序中编译器会产生警告消息。右花括号(})表示main函数结束。------------------------------------------------------------------------------转义序列说明------------------------------------------------------------------------------\n换行符,使屏幕光标移到屏幕中下一行开头\t水平制表符,使屏幕光标移到下一制表位\r回车符,使屏幕光标移到当前行开头,不移到下一行\a警告,发出系统警告声音\\反斜杠,打印反斜杠符\"双引号,打印双引号-------------------------------------------------------------------------------图2.3常用转义序列许多程序员让函数打印的最后一个字符为换行符(\n)。这样可以保证函数使屏幕光标移到屏幕中下一行开头,这种习惯能促进软件复用,这是软件开发环境中的关键目标。确定一个喜欢的缩排长度,然后一直坚持这个缩排长度。可以用制表符生成缩排,但制表位可能改变。建议用1/4英寸制表位或三个空格的缩排长度。"WelcomtoC++!"可用多种方法打印。例如,图2.4的程序用多条流插入语句,产生的程序输出与图2.2相同,因为每条流插人语句在上一条语句停止的位置开始打印。第一个流插入语句打印“Welcome”和空格,第二条流插入语句打印同一行空格后面的内容。C++允许以多种方式表达语句。一条语句也可以用换行符打印多行,如图2.5。每次在输出流中遇到\n转义序列时,屏幕光标移到下一行开头。要在输出中得到空行,只要将两个\n放在一起即可,如图2.5。1//Fig.2.4:fig01_04.cpp2//printingalinewithmultplestatements3#include<iostream>4usingnamespacestd;5intmain()6{7cout<<"Welcom";8cout<<"toC++!\n";910return0;11}输出结果:WelcomtoC++!图2.4用多条流插入语句打印一行1//Fig.2.5:fig01_05.cpp2//printingmultiplelineswithasinglestatement3#include<iostream>4usingnamespacestd;5intmain()6{7cout<<"Welcom\nto\n\nC++!\n";89return0;//indicatethatprogramendedsucessfully10}输出结果:WelcometoC++!图2.5用一条流插入语句打印多行2.6简单程序:两个整数相加下一个程序用输入流对象cin和流读取运算符>>取得用户从键盘中输入的两个整,计算这两个值的和,并将结果用cout输出。程序及其输出如图2.6。1//Fig.2.6:fig01_06.cpp2//Additionprogram3#include<iostream>4usingnamespacestd;5intmain()6{7intinteger1,integer2,sum;//声明三个变量89cout<<"Enterfirstinteger\n";//提示信息10cin>>integer1;//从键盘读一个整数11cout<<"Entersecondinteger\n";//提示信息12cin>>integer2;//从键盘读一个整数13sum=integer1+integer2;//两整数相加,值赋给变量sum14cout<<"Sumis"<<sum<<endl;//输出和1516return0;//返回0值表示程序运行成功。17}输出结果:Enterfirstinteger45Entersecondinteger72Sumis117图2.6两个整数相加注释语句://Fig.2.6:fig01_06.cpp//Additionprogram指定文件名和用途。C++预处理指令:#include<iostream>将iostream.h头文件的内容放进程序中。前面介绍过,每个程序从main函数开始执行。左花括号表示main函数体开头,相应右花括号表示main函数体结束。下列语句:intinteger1,integer2,sum;是个声明(declaration)。integer1,integer2和sum是变量(variable)名。变量是计算机内存中的地址,存放程序使用的值。这个声明指定变量integer1,integer2和sum的数据类型为int,表示这些变量保存整数值,如7、-11、0、31914。所有变量都应先声明名称和数据类型后才能在程序中使用。几个相同类型的变量可以在同一声明或多个声明中声明。我们可以一次只声明一个变量,但一次声明多个同类型变量更加简练。稍后要介绍数据类型float,(定义实数,即带小数点的数,如3.4、0.0、—11.19)和char(定义字符型数据.变量char只能保存一个小写字母、一个大写字母、一个数字或一个特殊字符,如x、$、7、*等等)。变量声明可以放在函数的任何位置,但变量声明必须放在程序使用变量之前。如果不用一条语句声明三个变量也可以分别声明。下列声明:intinteger1;可以放在下列语句之前:cin>>integerl;下列声明:intinteger2;可以放在下列语句之前:cin>>integer2;下列声明:intsum;可以放在下列语句之前:sum=integer1+integer2;下列句:cout<<"Enterfirstinteger\n";在屏幕上打印字符串Enterfirstinteger(b也称为字符串直接量(stringliteral)或直接量(literal)),将光标移到下一行开头。这个消息称为提示(prompt),提示用户进行特定操作。上述语句表示cout得到字符串“Enterfirstinteger\n".下列语句:cin>>integer1;用输入流对象cin和流读取运算符>>取得键盘中的值。利用流读取运算符cin从标准输入流读取输入(通常是键盘输入)。上述语句表示cin提供integer1的值。计算机执行上述语句时,等待用户输入变量integer1的值。用户输入整数值并按Enter键(或Return键),将数值发送给计算机。然后计算机将这个数(值)赋给变量integer1。程序中后面引用integer1时都使用这个值。cout和cin流对象实现用户与计算机之间的交互。由于这个交互像对话一样,因此通常称为对话式计算(conversationalcomputing)或交互式计算(interactivecomputing)。下列语句:cout<<"Entersecondinteger\n";在屏幕上打印”Entersecondinteger"字样,然后移到下一行的开头。这个语句提示用户进行操作。下列语句:cin>>integer2;从用户取得变量integer2的值。赋值语句:sum=integer1+integer2;计算变量integer1和integer2的和,然后用赋值运算符(assignmentoperator)"="将结果赋给变量sum。这个语句表示sum取得integer1加integer2的值。大多数计算都是在赋值语句中进行的。“=”运算符和前面的“+”运算符称为二元运算符,两个操作数是integer1和integer2。而对于“=”运算符,两个操作数是sum和表达式integer1+integer2的值。下列语句:cout<<"Sumis"<<sum<<endl;打印字符串"Sumis"和变量sum的数值,加上称为流操纵算子的endl(endline的缩写)。endl输出一个换行符,然后刷新输出缓冲区,即在一些系统中,输出暂时在机器中缓存,等缓冲区满时再打印到屏幕上,endl强制立即输出到屏幕上。注意,上述语句输出多种不同类的值,流插入运算符知道如何输出每个数据。在一个语句中使用多个流插入运算符称为连接(concatenating)、链接(chaining)或连续使用流插入操作。这样,就不必用多条输出语句输出多个数据。计算可以在输出语句中进行。可以将上述语句合二为一:cout<<"Sumis"<<integer1+integer2<<endl;从而不需要变量sum。右花括号告诉计算机到达了函数main的结尾。C++的一个强大我就是用户可以生成自己的数据类型(详见第6章),然后可以告诉C++如何用>>和<<运算符输入或输出这种类型的值(称为运算符重载,见第8章)。2.7算术运算算术运算符见图2.10,注意这里使用了许多代数中没有使用的符号。星号(*)表示乘法、百分号(%)表示求模(modulus)。图2.10所示的算术运算符都是二元运算符,即这些运算符取两个操作数。例如,表达式"integer1+integer2"包含二元运算符“+”和两个操作数integer1和integer2。C++操作算术运算符代数表达式C++表达式加+f+7f+7减-p-cp-c乘*bmb*m除/x/y或x÷yx/y求模%rmodsr%s图2.10算术运算符整除(即除数和被除数均为整数)取得整数结果。例如,表达式7/4得1,表达式17/5得3。注意,整除结果忽略分数部分,不用取整。C++提供求模(modulus)运算符“%”即求得整除的余数。求模运算是个整数运算符,只能使用整数操作数。表达式x%y取得x除以y的余数,这样,7%4得3,17%5得2。后面几章将介绍求模运算符许多有趣的应用。如确定一个数是否为另一个数的倍数(确定一个数为奇数或偶数是这个问题的一个特例)。C++中的算术运算表达式应以直线形式在计算机中输入。这样,a除以b应输入为"a/b",使所有常量、变量和运算符放在一行中。编译器通常不接受下列代数符号:a-b但有些特殊专业软件包支持复杂数学表达式更自然的表示方法。C++表达式中括号的使用和代数表达式中相同。例如,要将a乘以b+c的和,可以用:a*(b+c)C++中算术运算符的运算顺序是由运算符的优先级规则确定的,与代数中的相同:1.括号中的表达式先求值,程序员可以用括号指定运算顺序。括号具有最高优先级,对于嵌套括号,由内存向外层求值。2.乘法、除法、求模运算优先。如果表达式中有多个乘法、除法、求模运算,则从左向右求值。乘法、除法、求模的优先级相同。3.然后再进行加法和减法。如果表达式中有多个加法和减法,则从左向右求值。加法和减法的优先级相同。运算符优先级保证C++按正确顺序采用运算符。从左向右求值指的是运算符的结合律(associativity),也有一些运算符结合律是从右向左。图2.11总结了运算符优先级规则,引入其它C++运算符时,这个表可以扩充,详细的运算符优先级请参见附录。运算符运算求值顺序()括号最先求值,如果有嵌套括号,则先求最内层表达式的值,如果同一层有几对括号,则从左向右求值。*、/、或%乘、除、求模其次求值。如果有多个,则从左向右求值。+或-加、减最后求值。如要有多个,则从左向右求值。图2.11算术运算符优先级下面用几个表达式说明运算符优先级规则。每个例子都列出代数表达式和对应的C+表达式。下例求五个值的算术平均值:代数:C++:m=(a+b+c+d+e)/5;括号是必须的,因为作法的优先级比加法高,要把整个和(a+b+c+d+e)除以5,如果不加括号,则a+b+c+d+e/5的取值为:a+b+c+d+(e/5)下例是直线的方程:代数:y=mx+bC++:y=m*x+b;不需要括号,乘法优先于加法,因此先乘后加。下列包含模(%)、乘、除、加、减运算:代数:z=pr%q+w/x-yC++:z=p*r%q+w/x-y;⑥①②④③⑤语句下面的圆圈数字表示C++采用运算符的顺序。乘法、求模和除法首先从左向右求值(结合律为从左向右)因为它们的优先级高于加法和减法。然后进行加法和减法运算,也是从左向右求值。并不是有多对括号的表达式都包含嵌套括号。例如下列表达式不包含嵌套括号:a*(b+c)+c*(d+e)这些括号在同一层。要更好地了解运算符优先级规则,考虑二次多项式的求值:y=a*x*x+b*x+c;⑥①②④③⑤语句下面的圆圈数字表示C++采用运算符的顺序。C++中没有指数运算符,因此我们把x2表示为x*x,稍后会介绍标准库函数pow(数)。由于pow所需要的数据类型有一些特殊情况,因此放第3章再介绍。假设变量a、b、c、x初始化如下:a=2,b=3,c=7和x=5。图2.12演示了上述二次多项式的运算符优先级。上述赋值语句可以加上多余的括号,使代码更清晰:y=(a*x*x)+(b*x)+c;2.8判断:相等与关系运算符本节介绍简单的C++if结构,使程序根据某些条件的真假做出判断。如果条件符合,即为真(true),则执行if结构体的语句;如果不符合,即条件为假(false),则不执行语句,稍后将举例说明。if结构中的条件可以用相等运算符(equalityoperator)和关系运算符(relationaloperator)表示,如图2.13关系运算符具有相同的优先级,结合律为从左向右。相等运算符的优先级也相同,但低于关系运算符的优先级,结合律也为从左向右。标准代数相等与关系运算符C++相等与关系运算符C++条件举例C++条件含义===x==yx等于y≠!=x!=yx不等于y关系运算符>>x>yx大于y<<x<yx小于y≥>=x>=yx大于或等于y≤<=x<=yx小于或等于y图2.13相等与关系运算符下例用六个if语句比较用户输入的两个数。如果其中任何一个if语句的条件成立,则执行与该if相关联的输出语句。图2.14显示了这个程序和三个示例输出。注意图2.14的程序边疆使用流读取操作输入两个整数。首先将一个值读到num1中,然后将一个值读到num2中。if语句的缩排是为了提高程序的可读性。另外,注意图2.14中每个if语句体中有一条语句。第2章将会介绍结构体中有多条语句的if语句(将语句体放在花括号“{}”中)。1//Fig.2.14:fig01_14.cpp2//Usingifstatements,relationnal3//operators,andequalityoperators4#include<iosream>5usingnamespacestd;6intmain()7{8intnum1,num2;910cout<<"Entertwointegers,andIwilltellyou\n"11<<"therelationshipstheysatisfy:";12cin>>num1>>num2;//读取两个整数1314if(num1==num2)15cout<<num1<<"isequalto"<<num2<<endl;1617if(num1!=num2)18cout<<num1<<"isnotequalto"num2<<endl;1920if(num1<num2)21cout<<num1<<"islessthan"<<num2<<endl;2223if(num1>num2)24cout<<num1<<"isgreaterthan"<<num2<<endl;2526if(num1<=num2)27cout<<num1<<"islessthanorequalto"28<<num2<<endl;2930if(num1>=num2)31cout<<num1<<"isgreaterthanorequalto"32<<num2<<endl;3334return0;//返回一个程序正常结束的标识35}输出结果:Entertwointegers,andIwilltllyouTherelationshipstheysatisfy:373isnotequal73islessthan73islessthanorequalto7Entertwointegers,andIwilltellyoutherelationshipstheysatisfy:221222isnotequal1222isgretaerthan1222isgreaterthanorequalto12Entertwointegers,andIwilltellyoutherelationshipstheysatisfy:777isequalto77islessthanorequalto77isgreaterthanorequalto7图2.14使用相等和关系运算符注意图2.14中空格的用法。在C++语言中,空白字符(如制表符、换行符和空格)通常都被编译器忽略。因此,语句中可以根据程序员的爱好加上换行符和空格,但不能用换行符和空格分隔标识符。图2.15显示了本章介绍的运算符优先级,运算符优先顺序从上向下递减。注意赋值运算符之后的所有运算符结合律均为从左向右。加法是左结合的,因此表达式x+y+z求值为(x+y)+z。赋值运算符是从右向左结合的,因此表达式x=y=0求值为x=(y=0),首先将0赋给y,然后将结果0赋给x。运算符结合律类型()从左向右括号*/%从左向右乘+-从左向右加<<>>从左向右流插入/读取<<=>>=从左向右关系==!=从左向右等于=从右向左赋值图2.15运算符优先级和结合律2.9新型头文件与名字空间本节是为使用支持ANSI/ISO草案标准的编译器用户提供的。草案标准指定了许多旧式C++头文件的新名,包括iostream.h,大多数新式头文件不再用扩展名.h。图2.16改写图2.2,演示新型头文件和两种使用标准库头文件的方法。第3行:#include<iostream>演示新型头文件名语法。第5行:usingnamespacestd;指定用std名字空间(namespace),这是C++中的新特性。名字空间可以帮助程序员开发新的软件组件而不会与现有软件组件产生命名冲突。开发类库的一个问题是类和函数名可能已经使用。名字空间能为每个新软件组件保持惟一的名称。1//Fig.2.16:fig01_16.cpp2//Usingnew-styleheaderfiles3#include<iostream.h>45usingnamespacestd;67intmain()8{9cout<<"WelcomtoC++!\n";10std::cout<<"WelwcomtoC++!\n";1112return0;13}输出结果:WelcomtoC++!WelcomtoC++!图2.16使用新型头文件C++草案标准中的每个头文件用名字空间std保证今后C++标准库操作的每个特性是惟一的,不会与其它程序员开发的软件组件混淆起来,程序员不能用名字空间定义新的类库。上述语句只是表示我们使用C++标准库中的软件组件,要定义自己的类库,则可以将我们的所有类和函数放在名字空间deitel中,使我们的类库与所有其它公司的类库和C++标准类库区别开来。程序中出现"usingnamespacestd"语句之后,就可以像第9行那样用cout对象将数值输出到标准输出流中。如果使用两个或几个类库,其中有的特性名称相同,则可能引起名称冲突。这时就要用名字空间来完全限定所用的名称,例如第10行的std::cout:std::cout<<"WelcomtoC++!\n";cout的完全限定名为std::cout,如果全部采用这种形式,虽然比较繁琐,但程序中第5行的"usingnamespacestd"语句就没有必要了。using语句可以在C++标准库中使用每个名称的简写版本(或其它指定名字空间的名称)。我们将在本教程稍后详细介绍名字空间。目前并不是所有C++环境都已经支持新的头文件的命名格式。为此,我们在本教程大部分地方使用旧式头文件,只在介绍C++标准新特性时才使用新的头文件命名格式,使用新格式时将特别注明。第3章C++输入/输出流3.1简介C++标准库提供了—组扩展的输入/输出(I/O)功能。本章将详细介绍C++中最常用的一些I/O操作,并对其余的输入/输出功能做一简要的概述。本章的有些内容已经在前面提到,这里对输入/输出功能做一个更全面的介绍。本章讨论的许多输入/输出功能都是面向对象的,读者会发现C++的I/O操作能够实现许多功能。C++式的I/O中还大量利用了C++的其他许多特点,如引用、函数重载和运算符重载等等。C++使用的是类型安全(typesafe)的I/O操作,各种I/O操作都是以对数据类型敏感的方式来执行的。假定某个函数被专门定义好用来处理某一特定的数据类型,那么当需要的时候,该函数会被自动调用以处理所对应的数据类型。如果实际的数据类型和函数之间不匹配,就会产生编译错误。因此,错误数据是不可能通过系统检测的(C语言则不然。这是C语言的漏洞,会导致某些相当微妙而又奇怪的错误)。用户既可以指定自定义类型的I/O,也可以指定标准类型的I/O。这种可扩展性是C++最有价值的特点之一。3.2流C++的I/O是以字节流的形式实现的,流实际上就是一个字节序列。在输入操作中,字节从输入设备(如键盘、磁盘、网络连接等)流向内存;在输出操作中,字节从内存流向输出设备(如显示器、打印机、磁盘、网络连接等)。应用程序把字节的含义与字节关联起来。字节可以是ASCII字符、内部格式的原始数据、图形图像、数字音频、数字视频或其他任何应用程序所需要的信息。输人/输出系统的任务实际上就是以一种稳定、可靠的方式在设备与内存之间传输数据。传输过程中通常包括一些机械运动,如磁盘和磁带的旋转、在键盘上击键等等,这个过程所花费的时间要比处理器处理数据的时间长得多,因此要使性能发挥到最大程度就需要周密地安排I/O操作。一些介绍操作系统的书籍(见参考文献)深入地讨论了这类问题。C++提供了低级和高级I/O功能。低级I/O功能(即无格式I/O)通常只在设备和内存之间传输一些字节。这种传输过程以单个字节为单位.它确实能够提供高速度并且可以大容量的传输,但是使用起来不太方便。人们通常更愿意使用高级I/O功能(即格式化I/O)。高级I/O把若干个字节组合成有意义的单位,如整数、浮点数、字符、字符串以及用户自定义类型的数据。这种面向类型的I/O功能适合于大多数情况下的输入输出,但在处理大容量的I/O时不是很好。3.2.1iostream类库的头文件C++的iostream类库提供了数百种I/O功能,iostream类库的接口部分包含在几个头文件中。头文件iostream.h包含了操作所有输入/输出流所需的基本信息,因此大多数C++程序都应该包含这个头文件。头文件iostream.h中含有cin、cout、cerr、clog4个对象,对应于标准输入流、标准输出流、非缓冲和经缓冲的标准错误流。该头文件提供了无格式I/O功能和格式化I/O功能。在执行格式化I/O时,如果流中带有含参数的流操纵算子,头文件iomanip.h所包含的信息是有用的。头文件fstream.h所包含的信息对由用户控制的文件处理操作比较重要。第13章将在文件处理程序中使用这个头文件。每一种C++版本通常还包括其他一些与I/O相关的库,这些库提供了特定系统的某些功能,如控制专门用途的音频和视频设备。3.2.2输入/输出流类和对象iostream类库包含了许多用于处理大量I/O操作的类。其中,类istream支持流输入操作.类ostream支持流输出操作,类iostream同时支持流输入和输出操作。类istream和类ostream是通过单一继承从基类ios派生而来的。类iostream是通过多重继承从类istream和ostream派生而来的。运算符重载为完成输入/输出提供了一种方便的途径。重载的左移位运算符(<<)表示流的输出,称为流插入运算符;重载的右移位运算符(>>)表示流的输入,称为流读取运算符。这两个运算符可以和标准流对象cin、cout、cerr、clog以及用户自定义的流对象—起使用。cin是类istream的对象,它与标准输入设备(通常指键盘)连在一起。下面的语句用流读取运算符把整数变量grade(假设grade为int类型)的值从cin输入到内存中。cin>>grade;注意,流读取运算符完全能够识别所处理数据的类型。假设已经正确地声明了grade的类型,那么没有必要为指明数据类型而给流读取运算符添加类型信息。cout是类ostream的对象,它与标准输出设备(通常指显示设备)连在一起。下面的语句用流插入运算符cout把整型变量grade的值从内存输出到标准输出设备上。cout<<grade;注意,流插入运算符完全能够识别变量grade的数据类型,假定已经正确地声明了该变量,那么没有必要再为指明数据类型而给流插入运算符添加类型信息。cerr是类osteam的对象,它与标准错误输出设备连在一起。到对象cerr的输出是非缓冲输出,也就是说插入到cerr中的输出会被立即显示出来,非缓冲输出可迅速把出错信息告诉用户。clog是类ostream的对象,它与标准错误输出设备连在一起。到对象clog的输出是缓冲翰出。即每次插入clog可能使其输出保持在缓冲区,要等缓冲区刷新时才输出。C++中的文件处理用类ifstream执行文件的输入操作,用类ofstream执行文件的输出操作,用类fstream执行文件的输入/输出操作。类ifstream继承了类istream,类ofstream继承了类ostream,类fstream继承了类iostream。虽然多数系统所支持的完整的输入/输出流类层次结构中还有很多类,但这里列出的类能够实现多数程序员所需要的绝大部分功能。如果想更多地了解有关文件处理的内容,可参看C++系统中的类库指南。3.3输出流C++的类ostream提供了格式化输出和无格式输出的功能。输出功能包括:用流插入运算符输出标准类型的数据;用成员函数put输出字符;成员函数write的无格式化输出(3.5节);输出十进制、八进制、十六进制格式的整数(3.6.1节);输出各种精度的浮点数(3.6.2节)、输出强制带有小数点的浮点数(3.7.2节)以及用科学计数法和定点计数法表示的浮点数(3.7.6节);输出在指定域宽内对齐的数据(3.7.3节);输出在域宽内用指定字符填充空位的数据(3.7.4节);输出科学计数法和十六进制计数法中的大写字母(3.7.7节)。3.3.1流插入运算符流插入运算符(即重载的运算符<<)可实现流的输出。重载运算符<<是为了输出内部类型的数据项、字符中和指针值。3.9节要详细介绍如何用重载运算符<<输出用户自定义类型的数据项。图3.3中的范例程序用一条流插入语句显示了输出的字符串。图3.4中的范例程序用多条流插入语句显示输出的字符串,该程序的运行结果与图3.3中程序的运行结果相同。1//Fig.3.3:figll03.cpp2//Outputtingastringusingstreaminsertion.3#include<iostream>45usingnamespacestd;6intmain()7cout<<"WelcometoC++!\n";89returnO;10}输出结果:WelcometoC++!图3.3用一条流插入语句输出字符串1//Fig.3.4:figllO4.cpp2//Outputtingastringusingtwostreaminsertions.3#include<iostream>4usingnamespacestd;5intmain()6{7cout<<"Welcometo";8cout<<"C++!\n";910return0;11}输出结果:WelcometoC++!图3.4用两条流插入语句输出字符串也可以用流操纵算子endl(行结束)实现转义序列\n(换行符)的功能(见图3.5)。流操纵算子endl发送一个换行符并刷新输出缓冲区(不管输出缓冲区是否已满都把输出缓冲区中的内容立即输出)。也可以用下面的语句刷新输出缓冲区:cout<<flush;3.6节要详细讨论流操纵算子。1//Fig.3.5:fig11_05.cpp2//Usingtheendlstreammanipulator.3#include<iostream.h>4usingnamespacestd;5intmain{)6{7cout<<"Welcometo";8cout<<"c++!';9cout<<endl;//endlinestreammanipulator1011return0;12}输出结果:WelcometoC++!图3.5使用流操纵算子endl流插入运算符还可以输出表达式的值(见图3.6)1//Fig.3.6:fig11_O6.cpp2//Outputtingexpressionvalues.3#include<iostream.h>4usingnamespacestd;5intmain()6{7cout<<"47plus53is";89//parenthesesnotneeded;usedforclarity10cout<<(47+53);//expression11cout<<endt;1213returnO;14}输出结果:47plus53is100图3.6输出一个表达式的值3.3.2连续使用流插入/流读取运算符重载的运算符<<和>>都可以在一条浯句中连续使用(见图3.7)。1file://Fig.3.7:fig11_07.cpp2file://CascadlngtheoverlOadGd<<OPeratOr.3#includc<iostream.h>45intmain()6{7cout<<"47plus53is"<<(47+53)<<endl;89returnO;10}输出结果:47plus53is100图3.7连续使用重载运算符<<图中多次使用流插入运算符的语句等同于下面的语句:(((cout<<"47plus53is")<<47+53)<<endl);之所以可以使用这种写法,是因为重载的运算符<<返回了对其左操作数对象(即cout)的引用,因此最左边括号内的表达式:(cout<<"47plus53is")它输出一个指定的字符串,并返回对cout的引用,因而使中间括号内的表达式解释为:(cout<<(47+53))它输出整数值100,并返回对cout的引用。于是最右边括号内的表达式解释为:cout<<endl;它输出一个换行符,刷新cout并返回对cout的引用。最后的返回结果未被使用。3.3.3输出char*类型的变量C语言式的I/O必须要提供数据类型信息。C++对此作了改进,能够自动判别数据类型。但是,C++中有时还得使用类型信息。例如,我们知道字符串是char*类型,假定需要输出其指针的值,即字符串中第一个字符的地址,但是重载运算符<<输出的只是以空(null)字符结尾的char*类型的字符串,因此使用void*类型来完成上述需求(需要输出指针变量的地址时都可以使用void*类型)。图3.8中的程序演示了如何输出char*类型的字符串及其地址,辅出的地址是用十六进制格式表示。在C++中,十六进制数以0x或0X打头,3.6.1节、3.7.4节、3.7.5节和3.7.7节要详细介绍控制数值基数的方法。1//Fig.3.8:fig11_08.cpp2//Printingtheaddressstoredinachar*variable3#include<iostream.h>4usingnamespacestd;5intmain()6{7char*string="test";89cout<<"Valueofstringis:"<<string10<<"\nValueofstaticcast<void*>(string)is:"11<<static_cast<void*>(string)<<endl;12return0;13}输出结果:Valueofstringis:testValueofstaticcast<void*>(string)is:Ox00416D50图3.8输出char*类型变量的地址3.3.4用成员函数put输出字符和put函数的连续调用put成员函数用于输出一个字符,例如语句:cout.put('A');将字符A显示在屏幕上。也可以像下面那样在一条语句中连续调用put函数:cout.put('A').put('\n');该语句在输出字符A后输出一个换行符。和<<一样,上述语句中圆点运算符(.)从左向右结合,put成员函数返回调用put的对象的引用。还可以用ASCII码值表达式调用put函数,语句cout.put(65)也输出字符A。3.4输入流下面我们要讨论流的输入,这是用流读取运算符(即重载的运算符>>)实现的。流读取运算符通常会跳过输人流中的空格、tab键、换行符等等的空白字符,稍后将介绍如何改变这种行为。当遇到输入流中的文件结束符时,流读取运算符返回0(false);否则,流读取运算符返回对调用该运算符的对象的引用。每个输入流都包含一组用于控制流状态(即格式化、出错状态设置等)的状态位。当输入类型有错时,流读取运算符就会设置输人流的failbit状态位;如果操作失败则设置badbit状态位,后面会介绍如何在I/O操作后测试这些状态位。3.7节和3.8节详细讨论了流的状态位。3.4.1流读取运算符图3.9中的范例程序用cin对象和重载的流读取运算符>>读取了两个整数。注意流读运算符可以连续使用。1//Fig.3.9:figll_09.cpp2//Calculatingthesumoftwointegersinputfromthekeyboard3//withthecinoctandthestream-extractionoperator.4#include<iostream>5usingnamespacestd;6intmain()7{8intx,y;910cout<<"Entertwointegers:";11cin>>x>>y;12cout<<"Sumof"<<x<<"and"<<y<<"is:"13<<(x+y)<<endl;1415return0;16}输出结果:Entertwointegers:3092Sumof30and92is:122图3.9计算用cin和流读取运算符从键盘输入的两个整数值的和如果运算符>>和<<的优先级相对较高就会出现问题。例如,在图3.10的程序中,如果条件表达式没有用括号括起来,程序就得不到正确的编译(学员可以试一下)。1//Fig.3.10:figlll0.cpp2//Avoidingaprecedenceproblembetweenthestream-insertion3//operatorandtheconditionaloperator.4//Needparenthesesaroundtheconditionalexpression.5#include<iostream.h>6usingnamespacestd;7intmain()89intx,y;1011cout<<"Entertwointegers:";12cin>>x>>y;13cout<<x<<(x==y?"is":"isnot")14<<"equalto"<<y<<endl;1516return0;17}输出结果:Entertwointegers:757isnotequalto5Entertwointegers:888isequalto8图3.10避免在流插入运算符和条件运算符之间出现优先级错误我们通常在while循环结构的首部用流读取运算符输入一系列值。当遇到文件结束符时,读取运算符返回0false)。图3.11中的程序用于查找某次考试的最高成绩。假定事先不知道有多少个考试成绩,并且用户会输入表示成绩输入完毕的文件结束符。当用户输入文件结束符时,while循环结构中的条件(cin>>grade)将变为0(即false)。在图3.11中,cin>>grade可以作为条件,因为基类ios(继承istream的类)提供一个重载的强制类型转换运算符,将流变成void*类型的指针。如果读取数值时发生错误或遇到文件结束符,则指针值为0。编译器能够隐式使用void*类型的强制转换运算符。1//Fig.3.11:figll_ll.cpp2//Stream-extractionoperatorreturningfalseonend-of-file.3#include<iostream.h>4usingnamespacestd;5intmain()6{7intgrade,highestGrade=-1;89cout<<"Entergrade(enterend-of-filetoend):";10while(cin>>grade){11if(grade>highestGrade)12highestGrade=grade;1314cout<<"Entergrade(enterend-of-filetoend):";15}1617cout<<"\n\nHighestgradeis:"<<highestGrade<<endl;18return0;19}输出结果:Entergrade(enterend-of-filetoend):67Entergrade(enterend-of-filetoend):87Entergrade(enterendoffiletoend):73Entergrade(enterend-of-filetoend):95Entergrade(enterend-of-filetoend):34Entergrade(enterend-of-filetoend):99Entergrade(enterend-of-filetoend):^zHeighestgradeis:99图3.11流读取运算符在遇到文件结束符时返回false3.4.2成员函数get和getline不带参数的get函数从指定的输入流中读取(输入)一个字符(包括空白字符),并返回该字符作为函数调用的值;当遇到输入流中的文件结束符时,该版本的get函数返回EOF。图3.12中的程序把成员函数eof和get用于输入流cin,把成员函数put用于输出漉cout。程序首先输出了cin.eof()的值,输出值为0(false)表明没有在cin中遇到文件结束符。然后,程序让用户键入以文件结束符结尾的一行文本(在IBMPC兼容系统中,文件结束符用<ctrl>-z表示;在UNIX和Macintosh系统中,文件结束符用<ctrl>-d表示)。程序逐个读取所输入的每个字符,井调用成员函数put将所读取的内容送往输出流cout。当遇到文件结束符时,while循环终止,输出cin.eof()的值,输出值为1(true)表示已接收到了cin中的文件结束符。注意,程序中使用了类istream中不带参数的get成员函数,其返回值是所输入的字符。1//Fig.3.12:figll_12.cpp2//Usingmemberfunctionsget,putandeof.3#include<iostream>4usingnamespacestd;5intmain()6{7charc;89cout<<"Beforeinput,cin.eof()is"<<cin.eof()10<<"\nEnterasentencefollowedbyend-of-file:\n";1112while((c=cin.get())!=EOF)13cout.put(c);1415cout<<"\nEOFinthissystemis:"<<c;16cout<<"\nAfterinput,cin.eof()is"<<cin.eof()<<endl;17return0;18}输出结果:Beforeinput,cin.eof()is0Enterasentencefollowedbyend-of-file:Testingthegetandputmemberfunctions^zTestingthegetandputmemberfunctionsEOFinthissystemis:-1Afterinputcin.eof()is1图3.12使用成员函数get.put和eof带一个字符型参数的get成员函数自动读取输人流中的下一个字符(包括空白字符)。当遇到文件结束符时,该版本的函数返回0,否则返回对istream对象的引用,并用该引用再次调用get函数。带有三个参数的get成员函数的参数分别是接收字符的字符数组、字符数组的大小和分隔符(默认值为'\n')。函数或者在读取比指定的最大字符数少一个字符后结束,或者在遇到分隔符时结束。为使字符数组(被程序用作缓冲区)中的输入字符串能够结束,空字符会被插入到字符数组中。函数不把分隔符放到字符数组中,但是分隔符仍然会保留在输人流中。图3.13的程序比较了cin(与流读取运算符一起使用)和cin.get的输入结果。注意调用cin.get时未指定分隔符,因此用默认的'\n'。1//Fig.3.13:figll13.cpp2//contrastinginputofastringwithcinandcin.get.3#include<iostream.h>4usingnamespacestd;5intmain()6{7constintSIZE=80;8charbuffer1[SIZE],buffer2[SIZE];910cout<<"Enterasentence:\n";11cin>>buffer1;12cout<<"\nThestringreadwithcinwas:\n"13<<buffer1<<"n\n";1415cin.get(buffer2,SIZE);16cout<<"Thestringreadwithcin.getwas:\n"17<<buffer2<<endl;1819return0;20}输出结果:Enterasentence:Contrastingstringinputwithcinandcin.getThestringreadwithcinwas:ContrastingThestringreadwithcin.getwas:stringinputwithcinandcin.get图3.13比较用cin和cin.get输入的字符串的结果成员函数getline与带三个参数的get函数类似,它读取一行信息到字符数组中,然后插入一个空字符。所不同的是,getline要去除输入流中的分隔符(即读取字符并删除它),但是不把它存放在字符数组中。图3.14中的程序演示了用getline输入一行文本。1//Fig.3.14:figll_14.cpp2//Characterinputwithmemberfunctiongetline.3#include<iostream.h>45intmain()6{7constSIZE=80;8charbuffe[SIZE];910cout<<"Enterasentence:\n";11cin.getline(buffer,SIZE);1213cout<<"\nThesentenceenteredis:\n"<<buffer<<endl;14return0;15}输出结果:Enterasentence:UsingthegetlinememberfunctionThesentenceenteredis:Usingthegetlinememberfunction图3.14用成员函数getline输入字符3.5成员函数read、gcount和write的无格式输入/输出调用成员函数read、write可实现无格式输入/输出。这两个函数分别把一定量的字节写入字符数组和从字符数组中输出。这些字节都是未经任何格式化的,仅仅是以原始数据形式输入或输出。例如:charbuffe[]="HAPPYBIRTHDAY";cout.write(buffer,10);输出buffet的10个字节(包括终止cout和<<输出的空字符)。因为字符串就是第一个字符的地址,所以函数调用:cout.write("ABCDEFGHIJKLMNOPQRSTUVWXYZ",10);显示了字母表中的前10个字母。成员函数read把指定个数的字符输入到字符数组中。如果读取的字符个数少于指定的数目,可以设置标志位failbit(见3.8节)。成员函数gcount统计最后输入的字符个数。图3.15中的程序演示了类istream中的成员函数read和gcount,以及类ostream中的成员函数write。程序首先用函数read向字符数组buffer中输入20个字符(输入序列比字符数组长),然后用函数gcount统计所输入的字符个数,最后用函数write输出buffer中的字符。1//Fig.3.15:fig11_15.cpp2//UnformattedI/Owithread,gcountandwrite.3#include<iostream.h>4usingnamespacestd;5intmain()6{7constintSIZE=80;8charbuffer[SIZE];910cout<<"Enterasentence:\n";11cin.read(buffer,20);12cout<<"\nThesentenceenteredwas:\n";13cout.write(buffer,cin.gcount());14cout<<endl;15return0;16}输出结果:Enterasentence:Usingtheread,write,andgcountmemberfunctionsThesentenceenteredwas:Usingtheread,write图3.15成员函数read,gcount和write的无格式I/O3.6流操纵算子C++提供了大量的用于执行格式化输入/输出的流操纵算子。流操纵算子提供了许多功能,如设置域宽、设置精度、设置和清除格式化标志、设置域填充字符、刷新流、在输出流中插入换行符并刷新该流、在输出流中插入空字符、跳过输入流中的空白字符等等。下面几节要介绍这些特征。3.6.1整数流的基数:流操纵算子dec、oct、hex和setbase整数通常被解释为十进制(基数为10)整数。如下方法可改变流中整数的基数:插人流操纵算子hex可设置十六进制基数(基数为16)、插人流操纵算子oct可设置八进制基数(基数为8)、插人流操纵算子dec可恢复十进制基数。也可以用流操纵算子setbace来改变基数,流操纵算于setbase带有一个整数参数10、8或16。因为流操纵算子setbase是带有参数的,所以也称之为参数化的流操纵算子。使用setbose或其他任何参数化的操纵算子都必须在程序中包含头文件iomanip.h。如果不明确地改变流的基数,流的基数是不变的。图3.16中的程序示范了流操纵算子hex、oct、dec和setbase的用法。1//Fig.3.16:fig11_16.cpp2//Usinghex,oct,decandsetbasestreammanipulators.3#include<iostream>4#include<iomanip.h>5usingnamespacestd;6intmain()7{8intn;910cout<<"Enteradecimalnumber:";11cin>>n;1213cout<<n<<"inhexadecimalis:"14<<hex<<n<<'\n'15<<dec<<n<<"inoctalis:"16<<oct<<n<<'\n'17<<setbase(10)<<n<<"indecimalis:"18<<n<<endl;1920return0;21}输出结果:Enteradecimalnumber:2020inhexadecimalis:1420inoctalis:2420indecimalis:20图3.16使用流操纵算子hex、oct、dec和setbase3.6.2设置浮点数精度(precision、setprecision)在C++中可以人为控制浮点数的精度,也就是说可以用流操纵算子setprecision或成员函数percision控制小数点后面的位数。设置了精度以后,该精度对之后所有的输出操作都有效,直到下一次设置精度为止。无参数的成员函数percision返回当前设置的精度。图3.17中的程序用成员函数precision和流操纵算子setprecision打印出了2的平方根表,输出结果的精度从0连续变化到9。1//Fig.3.17:fig11_17.cpp2//Controllingprecisionoffloating-pointvalues3#include<iostream.h>4#include<iomanip.h>5#include<math.h>6usingnamespacestd;7intmain()8{9doubleroot2=sqrt(2.0);10intplaces;1112cout<<setiosflags(ios::fixed)13<<"Squarerootof2withprecisions0-9.\n"14<<"Precisionsetbythe"15<<"precisionmemberfunction:"<<endl;1617for(places=0;places<=9;places++){18cout.precision(places);19cout<<root2<<'\n';20}2122cout<<"\nPrecisionsetbythe"23<<"setprecisionmanipulator:\n";2425for(places=0;places<=9;places++)26cout<<setprecision(places)<<root2<<'\n';2728return0;29}输出结果:Squarerootof2withpzecisions0-9.Precisionsetbytheprecisionmemberfunction:11.41.411.4141.41421.414211.4142141.41421361.414213561.414213562Precisionsetbythesetprecisionmanipulator:11.41.4l1.4141.41421.41421·1.4142141.41421361.414213561.414213562图3.17控制浮点数的精度3.6.3设置域宽(setw、width)成员函数ios.width设置当前的域宽(即输入输出的字符数)并返回以前设置的域宽。如果显示数据所需的宽度比设置的域宽小,空位用填充字符填充。如果显示数据所需的宽度比设置的域宽大,显示数据并不会被截断,系统会输出所有位。图3.18中的程序示范了成员函数width的输入和输出操作。注意,输入操作提取字符串的最大宽度比定义的域宽小1,这是因为在输入的字符串后面必须加上一个空字符。当遇到非前导空白字符时,流读取操作就会终止。流操纵算子setw也可以用来设置域宽。注意:提示用户输入时,用户应输入一行文本并按Enter键加上文件结束符(<ctrl>-z对于IDMPC兼容系统;<ctrl>-d对于UNIX与Macintosh系统)。1//fig11_18.cpp2//Demonstratingthewidthmemberfunction3#include<iostream>4usingnamespacestd;5intmain()6{7intw=4;8charstring[10];910cout<<"Enterasentence:\n";11cin.width(5);1213while(cin>>string){14cout.width(w++);15cout<<string<<endl;16cin.width(5);17}1819return0;20}输出结果:Enterasentence:ThisisatestofthewidthmemberfunctionThisisatestofthewidthmemberfunction图3.18演示成员函数width3.6.4用户自定义的流操纵算子用户可以建立自己的流操纵算子。图3.19中的程序说明了如何建立和使用新的流操纵算子bell、ret、tab和endline。用户还可以建立自己的带参数的流操纵算子,这方面内容可参看系统的手册。1/!Fig.3.19:fig11_19.cpp2//Creatingandtestinguser-defined,nonparameterized3//streammanipulators.4#include<iostream.h>5usingnamespacestd;6//bellmanipulator(usingescapesequence\a)7ostream&bell(ostream&output){returnoutput<<'\a';}89//retmanipulator(usingescapesequence\r)10ostream&ret(ostream&output)(returnoutput<<'\r';}1112//tabmanipulator(usingescapesequence\t)13ostream&tab(ostream&output){returnoutput<<'\t';]1415//endLinemanipulator(usingescapesequence\n16//andtheflushmemberfunction)17ostream&endLine(ostream&output)18{19returnoutput<<'\n'<<flush;20}2122intmain()23{24cout<<"Testingthetabmanipulator:"<<endLine25<<'a'<<tab<<'b'<<tab<<'c'<<endLine26<<"Testingtheretandbellmanipulators:"27<<endLine<<"..........";28cout<<bell;29cout<<ret<<.........<<endLine;30return0;31}输出结果:Testingthetabmanipulator:abcTestingtheretandbellmanipulators:--------........图3.19建立并测试用户自定义的、无参数的流操作算子3.7流格式状态各种格式标志指定了在I/O流中执行的格式类型。成员函数serf、unserf和flag控制标志的设置。3.7.1格式状态标志图3.20中的格式状态标志在类ios中被定义为枚举值,留到下几节解释。虽然成员函数flags、serf和unseff可以控制这些标志,但是许多C++程序员更喜欢使用流操纵算子(见3.7.8节)。程序员可以用按位或操作(|)把各种标志选项结合在一个long类型的值中(见图3.23)。调用成员函数flags并指定要进行或操作的标志选项就可以在流中设置这些标志,并返回一个包含以前标志选项的long类型的值。返回值通常要保存起来,以便用保存值调用flags来恢复以前的流选项。flags函数必须指定一个代表所有标志设置的值。而带有一个参数的函数setf可指定一个或多个要进行或操作的标志,并把它们与现存的标志设置相“或”,形成新的格式状态。参数化的流操纵算子setiosflags与成员函数serf执行同样的功能。流操纵算子resetiosflags与成员函数unsetf执行同样的功能。不论使用哪一种流操纵算子,在程序中都必须包含头文件iomanip.h。skipws标志指示>>跳过输人流中的空白字符。>>的默认行为是跳过空白字符,调用unsetf(ios::skipws)可改变默认行为。流操纵算子ws也可用于指定跳过流中的空白字符。----------------------------------------------------------------------------------------------格式状态说明----------------------------------------------------------------------------------------------ios::skipws跳过输入流中的空白字符ios::left在域中左对齐输出,必要时在右边显示填充字符ios:right在域中右对齐输出,必要时在左边显示填充字符ios::internal表示数字的符号应在域中左对齐,而数字值应在域中右对齐(即在符号和数字之间填充字符ios::dec指定整数应作为十进制(基数10)值ios::oct指定整数应作为八进制(基数8)值ios::hex指定整数应作为十六进制(基数16)值ios::showbase指定在数字疥面输出进制(0表示八进制,0x或0X表示十六进制)ios::showpoint指定浮点数输出时应带小数点。这通常和ios::fixed一起使用保证小数点后面有一定位数ios::uppercase指定表示十六进制Rx应为大写,表示浮点科学计数法的e应为大写ios::showpos指定正数和负数前面分别加上正号或-号ios::scientific指事实上浮点数输出采用科学计数法ios::fixed指事实上浮点数输出采用定点符号,保证小数点后面有一定位数-----------------------------------------------------------------------------------------------图 3.20 格式状态标志3.7.2尾数零和十进制小数点(ios::showpoint)设置showpoint标志是为了强制输出浮点数的小数点和尾数零。若没有设置showpoint,浮点数79.0将被打印为79,否则打印为79.000000(或由当前精度指定的尾数零的个数)。图3.21中的程序用成员函数setf设置showpoint标志,从而控制了浮点数的尾数零和小数点的输出。i//Fig.3.21:fig11_21.cpp2//Controllingtheprintingoftrailingzerosanddecimal3//pointsforfloating-pointvalues.4#include<iostream.h>5#include<iomanip.h>6#include<math.h>78intmain()9{10cout<<"Beforesettingtheios::showpointflag\n"11<<"9.9900printsas:"<<9.990012<<"\n9.9000printsas:"<<9.900013<<"\n9.0000printsas:"<<9.000014<<"\n\nAftersettingtheios::showpointflag\n";15cout.setf(ios::showpoint);16cout<<"9.9900printsas:"<<9.990017<<"\n9.90O0printsas:"<<9.900018<<"\n9.0000printsas:"<<9.0000<<endl;19return0;2O}输出结果:Beforesettingtheios::showpolntflag9.9900printsas:9.999.9000printsas:9.99.9000printsas:9Aftersettingtheios::showpointflag9.9900printsas:9.990009.9000printsas:9.900009.0000printsas:9.00000图3.21控制浮点数的尾数零和小数点的输出3.7.3对齐(ios::left、ios::right、ios::internal)left标志可以使输出域左对齐并把填充字符放在输出数据的右边,right标志可以使输出域右对齐并把填充字符放在输出数据的左边。填充的字符由成员函数fill或参数化的流操纵算子setfill指定。图3.22用流操纵算子setw、setiosflags、reseticsfiags以及成员函数serf和unsetf控制整数值在域宽内左对齐和右对齐。1//Fig.3.22:fig11_22.cpp2//Left-justificationandright-justification.3#include<iostream.h>4#include<iomanip.h>6intmain()7{8intx=12345;910cout<<"Defaultisrightjustified:\n"11<<setw(10)<<x<<"\n\nUSINGMEMBERFUNCTIONS"12<<"\nUseserftosetios::left:\n"<<setw(10);1314cout.setf(ios::left,ios::adjustfield);15cout<<x<<"\nUseunsetftorestoredefault:\n";16cout.unsetf(ios::left);17cout<<setw(10)<<x18<<"\n\nUSINGPARAMETERIZEDSTREAMMANIPUAATORS"19<<"\nUsesetiosflagstosetios::left:\n"20<<setw(10)<<setiosflags(ios::left)<<x21<<"\nUseresetiosflagstorestoredefault:\n"22<<setw(10)<<resetiosflags(ios::left)23<<x<<endl;24return0;25}输出结果:Defaultisrightjustified:12345USINGMEMBERFUNCTIONSUsesetftosetios::left:12345Useunsetftorestoredefault:12345USINGPARAMETERIZEDSTREAMMANIPULATORSUsesetiosflagstosetios::left:12345Useresetiosflagstorestoredefault:12345图3.22左对齐和右对齐internal标志指示将一个数的符号位(设置了ios::showbase标志时为基数,见3.7.5节)在域宽内左对齐,数值右对齐,中间的空白由填充字符填充。left、right和internal标志包含在静态数据成员ios::adjustfield中。在设置left、right和internal对齐标志时,setf的第二个参数必须是ios::adjustfield,这样才能使serf只设置其中一个标志(这三个标志是互斥的)。图3.23中的程序用流操纵算子setiosflags和setw指定中间的空白。注意,ios::showpos标志强制打印了加号。1//Fig.3.23:figll_23.cpp2//Printinganintegerwithinternalspacingand3//forcingtheplussign.4#include<iostream.h>5#include<iomanip.h>67intmain()8{9cout<<setiosflags(ios::internal|ios::showpos)10<<setw(10)<<123<<endl;11returnO;12}输出结果:+123图3.23打印中间带空白的整数并强制输出加号3.7.4设置填充字符(fill、setfill)成员函数fill设置用于使输出域对齐的填充字符,如果不特别指定,空格即为填充字符。fill函数返回以前设置的填充字符。图3.24中的程序演示了使用成员函数fill和流操纵算子,setfill来控制填充字符的设置和清除。1//Fig.3.24:fig11_24.cpp2//Usingthefillmemberfunctionandthesetfill3//manipulatortochangethepaddingcharacterfor4//fieldslargerthanthevaluesbeingprinted.5#include<iostream.h>6finclude<iomanip.h>78intmain()9{10intx=10000;1112cout<<x<<"printedasihtrightandleftjustified\n"13<<"andashexwithinternaljustification.\n"14<<"Usingthedefaultpadcharacter(space):\n";15cout.setf(ios::showbase);16cout<<setw(10)<<x<<'\n';17cout.setf(ios::left,ios::adjustfield);18cout<<setw(10}<<x<<'\n';19cout.serf(ios::internal,ios::adjustfield);20cout<<setw(10)<<hex<<x;2122cout<<"\n\nUsingvariouspaddingcharacters:\n";23cout.setf(ios::right,ios::adjustfield);24cout.fill('*');25cout<<setw(10)<<dec<<x<<'\n';26cout.setf(ios::left,ios::adjustfield);27cout<<setw(10)<<setfill('%')<<x<<'\n';28cout.setf(ios::internal,ios::adjustfield);29cout<<setw(10)<<setfill('^')<<hex<<x<<endl;30return0;31}输出结果:10000printedasintrightandleftjustifiedandashexwithinternaljustification.Usingthedefaultpadcharacter(space):l0000100000x2710Usingvariouspaddingcharacters:*****1000010000%%%%%图3.24用fill和setfill为实际宽度小于域宽的数据改变填充字符3.7.5整数流的基数:(ios::dec、ios::oct、ios::hex、ios::showbase)静态成员ios::basefield(在setf中的用法与ios::adjustfield类似)包括ios::oct、ios::hex和ios::dec标志位,这些标志位分别指定把整数作为八进制、十六进制和十进制值处理。如果没有设置这些位,则流插入运算默认整数为十进制数,流读取运算按整数提供的方式处理数据(即以零打头的整数按八进制数处理,以0x或0X打头的按十六进制数处理,其他所有整数都按十进制数处理)。一旦为流指定了特定的基数,流中的所有整数都按该基数处理,直到指定了新的基数或程序结束为止。设置showbase标志可强制输出整数值的基数。十进制数以通常方式输出,输出的八进制数以0打头,输出的十六进制数以0x或0X打头(由uppercase标志决定是0x还是0X,见3.7.7节)。图3.25中的程序用showbase标志强制整数按十进制、八进制和十六进制格式打印。1//Fig.3.25:figll_25.cpp2//Usingtheios::showbaseflag3#include<iostream.h>4#include<iomanip.h>56intmain()7{8intx=100;910cout<<setiosflags(ios::showbase)11<<"Printingintegersprecededbytheirbase:\n"12<<x<<'\n'13<<oct<<x<<'\n'14<<hex<<x<<endl;15return0;16}输入结果:Printingintegersprecededbytheirbase:I0001440x64图3.25使用ios::showbase标志3.7.6浮点数和科学记数法(ios::scientific、ios::fixed)ios::scientific和ios::fixed标志包含在静态数据成员ios::floatfield中(在setf中的用法与ios::adjustfield和ios::basefield类似)。这些标志用于控制浮点数的输出格式。设置scientific标志使浮点数按科学记数法输出,设置fixed标志使浮点数按照定点格式输出,即显示出小数点,小数点后边有指定的位数(由成员函数precision指定)。若没有这些设置,则浮点数的值决定输出格式。调用cout.setf(O,ios::floatfield)恢复输出浮点数的系统默认格式。图3.26中的程序示范了以定点格式和科学记数法显示的浮点数。1//Fig.3.26:flg11_26.cpp2//Displayingfloating-pointvaluesinsystemdefault,3//scientific,andfixedformats.4#include<iostream.h>56intmain()7{8doublex=.001239567,y=1.946e9;910cout<<"Displayedindefaultformat:\n"11<<x<<'\t'<<y<<'\n';12cout.setf(ios::scientific,ios::floatfield);13cout<<"Displayedinscientificformat:\n"14<<x<<'\t'<<y<<'\n';15cout.unsetf(ios::scientific);16cout<<"Displayedindefaultformatafterunsetf:\n"17<<x<<,\t'<<y<<,\n,;18cout.setf(ios::fixed,ios::floatfield);19cout<<"Displayedinfixedformat:\n"20<<x<<'\t'<<y<<endl;21return0;22}输出结果:Displayedindefaultformat:0.001234571.946e+009Displayedinscientificformat:1.234567e-0031.946000e+009Displayedindefaultformatafterunsetf:0.001234571.946e+009Displayedinfixedformat:0.0012351946000000.000000图3.26以系统默认格式、科学计数法和定点格式显示浮点数3.7.7大/小写控制(ios::upercase)设置ios::uppercase标志用来使大写的X和E分别同十六进制整数和以科学记数法表示的浮点数一起输出(见图3.27)一旦设置ios::uppercase标志,十六进制数中的所有字母都转换成大写。1//Fig.3.27:fig11_27.cpp2//Usingtheios::uppercaseflag3#include<iostream.h>4#include<iomanip.h>56intmain()7{8cout<<setiosflags(ios::uppercase)9<<"Printinguppercaselettersinscientific\n"10<<"notationexponentsandhexadecimalvalues:\n"11<<4.345e10<<'\n'<<hex<<123456789<<endl;12returnO;13}输出结果:Printinguppercaselettersinscientificnotationexponentsandhexadecimalvalues:4.395E+01075BCD16图3.27使用ios::uppercase标志3.7.8设置及清除格式标志(flags、setiosflags、resetosflags)无参数的成员函数flags只返回格式标志的当前设置(long类型的值)。带一个long类型参数的成员函数flags按参数指定的格式设置标志,返回以前的标志设置。flags的参数中未指定的任何格式都会被清除。图3.18的程序用成员函数flags设置新的格式状态和保存以前的格式状态,然后恢复原来的格式设置。1//Fig.3.28:fig11_28.cpp2//Demonstratingtheflagsmemberfunction.3#include<iostream.h>45intmain()6{7inti=1000;8doubled=0.0947625;910cout<<"Thevalueoftheflagsvariableis:"11<<cout.flags()12<<"\nPrintintanddoubleinoriginalformat:\n"13<<i<<'\t'<<d<<"\n\n";14longoriginalFormat=15cout.flags(ios::oct|ios::scientific);16cout<<"Thevalueoftheflagsvariableis:"17<<cout.flags()18<<"\nPrintintanddoubleinanewformat\n"19<<"specifiedusingtheflagsmemberfunction:\n"20<<i<<'\t'<<d<<"\n\n";21cout.flags(originalFormat);22cout<<"Thevalueoftheflagsvariableis:"23<<cout.flags()24<<"\nPrintvaluesinoriginalformatagain:\n"25<<i<<'\t'<<d<<endl;26return0;27}输出结果:Thevalueoftheflagsvariableis:0Printintanddoubleinoriginalformat:10000.0947628Thevalueoftheflagsvariableis:4040PrintihtanddoubleinanewformatSpecifiedusingtheflagsmemberfunction:17509.476280e-002Thevalueoftheflagsvariableis:0Printvaluesinoriginalformatagain:10000.0947628图3.28演示成员函数flags成员函数serf设置参数所指定的格式标志,并返回long类型的标志设置值,例如:longpreviousFlagSettings=cout.setf(ios::showpoint|ios::showpos);带两个long型参数的setf成员函数.例如:cout.setf(ios::left,ios::adjustfield);首先清除ios::adjustfield位,然后设置ios::left标志。该版本的setf用于与ios::basefield(用ios::dec、ios::oct和ios::hex表示)、ios::floatfield(用ios::scientific和ios::fixed表示)和ios::adjustfiald(用ios::left、ios::right和ios::internal表示)相关的位段。成员函数unsetf清除指定的标志并返回清除前的标志值。3.8流错误状态可以用类ios中的位测试流的状态。类ios是输入/输出类istream、ostream和iostream的基类。当遇到文件结束符时,输人流中自动设置eofbit。可以在程序中使用成员函数eof确定是否已经到达文件尾。如果cin遇到了文件结束符,那么函数调用:cin.eof()返回true,否则返回false。当流中发生格式错误时,虽然会设置failbit,但是字符并末丢失。成员函数fail判断流操作是否失败,这种错误通常可修复。当发生导致数据丢失的错误时,设置badbit。成员函数bad判断流操作是否失败,这种严重错误通常不可修复。如果eofbit、failbit或badbit都没有设置,则设置goodbit。如果函数bad、fail和eof全都返回false,则成员函数good返回true。程序中应该只对“好”的流进行I/O操作。成员函数rdstate返回流的错误状态。例如.函数调用cout.rdstate将返回流的状态,随后可以用一条switch语句测试该状态,测试工作包括检查ios::eofbit、ios::badbit、ios::failbit和ios::goodbit。测试流状态的较好方法是使用成员函数eof、bad、fail和good,使用这些函数不需要程序员熟知特定的状态位。成员函数clear通常用于把—个流的状态恢复为“好”,从而可以对该流继续执行I/O操作。由于clear的默认参数为ios::goodbit,所以下列语句:cin.clear();清除cin,并为流设置goodbit。下列语句:cin.clear(ios::failbit)实际上给流设置了failbit。在用自定义类型对cin执行输入操作或遇到问题时,用户可能需要这么做。clear这个名字用在这里似乎并不合适,但规定就是如此。图3.29中的程序演示了成员函数rdstate、eof、fail、bad、good和clear的使用。只要badbit和failbit中有一个被置位,成员函数operator!就返回true。只要badbit和failbit中有一个被置位,成员函数operatorvoid*就返回false。这些函数可用于文件处理过程中测试选择结构或循环结构条件的true/false情况。1//Fig.3.29:fig11_29.cpp2//Testingerrorstates.3#include<iostream.h>45intmain()6{7intx;8cout<<"Beforeabadinputoperation:"9<<"\ncin.rdstate():"<<cin.rdstate()10<<"\ncin.eof():"<<cin.eof()11<<"Incin.fail():"<<cin.fail()12<<"\ncin.bad():"<<cin.bad()13<<"\ncin.good():"<<cin.good()14<<"\n\nExpectsaninteger,butenteracharacter:";15cin>>x;1617cout<<"\nEnterabadinputoperation:"18<<"\ncin.rdstate():"<<cin.rdstate()19<<"\ncin.eof():"<<cin.eof()20<<"\ncin.fail():"<<cin.fail()21<<"\ncin.bad():"<<cin.badO22<<"\ncin.geed()"<<cin.good()<<"\n\n";2324cin.clear();2526cout<<"Aftercin.clear()"27<<"\ncin.fail():"<<cin.fail()28<<"\ncin.good():"<<cin.good{)<<endl;29return0;30}输出结果:Beforeabadinputoperation:cin.rdstate():0cin.eof():0cin.fail()0tin.bad():0cin.good():0Expectsaninteger,butenteracharacter:AAfterabadinputoperation:cin.eof():0cin.fail():2cin.bad():0cin.good():0Aftercin.clear()cin.fail():0cin.good():1图3.29测试错误状态第4章文件处理4.1简介存储在变量和数组中的数据是临时的,这些数据在程序运行结束后都会消失。文件用来永久地保存大量的数据。计算机把文件存储在二级存储设备中(特别是磁盘存储设备)。本章要讨论怎样用C++程序建立、更新和处理数据文件(包括顺序存储文件和随机访问文件)。我们要比较格式化与“原始数据”文件处理。后面将介绍从string而不是从文件输入和输出数据。4.2文件和流C++语言把每一个文件都看成一个有序的字节流(见图4.2),每一个文件或者以文件结束符(end-of-filemarker)结束,或者在特定的字节号处结束(结束文件的特定的字节号记录在由系统维护和管理的数据结构中)。当打开一个文件时,该文件就和某个流关联起来。第11章曾介绍过cin、cout、cerr和clog这4个对象会自动生成。与这些对象相关联的流提供程序与特定文件或设备之间的通信通道。例如.cin对象(标准输入流对象)使程序能从键盘输入数据,cout对象(标准输出流对象)使程序能向屏幕输出数据,cerr和clog对象(标准错误流对象)使程序能向屏幕输出错误消息。图4.2C++把文件看成n个字节要在C++中进行文件处理,就要包括头文件<iostream.h>和<fstream.h>。<fstream.h>头文件包括流类ifstream(从文件输入)、ofstream(向文件输出)和fstream(从文件输入,输出)的定义。生成这些流类的对象即可打开文件。这些流类分别从istream、ostream和iostream类派生(即继承它们的功能)。这样,第2章“C++输入,输出流”中介绍的成员函数、运算符和流操纵算子也可用于文件流。4.3建立并写入文件因为C++把文件看着是无结构的字节流,所以记录等等的说法在C++文件中是不存在的。为此,程序员必须提供满足特定应用程序要求的文件结构。下例说明了程序员是怎样给文件强加一个记录结构。先列出程序,然后再分析细节。图4.4中的程序建立了一个简单的访问文件,该文件可用在应收账目管理系统中跟踪公司借贷客户的欠款数目。程序能够获取每一个客户的账号、客户名和对客户的结算额。一个客户的数据就构成了该客户的记录。账号在应用程序中用作记录关键字,文件按账号顺序建立和维护。范例程序假定用户是按账号顺序键人记录的(为了让用户按任意顺序键入记录,完善的应收账目管理系统应该具备排序能力)。然后把键入的记录保存并写入文件。1//Fig.4.4;fig14_04.cpp2//Createasequentialfile3#include<iostream.h>##include<fstream.h>5#include<stdlib.h>6usingnamespacestd;7intmain()8{9//ofstreamconstructoropensfile10ofstreamfout("clients.dat");1112if(!fout){//overloaded!operator13cerr<<"Filecouldnotbeopened"<<endl;14exit(1);//prototypeinstdlib.h15}1617cout<<"Entertheaccount,name,andbalance.\n"18<<"Enterend-of-filetoendinput.\n?";1920intaccount;21charname[30];22floatbalance;2324while(cin>>account>>name>>balance){25fout<<account<<''<<name<<''<<balance<<'\n';2627cout<<"?";28}2930return0;//ofstreamdestructorclosesfile31}输出结果:Entertheaccount,name,andbalance.Enterend-of-filetoendinput.?100Jones24.98?200Doe345.67?300White0?400Stone-42.16?500Rich224.62?^z图4.4建立文件现在我们来研究这个程序。前面曾介绍过,文件通过建立ifstream、ofstream或fstream流类的对象而打开。图4.4中,要打开文件以便输出,因此生成ofstream对象。向对象构造函数传入两个参数——文件名和文件打开方式。对于ostream对象,文件打开方式可以是ios::out(将数据输出到文件)或ios::app(将数据添加到文件末尾,而不修改文件中现有的数据)。现有文件用ios::out打开时会截尾,即文件中的所有数据均删除。如果指定文件还不存在,则用该文件名生成这个文件。下列声明(第10行):ofstreamfout(“clients.dat”);生成ofstream对象fout,与打开输出的文件clients.dat相关联。参数"clients.dat"和ios::out传入ofstream构造函数,该函数打开文件,从而建立与文件的通信线路。默认情况下,打开ofstream对象以便输出,因此下列语句:ofstreamfout(”clients.dat”);也可以打开clients.dat进行输出。图14.5列出了文件打开方式。也可以生成ofstream对象而不打开特定文件,可以在后面再将文件与对象相连接。例如,下列声明:ofstreamfout;生成以ofstram对象fout。ofstream成员函数open打开文件并将其与现有ofstream对象相连接,如下所示:foutopen(“clients.dat”);------------------------------------------------------------------------------------------文件打开方式说明------------------------------------------------------------------------------------------ios::app将所有输出写入文件末尾ios::ate打开文件以便输出,井移到文件末尾(通常用于添加数据)数据可以写入文件中的任何地方ios::in打开文件以便输入ios::out打开文件以便输出ios::trunc删除文件现有内容(是ios::out的默认操作)ios::nocreate如果文件不存在,则文件打开失败ios::noreplace如果文件存在,则文件打开失败------------------------------------------------------------------------------------------图4.5文件打开方式生成ofstream对象并准备打开时,程序测试打开操作是否成功。下列if结构中的操作(第12行到第15行):if(!fout){cerr<<"Filecouldnotbeopened"<<endl;exit(1);}用重载的ios运算符成员函数operator!确定打开操作是否成功。如果open操作的流将failbit或badbit设置,则这个条件返回非0值(true)。可能的错误是试图打开读取不存在的文件、试图打开读取没有权限的文件或试图打开文件以便写人而磁盘空间不足。如果条件表示打开操作不成功.则输出错误消息“Filecouldnotbeopened",并调用函数exit结束程序,exit的参数返回到调用该程序的环境中,参数0表示程序正常终止.任何其他值表示程序因某个错误而终止。exit返回的值让调用环境(通常是操作系统)对错误做出相应的响应。另一个重载的ios运算符成员函数operatorvoid*将流变成指针,使其测试为0(空指针)或非0(任何其他指针值)。如果failbit或badbit(见第11章)对流进行设置,则返回0(false)。下列while首部的条件自动调用operatorvoid*成员函数:while(cin>>account>>name>>balance)只要cin的failbit和badbit都没有设置,则条件保持true。输入文件结束符设置cin的failbit。operatorvoid*函数可以测试输入对象的文件结束符,而不必对输入对象显式调用eof成员函数。如果文件打开成功,则程序开始处理数据。下列语句(第17行和第18行)提示用户对每个记录输入不同域,或在数据输入完成时输入文件结束符:cout<<"Entertheaccount,name,andbalance.\n"<<"EnterEOFtoandinput.\n?";图4.6列出了不同计算机系统中文件结束符的键盘组合。---------------------------------------------------计算机系统组合键---------------------------------------------------UNIX系统<ctrl>dIBMPC及其兼容机<ctrl>zMacintosh<ctrl>dVAX(VMS)<ctrl>z---------------------------------------------------图14.6各种流行的计算机系统中的文件结束组合键下列语句(第24行):while(cin>>account>>name>>balance)输入每组数据并确定是否输人了文件结束符。输入文件结束符或不合法数据时,cin的流读取运算符>>返回0(通常这个流读取运算符>>返回cin),while结构终止。用户输入文件结束符告诉程序没有更多要处理的数据。当用户输入文件结束符组合键时,设置文件结束符。只要没有输入文件结束符,while结构就一直循环。第25行和第26行:fout<<account<<''<<name<<''<<balance<<'\n';用流插人运算符<<和程序开头与文件相关联的fout对象将一组数据写入文件”clients.dat"。可以用读取文件的程序取得这些数据(见4.5节)。注意图4.4中生成的文件是文本文件,可以用任何文本编辑器读取。输人文件结束符后,main终止,使得fout对象删除,从而调用其析构函数,关闭文件clients.dat。程序员可以用成员函数close显式关闭ofstream对象,如下所示:fout.close();4.4读取文件中的数据为了在需要的时候能够检索要处理的数据,数据要存储在文件中。上一节演示丁怎样建立一个顺序访问的文件。这一节要讨论按顺序读取文件中的数据。图4.7中的程序读取文件"clients.dat"(图4.4中的程序建立)中的记录,并打印出了记录的内容。通过建立ifstream类对象打开文件以便输入。向对象传入的两个参数是文件名和文件打开方式。下列声明:ifstreamfin("clients.dat");生成ifstream对象fin,并将其与打开以便输入的文件clients.dat相关联。括号中的参数传入ifstream构造函数,打开文件并建立与文件的通信线路。打开ifstream类对象默认为进行输入,因此下列语句:ifstreamfin("Clients.dat");可以打开clients.dat以便输入。和ofstream对象一样,ifstream对象也可以生成而不打开特定文件,然后再将对象与文件相连接。程序用fin条件确定文件是否打开成功,然后再从文件中读取数据。下列语句:while(fin>>account>>name>>balance)从文件中读取一组值(即记录)。第一次执行完该条语句后,account的值为100,name的值为"John",balance的值为24.98。每次执行程序中的该条语句时,函数都读取文件中的另一条记录,并把新的值赋给account、name和balance。记录用函数outputLine显示,该函数用参数化流操纵算子将数据格式化之后再显示。到达文件末尾时,while结构中的输入序列返回0(通常返回fin流),ifstream析构函数将文件关闭,程序终止。1//Fig.4.7:fig14_O7.cpp2//Readingandprintingasequentialfile3#include<iostreamoh>4#include<fstream.h>5#include<iomanip.h>6#include<stdlib.h>78voidoutputLine(int,constchar*,double);910intmain()11{12//ifstreamconstructoropensthefile13ifstreamfin("clients.dat");1415if{!fin){16cerr<<"Filecouldnotbeopened\n";17exit(1);18}1920intaccount;21charname[30];22doublebalance;2324cout<<setiosflags(ios::left)<<setw(10)<<"Account"25<<setw(13)<<"Name"<<"Balance\n";2627while(fin>>account>>name>>balance)28outputLine(account,name,balance);2930return0;//ifstreamdestructorclosesthefile31}3233voidoutputLine(intacct,constchar*name,doublebal)34{35cout<<setiosflags(ios::left)<<setw(10)<<acct36<<setw(13)<<name<<setw(7)<<setprecision(2)37<<resetiosflags(ios::left)38<<setiosflags(ios::fixed|ios::showpoint)39<<bal<<'\n';40}输出结果:AccountNameBalancei00Jones24.98200Doe345.67300White0.00400Stone-42.16500Rich224.62图4.7读取并打印一个顺序文件为了按顺序检索文件中的数据,程序通常要从文件的起始位置开始读取数据,然后连续地读取所有的数据,直到找到所需要的数据为止。程序执行中可能需要按顺序从文件开始位置处理文件中的数据好几次。istreatrl类和ostream类都提供成员函数,使程序把“文件位置指针”(filepositionpointer,指示读写操作所在的下一个字节号)重新定位。这些成员函数是istream类的seekg(“seekget”)和ostream类的seekp(“seekput”)。每个istream对象有个get指针,表示文件中下一个输入相距的字节数,每个ostream对象有一个put指针,表示文件中下一个输出相距的字节数。下列语句:fin.seekg(0);将文件位置指针移到文件开头(位置0),连接fin。seekg的参数通常为long类型的整数。第二个参数可以指定寻找方向,ios::beg(默认)相对于流的开头定位,ios::cur相对于流当前位置定位,ios::end相对于流结尾定位。文件位置指针是个整数值,指定文件中离文件开头的相对位置(也称为离文件开头的偏移量)。下面是一些get文件位置指针的例子://positiontothenthbyteoffileObject//assumesios::begfileObject.seekg(n);//positionnbytesforwardinfileObjectfileObject.seekg(n,ios::cur);//positionybytesbackfromendoffileObjectfileObject.seekg(y,ios::end);//positionatendoffileObjectfileObject.seekg(o,ios::end);ostream成员函数seekp也可以进行类似的操作。成员函数tellg和tellp分别返回get和put指针的当前位置。下列语句将get文件位置指针值赋给long类型的变量location。location=filObject.tellg();4.5更新访问文件4.4节介绍了格式化和写入访问文件的数据修改时会有破坏文件中其他数据的危险。例如,如果要把名字"White"改为"Worthinglon",则不是简单地重定义旧的名字。White的记录是以如下形式写人文件中的:300White0.00如果用新的名字从文件中相同的起始位置重写该记录,记录的格式就成为:300Worthington0.00因为新的记录长度大于原始记录的长度,所以从“Worthington"的第二个“0”之后的字符将重定义文件中的下一条顺序记录。出现该问题的原因在于:在使用流插入运算符<<和流读取运算符>>的格式化输人,输出模型中,域的大小是不定的,因而记录的大小也是不定的。例如,7、14、-117、2047和27383都是int类型的值,虽然它们的内部存储占用相同的字节数,但是将它们以格式化文本打印到屏幕上或存储在磁盘上时要占用不同大小的域。因此,格式化输入,输出模型通常不用来更新已有的记录。也可以修改上述名字,但比较危险。比如,在300Whlte0.00之前的记录要复制到一个新的文件中,然后写入新的记录并把300White0.00之后的记录复制到新文件中。这种方法要求在更新一条记录时处理文件中的每一条记录。如果文件中一次要更新许多记录,则可以用这种方法。第5章C++的字符串流5.1流的继承关系在C++中,有一个stream这个类,所有的I/O都以这个“流”类为基础的,里面包括了所有的输入输出类,今天我们就来介绍一下sstream.h(字符串流)这个类:  C++引入了ostringstream、istringstream、stringstream这三个类,要使用他们创建对象就必须包含sstream.h头文件。  istringstream类用于执行C++风格的串流的输入操作。  ostringstream类用于执行C风格的串流的输出操作。  strstream类同时可以支持C风格的串流的输入输出操作。  istringstream类是从istream(输入流类)和stringstreambase(c++字符串流基类)派生而来,ostringstream是从ostream(输出流类)和stringstreambase(c++字符串流基类)派生而来,stringstream则是从iostream(输入输出流类)和和stringstreambase(c++字符串流基类)派生而来。  他们的继承关系如下图所示:    HYPERLINK"http://baike.baidu.com/image/064936382dcc3a0ab9998f08"\t"_blank"  5.2字串流的输入操作istringstream是由一个string对象构造而来,istringstream类从一个string对象读取字符。  istringstream的构造函数原形如下:  istringstream::istringstream(stringstr);  #include<iostream>  #include<sstream>  usingnamespacestd;  intmain()  {  istringstreamistr;  istr.str("156.7",);  //上述两个过程可以简单写成istringstreamistr("156.7");  cout<<istr.str()<<endl;  inta;  floatb;  istr>>a;  cout<<a<<endl;  istr>>b;  cout<<b<<endl;  system("pause");  }  上例中,构造字符串流的时候,空格会成为字符串参数的内部分界,例子中对a,b对象的输入"赋值"操作证明了这一点,字符串的空格成为了整型数据与浮点型数据的分解点,利用分界获取的方法我们事实上完成了字符串到整型对象与浮点型对象的拆分转换过程。str()成员函数的使用可以让istringstream对象返回一个string字符串(例如本例中的输出操作(cout<<istr.str();)。5.3字串流的输出操作  ostringstream同样是由一个string对象构造而来,ostringstream类向一个string插入字符。  ostringstream的构造函数原形如下:  ostringstream::ostringstream(stringstr);  示例代码如下:  #include<iostream>  #include<sstream>  #include<string>  usingnamespacestd;  intmain()  {  ostringstreamostr;  ostr.str("abc");//如果构造的时候设置了字符串参数,那么增长操作的时候不会从结尾开始增加,而是修改原有数据,超出的部分增长  ostr.put('d');  ostr.put('e');  ostr<<"fg";  stringgstr=ostr.str();  cout<<gstr;  system("pause");  }  在上例代码中,我们通过put()或者左移操作符可以不断向ostr插入单个字符或者是字符串,通过str()函数返回增长过后的完整字符串数据,但值得注意的一点是,当构造的时候对象内已经存在字符串数据的时候,那么增长操作的时候不会从结尾开始增加,而是修改原有数据,超出的部分增长。  对于stringstream了来说,不用我多说,大家也已经知道它是用于C++风格的字符串的输入输出的。  stringstream的构造函数原形如下:  stringstream::stringstream(stringstr);  示例代码如下:  #include<iostream>  #include<sstream>  #include<string>  usingnamespacestd;  intmain()  {  stringstreamostr("ccc");  ostr.put('d');  ostr.put('e');  ostr<<"fg";  stringgstr=ostr.str();  cout<<gstr<<endl;  chara;  ostr>>a;  cout<<a  system("pause");}5.4字串流在数据类型转换中的应用  除此而外,stringstream类的对象我们还常用它进行string与各种内置类型数据之间的转换。  示例代码如下:  #include<iostream>  #include<sstream>  #include<string>  usingnamespacestd;  intmain()  {  stringstreamsstr;  //--------int转string-----------  inta=100;  stringstr;  sstr<<a;  sstr>>str;  cout<<str<<endl;  //--------string转char[]--------  sstr.clear();//如果你想通过使用同一stringstream对象实现多种类型的转换,请注意在每一次转换之后都必须调用clear()成员函数。  stringname="colinguan";  charcname[200];  sstr<<name;  sstr>>cname;  cout<<cname;  system("pause");}5.5输入/输出的状态标志  接下来我们来学习一下输入/输出的状态标志的相关知识,C++中负责的输入/输出的系统包括了关于每一个输入/输出操作的结果的记录信息。这些当前的状态信息被包含在io_state类型的对象中。io_state是一个枚举类型(就像open_mode一样),以下便是它包含的值。  goodbit无错误  Eofbit已到达文件尾  failbit非致命的输入/输出错误,可挽回  badbit 致命的输入/输出错误,无法挽回  有两种方法可以获得输入/输出的状态信息。一种方法是通过调用rdstate()函数,它将返回当前状态的错误标记。例如,假如没有任何错误,则rdstate()会返回goodbit.  下例示例,表示出了rdstate()的用法:  #include<iostream>  usingnamespacestd;  intmain()  {  inta;  cin>>a;  cout<<cin.rdstate()<<endl;  if(cin.rdstate()==ios::goodbit)  {  cout<<"输入数据的类型正确,无错误!"<<endl;  }  if(cin.rdstate()==ios_base::failbit)  {  cout<<"输入数据类型错误,非致命错误,可清除输入缓冲区挽回!"<<endl;  }  system("pause");  }  另一种方法则是使用下面任何一个函数来检测相应的输入/输出状态:  boolbad();  booleof();  boolfail();  boolgood();  下例示例,表示出了上面各成员函数的用法:  #include<iostream>  usingnamespacestd;  intmain()  {  inta;  cin>>a;  cout<<cin.rdstate()<<endl;  if(cin.good())  {  cout<<"输入数据的类型正确,无错误!"<<endl;  }  if(cin.fail())  {  cout<<"输入数据类型错误,非致命错误,可清除输入缓冲区挽回!"<<endl;  }  system("pause");  }  如果错误发生,那么流状态既被标记为错误,你必须清除这些错误状态,以使你的程序能正确适当地继续运行。要清除错误状态,需使用clear()函数。此函数带一个参数,它是你将要设为当前状态的标志值。,只要将ios::goodbit作为实参。  示例代码如下:  #include<iostream>  usingnamespacestd;  intmain()  {  inta;  cin>>a;  cout<<cin.rdstate()<<endl;  cin.clear(ios::goodbit);  cout<<cin.rdstate()<<endl;  system("pause");  }  通常当我们发现输入有错又需要改正的时候,使用clear()更改标记为正确后,同时也需要使用get()成员函数清除输入缓冲区,以达到重复输入的目的。  示例代码如下:  #include<iostream>  usingnamespacestd;  intmain()  {  inta;  while(1)  {  cin>>a;  if(!cin)//条件可改写为cin.fail()  {  cout<<"输入有错!请重新输入"<<endl;  cin.clear();  cin.get();  }  else  {  cout<<a;  break;  }  }  system("pause");  }  最后再给出一个对文件流错误标记处理的例子,巩固学习,代码如下:  #include<iostream>  #include<fstream>  usingnamespacestd;  intmain()  {  ifstreammyfile("c:.txt",ios_base::in,0);  if(myfile.fail())  {  cout<<"文件读取失败或指定文件不存在!"<<endl;  }  else  {  charch;  while(myfile.get(ch))  {  cout<<ch;  }  if(myfile.eof())  {  cout<<"文件内容已经全部读完"<<endl;  }  while(myfile.get(ch))  {  cout<<ch;  }  }  system("pause");  }第6章控制结构6.1简介编写解决特定问题的程序之前,首先要彻底了解问题并认真计划解决问题的方法。编写程序时,还要了解可用的基本组件和采用实践证明的程序结构原则。本章将讨论结构化编程的理论和原理的所有问题。这里介绍的技术适用于大多数高级语言,包括C++。6.2算法任何计算问题都可以通过按特定顺序执行一系列操作而完成。解决问题的过程(procedure)称为算法(algorithm),包括:1.执行的操作(action)2.执行操作的顺序(order)下例演示正确指定执行操作的顺序是多么重要:考虑每个人早晨起床到上班的“朝阳算法”:(1)起床,(2)脱睡衣,(3)洗澡,(4)穿衣,(5)吃早饭,(6)搭车上班。总裁可以按这个顺序,从容不迫地来到办公室。假设把顺序稍作调换:(1)起床,(2)脱睡衣,(3)穿衣,(4)洗澡,(5)吃早饭.(6)搭车上班。如果这样,总裁就得带着肥皂水来上班。指定计算机程序执行语句的顺序称为程序控制(programcontrol),本章介绍C++程序的控制功能。6.3控制结构通常,程序中的语句按编写的顺序一条一条地执行,称为顺序执行(sequentialexecution)。程序员可以用稍后要介绍的不同C++语句指定下一个执行的语句不是紧邻其后的语句,这种技术称为控制转移(transferofcontrol)。20世纪60年代,人们发现,软件开发小组遇到的许多困难都是由于控制转移造成的。goto语句使程序员可以在程序中任意指定控制转移目标,因此人们提出结构化编程就是为了清除goto语句。Bohm和JMoP5n1的研究表明,不用goto语句也能编写程序。困难在于程序员要养成不用goto语句的习惯。直到20世纪70年代,程序员才开始认真考虑结构化编程,结果使软件开发小组的开发时间缩短、系统能够及时交付运行并在颅算之内完成软件项目。这些成功的关键是.结构化编程更清晰、更易调试与修改并且不容易出错。BohM和J·jecopini的研究表明,所有程序都可以只用三种控制结构(controlstructure)即顺序结构(sequencestructure)、选择结构(selectionstructure)和重复结构(repetitionstructure)。顺序结构是C++内置的,除非另外指定,计算机总是按编写的顺序一条一条地执行。图2.1的流程图(flowchart)演示了典型的顺序结构.按顺序进行两次计算。流程图是算法或部分算法的图形表示。流程图用一些专用符号绘制,如长方形、菱形、椭圆和小圆,这些符号用箭头连接,称为流程。C++提供三种选择结构,本章将介绍这三种选择结构。if选择结构在条件为true时执行一个操作,在条件为false时跳过这个操作。if/else选择结构在条件为true时执行一个操作,在条件为false时执行另一个操作。swutch选择结构根据表达式取值不同而选择不同操作。if选择结构称为单项选择结构(single—selection,structure),选择或忽略一个操作。if/else选择结构称为双项选择结构(double-selectionstructure),在两个不同操作中选择。switch选择结构称为多项选择结构(multiple-selectionstructure),在多个不同操作中选择。C++提供三种重复结构while、do/while和for。if、else、switch、while、do和for等都是C++关键字(keyword)。这些关键字是该语言保留的,用于实现如C++控制结构等不同特性。C++只有七种控制结构:顺序结构、三种选择结构和三种重复结构。每个C++程序都是根据程序所需的算法组合这七种控制结构。这种单入/单出控制结构(single-entry/single-exitcontrolstructure)使程序容易建立,只要将一个控制结构的出口与另一个控制结构的入口连接,即可组成程序。这点很像小孩子堆积木,因此称为控制结构堆栈(control-structurestacking),还有另一种控制结构连接方法,称为控制结构嵌套(control-structurenesing)。6.4if选择结构选择结构在不同操作之间选择。例如,假设考试成绩60分算及格,则下列伪代码:ifstudent'sgradeisgreaterthanorequalto60print"Passed"确定“学生成绩大于或等于60分”是true或false,如果是true,则该生及格,打印“Passed”字样,并顺序“执行”下一个伪代码语句(记住,伪代码不是真正的编程语言)。如果条件为false,则忽略打印语句,并顺序“执行”下一个伪代码语句。注意这个选择结构第二行的缩排,这种缩排是可选的,但值得提倡,因为它能体现结构化程序的内部结构。将伪代码变成C++代码时,C++编译器忽略空格、制表符、换行符等用于缩排和垂直分隔的空白字符。上述伪代码的if语句可以写成如下C++语句:if(grade>=60)cout<<"Passed";cout<<"Passed";注意C++代码与伪代码密切对应,这是伪代码的一个属性,使得其成为有用的程序开发工具。注:伪代码常用于程存设计期间“构思”程序,然后再将伪代码程序转换为C++程序。if结构也是单入/单出结构。6.5if/else选择结构if选择结构只在条件为true时采取操作,条件为false时则忽略这个操作。利用if/else选择结构则可以在条件为true时和条件为false时采取不同操作。例如,下列伪代码:ifstudent'sgradeisgreaterthanorequalto60print"Passed"elseprint"Failed"在学生成绩大于或等于60时打印“Passed”,否则打印"Failed"。打印之后,都“执行”下一条伪代码语句。注意else的语句体也缩排。上述伪代码if/else结构可以写成如下的C++代码:if(grade>=60)cout<<"Passed";elsecout<<"Failed";C++提供条件运算符(?:),与if/else结构密切相关。条件运算符是C++中惟一的三元运算符(thrnaryoperator),即取三个操作数的运算符。操作数和条件运算符一起形成条件表达式(conditionalexpression)。第一个操作数是条件,第二个操作数是条件为true时整个条件表达式的值.第三个操作数是条件为false时整个条件表达式的值。例如,下列输出语句:cout<<(grade>=60?"Passed":"Failed");包含的条件表达式在grade=60取值为true时,求值为字符串“Passed”;在grade>=60取值为false时,求值为字符串"Failed"。这样,带条件表达式的语句实际上与上述if/else语句相同。可以看出,条件运算符的优先级较低,因此上述表达式中的话号是必需的。条件表达式酌值也可以是要执行的操作。例如.下列条件表达式:grade>=60?cout<<"Passed":cout<<"Failed";表示如果grade大于或等于60,则执行cout<<"Passed",否则执行cout<<"Failed"。这与前面的if/else结构也是相似的。条件运算符可以在一些无法使用if/else语句的情况中使用。嵌套if/else结构(nestedif/elsestructure)测试多个选择,将一个if/else选择放在另一个if/else选择中。例如,下列伪代码语句在考试成绩大于或等于90分时打印A.在80到89分之间时打印B,在70到79分之间时打印C,在60到69分之间时打印D,否则打印F。ifstuden'sgradeisgreaterthanorequalto90print"A"elseIfstudent'sgradeisgreaterthanorequalto80print"B"elseIfstudent'sgradeisgreaterthanorequalto70print"C"elseIfstudent'sgradeisgreaterthanorequalto60print"D"elseprint"F"这个伪代码对应下列C++代码:if(grade>=90)cout<<"A";elseif(grade>=80)cout<<"B";elseif(grade>=70)cout<<"C";elseif(grade>=60)cout<<"D";elsecout<<"F";如果考试成绩大于或等于90分,则前4个条件都为true,但只执行第一个测试之后的cout语句。执行这个cout语句之后,跳过外层if/else语句的else部分。许多C++程序员喜欢将上述if结构写成:if(grade>=90)cout<<"A";elseif(grade>=80)cout<<"B";elseif(grade>=70)cout<<"C";elseif(grade>=60)cout<<"D";elsecout<<"F";两者形式是等价的。后者更常用,可以避免深层缩排便代码移到右端。深层缩排会使一行的空间太小,不长的行也要断行,从而影响可读性。下例在if/else结构的else部分包括复合语句:if(grade>=60)cout<<"Passed.\n";else{cout<<"Failed.\n";cout<<"Youmusttakethiscourseagain.\n";}如果grade小于60,则程序执行else程序体中的两条语句并打印:Failed.Youmusttakethiscourseagain.注意else从句中的两条语句放在花括号中。这些花括号很重要,如果没有这些花括号,则下列语句:cout<<"Youmusttakethiscoursagain.\n";在if语句else部分之外,不管成绩是否小于60都执行。本节介绍了复合语句的符号。复合语句可以包含声明(例如,和main程序体中一样),如果这,则这个复合语句称为块(block)。块中的声明通常放在块中任何操作语句之前,但也可以和操作语句相混和。6.6while重复结构重复结构(repetitionstrucure)使程序员可以指定一定条件下可以重复的操作。下列伪代码语句:WhiletherearemoreitemsonmyshoppinglistPurchasenextitemandcrossitoffmylist描述购物过程中发生的重复。条件"therearemoreltemsonmyshoppinglist"(购物清单中还有更多项目)可真可假。如果条件为true.则执行操作"Purchasenextitemandcrossitoffmylist"(购买下一个项目并将其从清单中划去)。如果条件仍然为true,则这个操作重复执行。while重复结构中的语句构成while的结构体,该结构体可以是单句或复合句。最终,条件会变为false(购买清单中最后一个项目并将其从清单中划去时),这时重复终止,执行重复结构之后的第一条伪代码语句。作为实际while的例子,假设程序要寻找2的第一个大于1000的指数值。假设整数变量product初始化为2,执行下列while重复结构之后,product即会包含所要值:intproduct=2;while(product<=1000)product=2*product;进入while结构时,product的值为2。变量product重复乘以2,连续取值4、8、16、32、64、128、256、512和1024。当product变为1024时,while结构条件product<=1000变为false,因此终止重复,product的最后值为1024。程序继续执行while后面的下一条语句。6.7构造算法:实例研究1(计数器控制重复)要演示如何设计算法,我们要解决几个全班平均成绩的问题。考虑下列问题:班里有10个学生参加测验,可以提供考试成绩(0到100的整数值),以确定全班平均成绩。全班平均成绩等于全班成绩总和除以班里人数。计算机上解决这个问题的算法是辅人每人的成绩,进行平均计算,然后打印结果。下面用伪代码列出要执行的操作,指定这些操作执行的顺序。我们用计数器控制重复(counter-conttrolledrepetition)一次一个地输人每人的成绩。这种方法用计数器(counter)变量控制一组语句执行的次数。本例中,计数器超过10时,停止重复。本节介绍伪代码算法(如图6.6)和对应程序(如图6.7)。下节介绍如何开发这个伪代码算法。计数器控制重复通常称为确定重复(definiterepetition),因为循环执行之前,已知重复次数。注意算法中引用了总数(total)和计数器。总数变量用于累计一系列数值的和。计数器变量用于计数,这里计算输人的成绩数。存放总数的变量通常应先初始化为0之后再在程序中使用,否则总和会包括总数的内存地址中存放的原有数值。SettotaltozeroSetgradecountertooneWhilegradecounterislessthanorequaltotenInputthenextgradeAddthegradei.tothetotalAddonetothegradecounterSettheclassaveragetothetotaldividedbytenPrinttheclassaverage图6.6用计数器控制重复解决全班平均成绩问题的伪代码算法1//Fig.2.7:fig0207.cpp2//Classaverageprogramwithcounter-controlledrepetition3#include<iostream.h>45intmain()6{7inttotal,//sumofgrades8gradeCounter,//numberofgradesentered9grade,//onegrade10average;//averageofgrades1112//initializationphase13total=0;//cleartotal14gradeCounter=1;//preparetoloop1516//processingphase17while(gradeCounter<=10){//loop10times18cout<<"Entergrade:";//promptforinput19cin>>grade;//inputgrade20total=total+grade;//addgradetototal21gradeCounter=gradeCounter+1;//incrementcounter22}2324//terminationphase25average-total/10;//integerdivision26cout<<"Classaverageis"<<average<<endl;2728return0;//indicateprogramendedsuccessfully29}输出结果:Entergrade:98Entergrade:76Entergrade:71Entergrade:87Entergrade:83Entergrade:90Entergrade:57Entergrade:79Entergrade:82Entergrade:94Classaverageis81图6.7用计数器控制重复解决全班平均成绩问题的C++程序和示例输出根据使用情况,计数器变量通常应先初始化为0或1(下面会分别举例说明)。未初始化变量会包含垃圾值“garbage”value),也称为未定义值(undefinedvalue),为该变量保存内存地址中最后存放的值。切记:一定要初始化计数器和总和变量。每个变量在单独一行中声明。注意程序中的平均计算产生一个整数结果。实际上,本例中的成绩总和是81.7,除以10时应得到81.7,是个带小数点的数,下节将介绍如何处理这种值(称为浮点数)。图6.7中,如果第21行用gradeCounter而不是10进行计算,则这个程序的输出显示数值74。6.8构造算法与自上而下逐步完善:实例研究2(标记控制重复)下面将全班平均成绩问题一般化,考虑如下问题:设计一个计算全班平均成绩的程序,在每次程序运行时处理任意个成绩数。在第一个全班平均成绩例子中,成绩个数(10)是事先预置的。而本例中,则不知道要输入多少个成绩,程序要处理任意个成绩数。程序怎么确定何时停止输入成绩呢?何时计算和打印全班平均成绩呢?一种办法是用一个特殊值作为标记值(sentinelvalue),也称信号值(signalvalue)、哑值(dummyvalue)或标志值(flagvalue),表示数据输入结束(“endofdataentry”)用户输入成绩,直到输入所有合法成绩。然后用户输入一个标记值,表示最后一个成绩已经输入。标记控制重复(sentinel-controlledrepetition)也称为不确定重复(indefiniterepetition),因为执行循环之前无法事先知道重复次数。显然,标记值不能与可接受的输入值混淆起来。由于考试成绩通常是非负整数,因此可以用-1作标记值。这样,全班平均成绩程序可以处理95、96、75、74、89和-l之类的输人流。程序计算并打印成绩95、96、75、74和89的全班平均成绩(不计入-1,因为它是标记值)。我们用自上而下逐步完善(top-down,stepwiserefinement)的方法开发计算全班平均成绩的程序,这是开发结构化程序的重要方法。我们首先生成上层伪代码表示:Determinetheclassavcraqeforthequiz上层伪代码只是一个语句,表示程序的总体功能。这样.上层等于是程序的完整表达式。但上层通常无法提供编写C++程序所需的足够细节。因此要开始完善过程。我们将上层伪代码分解为一系列的小任务,按其需要完成的顺序列出。这个结果就是下列第一步完善(first,refinement):InitializevariablesInput,sum,andcountthequizgradesCalculateandprinttheclassaverage这里只用了顺序结构,所有步骤按顺序逐步执行。要进行下一步完善(即第二步完善,secondrefinement),我们要指定特定变量,要取得数字的动态和以及计算机处理的数值个数,用一个变量接收每个输入的成绩值,一个变量保存计算平均值。下列伪代码语句:Initializevariables可以细化成:InitializetotaltozeroInitializecountertozero注意,只有total和counter变量要先初始化再使用,average和grade变量(分别计算平均值和用户输入)不需要初始化.因为它们的值会在计算或输入时重定义。下列伪代码语句:Input,sum,andcountthequizgrades需要用重复结构(即循环)连续输入每个成绩。由于我们事先不知道要处理多少个成绩,因此使用标记控制重复。用户一次一项地输入合法成绩。输入最后一个合法成绩后,用户输人标记值。程序在每个成绩输入之后测试其是否为标记值.如果用户输入标记值,则顺序循环终止。上述伪代码语句的第二步完善如下:Inputthefirstgrade(possiblythesentinel)WhiletheuserhasnotasyetenteredthesentinelAddthisgradeintotherunningtotalAddonetothegradecounterInputthenextgrade(possiblythesentinel)注意,在这个伪代码中,我们没有在while结构体中使用花括号,只是在while下面将这些语句缩排表示它们属于while。伪代码只是非正式的程序开发辅助工具。下列伪代码语句可以完善如下:IfthecounterisnotequaltozeroSettheaveragetothetotaldividedbythecounterPrinttheaverageelsePrint"Nogradeswereentered"注意我们这里要测试除数为0的可能性,这是个致命逻辑错误,如果没有发现,则会使程序失败(通常称为爆炸或崩溃)。以上显示了全班平均成绩问题第二步完善的完整伪代码语句。伪代码中增加了一些空行,使伪代码更易读。空行将程序分成不同阶段。图6.8所示的伪代码算法解决更一般的全班平均成绩问题,这个算法只进行了第二步完善,还需要进一步完善。InitializetotaltozeroInitializecountertozeroInputthefirstgrade(possiblythesentinel)WhiletheuserhasnotasyetenteredthesentinelAddthisgradeintotherunningtotalAddonetothegradecounterInputthenextgrade(possiblythesentinel)ifthecounterisnotrqualtozeroSettheaveragetothetotaldividedbythecounterPrinttheaverageelsePrint"Nogradeswereentered"图6.8用标记符控制重复解决全班平均成绩问题的伪代码算法图6.9显示了C++程序和示例执行结果。尽管只输入整数成绩,但结果仍然可能产生带小数点的平均成绩,即实数。int类型无法表示实数,程序中引入float数据类型处理带小数点的数(也称为浮点数,floatingpointnumber),并引入特殊的强制类型转换运算符(castoperator)处理平均值计算。这些特性将在程序之后详细介绍。1//Fig.2.9:fig02_09.cpp2//Classaverageprogramwithsentinel-controlledrepetition.3#include<iostream.h>4#include<iomanip.h>56intmain()7{8inttotal,//sumofgrades9gradeCounter,//numberofgradesentered10grade;//onegrade11floataverage;//numberwithdecimalpointforaverage1213//initializationphase14total=0;15gradeCounter=0;1617//processingphase18cout<<"Entergrade,-1toend:";19cin>>grade;2O21while(grade!=-1){22total=total+grade;23gradeCounter=gradeCounter+1;24cout<<"Entergrade,-1toend:";25cin>>grade;26}2728//terminationphase29if(gradeCounter!=0}{30average-static_cast<float>(total)/gradeCounter;31cout<<"Classaverageis"<<setprecision{2)32<<setiosflags(ios::fixed|ios::showpoint)33<<average<<endl;34}35else36cout<<"NOgradeswereentered"<<endl;3738return0;//indicateprogramendedsuccessfully39}输出结果:Entergrade,-1toend:75Entergrade,-1toend:94Entergrade,-1toend:97Entergrade,-1toend:88Entergrade,-1toend:70Entergrade,-1toend:64Entergrade,-1toend:83Entergrade,-1toend:89Entergrade,-1toend:-1Classaverageis82.50图6.9用标记符控制重复解决全班平均成绩问题的C++程序和示例执行结果注意图6.9中while循环中的复合语句。如果没有花括号,则循环体中的最后三条语句会放到循环以外,使计算机错误地理解如下代码:while(grade!=-1)total-total+grade;gradeCounter=gradeCounter+1;cout<<"Entergrade,-1toend:";cin>>grade;如果用户输入的第一个成绩不是-l,则会造成无限循环。注意下列语句:cin>>grade;前面用一个输出语句提示用户输入。平均值并不一定总是整数值,而常常是包含小数的值,如7.2或-93.5。这些值称为浮点数,用数据类型float表示。变量average声明为数据类型float,以获得计算机结果中的小数。但total/gradeCounter的计算结果是整数,因为total和gradeCounter都是整数变量。两个整数相除是整除(integerdivision),小数部分丢失(即截尾,truncated)。由于先要进行计算,因此小数部分在将结果赋给average之前已经丢失。要用整数值进行浮点数计算,就要先生成用于计算的临时浮点数值。C++提供了一元强制类型转换运算符(unarycastoperator)。下列语句:average=staticcast<float>(total)/gradeCounter;包括一元强制类型转换运算符static_cast<float>(),生成用于计算的临时浮点数值(total)。这样使用强制类型转换运算符称为显式类型转换(explicitconversion)。total中存放的值还是整数,而计算时则用浮点数值(total的临时float版本)除以整数gradcCounter。c++编译器只能对操作数的数据类型一致的表达式求值。要保证操作数的数据类型一致,编译器对所选择的操作数进行提升(promotion)操作(也称为隐式类型转换,implicitconversion)。例如,在包含数据类型float和int的表达式中,int操作数提升为float。本例中,gradeCounter提升为float之后进行计算,将浮点数除法得到的结果赋给average。本章稍后将介绍所有标准数据类型及其提升顺序。任何数据类型都可用强制类型转换运算符,static_cast运算符由关键字statlccast加尖括号(<>)中的数据类型名组成。强制类型转换运算符是个一元运算符(unaryperator),即只有一个操作数的运算符。第1章曾介绍过二元算术运算符。C++也支持一元正(+)、负(-)运算符,程序员可以编写-7、+5之类的表达式。强制类型转换运算符从右向左结合,其优先级高于正(+)、负(-)运算符等其他一元运算符,该优先级高于运算符*、/和%,但低于括号的优先级。优先级表中用static_cast<type>()表示强制类型转换运算符。图6.9中格式化功能将在第2章详细介绍,这里先做一简要介绍。下列输出语句中调用setpreclslon(2):cout<<"Classaverageis"<<setprecision(2)<<Setiosflaqs(iOS::fixed|iOS::showpoint)<<averaqe<<endl;表示float变量average打印小数点右边的位数为两位精度(precision),例如92.37,这称为参数化流操纵算子(parameterizedstreammanipulator)。使用这些调用的程序要包含下列预处理指令:#include<iomanip.h>注意endl是非参数化流操纵算子(nonparameterizedstreammanipulator),不需要iomanip.h头文件。如果不指定精度,则浮点数值通常输出六位精度(即默认精度,defaultprecision),但稍后也会介绍一个例外。上述语句中的流操纵算子setiosflags(ios::fixed|ios::showpoInt)设置两个输出格式选项ios::fixed和ios::showpoint。垂直条(1)分隔setiosflags调用中的多个选项(垂直条将在第16章详细介绍)。选项ios::fixed使浮点数值以浮点格式(而不是科学计数法,见第2章)输出。即使数值为整数,ios::showpoInt选项也会强制打印小数点和尾部O,如88.OO。如果不用ios::showpoint选项,则C++将该整数显示为88,不打印小数点和尾部0。程序中使用上述格式时,将打印的值取整,表示小数点位数,但内存中的值保持不变。例如,数值87.945和67.543分别输出为87.95和67.54。尽管浮点数算不总是100%精确,但其用途很广。例如,我们说正常体温98.6(华氏温度)时,并不需要精确地表示,如果温度计上显示98.6度.实际上可能是98.5999473210643度。这里显示98.6对大多数应用已经足够了。另一种得到浮点数的方法是通过除法。10除以3得到3.333333……,是无限循环小敷。计算机只分配固定空间保存这种值,因此只能保存浮点值的近似值。6.9构造算法与自上而下逐步完善:实例研究3(嵌套控制结构)下面介绍另一个问题。这里还是用伪代码和自上而下逐步完善的方法构造算法,然后编写相应的C++程序。我们介绍过按顺序堆叠的控制结构,就像小孩堆积木一样。这里显示C++中控制结构的另一种方法,称为嵌套控制结构。考虑下列问题:学校开了一门课,让学生参加房地产经纪人证书考试。去年,几个学生读完这门课并参加了证书考试。学校想知道学生考试情况,请编写一个程序来总结这个结果。已经得到了10个学生的名单,每个姓名后面写1时表示考试通过,写2时表示没有通过。程序应分析考试结果,如下所示:1.输入每个考试成绩(即l或2),每次程序请求另一个考试成绩时,在屏幕上显示消息“Enterresult"。2.计算每种类型的考试成绩数。3.显示总成绩,表示及格人数和不及格人数。6.如果超过8个学生及格,则打印消息“Raisetuition”。认真分析上述问题后,我们做出下列结论:1.程序要处理10个考试成绩,用计数器控制循环。2.每个考试成绩为数字l或2,每次程序读取考试成绩时,程序要确定成绩是否为数字1或2。我们的算法中测试1,如果不是l,则我们假设其为2(本章末尾的练习会考虑这个假设的结果)。3.使用两个计数器,分别计算及格人数和不及格人数。4.程序处理所有结果之后,要确定是否有超过8个学生及格。下面进行自上而下逐步完善的过程。首先是上层的伪代码表示:Analyzeexamresultsanddecideiftuitionshouldberaised我们再次强调,顶层是程序的完整表达,但通常要先进行几次完善之后才能将伪代码自然演变成C++程序。我们的第一步完善为:InitializevariableslnputthetenquizqradesandCOU~tpassesandfailuresPrintasugaryOfthecxamresultsanddecideiftuitionshouldberaised这里虽然有整个程序的完整表达式,但还需要进一步完善。我们要提供特定变量。要用两个计数器分别计算,用一个计数器控制循环过程,用一个变量保存用户输入。伪代码语句:Initializevariables可以细分如下:InitializepassestozerolnitializefailulestozeroInltiallzestudentcountertoOne注意.这里只初始化计数器和总和。伪代码语句:InputthetenquizgradesandcountPassesandfaiLures要求循环输入每个考试成绩。我们事先知道共有10个成绩,因此可以用计数器控制循环。在循环中(即嵌套在循环中),用一个双项选择结构确定考试成绩为数字1或2,并递增相应的计数器。上述伪代码语句细化如下:whilestudentcounterislessthanorequaltotenInputthenextexamresultifthestudentpassedAddonetoPasseselseAddOnetofailuresAddonetostudentcounter注意这里用空行分开if/else控制结构,以提高程序可读性。伪代码语句:PrintasugaryOftheexamresultsanddecldeiftuitionshouldberaised可以细化如下:PrintthenumberofpassesPrintthenumberoffiluiesifmorethaneightstudentsPassedPriht"Raisetuition"图6.10显示了完整的第2步完善结果。注意这里用空行分开while结构,以提高程序可读性。InitlalizepassestozeroInit±a1izefailurestozerolnitlallzestudentcountertoonewhilestudentcounterislessthanorequaltotenInputthenextexamresultifthestudentPassedAddonetopasseselseAddonetofailuresAddonetostudentcounterPrihtthenumberofpassesPrirtthenumberoffiluresifmorethaneightstudentspassedPrint”Raisetuition'’图6.10检查考试成绩的伪代码这个伪代码语句已经足以转换为C++程序。图6.11显示了C++程序及示例的执行结果。注意,我们利用C++的一个特性,可以在声明中进行变量初始化。循环程序可能在每次循环开头要求初始化,这种初始化通常在赋值语句中进行。1//Fig.2.11:fig02_ll.cpp3#include<iostream.h>45intmain()6{//initializevariablesindeclarationsintpasses=0,//numberofpassesPasses=v;//numberorpassesfailures=0,//numberoffailuresstudentCounter=1,//studentcounterresult;//oueexamresult//process10students;counter-controlledloopwhile(studentCounter<=10){cout<<"Enterresult(1=pass,2=fail):";cin>>result;if(result==1)//if/elsenestedinwhilepasses=passes+1;elsefailures=failures+1;studentcounter=studentCounter+1;}//terminationphasecout<<"Failed"<<failures<<endl;if(passes>8)cout<<"Raisetuition"<<endl;return0;//successfultermination输出结果:Enterresult(l=pass,2=fail}:1Enterresult(l=pass,2=fail):2Enterresult(l=pass,2=fail):1Enterresult(l=pass,2=fail):1Enterresult(l=pass,2=fail):1Enterresult(l=pass,2=fail)1Enterresult(l=pass,2=fail):2Enterresult{l=pass,2=fail):1Enterresult(l=pass,2=fail):1Enterresult)1=pass,2=fail):2Passed6Failed4Enterresult(a=pass,2=Fail):1Enterresult(l=pass,2=fail):1Enterresult(l=pass,2=fail)1Enterresult(1-pass,2=fail):2Enterresult{l=pass,2=fail):1Enterresult(l=pass,2=fail):1Enterresult{1=Pass,2=fail):1Enterresult(1=pass,2=fail):1Enterresult(1=pass,2=fail):1Enterresult(1=pass,2=fail):1Passed9Failed1Raisetuition图6.11检查考试成绩的C++程序及示例执行结果6.10赋值运算符C++提供了几个赋值运算符可以缩写赋值表达式。例如下列语句:c=c+3;可以用加法赋值运算符(additionassignmentoperator)“+=”缩写如下:c+=3;+=运算符将运算符右边表达式的值与运算符左边表达式的值相加,并将结果存放在运算符左边表达式的值中。下列形式的语句:variable=variableoperatorexpression;其中operator为二元运算符+、-、/或%之一(或今后要介绍的其他二元运算符),均可写成如下形式:variableoperator=exprission;这样,赋值语句c+=3将3与c相加。图2.12显示了算术赋值运算符、使用这些算术赋值运算符的示例表达式和说明。赋值运算符示例表达式说明赋值假设intc=3,d=5,e=4,f=6,g=12;+=e+=7c=c+710赋值给e-=d-=4d=d-41赋值d*=e*=5e=e*520赋值给e/=f/=3f=f/32赋值给f%=g%=9g=g%93赋值给g图6.12算术赋值运算符6.11自增和自减运算符C++还提供一元自增运算符(incrementoperator,++)和一元自减运算符(dcrementoperator),见图6.13。如果变量c递增1,则可以用自增运算符++,而不用表达式c=c+1或c+=1。如果将自增和自减运算符放在变量前面,则称为前置自增或前置递减运算符(preincrement或predecrementope~torL如果将自增和自减运算符放在变量后面,则称为后置自增或后置自减运算撤postmcrement或postdcrementoperator)。前置自增(前置自减)运算符使变量加1(减1),然后在表达式中用变量的新值。后置自增(后置自减)运算符在表达式中用变量的当前值,然后再将变量加1(减1)。运算符名称示例表达式说明++前置自增++a将a加1,然后在a出现的表达式中使用新值++后置自增a++在a出现的表达式中使用当前值,然后将a加1--前置自减--b将b减1,然后在b出现的表达式中使用新值--后置自减b--在b出现的表达式中使用当前值,然后将b减1图6.13自增和自减运算符图6.14的程序演示了++运算符的前置自增与后置自增计算之间的差别,后置自增变量c使其在输出语句中使用之后再递增,而前置自增变量c使其在输出语句中使用之前递增。1//Fig,2.14:fig0214.cpp2//Preincrementingandpostincrementing3#include<iostream.h>45intmain()6{7intc;89C=5;10cout<<C<<endl;//print511cout<<C++<<endl;//print5thenpostincrement12cout<<c<<endl<<endl;//print1314c=5;15cou<<c<<endl;//print516cout<<++c<<endl;//preincrementthenprint617cout<<c<<endl;1819return0;//successfultermination20}输出结果:556566图6.14前置自增与后置自增计算之间的差别程序显示使用++运算符前后的c值,自减运算符的用法类似。图6.11的三个赋值语句:passes=passes+1;failures=failures+1student=student+1;可以改写成更简练的赋值运算符形式:passes+=1;failures+=1;student+=1;使用前置自增运算符,如下所示:++passes;++failures;++student;或使用后置自增运算符,如下所示:passes++failures++student++注意,单独一条语句中自增或自减变量时,前置自增与后置自增计算之间的的结果一样,前置自减与后置自减计算之间的结果也相同。只有变量出现在大表达式中时,才能体现前置自增与后置自增计算之间的差别(和前置自减与后置自减计算之间的差别)。目前只用简单变量名作为自增和自减的操作数(稍后会介绍,这些运算符也可以用于左值)。图6.15显示了前面所介绍的运算符优先级和结合律,从上到下,优先级依次递减。第二栏介绍每一级运算符的结合律,注意条件运算符(?:)、一元运算符自增(++)、自减(--)、正(+)、负(-)、强制类型转换以及赋值运算符(=、+=、-;、*=、/=和%=)的结合律为从右向左。图1.15中所有其他运算符的结合律为从左向右。第三栏是运算符的组名。运算符结合律类型()括号++--+-static_cast<type>()从左向右一元*/%从右向左乘+-从左向右加<<>>从左向右插入/读取<<=>>=从左向右关系==!=从左向右相等?:从右向左条件=+=-=*=/=%=从右向左赋值,从左向右逗号图6.15前面所介绍的运算符优先级和结合律6.12计数器控制循环的要点计数器控制循环要求:l.控制变量(或循环计数器)的名称(name)。2.控制变量的初始值(initialvalue)。3.测试控制变量终值(finalvalue)的条件(即是否继续循环)。4.每次循环时控制变量修改的增量或减量(incrementdecrement)。考虑图6.16所示的简单程序,打印1到10的数字。声明:intcounter=1;指定控制变量(counter)并声明为整数,在内存中为其保留空间并将初始值设置为1。需要初始化的声明实际上是可执行语句。在C++中,将需要分配内存的声明称为定义(definition)更准确。1//Fig.2.16:fig02_16.cpp2//Counter-controlledrepetition3#include<iostream.h>45intmain()6{7intcounter=1;//initialization69while(counter<=10){//repetitioncondition10cout<<counter<<endl;11++counter;//increment12}1315}输出结果:24508910图6.16计数器控制循环counter的声明和初始化也可以用下列语句完成:intcounter;counter=1;声明不是可执行语句,但赋值是可执行语句。我们用两种方法将变量初始化。下列语句:++counter;在每次循环时将循环计数器的值加1。while结构中的循环条件测试控制变量的值是否小于或等于10(条件为true的终值)。注意,即使控制变量是10时,这个while结构体仍然执行。控制变量超过10时(即counter变成11时),循环终止。图6.16的程序也可以更加简化,将counter初始化为。并将while结构换成:while(++counter<=lO)cout<<counter<<endl;这段代码减少了语句,直接在while条件中先增加计数器的值再测试条件。这段代码还消除了while结构体的花括号,因为这时while只包含一条语句。6.13for重复结构for重复结构处理计数器控制循环的所有细节。要演示for的功能,可以改写图6.16的程序,结果如图6.17。执行for重复结构时,声明控制变量counter并将其初始化为1。然后检查循环条件counter<=10。由于counter的初始值为1,因此条件满足,打印Counter的值(1)。然后在表达式Counter++中递增控制变量counter,再次进行循环和测试循环条件。由于这时控制变量等于2,没有超过最后值,因此程序再次执行语句体。这个过程一直继续,直到控制变量counter递增到11,使循环条件的测试失败,重复终止。程序继续执行for结构后面的第一条语句(这里是程序末尾的return浯句)。1//Fig.2.17:fig0217.cpp2//Counter-controlledrepetitionwiththeforstructure3#include<iostream.h>45intmain()6{7//Initialization,repetitioncondition,andincrementing8//areallincludedintheforstructureheader.910for(ihtcounter=1;counter<=10;counter++)11cout<<counter<<endl;1213returnO;14}图6.17用for结构的计数器控制重复图6.18更进一步研究了图6.17中的for结构。注意for结构指定计数器控制重复所需的每个项目。如果for结构体中有多条语句,则应把语句体放在花括号中。注意图6.17用循环条件counter<=10。如果循环条件变为counter<lO,则循环只执行9次,这种常见的逻辑错误称为差1错误。for关键字控制变量名控制变量终值↓↓↓for(intcounter=1;counter<=10;counter++)-------------↑↑↑控制变量初始化循环条件控制变量递增图6.18典型for首部的组件for结构的一般格式如下:for(expression1;expression2;expression3)statement其中expression1初始化循环控制变量的值,expression2是循环条件,expression3递增控制变量。大多数情况下,for结构可以表示为等价的while结构:expression1while(expression2){statementexpression3;}如果for结构首部中的expression1(初始化部分)定义控制变量(即控制变量类型在变量名前面指定).则该控制变量只能在for结构体中使用,即控制变量值是for结构之外所未知的。这种限制控制变量名的用法称为变量的作用域(scope)。变量的作用域定义其在程序中的使用范围。for结构中的三个表达式是可选的。如果省略expression2,则C++假设循环条件为真,从而生成无限循环。如果程序其他地方初始化控制变量,则可以省略expression1。如果for语句体中的语句计算增量或不需要增量,则可以省略expression3。for结构中的增量表达式就像是for语句体末尾的独立语句。因此,下列表达式:counter=counter+1;counter+=1;++counter;counter++;在for结构的递增部分都是等价的。许多程序员喜欢counter++,因为递增在执行循环体之后发生,因此,后置自增形式似乎更自然。由于这里递增的变量没有出现在表达式中,因此前置自增与后置自增的效果相同。for结构首部中的两个分号是必需的。for结构的初始化、循环条件和递增部分可以用算术表达式。例如,假设x=2和y=10,如果x和y的值在循环体中不被修改,则下列语句:for(intj=x;j<=4*x*y;j+=y/x)等于下列语句:for(intj=2;j<=80;j+=5)for结构的增量也可能是负数(实际上是递减,循环向下计数)。如果循环条件最初为false,则for结构体不执行,执行for后面的语句。for结构中经常打印控制变量或用控制变量进行计算,控制变量常用于控制重复而不在for结构体中提及这些控制变量。for结构的流程图与while结构相似。例如,图6.19显示了下列for语句的流程图:for(intcounter=l;counter>=10;counter++)cout<<counter<<endl;从这个流程图可以看出初始化发生一次,井在每次执行结构体语句之后递增。注意,流程图(除了小圆框和流程之外)也只能包含表示操作的矩形框和表示判断的菱形框。这是我们强调的操作/判断编程模型。程序员的任务就是根据算法使用堆栈和嵌套两种方法组合其他几种控制结构,然后在这些框中填入算法所要的操作和判断,从而生成程序。6.14for结构使用举例下面的例子显示for结构中改变控制变量的方法。在每个例子中,我们都编写相应的for结构首部。注意循环中递减控制变量的关系运算符的改变。a)将控制变量从1变到100,增量为1。for(inti=l;i<=100;i++)b)将控制变量从100变到1,增量为-1。for(inti=100;i>=1;i--)c)控制变量的变化范围为7到77。for(inti=7;i<=77;i+=7)d)控制变量的变化范围为20到2。for(inti=20;i>=2;i-=2)c)按所示数列改变控制变量值:2、6、8、11、14、17、20。for(intj=2;j<=20;j+=3)f)按所示数列改变控制变量值:99、88、77、66、55、44、33、22、ll、0。for(intj=99;j>=0;j-=11)下面两个例子提供for结构的简单应用。图2.20所示的程序用for结构求2到100的所有整数的总和。注意图2.20中for结构体可以用下列逗号运算符合并成for首部的右边部分:for(intnumber=2;//initializationnumber<=100;//continuationconditionsum+=number,number+=2)//totalandincrement初始化sum=0也可以合并到for的初始化部分。1//Fig.2.20:fig02_20.cpp2//Summationwithfor3#include<iostream.h>45intmain()6{7intsum=0;89for(intnumber=2;number<=100;number+=2)10sum+=number;1112cout<<"Sumis"<<sum<<endl;1314return0;15}输出结果:sumis2550图6.20用for语句求和下列用for结构计算复利。考虑下列问题:一个人在银行存款1000.00美元,每利率为5%。假设所有利息留在账号中,则计算10年间每年年末的金额并打印出来。用下列公式求出金额:a=P(1+r)n其中:P是原存款(本金)r是年利率n是年数a是年未本息这个问题要用一个循环对10年的存款进行计算。解答如图6.21for结构执行循环体10次,将控制变量从1变到10,增量为1。C++中没有指数运算符,因此要用标准库函数pow。函数pow(x,y)计算x的y次方值。函数pow取两个类型为double的参数并返回double值。类型double与float相似,但double类型的变量能存放比float精度更大的数值。C++把常量(如图6.21中的1000.0和.05)当作double类型处理。1//Fig.2.21:fig02_21.cpp2//Calculatingcompoundinterest3#include<iostream.h>4#include<iomanip.h>5#include<math.h>67intmain()8{9doubleamount,//amountondeposit10principal=1000.0,//startingprincipal11rate=.05;//interestrate1213cout<<"Year"<<setw(21)14<<"Amountondeposit"<<endl;1516for(intyear=1;year<=10;year++){17amount=principal*pow{1.0+rate,year);10cout<<setw(4)<<year19<<setiosflags(ios::fixedIios::showpoint)20<<setw(21)<<setprecision(2)21<<amount<<endl;22}232425}输出结果:YearAmountondeposic11050.0021102.5031157.6241215.5151276.2861340.1071407.1081477.4691551.33101628.89图6.21用for结构计算复利这个程序必须包括math.h才能编译。函数pow要求两个double参数,注意year是个整数。math.h文件中的信息告诉编译器将year值转换为临时double类型之后再调用函数。这些信息放在pow的函数原型(functionprototype)中。第3章将介绍函数原型并总结pow函数和其他数学库函数。程序中将变量amount、principal和rate声明为double类型,这是为了简单起见,因为我们要涉及存款数额的小数部分,要采用数值中允许小数的类型。但是,这可能造成麻烦,下面简单介绍用float和double表示数值时可能出现的问题(假设打印时用setprecision(2)):机器中存放的两个float类型的值可能是14.234(打印14.23)和18.673(打印18.67)。这两个值相加时,内部和为32.907,打印32.91。结果输出如下:14.23+18.67-----------32.91但这个加式的和应为32.90。输出语句:cout<<setw(4)<<year<<setiosflags(10s::fixedlos::showpoint)<<setw(21)<<setpreclsion(2)<<amount<<endl;用参数化流操纵算于setw、setiosflags和setprecision指定的格式打印变量year和amount的值。调用selw(4)指定下一个值的输出域宽(fieldwidth)为4,即至少用4个字符位置打印这个值。如果输出的值宽度少于4个字符位,则该值默认在输出域中右对齐(ishljustl^ed),如果输出的值宽度大于4个字符,则该值将输出域宽扩大到能放下这个值为止。调用setiosflag(ios::left)可以指定输出的值为左对齐(1eftjustified)。上述输出中的其余格式表示变量amount打印成带小数点的定点值(用setiosflags(ios::fixed|ios::showpoint)指定),在输出域的21个字符位中右对齐(用setw(21)指定),小数点后面为两位(用setprecision(2)指定)。注意计算1.0+rate作为pow函数的参数,包含在for语句体中。事实上,这个计算产生的结果在每次循环时相同,因此是重复计算(是一种浪费)。6.15switch多项选择结构前面介绍了if单项选择结构和if/else双项选择结构。有时算法中包含一系列判断,用一个变量或表达式测试每个可能的常量值,并相应采取不同操作。C++提供的switch多项选择结构可以进行这种判断。switch结构包括一系列case标记和一个可选default情况。图6.22中的程序用switch计算学生考试的每一级人数。1//Fig.2.22:fig0222.cpp2//Countinglettergrades3#include<iostream.h>45intmain()6{7intgrade,//onegrade8aCount=0,//numberofA's9bCount=0,//numberofB's10cCount=O,//numberofC's11dCount=0,//numberofD's12fCount=0;//numberofF's1314cout<<"Enterthelettergrades."<<endl15<<"EntertheEOFcharactertoendinput."<<endl;1617while((grade=cin.get())!=EOF){1819switch(grade){//switchnestedinwhile2021case'A'://gradewasuppercasea22case'a'://orlowercasea23++aCount;24break;//necessarytoexitswitch2526case'B'://gradewasuppercaseB27case'b'://orlowercaseb28++bCount;29break;3031case'C'://gradewasuppercaseC32case'c'://orlowercasec33++cCount;34break;3536case'D'://gradewasuppercaseD37case'd'://orlowercased38++dCount;39break;4041case'F'://gradewasuppercaseF42case'f'://orlowercasef43++fCount;44break;4546case'\n'://ignorenewlines,47case'\t'://tabs,48case''://andspacesininput49break;5051default://catchallothercharacters52cout<<"Incorrectlettergradeentered."5354<<"Enteranewgrade."<<endl;55break;//optional56}57}58cout<<"\n\nTotalsforeachlettergradeare:"59"\nA:"<<aCount60<<"\nB:"<<bCount61<<"\nC:"<<cCount62<<"\nD:"<<dCount63<<"\nF:"<<fCount<<endl;6465return0;66}输出结果:Enterthelettergrades.EntertheEOFcharactertoendinput.ABCCADFCEIncorrectlettergradeentered.Enteranewgrade.DABTotalsforeachlettergradeare:A:3B:2C:3D:2F:1图6.22使用switch举例程序中,用户输入一个班代表成绩的字母。在while首部中:while((grade=cin.get())!=EOF)首先执行参数化赋值(grade=cin.get())。cin.get()函数从键盘读取一个字符,并将这个字符存放在整型变量grade中。cin.get()中使用的圆点符号将在第6章中介绍。字符通常存放在char类型的变量中,但是,C++的一个重要特性是可以用任何整数数据类型存放字符,因为它们在计算机中表示为1个字节的整数。这样,我们根据使用情况,可以把把字符当作整数或字符。例如,下列语句:cout<<"Thecharacter("<<'a'<<")hasthevalue"<<static_cast<int>('a')<<endl;打印字符a及其整数值如下所示:Thecharacter(a)hasthevalue97整数97是计算机中该字符的数字表示。如今许多计算机都使用ASCII(AmericanStandardCodeforlnformationInterehange,美国标准信息交换码)字符集(characterset),其中97表示小写字母“a”。附录中列出了ASCII字符及其十进制值的列表。赋值语句的整个值为等号左边变量指定的值。这样,赋值语句grade=cin.get()的值等于cin.get()返回的值,赋给变量grade。赋值语句可以用于同时初始化多个变量。例如:a=b=c=0;首先进行赋值c=o(因为:运算符从右向左结合),然后将c=0的值赋给变量b(为0),最后将b=(c=0)的值赋给变量a(也是0)。程序中,赋值语句grade=cin.get()的值与EOF值(表示文件结束的符号)比较。我们用EOF(通常取值为-1)作为标记值。用户输入一个系统识别的组合键,表示文件结束,没有更多要输入的数据。EOF是<iostream.h>头文件中定义的符号化整型常量。如果grade的取值为EOF,则程序终止。我们选择将这个程序中的字符表示为int,因为EOF取整数值(通常取值为-1)。用户通过键盘输入成绩。按Enter(或Return)键时,cin.get()函数一次读取一个字符。如果输入的字符不是文件结束符,则进入switch结构。关键字switch后面是括号中的变量名grade,称为控制表达式(controllingexpression)。控制表达式的值与每个case标记比较。假设用户输入成绩c,则c自动与switch中的每个case比较。如果找到匹配的(case'C:'),则执行该case语句。对于字母C的case语句中.cCount加1,并用break语句立即退出switch结构。注意,与其他控制结构不同的是,有多个语句的case不必放在花括号中。break语句使程序控制转到switch结构后面的第一条语句。break语句使switch结构中的其他case不会一起运行。如果switch结构中不用break语句,则每次结构中发现匹配时,执行所有余下case中的语句(这个特性在几个case要完成相同操作时有用,见图2.22的程序)。如果找不到匹配,则执行defaultcase并打印一个错误消息。每个case有一个或几个操作。switch结构与其他结构不同,多个语句的case不必放在花括号中。图6.23显示了一般的swish多项选择结构(每个case用一个break语句)的流程图。从流程图中可以看出,case末尾的每个break语句使控制立即退出switch结构。注意,流程图(除了小圆框和流程之外)也只能包含矩形框和菱形框。这是我们强调的操作/判断编程模型。程序员的任务就是根据算法需要用堆栈和嵌套两种方法组合其他几种控制结构,然后填入算法所要的操作和判断,从而生成程序。嵌套控制结构很常见,但程序中很少出现嵌套swilch结构。在图6.22中的switch结构中,第46到第49行:case'\n':case'\t':case'':break;使程序跳过换行符、制表符和空白字符。一次读取一个字符可能造成一些问题。要让程序读取字符,就要按键盘上的Enter键将字符发送到计算机中,在要处理的字符后面输入换行符。这个换行符通常需要特殊处理,才能使程序正确工作。通过在switch结构中包含case语句,可以防止在每次输入中遇到换行符、制表符或空格时defaultcase打印错误消息。注意几个标号列在一起时(如图6.22中的case'D':case'd':)表示每case发生一组相同的操作。使用switch结构时,记住它只用于测试常量整型表达式(constantintegralexpression),即求值为一个常量整数值的字符常量和整型常量的组合。字符常量表示为单引号中的特定字符(如,'A'),而整型常量表示为整数值。本教程介绍面向对象编程时,会介绍实现switch逻辑的更精彩方法。我们使用多态方法生成比使用switch逻辑的程序更清晰、更简洁、更易维护和扩展的程序。C++之类可移植语言应有更灵活的数据类型长度,不同应用可能需要不同长度的整数。C++提供了几种表示整数的数据类型。每种类型的整数值范围取决于特定的计算机硬件。除了类型int和char外,C++还提供short(shortint的缩写)和long(longint的缩写)类型。short整数的取值范围是±32767。对于大多数整数计算,使用long类型的整数已经足够。long整数的取值范围是±2147483647。在大多数系统中,int等价于short或long。int的取值范围在short和long的取值范围之间。char数据类型可以表示计算机字符集中的任何字符,char数据类型也可以表示小整数。6.16do/while重复结构do/while重复结构与while结构相似。在while结构中,先在循环开头测试循环条件之后再执行循环体。do/while重复结构执行循环体之后再测试循环条件,因此,do/while结构至少执行循环体一次。do/while结构终止时,继续执行while语句后面的话句。注意,如果结构体中只有一条浯句,则不必在do/while结构中使用花括号。但通常还是加上花括号,避免棍淆while与do/while重复结构。例如:while(condition)通常当作while结构的首部。结构体中只有一条语句的do/while结构中不使用花括号时:dostatementwhile(condition);最后一行while(condition)可能被误解成while结构包含空语句。这样,只有一个语句的do/while结构通常写成如下形式:do{statement)while(condltion);图6.24所示的程序用do/while重复结构打印数字1到10。注意控制变量counter在循环条件测试中是前置自增的。另外,只有一个语句的do/while结构也使用了花括号。1//Fig.2.24:fig0224.cpp2//Usingthedo/whilerepetitionstructure3#include<iostream.h>45intmain()6{7intcounter=l;89do{10cout<<counter<<"";11}while(++counter<=10);1213cout<<endl;1415return0;16}输出结果:123456789lO图6.24使用do/while重复结构do/while重复结构如图6.25。这个流程图显示循环条件要在至少进行一次操作之后才执行。注意.流程图(除了小圆框和流程之外)也只能包含矩形框和菱形框,这是我们强调的操作/判断编程模型。程序员的任务就是根据算法需要用堆栈和嵌套两种方法组合其他几种控制结构,然后填入算法所要的操作和判断,从而生成程序。6.17break和continue语句break和continue语句改变控制流程。break语句在while、for、do/while或switch结构中执行时,使得程序立即退出这些结构,从而执行该结构后面的第一条语句。break语句常用于提前从循环退出或跳过switch结构的其余部分(如图6.22)。图6.26演示了for重复结构中的break语句,if结构发现x变为5时执行break,从而终止for语句,程序继续执行for结构后面的cout语句。循环只执行四次。注意这个程序中控制变量x在for结构首部之外定义。这是因为我们要在循环体中和循环执行完毕之后使用这个控制变量。continue语句在while、for或do/while结构中执行时跳过该结构体的其余语句,进入下一轮循环。在while和do/while结构中,循环条件测试在执行continue语句之后立即求值。在for结构中,执行递增表达式,然后进行循环条件测试。前面曾介绍过.while结构可以在大多数情况下取代for结构。但如果while结构中的递增表达式在continue语句之后,则会出现例外。这时,在测试循环条件之前没有执行递增,并且while与for的执行方式是不同的。图6.27在for结构中用continue语句跳过该结构的输出语句,进入下一轮循环。1//Fig.2.26:fig0226.cpp2//Usinqthebreakstatementinaforstructure3#include<~ostream.h>45intmain()6{7//xdeclaredheresoitcanbeusedaftertheloop8intx;910for(x=1;x<=10;x++){1112if{x==5)13break;//breaklooponlyifxis51415cout<<x<<"";16}1718cout<<"\nBrokeoutofloopatxof"<<x<<endl;19returnO;20}输出结果:l234Brokeoutofloopatxof5图6.26for重复结构中的break语句1//Fig.2.27:figO2_OT.cpp2//Usingthecontinuestatementinaforstructure3#include<iostream.h>45intmain()6{7for(ihtx=1;x<-10;x++}{89if{x==5)10continue;//skipremainingcodeinloop11//onlyifxis51213cout<<x<<"";14}1516cout<<"\nUsedcontinuetoskipprintingthevalue5"17<<endl;18returnO;19}l2345679910USedcontinuetoskipprintingthevalue5图6.27在for结构用continue语句6.18逻辑运算符前面只介绍了courter<=10、total>1000和number!=sentinelValue之类的简单条件(simplecondition)。我们用关系运算符>、<、>=、<=和相等运算符==、!=表示这些条件。每个判断只测试一个条件。要在每个判断中测试多个条件,可以在不同语句中或嵌套if(if/else)结构中进行这些测试。C++提供的逻辑运算符(logicaloperator)可以用简单条件组合成复杂条件。逻辑运算符有逻辑与(&&)、逻辑或(||)和逻辑非(!),下面将举例说明。假设要保证两个条件均为true之后再选择某个执行路径,这时可以用&&逻辑运算符:if{gender==1&&age>=65)++seniorFemales;这个if语句包含两个简单条件。条件gender==1确定这个人是男是女,条件age>=65确定这个人是否为老年人。&&逻辑运算符左边的简单条件先求值,因为::的优先级高于&&。如果需要,再对&&逻辑运算符右边的简单条件求值,因为>=的优先级高级于&&(稍后将会介绍,&&逻辑运算符右边的条件只在左边为true时才求值)。然后if语句考虑下列组合条件:gender==l&&age>=65如果两边的简单条件均为true,则这个条件为true。最后.如果这个条件为true,则seniorFemales递增1。如果两边的简单条件有一个为false,则程序跳过该递增,处理if后面的语句。上述组合条件可以通过增加多余的括号而变得更加清楚:(gender==1)&&(age>=65)图6.28的表格总结了&&逻辑运算符。表中显示了表达式1和表达式2的四种false和true值组合,这种表称为真值表(truthtable)。C++对所有包括逻辑运算符、相等运算符和关系运算符的所有表达式求值为false或true。表达式1表达式2表达式1&&表达式2falsefalsefalsefalsetruefalsetruefalsefalsetruetruetrue图6.28逻辑与(&&)运算符真值表下面考虑逻辑或运算符(||)。假设要保证两个条件至少有一个为true之后再选择某个执行路径,这时可以用逻辑或运算符,如下列程序段所示:if(semesterAverage)>=90||finalExam>=90)cout<<"StudentgradeisA"<<endl;上述条件也包含两个简单条件。条件semesterAverage>=90确定学生整个学期的表现是否为"A",条件finalExam>=90确定该生期末考试成绩是否优秀。然后if语句考虑下列组合条件:semesterAverage>=90||finalExam>=90如果两个简单条件至少有一个为true,则该生评为“A”。注意只有在两个简单条件均为false时才不打印消息"StudentgradeisA"。图6.29是逻辑或(||)运算符的真值表。&&逻辑运算符的优先级高于||运算符。这两个运算符都是从左向右结合。包含&&和||运算符的表达式只在知道真假值时才求值。这样,求值下列表达式:gender==1&&age>=65将在gender不等于1时立即停止(整个表达式为false),而gender等于1时则继续求值(整个表达式在age>=65为true时为true。表达式1表达式2表达式1&&表达式2falsefalsefalsefalsetruetruetruefalsetruetruetruetrue图6.29逻辑或(||)运算符真值表C++提供了逻辑非(!)运算符,使程序员可以逆转条件的意义。与&&和||运算符不同的是,&&和||运算符组合两个条件(是二元运算符),而逻辑非(!)运算符只有一个操作数(是一元运算符)。逻辑非(!)运算符放在条件之前,在原条件(不带逻辑非运算符的条件)为false时选择执行路径,例如:if(grade!=sentinelValue)cout<<"Thenextgradeis"<<grade<<endl;这种灵活性有助于程序员以更自然或更方便的方式表达条件。图6.31显示了前面介绍的c++运算符的优先级和结合律。运算符优先级从上到下逐渐降低。运算符结合律类型()从左向右括号++--+-!static_cast<type>()从右向左一元*/%从左向右乘+-从左向右加<<>>从左向右插入/读取<<=>>=从左向右关系==!=从左向右相等&&从左向右逻辑与||从左向右逻辑或?:从右向左条件=+=-=*=/=%=从右向左赋值,从左向右逗号图6.31运算符优先级和结合律6.19混淆相等(==)与赋值(=)运算符这是C++程序员常见的错误,包括熟练的C++程序员也会把相等(==)与赋值(=)运算符相混淆。这种错误的破坏性在于它们通常不会导致语法错误,而是能够顺利编译,程序运行完后,因为运行时的逻辑错误而得到错误结果。C++有两个方面会导致这些问题。一个是任何产生数值的表达式都可以用于任何控制结构的判断部分。如果数值为0,则当作false,如果数值为非0,则当作true。第二是C++赋值会产生一个值,即赋值运算符左边变量所取得的值。例如,假设把下列代码:if(payCode==4)cout<<"Yougetabonus!"<<endl;误写成如下形式:if(payCode=4)cout<<"Yougetabonus!"<<endl;第一个if语句对paycode等于4的人发奖金。而第二个if语句则求值if条件中的赋值表达式为常量4。由于非0值解释为true,因此这个if语句的条件总是true,则人人都获得一份奖金,不管其paycode为多少。更糟的是,本来只要检查paycode,却已经修改了Poycode。变量名可称为左值(lvalue),因为它可以放在赋值运算符左边。常量称为右值(rvalue),因为它只能放在赋值运算符右边。注意,左值可以作为右值,但右值不能用作左值。还有一个问题也同样麻烦。假设程序员要用下列简单涪句给一个变量赋值:x=1;但却写成:x==1;这也不是语法错误,编译器只是求值条件表达式。如果x等于1,则条件为true,表达式返回true值。如果x不等于1,则条件为false,表达式返回false值。不管返回什么值都没有赋值运算符,因此这个值丢失,x值保持不变,有可能造成执行时的逻辑错误。但这个问题没有简单的解决办法。6.20结构化编程小结结构化编程提倡简单性。Bohm和Jacopini已经证明,只需要三种控制形式:●顺序(sequence)●选择(selection)●重复(repetmon)顺序结构很简单,选择可以用三种方法实现:●if结构(单项选择)●if/else结构(双项选择)●switch结构(多项选择)事实上很容易证明简单的if结构即可提供任何形式的选择,任何能用if/else结构和switch结构完成的工作,也可以组合简单if结构来实现(但程序可能不够流畅)。重复可以用三种方法实现:●while结构●do/while结构●for结构很容易证明简单的while结构即可提供任何形式的重复,任何能用do/while和for结构完成的工作,也可以组合简单while结构来实现(但程序可能不够流畅)。根据以上结果,C++程序中所需的任何控制形式均可以用下列形式表示:●顺序●if结构(选择)●while结构(重复)这些控制结构只要用两种方式组合,即嵌套和堆栈。事实上,结构化编程提倡简单性。第7章函数7.1简介解决实际问题的大多数程序都比前几章介绍的程序要大得多。经验表明,要设计和修改大程序,最好的办法是从更容易管理的小块和小组件开始。这种方法称为“分而治之,各个击破”(divideandconquer)。本章介绍C++语言中的许多关键特性,可以帮助设计、实现、操作和维护大程序。7.2数学函数库数学函数库使程序员可以进行某些常见数学计算。我们这里用各种数学库函数介绍函数概念。本书稍后会介绍c++标准库中的许多其他函数。调用函数时,通常写上函数名,然后是一对括号,括号中写上函数参数(或逗号分隔的参数表)。例如程序员可以用下列语句计算和打印900.0的平方根:cout<<sqrt(900.0);执行这个语句时,数学库函数sqrt计算括号中所包含数字(900.0)的平方根。数字900.0是sqrt函数的参数。上述语句打印30。sqrtd函数取double类型参数,返回double类型结果。数学函数库中的所有函数都返回double类型结果。要使用数学库函数,需要在程序中包含math.h头文件(这个头文件在新的C++标准库中称为cmath)。函数参数可取常量、变量或表达式。如果c1=13.0、d=3.0和f=4.0,则下列语句:cout<<sqrt(cl+d*f);计算并打印13.0+3.0*4.0=25.0的平方根,即5(因为C++通常对没有小数部分的浮点数不打印小数点和后面的零)。图7.2总结了一些数学库函数。图中变量x和y为double类型。------------------------------------------------------------------------------函数说明举例------------------------------------------------------------------------------ceil(x)将x取整为不小于x的最小整数ceil(9.2)=10.0ceil(-9.8)=-9.0cos(x)x(弧度)的余弦cos(0.0)=1.0exp(x)指数函数exexp(1.0)=2.71828exp(2.0)=7.38906fabs(x)x的绝对值x>0,abs(x)=xx=0,abs(x)=0.0x<0,abs(x)=-xfloor(x)将x取整为不大于x的最大整数floor(9.2)=9.0floor(-9.8)=-10.0fmod(x,y)x/y的浮点数余数fmod(13.657,2.333)=1.992log(x)x的自然对数(底数为e)log(2.718282)=1.0log(7.389056)=2.0log10(x)x的对数(底数为10)log(10.0)=1.0log(100.0)=2.0pow(x,y)x的y次方(xy)pow(2,7)=128pow(9,.5)=3sin(x)x(弧度)的正弦sin(0.0)=0sqrt(x)x的平方根sqrt(900.0)=30.0sqrt(9.0)=3.0tan(x)x(弧度的正切tan(0.0)=0-------------------------------------------------------------------------------7.3函数函数使程序员可以将程序模块化。函数定义中声明的所有变量都是局部变量(localvariable),只在所在的函数中有效。大多数函数有一系列参数,提供函数之间沟通信息的方式。函数参数也是局部变量。将程序函数化的目的有几个,“分而治之、各个击破”的方法使程序开发更容易管理。另一个目的是软件复用性(softwarereusability),用现有函数作为基本组件,生成新程序。软件复用性是面向对象编程的主要因素。有了好的函数命名和定义,程序就可以由完成特定任务的标准化函数生成,而不必用自定义的代码生成。第三个目的是避免程序中的重复代码,将代码打包成函数使该代码可以从程序中的多个位置执行,只要调用函数即可。7.4函数定义前面介绍的每个程序都有一个main函数,调用标准库函数完成工作。现在要考虑程序员如何编写自定义函数。考虑一个程序,用自定义函数,square计算整数1到10的平方(如图7.3)。1//Fig.3.3:fig03_03.cpp2//Creatingandusingaprogra~er-definedfunction3#include<iostream.h>45intsquare(int);//functionprototype(函数原型)67intmain()(S9for(inx=1;x<=10;x++)10cout<<square(x)<<"";1112cout<<endl;13return0;14}1516//Functiondefinition(函数定义)17intsquare(inty)18(19returny*y;20}输出结果:l4916253649648ll00图7.3生成和使用自定义函数main中调用函数square如下所示:square(x)函数square在参数y中接收x值的副本。然后square计算y*y,所得结果返回main中调用square的位置,并显示结果,注意函数调用不改变x的值。这个过程用for重复结构重复十次。square定义表示square需要整数参数y。函数名前面的关键字int表示square返回一个整数结果。square中的return语句将计算结果返回调用函数。第5行:intsquare(int);是个函数原型(functionprototype)。括号中的数据类型int告诉编译器,函数square要求调用者提供整数值。函数名square左边的数据类型int告诉编译器,函数square向调用者返回整数值。编译器通过函数原型检查square调用是否包含正确的返回类型、参数个数、参数类型和参数顺序。如果函数定义出现在程序中首次使用该函数之前,则不需要函数原型,这种情况下,函数定义也作为函数原型。如果图33中第17行到第20行在main之前,则第5行的函数原型是不需要的。函数原型将在第7.6节详细介绍。函数定义格式如下:return-value-typefunction-name(parameter-list){declarationsandstatements}函数名(function-name)是任何有效标识符,返回值类型(return-value-type)是函数向调用者返回值的数据类型,返回值类型void表示函数没有返回值。不指定返回值类型时默认为int。花括号中的声明(declaration)和语句(statement)构成函数体(fuctionbody),函数体也称为块(block),块是包括声明的复合语句。变量可以在任何块中声明,而且块也可以嵌套。任何情况下不能在一个函数中定义另一个函数。将控制返回函数调用点的方法有三种。如果函数不返回结果,则控制在到达函数结束的右花括号时或执行下列语句时返回:return;如果函数返回结果,则下列语句:returnexpression;向调用者返回表达式的值。第二个例子用自定义函数maximum确定和返回三个整数中的最大值(如图7.4)。输入三个整数,然后将整数传递到maximum中,确定最大值。这个值用maximum中的return语句返回main。返回的值赋给变量largest,然后打印。1//Fig.7.4:fig0304.cpp2//Findingthemaximumofthreeintegers3#include<iostream.h>45intmaximum(int,int,int);//functionprototype(函数原型)67intmain()8{9inta,b,c;1011cout<<"Enterthreeintegers:";12cin>>a>>b>>c;1314//a,bandcbelowareargumentsto15//themaximumfunctioncall(函数调用)16cout<<"Maximumis:"<<maximum(a,b,c)<<endl;1718return0;19}2021//functionmaximumdefinition22//x,yandzbelowareparametersto23//themaximumfunctiondefinition24intmaximum(intx,inty,intz)25{26intmax=x;273031if{z>max)32max=z;3334returnmax;35}输出结果:Enterthreeintegers:228517Maximumis:85Enterthreeintegers:923514Maximumis:92Enterthreeintegers:451998Maximumis:98图7.4自定义函数maximum7.5头文件每个标准库都有对应的头文件(headerfile),包含库中所有函数的函数原型和这些函数所需各种数据类型和常量的定义。图7.6列出了C++程序中可能包括的常用C++标准库头文件。图7.6中多次出现的宏(macro)将在第17章“预处理器”中详细介绍。以.h结尾的头文件是旧式头文件。对每个旧式头文件,我们介绍新标准中使用的版本。程序员可以生成自定义头文件,自定义头文件应以.h结尾。可以用#include预处理指令包括自定义头文件。例如,square.h头文件可以用下列指令:#include"square.h"放在程序开头。17.2节介绍了包含头文件的其他信息。旧式头文件说明旧式头文件(本书前面使用)(assert.h>包含增加诊断以程序调试的宏和信息。这个头文件的新版本为<cassert><ctype.h>包含测试某些字符属性的函数原型和将小写字母变为大写字母,将大写字母变为小写字母的函数原型。这个头文件的新版本为<cctype><float.h>包含系统的浮点长度限制。这个头文件的新版本为<cfloat><limits.h>包含系统的整数长度限制。这个头文件的新版本为<climits><math.h>包含数学库函数的函数原型。这个头文件的新版本为<cmath><stdio.h>包含标准输入/输出库函数的函数原型及其使用信息。这个头文件的新版本为<cstdio><stdlib.h>包含将数字变为文本、将文本变为数字、内存分配、随机数和各种其它工具函数的函数原型。这个头文件新版本为<cstdlib><string.h>包含C语言格式的字符串处理函数的函数原型。这个头文件的新版本为<cstring><time.h>包含操作时间和是期的函数原型和类型。这个头文件的新版本为<ctime><iostream.h>包含标准输入/输出函数的函数原型。这个头文件的新版本为<iostream><iomanip.h>包含能够格式化数据流的流操纵算子的函数原型。这个头文件的新版本为<iomanip><fstream.h>包含从磁盘文件输入输出到磁盘文件的函数原型。这个头文件的新版本为<fstream>标准库头文件说明<utility>包含许多标准库头文件使用的类和函数<vector>、<list>、包含实现标准库容器的类的头文件。容器在程序执行期间用于存放数据。我们将在<deque>、<queue>、“标准模板库”一章介绍这些头文件<stack>、<map>、<set>、<bitset><functional>包含用于标准库算法的类和函数<memory>包含用于向标准库容器分配内存的标准库使用的类和函数<iterator>包含标准库容器中操作数据的类<algorithm>包含标准库容器中操作数据的函数<exception>这些头文件包含用于异常处理的类(见第13章)<stdexcept><string>包含标准库中string类的定义(见第19章)<sstream>包含从内存字符串输入和输出到内存字符串的函数原型<locale>包含通常在流处理中用于处理其它语言形式数据的类和函数(例如货币格式、排序字符串、字符表示等)<limits>包含定义每种计算机平台特定数字数据类型的类<typeinfo>包含运行时类型信息的类(在执行时确定数据类型)图7.6常用C++标准库头文件7.6作用域规则程序中一个标识符有意义的部分称为其作用域。例如,块中声明局部变量时,其只能在这个块或这个块嵌套的块中引用。一个标识符的4个作用域是函数范围(functionscope)、文件范围(filescope)、块范围(blockscope)和函数原型范围(function-prototypescope)。后面还要介绍第五个——类范围(classscope)。任何函数之外声明的标识符取文件范围。这种标识符可以从声明处起到文件末尾的任何函数中访问。全局变量、任何函数之外声明的函数定义和函数原型都取文件范围。标号(后面带冒号的标识符,如start:)是惟一具有函数范围的标识符。标号可以在所在函数中任何地方使用,但不能在函数体之外引用。标号用于switch结构中(如case标号)和goto语句中(见第18章)。标号是函数内部的实现细节,这种信息隐藏(infomationhiding)是良好软件工程的基本原则之一。块中声明的标识符的作用域为块范围。块范围从标识符声明开始,到右花括号(})处结束。函数开头声明的局部变量的作用域为块范围,函数参数也是,它们也是函数的局部变量。任何块都可以包含变量声明。块嵌套时,如果外层块中的标识符与内层块中的标识符同名,则外层块中的标识符“隐藏”,直到内层块终止。在内层块中执行时,内层块中的标识符值是本块中定义的,而不是同名的外层标识符值。声明为static的局部变量尽管在函数执行时就已经存在.但该变量的作用域仍为块范围。存储时间不影响标识符的作用域。只有函数原型参数表中使用的标识符才具有函数原型范围。前面曾介绍过,函数原型不要求参数表中使用的标识符名称,只要求类型。如果函数原型参数表中使用名称,则编译器忽略这些名称。函数原型中使用的标识符可以在程序中的其他地方复用,不会产生歧义。图7.12的程序演示了全局变量、自动局部变量和static局部变量的作用域问题。1//Fig.7.12:fig0312.cpp2//Ascopingexample3#include<iostream.h>45voida(void);//functionprototype6voidb(void);//functionprototype7voidcvoid);//functionprototype89intx=1;//globalvariable1011intmain()12{13intx-5;//localvariabletomain1415cout<<"localxinouterscopeofmainis"<<x<<endl;1617{//startnewscope18intx=7;1920cout<<"localxininnerscopeofmainis"<<x<<endl;21}//endnewscope2223cout<<"localxinouterscopeofmainis"<<x<<endl;2425a();//ahasautomaticlocalx26b();//bhasstaticlocalx27c();//cusesglobalx28a();//areinitializesautomaticlocalx29b();//staticlocalxretainsitspreviousvalue30c();//globalxalsoretainsitsvalue3132cout<<"localxinmainis"<<x<<endl;3334return0;35}3637voida(void)38{39intx=25;//initiallzedeachtimeaiscalled4041cout<<endl<<"localxinais"<<x42<<"afterenteringa"<<endl;43++x;44cout<<"localxinais"<<x45<<"beforeexitinga"<<endl;46}4748voidb(void)4950staticintx=50;//Staticinitializationonly51//firsttimebiscalled.52cout<<endl<<"localstaticxis"<<x53<<-onenteringb"<<endl;54++x;55cout<<"localstaticxis"<<x56<<"onexitingb"<<endl;57}5859voidc(void)6061cout<<endl<<"globalxis"<<x62<<"onenteringc"<<endl;63x*=10;64cout<<"globalxis"<<x<<"onexitingc"<<endl;65}输出结果:localxinouterscopeofmainis5localxininnerscopeofmainiS7localxinouterscopeofmainis5localxinais25afterenteringalocalxinals26beforeexitingalocalstaticxis50onenteringblocalstaticxis51onexitingbglobalxis1onenteringcglobalxis10onexitingclocalxinais25afterenteringalocalxinais26beforeexitingalocalstaticxis51onenteringblocalstatzcxis52onexitingbglobalxisl0onenteringcglobalxis100onexitingclocalxinmainis5图7.12说明变量作用域的例子全局变量x声明并初始化为1。这个全局变量在任何声明x变量的块和函数中隐藏。在main函数中,局部变量x声明并初始化为5。打印这个变量,结果表示全局变量x在main函数中隐藏。然后在main函数中定义一个新块,将另一个局部变量x声明并初始化为7,打印这个变量,结果表示其隐藏main函数外层块中的x。数值为7的变量x在退出这个块时自动删除,并打印main函数外层块中的局部变量x,表示其不再隐藏。程序定义三个函数,都设有参数和返回值。函数a定义自动变量x并将其初始化为25。调用a时,打印该变量,递增其值,并在退出函数之前再次打印该值。每次调用该函数时,自动变量x重新初值化为25。函数b声明static变量x并将其初始化为10。声明为static的局部变量在离开作用域时仍然保持其数值。调用b时,打印x,递增其值,并在退出函数之前再次打印该值。下次调用这个函数时,static局部变量x包含数值51。函数c不声明任何变量,因此,函数c引用变量x时,使用全局变量x。调用函数c时,打印全局变量,将其乘以10,并在退出函数之前再次打印该值。下次调用函数c时,全局变量已变为10。最后,程序再次打印main函数中的局部变量x,结果表示所有函数调用都没有修改x的值,因为函数引用的都是其他范围中的变量。7.7递归前面介绍的程序通常由严格按层次方式调用的函数组成。对有些问题,可以用自己调用自己的函数。递归函数(recursivefunction)是直接调用自己或通过另一函数间接调用自己的函数。递归是个重要问题,在高级计算机科学教程中都会详细介绍。本节和下节介绍一些简单递归例子,本书则包含大量递归处理。图7.17(7.14节末尾)总结了本书的递归例子和练习。我们先介绍递归概念,然后再介绍几个包含递归函数的程序。递归问题的解决方法有许多相同之处。调用递归函数解决问题时,函数实际上只知道如何解决最简单的情况(称为基本情况)。对基本情况的函数调用只是简单地返回一个结果。如果在更复杂的问题中调用函数,则函数将问题分成两个概念性部分:函数中能够处理的部分和函数中不能够处理的部分。为了进行递归,后者要模拟原问题,但稍作简化或缩小。由于这个新问题与原问题相似,因此函数启动(调用)自己的最新副本来处理这个较小的问题,称为递归调用(reeursivecall)或递归步骤(reeursivestep)。递归步聚还包括关键字return,因为其结果与函数中需要处理的部分组合,形成的结果返回原调用者(可能是main)。递归步骤在原函数调用仍然打开时执行,即原调用还没有完成。递归步骤可能导致更多递归调用,因为函数-继续把函数调用的新的子问题分解为两个概念性部分。要让递归最终停止,每次函数调用时都使问题进一步简化,从而产生越来越小的问题,最终合并到基本情况。这时,函数能识别并处理这个基本情况,并向前一个函数副本返回结果,并回溯一系列结果,直到原函数调用最终把最后结果返回给main。这一切比起前面介绍的其他问题似乎相当复杂.下面通过一个例子来说明。我们用递归程序进行一个著名的数学计算。非负整数n的阶乘写成n!,为下列数的积:n*(n—1)·(n—2)*...*l其中1,等于1,0定义为1。例如,51为5*4*3*2*1.即120。整数number大于或等于0时的阶乘可以用下列for循环迭代(非递归)计算:factorial=l;for{intcounter=number;counter>=1;counter--)factorial*=counter;通过下列关系可以得到阶乘函数的递归定义:n!=n*(n-1)!例如,5!等于5*4!,如下所示:5!=5*4*3*2*l5!=5*(4*3*2*1)5!=5*(4!)求值5!的过程如图7.13。图7.13a)显示如何递归调用,直到1!求值为1,递归终止。图7.13b)显示每次递归调用向调用者返回的值,直到计算和返回最后值。图7.14的程序用递归法计算并打印。到10的整数阶乘(稍后将介绍数据类型unsignedlong的选择)。递归函数factorial首先测试终止条件是否为true,即number是否小于或等于1。如果number小于或等于1,则factorial返回1,不再继续递归,程序终止。如果number大于1,则下列语句。returnnumber*factorial(number-1);将问题表示为number乘以递归调用dactorial求值的number-1的阶乘。注意factorial(number-1)比原先factorial(number)的计算稍微简单一些。1//Fig.7.14:fig0314.cpp2//Recursivefactorialfunction3#include<iostream.h>4#include<iomanip,h>56unsignedlongfactorial(unsignedlong);78intmain()9{10for(ihti=O;i<=10:i++)11cout<<setw(2)<<1<<"!="<<factorial(i)<<endl;1213return0;14}1516//Recursivedefinitionoffunctionfactorial17unsignedlongfactorial(unsignedlongnumber)18{19if(number<=1)//basecase20return1;21else//recursivecase22returnnumber*factorial(number-1);23}输出结果:0!=11!=12!=23!=64!=245!=1206!=7207!=50408!=403209!=36288010!=3628800图7.4用递归法计算阶乘函数factorial声明为接收unsignedlong类型的参数和返回unsignedlong类型的值。unsignedlong是unsignedlongint的缩写,C++语言的规则要求存放unsignedlongint类型变量至少占用4个字节(32位),因此可以取0到4294967291之间的值(数据类型longint至少占内存中的4个字节,可以取±2147483647之间的值)。如图7.14,阶乘值很快就变得很大。我们选择unsignedlong数据类型,使程序可以在字长为16位的计算机上计算大于7!的阶乘。但是,factorial函数很快产生很大的值,即使unsignedlong也只能打印少量阶乘值,然后就会超过unsignedlong变量的长度。练习中将会介绍,用户最终可能要用float和double类型来计算大数的阶乘,这就指出了大多数编程语言的弱点,即不能方便地扩展成处理不同应用程序的特殊要求。从本书面向对象编程部分可以看到,C++是个可扩展语言,可以在需要时生成任意大的数。7.8使用递归举例,Fibonacci数列Fibonacci数列(斐波纳契数列):0,1,1,2,3,5,8,13,21,...以0和1开头.后续每个Fibonaeei数是前面两个Fibonacci数的和。自然界中就有这种数列,描述一种螺线形状。相邻Fibonacci数的比是一个常量1.618…,这个数在自然界中经常出现,称为黄金分割(goldenratio或goldenmean)。人们发现,黄金分割会产生最佳的欣赏效果。因此建筑师通常把窗户、房子和大楼的长和宽的比例设置为黄金分割数,明信片的长宽比也采用黄金分割数。Fibonacci数列可以递归定义如下:fibonacci(0)=0fibonacci(1)=lfibonacci(n)=fibonacci{n-1)+fibonacci(n-2)图7.15的程序用函数fibonaoci递归计算第i个Fibonacci数。注意Fibonacci数很快也会变得很大,因此把Fibonacci函数的参数和返回值类型设为unsignedlong数据类型。图7.11中每对输出行显示运行一次程序的结果。1//Fig.7.15:fig03_lS.cpp2//Recursivefibonaccifunction3#include<iostream.h>45longfibonacci(long);67intmain()8{9longresult,number;1011cout<<"Enteraninteger:";12cin>>number;13result=fibonacci(number);14tout<<"Fibonacci{"<<number<<")="<<result<<endl;15return0;16}1718//Recursivedefinitionoffunctionfibonacci19longfibonacci(longn)2O{21if(n==0||n==1)//basecase22returnn;23else//recursivecase24returnfibonacei(n-1)+fibonacci(n-2);25}输出结果如下:Enteraninteger:0Fibonacci(0)=0Enteraninteger:1Fibonacci(1)=1Enteraninteger:2Flbonacci(2)-1Enteraninteger:3Fibonacci(3)-2Enteraninteger:4Fibonacci(4)=3Enteraninteger:5Fibonacci(5)=5Enteraninteger:6Fibonacci(6)=8Enteraninteger:10Fibonacci(10)=55Enteraninteger:20Fibonacci(20)=6765Enteraninteger:30Fibonacci(30)=832040Enteraninteger:35Fibonacci(35)=9227465图7.15递归计算Fibonacci数列main中调用fibonacci函数不是递归调用,但后续所有调用fibonacci函数都是递归调用。每次调用fibonacci时.它立即测试基本情况(n等于0或1)。如果是基本情况,则返回n。有趣的是,如果n大于1,则递归步骤产生两个递归调用,各解决原先调用fibonacci问题的一个简化问题。图9.16显示了fibonacci函数如何求值fibonacci(3),我们将fibonacci缩写成f。图中提出了C++编译器对运算符操作数求值时的一些有趣的顺序问题。这个问题与运算符作用于操作数的顺序不同,后者的顺序是由运算符优先级规则确定的。图7.16中显示,求值f(3)时,进行两个递归调用,即f(2)和f(1)。但这些调用的顺序如何呢?大多数程序员认为操作数从左向右求值。奇怪的是,C++语言没有指定大多数运算符的操作数求值顺序(包括+),因此,程序员不能假设这些调用的顺序。调用可能先执行f(2)再执行f(1),也可能先执行f(1)再执行f(2)在该程序和大多数其他程序中,最终结果都是相同的。但在有些程序中,操作数求值顺序的副作用可能影响表达式的最终结果。C++语言只指定四种运算符的操作数求值顺序,即"&&"、"||"、逗号运算符(,)和“?:”。前三种是二元运算符,操作数从左向右求值。第四种是C++惟一的三元运算符,先求值最左边的操作数,如果最左边的操作数为非0,则求值中间的操作数,忽略最后的操作数;如果最左边的操作数为0,则求值最后的操作数,忽略中间的操作数。要注意这类程序中产生Fibonacci数的顺序。Fibonacci函数中的每一层递归对调用数有加倍的效果,即第n个Fibonaeci数在第2n次递归调用中计算。计算第20个Fibonacci数就要220次,即上百万次调用,而计算第30个Fibonacci数就要230次,即几十亿次调用。计算机科学家称这种现象为指数复杂性(exponentialcomplexity),这种问题能让最强大的计算机望而生畏。一般复杂性问题和指数复杂性将在高级计算机科学课程“算法”中详细介绍。7.9递归与迭代前面几节介绍了两个可以方便地用递归与迭代实现的函数。本节要比较递归与迭代方法,介绍为什么程序员在不同情况下选择不同方法。递归与迭代都是基于控制结构:迭代用重复结构,而递归用选择结构。递归与迭代都涉及重复:迭代显式使用重复结构,而递归通过重复函数调用实现重复。递归与迭代都涉及终止测试:迭代在循环条件失败时终止,递归在遇到基本情况时终止。使用计数器控制重复的迭代和递归都逐渐到达终止点:迭代一直修改计数器,直到计数器值使循环条件失败;递归不断产生最初问题的简化副本,直到达到基本情况。迭代和递归过程都可以无限进行:如果循环条件测试永远不变成false,则迭代发生无限循环;如果递归永远无法回推到基本情况,则发生无穷递归。递归有许多缺点,它重复调用机制,因此重复函数调用的开销很大,将占用很长的处理器时间和大量的内存空间。每次递归调用都要生成函数的另一个副本(实际上只是函数变量的另一个副本).从而消耗大量内存空间。迭代通常发生在函数内,因此没有重复调用函数和多余内存赋值的开销。那么,为什么选择递归呢?大多数有关编程的教材都把递归放在后面再讲。我们认为递归问题比较复杂而且内容丰富,应放在前面介绍,本书余下部分会通过更多例子加以说明。图7.17总结了本书使用递归算法的例子和练习。下面重新考虑书中重复强调的一些观点。良好的软件工程很重要,高性能也很重要,但是,这些目标常常是互相矛盾的。良好的软件工程是使开发的大型复杂的软件系统更容易管理的关键,面高性能是今后在硬件上增加计算需求时实现系统的关键,这两个方面如何取得折衷?7.10带空参数表的函数在C++中,空参数表可以用void指定或括号中不放任何东西。下列声明:voidprint();指定函数print不取任何参数,也不返回任何值。图7.18演示了C++声明和使用带空参数表的函数的方法。//Fig.7.18:fig03_l$.cpp2//Functionsthattakenoarguments3#include<iostream.h>45voidfunctionl();6voidfunction2(void);78intmain()9{10functionl();11function2();1213return0;14}1516voidfunctionl()17{18cout<<"functionltakesnoarguments"<<endl;19}2021voidfunction2(void)22{23cout<<"function2alsotakesnoarguments"<<endl;24}输出结果:functionltakesnoargumentsfunction2alsotakesnoarguments图7.18两种声明和使用带空参数表函数的方法7.11内联函数从软件工程角度看,将程序实现为一组函数很有好处,但函数调用却会增加执行时的开销。C++提供了内联函数(inlinefunction)可以减少函数调用的开销,特别是对于小函数。函数定义中函数返回类型前面的限定符inline指示编译器将函数代码复制到程序中以避免函数调用。其代价是会产生函数代码的多个副本并分别插入到程序中每一个调用该函数的位置上(从而使程序更大),而不是只有一个函数副本(每次调用函数时将控制传人函数中)。典型情况下,除了最小的函数以外编译器可以忽略用于其他函数的inline限定符。图7.19的程序用内联函数cube计算边长为s的立方体体积。函数cube参数表中的关键字const表示函数不修改变量的值s。1//Fig.7.19:fig0319.cpp2//Usinganinlinefunctiontocalculate3//thevolumeofacube.4#include<iostream.h>56inlinefloatcube(constfloats){returns*s*s;}78intmain()9{10cout<<"Enterthesidelengthofyourcube:";1112floatside;1314cin>>side;15cout<<"Volumeofcubewithside,,16<<side<<"is"<<cube(side)<<endl;17l8return0;19}输出结果:Enterthesidelengthofyourcube:3.5Volumeofcubewithside3.5is42.875图7.19用内联函数计算立方体的体积7.12函数重载C++允许定义多个同名函数,只要这些函数有不同参数集(至少有不同类型的参数)。这个功能称为函数重载(functionoverloading)。调用重载函数时,C++编译器通过检查调用中的参数个数、类型和顺序来选择相应的函数。函数重载常用于生成几个进行类似任务而处理不同数据类型的同名函数。图7.25用重载函数square计算int类型值的平方以及double类型值的平方。1//Fig.7.25:fig03_25.cpp2//Usingoverloadedfunctions3#include<iostream.h>45ihtsquare(ihtx){returnx*x;}67doublesquare(doubley){returny*y;}89intmain()10{11cout<<"Thesquareofinteger7is"<<square(7)12<<"\nThesquareofdouble7.5is"<<square(7.5)13<<endl;1415return0;16}输出结果:Thesquareofinteger7is49Thesquareofdouble7.5is56.25图7.25使用重载函数重载函数通过签名(signature)进行区别,签名是函数名和参数类型的组合。编译器用参数个数和类型编码每个函数标识符(有时称为名字改编或名字修饰),以保证类型安全连接(type-safelinkage)。类型安全连接保证调用合适的重载函数井保证形参与实参相符。编译器能探测和报告连接错误。图7.26的程序在BorlandC++编译器上编译,我们不显示程序执行的输出,图中用汇编语言输出了由BorlandC++编译器产生的改编函数名。每个改编名用@加上函数名,改编参数表以$q开头。在函数nothing2的参数表中,zc表示char、i表示int、pf表示float*、pd表示double*。在函数nothing1的参数表中,i表示int、f表示float、zc表示char、pi表示int*。两个square函数用参数表区分,一个指定d表示double,一个指定i表示沁。函数的返回类型不在改编名称中指定。函数名改编是编译器特定的。重载函数可以有不同返回类型.但必须有不同参数表。1//Namemangling2lntsquare(intx)(returnx*x;}34doublesquare(doubley)(returny*y;}56voidnothing1(inta,floatb,charc,int*d)7{}//emptyfunctionbody89char*nothing2(chara,intb,float*c,double*d)10{return0;}1112intmain()13{14return0;15}输出结果:public_mainpublic@nothing2$qzcipfpdpublic@nothing1$qifzcpipublic@square$qdpublic@square$qi图7.26名字改编以保证类型安全连接编译器只用参数表区别同名函数。重载函数不一定要有相同个数的参数。程序员使用带默认参数的重载函数时要小心,以免出现歧义。第8章数组8.1简介数组(array)数据结构由相同类型的相关数据项组成。数组和结构是静态项目,在整个程序执行期间保持相同长度(当然,也可以用自动存储类,在每次进入和离开定义的块时生成和删除)。8.2数组数组是具有相同名称和相同类型的一组连续内存地址。要引用数组中的特定位置或元素,就要指定数组中的特定位置或元素的位置号(positionnumber)。图8.1显示了整型数组c。这个数组包含12个元素。可以用数组名加上方括号(1))中该元素的位置号引用该元素。数组中的第一个元素称为第0个元素(zerothelemem)。这样,c数组中的第一个元素为c[0],c数组中的第二个元素为c[1],c数组中的第七个元素为c[6],一般来说,c数组中的第i个元素为c[i-1]。数组名的规则与其他变量名相同。方括号中的位置号通常称为下标(subscript),下标应为整数或整型表达式。如果程序用整型表达式下标,则要求值这个整型表达式以确定下标,例如,假设a等于5,b等于6,则下列语句:c[a+b]+=2将数组元素c[11]加2。注意带下标的数组名是个左值,可用于赋值语句的左边。图8.1中整个数组的名称为c,该数组的12个元素为c[0]、c[1]、c[2]...c[11]。的值为-45、c[1]的值为6、c[2]的值为0,c[7]的值为62、c[11]的值为78。要打印数组c中前三个元素的和,用下列语句:cout<<c[0]+c[1]+c[2]<<endl;要将数组c的第7个元素的值除以2,并将结果赋给变量x,用下列语句:x=c[6]/2;包括数组下标的方括号实际上是个C++运算符。方括号的优先级与括号相同。图8.2显示了本书前面介绍的C++运算符优先级和结合律。运算符优先级从上到下逐渐减少。运算符结合律类型()[]从左向右括号++--+-!static_cast<type>()从右向左一元*/%从左向右乘+-从左向右加<<>>从左向右插入/读取<<=>>=从左向右关系==!=从左向右相等&&从左向右逻辑与||从左向右逻辑或?:从右向左条件=+=-=*=/=%=从左向右赋值,从左向右逗号图8.2运算符的优先级和结合律8.3声明数组数组要占用内存空间。程序员指定每个元素的类型和每个数组所要的元素,使编译器可以保留相应的内存空间。要告诉编译器对整型数组c保留12个元素,可以声明如下:intc[12];可以在一个声明中为几个数组保留内存。下列声明对整型数组b保留100个元素,对整型数组x保留27个元素:intb[100],x[27];数组可以声明包含其他数据类型。例如,char类型的数组可以存放字符串。字符串及其与字符数组的相似性(C++从c语言继承的关系)和指针与数组的关系将在第5章介绍。在介绍面向对象编程后,我们将讨论成熟的字符串对象。8.4使用数组的举例图8.3的程序用for重复结构将10个元家的整型数组n的元素初始化为0,并用表格形式打印数组。第一个输出语句显示for结构中所打印列的列标题。记住,setw指定下一个值的输出域宽。可以在数组声明中用等号和逗号分隔的列表(放在花括号中)将数组中的元素初始化。程序8.4将七个元素的整型数组初始化并用表格形式打印数组。1//Fig.8.3:fig04_03.cpp2//initializinganarray3#include<iostream.h>4#include<iomanip.h>56intmain()7{8inti,n[10];910for(i=0;i<10;i++)//initializearray11n[i]=0;1213cout<<"Element"<<setw(13)<<"Value"<<endl;1415for(i=0;i<10;i++)//printarray16cout<<setw(7)<<i<<setw(13)<<n[i]<<endl;1718return0;19}输出结果:Elementvalue00102030405060708090图8.3将10个元素的整型数组n的元素初始化为01//Fig.8.4:fig0404.cpp2//Initializinganarraywithadeclaration3#include<iostream.h>4#include<iomanip.h>56intmain()7{8intn[10]={32,27,64,18,95,14,90,70,60,37};910cout<<"Element"<<setw(13)<<"Value"<<endl;1112for(inti=0;i<10;i++)13cout<<setw(7)<<i<<setw(13)<<n[i]<<endl;1415return0;16}输出结果:ElementValue032127264318495514690770860937图8.4用声明将数组中的元素初始化如果初始化的元素比数组中的元素少,则其余元素自动初始化为0。例如,可以用下列声明将图8.3中数组n的元素初始化为0:intn[10]={0};其显式地将第一个元素初始化为0,隐式地将其余元素自动初始化为0,因为初始化值比数组中的元素少。程序员至少要显式地将第一个元素初始化为0,才能将其余元素自动初始化为0。图8.3的方法可以在程序执行时重复进行。需要初始化数组元素而没有初始化数组元素是个逻辑错误。下列数组声明是个逻辑错误:intn[5]={32,27,64,18,95,14};因为有6个初始化值,而数组只有5个元素。初始化值超过数组元素个数是个逻辑错误。如果带初始化值列表的声明中省略数组长度,则数组中的元素个数就是初始化值列表中的元素个数。例如:intn[]={1,2,3,4,5};生成五个元素的数组。图8.5的程序将10个元素的数组s初始化为整数2、4、6、…20,并以表格形式打印数组。这些数值是将循环计数器的值乘以2再加上2产生的。1//Fig.8.5:fig0405.cpp2//Initializearraystotheevenintegersfrom2to20.3#include<iostream.h>4#include<iomanip.h>56intmain()78constintarraySize=10;9intj,s[arraySize];1011for(j=0;j<arraySize;j++)//setthevalues12si[j]=2+2*j;1314cout<<"Element"<<setw(13)<<"Value"<<endl;1516for(j-0;j<arraysize;j++)//printthevalues17cout<<setw(7)<<j<<setw(13)<<s[j]<<endl;1819return0;20}输出结果:ElementValue02142638410512614716818920图8.5将产生的值赋给数组元素下列语句:constintarraySlze:10用const限定符声明常量变量arrayySize的值为10。常量变量应在声明时初始化为常量表达式,此后不能改变(图8.6和图8.7)。常量变量也称为命名常量(namedconstant)或只读变量(read-onlyvariable)。1//Fig.8.6:fig04_06.cpp2//Usingaproperlyinitializedconstantvariable3#include<iostream.h>45intmain()6{7constintx-7;//initializedconstantvariable89cout<<"Thevalueofconstantvariablexis:"10<<x<<endl;1112return0;13}输出结果:Thevalueofconstantvariablexis:7图8.6正确地初始化和使用常量变量1//Fig.8.7:fig0407.cpp2//Aconstobjectmustbeinitialized34intmain()5{6constintx;//Error:xmustbeinitialized78x=7;//Error:cannotmodifyaconstvariable910return0;11}输出结果:CompilingFIG04-7.CPP:ErrorFIG04_7.CPP6:Constantvariable,x’mustbeinitializedErrorFIG04_7.CPP8:Cannotmodifyaconstobject图8.7const对象应初始化常量变量可以放在任何出现常量表达式的地方。图8.5中,用常量变量arraySize指定数组s的长度:intj,s[arraySize];用常量变量声明数组长度使程序的伸缩性更强。图8.5中,要让第一个for循环填上1000个数组元素,只要将anaySize的值从10变为1000即可。如果不用常量变量arraySize,则要在程序中进行三处改变才能处理1000个数组元素。随着程序加大,这个方法在编写清晰的程序中越来越有用。将每个数组的长度定义为常量变量而不是常量,能使程序更清晰。这个方法可以取消“魔数”,例如,在处理10元素数组的程序中重复出现长度10使数字10人为地变得重要,程序中存在的与数组长度无关的其他数字10时可能使读者搞乱。图5.8中的程序求12个元素的整型数组a中的元素和,for循环体中的语句进行求和。请注意,数组a的初始化值通常是用户从键盘输入的。例如,下列for结构:for(intj=0;j<arraySize;j++)cin>>a[j];一次一个地从键盘读取数值,并将数值存放在元素a[j]中。下一个例子用数组汇总调查中收集的数据。考虑下列问题:40个学生用1到10的分数评价学生咖啡屋中的食品质量(1表示很差,10表示很好)。将40个值放在整型数组中,并汇总调查结果。这是典型的数组应用(如图8.9)。我们要汇总每种回答(1到10)的个数。数组responses是40个元素的评分数组。我们用11个元素的数组frequency计算每个答案的个数,忽略第一个元素frequeney[0],因为用1分对应frequency[1]而不是对应frequency[0]更好理解。这样可以直接用回答的分数作为frequency数组的下标。1//Fig.8.8:fig04OS.cppf2//Computethesumoftheelementsofthearray3#include<iostream.h>45intmain()6{7constihtarraySize=12;8inta[arraySize]={1,3,5,4,7,2,99,916,45,67,89,45};10inttotal=0;1112for(inti=0;i<arraySize;i++)13total+=a[i];1415cout<<"Totalofarrayelementvaluesis"<<total<<endl;16return0;17}输出结果:Totalofarrayelementvaluesis383图8.8计算数组元素和1//Fig.8.9:fig04_09.cpp2//Studentpollprogram38include<iostream.h>4#include<iomanip.h>56intmain()78constintresponseSize=40,frequencySize=11;9intresponses[responseSizeI={1,2,6,4,8,5,9,7,8,1010,1,6,3,8,6,10,3,8,2,7,6,5,7,6,8,6,7,115,6,6,5,6,7,5,6,4,8,6,8,10];12intfrequency[frequencySize]={0};1314for(intanswer=0;answer<responseSize;answer++)15++frequency[responses[answer]];1617cout<<"Rating"<<setw(17)<<"Frequency"<<endl;1819for(intrating=1;rating<frequencySize;rating++)20cout<<setw(6)<<rating21<<setw(17)<<frequency[rating]<<endl;2223return0;24}输出结果:RatingFrequency122232425561175879l103图8.9学生调查分析程序第一个for循环一次一个地从responses数组取得回答,并将frequeney数组中的10个计数器(frequency[l]到frequency[10])之一加1。这个循环中的关键语句如下:++frequency[responses[answer>;这个语句根据responses[answer]的值相应递增frequency计数器。例如,计数器answer为0时,responses[answer]为1,因此“++frequency[responses[answer>;”实际解释如下:++frequency[1];将数组下标为1的元素加1。计数器answer为1时,responses[answer]为2,因此"++frqueney[responses[answer>;”实际解释如下:++frequency[2];将数组下标为2的元素加1。计数器answer为2时,response[answer]为6,因此"++frequency[responses[answer>;”实际解释如下:++frequency[6];将数组下标为6的元素加1等等。注意,不管调查中处理多少个回答,都只需要11个元素的数组(忽略元素0)即可汇总结果。如果数据中包含13之类的无效值,则程序对frequency[13]加1,在数组边界之外。C++没有数组边界检查,无法阻止计算机到引用不存在的元素。这样执行程序可能超出数组边界,而不产生任伺警告。程序员应保证所有数组引用都在数组边界之内。C++是个可扩展语言,第8章介绍扩展C++,用类将数组实现为用户自定义类型。新的数组定义能进行许多C++内建数组中没有的操作,例如,可以直接比较数组,将一个数组赋给另一数组,用cin和cout输入和输出整个数组,自动初始化数组,防止访问超界数组元素和改变下标范围(甚至改变下标类型),使数组第一个元素下标不一定为0。下一个例子(图8.10)从数组读取数值,并用条形图或直方图进行描述。打印每个数值,然后在数字旁边打印该数字所指定星号个数的条形图。嵌套for循环实现绘制条形图。注意用endl结束直方图。1//Fig.8.10:fig04_10.cpp2//Histogramprintingprogram3#include<iostream.h>4#include<iomanip.h>56intmain()7{8constintarraySize=10;9intn[arraySize]={19,3,15,7,11,9,13,5,17,1};1011cout<<"Element"<<setw(13)<<"Value"12<<setw(17)<<"Histogram"<<endl;1314for(inti=0;i<arraySize;i++){15cout<<setw(7)<<i<<setw(13)16<<n[i]<<setw(9);1718for(intj=0;j<n[i];j++)//printonebar19cout<<'*';2021cout<<endl;22}2324return0;25}输出结果:ElementValueHistogram019*******************13***215***************37*******411************$9*********613*************75******817*****************91前面只介绍了整型数组,但数组可以是任何类型,下面要介绍如何用字符数组存放字符串。前面介绍的字符串处理功能只有用cout和<<输入字符串,“hello'’之类的字符串其实就是一个字符数组。字符数组有几个特性。字符数组可以用字符串直接量初始化。例如,下列声明:charstring1[]="first";将数组string1的元素初始化为字符串"first"中的各个元索。上述声明中string1的长度是编译器根据字符串长度确定的。注意,字符串“first”包括五个字符加一个特殊字符串终止符,称为空字符(nullcharacter),这样,字符串string1实际上包含6个元素。空字符对应的字符常量为'\0'(反斜杠加0),所有字符串均用这个空字符结尾。表示字符串的字符数组应该足以放置字符中中的所有字符和空字符。字符数组还可以用初始化值列表中的各个字符常量初始化。上述语句也可以写成:charstring1[]={'f','i','r','s','t','\0'}由于字符串其实是字符数组,因此可以用数组下标符号直接访问字符串中的各个字符。例如,string1[0]是字符'f',string1[3]是字符's'。我们还可以用cin和>>直接从键盘输入字符数组。例如,下列声明:charstring2[20];生成的字符数组能存放19个字符和一个null终止符的字符串。下列语句:cin>>string2;从键盘中将字符串读取到string2中。注意,上述语句中只提供了数组名,没有提供数组长度的信息。程序员要负责保证接收字符串的数组能够放置用户从键盘输入的任何字符串。cin从键盘读取字符,直到遇到第一个空白字符,它不管数组长度如何。这样,用cin和>>输人数据可能插入到数组边界之。如果cin>>提供足够大的数组,则键盘输入时可能造成数据丢失和其他严重的运行时错误。可以用cout和<<输出表示空字符终止字符串的字符数组。下列语句打印数组string2:cout<<string2<<endl;注意cout<<和cin>>一样不在乎字符数组的长度。一直打印字符串的字符,直到遇到null终止符为止。图8.12演示直接用字符串初始化字符数组、将字符串读取到字符数组中、将字符数组作为字符串打印以及访问字符串的各个字符。1//Fig.4_12:fig04_12.cpp2//Treatingcharacterarraysasstrings3#include<iostream.h>45intmain()6{7charstring1[20],string2[]="stringliteral";89cout<<"Enterastring:";10cin>>string1;11cout<<"string1is:"<<string112<<"\nstring2is:"<<string213<<"stringlwithspacesbetweencharactersis:\n";1415for(inti=0;string1[i]!='\0';i++)16cout<<string1[i]<<'';1718cin>>stringl;//reads"there"19cout<<"\nstring1is:"<<string1<<endl;2O21cout<<endl;22return0;23}输出结果:Enterastring:Hellotherestring1is:Hellostring2is:stringliteralstring1withspacesbetweencharactersis:Hellostring1is:there图8.12将字符数组当作字符串图8.12用for结构在string1数组中循环,并打印各个字符,用空格分开。for结构的条件string1[i]!='\0'在遇到字符串的null终止符之前一直为真。第3章介绍了存储类说明符static。函数体中的static局部变量在程序执行期间存在,但只能在函数体中访问该变量。图8.13显示了带有声明为static的局部效组的staticArrayInit函数和带自动局部数组的automaticArrayInit函数。staticArrayInit调用两次,编译器将static局部数组初始化为0。该函数打印数组,将每个元素加5,然后再次打印该数组;函数第二次调用时.static数组包含第一次调用时所存放的值。函数automaticArrayInit也调用两次,但自动局部数组的元素用数值1、2、3初始化。该函数打印数组,将每个元素加5,然后再次打印该数组;函数第二次调用时,数组重新初始化为1、2、3,因为这个数组是自动存储类。1//Fig.8.13:fig0413.cpp2//Staticarraysareinitializedtozero3#include<iostream.h>45voidstaticArrayInit(void);6voidautomaticArrayInit(void);78intmain()9{10cout<<"Firstcalltoeachfunction:\n";11staticArrayInit();12automaticArrayInit();1314cout<<"\n\nSecondcalltoeachfunction:\n";15staticArrayInit();16automaticArrayInit();17cout<<endl;1819return0;202122//functiontodemonstrateastaticlocalarray23voidstaticArrayInit(void)24{25staticihtarrayl[3];26inti;2728cout<<"\nValuesonenteringstaticArrayInit:\n";2930for(i=0;i<3;i++)31cout<<"array1["<<i<<"]="<<array1[i]<<"";3233cout<<"\nValuesonexitingstaticArrayInit:\n";3435for(i=0;i<3;i++)36cout<<"array1["<<i<<"]="37<<(arrayl[i]+=5)<<"";38}3940//functiontodemonstrateanautomaticlocalarray41voidautomaticArrayInit(void)42{43inti,array2[3]={1,2,3};4445cout<<"\n\nValuesonenteringautomaticArrayInit:\n";4647for(i=0;i<3;i++)48cout<<"array2["<<i<<"]="<<array2[i]<<"";4950cout<<"\nValuesonexitingautomaticArrayInit:\n";5152for(i=0;i<3;i++}53cout<<"array2["<<i<<"]="54<<(array2[i]+=5)<<"";55}输出结果:Firstcalltoeachfunction:ValuesonenteringstaticArrayInit:arrayl[0]=0array1[1I=0array1[2]=0ValuesonexitingstaticArrayInit:arrayl[0]=5array1[1]=5array1[2]=SValuesonenteringautomaticArrayInit:array2[0]=1array2[1]=2array2[2]=3ValuesonexitimgautomaticArraylmit:arrayl[0]=6array2[1]=7array2[2]=8secondcalltoeachfunction:ValuesonenteringstaticArrayImit:arrayl[0]=5arrayl[1]=5arrayl[2]=5ValuesonexitingstaticArrayInit:arrayl[0]=10arrayl[1]=10arrayl[2]=10ValuesonenteringautomaticArrayInit:array2[0]=1array[1]=2array2[2]=3ValuesonexitingautomaticArrayInit:array2[0]=6array2[1]=7array2[2]=8图8.13比较static数组初始化和自动数组初始化8.5将数组传递给函数要将数组参数传递给函数,需指定不带方括号的数组名。例如,如果数组hourlyTemperatures声明如下:inthourlyTemperatures[24];则下列函数调用语句:modifyArray(hourlyTemperatutes,24);将数组hourlyTemperatures及其长度传递给函数modifyArray。将数组传递给函数时,通常也将其长度传递给函数,使函数能处理数组中特定的元素个数(否则要在被调用函数中建立这些信息,甚至要把数组长度放在全局变量中)。第8章介绍Array类时,将把数组长度设计在用户自定义类型中,每个Array对象生成时都“知道”自己的长度。这样,将Array对象传递给函数时,就不用把数组长度作为参数一起传递。C++使用模拟的按引用调用,自动将数组传递绐函数,被调用函数可以修改调用者原数组中的元素值。数组名的值为数组中第一个元素的地址。由于传递数组的开始地址,因此被调用函数知道数组的准确存放位置。因此,被调用函数在函数体中修改数组元素时.实际上是修改原内存地址中的数组元素。尽管模拟按引用调用传递整个数组,但各个数组元素和简单变量一样是按值传递。这种简单的单个数据称为标量(scalar或scalarquanity)。要将数组元素传递给函数,用数组元素的下标名作为函数调用中的参数。第5章将介绍标量(即各个变量和数组元素)的模拟按引用调用。要让函数通过函数调用接收数组,函数的参数表应指定接收数组。例如,函数modifyArray的函数苜部可能如下所示:voidmodifyArray(intb[],intarraySize)表示modifyArray要在参数b中接收整型数组并在参数arraySize中接收数组元素个数。数组方括号中的数组长度不是必需的,如果包括,则编译器将其忽略。由于模拟按引用调用传递数组,因此被调用函数使用数组名b时,实际上引用调用者的实际数组(上例中为数组hourlyTemperatures)。第5章介绍表示函数接收数组的其他符号,这些符号基于数组与指针之间的密切关系。注意modlfyArray函数原型的表示方法:voidmodifyArray(int[],int);这个原型也可以改写成:voidmodifyArray(intanyArrayName[],int(anyVariableName)但第3章曾介绍过,C++编译器忽略函数原型中的变量名。图8.14的程序演示了传递整个数组与传递数组元素之间的差别。程序首先打印整型数组a的五个元素,然后将a及其长度传递给函数modifyArray,其中将a数组中的元素乘以2,然后在main中重新打印a。从输出可以看出,实际由modifyAnay修改a的元素。现在程序打印a[3]的值并将其传递给函数modifyElement。函数modifyElement将参数乘以2。然后打印新值。注意在main中重新打印a[3]时,它没有修改,因为各个数组元素是按值调用传递。1//Fig.8.14:fig0414.cpp2//Passingarraysandindividualarrayelementstofunctions3#include<iostream.h>4#include<iomanip.h>56voidmodifyArray(int[],int);//appearsstrange7voidmodifyElement(int);89intmain()10{11constintarraySize=5;12ihti,a[arraySize]={0,1,2,3,4};1314cout<<"Effectsofpassingentirearraycall-by-reference:"15<<"\n\nThevaluesoftheoriginalarrayare:\n";1617for(i=0;i<arraySize;i++)18cout(<<setw(3)<<a[i];1920cout<<endl;2122//arrayapassedcall-by-reference23modifyArray(a,arraySize);2425cout<<"Thevaluesofthemodifiedarrayare:\n";2627for(i=0;i<arraySize;i++)28cout<<setw(3)<<a[i];2930cout<<"\n\n\n"31<<"Effectsofpassingarrayelementcall-by-value:"32<<"\n\nThevalueofa[3]is"<<a[3]<<'\n';3334modifyElement(a[3]);3536cout<<"Thevalueofa[3]is"<<a[3]<<endl;3738return0;39}4O41voidmodifyArray(intb[],intsizeofArray)42{43for(intj=0;j<sizeofArray;j++)44b[j]*=2;45}4647voidmodifyElement(inte)48{49cout<<"ValueinmodifyElementis"50<<(e*=2)<<endl;51}输出结果:Effectsofpassingentirearraycall-by-Value:Thevaluesoftheoriginalarrayare:01234Thevaluesofthemodifiedarrayare:02468Effectsofpassingarrayelementcall-by-value:Thevalueofa[3]is6ValueinmodifyElementis12Thevalueofa[3]is6图8.14向函数传递数组和数组元素有时程序中的函数不能修改数组元素。由于总是模拟按引用调用传递数组.因此数组中数值的修改很难控制。C++提供类型限定符const,可以防止修改函数中的数组值。数组参数前面加上const限定符时,数组元素成为函数体中的常量,要在函数体中修改数组元素会造成语法错误。这样,程序员就可以纠正程序,使其不修改数组元素。图8.15演示了const限定符。函数tryToModifyArray定义参数constintb[],指定数组b为常量,不能修改。函数想修改数组元素会造成语法错误“Canotmodifyconstobject”。const限定符将在第7章再次介绍。1//Fig.8.15:fig04_lS.cpp2//Demonstratingtheconsttypequalifier3#include<iostream.h>45voidtryToModifyArray(constint[]);67intmain()8{9inta[]={10,20,30};1011tryToModifyArray(a);12cout<<a[0]<<''<<a[1]<<''<<a[2]<<'\n';13return0;14}1516voidtryToModifyArray(constintb[])17{18b[0]/=2;//error19b[1]/=2;//error20b[2]/=2;//error21}输出结果:CompilingFIG0415.CPP:ErrorFIG04_iS.CPP18:CanotmodifyaconstobjectErrorFIG04_iS.CPP19:CanotmodifyaconstobjectErrorFIG0415.CPP20:CanotmodifyaconstobjectWarningFIG04_15,CPP21:Parameter'b'isneverused图8.15演示const限定符8.6排序数组排序(sort)数组(即将数据排成特定顺序,如升序或降序)是一个重要的计算应用。银行按账号排序所有支票,使每个月末可以准备各个银行报表。电话公司按姓氏排序账号清单并在同一姓氏中按名字排序,以便于找到电话号码。几乎每个公司都要排序一些数据,有时要排序大量数据。排序数据是个复杂问题,是计算机科学中大量研究的课题。本章介绍最简单的排序机制,在本章练习和第15章中,我们要介绍更复杂的机制以达到更高的性能。图8.16的程序排列10个元素数组a的值,按升序排列。我们使用冒泡排序(bubblesortsinkingsort)方法,较少的数值慢慢从下往上“冒”,就像水中的气泡一样,而较大的值则慢慢往下沉。这个方法在数组中多次操作,每一次都比较一对相邻元素。如果某一对为升序(或数值相等),则将数值保持不变。如果某一对为降序,则将数值交换。1//Fig.8.16:fig0416.cpp2//Thisprogramsortsanarray'svaluesinto3//ascendingorder4#include<iostream.h>5#include<iomanip.h>67intmain{)8{9constintarraySize=10;10inta[arraySize]={2,6,4,8,10,12,89,68,45,37};11inti,hold;1213cout<<"Dataitemsinoriginalorder\n";1415for(i=0;i<arraySize;i++)16cout<<setw(4)<<a[i];1718for(intpass=0;pass<arraySize-1;pass++)//passes1920for(i=0;i<arraySize-1;i++)//onepass2122if(a[i]>a[i+1]){//onecomparison23holda[i];//oneswap24a[i]=a[i+1];25a[i+1]=hold;26}2728cout<<"\nDataitemsinascendingorder\n";2930for(i=0;i<arraySize;i++)31cout<<setw(4)<<a[i];3233cout<<endl;34return0;35}输出结果:Dataitemsinoriqinalorder2648101289684537Dataitemsinascendinqorder2468101237456889图8.16用冒泡法排序数组程序首先比较a[0]与a[1],然后比较a[1]与a[2],接着是a[2]与a[3],一直到比较a[8]与a[9]。尽管有10个元素,但只进行9次比较。由于连续进行比较,因此一次即可能将大值向下移动多位,但小值只能向上移动一位。第1遍,即可把最大的值移到数组底部,变为a[9]。第2遍,即可将第2大的值移到a[8]第9遍,将第9大的值移到a[1]。最小值即为a[0],因此,只进行9次比较即可排序10个元素的数组。排序是用嵌套for循环完成的。如果需要交换,则用三条赋值语句完成:hold=a[i];;a[i]=a[i+1];a[i+1]=hold;其中附加的变量hold临时保存要交换的两个值之一。只有两个赋值语句是无法进行交换的:a[i]=a[i+1];a[i+l]=a[i];例如,如果a[i]为7而a[i+1]为5.则第一条赋值语句之后,两个值均为5,数值7丢失,因此要先用变量ho1d临时保存要交换的两个值之一。冒泡排序的主要优点是易于编程。但冒泡排序的速度很慢,这在排序大数组时更明显。练习中要开发更有效的冒泡排序程序,介绍一些比冒泡排序更有效的方法。高级课题中将介绍更深入的排序与查找问题。8.7查找数组:线性查找与折半查找程序员经常要处理数组中存放的大量数据,可能需要确定数组是否包含符合某关键值(keyvalue)的值。寻找数组中某个元素的过程称为查找(searching)。本节介绍两个查找方法:简单的线性查找(linersearch)方法和更复杂的折半查找(binarysearch)方法。练习8.33和练习8.34要求用递归法实现线性查找与折半查找。图8.19的线性查找比较数组中每个元素与查找键(searchkey)。由于数组没有特定的顺序,很可能第一个就找到,也可能要到最后一个才找到。因此,平均起来,程序要比较数组中一半的元素才能找到查找键值。要确定哪个值不在数组中,则程序要比较查找键与数组中每一个元素。1//Fig.8.19:fig04_19.cpp2//Linearsearchofanarray3#include<iostream.h>45intlinearSearch(constint[],int,int);67intmain()8{9constintarraySize=100;10inta[arraySize],searchKey,element;1112for(intx=0;X<arraySize;x++)//createsomedata13a[x]=2*x;1415cout<<"Enterintegersearchkey:"<<endl;16cin>>searchKey;17element=linearSearch(a,searchKey,arraySize);1819if(element!=-1)20cout<<"Foundvalueinelement"<<element<<endl;21else22cout<<"Valuenotfound"<<endl;2324return0;25}2627intlinearsearch(constintarraay[],intkey,intsizeofArray)28{29for(intn=0;n<sizeofArray;n++)30if(arraay[n]==key)31returnn;3233return-1;34}输出结果:Enterintegersearchkey:36foundvalueinelement18Enterintegersearchkey:37Valuenotfound图8.19数组的线性查找线性查找方法适用于小数组或未排序数组。但是,对于大数组,线性查找是低效的。如果是排序数组,则可以用高速折半查找。折半查找算法在每次比较之后排除所查找数组的一半元素。这个算法找到数组的中间位置,将其与查找键比较。如果相等,则已找到查找键,返回该元素的数组下标。否则将问题简化为查找—半数组。如果查找键小于数组中间元素,则查找数组的前半部分,否则查找数组的后半部分。如果查找键不是指定子数组(原始数组的一部分)中的中间元素,则对原数组的四分之—重复这个算法。查找一直继续,直到查找键等于指定子数组中的中间元素或子数组只剩一个元素且不等于查找键(表示找不到这个查找键)为止。在最糟糕的情况下,查找1024个元素的数组只要用折半查找进行十次比较。重复将1024除2(因为折半查找算法在每次比较之后排除所查找效组的一半元素)得到值512、256、128、64、32、16、8、4、1和1。数值1024(210)除2十次之后即变成1。除以1就是折半查找算法的一次比较。1048576(220)个元素的数组最多只要20次比较就可以取得查找键。十亿个元素的数组最多只要30次比较就可以取得查找键。这比线性查找的性能大有提高,后者平均要求比较数组元素个数一半的次数。对于十亿个元素的数组,这是5亿次比较与30次比较的差别。折半查找所需的最大比较次数可以通过大于数组元素个数的第一个2的次幂的指数确定。图8.20显示了函数binarySearch的迭代版本。函数取4个参数,一个整数数组b、一个整数searehKey、low数组下标和high数组下标。如果查找键不符合于数组的中间元素,则调整low数组下标和high数组下标,以便查找更小的子数组。如果查找键小于中间元素,则high数组的下标设置为middle-1,继续查找low到middle-1的元素。如果查找健大于中间元素,则low数组的下标设置为middle+1,继续查找middle+1到high的元素。程序使用15个元素的数组。大于数组元素个数的第一个2的次幂是16(24),因此寻找查找健最多只要四次比较。函数printHeader输出数组下标,函数printRow输出折半查找过程中的每个子数组。每个子数组的中间元素标上星号(*),表示用这个元素与查找键比较。1//Fig.8.20:fig0420.cpp2//Binarysearchofanarray3#include<iostream.h>4#include<iomanip.h>56intbinarySeareh(int[],int,int,int,int);7voidprintHeader(iht,8voidprintRow(int[],int,int,int,int);910intmain()11{12constihtarraySize=15;13inta[arraySize],key,result;1415for(inti=0;i<arraySize;i++)16a[i]=2*i;//placesomedatainarray1718cout<<"Enteranumberbetween0and28:";19cin>>key;2021printHeader(arraySize);22result=binarySearsh(a,key,0,arraySize-1,arraySize);2324if(result!=-1)25cout<<'\n'<<key<<"foundinarrayelement"26<<result<<endl;27else28cout<<'\n'<<key<<"notfound"<<endl;2930return0;31}3233//34intbinarysearch(intb[],intsearchKey,intlow,inthigh,35intsize)36{37intmiddle;3839while(low<=high){40middle=(low+high)/2;4142printRow(b,low,middle,high,size);4344if(searchKey==b[middle])//match45returnmiddle;46elseif(searchKey<b[middle])47high=middle-1;//searchlowendofarray48else49low=middle+1;//searchhighendofarray50}5152return-1;//searchKeynotfound5455//Printaheaderfortheoutput56voidprintHeader(intsize)57{58cout<<"\nSubscripts:\n";59for(inti=0;i<size;i++)60cout<<setw(3)<<i<<'';6162cout<<'\n';6364for(i=1;i<=4*size;i++)65cout<<'-';6667cout<<endl;68}6970//Printonerowofoutputshowingthecurrent71//partofthearraybeingprocessed.72voidprintRow(intb[],intlow,intmid,inthigh,intsize)73{74for(inti=0;i<size;i++)75if(i<low||i>high)76cout<<"";77elseif(i==mid)//markmiddlevalue78cout<<setw(3)<<b[i]<<'*';79else80cout<<setw(3)<<b[i]<<'';8182cout<<endl;83}输出结果:Enteranumberbetween0and28:25Subscripts:01234567891011121314------------------------------------------------------------------------------02468101214*1618202224262916182022*2426282426*2824*25notfoundEnteranumberbetween0and28:8Subscripts:0123456789l0ll121314------------------------------------------------------------------------------02468l01214*161820222426280246*810128l0*128*8foundinarrayelement4Enteranumberbetween0and28:6Subscrlpts:0l234567891011121314------------------------------------------------------------------------------02468101214*161820222426280246*810126foundlnarrayelement3图8.20排序数组的折半查找过程8.8多维数组C++中有多维数组。多维数组常用于表示由行和列组成的表格。要表示表格中的元素,就要指定两个下标:习惯上第一个表示元素的行,第二个表示元素的列。用两个下标表示特定的表格或数组称为二维数组(double-subscriptedarray)。注意,多维数组可以有多于两个的下标,C++编译器支持至少12个数组下标。图8.21演示了二维数组a。数组包含三行四列,因此是3x4数组。一般来说,m行和n列的数组称为mxn数组。数组a中的每个元素如图8.21所示,元素名字表示为a[i][j],i和j是数组名,i和j是惟一标识a中每个元素的下标。注意第一行元素名的第一个下标均为0,第四列的元素名的第二个下标均为3。把二维数组元素a[x][y]误写成a[x,Y]。实际上,a[x,y]等于a[y],因为C++将包含返号运算符的表达式(x,y)求值为y(逗号分隔的表达式中最后一个项目)。图8.21三行四列的二维数组多维数组可以在声明中初始化,就像一维数组一样。例如,二维数组b[2][2]可以声明和初始化如下:intb[2][2]={{l,2},{3,4}};数值用花括号按行分组,因此1和2初始化b[0][0]和b[0][1],3和4初始化b[1][0]和b[1][1]。如果指定行没有足够的初始化值,则该行的其余元素初始化为0。这样,下列声明:intb[2][2]={{1},{3,4}};初始化b[0][0]为1、b[0][1]为0、b[1][0)]为3、b[1][1]为4。图8.22演示了声明中初始化二维数组。程序声明三个数组,各有两行三列。array1的声明在两个子列表中提供六个初始化值。第一个子列表初始化数组第一行为1、2、3,第二个子列表初始化数组第二行为4、5、6。如果从array1初始化值列表中删除每个子列表的花括号,则编译器自动先初始化第一行元素,然后初始化第二行元素。array2声明提供六个初始化值。初始化值先赋给第一行,再赋给第二行。任何没有显式初始化值的元素都自动初始化为0,因此array2[1][2]自动初始化为0。1//Fig.8.22:fig04_22.cpp2//Initializingmultidimensionalarrays3#include<iostream.h>45voidprintArray(int[][3]);67intmain()8{9intarrayl[2][3]={{1,2,3},{4,5,6}},10array2[2][3]{1,2,3,4,5},11array3[2][3]={{1,2},{4}};1213cout<<"Valuesinarraylbyroware:"<<endl;14printArray(array1);1516cout<<"Valuesinarray2byroware:"<<endl;17printArray(array2);1819cout<<"Valuesinarray3byroware:"<<endl;20printArray(array3);2122return0;23}2425voidprintArray(inta[][3])26{27for(inti=0;i<2;i++){2829for(intj=0;j<3;j++)30cout<<a[i][j]<<'';3132cout<<endl;33}34}输出结果:Valuesinarray1byroware:123456Valuesinarray2byroware:123450Valuesinarray3byroware:120400图8.22初始化多维数组array3的声明在两个子列表中提供3个初始化。第一行的子列表显式地将第一行的前两个元素初始化为1和2,第3个元素自动初始化为0。第二行的子列表显式地将第一个元素初始化为4,最后两个元素自动初始化为0。程序调用函数printArray输出每个数组的元素。注意函数定义指定数组参数为inta[][3]。函数参数中收到单下标数组时,函数参数表中的数组括号是空的。多下标数组第一个下标的长度也不需要,但随后的所有下标长度都是必须的。编译器用这些长度确定多下标数组中元素在内存中的地址。不管下标数是多少,所有数组元素都是在内存中顺序存放。在双下标数组中,第一行后面的内存地址存放第二行。在参数声明中提供下标使编译器能告诉函数如何找到数组中的元素。在二维数组中,每行是一个一维数组。要找到特定行中的元素,函数要知道每一行有多少元素,以便在访问数组时跳过适当数量的内存地址。这样,访问a[1][2]时,函数知道跳过内存中第一行的3个元素以访问第二行(行1),然后访问这一行的第3个元素(元素2)。许多常见数组操作用for重复结构。例如,下列for结构(见图8.21)将数组a第三行的所有元素设置为0:for(column=0;coulumn<4;column++)a[2][Column]=0;我们指定第三行,因此知道第一个下标总是2(0是第一行的下标,1是第二行的下标)。for循环只改变第二个下标(即列下标)。上述for结构等同于下列赋值语句:a[2][0]=0;a[2][1]=0;a[2][2]=0;a[2][3]=0;下列嵌套for结构确定数组a中所有元素的总和:total=0;for(row=0;row<3;row++)for(column=0;column<4;column++)total+=a[row][column];for结构一次一行地求数组中所有元素的和。外层for结构首先设置row(即行下标)为0,使第一行的元素可以用内层for结构求和。然后外层for结构将row递增为1,使第二行的元素可以用内层for结构求和。然后外层for结构将mw递增为2,使第三行的元素可以用内层for结构求和。结束嵌套br结构之后,打印结果。图8.23的程序对3x4数组studentCrades进行几个其他常见数组操作。每行数组表示一个学生,每列表示学生期末考试中4门成绩中的一门成绩。数组操作用4个函数进行。函数mimimum确定该学期所有学生考试成绩中的最低成绩。函数maximum确定该学期所有学生考试成绩中的最高成绩。函数average确定每个学生该学期的平均成绩。函数printArray以整齐的表格形式输出二维数组。1//Fig.8.23:fig04_23.cpp2//Double-subscriptedarrayexample3#include<iostream.h>4#include<iomanip.h>56constihtstudents=3;//numberofstudents7constihtexams=4;//numberofexams89intminimum(int[][exams],int,int);10intmaximum(int[][exams],int,int};11floataverage(int[],int);12voidprintArray(int[][exams],int,int);1314intmain()I5{16intstudentGrades[students][exams]=17{{77,68,86,73},18{96,87,89,78},19{70,90,86,81}};2021cout<<"Thearrayis:\n";22printArray(studentGrades,students,exams);23cout<<"\n\nLowestgrade:"24<<minimum(studentGrades,students,exams)25<<"\nHighestgrade:"26<<maximum(studentGrades,students,exams)<<'\n';2728for(intperson=0;person<students;person++}29cout<<"Theaveragegradeforstudent"<<person<<"is"3O31<<setprecision(2)32<<average(studentGrades[person],exams)<<endl;3334return0;353637//Findtheminimumgrade38intminimum(intgrades[][exams],intpupils,inttests)39{40intlowGrade=100;4142for(inti=0;i<pupils;i++)44for(intj=0;j<tests;j++)4546if(grades[i][j]<lowGrade)47lowGrade=grades[i][j];4849returnlowGrade;50}5152//Findthemaximumgrade53intmaximum(intgrades[][exams],ihtpupils,ihttests)54{55inthighgrade=0;5657for(inti=0;i<pupils;i++)5859for(intj=0;j<tests;j++)6061if(grades[i][j]>highgrade)62highgrade=grades[i][j];6364returnhighgrade;65}6667//Determinetheaveragegradeforaparticularstudent68floataverage(intsetofGrades[],inttests)69{70inttotal=0;7172for(inti=0;i<tests;i++)73total+=setofGrades[i];7475return(float)total/tests;76}7778//Printthearray80{8Icout<<"[0][1][2][3]";83for(inti=0;i<pupils;i++){84cout<<"\nstudentGrades["<<i<<"]";8586for(intj=0;j<tests;j++)87cout<<setiosflags(ios::left)<<setw(5)88<<grades[i][j];89}90输出结果:Thearrayis:[0][1][2][3]StudentGrades[0]77688673StudentGrades[1]96879878StudentGrades[2]70908681Lowestgrade:68Highestgrade:96Theaveragegradeforstudent0is76.00Theaveragegradeforstudent1is87.50Theaveragegradeforstudent2is81.75图8.23使用二维数组举例函数minimum、maximum和printArray取得三个参数——studentGrades数组(在每个函数中调用grades)、学生数(数组行)、考试门数(数组列)。每个函数用嵌套for结构对grades数组循环。下列嵌套for结构来自函数mlmmum的定义:for(i=0;i<pupils;i++)for(j=0;j<tests;j++)if(grades[i][j]<lowGrade)lowGrade=grades[i][i];外层for结构首先将i(行下标)设置为0,使第一行的元素可以和内层for结构体中的变量lowGrade比较。内层for结构比较lowGrade与4个成绩中的每一门成绩。如果成绩小于lowGrade,则lowGrade设置为该门成绩。然后外层for结构将行下标加到1,第二行的元素与变量lowGrade比较。接着外层for结行下标加到2,第三行的元素与变量lowGrade比较。嵌套for结构执行完成后,lowGrade包含双下标数组中的最小值。函数maximum的执行过程与函数minimum相似。函数average取两个参数:一个单下标数组为某个学生的考试成绩,一个数字表示数组中有几个考试成绩。调用average时,第一个参数studentGrades[student]指定将双下标数组studentGrades的特定行传递给average。例如,参数studentGrad[1]表示双下标数组studentGrades第二行中存放的4个值(成绩的单下标数组)。双下标数组可以看作元素是单下标数组的数组。函数average计算数组元素的和,将总和除以考试成绩的个数,并返回一个浮点数结果。第9章指针与字符串9.1简介本章介绍“C++编程语言一个最强大的特性——指针。指针是C++中最难掌握的问题之一。前面介绍了引用可以用于实现按引用调用。指针使程序可模拟按引用调用,生成与操作动态数据结构,即能够伸缩的数据结构,如链表、队列、堆栈和树。本章介绍基本的指针概念,而且强调了数组、指针与字符串之间的密切关系,并包括一组很好的字符串操作练习。把数组和字符串看成指针是从C语言演变而来的。本书后面会介绍把数组和字符串当作成熟的对象。9.2指针变量的声明与初始化指针变量的值为内存地址。通常变量直接包含特定值,而指针则包含特定值变量的地址。因此可以说,变量名直接(directly)引用数值,而指针间接(indirectly)引用数值(如图9.1)。通过指针引用数值称为间接引用。指针和任何其他变量一样,应先声明后使用。下列声明:int*countPtr,count;声明变量countPtr的类型为int*(即指向整型值的指针),或者说成"countPtr是int的指针"或"countPtr指向整数类型的对象"。变量count声明为整数,而不是整型值的指针。声明中的*只适用于countPtr。声明为指针的每个变量前面都要加上星号(*)。例如,下列声明:float*xPtr,*yPtr;表示xPtr和yPtr都是指向float值的指针。声明中以这种方式使用*时,它表示变量声明为指针。指针可以声明为指向任何数据类型的对象。假设对指针的声明会分配到声明中逗号分隔的指针变量名列表中的所有指针变量名,从而将指针声明为非指针。声明为指针的每个变量前面都要加上星号(*)。图9.1直接和间接引用变量指针应在声明时或在赋值语句中初始化。指针可以初始化为0、NULL或—个地址。数值为0或NULL的指针不指任何内容。NULL是头文件<iostream.h>(和另外几个标准库头文件)中定义的符号化常量。将指针初始化为NULL等于将指针初始化为0,但C++中优先选择0。指定0时,它变为指针的相应类型。数值0是惟一可以不将整数转换为指针类型而直接赋给指针变量的整数值。9.3节将介绍将变量地址赋给指针。9.3指针运算符&(地址)运算符是个一元运算符,返回操作数的地址。例如,假设声明:inty=5;int*yPtr;则下列语句:yPtr=&y;将变量y的地址赋给指针变量yPtr。变量yPtr“指向”y。图9.2显示了执行上述语句之后的内存示意图。图中从指针向所指对象画一个箭头表示“指向关系”。图9.3显示了指针在内存中的表示,假设整型变量y存放在地址600000,指针变量yPtr存放在地址500000。地址运算符的操作数应为左值,(即要赋值的项目,如变量名).地址运算符不能用于常量、不产生引用的表达式和用存储类regtster声明的变量。“*”运算符通常称为间接运算符(indirectionoperator)或复引用运算符(dereferencingoperator),返回操作数(即指针)所指对象的同义词、别名或浑名。例如(图9.2再次引用),下列语句:cout<<*yPtr<<endl;指向变量y的值(5),如同下列语句:cout<<y<<endl;图9.2指针指向内存中整数变量的示意图这里使用*的方法称为复引用指针(dereferencingapointer)。注意复引用指针也可以用于赋值语句左边,例如下列语句:*yPtr=9;将数值9赋给图9.3中的y。复引用指针也可用于接收输入值,例如:cin>>*yPtr;复引用的指针是个左值。yptry5000006000006000005图9.3指针在内存中的表示图9.4的程序演示了指针运算符。本例中通过<<用十六进制整数输出内存地址(十六进制整数见附录“数值系统”)。注意a的地址和aPtr的值在输出中是一致的,说明a的地址实际赋给了指针变量aptr。&和*运算符是互逆的,如果两者同时作用于aPtr,则打印相同的结果。图9.5显示了前面所介绍的运算符的优先级和结合律。1//Fig.9.4:fig05_04.cpp2//Usingthe&and*operators3#include<iostream.h>45intmain()6{7inta;//aisaninteger8iht*aPtr;//aPtrisapointertoaninteger910a=7;11aPtr=&a;//aPtrsettoaddressofa1213cout<<"Theaddressofais"<<&a14<<"\nThevalueofaPtris"<<aPtr;1516cout<<"\n\nThevalueofais"<<a17<<"\nThevalueof*aPtris"<<*aPtr;1819cout<<"\n\nShowingthat*and&areinversesof"20<<"eachother.\n&*aPtr="<<&*aPtr21<<"\n*&aPtr="<<*&aPtr<<endl;22return0;23}输出结果:TheaddressofaisOx0064FDF4ThevalueofaPtris0x0064FDF4Thevalueofais7Thevalueof*aPtris7Showingthat*and&areinversesofeachother.&*aPtr=0x0064FDF4*&aPtr=0x0064FDF4图9.4&与*指针运算符------------------------------------------------------------------------------运算符结合律类型------------------------------------------------------------------------------()[]从左向右括号++--+-static_cast<type>()从右向左一元&**/%从左向右乘+-从左向右加<<>>从左向右插入/读取<<=>>=从左向右关系==!=从左向右相等&&从左向右逻辑AND||从左向右逻辑或?:从右向左条件=+=-=*=/=%=从右向左赋值,从左向右逗号------------------------------------------------------------------------------图9.5运算符的优先级和结合律9.4按引用调用函数C++用三种方式向函数传递数值:按值调用(call-by-value)、用引用参数按引用调用(call-by-referencereferenceargument)和用指针参数按引用调用(call-by-referencepointerargument)。第3章比较了按引用调用与按值调用,本章主要介绍用指针参数按引用调用。第3章曾介绍过,return可以从被调用函数向调用者返回一个值(或不返回值而从被调用函数返回控制)。我们还介绍了用引用参数将参数传递给函数,使函数可以修改参数的原有值(这样可以从函数“返回”多个值),或将大的数据对象传递给函数而避免按值调用传递对象的开销(即复制对象所需的开销)。指针和引用一样,也可以修改调用者的一个或几个变量,或将大的数据对象指针传递给函数而避免按值调用传递对象的开销。在C++中,程序员可以用指针和间接运算符模拟按引用调用(就像C语言程序中的按引用调用一样)。调用函数并要修改参数时,传递该参数地址,通常在要修改数值的变量名前面加上地址运算符(&)。第4章曾介绍过,数组不能用地址运算符(&)传递,因为数组名是内存中数组的开始位置(数组名等同于&arrayName[0]),即数组名已经是个指针。向函数传递参数地址时,可以在函数中使用间接运算符形成变量名的同义词、别名或浑名,并可用其修改调用者内存中该地址的值(如果变量不用const声明)。图9.6和9.7的程序是计算整数立方函数的两个版本cubeByValue和cubeByReference。图9.6按值调用将变量number传递给函数cubeByValue。函数cubeByValue求出参数的立方,并将新值用return语句返回main,井在main中将新值赋给number。可以先检查函数调用的结果再修改变量值。例如,在这个程序中,可以将cubeByValue的结果存放在另一变量中,检查其数值,然后再将新值赋给number。1//Fig.5,6:fig0506,cpp2//Cubeavariableusingcall-by-value3#include<iostream.h>45intcubeByValue(int);//prototype67intmain()8{9intnumber=5;1011cout<<"Theoriginalvalueofnumberis"<<number;12number=cubeByValue(number);13cout<<"\nThenewvalueofnumberis"<<number<<endl;14return0;15}1617intcubeByValue(intn){1819returnn*n*n;//cubelocalvariablen2O}输出结果:Theoriginalvalueofnumberis5Thenewvalueofnumberis125图9.6按值调用求出参数的立方图9.7的程序按引用调用传递变量nunber(传递number的地址)到函数cubeByReference。函数cubeByReference取nPtr(int的指针)作为参数。函数复引用指针并求出nPtr所指值的立方,从而改变main中的number值。图9.8和9.9分别分析了图9.6和9.7所示程序。1//Fig.9.7:fig05_07.cpp2//Cubeavariableusingcall-by-reference3//withapointerargument4#include<iostream.h>56voidcubeByReference(int*);//prototype78intmain()9{10intnumber=5;1112cout<<"Theoriginalvalueofnumberis"<<number;13cubeByReference(&number);14cout<<"\nThenewvalueofnumberis"<<number<<endl;15returnO;16}1718voidcubeByReference(int*nPtr)19{20*nPtr=*nPtr=*nptr**nptr;//cubenumberinmain21}输出结果:Theoriginalvalueofnumberis5Thenewvalueofnumberis125图9.7用指针参数按引用调用求出参数的立方接收地址参数的函数要定义接收地址的指针参数。例如,cubeByReference的函数首部如下所示:voidcubeByReference(int*nPtr)这个函数首部指定函数cubeByReferenee接收整型变量的地址(即整型指针)作为参数,在nPtr中局部存放地址,不返回值。cubeByReference的函数原型包含括号中的int*。和其他变量类型一样,不需要在函数原型中包括指针名。参数名仅用于程序中的说明,编译器将其忽略。在需要单下标数组参数的函数首部和函数原型中,可以用cubeByReference参数表中的指针符号。编译器并不区分接收指针的函数和接收单下标数组的函数。当然,函数必须“知道”何时接收数组或要进行按引用调用的单个变量。编译器遇到形如intb[]的单下标数组函数参数时,编译器将参数变为指针符号int*constb(b是指向整数的常量指针),const见第51节介绍。声明函数参数为单下标数组的两种形式可以互换。图9.8典型的按值调用分析图9.9典型的用指针参数按引用调用分析9.5指针与常量限定符const限定符可以使程序员告诉编译器特定变量的值不能修改。几年来,大量C语言遗留代码都是在没有const限定符的情况下编写的。因此,使用旧版C语言代码的软件工程有很大的改进空间。许多目前使用ANSIC和C++的程序员也没有在程序中使用const限定符,因为他们是从C语言的早期版本开始编程的,这些程序员错过了许多改进软件工程的好机会。函数参数使用或不用const限定符的可能性有六种,两种用按值调用传递参数,四种按引用调用传递参数,根据最低权限原则来进行选择。在参数中向函数提供完成指定任务所需的数据访问,但不要提供更多权限。前面曾经介绍,按值调用传递参数时,函数调用中要生成参数副本并将其传递给函数。如果函数中修改副本,则调用者的原值保持不变。许多情况下,需要修改传入函数的值以使函数能够完成任务。但有时即使被调用函数只是操作原值的副本,也不能在被调用函数中修改这个值。假设函数取一个单下标数组及其长度为参数,并打印数值。这种函数应对数组进行循环并分别输出每个数组元素。函数体中用数组长度确定数组的最高下标,以便在打印完成后结束循环。在函数体中不能改变这个数组长度。将指针传递给函数有四种方法:非常量数据的非常量指针、常量数据的非常量指针、非常量数据的常量指针和常量数据的常量指针。每种组合提供不同的访问权限。最高访问权限是非常量数据的非常量指针,可以通过复引用指针而修改,指针可以修改成指向其他数据。声明非常量数据的非常量指针时不用const。这种指针可以接收函数中的字符串,用指针算法处理或修改字符串中的每个字符。图9.10中的函数convertToUppercase声明参数sPtr(char*sPtr)为非常量数据的非常量指针。函数用指针算法一次一个字符地处理字符串string。字符串中,st到,x,的字符用函数toupper变为相应的大写字母,其余字符不变。函数toupper取一个字符作为参数。如果是小写字母,则返回相应的大写字母,否则返回原字符。函数toupper是字符处理库ctype.h中(见第16章)的一部分。常量数据的非常量指针,指针可以修改成指向其他数据,但数据不能通过指针修改。这种指针可以接收函数的数组参数,函数处理数组每个元素而不修改数据。例如,图9.1l的函数printCharacters将参数sPtr声明为constchar*类型.表示“sPtr是字符常量的指针”。函数体用for循环输出字符串中的每个字符,直到遇到null终止符。打印每个字符之后,指针sPtr递增,指向字符串中下一个宇符。1//Fig.9.10:fig0510.cpp2//Convertinglowercaseletterstouppercaseletters3//usinganon-constantpointertonon-constantdata4#include<iostream.h>5#include<ctype.h>67voidconvertToUppercase(char*);89intmain()10{11charstring[]="charactersand$32.98";1213cout<<"Thestringbeforeconversionis:"<<string;14convertToUppercase(string);15cout<<"\nThestringafterconversionis:"16cout<<string<<endl;17return0;18}1920voidconvertToUppercase{char*sPtr)21{22while(*sPtr!='\0'){2324if(*sPtr>='a'&&*sPtr<='z')25*sPtr=toupper(*sPtr);//converttouppercase2627++sPtr;//movesPtrtothenextcharacter28}29}输出结果:Thestringbeforeconversionis:charactersand$32.98Thestringafterconversionis:CHARACTERSAND$32.98图9.10将字符串变成大写1//Fig.9.11:fig05ll.cpp2//Printingastringonecharacteratatimeusing3//anon-constantpointertoconstantdata4#include<iostream.h>56voidprintCharacters(constchar*);78intmain()9{10charstring[]="printcharactersofastring";1112cout<<"Thestringis:\n";13printCharacters(string);14cout<<endl;15return0;16}1718//InprintCharacters,sPtrisapointertoacharacter19//constant.CharacterscannotbemodifiedthroughsPtr20//(i.e.,sPtrisa"read-only"pointer).21voidprintCharacters(constchar*sPtr)22{23for(;*sPtr!='\0';sPtr++)//noinitialization24cout<<*sPtr;25}输出结果:Thestringis:printcharactersofastring图9.11用常量数据的非常量指针打印字符串(一次打印一个字符)图9.12演示了函数接收常量数据的非常量指针,并试图通过指针修改数据在编译时产生的语法错误消息。1//Fig.9.12:fig05_12.cpp2//Attemptingtomodifydatathrougha3//non-constantpointertoconstantdata.4#include<iostream.h>56voidf(constint*);78intmain()9{10inty;1112f(&y);//fattemptsillegakmodification1314return0;15}1617//Inf,xPtrisapoinertoanintegerconstant18voidf(constint*xPtr)19{20*xPtr=100;//cannotmodifyaconstobject21}输出结果:CompilingFIG0512.CPP:ErrorFIG0512.CPP20:CannotmodifyaconstobjectWarningFIGOS_12.CPP21:Parameter'xPtr'isneverused图9.12试图通过常量数据的非常量指针修改数据众所周知,数组是累计数据类型,用同一名称存放相同类型的相关数据项。第6章将介绍另一种形式的累计数据类型——结构(structure),也称为记录(record)。结构可以用同一名称存放不同类型的相关数据项(例如,存放公司每个员工的信息)。调用带数组参数的函数时,数组模拟按引用调用自动传递给函数。但结构则总是按值调用,传递整个结构的副本。这就需要复制结构中每个数据项目并将其存放在计算机函数调用堆栈中的执行时开销(函数执行时用函数调用堆栈存放函数调用中使用的局部变量)。结构数据要传递绐函数时,可以用常量数据的指针(或常量数据的引用)得到按引用调用的性能和按值调用对数据的保护。传递结构的指针时,只要复制存放结构的地址。在4字节地址的机器上,只要复制4字节内存而不是复制结构的几百或几千字节。非常量数据的常量指针总是指向相同的内存地址,该地址中的数据可以通过指针修改。这里的数组名是默认的。数组名是数组开头的常量指针,数组中的所有数据可以用数组名和数组下标访问和修改。非常量数据的常量指针可以接收数组为函数参数,该函数只用数组下标符号访问数组元素。声明为const的指针应在声明时初始化(如果是函数参数,则用传入函数的指针初始化)。图9.13的程序想修改常量指针,指针ptr的类型声明为int*const,图中的声明表示“ptr是整数的常量指针”,指针用整型变量x的地址初始化。程序要将y的地址赋给ptr,但产生一个错误消息。注意数值7赋给*ptr时不产生错误,说明ptr所指的值是可修改的。常量数据的常量指针的访问权限最低。这种指针总是指向相同的内存地址,该内存地址的数据不能修改。数组传递到函数中,该函数只用数组下标符号读取,而不能修改数组。图9.14的程序演示声明指针变量ptr为constint*const,表示“ptr是常量整数的常量指针”。图中显示了修改ptr所指数据和修改存放指针变量的地址时产生的错误消息。注意输出ptr所指的值时不产生错误,因为输出语句中没有进行修改。1//Fig.9.13:fig0513.cpp2//Attemptingtomodifyaconstantpointerto3//non-constantdata4#include<iostream.h>56intmain()7{8intx,y;910int*constptr=&x;//ptrisaconstantpointertoan11//integer.Anintegercanbemodified12//throughptr,butptralwayspoints13//tothesamememorylocation.14*ptr=7;15ptr=&y;1617return0;18}输出结果:CompilingFIG0513.CPP:ErrorFIG0513.CPP15:CannotmodifyaconstobjectWarningFIGOS_13.CPP18:'y'isdeclaredbutneverused图9.13修改非常量数据的常量指针1//Fig.9.141fig0514.cpp2//Attemptingtomodifyaconstantpointerto3//constantdata.4#include<iostream.h>56intmain()7{8intx=5,y;910constiht*constptr=&x;//ptrisaconstantpointertoa11//constantinteger,ptralways12//pointstothesamelocation13//andtheintegeratthat14//locationcannotbemodified.15eout<<*ptr<<endl;16*ptr=7;17ptr=&y;1819return0;20}输出结果:CompilingFIG0514.CPP:ErrorFIG05_14.CPP16:CannotmodifyaconstobjectErrorFIG05_14.CPP17:CannotmodifyaconstobjectWarningFIG05_14.CPP20:'y'isdeclaredbutneverused图9.14修改常量数据的常量指针9.6按引用调用的冒泡排序下面将图8.16的冒泡排序程序修改成用两个函数bubbleSort和swap(如图9.15)。函数bubbleSort进行数组排序,它调用函数swap,变换数组元素array[j)和array[j+1]记住,C++强制函数之间的信息隐藏,因此swap并不能访问bubbleSort中的各个元素。由于bubbleSort要求swap访问交换的数组元素,因此bubbleSort要将这些元素按引用调用传递给swap.每个数组元素的地址显式传递。尽管整个数组自动按引用调用传递,但各个数组元素是标量,通常按值调用传递。因此,bubbleSort对swap调用中的每个数组元素使用地址运算符(&),如下所示的语句:swap(&array[j],array[j+1]);实现按引用调用。函数swap用指针变量element1Ptr接收&array[j]。由于信息隐藏,swap并不知道名称&array[j],但swap可以用*element1Ptr作为&array[j]的同义词。这样,swap引用*element1Ptr时,实际上是引用bubbleSort中的&array[j]。同样,swap引用*element2Ptr时,实际上是引用bubbleSort中的array[j+1]。虽然swap不能用:hold=array[j];array[j]=array[j+1];array[j+1]=hold;但图9.15中的swaP函数用hold=*element1Ptr;*element1Ptr=*element2Ptr;*element2Ptr=hold;达到相同的效果。1//Fig.9.15:fig05_15.cpp2//Thisprogramputsvaluesintoanarray,sortsthevaluesinto3//ascendingorder,andprintstheresultingarray.4#include<iostream.h>5#include<iomanip.h>67voidbubbleSort{int*,constint);89intmain(}l0{11constintarraySize=10;12inta[arraySize)={2,6,4,8,10,12,89,68,45,37};13inti;1415cout<<"Dataitemsinoriginalorder\n";1617for(i=0;t<arraySize;i++)18cout<<setw(4)<<a[i];1920bubbleSort(a,arraySize);//sortthearray21cout<<"\nDataitemsinascendingorder\n";2223for(i=0;i<arraySize;i++)24cout<<setw(4)<<a[i];2526cout<<endl;27return0;28}2930voidbubbleSort(int*array,constintsize)31{32voidswap(int*,iht*);3334for(intpass=0;pass<size-1;pass++)3536for(intj=0;j<size-1;j++)3738if(array[j]>array[j+1])39swap(&array[j],&arra[j+1]);40}4142voidswap(int*element1Ptr,int*element2Ptr)43{44inthold=*elementlPtr;45*element1Ptr=*element2Ptr;46*element2Ptr=hold;47}输出结果:Dataitem:inoriginalorder2648101289684537Dataitemsinascendinqorder2468l01237456889图9.15按引用调用的冒泡排序注意函数bubbleSort中的几个特性。函数首部中将array声明为int*array而不是intarray[],表示bubbleSort接收单下标数组作为参数(这些符号是可以互换的)。参数size声明为const以保证最低权限原则。尽管参数size接收main中数值的副本,且修改该副本并不改变main中的值,但是bubbleSort不必改变size即可完成任务。bubbleSort执行期间数组的长度保持不变,因此,size声明为const以保证不被修改。如果排序过程中修改数组长度,则排序算法无法正确执行。bubbleSort函数体中包括了函数swap的原型,因为它是调用swap的惟一函数。将原型放在bubbleSort中,使得只能从bubbleSort正确地调用swap。其他函数要调用swap时无法访问正确的函数原型,这通常会造成语法错误,因为C++需要函数原型。注意函数bubbleSort接收数组长度参数。函数必须知道数组长度才能排序数组。数组传递到函数时,函数接收数组第一个元素的内存地址。数组长度要单独传递给函数。通过将函数bubbleSort定义成接收数组长度作为参数,可以让函数在排序任何长度单下标整型数组的程序中使用。数组长度可以直接编程到函数内,这样会把函数的使用限制在特定长度的数组并减少其复用性。程序中只有处理特定长度的单下标整型数组时才能使用这个函数。C++提供一元运算符sizeof,确定程序执行期间的数组长度或其他数据类型长度(字节数)。采用数组名时(如图9.16所示),sizeof算符返回数组中的总字节数为size_t类型的值,通常是unsignedint类型。这里使用的计算机将float类型的变量存放在4字节内存中,array声明为20个元素,因此array使用80字节内存空间。在接收数组参数的函数中采用指针参数时,sizeof运算符返回指针长度的字节数(4)而不是数组长度。1//Fig.9.16:fig05_16.cpp2//Sizeofoperatorwhenusedonanarrayname3//returnsthenumberofbytesinthearray.4#include<iostream.h>56size_tgetSize(float*);78intmain()9{10floatarray[20];1112cout<<"Thenumberofbytesinthearrayis"13<<sizeof(array)14<<"\nThenumberofbytesreturnedbygetSizeis"15<<getSize(array)<<endl;1617return0;18}1920size_tgetSize(float*ptr)21{22returnsizeof(ptr);23}输出结果:Thenumberofbytesinthearrayis80ThenumberofbytesreturnedbygetSizeis4图9.16采用数组名时,sizeof运算符返回数组中的总字节数数组中的元素个数也可以用两个sizeof操作的结果来确定。例如,考虑下列数组声明:doublerealArray[22];如果double数据类型的变量存放在8个字节的内存中,则数组realArray总共包含176个字节。要确定数组中的元素个数,可以用下列表达式:sizeofrealArray/sizeof(double)这个表达式确定realArray数组中的字节数.并将这个值除以内存中存放—个double值的字节数,图9.17的程序用size运算符计算我们所用的个人计算机上存放每种标准数据类型时使用的字节数。1//Fig.9.17:fig0517.cpp2//Demonstratingthesizeofoperator3~include<iostream.h>4~include<iomanip.h>576~ntmain(}8charc;9shorts;10ihti;11long1;12floatf;13doubled;14longdoubleld;15intarras20],*ptr=array;1617cout<<"sizeofc="<<sizeofc18<<"\tsizeof(char)="<<sizeof(char)19<<"\nsizeofs="<<sizeofs20<<"\tsizeof(short)="<<sizeof(short)2I"\nsizeofi="<<sizeofi22<<"\tsizeof(int)="<<sizeof(int)23"\nsizeof1="<<sizeof124<<"\tsizeof(long)="<<sizeof(long)25<<"\nsizeoff="<<sizeoff27"\nsizeofd="<<sizofd28<<"\tsizeof(double)="<<sizeof(double)29<<"\nsizeofld="<<sizeofld30<<"\tsizeof(longdouble)="<<sizeof(longdouble)31<<"\nsizeofarray="<<sizeofarray32<<"\nsizeofptr="<<sizeofptr33<<endl;34return0;35}输出结果:sizeofc=1sizeof(char)=1sizeofs=2sizeof(short)=2sizeofi=4sizeof(int)=4sizeofl=4sizeof(long)=4sizeoff=4sizeof(float)=4sizeofd=8sizeof(double)=8sizeofld=8sizeof(longdouble)=8sizeofarray=sizeofptr=4图9.17用sizeof运算符计算存放每种标准数据类型时使用的字节数sizeof运算符可以用于任何变量名、类型名或常量值。用于变量名(不是数组名)和常量值时,返回存放特定变量或常量类型所用的字节数。注意,如果提供类型名操作数,则sizeof使用的括号是必需的;如果提供变量名操作数,则sizeof使用的括号不是必需的。记住,sizeof是个运算符而不是个函数。9.7指针表达式与指针算法指针是算术表达式、赋值表达式和比较表达式中的有效操作数。但是,通常并不是这些表达式中使用的所有运算符都在指针变量中有效。本节介绍可以用指针操作数的运算符及这些运算符的用法。只有少量操作可以对指针进行。指针可以自增(++)或自减(--),整数可以加进指针中(+或+=),也可以从指针中减去整数(-或-=),指针可以减去另一指针。假设声明了数组intv[5],其第一个元素位于内存地址3000。假设指针vPtr已经初始化为指向数组v[0],即vPtr的值为3000。图9.18演示了在32位的机器中的这种情况。注意vPtr可以初始化为数组v的指针,如下所示:vPtr=v;vPtr=&v[0]图9.18数组v和指向v的指针变量vPtr按照传统算法,3000+2得到3002。而指针算法通常不是这样。将指针增加或减去一个整数时,指针并不是直接增加或减去这个整数,而是加上指针所指对象长度的这个倍数。这些字节数取决于对象的数据类型。例如,下列语句:vPtr+=2;在用4字节内存空间存储整数时得到的值为3008(3000+2*4)。对数组v,这时vPtr指向v[2]如图9.19。如果用2字节内存空间,则上述结果得到3004(3000+2*2)。如果数组为不同数据类型,则上述语句将指针递增指针所指对象长度的2倍。对字符数组进行指针算法时,结果与普通算法相同,因为每个字符的长度为一个字节。图9.19经过指针运算之后的vPtr如果vPtr递增到3016,指向v[4],则下列语句:vptr-=4;将vPtr复位为3000,即数组开头。如果指针加1或减1,则可以用自增(++)和自减(--)运算符。下列语句:++vptr;vPtr++;将指针移到数组中的下一个位置。下列语句:--vPtr;vPtr--;将指针移到数组中的前一个位置。指针变量还可以相减。例如,如果vPtr包含地址3000,v2Ptr包含地址3008,则下列浯句:x=v2Ptr-vPtr;将x指定为vPtr到v2Ptr的元素个数,这里为2。指针算法只在对数组进行时才有意义。我们不能假设两个相同类型的变量在内存中相邻的地址存放,除非它们是数组的相邻元素。如果两个指针的类型相同,则可以将一个指针赋给另一个指针。否则要用强制类型转换运算符将赋值语句右边的指针值转换为赋值语句左边的指针值。这个规则的例外是void的指针(即void),该指针是个一般性指针,可以表示任何指针类型。所有指针类型都可以赋给void指针而不需要类型转换。但是,void指针不能直接赋给另一类型的指针,而要先将void指针转换为正确的指针类型。void*指针不能复引用。例如,编译器知道int指针指向32位机器中的4字节内存,但void指针只是包含未知数据类型的内存地址,指针所指的字节数是编译器所不知道的。编泽器要知道数据类型才能确定该指针复引用时的字节数。对于void指针,无法从类型确定字节数。指针可以用相等和关系运算符比较,但这种比较只在对相同数组成员进行时才有意义。指针比较是对指针存放的地址进行比较。例如,比较指向同一数组的两个指针可以表示一个指针所指的元素号比另一个指针所指的元素号更高。指针比较常用于确定指针是否为0。9.8指针与数组的关系C++中指针与数组关系密切,几乎可以互换使用。数组名可以看成常量指针,指针可以进行任何有关数组下标的操作。假设声明了整数数组b[5]和整数指针变量bPtr。由于数组名(不带下标)是数组第一个元素的指针.因此可以用下列语句将bPtr设置为b数组第一个元素的地址:bPtr=b;这等于取数组第一个元素的地址,如下所示:bPtr=&b[0];数组元素b[3]也可以用指针表达式引用:*(bPtr+3)上述表达式中的3是指针的偏移量(offset)。指针指向数组开头时,偏移量表示要引用的数组元素,偏移量值等于数组下标。上述符号称为指针/偏移量符号(pointer/offsetnotation)。括号是必需的,因为*的优先顺序高于+的优先顺序。如果没有括号,则上述表达式将表达式*bPtr的值加上3(即3加到b[0]中,假设bPtr指向数组开头)。就像数组元素可以用指针表达式引用一样,下列地址:&b[3]可以写成指针表达式:bPtr+3数组本身可以当作指针并在指针算法中使用。例如,下列表达式:*(b+3)同样引用数组元素b[3]。一般来说,所有带下标的数组表达式都可以写成指针加偏移量,这时使用指针/偏移量符号,用数组名作为指针。注意,上述语句不修改数组名,b还是指向数组中第一个元素指针和数组一样可以加下标。例如,下列表达式:bPtr[1]指数组元素b[1].这个表达式称为指针/下标符号(pointer/subscriptnotation)。记住,数组名实际上是个常量指针,总是指向数组开头。因此下列表达式:b+=3是无效的,因为该表达式试图用指针算法修改数组名的值。图9.20的程序用我们介绍的四种方法引用数组元素(数组下标、用数组名作为指针的指针/偏移量符号、指针下标和指针的指针/偏移量符号,打印数组的的4个元素)。要演示数组和指针的互换性,还可以看看程序9.21中的两个字符串复制函数copy1和copy2。这两个函数都是将字符串复制到字符数组中。比较copy1和copy2的函数原型可以发现,函数基本相同(由于数组和指针具有互换性)。这些函数完成相同的任务,但用不同方法实现。1//Fig.9.20:f~g05_20.cpp2//Usingsubscriptingandpointernotationswitharrays34#include<iostream.h>56intmain()7{8intb[]={10,20,30,40};9int*bPtr=b;//setbPtrtopointtoarrayb1011cout<<"Arraybprintedwith:\n"12<<"Arraysubscriptnotation\n";1314for(inti=0;i<4;i++),15cou<<"b["<<i<<"]=<<b[i]<<'\n';161718cout<<"\nPointer/offsetnotationwhere\n"19<<"thepointeristhearrayname\n";2O21for(intoffset=0;offset<4;offset++)22cout<<"*(b+"<<offset<<")="23<<*(b+offset)<<'\n';242526cout<<"\nPointersubscriptnotation\n";28for(i=0;i<4;i++)29cout<<"bPtr["<<i<<"]="<<bPtr[i]<<'\n';31cout<<"\nPointer/offsetnotation\n";3233for(offset=0;offset<4;offset++)34cout<<"*(bPtr+"<<offset<<")="35<<*(bPtr+offset)<<'\n';3637return0;38}输出结果:ArraybPrintedwith:ArraysubscriptnotationPointer/offsetnotationwherethepointeristhearrayname*(b+0)=10*(b+1)=20*(b+2)=30*(b+3)=40PointersubscriptnotationbPtr[0]=10bPtr[1]=20bPtr[2]=30bPtr{3]=40Pointer/offsetnotation*(bPtr+0)=10*(bPtr+1)=20*(bPtr+2)=30*(bPtr+2)=40图9.20用我们介绍的四种方法引用数组元素1//Fig.9.21:figOS_21.cpp2//Copyingastringusingarraynotation3//andpointernotation.4#include<iostream.h>56voidcopy1(char*,constchar*);7voidcopy2(char*,constchar*);89intmain()10{1112string3[10],string4[]="GoodBye";1314copy1(string1,string2);15cout<<"string1="<<string1<<endl;1617copy2(string3,string4);18cout<<"string3="<<string3<<endl';1920return0;21}2223//copys2toslusingarraynotation24voidcopy1(char*s1,constchar*s2)25{26for(inti=0;(s1[i]=s2[i])!='\0';i++)27;//donothinginbody28}2930//copys2toslusingpointernotation31voidcopy2(char*s1,constchar*s2)32{33for(;(*s1=*s2)!='\0';s1++,s2++)34;//donothinginbody35}输出结果:string1=Hellostring3=GoodBye图9.21使用数组和指针符号复制字符串函数copy1用数组下标符号将s2中的字符串复制到字符数组s1中。函数声明一个作为数组下标的整型计数器变量i。for结构的首部进行整个复制操作,而for结构体本身是个空结构。首部中指定i初始化为0,并在每次循环时加1。for的条件“(s1[i]=s2[i])!='\0',从s2向s1一次一个字符地进行复制操作。遇到s2中的null终止符时,将其赋给s1,循环终止,因为null终止符等于'\0'。记住.赋值语句的值是赋给左边参数的值。函数copy2用指针和指针算法将s2中的字符串复制到s1字符数组。同样是在for结构的首部进行整个复制操作.首部没有任何变量初始化。和copy1中一样,条件(*s1=*s1)!='\0'进行复制操作。复引用指针s2,产生的字符赋给复引用的指针s1。进行条件中的赋值之后,指针分别移到指向s1数组的下一个元素和字符串s2的下一个字符。遇到s2中的null终止符时,将其赋给s1,循环终止。注意copy1和copy2的第一个参数应当是足够大的数组,应能放下第二个参数中的字符串,否则可能会在写人数组边界以外的内存地址时发生错误。另外,注意每个函数中的第二个参数声明为constchar*(常量字符串)。在两个函数中,第二个参数都复制到第一个参数,一次一个地从第二个参数复制字符,但不对字符做任何修改。因此,第二个参数声明为常量值的指针,实施最低权限原则。两个函数都不需要修改第二个参数,因此不向这两个函数提供修改第二个参数的功能。9.9指针数组数组可以包含指针,这种数据结构的常见用法是构成字符串数组,通常称为字符串数组(stringarray)。字符串数组中的每项都是字符串,但在C++中,字符串实际上是第一个字符的指针。因此,字符串数组中的每项实际上是字符串中第一个字符的指针。下列字符串数组suit的声明可以表示一副牌:char‘*suit[4]={"Hearts","Diamonds","Clubs","Spades"};声明的suit[4]部分表示4个元素的数组。声明的char*部分表示数组suit的每个元素是char类型的指针。数组中的4个值为”Hearts'’、”Diamonds”、”Clubs”和”Spades”。每个值在内存中存放成比引号中的字符数多一个字符的null终上字符串。4个字符串长度分别为7、9、6、7。尽管这些字符串好像是放在suil数组中,其实数组中只存放指针(如图9.22)。每个指针指向对应字符串中的第一个字符。这样,尽管:suit数组是定长的,但可以访问任意长度的字符串,这是C++强大的数据结构功能所带来的灵活性。图9.22suit数组的图形表示suit字符串可以放在双下标数组中,每一行表示一个suit,每一列表示suit名的第一个字符、这种数据结构每一行应有固定列数,能够放下最长的字符串。因此,存放大量字符串而大部分字符串长度均比最长字符串短许多时,可能浪费很多内存空间。我们将在下一节用字符串数组帮助整理一副牌。9.10函数指针函数指针包含函数在内存中的地址。第4章介绍了数组名实际上是数组中第一个元素的内存地址。同样,函数名实际上是执行函数任务的代码在内存中的开始地址。函数指针可以传人函数、从函数返回、存放在数组中和赋给其他的函数指针。要演示如何使用函数指针,我们修改图9.15的冒泡排序程序,变成图9.26的程序。新程序包括main和函数bubble、swap、ascending和descending。函数bubbleSort接收ascending或descending函数的函数指针参数以及一个整型数组和数组长度。程序提示用户选择按升序或降序排序。如果用户输入1,则向函数bubble传递ascending函数的指针,使数组按升序排列。如果用户输入2,则向函数bubble传递descending函数的指针,使数组按降序排列。图9.27显示了示例的执行结果。1//Fig.9.26:fig0526.cpp2//Multipurposesortingprogramusingfunctionpointers3#include<iostream.h>4#include<iomanip.h>56voidbubble(int[],constint,int(*)(int,int));7ihtascending(int,int);8intdescending(int,int);910intmain()11{12constintarraySize=10;13intorder,14counter,15a[arraySize]={2,6,4,8,10,12,89,68,45,37};1617cout<<"Enter1tosortinascendingorder,\n"18<<"Enter2tosortdescendingorder:";19cin>>order;20cout<<"\nDataitemsinoriginalorder\n";2122for(counter=0;counter<arraySize;counter++)23cout<<setw(4)<<a[counter];2425if(order==1){26bubble(a,arraySize,ascending);27cout<<"\nDataitemsinascendingorder\n";29else{30bubble(a,arraySize,descending);31cout<<"\nDataitemsindescendingorder\n";32}3334for(counter=0;couner<arraySize;counter++)35cout<<setw(4)<<a[counter]3637cout<<endl;38return0;39}4041voidbubble(intwork[],constintsize,42int(*compare)(int,int))43{44voidswap(int*,int*);4546for(intpass=1;pass<size;pass++)4748for(intcount=0;count<size-1;count++)4950if((*compare)(work[count],work[count+1]))51swap(&work[count],&work[count+1]);52}5354voidswap(int*element1Ptr,int*element2Ptr)55{56inttemp;5758temp=*element1Ptr;59*element1Ptr=*element2Ptr;60*element2Ptr=temp;65returnb<a;//swapifbislessthana66}6768intdescending(inta,intb)69{70returnb>a;//swapifbisgreaterthana71}图9.26使用函数指针的多用途排序程序输出结果:Enter1tosortinascendingorder,Enter2tosortindescendingorder:1Dataitemsinoriginalorder2648101289684537Dataitemsinascendingorder2468101237456889Enter1tosortinascendinqorder,En(e[2tosortindescendingorder:1Dataitemsinoriqinalorder2648101289684537DatsitemsinascendinQorder8968453712109642图9.27使用函数指针的多用途排序程序的执行结果注意这里只包括类型,程序员可以加上名称.但参数名只用于程序中的说明,编译器将其忽略。if语句中调用传人bubble的函数,如下所示:if((*compare)(work[count],work[count+1]))就像复引用变量指针可以访问变量值一样,复引用函数指针可以执行这个函数。也可以不复引用指针而调用函数,如下所示:if(compare(work[count],work[count+1]))直接用指针作为函数名。我们更愿意使用第一种通过指针调用函数的方法,因为它显式说明compare是函数指针,通过复引用指针而调用这个函数。第二种通过指针调用函数的方法使compare好像是个实际函数。程序用户可能搞糊涂,想看看compare函数的定义却怎么也找不到。函数指针的一个用法是建立菜单驱动系统,提示用户从菜单选择一个选项(例如从1到5)。每个选项由不同函数提供服务,每个函数的指针存放在函数指针数组中。用户选项作为数组下标,数组中的指针用于调用这个函数。图9.28的程序提供了声明和使用函数指针数组的一般例子。这些函数(function1、function2和function3)都定义成取整数参数并且不返回值。这些函数的指针存放在数组f中,声明如下:void(*f[3])(int)={function1,function2,function3}声明从最左边的括号读起,表示f是3个函数指针的数组,各取整数参数并返回void。数组用三个函数名(是指针)初始化。用户输入0到2的值时,用这些值作为函数指针数组的下标。函数调用如下所示:(*f[choice])(choice);调用时,f[choice]选择数组中choice位置的指针。复引用指针以调用函数,并将choice作为参数传人函数中。每个函数打印自己的参数值和函数名,表示正确调用了这个函数。练习中要开发一个菜单驱动系统。1//Fig.9.28:fig05_28.cpp2//Demonstratinganarrayofpointerstofunctions3#include<iostream.h>4voidfunctionl(int);5voidfunction2(iht);6voidfunction3(int);78intmain()9{10void(*f[3])(int)={function1,function2,function3};11intchoice;1213cout<<"Enteranumberbetween0and2,3toend:";14cin>>choice;1516while(choice>=0&&choice<3){17(*f[choice])(choice);18cout<<"Enteranumberbetween0and2,3toend:";19cin>>choice;2O}2122cout<<"Programexecutioncompleted."<<endl;23return0;24}2526voidfunction1(inta)27{28cout<<"Youentered"<<a29<<"sofunction1wascalled\n\n";30}3132voidfunction2(intb)33{34cout<<"youentered"<<b35<<"sofunction2wascalled\n\n";34}3738voidfunction3(intc)39{40cout<<"Youentered"<<c41<<"sofunction3wascalled\n\n";42}输出结果:Enteranumberbetween0and2,3toend:0Youentered0sofunctionlwascalledEnteranumberbetween0and2,3toend:1Youentered1sofunction2wascalledEnteranumberbetween0and2,3toend:2Youentered2sofunction3wascalledEnteranumberbetween0and2,3toend:3Programexecutioncompleted图9.28声明和使用函数的指针数组9.11字符与字符串处理简介本节要介绍一些字符串处理的标准库函数。这里介绍的技术适用于开发文本编辑器、字处理器、桌面排版软件、计算机化打字系统和其他文本处理软件。我们这里使用基于指针的字符串,本书稍后还将介绍把字符串作为成熟的对象。9.11.1字符与字符串基础字符是C++编程语言的基本组件。每个程序都是由一系列字符用有意义的方式组合而成的,计算机将其解释为一系列指令,用来完成一组任务。程序可能包含字符常量(characterconstant)。字符常量是表示为单引号字符的整数值。字符常量的值是机器字符集中该字符的整数值。例如,'z'表示z的整数值(在ASCII字符集中为122),'\n'表示换行符的整数值(在ASCII字符集中为10)。字符串就是把—系列字符当作一个单元处理。字符串可能包含字母、数字和+、-、*、/、$等各种特殊字符(specialcharacter)。C++中的字符串直接量(stringliteral)或字符串常量(stringconstant)放在双引号中如下所示:“JohnQ.Doe”(姓名)“9999NainStreet”(街道)“Waltham,Massachusetts”(州)“(201)555—1212”(电话号码)C++中的字符串是以null终止符('\0')结尾的字符数组。通过字符串中第一个字符的指针来访问字符串。字符串的值是字符串中第一个字符的地址(常量),这样,c++中可以说字符串是个常量指针,是指向字符串中第一个字符的指针。从这个意义上说,字符串像数组一样,因为数组名也是第一个元素的(常量)指针。可以在声明中将字符串指定为字符数组或char*类型的变量。下列声明:charcolor[]="blue";char*ColorPtr="blue";分别将变量初始化为"blue"。第一个声明生成5个元素的数组color,包含字符'b'、'l'、'u'、'e'和'w'。第二个声明生成指针变量colorPtr,指向内存中的字符串"blue"。声明charcolor[]={"blue"};也可以改写成:charcolor[]={'b','l','u','e','\0'};声明包含字符串的字符数组时,数组应足够大,能存放字符串及其null终止符。上述声明自动根据初始化值列表中提供的初始化值的个数确定数组长度。字符数组中没有分配能存放字符串及其null终止符的足够空间。生成或使用不包含null终止符的字符串。字符串可以用cin通过流读取赋给数组。例如,下列语句将字符串赋给字符数组word[20]:cin>>word;用户输入的字符串存放在word中。上述语句读取字符,直到遇到空格、制表符、换行符或文件结束符。注意,字符串不能超过19个字符,因为还要留下nUll终止符的空间。第2章介绍的setw流操纵算子可以用于保证读取到word的字符串不超过字符数组长度。例如,下列语句:cin>>setw(20)>>word;指定cin最多读取19个字符到数组word中,并将数组第20个位置用于保存字符串的null终止符。setw流操作算子只能用于输入的下一个值。有时,可以将整行文本输入数组中。为此,C++提供了cin.getline函数。cin.getline函数取三个参数:存放该行文本的字符数组、长度和分隔符。例如,下列程序段:charsentence[80];cin.getline(sentence,80,'\n');声明80个字符的数组sentence,然后从键盘读取一行文本到该数组中。函数遇到分隔符'\n'、输入文件结束符或读取的字符数比第二个参数的长度少1时停止读取字符(最后一个字符位置用于保存字符串的null终止符)。如果遇到分隔符,则读取该分隔符并不再读入下一字符。cin.getline函数的第三个参数默认值为'\n',因此上述函数调用也可以改写成:cin.getline(sentence,80);第2章“C++输入/输出流”中详细介绍了cin.getline和其他函数。将单个字符作为字符串处理可能导致致命的运行时错误。字符串是指针,可以对应一个大整数。而单个字符是个小整数(0--255的ASCII值)在许多系统中.这会导致错误,因为低内存地址是用于特殊用途的,如操作系统中断处理器,因此会发生“访问无效”的错误。需要字符串参数时将字符传入函数可能导致致命的运行时错误。需要字符参数时将字符串传入函数是个语法错误。9.11.2字符串处理库的字符串操作函数字符串处理库提供许多操作字符串数据、比较字符串、搜索字符串中的字符与其他字符串、将字符串标记化(将字符串分成各个逻辑组件)和确定字符串长度的字符串操作函数。本节介绍字符串处理库(标准库)中常用的字符串操作函数。图9.29总结了这些函数。注意图9.29中的几个函数包含size_t数据类型的参数。这是在头文件<stddef.h)<标准库中的头文件,标准库中还有许多其他标准库头文件,包括<string.h,)中定义为unsignedint或unsignedlong之类的无符号整数类型。使用字符串处理库中的函数而不包括<string.h>头文件。函数stcpy将第二个参数(字符串)复制到第一个参数(字符数组)中,这个字符数组的长度应当足以放下字符串及其null终止符。函数strncpy与strcpy相似,只是strncpy指定从字符串复制到字符数组的字符数。注意函数strncpy不一定复制第二个参数的null终止符,null终止符要在复制的字符数比字符中长度至少多1时才复制。例如,如果第二个参数为“test”,则只在strncpy的第三个参数至少为5("test"的长度加null终止符)时才复制null终止符。如果第三个参数大于5,则数组后面添加null终止符,直到写入第三个参数指定的总字符数。函数原型函数说明char*strcpy(char*s1,constchar*s2)将字符串s2复制到字符数组s1中、返回s1的值char*strncpy(char*s1,char*s2,size_tn)将字符串s2中最多n个字符复制到字符数组s1中,返回s1的值char*strcat(char*s1,constchar*s2)将字符串s2添加到字符串s1后面。s2的第一个字符重定义s1的null终止符。返回s1的值char*strncat(char*s1,constchar*s2,size_tn)将字符串s2中最多n个字符添加到字符串s1后面。s2的第一个字符重定义s1的null终止符。返回s1的值intstrcmp(constchar*s1,constchar*s2)比较字符串s1与字符串s2函数在s1等于、小于或大于s2时分别返回0、小于0或大于0的值intstrncmp(constchar*s1,constchar*s2,size_tn)比较字符串s1中的n个字符与字符串s2。函数在s1等于、小于或大于s2时分别返回0、小于0或大于0的值char*strtok(char*s1,constchar*s2)用一系列strtok调用将s1字符串标记化(将字符串分成各个逻辑组件,如同一行文本中的每个单词),用字符串s2所包含的字符分隔。首次调用时包含s1为第一个参数,后面调用继续标记化同一字符串,包含NULL为第一个参数。每次调用时返回当前标记的指针。如果函数调用时不再有更多标记,则返回NULLsize_tstrlen(constchar*s)确定字符串的长度,返回null终止符之前的字符数图9.29字符串处理库的字符串操作函数第三个参数小于或等于第二个参数的宇符串长度时不在strncpy的第一个参数中添加null终止料可能造成严重的运行时错误。图9.30的程序用strcpy将数组x中的整个字符串复制到数组y中,并用strncpy将数组x的前14个字符复制到数组2中。将null字符('\0')添加到数组z,因为程序中调用strncpy时没有写入null终止符(第三个参数小于或等于第二个参数的字符串长度)。1//Fig.9.30:fig0530.cpp2//usingstrcpyandstrncpy3#include<iostream.h>4#include<string.h>56intmain()7{8charx[]="HappyBirthdaytoYou";9charx[25],Z[15];1011cout<<"Thestringinarrayxis:"<<x12<<"\nThestringinarrayyis:"<<strcpy(y,x)13<<'\n';14strncpy(z,x,14);//doesnotcopynullcharacter15z[14]='\0';16cout<<"Thestringinarrayzis:"<<z<<endl;1718return0;19}输出结果:Thestringinarrayxis:HappyBirthdaytoYouThestringinarrayyis:HappyBirthdaytoYouThestringinarrayzis:HappyBirthday图9.30使用stcpy和strncpy函数函数strcat将第二个参数(字符串)添加到第一个参数(字符数组)中。第二个参数的第一个字符代替终止第一个参数中字符串的null终止符('\0')。程序员要保证存放第一个字符串的数组应足以存放第一个字符串、第二个字符串和null终止符(从第二个字符串复制)的合并长度。函数strncat从第二个字符串添加指定字符数到第一个字符串中,并在结果中添加null终止符。图9.31的程序演示了函数stcat和strncat。图9.32用strcmp和strncmp比较三个字符串。函数strcmp一次一个字符地比较第一个字符串参数与第二个字符串参数。如果字符串相等,则函数返回0;如果第一个字符串小于第二个字符串,则函数返回负值;如果第一个字符串大于第二个字符串,则函数返回正值。函数strncmp等价于函数strcmp,只是strncmp只比较到指定字符数。函数strncmp不比较字符串中null终止符后面的字符。程序打印每次函数调用返回的整数值。假设strcmp和strncmp在参数相等时返回1是个逻辑错误。strcmp和strncmp在参数相等时返回0(C++的false值)。因此测试两个字符串的相等性时,strcmp和strncmp的结果应与0比较,确定字符串是否相等。1//Fig.9.31:fig0531.cpp2//usingstrcatandstrncat3#include<iostream.h>4#include<string.h>56intmain()7{8chars1[20]="Happy";9chars2[]="NewYear";10chars3[40]="";1112cout<<"s1="<<s1<<"\ns2="<<s2;13cout<<"\nstrcat(s1,S2)="<<strcat(s1,s2);14cout<<"\nstrncat(s3,s1,6)="<<strncat(S3,s1,6);15cout<<"\nstrcat(s3,s1)="<<strcat(S3,s1)<<endl;1617return0;18}输出结果:s1=Happys2=NewYearstrcat(sl,s2)=HappyNewYear$trncat{s3,s1,6)=Happystrcat(s3,s1)=HappyHappyNewYear图9.31使用strcat和strncat函数1//Fig.9.32:fig0532.cpp23#include<iostream.h>4#include<iomanip.h>5#include<string.h>67intmain()8{9char*s1="HappyNewYear";10char*s2="HappyNewYear";11char*s3="HappyHolidays";1213cout<<"s1="<<s1<<"\ns2="<<s214<<"\ns3="<<s3<<"\n\nstrcmp(s1,S2)="15<<setw(2)<<strcmp(s1,s2)16<<"\nstrcmp(s1,s3)="<<setw(2)17<<strcmp(s1,s3)<<"\nstrcmp(s3,s1)="18<<setw(2)<<strcmp(s3,s1);1920cout<<"\n\nstrncmp(s1,s3,6)="<<setw(2)21<<strncmp(si,s3,6)<<"\nstrncmp(sl,s3,7)="22<<setw(2)<<strncmp(s1,s3,7)23<<"\nstrncmp(s3,s1,7)="24<<setw(2)<<strncmp(s3,s1,7)<<endl;25return0;26}输出结果:s1=HappyNewYears2=HappyNewYears3=HappyHolidaysstrcmp(s1,s2)=0strcmp(s1,s3)=1strcmp(s3,s1)=-1strncmp(s1,s3,6)=0strncmp(s1,s3,7)=1strncmp(s3,s1,7)=-1图9.32使用strcmp和strncmp函数要了解一个字符串大于或小于另一字符串的含义,可以考虑一系列姓氏的字母顺序表。读者一定会把“Jones"放在“Smith"之前,因为"Jones"的第一个字母在"Smith"的第一个字母之前。但字母表中不仅有26个字母,而是个字母顺序表,每个字母在表中有特定位置。“z”不仅表示字母,而且是字母表中第二十六个字母。计算机怎么知道一个字母在另一字母之前呢?所有字符在计算机中均表示为数字代码,计算机比较两个字符串时,实际上是比较字符串中字符的数字代码。ASCII和EBCDIC称为字符编码(charactercode)或字符集(characterset)。字符串和字符操作实际上是在操作相应的数字代码,而不是操作字符本身。因此C++中字符和小整数具有互换性。由于数字代码之间有大于、等于、小于的关系,因此可以将不同字符或字符串通过字符编码相互比较。附录A列出了ASCII字符编码。函数strtok将字符串分解为一系列标记(token)标记就是一系列用分隔符(delimitingchracter,通常是空格或标点符号)分开的字符。例如,在一行文本中,每个单词可以作为标记,空格是分隔符。需要多次调用strtok才能将字符串分解为标记(假设字符串中包含多个标记)。第一次调用strtok包含两个参数,即要标记化的字符串和包含用来分隔标记的字符的字符串(即分隔符):在图9.33的例子中,下列语句:tokenPtr=Strtok(string"");将tokenPtr赋给string中第一个标记的指针。strtok的第二个参数””表示string中的标记用空格分开。函数strtok搜索string中不是分隔符(空格)的第一个字符,这是第一个标记的开头。然后函数寻找字符串中的下一个分隔符,将其换成null(,w,)字符,这是当前标记的终点。函数strtok保存string中标记后面的下一个字符的指针,并返回当前标记的指针。后面再调用strtok时,第一个参数为NULL,继续将string标记化。NULL参数表示调用strtok继续从string中上次调用strtok时保存的位置开始标记化。如果调用strtok时已经没有标记,则strtok返回NULL。图9.33的程序用strtok将字符串”Thisissentencewith7tokens”标记化。分别打印每个标记。注意strtok修改输入字符串,因此,如果调用strtok之后还要在程序中使用这个字符串,则应复制这个字符串。没有认识到strtok修改正在标记化的字符串,调用sstrtok后还在程序中使用这个字符串(以为还是原字符串)函数strlen取一个字符串作为参数,并返回字符串中的字符个数,长度中不包括null终止符。图9.34的程序演示了函数strlen。1//Fig.9.33:fig0533.cpp2//Usingstrtok3#include<iostream.h>4#include<string.h>56intmain()7{8charstring[]="Thisisasentencewith7tokens";9char*tokenPtr;1011cout<<"Thestringtobetokenizedis:\n"<<string12<<"\n\nThetokensare:\n";1314tokenPtr=strtok(string,"");1516while(tokenPtr!=NULL){17cout<<tokenPtr<<'\n';18tokenPtr=strtok(NULL,"");19}2021return0;22}输出结果:Thestringtobetokenizedis:Thisisasentencewith7tokensThetokensare:Thisisasentence7tokens图9.33使用strtok函数1//Fig.9.34:fig05_34.cpp2//Usingstrlen3#include<iostream>4#include<string.h>56intmain()7{8char*string1="abcdefghijklmnopqrstuvwxyz";9char*string2="four";10charstring3="Boston";1112cout<<"Thelengthof\""<<string113<<"\"is"<<strlen(string1)14<<"\nThelengthof\""<<string215<<"\"is"<<strlen(string2)16<<"\nThelengthof\""<<string317<<"\"is"<<strlen(string3)<<endl;1819return0;20}输出结果:Thelengthof"abcdefghijklmnopqrstuvwxyz"is26Thelengthof"four"is4图9.34使用strlen函数第10章信息学奥赛中的常用算法10.1算法简介学习过程序设计的人对算法这个词并不陌生,从广义上讲,算法是指为解决一个问题而采用的方法和步骤;从程序计设的角度上讲,算法是指利用程序设计语言的各种语句,为解决特定的问题而构成的各种逻辑组合。我们在编写程序的过程就是在实施某种算法,因此程序设计的实质就是用计算机语言构造解决问题的算法。算法是程序设计的灵魂,一个好的程序必须有一个好的算法,一个没有有效算法的程序就像一个没有灵魂的躯体。算法具有五个特征:1、有穷性:一个算法应包括有限的运算步骤,执行了有穷的操作后将终止运算,不能是个死循环;2、确切性:算法的每一步骤必须有确切的定义,读者理解时不会产生二义性。并且,在任何条件下,算法只有唯一的一条执行路径,对于相同的输入只能得出相同的输出。如在算法中不允许有“计算8/0”或“将7或8与x相加”之类的运算,因为前者的计算结果是什么不清楚,而后者对于两种可能的运算应做哪一种也不知道。3、输入:一个算法有0个或多个输入,以描述运算对象的初始情况,所谓0个输入是指算法本身定义了初始条件。如在5个数中找出最小的数,则有5个输入。4、输出:一个算法有一个或多个输出,以反映对输入数据加工后的结果,这是算法设计的目的。它们是同输入有着某种特定关系的量。如上述在5个数中找出最小的数,它的出输出为最小的数。如果一个程序没有输出,这个程序就毫无意义了;5、可行性:算法中每一步运算应该是可行的。算法原则上能够精确地运行,而且人能用笔和纸做有限次运算后即可完成。如何来评价一个算法的好坏呢?主要是从两个方面:一是看算法运行所占用的时间;我们用时间复杂度来衡量,例如:在以下3个程序中,(1)x=x+1(2)for(i=1;i<=n;i++)x=x+1(3)for(i=1;i<=n;i++)For(j=1;j<=n;j++)x=x+1含基本操作“x增1”的语句x:=x+1的出现的次数分别为1,n和n2则这三个程序段的时间复杂度分别为O(1),O(n),O(n2),分别称为常量阶、线性阶和平方阶。在算法时间复杂度的表示中,还有可能出现的有:对数阶O(logn),指数阶O(2n)等。在n很大时,不同数量级的时间复杂度有:O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n),很显然,指数阶的算法不是一个好的算法。二是看算法运行时所占用的空间,既空间复杂度。由于当今计算机硬件技术发展很快,程序所能支配的自由空间一般比较充分,所以空间复杂度就不如时间复杂度那么重要了,有许多问题人们主要是研究其算法的时间复杂度,而很少讨论它的空间耗费。时间复杂性和空间复杂性在一定条件下是可以相互转化的。在中学生信息学奥赛中,对程序的运行时间作出了严格的限制,如果运行时间超出了限定就会判错,因此在设计算法时首先要考虑的是时间因素,必要时可以以牺牲空间来换取时间,动态规划法就是一种以牺牲空间换取时间的有效算法。对于空间因素,视题目的要求而定,一般可以不作太多的考虑。我们通过一个简单的数值计算问题,来比较两个不同算法的效率(在这里只比较时间复杂度)。例:求N!所产生的数后面有多少个0(中间的0不计)。算法一:从1乘到n,每乘一个数判断一次,若后面有0则去掉后面的0,并记下0的个数。为了不超出数的表示范围,去掉与生成0无关的数,只保留有效位数,当乘完n次后就得到0的个数。(C++程序如下)//======================================//程序名:N!.cpp//程序说明:求N!所产生的数后面有多少个0//作者:杨时龙//编写日期:2011.2.22//------------------------------------------------#include<iostream>#include<fstream>#include<sstream>//------------------------------------------------usingnamespacestd;//------------------------------------------------intmain(){longinti,t,n,sum;t=0;sum=1;cin>>n;for(i=1;i<=n;i++){sum=sum*i;while(sum%10==0){sum=sum/10;t++;}sum=sum%1000;}cout<<t<<endl;system("pause");//等待按任意键结束}//======================================算法二:此题中生成O的个数只与含5的个数有关,n!的分解数中含5的个数就等于末尾O的个数,因此问题转化为直接求n!的分解数中含5的个数。//======================================//程序名:N!2.cpp//程序说明:求N!所产生的数后面有多少个0//作者:杨时龙//编写日期:2011.2.22//------------------------------------------------#include<iostream>#include<fstream>#include<sstream>//------------------------------------------------usingnamespacestd;//------------------------------------------------intmain(){intt,n;t=0;cin>>n;do{n=n/5;t=t+n;}while(n>5);cout<<t<<endl;system("pause");//等待按任意键结束}//======================================分析对比两种算法就不难看出,它们的时间复杂度分别为O(N)、O(logN),算法二的执行时间远远小于算法一的执行时间。在信息学奥赛中,其主要任务就是设计一个有效的算法,去求解所给出的问题。如果仅仅学会一种程序设计语言,而没学过算法的选手在比赛中是不会取得好的成绩的,选手水平的高低在于能否设计出好的算法。下面,我们根据全国分区联赛大纲的要求,一起来探讨信息学奥赛中的基本算法。10.2枚举算法枚举法,常常称之为穷举法,是指从可能的集合中一一枚举各个元素,用题目给定的约束条件判定哪些是无用的,哪些是有用的。能使命题成立者,即为问题的解。采用枚举算法解题的基本思路:(1)确定枚举对象、枚举范围和判定条件;(2)一一枚举可能的解,验证是否是问题的解下面我们就从枚举算法的的优化、枚举对象的选择以及判定条件的确定,这三个方面来探讨如何用枚举法解题。例1:百钱买百鸡问题:有一个人有一百块钱,打算买一百只鸡。到市场一看,大鸡三块钱一只,小鸡一块钱三只,不大不小的鸡两块钱一只。现在,请你编一程序,帮他计划一下,怎么样买法,才能刚好用一百块钱买一百只鸡?算法分析:此题很显然是用枚举法,我们以三种鸡的个数为枚举对象(分别设为x,y,z),以三种鸡的总数(x+y+z)和买鸡用去的钱的总数(x*3+y*2+z)为判定条件,穷举各种鸡的个数。下面是解这个百鸡问题的程序//======================================//程序名:chicken.cpp//程序说明:百钱习百鸡//作者:杨时龙//编写日期:2011.2.19//------------------------------------------------#include<iostream>#include<fstream>#include<sstream>//------------------------------------------------usingnamespacestd;//------------------------------------------------intmain(){intx,y,z;for(x=0;x<=100;x++)for(y=0;y<=100;y++)for(z=0;z<=100;z++)//枚举所有可能的解{if((x+y+z==100)&&(x*3+y*2+z/3==100)&&(z%3==0)){cout<<"x="<<x<<""<<"y="<<y<<""<<"z="<<z<<endl;}}system("pause");}//======================================上面的条件还有优化的空间,三种鸡的和是固定的,我们只要枚举二种鸡(x,y),第三种鸡就可以根据约束条件求得(z=100-x-y),这样就缩小了枚举范围,请看下面的程序:intmain(){intx,y,z;for(x=0;x<=100;x++)for(y=0;y<=100-x;y++){z=100-x-y;if((x+y+z==100)&&(x*3+y*2+z/3==100)&&(z%3==0))cout<<"x="<<x<<""<<"y="<<y<<""<<"z="<<z<<endl;}system("pause");}未经优化的程序循环了1013次,时间复杂度为O(n3);优化后的程序只循环了(102*101/2)次,时间复杂度为O(n2)。从上面的对比可以看出,对于枚举算法,加强约束条件,缩小枚举的范围,是程序优化的主要考虑方向。在枚举算法中,枚举对象的选择也是非常重要的,它直接影响着算法的时间复杂度,选择适当的枚举对象可以获得更高的效率。如下例:例2、将1,2...9共9个数分成三组,分别组成三个三位数,且使这三个三位数构成1:2:3的比例,试求出所有满足条件的三个三位数.例如:三个三位数192,384,576满足以上条件.(NOIP1998pj)算法分析:这是1998年全国分区联赛普及组试题(简称NOIP1998pj,以下同)。此题数据规模不大,可以进行枚举,如果我们不加思地以每一个数位为枚举对象,一位一位地去枚举:for(a=1;a<=9;a++)for(b=1;b<=9;b++)………For(i=1;i<=9;i++)这样下去,枚举次数就有99次,如果我们分别设三个数为x,2x,3x,以x为枚举对象,穷举的范围就减少为93,在细节上再进一步优化,枚举范围就更少了。程序如下:Intmain(){Intt,x;Strings,st;Charc;For(x=123;x<=321;x++)//枚举所有可能的解{t=0;str(x,st);//把整数x转化为字符串,存放在st中str(x*2,s);st=st+s;str(x*3,s);st=st+s;for(c=’c’;c<=’9’;c++)//枚举9个字符,判断是否都在st中if(pos(c,st)!=0)t++;elsebreak;//如果不在st中,则退出循环if(t==9)cout<<x<<”“<<x*2<<”“<<x*3<<endl;}}在枚举法解题中,判定条件的确定也是很重要的,如果约束条件不对或者不全面,就穷举不出正确的结果, 我们再看看下面的例子。例3一元三次方程求解(noip2001tg)问题描述有形如:ax3+bx2+cx+d=0这样的一个一元三次方程。给出该方程中各项的系数(a,b,c,d均为实数),并约定该方程存在三个不同实根(根的范围在-100至100之间),且根与根之差的绝对值>=1。要求由小到大依次在同一行输出这三个实根(根与根之间留有空格),并精确到小数点后2位。提示:记方程f(x)=0,若存在2个数x1和x2,且x1<x2,f(x1)*(x2)<0,则在(x1,x2)之间一定有一个根。样例输入:1-5-420输出:-2.002.005.00算法分析:由题目的提示很符合二分法求解的原理,所以此题可以用二分法。用二分法解题相对于枚举法来说很要复杂很多。此题是否能用枚举法求解呢?再分析一下题目,根的范围在-100到100之间,结果只要保留两位小数,我们不妨将根的值域扩大100倍(-10000<=x<=10000),再以根为枚举对象,枚举范围是-10000到10000,用原方程式进行一一验证,找出方程的解。有的同学在比赛中是这样做Intmain(){Intk;doublea,b,c,d,x;Cin>>a>>b>>c>>d;For(k=-10000;k<=10000;k++){x=k/100;if(a*x*x*x+b*x*x+c*x+d==0)cout<<x<<endl;}}用这种方法,很快就可以把程序编出来,再将样例数据代入测试也是对的,等成绩下来才发现这题没有全对,只得了一半的分。这种解法为什么是错的呢?错在哪里?前面的分析好象也没错啊,难道这题不能用枚举法做吗?看到这里大家可能有点迷惑了。在上面的解法中,枚举范围和枚举对象都没有错,而是在验证枚举结果时,判定条件用错了。因为要保留二位小数,所以求出来的解不一定是方程的精确根,再代入ax3+bx2+cx+d中,所得的结果也就不一定等于0,因此用原方程ax3+bx2+cx+d=0作为判断条件是不准确的。我们换一个角度来思考问题,设f(x)=ax3+bx2+cx+d,若x为方程的根,则根据提示可知,必有f(x-0.005)*(x+0.005)<0,如果我们以此为枚举判定条件,问题就逆刃而解。另外,如果f(x-0.005)=0,哪么就说明x-0.005是方程的根,这时根据四舍5入,方程的根也为x。所以我们用(f(x-0.005)*f(x+0.005)<0)和(f(x-0.005)=0)作为判定条件。为了程序设计的方便,我们设计一个函数f(x)计算ax3+bx2+cx+d的值,程序如下:Intk;doublea,b,c,d,x;Doublef(x:double)//计算ax3+bx2+cx+d的值{f=((a*x+b)*x+c)*x+d;}Intmain(){Cin>>a>>b>>c>>d;For(k=-10000;k<=10000;k++){x=k/100;if((f(x-0.005)*f(x+0.005)<0)or(f(x-0.005)==0))//若x两端的函数值异号或x-0.005刚好是方程的根,则确定x为方程的根cout<<x<<endl;}}用枚举法解题的最大的缺点是运算量比较大,解题效率不高,如果枚举范围太大(一般以不超过两百万次为限),在时间上就难以承受。但枚举算法的思路简单,程序编写和调试方便,比赛时也容易想到,在竞赛中,时间是有限的,我们竞赛的最终目标就是求出问题解,因此,如果题目的规模不是很大,在规定的时间与空间限制内能够求出解,那么我们最好是采用枚举法,而不需太在意是否还有更快的算法,这样可以使你有更多的时间去解答其他难题。10.3回溯算法如果上期的“百钱买百鸡”中鸡的种类数是变化的,用枚举法就无能为力了,这里介绍另一种算法——回溯法。回溯法是一种既带有系统性又带有跳跃性的搜索法,它的基本思想是:在搜索过程中,当探索到某一步时,发现原先的选择达不到目标,就退回到上一步重新选择。它主要用来解决一些要经过许多步骤才能完成的,而每个步骤都有若干种可能的分支,为了完成这一过程,需要遵守某些规则,但这些规则又无法用数学公式来描述的一类问题。下面通过实例来了解回溯法的思想及其在计算机上实现的基本方法。例1、从N个自然数(1,2,…,n)中选出r个数的所有组合。算法分析:设这r个数为a1,a2,…ar,把它们从大到小排列,则满足:(1)a1>a2>…>ar;(2)其中第i位数(1<=i<=r)满足ai>r-i;我们按以上原则先确定第一个数,再逐位生成所有的r个数,如果当前数符合要求,则添加下一个数;否则返回到上一个数,改变上一个数的值再判断是否符合要求,如果符合要求,则继续添加下一个数,否则返回到上一个数,改变上一个数的值……按此规则不断循环搜索,直到找出r个数的组合,这种求解方法就是回溯法。如果按以上方法生成了第i位数ai,下一步的的处理为:(1)若ai>r-i且i=r,则输出这r个数并改变ai的值:ai=ai-1;(2)若ai>r-i且i≠r,则继续生成下一位ai+1=ai-1;(3)若ai<=r-i,则回溯到上一位,改变上一位数的值:ai-1=ai-1-1;算法实现步骤:第一步:输入n,r的值,并初始化;i=1;a[1]=n;第二步:若a[1]>r-1则重复:若a[i]>r-i,①若i==r,则输出解,并且a[i]=a[i]-1;②若i!=r,则继续生成下一位:a[i+1]=a[i]-1;i=i+1;若a[i]<=r-i,则回溯:i=i-1;a[i]=a[i]-1;第三步:结束;程序实现Intmain(){Intn,r,i,j;Inta[10];Cin>>n>>r;i=1;a[1]=n;doif(a[i]>r-i)//{符合条件}if(i==r)//{输出}{For(j=1;j<=r;j++)Cout<<a[j];Cout<<endl;a[i]=a[i]-1;}else{//{继续搜索}a[i+1]=a[i]-1;i++;}Else//{回溯}{i--;a[i]=a[i]-1;}while(a[1]==r-1);}下面我们再通过另一个例子看看回溯在信息学奥赛中的应用。例2数的划分(noip2001tg)问题描述整数n分成k份,且每份不能为空,任意两份不能相同(不考虑顺序)。例如:n=7,k=3,下面三种分法被认为是相同的。1,1,5;1,5,1;5,1,1;问有多少种不同的分法。输入:n,k(6<n<=200,2<=k<=6)输出:一个整数,即不同的分法。样例输入:73输出:4{四种分法为:1,1,5;1,2,4;1,3,3;2,2,3;}算法分析:此题可以用回溯法求解,设自然数n拆分为a1,a2,…,ak,必须满足以下两个条件:(1)n=a1+a2+…+ak;(2)a1<=a2<=…<=ak(避免重复计算);现假设己求得的拆分数为a1,a2,…ai,且都满足以上两个条件,设sum=n-a1-a2-…-ai,我们根据不同的情形进行处理:(1)如果i=k,则得到一个解,则计数器t加1,并回溯到上一步,改变ai-1的值;(2)如果i<k且sum>=ai,则添加下一个元素ai+1;(3)如果i<k且sum<ai,则说明达不到目标,回溯到上一步,改变ai-1的值;算法实现步骤如下:第一步:输入自然数n,k并初始化;t=0;i=1;a[i]=1;sum=n-1;nk=ndivk;第二步:如果a[1]<=nk重复:若i=k,搜索到一个解,计数器t=t+1;并回溯;否则:①若sum>=a[i]则继续搜索;②若sum<a[i]则回溯;搜索时,inc(i);a[i]=a[i-1];sum=sum-a[i];回溯时,dec(i);inc(a[i]);sum=sum+a[i+1]-1;第三步:输出。程序如下:Intmain(){Intn,nk,sum,i,k;Longintt;Inta[6];Cin>>n>>k;nk=n/k;t=0;i=1;a[i]=1;sum=n-1;//初始化doif(i==k)//判断是否搜索到底{t++;dec(i);a[i]++;sum=sum+a[i+1]-1;//回溯}else{If(sum>=a[i])//判断是否回溯{i++;a[i]=a[i-1];sum=sum-a[i];//继续搜}else{dec(i);a[i]++;sum=sum+a[i+1]-1;//回溯}}while(a[1]>nk);cout<<t<<endl;}回溯法是通过尝试和纠正错误来寻找答案,是一种通用解题法,在NOIP中有许多涉及搜索问题的题目都可以用回溯法来求解。10.4递归算法递归算法的定义:如果一个对象的描述中包含它本身,我们就称这个对象是递归的,这种用递归来描述的算法称为递归算法。我们先来看看大家熟知的一个的故事:从前有座山,山上有座庙,庙里有个老和尚在给小和尚讲故事,他说从前有座山,山上有座庙,庙里有个老和尚在给小和尚讲故事,他说……上面的故事本身是递归的,用递归算法描述:voidbonze-tell-story;{if(讲话被打断)故事结束else{从前有座山,山上有座庙,庙里有个老和尚在给小和尚讲故事;bonze-tell-story;}};从上面的递归事例不难看出,递归算法存在的两个必要条件:(1)必须有递归的终止条件;(2)过程的描述中包含它本身;在设计递归算法中,如何将一个问题转化为递归的问题,是初学者面临的难题,下面我们通过分析汉诺塔问题,看看如何用递归算法来求解问题;例1:汉诺塔问题,如下图,有A、B、C三根柱子。A柱子上按从小到大的顺序堆放了N个盘子,现在要把全部盘子从A柱移动到C柱,移动过程中可以借助B柱。移动时有如下要求:(1)一次只能移动一个盘子;(2)不允许把大盘放在小盘上边;(3)盘子只能放在三根柱子上;算法分析:当盘子比较多的时,问题比较复杂,所以我们先分析简单的情况:如果只有一个盘子,只需一步,直接把它从A柱移动到C柱;如果是二个盘子,共需要移动3步:(1)把A柱上的小盘子移动到B柱;(2)把A柱上的大盘子移动到C柱;(3)把B柱上的大盘子移动到C柱;如果N比较大时,需要很多步才能完成,我们先考虑是否能把复杂的移动过程转化为简单的移动过程,如果要把A柱上最大的盘子移动到C柱上去,必须先把上面的N-1个盘子从A柱移动到B柱上暂存,按这种思路,就可以把N个盘子的移动过程分作3大步:(1)把A柱上面的N-1个盘子移动到B柱;(2)把A柱上剩下的一个盘子移动到C柱;(3)把B柱上面的N-1个盘子移动到C柱;其中N-1个盘子的移动过程又可按同样的方法分为三大步,这样就把移动过程转化为一个递归的过程,直到最后只剩下一个盘子,按照移动一个盘子的方法移动,递归结束。递归过程:procedureHanoi(N,A,B,C:integer;);{以B柱为中转柱将N个盘子从A柱移动到C柱}beginifN=1thenwrite(A,’->’,C){把盘子直接从A移动到C}elsebeginHanoi(N-1,A,C,B);{以C柱为中转柱将N-1个盘子从A柱移动到B柱}write(A,’->’,C);{把剩下的一个盘子从A移动到C}Hanoi(N-1,B,A,C);{以A柱为中转柱将N-1个盘子从B柱移动到C柱}end;end;从上面的例子我们可以看出,在使用递归算法时,首先弄清楚简单情况下的解法,然后弄清楚如何把复杂情况归纳为更简单的情况。在信息学奥赛中有的问题的结构或所处理的数据本身是递归定义的,这样的问题非常适合用递归算法来求解,对于这类问题,我们把它分解为具有相同性质的若干个子问题,如果子问题解决了,原问题也就解决了。例2求先序排列(NOIP2001pj)[问题描述]给出一棵二叉树的中序与后序排列。求出它的先序排列。(约定树结点用不同的大写字母表示,长度≤8)。[样例]输入:BADCBDCA  输出:ABCD算法分析:我们先看看三种遍历的定义:先序遍历是先访问根结点,再遍历左子树,最后遍历右子树;中序遍历是先遍历左子树,再访问根结点,最后遍历右子树;后序遍历是先遍历左子树,再遍历右子树,最后访问根结点;从遍历的定义可知,后序排列的最后一个字符即为这棵树的根节点;在中序排列中,根结点前面的为其左子树,根结点后面的为其右子树;我们可以由后序排列求得根结点,再由根结点在中序排列的位置确定左子树和右子树,把左子树和右子树各看作一个单独的树。这样,就把一棵树分解为具有相同性质的二棵子树,一直递归下去,当分解的子树为空时,递归结束,在递归过程中,按先序遍历的规则输出求得的各个根结点,输出的结果即为原问题的解。源程序programnoip2001_3;var z,h:string;proceduremake(z,h:string);{z为中序排列,h为后序排列}var s,m:integer;beginm:=length(h);{m为树的长度} write(h[m]);{输出根节点} s:=pos(h[m],z);{求根节点在中序排列中的位置} ifs>1thenmake(copy(z,1,s-1),copy(h,1,s-1));{处理左子树} ifm>sthenmake(copy(z,s+1,m-s),copy(h,s,m-s));{处理右子树}end;begin readln(z); readln(h); make(z,h);end.递归算法不仅仅是用于求解递归描述的问题,在其它很多问题中也可以用到递归思想,如回溯法、分治法、动态规划法等算法中都可以使用递归思想来实现,从而使编写的程序更加简洁。比如上期回溯法所讲的例2《数的划分问题》,若用递归来求解,程序非常短小且效率很高,源程序如下varn,k:integer;tol:longint;proceduremake(sum,t,d:integer);vari:integer;beginifd=ktheninc(tol)elsefori:=ttosumdiv2domake(sum-i,i,d+1);end;beginreadln(n,k);tol:=0;make(n,1,1);writeln(tol);end.有些问题本身是递归定义的,但它并不适合用递归算法来求解,如斐波那契(Fibonacci)数列,它的递归定义为:F(n)=1  (n=1,2)F(n)=F(n-2)+F(n-1)(n>2)用递归过程描述为:Funtionfb(n:integer):integer;Beginifn<3thenfb:=1elsefb:=fb(n-1)+fb(n-2);End;上面的递归过程,调用一次产生二个新的调用,递归次数呈指数增长,时间复杂度为O(2n),把它改为非递归:x:=1;y:=1;fori:=3tondobeginz:=y;y:=x+y;x:=z;end;修改后的程序,它的时间复杂度为O(n)。我们在编写程序时是否使用递归算法,关键是看问题是否适合用递归算法来求解。由于递归算法编写的程序逻辑性强,结构清晰,正确性易于证明,程序调试也十分方便,在NOIP中,数据的规模一般也不大,只要问题适合用递归算法求解,我们还是可以大胆地使用递归算法。10.5递推算法所谓递推,是指从已知的初始条件出发,依据某种递推关系,逐次推出所要求的各中间结果及最后结果。其中初始条件或是问题本身已经给定,或是通过对问题的分析与化简后确定。可用递推算法求解的题目一般有以下二个特点:(1)问题可以划分成多个状态;(2)除初始状态外,其它各个状态都可以用固定的递推关系式来表示。在我们实际解题中,题目不会直接给出递推关系式,而是需要通过分析各种状态,找出递推关系式。例1骑士游历:(noip1997tg)设有一个n*m的棋盘(2<=n<=50,2<=m<=50),如下图,在棋盘上任一点有一个中国象棋马,马走的规则为:1.马走日字2.马只能向右走,即如下图所示:任务1:当N,M输入之后,找出一条从左下角到右上角的路径。例如:输入N=4,M=4输出:路径的格式:(1,1)->(2,3)->(4,4)若不存在路径,则输出"no"任务2:当N,M给出之后,同时给出马起始的位置和终点的位置,试找出从起点到终点的所有路径的数目。例如:(N=10,M=10),(1,5)(起点),(3,5)(终点)输出:2(即由(1,5)到(3,5)共有2条路径)输入格式:n,m,x1,y1,x2,y2(分别表示n,m,起点坐标,终点坐标)输出格式:路径数目(若不存在从起点到终点的路径,输出0)算法分析:为了研究的方便,我们先将棋盘的横坐标规定为i,纵坐标规定为j,对于一个n×m的棋盘,i的值从1到n,j的值从1到m。棋盘上的任意点都可以用坐标(i,j)表示。对于马的移动方法,我们用K来表示四种移动方向(1,2,3,4);而每种移动方法用偏移值来表示,并将这些偏移值分别保存在数组dx和dy中,如下表 K Dx[k] Dy[k] 1 2 1 2 2 -1 3 1 2 4 1 -2根据马走的规则,马可以由(i-dx[k],j-dy[k])走到(i,j)。只要马能从(1,1)走到(i-dx[k],j-dy[k]),就一定能走到(i,j),马走的坐标必须保证在棋盘上。我们以(n,m)为起点向左递推,当递推到(i-dx[k],j-dy[k])的位置是(1,1)时,就找到了一条从(1,1)到(n,m)的路径。我们用一个二维数组a表示棋盘,对于任务一,使用倒推法,从终点(n,m)往左递推,设初始值a[n,m]为(-1,-1),如果从(i,j)一步能走到(n,m),就将(n,m)存放在a[i,j]中。如下表,a[3,2]和a[2,3]的值是(4,4),表示从这两个点都可以到达坐标(4,4)。从(1,1)可到达(2,3)、(3,2)两个点,所以a[1,1]存放两个点中的任意一个即可。递推结束以后,如果a[1,1]值为(0,0)说明不存在路径,否则a[1,1]值就是马走下一步的坐标,以此递推输出路径。 -1,-1 4,4 4,4 2,3    任务一的源程序:constdx:array[1..4]ofinteger=(2,2,1,1);dy:array[1..4]ofinteger=(1,-1,2,-2);typemap=recordx,y:integer;end;vari,j,n,m,k:integer;a:array[0..50,0..50]ofmap;beginread(n,m);fillchar(a,sizeof(a),0);a[n,m].x:=-1;a[n,m].y:=-1;{标记为终点}fori:=ndownto2do{倒推}forj:=1tomdoifa[i,j].x<>0thenfork:=1to4dobegina[i-dx[k],j-dy[k]].x:=i;a[i-dx[k],j-dy[k]].y:=j;end;ifa[1,1].x=0thenwriteln('no')elsebegin{存在路径}i:=1;j:=1;write('(',i,',',j,')');whilea[i,j].x<>-1dobegink:=i;i:=a[i,j].x;j:=a[k,j].y;write('->(',i,',',j,')');end;end;end.对于任务二,也可以使用递推法,用数组A[i,j]存放从起点(x1,y1)到(i,j)的路径总数,按同样的方法从左向右递推,一直递推到(x2,y2),a[x2,y2]即为所求的解。源程序(略)在上面的例题中,递推过程中的某个状态只与前面的一个状态有关,递推关系并不复杂,如果在递推中的某个状态与前面的所有状态都有关,就不容易找出递推关系式,这就需要我们对实际问题进行分析与归纳,从中找到突破口,总结出递推关系式。我们可以按以下四个步骤去分析:(1)细心的观察;(2)丰富的联想;(3)不断地尝试;(4)总结出递推关系式。下面我们再看一个复杂点的例子。例2、栈(noip2003pj)题目大意:求n个数通过栈后的排列总数。(1≤n≤18)如输入3输出5算法分析:先模拟入栈、出栈操作,看看能否找出规律,我们用f(n)表示n个数通过栈操作后的排列总数,当n很小时,很容易模拟出f(1)=1;f(2)=2;f(3)=5,通过观察,看不出它们之间的递推关系,我们再分析N=4的情况,假设入栈前的排列为“4321”,按第一个数“4”在出栈后的位置进行分情况讨论:(1)若“4”最先输出,刚好与N=3相同,总数为f(3);(2)若“4”第二个输出,则在“4”的前只能是“1”,“23”在“4”的后面,这时可以分别看作是N=1和N=2时的二种情况,排列数分别为f(1)和f(2),所以此时的总数为f(1)*f(2);(3)若“4”第三个输出,则“4”的前面二个数为“12”,后面一个数为“3”,组成的排列总数为f(2)*f(1);(4)若“4”第四个输出,与情况(1)相同,总数为f(3);所以有:f(4)=f(3)+f(1)*f(2)+f(2)*f(1)+f(3);若设0个数通过栈后的排列总数为:f(0)=1;上式可变为:f(4)=f(0)*f(3)+f(1)*f(2)+f(2)*f(1)+f(3)*f(0);再进一步推导,不难推出递推式:f(n)=f(0)*f(n-1)+f(1)*f(n-2)+…+f(n-1)*f(0);即f(n)=(n>=1)初始值:f(0)=1;有了以上递推式,就很容易用递推法写出程序。vara:array[0..18]oflongint;n,i,j:integer;beginreadln(n);fillchar(a,sizeof(a),0);a[0]:=1;fori:=1tondoforj:=0toi-1doa[i]:=a[i]+a[j]*a[i-j-1];writeln(a[n]);end.递推算法最主要的优点是算法结构简单,程序易于实现,难点是从问题的分析中找出解决问题的递推关系式。对于以上两个例子,如果在比赛中找不出递推关系式,也可以用回溯法求解。10.6分治算法分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。分治法解题的一般步骤:(1)分解,将要解决的问题划分成若干规模较小的同类问题;(2)求解,当子问题划分得足够小时,用较简单的方法解决;(3)合并,按原问题的要求,将子问题的解逐层合并构成原问题的解。例1、比赛安排(noip1996)设有2^n(n<=6)个球队进行单循环比赛,计划在2^n-1天内完成,每个队每天进行一场比赛。设计一个比赛的安排,使在2^n-1天内每个队都与不同的对手比赛。例如n=2时的比赛安排为:队   12 34比赛 1-2 3-4 第一天1-3 2-4 第二天1-4 2-3 第三天算法分析:此题很难直接给出结果,我们先将问题进行分解,设m=2^n,将规模减半,如果n=3(即m=8),8个球队的比赛,减半后变成4个球队的比赛(m=4),4个球队的比赛的安排方式还不是很明显,再减半到两个球队的比赛(m=2),两个球队的比赛安排方式很简单,只要让两个球队直接进行一场比赛即可: 1 2 2 1分析两个球队的比赛的情况不难发现,这是一个对称的方阵,我们把这个方阵分成4部分(即左上,右上,左下,右下),右上部分可由左上部分加1(即加m/2)得到,而右上与左下部分、左上与右下部分别相等。因此我们也可以把这个方阵看作是由M=1的方阵所成生的,同理可得M=4的方阵: 1 2 3 4 2 1 4 3 3 4 1 2 4 3 2 1同理可由M=4方阵生成M=8的方阵: 1 2 3 4 5 6 7 8 2 1 4 3 6 5 8 7 3 4 1 2 7 8 5 6 4 3 2 1 8 7 6 5 5 6 7 8 1 2 3 4 6 5 8 7 2 1 4 3 7 8 5 6 3 4 1 2 8 7 6 5 4 3 2 1这样就构成了整个比赛的安排表。在算法设计中,用数组a记录2^n个球队的循环比赛表,整个循环比赛表从最初的1*1方阵按上述规则生成2*2的方阵,再生成4*4的方阵,……直到生成出整个循环比赛表为止。变量h表示当前方阵的大小,也就是要生成的下一个方阵的一半。源程序:vari,j,h,m,n:integer;a:array[1..32,1..32]ofinteger;beginreadln(n);m:=1;a[1,1]:=1;h:=1;fori:=1tondom:=m*2;repeatfori:=1tohdoforj:=1tohdobegina[i,j+h]:=a[i,j]+h;{构造右上角方阵}a[i+h,j]:=a[i,j+h];{构造左下角方阵}a[i+h,j+h]:=a[i,j];{构造右下角方阵}end;h:=h*2;untilh=m;fori:=1tomdobeginforj:=1tomdowrite(a[i,j]:4);writeln;end;end.在分治算法中,若将原问题分解成两个较小的子问题,我们称之为二分法,由于二分法划分简单,所以使用非常广泛。使用二分法与使用枚举法求解问题相比较,时间复杂度由O(N)降到O(log2N),在很多实际问题中,我们可以通过使用二分法,达到提高算法效率的目的,如下面的例子。例2一元三次方程求解(noip2001tg)题目大意:给出一个一元三次方程f(x)=ax3+bx2+cx+d=0,求它的三个根。所有的根都在区间[-100,100]中,并保证方程有三个实根,且它们之间的差不小于1。算法分析:在讲解枚举法时,我们讨论了如何用枚举法求解此题,但如果求解的精度进一步提高,使用枚举法就无能为力了,在此我们再一次讨论如何用二分法求解此题。由题意知(i,i+1)中若有根,则只有一个根,我们枚举根的值域中的每一个整数x(-100≤x≤100),设定搜索区间[x1,x2],其中x1=x,x2=x+1。若:⑴f(x1)=0,则确定x1为f(x)的根;⑵f(x1)*f(x2)<0,则确定根x在区间[x1,x2]内。⑶f(x1)*f(x2)>0,则确定根x不在区间[x1,x2]内,设定[x2,x2+1]为下一个搜索区间;若确定根x在区间[x1,x2]内,采用二分法,将区间[x1,x2]分成左右两个子区间:左子区间[x1,x]和右子区间[x,x2](其中x=(x1+x2)/2)。如果f(x1)*f(x)≤0,则确定根在左区间[x1,x]内,将x设为该区间的右界值(x2=x),继续对左区间进行对分;否则确定根在右区间[x,x2]内,将x设为该区间的左界值(x1=x),继续对右区间进行对分;上述对分过程一直进行到区间的间距满足精度要求为止(即x2-x1<0.005)。此时确定x1为f(x)的根。源程序:{$N+}varx:integer;a,b,c,d,x1,x2,xx:extended;functionf(x:extended):extended;beginf:=((a*x+b)*x+c)*x+d;end;beginread(a,b,c,d);forx:=-100to100dobeginx1:=x;x2:=x+1;iff(x1)=0thenwrite(x1:0:2,'')elseiff(x1)*f(x2)<0thenbeginwhilex2-x1>=0.005dobeginxx:=(x1+x2)/2;iff(x1)*f(xx)<=0thenx2:=xxelsex1:=xx;end;{while}write(x1:0:2,'');end;{then}end;{for}end.10.7贪心算法在求最优解问题的过程中,依据某种贪心标准,从问题的初始状态出发,直接去求每一步的最优解,通过若干次的贪心选择,最终得出整个问题的最优解,这种求解方法就是贪心算法。从贪心算法的定义可以看出,贪心法并不是从整体上考虑问题,它所做出的选择只是在某种意义上的局部最优解,而由问题自身的特性决定了该题运用贪心算法可以得到最优解。我们看看下面的例子例1均分纸牌(NOIP2002tg)[问题描述] 有N堆纸牌,编号分别为1,2,…,N。每堆上有若干张,但纸牌总数必为N的倍数。可以在任一堆上取若干张纸牌,然后移动。移牌规则为:在编号为1堆上取的纸牌,只能移到编号为2的堆上;在编号为N的堆上取的纸牌,只能移到编号为N-1的堆上;其他堆上取的纸牌,可以移到相邻左边或右边的堆上。现在要求找出一种移动方法,用最少的移动次数使每堆上纸牌数都一样多。例如N=4,4堆纸牌数分别为:  ① 9 ② 8 ③ 17 ④ 6移动3次可达到目的:  从③取4张牌放到④(981310)->从③取3张牌放到②(9111010)->从②取1张牌放到①(10101010)。[输入]:键盘输入文件名。文件格式:N(N堆纸牌,1<=N<=100)  A1A2…An(N堆纸牌,每堆纸牌初始数,l<=Ai<=10000)[输出]:输出至屏幕。格式为:所有堆均达到相等时的最少移动次数。[输入输出样例]a.in: 4 98176屏慕显示:3算法分析:设a[i]为第i堆纸牌的张数(0<=i<=n),v为均分后每堆纸牌的张数,s为最小移到次数。我们用贪心法,按照从左到右的顺序移动纸牌。如第i堆(0<i<n)的纸牌数a[i]不等于平均值,则移动一次(即s加1),分两种情况移动:(1)若a[i]>v,则将a[i]-v张纸牌从第I堆移动到第I+1堆;(2)若a[i]<v,则将v-a[i]张纸牌从第I+1堆移动到第I堆;为了设计的方便,我们把这两种情况统一看作是将a[I]-v张牌从第I堆移动到第I+1堆;移动后有:a[I]:=v;a[I+1]:=a[I+1]+a[I]-v;在从第i+1堆中取出纸牌补充第i堆的过程中,可能会出现第i+1堆的纸牌数小于零(a[i+1]+a[i]-v<0)的情况。如n=3,三堆纸牌数为(1,2,27)这时v=10,为了使第一堆数为10,要从第二堆移9张纸牌到第一堆,而第二堆只有2张纸牌可移,这是不是意味着刚才使用的贪心法是错误的呢?我们继续按规则分析移牌过程,从第二堆移出9张到第一堆后,第一堆有10张纸牌,第二堆剩下-7张纸牌,再从第三堆移动17张到第二堆,刚好三堆纸牌数都是10,最后结果是对的,从第二堆移出的牌都可以从第三堆得到。我们在移动过程中,只是改变了移动的顺序,而移动的次数不变,因此此题使用贪心法是可行的。源程序:vari,n,s:integer;v:longint;a:array[1..100]oflongint;f:text;fil:string;beginreadln(fil);assign(f,fil);reset(f);readln(f,n);v:=0;fori:=1tondobeginread(f,a[i]);inc(v,a[i]);end;v:=vdivn;{每堆牌的平均数}fori:=1ton-1doifa[i]<>vthen{贪心选择}begininc(s);{移牌步数计数}a[i+1]:=a[i+1]+a[i]-v;{使第i堆牌数为v}end;{then}writeln(s);end.利用贪心算法解题,需要解决两个问题:一是问题是否适合用贪心法求解。我们看一个找币的例子,如果一个货币系统有3种币值,面值分别为一角、五分和一分,求最小找币数时,可以用贪心法求解;如果将这三种币值改为一角一分、五分和一分,就不能使用贪心法求解。用贪心法解题很方便,但它的适用范围很小,判断一个问题是否适合用贪心法求解,目前还没有一个通用的方法,在信息学竞赛中,需要凭个人的经验来判断何时该使用贪心算法。二是确定了可以用贪心算法之后,如何选择一个贪心标准,才能保证得到问题的最优解。在选择贪心标准时,我们要对所选的贪心标准进行验证才能使用,不要被表面上看似正确的贪心标准所迷惑,如下面的列子。例2(NOIP1998tg)设有n个正整数,将他们连接成一排,组成一个最大的多位整数。例如:n=3时,3个整数13,312,343,连成的最大整数为:34331213又如:n=4时,4个整数7,13,4,246连接成的最大整数为7424613输入:NN个数输出:连接成的多位数算法分析:此题很容易想到使用贪心法,在考试时有很多同学把整数按从大到小的顺序连接起来,测试题目的例子也都符合,但最后测试的结果却不全对。按这种贪心标准,我们很容易找到反例:12,121应该组成12121而非12112,那么是不是相互包含的时候就从小到大呢?也不一定,如:12,123就是12312而非12112,这样情况就有很多种了。是不是此题不能用贪心法呢?其实此题是可以用贪心法来求解,只是刚才的贪心标准不对,正确的贪心标准是:先把整数化成字符串,然后再比较a+b和b+a,如果a+b>b+a,就把a排在b的前面,反之则把a排在b的后面。源程序:vars:array[1..20]ofstring;t:string;i,j,k,n:longint;beginreadln(n);fori:=1tondobeginread(k);str(k,s[i]);end;fori:=1ton-1doforj:=i+1tondoifs[i]+s[j]<s[j]+s[i]thenbegin{交换}t:=s[i];s[i]:=s[j];s[j]:=t;end;fori:=1tondowrite(s[i]);end.贪心算法所作的选择可以依赖于以往所作过的选择,但决不依赖于将来的选择,也不依赖于子问题的解,因此贪心算法与其它算法相比具有一定的速度优势。如果一个问题可以同时用几种方法解决,贪心算法应该是最好的选择之一。10.8搜索算法一(深度优先)在这里介绍两种基本的搜索算法:深度优先搜索和广度优先搜索法,以树的搜索为例,深度优先搜索法是优先扩展尚未扩展的且具有最大深度的结点;广度优先搜索法是在扩展完第K层的结点以后才扩展K+1层的结点。深度优先搜索法与前面讲的回溯法差不多,主要的区别是回溯法在求解过程中不保留完整的树结构,而深度优先搜索则记下完整的搜索树,搜索树起记录解路径和状态判重的作用。为了减少存储空间,在深度优先搜索中,用标志的方法记录访问过的状态,这种处理方法使得深度优先搜索法与回溯法没什么区别了。在回溯法中,我们己分析了非递归的实现过程,在这里就只讨论深度优先的递归实现方法。深度优先搜索的递归实现过程:proceduredfs(i);fori:=1tordoif子结点mr符合条件then产生的子结点mr入栈;if子结点mr是目标结点then输出elsedfs(i+1);栈顶元素出栈(即删去mr);endif;endfor;在讲解递推法时,我们讨论了用递推法解骑土游历问题,在这里我们再看看如何用深度优先搜索法求解此题。例1骑士游历:设有一个n*m的棋盘,在棋盘上任一点有一个中国象棋马,马走的规则为:1.马走日字2.马只能向右走。当N,M输入之后,找出一条从左下角到右上角的路径。例如:输入N=4,M=4,输出:路径的格式:(1,1)->(2,3)->(4,4),若不存在路径,则输出"no"算法分析:我们以4×4的棋盘为例进行分析,用树形结构表示马走的所有过程(如下图),求从起点到终点的路径,实际上就是从根结点开始深度优先搜索这棵树。马从(1,1)开始,按深度优先搜索法,走一步到达(2,3),判断是否到达终点,若没有,则继续往前走,再走一步到达(4,4),然后判断是否到达终点,若到达则退出,搜索过程结束。为了减少搜索次数,在马走的过程中,判断下一步所走的位置是否在棋盘上,如果不在棋盘上,则另选一条路径再走。程序如下:constdx:array[1..4]ofinteger=(2,2,1,1);dy:array[1..4]ofinteger=(1,-1,2,-2);typemap=recordx,y:integer;end;vari,n,m:integer;a:array[0..50]ofmap;proceduredfs(i:integer);varj:integer;beginforj:=1to4doif(a[i-1].x+dx[j]>0)and(a[i-1].x+dx[j]<=n)and(a[i-1].y+dy[j]>0)and(a[i-1].y+dy[j]<=n)then{判断是否在棋盘上}begina[i].x:=a[i-1].x+dx[j];a[i].y:=a[i-1].y+dy[j];{入栈}if(a[i].x=n)and(a[i].y=m)thenbeginwrite('(',1,',',1,')');forj:=2toidowrite('->(',a[j].x,',',a[j].y,')');halt;{输出结果并退出程序}end;dfs(i+1);{搜索下一步}a[i].x:=0;a[i].y:=0;{出栈}end;end;begina[1].x:=1;a[1].y:=1;readln(n,m);dfs(2);writeln('no');end.从上面的例子我们可以看出,深度优先搜索算法有两个特点:1、己产生的结点按深度排序,深度大的结点先得到扩展,即先产生它的子结点。2、深度大的结点是后产生的,但先得到扩展,即“后产生先扩展”,与栈的工作原理相同,因此用堆栈作为该算法的主要数据结构,存储产生的结点。对于不同的问题,深度优先搜索算法基本上是一样的,但在具体处理方法和编程技巧上又都不相同,甚至会有很大的差别。我们再看看另一个例子。题二选数(存盘名:NOIP2002pj)[问题描述]:已知n个整数x1,x2,…,xn,以及一个整数k(k<n)。从n个整数中任选k个整数相加,可分别得到一系列的和。例如当n=4,k=3,4个整数分别为3,7,12,19时,可得全部的组合与它们的和为:3+7+12=22  3+7+19=29  7+12+19=38  3+12+19=34。现在,要求你计算出和为素数共有多少种。例如上例,只有一种的和为素数:3+7+19=29。[输入]:键盘输入,格式为:  n,k(1<=n<=20,k<n)  x1,x2,…,xn(1<=xi<=5000000)[输出]:屏幕输出,格式为:  一个整数(满足条件的种数)。[输入输出样例]:输入:43 371219输出:1算法分析:本题是求从n个数中选k个数的组合,并使其和为素数。求解此题时,先用深度优先搜索法生成k个数的组合,再判断k个数的和是否为素数,若为素数则总数加1。在程序实现过程中,用数组a存放输入的n个数,用s表示k个数的和,ans表示和为素数的个数。为了避免不必要的搜索,程序对搜索过程进行了优化,限制搜索范围,在搜索过程dfs(i,m)中,参数m为第i个数的上限,下限为n-k+i。源程序:varn,k,i:byte;ans,s:longint;a:array[1..20]ofLongint;procedureprime(s:longint);{判断K个数的和是否为素数}vari:integer;begini:=2;while(sqr(i)<=s)and(smodi<>0)doinc(i);ifsqr(i)>stheninc(ans){若为素数则总数加1}end;proceduredfs(i,m:byte);{搜索第i个数,}varj:byte;{j表示第i个数的位置beginforj:=mton-k+ido{枚举第i个数}begininc(s,a[j]);{入栈}ifi=kthenprime(s)elsedfs(i+1,j+1);{继续搜第i+1个数}dec(s,a[j]){出栈}endend;beginreadln(n,k);fori:=1tondoread(a[i]);ans:=0;s:=0;dfs(1,1);writeln(ans);end.从上面的两个例子我们可以看出,用递归实现深度优先搜索比非递归更加方便。在使用深度搜索法解题时,搜索的效率并不高,所以要重视对算法的优化,尽可能的减少搜索范围,提高程序的速度。在下一篇中将继续介绍另一种搜索方法——广度优先搜索法。10.9搜索算法二(广度优先)在深度优先搜索算法中,深度越大的结点越先得到扩展,若把它改为深度越小的结点越先得到扩展,就是广度优先搜索法。广度优先搜索基本算法:programbfs;初始化;建立队列data;设队列首指针closed:=0;队列尾指针open:=1;repeatclosed增1,取出closed所指结点进行扩展;fori:=1tordobeginif子结点符合条件thenbeginopen增1,并把新结点存入数据库队尾;if新结点与原有结点有重复then删于该结点(open减1)elseif新结点即目标then输出并退出;end{if};end{for};untilclosed>=open;{队列为空}使用广度优先搜索时,离根结点最近的结点先扩展,所以广度优先搜索法比较适合求步数最少的解,由于深度优先使用了标志法,使得存储空间大大减少,而广度优先要保留所有搜索过的节点,随着搜索程度的加深,所需的存储空间成指数增加。因此在必要时我们采用双向搜索来减少搜索空间和存储空间,如下面的例子。例字串变换(NOIP2002tg)[问题描述]:已知有两个字串A$,B$及一组字串变换的规则(至多6个规则):A1$->B1$ A2$->B2$规则的含义为:在A$中的子串A1$可以变换为B1$、A2$可以变换为B2$…。例如:A$='abcd' B$='xyz'变换规则为:‘abc’->‘xu’ ‘ud’->‘y’ ‘y’->‘yz’ 则此时,A$可以经过一系列的变换变为B$,其变换的过程为:‘abcd’->‘xud’->‘xy’->‘xyz’共进行了三次变换,使得A$变换为B$。[输入]:键盘输人文件名。文件格式如下:  A$B$  A1$B1$\  A2$B2$ |->变换规则  ....../   所有字符串长度的上限为20。[输出]:输出至屏幕。格式如下:  若在10步(包含10步)以内能将A$变换为B$,则输出最少的变换步数;否则输出"NOANSWER!"[输入输出样例]b.in:abcdxyzabcxuudyyyz屏幕显示:3算法分析:此题是求变换的最少步数,很显然可以使用广度优先搜索法,如果直接从初状态搜到目标状态,最坏情况下存储的结点数超过6的10次方幂,搜索空间过大,因此我们考虑使双向搜索,同时从初始状态和目标状态向中间状态搜索,当相遇时搜索结束。采用双向搜索,存储的结点数还有可能超限,我们在前向搜索队列中存储5步内变换的结点,在后向搜索队列中,由于第5步产生的结点只是用来与前向队列中的结点比较,所以可以不存储在队列中,后向搜索队列只需存储4步内的结点,这样就解决了存储空间问题。为了使用方便,在程序设计中用一个数组a[1..max]存储两个队列,前向搜索队列为a[1..mid],后向搜索队列为a[mid..max],用st存储搜索方向,st=0表示前向搜索,st=1表示后向搜索,用op[st]和cl[st]分别表示队列尾指针和首指针,用be表示队列起始位置,循环产生每一个结点,若在10内无解退出循环,若在10内找到解则输出解并退出程序。源程序:constmid=12000;max=16000;typenode=records:string;x:byte;end;vari,mark:integer;a:array[1..max]of^node;x:array[0..6,0..1]ofstring[20];d,fil:string;op,cl:array[0..1]ofinteger;procedureInit;{读取数据,初始化}varf:text;t:string;beginreadln(fil);assign(f,fil);reset(f);i:=0;whilenoteof(f)dobeginreadln(f,t);x[i,0]:=copy(t,1,pos('',t)-1);x[i,1]:=copy(t,pos('',t)+1,length(t));inc(i);end;{while}mark:=i-1;close(f);end;{判断是否到达目标状态}procedurebool(be,st:integer);beginfori:=mid-be+1tocl[1-st]doifa[cl[st]]^.s=a[i]^.sthenbeginwriteln(a[cl[st]]^.x+a[i]^.x);halt;end;{if}end;{判断节点是否与前面的结点重复}procedurecheck(be,st:integer);beginfori:=be+1tocl[st]-1doifa[i]^.s=a[cl[st]]^.sthenbegindec(cl[st]);exit;end;bool(be,st);end;{扩展产生新节点}procedureexpand(be,st:integer);vari,j,k,lx,ld:integer;begininc(op[st]);d:=a[op[st]]^.s;k:=a[op[st]]^.x;ld:=length(d);fori:=1tomarkdobeginlx:=length(x[i,st]);forj:=1tolddobeginif(copy(d,j,lx)=x[i,st])thenbeginif(st<>1)or(k<>4)thenbegininc(cl[st]);new(a[cl[st]]);end;{if}a[cl[st]]^.s:=copy(d,1,j-1)+x[i,1-st]+copy(d,j+lx,ld);a[cl[st]]^.x:=k+1;check(be,st);{检查是否重复}end;{if}end;{for}end;{for}end;procedurebfs;varbe,k,st:integer;Beginforst:=0to1dobeginifst=0thenbe:=0elsebe:=mid;op[st]:=be+0;cl[st]:=be+1;new(a[cl[st]]);a[cl[st]]^.s:=x[0,st];a[cl[st]]^.x:=0;end;{for}repeatif(op[0]<cl[0])and(a[cl[0]]^.x<=5)thenexpand(0,0);if(op[1]<cl[1])and(a[cl[1]]^.x<=5)thenexpand(mid,1);until(op[0]>=cl[0])or(a[cl[0]]^.x>5)or(op[1]>=cl[1])or(a[cl[1]]^.x>5);End;BEGINinit;bfs;writeln('NOANSWER!')END.两种搜索算法的比较: 搜索方式 扩展方式 数据结构 适合求解的问题 深度优先 后产生先扩展 栈 可行解或所有解 广度优先 先产生先扩展 队列 最优解在选择搜索方式时,并不是完全遵循以上原则,具体还是要根据题目的要求而定。在求最优解时,如果搜索的深度不大,我们也可以考虑使用深度优先搜索;在求解可行解时,如果搜索的深度没有限制,或者搜索的代价与搜索的深度成正比,我们也应该使用广度优先搜索。10.10动态规划法在学习动态规划法之前,我们先来了解动态规划的几个概念1、阶段:把问题分成几个相互联系的有顺序的几个环节,这些环节即称为阶段。2、状态:某一阶段的出发位置称为状态。3、决策:从某阶段的一个状态演变到下一个阶段某状态的选择。4、状态转移方程:前一阶段的终点就是后一阶段的起点,前一阶段的决策选择导出了后一阶段的状态,这种关系描述了由k阶段到k+1阶段状态的演变规律,称为状态转移方程。动态规划法的定义:在求解问题中,对于每一步决策,列出各种可能的局部解,再依据某种判定条件,舍弃那些肯定不能得到最优解的局部解,在每一步都经过筛选,以每一步都是最优解来保证全局是最优解,这种求解方法称为动态规划法。一般来说,适合于用动态规划法求解的问题具有以下特点:1、可以划分成若干个阶段,问题的求解过程就是对若干个阶段的一系列决策过程。2、每个阶段有若干个可能状态3、一个决策将你从一个阶段的一种状态带到下一个阶段的某种状态。4、在任一个阶段,最佳的决策序列和该阶段以前的决策无关。5、各阶段状态之间的转换有明确定义的费用,而且在选择最佳决策时有递推关系(即动态转移方程)。动态规划法所处理的问题是一个多阶段最优化决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线。如下图:动态规划设计都有着一定的模式,一般要经历以下几个步骤:1、划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。2、确定状态:将问题发展到各个阶段时所处的各种客观情况用不同的状态表示出来。3、确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态,所以如果确定了决策,状态转移方程也就可以写出。4、寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。5、程序设计实现:动态规划的主要难点在于理论上的设计,一旦设计完成,实现部分就会非常简单。根据以上的步骤设计,可以得到动态规划设计的一般模式:fork:=阶段最小值to阶段最大值do{顺推每一个阶段}forI:=状态最小值to状态最大值do{枚举阶段k的每一个状态}forj:=决策最小值to决策最大值do{枚举阶段k中状态i可选择的每一种决策}f[ik]:=min(max){f[ik-1]+a[ik-1,jk-1]|ik-1通过决策jk-1可达ik}有了以上的设计模式,对于简单的动态规划问题,就可以按部就班地进行动态规划设计。例1:合唱队形(noip2004tg)【问题描述】N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学排成合唱队形。合唱队形是指这样的一种队形:设K位同学从左到右依次编号为1,2,…,K,他们的身高分别为T1,T2,…,TK,则他们的身高满足T1<T2<…<Ti,Ti>Ti+1>…>TK(1<=i<=K)。你的任务是,已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。【输入文件】输入文件chorus.in的第一行是一个整数N(2<=N<=100),表示同学的总数。第一行有n个整数,用空格分隔,第i个整数Ti(130<=Ti<=230)是第i位同学的身高(厘米)。【输出文件】输出文件chorus.out包括一行,这一行只包含一个整数,就是最少需要几位同学出列。【样例输入】8186186150200160130197220【样例输出】4算法分析:此题采用动态规划法求解。先分别从左到右求最大上升子序列,从右到左求最大下降子序列,再枚举中间最高的一个人。算法实现起来也很简单,时间复杂度O(N^2)。我们先考虑如何求最大上升子序列的长度,设f1(i)为前i个同学的最大上升子序列长度。若要求f1(i),必须先求得f1(1),f1(2),…,f1(i-1),再选择一个最大的f1(j)(j<i),在前j个数中的最大上升序后添加Ti,就可得到前i个数的最大上升子序列f1(i)。这样就得到状态转移方程:f1(i)=max{f1(j)+1}(j<i,Tj<Ti)边界条件:f1(1)=1;设f2(i)为后面N-i+1位排列的最大下降子序列长度,用同样的方法可以得到状态转移方程:f2(i)=max{f2(j)+1}(i<j,Tj<Ti);边界值为f2(N)=1;有了状态转移方程,程序实现就非常容易了。源程序:vart,f1,f2:array[1..100]ofbyte;i,j,n,max:integer;beginassign(input,'chorus.in');reset(input);readln(n);fori:=1tondobeginread(t[i]);f1[i]:=1;f2[i]:=1;end;{for}close(input);max:=0;fori:=2tondoforj:=1toi-1dobeginif(t[i]>t[j])and(f1[j]>=f1[i])thenf1[i]:=f1[j]+1;{从左到右求最大上升子序列}if(t[n-i+1]>t[n-j+1])and(f2[n-j+1]>=f2[n-i+1])thenf2[n-i+1]:=f2[n-j+1]+1;{从右到左求最大下降子序列}end;{for}fori:=1tondoifmax<f1[i]+f2[i]thenmax:=f1[i]+f2[i];{枚举中间最高的}assign(output,'chorus.ans');rewrite(output);writeln(n-max+1);close(output);end.运用动态规划法求解问题的关键是找出状态转移方程,只要找出了状态转移方程,问题就解决了一半,剩下的事情就是解决如何把状态转移方程用程序实现。例2、乘积最大(noip2000tg)题目大意:在一个长度为N的数字串中插入r个乘号,将它分成r+1个部分,找出一种分法,使得这r+1个部分的乘积最大。算法分析:此题满足动态规划法的求解标准,我们把它按插入的乘号数来划分阶段,若插入K个乘号,可把问题看做是K个阶段的决策问题。设f[i,k]表示在前i位数中插入K个乘号所得的最大值,a[i,j]表示从第i位到第j位所组成的自然数。用f[i,k]存储阶段K的每一个状态,可以得状态转移方程:f[i,k]=max{f[j,k-1]*a[j+1,i]}(k<=j<=i)边界值:f[j,0]=a[1,j](1<j<=i)根据状态转移方程,我们就很容易写出动态规划程序:fork:=1tordofori:=k+1tondoforj:=ktoIdoiff[i,k]<f[j,k-1]*a[j+1,I]thenf[i,k]:=f[j,k-1]*a[j+1,i]源程序(略)。近年来,涉及动态规划的各种竞赛题越来越多,每一年的NOIP都至少有一道题目需要用动态规划法求解。而应用动态规划法解题是富于技巧性和创造性的,虽然在前面的求解过程中给出了一个解题的基本模式,但由于动态规划题目出现的形式多种多样,并且大部分题目表面上看不出与动态规划的直接联系,只有在充分把握其思想精髓的前提下大胆联想,多做多练才能达到得心应手,灵活运用的境界。10.11高精度计算C/C++中的int类型能表示的范围是-231-231–1。unsigned类型能表示的范围是0-232–1,即0-4294967295。所以,int和unsigned类型变量,都不能保存超过10位的整数。有时我们需要参与运算的数,可能会远远不止10位,例如,可能需要保留小数点后面100位(比如求π的值),那么,即便使用能表示的很大数值范围的double变量,但是由于double变量只有64位,所以还是不可能达到精确到小数点后面100位这样的精度。double变量的精度也不足以表示一个100位的整数。一般我们称这种基本数据类型无法表示的整数为大整数。如何表示和存放大整数呢?基本的思想就是:用数组存放和表示大整数。一个数组元素,存放大整数中的一位。那么,如何解决类似大整数这样的高精度计算问题呢?比如,一个最简单的例子,给定两个不超过200位的整数,求他们的和。[例题]大整数加法问题描述求两个不超过200位的非负整数的和。输入数据有两行,每行是一个不超过200位的非负整数,没有多余的前导0。输出要求一行,即相加后的结果。结果里不能有多余的前导0,即如果结果是342,那么就不能输出为0342。输入样例2222222222222222222233333333333333333333输出样例OutputSample:55555555555555555555解题思路首先要解决的就是存储200位整数的问题。显然,任何C/C++固有类型的变量都无法保存它。最直观的想法是可以用一个字符串来保存它。字符串本质上就是一个字符数组,因此为了编程更方便,我们也可以用数组unsignedan[200]来保存一个200位的整数,让an[0]存放个位数,an[1]存放十位数,an[2]存放百位数……那么如何实现两个大整数相加呢?方法很简单,就是模拟小学生列竖式做加法,从个位开始逐位相加,超过或达到10则进位。也就是说,用unsignedan1[201]保存第一个数,用unsignedan2[200]表示第二个数,然后逐位相加,相加的结果直接存放在an1中。要注意处理进位。另外,an1数组长度定为201,是因为两个200位整数相加,结果可能会有201位。实际编程时,不一定要费心思去把数组大小定得正好合适,稍微开大点也无所谓,以免不小心没有算准这个“正好合适”的数值,而导致数组小了,产生越界错误。参考程序:1.#include<stdio.h>2.#include<string.h>3.#defineMAX_LEN2004.intan1[MAX_LEN+10];5.intan2[MAX_LEN+10];6.charszLine1[MAX_LEN+10];7.charszLine2[MAX_LEN+10];8.intmain()9.{10.scanf("%s",szLine1);11.scanf("%s",szLine2);12.inti,j;13.14.//库函数memeset将地址an1开始的sizeof(an1)字节内容置成015.//sizeof(an1)的值就是an1的长度16.//memset函数在string.h中声明17.memset(an1,0,sizeof(an1));18.memset(an2,0,sizeof(an2));19.20.//下面将szLine1中存储的字符串形式的整数转换到an1中去,21.//an1[0]对应于个位22.intnLen1=strlen(szLine1);23.j=0;24.for(i=nLen1-1;i>=0;i--)25.an1[j++]=szLine1[i]-'0';26.27.intnLen2=strlen(szLine2);28.j=0;29.for(i=nLen2-1;i>=0;i--)30.an2[j++]=szLine2[i]-'0';31.32.for(i=0;i<MAX_LEN;i++){33.an1[i]+=an2[i];//逐位相加34.if(an1[i]>=10){//看是否要进位35.an1[i]-=10;36.an1[i+1]++;//进位37.}38.}39.boolbStartOutput=false;//此变量用于跳过多余的040.for(i=MAX_LEN;i>=0;i--){41.if(bStartOutput)42.printf("%d",an1[i]);//如果多余的0已经都跳过,则输出43.elseif(an1[i]){44.printf("%d",an1[i]);45.bStartOutput=true;//碰到第一个非0的值,就说明多余的0已经都跳过}46.}47.//--------------------------------------------------------------------------------48.return0;49.}实现技巧1.再次强调:实际编程时,不一定要费心思去把数组大小定得正好合适,稍微开大点也无所谓,以免不小心没有算准这个“正好合适”的数值,而导致数组小了,产生越界错误。2.语句25是把一个字符形式的数字转换成unsigned型的数。比如,要把字符’8’转换成unsigned型数8。char类型的变量,本质上实际是int类型,值就是字符的Ascii码。由于字符’0’到字符’9’的Ascii码是连续递增的,因此‘8’–‘0’的值就是8。思考题10.1:上面的程序,有一点点瑕疵。只在某一种情况下,不能输出正确的结果。指出这种情况并修正程序。提示:要改正此程序,程序中语句47前面的部分可以不做任何增删和修改,只需在其后添加一些代码即可。此思考题旨在培养阅读他人程序并发现bug的能力。[例题]:大整数乘法问题描述求两个不超过200位的非负整数的积。输入数据有两行,每行是一个不超过200位的非负整数,没有多余的前导0。输出要求一行,即相乘后的结果。结果里不能有多余的前导0,即如果结果是342,那么就不能输出为0342。输入样例1234567890098765432100输出样例1219326311126352690000解题思路在下面的例子程序中,用unsignedan1[200]和unsignedan2[200]分别存放两个乘数,用aResult[400]来存放积。计算的中间结果也都存在aResult中。aResult长度取400是因为两个200位的数相乘,积最多会有400位。an1[0],an2[0],aResult[0]都表示个位。计算的过程基本上和小学生列竖式做乘法相同。为编程方便,并不急于处理进位,而将进位问题留待最后统一处理。现以835×49为例来说明程序的计算过程。先算835×9。5×9得到45个1,3×9得到27个10,8×9得到72个100。由于不急于处理进位,所以835×9算完后,aResult如下:图7-2-1接下来算4×5。此处4×5的结果代表20个10,因此要aResult[1]+=20,变为:图7-2-2再下来算4×3。此处4×3的结果代表12个100,因此要aResult[2]+=12,变为:图7-2-3最后算4×8。此处4×8的结果代表32个1000,因此要aResult[3]+=32,变为:图7-2-4乘法过程完毕。接下来从aResult[0]开始向高位逐位处理进位问题。aResult[0]留下5,把4加到aResult[1]上,aResult[1]变为51后,应留下1,把5加到aResult[2]上……最终使得aResult里的每个元素都是1位数,结果就算出来了:图7-2-5总结一个规律,即一个数的第i位和另一个数的第j位相乘所得的数,一定是要累加到结果的第i+j位上。这里i,j都是从右往左,从0开始数。参考程序:1.#include<stdio.h>2.#include<string.h>3.#defineMAX_LEN2004.unsignedan1[MAX_LEN+10];5.unsignedan2[MAX_LEN+10];6.unsignedaResult[MAX_LEN*2+10];7.charszLine1[MAX_LEN+10];8.charszLine2[MAX_LEN+10];9.intmain()10.{11.gets(szLine1);//gets函数读取一行12.gets(szLine2);13.inti,j;14.intnLen1=strlen(szLine1);15.memset(an1,0,sizeof(an1));16.memset(an2,0,sizeof(an2));17.memset(aResult,0,sizeof(aResult));18.j=0;19.for(i=nLen1-1;i>=0;i--)20.an1[j++]=szLine1[i]-'0';21.intnLen2=strlen(szLine2);22.j=0;23.for(i=nLen2-1;i>=0;i--)24.an2[j++]=szLine2[i]-'0';25.26.for(i=0;i<nLen2;i++){//每一轮都用an1的一位,去和an2各位相乘27.//从an1的个位开始28.for(j=0;j<nLen1;j++)//用选定的an1的那一位,去乘an2的各位29.aResult[i+j]+=an2[i]*an1[j];//两数第i,j位相乘,累加到结果的第i+j位30.}31.//下面的循环统一处理进位问题32.for(i=0;i<MAX_LEN*2;i++){33.if(aResult[i]>=10){34.aResult[i+1]+=aResult[i]/10;35.aResult[i]%=10;36.}37.}38.//下面输出结果39.boolbStartOutput=false;40.for(i=MAX_LEN*2;i>=0;i--)41.if(bStartOutput)42.printf("%d",aResult[i]);43.elseif(aResult[i]){44.printf("%d",aResult[i]);45.bStartOutput=true;46.}47.if(!bStartOutput)48.printf("0");49.return0;50.}实现技巧不一定一出现进位就马上处理,而是等全部结果算完后再统一处理进位,有时会方便些。思考题10.2:上面程序中,被执行次数最多的语句是哪一条?会被执行多少次?[例题]:大整数除法问题描述求两个大的正整数相除的商输入数据第1行是测试数据的组数n,每组测试数据占2行,第1行是被除数,第2行是除数。每组测试数据之间有一个空行,每行数据不超过100个字符输出要求n行,每组测试数据有一行输出是相应的整数商输入样例324053373129633733590092604577420574392304964939303555957976607910827396462987192585318701752584429931160870372907079248971095012509790550883793197894100000000000000000000000000000000000000001000000000054096567750978508956870567980689709345465465756767686784354353451输出样例010000000000000000000000000000005409656775097850895687056798068970934546546575676768678435435345解题思路基本的思想是反复做减法,看看从被除数里最多能减去多少个除数,商就是多少。一个一个减显然太慢,如何减得更快一些呢?以7546除以23为例来看一下:开始商为0。先减去23的100倍,就是2300,发现够减3次,余下646。于是商的值就增加300。然后用646减去230,发现够减2次,余下186,于是商的值增加20。最后用186减去23,够减8次,因此最终商就是328。所以本题的核心是要写一个大整数的减法函数,然后反复调用该函数进行减法操作。计算除数的10倍、100倍的时候,不用做乘法,直接在除数后面补0即可。参考程序1.#include<stdio.h>2.#include<string.h>3.#defineMAX_LEN2004.charszLine1[MAX_LEN+10];5.charszLine2[MAX_LEN+10];6.intan1[MAX_LEN+10];//被除数,an1[0]对应于个位7.intan2[MAX_LEN+10];//除数,an2[0]对应于个位8.intaResult[MAX_LEN+10];//存放商,aResult[0]对应于个位9./*Substract函数:长度为nLen1的大整数p1减去长度为nLen2的大整数p210.减的结果放在p1里,返回值代表结果的长度11.如不够减返回-1,正好减完返回012.p1[0]、p2[0]是个位*/13.intSubstract(int*p1,int*p2,intnLen1,intnLen2)14.{15.inti;16.if(nLen1<nLen2)17.return-1;18.//下面判断p1是否比p2大,如果不是,返回-119.boolbLarger=false;20.if(nLen1==nLen2){21.for(i=nLen1-1;i>=0;i--){22.if(p1[i]>p2[i])23.bLarger=true;24.elseif(p1[i]<p2[i]){25.if(!bLarger)26.return-1;27.}28.}29.}30.for(i=0;i<nLen1;i++){//做减法31.p1[i]-=p2[i];//要求调用本函数时给的参数能确保当i>=nLen2时,p2[i]=032.if(p1[i]<0){33.p1[i]+=10;34.p1[i+1]--;35.}36.}37.for(i=nLen1-1;i>=0;i--)38.if(p1[i])39.returni+1;40.return0;41.}42.intmain()43.{44.intt,n;45.charszBlank[20];46.scanf("%d",&n);47.for(t=0;t<n;t++){48.scanf("%s",szLine1);49.scanf("%s",szLine2);50.inti,j;51.intnLen1=strlen(szLine1);52.memset(an1,0,sizeof(an1));53.memset(an2,0,sizeof(an2));54.memset(aResult,0,sizeof(aResult));55.j=0;56.for(i=nLen1-1;i>=0;i--)57.an1[j++]=szLine1[i]-'0';58.intnLen2=strlen(szLine2);59.j=0;60.for(i=nLen2-1;i>=0;i--)61.an2[j++]=szLine2[i]-'0';62.if(nLen1<nLen2){63.printf("0\n");64.continue;65.}66.nLen1=Substract(an1,an2,nLen1,nLen2);67.if(nLen1<0){68.printf("0\n");69.continue;70.}71.elseif(nLen1==0){72.printf("1\n");73.continue;74.}75.aResult[0]++;//减掉一次了,商加176.//减去一次后的结果长度是nLen177.intnTimes=nLen1-nLen2;78.if(nTimes<0)//减一次后就不能再减了79.gotoOutputResult;80.elseif(nTimes>0){81.//将an2乘以10的某次幂,使得结果长度和an1相同82.for(i=nLen1-1;i>=0;i--){83.if(i>=nTimes)84.an2[i]=an2[i-nTimes];85.else86.an2[i]=0;87.}88.}89.nLen2=nLen1;90.for(j=0;j<=nTimes;j++){91.intnTmp;92.//一直减到不够减为止93.//先减去若干个an2×(10的nTimes次方),94.//不够减了,再减去若干个an2×(10的nTimes-1次方),......95.while((nTmp=Substract(an1,an2+j,nLen1,nLen2-j))>=0){96.nLen1=nTmp;97.aResult[nTimes-j]++;//每成功减一次,则将商的相应位加198.}99.}100.OutputResult:101.//下面的循环统一处理进位问题102.for(i=0;i<MAX_LEN;i++){103.if(aResult[i]>=10){104.aResult[i+1]+=aResult[i]/10;105.aResult[i]%=10;106.}107.}108.//下面输出结果109.boolbStartOutput=false;110.for(i=MAX_LEN;i>=0;i--)111.if(bStartOutput)112.printf("%d",aResult[i]);113.elseif(aResult[i]){114.printf("%d",aResult[i]);115.bStartOutput=true;116.}117.if(!bStartOutput)118.printf("0\n");119.printf("\n");120.}121.return0;122.}常见问题问题一、忘了针对每一组测试数据,都要先将an1,an2和aResult初始化成全0,而是一共只初始化了一次。这导致从第二组测试数据开始就都不对了。问题二、减法处理借位的时候,容易忽略连续借位的情况,比如10000–87,借位会一直进行到1。[例题]:麦森数问题描述形如2p-1的素数称为麦森数,这时P一定也是个素数。但反过来不一定,即如果P是个素数。2p-1不一定也是素数。到1998年底,人们已找到了37个麦森数。最大的一个是P=3021377,它有909526位。麦森数有许多重要应用,它与完全数密切相关。你的任务:输入P(1000<P<3100000),计算2p-1的位数和最后500位数字(用十进制高精度数表示)输入数据只包含一个整数P(1000<P<3100000)输出要求第1行:十进制高精度数2p-1的位数。第2-11行:十进制高精度数2p-1的最后500位数字。(每行输出50位,共输出10行,不足500位时高位补0)不必验证2p-1与P是否为素数。输入样例1279输出样例38600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010407932194664399081925240327364085538615262247266704805319112350403608059673360298012239441732324184842421613954281007791383566248323464908139906605677320762924129509389220345773183349661583550472959420547689811211693677147548478866962501384438260291732348885311160828538416585028255604666224831890918801847068222203140521026698435488732958028878050869736186900714720710555703168729087解题思路第一个问题,输出2p-1有多少位。由于2p-1的各位数只可能是2,4,6,8所以2p-1和2p的位数相同。使用C/C++标准库中在math.h里声明的,求以10为底的对数的函数doublelog10(doublex)函数,就能轻松求得2p-1的位数。2p的值需要用一种高效率的办法来算。显然,对于任何p>0,考虑p的二进制形式,则不难得到:这里,ai要么是1,要么是0。因而:计算2p的办法就是,先将结果的值设为1,计算21。如果a0值为1,则结果乘以21;计算22,如果a1为1,则结果乘以22;计算24,如果a2为1,则结果乘以24;……总之,第i步(i从0到n,an是1)就计算,如果ai为1,则结果就乘以。每次由×就能算出。由于p可能很大,所以上面的乘法都应该使用高精度计算。由于题目只要求输出500位,所以这些乘法都是只须算出末尾的500即可。在前面的高精度计算中,我们用数组来存放大整数,数组的一个元素对应于十进制大整数的一位。本题如果也这样做,就会超时。为了加快计算速度,可以用一个数组元素对应于大整数的4位,即将大整数表示为10000进制,而数组中的每一个元素就存放10000进制数的1位。例如,用int型数组a来存放整数6373384,那么只需两个数组元素就可以了,a[0]=3384,a[1]=637。由于只要求结果的最后500位数字,所以我们不需要计算完整的结果,只需算出最后500位即可。因为用每个数组元素存放十进制大整数的4位,所以本题中的数组最多只需要125个元素。参考程序(改编自学生提交的程序):1.#include<stdio.h>2.#include<memory.h>3.#defineLEN125//每数组元素存放十进制的4位,因此数组最多只要125个元素即可。4.#include<math.h>5./*Multiply函数功能是计算高精度乘法a*b6.结果的末500位放在a中7.*/8.voidMultiply(int*a,int*b)9.{10.inti,j;11.intnCarry;//存放进位12.intnTmp;13.intc[LEN];//存放结果的末500位14.memset(c,0,sizeof(int)*LEN);15.for(i=0;i<LEN;i++){16.nCarry=0;17.for(j=0;j<LEN-i;j++){18.nTmp=c[i+j]+a[j]*b[i]+nCarry;19.c[i+j]=nTmp%10000;20.nCarry=nTmp/10000;21.}22.}23.memcpy(a,c,LEN*sizeof(int));24.}25.intmain()26.{27.inti;28.intp;29.intanPow[LEN];//存放不断增长的2的次幂30.intaResult[LEN];//存放最终结果的末500位31.scanf("%d",&p);32.printf("%d\n",(int)(p*log10(2))+1);33.//下面将2的次幂初始化为2^(2^0)(a^b表示a的b次方),34.//最终结果初始化为135.anPow[0]=2;36.aResult[0]=1;37.for(i=1;i<LEN;i++){38.anPow[i]=0;39.aResult[i]=0;40.}41.42.//下面计算2的p次方43.while(p>0){//p=0则说明p中的有效位都用过了,不需再算下去44.if(p&1)//判断此时p中最低位是否为145.Multiply(aResult,anPow);46.p>>=1;47.Multiply(anPow,anPow);48.}49.50.aResult[0]--;//2的p次方算出后减151.52.//输出结果53.for(i=LEN-1;i>=0;i--){54.if(i%25==12)55.printf("%02d\n%02d",aResult[i]/100,56.aResult[i]%100);57.else{58.printf("%04d",aResult[i]);59.if(i%25==0)60.printf("\n");61.}62.}63.return0;64.}语句17:j只要算到LEN-i-1,是因为b[i]×a[j]的结果总是加到c[i+j]上,i+j大于等于LEN时,c[i+j]是不需要的,也不能要,否则c数组就越界了。语句18:b[i]×a[j]的结果总是要加到c[i+j]上,此外还要再加上上次更新c[i+j-1]时产生的进位。语句19:由于c中的每一元素代表10000进制数的1位,所以c[i+j]的值不能超过10000。语句20:算出进位。语句43到语句48,每次执行循环都判断ai(i从0开始)的值是否为1,如果是,则将最终结果乘以。接下来再由算出。语句54:输出从万进制数的第124位开始,万进制数的每一位输出为十进制数的4位,每行只能输出50个十进制位,所以发现当i%25等于12时,第i个万进制位会被折行输出,其对应的后两个十进制位会跑到下一行。语句55:“%02d”表示输出一个整数,当输出位数不足2位的时候,用前导0补足到2位。本行将一个万进制位分两半折行输出。语句58:将一个万进制位以十进制形式输出,用前导0确保输出宽度是4个字符。语句59:满足条件的话就该换行了。常见问题问题一:没有想到用数学公式和库函数可以直接计算结果位数,而是用其他办法大费周折。问题二:试图用最简单的办法,做p次乘以2的操作,结果严重超时。问题三:没有对数据规模有足够估计,用数组表示十进制大整数而非万进制数,结果超时。思考题10.3:本题在数组中存储万进制大整数以加快速度。如果存储的是十万进制数,岂不更快?而且输出时十万进制数的一位正好对应于十进制的5位,计算折行也会方便很多。这种想法成立吗?这么写会真的会更方便吗?练习题1.计算2的N次方任意给定一个正整数N(N<=100),计算2的N次方的值。2.浮点数加法求2个不超过100位的浮点数相加的和3.孙子问题浮点数加法对于给定的正整数a1,a2,...an,问是否存在正整数b1,b2,...bn,使得对于任意的一个正整数N,如果用N除以a1的余数是p1,用N除以a2的余数是p2……用N除以an的余数是pn,那么M=p1*b1+p2*b2+...+pn*bn能满足M除以a1的余数也是p1,M除以a2的余数也是p2……M除以an的余数也是pn。如果存在,则输出b1,b2,...bn。题中1<=n<=10,a1,a2,...an均不大于50。4.浮点数求高精度幂有一个实数R(0.0<R<99.999),要求写程序精确计算R的n次方。n是整数并且0<n<=25。附录ASCII表 八进制 十六进制 十进制 字符 八进制 十六进制 十进制 字符 00 00 0 nul 100 40 64 @ 01 01 1 soh 101 41 65 A 02 02 2 stx 102 42 66 B 03 03 3 etx 103 43 67 C 04 04 4 eot 104 44 68 D 05 05 5 enq 105 45 69 E 06 06 6 ack 106 46 70 F 07 07 7 bel 107 47 71 G 10 08 8 bs 110 48 72 H 11 09 9 ht 111 49 73 I 12 0a 10 nl 112 4a 74 J 13 0b 11 vt 113 4b 75 K 14 0c 12 ff 114 4c 76 L 15 0d 13 er 115 4d 77 M 16 0e 14 so 116 4e 78 N 17 0f 15 si 117 4f 79 O 20 10 16 dle 120 50 80 P 21 11 17 dc1 121 51 81 Q 22 12 18 dc2 122 52 82 R 23 13 19 dc3 123 53 83 S 24 14 20 dc4 124 54 84 T 25 15 21 nak 125 55 85 U 26 16 22 syn 126 56 86 V 27 17 23 etb 127 57 87 W 30 18 24 can 130 58 88 X 31 19 25 em 131 59 89 Y 32 1a 26 sub 132 5a 90 Z 33 1b 27 esc 133 5b 91 [ 34 1c 28 fs 134 5c 92 \ 35 1d 29 gs 135 5d 93 ] 36 1e 30 re 136 5e 94 ^ 37 1f 31 us 137 5f 95 _ 40 20 32 sp 140 60 96 ' 41 21 33 ! 141 61 97 a 42 22 34 " 142 62 98 b 43 23 35 # 143 63 99 c 44 24 36 $ 144 64 100 d 45 25 37 % 145 65 101 e 46 26 38 & 146 66 102 f 47 27 39 ` 147 67 103 g 50 28 40 ( 150 68 104 h 51 29 41 ) 151 69 105 i 52 2a 42 * 152 6a 106 j 53 2b 43 + 153 6b 107 k 54 2c 44 , 154 6c 108 l 55 2d 45 - 155 6d 109 m 56 2e 46 . 156 6e 110 n 57 2f 47 / 157 6f 111 o 60 30 48 0 160 70 112 p 61 31 49 1 161 71 113 q 62 32 50 2 162 72 114 r 63 33 51 3 163 73 115 s 64 34 52 4 164 74 116 t 65 35 53 5 165 75 117 u 66 36 54 6 166 76 118 v 67 37 55 7 167 77 119 w 70 38 56 8 170 78 120 x 71 39 57 9 171 79 121 y 72 3a 58 : 172 7a 122 z 73 3b 59 ; 173 7b 123 { 74 3c 60 < 174 7c 124 | 75 3d 61 = 175 7d 125 } 76 3e 62 > 176 7e 126 ~ 77 3f 63 ? 177 7f 127 delPAGE-229-_1359696881.unknown_1035551524.unknown
本文档为【信息学奥赛培训教程C++版】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
该文档来自用户分享,如有侵权行为请发邮件ishare@vip.sina.com联系网站客服,我们会及时删除。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。
本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。
网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。
下载需要: ¥17.0 已有0 人下载
最新资料
资料动态
专题动态
个人认证用户
油条
暂无简介~
格式:doc
大小:1MB
软件:Word
页数:0
分类:
上传时间:2020-04-21
浏览量:10