admin 管理员组文章数量: 1184232
2024年12月31日发(作者:微信开发者工具有什么作用)
《高级语言程序设计》知识点总结
第一章 概述
1.1 程序设计和程序设计语言
1.程序
从最一般的意义来说,程序是对解决某个计算问题的方法(算法)步骤的一种描述;而从计算机来说,计算机程
序是用某种计算机能理解并执行的计算机语言作为描述语言,对解决问题的方法步骤的描述。计算机执行按程序所描
述的方法步骤,能完成指定的功能。所以,程序就是供计算机执行后能完成特定功能的指令序列。
一个计算机程序主要描述两部分内容:描述问题的每个对象和对象之间的关系,以及描述对这些对象作处理的处
理规则。其中关于对象及对象之间的关系是数据结构的内容,而处理规则是求解的算法。针对问题所涉及的对象和要
完成的处理,设计合理的数据结构常可有效地简化算法,数据结构和算法是程序最主要的两个方面。
2.程序设计的任条和主要步骤
程序设计的任务就是分析解决问题的方法步骤(算法),并将解决问题算法的方法步骤用计算机语言记录下来。程
序设计的主要步骤包括:认识问题、设计解决问题的算法、按算法编写程序、调试和测试程序。在程序开发过程中,
上述步骤可能有反复,如发现程序有错,严重情况可能会要求重新认识问题和重新设计算法等。
3.机器语言和汇编语言
计算机能直接识别和执行的二进制代码称为计算机的机器语言。用有助于记忆的符号来代表二进制代码,称为汇
编语言。汇编语言与机器语言几乎有一对一的关系。用汇编语言编写的程序称为“汇编源程序”,汇编源程序不能在计
算机上直接执行,需要用汇编程序将汇编源程序翻译成机器语言程序,然后执行由汇编程序翻译出来的机器语言程序。
机器语言和汇编语言是与具体计算机紧密相关的,称它们是面向机器的语言。
4.高级语言
与机器语言和汇编语言相比较,高级语言与具体计算机无关,是一种能方便描述算法过程的计算机程序设计语言。
高级语言种类千差万别,但一般包含有以下四种成分:数据成分用来描述程序所涉及的数据;运算成分用来描述运算;
控制成分用来表达程序的控制构造;传输成分用来表达数据的传输。由于高级语言程序主要是描述计算机的解题过程,
即描述复杂的加工处理过程,所以也称这种高级语言为面向过程语言。
用高级语言编写的程序称为“源程序”。计算机不能直接技源程序的语句运行,通常有解释方式和编译方式两种方
法在计算机上执行源程序。
解释方式,即让计算机运行解释程序,解释程序逐句取出源程序中的语句,对它作解释执行,输入数据,产生结
果。
编译方式,即先运行编译程序,从源程序一次翻译产生计算机可直接执行的二进制程序(称为目标程序);然后让
计算机执行目标程序,输入数据,产生结果。
解释方式的主要优点是计算机与人的交互性好,调试程序时,能一边执行一边直接改错,能较快得到一个正确的
程序。缺点是逐句解释执行,运行速度慢。
编译方式的主要优点是计算机运行目标程序快,缺点是修改源程序后必须重新编译以产生新的目标程序。
现在也有将上述两种方式结合起来的,即先编译源程序,产生计算机还是不能直接执行的中间代码,然后让解释
程序解释执行中间代码。这样做的好处首先是比直接解释执行快;更大的好处是中间代码独立于计算机,只要有相应
的解释程序,就可在任何计算机上运行。
5.面向问题语言
面向问题语言是为了易于描述和求解某类特定领域的问题而专门设计的一种非过程语言。用面向问题语言解题时,
不仅摆脱计算机的内部逻辑,也不必关心问题的求解算法和求解的过程,只需指出问题是做什么,数据的输入和输出
形式,就能由相应的计算机系统得到所需结果。如报表语言、SOL(Structured Query Language)语言等。SQL语言是
数据库查询和操纵语言,能直接使用数据库管理系统。由于使用面向问题语言来解题只要告诉计算机做什么,不必告
诉计算机如何做,能方便用户的使用和提高程序的开发速度。但实现面向问题语言的系统从最一般的意义下实现问题
如何求解,通常实现的效率较低。另外,面向问题语言要求问题已有通用的求解方法,目前其应用范围还比较狭窄。
1.2 C语言基础
1.C语言的发展史
目的是为编写系统程序研制一种高级程序语言。1963年,参照ALGOL60语言,增添了能描述计算机硬件特性的
能力,称为CPL语言。1967年,作进一步的简化,改称BCPL语言。1970年,再进一步简化,并突出了硬件处理能力,
称为B语言,并用于编写UNIX操作系统。1972年,进一步扩充数据类型和恢复通用性,称为C语言,并用C语言重
写了UNIX操作系统。以后 C语言开始流行,为统一版本,ANSI于 1987年制定了 C语言的标准,称为 ANSIC.
2.C语言的特点
C语言主要有以下特点:
(1)与其它高级语言比较,更接近硬件,与机器语言比较,又更接近算法。C程序易编写、易读、易查错和易修
改。
(2)数据类型与运算符丰富,描述算法更简单方便。
(3)C程序的结构简单,语言包含的语句类别少。
(4)是一种结构化语言,提供完善的结构化程序控制结构,适宜采用结构化程序设计方法开发程序。
(5)也是一种模块化程序设计语言,适宜大型程序的研制和开发。
(6)通常C系统都提供大量的库函数供程序开发人员选用,能简化程序开发。
3.C程序的基本结构
通常一个C程序包括一个或多个函数,其中必有一个main函数,称为主函数。C函数的定义主要分两部分:函数
说明部分和函数体。其中函数体由C语言的语句序列组成,实现函数的功能。C程序总是从主函数开始执行。
4.C语言的基本语句
C语言的语句主要分以下几种:
(l)数据定义语句——用来定义程序中使用的各种数据,及能存放数据的对象的名称和特性。
(2)表达式语句——任何在有意义的表达式之后接上分号(;)构成的语句。最常见的有赋值表达式和函数调用
表达式后加分号构成的表达式语句,分别称为赋值语句和函数调用语句。
(3)流程控制语句——用来控制程序执行过程的语句。它们有选择控制语句、循环控制语句、break语句。continue
语句、return语句和goto语句等。
(4)复合语句——用花括号括住一个语句序列,即构成复合语句。复合语句用来表示其中的语句序列是一个整体,
在逻辑上是单个语句,并且强调其中的语句按顺序逐一执行。
(5)空语句——只有单个分号(;)构成的语句。空语句表示没有任何操作,用于选择控制或循环控制没有特别
被控制的成分语句,或在复合语句的末尾放置语句标号等。
(6)其它语句——如类型定义语句等。
5.注释
为了便于阅读和理解程序,可以在程序的任何地方插入注释,以说明程序、函数、程序段。语句的功能或采用的
算法。C语言的注释是用/*“和”*/“括住的任意字符列。程序编译时,注释不参与编译,也不会出现在目标程序中。
6.C语言的字符集
C语言的基本字符集有:
(l)数字10个(0~9)。
(2)英文字母大、小写各26个(A~Z,a~z)。
(3)键盘符号33个,用于构成特殊符号,其中下线字符“起一个英文字母的作用,以构成标识符等语法成分。
(4)转义字符,转义字符是由字符()开始后跟单个字符或若干字符组成,通常用于表示控制代码或特殊符号。
7.C程序的基本词汇
C语言的基本词汇有:
(1)字面形式常量。如 100、15.0、‘A’、“ABC”。
(2)特殊符号。主要是运算符。
(3)保留字。在程序或语句中是用来表示特定语法含义的英文单词。
(4)标识符。用于命名程序对象,如变量、常量、函数、标号等。在C语言中,一个合理的标识符由英文字母或
下线符开头,后跟或不跟由字母、下线符、数字符组成的字符列。~般以下线符开头的标识符作为系统内部使用。
利用基本词汇,按照给定的C语言的句法规则就可命名程序对象,描述表达式计算、构造语句、函数,直至整个
程序。
第二章 C语言的基本知识.基本数据类型、运算符和表达式
2.1 C语言的数据类型
数据类型包含两方面的内容:数据的表示和对数据加工的操作。数据的全部可能表示构成数据类型的值的集合。
数据全部合理的操作构成数据类型的操作集合。
在C语言中,把整型、实型和字符型称为基本数据类型,又称整型和实型为数值型。为了描述更复杂的数据结构,
C语言还有构造类型、指针类型、放举类型和空类型。构造类型是指由若干个相关的数据组合在一起形成的一种复杂数
据类型。
1.整型
整型数据按其存储在内存中的二进位信息的最高位是当作数值信息位还是当作数据的符号位,将整型数据分成带
符号整型和无符号整型两种。每种整型又按所需的字节个数的多少分成三种。所以整型共有6种:
带符号整型(int)、带符号短整型(short int)、带符号长整型(long int,或 long)、无符号整型(unsigned int)、无
符号短整型(unsigned short int)以无符号长整型(unsigned long)。
2.实型
实型数据有表示范围和精度两个不同的特征,为了适应数的范围和精度的不同要求,实型数据分三种类型:单精
度型(也称浮点型 float)、双精度型(double)、长双精度型(long double)
3.构造类型
构造类型是指由若干个相关的数据组合在一起形成的一种复杂数据类型,构造数据类型的成分数据可以是基本数
据类型的,也可以是别的构造类型的。按构造方式和构造要求区分,构造类型主要有数组类型、结构类型和共用类型。
数组类型是由相同类型的数据组成;结构类型可以由不同类型的数据组成;当不同数据类型不会同时使用时,以节约
内存,让不同数据占用同一区域,这就是共用类型。
4.指针类型
指针类型是取程序对象(如变量)在内存中占居的地址为值的一种特殊的数据类型。
5.枚举类型
当变量只取很少几种可能的值,并分别用标识符对值命名时,这种变量的数据类型可用枚举类型来表示。如变量
表示一个星期中的某一天,就可用校举类型描述该变量的类型,并以星期见的英文名对日期命名,对应的变量取某日
的星期名称为其值。
类型
用保留字VOid表示的数据类型有两种完全相反的意思,或表示没有数据(没有结果、没有形式参数),或表示某
种任意类型的数据(如又与指针结合,用 void.标记)。 void表示空类型,void.表示任意数据的指针类型,程序如要使
用 void.类型的数据,应该将它强制地转换成某种具体的指针类型。
2.2 常量
常量是指程序运行过程中其值不可改变的数据。常量按其值的表现形式可分为如下类型:整型常量、实型常量、
字符型常量、字符串常量和指针常量。
1.整型常量
C语言整型常量的书写形式有三种:
(1)十进制整数。通常整数的写法,如0,123,-45,+25.
(2)八进制整数。以数字符0开头并由数字符0-7组成的数字符序列,为八进制整数。如 0123表示八进制整数,
其值等于十进制整数 l*8*8+2*8+3=83.
(3)十六进制整数。十六进制整数以OX(或OX)开头的整数。表示十六进制数的数字将有16个,它们分别是
0-9和A、B、C、D、E、F,其中六个英文字母也可以小写。例如,0x123表示十六进制整数,其值等于十进制整数
1*16*16+2*16+3=291; oxabc,其值等于10*16*16+11+16+12=2748.
整型常量也可在整数之后接上字母L(或l),特别指明它是 long型的。
整型常量也可在整数之后接上字母U(或u),特别指明是不带符号的,即是unsigned型的。为指明不带符号的long
型整型常量,则需在整数之后同时加上字母U和L,表明该整型常量是unsigned long型的。
整型数据以二进制形式存放,要求短整型数据的字节数不能大于整型数据的字节数;整型数据的字节数不能大于
长整型数据的字节数。对于带符号的整数,用二进制代码的最左二进位作为数的符号,1表示负数,0表示正数。
2.实型常量
C语言实型常量的一般书写格式是:
正负号 整数部分。小数部分 指数部分
其中,正负号可有可无,无正负号即为正实数;整数部分和小数部分都是十进数字符序列;指数部分是e(或E)
后接上正负号(正号可有可无)和十进数字符序列。
按上述格式书写实型常量,另有两条限制:
(l)整数部分和小数部分可以任选,但不可同时都没有。
(2)小数点和指数部分不可以同时都没有。
ANSIC引入两个后缀字符,用 f标识 float型实型常量,用 1(或 L)标识 fong double型实型常量,而无后缀符
的实型常量被认为是double型的实型常量。
要注意实型常量的有效位数,不要以为写上的数字都能被计算机所接受。在大多数C系统中,一个float型实型数
据在内存中占用4个字节(32个二进位),约7个十进位有效数字,能表示绝对值最接近0的实数约为10的-38次方,
最大的实数约为10的38次方。例如,对于代码 float x=123456.123f,在大多数系统中,实型常量123456.123f的最后
两位数字是无效的。
double型实型数据占用8个字节(64个二进位),约15个十进位有效数字,能表示绝对值最接近0的实数约为10
的一308次方,最大的实数约为10的308次方。
3.字符常量
字符型数据用于表示一个字符值,但字符型数据的内部表示是字符的ASCll代码(8位二进位的二进制形式)。字
符型数据的类型符用char来标记。字符型常量的书写方法:
(l)普通字符——用单引号括起一个字符。如‘a’、‘b’、‘B’、‘$’。
(2)特殊字符——用‘\字符或字符列采标记。这种标记方法有三种:
‘\特定字符’,标记特定控制符,如换行符用‘\ n’来标记。
·‘\ 1至3个人进制数字’,以人进数表示字符的ASCll代码。
‘\ X1至2个十六进制数字符’,以十六进制数表示字符的 ASCll代码。
由于字符以代码存放,所以也可把字符值当作一个小整数。反之,一个小整数也可把它理解为是某个字符的代码,
把它当作一个字符。
4.字符串常量
字符串常量用来表示一个字符序列,它的书写方法用双引号括住字符序列。例如:“ABC”、“”等。字符串常量“”
是一个空字符串,即不含任何有效字符的字符串。字符串数据顺序存储字符串字符的ASCll代码,并在最后字符后面
存储一个H进制代码全为0的特殊字符,用来标记字符串的结束。所以字符串常量“1234”在内存占5个字节,而空
字符串只占一个字节。字符串中的字符也可以是转义字符。
5.符号常量
为了提高程序的可读性,常量可以用以下形式命名:
# define 标识符 常量
其中的标识符也称作符号常量,这里的常量是前面所述的某种常量、或是程序中前面定义的符号常量。例如:
# define MAXN 100
2.3 变量
变量是程序执行时,其值允许改变的数据对象,用来存储输入数据、计算的中间结果和程序的最终结果等。
1.变量定义
变量用标识符命名,通过变量定义引入的变量名习惯用英文字母开头,C系统可能也会预定义一些标记系统特性的
变量,系统定义的变量习惯用下线字符开头。变量的数据特性由变量定义时指定的类型确定。若定义指定数据类型的
变量(即不在指定数据类型基础上定义新数据类型的变量,如指定类型的指针、数组等),这种变量定义的句法为:
数据类型符变量名1,变量名2,„„;
编绎系统根据变量的数据类型确定存放它的值所需要的内存字节数,变量值的内部表示形多也由其类型确定。
2.内部变量和外部变量
变量按其定义出现在程序中的位置不同分成两类:在函数内定义的变量称为内部变量,而在函数之外(函数之间)
定义的变量称为外部变量。
3.变量的存储类型
系统按程序对计算机存储空间使用的不同要求,将内存分成不同用途的块,与计算机的寄存器一起将存储空间分
成不同类别。而C程序通过指定变量的存储类表明变量的不同的使用要求,让系统将变量分配于不同的内存块或寄存
器。如在上述变量定义形式中,还要指定变量的存储类型,变量定义的形式为:
存储类型符数据类型符变量名1,变量名2,„;
其中存储类型有四种: auto(自动的)、static(静态的)、register寄存器的)和 extem外部的)。
外部变量只允许被指定为静态的,或不指定其存储类型。内部变量可以被指定为自动的或静态的、或寄存器的、
或不指定存储类型,若不指定存储类型,它就是自动的。
自动变量是内部变量,在函数内或复合语句内定义,它们被分配在堆栈区。
静态变量可以是内部变量,也可以是外部变量。静态变量表示永久性和专用性,即在程序执行过程中一直存在,
局限于定义它的函数(内部静态变量)或局限于定义它的程序文件中那些函数(外部静态变量)。静态变量被分配在与
程序相联的内存数据区。
寄存器变量是函数的内部变量或参数,也是一种临时性的变量。如因函数使用非常频繁程序希望将它分配在寄存
器,程序就可将变量指定为寄存器的,但编译系统也可能把它当作动变量处理。
指定存储类型是外部的,实际b是告诉编译器,这个变量是一个外部变量,在这里要使它,而它的定义或在别的
程序文件或在后面的程序段等别的地方。
4.变量的作用域和生存期
变量的使用要注意变量的作用域(可使用范围)和生存期(存在的时间)。C语言规定,内部变量的作用域只局限
于定义它的函数或复合语句。自动的内部变量是一种临时性变量,函数被调用时分配,函数执行结束时释放。而静态
变量在程序执行前分配,直至程序结束才释放。由于静态的内部变量在函数结束时,依旧保持存储,函数上一次调用
时留在内部静态变量中的结果能被下一次调用时继续使用。外部变量也在程序启动前分配,直至程序执行结束释放。
普通的外部变量能提供别的源程序文件中的函数使用(要对它作外部说明);静态的外部变量只能供定义它的源程序中
的全部函数专用。由于外部变量能供整个程序使用,所以外部量不能重名。
5.变量定义初始化
变量定义是对变量的存储空间提出一种要求,存储空间分配后,变量的初值通常是不拔的。但程序可以要求系统
在为变量分配存储空间同时为变量设定初值,这就是变量定义初始化。在变量的定义形式中,在变量名之后接上“=初
值表达式”,该初值表达式的值就作为该变量的初值。C语言另有约定,对于静态变量和外部变量,若定义它们时未指
定初值,系统给它们设置成全部二进位都是0的值。以下是各种变量定义的例子:
(l)定义整型,并对其初始化。
Short minInt =100;
int i= l,j= 2, k=3;
long p=-1L,q=1234567890L;
unsigned usi= 254u;
unsigned long up= 4294967295UL;
(2)定义字符型变量,并对其初始化。
char ch=‘A’;
(3)定义实型(浮点型、双精度型)变量,并对其初始化。
float f=1.23456f;
double d=1.2345678op87654;
2.4 运算符
每个运算符都代表对运算对象的某种运算,都有自已特定的运算规则,规定运算对象的个数、运算对象数据类型,
以及运算结果的数据类型。C语言还规定运算符有不同的优先级和结合性。运算符的优先级指表达式求值时,按运算符
的优先级由高到低的次序计算。如“先乘除后加减”。运算符的结合性是指运算符要求它的运算对象对它的结合方向。
结合性确定了在相同优先级运算符连续出现的情况下运算对象与运算符结合的顺序,通常也是计算顺序。如算术运算
符的结合性是从左至右的,则连续的加减或连续的乘除是从左向右计算。而赋值运算符的结合性是从右至左的,则连
续的赋值运算是从右向左逐个计算赋值。在C语言中,要特别注意某些运算符因运算对象数据类型不同,可能有不同
的意义。
1.算术运算符
算术运算符的运算对象是基本数据类型的数据,实现通常的取整、取负、四则运算、求两整数相除后的余数的运
算和增1减1运算。特别要留心的是整除运算、求余运算、增1运算和减1运算。
对两个整型数据执行除运算(/),称为整除运算,要特别注意的是两个整型数据的整除运算的结果是整型的,如
表达式3/2的结果为1,表达式2/3的结果为0.
求余运算符(%)要求参与运算的两个运算对象均为整型数据,如 5% 3的值为 2.一般来说,求余运算所得结果
的符号与被除数的符号相同。如-5%3的值为-2,5%-3的值为2.
增1(++)和减1(——)运算符都是单目运算符,以整型、字符型和指针型变量为运算对象,并改变运算对象的
值。按它们出现在变量之前和之后两种不同情况,其作用有微妙的差别。
前缀++
前缀++的一般形式是
++变量
例如,若X是整型或某种指针类型的变量,则++X使变量X的值增大1个单位,并以X的新值作为表达式“++X”
的运算结果。如以下语句执行前,变量X的值为1,语句
j= ++ x;
使变量X的值变成3,变量j的值也为3.这里所说的一个“单位”是指:如果X是整型的,则++x就是普通的解释:
“x的值比原值增加1”;x是指针,它指向数组的某个元素,则++x使它指向数组的后一个元素。
后缀++
后缀++的一般形式是
变量++
表达式“变量++”运算结果是该变量的原来值,在确定了表达式结果之后,用与前缀++相同的方式增大该变量的
值1个单位。
前缀++和后缀++都能使变量的值增加1个单位,但是它们所代表的表达式的值却不相同,前者是变量增加后的值,
后者是变量还未增加的原先值。例如i,j为整型变量,且i的值为4,以下分别用①和②表记的代码将使j获得不同的
值:
①j=++i ;
②j= i++
都使变量i的值变为5,但①使j的值为5;②使j的值为4.
前缀——
前缀—— 的一般形式是
——变量
前缀——使变量的情减少(或后退)l个单位,并以变量的新值为表达式“——变量”的运算结果。
后缀——
后缀—— 的一般形式是
变量——
后缀—— 作用于变量时,以该变量的值作为表达式“变量——”的运算结果,即先取其值为结果,然后用与前缀
——相同的方式减少该变量1个单位。
后缀—— 与前缀—— 的区别类似于后缀++与前缀++的区别。类似前面的例子,依旧假定i的值为4,两代码
③j= ——i
④j=i——
都使变量i的值变为3,但③使j的值为3;④使j的值为4.
使用++和——运算符时,其运算对象仅适用于变量,不能是常量等数据值表达式。如 4++或(i+j)++都是不合
法的。
++和——是带有副作用的运算符。建议读者不要在一个表达式中对同一变量多次使用这样的运算符,可能会发生
意想不到的结果。如i的值为4,对表达式
(i++)+(i++)
可能认为它的值为 9(+5)。然而在 TURBO C和MS C系统中,它的值为8.而表达式
(++i)+(++i)
的值为12.这是因为这些系统在处理 i++时,先使用 i的原值计算整个表达式,然后再让i连续两次自增;处理++i
时,在计算表达式值之前,先对 i执行两次自增,然后才计算表达式。放前一个表达式的值为8,后一个表达式的值为
12.
因+与++(-与——类似)是两个不同运算符,对于类似表达式i+++j会有不同的理解:(i++)+ j或i+(++j)。 C
编译的处理方法是自左至右让尽可能多的字符组成一个合法的句法单位(如标识符、数字、运算符等)。因此,i+++j
被解释成(i++)+j,而不是i+(++j)。
增1(++)和减1(——)运算符的结合方向是自右至左的。
2.关系运算符
用于关系运算的关系运算符有六个:<(小于)、>(大于)、<=(小于等于)、>=(大于等于)、==(等于)和!=
(不等)。关系运算是双目运算,它的运算对象可以是基本数据类型的数据,用于比较大小;或是指向同一个数组两元
素的指针,用于比较前后关系。在高级语言中,习惯称条件满足为“真”,不满足为“假”。特别在C语言中约定:比
较后,条件满足(真)的值为1;比较后,条件不满足(假)的值为儿用关系运算符将两个子表达式连接起来,构成关
系比较表达式,求得结果为1(真)或0(假)。
六个关系运算符中,运算符(<、<=、>、>=)的优先级高于运算符(==,!=)。如表达式X>y==C 式(X>y)==(C<d)。 另外,为便于描述两个复杂算式的比较,关系运算符的优先级低于算术运算符的优先级。 设有i=1,j=2,k=3,则表达式i>j的值为“假”,即表达式的值为0;表达式i==k>j的值为“真”,即表达式的 值为1(先计算k>j,其值为1,等于i);i+j 关系运算符的结合方向是自左至右。仍设i=1,j=2,k=3,则表达式k>j>i的值为0(先计算k>j,其值为1, 再计算1>1,结果为0)。 3.逻辑运算符 用于逻辑运算的逻辑运算符有三个: &&(逻辑与)、||(逻辑或)、!(逻辑非) 其中运算符“&&”和“||”是双目运算符,要求有两个整型或字符型的运算对象,用于连接多个判定条件,构成 更复杂的条件判定;运算符“!”是单目运算符,用于描述对给定条件的否定判定。 逻辑运算产生的结果也只能是1或0. 1表示逻辑运算结果为“真”;用0表示运算结果为“假”。 在判定一个运算对象的值为“真”或“假”时,以运算对象的值不等于零为“真”,运算对象的值等于0为“假”。 逻辑运算符中,按优先级排列为:逻辑非运算符!的优先级高于逻辑与运算符&&,逻辑与运算符&&的优先级 高于逻辑或运算符||.另外,&&和||的优先级低于关系运算符的优先级;!的优先级高于算术运算符的优先级。逻辑运算 符||和&&的结合方向是自左至右,而逻辑运算符!的结合方向是自有至左。 需要特别指出的是,“逻辑与”和“逻辑或”运算符分别有以下性质: a&&b,仅当a为0时,不管b为何值(实际上不再计算b),结果为0. a||b,仅当 a为 1时,不管 b为何值(实际上不再计算 b),结果为1. 上述性质就是说,对于表达式a&&b,仅当子表达式a为非零时,才计算子表达式b;对于表达式a||b,仅当子表 达式a为0时,才计算子表达式b.在具体编写程序时,也应利用以上性质。对于两个条件的逻辑与,如当条件1不成 立情况下,条件2的值没有意义或不可计算时,逻辑表达式应写成: 条件l&&条件2 避免在条件1不成立情况下,计算条件2.如有条件:y/x>2且 x!=0,应写成: x!=0 && y/x>2 当X为0时,不会计算y/X.而写成: y/X>2 && X!=0 是不正确的,因为当 X为0时,不能计算y/X.对于逻辑或也有类似情况。 由于上述性质,在计算连续的逻辑与和逻辑或运算时,实际上不分优先级,而是顺序从左至右计算。在计算逻辑 与运算时,若有左运算对象的值为0,则不再继续计算逻辑与运算,并立即以0为逻辑与运算的结果;在计算逻辑或运 算时,若有左运算对象的值为1,则不再继续计算逻辑或运算,并立即以1为逻辑或运算的结果。在顺序计算逻辑表达 式的过程中,一旦确定了表达式的最终结果,就不再继续计算。 4.赋值运算符 赋值运算的最简单形式是 变量=表达式 其中“=”是赋值运算符。赋值运算的执行过程是: (l)先计算赋值运算符右端的表达式; (2)如表达式的类型与赋值运算符左边的变量类型不同(仅限于基本类型),将表达式值的类型自动转换成变量 的类型; (3)将求得的值赋给变量,即存储到由变量所占的内存中。 简单地说,计算表达式的值,将该值赋给变量。 赋值运算也有结果,经赋值运算后,赋值表达式具有赋值后赋位运算符左边变量同样的类型和值。因赋位运算有 值,所以可以进一步参与运算,特别是可以再赋植给其它变量。赋值运算符的结合性是‘边有至左“的,当连续有多 个赋值运算时,则从右至左逐个赋值。如有变量定义: int k; double x; 则赋值表达式: x= k= 3.5 是先将实数3.5自动转换成整数3赋给整型变量k,然后又将整数3自动转换成实数3.0赋给实型变量X.所以,k 的值是3,X的值是3.0. 在程序中,经常遇到在变量当前值的基础上作某种修正的运算。如 x=x+5.0 这类运算的特点是:变量既是运算对象,又是赋值对象。为避免对同一存储对象的地址重复计算,C语言弓队复合 赋值运算符。它们是 +=、-=、*=、%=、〈〈=、〉〉= 、&= 、^=、|= 通常,记日为某个双目运算符,复合赋值运算 xθ=e 其等效的表达式为 x= xθ(e) 注意,当e是一个复杂表达式时,等效表达式的括号是必需的。如 y*= a+b 的等效表达式是y= y*(a+b) 赋值运算符和所有复合赋值运算符的优先级全相同,并且都是“自右至左”结合,它们的优先级高于远号运算符 的优先级,低于其它所有运算符的优先级。 5.逗号运算符 逗号运算符“,”用于将若干表达式连接起来顺序地逐个计算。连续返号运算的一般形式为: 表达式1,表达式2,„,表达式n 它的计算顺序是从左到右逐一计算各表达式,并以表达式n的值为连续逗号运算的结果。例如,表达式 x=( i=3, i*2) 使i等于3,X等于6.其实,逗号运算只是把多个表达式串联起来,在许多情况下,使用逗号运算的目的只是想分 别计算各个表达式的值,而并非想使用逗号运算中最后那个表达式的值。逗号运算最常用于for结构中,用于给多个变 量登初值,或用于对多个变量的值逐一修改等。逗号运算符的优先级最低,其结合性是“自左向右”的。 6.条件运算符 条件运算是一个三目运算,有三个运算对象。条件运算的一般形式为 表达式1?表达式2:表达式3 条件运算的计算规则是: (1)计算表达式1的值; (2)如果表达式1的值非0(真),则计算表达式2,并以表达式2的值为条件运算的结果(不再计算表达式3); (3)如果表达式1的值为0(假),则计算表达式3,并以表达式3的值为条件运算的结果(不再计算表达式2)。 例如,表达式 X>y?X:y 如果x>y条件为真,则条件运算取x值,否则取y值。 条件运算符(?:)的优先级高于赋值运算符,低于逻辑运算符,也低于关系运算符和算术运算符。例如,表达式 max= X> y?X: y+ l 等效于表达式 max=((x>y) ?x:( y+l)) 条件运算符的结合性为“自右至左”。例如,表达式X>y?X:u>V?u:V 等效于表达式 X>y?X:(u>V?u:V) 7.长度运算符 长度运算以字节为单位给出其运算对象所需(或所占)的字节数,运算对象可以是任何类型的数据对象或数据类 型。它是根据对象的类型来确定对象(所需)的字节数的。 长度运算有两种书写形式: sizeof变量名或sizeof(类型名) 如果有某种类型为江的变量V,其中t可以是系统提供的类型或用户自己定义的类型,可以是简单的,也可以是数 组、结构等。则 sizeof V就是变量 V所占的字节数。如有 int j; double x; 表达式sizeof j和sizeOf x分别是变量j和x所占的字节数。 sizeof(t)是系统为分配一个类型为t的数据对象所需的字节数。如sizeOf(int)和sizeof(dou-ble)分别是系统 为分配一个类型为int和double变量所需的字节数。 8.位运算符 位运算的运算对象只能是整型或字符型数据,位运算把运算对象看作是由二进位组成的位率信息,按位完成指定 的运算,得到位串信息的结果。位运算符又可分成两类:一类是位逻辑运算符,另一类是位移位运算符。 位逻辑运算符有:&(按位与)、|(按位或)、^(按位异或)、~(按位取反) 位移位运算有:<<(位左移)和>>(位右移)其中按位取反运算符是单目运算符。其余均为双位运算符。位逻辑 运算符的优先级从高到低,依次为~、&、^、|、,其中的结合方向自右至左,且优先级高于算术运算符,其余运算符 的结合方向都是自左至右 ,且优先级低于关系运算符。位移位运算符的优先级低于算术运算符,高于关系运算符,它 们的结合方向是自左至右。 按位与运算符(&) 按位与运算将将两个运算对象的对应位按位遵照以下规则进行计算: 0&0=0, 0&l=0,1&0=0,1&l=1 即同为一的位,结果为1,否则结果为0. 例如,设 3的内部表示为00000011,5的内部表示为00000101,则3&5的结果为00000001 按位与运算有两种典 型用法。一是取一个位率信息的某几位,如以下代码截取。的最低7位、&0177t二是让某变量保留某几位,其余位设 置成0,如以下代码让X只保留最低6位:x=X&077、以上用法都先要设计好一个常数,该常数只有需要的位是互, 不需要的位是 0.用它与指定的位串信息按位与。 按位或运算符(|) 按位或运算将两个运算对象的对应位按位遵照以下规则进行计算: 0|0=0, 0|l=1, 1|0=l, 1|1=1 即只要有五个是1的位,结果为1,否则为0. 例如,023|035结果为037. 按位或运算的典型用法是将一个位串信息的某几位设置成1.如将要获得最右4位为1,其它位与变量j的其它位相 同,可用逻辑或运算 017|j.若要把这结果赋给变量 j,可写成:j=017|j 按位异或运算符(^) 按位异或运算将两个运算对象的对应位按位遵照以下规则进行计算: 0^0= 0, 0^1=l, l^0=l, 1^l=0 即相应位的值相同的,结果为0,不相同的结果为l. 例如,013^035的结果为026. 异或运算的意思是求两个运算对象相应位值是否相异,相异的为1,相同的为0.按位异或运算的典型用法是求一个 位串信息的某几位信息的反。如欲求整型变量j的最右4位信息的反,用逻辑异或运算017^j,就能求得j最右4位的 信息的反,即原来为1的位,结果是0;原来为0的位,结果是1. 按位取反运算符(~) 按位取反运算是单目运算,用来求一个位串信息按位的反,即那些为0的位,结果是1;而那些为1的位,结果是 几例如,~7的结果为0xfff8.取反运算常用来生成与系统实现无关的常数。如要将变量X最低6位置成0,其余位不变, 可用代码 X= X&~077实现。以上代码与整数 X用 2个字节还是用 4个字节来实现无关。 当两个长度不同的数据进行位运算时(例如 long型数据与 int型数据),将两个运算对象的右端对齐进行位运算。 如果短的数为正数,高位用0补满;如果短的数为负数,高位用1补满。如果短的为无符号整数,则高位总是用0补 满。 位运算用来对位串信息进行运算,得到位串信息结果。如以下代码能取整型变量k的位串信息的最右边为亚的信 息位:((k-l)^k)&k. 位左移运算符(<<) 位左移运算符用来将左运算对象(整型或字符型数据)作为二进位信息串作整体向左移动,移动的位数由右运算 对象指定,右端空出的位用0补充,得到新的位申信息。例如014<<2,结果为060,即48. 位右移运算符(>>) 位右移运算将一个位串信息向右移指定的位,右端移出的位的信息被丢弃。例如12>>2,结果为3.与左移相反, 对于小整数,每右移1位,相当于除以人在右移时,需要注意符号位问题。对无符号数据,右移时,左端空出的位用0 补充。对于带符号的数据,如果移位前符号位为刚正数),则左端也是用0补充;如果移位前符号位为1(负数),则左 端用0或用1补充,这取决于计算机系统。对于负数右移,称用0补充的系统为“逻辑右移”,用1补充的系统为“算 术右移”。以下代码能说明读者上机的系统所采用的右移方法:printf(“%dnnn”,-2>>4);者输出结果为一l,是 采用算术右移;输出结果为一个大整数,则为逻辑右移。 2.5 表达式 表达式就是将运算符与运算对象连接起来描述计算的式予。按表达式的构成规则分,表达式可分以下几类: 1.初等量表达式 初等量表达式是常量、变量、字符串、函数调用、数组元素、结构成分和带圆括号的表达式等。 2.单目运算表达式 单目运算表达式是由单目运算符和一个运算对象构成的表达式。单目运算符的优先级低于初等量的运算符,它们 的结合性都是“自有向左”结合。 3.双目运算表达式 双目运算表达式的一般形式为 表达式 双目运算符 表达式 双目运算符自左向右结合。按双目运算符分类,又可分算术表达式、关系表达式、逻辑表达式、赋值表达式和远 号表达式。由于C语言没有特别的真、假值,判定时,以非0值为真,以0值为假。所以,前述的C语言的各种表达 式的计算结果都可作为逻辑值。 4.条件运算表达式 条件运算表达式的一般形式为 表达式?表达式:表达式 条件运算符自右向左结合。 2.6 数据类型转换 1.隐式类型转换 C语言允许基本数据类型中的不同类型数据进行混合运算。因不同类型的数据所占内存字节数和其内部表示形式的 不同,在算术运算中(其它运算例外)一个运算符所涉及到的各运算对象,能根据运算对象的情况,要求运算对象的 值从一种类型转换成另一种类型。这种类型转换是自动进行的,称作隐式类型转换。隐式类型转换严格遵守以下规则, 按所列优先顺序实行类型转换。 (1)如有运算对象是 long double型的,则其余运算对象也转换成 long doube型。 (2)如有运算对象是double型的,则其余运算对象也转换成double型。 (3)如有运算对象是float型的,则其余运算对象也转换成float型。 (4)如有运算对象是 unsigned long int型的,则其余运算对象也转换成 unsigned long int型。 (5)如有运算对象是 long int型的,则其余运算对象也转换成 long int型。 (6)如有运算对象是 unsigned int型的,则其余运算对象都转换成 unsigned int型。 (7)最后,运算对象和结果都是int型的。 根据最后一条规则,两个char型和short型运算对象都自动转换成int型参与运算,并且结果是int型的。 2.显式类型转换 算术运算中,基本数据类型的混合运算会发生隐式类型转换,当要求与隐式类型转换规则不一致时,可在表达式 中用显式类型转换运算,强制地将一种类型的表达式值转换成另一种类型的位。显式类型转换的书写形式为 (类型名)表达式 其中(类型名)是对其后的表达式作强制类型转换运算,它将表达式的值强制地转换成类型名所指明的类型。例 如,库函数sqrt()是求一个double型值的平方根。为求整型变量m的平方根,正确的写法是sqrt((double)m) 在求m的平方根之前,先将m的值强制地转换成double型,然后去调用函数sqrt()。 类型转换不只改变表达式的值的类型,也可能会因两种表示形式上的差异,值的大小会有一些误差。 第三章 顺序结构、选择结构和循环结构的程序设计 3.1 顺序结构 顺序结构用来描述一个计算或操作序列,表示从序列的第一个计算开始,顺序执行序列中的每个计算,直至序列 的最后一个计算。通常,一个复杂的计算过程不能用一个简单的计算来表达,而需把复杂的计算描述成简单计算的序 列。 1.复合语句 在C语言中,将顺序执行的语句序列,用花括号括起来,构成C语言的复合语句。在逻辑上视复合语句为单个语 句,它也能用作其它结构语句的成分语句。在很多场合,复合语句内还会包含其它结构语句。 2.表达式语句 在顺序结构中,最频繁使用的是表达式之后接上一个分号。例如,在赋值表达式之后接上分号,完成用表达式的 值更新某变量,习惯称这种表达式语句为赋值语句;在函数调用之后接上分号,完成指定的计算功能,习惯称这种表 达式语句为函数调用语句。 3.2 常用输入输出库函数 最基本的输入输出库函数有字符输入函数、字符输出函数、格式输入函数和格式输出函数。 1.字符输入函数 字符输入函数getchar()的功能是从标准输入设备上(通常是键盘终端)读取一个字符。该函数没有参数,对它 的每次调用,就返回下一个输入字符的ASCII代码值。例如,执行语句 ch=getchar(); 使变量ch得到输入字符的ASCII代码值。一般情况下,这里的变量ch为char型或int型。当程序在输入字符后, 用ch判定输入文件是否结束时,变量ch必须是int型的。这是因为文件结束标记值是-1,是int型的。程序中常用EOF 表示当前读人字符是文件结束标记,常量名EOF在文件Stdio.h中被定义为-1. 2.字符输出函数 字符输出函数ptuchar()有一个字符的ASCII代码值参数,函数调用putchar (ch)的功能是将以出值为其ASCII 代码的字符输出到标准输出设备(通常是终端显示屏)上。这里ch可以是char型或int型数据。 3.格式输入函数 格式输入函数scanf()的作用是从标准设备读人字符序列,按格式控制字符率所包含的格式解释输入字符序列, 并将解释结果存储到对应的变量中。调用格式输入函数scanf()的一般形式为 scanf(格式控制字符串,变量地址,变量地址,„„) 格式控制字符串是字符串表达式,通常是由一对双引号括起来的字符串常量,直接用于解释输入字符序列。格式 控制字符率可以包含: ?空白类字符(空格符或制表符),它们使输入跳过空白类字符,直到遇到下一个非空白类字符。 ?普通字符(不包括%),它们要求输入字符流中下一个字符与它相同。 ?格式转换说明,以字符‘%’开头至输入格式符结束的字符序列组成。格式转换说明引导对下一输入字符段进行 转换。 格式转换说明的一般形式为 %[*][w][h/l/L]输入格式符 输入格式符共有14个,有12种不同输入格式,其中大纲要求掌握的7种输入格式符的意义见表3.1.用方括号括住 的内容是输入格式修饰说明,可以缺省,它们的意义是: (1)*——星号(赋值抑制符),对应的输入数据项按格式要求被输入,但结果不存储。带星号的格式转换说明不 对应变量地址。用它来跳过一个输入数据项。 (2)——整型常数(域宽说明),表示输入数据项的字符段的有效字符数。若实际输入字符段的字符数小于W, 以实际有效字符为准。 对于数值数据输入格式来说,输入域定义为从下一个非空白类字符起(因此可能跳过若干个空格符、制表符、换 行符),到一个与数值数据相矛盾的字符,或直到输入了指定个数的字符数;对于字符率输入格式来说,输入域定义为 从下一个非空白类字符起,输入非空白类字符,直至遇到空白类字符,或直到输入了指定个数的非空白字符。 (3) h/l/L长度修饰符,指明输入数据项的存储类型。 h 修饰格式符d,o,X时,表示输入的整数按短整型存储。 l 修饰格式符d,O,X时,表示输入的整数按长整型存储。 l 修饰格式符e,f时,表示输入的实数按double型存储。 缺省时,对于格式符d,o,x,表示输入的整数按int整型存储;对于格式符e,f,表示输入的实数是按float型存 储。 表3.l常用输入格式符表 格式符& 第四章 数组 4.1 一维数组 1.数组的基本概念 数组是一组同类对象集合的一种表示。在C语言中,数组类型是这样一种数据结构:数组所有元素的数据类型相 同,元素个数固定,其元素按顺序存放,每个元素对应一个序号(称为下标),数组元素的下标从0开始顺序编号,各 元素按下标存取(引用)。数组元素变量能与相同类型的独立的变量一样使用。引用数组元素变量所需的下标个数由数 组的维数决定,数组有一维数组、二维数组或多维数组之分。 2.一线数组的定义 一维数组的定义形式为 类型说明符数组名[常量表达式」; 例如 int a[5] ; 定义一个名为a的数组,它有五个元素,每个元素都是整型。数组定义包含以下几个要点: (l)类型说明符用来指明数组元素的类型,同一数组的诸元素,它们的类型是相同的。 (2)数组是一个变量,与一般变量一样,用标识符命名,数组名遵守标识符的命名规则。 (3)方括号“[]”是数组的标志,方括号中的常量表达式的值表示数组的元素个数,即数组的长度。例如,int a[5] 中的 5表示数组 a有5个元素,下标从0开始,这五个元素分别是:a[0]、a[1]、a[2]、a[3]和a[4]. (4)常量表达式通常是整型常量、符号常量或sizeof(类型名),以及由它们组成的常量表达式。定义数组大小用 常量表达式,就是说数组的大小是固定的,不可以包含引用变量值的表达式来定义数组的大小。 (5)C语言有一个约定,当数组名单独出现在表达式中时,数组名可以代表为它分配的内存区域的开始地址,即 数组中下标为0的元素的地址。在这种情况下,数组名起着一个常量的作用,即a与&a[0]作用一样。如代码scanf(“%d”, &a[0])与 scanf(“%d”,a)都是为数组a的第一个元素输入值。 3.一维数组的初始化 可在数组定义同时,给出数组元素的初值。这种表述形式称为数组初始化。数组初始化可用以下几种方法实现: (l)数组定义时,顺序列出数组全部元素的初值。例如, int d[5]={0,l,2,3,4}; 将数组元素的初值依次写在一对花括弧内。经上面定义和初始化之后,就有d[0]=0.d[l]=l、d[2]=2、d[3]=3、d[4] =4. (2)只给数组的前面一部分元素设定初值。例如, int e[5]={0,l, 2}; 定义数组e有5个整型元素,其中前三个元素设定了初值,而后两个元素末明确地设定初值。系统约定,当一个 数组的部分元素被设定初值后,对于元素为数值型的数组,那些末明确设定初值的元素自动被设定0值。所以数组e 的后两个元素的初值为0.但是,当定义数组时,如未对它的元素指定过初值,对于内部的局部数组,则它的元素的值 是不确定的。 (3)当对数组的全部元素都明确设定初值时,可以不指定数组元素的个数。例如, int g[]={5,6,7,8,9}; 系统根据初始化的花括号内的初值个数确定数组的元素个数,所以数组g有五个元素。但若提供的初值个数小于 数组希望的元素个数时,则方括号中的数组元素个数不能省略。如代码int b[10]={1,2,3,4,5}定义数组 b有10 个元素,前五个元素如设定所示,后五个元素都为0.反之,如提供的初值个数超过了数组元素个数,就是一个错误。 4.一维数组元素的引用 程序定义了数组后,就可引用数组的元素。引用数组元素的一般形式为 数组名[下标] 其中下标可以是整型常量、整型变量或整型表达式。例如,数组a的五个元素可分别用a[0]、a[l]、a[2]、a[3]、a[4] 来引用它们。 设有定义: int x[20], i; 以下代码实现顺序输入数组X的全部元素: for(i=0; i<20;i++) scanf(“%d”,&x[i]); 4.2 二维数组和多维数组 1.多维数组定义 数组也可以是多维的。现以二维数组为例介绍二维及二维以上的多维数组。二维数组的定义形式为 类型说明符 数组名「常量表达式」[常量表达式」; 通常多维数组的定义形式有连续两个或两个以上“「常量表达式」”。例如, float a[2][3],b[3][4]; /*两个二维数组*/ float c[2][2][3] ;/*一个三维数组*/ 定义数组a为2行3列,数组b为3行4列。C语言把二维数组看作是一种特殊的一维数组,即它的元素又是一个 数组。例如,对于上述定义的数组a,把它看作有两个元素的一维数组: a[0]和 a[l] 每个元素又是一个包含3个元素的一维数组。通常,一个n维数组可看作是一个一维数组,而它的元素是一个(n-1) 维的数组。C语言对多维数组的这种观点和处理方法,使数组的初始化、引用数组的元素以及用指针表示数组带来很大 的方便。 在C语言中,二维数组的元素的存放顺序是按行存放的,即从数组的首地址开始,先顺序存放第一行的元素,再 存放第二行的元素。通常,对于一个多维数组,它的元素在内存中的存放顺序有这样特点:第一维的下标变化最慢, 最右边的下标变化最快。 2.引用多维数组元素 引用二维数组元素的表示形式为 数组名[下标][下标] 通常,引用n维数组元素的表示形式为数组名之后紧接连续n个“[下标]”。 在用下标引用数组的元素时,应该注意下标值的有效性,应在已定义的对应维大小的范围内,即大于等于0和小 于对应维的元素个数。 3.多线数组初始化 多维数组的初始化方法也有多种,以二维数组的初始化方法为例说明其初始化方法。 (1)按行给二维数组的全部元素赋初值。例如 int al[2][3]={{1,2,3 },{4,5,6 }}; 这种赋初值方法比较直观,第一个花括弧内的数据给第一行的元素赋初值,第二个花括弧内的数据给第二行的元 素赋初值,依次类推,按行给数组的全部元素赋初值。 (2)按元素的存储顺序给数组元素赋初值。例如, int a2[2][3]= {1,2,3,4,5,6 }; 这种赋初值方法结构性差,容易遗漏。 (3)按行给数组的部分元素赋初值。例如, int a3[2][3]={{1,2},{0,5}}; 其效果是使a3[0][0]=l,a3[0][1]=2,a3[1][0]=0,a3[1][l]=5,其余均为0. (4)按元素的存储顺序给前面部分元素赋初值。例如, int a4[2][3]={1,2,3,4 }; 其效果是使a4[0][0]=1,a4[0][l]=2,a4[0][2]=3,a4[1][0]=4,其余均为0. (5)按元素的存储顺序,给数组部分或全部元素赋初值,并且不指定第一维的元素个数。例如, int a5[][3]={l,2,3,4,5 }; 系统会根据结出的初始数据个数和其它维的元素个数确定第一维的元素个数。其效果是使: a5[0][0]=1,a5[0][1]=2,a5[0][2]= 3, a5[1][0]=4,a5[l][l]=5,a5[1][2]=0. 所以数组a5有2行。 (6)用按行赋初值方法,对各行的部分或全部元素赋初值,并省略第一维的元素个数。例如, int a6[][3]={{O,2},{}}; 也能确定数组a6共有2行。 4.3 字符数组和字符串 1.字符数组 如果数组的元素类型是字符型(char),则此数组就是字符数组。字符数组的每个元素只能存放一个字符(存放字 符的ASCII代码)。 字符数组的定义形式与其它数组的定义形式一样: char字符数组名[元素个数]; 例如, char S[5]; 表示数组S有五个元素,每个元素能存放一个字符,整个数组最多可存放五个字符。字符数组元素的引用方法也 与普通数组元素的引用方法相同。 字符数组也可与普通数组一样的初始化,字符数组也可利用字符串常量给字符数组初始化。例如, char aStr[]={“12345”}; 并可省略花括弧,简单地写为 char aStr[]=“ 12345” ; 注意:字符数组aStr[]的元素有六个,不是五个。用字符串常量对字符数组初始化,C系统会在字符列末尾添加一 个字符串结束符。 2.字符串 称最后有字符率结束符'0'的字符序列为字符串。字符数组中存储的字符序列本身并不要求最后一定要有字符'0'.但 当字符数组内存储的内容需要作为字符串时,就必须要有标记符''.当字符数组内存储的是字符串时,可用“%s”格式 输出,若是普通的字符序列,则它不能用格式“%s”输出,而只能结合循环用格式“%c”输出。 指定元素个数的字符数组用字符串常量给它初始化时,其元素个数不能小于字符串常量的字符数,但数组的元素 个数可以等于字符串常量的字符数。例如, char ss[3]=“abc”; 则, ss[0]='a',ss[l]='b', ss[2]='c'.此时,字符数组ss中存储的是字符序列,不是字符串。 字符率结束标记符的代码是8位全0,称为空字符,程序用'0'来标记。字符串的结束标记符紧接在字符串的有效字 符列之后。例如,一个有8个有效字符的字符串,其长度为8个字符,但在它的第九个位置存有结束标记符'0'. 请读者注意以下几点: (l)字符率与存储字符串的字符数组有区别。字符率的有效字符是指从所指位置的第一个字符开始至字符串结束 标记符之前的那些字符。格式符“%s”只输出字符串的有效字符,而不会再继续输出字符率结束标记符及其之后的字 符。例如, char str[50]=“Pas0cal Cobol Fortran C”; printf(“%sn”,str) ; 将只输出: Pas 而实际上,数组str[]字符率结束符之后还存有其它许多字符。 (2)用“%s”格式输出字符串时,不包括字符串结束标记符。对应的输出项是字符串或字符串名。字符数组名可 作为字符串名。对于上例,写成 printf(“%s”,s[0]); 是错误的。因s[0]是数组s的元素,是一个字符,不是字符串。 (3)在调用scanf()为字符数组输入字符串时,输入项是数组名,不要加地址运算符&. (4)若用“%c”格式结合循环输入字符序列,若程序又想将输入的字符序列构成字符串,则程序必须用赋值语句 在字符列之后存入字符串结束标记符,使其变成字符串。 程序经常要处理许许多多的字符串,如存储星期的名称。同时存储众多字符串的一个实现方法是定义一个二维字符 数组,让二维数组的每一行存储一个字符串。这样做,要求数组每行元素个数应比可能最长的字符串字符个数还要多1 个。如下面的示例所示: char weekDay[][9]= {“Sunday”,“Monday”,“Tuesday”,“Wednesday”, “Thursday”,“ Friday” ,“Saturday”}; 在C程序中,存储多个字符串更好的方法是用指针数组。 3.常用字符串库函数 求字符串长度函数strlen() 函数调用 strlen(str)返回str中的有效字符(不包括'0')的个数。 字符串拷贝函数strcpy() 函数调用Strcpy(strl,str2) 将字符串str2拷贝到字符数组strl.要求字符数组strl足够大,以便能容纳被拷贝的str2 的全部内容。 限制字符数的字符串拷贝函数strncpy() 函数调用strncpy(strl,str2,n)的作用是将str2中的前n个字符拷贝到strl(并附加'0')。其中n是整型表达式, 指明欲拷贝的字符个数。如果str2中的字符个数不多于n,则函数调用 strncpy(strl,str2,n)等价于strcpy(strl,str2)。 字符串连接函数strcat() 函数调用strcat(strl,str2) 将str2内容拷贝接在字符数组strl中的字符串的后面。要求字符数组strl必须足够大, 以便还能容纳str2的内容。该函数调用返回strl的开始地址。注意:字符串连接前,strl和str2都各自有'0',连接后, strl中原来的'0'在拷贝时被覆盖掉,而在新的字符率有效字符之后再保留一个'0'.例如 char strl[30]=“Beijing”; char str2[30]=“ Shanghai”; 函数调用 strcat(strl,str2); printf(“%s n”,strl); 将输出: BeijingShanghai 字符串比较函数strcmp() 函数调用strcmp(strl,str2)批较两个字符串的大小,对两个字符串自左至右逐对字符相比较(按字符的 ASCII 代码值的大小),直至出现不同的字符或遇到'' 字符为止。如直至'' 字符,全部字符都相同,则认为相等,函数返回0 值;若出现不相同的字符,则以这第一对不相同的字符比较结果为准,若strl的那个不相同字符小于str2的相应字符, 函数返回一个负整数;反之,返回一个正整数。 注意:对字符串不允许施行关系运算符比较两字符之间的大小关系,必须类似于本函数那样,通过逐个字符的比 较来实现。 字符串输出函数puts( ) 函数调用 puts(str) 将 str的字符串输出到终端,并将 str中的''以字符转换成换行符'n'输出。即输出字符串内 容后,并换行。所以,puts(str)相当于printf(“%sn”,str)。 字符串输入函数gets( ) 函数调用gets(str)从终端输入字符序列(包括空白符)到字符数组str,字符序列以回车符作为结束,并将输入 时的回车符转换成''字符存储。该函数调用返回str的存储开始地址。调用get()函数与用“%s”格式调用格式输入函 数scanf输入字符串不同,后者会自动跳过前导空白符,并以非空白符之后的空白符结束。前者用于输入一行内的全部 字符,包括可能有的空白符,存放于字符数组str,并将最后读人的换行符转换成字符率结束标记存储在str中。 第五章 指针 5.1 指针和指针变量 指针是程序设计语言的一个重要概念。指针在C程序中有以下多方面的作用: (1)利用指针能间接引用它所指的对象。 (2)利用各种类型的指针形式参数,能使函数增加描述能力。 (3)指针与数组结合,使引用数组元素的形式更加多样、访问数组元素的手段更加灵活。 (4)指针能用来描述数据和数据之间的关系,以便构造复杂的数据结构。当一个数据A要关联另一个数据B时, 在数据A中增加一个指向数据B的指针就可实现数据A关联数据B.结合系统提供的动态分配存储设施,又能构造出各 种动态数据结构。 1.指针的基本概念 为了区别内存的不同位置,内存被分成字节,内存的全部字节顺序地赋予一个称为地址的编号。程序中的变量将 在内存中占据一定的内存字节,在这些字节中存储的数据信息称为变量的内容。一个变量占用连续的若干个内存字节 时,最前面的一个字节的地址就作为该变量的地址。指针就是内存地址,是变量的地址,或函数的入口地址。变量的 地址在程序执行时,起着非常重要的作用。当计算机在计算含有变量的表达式时,计算机按变量的地址取出其内容, 并按变量的地址将计算结果存入到变量占据的内存中。如代码: int x=l; x=x+2; 其中语句“x=x+2;”中的第一个x涉及到变量x占据的内存,第二个 x是引用变量 x的内容。该语句的意义是 “取X的内容,完成加上2的计算,并将计算结果存入变量X占据的内存中。” 2.指针变量和它所指向的变量 在C语言中,地址也作为一种值,能被存储、比较、赋值,并称地址数据为指针类型,而称存储地址值的变量为 指针变量,简称指针。C程序可用运算符&取变量的地址,如表达式&x 的值就是变量X的地址。程序除能按名引用变量外,也可利用变量的地址引用变量。按变量名引用变量称为直接 引用,而将变量A的地址存于另一变量B中,借助于变量B引用变量A称为对A的间接引用。 3.指针变安的定义、初始化和引用 指针变量用于存放某个变量的地址。定义指针变量的一般形式为: 类型 * 指针变量名; 或 类型 * 指针变量名=初值表达式; 其中,指针变量名是标识符,指针变量名之前的符号“*”,表示该变量是指针类型的。而最前面的“类型”,表示 该指针变量能指向变量或函数的类型。初值表达式是一个地址表达式,如表达式中有某变量的地址表达式,则这个变 量应是前面已定义的。 在C语言中,当定义局部指针变量时,如未给它指定初值,则其值是不确定的。程序在使用它们时,应首先给它 们赋值。误用其值不确定的指针变量间接引用其它变量,会引起意想不到的错误。为明确表示指针变量不指向任何变 量,在C语言中用0值表示这种情况,记为NULL.如 ip= NULL; 也称指针值为0的指针变量为空指针。对于静态的指针变量,如在定义时未给它指定初值,系统自动给它指定初 值0. 指针变量取程序对象的(开始)地址值,不能将一个整型量或任何其它非地址值赋给一个指针变量。另外,指针 变量对所指向的对象也有类型限制,不能将一个不能指向的对象的地址赋给指针变量。如有以下定义: int i=100,j,*ip,*intpt; float f,*fp; 以下代码如注释所叙。 iP=&i;/*使ip指向i*/ intpt=ip;/*使intpt指向ip所指变量*/ fp= &f;/*使fp指向正*/ ip=NULL;/*使 ip不再指向任何变量*/ 5.2 指针变量的应用 1.指向变目的指针变量 当指针变量指向某个对象(它的值不是NULL)时,可以用 * 指针变量 引用指针变量所指向的对象。如语句: ip=&i; j=* ip; 实现将指针变量ip所指变量的内容(即变量i的内容)赋给变量j.其中,赋位号右边的*ip 表示引用中所指变量的 内容。上述赋值等价于: j=1; 语句 *ip=200; 实现向指针变量ip所指变量(即变量i)赋值200.其中,赋值号左边的。ip表示引用ip所指变量。上述赋值等价 于 i=200; 一般地,记号“* 指针变量名”与指针变量所指变量的“变量名”等价。要特别注意:指针变量之间的赋值,指 针变量所指向的变量之间的赋值,这两种赋值在表示方法上的区别。如语句 intpt=ip; 使两个指针变量intpt与ip指向同一个对象,或都不指向任何对象(如果ip的值为NULL)。而语句 * intpt=*ip; 实现将ip所指变量的值赋给intpt所指的变量。这里要求中与intpt的值都不可以是NULL.通过指针变量引用它所 指的变量,实际引用哪一个变量,取决于指针变量的值。改变指针变量的值,就是改变了它的指向。指针变量最主要 的应用有两个方面:一是让指针变量指向数组的元素,以便逐一改变指针变量的指向,遍历数组的全部元素;二是让 函数设置指针形式参数,让函数体中的代码通过指针形式参数引用调用环境中的变量或函数。 为正确使用指针变量和它所指向的对象,特指出以下几点注意事项: (1)指针变量定义与引用指针变量所指对象采用相似的标记形式(* 指针变量名),但它们的作用与意义是完全 不同的。在指针变量定义中(如int *ip;),指针变量名之前的符号“*”说明其随后的标识符是指针变量名。如果指针 变量定义时带有初始化表达式,如 int i, * ip=&i; 初始化表达式的地址是赋给指针变量本身,而不是指针变量所指对象(实际上,在初始化之前,指针变量还未指 向任何对象)。 (2)通过指向变量i的指针变量ip引用变量三与直接按其名i引用变量i ,效果是相同的,凡直接按名可引用处, 也可以用指向它的某个指针变量间接引用它。如有 int i, *ip=&i; 则凡变量i能使用的地方,*ip一样能用。 (3)因单目运算符* 、&、++和——是从右向左结合的。要注意分清运算对象是指针变量、还是指针变量所指对 象。如有 int i,j,*ip=&i; 语句 j=++*ip; 是指 ip所指向的变量(变量i)的内容加1,加1后的值赋给变量j.也就是说,++*ip相当于++(*ip)。而语句j =*ip++;相当于语句j=*ip; ip++;这是因为先求值的是表达式 ip++,它的求值规则是,表达式的值为原来ip的位, 然后ip的内容增加了 1个单位。所以。 ip++的表达式值与*ip相同,并在*ip++求出表达式值的同时,指针变量ip增 加了1个单位。这样,ip不再指向变量i,这种情况常用在指针指向数组元素的情况,在引用数组某元素之后,自动指 向数组的下一个元素。而语句j=(*ip)++;则是先引用ip所指向的对象,取ip所指向的对象的内容赋给j,并让中所 指向的对象的内容增加1个单位。 2.指向一维数组元素的指针变量 指针变量也能指向数组的元素。设有以下变量定义: int a[100],*p; 赋值运算p=&a[0]使p指向a[0].表示&a[0]还有更简洁的方法,即数组名a.按约定,一维数组名表达式的值为数组 存储区域的开始地址,即数组首元素的指针。对指向数组元素的指针允许作有限的运算。设有代码: int *p,*q,a[100] ; p=&a[10] ; q=&a[50] ; (l)指向数组元素的指针可与整数进行加减运算。利用数组元素在内存中顺序连续存放的规定,和地址运算规则, 有表达式 a+1为 a[1] 的地址,a+2为 a[2]的地址。一般地,表达式a+i为a[i]的地址。把这个结论应用于指向数组 元素的指针,同样地成立。若p的值为a[0]的地址,则表达式p+i的值为a[i]的地址。或者说,p+i的值为指向a[i]的 指针值。若p指向数组元素 a[10],则 p+n就表示指向数组元素 a[10+n],这里n是任意的整数表达式。 一般地,当指针变量指向数组a的元素时,不论数组元素的类型是什么,指针和整数n进行加减运算时,总是根 据所指元素的数据存储字节长度 sizeof a[0] ,对n放大,保证加减n,使指针植向前或向后移动n个元素位置。 (2)当两个指针指向同一个数组的元素时,允许两个指针作减法运算。其绝对值等于两指针所指数组元素之间相 差的元素个数。如表达式&a[4O]-&a[0]的值为40. (3)当两个指针指向同一个数组的元素时,这两个指针可以作关系比较(<,<=, ==,>,>=,!=)。若两指针p 和q指向同一个数组的元素,则p==q为真表示p,q指向数组的同一个元素;若p 的下标小于q所指向的数组元素的下标。 利用运算符*可引用指针所指对象,*(a+i)表示引用a+i所指向的数组元素a[i] .这样。(a+i)就是 a[i].对于指 向数组元素的指针变量p,若p指向a[10],*(p+i)表示引用p+i所指向的数组元素 a[10+i]. 与用数组名和下桥引用数组元素的标记法相一致,指向数组元素的指针变量也可带下标引用数组的元素,即*(p +i)也可写成p[i] .但若p=&a[10],则p[i]引用的是a[10+i],p[2]引用的是a[8]. 综上所述,引用数组元素有以下多种形式: (1)用数组元素的下标引用数组元素,如 a[5]. (2)利用数组名表达式的值是数组首元素指针的约定,可利用指针表达式间接引用数组元素,如*(a+i) . (3)利用指向数组元素的指针变量,用它构成指向数组元素的指针表达式,并用该表达式引用数组元素。如*(p +i)或p[i]. 这里要强调指出用数组名a表达数组元素指针与用指向数组元素的指针p来表达数组元素的指针,在实际应用上 的区别:p是变量,其值可改变,如p++;而数组名a只代表数组a的首元素的指针,它是不可改变的,程序只能把它 作为常量使用。 数组名表达式和指向数组元素指针表达式与数组元素的位置关系可用图5.l来示意: 图5.1指向数组元素的指针与数组元素位置之间的关系 3.指向字符串的指针变目 通常所说的字符串指针就是指向字符率某字符的字符指针。因字符率存储于字符数组中,所以字符串指针也就是 指向数组元素的指针。 为程序中引入的字符串常量提供存储空间有两种方法。一是把字符率常量存放在一个字符数组中。例如, char s[]=“I am a string.”; 数组s共有15个元素,其中 s[14] 为'0' 字符。对于这种情况,编译程序根据字符串常量所需的字节数为字符数 组分配存储,并把字符串复写到数组中,即对数组初始化。另一种方法是由编译系统将字符串常量与程序中出现的其 它常量一起存放在常量存储区中。程序为了能访问存于常量存储区中的字符串常量,可用一个字符指针指向它的第一 个字符。当字符串常量出现在表达式中时,系统将字符率常量放入常量存储区,而把表达式转换成字符指针,指向该 字符串常量的第一个字符。因此,可在定义字符指针变量时给它初始化指向某字符串常量,或用字符申常量给字符指 针变量赋值,这两种方法都使字符指针指向字符串常量的第一个字符。例如, char *cp1,*cp2=“I am a string”;/*定义字符指针变量,并赋初值*/ cp1=“Another string”;/* 先定义字符指针变量,然后按需要赋初值*/ 上述代码使字符指针变量cp2指向字符率常量“I am a string”的第一个字符I,使cpl指向字符串常量“Another string” 的第一个字符 A. 4.指向二维数组中的某个一维数组的指针变量 如有一个二维数组,且指针变量所指的是二维数组中的一整行,则指针变量另有一些很有意义的性质。设有二维 数组为 int a[3][4]={{1,2,3,4},{5,6,7,8 },{ 9,10,11,12 }}; 这里,数组a有3行4列。按行来看数组a,数组a有三个元素,分别为a[0],a[1],a[2].它们又分别是一个一维数 组,各有4个元素。例如,a[0]所代表的一维数组为 a[0][0] 、a[0][l] 、a[0]p[2], a[0][3]. 一维数组名表达式的值是数组首元素(下标为0)的地址,二维数组名a表达式是a的首行a[0]的地址。一般地, a+i可以看作二维数组a的第i+1行的首地址。 因二维数组a能用a[0].a[1]、a[2] 分别表示它的各行,所以a[0]能表示用a[0]标记的 a的第一行的首元素 a[0][0] 的 地址;a[1] 能表示用 a[l] 标记的 a的第二行的首元素 a[l][0] 的地址。一般地,a[i]能表示用 a[i]标记的a的第i+1行 的首元素a[i][0]的地址。由于数组的开始地址与数组首元素的地址相同,这样,a+i与a[i]应有相同的值,但它们的意 义不同,a+i表示用a[i]标记的a的第i+1行的首地址,a[i]表示用a[i]标记的a的第i+l行的首元素a[i][0]的地址。另 外,因a[i]可写成*(a+i),所以a+i与*(a+i)也有不同意义,而值相等。a[i]或*(a+i) 表示二维数组a的元素a[i][0] 的地址,即&a[i][0].根据地址运算规则,a[i]+j即代表数组a的元素a[i][j]的地址,即&a[i][j].因a[i]与*(a+i)等价, 所以*(a+i) +j也与&a[i][j]等价。 由二维数组元素a[i][j]的地址有多种表示形式,数组元素a[i][j]也有以下三种等价表示形式:*(a[i]+j)、*(*(a +i)+j)、(*(a+i))[i].特别是对于a[0][0],它的等价表示形式有*a[0]和**a.数组元素a[i][j]的地址也有三种等价的表 示形式:a[i]+j、*(a+i)+j、&a[i][j] . 也可以定义指向二维数组中某行由若干个元素所组成的一维数组的指针变量。如代码 int (*p)[4]; 定义指针变量p能指向一个由四个int型元素组成的数组。指针变量p不同于前面介绍的指向整型变量的指针。在 那里,指向整型变量的指针变量指向整型数组的某个元素时,指针增减1运算,表示指针指向数组的下一个或前一个 元素。在这里,p是一个指向由四个整型元素组成的数组,对p作增减1运算,就表示向前进或向后退四个整型元素。 用例子说明指向由若干个元素所组成的数组指针的用法,如有变量定义 int a[3][4],(*p)[4] ; 则赋值p=a+l,使p指向二维数组a的第二行,表达式p+l的值为指向二维数组a的第三行。同二维数组元素的 地址计算规则相对应,若 P=a+1,则*p+j指向a[l][j];*(p+i) +j,或者p[i]则指向数组a的元素a[i+l][j].二维数组名 和指向数组的指针与数组元素位置之间的关系如图5.2所示。 5.3 指针数组和多级指针 1.指针数组 当数组元素类型为某种指针类型时,该数组就是指针数组。指针数组的定义形式为 类型说明符 *数组名[常量表达式] ; 例如, int *p[10] ; 定义指针数组p的每个元素都是能指向int型数据的指针变量,p有10个元素,它们是p[0] 、p[l]、„、p[9].和一 般的数组定义一样,数组名p也可作为p[0]的地址。 在指针数组的定义形式中,由于“[ ]”比“*”的优先级高,使数组名先与“[]”结合,形成数组的定义,然后再 与数组名之前的“*”结合,表示此数组的元素是指针类型的。注意,在“*”与数组名之外不能加上圆括号,否则变 成指向数组的指针变量。 引人指针数组的主要目的是便于统一管理同类的指针。如利用指针数组能实现对一组独立的变量以数组的形式对 它们作统一处理。如有以下定义: in a,b,c,d,e,f; int *apt[]={&a,&b,&c,&d,&e,&f}; 下面的循环语句能顺序访问独立的变量a、b 、c、d、e、f; for( k=0; k<6;k++) printf(“%dt”,*apt[k]);/*其中*apt[k]可写成**(apt+k)*/ 当指针数组的元素分别指向二维数组各行首元素时,也可用指针数组引用二维数组的元素。以下代码说明指针数 组引用二维数组元素的方法。设有以下代码: int a[10][20] ,i; int *b[10]; for(1=0;i<10;i++)/*b[i]指向数组元素a[i][0]*/ b[i]=&a[i][0] ; 则表达式a[i][j]与表达式b[i][j]引用同一个元素,即从指针数组方向来看,因b[i]指向元素a[i][0],*(b[i]+j)或 b[i][j] 引用元素a[i][j]. 另外,当指针数组的元素指向不同的一维数组的元素时,也可通过指针数组,如同二维数组那样引用各一维数组 的元素。如以下代码所示: char w0[ ]=“Sunday”,w1[ ]=“Monday”,w2[ ]=“Tuesday”, w3[ ]=“Wednesday”, w4[ ]=“Thursday”, w5[ ]=“Friday”, w6[ ]=“saturday”; char *wName[ ]={w0,wl,w2,w3,w4,w5,w6 }; 则语句for(i=0;i<=6;i++) printf(“%sn”, wName[i]); 输出星期的英文名称。代码wName[2][4]引用字符w2[4],其值为'd'. 以下例子把一维数组分割成不等长的段,通过指针数组,把一维数组当作二维数组来处理。 # include # define N 8 int p[N*(N+l)/2],i,j,*pt[N] ; void main() { for(pt[0]=p, i=l;i pt[i]=pt[i-1]+i; for(i=0; i pt[i][0]=pt[i][i]=l; for(j=l;j pt[i][j]=pt[i-1][j-1]+pt[i-1][j]; } for(i=0;i< N; i++) { printf(“%*c”,40-2*i,''); for(j=0; j<=i ;j++) printf(“%4d”, pt[i][j]) ; printf(“n”); } } 程序产生如下形式的二项式系数三角形: 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 1 5 10 1O 5 1 1 6 15 20 15 6 1 1 7 21 35 35 21 7 1 2.多级指针 当指针变量pp所指的变量ip又是一种指针时,呷就是一种指向指针的指针,称指针变量如是一种多级指针。定义 指向指针变量的指针变量的一般形式为 数据类型 * *指针变量名; 例如, int * *pp,*ip ,i ; ip=&i; pp=&ip; 定义说明pp是指向指针的指针变量;它能指向的是这样一种指针对象,该指针对象是能指向int型的指针变量。 如上述代码让pp指向指针变量ip,中指向整型变量i. 多级指针与指针数组有密切的关系。若有指针数组: char * lines[ ]= {“ADA”,“ALGOL”,“C”,“C++”,“FORTRAN”,“PASCAL” }; 则lines指针数组的每个元素分别指向以上字符串常量的首字符。在这里数组名lines可以作为它的首元素lines[0] 的指针,lines+k是元素 lines[k]的指针,由于lines[k] 本身也是指针,所以表达式 lines+k的值是一种指针的指针。 如有必要还可引入指针变量cp,让它指向数组lines的某元素,如cp=&lines[k].这样,cp就是指向指针型数据的指针 变量。在这里,cp是指向字符指针的指针变量,它应被定义成: char * *cp; 为了定义这样的 cp,它的前面有两个*号。由于*自右向左结合,首先是“* cp”表示 cp是指针变量,再有**cp 表示cp能指向的是某种指针类型,最后“char * *cp”表示指针变量cp能 指向字符指针数据对象。如果有赋值cp=& lines[l],让它指向数组元素lines[1],则* cp引用 lines[1],是一个指针, 指向字符串“ALGOL”的首字符。* *cp引用lines[1][0],其值是字符'A'.下面的代码实现顺序输出指针数组lines各元 素所指字符串: for(c=lines;cp Printf(“%sn”,*cp); 设有数组a[]和指针数组pt[]有以下代码所示的关系: int a[]= {2,4,6,8,10 }; int *pt[]={&a[3],&a[2],a[4],&a[0],&[1]}; int * *p; 下面的代码利用指针数组pt[]和指针的指针p,遍历数组a[]: for( p=pt; p printf(“%dt”,* *p); 第六章 函数 6.1 函数定义 在C程序设计中,将完成指定功能的C代码定义成函数,变成逻辑上一个相对独立的程序单位。函数定义需要指 明函数返回值的类型、函数名、函数的形式参数(常简称形参)和函数体(包括说明和定义及语句序列)。函数定义的 一般形式为 存储类型说明符 数据类型说明符 函数名(形式参数表) 形式参数说明序列 { 说明和定义部分 执行语句序列 } 存储类型说明符或省缺,或为static.省缺表示一个全局函数,static表示一个静态函数,只供同一源程序文件中的 函数使用。 数据类型说明符用来指定函数返回值类型,可以是基本数据类型、某种指针类型、结构类型等。但不可以是数组 类型。特别当函数不返回结果时,可用void明确指明函数不返回值。 数据类型说明符也可省缺,省缺被默认为返回int型值。 函数名是一个标识符。形式参数表是用远号分隔的若干形式参数,用不同的标识符指明各形式参数的名。形式参 数说明序列用来说明各形式参数的数据类型,相同数据类型的形式参数可以一起说明。现在编写C程序的习惯是形式 参数说明序列直接放在形式参数表中,即在形式参数说明表中顺序列出各形式参数的数据类型和形式参数的名称。如 是这样,一般形式的第一行全部内容称为函数头,也称为函数模型。 特别情况,函数可能不设形式参数,也就没有形式参数表和形式参数说明序列。但函数名后的一对圆括号是不可 以没有的。 一对花括号括住的部分称为函数体,函数体包括类型说明、变量定义和函数的执行语句序列。在函数体内可以有 return语句终止函数的执行。如函数有返回值类型,则return语句中一定要有表达式,作为函数调用的返回值。 6.2 函数调用 函数被定义以后,凡要实现函数功能的地方,就可简单地通过函数调用来完成。按函数调用在程序中的作用,有 两种不同类型的应用: (1)函数调用只是利用函数所完成的功能。此时,将函数调用作为一个独立的语句。其调用的一般形式为 函数名(实际参数表); 这种应用不要求或程序不利用函数的返回值。如程序中经常调用格式输入函数scanf()和格式输出函数printf() 等。 (2)函数调用是利用函数的返回值。其调用的一般形式为 函数名(实际参数表) 这种应用是利用返回值继续进行表达式的计算,或输出函数返回值等。 函数调用时提供的实际参数按它们出现的顺序与函数定义中的形式参数—一对应,并要求实际参数类型与其对应 的形式参数类型相一致。一般情况下,函数调用应为函数定义中的每个形式参数提供实际参数,当有多个实际参数时, 实际参数之间用逗号分隔。 函数调用的执行过程描述如下: (l)为函数的形式参数分配内存空间; (2)计算实际参数表达式的值,并将实际参数表达式的值赋给对应的形式参数; (3)为函数的局部变量分配内存空间; (4)执行函数体内的语句序列; (5)函数体执行完成,或执行了函数体内的return语句(若return语句带表达式,则计算出该表达式的值,并以 此值作为函数的返回值)后,释放为这次函数调用分配的全部内存空间; (6)将函数值(如果有)返回到函数调用处继续执行。 下面以简单的程序例子说明函数调用的执行过程。 「例6.l」函数调用时,由实际参数向函数形式参数传递值的示意程序。 # include double x,y,d; double min(double a,double b) {double temp; temp=a> b? b :a; return temp; } void main() { printf(“Enter x, y.n”); scanf(“%lf%lf”,&x,&y); d=min(x,y); printf(“MIN(%.3f,%.3f=%.3fn”, x, y,d) } 以上示意程序的大致执行过程如下: 首先执行主函数的第一个语句,调用格式输出函数输出提示信息。接着调用格式输入函数,等待用户输入数据。 用户看到程序输出的提示信息,输入数据,输入的数据被格式输入函数所接受,并将输入数据译成内部形式后,存入 变量X和y.接着执行赋值语句,求右瑞表达式的值。该表达式以x和y的值为实际参数,调用函数main()。对函数 min()的调用发生时,系统先保留好控制的返回点。在执行被调用函数min()之前,先为函数的形式参数a和b分 配存储单元,并以它们对应的实际参数表达式的植给它们赋初值。接着为函数内部的变量(称为局部变量)分配存储 单元。之后才开始执行被调用函数体中的语句。执行完函数体的语句,或执行了return语句,函数准备返回。在返回之 前先将形式参数和局部变量所占用的存储单元全部释放。函数返回时,将函数的返回值带回调用处,从原先保留的控 制返回点,继续执行,将函数调用的返回值存于变量d,输出结果,结束程序。综上所述,函数调用时,系统要做许多 辅助工作,函数调用时发生的数据传递最主要的是实际参数向形式参数传递数据和函数的返回值传递给调用处。为正 确编写函数,实现函数调用所希望的要求,需正确了解以下几项内容: (1)当函数执行return语句或执行完函数体的语句序列后,函数的这次调用就执行结束,随之将控制返回到函数 调用处继续执行。 (2)函数的返回值是通过执行return语句时,计算return之后的表达式值而获得的。如果函数不提供返回值,则 return语句不应包含表达式。 (3)如果函数有返回值,则应有确定的类型,并在函数定义时指明。同时,return语句的表达式类型应与函数定 义中指明的返回值类型相一致。 (4)为了明确指明函数不提供返回值,建议在函数定义时,在函数名之前写上void.并在这样的函数体内,所有的 return语句都不应该带表达式。 6.3 函数说明 C函数都是外部的,一般来说,任一函数都能被其它函数调用。而一个函数要调用另一个函数,应知道被调用函数 的一些有关如何正确调用的信息。调用函数与被调用函数之间在程序正文中可能会存在以下几种情况。 (l)调用同一程序文件中前面已定义的函数。 (2)调用处于同一程序文件后面定义的函数。 (3)调用别的程序文件中定义的函数。 对于第一种情况,因在函数调用处,被调用函数的详细信息已被编译程序所接受,在函数定义之后调用前面已定 义的函数,能方便地检查调用的正确性。对于后两种情况,这时因被调用函数的信息还未被编译程序所接受,不能检 查函数调用的正确性,所以在调用之前需对被调用函数有关调用的一些信息作出说明。如函数的返回值类型、函数名 和函数有关形式参数的个数及其类型等。这样的说明称作函数说明。函数说明的一般形式为 存储类型说明符 数据类型说明符 函数名(形式参数说明表); 其中存储类型说明符可以省缺,或写成extern.形式参数说明表可以为空,也可以顺序列出各形式参数的类型,同 样也可以顺序列出各形式参数的类型和形式参数的名。 6.4 函数调用中的数据传递方式 函数调用时,调用处与被调用函数之间会有数据传递发生。在C程序中,函数调用的数据传递方式有四种:实际 参数的数据值传递给形式参数(值传递方式)、实际参数的指针值传递给形式参数(地址传递方式)、函数以返回值传 递给调用环境(返回值方式)、调用环境与被调用函数共用全局变量(全局变量传递方式)。在地址传递方式中,实际 参数可以是传递一般变量的指针、数组某元素的指针、字符串某字符指针等。 1.实际参数向形式参数传递非指针数据 调用带形式参数的函数时,调用处将实际参数的值传递给被调用函数的形式参数。在此要特别说明以下几点: ( l)函数调用时实际参数为对应的形式参数提供初值,实际参数表达式的值是在执行函数体之前计算的,函数调 用中的实际参数可以是常量、变量或是一般的表达式。 (2)C语言规定,实际参数表达式对形式参数的数据传递是“值传递”的,是单向传递。如实际参数也是变量, 则实际参数变量与形式参数是不同的变量,实际参数变量的值传给形式参数,而不能由形式参数直接传回给实际参数。 在函数执行过程中,形式参数变量的值可能被改变,但这改变对原先与它对应的实际参数变量没有影响。 2.实际参数向形式参数传递指针 实际参数向形式参数传递变量的指针 函数可以设置指针类型的形式参数,调用带指针类型形式参数的函数时,对应指针形式参数的实际参数必须是相 同类型的指针(如胡同类型的某变量的指针),指针形式参数变量从实际参数处得到某变量的指针。指针形式参数对于 函数来说有特别的作用,它使函数得到了调用环境中某变量的地址,函数就可用这个地址间接访问函数之外的变量。 因此,指针类型形式参数为函数改变调用环境中的数据对象提供了手段。 如希望函数能通过形式参数改变任意指定变量的值,需要在三个方面协调一致: (1)首先,函数应设置指针类型的形式参数; (2)其次,函数体必须通过指针形式参数间接访问变量,或引用其值或修改其值; (3)最后,调用函数时,要以欲改变值的变量的指针为实际参数调用函数。 实际参数向形式参数传递数组元素的指针 为了能使函数处理不同的成组变量,应向形式参数传递数组元素的指针,最通常的情况是数组首元素的指针。由 于数组名能代表数组首元素的指针,所以常用数组名实际参数给形式参数传递数组首元素的指针。例如,用于求数组 前n个元素和的函数sun(),这个函数被正确地设置有两个形式参数:一个形式参数是数组元素的指针;另一个整型 的形式参数用于指定求和数组的元素个数。 「例6.2」求数组元素和的函数。 int sum( int *a, int n) { int i, s; for(s=i=0; i< n; i++) s+=a[i]; return s; } 利用函数sum(),如有以下变量定义: int x[]= {1, 2, 3, 4, 5 }, i, j; 则语句 i=sum(x,5); j=sum(&x[2],3); printf(“i=%dn j=%dn”, i,j); 将输出: i=15 j= 12 函数调用sum(x,5)将数组x的首元素地址(&x[0])传送给形式参数a;函数调用sum(&x[2], 3)将数组x 的元素x[2]的地址(&x[2])传送给形式参数a,而x[2]的地址就是数组元素段x[2]、x[3]、x[4] 的开始地址。 为了明确指明形式参数是数组元素的指针,形式参数的类型可以指定为数组类型的。如改写后的函数sum()定义 如下: int sum(int a[], int n) {int i,s; for(s= i=0; i s+=a[i] ; return s; } 对于数组类型的形式参数来说,函数被调用时,与它对应的实在数组由多少个元素是不确定的,可能会对应一个 大数组,也可能会对应一个小数组,甚至会对应数组中的某一段。所以在数组形式参数说明中,形式参数数组不必指 定数组元素的个数。任何数组形式参数说明: 类型 形式参数名[ ] 都可改写成: 类型 *形式参数名 函数形式参数也是函数的一种局部变量,指针形式参数就是函数的指针变量,函数sum()的定义又可改写成如下 形式: int sum(int *a, int n) { int s=0; for(; n——;) s+=*a++; return s; } 实际参数向形式参数传递字符串某字符的指针 这种情况要求形式参数为字符指针的,对应的实际参数是字符数组某个元素的指针,通常是字符串的首字符指针。 由于字符率是用一维的字符数组来实现的,所以字符指针形式参数与指向数组元素指针形式参数有相同的使用方法。 但因字符串的特殊性,在编写字符串处理函数时还会有许多技巧。下面以字符串拷贝函数strcpy()的实现为例说明字 符指针形式参数的用法。 「例6.3」字符串拷贝函数strcpy()。 该函数功能是将一个已知字符串的内容复制到另一字符数组中。拷贝函数设有两个形式参数from,为已知 字符串的首字符指针,to为存储复制字符串首字符指针。函数定义如下: void strcpy(char *to, char *from,) { while( *to++=*from++); } 3.调用环境与函数共用全局变量 为了减少函数的形式参数,或因若干函数必须共同对一组变量进行处理。可让调用环境与被调用的函数共用一组 变量。即在函数调用之前先给变量设置初值,函数对这些变量进行处理,并将处理结果留在全局变量中。由于这种使 用方式函数之间相互影响太大,如程序有错,就会很难修正。 6.5 返回指针的函数 函数也可以返回指向某种数据对象的指针值。定义(或说明)返回指针值函数的函数头有以下形式: 类型说明符 * 函数名(形式参数表) 例如,函数说明: int *f(int,int); 说明函数f()返回指向int型数据的指针,该函数有两个整型形式参数。 在函数名的两侧分别为* 运算符和()运算符,而()的优先级高于*,函数名先与()结合。函数名()是函数 的说明形式。在函数名之前的* ,表示此函数返回指针类型的值。 「例6.4」 编制在给定的字符串中找特定字符的第一次出现。若找到,返回指向字符串中该字符的指针;否则, 返回NULL值。 设函数为search(),该函数有两个形式参数,指向字符串首字符的指针和待寻找的字符。以下是函数search() 的定义: char *search(char *s,char c) { while(*s && *s! = c) s++; return *s?s:NULL; } 6.6 函数递归调用 一个函数为完成它的复杂工作,可以调用其它别的函数。例如,从主函数出发,主函数调用函数A() ,函数A ()又调用函数B(),函数B()又调用函数C(),等等。这样从主函数出发,形成一个长长的调用链,就是通常所 说的函数嵌套调用。函数嵌套调用时,有一个重要的特征:先被调用的函数后返回。如这里所举例子,待函数C()完 成计算返回后,B()函数继续计算(可能还要调用其它函数) ,待计算完成,返回到函数A(),函数A()计算完 成后,才返回到主函数。 当函数调用链上的某两个函数为同一个函数时,称这种函数调用方式为递归调用。通过速归调用方式完成其功能 的函数称为递归函数。许多问题的求解方法具有递归特征,用递归函数描述这种求解算法比较简洁。计算n的阶乘(n!) 函数就是一个很好的例子。因 n! = l*2*3* „*n 按其定义用循环语句可以方便地实现,写成函数见下例6.5. 「例6.5」用循环实现阶乘计算的函数。 float fac(int n) {float s; int i; for(s=,i=l;i<=n; i++) s*= l; return s; } 然而,把n! 的定义改写成以下递归定义形式 (l)n!=1, n<=l; (2)n!= n*(n-1)!, n>l. 根据这个定义形式可用递归函数描述如下例6.6. 「例6.6」 用递归实现阶乘计算的函数。 float rfac(int n) { if( n<=1) return 1.0f; return n*rfac(n-1) ; } 以计算3! 为例,说明递归函数被调用时的执行过程。设有代码m= rfac(3) 调用函数rfac()。函数调用rfac (3) 的计算过程可大致叙述如下: 以函数调用rfac(3) 去调用函数rfac() ;函数rfac(n=3) 为计算3*2! ,用rfac(2) 去调用函数rfac(); 函数rfac(n=2) 为计算2*1!,用rfac(1)去调用函数rfac();函数 rfac(n=l) 计算1! ,以结果1.0返回;返 回到发出调用rfac(l) 处,继续计算,得到2! 的结果2.0返回;返回到发出调用rfac(2) 处,继续计算得到3! 的 结果6.0返回。 递归计算n! 有一个重要特征,为求n有关的解,化为求n-l的解,求n-1的解又化为求n-2的解,如此类推。 特别地,对于1的解是可立即得到的。这是将大问题解化为小问题解的递推过程。有了1的解以后,接着是一个回溯 过程,逐步获得2的解,3的解,„„,直至n的解。 「例6.7」 用递归函数实现数组元素的求和计算。 要采用递归方法计算数组元素的和,可把数组元素的累计和等于当前元素与数组其余元素的和,而对数组其余元 素的和通过递归实现。下面的函数定义是这样的解法之一。 int rsum(int *a, int n) { if( n==0) return 0;/*若数组没有元素,则返回0*/ return *a+rsum(a+l,n-1);/*当前元素与其余元素的和*/ } 第七章 结构型与自定义类型 7.1 结构型和结构变量 1.结构型 结构型是由若干独立意义成员组成的复杂数据。定义一个结构型的一般形式为 struct 结构型名{ 数据类型1 成员1名; 数据类型2 成员2名; „„ 数据类型n 成员n名; }; 其中关键字“struct”引出结构型的定义。用花括号括住结构型的成员说明表,指明组成此结构型全部成员的数据 类型和名称。结构型的成员也称为域。如某个系统的学生数据实体用如下结构型来描述: struct stdType { int num;/*学号*/ char * name;/*姓名,允许姓名字符串长度可变,按需要申请*/ char sex;/*性别'M' 表示男生,'F'表示女生*/ int age;/*年龄*/ int score;/*成绩*/ char *addr; /*家庭地址,允许地址字符串长度可变,按需要申请*/ } ; 上例定义结构型struct stdType,有六个成员。实际上,凡是相关的若干数据对象都可组合成一个结构,在一个结构 名下进行管理。 结构成员的数据类型可以是任何类型,包括前面定义的其它结构型,但是结构不能包含自身,而允许结构中可有 这样的指针成员,指向如同定义一样的结构。 2.结构变量 结构型的变量就是结构变量,程序要使用结构数据,需定义结构变量。结构变量也简称结构。定义结构变量有以 下几种不同的方法。 (l)先定义结构型,再定义结构变量。如利用前面已定义的结构型struct stdType,以下代码 struct stdType st1,st2,stdArray[200] 定义结构变量st1、st2和结构数组stdArray. (2) 在定义结构型时,同时定义结构变量。如代码 struct point {/*某绘图程序的坐标类型*/ int x; int y; } p1,p2; 定义struct point型变量 p1、p2. (3) 在定义无名结构型时,同时定义结构变量。如某种形式的结构型只是一次性定义几个变量,可以省略结构 型名,直接定义结构变量。如由日、月、年组成的日期结构变量为: struct int day; /*日*/ int month;/*月*/ int year;/*年*/ } date1,date2;/*定义两个日期变量datel和date2*/ 3.结构变量初始化 在定义结构变量时,可同时给它置初值,称为结构变量初始化。结构变量初始化时,要按其结构型定义中的成员 顺序逐一给出各成员的初值。如 struct point2 /* 说明绘图程序的坐标类型*/ int x; int y; } p3={ 20,50}; 结构变量初始化时,对初值表达式的要求与数组初始化对初值表达式的要求相同。 4.结构变量的引用 结构变量定义后,就可以用结构变量的名引用结构。 ANSI C还允许相同类型的结构变量相互赋值。 5.结构变量成员的引用 引用结构变量成员的标记形式为: 结构变量名。成员名 其中“。” 称为成员运算符。例如,引用结构变量datel的year成员。因该成员的类型为int型的,可以 对它施行任何int型变量可施行的运算。例如,赋值运算=2000.如结构变量的某成员又是结构,要引用其成员 的成员,则继续用成员运算符'.'接上更内居的成员名。如结构变量的某成员是数组,要引用其成员的元素,则继续用数 组元素的下标引用结构成员的元素。 结构有多个成员,通常结构成员顺序地被安排在结构变量的内存决中,结构变量的地址是结构所占内存的开始地 址,写成 & 结构变量 而结构成员的地址写成 & 结构变量。成员名 6.结构数组 一般地,常用结构描述有复杂数据信息的个体,而用数组描述个体的集合。当数组的元素是结构时,这种数组就 称为结构数组。如用结构型描述单个学生,而用结构数组表示一个班的学生。 与定义结构变量的方法相仿,在前述所有定义结构变量的方法中,在变量名之后指定元素个数,就能定义结构数 组,也可赋初值。 如同元素为标准数据类型的数组一样,结构数组各元素在内存中也顺序存放,初始化时,逐一给数组的每个元素 指定结构初值。 对结构数组元素的访问也是利用数组元素下标的引用方法,若引用结构数组元素的成员, 再用结构成员的引用方法,写成以下形式: 结构数组名[元素下标]. 成员名 即首先是指定数组的元素,再指定结构的成员。 引用结构数组元素成员地址的标记方法为 & 结构数组名[元素下标]. 成员名 引用结构数组元素地址的标记方法为 & 结构数组名[元素下标] 引用结构数组首元素地址的标记方法为 结构数组名 7.结构指针变量 把结构变量s所占据的存储段开始地址赋给能指向该结构的指针变量p,就说指针p指向结构变量s.指针p是一个 结构指针变量,简称结构指针。定义结构指针的方法,与定义一般指针变量一样,当类型区分符是结构型时,所定义 的指针变量即为结构指针。如代码 struct date *pd, d; 定义结构指针pd和结构变量成其中,指针变量pd能指向类型为struct date的结构。赋值代码 pd=&d,使指针pd 指向结构变量d. 由指向结构的指针引用结构成员的标记形式为 结构指针变量名->成员名 其中“ ->”称为指向成员运算符。例如,如下代码: pd->day /* 引用结构变量d的成员day*/ pd_>month /*引用结构变量d的成员month */ pd-> year /* 引用结构变量d的成员year */ 表达式“* 指针变量” 表示指针变量所指对象,所以通过指针引用其所指结构的成员也可写成以下形式: (*指针变量)。 结构成员名 这里圆括号是必需的,因为运算符“*”的优先级低于运算符“。”,但是几乎不用这种标记方法,习惯都采用指向 成员运算符“->”来标记。 8.指向结构数组元素的指针 结构指针变量也可指向结构数组的某个元素。如有定义: struct stdType std[50], *ps,*p; 代码: ps=&std[2]; p=std; 使指针ps指向结构std[2],指针p指向结构std[0]. 通过指针引用它所指数组元素的成员与指向普通结构一样,如代码 ps-> score引用std[2].score;而代码 p-> score 引用std[0].score. 当结构指针ps指向结构数组的元素std[k] 时,表达式ps+n表示指向结构数组元素 std[k+n].利用指向结构数组元 素的指针,引用结构数组元素的成员又有多种表示形式: (l) 利用结构数组元素的指针引用它所指数组元素的成员: 指针变量->成员名 /* 几乎都这样用 */ 指针变量[0]. 成员名 /* 几乎不用 */ ( *指针变量)。成员名 /* 几乎不用 */ (2) 利用结构数组元素的指针引用离它所指元素i个元素的成员: ( 指针变量+i)->成员名 /* 常用 */ 指针变量[i].成员名 /* 最常用 */ ( *(指针变量+i))。成员名 /* 几乎不用 */ 以下代码序列实现在结构数组std的前n个元素中找最高分的那个结构,并由指针p指向该结构: P= std; /* 等价于p=&std[0] */ for(ps=p+1; ps if(ps-> score> p-> score) p=ps; 「例 7.1」 以下函数 dayofYear()利用月份天数表,已知日、月、年,计算年中的第几天。 int dTbl[][12] ={{31,28,31,30,31,30,31,31,30,31,30,31},/*平年*/ {31,29,31,30,31,30,31,31,30,31,30,31}}; /* 闰年 */ int dayofYear( int d,int m,int y) /*计算年中第几天 */ { int i, leap, day=d; leap=(y%4==0 && y%100)||y%400==0; for( i=0; i day += dTbl[leap][i]; return day; } 9.在函数间传递结构数据 在函数间传递结构数据主要有以下几种形式: (l) 共用全局的外部结构变量。 (2) 函数设置结构形式参数。 (3) 函数返回结构值。 (4) 函数设置结构指针形式参数。 以函数 dayofYear()为例,设有以下形式的结构类型 struct date,以该类型的结构为形式参数,改写该函数为 struct date { int day; int month; int year; int yearDay; char * monthName; } date; int dayofYear(struct date d) { int i,leap,day=; leap =(%4==0 && %l00) ||%400==0; for( i=0;i<-1;i++) day += dTbl[leap][i]; return day; } 调用带结构形式参数的函数,必须提供与形式参数相同类型的结构变量实际参数。主函数中对函数dayofYear() 的调用应改写成: y=dayofYear(date); C语言允许函数返回结构型值,如将函数dayofYear()改为设置struct date类型的形式参数,并返回struct date类 型的值。对函数dayofYear()的新的改写如下: struct date dayofYear(struct date d) { int i, leap; y=; leap=(d. year%4==0 && %100) ||%400==0; for(i=0;i<-1;i++) y+=dTbl[leap][i]; return d; } 主函数调用函数dayofYear()把返回的结构值赋给结构变量 date: date=dayofYear(date) ; 再改写函数dayofYear(),使它的形式参数是以struct date结构指针为形式参数。 void dayofYear(struct date *dp) { int i,leap,day =dp-> day; leap =(dp -> year%4==0 && dp -> year%l00) ||dp ->year%400==0; for(i=0;i< dp -> month-1;i++) day+=dThl[leap][i] ; dp -> yearDay=day; } 改写后的函数通过指针形式参数引用结构成员,并将计算结果存放在结构的相应成员中,不再返回结果。对该函 数的调用方式也需相应地改写成: dayofYear(&date); 7.2 共用型和共用型变量 1.共用型 在某些特殊应用中,要求某数据对象在程序执行的不同时期能存储不同类型的值。C语言的共用型能满足这个要求。 共用型的成员从同一地址开始存储。但任一时刻只存储其中一个成员,由最近放入的内容决定该区域究竟是哪一个成 员的值。分配给共用型的存储区域大小,要求至少能存储其中最大一种成员。定义共用型的一般形式为 union 共用型名 { 数据类型1 成员1名; 数据类型2 成员2名; „„ 数据类型n 成员n名; }; 例如,下面定义的共用型(union udata) 能存储整型,或字符型,或浮点型的数据: union udata { int ival; char chval; float fval; }; 2.共用型变目的定义 与结构变量的定义方法一样,共用型变量的定义方法有以下几种: (1) 先定义共用型,然后定义共用型变量、共用型数组、共用型指针等。 (2) 定义共用型同时定义共用型变量、共用型数组、共用型指针等。 (3) 定义无名共用型同时定义共用型变量、共用型数组、共用型指针等。 3.共用型变目初始化 共用型变量定义时,也可初始化,但只能对共用型中的第一个成员初始化。 4.引用共用型变目的成员 引用共用型变量成员的方法与引用结构变量成员的方法相同。共用型也可出现在结构和数组中,共用型也可包含 有结构和数组。引用结构中的共用型或共用型中的结构的书写形式与引用嵌套结构成员的书写形式一样。例如,以下 代码 Struct { char name[30]; /* 标识符 */ int uflag; /* 存于共用型成员中的值的类型 */ union /* 存储变量值 */ { int ival; /* 当变量为整型时 */ char chval /* 当变量为字符型时 */ float fval; /*当变量为浮点型时 */ } uval; } symTbl[1000]; /* 变量表 */ 定义了一个结构数组symTbl.用symTbl[50].引用结构数组symTbl中的第50个结构的共用型成员uval的fval (视其中的共用型为浮点型数据)。 函数的形式参数不能是共用型类型,函数的结果也不能是共用型类型。但指向共用型的指针可以作为函数形式参 数,函数也可以返回指向共用型的指针。 7.3 枚举型和枚举型变量 1.枚举型 除数字、文字信息之外,还有专用名称信息,如反映电梯运行状态的有上(UP) ,下(DOWN) ,停(sTOP) ; 又如表示星期几的名称等。为提高程序描述问题时的直观性,引入枚举型。程序用枚举方法列举一组标识符作为枚举 型的值的集合。当一个变量具有这种枚举型时,它就能取枚举型的标识将值。定义枚举型的一般形式为 enum 枚举型名 {枚举常量1,枚举常量2,„„,枚举常量n}; 其中enum是枚举型的引导字,枚举型名是标识符,枚举常量也是用户指定的标识符,但它们被程序看作常量,习 惯称它们为枚举常量。例如, enum weekday { SUN, MON, TUE, WED, THU, FRI, SAT}; 通常,每个枚举常量都是有意义名称符号,但对程序本身来说,这些枚举常量并不自动代表什么含义。例如,并 不因为写成SAT就自动表示“星期六”,不写SAT写成SATDAY或任何其它标识符也是可以的。对于编译系统来说, 枚举型中的标识符只是一组互不相同的标识符而已,标识符本身的字面意义只是供阅读程序的人便于理解程序。 为了便于处理枚举型,编译系统将每个枚举常量与一个整数相联系,即枚举常量在内部被视作一个整数,值的大 小由它们在枚举型中出现的顺序确定,依次为0,l,2,„。如在上面的定义中,SUN值为0,MON值为1,„„,SAT 值为6.枚举型变量的值也可输出。例如, printf(“%d n”, SUN); 将输出整数0. 枚举常量的对应整数也可由程序直接指定。如 enum weekday { SUN= 7, MON= l, TUE, WED, THU, FRI, SAT}; 指定SUN为7,MON为1,后面未指定对应整数的枚举常量所代表的整数,则是前一个枚举常量代表的整数加1. 所以在上述定义中,TUE为2,„„,SAT为6. 因枚举常量代表一个整数,同一枚举型的变量、枚举常量或整数相互间都可以作关系比较。 2.枚举型变量 定义枚举型变量也有以下多种方法: (l) 先定义枚举型,然后定义枚举型变量、枚举型数组、枚举型指针等。 (2) 定义枚举型同时定义枚举型变量、枚举型数组、枚举型指针等。 (3) 定义无名枚举型同时定义枚举型变量、枚举型数组、枚举型指针等。例如: enum weekday today, yesterday, tomorrow; enum { RED, YELLOW, BLUE } color; 定义枚举型 enum weekday的变量 today,yesterday,tomorow;定义枚举型变量 color.例如, today=SUN; tomorrow = MON; yesterday =SAT ; color= YELLOW; 使用枚举型,除能命名见名议意的标识符外,对标识符值的内部实现,程序员可以不必考虑。另外,一个变量具 有枚举型,还能反映变量值的有限性。枚举型变量常用于循环的控制变量,枚举常量用于多路选择控制的情况。 7.4 用户自定义类型 C语言也提供类型定义外化成为类型命名的机制。让用户定义新的类型,并用这些新类型定义变量。用户自定义类 型的方法为: typedef 类型 用户自定义类型名; 其中类型可以是基本类型,也可以是前面用户自定义的类型,也还可以是任何C语言允许的类型描述,如数组类 型、结构型、共用型、枚举型,及各种指针类型。用户自定义类型名是标识符,以后就可用该用户自定义类型名定义 相应类型的变量。例如, typedef int INTEGER; tyPedef struet { int num; char * name; char sex; int age; int score; } stdType; /*定义结构型stdType */ typedef int INTARRAy[20] ; /* 含 20个整数的数组类型 INTARRAY */ typedef enum { RED, YELLOW, BLUE } COLOR; /* 枚举型COLOR */ typedef char *CHP; /* 定义字符指针类型CHP */ 利用以上类型定义,可定义变量如下: INTEGER X,Y; /* 定义int类型变量 x和 y */ stdType std1,std2; /* 定义两个结构变量 */ INTARRAY v1, v2; /* 定义两个各含20个整数的数组 */ COLOR c1,c2; /* 定义两个枚举变量 */ CHP cp1, cp2; /* 定义字符指针变量cpl和cp2 */ 在以上变量定义中,对于结构、枚举等类型,不必再冠相应的类型类别关键字。特别对于数组类型,当有多个数 组变量成员类型相同、数组元素个数也相同时,先用typedef定义一个数组类型,然后再定义数组变量就比较方便、简 洁。 通常,在组织复杂的程序时,不同源程序文件中用到的同一数据类型,如数组、结构、共用型、指针等,常用外 typedef定义来给有关数据类型命名,并将这些类型定义单独放在一个源文件中,凡要用到它们的源文件,就用# include 预处理命令将它包含进来。 第八章 文件 8.1 C文件概述 1.文件引用规则 为使计算机程序能处理大量的数据信息,常将数据存储在计算机外部存储介质中,如磁带、磁盘等。计算机操作 系统将存储在外部存储介质中的数据以数据流的形式来组织。每个独立的数据流称作文件,每个文件有一个名字。为 便于管理文件,操作系统维持一个呈层次状的目录结构,每个文件都被登录在某一目录下。习惯也将从键盘输入的数 据流和向显示屏或打印机输出的数据流称作文件。引用文件可由以下几部分组成: 盘符:路径文件名。扩展名 其中盘符表示文件所在存储块,系统将外部存储介质分成多个存储块,并用不同的盘符标识这些块。路径是文件 所在目录层次,文件名和扩展名通常是由字母开头、字母和数字符组成。扩展名可以多至3个字符,通常用来表示文 件的属性。因操作系统保留着当前盘和当前路径,若要引用当前盘或当前路径下的文件,盘符和路径可以省略。 2.文件的打开和关闭 由于文件存放在磁盘上,程序要处理文件上的数据,必须先将文件中的数据读人到内存;反之,程序要将产生的 数据永久保存,就应将数据写到文件中。文件受操作系统管理,程序要使用文件,就要请求操作系统,让程序与某文 件之间建立某种联系,习惯称程序与文件建立联系的过程为文件打开;反之,撤消程序与文件联系的过程为文件关闭。 所以,程序要使用文件,先要打开文件;程序使用文件结束后,应及时关闭文件。 3.文件缓冲技术 在现代计算机系统中,程序读文件中的数据或写数据到文件,都在操作系统控制下完成。若程序要从文件读人一 个数据,操作系统会一次性地读入一大块数据暂存于内存中,供程序以后再读入时使用。程序向文件写数据时,也不 是立即将数据写到文件中,而是暂时存于某个内存块中,待内存块写满,或程序明确告知写文件结束后,再将数据写 到文件中。这种文件数据读写技术称为缓冲。文件读写采用缓冲技术的系统称为缓冲文件系统。在缓冲文件系统中, 暂存输入输出数据的内存块称为文件缓冲区。不采用缓冲技术,操作系统直接按程序要求完成输入输出的系统称为非 缓冲系统。 操作系统为了控制和完成文件读写操作,为每个正与程序相联系的文件设有一个控制块,在控制块中记录文件的 名称、文件的属性、文件当前读写位置、文件缓冲区开始地址、文件当前读写位置所对应缓冲区的位置等等。文件缓 冲区和文件控制块都由系统分配和受系统控制。 4.二进制文件和文本文件 文件按其数据信息的存放格式分类,文件可分二进制文件和文本文件两种。二进制文件中的数据是按二进制方式 存放,即以数据在计算机内存的存放格式将数据存储在文件中。将数据转换成字符列,每个字符又以字符的代码(例 如,ASCII代码)存储的文件称为文本文件。一般来说,二进制文件比文本文件更紧凑,并在数据传输时不必进行格式 转换,常用于计算机与计算机之间、计算机与外部设备之间传输数据用。由于文本文件以字符的代码存储,输出内容 能让人直接阅读,常用于人与计算机之间通信时使用。 5.顺序文件和随机文件 文件按读写方式分,可以把文件分为顺序文件和随机文件。顺序文件要求文件读写从文件头开始,读或写操作顺 序进行。若临时要读取文件中间的某个数据,必须从头开始读,直至读人要读的数据;若在文件某位置要写入新的数 据,也必须从文件的第一个数据开始顺序读取和复写,并在要改写的数据写入后,还要继续读取和复写其后的全部数 据。随机文件允许随机地读取或改写文件任一位置上的数据。 C语言本身未提供有关文件操作的输入输出语句,但对文件的打开、关闭和读写操作都可用系统提供的库函数来实 现。程序可用它们对文件作各种复杂的处理。 6.设备文件 系统将常规设备上的输入输出数据流称为标准文件,程序运行前,系统自动打开这些标准文件。它们是标准输入 文件、标准输出文件、标准出错输出文件和标准打印输出文件。系统自动定义了这些标准文件的文件指针,它们依次 是stdin、stdout、stderr和stdprn,供程序直接使用。 程序除能直接使用前面各章都使用的不带文件指针的标准输入输出库函数外,也可对它们使用下面介绍的带文件 指针的一般形式的输入输出库函数。如stdin,就是指从终端输入数据;stdout,就是向终端输出数据。 7.文件类型和文件类型指针变量 为了正确地完成文件读写,操作系统为每个正被程序使用的文件在内存中开辟一个存储区,用于存放有关对文件 进行操作所需的控制信息(简称控制块)。如文件名、文件读写状态。文件缓冲区大小和位置、当前读写位置等。控制 块是一个结构变量,其类型由系统预定义,取名为FILE,习惯称文件类型。程序通过指向该控制块的指针调用系统提 供的文件处理库函数。 程序在使用文件前,先调用文件打开函数。打开函数为将要使用的文件指定一个FILE类型的结构变量,并返回该 结构的指针。系统通过指向该结构的指针来引用结构中的文件控制信息,实现正确读写对应的文件。 程序要使用文件,就要定义FILE类型的指针变量(称文件指针变量)。例如, FILE * fp; 定义如是一个文件指针变量,它能指向前述类型为FILE的文件控制块结构变量。 8.文件打开库函数 fopen() 在读写文件之前,先得打开文件。打开文件可使用库函数fopen() .调用函数fopen() 的一般形式为 fopen(文件名,使用方式) 其中文件名(可能还包括盘符和目录路径) 为字符串表达式。使用方式也是一个字符串,用来指明文件的读写方 式。函数fopen() 将返回文件控制块结构变量指针,程序应将调用函数fopen() 返回的指针值赋给某个文件指针变量来保存。如语句 fp = fopen(“”,“r”); 以文件读方式打开根目录下的usr4子目录中的文件。 调用函数fopen()时,可能会因某种原因不能打开文件。如读方式下打开一个不存在的文件;在写方式下,外部 存储介质已无剩余的自由空间,或外设故障,或超过系统能同时打开的文 件数;等等。文件不能打开时,函数fopen()将返回一个空指针值NULL.程序应考虑到文件不能正常打开的极端 情况,所以常用以下形式的C代码描述打开一个文件的要求: if((fp =fopen( filename,“r”))==NULL) { printf(“Can not open %s file.n”, filename); exit(0); /* 结束程序的执行,回到环境或操作系统 */ } 以上代码以读方式打开一个文件,其中filename是某文件名字符串表达式。上述代码在调用函数fopen()后立即 检查打开是否成功,如果打开不成功,就在终端上输出该文件不能打开字样,调用exit函数。exit函数是系统提供的函 数,该函数的执行将释放程序的全部资源,终止程序的执行。调用该函数时需指定一个整数,该整数将作为程序终止 时给系统的一个返回值。若程序使用该函数,应在程序的头写上包含stdio.h头文件的预处理命令。 关于函数fopen()的使用方式参数,说明以下几点: (1)用“r”方式打开的文件只能用于从文件输入数据,不能用于输出;而且要求该文件已经存在,否则函数fopen () 返回NULL值。 (2)用“w”方式打开的文件只能用于向文件输出数据,不能用于输入。如打开时,原文件不存在,则新建立一 个以指定名字命名的文件;如原文件已存在,则原文件上的数据被全部删除。 (3)如希望打开文件用于写,又不要删除原文件中的数据,并从原文件的末尾开始添加新的数据,应该用“a” 方式打开。 (4)用“r+”、“w+”、“a+” 方式打开的文件可以输入数据,也可以输出数据。用“r+”方式只允许打开已存 在的文件,以便程序能输入数据;用“w+”方式打开,则新建立一个文件,先是 向文件输出数据,然后可以从该文件读人数据;用“a+”方式打开一个已存在的文件,位置指针先移到文件的末 尾,准备添加数据,以后也可以输入数据。 ( 5)要打开二进制文件,只要在对应正文文件打开方式中接上字符b即可,如“rb”表示以输入方式打开二进制 文件。 正文文件与二进制文件在使用时,还有一点不同。对于正文文件,输入时,回车符和换行符合成为一个换行符输 入;输出时,换行符('n')转换成为回车符和换行符两个字符一起输出。对于二进制文件,不进行上述这种转换。 9.文件关闭库函数fclose() 在使用完一个文件后,程序应该立即关闭它,以防止后继执行的程序语句错误或人为的误操作破坏正打开着的文 件。关闭文件可调用库函数fclose()来实现。调用函数fclose()的一般形式为 fclose(文件指针); 例如, fclose(fp); 调用函数fclose()的作用是使文件指针变量终止原先调用函数fopen()时所建立的它与文件的联系。调用函数 fclose()之后,不能再通过该文件指针变量对其原先相连的文件进行读写操作,除非被再次打开。文件被关闭后,原 文件指针变量又可用来打开文件,或与别的文件相联系,或重新与原先文件建立新的联系。 8.2 文件处理程序结构和文件输入输出常用库函数 1.正文文件输入处理 从正文文件逐一输入字符,对输入的字符作某种处理的程序结构有: int c; /* 不能为char类型 */ „ /*说明有关变量和设置初值等 */ fp=fopen(文件名,“r”); /* 正文文件以读方式打开 */ while(( c= fgetc(f))!= EOF) { „ /* 这里对刚读人的存于C中的字符作某种处理 */ } fclose(fp); „„/*输出处理结果 */ 其中函数 fgetC()的说明形式为 int fgetc(FILE *fp) 该函数的功能是从与中相联系的文件中读人下一个字符。在文件的控制块中,有一个当前读字符的位置信息,每 读入一个字符后,在文件还未结束情况下,这个当前位置信息就移向其后一个字符,从而保证程序反复调用函数fgetc () 能顺序读人文件中的字符。函数fgetc() 的返回值就是读入字符的ASCII代码值。读八字符时,如遇到文件结 束,函数返回文件结束标记EOF.对于正文文件,由于字符的ASCII代码不可能是-1,因此可用EOF(定义为-1) 作为 文件结束标记。 「例8.1」 输入正文文件,统计文件中英文字母的个数,并输出。 为使程序更有一般性,设程序要统计的正文文件名在程序启动时由输入指定。 # include FILE *fp; int main() { int count, ch; char fname[40]; printf(“输入文件名!n”); scanf(“%s%*c”, fname) ; /* 读入文件名和名后的回车符 */ if((fp = fopen(fname,“r”))== NULL) { /* 以读方式打开正文文件 */ printf(“Can not open%s file.n”, fname); return 0; /* 程序非正常结束 */ } count= 0; while((ch =fgetc(fp))! =EOF) { / * 这里对刚读人的存于ch中的字符信息作某种处理 */ if(ch>='a' && ch<='z' || ch>='A' && ch<='Z' ) count++; } fclose(fp); /*输出处理结果 */ printf(“文件%s有英文字母%d个。n”, fname, count) return l; /* 程序执行正常结束 */ } 2.二进制文件输入处理 从二进制文件逐一输入字节,并作某种处理的程序结构有: char c; /* 也可以是 int类型 */ „ /* 说明有关变量和设置初值等 */ fp = fopen(文件名,“rb”); while(! feof(fp)) { c=fgetc(fp); „ /* 这里对存于C中的字节信息作某种处理 */ } fclose(fp); /* 输出处理结果 */ 其中函数feof() 用来判断文件是否结束。函数调用feof(fp) 用来测试与fp相联系的文件当前状态是否为“文 件结束”状态。如果是文件结束状态,函数调用feof(fp)返回非零值,否则返回零值。函数feof()也可用于测试正 文文件。 对于二进制文件,一般不能以读人字节的值是否为-1来判定二进制文件是否结束,而应该用函数feof()。 3.字符(或字节)逐一生成形成新文件的程序结构 正文文件与二进制文件的生成程序除文件打开方式不同外,它们的程序结构基本相同。字符(或字节)逐一生成 输出,形成新文件的程序结构有: int c; /* 也可以是char类型 */ „ /* 说明有关变量和设置初值等 */ fp = fopen(文件名,“W”); /* 或fP=fopen(文件,“wb”) */ while(还有字符(或字节)) { „ /* 生成字符(或字节)存于变量c */ fpute( c,fp); /* 将生成的字符(或字节)输出 */ } fclose(fp); „ /* 输出程序结束报告 */ 这里的函数fputc()的说明形式为 int fputc( char ch, FILE *fp) 该函数的功能是将ch中的字符输出到与fp文件指针相联系的文件中。函数fputc()返回一个整数值。如果输出 成功,则返回值就是输出字符的ASCII代码值;如果输出失败,则返回EOF,即-1. 曾介绍过的函数putchar()是在stdio.h中用函数fputc()定义的宏: # define putchar(c) fputc(c, stdout) 用宏putchar(c) 比写fputc(c,stdout) 在概念上更简单一些。从使用者来说,可以把putchar(c) 看作函数 调用,不必严格地称它为宏调用。 4. 数据成块输入/输出函数 为加快程序的处理速度,程序可以成批地读人文件中的数据,也可成批地写数据到文件。它们是函数fread()和 fwrite()。 成批读函数fread()的说明形式为int fread(char *buf,int size, int count, FILE *rfp); 成批写函数fwrite() 的说明形式为 int fwrite( char *buf, int size, int count,FILE *wfp); 其中,buf是字符数组首元指针。对fread()来说,它是读人数据的存放开始地址;对bote()来说,是要输出数 据的开始地址。size是读写的数据块的字节数。count为要进行读写的数据块的个数。rfp和wfp为文件指针。调用上述 函数共读写size. count个字节或字符。函数fread()和fwrite()的返回值是实际完成输入或输出的数据块的个数。一 般情况下,输出调用成功,返回值为count的值。 如果是读写二进制文件,用函数fread()和fwrite()可以读写任何类型的信息。如有一个如下形式的通信录结 构类型: typedef struct { char name[21]; /* 名字 */ char phone[15]; /* 电话 */ char zip[10]; /* 邮编 */ char addr[31]; /* 地址 */ } infoType; 利用类型infoType,可定义数组,如: infoType info[30]; 表示结构数组info[]能存放30个通信录数据。而下面的两个函数调用能分别实现20个通信录信息从某文件读出和 写入某文件: fread( info,sizeof(infoType),20,rfp); fwrite( info,sizeo(infoType),20,wfp); 5.格式输入/输出库函数 与用函数scanf()从标准设备输入和用函数printf()向标准设备输出一样,一般文件也可进行格式输入和格式输 出。 函数fscanf()和fprintf()分别能对一般文件进行格式输入和格式输出。它们的调用形式分别为 fscantf(文件指针,格式字符串,输入项地址表) 和 fprintf(文件指针,格式字符串,输出项表) 例如, fscanf(rp,“%d%f”,&i,&r); fprintf(wp,“i=%d, r=%6.4fn”, i,r); 前者表示从与叩相联系的文件为变量i和r读人数据;后者表示将整型变量i和实型变量r的值按格式输出到与呷 相联系的文件上。 6.字符串输入/输出库函数 函数fgets()和fputs()分别用于从正文文件输入字符串和向正文文件输出字符串。它们的说明形式分别为 char *fgets(char *str,int n, FILE *fp) 和 fputs(char *str, FILE *fp) 函数fgets() 用于从文件读取字符序列,并存于字符指针所指出的存储区域中。当连续读人n-1个字符,或遇到 换行符时,读字符过程结束。 函数fgets()与函数gets()都在读人的字符序列之后自动存储字符串结束标记'0',使其成为字符串,并返回字 符率的首字符指针。但它们有差别,函数fgets()除增加整型参数和文件指针参数之外,还在读到换行符时,存储换 行符,而函数gets()不存储换行符。 函数fputs()的作用是将字符串复制到文件。其中字符串的结束标记符是不复制的,也不在复制的字符序列之后 另外再添加换行符,这一点与函数puts()不同。 7.文件定位库函数 利用前面介绍的函数进行文件操作,只能顺序读写文件。然而,在有些场合,还需要能对文件作随机存取。实际 上,因文件是存储在外部存储介质上的数据流,程序即刻能读写的是文件某一位置上的数据,这个位置称为文件的当 前读写位置。对于顺序读写情况,每读写一个字符后,当前位置就自动向后移一个字符位置。对于允许随机读写的文 件,要读写其他别的位置上的数据,就得改变文件当前读写位置。实现这样的移动可调用库函数rewind()和fseek() 来实现。另设有函数ftell()用于查询文件当前读写位置。 函数rewind()的说明形式为 void rewind( FILE *fp) 函数rewind()的作用是使文件当前位置重新回到文件之首。该函数没有返回值。 函数fseek()是实现文件随机存取的最主要函数,用它可以将文件的当前位置移到文件的任意位置上。所谓文件 的随机存取(或随机读写),是指读写完一个字符(或字节)之后,并不一定要读写其后继的字符(或字节),可以改 变当前位置,去读写文件中其他位置上的字符(或字节)。 函数fseek()的说明形式为 fseek(FILE *fp, long offset, int ptrname) 其中ptname表示定位基准。只允许0,l或2.其中0代表以文件首为基准,1代表以当前位置为基准,2代表以文 件尾为基准。0、l和2分别被定义为名称SEEK-SET、SEEK-CUR和型形参offset是偏移量,表示以 prname为基准,偏移的字节数。因它是long型形参,当以整数作为它的实参调用函数 fseek()时,直在常数之后加 上字母L,表示是long型常量。见下面调用函数fseek()的例子: fseek(fp,4OL,SEEK-SET); fseek(fp,20L,SEEK-CUR); fseek(fp,-30L,SEEK-END); 分别表示将文件的当前位置定于离文件头4O个字节处、将文件当前位置定于离当前位置20个字节处、将文件的 当前位置定于文件尾后退30个字节处。 函数fseek()一般用于二进制文件的随机读写,这是因为数据在二进制文件中的表示形式与数据在内存中的表示 形式相同,各种类型的数据表示都有确定的字节数。而数据在正文文件中的表示形式与数据在内存中的表示形式不同, 它们在输入输出时,数据的内外表示形式要进行转换,数值类型的数据在正文文件中的表示没有固定的字节数,计算 位置时会发生混乱。 函数ftell()用于得到文件当前位置相对于文件首的偏移字节数。在随机方式存取文件时,由于文件位置频繁的前 后移动,程序不容易确定文件的当前位置。调用函数ftell()就能非常容易地确定文件的当前位置。 函数ftell()的说明形式为 long ftell(FILE *fp) 利用函数ftell()也能方便地知道一个文件的长。如以下语句序列: fseek(fp,OL,SEEK-END); len=ftell(fp) 首先将文件的当前位置移到文件的末尾,然后调用函数ftell()获得当前位置相对于文件首的偏移,该偏移值等于 文件所含字节数。 8.文件错误测式库函数 文件处理程序常用输入输出库函数的返回值来判断输入输出是否发生错误,也可以调用文件错误测试函数了解刚 完成的文件输入输出函数的调用是否有错。文件错误测试函数的说明形式为 int ferror(FILE *fp) 函数的返回值是与文件指针中相联的最近一次文件库函数调用是否发生错误。若有错,函数返回非0值,若没有 错误,函数返回0值。通常在这非0值,详细指出错误的类别和原因。 第九章 9.1 宏定义 宏定义有无参数宏定义和带参数宏定义两种。 无参数的宏定义的一般形式为 # define 标识符 字符序列 其中# define之后的标识符称为宏定义名(简称宏名),要求宏名与字符序列之间用空格符分隔。这种宏定义要求 编译预处理程序将源程序中随后所有的定名的出现(注释与字符串常量中的除外)均用字符序列替换之。前面经常使 用的定义符号常量是宏定义的最简单应用。如有: # define TRUE 1 # define FALSE 0 则在定义它们的源程序文件中,凡定义之后出现的单词TRUE将用1替代之;出现单词FALSE将用0替代之。 在宏定义的#之前可以有若干个空格、制表符,但不允许有其它字符。宏定义在源程序中单独另起一行,换行符 是宏定义的结束标志。如果一个宏定义太长,一行不够时,可采用续行的方法。续行是在键人回车符之前先键入符号 “”。注意回车要紧接在符号“”之后,中间不能插入其它符号。 宏定义的有效范围称为宏定义名的辖域,辖域从宏定义的定义结束处开始到其所在的源程序文件末尾。宏定义名 的辖域不受分程序结构的影响。可以用预处理命令#undef终止宏定义名的辖域。 在新的宏定义中,可以使用前面已定义的宏名。例如, # define R 2.5 # define PI 3.1415926 # define Circle 2*PI*R # define Area PI* R * R 程序中的Circle被展开为2*3.1415926* 2.5, Area被展开为3.1415926*2.5*2.5. 如有必要,宏名可被重复定义。被重复定义后,宏名原先的意义被新意义所代替。 通常,无参数的宏定义多用于定义常量。程序中统一用宏名表示常量值,便于程序前后统一,不易出错,也便于 修改,能提高程序的可读性和可移植性。特别是给数组元素个数一个宏定义,并用宏名定义数组元素个数能部分弥补 数组元素个数固定的不足。 注意:预处理程序在处理宏定义时,只作字符序列的替换工作,不作任何语法的检查。如果宏定义不当,错误要 到预处理之后的编译阶段才能发现。宏定义以换行结束,不需要分号等符号作分隔符。如有以下定定义: # define PI 3.1415926; 原希望用PI求圆的周长的语句 c=2*PI*r; 经宏展开后,变成 c=2*3.1415926*r; 这就不能达到希望的要求。 带参数宏定义进一步扩充了无参数宏定义的能力,在字符序列替换同时还能进行参数替换。带参数定定义的一般 形式为 # define 标识符(参数表)字符序列 其中参数表中的参数之间用逗号分隔,字符序列中应包含参数表中的参数。在定义带参数的宏时,宏名标识符与 左圆括号之间不允许有空白符,应紧接在一起,否则变成了无参数的宏定义。如有宏定义: # define MAX(A,B) ((A) > (B)?(A):(B)) 则代码 y= MAX( p+q, u+v)将被替换成 y=((p+q) >(u+v)?(p+q):(u+v)。 程序中的宏调用是这样被替换展开的,分别用宏调用中的实在参数字符序列(如p+q和u+V) 替换宏定义字符 序列中对应所有出现的形式参数(如用p+q替代所有形式参数A,用u+V替代所有形式参数B),而宏定义字符序列 中的不是形式参数的其它字符则保留。这样形成的字符序列,即为宏调用的展开替换结果。宏调用提供的实在参数个 数必须与宏定义中的形式参数个数相同。 注意:宏调用与函数调用的区别。函数调用在程序运行时实行,而宏展开是在编译的预处理阶段进行;函数调用 占用程序运行时间,宏调用只占编译时间;函数调用对实参有类型要求,而宏调用实在参数与宏定义形式参数之间没 有类型的概念,只有字符序列的对应关系。函数调用可返回一个值,宏调用获得希望的C代码。另外,函数调用时, 实参表达式分别独立求值在前,执行函数体在后。宏调用是实在参数字符序列替换形式参数。替换后,实在参数字符 序列就与相邻的字符自然连接,实在参数的独立性就不一定依旧存在。如下面的宏定义: # define SQR(x) x*x 希望实现表达式的平方计算。对于宏调用 P=SQR(y) 能得到希望的宏展开p= y*y.但对于宏调用q=SQR(u+v)得到的宏展开是q=u+V*u+V.显然,后者的展开结 果不是程序设计者所希望的。为能保持实在参数替换后的独立性,应在宏定义中给形式参数加上括号。进一步,为了 保证宏调用的独立性,作为算式的宏定义也应加括 号。如 SQR宏定义改写成: # define SQR((x)*(x)) 才是正确的宏定义。 对于简短的表达式计算函数,或为了提高程序的执行效率、避免函数调用时的分配存储单元、保留现场、参数值 传递、释放存储单元等工作。可将函数定义改写成宏定义。所以合理使用宏定义,可以使程序更简洁。 9.2 文件包含 文件包含预处理命令(简称文件包含命令)实现将指定文件的内容作为当前源程序的一部分。文件包含预处理命 令的一般形式为 # include “文件名”。 或 # include<文件名> 文件包含命令为组装大程序和程序文件复用提供了一种手段。在编写程序时,习惯将公共的常量定义、数据类型 定义和全局变量的外部说明构成一个源文件。称这类没有执行代码的文件为头文件,并以“。h”为文件名的后缀。其 它程序文件凡要用到头文件中定义或说明的程序对象时,就用文件包含命令使它成为自己的一部分。这样编程的好处 是各程序文件使用统一的数据结构和常量,能保证程序的一致性,也便于修改程序。头文件如同标准零件一样被其它 程序文件使用,减少了重复定义的工作量。 9.3 条件编译 条件编译是指在编译一个源程序文件时,其中部分代码段能根据条件成立与否有选择地被编译。即编译程序只编 译没有条件或条件成立的代码段,而不编译不满足条件的代码段。 条件编译为组装与环境有关的大程序提供有力的支持,能提高程序的可移植性和可维护性。通常在研制程序系统 时,设计者将所有与环境有关的内容编写成独立的程序段,并将它们配上相应的条件编译命令。因环境不同等原因, 只要设定相应条件,就能组装出适应环境要求的新程序。 条件编译命令主要有三种相似的形式。 (1) #if 表达式 程序段1 # else 程序段2 # endif 其中表达式为常量表达式,其意义是当指定的表达式值为非零时,程序段1参与编译,程序段2不参与编译;否 则,反之。 这种形式的条件编译命令使预处理程序能根据给定的条件确定哪个程序段参与编译,哪个程序段不参与编译。 在上述一般形式中,当程序段2不出现时,也可简写为 #if 表达式 程序段 # endif 条件编译与if语句有重要区别:条件编译是在预处理时判定的,不产生判定代码,其中一个不满足条件的程序段 不参与编译,不会产生代码;且语句是在运行时判定的,且编译产生判 定代码和两个分支程序段的代码。因此,条件编译可减少目标程序长度,能提高程序执行速度。但条件编译只能 测试常值或常量表达式,而且语句能对表达式作动态测试。 条件编译预处理命令也可呈嵌套结构。特别是为了便于描述# else后的程序段又是条件编译情况,引人须处理命令 符# elif.它的意思就是# else #if.所以条件编译须处理命令更一般的形式为 # if 表达式 1 程序段1 # elif 表达式 2 程序段2 # elif 表达式 n 程序段n # else 程序段 n+l # endif (2) # ifdef 标识符 程序段1 # else 程序段2 # endif 其中标识符是一个宏名,上述意义是当宏名已被定义,则程序段里参与编译,程序段2不参与编译;否则,反之。 这里的程序段可以是任何C代码行,也可以包含预处理命令行。其中的标识符只要求已定义与否,与标识符被定义成 什么是无关的。如标识符只用于这个目的,常用以下形式的宏定义来定义这类标识符: # define 标识符 即标识符之后为空,直接以换行结束该宏定义命令。 在上述一般形式中,如果程序段2为空,则可简写成如下一般形式: # ifdef 标识符 程序段 # endif 条件编译主要作用是能提高程序的通用性,应用于不同的计算机系统编译不同的源程序段。条件编译另一种应用 是在程序中插入调试状态下输出中间结果的代码。如: # ifdef DEBUG printf(“a=%d, b=%d n”, a, b); # endif 程序在调试状态下与包含宏定义命令 # define BEBUG 的头文件一起编译;在要获得最终目标程序时,不与包含该宏定义命令的头文件一起编译。这样,在调试通过后, 不必再修改程序,已获得了正确的最终程序。为了日后程序维护,将调试时使用的程序代码留在源程序中是专业程序 员习惯采用的方法。因为这些代码的存在不影响最终目标码,但有助于日后修改程序时的调试需要。 (3) # ifndef 标识符 程序段1 # else 程序段2 # endif 这种条件编译形式与前面介绍的形式的唯一差异是第一行的ifdef改为ifndef.其意义是当标识符末被定义时,程序 段1参与编译,程序段2不参与编译;否则,反之。在上述形式中,当程序段2不出现时,可简写成: # infdef 标识符 程序段 # endif 9.4 带参数的主函数 在操作系统下执行某个C程序,是环境对C程序的启动,可以看作是对该程序的main()函数的调用。main() 函数执行结束后,控制返回环境。为能从环境向C程序传递信息,启动C程序的命令行可带有任选的参数。命令行的 一般形式为 程序名 参数1 参数2„„参数n 其中程序名和各参数之间用空白符分隔。 为能让main()函数读取命令行中的参数,环境将多个参数以两个参数形式传递给main()函数、其中第一个参 数(习惯记作argc) 表示命令行中参数的个数(包括程序名);第二个参数(习惯记作argv)是一个字符指针数组。 其中argv[0] 指向程序名字符串的第一个字符,argv[1]指向参数1字符串的第一个字符,„,argv[argc-1] 指向最后一 个参数字符串的第一个字符。如果利因山等于1,则表示程序名后面没有参数。下面的例子用于说明main()函数对 参数argc 与argv的引用方法。 「例9.l」 回打启动程序时的命令行各参数。 # include void main(int argc, char *argv[] /* 或char **argv; */) { int k; for( k=l; k printf(“%c”,argv[k],k printf(“nn”); } 如上述程序的执行程序名为,执行该程序的命令行为: echopro Hello world! 则程序将输出 Hello world! 在以上命令行中,根据约定,main()函数的参数argc的值为3;argv[0],argv[1],argv[2]分别指向字符串“echopro”、 “Hello”、“world!”的第一个字符。在程序的printf()函数调用中,字符输出格式%c输出一个字符,若是已输出了 命令行最后一个参数,该格式将输出一个换行符,若是输出其它参数,则输出一个空白符。 因函数的数组参数是指向数组首元素的指针变量,所以在主函数main()中可对argv施行增量运算。例如,在argv[0] 指针指向程序名字符串的第一个字符情况下,对argv施增量运算++argv后,argv[0](或*argv)就指向参数1的第一个 字符c利用argv的这一性质,可改写上述程序为以下形式: # include void main(int argc,char **argv) { while(——argc>0) printf(“%s%c”,*++argv,argc>1? '':'n'); } 这里,++argv使指针argv先加1,让它一开始就指向参数1;逐次增回,使它遍历指向各参数。又利用函数printf ()的第一个格式参数是字符串表达式,上述程序对printf()的调用可改写成: printf((argc> l) ? “%s”:“%sn”,* ++argv); 「例9.2」 假定启动程序时给出的命令行参数是一串整数,程序将全部整数求和后输出。 # include # include void main( int argc,char **argv) { int k,s; for(s=0, k=l; k s+=atoi(*++agv); /* 从数字字符串译出整数 */ printf( “t%dn”, s); }
版权声明:本文标题:《高级语言程序设计》知识点总结 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.roclinux.cn/p/1735667509a1682611.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论