You are on page 1of 243

下载

第1章 程序设计与算法
1.1 程序设计语言的发展

自1 9 4 6年世界上第一台电子计算机问世以来,计算机科学及其应用的发展十分迅猛,计
算机被广泛地应用于人类生产、生活的各个领域,推动了社会的进步与发展。特别是随着国
际互联网( I n t e r n e t)日益深入千家万户,传统的信息收集、传输及交换方式正被革命性地改
变,我们已经难以摆脱对计算机的依赖,计算机已将人类带入了一个新的时代 — 信息时代。
新的时代对于我们的基本要求之一是:自觉地、主动地学习和掌握计算机的基本知识和
基本技能,并把它作为自己应该具备的基本素质。要充分认识到,缺乏计算机知识,就是信
息时代的“文盲”。
对于理工科的大学生而言,掌握一门高级语言及其基本的编程技能是必需的。大学学习,
除了掌握本专业系统的基础知识外,科学精神的培养、思维方法的锻炼、严谨踏实的科研作
风养成,以及分析问题、解决问题的能力的训练,都是日后工作的基础。学习计算机语言,
正是一种十分有益的训练方式,而语言本身又是与计算机进行交互的有力的工具。
一台计算机是由硬件系统和软件系统两大部分构成的,硬件是物质基础,而软件可以说
是计算机的灵魂,没有软件,计算机是一台“裸机”,是什么也不能干的,有了软件,才能灵
动起来,成为一台真正的“电脑”。所有的软件,都是用计算机语言编写的。
计算机程序设计语言的发展,经历了从机器语言、汇编语言到高级语言的历程。
1. 机器语言
电子计算机所使用的是由“ 0”和“1”组成的二进制数,二进制是计算机的语言的基础。
计算机发明之初,人们只能降贵纡尊,用计算机的语言去命令计算机干这干那,一句话,就
是写出一串串由“ 0”和“ 1”组成的指令序列交由计算机执行,这种语言,就是机器语言。
使用机器语言是十分痛苦的,特别是在程序有错需要修改时,更是如此。而且,由于每台计
算机的指令系统往往各不相同,所以,在一台计算机上执行的程序,要想在另一台计算机上
执行,必须另编程序,造成了重复工作。但由于使用的是针对特定型号计算机的语言,故而
运算效率是所有语言中最高的。机器语言,是第一代计算机语言。
2. 汇编语言
为了减轻使用机器语言编程的痛苦,人们进行了一种有益的改进:用一些简洁的英文字
母、符号串来替代一个特定的指令的二进制串,比如,用“ A D D”代表加法,“M O V”代表
数据传递等等,这样一来,人们很容易读懂并理解程序在干什么,纠错及维护都变得方便了,
这种程序设计语言就称为汇编语言,即第二代计算机语言。然而计算机是不认识这些符号的,
这就需要一个专门的程序,专门负责将这些符号翻译成二进制数的机器语言,这种翻译程序
被称为汇编程序。
汇编语言同样十分依赖于机器硬件,移植性不好,但效率仍十分高,针对计算机特定硬
件而编制的汇编语言程序,能准确发挥计算机硬件的功能和特长,程序精炼而质量高,所以
2 C语言程序设计
下载
至今仍是一种常用而强有力的软件开发工具。
3. 高级语言
从最初与计算机交流的痛苦经历中,人们意识到,应该设计一种这样的语言,这种语言
接近于数学语言或人的自然语言,同时又不依赖于计算机硬件,编出的程序能在所有机器上
通用。经过努力, 1 9 5 4年,第一个完全脱离机器硬件的高级语言 — FORTRAN问世了, 4 0多
年来,共有几百种高级语言出现,有重要意义的有几十种,影响较大、使用较普遍的有
F O RT R A N、A L G O L、C O B O L、B A S I C、L I S P、S N O B O L、P L / 1、P a s c a l、C、P R O L O G、
Ada、C++、VC、VB、Delphi、JAVA等。
高级语言的发展也经历了从早期语言到结构化程序设计语言,从面向过程到非过程化程
序语言的过程。相应地,软件的开发也由最初的个体手工作坊式的封闭式生产,发展为产业
化、流水线式的工业化生产。
6 0年代中后期,软件越来越多,规模越来越大,而软件的生产基本上是人自为战,缺乏
科学规范的系统规划与测试、评估标准,其恶果是大批耗费巨资建立起来的软件系统,由于
含有错误而无法使用,甚至带来巨大损失,软件给人的感觉是越来越不可靠,以致几乎没有
不出错的软件。这一切,极大地震动了计算机界,史称“软件危机”。人们认识到:大型程序
的编制不同于写小程序,它应该是一项新的技术,应该像处理工程一样处理软件研制的全过
程。程序的设计应易于保证正确性,也便于验证正确性。 1 9 6 9年,提出了结构化程序设计方
法,1 9 7 0年,第一个结构化程序设计语言 — P a s c a l语言出现,标志着结构化程序设计时期的
开始。
8 0年代初开始,在软件设计思想上,又产生了一次革命,其成果就是面向对象的程序设
计。在此之前的高级语言,几乎都是面向过程的,程序的执行是流水线似的,在一个模块被
执行完成前,人们不能干别的事,也无法动态地改变程序的执行方向。这和人们日常处理事
物的方式是不一致的,对人而言是希望发生一件事就处理一件事,也就是说,不能面向过程,
而应是面向具体的应用功能,也就是对象( o b j e c t)。其方法就是软件的集成化,如同硬件的
集成电路一样,生产一些通用的、封装紧密的功能模块,称之为软件集成块,它与具体应用
无关,但能相互组合,完成具体的应用功能,同时又能重复使用。对使用者来说,只关心它
的接口(输入量、输出量)及能实现的功能,至于如何实现的,那是它内部的事,使用者完
全不用关心,C++、VB、Delphi就是典型代表。
高级语言的下一个发展目标是面向应用,也就是说:只需要告诉程序你要干什么,程序
就能自动生成算法,自动进行处理,这就是非过程化的程序语言。

1.2 C语言的特点

1.2.1 C语言是中级语言

C语言通常称为中级计算机语言。中级语言并没有贬义,不意味着它功能差、难以使用、
或者比BASIC、Pascal那样的高级语言原始,也不意味着它与汇编语言相似,会给使用者带来
类似的麻烦。 C语言之所以被称为中级语言,是因为它把高级语言的成分同汇编语言的功能结
合起来了。表1-1表明了C语言在计算机语言中所处的地位。
下载
第1章 程序设计与算法 3
表1-1 C语言在计算机语言中的地位

高级 Ada、Modula-2、Pascal、COBOL、FORTRAN、BASIC
中级 C、FORTH、Macro-assembler
低级 Assembler

作为中级语言, C允许对位、字节和地址这些计算机功能中的基本成分进行操作。 C语言


程序非常容易移植。可移植性表示为某种计算机写的软件可以用到另一种机器上去。举例来
说,如果为苹果机写的一个程序能够方便地改为可以在 IBM PC 上运行的程序,则称为是可移
植的。
所有的高级语言都支持数据类型的概念。一个数据类型定义了一个变量的取值范围和可
在其上操作的一组运算。常见的数据类型是整型、字符型和实数型。虽然 C语言有五种基本数
据类型,但与 P a s c a l或A d a相比,它却不是强类型语言。 C程序允许几乎所有的类型转换。例
如,字符型和整型数据能够自由地混合在大多数表达式中进行运算。这在强类型高级语言中
是不允许的。
C语言的另一个重要特点是它仅有 3 2个关键字,这些关键字就是构成 C语言的命令。和
IBM PC的BASIC相比,后者包含的关键字达 159个之多。

1.2.2 C语言是结构化语言

虽然从严格的学术观点上看, C语言是块结构( b l o c k - s t r u c t u r e d)语言,但是它还是常被


称为结构化语言。这是因为它在结构上类似于 A L G O L、P a s c a l和M o d u l a - 2(从技术上讲,块
结构语言允许在过程和函数中定义过程或函数。用这种方法,全局和局部的概念可以通过
“作用域”规则加以扩展,“作用域”管理变量和过程的“可见性”。因为C语言不允许在函数
中定义函数,所以不能称之为通常意义上的块结构语言)。
结构化语言的显著特征是代码和数据的分离。这种语言能够把执行某个特殊任务的指令
和数据从程序的其余部分分离出去、隐藏起来。获得隔离的一个方法是调用使用局部(临时)
变量的子程序。通过使用局部变量,我们能够写出对程序其它部分没有副作用的子程序。这
使得编写共享代码段的程序变得十分简单。如果开发了一些分离很好的函数,在引用时我们
仅需要知道函数做什么,不必知道它如何做。切记:过度使用全局变量(可以被全部程序访
问的变量)会由于意外的副作用而在程序中引入错误。
结构化语言比非结构化语言更易于程序设计,用结构化语言编写的程序的清晰性使得它
们更易于维护。这已是人们普遍接受的观点了。 C语言的主要结构成分是函数 C的独立子程序。
在C语言中,函数是一种构件(程序块),是完成程序功能的基本构件。函数允许一个程序的
诸任务被分别定义和编码,使程序模块化。可以确信,一个好的函数不仅能正确工作且不会
对程序的其它部分产生副作用。

1.2.3 C语言是程序员的语言

也许你会问“所有的计算机语言不都是程序员使用的吗?”,回答是断然的“否”。我们
考虑典型的非程序员的语言 C O B O L和B A S I C。C O B O L的设计使程序员难以改变所编写代码
的可靠性,甚至不能提高代码的编写速度。
然而 C O B O L设计者的本意却是打算使非程序员能读程序(这是不大可能的事)。注意,
4 C语言程序设计
下载
这并不是攻击COBOL的优点,而是想指出,它没有被设计成为程序员的理想语言。 BASIC 的
主要目的是允许非专业程序员在计算机上编程解决比较简单的问题。与其形成鲜明对照的是 C
语言,由于程序生成、修改和现场测试自始至终均由真正的程序员进行,因而它实现了程序
员的期望:很少限制、很少强求、块结构、独立的函数以及紧凑的关键字集合。用 C语言编程,
程序员可以获得高效机器代码,其效率几乎接近汇编语言代码。
C语言被程序员广泛使用的另一个原因是可以用它代替汇编语言。汇编语言使用的汇编指
令,是能够在计算机上直接执行的二进制机器码的符号表示。汇编语言的每个操作都对应为
计算机执行的单一指令。虽然汇编语言给予程序员达到最大灵活性和最高效率的潜力,但开
发和调试汇编语言程序的困难是难以忍受的。非结构性使得汇编语言程序难于阅读、改进和
维护。也许更重要的是,汇编语言程序不能在使用不同 CPU的机器间移植。
最初,C语言被用于系统程序设计。一个“系统程序”是一大类程序的一部分,这一大类
构成了计算机操作系统及实用程序。通常被称为系统程序的有:
• 操作系统。
• 翻译程序。
• 编辑程序。
• 汇编程序。
• 编译程序。
• 数据库管理程序。
随着C语言的普及,加之其可移植性和高效率,许多程序员用它设计各类程序。几乎所有
的计算机上都有 C语言编译程序,这使我们可以很少改动甚至不加改动地将为一种机器写的 C
语言源程序在另一种机器上编译执行。可移植性节省了时间和财力。
C语言不仅在速度和结构上有它的优势,而且每个 C语言系统都提供了专门的函数库,程
序员可以根据不同需要对其进行剪裁,以适应各种程序的设计。由于它允许(更准确地说是
鼓励)分别编译,所以 C语言可使程序员方便地管理大型项目,最大限度地减少重复劳动。

1.3 C语言的程序结构

1.3.1 基本程序结构

任何一种程序设计语言都具有特定的语法规则和规定的表达方法。一个程序只有严格按
照语言规定的语法和表达方式编写,才能保证编写的程序在计算机中能正确地执行,同时也
便于阅读和理解。
为了了解C语言的基本程序结构,我们先介绍几个简单的 C程序。
[例1-1]
# include <stdio.h>
main() /* 主函数*/
{
printf("This is a sample of c program. \n"); 调用标准函数,
/*
显示引号中的内容 */
}
下载
第1章 程序设计与算法 5
这是一个最简单的 C程序,其执行结果是在屏幕上显示一行信息:
RUN ↵
This is a sample of c program.

[例1-2]
main() /*主函数*/
{
void proc(); /* 函数声明 */
int a=3; /*指定a为整数,初始值为 3*/
proc(); /* 调用函数proc, 无返回*/
a=func(); /*调用函数func, 结果返回给a*/
printf("This is a sample of c program. \n");
}

void proc() /* 定义函数proc,void 指定该函数不返回结果*/


{
printf("Hello. \n");
}

int func() /*定义函数func,int 指定该函数返回一个整数*/


{
return(2); /* 返回整数2*/
}

本程序的执行过程是:
• 程序从main()处开始。
• 变量a代表一个整数,并且初始值为 3。
• 执行程序(函数) proc();屏幕上显示Hello,\n为转义字符,代表换行的意思。
• 执行程序(函数) func();并将结果赋予 a,此时,a的值为2。
• 屏幕上显示“This is a sample of c program.”。
程序执行的结果是在屏幕显示两行信息:
RUN ↵
Hello.
This is a sample of c program.

程序中/ * . . . . . * /表示对程序的说明(称为注释),不参与程序的运行。注释文字可以是任意
字符,如汉字、拼音、英文等。
[例1-3]
/*输入长方体的长、宽、高,计算长方体体积 */
main()
{
int x,y,z,v; /* 定义整型变量 */
scanf("%d,%d,%d",&x,&y,&z); /*调用标准函数,从键盘输入x,y,z 的值*/
v = volume(x,y,z); /* 调用volume 函数,计算体积 */
prinf("v = %d\n",v);
}
6 C语言程序设计
下载
int volume(a,b,c) /* 定义volume 函数*/
int a,b,c; /* 对形参a,b,c 作类型定义*/
{
int p; /* 定义函数内部使用的变量p*/

p = a*b*c; /* 计算体积p的值*/
return(p); /* 将p值返回调用处*/
}

本程序的功能是对从键盘输入的长方体的长、宽、高三个整型量求其体积的值。程序运
行的情况如下:
RUN ↵
5,8,6 ↵
v = 240

在本例中, main函数在调用 volume函数时,将实际参数 x、y、z的值分别传送给 volume函


数中的形式参数 a、b、c。经过执行 v o l u m e函数得到一个结果(即 v o l u m e函数中变量 p的值)
并把这个值赋给变量 v。
从上面程序例子,可以看出 C程序的基本结构。
C程序为函数模块结构,所有的 C程序都是由一个或多个函数构成,其中必须只能有一个
主函数main()。程序从主函数开始执行,当执行到调用函数的语句时,程序将控制转移到调用
函数中执行,执行结束后,再返回主函数中继续运行,直至程序执行结束。 C程序的函数是由
编译系统提供的标准函数(如 p r i n t f、s c a n f等)和由用户自己定义的函数(如 p r o c、f u n c、
volume等)。虽然从技术上讲,主函数不是 C语言的一个成分,但它仍被看做是其中的一部分,
因此,“main”不能用作变量名。
函数的基本形式是:
函数类型 函数名(形式参数)
形式参数说明;
{
数据说明部分;
语句部分;
}

其中:
函数头 包括函数说明、函数名和圆括号中的形式参数(如 int volume(a,b,c)),如果函数
调用无参数传递,圆括号中形式参数为空(如 void proc()函数)。
形式参数说明 指定函数调用传递参数的数据类型(如例 1.3中语句int a,b,c;)。
函数体 包括函数体内使用的数据说明和执行函数功能的语句,花括号 {和}表示函数体的
开始和结束。

1.3.2 函数库和链接

从技术上讲,纯粹由程序员自己编写的语句构成 C语言程序是可能的,但这却是罕见的。
因为所有的 C编译程序都提供能完成各种常用任务的函数 — 函数库(如 printf、scanf等)。
C编译程序的实现者已经编写了大部分常见的通用函数。当我们调用一个别人编写的函数
下载
第1章 程序设计与算法 7
时编译程序“记忆”它的名字。随后,“链接程序”把我们编写的程序同标准函数库中找到的
目标码结合起来,这个过程称为“链接”。
保存在函数库中的函数是可重定位的。这意味着其中机器码指令的内存地址并未绝对地
确定, 只有偏移量是确定的。当把程序与标准函数库中的函数相链接时,内存偏移量被用来产
生实际地址。有关重定位的详细内容,请查阅其他技术书籍。
编写程序时用到的函数,许多都可以在标准函数库中找到。它们是可以简单地组合起来
的程序构件。编写了一个经常要用的函数之后,可将其放入库中备用。

1.3.3 开发一个C程序

开发一个C程序,包括以下四步:
1) 程序设计 程序设计亦称程序编辑。程序员用任一编辑软件(编辑器)将编写好的 C程
序输入计算机,并以文本文件的形式保存在计算机的磁盘上。编辑的结果是建立 C源程序文件。
C程序习惯上使用小写英文字母,常量和其他用途的符号可用大写字母。 C语言对大、小写字
母是有区别的。关键字必须小写。
2) 程序编译 编译是指将编辑好的源文件翻译成二进制目标代码的过程。编译过程是使用
C语言提供的编译程序(编译器)完成的。不同操作系统下的各种编译器的使用命令不完全相
同,使用时应注意计算机环境。编译时,编译器首先要对源程序中的每一个语句检查语法错
误,当发现错误时,就在屏幕上显示错误的位置和错误类型的信息。此时,要再次调用编辑
器进行查错修改。然后,再进行编译,直至排除所有语法和语义错误。正确的源程序文件经
过编译后在磁盘上生成目标文件。
3) 链接程序 编译后产生的目标文件是可重定位的程序模块,不能直接运行。链接就是把
目标文件和其他分别进行编译生成的目标程序模块(如果有的话)及系统提供的标准库函数
链接在一起,生成可以运行的可执行文件的过程。链接过程使用 C语言提供的链接程序(链接
器)完成,生成的可执行文件存在磁盘中。
4) 程序运行 生成可执行文件后,就可以在操作系统控制下运行。若执行程序后达到预期
目的,则 C程序的开发工作到此完成。否则,要进一步检查修改源程序,重复编辑 — 编译
— 链接 — 运行的过程,直到取得预期结果为止。
大部分C语言都提供一个独立的开发集成环境,它可将上述四步连贯在一个程序之中。本
书所涉及的程序全部在 Turbo C环境中进行。

1.3.4 C语言的关键字

表1-2列举了32个关键字,它们与标准 C句法结合,形成了程序设计语言 C。
表1-2 关 键 字

auto break case char const continue default do


double else enum extern float for goto if
int long register short signed sizeof static return
struct switch typedef union unsigned void volatile while
8 C语言程序设计
下载
C语言的关键字都用小写字母。 C语言中区分大写与小写, e l s e是关键字,“E L S E”则不
是。在C程序中,关键字不能用于其它目的,即不允许将关键字作为变量名或函数名使用。

1.4 算法

什么是程序?程序 = 数据结构 + 算法。


对于面向对象程序设计,强调的是数据结构,而对于面向过程的程序设计语言如 C、
P a s c a l、F O RT R A N等语言,主要关注的是算法。掌握算法,也是为面向对象程序设计打下一
个扎实的基础。那么,什么是算法呢?
人们使用计算机,就是要利用计算机处理各种不同的问题,而要做到这一点,人们就必
须事先对各类问题进行分析,确定解决问题的具体方法和步骤,再编制好一组让计算机执行
的指令即程序,交给计算机,让计算机按人们指定的步骤有效地工作。这些具体的方法和步
骤,其实就是解决一个问题的算法。根据算法,依据某种规则编写计算机执行的命令序列,
就是编制程序,而书写时所应遵守的规则,即为某种语言的语法。
由此可见,程序设计的关键之一,是解题的方法与步骤,是算法。学习高级语言的重点,
就是掌握分析问题、解决问题的方法,就是锻炼分析、分解,最终归纳整理出算法的能力。
与之相对应,具体语言,如 C语言的语法是工具,是算法的一个具体实现。所以在高级语言的
学习中,一方面应熟练掌握该语言的语法,因为它是算法实现的基础,另一方面必须认识到
算法的重要性,加强思维训练,以写出高质量的程序。
下面通过例子来介绍如何设计一个算法:
[例1-4] 输入三个数,然后输出其中最大的数。
首先,得先有个地方装这三个数,我们定义三个变量 A、B、C,将三个数依次输入到A、
B、C中,另外,再准备一个 MAX装最大数。
由于计算机一次只能比较两个数,我们首先把 A与B比,大的数放入 M A X中,再把 M A X
与C比,又把大的数放入 MAX中。
最后,把 M A X输出,此时 M A X中装的就是A、B、 C三数中最大的一个数。算法可以表
示如下:
1) 输入A、B、C。
2) A 与B中大的一个放入 MAX中。
3) 把C与MAX中大的一个放入 MAX中。
4) 输出MAX,MAX即为最大数。
其中的2)、3)两步仍不明确,无法直接转化为程序语句,可以继续细化:
2) 把A与B中大的一个放入 MAX中,若A>B,则MAX ← A;否则MAX ← B。
3) 把C与MAX中大的一个放入 MAX中,若C>MAX,则MAX←C。
于是算法最后可以写成:
1) 输入A,B,C。
2) 若A>B,则MAX ← A;
否则MAX←B。
3) 若C>MAX,则MAX← C。
4) 输出MAX,MAX即为最大数。
下载
第1章 程序设计与算法 9
这样的算法已经可以很方便地转化为相应的程序语句了。
[例1-5] 猴子吃桃问题:有一堆桃子不知数目,猴子第一天吃掉一半,觉得不过瘾,又
多吃了一只,第二天照此办理,吃掉剩下桃子的一半另加一个,天天如此,到第十天早上,
猴子发现只剩一只桃子了,问这堆桃子原来有多少个?
此题粗看起来有些无从着手的感觉,那么怎样开始呢?假设第一天开始时有 a 1只桃子,第
二天有a2只,...,第9天有a9只,第10天是a10只,在a 1,a2,...,a10 中,只有a10=1是知道的,现要求
a1,而我们可以看出, a1,a2,..,a 10之间存在一个简单的关系:
a9=2*(a 10+1)
a8=2*(a9+1)

a1=2*(a 2+1)
也就是:ai=2*(ai+1+1) i=9,8,7,6,...,1
这就是此题的数学模型。
再考察上面从 a9,a8直至a1的计算过程,这其实是一个递推过程,这种递推的方法在计算
机解题中经常用到。另一方面,这九步运算从形式上完全一样,不同的只是 a i的下标而已。由
此,我们引入循环的处理方法,并统一用 a0表示前一天的桃子数, a1表示后一天的桃子数,将
算法改写如下:
1) a 1=1; {第10天的桃子数,a1的初值}
i=9。 {计数器初值为 9}
2) a 0=2*(a 1+1)。 {计算当天的桃子数}
3) a 1=a0。 {将当天的桃子数作为下一次计算的初值}
4) i=i-1。
5) 若i>=1,转2)。
6) 输出a0的值。
其中2)~5)步为循环。
这就是一个从具体到抽象的过程,具体方法是:
1) 弄清如果由人来做,应该采取哪些步骤。
2) 对这些步骤进行归纳整理,抽象出数学模型。
3) 对其中的重复步骤,通过使用相同变量等方式求得形式的统一,然后简练地用循环解
决。
算法的描述方法有自然语言描述、伪代码、流程图、 N-S图、PAD图等。

1.4.1 流程图与算法的结构化描述

1. 流程图
流程图是一种传统的算法表示法,它利用几何图形的框来代表各种不同性质的操作,用
流程线来指示算法的执行方向。由于它简单直观,所以应用广泛,特别是在早期语言阶段,
只有通过流程图才能简明地表述算法,流程图成为程序员们交流的重要手段,直到结构化的
程序设计语言出现,对流程图的依赖才有所降低。
下面介绍常见的流程图符号及流程图的例子。
10 C语言程序设计
下载
本章例1-1的算法的流程图如图 1-2所示。本章例1-2的算法的流程图如图 1-3所示。
在流程图中,判断框左边的流程线表示判断条件为真时的流程,右边的流程线表示条件为假
时的流程,有时就在其左、右流程线的上方分别标注“真”
、“假”或“T”
、“F”或“Y”
、“N”。

起止框 判断框 输入、输出框 注释框

执行框 连接点 流程线

图1-1 常见的流程图符号

开始

输入A,B,C
开始

T F
A>B i=9

a1=1
MAX<=A MAX<=B
i=i-1

a0=2*(a1+1)
T F i>1
C>MAX a1=a0

MAX<=C

输出a0

输出MAX

结束 结束

图1-2 例1-1的算法流程图 图1-3 例1-2的算法流程图

另外还规定,流程线是从下往上或从右向左时,必须带箭头,除此以外,都不画箭头,
流程线的走向总是从上向下或从左向右。
2. 算法的结构化描述
早期的非结构化语言中都有 g o t o语句,它允许程序从一个地方直接跳转到另一个地方去。
执行这样做的好处是程序设计十分方便灵活,减少了人工复杂度,但其缺点也是十分突出的,
下载
第1章 程序设计与算法 11
一大堆跳转语句使得程序的流程十分复杂紊乱,难以看懂也难以验证程序的正确性,如果有
错,排起错来更是十分困难。这种转来转去的流程图所表达的混乱与复杂,正是软件危机中
程序人员处境的一个生动写照。而结构化程序设计,就是要把这团乱麻理清。
经过研究,人们发现,任何复杂的算法,都可以由顺序结构、选择(分支)结构和循环
结构这三种基本结构组成,因此,我们构造一个算法的时候,也仅以这三种基本结构作为
“建筑单元”,遵守三种基本结构的规范,基本结构之间可以并列、可以相互包含,但不允许
交叉,不允许从一个结构直接转到另一个结构的内部去。正因为整个算法都是由三种基本结
构组成的,就像用模块构建的一样,所以结构清晰,易于正确性验证,易于纠错,这种方法,
就是结构化方法。遵循这种方法的程序设计,就是结构化程序设计。
相应地,只要规定好三种基本结构的流程图的画法,就可以画出任何算法的流程图。
(1) 顺序结构
顺序结构是简单的线性结构,各框按顺序执行。其流程图的基本形态如图 1 - 4所示,语句
的执行顺序为: A→B→C。
(2) 选择(分支)结构
这种结构是对某个给定条件进行判断,条件为真或假时分别执行不同的框的内容。其基
本形状有两种,如图 1-5 a)、b)所示。图 1-5 a )的执行序列为:当条件为真时执行 A,否则
执行B;图1-5b)的执行序列为:当条件为真时执行 A,否则什么也不做。

T F T F
条件 条件
A
A B A
B

C
a) b)

图1-4 顺序结构的流程图 图1-5 选择(分支)结构的流程图

(3) 循环结构
循环结构有两种基本形态: while型循环和do-while型循环。
a. while 型循环
如图1-6所示。
其执行序列为:当条件为真时,反复执行 A,一旦条件为假,跳出循环,执行循环紧后的
语句。
b. do-while型循环
如图1-7所示。

A
A
A
A
T T
条件 条件 条件
T
F F

图1-6 while 型循环流程图 图1-7 do-while型循环流程图 图1-8 do-while 型循环转换为


while型循环
12 C语言程序设计
下载
执行序列为:首先执行 A,再判断条件,条件为真时,一直循环执行 A,一旦条件为假,
结束循环,执行循环紧后的下一条语句。
在图1-6、图1-7中,A被称为循环体,条件被称为循环控制条件。要注意的是:
1) 在循环体中,必然对条件要判断的值进行修改,使得经过有限次循环后,循环一定能
结束,如图 1-3中的i=i-1。
2) 当型循环中循环体可能一次都不执行,而直到型循环则至少执行一次循环体。
3) 直到型循环可以很方便地转化为当型循环,而当型循环不一定能转化为直到型循环。
例如,图1-7可以转化为图1-8。

1.4.2 用N-S图描述算法

N - S图是另一种算法表示法,是由美国人 I . N a s s i和B . S h n e i d e r m a n共同提出的,其根据是:


既然任何算法都是由前面介绍的三种结构组成,所以各基本结构之间的流程线就是多余的,
因此,N-S图也是算法的一种结构化描述方法。
N - S图中,一个算法就是一个大矩形框,框内又包含若干基本的框,三种基本结构的 N - S
图描述如下所示:
1. 顺序结构
如图1-9所示,执行顺序先 A后B。
2. 选择结构
对应于图 1 - 5的N - S图为图1 - 1 0。图1-10 a)条件为真时执行 A,条件为假时执行 B。图1 - 1 0
b)条件为真时执行 A,为假时什么都不做。
A T 条件 F T 条件 F
A B A
B
a) b)
图1-9 顺序结构的N-S图 图1-10 选择结构的N-S图
当<条件>为真
3. 循环结构
1) while型循环的 N - S图如图1 - 11所示,条件为真时一直循 A

环执行循环体A,直到条件为假时才跳出循环。 图1-11 while型循环的N-S图


2) do-while型循环的N-S图如图1-12,一直循环执行循环体
A
A,直到条件为假时才跳出循环。
当<条件>为真
本章例 1 - 1的N - S图如图 1 - 1 3,例1 - 2的N - S图如图 1 - 1 4。应
图1-12 do-while型循环的N-S图
该说,N-S图比流程图更直观易懂,而且相对简练一些。

a1=1
输入A,B,C

T A>B i=9
F
max<=A max<=B 当i>=1时,循环

C>max a0=2*(a1+1)
T F
a1=a0
max<=C i=i-1
输出 MAX 输出a0

图1-13 例1-1的N-S图 图1-14 例1-2的N-S图


下载
第1章 程序设计与算法 13
1.4.3 用PAD图描述算法

PAD(Problem Analysis Diagram),是近年来在软件开发中被广泛使用的一种算法的图形


表示法,与前述的流程图、 N - S图相比,流程图、 N - S图都是自上而下的顺序描述,而 PA D图
除了自上而下以外,还有自左向右的展开,所以,如果说流程图、 N-S图是一维的算法描述的
话,则PAD图就是二维的,它能展现算法的层次结构,更直观易懂。
下面是PAD图的几种基本形态:
1. 顺序结构:
如图1-15所示。
2. 选择结构
(1) 单分支选择,条件为真执行 A,如图1-16 a)。
(2) 两分支选择,如图 1-16 b),条件为真执行 A,为假执行 B。
(3) 多分支选择,如图1-16 c),当I=I1时执行A,I=I2时执行B,I=I3时执行C,I=I4时执行D。

A A A i1 A
条件 条件
I= i2 B
B
B
a) b) i3 C

C i4 D

图1-15 顺序结构的PAD 图1-16 选择结构的PAD

3. 循环结构
如图1-17所示。图1-17 a)为while型循环,图 1-17 b)为do-while型循环。

WHILE<条件> A UNTIL<条件> A
a) b)
图1-17 循环结构的PAD

本章例1.1的PAD图如图1-18,例1-2的PAD图如图1-19。

输入A,B,C a1=1

MAX<=A
A>B i=9
MAX<=B a0=2*a1+1

MAX<=C
while i>=1 a1=a0
C>MAX
i=i-1

输出MAX
输出 a0

图1-18 例1-1的PAD 图1-19 例1-2的PAD


下载

第2章 数据类型、运算符和表达式
2.1 C语言的数据类型

C语言有五种基本数据类型:字符、整型、单精度实型、双精度实型和空类型。尽管这几
种类型数据的长度和范围随处理器的类型和 C语言编译程序的实现而异,但以 b i t为例,整数
与C P U字长相等,一个字符通常为一个字节,浮点值的确切格式则根据实现而定。对于多数
微机,表2-1给出了五种数据的长度和范围。
表2-1 基本类型的字长和范围

类 型 长 度(bit) 范 围

char(字符型) 8 0~255
int(整型) 16 -32768~32767
float(单精度型) 32 约精确到 6位数
double(双精度型) 64 约精确到 12位数
void(空值型) 0 无值

表中的长度和范围的取值是假定 CPU的字长为16bit。
C语言还提供了几种聚合类型( aggregate types),包括数组、指针、结构、共用体(联合)

位域和枚举。这些复杂类型在以后的章节中讨论。
除v o i d类型外,基本类型的前面可以有各种修饰符。修饰符用来改变基本类型的意义,
以便更准确地适应各种情况的需求。修饰符如下:
• signed(有符号)。
• unsigned(无符号)。
• long(长型符)。
• short(短型符)。
修饰符s i g n e d、s h o r t、l o n g和u n s i g n e d适用于字符和整数两种基本类型,而 l o n g还可用于
double(注意,由于long float与double意思相同,所以 ANSI标准删除了多余的 long float)。
表2-2给出所有根据 ANSI标准而组合的类型、字宽和范围。切记,在计算机字长大于 16位
的系统中, short int与signed char可能不等。
表2-2 ANSI标准中的数据类型

类 型 长 度(bit) 范 围

char(字符型) 8 ASCII字符
unsigned char(无符号字符型) 8 0~255
signed char(有符号字符型 ) 8 -128~127
int(整型) 16 32768~32767
unsigned int(无符号整型 ) 16 0~65535
signed int(有符号整型 ) 16 同int
下载
第2章 数据类型、运算符和表达式 15
(续)

类 型 长 度(bit) 范 围

short int(短整型) 8 128~127


unsigned short int(无符号短整型 ) 8 0~255
signed short int(有符号短整型 ) 8 同shortint
long int(长整型) 32 2147483648~2147483649
signed long int(有符号长整型 ) 32 2147483648~2147483649
unsigned long int(无符号长整型 ) 32 0~4294967296
float(单精度型) 32 约精确到6位数
double(双精度型) 64 约精确到12位数

*表中的长度和范围的取值是假定 CPU的字长为16bit。
因为整数的缺省定义是有符号数,所以 singed这一用法是多余的,但仍允许使用。
某些实现允许将 unsigned用于浮点型,如 unsigned double。但这一用法降低了程序的可移
植性,故建议一般不要采用。
为了使用方便, C编译程序允许使用整型的简写形式:
• short int 简写为short。
• long int 简写为long。
• unsigned short int 简写为unsigned short。
• unsigned int 简写为unsigned。
• unsigned long int 简写为unsigned long。
即,int可缺省。

2.2 常量与变量

2.2.1 标识符命名

在C语言中,标识符是对变量、函数标号和其它各种用户定义对象的命名。标识符的长度
可以是一个或多个字符。绝大多数情况下,标识符的第一个字符必须是字母或下划线,随后
的字符必须是字母、数字或下划线(某些 C语言编译器可能不允许下划线作为标识符的起始字
符)。下面是一些正确或错误标识符命名的实例。
正确形式 错误形式
count 2count
test23 hi! there
high_balance high..balance
A N S I标准规定,标识符可以为任意长度,但外部名必须至少能由前 8个字符唯一地区分。
这里外部名指的是在链接过程中所涉及的标识符,其中包括文件间共享的函数名和全局变量
名。这是因为对某些仅能识别前 8个字符的编译程序而言,下面的外部名将被当作同一个标识
符处理。
counters counters1 counters2

A N S I标准还规定内部名必须至少能由前 3 1个字符唯一地区分。内部名指的是仅出现于定
16 C语言程序设计
下载
义该标识符的文件中的那些标识符。
C语言中的字母是有大小写区别的,因此 count Count COUNT是三个不同的标识符。
标识符不能和C语言的关键字相同,也不能和用户已编制的函数或 C语言库函数同名。

2.2.2 常量

C语言中的常量是不接受程序修改的固定值,常量可为任意数据类型,如下例所示 :
数据类型 常量举例
char 'a'、'\n'、'9'
int 21、 123 、2100 、-234
long int 35000、 -34
short int 10、-12、90
unsigned int 10000、 987、 40000
float 123.23、 4.34e-3
double 123.23、 12312333、 -0.9876234
C语言还支持另一种预定义数据类型的常量,这就是串。所有串常量括在双撇号之间,例
如"This is a test"。切记,不要把字符和串相混淆,单个字符常量是由单撇号括起来的,如 'a'。

2.2.3 变量

其值可以改变的量称为变量。一个变量应该有一个名字 (标识符),在内存中占据一定的存
储单元,在该存储单元中存放变量的值。请注意区分变量名和变量值这两个不同的概念。
所有的C变量必须在使用之前定义。定义变量的一般形式是:
type variable_list;

这里的type必须是有效的 C数据类型,v a r i a b l e _ l i s t(变量表)可以由一个或多个由逗号分


隔的多个标识符名构成。下面给出一些定义的范例。
int i, j, l;
short int si;
unsigned int ui;
double balance, profit,loss;

注意 C语言中变量名与其类型无关。

2.3 整型数据

2.3.1 整型常量

整型常量及整常数。它可以是十进制、八进制、十六进制数字表示的整数值。
十进制常数的形式是:
digits

这里digits可以是从0到9的一个或多个十进制数位,第一位不能是 0。
八进制常数的形式是:
下载
第2章 数据类型、运算符和表达式 17
0digits

在此,digits可以是一个或多个八进制数( 0~7之间),起始0是必须的引导符。
十六进制常数是下述形式:
0xhdigits
0Xhdigits

这里h d i g i t s可以是一个或多个十六进制数(从 0~9的数字,并从“ a”~“ f”的字母)。


引导符0是必须有的,X即字母可用大写或小写。
注意,空白字符不可出现在整数数字之间。表 2-3列出了整常数的形式。
表2-3 整常数的例子

十 进 制 八 进 制 十六进制

10 012 0Xa或0XA
132 0204 0X84
32179 076663 0X7db3或0X7DB3

整常数在不加特别说明时总是正值。如果需要的是负值,则负号“ -”必须放置于常数表
达式的前面。
每个常数依其值要给出一种类型。当整常数应用于一表达式时,或出现有负号时,常数
类型自动执行相应的转换,十进制常数可等价于带符号的整型或长整型,这取决于所需的常
数的尺寸。
八进制和十六进制常数可对应整型、无符号整型、长整型或无符号长整型,具体类型也
取决于常数的大小。如果常数可用整型表示,则使用整型。如果常数值大于一个整型所能表
示的最大值,但又小于整型位数所能表示的最大数,则使用无符号整型。同理,如果一个常
数比无符号整型所表示的值还大,则它为长整型。如果需要,当然也可用无符号长整型。
在一个常数后面加一个字母 l或L,则认为是长整型。如10L、79L、012L、0115L、0XAL、
0x4fL等。

2.3.2 整型变量

前面已提到, C规定在程序中所有用到的变量都必须在程序中指定其类型,即“定义”。
这是和BASIC、FORTRAN不同的,而与Pascal相似。
[例2-1]
main()
{
int a,b,c,d; /* 指定a,b,c,d 为整型变量*/
unsigned u; /*指定u为无符号整型变量 */
a=12; b=-24; u=10;
c=a+u; d=b+u;
printf("a+u=%d, b+u=%d\n",c,d);
}

运行结果为 :
RUN ↵
a+u=22, b+u=-14
18 C语言程序设计
下载
可以看到不同类型的整型数据可以进行算术运算。在本例中是 int型数据与unsingned int型
数据进行相加减运算。

2.4 实型数据

2.4.1 实型常量

实型常量又称浮点常量,是一个十进制表示的符号实数。符号实数的值包括整数部分、
尾数部分和指数部分。实型常量的形式如下:
[digits][.digits][E|e[+|-]digits]

在此d i g i t s是一位或多位十进制数字(从 0~9)。 E(也可用 e)是指数符号。小数点之前


是整数部分,小数点之后是尾数部分,它们是可省略的。小数点在没有尾数时可省略。
指数部分用 E或e开头,幂指数可以为负,当没有符号时视为正指数的基数为 1 0,如
1.575E10表示为:1.575×1010。在实型常量中不得出现任何空白符号。
在不加说明的情况下,实型常量为正值。如果表示负值,需要在常量前使用负号。
下面是一些实型常量的示例:
15.75, 1.575E10, 1575e-2, -0.0025, -2.5e-3, 25E-4

所有的实型常量均视为双精度类型。
实型常量的整数部分为 0时可以省略,如下形式是允许的:

.57, .0075e2, -.125, -.175E-2

注意 字母E或e之前必须有数字,且E或e后面指数必须为整数,如e3、2.1e3.5、.e3、e
等都是不合法的指数形式。

2.4.2 实型变量

实型变量分为单精度( f l o a t型)和双精度( d o u b l e型)。对每一个实型变量都应再使用前


加以定义。如:
float x,y; /*指定x,y为单精度实数*/
double z; /*指定z为双精度实数*/
在一般系统中,一个 f l o a t型数据在内存中占 4个字节( 3 2位)一个 d o u b l e型数据占 8个字
节(64位)。单精度实数提供 7位有效数字,双精度提供 15~16位有效数字,数值的范围随机器
系统而异。
值得注意的是,实型常量是 double型,当把一个实型常量赋给一个 float型变量时,系统会
截取相应的有效位数。例如
float a;
a=111111.111;

由于float型变量只能接收 7位有效数字,因此最后两位小数不起作用。如果将 a改为double


型,则能全部接收上述 9位数字并存储在变量 a中。
下载
第2章 数据类型、运算符和表达式 19
2.5 字符型数据

2.5.1 字符常量

字符常量是指用一对单引号括起来的一个字符。如‘ a’,‘9’,‘!’。字符常量中的单引
号只起定界作用并不表示字符本身。单引号中的字符不能是单引号(’)和反斜杠( \),它们
特有的表示法在转义字符中介绍。
在C语言中,字符是按其所对应的 ASCII码值来存储的,一个字符占一个字节。例如:
字符 ASCII码值(十进制)
! 33
0 48
1 49
9 57
A 65
B 66
a 97
b 98

注意 字符'9'和数字9的区别,前者是字符常量,后者是整型常量,它们的含义和在计
算机中的存储方式都截然不同。

由于C语言中字符常量是按整数( short型)存储的,所以字符常量可以像整数一样在程序
中参与相关的运算。例如:
'a'-32; /* 执行结果97-32 = 65 */
'A' + 32; /* 执行结果65+32 = 97 */
'9'-9; /* 执行结果57-9 = 48 */

2.5.2 字符串常量

字符串常量是指用一对双引号括起来的一串字符。双引号只起定界作用,双引号括起的
字符串中不能是双引号( ")和反斜杠(\),它们特有的表示法在转义字符中介绍。例如:
"China" ,"C program", "YES&NO", "33312-2341", 等。
"A"

C语言中,字符串常量在内存中存储时,系统自动在字符串的末尾加一个“串结束标志”,
即A S C I I码值为 0的字符 N U L L,常用 \ 0表示。因此在程序中,长度为 n个字符的字符串常量,
在内存中占有n+1个字节的存储空间。
例如,字符串 C h i n a有5个字符,作为字符串常量 " C h i n a "存储于内存中时,共占 6个字节,
系统自动在后面加上 NULL字符,其存储形式为:
C h i n a NULL

要特别注意字符串与字符串常量的区别,除了表示形式不同外,其存储性质也不相同,
字符'A'只占1个字节,而字符串常量 "A"占2个字节。
20 C语言程序设计
下载
2.5.3 转义字符

转义字符是 C语言中表示字符的一种特殊形式。通常使用转义字符表示 A S C I I码字符集中


不可打印的控制字符和特定功能的字符,如用于表示字符常量的单撇号( '),用于表示字符串
常量的双撇号( ")和反斜杠( \)等。转义字符用反斜杠 \后面跟一个字符或一个八进制或十
六进制数表示。表 2-4给出了C语言中常用的转义字符。
表2-4 转义字符

转义字符 意 义 ASCII码值(十进制)

\a 响铃(BEL) 007
\b 退格(BS) 008
\f 换页(FF) 012
\n 换行(LF) 010
\r 回车(CR) 013
\t 水平制表(HT) 009
\v 垂直制表(VT) 011
\\ 反斜杠 092
\? 问号字符 063
\' 单引号字符 039
\" 双引号字符 034
\0 空字符(NULL) 000
\ddd 任意字符 三位八进制
\xhh 任意字符 二位十六进制

字符常量中使用单引号和反斜杠以及字符常量中使用双引号和反斜杠时,都必须使用转
义字符表示,即在这些字符前加上反斜杠。
在C程序中使用转义字符 \ d d d或者\ x h h可以方便灵活地表示任意字符。 \ d d d为斜杠后面跟
三位八进制数,该三位八进制数的值即为对应的八进制 ASCII码值。\x后面跟两位十六进制数,
该两位十六进制数为对应字符的十六进制 ASCII码值。
使用转义字符时需要注意以下问题:
1) 转义字符中只能使用小写字母,每个转义字符只能看作一个字符。
2) \v 垂直制表和 \f 换页符对屏幕没有任何影响,但会影响打印机执行响应操作。
3) 在C程序中,使用不可打印字符时,通常用转义字符表示。

2.5.4 符号常量

C语言允许将程序中的常量定义为一个标识符,称为符号常量。符号常量一般使用大写英
文字母表示,以区别于一般用小写字母表示的变量。符号常量在使用前必须先定义,定义的
形式是:
#define <符号常量名> <常量>

例如:
#define PI 3.1415926
#define TRUE 1
#definr FALSE 0
#define STAR '*'
这里定义 P I、T R U E、F L A S E、S TA R为符号常量,其值分别为 3 . 1 4 1 5 9 2 6,1,0,' * '。
下载
第2章 数据类型、运算符和表达式 21
#define是C语言的预处理命令,它表示经定义的符号常量在程序运行前将由其对应的常量替换。
定义符号常量的目的是为了提高程序的可读性,便于程序的调试和修改。因此在定义符
号常量名时,应使其尽可能地表达它所代表的常量的含义,例如前面所定义的符号常量名
P I (π),表示圆周率 3 . 1 4 1 5 9 2 6。此外,若要对一个程序中多次使用的符号常量的值进行修改,
只须对预处理命令中定义的常量值进行修改即可。

2.5.5 字符变量

字符变量用来存放字符常量,注意只能存放一个字符,不要以为在一个字符变量中可以
放字符串。
字符变量的定义形式如下:
char c1, c2;

它表示c1和c2为字符变量,各放一个字符。因此可以用下面语句对 c1、c2赋值:
c1 = 'a'; c2 = 'b';

[例2-2]
main()
{
char c1,c2;
c1=97; c2=98;
printf("%c %c",c1,c2);
}

c 1、c 2被指定为字符变量。但在第 3行中,将整数 9 7和9 8分别赋给 c 1和c 2,它的作用相当


于以下两个赋值语句:
c1='a'; c2='b';

因为' a '和' b '的A S C I I码为9 7和9 8。第4行将输出两个字符。 " % c "是输出字符的格式。程序


输出:
RUN ↵
a b

[例2-3]
main()
{ char c1,c2;
c1='a' ;c2='b';
c1 = c1 - 32; c2 =c2 - 32;
printf("%c %c",c1,c2);
}

运行结果为:
RUN ↵
A B

它的作用是将两个小写字母转换为大写字母。因为 'a'的ASCII码为97,而'A'为65,'b'为98,
' B '为6 6。从A S C I I代码表中可以看到每一个小写字母比大写字母的 A S C I I码大3 2。即'a'='A' +
32。
22 C语言程序设计
下载
2.6 运算符

C语言的内部运算符很丰富,运算符是告诉编译程序执行特定算术或逻辑操作的符号。 C
语言有三大运算符:算术、关系与逻辑、位操作。另外, C还有一些特殊的运算符,用于完成
一些特殊的任务。

2.6.1 算术运算符

表2 - 5列出了 C语言中允许的算术运算符。在 C语言中,运算符“ +”、“-”、“*”和“ /”


的用法与大多数计算机语言的相同,几乎可用于所有 C语言内定义的数据类型。当“ /”被用
于整数或字符时,结果取整。例如,在整数除法中, 10/3=3。
一元减法的实际效果等于用 - 1乘单个操作数,即任何数值前放置减号将改变其符号。模
运算符“ %”在C语言中也同它在其它语言中的用法相同。切记,模运算取整数除法的余数,
所以“%”不能用于 float和double类型。
表2-5 算术运算符

运 算 符 作 用 运 算 符 作 用

- 减法,也是一元减法 % 模运算
+ 加法 -- 自减(减 1)
* 乘法 ++ 自增(增 1)
/ 除法

下面是说明 %用法的程序段。
int x,y;
x=10;
y=3;
printf("%d",x/y); /* 显示 3 */
printf("%d",x%y); /* 显示 1,整数除法的余数 */

x=1;
y=2;
printf("%d,%d",x/y,x%y); /* 显示 0,1 */

最后一行打印一个 0和一个1,因为1/2整除时为0,余数为1,故1%2取余数1。

2.6.2 自增和自减

C语言中有两个很有用的运算符,通常在其它计算机语言中是找不到它们的 — 自增和自
减运算符, ++和--。运算符“ ++”是操作数加1,而“--”是操作数减1,换句话说:
x=x+1; 同++x;
x=x-1; 同--x;

自增和自减运算符可用在操作数之前,也可放在其后,例如: x = x + 1;可写成 + + x;或


x + +;但在表达式中这两种用法是有区别的。自增或自减运算符在操作数之前, C语言在引用
下载
第2章 数据类型、运算符和表达式 23
操作数之前就先执行加 1或减1操作;运算符在操作数之后, C语言就先引用操作数的值,而后
再进行加1或减1操作。请看下例:
x=10;
y=++x;

此时,y=11。如果程序改为:
x=10;
y=x++;

则y = 1 0。在这两种情况下, x都被置为 11,但区别在于设置的时刻,这种对自增和自减发


生时刻的控制是非常有用的。
在大多数C编译程序中,为自增和自减操作生成的程序代码比等价的赋值语句生成的代码
要快得多,所以尽可能采用加 1或减1运算符是一种好的选择。
下面是算术运算符的优先级:
最高 ++、--
-(一元减)
*、/、%
最低 +、-
编译程序对同级运算符按从左到右的顺序进行计算。当然,括号可改变计算顺序。 C语言
处理括号的方法与几乎所有的计算机语言相同:强迫某个运算或某组运算的优先级升高。

2.6.3 关系和逻辑运算符

关系运算符中的“关系”二字指的是一个值与另一个值之间的关系,逻辑运算符中的
“逻辑”二字指的是连接关系的方式。因为关系和逻辑运算符常在一起使用,所以将它们放在
一起讨论。
关系和逻辑运算符概念中的关键是 Tr u e(真)和 F l a s e(假)。C语言中,非 0为Tr u e,0为
Flase。使用关系或逻辑运算符的表达式对 Flase和Ture分别返回值0或1(见表2-6)。
表2-6 关系和逻辑运算符

关系运算符 含 义 关系运算符 含 义

> 大于 <= 小于或等于


>= 大于等于 == 等于
< 小于 != 不等于

逻辑运算符 含 义

&& 与
|| 或
! 非

表2-6给出于关系和逻辑运算符,下面用 1和0给出逻辑真值表。
关系和逻辑运算符的优先级比算术运算符低,即像表达式 10>1+12的计算可以假定是对表
达式10>(1+12)的计算,当然,该表达式的结果为 Flase。
在一个表达式中允许运算的组合。例如:
10>5&&!(10<9)||3<=4
24 C语言程序设计
下载
p q p&&q p||q !p

0 0 0 0 1

0 1 0 1 1

1 1 1 1 0

1 0 0 1 0

这一表达式的结果为 True。
下表给出了关系和逻辑运算符的相对优先级:
最高 !
>= <=
== !=
&&
最低 ||
同算术表达式一样,在关系或逻辑表达式中也使用括号来修改原计算顺序。
切记,所有关系和逻辑表达式产生的结果不是 0就是1,所以下面的程序段不仅正确而且
将在屏幕上打印数值 1。
int x;
x=100;
printf("%d",x>10);

2.6.4 位操作符

与其它语言不同, C语言支持全部的位操作符( Bitwise Operators)。因为C语言的设计目


的是取代汇编语言,所以它必须支持汇编语言所具有的运算能力。位操作是对字节或字中的
位(bit)进行测试、置位或移位处理,这里字节或字是针对 C标准中的char和int数据类型而言
的。位操作不能用于 f l o a t、d o u b l e、long double、v o i d或其它复杂类型。表 2 - 7给出了位操作
的操作符。位操作中的 A N D、O R和N O T(1的补码)的真值表与逻辑运算等价,唯一不同的
是位操作是逐位进行运算的。
表2-7 位操作符

操 作 符 含 义 操 作 符 含 义

& 与(AND) ~ 1的补(NOT)


| 或(OR) >> 右移
^ 异或(XOR) << 左移

下面是异或的真值表。
表2-8 异或的真值表

P q p^q
0 0 0
1 0 1
1 1 0
0 1 1
下载
第2章 数据类型、运算符和表达式 25
如表2-8所示,当且仅当一个操作数为 True时,异或的输出为 True,否则为Flase。
位操作通常用于设备驱动程序,例如调制解调器程序、磁盘文件管理程序和打印机驱动
程序。这是因为位操作可屏蔽掉某些位,如奇偶校验位(奇偶校验位用于确保字节中的其它
位不会发生错误通常奇偶校验位是字节的最高位)。
通常我们可把位操作 AND作为关闭位的手段,这就是说两个操作数中任一为 0的位,其结
果中对应位置为 0。例如,下面的函数通过调用函数 read_modem(),从调制解调器端口读入一
个字符,并将奇偶校验位置成 0。
[例2-4]
Char get_char_from_modem()
{
char ch;
从调制解调器端口中得到一个字符 */
ch=read_modem(); /*
return(ch&127);
}

字节的位 8是奇偶位,将该字节与一个位 1到位7为1、位8为0的字节进行与操作,可将该


字节的奇偶校验位置成 0。表达式 c h & 1 2 7正是将 c h中每一位同 1 2 7数字的对应位进行与操作,
结果ch的位8被置成了0。在下面的例子中,假定 ch接收到字符 "A"并且奇偶位已经被置位。
奇偶位

110000001 内容为‘A’的ch,其中奇偶校验位为 1
011111111 二进制的127执行与操作
& 与操作
= 010000001 去掉奇偶校验的‘ A’
位操作O R与A N D操作相反,可用来置位。任一操作数中为 1的位将结果的对应位置 1。如
下所示,128|3的情况是:
1000000 128的二进制
0000011 3的二进制
| 或操作
= 1000011 结果
异或操作通常缩写为 X O R,当且仅当做比较的两位不同时,才将结果的对应位置位。如
下所示,异或操作 127^120的情况是:
01111111 127 的二进制
01111000 120的二进制
^ 异或操作
= 00000111 结果
一般来说,位的 AND、OR和XOR操作通过对操作数运算,直接对结果变量的每一位分别
处理。正是因为这一原因(还有其它一些原因),位操作通常不像关系和逻辑运算符那样用在
条件语句中,我们可以用例子说明这一点:假定 X = 7,那么 x & & 8 为Tu r e ( 1 ) ,而x & 8却为
Flase(0)。
记住,关系和逻辑操作符结果不是 0就是1。而相似的位操作通过相应处理,结果可为任
26 C语言程序设计
下载
意值。换言之,位操作可以有 0或1以外的其它值,而逻辑运算符的计算结果总是 0或1。
移位操作符 >>和<<将变量的各位按要求向或向左移动。右移语句通常形式是:
variable >>右移位数
左移语句是:
variable<<左移位数
当某位从一端移出时,另一端移入 0(某些计算机是送 1,详细内容请查阅相应 C编译程序
用户手册)。切记:移位不同于循环,从一端移出的位并不送回到另一端去,移去的位永远丢
失了,同时在另一端补 0。
移位操作可对外部设备(如 D/A转换器)的输入和状态信息进行译码,移位操作还可用于
整数的快速乘除运算。如表 2 - 9所示(假定移位时补 0),左移一位等效于乘 2,而右移一位等
效于除以2。
表2-9 用移位操作进行乘和除

字 符 x 每个语句执行后的 x x 的 值

x=7 00000111 7
x<<1 00001110 14
x<<3 01110000 112
x<<2 11000000 192
x>>1 01100000 96
x>>2 00011000 24

每左移一位乘 2,注意 x < < 2后,原 x的信息已经丢失了,因为一位已经从一端出,每右移


一位相当于被2除,注意,乘后再除时,除操作并不带回乘法时已经丢掉的高位。
反码操作符为 ~。~的作用是将特定变量的各位状态取反,即将所有的 1位置成0,所有的 0
位置成1。
位操作符经常用在加密程序中,例如,若想生成一个不可读磁盘文件时,可以在文件上
做一些位操作。最简单的方法是用下述方法,通过 1的反码运算,将每个字节的每一位取反。
原字节 00101100
第一次取反码 11010011
第二次取反码 00101100
注意,对同一行进行连续的两次求反,总是得到原来的数字,所以第一次求反表示了字
节的编码,第二次求反进行译码又得到了原来的值。
可以用下面的函数 encode()对字符进行编码。
[例2-5]
char encode(ch)
char ch;
{
return (~ch);
}

2.6.5 ?操作符

C语言提供了一个可以代替某些 i f - t h e n - e l s e语句的简便易用的操作符?。该操作符是三元
下载
第2章 数据类型、运算符和表达式 27
的,其一般形式为:
EXP1?EXE2:EXP3

EXP1,EXP2和EXP3是表达式,注意冒号的用法和位置。
操作符“ ? ”作用是这样的,在计算 EXP1之后,如果数值为 True,则计算EXP2,并将结
果作为整个表达式的数值;如果 E X P 1的值为 Fl a s e,则计算 E X P 3,并以它的结果作为整个表
达式的值,请看下例:
x=10;
y=x>9?100:200;

例中,赋给 y的数值是 100,如果x被赋给比9小的值,y的值将为200,若用if-else语句改写,有


下面的等价程序:
x=10;
if(x>9) y=100;
else y=200;

有关C语言中的其它条件语句将在第 3章进行讨论。

2.6.6 逗号操作符

作为一个操作符,逗号把几个表达式串在一起。逗号操作符的左侧总是作为 v o i d (无值),
这意味着其右边表达式的值变为以逗号分开的整个表达式的值。例如:
x=(y=3,y+1);

这行将 3赋给y,然后将 4赋给x,因为逗号操作符的优先级比赋值操作符优先级低,所以


必须使用括号。
实际上,逗号表示操作顺序。当它在赋值语句右边使用时,所赋的值是逗号分隔开的表
中最后那个表达式的值。例如,
y=10;
x=(y=y-5,25/y);

执行后,x的值是5,因为y的起始值是 10,减去5之后结果再除以 25,得到最终结果。


在某种意义上可以认为,逗号操作符和标准英语的 and是同义词。

2.6.7 关于优先级的小结

表2 - 1 0列出了C语言所有操作符的优先级,其中包括将在本书后面讨论的某些操作符。注
意,所有操作符(除一元操作符和?之外)都是左结合的。一元操作符( *,&和-)及操作符
“?”则为右结合。
表2-10 C语言操作符的优先级

最 高 级 ()[] →
!~ ++ -- -(type) * & sizeof
*/%
+-
<< >>
<= >=
== !=
28 C语言程序设计
下载
(续)

&
^
|
&&
||
?
= += -= *= /=
最低级 ,

2.7 表达式

表达式由运算符、常量及变量构成。 C语言的表达式基本遵循一般代数规则,有几点却是
与C语言紧密相关的,以下将分别加以讨论。

2.7.1 表达式中的类型转换

混合于同一表达式中的不同类型常量及变量,应均变换为同一类型的量。 C语言的编译程
序将所有操作数变换为与最大类型操作数同类型。变换以一次一操作的方式进行。具体规则
如下:
char ch;
int i;
float f;
double d;
result=(ch / i) + ( f * d ) - ( f + i );
int double double

int double double

double

double

图2-1 类型转换实例

1) 所有char及short int 型量转为int型,所有float转换为double。


2) 如操作数对中一个为 long double ,另一个转换为 long double 。① 要不然,一个为
d o u b l e,另一个转为 d o u b l e。② 要不然,一个为 l o n g,另一个转为 l o n g。③ 要不然,一个为
unsigned,另一个转为unsigned。
一旦运用以上规则。每一对操作数均变为同类型。注意,规则 2 )有几种必须依次应用的
条件。
图2 - 1示出了类型转换。首先, char ch转换成 i n t,且float f 转换成d o u b l e;然后c h / i的结
果转换成 d o u b l e,因为 f * d是d o u b l e;最后由于这次两个操作数都是 d o u b l e,所以结果也是
下载
第2章 数据类型、运算符和表达式 29
double.

2.7.2 构成符cast

可以通过称为cast的构成符强迫一表达式变为特定类型。其一般形式为:
(type )expression

( t y p e )是标准 C语言中的一个数据类型。例如,为确保表达式 x / 2的结果具有类型 f l o a t,可写


为:
(float )x/2

通常认为cast是操作符。作为操作符, cast是一元的,并且同其它一元操作符优先级相同。
虽然c a s t在程序中用得不多,但有时它的使用的确很有价值。例如,假设希望用一整数控
制循环,但在执行计算时又要有小数部分。
[例2-6]
main()
{
int i ;
for (i+1;i<=100;++i)
printf("%d/2 is :%f",i,(float)i/2);
}

若没有 c a s t ( f l o a t ),就仅执行一次整数除;有了 c a s t就可保证在屏幕上显示答案的小数部


分。

2.7.3 空格与括号

为了增加可读性,可以随意在表达式中插入tab和空格符。例如,下面两个表达式是相同的。
x=10/y*(127/x);
x=10/y*(127/x);

冗余的括号并不导致错误或减慢表达式的执行速度。我们鼓励使用括号,它可使执行顺
序更清楚一些。例如,下面两个表达式中哪个更易读一些呢?
x=y/2-34*temp&127;
x=(y/2)-((34*temp)&127);

2.7.4 C语言中的简写形式

C语言提供了某些赋值语句的简写形式。例如语句:
x=x+10;

在C语言中简写形式是:
x+=10 ;

这组操作符对 + =通知编译程序将 X + 1 0的值赋予 X。这一简写形式适于 C语言的所有二元


操作符(需两个操作数的操作符)。在C语言中,
variable=variable1 operator expression;
30 C语言程序设计
下载
与variable1 operator=expression相同。
请看另一个例子:
x=x-100;

其等价语句是
x-=100;

简写形式广泛应用于专业 C语言程序中,希望读者能熟悉它。
下载

第3章 程序控制语句
3.1 程序的三种基本结构

通常的计算机程序总是由若干条语句组成,从执行方式上看,从第一条语句到最后一条
语句完全按顺序执行,是简单的顺序结构;若在程序执行过程当中,根据用户的输入或中间
结果去执行若干不同的任务则为选择结构;如果在程序的某处,需要根据某项条件重复地执
行某项任务若干次或直到满足或不满足某条件为止,这就构成循环结构。大多数情况下,程
序都不会是简单的顺序结构,而是顺序、选择、循环三种结构的复杂组合。
三种基本结构的流程图、 N-S图以及PAD图可以参看本书第 1章1.4节“算法”相关内容。
C语言中,有一组相关的控制语句,用以实现选择结构与循环结构:
选择控制语句: if;
switch、case
循环控制语句: for、while、do…while
转移控制语句: break、continue、goto
我们将在后面几节中详细介绍。

3.2 数据的输入与输出

在程序的运行过程中,往往需要由用户输入一些数据,而程序运算所得到的计算结果等
又需要输出给用户,由此实现人与计算机之间的交互,所以在程序设计中,输入输出语句是
一类必不可少的重要语句,在 C语言中,没有专门的输入输出语句,所有的输入输出操作都是
通过对标准I / O库函数的调用实现。最常用的输入输出函数有 s c a n f ( )、p r i n t f ( )、g e t c h a r ( )和
putchar(),以下分别介绍。

3.2.1 scanf()函数

格式化输入函数 s c a n f ( )的功能是从键盘上输入数据,该输入数据按指定的输入格式被赋
给相应的输入项。函数一般格式为:
scanf("控制字符串 ",输入项列表);
其中控制字符串规定数据的输入格式,必须用双引号括起,其内容是由格式说明和普通
字符两部分组成。输入项列表则由一个或多个变量地址组成,当变量地址有多个时,各变量
地址之间用逗号“,”分隔。
s c a n f ( )中各变量要加地址操作符,就是变量名前加“ &”,这是初学者容易忽略的一个问
题。应注意输入类型与变量类型一致。
下面探讨控制字符串的两个组成部分:格式说明和普通字符。
1. 格式说明
格式说明规定了输入项中的变量以何种类型的数据格式被输入,形式是:
32 C语言程序设计
下载
%[<修饰符>]<格式字>
各个格式字符及其意义见表 3-1。
表3-1 输入格式字符

格式字符 意 义

d 输入一个十进制整数
o 输入一个八进制整数
x 输入一个十六进制整数
f 输入一个小数形式的浮点数
e 输入一个指数形式的浮点数
c 输入一个字符
s 输入一个字符串

各修饰符是可选的,可以没有,这些修饰符是:
⑴ 字段宽度
例如:scanf("%3d",&a)
按宽度3输入一个整数赋给变量 a。
⑵ l和h
可以和 d、o、x一起使用,加 l表示输入数据为长整数,加 h表示输入数据为短整数,例
如:
scanf("%10ld%hd",&x,&i)

则x按宽度为10的长整型读入,而 i按短整数读入。
⑶ 字符*
*表示按规定格式输入但不赋予相应变量,作用是跳过相应的数据。
例如:
scanf("%4d%*d%4d",&x,&y,&z)

执行该语句,若输入为“1 2 3↵”
结果为x=1,z=3,y未赋值,2被跳过。
2. 普通字符
普通字符包括空格、转义字符和可打印字符。
(1) 空格
在有多个输入项时,一般用空格或回车作为分隔符,若以空格作分隔符,则当输入项中
包含字符类型时,可能产生非预期的结果,例如:
scanf("%d%c",&a,&ch)

输入32 q
期望a=32,ch=q,但实际上,分隔符空格被读入并赋给 ch。
为避免这种情况,可使用如下语句:
scanf("%d %c",&a,&ch)

此处%d后的空格,就可跳过字符“ q”前的所有空格,保证非空格数据的正确录入。
(2) 转义字符: \n、\t
先看下面的例子:
下载
第3章 程序控制语句 33
scanf("%d%d",&a,&b);
scanf("%d%d%d",&x,&y,&z);

输入为 1 2 3↵
4 5 6↵
结果为:a=1, b=2, x=3, y=4, z=5
若将上述语句改为:
scanf("%d%d\n",&a,&b);
scanf("%d%d%d",&x,&y,&z);

对同样的输入,其结果为 a = 1,b = 2,x = 4,y = 5,z = 6,由于在第一个 s c a n f的最后有一个


\n,所以第二个scanf语句将从第二个输入行获得数据。
(3) 可打印字符
例如:scanf("%d,%d,%c",&a,&b,&ch);
当输入为: 1, 2, q↵
即:a=1,b=2,ch=q
若输入为1 2 q↵
除a = 1正确赋值外,对 b与c h的赋值都将以失败告终,也就是说,这些不打印字符应是输
入数据分隔符, scanf在读入时自动去除与可打印字符相同的字符。
[例3-1] 试编写求梯形面积的程序,数据由键盘输入。
分析:设梯形上底为 A,下底为B,高为H面职为S,则
S=(A+B)×H÷2

程序如下:
main()
{
float a,b,h,s;
printf("please input a,b,h:");
scanf("%f%f%f",&a,&b,&h);
s=0.5*(a+b)*h;
printf("a=%5.2f b=%5.2f h=%5.2f",a,b,h);
printf("s=%7.4f",s);
}

运行结果如下:
RUN ↵
3.5 4.2 2.8↵
please input a,b,h:
a=3.50 b=4.20 h=2.80
s=10.7800

3.2.2 printf( )函数

与格式化输入函数 s c a n f ( )相对应的是格式化输出函数 p r i n t f ( ),其功能为按控制字符串规


定的格式,向缺省输出设备(一般为显示器)输出在输出项列表中列出的各输出项,其基本
格式为:
printf(“控制字符串”,输出项列表)
34 C语言程序设计
下载
输出项可以是常量、变量、表达式,其类型与个数必须与控制字符串中格式字符的类型、
个数一致、当有多个输出项时,各项之间用逗号分隔。
控制字符串必须用双引号括起,由格式说明和普通字符两部分组成。
1. 格式说明
一般格式为:
%[<修饰符>]<格式字符>
格式字符规定了对应输出项的输出格式,常用格式字符见表 3-2。
表3-2 输出格式字符

格式字符 意 义 格式字符 意 义

c 按字符型输出 o 按八进制整数输出
d 按十进制整数输出 x 按十六进制整数输出
u 按无符号整数输出 s 按字符串输出
f 按浮点型小数输出 g 按e和f格式中较短的一种输出
e 按科学计数法输出

修饰符是可选的,用于确定数据输出的宽度、精度、小数位数、对齐方式等,用于产生
更规范整齐的输出,当没有修饰符时,以上各项按系统缺省设定显示。
(1) 字段宽度修饰符
表3-3列出了字段宽度修饰符。
表3-3 字段宽度修饰符

修 饰 符 格式说明 意 义

M %md 以宽度m输出整型数,不足 m时,左补空格


0m %0md 以宽度m输出整型数,不足 m时,左补零
m,n %m .nf 以宽度m输出实型小数,小数位为n位

例如:设i=123,a=12.34567,
则:
printf("%4d+++%5.2f",i,a);
输出: 123+++12.35
printf("%2d+++%2.1f",i,a);
输出:
123+++12.3
可以看出,当指定场宽小于数据的实际宽度时,对整数,按该数的实际场宽输出,对浮
点数,相应小数位的数四舍五入。例如: 1 2 . 3 4 5 6 7按%5.2f 输出,输出 1 2 . 3 5。若场宽小于等
于浮点数整数部分的宽度,则该浮点数按实际位数输出,但小数位数仍遵守宽度修饰符给出
的值。如上面的 12.34567按%2.1f 输出,结果为: 12.3。
在实际应用中,还有一种更灵活的场宽控制方法,用常量或变量的值作为输出场宽,方
法是以一个 "*"作为修饰符,插入到 %之后。
例如:i=123;
printf("%*d",5,i);
下载
第3章 程序控制语句 35
此处,5为场宽,输出为
123

在程序中,可以用一个整形变量K来指示场宽:
printf("%*d",k,i);

可以根据k的值动态地决定 i的显示场宽,这在解某些问题时是相当有用的。
(2) 对齐方式修饰符
负号“-”为“左对齐”控制符,一般所有输出数据为右对齐格式,加一个“ -”号,则
变为“左对齐”方式。
例如: i=123,a=12.34567
printf(“%4d%10.4f”,i,a);
输出为: 123 12.3457
printf(“%-4d%10.4f”,i,a);
输出为: 123 12.3457
printf(“%4d%-10.4f”,i,a);
输出为: 12312.3457
(3) l和h
可以与输出格式字符 d、f、u等连用,以说明是用 long型或short型格式输出数据,如:
%hd 短整型
%lf 精度型
%ld 长整型
%hu 无符号短整型
2. 普通字符
普通字符包括可打印字符和转义字符,可打印字符主要是一些说明字符,这些字符按原
样显示在屏幕上,如果有汉字系统支持,也可以输出汉字。
转义字符是不可打印的字符,它们其实是一些控制字符,控制产生特殊的输出效果。
例如:i=123,n=456,a=12.34567,且i为整型,n为长整型。
printf("%4d\t%7.4f\n\t%lu\n",i,a,n);

输出为:
123 12.3457
456
其中\ t为水平制表符,作用是跳到下一个水平制表位,在各个机器中,水平制表位的宽度
是不一样的,这里设为8个字符宽度,那么“ \t”跳到下一个8的倍数的列上。
“\n”为回车换行符,遇到“ \n”,显示自动换到新的一行。
在c语言中,如果要输出%,则在控制字符中用两个%表示,即%%。
[例3-2] 输出格式控制符的使用。
# include<stdio.h>
main()
{
int a;
36 C语言程序设计
下载
long int b;
short int c;
unsigned int d;
char e;
float f;
double g;
a=1023;
b=2222;
c=123;
d=1234;
e='x';
f=3.1415926535898 ;
g=3.1415926535898;
printf("a=%d\n",a);
printf("a=%0\n",a);
printf("a=%x\n",a);
printf("b=%ld\n",b);
printf("c=%d\n",c);
printf("d=%u\n",d);
printf("e=%c\n",e);
printf("f=%f\n",f);
printf("g=%f\n",g);
printf("\n");
}

执行程序,输出为:
RUN ↵
a=1023
a=1777
a=3ff
b=2222
c=123
d=1234
e=x
f=3.141593
g=3.141593

3.2.3 getchar()函数与putchar()函数

putchar() 与getchar()是对单个字符进行输入输出的函数。
getchar()的功能是返回键盘输入的一个字符,它不带任何参数,其通常格式如下:
ch=getchar()

ch为字符型变量,上述语句接收从键盘输入的一个字符并将它赋给 ch。
putchar()的作用是向屏幕上输出一个字符,它的功能与 printf函数中的%c相当。putchar()必
须带输出项,输出项可以是字符型常量、变量、表达式,但只能是单个字符而不能是字符串。
[例3-3] 输入一个字符,回显该字符并输出其 ASCII码值。
#include<stdio.h>
main()
下载
第3章 程序控制语句 37
{
char ch;
ch=getchar();
putchar(ch);
printf("%d\n",ch);
}

运行程序:
RUN ↵
g↵
g 103

需要注意的是,字符输入输出函数定义在头文件 s t d i o . h中,故当程序中使用 p u t c h a r ( )或
getchar()函数时,必须在 main()之前用语句:
#include"stdio.h"

将stdio.h包含进来。

3.2.4 程序应用举例

[例3-4] 下面的程序是一个复数加法的例子。
#include <stdio.h>
main()
{
float a1,b1,a2,b2;
char ch;
printf("\t\t\tcomplexs Addition\n");
printf("please input the first complex:\n");
printf("\t realpart:");
scanf("%f",&a1);
printf("\t virtualpart:");
scanf("%f",&b1);
printf("%5.2f +i %5.2f\n",a1,b1);
printf("\n please input the second complex:\n");
printf("\t realpart:");
scanf("%f",&a2);
printf("\t virtualpart :");
scanf("%f",&b2);
printf("%5.2f +i %5.2f\n",a2,b2);
printf("\n The addition is :");
printf("%6.3f +i %6.3f\n",a1+a2,b1+b2);
printf(" program normal terminated,press enter...");
ch=getchar();
ch=getchar();
}

运行结果如下:
RUN ↵
complexs addition
please input the first complex :
realpart :1.2 ↵
virtualpart :3.4 ↵
38 C语言程序设计
下载
1.20 +i 3.40
please input the second complex :
realpart :5.6 ↵
virtualpart :7.8 ↵
5.60 +i 7.80
The addition is:6.800 +i 11.200
program normal terminated, press enter....

3.3 条件控制语句

在程序的三种基本结构中,第二种即为选择结构,其基本特点是 :程序的流程由多路分支
组成,在程序的一次执行过程中,根据不同的情况,只有一条支路被选中执行,而其他分支
上的语句被直接跳过。
C语言中,提供 i f语句和s w i t c h语句选择结构, i f语句用于两者选一的情况,而 s w i t c h用于
多分支选一的情形。

3.3.1 if语句

1. if语句的两种基本形式
首先,我们看一个例子,由此了解选择结构的意义及设计方法。
[例3-5] 输入三个数,找出并打印其最小数。
分析:设三个数为 A、B、C,由键盘读入,我们用一个变量 M I N来标识最小数, A、B、
C与MIN皆定义为int型变量。
每次比较两个数,首先比较 A和B,将小的一个赋给 M I N,再把第三个数 C与M I N比较,
再将小的一个赋给 MIN,则最后MIN即为A、B、C中最小数。
算法如下:
1) 输入A、B、C。
2) 将A与B中小的一个赋给 MIN。
3) 将MIN与C中小的一个赋给 MIN。
4) 输出MIN。
将第2)步细化为:若 A<B,则MIN <==A,否则:MIN <==B;其流程图见图3 -1。
第3)步细化为:若 C<MIN,则MIN <==C;其流程图见图 3-2。

真 假 真
A<B C<MIN

MIN<==A MIN<==B MIN<==C

图3-1 例3-5中第2)步的流程图 图3-2 例3-5中第3)步的流程图

对应图3-1和图3-2,正是if语句的两种基本形式,与图 3-2对应的if语句的格式为:
if <表达式> 语句
当表达式为真时,执行语句,表达式为假时跳过语句。
下载
第3章 程序控制语句 39
与图3-1对应的if语句的格式为:
if 〈表达式〉
语句1
else
语句2

当表达式为真时,执行语句1,表达式为假时执行语句 2。无论如何,语句1与语句2每
次只能有一个被执行。
要注意的是: i f或i f . . . e l s e,包括后面要讲到的嵌套 i f,即if...else if...被看成是一条语句,
即使其中的语句是包含多条语句的复合语句,仍然如此。
下面是例3-5的源程序:
main()
{
int a,b,c,min;
printf(" input a,b,c :");
scanf("%d%d%d",&a,&b,&c);
if (a<b)
min = a;
else
min = b;
if (c<min)
min = c;
printf("The result is %d\n",min);
}

执行情况如下:
RUN ↵
input a,b,c: 3 5 2↵
The result is : 2

这里顺便提一下程序书写的缩排问题,所谓缩排,就是下一行与上一行相比,行首向右
缩进若字符,如上例的 min = a 、min = b等。适当的缩排能使程序的结构、层次清晰、一目了
然,增加程序的易读性。应该从一开始就养成一个比较好的书写习惯,包括必要的注释、适
当的空行以及缩排。
2. 复合语句
if语句中,有时需要执行的语句不止一条,这就要用到复合语句。
复合语句,就是用一对花括号括起来的一条或多条语句,形式如下:
{
语句1;
语句2;
……
语句n;
}

无论包括多少条语句,复合语句从逻辑上讲,被看成是一条语句。
复合语句在分支结构、循环结构中,使用十分广泛。
[例3-6] 读入两个数 x、y,将大数存入x,小数存入 y。
40 C语言程序设计
下载
分析: x、y从键盘读入,若 x > = y,只需顺序打出,否则,应将 x,y中的数进行交换,然
后输出。两数交换必须使用一个中间变量 t, 开始
定义三个浮点数 x、y、t。
输入x,y
算法:
1) 读入x、y; 真
x<y
2) 大数存入x,小数存入 y;
t<==x
3) 输出x、y。 x<==y
第2)步求精: y<==t

若x<y,则交换x与y;
再求精,x与y交换; 输入x,y
① t <== x
② x <== y 结束

③ y <== t 图3-3 例3-6算法流程图


算法的流程图见图 3-3,程序如下:
# include <stdio.h>
main()
{
float x,y,t;
printf("input x,y:");
scanf("%f%f",&x,&y);
if (x<y)
{
t=x;
x=y;
y=t;
}
printf ("result:%7.3f\t%7.3f\n",x,y);
}

执行结果:
input x,y :43.2 56.7↵
result : 56.700 43.200

3. if...else if 语句
实际应用中常常面对更多的选择,这时,将 i f . . . e l s e扩展一下,就得到 if...else if结构,其
一般形式为:
if <表达式1>
语句1
else if<表达式2>
语句2
else if <表达式3>
语句3
else 语句4

对应的流程图见图 3-4。
下载
第3章 程序控制语句 41

表达式1


表达式2

真 假
表达式3

语句1 语句2 语句3 语句4

图3-4 if...else if 语句的流程图

[例3 -7] 货物征税问题,价格在1万元以上的征 5 %,5 0 0 0元以上1万元以下的征 3 %,


1000元以上5000以下的征2%,1000元以下的免税,读入货物价格,计算并输出税金。
分析:读入 price,计算tax,这是一个较复杂的分支结构程序设计 (应注意避免重复征税 )。
假定货物的价格在1万元以上,征税应分段累计,各段采用不同税率进行征收。
算法:若price>=10000
则 tax=0.05*(price-10000); price=10000;
否则,若price>=5000
则 tax=0.03*(price-5000)+tax; price=5000;
否则,若price>=1000
则 tax=0.02*(price-1000)+tax; price=1000;
程序如下:
# include <stdio.h>
main()
{
float price,tax=0;
printf("input price:");
scanf("%f",&price);
if(price>=10000.0)
{
tax=0.05*(price-10000)+tax; price=10000;
}
if (price>=5000.0)
{
tax=0.03*(price-5000)+tax;price=5000;
}
if(price>=1000.00)
{
tax=0.02*(price-1000)+tax;
}
printf("the tax=%10.3 f",tax);
}
42 C语言程序设计
下载
运行程序:
RUN ↵
15000 ↵
the tax=480.000

4. if 语句嵌套
在一个if 语句中可以又出现另一个 if语句,这称为if语句的嵌套或多重 if语句:
if <表达式1>
if< 表达式11>
……
else
语句2;
[例3-8] 计算函数
1 x>0
y= 0 x=0
-1 x<0
流程图见图 3-5。

BEGIN

输入x


x>=0

真 x>0 假

y=1 y=0
y=-1

输出 y

END

图3-5 例3-8的流程图

源程序如下:
main()
{
float x,y;
printf("input x,y:");
scanf("%f",&x);
if (x>=0)
if (x>0)
y=1;
else
下载
第3章 程序控制语句 43
y=0;
else
y=-1; y=0
printf("y=%4.0f\n",y);

} x>=0

对多重 i f,最容易犯的错误是 i f与e l s e配对错误, 真


例如,写成如下形式: x>0
真 y=-1
y=0;
y=1
if (x>=0)
if (x>0)
y=1;
图3-6 错误的算法流程图
else
y=-1;

从缩排上可以看出,作者希望 e l s e是与if x>=0 配对,但是 C语言规定 e l s e总是与离它最近


的上一个if 配对,结果,上述算法的流程图变成图 3-6,完全违背了设计者的初衷。
改进的办法是使用复合语句,将上述程序段改写如下:
y=0;
if (x>=0 )
{
if (x>0 )
y=1;
}
else
y=-1;

3.3.2 switch 语句

i f 语句只能处理从两者间选择之一,当要实现几种可能之一时,就要用 if...else if甚至多


重的嵌套 i f来实现,当分支较多时,程序变得复杂冗长,可读性降低。 C语言提供了 s w i t c h开
关语句专门处理多路分支的情形,使程序变得简洁。
switch语句的一般格式为:
switch <表达式>
case 常量表达式1:语句序列1;
break;
case 常量表达式2:语句序列2;
break;
……
case 常量表达式n: 语句n;
break;
default: 语句n+1;

其中常量表达式的值必须是整型,字符型或者枚举类型,各语句序列允许有多条语句,
不需要按复合语句处理,若语句序列 i为空,则对应的 break语句可去掉。图 3-7是switch语句的
流程图。
特殊情况下,如果switch表达式的多个值都需要执行相同的语句,可以采用下面的格式:
44 C语言程序设计
下载
switch (i)
{
case 1:
case 2:
case 3: 语句1;
break;
case 4:
case 5: 语句2;
break;
default: 语句3;
}

表达式=
语句序列1 BREAK
常量表达式1

表达式=
语句序列2 BREAK
常量表达式2

表达式=
常量表达式i

表达式=常
语句序列i+1 BREAK
量表达式i+1

表达式=
语句序列n BREAK
常量表达式n

语句序列n+1

图3-7 switch 语句的流程图

当整型变量 i的值为 1、2或3时,执行语句 1,当i的值为 4或5时,执行语句 2,否则,执行


语句3。
[例3-9] 输入月份,打印 1999年该月有几天。
程序如下:
#include <stdio.h>
main()
{
int month;
int day;
下载
第3章 程序控制语句 45
printf("please input the month number :");
scanf("%d",&month);
switch (month)
{
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12: day=31;
break;
case 4:
case 6:
case 9:
case 11:day=30;
break;
case 2: day=28;
break;
default : day=-1;
}
if day=-1
printf("Invalid month input !\n");
else
printf("1999.%d has %d days \n",month,day);
}

3.3.3 程序应用举例

[例3-10] 解一元二次方程 ax2+bx+c=0,a、b、c由键盘输入。


分析:对系数a、b、c考虑以下情形
1) 若a=0:
① b<>0,则x=-c/b ;
② b=0, 则:① c=0, 则x无定根;
② c<>0,则x无解。
2) 若a<>0;
① b2-4ac>0,有两个不等的实根;
② b2-4ac=0,有两个相等的实根;
③ b2-4ac<0,有两个共轭复根。
用嵌套的if语句完成。程序如下:
#include <math.h>
# include <stdio.h>
main()
{
float a,b,c,s,x1,x2;
double t;
46 C语言程序设计
下载
printf(" please input a,b,c:");
scanf("%f%f%f",&a,&b,&c);
if (a==0.0)
if(b!=0.0)
printf("the root is :%f\n",-c/b);
else if (c==0.0)
printf("x is inexactive\n ");
else
printf("no root!\n");
else
{
s=b*b-4*a*c;
if(s>=0.0)
if(s>0.0)
{
t=sqrt(s);
x1=-0.5*(b+t)/a;
x2=-0.5*(b-t)/a;
,x1,x2);
printf("There are two different roots:%f and%f\n"
}
else
printf("There are two equal roots:%f\n",-0.5*b/a);
else
{
t=sqrt(-s);
x1=-0.5*b/a; /*实部*/
x2=abs(0.5*t/a); 虚部的绝对值*/
/*
printf("There are two virtual roots:");
printf("%f+i%f\t\t%f-i%f\n",x1,x2,x1,x2 );
}
}
}

运行结果如下:
RUN ↵
please input a,b,c :1 2 3↵
There are two virtual roots:
-1.000000 + i 1.000000 -1.000000 - i 1.000000
RNU ↵
please input a,b,c :2 5 3↵
There are two different roots : -1.500000 and -1.000000
RNU ↵
please input a,b,c :0 0 3↵
No root!

3.4 循环控制语句

循环控制结构(又称重复结构)是程序中的另一个基本结构。在实际问题中,常常需要
进行大量的重复处理,循环结构可以使我们只写很少的语句,而让计算机反复执行,从而完
下载
第3章 程序控制语句 47
成大量类同的计算。
C语言提供了 while语句、do...while语句和for语句实现循环结构。

3.4.1 while语句

while语句是当型循环控制语句,一般形式为 :
while <表达式> 语句;

语句部分称为循环体,当需要执行多条语句时,应使用复合语 语句
句。 真 表达式
w h i l e语句的流程图见图 3 - 8,其特点是先判断,后执行,若条 假
件不成立,有可能一次也不执行。
图3-8 while语句的流程图
[例3-11] 求n!
分析: n!= n* (n-1)*(n-2)* .. 2*1, 0!=1。即S0=1,Sn=Sn-1*n。可以从S0开始,依次
求出S1、S2、...Sn。
统一令 S等于阶乘值, S的初值为 0!= 1;变量 i为计数器, i从1变到n,每一步令 S = S * i,
则最终S中的值就是 n!。
流程图见图 3-9,程序如下:
main()
{
int n,i;
long int s;
printf(" please input n (n>=0) :");
scanf("%d",&n);
if (n>=0)
{
s=1;
if (n>0)
{
i=1;
while (i<=n)
{
s*=i;
i=i+1;
}
}
printf("%d! = %ld \n",n,s);
}
else
printf("Invalid input! \n");
}

运行结果如下:
RUN ↵
please input n(n>=0):0↵
0!= 1
48 C语言程序设计
下载
RUN ↵
please input n(n>=0):6↵
6!= 720
RUN ↵
please input n(n>=0):-2 ↵
Invalid input!

考察图3 - 9中循环部分的流程图可以看出,在循环前各变量应有合适的值 ( s = 1 ),另外,控


制循环结束的变量 (此处为 i )必须在循环体中被改变,否则,循环将无限进行下去,成为死循
环。

BEGIN

读入 n



N>=0
S=1

真 假
N>=0
i=1

i=i+1 报错

s=s*i

真 i<=n

输出s

END

图3-9 例3-11的算法流程图

[例3-12] 利用格里高利公式求 π:
π/4 = 1 - 1/3 + 1/5 - 1/7 + ...

直到最后一项的绝对值小于等于 10-6为止。
程序如下:
# include <stdio.h>
# include <math.h>
{
main()
{
double e,pi;
long int n,s;
t=1.0;
n=1;
s=1;
pi=0.0;
下载
第3章 程序控制语句 49
while (fabs(t)>=1e-6)
{
pi=pi+t;
n=n+2;
s=-s;
t=(float)(s)/(float)(n);
}
pi=pi*4;
printf(" pi = %lf\n" ,pi);
}

运行结果为:
RUN ↵
pi = 3.141591

本题中,将多项式的每一项用 t表示, s代表符号,在每一次循环中,只要改变 s、n的值,


就可求出每一项 t。
一般情况下, w h i l e型循环最适合于这种情况:知道控制循环的条件为某个逻辑表达式的
值,而且该表达式的值会在循环中被改变,如同例 3-12的情况一样。

3.4.2 do... while 语句

在C语句中,直到型循环的语句是 do ... while ,它的一般形式为:


语句
do 语句 while <表达式>

其中语句通常为复合语句,称为循环体。 表达式
d o. . .while 语句的流程图见图 3 - 1 0,其基本特点是:先执行后判断, 真
因此,循环体至少被执行一次。
但需要注意的是, do...while与标准的直到型循环有一个极为重要的区 图3-10 do...while语
别,直到型循环是当条件为真时结束循环,而 d o. . .w h i l e语句恰恰相反, 句的流程图

当条件为真时循环,一旦条件为假,立即结束循环,请注意 do...while语句的这一特点。
例[3-13] 计算sin(x) = x- x 3/3! + x 5/5! - x 7/7! + ...
直到最后一项的绝对值小于 1e-7时为止。
分析:这道题使用递推方法来做。
让多项式的每一项与一个变量 n对应, n的值依次为 1,3,5,7,. . .,从多项式的前一项
算后一项,只需将前一项乘一个因子:
(-x 2)/((n-1)*n)

用s表示多项式的值,用t表示每一项的值,程序如下 :
#include <math.h>
# include <stdio.h>
main()
{
double s,t,x;
int n ;
printf("please input x :");
50 C语言程序设计
下载
scanf("%lf" ,&x);
t=x;
n=1;
s=x;
do
{
n=n+2;
t=t*(-x*x)/((float)(n)-1)/(float)(n);
s=s+t;
} while (fabs(t)>=1e-7);
printf("sin(%f )=%lf" ,x,s);
}

运行结果如下:
RUN ↵
please input x:1.5753 ↵
sin(1.575300)=0.999990
RUN ↵
please input x:-0.65 ↵
sin(-0.650000)=-0.605186

3.4.3 for 语句

f o r语句是循环控制结构中使用最广泛的一种循环控制语句,特别适合已知循环次数的情
况。它的一般形式为:
for (<表达式1> ;<表达式2> ;<表达式3>) 语句

for语句很好地体现了正确表达循环结构应注意的三个问题:
1) 控制变量的初始化。 表达式1
2) 循环的条件。
3) 循环控制变量的更新。 表达式 3

表达式1:一般为赋值表达式,给控制变量赋初值; 循环体
表达式2:关系表达式或逻辑表达式,循环控制条件;
真 表达式 2
表达式3:一般为赋值表达式,给控制变量增量或减量。

语句:循环体,当有多条语句时,必须使用复合语句。
图3-11 for 循环的流程图
for循环的流程图如图 3-11,其执行过程如下:
首先计算表达式 1,然后计算表达式 2,若表达式 2为真,则执行循环体;否则,退出 for循
环,执行 for循环后的语句。如果执行了循环体,则循环体每执行一次,都计算表达式 3,然后
重新计算表达式 2,依此循环,直至表达式 2的值为假,退出循环。
[例3-14] 计算自然数 1到n的平方和。
# include <stdio.h>
# include <math.h>
main()
{
int i;
float s;
下载
第3章 程序控制语句 51
printf("please input n :");
scanf("%d" ,&n);
s=0.0;
for(i=1;i<=n;i++)
s=s+(float)(i)*(float)(i);
printf("1*1 + 2*2 +...+%d*%d = %f\n" ,n,n,s);
}

运行结果如下:
RUN ↵
please input n : 5↵
1*1 + 2*2 + ... + 5* 5 = 55.000000

for语句的几种格式
for语句的三个表达式都是可以省略的,但分号“;”绝对不能省略。
a. for(; ;)语句;
这是一个死循环,一般用条件表达式加 break语句在循环体内适当位置,一旦条件满足时,
用break语句跳出for循环。
例如,在编制菜单控制程序时,可以如下:
for(; ;)
{
printf("please input choice( Q=Exit):"); 显示菜单语句块:
/* */
scanf("%c" ,&ch);
if (ch=='Q') or (ch=='q') break; 语句段
/* */
}

b. for(;表达式2;表达式3)
使用条件是:循环控制变量的初值不是已知常量,而是在前面通过计算得到,例如:
i=m-n;
……
for( ;i<k ;i++) 语句;

c. for(表达式1;表达式2;)语句
一般当循环控制变量非规则变化,而且循环体中有更新控制变量的语句时使用。
例如:
for(i=1 ;i<=100 ;)
{
……
i=i*2+1;
……
}

d. for(i=1,j=n;i<j;i++,j--)语句;
在f o r语句中,表达式 1、表达式 3都可以有一项或多项,如本例中,表达式 1同时为 i和j赋
初值,表达式3同时改变i和j的值。当有不止一项时,各项之间用逗号“,”分隔。
另外,C语言还允许在循环体内改变循环变量的值,这在某些程序的设计中是很有用的。
到此,我们已经学习了 C语言中三种循环控制语句 w h i l e、d o. . .w h i l e和f o r语句,下面再讨
52 C语言程序设计
下载
论两个问题:
三种语句的选用
同一个问题,往往既可以用 while语句解决,也可以用 do...while或者for语句来解决,但在
实际应用中,应根据具体情况来选用不同的循环语句,选用的一般原则是:
1) 如果循环次数在执行循环体之前就已确定,一般用 f o r语句;如果循环次数是由循环体
的执行情况确定的,一般用 while语句或者do... while语句。
2) 当循环体至少执行一次时,用 d o. . .w h i l e语句,反之,如果循环体可能一次也不执行,
选用while语句。
循环的嵌套
一个循环的循环体中有另一个循环叫循环嵌套。这种嵌套过程可以有很多重。一个循环
外面仅包围一层循环叫二重循环;一个循环外面包围两层循环叫三重循环;一个循环外面包
围多层循环叫多重循环。
三种循环语句 for、while、do...while可以互相嵌套自由组合。但要注意的是,各循环必须
完整,相互之间绝不允许交叉。如下面这种形式是不允许的:
do
{ ……
for (;;)
{
……
}while( );
}

[例3-15] 打印8行7列的星形矩阵。
流程图见图 3-12,程序如下:
BEGIN

i=0 外循环

i<8

k=0 内循环

k<7

'*'

k=k+1

换行

i=i+1

END

图3-12 例3-15的算法流程图

# include<stdio.h>
下载
第3章 程序控制语句 53
main( )
{
int i,j;
for(i=0;i<8 ,i++) /*控制行*/
{
for(j=0;j<7>;j++) /* 控制列*/
printf("*") ;
printf("\n") ; /*换行*/
}
}

打印结果如下:
RUN ↵
*******
*******
*******
*******
*******
*******
*******
*******

将程序中for(j=0; j<7; j++)改为for(j=0; j<i; j++),用行数来控制每行星号的多少,就可以


打印三角形。

3.4.4 break与continue语句

有时,我们需要在循环体中提前跳出循环,或者在满足某种条件下,不执行循环中剩下
的语句而立即从头开始新的一轮循环,这时就要用到 break和continue语句。
1. break语句
在前面学习 s w i t c h语句时,我们已经接触到 b r e a k语句,在 c a s e子句执行完后,通过 b r e a k
语句使控制立即跳出 s w i t c h结构。在循环语句中, b r e a k语句的作用是在循环体中测试到应立
即结束循环时,使控制立即跳出循环结构,转而执行循环语句后的语句。
[例3-16] 打印半径为 1到10的圆的面积,若面积超过 100,则不予打印。
# include <stdio.h>
main( )
{
int r;
float area;
for(r=1;r<=10;r++)
{
area=3.141593*r*r;
if(area>100.0)
break;
printf("square=%f\n" ,area);
}
printf("now r=%d\n" ,r);
}
54 C语言程序设计
下载
运行程序:
RUN↵
square=3.141593
square=12.566373
square=28.274338
square=50.265488
square=78.539825
now r=6

当break处于嵌套结构中时,它将只跳出最内层结构,而对外层结构无影响。
2. continue语句
c o n t i n u e语句只能用于循环结构中,一旦执行了 c o n t i n u e语句,程序就跳过循环体中位于
该语句后的所有语句,提前结束本次循环周期并开始新一轮循环。
[例3-17] 计算半径为 1到15的圆的面积,仅打印出超过 50的圆面积。
# include <stdio.h>
main()
{
int r;
float area;
for (r=1;r<=5;r++)
{
area=3.141593*r*r;
if(area<50.0)
continue;
printf("square =%f",area);
}
}

结果为:
RUN ↵
square=50.265488
square=78.539825

同b r e a k一样, c o n t i n u e语句也仅仅影响该语句本身所处的循环层,而对外层循环没有影
响。

3.4.5 程序应用举例

[例3-18] 验证哥德巴赫猜想:任一充分大的偶数,可以用两个素数之和表示,例如:
4=2+2
6=3+3
……
98=19+79

哥德巴赫猜想是世界著名的数学难题,至今未能在理论上得到证明,自从计算机出现后,
人们就开始用计算机去尝试解各种各样的数学难题,包括费马大定理、四色问题、哥德巴赫
猜想等,虽然计算机无法从理论上严密地证明它们,而只能在很有限的范围内对其进行检验,
但也不失其意义。费马大定理已于 1 9 9 4年得到证明,而哥德巴赫猜想这枚数学王冠上的宝石,
下载
第3章 程序控制语句 55
至今无人能及。
分析:我们先不考虑怎样判断一个数是否为素数,而从整体上对这个问题进行考虑,可
以这样做:读入一个偶数 n,将它分成p和q,使n=p+q。怎样分呢?可以令 p从2开始,每次加1,
而令q=n-p,如果p、q均为素数,则正为所求,否则令 p=p+q再试。
其基本算法如下:
1) 读入大于3的偶数n。
2) P=1
3) do {
4) p=p+1;q=n-p;
5) p是素数吗?
6) q是素数吗?
7) } while p、q有一个不是素数。
8) 输出n=p+q。
为了判明 p、q是否是素数,我们设置两个标志量 f l a g p和f l a g q,初始值为 0,若p是素数,
令flagp=1,若q是素数,令 flagq=1,于是第7步变成:
7) } while (flagp*flagq==0);
再来分析第 5、第6步,怎样判断一个数是不是素数呢?
素数就是除了1和它自身外,不能被任何数整除的整数,由定义可知:
2、3、5、7、11、13、17、19等是素数;
1、4、6、8、9、10、12、14等不是素数;
要判断i是否是素数,最简单的办法是用 2、3、4、……i-1这些数依次去除 i,看能否除尽,
若被其中之一除尽,则 i不是素数,反之, i是素数。
但其实,没必要用那么多的数去除,实际上,用反证法很容易证明,如果小于等于 i的平
方根的数都除不尽,则 i必是素数。于是,上述算法中的第 5步、第6步可以细化为:
第5)步 p是素数吗?
flagp=1;
for (j=2;j<=[sqrt(p)];j++)
if p除以j的余数 = 0
{ flagp=0;
break; }

第6)步 q是素数吗?
flagq=1;
for (j=2;j<=[sqrt(q)];j++)
if q除以j的余数 = 0
{ flagq=0;
break; }

程序如下:
#include <math.h>
# include <stdio.h>
main()
{
56 C语言程序设计
下载
int j,n,p,q,flagp ,flagq;
printf("please input n :");
scanf(" %d",&n);
if (((n%2)!=0)||(n<=4))
printf("input data error!\n");
else
{
p=1;
do {
p=p+1;
q=n-p;
flagp=1;
for(j=2;j<=(int)(floor(sqrt((double)(p))));j++)
{
if ((p%j)==0)
{
flagp=0;
break;
}
}
flagq=1;
for (j=2;j<=(int)(floor(sqrt((double)(q))));j++)
{
if ((q%j)==0)
{
flagq=0;
break;
}
}
} while (flagp*flagq==0);
printf("%d = %d + %d \n" ,n,p,q);
}
}

程序运行结果如下:
RUN ↵
please input n : 8↵
8 =3+5
RUN ↵
please input n : 98↵
98 =19+79
RUN ↵
please input n : 9↵
input data error!
下载

第4章 函 数
在学习C语言函数以前,我们需要了解什么是模块化程序设计方法。
人们在求解一个复杂问题时,通常采用的是逐步分解、分而治之的方法,也就是把一个
大问题分解成若干个比较容易求解的小问题,然后分别求解。程序员在设计一个复杂的应用
程序时,往往也是把整个程序划分为若干功能较为单一的程序模块,然后分别予以实现,最
后再把所有的程序模块像搭积木一样装配起来,这种在程序设计中分而治之的策略,被称为
模块化程序设计方法。
在C语言中,函数是程序的基本组成单位,因此可以很方便地用函数作为程序模块来实现
C语言程序。
利用函数,不仅可以实现程序的模块化,程序设计得简单和直观,提高了程序的易读性
和可维护性,而且还可以把程序中普通用到的一些计算或操作编成通用的函数,以供随时调
用,这样可以大大地减轻程序员的代码工作量。
函数是C语言的基本构件,是所有程序活动的舞台。函数的一般形式是 :
type-specifier function_name(parameter list)
parameter declarations
{
body of the function
}

类型说明符定义了函数中 r e t u r n语句返回值的类型,该返回值可以是任何有效类型。如果
没有类型说明符出现,函数返回一个整型值。参数表是一个用逗号分隔的变量表,当函数被
调用时这些变量接收调用参数的值。一个函数可以没有参数,这时函数表是空的。但即使没
有参数,括号仍然是必须要有的。参数说明段定义了其中参数的类型。

4.1 函数说明与返回值

当一个函数没有明确说明类型时, C语言的编译程序自动将整型( int)作为这个函数的缺


省类型,缺省类型适用于很大一部分函数。当有必要返回其它类型数据时,需要分两步处理 :
首先,必须给函数以明确的类型说明符;其次,函数类型的说明必须处于对它的首次调用之
前。只有这样, C编译程序才能为返回非整型的值的函数生成正确代码。

4.1.1 函数的类型说明

可将函数说明为返回任何一种合法的 C语言数据类型。
类型说明符告诉编译程序它返回什么类型的数据。这个信息对于程序能否正确运行关系
极大,因为不同的数据有不同的长度和内部表示。
返回非整型数据的函数被使用之前,必须把它的类型向程序的其余部分说明。若不这样
做,C语言的编译程序就认为函数是返回整型数据的函数,调用点又在函数类型说明之前,编
译程序就会对调用生成错误代码。为了防止上述问题的出现,必须使用一个特别的说明语句,
58 C语言程序设计
下载
通知编译程序这个函数返回什么值。下例示出了这种方法。
[例4-1]
float sum( ); /* 函数说明 */
main ( )
{
float first,second;
first =123.23;
second=99.09 ;
printf ("%f",sum (first,second)) ;
}
float sum (a,b) /* 函数定义*/
float a,b;
{
return a+b;
}

第一个函数的类型说明sum( )函数返回浮点类型的数据。这个说明使编译程序能够对 sum( )


的调用产生正确代码。
函数类型说明语句的一般形式是 :
type_specifier function_name (;)

即使函数使用形参,也不要将其写入说明句。若未使用类型说明语句,函数返回的数据类
型可能与调用者所要求的不一致,其结果是难以预料的。如果两者同处于一个文件中,编译程
序可以发现该错误并停止编译。如果不在同一个文件中,编译程序无法发现这种错误。类型检
查仅在编译中进行,链接和运行时均不检查。因此,必须十分细心以确保绝不发生上述错误。
当被说明为整型的函数返回字符时,这个字符值被转换为整数。因为 C语言以不加说明的
方式进行字符型与整型之间的数据转换,因而多数情况下,返回字符值的函数并不是说明为
返回字符值,而是由函数的这种字符型向整型的缺省类型转换隐含实现的。

4.1.2 返回语句

返回语句 r e t u r n有两个重要用途。第一,它使得内含它的那个函数立即退出,也就是使程
序返回到调用语句处继续进行。第二,它可以用来回送一个数值。本章将说明这两个用途。
1. 从函数返回
函数可以用两种方法停止运行并返回到调用程序。第一种是在执行完函数的最后一个语
句之后,从概念上讲,是遇到了函数的结束符“ }”(当然这个花括号实际上并不会出现在目
标码中,但我们可以这样理解)。例如,下面的函数在屏幕上显示一个字符串。
[例4-2]
pr_reverse ()
{
char s[80]; /*定义一个字符数组 */
scanf("%s" ,s); /* 输入一个字符串,其长度不超过 79 个字符*/
printf("%s\n" ,s) ;
}
下载
第4章 函 数 59
一旦字串显示完毕,函数就没事可做了,这时它返回到被调用处。
在实际情况中,没有多少函数是以这种缺省方式终止运行的。因为有时必须送回一个值,
大多数函数用return语句终止运行,有时在函数中设立了多个终止点以简化函数、提高效率。切
记,一个函数可以有多个返回语句。如下所示,函数在s1、s2相等时返回1,不相等时返回-1。
[例4-3]
find_char(s1 ,s2)
char s1,s2 ;
{
if(s1==s2)
return 1;
else
return -1;
}

2. 返回值
所有的函数,除了空值类型外,都返回一个数值(切记,空值是 A N S I建议标准所做的扩
展,也许并不适合读者手头的 C编译程序)。该数值由返回语句确定。无返回语句时,返回值
是0。这就意味着,只要函数没有被说明为空值,它就可以用在任何有效的 C语言表达式中作
为操作数。这样下面的表达式都是合法的 C语言表达式。
x = power (y);
if (max (x,y) >100) printf("greater");
; isdigit (ch);)... ;
for (ch=getchar( )

可是,函数不能作为赋值对象,下列语句是错误的:
swap(x ,y) =100;

C编译程序将认为这个语句是错误的,而且对含有这种错误语句的程序不予编译。
所有非空值的函数都会返回一个值。我们编写的程序中大部分函数属于三种类型。第一
种类型是简单计算型 — 函数设计成对变量进行运算,并且返回计算值。计算型函数实际上
是一个“纯”函数,例如 sqr( )和sin( )。第二类函数处理信息,并且返回一个值,仅以此表示
处理的成功或失败。例如 write( ),用于向磁盘文件写信息。如果写操作成功了, write( )返回
写入的字节数,当函数返回- 1时,标志写操作失败。最后一类函数没有明确的返回值。实际
上这类函数是严格的过程型函数,不产生值。如果读者用的是符合 A N S I建议标准的 C编译程
序,那么所有这一类函数应当被说明为空值类型。奇怪的是,那些并不产生令人感兴趣的结
果的函数却无论如何也要返回某些东西。例如 printf( )返回被写字符的个数。然而,很难找出
一个真正检查这个返回值的程序。因此,虽然除了空值函数以外的所有函数都返回一个值,
我们却不必非得去使用这个返回值。有关函数返回值的一个常见问题是:既然这个值是被返
回的,我是不是必须把它赋给某个变量?回答是:不必。如果没有用它赋值,那它就被丢弃
了。请看下面的程序,它使用了 mul( )函数。mul( )函数定义为:int mul(int x, int y){......}
[例4-4]
main( )
{
int x,y,z;
x=10 ; y=20 ;
60 C语言程序设计
下载
z=mul(x ,y) ; /* 1 */
printf("%d" ,mul(x ,y)) ; /* 2 */
mul(x ,y) ; /* 3 */
}

在第一行, mul( )的返回值被赋予z,在第二行中,返回值实际上没有赋给任何变量,但


被printf( )函数所使用。最后,在第三行,返回值被丢弃不用,因为既没有把它赋给第一个变
量,也没有把它用作表达式中的一部分。

4.2 函数的作用域规则

“语言的作用域规则”是一组确定一部分代码是否“可见”或可访问另一部分代码和数据
的规则。
C语言中的每一个函数都是一个独立的代码块。一个函数的代码块是隐藏于函数内部的,
不能被任何其它函数中的任何语句(除调用它的语句之外)所访问(例如,用 g o t o语句跳转
到另一个函数内部是不可能的)。构成一个函数体的代码对程序的其它部分来说是隐蔽的,它
既不能影响程序其它部分,也不受其它部分的影响。换言之,由于两个函数有不同的作用域,
定义在一个函数内部的代码数据无法与定义在另一个函数内部的代码和数据相互作用。
C语言中所有的函数都处于同一作用域级别上。这就是说,把一个函数定义于另一个函数
内部是不可能的。

4.2.1 局部变量

在函数内部定义的变量成为局部变量。在某些 C语言教材中,局部变量称为自动变量,这
就与使用可选关键字 a u t o定义局部变量这一作法保持一致。局部变量仅由其被定义的模块内
部的语句所访问。换言之,局部变量在自己的代码模块之外是不可知的。切记:模块以左花
括号开始,以右花括号结束。
对于局部变量,要了解的最重要的东西是:它们仅存在于被定义的当前执行代码块中,
即局部变量在进入模块时生成,在退出模块时消亡。
定义局部变量的最常见的代码块是函数。例如,考虑下面两个函数。
[例4-5]
func1()
{
int x; /* 可定义为 auto int x; */
x=10;
}
func2()
{
int x; /* 可定义为 auto int x; */
x=-1999;
}

整数变量 x被定义了两次,一次在 f u n c 1 ( )中,一次在 f u n c 2 ( )中。f u n c 1 ( )和f u n c 2 ( )中的x互


不相关。其原因是每个 x作为局部变量仅在被定义的块内可知。
下载
第4章 函 数 61
语言中包括了关键字 auto,它可用于定义局部变量。但自从所有的非全局变量的缺省值假
定为auto以来,auto就几乎很少使用了,因此在本书所有的例子中,均见不到这一关键字。
在每一函数模块内的开始处定义所有需要的变量,是最常见的作法。这样做使得任何人
读此函数时都很容易,了解用到的变量。但并非必须这样做不可,因为局部变量可以在任何
模块中定义。为了解其工作原理,请看下面函数。
[例4-6]
f()
{
int t;
scanf("%d",&t);
if(t==1){
char s[80]; 此变量仅在这个块中起作用*/
/*
printf("enter name:");
gets(s); /* 输入字符串*/
process(s); /* 函数调用*/
}
}

这里的局部变量 s就是在 i f块入口处建立,并在其出口处消亡的。因此 s仅在 i f块中可知,


而在其它地方均不可访问,甚至在包含它的函数内部的其它部分也不行。
在一个条件块内定义局部变量的主要优点是仅在需要时才为之分配内存。这是因为局部
变量仅在控制转到它们被定义的块内时才进入生存期。虽然大多数情况下这并不十分重要,
但当代码用于专用控制器(如识别数字安全码的车库门控制器)时,这就变得十分重要了,
因为这时随机存储器( RAM)极其短缺。
由于局部变量随着它们被定义的模块的进出口而建立或释放,它们存储的信息在块工作
结束后也就丢失了。切记,这点对有关函数的访问特别重要。当访问一函数时,它的局部变
量被建立,当函数返回时,局部变量被销毁。这就是说,局部变量的值不能在两次调用之间
保持。

4.2.2 全局变量

与局部变量不同,全局变量贯穿整个程序,并且可被任何一个模块使用。它们在整个程
序执行期间保持有效。全局变量定义在所有函数之外,可由函数内的任何表达式访问。在下
面的程序中可以看到,变量 c o u n t定义在所有函数之外,函数 m a i n ( )之前。但其实它可以放置
在任何第一次被使用之前的地方,只要不在函数内就可以。实践表明,定义全局变量的最佳
位置是在程序的顶部。
[例4-7]
int count; /*count 是全局变量 */
main()
{
count = 100;
func1();
}
func1()
62 C语言程序设计
下载
{
int temp;
temp = count;
func2();
printf("count is %d",count); /* 打印 100 */
}
func2()
{
int count;
for(count = 1; count < 10; count++)
putchar('.'); /* 打印出"。" */
}

仔细研究此程序后,可见变量 c o u n t既不是 m a i n ( )也不是 f u n c 1 ( )定义的,但两者都可以使


用它。函数 f u n c 2 ( )也定义了一个局部变量 c o u n t。当f u n c 2访问c o u n t时,它仅访问自己定义的
局部变量 c o u n t,而不是那个全局变量 c o u n t。切记,全局变量和某一函数的局部变量同名时,
该函数对该名的所有访问仅针对局部变量,对全局变量无影响,这是很方便的。然而,如果
忘记了这点,即使程序看起来是正确的,也可能导致运行时的奇异行为。
全局变量由 C编译程序在动态区之外的固定存储区域中存储。当程序中多个函数都使用同
一数据时,全局变量将是很有效的。然而,由于三种原因,应避免使用不必要的全局变量:
①不论是否需要,它们在整个程序执行期间均占有存储空间。②由于全局变量必须依靠外部
定义,所以在使用局部变量就可以达到其功能时使用了全局变量,将降低函数的通用性,这
是因为它要依赖其本身之外的东西。③大量使用全局变量时,不可知的和不需要的副作用将
可能导致程序错误。如在编制大型程序时有一个重要的问题:变量值都有可能在程序其它地
点偶然改变。
结构化语言的原则之一是代码和数据的分离。 C语言是通过局部变量和函数的使用来实现
这一分离的。下面用两种方法编制计算两个整数乘积的简单函数 mul()。
通用的 专用的
mul(x,y) int x,y;
int x,y; mul()
{ {
return(x*y); return(x*y);
} }
两个函数都是返回变量 x和y的积,可通用的或称为参数化版本可用于任意两整数之积,
而专用的版本仅能计算全局变量 x和y的乘积。

4.2.3 动态存储变量

从变量的作用域原则出发,我们可以将变量分为全局变量和局部变量;换一个方式,从
变量的生存期来分,可将变量分为动态存储变量及静态存储变量。
动态存储变量可以是函数的形式参数、局部变量、函数调用时的现场保护和返回地址。
这些动态存储变量在函数调用时分配存储空间,函数结束时释放存储空间。动态存储变量的
定义形式为在变量定义的前面加上关键字“ auto”,例如:
下载
第4章 函 数 63
auto int a, b, c;

“a u t o”也可以省略不写。事实上,我们已经使用的变量均为省略了关键字“ a u t o”的动
态存储变量。有时我们甚至为了提高速度,将局部的动态存储变量定义为寄存器型的变量,
定义的形式为在变量的前面加关键字“ register”,例如:
register int x, y, z;

这样一来的好处是:将变量的值无需存入内存,而只需保存在 C P U内的寄存器中,以使
速度大大提高。由于 C P U内的寄存器数量是有限的,不可能为某个变量长期占用。因此,一
些操作系统对寄存器的使用做了数量的限制。或多或少,或根本不提供,用自动变量来替代。

4.2.4 静态存储变量

在编译时分配存储空间的变量称为静态存储变量,其定义形式为在变量定义的前面加上
关键字“static”,例如:
static int a=8;

定义的静态存储变量无论是做全程量或是局部变量,其定义和初始化在程序编译时进行。
作为局部变量,调用函数结束时,静态存储变量不消失并且保留原值。
[例 4-8]
main( )
{
inf f( ); /*函数声明*/
int j;
for (j=0; j<3; j++)
printf ("%d\n",f( ));
}
int f( ) /*无参函数*/
{
static int x=1;
x++;
return x;
}

运行程序:
RUN ↵
2
3
4

从上述程序看,函数 f( ) 被三次调用,由于局部变量 x是静态存储变量,它是在编译时分


配存储空间,故每次调用函数 f( )时,变量 x不再重新初始化,保留加 1后的值,得到上面的输
出。

4.3 函数的调用与参数

如果一个函数要使用参数,它就必须定义接受参数值的变量。
64 C语言程序设计
下载
4.3.1 形式参数与实际参数

函数定义时填入的参数我们称之为形式参数,简称形参,它们同函数内部的局部变量作
用相同。形参的定义是在函数名之后和函数开始的花括号之前。
调用时填入的参数,我们称之为实际参数,简称实参。
必须确认所定义的形参与调用函数的实际参数类型一致,同时还要保证在调用时形参与
实参的个数出现的次序也要一一对应。如果不一致,将产生意料不到的结果。与许多其它高
级语言不同,(是健壮的,它总要做一些甚至你不希望的事情,几乎没有运行时错误检查,完
全没有范围检测。作为程序员,必须小心行事以保证不发生错误,安全运行。

4.3.2 赋值调用与引用调用

一般说来,有两种方法可以把参数传递给函数。第一种叫做“赋值调用”(call by value),
这种方法是把参数的值复制到函数的形式参数中。这样,函数中的形式参数的任何变化不会
影响到调用时所使用的变量。
把参数传递给函数的第二种方法是“引用调用”(call by reference)。这种方法是把参数
的地址复制给形式参数,在函数中,这个地址用来访问调用中所使用的实际参数。这意味着,
形式参数的变化会影响调用时所使用的那个变量 (详细内容请参见后续章节 )。
除少数情况外, C语言使用赋值调用来传递参数。这意味着,一般不能改变调用时所用变
量的值。请看例 4-9。
[例4-9]
main ( )
{
int t =10;
printf("%d %d ",sqr(t) ,t) ; /* sqr(t)是函数调用,t是实参*/
}
int sqr(x) /* 函数定义,x是形式参数*/
int x;
{
x=x*x ;
return (x);
}

在这个例子里,传递给函数 sqr( )的参数值是复制给形式参数 x的,当赋值语句 x = x * x执行


时,仅修改局部变量 x。用于调用 sqr()的变量 t,仍然保持着值 10。
执行程序:
RUN ↵
100 10

切记,传给函数的只是参数值的复制品。所有发生在函数内部的变化均无法影响调用时
使用的变量。

4.4 递归

C语言函数可以自我调用。如果函数内部一个语句调用了函数自己,则称这个函数是“递
下载
第4章 函 数 65
归”。递归是以自身定义的过程。也可称为“循环定义”。
递归的例子很多。例如定义整数的递归方法是用数字 1,2,3,4,5,6,7,8,9加上或
减去一个整数。例如,数字 15是7+8;数字21是9+12; 数字12是9+3。
一种可递归的计算机语言,它的函数能够自己调用自己。一个简单的例子就是计算整数
阶乘的函数 factor( )数N的阶乘是1到N之间所有数字的乘积。例如 3的阶乘是1×2×3,即是6。
factor( )和其等效函数fact( )如例4-10所示。
[例4-10]
factor(n) /* 递归调用方法 */
int n;
{
int answer;
if (n==1)
return (1);
answer=factor(n-1) * ;
n /* 函数自身调用 */
return(answer) ;
}

[例4-11]
fact(n) /* 非递归方法 */
int n;
{
int t,answer ;
answer=1 ;
for (t=1; t<=n ; t++)

answer = answer * t
return(answer) ;
}

非递归函数 fact( )的执行应该是易于理解的。它应用一个从 1开始到指定数值结束的循环。


在循环中,用“变化”的乘积依次去乘每个数。
factor( )的递归执行比 fact( )稍复杂。当用参数 1调用factor( )时,函数返回 1;除此之外的
其它值调用将返回 factor(n-1) * n这个乘积。为了求出这个表达式的值,用( n - 1)调用f a c t o r
( )一直到n等于1,调用开始返回。
计算2的阶乘时对 factor( )的首次调用引起了以参数 1对factor( )的第二次调用。这次调用返
回1,然后被2乘(n的初始值),答案是2(把printf( )语句插入到 factor ( )中,察看各级调用及
其中间答案,是很有趣的)。
当函数调用自己时,在栈中为新的局部变量和参数分配内存,函数的代码用这些变量和
参数重新运行。递归调用并不是把函数代码重新复制一遍,仅仅参数是新的。当每次递归调
用返回时,老的局部变量和参数就从栈中消除,从函数内此次函数调用点重新启动运行。可
递归的函数被说成是对自身的“推入和拉出”。
大部分递归例程没有明显地减少代码规模和节省内存空间。另外,大部分例程的递归形
式比非递归形式运行速度要慢一些。这是因为附加的函数调用增加了时间开销(在许多情况
下,速度的差别不太明显)。对函数的多次递归调用可能造成堆栈的溢出。不过溢出的可能性
66 C语言程序设计
下载
不大,因为函数的参数和局部变量是存放在堆栈中的。每次新的调用就会产生一些变量的复
制品。这个堆栈冲掉其它数据和程序的存储区域的可能性是存在的。但是除非递归程序运行
失控,否则不必为上述情况担心。
递归函数的主要优点是可以把算法写的比使用非递归函数时更清晰更简洁,而且某些问
题,特别是与人工智能有关的问题,更适宜用递归方法。递归的另一个优点是,递归函数不
会受到怀疑,较非递归函数而言,某些人更相信递归函数。编写递归函数时,必须在函数的
某些地方使用 if语句,强迫函数在未执行递归调用前返回。如果不这样做,在调用函数后,它
永远不会返回。在递归函数中不使用 i f语句,是一个很常见的错误。在开发过程中广泛使用
printf( )和getchar( )可以看到执行过程,并且可以在发现错误后停止运行。

4.5 实现问题
在编写 C语言的函数时,有几个要点需要我们牢记,因为它们影响到函数的效率和可用
性。

4.5.1 参数和通用函数

通用函数是指能够被用在各种情况下,或者是可被许多不同程序员使用的函数。我们不
应该把通用函数建立在全局变量上(不应该在通用函数中使用全局变量)。函数所需要的所有
数据都应该用参数传递(在个别难以这样做的情况下,可以使用静态变量)。使用参数传递,
除了有助于函数能用在多种情况下之外,还能提高函数代码的可读性。不用全局变量,可以
使得函数减少因副作用而导致错误的可能性。

4.5.2 效率

函数是 C语言的基本构件。对于编写简单程序之外的所有程序来说,函数是必不可少的。
但在一些特定的应用中,应当消除函数,而采用内嵌代码。内嵌代码是指一个函数的语句中
不含函数调用语句。仅当执行速度是很关键的场合下,才用内嵌代码而不用函数。
有两个原因使得内嵌代码的执行速度比函数快。首先,调用需要花费时间;其次,如果
有参数需要传递,就要把它们放在堆栈中,这也要用时间。在几乎所有的应用中,执行时间
上的这些微小开销是微不足道的。不过当时间开销至关重要时,使用内嵌代码消除函数调用,
可以把每次函数调用的开销节省下来。下面的两个程序都是打印从 1到1 0的数的平方。由于函
数调用需要花费时间,所以内嵌代码版本运行的比另一个要快。
内嵌 函数调用
main ( ) main ( )
{ {
int x; int x;
for (x=1,x<11 ;++x) for (xx=1;x<11 ;++x)
printf ("%d",x*x) ; printf ("%d",sqr(x)) ;
} }
sqr(a) ;
int a;
{
return a*a;
}
下载
第4章 函 数 67
4.6 函数库和文件

一个函数设计完后,我们可以用三种方法处理它: 1) 把它放在 main( )函数的同一个文件


中;2) 把它和写好的其它函数一起放在另一个文件中; 3) 把它放在函数库中。下面分别讨论
这三种方法。

4.6.1 程序文件的大小

因为C语言允许分别编译,很自然就会提出这样的问题:一个文件的最适宜的规模是多
大?这规模很重要,因为编译时间与被编译文件的大小直接相关。一般说来,链接处理的时
间比编译处理的时间短得多,且不需要经常去重新编译已经运行过的代码;另一方面,不得
不同时处理多个文件也确实是件厌烦的事。
问题的答案是,每个用户、每个编译程序、每个操作系统环境都是不同的。可是对大部
分微型机和一般的 C编译程序来说。源程序文件不应长于 1 0 0 0 0个字节,建立短于 5 0 0 0个字节
的文件,可以避免不少麻烦。

4.6.2 分类组织文件

在开发一个大型程序时,最令人烦恼的而又是最常遇到的工作之一就是需要检查每个文
件,以确定某个函数的存放。在程序开发的早期做一点文件组织工作就可以避免这一问题。
首先可以把概念上有关的函数组织到一个文件中。如果在编写正文编辑程序时,把删除
正文所用的所有函数放进另一个文件,等等。
第二,把所有的通用函数放在一起。例如,在数据库程序中,输入/输出格式编排函数
是被其它函数调用的通用函数,应把它们放进一个单独的文件里。
第三,把最高层函数放进一个单独的文件中,如果空间允许,就和 main ( ) 放在一起。最
高层函数被用来启动程序的总体活动。这些例程从本质上定义了程序的操作。

4.6.3 函数库

从技术上讲,函数库与分别编译的函数文件不同。当库中例程被链接到程序中,或当使
用一个分别编译的文件时,文件中的所有函数都被装入和链接到程序中去。对自己创建的函
数文件中的大多数文件来说,文件中所有的函数都是要用到的。而对 C的标准函数库,永远也
无法把所有的函数都连接到自己的程序中去,因为目的码会大得吓人!
有时候我们需要建立一个函数库,例如,假定已经完成了一套专门的统计函数,如果当
前开发的某个程序仅仅需要求出一批数值的均值,我们就不必把这些函数全部装入。在这种
情况下,函数库是很有用的。
大部分C语言的编译程序都有建立函数库的指令。操作过程因编译程序不同而异,可从用
户手册中寻找建库的具体步骤。

4.7 C语言的预处理程序与注释

C程序的源代码中可包括各种编译指令,这些指令称为预处理命令。虽然它们实际上不是
C语言的一部分,但却扩展了 C程序设计的环境。本节将介绍如何应用预处理程序和注释简化
68 C语言程序设计
下载
程序开发过程,并提高程序的可读性。

4.7.1 C语言的预处理程序

ANSI 标准定义的 C语言预处理程序包括下列命令:


#define
#error
#include
#if
#else
#elif
#endif
#ifdef
#ifndef
#undef
#line
#pragma

非常明显,所有预处理命令均以符号#开头,下面分别加以介绍。

4.7.2 #define

命令#d e f i n e定义了一个标识符及一个串。在源程序中每次遇到该标识符时,均以定义的
串代换它。 ANSI标准将标识符定义为宏名,将替换过程称为宏替换。命令的一般形式为:
#define identifier string

注意,该语句没有分号。在标识符和串之间可以有任意个空格,串一旦开始,仅由一新
行结束。
例如,如希望TURE取值1,FALSE 取值0,可说明两个宏 #define
#define TURE 1
#define FALSE 0

这使得在源程序中每次遇到 TURE或FALSE就用0或1代替。
例如,在屏幕上打印“ 0 1 2 ”:
printf("%d %d %d",FALSE ,TRUE ,TRUE+1) ;

宏名定义后,即可成为其它宏名定义中的一部分。例如,下面代码定义了 O N E、T W O及
THREE的值。
#define ONE 1
#define TWO ONE+ONE
#define THREE ONE+TWO

懂得宏替换仅仅是以串代替标识符这点很重要。因此,如果希望定义一个标准错误信息,
可编写如下代码:
#define E_MS "standard error on input\n"
printf(E_MS) ;

编译程序遇到标识符 E _ M S时,就用“ standard error on input\n ”替换。对于编译程序,


printf()语句实际是如下形式:
下载
第4章 函 数 69

printf("standard error on input\n")

如果在串中含有标识符,则不进行替换。例如:
#define XYZ this is a test
.
.
.
printf("XYZ") ;

该段不打印 "this is a test"而打印"XYZ"。


如果串长于一行,可以在该行末尾用一反斜杠续行,例如:
#define LONG_STRING "this is a very long \
string that is used as an example"

C语言程序普遍使用大写字母定义标识符。这种约定可使人读程序时很快发现哪里有宏替
换。最好是将所有的 # d e f i n e放到文件的开始处或独立的文件中 (用# i n c l u d e访问),而不是将它
们分散到整个程序中。
宏代换的最一般用途是定义常量的名字和程序中的“游戏数”。例如,某一程序定义了一
个数组,而它的几个子程序要访问该数组,不应直接以常量定数组大小,最好是用名字定义
之(需改变数组大小时)。
#define MAX_SIZE 100

float balance[MAX_SIZE]

# d e f i n e命令的另一个有用特性是,宏名可以取参量。每次遇到宏名时,与之相连的形参
均由程序中的实参代替。例如:
[例4-12]
#define MIN(a,b) (a<b) ? a : b
main()
{
int x, y;
x = 10;
y = 20;
,MIN(x ,y)) ;
printf("the minimum is: %d"
}

当编译该程序时,由 MIN(a,b)定义的表达式被替换, x和y用作操作数,即 printf()语句被


代换后取如下形式:
printf("the minimum ,(x<y) ? x : y);
is: %d"

用宏代换代替实在的函数的一大好处是宏替换增加了代码的速度,因为不存在函数调用
的开销。但增加速度也有代价:由于重复编码而增加了程序长度。

4.7.3 #error

处理器命令 #error强迫编译程序停止编译,主要用于程序调试。

4.7.4 # include

命令#include 使编译程序将另一源文件嵌入带有 #include的源文件,被读入的源文件必须


70 C语言程序设计
下载
用双引号或尖括号括起来。例如:
#include "stdio.h"
#include <stdio.h>
这两行代码均使用 C编译程序读入并编译用于处理磁盘文件库的子程序。
将文件嵌入# i n c l u d e命令中的文件内是可行的,这种方式称为嵌套的嵌入文件,嵌套层次
依赖于具体实现。
如果显式路径名为文件标识符的一部分,则仅在哪些子目录中搜索被嵌入文件。否则,
如果文件名用双引号括起来,则首先检索当前工作目录。如果未发现文件,则在命令行中说
明的所有目录中搜索。如果仍未发现文件,则搜索实现时定义的标准目录。
如果没有显式路径名且文件名被尖括号括起来,则首先在编译命令行中的目录内检索。
如果文件没找到,则检索标准目录,不检索当前工作目录。

4.7.5 条件编译命令

有几个命令可对程序源代码的各部分有选择地进行编译,该过程称为条件编译。商业软
件公司广泛应用条件编译来提供和维护某一程序的许多顾客版本。
1. #if、#else,#elif及#endif
#if 的一般含义是如果 #if 后面的常量表达式为 t r u e,则编译它与 # e n d i f之间的代码,否则
跳过这些代码。命令 #endif 标识一个#if 块的结束,参见例 4-13。
#if constant-expression
statement sequence
#endif

[例4-13]
# define MAX 100
main( )
{
# if MAX>99

printf("compiled for array greater than 99\n")
# endif
}

由于M A X大于9 9,以上程序在屏幕上显示一串消息。该例说明了一个重点:跟在 # if 后


面的表达式在编译时求值,因此它必须仅含常量及已定义过的标识符,不可使用变量。表达
式不许含有操作符 sizeof。
# else 命令的功能有点象 C语言中的else; #else建立另一选择(在 # if失败的情况下)。因
而上面的例子可扩充,参见例 4-14。
[例4-14]
# define MAX 10
main ( )
{
# if MAX>99

printf("compiled for array greater than 99\n")
#else
下载
第4章 函 数 71

printf("compiled for small array \ n")
#endif
}

在此例中,因为 M A X小于9 9,所以,不编译 # i f块,而是编译 # else块,因此,屏幕上显


示"compiled for small array"这一消息。
注意,# else 既是# if 块又是#else 块头。这是因为任何 #if 仅有一个#endif。
# e l i f命令意义与 ELSE IF 相同,它形成一个 if else-if阶梯状语句,可进行多种编译选择。
#elif 后跟一个常量表达式。如果表达式为 true,则编译其后的代码块,不对其它 #elif表达式进
行测试。否则,顺序测试下一块。
#if expression
statement sequence
#elif expression1
statement sequence
#elif expression2
statement sequence
#elif expression3
statement sequence
#elif expression4
#elif expression3N
statement sequence
#endif

例如:下面程序利用 ACTIVE_COUNTRY定义货币符号。
#define US 0
#define ENGLAND1
#define FRANCE 2
# define ACTIVE_COUNTRY US
#if ACTIVE_COUNTRY = = US
char currency[ ]="dollar;"
#elif ACTIVE_COUNTRY= =ENGLAND
char currency[ ]="pound;"
#else

char currency[ ]="franc"
#endif

#if与#elif命令可能一直嵌套到实现规定的权限,其中 #endif、#else或#elif与最近#if或#elif
关联。例如,下面程序是完全有效的。
#if MAX>100
#if SERIAL_VERSION
int port=198;
#elif
int port=200;
#elif
#else
char out_buffer[100];
#endif
72 C语言程序设计
下载
2. # ifdef 和# ifndef
条件编译的另一种方法是用 # i f d e f与# i f n d e f命令,它们分别表示“如果有定义”及“如果
无定义”。
# ifdef的一般形式是:
# ifdef macroname
statement sequence
#endif

如果宏名在前面 #define语句中已定义过,则该语句后的代码块被编译。
#ifndef的一般形式是:
#ifndef macroname
statement sequence
#endif

如果宏名在 #define 语句中无定义,则编译该代码块。


#ifdel 与#ifndef可以用于#else 语句中,但 #elif 不行。参见 4-15。
[例4-15]
#define TED 10
main ()
{
#ifdef TED
printf("Hi Ted\n");
#else
printf("Hi anyone\n") ;
#endif
#ifndef RALPH

printf ("RALPH not defined\n")
#endif
}

上述代码打印“ Hi Ted ”及“ RALPH not defined ”。如果 T E D没有定义,则显示“ H i


anyone”,后面是“ RALPH not defined”。
可以像嵌套 #if 那样将#ifdef 与#ifndef 嵌套至任意深度。

4.7.6 #undef

命令 #undef 取消其后那个前面已定义过有宏名定义。一般形式为:
#undef macroname

例如:
# define LEN 100
#difine WIDTH 100

char array[LEN][WIDTH]
# undef LEN
# undef WIDTH
/ *at this point both LEN and WIDTH are undefined * /
直到遇到#undef 语句之前, LEN与WIDTH均有定义。
# undef 的主要目的是将宏名局限在仅需要它们的代码段中。
下载
第4章 函 数 73
4.7.7 #line

命令#line改变_LINE_ 与_FILE_的内容,它们是在编译程序中预先定义的标识符。
命令的基本形式如下:
# line number["filename"]

其中的数字为任何正整数,可选的文件名为任意有效文件标识符。行号为源程序中当前
行号,文件名为源文件的名字。命令 #line主要用于调试及其它特殊应用。
例如,下面说明行计数从 100开始;printf( ) 语句显示数 102,因为它是语句 #line 100后的
第3行。
#line 100 /* 初始化行计数器 */
main ( ) /* 行号 100 */
{ /* 行号101 */
printf("%d\n" ,_LINE_) ; /* 行号 102 */
}

4.7.8 #pragma

命令#pragma 为实现时定义的命令,它允许向编译程序传送各种指令。例如,编译程序
可能有一种选择,它支持对程序执行的跟踪。可用 #pragma语句指定一个跟踪选择。

4.7.9 预定义的宏名

ANSI标准说明了五个预定义的宏名。它们是:
_LINE_
_FILE_
_DATE_
_TIME_
_STDC_

如果编译不是标准的,则可能仅支持以上宏名中的几个,或根本不支持。记住编译程序
也许还提供其它预定义的宏名。
_LINE_及_FILE_宏指令在有关#line的部分中已讨论,这里讨论其余的宏名。
_DATE_宏指令含有形式为月 /日/年的串,表示源文件被翻译到代码时的日期。
源代码翻译到目标代码的时间作为串包含在 _TIME_中。串形式为时:分:秒。
如果实现是标准的,则宏 _ S T D C _含有十进制常量 1。如果它含有任何其它数,则实现是
非标准的。
注意:宏名的书写由标识符与两边各二条下划线构成。

4.7.10 注释

在C语言中,所有的注释由字符 / *开始,以 * /结束。在星号及斜杠之间不允许有空格。编


译程序忽略注释开始符到注释结束符间的任何文本。例如,下面程序在屏幕上只打印
“hello”。
main ()
{
printf("hello") ;
74 C语言程序设计
下载
;*/
/*printf ("This is a sample to print hello")
}

注释可出现在程序的任何位置,但它不能出现在关键字或标识符中间。
即,注释x=10+ /*add the numbers */ 5;是有效的,但 swi/* this will not work */tch(c){...
是不正确的,因为 C的关键字不能含有注释。通常也不希望表达式中间出现注释,因为这会使
意义含混不清。
注释不可嵌套,即一个注释内不可含有另一个注释。例如,下面代码段在编译时出错:
/*this is an outer comment
x=y/a ;
/*this is an inner comment -and causes an error */
*/

当需要解释程序的行为时,注释应简明扼要。除了最简单和最直观的函数外,都应有注
释,在函数开始处说明其功能,如何调用以及返回何处。

4.8 程序应用举例

[例4-16] 字符串的显示及反向显示。
#include <stdio.h>
#include <string.h> 包含字符串库函数说明的头文件
/* */
# include <stdio.h>
,int index);
void forward_and_backwards(char line_of_char[] /* 函数声明*/

void main()
{

char line_of_char[80] /*定义字符数组 */
int index = 0;

strcpy(line_of_char ,"This is a string."); /* 字符串拷贝*/


forward_and_backwards(line_of_char ,index) ; /*函数调用*/
}

void forward_and_backwards(char line_of_char[] ,int index) /*函数定义*/


{
if (line_of_char[index])
{
printf("%c" ,line_of_char[index]) ; /* 输出字符*/
forward_and_backwards(line_of_char ,index+1) ; /* 递归调用*/
printf("%c" ,line_of_char[index]) ; /* 输出字符*/
}
}

这是一个递归函数调用的例子。程序中函数 f o r w a r d _ a n d _ b a c k w a r d s ( )的功能是显示一个
字符串后反向显示该字符串。
[例4-17] 计算1~7的平方及平方和。
#include <stdio.h>
下载
第4章 函 数 75
# include<math.h>
void header(); /*函数声明*/
void square(int number) ;
void ending();
int sum; /* 全局变量 */
main()
{
int index;
header() ; /* 函数调用*/
for (index = 1;index <= 7;index++)
square(index) ;
ending() ; /*结束*/
}

void header()
{
sum = 0; /* 初始化变量"sum" */

printf("This is the header for the square program\n\n")
}

void square(int number)


{
int numsq;


numsq = number * number
sum += numsq;
,number ,numsq) ;
printf("The square of %d is %d\n"
}

void ending()
{
,sum) ;
printf("\nThe sum of the squares is %d\n"
}

运行程序:
RUN ↵
This is the header for the square program
The square of 1 is 1
The square of 2 is 4
The square of 3 is 9
The square of 4 is 16
The square of 5 is 25
The square of 6 is 36
The square of 7 is 49
The sum of the squares is 140

这个程序打印出 1到7的平方值,最后打印出 1到7的平方值的和,其中全局变量 sum在多个


函数中出现过。
全局变量在 h e a d e r中被初始化为零;在函数 s q u a r e中,s u m对n u m b e r的平方值进行累加,
76 C语言程序设计
下载
也就是说,每调用一次函数 s q u a r e和s u m就对n u m b e r的平方值累加一次;全局变量 s u m在函数
ending中被打印。
[例4-18] 全局变量与局部变量的作用。
#include <stdio.h>

void head1(void);
void head2(void);
void head3(void);
int count; /* 全局变量 */
main()
{

register int index /* 定义为主函数寄存器变量 */

head1() ;
head2() ;
head3() ;

for (index = 8;index > 0;index--) /* 主函数 "for" 循环 */


{
int stuff; /* 局部变量 */
/* 这种变量的定义方法在 Turbo C 中是不允许的 */
/* stuff 的可见范围只在当前循环体内 */

for(stuff = 0;stuff <= 6;stuff++)


printf("%d ",stuff) ;
,index) ;
printf(" index is now %d\n"
}
}

int counter; /* 全局变量 */


/* 可见范围为从定义之处到源程序结尾 */
void head1(void)
{
int index; /* 此变量只用于 head1 */

index = 23;
,index) ;
printf("The header1 value is %d\n"
}

void head2(void)
{
int count; /* 此变量是函数 head2() 的局部变量 */
/* 此变量名与全局变量 count 重名 */
/* 故全局变量count 不能在函数head2() 中使用*/
count = 53;
,count) ;
printf("The header2 value is %d\n"
counter = 77;
}
下载
第4章 函 数 77
void head3(void)
{
,counter) ;
printf("The header3 value is %d\n"
}

运行程序:
RUN ↵
The headerl value is 23
The header2 value is 53
The header3 value is 77
0 1 2 3 4 5 6 index is now 8
0 1 2 3 4 5 6 index is now 7
0 1 2 3 4 5 6 index is now 6
0 1 2 3 4 5 6 index is now 5
0 1 2 3 4 5 6 index is now 4
0 1 2 3 4 5 6 index is now 3
0 1 2 3 4 5 6 index is now 2
0 1 2 3 4 5 6 index is now 1

该程序的演示帮助读者来了解全局变量、局部变量的作用域,请仔细理解体会。
下载

第5章 数 组
数组是一个由若干同类型变量组成的集合,引用这些变量时可用同一名字。数组均由连
续的存储单元组成,最低地址对应于数组的第一个元素,最高地址对应于最后一个元素,数
组可以是一维的,也可以是多维的。

5.1 一维数组

一维数组的一般说明形式如下 :
type-specifier var_name [size];

在C语言中,数组必须显示地说明,以便编译程序为它们分配内存空间。在上式中,类型
说明符指明数组的类型,也就是数组中每一个元素个数,一维数组的总字节数可按下式计算 :
sizeof( 类型)* 数组长度 = 总字节数

[例5-1] 将数字0到9装入一个整型数组。
main( )
{
int x[10]; /* 定义包含10个整型数的数组,引用为 x[0] ,x[1]...x[9]*/
int t ;
for (t=0; t<10;++t) x[t]=t;
}

C语言并不检验数组边界,因此,数组的两端都有可能越界而使其它变量的数组甚至程序
代码被破坏。在需要的时候,数组的边界检验便是程序员的职责。例如,当使用 gets( )接收字
符输入时,必须确认字符数组的长度足以存放最长的字符串。
一维数组在本质上是由同类数据构成的表,例如,对下列数组 a:
char a[7]

图5-1说明了数组 a在内存中的情形,假定起始地址为 1000。

元素 0 1 2 3 4 5 6
地址 1000 1001 1002 1003 1004 1005 1006

图5-1 起始地址为1000的7元素字符数组

5.1.1 向函数传递一维数组

将一维数组传递给函数时,把数组名作为参数直接调用函数即可,无需任何下标。这样,
数组的第一个元素的地址将传递给该函数。 C语言并不是将整个数组作为实参来传递,而是用
指针来代替它。例如,下面的程序将数组 i的第一个元素的地址传递给函数 func1( )。
main( )
下载
第5章 数 组 79
{
int i[10];
func1(i); /*函数调用,实参是数组名*/
.
.
.
}

函数若要接收一维数组的传递,则可以用下面的二种方法之一来说明形式参数; 1) 有界
数组;2) 无界数组。例如,函数 func1 ( )要接收数组 i可如下说明 :
func1(str)
char str[10]; /* 有界数组,数组的下标只能小于或等于传递数组的大小。 */
{
.
.
.
}
也可说明为:
func1(str)
char str[ ]; / * 无界数组 * /
{
.
.
.
}

这二种说明方法的效果是等价的,它们都通知编译程序建立一个字符指针。第一种说明
使用的是标准的数组说明;后一种说明使用了改进型的数组说明,它只是说明函数将要接收
一个具有一定长度的整型数组。细想就会发现,就函数而言,数组究竟有多长并无关紧要,
因为C语言并不进行数组的边界检验。事实上,就编译程序而言,下面的说明也是可行的。
func1 (str);
int str[32];
{
.
.
.
}
因为编译程序只是产生代码使函数 func1( )接收一个指针,并非真正产生一个包含 3 2个元
素的数组。

5.1.2 字符串使用的一维数组

显然,一维数组的最普通的用法是作为字符串。在 C语言中,字符串被定义为一个以空字
符终结的字符数组。空字符以‘ \ 0’来标识,它通常是不显示的。因此,在说明字符数组时,
必须比它要存放的最长字符串多一个字符。例如,假如要定义一个存放长度为 1 0的字符串的
数组s,可以写成 :
char s[11];
80 C语言程序设计
下载
这样就给字符串末尾的空字符保留了空间。
尽管C语言并不把字符串定义为一种数据类型,但却允许使用字符串常量。字符串常量是
由双引号括起来的字符表。例如,下面两个短语均为字符串常量 :
"hello there"
"this is a test"

不必向字符串的末尾加空字符, C编译程序会自动完成这一工作。
C语言支持多串操作函数,最常用的有 :
名字 功能
strcpy(s1 s2) 将s2拷贝到s1
strcat(s1 s2) 将s2连接到s1的末尾
strlen(s1) 返回s1的长度
strcmp(s1,s2) 若s1与s2相等,返回值为0
若s1<s2,返回值小于0
若s1>s2,返回值大于0
例5-2说明了这些函数的用法。
[例5-2]
# include <stdio.h>
main ( )
{
char s1[80],s2[80]; /*定义字符数组*/
gets (s1); /*输入字符串*/
gets (s2);
,strlen(s1) ,strlen(s2));
printf ("lengthsf: %d %d \n"
if (!strcmp(s1,s2))
printf ("the strings are equal \n");
strcat(s1 ,s2);
printf ("%s\n",s1);
}

切记,当两个串相等时,函数 strcmp( )将返回 Fa l s e,因而当测试串的等价性时,要像前


例中的那样,必须用逻辑运算符!将测试条件取反。
当程序运行并以“ hello”和“hello”这两个串作为输入时,其输出为 :
RUN ↵
hello ↵
hello ↵
lengths:5 5
The strings are equal
hellohello

5.2 二维数组

5.2.1 二维数组的一般形式

C语言允许使用多维数组,最简单的多维数组是二维数组。实际上,二维数组是以一维数
下载
第5章 数 组 81
组为元素构成的数组,要将 d说明成大小为( 10,20)的二维整型数组,可以写成 :
int d[10][20]

请留心上面的说明语句, C不像其它大多数计算机语言那样使用逗号区分下标,而是用方
括号将各维下标括起,并且,数组的二维下标均从 0计算。
与此相似,要存取数组 d中下标为( 3,5)的元素可以写成 :
d[3][5]

在例5-3中,整数1到12被装入一个二维数组。
[例5-3]
main ( )
{
int t,i,num[3][4]
for (t=0; t<3; ++t)
for (i=0;i<4;++i)
num[t][i]=(t*4)+i+1;
}

在此例中, n u m [ 0 ] [ 0 ]的值为 1,n u m [ 0 ] [ 2 ]的值为3, . . . . . .,n u m [ 2 ] [ 3 ]的值为 1 2。可以将


该数组想象为如下表格:
0 1 2 3
0 1 2 3 4
1 5 6 7 8
2 9 10 11 12

二维数组以行—列矩阵的形式存储。第一个下标代表行,第二个下标代表列,这意味着
按照在内存中的实际存储顺序访问数组元素时,右边的下标比左边的下标的变化快一些。图
5-2是一个二维数组在内存中的情形,实际上,第一下标可以认为是行的指针。
记住,一旦数组被证明,所有的数组元素都将分配相应的存储空间。对于二维数组可用
下列公式计算所需的内存字节数:
行数×列数×类型字节数=总字节数

因而,假定为双字节整型,大小为( 10,5)的整型数组将需要: 10×5×2=100 字节


当二维数组用作函数的参数时,实际上传递的是第一个元素(如 [ 0 ] [ 0 ])的指针。不过该
函数至少得定义第二维的长度,这是因为 C编译程序若要使得对数组的检索正确无误,就需要
知道每一行的长度。例如,将要接收大小为( 10,10)的二维数组的函数,可以说明如下:
func1 (x)
int x[ ][10]
{
.
.
.
}
82 C语言程序设计
下载
第2下标

0,0 0,1 0,2 0,3 0,4 0,5 0,6 0,7


1,0 1,1 1,2 1,3 1,4 1,5 1,6 1,7
2,0 2,1 2,2 2,3 2,4 2,5 2,6 2,7
3,0 3,1 3,2 3,3 3,4 3,5 3,6 3,7
4,0 4,1 4,2 4,3 4,4 4,5 4,6 4,7
5,0 5,1 5,2 5,3 5,4 5,5 5,6 5,7
6,0 6,1 6,2 6,3 6,4 6,5 6,6 6,7

图5-2 内存中的二维数组

第一维的长度也可指明,但没有必要。
C编译程序对函数中的如下语句:
X[2][4]

处理时,需要知道二维的长度。若行长度没定义,那么它就不可能知道第三行从哪儿开
始。
[例5-4] 用一个二维数组存放某一教师任教的各班学生的分数。假定教师有三个班,每班
最多有三十名学生。注意各函数存取数组的方法。
#define classes 3
#define grades 30
#include <stdio.h>
main( )
{
void enter_grades() ;
void disp_grades( );
int get_grade( );
定义二维数组,每行存放一个班学生成绩 */
int a[classes] [grades];/*
char ch;
for( ; ;)
{
do { /*菜单显示*/
printf("(E)nter grades\n");
printf("(R)eport grades\n");
printf("(Q)uit\n");
将键盘输入字符转换为大写 */
ch=toupper(getchar()); /*
} while(ch!='E' && ch!='R' && ch!='Q');
switch(ch)
{
case 'E':
enter_grades( );
break;
case 'R':
下载
第5章 数 组 83
disp_grades(grade);
break;
case 'Q':
exit(0);
}
}
}

void enter_grades(a)
int a[][grades];
{
int t, i;
for (t=0;t<classes;t++)
{
printf (" class #%d:\n",t+1);
for (i=0; i<grades; i++)
a[t][i]=get_grade(i);
}
}
int get_grades(num)
int num;
{
char s[80];
,num+1);
printf("enter grade for student # %d:\n"
gets(s) ;/* 输入成绩*/
return(atoi(s));
}
void disp_grades(g) /* 显示学生成绩*/
int g[ ][grades];
{
int t,i;
for(t=0; t<classes; ++t) {
printf("class # %d:\n" ,t+1);
for(i=0;i<grades;++i)
,i+1 ,g[t][i]);
printf("grade for student #%d is %d\n"
}
}

我们将实际问题简化为共有 2个班,每班两个学生,即将程序中的常量定义修改如下:
#define classes 2
#define grades 2

运行程序:
RUN ↵
(E)nter grades
(R)eport grades
(Q)uit: e↵
class #1:
78 ↵
enter grade for student #1:
84 C语言程序设计
下载
89 ↵
enter grade for student #2:
class #2
enter grade for student #1:98 ↵
enter grade for student #2:90 ↵
(E)nter grades
(R)eport grades
(Q)uit: r↵
class #1
grade for student #1 is 78
grade for student #2 is 89
class #2
grade for student #1 is 98
grade for student #2 is 90
(E)nter grades
(R)eport grades
(Q)uit :q↵

运行程序,我们首先看到一个菜单,选择“ e”输入成绩,选择“ r”显示成绩,选择“ q”


退出。atoi()函数用于将实参字符串转换为整型。

5.2.2 字符串数组

程序设计中经常要用到字符串数组。例如,数据库的输入处理程序就要将用户输入的命
令与存在字符串数组中的有效命令相比较,检验其有效性。可用二维字符数组的形式建立字
符串数组,左下标决定字符串的个数,右下标说明串的最大长度。例如,下面的语句定义了
一个字符串数组,它可存放 30个字符串,串的最大长度为 80个字符:
char str_array[30][80];

要访问单独的字符串是很容易的,只需标明左下标就可以了。例如,下面的语句以数组
str_array中的第三个字符串为参数调用函数 gets( )。
gets(str_array[2]);

该语句在功能上等价于:
gets(&str_array[2][0]);

但第一种形式在专业程序员编制的 C语言程序中更为常见。
为帮助理解字符串数组的用法,研究例5-5。它以一个字符串数组为基础做简单的文本编辑。
[例5-5]
#include <stdio.h >
#define MAX 100
#define LEN 80
char text [MAX][LEN]
/* 一个非常简单的文本编辑器 */
main( )
{
register int t,i,j;
for(t=0;t<MAX; t++) 逐行输入字符串 */
/*
{
下载
第5章 数 组 85
printf("%d:" ,t);
gets(text[t]);
if(! text[t][0])
break; /* 空行退出 */
}
for(i=0;i<t ,i++) /*按行,逐个字符输出字符串 */
{
for(j=0; text [i][j];j++)
putchar(text [i][j]);
putchar( '\n');
}
}

该程序输入文本行直至遇到一个空行为止,而后每次一个字符地重新显示各行。

5.3 多维数组

C语言允许有大于二维的数组,维数的限制(如果有的话)是由具体编译程序决定的。多
维数组的一般说明形式为:
Type-specifier name [a][b][c]...[z];

由于大量占有内存的关系,二维或更多维数组较少使用。如前所述,当数组定义之后,
所有的数组元素都将分配到地址空间。例如,大小为( 10,6,9,4)的四维字符数组需要 10
×6×9×4即2160字节。
如果上面的数组是两字节整型的,则需要 4 3 2 0字节,若该数组是双字型的(假定每个双
字为8字节)则需要34560字节,存储量随着维数的增加呈指数增长。
关于多维数组,需要注意一点:计算机要花大量时间计算数组下标,这意味着存取多维
数组中的元素要比存取一维数组的元素花更多的时间。由于这些和其它原因,大量的多维数
组一般采用 C语言动态分配函数及指针的方法,每次对数组的一部分动态地分配存储空间。
多维数组传递给函数时,除第一维外,其它各维都必须说明。例如,将数组 m定义成:
int m[4][3][6][5];

那么接收m的函数应写成:
func1 (d)
int d[][3][6][5];

当然,如果愿意,也可加上第一维的说明。

5.4 数组的初始化

5.4.1 数组初始化

C语言允许在说明时对全局数组和静态局部数组初始化,但不能对非静态局部数组初始化。
与其它变量相似,数组初始化的一般形式如下:
type-specifier array_name[size1]...[sizen]={value-list};

数值表是一个由逗号分隔的常量表。这些常量的类型与类型说明相容,第一个常量存入
86 C语言程序设计
下载
数组的第一个单元,第二个常量存入第二个单元,等等,注意在括号“ }”后要加上分号。
下列中一个 10元素整型数组被初始化装入数字 1到10:
int i[10]={1,2,3,4,5,6,7,8,9,10};

这意味着i[0]的值为1,而i[9]的值为10。
存放字符串的字符数组的初始化可采用如下简化的形式:
char array_name[size] = "string";

例如,以下代码段将 str 初始化为” hello”。


char str[6] = "hello";

上面代码产生和下面代码相同的结果:
char str[6]={'h','e' ,'l' ,'l' ,'o' ,'\o'};

因为C语言中的字符串都以空( N U L L)字符为终结,故要确认定义的数组足够长以存放
空字符。这就是为什么 h e l l o只有5个字符,而 str 要有6个字符长的原因。使用字符串常量时,
编译程序自动地在末尾加上空字符。
多维数组初始化的方法与一维数组相同,例如,下式将 s q r s初始化为从 1到1 0及它们各自
的平方数。
int sqrs[10][2]={
1,1,
2,4,
3,9,
4,16 ,
5,25 ,
6,36 ,
7,49 ,
8,64 ,
9,81 ,
10 ,100 ,
};

5.4.2 变长数组的初始化

设想用数组初始化的方法建立一个如下错误信息表:
char e1[12] = "read error\n";
char e2[13] = "write error\n";
char e3[18] = "cannot open file\n";
可以想象,如果用手工去计算每一条信息的字符数以确定数组的长度是何等的麻烦。利
用变长数组初始化的方法可以使 C自动地计算数组的长度。变长数组初始化就是使 C编译程序
自动建立一个不指明长度的足够大的数组以存放初始化数据。使用这种方法,以上信息表变
为:
char e1[] = "read error\n";
char e2[] = "write error\n";
char e3[] = "cannot open file\n";

给定上面的初始化,下面的语句
下载
第5章 数 组 87
,e2,sizeof(e2));
printf("%s has length %d\n"

将打印出:
write error
has length 13

除了减少麻烦外,应用变长数组初始化使程序员可以修改任何信息,而不必担心随时可
能发生的计算错误。
变长数组初始化的方法不仅仅限于一维数组。但在对多维数组初始化时,必须指明除了
第一维以外其它各维的长度,以使编译程序能够正确地检索数组。其方法与数组形式参数的
说明类似。这样就可以建立变长表,而编译程序自动地为它们分配存储空间。例如,下面用
变长数组初始化的方法定义数组 sqrs:
int sqrs[ ][2]={
1,1,
2,4,
3,9,
4,16,
5,25 ,
6,36 ,
7,49,
8,64 ,
9,81 ,
10 ,100
};

相对定长数组的初始化而言,这种说明的优点在于可以在不改变数组各维长度的情况下,
随时增加或缩短表的长度。

5.5 应用程序举例

[例5-6] 为比赛选手评分。
计算方法:从 1 0名评委的评分中扣除一个最高分,扣除一个最低分,然后统计总分,并
除以8,最后得到这个选手的最后得分 (打分采用百分制 )。
#include<stdio.h>
main()
{
int score[10]; /*10 个评委的成绩*/
float mark; 最后得分*/
/*
int i;
int max = -1; /* 最高分*/
int min = 101; /*最低分*/
int sum = 0; 个评委的总和*/
/*10

for(i=0;i<10;i++)
{
printf("Please Enter the Score of No. ,i
%d"+1);
scanf("%d\n" ,&score[i]);
88 C语言程序设计
下载
sum=sum+score[i];
}
for(i=0;i<10;i++)
{
if(score[i]>max)
max=score[i];
}
for(i=0;i<10;i++)
{
if(score[i]<min)
min=score[i];
}
mark=(sum-min-max)/8.0;
,mark);
printf("The mark of the player is %.1f\n"
}

[例5-7] 数列排序,采用选择法实现对有 5个数的数列进行排序。


选择法的算法思想是:(降序)
1. 将待排序的 n个数放入数组num中,即num[0]、num[1]、...num[n-1]。
2. 让n u m [ 0 ]与后续 n u m [ 1 ] . . . n u m [ n - 1 ]依次比较,保证大数在前、小数在后。此次比较,
num[0]是数组中最大。
3. 余下n-1个元素
4. num[1]与num[2]...num[n-1]依次比较,大数在前、小数在后,此次 num[1]是全部元素的
最大。
num[n-2]与num[n-1]比较,num[n-2]存大数。
num[n-1]存小数,比较结束,整理有序。
例:待排序 5个数为: 44 76 82 63 71

一趟排序: 1次比较:76 44 82 63 71

2次比较:82 44 76 63 71

3次比较:82 44 76 63 71

4次比较:82 44 76 63 71

最大
#include <stdio.h>
main()
{
int num[5];
int i,j;
int temp;

num[0]=94; num[1]=76; num[2]=82; num[3]=63; num[4]=71;


for(i=0; i<4; i++)
for(j=i+1; j<5; j++)
{
下载
第5章 数 组 89
if(num[i]>num[j])
{
temp=num[i];
num[i]=num[j];
num[j]=temp;
}
}
for(i=0; i<5; i++)
printf("%4d" ,num[i]);
printf("ok\n");
}

这是一个非常简单的排序程序,我们只需稍加扩展就可以编制出很多功能强大的管理程
序,如学生统计总分、平均排列年级名次等。
[例5-8] 简易学生成绩查询系统。
图5-3为学生成绩登记表,下例程序完成如下功能:
1) 根据输入的学生学号,给出各次考试成绩及平均成绩;
2) 根据输入考试的次数,打印出该次考试中每个学生的成绩,并给出平均分;
3) 根据学号查出学生某次考试成绩;
4) 录入考试成绩。

考试
1 2 3 4 5 6
学号 成绩
1 80 60 70 80 50 90
2 80 70 82 50 90 60
3 75 86 74 81 92 61
4 55 61 70 72 74 81

图5-3 学生成绩表

#include <stdio.h>
mian()
{
int select;
int i,j;
int score[5][7];
int average=0;
int sum=0;
do{
printf(" 本程序有4项功能\n");
printf("1 、根据学号查询学生成绩 \n");
printf("2 、根据考试号统计成绩\n");
printf("3 、根据考试号和学号查询成绩 \n");
printf("4 、成绩录入\n");
printf("0 、退出\n");
printf(" 请输入选择(0-4 ):");
scanf("%d\n" ,&select);
90 C语言程序设计
下载
switch(select)
{
case 0:
printf("OK\n");
exit(0)
break;
case 1:
printf(" 输入学号:");
scanf("%d\n" ,&i);
for(j=1; j<7; j++)
{
printf(" 第%d科成绩是%d\n" ,j,score[i][j]);
sum += score[i][j];
}
average=sum/6;
printf(" 学生的平均成绩是 %d\n" ,average);
break;
case 2:
printf(" 输入考试号:");
scanf("%d\n" ,&j);
for(i=1; i<5; i++)
{
printf(" 第%d 号学生本科成绩是 %d\n" ,i,score[i][j]);
sum += score[i][j];
}
average=sum/4;
printf(" 本科平均成绩是 %d\n" ,average);
break;
case 3:
printf(" 输入学号和考试号 :");
scanf("%d %d\n",&i ,&j);
printf(" 第 %d 号学生的第 %d 科考试成绩是 %d\n" , i , j ,
score[i][j]);
break;
case 4:
printf(" 请输入成绩\n");
for(i=1; i<5; i++)
for(j=1; j<7; j++)
scanf("%d\n" ,&score[i][j]);
break;
default:
break;
}while(1);
}

从本例中可以看出,当涉及到二维数组时,通常用两重 for循环来存取元素。
下载

第6章 指 针
指针是C语言的精华部分,通过利用指针,我们能很好地利用内存资源,使其发挥最大的
效率。有了指针技术,我们可以描述复杂的数据结构,对字符串的处理可以更灵活,对数组
的处理更方便,使程序的书写简洁,高效,清爽。但由于指针对初学者来说,难于理解和掌
握,需要一定的计算机硬件的知识做基础,这就需要多做多练,多上机动手,才能在实践中
尽快掌握,成为 C的高手。

6.1 指针与指针变量

过去,我们在编程中定义或说明变量,编译系 2000 1 变量a


统就为已定义的变量分配相应的内存单元,也就是
2002 2 变量b
说,每个变量在内存会有固定的位置,有具体的地
2004
址。由于变量的数据类型不同,它所占的内存单元 3.4 变量x
数也不相同。若我们在程序中做定义为:
int a=1, b=2; 2008 4.5 变量y
float x=3.4, y=4.5;
double m=3.124;
char ch1='a', ch2='b';
变量m
让我们先看一下编译系统是怎样为变量分配内 3.124
2012
存的。变量 a , b是整型变量,在内存各占 2个字节;
2020 a 变量ch1
x , y是实型,各占 4个字节; m是双精度实型,占 8个 2021 b 变量ch2
字节; c h 1 , c h 2是字符型,各占 1个字节。由于计算
机内存是按字节编址的,设变量的存放从内存 2 0 0 0
单元开始存放,则编译系统对变量在内存的安放情
图6-1 不同数据类型的变量在内存中
况为图6-1所示。
占用的空间
变量在内存中按照数据类型的不同,占内存的
大小也不同,都有具体的内存单元地址,如变量 a 在内存的地址是 2 0 0 0,占据两个字节后,
变量 b的内存地址就为 2 0 0 2 ,变量 m的内存地址为 2 0 1 2等。对内存中变量的访问,过去用
scanf("%d%d%f",&a,&b,&x) 表示将数据输入变量的地址所指示的内存单元。那么,访问变量,
首先应找到其在内存的地址,或者说,一个地址唯一指向一个内存变量,我们称这个地址为
变量的指针。如果将变量的地址保存在内存的特定区域,用变量来存放这些地址,这样的变
量就是指针变量,通过指针对所指向变量的访问,也就是一种对变量的“间接访问”。
设一组指针变量 p a、p b、p x、p y、p m、p c h 1、p c h 2,分别指向上述的变量 a、b、x、y、
m、ch1、ch2,指针变量也同样被存放在内存,二者的关系如图 6-2所示:
在图6 - 2中,左部所示的内存存放了指针变量的值,该值给出的是所指变量的地址,通过
该地址,就可以对右部描述的变量进行访问。如指针变量 p a的值为2 0 0 0,是变量 a在内存的地
92 C语言程序设计
下载
址。因此, pa就指向变量 a。变量的地址就是指针,存放指针的变量就是指针变量。

2000 1 变量a
2000 pa
1000
2002 2 变量b
1002 2002 pb

1004 2004 px 3.4 变量x


2004
1006 2008 py

1008 2012 pm 变量y


4.5
2008
1010 2020 pch1
1012 2021 pch2 2012
1014 变量m
3.124
1016

2020 a 变量ch1
2021 b 变量ch2

图6-2 指针变量与变量在内存中的关系

6.2 指针变量的定义与引用

6.2.1 指针变量的定义

在C程序中,存放地址的指针变量需专门定义;
int *ptr1;
float *ptr2;
char *ptr3;

表示定义了三个指针变量 p t r 1、p t r 2、p t r 3。p t r 1可以指向一个整型变量, p t r 2可以指向一


个实型变量, ptr3可以指向一个字符型变量,换句话说, ptr1、ptr2、ptr3可以分别存放整型变
量的地址、实型变量的地址、字符型变量的地址。
定义了指针变量,我们才可以写入指向某种数据类型的变量的地址,或者说是为指针变
量赋初值:
int *ptr1,m= 3;
float *ptr2, f=4.5;
char *ptr3, ch='a';
ptr1=&m;
ptr2=&f;
ptr3=&ch;

上述赋值语句 p t r 1 = & m表示将变量m的地址赋给指针变量 p t r 1,此时 p t r 1就指向m。三条


赋值语句产生的效果是 ptr1指向m;ptr2指向f;ptr3指向ch 。用示意图 6-3描述如下:
下载
第6章 指 针 93
ptr1 m ptr2 f ptr3 ch
&m 3 &f 4.5 &ch a

图6-3 赋值语句的效果

需要说明的是,指针变量可以指向任何类型的变量,当定义指针变量时,指针变量的值
是随机的,不能确定它具体的指向,必须为其赋值,才有意义。

6.2.2 指针变量的引用

利用指针变量,是提供对变量的一种间接访问形式。对指针变量的引用形式为:
*指针变量
其含义是指针变量所指向的值。
[例6-1] 用指针变量进行输入、输出。
main()
{
int *p,m;
scanf("%d",&m);
p=&m; /* 指针p指向变量m*/
printf("%d",*p);
/* p是对指针所指的变量的引用形式 ,与此m意义相同*/
}

运行程序:
RUN ↵
3↵
3

上述程序可修改为:
main()
{
int *p,m;
p=&m;
scanf("%d",p); /* p是变量m的地址,可以替换&m*/
printf("%d", m);
}

运行效果完全相同。请思考一下若将程序修改为如下形式:
main()
{
int *p,m;
scanf("%d",p);
p=&m;
printf("%d", m);
}

会产生什么样的结果呢?事实上,若定义了变量以及指向该变量的指针为:
int a,*p;
94 C语言程序设计
下载
若p=&a; 则称p指向变量 a,或者说 p具有了变量 a的地址。在以后的程序处理中,凡是可
以写&a的地方,就可以替换成指针的表示 p,a就可以替换成为 *p。

6.3 指针运算符与指针表达式

6.3.1 指针运算符与指针表达式

在C中有两个关于指针的运算符:
• &运算符: 取地址运算符, &m即是变量m的地址。
• *运算符:指针运算符, *ptr表示其所指向的变量。
[例6-2] 从键盘输入两个整数,按由大到小的顺序输出。
main()
{
int *p1,*p2,a,b,t; /* 定义指针变量与整型变量*/
scanf("%d,%d",&a,&b);
p1=&a; /* 使指针变量指向整型变量*/
p2=&b;
if(*p1<*p2)
{ /*交换指针变量指向的整型变量 */
t=*p1;
*p1=*p2;
*p2=t;
}
printf("%d,%d\n",a,b);
}

在程序中,当执行赋值操作 p 1 = & a和 p 2 = & b后,指针实实在在地指向了变量 a与b,这时


引用指针*p1与*p2,就代表了变量 a与b。
运行程序:
RUN ↵
3,4 ↵
4,3

在程序运行过程中,指针与所指的变量之间的关系如图 6-4所示:
p1 a *p1 p1 a *p1
&a 3 &a 4
p2 b *p2
&b 4 p2 b *p2

a) &b 3
b)

图6-4 程序运行中指针与变量之间的关系

当指针被赋值后,其在内存的安放如 a),当数据比较后进行交换,这时,指针变量与所指
向的变量的关系如 b)所示,在程序的运行过程中,指针变量与所指向的变量其指向始终没变。
下面对程序做修改。
下载
第6章 指 针 95
[例6-3]
main()
{
int *p1,*p2,a,b,*t;
scanf("%d,%d",&a,&b);
p1=&a;
p2=&b;
if(*p1<*p2)
{ /* 指针交换指向 */
t=p1;
p1=p2;
p2=t;
}
printf("%d,%d\n",*p1,*p2);
}

程序的运行结果完全相同,但程序在运行过程中,实际存放在内存中的数据没有移动,
而是将指向该变量的指针交换了指向。其示意如图 6-5:
p1 a *p1 p1 a *p2
&a 3 &b 3

p2 b *p2 p2 b p1
&b 4 &a 4
a) b)

图6-5 修改后的程序在运行中指针与变量之间的关系

当指针交换指向后, p 1和p 2由原来指向的变量 a和b改变为指向变量 b和a,这样一来, * p 1


就表示变量b,而* p 2就表示变量a。在上述程序中,无论在何时,只要指针与所指向的变量满
足p=&a;我们就可以对变量 a 以指针的形式来表示。此时 p等效于&a,*p等效于变量 a 。

6.3.2 指针变量作函数的参数

函数的参数可以是我们在前面学过的简单数据类型,也可以是指针类型。使用指针类型
做函数的参数,实际向函数传递的是变量的地址。由于子程序中获得了所传递变量的地址,
在该地址空间的数据当子程序调用结束后被物理地保留下来。
[例6-4] 利用指针变量作为函数的参数,用子程序的方法再次实现上述功能。
main()
{
void chang(); /* 函数声明*/
int *p1,*p2,a,b,*t;
scanf("%d,%d",&a,&b);
p1=&a;
p2=&b;
chang(p1,p2); /*子程序调用*/
printf("%d,%d\n",*p1,*p2);
96 C语言程序设计
下载
return 0;
}
void chang(int *pt1,int *pt2)
{ /* 子程序实现将两数值调整为由大到小 */
int t;
if (*pt1<*pt2) /* 交换内存变量的值 */
{
t=*pt1; *pt1=*pt2; *pt2=t;}
return;
}

由于在调用子程序时,实际参数是指针变量,形式参数也是指针变量,实参与形参相结
合,传值调用将指针变量传递给形式参数 pt1和pt2。但此时传值传递的是变量地址,使得在子
程序中pt1和pt2具有了p1和p2的值,指向了与调用程序相同的内存变量,并对其在内存存放的
数据进行了交换,其效果与 [例6-2]相同。
思考下面的程序,是否也能达到相同的效果呢?
main()
{
void chang();
int *p1,*p2,a,b,*t;
scanf("%d,%d",&a,&b);
p1=&a;
p2=&b;
chang(p1,p2);
printf("%d,%d\n",*p1,*p2);
}
void chang(int *pt1,int *pt2)
{
int *t;
if (*pt1<*pt2)
{
t=pt1; pt1=pt2; pt2=t;
}
return;
}

程序运行结束,并未达到预期的结果,输出与输入完全相同。其原因是对子程序来说,
函数内部进行指针相互交换指向,而在内存存放的数据并未移动,子程序调用结束后,
main()函数中p1和p2保持原指向,结果与输入相同。

6.4 指针与数组

变量在内存存放是有地址的,数组在内存存放也同样具有地址。对数组来说,数组名就
是数组在内存安放的首地址。指针变量是用于存放变量的地址,可以指向变量,当然也可存
放数组的首址或数组元素的地址,这就是说,指针变量可以指向数组或数组元素,对数组而
言,数组和数组元素的引用,也同样可以使用指针变量。下面就分别介绍指针与不同类型的
数组。
下载
第6章 指 针 97
6.4.1 指针与一维数组

假设我们定义一个一维数组,该数组在内存会有系统分配的一个存储空间,其数组的名
字就是数组在内存的首地址。若再定义一个指针变量,并将数组的首址传给指针变量,则该
指针就指向了这个一维数组。我们说数组名是数组的首地址,也就是数组的指针。而定义的
指针变量就是指向该数组的指针变量。对一维数组的引用,既可以用传统的数组元素的下标
法,也可使用指针的表示方法。
int a[10] , *ptr; /* 定义数组与指针变量 */
做赋值操作:ptr=a; 或 ptr=&a[0];
则ptr就得到了数组的首址。其中, a是数组的首地址, &a[0]是数组元素 a[0]的地址,由于
a [ 0 ]的地址就是数组的首地址,所以,两条赋值操作效果完全相同。指针变量 p t r就是指向数
组a的指针变量。
若ptr指向了一维数组,现在看一下 C规定指针对数组的表示方法:
1) ptr+n与a + n表示数组元素 a [ n ]的地址,即&a[n] 。对整个 a数组来说,共有 1 0个元素, n
的取值为 0~9,则数组元素的地址就可以表示为 p t r + 0~p t r + 9或a + 0~a + 9,与&a[0] ~& a [ 9 ]
保持一致。
2) 知道了数组元素的地址表示方法, * ( p t r + n )和* ( a + n)就表示为数组的各元素即等效于
a[n]。
3) 指向数组的指针变量也可用数组的下标形式表示为 ptr[n],其效果相当于 *(ptr+n)。
[例6-5] /*以下标法输入输出数组各元素。
下面从键盘输入 10个数,以数组的不同引用形式输出数组各元素的值。
# include <stdio.h>
main()
{
int n,a[10],*ptr=a;
for(n=0;n<=9;n++)
scanf("%d",&a[n]);
printf("1------output! \n");
for(n=0;n<=9;n++)
printf("%4d",a[n]);
printf("\n");
}

运行程序:
RUN ↵

1 2 3 4 5 6 7 8 9 0
1------output!
1 2 3 4 5 6 7 8 9 0

[例6-6] 采用指针变量表示的地址法输入输出数组各元素。
# include<stdio.h>
main()
{
int n,a[10],*ptr=a; /* 定义时对指针变量初始化*/
98 C语言程序设计
下载
for(n=0;n<=9;n++)
scanf("%d",ptr+n);
printf("2------output! \n");
for(n=0;n<=9;n++)
printf("%4d",*(ptr+n));
printf("\n");
}

运行程序:
RUN ↵

1 2 3 4 5 6 7 8 9 0
2------output!
1 2 3 4 5 6 7 8 9 0
[例6-7] 采用数组名表示的地址法输入输出数组各元素。
main()
{
int n,a[10],*ptr=a;
for(n=0;n<=9;n++)
scanf("%d",a+n);
printf("3------output! \n");
for(n=0;n<=9;n++)
printf("%4d",*(a+n));
printf("\n");
}

运行程序:
RUN ↵

1 2 3 4 5 6 7 8 9 0
3------output!
1 2 3 4 5 6 7 8 9 0

[例6-8] 用指针表示的下标法输入输出数组各元素。
main()
{
int n,a[10],*ptr=a;
for(n=0;n<=9;n++)
scanf("%d",&ptr[n]);
printf("4------output! \n");

for(n=0;n<=9;n++)
printf("%4d",ptr[n]);
printf("\n");
}

运行程序:
RUN ↵

1 2 3 4 5 6 7 8 9 0
4----output!
下载
第6章 指 针 99
1 2 3 4 5 6 7 8 9 0

[例6-9] 利用指针法输入输出数组各元素。
main()
{
int n,a[10],*ptr=a;
for(n=0;n<=9;n++)
scanf("%d",ptr++);
printf("5------output! \n");
ptr=a; /*指针变量重新指向数组首址 */
for(n=0;n<=9;n++)
printf("%4d",*ptr++);
printf("\n");
}

运行程序:
RUN ↵

1 2 3 4 5 6 7 8 9 0
5-----output!
1 2 3 4 5 6 7 8 9 0

在程序中要注意 * p t r + +所表示的含义。 * p t r表示指针所指向的变量; p t r + +表示指针所指


向的变量地址加 1个变量所占字节数,具体地说,若指向整型变量,则指针值加 2,若指向实
型,则加 4,依此类推。而 p r i n t f (“% 4 d”, * p t r + + )中,* p t r + +所起作用为先输出指针指向的变
量的值,然后指针变量加 1。循环结束后,指针变量指向如图 6-6所示:
a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9]
1 2 3 4 5 6 7 8 9 0

ptr

图6-6 例6-9中循环结束后的指 针变量

指针变量的值在循环结束后,指向数组的尾部的后面。假设元素 a [ 9 ]的地址为 1 0 0 0 ,整型


占2字节,则ptr的值就为1002。请思考下面的程序段:
main()
{
int n,a[10],*ptr=a;
for(n=0;n<=9;n++)
scanf("%d",ptr++);
printf("4------output! \n");
for(n=0;n<=9;n++)
printf("%4d",*ptr++);
printf("\n");
}

程序与例6-9相比,只少了赋值语句 ptr=a;程序的运行结果还相同吗?

6.4.2 指针与二维数组

定义一个二维数组:
100 C语言程序设计
下载
int a[3][4];

表示二维数组有三行四列共 12个元素,在内存中按行存放,存放形式为图 6-7:


其中a是二维数组的首地址, & a [ 0 ] [ 0 ]既可以看作数组 0行0列的首地址,同样还可以看作
是二维数组的首地址, a [ 0 ]是第0行的首地址,当然也是数组的首地址。同理 a [ n ]就是第n行的
首址;&a[n][m]就是数组元素a[n][m]的地址。
既然二维数组每行的首地址都可以用 a [ n ]来表示,我们就可以把二维数组看成是由 n行一
维数组构成,将每行的首地址传递给指针变量,行中的其余元素均可以由指针来表示。下面
的图6-8给出了指针与二维数组的关系:

图6-7 二维数组在内存中的存放

图6-8 指针与二维数组的关系

我们定义的二维数组其元素类型为整型,每个元素在内存占两个字节,若假定二维数组
从1000单元开始存放,则以按行存放的原则,数组元素在内存的存放地址为 1000~1022。
用地址法来表示数组各元素的地址。对元素 a [ 1 ] [ 2 ],& a [ 1 ] [ 2 ]是其地址, a [ 1 ] + 2也是其地
址。分析 a [ 1 ] + 1与a [ 1 ] + 2的地址关系,它们地址的差并非整数 1,而是一个数组元素的所占位
置2,原因是每个数组元素占两个字节。
对0行首地址与 1行首地址 a与a + 1来说,地址的差同样也并非整数 1,是一行,四个元素占
的字节数8。
由于数组元素在内存的连续存放。给指向整型变量的指针传递数组的首地址,则该指针
指向二维数组。
int *ptr, a[3][4];
下载
第6章 指 针 101
若赋值: ptr=a;则用ptr++ 就能访问数组的各元素。
[例6-10] 用地址法输入输出二维数组各元素。
# include <stdio.h>
main()
{
int a[3][4];
int i,j;
for(i=0;i<3;i++)
for(j=0;j<4;j++)
scanf("%d",a[i]+j); /*地址法*/
for(i=0;i<3;i++)
{
for(j=0;j<4;j++)
是地址法所表示的数组元素*/
printf("%4d",*(a[i]+j)); /* *(a[i]+j)
printf("\n");
}
}

运行程序:
RUN ↵
1 2 3 4 5 6 7 8 9 10 11 ↵
12
1 2 3 4
5 6 7 8
9 10 11 12

[例6-11] 用指针法输入输出二维数组各元素。
# include<stdio.h>
main()
{
int a[3][4],*ptr;
int i,j;
ptr=a[0];
for(i=0;i<3;i++)
for(j=0;j<4;j++)
scanf("%d",ptr++); /* 指针的表示方法 */
ptr=a[0];
for(i=0;i<3;i++)
{
for(j=0;j<4;j++)
printf("%4d",*ptr++);
printf("\n");
}
}

运行程序:
RUN ↵
1 2 3 4 5 6 7 8 9 10 11 ↵
12
1 2 3 4
102 C语言程序设计
下载
5 6 7 8
9 10 11 12

对指针法而言,程序可以把二维数组看作展开的一维数组:
main()
{
int a[3][4],*ptr;
int i,j;
ptr=a[0];
for(i=0;i<3;i++)
for(j=0;j<4;j++)
scanf("%d",ptr++); /* 指针的表示方法*/
ptr=a[0];
for(i=0;i<12 ;i++)
printf("%4d",*ptr++);
printf("\n");
}

运行程序:
RUN ↵
1 2 3 4 5 6 7 8 9 10 11 ↵
12
1 2 3 4 5 6 7 8 9 10 11 12

6.4.3 数组指针作函数的参数

学习了指向一维和二维数组指针变量的定义和正确引用后,我们现在学习用指针变量作
函数的参数。
[例6-12] 调用子程序,实现求解一维数组中的最大元素。
我们首先假设一维数组中下标为 0的元素是最大和用指针变量指向该元素。后续元素与该
元素一一比较,若找到更大的元素,就替换。子程序的形式参数为一维数组,实际参数是指
向一维数组的指针。
# include <stdio.h>
main()
{
int sub_max(); /* 函数声明*/
int n,a[10],*ptr=a; /* 定义变量,并使指针指向数组 */
int max;
for(n=0;n<=i-1;n++) /* 输入数据*/
scanf("%d",&a[n]);
max=sub_max(ptr,10); /* 函数调用,其实参是指针*/
printf("max=%d\n",max);
}
int sub_max(b,i) /* 函数定义,其形参为数组*/
int b[],i;
{
int temp,j;
temp=b[0];
下载
第6章 指 针 103
for(j=1;j<=9;j++)
if(temp<b[j]) temp=b[j];
return temp;
}

程序的m a i n ( )函数部分,定义数组 a 共有1 0个元素,由于将其首地址传给了 p t r,则指针


变量 ptr 就指向了数组,调用子程序,再将此地址传递给子程序的形式参数 b,这样一来, b
数组在内存与 a 数组具有相同地址,即在内存完全重合。在子程序中对数组 b 的操作,与操
作数组 a 意义相同。其内存中虚实结合的示意如图 6-9所示。
m a i n ( )函数完成数据的输入,调用子程序并输出运行结果。 s u b _ m a x ( )函数完成对数组元
素找最大的过程。在子程序内数组元素的表示采用下标法。运行程序:
RUN ↵

1 3 5 7 9 2 4 6 8 0
max=9
[例6-13] 上述程序也可采用指针变量作子程序的形式参数。
# include <stdio.h>
main()
{
int sub_max();
int n,a[10],*ptr=a;
int max;
for(n=0;n<=9;n++)
scanf("%d",&a[n]);
max=sub_max(ptr,10);
printf("max=%d\n",max);
}
int sub_max(b,i) /* 形式参数为指针变量*/
int *b,i;
{
int temp,j;
temp=b[0]; /*数组元素指针的下标法表示 */
for(j=1;j<=i-1;j++)
if(temp<b[j]) temp=b[j];
return temp;
}

在子程序中,形式参数是指针,调用程序的实际参数 p t r为指向一维数组 a的指针,虚实结


合,子程序的形式参数 b得到ptr的值,指向了内存的一维数组。数组元素采用下标法表示,即
一维数组的头指针为 b,数组元素可以用 b[j]表示。其内存中虚实参数的结合如图 6-10所示。
运行程序:
RUN ↵

1 3 5 7 9 2 4 6 8 0
max=9

[例6-14] 上述程序的子程序中,数组元素还可以用指针表示。
# include <stdio.h>
main()
104 C语言程序设计
下载
{
int sub_max();
int n,a[10],*ptr=a;
int max;
for(n=0;n<=9;n++)
scanf("%d",&a[n]);
max=sub_max(ptr,10);
printf("max=%d\n",max);
}
int sub_max(b,i)/* 子程序定义*/
int *b,i;
{
int temp,j;
temp=*b++;
for(j=1;j<=i-1;j++)
if(temp<*b) temp=*b++;
return temp;
}

主程序 主程序
子程序
子程序
a a
a[0] b[0] a[0] b
ptr ptr

a[1] b[1] a[1]

a[2] b[2] a[2]

a[3] b[3] a[3]

a[4] b[4] a[4]

a[5] b[5] a[5]

a[6] b[6] a[6]

a[7] b[7] a[7]

a[8] a[8]
b[8]
a[9] a[9]
b[9]

图6-9 例6-12程序在内存中虚实结合示意图 图6-10 例6-13程序在内存中虚实结合示意图

在程序中,赋值语句 temp=*b++;可以分解为: temp=*b;b++;两句,先作temp=*b;后


作b++;程序的运行结果与上述完全相同。
对上面的程序作修改,在子程序中不仅找最大元素,同时还要将元素的下标记录下来。
# include <stdio.h>
main()
{
int *max();/* 函数声明*/
int n,a[10],*s,i;
for(i=0;i<10;i++)/* 输入数据*/
下载
第6章 指 针 105
scanf("%d",a+i);
s=max(a,10); /*函数调用*/
printf("max=%d,index=%d\n",*s,s-a);
}
int *max(a,n) /* 定义返回指针的函数*/
int *a,n;
{
int *p,*t; /*p 用于跟踪数组, t用于记录最大值元素的地址 */
for(p=a,t=a;p-a<n;p++)
if(*p>*t) t=p;
return t;
}

在m a x()函数中,用 p - a < n来控制循环结束, a是数组首地址, p用于跟踪数组元素的地


址,p - a正好是所跟踪元素相对数组头的距离,或者说是所跟踪元素相对数组头的元素个数,
所以在main()中,最大元素的下标就是该元素的地址与数组头的差,即 s-a。运行程序:
RUN ↵

1 3 5 7 9 2 4 6 8 0
max=9,index=4

[例6-15] 用指向数组的指针变量实现一维数组的由小到大的冒泡排序。编写三个函数用
于输入数据、数据排序、数据输出。
在第5章的例题中,我们介绍过选择法排序及算法,此例再介绍冒泡排序算法。为了将一
组n个无序的数整理成由小到大的顺序,将其放入一维数组 a[0]、a[1]...a[n-1]。冒泡算法如下:
(开序)
① 相邻的数组元素依次进行两两比较,即 a [ 0 ]与a [ 1 ]比、a [ 1 ]与a [ 2 ]比. . . a [ n - 2 ]与a [ n - 1 ]比,
通过交换保证数组的相邻两个元素前者小,后者大。此次完全的两两比较,能免实现 a [ n - 1 ]成
为数组中最大。
② 余下n - 1个元素,按照上述原则进行完全两两比较,使 a [ n - 2 ]成为余下 n - 1个元素中最
大。
③ 进行共计n-1趟完全的两两比较,使全部数据整理有序。
下面给出一趟排序的处理过程:

原始数据 3 8 2 5

第一次相邻元素比: 3 8 2 5

第二次相邻元素比: 3 2 8 5

第三次相邻元素比: 3 2 5 8
4个元素进行 3次两两比较,得到一个最大元素。若相邻元素表示为 a [ j ]和a [ j + 1 ],用指针
变量P指向数组,则相邻元素表示为 *(P+j)和*(P+j+1)程序实现如下:
# include<stdio.h>
#define N 10
main()
{
void input(); /* 函数声明*/
106 C语言程序设计
下载
void sort();
void output();
int a[N],*p; /* 定义一维数组和指针变量*/
input(a,N); /*数据输入函数调用,实参a是数组名*/
p=a; /* 指针变量指向数组的首地址 */
sort(p,N); /* 排序,实参p是指针变量*/
output(p,N); /*输出,实参p是指针变量*/
}
void input(arr,n) /* 无需返回值的输入数据函数定义 ,形参arr 是数组*/
int arr[],n;
{
int i;
printf("input data:\n");
for(i=0;i<n;i++) /* 采用传统的下标法 */
scanf("%d",&arr[i]);
}

void sort(ptr,n) /* 冒泡排序,形参 ptr 是指针变量*/


int *ptr,n;
{
int i,j,t;
for(i=0;i<n-1;i++)
for(j=0;j<n-1-i;j++)
if (*(ptr+j)>*(ptr+j+1))/* 相临两个元素进行比较*/
{
t=*(ptr+j); /*两个元素进行交换 */
*(ptr+j)=*(ptr+j+1);
*(ptr+j+1)=t;
}
}
void output(arr,n) /* 数据输出*/
int arr[],n;
{
int i,*ptr=arr; /* 利用指针指向数组的首地址 */
printf("output data:\n");
for(;ptr-arr<n;ptr++) /*输出数组的n个元素*/
printf("%4d",*ptr);
printf("\n");
}

运行程序:
RUN ↵
3 5 7 9 3 23 43 2 1 ↵10
1 2 3 3 5 7 9 10 23 43

由于C程序的函数调用是采用传值调用,即实际参数与形式参数相结合时,实参将值传给
形式参数,所以当我们利用函数来处理数组时,如果需要对数组在子程序中修改,只能传递
数组的地址,进行传地址的调用,在内存相同的地址区间进行数据的修改。在实际的应用中,
如果需要利用子程序对数组进行处理,函数的调用利用指向数组(一维或多维)的指针作参
数,无论是实参还是形参共有下面四种情况:
下载
第6章 指 针 107
实 参 形 参
1 数组名 数组名
2 数组名 指针变量
3 指针变量 数组名
4 指针变量 指针变量

在函数的调用时,实参与形参的结合要注意所传递的地址具体指向什么对象,是数组的
首址,还是数组元素的地址,这一点很重要。
[例6-16] 用指向二维数组的指针作函数的参数,实现对二维数组的按行相加。
# include <stdio.h>
#define M 3
#define N 4
main()
{
float a[M][N];
float score1,score2,score3, *pa=a[0];/* 指针变量pa指向二维数组*/
/* score1,score2,score3 分别记录三行的数据相加*/
int i,j;
void fun();
for(i=0;i<M;i++)
for(j=0;j<N; j++) /* 二维数组的数据输入 */
scanf("%f",&a[i][j]);
fun(pa,&score1,&score2,&score3);
/* 函数调用,不仅传递数组首地址,还要传递变量的地址 */
printf("%.2f,%.2f,%.2f\n",score1,score2,score3);
}
void fun(b,p1,p2,p3)
float b[ ][N],*p1,*p2,*p3;
{int i,j;
*p1=*p2=*p3=0;
for(i=0;i<M;i++)
for(j=0;j<N;j++)
{
if(i==0) *p1=*p1+b[i][j]; /*第0行的数据相加*/
if(i==1) *p2=*p2+b[i][j]; /* 第1行的数据相加*/
if(i==2) *p3=*p3+b[i][j]; /* 第2行的数据相加*/
}
}

程序中与形式参数 p 1、p 2和p 3相对应的是实际参数 & s c o r e 1、& s c o r e 2和& s c o r e 3,其实际


含义为p1=&score1等,即将变量的地址传递给指针变量达到按行相加。运行程序,
RUN ↵
1 2 3 4↵
3 4 5 6↵
5 6 7 8↵
10 .00 ,18.00 ,26 .00

[例6-17] 求解二维数组中的最大值及该值在二维数组中的位置。
108 C语言程序设计
下载
我们知道,二维数组在内存中是按行存放,假定我们定义二维数组和指针如下:
int a[3][4],*p=a[0];

则指针 p就指向二维数组。其在内存的存放情况如图 6-11所示。


a[0][0] a[0][1] a[0][2] a[0][3] a[1][0] a[1][1] a[1][2] a[1][3] a[2][0] a[2][1] a[2][2] a[2][3]

P[0] p[1] p[2] p[3] p[4] p[5] p[6] p[7] p[8] p[9] p[10] p[11]
图6-11 例6-17中二维数组在内存中的存放

从上述存放情况来看,若把二维数组的首地址传递给指针 p,则映射过程如图 6 - 11
所示。我们只要找到用 p所表示的一维数组中最大的元素及下标,就可转换为在二维数组中的
行列数。
# include<stdio.h>
main()
{
int a[3][4],*ptr,i,j,max,maxi,maxj;
/*max 是数组的最大, maxi 是最大元素所在行, maxj 是最大元素所在列 */
for(i=0;i<3;i++)
for(j=0;j<4;j++)
scanf("%d",&a[i][j]);
ptr=a[0]; /* 将二维数组的首地址传递给指针变量 */
max_arr(ptr,&max,&maxi,12);
maxj=maxi%4; /* 每行有四个元素,求该元素所在列 */
maxi=maxi/4; /* 求该元素所在行*/
printf("max=%d,maxi=%d,maxj=%d",max,maxi,maxj);
}
int max_arr(b,p1,p2,n)
int *b,*p1,*p2,n;
/*b 指向二维数组的指针, p1指向最大值,p2 指向最大值在一维数组中的位置, */
/*n 是数组的大小*/
{
int i;
*p1=b[0]; *p1=0;
for(i=1;i<n;i++)/* 找最大*/
if (b[i]>*p1) {*p1=b[i]; *p2=i;}
}

运行程序:
RUN ↵
4 7 8 9↵
3 7 9 3↵
1 5 2 6↵
max=9,maxi=0,maxj=3

6.4.4 指针与字符数组

在前面的课程中,我们用过了字符数组,即通过数组名来表示字符串,数组名就是数组
的首地址,是字符串的起始地址。下面的例子用于简单字符串的输入和输出。
下载
第6章 指 针 109
#include <stdio.h>
main()
{
char str[20];
gets(str);
printf("%s\n",str);
}

RUN ↵
good morning!↵
good morning!↵

现在,我们将字符数组的名赋予一个指向字符类型的指针变量,让字符类型指针指向字
符串在内存的首地址,对字符串的表示就可以用指针实现。其定义的方法为: char str[20] ,
*P=str;这样一来,字符串 str就可以用指针变量 P来表示了。
#include <stdio.h>
main()
{
char str[20],*p=str ; /* p=str则表示将字符数组的首地址传递给指针变量 p */
gets(str);
printf("%s\n",p);
}
RUN ↵
good morning!↵
good morning!↵

需要说明的是,字符数组与字符串是有区别的,字符串是字符数组的一种特殊形式,存
储时以“ \ 0”结束,所以,存放字符串的字符数组其长度应比字符串大 1。对于存放字符的字
符数组,若未加“ \0”结束标志,只能按逐个字符输入输出。
[例6-18] 字符数组的正确使用方法。
# include<stdio.h>
main()
{
char str[10],*p=str;
int i;
scanf("%s",str); /* 输入的字符串长度超过 10*/
for( i=0;i<10;i++)
printf("%c",*p++); /*正确输出*/
printf("\n");
p=str;
printf("%s",p); /* 字符数组无'\0' 标志,输出出错*/
puts(str); /* 字符数组无'\0' 标志,输出出错*/
}

对上述程序中字符数组以字符串形式输出,若无“ \0”标志,则找不到结束标志,输出出
错。
[例6-19] 用指向字符串的指针变量处理两个字符串的复制。
110 C语言程序设计
下载
字符串的复制要注意的是:若将串 1复制到串2,一定要保证串 2的长度大于或等于串 1。
#include<stdio.h>
main()
{
char str1[30],str2[20],*ptr1=str1,*ptr2=str2;
printf("input str1:");
gets(str1); /* 输入str1*/
printf("input str2:");
gets(str2); /* 输入str2*/
printf("str1------------str2\n");
printf("%s.......%s\n",ptr1,ptr2);
while(*ptr2) *ptr1++=*ptr2++; /* 字符串复制*/
*ptr1='\0'; /* 写入串的结束标志 */
printf("str1------------str2\n");
printf("%s.......%s\n",str1,str2);
}

在程序的说明部分,定义的字符指针指向字符串。语句 while(*ptr2) *ptr1++=*ptr2++;先


测试表达式的值,若指针指向的字符是“ \ 0”,该字符的 A S C I I码值为0,表达式的值为假,循
环结束,表达式的值非零,则执行循环 *ptr1++=*ptr2++。语句*ptr1++按照运算优先级别,先
算*ptr1,再算ptr1++。
运行程序: RUN↵
input str1: I love China! ↵
input str2: I love Chengdu! ↵
str1--------------------str2
I love China! ....... I love Chengdu!
str1--------------------str2
I love Chengdu! ....... I love Chengdu!

现在,我们修改程序中语句 printf("%s.......%s\n",str1,str2)为printf("%s.......%s\n",ptr1, ptr2);


会出现什么结果呢?请思考。
[例6-20] 用指向字符串的指针变量处理两个字符串的合并。
# include<stdio.h>
main()
{
char str1[50],str2[20],*ptr1=str1,*ptr2=str2;
printf( "input str1:");
gets(str1);
printf( "input str2:");
gets(str2);
printf( "str1------------str2\n");
printf( "%s.......%s\n",ptr1,ptr2);
while(*ptr1) ptr1++; /* 移动指针到串尾*/
while(*ptr2) *ptr1++=*ptr2++; 串连接* /* /
*ptr1='\0'; /* 写入串的结束标志 */
ptr1=str1; ptr2=str2;
printf( "str1------------------str2\n");
下载
第6章 指 针 111
printf( "%s.......%s\n",ptr1,ptr2);
}

RUN ↵
input str1: I love China! ↵
input str2: I love Chengdu! ↵
str1--------------------str2
I love China! ....... I love Chengdu!
str1------------------------------------------str2
I love China! I love Chengdu! ...... I love Chengdu!.

需要注意的是,串复制时,串 1的长度应大于等于串 2;串连接时,串 1的长度应大于等于


串1与串2的长度之和。

6.5 指针的地址分配

我们可以定义指针变量指向任何类型的变量。在上述的处理过程中,指针变量指向的变
量通过传递变量的地址来实现。指针变量的取值是内存的地址,这个地址应当是安全的,不
可以是随意的,否则,写入内存单元的值将会使得已存放的数据或程序丢失。应使用编译系
统提供的标准函数来实现地址分配。
A N S I标准建议设置了两个最常用的动态分配内存的函数 malloc() 和f r e e ( ),并包含在
stdlib.h中,但有些 C编译却使用 malloc.h包含。使用时请参照具体的 C编译版本。
我们这里所指的动态内存分配其含义是指:当定义指针变量时,其变量的取值是随机的,
可能指向内存的任一单元。若指针的指向是不安全的内存地址,在该地址空间上的数据交换
就会产生意料不到的效果。为此,在程序的执行过程中,要保证指针操作的安全性,就要为
指针变量分配安全地址。在程序执行时为指针变量所做的地址分配就称之为动态内存分配。
当无需指针变量操作时,可以将其所分配的内存归还系统,此过程我们称之为内存单元的释
放。
malloc( )用以向编译系统申请分配内存; free( )用以在使用完毕释放掉所占内存。
[例6-21] 两个字符串的交换。
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
main()
{
char *ptr1,*ptr2,*temp;
ptr1=malloc(30); /* 动态为指针变量分配长度为30 字节的存储空间 */
ptr2=malloc(20);
temp=malloc(30);
printf("input str1:");
gets(ptr1); /* 输入字符串*/
printf("input str2:");
gets(ptr2);
printf("str1------------str2\n");
printf("%s.......%s\n",ptr1,ptr2);
strcpy(temp,ptr1);/* 串复制*/
112 C语言程序设计
下载
strcpy(ptr1,ptr2);
strcpy(ptr2,temp);
printf("str1------------str2\n");
printf("%s.......%s\n",ptr1,ptr2);
free(ptr1);
free(ptr2);
}

为指针变量分配的存储空间长度取决于存放字符的多少。在上述的程序中,两个串的交
换可以通过标准函数 strcpy() 来完成,也可以通过串指针交换指向完成,用 t e m p = p t r 1;
p t r 1 = p t r 2;p t r 2 = t e m p;三条赋值语句实现。但是,利用指针交换指向,其物理意义与串通过
函数进行的复制完全不同。前者是存放串地址的指针变量数据交换,后者是串在内存物理空
间的数据交换。指针变量用完后,将指针变量所占的存储空间释放。
运行程序: run↵
input str1: China↵
input str2: Chengdu ↵
str1------------str2
China----------Chengdu
str1------------str2
Chengdu----- China

6.6 指针数组

前面介绍了指向不同类型变量的指针的定义和使用,我们可以让指针指向某类变量,并
替代该变量在程序中使用;我们也可以让指针指向一维、二维数组或字符数组,来替代这些
数组在程序中使用,给我们在编程时带来许多方便。
下面我们定义一种特殊的数组,这类数组存放的全部是指针,分别用于指向某类的变量,
以替代这些变量在程序中的使用,增加灵活性。指针数组定义形式:
类型标识 *数组名[数组长度]
例如: char *str[4];
由于[ ] 比*优先权高,所以首先是数组形式 str[4 ],然后才是与“ *”的结合。这样一来指
针数组包含 4个指针 s t r [ 0 ]、s t r [ 1 ]、s t r [ 2 ]、s t r [ 3 ],各自指向字符类型的变量。例如: int *
ptr[5];
该指针数组包含 5个指针 p t r [ 0 ]、p t r [ 1 ]、p t r [ 2 ]、p t r [ 3 ]、p t r [ 4 ],各自指向整型类型的变
量。
[例6-22] 针对指针数组的应用,我们分别用指针数组的各指针指向字符串数组、指向一
维整型数组、指向二维整型数组。
#include <stdlib.h>
#include <stdio.h>
main()
{
char *ptr1[4]={"china","chengdu","sichuang","chongqin"};
/* 指针数组ptr1 的4个指针分别依此指向4个字符串*/
int i,*ptr2[3],a[3]={1,2,3},b[3][2]={1,2,3,4,5,6};
下载
第6章 指 针 113
for (i=0;i<4;i++)
printf("\n%s",ptr1[i]); /* 依此输出 ptr1 数组4个指针指向的 4个字符串*/
printf("\n");
for(i=0;i<3;i++)
ptr2[i]=&a[i]; /*将整型一维数组 a的3个元素的地址传递给指针数组 ptr2*/
for(i=0;i<3;i++)/* 依此输出ptr2 所指向的3个整型变量的值 */
printf("%4d",*ptr2[i]);
printf("\n");
for(i=0;i<3;i++)
ptr2[i]=b[i]; /*传递二维数组b的每行首地址给指针数组的 4 个指针*/
for(i=0;i<3;i++)/* 按行输出*/
printf("%4d%4d\n",*ptr2[i],*ptr2[i]+1);
}

程序中指针数组与所指对象的关系如图 6-12所示。

图6-12 例6-22程序中指针数组与所指对象的关系

p t r 1指针数组中的 4个指针分别指向 4个字符串,如图 6 - 11的a)所示,程序中依此输出;


p t r 2指针数组共有 3个指针,若将整型一维数组 a中各元素地址分别传递给指针数组的各指针,
则ptr2[0]就指向a[0];ptr2[1]就指向a[1];ptr2[2]就指向a[2]。若将二维数组各行的首地址分别
传递给指针数组的各指针,如图 6 - 11 b)所示,这样一来, p t r 2 [ 0 ]就指向了 b数组的第 0行,该
行有两个元素,其地址为 ptr2[0]与ptr2[0]+1;相应指针数组第 i个元素ptr2[i]指向的b数组的第i
行两个元素地址分别为 ptr2[i]与 ptr[i]+1。
运行程序:
RUN ↵

china
chengdu
sichuang
chongqin
1 2 3
1 2
2 4
5 6

在处理二维字符数组时,我们可以把二维字符数组看成是由多个一维字符数组构成,也
就是说看成是多个字符串构成的二维字符数组,或称为字符串数组。
指针数组对于解决这类问题(当然也可以解决其它问题)提供了更加灵活方便的操作。
有一点需要说明,若定义一个指针数组后,指针数组各元素的取值(即地址)要注意安全性。
114 C语言程序设计
下载
如定义指针数组:
char *ptr[3];

我们说该数组包含三个指针,但指针的指向是不确定的,指针现在可能指向内存的任一
地址。假定现在作语句: scanf("%s", ptr[ i ] ), 则输入的字符串在内存的存放其地址由 ptr[ i ]
决定。除非给指针数组元素赋值安全的地址。
[例6-23] 定义字符指针数组,包含 5个数组元素。同时再定义一个二维字符数组其数组
大小为5*10,即5行10列,可存放5个字符串。若将各字符串的首地址传递给指针数组各元素,
那么指针数组就成为名副其实的字符串数组。下面对各字符串进行按字典排序。
在字符串的处理函数中, s t r c m p ( s t r 1 , s t r 2 )函数就可以对两个字符串进行比较,函数的返
回值> 0、= 0、< 0分别表示串 s t r 1大于s t r 2、s t r 1等于s t r 2、s t r 1小于s t r 2。再利用 s t r c p y ( )函数实
现两个串的复制。下面选用冒泡排序法。
#include <stdlib.h>
#include <string.h>
#include<stdio.h>
main()
{
char *ptr1[4],str[4][20],temp[20];
/* 定义指针数组、二维字符数组、用于交换的一维字符数组 */
int i,j;
for (i=0;i<4;i++)
gets(str[i]); /* 输入4个字符串*/
printf("\n");
for(i=0;i<4;i++)
ptr1[i]=str[i]; /* 将二维字符数组各行的首地址传递给指针数组的各指针 */
printf("original string:\n");
for(i=0;i<4;i++) /* 按行输出原始各字符串 */
printf("%s\n",ptr1[i]);
printf("ordinal string:\n");
for(i=0;i<3;i++) /* 冒泡排序*/
for(j=0;j<4-i-1;j++)
if(strcmp(ptr1[j],ptr1[j+1])>0)
{ strcpy(temp,ptr1[j]);
strcpy(ptr1[j],ptr1[j+1]);
strcpy(ptr1[j+1],temp);
}
for( i=0;i<4;i++) /* 输出排序后的字符串 */
printf("%s\n" , ptr1[i]);
}

运行程序:
RUN ↵
jkjkdkddfs ↵
fhfgkjkfgkf ↵
hkfgkgfkklg ↵
jjkdjdk ↵
original string:
下载
第6章 指 针 115
jkjkdkddfs
fhfgkjkfgkf
hkfgkgfkklg
jjkdjdk
ordinal string:
fhfgkjkfgkf
hkfgkgfkklg
jjkdjdk
jkjkdkddfs

程序中一定要注意指针的正确使用。一旦将二维字符数组的各行首地址传递给指针数组
的各指针,则相当于给指针分配了安全可操作的地址,地址空间大小由二维字符数组来决定。
当然也可由编译系统为指针分配地址用于字符串的存放。
[例6-24] 利用 malloc()函数为指针分配存储空间,实现字符串的排序。
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
main()
{
char *ptr1[4],*temp;
int i,j;
for (i=0;i<4;i++)
{
ptr1[i]=malloc(20); /* 为指针数组各指针分配 20字节的存储空间 */
gets(ptr1[i]);
}
printf("\n");
printf("original string:\n");
for(i=0;i<4;i++)
printf("%s\n",ptr1[i]);
printf("ordinal string:\n");
for(i=0;i<3;i++)
for(j=0;j<4-i-1;j++)
if(strcmp(ptr1[j],ptr1[j+1])>0)
{
temp=ptr1[j]; /* 利用指向字符串的指针,进行指针地址的交换 */
ptr1[j]=ptr1[j+1];
ptr1[j+1]=temp;
}
for( i=0;i<4;i++) /* 字符串输出*/
printf("%s\n" , ptr1[i]);
}

运行程序,其结果与上述例 6-23完全相同。
[例6-25] 对已排好序的字符指针数组进行指定字符串的查找。字符串按字典顺序排列,
查找算法采用二分法,或称为折半查找。
折半查找算法描述:
116 C语言程序设计
下载
1. 设按开序(或降序)输入 n 个字符串到一个指针数组。
2. 设low 指向指针数组的低端, high 指向指针数组的高端, mid=(low+high)/2
3. 测试mid所指的字符串,是否为要找的字符串。
4. 若按字典顺序, m i d所指的字符串大于要查找的串,表示被查字符串在 l o w和m i d之间,
否则,表示被查字符串在 mid和high之间。
5. 修改low式high的值,重新计算 mid,继续寻找。
#include <stdlib.h>
#include <alloc.h>
#include <string.h>
#include <stdio.h>
main()
{
char *binary(); /* 函数声明*/
char *ptr1[5],*temp;
int i,j;
for (i=0;i<5;i++)
{
ptr1[i]=malloc(20); /* 按字典顺序输入字符串 */
gets(ptr1[i]);
}
printf("\n");
printf("original string:\n");
for(i=0;i<5;i++)
printf("%s\n",ptr1[i]);
printf("input search string:\n");
temp=malloc(20);
gets(temp); 输入被查找字符串 */
/*
i=5;
temp=binary(ptr1,temp,i ); /* 调用查找函数*/

if (temp) printf("succesful-----%s\n" ,temp);


else printf("no succesful!\n");
return;
}

char *binary(char *ptr[],char *str,int 定义返回字符指针的函数


n) * /
{ /*折半查找*/
int hig,low,mid;
low=0;
hig=n-1;
while(low<=hig)
{
mid=(low+hig)/2;
if (strcmp(str,ptr[mid])<0)
hig=mid-1;
else if(strcmp(str,ptr[mid])>0)
low=mid+1;
else return(str); /* 查帐成功,返回被查字符串 */
下载
第6章 指 针 117
}
return NULL; /* 查找失败,返回空指针 */
}

运行程序:
RUN ↵
chengdu ↵
chongqin ↵
beijing ↵
tianjin ↵
shanghai ↵
original string:
chengdu
chongqin
beijing
tianjin
shanghai
input search string:
beijing ↵
succesful----- beijing

[例6-26] 在一个已排好序的字符串数组中,插入一个键盘输入的字符串,使其继续保持
有序。
在上述程序查找成功的基础上,我们将该字符串插入到字符数组中。插入的位置可以是
数组头、中间或数组尾。查找的算法采用折半算法,找到插入位置后,将字符串插入。
#include <stdlib.h>
#include <alloc.h>
#include <string.h>
#include <stdio.h>
main()
{
int binary(); /*查找函数声明 */
void insert(); /* 插入函数声明 */
char *temp,*ptr1[6];
int i,j;
for (i=0;i<5;i++)
{
ptr1[i]=malloc(20); /* 为指针分配地址后 */
gets(ptr1[i]); /*输入字符串*/
}
ptr1[5]=malloc(20);
printf("\n");
printf("original string:\n");
for(i=0;i<5;i++) /*输出指针数组各字符串 */
printf("%s\n",ptr1[i]);
printf("input search string:\n");
temp=malloc(20);
gets(temp); /*输入被插字符串 */
118 C语言程序设计
下载
i=binary(ptr1,temp,5 ); /* 寻找插入位置i*/
printf("i=%d\n",i);
insert(ptr1,temp,5,i); /* 在插入位置 i处插入字符串 */
printf("output strings:\n");
for(i=0;i<6;i++) /* 输出指针数组的全部字符串 */
printf("%s\n",ptr1[i]);
return;
}

int binary(char *ptr[],char *str,int n)


{ /*折半查找插入位置 */
int hig,low,mid;
low=0;
hig=n-1;
if (strcmp(str,ptr[0])<0) return 0;
/* 若插入字符串比字符串数组的第 0个小,则插入位置为 0 */
if (strcmp(str,ptr[hig])>0) return n;
/*若插入字符串比字符串数组的最后一个大,则应插入字符串数组的尾部 */
while(low<=hig)
{
mid=(low+hig)/2;
if (strcmp(str,ptr[mid])<0)
hig=mid-1;
else if(strcmp(str,ptr[mid])>0)
low=mid+1;
else return(mid); /* 插入字符串与字符串数组的某个字符串相同 */
}
return low; /* 插入的位置在字符串数组中间 */
}
void insert(char *ptr[],char *str,int n,int i)
{

int j;
for (j=n;j>i;j--) /* 将插入位置之后的字符串后移 */
strcpy(ptr[j],ptr[j-1]);
strcpy(ptr[i],str); 将被插字符串按字典顺序插入字符串数组 */
}

在程序中,字符串数组的 6个指针均分配 p i(整型变量)


存放2 0字节的有效地址。语句 p t r 1 [ 5 ] = m a l l o c &i 5

( 2 0 )保证插入字符串后,也具有安全的存储空 p j(实型变量)
&j 5.3
间,字符串的长度以串中最长的为基准向系
p ch(字符变量)
统申请存储空间,以保证在串的移动中有足
&ch ‘a’
够的存储空间。
p1(双重指针) p2(指针变量) x(整型变量)
6.7 指向指针的指针 &p2 &x 4

图6-13 双重指针
一个指针变量可以指向整型变量、实型
下载
第6章 指 针 119
变量、字符类型变量,当然也可以指向指针类型变量。当这种指针变量用于指向指针类型变
量时,我们称之为指向指针的指针变量,这话可能会感到有些绕口,但你想到一个指针变量
的地址就是指向该变量的指针时;这种双重指针的含义就容易理解了。下面用一些图来描述
这种双重指针,见图 6-13。
在图中,整型变量 i的地址是 & i,将其传递给指针变量 p,则 p指向 i;实型变量 j的地址
是&j,将其传递给指针变量 p,则p指向j; 字符型变量 ch的地址是&ch,将其传递给指针变量 p,
则p指向ch; 整型变量 x的地址是 &x,将其传递给指针变量 p2,则p2指向x,p2是指针变量,同
时,将p 2的地址 & p 2传递给p 1,则p 1指向p 2。这里的 p 1就是我们谈到的指向指针变量的指针
变量,即指针的指针。
指向指针的指针变量定义如下:
类型标识符 **指针变量名
例如: float **ptr;
其含义为定义一个指针变量 p t r,它指向另一个指针变量(该指针变量又指向一个实型变
量)。由于指针运算符“ *”是自右至左结合,所以上述定义相当于:
float *(*ptr);

下面看一下指向指针变量的指针变量怎样正确引用。
[例6-27] 用指向指针的指针变量访问一维和二维数组。
#include <stdio.h>
#include <stdlib.h>
main()
{
是指向指针的指针变量 */
int a[10],b[3][4],*p1,*p2,**p3,i,j; /*p3
for(i=0;i<10;i++)
scanf("%d",&a[i]); /* 一维数组的输入 */
for (i=0;i<3;i++)
for(j=0;j<4;j++)
scanf("%d",&b[i][j]); /* 二维数组输入 */
for (p1=a,p3=&p1,i=0;i<10;i++)
printf("%4d",*(*p3+i)); /* 用指向指针的指针变量输出一维数组 */
printf("\n");
for (p1=a;p1-a<10;p1++) /* 用指向指针的指针变量输出一维数组 */
{
p3=&p1;
printf("%4d",**p3);
}
printf("\n");
for(i=0;i<3;i++) /* 用指向指针的指针变量输出二维数组 */
{
p2=b[i];
p3=&p2;
for (j=0;j<4;j++)
printf("%4d",*(*p3+j));
printf("\n");
}
for(i=0;i<3;i++) /* 用指向指针的指针变量输出二维数组 */
120 C语言程序设计
下载
{
p2=b[i];
for(p2=b[i];p2-b[i]<4;p2++)
{
p3=&p2;
printf("%4d",**p3);
}
printf("\n");
}
}

程序的存储示意如图 6 - 1 4所示,对一维数组 a来说,若把数组的首地址即数组名赋给指针


变量 p 1,p 1就指向数组 a,数组的各元素用 p 1表示为, *(p 1 + i),也可以简化为 * p 1 + i表示。
如果继续作将 p 3 = & p 1,则将p 1的地址传递给指针变量 p 3,* p 3就是p 1。用p 3来表示一维数组
的各元素,只需要将用 p1表示的数组元素 *(p1+i)中的p1换成*p3即可,表示为*(*p3+i)。

图6-14 例6-27程序的存储示意图

同样,对二维数组 b来说,b[i]表示第i行首地址,将其传递给指针变量 p2,使其指向该行。


该行的元素用 p 2表示为 * ( p 2 + i )。若作 p 3 = & p 2,则表示 p 3指向p 2,用p 3表示的二维数组第 i行
元素为:*(*p3+i)。这与程序中的表示完全相同。
运行程序:
RUN ↵
1 2 3 ↵
4 5 6 7 8 9 0
1 3 5 7↵
2 4 6 8↵
5 7 9 2↵
1 2 3 4 5 6 7 8 9 0
1 2 3 4 5 6 7 8 9 0
1 3 5 7
2 4 6 8
5 7 9 2
1 3 5 7
2 4 6 8
5 7 9 2
下载
第6章 指 针 121
[例6-28] 利用指向指针的指针变量对二维字符数组的访问。
#include <stdio.h>
#include <stdlib.h>
main()
{
int i;
static char c[][16]={"c language","fox","computer","home page"};
/*二维字符数组*/
指针数组*/
static char *cp[]={c[0],c[1],c[2],c[3]};/*
static char **cpp; /* 指向字符指针的指针变量 */
cpp=cp; /* 将指针数组的首地址传递给指向字符指针的指针变量 */
for (i=0;i<4;i++) /* 按行输出字符串 */
printf("%s\n",*cpp++);
printf("-----------\n");
for (i=0;i<4;i++) /* 按行输出字符串 */
{
cpp=&cp[i];
printf("%s\n",*cpp);
}
}

运行程序:
RUN ↵
c language
fox
computer
home page
----------
c language
fox
computer
home page

程序中需要注意的是,执行 c p p = c p 是将指针数组的首地址传递给双重指针,所以 *
(cpp+i)表示第i行的首地址,而不是 cpp+i。在程序设计时一定分清。

6.8 main函数的参数

C程序最大的特点就是所有的程序都是用函数来装配的。 m a i n ( )称之为主函数,是所有程
序运行的入口。其余函数分为有参或无参两种,均由 main()函数或其它一般函数调用,若调用
的是有参函数,则参数在调用时传递。
main()
{
...
y1=f1(x1,x2);
...
}
f1(int a,int b)
122 C语言程序设计
下载
{
....
Y2=f2(x3,x4);
....
}
f2( int m,int n)
{
....
.....
}

在前面课程的学习中,对 m a i n ( )函数始终作为主调函数处理,也就是说,允许 m a i n ( )调用


其它函数并传递参数。事实上, main()函数既可以是无参函数,也可以是有参的函数。对于有
参的形式来说,就需要向其传递参数。但是其它任何函数均不能调用 main()函数。当然也同样
无法向main()函数传递,只能由程序之外传递而来。这个具体的问题怎样解决呢?
我们先看一下main( )函数的带参的形式:
main(argc,argv)
int argc,char * argv[];
{
.....
}

从函数参数的形式上看,包含一个整型和一个指针数组。当一个 C的源程序经过编译、链
接后,会生成扩展名为 . E X E的可执行文件,这是可以在操作系统下直接运行的文件,换句话
说,就是由系统来启动运行的。对 main()函数既然不能由其它函数调用和传递参数,就只能由
系统在启动运行时传递参数了。
在操作系统环境下,一条完整的运行命令应包括两部分:命令与相应的参数。其格式为:
命令 参数1 参数2 . . . .参数n↵
此格式也称为命令行。命令行中的命令就是可执行文件的文件名,其后所跟参数需用空
格分隔,并为对命令的进一步补充,也即是传递给 main()函数的参数。
命令行与main()函数的参数存在如下的关系:
设命令行为: program str1 str2 str3 str4 str5↵
其中 p r o g r a m 为文 件 名, 也 就是 一 个由
p r o g r a m . c 经编译、链接后生成的可执行文件
p r o g r a m . e x e,其后各跟 5个参数。对 main( ) 函数
来说,它的参数 a rg c记录了命令行中命令与参数的
个数,共 6个,指针数组的大小由参数 a rg c的值决
定,即为 char *arg v [ 6 ],指针数组的取值情况如图
6-15所示。
图6-15 指针数组的取值情况
数组的各指针分别指向一个字符串。应当引起
注意的是接收到的指针数组的各指针是从命令行的开始接收的,首先接收到的是命令,其后
才是参数。
下面用实例来说明带参数的 main()函数的正确使用。
下载
第6章 指 针 123
[例6-29] 利用图形库函数绘制一个变化的环。它是把一个半径为 R 1的圆周分成 n份,然
后以每个等分点为圆心,以 Rs为半径画 n个圆(关于作图的详细理论本教材第 9章第1节作了专
门介绍,这里只作简单分析)。利用 m a i n ( )函数的带参数形式,我们可以从键盘以命令行的方
式输入R1和Rs及屏幕的背景色。
#include <graphics.h> /*包含图形库函数的头文件 */
#include <math.h>
#define pi 4.1415926
main(argc,argv)
int argc;char *argv[]; /* 定义带参数的main()*/
{
int x,y,r1,rs,color;
double a;
int gdriver=DETECT,gmode;
启动图形工作方式 */
initgraph(&gdriver,&gmode,"..\\bgi ");/*
r1=atoi(argv[1]); /*计算基础圆半径 */
rs=atoi(argv[2]); /* 计算同心圆半径 */
color=atoi(argv[3]); /* 背景色*/
cleardevice(); /*清除图形屏幕 */
setbkcolor(color); /* 设置背景色*/
setcolor(4); /*设置图形显示颜色 */
for(a=0; a<=2*pi;a+=pi/18) /* 绘制同心圆*/
{
x=r1*cos(a)+320;
y=r1*sin(a)+240;
circle(x,y,rs); /* 以圆心坐标为 x、y,半径为rs画圆*/
}
getch(); /* 等待按键继续 */
closegraph(); /*关闭图形工作方式 */
}

若程序名为L 6 - 2 9 . c,经编译、连结生成可执行文件 L 6 - 2 9 . e x e。在操作系统的环境下运行


程序,命令行方式为:
l6-29 40 20 3↵

则命令行与 m a i n ( )函数的参数有如图 6 - 1 6所示的关


系。
图6 - 1 6 中, a rg v [ 0 ] 是程序名, a rg v [ 1 ] 是r 1 的值,
argv[2]是rs的值,argv[3]是屏幕的背景色。 图6-16 例6-29的命令行与main( )函数的
由于指针数组均存放字符串,所需的圆半径及背景 参数间的关系

色彩通过atoi()函数转换为整型。
通过带参数的main()函数,我们可以为自己的程序设置口令,在运行程序的命令行中给出
所需的口令,正确则继续,否则退出。程序图形输出如图 6-17所示。
[例6-30] 将上述程序作修改,在程序的入口处添置密码,若给定密码正确,则显示图
形。
#include <graphics.h>
124 C语言程序设计
下载
#include <math.h>
#define pi 4.1415926
main(argc,argv)
int argc;char *argv[];
{
int x,y,r1,rs,color;
double a;
int gdriver=DETECT,gmode;
if (strcmp(argv[1],"pass")!=0) 设置口令的比较
/* */
{ printf("password error!\n");
exit(0);
}
initgraph(&gdriver,&gmode,"..\\bgi ");
r1=atoi(argv[2]);
rs=atoi(argv[3]);
color=atoi(argv[4]);
cleardevice();
setbkcolor(color);
setcolor(4);
for(a=0; a<=2*pi;a+=pi/18)
{
x=r1*cos(a)+320;
y=r1*sin(a)+240;
circle(x,y,rs);
}
getch();
closegraph();
}

图6-17 例6-29程序输出的图形

在操作系统的环境下运行程序, 命令行
中增加口令“pass”,命令行方式为:
l6-30 pass 20 40 3↵

指针数组的存储字符串如图 6-18所示。
若给定字符串a rg v [ 1 ]的值是pass,则程序
正确运行,否则程序退出。口令正确的情况
下,显示的图形为图 6-17中的一个。
图6-18 例6-30程序中指针数组的存储字符串
下载

第7章 结构体与共用体
前面的课程我们学习了一些简单数据类型(整型、实型、字符型)的定义和应用,还学
习了数组(一维、二维)的定义和应用,这些数据类型的特点是:当定义某一特定数据类型,
就限定该类型变量的存储特性和取值范围。对简单数据类型来说,既可以定义单个的变量,
也可以定义数组。而数组的全部元素都具有相同的数据类型,或者说是相同数据类型的一个
集合。
在日常生活中,我们常会遇到一些需要填写的登记表,如住宿表、成绩表、通讯地址等。
在这些表中,填写的数据是不能用同一种数据类型描述的,在住宿表中我们通常会登记上姓
名、性别、身份证号码等项目;在通讯地址表中我们会写下姓名、邮编、邮箱地址、电话号
码、E - m a i l等项目。这些表中集合了各种数据,无法用前面学过的任一种数据类型完全描述,
因此C引入一种能集中不同数据类型于一体的数据类型 — 结构体类型。结构体类型的变量可
以拥有不同数据类型的成员,是不同数据类型成员的集合。

7.1 结构体类型变量的定义和引用

在上面描述的各种登记表中,让我们仔细观察一下住宿表、成绩表、通讯地址等。
住宿表由下面的项目构成:

姓 名 性 别 职 业 年 龄 身份证号码

(字符串) (字符) (字符串) (整型) (长整型或字符串)

成绩表由下面的项目构成:

班 级 学 号 姓 名 操作系统 数据结构 计算机网络


(字符串) (长整型) (字符串) (实型) (实型) (实型)

通讯地址表由下面的项目构成:

姓 名 工作单位 家庭住址 邮 编 电话号码 E-mail


(字符串) (字符串) (字符串) (长整型) (字符串或长整型) (字符串)

这些登记表用C提供的结构体类型描述如下:
住宿表:
struct accommod
{
char name[20]; /*姓名*/
char sex; /* 性别*/
char job[40]; /* 职业*/
int age; /*年龄*/
long number; /* 身份证号码*/
126 C语言程序设计
下载
};
成绩表:
struct score
{
char grade[20]; /* 班级*/
long number; /* 学号*/
char name[20]; /* 姓名*/
float os; /* 操作系统*/
float datastru; /* 数据结构*/
float compnet; /* 计算机网络*/
};
通讯地址表 :
struct addr
{
char name[20];
char department[30];/* 部门*/
char address[30]; /* 住址*/
long box; /* 邮编*/
long phone; /* 电话号码*/
char email[30]; /*Email*/
};
这一系列对不同登记表的数据结构的描述类型称为结构体类型。由于不同的问题有不同
的数据成员,也就是说有不同描述的结构体类型。我们也可以理解为结构体类型根据所针对
的问题其成员是不同的,可以有任意多的结构体类型描述。
下面给出C对结构体类型的定义形式:
struct 结构体名
{
成员项表列
};
有了结构体类型,我们就可以定义结构体类型变量,以对不同变量的各成员进行引用。

7.1.1 结构体类型变量的定义

结构体类型变量的定义与其它类型的变量的定义是一样的,但由于结构体类型需要针对
问题事先自行定义,所以结构体类型变量的定义形式就增加了灵活性,共计有三种形式,分
别介绍如下:
1) 先定义结构体类型,再定义结构体类型变量:
struct stu /* 定义学生结构体类型 */
{
char name[20]; /* 学生姓名*/
char sex; /* 性别*/
long num; /* 学号*/
float score[3]; /* 三科考试成绩*/
};
/* 定义结构体类型变量 */
struct stu student1,student2;
下载
第7章 结构体与共用体 127
struct stu student3,student4;

用此结构体类型,可以定义更多的该结构体类型变量。
2) 定义结构体类型同时定义结构体类型变量:
struct data
{
int day;
int month;
int year;
} time1,time2;

也可以再定义如下变量:
struct data time3,time4;

用此结构体类型,同样可以定义更多的该结构体类型变量。
3) 直接定义结构体类型变量:
struct
{
char name[20]; /* 学生姓名*/
char sex; /* 性别*/
long num; /*学号*/
float score[3]; /* 三科考试成绩 */
} person1,person2; /* 定义该结构体类型变量 */

该定义方法由于无法记录该结构体类型,所以除直接定义外,不能再定义该结构体类型
变量。

7.1.2 结构体类型变量的引用

学习了怎样定义结构体类型和结构体类型变量,怎样正确地引用该结构体类型变量的成
员呢?C 规定引用的形式为:
<结构体类型变量名 >.<成员名>
若我们定义的结构体类型及变量如下:
struct data
{ time 1
int day; day month year
int month;
timel.day timel.month timel.year
int year;
} time1,time2; time2

则变量time1和time2各成员的引用形式为:time1.day、 day month year


t i m e 1 . m o n t h、t i m e 1 . y e a r及t i m e 2 . d a y、t i m e 2 . m o n t h、
time2.day time2.month time2.year
time2.year,如图7-1所示。
其结构体类型变量的各成员与相应的简单类型变量使 图7-1 结构体类型示例中变量各成员
用方法完全相同。 的引用形式

7.1.3 结构体类型变量的初始化

由于结构体类型变量汇集了各类不同数据类型的成员,所以结构体类型变量的初始化就
128 C语言程序设计
下载
略显复杂。
结构体类型变量的定义和初始化为:
struct stu /* 定义学生结构体类型 */
{
char name[20]; /* 学生姓名*/
char sex; /* 性别*/
long num; /* 学号*/
float score[3]; /* 三科考试成绩*/
};
struct stu student={"liping",'f',970541,98.5,97.4,95};

上述对结构体类型变量的三种定义形式均可在定义时初始化。结构体类型变量完成初始
化后,即各成员的值分别为: s t u d e n t . n a m e = " l i p i n g "、s t u d e n t . s e x = ' f '、s t u d e n t . n u m = 9 7 0 5 4 1、
s t u d e n t . s c o r e [ 0 ] = 9 8 . 5、s t u d e n t . s c o r e [ 1 ] = 9 7 . 4、s t u d e n t . s c o r e [ 2 ] = 9 5。其存储在内存的情况如图
7-2所示。

Liping f 970541 98.5 97.4 95

图7-2 结构体类型变量在内存中的存储

我们也可以通过 C提供的输入输出函数完成对结构体类型变量成员的输入输出。由于结构
体类型变量成员的数据类型通常是不一样的,所以要将结构体类型变量成员以字符串的形式
输入,利用 C的类型转换函数将其转换为所需类型。类型转换的函数是:
int atoi( char *str);转换str所指向的字符串为整型,其函数的返回值为整型。
double atof(char *str);转换str所指向的字符串为实型,其函数的返回值为双精度的实型。
long atol(char *str);转换str所指向的字符串为长整型,其函数的返回值为长整型。
使用上述函数,要包含头文件 "stdlib.h"。
对上述的结构体类型变量成员输入采用的一般形式:
char temp[20];
gets(student.name); /* 输入姓名*/
student.sex=getchar(); /* 输入性别*/
gets(temp); /* 输入学号*/
student.num=atol(temp); /* 转换为长整型*/
for(i=0;i<3;i++) /* 输入三科成绩*/
{
gets(temp);
student.score[i]=atoi(temp);
}

对该结构体类型变量成员的输出也必须采用各成员独立输出,而不能将结构体类型变量
以整体的形式输入输出。
C允许针对具体问题定义各种各样的结构体类型,甚至是嵌套的结构体类型。
struct data
{
int day;
下载
第7章 结构体与共用体 129
int mouth;
int year;
};
struct stu
{
char name[20];
struct data birthday; 出生年月,嵌套的结构体类型
/* */
long num;
} person;

该结构体类型变量成员的引用形式: person.name 、person.birthday.day、person. birthday.


month、person. birthday.year、person.num 。

7.2 结构体数组的定义和引用

单个的结构体类型变量在解决实际问题时作用不大,一般是以结构体类型数组的形式出
现。结构体类型数组的定义形式为:
struct stu /* 定义学生结构体类型*/
{
char name[20]; /* 学生姓名*/
char sex; /* 性别*/
long num; /*学号*/
float score[3]; /* 三科考试成绩 */
};
struct stu stud[20]; 定义结构体类型数组stud
/* ,*/
/* 该数组有20个结构体类型元素 */

其数组元素各成员的引用形式为:
stud[0].name 、stud[0].sex 、stud[0].score[i];
stud[1].name 、stud[1].sex 、stud[1].score[i];
...
...
stud[19].name 、stud[19].sex 、stud[19].score[i];

[例7-1] 设某组有4 个人,填写如下的登记表,除姓名、学号外,还有三科成绩,编程实


现对表格的计算,求解出每个人的三科平均成绩,求出四个学生的单科平均,并按平均成绩
由高分到低分输出。
Number Name English Mathema Physics Average
1 Liping 78 98 76
2 Wangling 66 90 86

3 Jiangbo 89 70 76
4 Yangming 90 100 67

题目要求的问题多,采用模块化编程方式,将问题进行分解如下:
1) 结构体类型数组的输入。
2) 求解各学生的三科平均成绩。
3) 按学生的平均成绩排序。
130 C语言程序设计
下载
4) 按表格要求输出。
5) 求解组内学生单科平均成绩并输出。
6) 定义 main()函数,调用各子程序。
第一步,根据具体情况定义结构体类型。
struct stu
{
char name[20]; /*姓名*/
long number; /*学号*/
float score[4]; /* 数组依此存放English 、Mathema 、Physics ,及Average*/

};

由于该结构体类型会提供给每个子程序使用,是共用的,所以将其定义为外部的结构体
类型,放在程序的最前面。
第二步,定义结构体类型数组的输入模块。
void input(arr,n) /* 输入结构体类型数组 arr 的n个元素*/
struct stu arr[];
int n;
{ int i,j;
char temp[30];
for (i=0;i<n;i++)
{
printf("\ninput name,number,English,mathema,physic\n"); /*打印提示信息*/
gets(arr[i].name); /* 输入姓名*/
gets(temp); 输入学号*/
/*
arr[i].number=atol(temp);
for(j=0;j<3;j++)
{
gets(temp); /*输入三科成绩*/
arr[i].score[j]=atoi(temp);
};
}
}

第三步,求解各学生的三科平均成绩。
在结构体类型数组中第 i个元素 a r r [ i ]的成员 s c o r e的前三个元素为已知,第四个 Av e r a g e需
计算得到。
void aver(arr,n)
struct stu arr[];
int n;
{
int i,j;
for(i=0;i<n;i++) /*n 个学生*/
{
arr[i].score[3]=0;
for(j=0;j<3;j++)
arr[i].score[3]=arr[i].score[3]+arr[i].score[j];求和*
/*/
下载
第7章 结构体与共用体 131
arr[i].score[3]=arr[i].score[3] /3; 平均成绩*
/* /
}
}

第四步,按平均成绩排序,排序算法采用冒泡法。
void order(arr,n)
struct stu arr[];
int n;
{ struct stu temp;
int i,j,x,y;
for(i=0;i<n-1;i++)
for(j=0;j<n-1-i;j++)
if (arr[j].score[3]>arr[j+1].score[3])
{ temp=arr[j]; 结构体类型变量不允许以整体输入或输出,但允许相互赋值 */
/*
进行交换*/
arr[j]=arr[j+1]; /*
arr[j+1]=temp;
}
}

第五步,按表格要求输出。
void output(arr,n) /* 以表格形式输出有 n个元素的结构体类型数组各成员 */
int n;
struct stu arr[];
{int i,j;
printf("********************TABLE********************\n"); 打印表头*
/* /
printf("----------------------------------------------------\n");
/* 输出一条水平线*/
printf("|%10s|%8s|%7s|%7s|%7s|%7s|\n","Name","Number","English","Mathema",
"physics","average");
/* 输出效果为:| Name| Number|English|Mathema|Physics|Average|*/
printf("----------------------------------------------------\n");
for (i=0;i<n;i++)
{
printf("|%10s|%8ld|",arr[i].name,arr[i].number);/* 输出姓名、学号 */
for(j=0;j<4;j++)
printf("%7.2f|",arr[i].score[j]);/* 输出三科成绩及三科的平均 */
printf("\n");
printf("---------------------------------------------------\n");
}
}

第六步,求解组内学生单科平均成绩并输出。在输出表格的最后一行,输出单科平均成
绩及总平均。
void out_row(arr,n) /* 对n个元素的结构体类型数组求单项平均 */
int n;
struct stu arr[];
{
/*定义存放单项平均的一维数组 */
float row[4]={0,0,0,0};
int i,j;
132 C语言程序设计
下载
for(i=0;i<4;i++)
{
for(j=0;j<n;j++)
row[i]=row[i]+arr[j].score[i];/* 计算单项总和*/
row[i]=row[i]/n; 计算单项平均*
/* /
}
printf("|%19c|",' '); 按表格形式输出
/* */
for (i=0;i<4;i++)
printf("%7.2f|",row[i]);
printf("\n------------------------------------------\n");
}
第七步,定义main()函数,列出完整的程序清单。
#include <stdlib.h>
#include <stdio.h>

struct stu
{
char name[20];
long number;
float score[4];
};
main()
{
void input(); /* 函数声明*/
void aver();
void order();
void output();
void out_row();
struct stu stud[4]; /* 定义结构体数组*/
float row[3];
input(stud,4); /* 依此调用自定义函数 */
aver(stud,4);
order(stud,4);
output(stud,4);
out_row(stud,4);
}
/****************************/
void input(arr,n)
struct stu arr[];
int n;
{ int i,j;
char temp[30];
for (i=0;i<n;i++)
{
printf("\nInput Name,Number,English,Mathema,Physic\n");
gets(arr[i].name);
gets(temp);
arr[i].number=atol(temp);
下载
第7章 结构体与共用体 133
for(j=0;j<3;j++)
{
gets(temp);
arr[i].score[j]=atoi(temp);
};
}
}
/***********************/
void aver(arr,n)
struct stu arr[];
int n;
{
int i,j;
for(i=0;i<n;i++)
{
arr[i].score[3]=0;
for(j=0;j<3;j++)
arr[i].score[3]=arr[i].score[3]+arr[i].score[j];
arr[i].score[3]=arr[i].score[3] /3;
}
}
/***********************/
void order(arr,n)
struct stu arr[];
int n;
{ struct stu temp;
int i,j,x,y;
for(i=0;i<n-1;i++)
for(j=0;j<n-1-i;j++)
if (arr[j].score[3]>arr[j+1].score[3])
{ temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
/******************/
void output(arr,n)
int n;
struct stu arr[];
{int i,j;
printf("********************TABLE********************\n");
printf("----------------------------------------------------\n");
printf("|%10s|%8s|%7s|%7s|%7s|%7s|\n","Name","Number","English","mathema",
"physics","average");
printf("----------------------------------------------------\n");
for (i=0;i<n;i++)
{
printf("|%10s|%8ld|",arr[i].name,arr[i].number);
134 C语言程序设计
下载
for(j=0;j<4;j++)
printf("%7.2f|",arr[i].score[j]);
printf("\n");
printf("---------------------------------------------------\n");
}
}
/*************************************/
void out_row(arr,n)
int n;
struct stu arr[];
{
float row[4]={0,0,0,0};
int i,j;
for(i=0;i<4;i++)
{
for(j=0;j<n;j++)
row[i]=row[i]+arr[j].score[i];
row[i]=row[i]/n;
}
printf("|%19c|",' ');
for (i=0;i<4;i++)
printf("%7.2f|",row[i]);
printf("\n------------------------------------------\n");
}
运行程序:
RUN ↵
Input Name,Number,English,Mathema,Physic
Liping ↵
1↵
78 ↵
98 ↵
76 ↵
Input Name,Number,English,Mathema,Physic
Wangling ↵
2↵
66 ↵
90 ↵
86 ↵
Input Name,Number,English,Mathema,Physic
Jiangbo ↵
3↵
89 ↵
70 ↵
76 ↵
Input Name,Number,English,Mathema,Physic
Yangming ↵
4↵
90 ↵
100
下载
第7章 结构体与共用体 135
67 ↵
*******************TABLE*********************
-------------------------------------------------------
| Number| Name|English| Mathema| Physics| Average|
-------------------------------------------------------
| Yangming| 4| 90.00| 100.00| 67.00| 85.67|
-------------------------------------------------------
| Liping| 1| 78.00| 98.00| 76.00| 84.00|
-------------------------------------------------------
| Wangling| 2| 66.00| 90.00| 86.00| 80.72|
-------------------------------------------------------
| Jiangbo| 3| 89.00| 70.00| 76.00| 78.33|
-------------------------------------------------------
| | 80.75| 89.50| 76.25| 82.18|
-------------------------------------------------------

程序中要谨慎处理以数组名作函数的参数。由于数组名作为数组的首地址,在形参和实
参结合时,传递给子程序的就是数组的首地址。形参数组的大小最好不定义,以表示与调用
函数的数组保持一致。在定义的结构体内,成员 s c o r e [ 3 ]用于表示计算的平均成绩,也是我们
用于排序的依据。我们无法用数组元素进行相互比较,而只能用数组元素的成员 s c o r e [ 3 ]进行
比较。在需要交换的时候,用数组元素的整体包括姓名、学号、三科成绩及平均成绩进行交
换。在程序 o r d e r()函数中,比较采用: a r r [ j ] . s c o r e [ 3 ] > a r r [ j + 1 ] . s c o r e [ 3 ],而交换则采用:
arr[j] ←
→ arr[j+1]

7.3 结构体指针的定义和引用

指针变量非常灵活方便,可以指向任一类型的变量,若定义指针变量指向结构体类型变
量,则可以通过指针来引用结构体类型变量。

7.3.1 指向结构体类型变量的使用

首先让我们定义结构体:
struct stu
{
char name[20];
long number;
float score[4];
};

再定义指向结构体类型变量的指针变量:
struct stu *p1, *p2 ;

定义指针变量p1、p2,分别指向结构体类型变量。引用形式为:指针变量→成员;
[例7-2] 对指向结构体类型变量的正确使用。输入一个结构体类型变量的成员,并输出。
#include <stdlib.h> 使用malloc() 需要*/
/*
struct data /* 定义结构体*/
{
136 C语言程序设计
下载
int day,month,year;
};
struct stu /*定义结构体*/
{
char name[20];
long num;
struct data birthday; 嵌套的结构体类型成员*/
/*

} ;
main() /*定义main() 函数*/
{
struct stu *student; 定义结构体类型指针*
/* /
student=malloc(sizeof(struct stu)); 为指针变量分配安全的地址*
/* /
printf("Input name,number,year,month,day:\n");
scanf("%s",student->name); 输入学生姓名、学号、出生年月日
/* */
scanf("%ld",&student->num);
scanf("%d%d%d",&student->birthday.year,&student->birthday.month,
&student->birthday.day);
printf("\nOutput name,number,year,month,day\n" );
/* 打印输出各成员项的值 */
printf("%20s%10ld%10d//%d//%d\n",student->name,student->num,
student->birthday.year,student->birthday.month,
student->birthday.day);
}

程序中使用结构体类型指针引用结构体变量的成员,需要通过 C提供的函数 m a l l o c ( )来为


指针分配安全的地址。函数 s i z e o f ( )返回值是计算给定数据类型所占内存的字节数。指针所指
各成员形式为:
student->name
student->num
student->birthday.year
student->birthday.month
student->birthday.day

运行程序:
RUN ↵
Input name,number,year,month,day:
Wangjian 34 1987 5 23↵
Wangjian 34 1987//5//23

7.3.2 指向结构体类型数组的指针的使用

定义一个结构体类型数组,其数组名是数组的首地址,这一点前面的课程介绍得很清楚。
定义结构体类型的指针,既可以指向数组的元素,也可以指向数组,在使用时要加以区分。
[例7-3] 在例7 - 2中定义了结构体类型,根据此类型再定义结构体数组及指向结构体类型
的指针。
struct data
下载
第7章 结构体与共用体 137
{
int day,month,year;
};
struct stu /*定义结构体*/
{
char name[20];
long num;
struct data birthday; 嵌套的结构体类型成员*/
/*

} ;
struct stu student[4],*p; 定义结构体数组及指向结构体类型的指针
/* */

作 p=student,此时指针 p就指向了结构体数组 student。


p是指向一维结构体数组的指针,对数组元素的引用可采用三种方法。
1) 地址法
student+i和p+i均表示数组第 i个元素的地址,数组元素各成员的引用形式为:
(s t u d e n t + i)- > n a m e、( s t u d e n t + i ) - > n u m和( p + i ) - > n a m e、(p + i)- > n u m等。s t u d e n t + i和p + i
与&student[i]意义相同。
2) 指针法
若p指向数组的某一个元素,则 p++就指向其后续元素。
3) 指针的数组表示法
若p = s t u d e n t,我们说指针 p指向数组 s t u d e n t,p [ i ]表示数组的第 i个元素,其效果与
student[i]等同。对数组成员的引用描述为 :p[i].name、p[i].num等。
[例7-4] 指向结构体数组的指针变量的使用。
struct data /*定义结构体类型 */
{
int day,month,year;
};
struct stu /*定义结构体类型 */
{
char name[20];
long num;
struct data birthday;
} ;
main()
{ int i;
struct stu *p,student[4]={{"liying",1,1978,5,23},{"wangping",2,1979,3,14},
{"libo",3,1980,5,6},{"xuyan",4,1980,4,21}};
/*定义结构体数组并初始化 */
p=student; /*将数组的首地址赋值给指针 p,p 指向了一维数组student*/
printf("\n1----Output name,number,year,month,day\n" );
for(i=0;i<4;i++) /*采用指针法输出数组元素的各成员 */
printf("%20s%10ld%10d//%d//%d\n",(p+i)->name,(p+i)->num,
(p+i)->birthday.year,(p+i)->birthday.month,
(p+i)->birthday.day);
138 C语言程序设计
下载
printf("\n2----Output name,number,year,month,day\n" );
for(i=0;i<4;i++,p++) /* 采用指针法输出数组元素的各成员 */
printf("%20s%10ld%10d//%d//%d\n",p->name,p->num,
p->birthday.year,p->birthday.month,
p->birthday.day);

printf("\n3-----Output name,number,year,month,day\n" );
for(i=0;i<4;i++) /* 采用地址法输出数组元素的各成员 */
printf("%20s%10ld%10d//%d//%d\n",(student+i)->name,(student+i)->num,
(student+i)->birthday.year,(student+i)->birthday.month,
(student+i)->birthday.day);
p=student;
printf("\n4-----Output name,number,year,month,day\n" );
for(i=0;i<4;i++) /* 采用指针的数组描述法输出数组元素的各成员 */
printf("%20s%10ld%10d//%d//%d\n",p[i].name,p[i].num,
p[i].birthday.year,p[i].birthday.month,
p[i].birthday.day);
}

运行程序:
RUN ↵
1----Output name,number,year,month,day
liying 1 1978//5//23
wangping 2 1979//3//14
libo 3 1980//5//6
xuyan 4 1980//4//21

2----Output name,number,year,month,day
liying 1 1978//5//23
wangping 2 1979//3//14
libo 3 1980//5//6
xuyan 4 1980//4//21

3----Output name,number,year,month,day
liying 1 1978//5//23
wangping 2 1979//3//14
libo 3 1980//5//6
xuyan 4 1980//4//21

4----Output name,number,year,month,day
liying 1 1978//5//23
wangping 2 1979//3//14
libo 3 1980//5//6
xuyan 4 1980//4//21
对二维或多维数组的指针,有兴趣的同学可课后讨论,总结出来。

7.4 链表的建立、插入和删除
数组作为存放同类数据的集合,给我们在程序设计时带来很多的方便,增加了灵活性。
下载
第7章 结构体与共用体 139
但数组也同样存在一些弊病。如数组的大小在定义时要事先规定,不能在程序中进行调整,
这样一来,在程序设计中针对不同问题有时需要 3 0个大小的数组,有时需要 5 0个数组的大小,
难于统一。我们只能够根据可能的最大需求来定义数组,常常会造成一定存储空间的浪费。
我们希望构造动态的数组,随时可以调整数组的大小,以满足不同问题的需要。链表就
是我们需要的动态数组。它是在程序的执行过程中根据需要有数据存储就向系统要求申请存
储空间,决不构成对存储区的浪费。
链表是一种复杂的数据结构,其数据之间的相互关系使链表分成三种:单链表、循环链
表、双向链表,下面将逐一介绍。

7.4.1 单链表

图7-3是单链表的结构。

1200 2000 1800 2400


head

1200 12 34 56 78
2000 1800 2400 NULL

图7-3 单链表

单链表有一个头节点 h e a d,指向链表在内存的首地址。链表中的每一个节点的数据类型
为结构体类型,节点有两个成员:整型成员(实际需要保存的数据)和指向下一个结构体类
型节点的指针即下一个节点的地址(事实上,此单链表是用于存放整型数据的动态数组)。链
表按此结构对各节点的访问需从链表的头找起,后续节点的地址由当前节点给出。无论在表
中访问那一个节点,都需要从链表的头开始,顺序向后查找。链表的尾节点由于无后续节点,
其指针域为空,写作为 NULL。
图7 - 3还给出这样一层含义,链表中的各节点在内存的存储地址不是连续的,其各节点的
地址是在需要时向系统申请分配的,系统根据内存的当前情况,既可以连续分配地址,也可
以跳跃式分配地址。
看一下链表节点的数据结构定义:
struct node
{
int num;
struct node *p;
};

在链表节点的定义中,除一个整型的成员外,成员 p是指向与节点类型完全相同的指针。
在链表节点的数据结构中,非常特殊的一点就是结构体内的指针域的数据类型使用了未定义
成功的数据类型。这是在 C中唯一规定可以先使用后定义的数据结构。
• 单链表的创建过程有以下几步:
1) 定义链表的数据结构。
2) 创建一个空表。
3) 利用malloc()函数向系统申请分配一个节点。
140 C语言程序设计
下载
4 ) 将新节点的指针成员赋值为空。若是空表,将新节点连接到表头;若是非空表,将新
节点接到表尾。
5) 判断一下是否有后续节点要接入链表,若有转到3),否则结束。
• 单链表的输出过程有以下几步
1) 找到表头。
2) 若是非空表,输出节点的值成员,是空表则退出。
3) 跟踪链表的增长,即找到下一个节点的地址。
4) 转到 2)。
[例7-5] 创建一个存放正整数(输入 -999做结束标志)的单链表,并打印输出。

#include <stdlib.h> 包含malloc() 的头文件*/


/*
#include <stdio.h>
struct node /*链表节点的结构*/
{
int num;
struct node *next;
};
main()
{
struct node *creat(); /* 函数声明*/
void print();
struct node *head; /* 定义头指针*/
head=NULL; /* 建一个空表*/
head=creat(head); /* 创建单链表*/
print(head); /* 打印单链表*/
}
/******************************************/
struct node *creat(struct node *head) 函数返回的是与节点相同类型的指针
/* */
{
struct node *p1,*p2;
p1=p2=(struct node*) malloc(sizeof(struct node));申请新节点* /* /
scanf("%d",&p1->num); /* 输入节点的值*/
p1->next=NULL; /* 将新节点的指针置为空 */
while(p1->num>0) /* 输入节点的数值大于 0*/
{
if (head==NULL) head=p1; /* 空表,接入表头*/
else p2->next=p1; /* 非空表,接到表尾 */
p2=p1;
p1=(struct node *)malloc(sizeof(struct node)); 申请下一个新节点*
/* /
scanf("%d",&p1->num); /* 输入节点的值*/
}
return head; /*返回链表的头指针 */
}
/*******************************************/
void print(struct node *head) 输出以h /* ead 为头的链表各节点的值*/
{
下载
第7章 结构体与共用体 141
struct node *temp;
temp=head; /* 取得链表的头指针 */
while (temp!=NULL) /* 只要是非空表 */
{
printf("%6d",temp->num); /*输出链表节点的值 */
temp=temp->next; /* 跟踪链表增长 */
}
}

在链表的创建过程中,链表的头指针是非常重要的参数。因为对链表的输出和查找都要
从链表的头开始,所以链表创建成功后,要返回一个链表头节点的地址,即头指针。
运行程序:RUN ↵
1 2 3 4 5 6 7 ↵
-999
1 2 3 4 5 6 7
链表的创建过程用图示如下:
第一步,创建空表: head NULL
第二步,申请新节点:
p1
1 NULL
p2
第三步,若是空表,将新节点接到表头:
head
P1 1 NULL

P2
若是非空表,head ...
NULL
p2 p1
P2->next=p1。
第四步,p2=p1: head NULL

第五步,申请新节点: p1 p2
NULL

若数值为负,则结束;否则转到第三步。

7.4.2 单链表的插入与删除

在链表这种特殊的数据结构中,链表的长短需要根据具体情况来设定,当需要保存数据
时向系统申请存储空间,并将数据接入链表中。对链表而言,表中的数据可以依此接到表尾
或连结到表头,也可以视情况插入表中;对不再需要的数据,将其从表中删除并释放其所占
空间,但不能破坏链表的结构。这就是下面将介绍的链表的插入与删除。
1. 链表的删除
在链表中删除一个节点,用图 7-4描述如下:
[例7-6] 创建一个学生学号及姓名的单链表,即节点包括学生学号、姓名及指向下一个
节点的指针,链表按学生的学号排列。再从键盘输入某一学生姓名,将其从链表中删除。
首先定义链表的结构:
struct
142 C语言程序设计
下载
{
int num;/* 学生学号*/
char str[20]; /* 姓名*/
struct node *next;
};
删除表中节点s'p->next=s->next
p s
head
NULL

删除表头节点head=head->next
head
NULL

删除表尾节点s,p->next=NULL
p s
head
NULL

NULL

图7-4 链表中节点的删除

从图7 - 4中看到,从链表中删除一个节点有三种情况,即删除链表头节点、删除链表的中
间节点、删除链表的尾节点。题目给出的是学生姓名,则应在链表中从头到尾依此查找各节
点,并与各节点的学生姓名比较,若相同,则查找成功,否则,找不到节点。由于删除的节
点可能在链表的头,会对链表的头指针造成丢失,所以定义删除节点的函数的返回值定义为
返回结构体类型的指针。
struct node *delet(head,pstr)/* 以head 为头指针,删除 pstr 所在节点*/
struct node *head;
char *pstr;
{
struct node *temp,*p;
temp=head; /* 链表的头指针*/
if (head==NULL) /* 链表为空*/
printf("\nList is null!\n");
else /*非空表*/
{
temp=head;
while (strcmp(temp->str,pstr)!=0&&temp->next!=NULL)
/* 若节点的字符串与输入字符串不同,并且未到链表尾 */
{
p=temp;
temp=temp->next; /* 跟踪链表的增长,即指针后移 */
}
if(strcmp(temp->str,pstr)==0 ) /* 找到字符串*/
{
if(temp==head) { /* 表头节点*/
printf("delete string :%s\n",temp->str);
head=head->next;
free(temp); /* 释放被删节点*/
下载
第7章 结构体与共用体 143
}
else
{
p->next=temp->next; 表中节点*/
/*
printf("delete string :%s\n",temp->str);
free(temp);
}
}
没找到要删除的字符串*/
else printf("\nno find string!\n");/*
}
return(head); /* 返回表头指针 */
}

2. 链表的插入
首先定义链表的结构:
struct
{
int num; /*学生学号*/
char str[20]; /*姓名*/
struct node *next;
};

在建立的单链表中,插入节点有三种情况,如图 7-5所示。

在表头插入节点p1,head=p1

head
NULL
p1

在表中插入节点p1,p3->next=p1;p1->next=p2;
p3 p2
head
NULL

p1

在表尾插入节点p1,p2->next=p1;p1->next=NULL
p2
head
NULL
p1

图7-5 单链表中插入节点

插入的节点可以在表头、表中或表尾。假定我们按照以学号为顺序建立链表,则插入的
节点依次与表中节点相比较,找到插入位置。由于插入的节点可能在链表的头,会对链表的
头指针造成修改,所以定义插入节点的函数的返回值定义为返回结构体类型的指针。节点的
插入函数如下:
struct node *insert(head,pstr,n) /* 插入学号为n、姓名为pstr 的节点*/
struct node *head; /* 链表的头指针 */
char *pstr;
144 C语言程序设计
下载
int n;
{
struct node *p1,*p2,*p3;
分配一个新节点 */
p1=(struct node*)malloc(sizeof(struct node));/*

strcpy(p1->str,pstr); /* 写入节点的姓名字串 */
p1->num=n; /* 学号*/
p2=head;
if (head==NULL) /* 空表*/
{
head=p1; p1->next=NULL;/* 新节点插入表头*/
}
else
{ /*非空表*/
while(n>p2->num&&p2->next!=NULL)
/*输入的学号小于节点的学号,并且未到表尾 */
{
p3=p2;
p2=p2->next; /* 跟踪链表增长*/
}
if (n<=p2->num) /* 找到插入位置*/

if (head==p2) /* 插入位置在表头*/
{
head=p1;
p1->next=p2;
}
else
{ /*插入位置在表中 */
p3->next=p1;
p1->next=p2;
}
else
{ /*插入位置在表尾 */
p2->next=p1;
p1->next=NULL;
}
}
return(head); /* 返回链表的头指针 */
}

3. 实例[例7-7]
创建包含学号、姓名节点的单链表。其节点数任意个,表以学号为序,低学号的在前,
高学号的在后,以输入姓名为空作结束。在此链表中,要求删除一个给定姓名的节点,并插
入一个给定学号和姓名的节点。
# include "stdlib.h"
# include "malloc. h"
下载
第7章 结构体与共用体 145
struct node /*节点的数据结构 */
{
int num;
char str[20];
struct node *next;
};
/****************************/
main( )
{
/* 函数声明*/
struct node *creat();
struct node *insert();
struct node *delet();
void print( );
struct node *head;
char str[20];
int n;
head=NULL; /*做空表*/
head=creat (head); /* 调用函数创建以 head 为头的链表*/
print(head) ;/* 调用函数输出节点 */
printf("\n input inserted num,name:\n");
gets(str); /*输入学号*/
n=atoi (str);
gets(str); /*输入姓名*/
head=insert (head, str, n); 将节点插入链表
/* */
print (head); /* 调用函数输出节点 */
printf("\n input deleted name:\n");
gets(str); /*输入被删姓名*/
head=delet(head,str); 调用函数删除节点 */
/*
print (head); /*调用函数输出节点 */
return;
}
/**********************/
/*** 创建链表************/
struct node *creat(struct node *head)
{
char temp[30];
struct node *pl,*p2;
pl=p2=(struct node*) malloc(sizeof(struct node));
printf ("input num, name: \n");
printf("exit:double times Enter!\n");
gets(temp);
gets (p1->str);
pl->num=atoi (temp);
pl->next=NULL;
while (strlen (pl->str)>0
{

if (head==NULL) head=pl
else p2->next=p1;
146 C语言程序设计
下载
P2=pl ;
pl=(struct node *)malloc(sizeof(struct node));
printf ("input num, name: \n");
printf("exit:double times Enter!\n");
gets(temp);
gets(pl ->str);
p1->num=atoi (temp);
P1->next=NULL;
}
return head;
}
/********************/
/********** 插入节点**********/
struct node *insert (head, pstr,n);
struct node *head;
char *pstr;
int n;
{
struct node *pl,*p2,*p3;
p1=(struct node*)malloc(sizeof(struct node));
strcpy (p1->str, pstr);
p1->num=n;
p2=head;
if(head==NULL)
{
head=pl;pl->next=NULL;
}
else
{
while (n>p2->num&&p2->next!=NULL)
{
p3=P2
p2=p2->next;
}
if (n<=p2->num)
if (head==p2)
{
head=pl;
pl->next=p2;
}
else
{
p3->next=pl;
pl->next=p2;
}
else
{
p2->next=pl;
pl->next=NULL;
下载
第7章 结构体与共用体 147
}
}
return(head);
}
/*************************/
/***** 删除节点*************/
struct node *delet (head, pstr)
struct node *head;
char *pstr;
{
struct node *temp,*p;
temp=head;
if (head==NULL)
printf("\nList is null!\n");
else
{
temp=head;
while (strcmp(temp->str,pstr)!=O&&temp->next!=NULL)
{
p=temp;
temp=temp->next,
}
if(strcmp(temp->str,pstr)==0)
{
if (temp== head)
{
head=head->next;
free(temp);
}
else
{
p->next =temp->next;
printf("delete string :%s\n",temp->str);
free(temp);
}
}
else printf("\nno find string!\n");
}
return(head);
}
/**********************************/
/********** 链表各节点的输出 **********/
void print (struct node *head)
{
struct node *temp;
temp=head;
printf("\n output strings:\n");
while (temp!=NULL)
{
printf("\n%d----%s\n",temp->num ,temp->str) ;
148 C语言程序设计
下载
temp=temp->next ;
}
return;
}

运行程序:
RUN ↵
input num,name:
exit:double times Enter!
1↵
Huangping ↵
input num,name:
exit:double times Enter!
3↵
Lixiaobo ↵
input num,name:
exit:double times Enter!
4↵
Yangjinhua ↵
input num,name:
exit:double times Enter!
7↵
xuehong ↵
input num,name:
exit:double times Enter!


output strings:
1------- Huangping
3--------Lixiaobo
4--------Yangjinhua
7--------xuehong
input inserted num,name:
5↵
Liling ↵
output strings:
1------- Huangping
3--------Lixiaobo
4--------Yangjinhua
5--------Liling
7--------xuehong
input deleted name:
Lixiaobo ↵
delete string : Lixiaobo
1------- Huangping
4--------Yangjinhua
5--------Liling
7--------xuehong
下载
第7章 结构体与共用体 149
7.5 共用体

所谓共用体类型是指将不同的数据项组织成一个整体,它们在内存中占用同一段存储单
元。其定义形式为:
union 共用体名
{成员表列};

7.5.1 共用体的定义

union data
{
int a;
float b;
double c;
char d;
} obj;
该形式定义了一个共用体数据类型 union data ,定义了共用体数据类型变量 o b j。共用体
数据类型与结构体在形式上非常相似,但其表示的含义及存储是完全不同的。先让我们看一
个小例子。
[例7-8]
union data /*共用体*/
{
int a;
float b;
double c;
char d;
}mm;
struct stud /*结构体*/
{
int a;
float b;
double c;
char d;
};
main()
{
struct stud student
printf("%d,%d",sizeof(struct stud),sizeof(union data));
}

运行程序输出:
RUM ↵
15 ,8

程序的输出说明结构体类型所占的内存空间为其各成员所占存储空间之和。而形同结构
体的共用体类型实际占用存储空间为其最长的成员所占的存储空间。详细说明如图 7-6所示。
150 C语言程序设计
下载
结构体
共用体
a 2
d
4 a
b
15 b 8
c 8 c

d 1

图7-6 共用体类型与结构体类型占用存储空间的比较

对共用体的成员的引用与结构体成员的引用相同。但由于共用体各成员共用同一段内存
空间,使用时,根据需要使用其中的某一个成员。从图中特别说明了共用体的特点,方便程
序设计人员在同一内存区对不同数据类型的交替使用,增加灵活性,节省内存。

7.5.2 共用体变量的引用

可以引用共用体变量的成员,其用法与结构体完全相同。若定义共用体类型为:
union data /*共用体*/
{
int a;
float b;
double c;
char d;
}mm;

其成员引用为: mm.a, mm.b, mm.c, mm.d


但是要注意的是,不能同时引用四个成员,在某一时刻,只能使用其中之一的成员。
[例7-9] 对共用体变量的使用。
main()
{
union data
{
int a;
float b;
double c;
char d;
}mm;
mm.a=6;
printf("%d\n",mm.a);
mm.c=67.2;
printf("%5.1lf\n",mm.c);
mm.d='W';
mm.b=34.2;
printf("%5.1f,%c\n",mm.b,mm.d);
}

运行程序输出为:
6
下载
第7章 结构体与共用体 151
67 .2
34.2,=

程序最后一行的输出是我们无法预料的。其原因是连续做 m m . d = ' W ';m m . b = 3 4 . 2;两个


连续的赋值语句最终使共用体变量的成员 m m . b所占四字节被写入 34 .2,而写入的字符被覆盖
了,输出的字符变成了符号“ =”。事实上,字符的输出是无法得知的,由写入内存的数据决
定。
例子虽然很简单,但却说明了共用体变量的正确用法。
[例7-10] 通过共用体成员显示其在内存的存储情况。
定义一个名为time的结构体,再定义共用体 dig:
struct time
{
int year; /*年*/
int month;/* 月*/
int day; /* 日*/
};
union dig
{
嵌套的结构体类型 */
struct time data; /*
char byte[6];
};

假定共用体的成员在内存的存储是从地址 1 0 0 0单元开始存放,整个共用体类型需占存储
空间6个字节,即共用体 d i g的成员 d a t a与b y t e共用这 6个字节的存储空间,存储空间分配示意
如图7-7所示。
存储器
1000
byte[0]
data.year
1001 byte[1] 共

1002 byte[2]
data.month 体
1003 byte[3] 类
byte[4] 型
1004 data.day
1005 byte[5]

图7-7 共用体dig成员在存储空间中的分配示意图

由于共用体成员 d a t a包含三个整型的结构体成员,各占 2个字节。由图 7 - 7所示可见,


d a t a . y e a r是由 2个字节组成,用 b y t e字符数组表示为 b y t e [ 0 ]和byte[1] 。b y t e [ 1 ]是高字节,
byte[0]是低字节。下面用程序实现共用体在内存中的存储。
struct time
{
int year; /*年*/
int month; /* 月*/
int day; /*日*/
};
152 C语言程序设计
下载
union dig
{
嵌套的结构体类型 */
struct time data; /*
char byte[6];
};

main()
{
union dig unit;
int i;
printf("enter year:\n");
scanf("%d",&unit.data.year); /* 输入年*/
printf("enter month:\n");
scanf("%d",&unit.data.month); /* 输入月*/
printf("enter day:\n");
scanf("%d",&unit.data.day); /* 输入日*/
printf("year=%d month=%d day=%d\n", unit.data.year,unit. data. month, un
data.day); /* 打印输出*/
for(i=0;i<6;i++)
printf("%d,",unit.byte[i]); /* 按字节以十进制输出 */
printf("\n");
}

运行程序:
RUN ↵
enter year:
1976 ↵
enter month:
4↵
enter day:
23 ↵
year=1976 month=4 day=23
184,7,4,0,23,0

从程序的输出结果来看, 1 9 7 6占两个字节,由第 0、1字节构成,即 7×2 5 6 + 1 8 4 = 1 9 7 6。4


同样占两个字节,由第 2、3字节构成, 0×256+4=4,23由第4、5字节构成, 23=0×256+23。
下载

第8章 输入、输出和文件系统
在前面的程序设计中,我们介绍了输入和输出,即从标准输入设备 — 键盘输入,由标准
输出设备 — 显示器或打印机输出。不仅如此,我们也常把磁盘作为信息载体,用于保存中
间结果或最终数据。在使用一些字处理工具时,会利用打开一个文件来将磁盘的信息输入到
内存,通过关闭一个文件来实现将内存数据输出到磁盘。这时的输入和输出是针对文件系统,
故文件系统也是输入和输出的对象,谈到输入和输出,自然也离不开文件系统。
文件可以从不同的角度来分类:
1) 按文件所依附的介质来分:有卡片文件、纸带文件、磁带文件、磁盘文件等。
2) 按文件内容来分:有源文件、目标文件、数据文件等。
3) 按文件中数据组织形式分:有字符文件和二进制文件。
字符文件通常又称为 ASCII码文件或正文文件,按字符存储,具有可读性;而二进制文件
是以二进制存储,不具备可读性,但从存储空间的利用来看,实型数无论位数大小均占 4位,
字符确需按位数来存放,这样的话,二进制文件相对就节省了空间。
目前C语言使用的文件系统分为缓冲文件系统(标准 I/O)和非缓冲文件系统(系统 I/O)。

8.1 缓冲文件系统

缓冲文件系统的特点是:在内存开辟一个“缓冲区”,为程序中的每一个文件使用,当执
行读文件的操作时,从磁盘文件将数据先读入内存“缓冲区”,装满后再从内存“缓冲区”依
此读入接收的变量。执行写文件的操作时,先将数据写入内存“缓冲区”,待内存“缓冲区”
装满后再写入文件。由此可以看出,内存“缓冲区”的大小,影响着实际操作外存的次数,
内存“缓冲区”越大,则操作外存的次数就少,执行速度就快、效率高。一般来说,文件
“缓冲区”的大小随机器而定。

8.1.1 文件的打开与关闭

任何关于文件的操作都要先打开文件,再对文件进行读写,操作完毕后,要关闭文件。
1. 文件类型指针
人们在操作文件时,通常都关心文件的属性,如文件的名字、文件的性质、文件的当前
状态等。对缓冲文件系统来说,上述特性都是要仔细考虑的。 ANSI C 为每个被使用的文件在
内存开辟一块用于存放上述信息的小区,利用一个结构体类型的变量存放。该变量的结构体
类型由系统取名为 FILE,在头文件 stdio.h中定义如下:
typedef struct{
int_fd; /* 文件号*/
int_cleft; /* 缓冲区中的剩余字符*/
int_mode; /* 文件的操作模式 */
char*_next; /*下一个字符的位置 */
char *_buff; /* 文件缓冲区的位置 */
154 C语言程序设计
下载
} FILE;

在操作文件以前,应先定义文件变量指针:
FILE *fp1,fp2;

按照上面的定义,fp1和fp2均为指向结构体类型的指针变量,分别指向一个可操作的文件,
换句话说,一个文件有一个文件变量指针,今后对文件的访问,会转化为针对文件变量指针
的操作。
2. 文件的打开
ANSI C 提供了打开文件的函数:
FILE *fopen(char *fname,char *mode)

函数原型在 stdio.h文件中,fopen()打开一个fname指向的外部文件,返回与它相连接的流。
f n a m e是字符串,应是一个合法的文件名,还可以指明文件路经。对文件的操作模式由 m o d e
决定,mode也是字符串,由表 8-1给出mode的取值表。
表8-1 mode的取值表

Mode 含 义

r 打开一个文本文件只读
w 打开一个文本文件只写
a 打开一个文本文件在尾部追加
rb 打开一个只读的二进制文件
wb 打开一个只写的二进制文件
ab 对二进制文件追加
r+ 打开一个可读 /写的文本文件
w+ 创建一个新的可读 /写的文本文件
a+ 打开一个可读 /写的文本文件
rb+ 打开一个可读 /写的二进制文件
wb+ 创建一个新的可读 /写的二进制文件
ab 打开一个可读 /写的二进制文件

如表8 - 1所示,文件的操作方式有文本文件和二进制文件两种,打开文件的正确方法如下
例所示:
#include <stdio.h>
FILE *fp;

If ((fp=fopen("test.txt","w"))==NULL)
{ /*创建一个只写的新文本文件*/
printf("cannot open file \n");
exit(0);
}

这种方法能发现打开文件时的错误。在开始写文件之前检查诸如文件是否有写保护,磁
盘是否已写满等,因为函数会返回一个空指针 NULL,NULL值在stdio.h中定义为0。事实上打
开文件是要向编译系统说明三个信息:①需要访问的外部文件是哪一个。②打开文件后要执
行读或写即选择操作方式。③确定哪一个文件指针指向该文件。对打开文件所选择的操作方
式来说,一经说明不能改变,除非关闭文件后重新打开。是只读就不能对其写操作,对已存
下载
第8章 输入、输出和文件系统 155
文件如以新文件方式打开,则信息必丢失。
3. 文件的关闭
ANSI C 提供了关闭文件的函数:
int fclose(FILE *stream)

f c l o s e ( )函数关闭与 s t r e a m相连接的文件,并把它的缓冲区内容全部写出。在 f c l o s e ( )函数


调用以后,流stream与此文件无关,同时原自动分配的缓冲区也失去定位。
fclose()函数关闭文件操作成功后,函数返回 0;失败则返回非零值。
[例8-1] 打开和关闭一个可读可写的二进制文件:
#include <stdio.h>

main()
{
FILE *fp;
If ((fp=fopen("test.dat","rb"))==NULL)
{
printf("cannot open file\n");
exit(0);
}
/*写入对文件执行读写的代码
…… */
if (fclose(fp)) printf("file close error!\n");
}

8.1.2 文件的读写

当文件按指定的工作方式打开以后,就可以执行对文件的读和写。下面按文件的性质分
类进行操作。针对文本文件和二进制文件的不同性质,对文本文件来说,可按字符读写或按
字符串读写;对二进制文件来说,可进行成块的读写或格式化的读写。
1. 读写字符
C提供f g e t c和f p u t c函数对文本文件进行字符的读写,其函数的原型存于 s t d i o . h头文件中,
格式为:
int fgetc(FILE *stream)

f g e t c ( )函数从输入流的当前位置返回一个字符,并将文件指针指示器移到下一个字符处,
如果已到文件尾,函数返回 EOF,此时表示本次操作结束,若读写文件完成,则应关闭文件。
int fputc(int ch,FILE *stream)

f p u t c()函数完成将字符 c h的值写入所指定的流文件的当前位置处,并将文件指针后移
一位。fputc ()函数的返回值是所写入字符的值,出错时返回 EOF。
[例8-2] 将存放于磁盘的指定文本文件按读写字符方式逐个地从文件读出,然后再将其
显示到屏幕上。采用带参数的 main(),指定的磁盘文件名由命令行方式通过键盘给定。
#include <stdio.h>
main( argc,argv)
int argc;
156 C语言程序设计
下载
char *argv[];
{
char ch;
FILE *fp;
int i;
if((fp=fopen(argv[1],"r"))==NULL) /* 打开一个由argv[1] 所指的文件*/
{
printf("not open");
exit(0);
}
while ((ch=fgetc(fp))!=EOF) /* 从文件读一字符,显示到屏幕 */
putchar(ch);
fclose(fp);
}

程序是一带参数的 m a i n ( )函数,要求以命令行方式运行,其参数 a rg c是用于记录输入参数


的个数, argv是指针数组,用于存放输入参数的字符串,串的个数由 a rg c描述。假设我们指定
读取的文件名为 L 8 - 2 . c,并且列表文件内容就是源程序。经过编译和连接生成可执行的文件
L8-2.exe。运行程序 l8-2.exe,输入的命令行方式为:
c:\tc> l8-2 L8-2.c ↵

上述程序以命令行方式运行,其输入参数字符串有两个,即 a rg v [ 0 ] = " c : \ t c > l 8 - 2 "、


argv[1]=" L8-2.c ",a rg c = 2。故打开的文件是 L8-2.c 。程序中对 fgetc()函数的返回值不断进行
测试,若读到文件尾部或读文件出错,都将返回 C的整型常量 EOF,其值为非零有效整数。程
序的运行输出为源程序本身:
c:\tc> l8-2 L8-2.c ↵

#include <stdio.h>
main( argc,argv)
int argc;
char *argv[];
{
char ch;
FILE *fp;
int i;
if((fp=fopen(argv[1],"r"))==NULL) /* 打开一个由argv[1] 所指的文件*/
{
printf("not open");
exit(0);
}
while ((ch=fgetc(fp))!=EOF) /* 从文件读一字符,显示到屏幕 */
putchar(ch);
fclose(fp);
}

[例8-3] 从键盘输入字符,存到磁盘文件 test.txt中:


#include <stdio.h>
main()
下载
第8章 输入、输出和文件系统 157
{
FILE fp; /* 定义文件变量指针 */
char ch;
if((fp=fopen("test.txt","w"))==NULL) /* 以只写方式打开文件* /
{
printf("cannot open file!\n");
exit(0);
}
while ((ch=fgetchar())!='\n') /*只要输入字符非回车符*/
fputc(ch,fp) /*写入文件一个字符 */
fclose(fp);
}

程序通过从键盘输入一以回车结束的字符串,写入指定的流文件 test.txt,文件以文本只写
方式打开,所以流文件具有可读性,能支持各种字符处理工具访问。简单地说,我们可以通
过DOS提供的type命令来列表显示文件内容。
运行程序:
RUN ↵
I love china!↵
在DOS操作系统环境下,利用 type 命令显示test.txt文件如下:
c:\tc> type test.txt↵
I love china!
2. 读写字符串
C提供读写字符串的函数原型在 stdio.h头文件中,其函数形式为:
Char *fgets(char *str,int num,FILE *stream)

fgets() 函数从流文件stream中读取至多 num-1个字符,并把它们放入str指向的字符数组中。


读取字符直到遇见回车符或 EOF(文件结束符)为止,或读入了所限定的字符数。
int fputs(char *str,FILE *stream)

fputs()函数将str指向的字符串写入流文件。操作成功时,函数返回 0值,失败返回非零值。
[例8-4] 向磁盘写入字符串,并写入文本文件 test.txt:
#include <stdio.h>
#include <string.h>
main()
{
FILE *fp;
char str[128];
/* 打开只写的文本文件*/
if ((fp=fopen("test.txt","w"))==NULL)
{
printf("cannot open file!");
exit(0);
}
while((strlen(gets(str)))!=0)
{ /*若串长度为零,则结束 */
fputs(str,fp); /*写入串*/
fputs("\n",fp); /* 写入回车符*/
158 C语言程序设计
下载
}
fclose(fp); /*关文件*/
}

运行该程序,从键盘输入长度不超过 1 2 7个字符的字符串,写入文件。如串长为 0,即空


串,程序结束。
输入:Hello!↵
How do you do↵
Good-bye! ↵

运行结束后,我们利用 dos的type命令列表文件:
c:\tc> type test.txt↵
Hello!
How do you do
Good-bye!

这里所输入的空串,实际为一单独的回车符,其原因是 gets函数判断串的结束是以回车作
标志的。
[例8-5] 从一个文本文件 test1.txt中读出字符串,再写入令一个文件 test2.txt。
#include <stdio.h>
#include <string.h>
main()
{
FILE *fp1,*fp2;
char str[128];
if ((fp1=fopen("test1.txt","r"))==NULL)
{ /* 以只读方式打开文件 1*/
printf("cannot open file\n");
exit(0);
}
if ((fp2=fopen("test2.txt","w"))==NULL)
{ /*以只写方式打开文件 2*/
printf("cannot open file\n");
exit(0);
}
while ((strlen(fgets(str,128,fp1)))>0)
/*从文件中读回的字符串长度大于 0 */
{
fputs(str,fp2 ); /* 从文件1读字符串并写入文件 2*/
printf("%s",str); /* 在屏幕显示*/
}
fclose(fp1);
fclose(fp2);
}

程序共操作两个文件,需定义两个文件变量指针,因此在操作文件以前,应将两个文件
以需要的工作方式同时打开(不分先后),读写完成后,再关闭文件。设计过程是按写入文件
下载
第8章 输入、输出和文件系统 159
的同时显示在屏幕上,故程序运行结束后,应看到增加了与原文件相同的文本文件并显示文
件内容在屏幕上。
3. 格式化的读写
前面的程序设计中,我们介绍过利用 s c a n f ( )和p r i n t f ( )函数从键盘格式化输入及在显示器
上进行格式化输出。对文件的格式化读写就是在上述函数的前面加一个字母 f成为f s c a n f ( )和
fprintf()。其函数调用方式:
int fscanf(FILE *stream,char *format,arg_list)
int fprintf(FILE *stream,char *format,arg_list)

其中,stream为流文件指针,其余两个参数与 scanf()和printf()用法完全相同。
[例8-6] 将一些格式化的数据写入文本文件,再从该文件中以格式化方法读出显示到屏
幕上,其格式化数据是两个学生记录,包括姓名、学号、两科成绩。
#include <stdio.h>
main()
{
FILE *fp;
int i;
struct stu{ /* 定义结构体类型 */
char name[15];
char num[6];
float score[2];
}student; /* 说明结构体变量 */
if ((fp=fopen("test1.txt","w"))==NULL)
{ /* 以文本只写方式打开文件*/
printf("cannot open file");
exit(0);
}
printf("input data:\n");
for( i=0;i<2;i++)
{
scanf("%s %s %f %f",student.name,student.num,&student.score[0],
&student.score[1]); /* 从键盘输入*/
fprintf(fp,"%s %s %7.2f %7.2f\n",student.name,student.num,
student.score[0],student.score[1]);/* 写入文件*/
}
fclose(fp); /* 关闭文件*/

if ((fp=fopen("test.txt","r"))==NULL)
{ /*以文本只读方式重新打开文件 */
printf("cannot open file");
exit(0);
}
printf("output from file:\n");
while (fscanf(fp,"%s %s %f %f\n",student.name,student.num,
&student.score[0],student.score[1])!=EOF )
/* 从文件读入*/
printf("%s %s %7.2f %7.2f\n",student.name,student.num,
160 C语言程序设计
下载
student.score[0],student.score[1]); 显示到屏幕*
/* /
fclose(fp); /*关闭文件*/
}

程序设计一个文件变量指针,两次以不同方式打开同一文件,写入和读出格式化数据,
有一点很重要,那就是用什么格式写入文件,就一定用什么格式从文件读,否则,读出的数
据与格式控制符不一致,就造成数据出错。上述程序运行如下:
input data:

xiaowan j001 87.5 98.4

xiaoli j002 99.5 89.6
output from file:
xiaowan j001 87.50 98.40
xiaoli j002 99.50 89.60

列表文件的内容显示为:
c:\> type test.txt↵
xiaowan j001 87.50 98.40
xiaoli j002 99.50 89.60

此程序所访问的文件也可以定为二进制文件,若打开文件的方式为:
if ((fp=fopen("test1.txt","wb"))==NULL)
{ /* 以二进制只写方式打开文件 */
printf("cannot open file");
exit(0);
}

其效果完全相同。
4. 成块读写
前面介绍的几种读写文件的方法,对其复杂的数据类型无法以整体形式向文件写入或从
文件读出。C语言提供成块的读写方式来操作文件,使其数组或结构体等类型可以进行一次性
读写。成块读写文件函数的调用形式为:
int fread(void *buf,int size,int count,FILE *stream)
int fwrite(void *buf,int size,int count,FILE *stream)
fread ()函数从 stream 指向的流文件读取 count (字段数)个字段,每个字段为 s i z e (字
段长度)个字符长,并把它们放到 buf(缓冲区)指向的字符数组中。
fread ()函数返回实际已读取的字段数。若函数调用时要求读取的字段数超过文件存放
的字段数,则出错或已到文件尾,实际在操作时应注意检测。
f w r i t e ( )函数从 b u f (缓冲区)指向的字符数组中,把 c o u n t (字段数 )个字段写到 s t r e a m所指向
的流中,每个字段为 size个字符长,函数操作成功时返回所写字段数。
关于成块的文件读写,在创建文件时只能以二进制文件格式创建。
[例8-7] 向磁盘写入格式化数据,再从该文件读出显示到屏幕。
#include "stdio.h"
#include "stdlib.h"
main()
{
FILE *fp1;
下载
第8章 输入、输出和文件系统 161
int i;
struct stu{ /*定义结构体*/
char name[15];
char num[6];
float score[2];
}student;
if ((fp1=fopen("test.txt","wb"))==NULL)
{ /*以二进制只写方式打开文件 */
printf("cannot open file");
exit(0);
}
printf("input data:\n");
for( i=0;i<2;i++) {
scanf("%s %s %f %f",student.name,student.num,
&student.score[0],&student.score[1]);/* 输入一记录*/
fwrite(&student,sizeof(student),1,fp1);成块写入文件*
/* /
}
fclose(fp1);

if ((fp1=fopen("test.txt","rb"))==NULL)
{ /*重新以二进制只写打开文件*/
printf("cannot open file");
exit(0);
}
printf("output from file:\n");
for (i=0;i<2;i++)
{
fread(&student,sizeof(student),1,fp1);/* 从文件成块读*/
printf("%s %s %7.2f %7.2f\n",student.name,student.num,
student.score[0],student.score[1]);/* 显示到屏幕*/
}
fclose(fp1);
}

运行程序:
input data:

xiaowan j001 87.5 98.4

xiaoli j002 99.5 89.6
output from file:
xiaowan j001 87.50 98.40
xiaoli j002 99.50 89.60

通常,对于输入数据的格式较为复杂的话,我们可采取将各种格式的数据当做字符串输
入,然后将字符串转换为所需的格式。 C提供函数:
int atoi(char *ptr)
float atof(char *ptr)
long int atol(char *ptr)

它们分别将字符串转换为整型、实型和长整型。使用时请将其包含的头文件 m a t h . h或
162 C语言程序设计
下载
stdlib.h写在程序的前面。
[例8-8] 将输入的不同格式数据以字符串输入,然后将其转换进行文件的成块读写。
#include <stdio.h>
#include <stdlib.h>
main()
{
FILE *fp1;
char *temp;
int i;
struct stu{ /* 定义结构体类型*/
char name[15]; /* 姓名*/
char num[6]; /* 学号*/
float score[2]; /* 二科成绩*/
}student;
if ((fp1=fopen("test.txt","wb"))==NULL) /* 打开文件*/
{
printf("cannot open file");
exit(0);
}
for( i=0;i<2;i++) {
printf("input name:");
gets(student.name); /* 输入姓名*/
printf("input num:");
gets(student.num); /* 输入学号*/
printf("input score1:");
gets(temp); /* 输入成绩*/
student.score[0]=atof(temp);
printf("input score2:");
gets(temp);
student.score[1]=atof(temp);

fwrite(&student,sizeof(student),1,fp1); /* 成块写入到文件*/
}
fclose(fp1);

if ((fp1=fopen("test.txt","rb"))==NULL)
{
printf("cannot open file");
exit(0);
}
printf("---------------------\n");
printf("%-15s%-7s%-7s%-7s\n","name","num","score1","score2");
printf("---------------------\n");
for (i=0;i<2;i++)
{
fread(&student,sizeof(student),1,fp1);
printf("%-15s%-7s%7.2f%7.2f\n",student.name,student.num,
student.score[0],student.score[1]);
下载
第8章 输入、输出和文件系统 163
}
fclose(fp1);
}

运行程序如下:RUN↵
input name:li-ying
input num: j0123
input score1:98.65
input score2:89.6
input name:li-li
input num: j0124
input score1:68.65
input score2:86.6
----------------------------------
name num score1 score2
----------------------------------
li-ying j0123 98.65 89.60
li-li j124 68.64 86.60

8.1.3 随机读写文件

随机对文件的读写是指在文件内部任意对文件内容进行访问,这也就需要对文件进行详
细的定位,只有定位准确,才有可能对文件随机访问。
C语言提供了用于文件定位的函数,它的作用是使文件指针移动到所需要的位置。
int fseek(FILE *fp,long d,int pos)

fp是文件指针,d是位移量, pos是起始点。
Pos的取值为:
0 :文件开始处
1 :文件的当前位置
2 :文件的尾部
位移量d是l o n g型的数据,可以为正或负值。表示从起始点向下或向上的指针移动。函数
的返回值若操作成功为 0,操作失败为非零。
例如:fseek(fp,5L,0);将文件指针从文件头向下移动 5个字节。
fseek(fp,-10L,2);将文件指针从当前位置向上移动 10个字节。
rewind() 将文件指针移动到文件头。
ftell(FILE *fp) 返回文件指针的当前位置。
[例8-9] 写入5个学生记录,记录内容为学生姓名、学号、两科成绩。写入成功后,随机
读取第三条记录,并用第二条记录替换。
#include <stdio.h>
#include <stdlib.h>
#define n 5
main()
{
FILE *fp1; /* 定义文件指针 */
164 C语言程序设计
下载
char *temp;
int i,j;
struct stu{ /* 定义学生记录结构 */
char name[15];
char num[6];
float score[2];
}student[n];
if ((fp1=fopen("test.txt","wb"))==NULL) /* 以二进制只写方式打开文件 */
{
printf("cannot open file");
exit(0);
}
for( i=0;i<n;i++)
{

printf("input name:"); /* 输入姓名*/


gets(student[i].name);
printf("input num:");
gets(student[i].num); /* 输入学号*/
printf("input score1:");
gets(temp); /* 输入一科成绩*/
student[i].score[0]=atof(temp);
printf("input score2:");
gets(temp); /* 输入第二科成绩*/
student[i].score[1]=atof(temp);

fwrite(&student[i],sizeof(struct stu),1,fp1);成块写入*
/* /
}
fclose(fp1); /*关闭*/

if ((fp1=fopen("test.txt","rb+"))==NULL)
{ /*以可读写方式打开文件*/
printf("cannot open file");
exit(0);
}
printf("---------------------\n");
printf("%-15s%-7s%-7s%-7s\n","name","num","score1","score2");
printf("---------------------\n");
for (i=0;i<n;i++)
{ /*显示全部文件内容 */
fread(&student[i],sizeof(struct stu),1,fp1);
printf("%-15s%-7s%7.2f%7.2f\n",student[i].name,student[i].num,
student[i].score[0],student[i].score[1]);
}
/* 以下进行文件的随机读写 */
fseek(fp1,3*sizeof(struct stu),0); /* 定位文件指针指向第三条记录 */
fwrite(&student[1],sizeof(struct stu),1,fp1);
/* 在第三条记录处写入第二条记录 */
rewind(fp1); /*移动文件指针到文件头 */
printf("---------------------\n");
下载
第8章 输入、输出和文件系统 165
printf("%-15s%-7s%-7s%-7s\n","name","num","score1","score2");
printf("---------------------\n");

for (i=0;i<n;i++)
{ /*重新输出文件内容 */
fread(&student[i],sizeof(struct stu),1,fp1);
printf("%-15s%-7s%7.2f%7.2f\n",student[i].name,student[i].num,
student[i].score[0],student[i].score[1]);
}

fclose(fp1); /*关闭文件*/
}
运行程序:
RUN ↵
input name:li-ying
input num: j0123
input score1:98.65
input score2:89.6
input name:li-li
input num: j0124
input score1:68.65
input score2:86.6
input name:li-ping
input num: j0125
input score1:88.5
input score2:84.6
input name:Wang-xian
input num: j0126
input score1:98
input score2:94
input name:Ma-ling
input num: j0127
input score1:66.5
input score2:80.6
----------------------------------
name num score1 score2
----------------------------------
li-ying j0123 98.65 89.60
li-li j0124 68.64 86.60
li-ping j0125 88.50 84.60
Wang-xian j0126 98.0094.00
Ma-ling j012766.50 80.60
----------------------------------
name num score1 score2
----------------------------------
li-ying j0123 98.65 89.60
li-li j0124 68.64 86.60
li-li j0124 68.64 86.60
Wang-xian j0126 98.0094.00
166 C语言程序设计
下载
Ma-ling j0127 66.50 80.60

程序的第二次输出,即随机访问后,文件中会有两条相同的记录。

8.2 非缓冲文件系统

前面介绍的缓冲文件系统是借助文件结构体指针来对文件进行管理,通过文件指针来对
文件进行访问,既可以读写字符、字符串、格式化数据,也可以读写二进制数据。非缓冲文
件系统依赖于操作系统,通过操作系统的功能对文件进行读写,是系统级的输入输出,它不
设文件结构体指针,只能读写二进制文件,但效率高、速度快,由于 A N S I标准不再包括非缓
冲文件系统,因此建议大家最好不要选择它。本书只作简单介绍。
1. 文件的打开与关闭
非缓冲文件系统不是 A N S I标准定义的,是 U N I X型I / O系统的一员,所以,其原型位于
io.h文件中。
打开文件:

int open(char *fname,int access)

打开文件名为fname,以access方式访问:
access的值为:O_RDONLY 只读
O_WRONLY 只写
O_RDWR 读写
关闭文件:
close(int fd);

下述程序用 UNIX系统打开和关闭一个文件:
#include "io.h"
#include "fcntl.h"
#include "sys\stat.h"
main(argc,argv)
int argc;
char *argv[]
{
int fd;
if((fd=open(argv[1],O_RDONLY))==-1) 以只读方式打开文件
/* */
{
printf("cannt open file!");
exit(0);
}
printf("file existent!");
if (close(fd)) printf("error in closing file\n");
}

2. 文件的读写
对非缓冲文件系统的读写函数的原型在 io.h头文件中,其调用形式为:
int read(int fd,void *buf,int count)

r e a d ( )函数从 f d说明的文件中读取 c o u n t个字节到 b u f所指向的缓冲区。函数的返回值是实


下载
第8章 输入、输出和文件系统 167
际读写的字节数。
int write(int fd,void *buf,int count)

w r i t e ( )函数把 c o u n t个字节从 b u f写入到f d说明的文件中。函数的返回值是实际写入的字节


数。
下面例子从文件 TEST.TST中读取它的前半 100个字节并放到数组 b u ff e r中。
#include "io.h"
#include "stdio.h"
#include "fcntl.h"

main()
{
int fd;
char buffer[100];
if ((fd=open("TEST.TST",O_RDONLY))==-1)打开文件*
/* /
{
printf("cannot open file !\n");
exit(0);
}
if (read(fd,buffer,100)!=100) /* 判断读写的字节数是否正确 */
printf("Possible read error.");
}

8.3 文件系统应用举例

文件操作在程序设计中是非常重要的技术,文件的数据格式不同,决定了对文件操作方
式的不同。
[例8-10] 我们需要同时处理三个文件。文件 a d d r. t x t记录了某些人的姓名和地址;文件
tel.txt记录了顺序不同的上述人的姓名与电话号码。希望通过对比两个文件,将同一人的姓名、
地址和电话号码记录到第三个文件 addrtel.txt。首先看一下前两个文件的内容:
type addr.txt↵
hejie tianjing
liying shanghai
liming chengdu
wangpin chongqing
type tel.txt↵
liying 12345
hejie 8764
wangpin 87643
liming 7654322

这两个文件格式基本一致,姓名字段占 1 4个字符,家庭住址或电话号码长度不超过 1 4个
字符,并以回车结束。文件结束的最后一行只有回车符,也可以说是长度为 0的串。在两个文
件中,由于存放的是同一批人的资料,则文件的记录数是相等的,但存放顺序不同。我们可
以任一文件记录为基准,在另一文件中顺序查找相同姓名的记录,若找到,则合并记录存入
168 C语言程序设计
下载
第三个文件,将查找文件的指针移到文件头,以备下一次顺序查找。
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <string.h>
main()
{
FILE *fptr1,*fptr2,*fptr3; /* 定义文件指针*/
char temp[15],temp1[15],temp2[15];
if ((fptr1=fopen("addr.txt","r"))==NULL)/* 打开文件*/
{
printf("cannot open file");
exit(0);
}
if ((fptr2=fopen("tel.txt","r"))==NULL)
{
printf("cannot open file");
exit(0);
}
if ((fptr3=fopen("addrtel.txt","w"))==NULL)
{
printf("cannot open file");
exit(0);
}
clrscr(); /* 清屏幕*/
while(strlen(fgets(temp1,15,fptr1))>1) 读回的姓名字段长度大于1
/* */
{
fgets(temp2,15,fptr1); /* 读地址*/
fputs(temp1,fptr3); /* 写入姓名到合并文件 */
fputs(temp2,fptr3); /* 写入地址到合并文件 */
strcpy(temp,temp1); /* 保存姓名字段*/
do /*查找姓名相同的记录 */
{
fgets(temp1,15,fptr2);
fgets(temp2,15,fptr2);
} while (strcmp(temp,temp1)!=0);
rewind(fptr2); /* 将文件指针移到文件头,以备下次查找 */
fputs(temp2,fptr3); /* 将电话号码写入合并文件*/

}
fclose(fptr1); /* 关闭文件*/
fclose(fptr2);
fclose(fptr3);

}
下载
第8章 输入、输出和文件系统 169
程序运行后,我们来看一下合并后的文件 addrtel.txt的内容:
type addrtel.txt↵
hejie tianjing
8764
liying shanghai
12345
liming chengdu
7654322
wangpin chongqing
87643
下载

第9章 实用编程技巧
9.1 图形应用技巧

9.1.1 显示适配器类型的自动测试

目前P C机及兼容机的显示器及其适配器的类型非常多,有单色的,也有彩色的。这些显
示器及适配器的模式对应用程序来说是非常重要的。如何在程序中自动识别显示器的模式,
以便更好地使用当前的显示模式是每个微机应用程序开发者的一个重要课题。下面程序可以
方便测出当前显示器适配器的模式(有关具体知识,请参见其它相关的技术书籍)。
[例9-1] 测试显示适配器类型。
#include <stdio.h>
#include <graphics.h>
#define P(note) printf(note)
#define ,value)
PV(format ,value)
printf(format
#define PM printf("mode is ")
#define PD printf("\n\tdetected graphics drive is")

void main( )
{
int gdrive ,gerror ,gmode ;

detectgraph(&gdrive ,&gmode) ; /* 标准测试函数 */


if(gdrive<0)

{P("No graphics hardware detected !\n")
return ;
}
switch (gdrive)
{case 1: PD;
P("CGA") ;
switch(gmode)
{ case 0 :
PM ;
P("CGAC0 320×200") ;
break ;
case 1 :
PM ;
P("CGAC1 320×200") ;
break ;
case 2 :
PM ;
下载
第9章 实用编程技巧 171
P("CGAC2 320×200") ;
break ;
case 3 : PM; P("CGAC3 640×200") ; break ;
case 4 : PM; P("CGAh4 320×200") ; break ;
}
break ;
case 2: PD;
P("MCGA") ;
switch(gmode)
{ case 0 : PM ; P("MCGAC0 320×200") ; break ;
case 1 : PM ; P("MCGAC1 320×200") ; break ;
case 2 : PM ; P("MCGAC2 320×200") ; break ;
case 3 : PM ; P("MCGAC3 320×200") ; break ;
case 4 : PM ; P("MCGAC4 620×200") ; break ;
case 5 : PM ; P("MCGAC5 620×480") ; break ;
}
break ;
case 3 : PD;
P("EGA") ;
switch(gmode)
{ case 0 :PM ;
P("EGALO 640×200") ;
break ;
case 1 :PM ;
P("EGALO 640×350") ;
break ;
}
break ;
case 4 :PD ;
P("EGA64") ;
switch(gmode)
{case 0 :PM ;
P("EGA64LO 640×200") ;
break ;
case 1 : PM ;
P("EGA64HI 640×350") ;
break ;
}
break ;
case 5 :PD ;
P("EGAMONO") ;
PM;
P("EGAMONO 640×350") ;
break ;
case 6 :PD ;
P("IMB8614") ;
switch(gmode)
{case 0 : PM ;
P("IMB8514LO 640×480") ;
172 C语言程序设计
下载
break ;
case 1 : PM ;
P("IMB8514HI 1024 ×768") ;
break ;
}
break ;
case 7 :PD ;
P("HERCMONO") ;
PM ;
P("HERCMONO 720×348") ;
break ;
case 8 :PD ;
P("ATT400") ;
switch(gmode)
{case 0 : PM ;
P("ATT400C0 320×200") ;
break ;
case 1 : PM ;
P("ATT400C1 320×200") ;
break ;
case 2 : PM ;
P("ATT400C2 320×200") ;
break ;
case 3 : PM ;
P("ATT400C3 320×200") ;
break ;
case 4 : PM ;
P("ATT400CMCD 640×400") ;
break ;
}
break ;
case 9 :PD ;
P("VGA") ;
switch(gmode)
{case 0 : PM ;
P("VGALO 640×400") ;
break ;
case 1 : PM ;
P("VGALO 640×350") ;
break ;
case 2 : PM ;
P("VGALO 640×480") ;
break ;
}
break ;
case 10 :PD ;
P("PC3270") ;
PM ;
P("PC3270HI 720×350") ;
下载
第9章 实用编程技巧 173
break ;
}

P("\n\n\t\t\t THANK YOU !")
}

开发图形软件的基本方法
大家都知道,Turbo C 具有汇编语言那样直接控制系统硬件以及调用操作系统资源的功能,
同时又具有一般高级语言完成复杂运算的能力。因此, C语言已成为开发图形软件最理想的程
序语言。下面主要介绍几个生成基本图形的函数,它们是开发复杂图形软件的基础。
显示方式与色调函数
若要在屏幕上显示图形,首先要把屏幕设置为彩色图形显示方式,常用的方式是:
mode 4 320×200 4色

在这种显示方式下,可以使用两种不同的配色器来配置色调,见表 9-1。
表9-1 屏幕色调

配色器号 颜 色 0 颜 色 1 颜 色 2 颜色 3

0 同底色 绿 红 黄
1 同底色 青 淡红 白

利用C语言标准库函数 int86( ),可以方便地完成显示与色调的设置。这两个函数 s e t m o d e


( ) 和palet( ) 见程序TX.c。例如,要求设置 4号显示模式和使用 0号配色器时,调用形式如下:
setmode(4);
palet(0);
画点函数和画线函数
在设置屏幕显示和色调后,就可使用各种绘图函数绘制不同颜色不同形状的图形。任何
图形都有是由点组成的,所以画点函数是其它函数的基础。
在屏幕上画一个点有两种方法:一是调用 D O S 功能,二是直接存取显示缓冲区(视频
RAM)。由于后者有更高的执行速度,所以一般人都使用第二种方法。画点函数 point()中有三
个参数: x、y、c o l o r,其中 x、y分别是显示点的行和列坐标, c o l o r是点的颜色,取值 0 ~ 3,
对应的颜色如表 9-1所示。该函数根据彩色图形显示的原理,直接控制视频 RAM区。它通过指
针ptr访问该内存区域。指针初始化时,
char far ptr=(char far *)×b
0 8000000

视频RAM的首地址赋予了指针 ptr,从而通过该指针就可以直接存取视频 RAM的任何单元。


(针对不同的显示模式,此地址可能不同 )。
使用p o i n t ( )函数可以在屏幕的任意位置上显示指定颜色的一个点。例如在 2 0行1 5列上显
示一个红色点时,使用:
point(20 ,15 ,2);

使用画点函数可以编写画直线的函数 l i n e ( ),其原理是已知直线的两个端点坐标时,用迭
代过程确定组成直线各点的位置。函数 line()的参数为 x1,y1,x2,y2,color。其中x1、y1和
x 2、y 2分别是直线的起点和终点坐标。 C o l o r是直线的颜色,取值 0 ~ 3。例如在屏幕的右斜对
角上画一条绿色直线,使用的格式为:
174 C语言程序设计
下载
line(0 ,0,119 ,319 ,1);

矩形与填充函数
矩形是由四条直线组成的。使用直线函数可以矩形。绘制时,只需知道它左上角和右下
角的坐标,就可以用直线函数绘出它的四条边,矩形函数 b o x ( )的参数为 x 1、y 1、x 2、y 2、
color。其中x1、y1和x2、y2分别是矩形左上角和右下角的坐标, color是四条边的颜色。
矩形填充块,实际是在指定位置上画出具有相同长度和颜色的直线,其填充函数 f i l l b o x ( )
中的参数与 box()中的参数相同。
绘制图形
使用上述几个基本图形的函数,可以编写出在屏幕上绘制任意图形的程序。它使用键盘
上的箭头等功能键,实现显示位置和颜色的控制。为了获取键盘扫描代码和显示当前绘图位
置,要使用十字函数 xhair()和获取键盘扫描代码函数 getkey()。
下面给出一个简单的绘图程序 t x . c。它相当于用画笔在屏幕上绘制图形,画笔的位置用十
字光标显示。画笔的移动由↓、→、↑、←四个键控制。用 H o m e、P g U p、P g D n和E n d键分
别控制十字光标向 4 5°方向移动。画笔的抬起落下由字母键 O控制,颜色由数字键 0~3控制。
功能键F1用于设定单步前进, F2用于设定5步前进。
该程序还可以画出矩形、填充矩形和直线。这时需要设置它们的坐标位置,直线需要两
个端点坐标,矩形需要两个对角的坐标。例如,在抬笔状态下,把十字光标移至第一个位置
后按回车键,然后再移至第二个位置按回车键。之后按下 L键时,则在设定的两个端点的位置
上画出一条直线;如果按 B键,则以两个位置为对角画出矩形;如果按 F键,则画出填充矩形。
另外,用P键可改变色调,按 Q键结束运行,返回到 DOS状态。
[例9-2] 绘图程序tx.c
#include<stdio.h>
#include<dos.h>
#include<ctype.h>
#include<conio.h>
#include<math.h>

void setmode(int);
void palet(int);
void point(int,int ,int) ;
void line(int,int ,int ,int ,int) ;
void box(int,int ,int ,int ,int) ;
void fillboX(int,int ,int ,int ,int) ;
void Xhair(int,int) ;
int getkey(void);

void main()
{
union{
char c[2];
int i;
}key ;
int X=10,y=10 ,cc=2 ,onflag=1 ,palnum=1 ;
下载
第9章 实用编程技巧 175
intX1=0 ,y1=0 ,X2=0 ,y2=0 ,firstpoint=1 ;
int d=1;
setmode(4) ;
palet(0) ;
Xhair(X ,y);
do {
key.i=getkey() ;
Xhair(X ,y);
if(!key.c[0])
switch(key.c[1]){
case 75: if(onflag) /*left*/
line(X ,y,X,y-d ,cc) ;
y-=d ;
break ;
case 77: if(onflag) /* right */
line(X ,y,X,y+d ,cc) ;
y+=d ;
break ;
case 72:if(onflag) /* up */
line(X ,y,X-d ,y,cc) ;
X-=d ;
break ;
case 80: if(onflag) /* down */
line(X ,y,X+d ,y,cc) ;
X+=d ;
break ;
case 71:if(onflag) /* Home-up left */
line(X ,y,X-d ,y+d ,cc) ;
X-=d ;
X-=d ;
break ;
case 73:if(onflag) /*PgUp-up right */
line(X ,y,X-d ,y+d ,cc) ;
X-=d ;
y+=d ;
break ;
case 79:if(onflag) /*End-down left */
line(X ,y,X+d ,y-d ,cc) ;
X+=d ;
y-=d ;
break ;
case 81:if(onflag) /* PgUp-down right */
line(X ,y,X+d ,y+d ,cc) ;
X+=d ;
y+=d ;
break ;
case 59: /*F1*/
d=1 ;
break ;
176 C语言程序设计
下载
case 60: /*F2*/
d=5 ;
break ;
}
else
switch(tolower(key.c[0])){
case 'o': /* brush on-off */
onflag=!onflag ;
break ;
case '1': /* color 1*/
cc=1 ;
break ;
case '2': /* color2 */
cc=2 ;
break ;
case '3': /* color 3*/
cc=3 ;
break ;
case '0': /* color 0*/
cc=0 ;
break ;
case 'b': /* set boX */
boX(X1 ,y1,X2 ,y2 ,cc) ;
break ;
case 'f': /*set fill boX */
fillboX(X1 ,y1 ,X2 ,y2,cc) ;
break ;
case 'l': /* set line */
line(X1 ,y1 ,X2,y2 ,cc) ;
break ;
case 'r': /*set endpoint */
if(firstpoint){
X1=X ;
y1=y ;
}
else {
X2=X ;
y2=y ;
}
firstpoint = !firstpoint ;
break ;
case 'p': /* set color */
palnum = palnum==1 ? 2 :;1
palet(palnum) ;
break ;
}
Xhair(X ,y) ;
}while(key.c[0]!='q') ;
getch() ;
下载
第9章 实用编程技巧 177
setmode(2) ;
}
/* 设置显示方式 */
void setmode(mode)
int mode;
{
union REGS regs;
regs.h.al=mode ;
regs.h.ah=0 ;
int86(0X10 ,&regs ,&regs) ;
}
/* 设置色调*/
void palet(pn)
int pn;
{
union REGS regs;
regs.h.bh=1 ;
regs.h.bl=pn ;
regs.h.ah=11 ;
int86(0X10 ,&regs ,&regs) ;
}
/*画点函数*/
void point(X,y,color)
int X,y,color ;
{
union {
char cc[2];
int i;
}mask ;
int i,indeX ,posit ;
unsigned char t;
char Xor;

char far *ptr=(char far *)0Xb8000000
mask.i=0Xff3f ;
if(X<0||X>199||y<0||y>319)
return ;
Xor=color&128 ;
color=color&127 ;
posit=y%4 ;
color<<=2*(3-posit) ;
mask.i>>=2*posit ;
indeX=X*40+(y/4) ;
if(X%2)indeX+=8152 ;
if(! Xor){
t=*(ptr+indeX)&mask.cc[0] ;
*(ptr+indeX)=t|color ;
}
else{
t=*(ptr+indeX)|(char)0 ;
178 C语言程序设计
下载
*(ptr+indeX)=t^color ;
}
}
/*直线函数*/
void line (X1,y1,X2 ,y2 ,color)
int X1,y1 ,X2,y2 ,color ;
{
register int t,dis ;
int Xerr=0,yerr=0 ,dX ,dy;
int incX,incy ;
dX=X2-X1 ;
dy=y2-y1 ;
if (dX>0)incX=1;
else if(dX==0)incX=0 ;
else incX=-1;
if(dy>0)incy=1 ;
else if(dy==0)incy=0 ;
else incy=-1;
dX=abs(dy) ;
dy=abs(dy) ;
for (t=0;t<=dis+1 ;t++){
point(X1 ,y1 ,color) ;
Xerr+=dX ;
yerr+=dy ;
if(Xerr>dis){
Xerr-=dis ;
X1+=incX ;
}
if(yerr>dis){
yerr-=dis ;
y1+=incy ;
}
}
}
/*矩形函数*/
void boX(X1,y1 ,X2,y2 ,color)
int X1,y1 ,X2 ,y2,color ;
{
line (X1,y1,X2 ,y1 ,color) ;
line(X1 ,y1 ,X2,y2 ,color) ;
line(X1 ,y2 ,X2,y2 ,color) ;
line(X2 ,y1 ,X2,y2 ,color) ;
}
/* 矩形填充函数*/
void fillboX(X1,y1,X2 ,y2 ,color)
int X1,y1 ,X2,y2 ,color ;
{
register int i,begin ,end ;
begin=X1<X2? X1:X2;
下载
第9章 实用编程技巧 179
end=X1>X2? X1:X2;
for (i=begin;i<=end ;i++)
line(i ,y1 ,i,y2 ,color) ;
}
/* 十字光标定位函数 */
void Xhair(X,y)
int X,y;
{
line(X-4 ,y,X+3 ,y,1|128) ;
line(X ,y+4 ,X,y-3 ,1|128) ;
}
/* 获取键盘扫描码函数 */
int getkey()
{
union REGS regs;
regs.h.ah=0 ;
return int86(0X16,&regs ,&regs) ;
}

9.1.2 屏幕图像的存取技巧

Turbo C提供了丰富的图形操作函数,利用这些函数可以很容易编写图形和图像处理程序,
但是Turbo C没有提供屏幕图像存储和恢复的函数,而在许多情况下需要将屏幕上的图像全部
或部分的以文件的形式保存在磁盘上,在需要时快速地从磁盘调入内存并重现在屏幕上。下
面介绍的程序就是用来对屏幕上任一块矩形区域进行存取的程序 t x 1 . c。saveimage( )函数首先
将屏幕上的一块矩形区域图像的数据写入内存某地址,然后创建一个二进制文件,并把该地
址的数据写入此文件中。这样屏幕上的图像就以文件的形式存储在磁盘上了。 loadimage( )函
数首先将磁盘上的图像数据文件打开并读入到内存某地址,然后从该地址将这些数据读到视
屏缓冲区。这样,原先保存的图像就重现在屏幕上了。
t x 1 . c程序首先测试硬件的图形适配卡类型,并根据其值进入相应的图形方式,然后在屏
幕上画一个彩色饼形统计图,并将该图像存入文件 graph.dat中,最后打开graph.dat文件并读入
内存,将这幅图像重现在屏幕上。
[例9-3] 存取任意屏幕图像程序 tx1.c
/* 存取任意屏幕图像头文件 tX1.h */
#include<stdio.h>
#include<graphics.h>
#include<conio.h>
#include<fcntl.h>
#include<alloc.h>
#include<stdlib.h>
#include<io.h>

void saveimage(X1,y1,X2 ,y2 ,f)


int X1,y1 ,X2 ,y2;
char *f;
180 C语言程序设计
下载
{
char *ptr;
unsigned size;
FILE *fn;
size=imagesize(X1 ,y1 ,X2 ,y2) ;
ptr=malloc(size) ;
getimage(X1 ,y1,X2 ,y2 ,ptr) ;
if ((fn=fopen(f,"wb"))==NULL) {
restorecrtmode() ;
printf("Cannot create file %s\n" ,f);
eXit(1) ;
}
fwrite(ptr ,size ,1,fn) ;
fclose(fn) ;
free(ptr) ;
}

void loadimage(X1,y1 ,f)


int X1,y1;
char *f;
{
char *ptr;
unsigned size;
FILE *fn;
if((fn=fopen(f ,"rb"))==NULL){
restorecrtmode() ;
printf("Cannot open file %s\n" ,f) ;
eXit(1) ;
}
size = 0;
while(fgetc(fn) != EOF) size++ ;
ptr=malloc(size) ;
rewind(fn) ;
fread(ptr ,size ,1,fn) ;
fclose(fn) ;
putimage(X1 ,y1 ,ptr ,COPY_PUT) ;
free(ptr) ;
}
/* 图像存取示例*/
void initialize(void) ;
void quit(void);
int X,y;
char *f="graph.dat" ;

void main(void)
{
initialize() ;
outteXtXy(X/2 ,-10 ,"Saving Image");
下载
第9章 实用编程技巧 181
saveimage(0 ,0,X,y,f) ;
outteXtXy(X+50 ,y+y/2+10 ,"Loading Image");
loadimage(X ,y/2 ,f) ;
quit( );
}

void initialize( )
{
int gdrive=DETECT,gmode ,errorcode ;
int angle=360/MAXCOLORS ,color ;
initgraph(&gdrive ,&gmode , "") ;
errorcode=graphresult( ;)
if (errorcode!=grOk)
{
printf("graphics error:%s\n" ,grapherrormsg(errorcode)) ;
printf("press any key to halt:") ;
getch( );
eXit(1) ;
}
X=getmaXX() *2/5;
y=getmaXy( ) *2/5;
setviewport(X/2 ,y/2 ,getmaXX( )-X/2,getmaXy( )-y/2,0) ;
setteXtjustify( CENTER_TEXT ,CENTER_TEXT) ;
rectangle(0 ,0,X,y) ;
for (color=0;color<MAXCOLORS ;color++){
setfillstyle(SOLID_FILL ,color) ;
pieslice(X/2 ,y/2 ,color*angle ,(color+1)*angle ,y/3) ;
}
}

void quit( )
{
getch( );
closegraph( );
}

9.1.3 屏幕显示格式的控制方法

一个良好的屏幕格式能给操作者提供很大的方便,也给人们一种赏心悦目的感觉。
为了控制屏幕显示格式,需要编写两个屏幕控制。我们可以借助这两个函数,设计出用
户所需要的屏幕显示格式。
下面是一个简单的演示程序。程序中借助这两个函数,在屏幕中间显示变动的数字。
[例9-4] 控制显示格式 tx3.c
# include<stdio.h>
# include<bios.h>
#include<dos.h>
182 C语言程序设计
下载
void cls(int);
void gotoXy(int,int) ;

void cls(line)
int line;
{
union REGS in,out ;
in.X.aX=0600 ;
in.X.cX=0000 ;
in.h.dh=line-1 ;
in.h.dl=79 ;
in.h.bh=07 ;
int86(0X10 ,&in ,&out) ;
}
void gotoXy (X,y)
int X,y;
{
union REGS in,out ;
in.h.dh=X ;
in.h.dl=y ;
in.h.ah=02 ;
in.h.bh=0 ;
int86 (0X10,&in ,&out) ;
}
void main( )
{
int i;
gotoXy(0 ,0) ;
cls(11) ;
gotoXy(4 ,20) ;
printf("------------------------") ;
gotoXy(5 ,20) ;
printf("|proceeding record No: |") ;
gotoXy(6 ,20) ;
printf("------------------------") ;
for (i=1;i<=1000 ;i++)
{
gotoXy(5 ,45) ;
printf("%4d" ,i) ;
}
}

9.1.4 使图形软件脱离BGI的方法

大家知道,用 Tu r b o编译的图形软件,在运行时,当前目录下必须要有相应的 B G I文件。


例如C G A . B G I、EGAVGA.BGI等。这对于应用程序是不太方便的。为了解决这个问题,可使
用以下方法。
1) 用Turbo C提供的BTIOBJ.EXE把*.BGI编译成目标文件 *.OBJ。例如:
下载
第9章 实用编程技巧 183
C> BGIOBJ CGA

这样就产生了一个 C G A . O B J。同样,将 EGAVGA.BGI进行编译,再把这些 * . O B J拷入T C


目录下的LIB 子目录中。
使用时,先编译一个 p r o j e c t 文件,把需要的 O B J 文件列入 p r o j e c t文件中。例如对
BGIDEMO .C,相应BGIDEMO.PRJ可写为:
BGIDEMO.C
EGAVGA.OBJ

这样在集成环境下调试 BGIDEMO.C时,当前目录下不需要有 EGAVGA.BGI,最后生成的


EXE文件,运行时也不需要 EGAVGA.BGI。
2) 对命令行编译TCC.EXE只要在编译时列入相应的 EGAVGA.BGI,最后生成的EXE文件。
运行时也不需要 EGAVGA.BGI的EXE文件。
3) 对于经常使用 TCC.EXE的用户,可用 Turbo C提供的TLIB.EXE将上述*.obj扩充进图形
库GRAPHICS.LIB。方法为:
C> TLIB GRAPHICS.LIB +EGAVGA.OBJ

这样,以后在编译时,只要联贯新的 GRAPHICS.LIB就可编译出不需要 BGI的图形软件。

9.1.5 拷贝屏幕图形的方法

在图形方式下,有时需将屏幕信息在打印机上输出。为了输出屏幕图形,要有一个内存
驻留程序。这时要考虑内存驻留程序的激活问题,激活时机的控制以及 TSR的初始化问题。
所有 T S R程序都靠键来激活,因此需用自己的键盘中断程序代替 D O S的键盘中断程序,
激活T S R程序是在 D O S不忙时进行的。当 D O S正在使用时,有一个字节被置 1,当它未被使用
时,该字节为 0,这个地址可用 3 4 H号中断获取,该中断返回后, E S寄存器存放段地址, B X
寄存器放位移,因而,当该字节为 0时,TSR程序才允许激活。
实现时,使用了 interrupt类型说明、寄存器伪变量和程序终止并驻留的技术。
首先由 main( )函数完成初始化工作,它先取得中断 5子程序的地址,然后设置新的 5号中
断程序,这由 g e t v e c t和s e t v e c t来实现的。最后用 k e e p ( 0,s i z e )使程序驻留,并保存 1 6 * s i z e大
小的数据空间。
i n t e r r u p t类型说明符允许我们编写中断处理程序,说明该类型的函数进入时,会自动保存
各寄存器的值,退出时恢复各寄存器的值,新的中断处理程序先判断当前的显示方式,若是
文本方式,则执行老中断程序,若是图形方式,就执行新的图形拷贝程序。
每当发生中断调用时, D O S转移到一个内部很小的数据栈上工作,为了确保程序能正常
工作,必须建立起自己的数据栈,用寄存器伪变量 S P和S S实现。打印完屏幕后,恢复原环境
(此程序针对 EPSON LQ系列打印机)。
[例9-5] 屏幕图形硬拷贝程序 tx4.c
#include <dos.h>
#include<stdio.h>
#include<graphics.h>
#include<bios.h>

×1000
#define STK_SIZE 0
184 C语言程序设计
下载
#define print(ch) ,ch ,0)
biosprint(0

void set_graphics(int cols) ;


void print_scr(int X1 ,int y1,int X2,int y2);
char video_mode(void) ;
void interrupt new_int5(void) ;
void interrupt (*old_int5)() ;
unsigned char stack [STK_SIZE] ;
unsigned sp,ss ;
main()
{
union REGS r;
struct SREGS s;
old_int5=getvect(5) ;
keep(0 ,2000) ;
return 0;
}
void interrupt new_int(void)
{
char vmode;
vmode=video_mode() ;
if((vmode==2)|(vmode==3)|(vmode==7))
setvect(5 ,old_int5) ;
else{
disable() ;
ss=_SS ;
sp=_SP ;
_SS=_DS ;
_SP=(unsigned)&stack[STK_SIZE-2] ;
enable( );
print_scr(0 ,0,639 ,349) ;
disable() ;
_SP=sp ;
_SS=ss ;
enable( );}
}
void print_scr(int X1 ,int y1,int X2,int y2)
{
register int i,X,y,pX;
int cols,color ,sum ;
X2++ ;
y2++ ;
cols=X2-X1 ;
for(y=y1 ;y<y2 ;y+=8){
set_graphics(cols) ;
for(X=X1 ;X<=X2 ;X++){
sum=0 ;
for(i=0 ;i<8 ;i++){
下载
第9章 实用编程技巧 185
if(y+i<y2){
color=getpiXel(X ,y+i) ;
if(color)sum+=1<<(7-i) ;
}
}
print(sum) ;
}
printf("\n") ;
}
}

void set_graphics(int cols)


{
char den_code;
union aa{
unsigned char c[2];
unsigned int i;
}u ;
u.i=cols ;
print(27) ;
print(65) ;
print(8) ;
print(27) ;
print(76) ;
print(u.c[0]) ;
print(u.c[1]) ;
}
char video_mode(void)
{
union REGS r;
r.h.ah=15 ;
return int86(0×10,&r ,&r)&255 ;
}

9.1.6 随意改变VGA显示器显示颜色的技巧

V G A显示适配器是一种使用很普遍的高性能图形适配器,最多有 2 6 1 2 4 4(6 4×6 4×6 4)


种颜色,可同时使用其中的任意种。但是目前来说,普遍使用的仍是非曲直 6种颜色的显示模
式。
若仅仅用现有的 1 6种颜色编制图形软件或窗口软件,画面则显得单调,能否根据需要自
由设置这 1 6种颜色呢?答案是肯定的。在显示器上某一色号所显示的颜色仅由显示卡上的
D A C颜色寄存器中的值决定。 D A C颜色寄存器是一个 1 8位的寄存器,红、绿、蓝各占六位,
卡上共有 2 5 6个这样的寄存器,分别对应于 2 5 6个色号。开机时的 1 6种颜色(即 0 ~ 1 5号)被设
置成如下寄存器及比色:
色号 对应寄存器号码 红 绿 蓝
0 0 0 0 0
186 C语言程序设计
下载
1 1 0 0 42
2 2 0 42 0
3 3 0 42 42
4 4 42 0 0
5 5 42 0 42
6 20 42 21 0
7 7 42 42 42
8 56 21 21 21
9 57 21 21 63
10 58 21 63 21
11 59 21 63 63
12 60 63 21 21
13 61 63 21 63
14 62 63 63 21
15 63 63 63 63
如果改变这 1 6个寄存器中的值,即可改变在屏幕上显示的 1 6种颜色,而对程序运行没有
任何其它影响。
具体实现可以通过调用 VGA BIOS中断进行,也可以通过 V G A寄存器编程实现。在西文
方式下以上两种方法都可以使用,但在中文系统下,由于中文系统修改了视频中断 10H,因此
只能通过VGA寄存器编程实现。
下面两个程序,一个用于设置颜色,另一个用于检查设置。设置的颜色可从 2 6 1 2 4 4种颜
色中任意设定 0 ~ 1 5号颜色。在程序中: r e d,g r e e n,blue 取值为 0 ~ 6 3;c o l o r n u m为要改变颜
色所对应的寄存器号。使用的格式为:
setcolor <寄存器中> <红> <绿> <蓝>
getcolor <寄存器号>

例如,对亮绿色( 10号)进行改色,可在 DOS提示符下,键入:


setcolor 58 35 25 15

回车后, 1 0号码色将成为由红色 3 5、绿色 2 5、蓝色 1 5、调成的新颜色。若要检查 1 0号的


设置,可在 DOS提示符下,键入:
getcolor 58

回车后,屏幕上将出现寄存器号、红、绿、蓝的颜色值。
[例9-6] 设置新色彩 setcolor.c。
/* 格式:setcolor< 寄存器中 >< 红 > < 绿 > < 蓝 >*/
#include<dos.h>
#include<stdlib.h>
#include<stdio.h>

void main(int argc,char *argv[])


{
int colornum,read0 ,green0 ,blue0 ;
union REGS r;
下载
第9章 实用编程技巧 187
if(argc<5)
{
printf("input error!\n");
eXit(1) ;
}
colornum=atoi(argv[1]) ;
read0=atoi(argv[2]) ;
green0=atoi(argv[3]) ;
blue0=atoi(argv[4]) ;
outportb(0 ×3c8 ,colornum) ;
outportb(0 ×3c9 ,read0) ;
outportb(0 ×3c9 ,green0) ;
outportb(0 ×3c9 ,blue0) ;
}

/* 检查颜色设置 getcolor.c*/
/* 格式:getcolor< 寄存器号>*/

#include<dos.h>
#include<stdio.h>
#include<stdlib.h>

main(int argc,char *argv[ ] )


{
int colornum,read0 ,green0 ,blue0 ;
union REGS r;
if(argc<2)
{

printf("input error!\n")
eXit(1) ;
}
colornum=atoi(argv[1]) ;
outportb (0×3c7 ,colornum) ;
read0=inportb(0 ×3c9) ;
green0=inportb(0 ×3c9) ;
blue0=inportb(0 ×3c9) ;
printf("No=%d ,read=%d ,green=%d ,blue=%d\n" ,colornum ,read0 ,green0 ,
blue0) ;
}

9.1.7 用随机函数实现动画的技巧

在一些特殊的C语言动画技术中,可以利用随机函数 int random(int num ) 取一个0~num范


围内的随机数,经过某种运算后,再利用 C 语言的作图语句产生各种大小不同的图形,也能
产生很强的移动感。
程序d h 1 . c就是利用随机函数来产生动画应用。该程序运行后,屏幕中间绘出一台微型计
算机,微机下方同时显示“ c o m p u t e r”的放大字形,在画出微机的小屏幕内,产生各种大小
不同、颜色各异的矩形,这些矩形互相覆盖,给人以极强的动画感。
188 C语言程序设计
下载
程序中改变 x 1、x 2、y 1、y 2的值,能将图形移动屏幕的任何位置,改变 x、y的值,能将
图形放大或缩小。
[例9-7] 动画显示程序DH1.C
#include<conio.h>
#inclu]de<stdio.h>
#include<stdlib.h>
#include<graphics.h>
#include<time.h>
#define X1 260
#define X2 320
#define y1 140
#define y2 180
#define Xy 16
int gdrive,gmode ,mcolor ,ecode ;
struct palettetype palette ;
void ;
initialize(void)
void rbars(void);

int main( )
{
initialize( ); /*初始化图形系统 */
/* 显示放大字体*/
setcolor(YELLOW) ;
settextstyle(TRIPLEX_FONT ,HORIZ_DIR ,4) ;
settextjustify(CENTER_TEXT ,CENTER_TEXT) ;
outtextxy((getmaXX( )/2-17) ,360 ,"COMPUTER") ;
rbars( ); /* 主程序*/
closegraph( );/* 关闭图形系统 */
return 1;
}

void initialize(void)
{
gdrive=DETECT ;
initgraph (&gdrive ,&gmode ,"") ;
ecode=graphresult( ) ;
if (ecode!=0)
{
printf("Graphice Error : %d\n,g
" rapherrormsg(ecode)) ;
eXit(1) ;
}
getpalette(&palette) ;
mcolor=getmaXcolor( )+1 ;
}
void rbars(void)
{
int color ; /* 画计算机图形*/
setcolor(DARKGRAY) ;
下载
第9章 实用编程技巧 189
bar3d(X1-20 ,y1-20 ,X2+56 ,y2+70 ,0,3) ;
setfillstyle(CLOSE_DOT_FILL ,BLUE) ;
setfillstyle(SOLID_FILL ,RED) ;
circle(X2+28 ,y2+60 ,4) ;
bar(X1+4 ,y1+78 ,X1+20 ,y1+83) ;
setcolor(MAGENTA) ;
circle(X2+28 ,y2+60 ,4) ;
circle(X2+16 ,y2+60 ,4) ;
circle(X2+4 ,y2+60 ,4);
setcolor(WHITE) ;
setfillstyle(SOLID_FILL ,DARKGRAY) ;
bar3d(X1-60 ,y1+120 ,X1+154 ,y1+170 ,0,2) ;
bar3d(X1+120 ,y1+126 ,X1+100 ,y1+164 ,0,2) ;
line (X1+20,y1+145 ,X1+100 ,y1+145) ;
setfillstyle(SOLID_FILL ,GREEN) ;
bar(X1+26 ,y1+130 ,X1+34 ,y1+132) ;
bar(X1+26 ,y1+150 ,X1+34 ,y1+152) ;
setfillstyle(WIDE_DOT_FILL ,RED) ;
bar(X1-24 ,y1+128 ,X1-44 ,y1+142) ;
/*利用随机函数实现矩形画面互相覆盖,产生动感 */
while(!kbhit( ))
{
color=random(mcolor-1)+1 ;
setcolor(color) ;
setfillstyle(random(11)+1 ,color) ;
bar3d(X1+random(getmaXX( )/Xy) ,y1+random(getmaXy( )/Xy)

X2+getmaXX( )/Xy,y2+ getmaXy( )/Xy,0,5) ;
}
}

9.1.8 用putimage 函数实现动画的技巧

计算机图形动画显示的是由一系列静止图像在不同位置上的重现。计算机图形动画技术
一般分为画擦法和覆盖刷新法两大类。画擦法是先画 T时刻的图形,然后在 T +△T时刻把它擦
掉,改画新时刻的图形是由点、线、圆等基本图元组成。这种一画一擦的方法对于实现简单
图形的动态显示是比较有效的。而当需要显示比较复杂的图形时,由于画擦图形时间相对较
长,致使画面在移动时出现局部闪烁现象,使得动画视觉效果变差。所以,为提高图形的动
态显示效果,在显示比较复杂的图形时多采用覆盖刷新的方法。
在Turbo C 的图形函数中,有几个函数可完成动画的显示:
getimage(int left,int top,int right,int bottom,void far*buf) 函数把屏幕图形部分拷贝
到由buf所指向的内存区域。
imagesize() 函数用来确定存储图形所需的字节数,所定义的字节数根据实际需要可以定
义得多一些。
p u t i m a g e ( )函数可以把 g e t i m a g e ( )存储的图形重写在屏幕上。利用 p u t i m a g e ( )函数中的
C O P Y _ P U T项,在下一个要显示的位置上于屏幕中重写图像,如此重复、交替地显示下去,
190 C语言程序设计
下载
即可达到覆盖刷新的目的,从而实现动画显示。由于图形是一次性覆盖到显示区的,并在瞬
间完成,其动态特性十分平滑,动画效果较好。
程序d h 2 . c就是根据上述思路而实现的。程序运行时,将在屏幕上出现一个跳动的红色小
球。
[例9-8] 动画显示程序dh2.c
#include <stdio.h>
#include<graphics.h>
#include<alloc.h>
#include<conio.h>

void main(void)
{
int driver=DETECT,mode ;
int k=0,i,m,m1 ;
int maXX,mayy ,size ;
char *buf;

initgraph(&driver ,&mode ," ");


maXX=getmaXX() ;
mayy=getmaXy() ;
setfillstyle(SOLID_FILL ,LIGHTGRAY) ;
bar(1 ,1,maXX ,mayy) ;
setcolor(RED) ;
for(i=0 ;i<=10 ;i++)
circle(150 ,150 ,i);
size=imagesize(100 ,100 ,250 ,200) ;
if(size != -1)
buf=malloc(size) ;
if(buf)
{
getimage(100 ,100 ,250 ,200 ,buf) ;
m=120 ;m1=m ;
do{
k=k+1 ;
if ((m1+100)>mayy)
{
for(m=m+30 ;m<maXX ;m=m+30)
{
m1=m1-20 ;
putimage(m ,m1,buf ,COPY_PUT) ;
}
}
if((m+100)>maXX)
{
m=m-100 ;
for(m1=m1+100 ;m1>=1 ;m1=m1-10)
{
下载
第9章 实用编程技巧 191
m1=m1-19 ;
putimage(m ,m1,buf ,COPY_PUT) ;
}
for(m=m ;m>1 ;m=m-30)
{
m1=m1-17 ;
putimage(m ,m1 ,buf ,COPY_PUT) ;
}
}
m1=m1+20 ;
m=m+20 ;
putimage(m ,m1 ,buf ,COPY_PUT) ;
}while(k!=1000) ;
getch() ;
}
restorecrtmode() ;
}

9.2 菜单设计技术

菜单在用户编写的程序中占据相当一部分内容。设计一个高质量的菜单,不仅能使系统
美观,更主要的是能够使操作者使用方便,避免一些误操作带来的严重后果。

9.2.1 下拉式菜单的设计

下拉式菜单是一个窗口菜单,它具有一个主菜单,其中包括几个选择项,主菜单的每一
项又可以分为下一级菜单,这样逐级下分,用一个个窗口的形式弹出在屏幕上,一旦操作完
毕又可以从屏幕上消失,并恢复原来的屏幕状态。
设计下拉式菜单的关键就是在下级菜单窗口弹出之前,要将被该窗口占用的屏幕区域保
存起来,然后产生这一级菜单窗口,并可用光标键选择菜单中各项,用回车键来确认。如果
某选择项还有下级菜单,则按同样的方法再产生下一级菜单窗口。
用Turbo C 在文本方式时提供的函数 gettext( ) 来放屏幕规定区域的内容,当需要时用
puttext( )函数释放出来,再加上键盘管理函数 bioskey( ),就可以完成下拉式菜单的设计。
程序m e n u 1 . c是一个简单拉式菜单。运行时在屏幕上一行显示主菜单的内容,当按 ALT+F
则进入 F i l e子菜单,然后可用光标键移动色棒选择操作,用回车确认。用 E s c键退出主菜单,
并可用ALT+X退出菜单系统。
[例9-9] 下拉式菜单 menu1.c
/* 下拉式菜单 menu1.c*/
#include<conio.h>
#include<stdio.h>
#include<stdlib.h>
#include<bios.h>

void main(void)
{
192 C语言程序设计
下载
int i,key ,key0 ,key1 ,y,test ;
char *m[ ]={"File " ,"Edit ","Run ","Compile ","Projsct ",
"Options ","Debug ","Break/watch "}; /*定义主菜单的内容 */
char *f[ ]={"Load F3", /* 定义FILE 子菜单的内容*/
"Pick ALT+F3",
"New ",
"Save F2",
"Write to ",
"Directory ",
"Change dir ",
"Os shell ",
"Quit ALT+X"};
char buf[16*10*2],buf1[16*2] ; /* 定义保存屏幕区域的数组变量 */
textbackground(BLUE) ; /* 设置文本屏幕背景色 */
clrscr( ); /* 屏幕背径着色*/
window(1 ,1,80 ,1); /* 定义一个文本窗口 */
textbackground(WHITE) ; /* 设置窗口背景色*/
textcolor(BLACK) ;
clrscr( );
window(1 ,1,80 ,2) ;
for (i=0;i<8 ;i++)
cprintf("%s" ,m[i]) ; /*显示主菜单的内容 */
while(1)
{
key=0 ;
while(bioskey(1) == 0) ; /* 等待键盘输入 */
key = bioskey(0); /* 取键盘输入码 */
key = key&0Xff? 0:key>>8 ; /*只取扩充键码 */
if(key == 45) eXit (0) ; /* 如果按ALT+X 键则退出*/
if(key == 33) /* 如果按ALT+F 则显示子菜单*/
{
textbackground(BLACK) ;
textcolor (WHITE);
gotoxy(4 ,1);
cprintf("%s" ,m[0]) ;
gettext(4 ,2,19 ,11,buf) ;/* 保存窗口区域的在原有内容*/
window(4 ,2,19 ,11) ;
textbackground(WHITE) ;
textcolor(BLACK) ;
clrscr( );
window(4 ,2,19,12) ;
gotoxy(1 ,1) ; /*作一个单线形边框 */
putch(0xff) ;
for (i=2;i<10 ;i++)
{
gotoxy(1 ,i) ; putch(0 ×b3) ;
gotoxy(16 ,i) ; putch(0 ×b3) ;
}
gotoxy(1 ,10) ;
下载
第9章 实用编程技巧 193
putch(0Xc0) ;
for (i=2;i<16 ;i++)
putch(0Xc4) ;
putch(0Xd9) ;
for (i=2;i<10 ;i++)
{
gotoxy(2 ,i) ;
cprintf("%s" ,f[i-1]) ;
}
gettext(2 ,2,18 ,3,buf1) ;
textbackground(BLACK) ;
textcolor(WHITE) ;
gotoxy(2 ,2);
cprintf("%s" ,f[0]) ;
y=2 ;
key1=0 ;
while((key0!=27)&&(key1!=45)&&(key0!=13))
{/* 输入为ALT+X ,回车或ESC 键退出循环*/
while(bioskey(1)==0) ; /* 等待键盘输入 */
key0=key1=bioskey(0) ; /*取键盘输入码 */
key0=key0&0Xff ; /* 只取扩充码*/
key1=key1&0Xff? 0:key1>>8 ;
if (key1==72||key1==80) /*如果为上下箭头键 */
{
puttext(2 ,y,18 ,y+1 ,buf1) ; /*恢复原来的信息 */
if (key1==72) y= y==2? 9:y-1 ; /* 上箭头处理*/
if (key1==80) y= y==9? 2:y+1 ; /* 下箭头处理*/
getteXt(2 ,y,18 ,y+1 ,buf1) ; /* 保存新色棒前产生这一位置屏幕内容 */
textbackground(BLACK) ; /*产生新色棒*/
textcolor(WHITE) ;
gotoxy(2 ,y) ;
cprintf("%s" ,f[y-1]) ;
}
}
if(key1 == 45) eXit(0) ; /* 按ALT+X 退出*/
if(key0 == 13) /* 回车按所选菜单项进行处理 */
{
switch(y)
{
case 1:
break ;
case 2:
break ;
case 9:
eXit(0) ;
default:
break ;
}
}
194 C语言程序设计
下载
else /*按ESC 键返回主菜单*/
{
window(1 ,1,80 ,2) ;
puttext(4 ,2,19 ,11 ,buf) ; /*释放子菜单窗口占据的屏幕原来内容 */
textbackground(WHITE) ;
textcolor(BLACK) ;
gotoxy(4 ,1) ;
cprintf("%s" ,m[0]) ;
}
}
}
}

9.2.2 选择式菜单的设计

所谓选择式菜单,就是在屏幕上出现一个菜单,操作者可根据菜单上所提供的数字或字
母按相应的键去执行特定的程序,当程序执行完后又回到主菜单上。
这种菜单编制简单,操作方便,使用灵活,尤其适用于大型管理程序。如果在自动批处
理文件上加入这种菜单后,操作者可根据菜单上的提示,进行相应的操作,这样可以简化许
多步骤,对一般微机用户来说是比较适合的。
[例9-10] 选择式菜单程序 menu2.c
#include<stdio.h>
#include<stdlib.h>
#include<conio.h>

main( )
{
char ch;
int i;
do {
system("cls") ;
printf("\n\t1. into Turbo C ;")
printf("\n\t2. into Windows "; )
printf("\n\t3. into Wps ;")
printf("\n\t4. into Dbase ; ")
printf("\n\t0. Quit \n\n ;")
printf("\t ;
Please select:")
ch=getch( );
switch(ch)
{
;break ;
case '1' : system("tc")
;break ;
case '2' : system("win")
;break ;
case '3' : system("wps")
;break ;
case '4' : system("dbase")
;eXit(1) ;
case '0' : system("cls")
default:

printf("\n\t*** wrong !!! ***\n")
下载
第9章 实用编程技巧 195
for (i=0;i<600 ;i++) ;
{ ; }
}
}
while (1);
}

9.2.3 实现阴影窗口的技巧

目前,许多应用软件都采用了输出窗口技术,有的甚至使用了带有阴影的输出窗口。这
种技术给人们以新鲜醒目的感觉,可达到事半功倍的作用。
程序 m e n u 3 . c是一个阴影窗口的例子。其中用到两个自编函数,一个是建立窗口函数
set_win( ),另一个是建立带有阴影部分的窗口函数 set_bkwin( )。这两个函数需要传递以下几
个参数:
int X1,y1 ,X2,y2 ,b,bc ,tc
char *head

其中:x1、y1、x2、y2决定了窗口边框大小, b用来选择窗口的类型;当 b=0时,无边框,


当b =1时,单线边框,当 b = 2时,上下边框为双线,左右边框为单线,当 b = 3时,双线边框,
当b = 4时,上下边框为单线,左右边框为双线;参数 b c用来决定窗口的背景颜色,其范围为
0~7;参数tc决定窗口内字符的颜色,其范围为 0~15; 参数head为窗口名称。
在文本状态下,一个字符在屏幕缓冲区内要占用2个字节来存储。且字符内容在前属性
在后,顺序存储。所谓属性,就是字符的背景颜色和字符颜色,我们可以通过改变其属性字
节,来实现窗口的阴影。
[例9-11] 阴影窗口程序menu3.c
#include<stdio.h>
#include<string.h>
#include<conio.h>
#define screen (*screen_ptr)

typedef struct texel_struct


{
unsigned char attr;
}teXel ;

typedef texel screen_array[25][80];


screen_array far *screen_ptr = (screen_array far *) 0Xb800;
void set_win( int X1,int y1, int X2,int y2,int b ,int bc, int tc,
char *head);
void set_bkwin(int ,iX1n t y ,
1 i n t X,
2 i n t y,i
2 nt ,
b i n t b,
c i n t t,c
c har
*head) ;

void main(void )
{
set_bkwin(1 ,2,25 ,18,2,2,1,"window") ;
getch() ;
196 C语言程序设计
下载
}

void set_bkwin(X1,y1 ,X2 ,y2 ,b,bc ,tc,head)


int X1, y1 , X2 , y2 , b, bc, tc ;
char *head;
{
int i, j;
for (i=X1+1;i<X2+2 ;i++)
{
for (j=y2;j<y2+1 ;j++)
screen[j][i].attr=8 ;
}
for (i=X2+1;i<X2+2 ;i++)
{
for (j=y1;j<y2+1 ;j++)
screen[j][i].attr=8 ;
}
set_win(X1 ,y1 ,X2 ,y2,b,bc ,tc ,head) ;
}

void set_win(X1,y1 ,X2,y2 ,b,bc ,tc,head)


int X1,y1 ,X2,y2 ,b,bc ,tc;
char *head;
{
int i,j ;
int c[4][6] ={
{0Xda , 0Xc4 , 0Xbf ,0Xb3 , 0Xc0 , 0Xd9 },
{0Xd5 , 0Xcd , 0Xb8 ,0Xb3 , 0Xd4 , 0Xbe },
{0Xc9 , 0Xcd , 0Xbb ,0Xba , 0Xc8 , 0Xbc },
{0Xd6 , 0Xc4 , 0Xb7 ,0Xba , 0Xb3 , 0Xbd },
};
j=(X2-X1)/2-strlen(head)/2+X1+1 ;
textbackground(bc) ;
textcolor(tc) ;
if(b!=0)
{
window(1 ,1,80,25) ;
gotoxy(X1 ,y1) ;
putch(c[b-1][0]) ;
for (i=X1+1;i<X2 ;i++)
putch(c[b-1][1]) ;
putch(c[b-1][2]) ;
for (i=y1+1;i<y2 ;i++)
{
gotoxy(X1 ,i) ;
putch(c[b-1][3]) ;
gotoxy(X2 ,i) ;
putch(c[b-1][3]) ;
}
下载
第9章 实用编程技巧 197
gotoxy(X1 ,y2) ;
putch(c[b-1][4]) ;
for (i=X1+1;i<X2 ;i++)
putch(c[b-1][1]) ;
}
if (head[0]!=NULL)
{
gotoxy(j ,y1) ;
textcolor(WHITE) ;
textbackground(BLUE) ;
cprintf("%s" ,head) ;
}
textcolor(tc) ;
textbackground(tc) ;
window(X1+1 ,y1+1 ,X2-1 ,y2-1) ;
clrscr( );
}

9.3 音响技巧

9.3.1 音乐程序设计

我们知道,音乐是音高和音长的有序组合,设计微机音乐最重要的就是如何定义音高和
音长,以及如何让扬声器发出指定的音符。下面给出音符与频率的关系表。 C语言提供的三个
函数s o u n d ( )、n o s o u n d ( )和c l o c k ( )可以很方便地解决上述的问题。 s o u n d ( )函数可以用指定频率
打开P C机扬声器直到用 n o s o u n d ( )函数来关闭它; c l o c k ( )函数正好用来控制发声时间,而且它
不受PC机主频高低的影响。下面这段程序可使微机发出 c调1的声音。
表9-2 音符与频率关系表

c d e f g a b
音符
1 2 3 4 5 6 7
频率 262 294 330 349 392 440 494
c d e f g a b
单符
1 2 3 4 5 6 7
频率 523 587 659 698 784 880 988
c d e f g a b
音符
1 2 3 4 5 6 7
频率 1047 1175 1319 1397 2568 1760 1976

[例9-12] 音乐程序music1.c
#include<stdio.h>
#include<dos.h>

void pause(int);
void sound1(int,int) ;

void main(void)
198 C语言程序设计
下载
{
int i,freq ,speed=5 ;
int time=4*speed;
char *qm="iddgwwwqqgfff dddfghhhggg ddgwwwqqgfff\
ddffhjqqqqq wpggjhgddgqq hhqwwqjjjggg\
ddgwwwqqqgfff ddffhjqqqqqq" ;/* 定义歌曲*/
while (*qm++ !='\0'){
i=1 ;
switch(*qm){
case 'k':
time=1*speed ; i=0 ;
break ;
case 'i':
time=6*speed ; i=0 ;
break ;
case 'o':
time=10*speed ; i=0 ;
break ;
case 'p':
pause(time) ; i=0 ;
break ;
case 'a':
freq=523 ;
break ;
case 's':
freq=587 ;
break ;
case 'd':
freq=659 ;
break ;
case 'f':
freq=698 ;
break ;
case 'g':
freq=784 ;
break ;
case 'h':
freq=880 ;
break ;
case 'j':
freq=988 ;
break ;
case 'z':
freq=262 ;
break ;
case 'X':
freq=294 ;
break ;
case 'c':
下载
第9章 实用编程技巧 199
freq=330 ;
break ;
case 'v':
freq=349 ;
break ;
case 'b':
freq=392 ;
break ;
case 'n':
freq=440 ;
break ;
case 'm':
freq=494 ;
break ;
case 'q':
freq=1047 ;
break ;
case 'w':
freq=1175 ;
break ;
case 'e':
freq=1319 ;
break ;
case 'r':
freq=1397 ;
break ;
case 't':
freq=2568 ;
break ;
case 'y':
freq=1760 ;
break ;
case 'u':
freq=1976 ;
break ;
default:
i =0;
break ;
}
if(i)
sound1(freq ,time) ;
}
}
,int time) /*freq为频率,time为持续时间 */
void sound1(int freq
{
union {
long divisor;
unsigned char c[2];
} count;
200 C语言程序设计
下载
unsigned char ch;

count.divisor=1193280/freq ; /* 1193280 是系统时钟速率 */


outp(67 ,182) ;
outp(66 ,count.c[0]) ;
outp(66 ,count.c[1]) ;
ch=inp(97) ;
outp(97 ,ch|3) ;
pause(time) ;
outp(97 ,ch) ;
}
void pause(int time)
{
int t1,t2 ;
union REGS in,out ;
in.h.ah=0X2c ;
int86(0X21 ,&in ,&out) ; /* 取当前时间 */
t1=t2=100*out.h.dh+out.h.dl ; /*out.h.dh 为秒值,out.h.dl 为1/100 秒值 */
while(t2-t1<time)
{
int86(0X21 ,&in ,&out) ;
t2=100*out.h.dh+out.h.dl ;
if (t2<t1)t2+=6000 ; /* 增加一分钟 */
}
}

9.3.2 自动识谱音乐程序

音乐的简谱是由各种音符构成的,将这些音符按不同的频率、持续时间连续发出声音,
就形成了旋律。因此音乐演奏的关键是曲调的定义与识别及发音时间的控制。
为了实现计算机自动识谱,可定义一套曲调的编码,其中“ 1 2 3 4 5 6 7”表示中音的 1、2、
3、5、6、7;高音可在中音之后加“ *”;低音在中音之后加“;”号;减号“ -”表示两
拍;“.”表示一拍半;“ =”表示四分之一拍;下画线“ _”表示1/2拍。
我们可用文本编辑软件(如 E D I T)按上述编码将一首曲子的乐谱输到计算机中,得到乐
谱文件供程序调用。其中乐谱文件的第一节的数字分别为:节拍基数和速度,输入时用空格
分开。乐谱文件的第二行到最后一行为歌曲的内容,每小节之间用“ /”分开。
下面根据“世上只有妈妈好”编制曲谱文件如下Ma.txt:
8 50
6.5_3 5 /1* 6_5_6-/3 5_6_5 3 /1_6;5_
3_2-/2_3_5 5_6_/3 2 1-/5.3 2_/_/6.;/_5-/

曲谱文件首行是 8节拍基数, 50是演奏速度,从第二行开始至文件尾,均为曲谱正文。


需要说明的是在曲谱文件中,每个音符应跟上其音符的节拍,文中空格‘ ’符表示其
音符是全音符。
程序中设置两个整型数组 S a和S b用于存放音符的频率及节拍,其内容是一一对应的,若
下载
第9章 实用编程技巧 201
Sa[i]存放音符,则Sb[i]是该音符的演奏节拍。
发声的原理是利用 C的标准库函数 S o u n d ( ) 发声,若要发音中音“ 1”音符,其音频为
“2 6 2”,则函数调用 S o u n d ( 2 6 2 )则可通过 P C机扬声器发音控制音符发声的时间由标准函数
delay( )决定,nosound( )函数为关闭扬声器发音。
源程序如下:
#include <stdio.h>
#include<dos.h>
#include<math.h>
#include<stdlib. h>
#include<string.h>
#include<bios.h>
int sa[1000],sb[1000];
int j,step,rate,len,lenl,half;
chat, strl27[127];
int getmusic();
void chang(void);
void music(void);
/************************/
int main()
{
len=0; len1=0;
if(getmusic()!=0)
exit (1);
delay(500);
music ( );
return 0;
}
/******************/
int getmusic()
{
FILE * fp;
if ((fp=fopen("ma.txt","r"))==NULL) 打开曲谱文件*
/*
{
printf("file not open\n") ;
return 1;
}
fscanf(fp, "%d %d\n", &step,&rate);
while(! fgets(str127, 127, fp)==NULL)
{
chang ( );
}
fclose (fp);
return 0;
}
/*******************/
void chang(void)
{
202 C语言程序设计
下载
int k;
for (k=0;k<strlen(strl27) ;k++) /*组合音符频率 */
if ((str127[k]>=‘0’ )&&(str127[k]<’7’))
{
sa[len]=str127[k]-48;
Switch(sa[len])
{
case 1:sa[len]=262;break;
case 2:sa[len]=294;break;
case 3:sa[len]=330;break;
case 4:sa[len]=349;break;
case 5:sa[len]=392;break;
case 6:sa[len]=440;break;
case 7:sa[len]=494;break;
case 0:sa[len]=0;
}
len++;
if(len>999)
exit(0);
half=0;
}
for (k=0;k<strlen(str127);k++)sb[k]=step;
for (k=0;k<strlen(str127);k++)
{
switch(str127[k]) 组合音符节拍*/
/*
{
case '_':len1++;
break;
case'.': sb[len1]=sb[len]*3/4;
lenl++;
break;
case '=': sb[len1]=ceil(sb[len1]/4);
lenl++;
break;
case'': sb[len1]=sb[len1]/2;
lenl++;
break;
case'_': sb[len1]=ceil(sb[len1]/2);
lenl++;
break;
case';':if(sa[len1]>0)
sa[len1]=sa[len1]/2;
break;
case'*':if(sa[len1]>0)
sa[len1]=sa[len1]*2;
break;
}
}
}
/******************/
void music(void)
{
下载
第9章 实用编程技巧 203
j=0;
while ((j<=len)&&(bioskey(1)==0))
{
sound(2*sa[j]); 发声*/
/*
delay(2*rate*sb[j]); 延迟*
/* /
nosound( ); /*关闭发声*/
j++;
}
}

9.3.3 实现后台演奏音乐的技巧

B A S I C语言有一个前后台演奏音乐的语句 p l a y,该语句有很强的音乐功能。而 C语言虽有


s o u n d ( )函数,但不能进行后台演奏,并且必须指明音乐频率,才能使它发声。为此可编制一
个与play语句相同的后台演奏音乐函数。
若要奏乐,每一个音符必须有一个频率用 sound 去发声,且必须有适当的时间延时,形成
拍子,这样才能演奏音乐。我们可用指定 1拍的时间来推出其它节拍。例如:
#define L1 1000
#define L2 L1/2
#define l4 L1/4

即L1为1拍,L2为1/2拍,L4为1/4拍。
后台演奏,可通过修改 1 C向量来实现。计算机每秒发出 1 8 . 2次中断调用 1 C,因此,就可
以通过它来计算,实现后台演奏。
程序P L AY. C只是一个简单的后台演奏音乐的例子,将其编译后,就可在 D O S提示符后直
接执行,演奏过程中,按任一键都将停止演奏。
[例9-13] 后台演奏程序PLAY.C
#include<stdio.h>
#include<dos.h>
#include<bios.h>
#include<conio.h>
#define L1 1000
#define L2 L2/2
#define L4 L1/4

void play(int *);



void interrupt new_int9(void)

void interrupt (*old_int9)(void)

int HZ[4][7]={
{131 ,147 ,165 ,175 ,196 ,220 ,247} ,
{262 ,294 ,330 ,349 ,392 ,440 ,494} ,
{523 ,587 ,659 ,698 ,784 ,880 ,980}
};
int *s;
int buf[100]={11 ,12 ,12 ,12 ,13 ,12 ,14 ,12 ,16 ,12 ,17 ,12 ,21 ,11 ,22 ,12 ,
204 C语言程序设计
下载
23 ,12 ,24 ,12,25 ,12 ,26,12 ,27 ,12,31 ,12 ,0,0,0};

void main(void)
{
play(buf) ;
; /* 判断结束条件 */
while (*s && ! bioskey(0))

nosound() ;
setvect(0X1c ,old_int9) ;
}

void play(int *ms)


{
s=ms ;
old_int9=getvect(0X1c) ;
setvect(0X1c ,new_int9) ;
}
void interrupt new_int9(void)
{
static int count=0,tt=0 ;
count++ ;
if (*s!=0)
{if (count>=tt)
{sound(HZ[*s/10][*s%10]) ;s++ ;
tt=*s*18.2/1000 ;
s++ ;
count=0 ;
}
else nosound();
old_int9() ;
}
}
下载

第10章 C++入门
90年代以来,面向对象的程序设计( Object-Oriented Programming,简称OOP)异军突起,
迅速在全世界流行,一跃成为主流的程序设计技术。在软件市场中,覆盖面大、垄断市场的
新一代程序设计语言、软件开发工具和环境以及操作系统大多是面向对象的。

10.1 面向对象的概念

10.1.1 面向对象的程序结构

面向对象的程序设计是一种基于结构分析的、以数据为中心的程序设计方法。在面向对
象的程序中,活动的基本单位是对象,向对象发送消息可以激活对象的行为。为此,许多人
把面向对象的程序描述为:
程序=对象+消息传递
1. 对象
对象类似于 C语言中的变量,可以泛指自然界中的任何事务,包括具体实物和抽象概念。
对象具有一些属性、状态和行为。例如,每个人都有姓名、性别、年龄、身高、体重等属性,
有工作、学习、吃饭、睡觉等行为。所以,对象一般可以表示为:属性 +行为。在面向对象的
程序设计中,对象被表示为:数据 +操作,操作也称为方法。这就是说,面向对象程序设计中
的对象是指由一组数据和作用于其上的一组方法组成的实体。
2. 类
在面向对象的程序设计中,会涉及到许多对象,我们无法将所有的对象都描述清楚,如
果那样做的话,程序将无限长或无法描述。因此在面向对象的程序设计中引入了类的概念,
将同类的对象归于一类,同类对象具有相同的属性和行为,如各种花同属一类,各种草、植
物等也分别属于不同的类。
3. 消息
消息就是对对象进行某种操作的信息。当要求对象执行某种特定操作时,就向该对象发
送操作消息,对象接收到指定操作的消息后,就调用相应的操作方法,完成有关操作。
消息及其传递机制是面向对象程序设计的一个重要角色。对象的一切活动,都要通过消
息来驱动,消息传递是对象间通信的唯一途径。
4. 方法
方法就是对对象进行的某种操作。当对象接收到相应的消息时,就调用对应的方法完成
指定的操作,有了消息,就驱动对象工作;有了方法,就能实现消息所要求的操作。
5. 继承
继承是面向对象语言的另一个重要概念。在客观世界中,存在着整体与个体的关系、一
般与特殊的关系,继承将后者模型化。
206 C语言程序设计
下载
例如,对人的分类我们用图 10-1描述如下。
在类的层次结构图中,下层节点都具有上层节点的特性,都具备人的共同特点。但下层
节点较之上层节点而言,又具有新的特性,是上层节点 人
所不具有的。这种下层节点对上层节点的特性的保持,
就是我们所说的继承。
在面向对象语言中,类功能支持这种层次结构。除 工人 农民 解放军
了根结点外,每个类都有它的超类,又称为父类或基类。
除了叶结点外,每个类都有它的子类,又称为派生类。
一个子类可以从它的基类继承所有的数据和操作,并扩
海军 空军 陆军
充自己的特殊数据和操作。基类抽象出共同的属性和操
作,子类体现其差别。有了类的层次结构和继承性,不 图10-1 对人的分类

同对象的共同特性只需定义一次,用户就可以充分利用已有的类,进行完善和扩充,达到软
件可重用的目的。

10.1.2 C++的类

C + +语言是一种面向对象的程序设计语言,是对传统 C语言的完善和扩充,并支持面向对
象的概念:对象、类、方法、消息和继承,下面给出一个 C++关于类的结构:
[例10-1] 栈操作。栈是一种后进先出的数据结构,我们利用数组这个静态的存储来实现
栈、充当栈,完成数据的压栈和出栈。
#include "iostream.h"
#define SIZE 100
// 定义栈类型
class stack /* 定义类*/
{
int stck[SIZE]; /* 数据成员,整型数组做堆栈 */
int top; /* 栈顶指针*/
public:
void init(void); /* 初始化成员函数*/
void push(int i); /* 压栈*/
int pop(void); /* 出栈*/
};
// 栈的初始化
void stack:::init(void) /* 类成员函数的定义 */
{
top=0; /* 定义栈顶指针指向数组头*/
}
//进栈操作
void stack::push(int i)
{
if (top= =SIZE)
{
cout<<"The stack is full!"; /* 栈满*/
return;
下载
第10章 C++入门 207
}
stck[top]=i; /* 压入数据到栈顶 */
top++; /* 指针加1*/
}
// 出栈操作
int stack::pop(void)
{
if (top= =0)
{
cout<<"The stack is underflow!"; /* 栈空,无数据 */
return 0;
}
top- -; /* 指针减1*/
return stck[top]; /* 返回栈顶元素 */
}
void main(void)
{
stack stack1, stack2; //创建对象,
stack1.init(); /*调用成员函数,对栈初始化 */
stack2.init();
stack1.push(1); //在stack1 栈中,压栈1。
stack2.push(2); //在stack2 栈中,压栈2。
stack1.push(3);
stack2.push(4);
cout<<stack1.pop()<<" "; 输出s // tack1 栈顶数据,即弹出
cout<<stack2.pop()<<" ";
cout<<stack1.pop()<<" ";
cout<<stack2.pop()<<"\n ";
}

在该程序中,定义了一个对象类型 s t a c k,我们称为类。类 s t a c k由数据成员(变量 s t c k,


t o p)和操作成员函数(函数 i n i t,p u s h,p o p)两部分构成,这种结构类似于 C语言中的结构
体类型。不同的是,将 C中结构体类型的描述符 struct 替换成了描述符 c l a s s。s t a c k就是类的类
型标识符。类 s t a c k将全部信息(数据或变量、方法或函数)都封装在其自身之内,这是 C + +
语言的最基本的特征。有了类(对象类型) stack,我们就可以定义对象(对象变量),如程序
中定义的 s t a c k 1和s t a c k 2,并在对象 s t a c k 1和s t a c k 2之上进行各种不同的操作(初始化、进栈、
出栈)。
上述程序运行后输出为: 3 4 1 2

10.2 C++的输入与输出

在C + +的程序设计中,除继续使用 C语言中的标准库函数(如 p r i n t f,s c a n f)进行输入输


出外,C++还提供自己的输入输出方式。
[例10-2] 显示一行输出。
#include <iostream.h>
main()
208 C语言程序设计
下载
{
cout<<"Hello ,World !";
}

C++标准流的输入输出可以在 iostream.h文件中找到。cout是C++中与标准输出设备相关的
输出流,“< <” 是运算符,该运算符完成将引号内的字符串写到标准输出流 c o u t,简单地说,
就是将字符串“ H e l l o,Wo r l d !”写到标准输出设备—显示器上,为此,运行程序,我们将
在屏幕上看到:
Hello ,World!

[例10-3] 利用海伦公式,输入三角形的三条边,若满足任意两边之和大于第三边,则计
算出给定三角形的面积。
#include <iostream.h>
#include <math.h>
main()
{
float s,s1 ,a1 ,a2 ,a3; // a1,a2 ,a3是三角形的三条边; s 是三角形的面积;
// s1是二分之一的周长
cout<<"a1=";
cin>>a1; /* 键盘输入*/
cout<<"a2=";
cin>>a2;
cout<<"a3=";
cin>>a3;
if (((a1+a2)>a3)&&((a1+a3)>a2)&&((a2+a3)>a1))
//任意两边之和应大于第三边
{
s1=(a1+a2+a3)/2;
s=sqrt(s1*(s1-a1)*(s1-a2)*(s1-a3)); // 计算面积
cout<<"area="<<s;
cout<<"\n";
}
else
cout<<"error!";
return 0;
}

程序中, c i n是C + +的标准输入流;“ > >”是运算符,该运算符完成从标准输入流(通常


cin与键盘相连)中读取数据。运行该程序后,可以看到如下的显示信息:
a1=3↵
a2=4↵
a3=5↵
area=6

10.3 类与对象

客观世界中的事物都包含属性和行为两个方面。在C++程序设计中,对事物的描述分别用数
据成员和成员函数来表现,并把它们封装起来,形成一个抽象的数据类型—类。这就是说,类
下载
第10章 C++入门 209
具有两种成员:数据成员和成员函数,按照面向对象的概念,成员函数又称为方法函数或方法。

10.3.1 类的定义与对象的引用

1. 类的定义
类定义的基本格式如下所示:
class 类型名
{
private:
私有成员声明;
protected:
保护成员声明;
public:
公有成员声明;
}
类成员分为私有成员和公有成员两部分。外界不能访问一个对象的私有成员,只能与对
象的公有成员之间进行信息交换。定义类即是确定选择成员并区分它们的访问权限。
[例10-4] C++程序结构示例。
#include <stdio.h>
class exam1 // 定义类
{
private: // 类的私有成员
int x,y; //数据成员
public: // 类的公有成员 类的声明部分
void init(); // 成员函数
float average();
void print();
};
void exam1::init() // 类成员函数的定义
{
x=3;
y=4;
}
float exam1::average() //类成员函数的定义 类的实现部分
{return (x+y)/2.0;
}
void exam1::print() // 类成员函数的定义
{
printf("\nx=%d ,y=%d ,aver=%7.2f\n" ,x,y,average());
}
main() // 主函数
{
exam1 obj; //声明并创建一个对象
obj.init(); // 调用成员函数初始化 类的使用
obj.print(); //输出运算结果
return 0;
}
210 C语言程序设计
下载
1) 类在 C++中用关键字 class来说明,紧跟其后的是类名 exam1,类中包含两个私有数据成
员x、y和三个公有的成员函数 init()、average()、print()。
C++允许隐藏内部状态,由 private开始的私有段成员就满足这一特性,它们只能允许该类
对象的成员函数来访问。类中的公有段既可以是数据成员,也可以是成员函数,这是类提供
给外部的接口。当然, C++还提供另一种保护段符号: protected ,下面会介绍到。
运行该程序,得到的输出显示为:
x=3 , y=4 , aver= 3.50
2) 类的成员在类的定义中出现的顺序可以任意,并且类的实现既可以放在类的外面,又
可以内嵌在类内,下面调整类成员的顺序为:
class exam1 // 定义类
{
public:
float average();
void print();
private:
int x,y;
public:
void init();
};

3) 若类的实现定义在类的外面,在成员函数的函数头中函数名前,应使用作用域限定符::
指明该函数是哪一个类中的成员函数,即有如下格式。
类型 类名::成员函数名(参数表)
{
函数体
}
4) 除特殊指明外,成员函数操作的是同一个对象的数据成员。下面的示例将类成员函数
的实现内嵌在类中:
class exam1 //定义类
{
public:
float average()
{
return (x+y)/2.0;
}
void print()
{
printf("\nx=%d ,y=%d ,aver=%7.2f\n" ,x,y,average());
}
private:
int x,y;
public:
void init()
{
x=3;
y=4;
}
};
下载
第10章 C++入门 211
5) 类定义的最后一个花括号的外面一定要有分号结束。程序中出现的“ //”符号是作为注
释开始的标志,与 C语言中“/* ...... */”用法完全相同。
6) 使用public、private和protected关键字
public、private和protected关键字称为访问说明符。
说明为p u b l i c的类成员可以被任何函数所使用(当它们是数据成员时)或调用(当它们是
成员函数时)。调用者不必属于这个类或任何类。
说明为 p r i v a t e的类成员只能被同类中的成员函数所使用或调用,也可以被同类的友元使
用或调用。在类的成员中,若没有额外声明访问权限,则表明为 private的类成员。
说明为 p r o t e c t e d的类成员只能被同类中的成员函数或同类的友元类,或派生类的成员函
数及友元类所使用或调用。
2. 类与对象
上述类e x a m 1提出了两个概念:类与对象。从形式上看,类与对象的关系类似于 C语言中
的数据类型与变量的关系,类是将具有相同属性和行为的事物做一个概括,它是普遍意义上
的一般概念,而对象是具有类特征的具体事物。一旦定义了类,那么就有无数的具有该属性
和行为的对象与之对应。
类在概念上是一种抽象机制,它抽象了一类对象的存储和操作特性;在系统实现中,类
是一种共享机制,它提供了一类对象共享其类的操作实现。
类是对象的模板,对象承袭了类中的数据和方法,只是各对象具有的初始化数据不同,
所表示的对象状态也不同。
一个C + +文件可以作为一个文件存储,其文件的扩展名为“ . c p p”,也可以作为几个文件
存储。若作为几个文件存储,一般说来应把类的声明部分存于“ . h”的头文件中,而把类的
实现部分和类的使用部分分别存于扩展名为“ . c p p”的文件中。包含主函数的 . c p p文件中应包
含.h和其它.cpp文件。规模较大的程序,应采用模块化的程序设计技术。
C + +程序的编辑、编译、连接及运行的方法和过程,在 D O S 下,与 C语言基本一样。
Borland C++或是Turbo C++,均有一个集成开发环境,易学易用(与 Turbo C大同小异),操
作非常方便。

10.3.2 构造函数与析构函数

C + +中,类是一种数据类型,这样的类型总与存储空间相关,即要占用一定的内存资源。
当我们定义了对象时,编译系统就会为它分配存储,进行一定的初始化,由于类的结构各不
相同,所需的工作量也各不相同,为此, C + +提供构造函数来完成上述工作。构造函数是属
于某一特定类,可由用户设置,也可用系统缺省设置。与之相对应的是类的析构函数,当类
的对象退出作用域时,析构函数负责回收存储空间,并做一些必要的善后处理。析构函数也
是属于某一特定类,可由用户设置,也可用系统缺省。
1. 构造函数
当定义一个对象时,我们需要给对象开辟一个存储空间,将对象的数据成员初始化。在
使用构造函数以前,首先对构造函数作如下说明:
1) 构造函数具有与类名相同的函数名。
2) 构造函数没有返回类型,即使void也不可以。它的返回值是隐含的,是指向类本身的指针。
212 C语言程序设计
下载
3) 构造函数在对象被定义时自动调用,作相应的初始化。
4) 构造函数可以有参数,也可无参数。
5) 构造函数名可以重载。
6) 当类中无与类名相同的构造函数时, C++编译系统为其设置缺省的构造函数。
[例10-5] 构造函数应用举例
#include <stdio.h>

class A{
int a,b,c; // 缺省访问权限,为私有数据成员
public: // 公有段
A(int=1 ,int=2 ,int=3); // 构造函数1
A(double ,double ,double); // 构造函数2
A(long); // 构造函数3
A(A&); // 构造函数4(拷贝构造函数)
void show() // 公有成员函数
{
printf("%d , %d , %d\n" ,a,b,c);
}
};
A::A(int I1,int I2,int I3) // 构造函数1的实现
{
a=I1; b=I2; c=I3;
}
A::A(double f1,double f2,double f3) // 构造函数2的实现
{
a=(int)f1; b=(int)f2; c=(int)f3;
}
A::A(long n) // 构造函数3的实现
{
a=b=c=(int)n;
}
A::A(A& other) // 构造函数4的实现
{
a=other.a;
b=other.b;
c=other.c;
}
main()
{
A x1; // 定义对象x1,调用缺省参数的构造函数1
x1.show(); // 调用公有段成员函数 show()
A x2(3); // 定义对象x2,调用构造函数 1
x2.show(); // 调用公有段成员函数 show()
A x3(3,1); // 定义对象x3 ,调用构造函数 1
x3.show(); // 调用公有段成员函数 show()
A x4(3.14,2.414 ,6.28); // 定义对象x4 ,调用构造函数 2
x4.show(); // 调用公有段成员函数 show()
A x5(53L); // 定义对象x5 ,调用构造函数 3
x5.show(); // 调用公有段成员函数 show()
下载
第10章 C++入门 213
A x6=x5; // 定义对象x6,调用拷贝构造函数 4
x6.show(); // 调用公有段成员函数show()
return 0;
}

运行上述程序,得如下输出:
1,2,3
3,2,3
3,1,3
3,2,6
53 ,53,53
53 ,53,53

可以提供不带参数的构造函数,即是缺省的构造函数。例如:
class A{
...
A();
...
};
A::A ()
{
a=0; b=0; c=0;
}

但要注意的是,不能将可缺省参数的构造函数与缺省的构造函数一起使用,以免编译系
统混淆。例如:
class A{
...
A( );
A(int=1 , int=2 , int=3);
...
};
void main( )
{
A obj; // 编译系统无法区分应调用哪一个构造函数
...
}

2. 析构函数
与构造函数对应的是析构函数。 C++用析构函数来处理对象的善后工作,在对象撤销时自
动调用,并可能要释放一些动态的存储空间等。析构函数具有如下的一些特点:
1) 与类同名,之前冠以波浪线,以区别构造函数。
2) 不指定返回类型。
3) 不能指定参数。
4) 一个类只能有一个析构函数。
析构函数可以这样写:
class A{
...
214 C语言程序设计
下载
public:
...
~
A();
};
A::A(){...} // 析构函数定义

[例10-6] 我们将构造函数进行编号,在程序的运行中去发现对象被定义后,调用的构造
函数及对象被撤销时,调用的析构函数的处理过程。
#include <stdio.h>
class A{
int a,b,c,number; // 类的私有成员,number 用于标志构造函数
public:
A(int=1 ,int=2 ,int=3); // 类的构造函数
A(double ,double ,double);
A(long);
A(A&);
~A(){ // 类的析构函数
,number);
printf("object x destroyed by constr %d created\r\n"
}
void show()
{
printf("%d , %d, %d\n" ,a,b,c);
}
};
A::A(int i1,int i2,int i3) // 构造函数1
{
a=i1; b=i2; c=i3; number=1;
}
A::A(double f1,double f2,double f3) // 构造函数2
{
a=(int)f1; b=(int)f2; c=(int)f3; number=2;
}
A::A(long n) // 构造函数3
{
a=b=c=(int)n; number= 3;
}
A::A(A& other) // 拷贝的构造函数4
{
a=other.a;
b=other.b;
c=other.c;
number=4;
}
main()
{
A x1; // 定义对象X1 调用构造1
x1.show();
A x2(3); // 定义对象X2 调用构造1
x2.show();
A x3(3,1); // 定义对象X3调用构造1
x3.show();
下载
第10章 C++入门 215
A x4(3.14,2.414 ,6.28); //定义对象X4 调用构造2
x4.show();
A x5(53L); //定义对象X5 调用构造3
x5.show();
A x6=x5; // 定义对象X 6调用构造4
x6.show();
return 0; //各对象调用相应的析构函数。
}

程序的输出:
1,2,3
3,2,3
3,1,3
3,2,6
53 ,53,53
53 ,53,53
object x destroyed by constr 4 created
object x destroyed by constr 3 created
object x destroyed by constr 2 created
object x destroyed by constr 1 created
object x destroyed by constr 1 created
object x destroyed by constr 1 created

从上述输出结果来看,对象一旦定义,就必须要调用构造函数,退出时要调用析构函数。
先定义的对象最后被毁灭或后定义的对象最先使用析构函数。

10.3.3 函数重载

上述程序中出现的构造函数,在形式上看,参数各不相同。正是由此,被定义的对象根
据参数形式的不同,调用不同的构造函数,这个过程我们称为函数的重载。当然,不仅构造
函数可以重载,其它的类成员函数也同样可以重载。
[例10-7] 类的成员函数的重载。
#include <stdlib.h>
#include <string.h>
#include <iostream.h.>
class string{
int length;
char str[256];
public:
string(){ length=0; ,"");}
strcpy(str
// 类的构造函数 1,完成字符串的初始化
string(char *); //重载的构造函数 2
char * search(char); // 返回字符指针的成员函数1
char * search(char *); // 返回字符指针的重载成员函数 2
};
string::string(char *text) // 构造函数2的定义
{
if (strlen(text)<256) // 若串text 的长度小于256
strcpy(str ,text); // 将串text 复制给串str
216 C语言程序设计
下载
else
strncpy(str ,text ,255); // 若串text 的长度超过255
// 则将串text 的255 个字符复制给串 str
length=strlen(str);
}
char *string::search(char arg)// 成员函数1的定义
{
return strchr(str,arg); // 返回串str 中第一次出现字符 arg 的位置
}
char *string::search(char *arg)// 重载成员函数2的定义
{
return strstr(str,arg); // 返回串str 在串arg 中第一次出现的位置
}
void main()
{
string hellomsg='Hello ,there ,I'm a string!';
// 定义串string 的对象,自动调用构造函数1
char *found; // 定义字符串found
cout<<hellomsg.search('t')<<"\r\n";
// 输出对象的成员函数 1的返回值
cout<<hellomsg.search("string")<<"\r\n";
// 输出对象的成员函数 2的返回值
}

运行程序,输出为 :
Hello, there,I'm a string!
string!

10.3.4 友元

在类成员的介绍中,我们对类的三种成员做过详细的说明,特别是强调了类的私有成员
和类保护段成员的隐蔽性,它们只能被类对象的成员函数所访问,这也同样表明,类的成员
函数可以访问类中的所有成员。友元是 C + +提供给外部的类或函数访问类的私有成员和保护
成员的一种途径。
在一个类中,将friend加在某个函数或某个类的前面,则该函数或类将成为所在类的友元。
友元不受它们在类中出现次序的影响,而仅表明其是类的友元。
[例10-8] 说明类的友元函数的作用及与成员函数的区别。
#include <iostream.h>
#include <string.h>
class stud{
char *name,*num ,*tel; // 类的私有成员:姓名,学号,电话号码
public:
stud(char *na,char *nu, char *pho ) // 类的构造函数
{
name=new char[strlen(na)+1];
//使用运算符 new 向系统申请 name 所需的存
储空间
strcpy(name ,na); // 将参数na的值复制给姓名 name
下载
第10章 C++入门 217
num=new char[strlen(nu)+1];
// 使用运算符new 向系统申请num 所需的存储空间
strcpy(num ,nu); // 将参数nu的值复制给学号 num
tel=new char[strlen(pho)+1];
// 使用运算符new 向系统申请tel 所需的存储空间
strcpy(tel ,pho); // 将参数pho 的值复制给电话号码 tel
}
void show(stud&); // 类的公有成员函数
friend void show(stud&); // 类的友元函数
~stud() // 类的析构函数
{ delete name; delete num; delete tel;}
// 使用运算符delete 释放类的各数据成员所占存储空间
};
void show(stud &student) //友元函数的定义
{
cout<<" 类的友元函数的调用:\n";
cout<<"student\"s name is:"<<student.name
<<"\nstudent\"s number:"<<student.num
<<"\nstudent\"s telephone:"<<student.tel<<"\n";
cout<<" 友元函数调用类的成员函数: \n";
student.show(student); //调用类的成员函数
}
void stud::show(stud &student) // 类的成员函数的定义
{
cout<<" 类的成员函数的调用:\n";
cout<<"student\"s name is:"<<student.name
<<"\nstudent\"s number:"<<student.num
<<"\nstudent\"s telephone:"<<student.tel<<"\n";
}
void main()
{
stud x("li-ling" ,"j98-10323" ,"0285533123"); // 定义类的对象 x
show(x); //调用类的友元函数
x.show(x); // 调用类的成员函数
}

上述程序中,类的友元函数和类的成员函数完成相同的功能,并且函数名也完全相同,
不同的只是说明和定义的形式。让我们先看一下程序运行后的输出:
类的友元函数的调用:
student\'s name is: li-ling
student\'s number: j98-10323
student\'s telephone: 0285533123

友元函数调用类的成员函数:
student\'s name is: li-ling
student\'s number: j98-10323
student\'s telephone: 0285533123

类的成员函数的调用:
218 C语言程序设计
下载
student\'s name is: li-ling
student\'s number: j98-10323
student\'s telephone: 0285533123

顺便需要说明的是:类的友元函数的定义既可放在类内,也可在类外,与成员函数是一
致的。如放在类内,则不受段访问特性的影响。
友元可以是多个类的友元,即可以跨类访问。
[例10-9] 关于跨类友元的访问,程序能验证其各位数的立方和等于其数本身的任一个三
位数,如: 13+53+33=150等。
#include <iostream.h>
class hundreds; // 类的声明。在类未定义前需使用时,必须先声明。
class cubic // 类cubic 的定义
{
int sum;
public:
void set_sum(int a,int b,int c){ sum=a*a*a+b*b*b+c*c*c;}
// 类的成员函数用于求其各位数的立方和。
friend int equal(cubic ,hundreds
c h); // 类的友元函数
} cub; // 在类定义的同时定义对象cub
class hundreds{// 类hundreds 的定义
int num;
public:
void set_num(int a,int b,int c){
num=100*a+10*b+c;
}
// 类的成员函数用于求其三个数构成的三位数
friend int equal(cubic ,hundreds
c h);//类的友元函数
void display(void){cout<<num<<endl;}类的成员函数 //
}hun; // 在类定义的同时定义对象hun
int equal(cubic c,hundreds h){ return !(c.sum-h.num);}
// 类cubic 和类hundreds 的友元函数equal 的定义,用于验证由三个数构成的
main(void)
{
int d3,d2,d1;
for (d3=1;d3<10;d3++)
for (d2=0;d2<10;d2++)
for (d1=0;d1<10;d1++)
{
cub.set_sum(d3 ,d2 ,d1);
// 对象cub 调用其成员函数求得三个数的立方和
hun.set_num(d3 ,d2 ,d1);
// 对象hun 调用其成员函数求得构成的三位数
if (equal(cub,hun)) hun.display(); // 相等则显示出三位数
}
return 0;
}

该程序的输出是:
150
370
371
407
下载
第10章 C++入门 219
一定请注意,友元函数在使用时,与成员函数的使用有所区别,它不能在函数名前加上
类名作函数调用的限定,即不能写成 cubic::equal或hundreds::equal。同时,我们也可以将类声
明为友元。
class Y; //类的声明
class X{
friends Y; //说明类Y是类X的友元
int data1;

void fun_mem1(…){ …}

};
class Y{
float data2;

void fun_mem2(…){ …}

};

上述定义形式表明,类 Y的全部成员均可访问类 X的成员。

10.4 对象指针
当我们需要动态地在存储空间上使用对象时,就需要定义对象指针。正如在 C语言中使用
指针变量一样,什么时候需要,就使用 m a l l o c ( )为其分配内存,使用完毕,就利用 f r e e ( )释放
其占用的内存。
1. 运算符new与delete
在C + +中,运算符 n e w和d e l e t e可以让用户创建任意持续时间的动态变量,也可以让用户
为任意种类的 C语言的对象分配内存空间。通常,我们用 n e w来为C或C+ +的对象动态地分配
存储空间,用 delete来将占用内存的对象移走或称为释放,其意义与 C语言提供的 malloc()功能
与free()功能类似。
运算符new和delete的用法很简单,它们均为单目运算符:
例:我们定义对象指针如下:
int *num1;
float *num2;
double *num3
char *str1;
class X{…}*objx;
为对象指针分配内存空间:
num1=new int;
num2=new float;
num3=new double;
str1=new char[80];
objx=new X;
利用new运算符分配空间的对象,使用完毕后应通过 delete对所占空间进行释放。
220 C语言程序设计
下载
delete num1;
delete num2;
delete num3;
delete []str1;
delete objx;

2. 动态地创建类对象
生活中常会遇到的实际问题是处理的数据量不固定,数据的个数在动态地改变。链表技
术就是为解决这一问题应运而生的,这种特定的数据结构中每一个节点用于存放一组数据,
随着数据的不断增加,链表也在不断增长,占用的内存不断增加。当问题得到解决后,数据
不再需要保存,占用的内存就释放掉,这个过程就是数据的动态存储。
[例10-10] 动态存放一组字符串在一双向链表,链表结构如图 10-2所示。

head
NULL prev prev

nodebody nodebody nodebody

nodenum nodenum nodenum

next next NULL

图10-2 链表结构示意

其中,h e a d是头指针,指向链表的头; p r e v指向链表的前一个节点; n e x t指向链表的下一


个节点;nodebody是链表节点存放的字符串; nodenum是链表节点存放的字符串长度。
链表节点的结构为:
class tnode{
tnode *prev;
tnode *next;
char * nodebody;
int nodenum;
}

双向链表的操作有:
1) 移动指针到链表头。
2) 移动指针到链表尾。
3) 在链表尾部追加一个字符串。
4) 从链表开始,按字母的排列顺序插入一个字符串。
5) 释放链表各节点所占内存。
程序如下:
#include "stdlib.h"
#include <stdio.h>
#include <string.h>
#include <conio.h>
class tnode{ // 链表节点的数据结构
public:
tnode *prev; // 指向前一个节点
下载
第10章 C++入门 221
tnode *next; //指向下一个节点
char *nodebody; // 节点所存字符串
int nodenum; // 节点字符串的长度
};
class dbllist{ // 链表结构
tnode *head; // 链表的头指针

tnode *base;
tnode *hold; //base 与hold 用于跟踪链表增长
tnode *create(char *); //为一个节点分配存储空间
public:
dbllist(); // 链表的构造函数
~dbllist(); // 链表的析构函数
void clear(); // 释放链表所占空间
tnode *gohead(); //将指针移到链表头
tnode *gotail(); // 将指针移到链表尾
tnode *gonext(); // 将指针移到链表的下一个节点
tnode *goprev(); //将指针移到链表的前一个节点
tnode *append(char *); // 追加一个字符串
tnode *insert(char *); // 插入一个字符串
char* accept(tnode *); //接收指定节点的字符串
};
dbllist::dbllist()
{
head=base=hold=NULL; // 链表指针初始化
}
dbllist::~dbllist() //析构函数
{
clear();
}
void dbllist::clear() // 释放链表各节点
{
base=head; // 头指针
while(base)
{ // 链表非空
hold=base->next;
// 删除节点如
// 图10-3 所示
delete base;
base=hold; //在跟踪链表的过程中释放各节点
}
head=base=hold=NULL;
}
tnode * dbllist::gohead() //将指针移到链表头
{
base=head;
if (base) return base; // 返回头指针
else return NULL;
}
tnode * dbllist::gotail() //将指针移到链表尾
{
if (base)
222 C语言程序设计
下载
{
while(base->next) base=base->next; // 跟踪链表到尾,返回尾指针
return base;
}
else return NULL;
}
tnode* dbllist::gonext() // 将指针移到链表的下一个节点
{
if (base)
{
if (base->next)
{
base=base->next; // 节点指针后移
return base;
}
else return NULL;
}
else return NULL;
}
tnode * dbllist::goprev() // 将指针移到链表的前一个节点
{
if (base)
{
if (base->prev)
{
base=base->prev; // 节点指针前移
return base;
}
else return NULL;
}
else return NULL;
}
tnode* dbllist::append(char * str) // 在尾部追加一个字符串
{
tnode *temp;
if((temp=create(str))==NULL) // 申请创建一个新节点(分配存储空间)
return NULL;
gotail(); // 找到尾节点
if (!base)
{
head=base=temp; // 链表无节点,连接到头
}
else
{ // 追加到尾部
base->next=temp;
temp->prev=base;
base=temp;
}
return base;
下载
第10章 C++入门 223
}
tnode * dbllist::insert(char* str) //插入一个字符串
{
tnode *temp;
gohead(); //指向链表头
if (!base) return(append(str)); // 若是空链表,直接追加后返回
if ((temp=create(str))==NULL) // 申请一个新节点 temp
return NULL;
while (base->next&&memcmp(str ,base->nodebody ,strlen(str)+1)>0)
base=base->next;
// 若当前节点不是尾,同时被插字符串按字母表顺序排在该节点的前面,则指针后移
if (!base->next&&memcmp(str ,base->nodebody ,strlen(str)+1)>0)
{ // 插入位置是链表尾。
base->next=temp;
//插入链表尾的操作如图 10-4
//
//
temp->prev=base;
base=temp;
}
else
{ // 非尾节点,将新插节点连结到链表内
hold=base->prev;
temp->prev=hold;
temp->next=base;
base->prev=temp;
if (!hold) head=temp; // 插入位置在表头
else hold->next=temp;
base=temp;
}
return base;
}
tnode * dbllist::create(char* str) //为新节点分配空间
{
hold=new tnode; // 申请新节点
hold->nodebody=new char[strlen(str)+1];
// 申请插入字符串所占空间
memmove(hold->nodebody ,str ,strlen(str)+1);// 复制字符串到该节点
hold->prev=hold->next=NULL; // 该节点的指向前后的指针为空
hold->nodenum=strlen(str)+1; // 节点字符串的长度
return hold;
}
char* dbllist::accept(tnode *ptr) //返回节点字符串的值
{
return ptr->nodebody;
}
void main() // 主程序
224 C语言程序设计
下载
{
tnode *pointer=NULL; // 节点类对象指针
dbllist lex; // 链表类对象
clrscr(); // 清屏幕
lex.append("aaaaa"); // 追加字符串
lex.append("bbbbbbbb");
lex.append("cccccc");
lex.append("aaaaaaaaaaaa");
pointer=lex.gohead(); // 得到链表头指针
while(pointer)
{ // 非空链表,顺序输出字符串
printf("%s\n" ,lex.accept(pointer));
pointer=lex.gonext();
}
pointer=lex.gotail(); // 得到链表尾指针
while(pointer)
{ // 非空链表,从后向前输出字符串
printf("%s\n" ,lex.accept(pointer));
pointer=lex.goprev();
}
lex.clear(); // 释放链表
lex.insert("xxxxxx"); // 按字母表顺序插入字符串
lex.insert("yyyyyy");
lex.insert("zzzzzz");
lex.insert("aaaaaaa");
pointer=lex.gohead();
while(pointer)
{
printf("%s\n" ,lex.accept(pointer));
pointer=lex.gonext();
}
pointer=lex.gotail();
while(pointer)
{
printf("%s\n" ,lex.accept(pointer));
pointer=lex.goprev();
}
}

base
next Prev next
base next next
hold
temp

图10-3 删除节点 图10-4 在链表尾插入一个节点

程序运行后,输出为:
aaaaa
bbbbbbbb
cccccc
aaaaaaaaaaaa
aaaaaaaaaaaa
下载
第10章 C++入门 225
cccccc
bbbbbbbb
aaaaa
aaaaaaa
xxxxxx
yyyyyy
zzzzzz
zzzzzz
yyyyyy
xxxxxx
aaaaaaa

程序按不同接入链表的方法,将会按正序和逆序输出各节点所存字符串。
最后,我们再重申, new和delete的用法为:
obj_ptr=new obj_type(new_initializer);
delete obj_ptr;
delete [ ]obj_ptr;

其中,obj_ptr是对象指针或是变量指针; obj_type是类类型或变量类型;若 new_initializer


不空,则表示分配空间的大小。若 o b j _ p t r是指针,则通过“ delete obj_ptr;”来释放空间;若
obj_ptr是指针数组,则通过“ delete [ ]obj_ptr;”来释放所占内存空间。

10.5 派生类与继承类

在C++中派生类是指从某一类派生出新类的过程,它能继承基类或父类的属性和功能,所
以,我们也称派生类为继承类。派生或继承的过程类似与我们在 C语言中的一些代码的可重用。
各种C的编译版本事先为使用者开发出尽可能多的标准函数,以方便用户使用,使用者无需了
解函数实现的具体细节,就能方便灵活地使用。
在软件的开发过程中,要充分利用系统提供的各种资源,以减少开发人员的劳动。 C++对
系统或用户开发的代码即类的实现补充了更为广大的发展空间,既可以做类代码的再利用,
也可以做包含继承性的类的派生;既可以做某一个类的继承或派生,也可以做多个类的继承
或派生,这就是我们要谈到的单继承的派生和多继承的派生。

10.5.1 单继承的派生类

通过基类或父类继承产生新类的过程称派生,新类则称为派生类,旧的代码或旧类称为
基类。
从一个类派生出另一个类的语法非常简单:
class base{……};
……
……};
class derived:base{
……

对于类b a s e来说,它作为基类,应有完整的定义和说明,只有名字,不能作为一个基类。
所有基类成员相对派生类来说都是局部成员,换句话说,对派生类是隐蔽的、不可访问的,
如果需要对基类成员进行访问,则需在基类的类名前加上访问限制符如下:
class base{……};
226 C语言程序设计
下载
……
……};
class derived:public base{
……

这种派生或继承的方法也称为公有派生。存取访问限制符分为 p u b l i c、p r i v a t e、p r o t e c t e d


三种,也就是对基类的派生或继承有三种,分别说明如下。
1. 声明一个基类为 public
存取访问符 p u b l i c使基类成员保持基类成员的特性,原来是 p u b l i c的成员,在派生类中继
续保持公有,原为 p r i v a t e的成员, 在派生类中继续保持私有,原来是 p r o t e c t e d的成员,在派生
类中继续保持其保护特性。
2. 声明一个基类为 private
存取访问符 p r i v a t e使基类成员中原为 p u b l i c和p r o t e c t e d的成员派生为私有成员,而原为私
有的成员对派生类来说,则是隐蔽的,不透明的,不可访问的。
3. 声明一个基类是 protected
存取访问符 p r o t e c t e d使基类成员中原为 p u b l i c的成员成为派生类中的 p r o t e c t e d成员,原为
protected的成员成为派生类中的 private成员。
[例1 0 - 11] 利用一单链表派生为一堆栈,实现堆栈的出入功能。我们通过创建一个工程
项目来实现:
list.h
stack.prj
stack.h
list.cpp
stack.cpp
exam.cpp
接下来分段介绍:
头文件list.h清单:
const int Max_elem = 10;
class List // 定义单链表
{
int *list; // 整型数组
int nmax; //数组大小
int nelem; //数组下标
public:
List(int n = Max_elem) {list = new int[n]; nmax = n; nelem = 0;};
// 单链表的构造函数(长度为 10 的整型数组)
~List() {delete list;}; // 析构函数
int put_elem(int, int); // 成员函数
int get_elem(int&, int);
void setn(int n) {nelem = n;};
int getn() {return nelem;};
void incn() {if (nelem < nmax) ++nelem;};
int getmax() {return nmax;};
void print();
};
下载
第10章 C++入门 227
文件list.cpp清单:
#include <iostream.h>
#include "list.h"
int List::put_elem(int elem, int pos) // 在数组的指定位置安放一整数
{
if (0 <= pos && pos < nmax)
{
list[pos] = elem; 安放一整数
//
return 0;
}
else
return -1; //出错返回
}
int List::get_elem(int& elem, int pos) //在指定位置获取元素
{
if (0 <= pos && pos < nmax)
{
elem = list[pos]; //取数组元素
return 0;
}
else
return -1; //出错返回
}
void List::print() // 顺序输出数组元素
{
for (int i = 0; i < nelem; ++i)
cout << list[i] << " ";
cout<<"\n"
}

派生类stack的定义,文件stack.h清单:
#include "list.h"
class Stack : public List //关于基类的共有派生
{
int top; // 栈顶指针
public:
Stack() {top = 0;}; //构造函数
Stack(int n) : List(n) {top = 0;};
// 重载的构造函数,包括对基类的初始化
int push(int elem); // 压栈
int pop(int& elem); //出栈
void print(); // 输出
};

派生类stack的成员函数定义,文件 stack.cpp清单:
#include <iostream.h>
#include "stack.h"
int Stack::push(int elem) //压栈
228 C语言程序设计
下载
{
int m = getmax();
if (top < m)
{
put_elem(elem,top++);
return 0;
}
else
return -1;
}
int Stack::pop(int& elem) // 出栈
{
if (top > 0)
{
get_elem(elem,--top);
return 0;
}
else
return -1;
}
void Stack::print() // 输出
{
int elem;
for (int i = top-1; i >= 0; --i)
{ // 按先进后出的顺序
get_elem(elem,i);
cout << elem << " ";
}
cout<<"\n"
}

最后给出main函数exam.cpp实现堆栈的操作:
#include "stack.h"
main()
{
Stack s(5); // 定义堆栈Stack 类的对象s ,堆栈大小为5
int i = 0;
// 插入1~5
while (s.push(i+1) == 0)
++i;
s.print(); // 输出
return 0;
}

程序运行的结果为 :
5 4 3 2 1

上述程序在定义类对象 s时,调用了类的构造函数 Stack(int n) : List(n) {top = 0;}进行初始


化,由于是派生类,还需对基类进行初始化,当然需要调用基类的构造函数 List(int n =
下载
第10章 C++入门 229
Max_elem) {list = new int[n]; nmax = n; nelem = 0;}使其基类初始化为能存放 5个元素的整型数
组作为堆栈。通过对基类的公有派生过程,类 s t a c k实际具有成员相当于基类的全部成员与派
生类定义的成员总和,我们同样可以在派生类中对基类成员进行调用,只要满足派生类对基
类的访问要求即可。下面对程序的 m a i n函数进行修改,充分利用派生类中的各成员。修改后
的exam.cpp如下:
#include "stack.h"
main()
{
Stack s(5);
int i = 0;
// Insert the numbers 1 through 5
while (s.push(i+1) == 0)
++i;
s.print();
List l(5); //定义基类对象并初始化
i = 0;
// 调用基类成员写入 1~5 在整型数组中
while (l.put_elem(i+1,i) == 0)
++i;
l.setn(i);
l.print(); //输出数组各元素
i = 0;
// 利用派生类对象调用基类成员
while (s.put_elem(i+1,i) == 0)
++i;
s.setn(i);
s.List::print(); //派生类对象访问基类的同名成员函数
return 0;
}

重新运行程序,得输出为:
5 4 3 2 1
1 2 3 4 5
1 2 3 4 5
由于派生类与基类均有 print(),所以为避免冲突通过派生类访问基类的同名函数,则需要
在基类成员的前面加上类的限制符,即“派生类对象 . 基 类 名 :: 成 员 ”,在程序中为
s.List.print().
还有一种情况也是我们应当引起重视的:
class A{......};
class B:public A{......};
class C:public B{......};
这种结构是按层次进行派生,每层派生类都只有一个直接基类,类 C继承类 A和类B的成
员特性,当然受到派生访问控制符的限制。
下面的例子是一个单基多级派生的问题。定义一个基类 Location用于定义点坐标,在此基
础上公有派生出类 Point, 以完成一个定位象素点的输出。由于 Point类继承了基类 Location的坐
标点,我们在此基础之上公有派生类 C i r c l e,利用此坐标点做圆心在屏幕上绘 制一个圆,并
230 C语言程序设计
下载
画出大小不一的、圆心、半径均不同的各种圆。程序设计的思路是:首先定义一个基本类
L o c a t i o n,它是针对一个象素点的坐标及初始化:在此基础上派生一个类 P o i n t,该类具有关
于点的属性及绘制点的基本操作。最后,定义一个公有派生类 Circle画圆。
[例10-12] 做工程项目:

point.h
demo.prj point2.cpp
circle.cpp

文件point.h清单
enum Boolean {false, true}; // 定义枚举类型
class Location { // 基类Location ,用于设置点坐标。
protected: // 允许派生类访问的保护段成员
int X; // 坐标点
int Y;
public: // 允许派生类访问
Location(int InitX, int InitY); // 构造函数
int GetX();
int GetY();
};
class Point : public Location
// 从类Location 一级派生,用于绘制点。
protected:
Boolean Visible; //下级派生类可以访问的 protected 段
public:
Point(int InitX, int InitY); 构造函数
//
void Show();
void Hide();
Boolean IsVisible();
void MoveTo(int NewX, int NewY);
};

类Point是从基类Location公有派生而来,对基类中的 protected 段和public 段成员保持属性


不变。下面在point2.cpp文件中对基类和一级派生类各成员函数进行定义。
文件point2.cpp清单
#include "point.h"
#include <graphics.h>
// 类Location 的成员函数
Location::Location(int InitX, int InitY) { 构造函数
//
X = InitX; // 设置点
Y = InitY;
};
int Location::GetX(void) { // 类Location 的成员函数,返回点的 x
return X;
};
int Location::GetY(void) { // 类Location 的成员函数,返回点的 Y
return Y;
};
下载
第10章 C++入门 231
// 类Point 的成员函数的定义
// 类Point 的构造函数,包括对基类的初始化 .
Point::Point(int InitX, int InitY) : Location(InitX,InitY) {
Visible = false; // make invisible by default
};
void Point::Show(void) {
Visible = true;
putpixel(X, Y, getcolor()); 用当前字符颜色写一个像素点
//
};
void Point::Hide(void) / {/ 擦除一个像素点
Visible = false;
用屏幕背景色画一个像素点
putpixel(X, Y, getbkcolor()); //
};
Boolean Point::IsVisible(void) {
return Visible;
};
void Point::MoveTo(int NewX, int NewY) { // 移动屏幕上的一点
Hide(); // 擦除点
X = NewX; // 改变坐标X和Y到新位置
Y = NewY;
Show(); // 在新位置显示点
};

文件circle.cpp清单
#include <graphics.h> // graphics library declarations
#include "point.h" // Location and Point class declarations
#include <conio.h> // for getch() function
// link with point2.obj and graphics.lib
class Circle : Point { 从类Point 和类Location 的二级派生
//
int Radius; 私有成员
//
public:
Circle(int InitX, int InitY, int InitRadius);
void Show(void);
void Hide(void);
void Expand(int ExpandBy);
void MoveTo(int NewX, int NewY);
void Contract(int ContractBy);
};
Circle::Circle(int InitX, int InitY, int InitRadius) : Point(InitX,InitY)
{
Radius = InitRadius;
};
void Circle::Show(void) // 画圆
{
Visible = true; // 显示标志
circle(X, Y, Radius); 利用标准函数画圆
//
}
232 C语言程序设计
下载
void Circle::Hide(void)
{
unsigned int TempColor; 用于存放当前屏幕色彩
//
TempColor = getcolor(); 读取当前屏幕色彩
//
setcolor(getbkcolor()); 设置当前屏幕色彩为背景色
//
Visible = false;
circle(X, Y, Radius); 删除圆
//
setcolor(TempColor); 恢复当前屏幕色彩
//
};
void Circle::Expand(int ExpandBy) // 放大圆
{
Hide(); 擦除圆
//
Radius += ExpandBy; 修改圆半径
//
if (Radius < 0)
Radius = 0;
Show(); 画圆
//
};
void Circle::Contract(int ContractBy) 缩小圆//
{
Expand(-ContractBy); 利用成员函数修改
//
}; // 圆半径,画圆
void Circle::MoveTo(int NewX, int NewY) 移动圆
//
{
Hide(); 擦除圆
//
X = NewX; 设置新圆心坐标
//
Y = NewY;
Show(); 重画圆
//
};
main() 测试函数
//
{
// 初始化图形系统
int graphdriver = DETECT, graphmode;
initgraph(&graphdriver, &graphmode, "..\\bgi");
Circle MyCircle(100, 200, 50); 定义类circle 的对象
//
MyCircle.Show(); 显示圆
//
getch(); 等待一个按键
//
MyCircle.MoveTo(200, 250); 移动圆心到(200,250 )、重画圆。
//
getch(); // 按一键
MyCircle.Expand(50); 增加圆半径50 ,放大圆
//
getch();
MyCircle.Contract(75); 缩小圆
//
getch();
closegraph(); // 关闭图形系统。
return 0;
}

运行上述程序:会看到一个圆,在键盘上按键,圆移动;再按任一键,圆放大;再按任
一键,圆缩小。
下载
第10章 C++入门 233
10.5.2 多继承的派生类

派生类只有一个基类时,称为单基继承或单基派生;若具有多个基类时,称为多基继承
或多基派生。那么多基继承或多基派生在语法上与单继承有所不同,其语法结构为:
class A{......};
......
class B{......};
......
class c:public A,public B{......};

由类A和类B共同派生出新类 C,这样一来,类 C继承了类A和类B的成员。


在前面的例子中,我们定义了点,并继承点的特性后再画圆。由于派生类只有一个直接
基类,所以称为单继承。
[例10-13] 通过上述画圆的派生过程,我们在圆内写一个字符串,也就是要作多基派生。
先定义一个 G M e s s a g e的类,该类完成在 x和y坐标点处开始写一个字符串,利用前面定义
的C i r c l e的类,显示一个圆。在此基础上,再定义一个新类 M C i r c l e,既要画圆,又要写字符
串,应具有 GMessage类和Circle类的共同特性。见图 10-5。

Class Location
Class Location
int x;
int x;
int y; ....
int y; ....

Class Point: Class GMessage:


Public Location Public Location
{.... {....

Class Circle: Class Mcircle:


Public Point Circle, GMessage
{.... {....

图 10-5

定义项目CIRCLESTR.PRJ Point.h
Point2.cpp
Mcircle.cpp
234 C语言程序设计
下载
/* point.h--Example from Getting Started */
//***********************************************
// point.h 包含两个类:
// class Location
// class Point
enum Boolean {false, true};// 定义枚举类型
class Location { 类定义
//
protected: //可继承的受保护成员
int X;
int Y;
public: //公有成员
Location(int InitX, int InitY);
int GetX();
int GetY();
};
class Point : public Location { // class Location的派生
protected:
Boolean Visible; // 可继承的受保护成员
public:
Point(int InitX, int InitY); // constructor
void Show(); // 显示
void Hide(); // 隐藏
Boolean IsVisible();
void MoveTo(int NewX, int NewY); // 移动
};
/* POINT2.CPP--Example from Getting Started */
//**********************************************
// POINT2.CPP 包含Point 类和Location 类的说明
#include "point.h"
#include <graphics.h>
// Location类的成员函数
Location::Location(int InitX, int InitY) {
X = InitX;
Y = InitY;
};
int Location::GetX(void) {
return X;
};
int Location::GetY(void) {
return Y;
};

// Point类的成员函数
Point::Point(int InitX, int InitY) : Location(InitX,InitY) {
Visible = false; // make invisible by default
};
void Point::Show(void) {
Visible = true;
下载
第10章 C++入门 235
putpixel(X, Y, getcolor()); // uses default color
};
void Point::Hide(void) {
Visible = false;
putpixel(X, Y, getbkcolor()); // uses background color to erase
};
Boolean Point::IsVisible(void) {
return Visible;
};
void Point::MoveTo(int NewX, int NewY) {
Hide(); // make current point invisible
X = NewX; // change X and Y coordinates to new location
Y = NewY;
Show(); // show point at new location
};
// MCIRCLE.CPP
//******************************************************
#include <graphics.h> // Graphics library declarations
#include "point.h" // Location and Point class declarations
#include <string.h> // for string functions
#include <conio.h> // for console I/O
// link with point2.obj and graphics.lib
// The class hierarchy:
//
// (Circle and CMessage)->MCircle
class Circle : public Point { // Location->Point->Circle
// 多重派生
protected:
int Radius;
public:
Circle(int InitX, int InitY, int InitRadius);
void Show(void);
};
class GMessage : public Location
// 在图形屏幕显示字符串
char *msg; 被显示信息
//
int Font; 文字字体
//
int Field; 字型
//
public:
// 构造函数初始化
GMessage(int msgX, int msgY, int MsgFont, int FieldSize,
char *text);
void Show(void); 显示信息
//
};
class MCircle : Circle, GMessage { // 多类继承
public:
MCircle(int mcircX, int mcircY, int mcircRadius, int Font,
char *msg);
void Show(void); 画带字符串的圆
//
236 C语言程序设计
下载
};
// Circle类的成员函数
//Circle 类的构造函数
Circle::Circle(int InitX, int InitY, int InitRadius) :
Point (InitX, InitY) 构造函数的初始化
//
//包括对基类构造函数的初始化
{
Radius = InitRadius;
};
void Circle::Show(void)
{
Visible = true;
circle(X, Y, Radius); //画圆
}
// Gmessage类的成员函数
//Gmessage 类的构造函数的初始化
GMessage::GMessage(int msgX, int msgY, int MsgFont,
int FieldSize, char *text) :
Location(msgX, msgY)
//对基类构造函数的处理
{
Font = MsgFont; // standard fonts defined in graph.h
Field = FieldSize; // width of area in which to fit text
msg = text; // point at message
};
void GMessage::Show(void)
{
int size = Field / (8 * strlen(msg)); // 8 pixels per char.
settextjustify(CENTER_TEXT, CENTER_TEXT); // centers in circle
settextstyle(Font, HORIZ_DIR, size); // magnify if size > 1
outtextxy(X, Y, msg); // display the text
}
// Mcircle类的成员函数
//Mcircle 类的构造函数
MCircle::MCircle(int mcircX, int mcircY, int mcircRadius, int Font,
char *msg) : Circle (mcircX, mcircY, mcircRadius),
GMessage(mcircX,mcircY,Font,2*mcircRadius,msg)
//多继承应处理其多个基类的构造函数
{
}
void MCircle::Show(void)
{
Circle::Show(); //画圆
GMessage::Show(); // 写字符串
}
main() //画圆并写入字符串
{
int graphdriver = DETECT, graphmode;
下载
第10章 C++入门 237
initgraph(&graphdriver, &graphmode, "..\\bgi");
setbkcolor(15); //背景色
setcolor(4); //前景色
MCircle Small(250, 100, 25, SANS_SERIF_FONT, "You");
Small.Show();
MCircle Medium(250, 150, 100, TRIPLEX_FONT, "World");
Medium.Show();
MCircle Large(250, 250, 225, GOTHIC_FONT, "Universe");
Large.Show();
getch();
closegraph();
return 0;
}

运行程序显示为:
下载

附录A 常用字符与ASCII代码对照表
ASCII值 字 符 ASCII值 字 符 ASCII值 字 符

32 [space] 64 @ 96 `
33 ! 65 A 97 a
34 " 66 B 98 b
35 # 67 C 99 c
36 $ 68 D 100 d
37 % 69 E 101 e
38 & 70 F 102 f
39 ‘ 71 G 103 g
40 ( 72 H 104 h
41 ) 73 I 105 i
42 * 74 J 106 j
43 + 75 K 107 k
44 , 76 L 108 l
45 - 77 M 109 m
46 . 78 N 110 n
47 / 79 O 111 o
48 0 80 P 112 p
49 1 81 Q 113 q
50 2 82 R 114 r
51 3 83 S 115 s
52 4 84 T 116 t
53 5 85 U 117 u
54 6 86 V 118 v
55 7 87 W 119 w
56 8 88 X 120 x
57 9 89 Y 121 y
58 : 90 Z 122 z
59 ; 91 [ 123 {
60 < 92 \ 124 |
61 = 93 ] 125 }
62 > 94 ^ 126 ~
63 ? 95 _ 127 *
下载

附录B C库函数
数学函数

使用数学函数时,应在该源文件中使用: #include "math.h"


函 数 名 函数类型和形参类型 功 能 返 回 值 说 明

acos double acos(x) 计计算sin (x)的值


-1
计算结果 计x应在-1~
double x; 1范围内
asin double asin(x) 计计算cos - 1(x)的值 计算结果 计x应在-1~
double x; 1范围内
atan double .atan(x) 计计算tan- 1(x)的值 计算结果
double x;
cos double cos(x) 计计算cos(x)的值 计算结果 计x的单位为
double x; 弧度
cosh double cosh(x) 计计算x的双曲余弦 c o s h ( x ) 计算结果
double x; 的值
exp double exp(x) 计求e x的值 计算结果
double x;
fabs double fabs(x) 计求x的绝对值 计算结果
double x;
floor double floor (x) 计求出不大于 x的最大整数 该整数的双
double x; 精度实数
fmod double fmod(x,y) 计求整数x/y的余数 返回余数双
double x,y; 精度数
frexp double frexp(val,eptr) 计把双精度数 v a l分解为数 返回数字部
double val; 字部分(尾数) x和以 2为 分x0.5≤x<1
int *eptr; 底的指数 n,即 v a l = x×2 n ,
n存放在eptr指向的变量中
log double log(x) 求log ex,即ln x 计算结果
double x;
log10 double log10(x) 计求log 1 0x 计算结果
double x;
modf double modf(val,iptr) 计把双精度数 v a l分解为整 Val的小数部分
double val; 数部分和小数部分,把整
double *iptr; 数部分存到 iptr指向的单元
pow double pow (x,y) 计计算x y的值 计算结果
double x,y;
sin double sin (x) 计计算sinx的值 计算结果 计x单位为弧度
double x;
sinh double sinh (x) 计 计算 x 的双曲正弦函数 计算结果
double x; sinh(x) 的值
sqrt double sqrt (x) 计计算根号下 x 计算结果 计x应≥0
double x;
tan double tan (x) 计计算tan(x) 的值 计算结果 计x单位为弧度
double x;
tanh double tanh (x) 计 计算 x 的双曲正切函数 计算结果
double x; tanh(x)的值
240 C语言程序设计
下载
字符函数和字符串函数

ANSI C 标准要求在使用字符串函数时要包含头文件 ' s t r i n g . h ' ,使用字符函数时要包含


'ctype.h'。
函 数 名 函数类型和形参类型 功 能 返 回 值 包含文件

isdight int isdight(ch) 检检查ch是否是数字 0~9 是,返回 1;不是,返回 0 ctype.h


int ch
islower int islower(ch) 检检查ch是否小写字母 a~z 是,返回 1;不是,返回 0 ctype.h
int ch
isupper int isupper(ch) 检检查ch是否小写字母 A~Z 是,返回 1;不是,返回 0 ctype.h
int ch
strcat char*strcat(str1,str2) 检把字符串str2接到str1的后 str1 string.h
char *str1,*str2 面,str1最后的‘\0’去掉
strcmp int strcmp)(str1,str2) 检比较两个字符号串 str1<str2返回负数 string.h
char *str1,*str2 str1=str2返回0
str1>str2返回正数

strcpy char * strcpy(str1,Str2) 检拷贝str2串到str1中 str1 string.h


char *str1,*str2
tolower int tolower(ch) 检将ch字符转换为小写字符 返回小写字母 ctype.h
toupper int toupper(ch) 检将ch字符转换为大写字符 返回大写字母 ctype.h

输入输出函数

使用以下函数,应在源文件中使用 stdio.h
函 数 名 函数类型和形参类型 功 能 返 回 值

fclose int fclose(fp) 关 关闭 f p所指文 关有错,返回非 0


FILE *fp 件,释放文件缓 关无错,返回 0
冲区
fgetc int fgetc(fp) 关 从 f p所指文件 关 无错,返回所
FILE *fp 读取一个字符 得字符,有错,
返回EOF
fgets int fgets(buf,n,fp) 关 从 f p所指文件 关 返回地址 b u f ,
FILE *fp 读取一个长度为 若遇文件结束或
(n - 1)的字符串, 出 错 , 返 回
存入buf NULL
fopen FILE *fopen(filename,mode) 关以m o d e方式打 关 成功,返回一
char *filename,*mode 开filename文件 个文件指针,失
败,返回0
fprintf int fprintf(fp,format,arg_list) 关把a rg _ l i s t的值 关 输出字符的个
FILE *fp; char *format 以 f o r m a t 指定的 数
格式输出到文件

sputc int fputc(ch,fp) 关 将字符 c h输出 关 成功,返回该
char ch; 到fp所指的文件 字符,失败,返
FILE *fp
下载
附录B C库函数 241
(续)

函 数 名 函数类型和形参类型 功 能 返 回 值

fputs int fputs(str,fp) 将字符串 s t r 写到 成成功,返回 0失


char *str,FILE*fp fp所指文件 败,返回非 0
fread int fread(buf,size,n,fp) 成 从 f p指向的文 成 返回实际读取
char *buf; int size; 件读取n个长度为 的数据项个数。
int n; FILE *fp s i z e的 数 据 项 , 读到文件结束或
存到buf 出错返回 0
fscanf int fscanf(fp,format,arg_list) 成 从 f p所到之处 成 实际输入的数
FILE *fp; char *format; 指的文件按format 据个数
给定的格式输入
数据到 a rg _ l i s t所
指的内存
fwrite int fwrite(buf,size,n,fp) 成将b u f所指向的 成 实际写入的数
char *buf; int size; n个s i z e字节输出 据项的个数
int n; FILE *fp 到fp所指文件
getchar int getchar 成 从键盘输入一 成所读字符
个字符
printf int printf(format,arg_list) 成将输出项arg_list 成 输出字符的个
char *format 的值输出到标准 数
输出设备上
putchar int putchar(ch) 成输出字符 c h 到 成输出字符ch.
char ch 标准输出设备
puts int puts(str) 成输出字符串 st r 成 成功,返回换
char *str 到标准输出设备 行符;失败,返
回EOF
scanf int scanf(format,arg_list) 成 从标准输入设 成读入并赋个 a rg
char *format 备按format格式输 _list数据个数。遇
入数据到 a rg _ l i s t 文件结束,返回
所指内存 EOF,出错返回0

字符屏幕和图形功能函数

字符屏幕处理函数的头部信息在 conio.h中,图形系统的有关函数和原型在 graphics.h中。

函 数 名 函数类型和形参类型 功 能 说 明

arc void arc(x, y, start,end, radius) 以以radius为半径,以 x,y为 graphics.h


int x,y,start,end,radius 圆心,从 s t a r t到e n d画一弧
线
bar void bar(left,top,right,bottom) 以以左上角 l e f t , t o p到右下 graphics.h
int left,top,right,bottom 角right,bottom画一矩形条
以( x , y )为圆心,以 r a d i u s为
半径画一个圆
circle void circle(x,y,radius) 以以( x , y )为圆心,以 r a d i u s graphics.h
int x,y,radius 为半径画一个圆
242 C语言程序设计
下载
(续)

函 数 名 函数类型和形参类型 功 能 说 明

closegraph void closegraph(void) 以关闭图形工作方式,释 graphics.h


放用于保存图形驱动器和
字体的系统内存
clrscr void clrscr() 以清除整个当前字符窗口, conio.h
将光标定到左上角( 1,1)

cputs int cputs(str) 以把字符串 s t r输出到当前 conio.h
const char *str 字符窗口
detecgraph void detecgraph(drive,mode) 以确定图形适配器的类型 graphics.h
int *drive,*mode
floodfill void floodfill(x,y,border) 以用图形块中给定点和形 graphics.h
int x,y,border 状块边界线的当前颜色和
模式,填充该图形块
getbkcolor int far getbkcolor(void) 以返回当前背景颜色 graphics.h
getcolor int getcolor 以返回当前画线颜色 graphics.h
getfillpattern void far getfillpattren(pa) 以 填写由 p a指 向 的 数 组 , graphics.h
char far *pa; 填写内容为构成当前填充
图案的8个字节
getgraphmode int getgraphmode() 以返回当前图形模式 graphics.h
getimage void far getimage( left,top,right,bottom,buf) 以 把屏幕图形部分拷贝到 graphics.h
int left,top,right,bottom;void far *buf; b u f指向的内存。左上角为
left,top;右下角为right,bottom
gettext int gettext(left,top,right,bottom,buf) 以从左上角度 l e f t . t o p到右 conio.h
int left,top,right,bottom;char *buf; 下角 r i g h t , b o t t o m的矩形区
上的字符拷贝到内存
gotoxy void gotoxy(x,y) 以把字符屏幕上的光标移 conio.h
int x,y 动到x,y 处
imagesize unsigned far imagesize 以返回存储一块屏幕图形 graphics.h
(left,top,right, bottom,) 所需的存储器字节数。该
int left,top,right,bottom; 块屏幕左上角为 l e f t , t o p ;右
下角为right,bottom
initgraph void initgraph(drive,mode,path) 以把d r i v e所指的图形驱动 graphics.h
int *drive,*mode;char *path; 器装入内存,屏显模式有
m o d e确定,图形驱动器路
径有碍path给定
line void line(sx,sy,ex,ey) 以从( s x , s y )到( e x , e y )画一直 graphics.h
int sx,sy,ex,ey,endu; 线
outtext void outtext(str) 以 在光标处显示一字符串 graphics.h
char *str; str
puttext int puttext(left,top,right,bottom,buf) 以把由g e t t e x t ( )储存到内存
int left,top,right,bottom;char *buf; b u f的字符拷贝到左上角和
右下角的区域
rectangle void rectangle(left,top,right,bottom) 以用当前画线的颜色画一 graphics.h
int left,top,right,bottom; 个左上角为 l e f t , t o p和右下
角right,bottom的矩形
setbkcolor void setbkcolor(color)int color 以改变背景色为 c o l o r所指 graphics.h
颜色
下载
附录B C库函数 243
(续)

函 数 名 函数类型和形参类型 功 能 说 明

setcolor void setcolor(color)int color; 以设置当前画线颜色 graphics.h


setfillstyle void far setfillstyle(pa,color) 以为各种图形设置填充式 graphics.h
int pa,color; 样和颜色
settextstyle void far settextstyle(font,direct,size) 以为图形字符输出设置当 graphics.h
int font,direct,size; 前的字体、方向和字符大

textbackground void textbackground(col)int col; 设置字符屏幕的背景 conio.h
textcolor void textcolor(color)int color; 设置字符屏幕下的字符颜 conio.h

window Void window(left,top,right,bottom) 用于建立字符窗口 conio.h
int left,top,right,bottom;

动态存储分配

ANSI标准建议在头文件 stdlib.h中包含动态存储分配库函数,但有许多的 C编译用malloc.h


包含。使用时,请查阅。

函 数 名 函数类型和形参类型 功 能 返 回 值

free void free(p) 释放 p所占的内存区


void *p;
malloc void *malloc(size) 分配size字节的存储区 被 被分配的内存区的地址,
unsigned size 如内存不够,返回 0

类型转换函数

函 数 名 函数类型和形参类型 功 能 返 回 值

atof float atof(char *str) 把由str指向的字符串转换为实型 float


atoi int atoi(char *str) 把由str指向的字符串转换为整型 int
atol long atol(char *str) 把由str指向的字符串转换为长整型 long int

You might also like