目 录目 录 1
第一章 C程序基础 6
1.1 C语言程序的基本结构 6
1.1.1 认识C语言程序 6
1.1.2 C语言程序的基本结构 8
1.1.3 C语言的表达式和语句 10
1.2 C程序运行过程 11
1.2.1 源程序、目标程序和可执行程序的概念 11
1.2.2 C程序上机步骤 12
1.3 编写简单的C语言程序 13
1.4 C语言基本语法成分 16
1.5 C语言数据类型 18
1.5.1 整型数据 19
1.5.2 实型数据 20
1.5.3 字符型数据 21
1.6 数据的输入与输出 22
1.6.1 printf()函数 23
1.6.2 scanf()函数 25
1.7 算法 27
1.7.1 算法的概念及特性 27
1.7.2 算法的表示方法 28
1.8 C语言的产生、发展及特点 30
1.8.1 C语言的产生及发展 30
1.8.2 C语言的特点 31
习题 一 33
第二章 程序基本结构 36
2.1 分支结构 36
2.1.1 单分支结构 36
2.1.2 双分支结构 37
2.1.3 多分支结构 38
2.1.4 if语句的嵌套 40
2.1.5 条件运算符 42
2.1.6 switch语句 43
2.2 关系运算和逻辑运算 47
2.3 循环结构 49
2.3.1 概述 49
2.3.2 当型循环while 51
2.3.3 直到型循环do-while 52
2.3.4 当型循环for 55
2.3.5 几种循环的比较 57
2.4 break和continue语句 59
2.4.1 break语句 59
2.4.2 continue语句 61
2.5 goto语句 62
2.6 经典算法举例 63
习题二 70
第三章 模块化程序设计 73
3.1 模块化程序设计的方法与特点 73
3.2函数的定义 74
3.3无返回值函数的定义与调用 76
3.4有返回值函数的定义与调用 80
3.5函数嵌套调用和函数声明 86
3.6函数的递归调用 88
3.7库函数的使用 89
3.8全局变量和局部变量 93
3.9指针和指针作为函数参数 98
3.10返回指针值的函数 102
3.11函数的指针 105
3.12典型例题 108
习 题 三 114
第四章 简单构造数据类型 118
4.1 一维数组的引出及使用 118
4.1.1一维数组的引出 118
4.1.2一维数组特点及其引用 120
4.2 二维数组的引出及使用 122
4.2.1二维数组的引出 122
4.2.2 二维数组特点及其引用 124
4.2.3数组程序举例 125
4.3字符数组 130
4.3.1字符数组的引出 130
4.3.2 字符数组的定义和使用 132
4.3.3 字符串的定义及使用 133
4.3.4字符数组程序举例 138
4.4 数组与函数 140
4.4.1数组元素作为函数参数 141
4.4.2数组名作为函数参数 142
4.4.3多维数组作为函数参数 144
4.5 数组与指针 145
4.5.1一维数组与指针 145
4.5.2多维数组与指针 148
4.5.3用数组名作函数参数 149
4.6字符串与指针 152
4.6.1字符串的指针和指针变量 152
4.6.2用字符串指针访问字符串 153
4.6.3字符指针和字符数组的区别 154
4.6.4字符串指针作函数参数 157
4.7典型例题 159
*4.8链表 170
4.8.1概述 170
4.8.2简单链表 171
4.8.3动态链表 171
4.8.4输出链表链表结构的实现和使用 174
习题四 179
第5章 复杂构造数据类型 189
5.1结构体 190
5.1.1结构体的引出及使用 190
5.1.2结构体数组的引出及使用 195
5.1.3结构体程序举例 198
5.1.4结构体与指针 201
5.2共用体 205
5.2.1共用体的引出 205
5.2.2共用体的定义及使用 208
5.3枚举类型 210
5.3.1枚举类型的引出 210
5.3.2枚举类型的定义及使用 211
习题五 213
第六章 磁盘数据存储 218
6.1 将数据写入文件 218
6.2 文件读写分类函数 221
6.3文件定位函数 227
习题五 230
第七章 实用程序设计技巧 235
7.1 程序的模块化结构 235
7.1.1 软件工程的思想 235
7.1.2 模块设计 236
7.1.3 使用模块化方法开发程序的好处 237
7.2 模块的组装 238
6.2.1 文件包含与头文件的使用 239
6.2.2 模块间的连接 242
6.2.3 标识符的一致性 243
6.2.4 条件编译 244
7.3 模块设计风格简述 246
6.3.1 数据风格 246
6.3.2 标识符风格 246
6.3.3 算法风格 247
6.3.4 输入/输出风格 247
6.3.5 书写风格 248
7.4 大型程序开发的项目管理 249
6.4.1 项目管理器 249
6.4.2 用项目管理器开发程序项目的步骤 250
6.4.3 项目管理器的使用技巧 251
7.5 几个应用程序设计实例 251
习题 262
C语言实验教程 263
实验一 C语言运行环境和C程序初步 263
一、目的和要求 263
二、实验内容 263
1 进入Turbo C 263
2 熟悉Turbo C程序运行过程 266
3 简单编程练习 271
实验二 顺序结构程序设计 272
一、目的和要求 272
二、实验内容 273
实验三 选择结构程序设计 275
一、目的和要求 275
二、实验内容 275
实验四 循环结构程序设计 277
一、目的和要求 277
二、实验内容 277
实验五 函数编程 280
一、目的和要求 280
二、实验内容 281
实验六 构造数据类型 284
一、目的和要求 284
二、实验内容 284
实验七 磁盘数据存储 289
一、目的和要求 289
二、实验内容 289
实验八 项目开发与模块设计 289
一、目的和要求 289
二、实验内容 289
附录 292
附录一 Turbo C 2.0集成开发环境的使用 292
附录二 常用功能键简表 297
附录三 Turbo C编译出错信息 298
1,严重错误 299
2,一般错误 299
3,警告 309
附录四 常用C库函数 312
4.1 数学函数 312
4.2 输入输出函数 315
4.3 字符函数 321
4.4 字符串函数 323
4.5 动态存储分配函数 326
4.6 时间函数 327
4.7 其它函数 330
参考文献 335

第一章 C程序基础我们用计算机解决问题,都是利用某种软件。如我们使用Word编辑文件,用Excel处理电子表格,用IE浏览网络资源,用QQ上网聊天等。这些软件都是按照一定算法编制的计算机程序。而算法是为解决一个问题而采取的方法和步骤。
从广义的角度说,算法早就融入我们的生活中。比如,早上上学,就开始了上学的算法:走哪条路,坐什么车,如果堵车怎么办等等,一直到到达学校,这个算法就完成了。用计算机解决问题也是按照相应的步骤(算法)一步一步完成的。这些步骤的实现用的是计算机语言,也就是编写计算机程序的语言。C语言就是一种计算机语言。
现在我们要学习的就是如何用C语言编写计算机程序。
1.1 C语言程序的基本结构
1.1.1 认识C语言程序首先来看两个简单的C语言程序。
【例1.1】 在屏幕上输出一行信息:This is a C program.
#include <stdio.h> /* 预处理命令 */
void main( ) /*定义主函数*/
{
printf("This is a C program.\n"); /* 调用库函数,输出信息 */
}
【例1.2】 计算两数之和,并输出结果。
#include <stdio.h> /* 预处理命令 */
void main( ) /*定义主函数,计算两数之和 */
{
int a,b,sum; /* 定义三个整型变量 */
a=123; b=456; /* 为变量a,b赋初值 */
sum=a+b; /* 让sum等于a+b的值 */
printf("sum=%d\n",sum); /* 调用库函数,输出结果 */
}
现在我们对这两个C语言程序作解析。
⑴ #include <stdio.h> 表示文件包含,其功能是将头文件stdio.h的内容包含到用户源程序中。文件stdio.h中声明了程序所需要的输入和输出操作的有关信息,有了“#include <stdio.h>”后,该程序就可以利用包含文件中的内容了。
⑵ main( )表示“主函数”,main是函数名。每个C程序必须有main( )函数。函数名后的一对圆括号不能省略。一个C程序可以包含若干个函数,但只能有一个主函数。C程序的执行从主函数开始,而不论主函数在什么位置(所有函数之前、所有函数之后或一些函数之后另一些函数之前)。
main( )前面的void是一种数据类型,这里说明本函数没有返回值。
⑶ 用{ }括起来的是main的函数体。main函数中的所有操作(语句)都在这一对{ }之间。也就是说main函数的所有操作都在main函数体中。
⑷ printf( )是C语言的标准输出函数,其含义是将双引号内的内容输出到显示器屏幕上。例1.1中的printf( )用于将字符串“This is a C program.\n”输出。即在屏幕上显示:This is a C program.。“\n”是换行符,即使光标跳到下一行行首(第一列)。例1.2中的%d为格式控制符,表示在此位置将代以一个十进制整数,该整数由逗号后面的变量sum提供。程序运行后,将在屏幕上输出:sum=579。
⑸ 分号“;”是C语句结束符,表示该语句结束。
⑹ /* */括起来的部分是一段注释,注释只是为了改善程序的可读性,在编译、运行时不起作用(事实上编译时会跳过注释,目标代码中不会包含注释)。注释可以放在程序的任何位置,并允许占用多行,只是需要注意“/*”、“*/”匹配,不能嵌套注释。
⑺ int a,b,sum;是变量声明。声明了三个具有整数类型的变量a,b,sum。C语言的变量必须先声明再使用。
⑻ a=123;b=456;是两条赋值语句。将123赋给变量a,456赋给变量b。执行上述两条语句后,a、b两个变量的值分别为123和456。
⑼ sum=a+b;是将a,b两变量的内容相加,然后将结果赋值给整型变量sum。此时sum的内容为579。
【例1.3】 输入两个整数,计算并输出两者较大的数。
#include <stdio.h>
void main( ) /* 主函数 */
{ /* main函数体开始 */
int a,b,c; /* 声明部分,定义变量 */
scanf("%d%d",&a,&b); /* 调用库函数,输入变量a、b的值 */
c=max(a,b); /* 调用max函数,将调用结果赋给c */
printf("max=%d\n",c);
} /* main函数体结束 */
int max(int x,int y) /* max函数,用于计算两数中较大的数 */
{ /* max函数体开始 */
int z; /* 声明部分,定义变量 */
if (x>y) z=x;
else z=y;
return z; /* 将z值返回,通过max带回调用处 */
} /* max函数体结束 */
说明:
⑴ 本程序包括两个函数。其中主函数main仍然是整个程序执行的起点。函数max的功能是计算两数中较大的数。
⑵ scanf( )是C语言的标准输入函数,用于从键盘输入若干数据给指定的变量。%d表示输入十进制整数。主函数main调用scanf函数获得两个整数,存入a、b两个变量,然后调用函数max获得两个数中较大的值,并赋给变量c。最后输出变量c的值(结果)。
⑶ int max(int x,int y)是函数max的函数头,函数max的函数头表明此函数获得两个整数,返回一个整数。
⑷ 用{ }括起来的部分是max函数的函数体。max的函数体是函数max的具体实现。它从参数表获得数据,处理后得到结果z,然后将z返回调用函数main。
⑸ 本例表明除了可以调用库函数外,还可以调用用户自己定义、编写的函数。
1.1.2 C语言程序的基本结构综合上述三个例子,下面对C语言程序的基本组成和形式(程序结构)加以说明。
C程序的结构:
预处理命令
void main( ) /* 主函数 */
{ /* 函数体开始 */
声明部分
执行部分
} /* 函数体结束 */
其他函数
{
声明部分
执行部分
}
下面对C程序的结构作简单说明:
1,函数是C程序的基本单位
① 一个C源程序必须包含一个main函数,也可以包含其它函数。函数是C程序的基本单位。
② 程序中的函数有些是C语言的标准库提供的,称为标准函数(或库函数)。如printf( )函数和scanf( )函数。编写程序时,如果有标准函数,就使用标准函数;如果没有标准函数,则用户需要自己编写。
被调用的函数可以是系统提供的库函数,也可以是用户根据需要自己编写的函数。C是函数式的语言,程序的全部工作都是由各个函数完成的。编写C程序就是编写一个个函数。
③ C函数库非常丰富,ANSI C提供100多个库函数,Turbo C提供300多个库函数。
2,main()函数主函数是每个程序执行的起点。一个C程序总是从main函数开始执行,而不论main函数在程序中的位置。可以将main函数放在整个程序的最前面,也可以放在整个程序的最后,或者放在一些函数之后另一些函数之前。
3,函数的结构一个函数由函数首部和函数体两部分组成。
① 函数首部:一个函数的第一行,格式如下:
返回值类型 函数名([函数参数类型1 函数参数名1][,…,函数参数类型n,函数参数名n])
如:int max(int x,int y)
注意:一个函数可以没有参数,但是后面的一对( )不能省略,这是格式的规定,如main( )。
② 函数体:函数首部下面用一对{ }括起来的部分。如果函数体内有多对{ },则最外层是函数体的范围。函数体一般包括声明部分和执行部分。
声明部分:定义本函数所使用的变量,并为变量分配相应大小的内存单元。变量名是内存单元的符号地址。
执行部分:由若干条语句组成的命令序列(可以在其中调用其它函数)。
4,书写风格
C程序书写格式自由,一行可以写几个语句,一个语句也可以写在多行上。每条语句的最后必须有一个分号“;”表示语句的结束。
5,输入/输出
C语言本身不提供输入/输出语句,输入/输出操作是通过调用库函数(如scanf和printf)完成的。
由于输入/输出操作涉及具体的计算机硬件,因此把输入/输出操作放在函数中处理可以简化C语言和C的编译系统,便于C语言在各种计算机上实现。
不同的编译系统除了提供函数库中的标准函数外,还按照硬件的情况提供一些专门的函数。因此不同编译系统提供的函数数量、功能会有一定差异。
6,注释注释部分要放在符号“/*”和“*/”之间。为了便于阅读,程序中应适当地加入注释。注释只起帮助阅读和理解程序的作用,不参加程序的编译,也不影响程序的运行。使用注释是编程人员的良好习惯。
实践中,编写程序往往需要不断的修改、完善,事实上没有一个应用系统是不需要修改、完善的。很多人会发现自己编写的程序在经历了一些时间以后,由于缺乏必要的文档和必要的注释,最后连自己都很难再读懂。需要花费大量时间重新思考、理解原来的程序,浪费了大量的时间。如果一开始编程就对程序进行注释,刚开始麻烦一些,但日后可以节省大量的时间。
7,预处理命令预处理命令能够改进程序的设计环境,提高编程效率。C语言的预处理功能主要包括:宏定义、文件包含、条件编译,分别用宏定义命令(#define)、文件包含命令(#include)、条件编译命令(#ifdef…#else…#endif)实现,为了与一般语句区别,这些命令以“#”开头。
1.1.3 C语言的表达式和语句用表达式构成语句,表示一种运算或操作。表达式语句是在表达式的最后加上一个“;”构成。一个表达式语句必须在最后出现分号,分号是语句不可缺少的一部分。C程序中大多数语句是表达式语句(包括函数调用语句)。
表达式语句的一般形式:
表达式;
表1-1表达式与表达式语句举例表 达 式
表达式语句
说 明
a=3(赋值表达式)
a=3;(赋值语句)
将3赋给变量a
getch( )(函数调用表达式,
函数调用也属于表达式)
getch( );
(函数调用语句)
getch( ); 合法且有意义,只关心是否有击键,不关心具体的值
i++ (自增表达式)
i++;(一般表达式语句)
变量i的值加1
ch=getch( )(函数调用表达式,赋值表达式)
ch=getch( );
(一般表达式语句)
将函数的返回值赋给变量ch
x+y(算术表达式)
x+y; (一般表达式语句)
x+y; 是一个语句,其作用是完成x+y操作,是合法的,但是并不将结果赋给另外的变量,所以并无实际意义
表达式语句常见的形式有:赋值语句、函数调用语句和空语句。
1,赋值语句赋值语句由赋值表达式加上一个分号构成。
C语言的赋值语句先计算赋值运算符(=)右边子表达式的值,然后将此值赋值给左边的变量。
对变量的赋值:<变量>=<表达式>
例:b=30.0; // 将表达式的值30.0赋给变量b
a=sin(b*3.14159/180); //将表达式(正弦函数)的值赋给变量a
变量赋值的特点:
① 变量必须先定义,后使用。int d,e,f; 定义三个变量为整数类型。如未定义,则在编译时认为非法,提示错误。
② 变量被赋值前,值不确定。
③ 对变量的赋值过程是“覆盖”过程,即用新值去替换旧值。
④ 读出变量的值,变量的值保持不变。
⑤ 参与表达式运算的所有变量都保持原来的值不变。
⑥ 赋值运算是右结合性,即“取右送左”。若有声明语句:
int a=1,b=2,c=3;
则执行表达式:
a=b=c;
结果a、b、c的值都是3(先运算b=c,再运算a=b)。
2,函数调用语句函数调用语句由函数调用表达式加一个分号构成。
例如:printf("This is a C statement.");
3,空语句即只有一个分号的语句,它什么也不做(表示这里需要一个语句,但是不需要做任何工作)。
1.2 C程序运行过程
1.2.1 源程序、目标程序和可执行程序的概念
1,程序:程序是一组计算机可以识别和执行的指令,每一条指令使计算机执行特定的操作。
2,源程序:程序可以用高级语言或汇编语言编写,用高级语言或汇编语言编写的程序称为源程序。C源程序的扩展名为“.C”。
源程序不能直接在计算机上执行,需要用“编译程序”将源程序翻译为二进制形式的代码。
3,目标程序:源程序经过“编译程序”翻译所得到的二进制代码称为目标程序。目标程序的扩展名为“.OBJ”。
目标代码尽管已经是机器指令,但是还不能运行,因为目标程序还没有解决函数调用问题,因此需要将各个目标程序与库函数连接起来,才能形成完整的可执行程序。
4,可执行程序:目标程序与库函数连接,形成完整的可在操作系统下独立执行的程序称为可执行程序。可执行程序的扩展名为“.EXE”(在DOS/Windows环境下)。
1.2.2 C程序上机步骤
C语言采用编译方式将源程序转换为二进制目标代码。程序的开发过程包括编辑、编译、连接和运行几个步骤,如图1.1所示。
1,编辑将编写完成的源程序输入到计算机中,并保存为磁盘文件,扩展名为.C。编辑的对象是源程序,它以ASCII代码的形式输入和存储,不能被计算机执行。常用的编辑软件是Turbo C、Borland C,也可以使用Windows下的记事本等字处理软件。
2,编译编译具体分为执行预处理和编译两个阶段。
⑴ 执行预处理:如果程序中有预处理命令(如#include命令或#define命令),则首先要处理预处理指令,然后再进行下面的编译过程;如果源程序中无预处理指令,那么就直接进行下面的编译过程。
⑵ 执行编译:编译是将源程序代码翻译为二进制目标代码的形式——目标程序,扩展名为.OBJ。编译过程中,如果发现语法错误,则在屏幕上显示出错信息并中止编译。此时应对源程序的错误进行修改,然后重新进行编译,如此反复进行,直到完全正确为止,最后生成目标代码程序。
应当指出,经编译后得到的二进制目标代码还是不能直接执行,因为每一个模块往往是单独编译的,必须把经过编译的各个模块的目标代码与系统提供的标准模块(如C语言中的标准库函数)连接后才能运行。
3,连接将各模块的二进制目标代码与系统标准模块连接在一起,生成扩展名为.EXE的可执行文件。
4,执行执行经过编译和连接的可执行文件。一般在显示器上显示运行结果,可根据运行结果来判断程序是否有错。如果有错误,则须改正,并重新进行编译和连接,然后运行。这样反复运行,直到得到正确的运行结果。
集成化的工具环境(如Borland C和Visual C++等)已经将编辑、编译、连接和调试工具集于一身,用户可以方便地在窗口状态下连续进行编辑、编译、连接、调试和运行的全过程。
C程序的上机步骤如图1.1所示。Turbo C的使用见C语言实验教材。
1.3 编写简单的C语言程序程序设计时,通常采用三种不同的程序结构,即顺序结构、选择结构和循环结构,其中顺序结构是最基本、最简单的程序结构。现在开始最简单的C程序设计,即顺序程序设计的学习。
【例1.4】 已知a=5,b=10,试交换a、b的值。
分析:交换两个变量的值,就像一个瓶装酱油,一个瓶装醋要交换内容需要借助第三个空瓶一样。交换两个数也需要借助第三个变量。
C语言程序:
#include <stdio.h>
void main( )
{
int a=5,b=10,temp;
temp=a;
a=b;
b=temp;
printf("a=%d,b=%d\n",a,b);
}
运行结果:a=10,b=5
【例1.5】 编写一个显示基本算术运算功能的程序。
分析:C语言的基本算术运算符包括两类:
① 单目运算符:-(负)、+(正)。
② 双目运算符:+(加)、-(减)、*(乘)、/(除)、%(取余,模)。
所谓单目运算,是指只需要一个操作数,如-5、+2.3等。双目运算是需要两个操作数,如a+b、3*c、n/m等。
C语言程序:
#include <stdio.h>
void main( )
{
printf("\n30%7 is %d",30%7);
printf("\n30%%7 is %d",30%7);
printf("\n2.0/-3+1.8 is %f",2.0/-3.0+1.8);
printf("\n2/3*10000 is %d",2/3*10000);
printf("\n2/3*10000 is %f",2/3*10000);
printf("\n2.0/3*10000 is %f",2.0/3*10000);
printf("\n2.0/3*10000 is %d",2.0/3*10000);
printf("\n2.0/3*10000 is %e",2.0/3*10000);
}
运行结果:
30%7 is %d
30%7 is 2
2.0/-3+1.8 is 1.133333
2/3*10000 is 0
2/3*10000 is 0.000000
2.0/3*10000 is 6666.666667
2.0/3*10000 is -21845
2.0/3*10000 is 6.666667e+03
说明:
⑴ 求模运算符%,得到的是余数。注意printf( )中“%”是格式符,要输出“%”需要再加一个“%”。
如:printf("\n30%7 is %d",30%7);
的输出为:30%7 is %d,%使用错误使得后面的格式字符都失效了。
⑵ C语言的算术运算符中,单目运算符的优先级高于双目运算符;双目算术运算的优先级与普通数学中的相同;单目算术运算符是自右向左结合的。
⑶ 输出结果中,2/3*10000的值为0,因为C语言规定两个整数相除结果为整数,即舍去小数部分。但是如果除数或被除数中有一个为负值,则舍入的方向是不固定的,多数机器采用“向0取整”的方法,取整后向零靠拢,如-5/3=-1。
那么如何才能得到浮点数呢?
方法1:将参加运算的变量定义为实型(float或double)。似乎我们可以把所有的变量都定义为实型,这样就不会出现错误了。但是,实型在内存中占用的字节数大于整型,同时实型数据运算需要的时间也比整型长。这不利于程序的优化和执行效率的提高,所以只在需要使用时才定义为实型。
方法2:令f=1.0*a/b;(设有int a=1,b=2),因为C语言规定如果参加+、-、*、/运算的两个数有一个为实数,则结果为double型,因为所有实数都按double型进行计算。这样1.0*a的数据类型就是实型(因为1.0是实数)。最终的结果就是实数0.5。
方法3:利用强制类型转换。即:f=(float)a/b;C语言允许将一种数据类型强制转换成另一种数据类型,方法是:(数据类型)变量。这样a就被转换成实型。
【例1.6】 试计算圆柱体的表面积。
分析:为了计算圆柱体的表面积,可以利用公式2πr(r为半径)得到底面周长d,利用公式d×h计算侧面积s1,利用π×r2计算底面积s2,则表面积s=s1+2×s2。可以按照以下步骤:
① 设圆柱体的高为h,半径为r,表面积为s;
② 输入r、h的值;
③ 计算底面周长d=2πr;
④ 计算侧面积s1=dh;
⑤ 计算底面积s2=πr2;
⑥ 计算表面积:s=s1+2×s2;
⑦ 输出s的值。
这是用自然语言描述的计算圆柱体表面积的算法。我们在计算过程中,有些步骤可以合并。现在我们将其转换为C语言程序。注意r、h和s的数据类型。
C语言程序:
#include <stdio.h> /* 预编译命令 */
#define PI 3.1415926 /* 定义符号PI代表常量3.1415926 */
void main( ) /* 主函数main,void是类型说明符 */
{
float r,h,s; /* 声明部分:定义变量 */
printf("input r,h:\n"); /* 执行部分:输出语句*/
scanf("%f%f",&r,&h); /* 执行部分:输入语句 */
s=2*PI*r*h+2*PI*r*r; /* 执行部分:计算表面积 */
printf("Total area is %.2f\n",s); /* 执行部分:输出表面积 */
}
说明:①,#define PI 3.1415926”是宏定义命令,PI称为符号常量,即用一个标识符代表一个常量。此处表示用标识符PI代表常量3.1415926。
符号常量的定义形式为:
#define 标识符 常量
② float表示实型,即定义r,h和s为实型变量。float型数据在输入输出时需要用%f格式控制符。
1.4 C语言基本语法成分
1,C语言字符集字符是C语言最基本的元素,C语言字符集由字母、数字、空白、标点符号和特殊字符组成(在字符串常量和注释中还可以使用汉字和其它图形符号)。由字符集中的字符可以构成C语言进一步的语法成分,如标识符、关键字、运算符等。
C程序是用下列字符所组成的字符集写成的:
⑴ 字母:A-Z,a-z
⑵ 数字:0-9
⑶ 标点符号、特殊字符:
!
#
%
^
&
+
-
*
/
=
~
<
>
\
|
.
,
;
:
'
"
(
)
[
]
{
}
⑷ 空白符:空格,制表符(Tab,跳格健),换行符(空行)的总称。空白符除了在字符、字符串中有意义外,编译系统忽略其它位置的空白。空白符在程序中只是起到间隔的作用。在程序的恰当位置使用空白符将使程序更加清晰,增强程序的可读性。
2,标识符标识符是用来标识变量名、符号常量名、函数名、数组名、类型名等实体(程序对象)的有效字符序列。标识符由用户定义。
C语言标识符定义规则包括:
⑴ 标识符只能由字母、数字和下划线三种字符组成,且第一个字符必须为字母或下划线。
例如,合法的标识符:a,i,sum,average,_total,Class,day,student,p405;不合法的标识符:5a,M.D.John,$123,3D64,a-b。
⑵ 大小写敏感,即C认为大小写字母为不同的字符。例如:sum不同于Sum,BOOK不同于book。C程序员习惯:变量名小写,常量名大写,但不绝对。
⑶ ANSI C没有限制标识符长度,但各个编译系统都有自己的规定和限制(TC 32个字符,MSC 8个字符)。
例如:student_name和student_number,如果取8个字符,系统认为这两个标识符是相同的。
⑷ 标识符不能与“关键字”同名,也不与系统预先定义的“标准标识符”同名,如main、printf等。
在定义标识符时,建议遵循下面的原则:
⑴ 尽量不要用下划线开头。因为系统内部使用了一些下划线开头的标识符(如_fd、_cs、_ss),避免与系统定义的标识符冲突。
⑵ 尽量做到“见名知义”,如sum,area,score,day,name,age等。⑶ 变量名、函数名用小写,符号常量用大写。
⑷ 在容易出现混淆的地方应尽量避免使用容易认错的字符。如:数字1与字母l和字母I,数字0与字母o和O,数字2与字母Z和z。
3,关键字关键字是C语言预先定义的、具有特定意义的标识符,也称为保留字。C语言包括32个关键字:
auto
break
case
char
const
continue
default
do
double
else
enum
extern
float
for
goto
if
int
long
register
return
short
signed
sizeof
static
struct
switch
typedef
union
unsigned
void
volatile
while
C语言的关键字都是小写。不能重新定义关键字,也不能把关键字定义为一般标识符。
4,运算符运算符是用于描述某种运算功能的符号,如+、-、*、/、%等,运算符可以由一个或多个字符组成。由运算符将常量、变量和函数调用连接起来的式子称为表达式。
根据参与运算的操作数个数,运算符分为:单目(一元)运算符、双目(二元)运算符和三目(三元)运算符。
C语言运算符分为以下几类:
⑴ 算术运算符:+、-、*、/、%
⑵ 关系运算符:<、<=、>、>=、= =、!=
⑶ 逻辑运算符:!、&&、||
⑷ 位运算符:<<、>>、~、|、^、&
⑸ 赋值运算符:=及扩展赋值运算符
⑹ 条件运算符:?,
⑺ 逗号运算符:,
⑻ 指针和地址运算符:*、&
⑼ 求字节运算符:sizeof
⑽ 分量运算符:.、->
⑾ 下标运算符:[ ]
⑿ 强制类型转换运算符:(类型)
⒀ 其他:如函数调用运算符( )
5,分隔符就像写文章要有标点符号一样,写程序也要有一些分隔符。在C语言程序中,空格、逗号、回车/换行等,在各自不同的应用场合起着分隔符的作用。例如,在程序中的相邻保留字、标识符之间应有空格或回车/换行作为分隔符,相邻同类项之间用逗号作为分隔符。
如语句int x,y; 中的空格和逗号都起着分隔符的作用,如果缺少了空格程序就会出错,缺少了逗号x和y就会被认为是一个变量。
6,其他符号花括号“{”和“}”通常用于标识函数体或者一个语句块(即复合语句)。
“/*”和“*/”构成一组注释符。编译系统将/*,.,*/之间的所有内容看作注释,编译时编译系统忽略注释。
⑴ 注释在程序中的作用:提示、解释作用。
注释与软件的文档同等重要,要养成使用注释的良好习惯,这对软件的维护相当重要。记住:程序是要给别人看的,自己也许还会看自己几年前编制的程序(相当别人看你的程序),清晰的注释有助于他人理解你的程序、算法的思路。
⑵ 在软件开发过程中,还可以将注释用于程序的调试,暂时屏蔽一些语句。
例如,在调式程序时暂时不需要运行某段语句,而又不希望立即从程序中删除它们,可以使用注释符将这段程序框起来,暂时屏蔽这段程序,以后可以方便地恢复。
1.5 C语言数据类型为了按不同的方式和要求处理数据,数据必须区分为不同的类型。在C语言程序中,每个数据都属于一个确定的、具体的数据类型。不同的数据类型在数据标示形式,合法的取值范围,占用内存空间大小,可参与的运算种类等方面都有所不同。
C语言的数据类型十分丰富,见图1.3。
图1.3 C语言的数据类型
C语言中的数据有常量与变量之分,它们分别属于上述类型。常量与变量的区别是:在程序执行过程中,常量的值不能由程序改变,而变量的值可由程序改变。实际上变量是内存中的一个存储区,在存储区中存放着该变量的值,每个变量有一个名字,如:x,sum,area等。
变量在使用前必须进行说明,其目的是为变量在内存中申请存放数据的内存空间。
1.5.1 整型数据
1,整型常量整型常量即整型常数。C语言中,整型常量可用3种表示方式:
⑴ 十进制数:以非0数字开头的数,如123,-5,0。
⑵ 八进制数:以数字0开头的数,如0123表示八进制数(123)8,等于十进制数83。
⑶ 十六进制数:以0x或0X开头的数,如0x123表示十六进制数(123)16,等于十进制数291。
2,整型变量
C语言中的整型变量分为short int、int和long int三种类型,每种类型又分为有符号类型和无符号类型,分别用unsigned和signed表示,缺省为signed类型。各种整型变量的长度和表示范围如表1-10所示。
表1-10 整数类型类型
比特数
最小取值范围
[signed] int
16
-32768~32767 即-215~(215-1)
unsigned [int]
16
0~65535 即0~(216-1)
[signed] short [int]
16
-32768~32767 即-215~(215-1)
unsigned short [int]
16
0~65535 即0~(216-1)
long [int]
32
-2147483648~2147483647 即-231~(231-1)
unsigned long [int]
32
0~4294967295 即0~(232-1)
如果在整常数后面加字母U或u,表示short int型常数;加字母L或l表示long int型常数。
3,整型变量的定义变量的定义形式为:
数据类型 变量名表;
其中变量名表的变量可为1个或多个,中间用逗号“,”分隔。
如:int a,b,sum;
unsigned long x;
1.5.2 实型数据带有小数点的数称为实数,又称为浮点数。
1,实型常量
C语言中,实型常量可用两种表示方式:
⑴ 十进制形式:由数字和小数点组成(必须有小数点),但小数点前后的0可以省略,如0.12,.12,123.0,123.,0.等。
⑵ 指数形式:如1.2e3和0.12E4都表示1.2×103。注意e(或E)前面要有数字,且e(或E)后面的指数必须为整数。如1.E2,2.3e3.5,e2等都是不合法的表示形式。
2,实型变量
C语言中的实型变量分为单精度型(float)、双精度型(double)和长双精度型(long double),各种类型的长度和数值范围如表1-11所示。
表1-11 实数类型类型
比特数
有效数字
数值范围
float
32
6-7
-3.4×1038 ~ 3.4×1038
double
64
15-16
-1.7×10308 ~ 1.7×10308
long double
80
18-19
-3.4×104932 ~ 1.1×104932
一个实型常数默认为double型。要表示float型数,则必须在实数后加上字母f或F。表示long double型数,则必须在实数后加字母l或L。
1.5.3 字符型数据
1,字符常量字符常量是用单引号( ' )括起来的单个字符,如:'A'、'a'、'0'、'$'、','等。
C语言中,还有一种特殊的字符常量,即以“\”开头的字符序列,称为转义字符。意思是将反斜杠“\”后面的字符转变成另外的意义,它通常用来表示ASCII码字符集中一些特定的、具有控制功能的字符,如'\n'。表1-12中列出了C语言中常用的转义字符。
表1-12 常用的转义字符字符形式
ASCII码
功 能
\a
7
响铃
\n
10
换行
\t
9
制表符(横向跳格),光标跳到下一输出区(每个输出区占8列)
\v
11
竖向跳格
\b
8
退格
\r
13
回车但不换行,光标移到本行第一列
\\
92
反斜杠字符“\”
\"
34
双引号
\'
39
单引号
\ddd
用1~3位八进制数表示的任意字符,如'\101'为'A'
\xhh
用1~2位十六进制数表示的任意字符,如'\x41'为'A'
2,字符变量字符变量是用来存放字符常量的。每个字符变量占一个字节的内存单元,在一个字符变量中,只能存放一个字符常量。
字符变量的定义形式如下:
char ch1,ch2;
注意:① 大多数编译系统中,将char型数据作为有符号类型(signed)处理,即表示范围为-128~+127,如果要表示128~255范围内的字符,需要定义为unsigned类型,见表1-13。
表1-13 字符类型类型
比特数
取值范围
[signed] char
8
-128~+127
unsigned char
8
0~255
② '0'与0是截然不同的两个数。'0'是数字字符,其ASCII码值为48(0x30),而0则是整数值,可以表示ASCII为0的字符(即'\0')。
3,字符串常量字符串常量是由一对双引号(" ")括起来的字符序列。如:"Hello"、"I love China!"、"How do you do?"等都是字符串常量。
C语言未对字符串的长度加以限制。为了判断字符串结束,C编译器会自动在字符串的末尾加一个转义字符'\0',作为字符串常量的结束标志。因此,字符串"China"在内存中占有6个字节的连续内存单元,如图1.4所示。
C
H
i
n
a
\0
图1.4 字符串在内存的存放另外,C语言中无专门的字符串变量,而将字符串常量存放在字符型数组中(将在第4章中介绍)。
4,字符型数据与整型数据的相互运算由于整型数据在内存中是以二进制补码形式存放的,字符型数据在内存中以相应的ASCII码值存放,实际上也是以二进制形式存放的。因此,C语言允许字符型数据与整型数据之间可以通用。
【例1.7】 字符数据用整数形式输出
#include <stdio.h>
void main( )
{
char ch;
ch=65;
printf("%c,%d\n",ch,ch);
}
运行结果:
A,65
【例1.8】 字符数据与整数进行算术运算
#include <stdio.h>
void main( )
{
char ch;
int x;
ch='A';
x=ch+32;
printf("%c,%d\n",ch,ch);
printf("%c,%d\n",x,x);
}
运行结果:
A,65
a,97
1.6 数据的输入与输出输入/输出是对数据的一种重要操作。没有输出的程序是没有用的,没有输入的程序是缺乏灵活性的。程序在多次运行时,用到的数据可能是不同的,因此,需要由用户临时输入程序运行所需要的数据。
C语言本身无输入输出语句,输入/输出均由函数来实现。C语言提供了多种输入输出函数,标准I/O函数库中有一些公用信息都写在头文件stdio.h中,因此,在使用输入输出函数时,一般应在程序开头先写预编译命令:
#include <stdio.h> 或 #include "stdio.h"
本节介绍最常用的输入输出函数scanf和printf。
1.6.1 printf()函数
1,一般形式
printf函数的一般形式为:
printf("格式控制",输出表列);
其功能是:按照格式控制部分指定的格式,将输出表列的数据在标准输出设备上输出。
2,格式控制
“格式控制”是用双引号括起来的字符序列,包括格式说明和普通字符两部分,格式说明由“%”和格式字符组成,如%d、%f等。printf函数的格式字符及含义见表1-14。
表1-14 printf格式字符格式字符
含 义
%d(或%i)
输出带符号的十进制整数
%x(或%X)
输出无符号十六进制整数(不输出前导符0x)
%o
输出无符号八进制整数(不输出前导符0)
%u
输出无符号十进制整数
%f
输出十进制实数(隐含输出6位小数)
%e(或%E)
以指数形式输出实数(隐含输出6位小数)
%g(或%G)
自动选用%f或%e格式中输出宽度较短的一种格式输出实数,不输出无意义的0
%c
输出单个字符
%s
输出字符串
%%
输出%
除了格式控制字符之外的字符均是普通字符,普通字符原样输出。下面举例说明格式字符的用法:
int a=123,b=-1;
float x=12.34;
char ch=65;
printf("a=%d,b=%d",a,b);
输出结果:a=123,b=-1
printf("a=%X,b=%x",a,b);
输出结果:a=7B,b=ffff
printf("a=%o,b=%o",a,b);
输出结果:a=173,b=177777
printf("a=%u,b=%u",a,b);
输出结果:a=123,b=65535
printf("x=%f",x);
输出结果:x=12.340000
printf("x=%e",x);
输出结果:x=1.234000e+001
printf("x=%g",x);
输出结果:x=12.34
printf("%f%%",1.0/3);
输出结果:0.333333%
printf("ch=%c",ch);
输出结果:ch=A
printf("str=%s","I live China!");
输出结果:str=I love China!
注意格式字符与其对应的输出项的类型要保持一致,例如,不应用%f输出整数。
另外,格式控制字符中,在“%”和其后的格式字符之间,还可以插入附加的格式说明符。见表1-15所示。
表1-15 printf的附加格式说明符附加格式说明符
含 义
字母h
用于输出短整型数据,可加在格式符d、o、x、u前面
字母l
用于输出长整型数据或double型数据,可加在格式符d、o、x、u、f、e(或E)、g(或G)前面
m(代表一个正整数)
输出数据的最小宽度,如果数据的实际宽度超过m,则按实际宽度输出;如果小于m,则补空格
,n(代表一个正整数)
对于实数,表示输出n位小数;对于字符串,表示截取的字符个数
-(负号)
输出的数据或字符在域内向左对齐
下面给出了域宽与精度描述符的一些例子。
int a=123;
float x=12.345;
printf("a=%5d ",a);
输出结果:a=_ _123(_表示空格)
printf("a=%2d",a);
输出结果:a=123
printf("a=%-5X",a);
输出结果:a=7B_ _ _
printf("x=%7.2f",x);
输出结果:x=_ _12.35
printf("x=%.2f",x);
输出结果:x=12.35
printf("x=%10.2e",x);
输出结果:x=_1.23e+001
printf("x=%2E",x);
输出结果:x=1.234500E+001
printf("str=%5.3s","Computer");
输出结果:str=_ _Com
printf("str=%3s","Computer");
输出结果:str=Computer
1.6.2 scanf()函数
1,一般形式
scanf函数的一般形式为:
scanf("格式控制",地址表列);
其功能是:按照格式控制部分的要求,把从终端输入的数据传送到地址表列指定的内存单元中。
scanf函数的“格式控制”与printf函数的“格式控制”相似。“地址表列”是由地址组成的,表示每个输入数据应存储的内存单元地址,它可以是变量的地址,也可以是字符串的首地址。在变量名前加符号“&”表示该变量的地址,如“&a”。
【例1.9】 格式化输入与输出。
#include <stdio.h>
void main( )
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
printf("a=%d,b=%d,c=%d\n",a,b,c);
}
执行scanf函数时,如果格式控制部分只包含格式控制字符,不含其它字符,则在输入数据时,数据之间可用一个或多个空格隔开,也可用回车键或跳格键(Tab)隔开。因此,执行上面的程序时,下面的输入均正确:
① 3_ _4_ _ _ _5↙
② 3↙
4_ _ _5↙
③ 3(按Tab键)4_ _5↙
输出结果均为:a=3,b=4,c=5
非法的输入:3,4,5↙
2,格式说明
scanf函数的“格式控制”与printf函数的“格式控制”类似,以“%”开始,后面跟一个格式字符,中间也可插入附加格式说明符。表1-16列出了scanf函数中可以使用的格式字符,表1-17列出了scanf函数可以使用的附加格式说明符。
表1-16 scanf格式字符格式字符
含 义
%d(或%i)
用来输入十进制整数
%x(或%X)
用来输入无符号十六进制整数(大小写作用相同)
%o
用来输入无符号八进制整数
%u
用来输入无符号十进制整数
%f,%e,%E,%g,%G
用来输入实数,可以用小数形式或指数形式输入(作用相同)
%c
用来输入单个字符
%s
用来输入字符串,并将字符串送到一个字符数组中。输入时以非空白字符开始,以第一个空白字符结束,并在最后加一个'\0'作为结束标志
表1-17 scanf的附加格式说明符附加格式说明符
含 义
字母h
用于输入短整型数据(可用%hd,%ho,%hx)
字母l
用于输入长整型数据(可用%ld,%lo,%lx)以及double型数据(可用%lf或%le)
m(正整数)
指定输入数据所占宽度(列数)
*
表示本输入项在读入后不赋给相应的变量
说明:
⑴ 可以指定输入数据所占列数,系统自动按它截取所需数据。
如:int a,b; char c;
scanf("%3d%3c%3d",&a,&c,&b);
输入:123ABC456↙后,a=123,b=456,c='A'。
⑵ 如果%后有附加格式说明符“*”,表示跳过它指定的列数。
如:scanf("%3d%*3c%2d",&a,&b);
输入:123456789↙后,a=123,b=78(456被跳过)。
在利用现有的一批数据时,有时不需要其中某些数据,可用此方法“跳过”它们。
⑶ 输入实型数据时可以指定数据的宽度,但不能规定数据的精度。
例如,scanf("%5.2f",&a); 是不合法的。
3,使用scanf函数应当注意的问题
⑴ 如果在“格式控制”字符串中除了格式说明以外还有其它字符,则在输入数据时在对应位置应输入这些字符。例如:
scanf("%d,%d,%d",&a,&b,&c); 应当输入3,4,5;不能输入3 4 5。
scanf("%d:%d:%d",&h,&m,&s); 应当输入12:23:36。
scanf("a=%d,b=%d,c=%d",&a,&b,&c); 应当输入a=1,b=2,c=3(太罗嗦)。
因此建议在“格式控制”字符串不要使用其它字符。
⑵ 在用“%c”格式输入字符时,空格字符和转义字符都作为有效字符输入。%c只要求读入一个字符,后面不需要用空格作为两个字符的间隔。
对于scanf("%c%c%c",&c1,&c2,&c3);
输入:a b c后,c1='a',c2=' ',c3='b'。
⑶ 输入数据时,遇到下列情况之一认为该数据结束:
遇到空格,或回车键或跳格键(Tab)。
满足指定的宽度。
遇到非法的输入。
如:int a; float b; char c;
scanf("%d%c%f",&a,&c,&b);
输入:1234a123o.26<CR>后,a=1234,c='a',b=123.0(而不是希望的1230.26)。
总之,C语言的格式输入输出的规定比较繁琐,重点掌握最常用的一些用法即可,其它部分可在需要时随时查阅。
1.7 算法
1.7.1 算法的概念及特性
1,算法前面我们已经介绍了算法的基本概念,现在我们对算法进行进一步的讨论。一般来说,不同的工作,采用的方法和步骤也不同。对于同一个问题可以有不同的解题方法和步骤,也就是有不同的算法。算法有优劣,一般而言,应当选择简单的、运算步骤少的,运算快、内存开销小的算法。
2,算法的五大特性
⑴ 有穷性:一个算法应当“在合理范围之内”的有限步骤内结束,而不能是无限的;同时应当在执行一定数量的步骤后,算法结束,不能死循环。
⑵ 确定性:算法中的每一个步骤都不应当产生歧义,其含义应当是确定的。例如:“将成绩优秀的同学名单打印输出”就是有歧义的,“成绩优秀”的含义不明确。
⑶ 有0个或多个输入,所谓输入是指算法执行时从外界获取必要的信息(可以是从键盘输入的数据,也可以是其它部分传递给算法的数据)。一个算法可以没有输入,也可以有输入。
⑷ 有1个或多个输出:即算法必须得到结果,没有结果的算法是没有意义的(结果可以显示在屏幕上,也可以传递给程序的其它部分或文件)。
⑸ 有效性:算法的每个步骤都应当能有效执行,并能得到确定的结果。例如:b=0,则a/b是不能有效执行的。
1.7.2 算法的表示方法描述算法有很多种方法,一般有自然语言、流程图、N-S图、伪代码、程序语言等。最常见的是流程图和N-S图,下面进行简单的介绍。
1,流程图表示算法流程图用一些图框表示各种操作,用箭头表示算法流程。这种用图形表示算法的方法,直观形象,易于理解。
美国标准化协会ANSI规定了一些常用的流程图符号,如图1.5所示,已为世界各国程序工作者普遍采用。
起止框:表示算法的开始和结束。一般内部只写“开始”或“结束”。
输入输出框:表示算法请求输入需要的数据或算法将某些结果输出。一般内部常常填写“输入…”或“打印/显示…”。
处理框:表示算法的某个处理步骤,一般内部常常填写赋值操作。
菱形框(判断框):作用是对一个给定条件进行判断,并根据给定的条件是否成立来决定如何执行其后的操作。它有一个入口,两个出口。
连接点:用于将画在不同地方的流程线连接起来。同一个编号的点是相互连接在一起的,实际上同一编号的点是同一个点,只是画不下才分开画。
注释框:注释框不是流程图中必须的部分,不反映流程和操作,它只是对流程图中某些框的操作做必要的补充说明,以帮助阅读流程图的人更好地理解流程图的作用。
C语言是一种结构化程序设计语言,结构化程序设计是20世纪60年代末被提出来的,主要采用自上而下、逐步细化的方法。结构化程序有三种基本结构:
⑴ 顺序结构:最简单的一种基本结构,自顶向下、自左向右顺序地执行程序中的每一条语句。
⑵ 选择结构:也称为分支结构,当判断表达式为真时,执行语句A;否则执行语句B。
⑶ 循环结构:分为当型循环和直到型循环。当型循环结构的特点是,当判断表达式为真时,执行循环体;否则退出循环。直到型循环结构的特点是,先执行循环体,直到判断条件表达式为假时,退出循环。
三种结构的流程图表示如图1.6所示。
由上述三种结构构成的程序称为结构化程序,也就是说,任何一个结构化程序都可以分解为一个个基本结构。
用流程图表示的算法直观形象,比较清楚地显示出各个框之间的逻辑关系,因此得到广泛使用。
2,N-S流程图(盒图)表示算法
1973年美国学者I.Nassi和B.Shneiderman提出了一种新的流程图形式,称为N-S流程图。N-S图中,完全去掉了带箭头的流程线,每种结构用一个矩形框表示。
结构化程序设计的三种基本结构在N-S流程图中的表示方法如图1.7所示。

图1.7 用N-S流程图表达的三种基本程序结构
N-S流程图的符号含义如下:
⑴ 顺序结构:先执行A操作,再执行B操作。
⑵ 选择结构:当p条件成立时,执行A操作,当p条件不成立时,执行B操作。A,B操作允许空操作,即什么都不做。注意选择结构是一个整体,代表一个基本结构。
⑶ 循环结构:a) 当型循环:当条件p成立时,反复执行A操作,直到p条件不成立为止。当型循环先判断,再决定是否执行循环体,所以在条件p一次都不满足时,循环体A可能一次都不执行。b) 直到型循环:当条件p不成立时,反复执行A操作,直到p条件成立为止。直到型循环先执行循环体A,然后再判断条件p,所以循环体至少执行一次。
一般情况下,循环算法既可以用当型循环,也可以用直到型循环实现。循环结构也是一个整体,同样也代表一个基本结构。
注意:
三种结构中的A、B框可以是一个简单的操作,也可以是3个基本结构之一。也就是说基本结构可以嵌套。
N-S流程图表示算法的优点:
比文字描述更加直观、形象,易于理解;比传统的流程图紧凑易画;废除流程线,整个算法结构是由各个基本结构按顺序组成的。N-S流程图的上下顺序就是执行时的顺序,N-S图表示的算法都是结构化的算法。
1.8 C语言的产生、发展及特点
1.8.1 C语言的产生及发展
C语言是国际上流行的、很有发展前途的计算机高级语言。C语言适合于作为系统描述语言,它既可以用来编写系统软件,也可以用来编写应用软件。
以前操作系统等系统软件主要采用汇编语言编写。汇编语言依赖于计算机硬件,程序的可读性、可移植性都比较差。为了提高可读性和可移植性,人们希望采用高级语言编写这些软件,但是一般的高级语言难以实现汇编语言的某些操作,特别是针对硬件的一些操作(如:内存地址的读写,位操作等)。人们设法寻找一种既具有一般高级语言特性,又具有低级语言特性的语言,C语言就在这种情况下应运而生。
C语言的产生和发展经历了以下过程:ALGOL60 → CPL → BCPC → B → C → 标准C → ANSI C → ISO C。
(1)ALGOL 60:一种面向问题的高级语言。ALGOL 60离硬件较远,不适合编写系统程序。
(2)CPL(Combined Programming Language,组合编程语言):CPL是一种在ALGOL 60基础上更接近硬件的一种语言。CPL规模大,实现困难。
(3)BCPL(Basic Combined Programming Language,基本的组合编程语言):BCPL是对CPL进行简化后的一种语言。
(4)B语言:是对BCPL进一步简化所得到的一种很简单且很接近硬件的语言。B语言取BCPL语言的第一个字母。B语言精练、接近硬件,但过于简单,数据无类型。B语言诞生后,Unix开始用B语言改写。
(5)C语言:是在B语言基础上增加数据类型而设计出的一种语言。C语言取BCPL的第二个字母。C语言诞生后,Unix很快用C语言改写,并被移植到其它计算机系统。
(6)标准C、ANSI C、ISO C:C语言的标准化。
注:最初Unix操作系统是采用汇编语言编写的,B语言版本的Unix是第一个用高级语言编写的Unix。在C语言诞生后,Unix很快用C语言改写,C语言良好的可移植性很快使Unix从PDP计算机移植到其它计算机平台,随着Unix的广泛应用,C语言也得到推广。从此C语言和Unix像一对孪生兄弟,在发展中相辅相成,Unix和C很快风靡全球。
从C语言的发展历史可以看出,C语言是一种既具有一般高级语言特性(ALGOL 60带来的高级语言特性),又具有低级语言特性(BCPL带来的接近硬件的低级语言特性)的程序设计语言。C语言从一开始就是用于编写大型、复杂系统软件的,当然C语言也可以用来编写一般的应用程序。也就是说:C语言是程序员的语言!
IBM PC微机DOS、Windows平台上常见的C语言版本有:
Borland公司:
Turbo C,Turbo C++,Borland C++,C++ Builder(Windows版本)
Microsoft公司:
Microsoft C,Visual C++(Windows版本)
1.8.2 C语言的特点
C语言具有下面的特点(其中1-6属于高级语言特性,7-8属于低级语言特性):
1,C语言的语言成分简洁、紧凑、书写形式自由。
2,C语言拥有丰富的数据类型。
C语言具有整型、实型、字符型、数组类型、指针类型、结构体类型、共用体类型等数据类型。能方便地构造更加复杂的数据结构(如:使用指针构造链表、栈、树、图等)。
3,C语言的运算符丰富、功能更强大。
例如:
⑴ C语言具有复合的赋值运算符“+=、-=、*=、/=、%=”(加、减、乘、除、取余),“>>=、<<=”(右移、左移),“&=、|=、~=”(与、或、非)。如x+=5等价于x=x+5。
⑵ C语言有条件运算符“?:”可代替简单的if/else语句。
“如果x小于或等于0,y为0,否则y为1”可以写成下面的条件表达式:y=x<=0?0:1;
如果用if语句需要写成下面的形式:
if (x<=0) y=0;
else y=1;
C语言避免了一切可能的罗嗦。
⑶ C语言中连赋值这种操作都定义为运算符,也就是说赋值操作本身可以作为表达式的一部分参与运算。如:
if ((p=malloc(sizeof(int)))= =NULL)
{ printf("Error!"); exit(1); }
如果改写为一般形式:
p=malloc(sizeof(int));
if (p= =NULL)
{ printf("Error!"); exit(1); }
又如下面的算式是正确的:
x=y=z=6;
如果改写为一般形式:z=6; y=6; x=6;
4,C语言是结构化程序设计语言。
⑴ C语言具有结构化的控制语句,如:if/else、switch/case、for、while、do…while等。
⑵ 函数是C语言程序的模块单位。
5,C语言对语法限制不严格,程序设计灵活。
C语言不检查数组下标越界、不限制对各种数据的转换(编译系统可能对不合适的转换进行警告,但不限制)、不限制指针的使用,而程序的正确性由程序员来保证。
实践中,C语言程序编译时会提示:“警告”、“严重错误”。警告表示所使用的语法可能有问题,但是有时可以忽略,程序仍然可以完成编译工作,然后运行(但是一般情况下,警告往往意味着程序真的有问题,还是应该认真地检查)。“严重错误”是不能忽略的,编译系统发现严重错误,就不会产生目标代码。
灵活和安全是一对矛盾,对语法限制的不严格可能也是C语言的一个缺点,比如:黑客可能使用越界的数组攻击计算机系统。JAVA语言是优秀的网络应用程序开发语言,它必须保证安全性,它绝对不允许数组越界。此外JAVA不使用指针,不能直接操作客户计算机上的文件,语法检查相当严格,程序正确性容易保证,但是JAVA在编程时却缺乏灵活。
6,C语言编写的程序具有良好的可移植性。
C语言编写的程序基本上不需要修改或只需要少量修改就可以移植到其它的计算机系统或其它的操作系统。
7,C语言可以实现汇编语言的大部分功能:
⑴ 可以直接操作计算机硬件,如寄存器和各种外设I/O端口等。
⑵ 可以通过指针直接访问内存物理地址。
⑶ 类似汇编语言的位操作可以方便地检查系统硬件的状态。
因此,C语言适合编写系统软件。
8,C语言编译后生成的目标代码小,质量高,程序的执行效率高。有资料显示只比汇编代码效率低10%-20%。
习题 一一.选择题
1.一个C语言程序总是从______
A.主过程开始执行 B.主函数开始执行
C.子程序开始执行 D.主程序开始执行
2.若已定义x和y为double类型,则表达式:x=1,y=x+3/2的值是_______.
A.1 B.2 C.2.0 D.2.5
3.若有以下程序段,
int c1=1,c2=2,c3;
c3=1.0/c2*c1;
则执行后,c3中的值是______
A.0 B.0.5 C.1 D.2
4.下面四个选项中,均是合法的标识符是_____
A.abc A_4d _student xyz_abc
B.auto 12-a a_b ab5.x
C.A_4d _student xyz_abc if
D.abc a_b union scan
5.下列数据中,不合法的C语言实型数据的是
A)0.123 B)123e3 C)2.1e3.5 D)789.0
6.以下不正确的叙述是
A)在C程序中,逗号运算符的优先级最低
B)在C程序中,APH和aph是两个不同的变量
C)若a和b类型相同,在计算了赋值表达式a=b后,b中的值将放入a中,而b中的值不变
D)当从键盘输入数据时,对于整型变量只能输入整型数值,对于实型变量只能输入实型数值
7.若有程序段
int a=1234; float b=123.456; double c=12345.54321;
printf("%2d,%2.1f,%2.1f",a,b,c);
其输出结果正确的是:
A)无输出 B)12,123.5,12345.5
C)1234,123.5,12345.5 D)1234,123.4,1234.5
8.设变量a是int型,f是float型,i是double型,则表达式10+ˊaˊ+i*f值的数据类型为
A)int B)float C)double D)不确定
9.若变量a是int类型,并执行了语句:a=ˊAˊ+1.6;,则正确的叙述是
A)a的值是字符C B)a的值是浮点型
C)不允许字符型和浮点型相加
D)a的值是字符ˊAˊ的ASCII值加上1
10.设x和y均为int型变量,则以下语句:
x+=y;y=x-y;x-=y;
的功能是_____
A.把x、y按从小到大排序 B.把x、y按从大到小排序
C.无确定结果 D.交换x、y中的值二.填空题
1.若有程序main()
{int i,j;
scanf("i=%d,j=%d",&i,&j);
printf("i=%d,j=%d\n",i,j);
}
若要求i赋10,j赋20,那么我们应该从键盘上输入______
2.若有说明int i,j,k;则表达式i=10,j=20,k=30,k*=i+j的值为_______________。
3.main( )
{ int i=3,j=4;
printf(“%d,%d\n”,i++,j++);
printf(“%d,%d\n”,++i,++j);
}
运行结果:_______________。
4.printf("%5.3f\n",123456.12345);输出为_____________。
5.用scanf函数输入数据,使a=3,b=7,x=8.5,y=71.82,c1=ˊAˊ,c2=ˊaˊ,问在键盘上如何输入?
main()
{
int a,b;float x,y;char c1c2;
scanf("a=%d_b=%d",&a,&b);
scanf("_x=%f_y=%e",&x,&y);
scanf("_c1=%c_c2=%c",&c1,&c2);
}
三.简答题
1.C语言程序的总体结构是怎样的。
2.C语言程序的基本单位是什么?它的结构又如何?
3.主函数main()在程序中的地位如何。程序总是从哪个函数开始执行,到哪个函数执行完后结束?
4.C语言的语句分为哪几类?
5.执行一个C语言程序的一般过程是什么?
四.编程题
1.若a=3,b=4,c=5,x=1.2,y=2.4,z=-3.6,u=51274,n=128765,c1=‘a’,c2=‘b’。想得到以下输出格式和结果,请写出程序(包括定义变量类型和设计输出)。
a=□3□□b=□4□□c=□5
x=1.200000,y=2.400000,z=-3.600000
x+y=□3.600□□y+z=-1.20□□z+x=-2.40
c1=ˊaˊ□or□97(ASCII)
c2=ˊbˊ□or□98(ASCII)
(□表示空格,下同)
2.设圆半径r=1.5,圆柱高h=3,求圆周长、圆面积、圆球表面积、圆球体积、圆柱体积。用scanf输入数据,输出计算结果,输出时要求文字说明,取小数点后两位数字。请编程序。
3.输入一个华氏温度,要求输出摄氏温度。公式为c=5(F-32)/9,输出要求有文字说明,取2位小数。
4.已知三角形的三边长,求其面积。
假设输入的三边能构成三角形,三角形的面积公式为:
Area=
其中S=(a+b+c)/2。
第二章 程序基本结构在上一章我们求解的问题都是一步一步按顺序执行的,我们称为顺序结构。但在解决实际问题的过程中,一个稍微复杂点的问题,往往不可能顺序完成。就像我们走路经常会遇到岔路口一样,有很多情况需要根据某些条件决定下一步如何进行,也就是说程序产生了分支。还有一些工作需要重复很多次才能完成。这就是我们在本章要介绍的分支结构和循环结构。
顺序结构、分支结构、循环结构是程序的三种基本结构,已经证明:由三种基本结构顺序组成的算法结构,可以解决任何复杂的问题。由基本结构组成的算法属于“结构化”算法。
2.1 分支结构
2.1.1 单分支结构
【例2.1】 输入两个整数,按从小到大的顺序输出这两个数。
分析:对于输入的这两个数a和b,我们可以比较它们的大小关系,将较小的数存放在第一个变量中,较大的数存放在第二个变量中,然后按顺序输出a和b。是否要交换两个变量的值,需要根据a和b的值来确定,这就产生了分支情况。
C语言提供了单分支选择结构解决上述问题,形式如下:
if (表达式)
语句;
其中“表达式”是判断条件,只要表达式的值不为“0”,就认为条件成立。而“语句”可以是单语句,也可以是复合语句。所谓复合语句是用一对花括号“{ }”括起来的一组语句。单分支结构的执行过程如图2.1所示。
例2.1的C语言程序如下:
#include <stdio.h>
void main( )
{
int a,b,temp;
printf("Input a,b:");
scanf("%d%d",&a,&b);
if (a>b) /* 判断a、b的关系 */
{ temp=a; a=b; b=temp; } /* a大于b,则交换a,b的值 */
printf("Return is %d,%d\n",a,b); /*按从小到大顺序输出 */
}
说明:“a>b”是关系运算,如果a的值大于b的值表示条件成立,表达式的值为真,则执行if后的语句;否则表达式的值为假,不执行if后的语句。
2.1.2 双分支结构
【例2.2】 输入两个整数,输出其中较大的一个。
分析:输入的是两个整数,需要定义两个整型变量x和y。由于是随机输入的两个数,这就分成三种情况:
(1)如果x>y,输出x;
(2)如果x<y,输出y;
(3)如果x=y,输出x或者y都可以。
输出x还是y,就需要根据x、y的值来决定,这就产生了分支情况。
1,双分支结构
C语言提供了双分支选择结构,形式如下:
if (表达式)
语句1;
else
语句2;
表达式的值不为“0”,执行语句1,否则执行语句2。同样,“语句1”和“语句2”既可以是单语句,也可以是复合语句。双分支选择结构的执行过程如图2.2所示。
例2.2的C语言程序如下:
#include <stdio.h>
void main( )
{
int x,y;
printf("Input x,y:");
scanf("%d%d",&x,&y);
if (x>=y) /* 判断x、y的关系 */
printf("max=%d\n",x); /* x大于或等于y,输出x */
else
printf("max=%d\n",y); /* x小于y,输出y */
}
说明:if语句中的“表达式”一般为关系表达式或逻辑表达式,但不限于这两种表达式。是执行语句1,还是执行语句2,取决于“表达式”运算的结果。
【例2.3】 输入三个数x1、x2、x3,按从小到大的顺序输出这三个数。
分析:实际这就是一个排序的问题,如果x1>x2,则交换两个数,同理x1和x3比较,x2 和x3比较。这样经过比较交换三个数按从小到大的顺序分别存储在x1、x2、x3中。数据的交换在程序设计中经常用到。
C语言程序如下:
#include <stdio.h>
void main( )
{
int x1,x2,x3,temp;
scanf("%d%d%d",&x1,&x2,&x3);
if (x1>x2) /*交换x1和x2,temp作为临时变量暂存x1的值*/
{ temp=x1; x1=x2; x2=temp; } /*括号部分构成复合语句*/
if (x1>x3)
{ temp=x1; x1=x3; x3=temp; } /*经过两次交换,x1为三者中最小的*/
if (x2>x3)
{ temp=x2; x2=x3; x3=temp; }
printf("%d,%d,%d",x1,x2,x3);
}
2.1.3 多分支结构
【例2.4】 设有分段函数:
-e2x+1+3 (x<-2)
y= 2x-1 (-2≤x≤3)
3log10(3x+5)-11 (x>3)
编写一个程序,输入x,输出y值。
分析:在这里,涉及多个条件的判断,并根据判断结果选择不同的公式计算y值。因此需要使用多分支结构。
多分支结构的形式为:
if (表达式1)
语句1;
else if (表达式2)
语句2;
else if (表达式3)
语句3;

else if (表达式n)
语句n;
else
语句n+1;
其功能为:按顺序求各表达式的值。如果某一表达式的值为真(非0),那么执行其后相应的语句,执行完后整个if语句结束,其余语句则不被执行;如果没有一个表达式的值为真,那么执行最后的else语句。执行过程如图2.3所示。
例2.4的C语言程序:
#include <stdio.h>
#include<math.h>
void main( )
{
double x,y;
printf("Input x:");
scanf("%lf",&x);
if (x<-2)
y=-exp(2*x+1)+3;
else if (x<=3) /* 表达式也可以写成:x>=-2&&x<=3 */
y=2*x-1;
else /* 本行也可以写成:else if (x>3) */
y=3*log10(3*x+5)-11;
printf("y=%.2lf\n",y);
}
说明:在本例中,需要用到两个数学函数,即exp( )和long10( ),分别是以e为底的指数函数和以10为底的对数函数,它们都只有一个参数,计算结果均为double型。这两个函数是系统提供的,为了使用这些数学函数,需要包含头文件“math.h”。即增加语句
#include<math.h>
其他数学函数也是包含在该头文件中。
2.1.4 if语句的嵌套所谓if语句的嵌套是指if语句的if块或else块中,又包含一个if语句。一般形式为:
if (表达式1)
if (表达式2) 语句1;
else 语句2;
else
if (表达式3) 语句3;
else 语句4;
对于嵌套结构,应当注意else与if的配对关系。C语言规定else总是与它前面最近的、而且没有与其他else配对的if进行配对。特别是if…else子句数目不一样时(if的数量只会大于或等于else的数量)。
例如,下面的if语句中:
if (表达式1)
if (表达式2) 语句1;
else 语句2;
根据C语言规定,else应与第二个if配对。如果希望else与第一个if配对,可以将第二个if用一对花括号“{ }”括起来,即写成下面的形式:
if (表达式1)
{ if (表达式2) 语句1; }
else 语句2;
【例2.5】 求一元二次方程ax2+bx+c=0的根,a,b,c由键盘输入。
分析:对于一元二次方程有以下几种可能,
a=0,不是二次方程;
b2-4ac=0,有两个相等的实根;
b2-4ac>0,有两个不等的实根;
b2-4ac<0,有两个共轭复数根。
该问题有多个条件,这就涉及到条件语句的嵌套问题;另外由于该问题涉及开平方运算,有可能出现无限小数,为此需要设置一个最小值,小于等于该值就认为是零。这里使用了C语言系统提供的开平方函数“sqrt( )”、取绝对值函数“fabs( )”。在程序的开始必须包含一条“#include <math.h>”语句。
图2.4是用N-S图描述的求解二元一次方程的算法。
假
输出
“非二次方程”
真
假
输出两个相等的实根:
-b/(2a)
真
假
x1=(-b+sqrt(disc))/(2a)
x2=(-b-sqrt(disc))/(2a)
计算复根的实部和虚部:
实部:p=-b/(2a)
虚部:q=sqrt(-disc)/(2a)
输出两个实根:x1、x2
输出两个复根:p+qi、p-qi
图2.4 用N-S图描述的求解二元一次方程的算法
C语言程序:
#include <stdio.h>
#include <math.h> /* 数学函数头文件 */
void main( )
{
double a,b,c;
double disc,x1,x2,rpart,ipart;
printf("Input a,b,c:");
scanf("%lf%lf%lf",&a,&b,&c);
if (fabs(a)<=1E-6) /*相当于a=0,若不是一元二次方程则程序结束 */
printf("not a quadratic.\n");
else /* a不等于0,则计算方程的根 */
{ disc=b*b-4*a*c; /* 计算disc */
if (fabs(disc)<=1E-6) /* disc等于0,输出两个相等的实根 */
printf("two equal roots:%.2f\n",-b/(2*a));
else if (disc>1E-6) /* disc大于0,输出两个不等的实根 */
{ x1=(-b+sqrt(disc))/(2*a);
x2=(-b-sqrt(disc))/(2*a);
printf("two distinct real roots:%.2f,%.2f\n",x1,x2);
}
else /* disc小于0,输出两个不等的虚根 */
{ rpart=-b/(2*a);
ipart=sqrt(-disc)/(2*a);
printf("two complex roots:%.2f+%.2fi,%.2f-%.2fi\n",
rpart,ipart,rpart,ipart);
}
} /* 第一个else结束 */
} /* main( )结束 */
在本例中,要特别注意“{ }”的使用,左括号“{”一定与最近的右括号“}”结合成一对,而不能交叉。if…else…在逻辑上是一条语句。
运行结果如下:
① Input a,b,c:1 2 1↙
two equal roots:-1.00
② Input a,b,c:1 0 -1↙
two distinct real roots:1.00,-1.00
③ Input a,b,c:2 1 2↙
two complex roots:-0.25+0.97i,-0.25-0.97i
2.1.5 条件运算符条件运算符“?:”是if语句的缩写形式。条件表达式的一般形式为:
表达式1?表达式2:表达式3
其功能是:先计算“表达式1”的值,若为真(非0)则取“表达式2”的值为整个条件表达式的值;若“表达式1”的值为假(0),则取“表达式3”的值为整个条件表达式的值。执行过程如图2.5所示。
若if语句中,在表达式为“真”和“假”时,都只执行一个赋值语句且给同一个变量赋值时,可以使用简单的条件运算符来处理。
例如:
max=a>b?a:b
该式中,如果a>b,则max=a;否则max=b。将该式用if语句表示,即:
if (a>b) max=a;
else max=b;
说明:
① 条件运算符的优先级高于赋值运算符,低于关系运算符和算术运算符。例如:
max=a>b?a:b 等价于,max=((a>b)?a:b)
② 条件运算符的结合性为“自右向左”。例如:
a>b?a:c>d?c:d 等价于,(a>b)?a:((c>d)?c:d)
③ 表达式2和表达式3不仅可以是数值表达式,还可以是赋值表达式或函数表达式。例如:
a>b?(a=100):(b=100)
a>b?prinf("%d\n",a):prinf("%d\n",b)
④ 表达式1、表达式2和表达式3的类型都可以不同。条件表达式值的类型是表达式2和表达式3中类型较高的类型。例如:
x>y?1:1.5 ;x>y时条件表达式的值为double型数据1.0
【例2.6】 输入一个字符,如果是大写字母,转换为小写,如果不是则不转换。输出最后得到的字符。
#include<stdio.h>
void main( )
{
char ch;
scanf("%c",&ch);
ch= (ch>='A'&&ch<='Z')? (ch+32):ch;
printf("%c",ch);
}
表达式ch= (ch>='A'&&ch<='Z')? (ch+32),ch中的括号“( )”可以不要,但有括号看上去程序更加清晰。
2.1.6 switch语句
【例2.7】 在学生成绩管理中,成绩经常要在百分制与等级制之间进行转换。90分以上为A等,80-89为B等,70-79为C等,60-69分为D等,其余为E等。编制程序,根据输入的百分制,输出对应的等级。
分析:设用变量score表示成绩,而等级是依据score的值变化的,共有五种情况:
成绩
等级
判断方法
score>=90
A
score/10=10或9
80<=score<90
B
score/10=8
70<=score<80
C
score/10=7
60<=score<70
D
score/10=6
score<60
E
score/10=其他值
很明显这是一个多分支问题,利用if语句的嵌套完全可以解决这类问题。但是如果if…else…语句嵌套过多,会令人眼花缭乱。幸运的是C语言提供了另外一种多分支语句:switch语句。
switch的基本格式:
switch(表达式)
{
case 常量表达式1:语句组1;[break;]
case 常量表达式2:语句组2;[break;]
… … …
case 常量表达式n:语句组n;[break;]
[default,语句组n+1]
}
switch总结说明:
① switch括号后面的表达式,只能是整型、字符型或枚举型表达式。
② 当“表达式”的值与某个case后面的常量表达式的值相等时,就执行此case后面的语句。执行完后,流程控制转移到下一个case(包括default)中的语句继续执行。如果不想继续执行,就需要使用break语句使流程跳出switch结构,即终止switch语句的执行(最后一个分支可以不用break语句)。
③ 如果表达式的值与所有常量表达式都不匹配,就执行default后面的语句(如果没有default就跳出switch,执行switch语句后面的语句)。
④ 各个常量表达式的值必须互不相同,否则出现矛盾。
⑤ case后面如果有多条语句,不必用“{ }”括起来。
C语言程序:
#include <stdio.h>
void main( )
{
int score,temp;
printf("Input score of student:");
scanf("%d",&score);
temp=score/10;
switch(temp)
{
case 10:
case 9,printf("A"); break;
case 8,printf("B"); break;
case 7,printf("C"); break;
case 6,printf("D"); break;
default,printf("E");
}
}
本例中,temp=10和temp=9 都输出“A”,共用输出语句printf("A");所以“case 10:”后面可以没有语句,并且一定不能使用 break;但以后的语句不再共享,必须使用break终止。否则,程序将继续执行下面的语句。
【例2.8】 某运输公司对用户按路程计算每公里运费。路程越远,每公里运费越低。标准如下:
路程km
折扣
c=(int)(s/250)
s<250
没有折扣
0
250≤s<500
2%
1
500≤s<1000
5%
2,3
1000≤s<2000
8%
4,5,6,7
2000≤s<3000
10%
8,9,10,11
s≥3000
15%
12,13…
分析:我们注意到折扣的里程是250的倍数,将其倍数填入表的右侧。设每公里每吨货物的基本运费为p,货物重量为w,距离为s,折扣为d,则总运费f的计算公式为:f=p*w*s*(1-d)。显然该问题需要采用分支的方法编写程序。由于分支比较多,最好采用switch语句。p、w、s需要用户输入,考虑程序的通用性,最好定义为实数型。注意break的使用。
C语言程序如下:
#include <stdio.h>
void main( )
{
int c,s;
float p,w,d,f;
scanf("%f%f%d",&p,&w,&s);
c=s/250;
switch(c)
{
case 0,d=0; break;
case 1,d=2; break;
case 2,
case 3,d=5; break;
case 4,
case 5,
case 6,
case 7,d=8; break;
case 8,
case 9,
case 10,
case 11,d=10; break;
default,d=15;
}
f=p*w*s*(1-d/100.0);
printf("%f\n",f);
}
【例2.9】 某公园的票价是每人10元,一次购票满30张,每张票可以少收1元。试编写自动计费系统。
分析:这个问题并不能简单地根据购票数是否大等于30计算相应的费用。因为若购29张票用购票数member×单价10=290元,若购30张,金额=30×9=270元。需要考虑人数少于30人的情况下,member×10是否大于270。此外,为了完善功能程序应该计算实收金额与门票的差额。
C语言程序:
#include <stdio.h>
void main( )
{
int member,sum,money,balance;
printf("Input the number of entering park:");
scanf("%d",&member);
if (member>=30)
sum=member*9;
else if (10*member<270)
sum=10*member;
else
{ sum=270; member=30; }
printf("cost:%d yuan\n",sum);
printf("Input Money:");
scanf("%d",&money);
balance=money-sum;
printf("Tickets Money Cost Balance\n");
printf("%d,%d,%d,%d\n",member,money,sum,balance);
}
2.2 关系运算和逻辑运算前面我们已经使用了简单的关系运算,现在对关系运算和逻辑运算作详细介绍。掌握关系运算和逻辑运算对于正确编写分支程序和循环程序是非常重要的。
2.2.1 关系运算符和关系表达式关系运算:即比较运算。即将两个值进行比较,判断是否符合或满足给定的条件。C语言提供如下6个关系运算符:
<、<=、>、>=、==、!=
关系表达式:用关系运算符将两个表达式(算术、关系、逻辑、赋值表达式等)连接起来所构成的表达式,称为关系表达式。
关系表达式的值是一个逻辑值,只有两种取值,即“真”或“假”。C语言没有逻辑型数据,关系运算的结果是int型,以1表示关系表达式成立,代表“真”,0表示关系表达式不成立,代表“假”。
关系运算的特点:
① 关系运算符都是双目运算符,并且都是从左向右结合。
② 关系运算符的优先级比算术运算符低,但都比赋值运算符高。在关系运算符内部,“>”、“>=”、“<”、“<=”运算符的优先级相同;“==”和“!=”两种运算符的优先级相同,且低于其他四种关系运算符。
c>a+b 等价于 c>(a+b) ;关系运算符的优先级低于算术运算符
a>b==c 等价于 (a>b)==c ;“>”优先级高于“==”
a==b<c 等价于 a==(b<c) ;“<”优先级高于“==”
a=b>c 等价于 a=(b>c) ;关系运算符的优先级高于赋值运算符
2.2.2,逻辑运算在执行分支语句时,经常会遇到需要两个或两个以上条件同时满足或者满足一个就可以的情况。
在C语言中表达式“x>y>z”,并不表示“x>y”并且“y>z”。那么在C语言中如何表示呢,如何将两个条件合并到一起呢?这就需要采用“逻辑运算”。C语言中的逻辑运算有以下3种:
逻辑与:两个条件需要同时成立(真)结果才成立(真),否则不成立(假)(相当日常生活中:而且,并且)。用符号“&&”表示。
逻辑或:两个条件只要有一个成立(真)时结果就成立(真),否则不成立(假)(相当日常生活中:或者)。用符号“||”表示。
逻辑非:条件成立(真),结果不成立(假);条件不成立(假),结果成立(真)。用符号“!”表示。
逻辑表达式:用逻辑运算符(逻辑与、逻辑或、逻辑非)将关系表达式或逻辑量连接起来构成逻辑表达式。
逻辑表达式的值是一个逻辑量“真”或“假”。C语言编译系统在给出逻辑运算结果时,以1代表“真”,以0代表“假”,但在判断一个量是否为“真”时,以0代表“假”,以非0代表“真”(即认为一个非0的数值是“真”)。
在一个逻辑表达式中如果包含多个逻辑运算符,则按照以下的优先顺序:
⑴ !(非)→&&(与)→||(或),“!”为三者中最高。
⑵ 逻辑运算符中的&&和||的优先级低于关系运算符,!高于算术运算符。
例:
a>b&&x>y 等价于 (a>b)&&(x>y)
a==b||x==y 等价于 (a==b)||(x==y)
!a||a>b 等价于 (!a)||(a>b)
⑶ 逻辑运算的真值表如表2-1所示。
表2-1 逻辑运算真值表
a
b
!a
!b
a&&b
a||b
非0
非0
0
0
1
1
非0
0
0
1
0
1
0
非0
1
0
0
1
0
0
1
1
0
0
在逻辑表达式的求解中,并不是所有的逻辑运算符都被执行,只是在必须执行下一个逻辑运算符才能求出表达式的解时,才执行该运算符。
例如:
① a&&b&&c 只有a为真,才需要判别b的值;只有a、b都为真,才需要判别c的值。只要a为假,此时整个表达式已经确定为假,就不必判别b和c;如果a为真,b为假,则不必判断c。
② a||b||c 只要a为真,整个表达式已经确定为真,就不必判断b和c;只有a为假,才判断b;a、b都为假才判断c。
例如,设a=1,b=2,c=3,d=4,m=n=1,则执行下面的表达式后m=? n=?
(m=a>b)&&(n=c>d)
由于“a>b”为假(0),所以赋值后m=0,赋值表达式“m=a>b”的值也为0。此时整个表达式的结果已经知道(0),所以不进行表达式“n=c>d”的计算,所以表达式计算结束后,n=1(未改变)。
【例2.10】 判断某一年是否闰年。
分析:所谓闰年,是指年份符合下面两个条件之一:能被4整除,但不能被100整除;能被4整除,又能被400整除。因为能够被400整除一定能被4整除所以第二个条件可以简化为能够被400整除。判断闰年的条件可以用一个逻辑表达式表示:
(year%4==0&&year%100!=0)||year%400==0
表达式为“真”,闰年条件成立,是闰年,否则不是闰年。
请同学们自己编写判断是否闰年的程序。
2.3 循环结构
2.3.1 概述
【例2.11】 编写程序计算:1+2+3+…+100。
分析,
算法1:直接写出算式
S1,result=1+2+3+4+5+…+100
很简单。但是写都写得累死了。
算法2:
考虑到1+2+3+…+100可以改写为:(((1+2)+3)+…+100)
S1,p1=1+2
S2,p2=p1+3
S3,p3=p2+4

S99,p99=p98+100,结果在p99里。
此算法也一样麻烦,要写99步,同时要使用99个变量。本算法同样不适合编程。
但是可以从本算法看出一个规律。即:每一步都是两个数相加,加数总是比上一步的加数增加1,被加数总是上一步加法运算的和。可以考虑用一个变量i存放加数,一个变量p存放上一步的和。那么每一步都可以写成:p+i,然后让p+i的和存入p,即:每一步都是p=p+i。也就是说p既代表被加数又代表和。这样可以得到算法3。执行完步骤S99后,结果在p中。
算法3:
S0,p=0,i=1
S1,p=p+i,i=i+1
S2,p=p+i,i=i+1
S3,p=p+i,i=i+1

S99,p=p+i,i=i+1
算法3从表面上看与算法2差不多,同样要写99步。但是从算法3可以看出S1~S99步骤实际上是一样的,也就是说S1~S99同样的操作重复做了99次。计算机对同样的操作可以用循环完成,循环是计算机工作的强项(计算机高速度运算)。算法4就是在算法3的基础上采用循环功能的算法实现。
算法4:
S0,p=0,i=1(循环初值)
S1,p=p+i,i=i+1(循环体)
S2,i<=100,则返回重新执行步骤S1及S2;否则,算法结束(循环控制)。
p中的值就是1+2+…+100的值。
算法4描述的是一个典型的循环结构程序,其流程图如图2.6所示,N-S图如图2.7所示。
事实上,许多问题的求解都归结为重复执行的操作,比如数值计算中的方程迭代求根,非数值计算中的对象遍历。重复执行就是循环,循环是计算机特别擅长工作之一。
循环并不是简单地重复,每次循环,操作的数据(状态、条件)都可能发生变化。
循环的动作是受控制的,比如满足一定条件才继续做,一直做到某个条件满足或者做多少次结束等。也就是说重复工作需要进行控制——循环控制。C语言提供了三种循环控制语句(不考虑goto和if构成的循环),构成了三种基本的循环结构:
while语句构成的循环结构(“当型循环”)
do-while语句构成的循环结构(“直到型循环”)
for语句构成的循环结构(“当型循环”)
2.3.2 当型循环while
while循环的一般形式:
while (表达式)
语句其中:表达式称为“循环条件”,语句称为“循环体”。执行过程为:
① 先计算while后面表达式的值,如果其值为“真”(非0)则执行循环体。
② 执行完循环体后,再次计算while后面表达式的值,如果其值为“真”则继续执行循环体,如果表达式的值为“假”(0),退出循环。
使用while语句需要注意以下几点:
① while语句的特点是先计算表达式的值,然后根据表达式的值决定是否执行循环体中的语句。因此,如果表达式的值一开始就为“假”,那么循环体一次也不执行。
② 当循环体为多个语句组成时,必须用“{ }”括起来,构成复合语句。
③ 在循环体中应有使循环趋于结束的语句,以避免“死循环”的发生。
对于例2.11,用while语句实现的C语言程序如下:
#include <stdio.h>
void main( )
{
int i=1,sum=0;
while (i<=100)
{
sum=sum+i;
i++;
}
printf("%d\n",sum);
}
编制循环程序要注意下面几个方面:
⑴ 遇到数列求和,求积的一类问题,一般可以考虑使用循环解决。
⑵ 注意循环初值的设置。一般对于累加器常常设置为0,累乘器常常设置为1。
⑶ 循环体中做需要重复的工作,同时要保证使循环逐渐趋于结束。循环的结束由while中的表达式(条件)控制。
从这个问题我们可以看出,循环对我们编程带来很大方便,循环也是程序设计不可或缺的。
再来分析例2.6(大写字符转换为小写字符),根据我们设计的程序,一次只能转换一个字符,如果要再次转换另一个字符必须重新运行程序,那么是否有办法不重新运行程序而一次转换多个字符呢?实际上这个问题可以方便地借助循环来解决。
【例2.12】 输入一个字符,如果是大写字母,转换为小写,如果不是则不转换。输入“0”结束程序。
#include <stdio.h>
void main( )
{
char ch;
while ((ch=getchar( ))!='0') /*如果输入的字符不是‘0’则执行循环体*/
{
ch= (ch>='A'&&ch<='Z')? (ch+32),ch;
printf("%c",ch);
}
}
我们注意到,对于该程序如果输入0直接退出,不会输出。如果现在要求字符0也应该输出,程序应该如何修改?这很简单,只要在循环体外,再增加一条printf("%c",ch);语句就可以了。还有另外一种方法,就是先执行语句,然后再进行条件的判断。实现这种功能的语句就是do…while循环。
在这里我们使用函数getchar()。getchar()是系统提供的字符输入函数。其作用是从系统隐含指定的输入设备(如键盘)输入一个字符,没有参数。其常用形式为:
ch=getchar();从键盘输入一个字符,赋值给字符变量ch;
getchar();等待输入,输入值不赋给任何变量。一般用于显示屏幕运行情况,按任意键程序继续运行。
与其对应的函数putchar()是字符输出函数。使用格式:
putchar(c);c为待输出的字符变量。
如:c=‘a’;putchar(c);输出字符a;
putchar(‘\n’);输出换行符。
2.3.3 直到型循环do-while
do-while语句的一般形式是:
do
{
语句;
}while(表达式);
其中:表达式称为“循环条件”,语句称为“循环体”。执行过程是:
① 执行do后面循环体语句。
② 计算while后面表达式的值,如果其值为“真”(非0),则继续执行循环体,如果表达式的值为“假”(0),退出循环。
do-while语句的执行流程如图2.8所示。
说明:
① do-while循环,总是先执行一次循环体,然后再求表达式的值,因此,无论表达式是否为“真”,循环体至少执行一次。
② do-while循环与while循环十分相似,它们的主要区别是:while循环先判断循环条件再执行循环体,循环体可能一次也不执行。do-while循环先执行循环体,再判断循环条件,循环体至少执行一次。
③ 当do-while语句的循环体语句中只有一条语句时,也可不用花括号;但是加上花括号,可增加程序的可读性。
【例2.13】 设计一个菜单,格式如下:
学生成绩管理系统
1,学生成绩输入
2,平均成绩统计
3,学生成绩查询
4,学生成绩修改
0.系统退出要求输入数字1-4,显示所输的数字;输入数字0,退出程序;其他不做任何操作。
分析:根据设计要求,程序应该重复显示,直到输入数字0,程序退出。属于直到型循环,因此可以用do-while实现。
程序:
#include <stdio.h>
void main( )
{
int x;
do
{
printf("********************\n");
printf(" 学生成绩管理系统 \n");
printf("********************\n");
printf(" 1,学生成绩输入 \n");
printf(" 2,平均成绩统计 \n");
printf(" 3,学生成绩查询 \n");
printf(" 4,学生成绩修改 \n");
printf(" 0,系统退出 \n");
printf("********************\n");
scanf("%d",&x);
if (x>=1&&x<=4)
printf("你选择的是第%d项命令!\n",x);
}while(x!=0);
}
【例2.14】 计算:1+1/2+1/4+…+1/50
分析:观察数列1,1/2,1/4,…,1/50。除第一项为1外,其它项分子全部为1,分母全部是偶数。同样考虑用循环实现。其中累加器用sum表示(初值设置为第一项1,以后不累加第一项),循环控制用变量i(i:2-50)控制,数列通项:1/i。
下面分别是用while循环和do-while循环实现的程序:
#include <stdio.h>
void main( )
{
float sum=1;
int i=2; /*表达式1*/
while (i<=50) /* 表达式2*/
{
sum=sum+1.0/i;
i+=2; /*表达式3*/
}
printf("%f\n",sum);
}
while方案
#include <stdio.h>
void main( )
{
float sum=1;
int i=2; /*表达式1*/
do
{
sum=sum+1.0/i;
i+=2; /*表达式3*/
} while (i<=50); /* 表达式2*/
printf("%f\n",sum);
}
do-while方案
我们注意到,很多循环都有一个控制循环的变量,如上例的变量“i”,在进入循环前,为该变量赋初值(表达式1),根据该变量的值确定是否结束循环(表达式2),在循环体内改变该变量的值(表达式3)使循环趋于结束。对于此类循环结构,C语言提供了一种更紧凑的结构——for循环。
2.3.4 当型循环for
for语句的一般形式是:
for (表达式1;表达式2;表达式3)
循环体;
其中,for是关键字,其后有3个表达式,各表达式用“;”分隔。3个表达式可以是任意的表达式,通常主要用于for循环控制。
for循环执行过程如下:
① 计算表达式1。
② 计算表达式2,若其值为真(非0,表示循环条件成立),则转③;若其值为假(0,循环条件不成立),则转⑤。
③ 执行循环体。
④ 计算表达式3,然后转②判断循环条件是否成立。
⑤ 结束循环,执行for循环之后的语句。
for循环的执行流程如图2.9所示。
例2.14的for 方案:
#include <stdio.h>
void main( )
{
int i;
float sum=1;
for (i=2;i<=50;i+=2)
sum=sum+1.0/i;
printf("%f\n",sum);
}
注意:若循环体包含一条以上的语句,则必须用一对花括号括起来构成复合语句;否则将仅把第一条语句作为循环体的内容,这样就会产生逻辑错误。
【例2.15】 求正整数n的阶乘n!,其中n由用户输入。
解:n!=1×2×…×n;设置变量fact为累乘器(被乘数),i为乘数,兼做循环控制变量。
#include <stdio.h>
void main( )
{
float fact;
int i,n;
scanf("%d",&n);
for (i=1,fact=1.0; i<=n; i++)
fact=fact*i;
printf("%.0f\n",fact);
}
因此,for语句最容易理解、最常用的形式是:
for (循环变量赋初值;循环条件;循环变量修正)
循环体;
说明:
⑴ for语句中的表达式1,表达式2,表达式3都可以省略,甚至三个表达式都可以同时省略,但是起分隔作用的“;”不能省略。
⑵ 如果省略表达式1,即不在for语句中给循环变量赋初值,则应该在for语句前给循环变量赋初值。如:
fact=1.0;
for (i=1; i<=n; i++) 等价于:
fact=fact*i;
fact=1.0; i=1;
for ( ; i<=n; i++)
fact=fact*i;
⑶ 如果省略表达式2,即不在表达式2的位置判断循环终止条件,也就是认为表达式2始终为“真”,此时循环将无终止地进行,则应该在其它位置(如循环体中)安排检测及退出循环的机制。
for (i=1,fact=1.0; ; i++)
{
fact=fact*i;
if (i==n)
break;
}
⑷ 如果省略表达式3,即不在此位置进行循环变量的修改,则应该在其它位置(如循环体中)安排使循环趋向于结束的工作。
for(i=1,fact=1.0; i<=n; )
{
fact=fact*i;
i++;
}
⑸ 表达式1可以是设置循环变量初值的表达式(常用),也可以是与循环变量无关的其它表达式;表达式1、表达式3可以是简单表达式,也可以是逗号表达式。
⑹ 表达式2一般为关系表达式或逻辑表达式,也可以是数值表达式或字符表达式,事实上只要是表达式就可以。
例如:
for ( ; (c=getchar( ))!='\n'; i+=c)
printf("%c",c);
小结:从上面的说明可以看出,C语言的for语句功能强大,使用灵活,可以把循环体和一些与循环控制无关的操作也作为表达式出现,程序短小简洁。但是,如果过分使用这个特点会使for语句显得杂乱,降低程序可读性。建议不要把与循环控制无关的内容放在for语句的三个表达式中,这是程序设计的良好风格。
从前面循环结构的语法和例子的介绍,我们可以看出循环结构由4部分组成:
(1)循环变量、条件(状态)的初始化。
(2)循环变量、条件(状态)检查,以确认是否进行循环。
(3)循环变量、条件(状态)的修改,使循环趋于结束。
(4)循环体处理的其它工作。
2.3.5 几种循环的比较
C语言中,三种循环结构(不考虑用if和goto构成的循环)都可以用来处理同一个问题,但在具体使用时存在一些细微的差别。如果不考虑可读性,一般情况下它们可以相互代替。
⑴ 循环变量初始化:while和do-while循环,循环变量初始化应该在while和do-while语句之前完成;而for循环,循环变量的初始化可以在表达式1中完成。
⑵ 循环条件:while和do-while循环只在while后面指定循环条件;而for循环可以在表达式2中指定。
⑶ 循环变量修改使循环趋向结束:while和do-while循环要在循环体内包含使循环趋于结束的操作;for循环可以在表达式3中完成。
⑷ for循环可以省略循环体,将部分操作放到表达式2、表达式3中,for语句功能强大。
⑸ while和for循环先测试表达式,后执行循环体,而do-while是先执行循环体,再判断表达式。(所以while、for循环是典型的当型循环,而do-while循环可以看作是直到型循环)。
⑹ 三种基本循环结构一般可以相互替代,不能说哪种更优越。具体使用哪一种结构依赖于程序的可读性和程序设计者个人程序设计的风格(偏好)。我们应当尽量选择恰当的循环结构,使程序更加容易理解。(尽管for循环功能强大,但是并不是在任何场合都可以不分条件使用)。
【例2.16】 输出9*9口诀。
分析:分行与列考虑,共9行9列,可用i控制行、j控制列。
#include "stdio.h"
void main( )
{
 int i,j;
 printf("\n\n");
 for (i=1;i<=9;i++)
{ for (j=1;j<=i;j++)
printf("%d*%d=%-3d",i,j,i*j); /* -3d表示左对齐,占3位 */
 printf("\n"); /* 输出完一行后换行 */
}
}
【例2.17】 古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少?
分析:兔子的规律为数列1,1,2,3,5,8,13,21....
#include "stdio.h"
void main( )
{
long f1=1,f2=1;
int i;
for(i=1;i<=20;i++)
 {
printf("%12ld %12ld",f1,f2);
if (i%2==0)
printf("\n"); /* 控制输出,每行四个 */
f1=f1+f2; /* 计算第三个月的兔子数 */
f2=f1+f2; /* 计算第四个月的兔子数 */
 }
}
2.4 break和continue语句前面介绍的三种循环结构都是在执行循环体之前或之后通过对一个表达式的测试来决定是否终止对循环体的执行。有时出于程序设计的需要,需要提前结束循环体。这时我们可以利用系统提供的break语句立即终止循环的执行,而转到循环结构的下一语句处执行。循环中还有一种情况是,根据执行结果,本次循环不需要(或不能)执行到最后,就应开始下一次循环。这可以利用continue语句结束本次循环,开始下一次循环。
2.4.1 break语句
break语句的一般形式为:
break;
break语句的执行过程是:终止对switch语句或循环语句的执行(跳出这两种语句),而转移到其后的语句处执行。
说明:
break语句只用于循环语句或switch语句中。在循环语句中,break常常和if语句一起使用,表示当条件满足时,立即终止循环。注意break不是跳出if语句,而是跳出循环结构。
【例2.18】 判断101-200之间有多少个素数,并输出所有素数。
分析:判断素数的方法是用一个数m分别去除2到sqrt(m),如果能被整除,则表明此数不是素数,反之是素数。显然这个问题需要两层循环,外层循环从101-200,分别判断这之间的数m。内层循环是将该数被2到sqrt(m)去除。但是如果m能被某数i除尽,则m不是素数,循环可以提前结束。
C语言程序:
#include <stdio.h>
#include <math.h>
void main( )
{
 int m,i,k,count=0,leap;
 printf("\n\n");
 for (m=101; m<=200; m+=2)
{
k=sqrt(m); leap=1;
 for (i=2;i<=k;i++)
  if (m%i==0)
 { leap=0; break; } /*循环被中断,leep置0表示非素数*/
if (leap==1)
{ printf("%-5d",m); count++; } /*输出m,素数个数加1 */
}
printf("\nThe total is %d",count);
}
循环语句可以嵌套使用,break语句只能跳出其所在的循环,而不能一下子跳出多层循环。要实现跳出多层循环可以设置一个标志变量,控制逐层跳出。如:

for (…)
{

flag=0; /*标志变量初始化*/
for (…)
{

if (…)
{ flag=1; break; } /*如果满足一定条件需要跳出各层循环,先设置标志变量,然后跳出本层循环*/

}
if (flag==1) break; /*继续跳出外层循环*/
}

【例2.19】 从键盘上连续输入字符,并统计其中大写字母的个数,直到输入“换行”字符时结束。
分析:该问题while的条件可以用“死循环”,以if语句控制循环的结束。
#include <stdio.h>
void main( )
{
char ch;
int sum=0;
while (1) /*条件总为1*/
{
ch=getchar( );
if (ch=='\n')
break; /*结束循环*/
if (ch>='A'&&ch<='Z')
sum++;
}
printf("%d\n",sum);
}
2.4.2 continue语句
continue语句的一般形式是:
continue;
continue语句的功能是结束本次循环,即跳过本层循环体中余下尚未执行的语句,接着再一次进行循环条件的判定。注意:与break语句不同,执行continue语句并没有使整个循环终止。
在while和do-while循环中,continue语句使流程直接跳到循环控制条件的测试部分,然后决定循环是否继续执行。在for循环中,遇到continue后,跳过循环体中余下的语句,而去对for语句中的表达式3求值,然后进行表达式2的条件测试,最后决定for循环是否执行。
【例2.20】 从键盘输入30个字符,统计其中数字字符的个数。
#include <stdio.h>
void main( )
{
int i,sum=0;
char ch;
for (i=1; i<=30; i++)
{ ch=getchar( );
if (ch<'0'||ch>'9')
continue;
sum++;
}
printf("%d\n",sum)
}
其执行流程如图2.10所示。注意break语句和continue语句的主要区别:continue语句只终止本次循环,而不是终止整个循环结构的执行;break语句是终止循环,不再进行条件判断。
图2.10 例2.20程序流程图
2.5 goto语句
1,语句标号语句标号:一个标识符+“:”,它表示程序指令的地址。语句标号的标识符遵守“标识符”命名规定。
例如:
loop,ERR:
2,goto语句
goto语句是无条件转移(转向)语句。格式为:
goto 语句标号;
goto语句的功能:程序无条件转移到“语句标号”处执行。
结构化程序设计方法主张“限制”(注意不是“禁止”)使用goto语句。因为goto语句不符合结构化程序设计的准则——模块化。无条件转移使程序结构无规律,可读性变差。但是,任何事情都是一分为二的,如果能大大提高程序的执行效率,也可以使用。
goto语句常见的两种用途:
① if/goto构成循环——被while、do-while和for代替例:仅仅作为goto语句标号概念的理解,实际建议不要这样使用。
【例2.21】 计算1+2+3…+100。
分析:可用if和goto构成循环。
程序如下:
#include <stdio.h>
void main( )
{
int i=1,sum=0;
LOOP:
sum+=i;
i++;
if (i>100) goto PRT;
goto LOOP;
PRT:
printf("sum=%d\n",sum);
}
② 从循环体跳到循环体外——被break和continue代替
2.6 经典算法举例
1.穷举法求解不定方程
【例2.22】 百钱百鸡问题:公鸡5文钱买1只,母鸡3文钱买1只,小鸡1文钱买3只。用100文钱,买100只鸡,问公鸡、母鸡、小鸡各有几只?
分析:该问题有三个变量,公鸡数、母鸡数、小鸡数,但只能列出两个方程:
x+y+z=100
5x+3y+z/3=100
这是一个不定方程,解决这类问题,可以先假设x的值,再设y的值,找出满足条件的z值。为找出所有的解答,需要验证所有满足条件的取值。因此x应该从1循环到20(第二个方程约束条件),在内层y从1循环到33,在y循环的内层z从3循环到100,如果两个方程都满足就是我们需要的解答。
参考程序:
#include <stdio.h>
void main( )
{
int x,y,z;
printf("*** 百鸡问题 ***\n");
for (x=1;x<20;x++)
for (y=1;y<33;y++)
for (z=3;z<100;z+=3)
if (5*x+3*y+z/3==100 && x+y+z==100)
printf("cock:%d,hen:%d,chicken:%d\n",x,y,z);
}
上面的程序使用了三层循环来解决问题,程序结构简单明了。但是我们设计程序不仅要正确无误,还要注意程序的执行效率。
一般来说,在循环嵌套中,内层循环执行的次数等于该循环嵌套结构中每一层循环重复次数的乘积。
例如,上面的程序中,外层每循环一次,第二层要循环32次,而第三层要循环32*33=1056次。这样程序执行下来,最内层的if语句要执行19*32*33次。所以我们在编写程序时,需要考虑尽可能的减少循环执行的次数,特别是循环的嵌套。
对于“百鸡问题”,由方程组:x+y+z=100,5x+3y+z/3=100可以导出:
x=4z/3-100
y=100-x-z
这样就只有z一个未知数,如果知道了z就可以求出x值,进而求出y值。因此我们只要将z作为循环变量就可以了。
#include <stdio.h>
void main()
{
int x,y,z;
printf(“*** 百鸡问题 ***\n”);
for (z=3;z<100;z+=3)
{
x=4*z/3-100;
y=100-x-z;
if (5*x+3*y+z/3==100 && x+y+z==100)
printf("cock:%d,hen:%d,chicken:%d\n",x,y,z);
}
}
尽管每次循环执行的语句增多了,但是循环次数只有33次,运行效率大大提高。
从上面的分析可以看到,一个好的算法可以提高程序的执行效率,但是要设计出一个好的算法却要花费很大的精力,且有时提高效率的同时可能会降低程序的可读性。如上例就是如此。
如何掌握好程序的易读性和程序的效率之间的关系,因需要不同重点也就不同。如在处理实时问题时,效率应该优先;而程序量不大,计算机速度又非常快的情况下,效率就不是很重要。
【例2.23】有一本书,被人撕掉了其中一页。已知剩余页码之和为140,问这本书原来共有多少页?撕掉的是哪一页?
分析:书的页码总是从第1页开始,每张纸的页码均为奇数开头,结束页未必是偶数,一页纸上有两个连续的页码。设为x、x+1,由前面的分析知道x为奇数。设n表示原书的页码数,总页码之和为s,因为页码之和为140,所以n<20。
由此可以写出不定方程:s-x-(x+1)=140 其中1≤x≤n-1且为奇数。
参考程序:
#include <stdio.h>
void main()
{
int n=1,s=0,x;
clrsc(); /*清屏函数*/
do
{
s=s+n;
for(x=1;x<=n-1;x+=2)
if(s-x-(x+1)==140) printf(“%d,%d,%d,%d”,n,s,x,x+1);
n++;
}
while(n<=20);
}
这种算法也称为尝试法、枚举法,其核心是全面排查,找出满足条件的所有情况。程序设计简单,但只能解决有限情况的场合。
2.递推法(逆向思维)
【例2.24】猴子吃桃问题:猴子第一天摘下若干个桃子,当即吃了一半,还不瘾,又多吃了一个第二天早上又将剩下的桃子吃掉一半,又多吃了一个。以后每天早上都吃了前一天剩下的一半零一个。到第10天早上想再吃时,见只剩下一个桃子了。求第一天共摘了多少。
分析:采取逆向思维的方法,由第10天递推到第9天,依次向前一直推到第一天。
参考程序:
#include <stdio.h>
void main()
{
int day,x1,x2;
day=9;
x2=1;
while(day>0)
 {x1=(x2+1)*2;/*第一天的桃子数是第2天桃子数加1后的2倍*/
 x2=x1;
 day--;
 }
printf("the total is %d\n",x1);
}
3.逻辑问题求解
【例2.25】 某班有四位同学中的一位恶作剧,但是谁都不承认。A说:不是我;B说:是C;C说:是D。D说:C胡说。
已知其中三个人说的是真话,一个人说的是假话。编写程序根据这些信息,找出恶作剧的同学。
分析:为解这道题,需要逻辑思维与判断,下面,我们把四个人说的四句话写成关系表达式。在声明变量时,让thisman表示要找的人,定义它是字符变量
char thisman;
让“==”的含义为“是”,让“!=”的含义为“不是”
A说:不是我。 写成关系表达式为(thisman!='A')
B说:是C。 写成关系表达式为(thisman=='C')
C说:是D。 写成关系表达式为(thisman=='D')
D说:C胡说。 写成关系表达式为(thisman!='D')
如何找到该人,一定是“先假设该人是恶作剧者,然后到每句话中去测试看有几句是真话”,有三句是真话就确定是该人,否则换下一人再试。
比如,先假定是A同学,让thisman='A',代入到四句话中:
A说:thisman!='A'; 'A'!='A' 假,值为0。
B说:thisman=='C'; 'A'=='C' 假,值为0。
C说:thisman=='D'; 'A'=='D' 假,值为0。
D说:thisman!='D'; 'A'!='D' 真,值为1。
显然,不是'A'做的好事(四个关系表达式值的和为1)
再试B同学,让thisman='B',代入到四句话中:
A说:thisman!='A'; 'B'!='A' 真,值为1。
B说:thisman=='C'; 'B'=='C' 假,值为0。
C说:thisman=='D'; 'B'=='D' 假,值为0。
D说:thisman!='D'; 'B'!='D' 真,值为1。
显然,不是'B'所为(四个关系表达式值的和为2)
再试C同学,让thisman='C',代入到四句话中:
A说:thisman!='A'; 'C'!='A' 真,值为1。
B说:thisman=='C'; 'C'=='C' 真,值为1。
C说:thisman=='D'; 'C'=='D' 假,值为0。
D说:thisman!='D'; 'C'!='D' 真,值为1。
显然,就是'C'做了坏事(四个关系表达式值之和为3)这时,我们可以理出头绪,要用所谓枚举法,一个人一个人地去试,也就是依次用'A'、'B'、'C'、'D'与'A'、'C'、'D'、'D'比较,四句话中有三句为真,该人即所求。从编写程序的角度看,最好用循环结构。
在C语言中字符也是有数值的,这个数值就是字符的ASCII码值,如:
字符 A B C D
ASCII码值 65 66 67 68
字符存放在内存中是以ASCII码的形式存放的,因此,用赋值语句
thisman = 'A';
thisman = 65;
两者是等效的,在内存中存的都是65。这样我们就可以用65、66、67、68分别代表'A'、'B'、'C'、'D'参与比较。
以下是主要程序段,请读者自己补充为完整程序。
for (k=0; k<=3; k=k+1)
{ /* 循环体开始 */
thisman = 65+k; /* 产生被试者,依次给 */
/* thisman赋值为'A'、'B'、'C'、'D' */
sum = (thisman!='A')+ /* 'A'的话是否为真 */
(thisman=='C')+ /* 'B'的话是否为真 */
(thisman=='D')+ /* 'C'的话是否为真 */
(thisman!='D'); /* 'D'的话是否为真 */
if (sum==3)
{
printf("This man is %c\n",thisman);
break; /* 得到答案,结束循环 */
}
} /* 循环体结束 */
【例2.26】两个乒乓球队进行比赛,各出三人。甲队为a,b,c三人,乙队为x,y,z三人。已抽签决定比赛名单。有人向队员打听比赛的名单。a说他不和x比,c说他不和x,z比。请编程序找出三队赛手的名单。
参考程序:
#include <stdio.h>
void main()
{
char i,j,k;/*i是a的对手,j是b的对手,k是c的对手*/
for(i='x';i<='z';i++)
 for(j='x';j<='z';j++)
 {
 if(i!=j)
for(k='x';k<='z';k++)
{ if(i!=k&&j!=k)
 { if(i!='x'&&k!='x'&&k!='z')
 printf("order is a--%c\tb--%c\tc--%c\n",i,j,k);
 }
}
 }
}
4.迭代法解方程
牛顿迭代法:设方程f(x)=0有实数根,若能够将方程等价地转化成x=g(x),取一个初始值x 代入x=g(x)的右端算得x1=g(x0),依次再计算x2=g(x1)类推可得序列
xk+1=g(xk),k=0,1,2,…。
称此序列为由迭代函数g(x)产生的迭代序列,x0为迭代初始值。若该迭代序列收敛,则它的极限就是方程f(x)=0 的一个根,xk称为方程根的k次近似值。称使得迭代法收敛的初始值的取值范围为迭代收敛域。如图2.11。
由图2.11可以看出,
f’(x0)=f(x0)/(x1-x0)
x1=x0-f(x0)/f’(x0)
这就是牛顿迭代公式。
【例2.27】用牛顿迭代法求方程在1.5附近的根。
2x3-4x2+3x-6=0
解:f’(x)=6x2-8x+3=(6x-8)x-6
参考程序:
#include <math.h>
void main()
{
float x,x0,f,f1;
x=1.5;
do
{
x0=x;
f=((2*x0-4)*x0+3)*x0-6;
f1=(6*x0-8)*x0+3;
x=x0-f/f1;
}
while(fabs(x-x0)>1e-5);
print(“The toot of equation is %5.2f\n”,x);
}
5.二分法解方程二分法解方程的基本思想:
设根x在(a,b)间(注意选正确的区间)
(1)取a,b的中间值c=(a+b)/2,将根区间分为两半,判断根在哪个区间。有三种情况:
(2)f(c) <= 精度,C为求得根;
(3)若f(c)*f(a) < 0,求根区间在[a,c],b=c,转(1);
(4)若f(c)*f(a) > 0,求根区间在[c,b],a=c,转(1)。
该算法只能求出一个根,解3次方程适用。
【例2.28】求方程2x3-4x2-+3x-6=0 在(-10,10)之间的根。
参考程序:
#include <math.h>
void main()
{
float x0,x1,x2,fx0,fx1,fx2;
do{
printf(“Enter x1 & x2:”);
scanf(%f%f”,&x1,&x2);
fx1=x1*(x1*(2*x1-4)+3)-6;
fx2=x2*(x2*(2*x2-4)+3)-6;
}while(fx1*fx2>0);
do{
x0=(x1+x2)/2;
fx0=x0*(x0*(2*x0-4)-6;
if((fx0*fx1)<0)
{x2=x0;
fx2=fx0;
}
else
{x1=x0;
fx1=fx0;
}
}while(fabs(fx0)>1e-5);
printf(“x=%6.2f\n”,x0);
}
习题二选择题
1.以下的for循环 ( ) 。
for(x=0,y=0; (y!=123)&&(x<4); x + + );
A) 是无限循环 B)循环次数不定 C)执行4次 D)执行3次
2.C语言中 ( ) 。
A)不能使用do-while语句构成的循环
B)do-while语句构成的循环必须用break语句才能退出
C)do-while语句构成的循环,当while语句中的表达式值为非零时结束循环
D)do-while语句构成的循环,当while语句中的表达式值为零时结束循环
3.语句while(!E);中的条件!E等价于 ( ) 。
A)E = = 0 B)E!=1 C)E!=0 D)~E
4.有以下程序段
int k=0;
while(k=1) k++;
while循环执行的次数 ( )。
A)无限次 B)有语法错,不能执行 C)一次也不执行 D)执行1次填空题
1.若有以下程序
main()
{int a=4,b=3,c=5,t=0;
if(a<b) {t=a;a=b;b=t;}
if(a<c) t=a;a=c;c=t;
printf("%d %d %d\n",a,b,c);
}
执行后输出结果是______。
2.能表述"20<x<30"或"x<-100"的C语言表达式是__ ____。
3.若有以下程序:
main()
{ int i=10,j=0;
do
{
j=j+i; i--;
}while(i>2);
printf("%d\n",j);
}
运行程序后的结果为______________。
4.下面的程序是求1*2*3*…10的,请填空:
main()
{int I,s=_____;
_____
s=s*I;
printf("%d\n",s);
}
5.以下程序的功能是:从键盘上输入若干个学生的成绩,统计并输出最高成绩和最低成绩,当输入负数时结束输入.请填空.
main()
{
float x,amax,amin;
scanf("%f",&x);
amax=x; amin=x;
while(_______)
{
if(x>amax) amax=x;
if(________) amin=x;
scanf("%f",&x);
}
printf("\namax=%f\namin=%f\n",amax,amin);
}
三.编程题
1.一个整数,它加上100后是一个完全平方数,再加上168又是一个完全平方数,请问该数是多少?
2.给一个不多于5位的正整数,要求:①求出它是几位数;②分别打印出每一位数字;③按逆顺序打印出各位数字,例如原来为321,应输出123。
3.求Sn=a+aa+aaa+…+aa…aaa(有n个a)之值,其中a是一个数字。例如:2+22+222+2222+22222(n=5),n由键盘输入。
4.一个数如果恰好等于它的因子之和,这个数就称为"完数"。例如,6的因子为1、2、3,而6=1+2+3,因此6是"完数"。编程序找出1000之内的所有完数,并按下面格式输出其因子:
6 its factors are 1、2、3
5.一球从100米高度自由下落,每次落地后返回原高度的一半,再落下。求它在第10次落地时共经过多少米?第10次反弹多高?
6.用迭代法求x=。求平方根的迭代公式为:
Xn+1=(Xn+1/Xn)/2
要求前后两次求出的得差的绝对值少于0.00001。
7.有一分数序列:
2/1,3/2,5/3,8/5,13/8,21/13…
求出这个数列的前20项之和。
8.输入一行字符,分别统计出其中英文字母、空格、数字和其他字符的个数。
第三章 模块化程序设计
“分而治之”是解决复杂问题的常用方法。大的问题可以分成若干小问题,小问题还可以分为更小的问题。把一个个小问题逐一解决了,大问题也就随之解决了。所谓模块分解就是基于这种思想,采用模块化程序设计的方法,将一个复杂的较大的问题,分解为一个个独立的简单模块,分别解决简单的模块,进而解决复杂问题。
在C语言中,这些简单独立的模块是由一个个函数实现的。我们编写程序的过程就是编写一个个函数的过程。本章将通过实例学习函数的定义及使用。
3.1 模块化程序设计的方法与特点模块化程序设计首先要进行模块分解,模块的分解总结起来主要有两种:功能分解法和面向对象法。面向对象法是目前的主流方法,但在结构化程序设计中,主要还是采用功能分解法。而且,面向对象设计在每个对象内部还是使用功能分解进行模块划分。因此,功能分解法是面向对象法的基础。
功能分解是一个自顶向下、逐步求精的过程。具体的说就是将一个复杂的大问题,分解为几个功能相对独立的模块,再将几个模块,根据情况分解为小模块,还可以继续分解为更小的模块。模块划分的原则是:高聚合、低耦合。高聚合是对模块本身的要求,高聚合的模块功能应该单一,不能身兼数职。一个模块就解决一个单一问题。低耦合是对模块之间关系的要求,希望模块间的联系越小越好。最终的模块便于程序编写,特别是多个模块有共同的功能(比如多个模块都涉及阶乘的计算),一定要单独分出一个模块,便于共享。
【例3.1】编程求解。
分析:该问题显然可以使用多层的嵌套循环编写,但是很麻烦,容易出错。如果我们将其分解为以下4个模块,① 输入n和k的值,② 乘方运算,即计算1k、2k、…nk,③ 求和运算,即将乘方运算所得结果求和,将得到1k+2k+…nk,④ 输出结果。分别编写一个乘方函数、求和函数,再加上输入输出,然后将几个函数连接成一个完整的程序,问题将变得十分简单。
【例3.2】从键盘上输入两个数,分别求出这两个数的最小公倍数和最大公约数。
分析:该问题可以在主函数中完成数据(x,y)的输入和结果的输出,一个函数计算最小公倍数,一个函数求解最大公约数。每个函数需要接受主函数传递的x和y的值,分别返回计算的结果。
由此我们可以看出模块化程序设计有如下特点:
(1)模块相对独立,功能单一。编写相对简单,可以独立编写调试。
(2)可集体开发,缩短开发周期。不同的模块可以由不同的人员开发,最终能够合成完整的程序。
(3)开发出的模块,可在不同的应用程序中多次使用,减少重复劳动,提高开发效率。
(4)测试、更新以模块为单位进行而不会影响其他模块。
在C语言中,一个功能模块就可以编写一个函数。用户可以根据需要定义任意多个自己的函数,供公共选用。另外,为了方便用户的编程,系统还提供了丰富的库函数。不同的编译系统提供了不同数量和功能的库函数,但一般都提供了ANSI C标准所建议的数百个库函数。
现在的问题是如何将几个模块(函数)连接成一个完整的程序。
3.2函数的定义下面的程序是从键盘输入一个字符,如果是大写字母就将其转换为小写输出,否则,直接输出。
#include<stdio.h>
void main( )
{
char ch;
ch=getchar( );/*调用字符输入函数,输入一个字符*/
ch=(ch>='A'&&ch<='Z')?(ch+32):ch;/*如果是大写字母就转换为小写*/
printf("%c",ch);/*调用输出函数,将字符输出*/
}
该程序调用了两个系统提供的函数:getchar()和printf()。现在比较两个函数使用方法,由于getchar()需要得到从键盘输入的字符,通过“ch=getchar();”语句赋给变量ch。也就是说,getchar()有返回值。printf()只是输出结果,没有返回值。但是,printf()需要提供输出的格式和待输出的变量,因此括弧内包含了“%c”和ch,这就给printf()函数提供了参数,getchar()函数不需要提供参数,所以括号内为空。
现在我们明白了,当前函数参与运算的数据可以由调用函数传递,当前函数运算的结果,可以返回给主调函数。
函数定义的一般格式:
<数据类型> 函数名 (<参数表>)
{
<说明语句>
<执行语句>
}
说明:<数据类型>是由当前函数返回到主调函数的数据类型,可以是我们前面我们介绍的任何类型;<函数名>是为当前函数起的名字,命名规则同变量的命名规则相同;<参数表>是主调函数传递给当前函数的数据,同时说明数据类型;{ }部分是当前函数的函数体,其编写方法与主函数的编写方法一样。
由此,我们可以写出例3.1的参考程序。
/*定义sop()函数*/
int sop(int m,int t) /*整型自定义函数,m,t 为形参*/
{ /*自定义函数体开始*/
int i,sum,p; /*整型变量i,sum*/
sum=0; /*初始化累加器*/
for (i=1; i<=m; i++ ) /*计数循环(i)*/
{ /*循环体开始*/
p=power(i,t); /*调用power( )函数,返回值赋给变量p*/
sum=sum+p; /* 累加 */
} /*循环体结束*/
return (sum) ; /*返回值sum给函数sop(n,k)*/
} /*自定义函数体结束*/
该函数返回值类型是int,由return(sum)实现结果的返回,由主程序传递了两个整型数m和t。
/*定义函数power()*/
int power(int p,int q) /*整型自定义函数*/
{ /*函数开始*/
int i,product; /*整型变量*/
product=1; /*初始化累乘器*/
for(i=1; i<=q; i++) /*计数循环( i )*/
{ /*循环体开始( i )*/
product=product*p; /*累乘*/
} /*循环体结束( i )*/
return(product); /*累乘值product返回给power*/
} /*函数结束*/
该函数同sop()函数一样,返回值类型是int,由return(product)实现结果的返回,由sop()函数传递了两个整型数p和q。
#include <stdio.h> /*预编译命令*/
void main( ) /*主函数*/
{ /*主程序开始*/
int k,n,sum;
printf(“input:k,n,); /*提示信息*/
scanf(“%d%d”,k,n); /*输入k,n的值 */
sum=sop(n,k); /*调用函数sop,k,n是传递给sop的参数,返回值赋给sum*/
printf("%d\n",sum)); /*输出结果*/
} /*主程序结束*/
我们注意到,自己编写的两个函数名字不再使用main(),因为C语言规定,一个程序只能有一个主函数,其他函数的命名规则同变量一样。在sop()函数中,使用了变量m,t,在power()函数中使用了变量p,q,并没有定义,这是因为这些变量已经由主调函数进行了传递。
通过该例我们可以进一步看出,每个函数一般完成一个相对独立的功能,函数之间连接的关键问题是参数的传递和返回值。在确定了参数传递和返回值后,就可以独立地编写函数了。实际上函数的参数传递有很多类型,下面逐步介绍。
3.3无返回值函数的定义与调用
【例3.3】编写函数实现例2.13。
分析:在例2.13实现的程序中语句printf(,********************\n”)出现了三次,如果将该语句写成一个单独的函数,在使用的时候就可以直接调用了,而不必再完整地书写该语句,并且可以重复调用任意多次。
C语言程序
void print1( ) /* 函数的定义
{
printf("********************\n");
}
void main( )
{
int x;
do {
print1( ); /*函数调用*/
printf(" 学生成绩管理系统 \n");
print1( ); /*函数的再次调用*/
printf(" 1.学生成绩输入 \n");
printf(" 2.平均成绩统计 \n");
printf(" 3.学生成绩查询 \n");
printf(" 4.学生成绩修改 \n");
printf(" 0.系统退出 \n");
print1( ); /*函数的第三次调用*/
scanf("%d",&x);
if(x>=1 && x<=4)
printf("你选择的是%d项命令\n",x);
}while(x);
}
在程序中定义了函数print1()来实现printf(,********************\n”)的功能,一次定义,可以多次调用。print1()函数在调用时没有数据传递,所以定义时函数名后没有参数,我们把这种函数称为无参函数。
无参函数的定义形式:
void 函数名()
{
说明语句;
语句;
}
函数名前面的void表示该函数没有返回值。无参函数一般不需要带回函数值。另外,函数名必须是合法的标识符,其命名规则和变量一样。
无参函数的调用:
函数名();
这里需特别注意的是函数名后的括号不能丢。
说明:
C语言程序由函数构成,一个源程序文件包含一个或多个函数。一个C程序必须包含main函数。
C程序的执行从main函数开始,调用其它函数后最终回到main函数,在main函数中结束整个程序的运行。
一个函数(主调函数)可以多次调用多个函数(被调函数)。同一个函数也可以被一个或多个函数调用任意多次。但不能调用main函数(系统定义的,由系统调用)。
所有函数在定义时都是平行的,相互独立(一个函数并不从属于另一个函数),即函数不能嵌套定义,但可以相互调用。
【例3.4】用函数实现两个数的数值交换。
分析:交换变量的值在C语言编程中属于经典问题,经常用到。本例通过编写函数swap()来实现。由于在函数swap()实现数值交换,而需要交换的数由调用函数提供,这就涉及调用函数的数值如何传递到被调函数的问题。
根据函数有无参数,可以把函数分为无参函数(如例3.3中函数print1)和有参函数两类。对于有参函数来说,在调用函数时,主调函数和被调函数之间有数据传递关系。数据的传递就是通过参数来实现的。函数定义时函数名后面的括号中的变量称为“形式参数”,简称形参;函数调用时,函数名后面括号内的参数称为实际参数,简称为“实参”。
无返回值的有参函数的定义形式:
void 函数名(类型名 形式参数1,类型名 形式参数2,…)
{
说明部分;
语句;
}
有参函数调用的一般形式为:
函数名(实际参数1,实际参数2,…);
C语言程序
void swap(int a,int b)
{
int t;
t=a;
a=b;
b=t;
printf("交换后:%d,%d\n",a,b);
}
void main( )
{
int x=10,y=5;
printf("数字交换\n\n");
printf("未交换前:%d,%d\n",x,y);
swap(x,y);
}
运行结果:
未交换前:10,5
交换后:5,10
程序中,定义了函数swap,因为本函数不需要返回值,所以函数swap在定义时函数名前面写上了关键字void。形参为int型变量,在调用函数swap时,使用了“swap(x,y)”进行调用,在实参和形参之间有数据传递。将实参x、y的值复制了一份,赋给函数swap中的形参x、y。在函数swap内部,通过中间变量t对形参x、y的内容进行了交换。
说明:
对于有参函数,除了例3.4的定义方式以外,还有一种老式的定义形式,对形式参数的类型的说明放在函数定义的第二行,也就是不在第一行的括号内指定形式参数的类型,而在括号外单独指定,如例3.4中的函数swap,如果用这种定义形式,应为:
void swap( a,b)
int a,b;
{
int t;
t=a;
a=b;
b=t;
printf("交换后:%d,%d\n",a,b);
}
有参函数的调用,如果有多个实际参数,则各参数间用逗号分开。如果函数不带参数(无参函数),函数名后的括号内可以空着,但括号不能省略。
实际参数可以是常量、变量或其他表达式。实际参数和形式参数的个数应相等,类型应一致。实际参数与形式参数按顺序对应,一一传递数据。在函数调用时,要进行形参和实参的结合,将实参的值传递给形参,然后再执行函数体。
在[例3-4]中,从运行结果来看,好像变量x,y的值发生了交换,其实通过仔细的分析,可以看到在swap函数中形参a和b的值交换了,但在主调函数中实参x和y并未变化。为什么?
对这个程序,系统是这样执行的:函数调用时给形参分配存储空间(值得注意的是形参和实参在内存中占有不同的存储空间),然后由实参向对应的形参传递数据。这种数据的传递是单向的,只能由实参传递给形参,而不能由形参传递给实参。我们称之为参数的按值传递(简称值传递)。在按值传递的情况下,函数中形参的值虽然改变了,但不会回传给实参,无法最终改变实参的值。另外要注意,在函数调用前,形参并不占内存中的存储单元。只有在发生函数调用时,函数中的形参才被分配内存单元。在函数调用结束后,形参所占的内存单元也被释放。
3.4有返回值函数的定义与调用
【例3.5】从键盘中输入一个年份,判断该年是否是闰年。
分析:我们知道能被4整除,但不能被100整除的年份都是闰年,如1996年、2004年是闰年;②能被100整除,又能被400整除的年份是闰年。如1600年、2000年是闰年。不符合这两个条件的不是闰年。编写函数leap,通过函数的返回值判断该年是否是闰年。如果该函数的返回值(标志位)为1,则该年为闰年;如果该函数的返回值(标志位)为0,则该函数为非闰年。在主函数中输入年份,并输出结果信息。图3.1和图3.2分别是主函数和leap函数的N-S图。
图3.1主调函数的N-S图
图3.2 被调函数leap的N-S图
通过函数调用使主调函数能得到一个确定的值,这个值就是函数的返回值。
一般函数的定义形式:
类型标识符 函数名(形式参数列表)
{
说明部分;
语句;
}
类型标识符表示函数返回数值的类型。其实上面的函数形式是该函数形式的特例。函数的返回值是通过return语句获得的。return语句结束被调函数的执行,将被调函数中的运算结果带回到主调函数,然后继续执行主调函数。
return语句的一般形式:
return (变量或表达式);
也可去掉括号,
return 变量或表达式;
也就是说,有返回值的函数一般要有一个或多个return语句。
C语言程序
#include <stdio.h>
int leap(int year)
{
int st;
if(year%4= =0)
{if(year%100= =0)
{if(year%400= =0)
st=1;
else st=0;}
else
st=1;}
else
st=0;
return(st);
}
void print1( )
{
printf("请输入年份:\n");
}
void main()
{
int year,st;
print1( );
scanf("%d",&year);
printf("\n");
st=leap(year);
if(st= =1)
printf("%d年是闰年",year);
else
printf("%d年不是闰年",year);
}
也可以将函数leap中3-11行语句改写成以下的if语句:
if(year%4!=0)
st=0;
else if(year%100!=0)
st=1;
else if(year%400!=0)
st=0;
else
st=1;
也可以用一个逻辑表达式包含所用的闰年条件,将上述if语句用下面的if语句代替:
if(year%4= =0&&year%100!=0|| (year%400= =0)) st=1;
else st=0;
在主函数中分别调用了函数print1()和函数leap()。我们可以看到调用的方式是不一样的。在调用函数print1时,把函数调用作为一个语句:
print1( );
当不需要函数带回返回值时,常用这种方式。这时函数只完成一定的操作。
在调用函数leap时,函数出现在一个表达式中,
st=leap(year);
我们称这种表达式为函数表达式。这时要求函数带回一个确定的值。即把函数leap的返回值赋给变量st。同样,也可以这样使用:
z=3*leap(year);
即函数是表达式的一部分。
另外,函数调用也可作为一个函数的实际参数。例如:
if(leap(year)= =1)
printf("%d年是闰年",year);
else
printf("%d年不是闰年",year);
}
它的执行过程是用year作为实际参数调用函数leap,得到函数leap的返回值,再将该值作为if语句的判断表达式。
实际上还可以简化:
if(leap(year))
printf("%d年是闰年",year);
else
printf("%d年不是闰年",year);
}
说明:
函数返回值如果是整型的,则类型名可以省略。如[例3-5]中函数leap定义时可以写成leap(int year)。
如果需要从被调函数带回一个确定的值,被调函数中必须含有return语句。如果不需要从被调函数带回确定的值可以不要return语句。
这里需要特别指出的是:如果被调函数中没有return语句,并不是不带回值,而只是带回一个不确定的值。例如:
#include <stdio.h>
print2(int n )
{
int t;
for(t=1;t<=n;t++)
putchar('#');
 putchar('\n');
}
void main( )
{
print2(5);
print2(4);
print2(3);
print2(2);
print2(1);
}
运行结果:
#####
####
###
##

被调函数print2中没有return语句,函数也带回值,只是一个不确定的值。所以主函数中对函数print2的调用也可改为:
void main( )
{int a,b,c,d,e;
a=print2(5);
b=print2(4);
c=print2(3);
d=print2(2);
e=print2(1);
printf("%d:%d:%d:%d:%d",a,b,c,d,e);
}
这样写也是合法的。通过printf函数可以打印出函数的返回值。但这些返回值不一定有实际意义。
为了明确表示不带回值,可以用“void”定义“无类型”。例如,上面的例子中函数print2的定义可以改为:
void print2(int n)
{…}
另外,函数中也可以有多个return语句。这时,一旦执行到其中的一个return语句,就立即返回到主调函数,被调函数中其他语句不再执行。例如:
int max(int x,int y) /*函数max的定义*/
{
if (x>y) return(x);
return(y);
}
void main( ) /*主函数*/
{int a,b,c;
scanf("%d,%d",&a,&b);
c=max(a,b); /*对函数max的调用*/
printf("max is %d",c);
}
运行结果:
5,6
max is 6
可以看到,在调用过程中,如果if语句满足条件,执行第一个return语句后就返回主调函数,第二个return语句将不再执行。
在函数调用时,关键字return后可以是变量也可以是一个表达式。例如上例中的函数max可以改为:
int max(int x,int y)
{
return(x>y?x:y);
}
这样就使程序看上去更简洁了。
不管renturn后跟的是变量也好,还是表达式子也好,它们的值的数据类型,应当和函数定义中的函数类型(返回值)一致。如果不一致,则以函数类型为准。对数值型数据,可以自动进行类型转换。但这样一方面会使程序可读性降低,容易出错;另一方面,并不是所有的类型都能互相转换的,所以建议读者在函数调用时,最好能使两者保持一致。
现在我们来编写函数实现例3.2
求两数最大公约数的算法是:如果两数相除的余数不是0,则除数作为新的被除数,余数作为新的除数,进行下一次循环。最后一次循环时(即两数相除的余数是0)的除数就是最大公约数。
求两数最小公倍数的算法是:两数的乘积除以两数的最大公约数。
下面我们编写函数gys和gbs分别求两个数的最大公约数和最小公倍数。
int gys(int a,int b)
{ int t,s;
if(b>a) /* 如果除数比被除数小,则交换两数*/
{ t=a;
a=b;
b=t;
}
while((s=a%b)!=0)
{
a=b;
b=s;
}
return b;
}
int gbs(int a,int b,int h)
{
return(a*b/h);
}
void main( )
{
int p,q,r;
printf("please input 2 numbers:");
scanf("%d,%d",&p,&q);
r=gys(p,q);
printf("两数最大公约数为:%d\n",r);
printf("两数最小公倍数为:%d\n",gbs(p,q,r));
}
3.5函数嵌套调用和函数声明
【例3.6】求1-n之间所有素数的和。
分析:该问题是求和运算,而参与求和的数是1-n之间的素数。该问题可以分解为数据输入、求和、数据输出三个模块;而求和部分又要分解出素数判断模块。我们把输入、输出模块合并到主函数main实现,求和运算作为一个函数sum,该函数再调用素数判断函数isprime。这样主函数main调用sum,函数sum又调用函数isprime,这就涉及函数的嵌套调用。
所谓嵌套调用是指在被调函数的执行过程中又调用另一个函数。图3.3就是一个嵌套调用的图示。一个程序包含main函数、f1函数、f2函数,箭头表示函数调用,①②……⑨表示程序的执行顺序。
① ② ③ ④ ⑤
⑨ ⑧ ⑦ ⑥
图3.3 函数的嵌套调用函数声明的一般形式为:
类型名 函数名(类型名 形式参数1,类型名,形式参数2,……);
这种包含参数和返回值类型的函数声明称为函数原型。
C语言程序
#include <stdio.h>
#include <math.h>
void main( )
{
long sum(int n); /*函数的声明*/
int n;
long s;
scanf("%d",&n);
s=sum(n);
printf("%ld",s);
}
long sum(int n) /*函数sum的定义*/
{
int isPrime(int m); /*函数的声明*/
int i;
long s=0;
for(i=1;i<=n;i++)
if(isPrime(i)==1)
s=s+i;
return s;
}
int isPrime(int m) /*函数isPrime的定义*/
{ int i,k;
k=sqrt(m);
for(i=2;i<=k;i++)
if(m%i==0)
return(0);
return(1);
}
说明:
C语言中,在函数调用之前应当对所调用的函数进行声明,指出该函数的返回值的类型以及形参的个数和类型,编译器据此信息对函数调用进行语法检查,保证形参和实参的个数和类型的一致性,保证返回值的使用正确性。
可以简单地照写已经定义的函数的首部,再加一个分号,就成为了对函数的声明。
在函数的声明中,由于编译系统不检查参数名,因此形参表中参数名是什么都无所谓,也可只写数据类型,不写形参名。如上例中在主函数中对函数的sum的声明可以写成:
long sum(int ); 或 long sum(int x);
函数的声明中,函数类型、函数名、参数个数、参数类型和参数顺序应与函数的首部保持一致。
如果函数的值是整型或字符型,可以不必进行声明,系统自动按整型声明。但不建议这样做,因为系统无法对参数的类型做检查。
如果被调函数出现在主调函数之前,可以不必进行声明。
如果已在所有函数定义之前,在函数的外部已作了函数声明,则在各个主调函数中不必对所调用的函数再作声明。
3.6函数的递归调用
【例3.7】计算n!
分析:一般来说,将n!描述成为:
n!=1*2*3….*(n-1)*n
但是,只要稍稍变换一下,就可以将其描述成为:
n!=n*(n-1)….3*2*1=n*(n-1)!
这样,一个整数的阶乘就被描述成为一个规模较小的阶乘与一个数的积。同样,可以将(n-1)!描述成(n-1)*(n-2)!。依次类推。于是,一个问题就被描述成了一个较小规模的同样类型的问题了。
本例的算法就是基于如下的递归数学模型的:
非法 (n<0)
func(n) 1 (n=0)
n*func(n-1) (n>0)
C语言程序
#include <stdlib.h>
int func(int n)
{
if(n<0)
{
printf("negative argument!\n");
exit(-1) /*正常退出*/
}
else
if(n==0)
return 1;
else
return n*func(n-1); /*递归调用*/
}
说明:
当用函数func来表示n!,就有形式:
func(n)
{return(n*func(n-1));}
这种函数形式,就称为递归函数。所以称其为递归,是因为其求解过程是一个如图3.4所示的调用-回代过程。
func(5) 120
调用 回代结束
5*func(4) 5*24=120
调用 回代
4*func(3) 4*6=24
调用 回代
3*func(2) 3*2=6
调用 回代
2*func(1) 2*1=2
调用结束 回代
1
图3.4 函数的递归调用
在递归调用过程中,每次调用都将使问题用较小规模的问题描述代替,直到问题的描述小到可以直接给出解为止;接着便开始一个回代过程。回代的过程是从一个已知值推出下一个值。
在图3.4中,func(n)的返回值是n×func(n-1),而求func(n-1)的值,需要func(n-2),……,直到最后找到func(1)为止。如n=5时,返回值是5* func(4),而func(4)调用的返回值是4* func(3),func(3) 调用的返回值是3*func(2),func(2)调用的返回值为2×func(1)。现在func(1)的返回值为1,是一个已知数;然后回过头根据func(1)在求出func(2),将func(2)得值乘上3得到func(3),将func(3)得值乘上4得到func(4),在将func(4)乘上5,得到func(5)。程序的回代过程是由编译程序自动完成的,无需程序员书写回代的代码。
递归过程不应无限制地进行下去,当调用有限次以后,就应当到达递归调用的终点得到一个确定值(例如本例中的func(1)= =1),然后进行回代。在这样的递归程序中,程序员要根据数学模型写清楚调用结束的条件,以保证程序不会无休止地调用。任何有意义的递归总是有两部分组成的:递归形式与递归终止条件。
3.7库函数的使用
【例3.8】编写一个小学生算术自测练习系统。要求本系统具有以下功能:
程序随机产生两位数以内的n道算术题(只做加减乘),要学生回答答案。
n道题做完后,程序给出评语:
做对9题以上----优秀做对7题或8题----良好做对5题或6题----不错其它-----还要努力学生做完一轮后,不用关机,可以进入下一轮。
分析:① 本题采用一个函数test实现出题以及对学生计算的判断和记分;主函数调用test,并控制还是否继续。
② 计算机自动出题,计算数据和计算类型都是随机的。为此,要调用产生随机数的库函数。
我们前面提到C语言系统提供了大量的库函数,为了方便实用,这些库函数分别包含在不同的头文件中。例如,输入输出函数包含在“stdio.h”、数学函数包含在“math.h”。为了使用这些系统函数必须将这些头文件加入到程序中。
一般格式为:
#include <头文件名>
本例中的随机数可采用随机函数rand()产生。函数rand()可以返回一个0----rand_max(32767)之间的随机整数。rand_max和rand定义在头文件stdlib.h中。
在程序中,加、减、乘运算也应该随机产生。可产生0-2之间的随机整数,分别代表加、减、乘运算。而若要用1、2、3分别代表加、减、乘,就要对结果再加1。
伪随机数序列计算机产生的随机数都是伪随机数,即这个随机数序列有一个长度,因而会出现重复。运行程序可以发现,每次程序重新执行时,所产生的题目是相同的。为了弥补这一不足,可以使用随机数序列函数srand,使rand产生不同的伪随机数序列。srand的参数称为随机数序列种子(seed)。即不同的伪随机数序列种子,可以得到不同的伪随机数序列。所以,为了让rand每次产生不同的伪随机数序列,每次要给srand以不同的参数----种子。最好这个种子也是随机的。
一个办法是采用系统时间作为伪随机数序列种子,即:
srand((unsigned int) time(NULL));
在rand的前面加上上述语句,就可以产生出相当随机的随机数序列了。表达式(unsigned int)time(NULL)是把时间转换成无符号的整型数,作为rand函数的种子使用。
此外,还可以使用伪随机数初始化函数randomize来动态的产生伪随机数序列。
应当注意,srand和randomize的说明在头文件stdlib.h中,time说明在头文件time.h中
C语言程序
#include <stdio.h>
#include<stdlib.h>
void test( )
{
int algtype=0; /*计算类型*/
int i, /*for循环控制变量*/
points=0, /*成绩*/
num1=0,num2=0, /*两个操作数*/
result=0, /*程序的计算结果*/
answer=0; /*学生回答*/
printf("现在开始计算,请看题:");
for(i=1;i<=10;++i)
{
randomize( );
num1=rand( )%100; /*产生100以内的两个随机数*/
num2=rand( )%100;
algtype=rand( )%3+1; /*产生一个运算类型*/
switch(algtype)
{
case1,/*加法出题*/
result=num1+num2;
printf("\n%d+%d",num1,num2);
break;
case 2,/*减法出题*/
if(num1>num2){
printf("\n%d-%d",num1,num2);
result=num1-num2;
}
else{
printf("\n%d-%d",num2,num1);
result=num2-num1;
}
break;
case 3,/*乘法出题*/
result=num1*num2;
printf("\n%d*%d");
break;
}
printf("=?");
scanf("%d",&answer); /*要求学生输入答案*/
if(answer==result) /*积分*/
++points;
}
printf("\n你的成绩"); /*打印成绩*/
switch(points)
{
case 10:
case 9:printf("优秀");break;
case 8:
case 7:printf("良好");break;
case 6:
case 5:printf("不错");break;
default:printf("还要努力");
}
}
void main(void)
{
char yesoron; /*回答是否继续*/
test( );
do
{
printf("你还想继续吗(y/n)"); /*决定是否继续*/
yesoron=getchar();
if(yesoron=='n'||yesorno=='N') exit(0);
if(yesoron=='y'||yesorno=='Y') test( );
}while(1);
}
说明:
每一种编译系统提供的库函数的数量和功能是不尽相同的。但对于ANSI C标准所建议的标准库函数,目前各编译系统一般都提供了这些库函数,具有统一的函数名和一致的函数功能。标准函数库包括输入输出函数、数学函数、时间函数、进程管理函数、字符与字符串函数等。
如果使用库函数,一般还应该在本文件开头用#include命令将调用有关库函数时所需用到的信息“包含”到本文件中来。如本例中用到的#include<stdlib.h>。其中stdlib.h是一个“头文件”,头文件中有函数调用时所需要的一些信息,比如函数声明、变量的定义和与函数有关的常量的,如果不包含“stdlib.h”文件中的信息,就无法使用该库中的函数。
3.8全局变量和局部变量
【例3.9】编写一个函数求某班级学生成绩的最高分、最低分和平均分。
分析:本题目要求编写一个函数得到学生成绩的最高分、最低分和平均分等三个值,但根据前面讲述可知一个函数最多有一个返回值。解决这类问题的方法有多种,在这里通过全局变量来实现。针对本例编写函数average返回平均值,最高分和最低分通过定义两个全局变量得到。
1.局部变量:
指在一个函数内部定义的变量,因此也称为内部变量,它只在本函数范围内有效,也就是说只有在本函数内才能使用它们,在此函数以外是不能使用这些变量的。
2.全局变量:
指在函数外定义的变量,因此也称为外部变量。其作用域是从源程序文件中定义该变量的位置开始,到本源程序文件结束,即位于全局变量定义后的所有函数都可以使用该变量。
C语言程序
float max=0,min=100; /*定义全局变量*/
float average(int n) /*定义函数average*/
{
int i;
float s,aver1,sum=0;
for(i=1;i<=n;i++)
{
printf("请输入一个学生的成绩:");
scanf("%f",&s);
if(s>max) max=s;
if(s<min) min=s;
sum=sum+s;
}
aver1=sum/n;
return(aver1);
}
void main( )
{
int n;
float aver2;
printf("请输入班级人数:");
scanf("%d",&n);
aver2=average(n); /*函数调用*/
printf("平均分为 %6.2f,最高分为%6.2f,最低分为%6.2f\n",aver2,max,min);
}
3.说明:
(1)局部变量:
局部变量只在本函数的范围内有效,包括在main函数中定义的变量也只能在main函数中使用。
不同函数中可以使用相同名字的变量,因为其作用域都是自身函数。它们代表不同的对象,占用不同的内存单元,互相独立函数的形式参数也是局部变量可以在复合语句中定义变量,其作用域只是本复合语句
例如:
main( )
{
int i,a=0;
for (i=1; i<=2; i++)
{
int a=1;
a++;
printf("i=%d,a=%d\n",i,a);
}
printf("i=%d,a=%d\n",i,a);
}
定义了两个同名局部变量a,第二个变量a在复合语句中定义,其作用域也是本复合语句。第一个变量a在此复合语句中不起作用。
函数中的局部变量,如果不专门声明为static,都是动态分配存储空间的,数据存储在动态存储区中。用关键字auto作存储类别声明的变量,称为自动变量或动态变量。通常auto被省略。
auto int b,c=3; 等价于 int b,c=3;
用关键字static声明的局部变量是局部静态变量。函数编译时在静态存储区分配存储单元,函数调用结束后不释放存储单元,直到程序运行结束才释放存储单元。若对静态变量赋初值,只执行一次,再次调用函数时不再赋初值而保留上次函数调用结束时的值;而对于自动变量,每次调用都要重新分配内存单元并赋初值。例如:
void f(int c)
{
int a=0;
static int b=0;
a++; b++;
printf("%d,a=%d,b=%d\n",c,a,b);
}
void main( )
{
int i;
for (i=1; i<=3; i++)
f(i);
}
运行结果为:
1,a=1,b=1
2,a=1,b=2
3,a=1,b=3
一般情况下,变量都是存放在内存中的。为了减少从内存中存取变量值的时间,C语言允许将局部变量的值放在寄存器中,用关键字register声明。只有局部自动变量和形参可以定义为寄存器变量,另外因为寄存器数量有限,不能定义太多的寄存器变量。
(2)全局变量:
全局变量增加了函数间的数据联系。由于在同一文件中的所有函数都能使用全局变量,所以可以利用全局变量从函数中得到一个以上的返回值。
建议不要过多的使用全局变量,因为全局变量在程序的执行过程中一直占用存储单元,如果在程序中使用过多的全局变量,则浪费空间。另外使用全局变量过多,人们难以清楚地判断出每个瞬时各个外部变量的值,会降低程序的清晰性。如:
#include <stdio.h>
int k;
void show( )
{
for (k=1; k<=10; k++)
putchar('*');
putchar('\n');
}
void main( )
{
for (k=1; k<=4; k++)
show( );
}
读者可以自己看一下程序的执行结果。
若在程序中全局变量与局部变量同名,则在局部变量的作用范围内,全局变量不起作用。
如果想在定义之前的函数中引用全局变量,则在函数中用关键字“extern”作“外部变量声明”,在函数内部,从声明之处起,可以使用它们。如:
main( )
{
extern int a,b;
printf("%d",max(a,b) );
}
int a=13,b=-8;
则变量a,b的作用域就扩展到了整个主函数。
如果一个C程序由多个源程序文件组成,那么一个某文件中的函数能否引用另一个文件中的全局变量,有两种情况。一种情况是在一个文件中要引用另一文件中定义的全局变量,要在引用它的文件中用extern作声明。如图3.5。
图3.5 多个文件间变量的引用
在文件file1.c中定义的变量sort,在文件file2.c中引用,引用前加上extern进行声明。
另一种情况,如果要全局变量仅限于被本文件中的函数引用,其它文件不能使用。定义全局变量时用static进行声明。如图3.6。
图3.6 限制文件间变量的应用在文件file1.c定义全局变量sort时加上关键字static,限制了变量sort的作用域,在其他文件中就不能引用,但要注意的是不管是否加上关键字static,变量sort都按静态存储方式存放。
(3)函数:
如果限制一个函数只能被本文件中其它函数所调用,称为内部函数(或静态函数)。定义时在函数类型前加static。如
static int func(int x)
{
……
}
使用内部函数(静态函数),可以使函数局限于该函数所在文件,如果在其他文件中有同名的内部函数,则互不干扰。
如果一个函数能被本文件和其他文件中的函数调用,称为外部函数。定义时在函数类型前加extern。在需要调用此函数的文件中,要用extern声明所调用函数的原型。
/*file1.c*/
main( )
{
int c,a=2;
extern int fac(int x); /*外部函数的声明*/
……
c=fac(a);
……
}
/*file2.c*/
extern int fac(int x)
{
……
}
C语言规定,如果在定义函数时省略extern,则隐含为外部函数。
3.9指针和指针作为函数参数
【例3.10】交换两个数的值,要求在主函数中输出结果。
分析:在例3.4中将变量作为函数参数传递给被调函数,结果输出也是在被调函数中完成的。由于参数的传递是单向的,也就是说参数只能由主调函数传递到被调函数,而不能反向传递,在函数的调用过程中,即使在被调函数中对形参进行了操作,改变了形参的值,也不会影响到实参。而利用函数的返回值也只能得到一个数值。当然可以使用上一节介绍的全局变量,但是全局变量的使用容易造成程序的混乱。比如对于数据交换来说,如果有多个地方需要数据交换,这些数据就要采用相同的变量,任何一次改变变量都产生新值,这样我们自己都可能搞不清楚变量的值到底是什么了。因此,必须寻求另外的解决途径。
我们知道每个变量在内存中都占有一定字节数的存储单元。程序编译时,根据程序中定义的变量类型,在内存中为其分配了相应字节数的存储空间。变量在内存中所占存储空间的首地址,称为变量的地址。该地址存储的内容,就是变量的数值。
例如:有如下语句 int a=15; 则变量的值与地址的关系如图3.7所示。 
图3.7 变量的值与地址的关系由此,我们可以看出由变量a可以取得变量的值(以前一直使用的方法),也可以通过变量的地址取得变量的值。前一种方法称为直接访问,后一种方法称为间接访问。&a表示变量a的地址。
因此,如果我们将变量的地址存到另一个变量p中,为了取得a的值,就可以通过变量p得到a的地址,进而取得a的值。
对于例3.10的问题就可以通过传递变量的地址给被调函数,被调函数将变量处理后再放回到原来的地址,在主调函数中就可以得到处理后的数据。这就像两个人对同一个抽屉里的物品进行处理一样,处理的结果当然是相互影响的。
在C语言中为了处理地址问题,引入了“指针变量”。
变量的地址也称为变量的指针。指针变量就是存放变量地址的变量。
指针变量的定义形式:
基类型 * 指针变量名;
指针变量的赋值:
指针变量名 = &变量在变量定义时,“*”表示定义的变量为指针变量;在使用时,“*”表示取指针变量所指向的变量的内容。“&”称为取地址运算符,表示取变量的地址。
例:int a=1,b,*p; /*定义变量a,赋初值1,定义变量b,指针变量p。*/
p=&a; /*将变量a的地址赋值给p。 */
b=*p; /*将p所指向的变量a的值(即1),赋给变量b。此时,变量a,b的值都是1。指针变量p仍然指向a。*/
指针变量作为函数的参数:
类型名 函数名(类型名 *形式参数1,类型名 *形式参数2,…)
{
说明部分
语句
}
现在我们可以编写例3.10的C语言程序
#include <stdio.h>
void swap(int *px,int *py)
{
int t;
t=*px; /*指针变量px所指向的变量的值赋给变量t*/
*px=*py; /*指针变量py指向的变量的值赋给指针变量px指向的变量*/
*py=t; /*t的值赋给py指向的变量,实现了数据交换 */
}
void main( )
{
int a,b,*p1,*p2;
printf("\nInput a,b,");
scanf("%d%d",&a,&b);
p1=&a;
p2=&b;
swap(p1,p2); /*变量a,b的地址传递给swap()函数 */
printf("max=%d,min=%d\n",a,b);
}
程序运行时,先执行主函数,输入变量a和b的值。然后将a和b的地址分别赋给指针变量p1和p2,使得p1指向a,p2指向b。在函数调用时,将实参变量的值传给形参变量。即px的值为&a,py的值为&b。这时,px和p1都指向了变量a,py和p2都指向了变量b。接着执行swap函数,由于函数swap()通过指针变量的传递得到变量a、b的地址,通过t=*px;将a的值暂存在变量t中,通过*px=*py;将变量b的值放到a的地址中,通过*py=t将a的值放入b的地址中,实现了变量交换。在main()中读取a、b的值就是交换后的值。
说明:
地址和指针的概念在计算机中,把内存区划分为一个一个的存储单元,每个单元为一个字节(8位),它们都有一个编号,这个编号就是内存单元的地址。程序中定义的每个数据在编译后都占有各自的内存区。数据所占有的存储单元个数是由其类型决定的。如整型占2个字节、实型占4个字节等。第1个单元的地址被称为首地址,用它来表示一个数据的地址。
在程序中一般是通过变量名来对内存单元进行存取操作的。其实程序经过编译以后已经将变量名转换为变量的地址了。这种按变量地址存取变量值的方式成为“直接访问”方式。还可以采用另一种称之为“间接访问”的方式,将变量i的地址存放在另一个变量中。先找到存放i的地址的变量,从中取出i的地址,然后到这个地址中取出i的值。由于通过地址能找到所需的变量单元,我们可以说,地址象一根针一样“指向”该变量单元。所以我们将地址形象的称为“指针”。
如果有一个变量专门用来存放另一个变量的地址,则称它为“指针变量”。首先指针变量就是一个变量,和我们以前所讲的其它变量没有本质区别。不同之处在于这种变量中所存放的内容是地址。
指针变量的定义
C语言规定所有变量在使用之前必须定义,制定其类型,并按此分配内存单元。指针变量也要先定义后使用。
在指针变量的定义形式:
基类型 * 指针变量名;
其中:基类型是指针变量指向的变量的类型;* 是指针类型的变量的标记,不可去掉。如本例中:
int a,b,*p1,*p2;
定义了两个整型变量a和b,两个指向整型变量的指针变量p1和p2。也就是说指针变量p1和p2中存放的是整型变量的地址。值得注意的是p1和p2前面的*表示该变量的类型是指针型变量,而不是指针变量名的一部分。
指针变量定义时必须指定其所指向的变量的数据类型,而且使用过程中只能指向同一种类型的变量。
指针变量定义后,系统为变量分配一个存储单元,用来存放地址;根据存储单元的长度分为大存储模式(长指针,4 Byte)和小存储模式(短指针,2 Byte);
指针变量的赋值怎样使一个指针变量指向另一个变量呢?可以进行初始化,也可以使用赋值语句。如,int a,*s=&a;
也可以把定义和初始化分开写在两行
int a,*s;
s=&a;
只能用同类型变量的地址进行赋值。如:
int *s;
float f;
则 s=&f;是非法的。
在指针变量定义后,该指针变量的值是不定的,即它所存放的地址是不确定的,它指向内存中的一个不确定单元,该单元中可能存放数据,也可能存放程序代码。如果不经对指针变量赋值就利用它对它所指向的内存单元进行间接访问,就有可能导致不可预料的后果。要使指针变量值指向某具体的变量,必须对其赋值。如果本例中函数swap写成:
void swap(int *px,int *py)
{
int *t;
*t=*px;
*px=*py;
*py=*t;
}
指针变量t没有被赋值,t中并无确定的地址值,它的值是不可预见的。t所指向的单元的值(即*t)也是不可预见的。因此对*t赋值可能会破坏系统的正常工作状况。
指针变量的引用对指针变量的引用,由取地址运算符(&)和指针运算符(*,也称为取值运算符)来完成。
取地址运算符“&”在前面的学习中已经见过,例如:在使用scanf( )函数进行输入时,就使用了“&”运算符将数据存储到指定的存储空间。如:
int b;
scanf("%d",&b);
这段程序的意思就是以“%d”规定的格式将输入的数据存储到整型变量a所在的地址单元中。
在指针运算中,取地址运算符可用来将变量的地址赋给指针变量。需要注意的是在为指针变量赋地址时,指针的类型应该与所指向的变量数据类型一致。如:
int a,*px,*py;
px=&a;
py=px;
定义了整型变量a以及指向整型变量的指针变量px和py。然后把a的地址赋给px,即指针变量px指向了整型变量a,第三条语句又把px的值赋给了py,也就是说指针变量py也指向了变量a。
另外,取地址运算符只能用于变量或数组元素,而不能用于表达式或常量。如下所示是错误的用法:
int *px,a;
px=&(a+1);
p=&123;
对指针内容的访问由指针运算符实现,如*px表示px指向的变量。注意,表达式中的*px与变量定义中的*px含义不同。在定义中“*”不是运算符,它只是表示其后的标识符是一个指针变量。而在表达式中的“*”是指针运算符,*px是一个整体,表示指针px所指向的变量。如:
main( )
{
float a=12.34,*px;
px=&a;
printf("%f",*px);
}
程序的执行结果为12.34。
3.10返回指针值的函数
【例3.11】 编写一个函数求某班级学生成绩的最高分、最低分和平均分。(用指针实现)
分析:本题目要求编写一个函数得到学生成绩的最高分、最低分和平均分等三个值,在例3.9中通过定义全局变量实现。但全局变量的使用,会造成程序各模块间的相互联系、相互影响太多,从而会降低模块的独立性、清晰度。通过前面的讲述,得知将主调函数中某变量的地址传递到被调函数,在被调函数中通过该地址进行间接访问,就可以改变主调函数中的变量的值。本例我们通过指针来完成。
如果想通过函数调用得到几个要改变的值,可以:
在主调函数中设n个变量;
将n个变量的地址作为实参传给所调用的函数的形参;
③ 通过形参指针变量,改变该n个变量的值;
④ 主调函数就可以使用这些改变了的值;
返回指针值的函数:
类型名 *函数名(类型名 *形式参数1,类型名 *形式参数2,…)
{
说明部分
语句
}
C语言程序1:
void func(int n,float *aver,float *max,float *min) /*定义函数*/
{
int i;
float s;
for(i=1;i<=n;i++)
{
printf("请输入一个学生的成绩:");
scanf("%f",&s);
if(s>*max) *max=s; /*求最高分*/
if(s<*min) *min=s; /*求最低分*/
*aver+=s;
}
*aver/=n; /*求平均分*/
}
void main( )
{
int n;
float x=0,y=100,aver1=0;
printf("请输入班级人数:");
scanf("%d",&n);
func(n,&aver1,&x,&y); /*函数调用*/
printf("平均分为 %6.2f,最高分为%6.2f,最低分为%6.2f ",aver1,x,y);
}
函数func有三个形参,分别是三个指向实型的指针变量。在主函数中调用函数func,把变量aver1的地址传给形参aver,变量x的地址传给形参max,变量y的地址传给形参min,执行函数func的函数体,改变形参变量指向的变量的值(即主函数中变量aver1、x和y的值),得到最高分、最低分和平均分。
C语言程序2:
float *average(int n,float *max,float *min) /*定义返回指针值的函数*/
{
int i;
float s,*aver,sum=0;
static float aver1;
aver=&aver1;
for(i=1;i<=n;i++)
{
printf("请输入一个学生的成绩:");
scanf("%f",&s);
if(s>*max) *max=s; /*求最大值*/
if(s<*min) *min=s; /*求最小值*/
sum+=s;
}
aver1=sum/n;
return(aver); /*返回平均值*/
}
void main( )
{
int n;
float x=0,y=100,*p;
printf("请输入班级人数:");
scanf("%d",&n);
p=average(n,&x,&y); /*函数调用*/
printf("平均分为 %6.2f,最高分为%6.2f,最低分为%6.2f ",*p,x,y);
}
函数average是返回指针值的函数。函数的返回值仍然通过return语句返回,只是返回的数据类型是指针类型。在本例中,通过函数的返回值得到保存平均分的变量的地址,进而得到平均分。最高分和最低分仍通过“C语言程序1”的方法得到。
3.11函数的指针
【例3.12】写一个用矩形法求定积分的通用函数,分别求:
  
分析:矩形法是把所要求的面积垂直x轴分成n个小矩形,然后把这n个小矩形的面积相加,即为所求的定积分的值。这种近似求值的精度随分割个数n的增加而增加。如图3.8。
图3.8 矩形法求积分图3.8中画阴影线的小面积有 4 条边,第一条长度f(c),第二条长度f(d),第三条长度d-c,而第四条线我们采用近似的办法,以第一条线高度作x轴的平行线得y=f(c),则小矩形面积为(d-c)·f(c);或以第二条线高度作x轴平行线得y=f(d),则小矩形面积为(d-c)·f(d)。 然后将各小块面积累加起来。
函数的指针(地址):
函数在编译时被分配的一个入口地址。C语言中,函数名本身是函数指令代码在内存的首地址。
指向函数的指针变量的定义形式:
类型名 (*指针变量名)(类型名 形式参数1,类型名 形式参数2,……);
指向函数的指针变量的赋值与使用:
指针变量名 = 函数名
C语言程序:
#include <math.h>
void main( )
{ float func(float (*p)(float),float a,float b,int n);
float a1,b1,a2,b2,a3,b3,c;
float (*p)(float); /* 声明一个指向函数的指针变量*/
float fsin(float); /*声明fsin函数*/
float fcos(float);
float fexp(float);
int n=20;
printf("input a1,b1:"); /*输入求sinx定积分的下限和上限*/
scanf("%f,%f",&a1,&b1);
printf("input a2,b2:");
scanf("%f,%f",&a2,&b2);
printf("input a3,b3:");
scanf("%f,%f",&a3,&b3);
p=fsin; /*将fsin函数的入口地址赋给p*/
c=func(p,a1,b1,n); /*求出sinx的定积分*/
printf("the integral of sin(x) is:%f\n",c);
p=fcos;
c=func(p,a2,b2,n);
printf("the integral of cos(x) is:%f\n",c);
p=fexp;
c=func(p,a3,b3,n);
printf("the integral of exp(x) is:%f\n",c);
}
float func(float (*p)(float),float a,float b,int n) /*用矩形法求定积分的通用函数*/
{ int i;
float x,h,s;
h=(b-a)/n;
x=a;
s=0;
for(i=1;i<=n;i++)
{ x=x+h;
s=s+(*p)(x)*h;
}
return(s);
}
float fsin(float x) /*计算sinx的函数*/
{
return sin(x);
}
float fcos(float x)
{
return cos(x);
}
float fexp(float x)
{
return exp(x);
}
函数sin、cos和exp是系统提供的数学函数,程序开头通过include命令把所需头文件math.h包含进来。在程序中定义三个函数fsin、fcos和fexp分别用来计算sin(x)、cos(x)和exp(x)的值,在main函数中要声明这三个函数。在main函数中定义p为指向函数的指针变量,定义形式为“float (*p)(float)”,表示p指向的函数有一个实型形参,p可指向返回值为实型的函数。在主函数中有“p=fsin;”表示将fsin函数的入口地址赋给p。在调用func函数时,用p作为实参,把fsin函数的入口地址传递给形参p(形参p也定义为指向函数的指针变量),这样形参p也指向fsin函数,(*p)(x)就相当于fsin(x)。fsin(x)的值就是sinx的值。因此通过调用func函数求出了sinx的定积分。求其余两个函数的定积分的情况与次类似。
说明:
从本例可以清楚的看到,不论调用fsin、fcos和fexp,函数func一点都没有改动,只是在调用func函数时将实参函数名改变而已。这就增加了函数使用的灵活性。可以编写通用的函数来实现各种专用的功能。在使用时,应注意:
指向函数的指针变量的定义,也可以用另外一种写法:
类型名 (*指针变量名)();
定义指向函数的指针变量时,不要将(*指针变量名)两侧的括号省略。因为如果省略,则变成一个返回指针值的函数的声明了。
在指向函数的指针变量使用之前,必须赋值。也就是说该指针变量要有确切的指向。在给指针变量赋值时,只需把函数名赋给它,不必给出参数。
如果函数的指针赋给了指针变量,则函数的调用即可以通过函数名调用,也可以通过指向函数的指针变量调用。当通过后者调用时,只需用(*指针变量名)代替函数名即可。
3.12典型例题
【例3.14】用函数实现牛顿迭代法求一元三次方程的根。
分析:牛顿迭代法的公式是:x=x0-f(x)/f’(x),设迭代误差小于10-5时结束。
参考程序:
#include <math.h>
float solut(float a,float b,float c,float d)
{ float x=1,x0,f,f1;
do{
x0=x;
f=((a*x0+b)*x0+c)*x0+d;
f1=(3*a*x0+2*b)*x0+c;
x=x0-f/f1;}while(fabs(x-x0)>=1e-5);
return(x);
}
void main()
{ float a,b,c,d;
printf("请输入一元三次方程的系数a,b,c,d:\n");
scanf("%f,%f,%f,%f",&a,&b,&c,&d);
printf("一元三次方程为:%5.2fx^3+%5.2fx^2+%5.2fx+%5.2f=0\n",a,b,c,d);
printf("该方程的根为:x=%7.2f\n",solut(a,b,c,d));
}
【例3.15】输入某年某月某日,判断这一天是这一年的第几天?
分析:以3月5日为例,应该先把前两个月的加起来,然后再加上5天即本年的第几天,特殊情况,闰年且输入月份大于3时需考虑多加一天。
参考程序:
#include <stdio.h>
void main()
{
int day,month,year,sum;
int mon(int year,int month,int day);
int leap_year(int year);
printf("\nplease input year,month,day\n");
scanf("%d,%d,%d",&year,&month,&day);
sum=mon(year,month,day);
printf("It is the %dth day.",sum);}
}
int mon(int year,int month,int day)
{ int sum,leap;
switch(month)/*先计算某月以前月份的总天数*/
{
 case 1:sum=0;break;
 case 2:sum=31;break;
 case 3:sum=59;break;
 case 4:sum=90;break;
 case 5:sum=120;break;
 case 6:sum=151;break;
 case 7:sum=181;break;
 case 8:sum=212;break;
 case 9:sum=243;break;
 case 10:sum=273;break;
 case 11:sum=304;break;
 case 12:sum=334;break;
 default:printf("data error");break;
}
sum=sum+day;  /*再加上某天的天数*/
leap=leap_year(year);
if(leap==1&&month>2)/*如果是闰年且月份大于2,总天数应该加一天*/
sum++;
return sum;
}
int leap_year(int year)
{
 int leap;
if(year%400==0||(year%4==0&&year%100!=0))/*判断是不是闰年*/
leap=1;
  else
leap=0;
return leap;
}
【例3.16】利用指针方法输入3个数a,b,c,按大小顺序输出。  
程序分析:注意交换顺序
参考程序:
#include <stdio.h>
void main()
{
int n1,n2,n3;
void swap(*p1,*p2)
int *pointer1,*pointer2,*pointer3;
printf("please input 3 number:n1,n2,n3:");
scanf("%d,%d,%d",&n1,&n2,&n3);
pointer1=&n1;
pointer2=&n2;
pointer3=&n3;
if(n1>n2) swap(pointer1,pointer2);
if(n1>n3) swap(pointer1,pointer3);
if(n2>n3) swap(pointer2,pointer3);
printf("the sorted numbers are:%d,%d,%d\n",n1,n2,n3);
}
void swap(int *p1,int *p2)
{
int p;
p=*p1;*p1=*p2;*p2=p;
}
【例3.17】编写一个函数,输入n为偶数时,调用函数计算1/2+1/4+...+1/n;当输入n为奇数时,调用函数计算1/1+1/3+...+1/n。
分析:略。
参考程序:
#include "stdio.h"
void main()
{
float peven(int n);
float podd(int n);
float sum;
int n;
while (1)
{
scanf("%d",&n);
if(n>1)
break;
}
if(n%2==0)
{
printf("Even=");
sum=peven(n);
}
else
{
printf("Odd=");
sum=podd(n);
}
printf("%f",sum);
}
float peven(int n)
{
float s;
int i;
s=0;
for(i=2;i<=n;i+=2)
s+=1/(float)i;
return(s);
}
float podd(int n)
{
float s;
int i;
s=0;
for(i=1;i<=n;i+=2)
s+=1/(float)i;
return(s);
}
【例3.18】字符显示分析:
(1)字符的定位字符坐标在文本窗口中,字符按照行、列显示。一个窗口分为若干行,一行可以显示若干字符(称为列)。于是,就形成一个以字符为单位的二维坐标空间:行为X轴,列为Y轴;窗口的左上角为(1,1)。
字符定位函数使用字符定位函数,可以在窗口的任何位置显示字符。有关函数有:
指定一个显示字符的位置(设定光标位置),使用函数void gotoxy(int x,int y);。
返回一个字符的坐标位置,使用函数int wherex();和int wherey();。
清除文本窗口,把光标位置移到窗口左上角,使用函数void clrscr();。
这些函数的说明都在头文件conio.h中。
字符输出的定位实例
#include,conio.h”
#include,stdio.h”
#include,bios.h” /*含bioskey说明*/
void main( )
{
int x,y;
clrscr( ); /*清屏*/
x=wherex( );y=wherey( ); /*返回当前光标位置*/
printf("\n清屏后的坐标:x=%d,y=%d",x,y); /*输出坐标值*/
gotoxy(33,15); /*移动光标到(33,15)*/
printf("hello! "); /*在光标处输出*/
x=wherex( );y=wherey( ); /*返回当前光标位置*/
gotoxy(22,10); /*移动光标到(22,10)*/
printf("显示hello的坐标:x=%d,y=%d",x,y); /*输出坐标值*/
while(bioskey(1)= =0); /*等待键盘输入*/
clrscr( ); /*清屏*/
return(0);
}
(2)字符的属性字符的属性由一个8位的二进制字控制。这个控制字的格式为:
7 6 5 4 3 2 1 0
闪烁
背景
字符色
可用的字符颜色定义见表3.1。
表3.1字符颜色的定义字符常量
数值
字符常量
数值
BLACK
BLUE
GREEN
CYAN
RED
MAGENTA
BROWN
LIGHTGRAY
DARKGAY
0
1
2
3
4
5
6
7
8
LIGHTBLUE
LIGHTGREEN
LIGHTCYAN
LIGHTRED
LIGHTMAGENTA
YELLOW
WHITE
BLINK
9
10
11
12
13
14
15
128
字符及其背景的颜色,可以用下面的两个函数设置:
void textcolor(int newcolor); /*设置字符颜色0--15*/
void textbackground(int newcolor); /*设置背景颜色0--7*/
这两个函数的说明也在conio.h中。
(3)字符屏幕输出函数
TC提供与标准输出函数基本相同的3个文本窗口的输出函数:
cprintf("格式字符串",输出项表) /*输出格式化数据到屏幕*/
cputs("字符串"); /*输出字符串到屏幕*/
getche( ); /*从键盘获得一个字符并回显*/
这3个函数的说明也在conio.h中。
例:字符输出函数的使用实例
#include "conio.h"
void main( )
{ clrscr( );
gotoxy(15,10);
textcolor(BLUE); /*设置字体为蓝色*/
textbackground(YELLOW); /*设置字体为黄色*/
cprintf("Hello! "); /*显示hello!*/
getch( );
gotoxy(16,10);
textcolor(RED+BLINK); /*设置字体为闪烁红色*/
textbackground(GREEN); /*设置字体为绿色*/
cprintf("TC");
textmode(LASTMODE); /*恢复前一个文本模式*/
clrscr( ); /*清屏*/
return (0);
}
习 题 三一.选择题
1.C语言程序的基本单位是________。
A,字段 B语句 C函数 D字符
2.C语言程序从________开始执行。
A,程序中的第一条语句 B程序中的main函数
C 程序中的第一个函数 D程序中包含文件的第一个函数
3.C语言规定:在一个源程序中,main 函数的位置_________。
A 必须在最开始 B 必须在系统调用的库函数的后面
C 可以任意 D 必须在最后
4.C语言程序中,若对函数类型未加显式说明,则函数的隐含类型为_______。
A,int B double C void D char
5.C语言程序中,当调用函数时_______。
A.实参和虚参各占一个独立的存储单元 B实参和虚参可以共用存储单元
C可以由用户指定是否共用存储单元 D由计算机系统自动确定是否共用存储单元
6.按C语言的规定,以下不正确的说法是________。
A,实参可以是常量、变量或表达式 B 形参可以是常量、变量或表达式
C 实参可以为任意类型 D 形参与其对应的实参类型一致
7.已定义的函数有返回值,则以下关于该函数调用的叙述中错误的是________。
A.函数调用可以作为独立的语句存在 B函数调用可以作为一个函数的实参
C函数调用可以出现在表达式中 D函数调用可以作为一个函数的形参
8.以下对C语言函数的有关描述中,正确的是_________。
A 在C中,调用函数时,只能把实参的值传送给形参,形参的值不能传送给实参。
B C函数既可以嵌套定义又可以递归调用
C 函数必须有返回值,否则不能使用函数
D 程序中有调用关系的所有函数必须放在同一个源程序文件中。
9.以下对C语言叙述不正确的是________。
A 函数中的自动变量可以赋初值,每调用一次,赋一次初值
B 在调用函数时,实参和对应形参在类型上只需赋值兼容
C 外部变量的隐含类型是自动存储类型
D 函数形参可以说明为register变量。
10.以下正确的函数声明是______。
A int fun(int x,int y) B float fun(int a;int b);
C double fun(float x,float y); D int fun(int x,y);
11.在一个源文件中定义的全局变量的作用域为__________。
A,本文件的全部范围 B 本程序的全部范围
C 本函数的全部范围 D从定义该变量的位置开始到本文件的结束
12.若a是int型变量,则____是对指针变量p的正确定义和初始化。
A,int *p=&a; B int *p=*a; C int p=&a; D int *p=a;
13.关于指针概念不正确的说法是____。
A 一个指针变量只能指向同一类型的变量
B 一个变量的地址称为该变量的指针
C 只有同一类型变量的地址才能放到指向该类型变量的指针变量之中
D 指针变量可以由整数赋、也可由浮点数赋二.程序填空
1.下面程序的输出是________。
void fun(int *x)
{ printf("%d\n",++*x);
}
main( )
{ int a = 25;
fun(&a);
}
2.写出下列程序的运行结果。
int t(int x,int y,int cp,int dp)
{ cp=x*x+y*y;
dp=x*x-y*y;
}
main( )
{ int a=4,b=3,c=5,d=6;
t(a,b,c,d);
printf("%d %d\n",c,d);
}
3.写出下列程序的运行结果。
void num( )
{ extern int x,y;
int a=15,b=13;
x=a-b;
y=a+b;
}
int x,y;
main( )
{ int a=8,b=5;
x=a+b;
y=a-b;
num();
printf("%d,%d",x,y);
}
4.有以下程序:
main( )
{ int i=0;printf("%d",i);
f1( );
f1( );
printf("%d",i);
}
f1( )
{ static int i=1;
i++;
printf("%d",i);
}
执行后的输出结果是____________。
5.有以下程序
void f(int v,int w)
{ int t;
t=v; v=w; w=t;
}
main( )
{ int x=1,y=3,z=2;
if(x>y) f(x,y);
else if(y>z) f(y,z);
else f(x,z);
printf(“%d,%d,%d\n”,x,y,z);
}
执行后的输出结果是______________。
6.以下程序的功能是计算sum=1+1/2!+1/3!+…+1/n!,请填空完成程序。
______func(_______)
{ double sum=0.0,f=1.0;int i;
for(i=1;i<=n;i++)
{ f=f*________;
s=s+f;}
return sum;
}
三.编程题
1.编写函数,求整数x的n次幂(n>0)。
2.编写函数,将指定的字符打印n次。
3.编写函数判断是否是素数,在主函数中输出是否素数的信息。
4.编写函数,求13+23+33+……n3的值
5.编写函数,计算一个整数各位数字之和。如123,各位之和为1+2+3=6。
第四章 简单构造数据类型前面我们已经学习了具有分支的程序设计、循环程序设计,还能够将复杂的问题分解为几个简单独立的问题,利用函数的方法分而治之。但是到目前为止,我们处理的都是简单数据类型,它们的特点是一个该类型的变量只能对应一个数据,针对单一的变量进行操作,变量与变量值是一一对应关系。但是,在解决实际问题的过程中,数据类型是复杂的。比如要处理一个班的学生成绩,尽管可以定义几十个变量,但是处理起来很不方便。再比如,我们只学习过字符变量,如何处理字符串的问题?还有我们能否将一个学生的信息(包括学号、姓名、联系电话、家庭住址等)统一处理?应该采用什么样的数据类型?这就将要研究的构造数据类型。构造数据类型又分为简单构造数据类型和复杂构造数据类型。简单构造数据类型指的是相同数据类型的构造,即数组等;复杂构造数据类型指的是不同数据类型的构造,即结构体、联合体等。本章学习数组的定义和使用方法。
4.1 一维数组的引出及使用
4.1.1一维数组的引出
【例4.1】已知一个班30名学生参加了C语言考试,要求编写程序,输入每个同学的成绩,计算平均成绩。并输出所有学生的考试成绩和平均成绩。
分析:按照前几章所讲的知识,如果定义一个简单的变量cscore,该变量只能保存一个学生的成绩,输入新的值,原来的值将被覆盖。如果定义30个变量来表示30名学生的成绩,显然很不方便。但是我们发现所有同学成绩的数据类型是一致的,都是整型或实数型,并且构成一行数据。可以使用一维数组来存放数据。
一维数组的定义格式,类型标识符 数组名[常量表达式];
例:int a[10]; char c[20]; double b[5];
类型标识符表示数组的元素具有统一的数据类型,数组a的元素都是整型,c都是字符型,b都是实数型。
数组名同变量名一样,命名规则也相同。
常量表达式用方括号括起,所给出的值表示数组有多少个元素,也就是能够存储多少个类型标识符所规定的数据类型的值。
上述问题中30名学生的C语言考试成绩可定义如下数组:float cscore[30]; 表示定义了一个长度为30的实型数组,该数组含有30个数组元素,分别为:cscore[0],cscore[1],……
score[29]。每个数组元素相当于一个实型变量,分别存放30名学生的C语言考试成绩。
参考程序:
#include <stdio.h>
void main()
{
float cscore[30]; /*定义包含30个元素的数组,每个元素都是实型*/
float sum=0,average;
int i;
printf(“enter 30 students’ C scores:\n”);
for(i=0;i<30;i++)
{
scanf(“%f”,&cscore[i]); /*输入每个学生的成绩*/
sum=sum+cscore[i]; /*将每个学生的成绩累加起来*/
}
average=sum/30; /*求平均成绩*/
for(i=0;i<30;i++) printf(“%6.2f”,cscore[i]); /*将所有学生的成绩输出*/
printf(“average=%f\n”,average); /*输出平均成绩*/
}
程序的执行过程如图4.1所示。
图4.1流程图通过该例我们可以看出,数组的输入、元素求和、输出都可以通过循环来完成,这也是定义多个变量所无法实现的。
4.1.2一维数组特点及其引用一.一维数组的特点数组是为了处理一批类型相同的数据而引入的构造数据类型,其特点是:
⑴ 是由相同类型的具有固定个数的元素组成的集合。因此,在定义数组的表达式中不允许出现变量。数组中的所有元素都属于同一个数据类型。
①#define M 20
int a[M];
M为符号常量,为正确定义方法;
②int n;
scanf("%d",&n);
int a[n];
n为变量,为错误定义方法;
③int n=5;
int a[n];
n为变量,为错误定义方法
⑵ 每个数组元素都是一个变量,其类型为数组的类型。与相同类型的普通变量完全一样。
⑶ 数组元素在数组中的序号称为下标。通过数组名和下标来唯一的确定每一个元素。
float x[10]
2000
x[0]
2004
x[1]
2008
x[2]
2012
x[3]
2016
x[4]
2020
x[5]
2024
x[6]
2028
x[7]
2032
x[8]
2036
x[9]
图4.2 数组在内存中的存放
⑷ 一维数组在内存中的存放:整个数组占用一段连续的内存单元,各元素按下标顺序存放。数组名表示数组的首地址。如定义float x[10],则在内存的存放如图4.2所示,x代表数组的首地址2000,也是x[0]的地址。
二.一维数组的引用引用形式:
数组名[下标]
引用时下标可以是变量或表达式。注意与定义时的区别。
数组名是用户自定义标识符;数组的引用使用数组名加下标的方式。x[0],x[1],…,x[9]分别表示数组的第1个到第10个元素。数组名x表示数组x所占内存区域的首地址(即第1个元素的地址:&x[0])。通常称为指针常量,其值在程序中不能改变。
数组的下标是从0开始的,即定义float x[10]中的10表示可以存储10个元素,其元素范围从x[0]到x[9],在使用中不可能出现x[10]。如果使用了x[10]将造成下标越界,侵占其他变量的存储空间,可能造成严重后果,但C语言系统编译时并不检查,需要特别注意。
对于数组:int a[10];
数据输入:
for (i=0; i<=9; i++)
scanf("%d",&a[i]);
数据输出:
for (i=9; i>=0; i--)
printf("%4d",a[i]);
三.一维数组的初始化
⑴ 在定义数组时对全部数组元素赋以初值(最好将数组的存储类型定义为static或extern)
static int a[10]={0,1,2,3,4,5,6,7,8,9};
⑵ 可以只给一部分数组元素赋初值,系统自动对其余元素赋一缺省值:
static int a[10]={1,3,5,7,9};
等价于:static int a[10]={1,3,5,7,9,0,0,0,0,0};
⑶ 若对static数组不赋值,系统自动对所有元素赋一缺省值:
static int a[5];
等价于:static int a[5]={0,0,0,0,0};
⑷ 对全部数组元素赋初值时,可以不指定数组长度,其长度由初值个数自动确定:
static int a[ ]={0,1,2,3,4};
等价于:static int a[5]={0,1,2,3,4};
⑸ 不允许数组指明的元素个数小于初值个数:
static int a[5]={0,1,2,3,4,5}; 编译时出错
4.2 二维数组的引出及使用
4.2.1二维数组的引出
【例4.2】假设考试共有5个科目,一个班有20名学生。输入所有学生的各科成绩,求出每名学生的总成绩。
分析:本题中一名学生有5个科目成绩,对于单个科目成绩的考虑可参见例4.1,如其中有一科为C语言,可定义float cscore[20];表示存放20名学生的C语言成绩。现在有5个科目的成绩,当然我们也可以定义五个一维数组,分别存放20名学生五个科目的成绩,但当考试科目再多时,显然这样定义是非常麻烦的。仔细分析我们会发现,对于每个学生都有5个成绩,这些成绩都是实型的数据,即都为相同类型的数据。并且可以把5个科目和20位学生可以看作一个二维表格,每个科目一行,每名学生一列。每一行就是一个一维数组,一个二维表格我们可以定义二维数组。
二维数组的定义格式,类型标识符 数组名[常量表达式][常量表达式];
例:int a[3][4]; float b[5][10];
第一个方括号表示二维数组的第一维,第二个方括号表示第二维,总的数组元素个数为两维长度的乘积。
上述问题中20名学生的五个科目成绩可定义如下数组:float score[20][5]; 表示定义了一个二维的实型数组,该数组含有20×5个数组元素,分别为:score[0][0],score[0][1],…,score[0][4],score[1][0],score[1][1],…,score[1][4],… score[19][0],score[19][1],…score[19][4],其中score[i][0]…score[i][4]存放第i名学生的5科成绩,i的值从0到19。在进行输入时,我们一般是先输入第一个学生的第一门课成绩,再输入第二门课成绩,等5个科目的成绩都输入完后,再分别输入第二个学生的5个科目成绩,以此类推,因此,我们可使用两层循环来控制输入20个学生的5个科目成绩,外层循环控制20名学生,内层循环控制5门课程。总成绩存放在一维数组sum[20]中。
参考程序:
#include <stdio.h>
#define M 20 /*定义符号常量M,代表学生总人数*/
#define N 5 /*定义符号常量N,代表5个考试科目*/
void main()
{
float score[20][5],sum[20];
int i,j;
printf(“\nInput %d students’ %d scores(score[%d][%d]):\n”,M,N,M,N);
for(i=0;i<M;i++)
{
sum[i]=0; /*第i个学生的总成绩清0*/
for(j=0;j<N;j++)
{
scanf(“%f”,&score[i][j]); /*输入第i个学生的成绩*/
sum[i]=sum[i]+score[i][j]; /*计算第i个学生的总成绩*/
}
}
printf(“\nScore1 Score2 Score3 Score4 Score5 Sum:\n”); /*输出提示信息*/
for(i=0;i<M;i++)
{
for(j=0;j<N;j++) printf(“%-8f”,score[i][j]);
/*将每名学生的5个科目的成绩输出*/
printf(“%-8f\n”,sum[i]); /*输出第i个学生的总成绩*/
}
}
通过该例我们可以看出,有关一组相同类型的数据都可以用数组进行考虑,用二维数组可以强化一维数组间的横向比较。
4.2.2 二维数组特点及其引用一.二维数组的特点从本质上来说,二维数组可以理解为一维数组的一维数组,即二维数组也是一个特殊的一维数组,这个数组的每一个元素都是一个一维数组。
二维数组的数组名也表示数组在内存中的首地址。二维数组的下标同样不能越界,也不能是负数或小数。
在对每个数组元素进行具体操作时,比如赋值,一般使用循环来实现,对二维数组常用两层循环控制,外层循环控制数组的第一个下标,内层控制数组的第二个下标。
由于内存本身是一种线性结构,因此二维数组在内存中的存储空间也是连续的线性空间。在内存中,二维数组的存放顺序是按行存储,即先顺序存放第一行的元素,再存放第二行元素。如图4.3所示。
通常二维数组可以作为矩阵的形式进行考虑,第一维表示行,第二维表示列。
2000
a[0][0]
2002
a[0][1]
2004
a[0][2]
2006
a[0][3]
2008
a[1][0]
2010
a[1][1]
2012
a[1][2]
2014
a[1][3]
2016
a[2][0]
2018
a[2][1]
2020
a[2][2]
2022
a[2][3]
图4.3 二维数组在内存中的存放
二,二维数组的引用二维数组元素的表示形式: 
数组名[下标][下标]
其中行号和列号可以是整型表达式或整型常量,其值从0开始,不能超过数组定义的范围。
正确的二维数组引用举例:
a[1][2] a[2-1][2*2-1] a[i][j] b[1][2]=a[2][3]/2
以下的引用是错误的:
static int a[3][4]; a[3][4]=5; (越界)
三.二维数组的初始化
⑴按行给二维数组赋初值:
static int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
则结果为:1 2 3 4
5 6 7 8
9 10 11 12
⑵按数组存储顺序依次给各元素赋初值:
static int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
注意,此方法数据没有明显的界限,当数据较多时容易出错
⑶可以对部分元素赋初值:(其余赋缺省值)
static int a[3][4]={{1},{5},{9}};
则结果为:1 0 0 0
5 0 0 0
9 0 0 0
⑷只对数组各行中的某些元素赋初值:
static int a[3][4]={{1},{0,5},{0,0,9}};
则结果为:1 0 0 0
0 5 0 0
0 0 9 0
⑸只对数组中前面几行赋初值,后面各行的元素自动赋缺省值:
static int a[3][4]={{1},{3,5}};
则结果为:1 0 0 0
3 5 0 0
0 0 0 0
⑹当对全部数组元素赋初值时,第一维的长度可以不说明,但第二维长度不能省:
static int a[ ][3]={1,2,3,4,5,6,7,8,9};
则结果为:1 2 3
4 5 6
7 8 9
⑺按行对部分元素赋初值,可以省略第一维的长度说明:
static int a[ ][3]={{0,0,3},{0,2},{1}};
则结果为:0 0 3
0 2 0
1 0 0
⑻同一维数组一样,不允许所提供的初值个数多于数组的元素个数。
4.2.3数组程序举例
【例4.3】用数组来处理Fibonacci数列问题。
分析:Fibonacci数列的特点是,前两个数为1,之后的数是前两个数的和。即:
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765
可以把Fibonacci数列存放到数组中,若f[i]表示数列中的第i个数,它的前两个数分别为f[i-2]、f[i-1],所以f[i]=f[i-2]+f[i-1],由于数列的前两个数为1,这种规律应该从第三个数开始,因此i的取值应该从2开始。
参考程序:
#include <stdio.h>
void main()
{
int i,f[20]={1,1};
for(i=2; i<20;i++)
f[i]=f[i-2]+f[i-1];
for(i=0;i<20;i++)
{
if(i%5==0) printf(“\n”); /*if语句用来控制换行,每行输出5个数据。*/
printf(“%-12d”,f[i]);
}
}
运行结果:
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765
【例4.4】将1到100之内的素数打印出来。
分析:本题可用筛法求素数,即在纸上写上1到100全部整数,然后逐个判断它们是否素数,找出一个非素数,就把它挖掉,最后剩下的就是素数。
要打印1到100之间的素数,就要把1到100每个数都拿来判断一下,看这个数是否素数,可以把这100个数存入一个数组,一个数组元素对应1到100的一个数,为使下标与数对应起来,可定义数组的长度为101,从a[1]开始存放数1,a[100]存放数100,要比较100个数,可以用循环实现,每次比较一个数。具体做法如下:
⑴ 先将1挖掉(因为1不是素数)。
⑵ 用2去除它后面的各个数,把能被2整除的数挖掉,即把2的倍数挖掉。
⑶ 用3去除它后面各数,把3的倍数挖掉。
⑷ 分别用4、5……各数作为除数去除这些数以后的各数。这个过程一直进行到在除数后面的数已全被挖掉为止。为了简化,只需进行到除数为(取其整数)即可。
要实现挖掉能整除的数,可将其对应的数组元素的值赋为0。最后不为0的数组元素即为素数。
参考程序:
#include <math.h>
#include <stdio.h>
void main()
{
int i,j,n,a[101];
for(i=1;i<=100;i++)
a[i]=i; /*将数存入数组,下标与数相对应*/
for(i=2;i<sqrt(100);i++)
for(j=i+1;j<=100;j++)
{
if(a[i]!=0&&a[j]!=0) /*已被挖掉的数不用比较*/
if(a[j]%a[i]==0)
a[j]=0; /*确定非素数的元素赋0值*/
}
printf(“\n”);
for(i=2,n=0;i<=100;i++)
{
if(a[i]!=0)
{
printf(“%5d”,a[i]);
n++;
}
if(n==10) /*每行输出10个元素*/
{
printf(“\n”);
n=0;
}
}
}
运行结果:
2
3
5
7
11
13
17
19
23
29
31
37
41
43
47
53
59
61
67
71
73
79
83
89
97
【例4.5】将一个二维数组行和列元素互换,存到另一个二维数组中。例如:
 
分析:要将二维数组的行和列元素互换,即第一行第一列变为第一行第一列,第一行第二列变为第二行第一列,第一行第三列变为第三行第一列,……,第i行第j列变为第j行第i列。
参考程序:
#include <stdio.h>
void main()
{
int b[3][2],i,j,a[2][3]={{1,2,3},{4,5,6}};
printf(“array a:\n”);
for(i=0;i<=1;i++)
{
for(j=0;j<=2;j++)
{
printf(“%5d”,a[i][j]);
b[j][i]=a[i][j];
}
printf(“\n”);
}
printf(“array b:\n”);
for(i=0;i<=2;i++)
{
for(j=0;j<=1;j++) printf(“%5d”,b[i][j]);
printf(“\n”);
}
}
运行结果:
array a:
1 2 3
4 5 6
array b:
1 4
2 5
3 6
【例4.6】有一个3×4的矩阵,要求编程求出其中值最大的那个元素的值,以及其所在的行号和列号。
分析:先设矩阵的第一个元素为最大max,用该值按行到列与矩阵其它元素进行比较,若当前比较的矩阵元素比max中的值大,则将max赋值为当前的矩阵元素,并记下该矩阵元素的行号和列号。N-S流程图见图4.4。
Max=a[0][0],row=0,colum=0
for i=0 to 2
for j=0 to 3
a[i][j]>max
T
F
max=a[i][j]
row=i
colum=j
输出:max和row,colum
图4.4 N-S流程图参考程序:
#include <stdio.h>
void main()
{
int i,j,row=0,colum=0,max;
int a[3][4]={{1,2,3,4},{9,8,7,6},{-10,10,-5,2}};
max=a[0][0];
for(i=0;i<=2;i++)
for(j=0;j<=3;j++)
if(a[i][j]>max)
{
max=a[i][j];
row=i; /*纪录行号*/
colum=j; /*纪录列号*/
}
printf(“max=%d,row=%d,colum=%d\n”,max,row,colum);
}
运行结果:
max=10,row=2,colum=1
4.3字符数组
4.3.1字符数组的引出在上一节中我们介绍了数组,数组是由相同类型的具有固定个数的元素组成的集合。数组中的所有元素都属于同一个数据类型。每个数组元素都是一个变量,其类型为数组的类型。与相同类型的普通变量完全一样。因此数组元素可以是整型或实型或字符类型等等。当数组元素是字符类型时,我们称之为字符数组。字符数组中的每一个数组元素都是一个字符。在实际问题中我们经常会遇到使用字符的情况,比如学生的姓名,一个英文单词等等,但是在这些字符中,我们往往考虑的是它们的整体,比如输出学生的姓名,统计英文单词个数等等。当把多个字符进行整体考虑,即考虑一串字符时,就涉及到字符串的概念。C语言中字符串是用字符数组存放的。
在日常生活和工作中,有很多地方要进行密码识别,比如在银行办理业务时要求输入密码,用户在计算机开机时需要输入密码,等等。
【例4.7】用户预先设置一个N位长度的密码,密码由数字、字母、符号等组成。当计算机开机时要求用户输入密码。3次之内,按用户输入的密码正确与否,输出“欢迎!”、“密码错误!”;超过3次后输出“退出!”。
分析:解决这个问题,首先要预设一个长度为N的密码,一个密码为一串字符,由于字符类型变量只能存放一个字符,所以作为密码的一串字符(称为字符串)就无法存放在一个字符类型变量中。C语言的数据类型中没有字符串类型,一般定义字符数组来存放字符串。字符串必须以'\0'字符作为结尾,称为字符串结束标志,在存储字符串时'\0'也算作一个字符。
字符数组的定义格式:char 数组名[字符个数];
例,char c[5];
c[0]='C'; c[1]='h'; c[2]='i'; c[3]='n'; c[4]='a';
该题中先预设一个长度为N的密码,可用一个长度为N+1的字符数组code[N+1]来存放,注意最后一个数组元素code[N]用来存放串结束标志'\0'。当计算机开机时,要求用户输入密码,所以可再定义一个字符数组user[N+1]用来存放用户输入的密码。在进行密码正确与否的判断时,只要将预设密码code[N+1]与用户输入的密码user[N+1]里面的每一个字符进行比较,都相等表示密码正确,只要比较到有一位不相等,则说明密码错误。
在有了'\0'作为字符串结束标识后,字符串就可以作为整体在程序中进行调用。在进行字符串输入输出时,除了用%c对各个字符进行操作外,还可以用%s控制符直接对这个字符串进行整体性的输入输出。
参考程序:
#define N 10 /*定义常量N表示密码的长度*/
void main()
{
char code[N+1]="ABC12*xyz"; /*预设的密码,注意字符数组的长度要加上'\0'*/
char user[N+1]; /*存放用户输入的密码*/
int i,k,t;
t=0;
while(1) /*进入循环,只有遇到程序员设定的break时才退出循环*/
{
k=1; /*定义预设密码与用户输入密码位是否相等的标志k*/
printf(“\nPlease input your key:”);
gets(user); /*读入用户输入的密码*/
for(i=0;i<N;i++) /*利用循环按字符比较输入密码与原密码*/
if(code[i]!=user[i])
k=0; /*当其中任一位不相同,令k=0*/
t++; /*尝试次数t加1*/
if(k==0) /*当用户输入的密码有错误,输出提示信息*/
printf(“\nKey error!”);
else
/*用户输入密码正确,输出欢迎信息,退出循环*/
{
printf(“\nWELCOME!”);
break;
}
if(t>3)
/*如果试了3次都不正确,那么输出提示信息,退出循环*/
{
printf(“\nExit!”);
break;
}
}
}
4.3.2 字符数组的定义和使用一.字符数组的定义用来存放字符型数据的数组称为字符数组,就是数组元素类型为字符型的数组,它主要用于存储一串连续的字符。字符数组的每个数组元素只能存放一个字符。
二.字符数组的初始化
⑴ 在进行字符数组初始化时,可逐个地将字符赋值给数组中的元素:
static char c[5]={'C','h','i','n','a'};
如果花括弧中的初值个数小于数组长度,按顺序赋值后,其余元素自动赋空字符'\0';
static char str[10]={'B','e','i','J','i','n','g'}; 则:str[7]=str[8]=str[9]='\0';
若初值个数大于数组长度,按语法错误处理;
若提供的初值个数与预定的数组长度相同,在定义时可省略数组长度。
static char c[ ]={'C','h','i','n','a'};(省略的长度应为5)
⑵ 也可以定义和初始化一个二维字符数组,如
char diamond[5][5]={{' ',' ','*'},{' ','*',' ','*'},{'*',' ',' ',' ','*'},{' ','*',' ','*'},{' ',' ','*'}}
这些字符排列出来如图4.5所示,
*
* *
* *
* *
*
图4.5字符排列三.字符数组的引用引用字符数组中的一个元素,可以得到一个字符。例如:
输出一个字符串
main()
{
char c[10]={ 'I',' ','a','m',' ','a',' ','s','t','u','d','e','n','t'};
int i;
for(i=0;i<10;i++)
printf(“%c”,c[i]);
printf(“\n”);
}
运行结果:I am a student
4.3.3 字符串的定义及使用一.字符串的定义字符串是由双引号括起来的字符序列,例如:
"Hello World!" "China" "How are you? "
C语言没有提供字符串类型,因此字符串的处理是依靠字符数组来完成。
字符串常量与字符常量不同,程序在定义时会在每个字符串的后面自动加上一个空操作符'\0'以示区别,在计算字符串长度时,'\0'不会计入字符串的长度中。
字符串必须以'\0'作为结束标志,它只是表示一个字符串的结束,没有任何的具体含义。在存储字符串时,虽然'\0'不会计入字符串长度中,但'\0'将会占用一个元素的存储空间,所以在定义字符数组时,应在字符串长度的基础上增加一个元素。定义字符数组时,要保证数组长度始终大于字符串实际长度。
例如:字符串"How are you?"有12个字符,但在定义时要定义的数组长度至少应为13,在内存中实际占13个字,包含一个字符串结束标志’\0’。
在有了'\0'作为字符串结束标志后,字符串就可以作为整体在程序中进行调用。
二.字符串的初始化可对字符串整体进行赋值初始化,
static char c[ ]={"China"}; 或static char c[ ]="China";
  等价于static char c[6]="China";
也等价于static char c[6]={'C','h','i','n','a','\0'};
不能写成:static char c[5]="China";
问题:static char c[5]={'C','h','i','n','a'}; 是否合法?
注意:字符数组并不要求包含'\0',这一点与字符串不同。
三.字符串的输入输出
⑴ 用"%c"格式控制符实现逐个字符的输入输出。
#include <stdio.h>
main()
{
int i=0;
char str[20]; /*定义字符数组str[20],最多可存放20个字符*/
while ((str[i++]=getchar( ))!='\n') ;
/*用getchar()函数输入字符,当输入的字符不是回车'\n'时,将继续输入,用i++控制字符数组下标的变化,直到遇到输入回车结束*/
str[i]='\0';
/*i的值为存放输入最后一个字符的数组元素的后一个元素,赋值为串结束标志'\0',表示字符串的结束*/
for (i=0; str[i]!= '\0'; i++)
printf("%c",str[i]);
/*用for循环控制字符串中各个字符的输出,遇到串结束标志'\0'时结束输出*/
}
⑵ 用"%s"格式控制符对数组进行整体输入和输出:
#include <stdio.h>
main()
{
char str[20];
scanf("%s",str);
printf("%s\n",str);
}
注意:
  a,从键盘输入字符串时不需加双引号"";
  b,用scanf()输入字符串时,空格和回车符都会作为字符串的分隔符,即scanf()的“%s”格式不能用来输入包含有空格的字符串。
c,在用printf()函数的“%s”格式输出字符串时,输出项是字符数组名,而不是数组元素名。
d,在用scanf()函数的“%s”格式输入字符串时,输入项是字符数组名,既不是数组元素,也不要数组名前加取地址符号'&'。因为C语言中数组名代表该数组的起始地址。
e,如果数组长度大于字符个数时,只输出到字符串结束标志符为止。
如:static char c[10]= "China";
printf(“%s”,c);
则输出仅为"China",而不是"China "。
⑶ 用gets( )和puts( )函数实现字符串的输入输出:
#include <stdio.h>
main()
{
char str[20];
gets(str);
puts(str);
}
注意,要使用gets和puts函数,需要在程序的开头添加“#include <stdio.h>”来进行说明。输入有空格的字符串时应使用gets函数,它可以读入包括空格在内的全部字符直到遇到回车符为止;用gets输入字符串时,若输入字符数大于字符数组的长度,则多出的字符会存放在数组的合法存储空间之外。puts函数一次输出一个字符串,输出时将'\0'自动转换成换行符。
说明:
① 输出字符不包括结束符'\0';
② 注意输入输出格式
int str[20],ch;
scanf("%s",str); scanf("%c",&ch);
printf("%s",str); printf("%c",str[0]);
③ 如果一个字符数组中包含多个'\0',则遇第一个'\0'时输出就结束。
static char str[10]={'a','b','c','\0','d','e','\0'};
printf(“%s”,str);
输出结果:abc
四.字符串数组如果要存储若干个相关的字符串可以用字符串数组。字符串数组就是数组中的每一个元素是一个字符串,因为存放字符串的本身是一个字符数组,所以字符串数组实际上是一个二维字符数组。该二维字符数组的第一维表示字符串的个数,第二维表示每个字符串的存储的长度(前面介绍的字符串用字符数组存储)。例如:char str[5][10];
数组str[5][10]可以存储5个字符串,每个字符串可以存储9个字符,注意,需要将最后一个存储空间用于存储字符串结束标志’\0’。
由于二维数组元素在内存中是按行连续分配存储空间的,因此字符串数组也是按字符串在内存中按行连续分配存储空间。即按字符串一个一个存储,先存储完第一个字符串再存储第二个字符串,直到所有的字符串都存储完。
五.字符串处理函数由于字符串有其特殊性,很多常规操作都不能用处理数值型数据的方法来完成,例如:赋值、比较等等。此外,字符串还有一些特殊的操作,例如:计算字符串长度、查找字符串的子串、字符串的连接等。
C语言中为字符串提供了一系列字符串处理函数来完成这些工作,这些字符串函数都包含在头文件“string.h”中,在使用这些函数时需要在程序的开头添加“#include,string.h””或“#include <string.h>”来进行说明。
1.字符串拷贝函数strcpy( )
格式:strcpy(字符数组1,字符串2)
功能:将字符串2复制到字符数组1中。
例:static char str1[10],str2[ ]="China";
strcpy(str1,str2);
执行后,str1的状态如图4.6所示。
C
h
i
N
a
\0
\0
\0
\0
\0
图4.6 str1的状态说明:
① 字符数组1的长度不应小于字符串2的长度。
②,字符数组1”必须写成数组名形式或字符型指针变量。“字符串2”可以是字符数组名、字符型指针变量,或字符串常量。
③ 拷贝时连同 '\0' 一起拷贝;
④ 不能用赋值语句将一个字符串常量或字符数组直接赋给另一个字符数组。例如下面两行都是不合法的:
str1={"China"};
str1=str2;
扩展函数:strncpy(字符数组1,字符串2,n )
作用:将字符串2的前n个字符复制到字符数组1中。
例:char c1[10],c2[ ]="abcdef";
strncpy(c1,c2,3);
2.字符串连接函数strcat( )
格式:strcat(字符数组1,字符数组2)
功能:把字符串2连接到字符串1的后面,仍存放在字符数组1中例:static char str1[20]="China ",str2[ ]="Bei jing";
printf(“%s”,strcat(str1,str2));
输出:China Bei jing
连接前后的状况见图4.7所示。
str1:
C
h
i
N
a
\0
\0
\0
\0
\0
\0
\0
\0
\0
str2:
B
e
i
j
i
n
g
\0
str3:
C
h
i
N
a
B
e
i
j
i
n
g
\0
\0
图4.7 连接前后的状况说明:
① 字符数组1必须定义得足够大,以便容纳连接后的新字符串;
② 连接前两个字符串后面都有一个 '\0',连接时将字符串1后面的'\0'去掉,只在新串最后保留一个'\0'。
3.字符串比较函数strcmp( )
格式:int strcmp(字符串1,字符串2)
功能:比较字符串1和字符串2(从左到右逐个字符比较ASCII值的大小,直到出现的字符不一样或遇到'\0'为止,比较结果由函数返回)。
① 若字符串1=字符串2,函数的返回值为0
② 若字符串1>字符串2,函数的返回值为一正整数
③ 若字符串1<字符串2,函数的返回值为一负整数例,strcmp("China","China") strcmp("35+78","4")
strcmp(str,"China") strcmp("computer","compare")
注意:对两个字符串比较,不能用以下形式:
if(str1==str2) printf(“yes”);
而只能用
if(strcmp(str1,str2)==0) printf(“yes”);
4.测试字符串长度函数strlen( )
格式:int strlen(字符串)
功能:测试字符串长度(函数的值为字符串的实际长度,不包括'\0'在内)。
例:static char str[20]="China";
printf("%d\n",strlen(str));
输出结果不是20,也不是6,而是5。也可以直接测字符串常量的长度,如:
strlen("China")
5.大小写转换函数strupr( )、strlwr( )
格式:strupr(字符串) strlwr(字符串)
功能:strupr( )函数将字符串中的小写字母转换为大写字母;strlwr( )函数将字符串中的大写字母转换为小写字母。
例:strupr("abC")=="ABC"  strlwr("abC")=="abc"
【例4.8】有3个字符串,要求找出其中最大者。
分析:3个字符串可考虑用二维字符数组来存放,现设一个二维的字符数组str,大小为3×20,即有3行20列,每一行可以容纳20个字符。图4.8表示此二维数组的情况。
str[0]:
C
h
i
n
a
\0
\0
…
\0
\0
str[1]:
J
a
p
a
n
\0
\0
…
\0
\0
str[2]:
I
n
d
i
a
\0
\0
…
\0
\0
图4.8 二维数组str[3][20]
可以把str[0]、str[1]、str[2]看作3个一维字符数组,它们各有20个元素。可以把它们如同一维数组那样进行处理。可以用gets函数分别读入3个字符串。经过二次比较,就可得到值最大者,把它放在一维字符数组string中。
参考程序:
#include <stdio.h>
#include <string.h>
main()
{
char string[20];
char str[3][20];
int i;
for(i=0;i<3;i++)
gets(str[i]);
if(strcmp(str[0],str[1])>0)
strcpy(string,str[0]);
else
strcpy(string,str[1]);
if(strcmp(str[2],string)>0)
strcpy(string,str[2]);
printf(“\nthe largest string is:\n%s\n”,string);
}
4.3.4字符数组程序举例
【例4.9】输入一行字符,统计其中有多少个单词,单词之间用空格分隔开。
分析:单词的数目可以由空格出现的次数决定(连续的若干个空格作为出现一次空格;一行开头的空格不统计在内)。如果测出某一个字符为非空格,而它的前面的字符是空格,则表示“新的单词开始了”,此时使num(单词数)累加1。如果当前字符为非空格而其前面的字符也是非空格,则意味着仍然是原来那个单词的继续,num不应再累加1。前面一个字符是否空格可以从word的值看出来,若word等于0,则表示前一个字符是空格;如果word等于1,意味着前一个字符为非空格。可以用图4.9表示。
未出现新单词,使word=0,num不累加。
当前字符=空格
—前一字符为空格(word=0),新单词出现,使num加1,word=1
—前一字符为非空格(word=1),为出现新单词,num不加1
图4.9 统计单词
N-S图如图4.10所示。
输入一字符串给string
i=0
当((c=string[i])!= '\0')
c等于空格?
真
假
word=0?
真
假
word=0
word=1
num=num+1
i=i+1
输出num
图4.10 N-S图参考程序:
#include <stdio.h>
void main()
{
char string[81];
int i,num=0,word=0; /* num用来统计单词个数,word作为判别是否单词的标志,若word=0表示未出现单词,如出现单词word就置成1。*/
char c;
gets(string);
for(i=0;(c=string[i])!= '\0';i++)
if(c==' ')
word=0;
else if(word==0)
{
word=1;
num++;
}
printf(“There are %d words in the line.\n”,num);
}
程序中for语句的“循环条件”为(c=string[i])!= '\0',它的作用实现将字符数组的某一元素(一个字符)赋给字符变量c。此时赋值表达式的值就是该字符,然后再判定它是否结束符。这个“循环条件”包含了一个赋值操作和一个关系运算。可以看到用for循环可以使程序简练。
运行结果:
I am a boy.
There are 4 words in the line.
【例4.10】编一个程序,将两个字符串连接起来,不要用strcat函数。
分析:两个字符串进行连接时,将第一个字符串的结束标志去掉,第二个字符串的第一个字符连到第一个字符串的结束标志的位置。注意连接后的字符串必须有串结束标志。
参考程序:
#include <stdio.h>
void main()
{
char s1[80],s2[40];
int i=0,j=0;
printf(“\nInput string1:”);
gets(s1);
printf(“Input string2:”);
gets(s2);
while(s1[i]!='\0')
i++;
while(s2[j]!='\0')
s1[i++]=s2[j++];
s1[i]='\0'; /*注意!*/
printf(“The new string is:%s”,s1);
}
运行结果:
Input string1:country↓
Input string2:side↓
The new string is:countryside
4.4 数组与函数上一章我们学习了函数的定义与使用,知道了函数之间主要是通过调用来联系的。实际上,函数之间的调用是通过参数之间的值传递或地址传递来实现的。我们已经学会了用普通变量作为函数的参数,下面我们来看看数组与函数之间的关系。
4.4.1数组元素作为函数参数对于单个的数组元素,其实质就是一个普通变量。因此,数组元素也可以作为函数的参数。与一般变量作函数参数是一样的,数组元素作为函数参数时,也是“值传递”,即参数之间传递的是变量的值,这种值的传递是单向的。例如在main函数中有以下调用:
ave=fun(a[0],a[1],a[2]); /*主函数中的调用函数语句*/
……
float fun(float a,float b,float c) /*定义一个fun函数*/
{
float sum,aver;
sum=a+b+c;
aver=sum/3.0;
return(aver);
}
在调用fun函数时,将a[0],a[1],a[2]分别传送给fun函数中的形参a,b,c,在求出平均值aver后,将aver的值返回main函数,赋给变量ave。
从该例中我们可看出,这种传送方式仍然是“值传送”方式,即只能从实参传给形参,而不能从形参传给实参。
【例4.11】将1到100之内的素数打印出来。
判断素数除了用筛法,还可以从素数的定义出发来考虑。素数是指除了1和本身,不能再被其它数整除的数,因此我们可以将每个数进行判断,看是否为素数。判断时,将当前判断的这个数与其后所有的数进行相除(为了简化同样可以只除到),看有没有能被整除的,只要有一个数能整除,则说明当前所判断的这个数不是素数,只有当所有的数都判断完,没有一个数能整除时,才能判断当前的这个数是素数。由于本题一直在进行判断素数的操作,因此也可以把求素数这个功能单列出来作为一个模块,函数的参数即所要判断是否为素数的那个数。具体实现如下:
参考程序:
#include <math.h>
int isPrime(int m) /*判断m是否为素数*/
{
int i,k;
k=sqrt(m);
for (i=2; i<=k; i++)
if (m%i==0) /*只要有一个数能被m整除就表示m不是素数*/
return(0);
return(1); /*如果该语句能被执行到,说明前面的循环都执行完也没有一个数能被m整除,则m为素数*/
}
main( )
{
int s=0,i,a[101];
for(i=1;i<=100;i++)
a[i]=i;
printf(“\nThe prime are:”);
for(i=1;i<=100;i++)
{
if (isPrime(a[i])==1) /*调用isPrime函数,判断a[i]是否为素数*/
{
printf("%4d",i);
s++;
}
if(s==10) /*一行输出10个数据*/
{
printf(“\n”);
s=0;
}
}
}
4.4.2数组名作为函数参数前面我们把30个学生的成绩组织成一个数组cscore[30]。要用函数计算30个元素的平均值,就应该由主调函数将这30个数组元素传递给被调函数。但是如果把30个数组元素作实参,那就对应有30个变量作形参,这显然是不实际的。因此,这种情况下,我们希望能将数组作为一个整体传递给被调函数。
由于数组名代表数组在内存中存放的首地址,而且数组元素在内存中是按照顺序存储的,因此只要我们能知道数组在内存中存储的首地址,其余数组元素都可以找到,而数组名就代表数组在内存中存放的首地址,所以,如果我们要传递整个数组,只要传递数组名就可以。
【例4.12】已知一个班30名学生参加了C语言考试,要求编写程序,输入每个同学的成绩,计算平均成绩。并输出所有学生的考试成绩和平均成绩。用函数实现。
参考程序:
main()
{
float average(float a[30]); /*定义数组作形参*/
float cscore[30];
float ave;
int i;
printf(“enter 30 students’ C scores:\n”);
for(i=0;i<30;i++)
scanf(“%f”,&cscore[i]);
ave=average(cscore); /*以数组名作函数实参*/
for(i=0;i<30;i++)
printf(“%6.2f”,cscore[i]);
printf(“average=%f\n”,ave);
}
float average(float a[30]) /*函数定义,形参数组应与实参数组类型相同*/
{
float sum=0,aver;
int i;
for(i=0;i<30;i++)
sum=sum+a[i];
aver=sum/30.0;
return(aver); /*将平均成绩返回主调函数*/
}
数组名作为函数参数时,要注意观察函数定义形式和调用形式。数组名作实参时,形参、实参必须是相同类型的数组。
用数组名作实参进行参数传递时,与指针传递参数相同,系统并不给形参分配内存空间,系统将实参数组的首地址赋给形参,这样形参也就指向了内存中的同一地址。也就是说,对应的实参、形参都指向相同的内存空间,因此在被调函数中对形参数组进行操作,实际上就是对实参数组进行操作。如果在被调函数中改变形参数组中某些元素的值,当然就改变了实参数组元素的值(这一点也与一般变量作函数参数不同)。这种传址与传值显然不同,传地址也可以说是一种特殊的传值,因为它传递的不是普通的数值,而是地址值。
在被调函数中所声明的形参数组的大小实际上是不起任何作用的(形参数组的标识符实际只是一个地址);形参数组不必指定长度,在定义的数组名后只跟一个空的方括号。如果确实需要知道数组的长度,可以另外设置一个参数传递数组的长度。例如,上例改为如下形式:
main()
{
float average(float a[],int n);
float cscore[30],ave;
int i,m=30;
……
ave=average(cscore,m);
……
}
float average(float a[],int n)
{
……
for(i=0;i<n;i++)
sum=sum+a[i];
aver=sum/n;
……
}
4.4.3多维数组作为函数参数多维数组也可以作为函数的参数。当多维数组元素作为函数实参时,形参需定义为同类型的变量;当多维数组名作为函数实参时,形参需定义为多维数组。定义形参数组时可以指定每一维的大小,也可省略第一维的大小说明(但不能省略第二维及其它高维的大小说明);
int fun(int array[3][4]) 或 int fun(int array[ ][4])
但不能写成,int fun(int array[ ][ ])
也不能写成,int fun(int array[3][ ])
实参数组可以大于形参数组,此时实参中只有前面数据起作用。如:
int score[5][10];
int array[3][10];
【例4.13】编写一个函数,求3×4矩阵中的最大元素并返回该值。
分析:先使变量max的初值为矩阵中的一个元素的值,然后将矩阵中各个元素的值与max相比,每次比较后都把“大者”存放在max中,全部元素比较完后,max的值就是所有元素的最大值。
参考程序:
int max_value(int a[ ][4])
{
int i,j,max;
max=a[0][0];
for (i=0; i<3; i++)
for (j=0; j<4; j++)
if (a[i][j]>max)
max=a[i][j];
reutrn(max);
}
main()
{
int a[3][4]={{1,3,5,7},{2,4,6,8},{15,17,34,12}};
printf(“max value is %d\n”,max_value(a));
}
4.5 数组与指针前面已经介绍,指针变量可以指向变量,数组元素相当于变量,而数组可以理解为一组变量,那么,指针变量能否指向数组元素或是数组?当然可以。指针变量指向数组元素的含义是,指针变量存储某一数组元素的地址。而对于数组来说,数组名表示数组在内存中的首地址,知道了首地址就可以依次取得该数组中的每一个数组元素,因此我们可以考虑定义指针变量指向数组的第一个元素,后面的数组元素可以通过指针的移动来获得。下面我们来学习数组与指针的关系。
4.5.1一维数组与指针一个变量有地址,一个数组包含若干个元素,每个数组元素都在内存中占存储单元,它们都有相应的地址。指针变量既然可以指向变量,当然也可以指向数组元素。所谓数组元素的指针就是数组元素的地址。
数组名代表数组在内存中的首地址,表示的是地址信息,因此数组名也可以理解为一个指针,不过数组名是一个指针常量,不能改变。可以用数组名来将数组的首地址赋给指针变量,找到了数组的首地址,就相当于找到了数组中的每一个元素。
一.指向数组元素的指针定义指向数组元素的指针就是将数组元素的地址赋值给指针变量,其方法与前面指向变量的指针变量相同,但是要注意指针变量的基类型要与数组元素的类型一致。例如:
int a[10]; /*定义整型数组*/
int *p; /*定义整型指针*/
p=a; /*指针变量p指向a[0]*/
这样定义之后,指针p与数组名a等价,都表示数组a的首地址(但a是常量,p是变量),所以也可以写成:p=&a[0];表示把数组的首地址赋给指针变量p。
二.通过指针引用数组元素前面介绍的数组元素的引用都是采用下标法,如a[i],代表数组中的第i+1个数组元素(下标i是从0开始的)。由于指针可以指向数组元素,因此引用数组元素时我们也可以考虑采用指针。
当指针指向一个数组元素时,我们可以通过改变指针变量中的地址值,即改变指针的指向来使指针指向下一个数组元素。使用指针法能使目标程序质量高(占内存少、运行速度快),就好像是一根指针在内存中移动,以此来存取数组元素的值。
如果指针变量p已指向数组中的一个元素,要想让指针指向下一个数组元素,C语言中用p+1来表示,所以p+1不是指简单的地址值加1,而是表示指向同一个数组中的下一个元素。以此类推,p+2表示指针变量p指向同一数组中的下两个元素,p+i表示指针变量p指向同一数组中的下i个数组元素。
其关系如图4.11所示。
图4.11 指针与数组从上图我们可以看出,p+i就表示a[i]的地址。因为*p表示指针p所指向地址的内容,所以*(p+i)就表示数组元素a[i]。
用多种方法访问一维数组各元素:
#include <stdio.h>
main( )
{
int a[5]={1,3,5,7,9},i,*p=a;
for (i=0;i<5;i++) printf(“%d”,a[i]);
for (i=0;i<5;i++) printf(“%d”,*(a+i));
for (i=0;i<5;i++) printf(“%d”,p[i]);
for (i=0;i<5;i++) printf(“%d”,*(p+i));
for (;p<a+5;p++) printf(“%d”,*p);
p=a; while (p<a+5) printf(“%d”,*p++);
}
在使用指针变量时,要注意:
⑴ 可以改变指针变量的值,但指针常量不可改变。
例如,p++合法,但a++不合法(因为a是数组名,是指针常量)
⑵ 要注意指针变量的当前值。
⑶ 使用指针变量指向数组元素时,应保证指向数组中有效的元素。
⑷ 注意指针变量的运算(指向运算符的优先级最高):
p++ 是p指向下一个元素
*p++等价于*(p++)
*(p++)与*(++p)不同
(*p)++使p所指向的元素加1
【例4.14】一个数组含有10个学生的成绩。输出所有学生的成绩,以及最高分和最低分。
分析:本题可用一般的方法解答,也可以用指向一维数组元素的指针解决。用指针解答时,将定义的指针变量指向数组的首地址,即第一个数组元素的地址,然后通过指针指向的改变来访问不同的数组元素,来输出成绩和求取最大值和最小值。
参考程序:
main()
{
int score[10],*p=&score[0]; /*指针变量p指向score[0]*/
int i,max,min; /*max、min分别存放成绩的最大值和最小值*/
for(i=0;i<10;i++)
scanf(“%d”,p++); /*读入数组元素,指针值自增*/
p=&score[0]; /*指针变量p回到第一个元素*/
max=*p; /*以score[0]作为最大值的初值*/
min=*p; /*以score[0]作为最小值的初值*/
for(i=0;i<10;i++) /*凡比max大的值就给max,比min小的值就给min*/
{
if(*(p+i)>max) max=*(p+i);
if(*(p+i)<min) min=*(p+i);
}
for(i=0;i<10;i++)
printf(“%4d”,*(p+i)); /*利用指针变量输出数组*/
printf(“\nmax=%4d,min=%4d\n”,max,min);/*输出最大值,最小值*/
}
注意,在上面程序中,利用指针输入数组元素的值,每输完一个值指针就指向下一个数组元素,即输入循环中指针不断改变。当输入结束时,指针p已不再指向score数组中的任何一个元素了。可是后面还要用指针p参与比较数组元素的大小,所以在执行完输入后,指针p要回位,重新赋值,指向数组的第一个元素。
4.5.2多维数组与指针数组是具有相同“数据类型”的数据的顺序集合,而数组本身也是C语言的一种数据类型,同样可以作为数组的元素类型。当一个一维数组的元素类型为数组时,便构成了多维数组。
熟记下面两组等价式:
x[i]? *(x+i) &x[i]? x+i
一.多维数组元素的地址先回顾一下多维数组的性质。以二维数组为例:
设有数组定义为:int a[3][4];
则有:a表示数组在内存中的首地址,也就是数组中第1个元素(也是第一行)的首地址,它是一个地址常量,其值由系统在编译时确定,程序运行期间不能改变。
该二维数组可以理解为:它是一个一维数组,含有3个元素,每个元素又是一个一维数组,该一维数组含有4个元素,每个元素是int类型。
思考:
a+1代表哪个元素的地址?a[0]、a[1]、a[2]分别代表什么?
二维数组的逻辑结构图如图4.12所示。
图4.12 二维数组的逻辑结构图
a[i]? *(a+i) a[i][j]? *(*(a+i)+j)
二.指向多维数组元素的指针变量指向由m个元素组成的一维数组的指针变量的定义:
类型名(*指针变量名)[长度];
例如:int (*pa)[4];
pa是指针变量,它指向一个一维数组,该数组含有4个元素,每个元素的类型是int。
说明:
a,与定义int *pa; 以及int *pa[4]; 含义不 同。
b,如果执行pa++,则pa实际增加了多少呢?
4.5.3用数组名作函数参数前面内容中讲到:用数组名作函数参数时,如果形参数组中各元素的值发生变化,实参数组元素的值也随之变化。这是为什么?
当用数组名作函数参数时,实参传给形参的是一个地址值,称为地址传递,作为形参就要定义一个变量来接收实参传过来的地址,而我们学过的只有指针变量可以存放地址,因此,形参应定义为一指针变量,它是指向实参的,因此对形参的操作实质上是访问实参。
函数的定义形式,void f(int arr[],int n)
也可写为:void f(int *arr,int n)
归纳起来,如果有一个实参数组,想在函数中改变此数组的元素的值,实参与形参的对应关系有以下四种:
⑴ 形参与实参都用数组名。如:
f(int x[ ],int n)
{

}
main()
{
int a[10];

f(a,10);

}
⑵ 实参用数组名,形参用指针变量。如:
f(int *x,int n)
{

}
main()
{
int a[10];

f(a,10);

}
⑶ 实参形参都用指针变量。例如:
f(int *x,int n)
{

}
main()
{
int a[10],*p;
p=a;

f(p,10);

}
⑷ 实参为指针变量,形参为数组名。如:
f(int x[ ],int n)
{

}
main()
{
int a[10],*p;
p=a;

f(p,10);

}
注意:实参数组名是指针常量,形参数组名是指针变量。
【例4.15】将数组 a中的n个数按相反顺序存放。
分析:将数组中的n个数按相反的顺序存放,即将数组的第一个元素与最后一个元素交换,第二个与倒数第二个交换,以此类推,第i个数与第n-1-i个数交换顺序。注意,交换是前后数进行,所以交换一半的数就可以,否则会使交换好的数再交换回去。
参考程序:
void inv(int x[ ],int n)
{
int temp,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;
}
}
main( )
{
int i,a[10]={3,7,9,11,0,6,7,5,4,2};
printf(“the original array:\n”);
for(i=0;i<10;i++) printf(“%d,”,a[i]);
inv(a,10);
printf(“the array has been inverted:\n”);
for(i=0;i<10;i++) printf(“%d,”,a[i]);
printf(“\n”);
}
函数可改写为:
void inv(int *x,int n)
{
int *p,*i,*j,temp,m=(n-1)/2;
i=x;j=x+n-1;p=x+m;
for(;i<=p;i++,j--)
{
temp=*i;*i=*j;*j=temp;
}
}
4.6字符串与指针
4.6.1字符串的指针和指针变量从上一节中指针与数组的关系可以看到,利用指针处理连续的内存单元是非常方便的,因此,也可以利用指针来进行字符串的处理,只要将指针指向字符串的起始位置就可以方便的使用了。
可以定义一个字符型指针,使该指针指向字符串起始地址,就可以使用该指针来进行字符串的引用,这个指针称为字符串的指针。
例如下面两句定义一个指向字符串的指针s,并利用它输出字符串。
char *s="Hello";
printf("%s\n",s);
定义字符串指针的一般形式是:
char *字符串指针变量名;
所以,到现在我们学过的字符串的表示形式有以下两种:
⑴ 用字符数组存放一个字符串,然后输出字符串。
main( )
{
char s[ ]="I love China!";
printf("%s\n",s);
}
如图4.13所示。
I l o v e c h i n a !\0
图4.13 字符数组存放字符串
⑵ 用字符指针指向一个字符串。
main( )
{
char *s="I love China!";
printf("%s\n",s);
}
程序在定义字符指针变量s时,把字符串首地址(即存放字符串的字符数组的首地址)赋给s。如图4.14所示。
图4.14 字符指针指向字符串
4.6.2用字符串指针访问字符串输出的一般形式是:printf(“%s”,字符串指针变量名);
输出还可以使用puts,输入可以使用gets或者scanf。
对字符串中字符的存取,可以用下标方法,也可以用指针方法。
① 下标法:
#include <stdio.h>
main( )
{
char a[ ]="Hello,World!",b[20];
int i;
for (i=0; a[i]!='\0'; i++)
b[i]=a[i];
b[i]='\0';
printf("%s\n",b);
}
② 指针法:
#include <stdio.h>
main( )
{
char a[ ]="Hello,World!",b[20];
char *pa=a,*pb=b;
for ( ; *pa!='\0'; pa++,pb++)
*pb=*pa;
*pb='\0';
printf("%s\n",b);
}
【例4.16】比较两个单词的大小(单词全部用小写字母)。按照字典排列法,前面的单词小,后面的单词大。
分析:定义两个字符串指针,分别指向这两个单词。然后利用循环结构进行比较,从各自的第1个字母开始比较,如果两个字母相同,那么各自指针增1,指向下一个字母,继续比较;如果不同就可以退出循环,退出循环后比较使循环结束的那两个字母就可以区分出两个单词谁大谁小;如果退出循环后字符相同,则两单词相等。
参考程序:
main( )
{
char a[20],b[20],*sa,*sb;
int i=0;
sa=a;sb=b; /*指针变量sa,sb分别指向字符串a,b*/
gets(sa); gets(sb);
while((*(sa+i)==*(sb+i)&&*(sa+i)!= '\0'&&*(sb+i)!= '\0')
/*当对应的字母相同且sa、sb指向的单词没到末尾*/
i++; /*使sa、sb指针继续指向下一个字母;如果对应字母不同,循环立即结束*/
if(*(sa+i)== '\0'&&*(sb+i)== '\0')
/*循环退出后判断,如果sa、sb指向的单词同时结束,说明两个单词相同*/
printf(“%s and %s is the same name!\n”,sa,sb); /*如果sa指向的大于sb指向*/
else
if(*(sa+i)<*(sb+i))
printf(“max is %s,min is %s\n”,sb,sa);/*否则说明sb指向的大于sa指向的*/
else
printf(“max is %s,min is %s\n”,sa,sb);
}
4.6.3字符指针和字符数组的区别虽然字符指针和字符数组都可以用于字符串的处理,但两者是不同的。
字符数组在定义时,不论是否进行初始化,都会为其分配存储空间,以存储数组的内容,它存储的是字符串本身的内容。
字符指针则不同,如果字符指针在定义时没有进行初始化,则不会为其分配字符串的存储空间,而只是分配一个指针变量的存储单元,用于存储指针;如果定义时进行了初始化,则在分配一块连续内存空间存储字符串外,还分配一个存放指针变量的存储单元,并将字符串存储空间的起始地址赋给字符指针。
字符数组一旦定义,其使用的存储空间就是固定的,在任何时候都可以使用数组名对数组进行访问。
字符指针变量只是一个指向内存地址的指针,在程序中改变后将不再指向原来的内容。
例如:
main()
{
char str[]="this is a test!"; /*字符数组*/
char *s; /*字符指针*/
int i=0;
s=str;
while(*s!= '\0')
{
printf(“%c”,*s);
s++;
printf(“%c”,str[i]);
i++;
}
printf(“%s\n”,s); /*由于此前指针已被改变,所以此时会出错*/
printf(“%s\n”,str); /*使用str来输出字符串*/
}
字符数组只能在初始化时进行字符串的整体赋值,在程序运行中则不能。例如:
char str[]="this is a test!"; /*初始化时赋值*/
str="Hello world!"; /*错误的用法*/
字符指针变量既可以在初始化时进行字符串的赋值,又可以在程序运行中进行赋值,因为字符指针只是个指针,在程序中是可变的,在程序中赋值后将指向所赋的字符串的内存起始地址。例如:
char *str="this is a test!"; /*初始化时赋值,指向字符串"this is a test!"在内存的起始地址*/
str="Hello world!"; /*此时将指向字符串"Hello world!"在内存的起始地址*/
字符数组名是一个常量,在程序中只能引用,不能改变。字符指针是一个变量,在程序中可以指向任一位置,因此应该注意指针的位置,以防止引用出错。
总结起来有以下几点:
① 字符数组由若干个元素组成,每个元素中存放着一个字符;而字符指针变量中存放的是地址,不是将字符串放到字符指针变量中。
② 赋值方式:对于字符数组只能对各个元素赋值。
char str[20];
str[20]="I love China!";(是错误的)
而对于字符指针可进行整体赋值:
char *ps;
ps="I love China!";
③ 对字符指针变量赋初值:
char *s="I love China!";
等价于,char *s;
s="I love China!";
char str[ ]="I love China!";
等价于:char str[14];
strcpy(str,"I love China!");
④ 如果定义了一个字符数组,在编译时为它分配内存单元,它有确定的地址。而定义一个字符指针变量时,给指针变量分配内存单元,在其中可以放一个地址,也就是说,该指针变量指向一个字符型数据,但如果未对它赋予一个地址值,则它并未具体指向一个确定的字符数据。例如:
char str[10];
scanf(“%s”,str);
以下的定义与输入是不对的:
char *a;
scanf(“%s”,a);
原因是,scanf的含义是将输入的字符串放入a所指向的内存单元中,但此时指针变量a并没有指向,所以不能完成输入。
应改为:
char *a,str[10];
a=str;
scanf(“%s”,a);
所以,要先使a有一个确定值,也就是使a指向一个数组的开头,然后输入一个字符串,把它存放在以该地址开始的若干单元中。
⑤ 指针变量的值是可以改变的。
main( )
{
char *a="I love china!";
a=a+7;
printf(“%s”,a);
}
4.6.4字符串指针作函数参数将一个字符串从一个函数传递到另一个函数,可以用地址传递的方法,即用字符数组名作参数或用指向字符串的指针变量作参数。在被调用的函数中可以改变字符串的内容,在主调函数中可以得到改变了的字符串。
【例4.17】用函数调用实现字符串的复制。
分析:要实现字符串的复制,可对字符串中的每个字符进行一个一个的复制,用字符串结束标志'\0'来控制,当没有遇到串结束标志时就进行字符的复制,遇到时就结束复制。要注意,产生的新串也必须要有串结束标志,如果进行复制后没有串结束标志'\0'的话,要手动的为新串加上一个串结束标志'\0'。可将复制的操作作为函数实现,有以下几种形式:
① 用字符数组作参数
void copy_string(char to[],char from[])
{
int i=0;
while (from[i]!='\0') /*判断是否已复制完*/
{
to[i]=from[i]; /*进行字符的复制*/
i++;
}
to[i]='\0'; /*在新串最后加上串结束标志*/
}
main( )
{
char a[]="abcdefg";
char b[]="12345";
……
copy_string(a,b);
……
}
在main函数中也可以不定义字符数组,而用字符型指针变量。
main( )
{
char *a="abcdefg";
char *b="12345";
……
copy_string(a,b);
……
}
②形参用字符指针变量
void copy_string(char *to,char *from)
{
for ( ; *from!='\0'; from++,to++)
*to=*from;
*to='\0'; /*注意!*/
}
main( )
{
char *a="abcdefg";
char *b="12345";
……
copy_string(a,b);
……
}
可以对copy_string函数作下列简化:
①void copy_string(char *to,char *from)
{
while ( (*to=*from)!='\0' ) while (*to=*from)
{
to++;
from++;
}
}
思考:为什么此时函数中没有*to='\0';语句?
②void copy_string(char *to,char *from)
{
while ( (*to++=*from++)!='\0' ); while (*to++=*from++)
}
③void copy_string(char *to,char *from)
{
while (*from!='\0' ) while (*from)
*to++=*from++;
*to='\0';
}
④void copy_string(char *to,char *from)
{
for ( ; (*to++=*from++)!='\0'; );
}
⑤void copy_string(char *to,char *from)
{
for ( ; (*to++=*from++) ; );
}
4.7典型例题
【例4.18】冒泡法排序:输入一个班级20名学生的C语言考试成绩,按成绩从高到低的顺序排序。
分析:要让成绩从高到低进行排序,可采用以下的考虑:让第一个学生的成绩与第二个学生的成绩进行比较,将成绩高的放前面,成绩低的放后面,再让第二个学生与第三个学生进行成绩比较,同样将低的成绩放到后面,这样比较完19次后,最低的成绩就会排到最后一个,再在剩下的19个成绩中进行相邻两个成绩的比较,比较完会把次最低的成绩排到倒数第二个位置,以此类推,比较到剩下两个成绩时,把高的成绩放前面,低的成绩放后面,最后就得到一个成绩从高到低排列的序列。这就是冒泡法排序的思想。
第一趟排序可让最低的成绩排到最后,第二趟排序让次最低的成绩排到倒数第二个位置,最后剩两个成绩时,比较完就可以得出结果,因此总共要进行19趟排序。而且,在进行完第一趟排序后,最低的成绩已经排在了最后,在进行下一趟的排序时,只要对剩下的19个成绩进行比较就可以,要进行18次的两两比较,在进行了第二趟排序后,又得到了次最低的成绩,排在倒数第二的位置,那么,在进行第三趟排序时,只要考虑剩下的18个成绩。这样,第一趟排序19次两两比较20个成绩,第二趟排序18次两两比较19个成绩,则第i趟排序要进行(20-i)次两两比较。
参考程序:
#define N 20
main()
{
float cscore[N],temp;
int i,j;
printf(“Input 20 scores:\n”);
for(i=0;i<N;i++)
scanf(“%f”,&cscore[i]);
for(i=0;i<N-1;i++)
for(j=0;j<N-1-i;j++)
if(cscore[j]<cscore[j+1])
{
temp=cscore[j];
cscore[j]=cscore[j+1];
cscore[j+1]=temp;
}
printf(“\nThe sorted score is:\n”);
for(i=0;i<N;i++)
printf(“%8.2f”,cscore[i]);
}
【例4.19】选择法排序:对10个整数排序(从小到大)。
分析:选择排序的思路如下:
设有10个元素a[1]…a[10],将a[1]与a[2]…a[10]比较,若a[1]比a[2]…a[10]都小,则不进行交换,即无任何操作。若a[2]…a[10]中有一个以上比a[1]小,则将其中最小的一个(假设为a[i])与a[1]交换,此时a[1]中存放了10个中最小的书。第二轮将a[2]与a[3]…a[10]比较,将剩下9个数中的最小者a[i]与a[2]对换,此时a[2]中存放的是10个中第2小的数。依此类推,共进行9轮比较,到a[10]就已按由小到大顺序存放。
参考程序:
main()
{
int i,j,min,temp,a[11];
printf(“Enter data:\n”);
for(i=1;i<=10;i++)
{
printf(“a[%d]=”,i);
scanf(”%d”,&a[i]); /*输入10个数*/
}
printf(“\n”);
for(i=1;i<=10;i++)
printf(“%5d”,a[i]); /*输出这10个数*/
printf(“\n”);
for(i=1;i<=9;i++) /*以下8行是对10个数排序*/
{
min=i;
for(j=i+1;j<=10;j++)
if(a[min]>a[j])
min=j;
temp=a[i]; /*以下3行将a[i+1]…a[10]中最小者与a[i]对换*/
a[i]=a[min];
a[min]=temp;
}
printf(“\nThe sorted numbers:\n”);
for(i=1;i<=10;i++) /*数出已排好序的10个数*/
printf(“%5d”,a[i]);
}
【例4.20】折半查找:有15个数按从大到小的顺序存放在一个数组中,输入一个数,要求找出该数是数组中第几个元素的值。如果该数不在数组中,则打印出“无此数”。
分析:从表列中查一个数最简单的方法是从第1个数开始顺序查找,将要找的数与表列中的数一一比较,直到找到为止。这种“顺序查找法”效率较低。
折半查找法是效率较高的一种方法。基本思路如下:
假如有已按有小到大排好序的9个数,a[1]…a[9],其值分别为:1、3、5、7、9、11、13、15、17。若数入一个数3,想查3是否在此数列中,先找出表列中居中的数,即a[5],将要找的数3与a[5]比较,a[5]的值是9,发现a[5]>3,显然3应当在a[1]到a[5]范围内,而不会再a[6]到a[9]范围内。这样就可以缩小查找范围,甩掉a[6]到a[9]这一部分,即查找范围缩小为一半。再找a[1]到a[5]范围内居中的数,即a[3],将要找的数3与a[3]比较,a[3]的值是5,发现a[3]>3,显然3应当在a[1]到a[3]范围内。这样又将范围缩小一半。再将3与a[1]到a[3]范围内的居中的数a[2]比较,发现要找的数3等于a[2],查找结束。一共比较了3次,如果表列中有n个数,则最多比较的次数为int(log n)+1。
参考程序:
#include <stdio.h>
#define N 15
main()
{
int i,j,number,top,bott,mid,loca,a[N],flag=1,sign=1;
char a;
printf(“Enter data:\n”);
scanf(“%d”,&a[0]);
i=1;
while(i<N)
{
scanf(“%d”,&a[i]);
if(a[i]>=a[i-1])
i++;
else
printf(“Enter this data again:”);
}
printf(“\n”)
for(i=0;i<N;i++)
printf(“%4d”,a[i]);
printf(“\n”);
flag=1;
while(flag)
{
printf(“Input number to look for:”);
scanf(“%d”,&number);
loca=0;
top=0;
bott=N-1;
if((number<a[0])||(number>a[N-1]))
loca=-1;
while((sign==1)&&(top<=bott))
{
mid=(bott+top)/2;
if(number==a[mid])
{
loca=mid;
printf(“Find %d,its position is %d\n”,number,loca+1);
sign=0;
}
else if(number<a[mid])
bott=mid-1;
else
top=mid+1;
}
if(sign==1||loca==-1)
printf(“%d is not found.\n”,number);
printf(“Continue or not(Y/N)?”);
scanf(“%c”,&c);
if(“c=='N'||c=='n')
flag=0;
}
}
【例4.21】回溯法:求从1-N中任选M(M≤N)个不许重复的全部排列情况。
分析:这是一个排列问题,完全可以使用多重循环解决。如N=M=3,
for(i=1;i<=3;i++)
for(j=1;j<=3;j++)
for(k=1;k<=3;k++)
if(i!=j&&i<=k&&j<=k)
printf(“%d%d%%d”,i,j,k);
N越大循环的次数就越多。所以,这不是一个好办法,必须另辟蹊径。回溯法属于一种盲目搜索方法。设解的结果由(x1,x2,x3,…,xn)这n元组组成,xi属于某个集合si(i<n),假设已经搜索到(x1,x2,x3,…,xi)这个i元组了,继续在集合si+1里搜索到xi+1,并检查(x1,x2,x3,…,xi,xi+1)是否满足条件;如果满足就向解集添加xi+1,否则就继续在集合si+1搜索,并判断。当在整个搜索空间找不到一个xi+1使(x1,x2,x3,…,xi,xi+1)满足条件,就舍弃xi,回溯到(x1,x2,x3,…,xi-1)重新在集合si中搜索新的xi……,直到x1也被舍弃,回溯结束。
参考程序:
#include<stdio.h>
int q; /*声明全局变量*/
void main()
{
void pri(int m0,int b[]);/*输出函数*/
int goal(int r,int b[]);/*条件判断函数*/
int t,n,m,a[100];
printf("intput n,m(n>=m):");
scanf("%d%d",&n,&m);
for(t=1;t<=m;t++) a[t]=0;/*变量赋初值*/
t=1;q=0;
while(t>0)
{
a[t]++; /*添加第一个部分解*/
if(a[t]>n)
{
a[t]=0;
t--;
} /*回溯*/
else if(goal(t,a)==1) /*调用条件判断函数,如果满足条件*/
{
if(t==m) pri(m,a); /*如果部分解全部找到,就输出结果*/
else
t++; /*否则继续找下一个*/
}
}
}
int goal(int r,int b[]) /*条件判断函数*/
{
int i=1,g1;
while((b[i]!=b[r])&&(i<r)) i++;
if(i!=r) g1=0;
else
g1=1;
return g1;
}
void pri(int m0,int b[])
{
int i;
q++;
printf("(%d) ",q);
for(i=1;i<=m0;i++)
printf("%d",b[i]);
printf("\n\r");
}
【例4.22】贪心法:设计一个找零钱的算法。要求从面值最大币种开始,只有不能找大的,才找下一个面值的币种。
分析:该算法需要从问题的初始解出发,一步一步接近给定目标,并尽可能快地去逼近更好的解。这种方法称为“贪心法”。贪心法追求最快、最优解,不能得到全部解。
参考程序:
#include <conio.h>
#include<stdio.h>
#define N 20 /*限制找零的最大张数*/
void main()
{
int find(int n,int *d,int c,int *pd);
int n,k,i,p[N],d[7]={100,50,20,10,5,2,1};
// clrscr();
printf("Plase input moneny n:");
scanf("%d",&n);
for(i=0;i<7;i++) /*判断找零的首张币种面额*/
if(n>=d[i])
break;
k=find(n,&d[i],7-i,p); /*7-i代表找零的币种数*/
if(k<=0)
printf("Error,Sorry!\n");
else /*输出p[k]数组*/
{
printf("%d=%d",n,p[0]); /*处理只找一张币的情况*/
for(i=1;i<k;i++)
printf("+%d",p[i]);
}
}
int find(int n,int *d,int c,int *pd) /*产生p数组,并统计找零币的张数*/
{
int r;
if(n==0) return 0;
if(c==0) return -1;
if(n<*d)
return find(n,d+1,c-1,pd); /*找下一个新的币种*/
else /*找同面额币种,放入pd中*/
{*pd=*d;
r=find(n-*d,d,c,pd+1);
if(r>=0)
return r+1;
return -1;
}
}
【例4.23】某歌手大赛,共有10个评委给歌手打分,分数采用百分制,去掉一个最高分,去掉一个最低分,然后取平均分,得到歌手的成绩。10个分数由键盘输入,编写程序计算某歌手的成绩。
参考程序:
float calculates(float s[10])
{
int i;
float max=s[0],min=s[0],sum=0,ave;
for(i=0;i<10;i++)
{
if(s[i]>max)
max=s[i];
if(s[i]<min)
min=s[i];
sum=sum+s[i];
}
ave=(sum-max-min)/8;
return(ave);
}
main()
{
int i;
float score,s[10];
printf("\nPlease input the 10 scores:\n");
for(i=0;i<10;i++)
scanf("%f",&s[i]);
score=calculates(s);
printf("\nThe singer's score is %f",score);
}
【例4.24】编程输出如下所示的斜塔形数字。
3 6 10 15
5 9 14
4 8 13
7 12
11
分析:该三角形为矩阵的上斜三角,除第一列外,其余元素都有规律可循。设行数为i,列数为j,矩阵数值存储于数组t中,矩阵有k层。同一行中,第i列的值等于同行中i-1列的值、列数及行数之和加1,即t[i][j]=t[i][j-1]+j+i+1,因此求出第一列就能计算出所有的值。第一列中,第i行的值等于同列中i-1行的值和行数之和,即t[i][0]=t[i-1][0]+1。由于为矩阵上斜三角,当元素的行列之和等于矩阵层数时就应该换行,但由于C语言中的数组下界从0开始,因此层数应该减1,即i+j==k-1时换行。
参考程序:
#include,stdio.h”
main()
{
int t[40[40];
int k,j,i;
t[0][0]=1; /*给左上角元素赋值*/
printf(“\nPlease input the layer(<20):”);
scanf(“%d”,&k);
for(i=0;i<k;i++)
{
if(i>0) /*对第一列除左上角以外的元素赋值*/
t[i][0]=t[i-1][0]+i;
for(j=0;j<k;j++)
{
if(j>0) /*对第i行除第一列外的元素赋值*/
t[i][j]=t[i][j-1]+j+i+1;
if(j+i==k-1) /*当超出上斜三角范围时停止赋值*/
break;
}
}
for(i=0;i<k;i++)
{
for(j=0;j<k;j++)
{
if(j+i==k-1) /*当超出上斜三角范围时换到下一行*/
break;
printf(“%4d”,t[i][j]);
}
printf(“\n”);
}
}
【例4.25】有一行电文,已按下面规律译成密码:
A—>Z a—>z
B—>Y b—>y
C—>X c—>x
……
即第一个字母变成第26个字母,第i个字母变成第(26-i+1)个字母。非字母字符不变。要求编程序将密码译成原文。
分析:将字符存放到数组ch中,如果ch[j]是大写字母,则它是第(ch[j]-64)个大写字母。例如ch[j]的值是大写字母'B',它的ASCII码为66,它应是第(66-64)=2个大写字母。按密码规定应将它转换为第(26-i+1)个大写字母,即第(26-2+1)=25个大写字母。而26-i+1=26-(ch[j]-64)+1=26+64-ch[j]+1,即91-ch[j](如ch[j]等于'B',91-'B'=91-66=25,ch[j]应为与第25个大写字母对换)。该字母的ASCII码为91-ch[j]+64,即25+64=89,89是'Y'的ASCII码。可以表示为155-ch[j]。小写字母情况与此相似,但由于'a'的ASCII码为97,因此公式应改为26+96-ch[j]+1+96=123-ch[j]+96=219-ch[j]。若ch[j]的值为'b',则其交换对象为219-'b'=219-98=121,它是'y'的ASCII码。
由于此密码的规律是对称交换,即第一个字母与最后一个字母交换,第二个字母与最后第二个字母交换……因此从原文译为密码和从密码译为原文,都是利用同一个公式。
参考程序:
#include <stdio.h>
main()
{
int j,n;
char ch[80],tran[80];
printf(“Input cipher code:”);
gets(ch);
printf(“\ncipher code:%s”,ch);
j=0;
while(ch[j]!= '\0')
{
if((ch[j]>= 'A')&&(ch[j]<= 'Z'))
tran[j]=155-ch[j];
else if((ch[j]>= 'a')&&(ch[j]<= 'z'))
tran[j]=219-ch[j];
else
tran[j]=ch[j];
j++;
}
n=j;
printf(“\noriginal text:”);
for(j=0;j<n;j++)
putchar(tran[j]);
}
运行结果:
Input cipher code:R droo erhrg Xsrmz mvcg dvvp.
cipher code,R droo erhrg Xsrmz mvcg dvvp.
Original text:I will visit China next week.
【例4.26】有n个人围成一圈,顺序排号。从第一个人开始报数(从1报到3),凡报到3的人退出圈子,问最后留下的是原来第几号的那位。
分析:本题使用数组非常方便,定义一个数组,将1-n放入数组,利用指针的移动,选择对应元素。退出的人将其对应的元素置0,每次计数时排除为0的元素,最后不为0的元素就是剩下的人。
参考程序:
#define N 50
main()
{
int i,k,m,n,num[N],*p;
printf(“Input number of person,n=”);
scanf(“%d”,&n);
p=num;
for(i=0;i<n;i++)
*(p+i)=i+1; /*以1至n为序给每个人编号*/
i=0; /*i为每次循环时的计数变量*/
k=0; /*k为按1、2、3报数时的计数变量*/
m=0; /*m为退出人数*/
while(m<n-1) /*当退出人数比n-1少时(即未退出人数大于1时)执行循环体*/
{
if(*(p+i)!=0)
k++;
if(k==3) /*对退出的人的编号置为0*/
{
*(p+i)=0;
k=0;
m++;
}
i++;
if(i==n) i=0; /*报数到尾后,i恢复为0*/
}
while(*p==0) p++;
printf(“The last one is NO.%d\n”,*p);
}
*4.8链表
4.8.1概述链表是一种常见的数据结构,是动态的进行存储分配,不同于数组,数组是事先定义固定的长度。
当实际使用时如果元素个数不确定,例如,有的班级有30名同学,而有的班级有100名同学,或者不能确定最多人数,此时定义数组长度必须足够大,而当实际人数很少时使用该数组,势必造成内存的巨大浪费。
链表是根据实际需要,动态的开辟内存单元。如图4.15所示。
head
1249
1356
1475
1021
1249
A
B
C
D
1356
1475
1021
NULL
图4.15 链表链表中的每一个元素称为“结点”,每个结点包括两部分:一为用户需要用的数据,二为指针,存放下一个结点的地址。
此外每个链表都有一个头节点head和一个表尾,head是访问整个链表的开始,指向第一个实际结点,尾结点的指针为NULL,表示结束。
链表中的“结点”可以不是连续存放的,由上一个元素根据地址找到下一个元素,一环扣一环。
链表结点可用结构体类型定义,成员中必须有一个指针变量,用来指向下一个结点。见下例:
struct student
{
long num;
char name[20];
float score;
struct student *next;
};
注意:指针类型应为结构体类型,因为它指向下一个结点。
4.8.2简单链表
struct student
{
long num;
float score;
struct student *next;
};
main()
{
struct student a,b,c,*head,*p;
a.num=99101; a.score=89;
b.num=99102; b.score=90;
c.num=99103; c.score=85;
head=&a;
a.next=&b;
b.next=&c;
c.next=NULL;
p=head;
do
{
printf("%ld %5.1f\n",p->num,p->score);
p=p->next;
}while(p!=NULL);
}
思考:各个结点如何连成一个链表?p起什么作用? p=p->next; 后p指向谁?
4.8.3动态链表上例中的结点数都是在程序中定义好的,并不是运行程序后临时开辟的,因此称为“静态链表”。
动态链表需要特殊的函数,向系统申请存储空间,如malloc( )函数和calloc()函数,使用完毕后,需使用free()函数释放申请的空间。
【例4.27】编写一个函数建立一个有3名学生数据的单向动态链表。
分析:定义3个指针变量:head、p1、p2,都是结构体student类型。用malloc函数开辟一个个结点。约定如果输入结点数据的学号为0,则表示链表建立完毕。head存放头结点,p1用于指向新开辟的结点,p2用于指向已链接的上一个结点。每建立一个新结点p1=(struct student *)malloc(LEN),就让p2.next=p1; p2=p1;
动态链表建立过程如下:
n=0;
head=NULL;
p1=p2=(struct student *)malloc(LEN);
scanf("%ld%f",&p1->num,& p1->score );
head
p2
p1
(1)
while(p1->num!=0)
{ n=n+1;
if(n==1) head=p1;
else p2->next=p1;
p2=p1;
p1=(struct student *)malloc(LEN);
scanf("%ld%f",&p1->num,&p1->score);
head
p2
p1
9001
89
(2)
p2.next=p1;
head
p2
p1
9001
89
(3)
head
p2
p1
9001
9002
89
78
(4)
p2=p1;
head
p2
p1
9001
9002
89
78
(5)
p2->next=NULL;
head
p2
p1
9001
9002
9003
89
78
86
(6)
head
p2
p1
9001
9002
9003
89
78
86
(7)
head
P2
P1
9001
9002
9003
0
89
78
86
0
(8)
head
P2
P1
9001
9002
9003
0
89
78
86
0
NULL
(9)
图4.16 动态链表的建立该过程可用图4.16表示。
4.8.4输出链表链表结构的实现和使用输出链表:
p=head;
if (head!=NULL)
do
{
printf("%ld %5.1f\n",p->num,p->score);
p=p->next;
}while (p!=NULL);
【例4.28】建立链表、搜索链表、插入结点、删除结点等。
参考程序:
#include <stdlib.h>
#include <stdio.h>
#include <malloc.h>
#include <string.h>
#include <conio.h>
#define N 10
typedef struct node
{
char name[20];
struct node *link;
}stud;
stud * creat(int n) /*建立链表*/
{
stud *p,*h,*s;
int i;
if((h=(stud *)malloc(sizeof(stud)))==NULL)
{
printf("cannot find space!");
exit(0);
}
h->name[0]='\0';
h->link=NULL;
p=h;
for(i=0;i<n;i++)
{
if((s= (stud *) malloc(sizeof(stud)))==NULL)
{
printf("cannot find space!");
exit(0);
}
p->link=s;
printf("please input %d student's name:",i+1);
scanf("%s",s->name);
s->link=NULL;
p=s;
}
return(h);
}
stud * search(stud *h,char *x) /*搜索链表*/
{
stud *p;
char *y;
p=h->link;
while(p!=NULL)
{
y=p->name;
if(strcmp(y,x)==0)
return(p);
else p=p->link;
}
if(p==NULL)
printf("data not find!");
return 0;
}
stud * search2(stud *h,char *x)
{
stud *p,*s;
char *y;
p=h->link;
s=h;
while(p!=NULL)
{
y=p->name;
if(strcmp(y,x)==0)
return(s);
else
{
p=p->link;
s=s->link;
}
}
if(p==NULL)
printf("data not find!");
return 0;
}
void insert(stud *p) /*插入结点*/
{
char stuname[20];
stud *s;
if((s= (stud *) malloc(sizeof(stud)))==NULL)
{
printf("cannot find space!");
exit(0);
}
printf("\nplease input the student's name:");
scanf("%s",stuname);
strcpy(s->name,stuname);
s->link=p->link;
p->link=s;
}
void del(stud *x,stud *y) /*删除结点*/
{
stud *s;
s=y;
x->link=y->link;
free(s);
}
void print(stud *h)
{
stud *p;
p=h->link;
printf("data information:\n");
while(p!=NULL)
{
printf("%s ",&*(p->name));
p=p->link;
}
}
void quit()
{
exit(0);
}
void menu(void)
{
clrscr();
printf("\t\t\t simple linklise realization of c\n");
printf("\t\t|————————————————-----|\n");
printf("\t\t| |\n");
printf("\t\t| [1] create linklist |\n");
printf("\t\t| [2] seach |\n");
printf("\t\t| [3] insert |\n");
printf("\t\t| [4] delete |\n");
printf("\t\t| [5] print |\n");
printf("\t\t| [6] exit |\n");
printf("\t\t| |\n");
printf("\t\t| if no list exist,create first |\n");
printf("\t\t| |\n");
printf("\t\t|————————————————-----|\n");
printf("\t\t please input your choose(1-6):");
}
main()
{
int choose;
stud *head,*searchpoint,*forepoint;
char fullname[20];
while(1)
{
menu();
scanf("%d",&choose);
switch(choose)
{
case 1:head=creat(N);
break;
case 2:printf("input the student's name which you want to find:");
scanf("%s",fullname);
searchpoint=search(head,fullname);
printf("the stud name you want to find is:%s",*&searchpoint->name);
printf("\n push returen to main menu.");
getchar();
getchar();
break;
case 3,printf("input the insert position:");
scanf("%s",fullname);
searchpoint=search(head,fullname);
printf("the stud name you want to find is:%s",*&searchpoint->name);
insert(searchpoint);
print(head);
printf("\npush returen to main menu.");
getchar();getchar();
break;
case 4:print(head);
printf("\ninput the student's name which you want to delete:");
scanf("%s",fullname);
searchpoint=search(head,fullname);
forepoint=search2(head,fullname);
del(forepoint,searchpoint);
break;
case 5:print(head);
printf("\npush returen to main menu.");
getchar();getchar();
break;
case 6:quit();
break;
default:printf("illegal letter!push returen to main menu.。");
clrscr();
menu();
getchar();
}
}
}
习题四填空题数组是一组具有______________的元素组成的有序的数据集合,在内存中按元素的__________进行存储。
在C语言中,数组的下标是从_____开始的。
对于数组a[10],数组名a实际上是代表该数组的__________________。
对数组进行访问时,___________对数组的某一个元素进行单独的访问,而__________________对数组的全部数据进行访问。
若有定义,int a[][3]={1,2,3,4,5,6,7}; 数组a的第一维大小是___________。
若有以下定义,int a[5]; 则a数组中首元素的地址可以表示为_________。
若有定义 int a[10],*p=a; 则*(p+5)表示________________。
当数组名作为函数参数时,是将实参数组的______的传递给形参数组。
有下列初始化语句:static int a[5]={1,2,3};问 a[2]+a[4] 的值为______。
10.写出下列描述的指针定义形式_______。pa是一个指针变量,它指向一维数组,数组有4个元素,元素的类型是整型。
11.二维数组可以看成一个矩阵,二维数组的第一维决定矩阵的_____________,第二维决定矩阵的______________。
12.如果在为数组赋初值时,没有足够的数据赋给数组元素,则对于整型/实型数组未赋值的数组元素值为___________,对于字符数组为___________。
13.如果对二维数组的所有元素都赋初值,则数组的第____维长度可以省略。
14.二维数组在内存中的存储空间也是连续的线性空间。在内存中,数组元素的存储顺序是________________。
15.若二维数组y有m列,则在y[i][j]前的元素个数为______________。
16.若有定义:int a[3][4]={{1,2},{0},{4,6,8,10}};,则初始化后,a[1][2]得到的初值是_______________,a[2][1]得到的初值是__________________。
17.字符串用一维字符数组形式进行存储,它以___________结尾。
18.字符数组str[10]用于存储一个字符串,则该字符串最多可包含____个字符。
19.字符串处理函数包含在______________文件中。
20.存储'A'时占用1个字节,存储"A"时占用______个字节。
21.如果想从键盘获得一个带有空格的字符串,应使用函数________(只填函数名)。
22.请填写下面函数的形参定义_____________。
void input(_____________,int n)
{ int i;
for (i=0;i<n;i++) a[i]=i;
a[3]+=3;
for(i=0;i<n;i++) printf("%4d",a[i]);
}
main()
{ static int i,b[10];
input(b,10); printf("\n");
for(i=0;i<10;i++) printf("%4d",b[i]);
}
23.下面程序段把从终端读入的一行字符作为字符串放在字符数组中,然后输出,请填空。
……
int i;
char s[80],*p;
for(i=0;i<79;i++)
{ s[i]=getchar();
if (s[i]=='\n') break;
}
s[i]=__________________;
p=s;
puts(p);
24.下面程序是用起泡法对数组的各元素进行排序,请填空。
main()
{ int a[10],i,j,t;
for(i=0;i<10;i++) scanf("%d",&a[i]);
for(i=0;i<10;i++)
for(j=0;______________;j++)
if (a[j]>a[j+1])
{ t=a[j];a[j]=a[j+1];a[j+1]=t; }
for(i=0;i<10;i++) printf("a[%d]=%d\n",i,a[i]);
}
25.以下程序可求出所有水仙花数。所谓水仙花数是指一个三位正整数,其各位数字的立方之和等于该正整数。例如:407=4*4*4+0*0*0+7*7*7,请填空。
main()
{ int x,y,z,a[16],m,i=0;
for(m=100;m<=999;m++)
{ x=m/100;
y=m/10-x*10;
z=m%10;
if(m==x*x*x+y*y*y+z*z*z)
{ a[i]=m;
___________________;
}
}
for(x=0;x<i;x++) printf("%6d",a[x]);
}
26.设数组a中的元素均为正整数,以下程序是求a中偶数的个数和平均值。请填空。
main()
{ int a[10]={1,2,3,4,5,6,7,8,9,10};
int k,s,i;
float ave;
for(k=s=i=0;i<10;i++)
{ if(a[i]%2!=0) continue;
s+=a[i]; k++;
}
if(k!=0)
{ ave=________________;
printf("%d,%f\n",k,ave);
}
}
27.以下程序在a数组中查找与x值相同的元素所在的位置。请填空.
main()
{ int a[11],x,i;
printf("Enter 10 integers:\n");
for(i=1;i<=10;i++) scanf("%d",a+i);
printf("Enter x:"); scanf("%d",&x);
*a=x; i=10;
while((x!=*(a+i))&&i>0) i--;
if( ______________) printf("%5d's position is:%4d\n",x,i);
else
printf("%d not been found!\n",x);
}
28.以下程序的运行结果为_________________。
main()
{ int i,j,row,col,min;
int a[3][4]={{1,2,3,4},{9,8,7,6},{-1,-2,0,5}};
min=a[0][0];
for(i=0;i<3;i++)
for(j=0;j<4;j++)
if(a[i][j]<min)
{min=a[i][j];row=i;col=j;}
printf("min=%d,row=%d,col=%d",min,row,col);
}
29.下面程序的运行结果是________。
# include <stdio.h>
int s(char *s)
{ char *p=s;
while (*p) p++;
return (p-s);
}
main()
{ char *a="abded";
int i;
i=s(a);
printf("%d",i);
}
30.写出程序的运行结果______。
#include <stdio.h>
#include <string.h>
fun(char *w,int n)
{ char t,*s1,*s2;
s1=w; s2=w+n-1;
while(s1<s2)
{ t=*s1;*s1=*s2;*s2=t;
s1++;
s2--;
}
}
main()
{ char *p="1234567";
fun(p,strlen(p));
puts(p);
}
31.以下程序的运行结果是_______。
void num()
{ extern int x,y;
int a=15,b=13;
x=a-b;
y=a+b;
}
int x,y;
main()
{ int a=8,b=5;
x=a+b;
y=a-b;
num();
printf("%d,%d",x,y);
}
二、选择题
1.以下对一维数组a的正确说明是_______________。
A、char a(10);  B、int a[ ];
C、int k=5,a[k]; D、char a[ ]={'a','b','c'};
2.若用数组名作为函数调用时的实参,则实际上传递给形参的是_______________。
数组首地址
数组第一个元素的值数组中全部元素的值
数组中元素的个数
3.若有以下定义,int a[5],*p=a; 则对a数组元素地址的正确引用是____。
A.、p+5 B、*a+1 C、&a+1 D、&a[0]
4.若已定义 char s[10]; 则在下面表达式中不表示s[1]的地址的是______。
A、s+1 B、s++ C、&s[0]+1 D、&s[1]
5.以下正确的是____
a[i]等价于*(a+i)
&a[i]等价于*(a+i)
a[i]等价于a+i
a[i]等价于*a+i
6.若使用一维数组名作函数实参,则以下正确的说法是__________。
A、必须在主调函数中说明此数组的大小
B、实参数组类型与形参数组类型可以不匹配
C、在被调函数中,不需要考虑形参数组的大小
D、实参数组名与形参数组名必须一致
7.若有说明语句:int a[2][4];则对a数组元素的正确引用是_____________。
A、a[0][3] B、a[0][4] C、a[2][2] D、a[2][2+1]
8.若有说明语句:int y[ ][4]={0,0}; 则下面叙述不正确的是_____________。
A、数组y的每个元素都可得初值0
B、二维数组y的行数为1
C、该说明等价于int y[ ][4]={0};
D、只有元素y[0][0]和y[0][1]可得到初值0,其余元素均得不到初值0
9.若有说明语句:int a[ ][3]={1,2,3,4,5,6,7,8};,则a数组的行数为___________。
A、3  B、2  C、无确定值  D、1
10.若有变量定义:int i,x[3][4]; 则以下关于x、*x、x[0]、&x[0][0]的正确描述为________。
A、x、*x、x[0]、&x[0][0]均表示元素x[0][0]的地址
B、只有x、x[0]和&x[0][0]表示的是元素x[0][0]的地址
C、只有x[0]和&x[0][0]表示的是元素x[0][0]的地址只有&x[0][0]表示的是元素x[0][0]的地址
11.定义如下变量和数组:int k; int a[3][3]={1,2,3,4,5,6,7,8,9};则下面语句的输出结果是___________。
for (k=1;k<3;k++) printf("%d ",a[k][2-k]);
A、3 5 B、3 6 C、5 7 D、1 3
12.下列数组定义中,正确的是___________。
A、int a[][4]={1,2,3,4,5,6,7,8,9,10,11,12}; B、int a[][]; C、int a[][][5]; D、int a[3][];
13.以下对字符数组的描述中错误的是_____________。
A、字符数组中可以存放字符串;
B、字符数组中的字符串可以整体输入、输出;
C、可以在赋值语句中通过赋值运算符"="对字符数组整体赋值;
D、不可以用关系运算符对字符数组中的字符串进行比较。
14.下面程序段的运行结果是_______________。
char x[5]={'a','b','\0','c','\0'};
printf("%s",x);
A、'a''b' B、ab C、ab└┘c D、abc
15.有两个字符数组a,b,则以下能正确为a,b进行赋值的语句是_____________。
A、gets(a,b);  B、scanf("%c%c",&a,&b);
C、getchar(a); getchar(b); D、gets(a); gets(b);
16.以下描述正确的是________________。
A、两个字符串所包含的字符个数相同时,才能比较字符串
B、字符个数多的字符串比字符个数少的字符串大
C、字符串"STOP└┘"与"STOP"相等
D、字符串"That"小于字符串"The"
17.有以下定义和语句,则输出结果是_______________。
char s[12]="This is a book!"; printf("%d",strlen(s));
A、15 B、16 C、12 D、14
18.为了判断两个字符串s1和s2是否相等,应使用
A、if(strcpy(s1,s2)) B、if(s1=s2) C、if(strcmp(s1,s2)==0) D、if(s1==s2)
19.有下面的程序段,则______________。
char a[3],b[]="China";
a=b;
printf("%s",a);
A、运行后将输出China B、运行后将输出Ch
C、运行后将输出Chi D、编译出错
20.下面对s的初始化不正确的是_____________。
char s[]="abc";
char s[5]={'a','b','c'};
char s[5]=" "
char s[5]="abcdef"
21.以下程序的运行结果是______________。
main( )
{ char str1[ ]="abcdef";
 ff(str1);
 printf("str[ ]=%s\n",str1);
}
ff(char str1[ ])
{ int x,y;
 for (x=y=0; str1[x]!='\0'; x++)
if (str1[x]!='c')
str1[y++]=str1[x];
 str1[y]='\0';
}
A、str[ ]=abdef B、str[ ]=abcdef
C、str[ ]=a D、程序中存在语法错误
22.执行下面语句的输出为_________________。
char s1[]="12345",* ptr;
ptr=s1;
printf("%c",*(ptr+3));
A、字符'2' B、字符'4' C、字符'4'的地址 D、不确定
23.以下程序段的运行结果为_______________。
static char a[]="Program";
char *ptr=&a[1];
for (ptr=a;ptr<a+7;ptr+=2) putchar(*ptr);
A、Program B、Porm C、Por D、有语法错误
24.下面程序的输出是_____________。_
char s[]="ABCD";
main ()
{ char *p;
for(p=s;p<s+4;p++) printf(%s",p); }
A、ABCDBCDCDD B、ABCD C、DCBA D、ABCDABCABA
25.下面程序段的运行结果是____________。
char *s="abcde";
s+=1;
printf("%d",s);
A、bcde B、字符'b'的地址
C、字符'c'的地址  D、字符'b'的ASCII值
26.设有说明语句:char *str="\t\'c\\Language\n";则指针str所指字符串的长度为________。
A、13 B、15 C、17 D、说明语句不合法
27.以下程序段的运行结果是_____________。
char a[ ]="program",*p;
p=a;
while (*p!='g')
 { printf("%c",*p-32);
p++;
}
A、PROgram B、PROGRAM
C、PRO D、proGRAM
A、10 B、4 C、3 D、5
三、程序题编写函数,将一个无符号十进制整数转化为二进制形式,保存在形参数组中(主函数中输出其二进制形式)。
设有一个已按从大至小顺序排好的数列存放在一维数组中,现输入一个数,仍按原来的排序规律将其插入到数组中,试编程。
输出能被3整除且至少有一位是5的两位数,要求在函数fun中输出所有这样的数,在主函数中完成相应的输入并打印出这些数的个数。请编程。
用指针访问一个一维数组。包括输入各数组元素的值,将下标为3的倍数的元素置0(包括0下标),最后输出数组各元素值。
求出一个3*3矩阵的主对角线元素之和。
将二维数组a中每个元素向右移一列,最右一列换到最左一列,移后的数组存到另一个二维数组b中,并按矩阵形式输出a和b。请编程。例如:
 
检查一个二维数组是否对称(即对所有的a[i][j]=a[j][i])。
找出一个二维数组中的鞍点,即该位置上的元素在该行上最大,在该列上最小。也可能没有鞍点。
将一个5×5的矩阵中最大的元素放在中心,4个角分别放4个最小的元素(顺序为从左到右,从上到下顺序依次从小到大存放),写一函数实现之。用main函数调用。
10.编写程序,打印出九九乘法表:
1×1=1
2×1=2 2×2=4
3×1=3 3×2=6 3×3=9
4×1=4 4×2=8 4×3=12 4×4=16
5×1=5 5×2=10 5×3=15 5×4=20 5×5=25
6×1=6 6×2=12 6×3=18 6×4=24 6×5=30 6×6=36
7×1=7 7×2=14 7×3=21 7×4=28 7×5=35 7×6=42 7×7=49
8×1=8 8×2=16 8×3=24 8×4=32 8×5=40 8×6=48 8×7=56 8×8=64
9×1=9 9×2=18 9×3=27 9×4=36 9×5=45 9×6=54 9×7=63 9×8=72 9×9=81
11.有n个整数,使前面各数顺序向后移m个位置,最后m个数变成最前面m个数。写一函数实现以上功能,在主函数中输入n个整数和输出调整后的n个数。要求用指针实现。
12.编写一个函数,将一个长整型数据转换成字符串,如输入长整数824734,应输出字符串“824734”。要求使用字符串作为函数参数。
13.有10个字符串,编程实现在每个字符串中找出最大字符,按一一对应的顺序放在一维数组a中,即第i个字符串中的最大字符放入a[i]中。最后输出每个字符串中的最大字符。
14.输入若干学生姓名,按字母排序后输出。
15.请编写一个函数 int func(char *str,char ch),它的功能是:求出str字符串中指定字符ch的个数,并返回此值。例如:若输入字符串str="abEF123112",ch='1',则输出3。
int func(char *str,char ch) { }
main()
{char s[81],c;
clrscr();
printf("\nPlease input a string:");
gets(s);
printf("\nPlease input a char:");
c=getchar();
printf("\nThe number of the char is,%d\n",func(s,c));
}
16.请编写一个函数void func(char a[],char b[],int n),其功能是:删除一个字符串中指定位置以后的所有字符(包含指定位置)。其中,a存放原字符串,b存放删除后的字符串。
例如,输入一个字符串"World",然后输入3,则调用该函数后的结果为"Wor"。
17.编写函数,求一个字符串中的最长单词,将最长单词保存在形参字符数组中,在主函数中输出。
18.设有一行文字,现要求从其中删去某个指定的字符(如:输入a,表示将从该行文字中删去所有是a的字符),要求该行文字和待删的字符均从键盘上输入,请编写程序。
19.设有一行文字,现要求将每一个单词中的第一个字母改成大写字母(原来已经是大写字母的不变)。
20.编写程序判断输入的字符串是否是“回文”,(顺读和倒读都一样的字符串称“回文”,如:level)。
第五章 复杂构造数据类型前面我们学了对于同种类型的数据可以用数组来描述。但在解决实际问题过程中,还会遇到很多在描述同一个对象时,数据的类型不一致的情况,如学生个人信息,包括姓名、学号、年龄等,就包含了字符数组和整型数据。本章将讨论这种复杂数据类型的构造方法及其使用。
5.1结构体表5-1和表5-2中的一行表示一名学生或一个职工的相关信息,这样的一行信息又称为一条记录,在进行信息处理时,通常都是以一条记录为单位进行的。
我们可发现,在一条记录中既有整型数据(学号、编号等),又有字符串(姓名),像这样的数据是由几种不同类型的数据构成,是不能用前面学过的数组来表示的,因为数组的各个元素都是相同的数据类型。按照以前所学的数据类型是无法处理这种复杂数据的。
表5-1 学生成绩表学号
姓名
语文
数学
英语
总分
1001
周炎
78
62
67
207
1002
王林
87
73
86
246
1003
丁鹏
65
85
93
243
1004
李娟
68
82
81
231
…
…
…
…
…
…
表5-2 职工工资表编号
姓名
基本工资
奖金
提成
实发工资
1
刘俊
500
100
400
1000
2
王军
500
100
200
800
3
丁建
500
100
500
1100
4
李文
500
200
700
1400
…
…
…
…
…
…
为了解决这种问题,C语言将几种不同类型的数据组合到一起,这就是我们下面要介绍的结构体类型。
(建议将下例中的工人改为职工,这样更符合目前的习惯叫法。如果麻烦不改也可。)
5.1.1结构体的引出及使用一.结构体的引出
【例5.1】工人工资管理系统需要每个工人的下列数据:编号、姓名、性别、年龄、班组、基本工资、奖金、保险,在工人工资管理系统中,求某个工人的实发工资,并打印该名工人的全部信息。
分析:每名工人共有8项基本信息,再根据基本工资、奖金、保险计算出第9项实发工资,因此每名工人总计9项参数。由于各个参数类型不同,我们无法将这些参数定义为数组,只能分别定义9个变量。编号num、年龄age定义为整型,姓名name、性别sex、班组branch定义为字符数组(实际作为字符串考虑),基本工资、奖金、保险、实发工资都定义为实型。在这种情况下,使用结构体类型最为方便。
结构体的定义格式,struct 结构类型名
{
类型名1 成员名表1;
类型名2 成员名表2;
……
类型名n 成员名表n;
};
将每个工人的信息定义为一个结构体类型,设现在要定义的具体类型名是worker,则worker类型的定义语句应为:
struct worker
{
int num;
char name[20];
char sex;
int age;
char branch[20];
float pay;
float bonus;
float insurance;
float realpay;
};
要求某个工人的实发工资和工人的全部信息,可先定义结构体类型struct worker的变量w,定义语句:struct worker w;
这里,struct worker是一个类型,结构体类型,类似于以前用过的int和char等类型声明符。不过int和char是系统给出的,而struct worker是程序员自己定制的。在使用时,引用一个结构体类型的变量,实际上是引用该变量以及其中的成员。对上述定义的变量w来说,它的num、name、sex、age等九项就是w的成员。引用成员时,要在变量名与成员名中间加一个小数点,如:w.num=1001。
参考程序
main()
{
struct worker
{
int num;
char name[20];
char sex;
int age;
char branch[20];
float pay;
float bonus;
float insurance;
float realpay;
}w;
printf(“\nPlease input worker’s num,name,sex,age,branch,pay,bonus,
insurance:”);
scanf(“%d%s%c%d%s%f%f%f”,&w.num,w.name,&w.sex,&w.age,w.branch,
&w.pay,&w.bonus,&w.insurance); /*输入工人的各项信息*/
w.realpay=w.pay+w.bonus-w.insurance; /*计算该工人的实发工资*/
printf(“%d\t%s\t%c\t%d\t%s\t%f\t%f\t%f\t%f\n”,w.num,w.name,w.sex,w.age,
w.branch,w.pay,w.bonus,w.insurance,w.realpay); /*打印该名工人的全部信息*/
}
二.结构体的定义及使用
1.结构体类型定义在解决实际问题时,有时需要将多个不同数据类型的数据组合在一起表达一个整体的信息。
例如:描述一个学生的完整信息时,有学号、姓名、年龄、成绩、家庭地址等项,如表5-3。这些项目之间是彼此联系的,应组织定义成一个组合项,统一表示和使用。
表5-3 学生信息表学号
姓名
性别
年龄
成绩
家庭地址
int num
char name[8]
char sex
int age
float score
char ddr[20]
例如,某一个学生的具体信息表示:
9001
李华
女
20
86
济南
这种由若干个不同类型的数据项组成的组合类型,在C语言中叫做结构体类型,相当于其它语言中描述的“记录”。
结构体是一种复合的数据类型,它允许用其它数据类型构成一个结构类型,而一个结构类型变量内的所有数据可以作为一个整体进行处理。
同数组类似,一个结构体也是若干数据项集合,但与数组不同,数组中的所有元素都只能是同一类型,而结构体中的数据项可以是不同的类型。
结构体类型用struct关键字定义,花括号里的每一项称为结构体成员,结构体成员的类型可以是普通的数据类型(如int、char等),也可以是数组、指针或已经定义的结构体等任意的数据类型。
需要注意,结构体类型的定义并没有在内存中为其分配空间,仅仅定义了数据的组织形式,创立了一种数据类型,是对数据的一种抽象,可以理解为以前学过的int、char等基本数据类型,只有在定义了结构体类型的变量后,才会在内存中为该变量分配空间。在为结构体变量分配存储空间时,每个结构体变量所占存储空间大小为其成员所占存储空间的总和。
例如:上面定义的结构体类型struct worker,有9个成员。其中,num、age是整型,分别占用2个字节;name[20]、branch[20]是字符类型数组,都有20个数组元素,分别占20个字节;sex是字符类型,占1个字节;pay、bonus、insurance、realpay都是实数类型,分别占4个字节。那么,这个结构体类型的变量共占2+20+1+2+20+4+4+4+4=61个字节。
结构体类型在使用之前应先定义其类型结构,然后再定义该类型变量,才能使用。
2.定义结构体类型变量的方法注意前面定义的worker同int、char等一样只是类型的名字,不是结构体变量,不能直接使用,需由此类型继续定义结构体变量才能使用,
步骤:先定义一个结构体类型,再定义变量。
struct 结构体类型名 变量名列表;
注意:关键字struct 与结构体类型标识符一起使用。
例,struct worker w1,w2,w[50];
定义了结构体变量后,系统为之分配内存单元,结构体的各成员在内存中是按顺序连续存放的,所以结构体变量在内存中占据的字节数是各个成员的长度和。
可用求类型长度运算符求出
sizeof(类型名)
如:sizeof(int)为2
sizeof(struct worker)为61;
定义结构体变量必须指明具体的类型名,不同于定义int等标准数据类型变量,因为不同的结构体类型具有不同的结构组成。
还可以用如下方法定义结构体变量,定义类型的同时定义结构体变量,或者直接定义结构体类型变量。
3.结构体变量的引用一般来说,在程序设计中不直接引用结构体变量,而是引用结构体变量的某个成员变量。
⑴ 成员的引用形式结构体变量名.成员名例,struct birth
{
int month;
int day;
int year;
};
struct student
{
int num;
char name[10];
char sex;
int age;
struct birth birthday;
float score;
char addr[20];
} stu1;
引用成员 stu1.num=9002;
gets(stu1.name);
stu1.sex='m' ;
printf(“%d,%d”,stu1.num,stu1.name);
⑵ 结构体变量使用说明
① 结构体变量通常不能整体使用,不能整体输入、输出,只能对单个成员分别引用。但当结构体变量作为函数参数或赋初值时,可以整体使用;或者两个相同类型的结构体变量,如果一个已经赋值,可以对另外一个整体赋值。
② 如果成员本身又属于一个结构体类型,则这个成员也不能整体赋值,要用若干各成员运算符引用,如前面的birthday成员本身又是birth类型的结构体变量,则需
stu1.birthday.month=4;
stu1.birthday.day=5;
stu1.birthday.year=1998;
③ 可以引用结构体变量成员的地址,也可以引用结构体变量的地址,但要区分清楚。如:
printf(“%x”,&stu1.num);
printf(“%x”,&stu1);
4.结构体变量的初始化可以在定义结构体变量的同时,对其初始化例:struct student
{ long int num;
char name[20];
char sex;
char addr[20];
}a={9001,"曹名",'m',"123 北京路" };
5.1.2结构体数组的引出及使用一.结构体数组的引出
【例5.2】一个工人工资管理系统,其内容包括:编号、姓名、性别、年龄、班组、基本工资、奖金、保险。共有10名工人,求每名工人的实发工资,并打印所有工人的全部信息。
分析:一个工人的信息包括:编号、姓名、性别、年龄、班组、基本工资、奖金、保险。由于各个部分的类型不一致,所以我们把一个工人的信息定义成了结构体类型struct worker,现在有10名工人,每个工人都是结构体struct worker类型,即每个工人的属性类型是一样的,而数组是具有相同属性的变量的集合,因此,我们可以考虑把结构体类型变量定义成数组元素,表示一组相同类型的结构体变量,该数组称为结构体数组,即数组中每一个元素都是结构体类型。
该题中有10个工人,每个工人被定义为结构体struct worker类型,可作如下定义:
struct worker w[10];
或者在定义结构体类型的同时定义结构体数组,即:
struct worker
{
int num;
char name[20];
char sex;
int age;
char branch[20];
float pay;
float bonus;
float insurance;
float realpay;
}w[10];
对第i个结构体数组元素,即第i个工人的具体信息引用可以用表达式引用下标变量成员,如:
w[i].num。
N-S图如图5.1。
定义变量i、k,结构数组w[10]
i=0;i<10;i++
输入数组元素的各个成员
w[i].realpay=w[i].pay+w[i].bonus-w[i].insurance
输出所有元素
图5.1 N-S图参考程序:
struct worker
{
int num;
char name[20];
char sex;
int age;
char branch[20];
float pay;
float bonus;
float insurance;
float realpay;
};
main()
{
int i;
struct worker w[10];
for(i=0;i<10;i++)
{
printf(“\nPlease input %d worker’s num,name,sex,age,branch,pay,bonus,insurance:”,i+1);
scanf(“%d%s%c%d%s%f%f%f”,&w[i].num,w[i].name,&w[i].sex,&w[i].age,w[i].branch,&w[i].pay,&w[i].bonus,&w[i].insurance);
/*输入工人i的各项信息*/
w[i].realpay=w[i].pay+w[i].bonus-w[i].insurance;
/*计算工人i的实发工资*/
printf(“\nThe %d worker’s num,name,sex,age,branch,pay,bonus,insurance,realpay are:”,i+1);
printf(“%d\t%s\t%c\t%d\t%s\t%f\t%f\t%f\t%f\n”,w[i].num,w[i].name,w[i].sex,w[i].age,w[i].branch,w[i].pay,w[i].bonus,w[i].insurance,w[i].realpay);
/*打印工人i的全部信息*/
}
}
一个结构体变量只能存储一条记录,对于多条相关记录,可以使用结构体数组的方式来完成。可以像定义普通数组一样定义结构体数组,结构体数组每一个元素都是一个结构体变量。同数组一样,结构体数组的名称也是一个常量,表示该结构体数组在内存中存放首地址。
二.结构体数组的定义及使用在结构体数组中每一个数组元素都是一个结构体类型的数据,它们都分别包括各个成员项,例如一个班级的40名同学信息组成的数组,每一个同学的数据都是一个结构体类型,分别由不同类型的数据项组成。
1.定义结构体数组定义方法和定义结构体变量相仿。
例,struct student
{
int num;
char name[20];
char sex;
float score;
char addr[20];
};
struct student stu[40];
例,struct
{
int num;
char name[20];
char sex;
float score;
char addr[20];
}stu[40];
结构体数组各元素在内存中是连续存放的,各元素的成员也是按顺存放。如图5.2所示。
num
name
sex
score
addr
stu[0]
10101
Li Lin
M
87.5
103 Beijing
stu[1]
10102
Zu Feng
F
98
130 Shanghai
stu[2]
10103
Wang Mi
M
78
1o4 Jinan
……
……
图5.2学生具体信息图
2.结构体数组的初始化例,struct student
{
int num;
char name[20];
char sex;
float score;
char addr[20];
}stu[3]={{10101,”Li Lin”,’M’,87.5,”103 Beijing”},
{10102,”Zu Feng”,’F’,98,”130 Shanghai”},
{10103,”Wang Mi”,’M’,78,”104 Jinan”}};
赋初值时也可以不指定元素的个数,系统根据初值个数确定元素个数。
5.1.3结构体程序举例
【例5.3】定义一个结构体变量(包括年、月、日)。计算该日在本年中是第几天,注意闰年问题。
分析:计算某天在一年中的第几天,需要考虑该天所在的月份,用日期加上所在月份之前的月份的天数,即可算得天数。如输入的是2月15日,则应该是本年的第(31+28+15=74)天,若是闰年,而且需要计算的日期是在2月份之后,则需要在算得的天数基础上再加一天。由于是多分支情况,可用switch语句实现。N-S图如图5.3。
输入年、月、日
1
days=date.day
2
days=date.day+31
3
days=date.day+59
4
days=date.day+90
5
days=date.day+120
月
6
days=date.day+151
7
days=date.day+181
8
days=date.day+212
9
days=date.day+243
10
days=date.day+273
11
days=date.day+304
12
days=date.day+334
闰年&&month>=3
T
F
days=days+1
输出结果
图5.3 例5.3的N-S图结构体变量date中的成员对应于输入的年、月、日。Days为天数。
参考程序:
struct
{
int year;
int month;
int day;
}date;
main()
{
int days;
printf(“Input year,month,day:”);
scanf(“%d,%d,%d”,&date.year,&date.month,&date.day);
switch(date.month)
{
case 1:days=date.day; break;
case 2:days=date.day+31; break;
case 3:days=date.day+59; break;
case 4:days=date.day+90; break;
case 5:days=date.day+120; break;
case 6:days=date.day+151; break;
case 7:days=date.day+181; break;
case 8:days=date.day+212; break;
case 9:days=date.day+243; break;
case 10:days=date.day+273; break;
case 11:days=date.day+304; break;
case 12:days=date.day+334; break;
}
if((date.year%4==0&&date.year%100!=0||date.year%400==0)&&date.month>=3) day+=1;
printf(“\n%d/%d is the %dth day in %d.”,date.month,date.day,days,date.year);
}
运行结果:
Input year,month,day:2000,10,1↓
10/1 is the 275th day in 2000.
【例5.4】对候选人得票进行统计的程序。假设有3个候选人,每次输入一个得票的候选人的名字,最后输出各人的得票结果。假设共有10人投票。
分析:定义一个结构数组,包含3个元素,表示有3个候选人。每个元素有两个成员,一个是名字,一个是得票数,得票数初始化为0。根据输入的名字进行判断,输入是谁的名字,就给哪个元素的得票数成员加1。全部输入完成后,输出结构数组的每个元素的每个成员。
参考程序
#include <string.h>
struct person
{
char name[20];
int count;
}
main()
{
struct person candidate[3]={{“Li”,0},{“Zhang”,0},{“Fun”,0}};
/*定义结构数组并赋初值*/
int i,j;
char cname[20]; /*定义字符数组,存放键盘输入的候选人名字*/
for(i=0;i<10;i++) /*变量i控制外循环共10此,表示10张选票*/
{
scanf(“%s”,cname); /*读入第i张选票上所选举的人的名字*/
for(j=0;j<3;j++)
if(strcmp(cname,candidate[j].name)==0)
candidate[j].count++; /*选票上的名字与候选人名比较,相等者得票数加1*/
}
printf(“\n”);
for(i=0;i<3;i++) printf(“%5s:%d\n”,candidate[i].name,candidate[i].count);
}
5.1.4结构体与指针结构体类型指的是将多个不同的数据类型组合在一起表达一个整体的信息。结构体类型中含有多个成员项,每个成员项的数据类型可能是不同的。结构体类型是我们自己定义的数据类型,使用时同样要定义结构体变量。定义了结构体变量后,系统为之分配内存单元,结构体的各成员在内存中是按顺序连续存放的,所以结构体变量在内存中占据的字节数是各个成员的长度和。因此找到结构体第一个成员的地址,根据成员之间存放的连续性,我们就可以找到该结构体的所有成员,即访问整个结构体。根据地址的概念我们可以定义结构体变量指针。
结构体变量的指针就是该变量所占据的内存段的起始地址。定义一个指针变量,指向一个结构体变量,此时该指针变量的值就是结构体变量的起始地址。可以用此指针来使用该结构体变量。
一.指向结构体变量的指针定义了结构体变量的指针后,就可以将结构体变量的地址,即变量的起始地址赋值给指针变量,则根据以前指针的定义,*p就相当于结构体的变量。在进行成员引用时,可以用“(*p).”来表示,这里要注意,( )不能省略,因为成员运算符优先级最高。例如:
struct student
{
long num;
char name[20];
char sex;
float score;
} stu1,*p;
p=&stu1;
(*p).num=89101; /*相当于:stu1.num=89101;*/
strcpy((*p).name,"Li Lin"); /*相当于,strcpy(stu1.name,,Li Lin");*/
(*p).sex='M'; /*相当于:stu1.sex='M'; */
(*p).score=89; /*相当于:stu1.score=89; */
为了使用方便和直观,可以用指向运算符p->num来代替(*p).num(指向运算符优先级也是最高的)。因此有三种引用结构体成员的方法:
⑴ 结构体变量,成员名
⑵ (*p),成员名
⑶ p->成员名分析以下几种运算符:
p->n
p->n++
++p->n
*p.num(错误!)应改为:(*p).num
这样,上例也可写为:
struct student
{
long num;
char name[20];
char sex;
float score;
} stu1,*p;
p=&stu1;
p->num=89101;
strcpy(p->name,"Li Lin");
p->sex='M';
p->score=89;
二.指向结构体数组的指针结构体数组的每一个数组元素都是一个结构体变量,结构体变量在内存中是按顺序存放的,而数组元素在内存中也是按顺序存放的,即结构体数组在内存中存放时,先存放第一个数组元素的结构体变量的第一个成员,依次存放第一个数组元素代表的结构体变量的各个成员,接着再存放第二个数组元素代表的结构体变量的各个成员,以此类推。因此,只要我们知道结构体数组的首地址,就可以依次找到结构体数组中每一个结构体变量的每个成员。根据指针的定义,我们也可以定义指向结构体数组的指针,即指向结构体数组的首地址。例如:
struct student
{
int num;
char name[20];
char sex;
float score;
};
struct student stu[3],*p;
……
for(p=stu;p<stu+3;p++) printf(“%d,%s,%c,%f”,p->num,p->name,p->sex,p->score);
思考:上例中的指针p++后,p所增加的值是多少?
随着p指针指向的改变,以下结构体成员的引用所代表的信息也发生了变化,见图5.4。
p->num; p->name; p->sex; p->score;
10101
Li Lin
M
87.5
10102
Zu Feng
F
98
10103
Wang Mi
M
78
图5.4 指向结构体数组的指针注意:p是指向结构体数组的元素的,不能用来指向数组元素中的某一成员,如p=&stu[1].num 是错误的。
可以采用强制类型转换的方法,先将成员的地址转换成p的类型,如:
p=(struct student *)stu[1].num;但通常不这样使用。
对指针变量进行赋值时,写成p=&stu; 是不正确的。数组名本身就代表该数组的首地址,因此不能使用地址运算符&,应直接写成p=stu;
三.用结构体变量和其指针作函数参数结构体变量也是一种数据类型变量,因此也可以作为函数的参数。将一个结构体变量的值传递给一个函数有3个方法:
(1)用结构体类型作参数。采用的是“值传递”的方式。调用时形参要占用内存单元,如果结构体复杂、规模大,其在时间和空间上的开销很大。而且值传递是单向的,使用不便。
(2)用指向结构体变量的指针作实参,将结构体变量(或数组)的地址传递给形参。这种方法传递地址,既节省空间又可以双向传递值。
(3)用结构体变量的成员分别作参数,用法和普通变量作实参是一样的,属于“值传递”。此时应当注意形参与实参的类型、顺序、个数等要保持一致。此种用法不多见。
【例5.5】有一个结构体变量stu,内含学生学号、姓名和3门课的成绩,要求在main函数中赋值,在另一函数print中将它们打印输出。
根据题意,可定义如下结构体:
struct student
{
int num; /*学号*/
char name[20]; /*姓名*/
float score[3]; /*三门课成绩*/
};
定义一个结构体变量:struct student stu; 在进行参数传递时,可用该结构体变量作为参数,即把结构体变量的值传递给形参。
参考程序:
#include <string.h>
struct student
{
int num;
char name[20];
float score[3];
};
void print(struct student stu)
{
printf("%d\n%s\n%f\n%f\n%f\n",stu.num,stu.name,stu.score[0],
stu.score[1],stu.score[2]);
printf("\n");
}
main()
{
struct student stu;
stu.num=12345;
strcpy(stu.name,"Li Li");
stu.score[0]=67.5;
stu.score[1]=89;
stu.score[2]=78;
print(stu);
}
将上题改用指向结构体变量的指针作实参。程序如下:
参考程序
#include <string.h>
struct student
{
int num;
char name[20];
float score[3];
}stu={12345,"Li Li",67.5,89,78};
main()
{
void print(struct student *p);
print(&stu); /*注意与上题的区别!*/
}
void print(struct student *p)
{
printf("%d\n%s\n%f\n%f\n%f\n",
p->num,p->name,p->score[0],p->score[1],p->score[2]);
printf("\n");
}
5.2共用体在某些实际问题中,有时候会发现所定义的两个结构体类型中只有一个或很少数的成员不同,其它大部分成员都是一样的,这样如果使用两类结构,会发现有大部分的程序代码是重复的。那么,能否设计一种新数据类型,使属性相同的成员在同一结构里,而属性不同的成员随机可用?C语言提供了一种新的结构类型——共用体类型,正好可以解决这个问题。
5.2.1共用体的引出
【例5.6】有一个学校若干人员的数据,其中有教师、职工和学生。教师的信息包括姓名、性别、年龄、职业、职称,职工的信息包括姓名、性别、年龄、职业、职位,学生的信息包括姓名、性别、年龄、职业、年级。要求输入若干人员的信息并输出。
分析:在每一个结构体变量中,所有的数据成员都要单独占用一个存储空间。但是,有些问题中,并不需要所有的数据成员同时出现。例如一个学校人员的通用管理程序中,要使用下列数据:
姓名(name)。
性别(sex)。
年龄(age)。
职业(occupation)。
级别(rank):
学生(student):grade(取值:1,2,3,4);
教师(teacher):title(取值:教授、副教授、讲师、助教);
职工(worder):post(校长、处长、科长等)。
其中,name、sex、age、occupation等4项对学生和教师职工是一样的,但是数据rank则因学生和教师职工而不同。学生的rank指年级(grade),教师的rank指职称(title),职员的rank指职位或职务(post)。因而,grade、title和post不需要同时存储,当occupation为student时,使用grade,取值范围为整数1,2,3,4;当occupation为teacher时,使用title,取值为字符串;当occupation为worker,使用post,取值为字符串。也就是说,grade、title和post虽类型不同,但它们不同时出现。出现的时间是根据occupation的条件来判断。
C语言为解决这类问题,提供了一种特别的数据类型:union(共用体)。顾名思义,它能提供多个数据共用同一个存储空间的存储。即上述grade、title和post可以共用一个存储空间。
共用体的定义
union 共用体名
{
类型名1 成员名组1;
类型名2 成员名组2;
……
类型名n 成员名组n;
}变量表列;
该题中学生的年级、教师的职称和职工的职位虽然类型不同,但它们不同时出现。出现的时间根据occupation的条件来判断。因此让它们共用同一个存储空间,用共用体类型来定义:
union rnk
{
int grade;
char title[10];
char post[16];
};
在某一时刻,只有一个共用体成员起作用。而对于姓名、性别、年龄等这些信息,都属于一个对象,或是学生或是教师或是职工,因此,可以用结构体类型来进行定义。
参考程序:
#include,stdio.h”
#define N 10
union rnk
{
int grade; /*学生的年级信息*/
char title[10]; /*教师的职称信息*/
char post[16]; /*职工的职务信息*/
};
struct persn
{
char name[20];
char sex;
int age;
char occp;
union rnk rank;
};
int main()
{
struct persn person[N];
for(i=0;i<N;i++)
{
printf(“\nPlease input the %d people’s name,sex,age,occupation:”,i);
scanf(“%s %c %d %c”,person[i].name,&person[i].sex,
&person[i].age,&person[i].occp);
switch(person[i].occp)
{
case 's',/*职业为学生(s)*/
scanf(“%d”,&person[i].rank.grade);
printf(“\n%s %c %d %c,,person[i].name,person[i].sex,
person[i].age,person[i].occp);
printf(“%d”,person[i].rank.grade);
break;
case 't',/*职业为教师(t)*/
scanf(“%s”,person[i].rank.title);
printf(“\n%s %c %d %c,,person[i].name,person[i].sex,
person[i].age,person[i].occp);
printf(“%s”,person[i].rank.title);
break;
case 'w',/*职业为职工(w)*/
scanf(“%s”,person[i].rank.post);
printf(“\n%s %c %d %c,,person[i].name,person[i].sex,
person[i].age,person[i].occp);
printf(“%s”,person[i].rank.post);
break;
}
}
}
共用体类型也是用户自定义的一种构造类型,和结构类型一样,也是由不同类型、固定个数成员组成的。但是结构体的成员分别占用不同的内存单元,是同时存在的;而共用体的所有成员共同占用同一段内存,某一时刻只能有某一个成员存在,其他成员是不存在的。
由于共用体的所有成员共占同一段内存空间。整个共用体所占内存空间的大小,等于需要占用内存空间最多的那个成员所占的空间大小,这样就可以保证所有的共用体成员都能使用该段内存空间。
例如:上面定义的共用体类型union rnk,有三个成员。其中,grade是整型,占用2个字节;title[10]是字符类型数组,有10个数组元素,占10个字节;post[16] 也是字符类型数组,有16个数组元素,占16个字节。那么,这个共用体类型的变量共占16个字节。
由于共用体成员共用一片内存区,因此在使用共用体变量时,在某一瞬间只能存放其中一个成员;或者说,某一瞬间只有一个成员其作用。表现出来是最后一次存放的成员占用内存。因此,引用共用体变量时,应十分注意当前存放在共用体变量中的究竟是哪一个成员。
5.2.2共用体的定义及使用一.共用体的定义所谓“共用体”类型,是指使几个不同类型的变量共同占用同一段内存单元。它又称为联合,是与结构体相类似的一种数据类型。
例:
union data
{ int i;
char ch;
float f;
}aa,bb,cc;
与结构体不同的是,构成结构体的各成员有自己独立的存储空间,结构体的大小为所有成员所占存储空间的总和。而共用体的各个成员拥有共同的存储空间,在定义时为共用体所分配的内存大小为其最大成员所需的存储空间的大小。
共用体类型的变量声明形式也有3种,同结构体类型。
共用体变量所占的内存空间:共用体变量所占内存的长度等于最长的成员的长度,而不是各成员的长度之和,这一点不同于结构体。
例如前面的例子data类型的变量aa,占据的内存空间为4个字节,而不是2+1+4=7个字节。
二.共用体变量的引用共用体变量只能引用它的成员,不能引用共用体变量本身。形式如下:
共用体变量.成员名
例如前例中的变量aa,可以引用它的成员
aa.i=56;
aa.f=123.6432;
aa.ch=‘A’;
三.共用体类型数据的特点
⑴ 每一瞬时只能存放其中的一个成员,而不是同时存放几种,即其它成员不起作用。
⑵ 由于共用体的成员占有共同的存储空间,存入新成员后,原来的数据被覆盖,因而共用体中的数据始终是最后一次修改成员后的数据。
如上例中的变量aa 只有最后一个成员值aa.ch='A'是有效的。
⑶ 共用体变量的地址和它的成员地址都是同一地址。
即&aa和&aa.i、&aa.ch、&aa.f的起始地址都是一样的。
⑷ 共用体变量不能初始化,也不能对变量名整体赋值,不能引用变量名来输出一个值,只能引用它的某个成员。
⑸ 共用体变量不能作为函数的参数,也不能作为函数返回值。但可以使用指向共用体变量的指针。
⑹ 共用体类型可以出现在结构体中,共用体成员也可以是结构体类型。
⑺ 可以定义共用体数组。
5.3枚举类型
5.3.1枚举类型的引出在实际问题中,有时候对某些数据的取值用有意义的名字来表示更为直观,例如月份、星期,用相应的英文单词来表示比用整型和字符型表示更一目了然。在C语言中,用枚举类型来定义一些具有赋值范围的变量。
【例5.7】程序中提示输入月份数,然后根据输入显示该月的天数。
分析:由于月份是个固定的概念,即一年固定只有12个月,而根据月份的情况,每个月份的天数也是固定的,最多31天,因此我们就可以把这些值用有限个常量来叙述。可以用枚举类型来表示,将变量所能赋的值一一列举出来,给出一个具体的范围。
在本题中,我们可以用枚举来定义一年中的12个月,定义如下:
enum month{Jan=1,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec};
在枚举类型定义后,枚举类型的标识符在程序中代表其后的常数,依次代表0、1、2、……,依次递增。本题中我们可以让第一个一月份Jan的值赋为1,即Jan就代表值1,只是我们在使用时用Jan来表示更容易理解。后面的值依次增1,即Feb的值为2,Mar的值为3,以此类推。
要求每个月份的天数,可对月份进行分类,1、3、5、7、8、10、12都是31天可分为一类,因为本题中只单纯考虑月份与天数的对应关系,所以可不考虑润年的情况,认为2月份为28天,为一类。而4,6,9,11月都是30天分为第三类。因为是根据输入的月份来判断属于哪一类,可考虑使用多分支选择语句swith-case来实现,switch后为输入的月份,输入1就对应枚举类型中的Jan,输入2对应Feb,依此类推。
参考程序:
#include,stdio.h”
enum month{Jan=1,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec};
/*定义枚举类型*/
main()
{
enum month m;
int n;
printf(,Month and Day\n”);
printf(“Please input the month(1-12):”); /*输入要计算的月份*/
scanf(“%d”,&m);
switch(m)
{
case Jan,/*1,3,5,7,8,10,12月都是31天*/
case Mar:
case May:
case Jul:
case Aug:
case Oct:
case Dec:
n=31;
break;
case Feb,/*不考虑润年时,2月为28天*/
n=28;
break;
case Apr,/*4,6,9,11月都是30天*/
case Jun:
case Sep:
case Nov:
n=30;
break;
default:
printf(“Input error!The month must be from 1 to 12!\n”);
return;
}
printf(“The %d month have %d days\n”,m,n);
}
在定义枚举时,由于各元素默认是从0开始计算,与月份数从1到12不符,因此,在程序中使用语句“enum month{Jan=1,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec}”强制第一个元素Jan代表1,其他元素依次递增Feb为2,Mar为3……Dec为12,从而实现了枚举值与实际月份相符。
从实例中我们可看出,使用枚举类型,可让数值与有意义的名字对应,更方便于实际问题的理解。
5.3.2枚举类型的定义及使用一.枚举的概念所谓“枚举”,是指将变量的所有取值一一列举出来,变量的取值只限于列举出来的值的范围。该变量称之为枚举型变量。所列举的值叫做枚举元素(枚举常量)。
二.枚举类型的定义定义一个枚举类型名,再用它定义变量。其中,enum为枚举类型的关键字,类型名和枚举元素名由用户自己命名,应符合标识符的命名规则。
enum 枚举类型名 
 {
标识符[=整型常数],
标识符[=整型常数],
...,
标识符[=整型常数]
};
enum 枚举类型名 变量列表;
例如:enum weekday{sun,mon,tue,wd,thu,fri,sat};
enum weekday day;
注意:枚举中每个成员(标识符)分隔符是“,”,不是“;”。
在定义后,枚举中的标识符在程序中代表其后的常数,枚举定义中的整型常数可以省略,如果省略后则依次代表0、1、2、…,依次递增。其实,枚举常量这些标识符只是一个符号,完全是出于用户的需要及平时的习惯定义的。例如,我们习惯用sun表示星期天,定义时就可以用sun这个标识符表示星期天,但这不是强制性的,也可以用别的字符串来表示,只要用户明白就可以了。
三.枚举类型的引用枚举变量的引用与引用普通变量的方法是一样的,但要注意枚举变量的取值不能超出所罗列的枚举常量的范围。
例如定义:
enum WEEKDAY{sun,mon,tue,wed,thu,fri,sat}workday,weekend,w[3];
那么,下面的引用是正确的:
workday=mon;或w[2]=sat;
说明:
⑴ C语言中枚举元素按常量处理,它们是有值的。它们的值是系统按其定义顺序自动赋予的 0、1、2、3、4、……。
因此上例中的枚举元素sun的值为0,mon的值为1,依次类推。枚举变量的值即是它所取的枚举元素的值,此值可输出查看,如day=fri;则day的值为5,可以输出printf(“%d”,day); 输出结果为5。
⑵ 枚举元素的值也可以改变,但必须在定义时指定。
如:enum weekday {sun=7,mon=1,tue,wd,thu,fri,dat };
如果定义时未指定值,则按顺序取默认值。
⑶ 枚举元素是常量,不是变量,不能在定义以外的任何位置对它们赋值,如:
sun=5;是不正确的。
⑷ 枚举值可用来做判断比较,如:
if(day==sun) ……;
if(day>mon&&day<fri) ……
⑸ 枚举变量取值只能是所列举的枚举元素,而不能直接赋予一个整数值,如:day=2;当然可以采用强制类型转换的方式赋值,但多数情况不采用。
习题五填空题
1.____________是一种复合的数据类型,它允许用其他数据类型构成一个结构类型。
2.同数组类似,一个结构体也是若干数据项的集合,但与数组不同,数组中的所有元素_____________________,而结构体中的数据项可以是_________________。
3.____________运算符用于引用结构体的成员,_________________运算符用于引用结构指针所指对象的成员。
4.在任一时刻,共用体中能存放了_________个有效的成员数据。
5.枚举中的元素不是变量,也不是字符串,它是_____________。
6.枚举中的元素是有值的,它们都是_____________型值,因此,枚举可以比较大小,在定义时靠后的较大。
7.有枚举类型定义如下:enum { a1,a2=4,a3,a4=10 }; 则元素a3的序号为____________。
8.以下程序的运行结果是________
struct
{ int a;
int b;
struct
{ int x;
int y;
}ins;
}outs;
main( )
{ outs.a=11;
outs.b=4;
outs.ins.x=outs.a+outs.b;
outs.ins.y=outs.a-outs.b;
printf("%d,%d",outs.ins.x,outs.ins.y);
}
二、选择题
1.设有以下说明语句,则下面叙述不正确的是______________。
struct stu
{ int a;
float b;
}stutype;
A、struct 是结构体类型的关键字 B、struct stu 是用户定义的结构体类型
C、stutype 是用户定义的结构体类型名 D、a 和 b 都是结构体成员名
2.当说明一个结构体变量时系统分配给它的内存是___________。
A、各成员所需内存量的总和 B、结构中第一个成员所需内存量
C、成员中占内存量最大者所需的容量 D、结构中最后一个成员所需内存量
3.以下程序的运行结果是________________。
# include "stdio.h"
main()
{ struct date
{ int y,m,d;} today;
printf("%d\n",sizeof(struct date));
}
A、2 B、3 C、6 D、出错
4.根据下面的定义,能打印出字母M的语句是_____________。
struct person { char name[9];int age;};
struct person class[10]={"John",17,"PauI",19,"Mary",18,"Adam",16};
A、printf("%c\n",class[3].name); B、printf("%c\n",class[3].name[1]);
C、printf("%c\n",class[2].name[1]); D、printf("%c\n",class[2].name[0]);
5.以下对结构体变量stu1中成员age的非法引用是____________。_
struct student
{ int age;
int num;
}stu1,*p;
p=&stu1;
A、stu1.age B、student.age C、p->age D、(*p).age
6.已知学生记录描述如下,下面对结构体成员"computer"的赋值方式正确的是_______。
struct student
{ int num;
char name[8];
struct
{ float math;
float engl;
float computer;
} mark;
} std;
A、student.computer=84;
B、mark.computer=84;
C、std.mark.computer=84;
D、std.computer=84;
7.当说明一个共用体变量时系统分配给它的内存是________。
A、各成员所需内存量的总和 B、结构中第一个成员所需内存量
C、成员中占内存量最大者所需的容量 D、结构中最后一个成员所需内存量
8.以下对C语言中共用体类型数据的叙述正确的是________。
A、可以对共用体变量名直接赋值
B、一个共用体变量中可以同时存放其所有成员
C、一个共用体变量中不能同时存放其所有成员
D、共用体类型定义中不能出现结构体类型的成员
9.定义union {char a[10];int b;char c[20]}comm; 当执行以下语句,输出结果是______。
strcpy(comm.a,"hello");
comm.b=3;
strcpy(comm.c,"my computer");
printf("%s",comm.a);
A、hello B、hello my computer C、my computer D、hellomputer
10.以下程序的运行结果是________。
# include "stdio.h"
main( )
{ union
{ long a;
int b;
char c;
struct
{ int x;
int y;
} ss;
} m;
printf("%d\n",sizeof(m));
}
A、2 B、4 C、6 D、8
11.设定义enum color {red=4,green,white=red+green};则printf("%d %d %d ",red,green,white);的输出结果是____________。
A、4 5 9 B、4 1 5 C、0 1 2 D、0 1 1
12.设有如下枚举类型定义:
enum language { English=6,French,Chinese=1,Japanese,Italian};
则枚举量Italian的值为________。
A、10 B、4 C、3 D、5
13.以下对枚举类型名ee的定义中正确的是________。
A、enum ee {A,B,C,D};
B、enum ee {'A','B','C','D'};
C、enum ee = {A,B,C,D};
D、enum ee = {'A','B','C','D'};
三、程序题
1.编写函数ReadInfo读入10名职工的编号(整型)、姓名(字符串)、联系电话(字符串)放在结构体数组work中;编写函数WriteInfo输出10名职工的记录;在主函数中分别调用上述两个函数,实现程序的功能。
2.定义一个结构体用于存储年、月、日数据,并定义函数用于求两个日期之间的天数。
3.从键盘上输入任一天的日期,如“2006-6-1”,编程求出该天是星期几。提示:公元1年1月1日是星期天。
第六章 磁盘数据存储经过前几章的学习,我们可以使用数组、结构体等构造数据类型进行复杂编程,并且将任务按功能模块分解,进行比较大型的程序开发了。这里还有一个问题,如果程序处理的数据需要长期保存起来该怎么办?虽然数组可以用于存储N本图书的信息,但数组属于程序运行过程中的临时变量,当程序运行结束后系统将释放该程序所占的内存区,处理结果就不再保存了。另外,有时程序处理的数据不是从键盘上输入的,而是从数据文件里取出来的,这些将如何实现呢?这就是本章所解决的问题。
6.1 将数据写入文件
【例5.1】将一串字符‘A’~‘Z’写入文件保存起来。
#include <stdio.h>
main()
{
char ch;
FILE *fp; /* 定义文件指针;使用文件的通用步骤1。*/
fp=fopen("letter.dat","w"); /* 新建并打开一个磁盘文件letter.dat;使用文件的步骤2*/
if(fp==NULL) /* 判断文件打开成功与否 */
{ printf("\n Opening file error");
exit(0);
}
for (ch='A';ch<='Z';ch++)
fputc(ch,fp); /* 将存储在变量ch中的字符写入文件;
使用文件的步骤3*/
fputc('\n',fp); /* 最后写入一个换行符 */
fclose(fp); /* 关闭文件;使用文件的步骤4*/
}
说明:
由以上简单示例我们可以看出使用文件的一些必要步骤和操作特点。
(1)定义文件类型指针。文件类型FILE在头文件stdio.h中声明,我们先不必关注其中定义的细节,先拿来使用即可。
(2)打开文件。使用文件,不论是写入数据还是读出数据,不论是对一个已有的文件进行读写还是对新文件进行操作,首先都是使用fopen()函数打开文件。
(3)向文件写入数据,或从文件读取数据。写入和读取的数据方式不同,需使用不同的读写文件函数,如下面例题所用的读写函数就不同于本题;这是最关键的一步。
(4)关闭文件。这是文件使用的最后一步,使用完毕必须关闭文件,才能彻底的将文件缓冲区的数据写入文件,并释放系统分配的文件缓冲区。
以上是磁盘数据存储,即文件操作的几个通用步骤,当然实际的文件操作不限于这几个步骤,例如打开文件后可以检测打开是否成功等,再如文件使用过程中的使文件指针复位或调整等也都是常用操作,但文件操作至少要包含上述的4个基本步骤。
打开文件函数
fp=fopen(“文件名”,”打开方式”);
说明:
文件名:是指包含主文件名和扩展名的文件全称,如果文件不在当前目录下,还应该给出文件的路径。
打开方式:C语言操作的数据文件有两种格式:二进制文件和ASCII码文件。其存储形式有所不同,ASCII码文件(文本文件)的每个字节存放一个ASCII码,代表一个字符,因而便于对字符进行逐个处理(如字符串)。ASCII码文件可以阅读,可以打印,但是它与内存数据交换时需要转换。二进制文件是将内存中的数据按照其在内存中的存储形式原样输出,并保存在文件中。内存数据和磁盘数据交换时无须转换,但是二进制文件不可阅读、打印。它的1个字节并不对应1个字符,例如数值32767在内存中的二进制形式为01111111 11111111,写入到二进制文件中将占2个字节,存储形式就是它的内存二进制形式。而如果用一个文本文件存储32767,将作为5个数字字符存储,因此文件将占5个字节的长度。
两种不同格式的文件在打开方式上的区别是:二进制文件在原打开方式后面加一个“b”,来表示操纵的是二进制文件,例如以读数据的方式打开文本文件,打开方式用“r”,而打开二进制文件读取数据,打开方式应为“rb”。具体的几种打开方式如下:
读的方式打开文件:使用“r”和“r+”(r+表示既可读文件也可写文件,两种目的);使用r的方式文件应该是已经存在或建立过的文件。
写的方式打开文件:使用“w”和“w+”(表示既可读文件也可写文件,两种目的);使用w的方式表示新建一个文件,若磁盘上已有同名文件,则被覆盖。
追加的方式打开文件:使用“a”和“a+”时,文件应已经存在,原有内容不被删除,文件位置指针打开后直接移到数据末尾,可以在后面追加内容。
如果操作的是二进制文件,在打开方式中加上字母“b”。
关闭文件函数
fclose(fp);
各种使用方式的文件,其关闭函数是相同的。
【例5.2】将图书信息数据存储于数据文件中,观察该例所使用的写入文件函数。
#include <stdio.h>
#include <string.h>
#define N 5
main()
{
struct book
{
int num; /*图书编号*/
char name[50]; /*图书名*/
char publish[40]; /*出版社*/
struct
{
int year;
int month;
}day; /*出版时间*/
char borrow; /*是否已被借阅*/
};
struct book books[N]; /* 定义图书类型的数组,用于存储N本图书信息*/
char name[50];
int i,num;
FILE *fp; /* 定义文件指针*/
clrscr();
fp=fopen("books.dat","wb+"); /* 新建一个磁盘文件books.dat */
if(fp==NULL) /* 判断文件打开成功与否 */
{ printf("\n Opening file error");
exit(0);
}
printf("Input book's num,name,publish,day,borrow:\n");
printf("Be caution,you should input the <Space> to seperate every item!\n"); /* 提示操作人员从键盘上输入图书的信息 */
/* 下面循环N次,从键盘上输入N本图书的信息,先放在长度为N的结构体类型的数组books中临时保存,以便后面写入到文件中 */
for(i=0;i<N;i++)
{ printf("number %d:",i+1);
scanf("%d",&books[i].num);
scanf("%s %s",books[i].name,books[i].publish);
scanf("%d%d%c",&books[i].day.year,&books[i].day.month,&books[i].borrow);
}
/* 下面语句将books数组中的图书信息写入到文件books.dat中 */
fwrite(books,sizeof(struct book),N,fp); /* 此处使用fwrite函数写入文件*/
output(books); /* 将图书信息同时显示到屏幕上 */
fclose(fp); /* 关闭文件 */
getch();
} /* 结束 */
/*向屏幕上输出图书信息子函数定义 */
void output(struct book b)
{ int i;
printf("\n Output the book's information:\n");
printf("---------------------------------\n");
printf(" num name publish publishday borrow:\n\n");
for(i=0;i<N;i++,b++)
printf("% -10d%-16s%-12s%6d,%d%9c\n",b->num,b->name,b->publish,b->day.year,b->day.month,b->borrow);
printf("----------------------------------------------------\n\n");
}
说明:
通过上例我们仔细观察,在文件使用上打开函数fopen()的使用略有不同,而且向文件写入数据也使用了不同的函数。
6.2 文件读写分类函数由例5.2我们看出不同文件格式或数据类型可使用不同的读写函数,上例中的图书信息使用fwrite函数将信息写入文件,而例5.1中则使用fputc函数将字符一个一个写入到文件中,下面我们来总结和归纳一下各种类型的文件读写函数。
1.单字符的写入函数格式:int fputc(int ch,FILE *fp);
功能:将字符ch(可以是字符表达式,字符常量、变量等)写入fp所指向的文件。
返回:输出成功,返回输出的字符ch;输出失败,返回EOF(-1)。
其它说明:每次写入一个字符,文件位置指针自动指向下一个字节。
【例5.3】从键盘输入一行字符,写入到文本文件string.txt中。
#include <stdio.h>
main()
{
FILE *fp;
char ch;
if((fp=fopen("string.txt","w"))==NULL) /* 打开文件string.txt(写) */
{
printf("can't open file\n");exit(1);
}
do /* 不断从键盘读字符并写入文件,直到遇到换行符 */
{
ch=getchar(); /* 从键盘读取字符 */
fputc(ch,fp); /* 将字符写入文件 */
}while(ch!='\n');
fclose(fp); /* 关闭文件 */
}
2.单字符的读取函数格式:int fgetc(FILE *fp);
功能:从fp所指向的文件读一个字符,字符由函数返回。返回的字符可以赋值给字符变量,也可以直接参与表达式运算。
返回:输入成功返回输入的字符;遇到文件结束,返回EOF(-1)。
其它说明:每次读入一个字符,文件位置指针自动指向下一个字节。
文本文件的内部全部是ASCII字符,其值不可能是EOF(-1),所以可以使用EOF(-1)确定文件结束;但是对于二进制文件不能这样做,因为可能在文件中间某个字节的值恰好等于-1,如果此时使用-1判断文件结束是不恰当的。为了解决这个问题,ANSI C提供了feof(fp)函数判断文件是否真正结束。
feof函数既适合文本文件,也适合二进制文件文件结束的判断。
【例5.4】将磁盘上一个文本文件的内容复制到另一个文件中。
#include <stdio.h>
main()
{
FILE *fp_in,*fp_out;
char infile[20],outfile[20];
printf("Enter the infile name:");
scanf("%s",infile); /* 输入欲拷贝源文件的文件名 */
printf("Enter the outfile name:");
scanf("%s",outfile); /* 输入拷贝目标文件的文件名 */
if((fp_in=fopen(infile,"r"))==NULL) /* 打开源文件 */
{
printf("can't open file:%s",infile); exit(1);
}
if((fp_out=fopen(outfile,"w"))==NULL) /* 打开目标文件 */
{
printf("can't open file:%s",outfile); exit(1);
}
while(!feof(fp_in)) /* 若源文件未结束 */
{
fputc(fgetc(fp_in),fp_out); /* 从源文件读一个字符,写入目标文件 */
}
fclose(fp_in); /* 关闭源、目标文件 */
fclose(fp_out);
}
3.字符串读写函数格式:char *fgets(char *str,int n,FILE *fp)
功能:从fp所指向的文件读n-1个字符,并将这些字符放到以str为起始地址的单元中。如果在读入n-1个字符结束前遇到换行符或EOF,读入结束。字符串读入后最后加一个‘\0’字符。
返回:输入成功返回输入串的首地址;遇到文件结束或出错返回NULL。
【例5.5】编制一个将文本文件中全部信息显示到屏幕的程序(类似于dos的type命令)。
#include <stdio.h>
void main(int argc,char *argv[])
{
FILE *fp;
char string[81]; /* 最多保存80个字符,外加一个字符串结束标志 */
if(argc!=2||(fp=fopen(argv[1],"r"))==NULL) /* 打开文件 */
{
printf("can't open file"); exit(1);
}
while(fgets(string,81,fp)!=NULL)
/* 如果未读到文件末尾(EOF),函数不会返回NULL,继续循环(执行循环体) */
/* 从文件一次读80个字符,遇换行或EOF,提前带回字符串 */
printf("%s",string); /* 打印串 */
fclose(fp); /* 关闭文件 */
}
4.写一个字符串到磁盘文件格式:fputs(char *str,FILE *fp)
功能:向fp所指向的文件写入以str为首地址的字符串。
返回:输入成功返回值0;出错返回非0值。
【例5.6】 在文本文件string.txt末尾添加若干行字符。使用fputs的例子。
#include <stdio.h>
void main()
{
FILE *fp;
char s[81];
if((fp=fopen("string.txt","a"))==NULL) /* 打开文件 */
{
printf("can't open file\n"); exit(1);
}
while(strlen(gets(s))>0)/* 从键盘读入一个字符串,遇到空行(strlen=0)结束 */
{
fputs(s,fp); /* 将字符串写进文件 */
fputs("\n",fp); /* 补一个换行符 */
}
fclose(fp); /* 关闭文件 */
}
5.格式化读写函数格式化文件读写函数fprintf,fscanf与函数printf,scanf作用基本相同,区别在于fprintf,fscanf读写的对象是磁盘文件,printf,scanf读写的对象是终端。
格式:
fprintf(fp,格式字符串,输出表列);
fscanf(fp,格式字符串,输入表列);
其中:fp是文件指针。
数据块读写函数(一般用于二进制文件读写)。
从文件(特别是二进制文件)读写一块数据(如一个数组元素,一个结构体变量的数据记录)使用数据块读写函数非常方便。
数据块读写函数的调用形式为:
int fread(void *buffer,int size,int count,FILE *fp);
int fwrite(void *buffer,int size,int count,FILE *fp);
其中:
buffer是指针,对fread用于存放读入数据的首地址;对fwrite是要输出数据的首地址。
size是一个数据块的字节数(每块大小),count是要读写的数据块块数。
fp文件指针
fread、fwrite返回读取/写入的数据块块数。(正常情况=count)
以数据块方式读写,文件通常以二进制方式打开。
例如:
float f[2];
FILE *fp=fopen(“...”,”r”);
fread(f,4,2,fp); /* 或fread(f,sizeof(float),2,fp); */
【例5.7】从键盘输入一批学生的数据,然后把它们转存到磁盘文件stud.dat中。
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
struct student
{
int num;
char name[20];
char sex;
int age;
float score;
}; /* 共5个成员,占用29 bytes */
main()
{
struct student stud;
char numstr[20],ch;
/* numstr-临时字符串,保存学号/年龄/成绩,然后转换为相应类型; ch-Y/N */
FILE *fp;
if((fp=fopen("stud.dat","wb"))==NULL) /* 以二进制、写方式打开文件 */
{
printf("can't open file stud.dat\n");
exit(1);
}
do
{
printf("enter number:"); gets(numstr); stud.num=atoi(numstr);
printf("enter name:"); gets(stud.name);
printf("enter sex:"); stud.sex=getchar(); getchar();
printf("enter age:"); gets(numstr); stud.age=atoi(numstr);
printf("enter score:"); gets(numstr); stud.score=atof(numstr);
/* 每次将一个准备好的结构体变量的所有内容写入文件(写一个记录) */
fwrite(&stud,sizeof(struct student),1,fp);
printf("have another student record(y/n)?"); ch=getchar(); getchar();
}while(toupper(ch)=='Y'); /* 循环读数据/写记录 */
fclose(fp); /* 关闭文件 */
}
说明:
空读:在输入字符,并按回车后,实际缓冲中有两个字符(如‘f/m’和‘\n’),只要前面有意义的字符(‘f/m’)。可以用“空读”略过‘\n’。
什么情况要空读?如果后面的读取键盘是读取数字(整数/浮点数),不必空读;如果后面的读取键盘是读字符或字符串,应当“空读”。
思考:如果输入两个记录的数据,那么最后产生的文件长度是多少?(58 bytes)
备注:C语言即使写文本文件,关闭时,也不自动加文件结束符。
6.3文件定位函数对文件的读写可以顺序读写,也可以随机读写。
文件顺序读写:从文件的开头开始,依次读写数据。(从文件开头读写直到文件尾部)
文件随机读写(文件定位读写):从文件的指定位置读写数据。
文件位置指针:在文件的读写过程中,文件位置指针指出了文件的当前读写位置(实际上是下一步读写位置),每次读写后,文件位置指针自动更新指向新的读写位置(实际上是下一步读写位置)。 注意区分:文件位置指针,文件指针。
可以通过文件位置指针函数,实现文件的定位读写。文件位置指针函数有:
位置指针复位函数rewind(fp)
将文件指针重新定位到文件开始的地方。此函数没有返回值。
位置指针的随机移动函数 int fseek(FILE *fp,long offset,int base);
功能:将文件的位置指针移到以base为起始点,offset为位移量的位置,同时清除文件结束标志。
base常用三个符号常量:SEEK_SET、SEEK_CUR和SEEK_END,分别表示文件开始、当前位置和文件末尾。
offset表示以起始点为基准,向前或向后移动的字节数(正、负)。
求文件位置指针当前位置的函数long ftell(FILE *fp);
功能:返回位置指针相对于文件开头的位移量。若出错返回-1L。
【例5.8】继续例5.2,从图书信息数据文件中读取图书信息,并且修改和保存图书信息到数据文件中。
分析:此种情况下应采用何种打开方式?使用什么样的读写函数对图书信息数据文件中的图书信息数据进行读取。
#include <stdio.h>
#include <string.h>
#define N 5
struct book
{
int num; /*图书编号*/
char name[50]; /*图书名*/
char publish[40]; /*出版社*/
struct
{
int year;
int month;
}day; /*出版时间*/
char borrow; /*是否已被借阅*/
};
main()
{
struct book books[N];
char name[50];
int i,num;
FILE *fp;
fp=fopen("books.dat","rb+"); /* 因为对已有文件进行读、写,而且上例建立的数据文件是以二进制形式存储的,因此使用rb+的方式 */
if(fp==NULL) /* 判断文件打开成功与否 */
{ printf("\n Opening file error");
exit(0);
}
clrscr();
fread(books,sizeof(struct book),N,fp); /* 使用块读的方式,整批的读入N个图书的数据 */
output(books); /* 调用输出子函数,显示图书信息到屏幕上 */
printf("Now you can input the book name you are going to borrow:\n");
scanf("%s",name); /*读入所借图书书名 */
borrow(books,name); /* 调用借书子函数 */
rewind(fp); /* 将文件指针重新定位到文件开始的地方 */
fwrite(books,sizeof(struct book),N,fp); /* 使用块写的方式,整批的写入修改后的图书数据 */
output(books);
printf("Now you can input the book name you are going to return:\n");
scanf("%s",name);
back(books,name); /* 调用还书子函数 */
rewind(fp);
fwrite(books,sizeof(struct book),N,fp);
output(books);
fclose(fp);
getch();
}
void output(struct book *b) /*图书信息输出子函数 */
{ int i;
printf("\n Output the book's information:\n");
printf("---------------------------------\n");
printf(" num name publish publishday borrow:\n\n");
for(i=0;i<N;i++,b++)
printf("% -10d%-16s%-12s%6d,%d%9c\n",b->num,b->name,b->publish,b->day.year,b->day.month,b->borrow);
printf("----------------------------------------------------\n\n");
}
void borrow(struct book *b,char name[])/*图书借阅子函数 */
{ int i,flag=0;
for(i=0;i<N;i++,b++)
{
if(strcmp(b->name,name)==0)
{ flag=1;
if(b->borrow=='n')
{ b->borrow='y';
printf("success!\n");
}
else printf("The book has been borrowed.You can't borrow it.\n");
}
}
if(flag==0)
printf("We can't find this book.\n");
}
void back(struct book *b,char *name) /*还书子函数 */
{ int i,flag=0;
for(i=0;i<N;i++,b++)
{
if(strcmp(b->name,name)==0)
{ flag=1;
b->borrow='n';
printf("The book has been returned.\n");
}
}
if(flag==0)
printf("Can't find this book! The book doesn't belong to our library.\n");
}
习题五选择题
1、若执行fopen函数时发生错误,则函数的返回值是_________
A、随机值 B、1
C、NULL D、EOF
2、若用fopen打开一个新的二进制文件,要求文件既能读又能写,则应选用的文件方式字符串是____________
A、"wb+" B、"r+"
C、"rb+" D、"ab+"
3、当正常执行了文件关闭操作时,fclose函数的返回值是_________
A、-1 B、随机值 C、0 D、1
4、若有以下定义和函数说明,以下不能将文件内容读入数组x中的语句是_______
struct ss
{ int n;
float x;
}x[30];
FILE *fp;
A、for(i=0; i<10; i++)
fread(&x[i],sizeof(struct ss),1L,fp);
B、for(i=0; i<10; i++)
fread(&x[i],sizeof(struct ss),2L,fp);
C、for(i=0; i<30; i++)
fread(&x[i],sizeof(struct ss),1L,fp);
D、fread(x,sizeof(struct ss),30L,fp);
5、设有以下结构体类型数组的定义,且数组mini的10个元素都已赋值,若要将这些元素写到文件fp中,以下不正确的形式是________
struct abc
{ int a; char b; float c[4]; } mini[10];
A、fwrite(mini,10*sizeof(struct abc),1,fp);
B、fwrite(mini,5*sizeof(struct abc),2,fp);
C、for(i=0; i<10; i++)
fwrite(mini,sizeof(struct abc),1,fp);
D、fwrite(mini,sizeof(struct abc),10,fp);
6、若调用fputc函数输出字符成功,则函数的返回值是________
A、输出的字符  B、-1
C、0   D、EOF
7、函数调用语句:fseek(fp,10L,2);的含义是:将文件位置指针________
A、移动到距离文件头10个字节处
B、从当前位置向后移动10个字节
C、从文件尾部前移10个字节
D、移到离当前位置10个字节处
8、函数rewind(fp)的作用是:使文件位置指针________
A、重新返回文件的开头
B、返回到前一个字符的位置
C、指向文件的末尾
D、自动移至下一个字符的位置
9、函数fgets(string,m,fp)的作用是_______
A、从fp所指向的文件中读取长度不超过m的字符串,存入由指针string指向的内存区域
B、从fp所指向的文件中读取长度为m的字符串存入由指针string指向的内存区域
C、从fp所指向的文件中读取m个字符串,存入由指针string指向的内存区域
D、从fp所指向的文件中读取长度不超过m-1的字符串,存入由指针string指向的内存区域二、填空题
1、在C程序中文件可以用两种方式存取,它们是___________和_____________
2、在C程序中数据可以用两种代码形式存放,它们是_________和__________
3、在C语言中,文件的存取是以_____为单位的,这种文件被称做______文件。
4、下面程序用以统计文件中小写字母a的个数。请填空。
#include<stdio.h>
main( )
{ FILE *fp;
char m; long n=0;
if ( (fp=fopen("letter.txt","r"))==NULL)
{printf("cannot open file\n"); exit(0); }
while(__________________)
{ m=__________;
if (m=='a') ______________; }
printf("n=%ld\n",n);
fclose (fp);
}
5、下面程序从键盘输入字符存放到文件中,用#结束输入。存放字符的文件名也由键盘输入。请填空。
#include<stdio.h>
main( )
{FILE *fp;
char ch,fname[10];
printf("Input name of file\n");
gets(fname);
if ((fp=fopen(________,"w"))= =NULL)
{ printf(“cannot open\n”); exit(0); }
printf(“Enter character:\n”);
while(______________!='#')
fputc(ch,____________);
fclose(fp);
}
6、以下程序的功能是将文件exam1.c的内容复制到exam2.c中,请填空。
#include<stdio.h>
main( )
{ FILE fp1,fp2;
fp1=fopen(______________);
fp2=fopen(______________);
while (!feof(fp1))
fputs(__________________________);
fclose(fp1); fclose(fp2);
}
7、设文件num.dat中存放了一组整数。以下程序实现统计并输出文件中正整数、零、负整数的个数。请填空。
#include<stdio.h>
main()
{ FILE *fp;
int p=0,n=0,z=0,temp;
fp=_______________________;
if(fp==NULL)
printf(“file not found\n);
else
{ while(!feof(fp))
{ fscanf(____________________);
if(temp>0) p++;
else if(temp<0) n++;
else __________________;
}
}
fclose(fp);
printf("positive=%d,negtive=%d,zero=%d\n",p,n,z);
}
三、编程题
1、按下面的近似公式计算e的值。误差小于10-5。试编程:

2、设有一个已按从大至小顺序排好的数列存放在一维数组中,现输入一个数,仍按原来的排序规律将其插入到数组中,试编程。
3、设有一行文字,现要求从其中删去某个指定的字符(如:输入a,表示将从该行文字中删去所有是a的字符),要求该行文字和待删的字符均从键盘上输入,请编写程序。
4、设有一行文字,现要求将每一个单词中的第一个字母改成大写字母(原来已经是大写字母的不变),请编程。
5、输出能被3整除且至少有一位是5的两位数,要求在函数fun中输出所有这样的数,在主函数中完成相应的输入并打印出这些数的个数。请编程。
6、已有变量说明:double num; int n; 且给出函数调用语句mypow(num,n);,和函数说明double mypow(double x,int y),函数mypow用来求num的n次方。请编写函数mypow。
7、用递归算法将一个整数x转换成对应的字符串(例如:输入整数123,应输出字符串"123")。x可以是位数不超过5 且数值在-32768到32767之间的任意整数。
8、编写一个函数,求出一个正整数的所有因子(例如:72=2*2*2*3*3)。
9、编写程序:动态开辟3个存储单元存放从键盘输入的3个整数,然后找出并输出其中的最大值。
10、编写函数ReadInfo读入10名职工的编号(整型)、姓名(字符串)、联系电话(字符串)放在结构体数组work中;编写函数WriteInfo输出10名职工的记录;在主函数中分别调用上述两个函数,实现程序的功能。
第七章 实用程序设计技巧程序设计课程是计算机科学教育的第一门专业课程,其目标是介绍如何理解程序语言,如何学习程序设计,如何为计算机领域中的进一步学习和工作做好准备。C作为一个通用的程序设计语言,由于它的紧凑、高效、易于移植、模块化等特点已被广泛地应用于系统程序设计、人工智能、文字处理、科学计算、计算机绘图等领域,尤其在编写系统软件或进行算法实现等领域,C语言更以它的简洁、高效、能和硬件交互等而被广泛采用。
本书讨论了基本程序设计的各方面问题,除了我们给出程序设计实例,帮助读者认识程序设计过程的实质,理解从问题到程序的思考过程以外,还将在这一章进行与程序设计技巧有关的问题讨论。
本章我们首先讨论程序的模块化层次结构,引入软件工程的思想,介绍模块设计、分解和组装的原则和方法,然后再讨论大型C程序的设计风格和书写风格。
7.1 程序的模块化结构程序设计也是一种工程性的工作,通过对任务进行分析和功能模块分解,将大任务分解为若干子任务,子任务分别进行设计之后,再进行组合,合并为功能强大而复杂的一个整体。我们可以把这些小功能模块看成“零件”,需要时就可以用这些“零件”组成“机器”,而不必每次都重新做起。这里就存在很多程序设计的技巧,我们可以加以总结和利用,这样可大大的提高程序设计的效率并且让编程者保持一个清晰的思路,不被具体的细节捆住手脚。这里先引入软件工程的思想,分析如何进行模块分解。
7.1.1 软件工程的思想系统设计分四方面内容:体系结构设计、模块设计、数据结构与算法设计、用户界面设计。如果将软件系统比喻为人体,那么,
(1)体系结构就如同人的骨架。如果它的骨架是猴子,那么无论怎样喂养和美容,它始终都是猴子,不会成为人。
(2)模块就如同人的器官,具有特定的功能。人体中最出色的模块设计之一是手,手只有几种动作,却能做无限多的事情。人体中最糟糕的模块设计之一是嘴巴,嘴巴将最有价值但毫无相干的几种功能如吃饭、说话、哭笑混为一体,使之无法并行处理。
(3)数据结构与算法就如同人的血脉和神经,它让器官具有生命并能发挥功能。数据结构与算法分布在体系结构和模块中,它将协调系统的各个功能。人的耳朵和嘴巴虽然是相对独立的器官,但如果耳朵失聪了,嘴巴就只能发出“啊”“呜”的声音,等于丧失了说话的功能(所以聋子天生就是哑巴)。
(4)用户界面就如同人的外表,最容易让人一见钟情或一见恶心。象人类追求心灵美和外表美那样,软件系统也追求(内在的)功能强大和(外表的)界面友好。
7.1.2 模块设计在设计好软件的体系结构后,就已经在宏观上明确了各个模块应具有什么功能,应放在体系结构的哪个位置。我们习惯地从功能上划分模块,保持“功能独立”是模块化设计的基本原则。因为“功能独立”的模块可以降低开发、测试、维护等阶段的代价。但是“功能独立”并不意味着模块之间保持绝对的孤立。一个系统要完成某项任务,需要各个模块相互配合才能实现,此时模块之间就要进行信息交流。
比如手和脚是两个“功能独立”的模块。没有脚时,手照样能干活。没有手时,脚仍可以走路。但如果希望跑得快,那么迈左脚时一定要伸右臂甩左臂,迈右脚时则要伸左臂甩右臂。在设计一个模块时不仅要考虑“这个模块就该提供什么样的功能”,还要考虑“这个模块应该怎样与其它模块交流信息”。 评价模块设计优劣的三个特征因素:“信息隐藏”、“内聚与耦合”和“封闭——开放性”。
以上是形象化的描述软件工程的思想和模块化的含义和分解特征,我们可以堪称模块化程序设计是软件工程思想的核心和主题。模块化设计时通常是将一个大型的程序自上向下的进行功能分解,分成若干个子模块,每个模块对应了一个功能,有自己的界面,有相关的操作,完成独立的功能。各个模块可以分别由不同的人员编写和调试,最后,将不同的模块组装成一个完整的程序。
在C语言中,用函数实现功能模块的定义,程序的功能可以通过函数之间的调用实现。一个完整工程项目的C程序可能由多个源程序文件组成,每一个文件中又可以包含多个函数。图6-1给出了一般的C语言程序的结构。关于函数的相关定义和调用知识我们已在第3章进行了学习和讨论,在此就不详细介绍了。
7.1.3 使用模块化方法开发程序的好处把问题的解决方案细分为一系列的模块有许多好处。因为每个模块都有一个特殊的目的,所以就能撇开问题解决方案的其他部分而单独编写和测试该模块。独立的模块比完整的模块的解决方案要小,因此测试起来更加容易。同时,只要仔细测试过一个模块,不需要重新测试就可以直接将之用在新的问题解决方案 中。例如,假设已开发了一个模块来计算一组数字的最大值,只要编写并测试了此模块,就能在需要计算最大值的其他程序中使用它。复用性是大型软件系统开发中一个非常重要的问题,它可以节省大量的开发时间。事实上,在计算机系统中经常可以找到各种使用频繁的模块库(比如标准C库),在C程序中也可以找到使用频繁的系统模块(比如scanf输入函数和printf输出函数)。
模块的使用常常可以缩短程序的总长度。因为许多问题解决方案包含的步骤在程序中都是多次重复的,所以可以把这些重复的步骤都整合到一个函数中,每次需要他们时只需引用一个单独的语句。
如果一个项目被分割为几个模块,那么,同一个项目的开发工作就可以在若干程序员之间展开,每个独立的模块都可以单独地进行开发与测试。这就能够加快开发进度,因为许多工作可以同时进行。
用于完成特定任务的模块支持抽象的概念,也就是说,模块内部包含了实现任务的具体细节,而对于使用该模块的程序或程序员而言,无需关注这些细节就可以引用该模块。在制定问题的解决方案时,我们使用的I/O图就是一个抽象的例子:我们指定输入信息和输出信息,但没有给出如何计算输出信息的细节。因此,我们可以把模块当成“黑匣子”,他们具有指定的输入并具有指定的输出信息。在项目分解时我们可以先这样笼统的划分模块,这样,我们才可以通过更高级的抽象来解决问题,而无需一下子就关注所有细节。使用抽象,我们可以增强软件质量,同时也可以减少软件的开发时间。
总的说来,在问题解决方案中使用模块有以下优点:
模块可以独立于解决方案的其他部分进行单独的编写和测试,因此对于大型项目,各个模块的开发可以同步进行。
模块是解决方案的一小部分,因此单独测试起来更加容易。
经过仔细的测试之后,不需要重新测试就可以将模块直接应用于新的问题解决方案中。
使用模块通常可缩短程序的长度,使程序更具可读性。
模块的使用促使采用抽象的概念,从而允许程序员把细节“隐藏”于模块之中,这就使得我们能够像使用系统库函数一样使用模块,而无须考虑具体的细节。
结构图或模块图显示了一个程序的模块结构。main函数可引用其他函数,而这些函数自身又可以引用其他函数。例如图6-2中给出了一个语音信号分析系统的模块结构图例子。这里main主函数调用方差函数std_dev、平方平均值函数ave_power、绝对值平均值函数ave_magn和零交叉次数crossings函数,而方差函数std_dev中又调用了偏差函数variance,在偏差函数variance中又调用了平均值函数mean,后两个函数同时也被main函数直接调用使用,由此我们可以看出这个系统的功能模块分解情况,系统主功能被分解为求平均值mean、计算方差std_dev、标准偏差variance、平方平均值ave_power、绝对值平均值ave_magn和零交叉次数crossings等六个功能模块,其中模块mean和variance具有复用功能,又被其他功能模块调用,从图中可以看出整个系统模块的层次结构。有了这样的分析和模块层次结构,我们开发起系统来就会思路清晰,目的明确,模块的组装和功能调用也都有了明确的指导,若干模块同时开发,程序的开发效率大大提高。
7.2 模块的组装在用模块化方法开发程序时,一个完整工程项目的C程序通常会由多个源程序文件组成,每一个文件中又可以包含多个函数。模块的组装既涉及到多个源文件的连接问题,也涉及到实现具体模块的函数之间的连接调用关系。本节我们将讨论使用文件包含#include命令来实现多个源程序文件之间的连接,同时讨论模块之间的各种连接情况。
文件包含与头文件的使用文件包含是指一个源文件可以将另一个源文件的全部内容包含进来,它是一个编译预处理命令。编译预处理命令是在程序编译之前进行的工作,不属于程序中的可执行语句,因此也不占用程序的运行时间。编译预处理命令都以“#”号打头,一般放在源程序首部,单独占源程序中的一行,行末不必加分号。C语言提供的预处理指令主要有三种:宏、文件包含、条件编译。
文件包含命令一般格式为:
# include <包含文件名>
或者
# include,包含文件名”
其中include是文件包含命令的关键字,文件名是指要被包含的源文件的文件名,可以是扩展名为.c的C语言源程序文件,也可以是扩展名为.h的库函数头文件。
前面例题中曾多次使用include命令包含系统库函数头文件,例如使用 #include <stdio.h>,包含系统库函数头文件stdio.h,这样就可以将此头文件中的全部内容包含到现有文件中。stdio.h头文件中大多都是系统定义好的关于输入输出函数的函数声明语句,以及一些使用这些函数时需要用到的符号常量的定义,此外还有部分条件编译语句。使用了文件包含命令,我们就可以使用系统库函数功能,不必自己亲手编写许多模块功能。 C语言中类似于stdio.h这样的头文件还有许多,每一个头文件均包含了某一类的系统功能,例如string.h头文件包含了字符串操作函数,math.h包含了数学功能函数等。知道了这些,我们就可以充分利用已有的系统资源,提高程序的开发效率。
然而头文件的使用并不限于对系统资源的利用,我们在进行大型项目开发时,将程序功能分解为许多功能模块,模块通常都是用函数来编程实现的。在使用模块功能时,我们不需要将模块函数一一声明,通常会定义一些头文件,在头文件中进行相关函数的声明,或者声明一些许多模块都能用到的系统符号常量。这样我们在使用这些模块功能时,只需要包含已定义好的头文件,即可方便的使用这些模块功能了;同时头文件中定义的符号常量,也便于整个系统所使用的符号常量的含义和功能统一,因为当不同的源程序文件包含了同一个头文件时,它们便拥有了相同的符号常量定义。
注意,头文件可以在C语言的源程序编辑环境中编写,也可以用记事本等文本编辑软件进行编写,但格式必须为文本文件。头文件的扩展名通常为.h,我们自己编写头文件时,文件保存的扩展名可以为.h,也可以是其它扩展名,但文件格式必须为文本文件。
用文件包含可以实现文件的拼接,即从磁盘中读取要包含的文件,然后把文件的内容插入到源程序中编译预处理命令行的位置上,使它成为源文件的一部分,然后再对合并的源文件进行编译。
【说明】
使用文件包含时文件名两侧的< >与,”的区别是:< >表示直接到指定的标准包含文件目录中寻找包含文件;,”表示先在当前目录寻找,如果找不到,再到标准包含文件目录寻找。
使用,”时,头文件包含名可加路径。例如:
# include,d:\teach\include\compute.h”
一个#include命令只能指定一个被包含的文件,如果要包含n个文件,要用n个#include命令。
如果file2.c中包含了file3.c,而file1.c中需要包含使用file2.c和file3.c的内容,则file1.c直接包含file2.c即可使用file3.c中的内容。
【例6.1】分析以下程序的功能和结构。
/* file.c */
#include "myhead.h"
main()
{
int i,n=0;
char str[100];
gets(str);
for (i=0;str[i]!='\0';i++)
{
if (isword(str[i])==TRUE)
n++;
}
printf("There are %d words\n",n);
getch();
}
/* myhead.h 的文件内容如下*/
#include <stdio.h>
#define TRUE 1
#define FLASE 0
extern int isword(char c);
/* myfunc.c 源文件内容如下*/
int isword(char c)
{
if(c==' '||c=='\n'||c=='\t')
return TRUE;
else
return FLASE;
}
【例题分析】
以上实例中共有3个源文件,其中一个是file.c,为主程序文件,一个是myhead.h,是用户自定义的头文件,另一个是myfunc.c,是实现模块功能的自定义函数文件,当然此文件中还可包含多个自定义函数。
该实例的功能是统计一串字符中的单词个数,模块isword的功能是判断一个字符是否是单词结束字符,该函数代码单独作为一个文件存储在磁盘上,文件名为myfunc.c。实例中通过自定义的头文件myhead.h将模块功能函数包含在内,并进行有关的系统符号常量说明,然后在主程序文件file.c中通过包含头文件myhead.h从而使用模块功能isword和系统符号常量TRUE和FALSE来判断单词并统计单词个数。
例6.1只是一个简单使用举例,在实际开发项目时,我们可以将具有相似功能的模块函数代码编写在同一个文件中,也可将具体的模块代码编译成目标代码,以链接库的形式供主程序使用,同时定义相关的头文件进行相关函数的声明,这样系统就可以方便的使用由不同开发人员编写的各种模块功能了。
【总结】使用文件包含和头文件的好处:
可以充分利用系统资源,提高编程效率。
可实现多个源程序文件的拼接,将模块根据程序功能进行组装。
将自定义函数的说明编写为头文件,然后包含此头文件,可使主程序简洁,模块结构明确。
将不同功能的模块分别声明为不同的头文件,可以实现函数功能的自由组合。
模块间的连接大型程序通常都由多个模块组成,除了文件包含的方法实现模块文件间的互相包含外,还有模块函数间通过参数或返回值等形式构成的连接,我们在进行模块组装时需要对模块间的连接进行充分的认识,以便在进行模块函数设计时,根据不同的情况选择不同的连接方式。
模块函数间的连接可以分为短暂连接和长久连接。所谓的短暂连接是指只有在模块函数被调用时通过函数参数的传递或函数返回值和其他模块发生的连接关系,这是模块连接所采用最普遍的连接方式,也叫做临时连接,当函数调用结束后连接关系就消失了。而长久连接是通过全局变量或静态存储的变量和其他模块之间产生的连接关系,这种连接类似于人体的筋脉和血管等,使不同的模块之间产生相互的作用,这种连接是长期存在的,当函数一次调用结束后连接仍然存在,只有当整个程序运行全部结束时连接关系才取消。
模块间的短暂连接使得模块的功能比较独立,模块调用和组装非常灵活。通常模块间的短暂连接以三种形式存在:
普通参数。函数调用时主调函数以实际参数赋值给函数的形式参数,完整模块功能。
返回值。函数调用结束后,返回指定的值给主调函数。
指针参数。指针形式的参数使得函数调用时,函数通过指针直接访问主调函数中变量的内存单元,以此取得变量值或将函数处理结果放到指定的内存单元中。
模块间的长久连接使得模块间衔接紧密,模块间的耦合加强,使得模块的独立性下降,各模块间通过全局变量等形式的连接产生了相互的影响。 在某些情况下可以使用此种形式的连接,例如模块对统一的系统变量产生作用,使系统变量的值发生改变,而这种改变需要保留到下一个程序运行时刻等。一般情况不采用长久连接形式来完成模块间的连接和组装。模块间的长久连接以如下两种形式表现:
全局变量。全局变量又成为外部变量,是定义在函数外部的变量,可以被若干个函数模块所访问,每一个程序模块访问全局变量改变其值后,全局变量的值就发生了永久的改变,即函数调用结束后这种值的改变仍然生效。全局变量又分为文件内部的全局变量和文件间的全局变量。文件内部的全局变量其作用域是从定义它的位置开始直到源文件结束,即在此段之间的函数都可访问该全局变量,文件内部的全局变量使得一个文件内部的函数之间可以相互影响和作用。文件间的全局变量是指将全局变量声明为extern,以供多个源程序文件访问,这样就将全局变量的作用域范围扩大了,扩大到了多个文件内部,这样就在多个源程序文件通过全局变量实现了连接。实际使用时需根据系统功能要求有选择的使用全局变量。使用全局变量时一定要慎重,因为任何模块对全局变量的修改都是长久的,这样有可能会带来副作用,使不同模块访问全局变量时对值的大小产生了歧义。
static静态存储类。函数内部用static声明的静态变量是存储在系统的静态存储区的,其生存期较长,不随函数的调用结束而释放,而是在整个程序运行期间一直都保持有效。这样该函数的多次被调用就通过静态变量和主调函数之间或多个不同的主调函数之间产生了长期的作用关系。这种改变虽然是反映在函数内部的静态变量上的,但也是长期存在的,因此也属于模块间的长久连接。在编程时要根据情况有选择的使用,在特殊的情况下巧妙的利用函数的静态变量可以实现特殊功能,但大多数情况下应避免使用静态变量使函数间产生长久连接关系。
标识符的一致性在由许多分别编译的单元所组成的程序中,需要考虑的问题是如何保证在各个单元中使用的标识符的每一个实例都能正确地与其所指定的程序实体相联系。使它们在各编译单元中的名字和类型的定义与使用时严格一致,这是模块连接成功与否的关键。
程序的一致性尽管原则上可以由一些高级的连接程序(也称装载程序)来保证,但是C语言鼓励程序员进行显式连接,以防不测。
C系统进行编译时,要对统一编译单元(同一源文件)内使用的变量及函数的类型进行一致性检查。当程序由多个文件组成时,要对不同模块内定义及使用的变量及函数的一致性进行检查,以便实现类型的安全连接。一个模块中的程序实体的标识符的连接属性用于确定该实体是否可被其他模块引用。每个标识符要么具有连接性,要么无连接性。
连接属性与标识符的作用域密切相关,并且由声明的布局与格式决定。对于动态变量是无连接性的,具有连接性的变量是静态变量和全局变量。全局变量的作用域范围较大,属于公有变量,可以被许多函数访问使用,但注意当函数内部有与全局变量同名的局部变量时,系统默认使用的是函数内部的局部变量,因此应注意函数内部变量的命名与全局变量的一致性的冲突。对于静态变量其作用域范围仅限于本函数内部,属于私有变量,但其生存期较长,在整个程序运行期间均有效,这一点应加以注意。
在实际开发系统时,应注意标识符的一致性问题,在进行模块设计初期就要考虑到标识符间的连接属性和标识符的一致性问题。当某个标识符在各模块间具有较强的连接作用时,该标识符的意义和类型一定要一致,而且要避免函数内部变量的命名与其发生同名冲突。当某些标识符需被某个特定的模块单独且长期的使用时,应该把它定义为私有性质的变量,即声明为函数内部的static类型变量。
条件编译一般情况下,源程序中所有的内容都参加编译,但是有时希望其中一部分内容只在满足一定条件下进行编译,也就是对一部分内容指定编译条件,这就是条件编译。
条件编译的作用:
系统适应性强功能灵活条件编译命今的几种形式:
(1) #ifdef 标识符
程序段1
#else
程序段2
#endif
其作用是:当标识符已经被定义过(一般用#define命令定义),则对程序段1进行编译,否则对程序段2进行编译。其中#else可以没有,即
#ifdef标识符
程序段1
#endif
源程序段部分可以是任何预处理命令、C语句、或其它任何语法成分。
(2) #ifndef标识符
程序段1
#else
程序段2
#endif
其作用是:当标识符未被定义则对程序段1进行编译,否则则对程序段2进行编译,其中#else可以没有,它的作用恰好与第一种形式相反。
(3) #if 表达式
程序段1
#else
程序段2
#endif
其作用是:当指定的表达式值为真时编译程序段1,否则编译程序段2。
【例6.2】求一组整数的最大值、最小值及平均值。通过条件编译使本程序在调试时和正式运行时具有不同特征和功能。
在设计程序时,应考虑到调试程序和正式运行程序的不同。在调试程序时,数据输入要少、简单,加快程序调试过程,同时输出信息要多,有利于分析程序。正式运行程序时,输人数据要真实,不输出某些中间结果。
源程序如下:
#define N 10
#define test 1
main()
{ int i,max,min,a[N]
float avg;
#if test
for(i=0;i<N; i++)
a[i]=i+5; /* 调试时通过赋值得数组a的各元素值 */
#else /* 正式运行通过输入,得数组a的各元素值 */
{ printf(“Please enter array a[0]~a[%d]:”,N-1);
for(i=0;i<N; i++)
scanf(“%d”,&a[i]);
}
#endif
max=a[0]; min=a[0]; avg=a[0];
for(i=0;i<N; i++)
{ if(max<a[i]) max=a[i];
if(min>a[i]) min=a[i];
avg=avg+a[i];
}
#if test
printf(“sum=%f\n”,avg); /* 调试时查看累加和 */
#endif
printf(“max=%d,min=%d,avg=%f\n”,max,min,avg/N);
}
【分析】
条件编译的使用:定义符号常量test,如果是调试程序,则将test置1,则通过条件编译使数组各元素的值通过赋值形式快速获得,同时输出程序的中间结果,即为了求平均值首先得到的累加和。而正式运行程序时则编译另外的部分代码,使数组各元素的值通过输入函数,让用户从键盘输入,同时也省略了部分中间结果(即累加和)的输出。
7.3 模块设计风格简述
本节所讨论的模块设计风格是指实现模块功能代码时经常采用的一些设计风格。我们在编程时应注意养成良好的编程习惯和编程风格,良好的模块设计风格可以使程序便于阅读、理解和调试,从而提高编程效率。在大型程序开发时,注重编程风格尤为重要。大型程序通常是由不同的开发人员编写的,不同编程人员编写的程序代码必须能够相互理解、相互通融,表达一致等,这样必然要有一个统一的、良好的模块设计风格。
模块的设计风格包含很多方面,我们在此主要从模块的数据风格、标识符风格、算法风格、输入/输出风格、书写风格等几方面加以总结和讨论。
数据风格
1.编程时所使用的数据类型和数据结构要清晰、明确,同时尽可能的用接近现实意义中的类型来表达。例如求圆的面积时,定义的圆半径变量和圆面积变量根据现实意义应定义为实数类型。
2.指针类型是C语言特有的数据类型,它使得程序可以通过指针直接访问某些内存单元,指针的使用一方面增强了程序的功能,同时也带来了一定的副作用。如果对指针的含义弄不清楚、或二级指针等复杂使用,容易出现错误的内存单元访问情形,导致程序运行结果错误,甚至对系统造成破坏,因此应有限制的使用指针。
3.使用数组形式来操作一组数据时,注意数组的长度与实际数据长度的关系,尤其是对于字符数组,应考虑到各种情况的数组长度包容关系,同时又尽可能的节省内存空间。
4.在用结构体和共用体类型来表达复杂结构的数据类型时,要注意类型定义的一致性,通常将数据类型的定义放在头文件中,或放在函数外部定义,这样定义的类型具有公有性质,可以被众多模块访问使用。
5.对于构造数据类型或对于具体开发来说有特殊意义的基本数据类型,可以使用typedef命令重新定义类型名,以便于和现实意义相统一,方便理解。
6.3.2 标识符风格
1.见名知意。在进行变量命名或函数命名时,应注意做到“见名知意”,即选择有含意的英文单词(或其它缩写)作标识符,如count,name,day,calss,city,area等。同时可以采用较长的描述性名字来命名变量或函数等,如驼峰式命名法或加下划线命名法。
例如:PrintEmployeePaychecks
Print_Employee_Paychecks
2.附加类型关键字。变量标识符可在变量名后加一下划线,后面附上该变量的类型(或类型缩写),这样容易理解变量类型和含义,使程序容易阅读和调试。例如变量day_int,sum_float等。
3,在对函数命名时,有时可以加上主体功能模块的缩写以表示该模块的适用范围。如age_day(),age_year(),age_month()等。或采用动宾结构来描述函数标识符,如ResetCounter();ReleaseLock();等。
4.使用符号常量。符号常量是一种无参宏,用#define命令定义。有时为了提高程序的可读性、增强程序的可修改性,可在程序开始之处用符号常量的形式来表达某种常量信息。如圆周率∏值,数组长度的定义N等,均可在主程序开始处定义为符号常量。同时对于多个模块都要访问的具有现实意义的常量,也应定义为符号常量,供多个模块访问,这样含义明确,易于理解。
6.3.3 算法风格
1.算法要简洁、明了,少使用技巧。
例如两个变量互换值的算法可以有两种算法实现。
算法1:a=a+b; b=a-b; a=a-b;
算法2:定义一个临时变量temp,temp=a; a=b; b=temp;
比较上述两种算法,算法2简洁明了,易于阅读和理解。
2.尽量避免使用多重循环嵌套或条件嵌套结构。
3.在条件或循环结构中尽量避免采用“非”条件测试。
4.尽量避免复杂条件测试
5.语句和表达式要清晰、易读,如表达式x=(y=2)+(z=5)写成下面的形式会使算法更清晰容易理解,y=2; z=5; z=y+z;
6.充分利用系统库函数功能。
7.要注意浮点运算误差,在编程时对于实数类型的数据要避免进行精确相等的比较。例如:10.0* 0.1 一般与1.0不相等。再例如实数与0的比较通常要近似表达为fabs(a)<=1e-6等。
6.3.4 输入/输出风格输入/输出风格决定了用户可以接受程序的程度。好的输入输出风格能使用户工作在轻松的环境之中,提高他们的工作效率。在程序设计时,考虑输入输出风格的具体做法有:
1.提高输入操作的坚固性,以适当的方式对输入数据进行检验,以确认每个输入数据的有效性;对无效数据,也能给出必要的提示,而不导致死机或输出错误结果。
2.输入格式简单、单一、统一,容易核对。
3.输入格式与用户水平相对应;
4.输入时能给用户以提示,指明可使用的选择和边界值。
5.输出格式应满足用户要求,符合使用意图。
6.对输出操作有必要的提示。
7.简化用户操作,减少用户出错处理。
6.3.5 书写风格程序代码的书写风格的核心目标是提高程序的可读性。一般来说包括以下几个主要方面。
1.使用足够的注释。
为了帮助阅读理解程序,应当使用足够的注释。特别要注意在下列地方使用注释:
一个文件的文件名程序或函数的功能变量的用途特殊数据结构的特点和实现方法特殊算法技巧任何容易误解或别人不容易看得懂的地方。
2.缩进格式。
对于层次不同的控制结构,分别缩进几格书写,可以使程序结构层次清晰,容易观察程序的控制结构和进行算法分析。例如如下程序段的缩进格式:
struct student *creat(void)
{
struct student *head,*p1,*p2;
float s;
n=0;
head=NULL; /*初始化链表为空*/
p1=p2=(struct student *)malloc(LEN); /*开辟第一个新单元 */
scanf("%ld%f",&p1->num,&s);
p1->score=s; /*读入第一个结点的数据 */
while(p1->num!=0)
{ n=n+1;
if(n==1)
head=p1;
else
p2->next=p1;
p2=p1;
p1=(struct student *)malloc(LEN); /* 开辟新结点 */
scanf("%ld%f",&p1->num,&p1->score); /* 读入新结点的数据 */
}
p2->next=NULL; /*链表建立结束*/
return(head);
}
3.括号风格。
花括号。使用缩进书写格式,同时选择统一的语句花括号将有关联的多条语句包括为一个整体,最能体现具体的模块功能,例如分支模块、循环模块等,可以突出结构的层次关系。
圆括号。使用冗余的圆括号使表达式易读。
如把表达式:c=b*=a+2改写为c=(b*=(a+2))。
7.4 大型程序开发的项目管理
6.4.1 项目管理器程序项目是指由多个文件组成的大程序。在编制稍大一些的程序时,常常会碰到一个程序包含几个甚至多个文件,而又常常要不断的对其中的一些文件进行调试、编译、连接等情形。为了节省时间,提高效率,最好只编译、连接那些修改过的文件。C语言编辑环境提供的程序项目管理器(project)用于对由多个文件组成的程序进行编译与连接的管理。当程序项目中的某些文件做了修改后,程序项目管理器能自动地识别出哪些文件需要重新编译,可以减少编译过程中不必要的麻烦。
有关项目管理器的各项功能在C语言的编译环境里选择Project主菜单下的各项子菜单命令即可。
6.4.2 用项目管理器开发程序项目的步骤
1.程序项目文件的组成和命名。
项目管理器把组成项目的多个文件作为一个整体来处理,这个整体就是项目文件,因此在组装程序项目文件之前要先给项目文件命名。通常项目文件扩展名为.prj,例如命名一个项目文件名为myprog1.prj,则该程序项目的可执行文件名为myprog1.exe,如果选择生成映象文件,映象文件名为myprog1.map。
选择项目管理器的各项功能进行项目管理。
按Alt+P键拉下Project主菜单。Project菜单中的命令如图5.1所示,Project菜单中各命令作用如表5.1所示。

图5.1 Project 菜单
表5.1 Project菜单中各命令的用途命 令
作 用
Project name
弹出对话框,要求输入.PRJ文件名
Break make on
弹出一个菜单,继续提供4个中止Make的条件选项。
Warning:设定遇到警告错误时,中止Make
Error:设定遇到错误时,中止Make
Fatal Error:设定遇到致命错误时,中止Make
Link:设定在链接前,中止Make
Auto dependencies
TC在编译时,将.C文件的日期和时间放在了.obj文件中。这条命令用于设置是否要检查.c文件与相应的.obj文件的日期和时间关系。
on:自动检查。不一致,就重新编译
off:不检查
Clear project
清除Project name并重置消息窗口
Remove messages
清除消息窗口中的错误信息
6.4.3 项目管理器的使用技巧为提高编译效率,可采用如下方法组成项目文件:
将经过反复调试运行、已经成熟的常用函数编译成.lib文件,放进自己的库文件,再把该库文件列入程序项目文件中。
将常用的、暂不需修改的文件编译成.obj(目标)文件,代替其源文件列入程序项目文件中。
将正在调试或有可能还需要改动的文件的.c(源)文件列入程序项目文件中。
应当注意以下几点:
程序项目文件含有环境信息。当环境(如盘号、路径、设置的参数)等改变时,应重新建立程序项目文件,否则可能出现莫名其妙的错误。
一个程序项目文件要涉及许多头文件,因此要注意它们之间的一致性。
一个程序项目文件有且仅有一个主函数。
7.5 几个应用程序设计实例
【例6.3】用C语言实现艺术清屏。
#include<stdio.h>
#include<dos.h>
#include<conio.h>
void goto_xy(int x,int y);
void dcls(int x1,int x2,int y1,int y2);
void bcls(int x1,int x2,int y1,int y2);
void recls(int x1,int x2,int y1,int y2);
void zcls(int x1,int x2,int y1,int y2);
void puta(void);
/*--------------演示程序---------------------*/
main()
{ puta();
getch();
dcls(0,4,0,79);
getch();
puta();
getch();
bcls(0,25,0,79);
getch();
puta();
getch();
zcls(0,25,0,79);
getch();
}
/*********center clear screen(中心清屏)***********/
void zcls(int x1,int x2,int y1,int y2)
{ int x00,y00,x0,y0,i,d;
if((y2-y1)>(x2-x1))
{
d=(x2-x1)/2;
x0=(x1+x2)/2;
y0=y1+d;
y00=y2-d;
for(i=0;i<(d+1);i++)
recls((x0-i),(x00+i),(y0-i),(y00+i));
}
else
{
d=(y2-y1)/2;
y0=(y1+y2)/2;
x0=x1+d;
x00=x2-d;
for(i=0;i<d+1;i++)
recls(x0-i,x00+i,y0-i,y00+i);
}
}
/********* clear rectangle side(矩形边清屏)***********/
void recls(int x1,int x2,int y1,int y2)
{ int i,j;
for(i=y1;i<y2;i++)
{
goto_xy(x1,i);
putchar(' ');
goto_xy(x2,i);
putchar(' ');
}
for(j=x1;j<x2;j++)
{
goto_xy(i,y1);
putchar(' ');
goto_xy(j,y2);
putchar(' ');
}
}
/**********close screen clear*****闭幕式清屏*************/
void bcls(int x1,int x2,int y1,int y2)
{ int t,s,j;
t=y1;
s=y2;
for(t=y1;t<(y1+y2)/2;t++,s--)
for(j=x1;j<x2;j++)
{ goto_xy(j,t);
putchar(' ');
goto_xy(j,s);
putchar(' ');
delay(1);
}
}
/*****bottom screen clear(自下清屏)*************/
void dcls(int x1,int x2,int y1,int y2)
{ int t,s,j,i;
t=s=(y1+y2)/2;
for(j=x2;j>x1;j--)
for(i=y1;i<y2;i++)
{
goto_xy(j,i);
putchar(' ');
delay(1);
}
}
/******************设置光标子函数******************/
void goto_xy(int x,int y)
{ union REGS r;
r.h.ah=2;
r.h.dl=y;
r.h.dh=x;
r.h.bh=0;
int86(0x10,&r,&r);
}
/**********在屏幕上打出一连串的a字母用于演示程序*******/
void puta(void)
{
int i,j;
for(i=0;i<24;i++)
{
for(j=0;j<79;j++)
{
goto_xy(i,j);
printf("a");
}
}
}
【例6.4】最短交通路径程序。
#include "string.h"
#include "stdio.h"
typedef struct ArcCell
{
int adj; /*相邻接的城市序号*/
}ArcCell; /*定义边的类型*/
typedef struct VertexType
{
int number; /*城市序号*/
char *city; /*城市名称*/
}VertexType; /*定义顶点的类型*/
typedef struct
{
VertexType vex[25]; /*图中的顶点,即为城市*/
ArcCell arcs[25][25]; /*图中的边,即为城市间的距离*/
int vexnum,arcnum; /*顶点数,边数*/
}MGraph; /*定义图的类型*/
MGraph G; /*把图定义为全局变量*/
int P[25][25];
long int D[25];
void CreateUDN(v,a) /*造图函数*/
int v,a;
{ int i,j;
G.vexnum=v;
G.arcnum=a;
for(i=0;i<G.vexnum;++i)
G.vex[i].number=i;
/*下边是城市名*/
G.vex[0].city="乌鲁木齐";
G.vex[1].city="西宁";
G.vex[2].city="兰州";
G.vex[3].city="呼和浩特";
G.vex[4].city="北京";
G.vex[5].city="天津";
G.vex[6].city="沈阳";
G.vex[7].city="长春";
G.vex[8].city="哈尔滨";
G.vex[9].city="大连";
G.vex[10].city="西安";
G.vex[11].city="郑州";
G.vex[12].city="徐州";
G.vex[13].city="成都";
G.vex[14].city="武汉";
G.vex[15].city="上海";
G.vex[16].city="昆明";
G.vex[17].city="贵州";
G.vex[18].city="株洲";
G.vex[19].city="南昌";
G.vex[20].city="福州";
G.vex[21].city="柳州";
G.vex[22].city="南宁";
G.vex[23].city="广州";
G.vex[24].city="深圳";
/*这里把所有的边假定为20000,含义是城市间不可到达*/
for(i=0;i<G.vexnum;++i)
for(j=0;j<G.vexnum;++j)
G.arcs[i][j].adj=20000;
/*下边是可直接到达的城市间的距离,由于两个城市间距离是互相的,
所以要对图中对称的边同时赋值。*/
G.arcs[0][2].adj=G.arcs[2][0].adj=1892;
G.arcs[1][2].adj=G.arcs[2][1].adj=216;
G.arcs[2][3].adj=G.arcs[3][2].adj=1145;
G.arcs[2][10].adj=G.arcs[10][2].adj=676;
G.arcs[3][4].adj=G.arcs[4][3].adj=668;
G.arcs[4][5].adj=G.arcs[5][4].adj=137;
G.arcs[5][6].adj=G.arcs[6][5].adj=704;
G.arcs[6][7].adj=G.arcs[7][6].adj=305;
G.arcs[7][8].adj=G.arcs[8][7].adj=242;
G.arcs[6][9].adj=G.arcs[9][6].adj=397;
G.arcs[4][11].adj=G.arcs[11][4].adj=695;
G.arcs[5][12].adj=G.arcs[12][5].adj=674;
G.arcs[10][13].adj=G.arcs[13][10].adj=842;
G.arcs[11][14].adj=G.arcs[14][11].adj=534;
G.arcs[12][15].adj=G.arcs[15][12].adj=651;
G.arcs[13][16].adj=G.arcs[16][13].adj=1100;
G.arcs[13][17].adj=G.arcs[17][13].adj=967;
G.arcs[14][18].adj=G.arcs[18][14].adj=409;
G.arcs[17][18].adj=G.arcs[18][17].adj=902;
G.arcs[15][19].adj=G.arcs[19][15].adj=825;
G.arcs[18][19].adj=G.arcs[19][18].adj=367;
G.arcs[19][20].adj=G.arcs[20][19].adj=622;
G.arcs[17][21].adj=G.arcs[21][17].adj=607;
G.arcs[18][21].adj=G.arcs[21][18].adj=672;
G.arcs[21][22].adj=G.arcs[22][21].adj=255;
G.arcs[18][23].adj=G.arcs[23][18].adj=675;
G.arcs[23][24].adj=G.arcs[24][23].adj=140;
G.arcs[16][17].adj=G.arcs[17][16].adj=639;
G.arcs[10][11].adj=G.arcs[11][10].adj=511;
G.arcs[11][12].adj=G.arcs[12][11].adj=349;
}
void narrate() /*说明函数*/
{ int i,k=0;
printf("\n**********************欢迎使用公里交通图程序!***********************\n");
printf("\n这里是城市列表:\n\n");
for(i=0;i<25;i++)
{ printf("(%2d)%-15s",i,G.vex[i].city); /*输出城市列表*/
k=k+1;
if(k%4==0) printf("\n");
}
}
void ShortestPath(num) /*最短路径函数*/
int num;
{ int v,w,i,t;
int final[25];
int min;
for(v=0;v<25;++v)
{ final[v]=0;D[v]=G.arcs[num][v].adj;
for(w=0;w<25;++w)
P[v][w]=0;
if(D[v]<20000)
{ P[v][num]=1;
P[v][v]=1;
}
}
D[num]=0;final[num]=1;
for(i=0;i<25;++i)
{ min=20000;
for(w=0;w<25;++w)
if(!final[w])
if(D[w]<min)
{v=w; min=D[w]; }
final[v]=1;
for(w=0;w<25;++w)
if(!final[w]&&((min+G.arcs[v][w].adj)<D[w]))
{ D[w]=min+G.arcs[v][w].adj;
for(t=0;t<25;t++)
P[w][t]=P[v][t];
P[w][w]=1;
}
}
}
void output(city1,city2) /*输出函数*/
int city1;
int city2;
{ int a,b,c,d,q=0;
a=city2;
if(a!=city1)
{ printf("\n从%s到%s的最短路径是",G.vex[city1].city,G.vex[city2].city);
printf("(最短距离为 %dkm.)\n\t",D[a]);
printf("%s",G.vex[city1].city);
d=city1;
for(c=0;c<25;++c)
{ gate,; /*标号,可以作为goto语句跳转的位置*/
P[a][city1]=0;
for(b=0;b<25;b++)
{ if(G.arcs[d][b].adj<20000&&P[a][b])
{ printf("-->%s",G.vex[b].city);q=q+1;
P[a][b]=0;
d=b;
if(q%8==0)
printf("\n");
goto gate;
}
}
}
}
}
void main() /*主函数*/
{ int v0,v1;
CreateUDN(25,30);
narrate();
printf("\n\n请选择起始城市!\n");
scanf("%d",&v0);
printf("请选择终到城市!\n");
scanf("%d",&v1);
ShortestPath(v0); /*计算两个城市之间的最短路径*/
output(v0,v1); /*输出结果*/
printf("\n");
}
【例6.5】屏幕上的时钟程序。
#include<math.h>
#include<dos.h>
#include<graphics.h>
#define CENTERX 320 /*表盘中心位置*/
#define CENTERY 175
#define CLICK 100 /*喀嗒声频率*/
#define CLICKDELAY 30 /*喀嗒声延时*/
#define HEBEEP 10000 /*高声频率*/
#define LOWBEEP 500 /*低声频率*/
#define BEEPDELAY 200 /*报时声延时*/
/*表盘刻度形状*/
int Mrk_1[8]={-5,-160,5,-160,5,-130,-5,-130,};
int Mrk_2[8]={-5,-160,5,-160,2,-130,-2-130,};
/*时针形状*/
int HourHand[8]={-3,-100,3,-120,4,10,-4,10};
/*分针形状*/
int MiHand[8]={-3,-120,3,-120,4,10,-4,10};
/*秒针形状*/
int SecHand[8]={-2,-150,2,-150,3,10,-3,10};
/*发出喀嗒声*/
void Click()
{ sound(CLICK);
delay(CLICKDELAY);
nosound();
}
/*高声报时*/
void HighBeep()
{ sound(HEBEEP);
delay(BEEPDELAY);
nosound;
}
/*低声报时*/
void LowBeep()
{ sound(LOWBEEP);
}
/*按任意角度画多边形*/
void DrawPoly(int *data,int angle,int color)
{ int usedata[8];
float sinang,cosang;
int i;
sinang=sin((float)angle/180*3.14);
cosang=cos((float)angle/180*3.14);
for(i=0;i<8;i+=2)
{ usedata[i] =CENTERX+ cosang*data[i]-sinang*data[i+1]+.5;
usedata[i+1]=CENTERY+sinang*data[i]+cosang*data[i+1]+.5;
}
setfillstyle(SOLID_FILL,color);
fillpoly(4,usedata);
}
/*画表盘*/
void DrawClock(struct time *cutime)
{ int ang;
float hourrate,minrate,secrate;
setbkcolor(BLUE);
cleardevice();
setcolor(WHITE);
/* 画刻度*/
for(ang=0;ang<360;ang+=90)
{ DrawPoly(Mrk_1,ang,WHITE);
DrawPoly(Mrk_2,ang+30,WHITE);
DrawPoly(Mrk_2,ang+60,WHITE);
}
secrate=(float)cutime->ti_sec/60;
minrate=((float)cutime->ti_min+secrate)/60;
hourrate=(((float)cutime->ti_hour/12)+minrate)/12;
ang=hourrate*360;
DrawPoly(HourHand,ang,YELLOW);/*画时针*/
ang=minrate*360;
DrawPoly(MiHand,ang,GREEN);/*画分针*/
ang=secrate*360;
DrawPoly(SecHand,ang,RED);/*画秒针*/
}
main()
{ int gdriver=EGA,
int gmode=EGAHI;
int curpage;
struct time curtime,newtime ;
initgraph(&gdriver,&gmode,"f:\\tc30\\bgi");
setbkcolor(BLUE);
cleardevice();
gettime(&curtime);
curpage=0;
DrawClock(&curtime);
while(1)
{ if(kbhit())
break; /*按任意键退出*/
gettime(&newtime); /*检测系统时间*/
if(newtime.ti_sec!=curtime.ti_sec) /*每1秒更新一次时间*/
{ if(curpage==0)
curpage=1;
else
curpage=0;
curtime=newtime; /*设置绘图页*/
setactivepage(curpage); /*在图页上画表盘*/
DrawClock(&curtime); /*设置绘图页为当前可见页*/
setvisualpage(curpage); /*0分0秒高声报时*/
if(newtime.ti_min==0&&newtime.ti_sec==0)
HighBeep(); /* 59分55至秒时低声报时*/
else if(newtime.ti_min==59&&newtime.ti_sec<=59)
LowBeep(); /*其他时间只发出喀嗒声*/
else
Click();
}
}
closegraph();
}
习题一、填空题
1、按照软件工程的思想,系统设计包括四个方面的内容,分别是____________、_____________、____________、_____________。
2、模块设计的基本原则是“______________________”。
3、C语言中用_____________实现模块的定义。
4、模块的组装既涉及到多个________的连接问题,也涉及到实现具体模块的________之间的连接调用关系。
5、使用__________预处理命令来实现多个源程序文件之间的连接,
6、文件包含是指__________________________。
7、若想使用C语言提供的数学计算方面的系统功能,应包含的头文件是____________。
8、模块函数间的连接可以分为________连接和________连接。
9、长久连接是通过_________或_____________和其他模块之间产生的连接关系。
10、模块间的短暂连接以三种形式存在:__________、___________、____________。
11、模块间的___________使得模块的功能比较独立,模块调用和组装非常灵活;而模块间的___________使得模块间衔接紧密,模块间的耦合加强。
12、全局变量属于公有变量,可以被许多函数访问使用,但注意当函数内部有与全局变量同名的局部变量时,系统默认使用的是____________。
13、源程序中所有的内容都参加编译,但是有时希望其中一部分内容只在满足一定条件下进行编译,也就是对一部分内容指定编译条件,这就是__________。
14、C语言中使用_________进行程序项目的管理。
15、如果命名一个项目文件名为myprog1.prj,则该程序项目的可执行文件名为__________。
二、简答题
1、论述使用模块化方法开发程序的好处。
2、简述什么是短暂连接?什么是永久连接?
3、请论述模块设计的标识符风格。
4、什么是程序项目?
C语言实验教程实验一 C语言运行环境和C程序初步一、目的和要求
1,熟悉C程序编辑环境,掌握打开菜单的方法,掌握File菜单的使用,大致了解其它菜单各选项的作用。
2,熟悉编写一个C程序的上机过程(编辑、编译、链接和运行)。
二、实验内容
1 进入Turbo C
在TC主目录中存放了两个可执行文件:TC.EXE和TCC.EXE。其中TC.EXE是将编辑、编译、链接、调试和运行集成为一体的基本模块;TCC.EXE则提供了某些补充功能,例如可以在程序中嵌入汇编代码等。一般情况下只需要用到TC.EXE。
由Windows平台进入Turbo C环境有下面两种方法:
⑴、通过“我的电脑”或“资源管理器”窗口找到TC文件夹,双击该文件夹中的TC.EXE文件,即可进入Turbo C环境。
⑵、从桌面上选择“开始”→“程序”→“MS-DOS方式”选项,打开MS-DOS方式窗口,在该窗口中的DOS提示符后输入以下DOS命令,进入Turbo C环境。
CD C:\TC<Enter> (将当前目录改变为C:\TC,如果TC主目录放在其它磁盘上,将命令中的“C:”修改为相应的盘符即可)
TC<Enter>
⑶、如果在桌面上有TC.EXE的快捷方式,双击该图标也可进入Turbo C环境。
进入Turbo C后可看到如图1.1所示的Turbo C编辑环境的初始屏幕。

图1.1 Turbo C初始屏幕在图1.1的初始屏幕中,中间的小窗口显示的是Turbo C的版本信息,标明Turbo C的版本号、生产日期和公司名称。按下任一键时,该窗口就会消失,然后出现的就是Turbo C的工作窗口。如图1.2所示。

图1.2 Turbo C工作窗口
Turbo C工作窗口主要分为以下几个子窗口:
⑴、主菜单子窗口:位于工作窗口的顶部。主要包括8个菜单:File(文件)、Edit(编辑)、Run(运行)、Compile(编译)、Project(项目)、Options(选项)、Debug(调试)、Break/watch(断点/监视),每一个主菜单还有下一层子菜单,分别用来实现各种操作。
⑵、编辑子窗口:这是程序设计的主要区域,位于工作窗口的中部、主菜单下方,窗口正上方有“Edit”字样。编辑字窗口的作用是对Turbo C源程序进行输入和编辑。
在编辑子窗口的上部有一行英文:
Line 1 Col 1 Insert Indent Tab Fill Unindent C:NONAME.C
其中Line 1和Col 1表示当前光标的位置在第1行第1列。当光标移动时,Line和Col后面的数字也将随之改变,用于提示用户当前光标所在的位置。
Insert显示时表示当前正处于插入状态,输入的字符将插入到当前光标位置之前。如果没有Insert,表示当前处于覆盖状态,输入的字符将替换当前光标处的字符。按“Insert”键或“Ins”键可在“插入”和“覆盖”两种状态间切换。
最右端显示的是当前正在编辑的文件所在的磁盘和文件名,对新文件自动命名为NONAME.C。如果是一个已经存在的文件或新文件已经存盘,则在该位置上显示的不再是NONAME.C,而是该文件的名字。
⑶、信息子窗口:位于编辑子窗口下面,用于显示编译和链接时的有关信息,如错误提示信息。窗口正上方有“Message”字样。
⑷、功能键提示行:位于屏幕最下方,用于显示一些功能键的作用。包括:
① F1-Help(帮助):任何时候按F1都会显示帮助信息。
② F5-Zoom(分区控制):如果当前在编辑窗口工作,也就是说编辑窗口处于激活状态,按F5键将关闭信息窗口(Message窗口),它的作用是扩大编辑窗口,以便容纳和显示较长的源程序;若再按一次F5键,就会重新显示信息窗口。也就是说,反复不断按F5键,将会使编辑窗口在原始大小和最大状态下切换。
如果当前信息窗口是激活的,按F5键就不显示编辑窗口,它的作用是扩大信息窗口,以便能显示较多的信息,便于用户查看。若再按一次F5键,信息窗口就会恢复原来大小。
③ F6-Switch(转换):按F6键就激活信息窗口,此时信息窗口的标题Message以高亮显示,编辑窗口不能工作;再按一次F6键,又将激活编辑窗口,此时可以在编辑窗口中编辑源程序。
④ F7-Trace(跟踪):用于单步跟踪程序的运行情况,每按一次F7键将执行一条语句。
⑤ F8-Step(步进):用于单步执行程序,每按一次F8键将执行一条语句。
⑥ F9-Make(生成目标文件):进行编译和链接,并生成.OBJ文件和.EXE文件,但不能运行程序。
⑦ F10-Menu(菜单):打开主菜单,默认激活第一个菜单File(此时File以反相显示),然后按回车键将打开File菜单。
2 熟悉Turbo C程序运行过程操作小提示:Turbo C菜单命令的选择有两种方法:
方法一:按功能键F10键,将光标移至主菜单名称上(默认为File主菜单),然后可以通过左、右光标键来移动主菜单选项,最后敲入Enter键,拉下该主菜单。或者在编辑状态下直接按下“Alt键+菜单名称中的第一个字母键”也可以拉下该项主菜单。拉下主菜单后,接着进入其下一层菜单的选择过程,此时可用上、下光标键将光标移至某个菜单命令上,然后敲入Enter键执行菜单命令。
方法二:对于常用功能,系统在菜单命令后面提示有快捷功能键,因此可直接在编辑状态下按下功能键执行菜单命令,如F2键为保存程序的命令,F9键为编译程序的命令,Ctrl键+F9键为运行程序的命令,F5键为切换到用户屏幕查看执行结果的命令。
2.1 编辑一个新文件如果要输入和编辑一个新的C程序,需要先打开File菜单,选择其中的New选项(简记为File|New),然后按回车键,编辑窗口的内容将会被清空,光标定位在编辑窗口的左上角(Line 1,Col 1),进入了编辑状态。
然后就可以将已经编写好的源程序输入到编辑窗口,如发现错误可随时修改。编辑过程中会用到前面介绍的编辑命令。如:Insert或Ins键用于切换输入的字符处于“插入”或“覆盖”状态,Delete或Del键用于删除光标处的字符,Backspace用于删除光标之前的字符,Ctrl+Y用于光标所在的行等。
2.2 保存一个新文件输入程序的过程中,为防止断电后程序丢失,应及时将源程序保存起来,方法是:打开File菜单并选择Save选项或直接按快捷键F2,TC就会弹出“Rename NONAME”对话框,如图1.3所示,要求用户指定保存位置和文件名。

图1.3 Rename NONAME对话框在该对话框中,默认保存在用户的工作目录内,文件名为NONAME.C。如果是从Windows目录调用TC.EXE进入Turbo C环境的,则Windows目录就是当前工作目录,在图1.3的对话框中将显示为“D:\TC\NONAME.C”。也就是说,如果不特别指定,源程序将自动保存在用户的工作目录内。默认的文件名是NONAME.C,“.C”是TC源程序文件的默认扩展名。
一般情况下,用户不希望以NONAME.C作为文件名,它不仅不便于辨别,而且每次都用NONAME.C作文件名,第一次保存文件的内容就会被第二次保存的文件的内容取代。当然,文件最好不保存在工作目录内,而指定另外的路径保存,如“D:\MYTC\TEX1.C”。如果不指定路径而只输入文件名,则文件将保存在当前盘当前目录内(一般为工作目录)。
指定文件名保存了文件后,编辑窗口左上角的文件名就自动改为刚才输入的文件名了,如TEX1.C,表示当前正在编辑的文件已不再是NONAME.C了,而是TEX1.C,如图1.4所示。 图1.4 保存后的名为TEX1.C的文件编辑窗口在编辑过程中可以随时将修改过的源程序存盘,同样是选择File|Save选项或者直接按F2,但不再弹出对话框,而是立即将源程序以上次指定的名字存盘。
2.3 打开一个已存在的文件如果要编辑一个已经存在的文件,就需要把它从磁盘中调出来。方法是:打开File菜单,选择Load,或直接按快捷键F3,弹出如图1.5所示的“Load File Name”对话框,要求用户输入装入文件的路径和文件名。

图1.5 打开文件的初始窗口输入文件路径和文件名后,按回车键,该文件就被调入内存,并显示在编辑窗口中。如图1.6所示为打开上面建立的D:\MYTC\TEX1.C文件。

图1.6 打开TEX1.C文件的输入如果输入的文件不存在,则屏幕上将显示一片空白,表示文件无内容,这相当于在指定位置建立了一个有名字的新文件,屏幕右上角将显示新文件名。
如果记不清装输入的源文件名,例如可以输入“D:\MYTC\*.C”,Turbo C就会显示指定目录中所有扩展名为.C的文件名,如图1.7所示。然后利用光标移动键(→←↑↓)将亮条移到需要装入的文件名处,按回车键。

图1.7 显示指定目录下的所有C源程序编辑后可对文件存盘,如果不改变文件名,选择File/Save或直接按F2即可。如果要以另一个文件名存盘,则打开File菜单后,选择Write to选项,按回车键后弹出一个“New Name”对话框,如图1.8,输入新文件的路径及文件名(如TEX2.C)后按回车键,文件就会以新名存盘(原来的文件仍存在),编辑窗口左上角显示的文件名也将自动改为TEX2.C。

图1.8 New Name对话框需要注意的是,如果在“New Name”对话框中未指定路径,TEX2.C将保存在用户工作目录下面。如需保存在其它位置,必须指定路径。
2.4 编译、链接及运行编辑好源程序并存盘后,应当对源程序进行编译、链接和运行。在TC集成环境下,对程序的编译、链接和运行是很方便的。既可像传统方法那样把编译、链接和运行分为三步完成;也可以把编译和链接合为一步,然后再运行;还可把三者合为一步完成。TC既可以对单个模块文件进行编译、链接和运行;也可以对多个模块文件进行以上处理。
一 处理单模块文件例,设有一求两整数最大数的程序为(文件名为SUM.C):
#include <stdio.h>
int max(int x,int y);
void main( )
{ int a,b,c;
scanf("%d%d",&a,&b);
c=max(a,b);
printf("max=%d\n",c);
}
int max(int x,int y)
{ int z;
if (x>y)
z=x;
else
z=y;
return(z);
}
为了处理对它进行编译、链接和运行,可采用下述三种方法之一。
1,分别编译、链接
⑴ 编译按下Alt+C键打开Compile菜单,选择Compile to OBJ选项,如图1.9,此时系统显示出默认的目标文件名,主文件名与当前编译的源程序主文件名相同,扩展名为.OBJ,按回车键后就可进行编译,并在当前工作目录或Options|Directories指定的OUTPUT目录中产生目标文件SUM.OBJ。

图1.9 Compile菜单
⑵ 链接有了目标文件后,还不能直接运行,还要将目标文件与系统提供的库函数和包含文件等链接成一个可执行文件(扩展名为.EXE)。
选择Compile菜单中的Link EXE file选项,按回车键后就可进行链接,在当前工作目录或Options|Directories指定的OUTPUT目录中产生可执行文件SUM.EXE。
需要注意的是,必须先进行编译,得到.OBJ文件后才能进行链接,否则会出错。
⑶ 运行如果在TC环境下运行,可打开Run菜单后选择Run选项或直接按快捷键Ctrl+F9,即可运行程序。
运行完毕后可选择Run|User Screen或按Alt+F5打开用户屏幕,查看运行结果。
如果在DOS下运行,可返回DOS后,在DOS提示符后键入主文件名(如C:\TC\SUM<Enter>)即可运行。
2,编译、链接一步法在图1.9的Compile菜单中选择Make EXE file选项,按回车键,就可一次完成编译和链接,在当前工作目录或Options|Directories指定的OUTPUT目录中生成一个目标文件SUM.OBJ和一个可执行文件SUM.EXE。运行方法同上。
3,编译、链接、运行一步法这是一种最简单的方法。按Ctrl+F9键后,Turbo C即按照编译、链接的顺序生成目标文件和可执行文件,然后运行。
3 简单编程练习
1,输入下面的程序,检查错误,并对其进行编译、链接和运行。
#include <stdio.h>
void mian( )
{
printf("This is a C program.\n');
}
2,理解下面程序功能,并运行程序,输入两个操作数(注意输入函数的格式),观察程序运行结果。
int sum(int x,int y)
{
int z;
z=x+y;
return(z);
}
main( )
{
int a,b,c;
scanf("%d,%d",&a,&b);
c=sum(a,b);
printf("sum=%d\n",c);
}
3,编写一个程序,输入一个天数,求这个天数包含几周零几天。
算法提示:
① 利用除法算术运算符“/”(整除运算,即商的整数部分)和“%”(取余除运算,即商的余数部分)。
②本题的输入数据有一个,输出数据有两个,因此需要定义3个变量来保存这些数据,并且都应定义为整数类型。
4,编写一个程序,输出以下图形:
$
$ $
$ $ $
$ $ $ $
$ $ $ $ $
算法提示:
本题是反复利用输出函数printf()来实现这些符号的输出的。要注意的是printf()函数一次可以输出多个数据或字符。
5,编写一个程序,输出以下图形:
M
M M M
M M M M M
M M M M M M M
M M M M M
M M M
M
6,参照上面的第3题,在程序中编写一个函数,用于计算两个整数的积。
实验二 顺序结构程序设计一、目的和要求
1,掌握顺序程序设计方法。
2,掌握C语言数据类型,熟悉各种类型变量的定义和含义。
3,进一步熟悉C程序的编辑、编译、链接和运行的过程。
二、实验内容
1,编写程序,从键盘输入一个大写字符,将它转换为对应的小写字母后输出。
算法提示:
根据大小写字母的ASCII码值相差32来进行转换。
2,编写程序,输入圆半径r,求圆周长、圆面积、圆球表面积、圆球体积。
要求:为加强界面的友好,输入数据与输出结果都应有相应的提示信息。且输出数据取小数点后两位数字显示。
3,输入一个华氏温度,要求输出摄氏温度。公式为:

输出要有文字说明,取2位小数。
4.有三个电阻r1、r2、r3并联,编写程序计算并输出并联后的电阻r。已知电阻并联公式为:

5,编写程序,输入三角形的三条边,计算并输出三角形的面积。(注意输入的三条边必须要能构成一个三角形)
求三角形的面积公式为:

其中s=(a+b+c)/2。
算法提示:
本题需要用到求平方根的数学函数sqrt(),应在文件开始部分写下如下文件包含预处理命令:
#include <math.h>
6,编写程序,输入梯形的上底和下底,计算并输出梯形的面积。精确到小数点后2位,输入输出时要有文字说明。
7,周期为T秒的人造卫星离地面的平均高度H的计算公式为:

其中:M=6×1024kg是地球质量,R=6.371×1064m是地球半径。
编写程序,输入人造卫星的周期T,计算并输出人造卫星离地面的高度H。
算法提示:
本题需要用到求xy结果的数学函数pow(),具体函数使用说明请参见书后目录。为了使用系统提供的数学函数应在文件开始部分写下文件包含预处理命令:
#include <math.h>
8,操作符sizeof用以测试一个数据或数据类型所占用的存储空间字节数。请编写一个程序,测试各基本数据类型所占用的存储空间大小。
sizeof的格式:
sizeof(数据类型) 或 sizeof(表达式)
9,分析下面程序的应得结果,并与上机运行结果进行比较。
#include <stdio.h>
main( )
{
 int a,b;
 float d,e;
 char c1,c2;
 double f,g;
 a=61; b=62;
 c1='a'; c2='b';
 f=3157.890121; g=0.123456789;
 d=3.56; e=-6.87;
 printf("a=%d,b=%d\nc1=%c,c2=%c\nd=%6.2f,e=%6.2f\n",a,b,c1,c2,d,e);
 printf("f=%15.6f,g=%15.12f\n",f,g);
}
① 修改程序的第11行:d=f; e=g; 然后运行程序,分析结果。
② 将两个printf语句分别改为:
printf("a=%d,b=%d\nc1=%c,c2=%c\nd=%15.6f,e=%15.12f\n",a,b,c1,c2,d,e);
 printf("f=%f,g=%f\n",f,g);
运行程序,并分析结果。
10,下面程序的功能为计算由键盘输入的任意两个整数的平均值:
#include <stdio.h>
void main( )
{
int x,y,a;
scanf("%x,%"y,&x,&y);
a=(x+y)/2;
 printf("The average is:"a);
}
调试上面的程序,指出其中的错误,改正后用下面的测试用例进行测试:
① 2,6
② 1,3
③ -02,-10
④ -2,6
⑤ 3,-1
⑥ 1,0
⑦ 33000,31542
⑧ -33000,31542
⑨ 2.3,5.4
⑩ 100,80000
记录每组测试用例的输出结果,通过测试,你发现程序有什么错误了吗?请分析错误原因,并对程序作适当的修改。
实验三 选择结构程序设计一、目的和要求
1,了解选择结构的用法。
2,掌握关系运算符和逻辑运算符的使用。
3,掌握if语句和switch语句的使用。
4,掌握多重条件下的if语句嵌套使用二、实验内容
1,编写程序,输入一个字存入变量ch中,根据该字符的ASCII码值判断并输出字符的类型,即字母(alpha)、数字(numeric)或其他字符(other)。
2,编写程序,输入一个正整数,判断该数是奇数还是偶数,并输出判断结果。
3,有一函数:
 
编写一个程序,用scanf函数输入x的值,输出y值。注意表达式的书写方法。
提示:此题要用到数学函数exp()和log10(),因此应包含相应的头文件。
4,经典编程。输入年号,判断并输出该年是否闰年。所谓闰年,是指能被4整除,但不能被100整除;或能被400整除的年份。
算法提示:此题要注意条件的表达。或者通过逻辑运算符构造复杂的条件表示;或者通过if分支嵌套来完成所有条件的表示。
5,从键盘输入某个日期(包括年、月、日),编写程序,计算并输出这一天是该年的第多少天。
算法提示:此题应注意每月不同天数的情况,对于2月份的天数还应判断当年是否是闰年。
6,从键盘输入三个数,代表三条线段的长度。请编写程序,判断这三条线段组成的三角形是什么类型(不等边、等腰、等边或不能构成三角形)。
7,简单选择界面的编程。从键盘输入整数,输出不同的字符串:
输入1,输出Good morning;
输入2,输出Good afternoon;
输入3,输出Good evening;
输入4,输出Good night;
输入其它数字,输出Bye-bye。
算法提示:此题的输入变量只有1个,但程序设计时要根据输入变量的可能取值实现不同的输出内容。可用switch语句实现。
8,已知从银行贷款月利率为:期限一年,为0.90%;期限2年,为1%;期限3年,为1.11%;三年以上为1.2%。从键盘输入贷款金额和期限,计算到期后应归还银行本金和利息合计为多少钱。
9,输入一个不多于5位的正整数,要求:① 求出它是几位数;② 分别打印出每一位数字;③ 按逆序打印出各位数字。
提示:判断位数应使用选择嵌套结构;求每一位数字的程序应放置在嵌套的最里层,需要使用“/”和“%”运算符来取得各个位数,并保存在相应的变量里。
运行程序时要分别输入以下测试数据调试:
1位正整数
2位正整数
3位正整数
4位正整数
5位正整数除此之外,程序还应当对不合法的输入作必要的处理,如负数或超过5位的正整数。
实验四 循环结构程序设计一、目的和要求
1,掌握循环语句的使用方法,及while语句、do-while语句和for语句格式。
2,掌握各种循环语句中如何正确的设定循环条件,以及如何正确的控制循环次数。
3,掌握用循环实现的一些常用算法(如穷举、迭代、递推等)。
二、实验内容
1.简单循环编程
1,从键盘输入若干整数,以0结束,判断并输出其中的最大数。
2,输入一行字符,以回车键作为结束标志,分别统计出大写字母、小写字母、空格、数字和其它字符的个数。
3,输入若干整数(以-32767作为结束标志),分别统计出正整数、负整数和0的个数并输出。
4,分别用while、do-while和for语句计算(即求1!+2!+3!+…+20!),并试着简化程序。
5,计算。
6,输出所有的水仙花数。水仙花数是指一个3位数,各位数字的立方和等于该数本身,例如153=13+53+33。
7,已知2006年农历为狗年,编写程序输出21世纪全部为狗年的年份。
8,从键盘上输入一个整数,判断这个数是否是素数。素数就是除了1和它自身外,不能被任何数整除的数。
9,编写程序,输入两个正整数m和n,求它们的最大公约数和最小公倍数。
① 输入两组数据,分别使m>n和m<n,观察结果是否正确。
② 分别用while语句、do-while语句和for语句实现。注意循环控制表达式的写法。
10,编写程序,计算2n。其中n为整数(注意n可能是正整数、负整数或0)。
2.递推问题编程
11,国民生产总值(GDP)每年递增7.5%,编写程序计算并输出需要多少年国民生产总值才能翻一番。
12,银行存款年利率为1.9%,编写程序计算并输出需要存多少年存款才能翻一番。
13,有一分数序列:

求出这个数列的前20项之和。
14,编写程序,用公式计算π的近似值,直到最后一项的绝对值小于10-6。
15,下面是一个计算e的近似值的C程序。从键盘输入δ,使误差小于δ。
main( )
{
double e=1.0,x=1.0,y,detax;
int i=1;
printf("\nInput a error:");
scanf("%lf",&detax);
y=1/x;
while (y>=detax)
{ x=x*i;
y=1/x;
e=e+y;
i+=1;
}
printf("e=%12.10lf\n",e);
}
① 理解并运行程序,写出程序所依据的计算公式。
② 当输入的detax各是什么值时,能分别使程序按下面的要求运行:
不进入循环只循环一次只循环两次进入死循环(程序将永远循环下去)
如何才能知道程序循环了多少次?
③ 若把程序中while语句之前的y=1/x语句去掉,运行并分析结果。
④ 把原程序中的while结构改为do-while结构,程序应作哪些修改?并运行修改后的程序,比较while语句和do-while语句的异同。
16 求的值,其中a是一个数字,如2+22+222+2222+22222(此时a=2,n=5),a和n均由键盘输入。
17,输入x,计算级数:

要求输出精度为10-8。
3.图形编程
18,用循环语句编写程序,输出如下图案:
M M M M M M
M M M M M
M M M M
M M M
M M
M
19,用循环语句编写程序,输出如下图案:
*
* * *
* * * * *
* * * * * * *
* * * * * * * * *
* * * * * * * * * * *
20,用循环语句编写程序,输出如下图案:
X Y X Y X Y X Y
X Y X Y X Y X
X Y X Y X Y
X Y X Y X
X Y X Y
X Y X
X Y
X
21,用循环语句编写程序,输出如下图案:
* * * * * *
* * * * *
* * * *
* * *
* *
*
22,编写程序,打印出九九乘法表:
1×1=1
2×1=2 2×2=4
3×1=3 3×2=6 3×3=9
4×1=4 4×2=8 4×3=12 4×4=16
5×1=5 5×2=10 5×3=15 5×4=20 5×5=25
6×1=6 6×2=12 6×3=18 6×4=24 6×5=30 6×6=36
7×1=7 7×2=14 7×3=21 7×4=28 7×5=35 7×6=42 7×7=49
8×1=8 8×2=16 8×3=24 8×4=32 8×5=40 8×6=48 8×7=56 8×8=64
9×1=9 9×2=18 9×3=27 9×4=36 9×5=45 9×6=54 9×7=63 9×8=72 9×9=81
4.迭代问题编程
23,用迭代法求x=。迭代公式为:

要求前后两次求出的x的差的绝对值小于10-6。
实验五 函数编程一、目的和要求
1,掌握模块化编程方法中自定义函数和主调函数的模块关系。
2,掌握模块的实现方法:定义无参函数和有参函数的方法。
3,掌握函数的调用方法:调用时实参的使用以及实参和形参的对应关系,知道如何处理函数的返回值。了解函数的声明。
4,了解系统的库函数分类,学会使用系统功能。
5,了解函数递归调用的原理和用途。
6,掌握全局变量和局部变量、静态变量和动态变量的使用方法。
7,理解指针的含义以及指针作为函数参数的意义,及实参的表达形式。
8,了解函数的指针含义。
二、实验内容
1.简单值传递参数函数编程
1,编写一个函数,输出语句:“I Love China!”。
2,调试下面的程序,记录系统给出的出错信息,并指出错误原因。
main( )
{
int x,y;
printf("%d\n",sum(x+y));
}
int sum(a,b);
{
int a,b;
return(a+b);
}
3,编写函数,判断指定的字符是否是数字字符,如果是返回1,不是则返回0。在主函数中输入该字符,调用函数判断该字符并输出是否是数字字符。
4,编写一个函数,求1!+2!+…+n!,要求:n在主函数中输入,并在主函数中输出计算结果。
5,编写一个函数,计算一个整数m的n次幂,在主函数中输入m和n,并在主函数中输出计算结果。
6,编写一个判别素数的函数,在主函数中输入一个整数,输出是否是素数的信息。
7,编写一个函数,输出如下图形
*
* * *
* * * * *
* * * * * * *
* * * * * * * * *
* * * * * * * * * * *
* * * * * * * * *
* * * * * * *
* * * * *
* * *
*
8,编写两个函数,分别用于计算两个整数m和n的最大公约数和最小公倍数。m和n在主函数中输入。
9,编写函数(非递归函数),计算Fibonacci数列第n项的值,n在主函数中由键盘输入,结果在主函数中输出。已知:
 
2.全局变量与局部变量
10,指出下列各变量的存储属性,分析程序的应得结果,并上机验证。
#include <stdio.h>
int n=1;
void func( )
{
static int a=2;
int b=5;
a+=2;
b+=5;
n+=12;
printf("a=%d,b=%d,n=%d\n",a,b,n);
}
void main( )
{
int a=0,b= -10;
printf("a=%d,b=%d,n=%d\n",a,b,n);
func( );
printf("a=%d,b=%d,n=%d\n",a,b,n);
func( );
}
3.函数递归调用
11,编写一个递归函数,将整数n转换成字符串。如:输入4281,应输出字符串“4281”。n在主函数中输入,位数不确定,可以是任意整数。
12,编写一个递归函数,输出十进制正整数m的二进制形式。如:输入22,应输出10110。
13,编写递归函数,计算Fibonacci数列第n项的值,n在主函数中由键盘输入,结果在主函数中输出。
4.指针及指针参数函数编程
14,调试下面的程序,指出错误的原因。
main( )
{
int x=10,y=5,*px,*py;
px=py;
px=&x;
py=&y;
printf("*px=%d,*py=%d\n",*px,*py);
}
15,仔细分析并比较下面两个程序的运行结果。

#include <stdio.h>
void main( )
{
int a1=11,a2=22;
int *p1,*p2,*p;
p1=&a1;
p2=&a2;
printf("%d,%d",*p1,*p2);
p=p1; p1=p2; p2=p;
printf("%d,%d",*p1,*p2);
printf("%d,%d",a1,a2);
}
⑵
#include <stdio.h>
void main()
{
int a1=11,a2=22;
int *p1,*p2,t;
p1=&a1;
p2=&a2;
printf("%d,%d",*p1,*p2);
t=*p1; *p1=*p2; *p2=t;
printf("%d,%d",*p1,*p2);
printf("%d,%d",a1,a2);
}
16,编写一个函数,用指针作为参数,分别得到双精度实型数据的整数部分和小数部分。
17,编写一个函数,返回三个整数中的最大数。要求用指针作为函数参数得到最大数,在主函数中输入输出数据。
18,编写一个函数,从键盘读入10个数,用指针作为函数参数返回最大数,在主函数中输出最大数。
19,编写函数,计算π。已知:
π/4=1-1/3+1/5-1/7+…
直到最后一项的绝对值小于10-8为止。要求用指针作为函数参数返回计算结果。
20,函数指针编程:设计一个函数process,在调用它的时候,每次实现不同的功能。输入a和b两个数,第一次调用process时返回a和b中的最大数,第二次调用process时返回a和b中的最小数,第三次调用process时求a与b之和,第四次调用process时返回a与b之差。
实验六 构造数据类型一、目的和要求
1,掌握数组的定义、赋值、使用方法和特点。
2,掌握字符数组的使用、字符串编程的特色及常用字符串函数。
3,掌握结构体和共用体类型的定义和使用。
4,掌握数组作为函数参数的使用。
5,掌握指向字符串的指针,字符串作为函数参数的使用。
6,掌握指向结构体变量的指针的应用。
7,了解链表的概念和应用。
二、实验内容
1.数组编程
1,运行下面的程序。根据运行结果,可以说明什么?
main( )
{
int i,x[5]={1,2,3,4,5};
for (i=0;i<=5;i++)
printf("%4d",x[i]);
}
2,从键盘输入10个整数,找出最大的数并输出该数及其下标。
3,某歌手大赛,共有10个评委给歌手打分,分数采用百分制,去掉一个最高分,去掉一个最低分,然后取平均分,得到歌手的成绩。10个分数由键盘输入,编写程序计算某歌手的成绩。
4,有一个一维数组包含10个元素,编写程序将其中的值按逆序重新存放。即第一个元素和最后一个元素交换位置,第二个元素和倒数第二个元素交换位置,……。
5,有n个数,已按由小到大的顺序排好,要求输入一个数,将它插入到数列的合适位置,使数组仍然有序,并输出新数列。编程时应考虑插入的数的各种可能性(比原所有数都大;比原所有数都小;在最大数和最小数之间)。
6,有13个人围成一圈(编号为0-12),从第0个人开始从1报数,凡报到7的倍数的人离开圈子,然后再从1数下去,直到只剩下最后一个人为止。编程输出此人原来的位置是多少号。
7,有一个4×5的矩阵,编写程序找出值最大的那个元素,输出其值以及所在的行号和列号。
8,输入一个M行M列的二维数组,计算四周元素之和。M由下面的符号常量定义:
#define M 5
9,输入一个M行M列的二维数组,分别计算两条对角线上的元素之和。M由下面的符号常量定义:
#define M 5
10.有一个班,有4个学生,5门课。①求各门课的平均分;②找出有两门以上课程不及格的学生,输出它们的学号和全部课程成绩及平均成绩。③找出平均成绩在90分以上或全部课程成绩在85分以上的学生,输出它们的学号和全部课程成绩。分别编三个函数实现以上三个要求。
11,有一段文字,共有5行,分别统计出其中英文大写字母、小写字母、数字、空格以及其它字符的个数。
12,输入一行字符,统计其中的单词个数,已知单词之间用空格分隔开。
13,从键盘输入一个字符串,分别输出其完全大写和完全小写的形式。
14,输入6个字符串,输出最大的字符串。
15,编写一个程序,将字符数组str2中的全部字符拷贝到字符数组str1中。不要使用strcpy函数。
16,编写一个程序,将字符数组str2中的全部字符连接到字符数组str1的后面。不要使用strcat函数。
3.结构体、共用体编程
17,定义一个结构体类型,用于存放职工信息,其中包括:职工号、姓名、性别、年龄、职称、家庭住址。然后定义该类型的变量,从键盘输入具体数据,然后打印出来。
18,有10名学生,每个学生的数据包括:学号、姓名、成绩,从键盘输入10个学生的数据,输出成绩最高者的姓名和成绩。
19,上题中,分别编写函数实现上述功能。用input函数输入10个学生数据,用max函数找出最高分的学生数据,最高分学生数据在主函数中输出。
20,按上面的结构体类型定义一个结构体数组(包含5个元素),从键盘输入每个结构体数组元素的数据,然后逐个输出。
21,定义一个结构体变量,存放年、月、日。从键盘输入一个日期,计算并输出该日在该年中是第几天。注意该年是闰年的情况。
22,编写一个简单的图书借阅程序。图书信息包含以下数据项:
图书编号、图书名、出版社、出版时间、是否已被借阅。
要求:
<1> 自己根据以上信息定义图书的结构体类型book。
<2> 假定该图书馆有图书5本(为简化调试,输入5本图书信息为例),定义该结构体类型数组,程序运行时先从键盘上输入图书信息,建立该图书信息库。
<3> 由用户从键盘上输入所借阅的“图书编号”或“图书名”,程序根据输入信息,查找有无该图书,如果没有则显示“没有该图书”;如果有该书,则查看该书是否已被借阅(最后一个成员值),如果已借阅则反馈信息为“该书已借出,不能借阅”;如果没被借阅,则将该书借出(借阅标志变为’Y’)并显示“借阅成功”。
23,输入并运行以下程序:
union data
{ int x[2];
flaot a;
long b;
char c[4];
}u;
main( )
{
scanf("%d,%d",&u.x[0],&u.x[1]);
printf("x[0]=%d,x[1]=%d\na=%f,b=%ld\n",u.x[0],u.x[1],u.a,u.b);
printf("c[0]=%c,c[1]=%c,c[2]=%c,c[3]=%c\n",u.c[0],u.c[1],u.c[2],u.c[3]);
}
输入两个整数10000、20000给u.x[0]和u.x[1],分析运行结果。
24,某校建立一个人员登记表,内容如下:
号 码
姓 名
性 别
职 业
班 级
单 位
95001
1004 a
Wang Fang
Feng Ming
M
F
S
T
js45
dept1
其中,职业S表示学生,T表示教师。即根据职业来决定最后一项的内容,如为学生,则最后一项为班级,如为教师,则最后一项为单位。编写程序,输入以上两人的数据,并输出之。
4.指针与数组作为函数的参数编程
25,用指针访问一个一维数组。包括输入各数组元素的值,将下标为3的倍数的元素置0(包括0下标),最后输出数组各元素值。
26,有一个数组,内放10个学生的成绩,编写函数,计算平均成绩。学生成绩在主函数中输入,并在主函数中输出平均成绩。
27,编写函数,使给定的一维数组(包含10个元素)中的元素逆序存放。在主函数中输入输出该数组。
28,编写函数,使给定的一个二维数组(4×4)转置,即行列互换。在主函数中输入输出该数组。
29.有一组学生信息,每个学生包含学号、姓名、班级三项信息,其中班级代号为1~3三种情况。用数组存储这些学生信息,要求:
1)将这一组学生信息按学号升序排序(用冒泡法)
2)将这一组学生信息按姓名升序排序(用选择法)
3)求每班人数,并在函数内部输出。
30,有一个4×5的矩阵,编写函数求出最大值和最小值。在主函数中输入该矩阵,并在主函数中输出最大值和最小值。要求使用指针作为函数参数。
31,有一个4×5的矩阵,编写函数求出最小元素,以及该元素所在的行号和列号。在主函数中输入该矩阵,并在主函数中输出元素值以及其所在的行号和列号。要求使用指针作为函数参数。
32,编写函数,将形参数组str中的字符串逆序存放。字符串str在主函数中输入,并在主函数中输出逆序后的结果。
33,编写函数,将一个长整型数据转换成字符串,如输入长整数824734,应输出字符串“824734”。要求使用字符串作为函数参数。
34,编写函数,将一个无符号十进制整数转化为二进制形式,保存在形参数组中,在主函数中输出其二进制形式。函数原型为:
void fun(unsigned x,int a[50]);
35,编写函数,将字符串中的小写字母转换成大写字母。函数原型为:
void mytoupper(char *t);
36,编写函数,将两个字符串连接成一个字符串,并且返回新字符串的首地址。函数原型为:
char *connect(char *t1,char *t2);
37,编写函数,将一个字符串复制到另一个字符串中,并返回新串的首地址。不能使用strcpy函数。函数原型为:
char *copy(char *t1,char *t2);
38,有一个已排好序的数组,现输入一个数,编写函数按原来排序的规律将它插入到数组中。在主函数中输入输出插入前后的数组。
39,有一个数组(12个元素),编写函数对数组中的元素从小到大排序,在主函数中输入输出该数组。
40,有一个数组,编写函数将指定范围内的元素按从大到小的顺序排序,在主函数中输入输出排序前后的数组。函数原型为:
void sort(int x[ ],int m,int n);
其中包含第m个元素和第n个元素。
41,有一个结构体变量,包括学生学号、姓名和3门课的成绩。要求在主函数中输入这些数据,在函数print中将它们输出。输入数据时使用指向结构体指针,使用指向结构体的指针作函数参数。
5.链表编程
42,建立一个链表,每个结点包括的成员为:职工号、工资。用malloc函数开辟新结点。要求链表包含5个结点,从键盘输入结点的有效数据,然后把这些结点的数据打印出来。用create函数来建立链表,用list函数输出数据。5个职工的职工号为101,103,105,107,109。
43,在上题基础上,新增加一个职工的数据,按职工号的顺序插入链表。新职工的职工号为106。编写一个函数insert来插入新结点。
44,在上面的基础上,写一函数delete,用来删除一个结点。今要求删除职工号为103的结点。打印出已删除后的链表。
45,13个人围成一圈,从第1个人开始顺序报号1、2、3。凡是报到3者退出圈子,找出最后留在圈子中的人原来的序号。提示:需要用链表来实现。
实验七 磁盘数据存储一、目的和要求
1,掌握文件和文件指针的概念。
2,掌握文件的打开、关闭、读、写等文件操作函数。
二、实验内容
1,从键盘输入一个字符串,然后将其以文件的形式存到磁盘上。磁盘文件名为file1.dat。
2.打开上题生成的文件,统计其中字符的个数。
3,从磁盘文件file1.dat中读入一行字符,将其中所有小写字母改为大写字母,然后输出到磁盘文件file2.dat中。
4,对文件file1.dat进行加密,加密算法采用异或操作,加密后的数据写入文件new.dat中。
5,有5个学生,每个学生的信息包括:学号、姓名和3门功课的成绩,从键盘上输入每个学生的信息,计算出平均成绩,然后将原有数据和计算的平均分数存放在磁盘文件file3.dat中。
6,将上题file3.dat文件中的学生数据,按平均分高低排序后存入一个新文件file4.dat中。
实验八 项目开发与模块设计一、目的和要求
1,掌握项目开发中多个模块文件的文件包含操作。
2,了解条件编译的含义和应用。
3,了解项目管理器的使用。
二、实验内容
1.当一个程序由多个模块组成时,则应当对各文件分别进行编译,得到多个.OBJ文件,然后再将这些目标文件以及库函数、包含文件等链接成一个可执行文件。
把下面的例子写成两个文件SUMA.C和SUMB.C。分别为:
/* SUMA.C */
#include <stdio.h>
int max(int x,int y);
void main( )
{ int a,b,c;
scanf("%d%d",&a,&b);
c=max(a,b);
printf("max=%d\n",c);
}
/* SUMB.C */
int max(int x,int y)
{ int z;
if (x>y)
z=x;
else
z=y;
return(z);
}
为了处理多个模块组成的程序。Turbo C要求将这些文件建立一个扩展名为.PRJ的“项目”(Project),为此要建立一个“项目文件”,其内容就是组成程序的每个文件的源文件名(*.C)或对每个源文件编译后产生的目标文件名(*.OBJ)。本例中,Project文件中的内容为:
SUMA.C
SUMB.C
或
SUMA.OBJ
SUMB.OBJ
在写.PRJ文件时,对于源文件,扩展名.C可以省略不写;可以使用路径名,如D:\MYTC\SUMA或C:\TC\SOURCE\SUMA.OBJ等;文件名的前后顺序可以是任意的。
写好.PRJ文件后,选择File|Write选项将文件存盘,如SUM.PRJ。主文件名可任意指定,只要符合文件名的命名规则即可,扩展名必须用.PRJ,表示是项目文件。
打开Project菜单,选择Project name选项,弹出Project name对话框,如图1.15所示。在该对话框中输入需要进行编译和链接的项目文件名(如SUM.PRJ)并按回车键,然后再按ESC键返回编辑状态。

图1.15
打开Compile菜单,选择Make EXE file选项,系统就会对此项目文件进行编译和链接,并生成两个目标文件SUMA.OBJ和SUMB.OBJ,以及可执行文件SUM.EXE。
2.开发一个学生成绩处理系统。该系统由以下几个模块文件组成:
1)、学生信息的生成模块。要求该模块输入学生的姓名、学号、成绩(成绩包括:语文、数学、英语、总分等)数据项。
2)、学生成绩统计模块。要求该模块能计算出每个学生的总分数据,并分项统计单科不及格人数。要求该模块具有对原始数据重新排序的功能,排序依据关键字段分别为:学号、总分、语文成绩、英语成绩、数学成绩,排序依据应由用户指定。
3)、学生信息输出模块。该模块可输出学生信息,并可根据要求输出部分学生信息,例如:输出“学号+总分”信息,“学号+姓名+语文成绩”信息等。输出的信息组合可由用户指定,默认为输出全部信息。
4)、主界面显示和选择模块。该模块由主程序调用,显示文本菜单供用户选择需要进行的操作:
A、输入学生信息 B、输出学生信息
C、统计学生成绩总分及排名 D、统计学生单科成绩及排名
E、退出系统
附录附录一 Turbo C 2.0集成开发环境的使用
Turbo C是Borland公司开发的一个用于微型计算机上的C语言编译系统,它具有友好的用户界面,提供了丰富的库函数,是目前流行的版本之一。
Turbo C是一个集程序编辑、编译、连接、调试及运行为一体的C程序开发软件,为用户提供了一个方便的集成开发环境。程序设计人员可在Turbo C环境下进行全屏幕编辑,利用窗口功能进行编译、连接、调试、运行、环境设置等工作。
运行Turbo C2.0时,只要在TC子目录下键入TC并回车即可进入Turbo C 2,0 集成开发环境。进入Turbo C 2.0集成开发环境中后,屏幕上显示,如图1.1所示:

图1.1 Turbo C 2.0界面其中顶上一行为Turbo C 2.0 主菜单,中间窗口为编辑区,接下来是信息窗口,最底下一行为参考行。这四个部分构成了Turbo C 2.0的主屏幕,以后的编程、编译、调试以及运行都将在这个主屏幕中进行。下面详细介绍主菜单的内容。
主菜单在Turbo C 2.0主屏幕顶上一行,显示下列内容:
File Edit Run Compile Project Options Debug Break/watch
除Edit外,其它各项均有子菜单,只要用Alt加上某项中第一个字母(即大写字母),就可进入该项的子菜单中。
File (文件)菜单
按Alt+F可进入File菜单,该菜单包括的子菜单见表1-1
表1-1 File菜单中的子菜单子菜单
功能
说明
Load
加载
装入一个文件,可用类似DOS的通配符(如*.C)来进行列表选择。也可装入其它扩展名的文件,只要给出文件名(或只给路径)即可。该项的热键为F3,即只要在主菜单中按F3即可进入该项,而不需要先进入File菜单再选此项。
Pick
选择
将最近装入编辑窗口的8个文件列成一个表让用户选择,选择后将该程序装入编辑区,并将光标置在上次修改过的地方。其热健为Alt+F3。
New
新建文件
开始一个新文件,缺省文件名为NONAME.C,存盘时可改名。
Save
存盘
将编辑区中的文件存盘,若文件名是NONAME.C时,将询问是否更改文件名,其热键为F2。
Write to
存盘
可由用户给出文件名将编辑区中的文件存盘,若该文件已存在,则询问要不要覆盖。
Directory
目录
显示目录及目录中的文件,并可由用户选择。
Change dir
改变目录
显示当前目录,用户可以改变显示的目录。
Os shell
DOS外壳
由Turbo C 2.0切换到DOS提示符下,此时可以运行DOS 命令,若想回到Turbo C 2.0中,只要在DOS状态下键入EXIT即可。
Quit
退出
退出Turbo C 2.0,返回到操作系统中,其热键为Alt+X。
说明:以上各项可用光标键移动色棒进行选择,回车则执行。也可用每一项的第一个大写字母直接选择。若要退到主菜单或从它的下一级菜单列表框退回均可用Esc键,Turbo C 2.0所有菜单均采用这种方法进行操作,以下不再说明。
Edit(编辑)菜单按Alt+E可进入编辑菜单,若再回车,则光标出现在编辑窗口,此时用户可以进行文本编辑。编辑方法基本与wordstar相同。
常用编辑命令见表1-2。
表1-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
如果光标所在处为Turbo C 2.0库函数,则获得有关该函数的帮助信息
Ctrl+Q[
查找Turbo C 2.0双界符的后匹配符
Ctrl+Q]
查找Turbo C 2.0双界符的前匹配符
说明,
Turbo C 2.0的双界符包括以下几种符号,
花括符 { } 尖括符 < > 圆括符 ( ) 方括符 [ ]
注释符 /* */ 双引号 " " 单引号 ''
Turbo C 2.0在编辑文件时还有一种功能,就是能够自动缩进,即光标定位和上一个非空字符对齐。在编辑窗口中,Ctrl+OL为自动缩进开关的控制键。
Run (运行)菜单按Alt+R可进入Run菜单,该菜单包含的子菜单见表1-3
表1-3 Run菜单包含的子菜单子菜单
功能
说明
Run
运行程序
运行由Project/Project name项指定的文件名或当前编辑区的文件。如果对上次编译后的源代码未做过修改,则直接运行到下一个断点(没有断点则运行到结束)。否则先进行编译、连接后才运行,其热键为Ctrl+F9。
Program reset
程序重启
中止当前的调试,释放分给程序的空间,其热键为Ctrl+F2。
Go to cursor
运行到光标处
调试程序时使用,选择该项可使程序运行到光标所在行。光标所在行必须为一条可执行语句,否则提示错误。其热键为F4。
Trace into
跟踪进入
在执行一条调用其它用户定义的子函数时,若用Trace into项,则执行长条将跟踪到该子函数内部去执行,其热键为F7。
Step over
单步执行
执行当前函数的下一条语句,即使用户函数调用,执行长条也不会跟踪进函数内部,其热键为F8。
User screen
用户屏幕
显示程序运行时在屏幕上显示的结果。其热键为Alt+F5。
Compile (编译)菜单按Alt+C可进入Compile菜单,该菜单包含的子菜单见表1-4。
表1-4 Compile菜单包含的子菜单子菜单
功能
说明
Compile to OBJ
编译生成目标码
将一个C源文件编译生成.OBJ目标文件,同时显示生成的文件名。其热键为Alt+F9。
Make EXE file
生成执行文件
此命令生成一个.EXE的文件,并显示生成的.EXE文件名。其中.EXE文件名是下面几项之一。
a,由Project/Project name说明的项目文件名。
b,若没有项目文件名,则由Primary C file说明的源文件。
c,若以上两项都没有文件名,则为当前窗口的文件名。
Link EXE file
连接生成执行文件
把当前.OBJ文件及库文件连接在一起生成.EXE文件。
Build all
建立所有文件
重新编译项目里的所有文件,并进行装配生成.EXE文件。该命令不作过时检查(上面的几条命令要作过时检查,即如果目前项目里源文件的日期和时间与目标文件相同或更早,则拒绝对源文件进行编译)。
Primary C file
主C文件
当在该项中指定了主文件后,在以后的编译中,如没有项目文件名则编译此项中规定的主C文件,如果编译中有错误,则将此文件调入编辑窗口,不管目前窗口中是不是主C文件。
Get info
获得信息
获得有关当前路径、源文件名、源文件字节大小、编译中的错误数目、可用空间等信息。
Project (项目)菜单按Alt+P可进入Project菜单,该菜单包含的子菜单见表1-5。
表1-5 Project菜单包含的子菜单子菜单
功能
说明
Project name
项目名
项目名具有.PRJ的扩展名,其中包括将要编译、连接的文件名。
Break make on
中止编译
由用户选择是否在有Warining(警告)、Errors(错误)、Fatal Errors( 致命错误)时或Link(连接)之前退出Make编译。
Auto dependencies
自动依赖
当开关置为on,编译时将检查源文件与对应的.OBJ文件日期和时间,否则不进行检查。
Clear project
清除项目文件
清除Project/Project name中的项目文件名。
Remove messages
删除信息
把错误信息从信息窗口中清除掉。
Options (选择) 菜单按Alt+O可进入Options菜单,该菜单对初学者来说要谨慎使用。该菜单包含的子菜单见表1-6。
表1-6 Options菜单包含的子菜单子菜单
功能
说明
Compiler
编译器
本项选择又有许多子菜单,可以让用户选择硬件配置、存储模型、调试技术、代码优化、对话信息控制和宏定义。
Linker
连接器
本菜单设置有关连接的选择项。
Environment
环境
本菜单规定是否对某些文件自动存盘及制表键和屏幕大小的设置。
Directories
路径
规定编译、连接所需文件的路径。
Save options
存储配置
保存所有选择的编译、连接、调试和项目到配置文件中,缺省的配置文件为TCCONFIG.TC。
Debug (调试)菜单按Alt+D可选择Debug菜单,该菜单主要用于查错。子菜单略。
Break/watch (断点及监视表达式)
按Alt+B可进入Break/watch菜单,该菜单包含的子菜单见表1-7。
表1-7 Break/watch菜单包含的子菜单子菜单
功能
Add watch
向监视窗口插入一监视表达式。
Delete watch
从监视窗口中删除当前的监视表达式。
Edit watch
在监视窗口中编辑一个监视表达式。
Remove all watches
从监视窗口中删除所有的监视表达式。
Toggle breakpoint
对光标所在的行设置或清除断点。
Clear all breakpoints
清除所有断点。
View next breakpoint
将光标移动到下一个断点处。
附录二 常用功能键简表通过前面的介绍我们知道:在集成环境下可以通过菜单操作完成各种相应的工作,菜单中的许多选项都有与之对应的功能键,这些功能键是操作热键,即不需通过菜单选择(选项所在菜单及其上一级菜单)就能很快完成操作。因此,使用热键能加快处理速度。在表1-8中列出了Turbo C中的常用功能键及其作用。
表1-8 常用功能键及其作用功能键
作 用
F1
打开帮助文件,其中简要说明了TC的各种用法
F2
把当前编辑的文件存到磁盘上,同File|Save
F3
装载一个文件,同File|Load
F4
使程序从执行长条执行到光标所在行,同Run|Go to cursor
F5
放大或缩小激活的窗口,同Options|Environment|Zoomed windows
F6
交替激活编辑窗口与信息窗口
F7
单步执行程序,跟踪函数调用,同Run|Trace into
F8
单步执行程序,但不跟踪函数调用,同Run|Step over
F9
编译并链接。同Compile|Make
F10
返回主菜单
Shift+F10
显示版本信息
Alt+F5
显示用户屏幕,同Run|User Screen
Alt+F7
光标指向上一个错误处
Alt+F8
光标指向下一个错误处
Alt+F9
不进行日期和时间检查的编译,生成.OBJ文件
Ctrl+F1
显示光标所指处的关键字或函数的帮助信息
Ctrl+F2
终止调试操作,同Run|Program reset
Ctrl+F3
显示程序运行到当前函数时的函数调用序列,同Debug|Call stack
Ctrl+F4
检查和改变表达式的值,同Debug|Evaluate
Ctrl+F7
在查看窗口输入表达式,同Break|Watch|Add watch
Ctrl+F8
在光标所在行设置或清除断点,同Break|watch|Toggle breakpoint
Ctrl+F9
编译、链接并运行程序,同Run|Run
Alt+C
打开Compile菜单
Alt+D
打开Debug菜单
Alt+E
进入编辑状态(由于TC无Edit菜单,所以并不打开Edit菜单)
Alt+F
打开File菜单
Alt+O
打开Options菜单
Alt+P
打开Project菜单
Alt+R
打开Run菜单
Alt+X
退出TC,返回DOS
ESC
返回上一级菜单
附录三 Turbo C编译出错信息
Turbo C编译程序查出的源程序错误分为三类:严重错误、一般错误和警告。
⑴ 严重错误(fatal error):很少出现,它通常是内部编译出错。在发生严重错误时,编译立即停止,必须采取一些适当的措施并重新编译。
⑵ 一般错误(error):指程序的语法错误以及磁盘、内存或命令行错误等。编译程序将完成现阶段的编译,然后停止。编译程序在每个阶段(预处理、语法分析、优化、代码生成)将尽可能多地找出源程序中的错误。
⑶ 警告(warning):不阻止编译继续进行。它指出一些值得怀疑的情况,而这些情况本身又可以合理地作为源程序的一部分。一旦在源文件中使用了与机器有关的结构,编译程序就将产生警告信息。
编译程序首先输出这三类出错信息,然后输出源文件名和发现出错的行号,最后输出信息的内容。
下面按字母顺序分别列出这三类出错信息。对每一条信息,均指出了可能产生的原因和纠正方法。
请注意出错信息处有关行号的一个细节:编译程序仅产生检测到的信息。因为C不限定在正文的某行设置语句,这样,真正产生错误的行可能在指出行号的前一行或前几行。在下面的信息列表中,我们指出了这种情况。
1,严重错误
Bad call of inline function 内部函数的不合法调用在使用一个宏定义的内部函数时,没有正确调用。一个内部函数以两个下划线(_ _)开始和结束
Irreducible expression tree 不可约表达式树文件中的表达式使得代码生成程序无法为其产生代码。应避免使用这种表达式。
Register allocation failure存储器分配失败源文件中的表达式太复杂,代码生成程序无法为它生成代码。此时应简化这种繁琐的表达式或干脆避免使用它。
2,一般错误
#operator not followed by macro argument name,#”运算符后没跟宏变元名在宏定义中,#用来标识一宏变元是串。“#”后必须跟一宏变元名。
'xxxxxxxx' not an argument,xxxxxxxx”不是函数参数在源程序中将该标识符定义为一个函数参数,但此标识符没有在函数的参数表中出现。
Ambiguous symbol 'xxxxxxxx' 歧义性符号“xxxxxxxx”
两个或多个结构体的某一域名(结构体变量)相同,但具有的位移、类型不同。在变量或表达式中引用这些结构体分量而未带结构名时,将产生歧义。这时需修改某个域名或在引用时加上结构名。
Argument # missing name 参数#名丢失参数名已脱离用于定义函数的函数原型。如果函数以原型定义,则该函数必须包含所有的参数名。
Argument list syntax error 参数表出现语法错误函数调用的一组参数其间必须以逗号隔开,并以一右括号结束。若源文件中含有一个其后不是逗号也不是右括号的参数,则出现此错。
Array bound missing 数组的界限符“]”丢失在源文件中定义了一个数组,但此数组没有以一右方括号结束。
Array size too large 数组长度过大定义的数组太大,可用内存不够。
Assembler statement too long 汇编语句太长直接插入的汇编语句最长不能超过480字节。
Bad configuration file 配置文件不正确
TURBOC.CFG配置文件中包含不是合适命令行选择的非注释文字。配置文件命令选项必须以一短横线开始(-)开始。
Bad file name format in include directive 包含命令中文件名格式不正确包含文件名必须用引号(“filename.h”)或尖括号(<filename.h>)括起来,否则将产生此类错误。如果使用了一个宏,则产生的扩展程序文本也是不正确的(因为没有加上引号)。
Bad ifdef directive syntas ifdef命令语法错误
#ifndef必须以单个标识符(仅此一个)作为该命令的体。
Bad ifndef directive syntas ifndef命令语法错误
#ifndef必须以单个标识符(仅此一个)作为该命令的体。
Bad undef directive syntas undef命令语法错误
#undef必须以单个标识符(仅此一个)作为该命令的体。
Bad file size syntax 位字段长语法错误一个位字段长必须是1~16位的常量表达式。
Call of non-function 调用未定义函数正被调用的函数无定义,通常是由于不正确的函数声明或函数名拼写错引起的。
Cannot modify a constant object 不能修改一个常量对象对定义为常量的对象进行不合法操作(例如常量的赋值)将引起本错误。
Case outside of switch case出现在switch外编译程序case 语句出现在switch语句外面,通常是由于括号不配对引起的。
Case statement missing,case语句漏掉“:”
Case语句必须含有一个冒号终结的常量表达式。可能是丢了冒号或冒号前多了别的符号。
Case syntax error case语法错误
Case中有一些不正确的符号。
Character constant too long 字符常量太长字符常量只能是一个或两个字符长。
Compound statement missing } 复合语句漏掉“}”
编译程序扫描到源文件结束时,未发现结束标记“}”,通常是由于花括号不配对引起的。
Conflicting type modifiers 类型修饰符冲突对同一指针,只能指定一种变址修饰符(如near或far);同样对于同一函数,也只能给出一种语言修饰符(如cdecl、pascal或interrupt)。
Constant experssion required 要求常量表达式数组的大小必须是常量。本错误通常由于#define常量的拼写出错而引起。
Could not find file 'xxxxxxxx.xxx' 找不到“xxxxxxxx.xxx”文件编译程序找不到命令行上给出的文件。
Declaration missing; 生命漏掉“;”
在源文件中包含一个类型或一个存储类,但后面漏掉了分号(;)。
Declaration needs type or storage class 声明必须给出类型或存储类声明必须包含一个类型或一个存储类,如声明:“i,j;”是不正确的。
Declaration syntax error 声明出现语法错误在源文件中,某个声明丢失了某些符号或有多余的符号。
Default outside of switch Default在switch外出现编译程序发现default语句出现在switch语句之外,通常是由于括号不配对引起的。
Define directive needs an identifier Define命令必须有一个标识符
#difine后面的第一个非空格符必须是一标识符。若编译程序发现一些其他字符,则出现本错误。
Division by zero 除数为零源文件的常量表达式中,出现除数为零的情况。
Do statement must have while do语句中必须有while
源文件中含有一无while关键字的do语句时,出现本错误。
Do-while statement missing( do-while语句中漏掉了“(”
在do语句中,编译程序发现while关键字后无左括号。
Do-while statement missing) do-while语句中漏掉了“)”
在do语句中,编译程序发现条件表达式后无右括号。
Do-while statement missing; do-while语句中漏掉了“;”
在do语句中的条件表达式中,编译程序发现右括号后面无分号。
Duplicate case case的情况值不唯一
switch语句的每个case必须有一个唯一的常量表达式值。
Enum syntax error enum语法错
Enum声明的标识符表的格式不对。
Enumeration constant syntax error 枚举常量语法错赋给enum类型变量的表达式值不为常量,产生本错误。
Error Directive:xxxx Error命令:xxxx
处理源文件中的#error命令时,显示该命令定义的信息。
Error writing output file 写输出文件错通常是由于磁盘空间引起的,可要删除一些不必要的文件,重新编译。
Expression syntax 表达式语法错当编译程序分析一表达式并发现一些严重错误时,出现本错误。通常是由于两个连续操作符、括号不配对或缺少括号,以及前一语句漏掉了分号等引起的。
Extra parameter in call 调用时出现多余参数调用函数时,其实际参数个数多于函数定义中的参数个数。
Extra parameter in call to xxxxxxxx 调用xxxxxxxx函数时出现了多余的参数调用一个指定的函数时(该函数由原型定义)出现了过多的参数。
File name too long 文件名太长
#include命令给出的文件名太长,编译程序无法处理。DOS中的文件名不应超过64个字符。
For statement missing( for语句漏掉“(”
编译程序发现在for关键字后缺少左括号。
For statement missing ) for语句缺少“)”
在for语句中,编译程序发现在控制表达式后缺少右括号。
For statement missing; for语句缺少“;”
在for语句中,编译程序发现在某个表达式后缺少分号。
Function call missing ) 函数调用缺少“)”
函数调用的参数表有几种语法错误,如左括号漏掉或括号不配对。
Function definition out of place 函数定义位置错函数定义不可出现在另一函数内。函数内的任何声明,只要以类似于带有一个参数表的函数开始,就被认为是一个函数定义。
Function doesn't take a varible of argument 函数不接受可变的参数个数源文件夹中的某个函数内使用了va_start宏,此类函数不能接受可变数量的参数。
Goto statement missing label goto语句缺少标号在goto关键字后面必须有一个标号。
If statement missing( if语句缺少“(”
在if语句中,编译程序发现if关键字后面缺少左括号。
If statement missing ) if语句缺少“)”
在if语句中,编译程序发现测试表达式后缺少右括号。
Illegal character 'c'(0xXX) 非法字符‘c’(0xXX)
编译程序发现输入文件中有一些非法字符,即以十六进制形式打印的字符。
Illegal initialization 非法初始化初始化必须是常量表达式,或是一个全局变量extern,或是static的地址加减一常量。
Illegal octal digit 非法八进制数编译程序发现一个八进制常数中包含了非八进制数字(例如8或9)。
Illegal pointer subtraction 非法指针相减这是由于试图以一个非指针变量减去一个指针变量而造成的。
Illegal structure operation 非法结构操作
结构只能使用(.)、取地址(&)和赋值(=)操作符,或作为函数的参数传递。当编译程序发现结构使用了其他操作符时,出现本错误。
Illegal use of floating point 非法浮点运算浮点运算分量不允许出现在移位运算符、按位逻辑运算符、条件(?:)、间接(*)以及其他一些运算符中。编译程序发现上述运算符中使用了浮点运算分量时,出现本错误。
Illegal use of point 指针使用不合法施于指针的运算符只能是加、减、赋值、比较、间接(*)或箭头。如用其他运算符,则出现本错误。
Improper use of a typedef symbol typedef符号使用不当源文件中使用了一个typedef符号,符号变量应出现在一个表达式中。检查一下此符号的说明和可能的拼写错误。
In-line assembly not allowed 不允许直接插入的汇编语句源文件中含有直接插入的汇编语句,若在集成环境下进行编译,则出现本错误。必须使用TCC命令行编译此文件。
Incompatible storage class 不相容的存储类源文件的一个函数定义中使用了extern关键字,但只有static(或根本没有存储类型)是允许的。
Incompatible type conversion 不相容的类型转换源文件中试图把一种类型转换成另一种类型,但这两种类型是不相容的。例如,函数与非函数间转换,一种结构体或数组与一种标准类型的转换,浮点数和指针间转换等。
Incorrect command lineargument:xxxxxxxx 不正确的命令行参数:xxxxxxxx
编译程序视此命令行参数是非法的。
Incorrect configuration file argument:xxxxxxxx 不正确的配置文件参数:xxxxxxxx
编译程序视此配置文件是非法的。检查一下前面的短横线(-)。
Incorrect number format 不正确的数据格式编译程序发现在十六进制数中出现十进制小数点。
Incorrect use of default defaule使用错编译程序发现default关键字后缺少分号。
Initialize syntax error 初始化语法错误初始化过程缺少或多出了运算符,或出现括号不匹配及其他不正常情况。
Invalid indirection 间接运算符错间接运算符(*)要求非空指针作为运算分量。
Invalid macro argument separator 无效的宏参数分隔符在宏定义中,参数必须用逗号分隔。编译程序发现在参数名后面有其他非法字符时,出现本错误。
Invalid pointer addition 无效的指针相加源程序中试图把两个指针相加。
Invalid use of arrow 箭头使用错在箭头运算符后必须跟一标识符。
Invalid use of dot 点使用错在点(.)运算符后必须跟一标识符。
Lvalue required 赋值请求赋值运算符的左边必须是一个地址表达式,包括数值变量、指针变量、结构引用域、间接指针和数组分量。
Macro argument syntax error 宏参数语法错误宏定义中的参数必须是一个标识符。若编译程序发现所需要的参数不是标识符的字符,则出现本错误。
Macro expansion too long 宏扩展太长一个宏扩展不能多余4096个字符。当宏递归扩展自身时,常出现本错误。宏不能对自身进行扩展。
May complied only one file when an output file name is given 给出一个输出文件名时,可能只编译一个文件在命令行编译中使用-o选择,只允许一个输出文件名。此时,只编译第一个文件,其他文件被忽略。
Mismatch number of parameters in definition 函数定义中参数个数不匹配函数定义中的参数和函数原型中提供的信息不匹配。
Misplaced break break位置错误编译程序发现break语句在switch语句或循环结构之外。
Misplaced continue continue位置错误编译程序发现continue语句在循环结构之外。
Misplaced decimal point 十进制小数点位置错编译程序发现浮点常数的指数部分有一个十进制小数点。
Misplaced else else位置错编译程序发现else语句缺少与之相匹配的if语句。本错误的产生,除了由于else多余外,还有可能由于多余的分号或漏写了大括号及前面的if语句出现语法错误而引起的。
Misplace elif directive elif命令位置错编译程序找不到与#elif命令相匹配的#if、#ifdef或#ifndef命令。
Misplace else directive else命令位置错编译程序找不到与#else命令相匹配的#if、#ifdef或#ifndef命令。
Misplace endif directive endif命令位置错编译程序找不到与#endif命令相匹配的#if、#ifdef或#ifndef命令。
Must be addressable 必须是可编址的取址操作(&)作用于一个不可编址的对象,如寄存器变量。
Must take address of memory location 地址运算符&作用于不可编址的表达式源文件中对不可编址的表达式使用了地址操作符(&),如对寄存器变量。
No file name ending 无文件名终止符在#include语句中,文件名缺少正确的闭引号(")或右尖括号(>)。
No file name giver 未给出文件名
Turbo C 编译命令(TCC)中没有包含文件名。必须指定一个源文件名。
Non-portable poiter assignment 不可移植指针赋值源程序中将一个指针赋给一个非指针或相反。但作为特例,允许把常量零值赋给一个指针。如果合适,应该强行抑制本错误信息。
Non-portable pointer comparison 不可移植指针比较源程序中将一个指针和一个非指针(常量零除外)进行比较。如果合适,应该强行抑制本错误信息。
Non-portable pointer conversion 不可移植返回类型转换在返回语句中的表达式类型与函数说明中的类型不同。但如果函数的返回表达式是指针,则可以进行转换。此时,返回指针的函数可能送回一常量零,而零被转换成一个适当的指针值。
Not an allowed type 不允许的类型在源文件中声明了几种禁止的类型,如声明函数返回一个函数或数组。
Out of memory 内存不够所有工作内存耗尽,应把文件放到一台有较大内存的机器去执行或简化源程序。
Pointer required on left side of-> ->操作符左边须是一指针在->的左边未出现指针。
Redeclaration of 'xxxxxxxx',xxxxxxxx”重定义此标识已经定义过。
Size of structure or array not known 结构体或数组大小不确定有些表达式(如sizeof或存储说明)中出现一个未定义的结构体或一个空长度数组。如果结构长度不需要,则在定义之前就可引用;如果数组不申请存储空间或者初始化时给定了长度,那么就可以定义为空长。
Statement missing ; 语句缺少“;”
编译程序发现一表达式语句后面没有分号。
Structure of union syntax error 结构体或共用(联合)语法错误编译程序发现struct或union关键字后面没有标识符或左花括号({)。
Structure size too large 结构体太大源文件中说明了一个结构体,它所需的内存区域太大以致内存不够。
Subscripting missing ] 下标缺少“]”
编译程序发现一个下标表达式缺少闭方括号。可能是由于漏掉、多写操作符或括号不匹配引起的。
Switch statement missing( 语句缺少“(”
在switch语句中,关键字switch后面缺少左括号。
Switch statement missing ) 语句缺少“)”
在switch语句中,测试表达式后面缺少右括号。
Too few parameters in call 函数调用参数太少对带有原型的函数调用(通过一个函数指针)参数太少。原型要求给出所有参数。
Too few parameter in call to 'xxxxxxxx' 调用“xxxxxxx”时参数太少调用指定的函数(该函数用一原型声明)时,给出的参数太少。
Too many cases case太多
Switch语句最多只能有257个case。
Too many decimal points 十进制小数点太多编译程序发现一个浮点常量中带有不止一个的十进制小数点。
Too many default cases default情况太多编译程序发现一个switch语句中有不止一个的default语句。
Too many exponents 阶码太多编译程序发现一个浮点常量中有不止一个的阶码。
Too many initializers 初始化太多编译程序发现初始化比声明所允许的要多。
Too many storage classes in delaration 声明中存储类太多一个声明只允许有一种存储类。
Too many types in declaration 声明中类型太多一个声明只允许有一种下列基本类型:char、int、float、double、struct、union、enum或typedef。
Too much auto memory in function 函数中自动存储太多当前函数声明的自动存储超过了可用的内存空间。
Too much code define in file 文件定义的代码太多当前文件中函数的总长度超过64K字节。可以移去不必要的代码或把源文件分开来写。
Too much global data define in file 文件中定义的全局数据太多全局数据声明的总数超过64K字节。检查一些数组的定义是否太长。如果所有的声明都是必要的,考虑重新组织程序。
Two consecutive dots 两连续点因为省略号包含三个点(…),而十进制小数点和选择运算符使用一个点(.),所以在C程序中出现两个连续点是不允许的。
Type mismatch in parameter # 参数“#”类型不匹配通过一个指针访问已由原型说明的参数时,给定参数#N(从左到右N逐个加1)不能转换为已声明的参数类型。
Type mismatch in parameter # in call to 'xxxxxxxx' 调用“xxxxxxxx”时参数类型不匹配源文件中通过一个原型说明了指定的函数,而给定的参数从左到右(从左到右N逐个加1)不能转换为已说明的参数类型。
Type mismatch in parameter 'xxxxxxxx' 参数“xxxxxxxx”类型不匹配源文件中通过一个原型声明了可由函数指针调用的函数,而所指定的参数不能转换为已声明的参数类型。
Tupe mismatch in parameter 'xxxxxxxx' in call to 'yyyyyyyy' 调用“yyyyyyyy”时参数“xxxxxxxx”类型不匹配源文件中通过一个原型声明了指定的参数,而指定参数不能转换为另一个已声明的参数类型。
Type mismatch in redeclaration of 'xxx' 重定义类型不匹配源文件中把一个已经声明的变量重新声明为另一种类型。如果一个函数被调用,而后又被声明成非整型也会产生本错误。发生这种情况时,必须在第一次调用函数前给函数加上extern声明。
Unable to create output file 'xxxxxxxx.xxx' 不能创建输出文件“xxxxxxxx.xxx”
当工作软盘已满或有写保护时产生本错误。如果软盘已满,则删除一些不必要的文件后重新编译;如果软盘有写保护,则把源文件移到一个可写的软盘上并重新编译。
Unable to creater turboc.lnk 不能创建turboc.lnk
编译程序不能创建临时文件TURBOC.LNK,因为它不能存取磁盘或者磁盘已满。
Unable to execute command 'xxxxxxxx' 不能执行“xxxxxxxx”命令找不到TLINK或MASM,或者磁盘出错。
Unable to open include file 'xxxxxxxx.xxx' 不能打开包含文件“xxxxxxxx.xxx”
编译程序找不到该包含文件。可能是由于一个#include文件包含它本身而引起的,也可能是根目录下的CONFIG.SYS中没有设置能同时带开的文件个数(试加一句files=20)。
Unable to open input file 'xxxxxx.xxx' 不能打开输入文件“xxxxxxxx.xxx”
当编译程序找不到源文件时出现本错误。检查文件名是否拼写错或检查对应的软盘或目录中是否有此文件。
Undefined label 'xxxxxxxx' 标号“xxxxxxxx”未定义函数中goto语句后的标号没有定义。
Undefined structure 'xxxxxxxx' 结构体“xxxxxxxx”未定义源文件中使用了未经说明的某个结构体。可能是由于结构体名拼写错误或缺少结构体说明而引起的。
Undefined symbol 'xxxxxxxx' 符号“xxxxxxxx”未定义标识符无定义,可能是由于说明或引用处有拼写错误,也可能是由于标识符说明错误引起。
Unexpected end of file in comment started on line 源文件在某个注释中意外结束通常是由于注释结束标志(*/)漏掉引起的。
Unexpected end of file in conditional stated on line # 源文件在#行开始的条件语句中意外结束在编译程序遇到#endif前源程序结束,通常是由于#endif漏掉或拼写错误引起的。
Unknown preprocessor directive 'xxx' 不认识的预处理命令:“xxx”
编译程序在某行的开始遇到“#”字符,但其后的命令名不是下列之一:define、undef、line、if、ifdef、ifndef、include、else或endif。
Unterminated character constant 未终结的字符常量编译程序发现一个不匹配的省略符。
Unterminated string 未终结的串编译程序发现一个不匹配的引号。
Unterminated string or character constant 未终结的串或字符常量编译程序发现串或字符常量开始后没有终结。
User break 用户中断在集成环境里进行编译或连接时用户按了Ctrl+Break键。
While statement missing( while的表达式语句漏掉“(”
在while语句中,关键字while后缺少左括号。
While statement missing ) while语句漏掉“)”
在while语句中,关键字while的表达式后缺少右括号。
Wrong number of arguments in of 'xxxxxxxx' 调用“xxxxxxxx”时参数个数错误源文件中调用某个宏时,参数个数不对。
3,警告
'xxxxxxxx' delared but never used 声明了“xxxxxxxx”但未使用在源文件中说明了此变量,但没有使用。当编译程序遇到复合语句函数的结束处括号时,发出本警告。
'xxxxxxxx' is assigned a value which is never used,xxxxxxxx”被赋一个不使用的值此变量出现在一个赋值语句里,但直到函数结束都未使用过。当编译程序遇到结束的闭花括号时发出本警告。
'xxxxxxxx' not part of structure,xxxxxxxx”不是结构体的一部分出现在点(.)或箭头(->)的左边的域名不是结构体的一部分,或者点的左边不是结构体,箭头的左边不指向结构。
Ambiguous operators need parenthese 歧义运算符需要括号当两个位移、关系或按位操作符在一起使用而不加括号时,发出本警告;当一加法或减法操作符不加括号与一位移操作符出现在一起时,也发出本警告。程序员常常混淆这些操作符的优先级,因为它们的优先级不太直观。
Both return and return of a value used 既使用返回又使用返回值编译程序发现一个与前面定义的return语句不一致的return语句,发出本警告。当某函数只在一些return语句中返回值时一般会产生错误。
Call to function with prototype 调用无原型函数如果“原型请求”警告可用,且又调用了一个原型的函数,就发出本警告。
Call to function 'xxxx' with prototype 调用无原型的函数“xxxx”
如果“原型请求”警告可用,且又调用了一个原先没有原型的函数“xxxx”,就发出本警告。
Code has no effect 代码无效当编译程序遇到一个含有无效操作符的语句时,发出本警告。例如语句“a+b”;对每一个变量都不起作用,无需操作,且可能引起一个错误。
Constant is long 常量是long类型若编译程序遇到一个十进制常量大于32767,或一个八进制常量大于65535,而其后没有字母“l”或“L”,把此常量当作long类型处理。
Constant out of range in comparison 比较时常量超出了范围在源文件中有一比较语句,其中一个常量子表达式超出了另一个子表达式类型所允许的范围。如一个无符号量与-1比较就没有意义。为得到一大于32767(十进制)的无符号常量,可以在常量前加上unsigned(如:(unsigned)65535)或在常量后加上字母“u”或“U”(如:65535u)。
Conversion may lose significant digits 转换可能丢失高位数字在赋值操作或其他情况下,源程序要求把long或unsigned long类型转变成int或unsigned int类型。在有些机器上,因为int型和long型变量具有相同长度,这种转换可能改变程序的输出特性。无论本警告何时发生,编译程序仍将产生代码来做比较。如果代码比较后总是给出同样结果,比如一个字符表达式与4000比较,则代码总要进行测试。这还表示一个无符号表达式可以与-1进行比较,因为8087机器上,一个无符号表达式与-1有相同的位模式。
Function should return a value 函数应该返回一个值源文件中声明的当前函数返回类型既非int也非void型,但编译程序未发现返回值。返回int型的函数可以不说明,因为在老版本的C语言中,没有void类型来指出函数不返回值。
Mixing pointers to signed and unsigned char 混淆signed和unsigned char指针没有通过显式的强制类型转换,就把一个字符指针转变为无符号指针,或把一个无符号指针转变为字符指针。
No decaration for function 'xxxxxxxx' 函数“xxxxxxxx”没有声明当“声明请求”警告可用,而又调用了一个没有预先声明的函数时,发出本警告。函数声明可以是传统的,也可以是现代(原型)的风格。
Non-portable pointer assignment 不可移植指针赋值源文件中把一个指针赋给另一个非指针或相反。作为特例,可以把常量零赋给一指针。如果合适,可以强行抑制本警告。
Non-portable pointer comparison 不可移植指针比较源文件中把一个指针和另一非指针(非常量零)进行比较。如果合适,可以强行抑制本警告。
Non-portable return type conversion 不可移植返回类型转换
return语句中的表达式类型和函数声明的类型不一致。作为特例,如果函数或返回表达式是一个指针,这是可以的。在此情况下返回指针的函数可能返回一个常数零,而零被转变成一个适当的指针值。
Parameter 'xxxxxxxx' is never used 参数“xxxxxxx”从未使用函数说明中的某参数在函数体里从未使用。这可以但不一定是一个错误,通常是由于参数名拼写错误而引起。如果在函数体内,该标识符被重新定义为一个自动(局部)变量,也将产生本警告。此参数被标识为一个自动变量但未使用。
Possible use of 'xxxxxxxx' befor definition 在定义“xxxxxxxx”之前可能已使用源文件的某表达中使用了未经赋值的变量,编译程序对源文件进行简单扫描以确定此条件。如果该变量出现的物理位置在对它赋值之前,就会产生本警告,当然程序的实际流程可能在使用前已赋值。
Possible incorrect assignment 可能的不正确赋值当编译程序遇到赋值操作符作为条件表达式(如if、while或do-while语句的一部分)的主操作符时,发生本警告,通常是由于把赋值号当作等号使用了。如果希望禁止此警告,则可把赋值语句用括号括起来,并且把它与零作显式比较。如:
if(a=b)应写成if ((a=b)!=0)
Redefinition of 'xxxxxxxx ' is not identical“xxxxxxxx”的重定义不相同源文件中对命名宏重定义时,使用的正文内容与第一次定义时不同,新内容将代替旧内容。
Restarting compiler using assembly 用汇编重新启动编译编译程序遇到一个未使用命令行选择项-B或#pragma inline 语句的asm,通过使用汇编重新启动编译。
Structure passed by value 结构按值传递如果“结构按值传送”警告可用,则在结构作为参数按值传送时产生本警告。通常是在编制程序时,把结构体作为参数传递,而又漏掉了地址操作符(&)。因为结构体可以按值传递,所以这种遗漏是可接收的。本警告只起一个提示作用。
Superfluous & with function or array 在函数或数组中有多余的“&”号取址操作符“&”对一个数组或函数名是不必要的,应该去掉。
Suspicious pointer conversion 可疑的指针转换编译程序遇到一些指针转换,这些转换引起指针指向不同的类型。如果合适,应强行抑制本警告。
Undefined structure 'xxxxxxxx' 结构体“xxxxxxxx”未定义在源文件中使用了该结构,但未定义。可能是由于结构体名拼写错误或忘记定义而引起的。
Unknown assembler instruction 不认识的汇编命令编译程序发现在插入的汇编语句中有一个不允许的操作码。检查此操作的拼写,并查看一下操作码表看该命令能否被接受。
Unreachable code 不可达代码
break、continue、goto或return语句后没有跟标号或循环函数的结束符。编译程序使用一个常量测试条件来检查while、do和for循环,并试图知道循环没有失败。
Void function may not return a value void函数不可以返回值源文件中的当前函数说明为void,但编译程序发现一个带值的返回语句,该返回语句的值将被忽略。
Zero length structure 结构长度为零在源文件中定义了一个总长度为零的结构,对此结构的任何使用都是错误的。
附录四 常用C库函数
4.1 数学函数在使用数学函数时,应该在该源文件中使用以下命令行:
#include <math.h> 或 #include "math.h"
表1-9 math.h函数列表函数名
函数原型
功 能
返回值
说 明
abs
int abs(int x);
求整数x的绝对值
计算结果
acos
double acos(double x);
计算arccos (x)的值
计算结果
-1≤x≤1
asin
double asin(double x);
计算arcsin(x)的值
计算结果
-1≤x≤1
atan
double atan(double x);
计算arctan (x)的值
计算结果
atan2
double atan2(double x,double y);
计算arctan(x/y)的值
计算结果
cos
double cos(double x);
计算cos (x)的值
计算结果
x单位为弧度
cosh
double cosh(double x);
计算x的双曲余弦值
计算结果
exp
double exp(double x);
计算ex的值
计算结果
ceil
double ceil(double x);
计算不小于x的最小整数
计算结果
fabs
double fabs(double x);
求x的绝对值
计算结果
floor
double floor(double x);
求不大于x的最大整数
该整数的双精度实数
fmod
double fmod(double x,double y);
求整除x/y的余数
返回余数的双精度数
frexp
double frexp(double val,int *eptr);
把双精度数val分解为数字部分(尾数)x和以2为底的指数n,即val=x*2n,n存放在eptr指向的变量中
返回数字部分x,0.5≤x≤1
log
double log(double x);
求logex,即lnx
计算结果
log10
double log10(double x);
求log10x
计算结果
modf
double modf(double val,int *iptr);
把双精度数val分解为整数部分和小数部分,把整数部分存在iptr指向的单元
val的小数部分
pow
double pow(double x,double y);
计算xy的值
计算结果
sin
double sin(double x);
计算sin(x)的值
计算结果
x单位为弧度
sinh
double sinh(double x);
计算x的双曲正弦值
计算结果
sqrt
double sqrt(double x);
计算
计算结果
x≥0
tan
double tan(double x);
计算tan(x)的值
计算结果
x单位为弧度
tanh
double tanh(double x);
计算x的双曲正切值
计算结果
4.2 输入输出函数在使用输入输出函数时,应该在该源文件中使用以下命令行:
#include <stdio.h> 或 #include "stdio.h"
表1-10 stdio.h函数列表函数名
函数原型
功 能
返回值
说 明
cleraerr
void clearer(FILE *fp);
清除fp指向的文件的错误标志,同时清除文件结束标志
无
close
int close(int fd);
关闭文件
关闭成功,返回0;否则返回-1
非ANSI标准
creat
int creat(char *filename,int mode);
以mode所指定的方式建立文件,文件名为filename
成功则返回正数;否则返回-1
非ANSI标准
eof
int eof(int fd);
判断是否处于文件结束
遇到文件结束,返回1;否则返回0
非ANSI标准
fclose
int fclose(FILE *fp);
关闭fp所指向的文件,释放文件缓冲区
关闭成功,返回0;否则返回非0
feof
int feof(FILE *fp);
检查文件是否结束
遇文件结束符返回非0,否则返回0
ferror
int ferror(FILE *fp);
测试fp所指向的文件是否有错
无错返回0,有错返回非0
fflush
int fflush(FILE *fp);
把fp指向的文件的所有数据和控制信息存盘
成功返回0,否则返回非0
fgetc
int fgetc(FILE *fp);
从fp所指向的文件中取得下一个字符
返回所得到的字符。出错则返回EOF
fgets
char *fgets(char *buf,int n,FILE *fp,);
从fp指向的文件读取一个长度为n-1的字符串,存入起始地址为buf的空间
成功,返回地址buf。若遇文件结束或出错,返回NULL
fopen
FILE *fopen(char *filename,char *mode);
以mode指定的方式打开名为filename的文件
成功,返回一个文件指针(文件信息区的起始地址),否则返回0
fprintf
int fprintf(FILE *fp,char *format,args,…);
把args的值以format指定的格式输出到fp所指定的文件中
实际输出的字符数
fputc
int fputc(char ch,FILE *fp);
将字符ch输出到fp指定的文件中
成功,则返回该字符;否则返回EOF
fputs
int fputs(char *str,FILE * fp);
将str指定的字符串输出到fp所指定的文件
成功,返回0;否则返回非0值
fread
int fread(char *pt,unsigned size,unsigned n,FILE *fp);
从fp所指定的文件中读取长度为size的n个数据项,存到pt指向的内存区
返回所读的数据项个数,如遇文件结束或出错则返回0
freopen
FILE *freopen(char *fname,char *mode,FILE *fp);
用fname所指定的文件替换fp所指定的文件,fname文件的打开方式由mode定义
成功,返回文件指针fp;否则返回NULL
fscanf
int fscanf(FILE *fp,char *format,args,…);
从fp指定的文件中按format给定的格式将输入数据送到args所指向的内存单元(args是指针)
已输入的数据个数
fseek
int fseek(FILE *fp,long offset,int base);
将fp所指向文件的位置指针移到以base所指出的位置为基准、以offset为位移量的位置
返回当前位置,否则返回-1
ftell
long ftell(FILE *fp);
返回fp所指向的文件中的读写位置
返回fp所指向的文件中的读写位置
fwrite
int fwrite(char *ptr,unsigned size,unsigned n,FILE *fp);
把ptr所指向的n*size个字节输出到fp所指向的文件中
写到文件中的数据项个数
getc
int getc(FILE *fp);
从fp所指向的文件读入一个字符
返回所读的字符,若文件结束或出错,返回EOF
getchar
int getchar(void);
从标准输入设备读取一个字符
所读字符,若文件结束或出错,则返回-1
gets
char *gets(char *str);
从标准输入设备读取字符串,并把它们放入由str指向的字符数组中
成功,返回str;否则返回NULL
getw
int getw(FILE *fp);
从fp所指向的文件读取下一个字(整数)
输入的整数。若文件结束或出错,返回-1
非ANSI标准函数
kbhit
int kbhit( );
判断是否有键被按下
若有键被按下,返回一个非0值,否则返回0
lseek
long lseek(int fd,long offset,int base);
根据base所确定的位置,按offset的偏移量调整fd所指定的文件中的读写位置
成功,返回该文件中的当前位置;否则,返回-1
open
int open(char *filename,int mode);
以mode指出的方式,打开已存在的名为filename的文件
成功,返回文件号(正数),否则返回-1
非ANSI标准函数
printf
int printf(char *format,args,…);
按format指向的字符串规定的格式,将输出表列args的值输出到标准输出设备
输出字符的个数。若出错,返回负数
format可以是一个字符串或字符数组起始地址
putc
int putc(int ch,FILE *fp);
把一个字符ch输出到fp所指定的文件中
输出的字符ch。若出错,返回EOF
putchar
int putchar(int ch);
把字符ch输出到标准输出设备
输出的字符ch。若出错,返回EOF
puts
int puts(char *str);
把str指向的字符串输出到标准输出设备,将\0转换为回车换行
成功,返回换行符;失败返回EOF
putw
int putw(int i,FILE *fp);
将一个整数i(即一个字)写到fp指向的文件中
返回输出的整数;失败返回EOF
非ANSI标准函数
read
int read(int fd,char *buf,unsigned count);
从文件号fd所指示的文件中读count个字节到由buf指示的缓冲区
返回读入的字节个数。如遇文件结束返回0,出错返回-1
非ANSI标准函数
remove
int remove(char *filename);
删除以filename为文件名的文件
成功,返回0;失败返回-1
rename
int rename(char *oldname,char *newname);
把oldname所指的文件名改为由newname所指的文件名
成功,返回0;失败返回-1
rewind
void rewind(FILE *fp);
将fp指示的文件中位置指针置于文件开头位置,并清除文件结束标志和错误标志
无
scanf
int scanf(char *format,args,…);
从标准输入设备按format指定的格式字符串规定的格式,输入数据给args所指向的单元(args为指针)
读入并赋给args的数据个数。遇文件结束返回EOF;出错返回0
tell
long int tell(int fd);
确定文件描述号fd所对应的文件位置指示器的当前值
返回文件位置指示器的当前值;若出错返回-1
setbuf
void setbuf(FILE *fp,char *buf);
说明fp将要使用的缓冲区(长度为buf)。若buf置为NULL,则关闭缓冲区
无
setvbuf
int setvbuf(FILE *fp,char *buf,int mode,int size);
为fp所指向的文件提供输入输出操作的缓冲区buf,缓冲区的大小是size,使用方式为mode
成功,返回0;失败返回非0
sprintf
int sprintf(char *buf,char *format,args,…);
把按forrnat规定的格式的args数据,送到buf所指向的数组中
返回实际放进数组中的字符数
sscanf
int sscanf(char *buf,char *format,args,…);
按format规定的格式从buf指向的数组中读入数据给args所指向的单元(args为指针)
返回值为实际赋值的个数;若返回0,则无任何字段被赋值
tmpnam
char *tmpnam(char *name);
生成一个与目录中其他文件名不同的临时文件名,并把它放入由name指向的数组中
成功,返回指向name的指针,否则返回NULL指针
tmpfile
FILE *tmpfile( );
打开一个临时文件并返回指向这个文件的指针。由该函数产生的临时文件在被关闭或程序结束时会自动被删除
成功,返回指向文件的指针;失败返回NULL
write
int write(int fd,char *buf,unsigned int size);
把buf指向的缓冲区中size个字节写到fd文件中
返回实际写出的字节数;出错,返回-1
非ANSI标准函数
4.3 字符函数
ANSI C标准要求在使用字符函数时要包含头文件ctype.h。有的C编译不遵循ANSI C标准的规定,而用其它名称的头文件。请使用时查阅相关手册。
表1-11 ctype.h函数列表函数名
函数原型
功 能
返回值
说 明
isalnum
int isalnum(int ch);
检查ch是否是字母(alpha)或数字(number)
是字母或数字返回非0值;否则返回0
isalpha
int isalpha(int ch);
检查ch是否是字母
是,返回非0值;不是则返回0
iscntrl
int iscntrl(int ch);
检查ch是否控制字符(其ASCII码在0和0x1F之间)
是,返回非0值;不是则返回0
isdigit
int isdigit(int ch);
检查ch是否是数字(0~9)
是,返回非0值;不是则返回0
isgraph
int isgraph(int ch);
检查ch是否可打印字符(其ASCII码在0x21到0x7E之间),不包括空格
是,返回非0值;不是则返回0
islower
int islower(int ch);
检查ch是否小写字母(a~z)
是,返回非0值;不是则返回0
isprint
int isprint(int ch);
检查ch是否可打印字符(其ASCII码在0x20到0x7E之间),包括空格
是,返回1;否则返回0
ispunct
int ispunct (int ch);
检查ch是否标点符号(不包括空格),即除字母、数字和空格以外的所有可打印字符
是,返回1;否则返回0
isspace
int isspace(int ch);
检查ch是否空格、制表符或换行符
是,返回1;否则返回0
isupper
int isupper(int ch);
检查ch是否大写字母(A~Z)
是,返回非0值;不是则返回0
isxdigit
int isxdigit(int ch);
检查ch是否一个16进制数学字符(即0~9,或A~F,或a~f)
是,返回非0值;不是则返回0
tolower
int tolower(int ch);
将ch字符转换为小写字母
返回ch所代表的字符的小写字母
toupper
int toupper(int ch);
将ch字符转换为大写字母
返回ch所代表的字符的大写字母
4.4 字符串函数在使用字符串函数时,应包含头文件“string.h”。
表1-12 string.h函数列表函数名
函数原型
功 能
返回值
说明
memchr
void memchr(void *buf,int ch,unsigned int count);
在buf的前n个字符里搜索ch第一次出现的位置
返回指向buf中ch第一次出现的位置的指针;未找到则返回NULL
memcmp
int memcmp(void *buf1,void *buf2,unsigned int count);
按字典顺序比较由buf1和buf2指向的数组的前count个字符
buf1小于buf2时,返回一个负整数;buf1等于buf2时,返回0;buf1大于buf2时,返回一个正整数;
memcpy
void *memcpy(void *to,void *from,unsigned int count);
把from指向的数组中的前count个字符拷贝到to指向的数组中
返回指向to的指针
memmove
void *memmove(void *to,void *from,unsigned int count);
从from指向的数组中把前count个字符拷贝到to指向的数组中
返回指向to的指针
memset
void *memset(void *buf,int ch,unsigned int count);
将buf中的前count个字符设置为ch
返回buf
strcat
char *strcat(char *str1,char *str2);
把字符串str2连接到str1后面,原str1后面的\0被删除
返回str1
strchr
char *strchr(char *str,int ch);
找出str指向的字符串中第一次出现字符ch的位置
返回指向该位置的指针;未找到则返回NULL
strcmp
int strcmp(char *str1,char *str2);
比较两个字符串str1和str2。从第一个字符开始比较,直到遇到不同的字符或已到串尾。
str1>str2,返回正整数;str1=str2,返回0;str1<str2,返回负整数
strcpy
char *strcpy(char *str1,char *str2);
把str2指向的字符串拷贝到str1中
返回str1
strlen
unsigned int strlen(char *str);
统计字符串str中字符的个数(不包括终止符'\0')
返回字符个数
strcspn
int strcspn(char *str1,char *str2);
确定str1中出现的属于str2的第一个字符的下标
返回字符下标
strncat
char *strncat(char *str1,char *str2,unsigned int count);
把str2指向的字符串中最多count个字符连到str1后面,并用NULL结尾
返回str1
strncmp
int strncmp(char *str1,char *str2,unsigned int count);
按字典顺序比较两个以NULL结尾的字符串中最多count个字符
str1>str2,返回正整数;str1=str2,返回0;str1<str2,返回负整数
strncpy
char *strncpy(char *str1,char *str2,unsigned int count);
把str2中最多count个字符拷贝到str1中去
返回str1
strpbrk
char *strcspn(char *str1,char *str2);
确定str1中第一个与str2中任何一个字符相匹配的指针位置
返回str1中第一个与str2中任何一个字符相匹配的字符指针。如果未找到则返回空
strspn
int strspn(char *str1,char *str2);
确定str1中出现的属于str2的第一个字符的下标
返回str1中属于str2的第一个字符的下标
strstr
char *strstr(char *str1,char *str2);
查找str2指向的字符串在str1指向的字符串中第一次出现的位置
子串首次出现的地址。如果找不到,则返回空指针NULL
4.5 动态存储分配函数
ANSI C标准建议设4个有关的动态存储分配的函数,即calloc( )、malloc( )、free( )、realloc( )。实际上,许多C编译系统在实现时,往往增加了一些其它函数。ANSI建议在头文件stdlib.h中包含有关信息,但许多C编译要求用malloc.h。因此,在使用时应查阅相关手册。
ANSI标准要求动态分配系统返回void指针。void指针具有一般性,它们可以指向任何类型的数据。但目前有的C编译系统所提供的这类函数返回char指针。但无论以上哪种情况,都需要用强制类型转换的方法把void或char指针转换成所需的类型。
表1-13 mallo.h函数列表函数名
函数原型
功 能
返回值
说明
calloc
void *calloc(unsigned int n,unsigned int size);
分配n个数据项的连续内存空间,每个数据项的大小为size
分配内存单元的起始地址。如分配不成功,则返回NULL
free
void free(void *p);
释放p所指向的内存区
无
malloc
void *malloc(unsigned int size);
分配size个字节的存储区
返回所分配内存区的起始地址。若内存不够,返回NULL
realloc
void *realloc(void *p,unsigned int size);
将p所指出的已分配内存区的大小改为size。size可以比原来分配的空间大或小
返回指向该内存区的指针
4.6 时间函数当需要使用系统日期和时间函数时,需要头文件time.h。其中定义了三个类型:clock_t和time_t用来表示系统的时间和日期,结构体类型tm把日期和时间分解成为它的成员。tm结构体类型的定义如下:
struct tm {
int tm_sec; /* 秒,0~59 */
int tm_min; /* 分,0~59 */
int tm_hour; /* 小时,0~23 */
int tm_mday; /* 天,0~31 */
int tm_mon; /* 从一月开始的月数,0~11 */
int tm_year; /* 自1990开始的年数 */
int tm_wday; /* 自星期日开始的天数,0~6 */
int tm_yday; /* 自1月1日起的天数,0~365 */
int tm_isdst; /* 夏季时间标志 */
};
另外用于存放时间和日期的结构体的定义为:
struct time {
unsigned char ti_min; /* 分 */
unsigned char ti_hour; /* 时 */
unsigned char ti_hund; /* 百分之一秒 */
unsigned char ti_sec; /* 秒 */
};
struct date {
int da_year; /* 年 */
char da_day; /* 日 */
char da_mon; /* 月,1~12 */
};
表1-14 time.h函数列表函数名
函数原型
功 能
返回值
说明
asctime
char *asctime(struct tm *p);
将日期和时间转换成ASCII字符串
返回一个指向字符串的指针
clock
clock_t clock( );
确定程序运行到现在所花费的时间(用返回值除以宏CLK_TCK得到的时间为秒数)
返回程序开始到该函数被调用所花费的时间。若失败,则返回-1
ctime
char *ctime(time_t *time);
把日期和时间转换成字符串
返回指向包含日期和时间的字符串指针
difftime
double difftime(time_t time2,time_t time1);
计算time1和time2之间所差的秒数
返回两个时间的双精度差值
getdate
void getdate(struct date *datep);
取得系统的当前日期,放在datep所指向的结构体中(需要包含的头文件是dos.h)
无
gettime
void gettime(struct time *timep);
取得系统的当前时间,放在datep所指向的结构体中(需要包含的头文件是dos.h)
无
gmtime
struct tm *gmtime(time_t *time);
将日期和时间转换为格林威治标准时间
返回指向结构体tm的指针
setdate
void setdate(struct date *datep);
将系统日期设置为datep所指向的结构体中定义的日期
无
settime
void settime(struct time *timep);
将系统时间设置为timep所指向的结构体中定义的时间
无
time
time_t time(time_t time);
取得系统的当前时间
返回系统的当前日历时间。若无系统时间,返回-1
4.7 其它函数这里列出的函数都是标准库函数,但却不容易归到前面的某一类中。使用这些函数要包含头文件stdlib.h。这个文件定义了两个类型:div_t和ldiv_t。它们的定义如下:
typedef struct {
int quot;
int rem;
} div_t;
typedef struct {
long quot;
long rem;
} ldiv_t;
表1-15 stdlib.h函数列表函数名
函数原型
功 能
返回值
说明
abort
void abort( );
立刻结束程序运行,不清理任何文件缓冲区
无
atof
double atof(char *str);
把str指向的字符串转换成一个double值
返回双精度计算结果
atoi
int atoi(char *str);
将ASCII字符串转换为整数
返回转换结果
atol
long atol(char *str);
将str指向的ASCII值符串转换为长整型值
返回转换结果。若不能转换,返回0
bsearch
void *bsearch(void *key,void *base,unsigned int num,unsigned int size,int (*compare)( ));
对一个base指向的已排好序的数组进行二分查找。数组的元素个数是num,每一元素的大小为size字节。compare指向的函数用来把数组元素与关键字进行比较
返回一个指向匹配key所指向的关键字的第一个成员的指针。若没有找到,则返回NULL
exit
void exit(int status);
使程序立刻正常终止。status的值传给调用过程
无
div
div_t div(int num,int denom);
计算num/denom
返回计算的商和余数。
itoa
char *itoa(int num,char *str,int radix);
把整数num转换成与其等价的字符串,并把结果放在str指向的字符串中,由radix决定在转换成输出串时所采用的进制数
返回一个指向str的指针
ldiv
ldiv_t ldiv(long int num,long int denom);
计算num/denom
返回商和余数。
ltoa
char *ltoa(long num,char *str,int radix);
把长整数num转换成与其等价的字符串,并把结果放在str指向的字符串中,由radix决定在转换成输出串时所采用的进制数
返回一个指向str的指针
qsort
void qsort(void *base,unsigned int num,unsigned int size,int (*comp)( ));
反复调用comp所指向的由用户自己编写的比较函数,对base指向的数组进行排序。num是数组的元素个数,size是数组元素的字节数。
无
rand
int rand( );
产生随机数
返回0到RAND_MAX之间的整数。RAND_MAX是返回的最大可能值,在头文件中定义
strtod
double strtod(char *start,char **end);
把存储在start指向的数字字符串转换成double类型,直到出现不能转换成浮点数的字符为止,剩余的字符串赋给指针end
返回转换结果。若未进行转换,则返回0。若转换发生错误,则返回HUGE_VAL或_HUGE _VAL表示上溢或下溢
strtol
long int strtol(char *start,char **end,int radix);
把start指向的数字字符串转换成long int类型,直到出现不能转换成长整数的字符为止,剩余的字符串赋给指针end,数字的进制由radix确定
返回转换结果。若未进行转换,则返回0。若转换发生错误,则返回LONG_MAX或LONG_MIN表示上溢或下溢
strtoul
unsigned long int strtoul (char *start,char **end,int radix);
把start指向的数字字符串转换成unsigned long int类型,直到出现不能转换成unsigned long int的字符为止,剩余的字符串赋给指针end,数字的进制由radix确定
返回转换结果。若未进行转换,则返回0。若转换发生错误,则返回ULONG_MAX或ULONG_MIN表示上溢或下溢
system
int system(char *str);
把str指向的字符串作为一个命令传送到操作系统的命令处理程序中
返回值依赖于不同的编译版本。通常,成功执行返回0,否则返回一个非0值
参考文献谭浩强,C程序设计(第二版),北京:清华大学出版社,1999
谭浩强,C程序设计题解与上机指导(第二版),北京:清华大学出版社,2000
谭浩强,张基温,唐永炎,C语言程序设计教程(第二版),北京:高等教育出版社,1998
谭浩强,张基温,C语言习题集与上机指导(第二版),北京:高等教育出版社,1998
James W.McCord,张素琴,李景淑,李旭译,Borland C++ 3.1程序员参考手册,北京:清华大学出版社,1995
Herbert Schildt,戴健鹏译,C语言大全(第二版),北京:电子工业出版社,1995
网冠科技,C语言时尚编程百例,北京:机械工业出版社,2004