You are on page 1of 322

高 等 学 校 教 材

C 语言程序设计
何钦铭 主编

颜 晖 杨起帆 韩 杰 编著

人 民 邮 电 出 版 社
编 者 的 话

C 语言是一种得到广泛使用的程序设计语言,它既包含高级语言的主要功能,又具有与
计算机硬件操作密切相关的功能。C 语言以其丰富灵活的控制和数据结构,简洁而高效的语
句表达,清晰的程序结构,良好的可移植性而拥有大量的使用者。著名的 Linux 和 UNIX 操
作系统都是用 C 语言编写而成的。
目前,C 语言被许多院校列为程序设计课程的首选语言。但 C 语言不容易掌握好,一是
其内容多、概念难;二是对初学者来说,学习程序设计的概念和方法是一个逐步探索的过程。
虽然介绍 C 语言的书籍和教材非常多,但在我们多年的教学实践中,感到真正适合大学教学
要求的教材并不多,为了更好地帮助读者掌握 C 语言程序设计,我们编写了本教材。
本书在内容编排上,与其他教材并无太大的区别。我们在具体内容的编写上,做到提出
问题,讲清概念,循序渐进,整体流畅。同时,我们还设计了许多典型例题,以帮助读者巩
固概念,避免常犯的错误。
读者在学习 C 语言程序设计过程中,要遵循循序渐进原则,自第 1 章起就要从编写几行
语句的小程序开始,逐步增强编程能力,使所编程序在规模和难度上逐渐加大。也可以仿照
书中一些例题,吸收例题中的一些典型编程技巧,但不能死记硬背概念和例题,因为程序设
计本身是一件十分灵活的工作,不同的问题有不同的解决方法,培养程序设计的能力最重要。
程序设计是实践性很强的课程,读者应充分重视上机练习。通过计算机运行、验证自己
编写的程序,将有助于理解和巩固所学的概念。对于稍微复杂的程序,初学时很难做到一次
上机运行正确,其中的错误往往要通过多次调试才能得到纠正。而调试的能力必须通过不断
地上机实习才能得到培养与提高。 为此,在本书的附录中我们介绍了 Turbo C 2.0 和 Visual C++
6.0 两种 C 语言平台的使用方法。前者小巧易安装,操作简便;后者应用面广,适用于 Windows
操作系统。
本书每章后均提供了经过精选的习题,选择题和问答题可以帮助读者巩固基本概念;程
序填空题和程序阅读题能提高读者阅读理解 C 语言程序的能力;编程题则能全面培养读者的
程序设计能力,建议读者把编程题同时作为上机实习题。
本书第 1、2、4、7、8 章由颜晖编写,第 3、5、6、10、11 章由杨起帆编写,第 9 章由
韩杰编写。附录由杨起帆和韩杰编写。
本书可以作为各类大专院校、各类培训与等级考试的教学用书,也可作为对 C 语言程序
设计感兴趣者的自学用书。相信通过本书的学习,能为你打下 C 语言程序设计的坚实基础。
书中若有错误或不当之处,恳请读者和专家指正,我们的电子信箱是:
yanhui_zd@163.com

编著者
2002.10
图书在版编目(CIP)数据

C 语言程序设计 / 颜晖等编著. —北京:人民邮电出版社,2003.1


高等学校教材

ISBN 7–115–11113–8

Ⅰ.C… Ⅱ.颜… Ⅲ.C 语言—程序设计—高等学校—教材 Ⅳ.TP312

中国版本图书馆 CIP 数据核字(2002)第 105801 号

内 容 提 要

本书主要内容包括 C 语言基本知识、基本数据类型和表达式、算法与 C 程序、分支结构


和循环结构程序设计、函数、数组、指针、结构、文件和 C 语言程序设计方法等。本书充分
考虑了各教学环节的要求,习题丰富,并为读者提供实用性强的附录。本书是编著者根据多
年教学经验,充分考虑学生学习规律,精心编写而成。全书体系合理、层次分明、重点突出、
深入浅出,是学习 C 语言程序设计的较为理想教材。
本书可以作为各类大专院校、各类培训与等级考试的教学用书,也可作为对 C 语言程序
设计感兴趣者的自学用书。

高等学校教材
C 语言程序设计
◆ 主 编 何钦铭
编 著 颜 晖 杨起帆 韩 杰
责任编辑 潘春燕
执行编辑 王健波
◆ 人民邮电出版社出版发行 北京市崇文区夕照寺街 14 号

邮编 100061 电子函件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者热线 010-67180876

北京汉魂图文设计有限公司制作
北京 印刷厂印刷
新华书店总店北京发行所经销
◆ 开本:787×1092 1/16
印张:20.25

字数:490 千字 2003 年 2 月第 1 版

印数:1—0 000 册 2003 年 2 月北京第 1 次印刷

ISBN 7-115-11113-8/TP・3337
定价:26.00 元
本书如有印装质量问题,请与本社联系 电话:
(010)67129223
目 录

目 录

第 1 章 用 C 语言编写程序・・・・・・・・・・・・・・・・・・・・・・・・・・・ 1
1.1 编写简单的 C 语言程序 ・・・・・・・・・・・・・・・・・・・・・・・・ 1
1.2 C 语言的基本输入输出函数・・・・・・・・・・・・・・・・・・・・・・・ 3
1.3 运行 C 语言程序 ・・・・・・・・・・・・・・・・・・・・・・・・・・・ 5
1.4 C 语言程序的基本结构・・・・・・・・・・・・・・・・・・・・・・・・・ 6
习题 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 7

第 2 章 基本数据类型和表达式 ・・・・・・・・・・・・・・・・・・・・・・・・・ 8
2.1 常量和变量 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 8
2.1.1 常量 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 8
2.1.2 变量 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 10
2.1.3 标识符 ・・・・・・・・・・・・・・・・・・・・・・・・・・・ 11
2.2 整数类型 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 12
2.2.1 整型常量(整数) ・・・・・・・・・・・・・・・・・・・・・・ 12
2.2.2 整型变量 ・・・・・・・・・・・・・・・・・・・・・・・・・・ 13
2.2.3 整型数据的输入和输出 ・・・・・・・・・・・・・・・・・・・・ 13
2.3 实数类型 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 14
2.3.1 实型常量(实数) ・・・・・・・・・・・・・・・・・・・・・・ 14
2.3.2 实型变量 ・・・・・・・・・・・・・・・・・・・・・・・・・・ 15
2.3.3 实型数据的输入和输出 ・・・・・・・・・・・・・・・・・・・・ 15
2.4 字符类型 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 17
2.4.1 字符常量 ・・・・・・・・・・・・・・・・・・・・・・・・・・ 17
2.4.2 字符变量 ・・・・・・・・・・・・・・・・・・・・・・・・・・ 18
2.4.3 字符型数据的输入和输出 ・・・・・・・・・・・・・・・・・・・ 19
2.4.4 转义字符 ・・・・・・・・・・・・・・・・・・・・・・・・・・ 21
2.5 表达式 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 22
2.5.1 算术表达式 ・・・・・・・・・・・・・・・・・・・・・・・・・ 22
2.5.2 赋值表达式 ・・・・・・・・・・・・・・・・・・・・・・・・・ 26
2.5.3 逗号表达式 ・・・・・・・・・・・・・・・・・・・・・・・・・ 28
2.6 数据的存储和类型转换 ・・・・・・・・・・・・・・・・・・・・・・・ 28
2.6.1 数据的存储 ・・・・・・・・・・・・・・・・・・・・・・・・・ 28
2.6.2 整数类型的扩展 ・・・・・・・・・・・・・・・・・・・・・・・ 30
2.6.3 数据类型转换 ・・・・・・・・・・・・・・・・・・・・・・・・ 32

–1–
C 语言程序设计

习题 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 34

第 3 章 算法与 C 语言程序・・・・・・・・・・・・・・・・・・・・・・・・・・ 36
3.1 计算机求解问题的步骤 ・・・・・・・・・・・・・・・・・・・・・・・ 36
3.2 算法的描述 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 37
3.3 算法与程序 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 40
3.3.1 算法特征 ・・・・・・・・・・・・・・・・・・・・・・・・・・ 40
3.3.2 算法的 C 语言实现・・・・・・・・・・・・・・・・・・・・・・ 41
3.3.3 算法与程序结构 ・・・・・・・・・・・・・・・・・・・・・・・ 42
3.4 C 语句分类・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 46
习题 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 48

第 4 章 分支结构程序设计 ・・・・・・・・・・・・・・・・・・・・・・・・・・ 49
4.1 关系表达式和逻辑表达式 ・・・・・・・・・・・・・・・・・・・・・・ 49
4.1.1 关系表达式 ・・・・・・・・・・・・・・・・・・・・・・・・・ 49
4.1.2 逻辑表达式 ・・・・・・・・・・・・・・・・・・・・・・・・・ 50
4.2 if 语句・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 53
4.2.1 基本的 if 语句・・・・・・・・・・・・・・・・・・・・・・・・ 53
4.2.2 嵌套的 if 语句・・・・・・・・・・・・・・・・・・・・・・・・ 57
4.2.3 条件表达式 ・・・・・・・・・・・・・・・・・・・・・・・・・ 61
4.3 switch 语句・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 62
习题 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 65

第 5 章 循环结构程序设计 ・・・・・・・・・・・・・・・・・・・・・・・・・・ 68
5.1 C 语言的循环语句・・・・・・・・・・・・・・・・・・・・・・・・・・ 68
5.1.1 for 语句 ・・・・・・・・・・・・・・・・・・・・・・・・・・ 68
5.1.2 while 语句 ・・・・・・・・・・・・・・・・・・・・・・・・・ 71
5.1.3 do-while 语句 ・・・・・・・・・・・・・・・・・・・・・・・・ 71
5.1.4 三种循环语句的使用 ・・・・・・・・・・・・・・・・・・・・・ 73
5.1.5 for 语句的形式变化 ・・・・・・・・・・・・・・・・・・・・・ 76
5.2 break 语句和 continue 语句 ・・・・・・・・・・・・・・・・・・・・・ 78
5.2.1 break 语句 ・・・・・・・・・・・・・・・・・・・・・・・・・ 78
5.2.2 continue 语句 ・・・・・・・・・・・・・・・・・・・・・・・・ 79
5.3 循环嵌套 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 81
5.4 循环程序设计 ・・・・・・・・・・・・・・・・・・・・・・・・・・・ 84
习题 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 88

第 6 章 函数 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 93
6.1 函数定义 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 93

–2–
目 录

6.1.1 函数概念 ・・・・・・・・・・・・・・・・・・・・・・・・・・ 93


6.1.2 函数定义 ・・・・・・・・・・・・・・・・・・・・・・・・・・ 94
6.1.3 函数的参数 ・・・・・・・・・・・・・・・・・・・・・・・・・ 95
6.2 函数调用 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 97
6.2.1 函数调用过程 ・・・・・・・・・・・・・・・・・・・・・・・・ 97
6.2.2 函数调用形式 ・・・・・・・・・・・・・・・・・・・・・・・・ 98
6.2.3 参数传递 ・・・・・・・・・・・・・・・・・・・・・・・・・・ 100
6.2.4 函数结果返回 ・・・・・・・・・・・・・・・・・・・・・・・・ 101
6.2.5 函数的嵌套调用 ・・・・・・・・・・・・・・・・・・・・・・・ 102
6.2.6 函数的声明 ・・・・・・・・・・・・・・・・・・・・・・・・・ 104
6.3 递归函数 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 105
6.3.1 递归函数基本概念 ・・・・・・・・・・・・・・・・・・・・・・ 105
6.3.2 递归程序设计 ・・・・・・・・・・・・・・・・・・・・・・・・ 108
6.4 变量与函数 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 112
6.4.1 局部变量和全局变量 ・・・・・・・・・・・・・・・・・・・・・ 112
6.4.2 变量生命周期和静态局部变量 ・・・・・・・・・・・・・・・・・ 116
6.4.3 寄存器变量和外部变量 ・・・・・・・・・・・・・・・・・・・・ 118
6.5 程序模块结构 ・・・・・・・・・・・・・・・・・・・・・・・・・・・ 119
6.5.1 文件包含 ・・・・・・・・・・・・・・・・・・・・・・・・・・ 119
6.5.2 全局变量与程序文件模块 ・・・・・・・・・・・・・・・・・・・ 121
6.5.3 函数与程序文件模块 ・・・・・・・・・・・・・・・・・・・・・ 124
6.5.4 变量、函数与程序文件模块关系 ・・・・・・・・・・・・・・・・ 124
6.6 宏定义 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 126
6.6.1 宏基本定义 ・・・・・・・・・・・・・・・・・・・・・・・・・ 126
6.6.2 带参数的宏定义 ・・・・・・・・・・・・・・・・・・・・・・・ 128
6.7 编译预处理 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 130
习题 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 132

第 7 章 数组 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 138
7.1 一维数组 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 139
7.1.1 一维数组的定义和引用 ・・・・・・・・・・・・・・・・・・・・ 139
7.1.2 一维数组的初始化 ・・・・・・・・・・・・・・・・・・・・・・ 144
7.2 二维数组 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 145
7.2.1 二维数组的定义和引用 ・・・・・・・・・・・・・・・・・・・・ 145
7.2.2 二维数组的初始化 ・・・・・・・・・・・・・・・・・・・・・・ 149
7.3 字符串 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 150
7.3.1 一维字符数组 ・・・・・・・・・・・・・・・・・・・・・・・・ 151
7.3.2 字符串 ・・・・・・・・・・・・・・・・・・・・・・・・・・・ 151
习题 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 154

–3–
C 语言程序设计

第 8 章 指针 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 157
8.1 指针 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 157
8.1.1 指针变量的定义 ・・・・・・・・・・・・・・・・・・・・・・・ 158
8.1.2 指针的基本运算 ・・・・・・・・・・・・・・・・・・・・・・・ 158
8.1.3 指针变量的初始化 ・・・・・・・・・・・・・・・・・・・・・・ 161
8.1.4 指针作为函数的参数 ・・・・・・・・・・・・・・・・・・・・・ 162
8.2 指针和数组 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 166
8.2.1 指针、数组和地址间的关系 ・・・・・・・・・・・・・・・・・・ 166
8.2.2 数组名作为函数的参数 ・・・・・・・・・・・・・・・・・・・・ 168
8.3 指针和字符串 ・・・・・・・・・・・・・・・・・・・・・・・・・・・ 173
8.3.1 常用的字符串处理函数 ・・・・・・・・・・・・・・・・・・・・ 173
8.3.2 字符串的指针表示 ・・・・・・・・・・・・・・・・・・・・・・ 176
8.3.3 字符数组和字符指针 ・・・・・・・・・・・・・・・・・・・・・ 178
8.4 指针数组和指向指针的指针 ・・・・・・・・・・・・・・・・・・・・・ 180
8.4.1 指针数组 ・・・・・・・・・・・・・・・・・・・・・・・・・・ 180
8.4.2 指向指针的指针 ・・・・・・・・・・・・・・・・・・・・・・・ 183
8.4.3 指针数组、二维字符数组和字符串 ・・・・・・・・・・・・・・・ 186
8.4.4 命令行参数 ・・・・・・・・・・・・・・・・・・・・・・・・・ 192
8.5 指针和函数 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 195
8.5.1 指针作为函数的返回值 ・・・・・・・・・・・・・・・・・・・・ 195
8.5.2 指向函数的指针 ・・・・・・・・・・・・・・・・・・・・・・・ 196
习题 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 199

第 9 章 结构 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 202
9.1 结构的概念 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 202
9.1.1 结构的定义 ・・・・・・・・・・・・・・・・・・・・・・・・・ 202
9.1.2 结构变量的定义和引用 ・・・・・・・・・・・・・・・・・・・・ 203
9.1.3 结构的嵌套定义 ・・・・・・・・・・・・・・・・・・・・・・・ 206
9.2 结构数组 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 207
9.2.1 结构数组的定义和引用 ・・・・・・・・・・・・・・・・・・・・ 207
9.2.2 结构数组的初始化 ・・・・・・・・・・・・・・・・・・・・・・ 208
9.3 结构指针 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 209
9.3.1 结构指针的概念和使用 ・・・・・・・・・・・・・・・・・・・・ 209
9.3.2 结构指针作为函数的参数 ・・・・・・・・・・・・・・・・・・・ 211
9.4 单向链表 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 213
9.4.1 单向链表的定义 ・・・・・・・・・・・・・・・・・・・・・・・ 213
9.4.2 单向链表的常用操作 ・・・・・・・・・・・・・・・・・・・・・ 213
9.5 联合 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 222
9.5.1 联合的定义 ・・・・・・・・・・・・・・・・・・・・・・・・・ 222
–4–
目 录

9.5.2 联合变量的定义和引用 ・・・・・・・・・・・・・・・・・・・・ 222


9.6 枚举 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 227
9.6.1 枚举类型的定义 ・・・・・・・・・・・・・・・・・・・・・・・ 227
9.6.2 枚举变量的定义和引用 ・・・・・・・・・・・・・・・・・・・・ 228
9.7 自定义类型 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 229
9.8 位运算与位段 ・・・・・・・・・・・・・・・・・・・・・・・・・・・ 231
9.8.1 位运算和位运算符 ・・・・・・・・・・・・・・・・・・・・・・ 231
9.8.2 位段 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 233
习题 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 237

第 10 章 文件 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 243
10.1 文件的基本概念 ・・・・・・・・・・・・・・・・・・・・・・・・・・ 243
10.1.1 文本文件和二进制文件 ・・・・・・・・・・・・・・・・・・・ 243
10.1.2 缓冲文件系统 ・・・・・・・・・・・・・・・・・・・・・・・ 244
10.1.3 缓冲文件与文件类型指针 ・・・・・・・・・・・・・・・・・・ 245
10.2 文件的打开与关闭 ・・・・・・・・・・・・・・・・・・・・・・・・・ 246
10.2.1 打开文件 ・・・・・・・・・・・・・・・・・・・・・・・・・ 246
10.2.2 关闭文件 ・・・・・・・・・・・・・・・・・・・・・・・・・ 248
10.3 文件的读写 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 248
10.3.1 字符文件读写 ・・・・・・・・・・・・・・・・・・・・・・・ 249
10.3.2 数值文件读写 ・・・・・・・・・・・・・・・・・・・・・・・ 253
10.3.3 二进制文件读写 ・・・・・・・・・・・・・・・・・・・・・・ 254
10.4 文件程序设计 ・・・・・・・・・・・・・・・・・・・・・・・・・・・ 254
10.5 标准文件的输入/输出 ・・・・・・・・・・・・・・・・・・・・・・・・ 258
10.5.1 字符的输入/输出 ・・・・・・・・・・・・・・・・・・・・・・ 258
10.5.2 格式化输入/输出 ・・・・・・・・・・・・・・・・・・・・・・ 259
10.6 文件的数据块读写 ・・・・・・・・・・・・・・・・・・・・・・・・・ 262
10.7 文件定位 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 265
10.8 同时对文件读和写 ・・・・・・・・・・・・・・・・・・・・・・・・・ 266
习题 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 268

第 11 章 C 语言程序设计方法 ・・・・・・・・・・・・・・・・・・・・・・・・ 271


11.1 结构化程序设计方法 ・・・・・・・・・・・・・・・・・・・・・・・・ 271
11.1.1 自顶向下分析设计问题 ・・・・・・・・・・・・・・・・・・・ 271
11.1.2 模块化程序设计 ・・・・・・・・・・・・・・・・・・・・・・ 272
11.1.3 结构化程序编写 ・・・・・・・・・・・・・・・・・・・・・・ 273
11.2 程序设计风格 ・・・・・・・・・・・・・・・・・・・・・・・・・・・ 276
11.2.1 源程序文档化 ・・・・・・・・・・・・・・・・・・・・・・・ 276
11.2.2 语句结构 ・・・・・・・・・・・・・・・・・・・・・・・・・ 276

–5–
C 语言程序设计

11.2.3 良好的交互特性 ・・・・・・・・・・・・・・・・・・・・・・ 277


11.3 C 语言程序设计中要注意的问题 ・・・・・・・・・・・・・・・・・・・ 277
11.3.1 正确使用运算符 ・・・・・・・・・・・・・・・・・・・・・・ 277
11.3.2 正确的数据类型操作 ・・・・・・・・・・・・・・・・・・・・ 279
11.3.3 正确的语句运用 ・・・・・・・・・・・・・・・・・・・・・・ 284

附录 A C 语言上机操作指导 ・・・・・・・・・・・・・・・・・・・・・・・・・ 286

附录 B ASCII 码集 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 302

附录 C C 语言中的关键字 ・・・・・・・・・・・・・・・・・・・・・・・・・・ 305

附录 D 运算符优先级 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 306

附录 E C 语言常用库函数 ・・・・・・・・・・・・・・・・・・・・・・・・・・ 308

–6–
第1章 用 C 语言编写程序

1.1 编写简单的 C 语言程序

为了让读者对 C 语言有一个感性认识,我们首先看一下用 C 语言编写的几个简单程序。


例 1-1 在屏幕上显示一个短句“Programming is fun!”。
源程序:
# include <stdio.h>
void main( )
{
printf("Programming is fun! \n");
}
运行结果如下:
Programming is fun!
程序中的 main 被称为“主函数”,任何一个 C 语言程序都必须有而且只能有一个 main()
函数。一对大括弧把构成函数的语句括起来,称为函数体,例 1-1 的函数体只有一条语句:
printf("Programming is fun! \n");
该语句由两部分组成:函数调用和分号。printf("Programming is fun!\n")是一个函数调用,
它的作用是将双引号中的内容原样输出,\n 是换行符,即在输出 Programming is fun!后换行;
而分号表示该语句的结束。需要强调的是,在 C 语言中,任何语句都由分号结束。
例 1-2 在屏幕上显示两个短句“Programming is fun.”和“And Programming in C is even
more fun!”,每行显示一句。
源程序:
# include <stdio.h>
void main( )
{
printf("Programming is fun.\n");
printf("And Programming in C is even more fun!\n");
}
运行结果如下:
Programming is fun.
And Programming in C is even more fun!
例 1-2 在例 1-1 的 main()函数中增加了一条语句,也可以只用一条语句完成:
# include <stdio.h>
void main( )
{

–1–
C 语言程序设计

printf("Programming is fun.\nAnd Programming in C is even more fun!\n");


}
如果将上述语句改成:
printf("Programming is fun. And Programming in C is even more fun!\n");
运行结果如下:
Programming is fun. And Programming in C is even more fun!
在例 1-1 和例 1-2 中,
“#include <stdio.h>”是编译预处理命令,因为程序中调用的 printf()
函数是 C 语言提供的标准输出函数,需要由系统文件 stdio.h 解释执行。请读者注意编译预处
理命令和语句在书写格式上的区别,编译预处理命令的末尾不加分号。
例 1-3 计算并显示两个数的和。
源程序:
/* This program adds two integer values and displays the result. */
# include <stdio.h>
void main( )
{
int value1, value2, sum; /* 定义 3 个整型(int)变量*/
/* 空行,用于分隔变量定义和可执行语句 */
value1=5; /* 对变量 value1 赋值 */
value2=2; /* 对变量 value2 赋值 */
sum=value1+value2; /* 求和并送入变量 sum */
printf("The sum is %d\n", sum);
}
运行结果如下:
The sum is 7
/*和*/之间的文本是注释,注释就是对程序的注解,它可以是任何可显示字符。在源程
序中插入适当的注释,可以使程序容易被人理解,并不影响程序的编译和运行。
调用 printf()函数输出信息,将双引号内除%d 以外的内容原样输出,在%d 的位置上输
出变量 sum 的值。
由此可见,使用 printf()函数调用不但能够输出固定不变的内容,如例 1-1 的 Programming
is fun!,还可以输出变量的值,如例 1-3 变量 sum 的值。
例 1-4 计算两个数的和,并以算式的形式显示。
源程序:
#include <stdio.h>
void main( )
{
int value1, value2, sum;

value1=5;
value2=2;
sum=value1+value2;
printf("%d+%d=%d\n", value1, value2, sum);

–2–
第1章 用 C 语言编写程序

}
运行结果如下:
5+2=7
printf()函数调用将双引号内的加号、等号和换行符原样输出,并在第一个%d 的位置上
输出变量 value1 的值,在后两个%d 的位置上,依次输出 value2 和 sum 的值。
例 1-4 的程序只能计算 5 和 2 的和,如果我们要求 123 和 345 的和,就必须修改程序,
将 123 和 345 分别赋给变量 value1 和 value2,也就是说,一旦改变了原始数据,就要修改程
序。显然,这不是一个好的解决方案。
比较好的方法是:程序运行时,询问我们要计算哪两个数的和,一旦我们回答了,就能
给出相应的和。在 C 语言中使用 scanf()函数调用就能达到这个目的。
例 1-5 输入两个整数,求它们的和。
源程序:
#include <stdio.h>
void main( )
{
int value1, value2, sum;

printf("Input two numbers:\n"); /* 输入提示 */


scanf("%d%d", &value1, &value2); /* 读入 2 个数 */
sum=value1+value2;
printf("%d+%d=%d\n", value1, value2, sum);
}
运行结果如下:
Input two numbers:
123 345<CR>
123+345=468
在运行结果中,如果一行以<CR>结束,表示该行是用户输入的数据,其余是程序的输出
结果。在本书的所有例题中,我们都遵循这种约定。
例 1-5 的 scanf()函数调用,表示系统接受用户输入的、用空格间隔的两个整数,并保存
在变量 value1 和 value2 中。请读者注意,变量名 value1 和 value2 的前面必须使用地址符&,
关于&的作用在“指针”一章中会详细讨论。调用 scanf()函数时,如果在变量名之前忘记使
用&,程序的运行结果会出乎意料,这也是初学者容易犯的错误。

1.2 C 语言的基本输入输出函数

计算机处理问题的过程就是数据的输入、处理和输出过程。在 C 语言中,数据的输入和
输出都是通过函数调用实现的。 为了使读者能尽快地用 C 语言编写一些功能比较简单的程序,
提高学习的兴趣,我们先简要介绍一些常用的输入输出函数的基本用法。这些函数都是系统
提供的库函数,在系统文件 stdio.h 中定义,所以在源程序开始时要使用编译预处理命令

–3–
C 语言程序设计

#include <stdio.h>。

1.格式化输出函数 printf()

函数 printf()用于输出数据,其一般调用格式为:
printf(格式控制,输出参数 1,…,输出参数 n);
其中格式控制是一个用双引号括起来的字符串,而输出参数则是一些要输出的数据。格
式控制包含两种信息,即格式控制说明(如表 1-1 所示)和普通字符。
(1)格式控制说明:按指定的格式输出数据,它包含以“%”开头的格式控制字符,不
同类型的数据采用不同的格式控制字符。
表 1-1 格式控制说明

函 数 数 据 类 型 格 式 含 义

整型 %d 输入/输出一个十进制整数
scanf ()
实型 %f 输入/输出一个实数
printf()
字符型 %c 输入/输出一个字符

(2)普通字符:在输出数据时,需要原样输出的字符。
例如:
printf (" X =% d, Y =% d", x, y);

格式控制 输出参数
当 x、y 的值分别为 10 和 500 时,输出结果为:
X = 10, Y = 500

格式控制中的普通字符
printf()函数的输出参数必须和格式控制字符串中的格式控制说明相对应,并且它们的个
数、位置和类型要一一对应。上例中,输出参数 x 与第一个%d 对应,输出参数 y 与第二个
%d 对应。

2.格式化输入函数 scanf()

函数 scanf()用于从键盘输入数据,其格式与函数 printf()类似:
scanf(格式控制,输入参数 1,…,输入参数 n);
其中格式控制是一个用双引号括起来的字符串,而输入参数是变量地址(变量名前加&)。
格式控制包含两种信息,即格式控制说明(如表 1-1 所示)和普通字符。
(1)格式控制说明:按指定的格式读入数据,它包含以“%”开头的格式控制字符,不
同类型的数据采用不同的格式控制字符。
scanf()函数的输入参数必须和格式控制字符串中的格式控制说明相对应,并且它们的个
数、位置和类型要一一对应。
如果读入多个数据,就需要有多个输入参数和多个格式控制说明。在程序运行时,输入
–4–
第1章 用 C 语言编写程序

的多个数据之间必须有间隔,可以用一个或多个空格作为间隔,也可以用回车或跳格(Tab)
作为间隔。
例 1-5 读入两个整数,并保存在变量 value1 和 value2 中,scanf()函数调用为:
scanf("%d%d", &value1, &value2);
程序运行时输入:
123 345
对应第一个%d 输入第一个数,然后输入一个空格作为间隔,对应第二个%d 输入第二个
数。
(2)普通字符:在输入数据时,需要原样输入的字符。
将例 1-5 中的 scanf()函数调用改为:
scanf("%d,%d", &value1, &value2);
程序运行时输入:
123,345
格式控制中的逗号作为普通字符,在输入时必须原样输入,由于它出现在两个格式控制
说明%d 之间,起到了分隔输入数据的作用,就不必再输入空格作为间隔了。
将例 1-5 中的 scanf()函数调用改为:
scanf("value1=%d,value2=%d", &value1, &value2);
程序运行时输入:
value1=123,value2=345
此时,格式控制中出现的所有普通字符 value1=,value2=都必须原样输入,否则会出现错误。
为了减少不必要的输入,编写 C 语言程序时,在 scanf()函数的格式控制中尽量不要出现
普通字符,尤其不要将输入提示放在 scanf()函数的格式控制中,如果需要出现输入提示,应
该调用 printf()函数(参见例 1-5)。

1.3 运行 C 语言程序

用 C 语言编写的源程序不能直接运行,需要先对源程序进行编译处理,生成由机器指令
组成的目标程序。由于用户在源程序中可以直接调用库函数实现输入、输出以及常用函数运
算等功能,因此,经编译生成的目标程序还不能交计算机直接运行,必须经过与“库函数”
的连接处理,生成可执行程序后,才能运行,并得到结果。
C 语言程序的调试、运行步骤如图 1-1 所示,图中的虚线表示当某一步骤出现错误时的
修改路线。
在 C 语言程序的调试和运行过程中,如果出现编译错误、连接错误,或者运行结果不对,
说明源程序中存在语法错误或逻辑错误,就需要修改该程序,然后重新编译、连接和运行,
直到将程序调试正确为止。
例如,在编辑例 1-3 的源程序时,如果漏写了语句中任何一个分号、逗号或冒号,就会
导致编译时出错(存在语法错误) ;如果误将“sum=value1+value2”写作“sum=value1-value2”,
则运行结果不对(存在逻辑错误),以上情况都需要修改源程序。

–5–
C 语言程序设计

编 辑 编译 连接 运行

源程序 目标程序 可执行程序


开始 结 果
.c/.cpp .obj .exe

图 1-1 C 语言程序的调试、运行步骤

在本书中,我们介绍标准 C(ANSI C) ,依此编制的程序一般可运行于 Turbo C(以下简


称 TC)、Borland C++(以下简称 BC++)、Visual C++(以下简称 VC++)等编程语言环境。
C 语言程序的上机步骤请看附录 A。

1.4 C 语言程序的基本结构

通过前面的学习,读者已经对 C 语言程序有了初步的认识,还可以编写一些简单的 C 语
言程序,并在计算机上运行这些程序。为了使读者比较全面地了解 C 语言程序的基本结构,
请继续看下面的例子。
例 1-6 输入正整数 m 和 n,求 m!和 n!。
分析:C 语言中对指数、对数和三角函数等运算提供了库函数,我们可以像使用 printf()
函数那样来调用它们,但求阶乘却没有现成的函数,需要自行定义。
源程序:
#include <stdio.h>
void main( )
{
int m, n;
float resm, resn; /* 定义实型(float)变量 */
float fact( int k ); /* 函数的原型说明 */

scanf("%d%d", &m, &n);


resm = fact(m); /* 调用 fact()函数计算 m!,并把返回值赋给 resm */
resn = fact(n); /* 调用 fact()函数计算 n!,并把返回值赋给 resn */
printf("%f, %f\n", resm, resn);
} /* main()函数结束 */

/* 定义 fact 函数,函数返回值的类型是实型,函数有一个参数 */
float fact( int k ) /* 函数首部 */
{
int i;
float y = 1;

–6–
第1章 用 C 语言编写程序

for(i = 2; i <= k; i++) /* 循环 */


y = y*i;
return y; /* 返回 main()函数的调用处,同时返回 y 的值 */
}
例 1-6 的 main()函数,通过依次执行 resm = fact(m);和 resn = fact(n);这两条语句,实现了
对 fact()函数的两次调用,调用参数分别为 m 和 n,计算出 m!和 n!。
例 1-6 涉及到函数定义、函数调用和函数的参数等概念,读者可能不大理解,对此不必
深究,在学习了以后的相关章节后,问题就迎刃而解了。
从例 1-6 可以看出,C 语言源程序由 1 个 main()函数与 n(n≥0)个自定义函数组成,程
序总是从 main()函数开始执行,在执行过程中完成对其他函数的调用,也就是说它常常还要
调用其他的函数来完成某些特定的操作,这些被调用的函数可以由程序员自己编写(如 fact()
函数) ,也可以由 C 语言编译系统中的函数库提供(如 scanf()函数和 printf()函数)。
从本章的示例中可以看出 C 语言程序的一些基本概貌。
(1)C 语言程序由函数组成,而且必须有一个以 main 命名的主函数。
(2)C 语言程序从 main()函数开始执行,在执行过程中完成对其他函数的调用。
(3)函数定义中的函数体必须用一对大括弧{}括起来。
(4)任何变量必须先定义类型,后使用。
(5)任何一条 C 语言语句必须以分号“;”结束。
(6)程序中可以使用编译预处理命令。
(7)程序中可以有注释(以“/*”和“*/”界定) ,用于解释程序的功能,增强可读性,
但注释对程序的编译和运行不起作用。
当然,读者只有学习更多的 C 语言知识,才能全面了解 C 语言程序的结构。
此外,C 语言是一个自由格式的语言,它允许一行写多条语句,一条语句也可以分写在
多行上,但这种自由书写的程序常常给阅读和理解带来了困难,应该提倡良好的程序设计风
格。在编写程序时,要适当地安排注释,添加空行和空格,采用缩进的格式,使程序结构清
晰、层次分明,易于阅读。有关程序设计风格的其他问题,我们将在以后的章节中逐步介绍,
读者可以先模仿本书中例题程序的风格书写程序。

习 题

1.请分别编写能显示以下内容的 C 语言程序:
(1)Programming Language
(2)A reaching out toward expresion,
An effort to find fulfillment.
(3)*************
Welcome
*************
2.编写程序,输入 2 个整数,求它们的和、差、积、商。

–7–

第2章 基本数据类型和表达式
我们编写 C 语言程序,是为了利用计算机实现对数据的处理。这就面临几个问题:计
算机能处理哪些数据?对这些数据能做哪些操作?通过怎样的操作步骤才能完成给定的工
作?
C 语言可以使用的数据类型如下:
整型
基本数据类型 字符型 单精度型
实型(浮点型)
双精度型
数组
数据类型 构造数据类型 结构
联合
枚举
指针类型
空类型
C 语言程序中所使用的每个数据都属于上述某一种类型,我们在编程时要正确地定义和
使用数据。
C 语言提供了 4 种基本数据类型:整型、字符型、单精度浮点型和双精度浮点型。用这
些基本类型还可以构造许多导出类型,以后将要讨论的指针、数组、结构和联合等就属于这
种导出类型。事实上,可以导出和定义的类型数目在理论上是没有限制的,C 语言的程序员
可以构造他自己所需要的任何类型。
在 C 语言中,对数据的操作就是对数据进行运算,C 语言提供了许多运算符,可以对不
同类型的数据进行处理。这些运算符与数据组合后便形成了表达式。
本章主要介绍基本数据类型、算术运算和赋值运算。

2.1 常量和变量

在C语言中,数据有常量和变量之分。在程序运行过程中,其值不能被改变的量称为常
量;其值可以改变的量称为变量。例如,在例 1-4 中,5 和 2 是常量,而 value1、value2 和
sum 就是变量。

2.1.1 常量
C 语言中,常量是有数据类型的,它的类型由书写格式决定。例如: −10、123、017、
–8–
第2章 基本数据类型和表达式

0x1f 是整型常量,−123.23、4.3e−3 是实型常量,而’a’、’\n’、’9’是字符常量。


一个常量也可以用一个标识符(见 2.1.3 节)来代表,称为符号常量。
例 2-1 输入球的半径,计算球的表面积和体积。
源程序:
#define PI 3.14 /* 定义符号常量 PI */
#include <stdio.h>
void main( )
{
float r, s, v;

scanf("%f", &r);
s=4.0*PI*r*r;
v=4.0/3.0*PI*r*r*r;
printf("s=%f,v=%f\n", s,v);
}
运行结果如下:
1.0 <CR>
s=12.560000,v=4.186666
用#define 命令定义符号常量 PI,它代表 3.14。定义后,凡在该程序中出现 PI 的地方都
用 3.14 来替代,这样符号常量 PI 就可以和实型常量 3.14 一样进行运算。
符号常量名一般使用大写字母,如 PI。
定义符号常量后,就可以引用它,但不能改变它的值。
使用符号常量的好处是修改方便,便于移植。例如,例 2-1 的程序中多处用到π,开始取
值 3.14,以后为了提高计算精度,π的值取 3.14159,在这种情况下,如果直接使用常数 3.14,
就要对程序做多处修改,将所有的 3.14 改为 3.14159,而使用符号常量,只需改动一处,即
改变符号常量的定义。
例 2-2 符号常量的使用比较。
(1)不使用符号常量,将所有的 3.14 改为 3.14159,需要对程序做多处修改。
#include <stdio.h> #include <stdio.h>
void main( ) void main( )
{ {
float r, s, v; float r, s, v;

scanf("%f ", &r); scanf("%f ", &r);


s=4.0*3.14*r*r; s=4.0*3.14159*r*r;
v=4.0/3.0*3.14*r*r*r; v=4.0/3.0*3.14159*r*r*r;
printf("s=%f,v=%f\n", s,v); printf("s=%f,v=%f\n", s,v);
} }
运行结果如下: 运行结果如下:
1.0 <CR> 1.0 <CR>
s=12.560000,v=4.186666 s=12.566360,v=4.188787

–9–
C 语言程序设计

(2)使用符号常量,将所有的 3.14 改为 3.14159,只需要改变符号常量的定义。


#define PI 3.14 #define PI 3.14159
#include <stdio.h> #include <stdio.h>
void main( ) void main( )
{ {
float r, s, v; float r, s, v;

scanf("%f", &r); scanf("%f", &r);


s=4.0*PI*r*r; s=4.0*PI*r*r;
v=4.0/3.0*PI*r*r*r; v=4.0/3.0*PI*r*r*r;
printf("s = %f,v = %f\n", s,v); printf("s = %f,v = %f\n", s,v);
} }
运行结果如下: 运行结果如下:
1.0 <CR> 1.0 <CR>
s=12.560000,v=4.186666 s=12.566360,v=4.188787
使用符号常量的另一个好处是提高程序的可读性。

2.1.2 变量
在程序运行过程中,其值可以改变的量称为变量。C 语言程序中用到的所有变量都必须
先定义,然后才能使用。定义变量时需要确定变量的名字和数据类型。

1.变量名

变量名应该采用一个合法的标识符(见 2.1.3 节),如 total、amount、average 等,其中


的英文字母习惯用小写字母。变量名的选择应尽量遵循“见名知义”的原则,用有明确含义
的英文单词(或拼音)来作为名字,这样看到变量名就知道它代表什么,便于自己或他人阅
读程序。

2.变量的类型

C 语言中,常量的数据类型通常由书写格式决定,而变量的数据类型在定义时指定。用
于定义变量的基本数据类型见表 2-1。
表 2-1 基本数据类型
类 别 名 称 类 型 名

整型 整型 int

字符型 字符型 char

单精度浮点型 float
实型(浮点型)
双精度浮点型 double

3.变量的定义方法

变量定义的一般形式如下:
– 10 –
第2章 基本数据类型和表达式

类型名 变量表;
类型名必须是有效的数据类型,变量表中可以有一个变量名或由逗号间隔的多个变量
名。
例如:
int i, j, k; /* 定义 i, j, k 为整型变量 */
char c; /* 定义 c 为字符型变量 */
float x, y; /* 定义 x, y 为单精度浮点型变量 */
double area, length; /* 定义 area, length 为双精度浮点型变量 */
定义了一些基本类型的变量。
由此可见,定义变量需要确定变量的名字和数据类型,每个变量必须有一个名字作为标
识,变量名代表内存中的一个存储单元,用于存放该变量的值,而该存储单元的大小由变量
的数据类型决定。例如,字符型变量用来存放字符,需一个字节,而整型变量用来存放整数,
需两个字节。定义变量后,就可以使用它,在程序中使用变量,就是使用该变量所代表的存
储单元。

4.变量的赋值

对变量的使用,包括赋值和引用。在定义变量后,首先应该对它赋值,然后就可以在该
程序中引用它的值,必要时还可以改变它的值,即再次赋值。
对变量的赋值有 3 种方法。
(1)在定义变量时对它赋值,称为变量赋初值。例如:
int a=5, b=3;
定义 a、b 为整型变量,同时变量 a 和 b 分别被赋初值 5 和 3。
(2)在可执行语句中,用赋值表达式对变量赋值。例如:
int a, b;
a=5;
b=3;
变量 a 和 b 分别被赋值 5 和 3。
(3)调用输入函数对变量赋值。例如:
int a, b;
scanf("%d%d", &a, &b);
运行时输入:
5 3
变量 a 和 b 分别被赋值 5 和 3。
C 语言中变量的含义和数学中变量的含义不同,数学中的变量代表未知数,而 C 语言中
的变量代表保存数据的存储单元。例如,表达式 x=x+1 在数学上没有任何意义,但在 C 语言
中却表示把变量 x 的值加 1,然后再保存到 x 中。

2.1.3 标识符
标识符用来标识或表示程序、函数、数据类型、变量等。
C 语言规定,标识符由字母、数字及下划线组成,必须以字母或下划线开头。标识符所
– 11 –
C 语言程序设计

包含的字符个数(即长度)不限,但一般不要超过 8 个字符,如果超过 8 个字符,系统只识


别前 8 个字符。标识符中的英文字母区分大小写。

1.保留字

在 C 语言中,保留字是有特定含义和专门用途的标识符,不能作为其他用途。保留字也
称作关键字或保留关键字。C 语言中一共有 32 个保留字(如表 2-2 所示)。
表 2-2 保留字
auto break case char const continue default do

double else enum extern float for goto if

int long register return short signed sizeof static

struct switch typedef union unsigned void volatile while

2.特定字

特定字是 C 语言定义的一些作为指令的标识符,它们主要用在预处理命令中,这些标识
符一般也不应另作它用。人们习惯将它们看作是保留字,赋予特定的含义。尽管语法允许将
这些特定字重定义为用户标识符,但为了保持程序的清晰易读,避免可能出现的错误,建议
读者不要这样做。特定字主要有以下 7 个:
define include undef ifdef ifndef endif line

3.自定义标识符

用户自己定义标识符时,不能使用保留字,也尽量不要使用特定字。C 语言的传统命名
习惯是变量名用小写字母,而符号常量全部用大写字母。一个优秀的程序员在定义标识符时
会遵循“见名知义”的原则,使程序易于阅读。
例如, total、_name1、Int 和 area_of_circle 是合法的自定义标识符,
而 9total、flag$、left..right
和 int 是非法的自定义标识符。

2.2 整数类型

整数类型是指不存在小数部分的数据类型。

2.2.1 整型常量(整数)
C 语言中的整数有十进制、八进制和十六进制 3 种表现形式。
(1)十进制整数由正、负号和阿拉伯数字 0~9 组成,但首位数字不能是 0。
(2)八进制整数由正、负号和阿拉伯数字 0~7 组成,首位数字必须是 0。
(3)十六进制整数由正、负号和阿拉伯数字 0~9、英文字符 a~f 或 A~F 组成,首位数
字前必须有前缀 0x 或 0X。
例如:10、010 和 0x10 分别为十进制、八进制和十六进制整数,它们表示着不同数值的
– 12 –
第2章 基本数据类型和表达式

整数。10 是十进制数值,010 的十进制数值是 8,0x10 的十进制数值是 16。


又如:16、020 和 0X10 分别为十进制、八进制和十六进制整数,它们表示着同一个数值
的整数,十进制数值 16。
0386 和 0x1g 是非法的整型常量,因为 0386 作为八进制整数含有非法数字 8,而 0x1g
作为十六进制整数含有非法字符 g。
任何一个整数都可以用 3 种形式来表示,这并不影响它的数值。例如,表示十进制数值
是 10 的整数,可以采用 10、012 或 0Xa。所谓十进制、八进制和十六进制只是整数数值的 3
种表现形式而已。

2.2.2 整型变量
整型变量在定义时用类型名 int。例如:
int ai, bi, ci, di=0;
定义了 4 个整型变量。定义变量以后,就可以使用它了。例如:
ai=1;
bi=-27;
ci=0;
对上述变量赋值。
整型变量的值是整数,它的取值范围是有限的,这与计算机的字长和具体的 C 编译系统
有关。ANSI C 规定整型数据的最小取值范围是[ – 32768,32767],TC 的规定与之相同,此时
每个整型数据在内存中占用两个字节存储。而使用 VC++编译系统时,整型数据的取值范围
是[ – 2147483648,2147483647],每个整型数据在内存中占用 4 个字节存储。
为了处理不同取值范围的整数,C 语言还提供了扩展的的整数类型,如短整型、长整型
和无符号整型等(见 2.6.2 节) 。

2.2.3 整型数据的输入和输出
整型数据(整型量)包括整型常量和变量。
整型数据的输入和输出可以调用函数 scanf()和 printf(),在函数调用的格式控制字符串中
相应的格式控制说明见表 2-3。
表 2-3 格式控制说明

函 数 类 型 格 式 含 义

%d 以十进制形式输入/输出一个整数
scanf
int %o 以八进制形式输入/输出一个整数
printf
%x 以十六进制形式输入/ 输出一个整数

例 2-3 调用 printf()函数输出整型数据。
源程序:
#include <stdio.h>
void main( )

– 13 –
C 语言程序设计

{
printf("%d,%o,%x\n", 10, 10, 10);
printf("%d,%d,%d\n", 10, 010, 0x10); /*等价于 printf("%d, %d, %d\n", 10, 8, 16);*/
printf("%d,%x\n", 012, 012); /*等价于 printf("%d, %x\n", 10, 0xa);*/
}
运行结果如下:
10,12,a
10,8,16
10,a
根据表 2-3 给出的格式控制说明,可以选用十进制、八进制和十六进制 3 种形式来输出
一个整数。同时,该整数也可以有十进制、八进制和十六进制 3 种表现形式,二者可以不一
致,输出结果以格式控制说明为准。例如,八进制数 012 就可以用十进制和十六进制的形式
输出,因为不管一个整数采用哪种表现形式,它的数值是确定的。
例 2-4 调用 scanf()函数和 printf()函数,实现整型数据的输入和输出。
源程序:
#include <stdio.h>
void main( )
{
int a,b ;

scanf("%o%d", &a, &b);


printf("%d%5d\n", a, b); /* %5d 指定变量 b 的输出宽度为 5 */
}
运行结果如下:
17 17 <CR>
15 17
输入时,用格式控制说明指定的形式来读入数据。首先,以八进制形式读入 17,相当于
将 017(即 15)赋值给变量 a。然后,以十进制形式读入 17,将其赋值给变量 b。
输出格式控制说明%md,指定了整型数据的输出宽度为 m(包括符号位) ,若数据的实
际位数(含符号位)小于 m,则左端补空格;若大于 m,则按实际位数输出。以%5d 输出 b
的值 17,左端补了 3 个空格。

2.3 实数类型

实数类型又称为浮点型,指存在小数部分的数。

2.3.1 实型常量(实数)
实型常量又称为浮点数,可以用十进制浮点表示法和科学计数法表示。
(1)浮点表示法:实数由正号/负号、阿拉伯数字 0~9 和小数点组成,必须有小数点,
并且小数点的前、后至少一边要有数字。实数的浮点表示法又称实数的小数形式。
– 14 –
第2章 基本数据类型和表达式

(2)科学计数法:实数由正号/负号、数字和字母 e(或 E)组成。e 是指数的标志,在 e


之前要有数据,e 之后的指数只能是整数。实数的科学计数法又称实数的指数形式。
例如:3.14 和 6.026E – 27 是合法的实数,而 0.2E2.3 和 E – 5 是非法的实数。
–
科 学 计 数 法 一 般 用 于 表 示 很 大 或 很 小 的 数 , 如 普 朗 克 常 数 6.026 × 10 27 表 示 为
6.026E – 27,也可表示为 60.26e – 28、602.6e – 29 或 0.6026e – 26。

2.3.2 实型变量
实型变量有单精度浮点型(float)和双精度浮点型(double)两种不同的类型,它们表
示数值的方法是一样的,主要区别在于表示数值的精度和取值范围不同。

1.单精度浮点型 float

单精度浮点型变量就是一般的实型变量,例如:
float fa, fb, fc;
定义了 3 个单精度浮点型变量。定义变量以后,就可以使用它了,例如:
fa=3.14159;
fb=6.026e-27;
fc=1234567.89;
对上述变量进行赋值。
由于实数在计算机中只能近似表示,在运算中也会产生误差,而且误差的积累很快,为
了减小误差,提高计算精度,C 语言还提供了另一种实数类型,这就是双精度浮点型。

2.双精度浮点型 double

与单精度浮点型数据相比,双精度浮点型数据具有较高的精度。
每个单精度浮点型数据在内存中占用 4 个字节存储,它的有效数字一般有 7~8 位,取值
范围约为±(10-38~1038)。双精度浮点型数据所占的存储空间是单精度浮点型数据的两倍,即
8 个字节,它的有效数字一般有 15~16 位,取值范围约为±(10-308~10308)。这些指标与具体
的计算机和 C 编译系统有关。
采用双精度浮点型数据会大大增加有效位数,减少舍入误差所造成的影响,但它的运行
速度比单精度浮点型数据慢。
就实型数据而言,数值精度和取值范围是两个不同的概念。例如实数 1234567.89 在单精
度浮点型数据的取值范围内,但它的有效数字超过了 8 位,如果将它赋值给单精度浮点型变
量,该变量的值就是 1234567.80,其中最后 1 位是一个随机数,损失了有效数字,降低了精
度。
实型常量的类型都是双精度浮点型。

2.3.3 实型数据的输入和输出
实型数据(实型量)包括实型常量和变量。
实型数据的输入和输出也可以调用函数 scanf()和 printf(),在函数调用的格式控制字符串
中相应的格式控制说明见表 2-4。

– 15 –
C 语言程序设计

表 2-4 格式控制说明(实型数据)
函 数 数据类型 格 式 含 义

float %f 以小数形式输出浮点数(保留 6 位小数)


printf
double %e 以指数形式输出浮点数(小数点前有且仅有一位非 0 的数字)

%f
float 以小数形式或指数形式输入一个单精度浮点数
%e
scanf
%lf
double 以小数形式或指数形式输入一个双精度浮点数
%le

例 2-5 实型数据的输出。
源程序:
#include <stdio.h>
void main( )
{
float f=123.45;
double d=3.1415926;

printf("%f,%e\n", f, f);
printf("%f,%e\n", d, d);
printf("%5.3f,%5.2f,%.2f \n", d, d, d); /* 指定数据的输出宽度*/
}
运行结果如下:
123.450000,1.23450e+02
3.141593,3.14159e+00
3.142, 3.14,3.14
根据表 2-4,输出浮点数时,单精度和双精度浮点型数据使用相同的格式控制说明%f 和%e。
输出格式控制说明%m.nf,指定输出浮点型数据时,保留 n 位小数,且输出宽度是 m(包
括符号位和小数点) 。若数据的实际位数小于 m,左端补空格;若大于 m,按实际位数输出。
输出 d 的值 3.1415926,%5.3f 输出 3.142(保留 3 位小数),%5.2f 输出 3.14(保留 2 位小数,
左端补 1 个空格),%.2f 输出 3.14(保留 2 位小数,按实际位数输出)。
例 2-6 设单精度浮点型数值和双精度浮点型数值分别有 7 位和 16 位有效数字,调用
printf()函数输出实型数据。
源程序:
#include <stdio.h>
void main( )
{

float f;
double d1, d2;

f=1234567890123.123456;
d1=1234567890123.123456;
– 16 –
第2章 基本数据类型和表达式

d2=1234567890123.12;
printf("f=%f\nd1=%f\nd2=%f\n ", f, d1, d2);
}
运行结果如下:
f=1234567954432.000000
d1=1234567890123.123540
d2=1234567890123.120120
1234567890123.123456 有 19 位数字,但单精度浮点型变量 f 只能取 7 位有效数字,即
双精度浮点型变量 d1 只能取 16 位有效数字,即 1234567890123.123???,
1234567??????.??????,
降低了精度。而 1234567890123.12 有 15 位数字,用双精度浮点型变量 d2 表示不会降低精度,
即 1234567890123.12????。运行结果中与?对应的数字不是有效数字,这些数字没有意义,
值也不确定。由此可知,如果原始数据的有效数字超出计算机能表示的精度,超出部分的数
字不再是有效数字,即原始数据的精度被降低。
例 2-7 运行下列程序,输入 4 次 1234567890123.123456,观察运行结果有何区别(设
单精度浮点型数值和双精度浮点型数值分别有 7 位和 16 位有效数字)。
源程序:
#include <stdio.h>
void main( )
{
float f1, f2;
double d1, d2;

scanf("%f%e", &f1, &f2);


scanf("%lf%le", &d1, &d2);
printf("f1=%f,f2=%f\n", f1, f2);
printf("d1=%f,d2=%f\n", d1, d2);
}
运行结果如下:
1234567890123.123456 1234567890123.123456<CR>
1234567890123.123456 1234567890123.123456<CR>
f1=1234567954432.000000,f2=1234567954432.000000
d1=1234567890123.123540,d2=1234567890123.123540
根据表 2-4,输入浮点数时,格式控制说明%f 和%e 可以通用。输入双精度浮点数时,
在 f 和 e 前必须加限制符 l,否则会出错。
本例中运行结果的解释同例 2-6。

2.4 字符类型

2.4.1 字符常量
字符常量指单个字符,用一对单引号及其所括起的字符来表示。例如:’A’、’a’、’9’、’$’
– 17 –
C 语言程序设计

是字符常量,它们分别表示字母 A、a、数字字符 9 和符号$。


ASCII 字符集(见附录 B)中列出了所有可以使用的字符,共 256 个,它具有以下特性:
(1)每个字符都有一个惟一的次序值,即 ASCII 码。
(2)数字字符’0’,’1’,’2’,…,’9’的 ASCII 码按升序连续排列。
(3)大写字母’A’,’B’,’C’,…,’Z’的 ASCII 码按升序连续排列。
(4)小写字母’a’,’b’,’c’,…,’z’的 ASCII 码按升序连续排列。
每个字符在内存中占用 1 个字节,用于存储它的 ASCII 码。所以 C 语言中的字符具有数
值特征,可以像整数一样参加运算,此时相当于对字符的 ASCII 码进行运算。例如:字符’A’
的 ASCII 码是 65,则’A’+1 = 66,对应于字符’B’,这是因为所有大写字母的 ASCII 码按升序
连续排列,字符’A’的 ASCII 码加 1,就是字符’B’的 ASCII 码。

2.4.2 字符变量
字符变量在定义时用类型名 char,例如:
char ac, bc;
定义了两个字符变量。字符变量的值是字符常量,即单个字符,例如:
ac = ’A’;
bc = ’#’;
由于字符型数据具有数值特征,它的值可以用相应的 ASCII 码表示,即可以用整数来表
示字符。例如:字符’A’的 ASCII 码值是 65,所以 ac = ’A’与 ac = 65 等价。
既然字符变量的值可以是字符或整数,它就可以被定义成整型变量;同时整型变量的值
也可以是字符型数据,它可以被定义成字符变量。也就是说,整型变量和字符变量的定义和
值都可以互相交换。请读者注意,此时整型数据的取值范围是有效的 ASCII 码。
例 2-8 已知字符’A’的 ASCII 码值是 65,观察下列程序的运行结果。
源程序:
#include <stdio.h>
void main( )
{
int i;
char ch;

i = 65;
ch = ’A’;
printf("%d,%c\n", i, ch);
i = ’A’;
ch = 65;
printf("%d,%c\n", i, ch);
}
运行结果如下:
65,A
65,A

– 18 –
第2章 基本数据类型和表达式

2.4.3 字符型数据的输入和输出
字符型数据包括字符常量和变量。
字符的输入输出可以调用函数 getchar()、putchar()和 scanf()、printf()。

1.字符输入函数 getchar()和字符输出函数 putchar()

getchar()函数从键盘输入一个字符,putchar()函数用于输出一个字符。
例 2-9 调用 getchar()和 putchar()处理字符的输入输出。
源程序:
#include <stdio.h>
void main( )
{
char ch1, ch2;

ch1=getchar( ); /* 从键盘输入一个字符,并赋值给变量 ch1 */


ch2=getchar( );
putchar(ch1); /* 输出存放在变量 ch1 中的字符 */
putchar(’#’);
putchar(ch2);
}
运行结果如下:
Ab <CR>
A#b
从运行结果看,输入输出字符时,字符两侧没有单引号,这与字符常量在程序中的表示
不同。
getchar()函数和 putchar()函数只能处理单个字符的输入和输出。

2.格式化输入函数 scanf()和格式化输出 printf()

scanf()函数和 printf()函数除了处理整型数据和浮点型数据的输入输出外,也可以处理字
符型数据的输入和输出。此时,在函数调用的格式控制字符串中相应的格式控制说明为%c。
例 2-10 调用 scanf()和 printf()处理字符的输入输出。
源程序:
#include <stdio.h>
void main( )
{
char ch1, ch2, ch3;

scanf("%c%c%c",&ch1, &ch2, &ch3);


printf("%c%c%c%c%c",ch1, ’#’, ch2, ’#’, ch3);
}
运行结果 1:
– 19 –
C 语言程序设计

AbC <CR>
A#b#C
运行结果 2:
A bC <CR>
A# #b
输入多个字符时,这些字符之间不能有间隔。如果使用了间隔(如空格’ ’),由于它本身
也是字符,该间隔就被作为输入字符。本例的运行结果 2,输入字符 A 后,输入了一个空格,
所以 ch2 的值是’ ’,ch3 的值是’b’。
C 语言中,一个字符型数据在内存中用 1 个字节存储它的 ASCII 码,它既可以按字符形
式输出,也可以按整数形式输出。按字符形式输出时,可以调用函数 putchar()或 printf()(格
式控制说明用%c) ,系统自动将存储的 ASCII 码转换为相应的字符后输出。按整数形式输出
时,可以调用函数 printf()(格式控制说明选用%d、%o、%x 等),直接输出它的 ASCII 码。
同样,一个整数(在有效的 ASCII 码范围内)也可以按字符形式输出,此时输出 ASCII 码等
于该数的字符。
例 2-11 已知字符’b’的 ASCII 码值是 98,’A’的 ASCII 码值是 65,观察下列程序的运行
结果。
源程序:
#include <stdio.h>
void main( )
{
char ch=’b’;

printf("%c,%d\n", ’b’, ’b’);


printf("%c,%d\n", 98, 98);
printf("%c,%d\n", 97, ’b’-1);
printf("%c,%d\n", ch-’a’+’A’, ch-’a’+’A’);
}
运行结果如下:
b,98
b,98
a,97
B,66
C 语言中,数字和数字字符是有区别的。例如,1 是数字,是整数,而’1’是字符,它的
值是 ASCII 码 49。
例 2-12 已知字符’0’的 ASCII 码是 48,观察下列程序的运行结果。
源程序:
#include <stdio.h>
void main( )
{
char ch=’1’;
int val=1;

– 20 –
第2章 基本数据类型和表达式

printf("%d,%c\n",val, ch);
printf("%d,%d\n", val, ch);
printf("%d\n", ch-’0’);
printf("%c\n", val+’0’);
}
运行结果如下:
1,1
1,49
1
1
字符运算在实际编程中是很有用的,例如,若变量 ch 的值是小写字母’a’~’z’,则运算
ch – ’a’+’A’可把小写字母转换为大写字母。又如,若变量 ch 的值是数字字符’0’~’9’,运算 ch – ’0’
可把数字字符转换为数字;若变量 val 的值是数字 0~9,运算 val+’0’可把数字转换为数字字符。

2.4.4 转义字符
有一些字符,如回车、退格等控制码,它们不能在屏幕上显示,也无法从键盘输入,只
能用转义字符来表示。转义字符由反斜杠跟上一个字符或数字组成,它把反斜杠后面的字符
或数字转换成别的意义。虽然转义字符形式上由多个字符组成,但它是字符常量,只代表一
个字符。表 2-5 列举了常见的转义字符。
表 2-5 转义字符

字 符 含 义

\n 换行

\t 横向跳格

\\ 反斜杠

\" 双引号

\’ 单引号

\ddd 1~3 位八进制整数所代表的字符

\xhh 1~2 位十六进制整数所代表的字符

转义字符的使用方法与其他字符常量相同。例如:
printf("%c", ’\t’);
printf("this is a test");
putchar(’\n’);
该程序段首先横向跳格,然后打印出 this is a test,再换行。
表 2-5 中最后两行采用 ASCII 码(八进制整数、十六进制整数)表示一个字符。例如,
\102 表示 ASCII 码是八进制数 102 的字符,即字母’B’;\x41 表示 ASCII 码是十六进制数 41
的字符,即字母’A’。这样,ASCII 字符集中所有的字符都可以用转义字符表示。
– 21 –
C 语言程序设计

例 2-13 已知字符’a’的 ASCII 码是 97,对应八进制数 141,十六进制数 61,观察下列程


序的运行结果。
源程序:
#include <stdio.h>
void main( )
{
printf("%o,%x\n", ’a’, ’a’);
printf("%c,%c,%c,%c,%c\n", ’a’, 0141, 0x61, ’\141’, ’\x61’);
}
运行结果如下:
141,61
a,a,a,a,a

2.5 表 达 式

常量、变量、函数是最简单的表达式,用运算符将表达式正确连接起来的式子也是表达
式。例如:
3
10+sqrt(2.0)
x – 2*3.14
都是表达式。也可以认为,表达式就是由运算符和运算对象(操作数)组成的有意义的运算
式子,它的值和类型由参加运算的运算符和运算对象决定。其中,运算符就是具有运算功能
的符号,运算对象指常量、变量和函数等表达式。
C 语言中有多种表达式和相应的运算符,包括算术表达式、赋值表达式、关系表达式、
逻辑表达式、条件表达式和逗号表达式等。本节介绍算术表达式、赋值表达式和逗号表达式。

2.5.1 算术表达式

1.算术运算符

算术运算符分为单目运算符和双目运算符两类(如表 2-6 所示)


,单目运算符需要一个操
作数,而双目运算符需要两个操作数。
表 2-6 算术运算符

目 数 单 目 双 目

运算符 ++ –– + – + – * / %

名 称 自增 自减 正值 负值 加 减 乘 除 模(求余)

在使用算术运算符时,需要注意以下几点。
(1)求余运算符取整型数据相除的余数,它不能用于实型数据的运算。

– 22 –
第2章 基本数据类型和表达式

例如:表达式 5%6 的值为 5,9%4 的值为 1,100%4 的值为 0(表示 100 能被 4 整除)。


(2)如果对两个整型数据作除法运算,其结果一定是整型。
例如:表达式 10/3 的值为 3,1/4 的值为 0。
(3)当两个整型数据相除且不能除尽时,如果有一个操作数是负数,则除和求余的结果
与具体实现有关。例如, – 10/3 在有的机器上得到 – 3,有的机器则得到 – 4。


(4)+和 – 在作为数值常量的符号时,是单目运算符,如+10 和 – 10。
(5)双目运算符两侧操作数的类型要相同,否则,系统自动进行类型转换,使它们具有
相同的类型,然后再运算。
类型转换的一般规则如下:
char →int → float → double
例如:计算表达式 12 – ’0’,先将’0’转换为整数 48,运算结果为 – 36。计算表达式 10.0/2,
先将 2 转换为双精度浮点数 2.0,运算结果为 5.0。
关于类型转换的详细说明见 2.6.3 节。

2.自增运算符和自减运算符

自增运算符++和自减运算符− −有两个功能。
(1)使变量的值增 1 或减 1。
例如,设 n 是一个整型变量并已赋值,则:
++n 和 n++都相当于 n=n+1;
− −n 和 n− −都相当于 n=n – 1。
(2)取变量的值作为表达式的值
例如,计算表达式++n 和 n++的值,则:
++n 的运算顺序是先执行 n=n+1,再将 n 的值作为表达式++n 的值;
n++的运算顺序是先将 n 的值作为表达式 n++的值,再执行 n=n+1。
例 2-14 自增运算符和自减运算符的运用。
源程序:
#include <stdio.h>
void main( )
{
int m, n;

n=3;
m=++n;
printf("m=%d,n=%d\n",m, n);
n=3;
m=n++;
printf("m=%d,n=%d\n",m, n);
}
运行结果如下:
m=4,n=4

– 23 –
C 语言程序设计

m=3,n=4
说明:m=++n 相当于顺序执行 n=n+1 和 m=n;
m=n++相当于顺序执行 m=n 和 n=n+1。
注意:自增运算符和自减运算符的运算对象只能是变量,不能是常量或表达式。例如,
3++或++(i+j)都是非法的表达式。

3.算术运算符的优先级和结合性

在算术四则运算中,遵循“先乘除后加减”的运算规则。同样,在 C 语言中,计算表达
式的值也需要按运算符的优先级从高到低顺序计算。例如,表达式 a+b*c 相当于 a+(b*c),这
是因为操作数 b 的两侧有运算符+和*,而*的优先级高于+。
如果操作数两侧运算符的优先级相同,则按结合性(结合方向)决定计算顺序。若结合
方向为“从左到右” ,则操作数先与左面的运算符结合;若结合方向为“从右到左” ,则操作
数先与右面的运算符结合。
C 语言中部分运算符的优先级和结合性见表 2-7,同一行上的运算符优先级相同,不同行
的运算符的优先级按从高到低的次序排列,可以用圆括号来改变运算符的执行次序。
表 2-7 部分运算符的优先级和结合性
运算符种类 运 算 符 结 合 方 向 优 先 级

逻辑运算符 ! 高
从右向左(右结合)
++ –– + – (单目)

算术运算符 * / % (双目)

+ – (双目)

< <= > >=


关系运算符 从左向右(左结合)
== !=

&&
逻辑运算符
||

条件运算符 ?:
从右向左(右结合)
赋值运算符 = += –= *= /= %=

逗号运算符 , 从左向右(左结合)

例如,表达式 – 5+3%2 等价于( – 5)+(3%2),结果为 – 4;表达式 3*5%3 等价于(3*5)%3,


结果为 0,这是因为 5 两侧运算符*和%的优先级相同,按从左到右的结合方向,5 先与*结合;
表达式 – i++等价于 – (i++),这是因为 i 两侧运算符 – 和++的优先级相同,按从右到左的结合方
向,i 先与++结合。

4.算术表达式

用算术运算符将运算对象连接起来的符合 C 语言语法规则的式子称为算术表达式。
其中,
运算对象包括常量、变量和函数等表达式。算术表达式的值和类型由参加运算的运算符和运

– 24 –
第2章 基本数据类型和表达式

算对象决定。
例 2-15 将下列数学式转换为 C 表达式。
(1)s(s−a)(s−b)(s−c)
(2)(x+2)e2x

− b + b 2 − 4ac 2
(3) (b – 4ac≥0)
2a

转换后:
(1)s*(s – a)*(s – b)*(s – c),必须用运算符连接操作数。
(2)(x+2)*exp(2*x),调用数学库函数 exp(x) 计算指数函数 ex。
(3)( – b+sqrt(b*b – 4*a*c))/(2*a),调用数学库函数 sqrt(x)计算平方根 x (x≥0)。
例 2-16 双目算术运算符的运用。
源程序:
#include <stdio.h>
void main( )
{
int x, y;

scanf("%d%d", &x, &y);


printf("x+y=%d,x-y=%d\n", x+y, x-y);
printf("(x/y)*y+x mod y=%d \n", x/y*y+x%y);
}
运行结果如下:
7 2 <CR>
x+y=9,x-y=5
(x/y)*y+x mod y=7
例 2-17 自增、自减运算符和加、减运算符的混合运用。
源程序:
#include <stdio.h>
void main( )
{
int a, b, c;

b=5; c=5;
a=++b+c--;
printf("%d,%d,%d\n", a, b, c);
a=b---c;
printf("%d,%d,%d\n", a, b, c);
a=-b+++c;
printf("%d,%d,%d\n", a, b, c);
}

– 25 –
C 语言程序设计

运行结果如下:
11,6,4
2,5,4
-1,6,4
分析:a=++b+c – – 相当于顺序执行 b=b+1, a=b+c, c=c – 1。
a=b – – – c 等价于 a=(b – – ) – c,相当于顺序执行 a=b – c, b=b – 1,因为 C 编译系统在
处理时从左向右尽可能多地将若干字符组成一个运算符,故 – – – 理解为 – – 和 – 。
a= – b+++c 等价于 a= – (b++)+c,相当于顺序执行 a= – b+c, b=b+1。

5.副作用的说明

C 语言中,自增和自减是两个很特殊的运算符,相应的运算会得到两个结果。例如,设 n=3,
表达式 n++经过运算之后,其值为 3,同时变量 n 的值增 1 为 4。可见,在求解表达式时,变
量的值改变了,我们称这种变化为副作用。在编程时,副作用的影响往往会使得运算的结果与
预期的值不相符合,故建议读者慎用自增、自减运算,尤其不要用它们构造复杂的表达式。
另外,C 语言中,根据运算符的优先级和结合性决定表达式的计算顺序,但对运算符两
侧操作数的求值顺序并未做出明确的规定,允许编译系统采取不同的处理方式。例如,计算
表达式 f( )+g( )时,可以先求 f( ),再求 g( ),也可以相反。如果求值顺序的不同影响了表达
式的结果,即相同的源程序在不同的编译系统下运行,结果可能不同,就给程序的移植造成
了困难。所以,在实际应用中应该避免这种情况。

2.5.2 赋值表达式

1.赋值运算符

C 语言将赋值作为一种运算,并定义了赋值运算符=。它的作用是把一个表达式的值赋予
一个变量,如 x=3*4。赋值运算符的左边必须是一个变量。
赋值运算符的优先级比算术运算符低,它的结合方向是从右向左(如表 2-7 所示)。例如,
表达式 x=(3*4)等价于 x=3*4,表达式 x=y=3 等价于 x=(y=3)。

2.赋值表达式

用赋值运算符将一个变量和一个表达式连接起来的式子称为赋值表达式。赋值表达式的
简单形式如下:
变量=表达式
赋值表达式的运算过程如下。
① 计算赋值运算符右侧表达式的值。
② 将赋值运算符右侧表达式的值赋给赋值运算符左侧的变量。
③ 将赋值运算符左侧的变量的值作为赋值表达式的值。
例如,设 n 是整型变量,已赋值 2,求解赋值表达式 n=n+1,首先计算 n+1 得到 3,再将
3 赋给 n,并取 n 的值作为该赋值表达式的值。
在赋值运算时,如果赋值运算符两侧的数据类型不同,在上述运算过程的第②步,系统

– 26 –
第2章 基本数据类型和表达式

首先将赋值运算符右侧表达式的类型自动转换成赋值运算符左侧变量的类型,再给变量赋值,
并将变量的类型作为赋值表达式的类型。
例如,设 n 是整型变量,计算表达式 n=3.14*2 的值,首先计算 3.14*2 得到 6.28,将 6.28
转换成整型值 6 后赋给 n。该赋值表达式的值是 6,类型是整型。
又如,设 x 是双精度浮点型变量,计算表达式 x=10/4 的值,首先计算 10/4 得到 2,将 2
转换成双精度浮点型值 2.0 后赋给 x。该赋值表达式的值是 2.0,类型是双精度浮点型。
在赋值表达式中,赋值运算符右侧的表达式,也可以是一个赋值表达式。例如:
x=(y=3)
求解时,先计算表达式 y=3,再将该表达式的值 3 赋给 x,结果使得 x 和 y 都赋值为 3,相当
于计算 x=3 和 y=3 两个赋值表达式。
由于赋值运算符的结合性是从右到左,因此 x=(y=3) 等价于 x=y=3,即多个简单赋值运
算可以组合为一个连赋值的形式。

3.复合赋值运算符

赋值运算符分为简单赋值运算符和复合赋值运算符。简单赋值运算符就是=,复合赋值
运算符又分为复合算术赋值运算符和复合位赋值运算符,在=前加上算术运算符就构成了复
合算术赋值运算符(如表 2-8 所示),加上位运算符(见 9.8.1 节)就构成了复合位赋值运算
符。
所以,赋值表达式的一般形式是:
变量 赋值运算符 表达式
表 2-8 复合算术赋值运算符
运 算 符 名 称 等 价 关 系

+= 加赋值 x+=exp 等价于 x=x+(exp)

–= 减赋值 x–=exp 等价于 x=x– (exp)

*= 乘赋值 x*=exp 等价于 x=x*(exp)

/= 除赋值 x/=exp 等价于 x=x/(exp)

%= 取余赋值 x%=exp 等价于 x=x%(exp)

注:exp 指表达式。

例 2-18 赋值运算的运用。
源程序:
#include <stdio.h>
void main( )
{
int x, y, z;

z=(x=7)+(y=3);
printf("%d,%d,%d\n", x, y, z);
x=y=z=x+2;
– 27 –
C 语言程序设计

printf("%d,%d,%d\n", x, y, z);
x*=y-3;
printf("%d,%d,%d\n", x, y, z);
}
运行结果如下:
7,3,10
9,9,9
54,9,9
说明:z=(x=7)+(y=3) 相当于顺序执行 x=7, y=3, z=x+y;
x=y=z=x+2 相当于顺序执行 z=x+2, y=z, x=y;
x*=y – 3 等价于 x=x*(y – 3),而不是 x=x*y – 3。

2.5.3 逗号表达式
C 语言中,逗号既可作分隔符,又可作运算符。逗号作为分隔符使用时,用于间隔说明
语句中的变量或函数中的参数,例如:int a, b, c; 和 printf("%d %d", x, y);等。
逗号作为运算符使用时,将若干个独立的表达式联结在一起,组成逗号表达式。逗号表
达式的一般形式是:
表达式 1,表达式 2,……,表达式 n
逗号表达式的运算过程是:先计算表达式 1 的值,然后计算表达式 2 的值,……最后计
算表达式 n 的值,并将表达式 n 的值作为逗号表达式的值,将表达式 n 的类型作为逗号表达
式的类型。
例如,设 a,b,c 都是整型变量,计算逗号表达式 (a=2), (b=3), (c=a+b) 的值。该表达
式由 3 个独立的表达式通过逗号运算符联结而成,从左到右依次求解这 3 个表达式后,该逗
号表达式的值和类型由最后一个表达式 c=a+b 决定,值是 5,类型是整型。
逗号运算符的优先级是所有运算符中最低的,它的结合性是从左到右(见表 2-7)。例如,
表达式(a=2), (b=3), (c=a+b) 等价于 a=2, b=3, c=a+b。
逗号表达式常用于 for 循环语句中(见第 5 章)。

2.6 数据的存储和类型转换

2.6.1 数据的存储

1.整型数据的存储

计算机处理的所有的信息都以二进制形式表示,即数据的存储和计算都采用二进制。我
们首先介绍整型数据的存储格式,设每个整数在内存中占用两个字节存储,最左边的一位(最
高位)是符号位,0 代表正数,1 代表负数。
数值可以采用原码、反码和补码等不同的表示方法。为了便于计算机内的运算,一般以补码
表示数值。正数的原码、反码和补码相同,即符号位是 0,其余各位(15 位)表示数值。例如:
– 28 –
第2章 基本数据类型和表达式

1 的补码是 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1

127(27 – 1)的补码是 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1
15
两个字节的存储单元能表示的最大正数是 2 – 1,即 32767。
32767 的补码是 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
负数的原码、反码和补码不同,具体特点如下。
(1)原码:符号位是 1,其余各位表示数值的绝对值。
(2)反码:符号位是 1,其余各位对原码取反。
(3)补码:反码加 1。
例如:
– 1 的原码是 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1

– 1 的反码是 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0

– 1 的补码是 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
同理,可以写出 – 32767 的补码:
– 32767 的补码是 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1
减 1,得到 – 32768,这是两个字节的存储单元能表示的最小负数。
– 32768 的补码是 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
通过对整型数据存储格式的介绍,读者就能理解 2.2.2 节中的说明——如果整型数据在内
存中占用两个字节存储,它的取值范围是[−32768,32767]。表 2-9 给出了用补码表示的一些
数值(以递减顺序排列)。
表 2-9 数的补码
数 值
补 码
十 进 制 十 六 进 制

32767 7fff 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

32766 7ffe 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0

…… …… ……

2 0002 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0

1 0001 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1

0 0000 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

–1 ffff 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

–2 fffe 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0

…… …… ……

–32767 8001 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1

–32768 8000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

– 29 –
C 语言程序设计

2.实型数据的存储

存储实型数据时,分为符号位、阶码和尾数 3 部分。例如:实数−1.2345e+02 是负数,


阶码是 2,尾数是 1.2345。实型数据的存储格式不属于本书的范围,在此不作详细讨论。

符号位 阶 码 尾 数

3.字符型数据的存储

每个字符在内存中占用 1 个字节,存储它的 ASCII 码。例如字符 ’A’ 的 ASCII 码为 65,


它在内存中以下列形式存放:
0 1 0 0 0 0 0 1

2.6.2 整数类型的扩展

1.整数类型的扩展

为了处理不同取值范围的整数,除了基本整型 int 以外,C 语言还提供了扩展的整数类型


(如表 2.10 所示) ,它们的表示方法是在 int 之前加上限定词 short、long 或 unsigned。
无符号的整型数据指不带符号的整数,即 0 或正整数,不包括负数。存储有符号的整型
数据时,存储单元的最高位是符号位,其余各位表示数值;存储无符号(指定 unsinged)的
整型数据时,存储单元全部用于表示数值。


设 int 型和 unsinged 型数据在内存中都占用两个字节存储,则 int 型数据的取值范围是
[ – 32768,32767],而 unsinged 型数据的取值范围是 [0,65535]。
int 型最大正数 32767(215 – 1) 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

unsinged 型最大正数 65535(216 – 1) 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1


C 语言并未规定各类整型数据的长度,只要求 short 型不长于 int 型,long 型不短于 int
型。表 2-10 列出了 ANSI C 规定的整数类型和相关数据,TC 的规定与之相同,而使用 VC++
编译系统时,int 型和 unsigned 型数据的长度是 32 位。
本书中讨论的整数类型,以表 2-10 为准。由于表 2-10 中指定 short 的数据类型与不指定
short 的数据类型的相关数据相同,我们不再单独讨论指定 short 的数据类型(即短整型和无
符号短整型)。
表 2-10 整数类型

名 称 类 型 名 数据长度 最小取值范围

[有符号]整型 int 16 位 –32768 ~ 32767 (–2 ~2 –1)

[有符号]短整型 short [int] 16 位 –32768 ~ 32767 (–2 ~2 –1)

[有符号]长整型 long [int] 32 位 –2147483648 ~ 2147483647 (–2 ~2 –1)

无符号整型 unsigned [int] 16 位 0 ~ 65535 (0 ~2 –1)

– 30 –
无符号短整型

无符号长整型

注:方括号中的内容可以省略。

2.整型常量的表示
称 类 型 名

unsigned short [int]

unsigned long [int]

只要整型常量的值不超出表 2-10 中列出的取值范围,就是合法的常量。判断整型常量的


类型,首先根据整数后的字母后缀,如果整数后没有出现字母,就根据值的大小。
第2章 基本数据类型和表达式

数据长度

16 位

32 位
0 ~ 65535

0 ~ 4294967295
最小取值范围


(0 ~2 –1)

(0 ~2 –1)
续表

(1)根据整数后的字母确定它的类型。后缀 l 或 L 表示 long 型常量,如 – 12L,


01234567890L;后缀 u 或 U 表示 unsigned 型常量,如 12u、034u、0x2fdU;后缀 lu 或 LU 表
示 unsigned long 型常量,如 4294967295LU。
(2)根据整型常量的值确定它的类型。例如:取值在 – 32768~32767 之间的整数是 int 型
常量;超出该范围,但取值在 32768~65535 之间的非负整数可以看成 unsigned 型常量;超出
上述范围,但取值在 – 2147483648~2147483647 之间的整数是 long 型常量;超出上述范围,
但取值在 2147483648~4294967295 之间的非负整数可以看成 unsigned long 型常量。
注意:12 与 12L 在数值上相等,但它们的类型不同,存储格式也不同。
12 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0

12L 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0

3.整型数据的输入输出

调用函数 scanf()和 printf()实现整型数据的输入和输出时,应根据数据的类型和输入输出


的形式,在函数调用的格式控制字符串使用相应的格式控制说明(如表 2-11 所示) 。基本的
格式控制说明有%d、%u、%o 和%x,其中%u 表示以十进制形式输入输出无符号整数, %d、
%o 和%x 的说明见表 2-3。输入输出长整型数据时,在格式控制说明中加限定词 l。
表 2-11 格式控制说明(整型数据)
输入输出形式
数 据 类 型
十进制 八进制 十六进制

int %d %o %x

long %ld %lo %lx

unsigned %u %o %x

unsigned long %lu %lo %lx

例 2-19 整型数据的输入输出。
源程序:
#include <stdio.h>

– 31 –
C 语言程序设计

void main( )
{
int ai; long cl; unsigned bu; unsigned long dul;

ai = 32767; bu = 65535; cl = -2147483648; dul = 4294967295;


printf("%d,%u,%ld,%lu\n",ai, bu, cl, dul);
ai = 32767; bu = 65535U; cl = -2147483648L; dul = 4294967295LU;
printf("%x,%x,%lx,%lx\n",ai, bu, cl, dul);
}
运行结果如下:
32767,65535, -2147483648,4294967295
7fff,ffff,80000000,ffffffff

2.6.3 数据类型转换
在 C 语言中,不同类型的数据可以混合运算,但这些数据首先要转换成同一类型,然后
再作运算。数据类型的转换包括自动转换和强制转换。自动转换由 C 语言编译系统自动完成,
强制转换则通过特定的运算完成。

1.自动类型转换

数据类型的自动转换需遵循的规则如图 2-1 所示。

高 double ← float

unsigned long ← long

unsigned ← unsigned short

低 int ← char, short

图 2-1 数据类型自动转换规则

(1)对于非赋值运算,为保证运算的精度不降低,采用以下方法。
① 水平方向的转换: 所有的 char 型和 short 型自动地转换成 int 型;所有的 unsugned short
型自动地转换成 unsugned 型; 所有的 long 型自动地转换成 unsugned long 型;
所有的 float 型
自动地转换成 double 型。
② 垂直方向的转换:经过水平方向的转换,如果参加运算的数据的类型仍然不相同,再
将这些数据自动转换成其中级别最高的类型。
例如,设变量 ac 的类型是 char,变量 bi 的类型是 int,变量 d 的类型是 double,求解表
达式 ac +bi – d。运算次序是:① 计算 ac +bi,将 ac 转换为 int 型后求和,结果是 int 型。② 将
ac+bi 的和转换为 double 型,再与 d 相减,结果是 double 型。
(2)赋值运算时,将赋值号右侧表达式的类型自动转换成赋值号左侧变量的类型。
例如, 设变量 x 的类型是 double, 计算表达式 x=1。 运算时,先将 int 型常量 1 转换成 double
– 32 –
第2章 基本数据类型和表达式

型常量 1.0,然后赋值给 x,结果是 double 型。


又如,设变量 a 的类型是 short,变量 b 的类型是 char,变量 c 的类型是 long,求解表达
式 c=a+b。运算次序是:① 计算 a+b,将 a 和 b 转换成 int 型后求和,结果是 int 型。② 将
a+b 的和转换成变量 c 的类型 long,然后赋值给 c,结果是 long 型。
利用这条规则时,如果赋值号右侧表达式的类型比赋值号左侧变量的类型级别高,运算
精度会降低。
例如,设变量 ai 的类型是 int,计算表达式 ai=2.56。运算时,先将 double 型常量 2.56
转换成 int 型常量 2,然后赋值给 ai,结果是 int 型。
又如,设变量 ai 的类型是 int,求解表达式 ai=0x12345678L。运算时,先将 long 型常量
0x12345678 转换成 int 型常量 0x5678,再赋值给 ai,结果是 int 型。由于 long 型数据需 4 个
字节存储,而 int 型数据只用 2 个字节存储,转换时,long 型数据存储单元中的高 16 位被舍
去。
从以上的说明中可以看出,赋值运算时,赋值号两侧数据的类型最好相同,至少右侧数
据的类型比左侧数据的类型级别低,或者右侧数据的值在左侧变量的取值范围内,否则,会
导致运算精度降低,甚至出现意想不到的结果。
例 2-20 自动类型转换的运用。
源程序:
#include <stdio.h>
void main( )
{
long a, b, c;

a=1000000L;
b=1000*1000LU;
c=1000*1000;
printf("%ld,%ld,%ld\n",a, b, c);
}
运行结果如下:
1000000,1000000,16960
说明:a, b, c 都是 long 型变量,计算 a = 1000000L 时,1000000 是 long 型常量,与变量
a 的类型相同;计算 b = 1000*1000LU 时,1000*1000LU 的运算结果是 1000000,类型为
unsigned long,虽然与变量 b 的类型不同,但结果在 b 的取值范围内;而 1000*1000 的结果
是 int 型(int 乘 int),但它的值超出了 int 型数据的取值范围,已溢出,无法得到预期的结果
1000000。

2.强制类型转换

使用强制类型转换运算符,可以将一个表达式转换成给定的类型。其一般形式是:
(类型名)表达式
例如,设 i 是 int 型变量,(double)i 将 i 的值转换成 double 型,而(int)3.8 将 3.8 转换成 int 型,
得到 3。
– 33 –
C 语言程序设计

强制类型转换运算符的优先级较高,与自增运算符++相同,它的结合性是从右到左。例
如,(int)3.8+1.3 等价于((int)3.8)+1.3,它的值是 4.3,而(int)(3.8+1.3)的值是 4。
注意:无论是自动类型转换,还是强制类型转换,都是为了本次运算的需要,对数据的
类型进行临时转换,并没有改变数据的定义。例如,表达式(double)i 的类型是 double,但 i
的类型仍然是 int。
例 2-21 强制类型转换的运用。
源程序:
#include <stdio.h>
void main( )
{
int i;
float x;

x=3.8;
i=(int) x;
printf("x=%f,i=%d\n", x, i);
printf("(double)(int)x=%f\n", (double)(int)x);
printf(" x mod 3=%d\n", (int)x%3);
}
运行结果如下:
x=3.800000,i=3
(double)(int)x=3.000000
x mod 3=0

习 题

1.C 语言中有哪些基本数据类型?如何表示?
2.已知:int a=3, b=4, c=9,求下列表达式的值。
(1)a/b
(2)c/b/a
(3)c%a
3.已知:int a, b, c, d; a = b = c = d = 5,求下列表达式的值。
(1)b– – –c
(2)d+=a+b
(3)++a/b++*– –c
4.写出下列程序的运行结果。
#include <stdio.h>
void main( )
{
int y;
double d=3.2, x;
– 34 –
第2章 基本数据类型和表达式

x=(y=4.0/d)/4;
printf("%0.2f, %2d", x, y);
}
5.写出下列程序的运行结果,并分析原因。
#include <stdio.h>
void main( )
{
int i;
long j;

i=1000;
j=i*100;
printf("%ld \n", j);
j=i*100L;
printf("%ld \n", j);
}
6.已知:short int s = 10; int i = 25; long int k = 1000000L; float f = 0.5; double d = 1.5,编
写程序求下列表达式的值。
(1)f+s*i – k
(2)i/f+s*d
(3)(double)i/s*f
(4)k/i+(int)d/f
7.编写程序,输入一个十进制数,输出相应的八进制数和十六进制数。例如输入 31,
输出 37 和 1F。
8.编写程序,输入华氏温度,输出相应的摄氏温度(保留 2 位小数) 。公式:C=(F – 32)/1.8,
其中 C 表示摄氏度,F 表示华氏度。
9.编写程序,输入一个大写英文字母,输出相应的小写字母。例如输入 G,输出 g。

– 35 –
第3章 算法与 C 语言程序

3.1 计算机求解问题的步骤

人类在几千年的发展和进步中,不断认识自然,征服自然,积累了大量解决自然界问题
的经验与方法。
人们所解决的问题,大致有两类:一类问题可以抽象成数学模型,通过解这个数学问题,
从而达到求解问题的本身;另一类问题由于人类的认识有限、使用方法有限或其他原因无法
抽象出数学模型,而采取模拟事物本身运作过程来解决。不管是哪一类问题,人们首先要明
确问题的描述、问题的要求,然后根据已有的知识、认识的能力,以一种较抽象的方式来表
达问题,再提出解决该问题的途径。若存在多种途径,可从中选择最合适的一种。然后设计
解决方案,最后把它付诸实施,最终解决这个问题。对于一些大的、复杂的问题还需作可行
性研究。
如国家建造三峡大坝,首先要明确建造大坝的用途,最终要达到的目的,建在长江上的
位置,采用的坝形等。再对这样的要求是否做得到(可行否)进行研究,这包括技术上的可
行性和人力、物力、资金上的可行性。如果可行,下一步就要进行设计,确定实施步骤,从
而给出具体的设计方案。最后进行施工,来完成这项人类的宏伟工程。
因此我们把人解决问题的步骤归纳如下:
(1)明确问题;
(2)精确表达问题;
(3)设计解决方案;
(4)实施解决。
我们从人解决具体问题的过程出发,目的是为了导出计算机帮助人解决问题的方法和步
骤。我们可以让计算机按照人的思维过程,完成人布置给计算机的任务。当然,目前计算机
只是一个电子装置,无法像科幻电影中的机器人,具有逻辑思维能力,能自动完成工作。计
算机硬件必须在系统软件和应用软件的支持下,按照人所规定的解决方案,完成指定的工作。
因此计算机在配合人解决问题过程中,只是根据人提出的解决方案,给出一个解决的数
值模拟。对于解决方案本身已经抽象出数学模型的话,它就可以解这个数学模型,得到一种
数值解;如果解决方案是一种逻辑模拟模型,计算机可求得模拟实现过程。
确定解决方案是一个由具体到抽象再到具体的过程。抽象强调问题内在的、本质的特性,
而暂不考虑其细节。与复杂的现实世界相比,人的思维能力是有限的,这使得人们对复杂的
问题,不可能一下子触及到其细节,做出精确思维。往往先通过抽象来构造问题解决的轮廓
(大步骤),然后再进一步理解尚未被精确认识的部分。如此不断进行,形成不同的抽象层次,
直到把问题精确构造、理解为止,从而实现了抽象到具体的过程。

– 36 –
第3章 算法与 C 语言程序

使用计算机解决问题时,首先要把具体问题抽象为模型,再把模型表现为用具体的计算
机语言描述的程序。在这一过程中,程序设计者不应首先考虑实现细节,而要把精力放在建
立系统的总体模型上。如果我们把程序看作是构成系统的程序零件,首先要根据系统的要求
或功能,考虑系统应由哪些程序零件构成,然后再考虑各个程序零件该如何实现。
综上所述,人使用计算机解决问题,大致有如下步骤:
(1)明确问题;
(2)精确表达问题;
(3)设计解决方案(模型或算法) ;
(4)把解决方案用计算机程序实现(程序设计);
(5)计算机运行、求解。
其中步骤(1)~(4)都要由人来完成,步骤(1)~(3)需要有问题所处领域的专
业知识,由专业人士完成或参与完成,不具有通用的方法;步骤(4)由程序员完成。步骤
(5)才是计算机的工作。
下节我们将讨论算法的描述方法及算法的有关属性特征。而本书的后面各章将解决步骤
(4)的实现,如何把算法转换成 C 语言程序——即 C 语言程序设计。

3.2 算法的描述

算法是对求解问题的方法或步骤的一种精确描述。例如,乐谱可以看作乐曲的算法,因
为它规定了乐器演奏的方式与曲调。算法可以用自然语言、流程图或类程序设计语言等描述。
下面针对例 3-1 分别介绍几种算法描述。
例 3-1 从键盘中输入 100 个整数,对其中的正整数和负整数分别进行累加,最后输出
两种累加结果。请写出相应的算法。

1.自然语言描述

用自然语言描述算法,比较容易理解和进行交流,但有时不够明确,容易发生二义性。
特别在描述一些复杂的算法时,很难把复杂的逻辑流程描述清楚,而且算法在转换成程序时
也比较困难,因此这种表达方法适合于简单问题的描述。
例 3-1 的算法描述 1(自然语言):
(1)输入一个数;
(2)如果该数>0,把它加到正累加和中,否则把它加到负累加和中;
(3)如果还没有输入完 100 个数,转步骤(1);
(4)输入完 100 个数后,输出累加和。
算法各步骤在执行过程中,除明确指明要转到某步骤外(如步骤(3)),其他都将从上到
下依次进行。

2.类程序设计语言描述

用程序设计语言描述算法简洁明确,但由于不同的程序设计语言有各自不同的语法规则,
– 37 –
C 语言程序设计

对使用不同程序设计语言的人来说,相互之间的交流不太方便。并且,算法的逻辑结构会湮
没在具体语句的实现中,不容易看得清楚。因此,变通的方法是用半自然语言来描述算法,
一般称它为类程序设计语言。
例 3-1 的算法描述 2(类程序设计语言):
开始
置 0 → sp,0 → sn
置 1 → count
当 count<= 100, 执行下面操作:
读入一个数 → x
IF x 为正
使 sp+x → sp
ELSE
使 sn+x → sn
使 count+1 → count
打印 sp 和 sn 的值
结束
为了算法的形式化描述,定义了 100 次计数的变量单元 count,保存正、负数累加和的变
量单元 sp 和 sn,读入的 100 个数先后保存在变量 x 中。

3.程序流程图描述

流程图使用特定的图形符号加上简单的文字说明,来表示数据处理过程和步骤。它能指
出计算机执行操作的逻辑顺序,表达非常简单、清晰。通过程序流程图,设计者能很容易了
解系统执行的全过程以及各部分之间的关系,便于优化程序并排除设计中的错误。
流程图是描述算法的良好工具,得到了普遍的使用。常见的程序流程图由逻辑框和流向
线组成,其中逻辑框是表示程序操作功能的符号,流向线用来指示程序的逻辑处理顺序。图
3-1 列出了程序流程图的常用符号,它们的功能简单说明如下。

起止框 处理框 判断框 连接框

流向线

图 3-1 流程图的表示符号

(1)起止框:表示程序的开始和结束,框内标以“开始”和“结束”字样。
(2)处理框:表示一种处理功能或程序段,框内用文字简述其功能。
(3)判断框:表示在此进行判断以决定程序的流向,框内注明判断条件,判断结果标注
在出口的流向线上,一般用“Y”表示条件满足,用“N”表示条件不满足。
(4)连接框:框内注有字母,当流程图跨页时,或者流程图比较复杂,可能出现流向线
交叉时,用它来表示彼此之间的关系,相同符号的连接框表示它们是相互连接的。
– 38 –
第3章 算法与 C 语言程序

(5)流向线:表示程序处理的逻辑顺序。
例 3-1 的算法描述 3(程序流程图):

开始

初始化:0→sp,0→sn,0→count

count+1→count

输入一个数到 x

N x>0 Y

x 加到正累加和中 x 加到负累加和中
sp + x → sp sn + sx → sn

Y count < 100

N
输出累加和 sp 和 sn

结束

图 3-2 例 3-1 的程序流程图

说明:图 3-2 以程序流程图形式给出了例 3-1 的算法表示,其中虚线框部分将重复 100


次。程序流程图直观明了,各种操作一目了然,操作之间的关系非常清晰。但由于使用了
流向线,各个框比较稀疏,占位置太大,对于步骤多的流程图,前后相距太远,不容易进
行总体把握。

4.N-S 流程图

根据 1973 年美国学者 Nassi 和 Schneiderman 提出的方法,形成了 N-S 流程图,它是一


种适于结构化程序设计的算法描述工具。由于流程图各步骤之间,一般总是按照从上到下顺
序执行,N-S 流程图中取消了流向线,一旦需要改变顺序时,再用专门的框来表示。如图 3-3
所示的流程图的判断框,其等价的 N-S 流程图如图 3-4 所示。图 3-5 是表示一些步骤需要重
复执行的 N-S 流程图,它是程序设计中必不可少的一种结构。其中,图 3-5(a)称为“直到
型”框,表示指定的操作一直被重复执行,直到条件不成立为止;图 3-5(b)称为“当型”
框,表示当条件成立时,指定的操作被重复执行。
– 39 –
C 语言程序设计

条件 P

步骤 A 步骤 B 条件 P
满足 不满足

步骤 A 步骤 B

图 3-3 判断框的流程图表示 图 3-4 判断框的 N-S 流程图表示

重复执行的操作步骤 维持重复执行的条件

重复执行的操作步骤
维持重复执行的条件

(a) 直到型 (b) 当型

图 3-5 表示“重复操作”的 N-S 流程图

例 3-1 的算法描述 4(N-S 流程图):

初始化:0→sp,0→sn,0→count

count+1→count

输入一个数到 x
x>0
满足 不满足
sp + x → sp sn + x → sn
count<=100 否
输出累加和 sp 和 sn

图 3-6 例 3-1 的 N-S 流程图

图 3-6 所示为例 3-1 的 N-S 流程图,它明显比图 3-2 紧凑,所表现的信息量更大。


从上面 4 种算法描述方法可以看出,算法描述只与问题的求解步骤有关,与使用什么程
序设计语言无关,它是一种通用的描述形式。在后面章节介绍中,我们会使用流程图和 N-S
流程图来说明有关 C 语句的执行过程。

3.3 算法与程序

3.3.1 算法特征
著名计算机科学家沃斯(Nikiklaus Wirth)曾简洁地描述程序设计为:
– 40 –
第3章 算法与 C 语言程序

算法+数据结构=程序设计
其中数据结构是程序中所处理的数据的表示方法,而算法是程序设计的核心。算法给出了问
题求解的步骤,我们可以把它看作指令的有限序列,其中每一条指令表示计算机的一个或多
个操作。算法一般具有以下性质。
(1)输入性:具有零个或若干个输入量。
(2)输出性:至少产生一个输出。
(3)有穷性:每一条指令的执行次数是有限的。
(4)确定性:每一条指令的含义明确,无二义性。
(5)可行性:每一条指令都应在有限的时间内完成。
当我们在进行算法设计时,应以下面 5 点要求作为设计目标。
(1)正确性。算法应当满足具体问题的需求,这是算法设计的基本目标。通常一个复杂
的大型问题,其功能需求是以特定的规格说明方式给出的。这种需求一般包括对于输入、输
出、处理等的明确的无歧义性的描述,设计的算法应当能正确地实现这种需求。
(2)可读性。即使算法已转变成机器可执行的程序,也需考虑人能较好地阅读理解。可
读性有助于人对算法的理解,这既有助于算法中隐藏错误的排除,也有助于算法的交流和维
护。
(3)健壮性。当输入数据非法时,算法应能作出适当的处理,而不应产生不可预料的结
果。
(4)高效率。算法的效率指算法在计算机中的执行时间。对于同一个问题如果有多个算
法可供选择,应尽可能选择执行时间短的算法。执行时间短的算法也称作高效率的算法。
(5)低存储量需求。算法的存储量需求指算法执行过程中所需要的计算机内存空间。对
于同一个问题如果有多个算法可供选择,应尽可能选择存储量需求低的算法。算法的存储量
需求也称作算法的空间复杂度。通常,算法的高效率和低存储量需求是互相矛盾的,需要根
据具体问题给予平衡。

3.3.2 算法的 C 语言实现


为了用计算机实现算法,必须编写出计算机程序。计算机程序就是用计算机语言来表示
算法,它规定了计算机如何一步一步地操作。实现算法到程序的转换,是我们全书的最终目
的。根据算法步骤描述的详略,每个步骤可能与一条 C 语句对应,也可能与多条 C 语句对应。
下面我们仍以例 3-1 为例,看看如何把算法转换成 C 语言程序。有一些语句将在后面章节陆
续学习。
例 3-2 把例 3-1 的算法用 C 语言程序实现。
源程序:
# include <stdio.h>
void main( )
{ /* 程序准备工作:工作变量定义 */
int x; /* 定义存放输入数的单元 */
int count ; /* 定义存放输入个数的单元 */
int sp, sn ; /* 定义存放正、负累加和的单元 */

– 41 –
C 语言程序设计

/* 工作变量初始化 */
count = 0 ;
sp = 0 ;
sn = 0 ;
/* 以下是与算法对应的程序执行步骤 */
do {
count ++ ; /* 每输入一个数,记一下数 */
scanf ("%d" , &x) ; /* 输入一个数 */
if (x>0) /* 判断输入数>0 否 */
sp = sp + x ; /* x>0,则加到累加和中 */
else
sn = sn + x ; /* 否则,则加到负累加和中 */
} while ( count <100 ); /* 未输入完 100 个数,则重复 */
printf ("%d %d" , sp, sn ) ; /* 输出累加和 */
}
说明:由于算法比较简单,除了程序一开始的准备工作——变量定义外,算法中的每一
个操作框,都对应了一条 C 语句。

3.3.3 算法与程序结构
一个算法是由一些特定的操作按一定的规则组成的有穷序列。它由可执行的基本操作集
合与对操作顺序进行控制的两大要素组成。

1.基本操作

对不同的数据所进行的操作类型不同,但是用计算机解题时,都要用计算机所能接受的
操作来模拟。这些操作主要有算术运算、逻辑运算、关系运算和赋值运算等。

2.控制结构

控制结构是用来控制或改变操作的执行顺序。1966 年 Bohm 和 Jacopini 首先证明了只用


3 种基本的控制结构就能够实现任何单入口单出口的程序。这 3 种基本的控制结构是顺序结
构、分支结构和循环结构。IBM 公司的 Mills 在 1971 年进一步提出“程序应该只有一个入口
和一个出口”,从而使 3 种基本的控制结构适用于任何程序。
(1)顺序程序结构
顺序结构程序是最简单的一种程序结构。在流程图中,从上到下的各个处理框就是顺序
结构的形式。这种结构主要用在表达式语句和函数调用语句上。
下面是一个简单的顺序程序设计的例子。
例 3-3 计算如下多项式:
y=5x3 + 4x2 – 3x + 21
分析:为了便于程序的实现,把多项式改写成:
y = ((5x + 4)x – 3)x + 21
源程序:

– 42 –
第3章 算法与 C 语言程序

#include <stdio.h>
void main( )
{
int x , y ;
scanf("%d", &x ) ;
y = ((5*x + 4)*x - 3)*x + 21 ;
printf( "%d" , y ) ;
}
说明:对于大部分的程序来说,都不外于 3 个大步骤,即输入 — 根据输入进行处理 —
输出处理结果。例 3-3 的 N-S 流程图如图 3-7 所示。另外要注意,多项式的程序表示中,乘
号*不要遗漏。

输入
计算
输出

图 3-7 例 3-3 的 N-S 流程图

例 3-4 对两个变量先输入两个不同的值,然后交换这两个变量的值。
分析:可以把这两个变量想象成两个杯子,分别盛放红墨水和黑墨水,现要求把两种墨
水交换杯子盛放,显然必须借用第三只杯子才能完成。本例的 N-S 流程图如图 3-8 所示。

输入两个数据存入 x, y
把第一个数据从 x 放到第三个变量 temp 中
把第二个数据从 y 放到第一个变量 x 中
把第一个数据从 temp 放到第二个变量 y 中
输出 x, y

图 3-8 交换两个变量的 N-S 流程图

源程序:
#include<stdio.h>
main()
{
int x, y, temp ;
scanf("%d%d", &x, &y ) ;
printf("first=%d, second=%d", x, y);
temp=x ;
x=y ;
y=temp ;
printf("\n after change \n");
printf("first=%d, second=%d", x, y);
}
– 43 –
C 语言程序设计

说明:x、y 和 temp 之间的操作步骤必须一环紧扣一环,顺序不能打乱。


(2)分支程序结构
顺序结构程序的设计和运行都是比较简单的,它只能解决简单的问题。在实际应用中往
往需要根据不同的情况和条件作出不同的处理。例如在例 3-1 中,要经过判断:大于 0 的数,
加到正累加和中;小于 0 的数,加到负累加和中。要编制这样的程序,需要事先把各种可能
出现的情况和处理方法写在程序中,由计算机在执行过程中根据当时的情况作出判断,转向
相应的处理步骤,各种情况的语句是选择其中一个执行的。通常这种程序称作分支结构程序
或选择结构程序。例 3-1 中对输入数据 x 是否大于 0 的判断是分支选择的依据。
例 3-5 输入 x,按下列式子计算 y 。

 x 当x ≥ 0
y=
1 / x 当x < 0

分析:随着 x 的值不同,y 所使用的公式也不同。因此在计算时,应首先判断 x 值,选


择其中的一个公式计算。这是典型的分支结构,其 N-S 流程图如图 3-9 所示。
源程序:
#include <stdio.h>
#include <math.h>
void main()
{
float x, y ;
scanf("%f" , &x);
if (x>=0) /* 分支判断 */
y = sqrt(x);
else
y=1/x;
printf("%f", y);
}

输入 x

x>=0
满足 不满足

y =sqrt(x) y =1/x

输出结果 y

图 3-9 例 3-4 的 N-S 流程图

(3)循环程序结构
在顺序程序设计和分支程序设计中,程序中的每条指令最多只能执行一次。但是,在实
际应用中还经常会遇到这样一类问题,即有些相同或类似的操作需要重复执行多次。
– 44 –
第3章 算法与 C 语言程序

例 3-6 计算 sum = 1 + 2 + 3 + 4 + … +100。


分析:本例存在连续相加运算,如果把 100 个数相加写在一个表达式中,该表达式会非
常长。这就需要通过循环结构,让计算机去自动求和。这里首先要确定重复步骤,即 99 次相
加,可以使用“当型”循环。该例的 N-S 流程图如图 3-10 所示。

sum=0, i=1

i <=100

sum=sum+i
i++
输出 sum

图 3-10 1~100 相加的 N-S 流程图

源程序:
# include<stdio.h>
void main( )
{
int i, sum ;
sum=0 ;
i=1;
while ( i <=100 ) {
/* 当 i<=100 重复执行 */
sum = sum + i ;
i ++ ;
}
printf ("sum is %d" , sum ) ; /* 输出累加和 */
}
说明:程序中“sum=sum+i”与“i++”操作会重复执行,以完成多次累加。循环程序结
构中必须有条件来控制循环重复的次数,像本例中用“i<=100”控制,每重复一次,i 加 1,
直至 i>100 为止。
在解决实际问题中,程序的结构往往是 3 种基本结构的组合体,如例 3-1 就把 3 种基本
程序结构包含了。从大的步骤看,它是一种顺序结构,如图 3-11 所示。其中第二个步骤要对
100 个数重复进行处理,是循环结构。它依次输入 100 个数,分别计算正数之和与负数之和。
把该步骤细化后,它包含了 3 个顺序步骤,如图 3-12 虚线框所示。

初始化: 0→sp,0→sn,0→count
输入 100 个数,分别计算正数之和与负数之和
输出正、负数的累加和

图 3-11 例 3-1 的大步骤流程图

– 45 –
C 语言程序设计

初始化:0→sp,0→sn,0→count

count+1→count

从键盘输入一个数到 x
分别计算正数之和与负数之和

count < 100


输出累加和 sp 和 sn

图 3-12 例 3-1 的带循环 N-S 流程图

最后再分析图 3-12 中求“正数之和与负数之和”步骤,它需要判断读入的数是正还是负,


决定加到哪一个累加和上,这需要由分支结构实现。其完整实现的 N-S 流程图如图 3-13 所示。

初始化:0→sp,0→sn,0→count

count+1→count

从键盘输入一个数到 x

x >0
满足 不满足
sp + x → sp sn + x → sn
count < 100
输出累加和 sp 和 sn

图 3-13 例 3-1 的完整 N-S 流程图

一般来说,一个问题的大的解决步骤是顺序的,其中各个步骤可能进一步用到分支结构
或循环结构,并且分支结构中可以再包含循环结构,循环结构中也可以包含分支结构,而顺
序结构无处不在。从整体看,程序结构总是由 3 种基本结构有机地结合起来的。
本节主要想使读者了解 3 种程序结构的形式。第 4 章和第 5 章将详细介绍分支结构与循
环结构的相应语句及程序设计方法。

3.4 C 语句分类

为了实现算法的各种程序设计结构,根据程序设计语言的基本要求,可以把 C 语言所有
的语句归纳成 5 类。

1.表达式语句

第 2 章中我们学习了各种表达式,在一个表达式的后面加一个分号,就形成了表达式语
句。其格式为:

– 46 –
第3章 算法与 C 语言程序

表达式;
如:count=0 是一个赋值表达式,count=0; 就是一条赋值表达式语句。
再如:count++ 是自增表达式,count ++; 就是自增表达式语句。

2.函数调用语句

C 语言中对输入输出功能,提供了两个标准函数 scanf( ) 和 printf( ),当我们在编程序时


需要进行输入输出,往往采用函数调用语句,如:
scan("%d " &x ) ;
printf("%d", sum ) ; /* 请注意函数调用后面的分号 */
函数调用语句格式为:
函数调用;
如果函数调用是出现在表达式计算中,则应归到表达式语句中,如:
y=x*sin(x);

3.程序结构控制语句

程序结构控制语句是用于实现 3 种基本控制结构中的“分支控制”和“循环控制”及有
关辅助功能的,包括以下几种语句。
(1)分支控制结构语句
二分支 if ( ) … else …
多分支 switch( ) — case
(2)循环控制语句
for ( ) ;
while ( ) ;
do - while( );
(3)函数返回语句
return
(4)其他语句
break continue goto
这 4 类语句将在第 4 章和第 5 章中详细介绍。

4.空语句

在某些特殊场合下,语句语法规则要求写一条语句,而实际又没有工作需要执行,可以
填一条空语句。其格式如下:
; (只有一个分号)
例如:
while ((c=getchar()) == ’ ’ )
; /* 空语句 */
……
表示当从键盘上输入的字符是空格时,继续输入下一个字符,直到输入的字符不是空格时才

– 47 –
C 语言程序设计

执行下一条语句。由于 while 语句的语法规则是:


while (条件)
语句
条件判断后必须指明要执行的语句,而实际上若输入的是空格的话,并没有其他工作要做,
只是继续读入下一个字符。为了保证语法的正确,只能跟一个空语句。

5.复合语句

上面所举的循环控制语句,如:
while (条件) 语句;
其语法格式规定被重复执行的只能是一条语句。而在实际程序设计中,需重复执行的步骤有
时会需要多个,为解决这类问题,C 语言提供了“复合语句”概念。复合语句格式为:
{ 语句 1 ;

语句 N;
}
其意义是把用花括号括起来的多条语句,看作为一条语句。循环语句就可以写成:
while (条件) {
语句 1 ;

语句 N;
}
实际上 C 语言的整个程序(主函数)就等价于一条复合语句。
main( )
{
……
}

习 题

1.什么是算法?计算机算法有哪些性质?
2.描述算法有哪些主要方法?它们各有什么特点?
3.请分别用自然语言、程序流程图和 N-S 流程图描述下列问题。
(1)输入 3 个电阻值,求并联后的电阻值并输出。
(2)输入 3 个值,求由这 3 个值为边构成的三角形面积并输出(需要首先判断它们能否
构成一个三角形)。
(3)输入 3 个值,求这 3 个值中的最大值和最小值。
(4)求一元二次方程 ax2 + bx + c = 0 的根,分别考虑包含实根与虚根的情况。
4.请叙述 C 语言程序的 3 类基本控制结构的特点。
5.C 语言有哪 5 类语句,请分别举例说明。
– 48 –
第 4 章 分支结构程序设计
计算机在执行程序时,一般按照语句的书写顺序执行,但在很多情况下需要根据条件选
择所要执行的语句。C 语言提供了 if 语句和 switch 语句来完成这样的功能,它们可根据条
件判断的结果,选择所要执行的程序分支。其中,条件可以用表达式来描述,如关系表达式
和逻辑表达式。
本章主要介绍关系表达式、逻辑表达式和 if 语句、switch 语句。

4.1 关系表达式和逻辑表达式

4.1.1 关系表达式

1.关系运算符

关系运算符是双目运算符,用于对两个操作数进行比较,比较的结果是“真”或“假” 。
例如:表达式 x < 0 比较两个数 x 和 0 的大小,若 x 的值是−2,该式成立,结果为“真”;若
x 的值是 1,该式不成立,结果为“假” 。C 语言共提供了 6 种关系运算符(如表 4-1 所示)

表 4.1 关系运算符

运算符 < <= > >= == !=

名 称 小于 小于或等于 大于 大于或等于 等于 不等于

优先级 高 低

关系运算符的优先级低于算术运算符,高于赋值运算符和逗号运算符,它的结合方向是
从左向右(见表 2-7)。例如,设 a、b、c 是整型变量,ch 是字符型变量,则:
a>b==c 等价于 (a>b)==c
d=a>b 等价于 d=(a>b)
ch>’a’+1 等价于 ch>(’a’+1)
d=a+b>c 等价于 d=((a+b)>c)
3<=x<=5 等价于 (3<=x)<=5
b−1==a!=c 等价于 ((b−1)==a)!=c

2.关系表达式

用关系运算符将两个表达式连接起来的式子,称为关系表达式。例如:i>j、’a’+1>c、
– 49 –
C 语言程序设计

a==b<c、(a=3)>(b=3) 都是合法的关系表达式。
关系表达式的值反映了关系运算(比较)的结果,它是一个逻辑量,取值“真”或“假”。
由于 C 语言没有逻辑型数据,就用整数 1 代表“真” ,0 代表“假”
。这样,关系表达式的值
就是 1 或 0,它的类型是整型。
例 4-1 关系表达式的运用。
源程序:
#include <stdio.h>
void main( )
{
char ch = ’w’;
int a = 2, b = 3, c = 1, d, x=10;

printf("%d ", a>b==c);


printf("%d ", d=a>b);
printf("%d ", ch>’a’+1);
printf("%d ", d=a+b>c);
printf("%d ", b-1==a!=c);
printf("%d\n", 3<=x<=5);
}
运行结果如下:
001101
说明:程序输出了 6 个表达式的值,其中有 2 个是赋值表达式,请读者根据运算符的优
先级做出判断。
表达式 b−1==a!=c 等价于((b−1)==a)!=c,当 a=2,b=3,c=1 时,(b−1)==a 的值是 1,再
计算 1!=1,得到 0。表达式 3<=x<=5 等价于(3<=x)<=5,当 x=10 时,3<=x 的值是 1,再计算
1<=5,得到 1。其实,无论 x 取什么值,关系表达式 3<=x 的值不是 1 就是 0,都小于 5,即
3<=x<=5 的值恒为 1。由此看出,表达式 3<=x<=5 无法正确表示代数式“3<=x<=5”。
在 C 语言中,我们可以用关系表达式来描述给定的一些条件。例如,判断 x 是否为负数,
可以用关系表达式 x<0 描述该条件,它对两个操作数 x 和 0 进行比较,如果 x 是负数,条件
成立,该表达式的值就是 1;如果 x 不是负数,条件不成立,该表达式的值就是 0。但是,如
果需要描述的条件比较复杂,涉及的操作数多于两个,用关系表达式就难以正确表示。例如,
判断 x 是否在闭区间[3, 5]内,即表示代数式“3<=x<=5” ,就需要对 3 个操作数 x、3 和 5 进
行比较,一般采用逻辑表达式(x>=3)&&(x<=5)来描述这个条件。

4.1.2 逻辑表达式

1.逻辑运算符

C 语言提供了 3 种逻辑运算符(如表 4-2 所示) ,逻辑运算对象可以是关系表达式或逻辑


量,逻辑运算的结果也是一个逻辑量,与关系运算一样,用 1 代表“真”,0 代表“假”。
例如,在(x>=3)&&(x<=5)中,&&是逻辑运算符,关系表达式 x>=3 和 x<=5 是逻辑运算

– 50 –
第4章 分支结构程序设计

对象,逻辑运算的结果是 1 或 0。
表 4-2 逻辑运算符
目 数 单 目 双 目

运算符 ! && ||

名 称 逻辑非 逻辑与 逻辑或

假设 a 和 b 是逻辑量,则对 a 和 b 可以进行的基本逻辑运算包括 !a(或 !b)


、a && b
和 a || b 三种。作为逻辑量,a 或 b 的值只能是“真”或“假”,所以 a 和 b 可能的取值组合
只有 4 种,即(“真” ,
“真”)、
(“真” ,“假”)、
(“假”
,“真”
)和(“假”,“假”) ,与之相应的
3 种逻辑运算的结果也随之确定。将这些内容用一张表格表示,就是逻辑运算的“真值表”
(如表 4-3 所示),它反映了逻辑运算的规则,其中 a 和 b 的取值见括号中的内容。
表 4-3 逻辑运算的“真值表”
a b !a a && b a || b

非0 (真) 非0 (真) 0 1 1

非0 (真) 0 (假) 0 0 1

0 (假) 非0 (真) 1 0 1

0 (假) 0 (假) 1 0 0

表 4-3 清楚地说明了逻辑运算符的功能,即:
!a 如果 a 为“真” ,结果是 0(“假”),如果 a 为“假”,结果是 1(“真”)。
a && b 当 a 和 b 都为“真”时,结果是 1(“真”),否则结果是 0(“假”)。
a || b 当 a 和 b 都为“假”时,结果是 0(“假”),否则结果是 1(“真”)。
如何判断逻辑量(如 a 和 b)的“真” 、
“假”呢?如果某个逻辑量的值为非 0,就是“真”;
如果值为 0,就是“假”(见表 4-3) 。
例如,计算(x>=3)&&(x<=5),若 x=4,则 x>=3 和 x<=5 的值都是 1(非 0 为“真”) ,
“逻辑
与”运算的结果就是 1;若 x=10,则 x>=3 的值是 1(非 0 为“真” ),而 x<=5 的值是 0(
“假”)

“逻辑与”运算的结果就是 0。又如,计算!(x==2),若 x=10,则 x==2 的值是 0( “假” )
,“逻辑
非”运算的结果是 1;若 x=2,则 x==2 的值是 1(非 0 为“真” )
,“逻辑非”运算的结果是 0。
逻辑运算符的优先级见表 2-7。
例如:
a||b&&c 等价于 a||(b&&c)
!a&&b 等价于 (!a)&&b
x>=3&&x<=5 等价于 (x>=3)&&(x<=5)
!x==2 等价于 (!x)==2
a||3+10&&2 等价于 a||((3+10)&&2)

2.逻辑表达式

用逻辑运算符将关系表达式或逻辑量连接起来的式子,称为逻辑表达式。例如,
– 51 –
C 语言程序设计

(x>=3)&&(x<=5)、!(x==2) 都是合法的逻辑表达式。
逻辑运算对象是值为“真”或“假”的逻辑量,它可以是任何类型的数据,如整型、浮
点型、字符型等,C 编译系统以非 0 和 0 判定“真”和“假”。逻辑表达式的值反映了逻辑运
算的结果,也是一个逻辑量,但系统在给出逻辑运算结果时,用 1 代表“真” ,0 代表“假”。
例 4-2 逻辑表达式的运用。
源程序:
#include <stdio.h>
void main( )
{
char ch = ’w’;
int a = 2, b = 0, c = 0;
float x = 3.0;

printf("%d ", a && b);


printf("%d ", a || b && c);
printf("%d ", !a && b);
printf("%d ", a||3+10&&2);
printf("%d ", !(x == 2));
printf("%d ", !x == 2);
printf("%d\n ", ch || b);
}
运行结果如下:
0101101
说明:字符型变量 ch 的值是 ’w’(其 ASCII 码值不为 0),整型变量 a 的值是 2,浮点型
变量 x 的值是 3.0,都是非 0 的数,在逻辑运算时相当于“真”;整型变量 b 和 c 的值都是 0,
在逻辑运算时相当于“假”,而逻辑运算的结果只能是 1 或 0。
!(x==2)是逻辑表达式,当 x=3.0 时,(x==2)的值是 0,再计算!0,得到 1。而!x==2 是关
系表达式,等价于(!x)==2,当 x=3.0 时,!x 的值是 0,再计算 0==2,得到 0。其实,无论 x
取什么值,逻辑表达式!x 的值不是 1 就是 0,不可能等于 2,即!x==2 的值恒为 0。
与其他表达式的运算过程不同,求解用逻辑运算符&&或者||连接的逻辑表达式时,按从
左到右的顺序计算该运算符两侧的操作数,一旦能得到表达式的结果,就停止计算。例如:
(1)求解逻辑表达式 exp1&&exp2 时,先算 exp1,若其值为 0,则 exp1&&exp2 的值一
定是 0。此时,已经没有必要计算 exp2 的值。例 4-2 中,计算表达式!a&&b 时,先算!a,由
于 a 的值是 2,!a 就是 0,该逻辑表达式的值一定是 0,不必计算 b 了。
(2)求解逻辑表达式 exp1||exp2 时,先算 exp1,若其值为非 0,则 exp1||exp2 的值一定是
1。此时,也不必计算 exp2 的值。例 4-2 中,计算表达式 a||3+10&&2 时,先算 a,由于 a 的
值是 2,该逻辑表达式的值一定是 1,就不必计算 3+10&&2 了。
现在,我们可以用关系表达式或逻辑表达式来描述给定的条件。例如:描述 4.1.1 节提到
的“判断 x 是否在闭区间[3, 5]内”这一条件,可以用逻辑表达式(x>=3)&&(x<=5),如果 x 在
闭区间[3,5]内,条件成立,该表达式的值就是 1;如果 x 不在闭区间[3, 5]内,即不能同时满

– 52 –
第4章 分支结构程序设计

足 x>=3 和 x<=5,条件不成立,该表达式的值就是 0。
例 4-3 写出满足下列条件的 C 表达式。
(1)ch 是小写英文字母。
(2)x 为 0。
(3)x 和 y 不同时为 0。
(4)year 是闰年,即 year 能被 4 整除但不能被 100 整除,或 year 能被 400 整除。
解答:
(1)逻辑表达式 ch>=’a’&&ch<=’z’。
(2)关系表达式 x==0 或逻辑表达式!x。
当 x 分别取值非 0 和 0 时,从真值表(如表 4-4 所示)可以看出,两个表达式的结果相
同,故二式等价。
表 4-4 真值表
x x == 0 !x

非0 0 0

0 1 1

(3)逻辑表达式!(x==0&&y==0)或 x!=0||y!=0 或 x||y。


从真值表(如表 4-5 所示)可以看出,3 个表达式的结果相同,故三者等价。
表 4-5 真值表
x y !(x == 0 && y == 0) x != 0 || y!=0 x || y

非0 非0 1 1 1

非0 0 1 1 1

0 非0 1 1 1

0 0 0 0 0

(4)逻辑表达式(year%4==0&&year%100!=0)||(year%400==0)
或(!(year%4)&&year%100)||!(year%400)。
其中,year%4==0 表示 year 能被 4 整除和!(year%4) 等价。

4.2 if 语句

if 语句可以根据条件判断的结果,选择所要执行的程序分支。

4.2.1 基本的 if 语句

1.if - else 结构

if-else 结构一般形式如下:
– 53 –
C 语言程序设计

if(表达式)
语句 1
else
语句 2
执行流程如图 4-1 所示。首先求解表达式,如果表达式的值为非 0,则执行语句 1;如果
表达式的值为 0,则执行语句 2。

非 0(真) 0(假)
表达式

语句1 语句2

图 4-1 if - else 流程图

例 4-4 输入 x,计算并输出下列分段函数 f(x) 的值(保留 2 位小数)。


ex x≤1
f(x) =
x2–1 x>1
源程序:
# include <stdio.h>
# include <math.h>
void main( )
{
double x, y;

printf("input x:\n");
scanf("%lf", &x);
if(x <= 1)
y = exp(x); /* 语句 1*/
else
y = x * x -1; /* 语句 2*/
printf("f(%f)=%.2f\n", x, y);
}
运行结果 1:
input x :
0.5<CR>
f(0.500000)=1.65
运行结果 2:

– 54 –
第4章 分支结构程序设计

input x :
2.4<CR>
f(2.400000)=4.76
if-else 结构中的表达式可以是任意合法的 C 语言表达式,不限于逻辑表达式和关系表达
式。其中的语句 1 和语句 2,也称为内嵌语句,只允许是一条语句,若需要多条语句,应该
用大括弧把这些语句括起来组成复合语句。
例 4-5 输入 2 个数,如果它们的值不相等,则交换并输出它们的值;否则,输出“equal”。
源程序:
# include <stdio.h>
void main( )
{
int a, b, t;

printf("input a, b:\n");
scanf("%d%d", &a, &b);
if(a != b){ /* 语句 1 是一条复合语句 */
t = a;
a = b;
b = t;
printf("a = %d, b = %d\n", a, b);
}
else
printf("equal\n", a, b); /* 语句 2*/
}
运行结果 1:
input a, b:
2 10<CR>
a = 10, b = 2
运行结果 2:
input a, b:
12 12<CR>
equal

2.省略 else 的 if 结构

if - else 结构中的 else 部分是可以选用的,省略 else 的 if 结构形式如下:


if(表达式)
语句 1
执行流程如图 4-2 所示。首先求解表达式,如果表达式的值为非 0,则执行语句 1。
例 4-6 输入一个字符,如果它是小写字母,将其转换成大写字母后输出;否则,原样
输出。
源程序:

– 55 –
C 语言程序设计

# include <stdio.h>
void main( )
{
char ch;

ch = getchar();
if(ch >= ’a’ && ch <= ’z’) /* 省略 else 的 if 结构*/
ch = ch - ’a’ + ’A’ ;
putchar(ch);
}
运行结果 1:
m<CR>
M
运行结果 2:
=<CR>
=

非0 (真) 0 (假)
表达式

语句1

图 4-2 省略 else 的 if 结构流程图

例 4-7 输入 3 个整数,输出其中的最大值。
源程序:
# include <stdio.h>
void main( )
{
int a, b, c, max; /* max 中放最大值 */

printf("input a, b, c:\n");
scanf("%d%d%d", &a, &b, &c);
max = a; /* 先假设 a 是最大的数 */
if(max < b) max = b; /* 如果 b 比假设的最大值大,再假设 b 是最大的数 */
if(max < c) max = c; /* 如果 c 比假设的最大值大,则 c 是最大的数 */
printf("max is %d\n", max);
}
运行结果如下:
input a, b, c:
– 56 –
第4章 分支结构程序设计

2 10 6<CR>
max is 10

4.2.2 嵌套的 if 语句
基本的 if 语句中,内嵌语句(语句 1 或语句 2)可以是任意一条合法的 C 语句,如果
它又是一条 if 语句,就构成了嵌套的 if 语句。

1.else - if 结构

else-if 结构的 if 语句是最常用的实现多路选择的方法,它的构成就是 if-else 结构中的语


句 2 是另一条基本的 if 语句。
它的一般形式如下:
if(表达式 1)语句 1
else if(表达式 2)语句 2

else if(表达式 n – 1)语句 n – 1
else 语句 n
它的执行流程如图 4-3 所示。首先求解表达式 1,如果表达式 1 的值为非 0,则执行语句
1,并结束整个 if 语句的执行;否则,求解表达式 2,……;最后的 else 处理给出的条件都
不满足的情况,即表达式 1、表达式 2、……、表达式 n−1 的值都是 0,这时执行语句 n。

0
表达式 1
0
非 0 表达式 2
0
……
非 0
0
表达式 n-1

非 0

语句 1 语句 2 …… 语句 n-1 语句 n

图 4-3 else-if 流程图

要正确地使用 else-if 结构的 if 语句,就必须清楚语句 1~语句 n 的执行条件。


– 57 –
C 语言程序设计

例 4-8 下列 if 语句中,e1~e3 是合法的表达式,s1~s4 是合法的语句,请分析 s1~s4 的


执行条件。
if (e1) s1
else if(e2) s2
else if(e3) s3
else s4
解答:表 4-6 给出了 s1~s4 的执行条件,空白部分说明该语句的执行不需要判断对应的
条件。
表 4-6 s1~s4 的执行条件
条件
e1 e2 e3
执行语句

s1 非0

s2 0 非0

s3 0 0 非0

s4 0 0 0

例 4-9 输入 x,计算并输出下列分段函数 f(x)的值(保留 2 位小数)。

x+1 x <1
f(x) = x+2 1≤x<2
x+3 x≥2

源程序 1:
# include <stdio.h>
# include <math.h>
void main( )
{
double x, y;

printf("input x:\n");
scanf("%lf", &x);
if(x < 1) y = x + 1; /* else-if 结构*/
else if (x < 2) y = x + 2;
else y = x + 3;
printf("f(%f)=%.2f\n", x, y);
}
运行结果 1:
input x :
0.5<CR>
f(0.500000)=1.50

– 58 –
第4章 分支结构程序设计

运行结果 2:
input x :
2<CR>
f(2.000000)=5.00
当我们针对给定的问题,编写 C 语言程序后,常常通过运行程序来发现程序中存在的错
误,并改正错误,也就是测试程序和调试程序。具体做法是,精心设计一批测试用例(包括
输入数据和与之相应的预期输出结果) ,然后分别用这些测试用例运行程序,看程序的实际运
行结果与预期输出结果是否一致,这就是软件测试的基本思想。如果发现运行结果有错误,
就要调试程序,即查找并改正程序中的错误。程序的测试和调试需要反复进行。
显然,程序测试时,使用的测试用例越多,就越容易发现隐藏的错误,但是穷举所有的
测试用例在实际应用中是不可行的。我们常常根据程序的逻辑结构和功能,设计一些有代表
性的测试用例,测试用例的格式是[输入数据,预期输出结果]。本例中,检查 else-if 结构的 3
个分支是否正确,输入数据应该包括小于 1、大于等于 1 且小于 2 和大于等于 2 的数,同时
根据输入数据的取值范围,取一些边界数据,如 1 和 2。测试用例可以选用:[0.5, 1.50],[1.5,
3.50],[3.1, 6.10],[1, 3.00],[2, 5.00]。
例 4-10 判断输入字符的种类,即区分空格、回车、数字字符、英文字母和其他字符。
源程序:
# include <stdio.h>
void main( )
{
char c;

printf("Please input a character:\n");


c=getchar();
if (c==’ ’||c==’\n’)
printf("This is a blank or enter");
else if (c>=’0’ &&c<=’9’ )
printf("This is a digit.\n");
else if (c>=’A’&&c<=’Z’ ||c>=’a’&&c<=’z’)
printf("This is a letter.\n");
else printf("This is an other character. \n");
}
运行结果如下:
Please input a character:
9<CR>
This is a digit.
要比较全面地测试该程序,测试用例至少应选取 6 个,输入数据分别是空格、回车、数
字字符、大写英文字母、小写英文字母和其他字符,还要给出相应的预期输出结果。

2.嵌套的 if - else 结构

如果 if - else 结构中的内嵌语句是另一条基本的 if 语句,就形成了嵌套的 if-else 结构。


– 59 –
C 语言程序设计

它的一般形式如下:
if(表达式 1)
if(表达式 2)语句 1
else 语句 2
else
if(表达式 3)语句 3
else 语句 4
为了正确地使用嵌套的 if-else 结构的 if 语句,同样要清楚其中各条语句的执行条件。
例 4-11 下列 if 语句中,e1~e3 是合法的表达式,s1~s4 是合法的语句,分析 s1~s4 的执
行条件。
if (e1)
if(e2) s1
else s2
else
if(e3) s3
else s4
解答:表 4-7 给出了 s1~s4 的执行条件,空白部分说明该语句的执行不需要判断对应的
条件。
表 4-7 s1~s4 的执行条件

条件
e1 e2 e3
执行语句

s1 非0 非0

s2 非0 0

s3 0 非0

s4 0 0

例 4-9 中的分段函数,也可以用嵌套的 if-else 结构实现。


if (x < 2)
if (x<1) y = x + 1;
else y = x + 2;
else y = x + 3;
在嵌套的 if-else 结构中,如果内嵌的 if 省略了 else 部分,可能在语义上产生二义性。
假设有以下形式的 if 语句,第一个 else 与哪一个 if 匹配呢?
if(表达式 1)
if(表达式 2)语句 1
else
if(表达式 3)语句 2
else 语句 3

– 60 –
第4章 分支结构程序设计

else 和 if 的匹配准则是 else 与最靠近它的、没有与别的 else 匹配过的 if 相匹配。这


里,虽然第一个 else 与第一个 if 书写格式对齐,但它与第二个 if 对应,因为它们的距离
最近。一般情况下,内嵌的 if 最好不要省略 else 部分,这样 if 的数量和 else 的数量相同,
从内层到外层一一对应,结构清晰,不易出错。
例 4-12 改写下列 if 语句,使 else 和第一个 if 配对。
if(m > 0)
if(a > b)
x = a;
else
x = b;
解答:上述 if 语句中,else 与第二个 if 匹配,它的含义是:当 m>0 时,如果 a>b,x
的值是 a,如果 a<=b,x 的值是 b。上述 if 语句更好的书写格式如下。
if(m > 0)
if(a > b) x = a;
else x = b;
改变 else 和 if 的配对,一般采用下列两种方法。
(1)使用大括号,即构造一个复合语句。
if(m > 0){
if(a > b)
x = a;
}
else
x = b;
(2)增加空的 else
if(m > 0)
if(a > b)
x = a;
else
else
x = b;
改写后的 if 语句的含义是:如果 m>0 且 a > b,x 的值是 a;如果 m<=0,x 的值是 b。
注意,如果 m>0 且 a <= b,x 的值不变。

4.2.3 条件表达式
条件运算符是 C 语言中惟一的一个三目运算符,它将 3 个表达式联结在一起,组成条件
表达式。条件表达式的一般形式如下:
表达式 1?表达式 2:表达式 3
条件表达式的运算过程是:先计算表达式 1 的值,如果它的值为非 0(真),将表达式 2
的值作为条件表达式的值,否则,将表达式 3 的值作为条件表达式的值。
例如,设 a、b 是整型变量,将 a、b 的最大值赋给 z。可以用 if 语句实现:
if (a > b)

– 61 –
C 语言程序设计

z = a;
else
z = b;
也可以用条件表达式求出 a、b 的最大值,再赋值给 z:
z = (a > b) ? a : b
如果条件表达式中表达式 2 和表达式 3 的类型不同,根据 2.6.2 节中讨论的类型自动转换
规则,确定条件表达式的类型。例如:
(n > 0) ? 2.9 : 1
的类型是 double 型。如果 n 是一个负数,该表达式的值是 1.0,而不是 1。
条件运算符的优先级较低,只比赋值运算符高。它的结合方向是自右向左(见表 2.7) 。
例如:
(n > 0) ? 2.9 : 1 等价于 n > 0 ? 2.9 : 1
a > b ? a : c > d ? c :d 等价于 a > b ? a : (c > d ? c :d)
灵活地使用条件表达式,不但可以使 C 语言程序简单明了,而且还能提高运算效率。
例 4-4 用 if 语句求分段函数的值,使用条件表达式,就可以写成:
y = (x<=1)?(exp(x)):(x*x-1)
例 4-6 用 if 语句将小写字母转换成大写字母,使用条件表达式,就可以写成:
ch = (ch>=’a’&&ch<=’z’)?(ch-’a’+’A’):ch

4.3 switch 语句

处理多分支选择问题除了采用嵌套的 if 语句外,还可以直接使用 switch 语句。


switch 语句的一般形式如下:
switch(表达式){
case 常量表达式 1: 语句段 1
case 常量表达式 2: 语句段 2

case 常量表达式 n: 语句段 n
default: 语句段 n+1
}
switch 语句的执行流程如图 4-4 所示。首先求解表达式,如果表达式的值与某个常量表
达式的值相等,则执行该常量表达式后的所有语句段;如果表达式的值与任何一个常量表达
式的值都不相等,则执行 default 后的所有语句段。
例 4-13 switch 语句的运用。
源程序:
# include <stdio.h>
void main( )
{

– 62 –
第4章 分支结构程序设计

int k;
scanf("%d", &k);
switch( k ){
case 1: printf ( "I’m in the case1\n" );
case 2: printf ( "I’m in the case2\n" );
case 3: printf ( "I’m in the case3\n" );
default: printf ( "I’m in the default\n");
}
}

表达式的值 = 常量表达式1的值
语句段1

表达式的值 = 常量表达式2的值
语句段2

达 ……
……

表达式的值=常量表达式n的值
语句段n

其他
语句段n+1

图 4-4 switch 语句流程图

运行结果 1:
2<CR>
I’m in the case2
I’m in the case3
I’m in the default
运行结果 2:
6<CR>
I’m in the default
switch 语句中的表达式和常量表达式的值一般是整型或字符型,所有的常量表达式的值
都不能相等,default 可以省略。如果 switch 语句中省略了 default,当表达式的值与任何一个
常量表达式的值都不相等时,就什么都不执行。switch 语句中的语句段可以包括一条或多条
语句,也可以为空。
从例 4-13 可以看到,switch 语句中的 case 常量表达式和 default 的作用相当于语句标号,
当表达式 k 的值与之相匹配时,不但执行相应的语句段,还按顺序执行后面的所有语句段。
如果执行相应的语句段后,要终止 switch 语句的继续执行,就可以使用 break 语句。
break 语句在 switch 语句中是可选的,它一般放在语句段的最后,用于跳出正在执行的

– 63 –
C 语言程序设计

switch 语句。
将例 4-13 中的 switch 语句改写为:
switch( k ){
case 1: printf ( "I’m in the case1\n" ); break;
case 2: printf ( "I’m in the case2\n" ); break;
case 3: printf ( "I’m in the case3\n" ); break;
default: printf ( "I’m in the default\n"); break;
}
运行结果 3:
2<CR>
I’m in the case2
说明:该 switch 语句的执行流程如图 4-5 所示,和下列嵌套的 if 语句的流程相同。
if(k==1) printf ( "I’m in the case1\n" );
else if(k==2) printf ( "I’m in the case2\n" );
else if(k==3) printf ( "I’m in the case3\n" );
else printf ( "I’m in the default\n");
由此可见,在 switch 语句所有语句段的末尾使用 break,就可以简单清晰地实现多分支
选择。这是 switch 语句的主要使用方法。

k=1
printf ( "I’m in the case1\n" );

k=2
printf ( "I’m in the case2\n" );

k
k=3
printf ( "I’m in the case3\n" );

其他
printf ( "I’m in the default\n");

图 4-5 例 4-13 修改后的 switch 语句流程图

例 4-14 判断输入字符的种类,即区分空格、回车、数字字符和其他字符。
源程序:
# include <stdio.h>
void main( )
{
char c;

printf("Please input a character:\n");


c=getchar();
switch(c) {
case ’ ’ :
– 64 –
第4章 分支结构程序设计

case ’\n’:
printf("This is a blank or enter");
break;
case ’0’ : case ’1’ : case ’2’ : case ’3’ : case ’4’ :
case ’5’ : case ’6’ : case ’7’ : case ’8’ : case ’9’ :
printf("This is a digit.\n");
break;
default:
printf("This is an other character. \n");
break;
}
}
如前所述,switch 语句中常量表达式或 default 后的语句段可以为空,如果表达式的值与
之相匹配,按顺序执行下一个语句段,即 2 个或多个常量表达式可以共用一个语句段。这也
是 switch 语句的灵活应用。本例中,常量表达式’ ’后的语句段为空,它就和常量表达式’\n’共
用一个语句段,10 个常量表达式’0’~’9’也共用了一个语句段。

习 题

1.计算下列表达式的值。设 c=’w’,a=1,b=2,d= −5。


(1)’x’+1<c
(2)’y’!=c −5
(3)−a −5*b>=d+1
(4)3<d<5
(5)b==2==2
(6)a=b=5, a=b!=5
2.计算下列表达式的值。设 c=’m’,x=5,y=5,z=0。
(1)!!c
(2)!!!z
(3)x&&y&&z −1
(4)c>=’a’&&c<=’z’
(5)c −1==’n’||c+1==’n’
(6)!x −y!=! −x −y
(7)x&&(y=5), x&&z&&(y=1), y
(8)x||(z=1), (z==1)||(x=0), x||(z=2), z
3.表达式 x&&1 等价于_____。
A.x==0 B.x==1 C.x != 0 D.x != 1
4.下列运算符中,优先级最低的是_____。
A.* B.!= C.+ D.=
5.算术运算符、赋值运算符和关系运算符的运算优先级按从高到低的顺序依次为_____。

– 65 –
C 语言程序设计

A.算术运算、赋值运算、关系运算 B.关系运算、赋值运算、算术运算
C.算术运算、关系运算、赋值运算 D.关系运算、算术运算、赋值运算
6.设 ch 是字符型变量,写出判断 ch 为英文字母的表达式。
7.写出下列程序段的输出结果。
int x = -10, y=0;
if(x>=0)
if(x==0) y=1;
else y=-1;
printf("%d", y);
8.写出下列程序段的输出结果。
int k, a=1, b=2 ;
k=(a++==b)?2:3 ;
printf("%d", k);
9.写出下列程序段的输出结果。
程序段 A 程序段 B 程序段 C
int a=1, s=0; int a=1, s=0; int a=1, s=0;
switch (a) { switch (a) { switch (a) {
case 1: s+=1; case 2: s+=2; default: s+=3;
case 2: s+=2; case 1: s+=1; case 2: s+=2;
default: s+=3; default: s+=3; case 1: s+=1;
} } }
printf("%d\n",s); printf("%d\n",s); printf("%d\n",s);

程序段 D 程序段 E 程序段 F


int a=1, s=0; int a=1, s=0; int a=1, s=0;
switch (a) { switch (a) { switch (a) {
case 1: s+=1; break; case 1: s+=1; default: s+=3; break;
case 2: s+=2; break; case 2: s+=2; break; case 2: s+=2; break;
default: s+=3; default: s+=3; case 1: s+=1;
} } }
printf("%d\n",s); printf("%d\n",s); printf("%d\n",s);

10.编写程序,输入一个学生的数学成绩,如果它低于 60,输出“Fail”,否则输出“Pass”

11.编写程序,输入 3 个整数,输出其中的最小值。
12.编写程序,输入一个字符,如果它是大写字母,输出相应的小写字母;如果它是小
写字母,输出相应的大写字母;否则,原样输出。例如,输入 F,输出 f;输入 b,输出 B;
输入 7,输出 7。
13.函数
1 x>0
y= 0 x=0
–1 x<0
– 66 –
第4章 分支结构程序设计

(1)用条件表达式求函数值 y。
(2)用 if 语句编程,输入 x,输出 y,并给出你所使用的测试用例。
14.编写程序,判断输入字符的种类,即区分空格、数字字符、大写英文字母、小写英
文字母和其他字符。
15.证明下列等价关系。
(1)a&&(b||c) 等价于 a&&b||a&&c
(2)a||(b&&c) 等价于 (a||b)&&(a||c)
(3)!(a&&b) 等价于 !a||!b
(4)!(a||b) 等价于 !a&&!b

– 67 –
第 5 章 循环结构程序设计
一般来说,计算机有两大特征:一是计算速度快,二是擅长做重复性的工作。我们常常
利用计算机来解决一些重复性的工作。只要在程序中给出运行一次的步骤,再告诉计算机这
些步骤需要重复多少次,整个过程将由计算机自动完成。我们在学习算法时知道,任何程序
都由 3 种基本控制结构组成,即顺序结构、分支结构和循环结构。其中,循环结构就是针对
语句的重复执行的情况,我们通过使用专门的循环语句实现循环结构。
C 语言提供了 3 种循环语句,for、while 和 do-while。本章将主要介绍 3 种循环语句的程
序设计方法和技巧,这是 C 语言程序设计中最基本和重要的方法之一。

5.1 C 语言的循环语句

5.1.1 for 语句
for 语句是 C 语言中最常用的循环语句。其语句格式为:
for (表达式 1;表达式 2;表达式 3)
语句
其中:表达式 1——初值表达式,用于完成进入循环前的准备工作,规定相关的初始值。
表达式 2——条件控制表达式,用于控制循环的进行,它描述的是维持循环的条
件。
表达式 3——循环变化表达式,在循环过程中,它将不断发生改变,用于控制循
环变量的值,最终保证循环的正常结束。
语句——需要重复执行的步骤,也称为循环体语句。这里只允许有一条语句,
如果需要使用多条语句的话,应使用复合语句表示。
因此 for 语句的格式,还可以写成:
for ( 初值表达式;条件控制表达式;循环变化表达式 )
1 条语句
或:
for ( 初值表达式;条件控制表达式;循环变化表达式 ){
语句 1 ;

语句 n;
}
在执行 for 循环时,通常先执行表达式 1,然后判断表达式 2,若其值为真(非 0)
,将执
– 68 –
第5章 循环结构程序设计

行循环体语句,之后执行表达式 3,通常该表达式改变了循环控制变量,从而可改变表达式 2
的条件真假性。当表达式 2 不被满足(为 0)时,结束 for 循环,转而执行 for 语句后面的语
句。for 语句的执行流程如图 5-1 所示。

表达式 1

假(0)
表达式 2

真(非 0)
语 句

表达式 3

for 的下一条语句

图 5-1 for 语句执行流程图

例 5-1 for 循环的运用。


for (i=0 ; i<3 ; i++)
printf ("%d ",i);
运行结果如下:
0 1 2
分析:对照 for 语句格式,i=0 是初值表达式;i<3 是条件控制表达式;i++是循环变化表
达式;需要重复执行的语句是 printf ("%d " , i ),根据 i 的变化不断输出 i 值。表 5-1 给出了
程序执行的过程。
表 5-1 例 5-1 程序执行过程

次 数 i值 判 断 循 环 动 作

第1次 i=0 i<3 成立,循环 输出 0 i++ 使得 i=1

第2次 i=1 i<3 成立,循环 输出 1 i++ 使得 i=2

第3次 i=2 i<3 成立,循环 输出 2 i++ 使得 i=3

第4次 i=3 i<3 不成立,循环结束

100
例 5-2 计算 sum = ∑i 。
i =1

分析:这是一个反复求和的过程,显然我们不能在程序中写语句:
sum = 1+2+3+ … +100;
因为程序中不允许有省略号,而真的写上 100 个数,一般人都会觉得麻烦。若要求计算:

– 69 –
C 语言程序设计

1000
sum= ∑i
i =1

就更无法想象。
显然这个式子需要相加 99 次,每次加法都是在前面累加和的基础上进行的。我们从中抽
取具有共性的算式:
sum = 部分累加和 + i
假设一开始的部分累加和为 0,把该算式重复 100 次,且 i 从 1 变到 100 ,就可以算出相应
结果。当使用 for 语句实现,必须确定 3 个表达式,在本例中:
初值表达式 i=1;
维持循环进行的表达式 i<=100;
循环变化表达式 i++
这样 for 语句可以写成:
for ( i=1;i<=100;i++)
sum = sum + i ;
sum 是专门存放部分累加和的变量。
源程序:
#include <stdio.h>
main( )
{
int i,sum;
sum=0;
for ( i=1;i<=100;i++)
sum = sum + i ;
printf("%d",sum);
}
由于 i 是在原累加和 sum 的基础上作进一步累加,一开始时,必须置 sum=0,以保证 sum
在 0 的基础上不断把 i 累加起来。如果漏写了 sum=0,则 sum 会含有一个随机值,若从一个
不确定的值开始累加,其计算结果是毫无意义的。
从这个例子,我们能看到,一个完整的循环结构程序必须包含 4 个组成部分。
(1)初始化部分:在进入循环之前,对有关变量的初值进行规定。如 sum=0,i=0 等。
(2)控制部分:决定循环是否进行下去的控制条件。如上例中,只有在 i<=100 时,循环
才会继续下去。
(3)工作部分:重复执行的语句,也称循环体。它可能是一条语句,也可能是包含多条
语句的复合语句。上例中,循环体是单一语句:sum=sum+i ;
(4)循环变化部分:它保证循环的展开过程中,每进行一次循环,应向循环结束条件靠
近一步,这样经过若干次的循环,最终使得维持循环的控制条件不再满足,从而结束循环。
上述 4 个组成部分,除第一部分是在进入循环之前执行(只执行一次) ,其他 3 个部分将
会重复执行。如果缺少第 4 部分的内容,将会造成循环的无法结束——“死循环”。这个错误
在初学时经常会出现。
– 70 –
第5章 循环结构程序设计

5.1.2 while 语句
while 语句也是较常用的循环语句。其格式为:
while (表达式)
语句;
while 的执行流程如图 5-2 所示。

假(0)
条件表达式
真(非 0)
语 句

while 的下一条语句
图 5-2 while 语句执行流程图

显然在流程图中,只体现了循环 4 个组成部分中的两个,缺少初始化部分和循环变化部
分,需要我们在使用 while 时另外加上去。
把 for 语句改写成 while 语句为:
表达式 1;
while (表达式 2) {
语句 ;
表达式 3 ;
}
100
例 5-3 用 while 语句实现 ∑i 。
i =0
源程序:
#include <stdio.h>
main( )
{ int i,sum = 0;
i=1 ;
while (i<=100) {
sum=sum+i ;
i++ ;
}
printf("%d\n" ,sum );
}

5.1.3 do-while 语句
for 语句和 while 语句都是在循环前先判断条件,只有条件满足才会进入循环,如果一开

– 71 –
C 语言程序设计

100
始条件就不满足,循环有可能一次都不进行。如求和 ∑ i ,若程序改为:
i =1

i=200 ; /* 初值置为 200 */


while (i<=100) {
……
}
则循环一次也不会进行。
而 do-while 语句适合于先循环,然后在循环过程中产生控制条件的情况,等一次循环结
束时,再判断条件,以决定是否进行下一次循环。其格式为:
do 语句
while(表达式)
do-while 语句执行流程如图 5-3 所示。

语 句
假(0)
条件表达式

真(非 0)

do-while 的下一条语句

图 5-3 do-while 语句执行流程图

与 while 语句一样,do-while 语句中也缺少初始化部分和循环变化部分,需要在程序中


加上去。
100
例 5-4 用 do-while 语句实现 ∑i 。i =0

源程序:
#include <stdio.h>
main( )
{ int i,sum = 0;
i=1 ; /* 循环初值 */
do {
sum=sum+i ;
i++; /* 循环变化表达式 */
} while (i<=100) ;
printf("%d\n" ,sum ) ;
}

– 72 –
第5章 循环结构程序设计

5.1.4 三种循环语句的使用
对于一个实际问题,应该使用三种循环语句中的哪一种呢?一般情况下,三种语句是通
用的。例 5-2 到例 5-4,分别使用了 3 种循环语句,解决同一个问题。当然,三种循环语句在
用法上还是有区别的,或者说各有特色。一般来说,循环次数在题目中已给出的话,使用 for
语句最清晰,循环的 4 个组成部分一目了然。当循环次数未知时,使用 while 语句较多。如
果必须从循环体中才能得到循环变量的值,然后再判断决定是否进行下一次循环,使用
do-while 语句最合适。
例 5-5 计算 1+3+5+…,共 20 项之和。
分析:由于从题目中已知一共相加 20 次,这样在循环次数已知的情况下,首选 for 语句。
初值: i=1
循环维持条件: i<=39(第 20 项值)
循环变化: i=i+2 (而不再是 i++)
源程序:
#include <stdio.h>
main( )
{
int i,sum;
sum=0 ;
for ( i=1;i<=39;i=i+2 )
sum = sum + i ;
printf("%d",sum);
}
说明:本例也可用下列方式实现。
t=1 ;
for (i=1; i<=20; i++) {
sum = sum + t ;
t=t+2 ;
}
例 5-6 输入 x 和 n,计算 x n。
分析:x n =x*x*…*x,一共 n 次连乘。虽然题目中并未说明循环的确切次数,但只要程
序执行的开始输入 n,就完全能控制循环次数。程序可以按循环次数已知的情况处理,选用
for 语句。
初值: i=1
循环维持条件: i<=n
循环变化: i=i++
源程序:
#include <stdio.h>
main( )
{

– 73 –
C 语言程序设计

int i, y, x, n;
y=1 ;
scanf ("%d%d", &x, &n) ;
for ( i=1;i<=n;i++ )
y = y*x ;
printf("%d",y);
}
说明:循环变量 i 只起计数作用,在循环体中并未用到 i 值,所以 i 也可从 0 开始。i<n
作为条件,甚至取 100~100+n,只要保证循环 n 次即可。
例 5-7 求菲波那契(Fibonacci)序列:1,1,2,3,5,8,…。请输出前 20 项。
分析:菲波那契序列的头两项均为 1,后面任一项都是其前两项之和。程序在计算中需
要用两个变量存储最近产生的两个序列值,且产生新数据后,两个变量要更新。题目要求输
出 20 项,循环次数确定,可采用 for 语句。
假定头两项用变量 x1=1、x2=1 表示,则新数据 t=x1+x2,然后需要对 x1 和 x2 更新:x1=x2
及 x2=t。
源程序:


#include <stdio.h>
main( )
{
int x1=1, x2=1, t, i ;
printf ("%d %d " , x1, x2 ) ; /* 输出头两项 */
for ( i=1;i<=18;i++ ) { /* 循环输出后 18 项 */
t = x1+x2 ;
printf("%d ",t );
x1=x2 , x2=t ;
}
}
运行结果如下:
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765

例 5-8 利用格里高利公式 p =1– 1 + 1 − 1 +…求π的近似值,精度要求到最后一项的绝


4 3 5 7
5
对值小于 10 。
分析:这是一个求和的例子,其中每一项都是分数,我们可以从例 5-5 变化而来。从题
目本身不能明显看出循环的次数,可使用 while 或 do-while 语句。循环的维持条件是任一项
值大于等于 10 5,所以在程序中增加存储第 i 项的变量 item。
初值: i=1; item=1;pi = 0 ;
循环维持条件: item>=10–5
循环体: pi=pi+item ; item = 1/ i ;
循环变化: i=i+2 (而不再是 i++)
由于循环变量 item 有初值,进入循环时可以先判断,故程序采用 while 语句。
本例中还要考虑每一项的符号是交替变化的。利用算术“负负得正”的特征,我们再设
– 74 –
第5章 循环结构程序设计

一个符号变量 s,开始 s=1,表示正号,每次循环 s= –s,改变符号。


源程序:
#include <stdio.h>
#include <math.h> /* 由于使用了求绝对值函数 fabs() */
#define EPS 1e-5 /* 把精度 10-5 定义成符号常量 */
main( )
{ int s=1 ;
float item,pi,i ;
pi=0; item=1; i=1 ; /* pi 存放结果累加和,item 存放第 i 项值 */
while (fabs (item) >=EPS) { /* fabs( )是求绝对值的函数 */
pi=pi+item ;
i=i+2 ;
s=-s ; /* s 是第 i 项符号,正负交错变化 */
item=s/i ; /* 第 i 项值 */
};
pi=pi*4 ; /* 原来得到的是 pi/4 */
printf ("pi=%8.6f" ,pi) ;
}
说明:本例也可以使用 do-while 语句,即先不规定 item 值,在循环体中计算 item 的值,
然后在循环体的后面,判断继续循环的条件是否满足。请读者写出相应程序。
例 5-9 输入一行字符,输出其中的英文字母,并分别统计出大写英文字母和小写英文
字母各有多少个。
分析:首先要通过键盘输入若个字符,经判断后输出,这就存在着重复输入与输出的过
程,需要使用循环实现。但循环的次数是不确定的,完全由输入决定,一旦按下回车键,循
环将结束。行标志由字符’\n’决定。
初值: 统计大小写英文字母个数的变量 low=0, up=0
循环维持条件: ch != ’\n’
循环体: ch = getchar( ) ;
if (ch 是字母) {
putchar(ch) ;
分别统计大小写英文字母个数
}
循环变化: 无(隐含在循环体 ch=getchar()中)
考虑使用 do-while 语句,因为判断循环维持条件的 ch 要在循环体中得到,并且输入至少
会有一个回车键,能保证循环次数大于等于 1。
源程序:
#include <stdio.h>
main( )
{
char ch ;
int low=0,up=0 ;

– 75 –
C 语言程序设计

do {
ch=getchar( ) ;
if (ch>=’A’&&ch<=’Z’||ch>=’a’&&ch<=’z’) {
putchar(ch) ;
if (ch>=’A’&&ch<=’Z’) up++ ;
else low++ ;
}
}while(ch!= ’\n’) ;
printf ("\n lowercase=%d , uppercase=%d \n" , low, up);
}

5.1.5 for 语句的形式变化


for 语句是使用得最多的循环语句形式,原因之一是 for 语句结构比较清晰,4 个组成部
分都体现在一条语句中,不易遗漏;原因之二就是 for 语句使用起来非常灵活。下面我们以
例 5-2 求和过程的循环为例,说明 for 语句的各种变化形式,以了解其使用灵活性。
【标准形式】
sum=0;
for ( i=1 ; i<=100 ; i++ )
sum=sum+i ;
执行流程如图 5-4 所示。

sum=0

i=1

i <=100
真(非 0)
sum=sum+i

i++

for 的下一条语句

图 5-4 执行流程

【变化形式 1】表达式 1 为逗号表达式


for ( sum=0, i=1 ; i<=100 ; i++ )
sum=sum+i ;
在表达式 1 位置上,完成了两个变量赋值,增强了表达式 1 的功能。
【变化形式 2】省略表达式 1

– 76 –
第5章 循环结构程序设计

sum=0 ;
i=1 ;
for ( ;i<=100 ; i++ )
sum=sum+i ;
表达式 1 的工作本身只在进入循环前执行一次,把它放到了循环之前,效果相同。
【变化形式 3】省略表达式 3
sum=0 ;
i=1 ;
for ( ;i<=100 ; ) { /* 该形式等价于 while 语句 */
sum=sum+i ; /* 表达式 3 放在循环体内 */
i++ ;
}
【变化形式 4】省略表达式 2
sum=0 ;
i=1 ;
for ( ; ; ){ /* 没有条件控制的无限循环 */
if ( i>100 ) break ;
sum=sum+i ;
i++ ;
}
for 语句中的条件控制表达式省略了,该控制功能放到循环体内,由 break 语句实现循环
结束。在学习 switch 语句时,我们曾用到过 break,其作用是强制结束 switch 语句的继续执
行,这里 break 同样能强制结束循环。break 的具体用法将在下一节作详细介绍。如果真的没
有循环条件控制,将会发生无限循环,造成程序无法正常结束。
【变化形式 5】省略循环体语句
for ( sum=0,i=1 ; i<=100 ; sum=sum+i ,i++ )
; /* 空语句 */
分号不能少,表示是空循环体,真正的循环工作在表达式 2 和表达式 3 中完成。
一般 for 语句中,表达式 2 尽量不要使用逗号表达式。如写成
for ( sum=0,i=1 ;i<=100 ,sum=sum+i ; i++) ;
循环将无法结束。因为在逗号表达式中,最后一个表达式的值作为整个逗号表达式的值,循
环拿 sum=sum+i 做判断条件,显然出错。即使写成:
for ( sum=0,i=1 ;sum=sum+i ,i< =100 ; i++) ;
仍然是错误的,因为 sum=sum+i 的执行先于判断 i< =100,与原来执行流程不符。
说明:
(1)3 个表达式的各种省略形式中,每一个表达式都是独立存在的,即任意一个表达式
的省略,不影响其他两个表达式的书写。
(2)不管是哪一种省略形式,只是表达式的书写不在 for ( )中,而写到了循环体中或循
环体前面,但作为一个完整的循环,循环 4 个组成部分缺一不可,且书写的位置必须符合 for
的执行流程,不得改变。否则会影响循环的正常执行。
(3)5 种 for 语句的变换形式,所对应的流程图是完全相同的。读者可以自己画出来对照。
– 77 –
C 语言程序设计

(4)建议初学者按照标准形式书写,以免带来混乱。本节的目的是给出 for 语句的灵活


描述方式,但主要是希望读者进一步巩固对 for 语句执行流程的理解。

5.2 break 语句和 continue 语句

对于简单的循环程序,选用 3 种循环语句即可完成。但对于一些复杂情况,尤其是多条
件控制循环时,还需要引入 break 和 continue 语句,它们在循环控制中起辅助作用。下面分
别介绍这两种语句。

5.2.1 break 语句
前面我们已经使用过 break 语句,它能够强制使循环结束。我们先看一个例子。
例 5-10 从键盘接收一行字符,个数不超过 10 个,并将它输出。
分析:当键盘输入 10 个字符回车,正好输出该 10 个字符,因此循环的最大次数为 10。
如果输入的一行字符少于 10 个,一旦碰到回车符,循环也将结束,所以回车符也是循环结束
的因素。这样字符个数等于 10 和输入字符为’\n’(回车)是结束循环的两个条件,对于多条
件控制循环时,可以使用 break 语句来实现。
源程序 1:
#include <stdio.h>
main()
{ char c;
int i ;
for (i=1;i<=10;i++) { /* i>10 是循环结束条件 */
c=getchar();
if (c==’\n’) break; /* c=回车,也是循环结束条件 */
putchar(c);
}
}
上述程序中 10 个字符用 for 语句控制,而回车符是在循环过程中出现的,该条件控制用
break 语句来实现。
从上面例子中可以看出,当循环结构中出现多个循环控制条件时,break 语句是实现循环
结束的有效方法。当然我们也可以只用 for 语句实现,把两个循环控制条件合起来作为 for
中的表达式 2,但在程序结构上要稍作调整。
源程序 2:
#include <stdio.h>
main()
{ char c;
int i=0;
c=getchar();
for (i=0; i<10 && c != ’\n’ ; i++) /* 合并两个循环控制条件 */
{ putchar(c);

– 78 –
第5章 循环结构程序设计

c=getchar();
}
}
说明:通过上面例子的两种实现方式,读者可以初步了解解决多个循环控制条件的方法。
但不是所有的循环控制条件都可合并到循环语句的条件控制上,有些控制条件是需要循环体
的复杂处理才能得到,而只能在循环体中通过 break 语句来实现,使得控制形式十分灵活有
效。for 结构中的 break 语句执行流程如图 5-5 所示。

表达式 1

假(0)
表达式 2
真(非 0)
语 句1


break

语 句2

表达式 3

for 的下一条语句

图 5-5 for 结构中 break 语句的使用

5.2.2 continue 语句
continue 语句的作用是跳过循环体中 continue 后面的语句,而继续展开下一次循环。
continue 语句只能用在循环体中,其工作流程如图 5-6 所示。
例 5-11 用 continue 语句来替代例 5-10 中的 break 语句。
源程序:
#include <stdio.h>
main()
{ char c;
int i=0;
for (i=0;i<10;i++) { /* i>10 是循环结束条件 */
c=getchar();
if (c==’\n’) continue; /* 当 c=回车,跳过后面的 putchar(c)语句 */
putchar(c);

– 79 –
C 语言程序设计

}
}
运行结果如下:
abcd <CR> (循环不会结束,继续等待输入直到 10 个字符为止)
efghijk<CR>
abcdefghi

表达式 1

假(0)
表达式 2

真(非 0)
语 句1

continue

语 句2

表达式 3

for 的下一条语句

图 5-6 for 结构中 continue 语句的使用

说明:共输出 9 个字符,但加上字符 d 后面的回车,正好是 10 个字符。例子中当 c==’\n’


时,continue 的作用是跳过后面的 putchar(c) 语句,但循环并未结束,而是开始下一次循环,
执行 i++,由于此时 i=5,循环将继续进行。该程序的执行情况如下:

i 值 输入 输出
0 c=’a’ a
1 c=’b’ ab
2 c=’c’ abc
3 c=’d’ abcd
4 c=’\n’ abcd (没有新输出)
5 c=’e’ abcde

9 c=’i’ abcdefghi
10 循环结束

读者可以自己对比一下两个程序,以区分 break 和 continue 两条语句的不同之处。

– 80 –
第5章 循环结构程序设计

5.3 循环嵌套

对于一些复杂的问题,受多个因素共同影响,且每个因素有自己的变化范围,普通的循
环只能解决一个因素,这就需要在一个因素的循环体中再包含其他因素的循环。这种在循环
体中包含另外循环的形式称为循环嵌套。
例 5-12 求 S = 1!+2!+3!+ … + n! (n 由输入决定)
分析:1!+ … + n!显然需要累加 n 次,这个重复过程需要靠循环实现。而 i!= 1*2* …
* i 又要通过 i 次循环来计算。因此,大的循环控制 n 次加法,循环体中 i!的计算由小的循
环 i 次得到,这样就需要用循环嵌套实现。由于循环次数 n 和 i 都是确定的,循环语句形式
可以使用 for 语句。
源程序 1:
main( )
{ int i, j, n ;
long int t=1, sum=0 ; /* 由于 n!的值较大,采用 long 变量 */
scanf("%d", &n);
for ( i= 1 ;i<=n; i++) {
t=1 ; /* 保证每一次阶层从头算起 */
for ( j= 1 ;j<=i; j++) /* 计算 i! */
t=t*j;
sum = sum + t ; /* 把 i!累加到 sum 中 */
}
printf("n!= %ld", sum) ;
}
该程序可以进一步改进。由于 i!= (i−1)!* i ,所以 t 不必每次都从 1 开始乘,只要对
前一次循环的结果(i−1)!再乘 i 即可。小的循环可以省略。
源程序 2:
main( )
{ int i, n ;
long int t=1, sum=0 ;
scanf("%d", &n);
for ( i= 1 ;i<=n; i++) {
t=t*i;
sum = sum + t ;
}
printf("n!= %ld", sum) ;
}
从程序可理解性看,源程序 1 较清楚,但使用了二重循环,执行起来不如源程序 2 效率
高。尤其对于多重循环,其计算量会成倍增加。
例 5-13 已知小鸡 1 文钱买 2 只,公鸡 2 文钱买 1 只,母鸡 3 文钱买 1 只。问用 100 文

– 81 –
C 语言程序设计

钱正好买 100 只鸡,有多少种买法。


分析:这是一个组合问题,共有 3 个因素决定了买法的种数,即小鸡、公鸡和母鸡的取
值范围均为 0~100,各种鸡的取值与其它鸡的取值无关。因此对于每一种鸡的取值都要反复
地试,最后确定满足 100 文钱正好买 100 只鸡的组合。显然这要用循环来解决,3 种鸡按照
各自的取值范围循环,需采用三重循环嵌套。
源程序 1:
main()
{
int chick,rooster,hen ;
for (rooster=0; rooster<=100; rooster++)
for (hen=0; hen<=100; hen++) {
for (chick=0; chick<=100; chick++)
if (rooster+hen+chick==100 && rooster*2+hen*3+chick*0.5 == 100)
printf("rooster=%d hen=%d chick=%d\n",rooster,hen,chick) ;
}
}
运行该程序,将得到 7 种组合,结果如下:
rooster=0 hen=20 chick=80
rooster=5 hen=17 chick=78
rooster=10 hen=14 chick=76
rooster=15 hen=11 chick=74
rooster=20 hen=8 chick=72
rooster=25 hen=5 chick=70
rooster=30 hen=2 chick=68
分析:上述程序有一些值得改进的地方。首先由于最多只有 100 文钱,公鸡的只数不会
超过 50 只,母鸡的只数不会超过 33 只。另外,一旦公鸡和母鸡的只数确定下来后,小鸡的
只数应该是:100−公鸡数−母鸡数。所以源程序 1 可应一步改进。
源程序 2:
main()
{
int chick,rooster,hen ;
for (rooster=0; rooster<=50; rooster++)
for (hen=0; hen<=33; hen++) {
chick = 100 - rooster - hen ;
if (rooster*2+hen*3+chick*0.5 == 100)
printf("rooster=%d hen=%d chick=%d\n",rooster,hen,chick) ;
}
}
源程序 2 只对两个因素进行循环,且循环的次数也远小于源程序 1,其执行效率得到了
极大提高。
对于循环嵌套程序,读者应该知道程序的执行流程。源程序 2 中:

– 82 –
第5章 循环结构程序设计

for (rooster=0;rooster<=50;rooster++) /* 循环① */


for (hen=0;hen<=33;hen++) /* 循环② */
循环体 body
采用二重循环,其中循环②是循环①的循环体,当 rooster=0 时,循环②要完整地执行一次,
即 hen 从 0~33 循环 34 次,然后 rooster=1,hen 再从 0 ~ 33 循环 34 次,直到 rooster=50 为
止。源程序 2 的执行情况如下:

rooster=0 hen=0 body 第 1 次循环执行


hen=1 body 第 2 次循环执行
 
hen=33 body 第 34 次循环执行
rooster=1 hen=0 body 第 35 次循环执行
hen=1 body 第 36 次循环执行
 
hen=33 body 第 68 次循环执行

rooster=50 hen=0 第 1700 次
hen=1 第 1701 次
 
hen=33 第 1734 次

而对源程序 1 来说,是个三重循环,其 if …printf 语句将共循环约 1 百万次。显然源程序 2


的执行效率大大高于源程序 1,因为源程序 1 中做了大量的无用功。源程序 1 的执行情况如下:

rooster=0 hen=0 chick=0 body 循环第 1 次


chick=1 第2次
 
chick=100 第 101 次
hen=1 chick=0 第 102 次
chick=1 第 103 次
 
chick=100 第 202 次

hen=100 chick=0 第 10101 次
chick=1 第 10102 次
 
chick=100 第 10201 次
rooster=1 hen=0 chick=0 → 101
hen=1 chick=0 → 101
 
hen=101 chick=0 → 101


– 83 –
C 语言程序设计

rooster=100 hen=0 chick=0 → 101


hen=1 chick=0 → 101
 
hen=101 chick=0 → 101 第 1030301 次

建议读者在上机时用单步调试工具执行源程序 1,观察各循环变量的变化情况,以进一
步加深理解(上机调试可参考附录 A) 。
在循环嵌套程序设计中,首先要确定那些因素需要重复运算,并且它们应有各自的取值
范围。然后判断各因素间是否有联系,以确定哪个因素做大循环,哪个因素做小循环。在例
5-12 中,累加是大循环,求 i!是小循环。而对于例 5-13 来说,是个组合问题,哪种鸡做大循


环,哪种鸡做小循环,都没有关系,可任意规定。

5.4 循环程序设计
在程序设计中,当遇到有一些工作要反复执行,则必须使用循环语句。这里要区分循环
结构与分支结构。这两种结构中都用到了条件判断,但条件判断后的行为完全不同,分支结
构中的语句只执行一次,而循环结构中的语句,则往往会反复多次地被执行。
循环程序实现要点:
(1)找出什么要反复执行 —— 循环体
(2)重复到何时结束 —— 循环控制条件
把这两点确定下来,循环结构就基本确定。
循环的具体实现有两类形式,一类是循环次数事先就明确的,如前面介绍过的求和例子(例
,一般使用 for 语句;另一类是循环次数不明确,而是通过别的条件来控制的,如求π的
5-2)
例子(例 5-8) ,题目中并不能直接看出循环的次数,但有一点是明确的,即一旦某一项值的绝
5
对值小于 10 ,循环就会结束,可使用 while 或 do-while 语句。这两类实现形式也可以理解为:
if (循环次数已知)
使用 for 语句
else /* 循环次数未知 */
if (循环控制条件在进入循环时明确)
使用 while 语句
else /* 循环控制条件需要在循环体中明确 */
使用 do-while 语句
下面我们通过几个例子的学习,进一步理解循环程序设计的思路与技巧。
例 5-14 从输入的若干个正数中选出最小数。
分析:本题要不断从键盘输入数据,判断选择出最小数,显然是一个重复性的工作,需
要循环来解决。但题目中对输入数据的个数没有指定,循环结束条件就无法确定,这是本题
的难点。解决的办法是自己增加循环结束条件。具体实现方法有两种。
① 先输入数据个数 n,然后陆续输入 n 个数据,用 n 来控制循环次数。
② 以特殊数据来作为正常输入的结束,比如正常数据均为正数,我们可以输入一个负数

– 84 –
第5章 循环结构程序设计

作结束标志。
方法①的程序请读者自己编程实现。方法②具有更好的灵活性,下面我们按照该思路编
写程序。由于循环次数未知,我们首先考虑使用 while 语句。
源程序 1:
main( )
{
float x , min ;
scanf ( "%f", &x) ; /* 输入第一个数据 */
min = x ; /* 假定它是最小数 */
while ( x>=0 ) { /* 只要输入数据 >0,循环 */
if ( x < min ) /* 若输入的数据比原来假定的最小数更小 */
min = x ; /* 更新最小数 */
scanf ("%f", &x) ; /* 为下一次循环读入新数据 */
}
printf("the minium number is %f" , min) ;
}
分析:源程序 1 中,由于采用 while 语句,先判断后循环,循环前 x 必须先输入一个确
定值。循环体内要先比较,把已输入的数据处理了,再输入新数据。本例程序也可以用 do-while
语句实现。
源程序 2:
main( )
{
float x , min ;
scanf ("%f", &x) ;
min = x ;
do {
if ( x < min ) min = x ;
scanf ("%f", &x) ;
} while ( x>=0 ) ;
printf("the minium number is %f" , min) ;
}
分析:源程序 2 循环的前提是第一个输入数据不能是负数,因为 do-while 语句是先无条
件循环一次,再判断要不要继续循环。但如果把源程序 2 改成下列形式将会出错:
float x , min=0 ;
do {
scanf ("%f", &x) ;
if ( x < min ) min = x ;
} while ( x>=0 )
当输入到最后一个特殊数据(即负数)时,先经判断,负数比前面的正数都要小,则 min=
负数,然后 while ( x>=0 ) 不成立,循环结束,这时得到的 min 是错误结果。源程序 2 还可以
改成如下程序。
源程序 3:
– 85 –
C 语言程序设计

main( )
{
float x , min=32767 ; /* 32767 是最大正数,任何输入的数都将比它小 */
do {
scanf ("%f", &x) ;
if ( x < min && x >=0 ) /* 加条件约束 */
min = x ;
} while ( x>=0 ) ;
printf("the minium number is %f" , min) ;
}
说明:求最小数时,初值应置成最大数,使得任何输入的数都比它小,从而找出最小数。
若初值被置成最小数的话,所有输入数都不会比它小,最后的结果变成初值。当求最大数时,
初值应置成最小数。
本例程序还可以用 for 语句实现。
源程序 4:
main( )
{
float x , min ;
scanf ("%f", &x) ;
for (min = x ; x>=0 ; ){
if ( x < min ) min = x ;
scanf ("%f", &x) ;
}
printf("the minium number is %f" , min) ;
}
上面对于同一个问题,我们采用了 3 种循环语句、4 个程序实例来实现。我们可以看出,
一般问题 3 种循环语句都可以解决,但与之配套的其他语句有所不同。不管采用什么方式实
现,最终都应满足题目要求。
例 5-15 将键盘读入的一个任意正整数逆序输出。例如键盘输入 12345,屏幕输出显示
54321。
分析:为了实现正整数逆序输出,需要把该正整数按逆序逐位拆开,然后输出。对于多
个位数,显然需要多次循环,每一次循环分离一位。分离方法通常采取求余运算(%)。
假定输入 x=12345 逆序输出,从低位开始分割:
12345%10 = 5
为了能继续使用%10 来分隔下一位,需改变 x 的值:
12345 /10 =1234
把上述操作不断重复:
1234%10 = 4
1234/10 =123
123%10 = 3
123/10 =12
– 86 –
第5章 循环结构程序设计

12%10 = 2
12/10 =1
1%10=1
1/10=0
当 x 经整除 10 后变成 0 时,过程结束。把处理过程归纳得到:
重复的步骤:
x % 10 分隔一位
x = x/10 为下一次分隔准备
循环直到:
x == 0
由于循环次数由数值的位数决定,不同的输入其循环次数不同,因此对程序来说,循环
次数属未知,循环语句采用 while 形式。
源程序:
main( )
{


int x ;
scanf ("%d", &x) ;
while ( x>0 ){
printf("%d " , x%10) ;
x=x/10 ;
}
}
上述程序非常简练,但分析过程和编程思路非常重要,希望读者能按照这样的思路解决
循环的问题。
例 5-16 求 500 以内的全部素数,并以每行 10 个进行输出。
分析:首先要通过 500 次循环,对 500 个数进行判断,若是素数,则输出。
for (m=2; m<=500; m++)
if (m 是素数)
printf(" %6d", m); /* 素数输出 */
而判断一个数是否是素数,需要检查该数是否能被其他数整除。假设该数为 m,则整除它的
最大数不超过 m。因此判断 m 是否是素数,可判断 m 能否被 2 ~ m −1 之间的数整除,若都不
能整除的话,则 m 是素数。这需要用一个小循环来实现。但一旦 m 能被某个数整除(m 除以
该数的余数为 0),则没有必要再用后面的数继续整除,需提前结束循环。
for ( i = 2; i < m ; i++)
if (m%i ==0) break ;
printf(" %6d", m); /* 素数输出 */
可以看出小循环有两个结束条件: i >= m 和 m%i ==0。
不管哪种条件结束,都将顺序执行 printf( )输出,而素数输出 printf(" %6d", m) 并不是
无条件执行的,只有经过判断,m 确实是素数(即不能被任一数整除)时才进行输出。因此
要做判断,循环是由 for 语句中 i == m 来结束循环,才进行输出。所以判断一个数据 m 是否

– 87 –
C 语言程序设计

位素数的程序段如下:
for ( i = 2; i < m ; i++)
if (m%i ==0) break ;
if (i == m ) printf(" %6d", m);
另外,在循环次数上还可以进行优化。如果 m 能被某数整除的话,即它可由两个整数构
成 m = x*y,那么一定有:x<= m ,y>= m 。所以,判断素数的循环只要循环到 2~ m (sqrt(m))
即可。
源程序:
#include "math.h" /* 使用开根号函数,需要包含数学库 */
main( )
{
int m, i , n=0 ; /* n 用于控制输出格式,每十个数据一行 */
for (m=2; m<=500; m++) {
for ( i=2; i<=sqrt(m); i++) /* 判断素数 */
if (m%i ==0) break ;
if (i > sqrt(m) ) {
printf(" %6d", m); /* 素数输出 */
n++ ; /* 已输出素数的个数 */
if (n%10==0) printf("\n") ; /* 保证输出素数时每行 10 个 */
}
}
}
程序的运行结果如下(每行输出 10 个)

2 3 5 7 11 13 17 19 23 29
31 37 41 43 47 53 59 61 67 71
… … … … … … …
419 421 431 433 439 443 449 457 461 463
467 479 487 491 499
在这个例子中,素数的判断需要循环来处理,判断的结果有两种,即是素数或不是素数,
它对应到循环就有两个结束条件。程序实现时,需引入 break 语句。另一方面,不管哪一种
条件导致循环结束,程序都将顺序执行循环后面的语句。而素数输出语句的执行不是无条件
的,所以必须经过判断,确实是素数时才能输出。这是一个相当经典的例子,所用的处理方
法,在后面的程序中还会用到,希望读者能够理解和掌握本例的精髓。

习 题

1.选择题
(1)C 语言 while 语句中,用于条件判断的表达式是 。
A.关系表达式 B.逻辑表达式 C.算术表达式 D.任意表达式
(2)下列 while 循环, 将执行 。
i= 4;

– 88 –
第5章 循环结构程序设计

while (--i) printf("%d ", i);


A.3 次 B.4 次 C.0 次 D.无限次
(3)下列程序段执行后 s 值为 。
int i=1, s=0;
while(i++) if (!(i%3)) break; else s+=i;
A.2 B.3 C.6 D.以上均不是
(4)下述程序的输出结果为 。
#include <stdio.h>
main()
{
int x=3, y=6, z=0 ;
while (x++!=(y-=1) {
z++ ;
if (y<x) break ;
}
printf(" x=%d, y=%d, z=%d",x, y, z) ;
}
A.x=4, y=4, z=1 B.x=5, y=4, z=3 C. x=5, y=4, z=1. D. x=5, y=5, z=1
2.填空题
(1)用 for 语句循环打印 0 1 2 0 1 2 0 1 2:
for( i=1; i<=9; i++ ) printf("%2d", ____);
(2)下列程序段的输出为_________ 。
#include <stdio.h>
main()
{
int i=1 ;
while (i<=-1)
printf("###") ;
printf("%d", i ) ;
}
(3)下列程序段的输出为_________ 。
#include <stdio.h>
main()
{
int i=5 ;
do {
i-- ;
printf("###") ;
} while (i) ;
printf("%d" , i ) ;
}
(4)下列程序段的输出为_________ 。

– 89 –
C 语言程序设计

int x=5 , y=10 ;


do {
x>y ? (x-=1 , y+3) : (x+=4 , y-=2 ) ;
} while ( x+y<15) ;
printf(("x=%d, y=%d\n", x, y ) ;
(5)下述程序的输出结果为_________ 。
#include <stdio.h>
main()
{
int k=1 ; long sum=0 ;
do {
k=k*(k+1)/2 ;
sum+=k ;
} while (sum%7) ;
printf("%ld" , sum ) ;
}
(6)下列程序求 Sn=a+aa+aaa+…+aa…aa(n 个 a)的值,其中 a 是一个数字。例如,若
a=2, n=5 时,Sn=2+22+222+2222+22222,其值应为 24690。
main()
{ int a, n, count=1, sn=0, tn=0;

printf("Please input a and n:\n");


scanf("%d%d", &a, &n);
while (count <=n) {
tn=tn+a;
sn=________;
a=a*10;
_______;
}
printf("the Sn is: %d \n", sn);
}
3.请说明下列程序中出现死循环的原因。
(1)#include <stdio.h>
main()
{
int i ;
scanf("%d", &i ) ;
while (i--)
printf("%d", i) ;
}
(2)#include <stdio.h>
main()

– 90 –
第5章 循环结构程序设计

{
int i ;
for ( ; ; )
printf("%d\n", i ) ;


}
(3)main()
{
int i=4 ;
do {
printf("%d", i++ ) ;
} while (i) ;
}
4.编写程序,计算正整数 1~ n(n 需键盘输入)之间所有奇数之和与偶数之和。
5.编写程序,输入 10 个数,打印出最大数和最小数。
6.编写程序,输入一个整数,求它的各位数之和及位数。例如 123 的各位数之和是 6,
位数是 3。
7.编写程序,求 e≈1/1!+1/2!+1/3!+…1/n!
循环结束条件如下:
(1)直到第 10 项。
(2)直到最后一项小于 10 7。
(3)直到最后两项之差小于 10 7。
8.有一个分数序列:
2 3 5 8 13
, , , , ,…
1 2 3 5 8
编写程序求出这个序列的前 n 项之和。
9.用一张 1 元票换 1 分、2 分和 5 分的硬币(至少各一枚) ,问有哪几种换法?各几枚?
请编写程序。
10.编写程序,求 1~10000 之间所有满足各位数字的立方和等于它本身的数。例如 153
的各位数字的立方和是 13+53+33=153。
11.编写程序,输入一行字符,分别统计出其中的英文字母、空格、数字和其他字符的
个数。
12.编写程序,输入一批由 5 个字母组成的英文单词,要求将其中以字母“A”或“a”
开头的单词打印出来。
13.编写程序,输入一行字符,统计其单词的个数,各单词间以空格为分隔,且空格数
可以是多个。
14.编程验证哥德巴赫猜想:任何一个大于 6 的偶数均可表示为两个素数之和。例如
6=3+3,8=3+5,…,18=7+11。要求将 6~100 之间的偶数都表示成两个素数之和,打印时一
行打印 5 组。
15.编写程序,输入一个长整数,从高位开始逐位分割并输出。例如输入 123456,逐位
输出:
– 91 –
C 语言程序设计

1,2,3,4,5,6。
16.编写程序,打印出以下图案:
*
***
*****
*******
*****
***
*
17.编写程序,模拟简单计算器的工作。假定简单计算器只能进行双目实型运算,运算
的次序按输入的运算数和运算符的顺序进行,在输入时运算数和运算符之间用空格隔开,例
如,输入“10 + 2 / 3 = ”后结果得 4.0。

– 92 –
第6章 函 数
通过前面几章的学习,我们可以编写一些简单的程序。通常我们把整段程序写在主函数
main( )中。如果我们还想编写一个解决复杂问题的程序,比如俄罗斯方块游戏,该游戏程序
一般需要 1000 多行语句。如果把这 1000 多行语句全部写在一个主函数中,无论从编程角度
还是从调试角度,都是十分困难的。
20 世纪 60 年代,为了解决复杂问题的编程,提出了结构化程序设计思想,即为了解决
一个大问题的编程,首先把大问题分解成若干个小问题,每一个小问题用一个程序模块实现,
再把这些小程序模块像搭积木那样合成起来,形成解决整个问题的大程序。这种方法的好处
是对问题逐步求精,程序结构清晰,逻辑性强,并且小程序模块的规模小,容易编制调试,
从而在最大程度上保证程序设计的正确,也有利于以后对程序的维护。
函数是实现程序模块的基本元素。main( )就是一个特殊函数,称为主函数。C 语言中所
有语句都是以函数做载体,就像磁盘中的信息是以文件做载体一样。一个完整的 C 语言程序
可由一个主函数和若干个函数组成。本章将首先介绍函数的定义与调用,然后介绍程序模块
的有关概念。

6.1 函数定义

6.1.1 函数概念
函数对我们来说其实并不陌生,从第 1 章开始,我们就不断地使用标准的输入输出函数
scanf( )和 printf( )。这些函数又称为 C 语言的标准库函数,由 C 语言系统提供。我们只需按
照 C 语言标准的规定来书写使用,而不必关心这些函数具体是如何实现的。但标准库函数提
供的只是最基本、最普通的一些功能,如基本的输入输出、数学函数的计算、字符串的处理
等。而我们在解决具体问题时,所需要的功能往往是各式各样的,无法都由库函数来提供,
这就需要我们自己来定义编写所需函数。这类函数称为自定义函数。因此,我们把函数分成
两类:标准函数和自定义函数。本章主要介绍自定义函数。
自定义函数的用途有如下两种。
(1)用于实现结构化程序设计中的程序模块。例如,我们要编写统计一个班级每个同学
所有课的平均成绩的程序,就可以把程序分解成 3 个功能模块,即输入成绩、计算平均分和
输出平均分,每一个模块用一个函数实现,通过主函数调用把它们连接起来。对于复杂的程
序,使用结构化程序设计的优点将更明显。
(2)用于实现需要反复多次使用的功能。例如在编写一个统计软件中,可以把经常用到
的求平均值功能写成一个函数,一旦要计算平均值,只需调用该函数即可,避免每次计算平
– 93 –
C 语言程序设计

均值都要把计算过程写一遍。这样,函数一次编写多次使用,大大提高了编程的效率,就像
我们需要输出时,只需调用 printf( )函数即可,而不必每一次都把输出的实现过程写上。
在程序设计中,一般一个函数的语句控制在 20 行以内为好,最多不宜超过 50 行语句。
这不仅易于程序在屏幕上的显示,更重要的是功能简洁,容易被人理解,编程时不易出错,
为程序良好的可读性奠定了基础。程序可读性是衡量一个程序质量的重要指标。

6.1.2 函数定义
函数是指完成一个特定工作的程序模块。而完成特定工作后有两种可能:
(1)有一个明确的运算结果产生并需要回送,比如像求平方根的标准函数 sqrt ( );
(2)只是完成一系列程序步骤,不需要回送明确的运算结果。
在自定义函数时,对于上述两种情况需要分别定义。

1.返回确定值的函数定义

返回确定值的函数定义的形式如下:
返回类型 函数名(参数表)
{
函数实现过程(函数体)
return 表达式;
}
函数名前面的“类型” ,我们通常称其为函数类型,它实际上是函数返回值的类型,一般
与 return 中结果的类型一致。
return 的作用是把函数运算的结果回送给调用者。请读者注意,return 返回的结果只能是
一个数值,如果函数的结果有多个值,将无法通过 return 返回。例如对求一元二次方程的函
数,就不可能用 return 来返回两个根。我们后面会介绍通过指针处理函数多结果返回的问题。
例 6-1 编写求 n!的函数。
分析:在第 5 章中我们通过循环已经解决了 n! 问题,但如果我们经常要求阶乘,若每
一次使用都要把循环写一遍,显然很不方便。函数可以解决这个问题。如果经常用到的功能
是一段复杂的程序,使用函数的优点就更明显。
源程序:
long fact ( int n )
{
int i ; /* 函数中需用到的工作单元 */
long res=1; /* 变量 res 用于存放阶乘结果 */
for (i=1; i<=n; i++)
res = res * i ;
return res ; /* 把阶乘的运算结果回送给调用者 */
}
说明:程序第一行定义了函数头,它不是一条语句,后面不跟分号。其中 fact 是函数名;
该函数的类型为 long; n 是参数,表示求阶乘函数运算的依据(已知条件)。

– 94 –
第6章 函数

2.不返回结果的函数定义

不返回结果的函数定义的形式如下:
void 函数名(参数表)
{
函数实现过程(函数体)
}
不返回结果的函数定义中,函数名前面的类型规定为 void,表示无明确的返回类型,它
必须注明,如果不指定任何类型,按 C 语言规定将隐含地认为是整型 int。另外函数的最后不
需要 return 语句。
void 类型的函数虽然不直接返回一个确切的值作为结果,但它的作用通常以屏幕输出、
改变变量值等方式表现出来。
例 6-2 编写输出 5 行“*******”的函数。
分析:函数的功能非常明确,它只是在屏幕上输出 5 行星号,不经过任何运算,没有明
显的运算结果。并且函数的输出星号行数已定,连函数参数都可以省去。
源程序 1:
void output ( ) /* 函数名为 output,无返回类型,无参数 */
{
int i ;
for ( i=0; i<5; i++)
printf ("*******\n") ;
}
当然该函数功能完全被固定,如果想变化函数中的输出行数,可以通过参数指定。
源程序 2:
void output1 ( int row ) /* 参数 row 是星号的行数 */
{
int i ;
for ( i=0; i<row ; i++)
printf ("*******\n") ;
}

6.1.3 函数的参数
函数参数是函数运行必须具备的已知条件,根据具体情况,参数可以是 1 个,也可以是
多个,或者没有参数。例 6-1 的函数是计算 n!,在 fact ( )定义中有一个参数 n。如果把 fact ( )
看作为问题的求解过程, 则 n 是解题必须具备的已知条件,主函数 main( )只有把 n 告诉 fact( ),
函数才能计算阶乘值。
例 6-3 对浮点数 x 和整数 n,编写求 x n 的函数。
分析:C 语言的标准库函数中提供了计算 x y 的函数 pow(x,y),本例将简单实现 x n 的过
程。必要的条件是已知 x 和 n,因此把它们确定为函数参数。
x n = x*x*……*x (共 n 个 x 连乘)

– 95 –
C 语言程序设计

该过程需要循环实现,思想与 n!相似。
源程序:
double expon( float x , int n)
{
double y ;
int i ;
for ( i=0 , y=1 ; i<n ; i++ )
y=y*x;
return y;
}
本例中函数的返回类型为 double,参数表中有两个参数,中间由逗号分隔,每个参数前
面的类型都必须写明。参数表的格式为:
类型 1 参数 1 ,类型 2 参数 2 ,……,类型 n 参数 n
例如:
int x , int y , float z
注意千万不可写成:
int x , y , float z
这样会造成 y 没有类型指定,它不是普通的变量定义。
另外,expon( )中还定义了普通变量 double y,y 只是函数当中用到的工作单元,它不是
已知条件之一。理解了这点后,以后我们在函数编写中,只有必须从主函数中得到的已知条
件,才定义为参数,其他用到的工作单元一律定义成普通变量。在函数执行过程中,参数的
使用与普通变量相同。
上述的 expon( )函数是无法独立运行的。任何完整的 C 语言程序都必须有主函数 main( ),
由它来调用 expon( )函数。
main( )
{
int a ;
double t , y ;
scanf ("%lf %d" , &t , &a ) ;
y = expon( t , a ) ; /* 调用上面定义的函数过程求 t a */
printf("%f \n" , y) ;
y = expon( 3.5 , 4 ) ; /* 调用上面定义的函数过程求 3.5 4 */
printf("%f \n" , y) ;
}
函数定义中的参数我们称其为形式参数(简称形参) ,x 和 n 是两个形参;对应到主函数,
y = expon( t , a )称为函数调用,其中 t、a 与形参 x、n 对应,称 t 和 a 为实际参数(简称实参)

它的作用是把已知条件告诉给形参。形参和实参必须个数相同,类型最好一致,如果两者类
型不一致,则以形参类型为准。当然不合适的转换会导致数据的不正确,所以建议在编程中
尽量保持形参与实参的类型一致。
函数的形参一定是变量,在函数第一行的参数表中专门定义,用于接受实参传递过来的

– 96 –
第6章 函数

已知条件,使用方式与普通变量相同。而实参可以是变量、常量甚至是表达式,其作用是把
变量、常量的值或者表达式结果值传递给形参。
例 6-3 主函数中, y = expon( t , a ),实参是两个变量,函数调用时把变量 t 和 a 的值传
递给形参 x 和 n 。 如果想计算 3.54,就以常量做实参: 同样道理,
y = expon( 3.5 , 4 )。 y = expon( t ,
a+4
a+4 )是计算 t ,函数调用时,首先计算表达式 a+4,然后把结果值传递给形参 n。如果实参
是变量的话,它与对应的形参完全是两个变量,实参是主函数的变量,形参是函数体的变量,
两者可以同名,也可不同名。

6.2 函数调用

6.2.1 函数调用过程
C 语言对于执行带有函数的程序,首先是执行主函数 main(),一旦执行到对其他函数的
调用时,相应函数才被真正执行,而主函数将暂时停一停,等函数执行完后,将返回到主函
数,再从原来暂停的位置继续执行。对于 void 类型的函数,缺少 return 并不意味着函数不能
返回,一旦函数中所有语句都执行完,遇到函数最后的花括号,也将返回到主函数。
n
例 6-4 编写计算 ∑ i! 的程序。
i =1

分析:我们在例 6-1 中编写过求阶乘的函数,本例中我们通过调用 fact()函数,来计算阶


乘,然后求和。
源程序:
long fact ( int n )
{
int i ;
long res=1;
for (i=1; i<=n; i++)
res = res * i ;
return res ;
}
main()
{
long sum=0 ;
int i , n ;
scanf ("%d", &n) ;
for (i=1 ; i<= n ; i++ )
sum = sum + fact (i) ; /* 调用 fact()求 i!
,该调用将循环 n 次 */
printf ("%ld", sum ) ;
}
自定义函数一般写在主函数的前面,例 6-4 中把 fact()函数写在了 main()前面。但计算机

– 97 –
C 语言程序设计

在执行程序时,必须从 main()开始逐条执行。当执行到 sum = sum + fact (i) 时,main()会暂


时停一下,转而去执行 fact()。对函数 fact()来说,形参 n 首先会从实参 i 得到数值,然后经
for 循环计算出 n!,最后通过 return res 返回到主函数,由主函数把 fact()函数结果 i!加到
sum 上,继续 main()的执行。由于 sum = sum + fact (i)处于 main()的循环语句中,随着 i 从 1
递增到 n,fact()的调用将循环 n 次,这也意味着 main()会停顿 n 次。
在 C 语言中,main()也是一个函数,所有程序的执行都必须从 main()开始,然后再调用
别的函数。一旦被调用的函数执行结束,会自动回到 main()停顿的地方,继续 main()的执行。

6.2.2 函数调用形式
主函数调用其他函数(包括标准函数)有 3 种形式。

1.调用语句

函数调用本身是一条语句。如:
printf ("%ld", sum ) ;
它通常用于 void 类型函数的调用。

2.表达式调用

在表达式运算中调用函数。如:
sum = sum + fact (i) ;

3.作函数参数

函数调用出现在另一个函数的实参中。如在例 6-3 中调用函数计算 t a,我们写成:


y = expon( t , a ) ;
printf("%f \n" , y) ;
这两条语句可以合起来写成一条语句:
printf("%f \n" , expon(t, a)) ;
其中函数调用 expon(t, a)作为标准函数 printf()的一个参数,执行过程将首先计算 expon(t, a),
然后把结果值作为 printf()的参数进行输出。函数调用作参数有时还能扩展函数的功能。
例 6-5 编写求两个数中较大值的函数 max(),并用它来求 3 个值中的最大值。
分析:如果我们把这个问题看作为 a、b、c 三个人比高矮,读者就很容易想象出找出最
高个的过程。首先 a 和 b 比,高者再与 c 比,便能找出最高者。
源程序:
int max(int x, int y)
{
return x>y ? x : y ; /* 先计算条件表达式,再把结果返回 */
}
main()
{
int a, b, c, t ;
scanf ("%d%d%d" , &a, &b, &c) ;

– 98 –
第6章 函数

t=max(c , max(a, b) ) ; /* 先调用 max()选出 a、b 中的大数,


与 c 一起再通过 max()函数选出最大数 */
printf("max=%d\n" , t);
}
说明:函数调用 max(a, b)作为实参进一步调用 max(),使求两个数最大值的函数 max(),
功能扩展到求 3 个数的最大值。
例 6-6 求 500 以内的全部素数,并以每行 10 个进行输出,其中判断素数要求用函数实
现。
源程序:
#include <math.h> /* 使用平方根函数,需要包含数学库 */
int prime(int x) /* 函数判断 x 是否素数,若 x 是素数则返回 1;否则返回 0 */
{
int i, r=1 ;
for ( i=2; i<=sqrt(x); i++)
if (x%i ==0) {
r=0 ;
break ;
}
return r ;
}
void prt(int x, int n)
/* x 是要输出的素数,n 是已输出素数的个数,以此控制输出行位置 */
{
printf(" %6d", x); /* 素数输出 */
if (n%10==0) printf("\n") ; /* 保证输出素数时每行 10 个 */
}
main( )
{
int m, i , n=0 ; /* n 用于控制输出格式,每十个数据一行 */
for (m=2; m<=500; m++)
if (prime(m) ) { /* 函数调用的结果作判断条件 */
n++ ;
prt(m, n) ; /* 调用输出函数 */
}
}
说明:本例中定义了两个函数 prime()和 prt(),分别用来完成素数判断和结果输出,使得
主函数非常简洁,看上去完全是一种算法步骤,大大增强了程序的可读性。两个函数各自独
立,完成自己的功能,由主函数按顺序调用,但主函数对 prime()函数的调用形式比较特别。
if 本身的条件可以是任意表达式,包括函数调用,而函数调用最终是以返回的结果来体现的,
因此该返回值若是非 0 表示条件成立,若为 0 表示条件不成立,if 据此进行判断。

– 99 –
C 语言程序设计

6.2.3 参数传递
我们知道在函数调用中,有两类参数:形式参数(简称形参)和实际参数(简称实参) 。
实参由主调函数(如 main())提供,它的作用是给定函数运算的已知条件。形参定义在被调
函数中,用于接收实参给定的已知条件,并作用于函数的运算过程。实参把已知条件数据告
诉形参的过程称为参数传递。
C 语言规定参数传递过程中,实参把数值复制给形参。实参可以是变量或常量,把变量
或常量的数值复制给形参;实参也可以是表达式,计算机先计算该表达式,然后把结果值复
制给形参。千万不能认为实参把表达式的式子传递过去,因为形参是一个变量,只能保存一
个值。当变量作实参时,参数传递是单向的,只允许实参把数值复制给形参,形参值在函数
中的改变不会反过来影响实参。
例 6-7 编写交换两个变量值的函数 swap()。
分析:交换两个变量值需要引入第三个变量。函数 swap()并不作具体计算,只是完成一
段功能,不需要返回明确结果值,因此函数类型定义为 void。
源程序:
void swap(int x, int y)
{
int t ;
t=x ; x =y ; y=x ;
}
main()
{
int a=1, b=2 ;
swap(a, b) ;
printf("a=%d, b=%d \n" , a , b );
}
运行结果如下:
a=1,b=2
说明:函数 swap()并没有完成交换两个变量值的功能,原因在于参数传递是单向的,形
参的变化不会影响实参。如果把 swap()的形参名改为 a 和 b 是否能完成交换两个变量呢?我
们说形参与实参是两个不同的变量,即使同名,也无法把两个变量合而为一,就好像两个同
名同姓的人无法成为一个人一样。
到目前为止,我们还无法编写交换两个变量值的函数,这个问题通过指针可以得到较好
的解决。通过这个不成功的例子,读者可以进一步理解函数参数传递是值传递,它是单向的,
形参得到的是实参值的副本,形参(副本)的变化不可能影响实参(原件)。
当多个实参都是表达式时,计算机将依次计算出来然后进行传递。但这里存在计算顺序
的问题,大部分 C 语言系统都是先计算后面的实参,再计算前面的实参。如果各实参表达式
之间有关联,需要注意这个问题,如:
printf ("%d , %d " , i , ++ i ) ;
若 i=1 的话,将输出“2,2”,而不是“1,2”
。读者在上机时,最好了解一下所用 C 语言系

– 100 –
第6章 函数

统的计算顺序。

6.2.4 函数结果返回
从函数的功效来说,有两种类型:一种是完成指定工作,没有确定的运算结果需返回给
主调函数,通常用于实现结构化程序设计中的过程模块,函数类型用 void 指定;另一种是完
成确定的运算,有一个明确的运算结果返回给主调函数,函数类型与运算结果类型有关。这
里就有结果返回的函数,下面介绍结果返回的有关问题。
函数结果返回的形式如下:
return 表达式; 或 return (表达式);
return 语句的作用有两个:一是结束函数的运行;二是带着表达式的运算结果返回主调函数。
例 6-8 编写比较两个变量值是否相等的函数。
源程序 1:
int equal (int x, int y)
{
if (x == y ) return 1 ;
else return 0;
}
equal() 中出现了两个 return 语句,执行时由 if 来判断,只执行其中的一个,它们的作
用相同——结束函数、回送结果。如果 return 语句后面还有其他语句,将不会被执行。equal()
中 return 语句返回的是常量 1 或 0。
源程序 2:
int equal 1 (int x, int y)
{
return x == y ;
}
说明:当 x 和 y 相等时,关系表达式 x == y 成立,返回 1;当 x ≠ y 时,关系表达式 x == y
不成立,返回 0。所以 equal 1()的作用与 equal()完全相同。equal 1() 的 return 语句返回的是
表达式,计算机将先根据 x 和 y 的值计算 x == y,然后把关系表达式的值返回。调用它的主
函数可以写成:
main()
{
……
if ( equal(a,b) ) /* 函数返回的就是比较结果,=1 表示相等 */
printf(" a=b" ) ;
else
printf(" a!=b" ) ;
……
}
其中,对 equal( )的使用,与例 6-6 的素数判断形式一致。
一般情况下,return 返回的结果类型应与函数类型一致,如果两者不一致的话,计算机
以函数类型为准。例如:
– 101 –
C 语言程序设计

int value (float x )


{
return x;
}
将返回整型值。
对 return 来说要引起注意的一点是,return 只能返回一个值,而不能同时返回几个值。
因此现在要编写求一元二次方程根的函数还有困难,因为计算出来的两个根 x1、x2 无法通过
return 语句返回。
return x1, x2 ;
从语法上说是错误的,也不可以写成:
return x1 ;
return x2 ;
因为执行了 return x1 后,函数就结束了,将返回到主调函数,不可能再去执行 return x2。
所以对于上面 swap()函数,也不可能通过增加 return 语句来实现。后面我们会通过指针来解
决多结果返回的问题。

6.2.5 函数的嵌套调用
前面的函数例子解决的都是简单问题,由主函数调用某函数实现。对于复杂问题,可能
出现主函数调用某函数,该函数又进一步调用其他函数的情况。在一个被调函数中再调用其
他函数称为函数的嵌套调用。在使用结构化程序设计方法解决复杂的问题时,通常会把大问
题分解成若干小问题,小问题再进一步分解成若干更小的问题。写成程序时,用 main()解决
整个问题,它调用解决小问题的函数,而这些函数又进一步调用解决更小问题的函数,从而
形成函数的嵌套调用,如图 6-1 所示。

main( )

函数 1 函数 2 …… 函数 m

函数 11 函数 1q 函数 m1 …… 函数 mp

图 6-1 结构化程序的构成

n
例 6-9 用函数嵌套调用计算 ∑i! 。
i=1

分析:我们在例 6-1 中编写过求阶乘的函数,本例中我们将编写求阶乘之和的函数 cacl(),


由它进一步调用计算阶乘函数 fact()。主函数的工作是输入 n,然后调用 cacl()计算阶乘之和,
最后输出结果。
源程序:

– 102 –
第6章 函数

long fact ( int k ) /* 计算 k! */


{
int i ;
long res=1;
for (i=1; i<=k ; i++)
res = res * i ;
return res ;
}
n
long cacl ( int n ) /* 计算 ∑i! */
i=1

{
long sum=0 ;
int i ;
for (i=1 ; i<= n ; i++ )
sum = sum + fact (i) ; /* 调用 fact()求 i! */
return sum ;
}
main()
{
int n ; long t ;
scanf ("%d" , &n) ; /* 输入 */
t = cacl (n ) ; /* 计算 */
printf ("result = %ld", t ) ; /* 输出 */
}
如果主函数中输入输出过程比较复杂,也可以分别定义输入函数和输出函数,如例 6-6
中就定义过输出函数 prt(),这样主函数可以只包含输入、计算和输出 3 个函数的调用,非常
简洁。
例 6-9 程序的执行过程如图 6-2 所示。
main() long cacl()
{ 调用① {
long fact()
…… ……
调用② {
t = cacl (n ) ; for (……)
共n次 ……
…… sum = sum + fact (i) ;
return res ;
} 返回② return sum ; 返回①
}
}

图 6-2 函数嵌套调用及返回过程

程序执行首先从 main()开始,遇到 cacl()调用,就暂停 main()的执行,计算机转到 cacl()


函数去运行,它进一步调用了 fact(),使得 cacl()函数的运行又被暂停,等 fact()结束、返回后,
cacl()才继续运行,这样经过 n 次调用与返回,最后再从 cacl()返回到 main()。请读者注意函
数调用与返回的顺序,最先进行的调用①,要最后才返回(返回②) ,而后展开的调用(调用
– 103 –
C 语言程序设计

②)将先返回(返回①)。
该例子从某种角度说并不复杂,可以只用 main()函数实现,而不需要函数嵌套调用,但
我们这里的目的是通过例子说明嵌套调用用法。

6.2.6 函数的声明
对于变量的使用,大家都非常清楚,要先定义后使用。对于函数来说,调用之前同样需
要先定义或说明。我们把书写函数的具体过程称为函数定义。前面我们曾要求读者把自定义
函数写在主函数前面,其目的就是实现函数的先定义后使用。对于函数嵌套情况,我们建议
把被调函数写在主调函数前面,这样既实现了函数定义又增加程序的可读性,使程序结构更
清晰。如例 6-9 中,fact()最先写,然后写 cacl(),最后才是 main()。被调函数在前,主调函数
在后。
C 语言也允许先写主调函数,后写被调函数,但为了运行正确,需要在主调函数中对被
调函数予以声明,其目的主要是说明函数的类型和参数的情况,以保证程序编译时对函数调
用是否正确做出判断。函数声明的一般格式如下:
函数类型 函数名(参数表) ;
只写函数定义中的第一行,并以分号结束。
例 6-9 可以写成:
main()
{
int n ; long t ;
long cacl(int n) ; /* 函数声明 */
scanf ("%d" , &n) ; /* 输入 */
t = cacl (n ) ; /* 计算 */
printf ("result = %ld", t ) ; /* 输出 */
}
long cacl ( int n ) /* 计算 */
{
long sum=0 ;
int i ;
long fact ( int k ); /* 函数声明 */
for (i=1 ; i<= n ; i++ )
sum = sum + fact (i) ; /* 调用 fact()求 i! */
return sum ;
}
long fact ( int k ) /* 计算 k! */
{
……
}
main()中调用的 cacl()虽然还没有看到具体实现,但经过了声明,计算机知道 cacl()是 long
型函数,参数为一个整型量,具体函数体后面将会定义,程序编译就能顺利通过。由于 main()
中没有直接调用 fact(),可以不必对 fact()进行声明。某函数内对被调函数 f()的声明,不对其
– 104 –
第6章 函数

他函数起作用,其他函数若也想调用函数 f(),仍需要自己声明。所有的函数声明也可以放到
程序最前面(main()的前面) ,使得后面所有的函数调用都不必再一一声明。
如果被调函数书写在后面而不作说明,计算机将默认该函数为整型。当被调函数恰好定
义成整型,程序将正确执行;若被调函数定义成其他类型的话,程序编译会出错,这是因为
计算机所默认的整型,与实际的定义不符,两者矛盾造成出错。因此建议读者不管函数是否
整型,要么先定义要么先说明,不要为几个字偷懒。养成一个良好的编程习惯非常重要,这
可以最大限度减少编程的潜在错误,提高程序的质量。
上面所述的声明是对自定义函数而言。使用标准函数时,一般通过使用文件包含 include
来进行的。例如,使用输入输出库函数需要进行如下声明:
#include "stdio.h"
常用的输入输出库函数有 scanf()、printf()、getchar()和 putchar()等。
再如,使用数学计算库函数需要进行如下声明:
#include "math.h"
常用的数学库函数有 sqrt(x)(求 x ) 、fabs(x)(求|x|)、exp(x)(求 ex)、pow(x,y)(求 x y)、
log(x)(求 log e x)、log10(x)(求 log10x)等。
使用 include 时,前面要加#,行尾不能有分号,因为文件包含不是 C 语言的真正语句,
这在本章后面会进一步介绍。附录 E 有 C 语言常用的标准库函数的汇总。

6.3 递归函数

6.3.1 递归函数基本概念
如果函数 A 调用函数 B,函数 B 再调用函数 C,一个调一个地嵌套下去,构成了函数的
嵌套调用。具有嵌套调用函数的程序,需要分别定义多个不同的函数体,每个函数体完成不
同的功能,它们合起来解决复杂的问题。
一个函数除了可以调用其他函数外,C 语言还支持函数直接或间接调用自己,这种函数
自己调用自己的形式称为函数的递归调用,带有递归调用的函数称为递归函数,如图 6-3 所
示。
下面我们通过例子来说明递归调用的方法。
例 6-10 用递归函数实现求 n!。
分析:
(1)递推法
在学习循环时计算 n!采用的就是递推法:
n!= 1*2*3*…*n
通过循环实现:
for (i=1 ; i<n; i++)
res = res * i

– 105 –
C 语言程序设计

函数直接递归调用 函数间接递归调用
int f(int x) int f(int x) int g(int x)
{ { {
int y ; int y ; int z ;
…… …… ……
y = f (x) y = g (x) z = f (x)
…… …… ……
return y ; return y ; return z ;
} } }

图 6-3 函数递归调用的两种形式

(2)递归法
把 n!以递归方式进行定义:

n*(n-1)! 当 n>1
n!=
1 当 n=1 或 0

即求 n!可以在(n−1)!的基础上再乘上 n。如果把求 n!写成函数 fact(n)的话,则 fact (n)的


实现依赖于 fact(n−1)。
源程序:
long fact(int n)
{
long res ;
if (n==1 || n == 0)
res = 1 ;
else
res = n*fact(n-1) ; /* 函数递归调用 */
return res ;
}
main()
{
int n ;
scanf ("%d", &n) ;
printf ("%ld", fact (n) ) ;
}
说明:递归函数 fact( )中必须定义保存运算结果的变量 res:
res = n*fact(n−1)
然后通过 return res 返回 n!的结果。千万不能写成 fact(n) = n*fact(n−1),因为从赋值表达式
语法规则上说,赋值号左边必须是变量,fact(n)表示的是函数调用,它最终是数值(函数结
果),无法用于存储数据。这是初学者较容易犯的错误。
读者对 fact(n)定义也许会觉得不完整,因为 fact(n−1)还不知道,res 无法算出。这里需要
区分程序书写与程序执行,就像循环程序并不是把所有的循环体语句重复书写,只是给出执
行规律,具体执行由计算机去重复。递归函数同样给出的是执行规律,至于 fact(n−1)如何求
得,应由计算机按照给出的规律自行计算。
– 106 –
第6章 函数

下面看一下递归函数的执行过程,以帮助我们更好地理解递归函数。图 6-4 给出了计算


fact(4)的调用过程。数字① ~ ⑧是递归函数调用返回的顺序编号。首先 main()函数以 4 作参
数调用 fact()函数, fact(4)依赖于 fact(3)的值,所以必须先计算出 fact(3)才能求 fact(4)。当 fact(4)
递归调用自己计算 fact(3)时,fact(4)并未结束,而是暂时停一下,等算出 fact(3)后再继续计算
fact(4),这时计算机内部 main()、fact(4)和 fact(3) 三个函数同时被执行。fact(3)是 fact(4)的克
隆体,尽管程序代码、变量名相同,但属不同的函数体、不同参数、不同变量。这样依次递
归,当调用到 fact(1)时,同时有 5 个函数被运行着,各个克隆的 fact()函数均未结束,只有当
n=1,fact(1)=1,不必再继续递归调用下去。有了 fact(1)的确切值,就可以计算 fact(2),不断
返回,不断结束原来递归克隆的函数,最后可以计算出 fact(4),返回到主函数。
从实现过程上看,fact()函数不断调用自己,如果没有终结的话会发生死机,就像循
环没有结束条件会导致死循环那样。任何递归函数都必须包含条件,来判断是否要递归下
去,一旦结束条件成立,递归克隆应该不再继续,而以某个初始值作为函数结果,然后返
回,结束一个递归克隆函数体。通过一层层的返回,一层层地计算出 i!(i=1,2,…,n−1),
最终算出 n!。

main()
{ ……
printf ("%ld",fact(4));
…… fact(4)
} { ……
①调用 ②递归调用
res=4*fact(3)
……
}
⑧递归返回 fact(3)
{ ……
⑦递归返回 res=3*fact(2)
……
}
⑥递归返回

fact(2)
⑤递归返回 { …… ③递归调用
res=2*fact(1)
……
}
fact(1)
{ if (n==1)
res=1;
return res ;
} ④递归调用

图 6-4 fact()函数的调用返回过程

递归的实质是把问题简化成形式相同,但较简单一些的情况。程序中每经过一次递归,
问题得到一点简化,比如 n!的计算简化成对(n−1)!的计算,不断的简化下去,最终归结
到一个初始值,就不必再递归了。
– 107 –
C 语言程序设计

例 6-11 求下列递归程序的输出。
源程序:
# include <stdio.h>
long fib(int g)
{ switch(g){
case 0: return(0);
case 1:
case 2: return(2);
}
printf("g=%d,", g);
return ( fib(g-1) + fib(g-2) );
}
main()
{ long k;
k = fib(4);
printf("k=%ld\n", k);
}
说明:程序首先从 main()开始执行,调用 fib(4)。fib()函数是一个递归函数:
0 g=0
fib(g)= 2 g=1,2
fib(g-1) + fib(g-2) g>=3

在 fib()函数中,switch 语句中对 case1 的情况没有指定,将顺序执行下一种值的情况,


即 return 2。
fib(4)= fib(3)+ fib(2)=6

fib(2)+fib(1) = 4

2 2

运行结果如下:
g=4, g=3, k=6

6.3.2 递归程序设计
从递归函数的程序编写角度看,必须紧紧抓住两个关键点。
(1)递归出口:递归的结束条件,到何时不再递归调用下去。
(2)递归式子:递归的表达式,如 fact(n)=n*fact(n−1)。
对于递归函数可以从数学归纳法来理解。用数学归纳法证明问题,首先证明初值成立,
然后假设 n 时成立,再证明 n+1 时也成立,问题即可得到证明。这里的初值验证就像是递归
的出口,从 n 到 n+1 的证明相当于找递归式子。
例 6-10 求阶乘的例子,该问题本身可以用循环实现,读者并没有感觉到递归有什么过人

– 108 –
第6章 函数

之处。下面我们再看一个非用递归不可的例子。
例 6-12 计算 Ackermenn 函数 ack(m,n),其定义如下:

n+1 m=0
ack(m,n)= ack(m–1,1) n=0 && m>0
ack(m–1, ack(m,n–1) ) m>0 && n>0

分析:定义中第一种情况是递归出口,后两种是递归式子,但第二个递归式子中参数为
递归调用。对于 ack(m,n)函数,若想理出个头绪来通过循环实现,是根本不可能的。惟一的
办法是遵循递归定义,采用递归函数来实现。
源程序:
int ack(int m,int n) /* 调用前提: m>=0, n>=0 */
{ int temp ;
if (m==0) temp = n+1 ;
else
if (n==0) temp = ack(m-1,1);
else
temp = ack(m-1, ack(m,n-1));
return temp ;
}
编写该递归函数非常轻松,因为它与题目定义完全一致。所以说,找出递归出口和递归
式子两个关键点,递归程序设计就可迎刃而解。
在递归程序设计中,读者千万不要钻到实现细节上去,如计算 ack(3,3)时,它被简化成
ack(2, ack(3, 2) ),而 ack(2, ack(3, 2) )又该如何进一步简化下去,因为该问题本身关系复杂,
很难理出头绪。我们编写的程序只给出运算规律,具体实现细节应该让计算机去算。例 6-10
之所以介绍实现过程中函数调用与返回情况,主要是在初学时,帮助读者更好地理解递归函
数。在以后的复杂递归问题中,找出递归式子是关键,而不要陷入实现细节的泥沼中。当然
像例 6-12 的程序必须要能读得懂。
递归程序设计是一个非常有用的工具,可以解决一些用其他方法很难解决的问题。如果
读者进一步学习计算机的其他后续课程的话,递归是一种常用手段。但递归程序设计的技巧
性要求比较高,对于一个具体问题,要想归纳出递归式子有时是很困难的,并不是每个问题
都像 ack()函数那样直截了当。
例 6-13 汉诺(Hanoi)塔问题:古代某寺庙有一个梵塔,塔内有 3 个座 A、B 和 C,座
A 上放着 64 个大小不等的盘,其中大盘在下,小盘在上。有一个和尚想把这 64 个盘从座 A
搬到座 C,但一次只能搬一个盘,搬动的盘只允许放在其他两个座上,且大盘不能压在小盘
上。现要求用程序模拟该过程,并输出搬动步骤。
分析:读者不妨拿 3 个盘子来模拟一下,看看是否能找出某种重复性的规律,以便通过
循环来实现。不幸的是虽然操作步骤类型不多,也有重复的要求,但重复步骤不尽相同,无
明确规律,循环无法实现。那么递归又该如何实现呢?我们从找出递归两个要点出发。
(1)递归出口:如果只有一个盘子,可直接搬动。
(2)递归式子:如何把 64 个盘子简化成 63 个盘子问题?假设要搬 64 个盘子的和尚是寺
– 109 –
C 语言程序设计

庙的方丈,它可以命令和尚甲,把上面 63 个盘子从座 A 先搬到座 B,然后方丈自己只需把


最大号盘,从座 A 搬到座 C,再命令和尚甲把 63 个盘子从座 B 搬到座 C。我们把上述工作
归纳成 3 个递归步骤:
① n−1 个盘从座 A 搬到座 B;
② 第 n 号盘从座 A 搬到座 C;
③ n−1 个盘从座 B 搬到座 C。
那 63 个盘又如何搬呢?如果和尚甲能命令和尚乙去搬其中的 62 个盘,对于和尚甲来说
也非常轻松。从编程的角度,我们曾讲过不要钻到实现细节上去,不必关心 63 个盘子如何搬,
62 个盘子如何搬,……我们只需做一个轻松的方丈,让计算机做小和尚。
程序算法:
hanio(n 个盘,A→C)
{ if (n== 1)
直接把盘子 A→C
else
{ hanio(n-1 个盘,A→B);
把 n 号盘子 A→C;
hanio(n-1 个盘,B→C);
}
}
按照搬动规则,必须有 3 个座才能完成搬动,一个座是搬动源,一个座是搬动目的地,
另一个座是中间过渡使用的。在搬动过程中,3 个座的作用是动态变化的,因此在函数中 3
个座必须指定,令其作为函数的参数。具体搬动步骤在程序中只能通过信息显示来仿真,用
printf()函数实现。
源程序:
void hanio(int n, char a, char b, char c)
/* 搬动 n 个盘,从 a 到 b,c 为中间过渡 */
{ if (n== 1)
printf("%c-->%c\n", a, b) ;
else
{ hanio(n-1, a, c, b);
printf("%c-->%c\n", a, b) ;
hanio(n-1, c, b, a);
}
}
main()
{
int n;
scanf("%d", &n) ;
printf("the step for %d disk are:\n",n);
hanio(n, ’a’, ’c’, ’b’) ;
}

– 110 –
第6章 函数

这样,一个非常难解决的问题让递归轻而易举地解决了,程序很简短。当输入 3 个盘时,
运行结果如下:
the step for 3 disk are:
a-->c
a-->b
c-->b
a-->c
b-->a
b-->c
a-->c
说明:若输入 4 个盘,将搬动 15 次。不难证明,n 个盘子将搬动 2n −1 次。若 n=64 时,
搬动数约为 1019 次,如果和尚们每天 24 小时不间断地搬,并假设每秒钟搬一次的话,大约
需要 1011 年,这比地球的年龄还要长,即使计算机每秒搬 109 次,也要 100 年。读者不妨让
计算机搬 20 个盘子,看看要耗时多少。
为了让读者更好地学习递归函数,下面再举一个递归式子较难归纳的例子,目的是希望
读者能进一步明确从递归编程两个要点出发的过程。
例 6-14 用递归方法实现对一个整数的逆序输出。
分析:这个例子在学习循环时作为例子已经介绍过,这里再以递归的方式来解决,主要
学习分析过程,即如何针对一个实际问题,分析它的递归关系。从本例来看,似乎并无明显
的递归关系。一般情况的递归总是把 n 的问题简化成 n−1 的问题,而在本例中仅仅处理一个
数,它的“n”指的是什么?我们不妨先考虑递归程序的另一个实现要点——递归出口,即什
么样的数求逆序最简单?答案很明显,一位数的逆序就是该数本身。对于不是一位数,能否
可以逐步转化最后变成一位数。因此递归的过程应在位数上做文章,即 n 位数的逆序简化成
n−1 位数的逆序,先输出最低位(x%10) ,对高 n−1(x/10)位再完成逆序处理,一旦简化成
一位数,递归结束。假定函数 f(x)完成对 x 的逆序,则
递归出口:
当 x<=9,输出 x
递归公式:
当 x>=10,f(x) = 输出 x%10 + f(x/10)
源程序:
void f(int x)
{
if (x<10) printf( "%d", x) ;
else {
printf("%d", x%10) ;
f(x/10) ;
}
}
main( )
{ int a;
scanf("%d", &a);
– 111 –
C 语言程序设计

f(a) ;
}

6.4 变量与函数

变量是程序中用于保存数据的工作单元,单元中的数据随着程序的运行会发生改变。到
目前为止,我们所使用的变量是随着定义而生成的,随着函数结束而消亡的,它与函数紧密
相连,使用方式非常单一。本节将介绍变量在多函数中的使用方式,包括全局变量、静态变
量等。

6.4.1 局部变量和全局变量
前面使用的变量都是定义在函数内部,它们有效的使用范围被局限在所在的函数内。例
如例 6-9 求阶乘之和,main()函数中定义的变量 n 和 t,其使用范围局限于该函数内部,同样
calc()函数中定义的变量 sum 只能在该函数中使用,该结果要通过 return 语句才能回送给主函
数,而不能由主函数来直接使用 sum 变量。C 语言中把定义在函数内部的变量称为局部变量,
局部变量的有效作用范围局限于所在的函数内部。形参就是局部变量。
使用局部变量可以避免各函数之间的变量相互干扰,尤其是同名变量。在例 6-9 中,main()
函数有局部变量 n(实参) ,cacl()函数也有局部变量 n(形参),由于它们是两个不同函数的
局部变量,尽管同名,但它们是两个不同的变量实体,有各自的存储单元和使用范围,两者
不会相互干扰,形参 n 的改变不会影响实参 n 的值。C 语言的这个特性在实现结构化程序设
计中非常有用,它可以保证每一个函数的独立性,减小程序设计的复杂性。
除了作用于函数的局部变量外,C 语言还允许定义作用于复合语句中的局部变量,其有
效使用范围当然也被局限于复合语句内。
例 6-15 在复合语句中定义局部变量。
源程序:
main()
{ int a ;
a=1 ;
{
int b=2 ;
b = a+b ; 复合语句:局部变量 b 的作用范围
a = a+b ;
}
printf("%d " , a );
}
变量 b 只能在复合语句中使用。复合语句中的局部变量一般用作小范围内的临时变量。
局部变量的定义一般要求在函数的一开始或复合语句的开始处,而不能在函数中任一可
执行语句的后面。下列定义将出错:
main()

– 112 –
第6章 函数

{ int a ;
a=1 ;
int c=3 ; /* 出错,应放到 a=1 语句的前面 */
{
int b=2 ;
……
}
}
局部变量避免了不同函数间的相互干扰,增加了函数的独立性,但程序设计有时还要考
虑不同函数之间的数据交流及各函数的某些统一设置。当一些变量需要在多个函数上共同使
用时,参数传递虽然是一个办法,但必须通过函数调用才能实现,并且函数只能返回一个结
果,这会使程序设计受到很大的限制。为了解决多个函数间的变量共用,C 语言允许定义全
局变量。所谓全局变量是指定义在函数外而不属于任意函数的变量。全局变量的作用范围是
从定义开始到程序所在文件的结束,它对作用范围内所有的函数都起作用。
全局变量的定义格式与局部变量完全一致,只是定义位置不同而已,它既可以定义在程
序的开头,也可以定义在两个函数的中间或程序尾部,只要在函数外部即可。一般情况下把
全局变量定义在程序的最前面,即第一个函数的前面。
例 6-16 全局变量定义。
源程序:
#include "stdio.h"
int x ; /* 定义全局变量 */
main()
{ int a ;
a=1 ;
x=a ; /* 全局变量赋值 */
{ /* 复合语句 */
int b=2 ;
b = a+b ;
x = x+b ; /* 全局变量运算 */
}
printf("%d %d" , a, x );
}
运行结果如下:
1 4
由于全局变量和局部变量的作用范围不同,因此 C 语言允许它们同名。当两者同名时,
在对应的函数中全局变量不起作用,而由局部变量起作用。对于其他不存在同名变量的函数,
全局变量仍然有效。同样对于重名的函数局部变量与复合语句的局部变量,以复合语句为准。
例 6-17 全局变量定义。
源程序:
全局变量 x 作用范围
#include "stdio.h"
int x ; /* 定义全局变量 */

– 113 –
C 语言程序设计

int f( )
{
int x=4 ; /* x 为局部变量,全局变量 x 不起作用 */
return x ;
}
main()
{ int a ;
a=1 ;
x=a ; /* 全局变量赋值 */
a=f() ; /* a=4,全局变量 x 仍为 1 */
{
int b=2 ;
b = a+b ; 全局变量 x 作用范围
x = x+b ; /* 全局变量运算 */
}
printf("%d %d" , a, x );
}
运行结果如下:
4 7
读者可能会觉得全局变量使用起来比局部变量自由度大,更方便。一旦定义,所有函数
都可直接使用,连函数参数都可省略,甚至函数返回结果个数也不受限制,不用 return 语句,
直接靠全局变量回送。从表面上看,全局变量确实能实现这些要求,但对于规模较大的程序,
过多使用全局变量会带来副作用,导致各函数间出现相互干扰。如果整个程序是由多个人合
作开发,各人都按自己的想法使用全局变量,相互的干扰可能会更严重。如果我们把变量比
喻成抗菌药的话,显然不管病情大小,一律吃最高档的抗菌药,效果虽好,但带来的副作用
是不言而喻的,只有对症下药才是最科学的。因此在变量使用中,应尽量使用局部变量,从
某个角度看使用似乎受到了限制,但从另一个角度看,它避免了不同函数间的相互干扰,没
有副作用。
从结构化程序设计角度说,全局变量会增加程序模块的关联性(耦合性),从而影响程序
的质量。使用局部变量能充分保证函数的完整性、独立性(内聚性),使函数成为一个独立的
封闭体,除了通过参数传递与函数外部发生联系外,函数与外界是隔绝的。好的程序设计应
提高程序模块的内聚性,降低程序模块的耦合性。如果把整个程序看作是一座大厦,则函数
是大厦的建筑构件,显然每一个建筑构件最好都是独立制造的,并且在大厦建造过程中,一
个建筑构件最好只与相邻构件进行连接,尽量避免在两个相距遥远的构件间发生连接。
由此可见,全局变量只能作为一种特殊手段,在特殊情况下用于多个函数之间的数据交
流。而一般情况下,应尽量使用局部变量和函数参数。
例 6-18 王婆卖瓜:为王婆设计一个帮助她在卖瓜时记账的程序,要求每卖一次瓜要记
下个数与重量,以统计所卖出瓜的总个数与总重量。另外需要说明的是,王婆允许退瓜。
分析:我们为卖瓜与退瓜各设计一个函数 sale()和 back(),由于瓜的总个数与总重量在两
个函数中都要用到,并且都将发生改变,但函数无法通过 return 返回两个变化量。其实瓜的

– 114 –
第6章 函数

总个数与总重量是表示总体情况的变量,不应该是某个函数的局部变量,所以可将它们设计
成全局变量,作为两个函数间的数据交流手段。另外把主函数写成菜单的形式,按字符’s’选
择卖瓜,按’b’进行退瓜,按’q’退出程序。
源程序:
#include <stdio.h>
float total_weight=0; /* 全局变量:瓜的总重量 */
int total_number=0; /* 全局变量:瓜的总个数 */
void sale(int number, float weight)
/* 卖瓜函数,参数 number 为一次所卖个数,weight 为一次所卖重量 */
{
total_number += number ; /* 改变全局变量 */
total_weight += weight ; /* 全局变量不需 return */
}
void back(int number, float weight)
/* 退瓜函数,参数 number 为退瓜个数,weight 为退瓜重量 */
{
total_number -= number ; /* 改变全局变量 */
total_weight -= weight ;
}
main()
{
char c;
float wt ;
int num ;
while(1) /* 无限循环,由操作命令 q 结束循环 */
{
printf ("\n\n\t \t s sale \t b back \t q quit \n") ; /* 操作命令提示 */
switch(c = getchar())
{
case ’ b’: printf("please input number : ");
scanf("%d" , &num) ; /* 输入退瓜个数 */
printf("please input weight : ");
scanf("%f" , &wt) ; /* 输入退瓜重量 */
back(num, wt ) ;
break ;
case ’s’: printf("please input number : ");
scanf("%d" , &num) ; /* 输入卖瓜个数 */
printf("please input weight : ");
scanf("%f" , &wt) ; /* 输入卖瓜重量 */
sale(num, wt ) ;
break ;
case ’ q’: exit(0) ; /* 结束主函数 */
}
– 115 –
C 语言程序设计

c = getchar(); /* 字符输入时去掉上一次的回车 */
printf ("Sale : total_number = %d total_weight = %f \n", total_number, total_weight ) ;
}
}

6.4.2 变量生命周期和静态局部变量
冯・诺依曼型计算机的核心思想是程序存储原理,即先把要运行的程序装载到计算机内
存中,然后由计算机自动执行。对 C 语言程序来说,内存分成两块区域:程序区和数据区,
分别存放 C 语言程序代码和变量数据,所有类型变量的存储单元都集中在数据区中。
对于一般程序来说,计算机都是从主函数开始运行,使得 main()函数中所有的局部变量,
一开始就在内存数据区分配了存储单元。而其他函数在被调用之前,其局部变量并未分配相
应存储单元,即它在内存中是不存在的,只有调用了某函数,其形参和局部变量经过定义,
内存中才分配相应存储单元。一旦函数运行结束,返回主调函数后,其定义的所有形参和局
部变量将不复存在,相应的存储单元由系统回收。根据这种特性,我们把局部变量称为自动
变量,即进入函数执行时,由系统为局部变量自动分配存储单元,一旦该函数运行结束(不
一定是整个程序运行结束) ,所有局部变量的单元又由系统自动回收。变量从定义开始分配存
储单元,到函数结束存储单元被回收,整个过程称为变量生命周期。
自动变量定义格式如下:
auto 类型名 变量表 ;
例如:
auto int x, y ;
在自动变量定义时,auto 可以省略,其形式与以前定义的普通变量完全相同。也就是说,前
面我们定义的局部变量都是自动变量。
当 main()函数调用其他函数时,由于它还未运行结束,其局部变量仍然存在,但由于变
量的作用范围,使得 main()函数中的局部变量单元不能在其他函数中使用。只有回到主函数
后,那些局部变量才可继续使用。变量的作用范围和生命周期是两个不同概念,请读者区分
清楚。
对于递归函数,函数体自己调用自己,经多次递归后,同一段程序代码会有多个克隆体
在运行。同时,函数的自动变量(局部变量和形参)也相应存在多个克隆体,每一个变量克
隆体仅属于它所在的函数,对其他递归展开的函数克隆体无任何作用,这是因为它们有自己
的克隆变量。例如 fact()函数计算,如图 6-5 所示。

fact(4) = 4* fact(3)
3*fact(2)
2*fact(1)
fact(1)=1

图 6-5 求 4!函数调用过程

当计算 fact(1)时,有 4 个 fact()函数克隆体同时打开运行,虽然名字都为 fact,但实体不

– 116 –
第6章 函数

同,每个 fact()函数都有自己的自动变量 n 和 res,相互之间互不干扰,每个变量的作用范围


只在所在的克隆函数内。
对于全局变量,由于它和具体函数无关,从程序执行的开始到整个程序的结束,全局变
量都有效,对应的存储单元始终保持。为了与自动变量加以区别,C 语言的数据区进一步分
成动态存储区和静态存储区,它们的管理方式完全不同。动态存储区是使用堆栈来管理的,
为函数动态分配与回收存储单元。而静态存储区相对固定,管理较简单,它用于存放全局变
量和静态局部变量。C 语言存储分布如图 6-6 所示。

系统存储区 操作系统

程序区 C 程序代码
数 全局变量
静态存储区
用户存储区 据 静态局部变量
区 动态存储区 自动变量

图 6-6 C 语言程序存储分布

在静态存储区中,除了全局变量外,还有一种特殊的局部变量——静态局部变量。由于
它存放在静态存储区中,不会像普通局部变量那样因为函数的结束而被系统回收,它的生命
周期会持续到程序结束。一旦含有静态局部变量的函数被再次调用,则上一次函数调用后,
留在静态局部变量上的值仍然保存着,可供本次调用继续使用。
静态局部变量定义格式如下:
static 类型名 变量表;
例 6-19 用静态局部变量实现计算 n!。
源程序:
long fact(int n)
{
static long t=1 ; /* 静态局部变量 */
t=t*n ;
return t ;
}
main()
{
int i, n ;
long res ;
scanf("%d", &n) ;
for (i=1; i<=n; i++)
res=fact(i) ; /* fact()被循环调用 */
printf("%d!=%ld\n", n, res) ;
}
说明:函数 fact()中首先定义了静态长整型变量 t,且赋予初值 1,函数中没有循环,相
应的循环放在了 main()函数中,也就是说主函数调用了 fact()函数 n 次。res 的值只有经过第 n

– 117 –
C 语言程序设计

次循环才被确定,前 n−1 次的结果都会被后一次函数调用替换,但各次调用产生的中间结果


被保存在静态长整型变量 t 上,后一次调用在前一次 t 的基础上乘以新的 n。程序运行过程中
t 值变化如图 6-7 所示。静态局部变量只有当函数被多次调用时才会显示出其作用。请读者把
本例与例 6-4 对比一下,看看它们在函数计算上的不同。

i t(刚进入函数时) t(函数结束时)
1 1 1
2 1 2
3 2 6

  
n (n-1)! n!

图 6-7 例 6-18 中的静态局部变量变化过程

对于一般的自动变量定义,若没有赋初值的话,其存储单元中将是随机值。而对于静态
局部变量来说,如果定义时没有赋初值,系统将自动赋 0,并且赋初值只在函数第一次调用
时起作用,以后调用都使用前一次调用保留的值。这是因为静态局部变量的生命周期始于函
数的第一次调用,贯穿于整个程序。当函数第一次调用时,静态局部变量的内存单元得以分
配,赋以初值,而函数被再次调用时,此静态局部变量单元已然存在,计算机不会再次为其
分配单元,赋初值也就不可能再发生。静态局部变量受变量作用范围限制,不能用于其他函
数(包括主函数)。
静态局部变量和全局变量一样,属于变量的特殊用法,若没有静态保存的要求,不建议
使用静态局部变量。除了静态局部变量外,C 语言也有静态全局变量,它的作用与程序文件
结构有关,后面会作介绍。

6.4.3 寄存器变量和外部变量
变量从存储的类别上分共有 4 类,除了上一节介绍的自动变量和静态局部变量外,还有
寄存器变量和外部变量。
寄存器变量是把数据存储在计算机寄存器单元上。寄存器是指 CPU(中央处理器)内部
的数据存储单元,其数据存取速度比内存单元快得多。从寄存器变量本意上说,其目的是为
了加快程序运行速度,但在目前微机上使用的大多数 C 语言系统,并不真正支持寄存器变量
实现,而是把寄存器变量当作普通的自动变量,数据仍然存储在内存单元上。
寄存器变量只适用于整形,其定义格式如下:
register int 变量表;
例如:
register int x, y=1 ;
寄存器变量的使用方式与自动变量完全相同,由于一般使用中并无特殊效果,因此较少
使用。
对于全局变量来说,还有一种称为外部变量的形式。其使用有两种情况,一种是全局变
– 118 –
第6章 函数

量的使用位置先于该全局变量的定义,按照标准 C 的规定,需要在使用之前用外部变量来声
明;第二种情况是用于当程序由多个文件模块构成时的全局变量声明,这种情况将在后面介
绍。这里主要针对第一种情况进行介绍。
外部变量声明格式如下:
extern 变量名表 ;
外部变量只是起说明作用,告诉 C 编译器该变量后面会定义。它不分配存储单元,所对
应存储单元在变量定义处分配。
例 6-20 使用外部变量。
源程序:
main()
{ int a ;
extern x; /* 声明 x 为外部变量,具体定义在后面 */
a=1 ;
x=a ; /* 全局变量赋值,使用先于定义 */
x = x+a ; /* 全局变量运算 */
}
int x ; /* 定义全局变量,后于全局变量的使用 */
一般从使用角度,建议读者把全局变量定义到程序的开头(特殊用法除外)
,免得再用外
部变量来声明,同时也使得程序看起来更清晰。

6.5 程序模块结构

结构化程序设计方法是指导我们编写出具有良好结构程序的有效方法,本章的一开始就
介绍过,一个大程序最好由一组小函数构成,存放在.C 文件中,经过编译连接生成可执行代
码。如果程序规模很大,需要几个人合作完成的话,每一个人所编写的程序就会保存在自己
的.C 文件中。有时候为了避免一个文件过长,也会把程序分别保存为几个文件。这样一个大
程序会由几个文件组成,每一个文件又可能包含若干个函数。我们把保存有一部分程序的文
件称为程序文件模块。当大程序分成若干文件模块后,可以对各文件模块分别编译,然后把
通过编译的文件模块再合起来,连接生成可执行程序。这就需要解决一个问题:如何把若干
程序文件模块连接成一个完整的可执行程序?本节将主要解决这个问题,同时还要解决不同
程序文件模块合起来后函数与变量的关系。

6.5.1 文件包含
当一个 C 语言程序由多个文件模块组成时,整个程序中只允许有一个 main()函数,千万
不可在每一个文件模块中都写一个主函数。程序的运行从 main()开始,为了能调用写在其他
文件模块中的函数,文件包含是一个有效的解决方法。
文件包含的格式如下:
#include <需包含的文件名>

– 119 –
C 语言程序设计

或 #include "需包含的文件名"
文件包含(include)对我们来说并不是新的内容。以前编写程序时,如果用到字符读写
函数 getchar()和 putchar(),或平方根函数 sqrt()等,需要在程序头写上:
#include <stdio.h>
或 #include <math.h>
文件包含的作用是把指定的文件模块内容插入到#include 所在的位置,当程序编译连接时,
系统会把所有#include 指定的文件拼接生成可执行代码。文件包含必须以#开头,表示这是编译
预处理命令。#include 将在程序编译时起作用,把指定的文件模块包含进来,当经过连接生成
可执行代码后,便不再存在。因此#include 不是真正的 C 语句,行尾不能用分号结束。
例 6-21 假定我们把例 6-9 求阶乘之和的 3 个函数分别存储在 3 个.C 文件中,要求通过
文件包含把它们连接起来。
图 6-8 的 F3.C 文件模块中包含了 F1.C 和 F2.C 两个文件模块。经程序编译连接后,F1.C
和 F2.C 的内容与 F3.C 一起生成 F3.exe 可执行程序,并且 fact()函数位置在最前面,cacl()函
数居中,main()函数最后,这样在编写程序模块时,无需再对函数进行声明。fact()函数和 cacl()
函数在程序中的位置与书写顺序一致。一般被调函数文件模块的包含位置,要写在主调函数
文件模块的前面。

文件模块名:F1.C
long fact ( int k ) 编译连接后包含的内容
{
long fact ( int k )
int i ;
{
long res=1;
for (i=1; i<=k ; i++) ……
res = res * i ; 文件模块名:F3.C return res ;
return res ; #include "F1.C" }
} #include "F2.C" long cacl ( int n )
main() {
{ ……
int n ; long t ; return sum ;
scanf ("%d", &n) ; }
t = cacl (n ) ; main()
printf ("%ld", t ) ; {
文件模块名:F2.C
} int n ; long t ;
long cacl ( int n )
scanf ("%d", &n) ;
{
t = cacl (n ) ;
long sum=0 ;
printf ("%ld", t ) ;
int i ;
}
for (i=1 ; i<= n ; i++ )
sum = sum + fact (i) ;
return sum ;
}

图 6-8 用# include 连接多个文件模块

include 除了能实现把多个程序文件模块连接起来外,在实际编程中,还经常用于做统一
的定义或声明。我们前面使用函数 getchar()和 putchar(),或平方根函数 sqrt()等,需在程序头
写上:#include <stdio.h>或#include <math.h>,目的就是对使用输入输出标准库函数或数学
– 120 –
第6章 函数

库函数进行声明,以保证程序能正确调用系统库函数。
include 所包含的文件,其扩展名可以是.C,表示普通 C 语言程序;也可以是.h,表示 C
语言程序的头文件(header)。头文件除了可以存放一些声明外,还常用于存放符号常量,以
及后面会学到的宏定义、结构体和共用体等一些数据结构定义。尤其在多人合作时,基本的
数据结构大家都要用到,需要一起协商定义好并写成头文件,以后便能通过文件包含,方便
地被大家引用。C 语言系统中大量的定义与声明是以头文件形式提供的,读者可以查看所使
用的 C 语言系统中 include 文件夹下有关.h 文件的内容。
文件包含中指定的文件名既可以用引号括起来,也可以用尖括号括起来,如
#include "stdio.h"
或 #include <stdio.h>
两者稍有区别,如果使用尖括号< >,则程序编译时,由编译程序到 C 系统中设置好的 include
文件夹中,把指定的文件包含进来;如果使用双引号,则编译程序首先到当前工作文件夹中
寻找被包含的文件,若找不到,再到 C 系统的 include 文件夹中查找文件。一般说来,对 C
语言的标准头文件,采用尖括号;对编程者自己的文件,使用双引号。如果编程者自己编写
的文件存放在特殊文件夹下,指定文件包含时,可以在文件名前加上路径。
通过#include 包含进来的文件模块中还可以再包含其他文件,这种用法称为嵌套包含。
嵌套的层数与具体 C 语言系统有关,但一般可以嵌套 8 层以上。
本书附录 E 将给出 ANSI C 的标准函数及对应的头文件。表 6-1 给出了 ANSI C 定义的一
些常用标准头文件。
表 6-1 常用标准头文件

头 文 件 名 作 用

ctype.h 字符处理

math.h 与数学处理函数有关的说明与定义

stdio.h 输入输出函数中使用的有关说明和定义

string.h 字符串函数的有关说明和定义

stddef.h 定义某些常用内容

stdlib.h 杂项说明

time.h 支持系统时间函数

为了把多个文件模块连接成一个完整程序,除了用#include 文件包含外,像 TC 和 BC++


等 C 语言系统还提供工程文件功能,即定义一个工程文件(扩展名为.prg),然后把需要连接
在一起的文件名加入到该工程文件中,再经程序连接后,便可生成完整的可执行文件。具体
有关工程文件的生成使用,读者可参阅附录 A“C 语言上机操作指导”。

6.5.2 全局变量与程序文件模块
局部变量从属于函数,仅在所属函数内部有效,而全局变量将在整个程序中起作用,如
果整个程序包含多个程序文件模块,该如何定义或声明全局变量呢?本小节将主要介绍全局

– 121 –
C 语言程序设计

变量在程序文件模块使用中,所采用的外部变量和静态全局变量。

1.外部变量

上一节已经提到过外部变量,它用于全局变量定义位置在后,而使用位置在前的情况。
全局变量是在同一个文件中使用,如果一个程序由多个文件模块构成,由于全局变量的作用
范围是整个程序,显然要包括所有的文件模块。如何在每一个文件模块中定义或声明全局变
量呢?
如果在每一个程序文件模块中都定义一次全局变量,各模块单独编译时不会发生错误,
一旦把所有模块连接在一起时,就会产生对同一个全局变量名多次定义的错误。全局变量只
能在某一个模块中定义一次,其他模块要使用该全局变量的话,应通过外部变量的声明,告
诉计算机这是一个在另外模块中定义的全局变量,当程序连接时会统一指向全局变量定义的
模块上。否则不经声明而直接使用全局变量,程序编译时会出现“变量未定义”的错误。
例 6-22 某程序由两个文件模块 prog1.C 和 prog2.C 构成,其中 prog1.C 定义了全局变量
PI,而 prog2.C 中的函数要使用全局变量 PI,因此需要在文件 prog2.C 中对 PI 声明成外部变
量,如图 6-9 所示。

文件:prog1.C 文件:prog2.C
......
double PI; /*全局变量定义*/ extern double PI; /*外部变量声明*/
main() double Area(double r)
{ {
...... /* 引用外部变量 */
return(4.0*PI*r*r);
PI=3.141592654; }
/* 使用全局变量 */ double volume(double r)
...... {
} /* 引用外部变量 */
return(4.0/3.0*PI*r*r*r);
}
......

图 6-9 不同文件模块中使用全局变量

2.静态全局变量

我们已经学习过静态局部变量,其作用是使变量存储单元在函数结束后不被动态回收,
一旦该函数被再次调用时,静态局部变量单元中保存的原来的值可继续使用。而全局变量本
身的作用范围包括整个程序,存储在内存的静态数据区中,再把它定义成静态意义何在呢?
如果整个程序只有一个文件模块,静态全局变量与一般的全局变量作用完全相同。当程序由
多个文件模块构成时,静态全局变量有特殊的作用。
当一个大的程序由多人合作完成时,每个程序员可能都会定义一些自己使用的全局变量,
这些全局变量与其他人编写的模块无关,并不是整个程序用到的真正的全局变量。为避免自
己定义的全局变量影响其他人编写的模块,即所谓的全局变量副作用,可以将这些自定义的
全局变量定义为静态全局变量。C 语言的静态全局变量可以把变量的作用范围仅局限于当前
– 122 –
第6章 函数

的文件模块中,哪怕其他文件模块使用外部变量声明,也不会有任何影响。即使同一个人编
写的程序分成几个文件模块,若使用了静态全局变量,各模块之间也不能共用。
静态全局变量要在程序文件模块开头定义,其格式同静态局部变量。
例 6-23 有两个文件模块 file1.C 和 file2.C,如图 6-10 所示。其中 file2.C 定义了外部变
量 x,它本想使用 file1.C 中的全局变量 x,但由于 x 在 file1.C 中被设置为静态全局变量,其
有效的作用范围只在 file1.C 文件中,因此程序编译时,系统将给出“x 未定义”的错误。如
果 file2.C 真的想使用 file1.C 中全局变量,全局变量 x 就不能定义为静态的。

模块文件名:file1.C 模块文件名:file2.C
static int x ; extern int x ; /* 定义外部变量 */
main() f( )
{ x=4 ; /* 全局变量赋值 */ { x++ ; /* 全局变量运算 */
printf("main: %d\n", x); printf("f : %d\n", x);
f(); }
t();
}
t()
{ x++ ;
printf("t : %d\n", x);
}

图 6-10 多文件模块中使用静态全局变量

如果再增加文件模块 file3.C(如图 6-11 所示),则程序可以运行,但 file2.C 中的外部变


量是 file3.C 中定义的全局变量 x,而非 file1.C 中的全局变量 x,因此程序结果如下:
main: 4
f: 8
t: 5
尽管文件模块 file3.C 中的函数 g()并未运行,但全局变量 x 已被定义,它不从属于函数
g()。file3.C 中定义的 x 是真正的全局变量,可被 file2.C 共用,而 file1.C 定义的是“局部”
的全局变量,只能被同一个文件模块中的其他函数使用。当然 file3.C 中的全局变量 x 无法在
file1.C 里使用,就像我们前面所说的,一旦全局变量与局部变量重名时,以局部变量起作用。

模块文件名:file3.C
int x=7; /* 定义全局变量 */
g()
{ x++ ; /* 全局变量运算 */
printf("g : %d\n", x);
}

图 6-11 增加的第 3 个模块

请注意例 6-23 的 file1.C 中并没有使用文件包含,运行该程序时要采用工程文件进行连


接。如果使用文件包含,就意味着几个文件模块在编译预处理时就会合在一个文件中,那么
全局变量的静态设置就不起作用,3 个模块中使用的 x 是同一个全局变量。
由此可见,普通全局变量的作用范围是整个程序,范围最广;静态全局变量的作用范围
– 123 –
C 语言程序设计

是所在的文件模块,它是小范围的全局变量;函数的局部变量的作用范围是所在函数;复合
语句的局部变量作用范围仅在复合语句内,范围最小。

6.5.3 函数与程序文件模块
函数是一个完成确定工作的完整程序块,只要经过适当的定义和声明,函数可以被其他
函数调用。如果一个程序包括多个文件模块,普通函数适用于同一个文件模块内函数间的调
用。当要实现一个模块调用另一模块中的函数时,需要对函数进行外部声明。声明格式如下:
extern 函数类型 函数名(参数表说明);
表示所声明的函数是外部函数,其定义体在其他文件模块中。一般情况下,extern 也可以省
略,只需像一般函数那样声明即可。编译程序如果在当前文件模块中找不到函数定义体,自
动认为该函数是外部函数。当然该函数若确实未被定义,在程序连接时会给出出错信息。
例 6-24 对例 6-23 进行函数外部声明。
由于在例 6-23 中,假设所有的函数类型都是整型,所以有关声明都省略了。如果函数类
型非整型的话,声明是必不可少的。我们在图 6-12 中改变函数类型,并加上必要的函数声明。
外部函数声明的原因和步骤与外部变量非常相似。同样为了避免各文件模块间函数的相
互干扰,C 语言也允许把函数定义成静态的,以便把函数的使用范围限制在文件模块内,不
致使某程序员编写的自用函数影响其他程序员的程序,即使其他文件模块有同名的函数定义,
相互间也没有任何关联,从而增加了模块的独立性。

模块文件名:file1.C 模块文件名:file2.C
double x ; extern double x ; /* 定义外部变量 */
main() double f( )
{ double t(); /* 同模块函数声明 */ { x++ ; /* 全局变量运算 */
extern double f() ; /* 外部函数声明 */ printf("f : %f\n", x);
double y ; return x;
x=4 ; /* 全局变量赋值 */ }
printf("main: %f\n", x);
y=f();
y=t();
}
double t()
{ x++ ;
return x;
}

图 6-12 一个模块调用另一个模块的函数

静态的函数在 C 语言中也称为内部函数。其定义格式如下:
static 函数类型 函数名(参数表说明);

6.5.4 变量、函数与程序文件模块关系
本章 6.4 节主要介绍的是变量与函数的关系,6.5 节引入了程序文件模块概念,介绍了变
量和函数在不同文件模块间的使用方法。这里对它们作一个简单的小结。
C 语言把变量的存储类别分为 4 类:自动变量(auto)、静态变量(static)
、寄存器变量
(register)和外部变量(extern)。
– 124 –
第6章 函数

变量的作用范围指变量能被有效使用的范围,包括是否能在各程序文件模块上使用,以
及哪些函数可以引用。局部变量的作用范围在函数内,全局变量的作用范围可扩展到多个函
数上。
变量的生存周期指变量在计算机存储器上保存的时间周期。全局变量和静态变量的生存
期,从变量定义开始一直到整个程序结束为止,自动变量的生存期与函数一致。
变量的作用范围与生存周期是两个不同概念,如果变量的生存期已经结束,则不存在作
用范围;只有在变量的生存期内,才能进一步讨论作用范围。例如,静态局部变量的作用范
围仅在函数内,但生存期却能延续到程序结束。因此整个程序还在运行中,而静态局部变量
所在函数已经完成时,就出现变量存在,但因不在作用范围里而无法使用的情况。
本章不管是变量还是函数,都涉及了定义和声明(说明)两个概念,这里我们再做一下
区分。
任何一个变量或函数只能定义一次,再次定义都将导致编译出错。变量或函数的声明允
许多次出现。
对于变量定义,编译程序将会在计算机存储器中分配相应单元,以后使用该变量,实际
上是使用相应的存储单元。除了外部变量属于变量声明外,程序中其他类别的变量书写都是
变量定义。外部变量声明只用于全局变量,其作用是告诉编译程序,该全局变量在本模块后
面或其他模块中定义了。
带有程序实现过程的函数书写是函数定义,只写函数头(只包括函数类型、函数名和参
数表)的形式是函数声明(说明)。函数声明(说明)只是告诉编译程序该函数的实现过程(定
义体)在其他地方已定义,使得编译程序不致因为暂时找不到函数定义而给出出错信息。如
果程序在连接时还找不到函数定义,将给出连接出错信息。
变量、函数与程序文件模块关系如表 6-2 所示。
表 6-2 变量、函数与程序文件模块关系
自动变量(auto)

静态局部变量(static)
函数内:局部变量
形式参数
量 单模块
寄存器变量(register)

全局变量定义
使 函数间:全局变量
外部变量声明(extern)

文件模块内 静态全局变量

多模块
文件模块之间 外部变量

函 被调函数定义在主调函数之前,可直接调用
单模块
数 被调函数定义在主调函数之后,主调函数需声明

使 静态函数(static): 限制函数在模块内部使用
多模块
用 外部函数(extern):调用其他模块中定义的函数

– 125 –
C 语言程序设计

6.6 宏 定 义

宏定义(#define)是 C 语言中常用的功能。从第 2 章开始,我们就在程序中用宏来定义


一些符号常量了,以方便程序编制。

6.6.1 宏基本定义
宏定义格式如下:
#define 宏名 宏定义字符串
和#include 相同,define 前面也要以#开始,表示它在编译预处理中起作用,而不是真正
的 C 语句,行尾不需跟分号。宏名可以按照 C 语言标识符规定自己定义,一般为了与变量名、
函数名区别,常采用大写字母串作宏名。宏名与宏定义字符串间用空格分隔,所以宏名中间
不能有空格。宏定义字符串是宏名对应的具体实现过程,可以是任意字符串,中间可以有空
格,以回车作结束。例如:
#define PI 3.1415926
#define TRUE 1
#define FALSE 0
在程序编译时,所有出现宏名的地方,都会用宏定义字符串来替换,所以宏也常称为宏
替换(宏代换) 。如果宏定义字符串后跟分号的话,编译预处理时,把分号也作为宏代换内容。
例 6-25 字符常量 π 的宏定义。
源程序:
#define PI 3.1415926
main()
{
float m, s, r ;
scanf("%f", &r) ;
m = 2*PI*r ;
s = PI*r*r ;
printf("The perimeter = %f, and area = %f\n", m, s) ;
}
上述程序经过编译处理后,程序将转化成:
main()
{
float m, s, r ;
scanf("%f", &r) ;
m = 2*3.1415926*r ;
s = 3.1415926*r*r ;
printf("The perimeter = %f, and area = %f\n", m, s) ;
}
两个 PI 分别以 3.1415926 进行替换,程序中不再存在 PI。如果在宏定义后跟分号的话:

– 126 –
第6章 函数

#define PI 3.1415926 ;
则替换后就成为:
m = 2*3.1415926;*r ;
表达式语法出错。
宏在程序设计中非常有用,许多 C 语言编写的实用程序中,一般都会有大量的宏定义,
包括在 C 语言本身的系统头文件(.h 文件)中也有大量的宏定义。
在一般程序设计中,宏的用途包括以下几项。
(1)符号常量,如 PI、数组大小定义,以增加程序的灵活性。
(2)简单的函数功能实现,由于宏要在一行内完成,只能实现简单的函数功能。
(3)为程序书写带来一些方便。当程序中需要多次书写一些相同内容时,不妨把它简写
成宏。例如:
#define LONG_STRING "It represent a long string that \
is used as an example . "
#define 最后跟的“\”表示该行未结束,与下一行合起来成为完整一行。使用方式可以是:
printf (LONG_STRING) ;
LONG_STRING 代表的是带引号的字符串,因此在 printf()函数中不必再加引号。但一般不要
把整个 C 语句简写成宏:
#define F for (i=0 ; i<n ; i++ )
这样写确实方便,但影响了程序的可读性,也限制了语句的灵活性。
C 语言允许宏嵌套定义。例如:
#define PI 3.1415926
#define S PI*r*r
S 的宏定义使用了前面的 PI 宏定义。
宏定义可以写在程序中任何位置,它的作用范围从定义书写处到文件尾,在此范围内都
可以引用宏名进行替换。另外,可以通过“#undef”强制指定宏的结束范围。
例 6-26 宏的作用范围。
源程序:
#define A "This is the first macro"
void f1()
{
printf("A" ) ; printf("\n") ;
}
#define B "This is the second macro" A 的有效范围
void f2()
{
printf( B ) ; B 的有效范围
}
#undef B
main()
{
f1() ;

– 127 –
C 语言程序设计

f2() ;
}
运行结果如下:
A
This is the second macro
宏替换对双引号内的宏名无效,因此宏 A 在 f1()中无法代换,输出是字符 A 本身。

6.6.2 带参数的宏定义
宏要实现简单的函数功能,参数使用必不可少。由于宏常常限制在一行中,因此只能实
现简单的函数功能。
例 6-27 简单的带参宏定义程序。
源程序:
#define MAX(a, b) a>b? a: b
#define SQR(x) x*x
main()
{
int x,y;
scanf ("%d %d" , &x, &y ) ;
x = MAX (x, y) ; /* 引用宏定义 */
y = SQR(x) ; /* 引用宏定义 */
printf("%d %d\n" , x, y) ;
}
说明:宏引用形式与函数调用非常相似,但两者的实现过程完全不同。宏代换是在程序
的编译预处理时完成的,对于 MAX (x, y)的编译预处理,首先用变量名 x 和 y 分别替换 a、b,
然后再用包含 x、y 条件表达式代换 MAX。编译结束后,程序中 MAX (x, y)便消失,如图 6-13
所示。如果定义函数 max(x, y )的话,对它的处理要到程序执行时才进行,首先进行参数传递,
把实参值复制给形参 a 和 b,然后主函数暂停执行,计算机转去执行函数 max(),等求出较大
值后,通过 return 语句返回,主函数再继续运行,如图 6-14 所示。

main()
{
int x , y ;
scanf ("%d%d", &x, &y ) ;
x = x> y ? x: y ;
y = x*x ;
printf("%d %d\n", x, y) ;
}

图 6-13 宏代换后的程序

如果实参是表达式,要先计算表达式,再把结果值传递过去,这与宏代换完全不同,宏
代换不作计算,直接替换进去。如果对例 6-27 中求 y =(x+y)2,不可写成 y = SQR(x+y),因为
计算机对它作宏替换将变成:

– 128 –
第6章 函数

int max( int a, int b )


{
return a>b ? a : b ;
}


main()


{
int x , y ;
scanf ("%d %d" , &x, &y ) ;
x = max (x, y) ;
printf("%d %d\n" , x, y) ;
}

图 6-14 函数运行过程

y = x+y*x+y ≠(x+y)2
原因在于宏只是进行代换,而不会像函数那样先计算实参表达式,然后把结果再作传递。要
想避免类似问题,可以在宏定义中增加括号:
#define SQR(x) (x)*(x)
宏替换时括号保留,y = SQR(x+y)会成为:
y = (x+y)*(x+y)
宏定义中对变量加上括号,可提高代换后的运算优先级,有效避免宏代换带来的副作用,
保证宏代换的正确性。对于 MAX 来说,没有括号问题不是太大,因为条件表达式的优先级
比算术运算符低。
另外,可以用宏定义其他功能。
(1)判断字符 c 是否为小写字母的宏:
#define LOWCASE(c) ( ((c)>=’a’) && ((c)<=’z’) )
(2)把一位数字字符(’0’~’9’)转换到相应数值,−1 表示出错:
#define CTOD(c) ( ((c)>=)&&((c)<=’9’) ? c -’0’ : -1 )
例 6-28 求带宏定义的程序输出。
#define F(x) x-2
#define D(x) x*F(x)
main()
{
printf("%d,%d", D(3), D(D(3))) ;
}
分析:在阅读带宏定义的程序时,一定要坚持替换的原则,先一一替换好后,最后再进
行统一计算。千万不可一边替换一边计算,读者往往容易犯这样的错误。
D(3) = x*F(x) 先用 x 替换展开

= x*x 2 进一步对 F(x)展开,这里没有括号
= 3*3−2 = 7 最后把 x=3 代进去计算
D(D(3)) = D(7) 则大错特错,应坚持替换的原则,最后才作计算。
– 129 –
C 语言程序设计

D(D(3)) = D(x*x−2) 先对 D(3)用 x 替换展开,作为外层 D()的参数


= x*x−2* F(x*x−2) 拿展开后的参数对 D 进一步进行宏替换
= x*x−2* x*x−2−2 拿展开后的参数对 F 进一步进行宏替换
= 3*3−2*3*3−2−2 = −13 最后把 x=3 代进去计算
运行结果如下:
7 −13
本例非常容易出错,在替换过程中,读者往往会忍不住把值放进去计算。

6.7 编译预处理

编译预处理是 C 语言编译程序的组成部分,它用于解释处理 C 语言源程序中的各种预处


理指令。如前面介绍过文件包含(#include)和宏定义(#define)。它们在形式上都以“#”开
头,不属于 C 语言中真正的语句,但它们增强了 C 语言的编程功能,改进了 C 语言程序设计
环境,提高了编程效率。
C 语言程序的编译处理,目的是把每一条 C 语句用若干条机器指令来实现,生成目标程
序。由于#define 等编译预处理指令不是 C 语句,不能被编译程序翻译,需要在真正编译之前
作一个预处理,解释完成编译预处理指令,从而把预处理指令转换成相应的 C 语言程序段,
最终成为由纯粹 C 语句构成的程序,经编译最后得到目标代码。
C 语言的编译与处理功能主要包括 3 种。
(1)文件包含(#include) ;
(2)宏定义(#define) ;
(3)条件编译。
其中文件包含(#include)和宏定义(#define)已介绍过,下面简要介绍 C 的条件编译。
一般的程序经过编译后,所有的 C 语句都生成到目标程序中,如果只想把源程序中一部
分语句生成目标代码,可以使用条件编译。它广泛运用于商业软件,可以为一个程序提供多
个版本,不同的用户使用不同的版本,运行不同的程序功能。例如:
#define FLAG 1
# if FLAG
程序段 1
#else
程序段 2
#endif
程序段 1 和程序段 2 只有一个会被生成到目标程序中,由于 FLAG 被定义成 1,编译预
处理选择程序段 1 编译;若 FLAG 改为 0 的话,编译预处理选择程序段 2 编译。上例也可以
写成:
#define FLAG
# ifdef FLAG
程序段 1
– 130 –
第6章 函数

#else
程序段 2
#endif
“# ifdef FLAG”表示只要 FLAG 经过宏定义(宏定义字符串内容任意,也可以没有) ,程序
段 1 参加编译。如果没有 FLAG 的宏定义,程序段 2 参加编译。
条件编译指令均以“#”开头,其意义与 C 语言中的 if - else 语句完全不同。if-else 语句
的两个分支程序段都会被生成到目标代码中,由程序运行时根据条件决定执行哪一段;而条
件编译# if…#else…#endif,不仅形式不同,而且它的起作用时刻在编译预处理的时候。一旦
经过处理后,只有一段程序生成到目标程序中,另一段被舍弃。“#if”的条件只能是宏名,
不能是程序表达式,因为在编译预处理时是无法计算表达式的,必须在程序运行时才做计算。
采用条件编译的好处有两点,一是目标代码精简,不包含无关的代码;二是系统代码保
护性更好。如果用户 1 只花了较小的代价,得到只包含程序段 1 的可执行代码,不管它采取
何种方法,都无法找出程序段 2 的可执行代码,因为程序段 2 的可执行代码根本就不存在。
所有的编译预处理指令都是在编译预处理步骤中起作用,与程序真正运行过程无关,这
一点在介绍宏与函数间的区别时已经强调过。
条件编译指令的主要形式如表 6-3 所示。
表 6-3 条件编译指令的主要形式
指 令 形 式 含 义

#if 宏名/ 常量表达式 如果宏/常量表达式为非零,则编译代码块


代码块

#endif (#if 后面一定要有#endif 配对)

#if 宏名/常量表达式 如果宏/常量表达式为真,则编译代码块 1,否则编译代码块 2


代码块 1

#else

代码块 2

#endif

#ifdef 宏名标识符 如果宏名标识符被定义过,则编译代码块 1,否则编译代码块 2


代码块 1

#else

代码块 2 (#else 代码块 2 也可省略)


#endif

#if 宏名/常量表达式 1 如果宏/常量表达式 1 为真,则编译代码块 1;否则如果宏/常量表达式 2 为真,


代码块 1 则编译代码块 2;否则编译代码块 3(#elif 可以嵌套多层,#else 与最近的 #elif
#elif 宏名/常量表达式 2 配对,每个#if 只能有一个#else)
代码块 2

#else

代码块 3

#endif

– 131 –
C 语言程序设计

习 题

1.选择题
(1)在 C 语言程序中,若对函数类型未加显式说明,则函数的隐含类型为 。
A.void B.double C.char D.int
(2)下列程序的输出结果是 。
fun(int a, int b, int c)
{ c =a*b; }
main( )
{ int c;
fun(2,3,c);
printf("%d\n",c);
}
A.0 B.1 C.6 D.无法确定
(3)对于以下递归函数 f(),调用 f(4),其返回值为 。
int f(int n)
{ if (n) return f(n-1)+n;
else return n;
}
A.10 B.4 C.0 D.以上均不是
(4)执行下列程序:
#define MA(x, y) ( x*y )
i=5;
i=MA(i,i+1) -7;
后变量 i 的值应为 。
A.30 B.19 C.23 D.1
2.指出下列函数定义中的错误。
(1)double sum(double x , y ) ;
{ return (x*x+y*y) ; }
(2)f (double x , double y)
{ h=sqrt(x*x+y*y) ;
return h ;
}
3.填空题
(1)执行完下列语句段后,i 值为 。
int i;
int f(int x)
{ static int k = 0;
x += k++;
return x;

– 132 –
第6章 函数

}
i=f(f(1));
(2)执行完下列语句段后,i 值为 。
int i;
int f(int x)
{
return ((x>0)? f(x-1)+f(x-2):1);
}
i=f(3);
(3)下列程序段 A 与 B 功能等价。
程序段 A:
int f( int n )
{
if(n<=1)
return n;
else
return f(n-1)+f(n-2);
}
程序 B:
int f( int n )
{
______;
t0=0; t1=1; t=n;
while (_____)
{
t = ____;
t0 = t1;
t1 = t;
n --;
}
return ___;
}
(4)验证哥德巴赫猜想:任何一个大于 6 的偶数均可表示为两个素数之和。要求将 6~
100 之间的偶数都表示成两个素数之和。素数指只能被 1 和自身整除的正整数,1 不是素数,
2 是素数。
#include <stdio.h>
int prime(int n) /* 判断 n 是否为素数 */
{ int k;
for (k=2; k<=n/2; k++)
if (n%k==0) return 0;
return ;
}

– 133 –
C 语言程序设计

main()
{ int i, k;
for (i=6 ; i<=100; i+=2)
for (k=2; k<=i/2; k++)
if ( ){
printf("%d = %d +%d\n", i, k, i-k);
break;
}
}
(5)下面程序用于计算 f(k , n)=1k+2k+…+nk,其中 power(m , n )()函数用于求 mn,sum()
函数用于求 f(k, n)。
# include <stdio.h>
long power(int m , int n)
{ int i ;
__________ ;
for( i=1 ; i<=n ; i++)
___________ ;
return p ;
}
long sum(int k , int n)
{ int i ;
__________ ;
for( i=1 ; i<=n ; i++)
___________ ;
return s ;
}
main()
{ int k , n ;
scanf("%d%d", &k, &n ) ;
printf("f(%d, %d)=%ld" , k, n, f(k, n)) ;
}
4.写出下列程序运行结果。
(1)当从键盘上输入 abcdef<CR>时,下列程序将输出什么?
# include <stdio.h>
void fun()
{ char c ;
if ((c=getchar())!=’\n’)
fun( ) ;
putchar(c) ;
}
main()
{ fun() ; }

– 134 –
第6章 函数

(2)
#include <stdio.h>
#define C 5
int x=1,y=C;
main()
{
int x;
x = y++;
printf("%d %d\n",x,y);
if(x>4){
int x;
x = ++y;
printf("%d %d\n",x,y);
}
x += y--;
printf("%d %d\n",x,y);
}
(3)
# include <stdio.h>
int c, a=4 ;
func(int a, int b)
{
c=a*b;
a=b-1;
b++;
return(a+b+1);
}
main()
{ int b=2, p=0;
c=1;
p=func(b, a);
printf("%d, %d, %d, %d#", a, b, c, p);
}
(4)
# include <stdio.h>
long fib(int g)
{ switch(g){
case 0: return(0);
case 1:
case 2: return(2);
}
printf("g=%d,", g);
return ( fib(g-1) + fib(g-2) );

– 135 –
C 语言程序设计

}
main()
{ long k;
k = fib(4);
printf("k=%ld\n", k);
}
(5)
#define A 10
#define B (A<A+2)- 2
printf("%d", B*2);
(6)
#define F(x) x-2
#define D(x) x*F(x)
printf("%d,%d", D(3), D(D(3))) ;
5.编写一个函数,其功能是计算并返回两个正整数的最小公倍数,并写出主函数。要求
两个正整数作为形式参数,返回它们的最小公倍数。计算公式为:
lcm(u,v) = u * v / gcd(u, v) 当 u, v >=0 时
其中,gcd(u,v)是 u,v 的最大公因子,lcm(u,v)是 u,v 的最小公倍数。
6.编写程序,求 1~10000 之间所有的完数。所谓完数就是因子和与它本身相等的数。
例如 6=1+2+3,6 就是一个完数。定义函数 factor(m)判断 m 是否为完数,并由主函数调用它,
来对所有数进行判断。
7.编写一个函数,利用参数传入一个三位数 n,找出 101~ n 间所有既是完全平方数,
又有两位数字相同的数,如 144、676 等,函数返回找出这样的数据的个数。请同时编写主函
数。
8.编写一个函数,它从 main()函数得到一个类型为 long 的整数,将其逆序后仍以 long
类型整数返回给 main()函数,请同时写出主函数。
9.把判断某数是否为水仙花数(某三位数的各位数立方和等于该数本身)定义成函数,
并写出主函数。
10.用递归的方法对下列计算式子编写一个函数:
f(x , n) = x − x2 + x3− x4 + … + (−1) n−1x n n>0
并写出相应主函数。
11.用递归方法编写求斐波那契(Fibonacci)数列的函数,返回值为长整型。斐波那契
数列的定义为:
f(n)=f(n−2)+f(n−1) n>1
其中 f(0)=0,f(1)=1。请同时写出相应主函数。
12.请完成下列宏定义
① max(a,b,c) 求 a、b、c 的最大值
② min(a,b) 求 a、b 的最小值
③ isalnum(c) 判断 c 是否为字母数字
④ isupper(c) 判断 c 是否为大写字母

– 136 –
第6章 函数

⑤ islower(c) 判断 c 是否为小写字母
⑥ isleap(y) 判断 y 是否为闰年
⑦ cirfer(r) 计算半径为 r 的圆周长
13.分别用函数和带参宏实现从 3 个数中找出最大数,请比较两者在形式上和使用上的
区别。
14.三角形面积公式为:

area = s × ( s − a ) × ( s − b) × ( s − c ) 其中 s=(a+b+c)/2

a、b、c 分别是三角形的三条边。请分别定义计算 s 和 area 的宏,再使用函数实现,然后比


较两者在形式上和使用上的区别。

– 137 –
第7章 数 组
我们使用过的数据类型有整型、实型和字符型,它们都属于基本数据类型。除此之外,
C 语言还提供了一些更为复杂的数据类型,称为构造类型或导出类型,它由基本类型按一定
的规则组合而成。
数组是最基本的构造类型,它是一组相同类型数据的有序集合。数组中的元素在内存中
连续存放,每个元素都属于同一种数据类型,用数组名和下标可以惟一地确定数组元素。
例 7-1 输入 10 个整数,计算并输出它们的和,再分别输出这 10 个数。
分析:由于求和后,还要输出这些数,就必须保存输入数据。我们用 1 个整型数组,而
不是 10 个整型变量来存放它们。
源程序:
#include <stdio.h>
void main( )
{
int i;
int a[10]; /* 定义 1 个数组 a,它有 10 个整型元素*/
long sum=0;

for(i=0; i<10; i++) /* 将输入数依次赋给数组 a 的 10 个元素 a[0]~a[9] */


scanf("%d", &a[i]);
for(i=0; i<10; i++) /* 计算 10 个数组元素的和 */
sum=sum+a[i];
printf("sum=%ld \t", sum);
for(i=0; i<10; i++) /* 输出 10 个数组元素的值 */
printf("%d ", a[i]);
}
运行结果如下:
10 9 8 7 6 5 4 3 2 1 <CR>
sum=55 10 9 8 7 6 5 4 3 2 1
说明:定义数组 a 后,
在内存开辟出 10 个连续的单元,用于存放数组 a 的 10 个元素 a[0]~
a[9]的值,这些元素的类型都是整型,由数组名 a 和下标惟一地确定每个元素。这 10 个数组
元素接收输入数据后,相应内存单元的存储内容如图 7-1 所示。

a 10 9 8 7 6 5 4 3 2 1
a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9]

图 7-1 数组元素的存储

– 138 –
第7章 数组

在程序中使用数组,可以让一批相同类型的变量使用同一个数组变量名,用下标来相互
区分。它的优点是表达简洁,可读性好,利于使用循环结构。
本章主要介绍一维数组、二维数组和字符串。

7.1 一维数组

7.1.1 一维数组的定义和引用

1.定义

定义一个数组,需要明确数组变量名,数组元素的类型和数组的大小,即数组中元素的
数量。
一维数组定义的一般形式为:
类型名 数组名[数组长度]
类型名指定数组中每个元素的类型;数组名是数组变量(以下简称数组)的名称,是一
个合法的标识符;数组长度是一个整型常量表达式,用于给定数组的大小。
例如:
int a[10]; 定义一个有 10 个整型元素的数组 a
char c[200]; 定义一个有 200 个字符型元素的数组 c
char f[5]; 定义一个有 5 个单精度浮点型元素的数组 f

2.引用

定义数组后,就可以使用它了。C 语言规定,只能引用单个的数组元素,而不能一次引
用整个数组。
数组元素的引用要指定其下标,形式为:
数组名[下标]
下标可以是整型表达式。它的合理取值范围是[0,数组长度−1],前面定义的数组 a 就有
10 个元素 a[0]、a[1]、…、a[9],注意不能使用 a[10]。这些数组元素在内存中按下标递增的
顺序连续存储。
数组元素的使用方法与同类型的变量完全相同。例如:
int k, a[10];
定义了整型变量 k 和整型数组 a。在可以使用整型变量的任何地方,都可以使用整型数组 a
的元素。例如:
k=3;
a[0]=23;
a[k-2]=a[0]+1;
scanf("%d", &a[9]);
都是合法的 C 语句。

– 139 –
C 语言程序设计

请读者注意区分数组的定义和数组元素的引用,二者都要用到数组名[整型表达式]。定
义数组时,方括号内是常量表达式,代表数组长度,它可以包括常量和符号常量,但不能包
含变量。也就是说,数组的长度在定义时必须指定,在程序的运行过程中是不能改变的。而
表示数组元素时,方括号内是表达式,代表下标,可以包括变量,下标的合理取值范围是[0,
数组长度−1]。
C 语言的语法不检查数组下标是否越界,在编程时不要让下标越界。因为一旦发生下标
越界,就会把数据写到其他变量所占的存储单元中,甚至写入程序代码段,有可能造成不可
预料的运行结果。

3.程序举例

数组的应用离不开循环。例 7-1 中,将数组的下标作为循环变量,通过循环就可以对数


组的所有元素逐个进行处理,包括输入、输出和求和。
例 7-2 利用数组计算 Fibonacci 数列的前 20 个数,即 1,1,2,3,5,…,并按每行打
印 5 个数的格式输出。
分析:用数组计算并存放 Fibonacci 数列的前 20 个数,有下列关系式成立:
f[0]=f[1]=1
f[n]=f[n−1]+f[n−2] 2≤n≤19
源程序:
#include <stdio.h>
void main( )
{
int i, fib[20];

fib[0]=fib[1]=1; /* 生成 Fibonacci 数列前 2 个数 */


for(i=2; i<20; i++) /* 计算 Fibonacci 数列剩余的数 */
fib[i]=fib[i-1]+fib[i-2];
for(i=0; i<20; i++){
printf("%6d", fib[i]);
if((i+1)%5==0) printf("\n"); /* 每输出 5 个数就换行 */
}
}
运行结果如下:
1 1 2 3 5
8 13 21 34 55
89 144 233 377 610
987 1597 2584 4181 6765
例 7-3 输入 5 个互异的整数,将它们存入数组 a 中,再输入 1 个数 x,然后在数组中查
找 x。如果找到,输出相应的下标,否则输出“Not Found”。
源程序:
#include <stdio.h>
void main( )
– 140 –
第7章 数组

{
int i, x;
int a[5];

for(i=0; i<5; i++)


scanf("%d", &a[i]);
printf("Input x\n");
scanf("%d", &x);
for(i=0; i<5; i++)
if(a[i]==x){ /* 在数组 a 中找到了 x */
printf("Index is %d\n", i); /* 输出相应的下标 */
break; /* 跳出循环 */
}
if(i>=5) printf("Not Found\n"); /* 如果循环正常结束,说明 x 不在数组 a 中 */
}
运行结果 1:
2 9 8 1 6 <CR>
Input x
9 <CR>
Index is 1
运行结果 2:
2 9 8 1 6 <CR>
Input x
7 <CR>
Not Found
例 7-4 输入一个正整数 n(1<n≤10),再输入 n 个整数,将它们存入数组 a 中。
(1)输出最小值和它所对应的下标。
(2)将最小值与第一个数交换,输出交换后的 n 个数。
分析(1) :数组的长度在定义时必须确定,如果无法确定需要处理的数据数量,至少也
要估计其上限,并将该上限值作为数组长度。因为 n≤10,数组长度就取上限 10。此外,如
果用变量 index 记录最小值对应的下标,则最小值就是 a[index]。算法见图 7-2 中的虚线框。
源程序(1):
#include <stdio.h>
void main( )
{
int i, index, n;
int a[10];

printf("Input n\n");
scanf("%d", &n);
for(i=0; i<n; i++)
scanf("%d", &a[i]);

– 141 –
C 语言程序设计

index=0; /* 假设 a[0]是最小值,即下标为 0 的元素最小 */


for(i=1; i<n; i++)
if(a[i]< a[index]) /* 如果 a[i]比假设的最小值还小 */
index=i; /* 再假设 a[i]是新的最小值,即下标为 i 的元素最小 */
printf("min is %d\tsub is %d\n", a[index], index);
}

输入数组a
index=0
for i=1 to n-1
a[i]<a[index]
Y N
index=i
输出最小值a[index]和下标index
交换a[index]和a[0]
输出数组a

图 7-2 例 7-4 算法的 N-S 图

运行结果如下:
Input n
6 <CR>
2 9 -1 8 1 6 <CR>
min is-1 sub is 2
分析(2):将最小值与第一个数交换,就是将 a[index]与 a[0]交换,算法如图 7-2 所示。
源程序(2)只需在源程序(1)的基础上做一些改动,在最后一条语句 printf(...);后增加下列
程序段:
{ int temp;
temp=a[index];
a[index]=a[0];
a[0]=temp;
for(i=0; i<n; i++)
printf("%d", a[i]);
}
请读者自己完成源程序(2) ,并上机运行。
例 7-5 输入一个正整数 n(1<n≤10) ,再输入 n 个整数,并存入数组 a 中,用选择法
将它们从小到大排序后输出。
分析:排序又称为分类,是程序设计的常用算法,包括冒泡排序和选择排序等。
算法:选择排序的步骤如下。
(1)在未排序的 n 个数(a[0]~a[n–1])中找到最小数,将它与 a[0]交换。
(2)在未排序的 n–1 个数(a[1]~a[n–1])中找到最小数,将它与 a[1]交换。

– 142 –
第7章 数组


最后,在未排序的 2 个数(a[n−2]~a[n−1])中找到最小数,将它与 a[n−2]交换。
用 N-S 图描述的算法如图 7-3 所示。

输入数组a
for k=0 to n-2
index=k
for i=k+1 to n-1
a[i]<a[index]
Y N
index=i
交换a[index]和a[k]
输出数组a

图 7-3 选择排序算法的 N-S 图

源程序:
#include <stdio.h>
void main( )
{
int i, index, k, n, temp;
int a[10];

printf("Input n\n");
scanf("%d", &n);
for(i=0; i<n; i++)
scanf("%d", &a[i]);
for(k=0; k<n-1; k++){
index=k;
for(i=k+1; i<n; i++)
if(a[i]< a[index]) index=i;
temp=a[index];
a[index]=a[k];
a[k]=temp;
}
for(i=0; i<n; i++)
printf("%d", a[i]);
printf("\n");
}
运行结果如下:
Input n
5 <CR>

– 143 –
C 语言程序设计

3 5 2 8 1 <CR>
12358
说明:程序运行时,数组元素 a[0]~a[4]值的变化如图 7-4 所示。

k index a[0] a[1] a[2] a[3] a[4] 说明


3 5 2 8 1
0 4 1 5 2 8 3 a[0]~a[4]中最小数是a[4],a[4]与a[0]交换
1 2 1 2 5 8 3 a[1]~a[4]中最小数是a[2],a[2]与a[1]交换
2 4 1 2 3 8 5 a[2]~a[4]中最小数是a[4],a[4]与a[2]交换
3 4 1 2 3 5 8 a[3]~a[4]中最小数是a[4],a[4]与a[3]交换

图 7-4 例 7-5 数组元素 a[0]~a[4]值的变化

7.1.2 一维数组的初始化
和简单变量的初始化一样,在定义数组时,也可以对数组元素赋初值。其一般形式为:
类型名 数组名[数组长度]= {初值表};
初值表中依次放着数组元素的初值。例如:
int a[10]={1,2,3,4,5,6,7,8,9,10};
定义数组 a,并对数组元素赋初值。此时,a[0]为 1,a[1]为 2,……,a[9]为 10。
虽然 C 语言规定,只有静态存储的数组才能初始化,但一般的 C 编译系统都允许对自动
型数组赋初值。本书中也允许对静态数组、全局数组和自动型数组初始化。例如:
static int b[5]={1,2,3,4,5};
初始化静态数组 b。静态存储的数组如果没有初始化,系统自动给所有的数组元素赋 0。即
static int b[5];
等价于
static int b[5]={0,0,0,0,0};
数组的初始化也可以只针对部分元素,例如:
static int b[5]={1,2,3};
只对数组 b 的前 3 个元素赋初值,其余元素的初值为 0,即 b[0]为 1,b[1]为 2,b[2]为 3,b[3]
和 b[4]都为 0。又如:
auto int fib[20]={0,1};
对数组 fib 的前 2 个元素赋初值,其余元素的值不确定。
数组初始化时,如果对全部元素都赋了初值,就可以省略数组长度,例如:
int a[ ]={ 1,2,3,4,5,6,7,8,9,10};
此时,系统会根据初值的个数自动给出数组的长度,即上述初始化语句等价于:
int a[10]={ 1,2,3,4,5,6,7,8,9,10};
显然,如果只对部分元素初始化,数组长度不能省略。为了改善程序的可读性,尽量避免出
错,建议读者在定义数组时,不管是否对全部数组元素赋初值,都不要省略数组长度。

– 144 –
 C 语言支持多维数组,最常见的多维数组是二维数组,它主要用于表示二维表和矩阵。

7.2.1 二维数组的定义和引用
第7章

7.2 二维数组
数组

1.定义

二维数组的定义形式为:
类型名 数组名[行长度] [列长度]
例如:
int a[3][2];
定义 1 个二维数组 a,3 行 2 列,共 6 个元素。

2.引用

引用二维数组的元素要指定两个下标,即行下标和列下标,形式为:
数组名[行下标] [列下标]
行下标的合理取值范围是[0,行长度−1],列下标的合理取值范围是[0,列长度−1]。对前面定
义的数组 a,其行下标取值范围是[0,2],列下标取值范围是[0,1],6 个元素分别是 a[0][0]、
a[0][1]、a[1][0]、a[1][1]、a[2][0]和 a[2][1],可以表示 1 个 3 行 2 列的矩阵(如图 7-5 所示)。
注意下标不要越界。

列 下 标 a[0][0]
0 1 a[0][1]
a[1][0]
0 a[0][0] a[0][1]


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

图 7-5 用二维数组表示矩阵 图 7-6 二维数组在内存中的存放形式

二维数组的元素在内存中按行/列方式存放,即先存放第 0 行的元素,再存放第 1 行的元


素,……,其中每 1 行中的元素再按照列的顺序存放。数组 a 中各元素在内存中的存放顺序,
如图 7-6 所示。
由于二维数组的行(列)下标从 0 开始,而矩阵或二维表的行(列)从 1 开始,用二维
数组表示二维表和矩阵时,就存在行(列)计数的不一致。为了解决这个问题,可以把矩阵
或二维表的行(列)也看成从 0 开始,即如果二维数组的行(列)下标为 k,就表示矩阵或
二维表的第 k 行(列);或者定义二维数组时,将行长度(列长度)加 1,不再使用数组的第
0 行(列) ,数组的下标就从 1 开始。本书中除非特别声明,都采取前一种方法。

– 145 –
C 语言程序设计

3.程序举例

将二维数组的行下标和列下标分别作为循环变量,通过二重循环就可以遍历二维数组,
即访问二维数组的所有元素。由于二维数组的元素在内存中按行/列方式存放,将行下标作为
外循环的循环变量,可以提高程序的执行效率。
例 7-6 定义 1 个 3×2 的二维数组 a,数组元素的值由下式给出,按矩阵的形式输出 a。
a[i][j]=i+j(0≤i≤2,0≤j≤1)
源程序:
#include <stdio.h>
void main( )
{
int i, j;
int a[3][2]; /* 定义 1 个二维数组 a */

for(i=0; i<3; i++) /* 行下标是外循环的循环变量 */


for(j=0; j<2; j++) /* 列下标是内循环的循环变量 */
a[i][j]=i+j; /* 给数组元素赋值 */
for(i=0; i<3; i++){ /* 按矩阵的形式输出 a */
for(j=0; j<2; j++) /* 输出第 i 行的所有元素 */
printf("%4d", a[i][j]);
printf("\n"); /* 换行 */
}
}
运行结果如下:
0 1
1 2
2 3
说明:用二重循环给数组 a 的所有元素赋值时,执行(赋值)顺序如图 7-7 所示。

i j
第1次 0 0 a[0][0]=0
第2次 0 1 a[0][1]=1
第3次 1 0 a[1][0]=1
第4次 1 1 a[1][1]=2
第5次 2 0 a[2][0]=2
第6次 2 1 a[2][1]=3

图 7-7 例 7-6 用二重循环给数组 a 的所有元素赋值的顺序

例 7-7 将 1 个 3×2 的矩阵存入 1 个 3×2 的二维数组中,找出其中绝对值最小的元素,


以及它的行下标和列下标。
分析:求整型量的绝对值调用 abs()函数,相应的包含文件是 math.h。此外,用变量 row
– 146 –
第7章 数组

记录绝对值最小的元素的行下标,变量 col 记录它的列下标,则该元素就是 a[row][col]。


源程序:
#include <stdio.h>
#include <math.h>
void main( )
{
int col, i, j, row;
int a[3][2];

for(i=0; i<3; i++) /* 用输入函数给数组元素赋值 */


for(j=0; j<2; j++)
scanf("%d", &a[i][j]);
row=col=0; /* 先假设 a[0][0]是绝对值最小的元素 */
for(i=0; i<3; i++)
for(j=0; j<2; j++)
if(fabs(a[i][j])<fabs(a[row][col])){
row=i;
col=j;
}
printf("a[%d][%d]=%d\n", row, col, a[row][col]);
}
运行结果如下:
3 2 <CR>
10 -9<CR>
6 -1 <CR>
a[2][1]= -1
设 N 是正整数,定义 1 个 N 行 N 列的二维数组 a 后,数组元素表示为 a[i][j],行下标 i
和列下标 j 的取值范围都是[0,N−1]。用该二维数组 a 表示 N×N 方阵时,矩阵的一些常用术
语与二维数组行、列下标的对应关系如表 7-1 所示。
表 7-1 矩阵的术语与二维数组下标的对应关系

术 语 含 义 下标规律

主对角线 从矩阵的左上角至右下角的连线 i==j

上三角 主对角线以上的部分 i<=j

下三角 主对角线以下的部分 i>=j

副对角线 从矩阵的右上角至左下角的连线 i+j==N–1

例 7-8 输入一个正整数 n(1<n≤6) ,根据下式生成 1 个 n×n 的方阵,然后将该方阵


转置(行列互换)后输出。
a[i][j]=i*n+j+1(0≤i≤n−1,0≤j≤n−1)
例如:当 n=3 时,矩阵转置前后如下。

– 147 –
C 语言程序设计

转置前 转置后
1 2 3 1 4 7
4 5 6 2 5 8
7 8 9 3 6 9

分析:
由于 n≤6,
取上限,
定义 1 个 6×6 的二维数组 a,行列互换就是交换 a[i][j]和 a[j][i]。
源程序:
#include <stdio.h>
void main( )
{
int i, j, n, temp;
int a[6][6];

printf("Input n\n");
scanf("%d", &n);
for(i=0; i<n; i++)
for(j=0; j<n; j++)
a[i][j]=i*n+j+1;
for(i=0; i<n; i++) /* 行列互换*/
for(j=0; j<n; j++)
if(i<=j){ /* 只遍历上三角阵 */
temp=a[i][j];
a[i][j]=a[j][i];
a[j][i]=temp;
}
for(i=0; i<n; i++){ /* 按矩阵的形式输出 a */
for(j=0; j<n; j++)
printf("%4d", a[i][j]);
printf("\n");
}
}
运行结果如下:
Input n
3<CR>
1 4 7
2 5 8
3 6 9
说明:
(1)遍历上三角阵的循环也可以写成:
for(i=0; i<n; i++)
for(j=i; j<n; j++){
.......;

– 148 –
第7章 数组

}
(2)在行列互换时,遍历了上三角阵,请读者将程序改为遍历下三角阵,观察运行结果。
然后再将程序改为遍历整个矩阵,即不加条件限制,观察并分析运行结果。

7.2.2 二维数组的初始化
在定义二维数组时,也可以对数组元素赋初值,二维数组的初始化方法有两种。

1.分行赋初值

分行赋初值的一般形式为:
类型名 数组名[行长度] [列长度] = {{初值表 0},{初值表 1},…{初值表 k},…};
把初值表 k 中的数据依次赋给第 k 行的元素。例如:
int a[3][3] = {{1,2,3},{4,5,6},{7,8,9}};
初始化数组 a。此时,a 数组中各元素为:

1 2 3
4 5 6
7 8 9

二维数组的初始化也可以只针对部分元素,例如:
static int b[4][3] = {{1,2,3},{},{4,5}};
只对数组 b 第 0 行的全部元素和第 2 行的前 2 个元素赋初值,其余元素的初值都是 0。初始
化后,数组元素为:

1 2 3
0 0 0
4 5 0
0 0 0

2.顺序赋初值

顺序赋初值的一般形式为:
类型名 数组名[行长度] [列长度] = {初值表};
根据数组元素在内存中的存放顺序,把初值表中的数据依次赋给元素。例如:
int a[3][3] = {1,2,3,4,5,6,7,8,9};
等价于
int a[3][3] = {{1,2,3},{4,5,6},{7,8,9}};
如果只对部分元素赋初值,要注意初值表中数据的书写顺序。例如:
static int b[4][3] = {1,2,3,0,0,0,4,5};
等价于
static int b[4][3] = {{1,2,3},{},{4,5}};
由此可见,分行赋初值的方法直观清晰,不易出错,是二维数组初始化最常用的方法。

– 149 –
C 语言程序设计

二维数组初始化时,如果对全部元素都赋了初值,或分行赋初值时在初值表中列出了全
部行,就可以省略行长度。例如:
static int b[ ][3]={{1,2,3},{},{4,5},{}}; int a[ ][3]={1,2,3,4,5,6,7,8,9};
等价于:
static int b[4][3]={{1,2,3},{},{4,5}}; int a[3][3]={1,2,3,4,5,6,7,8,9};
与一维数组的情况类似,建议读者在定义二维数组时,不要省略行长度。
例 7-9 自定义一个函数 day_of_year(year,month,day),计算并返回 year(年)、month(月)
和 day(日)是该年的第几天。判别闰年的条件为能被 4 整除但不能被 100 整除,或能被 400
整除。例如,调用 day_of_year(2000,3,1)返回 61,调用 day_of_year(1981,3,1)返回 60。这是因
为 2000 年是闰年,1999 年不是闰年。
分析:表 7-2 列出了每月的天数,2 月的天数在闰年和非闰年有所不同。表格中增加第 0
月,使得表格中的月和二维数组的列一致,简化了编程。我们定义一个二维数组 tab 来保存
它们,tab[0][k]代表非闰年第 k 月的天数,tab[1][k]代表闰年第 k 月的天数。
表 7-2 每月的天数(非闰年和闰年)


0 1 2 3 4 5 6 7 8 9 10 11 12

非闰年 0 31 28 30 30 31 30 31 31 30 31 30 31

闰 年 0 31 29 30 30 31 30 31 31 30 31 30 31

源程序:
int day_of_year(int year, int month, int day)
{
int k, leap;
int tab[2][13]={
{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
{0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
};

leap=year%4==0 && year%100!=0 || year%400==0;


for(k=1; k<month; k++)
day += tab[leap][k];
return day;
}
说明:当 year 是闰年时,leap=1;当 year 是非闰年时,leap=0。
请读者自己编写主函数,在其中调用 day_of_year()函数。

7.3 字符串

在 C 语言中,字符串的存储和运算可以用一维字符数组来实现。
– 150 –
第7章 数组

7.3.1 一维字符数组
一维字符数组用于存放字符型数据。它的定义、初始化和引用与其他类型的一维数组一
样。例如:
char str[80];
定义一个有 80 个字符型元素的数组 str。又如:
char t[5]={’H’, ’a’, ’p’, ’p’, ’y’};
初始化数组 t,此时 t[0]为’H’,t[1]为’a’,t[2]和 t[3]都为’p’,t[4]为’y’。再如:
static char s[6]={’H’, ’a’, ’p’, ’p’, ’y’};
对静态数组 s 的前 5 个元素赋初值,其余元素的初值为 0。上述初始化语句等价于:
static char s[6]={’H’, ’a’, ’p’, ’p’, ’y’, 0};
整数 0 代表字符’\0’,也就是 ASCII 码为 0 的字符。上述初始化语句还等价于:
static char s[6]={’H’, ’a’, ’p’, ’p’, ’y’, ’\0’};
相应内存单元的存储内容如图 7-8 所示。

s H a p p y \0
s[0] s[1] s[2] s[3] s[4] s[5]

图 7-8 字符数组的存储

数组初始化时,如果对全部元素都赋了初值,就可以省略数组长度。例如:
static char s[ ]={’H’, ’a’, ’p’, ’p’, ’y’, ’\0’};
等价于前面的初始化语句,即数组长度是 6。
可以使用循环语句输出数组 t 的所有元素。
for(i=0; i<5; i++)
putchar(t[i]);

7.3.2 字符串
字符串常量就是用一对双引号括起来的字符序列,即一串字符,它有一个结束标志’\0’。
例如,字符串"Happy"由 6 个字符组成,分别是’H’、’a’、’p’、’p’、’y’和’\0’,其中前 5 个是字符
串的有效字符,’\0’是字符串结束符。字符串的有效长度就是有效字符的个数,如"Happy"的
有效长度是 5。
C 语言将字符串作为一维字符数组来处理,因此可以用一维字符数组来存放字符串。例
如:
static char s[6]={’H’, ’a’, ’p’, ’p’, ’y’, ’\0’};
数组 s 中就存放了字符串"Happy"。
字符数组的初始化还可以使用字符串常量,上述初始化等价于:
static char s[6]={"Happy"};

static char s[6]="Happy";
将字符串存入字符数组时,由于它有一个结束符’\0’,因此数组长度至少是字符串的有效
– 151 –
C 语言程序设计

长度加 1。例如,字符串"Happy"的有效长度是 5,存放它的数组的长度至少为 6,该字符串


可以存入上面定义的数组 s,但不能存入 7.3.1 节定义的数组 t,因为 t 的长度是 5。
如果数组长度大于字符串的有效长度加 1,则数组中除了存入的字符串,还有其他内容,
即字符串只占用了数组的一部分。例如:
auto char str[80]="Happy";
只对数组的前 6 个元素(str[0]~str[5])赋初值,其他元素的值不确定。但这并不会影响随后
对字符串"Happy"的处理,由于字符串遇’\0’结束,所以数组中第一个’\0’前面的所有字符和’\0’
一起构成了字符串"Happy",也就是说,’\0’之后的其他数组元素与该字符串无关。
例 7-10 计算字符串的有效长度,并输出该字符串。
分析:字符串的有效长度就是有效字符的个数,即数组中第一个’\0’前面的字符个数。
源程序:
#include <stdio.h>
void main( )
{
int i=0, len;
char str[80]="Happy";

for(i=0; str[i]!=’\0’; i++) /* 计算字符串的有效长度 */


;
len=i;
printf("len = %d\n", len);
for(i=0; str[i]!=’\0’; i++) /* 输出字符串 */
putchar(str[i]);
}
运行结果如下:
len = 5
Happy
说明:将字符串"Happy"存入数组 str 后,由于它只占用了数组的一部分,所以计算和输
出不能针对 str 的所有 80 个元素,只能针对该字符串,即数组 str 中第一个’\0’前面的字符。
从数组的首元素 str[0]开始,按下标递增的顺序,逐个处理数组元素,一旦遇到某个元素是’\0’,
说明字符串已结束,处理也随之结束。用这种方法计算出字符串的有效长度 len 后,字符串
的有效字符就存放在 str[0]~str[len−1]中。随后的处理也可以采用另一种方法,通过比较数组
下标与 len 的大小来判断字符串是否结束。例如,用下列循环输出字符串:
for(i=0; i<len; i++) /* 输出字符串 */
putchar(str[i])
由此可见,对存放了字符串的一维字符数组进行处理时,只需要处理字符串的有效字符,
即’\0’前面的字符。一般通过检测数组元素是否为’\0’来判断字符串的结束已否。
将字符串存入数组,除了上面介绍的初始化数组,还可以采用赋值和输入的方法。例如:
s[0]=’a’; s[1]=’\0’;
采用赋值的方法将字符串"a"存入数组 s。注意,"a"和’a’不一样,前者是字符串常量,包括 2
个字符’a’和’\0’,用一维字符数组存放;后者是字符常量,只有一个字符,可以赋给字符变量。
– 152 –
第7章 数组

由于字符串结束符’\0’代表空操作,无法输入,因此输入字符串时需要事先设定一个输入
结束符。一输入它,就表示字符串输入结束,并将输入结束符转换为字符串结束符’\0’。
例 7-11 输入一个以问号结束的字符串(少于 80 个字符)
,统计其中数字字符的个数。
分析:由于字符串少于 80 个字符,数组长度就取其上限 80,以’?’作为输入结束符。
源程序:
#include <stdio.h>
void main( )
{
int count=0, i=0 ;
char str[80];

while((str[i]=getchar( ))!=’?’) /* 输入字符串 */


i++;
str[i]=’\0’;
for(i=0; str[i]!=’\0’; i++) /* 统计字符串中数字字符的个数 */
if(str[i]<=’9’&&str[i]>=’0’)
count++;
printf("count = %d\n", count);
}
运行结果如下:
It’s 512?<CR>
count = 3
说明:输入一串字符后,输入结束符’?’被转换为字符串结束符’\0’,字符串"It’s 512"存入
数组 str 中。
例 7-12 输入一个以回车结束的字符串(少于 10 个字符)
,它由数字字符组成,将该字
符串转换成整数后输出。
分析:数组长度取上限 10,以回车符’\n’作为输入结束符。
源程序:
#include <stdio.h>
void main( )
{
int i=0;
char s[10];
long n;

while((s[i]=getchar( ))!=’\n’) /* 输入字符串 */


i++;
s[i]=’\0’;
n=0;
for(i=0; s[i]!=’\0’; i++) /* 将字符串转换为整数 */
if(s[i]<=’9’&&s[i]>=’0’)
n=n*10+(s[i]- ’0’);

– 153 –
C 语言程序设计

else /* 遇非数字字符结束转换 */
break;
printf("%Ld\n", n);
}
运行结果如下:
123<CR>
123
说明:将字符串"123"存入数组 s 后,将字符串转换为整数的循环执行过程如表 7-3 所示。
表 7-3 例 7-12 字符串转换为整数的循环过程

i s[i] s[i]–’0’ n=n*10+ s[i]–’0’

0 ’1’ 1 n=0*10+1=1

1 ’2’ 2 n=1*10+2=12

2 ’3’ 3 n=12*10+3=123

3 ’\0’

综上所述,C 语言中,将字符串作为一个特殊的一维字符数组来处理。采用数组初始化、
赋值或输入的方法把字符串存入数组后,对字符串的操作就是对字符数组的操作。此时,对
字符数组的操作只能针对字符串的有效字符和字符串结束符,这就需要通过检测字符串结束
符’\0’来判断是否结束字符串的操作。

习 题

1.数组定义为 int a[3][2]={1, 2, 3, 4, 5, 6},数组元素_____的值为 6。


A.a[3][2] B.a[2][1] C.a[1][2] D.a[2][3]
2.设变量定义为 char format[ ]="s=%d\n";,则数组 format 中有______个元素。
A.4 B.5 C.6 D.7
3.输入 3<回车>后,下列程序的输出结果是_______。
#include "stdio.h"
void main( )
{
int j,k,n,temp, sum=0;
int a[6][6];

scanf("%d",&n);
for(k=0;k<n;k++)
for(j=0;j<n;j++)
a[k][j]=k*n+j;
for(k=0;k<n;k++)
sum += a[k][n-k-1];

– 154 –
第7章 数组

printf("%d\n",sum);
for (k=0;k<n;k++)
for(j=0;j<n/2;j++){
temp=a[n-j-1][k];
a[n-j-1][k]=a[k][j];
a[k][j]=temp;
}
for(k=0;k<n;k++){
for(j=0;j<n;j++)
printf("%d ",a[k][j]);
printf("\n");
}
}
4.输入 elephant?后,下列程序的输出结果是_______。
# include <stdio.h>
void main()
{
int i=0, k, ch;
static int num[5];
char alpha[ ]={’a’, ’e’, ’i’, ’o’, ’u’}, in[80];

while((in[i++]=getchar( ))!=’?’)
in[i]=’\0’;
while(in[i]){
for(k=0; k<5; k++)
if(in[i] == alpha[k]) {
num[k]++;
break;
}
i++;
}
for(k=0; k<5; k++)
if(num[k]) printf("%c%d", alpha[k], num[k]);
printf("\n");
}
5.编写程序,输入一个正整数 n(1<n≤10),再输入 n 个整数,要求:
(1)求这 n 个数的平均值和最大值。
(2)按逆序输出这 n 个数。
(3)将最小值与第一个数交换,最大值与最后一个数交换,然后输出交换后的 n 个数。
6.编写程序,输入一个正整数 n(1<n≤10),再输入 n 个整数,将它们从大到小排序
后输出。
7.编写程序,输入一个 3×3 的整型矩阵,要求:

– 155 –
C 语言程序设计

(1)求该矩阵主对角线元素之和。
(2)输出该矩阵。
8.编写程序,输出一张九九乘法表。
9.编写程序,输入一个正整数 n(1<n≤6),再输入 n×n 矩阵,要求:
(1)将该矩阵转置后输出。
(2)求该矩阵每行元素的和。
(3)求该矩阵每列元素的最大值。
10.编写程序,输入日期(年、月、日),输出它是该年的第几天。
11.编写程序,输入一个以回车结束的字符串(少于 80 个字符) ,再输入一个字符,统
计并输出该字符在字符串中出现的次数,然后再输出该字符串。
12.编写程序,输入一个以回车结束的字符串(少于 80 个字符) ,输出其中 ASCII 值
最小的字符。
13.编写程序,输入一个以回车结束的字符串(少于 80 个字符),将该字符串逆序输出。

– 156 –
第8章 指 针
指针是 C 语言中一个很有特色的内容,其概念非常重要,通过它可以对变量的地址进行
操作。正确灵活地使用指针,能有效地表示复杂的数据结构,提高程序的效率。
本章主要介绍指针的基本概念和运算,指针和数组的关系以及指针和函数的关系。

8.1 指 针

内存由一系列连续的单元组成,以 PC 机为例,每个单元都有一个惟一的“编号”
,称为
内存地址。它是一个整数,系统根据这个地址来识别内存单元。
定义变量时,需要确定变量的名字和数据类型。变量名代表内存中的一个存储单元,
该存储单元的大小由变量的类型决定。在程序中使用变量,就是使用该变量所代表的存储
单元,变量的值就是存储单元的内容,显然该存储单元有一个惟一的地址。因此,每个变
量都隐含着一个内存地址。使用变量时,系统通过变量所隐含的地址找到存储单元,并对
该单元进行操作。在本书中,为了区分地址和内容(值),表示地址时用一对括号把它括
起来。
例如:
int a=3;
定义了一个整型变量 a,它代表内存中的一个存储单元,假设该单元的地址是(2000),对变
量 a 的操作,就是直接对(2000)单元进行操作。变量初始化将 a 的值赋为 3,该单元中就
存放了 3;如果要输出 a 的值,就取出该单元的内容并输出(如图 8-1 所示)。

变量名 地址 内 存 内 容
a (2000) 3
......

ap (3000) 2000
......

图 8-1 指针与地址

假设再定义一个变量 ap,它代表(3000)单元,该单元中存放了变量 a 的地址(2000),


此时,取出变量 ap 的值(2000)
,就可以访问(2000)单元,对变量 a 进行操作。也就是说,
通过变量 ap,可以间接访问变量 a。
– 157 –
C 语言程序设计

C 语言使用指针对变量的地址进行操作。指针是用来存放内存地址的变量,如果一个指
针变量的值是另一个变量的地址,我们就称该指针变量指向那个变量。前面提到的 ap 就是指
针变量,它存放了变量 a 的地址,即指针变量 ap 指向变量 a。

8.1.1 指针变量的定义
指针变量用于存放变量的地址。因为不同类型的变量在内存中占用不同大小的存储单
元,所以只知道内存地址,还不能确定该地址上的对象。在定义指针变量时,除了变量名,
还需要说明该指针变量指向的内存空间上所存放数据的类型。
指针变量定义的一般形式为:
类型名 *变量名
类型名指定指针变量所指向变量的类型,必须是有效的数据类型;变量名是指针变量(以
下简称指针)的名称,是一个合法的标识符。
例如:
int *ap; 定义一个指针变量 ap,它指向整型变量
char *cp; 定义一个指针变量 cp,它指向字符型变量

8.1.2 指针的基本运算
如果指针的值是某个变量的地址,通过指针就能间接访问那个变量,这些操作由取地
址运算符&和间接访问运算符*完成。此外,相同类型的指针还能进行赋值、比较和算术
运算。

1.取地址运算和间接访问运算

单目运算符&用于给出变量的地址。例如:
int *ap, a=3;
ap = &a;
将整型变量 a 的地址赋给整型指针 ap,即 ap 指向 a。
指针的类型和它所指向变量的类型必须相同。
单目运算符*用于访问指针所指向的变量。例如,当 ap 指向 a 时,*ap 和 a 访问同一个
存储单元,*ap 的值就是 a 的值(如图 8-2 所示)。

ap a
&a 3 *ap

图 8-2 指针运算示意

例 8-1 取地址运算和间接访问运算示例。
源程序:
#include<stdio.h>
void main( )
{

– 158 –
第8章 指针

int a=3, *ap; /* 定义整型变量 a 和整型指针 ap */

ap=&a; /* 把变量 a 的地址赋给指针 ap,即 ap 指向 a */


printf("a=%d, *ap=%d\n",a, *ap); /*输出变量 a 的值和指针 ap 所指向变量的值*/
*ap=10; /* 对指针 ap 所指向变量赋值 */
printf("a=%d, *ap=%d\n",a, *ap);
scanf("%d", &a); /* 输入 a */
printf("a=%d, *ap=%d\n",a, *ap);
(*ap)++;
printf("a=%d, *ap=%d\n",a, *ap);
}
运行结果如下:
a=3, *ap=3
a=10, *ap=10
5<CR>
a=5, *ap=5
a=6, *ap=6
说明:程序的第 4 行和其后都出现了*ap,二者的含义不同。前者用于定义指针变量, ap
是变量名,*表示其后的变量是指针;而后面出现的*ap 代表指针 ap 所指向的变量,本例中,
由于 ap 指向变量 a,因此,*ap 和 a 的值一样。
运算符&和*的优先级较高,与自增运算符++相同,结合方向是从右向左。例如,表达式
*ap=*ap+1、++*ap 和(*ap)++,分别将指针 ap 所指向变量的值加 1。而表达式*ap++等价于
*(ap++),将指针 ap 的值加 1。运算后,ap 不再指向变量 a。
例 8-2 使用指针改变变量的值。
源程序:
#include<stdio.h>
void main( )
{
int a, b, t, *p1, *p2;

a=3; b=5;
p1=&a; p2=&b; /* p1 指向 a , p2 指向 b */
printf("a=%d, b=%d, *p1=%d, *p2=%d \n",a, b, *p1, *p2);
*p1=1; b=2; /* 对 p1 所指向变量赋值, 给 b 赋值 */
printf("a=%d, b=%d, *p1=%d, *p2=%d \n",a, b, *p1, *p2);
t=*p1;*p1=*p2; *p2=t; /* 交换*p1 和*p2 的值 */
printf("a=%d, b=%d, *p1=%d, *p2=%d \n",a, b, *p1, *p2);
}
运行结果如下:
a=3, b=5, *p1=3, *p2=5
a=1, b=2, *p1=1, *p2=2
a=2, b=1, *p1=2, *p2=1

– 159 –
C 语言程序设计

说明:由于指针 p1 和 p2 分别指向变量 a 和 b,因此,*p1 和 a 的值一样,*p2 和 b 的值


一样(如图 8-3 所示)。交换*p1 和*p2 的值,就是交换 a 和 b 的值,即改变指针 p1 和 p2 所
指向变量的值,而指针 p1 和 p2 的值没有改变。
p1 a p1 a
&a 1 *p1 &a 2 *p1
p2 b p2 b
&b 2 *p2 &b 1 *p2
(a) 交换*p1 和*p2 的值前 (b)交换*p1 和*p2 的值后

图 8-3 交换两个指针所指向变量的值示意

2.赋值运算

可以将一个指针的值赋给另一个相同类型的指针。例如:
int a=3, *ap1, *ap2;
ap1=&a;
ap2=ap1;
将变量 a 的地址赋给指针 ap1,再将 ap1 的值赋给指针 ap2,因此指针 ap1 和 ap2 都指向变
量 a(如图 8-4 所示)。此时,*ap1、*ap2 和 a 访问同一个存储单元,它们的值一样。

ap1 a
&a 3 *ap1
ap2 *ap2
&a

图 8-4 指针赋值示意

例 8-3 指针赋值运算示例。
源程序:
#include<stdio.h>
void main( )
{
int a, b, c, *p1, *p2;

a=2; b=4; c=6;


p1=&a; p2=&b; /* p1 指向 a , p2 指向 b,见图 8-5(a) */
printf("a=%d, b=%d, c=%d, *p1=%d, *p2=%d \n",a, b, c, *p1, *p2);
(*p1)++; (*p2)++; /* 见图 8-5(b) */
printf("a=%d, b=%d, c=%d, *p1=%d, *p2=%d \n",a, b, c, *p1, *p2);
p2=p1; p1=&c; /* 改变指针 p1 和 p2 的值,见图 8-5(c) */
printf("a=%d, b=%d, c=%d, *p1=%d, *p2=%d \n",a, b, c, *p1, *p2);
(*p1)++; (*p2)++; /* 见图 8-5(d) */

– 160 –
第8章 指针

printf("a=%d, b=%d, c=%d, *p1=%d, *p2=%d \n",a, b, c, *p1, *p2);


}
运行结果如下:
a=2, b=4, c=6, *p1=2, *p2=4
a=3, b=5, c=6, *p1=3, *p2=5
a=3, b=5, c=6, *p1=6, *p2=3
a=4, b=5, c=7, *p1=7, *p2=4
说明:改变指针 p1 和 p2 的值后,它们分别指向变量 c 和 a(如图 8-5(c)所示),此时,
*p1 和 c 的值一样,*p2 和 a 的值一样。
p1 a p1 a
&a 2 *p1 &a 3 *p1
p2 b p2 b
&b 4 *p2 &b 5 *p2
c c
6 6
(a) (b)
p1 a p1 a
&a 3 *p2 &a 4 *p2
p2 b p2 b
&b 5 &b 5
c c
6 *p1 7 *p1
(c) (d)
图 8-5 指针赋值示意

相同类型指针的比较和算术运算在 8.2.1 中介绍。

8.1.3 指针变量的初始化
在定义指针变量时,可以对它赋初值。例如:
int a;
int *ap1 = &a;
int *ap2 = ap1; /* 等价于 int *ap2; ap2=ap1 */
指针 ap1 和 ap2 都指向变量 a。
例 8-4 指针变量初始化示例。
源程序:
#include<stdio.h>
void main( )
{
int a, b;
int *p1=&a, *p2=&b, *pt;

– 161 –
C 语言程序设计

a=1; b=2;
printf("a=%d, b=%d, *p1=%d, *p2=%d \n",a, b, *p1, *p2);
pt=p1; /* 交换 p1 和 p2 的值 */
p1=p2;
p2=pt;
printf("a=%d, b=%d, *p1=%d, *p2=%d \n",a, b, *p1, *p2);
}
运行结果如下:
a=1, b=2, *p1=1, *p2=2
a=1, b=2, *p1=2, *p2=1
说明:初始化指针 p1 和 p2,使它们分别指向变量 a 和 b(如图 8-6(a)所示)。交换
p1 和 p2 的值后,改变了指针 p1 和 p2 的值,此时,p1 指向 b,p2 指向 a(如图 8-6(b)
所示)。
p1 a p1 a
&a 1 *p1 &a 1 *p2
p2 b p2 b
&b 2 *p2 &b 2 *p1
pt pt

(a) 交换指针 p1 和 p2 的值前 (b) 交换指针 p1 和 p2 的值后

图 8-6 交换 2 个指针的值示意

与例 8-2 相比,经过交换操作,*p1 和*p2 的值都由 1 和 2 变成了 2 和 1,但采用的方法


却有本质区别。本例中交换指针 p1 和 p2 的值后,变量 a 和 b 的值并没有改变;例 8-2 中交
换*p1 和*p2 的值后,即改变了 a 和 b 的值,但指针 p1 和 p2 的值没有改变。前者直接改变指
针的值,后者改变指针所指向变量的值。
因为指针是用来存放内存地址的变量,它的值可以是另一个变量的地址。所以,我们不
仅可以像使用其他类型变量那样使用指针,包括直接对指针赋值,以及引用指针的值;还可
以通过指针间接访问它所指向的那个变量,改变或引用指针所指向变量的值。请读者注意区
分对指针的操作和对指针所指向变量的操作。
定义指针变量后,就可以使用它,但必须先赋值后引用。指针如果没有被赋值,它的值
是不确定的,即它指向一个不确定的单元,使用这样的指针,可能会出现难以预料的结果,
甚至导致操作系统错误。

8.1.4 指针作为函数的参数
C 语言中,函数的参数包括实参和形参,二者的类型要一致,可以是整型、字符型和
浮点型,也可以是指针类型。如果将某个变量的地址作为函数的实参,相应的形参就是指
针。
例 8-5 主函数 main()调用了函数 swap1()、swap2()和 swap3(),还定义了变量 a 和 b,要
求通过函数调用,交换 main()中变量 a 和 b 的值。请分析哪个函数可以实现这样的功能。
– 162 –
第8章 指针

源程序:
#include<stdio.h>
void main( )
{
int a=1, b=2;
int *pa=&a, *pb=&b;
void swap1(int x, int y), swap2(int *px, int *py), swap3(int *px, int *py);

swap1(a, b); /* 调用 swap1 */


printf("After calling swap1: a=%d b=%d\n",a, b);
a=1; b=2;
swap2(pa, pb); /* 调用 swap2 */
printf("After calling swap2: a=%d b=%d\n",a, b);
a=1; b=2;
swap3(pa, pb); /* 调用 swap3 */
printf("After calling swap3: a=%d b=%d\n",a, b);
}
void swap1(int x, int y)
{
int t;
t=x; x=y; y=t;
}
void swap2(int *px, int *py)
{
int t;
t=*px; *px=*py; *py=t;
}
void swap3(int *px, int *py)
{
int *pt;
pt=px; px=py; py=pt;
}
分析:例 6-7 介绍了函数 swap1(),在 C 语言的函数调用中,参数的传递是从实参到形参
的单向值传递,即使在函数中改变了形参的值,也不会影响到实参。因此,调用 swap1()不能
改变其实参 a 和 b 的值。
函数 swap2()的实参是指针 pa 和 pb,其值分别是变量 a 和 b 的地址。在函数调用时,将
实参 pa 和 pb 的值传送给形参 px 和 py,这样,px 和 py 中分别存放了 a 和 b 的地址,px 指向
a,py 指向 b(如图 8-7(a)所示) 。由于*px 和 a 代表同一个存储单元,只要在函数中改变
*px 的值,就改变了该存储单元的内容(如图 8-7(b)所示) 。返回主调函数后,由于 a 代表
的单元的内容发生了变化,a 的值就改变了(如图 8-7(c)所示) 。因此,在函数 swap2()中
交换*px 和*py 的值,主调函数中 a 和 b 的值也相应交换了。
函数 swap3()的参数与函数 swap2()一样,调用时的参数传递过程也相同(如图 8-7(a)

– 163 –
C 语言程序设计

所示)。函数 swap3()直接交换形参指针 px 和 py 的值(如图 8-7(d)所示)


,由于形参的改变
不会影响实参 pa 和 pb,调用该函数并不能改变主调函数中 a 和 b 的值(如图 8-7(e)所示)。
运行结果如下:
After calling swap1: a=1, b=2
After calling swap2: a=2, b=1
After calling swap3: a=1, b=2

pa a px *px px pa a
&a 1 &a 2 &a &a 2
pb b py *py py pb b
&b 2 &b 1 &b &b 1

(a) 参数传递 (b) 交换*px和*py的值 (c) 返回主调函数

*py px pa a
1 &b &a 1
*px py pb b
2 &a &b 2
(d) 交换 px 和 py 的值 (e) 返回主调函数

图 8-7 指针作为函数参数示意

总之,要通过函数调用来改变某个变量的值,可以把指针作为函数的参数。在主调函数
中,将该变量的地址作为实参,在被调函数中,用形参接受该变量的地址,并改变形参所指
向变量的值。
例 8-6 指针作为函数参数示例。
源程序:
#include<stdio.h>
void main( )
{
int x=3, y=5;
void p(int *a, int b);

p(&x, y); /* 第 1 次调用函数 p */


printf("x=%d, y=%d\n", x, y);
p(&y, x); /* 第 2 次调用函数 p */
printf("x=%d, y=%d\n", x, y);
}
void p(int *a, int b)
{
*a=10; b=20;

– 164 –
第8章 指针

}
运行结果如下:
x=10, y=5
x=10, y=10
说明:第 1 次调用函数 p()时,将变量 x 的地址和变量 y 作为实参,依次传给形参 a 和 b,
a 指向 x(如图 8-8(a)所示) 。在函数中,改变了形参 a 所指向变量*a 和形参 b 的值(如图
8-8(b)所示),由于*a 和 x 代表同一个存储单元,所以返回主调函数后,x 的值就是 10,而
形参 b 的变化不会影响实参 y,y 的值仍然是 5。请读者自己分析第 2 次调用函数 p()的情况。

x a *a a x
3 &x 10 &x 10
y b b y
5 5 20 5
(a) 参数传递 (b) 改变*a 和 b 的值 (c) 返回主调函数

图 8-8 指针作为函数参数示意

通过第 6 章的学习,我们知道函数只能返回 1 个值,但有时我们又希望函数能将计算的


多个结果带回主调函数,用指针作为函数的参数能使函数返回多个值。
例 8-7 输入年和天数,输出对应的月和日。例如,输入 2000 和 61,输出 3 和 1,即 2000
年的第 61 天在 3 月 1 日。要求自定义一个函数 month_day(year, yearday, pmonth, pday),其
中 year 是年,yearday 是天数,*pmonth 和*pday 是计算得出的月和日。
分析:用 2 个指针作为函数的参数可以带回 2 个运算结果。与例 7-9 类似,用二维数组
tab 来保存每月的天数(包括闰年和非闰年) ,判别闰年的条件见例 7-9 的题目说明。
源程序:
#include<stdio.h>
void main( )
{
int day, month, year, yearday;
void month_day(int year, int yearday, int * pmonth, int * pday);

printf("input year and yearday\n");


scanf("%d%d", &year, &yearday);
month_day(year, yearday, &month, &day);
printf("%d-%d\n", month, day);
}
void month_day(int year, int yearday, int * pmonth, int * pday)
{
int k, leap;
int tab[2][13]={
{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
{0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
– 165 –
C 语言程序设计

};

leap=year%4==0 && year%100!=0 || year%400==0;


for(k=1; yearday>tab[leap][k]; k++)
yearday -= tab[leap][k];
*pmonth=k;
*pday=yearday;
}
运行结果如下:
input year and yearday
2000 61<CR>
3-1
说明:在 main()函数中,将变量 month 和 day 的地址作为实参,在被调函数中,用形参
指针 pmonth 和 pday 分别接收地址,并改变了形参所指向变量的值。因此,main()函数中 month
和 day 的值也随之改变。

8.2 指针和数组

8.2.1 指针、数组和地址间的关系
指针和数组之间有着密切的关系。任何用数组下标完成的操作都可以用指针实现。例如:
int a[10];
int *ap;
ap = &a[0];
整型数组 a 占用 10 个连续的存储单元存放数据,每个数组元素代表一个存储单元。当整型指
针 ap 指向数组 a 的首元素 a[0]时,ap+1 就指向下一个元素 a[1],ap+2 指向 a[2],……,ap+i
指向 a[i](如图 8-9 所示)。
显然,ap+i 就是 a[i]的地址,而*(ap+I)和 a[i]代表同一个存储单元,它们的值相同。

ap a[0]
ap+1 a[1]
…

ap+i a[i]

ap+9 a[9]

图 8-9 指针和数组关系示意

C 语言规定,数组名代表数组首元素的地址。因此,语句 ap = &a[0];也可以写成:

– 166 –
第8章 指针

ap = a;
此时,ap 的值和 a 相同,都是首元素 a[0]的地址。因此,a+i 就是 a[i]的地址,而*(a+i)和 a[i]
等价。
C 语言中,把方括号[ ]称为下标运算符或求址运算符,它的功能是将下标表示的数组元
素 a[i]转换为指针形式*(a+i)。如果 pa 是指针,pa[i]也是合法的,它相当于*(pa+i)。
若数组 a 和指针 ap 的类型相同,当 ap=a 时,&a[i]、a+i、ap+i 和&ap[i]都可以表示数组
a 的第 i 个元素的地址,而 a[i]、*(a+i)、*(ap+i)和 ap[i]都可以表示数组 a 的第 i 个元素。
即数组的下标表示和指针表示是等价的。例如,输出数组 a 的所有元素,可以使用下列任意
一条语句。
for(i=0; i<10; i++) /* 语句 1 */
printf("%d ", a[i]);
for(i=0; i<10; i++) /* 语句 2 */
printf("%d ", *(a+i));
for(ap=a, i=0; i<10; i++) /* 语句 3 */
printf("%d ", ap[i]);
for(ap=a, i=0; i<10; i++) /* 语句 4 */
printf("%d ", *(ap+i));
语句 1 和语句 2 是一样的,因为 a[i]被自动转换为*(a+i),语句 3 和语句 4 也同样。虽然
后两句用到指针 ap,但它的值保持不变,没有体现出指针处理的特点。
请注意,数组名代表数组首元素的地址,它是一个常量,不是变量。因此,对于前面
定义的数组 a 和指针变量 ap,表达式 ap=a 和 ap++是合法的,而表达式 a=ap 和 a++是非
法的。
如果 ap 指向数组的某个元素,ap++使 ap 指向下一个元素。指针加上或减去一个整数,
就改变了原先的指向。例如:
int a[10], *ap1, *ap2, *ap3;
ap1=ap2=ap3=&a[1]; /* ap1, ap2 和 ap3 都指向数组元素 a[1] */
ap3--;
ap2 = ap2+2;
指针 ap3 自减 1 后,它不再指向 a[1],而是指向上一个元素 a[0],计算 ap2=ap2+2 后,ap2
向下移动,指向 a[3](如图 8-10 所示) 。

ap3 a[0]
ap1 a[1]
a[2]
ap2 a[3]

图 8-10 指针算术运算示意

请注意,指针每一次加 1 或减 1,并非指针的值加 1 或减 1,而是加上或减去该指针类型


的长度,即它所指向的存储单元占用的字节数。由于数组元素是连续存储的,假设用 2 个字

– 167 –
C 语言程序设计

节存储整型数据,且 a[1]的地址是(2004) ,则 ap3––后,ap3 的值就是(2002),正是 a[1]


的上一个存储单元 a[0]的地址,故 ap3 指向 a[0]。
两个相同类型的指针相减,表示它们之间相隔的存储单元数,如 ap2-ap3 得 3。
C 语言中,指针的算术运算只包括两个相同类型的指针相减以及指针加上或减去一
个整数,其他的操作如指针相加、相乘和相除,或指针加上和减去一个浮点数都是非法
的。
两个相同类型指针还可以使用关系运算符比较大小。如 ap2>ap3 的值是 1(真)。
例 8-8 输入 10 个整数,计算并输出它们的和。
源程序:
#include<stdio.h>
void main( )
{
int i, a[10], *ap;
long sum=0;

for(i=0; i<10; i++)


scanf("%d", &a[i]);
for(ap=a; ap<a+10; ap++) /* 求和 */
sum=sum+*ap;
printf("sum=%ld \n", sum);
}
运行结果如下:
10 9 8 7 6 5 4 3 2 1<CR>
sum=55
说明:在求和的循环中,ap 先指向数组首元素 a[0],累加*ap,然后 ap 自增 1,指向下
一个元素,再累加*ap,……,直至 ap 大于最后一个元素 a[9]的地址。通过指针 ap 值有规律
的变化,让它依次指向每一个数组元素,再用*ap 取出相应元素的值进行处理,体现出指针
操作的基本特点。
可见,用数组可以完成的操作也能用指针实现,指针比数组的效率高,但不够直观,初
学者较难掌握。

8.2.2 数组名作为函数的参数
由于数组名代表数组首元素的地址,它作为函数的参数,实质是指针作为函数的参数,
即数组名作为函数的实参时,相应的形参就是指针。
例 8-9 重做例 8-8,要求定义并调用函数 fsum(a, n)计算数组 a 的前 n 个元素之和。
源程序:
#include<stdio.h>
void main( )
{
int i, a[10];
long sum;
– 168 –
第8章 指针

long fsum(int *a, int n);

for(i=0; i<10; i++)


scanf("%d", &a[i]);
sum=fsum(a,10); /* 数组名 a 和数组长度作为函数的实参 */
printf("sum=%ld\t", sum);
}
long fsum(int *p, int n) /* 接收数组名的形参 p 是指针 */
{
int i ;
long sum;

for(i=0, sum=0; i<n; i++)


sum=sum+p[i];
return sum;
}
说明:函数调用时,数组名 a 作为实参,它的值被传送给形参指针 p,p 就指向了
数组的首元素,在函数中,用 p[i]或*(p+i)访问实参数组元素 a[i]所代表的存储单元(如
图 8-11(a)所示)。
形参 n 的含义是函数可以处理从 p 开始连续的 n 个单元,但这些单元不能超出实参数组
的有效范围。例如,数组 a 的长度是 10,调用 fsum(a,10),得到 a[0]~a[9]的和;调用 fsum(a,3),
得到 a[0]~a[2]的和;而调用 fsum(a,20),就要计算 a[0]~a[19]的和,由于超出了数组 a 的有
效范围,所以此调用没有意义。
函数定义时,形参指针 p 也可以写成数组的形式:
long fsum(int p[ ], int n)
下面是函数 fsum()的另一种形式,通过改变形参指针的值实现同样的功能。
long fsum(int *p, int n)
{
int i, *pa=p; /* 用 pa 保存 p 的初值 */
long sum;

for(sum=0; p<pa+n; p++)


sum=sum+*p;
return sum;
}
调用 fsum(a,10),形参指针 p 指向数组 a 的首元素,n 值为 10。用 pa 记录 p 的起始位置,
p 从 pa 变化到 pa+10,*p 就是相应单元的内容(如图 8-11(b)所示)。由于 p 是形参,其值
的改变不会影响实参 a。
调用函数 fsum()时,不仅数组名 a 可以作为实参,其他数组元素的地址也能作为实参。
如调用 fsum(a+2, 3),形参 p 的值是 a+2,指向 a[2],函数处理从 p 开始连续的 3 个单元,得
到 a[2]、a[3]和 a[4]的和(如图 8-12 所示)。

– 169 –
C 语言程序设计

p pa
a[0] p[0] a[0]
a[1] p[1] a[1]

p
*p
p+i
a[i] p[i] a[i]

a[9] p[9] a[9]


pa+10
(a) 形参指针 p 的值不变 (b) 改变形参指针 p

图 8-11 数组名 a 作为函数的实参示意

a[0]
a[1]
p
a[2]
a[3]
a[4]

a[9]

图 8-12 数组元素 a[2]的地址作为函数的实参示意

例 8-10 用函数实现数组内容的逆序存放。
分析:设数组中有 n 个元素,将(a[0],a[n−1])互换,(a[1],a[n−2])互换,……,直到每
对元素都互换一次。
源程序:
#include<stdio.h>
void main( )
{
int i, a[10];
void reverse(int *a, int n);

for(i=0; i<10; i++)


scanf("%d", &a[i]);
reverse(a,10); /* 调用函数 */
for(i=0; i<10; i++)
printf("%d\t", a[i]);
}
void reverse(int p[ ], int n)
{

– 170 –
第8章 指针

int i, j, t;

for(i=0, j=n-1; i<j; i++, j--) {


t=p[i];
p[i]=p[j];
p[j]=t;
}
}
运行结果如下:
10 9 8 7 6 5 4 3 2 1<CR>
1 2 3 4 5 6 7 8 9 10
说明:数组名作为函数的实参,在被调用函数中,就能访问实参数组所在的存储单元,
不但可以引用,还能改变这些单元的内容。返回主调函数后,相应数组元素的值就改变了。
和例 8-9 类似,下面用指针值的变化实现函数 reverse(),如图 8-13 所示。
void reverse(int *p, int n)
{
int *pj, t;

for(pj=p+n-1; p<pj; p++, pj--) {


t=*p;
*p=*pj;
*pj=t;
}
}
p
a[0]
a[1]

pj
a[9]

图 8-13 函数 reverse()中指针变化示意

例 8-11 输入 5 个整数,将它们从小到大排序后输出。要求定义并调用函数 fsort(a, n),


用冒泡法将数组 a 的前 n 个元素从小到大排序。
算法:冒泡排序的步骤如下。
(1)在未排序的 n 个数(a[0]~a[n−1])中,从 a[0]开始,依次比较相邻两个数,即比较
(a[0]和 a[1]),(a[1]和 a[2]),…,(a[n−2]和 a[n−1]),如果前面的数比后面的数大,就交换
它们的值。这样,最大的数被换到了最后,即 a[n−1]最大。
(2)在未排序的 n−1 个数(a[0]~a[n−2])中,比较相邻两个数,将大数换到后面。这样,
– 171 –
C 语言程序设计

a[n−2]就是这些数中最大的。

最后,比较未排序的 2 个数(a[0]~a[1]),把大数换到后面。a[1]就是其中最大的。用
N-S 图描述的算法如图 8-14 所示。

输入数组a
for k=1 to n-1
for j=0 to n-k-1
a[j]>a[j+1]
Y N
交换
a[j]和a[j+1]
输出数组a

图 8-14 冒泡排序算法的 N-S 图

源程序:
#include<stdio.h>
void main( )
{
int i, a[5];
void fsort(int *a, int n);

for(i=0; i<5; i++)


scanf("%d", &a[i]);
fsort(a,5); /* 调用函数 */
printf("after sorting\n");
for(i=0; i<5; i++)
printf("%d\t", a[i]);
}
void fsort(int a[ ], int n)
{
int k, j, temp;

for(k=1; k<n; k++)


for(j=0; j<n-k; j++)
if(a[j]>a[j+1]){
temp=a[j];
a[j]=a[j+1];
a[j+1]=temp;
}
}

– 172 –
第8章 指针

运行结果如下:
6 5 2 8 1<CR>
after sorting
12568
说明:以冒泡排序的第(1)步为例,分析数组元素 a[0]~a[4]值的变化(如图 8-15 所示)。

k j a[0] a[1] a[2] a[3] a[4] 说明


6 5 2 8 1
0 5 6 2 8 1 a[0]>a[1],交换
1 5 2 6 8 1 a[1]>a[2],交换
1
2 5 2 6 8 1 a[2]<a[3],不变
3 5 2 6 1 8 a[3]>a[4],交换

图 8-15 例 8-11 数组元素 a[0]~a[4]值的变化

8.3 指针和字符串

C 语言使用字符数组和字符指针来处理字符串。
用字符数组处理字符串时,先把字符串存入一维字符数组中,再对该数组进行操作。用
字符指针处理字符串时,将字符串首字符的地址赋给指针,即让指针指向字符串,再通过指
针对该字符串进行操作。

8.3.1 常用的字符串处理函数
C 语言提供了一些用来处理字符串的库函数,它们在系统文件 stdio.h 和 string.h 中定义。
对于字符串的常用操作,如输入、输出、复制和连接等,可以直接调用库函数,并将存放了
字符串的字符数组名或字符串常量作为函数的实参。

1.字符串的输入和输出

函数 scanf()和 printf()以及 gets()和 puts()都可用来输入输出字符串,它们在系统文件


stdio.h 中定义。
(1)scanf (格式控制字符串,输入参数表)
格式控制字符串中相应的格式控制说明用%s,输入参数必须是字符数组名。遇回车或空
格输入结束,自动将输入的数据和’\0’送入数组中。
(2)printf (格式控制字符串,输出参数表)
格式控制字符串中相应的格式控制说明用%s,输出参数可以是字符数组名或字符串常
量。遇’\0’输出结束。
(3)字符串输入函数 gets(s)

– 173 –
C 语言程序设计

参数 s 必须是字符数组名。遇回车输入结束,自动将输入的数据和’\0’送入数组中。
(4)字符串输出函数 puts(s)
参数 s 可以是字符数组名或字符串常量。输出时遇’\0’自动将其转换为’\n’,即输出字符串
后换行。
例 8-12 字符串输入输出函数的使用比较。
/* 程序 A */ /* 程序 B */
#include<stdio.h> #include<stdio.h>
void main( ) void main( )
{ {
char str[80]; char str[80];

scanf("%s", str); gets(str);


printf("%s", str); puts(str);
printf("%s", "Hello"); puts("Hello");
} }

运行结果 1: 运行结果 1:
Programming<CR> Programming<CR>
ProgrammingHello Programming
运行结果 2: Hello
Programming is fun!<CR> 运行结果 2:
ProgrammingHello Programming is fun!<CR>
Programming is fun!
Hello

从运行结果 1 看,printf()函数和 puts()函数的区别在于后者输出字符串后会自动换行。用


运行结果 2 比较 scanf()函数和 gets()函数的区别,程序 A 中,由于 scanf()函数遇空格结束输
入,数组 str 中存放"Programming";程序 B 中,由于 gets()函数遇回车结束输入,数组 str 中
存放了全部输入内容"Programming is fun!"。此外,数组 str 的长度要足够大,以便存放输入
的字符串,即字符串有效字符和’\0’。

2.字符串的复制、连接和比较

以下函数在系统文件 string.h 中定义。


(1)字符串复制函数 strcpy(s, ct)
参数 s 必须是字符数组名,参数 ct 可以是字符数组名或字符串常量。strcpy()函数将 ct
中的字符串有效字符和’\0’都复制到 s。数组 s 的长度要足够大,以便存放 ct 中的字符串。例
如:
int i;
char str1[80], str2[80], from[80]="happy"; /* 初始化数组 from */
strcpy(str1, from);
strcpy(str2, "key");

– 174 –
第8章 指针

调用函数 strcpy()把数组 from 中的字符串复制给数组 str1,并把字符串常量"key"复制给数组


str2 后,数组 str1 中存放了"happy",数组 str2 中存放了"key"。C 语言不允许用赋值表达式直
接对数组赋值,即 str1=from 和 str2="key"都是非法的。将一个字符串常量或存放在字符数组
中的字符串,赋给另一个数组,可以直接调用库函数 strcpy(),或逐个元素赋值。
为了叙述简洁,在本节的以下内容中,我们不再区分字符串常量和存放在字符数组中的
字符串,将它们统称为字符串。
(2)字符串连接函数 strcat(s, ct)
参数 s 必须是字符数组名,参数 ct 可以是字符数组名或字符串常量。strcat()函数将字符
串 ct 接到字符串 s 的后面,此时 s 中原有的结束符’\0’就不存在了。数组 s 的长度要足够大,
以便存放连接后的新字符串。例如:
char str1[80]="Hello ", str2[80], t[80]="world";
strcat(str1, t);
strcpy(str2, str1);
strcat(str2, "!");
先调用函数 strcat()连接数组 str1 和 t,结果放在 str1 中(如图 8-16 所示)
,再调用函数 strcpy()
将 str1 中的字符串赋给 str2,最后调用函数 strcat()连接 str2 和字符串常量"!"后,str2 中存放
了"Hello world!"。C 语言不允许将数组直接相加连接,即 str1=str1+t 和 str2=str2+"!"都是非法
的。连接 2 个字符串可以直接调用库函数 strcat(),或逐个元素赋值。

str1 h e l l o \0
t w o r l d \0
调用strcat(str1,t)后
str1 h e l l o w o r l d \0
t w o r l d \0

图 8-16 调用 strcat(str1,t)示意

(3)字符串比较函数 strcmp(cs, ct)


参数 cs 和 ct 可以是字符数组名或字符串常量。strcmp()函数返回一个整数,该数给出字
符串 cs 和 ct 的比较结果。若 cs 和 ct 相等,返回 0;若 cs 大于 ct,返回一个正数;若 cs 小于
ct,返回一个负数。
设 str1 和 str2 都是字符串,C 语言中,str1==str2、str1>str2 和 str1<=str2 都是非法的表
达式,应该用 strcmp(str1,str2)==0、strcmp(str1,str2)>0 和 strcmp(str1,str2)<=0 来表示上述关系。
字符串比较的规则是从两个字符串的首字符开始,依次比较相对应的字符(比较字符的
ASCII 码) ,直到出现不同的字符或遇’\0’为止。如果所有的字符都相同,返回 0;否则,以第
一个不相同字符的比较结果为准,返回这两个字符的差,即第一个字符串中的字符减去第二
个字符串中的字符得到的差。例如:
strcmp("sea", "sea")的值是 0,说明"sea"="sea"
strcmp("compute", "compare ")的值(’u’-’a’)是个正数,说明"compute">"compare"
strcmp("happy", "z")的值(’h’-’z’)是个负数,说明"happy"<"z"

– 175 –
C 语言程序设计

strcmp("sea", "seat")的值(’\0’-’t’)是个负数,说明"sea"<"seat"
(4)字符串长度函数 strlen(cs)
参数 cs 可以是字符数组名或字符串常量。strlen()函数返回字符串 cs 的有效长度,即字
符串有效字符的个数,不包括’\0’。例如,strlen("happy")的值是 5,strlen("A")的值是 1。
例 8-13 输入 5 个字符串,输出其中最小的字符串。
分析:为了让读者更好地理解字符串操作的特点,我们用对比的方法编写程序。先写程
序 B,它的功能是输入 5 个整数,输出其中的最小值。显然,这两个程序的算法相似,但需
要处理的数据类型不同,程序 B 定义了整型变量 x 和 min,分别存放输入数和最小数,程序
A 就要定义两个一维字符数组 sx 和 smin,分别保存输入的字符串和最小的字符串。运算的实
现方法也不同,如整型数据可以直接赋值,而数组的赋值就调用了函数 strcpy()。
源程序:
/* 程序 A */ /* 程序 B */
#include<stdio.h> #include<stdio.h>
#include<string.h>
void main( ) void main( )
{ {
int i; int i;
char sx[80], smin[80]; int x, min;

scanf("%s", sx); scanf("%d", &x);


strcpy(smin,sx); min=x;
for(i=1; i<5; i++){ for(i=1; i<5; i++) {
scanf("%s", sx); scanf("%d", &x);
if(strcmp(sx, smin)<0) if(x<min)
strcpy(smin,sx); min=x;
} }
printf("min is %s\n", smin); Printf("min is %d\n", min);
} }
运行结果如下: 运行结果如下:
tool key about zoo sea<CR> 2 8-1 99 0<CR>
min is about min is-1

8.3.2 字符串的指针表示
首先讨论字符串常量的存储。在 7.3.2 中提到,字符串常量是用一对双引号括起来的字符
序列,与基本类型常量的存储相似,字符串常量在内存中的存放位置由系统自动安排;由于
字符串常量是一串字符,通常被看做一个特殊的一维字符数组,与数组的存储类似,字符串
常量中的所有字符在内存中连续存放。所以,系统在存储一个字符串常量时,先给定一个起
始地址,从该地址指定的存储单元开始,连续存放该字符串中的字符。显然,该起始地址代
表了存放字符串常量首字符的存储单元的地址,被称为字符串常量的值,也就是说,字符串
常量实质上是一个指向该字符串首字符的指针常量。例如,字符串"hello"的值是一个地址,
从它指定的存储单元开始连续存放该字符串的 6 个字符。
– 176 –
第8章 指针

如果定义一个字符指针来接收字符串常量的值,该指针就指向字符串的首字符。这样,
字符数组和字符指针都可以用来处理字符串。例如:
char sa[ ]="array";
char *sp="point";
printf("%s ", sa); /* 数组名 sa 作为 printf 的输出参数 */
printf("%s ", sp); /* 字符指针 sp 作为 printf 的输出参数 */
printf("%s\n", "string"); /* 字符串常量作为 printf 的输出参数 */
输出:
array point string
调用 printf()函数,以%s 的格式输出字符串时,作为输出参数,数组名 sa、指针 sp 和字符串
"string"的值都是地址,从该地址所指定的单元开始,连续输出其中的内容(字符),直至遇
到’\0’为止。由此可见,输出字符串时,输出参数给出起始位置(地址),’\0’用来控制结束。
因此,字符串中其他字符的地址也能作为输出参数。例如:
printf("%s ", sa+2); /* 数组元素 sa[2]的地址作为输出参数 */
printf("%s ", sp+3); /* sp+3 作为起始地址 */
printf("%s\n", "string"+1);
输出:
ray nt tring
字符数组与字符指针都可以处理字符串,但两者之间有重要区别。例如:
char sa[ ]="This is a string";
char *sp="This is a string";
字符数组 sa 在内存中占用了一块连续的单元,有确定的地址,每个数组元素放字符串的一个
字符,字符串就存放在数组中。字符指针 sp 只占用一个可以存放地址的内存单元,存储字符
串首字符的地址,而不是将字符串放到字符指针变量中去(如图 8-17 所示)。

sa This is a string\0

sp This is a string\0

图 8-17 字符数组 sa 和字符指针 sp 的区别示意

如果要改变数组 sa 所代表的字符串,只能改变数组元素的内容。如果要改变指针 sp 所
代表的字符串,通常直接改变指针的值,让它指向新的字符串。因为 sp 是指针变量,它的值
可以改变,转而指向其他单元。例如:
strcpy(sa, "Hello");
sp="Hello";
分别改变了 sa 和 sp 所表示的字符串。而
sa="Hello";
是非法的,因为数组名是常量,不能对它赋值。
定义字符指针后,如果没有对它赋值,指针的值是不确定的,不能明确它指向的内存单
元。因此,如果引用未赋值的指针,可能会出现难以预料的结果。例如:
char *s;

– 177 –
C 语言程序设计

scanf("%s", s);
没有对指针 s 赋值,却对 s 指向的单元赋值。如果该单元已分配给其他变量,其值就改变了。

char *s, str[20];
s = str;
scanf("%s",s);
是正确的。数组 str 有确定的存储单元,s 指向数组 str 的首元素,并对数组赋值。
为了尽量避免引用未赋值的指针所造成的危害,在定义指针时,可先将它的初值置为空。
例如:
char *s=NULL;
符号常量 NULL 在系统文件 stdio.h 中定义,其值为 0,将它赋给指针时,代表空指针。C 语
言中的空指针不指向任何单元。

8.3.3 字符数组和字符指针
8.3.2 节说明了字符数组和字符指针的重要区别,除此以外,它们还有不同。通过分别用
指针和数组定义函数,例 8-14 展示了二者更多的差别。
例 8-14 自定义函数 strcpy()、strcmp()和 strlen(),要求分别用字符数组和字符指针实现。
函数的功能见 8.3.1 节。
分析:当字符数组名、字符串常量或字符指针作为函数实参时,相应的形参都是字符指
针,它也可以写成数组形式。
(1)定义函数 strcpy()
第 1.1~1.3 版用数组实现,第 2.1~2.2 版用指针实现。
void strcpy(char to[ ], char from[ ]) /* 第 1.1 版 */
{
int i;

for(i=0; from[i]!=’\0’; i++)


to[i]=from[i];
to[i]=’\0’;
}
把循环条件改为(to[i]=from[i])!=’\0’,先求解表达式 to[i]=from[i],即把 from[i]赋给 to[i],
当 from[i]为’\0’时也一样,再判断表达式是否为’\0’。
void strcpy(char to[ ], char from[ ]) /* 第 1.2 版 */
{
int i=0;

while((to[i]=from[i])!=’\0’)
i++;
}
因为转义字符’\0’代表 ASCII 码值为 0 的字符,它的值就是 0,故 x!=0 和 x!=’\0’等价,在
描述条件时,关系表达式 x!=0 又和 x 等价。所以,循环条件(to[i]=from[i])!=’\0’也可以写成
– 178 –
第8章 指针

to[i]=from[i]。
void strcpy(char to[ ], char from[ ]) /* 第 1.3 版 */
{
int i=0;

while(to[i]=from[i])
i++;
}
void strcpy(char *to, char *from) /* 第 2.1 版 (1.2 版的指针实现) */
{
while((*to=*from)!=’\0’){ /* 循环条件也可以写成 *to=*from */
to++;
from++;
}
}
指针 to 和 from 必须同步移动。
void strcpy(char *to, char *from) /* 第 2.2 版 */
{
while(*to++=*from++)
;
}
其中*to++=*from++先把*from 赋给*to,再自增指针 to 和 from。
(2)定义函数 strcmp()
第 1 版用数组实现,第 2 版用指针实现。
int strcmp(char s[ ], char t[ ]) /* 第 1 版 */
{
int i;

for(i=0;s[i]!=’\0’; i++) /* 循环条件也可以是 t[i]!=’\0’ */


if(s[i]!=t[i]) break;
return s[i]-t[i];
}
int strcmp(char *s, char *t) /* 第 2 版 (第 1 版的指针实现) */
{
for(;*s!=’\0’; s++, t++)
if(*s!=*t) break;
return *s-*t;
}
调用下列函数返回前,指针 s 和 t 的值如图 8-18 所示。
(3)定义函数 strlen()
第 1 版用数组实现函数 strlen(),第 2 版用指针实现。

– 179 –

 
strcmp("sea", "sea")
=*s-*t=’\0’-’\0’=0
s

sea\0

sea\0

int strlen(char s[ ])
{
int i;
=*s-*t=’u’-’a’>0

/* 第 1 版 */
s

compute\0

compare\0

t
图 8-18
C 语言程序设计

strcmp("compute", "compare ") strcmp("sea", "seat")


=*s-*t=’\0’-’t’<0

函数 strcmp()示意
s

sea\0

seat\0

t
strcmp("seat", "sea")
=*s-*t=’t’-’\0’>0
s

seat\0

sea\0

for(i=0; s[i]!=’\0’; i++)


;
return i;
}
int strlen(char *s) /* 第 2 版 (第 1 版的指针实现) */
{
char *p=s;
while(*p!=’\0’)
p++;
return p-s;
}
指针 s 指向首元素,指针 p 指向字符串结束符’\0’,两个指针的差 p−s,表示其间相隔的
存储单元数,即字符数。
字符指针经常用于字符串操作,它的使用比字符数组更灵活。

8.4 指针数组和指向指针的指针

8.4.1 指针数组
指针数组中,数组元素的类型是指针,用于存放内存地址。
一维指针数组定义的一般格式为:
类型名 * 数组名[数组长度]
类型名指定数组元素所指向变量的类型。
例如:
int a[10];
int * pa[10];

– 180 –
第8章 指针

分别定义了整型数组 a 和整型指针数组 pa。数组 a 有 10 个元素,其元素的类型是整型,可


以存放 10 个整型数据;数组 pa 也有 10 个元素,元素的类型是整型指针,用于存放整型单元
的地址。对指针数组元素的操作和对同类型指针变量的操作相同。
例 8-15 指针数组示例。
源程序:
#include<stdio.h>
void main( )
{
int i=1, j=2, k=3, m=4, n;
int a[4];
int *pa[4];

a[0]=i; a[1]=j; a[2]=k; a[3]=m;


pa[0]=&i; /* pa[0]指向变量 i */
pa[1]=&j; /* pa[1]指向变量 j */
pa[2]=&k; /* pa[2]指向变量 k */
pa[3]=&m; /* pa[3]指向变量 m */
for(n=0; n<4; n++)
printf("%d ", a[n]);
printf("\n");
for(n=0; n<4; n++) /* 程序段 1 */
printf("%d ", *pa[n]);
printf("\n");
for(n=0; n<4; n++) /* 程序段 2 */
printf("%x ", pa[n]);
printf("\n");
}
运行结果如下:
1 2 3 4
1 2 3 4
ffc8 ffca ffcc ffce
说明:因为 pa[n]中放着地址,指针数组 pa 的每个元素分别指向一个整型变量(如图 8-19
所示),*pa[n]就代表 pa[n]所指向的变量。例如,pa[0]指向变量 i,所以*pa[0]和 i 代表

1 i *pa[0]
a pa
1 pa [0] 2 j *pa[1]
2 pa [1]
3 pa [2] 3 k *pa[2]
4 pa [3]
4 m *pa[3]

图 8-19 指针数组示意

– 181 –
C 语言程序设计

同一个存储单元。运行程序,前两行输出相同,都是输出整型变量的值;最后一行则输出整
型变量 i、j、k、m 的地址。
读者运行该程序时,最后一行的结果很可能不一样,这并不重要,关键要掌握指针数组
中,数组元素存放地址,通过数组元素可以访问它所指向的单元。
例 8-16 写出下列程序的运行结果。
源程序:
#include<stdio.h>
void main( )
{
int i=1, j=2, k=3, m=4, n, t;
int *pa[4], *pt;

pa[0]=&i; pa[1]=&j; pa[2]=&k; pa[3]=&m;


pt=pa[0]; pa[0]=pa[3]; pa[3]=pt; /* 交换 pa[0]和 pa[3]的值 */
for(n=0; n<4; n++)
printf("%d ", *pa[n]);
printf("\n");
pa[0]=&i; pa[1]=&j; pa[2]=&k; pa[3]=&m; /* 恢复数组元素的初值 */
t=*pa[0]; *pa[0]=*pa[3]; *pa[3]=t; /* 交换*pa[0]和*pa[3]的值 */
for(n=0; n<4; n++)
printf("%d ", *pa[n]);
printf("\n");
}
运行结果如下:
4 2 3 1
4 2 3 1
说明:
(1)与例 8-15 类似,对指针数组的元素赋值后,它们指向整型变量(如图 8-19 所示) 。
(2)交换 pa[0]和 pa[3]的值后,pa[0]指向 m,pa[3]指向 i(如图 8-20(a)所示)。
(3)恢复数组元素的初值后,数组元素以及指向的变量如图 8-19 所示。
(4)交换*pa[0]和*pa[3]的值,就是交换 pa[0]所指向变量和 pa[3]所指向变量的值,即交
换变量 i 和 m 的值。此时,数组元素的值并没有改变(如图 8-20(b)所示)。

1 i *pa[3] 4 i *pa[0]
pa pa
pa[0] 2 j *pa[1] pa[0] 2 j *pa[1]
pa[1] pa[1]
pa[2] 3 k *pa[2] pa[2] 3 k *pa[2]
pa[3] pa[3]
4 m *pa[0] 1 m *pa[3]
(a) 交换*pa[0]和*pa[3]的值后 (b) 交换pa[0]和pa[3]的值后
图 8-20 指针数组示意

– 182 –
第8章 指针

由此可见,对指针数组元素的操作和对同类型指针变量的操作相同。我们可以对数组元
素赋值,以及引用数组元素的值;还可以间接访问它所指向的变量,改变或引用该变量的值。

8.4.2 指向指针的指针
在 8.2 节中曾指出,指针和数组之间有着密切的联系,用数组下标能完成的操作也能用
指针完成。指针数组也同样,以例 8-15 定义的指针数组 pa 为例:
int *pa[4];
用指针变量 pp 接受数组首元素的地址,即:
pp=pa; /* 或 pp=&pa[0]; */
则 pp 指向数组 pa 的首元素 pa[0],*pp 和 pa[0]代表同一个存储单元(如图 8-21 所示)。由于
pa[0]指向变量 i,故*pp 也指向变量 i,因此,i、*pa[0]和**pp 都代表同一个单元。其中**pp
等价于*(*pp),代表*pp 所指向的变量。

1 i *pa[0] **pp

pp pa
*pp
pa[0] 2 j *pa[1]

pa[1]

pa[2] 3 k *pa[2]

pa[3]
4 m *pa[3]

图 8-21 指针数组和二级指针示意

如果将 pp 自增 1,它指向 pa[1],这样,*pp 和 pa[1]代表同一个存储单元,故 j、*pa[1]


和**pp 也代表同一个单元。
例 8-15 中的程序段 1 用指针实现:
for(pp=pa; pp<pa+4; pp++)
printf("%d ", **pp);
例 8-15 中的程序段 2 用指针实现:
for(pp=pa; pp<pa+4; pp++)
printf("%x ", *pp);
我们继续讨论指针变量 pp 的类型,显然,pp 不是整型指针,因为它指向的不是一个整
型变量,在它指向的单元中,存放着一个整型变量的地址,即 pp 所指向单元的类型是整型指
针。换句话说,pp 指向一个整型指针变量,是一个指向指针的指针。
C 语言中,指向指针的指针一般定义为:
类型名 ** 变量名
例如:
int * p;

– 183 –
C 语言程序设计

int ** pp;
定义了指针变量 p 和 pp。p 是整型指针(一级指针),pp 是指向指针的指针(二级指针)。
与一级指针相比,二级指针的概念较难理解,它的运算也更复杂。例如:
int a=10;
int *p=&a;
int **pp=&p;
定义了 3 个变量 a、p 和 pp 并初始化。当二级指针 pp 指向指针 p,并且 p 指向 a 时(如图 8-22
所示) ,由于 p 指向 a,所以 p 和&a 的值一样,a 和*p 代表同一个单元;由于 pp 指向 p,所
以 pp 和&p 的值一样,p 和*pp 代表同一个单元;由此可知 pp、&p 和&&a 一样,p、&a 和
*pp 等价,a、*p 和**pp 代表同一个单元,故 a、*p 和**p 的值始终相同。
pp p a
&p &a 10 *p
&&a *pp **pp

图 8-22 二级指针示意

例 8-17 二级指针示例。
源程序:
#include<stdio.h>
void main( )
{
int a=10, *p=&a, **pp=&p;

printf("a=%d, *p=%d, **pp=%d\n",a, *p, **pp);


*p = 20;
printf("a=%d, *p=%d, **pp=%d\n",a, *p, **pp);
**pp = 30;
printf("a=%d, *p=%d, **pp=%d\n",a, *p, **pp);
}
运行结果如下:
a=10, *p=10, **pp=10
a=20, *p=20, **pp=20
a=30, *p=30, **pp=30
再看一个更复杂的二级指针的示例。
例 8-18 对如下变量定义和初始化,依次执行操作(1)~(3)后,部分变量的值如表
8-1 所示,请分析原因。
int a=10, b=20, t;
int *pa=&a, *pb=&b, *pt;
int **ppa=&pa, **ppb=&pb, **ppt;
操作(1):ppt=ppb; ppb=ppa; ppa=ppt;
操作(2):pt=pb; pb=pa; pa=pt;
操作(3):t=b; b=a; a=t;
– 184 –
第8章 指针

表 8-1 对变量定义和初始化并执行 3 次操作后的值

操作(行) **ppa **ppb *pa *pb a b

(0) 10 20 10 20 10 20

(1) 20 10 10 20 10 20

(2) 10 20 20 10 10 20

(3) 20 10 10 20 20 10

分析:由于二级指针 ppa 指向指针 pa,且 pa 指向 a,所以**ppa、*pa 和 a 三者等价;同


理,**ppb、*pb 和 b 三者等价(如图 8-23(a)所示)
,相应变量的初值见表 8-1 第(0)行。

ppa pa a ppa pa a
10 *pa 10 *ppb
**ppa **ppb

ppb pb b ppb pb b
20 *pb 20 *ppa
**ppb **ppa
(a) (b) 交换ppa和ppb的值后
ppa pa a ppa pa a
10 *pb 20 *pb
**ppa **ppa

ppb pb b ppb pb b
20 *pa 10 *pa
**ppb **ppb
(c) 交换pa和pb的值后 (d) 交换a和b的值后

图 8-23 二级指针运算示意

执行操作(1) ,交换 ppa 和 ppb 的值后,ppa 指向 pb,ppb 指向 pa,所以**ppa、*pb 和


b 三者等价;**ppb、*pa 和 a 三者等价(如图 8-23(b)所示) ,相应变量的值见表 8-1 第(1)
行。
再执行操作(2) ,交换 pa 和 pb 的值后,pa 指向 b,pb 指向 a,所以**ppa、*pb 和 a 三者
等价;**ppb、*pa 和 b 三者等价(如图 8-23(c)所示) ,相应变量的值见表 8-1 第(2)行。
最后执行操作(3) ,交换 a 和 b 的值。由于指针的值都没有改变,所以等价关系没有改
变(如图 8-23(d)所示),相应变量的值见表 8-1 第(3)行。
与一级指针相比,二级指针的应用更加灵活,也更难理解。虽然从理论上说,可以定义
任意多级的指针,如三级指针、四级指针等,但实际应用中多级指针的用法很少会超过二级。
级数过多的指针容易造成理解错误,使程序阅读困难。

– 185 –
C 语言程序设计

8.4.3 指针数组、二维字符数组和字符串
在 8.2 节中曾指出,C 语言使用一维字符数组和字符指针处理字符串。如果要处理多个
字符串,通常使用二维字符数组和指针数组。例如:
char cname[ ][6]={"Wang", "Li", "Zhang", "Jin", "Xian"};
char *pname[ ]={"Wang", "Li", "Zhang", "Jin", "Xian"};
定义了两个数组。cname 是二维字符数组,5 行 6 列共 30 个元素,每一行存放一个字符串(如
图 8-24(a)所示),共占用 30 个存储单元,每个元素的类型是字符型;pname 是指针数组,
有 5 个元素,占用 5 个存储单元,每个元素的类型是字符指针,指向一个字符串(如图 8-24
(b)所示) 。

cname pname
W a n g \0 pname[0] W a n g \0
L i \0 pname[1] L i \0
Z h a n g \0 pname[2] Z h a n g \0
J i n \0 pname[3] J i n \0
X i a n \0 pname[4] X i a n \0

(a) 二维字符数组 (b) 指针数组

图 8-24 用二维字符数组和指针数组表示多个字符串示意

定义二维字符数组时必须指定列长度,该长度要大于最长的字符串的有效长度,由于各
个字符串的长度一般并不相同,就会造成内存单元的浪费。而指针数组并不存放字符串,仅
仅用数组元素指向各个字符串,就没有类似的问题。

1.用指针数组处理多个字符串

指针数组最常见的用途就是处理多个不同长度的字符串。以上面定义的指针数组 pname
为例,它的首元素 pname[0]是字符指针,指向字符串"Wang",即指向该字符串的首字符 W,
语句
printf("%s", pname[0]);
输出该字符串。同理,语句
printf("%s", pname[2]);
输出 pname[2]所指向的字符串"Zhang"。
例 8-19 输入月份,输出对应的英文名称。例如,输入 5,输出 May。
分析:定义一个有 13 个元素的指针数组 month_name,首元素指向一个空字符串,其余
元 素 依 次 指向 一 个 英文月 份 的 名 称, month_name[1]指 向 "January", month_name[2]指 向
"February",……,month_name[12]指向"December"。
源程序:
#include<stdio.h>
void main( )

– 186 –
第8章 指针

{
int month;
char *month_name[ ]={
"", "January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"
};

printf("Enter month: \n");


scanf("%d",&month);
if(month>=1 && month<=12)
printf("%s\n", month_name[month]);
else
printf("Illegal month");
}
运行结果如下:
Enter month:
9<CR>
September
例 8-20 将 5 个字符串从小到大排序后输出。
分析:为了让读者更好地理解指针数组的应用特点,与例 8-13 相似,我们用对比的方法
编写程序。先写程序 B,它的功能是:用冒泡法将 5 个整数从小到大排序后输出(算法说明
见例 8-11) 。显然,这两个程序的算法相似,但需要处理的数据类型不同,程序 B 用整型数
组 a 来存放 5 个数,程序 A 就定义了一个有 5 个元素的指针数组 name,每个元素指向一个
字符串。其次,数组名作为函数的实参时,相应的形参也写成数组的形式。此外,整数可以
直接比较大小,而字符串的比较就调用了函数 strcmp()。
源程序:
/* 程序 A */ /* 程序 B */
#include<stdio.h> #include<stdio.h>
#include<string.h>
void main( ) void main( )
{ {
int i; int i;
char *name[ ]={"Wang", "Li", int a[5]={6, 5, 2, 8, 1};
"Zhang", "Jin", "Xian"};
void fsort(char *name[ ], int n); void fsort(int a[ ], int n);

fsort(name,5); /* 调用函数 */ fsort(a,5); /* 调用函数 */


for(i=0; i<5; i++) for(i=0; i<5; i++)
printf("%s ", name[i]); printf("%d ", a[i]);
} }
void fsort(char *name[ ], int n) void fsort(int a[ ], int n)
{ {

– 187 –
C 语言程序设计

int k, j; int k, j;
char *temp; int temp;

for(k=1; k<n; k++) for(k=1; k<n; k++=


for(j=0; j<n-k; j++) for(j=0; j<n-k; j++)
if(strcmp(name[j],name[j+1])>0){ if(a[j]>a[j+1]){
temp=name[j]; temp=a[j];
name[j]=name[j+1]; a[j]=a[j+1];
name[j+1]=temp; a[j+1]=temp;
} }
} }
运行结果如下: 运行结果如下:
Jin Li Wang Xian Zhang 12568

说明:在源程序 A 的排序函数中,比较指针数组的元素所指向字符串的大小,需要交换
时,直接交换数组元素的值,即改变它们的指向。
排序前数组元素的指向见图 8-24(b),排序后元素的指向见图 8-25(a)。

pname cname
pname[0] W a n g \0 J i n \0 cname[0]
pname[1] L i \0 L i \0 cname[1]
pname[2] Z h a n g \0 W a n g \0 cname[2]
pname[3] J i n \0 X i a n \0 cname[3]
pname[0] X i a n \0 Z h a n g \0 cname[4]

(a) 指针数组 (b) 二维字符数组

图 8-25 多个字符串排序示意

2.用二维字符数组处理多个字符串

在本小节开始处,我们定义了 5 行 6 列的二维字符数组 cname,用于存放 5 个字符串:


char cname[ ][6]={"Wang", "Li", "Zhang", "Jin", "Xian"};
二维数组实质上是一个一维数组的数组。以二维数组 cname 为例,它有 5 行,即 5 个元
素 cname[0]~cname[4](如图 8-26 所示),每个元素又是一个有 6 个元素的一维字符数组。
也就是说,cname[k](0≤k≤4,下同)是一个一维字符数组,它有 6 个元素,可以存放一个
字符串。
对字符串的操作就体现在对 cname[k]的操作上。例如,cname[0]存放了字符串"Wang",
语句
printf("%s", cname[0]);
输出该字符串。
必须指出,虽然指针数组和二维字符数组都可以处理多个字符串,但二者有明显的区别。
仍然以指针数组 pname 和二维字符数组 cname 为例,除了它们所占用存储单元数量的不同以
– 188 –
第8章 指针

外,pname[k]是字符指针变量,指向某个字符串,如果要改变它所代表的字符串,可以直接
改变它的值,让它指向一个新字符串;而 cname[k]是字符数组,实际存放着一个字符串,它
的值代表了二维字符数组 cname 第 k 行的起始地址,是常量,不能改变,如果要改变它所代
表的字符串,只能改变存储单元的内容。

cname
W a n g \0 cname[0]
L i \0 cname[1]
Z h a n g \0 cname[2]
J i n \0 cname[3]
X i a n \0 cname[4]

图 8-26 用二维字符数组表示多个字符串示意

可以看出,pname[k]和 cname[k]的区别,实质就是字符指针和字符数组的区别。
例 8-21 用二维字符数组重做例 8-20。
源程序:
#include<stdio.h>
#include<string.h>
void main( )
{
int i;
char cname[ ][6]={"Wang", "Li", "Zhang", "Jin", "Xian"};
void fsort(char cname[ ][6], int n);

fsort(cname,5); /* 调用函数 */
for(i=0; i<5; i++)
printf("%s ", cname[i]);
}
void fsort(char cname[ ][6], int n)
{
int k, j;
char temp[6];

for(k=1; k<n; k++)


for(j=0; j<n-k; j++)
if(strcmp(cname[j],cname[j+1])>0){
strcpy(temp, cname[j]);
strcpy(cname[j], cname[j+1]);
strcpy(cname[j+1], temp);
}
}

– 189 –
C 语言程序设计

运行结果如下:
Jin Li Wang Xian Zhang
说明:在排序函数中,比较 cname[j]和 cname[j+1]中存放的字符串的大小,需要交换时,用
strcpy()函数交换其中存放的内容。排序前 cname[k]的内容见图 8-26,排序后的内容见图 8-25(b)

请读者结合图 8-25,仔细分析例 8-20 和例 8-21,体会指针数组和二维字符数组的异
同。

3.对指针数组的进一步讨论

指针数组名和指针数组元素都代表地址,但它们的类型不同,即它们指向不同类型的存
储单元。
仍然以本小节开始处定义的指针数组 pname 为例:
char *pname[ ]={"Wang", "Li", "Zhang", "Jin", "Xian"};
数组名 pname 的类型是二级指针(char **),它代表了数组首元素 pname[0]的地址。由
于 pname 指向 pname[0],pname+k 就指向 pname[k](0≤k≤4,下同),*(pname+k)和 pname[k]
等价。例如,pname+2 指向 pname[2](见图 8-27),*(pname+2)和 pname[2]等价。
数组元素 pname[k]的类型是字符指针(char *),它指向一个字符串。由于 pname[k]指向
该字符串的首字符,则 pname[k]+j(j 是一个非负整数,下同)就指向首字符后的第 j 个字符,
*(pname[k]+j)代表该字符。例如,pname[0]指向字符串"Wang"的首字符 W,pname[0]+2 就指
向首字符’W’后的第 2 个字符’n’(如图 8-27 所示),*(pname[0])的值是字符 W,*(pname[0]+2)
的值是字符 n。

pname[0]+2

pname
pname pname[0] W a n g \0
pname[1] L i \0
pname+2 pname[2] Z h a n g \0
pname[3] J i n \0
pname[3] X i a n \0

图 8-27 用指针数组表示多个字符串示意

在 8.2.1 节中,介绍了下标运算符[ ],当 p 是指针或数组时,p[i]等价于*(p+i)。再次说明


了 pname[k]和*(pname+k)等价,存放它所指向字符串的首元素的地址。例如:
printf("%s", pname[2]);

printf("%s", *(pname+2));
输出 pname[2]所指向的字符串"Zhang"。而*(pname[k]+j)也可以写成*(*(pname+k)+j),与
pname[k][j]等价,存放字符。例如:
printf("%c %c", *(pname[0]), *(pname[0]+2));

– 190 –
第8章 指针

等价于
printf("%c %c", pname[0][0], pname[0][2]);
输出 pname[0]所指向字符串"Wang"的首字符’W’和其后的第 2 个字符’n’。
设 j 是整型变量,语句
for(j=0; *(pname[0]+j)!=’\0’; j++)
printf("%c", *(pname[0]+j));
将 pname[0]所指向的字符串"Wang"中的字符逐个输出。它和语句
printf("%s", pname[0]);
等价。
设 k, j 是整型变量,要输出 pname 所表示的 5 个字符串,可以用语句
for(k=0; k<5; i++)
printf("%s\n", pname[k]);

for(k=0; k<5; k++)
for(j=0; *(pname[k]+j)!=’\0’; j++) /* 输出 pname[k]所指向的字符串 */
printf("%c", *(pname[k]+j));
*(pname[k]+j)就是 pname[k]所指向字符串中首字符后的第 j 个字符。
请读者注意,pname[k]是指针数组的元素,它的类型是字符指针,指向一个字符串,也
就是指向字符串的首字符;而*(pname[k])代表 pname[k]所指向单元的内容,就是该字符串的
首字符。所以
printf("%s", pname[k]);
输出字符串。而
printf("%c", *(pname[k]));
输出字符。
例 8-22 指针数组和二级指针示例。
源程序:
#include<stdio.h>
#include<string.h>
void main( )
{
char *pname[ ]={"Wang", "Li", "Zhang", "Jin", "Xian"};
char **pp=pname; /* 二级指针初始化 */

printf("%s %c %c %s\n", *pp, **pp, *(*pp+2), *(pp+2));


for(pp=pname; pp<pname+5; pp++)
printf("%s ", *pp);
}
运行结果如下:
Wang W n Zhang
Wang Li Zhang Jin Xian
说明:二级指针 pp 接受指针数组 pname 首元素的地址,指向 pname[0](如图 8-28 所示)。

– 191 –
C 语言程序设计

此时,*pp 和 pname[0]等价,指向字符串"Wang"的首字符’W’,*pp+2 指向首字符’W’后的第 2


个字符’n’,故**pp 的值是字符’W’,*(*pp+2)的值是字符’n’;而 pp+2 指向 pname[2],*(pp+2)
和 pname[2]等价,指向字符串"Zhang"的首字符’Z’。在循环语句中,pp 依次指向指针数组的
每个元素,*pp 就和相应的数组元素等价,指向字符串。

*pp *pp+2

pname
pp pname[0] W a n g \0
pname[1] L i \0
pp+2 pname[2] Z h a n g \0
pname[3] J i n \0
pname[3] X i a n \0

图 8-28 指针数组和二级指针示意

本例中,pp 是二级指针,指向指针数组的元素;*pp 是字符指针,指向字符串的首字符;


**pp 才是字符。

8.4.4 命令行参数
我们在 1.3 节中介绍了如何运行 C 语言程序,C 语言源程序经编译和连接处理,生成可
执行程序后,才能运行。可执行程序又称为可执行文件或命令。
例如,test.C 是一个简单的 C 语言源程序:
#include<stdio.h>
void main( )
{
printf("Hello");
}
源程序 test.C 经编译和连接后,生成可执行程序 test.EXE,它可以直接在操作系统环境下以
命令方式运行。例如,在 DOS 环境的命令窗口中,输入可执行文件名(假设 test.EXE 放在
DOS 的当前目录下) :
test
作为命令,就以命令方式运行该程序。
输入命令时,在可执行文件(命令)名的后面可以跟一些参数,也就是说,在一个命令
行中可以包括命令和参数,这些参数被称为命令行参数。例如,输入
test world
运行程序。其中 test 是命令名,而 world 就是命令行参数。
命令行的一般形式为:
命令名 参数 1 参数 2 …… 参数 n
命令名和各个参数之间用空格分隔,也可以没有参数。

– 192 –
第8章 指针

用命令行的方式运行可执行文件 test.EXE 时,命令名后是否有参数并不影响程序的运行


结果。即
test

test world
的运行结果相同。因为参数 world 并没有被程序 test 接收。
在 C 语言程序中,主函数 main( )可以有两个参数,用于接收命令行参数。带有参数的
main( )函数习惯书写为:
main(int argc, char *argv[ ])
{
......
}
argc 和 argv 就是 main()函数的形参。用命令行的方式运行程序时,main()函数被调用,与命
令行有关的信息作为实参传递给两个形参。
第一个参数 argc 接收命令行参数(包括命令)的个数;第二个参数 argv 接收以字符串
常量形式存放的命令行参数(包括命令) 。用字符指针数组 argv 表示各个命令行参数(包括
命令) ,其中 argv[0]指向命令,argv[1]指向第一个命令行参数,argv[2] 指向第二个命令行参
数,……,argv[argc−1]指向最后一个命令行参数。
改写 test.C:
#include<stdio.h>
void main(int argc, char *argv[ ])
{
printf("Hello");
printf("%s", argv[1]);
}
编译和连接后,用命令行方式运行:
test world
输出:
Hello world
此时,argc 的值是 2,argv 的两个元素分别指向命令"test"和第一个命令行参数"world"(如
图 8-29 所示)。

argv argv[0] "test"

argv[1] "world"

图 8-29 命令行参数示意

例 8-23 编写 C 语言程序 echo,它的功能是将所有命令行参数在同一行上输出。


分析:题目要求回显所有的命令行参数,不包括命令。由于 argv[0]指向命令,故回显从
第一个命令行参数 argv[1]开始到最后一个命令行参数 argv[argc−1]结束。源程序保存在 echo.C
中。

– 193 –
C 语言程序设计

源程序:
#include<stdio.h>
#include<string.h>
void main(int argc, char *argv[ ])
{
int k;
for(k=1; k<argc; k++) /* 从第一个命令行参数开始 */
printf("%s ", argv[k]); /* 打印命令行参数 */
printf("\n");
}
输入命令行:
echo How are you?
输出:
How are you?
此时,argc 的值是 4,argv 的内容见图 8-30(a)。
如果输入命令行:
echo Hello world
输出:
Hello world
此时,argc 的值是 3,argv 的内容见图 8-30(b)。

argv "echo" argv "echo"


argv[0] argv[0]
"How" "Hello"
argv[1] argv[1]
"are" "world"
argv[2] argv[2]
"you?"
argv[3]
(a) (b)

图 8-30 命令行参数示意

由于 argv 是 main()函数的形参,尽管定义时一般都写成数组的形式,它实质上还是指针,
在程序中可以直接改变 argv 的值。
echo.C 中的循环也可以写成:
for(k=1, argv++; k<argc; k++)
printf("%s ", *(argv++));
argv 依次指向存放着命令行参数首地址的单元,*argv 就指向相应的命令行参数。
用命令行方式运行程序时,系统根据输入的命令行参数的数量和长度,自动分配存储空
间存放这些参数(包括命令) ,并将参数(包括命令)的数量和首地址传递给 main()函数中定
义的形参 argc 和 argv。
main()函数中的形参允许用任意合法的标识符来命名,但一般习惯使用 argc 和 argv。
有些 C 语言的集成环境提供了命令行参数的运行途径,如 TC,具体的使用方式请读者
查看编译系统的使用手册。在本书的附录 A 中,简要说明了在 TC 和 VC++下使用命令行参

– 194 –
第8章 指针

数的方法。

8.5 指针和函数

8.5.1 指针作为函数的返回值
在 C 语言中,函数返回值的类型除了整型、字符型和浮点型,也可以是指针,即函数可
以返回一个地址。定义和调用这类函数的方法与其他函数一样。
例 8-24 输入一个字符串和一个字符,如果该字符在字符串中,就从该字符首次出现的
位置开始输出字符串中的字符。例如,输入字符 r 和字符串 program 后,输出 rogram。要求
定义函数 match(s, ch),在字符串 s 中查找字符 ch,如果找到,返回第一次找到的该字符在字
符串中的位置(地址) ;否则,返回空指针 NULL。
分析:函数 match(s, ch)返回一个地址,所以函数返回值的类型是指针。
源程序:
#include<stdio.h>
char *match(char *s, char ch) /* 函数返回值的类型是字符指针 */
{
while(*s != ’\0’)
if(*s == ch) return(s); /* 若在字符串 s 中找到字符 ch,返回相应的地址 */
else s++;
return(NULL); /* 在 s 中没有找到 ch,返回空指针 */
}
void main( )
{
char ch, str[80], *p=NULL;
scanf("%s", str);
getchar(); /* 跳过输入字符串和输入字符之间的分隔符 */
ch=getchar();
if((p=match(str, ch))!=NULL)
printf("%s\n", p);
else
printf("Not Found\n");
}
运行结果 1:
university v<CR>
versity
运行结果 2:
school o<CR>
ool
说明:在 main()函数中,用字符指针 p 接收 match()返回的地址,如果 p 非空,调用 printf()

– 195 –
C 语言程序设计

以%s 的格式输出 p。这样,从 p 指向的存储单元开始,连续输出其中的内容,直至遇到字符


串结束符’\0’为止(参见 8.3.2 节)。
例 8-25 自定义函数 mystrcpy(char *s, char *t),将字符串 t 复制到 s,并返回 s 的值。
分析:与例 8-14 中定义的函数 strcpy()相比,要求函数返回 s 的值。由于 s 是字符指针,
所以函数返回值的类型是指针。
函数:
char *mystrcpy(char *s, char *t) /* 函数返回值的类型是字符指针 */
{
char *ss=s; /* 用指针 ss 保存指针 s 的初值 */

while(*s++ = *t++);
return ss;
}
说明:由于在函数中改变了指针 s 的值,又定义了一个指针 ss 保存 s 的初值。
请读者自己编写主函数调用 mystrcpy(),实现字符串的复制。

8.5.2 指向函数的指针
在 C 语言中,函数名代表函数的入口地址。我们可以定义一个指针变量,接收函数的入
口地址,让它指向函数,这就是指向函数的指针,也称为函数指针。通过函数指针可以调用
函数,它还可以作为函数的参数。

1.函数指针的定义

函数指针定义的一般格式为:
类型名(*变量名)();
类型名指定函数返回值的类型,变量名是指向函数的指针变量的名称。例如:
int (*funptr)( );
定义一个函数指针 funptr,它指向一个返回值类型为 int 的函数。

2.通过函数指针调用函数

在使用函数指针前,要先对它赋值。赋值时,将一个函数名赋给函数指针,但该函数必
须已定义或说明,且函数返回值的类型和函数指针的类型要一致。
假设函数 fun(x, y)已定义,它返回一个整型量,则:
funptr = fun;
将函数 fun()的入口地址赋给 funptr,funptr 就指向函数 fun()。
调用函数有两种方法,直接用函数名或通过函数指针。例如,调用上述函数 fun(),可以
写成:
fun(3, 5);

(*funptr)(3, 5);
通过函数指针调用函数的一般格式为:
– 196 –
第8章 指针

(*函数指针名) (参数表)
例 8-26 函数指针示例。
源程序:
#include<stdio.h>
void main( )
{
int r1, r2, r3, r4;
int (*fptr)( ); /* 定义函数指针 */

int odd(int x), even(int x); /* 函数原型说明 */

fptr = odd; /* 将函数 odd 的入口地址赋给函数指针 */


r1=odd(5); /* 通过函数名调用函数 odd */
r2=(*fptr)(5); /* 通过函数指针调用函数 odd */
printf("%d ,%d\n", r1, r2);

fptr = even; /* 将函数 even 的入口地址赋给函数指针 */


r3=even(5); /* 通过函数名调用函数 even */
r4=(*fptr)(5); /* 通过函数指针调用函数 even */
printf("%d ,%d\n", r3, r4);
}
int odd(int x)
{
return x%2 != 0;
}
int even(int x)
{
return x%2 == 0;
}
运行结果如下:
1, 1
0, 0
说明:当函数指针分别指向函数 odd()和 even()时,通过函数名调用函数和通过函数指针
调用函数是一样的。

3.函数指针作为函数的参数

C 语言的函数调用中,函数名或已赋值的函数指针也能作为实参,此时,形参就是函数
指针,它指向实参所代表函数的入口地址。
例 8-27 编写一个函数 calc(f, a, b),用梯形公式求函数 f(x)在[a, b]上的数值积分。
a
∫b
f(x)dx=(b−a)/2*(f(a)+f(b))

然后调用该函数计算下列数值积分。
– 197 –
C 语言程序设计

1 2 1
∫ x dx ∫ sinx/x dx ∫ x/(x +1)dx
2 2
0 1 0

分析:calc()是一个通用函数,用梯形公式求解数值积分。它和被积函数 f(x)以及积分
区间[a, b]有关,把它们都作为函数 calc()的参数。在函数调用时,把被积函数的名称和积分
区间的上下限作为实参,形参包括函数指针,用于接收实参函数名。
源程序:
#include<stdio.h>
double calc(double (*funp)(), double a, double b); /*函数原型说明 */
double f1(double x), f2(double x), f3(double x);
void main()
{
double a, b, result;
double (*funp1)( );

result=calc(f1, 0.0, 1.0); /* 函数名 f1 作为函数 calc 的实参 */


printf("1: resule=%.4f\n", result);
result=calc(f2, 1.0, 2.0); /* 函数名 f2 作为函数 calc 的实参 */
printf("2: resule=%.4f\n", result);
funp1=f3; /* 对函数指针 ftp 赋值 */
result=calc(funp1, 0.0, 1.0); /* 函数指针 ftp 作为函数 calc 的实参 */
printf("3: resule=%.4f\n", result);
}
double calc(double (*funp)(), double a, double b) /* 函数指针 funp 作为函数的形参 */
{
double z;
z=(b-a)/2*((*funp)(a)+(*funp)(b)); /* 调用 funp 指向的函数 */
return(z);
}
double f1(double x)
{
return(x*x);
}
double f2(double x)
{
return(sin(x)/x);
}
double f3(double x)
{
return(x/( x*x+1));
}
运行结果如下:
1: resule=0.5000

– 198 –
第8章 指针

2: resule=12280.0000
3: resule=0.2500
说明:函数 calc()的通用性较好,可以用梯形公式求解不同函数的数值积分。
函数指针是一个比较高深的概念,这里只介绍了一些基本的概念和用法,作为读者进一
步学习的基础。

习 题

1.选择题
(1)若 p1 和 p2 都是整型指针,p1 已经指向变量 x,要使 p2 也指向 x,_____是正确的。
A.p2=p1; B.p2=**p1; C.p2=&p1; D.p2=*p1;
(2)若变量已正确定义并且指针 p 已经指向变量 x,则&*p 相当于_____。
A.x B.*p C.p D.*&x
(3)下列程序段的输出是___________。
int c[]={1, 7, 12};
int *k;
k=c;
printf("next k is %d",*++k);
A.2 B.7 C.1 D.以上均不对
(4)不正确的赋值或赋初值的方式是______。
A.char str[ ]="string"; B.char str[7]={’s’, ’t’, ’r’, ’i’, ’n’, ’g’, ’\0’};
C.char str[10], *p="string"; D.char str[10]; str="string";
(5)对于以下变量定义,___________是正确的赋值。
int *p[3], a[3];
A.p=a B.*p=a[0] C.p=&a[0] D.p[0]=&a[0]
(6)下列程序的输出是___________。
void main()
{
int a[12]={1,2,3,4,5,6,7,8,9,10,11,12}, *p[4], i;
for (i=0; i<4; i++)
p[i]=&a[i*3];
printf("%d\n", p[3][2]);
}
A.上述程序有错误 B.6 C.8 D.12
2.写出下列程序(段)的输出结果。
(1)
#include<stdio.h>
int z;
void p(int *x,int y)
{ ++ *x;
y --;
– 199 –
C 语言程序设计

z = *x+y;
printf("%d,%d,%d#",*x, y, z);
}
void main()
{ int x=2, y=3, z=4;
p(&x, y);
printf("%d,%d,%d#",x, y, z);
}
(2)
static char s[ ]="student";
printf("%s\n", s+2);
(3)
#include<stdio.h>
void main()
{
char *pa,*s;
int i,n;

pa = "HappyNewYear";
for(s=pa,n=0; *s!=’\0’; ++s,++n);
for(s=pa,i=0; i<n; putchar(*s++),++i);
}
(4)
char *st[ ]={"one","two","three","four"};
printf("%s\n",*(st+3)+1);
(5)假设下列程序保存在 test.C 中,编译后运行 test hello world。
#include<stdio.h>
main(int agrc, char *argv[])
{
printf("%d %s", argc, argv[1]+1);
}
3.程序(段)填空
(1)判断字符 c 是否在字符串 s 中出现的函数如下。
int f(char *s, char c)
{ int i;
for(______)
if (c==s[i]) break;
return (c==s[i]);
}
(2)下列程序将字符串 s 逆序输出,如 f("abcd"),将输出"dcba"。
void f(_______)
{ int i=0;

– 200 –
第8章 指针

while(s[i]) i++;
for ( ______) printf("%c", s[i]) ;
}
4.定义一个函数 search(int list[],int n,int x),在数组 list 中查找元素 x,若找到则返回
相应下标,否则返回−1。在 main()函数中调用 search(),main()函数如下:
void main()
{
int i, x, a[10], res;
for(i=0; i<10; i++)
scanf("%d", &a[i]);
scanf("%d", &x);
res = search(a, 10, x);
printf("%d ", res);
}
5.定义函数 void sort(int a[],int n),用选择法对数组 a 中的元素排序。自己定义 main()
函数,并在其中调用 sort()函数。
6.编写程序,输入 5 个字符串,输出其中最长的字符串。
7.编写一个函数 delchar(s,c),该函数将字符串 s 中出现的所有 c 字符删除。自己定义
main()函数,并在其中调用 delchar(s,c)函数。
8.分别用字符数组和字符指针定义函数 strmcpy(s,t,m),将字符串 t 中从第 m 个字符开
始的全部字符复制到字符串 s 中去。
9.分别用字符数组和字符指针实现函数 strcat(s,t),将字符串 t 接到字符串 s 的后面,并
且返回字符串 s 的首地址。
10.编写程序,将 5 个字符串从大到小排序后输出。
11.使用命令行参数编写程序,将一个正整数转换为十六进制数后输出。
12.指出下列定义中 p 的含义:
int *p;
int *p[];
int **p;
int *p();
int (*p) ();

– 201 –
第9章 结 构
结构是一种数据类型,它与数组一样都是构造数据类型。它与数组的区别仅在于数组的
所有元素必须是相同类型的,而结构的元素可以是不同的数据类型。除此之外,两者都很相
似。因此,学习结构时要与数组进行类比,这样易于掌握。
本章主要介绍结构的基本概念、结构和数组、结构和指针的关系,以及使用结构指针构
造链表。本章还介绍了联合、枚举、自定义类型和位运算等。

9.1 结构的概念

9.1.1 结构的定义
C 语言中的结构是一个变量集合,变量集合中的元素称作结构分量或结构成员,在本书
中,我们统一称为结构分量。
结构是编程者自定义的新数据类型,它的定义格式较数组复杂。结构类型定义的一般形
式为:
struct 结构名 {
类型名 结构分量名 1;
类型名 结构分量名 2;

类型名 结构分量名 n;
};
struct 是结构类型的关键字;结构名应是一个合法的标识符,用于标识所定义的结构;类
型名指定结构分量的类型,它们必须是有效的数据类型;结构分量名也应是合法的标识符,
用于标识结构分量。struct 和结构名组合在一起,称为结构类型名。结构分量可以有多个,每
个结构分量对应于一个结构分量名以及指定的类型名,而且结构分量的类型可以不同。与数
组相比,结构是由不同类型数据组成的数据结构,它提供一个方便的手段将不同类型的相关
信息组织在一起。
例如,定义一个关于学生基本情况的结构,该结构由学号、姓名、成绩 3 个要素组成:
struct student {
int num;
char name[20];
int score;
};

– 202 –
第9章 结构

其中结构类型名为 struct student,它由结构分量 num、name 和 score 组成,每一分量又


有各自的数据类型,分别为 int、char 和 int。
注意:结构的定义以分号结束,这是因为结构的定义是一条语句。结构名 student 标识了
这一特殊的数据结构,同时 struct student 也由此成为结构类型名。
又如,平面上的任意一点都可以用 x 坐标和 y 坐标来共同确定:
struct point {
float x;
float y;
};
上例定义了一个结构类型名为 struct point 的结构,它由数据类型皆为实型的两个分量 x 和 y
组成。虽然 x 和 y 的类型相同,可以用两个元素的数组表示,但采用结构描述可以增加程序
的可读性,使程序更清晰。

9.1.2 结构变量的定义和引用

1.结构变量的定义

定义了结构类型后,还需要定义结构类型的变量。C 语言中,有三种定义结构变量的方法。
(1)先定义一个结构类型,再定义一个具有这种结构类型的结构变量。
定义了结构类型后,可以像使用其他基本数据类型(如 int、char)那样定义结构变量,
例如:
struct student stud1,stud2;
该语句定义了两个 struct student 型的结构变量 stud1 和 stud2,关键字 struct 和结构名 student
必须联合使用成为一个结构类型名。
C 语言自动为所有结构变量分配足够的存储单元。图 9-1 显示了结构变量 stud1 在内存中
的存储形式。

num name score


↓ ↓ ↓

201 Zhang Hong 92

图 9-1 结构变量的存储形式

(2)在定义结构类型的同时定义结构变量。
这种定义方法的一般形式为:
struct 结构名 {
类型名 结构分量名 1;
类型名 结构分量名 2;

类型名 结构分量名 n;
} 结构变量名表;

– 203 –
C 语言程序设计

结构变量 stud1,stud2 的类型定义也可以按如下形式定义:


struct student {
int num;
char name[20];
int score;
} stud1,stud2;
上述两种方法的实质是一样的,都是既定义了结构类型 struct student,也定义了结构变
量 stud1,stud2。
(3)在定义结构变量时省略结构名,也称之为无名结构类型定义。该方法采用如下形式:
struct {
类型名 结构分量名 1;
类型名 结构分量名 2;

类型名 结构分量名 n;
}结构变量名表;
例如,用这种方式定义结构变量 s1 和 s2:
struct {
int num;
char name[20];
int score;
} s1,s2;
这种方法没有定义一个特定的结构类型名,因此无法再定义此类型的其他结构变量,除
非把定义过程再写一遍。建议读者采用前两种方法定义结构变量。

2.结构变量的引用

结构变量代表的是分量的集合,和数组操作相似,结构一般是通过成员操作引用结构变
量的分量。
使用分量操作符“.”引用结构变量的分量,其一般格式为:
结构变量名. 分量名
如 stud1.name、stud1.score 分别表示结构变量 stud1 的分量 name、score。结构变量的分量的
使用方法与同类型的变量完全相同。
例如:
stud1.num = 201;
strcpy(stud1.name,"Zhang Hong");
stud1.score = 92;
分别对结构变量 stud1 的 3 个分量 stud1.num、stud1.name 和 stud1.score 进行赋值。又如:
scanf("%d %s %d ", &stud1.num, stud1.name, &stud1.score);
printf("%d %s %d \n", stud1.num, stud1.name, stud1.score);
从标准输入文件(键盘)上输入结构变量 stud1 的 3 个分量的相应数据,然后再把这 3 个分
量的内容输出到标准输出文件(屏幕)上。

– 204 –
第9章 结构

3.结构变量的初始化

对结构变量的初始化类似于对数组元素的初始化。标准 C 规定,能初始化的结构变量要
求是外部的或者静态的,但是许多高版本的 C 语言编译系统已经取消了这一限制。
结构变量的初始化采用初始表的方法,将花括号内的数据项对应的赋给结构变量的各个
成员,对应项要求类型一致,具体规定同数组元素赋初值相似。
例 9-1 对全局结构变量 stud1 进行初始化。
源程序:
struct student {
int num;
char name[20];
int score;
};
struct student stud1 = { 201,"Zhang Hong", 92};
main()
{
printf("%d %s %d \n", stud1.num, stud1.name, stud1.score);
}
在本例中,先定义结构类型 struct student,再在定义结构变量 stud1 的同时对其赋初
值。
例 9-2 对静态结构变量 stud1 和 stud2 进行初始化。
源程序:
main()
{
static struct student {
int num;
char name[20];
int score;
} stud1 = { 201,"Zhang Hong", 92},
stud2 = { 202,"Wang Fang", 80};
printf("%d %s %d \n", stud1.num, stud1.name, stud1.score);
printf("%d %s %d \n", stud2.num, stud2.name, stud2.score);
}
本例中,在定义结构类型 struct student 的同时定义结构变量 stud1 和 stud2,并对这两个
结构变量赋初值。

4.对结构变量赋值

虽然 C 语言规定,结构变量之间一般不能直接赋值,但如果两个结构变量具有相同的类
型,那么 C 编译系统允许将一个结构变量的值赋给另一个结构变量。这样,赋值号右边结构
变量的所有元素的值可以赋给左边结构变量的所有元素。
例 9-3 将结构变量 stud1 的值赋给结构变量 stud2。
– 205 –
C 语言程序设计

源程序:
#include <stdio.h>
#include <string.h>
main()
{
static struct student {
int num;
char name[20];
int score;
} stud1 = { 201,"Zhang Hong", 92}, stud2;
stud2 = stud1; /* 将结构变量 stud1 的值赋给结构变量 stud2*/
printf("%d %d ", stud2.num, stud2.score);
return 0;
}
运行结果如下:
201 92
其中,执行语句“stud2 = stud1;”等同于执行了下列语句:
stud2.num = stud1.num;
strcpy(stud2.name, stud1.name);
stud2. score = stud1. score;
注意:如果两个结构变量类型不同,那么即使它们具有相同的元素,也不能相互赋值。
除了两个相同类型结构变量之间能进行赋值外,一般情况下,对结构变量的操作主要是通过
分量来进行,而分量能实施的操作由分量本身类型决定。

9.1.3 结构的嵌套定义
一个较大的实体可能由多个成员构成,而这些成员中有些又有可能是由一些更小的成员
构成的实体。例如,一个学生的信息除了他(她)的学号、姓名及成绩等一些基本情况外,
还应包括其详细的家庭地址,而家庭地址又包括城市、街道、门牌号等信息项。显然,需要
用结构变量来表示学生的信息,同时也最好使用结构变量来记录家庭地址,此时可以用嵌套
的结构来定义。
嵌套的结构,即意味着结构的某些成员的数据类型也是结构。
例 9-4 若学生的信息构成如图 9-2 所示,请为其定义合适的数据类型。

学号 姓名 家庭地址 成绩
城市: 街道: 门牌号:

图 9-2 学生的信息构成

分析:可将家庭地址 addr 定义为结构类型 address,并使其成为结构类型 student 的成


员。
源程序:

– 206 –
第9章 结构

struct address {
char city[20];
char street[20];
int number;
};

struct student {
int num;
char name[20];
struct address addr;
int score;
};
struct student stud1;
在本例中,struct student 中的 addr 是结构类型,它又有自己的分量 city、street、number,
形成了结构的嵌套定义。
stud1. addr. number = 86;
意味着 stud1 的门牌号赋值为 86。由此可见,嵌套定义的结构变量中,每个分量按从左到右、
从外到内的方式引用。

9.2 结构数组

结构最普遍的应用是结构数组。在结构数组中,数组元素的类型是同一个结构类型。

9.2.1 结构数组的定义和引用
当有多个具有相同结构的概念或实体时,可用结构数组把它们组织起来。结构数组的定
义方法与基本数据类型的数组定义方法类似,只是结构数组中的每一分量的数据类型是一个
结构。因此,要定义一个结构数组,首先要定义一个结构类型,然后再把数组元素说明为这
种结构类型。
例 9-5 定义一个 10 个学生的结构数组。
假定每一个学生的数据结构为:
struct student {
int num;
char name[20];
int score;
};
则 10 个学生的结构数组可定义为:
struct student stud[10];
数组 stud 有 10 个元素,每个元素的类型是 struct student,并由 3 个分量组成。
对结构数组的定义,也可用如下方法:
struct student {

– 207 –
C 语言程序设计

int num;
char name[20];
int score;
} stud[10];
C 语言自动为所有结构数组元素分配足够的存储单元,结构数组的元素是连续存放的,
如图 9-3 所示(假设各元素已被赋值)

num name score


201 "Zhang Hong" 92 stud[0]
202 "Wang Fang" 80 stud[1]


0210 "Kang Fan" 80 stud[9]

图 9-3 结构数组的存储形式

由于每个结构数组元素的类型是结构,其使用方法和相同类型的结构变量一样,既可以
引用数组的元素,如 stud[0];也可以引用结构数组元素的分量,如 stud[0].num。像所有数组
一样,结构数组元素的下标也从 0 开始。对结构数组元素分量的引用是通过联合使用数组下
标和结构分量操作符“.”来完成的,其一般格式为:
结构数组名[下标]. 分量名
如 stud[0].name、stud[0].score 分别表示结构数组元素 stud[0]的分量 name 和 score。

9.2.2 结构数组的初始化
对结构数组也可以进行初始化。
例 9-6 初始化一个 10 个学生的结构数组。
源程序:
struct student {
int num;
char name[20];
int score;
};
struct student stud[10] ={{ 201,"Zhang Hong", 92},
{ 202,"Wang Fang", 80}};
对结构数组的初始化,也可用如下方法:
struct student {
int num;
char name[20];
int score;
} stud[10] ={{ 201,"Zhang Hong", 92},
{ 202,"Wang Fang", 80}};

– 208 –
第9章 结构


struct {
int num;
char name[20];
int score;
} stud[10] ={{ 201,"Zhang Hong", 92},
{ 202,"Wang Fang", 80}};
在初始化过程中,同样也要注意初始化值与各个结构分量的数据类型的匹配。
使用结构数组与使用其他数组一样,采用循环结构会带来很大的方便。
例 9-7 求 10 个学生的平均分数。
源程序:
#include <stdio.h>
#include <string.h>
struct student {
int num;
char name[20];
int score;
};
struct student stud[10];
main()
{
int i ,sum = 0 ;
for(i = 0; i < 10; i++){
scanf("%d%s%d", &stud[i].num,
stud[i].name, &stud[i].score);
sum += stud[i].score;
}
printf("aver = %d \n", sum/10);
}

9.3 结构指针

9.3.1 结构指针的概念和使用
结构指针就是指向结构类型变量的指针变量。
例如:
struct student {
int num;
char name[20];
int score;
} stud1 ={ 201,"Zhang Hong", 92},*ptr;
– 209 –
C 语言程序设计

ptr = &stud1;
结构指针 ptr 指向结构类型变量 stud1。由于一个结构类型的数据往往有多个分量组成,因而
结构指针指向结构变量中的第一个分量,如图 9-4 所示。

ptr
201 "Zhang Hong" 92

&stud1.num &stud1.name &stud1.score

图 9-4 结构指针指向结构类型变量

因此,让 ptr 指向 stud1,也可以写成:


ptr = &stud1.num;
通过结构指针可以访问它所指向的结构变量中的各个分量。
(1)用*ptr 访问结构分量。例如:
strcpy((*ptr).name, "Zhang Hong");
(*ptr). score= 92;
其中*ptr 表示的是 ptr 指向的结构变量,注意(*ptr)中的括号是不可少的,因为分量运算符“.”
的优先级高于“*”的优先级,若没有括号,就会发生混淆。例如:*ptr.name 等价于*(ptr.name)。
(2)用指向运算符“->”访问指针指向的结构分量。例如:
strcpy(ptr->name, "Zhang Hong");
ptr-> score = 92;
以上两种方法最终得到的效果是一样的,但在使用结构指针访问结构分量时,通常使用
指向运算符“->”访问结构分量。
假设 ptr 指向结构变量 stud1,则下面 3 个语句的效果是一样的。
stud1. score = 92;
等价于:
(*ptr). score = 92;

ptr-> score = 92;
例 9-8 用结构指针实现例 9-7。
源程序:
#include <stdio.h>
#include <string.h>
main()
{
struct student {
int num;
char name[20];
int score;
};

– 210 –
第9章 结构

struct student stud[10],*ptr; /* 定义结构数组和结构指针 */


int i ,sum = 0 ;
for(ptr = stud; ptr <= &stud[9]; ptr++){
scanf("%d%s%d", &ptr->num,
ptr->name, &ptr->score);
sum += ptr->score;
}
printf("aver = %d \n", sum/10);
}
说明:循环语句中的 ptr = stud;将结构指针 ptr 指向结构数组 stud 首元素 stud[0],ptr++
使结构指针 ptr 移向下一个数组元素。这样可以通过指针 ptr 来访问结构数组 stud 每个元素及
其各个分量。

9.3.2 结构指针作为函数的参数
结构指针作为函数的参数,可以完成比基本类型指针更为复杂的操作。
例 9-9 结构指针作为函数参数的示例。
源程序:
#include <stdio.h>
#include <conio.h>

struct time_struct { /* 定义全局结构 time_struct */


int hours;
int minutes;
int seconds;
};
void update(struct time_struct *t);
void display(struct time_struct *t);
void delay(void);

main()
{
struct time_struct time; /* 定义结构变量 time */

time.hours =time.minutes = time.seconds = 0;

while(!kbhit()){ /* 键盘按键监测 */
update(&time); /* 结构变量 time 的地址作为函数实参*/
display(&time);
}
return 0;
}

– 211 –
C 语言程序设计

void update(struct time_struct *t)


{
t->seconds++;
if(t->seconds == 60){
t->seconds = 0;
t->minutes++;
}
if(t->minutes == 60){
t->minutes = 0;
t->hours++;
}
if(t->hours == 24) t-> hours = 0;
delay();
}

void display (struct time_struct *t) /* 定义函数 */


{
printf("%d:",t->hours);
printf("%d:",t->minutes);
printf("%d:",t->seconds);
}

void delay(void)
{
long int t;
for(t=1;t<128000;++t);
}
说明:该程序是一个计时器程序,其主要功能是计时并在屏幕上显示时、分、秒。当按
键盘任意键(由函数 kbhit()监测,并返回结果)时,结束计时。该程序的计时通过改变 delay()
中的循环计数来调整。
程序中定义了全局结构类型 time_struct,并在 main()中定义结构变量 time,记录时、分、
秒,并将其初试化为 00∶00∶00。结构变量 time 的作用域仅局限在 main()中。
在本程序中,主调函数 main()把结构变量 time 的地址传送给函数 update()(用于更新时
间)及函数 display()(用于显示时间)
。在这两个函数中,形参以 time_struct 型结构指针接收
该结构变量的地址。对结构元素的引用是通过结构指针 t 完成的。在函数 update()中,通过改
变形参所指结构变量的值,达到了修改实参结构变量数值的目的,从而为下一次函数调用作
调整。
结构变量也可以做函数参数。对于函数 display(),实参也可以是结构变量 time(非变量
地址),形参用普通结构变量对应。当参数传递时,实参把结构中的每一个分量值传递给形参
结构的分量。如果结构参数包含的分量有很多,那么结构赋值需要花费一定时间。显然,用
结构指针进行传递的效率比值传递要高,因为指针值(地址)只有 2 个字节(或 4 个字节) 。

– 212 –
 9.4.1 单向链表的定义
9.4 单向链表

链表是一种常见而重要的动态存储分布的数据结构。它由若干个同一结构类型的“结点”
依次串接而成的。链表分单向链表和双向链表。在此,我们只介绍单向链表。
第9章 结构

单向链表的组成如图 9-5 所示。一般用指针 head 表示表头变量,用来存放链表首结点的


地址。链中每个结点由数据部分和下一个结点的地址部分组成,即每个结点都指向下一个结
点。链表中的最后一个结点称为表尾,其下一个结点的地址部分的值为 NULL(表示为空地
址)。链表的各个结点在内存中可能不是连续存放的,具体存放位置由系统分配。

head A B C D NULL

图 9-5 单向链表的组成示意

在用数组存放数据时,一般需要事先定义好数组长度,这样在数组元素个数不确定时,可
能会发生浪费内存空间的情况。比如利用数组来存放各系的学生,有的系有 500 名学生,而有
的系可能有 5000 名学生,为了能用统一的数组来表示,必须把数组定义得足够大(如 5000 个
元素大小),显然这是很浪费存储空间的。另外,当需要向已排好序的数组中添加新元素时,
操作起来很不方便,效率较低。由于链表的各个部分可以不连续存放,长度可以不加限定,并
可根据需要动态地开辟内存空间,还可以比较自由方便地插入新元素(结点),因此使用链表
可以节省内存,并提高操作效率。

9.4.2 单向链表的常用操作
在这一节中,我们将学习单向链表的建立、遍历、插入与删除等常用操作。
首先,我们通过使用结构的嵌套定义,来定义单向链表结点的数据类型。假设该链表用
来存储一系列学生记录,每一个学生记录(结点)定义为:
struct stud_node {
int num;
char name[20];
int score;
struct student_node *next;
};
结构类型 stud_node 中的 next 分量又是该结构类型的指针,称之为结构的递归定义。利
用这种定义方法,可构造出单向链表这一较为复杂的数据结构。
在由 stud_node 构成的单向链表中,每一结点均有 4 个分量组成,其中第 4 个分量 next
是一个结构指针,它指向链表中的下一个结点(即存放了下一个结点的地址) 。每一结点的第
4 个分量总是指向具有相同结构的结点,所以要用递归结构定义方法来定义。

– 213 –
C 语言程序设计

链表是一种动态存储分配的数据结构,在进行动态存储分配的操作中,C 语言提供了下
面几个常用的函数。
(1)void *malloc(unsigned size)
功能:在内存的动态存储区中分配一块连续空间,其长度为 size。若申请成功,则返回
一个指向所分配内存空间的起始地址的指针;若不成功,则返回 NULL(值为 0)。
(2)void *calloc( unsigned n, unsigned size)
功能:在内存的动态存储区中分配 n 个连续空间,每个连续空间的长度为 size。若申请成
功,则返回一个指向被分配内存空间的起始地址的指针;若不成功,则返回 NULL(值为 0)。
(3)void free(void *ptr)
功能:释放由函数 malloc()或 calloc()动态申请的整块内存空间,ptr 为指向要释放空间的
首地址。无返回值。
注意:类型 void 表示一种不确定的类型,使 void 指针具有一般性,可以指向任何类型
的数据,但在真正申请动态空间时,需要进行强制类型转换,按照要求明确它的类型,便于
操作时保持类型一致。
例如,要申请大小为 struct stud_node 结构的动态内存空间,新申请到的空间要被强制类
型转换成 struct stud_node 的指针,并保存到指针变量 p 上。经过强制类型转换,使得赋值号
两边的类型一致。这一过程由下面语句实现:
struct stud_node *p;
p = (struct stude_node *) malloc(sizeof(struct stud_node));
若申请成功,p 指向被分配内存空间的起始地址;若未申请到内存空间,则 p=NULL。
在编程中,若使用了 NULL,必须在程序头上指定:
#include <stdio.h>
因为系统在头文件 stdio.h 中,定义了 NULL 的值为 0。
例 9-10 编写用单向链表建立一个学校的学生档案的函数,从键盘输入每个学生的学号、
姓名和成绩。当输入学号为 0 时,输入结束并返回链表的头指针。
分析:链表中每个结点的数据类型为 struct stud_node。设单向链表的表头指针为 head,
尾指针为 tail,它们的初始值为 NULL。head 总是指向第一个结点,tail 总是指向链表中的最
后一个结点,一旦链表中有新的结点加入,把它添加到表尾。由于每增加一个结点,就要申
请一个动态存储空间,以便存放相应的数据,因此引入一个临时指针变量 p。
struct stud_node * head,* tail,* p;
新申请到的空间要被强制类型转换成 struct stud_node 的指针,
要申请空间的大小为 struct
stud_node 结构体的大小,这一过程由下面语句实现。
int size = sizeof(struct stud_node) ;
p = (struct stud_node *) malloc(size);
每个新增加的结点总是加在链表的末尾,所以该新增结点的 next 域应置成 NULL。
p->next= NULL;
并把原来链表的尾结点的 next 域指向该新增的结点,这样就把新增加的结点加入到了链表之
中。
tail->next=p;

– 214 –
第9章 结构

tail =p;
应该注意的是,建立链表的第一个结点时,整个链表是空的(head = NULL),这时 p 应
直接赋值给 head,而不是 tail->next,因为 tail 此时还没有结点可指向,即
head = p;
本例的具体算法如图 9-6 所示。

head = tail = NULL


Input num,name and score

while ( num != 0 )

p = (struct student *) malloc(size);


p->num = num;
strcpy(p->name, name);
p->score = score;

head == NULL

Y N

head = p tail->next=p

tail = p

Input num,name and score

图 9-6 链表建立算法的 N-S 图

源程序:
/* Function for creating students’ document */

struct stud_node *Creat_Stu_Doc()


{
struct stud_node * head,* tail,* p;
int num,score;
char name[20];

int size = sizeof(struct stud_node) ;

head = tail = NULL;


printf("Input num,name and score:\n");
scanf("%d%s%d", &num,name, &score);

– 215 –
C 语言程序设计

while ( num != 0 ){
p = (struct stud_node *) malloc(size);
p->num = num;
strcpy(p->name, name);
p->score = score;
if(head == NULL)
head = p;
else
tail->next = p;
tail = p;
scanf("%d%s%d", &num, name, &score);
}
return head;
}
例 9-11 编写一个函数,把例 9-10 建立的链表内容显示在屏幕上。
分析:为了逐个显示链表每个结点的数据,程序要不断从例 9-10 所建立的链表中取结点
内容,显然是一个重复性的工作,需要循环来解决。根据题意,循环结束的标志是显示完尾
结点的内容。这里,我们考虑使用 for 语句,并设一个指针变量 ptr。由于要从链表的首结点
开始输出内容,所以在 for 语句中将 ptr 的初值置为表头 head,当 ptr 不为 NULL 时(未显示
完尾结点)循环继续,否则循环结束。
源程序:
/* Function for printing students’ document */

void Print_Stu_Doc (struct stud_node *head)


{
struct stud_node * ptr;

if(head == NULL){
printf("\nNo Records\n");
return ;
}
printf("\nThe Students’ Records Are: \n");
printf(" Num Name Score\n");
for(ptr = head; ptr; ptr = ptr->next)
printf("%8d %20s %6d \n", ptr->num, ptr->name, ptr->score);

}
说明:每次循环后的 ptr 值变成了下一结点的起始地址,即
ptr = ptr->next;
请读者注意,由于各结点在内存中不是连续存放的,不可以用 ptr++来寻找下一个结点。
图 9-7 所示为指针 ptr 的移动过程。

– 216 –
第9章 结构

head
NULL

ptr

(a)执行 ptr = ptr->next 前

head
NULL

ptr

(b)执行 ptr = ptr->next 后

图 9-7 指针 ptr 的移动过程

例 9-12 编写一个函数,把由例 9-10 建立的链表中的学生分数小于 60 的结点删除。


分析:设要操作的链表头指针由函数参数传递而来。从头结点开始逐一检查,若结点的
score 分量的值小于 60,则把该结点删除。为了使程序更通用一些,把要删除结点的分数下
限值也用参数传递。由于函数返回删除结点后的链表表头,因此函数的返回值类型应为 struct
stud_node *。
为了在删除结点后还能使链表保持完整性,要引入两个辅助指针 ptr1 和 ptr2。ptr2 的作用
是始终指向当前准备删除的结点,而 ptr1 则总是指向 ptr2 的前一个结点,即两者的关系如下:
ptr2 = ptr1->next;
链表结点删除的原则是先链接,后断开,即先将 ptr1 指向的结点与 ptr2 指向的结点(当
前准备删除的结点)的下一个结点(ptr2->next)先链接上,并要将被删除结点的存储空间释
放。可以用下列语句表示:
ptr1->next = ptr2->next;
free(ptr2);
删除链表中所有符合要求的结点用循环来解决,但需考虑要被删除结点是否为表头。若
要被删除结点为表头(ptr2 = head),则表头要后移(head = head->next)。
源程序:
/* Function for deleting students’ document */
struct stud_node *DeleteDoc(struct stud_node *head,int min_score)
{
struct stud_node *ptr1, *ptr2;

/* 被删除结点为表头结点 */
if (head && head->score < min_score) { /* head != NULL */
ptr2 = head;
head = head->next;
free(ptr2);
}
if(head == NULL) /*链表空 */

– 217 –
C 语言程序设计

return NULL;

/* 被删除结点为非表头结点 */
ptr1 = head;
ptr2 = head->next; /* 从表头的下一个结点搜索所有符合删除要求的结点 */
while(ptr2)
{
if(ptr2->score < min_score){ /*ptr2 所指结点符合删除要求*/
ptr1->next = ptr2->next;
free(ptr2);
}
else ptr1 = ptr2; /* ptr1 后移一个结点 */
ptr2 = ptr1->next; /* ptr2 指向 ptr1 的后一个结点 */
}
return head;
}
从链表中删除一个结点的过程如图 9-8 所示。

head NULL

ptr1 ptr2

(a) ptr2 指向当前准备删除的结点

head NULL

ptr1 ptr2
(b) 执行 ptr1->next = ptr2->next 后

head NULL

ptr1 ptr2

(c) 执行 free(ptr2)后

head NULL

ptr1 ptr2
(d) 执行 ptr2 = ptr2->next 后

图 9-8 从链表中删除一个结点的过程

– 218 –
第9章 结构

例 9-13 假设由例 9-10 建立的链表中的学生记录是按学号由小到大顺序排列,现要按顺


序插入新的学生记录,编写一个函数实现此功能。
分析:设要操作的链表头指针 head 和要插入的新学生记录结点 stud 皆由函数参数传递
而来。若要按顺序正确插入新的学生记录,需要解决下述问题:首先找到正确的插入位置,
然后插入新的结点。
寻找正确的插入位置是一个循环过程,即从链表的 head 开始,把要插入的结点 stud 的
score 分量值与链表中各结点的 score 分量值逐一比较,直到出现要插入结点的值比第 i 结点
score 分量值大,但比第 i+1 结点 score 分量值小。显然,结点 stud 应插在第 i 结点与第 i+1
结点之间。根据上述分析,在链表的插入操作中引入 3 个辅助指针 ptr、ptr1 和 ptr2,ptr 指
向当前准备插入的结点 stud,而 ptr1 则是指向第 i 结点,ptr2 指向第 i+1 结点。
插入原则是先链接,后断开,即先将 stud 结点与第 i+1 结点相链接(即 ptr ->next = ptr2),
再将第 i 结点与第 i+1 结点断开,并使其与 stud 结点相链接(即 ptr1->next = ptr)。
源程序:
/* Function for inserting students’ document */
struct stud_node *InsertDoc(struct stud_node *head,struct stud_node *stud)
{
struct stud_node *ptr ,*ptr1, *ptr2;
ptr2 = head;
ptr = stud; /* ptr 指向待插入的新的学生记录结点 */

/* 原链表为空时的插入 */

if(head == NULL){
head = ptr; /* 新插入结点成为头结点 */
head->next = NULL;
}
else { /* 原链表不为空时的插入 */
while((ptr->num >ptr2->num) && (ptr2->next != NULL)) {
ptr1 = ptr2; /* ptr1, ptr2 各后移一个结点 */
ptr2 = ptr2->next;
}
if(ptr->num <= ptr2->num){
/* 在 ptr1 与 ptr2 之间插入新结点 */
if(head==ptr2) head=ptr;
else ptr1->next=ptr;
ptr->next=ptr2;
}
else{ /* 新插入结点成为尾结点 */
ptr2->next=ptr;
ptr->next= NULL;
}
}
– 219 –
C 语言程序设计

return head;
}
说明:函数参数 head 和 stud 皆为结构指针。其中,head 指向链表首结点,stud 指向待
插入的新的学生记录结点。函数类型是结构指针类型,其返回值为链表首结点 head。
在第 i 结点与第 i+1 结点之间插入结点 stud 的过程,如图 9-9 所示。

head
NULL

ptr1 ptr2
stud

ptr
(a) 将在第 i 结点与第 i+1 结点之间插入结点 stud

head
NULL

ptr1 ptr2
stud

ptr

(b) 执行 ptr->next = ptr2 后

head
NULL

ptr1 ptr2
stud

ptr

(c) 执行 ptr1->next = ptr 后

图 9-9 在第 i 结点与第 i+1 结点之间插入结点 stud 的过程

例 9-14 编写一个程序,要求其不仅可以建立学生记录链表,还可以对所建链表进行修
改,删除成绩低于某个分数线的学生记录,最后打印出修改后的学生记录链表。
分析:根据题目要求,只要编写一个依此调用前述各种链表操作函数的程序即可。为了
节省篇幅,各种链表操作函数具体定义不再重复,只给出主调函数的具体程序编码。
源程序
#include <stdio.h>
#include <stdlib.h> /* malloc, free 的原型在 stdlib.h 中定义*/

– 220 –
第9章 结构

struct stud_node {
int num;
char name[20];
int score;
struct stud_node *next;
};

main()
{
/* 指针、函数说明*/
struct stud_node * Creat_Stu_Doc(), * DeleteDoc(),*InsertDoc();
void Print_Stu_Doc();
struct stud_node *head;
int min_score;
/* 函数调用*/
head = Creat_Stu_Doc() ; /* 建立学生记录链表 */

Print_Stu_Doc(head); /*打印初试学生记录链表*/

printf("Input min score for deleting nodes:\n");


scanf("%d", &min_score); /* 删除成绩低于某个分数线的学生记录 */
head = DeleteDoc(head, min_score);

Print_Stu_Doc(head); /*打印出修改后的学生记录链表*/
}
运行结果如下:
Input num,name and score:
171 Han 100 <CR>
172 Zhang 40 <CR>
173 Wang 50 <CR>
174 Jiang 90 <CR>
0 0 0 <CR>

The Students’ Records Are:


171 Han 100
172 Zhang 40
173 Wang 50
174 Jiang 90

Input min score for deleting nodes::


60 <CR>

– 221 –
C 语言程序设计

The Students’ Records Are:


171 Han 100
174 Jiang 90

9.5 联 合

9.5.1 联合的定义
C 语言中的联合(也称共用体)是一种构造数据类型,它与结构有许多相似之处,也是一
个变量集,即变量的数目固定但类型不同。它的定义格式和分量表示等方面与结构非常相似。
联合的定义指定义联合类型,它的定义格式的一般形式为:
union 联合名 {
类型名 联合分量名 1;
类型名 联合分量名 2;

类型名 联合分量名 n;
};
union 是联合类型的关键字;联合名是一个合法的标识符,用于标识所定义的联合,关
键字 union 与联合名一起组成了一个联合类型名;类型名指定联合分量的类型,必须是有效
的数据类型;联合分量名也是一个合法的标识符,用于标识联合分量。联合分量可以有多个,
每个联合分量对应于一个联合分量名以及指定的类型名。同结构类型一样,联合中各分量的
类型可以不同。
联合与结构的最大区别在于联合的分量是同址的,而结构的分量是异址的。换句话说,
联合的各分量是存放在相同的内存位置上的,而结构的各分量是存放在不同的内存位置上的。
联合的若干分量共用一个内存空间,这是联合的最大特点。
实际上,联合提供了在同一存储单元中操作不同类型数据的机制。因此,联合类型的变
量可以在不同的应用环境下具有不同的数据类型。
例如:
union u_type {
char ch;
int i;
};
定义了一个联合类型,联合类型的名字为 union u_type,它由分量 ch 和 i 组成,每个分量又
有各自的数据类型,分别为 char 和 int,且分量 ch、i 共存于同一个内存单元。

9.5.2 联合变量的定义和引用

1.联合变量的定义

定义了一个联合类型后,
“union 联合名”就成为类型名,可以像使用其他基本数据类型

– 222 –
第9章 结构

那样,用它来定义变量。注意:同结构类型变量的定义相似,关键字 union 和联合名必须联


合使用成为一个类型名。
联合变量的定义方式与结构变量的定义方式一样,有 3 种定义的方法。
(1)可以先定义一个联合类型,然后再定义一个具有这种联合类型的联合变量。其一般
形式为:
union 联合名 联合变量表;
例如:
union u_type {
char ch;
int i;
};
union u_type u_var;
(2)联合变量的定义方式,也可以采用在定义联合类型的同时定义结构变量。其一般形
式为:
union 联合名 {
类型名 联合分量名 1;
类型名 联合分量名 2;

类型名 联合分量名 n;
}联合变量名表;
例如,联合变量 u_var 的定义可写为:
union u_type {
char ch;
int i;
} u_var;
(3)定义联合变量时可省略联合名。其一般形式为:
union {
类型名 联合分量名 1;
类型名 联合分量名 2;

类型名 联合分量名 n;
}联合变量名表;
例如:
union {
char ch;
int i;
} u_var;
定义了联合变量 u_var,它包含两个分量:ch 和 i,它们共享同一个内存空间。由于 char、
int 类型的数据所占内存长度不同,所以联合类型变量总是占据其分量中具有最大长度的空
间,即在定义一个联合变量时,编译器将自动产生一个足以存放联合中最长的分量类型的空
间。在本例中,分量 ch 仅占 1 个字节,分量 i 占 2 个字节,则联合类型变量 u_var 占据其分
量中具有最大长度的空间——int 类型的数据所占内存长度(2 个字节)。分量 ch、i 分别占有
– 223 –
C 语言程序设计

1 个和 2 个字节,但对联合变量 u_var 来讲总是占有 2 个字节的内存空间,两个分量共同使


用 2 个字节的内存空间。
联合类型的变量不需要具体硬件的有关信息,而只考虑联合中各分量的相互覆盖使用,
并按起始地址对齐。
联合变量 u_var 中的内存分配如图 9-10 所示。

ch

Byte1 Byte2

图 9-10 联合变量的内存分配

2.联合变量的引用

对联合变量的引用,与对结构变量的引用方式一样,常见的操作是用成员操作引用联合
变量的分量。使用分量操作符“.”引用联合变量的分量,其一般格式为:
联合变量名.分量名
例如 u_var.i、u_var.c,分别表示联合变量 u_var 的分量 i.、c,即 u_var.i 是对联合变量 u_var
的整型分量的引用,而 u_var.c 则是对联合变量 u_var 的字符型分量的引用。联合变量的分量
的使用方法与同类型的变量完全相同。但要注意的是,联合变量起作用的分量是最后一次存
放的分量,在存入一个新分量的值后,原来的分量就因被覆盖而失去作用。例如:
u_var.i = 17;
u_var.c = ’H’;
两个赋值语句最后起作用的是后一个语句,即只有 u_var.c 起作用,而 u_var.i 不起作用(其
值无意义),原因是这两个联合成员(分量)使用同一个内存空间,后面的赋值覆盖了前面的
赋值。
另外,可以通过指针来访问联合变量。例如:
union u_type u_var,* ptr;
ptr =&u_var;
则指针 ptr 指向联合变量 u_var,通过指针 ptr 可以访问 u_var 中的各个分量。
(1)用*ptr 访问联合分量。例如:
(*ptr).i = 17;
其中*ptr 表示的是 ptr 指向的联合变量,注意(*ptr)中的括号是不可缺少的,因为分量运算符
“.”的优先级高于“*”的优先级,若没有括号就会发生混淆,如:*ptr.i 等价于*(ptr.i)。
(2)用指向运算符“->”访问联合分量。例如:
ptr-> i = 17;
以上两种方法最终得到的效果是一样的,但在使用指针访问联合分量时,通常使用指向
运算符“->”访问联合分量。
假设 ptr 指向联合变量 u_var,则下面 3 个语句的效果是一样的。

– 224 –
第9章 结构

u_var.i = 17;
等价于:
(*ptr).i = 17;

ptr-> i = 17;
例 9-15 阅读并运行程序,其中函数 Creat_Doc()用于建立一个单向链表,从键盘输入每
个结点的数据,当输入为 0 时,输入结束,并返回链表的头指针;函数 Print_list()的功能是
输出一个链表,函数通过形参得到表头,再根据联合中的不同数据类型,灵活输出相应的结
果。
源程序:
#include <stdio.h>
#include <stdlib.h>
#define INTEGER 1
#define CHAR 2

struct list_node {
int t;
union {
char c;
int i;
} record;
struct list_node *next;
};

main()
{
/* 指针、函数说明*/
struct list_node *Creat_Doc();
void Print_list(struct list_node *vp) ;
struct list_node *head;

/* 函数调用*/
head = Creat_Doc() ; /* 建立链表 */

print_list(head) ; /*打印链表*/
}

struct list_node *Creat_Doc()


{
struct list_node * head,* tail,* p;
int t,i;

int size = sizeof(struct list_node) ;

– 225 –
C 语言程序设计

head = tail = NULL;


printf("Input t and i :\n");
scanf("%d %d", &t, &i);
while (t!= 0 )
{
p = (struct list_node *) malloc(size);
p-> t = t;
p-> record.i = i;
if(head == NULL)
head = p;
else
tail->next = p;
tail = p;
scanf("%d %d", &t, &i);
}
return head;
}

void Print_list(struct list_node *vp)


{
printf("\nThe Results Are: \n");
for( ; vp; vp = vp->next)
switch(vp->t) {
case INTEGER:
printf("%d\n", vp-> record.i); /*引用联合成员 record.i */
break;
case CHAR:
printf("%c\n", vp-> record.c); /*引用联合成员 record.c */
break;
default:
printf("Wrong list_node type \n");
}
}
运行结果如下:
Input Input t and i :
1 65 <CR>
2 66 <CR>
2 67 <CR>
00 <CR>

The Results Are:


65

– 226 –
第9章 结构

B
C
在本例中,结构分量 record 是联合类型,可以在不同的应用环境下具有不同的数据类型,
提供了在同一存储单元中操作不同类型数据的机制。
注意本例中联合分量的引用,该联合又是结构分量 record 的类型。引用方式应按从左到
右,从内到外的顺序。
从结构与联合的定义与分量引用的方法中可以看到,两者之间有许多相似之处,但又有
些不同。它们之间的异同点如下。
相同点:
(1)它们都是构造型数据类型,分量的数目固定但类型不同。
(2)对联合和结构施加的操作只能是存取分量或地址,不能对联合和结构赋值,也不能
把它们作为参数传递给函数,更不能从函数返回结构和联合。
(3)对联合和结构分量的引用都是用分量操作符“.”,若用联合或结构指针引用分量,
都可以是用指向操作符“->”。
不同点:
(1)联合的若干分量共用一个内存空间,因而联合的分量是同址的,而结构的分量是异
址的,即联合的分量间的地址偏移量为 0,而结构的分量间的地址偏移量为非 0。
(2)联合变量所占内存空间为其最大分量所占的内存空间,而结构变量所占内存空间为
其所有分量所占的内存空间之和。
(3)在结构中,同一时间内可以存取分量的分量;而在联合中,同一时间内只能引用最
近被赋值的分量,因为在联合中没有任何关于分量位置及当前数据类型方面的信息。
(4)对全局或静态的结构变量可以进行初始化,但对联合变量一般不能进行初始化,若
要进行初始化,它必须对应于第一个分量。

9.6 枚 举

枚举是一种构造型数据类型,其主要作用是将一些变量的取值范围限制在某一集合内。

9.6.1 枚举类型的定义
枚举类型的定义非常类似结构,它使用关键字 enum 来标志一个枚举类型的开始。枚举
类型定义的一般形式为:
enum 枚举名 { 枚举分量名 1,枚举分量名 2,……,枚举分量名 n};
enum 是枚举类型的关键字;枚举名是一个合法的标识符,用于标识所定义的枚举,其和
enum 共同组成了一个枚举类型名;枚举分量名也是一个合法的标识符,是编程者根据需要自
己定义的,它们列出一个枚举变量可以具有的值。
例如:
enum weekday {sun, mon, tue, wed, thu, fri, sat};
定义了一个枚举类型,枚举类型的名字为 enum weekday,它由分量 sun, mon, tue, wed, thu, fri

– 227 –
C 语言程序设计

和 sat 组成,即一个类型为 enum weekday 的枚举变量可以具有的值为 sun, mon, tue, wed, thu,
fri 和 sat。
枚举名 weekday 标识了这一特殊的数据结构,同时 enum weekday 也由此成为类型名。

9.6.2 枚举变量的定义和引用
定义了枚举类型,就可以定义该枚举类型的变量。与结构变量和联合变量相似,枚举变
量的定义也有 3 种方法。
(1)先定义一个枚举类型,再定义一个具有这种枚举类型的枚举变量。其一般形式为:
enum 枚举名 枚举变量名表;
例如:
enum weekday workday, holiday;
定义了两个变量 workday、holiday,它们的取值范围被限制在集合{sun, mon, tue,wed ,thu, fri,
sat}之内,即这两个变量在赋值时,只能被赋成这个集合中的元素,而不能被赋成任何其他
值。因此下面的代码是正确的:
workday = mon;
holiday = sun;
但下面的代码不正确:
workday = tomorrow;
因为 tomorrow 不是枚举类型 enum weekday 中的元素,也不是 enum weekday 类型的变
量。
(2)对于枚举类型变量的定义也可在定义枚举类型时直接进行,如:
enum weekday {sun,mon,tue,wed,thu,fri,sat} workday,holiday;
(3)可以定义无名枚举类型的变量,如:
enum {sun,mon,tue,wed,thu,fri,sat} workday,holiday;
在 C 语言中,枚举类型中的枚举分量与整数(int)相对应,如在 enum weekday 中,sun
有值 0,mon 有值 1,……,sat 有值 6,这是一种隐式定值方法。另外,也可用显式方法建
立枚举元素与整数之间的对应关系,把一个整型常量赋给一个枚举元素,并规定其余的枚举
元素就按顺序赋予其后继的值。例如:
enum {sun=7, mon,tue = 3,wed,thu,fri,sat} holiday;
元素 sun 的值为 7,所以 mon 的值为 8;tue 的值为 3,所以 wed,thu 的值分别为 4 和 5。此时,
若运行语句:
printf("%d,%d", mon,tue);
则在屏幕上显示 8,3。
事实上,枚举类型是用较清晰的标识符来代替整型常量,其中的枚举元素以常量看待,
所以不能对枚举元素再进行赋值。例如,不能直接对枚举元素 sun 进行下面的操作:
sun=7;
还有一种常见的错误是把一个整数(或整型表达式的计算结果)直接赋值给一个枚举变量。
正确的操作是进行强制类型转换,如:
workday = (enum weekday) (6 - 2);
把表达式(6- 2)的计算结果 4 用(enum weekday)强制类型转换成 enum weekday 类型,并赋值给

– 228 –
第9章 结构

变量 workday。
例 9-16 分析下列程序的输出结果。
源程序:
enum coin { penny,nickel,dime,quarter,half_dollar,dollar };
char *name[] = { "penny", "nickel", "dime", "quarter", "half_dollar", "dollar"};

main()
{
enum coin money1, money2;

money1 = dime;
money2 = dollar;

printf("%d %d\n", money1, money2);


printf("%s %s\n", name[(int)money1], name[(int)money2]);
}
运行结果如下:
2 5
dime dollar
说明:该程序先定义一个枚举类型 enum coin,它有 6 个枚举元素:penny, nickel, dime,
quarter, half_dollar, dollar。因为没有用显式定值方法,每个枚举元素按隐式定值。程序中又定
义了一个字符型的一维字符数组,用来存放 6 个字符串。在 main()函数中,定义两个枚举变
量 money1 和 money2,并且给它们赋了值。
枚举变量的值只能是枚举元素。将已赋值的枚举变量按%d 输出,其值分别是它们所对
应的枚举元素的隐式定值,即 2 和 5。
输出时,不能简单地按%s 输出,而应该像本例这样通过字符数组法,将枚举变量对应
的整数值作为数组下标来输出数组元素对应的字符串。
从本例可以看到,枚举类型中的各元素对应了一个整数,但在使用时要进行强制类型转
换,如对 name[(int)money1]的引用,先把变量 money1 强行转换成整型,然后作为下标取到
name 中的相应字符串。
枚举实际上是以名字符号出现的若干整型常量的集合,具有某种枚举类型的枚举变量的
取值范围被限制在该枚举类型所规定的枚举元素的范围内。利用枚举类型,一方面使程序编
写更为方便,另一方面也能使程序更加清晰,易于阅读。

9.7 自定义类型

自定义类型不是定义一些新的数据类型,而是将 C 语言中的已有类型(包括已定义过的
自定义类型)重新命名。
自定义类型的一般形式为:

– 229 –
C 语言程序设计

typedef <已有类型名> <新类型名表>;


typedef 是关键字,已有类型名包括C语言中规定的类型和已定义过的自定义类型,新类
型名表可由一个和多个重新定义的类型名组成。一般要求重新定义的类型名用大写。例如:
typedef int NUM ,INTEGER;
定义了两个名为 NUM 和 INTEGER 的新的数据类型,事实上该定义并没有创造新的数据类
型,只是通知编译器把 NUM 和 INTEGER 作为 int 的另外两个名字,
即可用 NUM 或 INTEGER
去替代C语言中的整型类型 int。所以下面的几个定义是等价的:
int i;
NUM i;
INTEGER i;
它们都定义了一个整型变量 i。
自定义类型可以嵌套,即用已定义过的自定义类型再定义类型。另外,也可以使用 typedef
定义更复杂的类型。例如:
typedef int NUM,SCORE;
typedef char *NAME;
typedef struct stud_type{
NUM num;
NAME name;
SCORE score;
} STUDENT;
在上述定义中,先自定义了 3 个类型:NUM、SCORE 为整型,NAME 为字符指针;而
STUDENT 则为一个结构类型,它是结构类型 struct stud_type 的别名,该结构类型中有 3 个
分量。
这样就可定义下面的变量:
STUDENT stud1, stud2, *stu_ptr;
stud1 与 stud2 是两个结构类型的变量,而 stu_ptr 则是一个结构指针变量。在定义
STUDENT 变量时,不需要再跟 struct 关键字。下面的定义也是合法的:
typedef int NUM[100];
NUM arr_var;
这里首先定义了一个整型数组类型 NUM,它由 100 个整型元素组成,然后用类型 NUM 去定
义一个变量 arr_var,显然 arr_var 是一个有 100 个整型元素的数组变量。
从以上的几个例子可以发现,用 typedef 可以定义许多用户需要的数据类型。很明显,利
用自定义类型可以大大提高 C 语言程序的可读性。
例 9-17 用 typedef 重新定义数据类型。
typedef int NUM;

main()
{
NUM a,b;

a = 5;

– 230 –
第9章 结构

b = 6;

printf("a =%d\tb = %d\n", a,b );


{
float NUM;
NUM = 3.0;
printf("2*NUM = %.2f\n", 2*NUM);
}
}
运行结果如下:
a=5 b= 6
2*NUM = 6.0
说明:该程序先定义类型 NUM 为 int 型。在 main()函数中,用 NUM 说明了两个变量 a
和 b,对它们赋值后,用 printf()将两个变量输出。
接着,在后面的复合语句中,定义了一个 float 型变量 NUM,此时,NUM 不再是自定义
的类型,而是一个变量名,对它赋值后,用 printf()输出 2*NUM。
自定义类型的作用如下。
(1)可将复杂类型重新定义为简单类型。
(2)给定义的变量增加一些新的信息。
(3)自定义类型的数据可进行安全检查,增加了安全性。
(4)使用自定义类型 typedef,有助于程序的可读性和可移植性,但要记住并没有产生新
的数据类型。

9.8 位运算与位段

与其他高级语言相比较,位运算是 C 语言中一个比较有特色的地方,利用位运算可以实
现许多汇编语言才能实现的功能。

9.8.1 位运算和位运算符
所谓位运算是指进行二进制位的运算。C 语言提供的位运算符如表 9-1 所示。
表 9-1 C 语言提供的位运算符
运算符 名 称

& 按位与

| 按位或

^ 按位异或

~ 取反

<< 左移

>> 右移

– 231 –
C 语言程序设计

注意:
(1)位运算符中除“~”是单目运算符,其余均为双目运算符。
(2)位运算符所操作的操作数只能是整型或字符型的数据以及它们的变体。位运算不能
用于 float、double、long 或其他更复杂的数据类型。
(3)操作数的移位运算不改变原操作数的值。
C 语言的位运算符分为位逻辑运算符和移位运算符两类。下面分别介绍这两类位运算符。

1.位逻辑运算符

位逻辑运算符有如下 4 种。
单目运算符:~(取反)
双目运算符:&(按位与) 、|(按位或)和 ^(按位异或)
二进制位逻辑运算的规则如表 9-2 所示。
表 9-2 二进制位逻辑运算的规则

a b ~a a|b a&b a^b

0 0 1 0 0 0

0 1 1 1 0 1

1 0 0 1 0 1

1 1 0 1 1 0

位逻辑运算符的运算规则是先将两个操作数(int 或 char 类型)化为二进制数,然后按


位运算。
例如位非运算(~) ,将操作数按二进制数逐位求反,即 1 变为 0,0 变为 1。
设 a=84,b=59,则 a&b 结果为 16。因为 84 的二进制数为 01010100,而 59 的二进制数
为 00111011,按表 9-2 所示的运算规则逐位求与,得二进制数 00010000,即是十进制数 16。
具体过程如下:
01010100 (84 的二进制数)
(&)00111011 (59 的二进制数)
00010000 (16 的二进制数)
注意:二进制位逻辑运算和普通的逻辑运算的区别。假设 x=0,y=28,则 x&y 等于 0,
x|y 等于 28,而 x&&y 等于 0,x||y 等于 1。在进行位运算时,只有对应位进行运算,相邻位
之间没有关系。
对于位异或运算(^)有几个特殊的操作:
a^a=0
a^~a=二进制全 1 (如果 a 是 16 位二进制表示,则为 65535)
~(a^~a)=0
除此以外,位异或运算(^)还有一个很特别的应用,即通过使用位异或运算不需临时变
量就可交换两个变量的值。假设 a=19,b=23,若要将 a 和 b 的值互换,可执行语句:
a^=b^=a^=b;

– 232 –
第9章 结构

该语句等效于:
b^=a^=b;
a= a^b;
b^=a^=b 可解释为:
b= b ^(a^b)⇔ a^b^b ⇔ a^0 = a
因为操作数的位运算并不改变原操作数的值,除最左边的 b 外,其余的 a、b 都是指原来的 a、
b,即 b 得到 a 原来的值。
a= a^b 可解释为:
a= a^b ⇔ (a^b)^(b ^ a ^ b) ⇔ a^ a ^ b ^ b ^ b = b
上面 b^=a^=b 中 a^=b 使 a 改变,b 也已经改变,分别拿原来的式子代入,最后 a 得到 b 原
来的值。

2.移位运算符

移位运算是指对操作数以二进制位为单位进行左移或右移的操作。移位运算符有两种:
>>(右移)
<<(左移)
例如,a >> b 表示将 a 的二进制值右移 b 位,a << b 表示将 a 的二进制值左移 b 位。进
行移位运算时,a 和 b 必须都是整型,b 只能为正数,且不能超过机器字所表示的二进制位数。
移位运算具体实现有 3 种方式:循环移位、逻辑移位和算术移位(带符号)。
(1)循环移位:在循环移位中,移入的位等于移出的位。
(2)逻辑移位:在逻辑移位中,移出的位丢失,移入的位取 0。
(3)算术移位:在算术移位中(带符号) ,移出的位丢失,左移入的位取 0,右移入的位
取符号位,即最高位代表数据符号,保持不变。
C 语言中的移位运算方式与具体的 C 语言编译器有关。通常实现中,左移位运算后右端
出现的空位补 0,移至左端之外的位则舍弃。右移位运算与操作数的数据类型是否带有符号
位有关,不带符号位的操作数右移位时,左端出现的空位补 0,移至右端之外的位则舍弃;
带符号位的操作数右移位时,左端出现的空位按符号位复制,其余的空位补 0,移至右端之
外的位则舍弃。
例如,假设 a= 58=00111010,a<<2 的值为:
←00 111010←00=11101000 = 232 = 58*4
在数据可表达的范围里,一般左移 1 位相当乘 2,左移 2 位相当乘 4。
同样,假设 a= 58=00111010,a>> 1 的值为:
0→0011101 0→ =00011101 = 29 = 58/2
一般右移 1 位相当除 2,右移 2 位相当除 4。
再次提醒:操作数的移位运算并不改变原操作数的值,即经过上述移位运算,a 仍为 58,
除非 a=a>>2,通过赋值改变 a 的值。

9.8.2 位段
与大多数其他语言不同,C 语言可以对一个字节或一个字节的一位或多位进行运算,这

– 233 –
C 语言程序设计

在实际应用中非常有用。原因如下:①如果存储空间有限,可以将几个布尔变量(真或假)
存入一个字节;②某些设备接口可将传送到一个字节的一位或几位中;③某些压缩和加密例
程需要访问字节的位信息。
C 语言访问字节的位信息是以位段为基础的。位段也称为位域(bit field),实际上是结
构的一种应用。它利用结构这种数据类型将若干段数据长度的变量压缩组成一个 int 数据。
换句话说,位段是一种特殊类型的结构成员,它定义了每个成员有几位。
位段说明的一般格式如下:
struct 结构名{
类型名 位段名 1:整常量表达式 1;

类型名 位段名 n;整常量表达式 n;
} 结构变量名表;
其中,位段名相当于结构分量,它们是被压缩的变量名。整常量表达式的值用来表示该
分量所占内存的二进制位数,它的值大于 0,但不大于 int 型变量所占内存的二进制位数。类
型名必须说明为 int、 unsigned 或 signed。
可以把一般的结构成员同位段元素混合使用,以节省存储空间,例如:
struct student {
int num;
char name[20];
struct address addr;
unsigned lay_off :1; /*学生状态:是否报到*/
unsigned pay_off :1; /*学费状态:是否已交*/
nsigned grade :3; /*所在年级*/
};
该结构定义了一个学生的记录,其中仅用 1 个字节来存放 3 个信息:学生状态、学费状
态和所在年级。若不是使用位段元素,这 3 个信息则要用至少 3 个字节。
例 9-18 在一个指令系统中,共有 ADD、SUB、TIME、DIV 四条指令,而每一条指令
的操作数是不大于 128 的正整数,请构造合适的数据结构来描述该指令系统。
分析:由于该指令系统中只有 4 条指令,所以可以用 2 个二进制位(bit)表示它们,即
用 00 表示 ADD,用 01 表示 SUB,用 10 表示 TIME,用 11 表示 DIV;又由于操作数不会大
于 128,所以只要用 7 个字节来表示操作数即可,这样两个操作数共需 14 个二进制位(bit)。
这样一条指令及其操作数只要 16 个二进制位(bit) ,即 2 个字节就足以存储了,这 2 个字节
的分配方式如图 9-11 所示。
F E D C B A 9 8 7 6 5 4 3 2 1 0

指令 第一操作数 第二操作数
图 9-11 两个操作数的存储情况

– 234 –
第9章 结构

该指令系统可通过由位段元素构造的结构数据类型来描述。
#define ADD 0x0
#define SUB 0x1
#define TIM 0x2
#define DIV 0x3

struct opr_type {
unsigned int operator: 2;
unsigned int operand1: 7;
unsigned int operand2: 7;
};
struct opr_type word;
在结构 struct opr_type 中,共有 3 个分量,它们都是位段,其中 operator 占有 2 位,而
operand1、operand2 均占有 7 位。这种方法的好处在于对压缩后的各数据值的存取,可采用
结构分量的操作方法。例如把 word 的运算符赋值成“加”运算的方法为:
word.operator = ADD;
而把 word 的第一、二个操作数赋值成 50、20 的方法为:
word.operand1 = 50;
word.operand2 = 20;
位段在一般的表达式中都被自动地转换成整数类型,这样取第一个操作数,并赋值给整
型变量 oprd 的方法为:
oprd = word.operand;
显然,使用这种位段的操作方法非常方便。
使用位段的注意事项如下。
(1)不同的机器分配位段的方向可能不同,有的机器从左向右分配,而有的机器则是从
右向左分配,这反映了不同的硬件特性。因此,在具体使用时应注意首尾连接问题,在 C 语
言程序移植时应注意位段分配方向。
(2)在位段结构中,不必对每个位段进行命名,这样就可以跳过未使用的位,从而很容
易达到所希望的位。例如:
struct emp {
unsigned type: 3;
unsigned :4;
unsigned f1: 4;
unsigned f2: 4;
};
其中第一个分量 type 占 3 位;第二个分量是无名位段,占 4 位,它把 type 与 f1、f2 两个位
段分隔开来,f1、f2 都占 4 位。
(3)不能对位段变量进行地址操作,即不能用&操作符。
(4)同一结构中,位段可以与非位段共存,但是不相邻的位段不可以进行合并处理,应
按成员定义时的顺序分配单元。
例 9-19 该程序将联合和位段结合起来使用,当按下某一键时,通过位段来显示生成的
– 235 –
C 语言程序设计

二进制 ASCII 码。
源程序:
#include <stdio.h>;
#include <conio.h>;

struct byte{
int a:1;
int b:1;
int c:1;
int d:1;
int e:1;
int f:1;
int g:1;
int h:1;
};

union bits{
char ch;
struct byte bit;
}ascii;

void decode(union bits b) ; /*函数说明*/

main(void)
{
do{
ascii.ch =getche();
printf(":");
decode(ascii);
}while(ascii.ch!= ’q’); /*当按下 q 键时,停止显示 ASCII 码 */

return 0;
}

void decode(union bits b) /*函数定义*/


{
if(b.bit.h)printf("1");
else printf("0");
if(b.bit.g)printf("1");
else printf("0");
if(b.bit.f)printf("1");
else printf("0");
if(b.bit.e)printf("1");

– 236 –
第9章 结构

else printf("0");
if(b.bit.d)printf("1");
else printf("0");
if(b.bit.c)printf("1");
else printf("0");
if(b.bit.b)printf("1");
else printf("0");
if(b.bit.a)printf("1");
else printf("0");
printf("\n");
}
说明:本程序通过调用 getche()函数将键值赋给联合的字符成员,再通过位段显示单个
位。因此使用联合,可以以两种根本不同的方式查看同一内存单元。

习 题

1.选择题
(1)对于以下结构定义:
struct { int len;
char *str;
} *p;
(*p)->str++中的++加在 。
A.指针 str 上 B.指针 p 上 C.str 所指的内容上 D.表达式语法有错
(2)联合定义为“union data {char ch;int x;} a;”,下列中 是不正确的。
A.a={’x’,10}; B.a.x=10;a.x++; C.a.ch=’x’;a.ch++; D.a.x=10;a.ch=’x’;
(3)按位与运算:int a=7,b=12,c=a&b; 变量 c 的值是 。
A.19 B.4 C.5 D.9
(4)按位异或运算:int a=14,b=15,c=a^b; 变量 c 的值是 。
A.1 B.15 C.−1 D.29
(5)按位取反运算:int a=16,c= ~a;变量 c 的值是 。
A.17 B.7 C.84 D.−17
(6)左移运算:int a=16,c=a<<2;变量 c 的值是 。
A.67 B.4 C.1 D.64
2.填空题
(1)下列程序将结构变量 stud1 的值赋给结构变量 stud2。
#include <stdio.h>
#include <string.h>
main()
{
static struct student {
int num;
– 237 –
C 语言程序设计

char name[20];
int score;
} stud1 = { 0213147,"Zhang Hong", 92};
;
printf("%d %d ", stud2.num, stud2.score);
return 0;
}
(2)现假定每一学生的数据结构为:
struct student {
int num;
char name[20];
int score; };
则 10 个学生的结构数组可定义为 。
(3)完成下列程序,该程序求一个学校中的 10 个学生的平均分数。
#include <stdio.h>
#include <string.h>
struct student {
int num;
char name[20];
int score;
};
struct student stud[10];
main()
{
int i ,sum = 0 ;
for(i = 0; i < 10; i++){
scanf("%d %s %d ", &stud[i].num, , &stud[i].score);
sum += stud[i].score;
}
printf("aver = %d \n", sum/10);
}
(4)通过使用结构的嵌套定义,定义单向链表结点的数据类型(假设该链表用来存储学
生记录)。
struct student {
int num;
char name[20];
int score;
;
};
(5)对于 enum Weather { RAIN, CLOUD, CLEAR } w; 则:
w= (enum Weather)1; 与 weather= ; 等价。
(6)typedef int (*A[10])();也可以用以下方式分开定义:

– 238 –
第9章 结构

typedef ___________; /*先定义 PT*/


typedef PT A[10] ;
(7)表达式 3&5、3|5、3||5 的值分别为 、 、 。
(8)设 int x=707,表达式 x^x、x|x、~x^x 的值分别为 、 、 。
(9)对于 enum Weather { RAIN, CLOUD, CLEAR } w; 则:
w= (enum Weather)1; 与 weather= __________; 等价。
(10)typedef int (*A[10])();也可以用以下方式分开定义:
typedef ___________; /*先定义 PT*/
typedef PT A[10] ;
(11)下列程序读入时间数值,将其加 1 秒后输出,时间格式为 hh: mm: ss,即时:分:秒。
当小时等于 24 小时,置为 0。
#include<stdio.h>
struct { int hour, minute, second; } time;
void main(void)
{ scanf("%d: %d: %d", ); time.second++;
if( ==60) {
time.second=0;
if(time.minute==60) {
time.hour++; time.minute=0;
if( ) time.hour=0;
}
}
printf ("%d: %d: %d \n",time.hour,time.minute,time.second );
}
(12)下列函数用于将链表中某个结点删除,其中 n 为全程量,表示链表中的结点个数。
struct tabdata {
int num;
struct tabdata *next;
};
struct tabdata *del(struct tabdata *h, long num)
{
struct tabdata *p1,*p2;
if(h==NULL) {
printf("\nlist null!\n");

}
p1=h;
while(num!=p1->num&& ){
p2=p1; p1=p1->next;
}
if(num==p1->num) {
if(p1==h) h=p1->next;

– 239 –
C 语言程序设计

else
n--; printf("delete:%ld\n",num);
}
else printf("%ld not been found!\n",num);
end: return h;
}
(13)下列函数用于将链表中各结点的数据依次输出。
void print(struct student *head)
{
p=head;
if(head!=NULL)
do {
printf("%ld\n",p->data);

} while ( );
}
(14)已建立学生“英语”课程的成绩链表(成绩存于 score 域中),下列函数用于计算
平均成绩并输出。
void print(struct student *head)
{ struct student *p;float num; ;
;
if(head!=NULL) {
for(num=0;p!=NULL; ,i++) num+=p->score;
num=num/i; printf("%8.1f\n",num);
}
}
(15)已建立学生“英语”课程的成绩链表(成绩存于 score 域中,学号存于 num 域中)
,下列
函数用于输出待补考学生的学号和成绩及补考学生人数。
void require(struct student *head)
{ struct student *p;
if( head!=NULL) {

while(p!=NULL) {
if( ){
printf("%7d %6.1f\n",p->num,p->score); x++;
}
p=p->next;
}
printf("%ld\n",x);
}
}
3.分析下列程序的输出结果。

– 240 –
第9章 结构

(1)struct abc{
int a;
float b;
char *c;
}
main()
{ static struct abc x = {23,98.5, "wang"};
struct abc *px = &x;
printf("%d %.1f %s\n", x.a, x.b, x.c);
printf("%d %.1f %s\n ", px->a, (*px).b, px->c);
printf("%c %s\n", *px->c+2, &px->c[1]);
}

(2)main()
{
struct tt {
char L;
char H;
};
union uu {
struct tt byte;
int word;
} ua;
ua.word = 0x5678;
printf("%04x\n", ua.word);
printf("%02x\n", ua.byte.H);
printf("%02x\n", ua.byte.L);
}

(3)#include <stdio.h>
main()
{
char *pc = NULL;
int *pi = NULL;
double *pd = NULL;
printf("\n%d %d %d\n%d %d %d\n\n",
(int)(pc+1), (int)(pi+1), (int)(pd+1),
(int)(pc+3), (int)(pi+5), (int)(pd+7));
}
4.定义一个关于年、月、日的结构,并编写一个函数计算某日是该年中的第几天。注意
闰年问题。
5.编写一个程序,计算两个时刻之间的时间差,并将其值返回。时间以时、分、秒表示,

– 241 –
C 语言程序设计

两个时刻的差小于 24 小时。
6.有两个单向链表,头指针分别为 list1、list2,链表中每一结点都包含姓名和工资等基本
信息。编写一个函数,把两个链表拼组成一个链表,并返回拼组后的新链表。
7.有一个单向链表 L,编写一个函数将 L 复制到一个新链表 NEW 上(链表结点信息与
上题相同) 。
8.设有枚举类型:
enum month {jan, feb, march, april, may, june, july,aug, sept, oct, nov, dec};
请编写一个函数 f(int m),它返回一个字符指针,指向与参数 m 相对应的月份名字符串。
9.使用类型定义重新命名类型有什么好处?
10.请用类型定义重新定义用下面方法定义的变量 ab:
struct xy{
int x, y;
} *(* ab) ;
11.分析下面表示位段的结构变量 a 占用的内存字节数。
struct {
int i: n1;
int :n2;
unsigned int j: n3;
} a;
其中,n1、n2、n3 分别取下表中的值。

n1 5 5 5 5 5 5 6 6

n2 1 4 0 0 14 10 10 0

n3 2 9 1 9 9 9 9 17

a 1

12.编写一个函数,得到一个 16 位的二进制数中任何一位的数值。

– 242 –
第 10 章 文 件
许多程序的实现过程中,依赖于把数据保存到变量中,而变量是通过内存单元存储数据
的,数据的处理完全由程序控制。当一个程序运行完成或运行终止时,所有变量的数据不再
保存。另外,一般的程序都会有数据输入与输出,如果输入/输出数据量不大,通过键盘和显
示器即可方便解决。当输入/输出数据量较大时,大量的键盘输入会使人厌烦,尤其是在程序
调试时,输入的次数会需要多次;结果数据显示超过一屏会使得前面数据无法看到。
文件是解决上述问题的有效办法,它通过把数据存储在磁盘文件中,得以长久保存。当
有大量数据输入时,通过编辑工具可事先建立输入数据的文件,程序运行时将不再从键盘输
入,而从指定的文件上读入,从而实现数据一次输入多次使用,对程序调试十分方便。同样,
当有大量数据输出时,可以将其输出到指定文件,不受屏幕大小限制,并且任何时候都可以
查看结果数据文件。另外,结果数据还可以作为另外程序的输入,进行进一步加工。
本章主要介绍 C 语言程序如何把数据存储到文件上,以及如何从文件上读取数据。包括
文件指针定义、文件打开、文件读写和文件定位。至于文件的管理操作(如复制、移动等)
可由操作系统完成,C 语言程序不必关心。

10.1 文件的基本概念

从我们开始学习计算机知识,我们就一直在与文件打交道。我们编写的程序在上机时都
是首先建立 C 语言源程序,然后经编译连接生成可执行文件。源程序是语句及相关符号的集
合,以文件的方式存储在计算机的磁盘中。C 语言处理的文件与 Windows 等操作系统操作的
文件概念相同。但在 C 语言中,文件是作为数据组织的一种方式,它与数组、结构体等相似,
是 C 语言程序处理的对象。
文件(File)是指保存在外存储器上的一组数据的有序集合,它有 3 个主要特征。
(1)文件被保存在外存储器上,如磁盘、磁带或光盘,可以长久保存。
(2)文件中的数据是有序的,按一定顺序存放和读取,一般情况下,数据的读取顺序与
存储顺序相同。
(3)文件中数据的数量可以是不定的,定义时不必像数组那样必须规定好大小,可以根
据实际需要存储,它只受外存自由空间的限制。因此,从某种角度看,文件可以作为动态的
数据结构。

10.1.1 文本文件和二进制文件
C 语言把文件看作为数据流,即数据以一维方式按顺序组织。如图 10-1 所示,它非常像
我们平时使用的录音磁带,在磁带够长的前提下,录音长短可以任意,录音和放音过程是顺
– 243 –
C 语言程序设计

序进行的。这正好与文件的数据动态性和操作顺序性一致。根据数据存储的形式,文件的数
据流又分成字符流和二进制流,前者称为文本文件(或字符文件) ,后者称为二进制文件。C
语言源程序是文本文件,其内容完全由 ASCII 码构成,通过“记事本”等编辑工具可以对文
件内容进行查看、修改等操作。C 语言程序的可执行文件是二进制文件,它包含的是计算机
内部的机器代码,如果也用编辑工具打开,将会看到稀奇古怪的符号。例如,对于整数 1234,
如果存放到文本文件中,文件内容将包含 4 个字节:49 50 51 52,它们分别是’1’、’2’、’3’、’4’
的 ASCII 码值。如果把整数 1234 存放到二进制文件中去,文件内容将为 1234 对应的二进制
数 0x04D2,共两个字节。对于具体的数据应该选择哪一类文件进行存储,由需要解决的问题
来决定,并在程序的一开始定义好。

字节 字节 字节 字节 ......

图 10-1 文件数据的字节流表示

10.1.2 缓冲文件系统
文件根据数据存取实现过程,分为缓冲文件系统与非缓冲文件系统。

1.缓冲文件系统

缓冲文件系统指系统会自动为每一个使用的文件分配一块缓冲区(内存单元)
,当 C 语
言程序需要把数据存入磁盘文件时,首先把数据存入缓冲区,缓冲区真正把数据存入磁盘文
件的工作由系统自动完成,其目的是为了提高文件操作速度。从磁盘读入数据同样也要经过
缓冲区。

2.非缓冲文件系统

磁盘文件对应的缓冲区不是由系统自动分配,而需要编程者在 C 语言程序中用 C 语句完


成缓冲区分配。
UNIX 操作系统中,用缓冲文件系统来处理文本文件,用非缓冲文件系统处理二进制文
件。ANSI C 中规定只采用缓冲文件系统。缓冲文件系统工作原理如图 10-2 所示。

内存缓冲器 磁盘
程序 fp

… … 由操作系统
程序控制
自动完成

512 字节

图 10-2 文件系统缓冲区的工作原理

使用缓冲文件系统可以大大提高文件操作的速度。文件是保存在磁盘上,磁盘数据的组
– 244 –
般需要花若干毫秒(10 3 秒)。
  第 10 章 文件

织方式按扇区进行,微机中每个扇区一般存储 512 字节,连续存放,扇区是磁盘的最小管理


单元。如果要把数据写到磁盘文件中,首先要找到文件在磁盘上存储的具体扇区,然后才能
写入数据。下面我们先了解几个时间概念。
(1)寻道时间:磁盘由于机械传动方面的限制,寻找文件在磁盘上存储的具体位置,一

(2)写磁盘时间:真正把数据写入磁盘确定位置的速度可以达到微秒级(10 6 秒)。
(3)写缓冲区时间:缓冲区(内存)的运行速度为纳秒级(10 9 秒),C 语言程序对缓冲
区读写数据的时间可以忽略不计。
如果要把 10 个数据存入文件,逐个写入会花费 10 次寻道时间。当采用了缓冲文件系统
时,这 10 个数据并不需要逐个写入磁盘,而是先存储在缓冲区中,当 10 个数据都准备好后,
一次性写入磁盘一个扇区中,则
总的写入时间 = 1 次寻道时间 + 10 个数据的写入时间
其中寻道时间远大于数据写入时间。这比 10 个数据分别直接写入磁盘要快得多。
同样,从文件读入数据时,系统不管具体需要的数据数量,先读入一个扇区的数据到缓冲
区中,C 语言程序再依次从缓冲区读出,用一次的寻道时间可得到 512 字节数据,这大大提高
了文件读取的效率。如果需要的数据少于 512 字节,一次读磁盘就可以了。如果需要的数据多
于 512 字节,缓冲区数据被读完后,系统会自动读入下一个扇区的 512 字节数据。即使在极端
情况下,程序只需从文件读一个字节数据,多读 511 字节所花的时间仍比寻道时间要小。
缓冲区的大小由具体的 C 语言版本决定,一般微机中使用的 C 语言把缓冲区定为 512 字
节,正好是磁盘的一个扇区,从而保证了磁盘操作的高效率。
缓冲文件系统将会自动在内存中,为被操作的文件开辟一块连续的内存单元(如 512 字
节)作为文件缓冲区。当要把数据存储到文件时,首先把数据写入文件缓冲区,一旦写满了
512 字节,系统自动把全部数据写入磁盘一个扇区,然后把文件缓冲区清空,新的数据继续
写入到文件缓冲区。当要从文件取得数据时,系统首先自动把一个扇区的数据导入文件缓冲
区,供 C 语言程序逐个读入数据,一旦 512 字节数据都被读入,系统自动把下一个扇内容导
入文件缓冲区,供 C 语言程序继续读入新数据。图 10-2 所示的工作原理图说明了该过程。

10.1.3 缓冲文件与文件类型指针
由于文件缓冲区与磁盘间的数据交换由系统自动完成,C 语言程序可以不关心。C 语言
程序对文件的操作可以看作对文件缓冲区的操作。但文件缓冲区中存储的是一大块数据,如
何定位某一具体数据,是文件操作中首先要解决的问题。因为文件缓冲区由系统自动分配,
不能像数组那样可以通过数组名加下标来定位,借鉴动态内存分配函数 malloc( )的用法,C
语言中采用指针来指示文件缓冲区单元。除此以外,在文件操作中还需用到文件的名字、状
态、位置等信息。为此,C 语言专门定义了新的类型——文件类型(FILE)。它采用结构体
表示。下面是 Turbo C 在 stdio.h 中对文件类型的说明:
typedef struct {
short level; /* 缓冲区使用量 */
unsigned flags; /* 文件状态标志 */
char fd; /* 文件描述符 */

– 245 –
C 语言程序设计

short bsize; /* 缓冲区大小 */


unsigned char *buffer; /* 文件缓冲区的首地址 */
unsigned char *curp; /* 指向文件缓冲区的工作指针 */
unsigned char hold; /* 其他信息 */
unsigned istemp;
short token;
} FILE;
由于 C 语言中的文件操作都是通过调用标准函数来完成的,结构体指针的参数传递效率
更高。因此,C 语言文件操作统一以文件指针方式实现。定义文件类型指针的格式为:
FILE *fp ;
其中大写 FILE 是文件类型定义符,fp 是文件类型的指针变量。
文件指针是特殊指针,指向的是文件类型结构体,它是多项信息的综合体。每一个文件
都有自己的 FILE 结构体和文件缓冲区,通过 fp->curp 可以指示文件缓冲区中数据存取的位
置。但对一般编程者来说,不必关心 FILE 结构体内部的具体内容,这些内容由系统填入和
使用,C 语言程序只使用文件指针 fp,用 fp 代表文件整体。所以文件指针不能像以前普通指
针那样,进行 fp++或*fp 等操作,fp++将意味着指向下一个 FILE 结构体(如果存在的话)。
由于文件操作具有顺序性的特点,前一个数据取出后,下一次将顺序取下一个数据,
fp->curp 的改变隐含在后面介绍的文件读写操作中,而不能人为改变,这一点在学习文件时
务必注意。
使用文件类型时,需要在程序头上指定文件包含:
#include <stdio.h>

10.2 文件的打开与关闭

文件最基本的操作有两个:从磁盘文件中读取信息(读操作)和把信息存放到磁盘文件
中(写操作)。为了实现读写操作,首先要定义文件指针变量,接着确定被操作文件的具体文
件名,请求系统分配文件缓冲区单元,然后进行文件读写,文件操作完成后关闭文件。本节
先介绍文件打开和文件关闭,文件的读写将在 10.3 节介绍。

10.2.1 打开文件
文件打开功能是用于指定被操作的文件名,并请求系统分配相应的文件缓冲区单元。文
件打开由标准函数 fopen( )实现,其调用的一般形式为:
FILE *fp;
fp = fopen(文件名,文件使用方式);
函数返回包含文件缓冲区等信息的 FILE 结构体地址,保存到文件指针上。文件名和文件使
用方式都是字符串,用"r"表示从文件读数据,"w"表示把数据写入文件。例如下面两种方法
都以读的方式打开 abc.txt 文件:
fp = fopen("abc.txt","r" ); /* 用字符串常量表示文件 */
或:
– 246 –
第 10 章 文件

char *p="abc.txt"; /* 用字符指针表示文件 */


fp = fopen( p ,"r" );
执行标准函数 fopen( ),计算机将完成下述步骤的工作。
① 在磁盘当前工作文件夹下找到指定文件。
② 内存中分配保存一个 FILE 类型结构体的单元(16 字节)。
③ 内存中分配文件缓冲区单元(512 字节)。
④ 为 FILE 结构体填入相应信息。
⑤ 返回 FILE 结构体地址(回送给 fp)。
文件打开的实质是把磁盘文件与文件缓冲区对应起来,保证后面的文件读写操作只需使
用文件指针即可。整个程序中磁盘文件名只在文件打开中出现一次。如果 fopen( )返回 NULL
(空值),表明文件 abc.txt 无法正常打开,其原因可能是 abc.txt 不存在或路径不对,也可能文
件存储有问题。一般为保证文件操作的可靠性,调用 fopen( )时最好做一个判断,以确保文件
正常打开后再进行读写。其形式为:
if ((fp=fopen("abc.txt","r")) == NULL) {
printf("File open error!\n");
exit(0);
}
其中 exit(0)是系统标准函数,作用是关闭所有打开的文件,并终止程序的执行。参数 0 表示
程序正常结束,非 0 参数通常表示不正常的程序结束。
一个文件经 fopen( )正常打开后,其使用方式就规定了,在关闭文件之前不得改变,即一
个文件打开时按"r"方式的话,该文件只能做读操作,而不能写入数据。表 10-1 给出了 C 语
言常用的文件使用方式。
表 10-1 文件使用方式

文 本 文 件(ASCII) 二 进 制 文 件

使用方式 含 义 使用方式 含 义

"r" 打开只读文件 "rb" 打开只读文件

"w" 建立只写新文件 "wb" 建立只写新文件

"a" 打开添加写文件 "ab" 打开添加写文件

"r+" 打开读/写文件 "rb+" 打开读/写文件

" w +" 建立读/写新文件 "wb+" 建立读/写新文件

"a +" 打开读/写文件 "ab+" 打开读/写文件

从表 10-1 可以看出,二进制文件操作方式与文本文件(字符文件)的操作方式不同,读
文件和写文件也不同。下面是文本文件读写的一些规则。
(1)if 读文件
指定的文件必须存在,否则出错;
(2)if 写文件(指定的文件可以存在,也可以不存在)
① if 以"w"方式写
– 247 –
C 语言程序设计

if 该文件已经存在
原文件将被删去重新建立;
else
按指定的名字新建一个文件;
② if 以"a"方式写
if 该文件已经存在
写入的数据将被添加到指定文件原有数据的后面,不会删去原来的内容;
else
按指定的名字新建一个文件(与"w"相同);
(3)if 文件同时读和写
使用"r+"、"w+"或"a+"打开文件 (10.6.3 节将做进一步介绍)
二进制文件使用方式与之相似。
C 语言允许同时打开多个文件,不同文件采用不同文件指针指示,但不允许同一个文件
在关闭前再次打开。

10.2.2 关闭文件
当文件操作完成后,应及时关闭它,以防止不正常的操作影响。另外,对于缓冲文件系
统来说,文件的操作是通过缓冲区进行的,如果把数据写入文件,首先是写到文件缓冲区里,
只有当写满 512 字节,才会由系统真正写入磁盘扇区。如果写的数据不到 512 字节,发生程
序异常终止,那么这些缓冲区中的数据将会被丢失。通过文件关闭,能强制把缓冲区中的数
据写入磁盘扇区,确保写文件的正常完成。
关闭文件通过调用标准函数 fcolse( )实现,其一般格式为:
fclose(文件指针)
该函数将返回一个整数,若该数为 0 表示正常关闭文件,否则表示无法正常关闭文件,所以
关闭文件也应使用条件判断:
if (fclose(fp)) {
printf ( "Can not close the file!\n" );
exit(0);
}
关闭文件操作除了强制把缓冲区中的数据写入磁盘外,还将释放文件缓冲区单元和 FILE
结构体,使文件指针与具体文件脱钩。但磁盘文件和文件指针变量仍然存在,只是指针不再
指向原来的文件。
读者在编写程序中应养成文件使用结束后及时关闭文件的习惯,一则确保数据完整写入
文件,二则及时释放不用的文件缓冲区单元。

10.3 文件的读写

scanf( )和 printf( )是针对键盘输入和屏幕输出的标准函数。C 语言为文件的读写也定义了

– 248 –
第 10 章 文件

一系列标准函数,它们都在 stdio.h 中说明,因此文件操作需要有相应的文件包含。


#include <stdio.h>
编写文件操作的程序,必须包括 4 个步骤:
定义文件指针 FILE *fp ;
打开文件 fp = fopen(文件名,使用方式);
文件读写
关闭文件 fclose(fp);
本节主要介绍有关文件读写的标准函数使用。

10.3.1 字符文件读写
字符文件通常称为文本文件,它存取的数据都是字符。假定文件指针 fp 和字符变量 ch
已定义。

1.fputc( )函数

格式:fputc(ch,fp) ;
功能:把一个字符 ch 写到 fp 所指示的磁盘文件上。

–1 写文件失败
函数返回值 =
ch 写文件成功

注意与屏幕字符输出函数 putchar(ch)的书写区别。
例 10-1 从键盘输入 10 个字符,写到文件 a.txt 中。
分析:按照文件操作的 4 个步骤述写程序:定义文件指针、打开文件、写字符到文件、
关闭文件。写字符到文件的函数 fputc( )需要循环调用 10 次。
源程序:
#include <stdio.h>
main()
{
int i ; char ch ;
FILE *fp ; /* 定义文件指针 */
if ((fp=fopen("a.txt","w")) == NULL) { /* 打开文件 */

printf("File open error!\n");


exit(0);
}
for( i=0 ; i<10 ; i++ ) { /* 写文件 10 次 */
ch=getchar();
fputc(ch, fp) ;
}
if (fclose(fp)) { /* 关闭文件 */
printf ( "Can not close the file!\n" );

– 249 –
C 语言程序设计

exit(0);
}
}
运行:abcdefghijk<CR>
该程序运行完后,可在程序所在的文件夹下,找到 a.txt 文件,双击它可以在记事本下显
示写入的 10 个字符。

2.fgetc( )函数

格式:ch = fgetc(fp);
功能:从 fp 所指示的磁盘文件上读入一个字符到 ch。
注意与键盘字符输入函数 getchar( )的书写区别。
例 10-2 从键盘输入 10 个字符,写到文件 a.txt 中,再重新读出,并在屏幕上显示验证。
分析:这是一个键盘输入→写入文件 a.txt→屏幕显示的过程,当键盘输入到 a.txt 时,文件
需按照写方式打开,而再把 a.txt 的内容显示到屏幕时,文件又要按照读方式打开。读和写是两
种不同的操作,所以在程序中 a.txt 会被分别打开、关闭两次。程序前半部分与例 10-1 相同。
源程序:
#include <stdio.h>
main()
{
int i ; char ch ;
FILE *fp ; /* 定义文件指针 */

if ((fp=fopen("a.txt","w")) == NULL) { /* 打开文件 */


printf("File open error!\n"); exit(0);
}
for( i=0 ; i<10 ; i++ ) { /* 写文件 10 次 */
ch=getchar();
fputc(ch, fp) ;
}
if (fclose(fp)) { /* 关闭文件 */
printf ( "Can not close the file!\n" ); exit(0);
}
if ((fp=fopen("a.txt","r")) == NULL) { /* 按读方式再次打开文件 */
printf("File open error!\n"); exit(0);
}
for( i=0 ; i<10 ; i++ ) { /* 读文件 10 次 */
ch=fgetc(fp);
putchar(ch) ;
}
if (fclose(fp)) { /* 再次关闭文件 */
printf ( "Can not close the file!\n" ); exit(0);
}

– 250 –
第 10 章 文件

}
文件在读写过程中,fgetc( )和 fputc( )实际上是对文件缓冲区进行读写,其工作过程与字
符数组的操作相似,无非存取单元由 fp->curp 指示。但文件操作中很重要的一点是,fp->curp
会随 fgetc( )和 fputc( )的执行而自动改变:
等价 *(fp->curp) = ch ;
fputc(ch, fp)
fp->curp++ ;
千万不要在程序中试图用 fp++来改变文件缓冲区的位置,fp 指向的是文件结构体。
上面两个例子并没有完全体现出文件的特点,文件特点之一是文件数据长度可以不定,
只要外存空间足够,数据就可以不受限制地写入文件中。但读一个文件全部数据时,如何确
定文件的数据量,从而决定读的循环次数呢?与字符串处理方式相似,文件中设置了文件结
束符 EOF(End Of File),它对应的数值是−1,在 stdio.h 中有说明。−1 不是正常的 ASCII 码,
以区别文件中的字符内容。仿照字符串处理程序,通过判断从文件中读入的字符是否为 EOF,
来决定循环继续否。
例 10-3 从键盘输入一行字符,写到文件 b.txt 中,并重新读出,最终在屏幕上显示验证。
分析:程序与例 10-2 相似,输入以读到回车符’\n’为结束,读文件时要用 EOF 来控制循
环。
源程序:
#include <stdio.h>
main()
{
char ch ;
FILE *fp ; /* 定义文件指针 */

if ((fp=fopen("b.txt","w")) == NULL) { /* 打开文件 */


printf("File open error!\n"); exit(0);
}
while ( (ch=getchar( ))!=’ \n’ )
fputc(ch, fp) ;
if (fclose(fp)) { /* 关闭文件,产生 EOF */
printf ( "Can not close the file!\n" ); exit(0);
}
if ((fp=fopen("b.txt","r")) == NULL) { /* 按读方式再次打开文件 */
printf("File open error!\n"); exit(0);
}
ch= fgetc(fp);
while ( ch!=EOF ) { /* 读到的不是 EOF 的话,继续读文件 */
putchar(ch) ; ch=fgetc(fp);
}
if (fclose(fp)) { /* 再次关闭文件 */
printf ( "Can not close the file!\n" ); exit(0);
}

– 251 –
C 语言程序设计

}
说明:只有读文件时才需判断 EOF,而写文件时无需做 EOF 判断。EOF 无法从键盘输
入,在关闭文件时系统自动产生。另外,EOF 不是正常 ASCII 字符,其值为−1,无法在屏幕
上显示。读者不妨用 printf("%c%d", EOF, EOF)在计算机上验证。

3.feof( )函数

格式:feof(fp);
功能:判断文件是否被读到了结束位置。

1 文件结束
函数返回值 =
0 文件未结束

使用 feof( )函数,可以不需要像例 10-3 那样,为了检查 EOF,需先从文件中读一个


字符。
例 10-4 将磁盘文件 a.txt 中的内容复制到文件 b.txt 中。
分析:需要同时打开两个文件,其中 a.txt 以读方式打开,b.txt 以写方式打开,要定义两
个文件指针。
源程序:
#include <stdio.h>
main()
{
FILE *fpa, *fpb ; /* 定义文件指针 */

if ((fpa=fopen("a.txt","r")) == NULL) { /* 打开文件 a.txt */


printf("can not open file a.txt !\n"); exit(0);
}
if ((fpb=fopen("b.txt","w")) == NULL) { /* 打开文件 b.txt */
printf("can not open file b.txt!\n"); exit(0);
}
while ( !feof(fpa) ) /* 文件复制 */
fputc(fgetc(fpa) , fpb) ;
if (fclose(fpa)) { /* 关闭文件 a.txt */
printf ( "Can not close file a.txt !\n" ); exit(0);
}
if (fclose(fpb)) { /* 关闭文件 b.txt */
printf ( "Can not close file b.txt !\n" ); exit(0);
}
}
说明:feof( )只对读操作的文件进行判断,返回值为 0 表示未读到文件结束符,循环维
持条件为! feof( )。
在例 10-1~例 10-4 程序中,文件指针定义、打开文件、读写文件、关闭文件 4 个步骤非
常清楚。尽管程序稍长一些,但一半多的语句是固定写法,程序结构并不复杂。
– 252 –
第 10 章 文件

10.3.2 数值文件读写
我们使用 scanf()和 printf()完成键盘屏幕数据的输入输出,
数据类型由格式控制字符指定。
文件操作中也有指定格式的输入/输出函数,即 fscanf()和 fprintf(),它们除了对字符类型有效
外,对数值类型也有效。使用格式为:
fscanf(文件指针,格式控制,输入参数表);
fprintf(文件指针,格式控制,输出参数表);
例如:
FILE *fp; int n; float x;
fp=fopen("a.txt","r");
fscanf(fp,"%d%f",&n,&x);
表示从文件 a.txt 分别读入整型数到变量 n、浮点数到变量 x。
int n; float x;
FILE *fp;
fp=fopen("b.txt","w");
fprintf(fp,"%d%f",n,x);
表示把变量 n 和 x 的数值写入文件 b.txt。
也许读者已经注意到文件 a.txt 和 b.txt 是以文本方式打开的,但读写操作的数据并不是
字符类型,变量 n 和 x 在内存中是以二进制形式存储的。两者间的不一致将由系统负责解决。
文本文件本身存储的是字符,当使用 fscanf()进行输入时,系统会自动根据规定的格式,把输
入的代表数值的字符串转换成数值。同样使用 fprintf()输出,系统也会自动根据规定的格式,
把输出的二进制数值转换成字符串,写到文件中。文件中数据之间的分隔符由读写格式决定,
可以是空格也可以是逗号,其意义与原来 scanf()和 printf()相同。
例 10-5 文件 a.txt 中有若干个实数,请分别读出,经平方操作后,存入文件 b.txt 中。
源程序:
#include <stdio.h>
main()
{
float x ;
FILE *fpa, *fpb ; /* 定义文件指针 */

if ((fpa=fopen("a.txt","r")) == NULL) { /* 打开文件 a.txt */


printf("can not open file a.txt !\n"); exit(0);
}
if ((fpb=fopen("b.txt","w")) == NULL) { /* 打开文件 b.txt */
printf("can not open file b.txt!\n"); exit(0);
}
while ( !feof(fpa) ) { /* 文件操作 */
fscanf(fpa,"%f",&x) ; /* 从文件读浮点数 */
fprintf(fpb,"%f , ",x*x) ; /* 浮点数写入文件,以逗号分隔 */
}

– 253 –
C 语言程序设计

if (fclose(fpa)) { /* 关闭文件 a.txt */


printf ( "Can not close file a.txt !\n" ); exit(0);
}
if (fclose(fpb)) { /* 关闭文件 b.txt */
printf ( "Can not close file b.txt !\n" ); exit(0);
}
}
运行:先用“记事本”输入若干个实数,各数之间用空格分隔,然后保存到 a.txt 文件中。
接着运行本例程序,运行结果在文件 b.txt 中,双击 b.txt 文件图标可以查看结果,各个数之
间以逗号分隔。分隔符完全由 scanf()和 printf()的格式指定。
说明:例 10-5 与例 10-4 的差别仅在于文件操作的循环中,文件打开、关闭对所有文件
处理程序来说,形式都一样。
在 Windows 下建立输入数据的文件,用“记事本”为好,若用“写字板”或 Word 需用
“纯文本”类型保存。

10.3.3 二进制文件读写
二进制文件中的数据流是非字符的,它包含的是数据在计算机内部的二进制形式。C 语
言程序对二进制文件的处理程序与文本文件相似,只在文件打开的方式上有所不同,分别用
"rb"、"wb"和"ab"表示二进制文件的读、写和添加,如表 10-1 所示。
如果把例 10-5 处理的文件改成二进制文件,只需把源程序 fopen( )中的打开方式改成"rb"
和"wb",其余不用做任何变动。读者可以自己上机试试。但有一点需要注意,程序中用于输
入的二进制文件无法用“记事本”等工具建立,它一般是其他程序或软件的处理结果。同样,
作为程序结果的二进制文件也无法用“记事本”等工具查看。
二进制文件的读写效率比文本文件要高,因为它不必在数据与字符之间做转换。

10.4 文件程序设计

编写文件操作的程序,必须包括 4 个步骤。
(1)定义文件指针;
(2)打开文件;
(3)文件读写;
(4)关闭文件。
其中步骤(1)、 (2)、(4)的写法基本一致。不同程序的差别主要体现在步骤(3)上。
下面以几个典型的例子来说明文件程序设计的方法。
例 10-6 读一个指定的文本文件,显示在屏幕上,如果有大写字母,则改成小写字母再
输出,并统计行数。
分析:根据回车符统计文件的行数。要处理的文件名通过键盘读入字符串指定。
源程序:
#include <stdio.h>
– 254 –
第 10 章 文件

main()
{
FILE *fp ; /* 定义文件指针 */
char name[10], ch ;
int line=1 ;
gets(name ) ; /* 输入要处理的文件名 */
if ((fp=fopen(name,"r")) == NULL) { /* 打开指定文件 */
printf("can not open file a.txt !\n"); exit(0);
}
while ( !feof(fp) ) {
ch=fgetc(fp) ; /* 从文件读一个字符 */
if (ch>=’A’&&ch<=’Z’) ch+=’a’-’A’ ; /* 大写字母换小写 */
putchar(ch) ; /* 屏幕输出 */
if (ch==’\n’) line++ ; /* 统计行数 */
}
if (fclose(fp)) { /* 关闭文件 */
printf ( "Can not close file a.txt !\n" ); exit(0);
}
printf ( "total line is %d !\n" , line ) ;
}
运行:如果上述源程序保存在 f1.C 中,运行时输入字符串 f1.C,把上述源程序作为处理
对象,屏幕上将显示程序所有语句,并把“FILE”改成“file”,最后输出行数:
total line is 20
例 10-7 把文本文件 data.txt 中的若干整数进行排序。
分析:排序需要交换数据的存储位置,即从一个位置读出,写到另一个位置上,这意味
着对文件不仅要同时读和写,还要对任意位置的数据操作,这对于具有顺序性质的文件是比
较难实现的。我们采取把文件所有数据读到一个数组上,然后对数组排序,最后再写回文件。
由于 data.txt 中数据个数未知,数组大小定义按最大情况考虑。
源程序:
#include <stdio.h>
#define MAX 1000
void sort(int a[ ], int n) ; /* 排序函数说明 */
/* a 是存放排序数据的数组,n 是数据个数 */
main()
{
int i, j, temp[MAX] ;
FILE *fp ; /* 定义文件指针 */

if ((fp=fopen("data.txt", "r")) == NULL) { /* 打开文件读 */


printf("can not open the file !\n"); exit(0);
}
for ( i=0; !feof(fp) ; i++)

– 255 –
C 语言程序设计

fscanf(fp, "%d", &temp[i]); /* 从文件读整数到数组 */


sort(temp, i) ; /* 调用排序函数 */
if(fclose(fp)) { /* 关闭文件 */
printf ( "Can not close file a.txt !\n" ); exit(0);
}
if ((fp=fopen(“data.txt”, "w")) == NULL) { /* 打开文件重写 */
printf("can not open the file !\n"); exit(0);
}
for ( j=0; j<i ; j++)
fprintf(fp, "%d", temp[j]); /* 把排好序的数据写入文件 */
if(fclose(fp)) { /* 关闭文件 */
printf ( "Can not close file a.txt !\n" ); exit(0);
}
}
运行:把要排序的数据先用“记事本”存入 data.txt 文件,运行后再查看 data.txt 文件内
排好序的数据。
说明:本例中未给出函数 sort( ),可以使用前几章介绍过的选择法或冒泡法排序程序。
在主函数中,也可以采用动态内存申请,用指针代替数组,即先读一遍文件,统计数据个数
n,然后调用
int *p ;
p=(int *)calloc(n,sizeof(int));
动态申请内存单元。请读者自己完成。
例 10-8 二进制文件 d.dat 中包含若干整数,从键盘输入一个整数,请在文件中找出该
整数的下一个数并输出。若找不到则输出“Not found!”。
分析:在文件中查找数据,有可能要检查文件中的所有数据,通常称之为文件遍历。有
两种情况造成“Not found!”:
① 输入数据不存在;
② 输入数据为文件中最后一个数据,无法输出再下一个数据。
源程序:
#include <stdio.h>
main()
{
int x , y , flag=1;
FILE *fp ; /* 定义文件指针 */

scanf("%d",&x) ;
if ((fp=fopen("d.dat", "rb")) == NULL) { /* 打开文件 */
printf("can not open the file !\n"); exit(0);
}
while ( !feof(fp) ) {
fscanf(fp, "%d", &y ) ; /* 从文件读一个整数 */
if (x==y) /* 找到数据 */
– 256 –
第 10 章 文件

if (!feof(fp) ) {
fscanf(fp, "%d", &y ) ; /* 读入下一个数据 */
printf("next %d is %d\n", x, y) ;
flag=0 ; /* 设置找到标志 */
break ; /* 已找到数据,运环结束 */
}
}
if (flag ) printf("Not found !\n") ;
if (fclose(fp)) { /* 关闭文件 */
printf ( "Can not close file a.txt !\n" ); exit(0);
}
}
说明:当找到等于 x 的数后,不能马上读下一个数,必须要确保文件未结束才能读。如
果 x 已经是文件中最后一个数了,再从文件读数将引起错误。所以 if(!feof(fp))的判断是必需
的。一般情况下,读文件的循环体中,不要轻易地写几个读文件操作,例如:
while ( !feof(fp) )
fscanf(fp, "%d%d", &x, &y ) ;
就有可能发生错误,当文件中只剩下一个可读数据时,!feof(fp)判断正确,但它只保证一次读
操作的正确,读了 x 后,feof(fp)=1,y 没有数据可读。如果在读文件的循环体中,需要有多
个读操作,应在其他的读操作前附加 feof()或 EOF 的判断。
例 10-9 编一个带命令行参数的程序 prog.C,若执行 prog filename,则原样输出 filename
文件的内容;若执行 prog -l filename,则大写输出 filename 文件的内容;若执行 prog -u
filename,则小写输出 filename 文件的内容;其他情况出错。
分析:这是文件与命令行参数的综合题。首先要根据命令行参数的意义得到正确的文件
名,然后将之打开。若执行:
prog filename argc=2 argv[1]是要打开的文件名字符串
prog -l filename 或 prog -u filename argc=3 argv[2]是要打开的文件名字符串
filename 是命令行参数,程序中用 argv[i]表示。文件操作本身相对容易,只需判断大小
写。
源程序:
main(int argc, char *argv[ ])
{
char *fname , ch ; /* fname 是指向将要打开的文件名的指针 */
int flag ; /* 标志,文件内容大小写控制 */
FILE fp ;

if ( argc<2|| argc>3) { printf(" error! " ) ; exit(0); }


if ( argc==2) fname=argv[1] ; /* 命令行只有两个参数 */
else { /* 命令行有 3 个参数 */
if (strcmp(argv[1], "-l" ) = =0) flag =2 ;
else if (strcmp(argv[1], "-u" ) = =0) flag =1 ;

– 257 –
C 语言程序设计

else {printf("error! " ) ; exit(0); /* 第二个参数不正确 */


}
fname=argv[2] ;
}
if ((fp=fopen(fname ,"r"))==NULL) { /* 打开文件 */
printf("cannot open file a.dat!"); exit(0);
}
while (!feof(fp)) { /* 根据要求输出字符 */
ch=fgetc(fp) ;
switch (flag) { /* 大写或小写转换 */
case 1: if (ch>=’a’&&ch<=’z’) ch - = ’a’+’A’; break ;
case 2: if (ch>=’A’&&ch<=’Z’) ch + = ’a’-’A’; break ;
}
putchar (ch) ; /* 3 种情况的统一输出 */
}
if (fclose(fp)) { /* 关闭文件 */
printf ( "Can not close file a.txt !\n" ); exit(0);
}
}

10.5 标准文件的输入/输出

文件读写操作大大增强了程序的输入/输出能力,输入数据不仅能从键盘得到,还能从文
件得到;输出数据不仅可以在屏幕上显示,还可以输出到文件。为了在处理形式上更为一致,
计算机操作系统一般把外设也看作文件,键盘是输入文件,显示器是输出文件。为区别于磁
盘上的普通文件,C 语言定义了 3 个标准文件。
(1)stdin:标准输入文件。
(2)stdout:标准输出文件。
(3)stderr:标准出错信息输出文件。
一般情况下,标准输入文件对应于输入设备(如键盘),标准输出文件和标准出错信息输
出文件对应于输出设备(如显示器) 。它们都是缓冲文件,定义在 stdio.h 中。由于它们的使
用频率很高,因而程序执行时它们都以缺省方式打开,可以直接对这些标准文件进行操作。
printf(格式控制,输出参数表) 等价于 fprintf(stdout,格式控制,输出参数表)
scanf(格式控制,输入参数表) 等价于 fscanf(stdin,格式控制,输入参数表)
标准头文件 stdio.h 中除了包含了本章介绍的文件操作有关说明外,还包含本书一开始介
绍过的标准输入/输出函数的说明,本节对它们再作进一步介绍。

10.5.1 字符的输入/输出
标准输入/输出设备上,单个字符的输入/输出操作由 getchar()、putchar()完成。

– 258 –
第 10 章 文件

1.getchar()

定义:int getchar()
参数:无。
返回:一个字符或 EOF。
说明:从标准输入文件(键盘)读入一个字符,把它转换成整型(ASCII 码值)并返回。
若遇到文件结束或读入错误,则返回 EOF。

2.putchar()

定义:int putchar(ch)
参数:int ch;
返回:字符 ch 或 EOF。
说明:把 ch 输出到标准输出文件(显示器)上。若输出成功,返回 ch;否则返回 EOF。

10.5.2 格式化输入/输出
getchar()和 putchar()函数只能处理字符,对于其他类型数据的输入/输出,C 语言标准库
中又提供了格式化输入/输出函数 printf()和 scanf()。

1.格式化输出函数 printf()

printf()函数的一般格式为:
printf(格式控制, 输出参数 1,…… , 输出参数 n);
其中“格式控制”是一个字符串,它可以含有以“%”开头的控制字符,也可含有普通字符;
而输出参数则是一些要输出的数据项。例如:

printf(" X = %d Y = %d", x, y);

格式 控制 输 出参数

当 x、y 的值分别为 1 和 20 时,输出结果为:


X=1 Y = 20

格式控制中的普通字符
输出参数的类型、个数、位置一般要与“格式控制”中的控制字符一一对应。如上例中,输
出参数 x 与第一个%d 对应,输出参数 y 与第二个%d 对应。
printf()中可出现的“控制字符”有下面几类。
(1)d 格式符:用来输出十进制整数,它又有下述 4 种用法。
① %d:按整型数据的实际长度输出。
② %md:m 是被指定的输出字段宽度,若数据的实际位数小于 m,则左端补以空格;
若大于 m,则按实际位数输出。
③ %ld:输出长整型(long)数据。
– 259 –
C 语言程序设计

④ %mld:输出长整型数据,m 是被指定的输出字段宽度。如:
long a = 2458;
printf("%8ld", a);
输出结果为:
□□□□2458 (其中□表示空格)
(2)o 格式符:以无符号的八进制形式输出整数。对于长整数(long)可以用“%lo”,
也可以用“%mo”及“%mlo”指定输出字段宽度。
(3)x 格式符:以无符号的十六进制形式输出整数。也有“%lx”、 “%mx”、“%mlx”等
格式,其中 m 是被指定的输出字段宽度。
(4)u 格式符:以无符号的十进制形式输出整数。也有“%lu”、 “%mu”、
“%mlu”等格
式,其中 m 是被指定的输出字段宽度。
(5)f 格式符:输出十进制浮点数(float 或 double)。它有下面两种用法。
① %f:不指定字段宽度,由系统自动指定,其整数部分全部输出,小数部分规定输出
6 位。
② %m.nf:指定输出数据共有 m 列,其中包括 n 位小数和一位小数点,整数位数为
− −
m n 1。若数值长度小于 m,则左端补空格。
一般 float 的有效位数为 7 位,浮点数的输出数字中并不都是有效位数。
(6)e 格式符:以指数形式输出浮点数,通过指数的变化,使整数位数保持一位,它有
下面两种用法。
① %e:系统自动指定。如 TC 中,系统自动指定 5 位小数,指数部分占 4 位(其中 e
占一位、指数符号占一位、指数二位)。
② %m.ne:指定输出数据共有 m 列,尾数部分中的小数为 n 位,若数值长度小于 m,
则左端补空格。
(7)g 格式符:用来输出实数,它根据数据的大小自动选 f 格式或 e 格式(选择宽度较
小的一种) ,且不输出无意义的零。
(8)c 格式符:输出单个字符。
(9)s 格式符:输出一个字符串,它又有下面三种用法。
① %s:按字符串的实际长度输出字符串。
② %ms:输出的字符串占 m 列,若字符串的位数小于 m,则左端补以空格,若大于 m,
则按实际位数输出。
③ %m.ns:输出占 m 列,但只取字符串的左边 n 个字符,这 n 个字符在 m 列中按右对
齐。
在 printf()函数中,输出一般都是向右对齐,实际数据宽度不足时,左面添空格。若在格
式控制中%后紧跟修饰符“-”,表示输出向左对齐。如:
char *str="Hello";
int x = 40;
printf("|%-10s|%5d|", str, x);
运行结果为:
|Hello | 40|

– 260 –
第 10 章 文件

另外还规定,在格式控制部分的字符%后不是上面 9 种格式符之一,就输出%后的字符,
但%不被输出;若要输出%,需在格式控制中写成%%。在 printf()还可利用C语言中的转义
字符:\n, \t, \0, \\, \f, \r, \101 等。

2.格式化输入函数 scanf()

scanf()函数从标准输入文件中输入数据,其格式与 printf()类似:
scanf(格式控制, 输入参数 1, ……, 输入参数 n);
其中输入参数是以指针形式出现(变量名前加地址符&),而格式控制是一个字符串,它
有下述三种类型。
(1)格式字符:以%开始,控制输入数据的类型。
(2)空白字符:跳过输入序列中前面出现的空白符(如空格、换行符、制表符)。
(3)普通字符:数要输入但不存储的字符,一般不建议使用。
scanf()的格式控制中可以有下面的修饰符(即在%与格式字符间出现)。
(1)*:输入的数据将舍弃,没有输入参数对应。
(2)数值:表示域所占的最大字符位置数,为一正整数常量。
(3)h:可在格式符 d、o、x 的前边出现,表示与此对应的参数是 short 型指针。
(4)l:当用于整型读入时,表示输入的数据是 long 型;当读入浮点数时,表示输入的
数据是 double 型。
scanf()的格式控制中可用下列格式符。
(1)d:以十进制方式输入整数,对应的参数是 int 型指针。
(2)o:以八进制方式输入整数,对应的参数是 int 型指针。
(3)x:以十六进制方式输入整数,对应的参数是 int 型指针。
(4)c:读入一个字符,对应的参数是字符型指针。若在 c 前面出现域宽,则说明了希望
输入的字符数,这时,对应的参数为一指向字符型数组的指针。
(5)s:读入一个字符序列。该序列以第一个非空白字符开始,读到下一个空白字符结束。
这时相应的参数是一个字符型数组的指针,该数组必须有足够的长度以容纳读入的字符以及
由系统自动加到字符串末尾的空字符(’\0’)。若有域宽修饰符,则说明了要读入的字符数。
但在读到空白字符后,即使还没有读到要求的字符数,也要结束对它的读入操作。
(6)f:读入一个浮点数,相应的参数是 float 型的指针。输入的数据可以是以自然计数
法或科学计数法表达的浮点数。
(7)e:与 f 相同。
(8)[…]:方括号中可以出现一个或多个字符。它说明读入的是一个字符串,所以与 s
格式符类似。但输入的字符串中的字符必须是方括号中出现的字符,所以当遇到不是方括号
中的字符输入时,则终止相应的输入。当方括号中的第一个字符为’^’时,则刚好相反,即输
入字符串时,遇到属于方括号中出现的字符时就结束。
例如:scanf("%[^#]", text);
当输入:(computer and#science),仅将字符串“(computer and”存入 text 中,因为字符串中出
现了字符’#’,与方括号中的字符一致,使得输入结束。
说明:
– 261 –
C 语言程序设计

(1)当要读入的是 long 或 double 时,必须使用 ld 或 lf。


(2)与 printf()一样,在 scanf()函数中,输入参数的个数、类型等应与格式控制中的格式
符相对应,而且必须是指针,它指出转换后的每一个输入应存储的地址。例如:
int cont;
scanf("%d", &cont);
把读入的数据存放在变量 cont 中,而&cont 指出了它的地址。但对于字符串的读入,如:
char name[80];
scanf("%s", name);
字符数组名 name 是数组中的第一个元素的地址,所有读入的字符都是存放在以该地址开始
的空间中,因而在前面不必再加上地址操作符&。当要读入的字符不多于 20 个时可用下面语
句完成:
scanf("%20s", name);
(3)输入数据必须由空格、制表符、换行符分开。例如:
scanf("%d%d", &int1, &int2);
可接收 12 33 的输入,并分别赋给 int1、int2。但如果输入格式说明中使用了逗号等,则输入
数据就必须用逗号等分隔。例如:
scanf("%d,%d", &int1, &int2);
输入数据应为:12,33。
(4)对于语句:
scanf("%d%*c%d", &x, &y);
即输入 10/20,把 10 存入 x,扫描/后,把 20 存入 y。这是由于在%c 中用了修饰符*,舍弃了
读入的字符’/’。再看:
scanf("%dt%d", &x, &y);
即输入 10t20,仅将 10 存入 x,将 20 存入 y,而不存入与输入串中 t 相匹配的 t。
(5)当读入的字符与 scanf()函数所期望的类型不相匹配时,它不再进一步读入余下的任
何字符,并立即返回。例如:
scanf("%d %f %d", &int1, &float1, &int2);
若输入:
-456 x 898.00 34
由于函数在输入与 float1 相对应的数据时要求一个浮点数,而在输入中却是一个字符 x,所
以它不再为变量 float1、int2 读入数据,函数直接返回,返回值为1以表示只完成了一个数据
的输入。

10.6 文件的数据块读写

像数组、结构体等表示的是包含多个数据的结构,其操作要通过数组元素或结构体成员
分别进行。如果把数组或结构体的数据存储到文件或从文件读入程序,除了按照前面介绍的
操作对元素或成员进行读写外,C 语言程序还提供了专门的数据块标准读写函数。但文件类
型必须是二进制的。
– 262 –
第 10 章 文件

1.fread( )函数

格式:fread(buffer, size, count, fp);


功能:从 fp 指示的文件中读入 count 个数据,每个数据的大小是 size 个字节,读入的数
据存放到指针 buffer 所指示的内存位置上。
0 文件结束或出错
函数返回值 =
>0 读入的数据项数
buffer 通常是数组名或指向确定单元(数组或结构体)的指针,其类型应与所读数据类
型一致,fp 是文件指针,经 fopen()指向一个打开的文件。

2.fwrite( )函数

格式:fwrite(buffer, size, count, fp);


功能:把一块由指针 buffer 所指示的数据,写到 fp 指示的文件中。写入的数据共 count
个,每个数据的大小是 size 个字节。
函数返回值:写入的数据项数。
buffer 和 fp 的意义与 fread()相同。
例 10-10 学生记录包括姓名、学号、年龄,现要求从键盘输入 n 个同学的信息,并写
入二进制文件 student.dat,再从键盘任意输入一个学号,查找是否在文件中。
分析:学生记录用结构体表示,通过循环从键盘输入 n 个记录,依次写入文件,由于一
个记录是一块数据,使用 fwrite()写。为了在文件中查找数据,再重新用 fread()读文件并判断。
源程序:
#include <stdio.h>
typedef struct {
int no, age ;
char name[20] ;
} STU ;
main()
{
int n , i , rec ;
FILE *fp ; /* 定义文件指针 */
STU s;

scanf("%d",&n) ;
if ((fp=fopen("student.dat", "wb")) == NULL) { /* 打开文件 */
printf("can not open the file !\n"); exit(0);
}
for ( i=0 ; i<n; i++) {
scanf("%d%d%s", &s.no, &s.age, s.name) ;
fwrite( &s, sizeof(STU), 1, fp) ; /* 写一个结构体 */
}
if (fclose(fp)) { /* 关闭文件 */

– 263 –
C 语言程序设计

printf ( "Can not close file a.txt !\n" ); exit(0);


}
if ((fp=fopen("student.dat", "rb")) == NULL) { /* 打开文件 */
printf("can not open the file !\n"); exit(0);
}
scanf("%d",&rec) ;
while ( !feof(fp) ) {
fread( &s, sizeof(STU), 1, fp) ; /* 读一个结构体 */
if (s.no == rec)
break ; /* 已找到数据,循环结束 */
}
if (feof(fp) ) printf("Not found !\n") ;
else printf("No %d found !\n", n) ;
if (fclose(fp)) { /* 关闭文件 */
printf ( "Can not close file a.txt !\n" ); exit(0);
}
}
说明:本例程序的方法也适用于数组的块数据读写。

3.fgets( )函数

格式:fgets(str, n, fp);
功能:从 fp 指示的文件中读入 n−1 个字符(加上’\0’共 n 个),存放到数组或指针 str 所
指示的内存位置上。
str 值 读文件正常
函数返回值 =
NULL 文件结束或出错
如果读的字符中出现<回车>或 EOF,则读文件结束,输入的字符串长度小于 n−1。

4.fputs( )函数

格式:fputs(str , fp);
功能:把 str 所表示的字符串,写到 fp 指示的文件中。

0 写文件正常
函数返回值 =
非0 文件结束或出错

5.getw( )函数

格式:i=getw(fp);
功能:从 fp 指示的二进制文件中读一个整数到变量 i 上。

6.putw( )函数

格式:putw(i , fp);

– 264 –
第 10 章 文件

功能:把整数 i 写到 fp 指示的二进制文件中。

10.7 文件定位

前面介绍的文件读写操作,不涉及文件读写的位置。写文件时,不管是"w"还是"a"方式,
一定写在文件的尾部。读文件时则必须顺序地从头读到尾。文件读写的位置由文件指针 fp 规
定(fp 指向的是文件结构体,其成员 curp 指示缓冲区读写位置)。每调用一次标准文件读写
操作,文件读写位置 fp->curp 自动改变。C 语言文件操作中,不能任意改变写位置,但可以
通过程序修改读的位置。本节介绍的 rewind()和 fseek()函数,是用于确定文件读写位置的。

1.rewind()函数

格式:rewind(fp);
功能:把文件操作位置 fp->curp 修正到文件的开始处。
例 10-11 假定文件 a.txt 中保存有“abcdefg…”,写出下面程序段的输出。
fp=fopen("a.txt", "r") ;
for (i=0; i<3; i++)
putchar(fgetc(fp));
rewind(fp) ;
for (i=0; i<3; i++)
putchar(fgetc(fp));
分析:文件打开后,当在第一次 for 循环时,从文件读出 3 个字符 a、b、c,按顺序规则
下一次读出的字符应该是字符 d, 但 rewind(fp)把文件操作位置 fp->curp 调回到第一个字符上,
第二次 for 循环,从文件读出的 3 个字符仍然是 a、b、c。
输出结果如下:
abcabc

2.fseek()函数

格式:fseek(fp,位置修正值,当前位置设定)
功能:根据所设定的当前位置,把文件操作位置 fp->curp 按修正值进行调整。
当前位置设定如表 10-2 所示。
表 10-2 当前位置设定值定义

设定值 对应整型值 意 义

SEEK_SET 0 从文件头开始调整

SEEK_CUR 1 从文件当前位置开始调整

SEEK_END 2 从文件尾开始调整

位置修正值:位置改变的字节数,类型为 long 型。正数表示 fp->curp 向文件尾方向调整;


负数表示 fp->curp 向文件头方向调整。
– 265 –
C 语言程序设计

例 10-12 假定文件 a.txt 中保存有“abcdefgh10ijklmnop20qrstuv…”,下面程序段将输出


什么?
fp=fopen("a.txt", "r") ;
for (i=0; i<3; i++)
putchar(fgetc(fp));
fseek(fp, 7, SEEK_CUR); /* fp->curp 从当前位置向后调整 7 个字节(字符) */
for (i=0; i<3; i++)
putchar(fgetc(fp));
fseek(fp, 10, SEEK_SET); /* fp->curp 从文件头向后调整 10 个字节(字符) */
for (i=0; i<3; i++)
putchar(fgetc(fp));
分析:文件打开后,当在第一次 for 循环时,从文件读出 3 个字符 a、b、c,按顺序规则
下一次读出的字符应该是 d,但 fseek(fp, 7, SEEK_CUR)使 fp->curp 向后跳了 7 个字符,第
二次 for 循环,从文件读出的 3 个字符是 i、j、k。再经过 fseek(fp, 10, SEEK_SET)操作,
文件操作位置指示器 fp->curp 又调整为从文件头开始第 10 个位置后,所以程序段的输出结果
为:abcijkijk 。
fseek(fp, 0, SEEK_SET)等价 rewind(fp)。fseek(fp, -2, SEEK_END)表示文件操作位置指示
器 fp->curp 定位到文件最后两个字节上。

10.8 同时对文件读和写

我们前面介绍的文件操作是以只读或只写方式进行的,但 C 语言允许同时对文件进行读
和写。由于文件是按顺序操作的,fp->curp 决定了读或写的位置,当 fp->curp 指向文件中间
时,只能读文件;而写文件时必须写在文件的尾部,这时又不能读文件。因此对某一时刻来
说,文件不能既读又写,但通过文件定位函数,可以在一段时间里同时读和写。当然不需像
前面那样通过多次打开和关闭文件。
同时允许读和写的打开方式有 6 种,文本文件 3 种,二进制文件 3 种,如表 10-3 所示。
从编程角度看,文本文件与二进制文件处理方式相同,下面我们以文本文件为例介绍。
表 10-3 同时允许读和写的文件打开方式

文本文件 二进制文件 含 义

" r+ " " rb+ " 先以读方式打开文件,允许在文件未添加数据

" w +" " wb+ " 先写一个新文件,允许对文件从头再读

" a +" " ab+ " 先在老文件后添加数据,允许对文件从头再读

要实现对文件同时进行读写,必须借助文件定位操作。例如"r+"打开方式,fp->curp 将指
向文件头,这意味着先以读方式打开指定文件,然后允许在文件尾写入新的数据。如果文件
所有数据都被读出,fp->curp 已指向文件尾,则可以直接写数据。反之,文件只被读了部分
数据,fp->curp 指向文件中间,则需要把它调整到文件尾,才能写数据。
– 266 –
第 10 章 文件

例 10-13 假定文件 a.txt 中保存有“abcdefgh10ijklmnop20qrstuv”,要求先读出文件所有


数据,然后把文件开始的 3 个字符再读一遍,最后写入字母 ABC。
分析:文件 a.txt 需要先读后写,应以"r+"方式打开。当读了文件所有数据后,文件操作
位置 fp->curp 指向文件尾,想在文件开始处再读 3 个字符,需要把 fp->curp 定位到文件头上。
读了 3 个字母后,为了完成写数据,又需要把 fp->curp 定位到文件尾。
源程序:
#include <stdio.h>
main()
{
FILE *fp ; /* 定义文件指针 */
int n , i ;
if ((fp=fopen("a.txt", "r+")) == NULL) { /* 打开文件,可读可写 */
printf("can not open the file !\n"); exit(0);
}
while(!feof(fp))
putchar(fgetc(fp));
rewind(fp) ; /* fp->curp 定位到文件头 */
for (i=0; i<3; i++)
putchar(fgetc(fp));
fseek(fp, 0, SEEK_END); /* fp->curp 定位到文件尾 */
for (i=0; i<3; i++)
fputc(’A’+i,fp);
if (fclose(fp)) { /* 关闭文件 */
printf ( "Can not close file a.txt !\n" ); exit(0);
}
}
说明:程序的核心在于文件定位。当文件以"r+"打开时,文件位置 fp->curp 首先定位在
文件头上,完成读操作。如果想写数据,必须把 fp->curp 定位到文件尾,文件的头上或中间
不允许写入新数据。
如果文件以"w+"打开,将创建一个新文件,只有写入数据后才能读,否则文件中不存在
内容,无法读取。另外在读之前,必须把位置指示器定位到文件头上或中间某位置,即需要
读取数据的位置上。
当文件以"a+"打开时,fp->curp 首先自动定位在文件尾部,提供的是添加操作。如果想
读数据,必须把位置指示器定位到具体数据所在的位置上。
不管是哪一种打开方式,如果要写数据,必需把位置指示器定位到文件尾。如果想在文
件现有数据中修改、删除或插入数据,是非常困难的,主要原因在于文件不能在数据中间写、
修改或删除数据。要实现文件数据的修改,只能先把数据写到另一个文件上,写的过程中进
行相应数据的修改,然后把文件名字换回来。
例 10-14 假定文本文件 a.txt 中保存有“abcdefgh10ijklmnop20qrstuv”,要求把文件所有
数字改成大写字母 A。
分析:文件文件内容是无法直接修改的,我们定义一个 a.tmp 文件,把 a.txt 内容修改后
– 267 –
C 语言程序设计

暂时存在 a.tmp,然后写回 a.txt。a.tmp 需要先写后读,用"w+"打开。


#include <stdio.h>
main()
{
FILE *fp, *fpt ; /* 定义文件指针 */
char c ;
if ((fp=fopen("a.txt", "r")) == NULL) { /* 打开 a.txt 文件 */
printf("can not open the file !\n"); exit(0);
}
if ((fpt=fopen("a.tmp", "w+")) == NULL) { /* 打开 a.tmp 文件 */
printf("can not open the file !\n"); exit(0);
}
while(!feof(fp)) { /* 修改 a.txt 内容,暂时写到 a.tmp 中 */
c = fgetc(fp);
if (c>=’0’&&c<=’9’) c=’A’ ;
fputc(c, fpt) ;
}
if (fclose(fp)) { /* 关闭文件,以便重新以写方式打开 */
printf ( "Can not close file a.txt !\n" ); exit(0);
}
if ((fp=fopen("a.txt", "w")) == NULL) { /* a.txt 重新按只写方式打开 */
printf("can not open the file !\n"); exit(0);
}
/* 第 23 行 */
rewind(fpt) ; /* fp->curp 定位到 a.tmp 文件头,重读 */
while(!feof(fpt)) /* 把 t.tmp 写回 a.txt */
fputc(fgetc(fpt) , fp ) ;
if (fclose(fp)) { /* 关闭文件 */
printf ( "Can not close file a.txt !\n" ); exit(0);
}
if (fclose(fpt)) { /* 关闭文件 */
printf ( "Can not close file a.txt !\n" ); exit(0);
}
}
说明:程序第 23 行后的 3 行程序也可以写成:
remove("a.txt"); /* 删除文件 aa.txt */
rename("a.tmp", "a.txt"); /* 将文件 a.tmp 改名为 a.txt */
remove()和 rename()是两个标准库函数,可直接调用。

– 268 –
第 10 章 文件

习 题

1.选择题
(1)语句
#include <stdio.h>
printf("%d %d %d", NULL, ’\0’, EOF);
将输出______。
A.0 0 1 B.0 0 −1 C.NULL EOF D.1 0 EOF
(2)如果二进制文件 a.dat 已经存在,现要求写入全新的数据,应以____方式打开。
A."w" B."wb" C."w+" D."wb+"
(3)定义 FILE *fp; 则文件指针 fp 指向的是____。
A.文件在磁盘上的读写位置 B.文件在缓冲区上的读写位置
C.整个磁盘文件 D.文件类型结构体
(4)缓冲文件系统的缓冲区位于_____。
A.磁盘缓冲区中 B.磁盘文件中
C.内存数据区中 D.程序中
2.填空题
(1)fopen()函数的返回值是 。
(2)文件操作的 3 大特征是 、 和 。
(3)缓冲文件系统与非缓冲文件系统的不同点在于 。
3.阅读下面程序并说明程序执行的功能。
#include <stdio.h>
main()
{ char infile[10],outfile[10] ;
FILE *fpa, *fpb ;
gets(infile) ; gets(outfile) ;
fpa=fopen(infile, "r") ;
fpb=fopen(outfile, "w") ;
while ( !feof(fpa) )
fputc(fgetc(fpa) , fpb) ;
fclose(fpa) ;
fclose(fpb) ;
}
4.编写程序,统计一个文本文件中字母、数字及其他字符的个数。
5.编写程序,从键盘输入一系列实数(以特殊数值−1 结束)
,写到一个文本文件中。程
序运行后请检查所写文件的内容,并对程序修改所写数据间的分隔符,运行后再检查文件的
内容。
6.编写程序,以比较两个文本文件的内容是否相同,并输出两文件内容首次不同的行号
和字符位置。

– 269 –
C 语言程序设计

7.编写程序,用于将文本文件 test.txt 中所有包含字符串"for"的行输出。


8.编写程序,将一个 C 语言源程序文件中所有注释去掉后,存入另一个文件。
9.文本文件 a1.txt 和 a2.txt 中包含若干从小到大排过序的整数,现要求把两个文件中的
数据合起来,仍按从小到大顺序写入文件 a3.txt 中,试编写相应程序。
10.文本文件 int.txt 中包含若干整数,请把文件中所有数据相加,并把累加和写入文件
最后,试编写相应程序。
11.学生信息组织成结构体,包含姓名和数学、英语、程序设计 3 门课成绩。请从键盘
输入 5 个同学的信息,分别用 fprinf()和 fwrite()写入文件,试编写相应程序。
12.二进制文件 dim.dat 中包含二维数组数据,已知二维数组每行有 5 个整数,行数不
定。请编写一个程序,找出平均值最大的行,输出行号和平均值。
13.改造例 10-7,采用动态申请内存的函数,用指针代替数组,完成对文件中数据的排
序。

– 270 –
第 11 章 C 语言程序设计方法
高效率、高水平地开发具有一定规模的软件,必须遵循一定的程序设计方法。在学习了
C 语言各部分内容后,下一阶段应逐步提高程序设计的能力,增大程序编写的规模。编程者
要综合运用 C 的各种功能(语句),从十几行的程序向上百行、上千行规模的程序发展,提
高对大程序的驾驭能力。编写一个具有一定规模的实用程序,是提高程序设计能力的有效途
径,如俄罗斯方块、贪吃蛇、五子棋、华荣道等游戏程序。
本章将主要介绍结构化程序设计的概念、程序设计风格以及在 C 语言程序设计中要注意
的问题。这对于进一步提高读者程序设计能力,培养良好的程序设计风格有一定的指导意义。
在学习了 C 语言前面章节后,经过一定量的编程实践,读者应该有了一些自己的程序设计体
会,学完本章内容后相信会对读者进一步了解程序设计有所帮助。

11.1 结构化程序设计方法

20 世纪 60 年代末,计算机界提出了“软件工程”概念,开始认识到软件作为一种产品,
必须走工程化的道路,以满足人们对软件日益增长的需求。结构化程序设计(Structured
Programming)首先由著名计算机科学家 E. W. Dijkstra 于 1969 年提出,此后计算机界对结构
化程序设计进行多方面的研究,设计了 PASCAL、C 等结构化语言。
结构化程序设计强调程序设计的风格和程序结构的规范化,提倡清晰的结构,其基本思
路是把一个复杂问题的求解过程分阶段进行,每个阶段处理的问题都控制在人们容易理解和
处理的范围内。具体实现步骤包括:
(1)按自顶向下方法对问题进行分析、设计;
(2)系统的模块设计;
(3)结构化编码。

11.1.1 自顶向下分析设计问题
由于人的思维能力有限,人们对问题规模的驾驭能力就受到限制,结构化程序设计方法
是解决人脑思维能力的局限性与所处理问题的复杂性之间矛盾的一个有效办法。自顶向下结
构化的程序设计方法,可以把大的复杂的问题分解成小问题,然后各个击破。
对于一个复杂问题,首先进行上层(整体)的分析与设计,按其组织或功能将问题分解
成若干个子问题,如果所有的子问题都得到了解决,整个问题就解决了。而解决子问题无论
在规模上还是在复杂性上都会大大低于原问题。如果子问题仍然十分复杂,再对它进一步分
解,如此一层一层地分解下去,直到处理对象相对简单,容易处理为止。每一次分解都是对
上一层进行细化,逐步求精,最终形成一种层次结构(类似树形),用于精确描述分析设计的
– 271 –
C 语言程序设计

结果。
例如,要构造一个图书馆管理程序,首先按其功能把整个管理分为 4 块:图书登录、借
书、还书和预约,一旦这 4 个功能都能实现,连接起来就形成了图书馆管理程序,但每一个
模块还比较复杂,难于直接实现,我们可以进一步分解每一个模块。图 11-1 给出了图书馆管
理程序设计的层次结构图。图中每一项内容都是程序设计中的模块,在 C 语言中用函数实现,
相互之间的连接线就是他们的调用关系。

主程序

图书登录 借书 还书 预约
 

  



   




 



!"

!
!"
图 11-1 图书馆管理程序设计的层次结构

若把问题 X 的复杂程度定义为 C(X),解决问题的工作量定义为 E(X)。如果 X 能分解成


x1 和 x2,一般我们能得到下面的结论:
(1)如果 C(x1)>C(x2),则 E(x1)>E(x2),即难的问题需要的工作量要大一些;
(2)C(X)>C(x1)+C(x2);
(3)E(X)>E(x1)+E(x2)。
但并不是说,把问题无限分下去实现最容易,模块过小,则管理这些模块会变得复杂,因此
需要寻找最佳的平衡点。
问题分解的方法便于验证算法的正确性。当向下一层展开时,应仔细检查本层设计是否
正确,只有上一层正确才能向下细化。如果每一层设计都没有问题,则整个算法基本上就是
正确的,由于每一层都是按照其组织形式或所具有的功能进行细化,是一个逐步的过程,难
度上易于把握,容易保证整个算法的正确性,而且模块之间关系明确,检查测试也可以从上
到下逐层进行。这样做,思路清楚,有条不紊地一步一步进行,即严谨又方便。
按照自顶向下方法设计系统,有助于后面模块的程序设计、各模块的调试验证以及系统
最终按层次集成起来。

11.1.2 模块化程序设计
经过自顶向下方法分解问题,设计出相应的层次结构图,图中每一个方块就是系统中的
一个模块。模块化程序设计可以使软件结构清晰,不仅容易设计也容易阅读和理解。由于程
序错误通常局限在有关的模块及它们的连接上,模块化使软件容易测试和调试,因而有助于
提高软件的可靠性。因为变动往往只涉及少数几个模块,所以模块化能够提高软件的可修改
– 272 –
第 11 章 C 语言程序设计方法

性。模块化也有助于软件开发工程的组织管理,一个复杂的大型程序可以由许多程序员分工
编写不同的模块。
在设计某一个具体的模块时,模块中包含的语句一般不要超过 50 行,这既便于编程者思
考与设计,也利于程序的阅读。一个模块应具有良好的独立性,使得程序模块的编写、调试
都可以独自完成,尽量减少模块之间的相互影响,以免带来相互间的干扰。模块在 C 语言中
通过函数来实现,一个模块对应一个函数,如果该模块功能复杂,可以进一步调用低一层的
模块函数,以实现结构化的程序设计思想。
模块独立性主要体现在以下几方面。
(1)一个模块只完成一个指定的功能。若一个模块完成多个逻辑功能,这对程序的调试
与维护都会带来一些问题,最突出的问题是对模块某一逻辑功能修改时,可能会影响到该模
块上的其他逻辑功能的实现。
(2)模块之间通过参数传递发生联系,尽量避免用全局变量做不同模块间的数据关
联。
(3)函数内要尽量使用自动变量,慎用全局变量。
(4)一个模块只有一个入口和一个出口。

11.1.3 结构化程序编写
当一个软件经模块化设计后,每一个模块可以独立编程。业已证明,任何只含有一个入
口和一个出口的程序均可由顺序、选择和循环 3 种结构组成。不仅程序本身是单入口单出口
的,而且程序的每一局部结构也应单入口单出口。从结构上讲,进入顺序、选择或循环结构
是单入口的,当执行完结构离开时,也必定是单出口的。因此在编程时,要严格采用 3 种基
本程序结构。
编程时应尽量少用或不使用 goto 语句,它会破坏程序整体的结构性,使程序结构变得复
杂,难以确保程序的正确性,并降低了程序的可阅读性。
C 语言程序提供了丰富的语句来实现这 3 种程序结构,对于编程者来说,应首先确定适
合于 3 种基本结构的程序流程图,然后使用相应语句。
例 11-1 编写一个函数模块,将两个已排序好的整型数组 a1 和整型数组 a2 合并到数组
a2,并使得合并后的数组也是有序的。假定合并后的数组不超过 100 个元素。
分析:函数需要以参数方式得到合并对象,即数组 a1 和数组 a2,经合并操作后,结果
体现在数组 a2 上。由于数组参数是以地址传递的,所以形参数组的改变将直接改变调用函数
中的实参,函数无需用 return 返回结果。为了使函数能处理不同个数的数据,形参中最好包
含数组 a1 和数组 a2 的数据个数 n1 和 n2。在模块设计中,参数与返回结果被称为模块的接
口,应在算法设计之前确定。
为了实现两个数组的合并,我们定义局部变量数组 b[100],用于存放数组 a2 的内容,腾
出数组 a2 空间保存结果。然后依次比较数组 a1 头上元素与数组 b 头上元素,小的值先赋值
到数组 a2 上,直到所有的数据都依次被赋值到数组 a2 为止。在循环比较过程中,必定存在
数组 a1 或数组 b 中的一个全部得到处理,而另一数组尚存未处理数据(必定有一个),需要
整体复制。函数的算法思路以流程图描述,如图 11-2 所示。

– 273 –
C 语言程序设计

开始

Y
a2 数据个数 n2=0

N 数组 a2 ← 数组 a1
Y
a1 数据个数 n1=0

N
数组 b ←数组 a1 循环 1(简化)

i=j=k=0
循环 2

i<n1&&j<n2 N
选择 31

Y
N Y
a1[i]>b[j]

a2[k]←a1[i], i++ a2[k]←b[ j], j++

选择 32
k++

Y
i<n1

N 循环:a2[k]←a[i]

j<n2 Y

N 循环:a2[k]←b[ j]

函数返回

图 11-2 例 11-1 的流程图

上述流程框图中,每一个虚线框都是单入口单出口的,各虚线框之间体现一种顺序结构,
循环 2 框内包含了两个顺序的选择结构框。程序总体结构如图 11-3 所示。

– 274 –
第 11 章 C 语言程序设计方法

选择 1

选择 2

循环 1

循环结构 2

选择 31

选择 4
选择 32

选择 5

图 11-3 程序总体结构

源程序:
void merge(int a1[ ], int n1, int a2[ ], int n2)
/* 将数组 a1 和 a2 合并到 a2 中。n1 是数组 a1 的数据个数,n2 是数组 a2 的数据个数 */
{ int i,j=0,k=0, b[10];
if (n2==0)
for ( ; j<n1; j++,k++) a2[k]=a1[j]; /* 这里数组 a2 中没有数据 */
else
if (n1>0){ /* 如果 n1=0,则数组 a1 中没有数据,函数结束 */
for (i=0; i<n2; i++) b[i]=a2[i];
i=0;
while (i<n1&&j<n2)
{ if (a1[i]>b[j]) {
a2[k]=b[j];
j++; }
else{
a2[k]=a1[i];
i++; }
k++ ;
}
/* 数组 a1 或数组 a2 中的其余数据复制到数组 a2 中的过程 */
if ( i<n1 )
for ( ; i<n1; i++,k++) a2[k]=a1[i];
if ( j<n2 )
for ( ; j<n2; j++,k++) a2[k]=b[j];

– 275 –
C 语言程序设计

}
} /* 没有返回值 */

11.2 程序设计风格

任何商品软件在开发过程中存在调试步骤,在使用过程中存在维护工作,维护包括新发
现的错误更正、新功能的补充。一个具有良好风格的程序,对程序的调试与维护效率起着至
关重要的作用。因为程序的可读性非常重要,即程序看上去要通俗易懂。所谓“通俗”指程
序实现的思路要清晰明了,不可为了刻意追求执行的效率,或为展示自己的与众不同,设计
别人难以看懂的算法;而为做到“易懂”,要求在书写格式上尽量提高可读性。下面介绍对具
有良好风格的程序的一些要求。

11.2.1 源程序文档化

1.符号的命名

在对函数名、变量名、常量名等符号命名时,应根据其作用、意义来命名,使得能见名
知意,有助于对函数或变量功能的理解。例如计数变量用 count,求最大值函数取名 max 等,
切不可为图输入方便而不考虑其意义,一律取名 x、y、a、b、c 等。用汉语拼音的缩写也不
是好办法,汉字存在一音多字。另外,一个变量应只用于一种用途,切勿“身兼数职” ,给阅
读程序带来混乱。

2.程序的注释

必要的注释对程序阅读是必不可少的,它可为其他人阅读程序带来方便。注释是工程化
文档的一个组成部分,包括以下几种。
(1)序言性注释
在每个程序模块的开头应注明模块的功能、主要算法思想、接口参数说明、返回结果说
明和主要数据描述等。
(2)功能性注释
在源程序中体现算法的主要语句上给予注解,解释该语句的作用、该语句对前面或后面
语句的影响。一般不需要对每一条语句都做注解。
(3)视觉组织
一个程序如果写得密密麻麻,将大大影响阅读时的视觉效果,要合理使用空格、空行及
缩进。在 C 语言程序中缩进尤其重要,通过缩进可以充分体现程序的结构化,使得顺序、选
择和循环结构更加清晰明了。读者可以参看本书中的例题源程序。

11.2.2 语句结构
程序中语句构造力求简单、直接,具体要求如下。
(1)一行内只写一条语句,并采用缩进格式。

– 276 –
第 11 章 C 语言程序设计方法

(2)程序编写首先应当考虑清晰性和易懂性,除非对效率有特殊要求,一般情况下程序
要做到清晰第一,效率第二,不要为追求效率而丧失清晰性。
(3)程序编写要简单清晰,直截了当地表达编程者的意图。
(4)增加括号来清晰表示各种表达式的运算顺序。
(5)经常反躬自省:如果这个程序是别人写的,我能看得懂吗?

11.2.3 良好的交互特性
交互性是软件非常基本的特性,就用户而言,交互性可以说是最重要的特性。下面是有
关交互性的一些基本要求。
(1)所有的输入都应该有提示,告诉用户应输入什么样的数,输入的格式是什么。
(2)尽量减少用户输入的数据量,具有常规值的数据,应进行缺省值设置。
(3)输出的数据应有所说明。
(4)输出数据尤其数据量较大时,要注意采用统一整齐的格式。

11.3 C 语言程序设计中要注意的问题

C 语言具有强大的功能,程序设计灵活方便,但初学者往往难于掌握程序设计的方法和
技巧,在运用 C 语言时常常会产生一些问题和错误。下面列举一些常见的问题,以帮助读者
引起注意,纠正概念上的模糊。

11.3.1 正确使用运算符

1.易混淆的运算符

C 语言中有些运算符(如表 11-1 所示)很容易混淆,读者在使用时要非常小心。


表 11-1 易混淆的运算符

赋值运算符“=” 逻辑运算符“==”

关系运算符“<” 位运算符“<<”

关系运算符“>” 位运算符“>>”

逻辑运算符“||” 位运算符“|”

逻辑运算符“&&” 位运算符“&”

逻辑运算符“!” 位运算符“~”

“.” “->”

(1)赋值运算符“=”和逻辑运算符“==”
语句
if(x=y)

– 277 –
C 语言程序设计

break;
在编译时不会出错,它是合法的,因为 x=y 是赋值表达式,其作用就是把 y 的值赋给 x,然
后将变量 x 的值作为表达式的值。if 语句以此作为判断条件,只有当 y=0 时,if 条件为假,
其他 y 值都会使 if 条件为真。因此它并没有判断 x 与 y 是否相等。
若要判断 x 是否等于 y,必须用逻辑运算符“==”,即
if(x==y)
break;
(2)位运算符“|”和逻辑运算符“||”
程序段
int x=2, y=1;
printf("x||y =%d/t",x||y);
printf("x|y =%d/n",x|y);
输出:x||y =1 x|y =3
上例中,x、y 都大于 0,因此执行 x||y 时结果为真(即 1);执行 x|y 时,要先将 2 和 1
转换为 0010 和 0001,再按位进行或运算,然后得出 0011(即 3)。

2.易用错的运算符

(1)除运算符“/”
当除运算符“/”的运算量是整型量时,运算结果也是整型。
例如:1/2=0
(2)求余运算符“%”
求余运算符“%”的运算量只能是整型。
例如:5%2=1,而 5.0%2 是错误的。
(3)强制类型转换“ (类型名)”
例如:(int)5.0%2=1,不能写成:int(5.0)%2=1。
(4)自加、自减运算符“++” 、“--”
自加、自减运算符有 4 种使用情况。
程序段 1
k=5;
x=k++;
执行后,x=5,k=6。
程序段 2
k=5;
x=++k;
执行后,x=6,k=6。
程序段 3
k=5;
x=k--;
执行后,x=5,k=4。
程序段 4

– 278 –
第 11 章 C 语言程序设计方法

k=5;
x=--k;
执行后,x=4,k=4。

3.运算符的优先级

语句
while( c=getchar() != EOF)
putchar(c);
由于赋值运算符的优先级比关系运算符的优先级低,相当于执行了
while( c=(getchar() != EOF))
putchar(c);
即通过函数 getchar()获得一个字符,并与 EOF(输入结束符)比较,将比较结果赋值给 c。c
的值就是一个逻辑量(0 或 1) ,当 c 的值是 1(即输入字符不是 EOF)时,输出 ASCII 码值
为 c 的字符;当 c 的值是 0(即输入字符是 EOF)时退出循环。如果编程者的原意是将输入
的一串字符(以 EOF 作为输入结束符)原样输出,则正确的写法应是:
while((c=getchar()) != EOF)
putchar(c);
此时,c 得到一个从键盘中输入的字符,而不是一个逻辑结果。
当编程时对运算符优先级没有把握时,不妨增加一些括号,以明确表达式运算顺序。

11.3.2 正确的数据类型操作

1.注意数据的合理取值范围

(1)基本整型数据
对只有两个字节的 int 型整数,其取值范围为−32768~32767。
程序段
int num;
num = 0x123456;
printf("%x", num);
输出:3456
因为 0x123456 超出了 int 型整数的取值范围,产生溢出,所以高位被截去。
(2)无符号类型数据
无符号型常量不能表示小于 0 的数,如−200U 是错误的。

2.变量要先定义后使用

程序
main()
{ a = 100;
printf("%d", a);
}

– 279 –
C 语言程序设计

在编译时会出错,因为变量 a 没有定义。

3.不要在定义变量时连续赋初值

语句
int a = b = c = 5;
是不合法的,因为对变量赋初值是在程序编译时进行的,对 a 赋初值时变量 b 和 c 还没有被
说明。下面的程序段是合法的:
int a, b, c;
a = b = c = 5;
它等价于:
int a, b, c;
a = (b = (c = 5)));

4.注意字符常量与字符串常量的区别

字符常量指用单引号括起来的一个字符,如’M’、’9’ 、’$’等。
字符串常量指用双引号括起来的一串字符,如"M"、"Hello"等。
程序段
char ch= "M";
char *ptr=’a’;
printf(’\t’);
putchar("\n");
是错误的,应改为:
char ch= ’M’;
char *ptr="a";
printf("\t");
putchar(’\n’);

5.正确调用输入函数 scanf()

scanf()函数的一般调用格式是:
scanf(格式控制,输入参数表)
(1)输入参数必须是某个存储单元的地址
程序段
int a;
char str[80];
scanf("%d%s", a, str);
是错误的,因为 a 不是一个地址,应改为:
scanf("%d%s", &a, str);
(2)输入参数的类型与格式说明的类型应该一一对应匹配
程序段
int a;
float f;
– 280 –
第 11 章 C 语言程序设计方法

scanf("%d%f", &f,&a);
是错误的,因为 f 是实型变量,对应的格式说明符是%f,而 a 是整型变量,对应的格式说明
符是%d,最后一句应改为:
scanf("%f%d", &f,&a);

6.调用标准库函数时必须包含相关的头文件

C 语言标准库中的许多函数同它们特定的数据类型一起工作,如果要用到这些库函数,
就必须访问这些数据类型。这些数据类型的定义放在 C 语言的标准头文件中。标准头文件还
存放库函数的函数原型说明以及一些常量等。因此,当我们调用库函数时,必须在源文件中
包含相关的头文件如表 11-2 所示。
表 11-2 与调用函数相对应的包含头文件

调用函数 被包含的头文件

数学函数 "math.h"

字符函数 "ctype.h"

字符串函数 "string.h"

输入输出函数 "stdio.h"

程序
main()
{
float m;
m = sqrt(5.0);
printf("%f\n", m);
}
是错误的,它在调用库函数 sqrt()和 printf()时没有包含相关的头文件,应改为:
#include <stdio.h>
#include <math.h>
main()
{
float m;
m = sqrt(5.0);
printf("%f\n", m);
}

7.数组元素的下标不能越界

语句
int arr[10];
定义了有 10 个元素的数组 arr,数组元素的下标从 0~9,所以下面的语句是不对的:
for(i = 1; i <= 10; i++)

– 281 –
C 语言程序设计

printf("%d\t", a[i]);
因为当 i 等于 10 时,a[i]就越界了。这类错误有时很难查出来,因为 C 编译不会给出任何越
界出错信息,所以读者在编程时应高度重视。

8.引用指针变量前要先赋值

程序段 1
char *ptr;
scanf("%s", ptr);
是错误的,因为 ptr 是一个指针变量,但它到底指向哪一个内存单元还没有明确。下面的程
序段是正确的:
char *ptr, buf[80];
ptr = buf;
scanf("%s", ptr);
程序段 2
int *p;
*p = 100;
是错误的,因为指针变量 p 没有先赋值,应改为:
int a;
int *p=&a;
*p = 100;

9.区别指针变量的值和它所指存储单元的内容

程序段 1
int a;
int *p;
p = &a;
指针变量 p 指向整型变量 a,p 的值与&a 相同,都是地址,而 a 的值与*p 的值一样,都是一
个整型量。此时,指针变量 p 的值是一个地址,它所指存储单元的值就是整型变量 a 的值。
程序段 2
int a=1;
int *p;
p = &a;
printf("%d",*p);
输出:1
此时,p 的值不是 1,而是一个地址(变量 a 的地址)
,*p 的值才是 p 所指向变量 a 的值 1。
程序段 3
int a=1;
int *p1, *p2;
p1 = &a;
p2 = p1;
a++;

– 282 –
第 11 章 C 语言程序设计方法

printf("%d %d %d",a,*p1,*p2);
输出:2 2 2
把 p1 的值(变量 a 的地址)赋给 p2 后,p1 和 p2 同时指向变量 a,*p1 和*p2 的值就是
变量 a 的值 2。

10.不同类型的指针变量不能混合使用

程序段
int k = 3, *pk;
float f = 1.5, *pf;
pk = &k;
pf = &f;
pf = pk;
混用了不同类型的指针变量,最后一句应改为:
pf = (float *) pk;
这样 pf 就指向了变量 k。

11.数组名是一个指针常量

程序段
#define N 4
int a[N]={1,2,3,4};
int i;
for(i=0; i<N; i++,a++)
printf("%d",*a);
是错误的,因为 a 是一个指针常量,它的值是不能改变的(a++是非法的),应改为:
#define N 4
int a[N]={1,2,3,4};
int *p;
for(p=a; p<a+N;p++)
printf("%d ",*p);

12.在函数调用时可以传送地址

程序段
#include <stdio.h>
main()
{
int x=30; y=20;
fun(&x,y);
printf("%d %d\n", x,y);
}
void fun(int *a, int b)
{

– 283 –
C 语言程序设计

*a++;
b++;
printf("%d %d;", *a,b);
}
输出:31 21;31 20
通过传送变量 x 的地址, 在被调用函数 fun()中直接改变了主调函数 main()中变量 x 的值。

11.3.3 正确的语句运用

1.注意 if-else 语句的匹配

程序段
int a = 2, b = 4;
if(a == 1)
if(b == 4)
printf("%d\n", a = a+1);
else
printf("%d\n", b = b-1);
printf("%d\n", a);
输出:2
这是因为 else 子句总是与前面最近的不带 else 的 if 相结合,与书写格式无关,若要改变
这种情况就要用大括号。
要输出 3 和 2,上例可改写成:
int a = 2, b = 4;
if(a == 1)
{
if(b == 4)
printf("%d\n", a = a+1);
}
else
printf("%d\n", b = b-1);
printf("%d\n", a);

2.注意分号在语句中的作用

程序段 1
if(a>b) ; /* 注意这里的分号是一个空语句 */
printf("A is larger than B\n");
不论 a>b 是否为真,都会输出 A is larger than B。
程序段 2
for(k=1;k<5;k++) ; /* 注意这里的分号是一个空语句 */
printf("%d ",k);
不会输出 1 2 3 4,而是输出 5,因为 for 循环的循环语句是一个空语句,当退出循环时,才输

– 284 –
第 11 章 C 语言程序设计方法

出 k 的值。

3.注意 switch 语句中的 break 语句的运用

程序段
count=1;
switch(count) {
case 1: printf("first ");
case 2: printf("second ");
case 3: printf("third");
}
输出:first second third
若对每一种情况只输出一种结果,应采用 break 语句,可将以上程序段改为:
count=1;
switch(count) {
case 1: printf("first "); break;
case 2: printf("second "); break;
case 3: printf("third");
}
输出:first
总之,在学习 C 语言的过程中,要注意细微之处,这样就可以避免一些常见的错误。由
于 C 语言中的有些错误是较难找出来的,如涉及有关指针、数组、地址和文件的操作,错误
往往是很隐蔽的,所以平时要多做编程练习,才能熟能生巧,避免错误。另外,熟练运用上
机调试手段是查找错误的有效途径。

– 285 –
附录 A C 语言上机操作指导
程序设计是实践性很强的过程,任何程序最终都必须在计算机上运行,以检验程序的正
确与否。因此在学习程序设计时,一定要重视上机实践环节,通过上机可以加深理解 C 语言
的有关概念,巩固理论知识,也可以培养程序调试的能力与技巧。

A.1 C 语言程序的上机步骤

按照 C 语言语法规则而编写的 C 语言程序称为源程序。源程序由字母、数字及其他符号
等构成,在计算机内部用相应的 ASCII 码表示,并保存在扩展名为“.C”的文件中。源程序
是无法直接被计算机运行的,因为计算机的 CPU 只能执行二进制的机器指令。这就需要把
ASCII 码的源程序先翻译成机器指令,然后计算机的 CPU 才能运行翻译好的程序。
源程序翻译过程由两个步骤实现:编译与连接。首先对源程序进行编译处理,即把每一
条语句用若干条机器指令来实现,以生成由机器指令组成的目标程序。但目标程序还不能马
上交给计算机直接运行,因为在源程序中,输入、输出以及常用函数运算并不是用户自己编
写的,而需要直接调用系统函数库中的库函数。因此,必须把“库函数”的处理过程连接到
经编译生成的目标程序中,生成可执行程序,并经机器指令的地址重定位,便可由计算机运
行,最终得到结果。
C 语言程序的调试、运行步骤,如图 A-1 所示。

编 译 连 接 运 行
编 辑

源程序 目标程序 可执行程序 结 果


开始
.C .OBJ .EXE

语法错误 连接错误 运行错误

图 A-1 C 语言程序的调试、运行步骤

图 A-1 中,虚线表示当某一步骤出现错误时的修改路线。运行时,无论是出现编译错误、
连接错误,还是运行结果不对(源程序中有语法错误或逻辑错误),都需要修改源程序,并对
它重新编译、连接和运行,直至将程序调试正确为止。
除了较简单的情况,一般的程序很难一次就做到完全正确。在上机过程中,根据出错现
象找出错误并改正称为程序调试。我们要在学习程序设计过程中,逐步培养调试程序的能力,
– 286 –
附录 A C 语言上机操作指导

它不可能靠几句话讲清楚,要靠自己在上机中不断摸索总结,可以说是一种经验积累。
程序中的错误大致可分为 3 类。
(1)程序编译时检查出来的语法错误。
(2)连接时出现的错误。
(3)程序执行过程中的错误。
编译错误通常是编程者违反了 C 语言的语法规则,如保留字输入错误、大括号不匹配、
语句少分号等。连接错误一般由未定义或未指明要连接的函数,或者函数调用不匹配等因素
引起,对系统函数的调用必须要通过“include”说明。
对于编译连接错误,C 语言系统会提供出错信息,包括出错位置(行号)和出错提示信
息等。编程者可以根据这些信息,找出相应错误所在。有时系统提示的一大串错误信息,并
不表示真的有这么多错误,往往是因为前面的一两个错误带来的。因此当你纠正了几个错误
后,不妨再编译连接一次,然后根据最新的出错信息继续纠正。
有些程序通过了编译连接,并能够在计算机上运行,但得到的结果不正确,这类程序执行
过程中的错误往往最难改正。错误的原因一部分是程序书写错误带来的,例如应该使用变量 x
的地方写成了变量 y,虽然没有语法错误,但意思完全错了;另一部分可能是程序的算法不正
确,解题思路不对。还有一些程序有时计算结果正确,有时不正确,这往往是编程时,对各种
情况考虑不周所致。解决运行错误的首要步骤就是错误定位,即找到出错的位置,才能予以纠
正。通常我们先设法确定错误的大致位置,然后通过 C 语言提供的调试工具找出真正的错误。
为了确定错误的大致位置,可以先把程序分成几大块,并在每一块的结束位置,手工计算
一个或几个阶段性结果,然后用调试方式运行程序,到每一块结束时,检查程序运行的实际结
果与手工计算是否一致,通过这些阶段性结果来确定各块是否正确。对于出错的程序块,可逐
条仔细检查各语句,找出错误所在。如果出错块程序较长,难以一下子找出错误,可以进一步
把该块细分成更小的块,按照上述步骤进一步检查。在确定了大致出错位置后,如果无法直接
看出错误,可以通过单步运行相关位置的几条语句,逐条检查,一定能找出错误的语句。
当程序出现计算结果有时正确有时不正确的情况时,其原因一般是算法对各种数据处理
情况考虑不全面。解决办法最好多选几组典型的输入数据进行测试,除了普通的数据外,还
应包含一些边界数据和不正确的数据。比如确定正常的输入数据范围后,分别以最小值、最
大值、比最小值小的值和比最大值大的值,多方面运行检查自己的程序。
下面我们分别以 Turbo C 2.0(以下简称 TC 2.0)和 Visual C++ 6.0(以下简称 VC++ 6.0)
为上机平台,对 C 语言程序编译、连接和调试做简单介绍。建议一开始学习上机时,把注意
力放在程序的编译、连接和运行,而把调试部分放到学习了第 5 章后再看,只有具有一定的
程序语句量,调试才有作用。

A.2 Turbo C 语言集成环境

Turbo C 是一个常用的、最基本的 C 语言工具,一般简称 TC。它为 C 语言程序开发提供


了操作便利的集成环境。源程序的输入、修改、调试及运行都可以在 TC 集成环境下完成,
非常方便有效。TC 2.0 系统非常小巧,但功能齐全。它主要支持 DOS 环境,因此在操作中
– 287 –
C 语言程序设计

无法使用鼠标,需要通过键盘操纵菜单或快捷键完成。

A.2.1 Turbo C 启动
由于 TC 支持 DOS 环境,TC 系统的安装十分方便。如果有安装盘,可以按照提示一步
步完成安装;如果没有安装盘,可以从其他机器直接拷贝已安装好的系统。
由于 TC 是在 DOS 环境下工作的,需要首先要找到 TC 系统的安装(复制)目录,用鼠
标双击其中的 tc.exe 启动 TC 系统,其界面如图 A-2 所示。
菜单

编辑窗口

信息窗口
热键
图 A-2 TC 界面

图 A-2 中,菜单包含了所有操作的功能;编辑窗口是用于输入、修改程序的区域;信息
窗口将显示程序编译、连接和运行过程中的错误信息或有关提示信息;热键提示将给出常用
操作的快捷键提示信息,以方便用户的操作。

A.2.2 运行程序
要运行一个 C 语言程序,必须经过输入源程序、修改错误、编译连接和运行几个步骤。

1.编辑

在编辑窗口中,直接输入程序。如果要进行修改,可以使用【↑】、
【↓】、
【←】、
【→】4
个方向键,移动光标到所需位置,然后删除错误,输入正确的内容。

2.编译、连接和运行

同时按下【Ctrl】+【F9】键,将对编辑窗口中的程序,完成编译、连接和运行 3 个步骤。
如果程序没有错误,将直接运行程序。
如果存在编译错误,信息窗口中将显示错误信息,并终止连接与执行步骤。
如果编译通过,但存在连接错误,信息窗口中将显示错误信息,并终止执行步骤。
任何错误都必须纠正后,重新按【Ctrl】+【F9】键运行。如果还有错,继续修改,直到
能正确执行为止。
即使通过了编译和连接,并不能说明程序就没有错误了,解题思路错误或语句的错误使
用(语句格式没有错) ,都会导致无法得到正确的结果,甚至程序无法正确执行。如果程序在
– 288 –
附录 A C 语言上机操作指导

执行过程中无法结束(死机),可以按【Ctrl】+【break】键强制结束。

3.输入数据

如果程序中有 scanf( )语句,则屏幕将出现一个黑底的输入窗口,等待输入数据。通常是


输入一个数据,再输入一个空格或回车,然后输入下一个数据,直到输入完所有的数据。如
果 scanf( )语句中格式规定是逗号分隔,则各数据之间要输入逗号。

4.查看结果

输入完数据,系统将自动关闭输入窗口,运行程序后,回到编辑窗口。但运行结果并未
在屏幕上显示,若要查看运算结果,需要按【Alt】+【F5】键才能显示输出窗口。看完后,
按任意键可关闭输出窗口。
对于没有输入要求的程序,按【Ctrl】+【F9】键运行,屏幕上不会有什么变化,但这并
不表示程序未被运行,按【Alt】+【F5】键就能看到结果。
通过上述步骤,读者可以快速掌握 C 语言的上机过程,运行自己的程序,但还只能处理
一些简单的问题,如果想更有效地调试运行 C 语言程序,还需要掌握下面的内容。

5.运行环境设置

第一次上机时,如果一个简单正确的程序无法运行,需要检查一下运行环境是否设置正
确。具体方法参看下面“Turbo C 菜单”一节中介绍的 Option 菜单项。

A.2.3 Turbo C 菜单
前面介绍了上机过程中一些最基本的操作。为了对 TC 的功能有一个全面的了解,我们
将对各主要菜单项分别予以介绍。
TC 环境下不能使用鼠标,打开菜单有两种方法。
(1)按【F10】键,然后按【←】或【→】键选择相应菜单位置,再按【Enter】键即可。
(2)按【Alt】+【菜单上红色大写字母】键。
当选中一个菜单后,会弹出下拉菜单项,通过【↑】或【↓】键可选择相应菜单项,再
按【Enter】键,完成菜单功能操作。按【Esc】键可以取消菜单选择(不是撤销菜单功能)。
TC 中提供了完善的帮助信息,按【F1】键屏幕上将弹出帮助窗口,但其中的信息都是
英文的。
TC 的快捷键通常是组合键,如【Ctrl】+【F9】键表示要同时按下两个键。对于三键的
组合键,其前两个键要同时按下,松手后再按第三个键,如【Ctrl】+【k】+【b】键表示先
同时按下【Ctrl】键和【k】键,松手后再按【b】键。

1.File 菜单(文件操作)

Load(【F3】 ):调入一个已存在的程序文件(扩展名为.C)。【F3】是快捷键。
Pick(【Alt】+【F3】):从最近曾经调入过的文件中选择一个调入。
New:清除编辑窗口中的程序,以供输入一个新程序。
Save(【F2】) :把编辑窗口中的程序保存到文件中。如果该程序已经保存过,该操作将更
– 289 –
C 语言程序设计

新文件内容;如果该程序是新输入的,需要进一步输入文件名称或路径。
Write to:把当前程序写到另外命名的文件上,相当于 Windows 系统中“文件”菜单中
的“另存为”命令。
Quit(【Alt】+【x】):退出 TC 系统。

2.Edit(编辑操作)

它没有下拉菜单,按回车键( 【Enter】)直接进入编辑窗口。在编辑过程中,一些常用的
编辑功能依靠快捷键实现。
【Ctrl】+【y】:删除光标所在行的整行信息。
【Ctrl】+【k】+【b】:把光标所在位置定义为块信息的头部。
【Ctrl】+【k】+【k】:把光标所在位置定义为块信息的尾部。块头部定义与块尾部定义
要按顺序配合起来使用,所定义的块变成白底蓝字。
【Ctrl】+【k】+【h】:取消所定义的块信息。若重新定义新块也会取消原定义块。
【Ctrl】+【k】+【c】:把定义的块信息复制到光标所在位置。
【Ctrl】+【k】+【v】:把定义的块信息移动到光标所在位置。
【Ctrl】+【q】+【f】:查找特定字符,它会在屏幕上部提示输入查找字符,如图 A-3 所
示。当输入了“main”后,屏幕上出现 Option 信息,要求输入查找方式。
(1)G:对整个文件进行查找。
(2)回车:从光标当前位置向后查找。

图 A-3 字符查找

【Ctrl】+【q】+【a】 :替换字符串。它在上面查找的过程中,再输入字符替换原字符。
【Ctrl】+【L】:重复上一次查找或替换。
【Ctrl】+【q】+【[】:对光标所处位置的“(”、
“[”或“{”定位相应的“}”
、“]”或“)”。
这在程序中检查 3 种括号是否匹配十分有用。

3.Compile 菜单(编译连接操作)

Compile(【Alt】+【F9】):把编辑窗口中的程序编译成目标文件。
Link:把编辑窗口中的程序连接成可执行文件。
Make(【F9】 ):把编辑窗口中的程序经编译、连接,生成可执行文件。

4.Run 菜单(运行操作)

【Ctrl】+【F9】
Run( ):执行编辑窗口中的程序。如果该程序最近未编译连接过,将先自
动编译连接,然后再执行。
– 290 –
附录 A C 语言上机操作指导

5.Option 菜单(建立工作环境)

(1)工作环境目录的设置
如果使用安装盘安装,该工作环境目录会自动设置好。如果是通过系统复制的,则需要
对 Option 菜单中的 Directories 菜单项进行设置。假设 TC 所在目录为:
“D:\TC”,在打开的
目录窗口中应填入:
Include directories: D:\TC\INCLUDE
Library directories: D:\TC\LIB
以确保程序连接时能从这两个位置找到系统包含文件和系统库文件,如图 A-4 所示。

图 A-4 TC 工作目录设置

(2)命令行参数输入
在学习了第 8 章“指针”后,会用到命令行参数。可以执行 Option 菜单的 Arguments 菜
单项,输入命令行参数(不包括可执行文件名,各参数用空格分隔),回车结束输入。按【Esc】
键隐去菜单,然后按【Ctrl】+【F9】键运行程序,参数便能被主函数接受。

A.2.4 窗口操作
在图 A-2 所示的 TC 界面下,屏幕上半部分是编辑窗口,下半部分是信息窗口。如果想
把编辑窗口扩大到整屏,可按【F5】键,这时信息窗口将被遮住,再按【F5】键又可以恢复
成上下两个窗口。如果编辑窗口被扩大到整屏,而又想看一下信息窗口,可使用【F6】键进
行窗口切换。如果在程序执行时又打开了观察窗口, 【F6】键可以对 3 个窗口进行切换,切
换过程是按一个方向循环。
在 Windows 中运行 TC,所打开的窗口往往较小,边框线也不对,按【Alt】+【Enter】
键可以使窗口最大化,成为仿真 DOS 界面,再按【Alt】+【Enter】又会恢复为窗口。

A.2.5 程序调试
TC 提供了必要的调试手段和工具,下面按照使用过程予以介绍。

1.让程序执行到中途暂停以便观察阶段性结果

方法一:使程序执行到光标所在的那一行暂停。
① 把光标移动到需暂停的行上;
– 291 –
C 语言程序设计

② 按【F4】键或执行菜单 Run 中的 Go to Cursor 命令。当程序执行到该行将会暂停。如


果把光标移动到后面的某个位置,再按【F4】键,程序将从当前的暂停点继续执行到新的光
标位置,第二次暂停。
方法二:把光标所在的那一行设置成断点,然后按【Ctrl】+【F9】键执行,当程序执行
到该行将会暂停。设置断点的步骤如下。
① 把光标移动到需暂停的行上;
② 按【Ctrl】+【F8】键或执行菜单 Break/watch 中的 Toggle breakpoint 命令。
不管是通过光标位置还是断点设置,其所在的程序行必须是程序执行的必经之路,因此
不能设置在分支结构中的语句上,因为该语句在程序执行中受到条件判断的限制,有可能因
条件的不满足而不被执行。这时程序将一直执行到结束位置或下一个断点位置。

2.设置需观察的结果变量

按照上面的操作,可使程序执行到指定位置时暂停,其目的是为了查看有关的中间结果。
按【Ctrl】+【F7】键或菜单 Break/watch 中的 Add watch 命令,屏幕上将会弹出一个窗口供
输入查看变量,如图 A-5 所示,我们输入了变量 i 进行查看。
对于图 A-5 中的例子,我们先把光标移动到第 5 行,然后按【F4】键执行,程序到第 5
行暂停,如图 A-6 所示,查看(Watch)窗口中就会显示变量 i 的当前值。绿色光条表示当前
将被执行的程序位置(或暂停位置) 。

图 A-5 输入查看变量

图 A-6 查看中间结果

多次使用【Ctrl】+【F8】键可增加多个新的查看变量。如果想改变查看变量的名字或删
除查看变量,可以按【F6】键,使查看窗口成为操作窗口,然后按【Enter】键,可以改变查

– 292 –
附录 A C 语言上机操作指导

看变量,按删除键【Delete】键可以删除查看变量。这些功能也可通过 Break/watch 菜单中的


相应命令实现。

3.单步执行

当程序执行到某个位置时发现结果已经不正确了,说明在此之前肯定有错误存在。如果
能确定一小段程序可能有错,先按上面步骤暂停在该小段程序的头一行,再输入若干个查看
变量,然后单步执行,即一次执行一行语句,逐行检查下来,看看到底是哪一行造成结果出
现错误,从而确定错误的语句并予以纠正。
单步执行按【F8】键或执行菜单 Run 中的 Step over 命令。如果遇到自定义函数调用,想
进入函数进行单步执行,可按【F7】键或执行菜单 Run 中的 Trace into 命令。对不是函数调
用的语句来说,【F7】键与【F8】键作用相同,但一般对系统函数不要使用【F7】键。

4.断点的使用

使用断点也可以使程序暂停,但一旦设置了断点,不管你是否还需要调试程序,每次执
行程序都会在断点上暂停,因此调试结束后应取消所定义的断点。方法是先把光标定位在断
点所在行,再按【Ctrl】+【F8】键或执行菜单 Break/watch 中的 Toggle breakpoint 命令,该操
作是一个开关,第一次按是设置,第二次按是取消设置。此时,被设置成断点的行将呈红色
背景。如果有多个断点想一下全部取消,可执行菜单 Break/watch 中的 Clear all breakpoints
命令。
断点通常用于调试较长的程序,可以避免使用【F4】键(运行程序到光标处暂停)功能
时,需要经常把光标定位到不同的地方。而对于长度为上百行的程序,要寻找某位置并不太
方便。
如果一个程序设置了多个断点,按一次【Ctrl】+【F9】键会暂停在第一个断点,再按一
次【Ctrl】+【F9】键会继续执行到第二个断点暂停,依次执行下去。

5.结束调试

TC 中通过“结束程序运行” (Program reset)来结束程序调试,可通过按【Ctrl】+【F2】


键或执行菜单 Run 中的 Program reset 命令实现。

6.循环的调试实例

下面用单步执行功能来看一下 for 语句的执行流程,如图 A-7 所示。程序中把 for 语句分


成 3 行,以便观察执行流程。先把光标移动到第 4 行,然后按【F4】键或执行菜单 Run 中的
Go to Cursor 命令,再按【Ctrl】+【F7】键输入查看变量 i,由于此时 i 未赋过值,所以显示
的是一个随机数。再连续按【F8】键单步执行,可以观察绿色光条的位置变动和查看变量 i
的变化。绿色光条的位置变动就是程序执行的过程,从而可以充分体会到 for 语句的执行流
程。
通过这个例子,读者可以举一反三调试自己的程序了。
上面我们只对 TC 中主要的功能作了介绍,对于其他的操作读者可以自己试验,或参考
有关 TC 手册。
– 293 –
C 语言程序设计

图 A-7 循环调试实例

A.2.6 常用快捷键
TC 中的常用快捷键如下。
【F2】:保存程序; 【Ctrl】+【F2】:结束程序调试运行;
【F3】:调入程序文件; 【Alt】+【F3】:调入最近曾经用过的文件;
【F4】:程序运行到光标处暂停; 【F5】:放大/缩小窗口;
【Alt】+【F5】:查看运行结果; 【F6】:窗口切换;
【F7】:单步执行(可进入函数) ; 【Ctrl】+【F7】:增加查看变量;
【F8】:单步执行(不能进入函数) ; 【Ctrl】+【F8】:把光标所在行设为断点或取
消断点;
【F9】 :编译、连接程序; 【Alt】+【F9】:编译程序;
【Ctrl】+【F9】:编译、连接、执行程序;【F10】:菜单选择;
【F1】 :显示帮助信息; 【Alt】+【x】:退出系统。

A.3 Visual C 语言集成环境

C++语言是在 C 语言的基础上发展而来的,它增加了面向对象的编程,成为当今最流行
的一种程序设计语言。Visual C++(以下简称 VC++)是微软公司开发的,面向 Windows 编
程的 C++语言工具。它不仅支持 C++语言的编程,也兼容 C 语言的编程。目前 VC++被广泛
地用于各种编程,使用面很广,这里只简要地介绍如何在 VC++下运行 C 语言程序。

A.3.1 启动 Visual C++


VC++是一个庞大的语言集成工具,安装后将占用几百兆磁盘空间。在 Windows 环境下,
依次选择“开始”→“程序”→Microsoft Visual Studio 6.0→Microsoft Visual C++ 6.0,即可启
动 VC++,屏幕上将显示图 A-8 所示的窗口。

A.3.2 新建/打开 C 语言程序文件


执行“文件”菜单中的“新建”命令,在弹出的对话框中单击图 A-9 所示的“文件”标
签,选中 C++ Source File 项,再单击“确定”按钮,即可新建一个文件。此时,在编辑窗口

– 294 –
附录 A C 语言上机操作指导

中可以输入程序。

菜单栏
工具栏

编辑窗口

信息窗口

图 A-8 VC++窗口

图 A-9 新建文件

如果程序已经存在,可执行“文件”菜单中的“打开”命令,在弹出的“打开”对话框
中找到文件,调入指定的程序文件。

A.3.3 保存程序
在 VC++界面中,可直接在编辑窗口输入程序,由于完全是 Windows 界面,编译及修改
程序可借助鼠标和菜单进行,十分方便。当输入结束后,执行“文件”菜单中的“保存”命
令可将文件存盘。如果该文件以前未保存过,还需要指定路径及文件名。保存文件时,应指
定扩展名为“.C” ,否则系统将按 C++扩展名“.CPP”保存,如图 A-10 所示。

– 295 –
C 语言程序设计

图 A-10 指定保存文件名

A.3.4 执行程序
执行程序前要先生成可执行文件,执行“编译”菜单中的“构件”命令(如图 A-11 所示)
或按快捷键【F7】即可。在编译连接过程中 VC++将保存该新输入的程序,并生成一个同名
的工作区。保存文件时需填入文件名,如“4-1.C”。如果不指定扩展名.C,VC++会把扩展名
定义为.CPP,即 C++程序。如果程序没有错误,将在图 A-12 所示的信息窗口中显示内容:
0 error(s) 0 warning(s)
表示没有任何错误。有时出现几个警告性信息(warning),不会影响程序执行。如果有致命
性错误(error),如图 A-13 所示,双击某行出错信息,程序窗口中会指示对应出错位置,再
根据信息窗口的提示分别予以纠正,然后执行“编译”菜单中的“执行”命令(或按快捷键
【Ctrl】+【F5】)执行程序。

编译连接生成可执行程序

图 A-11 “编译”菜单

图 A-12 编译连接正确

– 296 –
附录 A C 语言上机操作指导

图 A-13 编译连接出错

当运行 C 语言程序后,VC++将自动弹出数据输入输出窗口,如图 A-14 所示。程序运行


完毕,按任意键将关闭该窗口。

图 A-14 数据输入输出窗口

对于编译连接执行操作,VC++还提供了一组工具按钮,如图 A-15 所示。

编译 构造 执行
图 A-15 编译连接执行工具按钮

A.3.5 关闭程序工作区
当一个程序编译连接后,VC++系统自动产生相应的工作区,以完成程序的运行和调试。
若想执行第二个程序时,必须关闭前一个程序的工作区,然后通过新的编译连接,产生第二
个程序的工作区,否则的话运行的将一直是前一个程序。
“文件”菜单中提供了关闭程序工作区的功能,如图 A-16(a)所示,执行“关闭工作区”
– 297 –
C 语言程序设计

命令,然后在图 A-16(b)所示的对话框中单击“否”按钮即可。如果单击“是”按钮,将
同时关闭源程序窗口。

(a) (b)

图 A-16 关闭程序工作区

A.3.6 命令行参数处理
VC++是一个基于窗口操作的 C++系统,没有提供命令行参数功能。我们需要在 Windows
的“MS-DOS 方式”窗口里以命令方式实现。具体步骤参考如下。
① 正确编译连接,生成可执行程序。
② 通过“我的电脑”或“资源管理器”找到所运行的 C 源程序(设程序为 a.C)。
③ 进入 debug 文件夹(它包含 a.C 语言程序的可执行文件 a.EXE);
④ 执行“开始”菜单中的“运行”命令,输入 command,然后单击“确定”按钮。
⑤ 在打开的“MS-DOS 方式”窗口中输入:a 参数 1 参数 2 …… ,带参数运行程
序。

A.3.7 程序调试
VC++是一个完全基于 Windows 的系统,它的调试过程使用鼠标比较容易进行。

1.程序执行到中途暂停以便观察阶段性结果

方法一:使程序执行到光标所在的那一行暂停。
① 在需暂停的行上单击鼠标,定位光标。
② 执行“编译”菜单中“开始调试”子菜单内的 Run to Cursor 命令,或按【Ctrl】+
【F10】键,程序会在执行到光标所在行时暂停,如图 A-17 所示。如果把光标移动到后面的
某个位置,再按【Ctrl】+【F10】键,程序将从当前的暂停点继续执行到新的光标位置,第
二次暂停。
方法二:在需暂停的行上设置断点。
① 在需设置断点的行上单击鼠标,定位光标。
② 单击“编译微型条”工具栏中最右边的按钮(如图 A-18 所示),或按【F9】键。

– 298 –
附录 A C 语言上机操作指导

图 A-17 执行到光标所在行暂停

图 A-18 设置断点

此时,被设置了断点的行前面会有一个红色圆点标志。
与 TC 一样,不管是通过光标位置还是断点设置,其所在的程序行必须是程序执行的必
经之路。

2.设置需观察的结果变量

按照上面的操作,使程序执行到指定位置时暂停,目的是为了查看有关的中间结果。在图
A-19 所示的界面中,左下角窗口中系统自动显示了有关变量的值,其中 value1 和 value2 的值
分别是 3 和 4,而变量 i 和 sum 的值是不正确的,因为它们还未被赋值。图中左侧的箭头表示
当前程序暂停的位置。如果还想增加观察变量,可在图中右下角的 Name 框中输入相应变量名。

图 A-19 观察结果变量

– 299 –
C 语言程序设计

3.单步执行

当程序执行到某个位置时发现结果已经不正确了,说明在此之前肯定有错误存在。如果
能确定一小段程序可能有错,先按上面步骤暂停在该小段程序的头一行,再输入若干个查看
变量,然后单步执行,即一次执行一行语句,逐行检查下来,看看到底是哪一行造成结果出
现错误,从而确定错误的语句并予以纠正。
单步执行可单击“调试”工具栏中的 Step Over 按钮 或按【F8】键,如图 A-20 所
示。如果遇到自定义函数调用,想进入函数进行单步执行,可单击 Step Into 按钮 或按
【F11】键。当想结束函数的单步执行,可单击 Step Out 按钮 或【Shift】+【F11】键。
对不是函数调用的语句来说,【F11】键与【F8】键作用相同,但一般对系统函数不要使
用【F11】键。

图 A-20 单步调试

4.断点的使用

使用断点也可以使程序暂停,但一旦设置了断点,不管你是否还需要调试程序,每次执
行程序都会在断点上暂停,因此调试结束后应取消所定义的断点。方法是先把光标定位在断
点所在行,再单击“编译微型条”工具栏中最右边的按钮 或【F9】键,该操作是一个开关,
按一次是设置,按二次是取消设置。如果有多个断点想全部取消,可执行“编辑”菜单中的
“断点”命令,屏幕上会显示 Breakpoints 窗口,如图 A-21 所示。窗口下方列出了所有断点,
单击 Remove All 按钮,将取消所有断点。
断点通常用于调试较长的程序,可以避免使用 Run to Cursor(运行程序到光标处暂停)
或【Ctrl】+【F10】功能时,经常要把光标定位到不同的地方。而对于长度为上百行的程序,
要寻找某位置并不太方便。
如果一个程序设置了多个断点,按一次【Ctrl】+【F5】键会暂停在第一个断点,再按一
次【Ctrl】+【F5】键会继续执行到第二个断点暂停,依次执行下去。

图 A-21 取消所有断点

– 300 –
附录 A C 语言上机操作指导

5.停止调试

执行 Debug 菜单中的 Stop Debugging 命令,或按【Shift】+【F5】键,可以结束调试,


返回到正常的运行状态。
上面我们只对 VC++中主要的功能做了介绍,对于其他的操作读者可以自己试验,或参
考有关 VC++手册。

– 301 –
附录 B A SC II 码集
符 号 十进制 八进制 十六进制 符 号 十进制 八进制 十六进制

(null) 0 0 0 空格 32 40 20

 1 1 1 ! 33 41 21

? 2 2 2 " 34 42 22

♥ 3 3 3 # 35 43 23

♦ 4 4 4 $ 36 44 24

♣ 5 5 5 % 37 45 25

♠ 6 6 6 & 38 46 26

• 7 7 7 ’ 39 47 27

? 8 10 8 ( 40 50 28

? 9 11 9 ) 41 51 29

? 10 12 a * 42 52 2a

? 11 13 b + 43 53 2b

♀ 12 14 c , 44 54 2c

? 13 15 d − 45 55 2d

? 14 16 e . 46 56 2e

¤ 15 17 f / 47 57 2f

? 16 20 10 0 48 60 30

? 17 21 11 1 49 61 31

? 18 22 12 2 50 62 32

!! 19 23 13 3 51 63 33

¶ 20 24 14 4 52 64 34

§ 21 25 15 5 53 65 35

? 22 26 16 6 54 66 36

23 27 17 7 55 67 37

 24 30 18 8 56 70 38

– 302 –
附录 B ASCII 码集

续表
符 号 十进制 八进制 十六进制 符 号 十进制 八进制 十六进制

 25 31 19 9 57 71 39

 26 32 1A : 58 72 3a

 27 33 1b ; 59 73 3b

└ 28 34 1c < 60 74 3c

? 29 35 1d = 61 75 3d

? 30 36 1e > 62 76 3e

? 31 37 1f ? 63 77 3f

@ 64 100 40 、 96 140 60

A 65 101 41 a 97 141 61

B 66 102 42 b 98 142 62

C 67 103 43 c 99 143 63

D 68 104 44 d 100 144 64

E 69 105 45 e 101 145 65

F 70 106 46 f 102 146 66

G 71 107 47 g 103 147 67

H 72 110 48 h 104 150 68

I 73 111 49 i 105 151 69

J 74 112 4a j 106 152 6a

K 75 113 4b k 107 153 6b

L 76 114 4c l 108 154 6c

M 77 115 4d m 109 155 6d

N 78 116 4e n 110 156 6e

O 79 117 4f o 111 157 6f

P 80 120 50 p 112 160 70

Q 81 121 51 q 113 161 71

R 82 122 52 r 114 162 72

S 83 123 53 s 115 163 73

T 84 124 54 t 116 164 74

U 85 125 55 u 117 165 75

V 86 126 56 v 118 166 76

– 303 –
C 语言程序设计

续表
符 号 十进制 八进制 十六进制 符 号 十进制 八进制 十六进制

W 87 127 57 w 119 167 77

X 88 130 58 x 120 170 78

Y 89 131 59 y 121 171 79

Z 90 132 5a z 122 172 7a

[ 91 133 5b { 123 173 7b

\ 92 134 5c | 124 174 7c

] 93 135 5d } 125 175 7d

^ 94 136 5e ~ 126 176 7e

_ 95 137 5f 127 177 7f

– 304 –
附录 C C语言中的关键字
auto break case char const
continue default do double else
enum extern float for goto
if int long register return
short signed sizeof static struct
switch typedef union unsigned void
volatile while

– 305 –
附录 D 运算符优先级

优先级 运算符 名 称 特 征 结合方向

() 圆括号
[] 下标
1 初等运算符 从左到右
-> 指针引用结构体成员
. 取结构体变量成员

! 逻辑非
~ 按位取反
+ 正号
− 负号
单目运算
2 (类型名) 类型强制转换 从右到左
(只有一个操作数)
* 取指针内容
& 取地址
++ 自增
-- 自减

* 相乘
3 / 相除
% 取两整数相除的余数 算术运算

+ 相加
4
– 相减

<< 左移
5 移位运算
>> 右移 从左到右

> 大于
< 小于
6
>= 大于或等于
关系运算
<= 小于或等于

== 等于
7
!= 不等于

– 306 –
附录 D 运算符优先级

续表

优先级 运算符 名 称 特 征 结合方向

8 & 按位与

9 ^ 按位异或 位逻辑运算

10 | 按位或
从左到右
11 && 逻辑与
逻辑运算
12 || 逻辑或

13 ?: 条件 三目运算 从右到左

= += -= *= /=
14 %= &= ^= |= 赋值 赋值运算 从右到左
>>= <<=

15 , 逗号 逗号运算 从左到右

说明:
(1)优先级 1 最高,优先级 15 最低。运算时,优先级高的运算符先执行。
(2)运算符的优先级,可以根据表中的“特征”栏,按大类记忆。

– 307 –
附录 E C 语言常用库函数
C 语言程序设计中,大量的功能实现需要库函数的支持,包括最基本的 scanf()和 printf()
函数。虽然库函数不是 C 语言的一部分,但每一个实用的 C 语言系统都会根据 ANSI C 提出
的标准库函数,提供这些标准库函数的实现。因此对编程者来说,标准库函数已成为 C 语言
中不可缺少的组成部分。 实用的 C 语言系统一般还会根据其自身特点提供大量相关的库函数,
包括图形图像处理函数、输入输出通信函数和计算机系统功能调用函数等,不同的 C 语言系
统会有不同的库函数。限于篇幅,本附录提供了 ANSI C 的一些常用的标准库函数,读者若
对其他语言系统库函数感兴趣,可查阅相关 C 语言系统的使用手册。
下面各表中所列函数名前的类型说明是函数返回结果的类型,程序中使用这些库函数时
不必书写类型。

1.数学函数

下列数学函数中,除第一个求整型数绝对值函数 abs()外,均在头文件 math.h 中说明。对


应的编译预处理命令为:
#include<math.h>
函数名 函数定义格式 函数功能 返回值 说 明

函数说明在 stdlib.h
abs int abs(int x) 求整型数 x 的绝对值 计算结果

fabs double fabs(double x) 求 x 的绝对值 计算结果

sqrt double sqrt(double x) 计算 x 的平方根 计算结果 要求 x>=0

exp double exp(double x) 计算 ex 计算结果 e 为 2.718…

pow double pow(double x, double y) 计算 xy 计算结果

log double log(double x) 求 lnx 计算结果 自然对数

log10 double log10(double x) 求 log10x 计算结果

求不大于 x 的最小整
ceil double ceil(double x)

double 类型
floor double floor(double x) 求小于 x 的最大整数

fmod double fmod(double x, double y) 求 x/y 的余数

double modf(double x, double 把 x 分解,整数部分


modf x 的小数部分
*ptr) 存入*ptr

– 308 –

附录 E C 语言常用库函数

续表

函数名 函数定义格式 函数功能 返回值 说 明

sin double sin(double x) 计算 sin(x)


[-1 , 1 ]
cos double cos(double x) 计算 cos(x) x 为弧度值

tan double tan(double x) 计算 tan(x) 计算结果

asin double asin(double x) 计算 sin 1(x)


[0 ,π] x∈[-1 , 1]
acos double acos(double x) 计算 cos 1(x)

atan double atan(double x) 计算 tan 1(x)


[-π/2 ,π/2]
atan2 double atan(double x, double y) 计算 tan 1(x/y)

sinh double sinh(double x) 计算 sinh(x)

cosh double cosh(double x) 计算 cosh(x) 计算结果 x 为弧度值

tanh double tanh(double x) 计算 tanh(x)

2.输入输出函数

下列输入输出函数在头文件 stdio.h 中说明。对应的编译预处理命令为:


#include<stdio.h>
(1)格式化输入输出函数
函数名 函数定义格式 函 数 功 能 返 回 值

int printf (char *format, 输出表) 按字符串 format 给定输出格式,把输出 成功:输出字符数


printf
表中各表达式的值,
输出到标准输出文件 失败:EOF

int scanf(char *format, 输入项 按字符串 format 给定输入格式,从标准 成功:输入数据的个


scanf 地址列表) 输入文件读入数据,
存入各输入项地址列 数
表指定的存储单元中 失败:EOF

int sprintf(char *s, char 成功:输出字符数


sprintf 功能类似 printf(),但输出目标为字符串 s
*format, 输出表) 失败:EOF

int sscanf (char *s, char *format, 成功:输入数据的个


sscanf 输入项地址表) 功能类似 scanf(),但输入源为字符串 s 数
失败:EOF

(2)字符(串)输入输出函数
函数名 函数定义格式 函 数 功 能 返 回 值

getchar int getchar () 从标准输入文件读入 1 个字符 字符 ASCII 值或 EOF

– 309 –
C 语言程序设计

续表

函数名 函数定义格式 函 数 功 能 返 回 值

成功:ch
putchar int putchar(char ch) 向标准输出文件输出字符 ch
失败:EOF

从标准输入文件读入一个字符串到字 成功:s
gets char *gets(char *s)
符数组 s,输入字符串以回车结束 失败:NULL

把字符串 s 输出到标准输出文件,’ \0’ 成功:换行符


puts int puts(char *s)
转换为’\n’输出 失败:EOF

成功:所取字符
fgetc int fgetc(FILE *fp) 从 fp 所指文件中读取一个字符
失败:EOF

int fputc(char ch, FILE 成功:ch


fputc 将字符 ch 输出到 fp 所指向的文件
*fp) 失败:EOF

char *fgets(char *s, int 从 fp 所指文件最多读 n – 1 个字符(遇 成功:s


fgets
n,FILE *fp) ’\n’、^z 终止)到字符串 s 中 失败:NULL

int *fputs(char *s, FILE 成功:s 的末字符


fputs 将字符串 s 输出到 fp 所指向文件
*fp) 失败:0

(3)文件操作函数
函数名 函数定义格式 函 数 功 能 返 回 值

FILE *fopen(char *fname, char 成功:文件指针


fopen 以 mode 方式打开文件 fname
*mode) 失败:NULL

fclose int fclose (FILE *fp) 关闭 fp 所指文件 成功:0;失败:非 0

feof int feof(FILE *fp) 检查 fp 所指文件是否结束 是:非 0;失败:0

从 fp 所指文件复制 n×sizeof (T)


int fread(T *a, long sizeof(T), 成功:n
fread 个字节,到 T 类型指针变量 a 所
unsigned int n, FILE *fp) 失败:0
指内存区域

从T 类型指针变量a 所指处起复
int fwrite (T *a, long sizeof(T), 成功:n
fwrite 制 n×sizeof(T)个字节的数据,
unsigned int n, FILE *fp) 失败:0
到 fp 所指向文件

移动 fp 所指文件读写位置到文
rewind void rewind(FILE *fp)
件头

int fseek(FILE *fp, long n, 移动 fp 所指文件读写位置,n 为 成功:0


fseek
unsigned int posi) 位移量,posi 决定起点位置 失败:非 0

– 310 –
附录 E C 语言常用库函数

续表

函数名 函数定义格式 函 数 功 能 返 回 值

求当前读写位置到文件头的字 成功:所求字节数
ftell long ftell(FILE *fp)
节数 失败:EOF

remove int remove (char *fname) 删除名为 fname 的文件 成功:0;失败:EOF

int rename(char *oldfname ,char


rename 改文件名 oldfname 为 newfname 成功:0;失败:EOF
*newfname)
注:fread()和 fwrite()中的类型 T,可以是任一合法定义的类型。

3.字符判别函数

下列字符判别函数在头文件 ctype.h 中说明。对应的编译预处理命令为:


#include<ctype.h>
函数名 函数定义格式 函 数 功 能 返 回 值

isalpha int isalpha(char c) 判别 c 是否字母字符

islower int islower(char c) 判别 c 是否为小写字母

isupper int isupper(char c) 判别 c 是否为大写字母

isdigit int isdigit(char c) 判别 c 是否为数字字符


是:返回非 0
isalnum int isalnum(char c) 判别 c 是否为字母或数字字符
否:返回 0
isspace int isspace(char c) 判别 c 是否为空格字符

iscntrl int iscntrl(char c) 判别 c 是否为控制字符

isprint int isprint(char c) 判别 c 是否为可打印字符

ispunct int ispunct(char c) 判别 c 是否为标点符号

判别 c 是否是除字母、数字及空格外的可 是:返回非 0
isgraph int isgraph(char c)
打印字符 否:返回 0

tolower char tolower(char c) 将大写字母 c 转换为小写字母 c 对应的小写字母

toupper char toupper(char c) 将小写字母 c 转换为大写字母 c 对应的大写字母

4.字符串操作函数

下列字符串操作函数在头文件 string.h 中说明。对应的编译预处理命令为:


#include<string.h>

– 311 –
C 语言程序设计

函数名 函数定义格式 函 数 功 能 返 回 值

把字符串 t 连接到 s,使 s 成为包含 s


strcat char *strcat(char *s, char *t) s
和 t 的结果字符串

逐个比较两字符串中对应字符,直到 相等:0
strcmp int strcmp(char *s, char *t)
对应字符不等或比较到字符串尾 不等:不相等字符的差值

strcpy char *strcpy(char *s, char *t) 把字符串 t 复制到 s 中 s

strlen unsigned int strlen(char *s) 计算字符串 s 的长度(不包括’\0’) 字符串长度

strchr char *strchr(char *s, char c) 在字符串s 查找字符c 首次出现的地址


找到:相应地址
在字符串 s 中找字符串 t 首次出现的地 找不到:NULL
strstr char *strstr(char *s,char *t)

5.数值转换函数

这里提供了把内容为数值的字符串,转换成相应数值的函数。它们在头文件 stdlib.h 中说
明。对应的编译预处理命令为:
#include<stdlib.h>
函数名 函数定义格式 函 数 功 能 返 回 值

abs int abs(int x) 求整型数 x 的绝对值

atof double atof(char *s) 把字符串 s 转换成双精度数


运算结果
atoi int atoi(char *s) 把字符串 s 转换成整型数

atol long atol(char *s) 把字符串 s 转换成长整型数

rand int rand ( ) 产生一个伪随机的无符号整数 伪随机数

以 seed 为种子(初始值)计算产生一个
srand srand(unsigned int seed) 随机数
无符号的随机整数

6.动态内存分配函数

ANSI C 的动态内存分配函数共有 4 个,在头文件 stdlib.h 中说明。对应的编译预处理命令为:


#include<stdlib.h>
函数名 函数定义格式 函 数 功 能 返 回 值

void *colloc(unsigned int n, unsigned 分配 n 个连续存储单元(每 成功:分配单元首地址


colloc
int size) 个单元包含 size 字节) 失败:NULL

分配 size 个字节的存储单元 成功:分配单元首地址


malloc void *malloc(unsigned int size)
块 失败:NULL

– 312 –
附录 E C 语言常用库函数

续表

函数名 函数定义格式 函 数 功 能 返 回 值

释放 p 所指存储单元块(必
free 须是由动态内存分配函数, 无
void free(void *p)
一次性分配的全部单元)

void *realloc(void *p , unsigned int 将p 所指的已分配存储单元 成功:单元块首地址


realloc
size) 块的大小改为 size 失败:NULL
注:colloc()和 malloc()函数需强制类型转换成所需单元类型的指针。

– 313 –

You might also like