c语言教程(谭浩强版)

c语言教程(谭浩强版)

ID:30231290

大小:4.83 MB

页数:226页

时间:2018-12-28

上传者:U-14522
c语言教程(谭浩强版)_第1页
c语言教程(谭浩强版)_第2页
c语言教程(谭浩强版)_第3页
c语言教程(谭浩强版)_第4页
c语言教程(谭浩强版)_第5页
资源描述:

《c语言教程(谭浩强版)》由会员上传分享,免费在线阅读,更多相关内容在学术论文-天天文库

1.13.7Project菜单1.13.8Options菜单1.13.9Debug菜单1.13.10Break/watch菜单1.13.11TurboC2.0的配置文件2程序的灵魂—算法2.1算法的概念212.2简单算法举例212.3算法的特性242.4怎样表示一个算法242.4.1用自然语言表示算法242.4.2用流程图表示算法242.4.3三种基本结构和改进的流程图282.4.4用N-S流程图表示算法292.4.5用伪代码表示算法302.4.6用计算机语言表示算法312.5结构化程序设计方法313数据类型、运算符与表达式3.1C语言的数据类型323.2常量与变量332 3.2.1常量和符号常量333.2.2变量333.3整型数据343.3.1整型常量的表示方法343.3.2整型变量353.4实型数据373.4.1实型常量的表示方法373.4.2实型变量383.4.3实型常数的类型393.5字符型数据393.5.1字符常量393.5.2转义字符393.5.3字符变量403.5.4字符数据在内存中的存储形式及使用方法413.5.5字符串常量413.5.6符号常量423.6变量赋初值423.7各类数值型数据之间的混合运算433.8算术运算符和算术表达式443.8.1C运算符简介443.8.2算术运算符和算术表达式453.9赋值运算符和赋值表达式473 3.10逗号运算符和逗号表达式483.11小结493.11.1C的数据类型493.11.2基本类型的分类及特点493.11.3常量后缀493.11.4常量类型493.11.5数据类型转换493.11.6运算符优先级和结合性50表达式504最简单的C程序设计—顺序程序设计4.1C语句概述514.2赋值语句534.3数据输入输出的概念及在C语言中的实现544.4字符数据的输入输出544.4.1putchar函数(字符输出函数)544.4.2getchar函数(键盘输入函数)554.5格式输入与输出554.5.1printf函数(格式输出函数)564.5.2scanf函数(格式输入函数)58顺序结构程序设计举例604 5分支结构程序5.1关系运算符和表达式615.1.1关系运算符及其优先次序615.1.2关系表达式615.2逻辑运算符和表达式625.2.1逻辑运算符极其优先次序625.2.2逻辑运算的值635.2.3逻辑表达式635.3if语句645.3.1if语句的三种形式645.3.2if语句的嵌套675.3.3条件运算符和条件表达式695.4switch语句705.5程序举例716循环控制6.1概述716.2goto语句以及用goto语句构成循环716.3while语句726.4do-while语句746.5for语句766.6循环的嵌套795 6.7几种循环的比较796.8break和continue语句796.8.1break语句796.8.2continue语句806.9程序举例817数组7.1一维数组的定义和引用827.1.1一维数组的定义方式827.1.2一维数组元素的引用837.1.3一维数组的初始化847.1.4一维数组程序举例847.2二维数组的定义和引用867.2.1二维数组的定义867.2.2二维数组元素的引用867.2.3二维数组的初始化877.2.4二维数组程序举例897.3字符数组897.3.1字符数组的定义897.3.2字符数组的初始化897.3.3字符数组的引用907.3.4字符串和字符串结束标志916 7.3.5字符数组的输入输出917.3.6字符串处理函数927.4程序举例94本章小结978函数8.1概述988.2函数定义的一般形式998.3函数的参数和函数的值1008.3.1形式参数和实际参数1018.3.2函数的返回值1028.4函数的调用1068.4.1函数调用的一般形式1068.4.2函数调用的方式1068.4.3被调用函数的声明和函数原型1078.5函数的嵌套调用1088.6函数的递归调用1098.7数组作为函数参数1108.8局部变量和全局变量1128.8.1局部变量1138.8.2全局变量1198.9变量的存储类别1207 8.9.1动态存储方式与静态动态存储方式1208.9.2auto变量1208.9.3用static声明局部变量1218.9.4register变量122用extern声明外部变量1239预处理命令9.1概述1249.2宏定义1259.2.1无参宏定义1269.2.2带参宏定义1279.3文件包含1289.4条件编译1309.5本章小结10指针10.1地址指针的基本概念13110.2变量的指针和指向变量的指针变量13210.2.1定义一个指针变量13310.2.2指针变量的引用13310.2.3指针变量作为函数参数13710.2.4指针变量几个问题的进一步说明1408 10.3数组指针和指向数组的指针变量14110.3.1指向数组元素的指针14210.3.2通过指针引用数组元素14310.3.3数组名作函数参数14610.3.4指向多维数组的指针和指针变量14810.4字符串的指针指向字符串的针指变量15010.4.1字符串的表示形式15210.4.2使用字符串指针变量与字符数组的区别15810.5函数指针变量15910.6指针型函数16010.7指针数组和指向指针的指针16110.7.1指针数组的概念16110.7.2指向指针的指针16410.7.3main函数的参数16610.8有关指针的数据类型和指针运算的小结16710.8.1有关指针的数据类型的小结16710.8.2指针运算的小结16710.8.3void指针类型16811结构体与共用体11.1定义一个结构的一般形式17011.2结构类型变量的说明1729 11.3结构变量成员的表示方法17411.4结构变量的赋值17411.5结构变量的初始化17511.6结构数组的定义17511.7结构指针变量的说明和使用17711.7.1指向结构变量的指针17711.7.2指向结构数组的指针17911.7.3结构指针变量作函数参数18011.8动态存储分配18111.9链表的概念18211.10枚举类型18411.10.1枚举类型的定义和枚举变量的说明18411.10.2枚举类型变量的赋值和使用18511.11类型定义符typedef12位运算12.1位运算符C语言提供了六种位运算符:18912.1.1按位与运算19112.1.2按位或运算19212.1.3按位异或运算19212.1.4求反运算19312.1.5左移运算19310 12.1.6右移运算19312.2位域(位段)19412.3本章小结13文件13.1C文件概述19713.2文件指针19813.3文件的打开与关闭19913.3.1文件的打开(fopen函数)20013.3.2文件关闭函数(fclose函数)20213.4文件的读写20413.4.1字符读写函数fgetc和fputc20413.4.2字符串读写函数fgets和fputs20813.4.3数据块读写函数fread和fwtrite20913.4.4格式化读写函数fscanf和fprintf20113.5文件的随机读写20213.5.1文件定位20213.5.2文件的随机读写20313.6文件检测函数20413.6.1文件结束检测函数feof函数20413.6.2读写文件出错检测函数20511 13.6.3文件出错标志和文件结束标志置0函数20613.7C库文件20813.8本章小结12 谭浩强C语言程序设计2001年5月1日C语言教程1C语言概述1.1C语言的发展过程C语言是在70年代初问世的。一九七八年由美国电话电报公司(AT&T)贝尔实验室正式发表了C语言。同时由B.W.Kernighan和D.M.Ritchit合著了著名的“THECPROGRAMMINGLANGUAGE”一书。通常简称为《K&R》,也有人称之为《K&R》标准。但是,在《K&R》中并没有定义一个完整的标准C语言,后来由美国国家标准协会(AmericanNationalStandardsInstitute)在此基础上制定了一个C语言标准,于一九八三年发表。通常称之为ANSIC。1.2当代最优秀的程序设计语言早期的C语言主要是用于UNIX系统。由于C语言的强大功能和各方面的优点逐渐为人们认识,到了八十年代,C开始进入其它操作系统,并很快在各类大、中、小和微型计算机上得到了广泛的使用,成为当代最优秀的程序设计语言之一。1.3C语言版本目前最流行的C语言有以下几种:·MicrosoftC或称MSC·BorlandTurboC或称TurboC·AT&TC这些C语言版本不仅实现了ANSIC标准,而且在此基础上各自作了一些扩充,使之更加方便、完美。1.4C语言的特点1·C语言简洁、紧凑,使用方便、灵活。ANSIC一共只有32个关键字:autobreakcasecharconstcontinuedefaultdodoubleelseenumexternfloatforgotoifintlongregisterreturnshortsignedstaticsizofstructswitchtypedefunionunsignedvoidvolatilewhile9种控制语句,程序书写自由,主要用小写字母表示,压缩了一切不必要的成分。TurboC扩充了11个关键字:asm_cs_ds_es_sscdeclfar 谭浩强C语言程序设计2001年5月1日hugeinterruptnearpascal注意:在C语言中,关键字都是小写的。2·运算符丰富。共有34种。C把括号、赋值、逗号等都作为运算符处理。从而使C的运算类型极为丰富,可以实现其他高级语言难以实现的运算。3·数据结构类型丰富。4·具有结构化的控制语句。5·语法限制不太严格,程序设计自由度大。6·C语言允许直接访问物理地址,能进行位(bit)操作,能实现汇编语言的大部分功能,可以直接对硬件进行操作。因此有人把它称为中级语言。7·生成目标代码质量高,程序执行效率高。8·与汇编语言相比,用C语言写的程序可移植性好。但是,C语言对程序员要求也高,程序员用C写程序会感到限制少、灵活性大,功能强,但较其他高级语言在学习上要困难一些。1.5面向对象的程序设计语言在C的基础上,一九八三年又由贝尔实验室的BjarneStrou-strup推出了C++。C++进一步扩充和完善了C语言,成为一种面向对象的程序设计语言。C++目前流行的最新版本是BorlandC++,SymantecC++和MicrosoftVisualC++。C++提出了一些更为深入的概念,它所支持的这些面向对象的概念容易将问题空间直接地映射到程序空间,为程序员提供了一种与传统结构程序设计不同的思维方式和编程方法。因而也增加了整个语言的复杂性,掌握起来有一定难度。1.6C和C++但是,C是C++的基础,C++语言和C语言在很多方面是兼容的。因此,掌握了C语言,再进一步学习C++就能以一种熟悉的语法来学习面向对象的语言,从而达到事半功倍的目的。1.7简单的C程序介绍为了说明C语言源程序结构的特点,先看以下几个程序。这几个程序由简到难,表现了C语言源程序在组成结构上的特点。虽然有关内容还未介绍,但可从这些例子中了解到组成一个C源程序的基本部分和书写格式。【例1.1】main(){printf("世界,您好! ");}òmain是主函数的函数名,表示这是一个主函数。ò每一个C源程序都必须有,且只能有一个主函数(main函数)。ò函数调用语句,printf函数的功能是把要输出的内容送到显示器去显示。 谭浩强C语言程序设计2001年5月1日òprintf函数是一个由系统定义的标准函数,可在程序中直接调用。【例1.2】#include#includemain(){doublex,s;printf("inputnumber: ");scanf("%lf",&x);s=sin(x);printf("sineof%lfis%lf ",x,s);}òinclude称为文件包含命令ò扩展名为.h的文件称为头文件ò定义两个实数变量,以被后面程序使用ò显示提示信息ò从键盘获得一个实数xò求x的正弦,并把它赋给变量sò显示程序运算结果òmain函数结束程序的功能是从键盘输入一个数x,求x的正弦值,然后输出结果。在main()之前的两行称为预处理命令(详见后面)。预处理命令还有其它几种,这里的include称为文件包含命令,其意义是把尖括号<>或引号""内指定的文件包含到本程序来,成为本程序的一部分。被包含的文件通常是由系统提供的,其扩展名为.h。因此也称为头文件或首部文件。C语言的头文件中包括了各个标准库函数的函数原型。因此,凡是在程序中调用一个库函数时,都必须包含该函数原型所在的头文件。在本例中,使用了三个库函数:输入函数scanf,正弦函数sin,输出函数printf。sin函数是数学函数,其头文件为math.h文件,因此在程序的主函数前用include命令包含了math.h。scanf和printf是标准输入输出函数,其头文件为stdio.h,在主函数前也用include命令包含了stdio.h文件。需要说明的是,C语言规定对scanf和printf这两个函数可以省去对其头文件的包含命令。所以在本例中也可以删去第二行的包含命令#include。同样,在例1.1中使用了printf函数,也省略了包含命令。在例题中的主函数体中又分为两部分,一部分为说明部分,另一部为分执行部分。说明是指变量的类型说明。例题1.1中未使用任何变量,因此无说明部分。C语言规定,源程序中所有用到的变量都必须先说明,后使用,否则将会出错。这一点是编译型高级程序设计语言的一个特点,与解释型的BASIC语言是不同的。说明部分是C源程序结构中很重要的组成部分。本例中使用了两个变量x,s,用来表示输入的自变量和sin函数值。由于sin函数要求这两个量必须是双精度浮点型,故用类型说明符double来说明这两个变量。说明部分后的四行为执行部分或称为执行语句部分,用以完成程序的功能。执行部分的第一行是输出语句,调用printf函数在显示器上输出提示字符串,请操作人员输入自变量x的值。第二行为输入语句,调用scanf函数,接受键盘上输入的数并存入变量x中。第三行是调用sin函数并把函数值送到变量s中。第四行是用printf函数输出变量s的值,即x的正弦值。程序结束。运行本程序时,首先在显示器屏幕上给出提示串inputnumber,这是由执行部分的第一 谭浩强C语言程序设计2001年5月1日行完成的。用户在提示下从键盘上键入某一数,如5,按下回车键,接着在屏幕上给出计算结果。1.8输入和输出函数在前两个例子中用到了输入和输出函数scanf和printf,在以后要详细介绍。这里我们先简单介绍一下它们的格式,以便下面使用。scanf和printf这两个函数分别称为格式输入函数和格式输出函数。其意义是按指定的格式输入输出值。因此,这两个函数在括号中的参数表都由以下两部分组成:“格式控制串”,参数表格式控制串是一个字符串,必须用双引号括起来,它表示了输入输出量的数据类型。各种类型的格式表示法可参阅第三章。在printf函数中还可以在格式控制串内出现非格式控制字符,这时在显示屏幕上将原文照印。参数表中给出了输入或输出的量。当有多个量时,用逗号间隔。例如:printf("sineof%lfis%lf ",x,s);其中%lf为格式字符,表示按双精度浮点数处理。它在格式串中两次现,对应了x和s两个变量。其余字符为非格式字符则照原样输出在屏幕上。【例1.3】intmax(inta,intb);/*函数说明*/main()/*主函数*/{intx,y,z;/*变量说明*/intmax(inta,intb);/*函数说明*/printf("inputtwonumbers: ");scanf("%d%d",&x,&y);/*输入x,y值*/z=max(x,y);/*调用max函数*/printf("maxmum=%d",z);/*输出*/}intmax(inta,intb)/*定义max函数*/{if(a>b)returna;elsereturnb;/*把结果返回主调函数*/}上面例中程序的功能是由用户输入两个整数,程序执行后输出其中较大的数。本程序由两个函数组成,主函数和max函数。函数之间是并列关系。可从主函数中调用其它函数。max函数的功能是比较两个数,然后把较大的数返回给主函数。max函数是一个用户自定义函数。因此在主函数中要给出说明(程序第三行)。可见,在程序的说明部分中,不仅可以有变量说明,还可以有函数说明。关于函数的详细内容将在以后第五章介绍。在程序的每行后用/*和*/括起来的内容为注释部分,程序不执行注释部分。上例中程序的执行过程是,首先在屏幕上显示提示串,请用户输入两个数,回车后由scanf函数语句接收这两个数送入变量x,y中,然后调用max函数,并把x,y的值传送给max函数的参数a,b。在max函数中比较a,b的大小,把大者返回给主函数的变量z,最后在屏幕上输出z的值。 谭浩强C语言程序设计2001年5月1日1.9C源程序的结构特点1.一个C语言源程序可以由一个或多个源文件组成。2.每个源文件可由一个或多个函数组成。3.一个源程序不论由多少个文件组成,都有一个且只能有一个main函数,即主函数。4.源程序中可以有预处理命令(include命令仅为其中的一种),预处理命令通常应放在源文件或源程序的最前面。5.每一个说明,每一个语句都必须以分号结尾。但预处理命令,函数头和花括号“}”之后不能加分号。6.标识符,关键字之间必须至少加一个空格以示间隔。若已有明显的间隔符,也可不再加空格来间隔。1.10书写程序时应遵循的规则从书写清晰,便于阅读,理解,维护的角度出发,在书写程序时应遵循以下规则:1.一个说明或一个语句占一行。2.用{}括起来的部分,通常表示了程序的某一层次结构。{}一般与该结构语句的第一个字母对齐,并单独占一行。3.低一层次的语句或说明可比高一层次的语句或说明缩进若干格后书写。以便看起来更加清晰,增加程序的可读性。在编程时应力求遵循这些规则,以养成良好的编程风格。1.11C语言的字符集字符是组成语言的最基本的元素。C语言字符集由字母,数字,空格,标点和特殊字符组成。在字符常量,字符串常量和注释中还可以使用汉字或其它可表示的图形符号。1.字母小写字母a~z共26个大写字母A~Z共26个2.数字0~9共10个3.空白符空格符、制表符、换行符等统称为空白符。空白符只在字符常量和字符串常量中起作用。在其它地方出现时,只起间隔作用,编译程序对它们忽略不计。因此在程序中使用空白符与否,对程序的编译不发生影响,但在程序中适当的地方使用空白符将增加程序的清晰性和可读性。4.标点和特殊字符1.12C语言词汇在C语言中使用的词汇分为六类:标识符,关键字,运算符,分隔符,常量,注释符等。 谭浩强C语言程序设计2001年5月1日1.标识符在程序中使用的变量名、函数名、标号等统称为标识符。除库函数的函数名由系统定义外,其余都由用户自定义。C规定,标识符只能是字母(A~Z,a~z)、数字(0~9)、下划线(_)组成的字符串,并且其第一个字符必须是字母或下划线。以下标识符是合法的:a,x,x3,BOOK_1,sum5以下标识符是非法的:3s以数字开头s*T出现非法字符*-3x以减号开头bowy-1出现非法字符-(减号)在使用标识符时还必须注意以下几点:(1)标准C不限制标识符的长度,但它受各种版本的C语言编译系统限制,同时也受到具体机器的限制。例如在某版本C中规定标识符前八位有效,当两个标识符前八位相同时,则被认为是同一个标识符。(2)在标识符中,大小写是有区别的。例如BOOK和book是两个不同的标识符。(3)标识符虽然可由程序员随意定义,但标识符是用于标识某个量的符号。因此,命名应尽量有相应的意义,以便于阅读理解,作到“顾名思义”。2.关键字关键字是由C语言规定的具有特定意义的字符串,通常也称为保留字。用户定义的标识符不应与关键字相同。C语言的关键字分为以下几类:(1)类型说明符用于定义、说明变量、函数或其它数据结构的类型。如前面例题中用到的int,double等(2)语句定义符用于表示一个语句的功能。如例1.3中用到的ifelse就是条件语句的语句定义符。(3)预处理命令字用于表示一个预处理命令。如前面各例中用到的include。3.运算符C语言中含有相当丰富的运算符。运算符与变量,函数一起组成表达式,表示各种运算功能。运算符由一个或多个字符组成。4.分隔符在C语言中采用的分隔符有逗号和空格两种。逗号主要用在类型说明和函数参数表中,分隔各个变量。空格多用于语句各单词之间,作间隔符。在关键字,标识符之间必须要有一个以上的空格符作间隔,否则将会出现语法错误,例如把inta;写成inta;C编译器会把inta当成一个标识符处理,其结果必然出错。5.常量C语言中使用的常量可分为数字常量、字符常量、字符串常量、符号常量、转义字符等多种。在后面章节中将专门给予介绍。6.注释符C语言的注释符是以“/*”开头并以“*/”结尾的串。在“/*”和“*/”之间的即为注释。程序编译时,不对注释作任何处理。注释可出现在程序中的任何位置。注释用来向用户提示或解释程序的意义。在调试程序中对暂不使用的语句也可用注释符括起来,使翻译跳过不作处理,待调试结束后再去掉注释符。 谭浩强C语言程序设计2001年5月1日1.13TurboC2.0集成开发环境的使用1.13.1TurboC2.0简介和启动我们上机实习和将来考试都是使用BorlandTurboC2.0这个版本。该系统是DOS操作系统支持下的软件,在windows98环境下,可以在DOS窗口下运行。我们机房是在D盘根目录下建立一个TC子目录下安装TurboC2.0系统的。TC下还建立了两个了目录LIB和INCLUDE,LIB子目录中存放库文件,INCLUDE子目录中存放所有头文件。在DOS环境下或在windows98的DOS窗口下运行运行TurboC2.0时,只要在TC子目录下键入TC并回车即可进入TurboC2.0集成开发环境。在windows98环境下,也可以选运行菜单,然后键入d:tctc即可,也可以在tc文件夹找到tc.exe文件,然后用鼠标双击该文件名也可进入TurboC2.0集成开发环境。TurboC是美国Borland公司的产品,Borland公司是一家专门从事软件开发、研制的大公司。该公司相继推出了一套Turbo系列软件,如TurboBASIC,TurboPascal,TurboProlog,这些软件很受用户欢迎。该公司在1987年首次推出TurboC1.0产品,其中使用了全然一新的集成开发环境,即使用了一系列下拉式菜单,将文本编辑、程序编译、连接以及程序运行一体化,大大方便了程序的开发。1988年,Borland公司又推出TurboC1.5版本,增加了图形库和文本窗口函数库等,而TurboC2.0则是该公司1989年出版的。TurboC2.0在原来集成开发环境的基础上增加了查错功能,并可以在Tiny模式下直接生成.COM(数据、代码、堆栈处在同一64K内存中)文件。还可对数学协处理器(支持8087/80287/80387等)进行仿真。Borland公司后来又推出了面向对象的程序软件包TurboC++,它继承发展TurboC2.0的集成开发环境,并包含了面向对象的基本思想和设计方法。1991年为了适用Microsoft公司的Windows3.0版本,Borland公司又将TurboC++作了更新,即TurboC的新一代产品BorlandcC++也已经问世了。1.13.2TurboC2.0集成开发环境进入TurboC2.0集成开发环境中后,屏幕上显示: 谭浩强C语言程序设计2001年5月1日其中顶上一行为TurboC2.0主菜单,中间窗口为编辑区,接下来是信息窗口,最底下一行为参考行。这四个窗口构成了TurboC2.0的主屏幕,以后的编程、编译、调试以及运行都将在这个主屏幕中进行。主菜单在TurboC2.0主屏幕顶上一行,显示下列内容:FileEditRunCompileProjectOptionsDebugBreak/watch除Edit外,其它各项均有子菜单,只要用Alt加上某项中第一个字母,就可进入该项的子菜单中。1.13.3File菜单按Alt+F可进入File菜单,如图:File菜单的子菜单共有9项,分别叙述如下:1.Load:装入一个文件,可用类似DOS的通配符(如*.C)来进行列表选择。也可装入其它 谭浩强C语言程序设计2001年5月1日扩展名的文件,只要给出文件名(或只给路径)即可。该项的热键为F3,即只要按F3即可进入该项,而不需要先进入File菜单再选此项。2.Pick:将最近装入编辑窗口的8个文件列成一个表让用户选择,选择后将该程序装入编辑区,并将光标置在上次修改过的地方。其热健为Alt-F3。3.New:新建文件,缺省文件名为NONAME.C,存盘时可改名。4.Save:将编辑区中的文件存盘,若文件名是NONAME.C时,将询问是否更改文件名,其热键为F2。5.Writeto:可由用户给出文件名将编辑区中的文件存盘,若该文件已存在,则询问要不要覆盖。6.Directory:显示目录及目录中的文件,并可由用户选择。7.Changedir:显示当前默认目录,用户可以改变默认目录。8.Osshell:暂时退出TurboC2.0到DOS提示符下,此时可以运行DOS命令,若想回到TurboC2.0中,只要在DOS状态下键入EXIT即可。9.Quit:退出TurboC2.0,返回到DOS操作系统中,其热键为Alt+X。说明:以上各项可用光标键移动色棒进行选择,回车则执行。也可用每一项的第一个大写字母直接选择。若要退到主菜单或从它的下一级菜单列表框退回均可用Esc键,TurboC2.0所有菜单均采用这种方法进行操作,以下不再说明。1.13.4Edit菜单按Alt+E可进入编辑菜单,若再回车,则光标出现在编辑窗口,此时用户可以进行文本编辑。编辑方法基本与wordstar相同,可用F1键获得有关编辑方法的帮助信息。1.与编辑有关的功能键如下:F1获得TurboC2.0编辑命令的帮助信息;F5扩大编辑窗口到整个屏幕;F6在编辑窗口与信息窗口之间进行切换;F10从编辑窗口转到主菜单。2.编辑命令简介:PageUp向前翻页PageDn向后翻页Home将光标移到所在行的开始End将光标移到所在行的结尾Ctrl+Y删除光标所在的一行Ctrl+T删除光标所在处的一个词Ctrl+KB设置块开始Ctrl+KK设置块结尾Ctrl+KV块移动Ctrl+KC块拷贝Ctrl+KY块删除Ctrl+KR读文件Ctrl+KW存文件Ctrl+KP块文件打印Ctrl+F1如果光标所在处为TurboC2.0库函数,则获得有关该函数的帮助信息 谭浩强C语言程序设计2001年5月1日Ctrl+Q[查找TurboC2.0双界符的后匹配符Ctrl+Q]查找TurboC2.0双界符的前匹配符说明:1)TurboC2.0的双界符包括以下几种符号:a)花括符{和}b)尖括符<和>c)圆括符(和)d)方括符[和]e)注释符/*和*/f)双引号"g)单引号'2)TuroC2.0b在编辑文件时还有一种功能,就是能够自动缩进,即光标定位和上一个非空字符对齐。在编辑窗口中,Ctrl+OL为自动缩进开关的控制键。1.13.5Run菜单按Alt+R可进入Run菜单,该菜单有以下各项,如图所示:1.Run:运行由Project/Projectname项指定的文件名或当前编辑区的文件。如果对上次编译后的源代码未做过修改,则直接运行到下一个断点(没有断点则运行到结束)。否则先进行编译、连接后才运行,其热键为Ctrl+F9。2.Programreset:中止当前的调试,释放分给程序的空间,其热键为Ctrl+F2。3.Gotocursor::调试程序时使用,选择该项可使程序运行到光标所在行。光标所在行必须为一条可执行语句,否则提示错误。其热键为F4。4.Traceinto:在执行一条调用其它用户定义的子函数时,若用Traceinto项,则执行长条将跟踪到该子函数内部去执行,其热键为F7。5.Stepover:执行当前函数的下一条语句,即使用户函数调用,执行长条也不会跟踪进函数内部,其热键为F8。6.Userscreen:显示程序运行时在屏幕上显示的结果。其热键为Alt+F5。 谭浩强C语言程序设计2001年5月1日1.13.6Compile菜单按Alt+C可进入Compile菜单,该菜单有以下几个内容,如图所示:1.CompiletoOBJ:将一个C源文件编译生成.OBJ目标文件,同时显示生成的文件名。其热键为Alt+F9。2.MakeEXEfile:此命令生成一个.EXE的文件,并显示生成的.EXE文件名。其中.EXE文件名是下面几项之一:1)由Project/Projectname说明的项目文件名。2)若没有项目文件名,则由PrimaryCfile说明的源文件。3)若以上两项都没有文件名,则为当前窗口的文件名。3.LinkEXfileE:把当前.OBJ文件及库文件连接在一起生成.EXE文件。4.Buildall:重新编译项目里的所有文件,并进行装配生成.EXE文件。该命令不作过时检查(上面的几条命令要作过时检查,即如果目前项目里源文件的日期和时间与目标文件相同或更早,则拒绝对源文件进行编译)。5.PrimaryCfile:当在该项中指定了主文件后,在以后的编译中,如没有项目文件名则编译此项中规定的主C文件,如果编译中有错误,则将此文件调入编辑窗口,不管目前窗口中是不是主C文件。6.Getinfo:获得有关当前路径、源文件名、源文件字节大小、编译中的错误数目、可用空间等信息,如图: 谭浩强C语言程序设计2001年5月1日1.13.7Project菜单按Alt+P可进入Project菜单,该菜单包括以下内容,如图所示:1.Projectname:项目名具有.PRJ的扩展名,其中包括将要编译、连接的文件名。例如有一个程序由file1.c,file2.c,file3.c组成,要将这3个文件编译装配成一个file.exe的执行文件,可以先建立一个file.prj的项目文件,其内容如下:file1.cfile2.cfile3.c此时将file.prj放入Projectname项中,以后进行编译时将自动对项目文件中规定的三个源文件分别进行编译。然后连接成file.exe文件。如果其中有些文件已经编译成.OBJ文 谭浩强C语言程序设计2001年5月1日件,而又没有修改过,可直接写上.OBJ扩展名。此时将不再编译而只进行连接。例如:file1.objfile2.cfile3.c将不对file1.c进行编译,而直接连接。说明:当项目文件中的每个文件无扩展名时,均按源文件对待,另外,其中的文件也可以是库文件,但必须写上扩展名.LIB。2.Breakmakeon:由用户选择是否在有Warining、Errors、FatalErrors时或Link之前退出Make编译。3.Autodependencies:当开关置为on,编译时将检查源文件与对应的.OBJ文件日期和时间,否则不进行检查。4.Clearproject:清除Project/Projectname中的项目文件名。5.Removemessages:把错误信息从信息窗口中清除掉。1.13.8Options菜单按Alt+O可进入Options菜单,该菜单对初学者来说要谨慎使用,该菜单有以下几个内容,如图所示:1.Compiler:本项选择又有许多子菜单,可以让用户选择硬件配置、存储模型、调试技术、代码优化、对话信息控制和宏定义。这些子菜单如图所示: 谭浩强C语言程序设计2001年5月1日1)Model:共有Tiny,small,medium,compact,large,huge六种不同模式可由同户选择。2)Define:打开一个宏定义框,同户可输入宏定义。多重定义可同分号,赋值可用等号。3)Codegeneration:它又有许多任选项,这些任选项告诉编译器产生什么样的目标代码。µCallingconvention可选择C或Pascal方式传递参数。µInstructionset可选择8088/8086或80186/80286指令系列。µFloatingpoint可选择仿真浮点、数学协处理器浮点或无浮点运算。µDefaultchartype规定char的类型。µAlignonent规定地址对准原则。µMergeduplicatestrings作优化用,将重复的字符串合并在一起。µStandardstackframe产生一个标准的栈结构。µTeststackoverflow产生一段程序运行时检测堆栈溢出的代码。µLinenumber在.OBJ文件中放进行号以供调试时用。µOBJdebuginformation在.OBJ文件中产生调试信息。4)Optimization:它又有许多任选项。µOptimizefor选择是对程序小型化还是对程序速度进行优化处理。µUseregistervariable用来选择是否允许使用寄存器变量。µRegisteroptimization尽可能使用寄存器变量以减少过多的取数操作。µJumpoptimization通过去除多余的跳转和调整循环与开关语句的办法,压缩代码。5)Source:它又有许多任选项。µIndentifierlength说明标识符有效字符的个数,默认为32个。µNestedcomments是否允许嵌套注释。µANSIkeywordsonly是只允许ANSI关键字还是也允许TurboC2.0关键字。6)ErrorµErrorstopafter多少个错误时停止编译,默认为25个。µWarningstopafter多少个警告错误时停止编译,默认为100个。 谭浩强C语言程序设计2001年5月1日µDisplaywarningµPortabilitywarning移植性警告错误。µANSIViolations侵犯了ANSI关键字的警告错误。µCommonerror常见的警告错误。µLesscommonerror少见的警告错误。7)Names:用于改变段(segment)、组(group)和类(class)的名字,默认值为CODE,DATA,BSS。2.Linker:本菜单设置有关连接的选择项,它有以下内容,如图所示:1)Mapfilemenu选择是否产生.MAP文件。2)Initializesegments是否在连接时初始化没有初始化的段。3)Devaultlibraries是否在连接其它编译程序产生的目标文件时去寻找其缺省库。4)Graphicslibrary是否连接graphics库中的函数。5)Warnduplicatesymbols当有重复符号时产生警告信息。6)Stackwarinig是否让连接程序产生Nostack的警告信息。7)Case-sensitivelink是否区分大、小写字。3.Environment:菜单规定是否对某些文件自动存盘及制表键和屏幕大小的设置,它有以下内容,如图所示: 谭浩强C语言程序设计2001年5月1日1)Messagetracking:µCurrentfile跟踪在编辑窗口中的文件错误。µAllfiles跟踪所有文件错误。µOff不跟踪。2)Keepmessage:编译前是否清除Message窗口中的信息。3)Configautosave:选on时,在Run,Shell或退出集成开发环境之前,如果TurboC2.0的配置被改过,则所做的改动将存入配置文件中。选off时不存。4)Editautosave:是否在Run或Shell之前,自动存储编辑的源文件。5)Backupfile:是否在源文件存盘时产生后备文件(.BAK文件)。6)Tabsize:设置制表键大小,默认为8。7)Zoomedwindows:将现行活动窗口放大到整个屏幕,其热键为F5。8)Screensize设置屏幕文本大小。4.Directories:规定编译、连接所需文件的路径,有下列各项,如图所示: 谭浩强C语言程序设计2001年5月1日(1)Includedirectories:包含文件的路径,多个子目录用";"分开。(2)Librarydirectories:库文件路径,多个子目录用";"分开。(3)Outputdirectoried:输出文件(.OBJ,.EXE,.MAP文件)的目录。(4)TurboCdirectoried:TurboC所在的目录。(5)Pickfilename:定义加载的pick文件名,如不定义则从currentpickfile中取。5.Arguments:允许用户使用命令行参数。6.Saveoptions:保存所有选择的编译、连接、调试和项目到配置文件中,缺省的配置文件为TCCONFIG.TC。7.Retriveoptions装入一个配置文件到TC中,TC将使用该文件的选择项。1.13.9Debug菜单按Alt+D可选择Debug菜单,该菜单主要用于查错,它包括以下内容,如图所示: 谭浩强C语言程序设计2001年5月1日1.Evaluate1)Expression要计算结果的表达式。2)Result显示表达式的计算结果。3)Newvalue赋给新值。2.Callstack:该项不可接触。而在TurboCdebuger时用于检查堆栈情况。3.Findfunction在运行TurboCdebugger时用于显示规定的函数。4.Refreshdisplay如果编辑窗口偶然被用户窗口重写了可用此恢复编辑窗口的内容。1.13.10Break/watch菜单按Alt+B可进入Break/watch菜单,该菜单有以下内容,如图所示:1.Addwatch:向监视窗口插入一监视表达式。 谭浩强C语言程序设计2001年5月1日2.Deletewatch:从监视窗口中删除当前的监视表达式。3.Editwatch:在监视窗口中编辑一个监视表达式。4.Removeall:watches从监视窗口中删除所有的监视表达式。5.Togglebreakpoint:对光标所在的行设置或清除断点。6.Clearallbreakpoints:清除所有断点。7.Viewnextbreakpoint:将光标移动到下一个断点处。1.13.11TurboC2.0的配置文件所谓配置文件是包含TurboC2.0有关信息的文件,其中存有编译、连接的选择和路径等信息。可以用下述方法建立TurboC2.0的配置:1.建立用户自命名的配置文件:可以从Options菜单中选择Options/Saveoptions命令,将当前集成开发环境的所有配置存入一个由用户命名的配置文件中。下次启动TC时只要在DOS下键入:tc/c<用户命名的配置文件名>就会按这个配置文件中的内容作为TurboC2.0的选择。2.若设置Options/Environment/Configautosave为on,则退出集成开发环境时,当前的设置会自动存放到TurboC2.0配置文件TCCONFIG.TC中。TurboC在启动时会自动寻找这个配置文件。3.用TCINST设置TurboC的有关配置,并将结果存入TC.EXE中。TurboC在启动时,若没有找到配置文件,则取TC.EXE中的缺省值。 谭浩强C语言程序设计2001年5月1日2程序的灵魂—算法一个程序应包括:ò对数据的描述。在程序中要指定数据的类型和数据的组织形式,即数据结构(datastructure)。ò对操作的描述。即操作步骤,也就是算法(algorithm)。NikiklausWirth提出的公式:数据结构+算法=程序教材认为:程序=算法+数据结构+程序设计方法+语言工具和环境这4个方面是一个程序涉及人员所应具备的知识。本课程的目的是使同学知道怎样编写一个C程序,进行编写程序的初步训练,因此,只介绍算法的初步知识。2.1算法的概念做任何事情都有一定的步骤。为解决一个问题而采取的方法和步骤,就称为算法。ò计算机算法:计算机能够执行的算法。ò计算机算法可分为两大类:¢数值运算算法:求解数值;¢非数值运算算法:事务管理领域。2.2简单算法举例【例2.1】求1×2×3×4×5。最原始方法:步骤1:先求1×2,得到结果2。步骤2:将步骤1得到的乘积2乘以3,得到结果6。步骤3:将6再乘以4,得24。步骤4:将24再乘以5,得120。这样的算法虽然正确,但太繁。改进的算法:S1:使t=1S2:使i=2S3:使t×i,乘积仍然放在在变量t中,可表示为t×i→tS4:使i的值+1,即i+1→i 谭浩强C语言程序设计2001年5月1日S5:如果i≤5,返回重新执行步骤S3以及其后的S4和S5;否则,算法结束。如果计算100!只需将S5:若i≤5改成i≤100即可。如果该求1×3×5×7×9×11,算法也只需做很少的改动:S1:1→tS2:3→iS3:t×i→tS4:i+2→tS5:若i≤11,返回S3,否则,结束。该算法不仅正确,而且是计算机较好的算法,因为计算机是高速运算的自动机器,实现循环轻而易举。思考:若将S5写成:S5:若i<11,返回S3;否则,结束。【例2.2】有50个学生,要求将他们之中成绩在80分以上者打印出来。如果,n表示学生学号,ni表示第个学生学号;g表示学生成绩,gi表示第个学生成绩;则算法可表示如下:S1:1→iS2:如果gi≥80,则打印ni和gi,否则不打印S3:i+1→iS4:若i≤50,返回S2,否则,结束。【例2.3】判定2000—2500年中的每一年是否闰年,将结果输出。润年的条件:1)能被4整除,但不能被100整除的年份;2)能被100整除,又能被400整除的年份;设y为被检测的年份,则算法可表示如下:S1:2000→yS2:若y不能被4整除,则输出y“不是闰年”,然后转到S6S3:若y能被4整除,不能被100整除,则输出y“是闰年”,然后转到S6S4:若y能被100整除,又能被400整除,输出y“是闰年”否则输出y“不是闰年”,然后转到S6S5:输出y“不是闰年”。S6:y+1→yS7:当y≤2500时,返回S2继续执行,否则,结束。 谭浩强C语言程序设计2001年5月1日111111−+−+...+−【例2.4】求23499100。算法可表示如下:S1:sigh=1S2:sum=1S3:deno=2S4:sigh=(-1)×sighS5:term=sigh×(1/deno)S6:term=sum+termS7:deno=deno+1S8:若deno≤100,返回S4;否则,结束。【例2.5】对一个大于或等于3的正整数,判断它是不是一个素数。算法可表示如下:S1:输入n的值S2:i=2S3:n被i除,得余数rS4:如果r=0,表示n能被i整除,则打印n“不是素数”,算法结束;否则执行S5S5:i+1→iS6:如果i≤n-1,返回S3;否则打印n“是素数”;然后算法结束。改进:S6:如果i≤n,返回S3;否则打印n“是素数”;然后算法结束。 谭浩强C语言程序设计2001年5月1日2.3算法的特性ò有穷性:一个算法应包含有限的操作步骤而不能是无限的。ò确定性:算法中每一个步骤应当是确定的,而不能应当是含糊的、模棱两可的。ò有零个或多个输入。ò有一个或多个输出。ò有效性:算法中每一个步骤应当能有效地执行,并得到确定的结果。对于程序设计人员,必须会设计算法,并根据算法写出程序。2.4怎样表示一个算法2.4.1用自然语言表示算法除了很简单的问题,一般不用自然语言表示算法。2.4.2用流程图表示算法流程图表示算法,直观形象,易于理解。【例2.6】将例2.1求5!的算用流程图表示。 谭浩强C语言程序设计2001年5月1日【例2.7】将例2.2的算用流程图表示。 谭浩强C语言程序设计2001年5月1日【例2.8】将例2.3判定闰年的算用流程图表示。 谭浩强C语言程序设计2001年5月1日111111−+−+...+−【例2.9】将例2.4求23499100的算用流程图表示。一个流程图包括:1.表示相应操作的框;2.带箭头的流程线;3.框内外必要的文字说明。 谭浩强C语言程序设计2001年5月1日2.4.3三种基本结构和改进的流程图1.顺序结构:2.选择结构:3.循环结构 谭浩强C语言程序设计2001年5月1日三种基本结构的共同特点:ò只有一个入口;ò只有一个出口;ò结构内的每一部分都有机会被执行到;ò结构内不存在“死循环”。2.4.4用N-S流程图表示算法1973年美国学者提出了一种新型流程图:N-S流程图。顺序结构: 谭浩强C语言程序设计2001年5月1日选择结构:循环结构:2.4.5用伪代码表示算法伪代码使用介于自然语言和计算机语言之间的文字和符号来描述算法。 谭浩强C语言程序设计2001年5月1日2.4.6用计算机语言表示算法ò我们的任务是用计算机解题,就是用计算机实现算法;ò用计算机语言表示算法必须严格遵循所用语言的语法规则。【例2.20】求1×2×3×4×5用C语言表示。main(){inti,t;t=1;i=2;while(i<=5){t=t*i;i=i+1;}printf(“%d”,t);}【例2.21】求级数的值。main(){intsigh=1;floatdeno=2.0,sum=1.0,term;while(deno<=100){sigh=-sigh;term=sigh/deno;sum=sum+term;deno=deno+1;}printf(“%f”,sum);}2.5结构化程序设计方法ò自顶向下;ò逐步细化;ò模块化设计;ò结构化编码。 谭浩强C语言程序设计2001年5月1日3数据类型、运算符与表达式3.1C语言的数据类型在第一章中,我们已经看到程序中使用的各种变量都应预先加以定义,即先定义,后使用。对变量的定义可以包括三个方面:·数据类型·存储类型·作用域在本章中,我们只介绍数据类型的说明。其它说明在以后各章中陆续介绍。所谓数据类型是按被定义变量的性质,表示形式,占据存储空间的多少,构造特点来划分的。在C语言中,数据类型可分为:基本数据类型,构造数据类型,指针类型,空类型四大类。 谭浩强C语言程序设计2001年5月1日数据类型基本类型整型字符型实型(浮点型)单精度型双精度型枚举类型构造类型数组类型结构体类型共用体类型指针类型空类型1.基本数据类型:基本数据类型最主要的特点是,其值不可以再分解为其它类型。也就是说,基本数据类型是自我说明的。2.构造数据类型:构造数据类型是根据已定义的一个或多个数据类型用构造的方法来定义的。也就是说,一个构造类型的值可以分解成若干个“成员”或“元素”。每个“成员”都是一个基本数据类型或又是一个构造类型。在C语言中,构造类型有以下几种:·数组类型·结构体类型·共用体(联合)类型3.指针类型:指针是一种特殊的,同时又是具有重要作用的数据类型。其值用来表示某个变量在内存储器中的地址。虽然指针变量的取值类似于整型量,但这是两个类型完全不 谭浩强C语言程序设计2001年5月1日同的量,因此不能混为一谈。4.空类型:在调用函数值时,通常应向调用者返回一个函数值。这个返回的函数值是具有一定的数据类型的,应在函数定义及函数说明中给以说明,例如在例题中给出的max函数定义中,函数头为:intmax(inta,intb);其中“int”类型说明符即表示该函数的返回值为整型量。又如在例题中,使用了库函数sin,由于系统规定其函数返回值为双精度浮点型,因此在赋值语句s=sin(x);中,s也必须是双精度浮点型,以便与sin函数的返回值一致。所以在说明部分,把s说明为双精度浮点型。但是,也有一类函数,调用后并不需要向调用者返回函数值,这种函数可以定义为“空类型”。其类型说明符为void。在后面函数中还要详细介绍。在本章中,我们先介绍基本数据类型中的整型、浮点型和字符型。其余类型在以后各章中陆续介绍。3.2常量与变量对于基本数据类型量,按其取值是否可改变又分为常量和变量两种。在程序执行过程中,其值不发生改变的量称为常量,其值可变的量称为变量。它们可与数据类型结合起来分类。例如,可分为整型常量、整型变量、浮点常量、浮点变量、字符常量、字符变量、枚举常量、枚举变量。在程序中,常量是可以不经说明而直接引用的,而变量则必须先定义后使用。整型量包括整型常量、整型变量。3.2.1常量和符号常量在程序执行过程中,其值不发生改变的量称为常量。ò直接常量(字面常量):¢整型常量:12、0、-3;¢实型常量:4.6、-1.23;¢字符常量:‘a’、‘b’。ò标识符:用来标识变量名、符号常量名、函数名、数组名、类型名、文件名的有效字符序列。ò符号常量:用标示符代表一个常量。在C语言中,可以用一个标识符来表示一个常量,称之为符号常量。符号常量在使用之前必须先定义,其一般形式为:#define标识符常量其中#define也是一条预处理命令(预处理命令都以"#"开头),称为宏定义命令(在后面预处理程序中将进一步介绍),其功能是把该标识符定义为其后的常量值。一经定义,以后在程序中所有出现该标识符的地方均代之以该常量值。ò习惯上符号常量的标识符用大写字母,变量标识符用小写字母,以示区别。【例3.1】符号常量的使用。#definePRICE30main(){intnum,total;num=10; 谭浩强C语言程序设计2001年5月1日total=num*PRICE;printf(“total=%d”,total);}¢用标识符代表一个常量,称为符号常量。¢符号常量与变量不同,它的值在其作用域内不能改变,也不能再被赋值。¢使用符号常量的好处是:¾含义清楚;¾能做到“一改全改”。3.2.2变量其值可以改变的量称为变量。一个变量应该有一个名字,在内存中占据一定的存储单元。变量定义必须放在变量使用之前。一般放在函数体的开头部分。要区分变量名和变量值是两个不同的概念。a变量名变量值3存储单元3.3整型数据3.3.1整型常量的表示方法整型常量就是整常数。在C语言中,使用的整常数有八进制、十六进制和十进制三种。1)十进制整常数:十进制整常数没有前缀。其数码为0~9。以下各数是合法的十进制整常数:237、-568、65535、1627;以下各数不是合法的十进制整常数:023(不能有前导0)、23D(含有非十进制数码)。在程序中是根据前缀来区分各种进制数的。因此在书写常数时不要把前缀弄错造成结果不正确。2)八进制整常数:八进制整常数必须以0开头,即以0作为八进制数的前缀。数码取值为0~7。八进制数通常是无符号数。以下各数是合法的八进制数:015(十进制为13)、0101(十进制为65)、0177777(十进制为65535);以下各数不是合法的八进制数: 谭浩强C语言程序设计2001年5月1日256(无前缀0)、03A2(包含了非八进制数码)、-0127(出现了负号)。3)十六进制整常数:十六进制整常数的前缀为0X或0x。其数码取值为0~9,A~F或a~f。以下各数是合法的十六进制整常数:0X2A(十进制为42)、0XA0(十进制为160)、0XFFFF(十进制为65535);以下各数不是合法的十六进制整常数:5A(无前缀0X)、0X3H(含有非十六进制数码)。4)整型常数的后缀:在16位字长的机器上,基本整型的长度也为16位,因此表示的数的范围也是有限定的。十进制无符号整常数的范围为0~65535,有符号数为-32768~+32767。八进制无符号数的表示范围为0~0177777。十六进制无符号数的表示范围为0X0~0XFFFF或0x0~0xFFFF。如果使用的数超过了上述范围,就必须用长整型数来表示。长整型数是用后缀“L”或“l”来表示的。例如:十进制长整常数:158L(十进制为158)、358000L(十进制为358000);八进制长整常数:012L(十进制为10)、077L(十进制为63)、0200000L(十进制为65536);十六进制长整常数:0X15L(十进制为21)、0XA5L(十进制为165)、0X10000L(十进制为65536)。长整数158L和基本整常数158在数值上并无区别。但对158L,因为是长整型量,C编译系统将为它分配4个字节存储空间。而对158,因为是基本整型,只分配2个字节的存储空间。因此在运算和输出格式上要予以注意,避免出错。无符号数也可用后缀表示,整型常数的无符号数的后缀为“U”或“u”。例如:358u,0x38Au,235Lu均为无符号数。前缀,后缀可同时使用以表示各种类型的数。如0XA5Lu表示十六进制无符号长整数A5,其十进制为165。3.3.2整型变量1.整型数据在内存中的存放形式如果定义了一个整型变量i:inti;i=10;i100000000000001010数值是以补码表示的:¢正数的补码和原码相同;¢负数的补码:将该数的绝对值的二进制形式按位取反再加1。 谭浩强C语言程序设计2001年5月1日例如:求-10的补码:10的原码:0000000000001010取反:1111111111110101再加1,得-10的补码:1111111111110110由此可知,左面的第一位是表示符号的。2.整型变量的分类1)基本型:类型说明符为int,在内存中占2个字节。2)短整量:类型说明符为shortint或short。所占字节和取值范围均与基本型相同。3)长整型:类型说明符为longint或long,在内存中占4个字节。4)无符号型:类型说明符为unsigned。无符号型又可与上述三种类型匹配而构成:¢无符号基本型:类型说明符为unsignedint或unsigned。¢无符号短整型:类型说明符为unsignedshort。¢无符号长整型:类型说明符为unsignedlong。各种无符号类型量所占的内存空间字节数与相应的有符号类型量相同。但由于省去了符号位,故不能表示负数。有符号整型变量:最大表示327670111111111111111无符号整型变量:最大表示655351111111111111111下表列出了TurboC中各类整型量所分配的内存字节数及数的表示范围。类型说明符数的范围字节数1515int-32768~32767即-2~(2-1)216unsignedint0~65535即0~(2-1)21515shortint-32768~32767即-2~(2-1)216unsignedshortint0~65535即0~(2-1)23131longint-2147483648~2147483647即-2~(2-1)432unsignedlong0~4294967295即0~(2-1)4以13为例:int型:0000000000001101shortint型:0000000000001101longint型:00000000000000000000000000001101unsignedint型:0000000000001101 谭浩强C语言程序设计2001年5月1日unsignedshortint型:0000000000001101unsignedlongint型:000000000000000000000000000011013.整型变量的定义变量定义的一般形式为:类型说明符变量名标识符,变量名标识符,...;例如:inta,b,c;(a,b,c为整型变量)longx,y;(x,y为长整型变量)unsignedp,q;(p,q为无符号整型变量)在书写变量定义时,应注意以下几点:¢允许在一个类型说明符后,定义多个相同类型的变量。各变量名之间用逗号间隔。类型说明符与变量名之间至少用一个空格间隔。¢最后一个变量名之后必须以“;”号结尾。¢变量定义必须放在变量使用之前。一般放在函数体的开头部分。【例3.2】整型变量的定义与使用。main(){inta,b,c,d;unsignedu;a=12;b=-24;u=10;c=a+u;d=b+u;printf(“a+u=%d,b+u=%d ”,c,d);}4.整型数据的溢出【例3.3】整型数据的溢出。main(){inta,b;a=32767;b=a+1;printf("%d,%d ",a,b);}32767:0111111111111111-327681000000000000000【例3.4】 谭浩强C语言程序设计2001年5月1日main(){longx,y;inta,b,c,d;x=5;y=6;a=7;b=8;c=x+a;d=y+b;printf("c=x+a=%d,d=y+b=%d ",c,d);}从程序中可以看到:x,y是长整型变量,a,b是基本整型变量。它们之间允许进行运算,运算结果为长整型。但c,d被定义为基本整型,因此最后结果为基本整型。本例说明,不同类型的量可以参与运算并相互赋值。其中的类型转换是由编译系统自动完成的。有关类型转换的规则将在以后介绍。3.4实型数据3.4.1实型常量的表示方法实型也称为浮点型。实型常量也称为实数或者浮点数。在C语言中,实数只采用十进制。它有二种形式:十进制小数形式,指数形式。1)十进制数形式:由数码0~9和小数点组成。例如:0.0、25.0、5.789、0.13、5.0、300.、-267.8230等均为合法的实数。注意,必须有小数点。2)指数形式:由十进制数,加阶码标志“e”或“E”以及阶码(只能为整数,可以带符号)组成。其一般形式为:aEn(a为十进制数,n为十进制整数)n其值为a*10。如:52.1E5(等于2.1*10)-23.7E-2(等于3.7*10)70.5E7(等于0.5*10)-2-2.8E-2(等于-2.8*10)以下不是合法的实数:345(无小数点)E7(阶码标志E之前无数字)-5(无阶码标志)53.-E3(负号位置不对) 谭浩强C语言程序设计2001年5月1日2.7E(无阶码)标准C允许浮点数使用后缀。后缀为“f”或“F”即表示该数为浮点数。如356f和356.是等价的。【例3.5】说明了这种情况。main(){printf("%f ",356.);printf("%f ",356);printf("%f ",356f);}3.4.2实型变量1.实型数据在内存中的存放形式实型数据一般占4个字节(32位)内存空间。按指数形式存储。实数3.14159在内存中的存放形式如下:+.3141591数符小数部分指数ò小数部分占的位(bit)数愈多,数的有效数字愈多,精度愈高。ò指数部分占的位数愈多,则能表示的数值范围愈大。2.实型变量的分类实型变量分为:单精度(float型)、双精度(double型)和长双精度(longdouble型)三类。在TurboC中单精度型占4个字节(32位)内存空间,其数值范围为3.4E-38~3.4E+38,只能提供七位有效数字。双精度型占8个字节(64位)内存空间,其数值范围为1.7E-308~1.7E+308,可提供16位有效数字。类型说明符比特数(字节数)有效数字数的范围-3738float32(4)6~710~10-307308double64(8)15~1610~10-49314932longdouble128(16)18~1910~10实型变量定义的格式和书写规则与整型相同。例如:floatx,y;(x,y为单精度实型量)doublea,b,c;(a,b,c为双精度实型量)3.实型数据的舍入误差由于实型变量是由有限的存储单元组成的,因此能提供的有效数字总是有限的。如下例。【例3.6】实型数据的舍入误差。main(){floata,b;a=123456.789e5;b=a+20printf("%f ",a); 谭浩强C语言程序设计2001年5月1日printf("%f ",b);}注意:1.0/3*3的结果并不等于1。【例3.7】main(){floata;doubleb;a=33333.33333;b=33333.33333333333333;printf("%f %f ",a,b);}ò从本例可以看出,由于a是单精度浮点型,有效位数只有七位。而整数已占五位,故小数二位后之后均为无效数字。òb是双精度型,有效位为十六位。但TurboC规定小数后最多保留六位,其余部分四舍五入。3.4.3实型常数的类型实型常数不分单、双精度,都按双精度double型处理。3.5字符型数据字符型数据包括字符常量和字符变量。3.5.1字符常量字符常量是用单引号括起来的一个字符。例如:'a'、'b'、'='、'+'、'?'都是合法字符常量。在C语言中,字符常量有以下特点:1)字符常量只能用单引号括起来,不能用双引号或其它括号。2)字符常量只能是单个字符,不能是字符串。3)字符可以是字符集中任意字符。但数字被定义为字符型之后就不能参与数值运算。如'5'和5是不同的。'5'是字符常量,不能参与运算。 谭浩强C语言程序设计2001年5月1日3.5.2转义字符转义字符是一种特殊的字符常量。转义字符以反斜线""开头,后跟一个或几个字符。转义字符具有特定的含义,不同于字符原有的意义,故称“转义”字符。例如,在前面各例题printf函数的格式串中用到的“ ”就是一个转义字符,其意义是“回车换行”。转义字符主要用来表示那些用一般字符不便于表示的控制代码。常用的转义字符及其含义转义字符转义字符的意义ASCII代码 回车换行10t横向跳到下一制表位置9b退格8r回车13f走纸换页12\反斜线符""92'单引号符39”双引号符34a鸣铃7ddd1~3位八进制数所代表的字符xhh1~2位十六进制数所代表的字符广义地讲,C语言字符集中的任何一个字符均可用转义字符来表示。表中的ddd和xhh正是为此而提出的。ddd和hh分别为八进制和十六进制的ASCII代码。如101表示字母"A",102表示字母"B",134表示反斜线,XOA表示换行等。【例3.8】转义字符的使用。main(){inta,b,c;a=5;b=6;c=7;printf(“abctderf ”);printf(“hijktLbM ”);}3.5.3字符变量字符变量用来存储字符常量,即单个字符。字符变量的类型说明符是char。字符变量类型定义的格式和书写规则都与整型变量相同。例如:chara,b; 谭浩强C语言程序设计2001年5月1日3.5.4字符数据在内存中的存储形式及使用方法每个字符变量被分配一个字节的内存空间,因此只能存放一个字符。字符值是以ASCII码的形式存放在变量的内存单元之中的。如x的十进制ASCII码是120,y的十进制ASCII码是121。对字符变量a,b赋予'x'和'y'值:a='x';b='y';实际上是在a,b两个单元内存放120和121的二进制代码:a:01111000b:01111001所以也可以把它们看成是整型量。C语言允许对整型变量赋以字符值,也允许对字符变量赋以整型值。在输出时,允许把字符变量按整型量输出,也允许把整型量按字符量输出。整型量为二字节量,字符量为单字节量,当整型量按字符型量处理时,只有低八位字节参与处理。【例3.9】向字符变量赋以整数。main(){chara,b;a=120;b=121;printf("%c,%c ",a,b);printf("%d,%d ",a,b);}本程序中定义a,b为字符型,但在赋值语句中赋以整型值。从结果看,a,b值的输出形式取决于printf函数格式串中的格式符,当格式符为"c"时,对应输出的变量值为字符,当格式符为"d"时,对应输出的变量值为整数。【例3.10】main(){chara,b;a='a';b='b';a=a-32;b=b-32;printf("%c,%c %d,%d ",a,b,a,b);} 谭浩强C语言程序设计2001年5月1日本例中,a,b被说明为字符变量并赋予字符值,C语言允许字符变量参与数值运算,即用字符的ASCII码参与运算。由于大小写字母的ASCII码相差32,因此运算后把小写字母换成大写字母。然后分别以整型和字符型输出。3.5.5字符串常量字符串常量是由一对双引号括起的字符序列。例如:"CHINA",“Cprogram”,"$12.5"等都是合法的字符串常量。字符串常量和字符常量是不同的量。它们之间主要有以下区别:1)字符常量由单引号括起来,字符串常量由双引号括起来。2)字符常量只能是单个字符,字符串常量则可以含一个或多个字符。3)可以把一个字符常量赋予一个字符变量,但不能把一个字符串常量赋予一个字符变量。在C语言中没有相应的字符串变量。这是与BASIC语言不同的。但是可以用一个字符数组来存放一个字符串常量。在数组一章内予以介绍。4)字符常量占一个字节的内存空间。字符串常量占的内存字节数等于字符串中字节数加1。增加的一个字节中存放字符""(ASCII码为0)。这是字符串结束的标志。例如:字符串"Cprogram"在内存中所占的字节为:Cprogram字符常量'a'和字符串常量"a"虽然都只有一个字符,但在内存中的情况是不同的。'a'在内存中占一个字节,可表示为:a"a"在内存中占二个字节,可表示为:a3.6变量赋初值在程序中常常需要对变量赋初值,以便使用变量。语言程序中可有多种方法为变量提供初值。本小节先介绍在作变量定义的同时给变量赋以初值的方法。这种方法称为初始化。在变量定义中赋初值的一般形式为:类型说明符变量1=值1,变量2=值2,……;例如:inta=3;intb,c=5;floatx=3.2,y=3f,z=0.75;charch1='K',ch2='P';应注意,在定义中不允许连续赋值,如a=b=c=5是不合法的。【例3.11】main(){ 谭浩强C语言程序设计2001年5月1日inta=3,b,c=5;b=a+c;printf("a=%d,b=%d,c=%d ",a,b,c);}3.7各类数值型数据之间的混合运算变量的数据类型是可以转换的。转换的方法有两种,一种是自动转换,一种是强制转换。自动转换发生在不同数据类型的量混合运算时,由编译系统自动完成。自动转换遵循以下规则:1)若参与运算量的类型不同,则先转换成同一类型,然后进行运算。2)转换按数据长度增加的方向进行,以保证精度不降低。如int型和long型运算时,先把int量转成long型后再进行运算。3)所有的浮点运算都是以双精度进行的,即使仅含float单精度量运算的表达式,也要先转换成double型,再作运算。4)char型和short型参与运算时,必须先转换成int型。5)在赋值运算中,赋值号两边量的数据类型不同时,赋值号右边量的类型将转换为左边量的类型。如果右边量的数据类型长度左边长时,将丢失一部分数据,这样会降低精度,丢失的部分按四舍五入向前舍入。下图表示了类型自动转换的规则。doublelongunsignedintchar,short【例3.12】main(){floatPI=3.14159;ints,r=5;s=r*r*PI;printf("s=%d ",s); 谭浩强C语言程序设计2001年5月1日}本例程序中,PI为实型;s,r为整型。在执行s=r*r*PI语句时,r和PI都转换成double型计算,结果也为double型。但由于s为整型,故赋值结果仍为整型,舍去了小数部分。强制类型转换强制类型转换是通过类型转换运算来实现的。其一般形式为:(类型说明符)(表达式)其功能是把表达式的运算结果强制转换成类型说明符所表示的类型。例如:(float)a把a转换为实型(int)(x+y)把x+y的结果转换为整型在使用强制转换时应注意以下问题:1)类型说明符和表达式都必须加括号(单个变量可以不加括号),如把(int)(x+y)写成(int)x+y则成了把x转换成int型之后再与y相加了。2)无论是强制转换或是自动转换,都只是为了本次运算的需要而对变量的数据长度进行的临时性转换,而不改变数据说明时对该变量定义的类型。【例3.13】main(){floatf=5.75;printf("(int)f=%d,f=%f ",(int)f,f);}本例表明,f虽强制转为int型,但只在运算中起作用,是临时的,而f本身的类型并不改变。因此,(int)f的值为5(删去了小数)而f的值仍为5.75。3.8算术运算符和算术表达式C语言中运算符和表达式数量之多,在高级语言中是少见的。正是丰富的运算符和表达式使C语言功能十分完善。这也是C语言的主要特点之一。C语言的运算符不仅具有不同的优先级,而且还有一个特点,就是它的结合性。在表达式中,各运算量参与运算的先后顺序不仅要遵守运算符优先级别的规定,还要受运算符结合性的制约,以便确定是自左向右进行运算还是自右向左进行运算。这种结合性是其它高级语言的运算符所没有的,因此也增加了C语言的复杂性。3.8.1C运算符简介C语言的运算符可分为以下几类:1.算术运算符:用于各类数值运算。包括加(+)、减(-)、乘(*)、除(/)、求余(或称模运算,%)、自增(++)、自减(--)共七种。2.关系运算符:用于比较运算。包括大于(>)、小于(<)、等于(==)、大于等于(>=)、小于等 谭浩强C语言程序设计2001年5月1日于(<=)和不等于(!=)六种。3.逻辑运算符:用于逻辑运算。包括与(&&)、或(||)、非(!)三种。4.位操作运算符:参与运算的量,按二进制位进行运算。包括位与(&)、位或(|)、位非(~)、位异或(^)、左移(<<)、右移(>>)六种。5.赋值运算符:用于赋值运算,分为简单赋值(=)、复合算术赋值(+=,-=,*=,/=,%=)和复合位运算赋值(&=,|=,^=,>>=,<<=)三类共十一种。6.条件运算符:这是一个三目运算符,用于条件求值(?:)。7.逗号运算符:用于把若干表达式组合成一个表达式(,)。8.指针运算符:用于取内容(*)和取地址(&)二种运算。9.求字节数运算符:用于计算数据类型所占的字节数(sizeof)。10.特殊运算符:有括号(),下标[],成员(→,.)等几种。3.8.2算术运算符和算术表达式1.基本的算术运算符ò加法运算符“+”:加法运算符为双目运算符,即应有两个量参与加法运算。如a+b,4+8等。具有右结合性。ò减法运算符“-”:减法运算符为双目运算符。但“-”也可作负值运算符,此时为单目运算,如-x,-5等具有左结合性。ò乘法运算符“*”:双目运算,具有左结合性。ò除法运算符“/”:双目运算具有左结合性。参与运算量均为整型时,结果也为整型,舍去小数。如果运算量中有一个是实型,则结果为双精度实型。〖例3.14〗main(){printf(" %d,%d ",20/7,-20/7);printf("%f,%f ",20.0/7,-20.0/7);}本例中,20/7,-20/7的结果均为整型,小数全部舍去。而20.0/7和-20.0/7由于有实数参与运算,因此结果也为实型。ò求余运算符(模运算符)“%”:双目运算,具有左结合性。要求参与运算的量均为整型。求余运算的结果等于两数相除后的余数。【例3.15】main(){printf("%d ",100%3);}本例输出100除以3所得的余数1。2.算术表达式和运算符的优先级和结合性表达式是由常量、变量、函数和运算符组合起来的式子。一个表达式有一个值及其类型,它们等于计算表达式所得结果的值和类型。表达式求值按运算符的优先级和结合性规定的顺序进行。单个的常量、变量、函数可以看作是表达式的特例。 谭浩强C语言程序设计2001年5月1日算术表达式是由算术运算符和括号连接起来的式子。ò算术表达式:用算术运算符和括号将运算对象(也称操作数)连接起来的、符合C语法规则的式子。以下是算术表达式的例子:a+b(a*2)/c(x+r)*8-(a+b)/7++Isin(x)+sin(y)(++i)-(j++)+(k--)ò运算符的优先级:C语言中,运算符的运算优先级共分为15级。1级最高,15级最低。在表达式中,优先级较高的先于优先级较低的进行运算。而在一个运算量两侧的运算符优先级相同时,则按运算符的结合性所规定的结合方向处理。ò运算符的结合性:C语言中各运算符的结合性分为两种,即左结合性(自左至右)和右结合性(自右至左)。例如算术运算符的结合性是自左至右,即先左后右。如有表达式x-y+z则y应先与“-”号结合,执行x-y运算,然后再执行+z的运算。这种自左至右的结合方向就称为“左结合性”。而自右至左的结合方向称为“右结合性”。最典型的右结合性运算符是赋值运算符。如x=y=z,由于“=”的右结合性,应先执行y=z再执行x=(y=z)运算。C语言运算符中有不少为右结合性,应注意区别,以避免理解错误。3.强制类型转换运算符其一般形式为:(类型说明符)(表达式)其功能是把表达式的运算结果强制转换成类型说明符所表示的类型。例如:(float)a把a转换为实型(int)(x+y)把x+y的结果转换为整型4.自增、自减运算符自增1,自减1运算符:自增1运算符记为“++”,其功能是使变量的值自增1。自减1运算符记为“--”,其功能是使变量值自减1。自增1,自减1运算符均为单目运算,都具有右结合性。可有以下几种形式:++ii自增1后再参与其它运算。--ii自减1后再参与其它运算。i++i参与运算后,i的值再自增1。i--i参与运算后,i的值再自减1。在理解和使用上容易出错的是i++和i--。特别是当它们出在较复杂的表达式或语句中时,常常难于弄清,因此应仔细分析。【例3.16】main(){inti=8;printf("%d ",++i);printf("%d ",--i);printf("%d ",i++);printf("%d ",i--);printf("%d ",-i++); 谭浩强C语言程序设计2001年5月1日printf("%d ",-i--);}i的初值为8,第2行i加1后输出故为9;第3行减1后输出故为8;第4行输出i为8之后再加1(为9);第5行输出i为9之后再减1(为8);第6行输出-8之后再加1(为9),第7行输出-9之后再减1(为8)。【例3.17】main(){inti=5,j=5,p,q;p=(i++)+(i++)+(i++);q=(++j)+(++j)+(++j);printf("%d,%d,%d,%d",p,q,i,j);}这个程序中,对P=(i++)+(i++)+(i++)应理解为三个i相加,故P值为15。然后i再自增1三次相当于加3故i的最后值为8。而对于q的值则不然,q=(++j)+(++j)+(++j)应理解为q先自增1,再参与运算,由于q自增1三次后值为8,三个8相加的和为24,j的最后值仍为8。3.9赋值运算符和赋值表达式1.赋值运算符简单赋值运算符和表达式:简单赋值运算符记为“=”。由“=”连接的式子称为赋值表达式。其一般形式为:变量=表达式例如:x=a+bw=sin(a)+sin(b)y=i+++--j赋值表达式的功能是计算表达式的值再赋予左边的变量。赋值运算符具有右结合性。因此a=b=c=5可理解为a=(b=(c=5))在其它高级语言中,赋值构成了一个语句,称为赋值语句。而在C中,把“=”定义为运算符,从而组成赋值表达式。凡是表达式可以出现的地方均可出现赋值表达式。例如,式子:x=(a=5)+(b=8)是合法的。它的意义是把5赋予a,8赋予b,再把a,b相加,和赋予x,故x应等于13。在C语言中也可以组成赋值语句,按照C语言规定,任何表达式在其未尾加上分号就构成为语句。因此如x=8;a=b=c=5; 谭浩强C语言程序设计2001年5月1日都是赋值语句,在前面各例中我们已大量使用过了。2.类型转换如果赋值运算符两边的数据类型不相同,系统将自动进行类型转换,即把赋值号右边的类型换成左边的类型。具体规定如下:1)实型赋予整型,舍去小数部分。前面的例子已经说明了这种情况。2)整型赋予实型,数值不变,但将以浮点形式存放,即增加小数部分(小数部分的值为0)。3)字符型赋予整型,由于字符型为一个字节,而整型为二个字节,故将字符的ASCII码值放到整型量的低八位中,高八位为0。整型赋予字符型,只把低八位赋予字符量。【例3.18】main(){inta,b=322;floatx,y=8.88;charc1='k',c2;a=y;x=b;a=c1;c2=b;printf("%d,%f,%d,%c",a,x,a,c2);}本例表明了上述赋值运算中类型转换的规则。a为整型,赋予实型量y值8.88后只取整数8。x为实型,赋予整型量b值322,后增加了小数部分。字符型量c1赋予a变为整型,整型量b赋予c2后取其低八位成为字符型(b的低八位为01000010,即十进制66,按ASCII码对应于字符B)。3.复合的赋值运算符在赋值符“=”之前加上其它二目运算符可构成复合赋值符。如+=,-=,*=,/=,%=,<<=,>>=,&=,^=,|=。构成复合赋值表达式的一般形式为:变量双目运算符=表达式它等效于变量=变量运算符表达式例如:a+=5等价于a=a+5x*=y+7等价于x=x*(y+7)r%=p等价于r=r%p复合赋值符这种写法,对初学者可能不习惯,但十分有利于编译处理,能提高编译效率并产生质量较高的目标代码。3.10逗号运算符和逗号表达式在C语言中逗号“,”也是一种运算符,称为逗号运算符。其功能是把两个表达式连接 谭浩强C语言程序设计2001年5月1日起来组成一个表达式,称为逗号表达式。其一般形式为:表达式1,表达式2其求值过程是分别求两个表达式的值,并以表达式2的值作为整个逗号表达式的值。【例3.19】main(){inta=2,b=4,c=6,x,y;y=(x=a+b),(b+c);printf("y=%d,x=%d",y,x);}本例中,y等于整个逗号表达式的值,也就是表达式2的值,x是第一个表达式的值。对于逗号表达式还要说明两点:1)逗号表达式一般形式中的表达式1和表达式2也可以又是逗号表达式。例如:表达式1,(表达式2,表达式3)形成了嵌套情形。因此可以把逗号表达式扩展为以下形式:表达式1,表达式2,…表达式n整个逗号表达式的值等于表达式n的值。2)程序中使用逗号表达式,通常是要分别求逗号表达式内各表达式的值,并不一定要求整个逗号表达式的值。并不是在所有出现逗号的地方都组成逗号表达式,如在变量说明中,函数参数表中逗号只是用作各变量之间的间隔符。3.11小结3.11.1C的数据类型基本类型,构造类型,指针类型,空类型3.11.2基本类型的分类及特点类型说明符字节数值范围字符型char1C字符集基本整型int2-32768~32767短整型shortint2-32768~32767长整型longint4-214783648~214783647无符号型unsigned20~65535无符号长整型unsignedlong40~4294967295单精度实型float43/4E-38~3/4E+38双精度实型double81/7E-308~1/7E+308 谭浩强C语言程序设计2001年5月1日3.11.3常量后缀L或l长整型U或u无符号数F或f浮点数3.11.4常量类型整数,长整数,无符号数,浮点数,字符,字符串,符号常数,转义字符。3.11.5数据类型转换·自动转换:在不同类型数据的混合运算中,由系统自动实现转换,由少字节类型向多字节类型转换。不同类型的量相互赋值时也由系统自动进行转换,把赋值号右边的类型转换为左边的类型。·强制转换:由强制转换运算符完成转换。3.11.6运算符优先级和结合性一般而言,单目运算符优先级较高,赋值运算符优先级低。算术运算符优先级较高,关系和逻辑运算符优先级较低。多数运算符具有左结合性,单目运算符、三目运算符、赋值运算符具有右结合性。3.11.7表达式表达式是由运算符连接常量、变量、函数所组成的式子。每个表达式都有一个值和类型。表达式求值按运算符的优先级和结合性所规定的顺序进行。 谭浩强C语言程序设计2001年5月1日4最简单的C程序设计—顺序程序设计从程序流程的角度来看,程序可以分为三种基本结构,即顺序结构、分支结构、循环结构。这三种基本结构可以组成所有的各种复杂程序。C语言提供了多种语句来实现这些程序结构。本章介绍这些基本语句及其在顺序结构中的应用,使读者对C程序有一个初步的认识,为后面各章的学习打下基础。4.1C语句概述C程序的结构:C程序源程序文件1源程序文件2源程序文件n预处理命令全局变量声明函数1函数n函数首部函数体局部变量声明执行语句C程序的执行部分是由语句组成的。程序的功能也是由执行语句实现的。C语句可分为以下五类:1)表达式语句2)函数调用语句3)控制语句4)复合语句5)空语句1.表达式语句:表达式语句由表达式加上分号“;”组成。其一般形式为:表达式;执行表达式语句就是计算表达式的值。例如: 谭浩强C语言程序设计2001年5月1日x=y+z;赋值语句;y+z;加法运算语句,但计算结果不能保留,无实际意义;i++;自增1语句,i值增1。2.函数调用语句:由函数名、实际参数加上分号“;”组成。其一般形式为:函数名(实际参数表);执行函数语句就是调用函数体并把实际参数赋予函数定义中的形式参数,然后执行被调函数体中的语句,求取函数值(在后面函数中再详细介绍)。例如:printf("CProgram");调用库函数,输出字符串。3.控制语句:控制语句用于控制程序的流程,以实现程序的各种结构方式。它们由特定的语句定义符组成。C语言有九种控制语句。可分成以下三类:1)条件判断语句:if语句、switch语句;2)循环执行语句:dowhile语句、while语句、for语句;3)转向语句:break语句、goto语句、continue语句、return语句。4.复合语句:把多个语句用括号{}括起来组成的一个语句称复合语句。在程序中应把复合语句看成是单条语句,而不是多条语句。例如:{x=y+z;a=b+c;printf(“%d%d”,x,a);}是一条复合语句。复合语句内的各条语句都必须以分号“;”结尾,在括号“}”外不能加分号。5.空语句:只有分号“;”组成的语句称为空语句。空语句是什么也不执行的语句。在程序中空语句可用来作空循环体。例如while(getchar()!=' ');本语句的功能是,只要从键盘输入的字符不是回车则重新输入。这里的循环体为空语句。4.2赋值语句赋值语句是由赋值表达式再加上分号构成的表达式语句。其一般形式为:变量=表达式;赋值语句的功能和特点都与赋值表达式相同。它是程序中使用最多的语句之一。在赋值语句的使用中需要注意以下几点:1.由于在赋值符“=”右边的表达式也可以又是一个赋值表达式,因此,下述形式变量=(变量=表达式);是成立的,从而形成嵌套的情形。其展开之后的一般形式为: 谭浩强C语言程序设计2001年5月1日变量=变量=…=表达式;例如:a=b=c=d=e=5;按照赋值运算符的右接合性,因此实际上等效于:e=5;d=e;c=d;b=c;a=b;2.注意在变量说明中给变量赋初值和赋值语句的区别。给变量赋初值是变量说明的一部分,赋初值后的变量与其后的其它同类变量之间仍必须用逗号间隔,而赋值语句则必须用分号结尾。例如:inta=5,b,c;3.在变量说明中,不允许连续给多个变量赋初值。如下述说明是错误的:inta=b=c=5必须写为inta=5,b=5,c=5;而赋值语句允许连续赋值。4.注意赋值表达式和赋值语句的区别。赋值表达式是一种表达式,它可以出现在任何允许表达式出现的地方,而赋值语句则不能。下述语句是合法的:if((x=y+5)>0)z=x;语句的功能是,若表达式x=y+5大于0则z=x。下述语句是非法的:if((x=y+5;)>0)z=x;因为x=y+5;是语句,不能出现在表达式中。4.3数据输入输出的概念及在C语言中的实现1)所谓输入输出是以计算机为主体而言的。2)本章介绍的是向标准输出设备显示器输出数据的语句。3)在C语言中,所有的数据输入/输出都是由库函数完成的。因此都是函数语句。4)在使用C语言库函数时,要用预编译命令#include将有关“头文件”包括到源文件中。使用标准输入输出库函数时要用到“stdio.h”文件,因此源文件开头应有以下预编译命令:#include或#include”stdio.h”stdio是standardinput&outupt的意思。 谭浩强C语言程序设计2001年5月1日5)考虑到printf和scanf函数使用频繁,系统允许在使用这两个函数时可不加#include或#include”stdio.h”4.4字符数据的输入输出4.4.1putchar函数(字符输出函数)putchar函数是字符输出函数,其功能是在显示器上输出单个字符。其一般形式为:putchar(字符变量)例如:putchar('A');(输出大写字母A)putchar(x);(输出字符变量x的值)putchar(‘101’);(也是输出字符A)putchar(' ');(换行)对控制字符则执行控制功能,不在屏幕上显示。使用本函数前必须要用文件包含命令:#include或#include“stdio.h”【例4.1】输出单个字符。#includemain(){chara='B',b='o',c='k';putchar(a);putchar(b);putchar(b);putchar(c);putchar('t');putchar(a);putchar(b);putchar(' ');putchar(b);putchar(c);}4.4.2getchar函数(键盘输入函数)getchar函数的功能是从键盘上输入一个字符。其一般形式为:getchar();通常把输入的字符赋予一个字符变量,构成赋值语句,如:charc;c=getchar(); 谭浩强C语言程序设计2001年5月1日【例4.2】输入单个字符。#includevoidmain(){charc;printf("inputacharacter ");c=getchar();putchar(c);}使用getchar函数还应注意几个问题:1)getchar函数只能接受单个字符,输入数字也按字符处理。输入多于一个字符时,只接收第一个字符。2)使用本函数前必须包含文件“stdio.h”。3)在TC屏幕下运行含本函数程序时,将退出TC屏幕进入用户屏幕等待用户输入。输入完毕再返回TC屏幕。4)程序最后两行可用下面两行的任意一行代替:putchar(getchar());printf(“%c”,getchar());4.5格式输入与输出4.5.1printf函数(格式输出函数)printf函数称为格式输出函数,其关键字最末一个字母f即为“格式”(format)之意。其功能是按用户指定的格式,把指定的数据显示到显示器屏幕上。在前面的例题中我们已多次使用过这个函数。1.printf函数调用的一般形式printf函数是一个标准库函数,它的函数原型在头文件“stdio.h”中。但作为一个特例,不要求在使用printf函数之前必须包含stdio.h文件。printf函数调用的一般形式为:printf(“格式控制字符串”,输出表列)其中格式控制字符串用于指定输出格式。格式控制串可由格式字符串和非格式字符串两种组成。格式字符串是以%开头的字符串,在%后面跟有各种格式字符,以说明输出数据的类型、形式、长度、小数位数等。如:“%d”表示按十进制整型输出;“%ld”表示按十进制长整型输出;“%c”表示按字符型输出等。非格式字符串在输出时原样照印,在显示中起提示作用。输出表列中给出了各个输出项,要求格式字符串和各输出项在数量和类型上应该一一对应。【例4.3】main() 谭浩强C语言程序设计2001年5月1日{inta=88,b=89;printf("%d%d ",a,b);printf("%d,%d ",a,b);printf("%c,%c ",a,b);printf("a=%d,b=%d",a,b);}本例中四次输出了a,b的值,但由于格式控制串不同,输出的结果也不相同。第四行的输出语句格式控制串中,两格式串%d之间加了一个空格(非格式字符),所以输出的a,b值之间有一个空格。第五行的printf语句格式控制串中加入的是非格式字符逗号,因此输出的a,b值之间加了一个逗号。第六行的格式串要求按字符型输出a,b值。第七行中为了提示输出结果又增加了非格式字符串。2.格式字符串在TurboC中格式字符串的一般形式为:[标志][输出最小宽度][.精度][长度]类型其中方括号[]中的项为可选项。各项的意义介绍如下:1)类型:类型字符用以表示输出数据的类型,其格式符和意义如下表所示:格式字符意义d以十进制形式输出带符号整数(正数不输出符号)o以八进制形式输出无符号整数(不输出前缀0)x,X以十六进制形式输出无符号整数(不输出前缀Ox)u以十进制形式输出无符号整数f以小数形式输出单、双精度实数e,E以指数形式输出单、双精度实数g,G以%f或%e中较短的输出宽度输出单、双精度实数c输出单个字符s输出字符串2)标志:标志字符为-、+、#、空格四种,其意义下表所示:标志意义-结果左对齐,右边填空格+输出符号(正号或负号)空格输出值为正时冠以空格,为负时冠以负号对c,s,d,u类无影响;对o类,在输出时加前缀o;对x类,在输出时#加前缀0x;对e,g,f类当结果有小数时才给出小数点3)输出最小宽度:用十进制整数来表示输出的最少位数。若实际位数多于定义的宽度,则按实际位数输出,若实际位数少于定义的宽度则补以空格或0。4)精度:精度格式符以“.”开头,后跟十进制整数。本项的意义是:如果输出数字,则表示小数的位数;如果输出的是字符,则表示输出字符的个数;若实际位数大于所定义的精度数,则截去超过的部分。5.长度:长度格式符为h,l两种,h表示按短整型量输出,l表示按长整型量输出。【例4.4】 谭浩强C语言程序设计2001年5月1日main(){inta=15;floatb=123.1234567;doublec=12345678.1234567;chard='p';printf("a=%d,%5d,%o,%x ",a,a,a,a);printf("b=%f,%lf,%5.4lf,%e ",b,b,b,b);printf("c=%lf,%f,%8.4lf ",c,c,c);printf("d=%c,%8c ",d,d);}本例第七行中以四种格式输出整型变量a的值,其中“%5d”要求输出宽度为5,而a值为15只有两位故补三个空格。第八行中以四种格式输出实型量b的值。其中“%f”和“%lf”格式的输出相同,说明“l”符对“f”类型无影响。“%5.4lf”指定输出宽度为5,精度为4,由于实际长度超过5故应该按实际位数输出,小数位数超过4位部分被截去。第九行输出双精度实数,“%8.4lf”由于指定精度为4位故截去了超过4位的部分。第十行输出字符量d,其中“%8c”指定输出宽度为8故在输出字符p之前补加7个空格。使用printf函数时还要注意一个问题,那就是输出表列中的求值顺序。不同的编译系统不一定相同,可以从左到右,也可从右到左。TurboC是按从右到左进行的。请看下面两个例子:【例4.5】main(){inti=8;printf("%d %d %d %d %d %d ",++i,--i,i++,i--,-i++,-i--);}【例4.6】main(){inti=8;printf("%d ",++i);printf("%d ",--i);printf("%d ",i++);printf("%d ",i--);printf("%d ",-i++);printf("%d ",-i--);}这两个程序的区别是用一个printf语句和多个printf语句输出。但从结果可以看出是不同的。为什么结果会不同呢?就是因为printf函数对输出表中各量求值的顺序是自右至左进行的。在第一例中,先对最后一项“-i--”求值,结果为-8,然后i自减1后为7。再 谭浩强C语言程序设计2001年5月1日对“-i++”项求值得-7,然后i自增1后为8。再对“i--”项求值得8,然后i再自减1后为7。再求“i++”项得7,然后i再自增1后为8。再求“--i”项,i先自减1后输出,输出值为7。最后才求输出表列中的第一项“++i”,此时i自增1后输出8。但是必须注意,求值顺序虽是自右至左,但是输出顺序还是从左至右,因此得到的结果是上述输出结果。4.5.2scanf函数(格式输入函数)scanf函数称为格式输入函数,即按用户指定的格式从键盘上把数据输入到指定的变量之中。1.scanf函数的一般形式scanf函数是一个标准库函数,它的函数原型在头文件“stdio.h”中,与printf函数相同,C语言也允许在使用scanf函数之前不必包含stdio.h文件。scanf函数的一般形式为:scanf(“格式控制字符串”,地址表列);其中,格式控制字符串的作用与printf函数相同,但不能显示非格式字符串,也就是不能显示提示字符串。地址表列中给出各变量的地址。地址是由地址运算符“&”后跟变量名组成的。例如:&a,&b分别表示变量a和变量b的地址。这个地址就是编译系统在内存中给a,b变量分配的地址。在C语言中,使用了地址这个概念,这是与其它语言不同的。应该把变量的值和变量的地址这两个不同的概念区别开来。变量的地址是C编译系统分配的,用户不必关心具体的地址是多少。变量的地址和变量值的关系如下:在赋值表达式中给变量赋值,如:a=567则,a为变量名,567是变量的值,&a是变量a的地址。但在赋值号左边是变量名,不能写地址,而scanf函数在本质上也是给变量赋值,但要求写变量的地址,如&a。这两者在形式上是不同的。&是一个取地址运算符,&a是一个表达式,其功能是求变量的地址。【例4.7】main(){inta,b,c;printf("inputa,b,c ");scanf("%d%d%d",&a,&b,&c);printf("a=%d,b=%d,c=%d",a,b,c);}在本例中,由于scanf函数本身不能显示提示串,故先用printf语句在屏幕上输出提示,请用户输入a、b、c的值。执行scanf语句,则退出TC屏幕进入用户屏幕等待用户输入。用户输入789后按下回车键,此时,系统又将返回TC屏幕。在scanf语句的格式串中由于没有非格式字符在“%d%d%d”之间作输入时的间隔,因此在输入时要用一个以上的 谭浩强C语言程序设计2001年5月1日空格或回车键作为每两个输入数之间的间隔。如:789或7892.格式字符串格式字符串的一般形式为:%[*][输入数据宽度][长度]类型其中有方括号[]的项为任选项。各项的意义如下:1)类型:表示输入数据的类型,其格式符和意义如下表所示。格式字符意义d输入十进制整数o输入八进制整数x输入十六进制整数u输入无符号十进制整数f或e输入实型数(用小数形式或指数形式)c输入单个字符s输入字符串2)“*”符:用以表示该输入项,读入后不赋予相应的变量,即跳过该输入值。如:scanf("%d%*d%d",&a,&b);当输入为:123时,把1赋予a,2被跳过,3赋予b。3)宽度:用十进制整数指定输入的宽度(即字符数)。例如:scanf("%5d",&a);输入:12345678只把12345赋予变量a,其余部分被截去。又如:scanf("%4d%4d",&a,&b);输入:12345678将把1234赋予a,而把5678赋予b。4)长度:长度格式符为l和h,l表示输入长整型数据(如%ld)和双精度浮点数(如%lf)。h表示输入短整型数据。使用scanf函数还必须注意以下几点:1)scanf函数中没有精度控制,如:scanf("%5.2f",&a);是非法的。不能企图用此语句输入小数为2位的实数。2)scanf中要求给出变量地址,如给出变量名则会出错。如scanf("%d",a);是非法的,应改为scnaf("%d",&a);才是合法的。3)在输入多个数值数据时,若格式控制串中没有非格式字符作输入数据之间的间隔则可用空格,TAB或回车作间隔。C编译在碰到空格,TAB,回车或非法数据(如对“%d”输入“12A”时,A即为非法数据)时即认为该数据结束。4)在输入字符数据时,若格式控制串中无非格式字符,则认为所有输入的字符均为有效字符。 谭浩强C语言程序设计2001年5月1日例如:scanf("%c%c%c",&a,&b,&c);输入为:def则把'd'赋予a,''赋予b,'e'赋予c。只有当输入为:def时,才能把'd'赋于a,'e'赋予b,'f'赋予c。如果在格式控制中加入空格作为间隔,如:scanf("%c%c%c",&a,&b,&c);则输入时各数据之间可加空格。【例4.8】main(){chara,b;printf("inputcharactera,b ");scanf("%c%c",&a,&b);printf("%c%c ",a,b);}由于scanf函数"%c%c"中没有空格,输入MN,结果输出只有M。而输入改为MN时则可输出MN两字符。【例4.9】main(){chara,b;printf("inputcharactera,b ");scanf("%c%c",&a,&b);printf(" %c%c ",a,b);}本例表示scanf格式控制串"%c%c"之间有空格时,输入的数据之间可以有空格间隔。5)如果格式控制串中有非格式字符则输入时也要输入该非格式字符。例如:scanf("%d,%d,%d",&a,&b,&c);其中用非格式符“,”作间隔符,故输入时应为:5,6,7又如:scanf("a=%d,b=%d,c=%d",&a,&b,&c);则输入应为:a=5,b=6,c=76)如输入的数据与输出的类型不一致时,虽然编译能够通过,但结果将不正确。【例4.10】 谭浩强C语言程序设计2001年5月1日main(){inta;printf("inputanumber ");scanf("%d",&a);printf("%ld",a);}由于输入数据类型为整型,而输出语句的格式串中说明为长整型,因此输出结果和输入数据不符。如改动程序如下:【例4.11】main(){longa;printf("inputalonginteger ");scanf("%ld",&a);printf("%ld",a);}运行结果为:inputalonginteger12345678901234567890当输入数据改为长整型后,输入输出数据相等。【例4.12】main(){chara,b,c;printf("inputcharactera,b,c ");scanf("%c%c%c",&a,&b,&c);printf("%d,%d,%d %c,%c,%c ",a,b,c,a-32,b-32,c-32);}输入三个小写字母,输出其ASCII码和对应的大写字母。【例4.13】main(){inta;longb;floatf;doubled;charc;printf(" int:%d long:%d float:%d double:%d char:%d ",sizeof(a),sizeof(b),sizeof(f),sizeof(d),sizeof(c));} 谭浩强C语言程序设计2001年5月1日输出各种数据类型的字节长度。4.6顺序结构程序设计举例【例4.14】输入三角形的三边长,求三角形面积。已知三角形的三边长a,b,c,则该三角形的面积公式为:area=s(s−a)(s−b)(s−c),其中s=(a+b+c)/2源程序如下:#includemain(){floata,b,c,s,area;scanf(“%f,%f,%f”,&a,&b,&c);s=1.0/2*(a+b+c);area=sqrt(s*(s-a)*(s-b)*(s-c));printf(“a=%7.2f,b=%7.2f,c=%7.2f,s=%7.2f ”,a,b,c,s);printf(“area=%7.2f ”,area);}22【例4.15】求ax+bx+c=0方程的根,a,b,c由键盘输入,设b-4ac>0。求根公式为:−b+b2−4ac−b2x=2p=+−bb−4a1+−bb−4ax=x=12a2a12a2a2b2−4acb−4acq=q=令2a,2a则x1=p+qx2=p-q源程序如下:#includemain(){floata,b,c,disc,x1,x2,p,q;scanf(“a=%f,b=%f,c=%f”,&a,&b,&c);disc=b*b-4*a*c;p=-b/(2*a);q=sqrt(disc)/(2*a);x1=p+q;x2=p-q; 谭浩强C语言程序设计2001年5月1日printf(“ x1=%5.2f x2=%5.2f ”,x1,x2);} 谭浩强C语言程序设计2001年5月1日5分支结构程序5.1关系运算符和表达式在程序中经常需要比较两个量的大小关系,以决定程序下一步的工作。比较两个量的运算符称为关系运算符。5.1.1关系运算符及其优先次序在C语言中有以下关系运算符:1)<小于2)<=小于或等于3)>大于4)>=大于或等于5)==等于6)!=不等于关系运算符都是双目运算符,其结合性均为左结合。关系运算符的优先级低于算术运算符,高于赋值运算符。在六个关系运算符中,<,<=,>,>=的优先级相同,高于==和!=,==和!=的优先级相同。5.1.2关系表达式关系表达式的一般形式为:表达式关系运算符表达式例如:a+b>c-dx>3/2‘a’+1(b>c)a!=(c==d)等。关系表达式的值是真”和“假”,用“1”和“0”表示。如:5>0的值为“真”,即为1。(a=3)>(b=5)由于3>5不成立,故其值为假,即为0。【例5.1】main(){ 谭浩强C语言程序设计2001年5月1日charc='k';inti=1,j=2,k=3;floatx=3e+5,y=0.85;printf("%d,%d ",’a’+5=k+1);printf("%d,%d ",1b&&c>d等价于(a>b)&&(c>d)!b==c||dc&&x+yc)&&((x+y)0&&4>2由于5>0为真,4>2也为真,相与的结果也为真。2.或运算||:参与运算的两个量只要有一个为真,结果就为真。两个量都为假时,结果为假。例如:5>0||5>8由于5>0为真,相或的结果也就为真。3.非运算!:参与运算量为真时,结果为假;参与运算量为假时,结果为真。例如:!(5>0)的结果为假。虽然C编译在给出逻辑运算值时,以“1”代表“真”,“0”代表“假”。但反过来在判断一个量是为“真”还是为“假”时,以“0”代表“假”,以非“0”的数值作为“真”。例如:由于5和3均为非“0”因此5&&3的值为“真”,即为1。又如:5||0的值为“真”,即为1。5.2.3逻辑表达式逻辑表达式的一般形式为:表达式逻辑运算符表达式其中的表达式可以又是逻辑表达式,从而组成了嵌套的情形。例如:(a&&b)&&c根据逻辑运算符的左结合性,上式也可写为:a&&b&&c逻辑表达式的值是式中各种逻辑运算的最后值,以“1”和“0”分别代表“真”和“假”。【例5.2】main(){charc='k';inti=1,j=2,k=3;floatx=3e+5,y=0.85;printf("%d,%d ",!x*!y,!!!x);printf("%d,%d ",x||i&&j-3,ib)printf("max=%d ",a);elseprintf("max=%d ",b);}输入两个整数,输出其中的大数。改用if-else语句判别a,b的大小,若a大,则输出a,否则输出b。3.第三种形式为if-else-if形式前二种形式的if语句一般都用于两个分支的情况。当有多个分支选择时,可采用if-else-if语句,其一般形式为:if(表达式1)语句1;elseif(表达式2)语句2;elseif(表达式3)语句3;…elseif(表达式m)语句m;else语句n;其语义是:依次判断表达式的值,当出现某个值为真时,则执行其对应的语句。然后跳到整个if语句之外继续执行程序。如果所有的表达式均为假,则执行语句n。然后继续执行后续程序。if-else-if语句的执行过程如图3—3所示。 谭浩强C语言程序设计2001年5月1日【例5.5】#include"stdio.h"main(){charc;printf("inputacharacter:");c=getchar();if(c<32)printf("Thisisacontrolcharacter ");elseif(c>='0'&&c<='9')printf("Thisisadigit ");elseif(c>='A'&&c<='Z')printf("Thisisacapitalletter ");elseif(c>='a'&&c<='z')printf("Thisisasmallletter ");elseprintf("Thisisanothercharacter ");}本例要求判别键盘输入字符的类别。可以根据输入字符的ASCII码来判别类型。由ASCII码表可知ASCII值小于32的为控制字符。在“0”和“9”之间的为数字,在“A”和“Z”之间为大写字母,在“a”和“z”之间为小写字母,其余则为其它字符。这是一个多分支选择的问题,用if-else-if语句编程,判断输入字符ASCII码所在的范围,分别给出不同的输出。例如输入为“g”,输出显示它为小写字符。4.在使用if语句中还应注意以下问题: 谭浩强C语言程序设计2001年5月1日1)在三种形式的if语句中,在if关键字之后均为表达式。该表达式通常是逻辑表达式或关系表达式,但也可以是其它表达式,如赋值表达式等,甚至也可以是一个变量。例如:if(a=5)语句;if(b)语句;都是允许的。只要表达式的值为非0,即为“真”。如在:if(a=5)…;中表达式的值永远为非0,所以其后的语句总是要执行的,当然这种情况在程序中不一定会出现,但在语法上是合法的。又如,有程序段:if(a=b)printf("%d",a);elseprintf("a=0");本语句的语义是,把b值赋予a,如为非0则输出该值,否则输出“a=0”字符串。这种用法在程序中是经常出现的。2)在if语句中,条件判断表达式必须用括号括起来,在语句之后必须加分号。3)在if语句的三种形式中,所有的语句应为单个语句,如果要想在满足条件时执行一组(多个)语句,则必须把这一组语句用{}括起来组成一个复合语句。但要注意的是在}之后不能再加分号。例如:if(a>b){a++;b++;}else{a=0;b=10;}5.3.2if语句的嵌套当if语句中的执行语句又是if语句时,则构成了if语句嵌套的情形。其一般形式可表示如下:if(表达式)if语句;或者为if(表达式)if语句;elseif语句;在嵌套内的if语句可能又是if-else型的,这将会出现多个if和多个else重叠的情况,这时要特别注意if和else的配对问题。例如:if(表达式1) 谭浩强C语言程序设计2001年5月1日if(表达式2)语句1;else语句2;其中的else究竟是与哪一个if配对呢?应该理解为:if(表达式1)if(表达式2)语句1;else语句2;还是应理解为:if(表达式1)if(表达式2)语句1;else语句2;为了避免这种二义性,C语言规定,else总是与它前面最近的if配对,因此对上述例子应按前一种情况理解。【例5.6】main(){inta,b;printf("pleaseinputA,B:");scanf("%d%d",&a,&b);if(a!=b)if(a>b)printf("A>B ");elseprintf("AB、Ab)printf("A>B ");elseprintf("Ab)max=a;elsemax=b;可用条件表达式写为max=(a>b)?a:b;执行该语句的语义是:如a>b为真,则把a赋予max,否则把b赋予max。使用条件表达式时,还应注意以下几点:1)条件运算符的运算优先级低于关系运算符和算术运算符,但高于赋值符。因此max=(a>b)?a:b可以去掉括号而写为max=a>b?a:b2)条件运算符?和:是一对运算符,不能分开单独使用。3)条件运算符的结合方向是自右至左。例如:a>b?a:c>d?c:d应理解为a>b?a:(c>d?c:d)这也就是条件表达式嵌套的情形,即其中的表达式3又是一个条件表达式。【例5.8】main(){inta,b,max;printf(" inputtwonumbers:");scanf("%d%d",&a,&b);printf("max=%d",a>b?a:b);}用条件表达式对上例重新编程,输出两个数中的大数。 谭浩强C语言程序设计2001年5月1日5.4switch语句C语言还提供了另一种用于多分支选择的switch语句,其一般形式为:switch(表达式){case常量表达式1:语句1;case常量表达式2:语句2;…case常量表达式n:语句n;default:语句n+1;}其语义是:计算表达式的值。并逐个与其后的常量表达式值相比较,当表达式的值与某个常量表达式的值相等时,即执行其后的语句,然后不再进行判断,继续执行后面所有case后的语句。如表达式的值与所有case后的常量表达式均不相同时,则执行default后的语句。【例4.9】main(){inta;printf("inputintegernumber:");scanf("%d",&a);switch(a){case1:printf("Monday ");case2:printf("Tuesday ");case3:printf("Wednesday ");case4:printf("Thursday ");case5:printf("Friday ");case6:printf("Saturday ");case7:printf("Sunday ");default:printf("error ");}}本程序是要求输入一个数字,输出一个英文单词。但是当输入3之后,却执行了case3以及以后的所有语句,输出了Wednesday及以后的所有单词。这当然是不希望的。为什么会出现这种情况呢?这恰恰反应了switch语句的一个特点。在switch语句中,“case常量表达式”只相当于一个语句标号,表达式的值和某标号相等则转向该标号执行,但不能在执行完该标号的语句后自动跳出整个switch语句,所以出现了继续执行所有后面case语句的情况。这是与前面介绍的if语句完全不同的,应特别注意。为了避免上述情况,C语言还提供了一种break语句,专用于跳出switch语句,break语句只有关键字break,没有参数。在后面还将详细介绍。修改例题的程序,在每一case语句之后增加break语句,使每一次执行之后均可跳出switch语句,从而避免输出不应有的结果。【例4.10】main(){ 谭浩强C语言程序设计2001年5月1日inta;printf("inputintegernumber:");scanf("%d",&a);switch(a){case1:printf("Monday ");break;case2:printf("Tuesday ");break;case3:printf("Wednesday ");break;case4:printf("Thursday ");break;case5:printf("Friday ");break;case6:printf("Saturday ");break;case7:printf("Sunday ");break;default:printf("error ");}}在使用switch语句时还应注意以下几点:1)在case后的各常量表达式的值不能相同,否则会出现错误。2)在case后,允许有多个语句,可以不用{}括起来。3)各case和default子句的先后顺序可以变动,而不会影响程序执行结果。4)default子句可以省略不用。5.5程序举例【例4.11】输入三个整数,输出最大数和最小数。main(){inta,b,c,max,min;printf("inputthreenumbers:");scanf("%d%d%d",&a,&b,&c);if(a>b){max=a;min=b;}else{max=b;min=a;}if(maxc)min=c;printf("max=%d min=%d",max,min);}本程序中,首先比较输入的a,b的大小,并把大数装入max,小数装入min中,然后再与c比较,若max小于c,则把c赋予max;如果c小于min,则把c赋予min。因此max 谭浩强C语言程序设计2001年5月1日内总是最大数,而min内总是最小数。最后输出max和min的值即可。【例4.12】计算器程序。用户输入运算数和四则运算符,输出计算结果。main(){floata,b;charc;printf("inputexpression:a+(-,*,/)b ");scanf("%f%c%f",&a,&c,&b);switch(c){case'+':printf("%f ",a+b);break;case'-':printf("%f ",a-b);break;case'*':printf("%f ",a*b);break;case'/':printf("%f ",a/b);break;default:printf("inputerror ");}}本例可用于四则运算求值。switch语句用于判断运算符,然后输出运算值。当输入运算符不是+,-,*,/时给出错误提示。 谭浩强C语言程序设计2001年5月1日6循环控制6.1概述循环结构是程序中一种很重要的结构。其特点是,在给定条件成立时,反复执行某程序段,直到条件不成立为止。给定的条件称为循环条件,反复执行的程序段称为循环体。C语言提供了多种循环语句,可以组成各种不同形式的循环结构。1)用goto语句和if语句构成循环;2)用while语句;3)用do-while语句;4)用for语句;6.2goto语句以及用goto语句构成循环goto语句是一种无条件转移语句,与BASIC中的goto语句相似。goto语句的使用格式为:goto语句标号;其中标号是一个有效的标识符,这个标识符加上一个“:”一起出现在函数内某处,执行goto语句后,程序将跳转到该标号处并执行其后的语句。另外标号必须与goto语句同处于一个函数中,但可以不在一个循环层中。通常goto语句与if条件语句连用,当满足某一条件时,程序跳到标号处运行。goto语句通常不用,主要因为它将使程序层次不清,且不易读,但在多层嵌套退出时,用goto语句则比较合理。100∑n【例6.1】用goto语句和if语句构成循环,n=1。main(){inti,sum=0;i=1;loop:if(i<=100){sum=sum+i;i++;gotoloop;}printf("%d ",sum);} 谭浩强C语言程序设计2001年5月1日6.3while语句while语句的一般形式为:while(表达式)语句其中表达式是循环条件,语句为循环体。while语句的语义是:计算表达式的值,当值为真(非0)时,执行循环体语句。其执行过程可用下图表示。100∑n【例6.2】用while语句求n=1。用传统流程图和N-S结构流程图表示算法,见图: 谭浩强C语言程序设计2001年5月1日main(){inti,sum=0;i=1;while(i<=100){sum=sum+i;i++;}printf("%d ",sum);}【例6.3】统计从键盘输入一行字符的个数。#includemain(){intn=0;printf("inputastring: ");while(getchar()!=' ')n++;printf("%d",n);}本例程序中的循环条件为getchar()!=' ',其意义是,只要从键盘输入的字符不是回车就继续循环。循环体n++完成对输入字符个数计数。从而程序实现了对输入一行字符的字符个数计数。使用while语句应注意以下几点:1)while语句中的表达式一般是关系表达或逻辑表达式,只要表达式的值为真(非0)即可继续循环。【例6.4】main(){inta=0,n;printf(" inputn:");scanf("%d",&n);while(n--)printf("%d",a++*2); 谭浩强C语言程序设计2001年5月1日}本例程序将执行n次循环,每执行一次,n值减1。循环体输出表达式a++*2的值。该表达式等效于(a*2;a++)。2)循环体如包括有一个以上的语句,则必须用{}括起来,组成复合语句。6.4do-while语句do-while语句的一般形式为:do语句while(表达式);这个循环与while循环的不同在于:它先执行循环中的语句,然后再判断表达式是否为真,如果为真则继续循环;如果为假,则终止循环。因此,do-while循环至少要执行一次循环语句。其执行过程可用下图表示。100∑n【例6.5】用do-while语句求n=1。用传统流程图和N-S结构流程图表示算法,见图: 谭浩强C语言程序设计2001年5月1日main(){inti,sum=0;i=1;do{sum=sum+i;i++;}while(i<=100)printf("%d ",sum);}同样当有许多语句参加循环时,要用"{"和"}"把它们括起来。【例6.6】while和do-while循环比较。(1)main(){intsum=0,i;scanf(“%d”,&i);while(i<=10){sum=sum+i;i++;} 谭浩强C语言程序设计2001年5月1日printf(“sum=%d”,sum);}(2)main(){intsum=0,i;scanf(“%d”,&i);do{sum=sum+i;i++;}while(i<=10);printf(“sum=%d”,sum);}6.5for语句在C语言中,for语句使用最为灵活,它完全可以取代while语句。它的一般形式为:for(表达式1;表达式2;表达式3)语句它的执行过程如下:1)先求解表达式1。2)求解表达式2,若其值为真(非0),则执行for语句中指定的内嵌语句,然后执行下面第3)步;若其值为假(0),则结束循环,转到第5)步。3)求解表达式3。4)转回上面第2)步继续执行。5)循环结束,执行for语句下面的一个语句。其执行过程可用下图表示。 谭浩强C语言程序设计2001年5月1日for语句最简单的应用形式也是最容易理解的形式如下:for(循环变量赋初值;循环条件;循环变量增量)语句循环变量赋初值总是一个赋值语句,它用来给循环控制变量赋初值;循环条件是一个关系表达式,它决定什么时候退出循环;循环变量增量,定义循环控制变量每循环一次后按什么方式变化。这三个部分之间用“;”分开。例如:for(i=1;i<=100;i++)sum=sum+i;先给i赋初值1,判断i是否小于等于100,若是则执行语句,之后值增加1。再重新判断,直到条件为假,即i>100时,结束循环。相当于:i=1;while(i<=100){sum=sum+i;i++;}对于for循环中语句的一般形式,就是如下的while循环形式:表达式1;while(表达式2){语句表达式3;}注意:1)for循环中的“表达式1(循环变量赋初值)”、“表达式2(循环条件)”和“表达式3(循 谭浩强C语言程序设计2001年5月1日环变量增量)”都是选择项,即可以缺省,但“;”不能缺省。2)省略了“表达式1(循环变量赋初值)”,表示不对循环控制变量赋初值。3)省略了“表达式2(循环条件)”,则不做其它处理时便成为死循环。例如:for(i=1;;i++)sum=sum+i;相当于:i=1;while(1){sum=sum+i;i++;}4)省略了“表达式3(循环变量增量)”,则不对循环控制变量进行操作,这时可在语句体中加入修改循环控制变量的语句。例如:for(i=1;i<=100;){sum=sum+i;i++;}5)省略了“表达式1(循环变量赋初值)”和“表达式3(循环变量增量)”。例如:for(;i<=100;){sum=sum+i;i++;}相当于:while(i<=100){sum=sum+i;i++;}6)3个表达式都可以省略。例如:for(;;)语句相当于:while(1)语句7)表达式1可以是设置循环变量的初值的赋值表达式,也可以是其他表达式。例如:for(sum=0;i<=100;i++)sum=sum+i;8)表达式1和表达式3可以是一个简单表达式也可以是逗号表达式。for(sum=0,i=1;i<=100;i++)sum=sum+i;或:for(i=0,j=100;i<=100;i++,j--)k=i+j;9)表达式2一般是关系表达式或逻辑表达式,但也可是数值表达式或字符表达式,只要其值非零,就执行循环体。例如:for(i=0;(c=getchar())!=’ ’;i+=c);又如:for(;(c=getchar())!=’ ’;)printf(“%c”,c); 谭浩强C语言程序设计2001年5月1日6.6循环的嵌套【例6.7】main(){inti,j,k;printf("ijk ");for(i=0;i<2;i++)for(j=0;j<2;j++)for(k=0;k<2;k++)printf(“%d%d%d ",i,j,k);}6.7几种循环的比较1)四种循环都可以用来处理同一个问题,一般可以互相代替。但一般不提倡用goto型循环。2)while和do-while循环,循环体中应包括使循环趋于结束的语句。for语句功能最强。3)用while和do-while循环时,循环变量初始化的操作应在while和do-while语句之前完成,而for语句可以在表达式1中实现循环变量的初始化。6.8break和continue语句6.8.1break语句break语句通常用在循环语句和开关语句中。当break用于开关语句switch中时,可使程序跳出switch而执行switch以后的语句;如果没有break语句,则将成为一个死循环而无法退出。break在switch中的用法已在前面介绍开关语句时的例子中碰到,这里不再举例。当break语句用于do-while、for、while循环语句中时,可使程序终止循环而执行循环后面的语句,通常break语句总是与if语句联在一起。即满足条件时便跳出循环。【例6.8】main(){inti=0;charc;while(1)/*设置循环*/{c='';/*变量赋初值*/ 谭浩强C语言程序设计2001年5月1日while(c!=13&&c!=27)/*键盘接收字符直到按回车或Esc键*/{c=getch();printf("%c ",c);}if(c==27)break;/*判断若按Esc键则退出循环*/i++;printf("TheNo.is%d ",i);}printf("Theend");}注意:1)break语句对if-else的条件语句不起作用。2)在多层循环中,一个break语句只向外跳一层。6.8.2continue语句continue语句的作用是跳过循环本中剩余的语句而强行执行下一次循环。continue语句只用在for、while、do-while等循环体中,常与if条件语句一起使用,用来加速循环。其执行过程可用下图表示。1)while(表达式1){……if(表达式2)break;……}2)while(表达式1){……if(表达式2)continue;……} 谭浩强C语言程序设计2001年5月1日【例6.9】main(){charc;while(c!=13)/*不是回车符则循环*/{c=getch();if(c==0X1B)continue;/*若按Esc键不输出便进行下次循环*/printf("%c ",c);}}6.9程序举例π111=1−+−+...【例6.10】用4357公式求π。 谭浩强C语言程序设计2001年5月1日N-S流程图:#includemain(){ints;floatn,t,pi;t=1,pi=0;n=1.0;s=1;while(fabs(t)>1e-6){pi=pi+t;n=n+2;s=-s;t=s/n;}pi=pi*4;printf("pi=%10.6f ",pi);}【例6.11】判断m是否素数。N-S流程图:#includemain(){intm,i,k;scanf(“%d”,&m);k=sqrt(m);for(i=2;i<=k;i++)if(m%i==0)break;if(i>=k+1)printf(“%disaprimenumber ”,m);elseprintf(“%disnotaprimenumber ”,m);}【例6.12】求100至200间的全部素数。#includemain(){intm,i,k,n=0;for(m=101;m<=200;m=m+2){k=sqrt(m);for(i=2;i<=k;i++) 谭浩强C语言程序设计2001年5月1日if(m%i==0)break;if(i>=k+1){printf(“%d”,m);n=n+1;}if(n%n==0)printf(“ ”);}printf(“ ”);} 谭浩强C语言程序设计2001年5月1日7数组在程序设计中,为了处理方便,把具有相同类型的若干变量按有序的形式组织起来。这些按序排列的同类数据元素的集合称为数组。在C语言中,数组属于构造数据类型。一个数组可以分解为多个数组元素,这些数组元素可以是基本数据类型或是构造类型。因此按数组元素的类型不同,数组又可分为数值数组、字符数组、指针数组、结构数组等各种类别。本章介绍数值数组和字符数组,其余的在以后各章陆续介绍。7.1一维数组的定义和引用7.1.1一维数组的定义方式在C语言中使用数组必须先进行定义。一维数组的定义方式为:类型说明符数组名[常量表达式];其中:类型说明符是任一种基本数据类型或构造数据类型。数组名是用户定义的数组标识符。方括号中的常量表达式表示数据元素的个数,也称为数组的长度。例如:inta[10];说明整型数组a,有10个元素。floatb[10],c[20];说明实型数组b,有10个元素,实型数组c,有20个元素。charch[20];说明字符数组ch,有20个元素。对于数组类型说明应注意以下几点:1)数组的类型实际上是指数组元素的取值类型。对于同一个数组,其所有元素的数据类型都是相同的。2)数组名的书写规则应符合标识符的书写规定。3)数组名不能与其它变量名相同。例如:main(){inta;floata[10];……}是错误的。4)方括号中常量表达式表示数组元素的个数,如a[5]表示数组a有5个元素。但是其下标从0开始计算。因此5个元素分别为a[0],a[1],a[2],a[3],a[4]。5)不能在方括号中用变量来表示元素的个数,但是可以是符号常数或常量表达式。 谭浩强C语言程序设计2001年5月1日例如:#defineFD5main(){inta[3+2],b[7+FD];……}是合法的。但是下述说明方式是错误的。main(){intn=5;inta[n];……}6)允许在同一个类型说明中,说明多个数组和多个变量。例如:inta,b,c,d,k1[10],k2[20];7.1.2一维数组元素的引用数组元素是组成数组的基本单元。数组元素也是一种变量,其标识方法为数组名后跟一个下标。下标表示了元素在数组中的顺序号。数组元素的一般形式为:数组名[下标]其中下标只能为整型常量或整型表达式。如为小数时,C编译将自动取整。例如:a[5]a[i+j]a[i++]都是合法的数组元素。数组元素通常也称为下标变量。必须先定义数组,才能使用下标变量。在C语言中只能逐个地使用下标变量,而不能一次引用整个数组。例如,输出有10个元素的数组必须使用循环语句逐个输出各下标变量:for(i=0;i<10;i++)printf("%d",a[i]);而不能用一个语句输出整个数组。下面的写法是错误的:printf("%d",a);【例7.1】main(){inti,a[10];for(i=0;i<=9;i++) 谭浩强C语言程序设计2001年5月1日a[i]=i;for(i=9;i>=0;i--)printf("%d",a[i]);}【例7.2】main(){inti,a[10];for(i=0;i<10;)a[i++]=i;for(i=9;i>=0;i--)printf("%d",a[i]);}【例7.3】main(){inti,a[10];for(i=0;i<10;)a[i++]=2*i+1;for(i=0;i<=9;i++)printf("%d",a[i]);printf(" %d%d ",a[5.2],a[5.8]);}本例中用一个循环语句给a数组各元素送入奇数值,然后用第二个循环语句输出各个奇数。在第一个for语句中,表达式3省略了。在下标变量中使用了表达式i++,用以修改循环变量。当然第二个for语句也可以这样作,C语言允许用表达式表示下标。程序中最后一个printf语句输出了两次a[5]的值,可以看出当下标不为整数时将自动取整。7.1.3一维数组的初始化给数组赋值的方法除了用赋值语句对数组元素逐个赋值外,还可采用初始化赋值和动态赋值的方法。数组初始化赋值是指在数组定义时给数组元素赋予初值。数组初始化是在编译阶段进行的。这样将减少运行时间,提高效率。初始化赋值的一般形式为:类型说明符数组名[常量表达式]={值,值……值};其中在{}中的各数据值即为各元素的初值,各值之间用逗号间隔。例如: 谭浩强C语言程序设计2001年5月1日inta[10]={0,1,2,3,4,5,6,7,8,9};相当于a[0]=0;a[1]=1...a[9]=9;C语言对数组的初始化赋值还有以下几点规定:1)可以只给部分元素赋初值。当{}中值的个数少于元素个数时,只给前面部分元素赋值。例如:inta[10]={0,1,2,3,4};表示只给a[0]~a[4]5个元素赋值,而后5个元素自动赋0值。2)只能给元素逐个赋值,不能给数组整体赋值。例如给十个元素全部赋1值,只能写为:inta[10]={1,1,1,1,1,1,1,1,1,1};而不能写为:inta[10]=1;3)如给全部元素赋值,则在数组说明中,可以不给出数组元素的个数。例如:inta[5]={1,2,3,4,5};可写为:inta[]={1,2,3,4,5};7.1.4一维数组程序举例可以在程序执行过程中,对数组作动态赋值。这时可用循环语句配合scanf函数逐个对数组元素赋值。【例7.4】main(){inti,max,a[10];printf("input10numbers: ");for(i=0;i<10;i++)scanf("%d",&a[i]);max=a[0];for(i=1;i<10;i++)if(a[i]>max)max=a[i];printf("maxmum=%d ",max);}本例程序中第一个for语句逐个输入10个数到数组a中。然后把a[0]送入max中。在第二个for语句中,从a[1]到a[9]逐个与max中的内容比较,若比max的值大,则把该下标变量送入max中,因此max总是在已比较过的下标变量中为最大者。比较结束,输出max的值。【例7.5】main(){ 谭浩强C语言程序设计2001年5月1日inti,j,p,q,s,a[10];printf(" input10numbers: ");for(i=0;i<10;i++)scanf("%d",&a[i]);for(i=0;i<10;i++){p=i;q=a[i];for(j=i+1;j<10;j++)if(q0)printf("st1>st2 ");if(k<0)printf("st1st2”。6.测字符串长度函数strlen格式:strlen(字符数组名)功能:测字符串的实际长度(不含字符串结束标志‘’)并作为函数返回值。【例7.17】#include"string.h"main(){intk;staticcharst[]="Clanguage";k=strlen(st);printf("Thelenthofthestringis%d ",k);}7.4程序举例【例7.18】把一个整数按大小顺序插入已排好序的数组中。为了把一个数按大小插入已排好序的数组中,应首先确定排序是从大到小还是从小到大进行的。设排序是从大到小进序的,则可把欲插入的数与数组中各数逐个比较,当找到第一个比插入数小的元素i时,该元素之前即为插入位置。然后从数组最后一个元素开始到该元素为止,逐个后移一个单元。最后把插入数赋予元素i即可。如果被插入数比所有的元素值都小则插入最后位置。main(){inti,j,p,q,s,n,a[11]={127,3,6,28,54,68,87,105,162,18};for(i=0;i<10;i++){p=i;q=a[i];for(j=i+1;j<10;j++)if(qa[i]) 谭浩强C语言程序设计2001年5月1日{for(s=9;s>=i;s--)a[s+1]=a[s];break;}a[i]=n;for(i=0;i<=10;i++)printf("%d",a[i]);printf(" ");}本程序首先对数组a中的10个数从大到小排序并输出排序结果。然后输入要插入的整数n。再用一个for语句把n和数组元素逐个比较,如果发现有n>a[i]时,则由一个内循环把i以下各元素值顺次后移一个单元。后移应从后向前进行(从a[9]开始到a[i]为止)。后移结束跳出外循环。插入点为i,把n赋予a[i]即可。如所有的元素均大于被插入数,则并未进行过后移工作。此时i=10,结果是把n赋于a[10]。最后一个循环输出插入数后的数组各元素值。程序运行时,输入数47。从结果中可以看出47已插入到54和28之间。【例7.19】在二维数组a中选出各行最大的元素组成一个一维数组b。a=(31687654321110810251237)b=(8710837)本题的编程思路是,在数组A的每一行中寻找最大的元素,找到之后把该值赋予数组B相应的元素即可。程序如下:main(){inta[][4]={3,16,87,65,4,32,11,108,10,25,12,27};intb[3],i,j,l;for(i=0;i<=2;i++){l=a[i][0];for(j=1;j<=3;j++)if(a[i][j]>l)l=a[i][j];b[i]=l;}printf(" arraya: ");for(i=0;i<=2;i++){for(j=0;j<=3;j++)printf("%5d",a[i][j]);printf(" ");}printf(" arrayb: ");for(i=0;i<=2;i++)printf("%5d",b[i]);printf(" ");} 谭浩强C语言程序设计2001年5月1日程序中第一个for语句中又嵌套了一个for语句组成了双重循环。外循环控制逐行处理,并把每行的第0列元素赋予l。进入内循环后,把l与后面各列元素比较,并把比l大者赋予l。内循环结束时l即为该行最大的元素,然后把l值赋予b[i]。等外循环全部完成时,数组b中已装入了a各行中的最大值。后面的两个for语句分别输出数组a和数组b。【例7.20】输入五个国家的名称按字母顺序排列输出。本题编程思路如下:五个国家名应由一个二维字符数组来处理。然而C语言规定可以把一个二维数组当成多个一维数组处理。因此本题又可以按五个一维数组处理,而每一个一维数组就是一个国家名字符串。用字符串比较函数比较各一维数组的大小,并排序,输出结果即可。编程如下:main(){charst[20],cs[5][20];inti,j,p;printf("inputcountry'sname: ");for(i=0;i<5;i++)gets(cs[i]);printf(" ");for(i=0;i<5;i++){p=i;strcpy(st,cs[i]);for(j=i+1;j<5;j++)if(strcmp(cs[j],st)<0){p=j;strcpy(st,cs[j]);}if(p!=i){strcpy(st,cs[i]);strcpy(cs[i],cs[p]);strcpy(cs[p],st);}puts(cs[i]);}printf(" ");}本程序的第一个for语句中,用gets函数输入五个国家名字符串。上面说过C语言允许把一个二维数组按多个一维数组处理,本程序说明cs[5][20]为二维字符数组,可分为五个一维数组cs[0],cs[1],cs[2],cs[3],cs[4]。因此在gets函数中使用cs[i]是合法的。在第二个for语句中又嵌套了一个for语句组成双重循环。这个双重循环完成按字母顺序排序的工作。在外层循环中把字符数组cs[i]中的国名字符串拷贝到数组st中,并把下标i赋予P。进入内层循环后,把st与cs[i]以后的各字符串作比较,若有比st小者则把该字符串拷贝到st中,并把其下标赋予p。内循环完成后如p不等于i说明有比cs[i]更小的字符串出现,因此交换cs[i]和st的内容。至此已确定了数组cs的第i号元素的排序值。然后输出该字符串。在外循环全部完成之后即完成全部排序和输出。 谭浩强C语言程序设计2001年5月1日7.5本章小结1.数组是程序设计中最常用的数据结构。数组可分为数值数组(整数组,实数组),字符数组以及后面将要介绍的指针数组,结构数组等。2.数组可以是一维的,二维的或多维的。3.数组类型说明由类型说明符、数组名、数组长度(数组元素个数)三部分组成。数组元素又称为下标变量。数组的类型是指下标变量取值的类型。4.对数组的赋值可以用数组初始化赋值,输入函数动态赋值和赋值语句赋值三种方法实现。对数值数组不能用赋值语句整体赋值、输入或输出,而必须用循环语句逐个对数组元素进行操作。 谭浩强C语言程序设计2001年5月1日8函数8.1概述在前面已经介绍过,C源程序是由函数组成的。虽然在前面各章的程序中大都只有一个主函数main(),但实用程序往往由多个函数组成。函数是C源程序的基本模块,通过对函数模块的调用实现特定的功能。C语言中的函数相当于其它高级语言的子程序。C语言不仅提供了极为丰富的库函数(如TurboC,MSC都提供了三百多个库函数),还允许用户建立自己定义的函数。用户可把自己的算法编成一个个相对独立的函数模块,然后用调用的方法来使用函数。可以说C程序的全部工作都是由各式各样的函数完成的,所以也把C语言称为函数式语言。由于采用了函数模块式的结构,C语言易于实现结构化程序设计。使程序的层次结构清晰,便于程序的编写、阅读、调试。在C语言中可从不同的角度对函数分类。1.从函数定义的角度看,函数可分为库函数和用户定义函数两种。1)库函数:由C系统提供,用户无须定义,也不必在程序中作类型说明,只需在程序前包含有该函数原型的头文件即可在程序中直接调用。在前面各章的例题中反复用到printf、scanf、getchar、putchar、gets、puts、strcat等函数均属此类。2)用户定义函数:由用户按需要写的函数。对于用户自定义函数,不仅要在程序中定义函数本身,而且在主调函数模块中还必须对该被调函数进行类型说明,然后才能使用。2.C语言的函数兼有其它语言中的函数和过程两种功能,从这个角度看,又可把函数分为有返回值函数和无返回值函数两种。1)有返回值函数:此类函数被调用执行完后将向调用者返回一个执行结果,称为函数返回值。如数学函数即属于此类函数。由用户定义的这种要返回函数值的函数,必须在函数定义和函数说明中明确返回值的类型。2)无返回值函数:此类函数用于完成某项特定的处理任务,执行完成后不向调用者返回函数值。这类函数类似于其它语言的过程。由于函数无须返回值,用户在定义此类函数时可指定它的返回为“空类型”,空类型的说明符为“void”。3.从主调函数和被调函数之间数据传送的角度看又可分为无参函数和有参函数两种。1)无参函数:函数定义、函数说明及函数调用中均不带参数。主调函数和被调函数之间不进行参数传送。此类函数通常用来完成一组指定的功能,可以返回或不返回函数值。2)有参函数:也称为带参函数。在函数定义及函数说明时都有参数,称为形式参数(简称为形参)。在函数调用时也必须给出参数,称为实际参数(简称为实参)。进行函数调用时,主调函数将把实参的值传送给形参,供被调函数使用。4.C语言提供了极为丰富的库函数,这些库函数又可从功能角度作以下分类。1)字符类型分类函数:用于对字符按ASCII码分类:字母,数字,控制字符,分隔符,大小写字母等。2)转换函数:用于字符或字符串的转换;在字符量和各类数字量(整型,实型等)之间 谭浩强C语言程序设计2001年5月1日进行转换;在大、小写之间进行转换。3)目录路径函数:用于文件目录和路径操作。4)诊断函数:用于内部错误检测。5)图形函数:用于屏幕管理和各种图形功能。6)输入输出函数:用于完成输入输出功能。7)接口函数:用于与DOS,BIOS和硬件的接口。8)字符串函数:用于字符串操作和处理。9)内存管理函数:用于内存管理。10)数学函数:用于数学函数计算。11)日期和时间函数:用于日期,时间转换操作。12)进程控制函数:用于进程管理和控制。13)其它函数:用于其它各种功能。以上各类函数不仅数量多,而且有的还需要硬件知识才会使用,因此要想全部掌握则需要一个较长的学习过程。应首先掌握一些最基本、最常用的函数,再逐步深入。由于课时关系,我们只介绍了很少一部分库函数,其余部分读者可根据需要查阅有关手册。还应该指出的是,在C语言中,所有的函数定义,包括主函数main在内,都是平行的。也就是说,在一个函数的函数体内,不能再定义另一个函数,即不能嵌套定义。但是函数之间允许相互调用,也允许嵌套调用。习惯上把调用者称为主调函数。函数还可以自己调用自己,称为递归调用。main函数是主函数,它可以调用其它函数,而不允许被其它函数调用。因此,C程序的执行总是从main函数开始,完成对其它函数的调用后再返回到main函数,最后由main函数结束整个程序。一个C源程序必须有,也只能有一个主函数main。8.2函数定义的一般形式1.无参函数的定义形式类型标识符函数名(){声明部分语句}其中类型标识符和函数名称为函数头。类型标识符指明了本函数的类型,函数的类型实际上是函数返回值的类型。该类型标识符与前面介绍的各种说明符相同。函数名是由用户定义的标识符,函数名后有一个空括号,其中无参数,但括号不可少。{}中的内容称为函数体。在函数体中声明部分,是对函数体内部所用到的变量的类型说明。在很多情况下都不要求无参函数有返回值,此时函数类型符可以写为void。我们可以改写一个函数定义:voidHello(){printf("Hello,world ");}这里,只把main改为Hello作为函数名,其余不变。Hello函数是一个无参函数,当被其它函数调用时,输出Helloworld字符串。2.有参函数定义的一般形式 谭浩强C语言程序设计2001年5月1日类型标识符函数名(形式参数表列){声明部分语句}有参函数比无参函数多了一个内容,即形式参数表列。在形参表中给出的参数称为形式参数,它们可以是各种类型的变量,各参数之间用逗号间隔。在进行函数调用时,主调函数将赋予这些形式参数实际的值。形参既然是变量,必须在形参表中给出形参的类型说明。例如,定义一个函数,用于求两个数中的大数,可写为:intmax(inta,intb){if(a>b)returna;elsereturnb;}第一行说明max函数是一个整型函数,其返回的函数值是一个整数。形参为a,b,均为整型量。a,b的具体值是由主调函数在调用时传送过来的。在{}中的函数体内,除形参外没有使用其它变量,因此只有语句而没有声明部分。在max函数体中的return语句是把a(或b)的值作为函数的值返回给主调函数。有返回值函数中至少应有一个return语句。在C程序中,一个函数的定义可以放在任意位置,既可放在主函数main之前,也可放在main之后。例如:可把max函数置在main之后,也可以把它放在main之前。修改后的程序如下所示。【例8.1】intmax(inta,intb){if(a>b)returna;elsereturnb;}main(){intmax(inta,intb);intx,y,z;printf("inputtwonumbers: ");scanf("%d%d",&x,&y);z=max(x,y);printf("maxmum=%d",z);}现在我们可以从函数定义、函数说明及函数调用的角度来分析整个程序,从中进一步了解函数的各种特点。程序的第1行至第5行为max函数定义。进入主函数后,因为准备调用max函数,故先对max函数进行说明(程序第8行)。函数定义和函数说明并不是一回事,在后面还要专门讨论。可以看出函数说明与函数定义中的函数头部分相同,但是末尾要加分号。程序第12行为调用max函数,并把x,y中的值传送给max的形参a,b。max函数执行的结果(a或b) 谭浩强C语言程序设计2001年5月1日将返回给变量z。最后由主函数输出z的值。8.3函数的参数和函数的值8.3.1形式参数和实际参数前面已经介绍过,函数的参数分为形参和实参两种。在本小节中,进一步介绍形参、实参的特点和两者的关系。形参出现在函数定义中,在整个函数体内都可以使用,离开该函数则不能使用。实参出现在主调函数中,进入被调函数后,实参变量也不能使用。形参和实参的功能是作数据传送。发生函数调用时,主调函数把实参的值传送给被调函数的形参从而实现主调函数向被调函数的数据传送。函数的形参和实参具有以下特点:1.形参变量只有在被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存单元。因此,形参只有在函数内部有效。函数调用结束返回主调函数后则不能再使用该形参变量。2.实参可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参。因此应预先用赋值,输入等办法使实参获得确定值。3.实参和形参在数量上,类型上,顺序上应严格一致,否则会发生类型不匹配”的错误。4.函数调用中发生的数据传送是单向的。即只能把实参的值传送给形参,而不能把形参的值反向地传送给实参。因此在函数调用过程中,形参的值发生改变,而实参中的值不会变化。【例8.2】可以说明这个问题。main(){intn;printf("inputnumber ");scanf("%d",&n);s(n);printf("n=%d ",n);}ints(intn){inti;for(i=n-1;i>=1;i--) 谭浩强C语言程序设计2001年5月1日n=n+i;printf("n=%d ",n);}本程序中定义了一个函数s,该函数的功能是求∑ni的值。在主函数中输入n值,并作为实参,在调用时传送给s函数的形参量n(注意,本例的形参变量和实参变量的标识符都为n,但这是两个不同的量,各自的作用域不同)。在主函数中用printf语句输出一次n值,这个n值是实参n的值。在函数s中也用printf语句输出了一次n值,这个n值是形参最后取得的n值0。从运行情况看,输入n值为100。即实参n的值为100。把此值传给函数s时,形参n的初值也为100,在执行函数过程中,形参n的值变为5050。返回主函数之后,输出实参n的值仍为100。可见实参的值不随形参的变化而变化。8.3.2函数的返回值函数的值是指函数被调用之后,执行函数体中的程序段所取得的并返回给主调函数的值。如调用正弦函数取得正弦值,调用例8.1的max函数取得的最大数等。对函数的值(或称函数返回值)有以下一些说明:1)函数的值只能通过return语句返回主调函数。return语句的一般形式为:return表达式;或者为:return(表达式);该语句的功能是计算表达式的值,并返回给主调函数。在函数中允许有多个return语句,但每次调用只能有一个return语句被执行,因此只能返回一个函数值。2)函数值的类型和函数定义中函数的类型应保持一致。如果两者不一致,则以函数类型为准,自动进行类型转换。3)如函数值为整型,在函数定义时可以省去类型说明。4)不返回函数值的函数,可以明确定义为“空类型”,类型说明符为“void”。如例8.2中函数s并不向主函数返函数值,因此可定义为:voids(intn){……}一旦函数被定义为空类型后,就不能在主调函数中使用被调函数的函数值了。例如,在定义s为空类型后,在主函数中写下述语句sum=s(n);就是错误的。为了使程序有良好的可读性并减少出错,凡不要求返回值的函数都应定义为空类型。 谭浩强C语言程序设计2001年5月1日8.4函数的调用8.4.1函数调用的一般形式前面已经说过,在程序中是通过对函数的调用来执行函数体的,其过程与其它语言的子程序调用相似。C语言中,函数调用的一般形式为:函数名(实际参数表)对无参函数调用时则无实际参数表。实际参数表中的参数可以是常数,变量或其它构造类型数据及表达式。各实参之间用逗号分隔。8.4.2函数调用的方式在C语言中,可以用以下几种方式调用函数:1.函数表达式:函数作为表达式中的一项出现在表达式中,以函数返回值参与表达式的运算。这种方式要求函数是有返回值的。例如:z=max(x,y)是一个赋值表达式,把max的返回值赋予变量z。2.函数语句:函数调用的一般形式加上分号即构成函数语句。例如:printf("%d",a);scanf("%d",&b);都是以函数语句的方式调用函数。3.函数实参:函数作为另一个函数调用的实际参数出现。这种情况是把该函数的返回值作为实参进行传送,因此要求该函数必须是有返回值的。例如:printf("%d",max(x,y));即是把max调用的返回值又作为printf函数的实参来使用的。在函数调用中还应该注意的一个问题是求值顺序的问题。所谓求值顺序是指对实参表中各量是自左至右使用呢,还是自右至左使用。对此,各系统的规定不一定相同。介绍printf函数时已提到过,这里从函数调用的角度再强调一下。【例8.3】main(){inti=8;printf("%d %d %d %d ",++i,--i,i++,i--);}如按照从右至左的顺序求值。运行结果应为:8778如对printf语句中的++i,--i,i++,i--从左至右求值,结果应为:988 谭浩强C语言程序设计2001年5月1日9应特别注意的是,无论是从左至右求值,还是自右至左求值,其输出顺序都是不变的,即输出顺序总是和实参表中实参的顺序相同。由于TurboC现定是自右至左求值,所以结果为8,7,7,8。上述问题如还不理解,上机一试就明白了。8.4.3被调用函数的声明和函数原型在主调函数中调用某函数之前应对该被调函数进行说明(声明),这与使用变量之前要先进行变量说明是一样的。在主调函数中对被调函数作说明的目的是使编译系统知道被调函数返回值的类型,以便在主调函数中按此种类型对返回值作相应的处理。其一般形式为:类型说明符被调函数名(类型形参,类型形参…);或为:类型说明符被调函数名(类型,类型…);括号内给出了形参的类型和形参名,或只给出形参类型。这便于编译系统进行检错,以防止可能出现的错误。例8.1main函数中对max函数的说明为:intmax(inta,intb);或写为:intmax(int,int);C语言中又规定在以下几种情况时可以省去主调函数中对被调函数的函数说明。1)如果被调函数的返回值是整型或字符型时,可以不对被调函数作说明,而直接调用。这时系统将自动对被调函数返回值按整型处理。例8.2的主函数中未对函数s作说明而直接调用即属此种情形。2)当被调函数的函数定义出现在主调函数之前时,在主调函数中也可以不对被调函数再作说明而直接调用。例如例8.1中,函数max的定义放在main函数之前,因此可在main函数中省去对max函数的函数说明intmax(inta,intb)。3)如在所有函数定义之前,在函数外预先说明了各个函数的类型,则在以后的各主调函数中,可不再对被调函数作说明。例如:charstr(inta);floatf(floatb);main(){……}charstr(inta){……}floatf(floatb){……}其中第一,二行对str函数和f函数预先作了说明。因此在以后各函数中无须对 谭浩强C语言程序设计2001年5月1日str和f函数再作说明就可直接调用。4)对库函数的调用不需要再作说明,但必须把该函数的头文件用include命令包含在源文件前部。8.5函数的嵌套调用C语言中不允许作嵌套的函数定义。因此各函数之间是平行的,不存在上一级函数和下一级函数的问题。但是C语言允许在一个函数的定义中出现对另一个函数的调用。这样就出现了函数的嵌套调用。即在被调函数中又调用其它函数。这与其它语言的子程序嵌套的情形是类似的。其关系可表示如图。图表示了两层嵌套的情形。其执行过程是:执行main函数中调用a函数的语句时,即转去执行a函数,在a函数中调用b函数时,又转去执行b函数,b函数执行完毕返回a函数的断点继续执行,a函数执行完毕返回main函数的断点继续执行。22【例8.4】计算s=2!+3!本题可编写两个函数,一个是用来计算平方值的函数f1,另一个是用来计算阶乘值的函数f2。主函数先调f1计算出平方值,再在f1中以平方值为实参,调用f2计算其阶乘值,然后返回f1,再返回主函数,在循环程序中计算累加和。longf1(intp){intk;longr;longf2(int);k=p*p;r=f2(k);returnr;}longf2(intq){longc=1;inti;for(i=1;i<=q;i++) 谭浩强C语言程序设计2001年5月1日c=c*i;returnc;}main(){inti;longs=0;for(i=2;i<=3;i++)s=s+f1(i);printf(" s=%ld ",s);}在程序中,函数f1和f2均为长整型,都在主函数之前定义,故不必再在主函数中对f12和f2加以说明。在主程序中,执行循环程序依次把i值作为实参调用函数f1求i值。在f122中又发生对函数f2的调用,这时是把i的值作为实参去调f2,在f2中完成求i!的计算。2f2执行完毕把C值(即i!)返回给f1,再由f1返回主函数实现累加。至此,由函数的嵌套调用实现了题目的要求。由于数值很大,所以函数和一些变量的类型都说明为长整型,否则会造成计算错误。8.6函数的递归调用一个函数在它的函数体内调用它自身称为递归调用。这种函数称为递归函数。C语言允许函数的递归调用。在递归调用中,主调函数又是被调函数。执行递归函数将反复调用其自身,每调用一次就进入新的一层。例如有函数f如下:intf(intx){inty;z=f(y);returnz;}这个函数是一个递归函数。但是运行该函数将无休止地调用其自身,这当然是不正确的。为了防止递归调用无终止地进行,必须在函数内有终止递归调用的手段。常用的办法是加条件判断,满足某种条件后就不再作递归调用,然后逐层返回。下面举例说明递归调用的执行过程。【例8.5】用递归法计算n!用递归法计算n!可用下述公式表示:n!=1(n=0,1)n×(n-1)!(n>1)按公式可编程如下: 谭浩强C语言程序设计2001年5月1日longff(intn){longf;if(n<0)printf("n<0,inputerror");elseif(n==0||n==1)f=1;elsef=ff(n-1)*n;return(f);}main(){intn;longy;printf(" inputainteagernumber: ");scanf("%d",&n);y=ff(n);printf("%d!=%ld",n,y);}程序中给出的函数ff是一个递归函数。主函数调用ff后即进入函数ff执行,如果n<0,n==0或n=1时都将结束函数的执行,否则就递归调用ff函数自身。由于每次递归调用的实参为n-1,即把n-1的值赋予形参n,最后当n-1的值为1时再作递归调用,形参n的值也为1,将使递归终止。然后可逐层退回。下面我们再举例说明该过程。设执行本程序时输入为5,即求5!。在主函数中的调用语句即为y=ff(5),进入ff函数后,由于n=5,不等于0或1,故应执行f=ff(n-1)*n,即f=ff(5-1)*5。该语句对ff作递归调用即ff(4)。进行四次递归调用后,ff函数形参取得的值变为1,故不再继续递归调用而开始逐层返回主调函数。ff(1)的函数返回值为1,ff(2)的返回值为1*2=2,ff(3)的返回值为2*3=6,ff(4)的返回值为6*4=24,最后返回值ff(5)为24*5=120。例8.5也可以不用递归的方法来完成。如可以用递推法,即从1开始乘以2,再乘以3…直到n。递推法比递归法更容易理解和实现。但是有些问题则只能用递归算法才能实现。典型的问题是Hanoi塔问题。【例8.6】Hanoi塔问题一块板上有三根针,A,B,C。A针上套有64个大小不等的圆盘,大的在下,小的在上。如图5.4所示。要把这64个圆盘从A针移动C针上,每次只能移动一个圆盘,移动可以借助B针进行。但在任何时候,任何针上的圆盘都必须保持大盘在下,小盘在上。求移动的步骤。本题算法分析如下,设A上有n个盘子。如果n=1,则将圆盘从A直接移动到C。如果n=2,则:1.将A上的n-1(等于1)个圆盘移到B上;2.再将A上的一个圆盘移到C上;3.最后将B上的n-1(等于1)个圆盘移到C上。 谭浩强C语言程序设计2001年5月1日如果n=3,则:A.将A上的n-1(等于2,令其为n`)个圆盘移到B(借助于C),步骤如下:(1)将A上的n`-1(等于1)个圆盘移到C上。(2)将A上的一个圆盘移到B。(3)将C上的n`-1(等于1)个圆盘移到B。B.将A上的一个圆盘移到C。C.将B上的n-1(等于2,令其为n`)个圆盘移到C(借助A),步骤如下:(1)将B上的n`-1(等于1)个圆盘移到A。(2)将B上的一个盘子移到C。(3)将A上的n`-1(等于1)个圆盘移到C。到此,完成了三个圆盘的移动过程。从上面分析可以看出,当n大于等于2时,移动的过程可分解为三个步骤:第一步把A上的n-1个圆盘移到B上;第二步把A上的一个圆盘移到C上;第三步把B上的n-1个圆盘移到C上;其中第一步和第三步是类同的。当n=3时,第一步和第三步又分解为类同的三步,即把n`-1个圆盘从一个针移到另一个针上,这里的n`=n-1。显然这是一个递归过程,据此算法可编程如下:move(intn,intx,inty,intz){if(n==1)printf("%c-->%c ",x,z);else{move(n-1,x,z,y);printf("%c-->%c ",x,z);move(n-1,y,x,z);}}main(){inth;printf(" inputnumber: ");scanf("%d",&h);printf("thesteptomoving%2ddiskes: ",h);move(h,'a','b','c');}从程序中可以看出,move函数是一个递归函数,它有四个形参n,x,y,z。n表示圆盘数,x,y,z分别表示三根针。move函数的功能是把x上的n个圆盘移动到z上。当n==1时,直接把x上的圆盘移至z上,输出x→z。如n!=1则分为三步:递归调用move函数,把n-1个圆盘从x移到y;输出x→z;递归调用move函数,把n-1个圆盘从y移到z。在递归调用过程中n=n-1,故n的值逐次递减,最后n=1时,终止递归,逐层返回。当n=4时程序运行的结果为: 谭浩强C语言程序设计2001年5月1日inputnumber:4thesteptomoving4diskes:a→ba→cb→ca→bc→ac→ba→ba→cb→cb→ac→ab→ca→ba→cb→c8.7数组作为函数参数数组可以作为函数的参数使用,进行数据传送。数组用作函数参数有两种形式,一种是把数组元素(下标变量)作为实参使用;另一种是把数组名作为函数的形参和实参使用。1.数组元素作函数实参数组元素就是下标变量,它与普通变量并无区别。因此它作为函数实参使用与普通变量是完全相同的,在发生函数调用时,把作为实参的数组元素的值传送给形参,实现单向的值传送。例5.4说明了这种情况。【例8.7】判别一个整数数组中各元素的值,若大于0则输出该值,若小于等于0则输出0值。编程如下:voidnzp(intv){if(v>0)printf("%d",v);elseprintf("%d",0);}main(){inta[5],i;printf("input5numbers ");for(i=0;i<5;i++){scanf("%d",&a[i]);nzp(a[i]);}} 谭浩强C语言程序设计2001年5月1日本程序中首先定义一个无返回值函数nzp,并说明其形参v为整型变量。在函数体中根据v值输出相应的结果。在main函数中用一个for语句输入数组各元素,每输入一个就以该元素作实参调用一次nzp函数,即把a[i]的值传送给形参v,供nzp函数使用。2.数组名作为函数参数用数组名作函数参数与用数组元素作实参有几点不同:1)用数组元素作实参时,只要数组类型和函数的形参变量的类型一致,那么作为下标变量的数组元素的类型也和函数形参变量的类型是一致的。因此,并不要求函数的形参也是下标变量。换句话说,对数组元素的处理是按普通变量对待的。用数组名作函数参数时,则要求形参和相对应的实参都必须是类型相同的数组,都必须有明确的数组说明。当形参和实参二者不一致时,即会发生错误。2)在普通变量或下标变量作函数参数时,形参变量和实参变量是由编译系统分配的两个不同的内存单元。在函数调用时发生的值传送是把实参变量的值赋予形参变量。在用数组名作函数参数时,不是进行值的传送,即不是把实参数组的每一个元素的值都赋予形参数组的各个元素。因为实际上形参数组并不存在,编译系统不为形参数组分配内存。那么,数据的传送是如何实现的呢?在我们曾介绍过,数组名就是数组的首地址。因此在数组名作函数参数时所进行的传送只是地址的传送,也就是说把实参数组的首地址赋予形参数组名。形参数组名取得该首地址之后,也就等于有了实在的数组。实际上是形参数组和实参数组为同一数组,共同拥有一段内存空间。上图说明了这种情形。图中设a为实参数组,类型为整型。a占有以2000为首地址的一块内存区。b为形参数组名。当发生函数调用时,进行地址传送,把实参数组a的首地址传送给形参数组名b,于是b也取得该地址2000。于是a,b两数组共同占有以2000为首地址的一段连续内存单元。从图中还可以看出a和b下标相同的元素实际上也占相同的两个内存单元(整型数组每个元素占二字节)。例如a[0]和b[0]都占用2000和2001单元,当然a[0]等于b[0]。类推则有a[i]等于b[i]。【例8.8】数组a中存放了一个学生5门课程的成绩,求平均成绩。floataver(floata[5]){inti;floatav,s=a[0];for(i=1;i<5;i++)s=s+a[i];av=s/5;returnav;}voidmain() 谭浩强C语言程序设计2001年5月1日{floatsco[5],av;inti;printf(" input5scores: ");for(i=0;i<5;i++)scanf("%f",&sco[i]);av=aver(sco);printf("averagescoreis%5.2f",av);}本程序首先定义了一个实型函数aver,有一个形参为实型数组a,长度为5。在函数aver中,把各元素值相加求出平均值,返回给主函数。主函数main中首先完成数组sco的输入,然后以sco作为实参调用aver函数,函数返回值送av,最后输出av值。从运行情况可以看出,程序实现了所要求的功能。3)前面已经讨论过,在变量作函数参数时,所进行的值传送是单向的。即只能从实参传向形参,不能从形参传回实参。形参的初值和实参相同,而形参的值发生改变后,实参并不变化,两者的终值是不同的。而当用数组名作函数参数时,情况则不同。由于实际上形参和实参为同一数组,因此当形参数组发生变化时,实参数组也随之变化。当然这种情况不能理解为发生了“双向”的值传递。但从实际情况来看,调用函数之后实参数组的值将由于形参数组值的变化而变化。为了说明这种情况,把例5.4改为例5.6的形式。【例8.9】题目同8.7例。改用数组名作函数参数。voidnzp(inta[5]){inti;printf(" valuesofarrayaare: ");for(i=0;i<5;i++){if(a[i]<0)a[i]=0;printf("%d",a[i]);}}main(){intb[5],i;printf(" input5numbers: ");for(i=0;i<5;i++)scanf("%d",&b[i]);printf("initialvaluesofarraybare: ");for(i=0;i<5;i++)printf("%d",b[i]);nzp(b);printf(" lastvaluesofarraybare: "); 谭浩强C语言程序设计2001年5月1日for(i=0;i<5;i++)printf("%d",b[i]);}本程序中函数nzp的形参为整数组a,长度为5。主函数中实参数组b也为整型,长度也为5。在主函数中首先输入数组b的值,然后输出数组b的初始值。然后以数组名b为实参调用nzp函数。在nzp中,按要求把负值单元清0,并输出形参数组a的值。返回主函数之后,再次输出数组b的值。从运行结果可以看出,数组b的初值和终值是不同的,数组b的终值和数组a是相同的。这说明实参形参为同一数组,它们的值同时得以改变。用数组名作为函数参数时还应注意以下几点:a.形参数组和实参数组的类型必须一致,否则将引起错误。b.形参数组和实参数组的长度可以不相同,因为在调用时,只传送首地址而不检查形参数组的长度。当形参数组的长度与实参数组不一致时,虽不至于出现语法错误(编译能通过),但程序执行结果将与实际不符,这是应予以注意的。【例8.10】如把例8.9修改如下:voidnzp(inta[8]){inti;printf(" valuesofarrayaare: ");for(i=0;i<8;i++){if(a[i]<0)a[i]=0;printf("%d",a[i]);}}main(){intb[5],i;printf(" input5numbers: ");for(i=0;i<5;i++)scanf("%d",&b[i]);printf("initialvaluesofarraybare: ");for(i=0;i<5;i++)printf("%d",b[i]);nzp(b);printf(" lastvaluesofarraybare: ");for(i=0;i<5;i++)printf("%d",b[i]);}本程序与例8.9程序比,nzp函数的形参数组长度改为8,函数体中,for语句的循环条件也改为i<8。因此,形参数组a和实参数组b的长度不一致。编译能够通过,但从结果 谭浩强C语言程序设计2001年5月1日看,数组a的元素a[5],a[6],a[7]显然是无意义的。c.在函数形参表中,允许不给出形参数组的长度,或用一个变量来表示数组元素的个数。例如,可以写为:voidnzp(inta[])或写为voidnzp(inta[],intn)其中形参数组a没有给出长度,而由n值动态地表示数组的长度。n的值由主调函数的实参进行传送。由此,例8.10又可改为例8.11的形式。【例8.11】voidnzp(inta[],intn){inti;printf(" valuesofarrayaare: ");for(i=0;ib?a:b;return(c);}main(){inta=8;printf("%d ",max(a,b));} 谭浩强C语言程序设计2001年5月1日如果同一个源文件中,外部变量与局部变量同名,则在局部变量的作用范围内,外部变量被“屏蔽”,即它不起作用。8.9变量的存储类别8.9.1动态存储方式与静态动态存储方式前面已经介绍了,从变量的作用域(即从空间)角度来分,可以分为全局变量和局部变量。从另一个角度,从变量值存在的作时间(即生存期)角度来分,可以分为静态存储方式和动态存储方式。静态存储方式:是指在程序运行期间分配固定的存储空间的方式。动态存储方式:是在程序运行期间根据需要进行动态的分配存储空间的方式。用户存储空间可以分为三个部分:1)程序区;2)静态存储区;3)动态存储区;全局变量全部存放在静态存储区,在程序开始执行时给全局变量分配存储区,程序行完毕就释放。在程序执行过程中它们占据固定的存储单元,而不动态地进行分配和释放;动态存储区存放以下数据:1)函数形式参数;2)自动变量(未加static声明的局部变量);3)函数调用实的现场保护和返回地址;对以上这些数据,在函数开始调用时分配动态存储空间,函数结束时释放这些空间。在c语言中,每个变量和函数有两个属性:数据类型和数据的存储类别。8.9.2auto变量函数中的局部变量,如不专门声明为static存储类别,都是动态地分配存储空间的,数据存储在动态存储区中。函数中的形参和在函数中定义的变量(包括在复合语句中定义的变量),都属此类,在调用该函数时系统会给它们分配存储空间,在函数调用结束时就自动释放这些存储空间。这类局部变量称为自动变量。自动变量用关键字auto作存储类别的声明。例如:intf(inta)/*定义f函数,a为参数*/ 谭浩强C语言程序设计2001年5月1日{autointb,c=3;/*定义b,c自动变量*/……}a是形参,b,c是自动变量,对c赋初值3。执行完f函数后,自动释放a,b,c所占的存储单元。关键字auto可以省略,auto不写则隐含定为“自动存储类别”,属于动态存储方式。8.9.3用static声明局部变量有时希望函数中的局部变量的值在函数调用结束后不消失而保留原值,这时就应该指定局部变量为“静态局部变量”,用关键字static进行声明。【例8.15】考察静态局部变量的值。f(inta){autob=0;staticc=3;b=b+1;c=c+1;return(a+b+c);}main(){inta=2,i;for(i=0;i<3;i++)printf("%d",f(a));}对静态局部变量的说明:1)静态局部变量属于静态存储类别,在静态存储区内分配存储单元。在程序整个运行期间都不释放。而自动变量(即动态局部变量)属于动态存储类别,占动态存储空间,函数调用结束后即释放。2)静态局部变量在编译时赋初值,即只赋初值一次;而对自动变量赋初值是在函数调用时进行,每调用一次函数重新给一次初值,相当于执行一次赋值语句。3)如果在定义局部变量时不赋初值的话,则对静态局部变量来说,编译时自动赋初值0(对数值型变量)或空字符(对字符变量)。而对自动变量来说,如果不赋初值则它的值是一个不确定的值。【例8.16】打印1到5的阶乘值。intfac(intn){staticintf=1;f=f*n;return(f);}main(){inti;for(i=1;i<=5;i++) 谭浩强C语言程序设计2001年5月1日printf("%d!=%d ",i,fac(i));}8.9.4register变量为了提高效率,C语言允许将局部变量得值放在CPU中的寄存器中,这种变量叫“寄存器变量”,用关键字register作声明。【例8.17】使用寄存器变量。intfac(intn){registerinti,f=1;for(i=1;i<=n;i++)f=f*ireturn(f);}main(){inti;for(i=0;i<=5;i++)printf("%d!=%d ",i,fac(i));}说明:1)只有局部自动变量和形式参数可以作为寄存器变量;2)一个计算机系统中的寄存器数目有限,不能定义任意多个寄存器变量;3)局部静态变量不能定义为寄存器变量。8.9.5用extern声明外部变量外部变量(即全局变量)是在函数的外部定义的,它的作用域为从变量定义处开始,到本程序文件的末尾。如果外部变量不在文件的开头定义,其有效的作用范围只限于定义处到文件终了。如果在定义点之前的函数想引用该外部变量,则应该在引用之前用关键字extern对该变量作“外部变量声明”。表示该变量是一个已经定义的外部变量。有了此声明,就可以从“声明”处起,合法地使用该外部变量。【例8.18】用extern声明外部变量,扩展程序文件中的作用域。intmax(intx,inty){intz;z=x>y?x:y;return(z);}main(){externA,B; 谭浩强C语言程序设计2001年5月1日printf("%d ",max(A,B));}intA=13,B=-8;说明:在本程序文件的最后1行定义了外部变量A,B,但由于外部变量定义的位置在函数main之后,因此本来在main函数中不能引用外部变量A,B。现在我们在main函数中用extern对A和B进行“外部变量声明”,就可以从“声明”处起,合法地使用该外部变量A和B。 9预处理命令9.1概述在前面各章中,已多次使用过以“#”号开头的预处理命令。如包含命令#include,宏定义命令#define等。在源程序中这些命令都放在函数之外,而且一般都放在源文件的前面,它们称为预处理部分。所谓预处理是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作。预处理是C语言的一个重要功能,它由预处理程序负责完成。当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。C语言提供了多种预处理功能,如宏定义、文件包含、条件编译等。合理地使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。本章介绍常用的几种预处理功能。9.2宏定义在C语言源程序中允许用一个标识符来表示一个字符串,称为“宏”。被定义为“宏”的标识符称为“宏名”。在编译预处理时,对程序中所有出现的“宏名”,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”。宏定义是由源程序中的宏定义命令完成的。宏代换是由预处理程序自动完成的。在C语言中,“宏”分为有参数和无参数两种。下面分别讨论这两种“宏”的定义和调用。9.2.1无参宏定义无参宏的宏名后不带参数。其定义的一般形式为:#define标识符字符串其中的“#”表示这是一条预处理命令。凡是以“#”开头的均为预处理命令。“define”为宏定义命令。“标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。在前面介绍过的符号常量的定义就是一种无参宏定义。此外,常对程序中反复使用的表达式进行宏定义。例如:#defineM(y*y+3*y)它的作用是指定标识符M来代替表达式(y*y+3*y)。在编写源程序时,所有的(y*y+3*y)都可由M代替,而对源程序作编译时,将先由预处理程序进行宏代换,即用(y*y+3*y)表达式去置换所有的宏名M,然后再进行编译。【例9.1】#defineM(y*y+3*y)main(){ ints,y;printf("inputanumber:");scanf("%d",&y);s=3*M+4*M+5*M;printf("s=%d ",s);}上例程序中首先进行宏定义,定义M来替代表达式(y*y+3*y),在s=3*M+4*M+5*M中作了宏调用。在预处理时经宏展开后该语句变为:s=3*(y*y+3*y)+4*(y*y+3*y)+5*(y*y+3*y);但要注意的是,在宏定义中表达式(y*y+3*y)两边的括号不能少。否则会发生错误。如当作以下定义后:#difineMy*y+3*y在宏展开时将得到下述语句:s=3*y*y+3*y+4*y*y+3*y+5*y*y+3*y;这相当于:2223y+3y+4y+3y+5y+3y;显然与原题意要求不符。计算结果当然是错误的。因此在作宏定义时必须十分注意。应保证在宏代换之后不发生错误。对于宏定义还要说明以下几点:1)宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单的代换,字符串中可以含任何字符,可以是常数,也可以是表达式,预处理程序对它不作任何检查。如有错误,只能在编译已被宏展开后的源程序时发现。2)宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起置换。3)宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef命令。例如:#definePI3.14159main(){……}#undefPIf1(){……}表示PI只在main函数中有效,在f1中无效。4)宏名在源程序中若用引号括起来,则预处理程序不对其作宏代换。【例9.2】 #defineOK100main(){printf("OK");printf(" ");}上例中定义宏名OK表示100,但在printf语句中OK被引号括起来,因此不作宏代换。程序的运行结果为:OK这表示把“OK”当字符串处理。5)宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名。在宏展开时由预处理程序层层代换。例如:#definePI3.1415926#defineSPI*y*y/*PI是已定义的宏名*/对语句:printf("%f",S);在宏代换后变为:printf("%f",3.1415926*y*y);6)习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母。7)可用宏定义表示数据类型,使书写方便。例如:#defineSTUstructstu在程序中可用STU作变量说明:STUbody[5],*p;#defineINTEGERint在程序中即可用INTEGER作整型变量说明:INTEGERa,b;应注意用宏定义表示数据类型和用typedef定义数据说明符的区别。宏定义只是简单的字符串代换,是在预处理完成的,而typedef是在编译时处理的,它不是作简单的代换,而是对类型说明符重新命名。被命名的标识符具有类型定义说明的功能。请看下面的例子:#definePIN1int*typedef(int*)PIN2;从形式上看这两者相似,但在实际使用中却不相同。下面用PIN1,PIN2说明变量时就可以看出它们的区别:PIN1a,b;在宏代换后变成:int*a,b;表示a是指向整型的指针变量,而b是整型变量。然而:PIN2a,b;表示a,b都是指向整型的指针变量。因为PIN2是一个类型说明符。由这个例子可见,宏定义虽然也可表示数据类型,但毕竟是作字符代换。在使用时要分外小心,以避出错。8)对“输出格式”作宏定义,可以减少书写麻烦。 【例9.3】中就采用了这种方法。#definePprintf#defineD"%d "#defineF"%f "main(){inta=5,c=8,e=11;floatb=3.8,d=9.7,f=21.08;P(DF,a,b);P(DF,c,d);P(DF,e,f);}9.2.2带参宏定义C语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。对带参数的宏,在调用中,不仅要宏展开,而且要用实参去代换形参。带参宏定义的一般形式为:#define宏名(形参表)字符串在字符串中含有各个形参。带参宏调用的一般形式为:宏名(实参表);例如:#defineM(y)y*y+3*y/*宏定义*/……k=M(5);/*宏调用*/……在宏调用时,用实参5去代替形参y,经预处理宏展开后的语句为:k=5*5+3*5【例9.4】#defineMAX(a,b)(a>b)?a:bmain(){intx,y,max;printf("inputtwonumbers:");scanf("%d%d",&x,&y);max=MAX(x,y);printf("max=%d ",max);}上例程序的第一行进行带参宏定义,用宏名MAX表示条件表达式(a>b)?a:b,形参a,b均出现在条件表达式中。程序第七行max=MAX(x,y)为宏调用,实参x,y,将代换形参a,b。 宏展开后该语句为:max=(x>y)?x:y;用于计算x,y中的大数。对于带参的宏定义有以下问题需要说明:1.带参宏定义中,宏名和形参表之间不能有空格出现。例如把:#defineMAX(a,b)(a>b)?a:b写为:#defineMAX(a,b)(a>b)?a:b将被认为是无参宏定义,宏名MAX代表字符串(a,b)(a>b)?a:b。宏展开时,宏调用语句:max=MAX(x,y);将变为:max=(a,b)(a>b)?a:b(x,y);这显然是错误的。2.在带参宏定义中,形式参数不分配内存单元,因此不必作类型定义。而宏调用中的实参有具体的值。要用它们去代换形参,因此必须作类型说明。这是与函数中的情况不同的。在函数中,形参和实参是两个不同的量,各有自己的作用域,调用时要把实参值赋予形参,进行“值传递”。而在带参宏中,只是符号代换,不存在值传递的问题。3.在宏定义中的形参是标识符,而宏调用中的实参可以是表达式。【例9.5】#defineSQ(y)(y)*(y)main(){inta,sq;printf("inputanumber:");scanf("%d",&a);sq=SQ(a+1);printf("sq=%d ",sq);}上例中第一行为宏定义,形参为y。程序第七行宏调用中实参为a+1,是一个表达式,在宏展开时,用a+1代换y,再用(y)*(y)代换SQ,得到如下语句:sq=(a+1)*(a+1);这与函数的调用是不同的,函数调用时要把实参表达式的值求出来再赋予形参。而宏代换中对实参表达式不作计算直接地照原样代换。4.在宏定义中,字符串内的形参通常要用括号括起来以避免出错。在上例中的宏定义中(y)*(y)表达式的y都用括号括起来,因此结果是正确的。如果去掉括号,把程序改为以下形式:【例9.6】#defineSQ(y)y*ymain(){inta,sq;printf("inputanumber:"); scanf("%d",&a);sq=SQ(a+1);printf("sq=%d ",sq);}运行结果为:inputanumber:3sq=7同样输入3,但结果却是不一样的。问题在哪里呢?这是由于代换只作符号代换而不作其它处理而造成的。宏代换后将得到以下语句:sq=a+1*a+1;由于a为3故sq的值为7。这显然与题意相违,因此参数两边的括号是不能少的。即使在参数两边加括号还是不够的,请看下面程序:【例9.7】#defineSQ(y)(y)*(y)main(){inta,sq;printf("inputanumber:");scanf("%d",&a);sq=160/SQ(a+1);printf("sq=%d ",sq);}本程序与前例相比,只把宏调用语句改为:sq=160/SQ(a+1);运行本程序如输入值仍为3时,希望结果为10。但实际运行的结果如下:inputanumber:3sq=160为什么会得这样的结果呢?分析宏调用语句,在宏代换之后变为:sq=160/(a+1)*(a+1);a为3时,由于“/”和“*”运算符优先级和结合性相同,则先作160/(3+1)得40,再作40*(3+1)最后得160。为了得到正确答案应在宏定义中的整个字符串外加括号,程序修改如下:【例9.8】#defineSQ(y)((y)*(y))main(){inta,sq;printf("inputanumber:");scanf("%d",&a);sq=160/SQ(a+1);printf("sq=%d ",sq);} 以上讨论说明,对于宏定义不仅应在参数两侧加括号,也应在整个字符串外加括号。5.带参的宏和带参函数很相似,但有本质上的不同,除上面已谈到的各点外,把同一表达式用函数处理与用宏处理两者的结果有可能是不同的。【例9.9】main(){inti=1;while(i<=5)printf("%d ",SQ(i++));}SQ(inty){return((y)*(y));}【例9.10】#defineSQ(y)((y)*(y))main(){inti=1;while(i<=5)printf("%d ",SQ(i++));}在例9.9中函数名为SQ,形参为Y,函数体表达式为((y)*(y))。在例9.10中宏名为SQ,形参也为y,字符串表达式为(y)*(y))。例9.9的函数调用为SQ(i++),例9.10的宏调用为SQ(i++),实参也是相同的。从输出结果来看,却大不相同。分析如下:在例9.9中,函数调用是把实参i值传给形参y后自增1。然后输出函数值。因而要循环5次。输出1~5的平方值。而在例9.10中宏调用时,只作代换。SQ(i++)被代换为((i++)*(i++))。在第一次循环时,由于i等于1,其计算过程为:表达式中前一个i初值为1,然后i自增1变为2,因此表达式中第2个i初值为2,两相乘的结果也为2,然后i值再自增1,得3。在第二次循环时,i值已有初值为3,因此表达式中前一个i为3,后一个i为4,乘积为12,然后i再自增1变为5。进入第三次循环,由于i值已为5,所以这将是最后一次循环。计算表达式的值为5*6等于30。i值再自增1变为6,不再满足循环条件,停止循环。从以上分析可以看出函数调用和宏调用二者在形式上相似,在本质上是完全不同的。6.宏定义也可用来定义多个语句,在宏调用时,把这些语句又代换到源程序内。看下面的例子。【例9.11】#defineSSSV(s1,s2,s3,v)s1=l*w;s2=l*h;s3=w*h;v=w*l*h;main(){intl=3,w=4,h=5,sa,sb,sc,vv; SSSV(sa,sb,sc,vv);printf("sa=%d sb=%d sc=%d vv=%d ",sa,sb,sc,vv);}程序第一行为宏定义,用宏名SSSV表示4个赋值语句,4个形参分别为4个赋值符左部的变量。在宏调用时,把4个语句展开并用实参代替形参。使计算结果送入实参之中。9.3文件包含文件包含是C预处理程序的另一个重要功能。文件包含命令行的一般形式为:#include"文件名"在前面我们已多次用此命令包含过库函数的头文件。例如:#include"stdio.h"#include"math.h"文件包含命令的功能是把指定的文件插入该命令行位置取代该命令行,从而把指定的文件和当前的源程序文件连成一个源文件。在程序设计中,文件包含是很有用的。一个大的程序可以分为多个模块,由多个程序员分别编程。有些公用的符号常量或宏定义等可单独组成一个文件,在其它文件的开头用包含命令包含该文件即可使用。这样,可避免在每个文件开头都去书写那些公用量,从而节省时间,并减少出错。对文件包含命令还要说明以下几点:1.包含命令中的文件名可以用双引号括起来,也可以用尖括号括起来。例如以下写法都是允许的:#include"stdio.h"#include但是这两种形式是有区别的:使用尖括号表示在包含文件目录中去查找(包含目录是由用户在设置环境时设置的),而不在源文件目录去查找;使用双引号则表示首先在当前的源文件目录中查找,若未找到才到包含目录中去查找。用户编程时可根据自己文件所在的目录来选择某一种命令形式。2.一个include命令只能指定一个被包含文件,若有多个文件要包含,则需用多个include命令。3.文件包含允许嵌套,即在一个被包含的文件中又可以包含另一个文件。9.4条件编译预处理程序提供了条件编译的功能。可以按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件。这对于程序的移植和调试是很有用的。条件编译有三种形式,下面分别介绍:1.第一种形式:#ifdef标识符程序段1 #else程序段2#endif它的功能是,如果标识符已被#define命令定义过则对程序段1进行编译;否则对程序段2进行编译。如果没有程序段2(它为空),本格式中的#else可以没有,即可以写为:#ifdef标识符程序段#endif【例9.12】#defineNUMokmain(){structstu{intnum;char*name;charsex;floatscore;}*ps;ps=(structstu*)malloc(sizeof(structstu));ps->num=102;ps->name="Zhangping";ps->sex='M';ps->score=62.5;#ifdefNUMprintf("Number=%d Score=%f ",ps->num,ps->score);#elseprintf("Name=%s Sex=%c ",ps->name,ps->sex);#endiffree(ps);}由于在程序的第16行插入了条件编译预处理命令,因此要根据NUM是否被定义过来决定编译那一个printf语句。而在程序的第一行已对NUM作过宏定义,因此应对第一个printf语句作编译故运行结果是输出了学号和成绩。在程序的第一行宏定义中,定义NUM表示字符串OK,其实也可以为任何字符串,甚至不给出任何字符串,写为:#defineNUM也具有同样的意义。只有取消程序的第一行才会去编译第二个printf语句。读者可上机试作。2.第二种形式:#ifndef标识符程序段1#else 程序段2#endif与第一种形式的区别是将“ifdef”改为“ifndef”。它的功能是,如果标识符未被#define命令定义过则对程序段1进行编译,否则对程序段2进行编译。这与第一种形式的功能正相反。3.第三种形式:#if常量表达式程序段1#else程序段2#endif它的功能是,如常量表达式的值为真(非0),则对程序段1进行编译,否则对程序段2进行编译。因此可以使程序在不同条件下,完成不同的功能。【例9.13】#defineR1main(){floatc,r,s;printf("inputanumber:");scanf("%f",&c);#ifRr=3.14159*c*c;printf("areaofroundis:%f ",r);#elses=c*c;printf("areaofsquareis:%f ",s);#endif}本例中采用了第三种形式的条件编译。在程序第一行宏定义中,定义R为1,因此在条件编译时,常量表达式的值为真,故计算并输出圆面积。上面介绍的条件编译当然也可以用条件语句来实现。但是用条件语句将会对整个源程序进行编译,生成的目标代码程序很长,而采用条件编译,则根据条件只编译其中的程序段1或程序段2,生成的目标程序较短。如果条件选择的程序段很长,采用条件编译的方法是十分必要的。9.5本章小结1.预处理功能是C语言特有的功能,它是在对源程序正式编译前由预处理程序完成的。程序员在程序中用预处理命令来调用这些功能。2.宏定义是用一个标识符来表示一个字符串,这个字符串可以是常量、变量或表达式。在宏调用中将用该字符串代换宏名。3.宏定义可以带有参数,宏调用时是以实参代换形参。而不是“值传送”。4.为了避免宏代换时发生错误,宏定义中的字符串应加括号,字符串中出现的形式参数两 边也应加括号。5.文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。6.条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。7.使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。 谭浩强C语言程序设计2001年5月1日10指针指针是C语言中广泛使用的一种数据类型。运用指针编程是C语言最主要的风格之一。利用指针变量可以表示各种数据结构;能很方便地使用数组和字符串;并能象汇编语言一样处理内存地址,从而编出精练而高效的程序。指针极大地丰富了C语言的功能。学习指针是学习C语言中最重要的一环,能否正确理解和使用指针是我们是否掌握C语言的一个标志。同时,指针也是C语言中最为困难的一部分,在学习中除了要正确理解基本概念,还必须要多编程,上机调试。只要作到这些,指针也是不难掌握的。10.1地址指针的基本概念在计算机中,所有的数据都是存放在存储器中的。一般把存储器中的一个字节称为一个内存单元,不同的数据类型所占用的内存单元数不等,如整型量占2个单元,字符量占1个单元等,在前面已有详细的介绍。为了正确地访问这些内存单元,必须为每个内存单元编上号。根据一个内存单元的编号即可准确地找到该内存单元。内存单元的编号也叫做地址。既然根据内存单元的编号或地址就可以找到所需的内存单元,所以通常也把这个地址称为指针。内存单元的指针和内存单元的内容是两个不同的概念。可以用一个通俗的例子来说明它们之间的关系。我们到银行去存取款时,银行工作人员将根据我们的帐号去找我们的存款单,找到之后在存单上写入存款、取款的金额。在这里,帐号就是存单的指针,存款数是存单的内容。对于一个内存单元来说,单元的地址即为指针,其中存放的数据才是该单元的内容。在C语言中,允许用一个变量来存放指针,这种变量称为指针变量。因此,一个指针变量的值就是某个内存单元的地址或称为某内存单元的指针。图中,设有字符变量C,其内容为“K”(ASCII码为十进制数75),C占用了011A号单元(地址用十六进数表示)。设有指针变量P,内容为011A,这种情况我们称为P指向变量C,或说P是指向变量C的指针。严格地说,一个指针是一个地址,是一个常量。而一个指针变量却可以被赋予不同的指针值,是变量。但常把指针变量简称为指针。为了避免混淆,我们中约定:“指针”是指地址,是常量,“指针变量”是指取值为地址的变量。定义指针的目的是为了通过指针去访问内存单元。既然指针变量的值是一个地址,那么这个地址不仅可以是变量的地址,也可以是其它数据结构的地址。在一个指针变量中存放一个数组或一个函数的首地址有何意义呢?因为数组或函数都是连续存放的。通过访问指针变量取得了数组或函数的首地址,也就找到了该数组或函数。这样一来,凡是出现数组,函数的地方都可以用一个指针变量来表示,只要该指针变量中赋予数组或函数的首地址即可。这样做,将会使程序的概念十分清楚,程序本身也 谭浩强C语言程序设计2001年5月1日精练,高效。在C语言中,一种数据类型或数据结构往往都占有一组连续的内存单元。用“地址”这个概念并不能很好地描述一种数据类型或数据结构,而“指针”虽然实际上也是一个地址,但它却是一个数据结构的首地址,它是“指向”一个数据结构的,因而概念更为清楚,表示更为明确。这也是引入“指针”概念的一个重要原因。10.2变量的指针和指向变量的指针变量变量的指针就是变量的地址。存放变量地址的变量是指针变量。即在C语言中,允许用一个变量来存放指针,这种变量称为指针变量。因此,一个指针变量的值就是某个变量的地址或称为某变量的指针。为了表示指针变量和它所指向的变量之间的关系,在程序中用“*”符号表示“指向”,例如,i_pointer代表指针变量,而*i_pointer是i_pointer所指向的变量。因此,下面两个语句作用相同:i=3;*i_pointer=3;第二个语句的含义是将3赋给指针变量i_pointer所指向的变量。10.2.1定义一个指针变量对指针变量的定义包括三个内容:(1)指针类型说明,即定义变量为一个指针变量;(2)指针变量名;(3)变量值(指针)所指向的变量的数据类型。其一般形式为:类型说明符*变量名;其中,*表示这是一个指针变量,变量名即为定义的指针变量名,类型说明符表示本指针变量所指向的变量的数据类型。例如:int*p1;表示p1是一个指针变量,它的值是某个整型变量的地址。或者说p1指向一个整型变量。至于p1究竟指向哪一个整型变量,应由向p1赋予的地址来决定。再如:int*p2;/*p2是指向整型变量的指针变量*/float*p3;/*p3是指向浮点变量的指针变量*/char*p4;/*p4是指向字符变量的指针变量*/应该注意的是,一个指针变量只能指向同类型的变量,如P3只能指向浮点变量,不能 谭浩强C语言程序设计2001年5月1日时而指向一个浮点变量,时而又指向一个字符变量。10.2.2指针变量的引用指针变量同普通变量一样,使用之前不仅要定义说明,而且必须赋予具体的值。未经赋值的指针变量不能使用,否则将造成系统混乱,甚至死机。指针变量的赋值只能赋予地址,决不能赋予任何其它数据,否则将引起错误。在C语言中,变量的地址是由编译系统分配的,对用户完全透明,用户不知道变量的具体地址。两个有关的运算符:1)&:取地址运算符。2)*:指针运算符(或称“间接访问”运算符)。C语言中提供了地址运算符&来表示变量的地址。其一般形式为:&变量名;如&a表示变量a的地址,&b表示变量b的地址。变量本身必须预先说明。设有指向整型变量的指针变量p,如要把整型变量a的地址赋予p可以有以下两种方式:(1)指针变量初始化的方法inta;int*p=&a;(2)赋值语句的方法inta;int*p;p=&a;不允许把一个数赋予指针变量,故下面的赋值是错误的:int*p;p=1000;被赋值的指针变量前不能再加“*”说明符,如写为*p=&a也是错误的。假设:inti=200,x;int*ip;我们定义了两个整型变量i,x,还定义了一个指向整型数的指针变量ip。i,x中可存放整数,而ip中只能存放整型变量的地址。我们可以把i的地址赋给ip:ip=&i;此时指针变量ip指向整型变量i,假设变量i的地址为1800,这个赋值可形象理解为下图所示的联系。以后我们便可以通过指针变量ip间接访问变量i,例如:x=*ip;运算符*访问以ip为地址的存贮区域,而ip中存放的是变量i的地址,因此,*ip访问的 谭浩强C语言程序设计2001年5月1日是地址为1800的存贮区域(因为是整数,实际上是从1800开始的两个字节),它就是i所占用的存贮区域,所以上面的赋值表达式等价于x=i;另外,指针变量和一般变量一样,存放在它们之中的值是可以改变的,也就是说可以改变它们的指向,假设inti,j,*p1,*p2;i='a';j='b';p1=&i;p2=&j;则建立如下图所示的联系:这时赋值表达式:p2=p1就使p2与p1指向同一对象i,此时*p2就等价于i,而不是j,图所示:如果执行如下表达式:*p2=*p1;则表示把p1指向的内容赋给p2所指的区域,此时就变成图所示 谭浩强C语言程序设计2001年5月1日通过指针访问它所指向的一个变量是以间接访问的形式进行的,所以比直接访问一个变量要费时间,而且不直观,因为通过指针要访问哪一个变量,取决于指针的值(即指向),例如"*p2=*p1;"实际上就是"j=i;",前者不仅速度慢而且目的不明。但由于指针是变量,我们可以通过改变它们的指向,以间接访问不同的变量,这给程序员带来灵活性,也使程序代码编写得更为简洁和有效。指针变量可出现在表达式中,设intx,y,*px=&x;指针变量px指向整数x,则*px可出现在x能出现的任何地方。例如:y=*px+5;/*表示把x的内容加5并赋给y*/y=++*px;/*px的内容加上1之后赋给y,++*px相当于++(*px)*/y=*px++;/*相当于y=*px;px++*/【例10.1】main(){inta,b;int*pointer_1,*pointer_2;a=100;b=10;pointer_1=&a;pointer_2=&b;printf("%d,%d ",a,b);printf("%d,%d ",*pointer_1,*pointer_2);}对程序的说明:1)在开头处虽然定义了两个指针变量pointer_1和pointer_2,担它们并未指向任何一个整型变量。只是提供两个指针变量,规定它们可以指向整型变量。程序第5、6行的作用就是使pointer_1指向a,pointer_2指向b。 谭浩强C语言程序设计2001年5月1日2)最后一行的*pointer_1和*pointer_2就是变量a和b。最后两个printf函数作用是相同的。3)程序中有两处出现*pointer_1和*pointer_2,请区分它们的不同含义。4)程序第5、6行的“pointer_1=&a”和“pointer_2=&b”不能写成“*pointer_1=&a”和“*pointer_2=&b”。请对下面再的关于“&”和“*”的问题进行考虑:1)如果已经执行了“pointer_1=&a;”语句,则&*pointer_1是什么含义?2)*&a含义是什么?3)(pointer_1)++和pointer_1++的区别?【例10.2】输入a和b两个整数,按先大后小的顺序输出a和b。main(){int*p1,*p2,*p,a,b;scanf("%d,%d",&a,&b);p1=&a;p2=&b;if(apf2表示pf1处于高地址位置;pf1b){/*如果第一个数字大于第二个数字...*/pmax=&a;/*指针变量赋值*/pmin=&b;}/*指针变量赋值*/else{pmax=&b;/*指针变量赋值*/pmin=&a;}/*指针变量赋值*/if(c>*pmax)pmax=&c;/*判断并赋值*/if(c<*pmin)pmin=&c;/*判断并赋值*/printf("max=%d min=%d ",*pmax,*pmin);/*输出结果*/}10.3数组指针和指向数组的指针变量一个变量有一个地址,一个数组包含若干元素,每个数组元素都在内存中占用存储单元,它们都有相应的地址。所谓数组的指针是指数组的起始地址,数组元素的指针是数组元素的地址。10.3.1指向数组元素的指针一个数组是由连续的一块内存单元组成的。数组名就是这块连续内存单元的首地址。一个数组也是由各个数组元素(下标变量)组成的。每个数组元素按其类型不同占有几个连续的 谭浩强C语言程序设计2001年5月1日内存单元。一个数组元素的首地址也是指它所占有的几个内存单元的首地址。定义一个指向数组元素的指针变量的方法,与以前介绍的指针变量相同。例如:inta[10];/*定义a为包含10个整型数据的数组*/int*p;/*定义p为指向整型变量的指针*/应当注意,因为数组为int型,所以指针变量也应为指向int型的指针变量。下面是对指针变量赋值:p=&a[0];把a[0]元素的地址赋给指针变量p。也就是说,p指向a数组的第0号元素。C语言规定,数组名代表数组的首地址,也就是第0号元素的地址。因此,下面两个语句等价:p=&a[0];p=a;在定义指针变量时可以赋给初值:int*p=&a[0];它等效于:int*p;p=&a[0];当然定义时也可以写成:int*p=a;从图中我们可以看出有以下关系:p,a,&a[0]均指向同一单元,它们是数组a的首地址,也是0号元素a[0]的首地址。应该说明的是p是变量,而a,&a[0]都是常量。在编程时应予以注意。数组指针变量说明的一般形式为:类型说明符*指针变量名;其中类型说明符表示所指数组的类型。从一般形式可以看出指向数组的指针变量和指向普通变量的指针变量的说明是相同的。 谭浩强C语言程序设计2001年5月1日10.3.2通过指针引用数组元素C语言规定:如果指针变量p已指向数组中的一个元素,则p+1指向同一数组中的下一个元素。引入指针变量后,就可以用两种方法来访问数组元素了。如果p的初值为&a[0],则:1)p+i和a+i就是a[i]的地址,或者说它们指向a数组的第i个元素。2)*(p+i)或*(a+i)就是p+i或a+i所指向的数组元素,即a[i]。例如,*(p+5)或*(a+5)就是a[5]。3)指向数组的指针变量也可以带下标,如p[i]与*(p+i)等价。根据以上叙述,引用一个数组元素可以用:1)下标法,即用a[i]形式访问数组元素。在前面介绍数组时都是采用这种方法。2)指针法,即采用*(a+i)或*(p+i)形式,用间接访问的方法来访问数组元素,其中a是数组名,p是指向数组的指针变量,其处值p=a。【例10.9】输出数组中的全部元素。(下标法)main(){inta[10],i;for(i=0;i<10;i++)a[i]=i;for(i=0;i<5;i++)printf("a[%d]=%d ",i,a[i]);}【例10.10】输出数组中的全部元素。(通过数组名计算元素的地址,找出元素的值)main(){inta[10],i;for(i=0;i<10;i++)*(a+i)=i; 谭浩强C语言程序设计2001年5月1日for(i=0;i<10;i++)printf("a[%d]=%d ",i,*(a+i));}【例10.11】输出数组中的全部元素。(用指针变量指向元素)main(){inta[10],I,*p;p=a;for(i=0;i<10;i++)*(p+i)=i;for(i=0;i<10;i++)printf("a[%d]=%d ",i,*(p+i));}【例10.12】main(){inta[10],i,*p=a;for(i=0;i<10;){*p=i;printf("a[%d]=%d ",i++,*p++);}}几个注意的问题:1)指针变量可以实现本身的值的改变。如p++是合法的;而a++是错误的。因为a是数组名,它是数组的首地址,是常量。2)要注意指针变量的当前值。请看下面的程序。【例10.13】找出错误。main(){int*p,i,a[10];p=a;for(i=0;i<10;i++)*p++=i;for(i=0;i<10;i++)printf("a[%d]=%d ",i,*p++);}【例10.14】改正。main(){int*p,i,a[10]; 谭浩强C语言程序设计2001年5月1日p=a;for(i=0;i<10;i++)*p++=i;p=a;for(i=0;i<10;i++)printf("a[%d]=%d ",i,*p++);}3)从上例可以看出,虽然定义数组时指定它包含10个元素,但指针变量可以指到数组以后的内存单元,系统并不认为非法。4)*p++,由于++和*同优先级,结合方向自右而左,等价于*(p++)。5)*(p++)与*(++p)作用不同。若p的初值为a,则*(p++)等价a[0],*(++p)等价a[1]。6)(*p)++表示p所指向的元素值加1。7)如果p当前指向a数组中的第i个元素,则*(p--)相当于a[i--];*(++p)相当于a[++i];*(--p)相当于a[--i]。10.3.3数组名作函数参数数组名可以作函数的实参和形参。如:main(){intarray[10];…………f(array,10);…………}f(intarr[],intn);{…………}array为实参数组名,arr为形参数组名。在学习指针变量之后就更容易理解这个问题了。数组名就是数组的首地址,实参向形参传送数组名实际上就是传送数组的地址,形参得到该地址后也指向同一数组。这就好象同一件物品有两个彼此不同的名称一样。 谭浩强C语言程序设计2001年5月1日同样,指针变量的值也是地址,数组指针变量的值即为数组的首地址,当然也可作为函数的参数使用。【例10.15】floataver(float*pa);main(){floatsco[5],av,*sp;inti;sp=sco;printf(" input5scores: ");for(i=0;i<5;i++)scanf("%f",&sco[i]);av=aver(sp);printf("averagescoreis%5.2f",av);}floataver(float*pa){inti;floatav,s=0;for(i=0;i<5;i++)s=s+*pa++;av=s/5;returnav;}【例10.16】将数组a中的n个整数按相反顺序存放。算法为:将a[0]与a[n-1]对换,再a[1]与a[n-2]对换……,直到将a[(n-1/2)]与a[n-int((n-1)/2)]对换。今用循环处理此问题,设两个“位置指示变量”i和j,i的初值为0,j的初值为n-1。将a[i]与a[j]交换,然后使i的值加1,j的值减1,再将a[i]与a[j]交换,直到i=(n-1)/2为止,如图所示。 谭浩强C语言程序设计2001年5月1日程序如下:voidinv(intx[],intn)/*形参x是数组名*/{inttemp,i,j,m=(n-1)/2;for(i=0;i<=m;i++){j=n-1-i;temp=x[i];x[i]=x[j];x[j]=temp;}return;}main(){inti,a[10]={3,7,9,11,0,6,7,5,4,2};printf("Theoriginalarray: ");for(i=0;i<10;i++)printf("%d,",a[i]);printf(" ");inv(a,10);printf("Thearrayhasbenninverted: ");for(i=0;i<10;i++)printf("%d,",a[i]);printf(" ");}对此程序可以作一些改动。将函数inv中的形参x改成指针变量。【例10.17】对例10.16可以作一些改动。将函数inv中的形参x改成指针变量。程序如下:voidinv(int*x,intn)/*形参x为指针变量*/{int*p,temp,*i,*j,m=(n-1)/2;i=x;j=x+n-1;p=x+m;for(;i<=p;i++,j--){temp=*i;*i=*j;*j=temp;}return;}main(){inti,a[10]={3,7,9,11,0,6,7,5,4,2};printf("Theoriginalarray: "); 谭浩强C语言程序设计2001年5月1日for(i=0;i<10;i++)printf("%d,",a[i]);printf(" ");inv(a,10);printf("Thearrayhasbenninverted: ");for(i=0;i<10;i++)printf("%d,",a[i]);printf(" ");}运行情况与前一程序相同。【例10.18】从0个数中找出其中最大值和最小值。调用一个函数只能得到一个返回值,今用全局变量在函数之间“传递”数据。程序如下:intmax,min;/*全局变量*/voidmax_min_value(intarray[],intn){int*p,*array_end;array_end=array+n;max=min=*array;for(p=array+1;pmax)max=*p;elseif(*pmax)max=*p;elseif(*px[k])k=j;if(k!=i){t=x[i];x[i]=x[k];x[k]=t;}}}说明:函数sort用数组名作为形参,也可改为用指针变量,这时函数的首部可以改为:sort(int*x,intn)其他可一律不改。10.3.4指向多维数组的指针和指针变量本小节以二维数组为例介绍多维数组的指针变量。1.多维数组的地址设有整型二维数组a[3][4]如下:01234567891011它的定义为:inta[3][4]={{0,1,2,3},{4,5,6,7},{8,9,10,11}}设数组a的首地址为1000,各下标变量的首地址及其值如图所示。 谭浩强C语言程序设计2001年5月1日前面介绍过,C语言允许把一个二维数组分解为多个一维数组来处理。因此数组a可分解为三个一维数组,即a[0],a[1],a[2]。每一个一维数组又含有四个元素。例如a[0]数组,含有a[0][0],a[0][1],a[0][2],a[0][3]四个元素。数组及数组元素的地址表示如下:从二维数组的角度来看,a是二维数组名,a代表整个二维数组的首地址,也是二维数组0行的首地址,等于1000。a+1代表第一行的首地址,等于1008。如图:a[0]是第一个一维数组的数组名和首地址,因此也为1000。*(a+0)或*a是与a[0]等效的,它表示一维数组a[0]0号元素的首地址,也为1000。&a[0][0]是二维数组a的0行0列元素首地址,同样是1000。因此,a,a[0],*(a+0),*a,&a[0][0]是相等的。同理,a+1是二维数组1行的首地址,等于1008。a[1]是第二个一维数组的数组名和首地址,因此也为1008。&a[1][0]是二维数组a的1行0列元素地址,也是1008。因此a+1,a[1],*(a+1),&a[1][0]是等同的。由此可得出:a+i,a[i],*(a+i),&a[i][0]是等同的。此外,&a[i]和a[i]也是等同的。因为在二维数组中不能把&a[i]理解为元素a[i]的地址,不存在元素a[i]。C语言规定,它是一种地址计算方法,表示数组a第i行首地址。由此,我们得出:a[i],&a[i],*(a+i)和a+i也都是等同的。另外,a[0]也可以看成是a[0]+0,是一维数组a[0]的0号元素的首地址,而a[0]+1则是a[0]的1号元素首地址,由此可得出a[i]+j则是一维数组a[i]的j号元素首地址,它等于&a[i][j]。 谭浩强C语言程序设计2001年5月1日由a[i]=*(a+i)得a[i]+j=*(a+i)+j。由于*(a+i)+j是二维数组a的i行j列元素的首地址,所以,该元素的值等于*(*(a+i)+j)。【例10.22】main(){inta[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};printf("%d,",a);printf("%d,",*a);printf("%d,",a[0]);printf("%d,",&a[0]);printf("%d ",&a[0][0]);printf("%d,",a+1);printf("%d,",*(a+1));printf("%d,",a[1]);printf("%d,",&a[1]);printf("%d ",&a[1][0]);printf("%d,",a+2);printf("%d,",*(a+2));printf("%d,",a[2]);printf("%d,",&a[2]);printf("%d ",&a[2][0]);printf("%d,",a[1]+1);printf("%d ",*(a+1)+1);printf("%d,%d ",*(a[1]+1),*(*(a+1)+1));}2.指向多维数组的指针变量把二维数组a分解为一维数组a[0],a[1],a[2]之后,设p为指向二维数组的指针变量。可定义为:int(*p)[4]它表示p是一个指针变量,它指向包含4个元素的一维数组。若指向第一个一维数组a[0],其值等于a,a[0],或&a[0][0]等。而p+i则指向一维数组a[i]。从前面的分析可得出*(p+i)+j是二维数组i行j列的元素的地址,而*(*(p+i)+j)则是i行j列元素的值。二维数组指针变量说明的一般形式为: 谭浩强C语言程序设计2001年5月1日类型说明符(*指针变量名)[长度]其中“类型说明符”为所指数组的数据类型。“*”表示其后的变量是指针类型。“长度”表示二维数组分解为多个一维数组时,一维数组的长度,也就是二维数组的列数。应注意“(*指针变量名)”两边的括号不可少,如缺少括号则表示是指针数组(本章后面介绍),意义就完全不同了。【例10.23】main(){inta[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};int(*p)[4];inti,j;p=a;for(i=0;i<3;i++){for(j=0;j<4;j++)printf("%2d",*(*(p+i)+j));printf(" ");}}10.4字符串的指针指向字符串的针指变量10.4.1字符串的表示形式在C语言中,可以用两种方法访问一个字符串。1)用字符数组存放一个字符串,然后输出该字符串。【例10.24】main(){charstring[]=”IloveChina!”;printf("%s ",string);}说明:和前面介绍的数组属性一样,string是数组名,它代表字符数组的首地址。 谭浩强C语言程序设计2001年5月1日2)用字符串指针指向一个字符串。【例10.25】main(){char*string=”IloveChina!”;printf("%s ",string);}字符串指针变量的定义说明与指向字符变量的指针变量说明是相同的。只能按对指针变量的赋值不同来区别。对指向字符变量的指针变量应赋予该字符变量的地址。如:charc,*p=&c;表示p是一个指向字符变量c的指针变量。而:char*s="CLanguage";则表示s是一个指向字符串的指针变量。把字符串的首地址赋予s。上例中,首先定义string是一个字符指针变量,然后把字符串的首地址赋予string(应写出整个字符串,以便编译系统把该串装入连续的一块内存单元),并把首地址送入string。程序中的:char*ps="CLanguage";等效于:char*ps;ps="CLanguage";【例10.26】输出字符串中n个字符后的所有字符。main(){char*ps="thisisabook";intn=10;ps=ps+n; 谭浩强C语言程序设计2001年5月1日printf("%s ",ps);}运行结果为:book在程序中对ps初始化时,即把字符串首地址赋予ps,当ps=ps+10之后,ps指向字符“b”,因此输出为"book"。【例10.27】在输入的字符串中查找有无‘k’字符。main(){charst[20],*ps;inti;printf("inputastring: ");ps=st;scanf("%s",ps);for(i=0;ps[i]!='';i++)if(ps[i]=='k'){printf("thereisa'k'inthestring ");break;}if(ps[i]=='')printf("Thereisno'k'inthestring ");}【例10.28】本例是将指针变量指向一个格式字符串,用在printf函数中,用于输出二维数组的各种地址表示的值。但在printf语句中用指针变量PF代替了格式串。这也是程序中常用的方法。main(){staticinta[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};char*PF;PF="%d,%d,%d,%d,%d ";printf(PF,a,*a,a[0],&a[0],&a[0][0]);printf(PF,a+1,*(a+1),a[1],&a[1],&a[1][0]);printf(PF,a+2,*(a+2),a[2],&a[2],&a[2][0]);printf("%d,%d ",a[1]+1,*(a+1)+1);printf("%d,%d ",*(a[1]+1),*(*(a+1)+1));}【例10.29】本例是把字符串指针作为函数参数的使用。要求把一个字符串的内容复制到另一个字符串中,并且不能使用strcpy函数。函数cprstr的形参为两个字符指针变量。pss指向源字符串,pds指向目标字符串。注意表达式:(*pds=*pss)!=`'的用法。cpystr(char*pss,char*pds){while((*pds=*pss)!=''){ 谭浩强C语言程序设计2001年5月1日pds++;pss++;}}main(){char*pa="CHINA",b[10],*pb;pb=b;cpystr(pa,pb);printf("stringa=%s stringb=%s ",pa,pb);}在本例中,程序完成了两项工作:一是把pss指向的源字符串复制到pds所指向的目标字符串中,二是判断所复制的字符是否为`',若是则表明源字符串结束,不再循环。否则,pds和pss都加1,指向下一字符。在主函数中,以指针变量pa,pb为实参,分别取得确定值后调用cprstr函数。由于采用的指针变量pa和pss,pb和pds均指向同一字符串,因此在主函数和cprstr函数中均可使用这些字符串。也可以把cprstr函数简化为以下形式:cprstr(char*pss,char*pds){while((*pds++=*pss++)!=`');}即把指针的移动和赋值合并在一个语句中。进一步分析还可发现`'的ASCⅡ码为0,对于while语句只看表达式的值为非0就循环,为0则结束循环,因此也可省去“!=`'”这一判断部分,而写为以下形式:cprstr(char*pss,char*pds){while(*pdss++=*pss++);}表达式的意义可解释为,源字符向目标字符赋值,移动指针,若所赋值为非0则循环,否则结束循环。这样使程序更加简洁。【例10.30】简化后的程序如下所示。cpystr(char*pss,char*pds){while(*pds++=*pss++);}main(){char*pa="CHINA",b[10],*pb;pb=b;cpystr(pa,pb);printf("stringa=%s stringb=%s ",pa,pb);}10.4.2使用字符串指针变量与字符数组的区别用字符数组和字符指针变量都可实现字符串的存储和运算。但是两者是有区别的。在使用时应注意以下几个问题:1.字符串指针变量本身是一个变量,用于存放字符串的首地址。而字符串本身是存放在以该首地址为首的一块连续的内存空间中并以‘’作为串的结束。字符数组是 谭浩强C语言程序设计2001年5月1日由于若干个数组元素组成的,它可用来存放整个字符串。2.对字符串指针方式char*ps="CLanguage";可以写为:char*ps;ps="CLanguage";而对数组方式:staticcharst[]={"CLanguage"};不能写为:charst[20];st={"CLanguage"};而只能对字符数组的各元素逐个赋值。从以上几点可以看出字符串指针变量与字符数组在使用时的区别,同时也可看出使用指针变量更加方便。前面说过,当一个指针变量在未取得确定地址前使用是危险的,容易引起错误。但是对指针变量直接赋值是可以的。因为C系统对指针变量赋值时要给以确定的地址。因此,char*ps="CLangage";或者char*ps;ps="CLanguage";都是合法的。10.5函数指针变量在C语言中,一个函数总是占用一段连续的内存区,而函数名就是该函数所占内存区的首地址。我们可以把函数的这个首地址(或称入口地址)赋予一个指针变量,使该指针变量指向该函数。然后通过指针变量就可以找到并调用这个函数。我们把这种指向函数的指针变量称为“函数指针变量”。函数指针变量定义的一般形式为:类型说明符(*指针变量名)();其中“类型说明符”表示被指函数的返回值的类型。“(*指针变量名)”表示“*”后面的变量是定义的指针变量。最后的空括号表示指针变量所指的是一个函数。例如:int(*pf)();表示pf是一个指向函数入口的指针变量,该函数的返回值(函数值)是整型。【例10.31】本例用来说明用指针形式实现对函数调用的方法。intmax(inta,intb){if(a>b)returna;elsereturnb;}main(){intmax(inta,intb);int(*pmax)(); 谭浩强C语言程序设计2001年5月1日intx,y,z;pmax=max;printf("inputtwonumbers: ");scanf("%d%d",&x,&y);z=(*pmax)(x,y);printf("maxmum=%d",z);}从上述程序可以看出用,函数指针变量形式调用函数的步骤如下:1)先定义函数指针变量,如后一程序中第9行int(*pmax)();定义pmax为函数指针变量。2)把被调函数的入口地址(函数名)赋予该函数指针变量,如程序中第11行pmax=max;3)用函数指针变量形式调用函数,如程序第14行z=(*pmax)(x,y);4)调用函数的一般形式为:(*指针变量名)(实参表)使用函数指针变量还应注意以下两点:a)函数指针变量不能进行算术运算,这是与数组指针变量不同的。数组指针变量加减一个整数可使指针移动指向后面或前面的数组元素,而函数指针的移动是毫无意义的。b)函数调用中"(*指针变量名)"的两边的括号不可少,其中的*不应该理解为求值运算,在此处它只是一种表示符号。10.6指针型函数前面我们介绍过,所谓函数类型是指函数返回值的类型。在C语言中允许一个函数的返回值是一个指针(即地址),这种返回指针值的函数称为指针型函数。定义指针型函数的一般形式为:类型说明符*函数名(形参表){……/*函数体*/}其中函数名之前加了“*”号表明这是一个指针型函数,即返回值是一个指针。类型说明符表示了返回的指针值所指向的数据类型。如:int*ap(intx,inty){....../*函数体*/}表示ap是一个返回指针值的指针型函数,它返回的指针指向一个整型变量。【例10.32】本程序是通过指针函数,输入一个1~7之间的整数,输出对应的星期名。main(){inti;char*day_name(intn);printf("inputDayNo: "); 谭浩强C语言程序设计2001年5月1日scanf("%d",&i);if(i<0)exit(1);printf("DayNo:%2d-->%s ",i,day_name(i));}char*day_name(intn){staticchar*name[]={"Illegalday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"};return((n<1||n>7)?name[0]:name[n]);}本例中定义了一个指针型函数day_name,它的返回值指向一个字符串。该函数中定义了一个静态指针数组name。name数组初始化赋值为八个字符串,分别表示各个星期名及出错提示。形参n表示与星期名所对应的整数。在主函数中,把输入的整数i作为实参,在printf语句中调用day_name函数并把i值传送给形参n。day_name函数中的return语句包含一个条件表达式,n值若大于7或小于1则把name[0]指针返回主函数输出出错提示字符串“Illegalday”。否则返回主函数输出对应的星期名。主函数中的第7行是个条件语句,其语义是,如输入为负数(i<0)则中止程序运行退出程序。exit是一个库函数,exit(1)表示发生错误后退出程序,exit(0)表示正常退出。应该特别注意的是函数指针变量和指针型函数这两者在写法和意义上的区别。如int(*p)()和int*p()是两个完全不同的量。int(*p)()是一个变量说明,说明p是一个指向函数入口的指针变量,该函数的返回值是整型量,(*p)的两边的括号不能少。int*p()则不是变量说明而是函数说明,说明p是一个指针型函数,其返回值是一个指向整型量的指针,*p两边没有括号。作为函数说明,在括号内最好写入形式参数,这样便于与变量说明区别。对于指针型函数定义,int*p()只是函数头部分,一般还应该有函数体部分。10.7指针数组和指向指针的指针10.7.1指针数组的概念一个数组的元素值为指针则是指针数组。指针数组是一组有序的指针的集合。指针数组的所有元素都必须是具有相同存储类型和指向相同数据类型的指针变量。指针数组说明的一般形式为:类型说明符*数组名[数组长度]其中类型说明符为指针值所指向的变量的类型。 谭浩强C语言程序设计2001年5月1日例如:int*pa[3]表示pa是一个指针数组,它有三个数组元素,每个元素值都是一个指针,指向整型变量。【例10.33】通常可用一个指针数组来指向一个二维数组。指针数组中的每个元素被赋予二维数组每一行的首地址,因此也可理解为指向一个一维数组。main(){inta[3][3]={1,2,3,4,5,6,7,8,9};int*pa[3]={a[0],a[1],a[2]};int*p=a[0];inti;for(i=0;i<3;i++)printf("%d,%d,%d ",a[i][2-i],*a[i],*(*(a+i)+i));for(i=0;i<3;i++)printf("%d,%d,%d ",*pa[i],p[i],*(p+i));}本例程序中,pa是一个指针数组,三个元素分别指向二维数组a的各行。然后用循环语句输出指定的数组元素。其中*a[i]表示i行0列元素值;*(*(a+i)+i)表示i行i列的元素值;*pa[i]表示i行0列元素值;由于p与a[0]相同,故p[i]表示0行i列的值;*(p+i)表示0行i列的值。读者可仔细领会元素值的各种不同的表示方法。应该注意指针数组和二维数组指针变量的区别。这两者虽然都可用来表示二维数组,但是其表示方法和意义是不同的。二维数组指针变量是单个的变量,其一般形式中"(*指针变量名)"两边的括号不可少。而指针数组类型表示的是多个指针(一组有序指针)在一般形式中"*指针数组名"两边不能有括号。例如:int(*p)[3];表示一个指向二维数组的指针变量。该二维数组的列数为3或分解为一维数组的长度为3。int*p[3]表示p是一个指针数组,有三个下标变量p[0],p[1],p[2]均为指针变量。指针数组也常用来表示一组字符串,这时指针数组的每个元素被赋予一个字符串的首地址。指向字符串的指针数组的初始化更为简单。例如在例10.32中即采用指针数组来表示一组字符串。其初始化赋值为:char*name[]={"Illagalday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"};完成这个初始化赋值之后,name[0]即指向字符串"Illegalday",name[1]指向 谭浩强C语言程序设计2001年5月1日"Monday"......。指针数组也可以用作函数参数。【例10.34】指针数组作指针型函数的参数。在本例主函数中,定义了一个指针数组name,并对name作了初始化赋值。其每个元素都指向一个字符串。然后又以name作为实参调用指针型函数day_name,在调用时把数组名name赋予形参变量name,输入的整数i作为第二个实参赋予形参n。在day_name函数中定义了两个指针变量pp1和pp2,pp1被赋予name[0]的值(即*name),pp2被赋予name[n]的值即*(name+n)。由条件表达式决定返回pp1或pp2指针给主函数中的指针变量ps。最后输出i和ps的值。main(){staticchar*name[]={"Illegalday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"};char*ps;inti;char*day_name(char*name[],intn);printf("inputDayNo: ");scanf("%d",&i);if(i<0)exit(1);ps=day_name(name,i);printf("DayNo:%2d-->%s ",i,ps);}char*day_name(char*name[],intn){char*pp1,*pp2;pp1=*name;pp2=*(name+n);return((n<1||n>7)?pp1:pp2);}【例10.35】输入5个国名并按字母顺序排列后输出。现编程如下:#include"string.h"main(){voidsort(char*name[],intn);voidprint(char*name[],intn);staticchar*name[]={"CHINA","AMERICA","AUSTRALIA","FRANCE","GERMAN"};intn=5;sort(name,n); 谭浩强C语言程序设计2001年5月1日print(name,n);}voidsort(char*name[],intn){char*pt;inti,j,k;for(i=0;i0)k=j;if(k!=i){pt=name[i];name[i]=name[k];name[k]=pt;}}}voidprint(char*name[],intn){inti;for(i=0;i可执行文件名参数参数……;但是应该特别注意的是,main的两个形参和命令行中的参数在位置上不是一一对应的。因为,main的形参只有二个,而命令行中的参数个数原则上未加限制。argc参数表示了命令行中参数的个数(注意:文件名本身也算一个参数),argc的值是在输入命令行时由系统按实际参数的个数自动赋予的。例如有命令行为:C:>E24BASICfoxproFORTRAN由于文件名E24本身也算一个参数,所以共有4个参数,因此argc取得的值为4。argv参数是字符串指针数组,其各元素值为命令行中各字符串(参数均按字符串处理)的首地址。指 谭浩强C语言程序设计2001年5月1日针数组的长度即为参数个数。数组元素初值由系统自动赋予。其表示如图所示:【例10.38】main(intargc,char*argv){while(argc-->1)printf("%s ",*++argv);}本例是显示命令行中输入的参数。如果上例的可执行文件名为e24.exe,存放在A驱动器的盘内。因此输入的命令行为:C:>a:e24BASICfoxproFORTRAN则运行结果为:BASICfoxproFORTRAN该行共有4个参数,执行main时,argc的初值即为4。argv的4个元素分为4个字符串的首地址。执行while语句,每循环一次argv值减1,当argv等于1时停止循环,共循环三次,因此共可输出三个参数。在printf函数中,由于打印项*++argv是先加1再打印,故第一次打印的是argv[1]所指的字符串BASIC。第二、三次循环分别打印后二个字符串。而参数e24是文件名,不必输出。10.8有关指针的数据类型和指针运算的小结10.8.1有关指针的数据类型的小结定义含义inti;定义整型变量iint*pp为指向整型数据的指针变量inta[n];定义整型数组a,它有n个元素int*p[n];定义指针数组p,它由n个指向整型数据的指针元素组成int(*p)[n];p为指向含n个元素的一维数组的指针变量intf();f为带回整型函数值的函数int*p();p为带回一个指针的函数,该指针指向整型数据int(*p)();p为指向函数的指针,该函数返回一个整型值int**p;P是一个指针变量,它指向一个指向整型数据的指针变量 谭浩强C语言程序设计2001年5月1日10.8.2指针运算的小结现把全部指针运算列出如下:1)指针变量加(减)一个整数:例如:p++、p--、p+i、p-i、p+=i、p-=i一个指针变量加(减)一个整数并不是简单地将原值加(减)一个整数,而是将该指针变量的原值(是一个地址)和它指向的变量所占用的内存单元字节数加(减)。2)指针变量赋值:将一个变量的地址赋给一个指针变量。p=&a;(将变量a的地址赋给p)p=array;(将数组array的首地址赋给p)p=&array[i];(将数组array第i个元素的地址赋给p)p=max;(max为已定义的函数,将max的入口地址赋给p)p1=p2;(p1和p2都是指针变量,将p2的值赋给p1)注意:不能如下:p=1000;3)指针变量可以有空值,即该指针变量不指向任何变量:p=NULL;4)两个指针变量可以相减:如果两个指针变量指向同一个数组的元素,则两个指针变量值之差是两个指针之间的元素个数。5)两个指针变量比较:如果两个指针变量指向同一个数组的元素,则两个指针变量可以进行比较。指向前面的元素的指针变量“小于”指向后面的元素的指针变量。10.8.3void指针类型ANSI新标准增加了一种“void”指针类型,即可以定义一个指针变量,但不指定它是指向哪一种类型数据。 11结构体与共用体11.1定义一个结构的一般形式在实际问题中,一组数据往往具有不同的数据类型。例如,在学生登记表中,姓名应为字符型;学号可为整型或字符型;年龄应为整型;性别应为字符型;成绩可为整型或实型。显然不能用一个数组来存放这一组数据。因为数组中各元素的类型和长度都必须一致,以便于编译系统处理。为了解决这个问题,C语言中给出了另一种构造数据类型——“结构(structure)”或叫“结构体”。它相当于其它高级语言中的记录。“结构”是一种构造类型,它是由若干“成员”组成的。每一个成员可以是一个基本数据类型或者又是一个构造类型。结构既是一种“构造”而成的数据类型,那么在说明和使用之前必须先定义它,也就是构造它。如同在说明和调用函数之前要先定义函数一样。定义一个结构的一般形式为:struct结构名{成员表列};成员表列由若干个成员组成,每个成员都是该结构的一个组成部分。对每个成员也必须作类型说明,其形式为:类型说明符成员名;成员名的命名应符合标识符的书写规定。例如:structstu{intnum;charname[20];charsex;floatscore;};在这个结构定义中,结构名为stu,该结构由4个成员组成。第一个成员为num,整型变量;第二个成员为name,字符数组;第三个成员为sex,字符变量;第四个成员为score,实型变量。应注意在括号后的分号是不可少的。结构定义之后,即可进行变量说明。凡说明为结构stu的变量都由上述4个成员组成。由此可见,结构是一种复杂的数据类型,是数目固定,类型不同的若干有序变量的集合。11.2结构类型变量的说明说明结构变量有以下三种方法。以上面定义的stu为例来加以说明。1.先定义结构,再说明结构变量。如:structstu{intnum; charname[20];charsex;floatscore;};structstuboy1,boy2;说明了两个变量boy1和boy2为stu结构类型。也可以用宏定义使一个符号常量来表示一个结构类型。例如:#defineSTUstructstuSTU{intnum;charname[20];charsex;floatscore;};STUboy1,boy2;2.在定义结构类型的同时说明结构变量。例如:structstu{intnum;charname[20];charsex;floatscore;}boy1,boy2;这种形式的说明的一般形式为:struct结构名{成员表列}变量名表列;3.直接说明结构变量。例如:struct{intnum;charname[20];charsex;floatscore;}boy1,boy2;这种形式的说明的一般形式为:struct{成员表列 }变量名表列;第三种方法与第二种方法的区别在于第三种方法中省去了结构名,而直接给出结构变量。三种方法中说明的boy1,boy2变量都具有下图所示的结构。说明了boy1,boy2变量为stu类型后,即可向这两个变量中的各个成员赋值。在上述stu结构定义中,所有的成员都是基本数据类型或数组类型。成员也可以又是一个结构,即构成了嵌套的结构。例如,下图给出了另一个数据结构。按图可给出以下结构定义:structdate{intmonth;intday;intyear;};struct{intnum;charname[20];charsex;structdatebirthday;floatscore;}boy1,boy2;首先定义一个结构date,由month(月)、day(日)、year(年)三个成员组成。在定义并说明变量boy1和boy2时,其中的成员birthday被说明为data结构类型。成员名可与程序中其它变量同名,互不干扰。11.3结构变量成员的表示方法在程序中使用结构变量时,往往不把它作为一个整体来使用。在ANSIC中除了允许具有相同类型的结构变量相互赋值以外,一般对结构变量的使用,包括赋值、输入、输出、运算等都是通过结构变量的成员来实现的。表示结构变量成员的一般形式是:结构变量名.成员名例如:boy1.num即第一个人的学号boy2.sex即第二个人的性别如果成员本身又是一个结构则必须逐级找到最低级的成员才能使用。例如:boy1.birthday.month 即第一个人出生的月份成员可以在程序中单独使用,与普通变量完全相同。11.4结构变量的赋值结构变量的赋值就是给各成员赋值。可用输入语句或赋值语句来完成。【例11.1】给结构变量赋值并输出其值。main(){structstu{intnum;char*name;charsex;floatscore;}boy1,boy2;boy1.num=102;boy1.name="Zhangping";printf("inputsexandscore ");scanf("%c%f",&boy1.sex,&boy1.score);boy2=boy1;printf("Number=%d Name=%s ",boy2.num,boy2.name);printf("Sex=%c Score=%f ",boy2.sex,boy2.score);}本程序中用赋值语句给num和name两个成员赋值,name是一个字符串指针变量。用scanf函数动态地输入sex和score成员值,然后把boy1的所有成员的值整体赋予boy2。最后分别输出boy2的各个成员值。本例表示了结构变量的赋值、输入和输出的方法。11.5结构变量的初始化和其他类型变量一样,对结构变量可以在定义时进行初始化赋值。【例11.2】对结构变量初始化。main(){structstu/*定义结构*/{intnum;char*name;charsex;floatscore;}boy2,boy1={102,"Zhangping",'M',78.5};boy2=boy1; printf("Number=%d Name=%s ",boy2.num,boy2.name);printf("Sex=%c Score=%f ",boy2.sex,boy2.score);}本例中,boy2,boy1均被定义为外部结构变量,并对boy1作了初始化赋值。在main函数中,把boy1的值整体赋予boy2,然后用两个printf语句输出boy2各成员的值。11.6结构数组的定义数组的元素也可以是结构类型的。因此可以构成结构型数组。结构数组的每一个元素都是具有相同结构类型的下标结构变量。在实际应用中,经常用结构数组来表示具有相同数据结构的一个群体。如一个班的学生档案,一个车间职工的工资表等。方法和结构变量相似,只需说明它为数组类型即可。例如:structstu{intnum;char*name;charsex;floatscore;}boy[5];定义了一个结构数组boy,共有5个元素,boy[0]~boy[4]。每个数组元素都具有structstu的结构形式。对结构数组可以作初始化赋值。例如:structstu{intnum;char*name;charsex;floatscore;}boy[5]={{101,"Liping","M",45},{102,"Zhangping","M",62.5},{103,"Hefang","F",92.5},{104,"Chengling","F",87},{105,"Wangming","M",58};}当对全部元素作初始化赋值时,也可不给出数组长度。【例11.3】计算学生的平均成绩和不及格的人数。structstu{intnum;char*name; charsex;floatscore;}boy[5]={{101,"Liping",'M',45},{102,"Zhangping",'M',62.5},{103,"Hefang",'F',92.5},{104,"Chengling",'F',87},{105,"Wangming",'M',58},};main(){inti,c=0;floatave,s=0;for(i=0;i<5;i++){s+=boy[i].score;if(boy[i].score<60)c+=1;}printf("s=%f ",s);ave=s/5;printf("average=%f count=%d ",ave,c);}本例程序中定义了一个外部结构数组boy,共5个元素,并作了初始化赋值。在main函数中用for语句逐个累加各元素的score成员值存于s之中,如score的值小于60(不及格)即计数器C加1,循环完毕后计算平均成绩,并输出全班总分,平均分及不及格人数。【例11.4】建立同学通讯录#include"stdio.h"#defineNUM3structmem{charname[20];charphone[10];};main(){structmemman[NUM];inti;for(i=0;i成员名例如:(*pstu).num或者: pstu->num应该注意(*pstu)两侧的括号不可少,因为成员符“.”的优先级高于“*”。如去掉括号写作*pstu.num则等效于*(pstu.num),这样,意义就完全不对了。下面通过例子来说明结构指针变量的具体说明和使用方法。【例11.5】structstu{intnum;char*name;charsex;floatscore;}boy1={102,"Zhangping",'M',78.5},*pstu;main(){pstu=&boy1;printf("Number=%d Name=%s ",boy1.num,boy1.name);printf("Sex=%c Score=%f ",boy1.sex,boy1.score);printf("Number=%d Name=%s ",(*pstu).num,(*pstu).name);printf("Sex=%c Score=%f ",(*pstu).sex,(*pstu).score);printf("Number=%d Name=%s ",pstu->num,pstu->name);printf("Sex=%c Score=%f ",pstu->sex,pstu->score);}本例程序定义了一个结构stu,定义了stu类型结构变量boy1并作了初始化赋值,还定义了一个指向stu类型结构的指针变量pstu。在main函数中,pstu被赋予boy1的地址,因此pstu指向boy1。然后在printf语句内用三种形式输出boy1的各个成员值。从运行结果可以看出:结构变量.成员名(*结构指针变量).成员名结构指针变量->成员名这三种用于表示结构成员的形式是完全等效的。11.7.2指向结构数组的指针指针变量可以指向一个结构数组,这时结构指针变量的值是整个结构数组的首地址。结构指针变量也可指向结构数组的一个元素,这时结构指针变量的值是该结构数组元素的首地址。设ps为指向结构数组的指针变量,则ps也指向该结构数组的0号元素,ps+1指向1号元素,ps+i则指向i号元素。这与普通数组的情况是一致的。【例11.6】用指针变量输出结构数组。structstu{intnum; char*name;charsex;floatscore;}boy[5]={{101,"Zhouping",'M',45},{102,"Zhangping",'M',62.5},{103,"Lioufang",'F',92.5},{104,"Chengling",'F',87},{105,"Wangming",'M',58},};main(){structstu*ps;printf("NotNametttSextScoret ");for(ps=boy;psnum,ps->name,ps->sex,ps->score);}在程序中,定义了stu结构类型的外部数组boy并作了初始化赋值。在main函数内定义ps为指向stu类型的指针。在循环语句for的表达式1中,ps被赋予boy的首地址,然后循环5次,输出boy数组中各成员值。应该注意的是,一个结构指针变量虽然可以用来访问结构变量或结构数组元素的成员,但是,不能使它指向一个成员。也就是说不允许取一个成员的地址来赋予它。因此,下面的赋值是错误的。ps=&boy[1].sex;而只能是:ps=boy;(赋予数组首地址)或者是:ps=&boy[0];(赋予0号元素首地址)11.7.3结构指针变量作函数参数在ANSIC标准中允许用结构变量作函数参数进行整体传送。但是这种传送要将全部成员逐个传送,特别是成员为数组时将会使传送的时间和空间开销很大,严重地降低了程序的效率。因此最好的办法就是使用指针,即用指针变量作函数参数进行传送。这时由实参传向形参的只是地址,从而减少了时间和空间的开销。【例11.7】计算一组学生的平均成绩和不及格人数。用结构指针变量作函数参数编程。structstu{intnum;char*name;charsex;floatscore;}boy[5]={ {101,"Liping",'M',45},{102,"Zhangping",'M',62.5},{103,"Hefang",'F',92.5},{104,"Chengling",'F',87},{105,"Wangming",'M',58},};main(){structstu*ps;voidave(structstu*ps);ps=boy;ave(ps);}voidave(structstu*ps){intc=0,i;floatave,s=0;for(i=0;i<5;i++,ps++){s+=ps->score;if(ps->score<60)c+=1;}printf("s=%f ",s);ave=s/5;printf("average=%f count=%d ",ave,c);}本程序中定义了函数ave,其形参为结构指针变量ps。boy被定义为外部结构数组,因此在整个源程序中有效。在main函数中定义说明了结构指针变量ps,并把boy的首地址赋予它,使ps指向boy数组。然后以ps作实参调用函数ave。在函数ave中完成计算平均成绩和统计不及格人数的工作并输出结果。由于本程序全部采用指针变量作运算和处理,故速度更快,程序效率更高。11.8动态存储分配在数组一章中,曾介绍过数组的长度是预先定义好的,在整个程序中固定不变。C语言中不允许动态数组类型。例如:intn;scanf("%d",&n);inta[n];用变量表示长度,想对数组的大小作动态说明,这是错误的。但是在实际的编程中,往往会发生这种情况,即所需的内存空间取决于实际输入的数据,而无法预先确定。对于这种 问题,用数组的办法很难解决。为了解决上述问题,C语言提供了一些内存管理函数,这些内存管理函数可以按需要动态地分配内存空间,也可把不再使用的空间回收待用,为有效地利用内存资源提供了手段。常用的内存管理函数有以下三个:1.分配内存空间函数malloc调用形式:(类型说明符*)malloc(size)功能:在内存的动态存储区中分配一块长度为"size"字节的连续区域。函数的返回值为该区域的首地址。“类型说明符”表示把该区域用于何种数据类型。(类型说明符*)表示把返回值强制转换为该类型指针。“size”是一个无符号数。例如:pc=(char*)malloc(100);表示分配100个字节的内存空间,并强制转换为字符数组类型,函数的返回值为指向该字符数组的指针,把该指针赋予指针变量pc。2.分配内存空间函数calloccalloc也用于分配内存空间。调用形式:(类型说明符*)calloc(n,size)功能:在内存动态存储区中分配n块长度为“size”字节的连续区域。函数的返回值为该区域的首地址。(类型说明符*)用于强制类型转换。calloc函数与malloc函数的区别仅在于一次可以分配n块区域。例如:ps=(struetstu*)calloc(2,sizeof(structstu));其中的sizeof(structstu)是求stu的结构长度。因此该语句的意思是:按stu的长度分配2块连续区域,强制转换为stu类型,并把其首地址赋予指针变量ps。2.释放内存空间函数free调用形式:free(void*ptr);功能:释放ptr所指向的一块内存空间,ptr是一个任意类型的指针变量,它指向被释放区域的首地址。被释放区应是由malloc或calloc函数所分配的区域。【例11.8】分配一块区域,输入一个学生数据。main(){structstu{intnum;char*name;charsex;floatscore;}*ps;ps=(structstu*)malloc(sizeof(structstu)); ps->num=102;ps->name="Zhangping";ps->sex='M';ps->score=62.5;printf("Number=%d Name=%s ",ps->num,ps->name);printf("Sex=%c Score=%f ",ps->sex,ps->score);free(ps);}本例中,定义了结构stu,定义了stu类型指针变量ps。然后分配一块stu大内存区,并把首地址赋予ps,使ps指向该区域。再以ps为指向结构的指针变量对各成员赋值,并用printf输出各成员值。最后用free函数释放ps指向的内存空间。整个程序包含了申请内存空间、使用内存空间、释放内存空间三个步骤,实现存储空间的动态分配。11.9链表的概念在例7.8中采用了动态分配的办法为一个结构分配内存空间。每一次分配一块空间可用来存放一个学生的数据,我们可称之为一个结点。有多少个学生就应该申请分配多少块内存空间,也就是说要建立多少个结点。当然用结构数组也可以完成上述工作,但如果预先不能准确把握学生人数,也就无法确定数组大小。而且当学生留级、退学之后也不能把该元素占用的空间从数组中释放出来。用动态存储的方法可以很好地解决这些问题。有一个学生就分配一个结点,无须预先确定学生的准确人数,某学生退学,可删去该结点,并释放该结点占用的存储空间。从而节约了宝贵的内存资源。另一方面,用数组的方法必须占用一块连续的内存区域。而使用动态分配时,每个结点之间可以是不连续的(结点内是连续的)。结点之间的联系可以用指针实现。即在结点结构中定义一个成员项用来存放下一结点的首地址,这个用于存放地址的成员,常把它称为指针域。可在第一个结点的指针域内存入第二个结点的首地址,在第二个结点的指针域内又存放第三个结点的首地址,如此串连下去直到最后一个结点。最后一个结点因无后续结点连接,其指针域可赋为0。这样一种连接方式,在数据结构中称为“链表”。下图为最一简单链表的示意图。图中,第0个结点称为头结点,它存放有第一个结点的首地址,它没有数据,只是一个指针变量。以下的每个结点都分为两个域,一个是数据域,存放各种实际的数据,如学号num,姓名name,性别sex和成绩score等。另一个域为指针域,存放下一结点的首地址。链表中的每一个结点都是同一种结构类型。例如,一个存放学生学号和成绩的结点应为以下结构:structstu{intnum;intscore; structstu*next;}前两个成员项组成数据域,后一个成员项next构成指针域,它是一个指向stu类型结构的指针变量。链表的基本操作对链表的主要操作有以下几种:1.建立链表;2.结构的查找与输出;3.插入一个结点;4.删除一个结点;下面通过例题来说明这些操作。【例11.9】建立一个三个结点的链表,存放学生数据。为简单起见,我们假定学生数据结构中只有学号和年龄两项。可编写一个建立链表的函数creat。程序如下:#defineNULL0#defineTYPEstructstu#defineLENsizeof(structstu)structstu{intnum;intage;structstu*next;};TYPE*creat(intn){structstu*head,*pf,*pb;inti;for(i=0;inum,&pb->age);if(i==0)pf=head=pb;elsepf->next=pb;pb->next=NULL;pf=pb;}return(head);}在函数外首先用宏定义对三个符号常量作了定义。这里用TYPE表示structstu,用LEN表示sizeof(structstu)主要的目的是为了在以下程序内减少书写并使阅读更加方便。结构stu定义为外部类型,程序中的各个函数均可使用该定义。creat函数用于建立一个有n个结点的链表,它是一个指针函数,它返回的指针指向stu结构。在creat函数内定义了三个stu结构的指针变量。head为头指针,pf为指向两相邻结点的前一结点的指针变量。pb为后一结点的指针变量。 11.10枚举类型在实际问题中,有些变量的取值被限定在一个有限的范围内。例如,一个星期内只有七天,一年只有十二个月,一个班每周有六门课程等等。如果把这些量说明为整型,字符型或其它类型显然是不妥当的。为此,C语言提供了一种称为“枚举”的类型。在“枚举”类型的定义中列举出所有可能的取值,被说明为该“枚举”类型的变量取值不能超过定义的范围。应该说明的是,枚举类型是一种基本数据类型,而不是一种构造类型,因为它不能再分解为任何基本类型。11.10.1枚举类型的定义和枚举变量的说明1.枚举的定义枚举类型定义的一般形式为:enum枚举名{枚举值表};在枚举值表中应罗列出所有可用值。这些值也称为枚举元素。例如:该枚举名为weekday,枚举值共有7个,即一周中的七天。凡被说明为weekday类型变量的取值只能是七天中的某一天。2.枚举变量的说明如同结构和联合一样,枚举变量也可用不同的方式说明,即先定义后说明,同时定义说明或直接说明。设有变量a,b,c被说明为上述的weekday,可采用下述任一种方式:enumweekday{sun,mou,tue,wed,thu,fri,sat};enumweekdaya,b,c;或者为:enumweekday{sun,mou,tue,wed,thu,fri,sat}a,b,c;或者为:enum{sun,mou,tue,wed,thu,fri,sat}a,b,c;11.10.2枚举类型变量的赋值和使用枚举类型在使用中有以下规定:1.枚举值是常量,不是变量。不能在程序中用赋值语句再对它赋值。例如对枚举weekday的元素再作以下赋值:sun=5;mon=2;sun=mon;都是错误的。2.枚举元素本身由系统定义了一个表示序号的数值,从0开始顺序定义为0,1,2…。如在weekday中,sun值为0,mon值为1,…,sat值为6。【例11.10】main(){ enumweekday{sun,mon,tue,wed,thu,fri,sat}a,b,c;a=sun;b=mon;c=tue;printf("%d,%d,%d",a,b,c);}说明:只能把枚举值赋予枚举变量,不能把元素的数值直接赋予枚举变量。如:a=sum;b=mon;是正确的。而:a=0;b=1;是错误的。如一定要把数值赋予枚举变量,则必须用强制类型转换。如:a=(enumweekday)2;其意义是将顺序号为2的枚举元素赋予枚举变量a,相当于:a=tue;还应该说明的是枚举元素不是字符常量也不是字符串常量,使用时不要加单、双引号。【例11.11】main(){enumbody{a,b,c,d}month[31],j;inti;j=a;for(i=1;i<=30;i++){month[i]=j;j++;if(j>d)j=a;}for(i=1;i<=30;i++){switch(month[i]){casea:printf("%2d%ct",i,'a');break;caseb:printf("%2d%ct",i,'b');break;casec:printf("%2d%ct",i,'c');break;cased:printf("%2d%ct",i,'d');break;default:break;}}printf(" "); }11.11类型定义符typedefC语言不仅提供了丰富的数据类型,而且还允许由用户自己定义类型说明符,也就是说允许由用户为数据类型取“别名”。类型定义符typedef即可用来完成此功能。例如,有整型量a,b,其说明如下:inta,b;其中int是整型变量的类型说明符。int的完整写法为integer,为了增加程序的可读性,可把整型说明符用typedef定义为:typedefintINTEGER这以后就可用INTEGER来代替int作整型变量的类型说明了。例如:INTEGERa,b;它等效于:inta,b;用typedef定义数组、指针、结构等类型将带来很大的方便,不仅使程序书写简单而且使意义更为明确,因而增强了可读性。例如:typedefcharNAME[20];表示NAME是字符数组类型,数组长度为20。然后可用NAME说明变量,如:NAMEa1,a2,s1,s2;完全等效于:chara1[20],a2[20],s1[20],s2[20]又如:typedefstructstu{charname[20];intage;charsex;}STU;定义STU表示stu的结构类型,然后可用STU来说明结构变量:STUbody1,body2;typedef定义的一般形式为:typedef原类型名新类型名其中原类型名中含有定义部分,新类型名一般用大写表示,以便于区别。有时也可用宏定义来代替typedef的功能,但是宏定义是由预处理完成的,而typedef则是在编译时完成的,后者更为灵活方便。 12位运算前面介绍的各种运算都是以字节作为最基本位进行的。但在很多系统程序中常要求在位(bit)一级进行运算或处理。C语言提供了位运算的功能,这使得C语言也能像汇编语言一样用来编写系统程序。12.1位运算符C语言提供了六种位运算符:&按位与|按位或^按位异或~取反<<左移>>右移12.1.1按位与运算按位与运算符"&"是双目运算符。其功能是参与运算的两数各对应的二进位相与。只有对应的两个二进位均为1时,结果位才为1,否则为0。参与运算的数以补码方式出现。例如:9&5可写算式如下:00001001(9的二进制补码)&00000101(5的二进制补码)00000001(1的二进制补码)可见9&5=1。按位与运算通常用来对某些位清0或保留某些位。例如把a的高八位清0,保留低八位,可作a&255运算(255的二进制数为0000000011111111)。【例12.1】main(){inta=9,b=5,c;c=a&b;printf("a=%d b=%d c=%d ",a,b,c);}12.1.2按位或运算按位或运算符“|”是双目运算符。其功能是参与运算的两数各对应的二进位相或。只要对应的二个二进位有一个为1时,结果位就为1。参与运算的两个数均以补码出现。 例如:9|5可写算式如下:00001001|0000010100001101(十进制为13)可见9|5=13【例12.2】main(){inta=9,b=5,c;c=a|b;printf("a=%d b=%d c=%d ",a,b,c);}12.1.3按位异或运算按位异或运算符“^”是双目运算符。其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。参与运算数仍以补码出现,例如9^5可写成算式如下:00001001^0000010100001100(十进制为12)【例12.3】main(){inta=9;a=a^5;printf("a=%d ",a);}12.1.4求反运算求反运算符~为单目运算符,具有右结合性。其功能是对参与运算的数的各二进位按位求反。例如~9的运算为:~(0000000000001001)结果为:111111111111011012.1.5左移运算左移运算符“<<”是双目运算符。其功能把“<<”左边的运算数的各二进位全部左移若干位,由“<<”右边的数指定移动的位数,高位丢弃,低位补0。例如: a<<4指把a的各二进位向左移动4位。如a=00000011(十进制3),左移4位后为00110000(十进制48)。12.1.6右移运算右移运算符“>>”是双目运算符。其功能是把“>>”左边的运算数的各二进位全部右移若干位,“>>”右边的数指定移动的位数。例如:设a=15,a>>2表示把000001111右移为00000011(十进制3)。应该说明的是,对于有符号数,在右移时,符号位将随同移动。当为正数时,最高位补0,而为负数时,符号位为1,最高位是补0或是补1取决于编译系统的规定。TurboC和很多系统规定为补1。【例12.4】main(){unsigneda,b;printf("inputanumber:");scanf("%d",&a);b=a>>5;b=b&15;printf("a=%dtb=%d ",a,b);}请再看一例!【例12.5】main(){chara='a',b='b';intp,c,d;p=a;p=(p<<8)|b;d=p&0xff;c=(p&0xff00)>>8;printf("a=%d b=%d c=%d d=%d ",a,b,c,d);}12.2位域(位段)有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例 如在存放一个开关量时,只有0和1两种状态,用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。1.位域的定义和位域变量的说明位域定义与结构定义相仿,其形式为:struct位域结构名{位域列表};其中位域列表的形式为:类型说明符位域名:位域长度例如:structbs{inta:8;intb:2;intc:6;};位域变量的说明与结构变量说明的方式相同。可采用先定义后说明,同时定义说明或者直接说明这三种方式。例如:structbs{inta:8;intb:2;intc:6;}data;说明data为bs变量,共占两个字节。其中位域a占8位,位域b占2位,位域c占6位。对于位域的定义尚有以下几点说明:1)一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:structbs{unsigneda:4unsigned:0/*空域*/unsignedb:4/*从下一单元开始存放*/unsignedc:4}在这个位域定义中,a占第一字节的4位,后4位填0表示不使用,b从第二字节开始,占用4位,c占用4位。2)由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。 3)位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:structk{inta:1int:2/*该2位不能使用*/intb:3intc:2};从以上分析可以看出,位域在本质上就是一种结构类型,不过其成员是按二进位分配的。2.位域的使用位域的使用和结构成员的使用相同,其一般形式为:位域变量名·位域名位域允许用各种格式输出。【例12.6】main(){structbs{unsigneda:1;unsignedb:3;unsignedc:4;}bit,*pbit;bit.a=1;bit.b=7;bit.c=15;printf("%d,%d,%d ",bit.a,bit.b,bit.c);pbit=&bit;pbit->a=0;pbit->b&=3;pbit->c|=1;printf("%d,%d,%d ",pbit->a,pbit->b,pbit->c);}上例程序中定义了位域结构bs,三个位域为a,b,c。说明了bs类型的变量bit和指向bs类型的指针变量pbit。这表示位域也是可以使用指针的。程序的9、10、11三行分别给三个位域赋值(应注意赋值不能超过该位域的允许范围)。程序第12行以整型量格式输出三个域的内容。第13行把位域变量bit的地址送给指针变量pbit。第14行用指针方式给位域a重新赋值,赋为0。第15行使用了复合的位运算符"&=",该行相当于:pbit->b=pbit->b&3位域b中原有值为7,与3作按位与运算的结果为3(111&011=011,十进制值为3)。同样,程序第16行中使用了复合位运算符"|=",相当于:pbit->c=pbit->c|1其结果为15。程序第17行用指针方式输出了这三个域的值。 12.3本章小结1.位运算是C语言的一种特殊运算功能,它是以二进制位为单位进行运算的。位运算符只有逻辑运算和移位运算两类。位运算符可以与赋值符一起组成复合赋值符。如&=,|=,^=,>>=,<<=等。2.利用位运算可以完成汇编语言的某些功能,如置位,位清零,移位等。还可进行数据的压缩存储和并行运算。3.位域在本质上也是结构类型,不过它的成员按二进制位分配内存。其定义、说明及使用的方式都与结构相同。4.位域提供了一种手段,使得可在高级语言中实现数据的压缩,节省了存储空间,同时也提高了程序的效率。 13文件13.1C文件概述所谓“文件”是指一组相关数据的有序集合。这个数据集有一个名称,叫做文件名。实际上在前面的各章中我们已经多次使用了文件,例如源程序文件、目标文件、可执行文件、库文件(头文件)等。文件通常是驻留在外部介质(如磁盘等)上的,在使用时才调入内存中来。从不同的角度可对文件作不同的分类。从用户的角度看,文件可分为普通文件和设备文件两种。普通文件是指驻留在磁盘或其它外部介质上的一个有序数据集,可以是源文件、目标文件、可执行程序;也可以是一组待输入处理的原始数据,或者是一组输出的结果。对于源文件、目标文件、可执行程序可以称作程序文件,对输入输出数据可称作数据文件。设备文件是指与主机相联的各种外部设备,如显示器、打印机、键盘等。在操作系统中,把外部设备也看作是一个文件来进行管理,把它们的输入、输出等同于对磁盘文件的读和写。通常把显示器定义为标准输出文件,一般情况下在屏幕上显示有关信息就是向标准输出文件输出。如前面经常使用的printf,putchar函数就是这类输出。键盘通常被指定标准的输入文件,从键盘上输入就意味着从标准输入文件上输入数据。scanf,getchar函数就属于这类输入。从文件编码的方式来看,文件可分为ASCII码文件和二进制码文件两种。ASCII文件也称为文本文件,这种文件在磁盘中存放时每个字符对应一个字节,用于存放对应的ASCII码。例如,数5678的存储形式为:ASCII码:00110101001101100011011100111000↓↓↓↓十进制码:5678共占用4个字节。ASCII码文件可在屏幕上按字符显示,例如源程序文件就是ASCII文件,用DOS命令TYPE可显示文件的内容。由于是按字符显示,因此能读懂文件内容。二进制文件是按二进制的编码方式来存放文件的。例如,数5678的存储形式为:0001011000101110只占二个字节。二进制文件虽然也可在屏幕上显示,但其内容无法读懂。C系统在处理这些文件时,并不区分类型,都看成是字符流,按字节进行处理。输入输出字符流的开始和结束只由程序控制而不受物理符号(如回车符)的控制。因此也把这种文件称作“流式文件”。本章讨论流式文件的打开、关闭、读、写、定位等各种操作。13.2文件指针在C语言中用一个指针变量指向一个文件,这个指针称为文件指针。通过文件指针就可对它所指的文件进行各种操作。 定义说明文件指针的一般形式为:FILE*指针变量标识符;其中FILE应为大写,它实际上是由系统定义的一个结构,该结构中含有文件名、文件状态和文件当前位置等信息。在编写源程序时不必关心FILE结构的细节。例如:FILE*fp;表示fp是指向FILE结构的指针变量,通过fp即可找存放某个文件信息的结构变量,然后按结构变量提供的信息找到该文件,实施对文件的操作。习惯上也笼统地把fp称为指向一个文件的指针。13.3文件的打开与关闭文件在进行读写操作之前要先打开,使用完毕要关闭。所谓打开文件,实际上是建立文件的各种有关信息,并使文件指针指向该文件,以便进行其它操作。关闭文件则断开指针与文件之间的联系,也就禁止再对该文件进行操作。在C语言中,文件操作都是由库函数来完成的。在本章内将介绍主要的文件操作函数。13.3.1文件的打开(fopen函数)fopen函数用来打开一个文件,其调用的一般形式为:文件指针名=fopen(文件名,使用文件方式);其中,“文件指针名”必须是被说明为FILE类型的指针变量;“文件名”是被打开文件的文件名;“使用文件方式”是指文件的类型和操作要求。“文件名”是字符串常量或字符串数组。例如:FILE*fp;fp=("filea","r");其意义是在当前目录下打开文件filea,只允许进行“读”操作,并使fp指向该文件。又如:FILE*fphzkfphzk=("c:\hzk16","rb")其意义是打开C驱动器磁盘的根目录下的文件hzk16,这是一个二进制文件,只允许按二进制方式进行读操作。两个反斜线“\”中的第一个表示转义字符,第二个表示根目录。使用文件的方式共有12种,下面给出了它们的符号和意义。文件使用方式意义“rt”只读打开一个文本文件,只允许读数据“wt”只写打开或建立一个文本文件,只允许写数据“at”追加打开一个文本文件,并在文件末尾写数据“rb”只读打开一个二进制文件,只允许读数据“wb”只写打开或建立一个二进制文件,只允许写数据“ab”追加打开一个二进制文件,并在文件末尾写数据 “rt+”读写打开一个文本文件,允许读和写“wt+”读写打开或建立一个文本文件,允许读写“at+”读写打开一个文本文件,允许读,或在文件末追加数据“rb+”读写打开一个二进制文件,允许读和写“wb+”读写打开或建立一个二进制文件,允许读和写“ab+”读写打开一个二进制文件,允许读,或在文件末追加数据对于文件使用方式有以下几点说明:1)文件使用方式由r,w,a,t,b,+六个字符拼成,各字符的含义是:r(read):读w(write):写a(append):追加t(text):文本文件,可省略不写b(banary):二进制文件+:读和写2)凡用“r”打开一个文件时,该文件必须已经存在,且只能从该文件读出。3)用“w”打开的文件只能向该文件写入。若打开的文件不存在,则以指定的文件名建立该文件,若打开的文件已经存在,则将该文件删去,重建一个新文件。4)若要向一个已存在的文件追加新的信息,只能用“a”方式打开文件。但此时该文件必须是存在的,否则将会出错。5)在打开一个文件时,如果出错,fopen将返回一个空指针值NULL。在程序中可以用这一信息来判别是否完成打开文件的工作,并作相应的处理。因此常用以下程序段打开文件:6)if((fp=fopen("c:\hzk16","rb")==NULL){printf(" erroronopenc:\hzk16file!");getch();exit(1);}这段程序的意义是,如果返回的指针为空,表示不能打开C盘根目录下的hzk16文件,则给出提示信息“erroronopenc:hzk16file!”,下一行getch()的功能是从键盘输入一个字符,但不在屏幕上显示。在这里,该行的作用是等待,只有当用户从键盘敲任一键时,程序才继续执行,因此用户可利用这个等待时间阅读出错提示。敲键后执行exit(1)退出程序。7)把一个文本文件读入内存时,要将ASCII码转换成二进制码,而把文件以文本方式写入磁盘时,也要把二进制码转换成ASCII码,因此文本文件的读写要花费较多的转换时间。对二进制文件的读写不存在这种转换。8)标准输入文件(键盘),标准输出文件(显示器),标准出错输出(出错信息)是由系统打开的,可直接使用。13.3.2文件关闭函数(fclose函数)文件一旦使用完毕,应用关闭文件函数把文件关闭,以避免文件的数据丢失等错误。fclose函数调用的一般形式是:fclose(文件指针); 例如:fclose(fp);正常完成关闭文件操作时,fclose函数返回值为0。如返回非零值则表示有错误发生。13.4文件的读写对文件的读和写是最常用的文件操作。在C语言中提供了多种文件读写的函数:·字符读写函数:fgetc和fputc·字符串读写函数:fgets和fputs·数据块读写函数:freed和fwrite·格式化读写函数:fscanf和fprinf下面分别予以介绍。使用以上函数都要求包含头文件stdio.h。13.4.1字符读写函数fgetc和fputc字符读写函数是以字符(字节)为单位的读写函数。每次可从文件读出或向文件写入一个字符。1.读字符函数fgetcfgetc函数的功能是从指定的文件中读一个字符,函数调用的形式为:字符变量=fgetc(文件指针);例如:ch=fgetc(fp);其意义是从打开的文件fp中读取一个字符并送入ch中。对于fgetc函数的使用有以下几点说明:1)在fgetc函数调用中,读取的文件必须是以读或读写方式打开的。2)读取字符的结果也可以不向字符变量赋值,例如:fgetc(fp);但是读出的字符不能保存。3)在文件内部有一个位置指针。用来指向文件的当前读写字节。在文件打开时,该指针总是指向文件的第一个字节。使用fgetc函数后,该位置指针将向后移动一个字节。因此可连续多次使用fgetc函数,读取多个字符。应注意文件指针和文件内部的位置指针不是一回事。文件指针是指向整个文件的,须在程序中定义说明,只要不重新赋值,文件指针的值是不变的。文件内部的位置指针用以指示文件内部的当前读写位置,每读写一次,该指针均向后移动,它不需在程序中定义说明,而是由系统自动设置的。【例13.1】读入文件c1.doc,在屏幕上输出。#includemain(){FILE*fp;charch;if((fp=fopen("d:\jrzh\example\c1.txt","rt"))==NULL) {printf(" Cannotopenfilestrikeanykeyexit!");getch();exit(1);}ch=fgetc(fp);while(ch!=EOF){putchar(ch);ch=fgetc(fp);}fclose(fp);}本例程序的功能是从文件中逐个读取字符,在屏幕上显示。程序定义了文件指针fp,以读文本文件方式打开文件“d:\jrzh\example\ex1_1.c”,并使fp指向该文件。如打开文件出错,给出提示并退出程序。程序第12行先读出一个字符,然后进入循环,只要读出的字符不是文件结束标志(每个文件末有一结束标志EOF)就把该字符显示在屏幕上,再读入下一字符。每读一次,文件内部的位置指针向后移动一个字符,文件结束时,该指针指向EOF。执行本程序将显示整个文件。2.写字符函数fputcfputc函数的功能是把一个字符写入指定的文件中,函数调用的形式为:fputc(字符量,文件指针);其中,待写入的字符量可以是字符常量或变量,例如:fputc('a',fp);其意义是把字符a写入fp所指向的文件中。对于fputc函数的使用也要说明几点:1)被写入的文件可以用写、读写、追加方式打开,用写或读写方式打开一个已存在的文件时将清除原有的文件内容,写入字符从文件首开始。如需保留原有文件内容,希望写入的字符以文件末开始存放,必须以追加方式打开文件。被写入的文件若不存在,则创建该文件。2)每写入一个字符,文件内部位置指针向后移动一个字节。3)fputc函数有一个返回值,如写入成功则返回写入的字符,否则返回一个EOF。可用此来判断写入是否成功。【例13.2】从键盘输入一行字符,写入一个文件,再把该文件内容读出显示在屏幕上。#includemain(){FILE*fp;charch;if((fp=fopen("d:\jrzh\example\string","wt+"))==NULL){printf("Cannotopenfilestrikeanykeyexit!"); getch();exit(1);}printf("inputastring: ");ch=getchar();while(ch!=' '){fputc(ch,fp);ch=getchar();}rewind(fp);ch=fgetc(fp);while(ch!=EOF){putchar(ch);ch=fgetc(fp);}printf(" ");fclose(fp);}程序中第6行以读写文本文件方式打开文件string。程序第13行从键盘读入一个字符后进入循环,当读入字符不为回车符时,则把该字符写入文件之中,然后继续从键盘读入下一字符。每输入一个字符,文件内部位置指针向后移动一个字节。写入完毕,该指针已指向文件末。如要把文件从头读出,须把指针移向文件头,程序第19行rewind函数用于把fp所指文件的内部位置指针移到文件头。第20至25行用于读出文件中的一行内容。【例13.3】把命令行参数中的前一个文件名标识的文件,复制到后一个文件名标识的文件中,如命令行中只有一个文件名则把该文件写到标准输出文件(显示器)中。#includemain(intargc,char*argv[]){FILE*fp1,*fp2;charch;if(argc==1){printf("havenotenterfilenamestrikeanykeyexit");getch();exit(0);}if((fp1=fopen(argv[1],"rt"))==NULL){printf("Cannotopen%s ",argv[1]);getch(); exit(1);}if(argc==2)fp2=stdout;elseif((fp2=fopen(argv[2],"wt+"))==NULL){printf("Cannotopen%s ",argv[1]);getch();exit(1);}while((ch=fgetc(fp1))!=EOF)fputc(ch,fp2);fclose(fp1);fclose(fp2);}本程序为带参的main函数。程序中定义了两个文件指针fp1和fp2,分别指向命令行参数中给出的文件。如命令行参数中没有给出文件名,则给出提示信息。程序第18行表示如果只给出一个文件名,则使fp2指向标准输出文件(即显示器)。程序第25行至28行用循环语句逐个读出文件1中的字符再送到文件2中。再次运行时,给出了一个文件名,故输出给标准输出文件stdout,即在显示器上显示文件内容。第三次运行,给出了二个文件名,因此把string中的内容读出,写入到OK之中。可用DOS命令type显示OK的内容。13.4.2字符串读写函数fgets和fputs1.读字符串函数fgets函数的功能是从指定的文件中读一个字符串到字符数组中,函数调用的形式为:fgets(字符数组名,n,文件指针);其中的n是一个正整数。表示从文件中读出的字符串不超过n-1个字符。在读入的最后一个字符后加上串结束标志''。例如:fgets(str,n,fp);的意义是从fp所指的文件中读出n-1个字符送入字符数组str中。【例13.4】从string文件中读入一个含10个字符的字符串。#includemain(){FILE*fp;charstr[11];if((fp=fopen("d:\jrzh\example\string","rt"))==NULL){printf(" Cannotopenfilestrikeanykeyexit!");getch();exit(1); }fgets(str,11,fp);printf(" %s ",str);fclose(fp);}本例定义了一个字符数组str共11个字节,在以读文本文件方式打开文件string后,从中读出10个字符送入str数组,在数组最后一个单元内将加上'',然后在屏幕上显示输出str数组。输出的十个字符正是例13.1程序的前十个字符。对fgets函数有两点说明:1)在读出n-1个字符之前,如遇到了换行符或EOF,则读出结束。2)fgets函数也有返回值,其返回值是字符数组的首地址。2.写字符串函数fputsfputs函数的功能是向指定的文件写入一个字符串,其调用形式为:fputs(字符串,文件指针);其中字符串可以是字符串常量,也可以是字符数组名,或指针变量,例如:fputs(“abcd“,fp);其意义是把字符串“abcd”写入fp所指的文件之中。【例13.5】在例13.2中建立的文件string中追加一个字符串。#includemain(){FILE*fp;charch,st[20];if((fp=fopen("string","at+"))==NULL){printf("Cannotopenfilestrikeanykeyexit!");getch();exit(1);}printf("inputastring: ");scanf("%s",st);fputs(st,fp);rewind(fp);ch=fgetc(fp);while(ch!=EOF){putchar(ch);ch=fgetc(fp);}printf(" ");fclose(fp);} 本例要求在string文件末加写字符串,因此,在程序第6行以追加读写文本文件的方式打开文件string。然后输入字符串,并用fputs函数把该串写入文件string。在程序15行用rewind函数把文件内部位置指针移到文件首。再进入循环逐个显示当前文件中的全部内容。13.4.3数据块读写函数fread和fwtriteC语言还提供了用于整块数据的读写函数。可用来读写一组数据,如一个数组元素,一个结构变量的值等。读数据块函数调用的一般形式为:fread(buffer,size,count,fp);写数据块函数调用的一般形式为:fwrite(buffer,size,count,fp);其中:buffer是一个指针,在fread函数中,它表示存放输入数据的首地址。在fwrite函数中,它表示存放输出数据的首地址。size表示数据块的字节数。count表示要读写的数据块块数。fp表示文件指针。例如:fread(fa,4,5,fp);其意义是从fp所指的文件中,每次读4个字节(一个实数)送入实数组fa中,连续读5次,即读5个实数到fa中。【例13.6】从键盘输入两个学生数据,写入一个文件中,再读出这两个学生的数据显示在屏幕上。#includestructstu{charname[10];intnum;intage;charaddr[15];}boya[2],boyb[2],*pp,*qq;main(){FILE*fp;charch;inti;pp=boya;qq=boyb;if((fp=fopen("d:\jrzh\example\stu_list","wb+"))==NULL){ printf("Cannotopenfilestrikeanykeyexit!");getch();exit(1);}printf(" inputdata ");for(i=0;i<2;i++,pp++)scanf("%s%d%d%s",pp->name,&pp->num,&pp->age,pp->addr);pp=boya;fwrite(pp,sizeof(structstu),2,fp);rewind(fp);fread(qq,sizeof(structstu),2,fp);printf(" nametnumberageaddr ");for(i=0;i<2;i++,qq++)printf("%st%5d%7d%s ",qq->name,qq->num,qq->age,qq->addr);fclose(fp);}本例程序定义了一个结构stu,说明了两个结构数组boya和boyb以及两个结构指针变量pp和qq。pp指向boya,qq指向boyb。程序第16行以读写方式打开二进制文件“stu_list”,输入二个学生数据之后,写入该文件中,然后把文件内部位置指针移到文件首,读出两块学生数据后,在屏幕上显示。13.4.4格式化读写函数fscanf和fprintffscanf函数,fprintf函数与前面使用的scanf和printf函数的功能相似,都是格式化读写函数。两者的区别在于fscanf函数和fprintf函数的读写对象不是键盘和显示器,而是磁盘文件。这两个函数的调用格式为:fscanf(文件指针,格式字符串,输入表列);fprintf(文件指针,格式字符串,输出表列);例如:fscanf(fp,"%d%s",&i,s);fprintf(fp,"%d%c",j,ch);用fscanf和fprintf函数也可以完成例10.6的问题。修改后的程序如例10.7所示。【例13.7】用fscanf和fprintf函数成例10.6的问题。#includestructstu{charname[10];intnum;intage;charaddr[15];}boya[2],boyb[2],*pp,*qq; main(){FILE*fp;charch;inti;pp=boya;qq=boyb;if((fp=fopen("stu_list","wb+"))==NULL){printf("Cannotopenfilestrikeanykeyexit!");getch();exit(1);}printf(" inputdata ");for(i=0;i<2;i++,pp++)scanf("%s%d%d%s",pp->name,&pp->num,&pp->age,pp->addr);pp=boya;for(i=0;i<2;i++,pp++)fprintf(fp,"%s%d%d%s ",pp->name,pp->num,pp->age,pp->addr);rewind(fp);for(i=0;i<2;i++,qq++)fscanf(fp,"%s%d%d%s ",qq->name,&qq->num,&qq->age,qq->addr);printf(" nametnumberageaddr ");qq=boyb;for(i=0;i<2;i++,qq++)printf("%st%5d%7d%s ",qq->name,qq->num,qq->age,qq->addr);fclose(fp);}与例10.6相比,本程序中fscanf和fprintf函数每次只能读写一个结构数组元素,因此采用了循环语句来读写全部数组元素。还要注意指针变量pp,qq由于循环改变了它们的值,因此在程序的25和32行分别对它们重新赋予了数组的首地址。13.5文件的随机读写前面介绍的对文件的读写方式都是顺序读写,即读写文件只能从头开始,顺序读写各个数据。但在实际问题中常要求只读写文件中某一指定的部分。为了解决这个问题可移动文件内部的位置指针到需要读写的位置,再进行读写,这种读写称为随机读写。实现随机读写的关键是要按要求移动位置指针,这称为文件的定位。 13.5.1文件定位移动文件内部位置指针的函数主要有两个,即rewind函数和fseek函数。rewind函数前面已多次使用过,其调用形式为:rewind(文件指针);它的功能是把文件内部的位置指针移到文件首。下面主要介绍fseek函数。fseek函数用来移动文件内部位置指针,其调用形式为:fseek(文件指针,位移量,起始点);其中:“文件指针”指向被移动的文件。“位移量”表示移动的字节数,要求位移量是long型数据,以便在文件长度大于64KB时不会出错。当用常量表示位移量时,要求加后缀“L”。“起始点”表示从何处开始计算位移量,规定的起始点有三种:文件首,当前位置和文件尾。其表示方法如下表。起始点表示符号数字表示文件首SEEK_SET0当前位置SEEK_CUR1文件末尾SEEK_END2例如:fseek(fp,100L,0);其意义是把位置指针移到离文件首100个字节处。还要说明的是fseek函数一般用于二进制文件。在文本文件中由于要进行转换,故往往计算的位置会出现错误。13.5.2文件的随机读写在移动位置指针之后,即可用前面介绍的任一种读写函数进行读写。由于一般是读写一个数据据块,因此常用fread和fwrite函数。下面用例题来说明文件的随机读写。【例13.8】在学生文件stu_list中读出第二个学生的数据。#includestructstu{charname[10];intnum;intage;charaddr[15];}boy,*qq;main(){FILE*fp;charch; inti=1;qq=&boy;if((fp=fopen("stu_list","rb"))==NULL){printf("Cannotopenfilestrikeanykeyexit!");getch();exit(1);}rewind(fp);fseek(fp,i*sizeof(structstu),0);fread(qq,sizeof(structstu),1,fp);printf(" nametnumberageaddr ");printf("%st%5d%7d%s ",qq->name,qq->num,qq->age,qq->addr);}文件stu_list已由例13.6的程序建立,本程序用随机读出的方法读出第二个学生的数据。程序中定义boy为stu类型变量,qq为指向boy的指针。以读二进制文件方式打开文件,程序第22行移动文件位置指针。其中的i值为1,表示从文件头开始,移动一个stu类型的长度,然后再读出的数据即为第二个学生的数据。13.6文件检测函数C语言中常用的文件检测函数有以下几个。13.6.1文件结束检测函数feof函数调用格式:feof(文件指针);功能:判断文件是否处于文件结束位置,如文件结束,则返回值为1,否则为0。13.6.2读写文件出错检测函数ferror函数调用格式:ferror(文件指针);功能:检查文件在用各种输入输出函数进行读写时是否出错。如ferror返回值为0表示未出错,否则表示有错。13.6.3文件出错标志和文件结束标志置0函数clearerr函数调用格式: clearerr(文件指针);功能:本函数用于清除出错标志和文件结束标志,使它们为0值。13.7C库文件C系统提供了丰富的系统文件,称为库文件,C的库文件分为两类,一类是扩展名为".h"的文件,称为头文件,在前面的包含命令中我们已多次使用过。在".h"文件中包含了常量定义、类型定义、宏定义、函数原型以及各种编译选择设置等信息。另一类是函数库,包括了各种函数的目标代码,供用户在程序中调用。通常在程序中调用一个库函数时,要在调用之前包含该函数原型所在的".h"文件。下面给出TurboC的全部".h"文件。TurboC头文件¢ALLOC.H说明内存管理函数(分配、释放等)。¢ASSERT.H定义assert调试宏。¢BIOS.H说明调用IBM—PCROMBIOS子程序的各个函数。¢CONIO.H说明调用DOS控制台I/O子程序的各个函数。¢CTYPE.H包含有关字符分类及转换的名类信息(如isalpha和toascii等)。¢DIR.H包含有关目录和路径的结构、宏定义和函数。¢DOS.H定义和说明MSDOS和8086调用的一些常量和函数。¢ERRON.H定义错误代码的助记符。¢FCNTL.H定义在与open库子程序连接时的符号常量。¢FLOAT.H包含有关浮点运算的一些参数和函数。¢GRAPHICS.H说明有关图形功能的各个函数,图形错误代码的常量定义,正对不同驱动程序的各种颜色值,及函数用到的一些特殊结构。¢IO.H包含低级I/O子程序的结构和说明。¢LIMIT.H包含各环境参数、编译时间限制、数的范围等信息。¢MATH.H说明数学运算函数,还定了HUGEVAL宏,说明了matherr和matherr子程序用到的特殊结构。¢MEM.H说明一些内存操作函数(其中大多数也在STRING.H中说明)。¢PROCESS.H说明进程管理的各个函数,spawn…和EXEC…函数的结构说明。¢SETJMP.H定义longjmp和setjmp函数用到的jmpbuf类型,说明这两个函数。¢SHARE.H定义文件共享函数的参数。¢SIGNAL.H定义SIG[ZZ(Z][ZZ)]IGN和SIG[ZZ(Z][ZZ)]DFL常量,说明rajse和signal两个函数。¢STDARG.H定义读函数参数表的宏。(如vprintf,vscarf函数)。¢STDDEF.H定义一些公共数据类型和宏。¢STDIO.H定义Kernighan和Ritchie在UnixSystemV中定义的标准和扩展的类型和宏。还定义标准I/O预定义流:stdin,stdout和stderr,说明I/O流子程序。¢STDLIB.H说明一些常用的子程序:转换子程序、搜索/排序子程序等。¢STRING.H说明一些串操作和内存操作函数。¢SYSSTAT.H定义在打开和创建文件时用到的一些符号常量。¢SYSTYPES.H说明ftime函数和timeb结构。¢SYSTIME.H定义时间的类型time[ZZ(Z][ZZ)]t。 ¢TIME.H定义时间转换子程序asctime、localtime和gmtime的结构,ctime、difftime、gmtime、localtime和stime用到的类型,并提供这些函数的原型。¢VALUE.H定义一些重要常量,包括依赖于机器硬件的和为与UnixSystemV相兼容而说明的一些常量,包括浮点和双精度值的范围。13.8本章小结1.C系统把文件当作一个“流”,按字节进行处理。2.C文件按编码方式分为二进制文件和ASCII文件。3.C语言中,用文件指针标识文件,当一个文件被打开时,可取得该文件指针。4.文件在读写之前必须打开,读写结束必须关闭。5.文件可按只读、只写、读写、追加四种操作方式打开,同时还必须指定文件的类型是二进制文件还是文本文件。6.文件可按字节,字符串,数据块为单位读写,文件也可按指定的格式进行读写。7.文件内部的位置指针可指示当前的读写位置,移动该指针可以对文件实现随机读写。

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

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

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