You are on page 1of 262

21 世纪高等学校规划教材

C 语言程序设计
(第 2 版)
郑山红 李万龙 于秀霞 主编
岳 莉 边 晶 张春飞 主编

人 民 邮 电 出 版 社
北 京
第2版
前 言

C 语言具有概念简洁、数据类型丰富、表达能力强、使用灵活、运行效率高和
可移植性强等特点,因此“C 语言程序设计”成为大多数高校计算机专业一门重要
的基础课程。该课程的学习,可以为学生今后进一步学习专业课程奠定良好的基础。
本书是作者在多年的 C 语言教学改革与实践的基础上编写的,体现了以下 4
方面的特点。
1.保证知识的先进性
C 语言自 1972 年诞生以来,得到了广泛应用,并且有了较大发展,1983 年美
国 ANSI 制定了 C 标准 ANSI C,1989 年国际标准化组织(ISO)制定了 C89 标准,
1999 年 ISO 又在 C89 的基础上进行了完善和补充,制定了 C99 标准。本书的编写
在 C89 标准的基础上增加了 C99 的部分内容,保证了知识的先进性。
2.注重软件工程素质和能力的培养
本书采用简化的匈牙利编码规范,注重学生编程习惯的培养,强调“自顶向
下、逐步细化”、
“先分析后设计再编码”和“以需求为驱动”的软件工程思想与方
法的渗透,加强学生软件工程能力的训练。
3.注重教材的完整性
本书为授课教师提供电子教案、可执行的源程序文件和习题答案等参考资
料。需要者可在人民邮电出版社教学服务与资源网(http://www.ptpedu.com.cn)免
费下载。
4.体现先进的教学理念
本书在内容、结构和风格方面融合了“知识驱动”、
“问题驱动”、
“案例驱动”
和“项目驱动”的教学理念,使全书在内容上按“点—线—面—体”的方式组织,
在应用上按“由小到大、由浅入深、由单一到综合”逐步扩展。
本书由郑山红、李万龙、于秀霞任主编,岳莉、边晶、张春飞任副主编,在
本书的编写过程中,候秀萍、彭馨仪、王国春、赵辉、董亚则等同志做了很多工作,
在此表示衷心的感谢!
由于编者水平有限,加之编写时间仓促,书中难免有不当之处,敬请读者批
评指正。

编 者
2011 年 6 月
目 录

2.4.5 条件运算符及条件表达式 ............... 32


第1章 C 语言概述 ................................... 1
2.4.6 逗号运算符及逗号表达式 ............... 33
1.1 计算机语言的发展 .................................... 1 2.4.7 求字节数运算符及求字节数表
1.1.1 机器语言 ............................................ 1 达式................................................... 34
1.1.2 汇编语言 ............................................ 1 2.5 数据类型转换 .......................................... 34
1.1.3 高级语言 ............................................ 2 2.6 深入研究:整型数值的溢出问题 .......... 36
1.2 C 语言的发展及特点 ................................ 2 本章小结 ........................................................... 37
1.2.1 C 语言的发展..................................... 2 习题 ................................................................... 37
1.2.2 C 语言的特点..................................... 3
第 3 章 控制结构 ............................... 39
1.3 认识第一个 C 程序 ................................... 3
1.4 开发第一个 C 程序 ................................... 5 3.1 结构程序设计 .......................................... 39
1.4.1 C 程序的开发过程 ............................. 5 3.2 顺序结构程序设计 .................................. 39
1.4.2 Visual C++环境下运行 C 程序 ......... 7 3.2.1 表达式语句....................................... 39
1.5 深入研究:调试手段与错误定位 ............ 9 3.2.2 函数调用语句................................... 40
本章小结 .......................................................... 11 3.2.3 空语句............................................... 45
习题 .................................................................. 11 3.2.4 复合语句........................................... 46
3.3 选择结构程序设计 .................................. 46
第 2 章 数据类型、运算符和表
3.3.1 if 语句 ............................................... 46
达式 ................................................. 13
3.3.2 switch 语句 ....................................... 49
2.1 标识符和关键字 ...................................... 13 3.4 循环结构程序设计 .................................. 52
2.1.1 字符集 .............................................. 13 3.4.1 while 语句 ......................................... 52
2.1.2 标识符 .............................................. 14 3.4.2 do-while 语句 ................................... 54
2.1.3 关键字 .............................................. 14 3.4.3 for 语句 ............................................. 55
2.2 数据类型.................................................. 15 3.4.4 循环的嵌套....................................... 57
2.3 常量与变量.............................................. 17 3.5 转移控制语句 .......................................... 59
2.3.1 常量 .................................................. 17 3.5.1 break 语句 ......................................... 59
2.3.2 变量 .................................................. 21 3.5.2 continue 语句 .................................... 61
2.4 运算符与表达式 ...................................... 23 3.5.3 goto 语句 .......................................... 62
2.4.1 算术运算符及算术表达式 .............. 23 3.6 综合实例 .................................................. 63
2.4.2 关系运算符及关系表达式 .............. 27 3.7 深入研究:程序优化问题 ...................... 65
2.4.3 逻辑运算符及逻辑表达式 .............. 28 本章小结 ........................................................... 67
2.4.4 赋值运算符及赋值表达式 .............. 30 习题 ................................................................... 67
C 语言程序设计(第 2 版)

5.4 多维数组 ................................................ 112


第4章 函数 ...................................... 70
5.5 字符数组 ................................................ 114
4.1 函数的定义 ............................................. 70 5.5.1 字符数组的定义和引用 ................. 114
4.2 函数的调用 ............................................. 72 5.5.2 字符数组的初始化......................... 114
4.2.1 函数调用的一般形式 ...................... 72 5.5.3 字符数组的输入输出..................... 115
4.2.2 函数参数的传递 .............................. 76 5.5.4 字符串处理函数............................. 116
4.2.3 函数的嵌套调用 .............................. 77 5.6 数组与函数 ............................................ 118
4.3 函数的声明 ............................................. 78 5.7 综合实例 ................................................ 120
4.4 函数的返回与返回值.............................. 80 5.8 深入研究:数组的负数下标和动态数组
4.4.1 函数的返回 ...................................... 80 问题 ........................................................ 122
4.4.2 返回值 .............................................. 81 本章小结 ......................................................... 124
4.5 函数的递归调用 ..................................... 82 习题................................................................. 124
4.6 变量的作用域与生命期.......................... 85
第6章 指针 .................................... 127
4.6.1 局部变量 .......................................... 85
4.6.2 全局变量 .......................................... 86 6.1 指针与指针变量 .................................... 127
4.7 变量的存储类型 ..................................... 88 6.1.1 指针的概念 .................................... 127
4.7.1 自动变量 .......................................... 88 6.1.2 指针变量的提出............................. 128
4.7.2 静态变量 .......................................... 89 6.1.3 指针变量的定义............................. 129
4.7.3 寄存器变量 ...................................... 91 6.1.4 指针变量的初始化......................... 130
4.7.4 外部变量 .......................................... 92 6.1.5 指针变量的使用............................. 132
4.8 综合实例 ................................................. 93 6.2 指针与数组 ............................................ 135
4.9 深入研究:递归的设计与使用问题 ...... 95 6.2.1 指针与一维数组............................. 136
本章小结 .......................................................... 97 6.2.2 指针与二维数组............................. 140
习题 .................................................................. 98 6.2.3 指针与字符数组............................. 142
6.2.4 指针数组 ........................................ 144
第5章 数组 .................................... 101
6.3 指针与函数 ............................................ 145
5.1 为什么要使用数组 ............................... 101 6.3.1 指针作为函数参数......................... 145
5.2 一维数组 ............................................... 102 6.3.2 指针作为函数的返回值 ................. 149
5.2.1 一维数组的定义 ............................ 102 6.3.3 指向函数的指针............................. 150
5.2.2 一维数组的引用 ............................ 103 6.4 指向指针的指针 .................................... 152
5.2.3 一维数组的初始化 ........................ 107 6.5 动态内存分配 ........................................ 153
5.3 二维数组 ............................................... 108 6.6 带参数的 main( )函数 ........................... 155
5.3.1 二维数组的定义 ............................ 108 6.7 综合实例 ................................................ 156
5.3.2 二维数组的引用 ............................ 109 6.8 深入研究:多级指针问题 .................... 158
5.3.3 二维数组的初始化 ........................ 110 本章小结 ......................................................... 159

2
目 录

习题 ................................................................ 160 8.1.2 按位或运算符................................. 193


8.1.3 按位异或运算符 ............................. 194
第7章 结构体、共用体和枚举 .... 163
8.1.4 按位取反运算符 ............................. 195
7.1 结构体类型............................................ 163 8.1.5 按位左移运算符 ............................. 196
7.1.1 结构体类型的提出 ........................ 163 8.1.6 按位右移运算符 ............................. 196
7.1.2 结构体类型的定义 ........................ 163 8.2 位段 ........................................................ 198
7.2 结构体类型变量 .................................... 165 8.2.1 位段的定义..................................... 198
7.2.1 结构体类型变量的定义 ................ 165 8.2.2 位段的使用..................................... 199
7.2.2 结构体类型变量的引用 ................ 166 8.3 综合实例 ................................................ 201
7.2.3 结构体类型变量的初始化 ............ 167 8.4 深入研究:字段拼装问题 .................... 204
7.3 结构体类型数组 .................................... 168 本章小结 ......................................................... 205
7.3.1 结构体类型数组的定义 ................ 168 习题 ................................................................. 205
7.3.2 结构体类型数组的引用 ................ 169
第9章 文件 .....................................207
7.4 结构体类型指针 .................................... 170
7.4.1 指向结构体变量的指针 ................ 170 9.1 文件概述 ................................................ 207
7.4.2 指向结构体数组的指针 ................ 172 9.2 文件指针 ................................................ 208
7.5 结构体与函数........................................ 173 9.3 文件的打开和关闭 ................................ 208
7.6 共用体 ................................................... 175 9.3.1 文件打开函数 fopen( ) ................... 209
7.6.1 共用体类型的提出 ........................ 175 9.3.2 文件关闭函数 fclose( )................... 210
7.6.2 共用体类型的定义 ........................ 175 9.4 文件的读写 ............................................ 210
7.6.3 共用体变量的定义 ........................ 176 9.4.1 字符读写函数 fgetc( )和 fputc( ) ... 210
7.6.4 共用体变量的引用 ........................ 176 9.4.2 字符串读写函数 fgets( )和
7.7 枚举 ....................................................... 178 fputs( ) ............................................. 212

7.7.1 枚举类型的定义 ............................ 178 9.4.3 数据块读写函数 fread( )和

7.7.2 枚举变量的定义 ............................ 179 fwrite( ) ...................................................213

7.7.3 枚举变量的使用 ............................ 180 9.4.4 格式化读写函数 fscanf( )和


fpintf( ) ....................................................215
7.8 自定义数据类型 .................................... 182
9.4.5 自定义其他类型数据的读写
7.9 综合实例................................................ 183
函数................................................. 217
7.10 深入研究:单链表的插入与删除 ...... 185
9.5 文件的定位 ............................................ 217
本章小结 ........................................................ 187
9.6 文件的出错检测 .................................... 218
习题 ................................................................ 188
9.7 综合实例 ................................................ 219
第8章 位运算 ................................ 191 9.8 深入研究:读写效率问题 .................... 222
本章小结 ......................................................... 223
8.1 位运算符................................................ 191
习题 ................................................................. 224
8.1.1 按位与运算符 ................................ 191

3
C 语言程序设计(第 2 版)

第 10 章 编译预处理 ...................... 226 附录 A 预备知识 ............................. 237

10.1 宏定义 ................................................. 226 附录 A.1 计算机硬件系统的基本工作


10.1.1 不带参数的宏定义 ...................... 226 原理 .............................................. 237

10.1.2 带参数的宏定义 .......................... 228 附录 A.2 进制与进制转换 .......................... 239

10.1.3 宏定义的嵌套 .............................. 229 附录 A.3 规范化编程 .................................. 243

10.1.4 取消宏定义 .................................. 229


附录 B ASCII 码字符集 ................. 245
10.2 文件包含 ............................................. 230
10.3 条件编译 ............................................. 232
附录 C 运算符的优先级与结合性... 248
10.4 其他预处理功能 ................................. 233
10.5 预定义的宏名 ..................................... 234
附录 D C 库函数 ............................. 249
10.6 深入研究:头文件的重复引用问题 .... 234
本章小结 ........................................................ 235
参考文献 ........................................................ 254
习题 ................................................................ 235

4
第 章 1
C 语言概述

本章目标
◇ 了解计算机语言及 C 语言的发展历程
◇ 熟悉 C 程序的基本结构以及 C 语言的组成要素
◇ 明确 C 程序的开发过程以及 C 编程的基本思想
◇ 掌握在 VC 环境下 C 程序的创建与运行的方法

1.1 计算机语言的发展
人类对高效率与高精度计算需求的不断增长,成为计算机发展的强大动力,高效并方便地使
用计算机完成计算任务成为计算机语言发展的推动力量,使计算机语言的发展从面向机器逐渐向
面向人类转变,先后出现了机器语言、汇编语言和高级语言。

1.1.1 机器语言
现代计算机以冯·诺依曼结构为主体,冯·诺依曼结构计算机的内部采用二进制形式(即 0
和 1)表示数据和指令,这种结构的计算机能够直接识别和处理的就是由 0 和 1 编码组合而成的
二进制代码,我们称之为机器指令,一种计算机系统的全部机器指令的集合就是该计算机的机器
语言。机器语言是一种面向机器的计算机语言,与计算机型号有关,每种计算机只能识别自己的
机器语言。例如
001111
就是一条机器指令,通常表示向累加器送入一个数。
用机器语言编写的计算机程序,可以被计算机直接识别和处理,运算效率较高,但是缺点也
是显而易见的。第一,这种程序只能在特定的计算机上使用,不具有可移植性。第二,机器指令
本身很难表达出指令的含义,对于编程者来说记忆困难。第三,每条机器指令的功能过于单一,
编程效率低。

1.1.2 汇编语言
机器语言的上述缺点,制约了计算机的使用效率和使用范围,因此出现了汇编语言。汇编语
言用具有一定含义的符号表示机器指令,方便编程人员记忆和书写,提高了编程效率。例如:
ADD AX, 2

1
C 语言程序设计(第 2 版)

是一条汇编指令,通常表示将寄存器 AX 中存储的数据加上 2 再存入寄存器 AX 中。从指令本身


可以直接看出这条指令能够完成加法操作。
与机器语言相比,汇编语言容易记忆,含义明显,便于编程人员使用。但是,用汇编语言编
写的程序,计算机不能直接识别与处理,必须经过一种处理将其转换成二进制的机器指令才能执
行,这种处理过程称为“汇编”
,完成这种处理功能的程序称为“汇编程序”
。汇编语言中的汇编
指令与机器语言中的机器指令是一一对应的,
“汇编”过程主要就是将助记符式的汇编指令转换为
二进制的机器指令的过程,因此汇编语言也是机器相关的计算机语言。

1.1.3 高级语言
高级语言的出现,使计算机语言脱离了对计算机硬件的依赖,成为一种接近于人类自然语言
的计算机语言,为高效编程与计算机的广泛应用提供了可能,是计算机语言发展史上的一个飞跃。
例如:
y=x+2
就是一条高级语言的语句,表示将变量 x 的值加上 2 存入变量 y 中。
与汇编语言相比,高级语言具有以下优点。
(1)高级语言通常有一套特定的语法,这个语法与具体的计算机系统无关,用高级语言书写
的程序,可以独立于具体的计算机系统,也就是说,使用高级语言书写的程序,几乎不需做任何
修改,就可以运行在支持该语言的计算机系统上,具有可移植性。
(2)高级语言更接近于人类常用的表述方式,语句的含义更加明显,容易学习与记忆。
(3)一条高级语言的语句所完成的功能相当于多条机器指令,编程效率高。
自从高级语言问世以来,曾得到广泛应用的高级语言有 BASIC 语言、Fortran 语言、COBOL
语言、Pascal 语言、C 语言、Ada 语言以及 LISP 语言等。这些语言都有各自的特点和适用领域,
例如,Fortran 语言适用于数值计算,COBOL 语言适用于商业管理,C 语言适用于编写系统软件。
由于C语言的强大功能和诸多优点逐渐为人们所认识,到了 20 世纪 80 年代,C 语言很快在各大、
中、小和微型计算机上得到广泛的使用,成为当代最优秀的程序设计语言之一。
目前,面向对象编程语言已经成为继上述高级语言之后日益成熟并得到广泛应用的计算机语
言,如 Smalltalk、C++、Java 语言等,而且将可视化、事件驱动等新技术与计算机语言结合在一
起构建的各种集成开发环境已成为应用软件开发的重要工具,但是 C 语言的基础性作用不容忽视。

1.2 C 语言的发展及特点

1.2.1 C 语言的发展
C语言问世于 20 世纪 70 年代初,由美国电话电报公司(AT&T)贝尔实验室于 1978 年正式
发表,最初的 C 语言描述出现在由 Brian Kernighan 和 Dennis Ritche 合著的《the C Programming
(第 1 版)中,通常简称为“K&R”,也称为“K&R”标准。但是,在“K&R”中并没
Language》
有定义一个完整的标准 C 语言。1982 年,美国国家标准学会(American National Standards Institute,
ANSI)认识到标准化将有助于 C 语言在商业化编程中的普及,因此成立了一个委员会为 C 语言
及其运行库制定标准。1989 年,该委员会制定的标准被正式采用,即美国国家标准 X3.159-1989,

2
第1章 C 语言概述

通常称为 ANSI C89 标准。考虑到编程活动是国际化的,在完成 ANSI C 标准之后,国际标准化组


织(International Standard Organization,ISO)随即成立了 ISO/IEC JTC1/SC22/ WG14(ISO/IEC
联合技术第 1 委员会第 22 分委员会第 142 工作组),在 P. J. Plauger 的领导下,对 ANSI 标准做了
少量编辑性修改,变为国际标准 ISO/IEC 9899:1990,称为“标准 C 语言(1990)”
,该标准在技
术上与 C89 标准完全一致。1995 年,WG14 开始对 C 语言标准做更大的修订,于 1999 年完成并
获批准。新标准被命名为 ISO/IEC 9899:1999,简称“C99”,取代了原有的标准成为正式 C 语言
标准。
在 C 语言的发展过程中,出现了多种版本,例如,Microsoft 公司开发的 Microsoft C 或称 MS
C,Borland 公司开发的 Turbo C 和 Borland C,AT&T 公司开发的 AT&T C 等,这些C语言版本不
仅遵守了 ANSI C 标准,而且在此基础上各自做了一些扩充,使之更加方便和易用。

1.2.2 C 语言的特点
C 语言是一种小巧、高效的高级语言,具有丰富的运行库,可移植性强,这些优良的特性使
C 语言从众多的高级语言中脱颖而出,成为目前最流行的结构化程序设计语言之一。
C 语言的主要特点如下。
(1)语言简洁。C 语言仅有 32 个关键字和 9 种控制语句,程序书写形式自由。
(2)运算符丰富。在 C 语言中,括号、赋值、强制类型转换等都作为运算符处理,从而使 C
的运算类型极其丰富,表达式类型多样化,灵活使用各种运算符可以实现其他高级语言难以实现
的运算。
(3)数据类型丰富。C 的数据类型有整型、实型、字符型、数组类型、指针类型、结构体类
型和共用体类型,能用来实现对各种复杂数据(如链表、树、栈等)的处理。
(4)体现了结构化程序设计思想。C 语言具有典型的结构化控制语句,例如,选择语句(if
语句和 switch 语句)、循环语句(while 语句、do…while 语句和 for 语句),符合现代编程风格的
要求。
(5)便于实现模块化结构。C 语言是一种函数式语言,函数是组成 C 程序的基本模块。
(6)语法限制不太严格,程序设计自由度大。C 语言对数组不做下标越界检查,由编程人员保证数
组的正确使用;整型数据与字符型数据以及逻辑型数据可以通用等。
(7)允许直接访问物理地址,能进行位运算,能实现汇编语言的大部分功能,可以直接对硬
件进行操作。

1.3 认识第一个 C 程序
C 语言作为一种高级语言,与人类使用的自然语言具有一定的相似性。学习 C 语言的方式与
学习自然语言的方式相通。学习自然语言的最终目的是写文章,学习 C 语言的最终目的是编程
序,因此在学习 C 语言之前,对 C 程序的基本结构有一个初步的感性认识是必要的。
【例 1-1】 第一个 C 程序。
程序:
#include <stdio.h>
int main() /* 主函数 */

3
C 语言程序设计(第 2 版)

{
/* 调用标准函数,显示引号中的内容 */
printf("C is very fun!\n");
return 0;
}
运行结果如下:

例 1-1 是一个简单的 C 程序。第 2~7 行是该程序的主要组成部分,在 C 语言中称为主函数,


函数名为 main。可见,C 程序是由函数构成的。任何 C 程序有且仅有一个主函数,C 程序就从这
个主函数开始执行。
在函数 main( )中,第 5 行和第 6 行是完成函数功能的主要成分,在 C 语言中称为语句。可
见,C 函数是由语句构成的。
在第 6 行的语句中,可以看到一个英文单词“return”,它在 C 语言中有特殊的作用,即结束
本函数的执行,返回到函数的调用处。这种规定了特殊作用的单词称为关键字。可见,关键字是
C 语句的一种重要的组成要素,每个关键字表示某种特定的意义。
进一步地,关键字“return”是由六个英文字母组成的,在 C 语言中这些英文字母被称为字
符。这个程序中出现的“.”、
“\n”、“;
”等都是合法的 C 语言中的字符。
可以将 C 语言与自然语言进行类比,如表 1-1 所示。

表 1-1 自然语言与 C 语言组成要素的对比



自然语言 字 句 段 章
单词 短语
C 语言 字符 标识符(包括关键字) 表达式 语句 函数 程序

字符是 C 语言最基本的语言要素,采用一种编码形式描述,即美国国家标准信息交换码
(American National Standard Code for Information Interchange,ASCII),将所有的字符组织在一起
形成 C 语言的字符集。将字符集中的字符按照一定的规则进行组织,构成了 C 语言中的关键字或
标识符。将它们按照 C 语言规定的语法规则进行组织,构成了 C 语言中的各种语句。根据要完成
的特定功能将某些语句按照一定的规则组织在一起,构成了 C 语言的函数。多个函数组合在一起
构成了 C 程序,该程序能够完成指定的功能。因此,按照“字—词—句—段—章”的自然语言的
学习顺序学习 C 语言是一种非常有效的学习方法。
程序第 5 行语句中的“printf”是另一个函数的名字,功能是输出它后面圆括号中的字符串。
为了编程的方便,系统提供了许多标准函数供用户使用,这样的函数称为库函数。使用这些库函
数之前,需要在程序的开始包含该库函数所在的头文件,即例 1-1 程序中的第 1 行。
C 编译系统的实现者编写了很多库函数,统一放在函数库中,它们能够完成各种常用的功能,
利用这些库函数,程序员可以快速搭建功能强大的程序。因此,把经常要用到的功能编写成函数
并放在函数库中是一个很好的编程方法。
程序第 4 行以“/ *”开始、以“* /”结束的一段文字称为注释,注释文字可以是由任意字符
组成的。注释不参与程序的运行,主要用于对程序的某些关键部分进行说明,其目的是提高程序
的可读性。因此,在程序的适当位置添加必要的注释,是一种良好的编程习惯。

4
第1章 C 语言概述

C 语言不关心程序在文本行的开始位置,可以在任意位置输入程序。因此,编程人员在输入
源程序代码时,可以对源程序进行排版,使用 Tab 键缩进某些行是一种较好的排版方式,这样写
出的程序层次分明,可读性强。在例 1-1 中,第 4~6 行的程序代码缩进了 4 个空格。

1.4 开发第一个 C 程序

1.4.1 C 程序的开发过程
利用 C 语言编制程序的最终目的是高效地解决现实世界各领域中的实际问题,对实际问题进
行分析,以 C 语言构建程序的思想为指引设计解决问题的方案,是构建 C 程序的第一步,通常称
为程序设计。在此基础上,按照 C 语言的规则编写出 C 程序,把这个 C 程序存储在计算机中,运
行后产生正确的结果,是构建 C 程序的第二步,通常称为程序生成。经过这两步的工作,达到以
计算机为工具解决实际问题的目标。因此,C 程序的开发过程可以分为两个阶段:C 程序设计阶
段和 C 程序生成阶段。
1.C 程序设计阶段
C 程序设计是 C 程序开发过程中的重要阶段,在这一阶段中,编程人员需要根据实际问题的
要求,运用系统化的分析问题的方法与技术,完成解题过程的构思以及解决方案的设计,确定解
决该问题的算法,构造相应的数据结构和程序结构。
如果以自然语言的学习和运用与之类比,可以把用 C 语言开发一个程序看作是用自然语言写
一篇文章。在真正动笔写一篇文章之前,首先要进行布局和谋篇,根据文章的主题以及写作该文
章的目的,构思文章的总体结构,即文章共分几个部分,每个部分主要表现什么思想来支持整个
文章的主题;然后根据需要确定每个部分包括几个段落,是否引经据典,是否运用排比与对比等。
同样,在具体编写一个 C 程序之前,首先要明确该程序要解决的问题的目标和要求,根据目标和
要求构思程序的宏观结构,即程序共分几个部分,每个部分主要完成什么功能;然后根据需要确
定每个部分包含几个更小的子部分,需要存储哪些数据,是否使用库函数等,直到每一个子部分
都是可以解决的基本问题为止。通过这种逐步求精、逐层细化的方式进行问题求解是 C 程序设计
的基本方法,学习并熟练掌握这种方法是学习 C 语言的关键。
【例 1-2】 判断一个数是否是质数。
分析:
采用逐步求精的方式对该问题进行分析,显然该问题可以细化为 3 个子问题:给出待判断的
数、确定该数是否是质数、输出判断后的结果。
子问题 1:可以从键盘任意输入一个数,存储到计算机中。
子问题 2:常用的判断质数的方法是从 2 开始,依次用所有小于等于这个数的平方根的数来
进行测试,如果都不能整除,则该数是质数,否则不是质数。
子问题 3:记录判断后的结果,然后输出。
还可以对子问题 2 进行细化。仔细分析可以发现,对于质数的判断要经过多次除法操作,每
次进行除法运算时,除数都要在原有的基础上加 1。如果在进行某次的除法运算时能够整除,说
明该数不是质数,此时终止除法运算;如果除法运算没有被中途终止,说明该数是质数。
为了更好地把程序设计的结果表达出来,可以采用适当的方式描述程序设计的结果,编程人

5
C 语言程序设计(第 2 版)

员根据描述结果可以很方便地编写出 C 程序。描述程序设计结果的工具有程序流程图、N-S 图、
PAD 图、伪码等,伪码是最接近于自然语言的表达方式,对于初学者来说容易理解与掌握。
下面用伪码形式给出例 1-2 问题的算法描述。
(1)接收键盘输入的一个数,存储到变量 number 中;
(2)令变量 flag=1;
(3)令循环变量 i=2,当 i≤number 的平方根时,循环执行以下过程
如果 number 整除 i,那么令变量 flag=0,结束循环过程,进入第(4)步;
否则执行变量 i 加 1 操作;
(4)输出结果。如果 flag=1,那么输出“该数是质数”;
否则输出“该数不是质数”

2.C 程序生成阶段
C 程序设计阶段完成以后,要将伪码描述转换成 C 程序,这是 C 程序生成阶段的任务,通常
要经过程序编辑、程序编译、程序链接和程序运行 4 个步骤。
(1)程序编辑
把编好的程序输入计算机,以文件的形式存储在磁盘中的过程,称为程序编辑。能够完成这
项工作的软件称为编辑软件(也称为编辑器)。在编辑方式下建立起来的程序文件称为源程序文
件,简称源文件,源文件中的程序叫做源程序。绝大多数 C 语言编译器将文件名结尾为.c 的文件
看做 C 程序的源文件,因此,完成源程序编辑保存 C 源文件时,通常使用.c 作为文件名的后缀。
例如,可以将例 1-1 中的 C 源程序输入计算机保存到一个名为 ex1_1.c 的源文件中。
(2)程序编译
把编辑好的源程序翻译成目标代码的过程,称为程序编译。能够完成这项工作的软件称为编
译软件(也称为编译器)
。目标代码是指计算机能识别的二进制代码,存放目标代码的文件称为目
标文件。通常情况下,目标文件的名字与源文件名相同,后缀为.obj。但操作系统与编译软件不
同,也可能采用不同形式的后缀。UNIX 操作系统中,目标文件的后缀为.o。
在程序编译的过程中,对源程序中的每一条语句,编译器都要进行语法检查,当发现错误时,
会在屏幕上显示错误位置和错误类型的信息。用户看到这类提示信息后,要再次调用编辑器对源
程序中的错误进行修改,然后再次编译,直到排除所有的语法和语义错误。正确的源程序经过编
译后在磁盘上生成目标文件。例如,对源文件 ex1_1.c 编译完成后,系统会自动生成一个名为
ex1_1.obj 的目标文件,存放在源文件所在的存储路径下。
(3)程序链接
编译后产生的目标文件不能在机器上直接运行。程序中会用到库函数或者其他函数,它们都
是可重定位的程序模块,需要把它们连成一个统一的整体,这个过程称为程序链接。目标代码经
过链接后,生成可以运行的可执行程序,存储可执行程序的文件称为可执行文件,通常存放在目
标文件所在的存储路径下。通常情况下,可执行文件的名字也与源文件名相同,后缀为.exe。例
如,对目标文件 ex1_1.obj 完成链接后,系统会自动生成一个名为 ex1_1.exe 的可执行文件。
(4)程序运行
生成可执行文件后,就可以在操作系统控制下运行。若执行程序后达到了预期目的,则 C 程
序的开发工作到此完成。否则,要进一步对程序进行调试,重复编辑、编译、链接以及运行的过
程,直到取得预期结果为止。调试是指发现程序中的错误并改正错误的过程,是任何程序开发都
需要经历的一个非常重要的过程,需要编程人员发挥聪明才智,根据错误的表现分析错误的原因

6
第1章 C 语言概述

并找到错误的位置,然后进行正确的修改。
图 1-1 显示了开发一个 C 程序的基本过程。

开始

程序设计
伪码描述

程序编辑

源程序
程序编译 (file.c)


有错吗?
目标程序
(file.obj)

库函数和 程序链接
其他目标
程序


有错吗?
可执行程序
(file.exe)

程序运行


结果对吗?

结束

图 1-1 C 程序的开发过程

1.4.2 Visual C++环境下运行 C 程序


目前,有多种可用的 C 程序开发环境,如 Turbo C、Borland C、Visual C++、DEV-C 等,这
些开发工具已将程序的编辑、编译、链接和运行的过程集成在一起,由单个应用程序来完成上述
四项工作,并提供了多种帮助用户书写和修改程序的手段,用户使用非常方便。这样的应用程序
称为集成开发环境(Integrated Development Environment, IDE),IDE 通常基于窗口环境,在窗口
中完成对程序的编辑、编译、链接、运行和调试的全部工作,IDE 工具可以大大简化应用程序的

7
C 语言程序设计(第 2 版)

开发过程。因此在学习软件开发时,掌握一种 IDE 是非常必要的。在 Windows 操作系统中,Microsoft


Visual C++ 6.0(简称为 VC6.0)是一个非常受欢迎的 IDE。
1.在 VC6.0 中编辑 C 源程序
单击【开始】→【所有程序】→【Microsoft Visual Studio 6.0】→【Microsoft Visual C++ 6.0】,
启动 VC6.0,单击工具栏中的图标 ,系统弹出文本文件编辑窗口,标题为“Text1”,如图 1-2 所
示。单击菜单栏中的【File】→【Save as…】,系统弹出“保存为”窗口,如图 1-3 所示。在“保
存在(L):”右侧的下拉列表框中选择 C 源文件要保存的位置,在“文件名(N):
”右侧的文本框
中输入文件名称,如 ex1_1.c,单击“保存(S)”按钮,图 1-2 的文本文件编辑窗口的标题名会变
为“ex1_1.c”,此时完成了 C 源文件 ex1_1.c 的创建任务。在该窗口中可输入 C 源程序,并可以
实现对源程序的编辑工作。例如,可在该窗口中输入例 1-1 中的源程序,如图 1-4 所示。

图 1-2 文本文件编辑窗口

图 1-3 文件保存窗口

图 1-4 C 源程序编辑窗口

2.程序的编译与链接
单击菜单栏中的【Build】→【Build】,如果是首次编译一个 C 源程序,系统会出现一个提示
信息对话框,如图 1-5 所示。单击“是(Y)”按钮,如果此时用户对 C 源文件内容进行了修改但
没有保存,系统会弹出一个对话框,询问是否保存 C 源程序的内容,如图 1-6 所示。再次单击“是
(Y)”按钮,系统开始对源程序进行编译,并完成对目标文件的链接,如果没有任何编译和链接
错误,系统在编辑窗口下部出现“ex1_1.exe_0 error(s), 0 warning(s)”,表明没有错误,并生成可执

8
第1章 C 语言概述

行程序 ex1_1.exe,否则会出现一系列错误提示信息,编程人员要根据这些信息对程序进行修改,
直到没有错误、能够正确编译和链接为止。

图 1-5 提示信息对话框

图 1-6 询问是否保存对话框

3.程序的运行
单击菜单栏中的【Build】→【Execute ex1_1.exe】,运行 ex1_1.exe,系统在 DOS 窗口中给出
运行结果,如图 1-7 所示。按任意键可以返回 VC6.0 环境中的源程序编辑窗口。

图 1-7 显示运行结果窗口

特别提示:
不同操作系统下的各种编译器的使用命令不完全相同,使用时应注意计算机环境。

1.5 深入研究:调试手段与错误定位
除了少数极其简单的程序,很少会出现程序的编译、链接和运行一气呵成的情况,都会出现
或多或少的错误,找出程序中的错误并加以改正的过程就是调试,因此,程序设计是一项复杂的
智力活动,程序调试更是其中很具有挑战性的工作,是编程工作的一个重要组成部分,是保证程
序正常运行、能够满足任务规定的各项要求的重要步骤,提高程序测试和调试能力是优秀的 C 程
序员应努力的方向。因此,根据实际问题的需要完成了程序设计的重要工作之后,采用多种调试
手段发现程序中的错误并进行正确的修改,使程序能够正常运行并得到期望的结果,是 C 程序开
发中又一项艰巨而重要的任务。
程序出现的错误大致可以分为两类:程序编译错误和程序逻辑错误。
程序编译错误是由于源程序中存在语法错误而在编译过程出现的错误,这里所说的语法错误
是指不符合 C 语言的语法规则,如括号不成对,没有包含需要的头文件,语句后缺分号,函数未

9
C 语言程序设计(第 2 版)

声明等,是初学者经常出现的错误。如果源程序中出现了类似的错误,在编译软件对源程序进行
编译时就会输出相应的错误信息,对这些错误信息进行仔细阅读和分析,发现错误的原因和位置,
是改正这类错误的重要手段。例如,对于例 1-1 的程序,如果源代码写成下面的样子:
#include <stdio.h>
int main( )
{
/*调用标准函数,显示引号中的内容*/
printf("C is very fun!\n")
return 0
}
也就是丢掉了两个语句后面的分号,编译时会输出下面的错误信息:
ex1_1.c(5) : error C2143: syntax error : missing '; ' before 'return'
ex1_1.c(6) : error C2143: syntax error : missing '; ' before '}'
上述错误信息指出,在程序的第 5 行和第 6 行各丢失一个分号,这些信息会帮助程序员很容
易地找到错误的位置并改正。
对于有些语法错误,编译软件所报告的出错信息不一定非常准确。例如,如果在上例的 printf()
语句中漏掉了双引号的后半部分,即“printf("C is very fun!\n);”,此时编译软件可能报告下面的错
误信息:
error C2001: newline in constant
error C2143: syntax error : missing ')' before 'return'
这些错误信息和出现错误的位置及原因表面看起来关系不大,而且仅有一个错误时系统可能
会报告多条错误信息,在这种情况下,应该把注意力集中在产生第一条错误信息的语句及其前后
的语句,因为第一条错误信息往往在引发错误的真正源头附近。因此,对于初学者来说,出现错
误时不要烦躁或害怕,经常通过观察错误信息来分析错误原因,改正程序错误,不断地积累经验,
是提高调试能力的重要途径。同时,养成良好的编程习惯,避免出现语法错误是最明智的选择。
程序逻辑错误是指在程序通过了语法检查生成可执行文件后,在程序的运行过程中出现的错
误。这类错误对程序员来说是最难以解决的。如何发现错误出现的位置和产生的原因,是改正错
误的关键环节。一旦确定了排除错误的方案,就可以着手修改程序了。在修改代码时要注意以下
两点:第一,当程序有多处错误时,一般一次只修改一处错误,除非有其他的错误与这一错误紧
密地联系在一起;第二,在修改代码时,要保留原来代码的副本,以备在判断和修改方案不正确
时恢复程序的原状。错误修改完毕,要针对修改的部分进行测试,以保证发现的错误已经被正确
地修改,同时还要重新进行完整的测试,以确认在修改错误的过程中没有引入新的错误。
不同类型的逻辑错误应采用不同的错误定位方法。常见的程序逻辑错误一般可分为 3 类:
(1)正常结束但运行结果不对;(2)程序无法正常结束;(3)程序运行过程中崩溃。对于第
(1)类错误,首先要通过反复的测试,分析和发现错误产生的规律,例如,错误是固定的还是随
机的,是对所有的输入数据都产生还是只对部分输入数据产生,是在大规模数据的情况下产生还
是在小规模数据的情况下产生等,以便初步判断错误的性质。例如,随机产生的错误往往与变量
未初始化或数组访问时越界有关,固定的错误往往是由于完成计算任务的代码写错引起的。对于
第(2)类错误,往往是在运行中进入了无限循环,原因包括循环控制条件错误或对循环控制变量
的修改错误。为了判断程序在哪一个循环中出错,可以采用恰当的方法进行调试,例如,设置断
点使程序在某一条语句处停下来,设置打印语句输出适当的信息等,利用二分法搜索也是常用的
有效方法。对于第(3)类错误,多是由地址越界、数组下标和指针变量未初始化等地址访问错

10
第1章 C 语言概述

误引起的。这类错误可能会破坏程序的正常工作状态,并引起操作系统的干预,因此比较有效的
错误定位方法是使用二分排除法,即首先在程序的中间位置设置断点,或将程序的后一半注释掉,
以判断错误是在程序的前半部分还是后半部分,然后再对错误出现的区间进一步进行二分法排除,
这样可以快速定位到错误发生的位置。当错误区间已缩小到一定程度之后,可以采用单步执行的
调试手段把错误定位到特定的语句上。

本章小结
计算机语言的发展经历了机器语言、汇编语言和高级语言的发展历程,体现了计算机语言从
面向机器到面向人类的转变。C 语言是一种结构化的高级语言,自从 20 世纪 70 年代问世以来得
到了广泛的应用,学习 C 语言应按照“字—词—句—段—章”的自然语言的学习顺序进行学习,
这是一种自然、有效的学习方法。
C 语言有 32 个关键字、9 种控制结构和 34 种运算符,数据类型灵活多样。C 语言既具有高
级语言的功能,又具有低级语言的特性,这种双重性使 C 语言既是成功的系统描述语言,又是通
用的程序设计语言。
学习 C 语言的目的是编写 C 程序解决各领域中的实际问题。C 程序是由函数组成的,每个 C
程序有且仅有一个主函数。C 编译软件的实现者已经将很多常用的功能编写成标准函数放在函数
库中,大大减少了编程人员的工作量。
一个 C 程序的开发包括程序设计和程序生成两个阶段,逐步求精、逐层细化的方式进行问题
求解是 C 程序设计的基本方法,学习并熟练掌握这种方法是学习 C 语言的关键。根据程序设计阶
段的成果编写 C 源程序,然后经过编辑、编译和链接后生成可执行文件的过程是程序生成阶段的
任务。C 程序通常要经过测试与调试过程才能够得以正确运行,掌握测试与调试程序的手段与技
巧是成为成熟程序员的必备知识。
在众多的 IDE 环境中,VC++6.0 是一种较为流行和成熟的开发 C/C++程序的软件工具,本章
初步介绍了在 VC++6.0 环境下编辑、编译、链接和运行一个 C 程序的基本方法。熟练应用 VC++6.0
是学好 C 语言的基础。

习 题
【复习】
1.简要说明计算机语言的发展历程,剖析计算机语言发展的动力以及发展趋势。
2.简要叙述 C 程序的构成,并说明一个 C 程序要正确地运行,必须有什么函数,该函数在
程序中的地位如何?
3.开发一个 C 程序的一般过程是什么?
4.下列说法正确的是( )

A.在书写 C 语言源程序时,每个语句以逗号结束
B.注释时,“/”和“*”之间可以有空格
C.无论注释内容是多少,在对程序编译时都被忽略

11
C 语言程序设计(第 2 版)

D.C 程序每行只能写一个语句
5.C 语言源程序文件的后缀是( )
,经过编译和链接后生成了可执行文件,该文件的后
缀是( )

A..obj B..exe C..c D..doc
【应用】
参照例 1-1,编写一个 C 程序,输出以下信息。要求在 VC-6.0 环境下编辑和运行。
***********************************************************
Welcome to JiLin! Welcome to ChangChun!
***********************************************************
【探索】
1.从计算机语言的发展历程预测近 10 年计算机语言的发展趋势。 带格式的: 项目符号和编号
2.回文数字的判断问题。回文数字是这样的一类数字,它们顺着看和倒着看是相同的数,例
如 121、12321 等。

12
第 章 2
数据类型、运算符和表达式

本章目标
◇ 了解 C 语言的数据类型及分类
◇ 掌握 C 语言的常量和变量的定义及使用方法
◇ 掌握 C 语言运算符的使用方法
◇ 熟练掌握 C 语言各种表达式的用法

2.1 标识符和关键字
与从识字开始学习自然语言类似,下面从字符集开始学习 C 语言。

2.1.1 字符集
目前,国际上使用最广泛的计算机字符编码是 ASCII 码,标准的 ASCII 码字符集包括 128
个字符,主要由字母字符、数字字符、空格符、特殊字符和其他字符组成。ASCII 码字符集详见
附录Ⅱ。
(1)字母字符。包括大写字母 A~Z 以及小写字母 a~z 共 52 个字符。在 ASCII 码表中,字
母编码的排列顺序符合通常的自然语言顺序,而且对应的大、小写字母的 ASCII 码值相差 32。例
如,大写字母 A 的 ASCII 码值为 65,大写字母 B 的 ASCII 码值为 66,依此类推。与之相对应,
小写字母 a 的 ASCII 码值为 65+32=97,小写字母 b 的 ASCII 码值为 66+32=98。
(2)数字字符。包括 0~9 共 10 个字符。在 ASCII 码表中,数字编码的排列顺序也满足通常
的顺序即从 0 到 9,并且用 ASCII 码的十六进制值表示时,个位码正好与其对应的数字相同。例
如,数字 0 的 ASCII 码值为 48(十六进制表示为 30);数字 9 的 ASCII 码值为 57(十六进制表示
为 39)

(3)空格符。空格符只在字符常量和字符串常量中起作用。在其他地方出现时,只起间隔作
用,因此在程序中使用一个或多个空格符对程序的编译不产生影响,但在程序中恰当地使用空格
符能增强程序的清晰性和可读性。
(4)特殊字符。特殊字符是不可显示、不可打印的字符,用于计算机设备的操作控制以及在
数据通信时进行传输控制,因此也称为控制符。例如,FP 表示换页,用于在打印输出时进行换页
控制。
(5)其他字符。其他字符包括图形符、标点符和运算符等。例如,图形符“$”的 ASCII 码

13
C 语言程序设计(第 2 版)

值为 36,运算符“+”的 ASCII 码值为 43,标点符“,


”的 ASCII 码值为 44。

M提醒
在字符常量、字符串常量和注释中还可以使用汉字或其他可表示的图形符号。

2.1.2 标识符
C 语言中的标识符是由字符组成的有效序列,满足一定的构成规则,用于标识符号常量、变
量、函数、数组、用户自定义类型和文件。
C 语言规定,标识符由字母、数字、下划线组成,且第一个字符不能使用数字字符。例如,
Hello_c、student_1、age 都是合法的标识符,而 3Student、$Map、Sum&Mul 等是不合法的,不能
用作 C 语言的标识符。

C 语言的标识符区分大小写,如 Hello_c 和 hello_c 是两个不同的标识符。

在符合标识符命名规则的前提下,在程序中使用含义清晰的标识符,能够提高程序的可读性
和可理解性,从而为程序的编写和维护提供方便,因此建议程序编写者在命名标识符时尽量做到
“见名知意”,尽量避免使用没有实际含义的标识符。例如,存储面积值的标识符命名为 area,表
示累加和的标识符命名为 sum,不使用 value1、abc 等这样的标识符。

M提醒
C 语言的标识符分用户自定义的标识符和系统定义的标识符两种,虽然用户在定义标识符
时用下划线作首字符也符合命名规则,但通常将下划线开头的标识符留给系统使用。

2.1.3 关键字
关键字是系统定义的、具有特定含义、专门用作特定用途的 C 语言标识符,也称为保留字。
标准 C 语言中共有 32 个关键字,如表 2-1 所示。

表 2-1 C 语言的关键字
关 键 字 含 义 类 型

int 整型

short 短整型

long 长整型

float 单精度浮点型

double 双精度浮点型 数据类型

char 字符型

void 无值型

unsigned 无符号型

signed 有符号型

14
第2章 数据类型、运算符和表达式

续表
关 键 字 含 义 类 型

const 常量

struct 结构体型

union 联合型 数据类型

enum 枚举型

volatile 易变型

sizeof 求字节数 运算符

if 条件语句

else 与 if 配合使用

switch 开关语句

case 与 switch 配合使用

default 与 switch 配合使用

for 循环语句
流程控制
while 循环语句

do 循环语句

break 间断语句

continue 接续语句

return 返回语句

goto 跳转语句

auto 自动类型

extern 外部类型

static 静态类型 存储类型

register 寄存器类型

typedef 用户自定义类型命名

表 2-1 中的所有关键字都是由小写字母组成的。由于系统为每个关键字都规定了特
殊用途,因此,在 C 程序中,用户自定义标识符时不允许与关键字同名。

特别提示:
C99 标准中新增了一些关键字,如_Bool、_Complex、_Imaginary、inline 和 restrict。

2.2 数据类型
数据是程序的处理对象。为了便于对数据存储及运算的有效管理,C 语言规定了多种数据类
型,用来说明数据的存储格式、所占用的空间大小、可以表达的数据范围以及可以施加的运算。
C 语言的数据类型分类如图 2-1 所示。

15
C 语言程序设计(第 2 版)

整型
单精度实型
简单类型 实型
双精度实型
指针类型 字符
基本类型
枚举类型

数据类型 空类型

数组

构造类型 结构体

共用体

图 2-1 C 数据类型

C 程序中用到的数据在内存中都要根据其对应的数据类型分配一定大小的内存空间,分配多
大的内存空间,跟具体的硬件和编译软件有关。本章主要介绍其中的简单类型,其他数据类型在
后续章节中学习。ANSI C 标准规定了简单类型的最小长度和范围,如表 2-2 所示。

表 2-2 简单类型的最小长度和范围
类型名称 中文名称 字节数(byte) 位数(bit) 数值范围 备注
char 字符型 1 8 −128~127 −2 ~(27−1)
7

int 整型 2 16 −32768~32767 −215~(215−1)


float 单精度实型 4 32 −3.4×10−38~3.4×1038 6~7 位有效数字
double 双精度实型 8 64 −1.7×10−308~1.7×10308 15~16 位有效数字

ANSI C 标准规定,简单类型的前面还可以加上修饰符,使简单类型的语义更加丰富,方便 C
编程人员选用恰当的数据类型。这样的修饰符共有 4 种:
(1)signed:有符号;
(2)unsigned:无
符号;
(3)long:长型;
(4)short:短型。组合后形成的类型如表 2-3 所示。

表 2-3 ANSI C 标准中的简单类型


数据类型 中文名称 字节数(byte) 位数(bit) 数值范围 备注
char 字符型 1 8 −128~127
unsigned char 无符号字符型 1 8 0~255
signed char 有符号字符型 1 8 −128~127
int 基本整型 2 16 −32768~32767
unsigned int 无符号整型 2 16 0~65535
signed int 有符号整型 2 16 −32768~32767
short int 短整型 2 16 −32768~32767 可省略整型说明符 int
unsigned short int 无符号短整型 2 16 0~65535
signed short int 有符号短整型 2 16 −32768~32767

16
第2章 数据类型、运算符和表达式

续表
数据类型 中文名称 字节数(byte) 位数(bit) 数值范围 备注
long int 长整型 4 32 −231~231−1
unsigned long int 无符号长整型 4 32 0~232−1 可省略整型说明符 int

signed long int 有符号长整型 4 32 31 31


−2 ~2 −1
float 单精度实型 4 32 −3.4×10−38~3.4×1038 6~7 位有效数字

double 双精度实型 8 64 −1.7×10−308~1.7×10308 15~16 位有效数字

M提醒
(1)当无修饰符时,所有整型数据和实型数据的缺省类型都是有符号类型,char 的缺省类
型跟具体的编译器有关。
(2)某些编译器允许将 unsigned 用于实型,如 unsigned double,但这一用法降低了程序的
可移植性,故建议一般不要采用。
(3)修饰符 long 可以与 double 结合,但 long double 类型占用的字节数随编译器的不同而
不同,一般很少使用。
(4)在 Visual C++ 6.0 中,int(基本整型)与 long(长整型)在内存中所占的字节数以及
表示的取值范围对应相同。

特别提示:
C99 标准在 C89 标准的基础上做了一些修改,增加了三种基本类型:_Bool、_Complex
和_Imagimary,同时还增加了修饰符 long long,即出现了 long long int、unsigned long long int 等
数据类型,并允许用 LL 或 ll 作为后缀来表示 long long 型。

2.3 常量与变量

2.3.1 常量
在程序运行过程中,其值不能被改变的量称为常量。常量分为整型常量、实型常量、字符常
量、字符串常量和符号常量 5 种。
1.整型常量
整型常量也称整型常数、整常数或整数,可以用十进制、八进制和十六进制方式表示,分别
称为十进制整数、八进制整数和十六进制整数。
十进制整数由正、负号和阿拉伯数字 0~9 组成,如 10、−213 和 5678 都是正确的十进制整
数的例子。除整数 0 外,其他十进制整数的首位数字不能是 0。
八进制整数由正、负号和阿拉伯数字 0~7 组成,首位数字必须是 0,作为八进制整数的
前缀,说明其后的数字构成的是八进制整数。如 010、−0213 和 0567 都是正确的八进制整数
的例子。
十六进制整数由正、负号和阿拉伯数字 0~9、英文字符 a~f 或 A~F 组成,首位数字前必须
有前缀 0x 或 0X。如 0x10、−0x36 和 0XA5 都是正确的十六进制整数的例子。
17
C 语言程序设计(第 2 版)

0386 和 0x1g 是非法的整数,因为 0386 作为八进制整数含有非法数字 8,作为十进制整数首


位数字是 0,是非法的;0x1g 作为十六进制整数含有非法字符 g。
需要强调的是,十进制、八进制和十六进制只是整数的三种不同的表示形式,在计算机
内部都转换成相应的二进制数存储。因此,同一个整数可以有三种不同的表示方法。下面的
例子分别给出了整数 10 的十进制、八进制和十六进制表示。
10 /* 十进制整数 10,在内存中对应二进制数 0000000000001001 */
012 /* 八进制整数 12,在内存中对应二进制数 0000000000001001 */
0xa /* 十六进制数 a,在内存中对应二进制数 0000000000001001 */
缺省情况下,在−32768~32767 范围内的整型常数的数据类型是 int 型,超过此范围而
在−2147483648~2147483647 范围内的整型常数的数据类型是 long 型。也可以通过在整型常数后
面加上字母后缀来强制指定其数据类型,C 语言规定的字母后缀的具体含义如下。
(1)后缀 l 或 L 表示 long 型常数。例如,−12l、01235456720L。
(2)后缀 u 或 U 表示 unsigned 型常数。例如,12u、034u、0x2fdU。
(3)后缀 lu 或 LU 表示 unsigned long 型常数。例如,123246875LU。
2.实型常量
实型常量也称为实型常数或实数(也经常叫做浮点数)
,是以十进制方式表示的实数。实型常
量有两种表示形式:十进制小数形式和指数形式。
例如,1.23、0.123 和 123.0 都是正确的十进制小数形式的实型常数。当一个实型常数的整数
部分或小数部分为 0 时,可以将这个 0 省略,但小数点必须保留。例如:
.123 /* 实数 0.123 */
−.123 /* 实数 -0.123 */
123. /* 实数 123.0 */
0. /* 实数 0.0,也可以写成 .0,但不可以把两个 0 都省略,只写小数点 */
都是正确的实型常数。
采用指数形式表示实型常数时通常称为科学记数法,如实型常数 123.0 的指数形式表示为
1.23E2,其中,E(或小写字母 e)是指数符号,符号 E 后面的数为指数。因此,1.23E2=1.23×102=123.0。
显然,一个实型常数可以采用多种指数形式来表示。下面的例子分别给出了实数 123.0 的不同表示。
12.3e1 /* 实数 123.0 */
0.123E3 /* 实数 123.0 */
123.0E0 /* 实数 123.0 */
1230.0e-1 /* 实数 123.0 */
采用指数形式表示实型常数时,如果小数点后面的尾数部分是 0,可以将小数点以及其后的
数字 0 都省略。如 1230.0e-1 也可以写成 1230e-1。

采用指数形式表示实型常数时,指数符号之前必须有数字,而且指数符号之后的指
数必须为整数。如 E6、6E3.5、.e6、e 都是不合法的实型常数。

缺省情况下,一个实型常数的数据类型是 double 型,但可以通过在实型常数后面添加字母后


缀来强制指定其数据类型。C 语言规定的字母后缀的具体含义如下。
(1)后缀 f 或 F 表示 float 型常量。例如,−12.3f、.123F。
(2)后缀 l 或 L 表示 long double 型常量。例如,−12.3l、.123E3L。

18
第2章 数据类型、运算符和表达式

M提醒
123.是实型常数,而 123 是整型常数。如果要表示实型常数,则 123.后面的小数点不可以省略。

特别提示:
Visual C++ 6.0 以默认格式输出实数时,小数点后最多保留 6 位有效数字。
3.字符常量
用一对单撇号括起来的单个字符称为字符常量。字符常量有两种:一般字符常量和特殊字符常量。
单撇号中的字符是数字、字母等 ASCII 码字符集中包含的除“'”和“\”以外的所有可显示
字符时,该字符常量是一般字符常量。如'a'、'9'、'!'是正确的一般字符常量。
特殊字符常量也称转义字符,是 C 语言中表示字符的一种特殊形式,由反斜杠“\”和其后的
字符组成,其作用是转变“\”后面的字符的含义,实现某种特定的功能。例如,'\n'就是一个转义
字符,表示换行。常用转义字符的具体含义如表 2-4 所示。

表 2-4 常用转义字符及其含义
转 义 字 符 含 义 ASCII 码值(十进制)
\a 响铃(BEL) 07
\b 退格(BS) 08
\f 换页(FF) 12
\n 换行(LF) 10
\r 回车(CR) 13
\t 水平制表(HT) 09
\v 垂直制表(VT) 11
\\ 反斜杠 92
\? 问号字符 63
\' 单撇号字符 39
\" 双撇号字符 34
\0 空字符(NULL) 0
\ddd 任意字符 1~3 位八进制
\xhh 任意字符 1~2 位十六进制

可将表 2-4 中的转义字符的使用归纳为以下 3 种情况:


(1)反斜杠后面跟某些特定字符表示不可打印的控制字符和特定功能的字符;
(2)表示具有特定含义的单撇号、双撇号和反斜杠字符;
(3)反斜杠后面跟一个八进制或十六进制数表示任意字符,其中八进制或十六进制数表示该
字符对应的 ASCII 码值。
注意,计算机在存储字符常量时,存储的是该字符对应的 ASCII 值。
4.字符串常量
用一对双撇号括起来的字符序列称为字符串常量。其中,双撇号只起定界作用,字符序列中
的字符可以是数字、字母等 C 语言字符集中包含的除双撇号和反斜杠以外的所有可显示字符,也
可以是转义字符。如"ChangChun"、"Hello World!"、"ert\011ert"、"33\'1\'2-1233"都是正确的字符串
常量。
注意,计算机在存储字符串常量时,首先按字符串中字符的顺序逐一存储每个字符,当所有字符
19
C 语言程序设计(第 2 版)

存储完毕,最后存储一个转义字符‘\0’作为该字符串的结束标志。
字符串中字符的个数称为字符串长度,如上面的 4 个字符串的长度分别为 9、12、7 和
11。长度为 0 的字符串(即一个字符都没有的字符串)称为空串,表示为""(一对紧连着的
双撇号)

字符常量占一个字节的内存空间,而字符串常量在内存中存储时由系统自动在存储的所有字
符尾部添加串结束标记“\0”
,所以字符串常量 5 的内存字节数等于字符串长度加 1。

M提醒
对于初学者来说,弄清下面的每个常量所表达的数据类型非常重要。
5 /* int 型常量 */
5.0 /* double 型常量*/
'5' /* 字符常量*/
"5" /* 字符串常量 */

5.符号常量
在 C 语言中,允许将程序中的常量定义为一个标识符,该标识符称为符号常量。在使用符号
常量之前必须先加以定义,定义符号常量的一种方法是:
#define 符号常量名 常量值
其中,
“#define”是 C 语言的一条预处理命令,称为宏定义命令。符号常量名是一个合法的
用户定义的标识符。
将一个常量值定义为符号常量后,在 C 程序中就可以使用该符号常量了。例如:
#define PI 3.1415926 /*定义了一个符号常量 PI,常量值为 3.1415926*/
#define R 10 /*定义了一个符号常量 R,常量值为 10*/
在定义了上述符号常量后,在 C 程序中计算圆的面积时可以用 PI *R*R 表示,而不必写成
3.1415926 * 10 * 10。
在编写 C 程序时,对于那些具有某种特定含义的常量建议使用符号常量来表示,如上述的圆
的半径、销售商品时的折扣率等。使用符号常量有以下好处。
(1)程序的可读性好。在定义符号常量时,符号常量的标识符要尽可能地表达它所代表的含
义,也就是“见名知意”
,这样可以提高程序的可读性。
(2)程序的可维护性好。符号常量一经定义,以后在程序中所有出现该标识符的地方均代之
以该常量值。因此要对一个程序中多次使用的符号常量的值进行修改,只需对预处理命令中定义
的常量值进行修改即可。
(3)避免误操作。符号常量的值在它的作用域内不能再被赋以其他的值,这样可以避免程序
中的误操作。

在定义符号常量时,常量值后面没有分号。如果出现了分号,则分号也被作为常量
值的一部分。

M提醒
一般情况下,定义符号常量时使用的标识符全部由大写英文字母组成。

20
第2章 数据类型、运算符和表达式

2.3.2 变量
在程序运行过程中,其值可以被改变的量称为变量。
C 语言中的变量遵循“先定义,后使用”的原则,即所有变量在使用之前必须明确定义。
1.变量的定义
变量定义的一般形式如下:
类型说明符 变量名列表;
其中,类型说明符必须是有效的 C 数据类型,变量名列表是由一个或多个变量名组成的序列,
各变量名之间用逗号分隔,变量名应符合标识符的命名规则。
例如,
int sum; /* 定义 sum 为整型变量 */
double x, y; /* 定义 x,y 为双精度实型变量 */
char a, b; /* 定义 a,b 为字符型变量 */
系统根据变量的数据类型为其分配相应的内存空间。如上例中系统会为 int 型变量 sum 分
配 2 个字节的内存空间(在 Visual C++ 6.0 中为 4 个字节),为 double 型变量 x 和 y 分别分配 8 个
字节的内存空间,为 char 型变量 a 和 b 分别分配 1 个字节的内存空间。

在变量定义中,变量名后面必须有分号,否则会出现编译错误。

在变量定义之前加上修饰符 const,则该变量的值在程序运行期间不可改变,这也是一种常量
的定义方法。例如:
const double pi = 3.1415;
这里定义了一个常量 pi,其值为 3.1415,在程序中不可以修改 pi 的值。
2.变量的使用
完成了变量定义之后,系统会根据该变量的数据类型为变量分配一定数目的内存单元,在 C
程序中就可以利用赋值操作将该变量的值存储到对应的内存单元中,这是变量最基本、最简单的
使用方法。变量的正确、合理的使用对于实现程序的功能是非常重要的。
【例 2-1】 变量的使用示例。
int x, y, z;
float f;
char c;
x = 10;
y = -10;
f = 123.4567;
c = 'a';
下面以例 2-1 说明各类型数据在内存中的存储形式。
(1)整型数据在内存中的存储形式
定义了一个 int 型变量 x 后,系统会为其分配 2 个字节的内存单元。变量 x 的值 10 在内存中
的存储形式如下:

第 15 位 第0位
0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0

21
C 语言程序设计(第 2 版)

如果把一个负数赋值给定义的 int 型变量,则该数值在存储时以补码形式表示。变量 y 的


值−10 在内存中的存储形式如下:
第 15 位 第0位
1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 0

用补码表示数据时,最高位是符号位,如果该位为 0 表示正数,为 1 表示负数。


各种无符号类型变量所占的内存空间字节数与相应的有符号类型变量相同,只不过最高位不是符
号位而是一个普通二进制位。因此,有符号整型变量与无符号整型变量之间在存储形式上的区别如下:
有符号整型变量,最高位为符号位,能够表示的最大整数为 32767
第 15 位 第0位
0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

无符号整型变量,最高位为普通二进制位,能够表示的最大整数为 65535
第 15 位 第0位
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

(2)实型数据在内存中的存储形式
实型数据在内存中是按照指数形式存储的,系统把一个实型数据分成小数部分和指数部分分
别存放。对于 float 型数据,系统会为其分配 4 个字节的内存单元。变量 f 的值 123.4567 用指数形
式可表示为 0.1234567e4,它在内存中的存储形式如下:
第 31 位 第8位 第7位 第0位
+.1234567 +4

通常情况下,float 型数据以 24 位表示小数部分(包括符号位)


,以 8 位表示指数部分(包括
指数的符号)

(3)字符型数据在内存中的存储形式
字符型数据在内存中是以该字符相对应的 ASCII 码值来存储的,即以整数形式存储。变量 c
的值'a'对应的 ASCII 码值为 97,它在内存中的存储形式如下:
第7位 第0位
0 1 1 0 0 0 0 1

处理字符变量时经常会用到对应的 ASCII 码值,由于 ASCII 码值的规律性,记住下面这些字


符的 ASCII 码值会给编程带来一定的方便。
null:0 空格:32 '0':48 'A':65 'a':97

M技巧
对于 char 型数据来说,缺省类型是有符号类型还是无符号类型与具体的编译器有关,因此
在编程之前应该对其进行验证,确定它的缺省类型。验证方法如下:
char c = 255; /*定义一个字符变量 c*/
printf("c = %d \n", c); /*以整数形式输出字符变量*/
如果输出的结果是−1,则该编译系统的 char 型是有符号类型,如果输出的结果是 255,则
是无符号类型。

22
第2章 数据类型、运算符和表达式

3.变量的初始化
变量的初始化是指在变量定义的同时为其赋初值。例如:
int x = 10;
表示定义 x 为 int 型变量的同时为该变量赋初值为 10。
char c = 'p';
表示定义 c 为 char 型变量的同时为该变量赋初值为'p'。
可以同时为多个变量赋初值,中间用“,”分隔。例如:
int x = 10, y = 20;
表示定义 x 和 y 为 int 型变量的同时,分别为其赋初值为 10 和 20。
也可以仅对其中一部分变量赋初值。例如:
int x, y, z = 30;
表示定义 x、y 和 z 为 int 型变量,x 和 y 未赋初值,z 的初值为 30。

任何变量在进行运算前都应有明确而具体的值,为变量赋值时不要超出变量类型所
允许的范围,否则会出现错误。

2.4 运算符与表达式
为了解决现实世界中的复杂问题,在使用常量和变量保存数据后,还需要对这些数据进行各
种运算处理,这就涉及运算符与表达式。
运算符是告诉编译系统执行各种运算的符号。C 语言提供了 34 种运算符,包括算术运算符(9
种)、关系运算符(6 种)、逻辑运算符(3 种)、赋值运算符(1 种)
、条件运算符(1 种)、逗号运
算符(1 种)
、求字节数运算符(1 种)
、强制类型转换运算符(1 种)
、下标运算符(1 种)、成员
选择运算符(2 种)
、指针运算符(2 种)和位运算符(6 种)
。任何一种运算符都要有自己的操作
数(也称运算对象)
,按照操作数的个数不同将运算符分为 3 类:单目运算符(操作数只有 1 个)、
双目运算符(操作数必须是 2 个)和三目运算符(操作数必须是 3 个)

按照一定的 C 语法规则,用运算符将常量、变量和函数连接起来组合而成的式子,就是 C 表
达式。在 C 表达式中,还可以使用圆括号“( )”将需要优先计算的部分括起来。单个的常量或变
量可以看成是 C 表达式的特例。任何表达式按一定的规则计算后都会得到一个结果值,该结果具
有相应的数据类型。
C 语言规定了运算符的优先级和结合性。运算符的优先级是指,在同一表达式中具有不同的
运算符时表达式的求值顺序,优先级高的运算符先于优先级低的运算符进行运算;运算符的结合
性是指,在运算符的优先级相同时表达式的求值方向,如果在一个运算对象两侧的运算符的优先
级相同,则按规定的结合性处理。其中,结合性分为左结合性(自左向右)和右结合性(自右向
左)
。运算符的优先级与结合性参见附录Ⅲ。
本节详细介绍 C 语言中部分运算符及相应的表达式,其他运算符与表达式将在后续章节中介绍。

2.4.1 算术运算符及算术表达式
C 语言中的算术运算符包括:+、−、*、/、%、++、−−,如表 2-5 所示。

23
C 语言程序设计(第 2 版)

表 2-5 算术运算符
算术运算符 名称及含义 举 例
+ 加法运算符(双目)、正值运算符(单目) 1+2、+3
− 减法运算符(双目)、负值运算符(单目) 10−9、−8
* 乘法运算符(双目) 1*2
/ 除法运算符(双目) 3/2
% 取余运算符/模运算符(双目) 54%12
++ 自增运算符(单目) int i=3; i++;
−− 自减运算符(单目) int i=3; i−−;

表 2-5 中,运算符“+”作为加法运算符是双目运算符,作为正值运算符是单目运算符。
用算术运算符和圆括号将操作数连接起来的、符合 C 语法规则的式子称为算术表达式。操作
数可以是常量、变量、函数等。例如,x + y、2.3 − 3.4、(x * 2)/y 、x * x + y * y、sin(x) + sin(y)
都是合法的算术表达式。
对算术表达式求值,要依据不同算术运算符的运算规则以及 C 规定的运算符的优先级与结合
性来进行,结果值的数据类型是整型或实型。算术运算符的优先级与结合性如表 2-6 所示。
表 2-6 算术运算符的优先级与结合性
运 算 符 要求操作数的个数 优 先 级 结 合 性
++、−− 1(单目运算符) 高 自右向左(右结合)
、−(负值)
+(正值) 1(单目运算符) 自右向左(右结合)

*、/、% 2(双目运算符) 自左向右(左结合)


+、− 2(双目运算符) 低 自左向右(左结合)

1.加减乘除和取余运算
C 语言中的加法、减法、乘法运算与数学中的含义相同,如果参与运算的两个操作数都是整
数,则表达式结果类型为整型,只要有一个是实型,则表达式结果类型为实型。这里强调一下 C
语言的除法运算和取余运算。
对于除法运算,当两个整数相除时,结果仍为整数,小数部分舍去。例如,3/2 的结果为 1。
注意,是丢掉小数部分,而不是进行四舍五入。如果两个操作数中有一个为负值,则舍去的方向
是不固定的。例如,对于表达式−3/2,有的编译器得到结果为−1,有的编译器则给出结果为−2,
但通常情况下编译器采取“向零取整”的方法,即向零靠拢。根据这种舍去方法,−3/2 的结果为
−1。如果参加运算的两个操作数中有一个是实数,其结果也为实型,例如,表达式 3/2.0 的结果
为 1.5,表达式 4.0/4.0 的结果为 1.0。
取余运算的规则是取整数除法的余数,因此参与运算的操作数必须是整型数据。例如,3%2
的结果为 1,3%2.0 是非法的 C 表达式。如果参加运算的两个操作数中有一个是负数,则结果的
符号取决于被除数。例如,−3%2 的结果是−1,3%−2 的结果是 1。
【例 2-2】 计算表达式 10 − 9 / 6 * 5 的值。
表达式 10 − 9 / 6 * 5 用到了三个算术运算符,分别是减法运算符“−”、除法运算符“/”
和乘法运算符“*”。根据运算符的优先级和结合性,10 − 9 / 6 * 5 等价于 10 − ((9/6)*5),结果
为 5。
24
第2章 数据类型、运算符和表达式

M提醒
为了增强可读性,在书写表达式时,适当地加入一些空格符和圆括号是非常必要的,对于
程序员来说,这是一种良好的编程习惯。
例如,表达式 10 − 9/6*5、10−(9/6*5)和 10−9/6*5 的结果完全相同,但与第三个表达式相
比,前两个表达式的可读性更强。

2.自增和自减运算
高级语言中,对于变量 x,经常有 x=x+1 和 x=x−1 的运算,为了书写简单方便,C 语言提供
了另外的表达形式,属于紧凑表达:
x++、++x /* 相当于 x=x+1 */
x--、--x /* 相当于 x=x-1 */
其中,
“++”称为自增运算符,
“−−”称为自减运算符,都是单目运算符,在算术运算符中优
先级最高,是右结合性。

运算符++和−−的操作数只能是变量,不能是常量和表达式。但对于这个变量的数据
类型没有限制。

例如,表达式 3++和(i+k)++是非法的。假设 x 为整型变量,f 为实型变量,c 为字符型变量,


而 int x = 3;x++;或者 float f = 6.5;f−−;或者 char c = 'a';c++;都是合法的。
以自增运算符为例,x++和++x 都相当于 x = x + 1,但运算规则有所不同。对于 x++,采用
“先用后加”原则,即变量 x 先参与表达式运算,然后再将 x 的值加 1。对于++x,采用“先加后
用”原则,即先将变量 x 的值加 1,然后再用得到的结果参与表达式的运算。如果单独使用自增
运算符而不是在表达式中使用时,两者作用相同。
(1)单独使用自增运算符
假设整型变量 x 和 y 的值都为 3,可以对 x 和 y 进行自增运算。例如:
x++ /* 等价于 x=x+1,此时变量 x 的值为 4 */
++y /* 等价于 y=y+1,此时变量 y 的值为 4 */
(2)在表达式中使用自增运算符
假设有四个整型变量 x、y、p、q,x 和 y 的值都为 3,分析下面表达式的值。
p = x++ /* 等价于 p=x; x=x+1,此时 p 的值是 3,x 的值是 4 */
q = ++y /* 等价于 y=y+1; q=y,此时 q 的值是 4,y 的值是 4 */
C 语言中设增量运算符(自增运算符、自减运算符)主要有两个目的:
(1)程序书写简洁;
(2)相对于加 1、减 1 后再赋值的方式,生成代码效率高,速度快。因此在可能的情况下,编程
人员应该尽量使用增量运算符。增量运算符的使用是 C 语言中的一个难点,也是各类考试中的一
个重点,读者应仔细体会。
【例 2-3】 假设 x 的初值为 3,顺序执行下面的表达式,计算变量 x 和 y 的值。
(1)y = x++
(2)y = −++x
(3)y = ++x*x++
上述表达式的计算过程及结果如下。

25
C 语言程序设计(第 2 版)

(1)变量 x 和 y 的值分别是 4 和 3。

运算符“++”在变量 x 的后面,应用“先用后加”原则,变量 y 的值是 3,变量 x


的值是 4。

(2)变量 x 和 y 的值分别是 5 和−5。

运算符“++”的优先级高于“−”
,所以该表达式等价于− (++x),应用“先加后用”
原则,变量 y 的值是−5,变量 x 的值是 5。

(3)变量 x 和 y 的值分别是 7 和 36。

单目运算符的优先级高于双目运算符,所以该表达式等价于(++x) * (x++),对于++x,
应用“先加后用”原则,x 的值变为 6;对于第二个操作数(x++)
,应用“先用后加”
原则,首先取得变量 x 的值 6,与第一个操作数的值进行乘法运算,即 6*6=36,然后再
对变量 x 进行加 1 运算,即 x=6+1=7;最后将表达式的值 36 赋值给变量 y。

在表达式含有多个“++”或“− −”运算符时,不同编译器的计算结果并不完全相同,
使用之前应先测试。

例如,假设整型变量 x 和 y 的值都是 5,下面表达式的计算结果不完全相同。


p = (x++) + (x++) + (x++)
q = (++y) + (++y) + (++y)
在 TC 2.0 中,p、q、x 和 y 的值分别是 15,24,8 和 8;在 VC 6.0 中,p、q、x 和 y 的值分
别是 15,22,8 和 8。
3.数学中的算术表达式与程序中的算术表达式
对于初学 C 语言的人来说,要特别注意某些数学公式或数学表达式在程序中的表达方式和数
据类型的问题,否则会导致错误的结果。
【例 2-4】 写出计算三角形面积的 C 表达式。
分析:
三角形面积的计算公式是:底×高÷2,定义变量 bottom 和 height 分别存储三角形的底和高的
值,C 表达式可写成 (bottom * height) / 2。这里需注意除法运算符“/”的运算规则,当 bottom 和
height 均为整数时,除以整数 2 后的商也为整数,而实际的计算结果可能为实数(在不能整除的
情况下),为了解决这种精度损失而导致的运算错误,可以将表达式中三个操作数 bottom、height
和 2 中的任何一个变为实型。因此,应将上述表达式改写为(bottom * height) / 2.0。
在 C 语言中,其合法的算术表达式如下:
(1)(x + y) * h / 2.0
(2)s* (s − a) * (s − b) * (s − c)
(3)(x + 1) * (x + 1) + (y − 1) * (y − 1)
(4)(A * B + C) / (D * E−F)
【例 2-5】 写出下列数学表达式对应的合法的 C 算术表达式。
(1)(x+y)×h÷2
(2)s(s−a)(s−b)(s−c)
26
第2章 数据类型、运算符和表达式

(3)(x+1)2+(y−1)2
A×B + C
(4)
D× E − F

2.4.2 关系运算符及关系表达式
表 2-7 列出了 C 语言中的关系运算符。关系运算符用来比较两个运算对象的大小,比较的结
果是一个逻辑值(真或假)
,返回 1 或 0。关系运算符属于双目运算符。

表 2-7 关系运算符
关系运算符 名称及含义 举 例
> 大于(双目) 20>10
>= 大于等于(双目) x>=y
< 小于(双目) 20<10
<= 小于等于(双目) x<=y
== 等于(双目) 10= =10
!= 不等于(双目) 20!=10

用关系运算符和圆括号将操作数连接起来的、符合 C 语法规则的式子称为关系表达式。关系
运算符两侧的操作数可以是常量、变量和表达式,例如,'a' < 'b'、x > y、(x > y) < (x >= y)都是合
法的关系表达式。
C 语言提供的 6 种关系运算符中,前 4 种关系运算符(<、<=、>、>=)的优先级相同,后两
种关系运算符(= =、!=)的优先级相同,并且前 4 种高于后 2 种。
与算术运算符相比,关系运算符的优先级要低于算术运算符,关系运算符和算术运算符的优
先级与结合性如表 2-8 所示。

表 2-8 运算符的优先级与结合性
运算符 要求运算对象的个数 优 先 级 结 合 性
算术运算符 高
<、<=、>、>= 2(双目运算符) 自左向右(左结合)

= =、!= 2(双目运算符) 低 自左向右(左结合)

【例 2-6】 计算各关系表达式的值。
(1)10 > 10
(2)10 >= 10
(3)0 > 10! = 10
(4)3 * 8 − 9 != 5 + 7 % 3
上述关系表达式的计算过程及结果如下。
(1)10 与 10 是两个相等的常量,因此 10>10 的结果为假,该表达式的值为 0。
(2)同上题,表达式 10>=10 的结果为真,返回值为 1。
(3)根据关系运算符的优先级,该表达式等价于(0>10) != 10,0>10 的结果为假,返回值为 0,

27
C 语言程序设计(第 2 版)

0!=10 的结果为 1。
(4)根据运算符的优先级规则,该表达式等价于(3*8 − 9) != (5 + 7%3),进一步运算后表达式
转化为 15!=6,结果为 1。
【例 2-7】 变量 b 和 c 的初值均为 10,分别计算各表达式的值,并给出计算后变量 b、c 的值。
(1)b = = c
(2)b = = c++ * 2
(3)b++ >= ++b < c++
上述关系表达式的计算过程及结果如下。
(1)表达式的值为1,变量 b、c 的值分别为 10、10。

变量 b 和 c 的值都是 10,比较的结果是真(返回 1)
,该运算没有改变变量自身的值。
因此,表达式的值为1,变量 b、c 的值不变仍然是 10。

(2)表达式的值为0,变量 b、c 的值分别是 10、11。

该表达式等价于 b= = ((c++)*2)。首先变量 c 的值 10 与 2 进行乘法运算得到结果 20,


然后变量 c 的值做自增运算变为 11。变量 b 的值 10 和计算得到的结果 20 进行关系运算
“= = ”
,结果为假,返回值为 0。

(3)表达式的值为 1,变量 b、c 的值分别是 12、11。

该表达式等价于( (b++) >= (++b) ) < (c++)。首先进行“b++”和“++b”的运算,得


到表达式的结果 10 和 11;然后对 10 和 11 进行关系运算“>=”
,结果为假(返回 0),
此时变量 b 经过 2 次自增运算变为 12;再将表达式“c++”的值 10 与前面的计算结果 0
进行关系运算“<”,结果为真(返回 1)
,此时变量 c 经过 1 次自增运算变为 11。

【例 2-8】 用 C 语言描述满足下列条件的关系表达式。
(1)判断变量 n 是奇数的表达式。
(2)判断变量 n 是整数 3 的倍数的表达式。
满足上述条件的合法的 C 表达式如下。
(1)n%2 = = 1,表明变量 n 除以 2 余 1,此时 n 即为奇数。
(2)n%3 = = 0,表明变量 n 除以 3 余数为 0,此时 n 是 3 的倍数。

对于关系运算符“>=”、
“<=”、
“= =”和“!=”
,在书写时不能在两个字符之间加入
空格符,这样会出现编译错误。如把“> =”写成“> =”
。还要注意,判断两个操作数是
否相等的关系运算符是“= =”,而不是“=”
,后者是 C 语言中的赋值运算符(后面会有
详细介绍)
,这是初学者常犯的错误。

2.4.3 逻辑运算符及逻辑表达式
表 2-9 列出了 C 语言中的逻辑运算符。ANSI C 标准规定,参与逻辑运算的操作数可以不是逻
辑值,该操作数为非 0 时表示真,为 0 时表示假,但逻辑运算的结果只可以取逻辑值(真或假)

返回值为 1 或 0。

28
第2章 数据类型、运算符和表达式

表 2-9 逻辑运算符
逻辑运算符 名称及含义 举 例
! 逻辑非(单目) !x
&& 逻辑与(双目) x&&y
|| 逻辑或(双目) x||y

逻辑与运算符“&&”和逻辑或运算符“||”是双目运算符;逻辑非运算符“!”是单目运算符。
其运算规则与数学中的定义相同,表 2-10 列出了逻辑运算的真值表。

表 2-10 逻辑运算的真值表
a b !a !b a&&b a||b
非0 非0 0 0 1 1
非0 0 0 1 0 1
0 非0 1 0 0 1
0 0 1 1 0 0

从表 2-10 可以看出:
(1)对于逻辑与运算,当 a 和 b 同时为真时,a&&b 的值为真,否则为假;
(2)对于逻辑或运算,当 a 和 b 同时为假时,a||b 的值为假,否则为真;
(3)对于逻辑非运算,就是对操作数进行取反操作。
用逻辑运算符和圆括号将操作数连接起来的、符合 C 语法规则的式子称为逻辑表达式。例如,
x && y、'x' || 'y'、!x && y 都是合法的 C 逻辑表达式。
对逻辑表达式求值时,也需要考虑优先级与结合性的问题。根据 ANSI C 标准的规定,逻辑
运算符的优先级顺序由高到低为:!、&&、||,其中运算符“&&”和“||”的结合性为左结合,运
算符“!”的结合性为右结合。到目前为止,掌握的运算符的优先级与结合性如表 2-11 所示。

表 2-11 运算符的优先级与结合性
运 算 符 要求运算对象的个数 优 先 级 结 合 性
! 1(单目运算符) 自右向左(右结合)

算术运算符 自左向右(左结合)
关系运算符 自左向右(左结合)
&& 2(双目运算符) 自左向右(左结合)

|| 2(双目运算符) 自左向右(左结合)

逻辑运算符中的“&&”和“||”优先级低于关系运算符,
“!”高于算术运算符。
需要强调的是,在逻辑表达式求解时,并不是所有的逻辑运算符都被执行,当表达式的运
算结果能够确定以后,运算过程将立即终止,后面的部分不予执行。这种现象称为逻辑运算符
的短路现象,也叫懒惰求值法。具体情况如下。
(1)x && y && z 只有 x 为真(非 0)时,才需要判断 y 的值,只有 x 和 y 都为真时才需要
判断 z 的值。因此只要判定 x 为假,系统便终止运算,此时整个表达式的值已经确定为假。
(2)x || y || z 只有 x 为假,才需要判断 y 的值,只有 x 和 y 都为假才需要判断 z 的值。因此只
要判定 x 为真,系统便终止运算,此时整个表达式的值已经确定为真。
29
C 语言程序设计(第 2 版)

下面举例说明逻辑表达式的应用。
【例 2-9】 假设 x = 10,y = 20,分别计算各逻辑表达式的值。
(1)!x
(2)x && y
(3)!x + 5 || 10 % y >= x − 10 < y
上述逻辑表达式的计算过程及结果如下。
(1)由于 x = 10,根据 C 语言的规定,非 0 为真,因此表达式“!x”的计算结果为假,返回 0。
(2)操作数 x 和 y 的值都是非 0 值,根据运算符“&&”的运算规则,表达式“x&&y”的计
算结果为真,返回 1。
(3)该表达式等价于( (!x) + 5 ) || ( ( (10%y) >= (x−10) ) < y ),逻辑运算符“||”前面的表达式
结果为真,根据逻辑运算符的短路现象,不需要计算后面的表达式的值,此时整个表达式的结果
为真,返回 1。
【例 2-10】 写出满足要求的合法的 C 逻辑表达式。
(1)用 x 表示 0 到 9 之间的字符
(2)x 和 y 都是大于 0 的数
(3)判断 x 的取值范围在 40 和 100 之间,即 40≤=x≤=100
(4)判断某一年是闰年
满足上述条件的合法的 C 表达式如下。
(1)x >= 48 && x <= 57 或 x>'0' && x<='9'
(2)x > 0 && y > 0
(3)x >= 40 && x <= 100
(4)year % 4 = = 0 && year % 100 != 0 || year % 400 = = 0

C 表达式“x>=40 && x<=100”与数学表达式“40<=x<=100”的含义是相同的。其


实“40<=x<=100”也是合法的 C 表达式,但不能完成 x 取值范围的正确判断。例如,
变量 x 的值为 30,不处于 40 和 100 之间,但表达式“40<=x<=100”的值为真。原因
是按照关系运算符的运算规则,首先进行“40<=x”的计算,得到的结果为假,返回
0,然后再与 100 进行关系运算“<=”,即进一步计算表达式“0<=100”的值,结果
为真。

2.4.4 赋值运算符及赋值表达式
1.基本的赋值运算符
赋值运算符的符号为“=”
,作用是将右侧的表达式赋值给左侧的变量,是双目运算符。例如,
x = 10 表示执行一次赋值运算,把常量 10 赋值给变量 x。
由赋值运算符将一个变量和一个表达式连接起来的式子称为赋值表达式。这里要注意的是赋
值运算符的右侧可以是任意一个合法的 C 语言表达式,包括常量或者另一个赋值表达式,但左侧
必须是一个变量,不可以是常量或表达式。例如,x = 10 和 x = y = 10 都是合法的赋值表达式,但
5=8 和 x+y=8 都是不合法的赋值表达式。
ANSI C 标准规定,赋值运算符的优先级低于算术运算符、关系运算符和逻辑运算符,其结合
性为右结合。表 2-12 列出了已经学过的运算符的优先级与结合性。

30
第2章 数据类型、运算符和表达式

表 2-12 运算符的优先级与结合性
运 算 符 要求运算对象的个数 优 先 级 结 合 性
! 1(单目运算符) 高 自右向左(右结合)
算术运算符
关系运算符 2(双目运算符) 自左向右(左结合)
逻辑运算符(&&和||) 2(双目运算符) 自左向右(左结合)
赋值运算符 2(双目运算符) 低 自右向左(右结合)

【例 2-11】 假设变量 x 为整型,计算各赋值表达式的值。


(1)x = y = 10
(2)x = 10+ (y = 20)
(3)x = 10+ (y = 20) / (z = 30)
上述赋值表达式的计算过程及结果如下。
(1)根据赋值运算符的右结合性,先将 10 赋值给变量 y,此时变量 y 的值为 10,同时表达式
y = 10 的值也为 10,然后将表达式的值 10 赋值给变量 x,此时 x 的值为 10,整个赋值表达式的
值也为 10。
(2)首先计算赋值表达式“y = 20”的值,得到结果 20,同时变量 y 被赋值为 20;然后计算
10+20 的值得到结果为 30,再进行赋值运算,此时 x 的值为 30,整个表达式的值也为 30。
(3)先计算表达式“y = 20”和“z = 30”的值得到结果分别为 20 和 30,然后计算表达式
10 + 20 / 30 的值得到结果为 10,再进行赋值运算,此时 x 的值为 10,整个表达式的值也为 10。
在进行赋值运算时,如果赋值运算符两侧操作数的数据类型不一致,系统会将右侧操作数的
类型转换成左侧变量的数据类型,然后进行赋值运算,这种转换通常称为赋值转换。具体情况如下。
(1)左侧变量和右侧操作数都是整型或字符型时,根据左侧和右侧操作数所占的字节数不同
进行相应的处理。假设左侧和右侧操作数分别为 a 位和 b 位,具体处理方法如下:
当 a=b 时,直接赋值即可。
当 a<b 时,仅将右侧操作数低端的 a 位赋值给左侧变量即可。
当 a>b 时,将右侧操作数赋值给左侧变量低端的 b 位,高 a−b 位的处理方式是:如果左侧变
量是无符号数或正数,则全部补 0,否则全部补 1。
(2)右侧操作数为实型,左侧变量为整型或字符型,则将操作数变为整型(舍去小数部分)

再赋值给左侧变量。例如,变量 x 为 int 型,则经过表达式“x = 5.8”的运算后,x 的值是 5。
(3)右侧操作数为整型或字符型,左侧变量为实型,则按照左侧变量的数据类型,将操作数
补足有效位后赋值给左侧变量。例如,变量 f 为 float 型,则经过表达式“f='a'”的运算后,f 的值
是 97.000000。

赋值表达式的左端必须是一个可寻址的量(如变量)
。例如,表达式“x+y”是不可
寻址的,不能放在赋值表达式的左侧,因此赋值表达式“x+y=5”不是合法的 C 表达式。

2.复合的赋值运算符
在赋值运算符“=”之前可以加上算术运算符或移位运算符构成复合的赋值运算符。C 语言规
定可以使用 10 种复合赋值运算符,分别为+=、−=、*=、/=、%=、<<=、>>=、&=、^=、|=。本
节只介绍前五种,后五种将在第 8 章详细介绍。

31
C 语言程序设计(第 2 版)

对复合的赋值表达式求值时,先将该运算符右侧的操作数与左侧的变量进行指定的复合运
算,然后将计算结果赋值给左侧的变量,并作为该赋值表达式的值。复合的赋值运算符的优先级
与“=”相同,结合性也是右结合。
【例 2-12】 假设变量 x = 10,y = 20,计算各赋值表达式的值。
(1)x += 10
(2)x *= y + 20
(3)x += x−= x / 10
上述复合的赋值表达式的计算过程及结果如下。
(1)根据复合的赋值表达式的求解规则,x += 10 等价于 x = x + 10,先将变量 x 的值与操作
数 10 相加,得到计算结果为 20,然后进行赋值运算,将 20 赋值给变量 x,此时整个表达式的值
为 20,变量 x 的值也是 20。
(2)该复合赋值表达式等价于 x = x * (y + 20),首先将表达式“y + 20”的值与变量 x 的值 10
相乘,然后将结果 400 赋值给变量 x。此时整个表达式的值为 400,变量 x 的值也是 400。
(3)由于赋值运算符的右结合性,该表达式等价于 x = (x + (x = x − (x/10)))。因此,先计算表
达式“x =x−(x/10)”的值,得到结果为 9,此时变量 x 的值也是 9。然后计算表达式“x = x + 9”
的值,得到结果为 18,此时整个表达式的结果为 18,变量 x 的值也是 18。
根据实际问题构造表达式时,采用复合的赋值运算符有两点好处:一是可以简化程序的书写,
使程序精练;二是可以提高编译效率,产生质量较高的目标代码。

2.4.5 条件运算符及条件表达式
条件运算符“?:
”是 C 语言中唯一的三目运算符。条件运算符的优先级高于赋值运算符,低
于逻辑运算符。表 2-13 列出了已经学过的运算符的优先级与结合性。
表 2-13 运算符的优先级与结合性
运 算 符 要求运算对象的个数 优 先 级 结 合 性
! 1(单目运算符) 高 自右向左(右结合)
算术运算符
关系运算符 2(双目运算符) 自左向右(左结合)
逻辑运算符(&&和||) 2(双目运算符) 自左向右(左结合)
?: 3(三目运算符) 自右向左(右结合)

赋值运算符 2(双目运算符) 自右向左(右结合)

用条件运算符和圆括号将操作数连接起来的、符合 C 语法规则的式子称为条件表达式。
条件表达式的一般形式如下:
表达式 1?表达式 2:表达式 3
其中,表达式 1 一般为关系表达式或逻辑表达式。例如,x>10 ? 100 : 200 就是一个合法的条
件表达式。当表达式 1 为其他合法的 C 表达式时,系统自动将该表达式的结果转换为逻辑值。例
如,5 ? 100:200 也是一个合法的条件表达式。
条件表达式的运算规则是:首先计算表达式 1 的值,如果表达式 1 的值为真,则计算表达式
2 的值,并将结果作为整个表达式的值;如果表达式 1 的值为假,则计算表达式 3 的值,并将结
果作为整个表达式的值。

32
第2章 数据类型、运算符和表达式

【例 2-13】 假设变量 x = 10,y = 20,z = 30,计算各条件表达式的值。


(1)x > 10 ? 100:200
(2)x >=10 ? x+z:y > 20 ? y + z:(z=50)
上述条件表达式的计算过程及结果如下。
(1)根据条件表达式的运算规则,先计算表达式“x > 10”的值得到结果为假,然后计算冒
号“:”后面的表达式的值,得到结果为 200,因此该条件表达式的值为 200。
(2)根据条件运算符的运算规则,该表达式等价于“x>=10 ? (x+z) : (y>20 ? (y+z) : (z=50))”
先计算表达式 x > =10 的值得到结果为真,返回 1;然后根据规则计算冒号“:
”前面的表达式的
值,得到结果为 40。

根据条件运算符的右结合性,形如“a ? b : c ? d : e”的条件表达式等价于“a ? b: (c ?
,而不是“(a ? b : c) ? d : e”
d : e)” 。

M提醒
在条件表达式中,当表达式 2 和表达式 3 的数据类型不一致时,条件表达式运算结果的数
据类型与两者中精度较高的数据类型一致。

2.4.6 逗号运算符及逗号表达式
在C语言中,逗号“,”是一种运算符,称为逗号运算符,也称为顺序求值运算符,是一种双
目运算符。逗号运算符的优先级是所有运算符中级别最低的,结合性为左结合。
用逗号运算符将多个表达式连接起来构成的式子称为逗号表达式。
逗号表达式的一般形式如下:
表达式 1 , 表达式 2 , …, 表达式 n
例如,表达式“x + y , x − y”和“1 + 2 , 2 + 3”都是合法的逗号表达式。
逗号表达式的运算规则是:从左至右依次求出表达式 1、表达式 2 直到表达式 n 的值,并以
表达式 n 的值作为整个逗号表达式的值。
【例 2-14】 假设变量 x 为 int 型,计算各逗号表达式的值。
(1)x = 10 , x + 10
(2)(x = 1 + 2 , x + 3) , x + 4
上述逗号表达式的计算过程及结果如下。
(1)根据逗号运算符的运算规则,先计算表达式“x = 10”的值得到结果为 10,变量 x 的值
也是 10;然后计算第二个表达式“x+10”的值得到结果为 20,因此整个表达式的值为 20。
(2)先计算出 x 的值等于 3,再进行 x+3 的运算,得到结果为 6,此时表达式“x = 1 + 2 , x + 3”的
结果为 6,变量 x 的值为 3;然后进行 x+4 的运算,得到结果为 7,因此,整个逗号表达式的结果为 7。
需要强调的是,逗号“,
”表示运算的顺序,如果把这个逗号表达式放在赋值表达式的右边,
则赋值表达式左边变量的值就是逗号表达式中最后那个表达式的值。
例如,假设整型变量 y 的值是 20,则赋值表达式
x = (y = y − 10, 10 / y)
计算完成后,变量 x 的值是 1。因为变量 y 的初值是 20,减去 10 后结果再除 10,得到的最终结
果为 1,再赋值给变量 x。

33
C 语言程序设计(第 2 版)

2.4.7 求字节数运算符及求字节数表达式
求字节数运算符“sizeof”是 C 语言提供的一种特殊的运算符,是一种单目运算符。将求字
节数运算符与操作数组合在一起构成的式子称为求字节数表达式。
求字节数表达式的一般形式有以下两种:
sizeof(表达式)
或者
sizeof(数据类型名)
前一种的作用是求出整个表达式的值所对应的数据类型占内存单元的字节数,结果是一个正
整数。后一种的作用是求出指定数据类型所占内存单元的字节数,结果也是一个正整数。例如,
sizeof(int)、sizeof('\a')、sizeof(x)、sizeof(x+y)都是合法的求字节数表达式,如果变量 x 和 y 的数据
类型为 int 型,则表达式的值分别是 2、1、2 和 2。
由于系统为每种类型的数据分配的内存空间的大小与编译器有关,因此在某些情况下,编
程时使用求字节数运算符,可以使 C 程序不受编译器的影响,实现在一种环境下编写的程序在
其他 C 环境下也可以正确运行,从而增强 C 程序的可移植性。

2.5 数据类型转换
很多情况下,在进行某种数值运算的过程中,会对操作数的数据类型进行类型转换,有些转
换由系统自动进行,有些转换由程序员人为指定。对于系统自动进行的类型转换通常要遵循一定
的转换规则,如图 2-2 所示。 double float 高
图 2-2 体现了系统自动进行的两种情况的类型转换:基本转换 long
和自动转换(也称隐式转换),此外,数据类型转换还有第三种情况
unsigned
即强制转换(也称显式转换)。
1.基本转换 int char、short 低

图 2-2 中,横向向左的箭头表达的是基本转换的情况,基本转 图 2-2 数值型数据在


换是一种必定发生的数据类型转换。也就是说,在运算过程中,char 运算过程中的转换规则
型数据和 short 型数据一定会自动转换为 int 型数据,float 型数据一定会自动转换为 double 型数
据,然后再参与相应的运算。这样做的目的是为了提高运算的精度。
2.自动转换
图 2-2 中,纵向向上的箭头表达的是自动转换的情况,当参与运算的操作数的数据类型不一
致时会发生自动转换,箭头表示数据类型转换的方向,基本原则是把精度低的数值转换为精度高
的数值。例如,int 型与 double 型数据进行运算时,系统首先会自动将 int 型数据转换成 double 型,
然后进行运算,结果为 double 型。
【例 2-15】 计算各表达式的值,注意数据类型转换的过程。
(1)10 − 3.0 / 2
(2)5L + 8
上述表达式的计算过程及结果如下。
(1)操作数 3.0 为 double 型数据,因此操作数 10 和 2 会由系统转换为 10.0 和 2.0 参与运算,
最终得到的结果为 double 型数据 8.500000。
34
第2章 数据类型、运算符和表达式

(2)操作数 5L 为 long 型数据,因此操作数 8 会由系统转换为 8L 参与运算,最终得到的结


果为 long 型数据 13L。

M提醒
图 2-2 中纵向箭头只表明转换的方向,不是转换必须遵循的顺序。例如,对于 int 型与 double
型数据间的转换,不是经过 unsigned 型和 long 型的两级转换后才转换成 double 型,而是直接
转换成 double 型。

3.强制转换
除赋值转换外,系统自动进行的数据类型转换都是低精度类型向高精度类型的转换,如果
需要将高精度类型的数值转换为低精度类型,必须由程序员人为指定,即强制转换。因此,强
制转换需指定转换后的数据类型。
强制转换的一般形式如下:
(类型说明符)(表达式)
其功能是把表达式的值的数据类型强制转换成类型说明符所指定的类型。例如,(float) a 表
示将变量 a 转换成 float 型,而(int)(x + y)表示将表达式“x + y”的值转换成 int 型。
使用强制转换时,需要注意以下问题。
(1)强制类型转换的过程中有可能造成信息的丢失。例如,将实型数值转换为整型数值时
会舍去小数部分,表达式“(int) 5.8f”的值是 5。
(2)如果被转换的数值在指定的结果类型中无法表示,那么虽然符合 C 语言的语法,这种
转换也是没有意义的。例如,把 52769.8 强制转换成 int 型,即(int) 52769.8,得到结果−12767,
一般来说没有意义。
(3)进行强制转换时,系统自动生成一个所需类型的中间变量,原变量不会发生任何变化。
例如,假设 float 型变量 f 的值为 5.6f,进行强制转换“(int)f”后,得到一个 int 型的数据 5,但变
量 f 的值仍然是 5.6f。
(4)C 语言将强制转换类型符看做是单目运算符,单目运算符的优先级高于双目运算符。例
如,表达式“(int)(5.6+8.8)*3”的值是 42,而表达式“(int)5.6+8.8*3”的值是 31.4。
到目前为止学过的 C 语言运算符的优先级与结合性如表 2-14 所示。
表 2-14 C 语言运算符的优先级与结合性
运 算 符 要求运算对象的个数 优 先 级 结 合 性
! ++ − − + − (类型)sizeof 1(单目运算符) 右结合

* / % 2(双目运算符) 左结合
+ − 2(双目运算符) 左结合
< <= > >= 2(双目运算符) 左结合
== != 2(双目运算符) 左结合
&& 2(双目运算符) 左结合
|| 2(双目运算符) 左结合
? : 3(三目运算符) 右结合
= += −= *= /= %= 2(双目运算符) 右结合

, 2(双目运算符) 左结合

35
C 语言程序设计(第 2 版)

2.6 深入研究:整型数值的溢出问题
在 C 语言中,任何数据都有自己的数据类型,每种类型的数据都有取值范围的限制,如果超
出了这个范围,就无法正确地保存该数据的值,这就是通常所说的溢出。对于整型数据来说,如
果两个足够大的正数相加或相乘后,得到的结果就有可能超出整型的取值范围,从而产生溢出,
这样就会输出一个不正确的结果。通常系统会对超出范围的数据进行截断,即数据的高位部分被
抛弃,只保留数据的低位部分。
一旦产生溢出,计算结果就是错误的。例如:
int a = 1 , b = 68979 , c = 1024 , result_1 , result_2;
result_1 = a * c;
result_2 = b * c;
表达式执行后,result_1 的值是 1024,result_2 的值是−13312。
result_2 的值是由于计算溢出而产生的错误结果。当产生溢出时,计算结果到底是正数还是负
数取决于数据截断后保留在符号位上的是 0 还是 1。
因为整型运算有可能产生溢出,造成计算结果的错误,所以在可能产生溢出的运算表达式后
应当进行必要的判断。
整型运算在下列三种情况下可能产生数据的溢出。
(1)两个符号相同的数值相加;
(2)两个
符号相异的数值相减;
(3)两个数相乘。其中,第(2)种情况可以归入第(1)种情况讨论。
如果两个符号相同的数值相加时出现了溢出,则结果的符号必然与加数和被加数的符号相异。
因此可以根据加数、被加数与“和”的符号的异同来判断加法结果是否溢出。
下面是判断两数相加是否会溢出的方法:
int a , b , c;
int flag;
c = a + b;
flag = (a>=0)==(b>=0)&&(a>=0)!=(c>=0);
如果 flag 的值为 1,说明表达式“c=a+b”计算的结果溢出,否则该表达式计算的结果没有
溢出。
两个数相乘时溢出的判断要简单一些。乘法运算产生溢出的条件与乘数和被乘数的符号无
关,只与它们的绝对值有关。当乘法运算产生溢出时,乘积除以一个因子一定不等于另一个因子。
下面是判断两数相乘是否会溢出的方法:
int x , y , z ;
int flag ;
z = x * y;
flag = x != z/y;
如果 flag 的值为 1,说明表达式“z = x * y”计算的结果溢出,否则该表达式计算的结果没有
溢出。
整型数值的溢出表现了计算机程序和数学计算的差异,虽然在大多数情况下,程序中的算式
“应该”得到我们期望的算式结果,但这必须在满足一定条件的情况下才可以。在实际的程序开发
过程中,整型数值的溢出很少像我们前面举例那样用一个明显溢出的数据来赋值,更多的情况下
是发生在程序某处的运算结果中,或是在某种特例条件被满足的情况下,因此 C 程序员必须对程

36
第2章 数据类型、运算符和表达式

序的各种情况进行测试,以保证程序计算结果的正确性。

本章小结
学习语言的基本要素是字、词。本章认识了 C 语言中的字符、关键字和表达式。
在 C 程序中,算法处理的对象是数据,参与运算的数据种类各不相同,这就涉及数据类型的
问题。C 语言提供了丰富的数据类型,通常可以将其分为两大类,分别是基本类型和构造类型,
本章着重介绍了基本类型中的简单类型,主要包括整型、实型和字符型。本章还主要介绍了简单
数据类型的长度、占用字节数、数值范围等。
C 语言中的数据有常量和变量之分,它们分别属于以上这些数据类型,在程序运行过程中不
能被改变的量称为常量,可以改变的量称为变量。定义符号常量要使用预处理指令,在系统编译
之前由编译系统来处理,将算法中涉及的不可以在程序运行过程中进行更改的数据定义成常量,
可以增强程序的可读性和可修改性。变量在使用之前必须加以定义,定义变量的基本格式是先给
出数据类型说明符,然后以列表的形式给出需要定义的变量名,变量名的命名要符合标识符的命
名规则,并且尽量做到“见名知意”以增强程序的可理解性。
为了完成复杂的问题求解,C 语言规定了运算符,包括算术运算符、关系运算符、逻辑运算
符、赋值运算符、条件运算符、逗号运算符和求字节数运算符等。由这些运算符和圆括号将操作
数连接起来的符合 C 语法规则的式子称为表达式。不同类型的数值在运算过程中为了达到一致的
数据类型需要经过数据类型转换,C 语言的数据类型转换分为基本转换、自动转换和强制转换。
C 语言丰富的运算符是 C 语言具有高度灵活性的重要因素之一。

习 题
【复习】
1.下面的标识符哪些是合法的?
A.1_x B.Int C.ABC D.i_10
E.sum.a F.%abc G._stu H.abc d
2.C 语言的数据类型有哪些?
3.下面是一段程序:
#define N 10
int main( )
{
int sum;
sum = (20 + 30) / N;
printf("sum = %d\n",sum);
printf("result is 5\n");
return 0;
}
在这段程序中,哪些是常量?哪些是变量?
4.下面哪些写法是正确的,而且是常量?
A.12 B.−.345 C.1.23e4 D.'OPQ'

37
C 语言程序设计(第 2 版)

E.E10 F.0678 G.0xabcdef H."\n\\\'\123"


5.字符常量与字符串常量有什么区别?
6.简述转义字符的用途并用实例加以说明。
7.C 语言为什么要规定对所有用到的变量要“先定义,后使用”?这样做有什么好处?
8.下面哪个表达式的值为 5?
A.19/4 B.19.0/4 C.(float)19/4 D.(int)(19.0/4+0.5)
9.指出下面的表达式中,哪些是错误的?
(1)a+b=5
(2)56=a11
(3)i=i++
(4)12
(5)a=5, b=6, c=7
10.计算下列表达式的值。已知 int 型变量 a = 5, b = 6, c = 7,char 型变量 ch = '0',float 型变
量 f = 2.1。
(1)(a−b*2)/3
(2)3.2*(a+b+c)
(3)ch+a+f
(4)a+b%3*(int)(b+c)%3/6
11.写出下面表达式运算后 x 的值。设 x 定义为整型变量,其初值为 10。
(1)x+=10
(2)x−=10
(3)x*=5+10
(4)x/=x+x
(5)x+=x−=x*=x
(6)x+=x−=x*x
12.设 int 型变量 a=3, b=4, c=5,则表达式((a+b)>c)&&(b= =c)&&a||b+c&&b+c 的值是( )

A.0 B.1 C.2 D.3
13.如果 int 型变量 i=1, j=2, k=3, l=4,则条件表达式 i<j?i:k<l?k:l 的值是( )

A.1 B.2 C.3 D.4
【应用】
写出完成下列要求的 C 语言的合法表达式。
(1)已知半径 r,计算球的体积 V。
(2)已知 3 条边长度 a、b、c,计算三角形面积 S。
(3)已知上底 a、下底 b 和高 h,计算梯形面积 S。
【探索】
若变量 f 已说明为 float 类型,i 为 int 类型,则下面哪些表达式(或语句)能够实现将 f 中的
数值保留小数点后两位,第三位进行四舍五入的运算?
A.f = (f*100+0.5)/100.0; B.i = f*100+0.5,f=i/100.0;
C.f = (int)(f*100+0.5)/100.0; D.f = (f/100+0.5) *100.0;

38
第 章 3
控制结构

本章目标
◇ 了解结构程序设计的基本思想
◇ 掌握 C 语言中常用的标准输入/输出函数的用法
◇ 掌握流程控制语句的用法
◇ 熟练应用常用的流程控制语句编写程序

3.1 结构程序设计
1965 年,计算机科学家 E. W. Dijikstra 提出仅采用 3 种基本控制结构(顺序、选择和循环)
进行程序设计,随后 Bohm 和 Jacopini 对此给予了证明,也就是说任何程序功能的实现都可以仅
采用这 3 种基本结构来完成,从而提出了结构程序设计的概念,自此以后,所有的高级语言都支
持结构程序设计技术,当前流行的面向过程的高级语言都是以结构程序设计技术为基础设计的。
C 语言是一种充分体现结构程序设计思想的计算机语言,因此 C 程序一般由 3 种基本控制结
构构成,分别是顺序结构、选择结构和循环结构。顺序结构是指从第 1 条语句到最后一条语句完
全按顺序执行的结构;选择结构是指在程序执行过程中,根据用户的输入或中间结果而有选择地
执行若干不同的任务的结构;循环结构是指反复执行某一段程序,直到某种条件不满足时才结束
执行该段程序的结构。大多数情况下,C 程序是顺序、选择、循环 3 种结构的复杂组合。
C 语言通过语句来实现 3 种基本控制结构,这些语句可以归纳为 5 类,分别是表达式语句、
函数调用语句、空语句、复合语句和程序流程控制语句。

3.2 顺序结构程序设计
顺序结构是 C 语言中一种最基本的控制结构,语句按照位置的先后顺序执行,这些语句可以
归纳为 4 类,分别是表达式语句、函数调用语句、空语句和复合语句。

3.2.1 表达式语句
在表达式后面加上一个分号“;
”就构成了表达式语句。表达式语句的一般形式如下:
表达式;

39
C 语言程序设计(第 2 版)

这里的表达式是指 C 语言中任何合法的 C 表达式。例如,


“count = 0;”
、“count++;”、
“x < y;”、
“x + y * 2;”都是 C 语言中合法的表达式语句。可以看出,分号是语句必要的组成部分。
表达式语句中最常用的是赋值表达式语句,通常简称为赋值语句。例如,上例中的“count =
0;”就是一个赋值表达式语句。

赋值表达式与赋值语句在形式上相似,在使用过程中一定要注意二者的区别。例如,
“y=x+5”是一个赋值表达式,而“y=x+5;”是一个赋值语句。

M提醒
分号“;”是 C 语句的重要组成部分,是语句的结束标志,任何 C 语句都以分号结束。

3.2.2 函数调用语句
函数是 C 程序的基本组成单位,一个函数的执行是通过在程序中调用这个函数来实现的,调
用函数的操作由 C 语句来完成,通常称为函数调用语句。例如,“printf("hello!");”就是一个函数
调用语句。关于函数的具体内容在后续章节中介绍,这里仅简单介绍实现输入输出操作的 C 库函数。
一个完整的计算机程序应具备输入输出功能,C 语言本身并不提供数据的输入输出语句,有
关的输入输出操作都是通过调用 C 标准库函数来实现的。C 语言提供的输入输出标准库函数有
getchar( )、putchar( )、scanf( )和 printf( )等。

引用 C 语言标准库函数时,必须用编译预处理命令“#include”将头文件“stdio.h”
包含到用户源程序中,在程序的开始写一行命令#include<stdio.h>或#include“stdio.h”。

1.字符输入函数 getchar( )
字符输入函数的一般形式如下:
int getchar()
功能:接收从终端输入的一个字符,并返回其 ASCII 码值。
例如:
int ch = getchar() ;/* 从输入终端(如键盘)接收一个字符并把它赋给 ch 变量。*/

M提醒
函数 getchar( )只能接收单个字符,因此尽管用户可通过终端输入多个字符,但该函数仅接
收第一个字符,返回该字符的 ASCII 码值,其他字符不予处理。

2.字符输出函数 putchar( )
字符输出函数的一般形式如下:
int putchar(char ch)
功能:向终端输出一个字符,并返回该字符的 ASCII 码值。
【例 3-1】 分析以下程序的运行结果,注意函数 getchar( )和 putchar( )的使用。
程序:
#include <stdio.h>
int main()

40
第3章 控制结构

{
/*定义 4 个变量用于接收输入的 4 个字符*/
char ch1, ch2, ch3, ch4 ;
ch1 = getchar( );
ch2 = getchar( );
ch3 = getchar( );
ch4 = getchar( );
putchar(ch1);
putchar('\n');
putchar(ch2);
putchar('\n');
putchar(ch3);
putchar('\n');
putchar(ch4);
return 0;
}
运行结果如下:

函数 putchar( )和 getchar( )只能处理单个字符。回车符也被看做一个普通字符,存储


在变量 ch4 中。

函数 putchar( )不仅可以使用字符变量和字符常量完成字符的输出,如例 3-1 中的语句


“putchar(ch1);”和“putchar('\n');”,这里,后一个语句输出了一个回车符,从而实现了换行的功能。
还可以直接使用 ASCII 码值输出对应的字符,如语句“putchar(65);”输出一个 ASCII 码值为 65
的字符“A”。

M提醒
例 3-1 体现了第 1 章提到的一种良好的程序书写风格:缩进,适当的缩进使程序结构清晰,
具有层次感,从而提高程序的可读性。

3.格式输出函数 printf( )
字符输出函数 putchar( )仅能输出单个字符,对多个字符进行输出处理时,要使用格式输出函
数 printf( ),并且格式输出函数还可用于输出任意数据类型的数据。
函数 printf( )的一般形式如下:
int printf(格式控制,输出列表)
功能:按照格式控制的要求,向终端输出列表中各个输出项的值。其中,格式控制用于指定
数据的输出格式,是由双撇号括起来的字符串,该字符串中除格式说明符外的所有字符原样输出;
输出列表列出了各个输出项。其中,格式控制中的格式说明符和输出列表中的输出项在数量和类
型上要一一对应。
例如:
printf("The cal is %d+%d=%f", num1,num2,sum);

41
C 语言程序设计(第 2 版)

这里%d 和%f 是格式说明符,上面的语句中共有 3 个格式说明符,分别对应输出列表中的 3


个变量。
C 语言中常用的格式说明符有 4 种,分别是整型数据的格式说明符、实型数据的格式说明符、
字符型数据的格式说明符和字符串数据的格式说明符。
(1)整型数据的格式说明符
① %d 以十进制形式输出一个整数。
② %u 以十进制形式输出一个无符号整数。
③ %o 以八进制形式输出一个整数。
④ %x 以十六进制形式输出一个整数。
⑤ %ld 以十进制形式输出一个长整数。
⑥ %md 以指定宽度输出一个整数。m 为指定的输出宽度(包括符号位)
,如果数据的实际位
数小于 m,则左端补空格;如果大于 m,则按照实际位数输出。
⑦ %-md 以指定宽度输出一个整数。m 为指定的输出宽度(包括符号位)
,如果数据的实际
位数小于 m,则右端补空格;如果大于 m,则按照实际位数输出。
【例 3-2】 分析以下程序的运行结果,注意格式说明符的使用。
程序:
#include <stdio.h>
int main()
{
int a = 15;
printf("%d,%o,%x\n", a, a, a);
printf("%5d\n", a);
return 0;
}
运行结果如下:

第 1 个 printf( )函数分别采用十进制、八进制和十六进制 3 种形式输出整数 a 的值,


得到的结果分别为“15, 17, f ”,在第 2 个 printf( )函数中,通过%5d 指定以 5 个字符的
宽度输出整数 a 的值,而 a 的值为 15,仅占两个字符的位置,所以输出时整数 15 的前
面有 3 个空格。

M提醒
在使用 printf( )函数的时候,在各个格式说明符之间最好加入分隔符,如例 3-2 使用逗号作
为分隔符,否则输出的数据紧挨在一起,难以区分。

(2)实型数据的格式说明符
① %f 输出一个单精度实数,该实数的整数部分按实际位数输出,小数部分保留 6 位。如果
输出一个双精度实数,则格式说明符为%lf,即在字符“f”之前加上字符“l”

② %m.nf 指定以 m 个字符的宽度输出一个实数,其中小数位数为 n 位。如果数据的实际位
数小于 m,则左端补空格;如果大于 m,则按照实际位数输出。小数位数小于 n,则右端补 0;

42
第3章 控制结构

小数位数大于 n,则采用四舍五入方式进行处理。
③ −%m.nf 与%m.nf 基本相同,不同的是输出的数据是向左靠拢,右端补空格。
④ %e 以指数形式输出一个实数。
需要强调的是,由于实数都有有效数字位数的限制,当输出的实数的位数超出了允许的有效
数字范围,输出的实数可能与原数值不同。
【例 3-3】 分析以下程序的运行结果,注意格式说明符的使用。
程序:
#include <stdio.h>
int main()
{
float x;
x = 12345.129871;
printf("%f\n", x);
printf("%11.8f\n", x);
printf("%11.2f\n", x);
return 0;
}
运行结果如下:

由于输出的实数为 float 型,有效数字为 7 位,因此,输出的实数中,仅有 7


位数字是有效的,并不是所有打印出来的都是有效数字。对于第 3 条输出语句,小
数位数设置为 2,因此,对实数 12345.129871 进行四舍五入后输出,输出结果为
12345.13。

M提醒
由于 float 型数据精度的限制,在实际编程时,最好将实数定义为 double 型。

(3)字符型数据的格式说明符
输出字符型数据的格式说明符为%c,表示输出一个字符。
【例 3-4】 分析以下程序的运行结果,注意格式说明符的使用。
程序:
#include <stdio.h>
int main()
{
char c='A';
int a = 65;
printf("%c\n", c);
printf("%c\n", a);
printf("%d\n", a);
return 0;
}

43
C 语言程序设计(第 2 版)

运行结果如下:

当格式说明符为%c 时,整型变量 a 的值 65 被看做相应字符的 ASCII 码值,因此输


出对应的字符“A”。

(4)字符串数据的格式说明符
输出字符串的格式说明符为%s,表示输出一个字符串。
【例 3-5】 分析以下程序的运行结果,注意格式说明符的使用。
程序:
#include <stdio.h>
int main()
{
printf("%s","Welcome to China!\n");
return 0;
}
运行结果如下:

4.格式输入函数 scanf( )
函数 scanf( )的一般形式如下:
int scanf(格式控制,地址列表)
功能:按照格式控制的要求,将从终端输入的数据赋值给地址列表中的各个变量。格式控制
的含义和函数 printf( )中的格式控制类似,地址列表列出了各变量的地址,由取地址运算符“&”
后跟变量名组成。
【例 3-6】 分析以下程序的执行结果,注意函数 scanf( )的使用。
程序:
#include <stdio.h>
int main()
{
int a, b, temp;
printf("Please input a b:\n");
scanf("%d %d", &a, &b);
temp = b;
b = a;
a = temp;
printf("a = %d,b = %d\n", a, b);
return 0;
}
运行结果如下:

44
第3章 控制结构

scanf( )函数中的两个格式说明符%d 之间是一个空格符,因此,在输入 a 和 b 的值


时用空格作为间隔符,当用户输入 2 和 3 并按回车键结束输入过程后,变量 a 和 b 分别
被赋值为 2 和 3。从程序第 7 行开始的 3 条语句“temp = b; b = a; a = temp;”是典型的交
换两个变量值的方法,因此,第 10 行使用 printf( )函数输出变量 a 和 b 的值时,结果为
“a = 3, b = 2”

需要强调的是,如果两个格式说明之间没有分隔符,如“%d%d”
,那么在输入的时候要注意
用空格、回车或 Tab 键作为输入数据之间的间隔。例如,对于输入语句“scanf("%d%d%d", &a, &b,
&c);”,下面的输入方式:
① 1 2 3↙
② 1↙
2↙
3↙
③1(按 Tab 键)2↙
3
都是正确的。
如果格式控制中出现了除格式说明符外的其他字符,则在进行数据输入的时候也要在对应的
位置输入该字符。
例如:
scanf("%d,%d,%d", &a, &b, &c); /* 字符“,”作为间隔符,输入形式为:5,6,7↙ */
scanf("a = %d,b = %c", &a, &b); /* 输入形式为:a = 5,b = 7↙*/
如果不采用上面的输入形式,虽然不会出现编译错误,但会出现错误的运行结果。

scanf( )函数要求给出变量的地址,一般情况下取地址运算符“&”不能省略。

M提醒
(1)当地址列表中的变量是 double 型时,应使用格式说明符%lf。
(2)当采用格式说明符%s 输入字符串时,系统以空格符作为输入结束的标志,因此如果
用户输入的字符串中含有空格,则只接收第一个空格前的字符,并在之后加'\0'。。

3.2.3 空语句
仅由分号“;
”组成的语句称为空语句。空语句是不执行任何操作的语句。
例如:
while(getchar( ) !='\n')
{
;
}
空语句通常起到占位的作用,在程序没有完全开发完成前,可用空语句占位,以便后续开发
填充代码。

45
C 语言程序设计(第 2 版)

3.2.4 复合语句
复合语句是用一对花括号“{}”把多条语句括起来形成的语句块,语法上视为一条语句。
复合语句的一般形式如下:
{
语句 1

语句 n
}
复合语句可以放在能够使用语句的任何地方,它建立一个新的作用域或块。复合语句是 C 语
言中唯一不用分号“;
”结尾的语句。它在选择结构、循环结构中使用十分广泛。

3.3 选择结构程序设计
在实际应用中,并不是所有问题的解决都是通过顺序方式进行的,有时需要通过对特定条件
进行判断来选择相应的处理方式。例如:如果今天下雨,出门就打伞;否则不打伞。C 语言中选
择结构用来实现上述处理功能,主要有两种语句:if 语句和 switch 语句。

3.3.1 if 语句
if 语句包括基本 if 语句、双分支 if 语句、多分支 if 语句和嵌套 if 语句 4 种形式。
1.基本的 if 语句
该语句的一般形式如下:
if(表达式)
语句
功能:如果表达式的值为真,则执行其后面的语句,否则不执行该语句。
需要强调的是,if 语句中的“语句”是任何合法的 C 语句,可以是表达式语句、函数调用语
句、空语句、复合语句等。例如,下面的 if 语句
if(x > y) printf("%d", x);
表示,如果 x>y,则执行函数调用语句“printf("%d", x);”,否则什么也不执行。
对于上例,如果 x>y 成立,除了要输出变量 x 外,还要输出一个字符串“x>y”,则有
if(x > y)
{
printf("%d", x);
printf("x>y");
}

M提醒
如果 if 语句中的“语句”是一个复合语句,一定不要忘记加花括号“{}”,否则,系统仅
把这个复合语句的第 1 条语句作为 if 语句中的“语句”部分,从而造成错误。

if 语句中的“表达式”是任何合法的 C 表达式,例如:
if(x) printf("%d", x);

46
第3章 控制结构

也是正确的 if 语句,表示当变量 x 为非 0 值时,表达式“x”的值为真,执行后面的语句。


2.双分支 if 语句
该语句的一般形式如下:
if(表达式)
语句 1
else
语句 2
功能:如果表达式的值为真,则执行语句 1,否则执行语句 2。
例如,下面的 if 语句
if(x > y)
printf("%d", x);
else
printf("%d", y);
表示,如果 x > y,则输出变量 x 的值,否则输出变量 y 的值。该语句实现了输出两个数中较大者
的功能。
【例 3-7】 从键盘输入 3 个整数,找出其中最小的数并输出。
分析:
解决此问题需要分 3 步。第 1 步,接收从键盘输入的 3 个数,并用 3 个变量来保存。第 2 步,
从这 3 个变量中找出最小的数。找最小数的方法可以先比较前两个数,找到这两个数中最小的,
再拿最小的与第 3 个数比较。第 3 步,输出找到的最小的数。
算法描述:
(1)定义 3 个变量 a、b、c,接收从键盘输入的 3 个数;
(2)定义变量 min,用来保存最小数;
(3)如果 a < b,则 min←a,否则 min←b;
(4)如果 c < min,则 min←c;
(5)输出 min。
程序:
#include <stdio.h>
int main( )
{
int a, b, c, min;
printf("Please input a,b,c: ");
scanf("%d%d%d", &a, &b, &c);
if(a < b)
min = a;
else
min = b;
if(c < min)
min = c;
printf("The result is %d\n", min);
return 0;
}
运行结果如下:

47
C 语言程序设计(第 2 版)

3.多分支 if 语句
如果出现大于 2 种选择的情况,可采用多分支 if 语句形式。
该语句的一般形式如下:
if(表达式 1)
语句 1
else if(表达式 2)
语句 2
else if(表达式 3)
语句 3
...
else if(表达式 n)
语句 n
else
语句 n+1
功能:依次判断表达式的值,当出现某个表达式的值为真时,则执行其对应的语句;如果所
有的表达式均为假,则执行 else 后的语句,即语句 n+1。
【例 3-8】 输入一个百分制成绩,要求输出对应的成绩等级。百分制成绩与成绩等级的对照
关系如下:
90 分~100 分:等级 A;80 分~89 分:等级 B;70 分~79 分:等级 C;
60 分~69 分:等级 D;0 分~59 分:等级 E
程序:
#include <stdio.h>
int main()
{
float score;
char grade;
printf("please input the score:\n");
scanf("%f", &score);
if(score >= 90 && score <= 100)
grade = 'A';
else if(score >= 80 && score <= 89)
grade = 'B';
else if(score >= 70 && score <= 79)
grade = 'C';
else if(score >= 60 && score <= 69)
grade = 'D';
else if(score >= 0 && score <= 59)
grade = 'E';
else
{
printf("your score is wrong! ");
grade = '0';
}
printf("score = %.1f,grade = %c", score, grade);
printf("\n")
return 0;
}
运行结果如下:

48
第3章 控制结构

当从键盘上输入 86 按回车后,变量 score 被赋值为 86,因此,表达式“score >= 80


&& score <= 89”成立,变量 grade 被赋值为'B'。

4.嵌套的 if 语句
if 语句中的“语句”可以是任何合法的 C 语句,如果这个语句又是一个 if 语句,这种形式叫
做嵌套的 if 语句。
例如,下面的语句段:
if(x > y)
if(z > 0)
printf("%d\n",x);
else
printf("%d\n",y);
else
if(x == y)
printf("x is equal to y\n");
else
printf("x is smaller than y\n");
就是一个具有两层嵌套的 if 语句。
在使用嵌套的 if 语句时一定要注意 if 和 else 的配对,尤其是嵌套的 if 语句又是 if-else 型,这
时就会出现多个 if 和多个 else 重复的情形。为了避免出现二义性,C 语言规定,else 总是与它前面
最近的尚未配对的 if 配对。
例如,下面的语句段:
if(表达式 1)
if(表达式 2)
语句 1
else
语句 2
的实际意义与程序编写者的想法是不同的。从程序编写者的想法看,第 1 个 else 应该与第 1 个 if
对应,但事实上 else 是与第 2 个 if 配对的,因为它们相距最近。这样就会造成程序的混乱,导致
出错。因此在使用时,应通过加花括号的方法来确定配对关系。
可以把上面的例子改成如下形式:
if(表达式 1)
{
if(表达式 2)
语句 1
}
else
语句 2
这样,
“{}”限定了嵌套的 if 语句的范围,因此 else 与第 1 个 if 对应。

3.3.2 switch 语句
例 3-8 采用多分支 if 语句来实现成绩的等级分类,使程序变得复杂冗长,降低了程序的可读
性。C 语言提供 switch 语句专门处理多分支的情形,使程序变得简洁易懂。

49
C 语言程序设计(第 2 版)

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

case 常量表达式 n: 语句块 n
default: 语句块 n + 1
}
功能:首先计算“表达式”的值,然后依次与“常量表达式”的值进行比较,当“表达式”
的值与某一个“常量表达式”的值相等时,执行该 case 子句后面的语句块,并继续执行其后的所
有 case 子句直到程序结束。如果“表达式”的值与所有“常量表达式”的值都不相等,则执行 default
后面的语句。
【例 3-9】 输入年份和月份,打印输出该年该月有多少天。
分析:
除 2 月份外,一年中的其他月份总是满足“大月 31 天,小月 30 天”的规则,因此,根据输
入的月份是大月(1,3,5,7,8,10,12)还是小月(4,6,9,11)就可以确定该月的具体天
数。2 月份的天数与该年是平年还是闰年有关,该年为闰年则 2 月为 29 天,否则为 28 天,因此
需要判断该年是否是闰年。闰年的判断规则是,如果能被 400 整除或者能被 4 整除但不能被 100
整除,则该年为闰年,否则为平年。为了程序的完备性,还应考虑输入的月份值超出 1~12 范围
的情况,此时应该给出相应的提示信息。
算法描述:
(1)定义变量 iYear、iMonth 和 iDay,用来存储年份、月份和天数;
(2)定义变量 leap,用来标识 iYear 是否是闰年,若 iYear 是闰年,则 leap←1,否则 leap←0;
(3)如果 iMonth 为大月,则天数 iDay←31;如果 iMonth 为小月,则 iDay←30;
如果 iMonth 为 2 月&& iYear 为闰年,则 iDay←29,否则 iDay←28;
(4)输出 iDay。
程序:
#include <stdio.h>
int main()
{
int iYear, iMonth, iDay;
int leap;
printf("please input the year number:");
scanf("%d", &iYear);
printf("please input the month number:");
scanf("%d", &iMonth);
if((iYear%400 == 0)||(iYear%4 == 0 && iYear%100 != 0))
leap = 1;
else
leap = 0;
switch(iMonth)
{
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:

50
第3章 控制结构

case 12: iDay = 31; break;


case 4:
case 6:
case 9:
case 11: iDay = 30; break;
case 2: if(leap==0) iDay = 28;
else iDay = 29;
break;
default: iDay = −1;
}
if(iDay == −1)
printf("Invalid month input!\n");
else
printf("%d.%d has %d days.\n", iYear, iMonth, iDay);
return 0;
}
运行结果如下:

例 3-9 中出现了 break 语句,C 语言中,可以利用 break 语句中止该语句下面的所有 case 子句


和 default 子句的执行,直接跳出 switch 语句。这种用法在实际编程中比较常见。break 语句的具
体用法参见 3.5 节。
下面用带有 break 的 switch 语句重新解决例 3-8 的问题,注意程序结构与可读性的变化。
#include <stdio.h>
int main()
{
float score;
int temp;
char grade;
printf("please input the score:\n");
scanf("%f", &score);
temp = (int) score / 10;
switch(temp)
{
case 10:
case 9: grade = 'A'; break;
case 8: grade = 'B'; break;
case 7: grade = 'C'; break;
case 6: grade = 'D'; break;
default: grade = 'E';
}
printf("score = %.1f,grade = %c\n", score, grade);
return 0;
}
运行结果如下:

51
C 语言程序设计(第 2 版)

上例中,通过语句“temp = (int) score / 10;”将合法的分数值 score 转换为 0~10 范围内的整


数,作为 switch 语句中“表达式”的值,然后依次与 case 子句中的“常量表达式”的值进行匹配,
而不是直接用实型变量 score。ANSI C 标准规定,switch 后面括号内的表达式的值可以是任何类
型,但一般情况下是整型或字符型,而 case 后面的常量表达式的值必须是整型、字符型或者枚举
类型。

case 子句中各常量表达式的值不能相同,否则会出现错误。

需要强调的是,在编程时,通常总是将 case 子句放在 default 子句的前面,实际上,C 语言对


此没有限制,case 子句和 default 子句的出现顺序并不影响程序的执行结果,可以先出现 default
子句,再出现 case 子句。

M提醒
在 switch 语句中,每个 case 子句中的语句块都可以由多条语句组成,但并不需要用花括号
“{}”括起来。

特别提示:
C89 规定,编译器必须支持至少 257 个 case 子句,而 C99 规定,编译器至少支持 1023 个 case
子句。

3.4 循环结构程序设计
C 语言提供 3 种循环语句,分别是 while 语句、do-while 语句和 for 语句,利用这些语句可以
实现不同形式的循环结构。循环语句通常都要有终止条件,根据终止条件判断的时机不同,将循
环语句分为两种类型,分别称为“当型”循环和“直到型”循环。
“当型”循环是指首先判断条件
是否满足,如果满足才进入循环,否则什么都不做,取“当条件成立才执行”之意。相应地,
“直
到型”循环是指首先进入循环,然后再判断条件是否满足,取“一直执行直到条件不成立为止”
之意。

3.4.1 while 语句
while 语句是用来实现“当型”循环的循环语句。
while 语句的一般形式如下:
while(表达式)
循环体语句
功能:先计算表达式的值,如果为真,则执行循环体语句,当循环体语句全部执行完一次后,
再次计算表达式的值,如果为真,再次执行循环体语句,如此反复,直到表达式的值为假,则不
再执行循环体语句,退出循环,转去执行 while 语句后面的语句。
while 语句的特点是:先判断,后执行。如果第 1 次计算表达式的值就为假,那么循环体语句
一次也不执行。

52
第3章 控制结构

【例 3-10】 用 while 循环求 1~100 的和。


分析:
计算 1~100 的和,即计算 1 + 2 + 3 + L + n ,通常的做法是,先计算 1+2 得到结果 3,再用
这个结果和后一个数 3 相加,即 3+3 得到结果 6,如此反复下去,直到加到 100 为止。除第 1
次加法外,后面的加法运算都是用前一次相加的结果和后一个数相加,这就是本题的规律。如
果对第 1 次加法稍微做一点儿改变,也就是把“1+2=3”变为“0+1=1”和“1+2=3”,那么求 1~
100 的和就变成了“每次都用前一次相加的结果和后一个数相加”的过程,这里,最初的结果
设为 0。
这样,可以定义一个变量 sum 保存每次相加的结果,初值为 0,利用循环不断在 sum 上累加,
即 sum←sum+i,变量 i 从 1 开始,每次增 1,直到 100。这时的 sum 常被形象地称为累加器。
算法描述:
(1)定义累加器变量 sum,sum←0;
(2)定义变量 i,i←1;
(3)如果 i <= 100,则循环执行以下语句
sum←sum + i,实现累加功能;
i ++;
(4)输出 sum 的值。
程序:
#include <stdio.h>
int main()
{
int sum = 0 , i = 1;
while(i <= 100)
{
sum = sum + i;
i ++;
}
printf("sum = %d\n", sum);
return 0;
}
运行结果如下:

while 语句中表达式的值通常要随着循环体语句的执行而发生变化,使得该表达式
的值在某一时刻为假,从而使循环正常结束。如果表达式的值永远为真,循环可能将无
限进行下去,成为死循环。

【例 3-11】 用 while 循环求 n! 的值。


分析:
计算 n!,即计算 1 × 2 × 3 × L × n ,与例 3-10 中计算 1 + 2 + 3 + L × n 具有相似的规律,不同之
处仅是把重复做加法变成了重复做乘法,随之而来的变化就是定义一个保存相乘结果的变量
multi,初值应该为 1,而不是 0。这时的 multi 常被形象地称为累乘器。

53
C 语言程序设计(第 2 版)

算法描述:
(1)定义变量 n,接受用户的输入;
(2)定义累乘器变量 multi,multi←1;
(3)定义变量 i,i←1;
(4)如果 n < 0,则给出相应的提示信息;
否则,如果 n = 0,则直接输出变量 multi 的值 1;
否则,执行第(5)~(6)步。
(5)如果 i <= n,则循环执行以下语句
multi←multi * i,实现累加功能;
i ++;
(6)输出 multi 的值。
程序:
#include <stdio.h>
int main()
{
int n, i = 1;
long int multi = 1;
printf("Please input n(n >= 0): ");
scanf("%d", &n);
if(n < 0)
printf("invalid input! ");
else if(n == 0)
printf("%d! = %ld\n", n, multi);
else
{
while(i <= n)
{
multi = multi *i;
i++;
}
printf("%d! = %ld\n", n, multi);
}
return 0;
}
运行结果如下:

3.4.2 do-while 语句
do-while 语句是用来实现“直到型”循环的循环语句。
do-while 语句的一般形式如下:
do
循环体语句
while(表达式);
功能:先执行一次循环体语句,然后计算表达式的值,如果为真,则继续执行循环体语句,
然后再次计算表达式的值,如此反复,直到表达式的值为假,则不再执行循环体语句,退出循环,

54
第3章 控制结构

转去执行 do-while 语句后面的语句。


【例 3-12】 编写程序,找出满足条件 1+2+3+…+n<500 的最大的 n 值。
分析:
求解本题的基本方法是:定义一个累加器 sum,然后从自然数 1 开始,依次进行累加,直到
与自然数 n 相加后,累加器 sum 的值大于 500,则 n 的前一个自然数 n−1 即为所求。
算法描述:
(1)定义变量 n,n←0;定义累加器变量 sum,sum←0;
(2)循环执行下面的语句,直到 sum >= 500 为止
n++;
sum←sum + n;
(3)输出 n – 1 的值。
程序:
#include <stdio.h>
int main()
{
int n = 0, sum = 0;
do
{
n++;
sum = sum + n;
}while(sum < 500);
printf("s = %d\n", n-1);
return 0;
}
运行结果如下:

3.4.3 for 语句
C 语言提供了另外一种循环语句:for 语句,与 while 语句和 do-while 语句相比,for 语句使
用更方便,也更灵活,是最复杂的循环语句。
for 语句的一般形式如下:
for(表达式 1; 表达式 2; 表达式 3)
循环体语句
功能:先计算表达式 1 的值,再计算表达式 2 的值,如果为真,则执行循环体语句,循环体语句
执行一次后,计算表达式 3 的值,然后再次计算表达式 2 的值,如果为真,则继续执行循环体语句,
如此反复,直到表达式 2 的值为假,不再执行循环体语句,退出循环,转去执行 for 语句下面的语句。
需要指出的是,在 for 语句中,表达式 1 的值仅计算一次,表达式 2 的值不断地被计算,并
作为判断是否结束循环的条件,循环体语句每执行一次,表达式 3 的值就会被计算一次。因此,
通常情况下,如果表达式 3 的值的变化对表达式 2 的值有影响,使表达式 2 的值在某一时刻为假,
则循环就能够正常结束。
下面给出这种应用最广泛、也最容易理解的 for 语句的一般形式:
for(循环变量赋初值; 循环条件; 循环变量值改变)

55
C 语言程序设计(第 2 版)

循环体语句
这里,将控制循环次数的变量称为循环变量。
使用 for 语句完成例 3-10 的功能,计算 1~100 的和,可采用如下的语句段:
for(i = 1; i <= 100; i++)
sum = sum + i;
【例 3-13】 计算自然数 1~n 的平方和。
分析:
采用 for 语句实现求解自然数的平方和问题,需要定义循环变量 i,初值为 1,循环条件为 i <=
n,循环变量 i 每次增 1。然后定义一个累加器 sum,对 i 的平方即 i*i 实现累加即可。
算法描述:
(1)接收用户输入的 n 值,并进行有效性检验;
(2)定义循环变量 i;定义累加器变量 sum,sum←0;
(3)循环变量 i←1,循环条件 i <= n,循环变量 i++,循环执行以下语句
sum←sum + i * i;
(4)输出 sum 的值。
程序:
#include <stdio.h>
int main()
{
int i, n;
long int sum = 0;
printf("Please input n(n >= 1): ");
scanf("%d", &n);
if(n < 1)
{
printf("Invalid input!");
}
else
{
for(i = 1; i <= n; i++)
{
sum = sum + i * i;
}
printf("The result is:%ld\n", sum);
}
return 0;
}
运行结果如下:

for 语句中的各表达式都可以省略,但分号间隔符不能少。例如,for( ; ;)。编程时,


如果表达式 1 省略,应该在 for 语句之前给循环变量赋初值;如果表达式 2 或者表达式 3
省略,应该在循环体内设法结束循环。否则,将造成死循环。

在 for 语句中,表达式 1 和表达式 3 都可以是逗号表达式。

56
第3章 控制结构

例如:
for(i = 1, j = n ; i < j ; i++, j−−)
printf("Welcome to China! ")
表达式 1 同时为变量 i 和 j 赋初值,表达式 3 同时改变 i 和 j 的值。

M提醒
for 语句中的 3 个表达式可以是任意合法的 C 表达式,利用 for 语句的这个特点,可以编写
出更加简明易懂的 C 程序,但是,也要避免在 for 语句中使用过分复杂的表达式,导致程序的
可读性变差,因而这不是一个良好的编程习惯。

用 for 循环能够解决的问题,往往也可以用 while 循环或 do-while 循环解决,对于一个实际


问题,应该使用哪种循环语句并没有一个绝对的标准,在很大程度上取决于个人喜好。但是在实
际应用中,使用哪种语句会更自然更方便,也有一般性的规律。
① 如果循环次数在执行循环体之前就已确定,一般使用 for 语句;如果循环次数是由循环体
的执行情况确定的,一般使用 while 语句或者 do-while 语句。
② 当循环次数未知,循环体至少执行一次时,用 do-while 语句;反之,如果循环体可能一
次也不执行,选用 while 语句。
思考一下,对于例 3-12 的问题,采用 for 语句如何实现程序功能?

3.4.4 循环的嵌套
对于一个循环语句,如果其中的循环体语句中又包含了一个循环语句,就形成了循环的嵌套。
【例 3-14】 打印 n 行 m 列的星形矩阵。假设 n=4,m=5,打印出来的图形如下:
*****
*****
*****
*****
分析:
图形共有 n 行 m 列,对于每一行,都要依次打印 m 个星号*。因此对于一行星号*的打印,
可以采用一个循环语句(本题选用 for 语句)来实现。由于一行星号*打印完成后,下一行还是从
屏幕的第 1 个字符的位置开始打印,因此在这个循环语句的下面要输出一个换行符‘\n’。本题要
求输出 n 行,这就要将前面的过程重复 n 次,因此把前面的过程作为另外一个循环的循环体语
句,就完成了星形矩阵图形的输出。通常将这两个循环语句分别称为内层循环和外层循环,内层
循环控制输出的列数,外层循环控制输出的行数。这样外层循环执行一次,就输出一行。
算法描述:
(1)接收用户输入的 n 和 m 的值;
(2)定义外层循环变量 i 和内层循环变量 j;
(3)循环变量赋初值 i←1,循环条件 i <= n,循环变量 i++,循环执行以下语句
1)循环变量赋初值 j←1,循环条件 j <= m,循环变量 j++,循环执行以下语句
输出“*”;
2)输出“\n”。
程序:
#include <stdio.h>

57
C 语言程序设计(第 2 版)

int main()
{
int n, m ;
int i, j ;
printf("Please input n(n>=1): ");
scanf("%d",&n);
printf("Please input m(m>=1): ");
scanf("%d",&m);
if(n < 1 || m < 1)
printf("Invalid input!");
else
for(i = 0; i < 4; i++) /* 控制行 */
{
for(j = 0; j < 5; j++) /* 控制列 */
printf("*");
printf("\n"); /* 换行 */
}
return 0;
}
运行结果如下:

需要指出的是,循环的嵌套可以有多层,而且 3 种循环语句 while、do-while 和 for 可以互相


嵌套自由组合,从而实现更复杂的程序功能。
【例 3-15】 Marry 有 5 本新书,A、B、C 三位同学都想借,Marry 说:
“每人只能借 1 本书”

请给出所有可能的借书方案。
分析:
这是一个非常典型的利用穷举法解决的问题。穷举法是一种最直接、实现最简单的解决实际
问题的算法思想,但这种算法非常耗费时间,运行效率十分低下。然而,随着 CPU 运算速度的不
断提高,以及多处理器并行计算技术的发展,穷举法也不失为一种很好的选择。
穷举法的基本思想是:在可能的解空间中穷举出所有可能的解,并对每一个可能的解进行判
断,从中得到问题的答案。使用穷举法思想解决实际问题,最关键的步骤是划定问题的解空间,
并在该解空间中逐一枚举每一个可能的解。因此,使用该算法必须注意两个问题:一是解空间的
划定必须保证覆盖问题的全部解,二是解空间集合及问题的解集一定是离散的集合,也就是集合
中的元素是可列的、有限的。
对于本题,假设将 Marry 的 5 本新书分别编号为 1~5,每个同学可能借到的书的编号就限定
在[1,5]的范围内,因此,问题的解空间可描述为
{(x1 , x 2 , x 3 ) |1 ≤ x i ≤ 5, 且x i为整数}

由于每个人借到的书是不能重复的,只要 A、B、C 本人借到的书的编号不相同,就是一种


合理的借书方案,因此,在上述解空间中,只要满足下面的条件即可。
x1 ≠ x 2 ≠ x 3

58
第3章 控制结构

算法描述:
(1)定义循环变量 i,j,k 为 int 型;
(2)使用三重 for 循环构建问题的解空间,并进行条件判断。
第 1 层循环:循环变量 i←0,循环条件 i < 5,循环变量 i++,循环执行以下语句
第 2 层循环:循环变量 j←0,循环条件 j < 5,循环变量 j++,循环执行以下语句
第 3 层循环:循环变量 k←0,循环条件 k<5,循环变量 k++,循环执行以下语句
进行条件判断:如果 i ≠ j ≠ k ,则得到一种借书方案。
程序:
#include<stdio.h>
#define N 5
int main()
{
int i, j, k;
printf("There are different methods for Marry to distribute his books to A,B,C\n");
for(i = 1; i <= N; i++)
for(j = 1; j <= N; j++)
for(k = 1; k <= N; k++)
if(i!=j && j!=k && i!=k)
printf("(%d,%d,%d) ", i, j, k);
printf("\n");
return 0;
}
运行结果如下:

3.5 转移控制语句
在循环语句的执行过程中,当循环条件不满足时,可以正常退出循环,执行循环体后面的语
句。但是在实际应用中,可能会存在这样的情况,就是在循环条件仍满足的情况下终止整个循环
或者终止本次循环,这时就要用到转移控制语句:break 语句或 continue 语句。

3.5.1 break 语句
break 语句除了可以终止循环语句外,还可以终止 switch 语句。
break 语句的一般形式如下:
break ;
功能:跳出当前的 switch 语句或循环语句,转去执行当前 switch 语句或循环语句后面的
语句。
在 switch 语句中,如果在 case 子句或 default 子句中使用了 break 语句,则执行完当前子句后,

59
C 语言程序设计(第 2 版)

不再去执行后续的子句,直接跳出 switch 语句。因此,使用 break 语句可以使 switch 语句有多个


出口,在一些场合下使编程更加灵活、方便。
下面举例说明 break 语句在循环语句中的应用。
【例 3-16】 在半径为 1~10 的圆中,输出圆的面积不超过 100 的圆的半径和面积。
分析:
求解本题,可以将半径作为循环变量 r,初值为 1,循环条件为 r <= 10,在循环体中计算圆
的面积 s = π*r*r。如果 s <= 100,则输出 r 和 s,否则终止循环。
在圆的面积的计算公式中,用到了一个常量 π,因此在编程时,定义一个常量并将其命名为
PI,用于保存 π 的值,提高程序的可读性和可维护性。
算法描述:
(1)定义常量 PI,值为 3.14;
(2)定义循环变量 r 和存储圆的面积值的变量 s;
(3)循环变量赋初值 r←1,循环条件 r <= 10,循环变量 r++,循环执行以下语句。
计算圆的面积 s = PI * r * r;
如果 s <= 100,则输出 r 和 s 的值,否则跳出循环。
程序:
#include <stdio.h>
#define PI 3.14
int main()
{
int r;
float s;
for(r = 1; r <= 10; r++)
{
s = PI*r*r;
if(s <= 100.0)
printf("r = %d, s = %f\n", r,s);
else
break;
}
return 0;
}
运行结果如下:

M提醒
(1)break 语句只能用在 switch 语句和循环语句中。
(2)break 语句仅能退出当前的 switch 语句或循环语句,在循环嵌套的情况下,控制会转
向当前循环的外层循环,而不是退出最外层循环。

思考一下,如果不用 break 语句,怎样实现例 3-15 的功能?程序的可读性如何?

60
第3章 控制结构

3.5.2 continue 语句
continue 语句的一般形式如下:
continue ;
功能:结束本次循环,即跳过循环体中 continue 语句后面的语句,继续下一次循环。由于
continue 语句的这种特点,通常也称之为短路语句。
continue 语句和 break 语句的区别是:continue 语句只结束本次循环,并不终止整个循环的执
行;而 break 语句则是结束整个循环过程,不再判断循环条件是否成立。
【例 3-17】 在半径为 1~10 的圆中,输出圆的面积超过 100 的圆的半径和面积。
分析:
与例 3-16 相比,差别仅在于,本题输出的半径与面积是面积值超过 100 的部分,因此在循环
体中,当面积值不超过 100 时,应结束本次循环不予输出,进入下一次循环即可。
算法描述:
(1)定义常量 PI,值为 3.14;
(2)定义循环变量 r 和存储圆的面积值的变量 s;
(3)循环变量赋初值 r←1,循环条件 r <= 10,循环变量 r++,循环执行以下语句
1)计算圆的面积 s = PI * r * r;
2)如果 s <= 100,则执行 continue 语句,结束本次循环;
3)输出 r 和 s 的值。
程序:
#include<stdio.h>
#define PI 3.14
int main()
{
int r;
float s;
for(r = 1; r <= 10; r++)
{
s = PI * r * r;
if(s < 100.0) continue;
printf("r = %d, s = %f\n", r , s);
}
return 0;
}
运行结果如下:

M提醒
continue 语句只能用于循环语句中。

61
C 语言程序设计(第 2 版)

3.5.3 goto 语句
break 语句和 continue 语句都能改变程序的控制流程,但控制转向的位置是有限制的。break
语句只能转向 switch 语句或循环语句的下一条语句,continue 语句只能转向循环开始处,从而使
程序具有模块化结构,因此可以把这两种语句称为有条件的转向语句。如果要实现无条件转向功
能可以用 goto 语句。
goto 语句的一般形式如下:
goto <语句标号> ;
功能:程序流程无条件转向“语句标号”标识的语句。其中,goto 是关键字,语句标号是合
法的 C 标识符,用来标识 goto 语句所要转到的 C 语句。
如果程序中使用了 goto 语句,则在程序中一定要有一条 C 语句,它的前面有语句标号,该语
句标号与 goto 语句中的语句标号一致。具有语句标号的 C 语句的一般形式如下:
<语句标号> : <语句>
goto 语句一种常见的用法是与 if 语句配合使用。
【例 3-18】 计算 1~100 的和,要求使用 goto 语句。
程序:
#include<stdio.h>
int main()
{
int i = 1,sum = 0;
loop: if(i <= 100) /*添加语句标号 loop*/
{
sum = sum + i;
i++;
goto loop; /*程序转到 loop 语句标号处执行*/
}
printf("%d\n",sum);
return 0;
}
运行结果如下:

程序第 4 行 if 语句前设置了语句标号 loop,如果 if 条件成立,程序执行到第 7 行后


会通过 goto 语句转到第 4 行,然后再次判断 if 语句条件是否成立,从而实现了循环的
功能。

需要强调的是,由于 goto 语句可以实现程序控制的随意转移,违背结构程序设计的思想,从


而破坏程序的模块化结构,因此,通常建议在 C 程序中尽量不采用 goto 语句。但是,并不是不能
使用 goto 语句,在某些特殊情况下,使用 goto 语句还会提高程序的可读性。
【例 3-19】 分析以下程序的运行结果,注意 goto 语句的使用。
程序:
#include<stdio.h>
int main()

62
第3章 控制结构

{
int i ,j , k , order = 0 ;
for(i = 0; i < 2; i++) /*添加语句标号 loop*/
for(j = 0; j < 3; j++)
for(k = 0; k < 4; k++)
{
order++;
if( i + j + k == 3)
goto disp;
}
disp:
printf("%d\n",order);
return 0;
}
运行结果如下:

使用 goto 语句实现从多重循环中直接退出,执行循环体外的语句。如果使用 break


语句来实现,需要在每层循环中都使用 break 语句,并且还需要设置一个标志变量用以
识别是否是执行 break 语句退出该层循环的。这样,不仅程序的执行效率要低,而且可
读性也会变差。

3.6 综 合 实 例
【例 3-20】 输入一行字符,分别统计出英文字母、空格、数字和其他字符的个数。
分析:
求解本题,就是要分别计算出四类字符(字母、空格、数字、其他字符)的个数,因此,用
户每输入一个字符,就应该判断该字符所属的类别。如果该字符不是回车符,则在相应类别的累
加器上进行加 1 操作。
算法描述:
(1)定义 4 个累加器 letters、spaces、digits 和 others,初值为 0;
(2)定义变量 c 接收用户输入的字符;
(3)如果 c != ' \n',则循环执行以下语句
如果 c 是字母,则 letters++;
否则 如果 c 是空格,则 spaces++;
否则 如果 c 是数字,则 digits++;
否则 others++。
(4)输出 letters、spaces、digits 和 others 的值。
程序:
#include <stdio.h>
int main()

63
C 语言程序设计(第 2 版)

{
char c;
int letters = 0, spaces = 0, digits = 0, others = 0;
printf("Please input some characters:\n");
while((c = getchar( )) != '\n')
{
if(c >= 'a'&&c <= 'z'||c >= 'A'&&c <='Z')
letters++;
else if(c == ' ')
spaces++;
else if(c >= '0'&&c <='9')
digits++;
else
others++;
}
printf("letters=%d,spaces=%d,digits=%d,others=%d\n",letters,spaces, digits,
others);
return 0;
}
运行结果如下:

【例 3-21】 输出下列图形。
*
***
*****
*******
分析:
该图形共有 4 行,每行都是由空格符和星号*组成的,第 1 行的构成规则是:3 个空格、1 个
星号、3 个空格,第 2 行的构成规则是:2 个空格、3 个星号、2 个空格,第 3 行的构成规则是:1
个空格、5 个星号、1 个空格……这里,由于右端的空格输出与否并不影响输出图形的效果,因此
可以不予考虑。左端空格的个数与所在的行数存在关系:空格数+行数=4,星号的个数与所在的
行数存在关系:星号个数=2×行数−1。可以用双层循环实现,外层循环控制行的变化,内层循环
有两个循环语句,第 1 个循环输出空格,第 2 个循环输出星号即可。本题采用 for 语句比较合适。
算法描述:
(1)定义循环变量 row, col_space, col_asterisk;
(2)循环变量赋初值 row←1,循环条件 row <= 4,循环执行以下语句
1)循环变量赋初值 col_space←1,循环条件 col_space <= 4 – row,循环执行以下语句
输出空格符“ ”

2)循环变量赋初值 col_asterisk←1,循环条件 col_asterisk <= 2 * row − 1,循环执行以下语句
输出星号“*”;
3)输出换行符“\n”。
程序:
#include <stdio.h>
int main()

64
第3章 控制结构

{
int row, col_space, col_asterisk;
for(row = 1; row <= 4; row++)
{
for(col_space = 1; col_space <= 4−row; col_space++)
printf(" ");
for(col_asterisk = 1; col_asterisk <= 2*row-1; col_asterisk++)
printf("*");
printf("\n");
}
return 0;
}
运行结果如下:

思考一下,如果图形的行数由用户输入的数值指定,应该如何修改例 3-21 的程序?

3.7 深入研究:程序优化问题
编写一个能够正确运行的程序并不是编程的全部工作,还需考虑程序运行效率、内存使用效
率以及程序可读性、可靠性和可维护性等方面的问题,这就是程序优化。通过优化可以使程序在
相同的硬件条件下实现更强的功能和性能,或者是在相同的功能和性能前提下降低对硬件性能的
要求,并相应地降低硬件平台的成本。同时,通过对程序结构的改进,可以控制代码的规模,使
之便于维护和更新,以便延长其生命周期。
程序优化是一个复杂的过程,涉及的问题很多,需阅读相关书籍进行深入学习,这里仅对程
序运行效率的改进做简单介绍。调整代码顺序是改进程序运行效率的一种方法,包括提取和集中
处理公共表达式,将不变式条件移出循环体,将条件判断移出循环体,展开代码,预先计算,以
及用低价操作替代高价操作等。这些方法分别适用于不同的条件,既可以单独使用,也可以综合
使用。
1.提取公共表达式
对于需要多次使用的表达式的值只计算一次,并将计算结果保存在变量中,以避免对相同的
表达式多次求值。例如,在下列表达式中:
x = sqrt(dx * dx + dy * dy) + (sqrt(dx * dx + dy * dy)>0) ? vx : -vx;
子表达式“sqrt(dx * dx + dy * dy)”被使用了两次。因此在这个表达式的计算中可以把该子表达式
提取出来单独计算和保存。这样,上述表达式的计算可以改写为
sq = sqrt(dx * dx + dy * dy);
x = sq + (sq>0) ? vx : -vx;
2.将与循环无关的表达式移出循环语句
对于循环语句中的表达式,每一次循环都会对其求值,因此与循环无关的表达式只需要求值
一次即可,将与循环无关的表达式移出循环语句可以避免不必要的重复计算。例如,假设在下面

65
C 语言程序设计(第 2 版)

的代码中,变量 k 的值在循环语句中不改变:
for(i = 0; i < MAX_I; i++)
{ for(j = 0; j < MAX_J; j++)
{
x = sqrt(k);

}

}
那么,表达式“x = sqrt(k);”就是与循环无关的表达式,可以将其移出循环语句:
x = sqrt(k);
for(i = 0; i < MAX_I; i++)
{ for(j = 0; j < MAX_J; j++)
{

}

}
3.将与循环无关的条件判断移出循环语句
在循环语句的循环体中常常包含条件语句,如果这些条件语句与循环变量没有任何直接或间
接的关系,那么就可以将这些条件移到循环语句之外,以便改进程序的运行效率。例如,在下面
的代码中:
for(i = 0; i < MAX; i++)
if(agent_works(a))
{

}
表达式“agent_works(a)”需要执行 MAX 次,如果它的值与循环无关,那么可以把代码改成如下
的形式:
if(agent_works(a))
for(i = 0; i < MAX; i++)
{

}
4.预先计算可能用到的数值
这是一种典型的用存储空间换取运行时间的技术,常常用于数值计算、图像处理、信号处理
等的编程中,在这类程序中,常常会用到大量的函数值、质数表或多项式的系数表等,这些函数
值、质数表或系数表既可以在被用到时临时调用相应的函数或计算公式进行计算,也可以事先计
算出来保存在相应的表格中,以便减少计算工作量,提高程序运行速度。例如,如果程序中需要
使用以度为自变量单位的正弦函数和余弦函数值,就可以定义三解函数表如下:
double sin_tab[] = {…};
double cos_tab[] = {…};
这样,程序中对正弦函数和余弦函数的调用就可以转换为对数组元素的访问(数组见第 5 章,
下面所有关于数组的操作可以在学习了第 5 章后再深入分析)
。例如,下面的代码
x = sin(r);
就可以改写为
x = sin_tab[(int) (r * 360 / PI)];

66
第3章 控制结构

在很多计算平台上,上述两条语句的执行时间相差百倍以上。
5.用低价操作替代高价操作
同一个计算过程,在 C 语言中往往可以使用不同的机制和描述方式,这些在功能上等价的计
算机制和描述方式在性能上可能会有比较大的差别。因此,在计算中使用性能较高的“低价”操
作替代性能较低的“高价”操作,是程序优化中的一种常用技术。例如,当将局部数组变量的所
有元素初始化为 0 时,一般可以使用下面两种方法:
int i, array[N_ITEMS];
for(i = 0; i< N_ITEMS; i++)
array[i] = 0;

int array[N_ITEMS] = {0};
第二种方法不但运行效率远高于第 1 种方法,而且描述也更加简洁。
需要强调的是,虽然上述方法可以有效地改进程序的运行效率,但在实际的软件开发中,要
从全局考虑程序的优化问题,不能为了局部的运行效率的优化而影响整个软件的性能、可读性和
可靠性。

本章小结
程序流程控制语句是 C 程序设计语言中很重要的部分,可分为顺序语句、选择语句和循环语
句。顺序语句包括赋值语句和空语句,选择语句包括 if 语句和 switch 语句,循环语句包括 while
语句、do-while 语句和 for 语句。
if 语句中还可以嵌套 if 语句,这时会出现多个 if 和多个 else 重叠的情况,要特别注意 if 和 else
的配对问题,else 语句总是与它前面最近的那个未配对的 if 语句配对。为了使程序的条理更加清晰、
明确,可以使用语句的嵌套,但是要注意嵌套的语句之间的逻辑关系,嵌套的语句不能有交叉。
循环语句可以实现大量的重复工作,当需要某段程序至少执行一次时可以选择 do-while 语句,
而某段程序可能一次也不执行时可以选择 while 语句。
break 语句用在 switch 语句或循环语句中,作用是跳出 switch 语句或者本层循环而去执行下
一条语句,而 continue 语句只能用在循环语句中,它的作用是结束本次循环,继续下一次循环条
件的判断与执行。要特别注意 break 语句和 continue 语句的用法区别。

习 题
【复习】
1.下面的语句段执行后,输出的值是( )

int a = 3, b = 5;
printf("%d,%d", b, a);
A.5 3 B.3, 5 C.3 5 D.5, 3
2.若 x = 4, y = −2, z = 5,则表达式++x−y+z++的值为( )

A.10 B.11 C.12 D.13

67
C 语言程序设计(第 2 版)

3.以下给定程序的输出结果为( )

#include "stdio.h"
int main()
{
int x = 12;
while(x--);
printf("%d", x);
return 0;
}
A.−1 B.0 C.11 D.1
4.若 int i = 10;则执行下列语句后,变量 i 的正确结果是( )

switch (i)
{
case 9: i += 1;
case 10: i += 1;
case 11: i += 1;
default: i += 1;
}
A.10 B.11 C.12 D.13
5.C 语言允许 if-else 语句嵌套使用,规定 else 总是和( )配对。
A.其之前最近的 if B.第 1 个 if
C.缩进位置相同的 if D.其之前最近的且尚未配对的 if
6.把下面的程序补充完整。程序的功能是交换 x 和 y 的值。
#include "stdio.h"
int main()
{
int x = 5, y = 3, temp;
;
;
;
printf("%d %d", x, y);
return 0;
}
7.阅读下面的程序:
#include "stdio.h"
int main()
{
int a, b, c;
printf("please input:\n");
scanf("%d,%d,%d", &a, &b, &c);
if(a < b)
if(b < c)
printf("max = %d\n", c);
else
printf("max = %d\n", b);
else if(a < c)
printf("max = %d\n", c);
else
printf("max = %d\n", a);
return 0;
}

68
第3章 控制结构

该程序的功能是: 。
8.阅读下面的程序:
#include< stdio.h>
int main()
{
int count,num,total;
count=0;total=0;
while(count<10)
{count++;
printf("enter the NO.%d=",count)
scanf("%d",&num);
total+=num;
}
printf("Total=%\n",total);
return 0;
}
如果用户输入 0 1 2 3 4 5 6 7 8 9 十个数,则输出结果是: 。
【应用】
1.输入一个字符,将其转换成小写字母后输出。
2.编程求 1+3+5+…+101 的和。
3.输出下面的图形。
******
******
******
4.输出 9*9 口诀。
【探索】
1.爱因斯坦的阶梯问题。爱因斯坦曾出过这样一道有趣的数学题:有一个长阶梯,若每步上
2 阶,最后剩 1 阶;若每步上 3 阶,最后剩 2 阶;若每步上 5 阶,最后剩 4 阶;若每步上 6 阶,
最后剩 5 阶;只有每步上 7 阶,最后刚好一阶不剩。问该阶梯至少有多少阶?编写一个 C 程序解
决这个问题。
2.百钱买百鸡问题。100 元钱买 100 只鸡,公鸡 5 元 1 只,母鸡 3 元 1 只,小鸡 1 元 3 只,
编写一个 C 程序,给出该问题的解决方案。
3.凯撒密码程序。通过键盘输入一行字符,以@结束,对输入的字符串进行加密处理。要求:
a−>b,b−>c,…,z−>a 和 A−>b,B−>c,Z−>a,其他字符不变。

69
第 4章
函数

本章目标
◇ 了解 C 语言中函数的相关概念
◇ 熟悉 C 语言函数的定义与使用方法
◇ 掌握变量的作用域、生命期和存储类型的含义与使用
◇ 基于模块化思想运用函数解决较为复杂的实际问题
在现实世界中,很多问题都要比前面章节中解决的问题复杂和庞大得多。一般情况下,一个
大而复杂的问题通常被分解成小而独立的多个子问题分别完成,从而实现对问题的求解。实践证
明,在编写一个规模较大的程序时,这种方法也是非常可取的,一个大程序也被分解成多个相对
独立的小模块,从而实现程序的模块化结构。将模块化机制引入程序设计,可以提高代码的复用
性,增强代码的可读性和可维护性,同时还有利于团队开发,便于分工合作。
在 C 语言中,这些小模块由函数来实现,函数是 C 程序的基本组成单位。一个完整的 C
程序可以由多个函数组成,这些函数可以由编程者自己定义,如 main( )函数,也可以是别人定
义好的,如前面经常用到的 printf( )函数。函数定义完成后,在程序需要的地方调用,从而实现
指定的功能。

4.1 函数的定义
C 语言规定,函数要遵循“先定义,后使用”的原则。函数定义由两部分组成:函数头(也
叫函数首部)和函数体。
函数定义的一般形式如下:
类型说明符 函数名([形式参数列表])
{
函数体
}
其中,函数定义的第一行为函数头,包括类型说明符、函数名和形式参数列表。类型说明符
定义了函数返回值的数据类型,该返回值可以是任何数据类型,如果函数没有返回值则类型说明
符为 void,即空类型。函数名是任何合法的 C 标识符。形式参数列表包含一个或多个参数,也可
以没有参数,每个参数都需要说明其数据类型,各参数说明之间用逗号分隔。函数体由一对花括
号“{ }”括起来,由合法的 C 语句构成。花括号是函数的定界符。

70
第 4 章 函数

例如:
int exp(int x, int n)
{
int i,s = 1;
for(i = 1; i <= n; i++)
s = s * x;
return s ;
}
定义了一个返回值类型为 int 型的函数 exp( ),有两个形式参数分别为 x 和 n,数据类型为 int 型。
函数定义时,函数名后面的参数称为形式参数,通常简称形参。如果形参的个数为 0,则该
函数称为无参函数,反之称为有参函数。

M提醒
函数定义时,无论是有参函数还是无参函数,函数名后面的一对圆括号一定不能省略,圆
括号后面没有分号。

例如:
void Hello()
{
printf("Hello, C programming!\n") ;
}
定义了一个没有返回值的无参函数 Hello( )。

为了完成函数的功能,通常在函数体中需要定义变量,在该函数中的任何位置都可
以使用的变量,它们的定义要放在函数体的开始部分,否则会出现编译错误。

例如:
void Hello()
{
int i, j ;

}
是正确的,而
void Hello()
{
int x;
x = 1;
int i, j ;

}
是错误的。
在 C 语言中,函数体部分可以为空(即仅有一对花括号),这样的函数称为“空函数”
。由于
函数体中没有任何语句,所以空函数什么也不做,使用它的目的是为了“占位”
,也就是说,为程
序将来要完成的功能占个位置。
在模块化程序设计中,往往根据需要确定若干功能模块,分别由一些函数来实现。而在第一
阶段只设计实现最基本的功能模块,其他一些次要功能或锦上添花的功能则在以后的开发过程中
陆续完善。因此空函数对于较大程序的编写、调试及功能扩充很有用处。

71
C 语言程序设计(第 2 版)

需要强调的是,函数定义不能嵌套,即函数体内不能再定义函数,函数定义应该在所有函数
之外。例如,下面的定义方式是错误的。
int sum() /* 第一个函数的定义 */
{

int add() /* 第二个函数的定义 */
{

}
}
特别提示:
在 C89 标准中,无参函数的形参列表为空,即函数名后面的圆括号内什么都不写。在 C99 标
准中则要求用 void 明确表示无参函数。

4.2 函数的调用

4.2.1 函数调用的一般形式
函数定义后,要通过函数调用来使用该函数的功能。
函数调用的一般形式如下:
函数名([实际参数列表]);
其中,实际参数列表可以包含一个或多个参数,各参数之间用逗号分隔,也可以没有参数,
但一定要与定义该函数时形式参数列表中的参数个数相同,类型一致,顺序对应。通常实际参数
也简称为实参,实参可以是常量、变量或其他类型的数据及表达式。
【例 4-1】 函数调用举例。
程序:
#include <stdio.h>
/* fun()函数的定义*/
void fun(int x, int y)
{
printf("%d,%d\n", x, y);
}
/* 主函数 main()*/
int main()
{
int iFirst, iSecond;
iFirst = 123;
iSecond = 99;
fun(iFirst, iSecond); /* 调用函数 fun() */
return 0;
}
运行结果如下:

72
第 4 章 函数

在主函数中通过调用函数 fun( )输出 iFirst 和 iSecond 的值。x 和 y 为形参,iFirst 和


iSecond 为实参。

在函数调用时,实参可以是常量、变量、表达式和函数等,无论实参是何种类型,
都必须是有确定值的量。如例 4-1 中的 iFirst 的值是 123,iSecond 的值是 99。因此应预
先用赋值、输入等方法使实参获得确定值。

M提醒
在函数调用时,即使是无参函数,函数名后面的一对圆括号也一定不能省略。

在 C 语言中,函数定义完成后,函数调用是由函数调用语句来实现的,函数调用语句通常在
另外一个函数中,这个函数称为主调函数,定义的那个函数称为被调函数。在例 4-1 中,主调函
数是主函数 main( ),被调函数是函数 fun( )。
函数调用是指在一个函数内部转去执行另一个函数的过程。一般来说,函数调用时通过将实
际参数传递给形式参数的方式来实现数据传送,当被调函数执行完毕后,程序控制返回到主调函
数的调用处继续执行后续的语句。其调用过程如图 4-1 所示。

主调函数main( )
被调函数fun( )

int main( ) void fun(…)
{ {
… …
fun(iFirst,iSecond); }

return 0;
}

图 4-1 函数调用过程示意图

由于函数 fun( )没有返回值,程序在调用该函数完成其指定功能后,返回到主调函数的调用处


继续执行后面的语句。如果被调函数有返回值,则函数调用可以出现在表达式中或作为另一个函
数的实参,利用它的返回值参与主调函数的运算。
【例 4-2】 找出两个整数中的较大数并输出。
程序:
#include <stdio.h>
int max(int x, int y)
{
int z;
if(x>y)
z=x;
else
z=y;
return z;
}

73
C 语言程序设计(第 2 版)

int main()
{
int a = 5, b = 3, c;
c = max(a,b);
printf("The max number is %d\n", c);
printf("The max number is %d\n", max(200,300));
return 0;
}
运行结果如下:

在函数 main( )中通过语句“c=max(a,b);”实现对函数 max( )的调用,在函数 max( )


的最后通过语句“return z;”将返回值传递给主调函数,并赋值给变量 c,然后继续执行
后面的语句。

进一步对例 4-2 进行分析,实际上,该程序的功能完全可以在函数 main( )中直接完成,无须


定义函数 max( ),这是否说明被调函数的定义是画蛇添足呢?答案是否定的,被调函数的定义提
高了程序的可理解性、复用性和可维护性。一方面,通过函数 max( )的定义,使主函数变得更加
简单,一目了然;另一方面,定义了函数 max( )后,可以通过在主函数中多次调用函数 max( )实
现对多组整数的最大值求解,从而实现“定义一次,多次使用”,使函数 max( )的代码和功能被共
享,而且,一旦需要修改求最大值的方法,只要修改函数 max( )即可,使程序维护变得简单。因
此,将一个较大的任务分解成多个相对独立的模块,分别用函数来实现,是解决复杂问题的有效
方法。
【例 4-3】 求 ∑ i =1 i = 1 + 2 + 3 + ... + n 的值(n>0)
n

分析:
仔细观察题目可知本题是求解 1 到 n 的累加和,其中 n 是一个动态变化量,应该在程序执行
的过程中动态给出。求和过程是一个相对独立的功能,因此可以考虑将求和功能用一个函数来实
现,然后在主函数中调用它。因此求解该问题要完成两步:编写主函数和编写求和函数。
第一步:编写主函数。本题中的主函数要做三件事:接收键盘输入 n 的值、调用求和函数计
算 1 到 n 的和、打印输出结果。
第二步:编写求和函数。编写函数就是给出函数的定义。
(1)函数头的定义。一般情况下首先确定函数名,本着“见名知意”的命名规则,可以命名
为 integerSum。然后考虑函数的返回值,求和的结果应在主调函数中输出,因此函数 integerSum( )
应该有返回值,返回值类型可以是 int 型。进一步思考,由于 int 型最大有效值是 32767,这对于
求和上限 n 不确定的情况下很容易造成数据的溢出,因此用 long 型更恰当。最后考虑形参,随着
上限 n 值的不同,求和的结果也不同,因此函数 integerSum( )应该有一个形参 n。这样,函数头
可以定义为“long mySum(int n)”。
(2)函数体的定义。求和功能的实现可参照例 3-10,函数体的最后必须有一条 return 语句,
用于返回求和的结果,这是有返回值的函数的基本特征。

74
第 4 章 函数

算法描述:
被调函数 integerSum( )
(1)定义循环变量 i 和累加器 sum;
(2)循环变量赋初值 i←1,循环条件 i <= n,循环变量 i++,循环执行以下语句
sum←sum + i;
(3)返回 sum 的值。
主函数 main( )
(1)接收用户输入的 n 值;
(2)定义变量 result 存储求和的结果;
(3)进行函数调用 result = integerSum(n);
(4)输出 result 的值。
程序:
#include<stdio.h>
long integerSum(int n)
{
int i = 0;
long sum = 0;
for(i = 1; i <= n; i++)
sum = sum + i;
return sum;
}
int main()
{
int n;
long result;
printf("Please input n: ");
scanf("%d",&n);
result = integerSum(n);
printf("The sum of 1.. %d is %ld\n", n, result);
return 0;
}
运行结果如下:

对于有参函数,函数的形参与函数体中定义的变量一样可以在函数体中使用,因此,
函数体中定义的变量不能与形参同名。

M提醒
(1)C 程序设计时,通常采用“自顶向下、逐步细化、模块化”的程序设计方法。
(2)当程序中需要定义较多的函数时,不要把所有程序都编辑完才测试和调试,而是采用
逐步扩充功能的方式分批进行。

75
C 语言程序设计(第 2 版)

特别提示:
如果实参列表中的某一参数需要求值代入,那么求值的顺序并不是确定的,这与系统有关,
有的系统按自左至右顺序求值,
有的系统按自右至左顺序求值。许多 C 版本
(如 Turbo C 和 VC 6.0)
是按自右至左的顺序求值,例如,在 VC 6.0 中执行语句 int x = 2; printf("%d,%d", x, x = x + 1);后,
得到的结果是 3, 3 而不是 2, 3。因此在正式使用 C 程序之前,程序员应该编写简单的测试程序调
试一下,确定使用系统的求值顺序以避免这种问题。

4.2.2 函数参数的传递
如果被调函数是有参函数,当程序的控制由主调函数转向被调函数时,需将函数调用语句的
实参的值传递给被调函数的形参,这就是参数传递。例 4-2 和例 4-3 中都存在参数传递过程。参
数传递完成后,程序的控制就转向了被调函数,开始被调函数的执行。
C 语言中,参数传递的方式有两种:传值和传址。传值,是指进行参数传递时将实参的值赋
值给形参;传址,是指将实参的地址赋值给形参。采用传址方式进行参数传递时要借助指针来完
成,将在后续章节中介绍。
【例 4-4】 分析以下程序的运行结果,注意函数参数的传递方式。
程序:
#include <stdio.h>
void swap(int x, int y)
{
int temp;
printf("The values of formal parameter:\n");
printf("x = %d, y = %d\n", x, y);
temp = x;
x = y;
y = temp;
printf("The values of formal parameter after swapping:\n");
printf("x = %d, y = %d\n", x, y);
}
int main()
{
int a = 2, b = 6;
printf("The original values of actual parameter:\n");
printf("a = %d, b = %d\n", a, b);
swap(a,b);
printf("The values of actual parameter after calling function:\n");
printf("a = %d, b = %d\n", a, b);
return 0;
}
运行结果如下:

76
第 4 章 函数

系统首先为主函数 main( )中定义的变量 a 和 b 分别分配了 2 个字节大小的内存单元,


并将整数 2 和 6 存储起来,此时输出变量 a 和 b 的值分别为 2 和 6。然后,执行函数调
用语句“swap(a, b);”,程序的控制将由主函数 main( )转向函数 swap( ),此时系统首先进
行参数传递,将实参 a 和 b 的值 2 和 6 分别赋值给形参 x 和 y。按照形参 x 和 y 的数据
类型,系统为 x 和 y 分别分配 2 个字节的内存单元,接着将实参 a 和 b 的值 2 和 6 存储
其中,然后系统将程序的控制权交给被调函数 swap( ),此时 x 和 y 的值分别为 2 和 6。
在函数 swap( )中交换 x 和 y 的值后,x 和 y 的值分别为 6 和 2。需要强调的是,这种交
换改变的仅仅是形参 x 和 y 的值,实参 a 和 b 的值并不会发生变化,因此,从执行结果
可以看出,当函数 swap( )执行完毕,程序的控制回到主函数 main( )后,变量 a 和 b 的值
并没有发生变化。其示意图如图 4-2 所示。

a 2 b 6 a 2 b 6

x 2 y 6 x 6 y 2

(a)参数传递过程 (b)形参交换后对形参与实参的影响

图 4-2 实参与形参的变化示意图

例 4-4 的运行结果表明,采用传值方式进行参数传递时,只是将实参的值传递给了形参,在
被调函数内部对形参的修改不会对实参产生任何影响。

M提醒
只有在函数被调用时系统才会为形参分配内存空间,调用结束后,立刻释放所分配的
内存空间,因此形参只在被调函数内部有效。这里涉及变量的作用域问题,后续章节将详细
介绍。

4.2.3 函数的嵌套调用
函数的嵌套调用是指,在一个被调函数内部,又调用了其他的函数。其示意图如图 4-3 所示。

main( )函数 f1( )函数 f2( )函数

#include<stdio.h>
int f1(…) ② void f2(…)
int main( ) ①
{ {
{
… …

… …
A=f1(…);
… f2(…);
④ ③
return 0; … }

} }

图 4-3 函数嵌套调用的执行顺序

77
C 语言程序设计(第 2 版)

【例 4-5】 计算 1!+2!+…+n!的值,要求用函数的嵌套调用方式实现。
分析:
基于模块化思想,可以将该问题分解为两个子问题:
(1)依次计算自然数 1~n 的阶乘,
(2)求这些阶乘值的累加和。可以定义两个函数来实现。由于要计算的是 n 个不同的自然数的阶
乘的值,因此第一个函数(命名为 factorial)要有一个形参(命名为 m)
,用于识别要计算的是哪个自然
数的阶乘。第二个函数(命名为 factorialSum)要明确对多少个数求和,因此要有一个形参(命名为 n)

程序:
#include <stdio.h>
/* 计算 n!的函数*/
long factorial(int m)
{
int i;
long lFac = 1;
for(i = 1; i <= m; i++)
lFac = lFac * i;
return lFac;
}
/* 计算累加和的函数 */
long factorialSum(int n)
{
long sum = 0;
int i;
for(i = 1; i <= n; i++)
sum = sum + factorial(i);
return sum;
}
/* 主函数 * /
int main()
{
int n;
long t;
printf("Please input the value of n: ");
scanf("%d", &n);
t = factorialSum(n);
printf("result = %ld\n", t);
return 0;
}
运行结果如下:

函数的这种逐层调用构成了 C 程序的基本框架,因此,通常把 C 语言称为函数式语言。虽然


C 语言对函数的嵌套调用的层数未加限制,但嵌套的层数过多也会降低程序的运行效率。

4.3 函数的声明
当 C 程序中出现了多个用户自定义函数,要想该程序能够正常通过编译,则被调函数的定义

78
第 4 章 函数

要出现在主调函数之前,这样,在对主调函数进行编译时,编译系统能够认识主调函数中出现的
被调函数,从而完成编译过程。但是,C 语言对函数定义的位置并没有加以严格限制,被调函数
也可以出现在主调函数之后,这时必须在主调函数中的函数调用语句之前进行函数声明。所谓函
数声明就是在编译系统认识被调函数之前,先告诉编译系统该函数的存在,并将有关信息(如函
数的返回值类型、函数参数的个数、类型及其顺序等)通知编译系统,使编译过程正常执行。
函数声明的一般形式如下:
类型说明符 函数名([形式参数列表]);

类型说明符 函数名(数据类型列表);
例如:
int max(int x,int y);

int max(int,int);
就是正确的函数声明的形式。它告诉编译系统在程序中存在一个被调函数 max( ),该函数的返回
值为 int 型,有两个形参,类型都是 int 型。

M提醒
函数声明是通过函数声明语句实现的,因此进行函数声明时,最后必须以分号“;”结束。
必须注意函数声明与函数定义时的函数头在形式上的区别,并且理解两者本质上的不同。
函数定义时,函数类型、函数名、形参列表及函数体是一个整体,而函数声明仅是对被调
用函数的说明,其作用仅仅是告诉 C 编译器被调用函数的类型、名称以及使用的参数类型。

【例 4-6】 分析以下程序,找出其中的错误。
#include<stdio.h>
float fSum(float, float); /* 函数声明 */
int main()
{
float fFirst, fSecond;
fFirst = 123.23;
fSecond = 99.09;
printf ("sum = %f\n", fSum(fFirst, fSecond)); /* 调用函数 fSum() */
printf("multi = %f\n", fMulti(fFirst, fSecond)); /* 调用函数 fMulti() */
return 0;
}
int fMulti(float x, float y)
{
return (fSum(x,y)*fSum(x,y));
}
float fSum(float x, float y)
{
return (x + y);
}

该程序有三个函数,主函数 main( )、调用函数 fSum( )和 fMulti( )。由于函数 fSum( )的


定义在主函数之后,因此在主函数之前用了函数声明语句“float fSum(float, float);”
。主函数
还调用了函数 fMulti( ),该函数的定义也在主函数之后,但在主函数之前并没有给出该函数
的声明语句,只要在主函数的前面增加一条函数声明语句“float fMulti (float, float);”即可。

79
C 语言程序设计(第 2 版)

实际上,函数声明语句既可以在所有函数的外部,也可以在函数的内部。如果函数声明在所
有函数外部,那么在该函数声明语句之后出现的所有函数都可调用被声明的函数;如果函数声明
在函数内部,那么仅在声明它的函数内部可以调用该函数。
思考一下,在例 4-6 中,如果将函数声明语句“float fSum(float, float);”写在主函数的内部会
怎样?

M技巧
(1)为了增强程序的可读性,通常按照函数的逐层调用关系来书写函数的定义。即函数
main( )写在程序的最前面,其他被调函数按照调用的顺序依次写在函数 main( )的后面。
(2)根据实际问题的需要决定函数声明语句的位置,这是一种良好的编程习惯。如果被调
函数允许在任何函数中被调用,则将该函数的函数声明语句放在任何函数的外部,否则放在允
许调用该函数的某些主调函数的内部。

特别提示:
C99 标准要求,函数的声明和定义必须一致,否则可能导致编译系统也无法检测的错误。

4.4 函数的返回与返回值

4.4.1 函数的返回
函数返回有两种方式。第一种方式是在执行完函数的最后一个语句之后,从概念上讲,即遇
到了函数的结束符“}”后返回(当然这个花括号实际上并不会出现在目标码中,但可以这样理解)。
例如,下面的函数在屏幕上显示一个字符串。
void pr_reverse()
{
char s[80];
scanf("%s", s);
printf("%s\n", s);
}
一旦字符串显示完毕,函数就返回到被调用处。
第二种方式是采用函数返回语句 return,前面已有多处地方使用了 return 语句。
函数返回语句的一般形式如下:
return <表达式>;

return;
当函数有返回值时,在函数体中采用函数返回语句“return ,该语句的执行过程
<表达式>;”
如下:
(1)计算表达式的值。
(2)转换表达式的类型。当表达式的值的类型与函数返回值类型不一致时,应将表达式的
值的类型转换为返回值类型,如果是低精度到高精度转换由系统自动完成,反之就需要强制类型
转换。

80
第 4 章 函数

(3)实现程序控制的转移。将该表达式的值返回给主调函数后,将程序的控制权交给主调函
数继续执行后续的操作。
当函数没有返回值时,在函数体中采用函数返回语句“return;”,该语句的执行仅完成程序控
制的转移,由被调函数返回到主调函数,作用与第一种方式的函数返回相同。
例如:
int find_char(char s1,char s2)
{
if(s1 == s2)
return 1;
else
return –1;
}
定义了一个函数 find_char( ),函数在 s1 和 s2 相等时返回 1,不相等时返回–1。

一个函数可以有多个 return 语句,但在程序的执行过程中,只有一条 return 语句会


被执行;而且每个 return 语句最多只能有一个返回值。

M技巧
对于一个没有返回值的函数来说,如果函数的流程在任何条件下都要完成某些操作,然后
执行到最后一条语句后返回,此时可以不用 return 语句。但是,如果在某些条件满足的情况下,
函数不再作任何操作而直接退出,此时将分支语句和 return 语句配合使用可能会增强程序的可
读性。

4.4.2 返回值
只要函数没有被说明为 void 型,就说明该函数具有一个返回值,那么在主调函数中,该函数
就可以作为任何有效的 C 语言表达式中的操作数,使该函数的返回值在主调函数中参与相关的运
算。例如,假设在一个 C 程序中,已定义了一个函数 max( ):
int max(int x, int y)
{
if (x > y) return x ;
else return y;
}
那么,下面给出的对函数 max( )的调用方式都是正确的:
iMax = max(x,y);
printf("The max value is: %d\n",max(x,y));
max(x,y)* max(x,y) ;
if(max(x, y) > 100) printf("true");

在 C 语言中,通常不允许把函数作为赋值对象,也就是说函数不能出现在赋值运算
符的左边。例如,语句“max(x, y) = 5;”是错误的。

【例 4-7】 分析以下程序的运行结果,注意函数返回值的作用。
程序:
#include <stdio.h>
int integerMulti(int x, int y) ;

81
C 语言程序设计(第 2 版)

int main()
{
int x, y, z;
x = 10;
y = 20;
z = integerMulti(x, y); /*返回值作为表达式的操作数 */
printf("%d\n", z);
printf("%d\n", integerMulti (x, y)); /*返回值作为其他函数的参数 */
integerMulti (x, y); /*返回值丢失了 * /
return 0;
}
int integerMulti(int x, int y)
{
return x* y;
}
运行结果如下:

特别提示:
(1)在 C89 标准中,在有返回值的函数定义中,也可以在 return 语句后面不跟表达式,这样
函数将不会给主调函数返回有意义的值(即返回一个随机数)
;但 C99 标准中不允许出现不带返
回值的 return 语句,也不允许函数体没有 return 语句。
(2)在 C89 标准中,如果函数省略了返回值类型,则系统默认的返回值类型是 int 型。但是,
C99 标准要求任何函数(包括主函数 main( ))都必须有明确的返回值类型。

4.5 函数的递归调用
在 C 语言中,主调函数与被调函数可以是不同的函数(参见前面的例子)
,也可以是相同的
函数,即允许一个函数调用它自身。如果一个函数在它的函数体内直接或间接地调用函数自身,
则将这种调用形式称为函数的递归调用,将这个函数称为递归函数。函数的递归调用是函数嵌套
调用的一种特殊形式。
【例 4-8】 计算 n!,要求:定义递归函数实现。
分析:
例 3-11 使用 while 语句实现了 n!的计算,在例 3-11 中,计算 n!是基于数学公式“ n! = 1 ×
2 × 3 × L× n ”
,实际上还有另外一个数学公式“ n! = (n − 1)! × n ” ,也就是说要计算 n!,只要把 (n − 1)!
计算出来即可。而 (n − 1)! 与 n!的计算方法完全相同,仅仅是参数不同而已。可以用这种方法继续
向前推,即 (n − 1)! = (n − 2)! × (n − 1) ……依此类推,直到 2! = 1! × 2 , 1! = 0! × 1 ,而 0! = 1 ,此时
无须再类推下去。因此,可以用下面的递归公式计算 n!:
1 n=0
n! = 
 (n − 1)! × n n ≥1
这是一种基本的算法思想:分治与递归的思想,在本章的深入研究中进行详细介绍。

82
第 4 章 函数

程序:
#include<stdio.h>
long factorial(int n);
int main()
{
long result;
int n;
printf("Please input the value of n: ");
scanf("%d", &n);
result=factorial(n);
printf("%d! = %ld\n", n, result);
return 0;
}
long factorial(int n) /*递归函数定义*/
{
long iFac;
if(n == 0)
iFac = 1;
else
iFac = factorial(n – 1) * n; /*递归调用*/
return iFac;
}
运行结果如下:

下面以计算 3!为例,具体描述递归程序的执行过程:
3! = 3×2! 3! = 3×2 = 6

递推① 回归③

2! = 2×1! 2! = 2×1=2
递推② 回归②

1! = 1×0! 1! = 1×1=1
递推③
回归①
0! = 1

实现递归分为以下两个阶段。
(1)
“递推”阶段:将原问题不断分解为新的子问题,不断推进直到达到已知条件,即递归结
束条件。
(2)
“回归”阶段:从已知条件出发,按“递推”的逆过程,逐一求值回归,直到递推的开始
处,结束回归阶段。

M提醒
函数的递归调用仅是解决问题的一种方法,对于所有能用递归方法解决的问题,都可以用
非递归方法来实现。采用递归方式的算法简单,容易实现,代码简洁,但会在一定程度上降低
程序的运行效率,对函数的多次递归调用也可能造成堆栈的溢出。

83
C 语言程序设计(第 2 版)

【例 4-9】 从键盘上输入两个整数,使用递归和非递归两种方法编程,求出它们的最大公约数。
分析:
求最大公约数的基本方法是辗转相除法,即:两个数中的一个数为被除数,另一个数为除数,
作除法运算,如果不能整除,则将除数作为新的被除数,将余数作为新的除数,继续作除法运算,
直到整除为止,此时的除数就是这两个数的最大公约数。
这种方法的基本做法就是不断地作除法运算,每次作除法运算时被除数和除数的变化是有规
律的,这种运算的终止条件是余数为 0。因此可以发现这种算法的基本数学模型如下:
y x%y = 0
gcd = 
gcd(y,x%y) x%y ≠ 0

如果不用递归方法,可以用 while 循环来实现,当 x%y == 0 时,终止循环,在循环体进行取


余运算,并不断改变被除数与除数。
程序-1(递归方法)

#include<stdio.h>
long gcd1(int x, int y);
int main()
{
int a, b;
long g;
printf("Input two numbers: ");
scanf("%d%d",&a, &b);
g = gcd1(a, b);
printf("The greatest common divisor is %ld\n", g);
return 0;
}
long gcd1(int x,int y)
{
if(x % y == 0)
return y;
else
return gcd1(y, x % y);
}

程序-2(非递归方法)

#include<stdio.h>
long gcd2(int x, int y) ;
int main()
{
int a,b;
long g;
printf("Input two numbers: ");
scanf("%d%d",&a, &b);
g=gcd2(a, b);
printf("The greatest common divisor is %ld\n", g);
return 0;
}
long gcd2(int x, int y)
{
int temp;
while(x % y != 0)
{
temp = x % y;

84
第 4 章 函数

x = y;
y = temp;
}
return y;
}
运行结果如下:

4.6 变量的作用域与生命期
在 C 程序中,经常要用到各种类型的变量,这些变量不仅可以定义在函数的内部,也可以定
义在函数的外部。变量的定义位置不同,它的作用域也不同。变量的作用域是指变量能够独立地
合法出现的区域,用于描述某个变量在程序中的可见范围。通常将定义在函数内部的变量称为局
部变量,定义在函数外部的变量称为全局变量。一般情况下,局部变量的内存空间在动态数据区
中分配,全局变量的内存空间在静态数据区中分配。
作用域是一个静态概念,它的作用就是规定变量合法使用的范围。如果在编写源程序时,在
作用域外使用了某个变量,则编译器不能识别该变量,就会出现编译错误。生命期是一个运行时
概念,是指一个变量在整个程序从载入到结束运行的过程中,在哪一个时间区间有效。

4.6.1 局部变量
局部变量是定义在函数内部的变量,如果变量的定义在函数体的开始处,则该变量可以在整个函
数中使用,它的作用域是从变量定义处到本函数的结束处;如果变量的定义在函数内部的某个复合语
句中,则该变量只能在这个复合语句中使用,它的作用域是从变量定义处到该复合语句的结束处。
系统在局部变量进入作用域时为其分配内存空间,并在离开作用域时自动释放这些内存空间,
从分配内存空间开始到释放内存空间之间的这段时间就是该局部变量的生命期。因此,对于局部
变量来说,作用域和生命期是一致的。
例如,
int f(int a)
a、b的作用域,在函数f( )中可用,在函数
{
int b; main()中不可用

if(a<0)
c的作用域,仅在if语句中可用
{
int c=0;

}

}

int main(int x) x、y的作用域,在函数main( )中可用,在


{
函数f()中不可用
int y;

}

85
C 语言程序设计(第 2 版)

给出了两种局部变量的定义位置。在函数 f( )的范围内 a、b 有效,在函数 main( )的范围内 x、y


有效,而变量 c 仅在 if 语句中有效。
通常将在整个函数中都有效的局部变量称为函数级变量,将仅在某个复合语句有效的局部变
量称为块级变量。

M提醒
函数的形参被当作该函数的函数级变量来处理。

局部变量只在自己的作用域内有效,因此在不同的作用域(如不同的函数)内可以
用相同的标识符定义不同的变量,系统会为这些同名变量分配不同的内存空间,不会发
生混淆。

【例 4-10】 分析以下程序的运行结果,注意局部变量的定义与使用。
程序:
#include <stdio.h>
int sum(int a,int b);
int main()
{
int x, y;
int iSum; /* 局部变量,在 main()中有效 * /
scanf("%d,%d",&x,&y);
iSum = sum(x,y);
printf("sum = %d\n",iSum);
return 0;
}
int sum(int a,int b)
{
int iSum; /* 局部变量,在 sum()中有效 */
iSum = a + b;
return iSum;
}
运行结果如下:

在主函数 main( )和函数 sum( )中都定义了一个标识符为 iSum 的局部变量,但是这


两个变量的作用域不同,它们之间互不干扰。

4.6.2 全局变量
全局变量是定义在函数外部的变量,程序中的任何函数都可以使用全局变量,它的作用域是
从变量定义处开始到程序的结束处。
系统在全局变量进入作用域时为其分配内存空间,并在程序执行完毕后自动释放这些内存空
间。因此,与局部变量相同,全局变量的作用域和生命期也是一致的。

86
第 4 章 函数

【例 4-11】 分析以下程序的运行结果,注意全局变量的定义与使用。
程序:
#include <stdio.h>
float fMax = 0, fMin = 0; /* 定义全局变量 */
float average(float fScore[], int n);
int main()
{
float fAvg, fScores[5];
int i;
printf("Please input five numbers: ");
for(i = 0; i < 5; i++)
scanf("%f", &fScores[i]);
fAvg = average(fScores, 5);
/*在函数 main()中使用全局变量*/
printf("max = %6.2f, min = %6.2f, average = %6.2f\n", fMax, fMin, fAvg);
}
float average(float fScore[], int n)
{
int i;
float fAver, fSum = fScore[0];
fMax = fMin = fScore[0]; /*在函数 average()中使用全局变量* /
/* 求最高分、最低分和总分 * /
for(i = 1; i < n; i++)
{
if(fScore[i] > fMax)
fMax = fScore[i];
else if(fScore[i] < fMin)
fMin = fScore[i];
fSum = fSum + fScore[i];
}
fAver = fSum/n;
return fAver;
}
运行结果如下:

变量 fMax 和 fMin 在函数 main( )和 average( )的外部定义,都是全局变量,由于它


们在程序的顶部进行定义,因此这两个函数都可以使用它们,如果在一个函数中改变了
它们的值,在其他函数中可以使用这个已改变的值。

(1)全局变量的定义既可以放在程序的顶部,也可以在程序中的其他位置,只要不
放在函数内部即可。一般的原则是:如果某个全局变量允许所有的函数使用,则放在顶
部,否则根据实际情况放在合适的位置。
(2)当全局变量和某个函数的局部变量同名时,局部变量优先,即局部变量将隐藏
全局变量,该函数内使用的这个同名变量是局部变量,不是全局变量。因此,尽量不要
使用与全局变量同名的局部变量。

87
C 语言程序设计(第 2 版)

例如:
定义了一个全局变量y
int y;
int f(int a)
{
int b;

if(a<0)
{ 全局变量y赋值为0
y = 0;

}

}
int main(int x)
{ 定义了一个局部变量y
int y;
y = 1;
… 局部变量y赋值为1
}

M技巧
在一个实际应用中,不要无条件地放大变量的作用域,也就是说,定义块级变量就能解决
的问题不要通过定义函数级变量来实现,定义函数级变量就能解决的问题不要通过定义全局
变量来实现。这样,不仅可以有效地避免变量的人为误用,而且还可以使内存空间的使用效
率更高。

4.7 变量的存储类型
变量定义后,系统会在内存中为其分配相应大小的空间用以保存变量的值,默认情况下,局
部变量在动态分配区(如栈区)分配,全局变量在静态分配区分配。动态分配区中内存单元的管
理采用动态方式进行,即在程序运行期根据需要动态地进行分配和释放;静态分配区中内存单元
的管理采用静态方式进行,即在程序运行期间分配固定的存储空间,直到程序结束才释放。
C 语言规定,在变量定义时还可以通过指定变量的存储类型来改变变量分配的方式。所谓变
量的存储类型就是指存储变量时的方式(动态或静态),也就是说变量的存储类型能够决定变量占
用的存储空间在动态分配区还是在静态分配区分配。C 语言提供了 4 种存储类型修饰符:auto(自
动变量)、static(静态变量)
、register(寄存器变量)和 extern(外部变量),在定义变量时放在类
型说明符的前面。

4.7.1 自动变量
只有局部变量的定义可以使用修饰符 auto,用于指定存储类型为动态方式,变量的值在动态

88
第 4 章 函数

存储区分配。
例如,下面的程序段
int fun(int a)
{
auto int c = 3;
...
}
定义了一个 int 型的自动变量 c,初值为 3。程序执行到语句“auto int c = 3;”时系统在动态分配
区为其分配 2 个字节的内存空间,执行完函数 fun( )后,系统会自动释放 c 所占的存储单元。因此,
变量 c 只在函数 fun( )中可用。
在变量声明的时候可以省略修饰符 auto,而且一般都采用省略的形式。前面介绍的函数中定
义的变量都没有声明为 auto,其实都隐含指定为自动变量。
例如,在函数体中:
auto int a,b = 5;

int a,b = 5;
是等价的。

4.7.2 静态变量
局部变量和全局变量的定义都可以使用修饰符 static,用于指定存储类型为静态方式,变量所
占的存储空间在静态存储区分配。因此,静态变量有两种:静态局部变量和静态全局变量。
1.静态局部变量 带格式的: 项目符号和编号
下面的程序段
int fun(int a)
{
static int c = 3;
...
}
在函数 fun( )中定义一个 int 型的静态局部变量 c,初值为 3。
需要强调的是,静态局部变量也是局部变量,它的作用域仅限于函数或复合语句内,但是,
由于它的静态特性,它不会随着函数或复合语句的结束而消失,因此,它的生命期会一直持续到
程序执行结束。

静态局部变量的初值不是在运行期赋值的,而是在编译期赋值的,因此静态局部
变量的初值只在编译期赋值一次,如果变量定义时进行了初始化,则存储该值;如果
变量定义时没有进行初始化,则系统自动存储 0 值或空字符(对于字符变量)。在程序
运行的过程中,每次调用函数时不再重新赋初值,而是引用上次函数调用结束时该变量
的值。

【例 4-12】 分析以下程序的运行结果,注意静态局部变量的使用。
程序:
#include <stdio.h>
int main()
{
int i;

89
C 语言程序设计(第 2 版)

void fun(int b); /* 在函数中进行函数声明 */


for(i= 1; i <= 3; i++)
fun(i);
return 0;
}
void fun(int b)
{
static int a = 0; /* 静态局部变量的定义 */
a = a + b; /* 静态局部变量的使用 */
printf("a + b = %d\n", a);
}
运行结果如下:

函数调用过程中 a、b 的值如表 4-1 所示。

表 4-1 调用 fun( )函数过程中 a、b 的值


a b a=a+b
第一次调用 fun() 0 1 1
第二次调用 fun() 1 2 3
第三次调用 fun() 3 3 6

思考一下,如果将例 4-12 中的变量 a 的定义改为“int a=0;”,运行结果会怎样?


2.静态全局变量
下面的程序段
static float f; /*静态全局变量*/
fun()
{
int i,j;

}
中定义了一个 float 型的静态全局变量 f。
与静态局部变量类似,静态全局变量在定义的同时如果没有初始化,则系统会自动赋 0 值或
空字符。因此,上例中的变量 f 的初值为 0。
需要强调的是,如果一个 C 程序由多个源文件组成,则静态全局变量的作用域是定义它的源
文件,也就是说仅在定义它的源文件中有效,其他源文件不能使用。但它的生命期会一直持续到
程序结束。因此,静态全局变量经常用在多个文件组成的程序中。

与自动变量相比,静态局部变量延长了变量的生命期;而与全局变量相比,静态全
局变量缩小了变量的作用域。

90
第 4 章 函数

M提醒
静态变量在定义时若没有进行初始化,则系统自动赋 0 值或空字符;而自动变量若没有进
行初始化,其初值是不确定的。

4.7.3 寄存器变量
一般情况下,变量的值是存储在内存中的。由于 CPU 对寄存器中数据的访问速度要比对内存
中数据的访问速度快得多,因此为了提高变量的存取速度,对于某些需要频繁使用的变量,可将
这些变量直接存储在寄存器中。为了实现上述功能,C 语言引入了寄存器变量,使用的存储类型
修饰符为 register。对于循环次数较多的循环控制变量及循环体内反复使用的变量均可定义为寄
存器变量。
例如:
register int x, y, z;
定义了三个 int 型的寄存器变量 x、y 和 z。
寄存器变量是局部变量,采用动态存储方式进行寄存器的分配与释放。因此,它的作用域和
生命期都与自动变量相同。
需要强调的是,当把函数的形参或自动变量定义为寄存器变量时,只有在发生函数调用时才
会分配寄存器,如果此时没有可用的寄存器可供分配,则会在动态分配区(栈区)为其分配相应
大小的内存空间。
【例 4-13】 编程计算 12 + 22 + L + n 2 。
程序:
#include <stdio.h>
int main()
{
long sum = 0;
int n;
register int i; /*寄存器变量 i 作为循环变量*/
printf("please input the number(n):\n");
scanf("%d",&n);
for(i = 1; i <= n; i++)
sum += i * i;
printf("sum = %ld\n", sum);
return 0;
}
运行结果如下:

寄存器变量的使用仅对程序的运行效率有影响。

91
C 语言程序设计(第 2 版)

M提醒
(1)凡需要采用静态存储方式的变量都不能定义为寄存器变量。
(2)由于 CPU 中寄存器的个数是有限的,因此定义寄存器变量的个数也是有限的。

4.7.4 外部变量
在函数外定义的变量就是外部变量,类型修饰符为 extern。因此外部变量就是全局变量,它
的作用域和生命期与全局变量完全相同。需明确,外部变量和全局变量指的是同一类变量,但是,
全局变量是从作用域的角度提出的,外部变量是从存储方式的角度提出的。
在定义外部变量时通常省略修饰符 extern,例如:
int i,j;
int fun()
{
int x,y;

}
定义了两个 int 型的外部变量 i 和 j。
需要强调的是,如果外部变量的定义在后,使用在前,或者要引用其他源文件中定义的外部
变量,则必须用 extern 对该变量进行外部说明。
外部说明的一般形式如下:
extern 类型说明符 变量名表;
例如:
/* 源文件 tc1.c */
#include <stdio.h> 在源文件 tc1.c 中定义外部变量 a
int a;
int f( );
void main( )
{
… 在源文件 tc1.c 中使用变量 a
a = 1;
m = f( );
}

/* 源文件 tc2.c */
#include <stdio.h> 在源文件 tc2.c 中对变量 a 进行外部说明
extern int a;
f( )
{
… 在源文件 tc2.c 中使用变量 a
a++;

}
本例表明,如果想在外部变量所在的源文件之外的其他源文件中使用该外部变量,需要进行
外部说明。
即使在同一个源文件中,如果外部变量定义在某些函数的后面,则在变量定义语句前的函数
也不能直接使用该变量。若要使用,需要在使用前进行外部说明。

92
第 4 章 函数

【例 4-14】 分析以下程序的运行结果,注意外部变量的定义与外部说明的使用。
程序:
#include <stdio.h>
int max(int x, int y)
{
int z;
z = x > y ? x : y;
return(z);
}
int main( )
{
extern int a, b; /* 对变量 a 和 b 进行外部说明 */
printf("max = %d\n", max(a, b));
return 0;
}
int a = 13,b = -8; /* 定义外部变量 a 和 b * /
运行结果如下:

例 4-14 中,外部变量 a 和 b 定义在程序末尾处,由于外部变量定义的位置在主函数


main( )之后,因此,需要在主函数 main( )中对 a 和 b 进行外部说明,这样,外部说明开
始之后的语句才可以合法地使用外部变量 a 和 b。

使用 extern 进行外部说明时,数据类型符可以省略。例如,
“extern int a;”可以写成
“extern a;”。

4.8 综 合 实 例
【例 4-15】 编程实现一个简单计算器,具有+、–、*、/的功能。要求:
(1)定义四个函数分
别实现+、–、*、/运算;(2)能够识别输入的运算符号,自动完成相应的运算。
分析:
可以将本题分解为两个子问题,分别是:
(1)接收用户输入的操作数和运算符号;
(2)调用
相应的函数完成计算,并输出计算结果。对于第一个子问题,需要根据输入的运算符号决定要进
行的运算,可采用 switch 语句实现。对于第二个子问题,需要定义四个函数分别完成求和、求差、
求积、求商的功能,并将计算结果返回给主函数,因此这些函数都有返回值,而且都要有两个形
参分别接收参与算术运算的操作数。
程序:
#include <stdio.h>
float add(float x, float y);
float sub(float x, float y);
float mul(float x, float y);

93
C 语言程序设计(第 2 版)

float div(float x, float y);


int main()
{
float a,b,c; /* a,b 为操作数,c 计算结果*/
char ch; /* ch 用来接收运算符号*/
printf("Please input two number: ");
scanf("%f,%f",&a,&b);
getchar( );
printf("Please input the operator: ");
ch = getchar( );
/*根据 ch 的取值来决定调用相应的子函数*/
switch(ch)
{
case '+': c = add(a, b); printf("%f + %f = %f\n",a,b,c); break;
case '-': c = sub(a, b); printf("%f - %f = %f\n",a,b,c); break;
case '*': c = mul(a, b); printf("%f * %f = %f\n",a,b,c); break;
case '/': c = div(a, b); printf("%f / %f = %f\n",a,b,c); break;
default: printf("can not compute! ");
}
return 0;
}
/*定义 add 函数,实现两个数的求和运算*/
float add(float x, float y)
{
float z;
z=x+y;
return z;
}
/*定义 sub 函数,实现两个数的减法运算*/
float sub(float x, float y)
{
float z;
z=x-y;
return z;
}
/*定义 mul 函数,实现两个数的乘法运算*/
float mul(float x, float y)
{
float z;
z=x*y;
return z;
}
/*定义 div 函数,实现两个数的除法运算*/
float div(float x, float y)
{
float z;
z=x/y;
return z;
}

94
第 4 章 函数

运行结果如下:

4.9 深入研究:递归的设计与使用问题
在解决一个比较复杂的问题,特别是解决一个规模较大的问题时,常常将问题进行分解,就是
将一个规模较大的问题分割成规模较小的同类问题,然后将这些小的子问题逐个加以解决,最终使
整个大的问题得以解决。这种分而治之的思想称为分治的思想。采用分而治之的策略,不断缩小问
题的规模,直到这个小问题能够直接求解,这就是递归的思想,在很多情况下,对于可以用分治的
思想解决的问题,都可以采用递归方式来编程实现,递归思想是一种常见的算法设计思想。
前面的学习告诉我们,如果采用递归思想求解问题,最重要的就是利用分治的思想对问题进
行分析,找出规律,设计出数学模型。只要设计出递归的数学模型,递归函数很容易编写。
在设计递归算法时要注意以下几点。
(1)每个递归函数都必须有一个非递归定义的初始值,作为递归结束标志,或递归结束的出
口。如果一个递归算法没有这个非递归定义的初始值,该递归函数就会不断地调用下去,无法正
常结束。
(2)在设计递归算法时,要解决的问题需具有递归性,所谓递归性就是一种反复调用自身过
程的特性。
【例 4-16】 用递归方法求解 xn。
分析:
对于 xn 的计算,数学公式为 xn=x×xn−1,显然它符合分治的思想,可以用递归方式来实现。这
里用符号 f(x, n)来表示 xn 的值,可以建立这样的递归关系:f(x, n)=x×f(x,n−1)。接下来的重要任务
就是找到递归出口,即非递归定义的初始值。当 n 的值递减到 0 时,f(x,0)=x0,此时 f(x, 0)的值可
直接计算出结果为 1。
这样便找到了该问题的递归模型:
1 n=0
f (x, n) = 
 x × f (x, n − 1) n>0
递归函数 f( )的定义:
int fun(int x,int n)
{
if(n==0)
return 1;
else
return(x * fun(x, n-1));
}
【例 4-17】 编写一个程序,实现分解质因数。
分析:
根据数论的知识可知,任何一个合数都可以写成几个质数相乘的形式,这几个质数都叫做这

95
C 语言程序设计(第 2 版)

个合数的质因数。例如, 24 = 2 × 2 × 2 × 3 。把一个合数写成几个质数相乘的形式,叫做分解质因
数。显然,对于一个质数,它的质因数就是这个质数本身,因此无须分解。
这样,分解质因数的问题可以归纳为如下几个方面:
(1)如果数 n 为质数,则直接输出该数;
(2)如果数 n 为合数,则在 2~n−1 之间找出 n 的两个因数(不一定是质因数)i 和 j;
(3)如果 i 是质数,则 i 一定是 n 的一个质因数;
否则继续对 i 进行质因数分解;
(4)如果 j 是质数,则 j 一定是 n 的一个质因数;
否则继续对 j 进行质因数分解。
显然,可以用递归方式进行求解。在寻找数 n 的因数时,通常可以从 2 开始,因为 2 是质数,
如果数 n 能被 2 整除,则 2 一定是该数的质因数,接着,判断 n 除以 2 的结果是否是质数,如果
是则程序结束,否则继续除以 2 进行判断,直到不再整除为止。此时,说明数 n 所有为 2 的质因
数都已经找到。然后依次对 3,4,5,…,n−1 重复上述过程。
递归模型如下:

输出i n%i = 0

Factorization(n) = 输出n/i n/i是质数
Factorization(n/i) n/i不是质数

这里,i 的值从 2 开始,直到 n−1 为止。
递归算法描述:Factorization(n)
(1)i←2;
(2)循环执行以下语句,直到 i = n − 1。
如果 n % i == 0,则
1)输出 i;
2)如果 n / i 是质数,则输出 n / i;
否则 Factorization(n / i)。
程序:
#include<stdio.h>
void Factorization(int n);
int isPrime(int a);
int main()
{
int n;
printf("Please input a integer for getting Prime factor:\n");
scanf("%d", &n);
Factorization(n);
printf("\n");
return 0;
}
void Factorization(int n)
{
int i;
if(isPrime(n))
printf("%d ", n);

96
第 4 章 函数

else
{
for(i = 2; i <= n - 1; i++)
if(n % i == 0)
{
printf("%d ", i);
if(isPrime(n/i))
{
printf("%d ", n/i);
break;
}
else
Factorization(n/i);
break;
}
}
}
int isPrime(int a)
{
int i;
for(i = 2; i <= a - 1; i++)
if(a % i == 0)
return 0;
return 1;
}
运行结果如下:

本章小结
函数是构成 C 程序的基本单位,任何 C 程序都是由一个或多个函数组成的,因此也有人将 C
语言称为函数式语言。函数是 C 语言实现自顶向下、逐步细化和模块化结构的前提,针对一个实
际问题,程序设计者通常采用分解的思想将大问题拆分成小问题,小问题拆分成更小的问题,直
到该问题能够很容易地求解出来;然后将每个子问题的求解定义成一个函数,利用函数之间的调
用构成完整的 C 程序。可以说,C 程序一般是由大量的函数构成的,即“小函数构成大程序”

主函数是 C 程序执行时的入口,函数名为 main( ),因此一个 C 程序有且仅有一个主函数,其
他函数可以有任意多个,这些函数的位置也没有限制,可以在 main( )函数前,也可以在其后。在
调用函数时,如果被调用的函数在调用它的函数之前定义,则可以直接调用;如果被调用的函数
在调用它的函数之后定义,那么就应先进行函数声明,才能够正确调用。
在函数调用的过程中,可以通过参数的传递,在主调函数和被调函数之间实现数据的交换。
参数传递采用“单向值传递”的方法,即仅能将实参传递给形参,而不能将形参传递给实参。当
函数的形参是简单类型时,参数传递的方式称为传值方式,也就是将实参的值传递给形参,在函
数内对形参的运算不会影响实参的值。

97
C 语言程序设计(第 2 版)

C 语言中,变量定义的位置可以在函数内,也可以在函数外,根据变量定义的位置决定变量
的作用域,变量的作用域是指可以引用变量的区域。变量还存在生命期,根据变量的存储类型决
定变量的生命期,变量的存储类型标识变量的内存分配方式是静态分配还是动态分配,变量的生
存期是指变量存在的期限,即从系统为变量分配存储空间开始到将其回收为止。
明确了变量的作用域和生命期的概念,对函数参数的传递方式与过程会有更深入的理解。实
际上,系统将函数的形参作为函数内的局部变量,在参数传递的过程中,系统首先在动态分配区
为形参分配内存空间,然后将实参的值存储在里面,因此,在被调函数内对形参的处理改变的是
这片内存空间中的值,而不是实参所在内存空间中的值,这就是传值方式不影响实参值的原因。

习 题
【复习】
1.在下列关于 C 函数定义的叙述中,正确的是( )。
A.函数可以嵌套定义,但不可以嵌套调用
B.函数不可以嵌套定义,但可以嵌套调用
C.函数不可以嵌套定义,也不可以嵌套调用
D.函数可以嵌套定义,也可以嵌套调用
2.以下描述错误的是( )

A.调用函数时,实参可以是表达式
B.调用函数时,实参与形参可以共用内存单元
C.调用函数时,将为形参分配内存单元
D.调用函数时,实参与形参的类型应该一致
3.在一个源文件中定义的全局变量的作用域为( )。
A.本文件的全部范围
B.本程序的全部范围
C.本函数的全部范围
D.从定义该变量的位置开始到本文件结束为止
4.凡是函数中未指定存储类别的局部变量,其隐含的存储类别为( )

A.自动(auto)
B.静态(static)
C.外部(extern)
D.寄存器(register)
5.递归函数 f(n)=f(n−1)+n(n>1)的递归体是( )

A.f(1)=0
B.f(0)=1
C.f(n)=f(n−1)+n
D.f(n)=n
6.下列程序的输出结果是( )。
#include "stdio.h"

98
第 4 章 函数

fun(int a,int b,int c)


{
c = a * b;
}
int main( )
{
int c;
fun(2,3,c);
printf("%d\n", c);
return 0;
}
A.0 B.1 C.6 D.无法确定
7.下列程序的输出结果是( )。
#include "stdio.h"
int x=1;
func(int x)
{
x=3;
}
int main( )
{
func(x);
printf("%d\n",x);
return 0;
}
A.3 B.1 C.0 D.无法确定
8.阅读下面的程序:
#include "stdio.h"
int m=5;
int f1( )
{
m = m + 3;
return(m);
}
int main( )
{
int m = 3;
{
int m = 10;
m++;
}
f1( );
m += 1;
printf("%d\n", m);
return 0;
}
该程序的运行结果是: 。
9.定义一个递归函数实现 f=1+1/2+1/3+…+1/n 运算,该递归函数的递归出口是 ,递
归体是 。
【应用】
1.设计一个函数,用于把数字字符串转换成整数,这里假设数字字符串表示的整数为正数。 带格式的: 项目符号和编号

99
C 语言程序设计(第 2 版)

2.编写函数 int fullNumber(int value),其功能是如果参数是完数则返回该数。完数是指该数


等于其所有真因子(不包括其本身)之和。例如,6 的因子有 1、2、3,且 6=1+2+3,故 6 为完数。
3.调用函数输出如下杨辉三角形。
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
4.用递归方法计算 x 的 n 阶勒让德多项式的值。递归公式如下:
1 (n=0)
Pn(x)= x (n=1)
((2n−1)*x*Pn-1(x) − (n−1) *Pn-2(x))/n (n>1)
【探索】
1.下面是一个 5×5 螺旋方阵,编程生成 n×n 的螺旋方阵。
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
2.用递归函数法求菲波那契数列。
3.给出年、月、日,计算该日是该年的第几天。

100
第 章 5
数组

本章目标
◇ 了解数组的基本概念和用途
◇ 掌握数组的定义、初始化和引用方法
◇ 熟练掌握一维数组、二维数组和字符数组的使用
◇ 学会恰当应用数组编写程序解决实际问题

5.1 为什么要使用数组
在实际问题中,经常需要对一系列性质相同的信息进行某种处理。例如,学校在期末考试结
束后,要根据总分对学生成绩从高到低进行排序。在这个问题中,每个学生的总分都是整数。
如果用 C 语言编写一个程序解决这个问题,首先想到的就是要把每个学生的总分都输入计算
机并保存起来。因此,必须为每个学生的总分定义一个单独的整型变量,用于存放其数值, 程序
代码示例如下:
printf("Input score 1\n");
scanf("%d",&score1);
printf("Input score 2\n");
scanf("%d",&score2);

printf("Input score 100\n");
scanf("%d",&score100);

输入了所有成绩之后,可以使用一系列 if 语句在这些成绩之间进行比较,找出其中最大的数、
次大的数,以此类推,直到找出这组成绩中最小的数,就完成了总分的从高到低的排序。显然,
随着学生人数的增多,采用这种想法写出的程序会相当庞大和复杂,程序的可维护性也很差。在
数学上通常用 xi 表示一系列变量,随着 i 值的变化来表示不同的变量。那么我们也可以用一个标
识符(如 score)而不是定义多个变量(如 score1,score2,…,score100 等)来存储每个学生的
总分,与数学的表示法类似,为这个标识符定义下标,根据下标的值来识别每个学生。由于这些
数据的类型相同,系统可以为标识符 score 分配多个连续的内存单元,将每个学生的总分分别存
储到下标所对应的单元中。学生成绩在内存中的存储示意图如图 5-1 所示。
采用这种方式对若干个具有相同类型的数据进行存储,就会使类似问题的处理变得既简单又
灵活。这种新的数据类型就是数组。

101
C 语言程序设计(第 2 版)

数组是具有相同数据类型的若干变量的集合体,这些变量拥有一个共同的名字,称为数组名,
每个变量都是数组中的一个元素,称为数组元素,使用数
组时通过下标来区分数组中的不同元素。在内存中,数组
均由连续的内存单元组成,最低地址对应于第一个数组元 1 56
素,最高地址对应于最后一个数组元素。 score: 2 87
因此,数组是一种构造数据类型。一个数组中包含多
个数组元素,这些数组元素的数据类型相同,既可以是基 M M
本数据类型,也可以是构造数据类型。从数组元素类型的 100 96
角度,可以把数组分为 int 型数组、float 型数组、char 型
M
数组和指针数组等;从数组下标维数的角度,又可以把数
组分为一维数组、二维数组和多维数组。 图 5-1 学生成绩存储示意图

5.2 一 维 数 组

5.2.1 一维数组的定义
在C语言中,数组与变量一样,遵循“先定义,后使用”的原则。
一维数组定义的一般形式如下:
类型说明符 数组名[常量表达式];
其中,类型说明符表示数组元素的数据类型。数组名是 C 语言合法的标识符,代表该数组的首地
址,即数组的第一个元素在内存中的存储地址,整个数组存放在以首地址开头的一块连续的内存
单元中,每个数组元素根据其数据类型占用相同大小的内存空间。方括号是数组标志,方括号中
的常量表达式的值表示数组元素的个数,也称为一维数组的长度。
因此,存储一个一维数组所需要的内存字节数可以用下列方式来计算:
总字节数 = sizeof(类型) × 数组长度
例如,语句“int score[100];”定义了一个数组,数组名为 score,数组元素为 int 型,数组长
度为 100。由于存储一个 int 型变量占用 2 个字节,因此,数组 score 在内存中占用的一段连续的
内存空间的大小为 sizeof(int)×100=2×100=200 个字节。

M提醒
(1)数组名之后是一对方括号,不能写成圆括号。
(2)在同一作用域内,数组名不能与其他变量名相同。
(3)定义数组时,数组长度必须用常量或常量表达式,不能用变量或包含变量的表达式。

例如:
int score;
float score[10];
是不合法的,因为已定义了一个名字为 score 的变量,不允许再定义一个名字为 score 的数组。
int n = 5;
int score[n];

102
第5章 数组

是不合法的,因为 n 是变量,不是常量。但是,下面程序:
#define N 5
int score[3+2], grade[7+N];
是合法的,因为 N 是整型常量,7+N 是整型常量表达式。
特别提示:
在 C89 标准中,不允许对数组长度做动态定义。而 C99 标准则允许定义变长数组,该数组的
长度不在编译时确定,而在运行时确定。

5.2.2 一维数组的引用
在 C 语言中,对数组的访问通常是通过对数组元素的引用来实现的,不能一次引用整个数组,
也就是说,不能把一个数组当做整体进行相应的运算,如赋值运算、算术运算和关系运算等(字
符数组的输入输出除外)

一维数组元素引用的一般形式如下:
数组名[下标]
其中,下标表示该数组元素在数组中的顺序号,可以是整型常量、整型变量或整型表达式。在 C
语言中,数组的下标从 0 开始,合理取值范围为 0~(数组长度−1)。
例如:
int array_data[5], i;
i = 3;
array_data[0] = 45;
array_data [i−2] = array_data [0] + 2;
定义了一个有 5 个元素的 int 型数组 array_data,数组的第一个元素 array_data[0]赋值为 45,由于
i−2=1,数组的第二个元素 array_data[1]=45+2=47。

M提醒
(1)定义数组时,方括号中表达式的值表示数组长度,即数组元素的个数;引用数组元素时,
方括号中表达式的值表示该数组元素的顺序号,即在数组中的位置。初学者一定要分清。例如,
int score[100],是一个具有 100 个元素的 int 型数组,可以引用的数组元素是 score[0] ~score[99]。
(2)定义数组时,方括号中必须是常量;引用数组元素时,方括号中可以是变量,而且通
常是变量。

C 语言不允许将数组作为一个整体参与相关运算(如赋值运算)。

例如,下面的语句段:
int array_a[5], array_b[5];
array_a = array_b;
是错误的。
【例 5-1】 从键盘输入 10 个数,分别按正序和逆序将其输出。
分析:
数组元素的正序输出就是从数组的第一个元素开始逐个输出,直到将最后一个元素输出完毕;
数组元素的逆序输出与正序输出正好相反。

103
C 语言程序设计(第 2 版)

程序:
#include <stdio.h>
int main()
{
int array_data[10], i;
printf("Input 10 numbers:\n");
/* 接收键盘输入的 10 个数,存入一维数组 array_data 中 */
for(i = 0; i <= 9; i++)
scanf("%d", &array_data[i]);
/* 正序输出一维数组 a */
for(i = 0; i <= 9; i++)
printf("%2d", array_data[i]);
printf("\n");
/* 逆序输出一维数组 a */
for(i = 9; i >= 0; i--)
printf("%2d", array_data[i]);
printf("\n");
return 0;
}
运行结果如下:

【例 5-2】 从键盘输入 10 个数后将其全部输出,并找出其中最大的数。


分析:
求最大值问题是很常见的一个数学问题,在数据较多的情况下,采用例 3-9 的实现方法显然
不合适,因此需要采用一种与数据个数无关的通用的求解方法。本题的求解可分为两个子问题:
(1)处理多个数据的输入输出操作,可采用例 5-1 的方法实现;(2)寻找最大值并输出。通过对
数组元素的依次比较,找出其中的最大值,这是本题的关键。
可以在这个数组中任意指定一个数组元素,把它暂时看做这个数组中的最大值,然后用数组
中的每一个元素与它相比,如果比它大,则替换它,当数组中的所有元素都比较完毕,就找到了
最大值。为了方便起见,可以先把数组中的第一个元素看做最大值,存储在一个变量中(一般命
名为 max),这样就可以从第二个元素开始逐一比较。
为了算法的通用性,可以定义一个符号常量 N 来表示数组长度。
算法描述:
(1)定义一个符号常量 N,令 N 为 10;
(2)输入 10 个数存储到已定义的数组 array_data 中;
(3)输出数组 array_data 中的全部元素;
(4)令 max ← array_data [0];
(5)循环变量 i ← 1,循环条件 i < N,循环执行以下语句:
如果 array_data [i] > max,则 max ← array_data [i];
(6)输出 max 的值。
程序:
#include <stdio.h>
#define N 10
104
第5章 数组

int main()
{
int array_data [N], i, max;
printf("Input %d numbers:\n",N);
for(i= 0; i < N; i++)
scanf("%d", & array_data[i]);
for(i = 0; i < N; i++)
printf("%5d", array_data[i]);
printf("\n");
/*找出数组中的最大值存入 max 中*/
max = array_data[0];
for(i = 1; i < N; i++)
if(array_data[i] > max) max = array_data[i];
printf("maximum = %d\n", max);
return 0;
}
运行结果如下:

采用这种求最值的方法,先找出整个数组中的最大值,然后在余下的数组元素中再次求最值,
就找到了整个数组中的次大值,一直重复这个过程,就可以实现对数组元素的从大到小的排序。
不妨尝试一下。
【例 5-3】 对 10 个数排序,排序方式为升序。
分析:
排序的方法有多种,冒泡排序法是其中之一,一般分为“上浮法”和“下沉法”两种。本题
采用“下沉法”完成数据的排序,基本思想是:从第一个数开始,对相邻的两个数进行比较,通
过交换使较大的数在后,直到最后两个数比较完毕。完成一遍这样的比较过程(称为一趟排序)

就找到了这组数据中的最大值并且下沉到最后一个位置。对余下的数据重复这个比较过程,当只
有一个剩余数据时就完成了排序工作。
下面以 5 个数为例进行描述。首先从第一个数据开始,在所有的数据中采用上述方法求最值。
具体做法是:第一次比较第一个数和第二个数,即比较 9 和 7,因为 9>7,交换 9 和 7 的位置。
第二次比较第二个数和第三个数,即 9 和 4,交换 9 和 4 的位置,依次类推,一共进行了 4 次比
较和交换,得到了第 1 趟排序的结果 7-4-6-2-9,可以看出,经过了一趟排序,最大的数已经“沉
底”了,具体过程如图 5-2 所示。
9 9 7 7 7 7

7 7 9 4 4 4

4 4 4 9 6 6

6 6 6 6 9 2

2 2 2 2 2 9

初始 第1次 第2次 第3次 第4次 结果

图 5-2 “冒泡排序法”第 1 趟排序过程示意图

进行第 2 趟排序,对余下的 4 个数重复上述过程,如图 5-3 所示。

105
C 语言程序设计(第 2 版)

7 7 4 4 4

4 4 7 6 6

6 6 6 7 2

2 2 2 2 7

初始 第1次 第2次 第3次 结果 错误!

图 5-3 “冒泡排序法”第 2 趟排序过程示意图

四趟排序完成后,有序的数据是 4 个,无序的剩余数据仅有 1 个,无须再进行排序了,此时排序


过程终止,实现了对 5 个数据的排序。因此,如果对 n 个数进行排序,仅需进行 n−1 趟排序过程。显
然,该排序过程需用双重循环,外层循环控制排序的趟数,内层循环控制参与排序的数据的范围。
算法描述:
(1)定义符号变量 N,令 N 为 10;
(2)定义一个数组 array_sort[N],接收键盘输入的 N 个数;
(3)循环变量 i ← 0,循环条件 i < N−1,循环执行以下语句
循环变量 j ← 0,循环条件 j < N−1−i,循环执行以下语句
如果 array_sort[j] > array_sort[j+1],则 array_sort[j] ←→ array_sort[j+1];
(4)输出数组 array_sort。
程序:
#include <stdio.h>
#define N 10
int main()
{
int array_sort[N];
int i, j, t;
printf("Input %d numbers:\n",N);
for(i = 0; i < N; i++)
scanf("%d", &array_sort[i]);
printf("\n");
for(i = 0; i < N-1; i++)
for(j = 0; j < N-1−i; j++)
if(array_sort[j] > array_sort[j+1])
{
t = array_sort[j];
array_sort[j] = array_sort[j+1];
array_sort[j+1] = t;
}
printf("The sorted numbers:\n");
for(i = 0; i < N; i++)
printf("%5d", array_sort[i]);
printf("\n");
return 0;
}
运行结果如下:

106
第5章 数组

5.2.3 一维数组的初始化
在定义一维数组的同时给数组元素赋初值称为一维数组的初始化。
一维数组初始化的一般形式如下:
类型说明符 数组名[常量表达式] = {值 1,值 2,…,值 n};
其中,等号之前是一维数组定义的一般形式,花括号“{}”中给出的数值是对应数组元素的初值,各
个值之间用逗号分隔。
例如:语句“int score[10] = {0,1,2,3,4,5,6,7,8,9};”对数组 score 进行初始化。相当于
score [0] = 0;
score [1] = 1;

score [9] = 9;
这种初始化方式称为全部初始化。进行全部初始化时,可以不指定数组长度。对上述数组 score
全部初始化,也可以写成语句“int score [] = {0,1,2,3,4,5,6,7,8,9};”,省略数组长度 10。
C 语言也允许部分初始化。即初值列表中值的个数少于数组元素的个数。这时从数组的第
一个元素开始,用初值列表中给定的初值进行对应元素的初始化,对于没有给定初值的数组元
素,系统自动赋零值。例如,语句“int score [10] = {0,1,2,3,4};”对数组 score 进行初始化。相
当于
score [0] = 0;
score [1] = 1;

score [4] = 4;
score [5] = 0;

score [9] = 0;
需要强调的是,给定的初值个数不能大于数组的长度,否则在编译时会出现错误。因为对数
组初始化时,编译器对数组下标进行越界检查,但引用数组元素时不进行越界检查。
例如:
int score[3] = {1,2,3,4,5};
是错误的。但下面的语句
int score[3] = {1,2,3};
score[7] = score[4] + 3;
不会出现编译错误,但实际运行时会发生地址的越界访问,从而可能会产生意想不到的结果。因
此编程人员使用数组时一定注意下标越界问题。

M技巧
利用部分初始化的特点,在初值列表中只给定一个初值 0,实现数组元素全部初始化为 0。
例如 int score[10] = {0};

要想把数组元素全部初始化为除 0 以外的其他值时,只能采用全部初始化的方式。

107
C 语言程序设计(第 2 版)

例如:
给数组 score 中元素全部初始化为 1,只能采用下面的形式:
int score[10] = {1,1,1,1,1,1,1,1,1,1};
如果采用部分初始化的方式,写成:
int score[10] = {1};
相当于
int score[10] = {1,0,0,0,0,0,0,0,0,0};
编程时,除了可以采用初始化的方式为数组赋初值,还可以利用循环语句给数组元素赋值,
到底选择哪种方式,取决于实际问题的需要。
例如,可用下面的循环语句为数组元素赋值:
for(i = 0; i < 5; i++)
array_data[i] = i;

5.3 二 维 数 组
5.3.1 二维数组的定义
在某些实际应用(如矩阵运算)中,采用一维数组存储矩阵中的元素,处理起来不太方便,
因此 C 语言提供了二维数组。
二维数组定义的一般形式如下:
类型说明符 数组名[常量表达式 1][常量表达式 2];
其中,常量表达式 1 表示行数,常量表达式 2 表示列数。
因此,二维数组中的数组元素的个数=行数×列数,存储一个二维数组所需要的内存字节数
可以用下列方式来计算:
总字节数 = sizeof(类型) × 行数 × 列数
例如,语句“int matrix[3][4];”定义了一个 3 行 4 列的 int 型数组 matrix,共有 3×4=12 个数
组元素,存储数组 matrix 所需的内存空间为 sizeof(int) × 3 × 4=24 个字节。
C 语言中的二维数组在概念上是二维的,有行列之分,如图 5-4 所示。但实际的硬件存储器
是连续编址的,内存单元是线性排列的。因此在一维存储器中存放二维数组,C 语言采用“按行
存储”的方式,即先存储第一行的元素,再存储第二行的元素,依次类推,如图 5-5 所示。

……
matrix[0][0]
matrix[0][1]
matrix[0][2]
matrix[0][3]
matrix[1][0]


matrix[0][0] matrix[0][1] matrix[0][2] matrix[0][3]
matrix[1][0] matrix[1][1] matrix[1][2] matrix[1][3] matrix[2][3]
matrix[2][0] matrix[2][1] matrix[2][2] matrix[2][3] …

图 5-4 二维数组的矩阵表示 图 5-5 二维数组在内存中的存储方式

108
第5章 数组

定义二维数组时,行数与列数要分别写在不同的方括号中。例如,定义二维数组时,
写成“int a[3,4];”是错误的。

5.3.2 二维数组的引用
二维数组元素引用的一般形式如下:
数组名[行下标][列下标]
其中,行下标和列下标应为整型常量、整型变量或整型表达式。行下标的合理取值范围是 0~(行
数−1),列下标的合理取值范围是 0~(列数−1)。
例如,matrix[3][4]表示数组 matrix 第 3 行第 4 列的元素,matrix[i][j]表示数组 matrix 第 i 行第 j
列的元素。

数组的第 0 行对应矩阵的第 1 行。例如,matrix[0][0]表示数组 matrix 第 0 行第 0 列的


元素,对应矩阵的第 1 行第 1 列的元素。

与一维数组相似,引用数组元素时,应注意不要越界。
【例 5-4】 用二维数组保存 4 个学习小组的数学成绩(每个学习小组 5 人)
,并输出每个学习
小组的平均成绩。
分析:
可以将本题的求解过程分成两步:存储成绩和求平均值。
第一步,存储成绩。采用二维数组存储成绩时,需根据实际问题的需要确定数组的行与列的
含义,从而确定行与列的维数。本题可以定义一个二维数组 array_score,用行表示学习小组,行
数为 4,用列表示小组中的成员,列数为 5。array_score[0][0]就是第 1 个学习小组中的第一个人的
数学成绩。
第二步,对每个学习小组的所有成员的成绩求平均值。将每个小组中的所有成员(5 个人)
的成绩累加后除以人数,就得到了该小组的平均成绩,输出该成绩即可。每个学习小组都要进行
一次这样的计算过程。采用双重循环实现该功能,外层循环控制学习小组(即数组的行)的变化,
内层循环控制小组成员(即数组的列)的变化。
算法描述:
(1)定义二维数组 array_score[4][5];
(2)循环变量 i ← 0,循环条件 i < 4,循环执行以下语句
循环变量 j ← 0,循环条件 j < 5,循环执行以下语句
接收键盘输入的数据,存储到对应的数组元素 array_score[i][j]中;
(3)循环变量 i ← 0,循环条件 i < 4,循环执行以下语句
1)sum ← 0;
2)循环变量 j ← 0,循环条件 j < 5,循环执行以下语句
sum ← sum + array_score[i][j];
3)求平均:average ← sum / 5;
4)输出变量 average 的值。

109
C 语言程序设计(第 2 版)

程序:
#include <stdio.h>
int main()
{
int i, j;
float array_score[4][5], sum, average;
/*接收学习小组成员的数学成绩,存储在二维数组 array_score 中*/
for(i = 0; i < 4; i++)
{
printf("Input students’ scores in Group %d:\n", i+1);
for(j = 0; j < 5; j++)
scanf("%f", &array_score[i][j]);
}
/*求出每个小组的平均成绩并输出*/
for(i = 0; i < 4; i++)
{
sum=0;
for(j = 0; j < 5; j++)
sum = sum + array_score[i][j];
average = sum / 5;
printf("The average score in Group%d is:%.2f\n", i + 1, average);
}
return 0;
}
运行结果如下:

5.3.3 二维数组的初始化
在定义二维数组的同时给各数组元素赋初值称为二维数组的初始化。与一维数组类似,二维
数组也分为全部初始化和部分初始化两种形式。
1.数组元素全部初始化
下面的语句
int score [3][4] = {1,2,3,4,1,2,3,4,1,2,3,4};
实现对 int 型数组 score 采用“按行方式”进行全部初始化,相当于
score[0][0]=1; score[0][1]=2; score[0][2]=3; score[0][3]=4;
score[1][0]=1; … score[1][3]=4;
score[2][0]=1; … score[2][3]=4;
赋初值后数组元素的矩阵表示形式为:
1 2 3 4 
 
1 2 3 4 
1 2 3 4 
 
需要强调的是,二维数组的初始化也可以采用“分行方式”来实现。例如,语句“int score[3][4] =
110
第5章 数组

{{1,2,3,4},{1,2,3,4},{1,2,3,4}};”的功能与前述相同。
2.数组元素部分初始化
下面的语句
int score [3][4] = {1,2,3};
是采用“按行方式”对 int 型数组 score 进行部分初始化,相当于
score[0][0]=1; score[0][1]=2; score[0][2]=3; score[0][3]=0;
score[1][0]=0; … score[1][3]=0;
score[2][0]=0; … score[2][3]=0;
而语句“int score [3][4] = {{1,2},{1,2,3,4},{1}};”是采用“分行方式”对数组 score 进行部分
初始化,相当于
score[0][0]=1; score[0][1]=2; score[0][2]=0; score[0][3]=0;
score[1][0]=1; score[1][1]=2; score[1][2]=3; score[1][3]=4;
score[2][0]=1; score[2][1]=0; score[2][2]=0; score[2][3]=0;

对数组进行全部初始化时,数组的第一维长度可以省略,但第二维长度不能省略。

例如,
int a[][4] = {1,2,3,4,1,2,3,4,1,2,3,4};
是正确的,但是语句
int a[][] = {1,2,3,4,1,2,3,4,1,2,3,4};
是错误的。

M提醒
一定注意“按行方式”和“分行方式”对二维数组进行初始化的区别。例如语句“int a[3][4] =
{1,2,3};”和“int a[3][4] = {{1},{2},{3}};”的结果是不同的。

 1 2 3 4
【例 5-5】 有一个 3 × 4 的矩阵 a 3×4 
= 9 8 7 6  ,求出该矩阵中最大元素的值以及其
 −10 10 −5 2 
所在的行数和列数。
分析:
可以将本题的求解过程分成两步:
(1)定义一个二维数组并初始化;
(2)找出该二维数组的最
大元素并记录其在矩阵中的位置。
算法描述:
(1)定义一个二维数组 array_matrix[3][4],用已知数据对其初始化;
(2)定义变量 max、max_row 和 max_column,分别存储最大值、所在行和所在列;
(3)令 max ← array_matrix[0][0],max_row ← 0,max_column ← 0;
(4)循环变量 i ← 0,循环条件 i < 3,循环执行以下语句
循环变量 j ← 0,循环条件 j < 4,循环执行以下语句
如果 max < array_matrix[i][j],则
1)max ← array_matrix[i][j];
2)max_row ← i,max_column ← j;

111
C 语言程序设计(第 2 版)

(5)输出变量 max、max_row 和 max_column 的值。


程序:
#include <stdio.h>
int main()
{
int array_matrix[3][4] = {{1,2,3,4},{9,8,7,6},{−10,10,−5,2}};
int i, j, max_row = 0, max_column = 0, max= array_matrix[0][0];
for(i = 0; i < 3; i++)
for(j = 0; j < 4; j++)
if(array_matrix[i][j] > max)
{
max = array_matrix[i][j];
max_row = i;
max_column = j;
}
printf("max = %d,row = %d,column = %d\n", max, max_row, max_column);
return 0;
}
运行结果如下:

5.4 多 维 数 组
二维数组实际上是一种最简单的多维数组。C 语言允许使用高于二维的多维数组,如三维数
组、四维数组甚至更高维数的数组,最大维数由具体的 C 编译系统决定。在实际应用中,经常用
到的是一维、二维和三维数组,四维以上的数组极少使用。
多维数组定义的一般形式如下:
类型说明符 数组名[常量表达式 1][常量表达式 2]…[常量表达式 n];
其中,维数由常量表达式的个数 n 来决定。若 n 为 3,则是一个三维数组。
引入多维数组可以使编程更为灵活,因为多维数组的每一维都可以根据实际情况而赋予不同
的含义,从而使多维数组能描述比较复杂的数据结构。例如,语句“float score[3][5][2];”定义了
一个 float 型的三维数组 score,可以对 score 的每个维度赋予一定的含义,如第一维为班级的个数,
第二维为每个班的学生人数,第三维为每个学生学习的科目数,这样,数组元素 score[0][0][0]就
表示第一个班级的第一个学生的第一门科目的考试成绩。
【例 5-6】 某年级共有 3 个班,每个班各有 4 名学生,每个学生有 2 个科目的考试成绩。求
各班每个学生的平均成绩并输出。
分析:
可以将本题的求解过程分成两步:
(1)定义一个三维数组,存储输入的每个班级每个学生的
每门课程的成绩;(2)对每个学生的成绩求平均值并输出。
应该用三个控制变量分别控制班级(3 个)、学生(4 个)、科目(2 个)的变化,因此用三重
循环来实现。可以将每个班级的每个学生的平均成绩保存起来以备查用,因此需要定义一个二维
实型数组存储平均成绩,第一维表示班级,第二维表示学生,数组元素的值即为平均成绩。

112
第5章 数组

程序:
#include <stdio.h>
#define N1 3 /*N1 个班级*/
#define N2 4 /*N2 个学生*/
#define N3 2 /*N3 个科目*/
int main()
{
int i, j, k;
int multi_arr_score[N1][N2][N3], sum;
float average[N1][N2];
for(i = 0; i < N1; i++)
for(j = 0; j < N2; j++)
for(k = 0; k < N3; k++)
{
printf("Input the No.%d students' scores of %d subject in Class %d: ",
j + 1, k + 1, i + 1);
scanf("%d", &multi_arr_score[i][j][k]);
}
for(i = 0; i < N1; i++)
for(j = 0; j < N2; j++)
{
sum = 0;
for(k = 0; k < N3; k++)
sum = sum + multi_arr_score[i][j][k];
average[i][j] = sum / N3;
printf("The No.%d student average score in Class %d is %.2f\n", j + 1,
i + 1, average[i][j]);
}
return 0;
}
运行结果如下:

113
C 语言程序设计(第 2 版)

5.5 字 符 数 组
用来存放字符数据的数组称为字符数组。字符数组中的一个元素存放一个字符,根据实际问
题的需要,也可以将字符数组定义为一维数组、二维数组或多维数组。在 C 语言中没有专门的字
符串变量,通常用字符数组来存放字符串。

5.5.1 字符数组的定义和引用
字符数组的定义和引用符合前面所学过的数组的定义和引用方法。
例如:
char a[5] ; /* 定义一个一维字符数组 */
char b[2][3] ; /* 定义一个二维字符数组 */
char c[2][3][4] ; /* 定义一个三维字符数组 */
a[3] = 'a' ; /* 一维字符数组元素赋值 */
b[1][2] = 'b' ; /* 二维字符数组元素赋值 */

5.5.2 字符数组的初始化
对字符数组初始化,也有全部初始化和部分初始化两种方式。
例如,语句“char ch_array[3] = {'C','+','+'};”对字符数组 ch_array 进行全部初始化,相当于
ch_array[0] = 'C';
ch_array[1] = '+';
ch_array[2] = '+';
也可以写成语句“char ch_array[] = {'C','+','+'} ;”的形式,省略数组长度。
对字符数组初始化,C 语言还允许采用字符串的方式。例如,语句“char ch_array[] = {"C++"};”
也可以实现对字符数组 ch_array 初始化,相当于
ch_array[0] = 'C';
ch_array[1] = '+';
ch_array[2] = '+';
ch_array[3] = '\0';

用字符串方式赋值时,除了要将初始化列表中的字符串按序赋值给对应的数组元素
之外,还要用一个数组元素来存储字符串的结束标志,即字符'\0',因此,在内存中要比
用字符逐个赋值多占一个字节。'\0'是由 C 编译器自动加上的,在输出时不显示。
例如,对上述字符数组 ch_array 初始化,要写成:
char ch_array[4] = {"C++"};
而不是:
char ch_array[3] = {"C++"};

M提醒
利用字符串方式进行数组初始化时,一般无须指定数组的长度,由系统自行处理。

对数组 ch_array 初始化通常写成:


char ch_array[] = {"C++"};

114
第5章 数组

对字符数组进行部分初始化时,系统为没有初值的数组元素自动赋值为'\0'。
例如:
char ch_array[5] = {'C','+','+'} ;
相当于
ch_array[0] = 'C';
ch_array[1] = '+';
ch_array[2] = '+';
ch_array[3] = '\0';
ch_array[4] = '\0';
对二维字符数组初始化的方法与前述类似,不再赘述。

5.5.3 字符数组的输入输出
字符数组的输入输出也可以用格式输入输出函数来实现,通常有以下两种方法。
(1)逐个字符输入输出,用格式符%c
例如:
for(i = 0; i < 10; i++)
scanf("%c", &c[i]) ; /* 逐个输入一维字符数组 c 的每个数组元素 */
for(i = 0; i < 10; i++)
printf("%c", c[i]) ; /* 逐个输出一维字符数组 c 的每个数组元素 */
(2)将整个字符串一次输入或输出,用格式符%s
例如:
char s[10] ;
scanf("%s", s) ; /*数组名 s 即代表数组元素首地址,所以不可以加&符*/
printf("%s", s);
【例 5-7】 输出一个字符串("Welcome to Beijing!")。
程序:
#include <stdio.h>
int main()
{
char c[20] ="Welcome to Beijing!";
int i;
for(i = 0; c[i] != '\0'; i++)
printf("%c", c[i]);
printf("\n");
printf("%s",c);
printf("\n");
return 0;
}
运行结果如下:

M提醒
(1)从键盘输入一个字符串时,一般用空格作为结束标志。
(2)如果一个字符数组中包含一个以上'\0',则遇到第一个'\0'时就会结束输出过程。

115
C 语言程序设计(第 2 版)

5.5.4 字符串处理函数
C语言提供了丰富的字符串处理函数,可以实现字符串的输入、输出、连接、比较、转换、
复制和搜索等功能,使用这些函数可以大大提高编程的效率。在使用字符串处理函数时,要用编
译预处理命令#include 将头文件“string.h”包含进来,函数 puts( )和 gets( )除外(需要包含头文件
“stdio.h”)

(1)字符串输出函数 puts( )
该函数的一般形式如下:
int puts(字符数组名) 或 int puts(字符串常量)
功能:把字符数组中的字符串输出到终端,并在输出时将字符串结束标志'\0'转换成'\n'。
例如,下面的语句段
char c[] = "C\nC++\nC#";
puts(c);
的输出结果为:
C
C++
C#
(2)字符串输入函数 gets( )
该函数的一般形式如下:
char *gets(字符数组名)
功能:接收从终端输入的字符串,并将该字符串存放到字符数组名所指定的字符数组中。
例如,对于语句:
gets(str);
如果从键盘输入:Java↙,则将输入的字符串"Java"存放到字符数组 str 中,数组长度为 5(末
尾自动加'\0'),函数 gets( )的返回值为字符数组 str 的起始地址。

M提醒
用 puts()和 gets()函数每次只能输出或输入一个字符串,不能写成
puts(str1,str2)或 gets(str1,str2)

(3)字符串连接函数 strcat( )
该函数的一般形式如下:
char *strcat(字符数组名 1,字符数组名 2 或 字符串常量)
功能:把字符数组 2 中的字符串连接到字符数组 1 中字符串的后面,并删去字符数组 1 中字
符串后面的串结束标志'\0'。字符数组 1 必须足够大,以便能容纳连接后的新字符串。
例如,下面的语句段:
char str1[30] = "New Beijing,";
char str2[] = {"New Olympic!"};
printf("%s", strcat(str1, str2));
输出结果为:
New Beijing,New Olympic!
连接前后的变化如图 5-6 所示。

116
第5章 数组

(4)字符串复制函数 strcpy( )
连接前字符数组 str1 与 str2 的存储情况:
str1
N e w B e i j i n g , ﹨0
str2
N e w O l y m p i c !﹨0
连接后字符数组 str1 的存储情况:
N e w B e i j i n g , N e w O L y m p i c ! ﹨0

图 5-6 字符串连接示意图

该函数的一般形式如下:
char * strcpy(字符数组名 1,字符数组名 2 或 字符串 2)
功能:把字符数组 2 中的字符串复制到字符数组 1 中,串结束标志'\0'也一同复制。与 strcat
函数一样,字符数组 1 也必须定义得足够大,以便能容纳被复制的字符串。字符数组 1 的长度不
应小于字符串 2 的长度。
例如,下面的语句段:
char str1[10], str2[] = {"Java"} ;
strcpy(str1, str2) ;
执行后,str1 的状态如图 5-7 所示。

J a v a \0

图 5-7 字符串复制示意图

不能用赋值语句将一个字符串常量或字符数组赋给另一个字符数组。

例如:
char str1[10],str2[] = {"Java"};
str1 = str2;
是错误的。
(5)字符串比较函数 strcmp( )
该函数的一般形式如下:
int strcmp(字符串 1,字符串 2)
功能:按照 ASCII 码值的大小逐个比较两个字符串的对应字符,直到值不相等或遇到'\0'时结
束比较。具体规则如下:
字符串 1 = 字符串 2,则返回值为 0;
字符串 1 > 字符串 2,则返回值为正数;
字符串 1 < 字符串 2,则返回值为负数。
本函数中的字符串 1 和字符串 2 可以是字符串常量,也可以是字符数组。
注意,两个字符串进行比较,不能用
if(str1 == str2) printf("yes") ;
而只能用
if(strcmp(str1, str2) == 0) printf("yes") ;

117
C 语言程序设计(第 2 版)

(6)求字符串长度函数 strlen( )
该函数的一般形式如下:
int strlen(字符串)
功能:计算出字符串的长度(不含字符串结束标志'\0'),并将该长度作为函数返回值。
例如,下面的语句段
char str[10] = {"Java"};
printf("%d", strlen(str));
的输出结果是 4。
(7)字符串小写转换函数 strlwr( )
该函数的一般形式如下:
char *strlwr(字符串)
功能:将字符串中的大写字母转换成小写字母,小写字母与其他字母不变。
例如,语句
printf("%s", strlwr("ASP"));
的输出结果是 asp。
(8)字符串大写转换函数 strupr( )
该函数的一般形式如下:
char *strupr(字符串)
功能:将字符串中的小写字母转换成大写字母,大写字母与其他字母不变。
例如,语句
printf("%s", strupr("jsp"));
的输出结果是 JSP。
以上介绍了常用的 8 种字符串处理函数。再次强调,库函数并非 C 语言本身的组成部分,而
是人们为使用方便而编写的、提供给大家使用的公共函数。每个系统提供的库函数数量和库函数
名、库函数功能都不尽相同,使用时要小心,必要时查找库函数手册。当然,有一些基本的函数
还是相同的(包括函数名和函数功能)
,这就为程序的通用性提供了基础。

5.6 数组与函数
数组是程序中常用的一种构造类型,经常作为函数的参数在主调函数与被调函数之间实现数
据传递。
【例 5-8】 编写一个程序,求出 5 个学生的平均成绩、最高分和最低分,要求用函数实现平
均成绩、最高分与最低分的计算。
分析:
计算最高分就是求分数的最大值,最低分就是分数的最小值。通过定义函数实现最高分、最
低分、平均分的计算可以使程序具有更大的灵活性和复用性。在函数中计算最高分、最低分和平
均分,该函数的返回值是平均分,定义两个全局变量 max 和 min 存储最高分和最低分。
程序:
#include <stdio.h>
float max = 0, min = 0; /* 全局变量 */

118
第5章 数组

float average(float score[], int n);


int main()
{
float avg, score[5]={87,57,63.5,98,71};
int i;
avg = average(score, 5);
printf("max = %6.2f\nmin = %6.2f\naverage = %6.2f\n", max, min, avg);
return 0;
}
float average(float score[], int n)
{
int i;
float avg, sum = score[0];
/* 求最高分、最低分和总分 */
max = min = score[0];
for(i = 1; i < n; i++)
{
if(score[i] > max)
max = score[i];
else if(score[i] < min)
min = score[i];
sum = sum + score[i];
}
avg = sum/n;
return avg;
}
运行结果如下:

【例 5-9】 编写一个程序,计算两个给定的 N×N 阶 double 型矩阵的乘积,并将其保存在第


三个给定的矩阵中,要求用函数实现矩阵相乘。
分析:
设 a 和 b 均是 N×N 阶 double 型数组,分别存储给定的矩阵,定义数组 c 存储 a×b 的结果。
矩阵相乘的计算公式为:
n
cij = ∑ aik × bkj
k =1

程序:
#include<stdio.h>
#define N 2
double c[N][N];
void mul_matrix (double a[][N], double b[][N])
{
int i, j, k ;
for ( i = 0 ; i < N ; i++)
for ( j = 0; j < N; j++)
{
c[i][j] = 0;
for (k = 0; k < N; k++)
c[i][j] += a[i][k] * b[k][j];

119
C 语言程序设计(第 2 版)

}
}
int main()
{
double source_a[N][N] , source_b[N][N] ;
int i, j ;
printf("Please input matrix source_a: \n");
for ( i = 0 ; i < N ; i++)
for ( j = 0; j < N; j++)
scanf ("%lf", &source_a[i][j]);
printf("Please input matrix source_b: \n");
for ( i = 0 ; i < N ; i++)
for ( j = 0; j < N; j++)
scanf ("%lf", &source_b[i][j]);
mul_matrix (source_a, source_b) ;
printf("the result matrix c is : \n");
for ( i = 0 ; i < N ; i++)
for ( j = 0; j < N; j++)
printf ("%6.2lf", c[i][j]);
printf("\n");
return 0;
}
运行结果如下:

在函数的参数列表中的二维数组只需指定第二维的长度而无须说明第一维的长度,
这是与数组元素在内存中的排列方式密切相关的。

5.7 综 合 实 例
【例 5-10】 为比赛选手评分。计算方法是:从 10 名评委的评分中扣除一个最高分和一个最
低分,计算总分后除以 8,得到这个选手的最后得分(分数采用百分制)。
分析:
本题的一种求解方法是:(1)在接收输入的评分的过程中计算出 10 个评委的评分总和;
(2)找出其中的最高分和最低分;
(3)最后得分为:(总分−最高分−最低分)/8。
要注意,评委给出的评分都是整型数据,通过计算得出的选手的得分是实型数据,利用除法
运算符“/”计算选手得分时应进行强制类型转换。
算法描述:
(1)定义符号常量 N,值为 10;
(2)定义数组 score,接收键盘输入的评委的评分;
(3)定义变量 max,min,sum,用于存储最高分、最低分和总分;

120
第5章 数组

(4)定义变量 mark,用于存储选手最后得分;
(5)循环变量 i ← 0,循环条件 i < N,循环执行以下语句
1)键盘输入评委的评分存储到 score[i]中;
2)对评分进行累加:sum ← sum + score[i];
(6)令 max ← score[0],min ← score[0];
(7)循环变量 i ← 0,循环条件 i < N,循环执行以下语句
1)如果 score[i] > max,则 max ← score[i];
2)如果 socre[i] < min,则 min ← score[i];
(8)计算选手得分,存储到变量 mark 中并输出。
程序:
#include <stdio.h>
#define N 10
int main()
{
int i;
int score[N];
int max, min;
int sum = 0;
float mark;
printf("Please Input the Scores:");
/*(1)输入 10 名评委的评分并计算 10 名评委评分的总和 */
for(i = 0; i < N; i++)
{
scanf("%d", &score[i]);
sum = sum + score[i];
}
/*(2)找出其中的最高分和最低分 */
max = min = score[0];
for(i = 0; i < N; i++)
{
if(score[i] > max)
max = score[i];
if(score[i] < min)
min = score[i];
}
/*(3)计算(总分-最高分-最低分)/8 的值并输出 */
mark = (sum – min - max) / (float)(N-2);
printf("The mark of the player is: %.1f\n", mark);
return 0;
}
运行结果如下:

【例 5-11】 输入 5 个国家的英文名字,找出英文名字的首字母按字母顺序排在最后的那个国
家的名字并输出。
分析:
本题的一种求解方法是:(1)定义一个字符数组接收用户输入的英文名字;
(2)按字母序对
121
C 语言程序设计(第 2 版)

存储国家名字的字符串进行比较,找到最大的字符串,这是本题的关键。
英文名字的输入可用函数 gets( )实现。找最大的字符串就是求最值问题,算法与例 5-2 相同,
将第 1 个字符串赋给字符数组 max,然后用 max 与剩余的字符串进行比较。由于字符串的赋值不
能直接用“=”运算符,因此需要使用字符串处理函数 strcpy( )实现。
算法描述:
(1)定义字符数组 country 存储国家的英文名字;
(2)循环变量 i ← 0,循环条件 i < 5,循环执行以下语句:
接收键盘输入的国家名存储到数组 country 中
(用 gets( )函数接收键盘输入的字符串)

(3)令变量 max ← country[0](用 strcpy( )函数进行字符串赋值)

(4)循环变量 i ← 0,循环条件 i < 5 时,循环执行以下语句:
如果 max < country[i],则 max ← country[i](用 strcmp( )函数进行字符串比较)

(5)输出 max 的值。
程序:
#include <string.h>
#include <stdio.h>
int main()
{
char country[5][20];
char max[20];
int i;
printf("Please input 5 country names:\n");
/*(1)输入 5 个国家的英文名字;*/
for(i = 0; i < 5; i++)
gets(country[i]);
strcpy(max,country[0]);
/*(2)按字母序对存储国家名字的字符串进行比较,找到较大的字符串。*/
for(i = 1; i < 5; i++)
{
if(strcmp(max, country[i]) < 0)
strcpy(max, country[i]);
}
printf("The max string is %s\n", max);
return 0;
}
运行结果如下:

5.8 深入研究:数组的负数下标和动态数组问题
1.数组的负数下标
C 语言中规定数组的下标必须是整型数据,但并没有要求其数值必须大于等于 0,因此数组

122
第5章 数组

的负数下标在语法上是合法的。至于负数下标在语义上是否正确,取决于通过一个具体的负数下
标访问数组元素时是否会造成地址越界,也就是所要访问的数据是否仍然是一个合法的数组元素。
例如对于原型为 int f(int array[])的函数 f( ),如果使用语句 f(&a[2])来调用该函数(这里&为取地址
运算符,即获取数组元素 a[2]的地址,具体内容将在后续章节介绍),则在函数内部 array[-1]和
array[-2]在语义上都是正确的,它们分别对应数组 a 中的元素 a[1]和 a[0],但 array[−3]在语义上是
不正确的,因为它所访问的数据超出了数组 a 的范围。
【例 5-12】 字符串中最后一个字符的获取。
在对字符串的处理中,有时需要直接读取字符串中最后一个字符。实现这一任务的方法有多
种,最常见的方法是首先计算字符串的长度,然后再根据字符串的长度取出该字符串的最后一个
字符。假设该字符串保存在数组 str 中,则相应的代码如下。
int ch;

ch=str[strlen(str)-1];
对于这个任务,也可以使用数组的负数下标来完成,相应的代码如下。
int ch;
char *p;

p=strchr(str,’\0’);
ch=p[-1];
这里,函数 strchr( )定义在 string.h 中,原型是:
char *strchr(const char *s, int c);
其中,函数 strchr( )的功能是返回字符 c 在字符串 s 中最后一次出现的位置(这里*为取内容运
算符,与指针有关,详情将在后续章节介绍)

在上面的代码中,字符 c = '\0',也就是字符串的结束符,返回值指向字符串 str 中最后一个字符
后面的位置,因此 p[−1]就是字符串中最后一个字符。
因此,充分利用数组的负下标可以使一些问题得到巧妙的解决。
2.动态数组问题
在一些函数中,经常需要使用一些大型数组作为数据处理的临时存储空间。这些数组只在一个函数
中被临时使用,因此人们往往把它们与其他临时存储空间一样对待,定义为局部变量。从语法和语义上
讲,这种方法没有任何问题。但对于规模比较大的数组来说,把它们定义为局部变量,由系统在函数被
调用时自动分配其所需要的存储空间,则可能会带来意想不到的后果:程序会因崩溃而无法运行。
【例 5-13】 作为局部变量的大型一维数组。
#define K 1024
#define M (K*K)
void func()
{
double locArr[64 * M];

}
由于函数试图建立一个巨大的临时数组,因此在函数 func( )被调用时就会引起程序的崩溃。
造成这种情况的原因是,系统会为所有函数的局部变量在栈区分配内存空间,而一个程序的函数
调用栈的存储空间是有限的。在默认状态下,MS Windows 分配给一个程序的栈空间大约是 1MB,
除了用于局部变量的分配,这个栈空间还被用于保存和函数调用相关的其他数据,如函数的参数
和返回地址等,因此不能为局部变量分配过大的存储空间。而上面代码中数组 locArr 所需要的存
储空间是 8×64MB,远远超过了栈空间的容量,因此必然会引起程序的崩溃。
123
C 语言程序设计(第 2 版)

可以采用下面的方法解决这个问题,即不预先为数组分配存储空间,只有当程序运行时明确地知道
所需要的存储空间的大小时才为数组分配存储空间,当数组使用完毕后及时释放其所占用的存储空间。
这就是动态内存分配机制,这样不仅提高了内存的使用效率,而且增加了程序的灵活性和适应性。
动态内存分配是从系统分配给一个程序的被称为“堆”的内存中分配存储空间。一般来说,
这一存储空间的大小只受计算平台的存储空间资源和操作系统对存储空间分配策略的限制,并且
远远大于系统分配给一个程序用作函数调用栈的存储空间。因此,对于大型的临时数组,应该使
用动态内存分配技术在堆上为其分配存储空间。
【例 5-14】 动态分配的大型一维数组。
#define K 1024
#define M (K*K)
void func()
{
double *locarr;
locarr=malloc(sizeof(double) * 64 * M);
......
free(locarr);
}
本节内容可以在学习了第六章后再进行深入分析。

本章小结
数组是若干具有相同数据类型的数据的集合,属于 C 语言中的构造数据类型,是程序设计中
常用的数据结构。按照数组元素的类型可将数组分为整型数组、实型数组、字符数组和指针数组
等,按照下标的维数又可以把数组分为一维数组、二维数组和多维数组。
数组定义时通常要给出数组的长度,数组引用时要通过下标指出要引用的数组元素,因此要特
别注意定义和引用时数组名后面方括号中数字的具体含义;当对数组进行初始化时可以省略数组长
度,但对于多维数组,只能省略第一维的长度,其他维的长度要给出确切的值。数组初始化可以采
用多种方式,在使用时应根据具体情况进行选用。除字符数组外,对数组进行赋值、输入和输出操
作时,必须使用循环语句通过引用数组元素来完成,不能将数组作为一个整体进行上述操作。
C语言中没有专门的字符串变量,通常用一个字符数组来存放一个字符串。为了便于对字符
串的处理,C语言提供了丰富的字符串处理函数,可以实现字符串的输入、输出、链接、修改、
比较、转换、复制、搜索等操作,使用这些函数可以提高编程的效率。
数组可以用作函数的参数,但不能用作函数的返回值。由于函数参数是数组时系统会将其作为
指针来处理,因此本章仅简单介绍了一般用法,其他更深入的内容在学习指针后进一步详细阐述。

习 题
【复习】
1.对于数组的描述错误的是( )

A.必须先定义,后使用

124
第5章 数组

B.数组元素引用时下标从 0 开始
C.数组中的所有元素必须是同一种数据类型的
D.定义时数组的长度可以用一个已经赋值的变量表示
2.定义数组时,表示数组长度的不能是( )

A.整型常量 B.符号常量 C.整型变量 D.整型常量表达式
3.设有定义 short x[5] = {4,6,8};,则数组 x 占用的内存字节数是( )。
A.10 B.6 C.5 D.3
4.已知 a[4]={1,2,3},那么 a[3]的值为( )

5.在 C 语言中,二维数组的元素在内存中存放的顺序是( )

A.按行存放 B.按列存放 C.用户自己定义 D.由编译程序决定
6.二维数组 M 的行下标 i 的范围从 0 到 4,列下标 j 的范围从 0 到 5,每个元素占 4 个存储
单元,M 按行序存储元素 M[3][5]的起始地址与 M 按列序存储时元素( )的起始地址相同。
A.M[2][4] B.M[3][4] C.M[3][5] D.M[4][4]
7.合法的数组定义语句是( )。
A.char a[ ]={0,1,2,3,4,5}; B.int a[5]={ 0,1,2,3,4,5};
C.char a="string"; D.int a[ ]= "string";
8.为了判断两个字符串 s1 和 s2 是否相等,应当使用( )。
A.if(s1 == s2) B.if(s1 = s2)
C.if(strcmp(s1,s2)) D.if(strcmp(s1,s2) == 0)
9.有字符数组 a[80]和 b[80],正确的输出语句是( )。
A.puts(a,b); B.printf("%s,%s",a[],b[]);
C.putchar (a,b); D.puts(a);puts (b);
10.若已定义:char ch[5] = {'e','f','\0','g','\0'};则 printf("%s", ch);的输出是( )

A.'e"f' B.ef C.efg D.ef\0g
11.阅读下面的程序:
#include<stdio.h>
int main( )
{
int y = 10,i = 0,j,a[5];
do
{
a[i] = y % 3;
i++;
y = y / 2;
}while(y >= 1);
for(j = i−1; j >= 0; j--)
{
printf("%d", a[j]);
printf(" ");
}
return 0;
}
该程序的输出结果是: 。
12.阅读下面的程序:
#include<stdio.h>

125
C 语言程序设计(第 2 版)

int main( )
{
char ch[7] = {"83ek47"};
int i,s = 1;
for(i = 0;ch[i] >= '0'&&ch[i] <= '9';i++)
s = 10*s+ch[i]−’0’;
printf("%d\n",s);
return 0;
}
该程序的输出结果是: 。
【应用】
1.从键盘输入 10 个数,输出其中的最小值。
2. 从键盘输入 10 个数,将其存入一个数组中,处理使这 10 个数在原数组中逆序存放。要求
输出原数组和处理后数组(注:要求处理过程占用最少存储空间)

3.程序读入 20 个整数,统计非负数的个数,并计算非负数的和。
4.打印出以下的杨辉三角形,要求打印的行数由用户通过键盘输入。

1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1

5.将给定数组 a 中的 10 个元素(初始值为 1001,1000,2001,1030,5201,4110,2111,
3302,6013,3123)按每个数的各位之和(千位+百位+十位+个位)从小到大排序。
6.将一个二维矩阵行和列元素互换,存储到另一个二维数组中并输出显示。
7.从键盘输入一个 3×3 的整型矩阵,求其中最大元素的值以及对角线元素之和。

8.从键盘输入一个字符串,将小写字母全部转换成大写字母并输出(不使用系统函数)
9.编写一个程序,将字符串 b 的每个字符按顺序插入到已排好序的字符串 a 中。
【探索】
1.字符串查询。用户输入一个名字,然后在已有字符串数据中进行查找,如果查找成功则输
出该名字以及它在数组中的位置,否则输出“找不到相关信息!”

2.编写一个程序,定义一个整型的二维数组,并用下面的矩阵将其初始化,将矩阵中所有小
于 5 的元素赋值为 0 后按行序输出。
3649 1 4800
2699 3 4
3.寻找假币问题。一个国王要赏赐一个大臣 30 枚金币,但是其中有一枚是假币。国王提出
要求:只能用一个天平作为测量工具,并用尽量少的比较次数找出这枚假币,那么余下的 29 枚金
币就赏赐给这位大臣;否则这位大臣将得不到赏赐。已知假币要比真币的分量略轻一些。编写一
个 C 程序模拟找假币的过程,注意用尽量少的比较次数找出这枚假币。

126
第 章 6
指针

本章目标
◇ 了解地址、指针和指针变量的概念及关系
◇ 熟悉指针变量的定义和使用方法
◇ 明确指针变量的间接访问方式以及在内存中的存储方式
◇ 掌握利用指针操作数组和函数的程序设计方法
指针是 C 语言中一种特殊的数据类型,也是 C 语言的一个重要特色。正确灵活地使用指针,
可以有效地描述复杂的数据结构,可以实现按传址方式进行参数传递,可以“动态”分配内存空
间,可以更加简捷、高效地处理数组。由于指针的使用与内存地址的关系紧密,处理不好也会出
现很多问题,因此指针也是 C 语言的一个难点并备受争议,在学习过程中应多加思考。

6.1 指针与指针变量

6.1.1 指针的概念
为了弄清指针的概念,必须先理解地址的概念以及数据在计算机内存中的存储方式。
计算机的内存是由连续的存储单元组成的,一个存储单元为一个字节,每个存储单元都有唯
一的编号,这个编号称为内存地址。通过内存地址可以找到相应的内存单元,在内存单元中存放
所需要的数据。在前面章节中,都是通过变量名来访问这些内

存单元中的数据。例如,语句“int x=10;”的功能是定义一个 str
0x2200 a
int 型变量 x,并赋值为 10。实际上,是在内存中开辟了 2 个内
0x2201 10
存单元,将 10 送入这 2 个内存单元中存储起来。因此,如果能 x

够确定这 2 个内存单元的起始地址,就能够找到值 10,通常把


0x2203 2.5
系统分配给变量的内存单元的起始地址称为变量地址。
例如,语句 m
char str = 'a';
int x = 10;
float m = 2.5;
0x2207 …
定义的变量 str、x 和 m 在内存中的存储方式如图 6-1 所示。
从图 6-1 可以看出,系统为 char 型变量 str 分配了 1 个内 图 6-1 不同类型的变量在

存单元,内存地址为 0x2200,内存单元中存储的数据为'a';为 内存中的存储示意图

127
C 语言程序设计(第 2 版)

int 型变量 x 分配了 2 个内存单元,内存地址为 0x2201~0x2202,内存单元中存储的数据为 10;


为 float 型变量 m 分配了 4 个内存单元,内存地址为 0x2203~0x2206,内存单元中存储的数据为
2.5。其中,0x2200、0x2201 和 0x2203 分别是 char 型变量 str、int 型变量 x 和 float 型变量 m 的变
量地址。

变量的地址和变量的值是两个不同的概念。

由于变量地址标识了该变量在内存中的位置,根据变量的地址就可以找到该变量的值,因此
C 语言把这个地址形象地称为“指针”

M提醒
“指针”这个名称仅是一个概念,表示访问某一存储区域时的指向关系,不要认为在内存
中真的存在一个指针在来回移动。

6.1.2 指针变量的提出
1.指针变量的概念
指针是一个内存地址,变量的指针就是存储该变量的值所用的内存单元的起始地址,例如,在
16 位机中该地址是一个 16 位二进制数,在 32 位机中是一个 32 位二进制数。把这个地址用一个变
量来存储,该变量就称为“指针变量”
。通过指针变量中存储的某个变量的地址,可以找到这个变
量在内存中的位置,从而可以访问这个变量的值。因此,通常称这个变量为指针变量所指向的变量。
图 6-2 描述了指针变量与该指针变量所指向的变量之间的关系,p1、p2 和 p3 分别是指向变
量 str、x 和 m 的指针变量。



0x2200 a str
0x2200 p1
0x2201 10
x
p2
0x2201
0x2203 2.5

p3
0x2203
m


0x2207 …

图 6-2 指针变量与该指针变量指向的变量之间的关系

2.直接访问与间接访问
指针变量的存在,使 C 语言中对变量访问的方式除了“直接访问”外,又出现了“间接访问”

通过变量名对变量的内容进行访问的方式称为“直接访问”,利用指针变量访问它所指向的变量的
值的方式称为“间接访问”

例如,语句“int i_Price=256;”对 int 型变量 i_Price 进行访问的方式就是直接访问,其执行过
程是:系统根据变量名 i_Price 与变量地址之间的映射关系,直接找到变量 i_Price 的地址 0x2000,

128
第6章 指针

然后将 256 送入以 0x2000 为起始地址的 2 个内存单元,如图 6-3 所示。这种访问方式的特点是,


直接根据变量名找到变量地址对变量的值进行存取。
如果定义一个指针变量 p 指向变量 i_Price,通过指针变量 p 对变量 i_Price 进行访问的方式就
是间接访问。系统根据指针变量名 p 与变量地址之间的映射关系,找到指针变量 p 的地址 0x1000,
然后读取该内存单元中的值,即变量 i_Price 的地址 0x2000,接着再访问以 0x2000 为起始地址的
2 个内存单元中的值,如图 6-4 所示。这种访问方式是利用指针变量存放变量地址的特性,在程
序中通过指针变量间接地找到它所指向的变量的地址,再对变量的值进行存取。

0x1000 0x2000
p



0x2000 256
i_Price
0x2001 256
0x2000 i_Price
0x2001
… …

图 6-3 变量 i_Price 的直接访问示意图 图 6-4 变量 i_Price 的间接访问示意图

严格地讲,指针是一个内存地址,变量的指针是某个变量的地址,而指针变量是存
储某个变量地址的变量,它们是两个不同的概念。但是在实际使用中,通常是通过指针
变量对变量地址进行操作,因此,在不会产生混淆的情况下,对指针变量和指针不进行
严格区分,后续章节中出现的指针一般就是指指针变量。

M提醒
在不同开发环境中,指针变量占用的内存单元会相应地变化,因此在实际应用中如果要确
定指针变量占用的内存空间的字节数,可使用 sizeof 运算符进行测试。

特别提示:
C99 标准增加了一个新的类型限定词 restrict,它只能用于限定指针。restrict 限定的指针只能
通过初始化方法让其指向它要存取的变量,其他指针要想存取该对象只有基于该指针,因此变量
的存取被限于基于 restrict 限定的指针。

6.1.3 指针变量的定义
指针变量定义的一般形式如下:
类型说明符 *指针变量名;
其中,类型说明符用来表示该指针变量所指向的变量的数据类型;
“*”是指针说明符,表示它后
面的变量是指针变量;指针变量名是 C 语言中合法的标识符。
例如:
int *p1; //定义了一个指向 int 型变量的指针变量 p1
float *p2; //定义了一个指向 float 型变量的指针变量 p2
char *p3; //定义了一个指向 char 型变量的指针变量 p3

129
C 语言程序设计(第 2 版)

这里,指针变量 p1 的值是 int 型变量的地址;指针变量 p2 的值是 float 型变量的地址;指针


变量 p3 的值是 char 型变量的地址。
需要强调的是,指针变量可以指向不同数据类型的变量,而指针变量本身存放的是这些变量的
地址。因此,定义了一个指针变量后,无论这个指针变量所指向的变量是什么类型,系统都会为这
个指针变量分配固定大小的内存空间(具体大小与系统有关)
,用来存放它所指向的变量的地址。

在定义指针变量时,任何一个指针变量的前面都要有指针说明符“*”。

例如,这个语句“int *p4, a, b;”定义了三个变量,但仅有 p4 是指针变量,它所指向的变量


的类型为 int 型,而 a 和 b 都是普通的 int 型变量。如果要定义三个指针变量,语句应改为“int *p4,
*a, *b;”。
指针变量除了可以指向简单类型的变量外,还可以指向数组、指针、函数或结构体变量,从
而可以表示复杂的数据类型。当然,也可以定义一个指针变量,使其指向的变量的类型为 void,
例如,
“void *p;”。这是一种特殊的指针,它仅保存内存的某个字节的地址,而这片内存暂时不
保存任何类型的数据。这种指针就是“空类型”的指针,称为空指针。空指针仅仅用来指向一片
内存空间,而不对该内存空间的值作任何解释。因此,任何类型的指针都可以直接赋值给它,无
须进行强制类型转换,但是空指针不能直接赋值给其他类型的指针,必须使用强制类型转换。
在 C 标准库中,提供了两个典型的使用空指针进行操作的函数:
void *memcpy(void *dest, const void *src, size_t len);
void *memset(void *buffer, int c, size_t num);
这两个函数是内存操作函数,功能是直接对内存空间进行访问。由于它们操作的对象仅仅是
一片内存,而不论这片内存中存放的数据是什么类型,因此任何类型的指针都可以传入函数
memcpy( )和 memset( )中,真实地体现了内存操作函数的意义。

M提醒
为了描述方便,通常将指向整型变量的指针变量称为整型指针,指向实型变量的指针变量
称为实型指针,以此类推。

6.1.4 指针变量的初始化
定义了一个指针变量后,该指针变量并没有确定的指向,要想使一个指针指向一个具体的地
址,需要对该指针进行初始化。在定义指针变量的同时为其赋初值,称为指针变量的初始化。
例如,下面的语句段:
int x = 3;
int * p1 = &x;
int * p2 = p1;
完成了指针 p1 和 p2 的初始化。首先定义了 int 型变量 x,然后定义了一个整型指针 p1,并将变
量 x 的地址(&x 的作用就是取得 x 的地址)赋值给指针 p1,实现了指针 p1 的初始化,此时指针
p1 指向了变量 x;接着,又定义了一个整型指针 p2,同时将 p1 的值赋给 p2,此时 p2 也指向了变
量 x。变量 x、指针 p1 和 p2 在内存中的存储示意图如图 6-5 所示。

130
第6章 指针

……

0x1000 0x2000
p2

0x2000
p1

……

0x2000 256
x
0x2001

……

图 6-5 变量的间接访问示意图

整型指针仅能指向整型变量,不能指向除整型之外的其他数据类型的变量。同理,
实型指针仅能指向实型变量。

例如,下面的语句段:
char x = '3';
int * p1 = &x;
是错误的。
对于未经赋值的指针变量,系统会为其随机产生一个地址,这个地址有可能指向某个系统程
序,因此可能会造成系统混乱,甚至造成系统崩溃。为了防止使用无确定指向的指针变量,常常
用 NULL 对暂时不用的指针变量赋初值,即“int *p1=NULL;”。NULL 是在头文件“stdio.h”中
定义的一个符号常量,值为 0。

M提醒
在实际应用中,无论采用初始化方式还是赋值方式,都要在使用指针之前,使指针指向合
法的内存空间。

例如,下面的语句段:
int x = 10, *p;
p = &x; /*赋值方式*/
与语句
int x = 10, *p = &x; /*初始化方式*/
的作用相同。

初始化与赋值是不同的概念。初始化是在建立一个变量的同时为其赋初值,赋值是
改变一个已存在的变量的值。

例如,语句:
int a = 0 ;

131
C 语言程序设计(第 2 版)

以初始化方式为变量 a 赋初值 0,而下面的语句段:


int a ;
a = 0 ;
也实现了为变量 a 赋值 0,但这种方法的处理过程与初始化方式不同。
首先定义了一个 int 型变量 a,此时变量 a 也是有值的,如果该变量是局部变量,则该变量的值是
不确定的,然后通过赋值语句把值 0 存储到变量 a 所在的内存空间,从而覆盖了变量 a 原来的值。
因此,为常量赋值必须采用初始化的方式,而不能采用赋值的方式。
例如,语句:
const int a = 0 ;
实现了把常量 a 赋值为 0,但下面的语句段:
const int a ;
a = 0 ;
则是错误的,因为常量的值不能更改。

6.1.5 指针变量的使用
指针变量与内存地址直接相关,因此使用指针变量时,经常会用到两个重要的指针运算符:
取地址运算符“&”和取内容运算符“*”。
1.取地址运算符
取地址运算符“&”的作用是取得符号“&”后的变量的地址。当对一个变量运用取地址运
算符时,得到的结果是这个变量的地址,前面的章节中已多次使用。
例如,下面的语句段:
int count;
int *iptr = &count;
将整型变量 count 的变量地址赋值给整型指针 iptr。
取地址运算符“&”是单目运算符,结合性为“左结合”。“&”符号后面能接任何类型的变
量(包括指针变量)

2.取内容运算符
取内容运算符“*”的作用是取得指针变量所指向的变量的值,即通过指针变量来间接访
问它所指向的变量。
例如,下面的语句段:
int x = 10, *p = &x;
int y;
y = *p;
实现将变量 x 的值 10 存入 int 型变量 y 中。具体过程是:首先将整型变量 x 的地址存入整型指针
p 中,此时 p 就指向了变量 x,然后通过语句“y = *p;”中的“*p”运算将指针所指向的变量的值
10 取出,再通过赋值运算存储到变量 y 中。
取内容运算符“*”是单目运算符,结合性为“左结合”。
“*”符号后面只能接指针变量。

M提醒
当对一个指针变量运用取内容运算符“*”时,系统根据该指针所指向的变量的类型来访
问它所指向的内存空间。如对 int 型指针运用“*”运算时,系统会把两个连续的内存单元中的
内容作为一个 int 型数据取出;如对 float 型指针运用“*”运算时,系统会把四个连续的内存单
元中的内容作为一个 float 型数据取出。

132
第6章 指针

3.使用中应注意的问题
使用指针变量时,应注意以下问题。
(1)同类型的指针变量之间可以相互赋值。如下面的语句段是合法的。
int x = 10,*p = &x,*q = p ;
q=p;
(2)不同类型的指针变量之间在经过强制转换后可以赋值,但是除了特殊用途外,不建议使
用这种赋值方式。
例如,下面的语句段:
int x = 10, *p = &x ;
float *q;
q = (float*)p;
printf(“%d”, *p); /*得到的输出结果是 10 */
printf(“%f”, *q); /*得到的输出结果是 0.000000 */
是合法的。
(3)无论是初始化方式还是赋值方式,都不允许把一个常量赋值给指针变量,字符串常量除外。
例如,下面的语句段:
int *p = 0x2000;
p = 0x2000;
是不合法的,而语句:
int *p = "abcde";
是正确的。指针 p 中存储的是字符串 "abcde" 在内存中的首地址。
(4)如果把一个变量的地址赋值给一个指针,使指针指向该变量,则该变量必须在指针赋值
之前定义,因为变量只有定义后方可使用。
例如,语句:
int *p = &x, x;
是不合法的。

M提醒
指针变量定义时出现的“*”和使用指针变量时出现的“*”的含义是不同的。例如,
“int *p;”
和“*p=3;”中“*”的含义不同。定义时“*”是指针说明符,表示其后面的变量是一个指针变
量;使用时“*”是取内容运算符,表示取得指针所指向的变量的值。

【例 6-1】 分析以下程序的运行结果,注意指针的用法。
程序:
#include<stdio.h>
int main()
{
int m , *p;
p = &m;
printf("input m: ");
scanf("%d", p); /* 该语句等价于:scanf("%d",&m);*/
printf("%d,%d\n", m, *p);
m = 4;
printf("%d,%d\n", m, *p);
*p = 6;

133
C 语言程序设计(第 2 版)

printf("%d,%d\n", m, *p);
return 0;
}
运行结果如下:

指针 p 指向变量 m 后,就可以通过 p 来引用 m。此时 m 和*p 是等价的,都表示变


量 m 的值,因此,对其中一个值的改变会影响另外一个的值。

【例 6-2】 从键盘输入两个整数,按由大到小的顺序输出。要求:使用指针实现。
程序:
#include<stdio.h>
int main()
{
int *p1, *p2, a, b, t;
printf("input a,b:");
scanf("%d,%d", &a, &b);
p1 = &a;
p2 = &b;
if (*p1 < *p2)
{ /* 交换指针变量指向的整型变量的值,指针指向不变 */
t = *p1;
*p1 = *p2;
*p2 = t;
}
printf("%d>%d\n", a, b);
return 0;
}
运行结果如下:

当执行赋值语句“p1 = &a;”和“p2 = &b;”后,指针 p1 和 p2 分别指向了变量 a 与


b,因此程序中出现的*p1 与*p2 和变量 a 与 b 是等价的。交换*p1 和*p2 的值也就是交换
了变量 a 和 b 的值。
在程序运行过程中,指针与变量的变化如图 6-6 所示。
p1 a p1 a

&a 3 &a 4

p2 b p2 b

&b 4 &b 3

(a) if 语句执行前 (b) if 语句执行后

图 6-6 程序运行中指针与变量的变化示意图

134
第6章 指针

【例 6-3】 重新实现例 6-2 的功能。要求:通过改变指针的指向来实现。


程序:
#include<stdio.h>
int main()
{
int *p1, *p2, a, b, *t;
printf("input a,b:");
scanf("%d,%d", &a, &b);
p1 = &a;
p2 = &b;
if(*p1 < *p2)
{ /* 交换指针变量的值,改变指针的指向 */
t = p1;
p1 = p2;
p2 = t;
}
printf("%d>%d\n", *p1, *p2);
return 0;
}
运行结果如下:

程序的运行结果与例 6-2 完全相同,但程序在运行过程中,实际存放在内存中的数


据没有移动,而是交换了指针的指向。当指针交换指向后,p1 和 p2 由原来的指向变量
a 和 b 改变为指向变量 b 和 a,这时,*p1 就表示变量 b,而*p2 就表示变量 a。其示意图
如图 6-7 所示。

p1 a p1 a

&a 3 &b 3

p2 b p2 b

&b 4 &a 4

(a) if 语句执行前 (b) if 语句执行后

图 6-7 程序运行中指针与变量的变化示意图

需指出,本节中的例子本身并没有太大的实际意义,通过这些例子仅仅是讲解指针的定义与
使用的方法以及需要注意的问题。

6.2 指针与数组
C 语言中,指针最常见的一个用法是使其指向一个数组。使用指向数组的指针来操作数组有
如下好处:书写方便和程序高效。通过使用指向数组的指针,常常可以写出占用较少内存并且执

135
C 语言程序设计(第 2 版)

行快速的代码。

6.2.1 指针与一维数组
1.指向一维数组元素的指针
如果定义了一个一维数组,系统会为该数组分配一段连续的内存单元,此时定义一个指针,
并将数组的第一个元素的起始地址赋值给该指针,则该指针就指向了这个一维数组。
例如,下面的语句段:
int a[3] = {11,22,33};
int *p;
p = &a[0];
的作用是把数组元素 a[0]的地址赋给指针 p,使 p 指向数组 a 的第 1 个元素,此时指针 p 就指向
了一维数组 a,如图 6-8 所示。 …
C 语言规定,数组名代表数组的第一个元素的起始地 p &a[0] 11 a[0]
址,也称为数组的首地址,它是一个地址常量。因此,数 22 a[1]
组名是指向数组首元素的常指针。 33 a[2]
例如,下面的两个语句: …
p = &a[0];
p = a; 图 6-8 指针指向一维数组

是等价的。

M提醒
对于一维数组来说,指向数组元素和指向数组是等价的。

需要强调的是,数组名 a 是指向数组首地址的常指针,而不是代表整个数组,语句“p=a;”
的作用是把数组 a 的首地址赋给 p,使指针 p 指向了数组 a。
2.指针的运算
定义一个指针指向了数组元素后,经常需要移动指针的位置使指针指向数组中不同的数组元
素,这就需要对指针进行算术运算。
由于指针变量中存储的是内存地址,是一种比较特殊的变量,因此指针的算术运算仅有以下
几种情况:
(1)指针的增量运算
例如,下面的语句段:
int a[5] = {0},*p = NULL;
float b[5] = {0},*q = NULL;
p = &a[0];
q = &b[4];
使指针 p 指向了 int 型数组 a 的第 1 个元素,指针 q 指向了 float 型数组 b 的第 5 个元素。此时可
通过对指针进行自增运算来改变指针的指向。下面的语句:
p++; /* 或者 ++p; */
使指针 p 在原值的基础上加上 2(int 型数据占 2 个字节),从而指向数组的下一个元素 a[1]。同
样,下面的语句:
q--; /* 或者 --q; */

136
第6章 指针

使指针 q 在原值的基础上减去 4(float 型数据占 4 个字节),从而指向数组的前一个元素 b[3]。

M提醒
指针的增量运算不是将指针变量的值(指针变量中存储的地址)加 1 或减 1,而是按照指
针所指向的变量的类型所占的字节数进行运算,从而使指针指向下一个合法的变量地址。该处
理过程由系统自动完成。

(2)指针与整数的加减运算
以指针与整数的加法运算为例,
“p+i”的计算方法是:用指针 p 的值,加上指针 p 所指向变
量的类型所占的字节数与 i 的乘积,相当于向后移动 i 个数组元素。
例如,下面的语句段:
int i=0, a[5] = {0}, *p = NULL;
float b[3] = {0}, *q = NULL;
p = a;
q = &b[2];
使指针 p 指向 int 型数组 a 的第 1 个元素 a[0],指针 q 指向了 float 型数组 b 的第 3 个元素 b[2]。
此时,下面的语句段:
p = p + 3;
q = q - 2;
使指针 p 按照 int 型数据所占的字节数(2 个字节)进行加法运算,即 p+3*2=p+6,也就是说指针
p 向后移动了 3 个数组元素,从而指向了数组 a 中的第 4 个元素 a[3],同理,指针 q 向前移动 2
个数组元素,指向了 float 型数组 b 的第 1 个元素 b[0]。
(3)指针与指针的减法运算
当两个指针指向同一片连续的存储单元时,指针的减法运算的结果是一个整数,其值为这两
个指针变量中的地址之差除以数据类型的长度。通过这种运算,可以计算出这两个指针间的距离,
也就是它们之间数据的个数。
例如,下面的语句段:
int a[5] = {0}, *p = NULL, *q = NULL;
p = &a[1];
q = &a[4];
printf("%d", q - p);
使指针 p 指向 int 型数组 a 的第 2 个元素 a[1],指针 q 指向数组 a 的第 5 个元素 a[4]。此时 p 和 q
同为 int 型的指针变量,且指向了同一个 int 型数组 a。因此,表达式“q-p”的含义就是计算这
两个指针之间 int 型数据的个数,输出结果为 3(因为 a[1]和 a[4]之间相差 3 个 int 型数据)。

两个指针不能进行加法,因为两个地址量相加是毫无意义的。

除了算术运算外,指针之间还可以进行关系运算,通过关系运算确定两个指针的相对位置。C
语言规定,对于一片连续的内存空间来说,指向前面内存单元的指针小于指向后面内存单元的指针。
例如,下面的语句段:
int a[10],*p = NULL, *q = NULL;
p = &a[2];
q = &a[8];
使指针 p 指向 a[2],指针 q 指向 a[8]。此时表达式“p < q”的值为真,返回值 1。

137
C 语言程序设计(第 2 版)

通常所说的两个指针相等,是指两个指针所指向的位置相同,即指向相同的内存空间。
3.一维数组元素的引用
C 语言规定,当指针 p 指向一个数组 a 后,表达式“p+i”的作用与“a+i”相同,都表示数组
元素 a[i]的地址,即&a[i]。对整个数组 a 来说,若有 3 个元素,则 i 的取值范围是 0~2,p+1 与
a+1 均表示数组元素 a[1]的地址,与&a[1]等价。
因此,C 语言中,对于一维数组元素的引用有两种方法:下标法和指针法。
(1)下标法:用 a[i]或 p[i] 表示数组第 i+1 个元素。
(2)指针法:用 *(a+i) 或 *(p+i) 表示数组元素 a[i],即数组第 i+1 个元素。
指向一维数组 a 的指针 p 与数组 a 的关系如表 6-1 所示。

表 6-1 指针 p 与一维数组 a 的关系


地 址 描 述 意 义 数组元素描述 意 义
a、&a[0]、p a 的首地址 *a、a[0]、*p 数组元素 a[0]的值
a+1、&a[1]、p+1 a[1]的地址 *(a+1)、a[1]、*(p+1)、p[1] 数组元素 a[1]的值
a+i、&a[i]、p+i a[i]的地址 *(a+i)、a[i]、*(p+i)、p[i] 数组元素 a[i]的值

M提醒
对于指向数组的指针来说,虽然它和数组名一样都存储了数组的首地址,但指针是一个变
量,在程序运行期间,它的值可以改变,而数组名是一个常量,其值不可改变。

【例 6-4】 分别用下标法和指针法访问数组元素。
程序:
#include<stdio.h>
int main()
{
int a[5]={0,2,4,6,8},i,*p;
p=a;
/* 通过下标法输出数组元素 */
for(i=0;i<5;i++)
printf("%d ",a[i]);
printf("\n");
/* 通过指针法输出数组元素 */
for(i=0;i<5;i++)
printf("%d ",*(a+i));
printf("\n");
/* 通过指针值的变化输出数组元素 */
for(;p<a+5;p++)
printf("%d ",*p);
printf("\n");
return 0;
}
运行结果如下:

138
第6章 指针

对数组元素的访问存在下标越界问题,因此,利用指针法引用数组元素时,也要注
意下标越界问题。

对于例 6-4 中的第三个 for 循环语句,当 for 循环执行结束后,指针 p 指向数组最后一个元素


的下一个内存单元,已经超过数组的范围,如图 6-9 所示。如果此时继续对指针 p 所指向的单元
做赋值操作有可能导致严重的后果。因此,在学习的过程中需要特别注意指针的指向。

a[0] 0 p for 循环开始时 p=a

a[1] 2

a[2] 4

a[3] 6

a[4] 8

… p 循环结束后,p 指向未知区域

图 6-9 指针访问越界示例

【例 6-5】 定义一个具有 10 个元素的数组,利用指针完成以下功能:


(1)按顺序输出数组中各元素的值。
(2)将数组中的元素按逆序重新存储后输出。
分析:
将数组元素按逆序存储的基本思想是,将数组中位置前后对应的数组元素进行交换。对于本
题,即 a[0]与 a[9],a[1]与 a[8],…,a[4]与 a[5]分别进行交换。要完成这种交换,需要定义两个
指针,一个指针指向数组的第一个元素,另一个指针指向数组的最后一个元素,交换这两个指针
所指向的数组元素,然后移动指针,当第一个指针的值大于或等于第二个指针的值时,说明已完
成了交换任务。其示意图如图 6-10 所示。

p p q q

a 1 2 3 4 5 6 7 8 9 10

图 6-10 逆序存放数组元素操作示意图

程序:
#include<stdio.h>
#define N 10
int main()
{
int a[N] = {1,2,3,4,5,6,7,8,9,10};
int *p = a,*q = NULL;
int i, t;
/*输出数组元素的值*/
for(i = 0; i < N; i++)

139
C 语言程序设计(第 2 版)

printf("%4d", *(p+i));
printf("\n");
/*指针 q 指向数组的最后一个元素*/
q = a + N - 1;
/*逆序存放数组元素*/
while(p < q)
{
t = *p; *p = *q; *q = t;
p++; q--;
}
/*输出逆序存放后数组元素的值*/
for(p = a; p - a < N; p++)
printf("%4d", *p);
printf("\n");
return 0;
}
运行结果如下:

6.2.2 指针与二维数组
1.二维数组元素的地址
C 语言中,弄清二维数组元素的存储方式与处理方法,有助于理解指针与二维数组的关系。
例如,语句:
int a[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
定义了一个二维数组 a。可以采用两种方式对数组 a 进行理解:
(1)如果将数组 a 直接看成是由数组元素组成的,则该数组的行数为 3,列数为 4,包
含 3×4=12 个元素。
(2)如果将数组 a 的每一行看成 1 个元素,则数组 a 包含 3 个元素,分别用 a[0]、a[1]
和 a[2]表示,此时每个元素又是一个一维数组,各包含 4 个元素。因此二维数组可以理解为
是由一维数组组成的,如图 6-11 所示。

&a[0][0]
a

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

1 2 3 4

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

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

图 6-11 二维数组的不同理解方式示意图

这里,二维数组名 a 是一个地址常量,表示二维数组的首地址,即数组 a 的第一个元素 a[0][0]


的地址& a[0][0]。由于二维数组 a 可以看成是由 3 个一维数组 a[0]、a[1]和 a[2]组成的,因此,数

140
第6章 指针

组名 a 也表示一维数组 a[0]的首地址即& a[0][0],


这时 a+1 就是一维数组 a[1]的首地址即& a[1][0]。
可以看出,一维数组名 a[0]、a[1]和 a[2]分别表示二维数组 a 第 0 行、第 1 行和第 2 行的首地址,
因此,可以利用一维数组名来表示二维数组各元素的地址,例如,a[0]+1 表示数组 a 第 0 行第 1
列元素的地址,a[2]+3 表示数组 a 第 2 行第 3 列元素的地址。
如果数组 a 的首地址为 0x2000,则在内存中的实际存储方式的抽象表示方法如图 6-12
所示。
地址 …
0x2000 1 a[0][0]

0x2002 2 a[0][1]
a[0]中的元素
0x2004 3 a[0][2]

0x2006 4 a[0][3]

0x2008 5 a[1][0]

0x200A 6 a[1][1]
a[1]中的元素
0x200C 7 a[1][2]

0x200E 8 a[1][3]

0x2010 9 a[2][0]

0x2012 10 a[2][1]
a[2]中的元素
0x2014 11 a[2][2]

0x2016 12 a[2][3]

… 错误!
图 6-12 二维数组在内存中的存储方式示意图

2.二维数组元素的引用
由于对二维数组有两种理解方式,相应地,对二维数组元素的引用也有两种方式。第一
种方式是让指针指向二维数组中的元素,此时指针的移动是以元素为单位进行的,这种指针
属于指向“数组元素”的指针,有时称为“列指针”;第二种方式是让指针指向组成二维数组
的一维数组(即二维数组的行),此时指针的移动是以一维数组为单位的,该指针属于指向“数
组”的指针,称为“行指针”。

(1)指向二维数组元素的指针 p
a[0][0]
当指针 p 指向二维数组的首元素后,p+1 将指向数组的第 2 个 p+1 a[0][1]
元素,p+2 将指向数组的第 3 个元素,依次类推。 p+2 a[0][2]
a[0][3]
例如,下面的语句段 p+3

int a[2][4]; p+4 a[1][0]


int *p = &a[0][0]; a[1][1]
p+5
定义了一个 int 型指针 p 指向二维数组 a 的首元素,如图 6-13 所示。 a[1][2]
p+6
因此,依次输出各元素的语句可以写为 a[1][3]
p+7
for(i = 0; i < 8; i++, p++) …
printf("%4d", *p);
图 6-13 利用指向数组元素的
(2)指向一维数组的指针
指针引用二维数组元素
当指针 p 指向一个构成二维数组的一维数组时,例如使 p 指
向 a[0],则 p+1 指向二维数组 a 的第 1 行 a[1],p 的增值是以一维数组的长度为单位的。

141
C 语言程序设计(第 2 版)

指向一维数组的指针定义的一般形式如下:
类型说明符 (*指针名)[常量表达式];
其中,类型说明符为指针所指向的变量的数据类型,指针名与前面的指针说明符“*”必须
用圆括号括起来,常量表达式是指针所指向的一维数组的长度。
例如,下面的语句段
int a[2][4];
int (*p)[4] = &a[0]; /*或者写成 int (*p)[4]=a;*/
定义了一个指向一维数组的指针 p,指向包含 4 个 int 型元素的一维数组 a[0]。此时 p+1 指向下一
个一维数组 a[1],因此,*(p + 1)+2 是二维数组 a 第 1 行第 2 列元素的地址,*(*(p + 1)+2)就是二
维数组元素 a[1][2]的值,如图 6-14 所示。
… p
a
a[0][0] *(p + 0) + 1或*p + 1

a[0][1]

a[0][2]

a[0][3] p+1

a[1][0]

a+1 a[1][1] *(p + 1) + 2

a[1][2]

a[1][3]

图 6-14 利用指向一维数组的指针访问二维数组元素

因此,依次输出各元素的语句可以写为
for (i = 0; i < 2; i++)
for(j = 0; j < 4; j++)
printf("%4d", *(*(p + i) + j));
由于 p 是指向一维数组的指针(即行指针)
,并且已经指向了第 0 行,因此 p+i 指向第 i 行,
*(p+i)就是第 i 行第 0 列元素的地址,再加上 j 以后,得到的便是第 i 行第 j 列元素的地址,因此,
最后一句中的“*(*(p + i) + j)”就是 a[i][j]的值。

6.2.3 指针与字符数组
C 语言中,定义一个指针指向字符数组后,就可以利用指针来处理该字符数组中存储的字符
串。使用指针处理字符串,不仅书写方便,而且程序的运行效率更高。
用指针处理字符串的方法是,首先定义一个字符型指针,然后将字符数组的首地址赋值给该指针。
例如,
char str[] = "Welcome to world!";
char *p = str;
其中,str 是一个含有 18 个字符的字符数组(最后一个字符是'\0'),p 是一个指向字符型
数据的指针,并指向了字符数组 str。此时就可以利用指针对该字符串进行处理了。
【例 6-6】 分析以下程序的运行结果,注意指针的使用。
程序:
#include <stdio.h>

142
第6章 指针

int main()
{
char str[ ] = "Beijing,China!";
char *p = str; /* 将字符数组的首地址传递给指针变量 p */
for( ;*p!= '\0';p++) /* 利用 for 循环输出字符数组中的每个元素 */
printf("%c", *p);
printf("\n");
p = str; /* 需要让指针 p 重新指向字符串 str */
printf("%s\n",p); /* 输出整个字符串 */
puts(p); /* 用字符串输出函数输出指针 p 指向的字符串 */
return 0;
}
运行结果如下:

当使用字符串常量对字符指针进行初始化时,虽然没有定义字符数组,但字符串在
内存中仍然以字符数组的形式存储。

例如:
char *p = "Beijing,China!";
这里,p 是一个指向字符串的指针,它的值是该字符串在内存中的首地址,如图 6-15 所示。

B e i j i n g , C h i n a ! \0

图 6-15 字符串的指针变量

需要强调的是,采用上述方式对指针进行初始化时,不能通过指针 p 来改变字符串的值。原
因是该字符串是一个常量,常量的值是不可以改变的。
例如,下面的语句段
char *p = "Beijing,China!";
*p='1';
是不合法的。

M提醒
不能采用赋值方式将字符串常量直接赋值给字符数组,只能在初始化的时候赋值,但可以
将字符串常量直接赋给字符指针。

例如,下面的语句段:
char str[15];
str = "Beijing,China!";
是错误的,而
char *str;
str = "Beijing,China!";
是正确的。

143
C 语言程序设计(第 2 版)

6.2.4 指针数组
当数组中的数组元素是指针时,这种数组称为指针数组。
一维指针数组定义的一般形式如下:
类型说明符 *数组名[数组长度];
其中,类型说明符标识了指针所指向的变量的数据类型,“*”说明该数组中的元素是指针
类型。
例如:
int *p[4];

定义了一个指针数组 p,该数组中有 4 个元素,每个元素都是一个指针,指向 int 型数据。

M提醒
要注意指针数组与指向二维数组中一维数组的指针在定义上的区别。指针数组的定义为
“int *p[4];”,而指向二维数组中一维数组的指针的定义为“int (*p)[4];”,区别在于是否有圆
括号。

【例 6-7】 分析以下程序的运行结果,注意指针数组的使用。
程序:
#include <stdio.h>
#include <string.h>
int main()
{
int i, flag = 0;
char yourname[20];
char *name[5] = {"LiJun", "ZhangLi", "LiMao", "SunFei", "WangLin"};
printf("input your name: ");
gets(yourname);
for(i = 0; i<5; i++)
if(strcmp(name[i],yourname) == 0)
{ flag = 1; break; }
if(flag == 1)
printf("%s is in this class\n",yourname);
else
printf("%s is not in this class\n",yourname);
return 0;
}
运行结果如下:

程序中定义了一个字符型的指针数组 name,有 5 个数组元素,分别用来存放 5 个字


符串的首地址。利用字符串处理函数 strcmp( )判断两个字符串是否相等。如果相等,则
函数返回值为 0,表示输入的姓名 yourname 在数组 name 中。

144
第6章 指针

M技巧
如果要存储多个字符串,最好定义指针数组来实现。因为指针数组中的数组元素是指针,
每个指针指向的字符串的长度可以不同。如果定义字符数组来存储,则需要定义二维字符数组,
数组的列数应以最长字符串为标准定义,浪费存储空间。

6.3 指针与函数
如果在函数定义中使用指针,可以完成更强大的程序功能。

6.3.1 指针作为函数参数
1.函数的形参是指针
在函数的定义中,如果函数的形参是指针,则函数调用时给出的实参可以是指针,也可以是
某变量的地址,这就是参数传递中的传址方式,也就是将实参的地址传递给形参。
假设函数的定义如下:
void subfunc(int *px, int *py)
{
*px = 10;
*py = 20;
}
在主函数 main( )中调用该函数“subfunc(&x, &y);”,实参分别为 int 型变量 x 和 y 的地址。在
参数传递过程中,将变量 x 和 y 的地址分别赋值给指针 px 和 py,此时形参 px 和 py 就分别指向
了变量 x 和 y,这样,在被调函数内部的语句“*px = 10;”和“*py = 20;”就会改变 px 和 py 所指向
的变量的值,即变量 x 和 y 的值。
采用传址方式进行参数传递时,由于作为形参的指针接收了与实参相关的变量的地址,因此
在被调函数中利用取内容运算符“*”改变形参所指向的变量的值,实际上就改变了主调函数中与
实参相关的变量的值,简单地说,就是形参的变化会影响到实参。
【例 6-8】 分析以下程序的运行结果,注意参数传递的方式。
程序:
#include<stdio.h>
int main()
{
void exchange(int *ptr1, int *ptr2); /* 函数声明 */
int a, b;
int *p1, *p2;
printf("input a,b:");
scanf("%d,%d", &a, &b);
p1 = &a;
p2 = &b;
exchange(p1, p2); /* 函数调用 */
printf("a = %d, b = %d\n", *p1, *p2);
return 0;
}

145
C 语言程序设计(第 2 版)

/* 函数 exchange()实现两个数交换的功能 */
void exchange(int *ptr1, int *ptr2)
{
int t;
if(*ptr1 < *ptr2)
{
t = *ptr1; *ptr1 = *ptr2; *ptr2 = t;
}
}
运行结果如下:

在主函数 main( )中的函数调用语句“exchange(p1, p2);”的实参为指针 p1 和 p2,分


别指向主函数中的局部变量 a 和 b,因此,在参数传递时,将实参 p1 和 p2 的值(即变
量 a 和 b 的地址)分别传递给形参 ptr1 和 ptr2,此时形参 ptr1 和 ptr2 也分别指向了变量
a 和 b,这样在被调函数 exchange( )中交换 ptr1 与 ptr2 所指向的变量值,就实现了交换 a
和 b 的值的功能。其示意图如图 6-16 所示。

完成参数传递

p1 a ptr1 a p1 a
&a 10 &a 20 &a 20

p2 ptr2 b p2
b b
&b 20 &b 10 &b 10

(a) 函数调用前 (b) 被调函数运行结束 (c) 返回主函数后

图 6-16 函数调用前后指针与变量的变化示意图

M提醒
在编写 C 语言程序时,一定要根据实际问题的需要正确选择参数的传递方式。C 语言中,
在参数传递时,采用传值方式不会因为形参的变化而影响到实参,也就是说不会将被调函数中
的形参的变化带到主函数中;采用传址方式时,形参的变化会影响到实参。

如果对例 6-8 中被调函数 exchange( )做如下修改:


void exchange(int *ptr1, int *ptr2)
{
int *t;
if(*ptr1 < *ptr2)
{
t = ptr1; ptr1 = ptr2; ptr2 = t;
}
}
通过 if 语句交换的仅是形参 ptr1 和 ptr2 的值,即 ptr1 和 ptr2 中存储的地址,使指针 ptr1 和
146
第6章 指针

ptr2 的指向发生了变化,此时 ptr1 指向了变量 b,ptr2 指向了变量 a,但主函数 main( )中变量 a


和 b 的值并没有发生任何变化。因此函数调用结束返回主函数后,变量 a 和 b 的值保持原值不变。
其示意图如图 6-17 所示。

完成参数传递

p1 a ptr1 a p1 a
&a 10 &a 10 &a 10

p2 ptr2 b p2
b b
&b 20 &b 20 &b 20

(a) 函数调用前 (b) 被调函数运行结束 (c) 返回主函数后

图 6-17 函数调用前后指针与变量的变化示意图

由于数组名是地址常量,因此可作为函数实参,从而使接收该值的形参指向主调函
数中对应的数组。

【例 6-9】 输出数组中最大的元素值。
程序:
#include<stdio.h>
int arrMax(int *arr, int n);
int main()
{
int array[10] = {1, 8, 10, 2, -5, 0, 7, 15, 4, -9};
int max = arrMax(array, 10);
printf("max = %d\n", max);
return 0;
}
int arrMax(int *arr, int n)
{
int i;
int max = arr[0];
for(i = 0; i < n; i++)
if(arr[i] > max)
max = arr[i];
return max;
}
运行结果如下:

函数 arrMax( )调用时的第一个实参是 int 型数组的数组名 array,参数传递后,作


为形参的指针 arr 就指向了数组 array,因此在被调函数中,通过下标法对指针 arr 的操
作就实现了对主函数 main( )中的数组 array 中的元素的访问,从而实现了在数组中找最
大值的功能。

147
C 语言程序设计(第 2 版)

2.函数的形参是数组
在函数的定义中,如果函数的形参是数组,则函数调用时给出的实参可以是数组,也可以是
指向数组的指针。
【例 6-10】 求二维数组中全部元素之和。
程序:
#include<stdio.h>
int arrAdd (int arr[3][4], int m, int n)
{
int i, j, sum = 0;
for(i = 0; i < m; i++)
for(j = 0; j < n; j++)
sum += arr[i][j];
return(sum);
}
int main()
{
int array[3][4] = {1,3,5,7,9,11,13,15,17,19,21,23};
int total;
total = arrAdd(array, 3, 4);
printf("total = %d\n", total);
return 0;
}
运行结果如下:

虽然函数的形参是数组,如例 6-10 中函数 arrAdd( )的形参 arr,但实际上系统仍把它看做是


指针,也就是说,仅将主调函数中的数组 array 的首地址赋值给 arr,因此仍是传址方式进行参数
传递。

对于二维数组作为函数的形参,可以省略第一维的长度,但第二维的长度必须明确
给出,不能省略。

例如:
int arrAdd (int arr[][4], int m, int n)
{

}
是正确的,而
int arrAdd (int arr[][], int m, int n)
{

}
是错误的。
【例 6-11】 重新实现例 6-10,注意函数参数的变化。
程序:
#include<stdio.h>
int main()

148
第6章 指针

{
int arrAdd(int arr[ ] ,int n);
int array[3][4] = {1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23};
int total;
total = arrAdd(array, 12);
printf("total = %d\n", total);
return 0;
}
int arrAdd(int arr[ ] ,int n)
{
int i, sum = 0;
for(i = 0; i < n; i++)
sum += arr[i];
return(sum);
}
运行结果与例 6-10 完全相同。

当形参用一维数组“int arr[ ]”的形式时,系统将形参 arr 看做是一个指向数组 array


的列指针,因此,在被调函数 arrAdd( )中用单循环即可。需指出,此时形参用“int *arr”
也是正确的。

6.3.2 指针作为函数的返回值
当指针作为函数的返回值时,主调函数中必须用指针接收该返回值。
指针作为返回值的函数定义的一般形式如下:
类型说明符 * 函数名([形参列表])
{
函数体
}
其中,
“*”是指针说明符,表明函数的返回值是指针,类型说明符是返回的指针所指向的变
量的类型。
例如:
int *func(int a, int b)
{

}
表示函数 func( )返回一个指向 int 型数据的指针。
【例 6-12】 编程实现字符查找功能。要求:在已知字符串中查找一个指定字符,并返回该字
符在字符串中的位置。
分析:
完成字符查找功能的函数要能够接收给定的字符串以及待查找的字符,并且能够将找到的字
符在字符串中的位置返回给主函数,因此定义函数 findChar( )完成字符查找功能,函数 findChar( )
要有两个形参,一个是字符指针(命名为 str)
,用于接收主函数中给定的字符串,一个是字符型
变量(命名为 ch)
,用于接收主函数中给定的字符,函数 findChar( )的返回值是一个指针,用于保
存待查字符的地址,从而确定该字符在字符串中的位置。
字符串查找功能用单重循环实现,在循环中通过不断移动指针 str 来改变 str 的指向,然后将
str 所指向的值与 ch 比较,若相等则将 str 作为函数的返回值。这样,在主函数中定义一个字符指
149
C 语言程序设计(第 2 版)

针(命名为 pt)来接收形参 str 的值,最后通过运算“pt-aStr”来计算找到的字符在字符串中的


顺序号(aStr 是存储已知字符串的字符数组名)

程序:
#include<stdio.h>
int main()
{
char *findChar(char *str, char ch);
char *pt, ch = 'C', aStr[ ] = "I love China";
pt = findChar(aStr, ch);
if (pt == NULL)
printf("not found\n");
else
printf("This is position %d(starting from 0).\n", pt - aStr);
return 0;
}
char *findChar(char *str, char ch)
{
while(*str!= '\0')
{
if(*str == ch)
return(str);
str++;
}
return(NULL);
}
运行结果如下:

6.3.3 指向函数的指针
C 语言中定义的指针,可以指向整型、实型、字符型等合法的 C 数据类型的变量,也允许指
向一个函数,只要将该函数的入口地址赋值给指针即可。函数的入口地址是指函数编译后生成的
机器指令中的第一条指令的地址。
一个指针中存储了函数的入口地址,这个指针就称为指向函数的指针,简称为函数指针。
函数指针定义的一般形式如下:
类型说明符 (*指针名)(参数列表);
其中,类型说明符是函数指针所指向的函数的返回值类型,指针名和指针说明符“*”必须用
圆括号括起来,后面的圆括号中的参数列表是函数指针所指向的函数的形参列表,参数列表中的
参数可以有一个或多个,也可以没有,当列表中无参数时圆括号也不能省略。
例如,语句“int (*p)(int a, int b);”定义了一个函数指针 p,指向一个函数返回值类型为 int
型的函数,该函数有两个 int 型的形参;语句“float (*pt)( );”定义了一个函数指针 pt,指向一个
返回值类型为 float 型的函数,该函数没有参数。
定义函数指针的目的,就是要利用该函数指针来调用它所指向的函数。因此,在使用函数指
针之前,首先要将一个函数赋给函数指针。需要强调的是,这个函数必须已经定义或声明,并且
函数形参在个数和类型上必须与函数指针一致,函数返回值的类型也必须相同。

150
第6章 指针

在 C 语言中,函数名代表函数的入口地址。为函数指针赋值,只需将函数名赋值给函数指针
即可。
例如,假设函数 function( )已声明:
int function(int, int);
则下面的语句段:
int (*p)(int,int);
p = function;
定义了一个函数指针 p,并通过将函数 function( )赋值给 p,使函数指针 p 指向了函数 function( )。

在为函数指针赋值时,等号右侧仅是函数名。例如,
“p = function;”是正确的,而
“p = function( );”是错误的。

M提醒
要严格区别函数指针的定义与指向数组的指针的定义。

通过函数指针调用函数的一般形式如下:
(*指针变量)(实参列表)

指针变量(实参列表)
【例 6-13】 分析以下程序的运行结果,注意函数指针的使用。
程序:
#include<stdio.h>
int main()
{
int a, b, c, d;
int (*p)(int); /* 定义函数指针 */
int odd(int x);
int even(int x);
p = odd; /* 函数指针 p 指向函数 odd() */
a = odd(6); /* 通过函数名调用函数 odd() */
b = (*p)(6); /* 通过函数指针调用函数 odd() */
printf("%d,%d\n", a, b);
p = even;
c = even(6);
d = p(6);
printf("%d,%d\n", c, d);
return 0;
}
/* 当参数为奇数时返回 1,否则返回 0 */
int odd(int x)
{
return x%2 != 0;
}
/*当参数为偶数时返回 1,否则返回 0*/
int even(int x)
{
return x%2 == 0;
}

151
C 语言程序设计(第 2 版)

运行结果如下:

程序定义了函数指针 p 后,先通过赋值语句“p=odd;”使指针 p 指向函数 odd( ),然后


利用语句“a = odd(6);”和语句“b = (*p)(6);” 分别调用函数 odd( );再通过赋值语句“p=even;”
使函数指针 p 指向另一个函数 even( ),然后利用语句“c = even(6);”和语句“d = p(6);”调
用函数 even( )两次。运行结果表明,用函数名和函数指针调用函数的运行结果是相同的。

在一个程序中利用函数指针分别指向不同的函数,就可以在主调函数中利用一个函数指针调
用不同的函数。

6.4 指向指针的指针
定义一个指针,当它所指向的变量又是一个指针时,称为指向指针的指针。
指向指针的指针定义的一般形式如下:
类型说明符 **指针名;
其中,类型说明符是“指针名”所指向的指针所指向的变量的数据类型,指针名前面有两个“*”

例如,下面的语句段
int x = 4; 0x1000 4
int * p1 = &x;

int ** p2 = &p1;
0x2000 0x1000 p1
定义了一个指向指针 p1 的指针 p2,其在内存中的存储示意图如

图 6-18 所示。将 int 型变量 x 的地址&x 赋值给指针 p1,则 p1
0x2000 p2
是指针变量,指向了变量 x;然后,将 p1 的地址&p1 赋值给指

针 p2,则 p2 指向 p1,从而间接地指向了变量 x。这里的 p2 就
是指向指针的指针。 图 6-18 指向指针的指针示意图

指向指针的指针是间接地指向目标变量,因此,通常将直接指向目标变量的指针
称为一级指针,指向指针的指针称为二级指针。

【例 6-14】 使用二级指针访问变量。
程序:
#include<stdio.h>
int main()
{
int x = 1, *p1, **p2;
printf("%d\n", x);
p1 = &x;
*p1 = 2;
printf("%d\n", x);
p2 = &p1;
**p2 = 3;

152
第6章 指针

printf("%d\n", x);
return 0;
}
运行结果如下:

程序定义了一个指针 p1 指向 int 型变量 x,通过语句“*p1 = 2;”实现了对变量 x 的赋值


运算,此时 x 的值为 2。然后,定义了一个指向指针的指针 p2,使其指向指针变量 p1,则“*p2”
就指向了变量 x,通过语句“**p2 = 3;”就可以间接改变变量 x 的值,此时 x 的值为 3。

M提醒
通过一级指针访问目标变量时需在一级指针名前加“*”,如果要通过二级指针来访问目标
变量时,则需在二级指针名前加“**”。

【例 6-15】 使用二级指针访问字符串。
程序:
#include<stdio.h>
int main()
{
char *p[4] = {"red", "yellow", "blue", "green"};
char **pp; /* 定义指向指针的指针变量 pp */
int i;
for(i = 0; i < 4; i++)
{
pp = p + i;
printf("%s\n", *pp); /* 依次输出 4 个字符串 */
}
return 0;
}
运行结果如下:

程序定义了一个指针数组 p 存放了四个字符串,通过 for 循环使二级指针 pp 指向每


一个字符串的首地址,并输出存储在其中的字符串。

6.5 动态内存分配
在前面的学习中,变量定义后,该变量的存储空间是系统自动分配的,这片内存空间的释放

153
C 语言程序设计(第 2 版)

也由系统自动完成。在实际应用中,有时要根据问题的需要由用户为变量申请内存空间,然后在
该变量使用完毕后由用户释放这片内存空间,将其归还给系统,这就是动态内存分配。ANSI C
标准建议设置两个库函数 malloc( )和 free( )来完成上述功能,多数编译器将其包含在头文件
“stdlib.h”中,但有些 C 语言编译器包含在头文件“malloc.h”中。使用时请参照具体的 C 语言编
译版本。
1.malloc( )函数
malloc( )函数的一般形式如下:
void *malloc(size)
功能:在内存的动态存储区(堆区)中分配一片连续的字节数为 size 的内存空间。函数的返
回值为该片内存空间的首地址,size 是一个无符号整数,如果没有可供分配的空间,函数返回
NULL。

函数 malloc( )返回值是空指针,因此在调用 malloc( )函数时,应该根据实际情况,


对返回值进行强制类型转换。如例 6-16 中的语句“ptr = (char *) malloc(30);”

2.free( )函数
free( )函数的一般形式如下:
free(指针变量)
功能:释放指针变量所指向的内存空间。
free( )函数释放的是 malloc( )函数所分配的内存空间。
【例 6-16】 分析以下程序的运行结果,注意函数 malloc( )和 free( )的使用。
程序:
#include<stdlib.h>
#include<string.h>
#include<stdio.h>
int main()
{
char *ptr;
ptr =(char *)malloc(30); /* 使用 malloc()函数分配存储空间 */
printf("Please input a string: ");
gets(ptr); /* 输入字符串 */
printf("The string you input is : %s\n", ptr);
free(ptr); /* 使用 free()释放指针 ptr 所指向的存储空间 */
return 0;
}
运行结果如下:

程序利用 malloc( )函数申请一片长度为 30 个字节的内存空间,并将这片内存空间的


首地址赋值给字符指针 ptr,使程序能够利用 ptr 访问这片内存空间。当不再需要这片内
存空间时,利用 free( )函数释放。

154
第6章 指针

M提醒
利用动态内存分配,可以根据需要分配指定大小的内存空间。因此, malloc( )函数通常与
sizeof 运算符结合使用。例如,malloc(sizeof(x))。

6.6 带参数的 main( )函数


到目前为止,用到的 main( )函数都是不带参数的。其实 main( )函数可以有两个参数,用于接
收命令行参数。带有参数的 main( )函数习惯书写成
int main(int argc, char *argv[])
{

}
的形式。其中,第一个形参 argc 用于接收命令行上用户输入的参数的个数,第二个形参 argv 是一
个字符指针数组,该数组中的第一个字符指针指向当前正在运行的程序的名字构成的字符串,如
果没有提供这个名字的话,那么该字符指针的值为 NULL。字符指针数组中的其他指针分别指向
程序运行时命令行上的其他参数。
系统处于命令行状态时,可以利用 main( )函数中的 argc 和 argv 接收运行程序时用户输入的参数。
例如,假设有一个源文件,命名为 cfile.c,经过编译链接后得到的可执行文件名为 cfile.exe。
在命令行状态下输入文件名:
cfile↙
计算机就会执行该程序。此时,main( )函数中的形参 argc 接收参数的个数,值为 1;形参 argv 中
的第一个字符指针指向字符串" cfile "。
在命令行中,除了给出要执行的文件名外,还可以有一个或多个字符串,作为传递给函数
main( )的实参值。
例如,
cfile Index↙
此时,系统将命令行的第一个字符串" cfile "的地址传给 argv[0],第二个字符串" Index "的地址传
给 argv[1]。main( )函数中的第一个形参 argc 的值是命令行中字符串的总个数,值为 2。
【例 6-17】 设 C 程序的源文件名为 ex6-17.c,经过编译和链接后得到的可执行文件的名称为
ex6-17.exe。当从命令行上输入 ex6-17 Index C_Language 后,分析下列程序的运行结果。
程序:
#include<stdio.h>
int main(int argc, char *argv[])
{
int i;
printf("Name:%s\n", argv[0]);
printf("Arguments:");
for(i = 1; i < argc; i++)
printf("%s ", argv[i]);
printf("\n");
return 0;
}

155
C 语言程序设计(第 2 版)

运行结果如下:

程序中的 for 循环共执行 2 次,第 1 次循环,循环变量 i = 1,输出 argv[1]指向的字


符串“Index”,第 2 次循环,循环变量 i=2,输出 argv[2]指向的字符串"C_Language "。

利用带参数的函数 main( ),可以直接从命令行得到某些参数,程序内部可以根据这些参数进


行相应的处理。例如,在使用数据文件时,可以根据不同的需要输入不同的命令,以打开不同的
文件。另外,利用函数 main( )中的参数可以使程序从系统得到所需的数据,使系统能够向程序传
递数据,增加了处理问题的灵活性。

M提醒
函数 main( )的形参名并不一定非用 argc 和 argv 不可,只是习惯上用这两个名字。如果改
用别的名称也可以,但其数据类型不能改变。

6.7 综 合 实 例
【例 6-18】 输入一个字符串,将其转换为对应的整型数据后输出。例如,主调函数接收的字
符串是“−12a3b4”
,则转换后对应的整型数据是−1234。
分析:
将一个字符串转换为对应的整型数据,其基本思想是,顺序扫描字符串的每一个字符,如果
扫描到的字符是数字字符的话,将该字符减去字符‘0’
,得到该数字字符对应的整型数字,然后
乘以 10 再加上扫描到的下一个数字字符对应的整型数字,重复该过程,直到所有字符扫描完毕。
需要注意的是,在程序执行过程中,如果当前扫描到的字符不是数字字符时,直接跳过该字符不
做任何处理,顺序扫描下一个字符。
根据模块化程序设计的基本思想,定义一个函数来实现转换功能。在主函数中接收从键盘
输入的字符串,将该字符串作为参数传递给定义的函数,该函数实现数据转换后将结果返回给
主函数。
程序:
#include<stdio.h>
int strToInt(char *p);
int main()
{
char str[6];
int result;
printf("please intput a string: ");
scanf("%s", str);
result = strToInt(str);
printf("the result is: %d\n", result);
return 0;

156
第6章 指针

}
int strToInt(char *p)
{
int st = 0,sign = 0;
if(*p == '+' || *p == '-')
{
sign = (*p=='+' ? 1 : -1);
p++;
}
while(*p)
{
if(*p >= '0' && *p <= '9')
st = st * 10 + (*p - '0');
p++;
}
if(sign == -1)
st = -st;
return st;
}
运行结果如下:

【例 6-19】 编程实现将某年某月某日转换为这一年的第几天。例如,2008 年 8 月 8 日是这一


年的第 221 天。
分析:
该程序需要设立一张每月天数表,指出每月有多少天。由于二月份平年是 28 天,闰年是 29
天,所以要用一个二维数组来表示这张表。然后,定义函数 days( )实现转换,在 days 函数中先判
断是否闰年,再通过指向数组的指针取出数组中该月份对应的天数求和,将结果返回到主函数中
输出。
本程序中,形参之所以采用指向数组的指针而不用二维数组的原因,主要是通过指针对内存
空间的操作,效率远高于数组,因此,在编写程序时,当实参是一个地址的话,形参通常采用指
针的方式来接收实参传递过来的地址值。
程序:
#include<stdio.h>
int days(int(*p)[13], int year, int month, int day);
int main()
{
int y, m, d, n;
static int day_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}};
printf("Please input year month day: ");
scanf("%d%d%d", &y, &m, &d);
n = days(day_tab, y, m, d);
printf("It is %d day\n", n);
return 0;
}
int days(int(*p)[13], int year, int month, int day)
{

157
C 语言程序设计(第 2 版)

int j, leap;
leap = year % 4 == 0 && year % 100 != 0 || year % 400 == 0;
for(j = 1; j < month; j++)
day += *(*(p + leap) + j);
return(day);
}
运行结果如下:

二维数组 day_tab 定义为 2 行 13 列。把它的列数定义为 13,是为了使第 j 列的值表示


第 j 月份的天数。月份是从 1~12 月,没有 0 月份,因而该数组中第 0 列的值是 0。

6.8 深入研究:多级指针问题
高于一级指针的指针,统称为多级指针。C 语言中利用多级指针,可以处理更复杂的问题。
【例 6-20】 分析以下程序的运行结果。
#include<stdio.h>
int main()
{
static int a[] = {1, 3, 5, 7};
int *p[3] = {a+2, a+1, a};
int **q = p;
printf("%d\n",*(p[0] + 1) + **(q + 2));
return 0;
}
运行结果如下:

指针数组 p 有 3 个元素,p[0]指向 a[2],p[1]指向 a[1],p[2]指向 a[0]。因 p[0]+1 指


向 a[3],*( p[0] + 1) = 7。二级指针 q 指向数组 p,即 q 指向 p[0],则 q+2 指向 p[2],*(p[2])
指向 a[0],**(p[2]) = 1。

【例 6-21】 分析以下程序的运行结果。
程序:
#include<stdio.h>
#include<string.h>
int main()
{
static char *c[] = {"you can make statement", "for the topic",
"The sentences", "How about"};
static char **p[] = {c+3,c+2,c+1,c};

158
第6章 指针

char ***pp = p;
printf("%s", **++pp);
printf("%s", *--*++pp+3);
printf("%s", *pp[-2]+3);
printf("%s", pp[-1][-1]+3);
printf("\n");
return 0;
}
运行结果如下:

指针数组 c 有 4 个元素,每个元素都是一个指向字符串的指针;指针数组 p 中的元


素分别指向指针数组 c 中的第 4,3,2,1 个元素,即 p[0]指向 c[3],p[1]指向 c[2],p[2]
指向 c[1],p[3]指向 c[0],都是二级指针;指针 pp 指向 p[0],是三级指针。其指向关系
如图 6-19 所示。

执行**++pp 时,先执行++pp,使 pp 指向 p[1],然后取**pp 的值,输出“The sentences”。


执行*−−*++pp+3 时,先执行++pp,使 pp 指向 p[2],*++pp 为 p[2]的值即指向 c[1],执行− −
*++pp 后,指针指向 c[0],取当前指针所指向的内容,返回“you can make statement”,执行
+3 后输出“ can make statement”。
执行*pp[−2]+3 时,此时 pp 指向 p[2],由于“[]”优先级大于“*”,先执行 pp[−2](pp 指针不
改变),即 p[0],*pp[−2]返回“How about”,执行+3 后返回“ about”。
执行 pp[−1][−1]+3 时,此时 pp 指向 p[2],pp[−1]返回 pp[1],pp[−1][−1]返回“for the topic”,
执行+3 运算后,返回“ the topic”。

图 6-19 各级指针的指向关系示意图

本章小结
指针是 C 语言中一种特殊的数据类型,定义的指针类型的变量称为指针变量,指针变量就是
存储变量地址的变量,通过该地址所指向的变量,实现指针对变量的间接访问。学习和掌握指针
的用法是 C 语言的难点,弄清地址、指针和指针变量三者的关系是学好指针的前提。在不会产生
混淆的情况下,通常将指针变量简称为指针,对两者不加区分。
定义变量时,在变量名前面加上“*”,表明该变量是指针变量。指针变量所指向的变
量可以是 C 语言中合法的任意数据类型。指向数组的指针和指向数组元素的指针是 C 语言

159
C 语言程序设计(第 2 版)

中常见的指针,利用这两种指针可以实现任何通过数组下标能完成的访问数组元素的操作,
从而使对数组元素的访问方式有两种:下标法和指针法,用指针操作比用数组操作通常效
率更高。
指针常见的另一个用法是作为函数的参数,在函数调用时,将实参的地址传递给形参,这种
参数传递方式称为“传址”
,从而实现通过被调函数中对形参的修改而改变实参的值。当需要从函
数返回多个值时,可以在该函数中定义多个指针类型的形参来实现。
还可以使指针指向一个函数,这样的指针称为函数指针,先后使指针指向不同的函数,可以
在主调函数中利用一个函数指针调用不同的函数。
必须注意,指针在使用前必须进行初始化,使其指向某个有意义的对象。未经初始化的指针
可能会指向某个不安全的地址,从而造成意想不到的结果。如果定义指针时还不明确应该指向哪
个变量,通常将其初始化为 NULL,这是一种良好的编程习惯。
在某些特殊情况下,用户希望自己申请内存空间用来存储相关数据,而不是由系统自动分配。
这类内存空间在使用完毕后必须由用户主动释放。ANSI C 标准建议设置两个函数 malloc( ) 和
free( )来完成内存的动态分配与释放。
指针是 C 语言的特色之一,正确灵活地使用指针,可以方便地求解某些实际问题,提高程序
的运行效率。

习 题
【复习】
1.指针的含义是( )

A.值 B.地址 C.名 D.一个标志
2.若已定义 a 为 int 型变量,则下面的语句中正确的是( )

A.int *p=a; B.int *p=*a;
C.int p=&a; D.int *p=&a;
3.若变量 x 已正确定义并且指针 p 已经指向变量 x,则&*p 相当于( )。
A.x B.*p C.p D.*&p
4.若有如下定义:
int a[8] = {2,4,6,8,10,12,14,16};
*p = a;
则下面的选项中,访问数组第 2 个元素“4”的正确方法是( )

A.a[1] B.p[1]
C.*p+1 D.*(p+1)
5.若有如下的语句段:
int s[2][3] = {0}, (*p)[3];
p = s;
则对数组 s 的第 i 行第 j 列(假设 i,j 已正确说明并赋值)元素地址的合法引用为( )。
A.*(p+i)+j B.*(p[i]+j)
C.(p+i)+j D.(*(p+i))[j]

160
第6章 指针

6.下面的语句段中,for 循环的执行次数是( )

char *s="\ta\018bc";
for(; *s!=’\0’;s++) printf("*");
A.9 B.5 C.6 D.7
7.假设 int (*p)[3]; 则以下叙述中正确的是( )

A.p 是一个指针数组
B.p 是一个指针,它只能指向一个每行包含 3 个 int 类型元素的二维数组
C.p 是一个指针,它可以指向一个一维数组中的任一元素
D.(*p)[3]与*p[3]等价
8.函数声明语句“void *fun( );”的含义是( )。
A.fun( )函数无返回值
B.fun( )函数的返回值可以是任意的数据类型
C.fun( )函数的返回值是空指针
D.指针 fun( )指向一个函数,该函数无返回值
9.阅读下面的程序:
#include<stdio.h>
int main()
{
int x[5]={2,4,6,8,10}, *p, **pp;
p = x;
pp = &p;
printf("%d", *(p++));
printf("%d\n", **pp);
return 0;
}
该程序的运行结果是:
( )。
A.4 4 B.2 4 C.2 2 D.4 6
10.阅读以下程序:
#include<stdio.h>
int main()
{
int a[10] = {2,4,6,8,10,12,14,16,18,20}, *p;
p = a;
printf("%x\n", p);
printf("%x\n", p + 9);
return 0;
}
若第一个 printf 语句输出的是 ffca,则第二个 printf 语句输出的是( )

A.ffdd B.ffdc C.ffde D.ffcd
【应用】
1.使用指针数组编写一个通用的英文月份名显示函数 void display(int month)。

2.编程实现将无符号八进制数字字符串转换为十进制整数。例如,输入的字符串为“556”
则输出十进制整数 366。
3.编写程序判断输入的字符串是否是“回文”,(顺读和倒读都一样的字符串称“回文”,
如 level)。

161
C 语言程序设计(第 2 版)

4.编写程序找出字符串中最大的字符并放在第一个位置上,并将该字符前面的原字符往后顺
序移动,如 chyab 变成 ychab。
【探索】
1.利用指针数组设计一个菜单管理程序,当输入字符串"save"、"open"、"quit"时,分别调用
其处理函数,完成不同的工作。各处理函数的具体工作可以自行设定,如可以仅显示出不同操作
的提示等。
2.Josephus 问题。有 30 个人围成一圈,顺序排号。从第一个人开始报数(从 1~4 报数)

凡是报到 4 的人退出圈子,最后留下的是原来的第几号。

162
第 章 7
结构体、共用体和枚举

本章目标
◇ 了解结构体和共用体的概念以及自定义数据类型的使用方法
◇ 掌握结构体类型的定义和使用方法
◇ 熟悉枚举类型的定义和使用方法
◇ 学会恰当应用共用体和结构体解决特定问题

7.1 结构体类型

7.1.1 结构体类型的提出
在日常生活中,常常需要填写一些登记表,如住宿登记表、学生入学登记表或职员通信表等。
在职员通信表中,通常需要登记姓名、工作单位、家庭住址、邮编、电话号码和 E-mail 等数据项,
如图 7-1 所示。

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

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

图 7-1 职员通信表的构成

职员通信表集合了多种数据,每个职员的这些数据项的值都不同,为了方便这种复合数据的
处理,C 语言引入了一种新的构造数据类型:结构体。在实际应用中,结构体类型由用户根据需
要进行定义。

7.1.2 结构体类型的定义
结构体类型定义的一般形式如下:
struct 结构体名
{
类型说明符 1 成员名 1;
类型说明符 2 成员名 2;

};
其中,struct 是 C 语言中的关键字,指明后面出现的标识符是一个用户定义的结构体类型的

163
C 语言程序设计(第 2 版)

名字,结构体名是合法的用户自定义的 C 标识符,花括号“{}”是结构体类型的定界符,花括号
中给出该结构体包含的数据项,称为结构体成员,每个成员都要有自己的数据类型,可以是 C 语
言允许的任何数据类型;花括号后的分号“;”是结构体类型定义的结束符。
例如:
struct Employee
{
char name[20]; /* 姓名 */
char department[30]; /* 工作单位 */
char address[30]; /* 家庭住址 */
long box; /* 邮编 */
long phone; /* 电话号码 */
char email[30]; /* E-mail */
};
定义了一个结构体类型,用于描述职员通信表。结构体名是 Employee,包含 6 个成员,分别是
name,department,address,box,phone 和 email,类型分别为字符数组、字符数组、字符数组、
long int 型、long int 型、字符数组。

M提醒
定义结构体类型时,必须要有定界符“{ }”与结束符“;”,它们是结构体类型定义时不可
缺少的组成部分。

同一结构体类型内的成员名不能相同,但是不同结构体类型中的成员名可以相同。

例如,
struct Test1
{
int x;
int y;
int z;
};
struct Test2
{
int a;
int b;
int z;
};
是合法的。
在结构体类型 struct Employee 中,如果增加一个结构体成员 birthday,用于描述职员的出生
日期,则需要把日期定义为一个结构体,然后将其作为 Employee 中的成员 birthday 的数据类型。
具体定义如下。
struct Date
{
int day;
int month;
int year;
};

164
第7章 结构体、共用体和枚举

struct Employee
{
char name[20];
char department[30];
char address[30];
long box;
long phone;
char email[30];
struct Date birthday; /* 出生日期,数据类型为 strut Date */
};

M提醒
一定要严格区分结构体名和结构体类型名,关键字 struct 和结构体名共同构成结构体类型
名。如上例中的“struct Date”。

“结构体”这个词是根据英文单词 structure 翻译而来的,也有很多书将其译为“结构”。另外,


结构体名又称做“结构体标记”,结构体中的成员表列又被称为“域表”。
需要强调的是,整型、实型、字符型等是系统提供的标准数据类型,结构体类型是用户根据
实际问题的需要定义的数据类型,它们都是 C 语言中合法的数据类型。因此,结构体类型定义后,
必须定义该类型的变量才能进行相关的运算处理,从而完成指定的功能。

7.2 结构体类型变量

7.2.1 结构体类型变量的定义
结构体类型变量与其他类型的变量一样,也遵循“先定义,后使用”的原则。
结构体类型变量定义的一般形式如下:
结构体类型名 变量名列表;
对于 7.1 节中定义的描述职员通信表的结构体类型 struct Employee,要想定义该类型的变量
可以写成如下形式:
struct Employee employee1, employee2;
其中,struct Employee 是结构体类型名,employee1 和 employee2 都是 struct Emplyee 类型的
变量。

M提醒
定义结构体类型变量时,变量名可以和结构体成员名相同。

为了程序书写方便,可以采用紧凑格式进行结构体类型变量的定义,也就是在定义结构体类型的同
时定义结构体类型变量。
这种定义方法的一般形式如下:
struct 结构体名
{
成员列表

165
C 语言程序设计(第 2 版)

}变量名列表;
例如:
struct Employee
{
char name[20];
char department[30];
char address[30];
long box;
long phone;
char email[30];
}employee1,employee2;
在定义结构体类型 struct Employee 的同时,定义了 struct Employee 类型的变量 employee1 和
employee2。
在采用紧凑格式定义结构体类型变量的方法中,还可以省略结构体名。
这种定义方法的一般形式如下:
struct
{
成员列表
} 变量名列表;
例如,同样是职员通信表,也可以定义成如下形式:
struct
{
char name[20];
char department[30];
char address[30];
long box;
long phone;
char email[30];
}employee1,employee2;
这种定义形式省略了结构体名,在此定义语句后面无法再定义该结构体类型的其他变量,除
非把定义过程重写一遍。因此,为了提高程序的可维护性,通常不建议采用省略结构体名的定义
形式。
在不发生混淆的情况下,结构体类型变量通常简称为结构体变量。

M技巧
结构体变量占用空间较大,因此,如果程序规模较大,使用的结构体较多,往往将结构体
类型的定义集中放到一个以“.h”为后缀的头文件中。哪个源文件需要用到此结构体类型时,
可以用#include 命令包含该头文件,这样做便于编译、修改和使用。

需要强调的是,结构体类型和结构体变量是两个完全不同的概念。在前面的例子中,struct
Employee 是结构体类型,而 employee1 和 employee2 是结构体变量,一个结构体变量所占用的内
存空间是该结构体类型中各个成员所占内存空间之和。例如结构体变量 employee1 在内存中占
118Byte(20+30+30+4+4+30=118)。

7.2.2 结构体类型变量的引用
在 C 程序中,不能将结构体变量作为一个整体进行引用,只能引用结构体变量中的成员。

166
第7章 结构体、共用体和枚举

结构体变量引用的一般形式如下:
结构体变量名.成员名
其中,
“.”是结构体成员运算符,它在运算符中优先级最高,结合性是自左向右。
【例 7-1】 分析以下程序的运行结果,注意结构体变量的定义和引用。
程序:
#include<stdio.h>
int main()
{
struct Date
{
int day;
int month;
int year;
} today;
today.day = 8;
today.month = 8;
today.year = 2008;
printf("Today's date is %d-%d-%d.\n", today.year, today.month, today.day);
return 0;
}
运行结果如下:

对结构体变量 today 中各成员变量的引用形式为:today.day,today.month 和 today.


year,并且对各成员分别赋值为:8,8 和 2008。

7.2.3 结构体类型变量的初始化
对结构体类型变量进行初始化,就是对结构体变量的各个成员进行初始化,即在定义结构体
变量的同时,为各个成员赋初值。
结构体类型变量初始化的一般形式如下:
结构体类型名 结构体变量名 = {各成员初值列表};
例如:
struct Employee
{
char name[20];
char department[30];
char address[30];
long box;
long phone;
char email[30];
};
struct Employee employee1 = {"liping", "Business department", "Changchun
China",130000,88547362, "liping@163.com"};
定义了结构体类型 struct Employee 之后,在定义该类型的结构体变量 employee1 的同时,为各成
员 name,department,address,box,phone,email 分别赋初值为"liping","Business department",

167
C 语言程序设计(第 2 版)

"changchun China",130000,88547362,“liping@163.com”。其存储示意图如图 7-2 所示。

地址
占用字节数

0x2000
liping 20

0x2014
Business department 30

0x2032
Changchun China 30

0x2050
130000 4

0x2054
88547362 4

0x2058
liping@163.com 30

图 7-2 结构体变量 employeel 的存储示意图

和数组的初始化类似,结构体变量也可以采用部分初始化的方式。
例如:
struct Employee employee1 = {"liping", "Business department"};
将结构体变量 employee1 中的成员 name 初始化为"liping",成员 department 初始化为"Business
department",其他成员的初值是未定义的,由编译系统自行决定,可能为 0,也可能为随机值。

M提醒
与数组类似,只有在初始化时才可以对结构体变量整体赋值,除此之外只能分别给每个成
员赋值。

7.3 结构体类型数组

7.3.1 结构体类型数组的定义
在实际应用中,定义了一个结构体类型后,通常需要定义该类型的数组实现问题域中的数据
存储。
例如:
struct Employee
{
char name[20];
char department[30];
char address[30];
long box;

168
第7章 结构体、共用体和枚举

long phone;
char email[30];
};
struct Employee employee[30];
定义了一个结构体类型数组 employee,该数组中有 30 个元素,每个元素都是 struct Employee 类型。
结构体类型数组同结构体类型变量一样,也有三种定义形式:
(1)先定义类型,再定义数组;
(2)定义类型的同时定义数组;
(3)定义匿名类型的同时定义数组。

7.3.2 结构体类型数组的引用
结构体类型数组引用的一般形式如下:
数组名[下标].成员名;
例如,对于结构体类型数组 employee,要获取第一个职员的姓名,也就是第一个数组元素
employee[0]中的第一个成员的值,引用形式为 employee[0].name。
【例 7-2】 输出某班不及格学生的名单和人数。要求:学生名单中包含的学生信息有学号、姓
名、性别、年龄、分数。
分析:
首先需要定义结构体类型用于描述学生的相关信息,并定义该结构体类型的数组用于保存学
生的相关数据。然后利用单循环实现不及格人数的计算。定义一个累加器 sum_failed,当学生的
分数低于 60 分时累加器增 1,直到所有学生都处理完为止。
算法描述:
(1)定义符号常量 N,值为 6;
(2)定义结构体 Student,成员为 num、name、sex、age、score;
(3)定义累加器 sum_failed,sum_failed ← 0;
(4)定义结构体类型数组 array_student 并初始化;
(5)循环变量 i ← 0,循环条件 i < N,循环执行以下语句
如果 array_student[i].score < 60 则
1)sum_failed++;
2)输出 array_student[i]各成员的值。
(6)输出不及格人数。
程序:
#include<stdio.h>
#include<stdlib.h>
#define N 6
int main()
{
struct Student
{
int num;
char *name;
char sex[6];
int age;
float score;
};
int i, sum_failed = 0;
/* 定义结构体类型数组并进行初始化 */

169
C 语言程序设计(第 2 版)

struct Student array_student[N] =


{
{10101, "Zhanghaitao", "man", 22, 98.1},
{10102, "Lichunling", "woman", 22, 99.8},
{10103, "Wanggang", "man", 21, 96.5},
{10104, "Zhaoxin", "man", 20, 16.5},
{10105, "Zhangzhenyu", "man", 23, 56.6},
{10106, "Dingyundong", "man", 22, 67.4}
};
/*计算不及格学生人数,并输出不及格学生信息*/
for(i = 0; i < N; i++)
if(array_student[i].score < 60)
{
sum_failed++;
printf("%d, %s, %s, %d, %f\n",array_student[i].num,array_student[i].name, array_
student[i].sex,array_student[i].age, array_student[i].score);
}
printf("failed: %d\n", sum_failed);
return 0;
}
运行结果如下:

7.4 结构体类型指针

7.4.1 指向结构体变量的指针
定义一个指针用来指向一个结构体变量时,该指针中的值就是它所指向的结构体变量的首地
址,通常称为结构体类型指针。
结构体类型指针定义的一般形式如下:
结构体类型名 * 结构体类型指针变量名;
例如:
struct Employee *pEmployee;
定义了一个指向结构体类型 struct Employee 的变量的指针 pEmployee。
在定义结构体类型的同时也可以定义结构体类型指针。
例如:
struct Employee
{
char name[20];
char department[30];
char address[30];
long box;
long phone;

170
第7章 结构体、共用体和枚举

char email[30];
} *pEmployee = Null;
结构体类型指针与其他类型的指针一样,必须遵循“先赋值,后使用”的原则。
例如:
struct Employee employee1, *pEmployee = NULL;
pEmployee = &employee1;
定义了一个指向 struct Employee 的结构体类型指针 pEmployee,使它指向结构体变量 employee1。
此时,可以存取由 pEmployee 所指向的结构体变量 employee1 中的任何一个成员。C 语言中,
利用指针访问结构体成员的方式有两种:
(1)使用“.”运算符
使用“.”运算符访问结构体成员的一般形式如下:
(*结构体类型指针变量).成员名
例如,语句:
(*pEmployee).phone = 88547362;
首先通过“*”运算符得到指针 pEmployee 所指向的结构体变量 employee1,然后通过“.”
运算符取得 employee1 的成员 phone,赋值为 88547362。

M提醒
由于“.”运算符的优先级高于“*”运算符,因此圆括号不能省略。

(2)使用“−>”运算符
该运算符的符号“−>”由一个减号和一个大于号组成。
使用“−>”运算符访问结构体成员的一般形式如下 :
结构体类型指针变量->成员名
例如,语句:
pEmployee -> phone = 88547362;
的作用与上例完全相同。
通常建议使用“−>”运算符,因为这种方式更容易理解,可读性更强。
【例 7-3】 使用结构体类型指针重新实现例 7-1,注意结构体类型指针的使用。
程序:
#include<stdio.h>
int main()
{
struct Date
{
int day;
int month;
int year;
};
struct Date today, *pDate;
pDate = &today;
pDate -> day = 8;
pDate -> month = 8;
pDate -> year = 2008;
printf("Today's date is %d-%d-%d.\n", pDate -> year, pDate -> month, pDate -> day);
}

171
C 语言程序设计(第 2 版)

运行结果如下:

7.4.2 指向结构体数组的指针
在应用中,也可以定义一个指针,用于指向结构体类型数组。
例如:
struct Employee
{
char name[20];
char department[30];
char address[30];
long box;
long phone;
char email[30];
};
struct Employee array_emp[30],*pEmployee = NULL;
pEmployee = array_emp;
定义了一个结构体类型指针 pEmployee 指向了结构体类型数组 array_emp,即指向了该数组的首
元素。
此时,访问数组 array_emp 的第一个职员的姓名可以采用如下形式:
pEmployee -> name
同理,访问第 i 个职员的姓名可以写成“(pEmployee + i) -> name”的形式。
【例 7-4】 分析以下程序的运行结果,注意结构体类型指针的使用。
程序:
#include<stdio.h>
struct Date
{
int year, month, day;
};
struct Student
{
char name[20];
int stuID;
struct Date birthday;
};
int main()
{
int i;
struct Student *pStudent;
struct Student array_student[4] = { {"liying", 1, 1978, 5, 23},
{"wangping", 2, 1979, 3, 14},
{"lijun", 3, 1980, 5, 6},
{"xuyan", 4, 1980, 4, 21} };
pStudent = array_student;
for(i = 0; i < 4; i++)
{

172
第7章 结构体、共用体和枚举

printf("%-12s", (pStudent + i) -> name);


printf("%-6d", (pStudent + i) -> stuID);
printf("%5d", (pStudent + i) -> birthday.year);
printf("%5d", (pStudent + i) -> birthday.month);
printf("%5d\n", (pStudent + i) -> birthday.day);
}
return 0;
}
运行结果如下:

首先定义了一个结构体类型为 struct Student 的数组 array_student,并进行了初始化;


然后定义了一个指针 pStudent 用于指向结构体数组元素,通过赋值语句“pStudent =
array_student;”使 pStudent 指向第一个元素 array_student[0];最后在 for 语句中通过一系列
形如“(pStudent + i) -> name”的表达式依次输出学生的相关信息:姓名、学号、出生日期。

需要强调的是,由于结构体类型 struct Student 中的成员 birthday 的类型是另一个结构体类型


struct Date,因此对成员 birthday 的访问还要指出具体的成员名,也就是在 birthday 的后面要用“.”
运算符,即“(pStudent + i) −> birthday.year”。

7.5 结构体与函数
定义函数时,函数的返回值可以是结构体类型,结构体变量也可以作为函数的参数。
【例 7-5】 分析以下程序的运行结果,注意结构体类型在函数定义中的使用。
程序:
#include<stdio.h>
#include<math.h>
struct Comp
{
float x, y;
float m;
};
int main()
{
float compare(struct Comp c1, struct Comp c2);
struct Comp comp_a, comp_b;
printf("Please input two struct Comp variables with two members(x,y):\n");
scanf("%f,%f",&comp_a.x, &comp_a.y);
scanf("%f,%f",&comp_b.x, &comp_b.y);
if(fabs(compare(comp_a, comp_b)) <= 1.0e-5)
printf("Equal\n");
else
printf("Unequal\n");
return 0;

173
C 语言程序设计(第 2 版)

}
float compare(struct Comp c1, struct Comp c2)
{
c1.m = sqrt(c1.x * c1.x + c1.y * c1.y);
c2.m = sqrt(c2.x * c2.x + c2.y * c2.y);
return (c1.m – c2.m);
}
运行结果如下:

程序中结构体类型 struct Comp 被定义为外部类型,这样同一个文件中的所有函数都


可以使用;在函数 compare( )中定义了两个类型为 struct Comp 的形参 c1 和 c2,用于接收
主函数 main( )中的结构体变量;函数 compare( )的功能是,首先分别计算 c1 和 c2 的两个
成员的平方和,然后计算它们的差并返回给主函数;主函数通过“fabs(compare(comp_a,
comp_b)) <= 1.0e-5”判断返回的差值是否为 0,如果为 0 则这两个复数相等。

需要强调的是,尽管两个浮点数相等,但它们的差在内存中存储的并非是 0 值,而是非常小
的一个小数,如本题中选用的 1.0e-5。
【例 7-6】 分析以下程序的运行结果,注意函数的返回值为结构体类型时的应用。
程序:
#include<stdio.h>
struct Student
{
char number[9];
char name[8];
char department[20];
};
struct Student input();
int main()
{
struct Student student1;
student1 = input();
printf("\n 学号:%s 姓名:%s 所在系:%s\n",
student1.number, student1.name, student1.department);
return 0;
}
struct Student input()
{
struct Student temp;
printf("学号:");
gets(temp.number);
printf("姓名:");
gets(temp.name);
printf("所在系:");
gets(temp.department);
return temp;
}
174
第7章 结构体、共用体和枚举

运行结果如下:

函数 input( )的功能是实现学生相关信息(学号、姓名、所在系)的输入,并将输入的
结果返回到主函数中进行相关处理,因此函数 input( )的返回值为结构体类型 struct Student。

7.6 共 用 体

7.6.1 共用体类型的提出
定义一个结构体变量后,系统会为其分配一片连续的内存空间,这片空间的大小就是该结构
体变量的各成员长度之和。也就是说,结构体变量中的成员在内存中是顺序存放的,每个成员都
会根据它的数据类型占用一定大小的内存单元。因此,如果结构体的成员较多,会占用较大的存
储空间。在某些实际应用中,可能会有这样一种情况:根据问题的需要在某一时刻仅使用其中的
某一个成员的值,此时如果可以让这些成员共享一片内存空间,不但可以实现问题求解,还可以
节省存储空间。
为了完成上述特点的数据存储,C 语言提出了一种新的构造类型:共用体。在共用体类型中,
各成员共享一段内存单元,一个共用体变量的长度等于各成员中最长成员的长度。使用共用体的
重要原因是它能方便程序设计人员在同一内存区交替使用不同的数据类型,增加了程序的灵活性,
节省了占用内存的空间。
结构体和共用体在定义和使用上有许多相似之处,但两者有本质上的差别。

7.6.2 共用体类型的定义
在使用共用体时,也应遵循“先定义类型,再定义变量”的原则。
共用体类型定义的一般形式如下:
union 共用体名
{
成员列表
};
例如:
union Info
{
int class;
char position[20];
};
定义了一个共用体类型 union Info,有两个成员,分别是 class 和 position,这两个成员共用一片大
小为 20 个字节的内存空间。

175
C 语言程序设计(第 2 版)

M提醒
在形式上与结构体类型定义相比,差别仅在于将关键字 struct 替换成 union,但系统分配内
存的方式不同。

7.6.3 共用体变量的定义
定义了共用体类型之后,就可以定义该类型的变量了,其定义形式与结构体类型变量相似,
也有三种形式。
第一种形式:
union Info
{
int class;
char position[20];
};
union Info dept;
第二种形式:
union Info
{
int class;
char position[20];
} dept;
第三种形式:
union
{
int class;
char position[20];
} dept;

7.6.4 共用体变量的引用
共用体变量引用的一般形式如下:
共用体变量名.成员名
例如:
dept.class = 2;
引用共用体变量 dept 的成员 class,并赋值为 2。
【例 7-7】 教师和学生共同使用以下表格,编程输入各人员数据后以表格形式输出,如表 7-1 所示。

表 7-1 人员信息表
姓 名 年 龄 职 业 单 位
张三 36 教师 计算机教研室
李四 21 学生 1001

其中,如果职业是学生,则单位应填入该学生所在的班级,如果职业是教师,则单位应填入
该教师所在的教研室。这里班级的类型是整型,教研室的类型是字符数组。
分析:
教师和学生都需存储姓名、年龄、职业和单位等数据信息,可通过定义一个结构体 Info 来实
176
第7章 结构体、共用体和枚举

现。但教师单位和学生单位的类型不同,如果输入的人员是教师,则单位需填写教研室,如果输
入的人员是学生,则单位需填入班级。因此,可在结构体中定义一个共用体来统一存储教师和学
生的单位信息,在存储数据时根据教师和学生分别存储不同类型的数据即可。
算法描述:
(1)定义结构体类型 struct info,成员包括:name,age,job,department;
(2)将 department 定义为共用体类型,成员包括:class,position;
(3)定义结构体数组 person;
(4)输入教师或学生的信息:
1)输入姓名,存储到 person[i]的 name 成员中;
2)输入年龄,存储到 person[i]的 age 成员中;
3)输入职业,存储到 person[i]的 job 成员中;
4)如果 person[i].job = = 's' ,则输入班级号,存储到 person[i]的成员 department 的 class 中;
如果 person[i].job == 't' ,则输入教研室名称,存入 person[i]的成员 department 的
position 中;
(5)输出结构体变量 person[i]各成员的值。
程序:
#include<stdio.h>
#include<stdlib.h>
struct Info
{
char name[20];
int age;
char job;
union
{
int class;
char position[20];
} department;
};
int main()
{
int i;
struct Info person[2];
char numstr[20], ch;
for(i = 0; i < 2; i++)
{
printf("please input data of person[%d]:\n",i);
gets(person[i].name);
gets(numstr);
person[i].age = atoi(numstr);
person[i].job = getchar();
ch = getchar( );
if(person[i].job == 's')
{
printf("input number of class:\n");
gets(numstr);
person[i].department.class = atoi(numstr);
}
else if(person[i].job == 't')

177
C 语言程序设计(第 2 版)

{
printf("input position:\n");
gets(person[i].department.position);
}
else
printf("input error!");
printf("\n");
}
printf(" name age job class/position\n");
for(i = 0; i < 2; i++)
if(person[i].job == 's')
printf("%−14s%3d%8c%20d\n", person[i].name, person[i].age, person[i].job,
person[i].department.class);
else
printf("%−14s%3d%8c%20s\n", person[i].name, person[i].age, person[i].job,
person[i].department.position);
return 0;
}
运行结果如下:

M提醒
对于共用体的成员,在某一时刻只有一个成员有意义,因为在共用体中,两个成员共用同
一段存储单元。

7.7 枚 举
在编写程序时,往往会遇到某些变量只能在一个有限的范围内取值的情况。例如,一个星期
有 7 天,一年有 12 个月,扑克牌的花色只有黑桃、红桃、方块和梅花等。如果把这些量定义为
int 型或 short int 型,则不能体现出这种量的含义和取值的限制,而且会造成处理上的麻烦。为此,
在标准的 C 语言中提供了枚举数据类型。
枚举是一个有名字的整型常量的集合,枚举类型的变量只能取集合中列举出来的合法值。

7.7.1 枚举类型的定义
枚举类型同样遵循“先定义类型再定义变量”的原则。
178
第7章 结构体、共用体和枚举

枚举类型定义的一般形式如下:
enum 枚举类型名
{
枚举值列表
};
其中,enum 是标志枚举类型的关键字,枚举值列表中的各个值用“,”隔开,枚举值也称为
枚举元素。
例如:
enum Color
{
red, green, blue, yellow, white
};
定义一个表示颜色的枚举类型 enum Color,这里 Color 是枚举类型名,枚举元素包括:red,green,
blue,yellow 和 white。
枚举类型定义后,系统会为每个枚举元素定义一个枚举值,默认从 0 开始。上例中的枚举元
素 red 的枚举值为 0,green 的枚举值为 1,blue 的枚举值为 2,yellow 的枚举值为 3,white 的枚
举值为 4。
也可以根据需要把某个枚举元素的值赋值为指定的整常数,此时该枚举元素后面的元素的值
将接着此整常数递增。
例如:
enum Color
{
red, green, blue = 5, yellow, white
};
定义了一个枚举类型 enum Color,对于 blue 之前的枚举元素 red 和 green,编译系统从 0 开始赋
予枚举值,而对 blue 之后的各枚举元素则从 6 开始赋值。结果如下:red 的值为 0,green 的值
为 1,blue 的值为 5,yellow 的值为 6,white 的值为 7。

枚举值是常量,而不是变量,不能在程序中用赋值语句对它进行赋值,也不能用“&”
运算符取其地址。

例如,对枚举类型 enum Color 的元素做以下赋值:


red = 5;
green = 8;
是错误的。

7.7.2 枚举变量的定义
枚举变量的定义与前面介绍的结构体和共用体变量的定义类似,有以下三种形式。
(1)先定义枚举类型,后定义枚举变量
其一般形式如下:
enum 枚举类型名
{
枚举值列表
};

179
C 语言程序设计(第 2 版)

enum 类型名 变量名列表;


例如,下面的语句段:
enum Color
{
red, green, blue, yellow, white
};
enum Color change, select;
定义了 2 个枚举类型 enum Color 的变量 change 和 select。
(2)定义枚举类型的同时定义枚举变量
其一般形式如下:
enum 枚举类型名
{
枚举值列表
}变量名列表;
例如:
enum Color
{
red, green, blue, yellow, white
}change, select;
的作用与上例相同。
(3)用无名枚举类型定义枚举变量
其一般形式如下:
enum
{
枚举值列表
}变量名列表;
例如:
enum
{
red, green, blue, yellow, white
}change, select;

7.7.3 枚举变量的使用
在使用枚举变量时,只能将枚举元素赋值给枚举变量,不能把枚举值直接赋值给枚举变量。
例如:
enum Color
{
red, green, blue, yellow, white
}change, select;
change = red;
是正确的,而
change = 0;
是错误的。
【例 7-8】 根据用户的输入确定下一天是星期几。要求采用枚举类型实现。
程序:
#include <stdio.h>
#include <string.h>

180
第7章 结构体、共用体和枚举

enum Day {Sun, Mon, Tue, Wed, Thu, Fri, Sat};


enum Day day_after(enum Day d)
{
enum Day nextDay;
switch(d)
{
case Sun: nextDay = Mon; break;
case Mon: nextDay = Tue; break;
case Tue: nextDay = Wed; break;
case Wed: nextDay = Thu; break;
case Thu: nextDay = Fri; break;
case Fri: nextDay = Sat; break;
case Sat: nextDay = Sun; break;
}
return(nextDay);
}
int main()
{
enum Day ex, ey;
char str[5];
printf("input current date:");
scanf("%s", str);
if(strcmp(str, "Sun") == 0)
ex = Sun;
else if(strcmp(str, "Mon") == 0)
ex = Mon;
else if(strcmp(str, "Tue") == 0)
ex = Tue;
else if(strcmp(str, "Wed") == 0)
ex = Wed;
else if(strcmp(str, "Thu") == 0)
ex = Thu;
else if(strcmp(str, "Fri") == 0)
ex = Fri;
else if(strcmp(str, "Sat") == 0)
ex = Sat;
ey = day_after(ex);
switch(ey)
{
case Sun: printf("tomorrow is Sun.\n"); break;
case Mon: printf("tomorrow is Mon.\n"); break;
case Tue: printf("tomorrow is Tue.\n"); break;
case Wed: printf("tomorrow is Wed.\n"); break;
case Thu: printf("tomorrow is Thu.\n"); break;
case Fri: printf("tomorrow is Fri.\n"); break;
case Sat: printf("tomorrow is Sat.\n"); break;
}
return 0;
}
运行结果如下:

181
C 语言程序设计(第 2 版)

为了得到给定日期的下一天是星期几,首先应得到当前日期是星期几,利用 C 库函
数 strcmp( )实现判断当前给定日期是星期几的功能,再调用函数 day_after( )得到下一天
是星期几。

7.8 自定义数据类型
前面介绍了 C 语言中丰富的数据类型,包括系统定义的简单类型(如整型、实型、字符
型等)和用户定义的构造类型(如结构体、共用体等)。为了提高 C 语言程序的可移植性,C
语言还提供了一种为数据类型起“别名”的方法,使用 typedef 定义新的类型名来代替原有的
类型名。
typedef 语句的一般形式如下:
typedef 原类型名 新类型名;
其中,原类型名必须是 C 语言提供的标准数据类型或用户自定义的数据类型。
例如:
typedef float REAL;
定义了 float 的别名为 REAL,此后可以用 REAL 来代替 float 对变量进行定义。
例如:
REAL x, y;
等价于
float x,y;
C 编译系统把变量 x 和 y 作为一般的 float 型变量来处理。

M提醒
使用 typedef 定义的数据类型仅是已有数据类型的别名,而不会产生一个新的数据类型。

下面举例说明用 typedef 定义结构体类型别名的方法。


typedef struct Student
{
int num; /* 学号 */
char * name; /* 姓名 */
char sex[3]; /* 性别 */
int age; /* 年龄 */
float score; /* 成绩 */
}StudentType;
这时,可以用新定义的类型名 StudentType 来代替结构体类型 struct Student,因此,变量定义
StudentType person;
相当于
struct Student person;

StudentType *pStudent;

182
第7章 结构体、共用体和枚举

相当于
struct student * pStudent;
需要强调的是,使用 typedef 有利于程序的移植。
例如,在有些计算机系统中,int 型数据占用 2 个字节,而在另外的一些计算机系统中 int 型
数据占用 4 个字节,如果将一个 C 语言程序从一个以 4 个字节存放 int 型数据的计算机系统移植
到以 2 个字节存放 int 型数据的计算机系统中,一般的方法是将程序中的所有 int 型都改为 long
型,这样会造成程序的大量修改。但是,如果在程序中使用了下面的定义:
typedef int INTEGER;
并且,
程序中所有的 int 型变量都用 INTEGER 来定义,那么在进行移植时,只需要改动 typedef
定义即“typedef long INTEGER;”,程序的其他部分无须进行任何修改。

7.9 综 合 实 例
【例 7-9】 创建一个存放正整数(输入-999 做结束标志)的单链表,并输出该链表中的正整数。
分析:
单链表是一种比较简单的链式存储结构,通过在结构体类型的定义中增加一个成员来实现,
该成员是指向该结构体的指针。
下面是一种典型的单链表的定义:
struct Node
{
char ch;
struct Node *next; /*定义指向结构体自身的指针*/
};
在该定义中,有两个结构体成员,其中,第一个成员用于存储数据值,根据实际问题的需要,
可以定义为不同的类型,同时也可以定义多个成员,通常称为数据域。next 是指向结构体自身的
指针,通常称为指针域。
在链表的创建过程中,链表的头指针是非常重要的参数。因为对链表的输出和查找都要从链
表头开始,所以链表创建成功后,要返回一个链表头节点的地址,即头指针。一般情况下,不能
修改头指针的值。
struct Node *head;
定义了一个指向结构体变量的头指针 head,这里把结构体变量称为节点。
假设结构体变量 A 的地址是 0x1294,B 的地址是 0x1723,C 的地址是 0x1586,则下面的程序代码:
head = &A;
A.ch = 'a';
A.next = &B;
B.ch = 'b';
B.next = &C;
C.ch = 'c';
C.next = NULL;
可以完成如下功能:指针变量 head 保存节点 A 的存储地址,节点 A 的指针域保存节点 B 的存储
地址,节点 B 的指针域保存节点 C 的存储地址,节点 C 的指针域为空,写作 NULL。这样就形成
了一个单链表。形成的单链表存储示意图如图 7-3 所示。

183
C 语言程序设计(第 2 版)

head 0x1294 0x1723 0x1586


0x1294 a 0x1723 b 0x1586 c NULL

图 7-3 一个简单的单链表存储示意图

在链表的实际应用中,节点应根据需要用函数 malloc( )动态分配。


程序:
#include<stdlib.h> /* 包含 malloc()函数所在的头文件 */
#include<stdio.h>
struct Node /* 链表节点的结构 */
{
int num;
struct Node *next;
};
struct Node *creat(struct Node *head);
void print(struct Node *head);
int main()
{
struct Node *head; /* 定义头指针 */
head = NULL; /* 建一个空表 */
head = creat(head); /* 创建单链表 */
print(head); /*打印单链表*/
return 0;
}
struct Node *creat(struct Node *head) /* 函数返回的是与节点相同类型的指针*/
{
struct Node *p1, *p2;
p1 = p2 = (struct Node*) malloc(sizeof(struct Node)); /* 申请新节点*/
scanf("%d", &p1 -> num); /* 输入节点的值 */
p1 -> next = NULL; /* 将新节点的指针置为空 */
while(p1 -> num > 0) /* 输入节点的数值大于 0 */
{
if(head == NULL)
head = p1; /* 空表,接入表头 */
else p2 -> next = p1; /* 非空表,接到表尾 */
p2 = p1;
p1 = (struct Node *)malloc(sizeof(struct Node)); /* 申请下一个新节点 */
scanf("%d", &p1 -> num); /* 输入节点的值 */
}
p2->next = NULL;
return head; /* 返回链表的头指针 */
}
void print(struct Node *head) /* 输出以 head 为头指针的链表各节点的值 */
{
struct Node *temp;
temp = head; /* 取得链表的头指针 */
while(temp != NULL) /* 只要是非空表 */
{
printf("%6d", temp -> num); /* 输出链表节点的值 */

184
第7章 结构体、共用体和枚举

temp = temp -> next; /* 跟踪链表增长 */


}
Printf("\n")
}
运行结果如下:

7.10 深入研究:单链表的插入与删除
单链表同数组一样,可以作为存放数据的一种容器,但两者在数据的存储方式与访问方式上
差别很大。第一,数组的大小在定义时要事先规定,不能在程序中进行调整,因此在程序设计时
只能根据可能的最大需求来定义数组,常常会造成一定存储空间的浪费。第二,数组元素可通过
下标法随机存取,单链表节点只能从表头顺序访问。最后,数组元素的插入或删除操作需要大量
地移动数组元素,时间开销较大。
因此,这两种数据存储方式各有自身的特点,需要结合实际问题进行选择。两者比较而言,
单链表应用更为广泛,能够处理更加复杂的数据结构。
单链表创建起来以后,对单链表的常规操作包括数据的插入和删除。
(1)数据的删除
首先找到待删除数据所在的节点,然后通过修改该节点的前一个节点中指针的指向实现删除
功能。具体方法:从表头开始沿单链表依次查找待删除节点,然后考虑待删除节点的情况,即该
节点是表头节点、中间节点或表尾节点。删除单链表中节点的具体情况如图 7-4 所示。
① 删除单链表的中间节点 s,p->next = s->next

head p s
NULL

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

head
NULL

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

head p s

NULL

NULL

图 7-4 删除单链表中节点的三种情况

(2)数据的插入
首先创建一个存储数据的新节点,将数据存储到相应的数据域中;然后通过改变指针的指向

185
C 语言程序设计(第 2 版)

实现插入功能。同理,节点的插入也存在三种情况:在表头插入、在表中间插入和在表尾插入。
其示意图如图 7-5 所示。

① 在单链表的中间节点 p 后插入节点 r,r->next = p->next; p->next = r;

p
head
NULL

② 在表头节点 head 后插入节点 r,r->next = head->next; head->next = r;

head p
NULL

③ 在表尾节点 s 后插入节点 r,s->next = r; r->next = NULL;

head p s

NULL
r
NULL

图 7-5 插入节点操作示意图

在单链表的实际应用中,节点应根据需要用函数 malloc( )随时生成,删除节点时用函数 free( )


彻底删除。
【例 7-10】 定义一个函数 delete( ),实现对单链表中某节点的删除,并返回该链表的头指针。
单链表中存储学生的相关信息:学号、姓名,链表按学生的学号排列。
程序:
/* 本例实现了一个函数 delete(),用于完成单链表中的节点删除操作*/
#include<stdio.h>
struct
{
int num; /* 学生学号 */
char str[20]; /* 姓名 */
struct node *next;
};
struct node *delete(struct node *head, char *pstr) /* 以 head 为头指针,删除 pstr 所在节点*/
{
struct node *temp,*p;
temp = head; /* 链表的头指针 */
if (head == NULL) /* 链表为空 */
printf("\nList is null!\n");
else /* 非空表 */
{
temp = head;
while(strcmp(temp->str,pstr) != 0&&temp->next != NULL)
{ /* 若节点的字符串与输入字符串不同,并且未到链表尾 */

186
第7章 结构体、共用体和枚举

p = temp;
temp = temp->next; /* 跟踪链表的增长,即指针后移 */
if(strcmp(temp->str,pstr) == 0) /* 找到字符串 */
{
if(temp == head)
{ /* 表头节点 */
printf("Delete string :%s\n",temp->str);
head = head->next;
free(temp); /* 释放被删节点 */
}
else
{
p->next = temp->next; /* 表中节点 */
printf("Delete string :%s\n",temp->str);
free(temp);
}
}
else
printf("\nNot find the string!\n"); /* 没找到要删除的字符串 */
}
return(head); /* 返回表头指针 */
}
}

本章小结
结构体是 C 语言中非常重要的一种用户自定义的构造数据类型,通过结构体类型的定义,可
以在 C 语言程序中根据实际问题方便地表达复杂的数据结构,例如,学生信息表、职员信息表、
通讯录等,从而使问题求解更加容易。
定义结构体类型是使用结构体必不可少的一环,使用关键字 struct。有了结构体类型,可以定
义该类型的变量存储相关数据,数据的存储可采用初始化和赋值两种方式,采用赋值方式时,只
能使用成员运算符逐个引用结构体的每个成员,不能将结构体作为一个整体进行操作。对于结构
体变量的操作,只有取地址运算和引用成员运算两种。
当数组中的元素是结构体类型时,这个数组就是结构体类型数组;当指针所指向的变量是结
构体变量时,这个指针就是结构体指针。使用结构体类型数组和结构体指针能够解决应用领域中
的很多实际问题。例如,可以利用结构体类型数组存储一个班学生的相关信息,并根据业务需要
进行适当的处理,当结构体中的某个成员是指向该结构体的指针时,可以建立一种链表结构,从
而实现数据链式存储,这是一种非常典型的数据存储方式。
定义一个结构体变量后,系统会根据该结构体包含的成员所占用的内存空间的总和为其分配
存储空间。但有时针对某些特殊的具体问题,可能让这些成员共享一片存储空间就可以实现应有
的功能,这样,C 语言提供了一种新的构造数据类型:共用体,定义共用体类型时使用关键字 union,
在一个共用体类型内可以说明多种不同的数据类型的成员,在一个被定义为共用体类型的变量中,
各个成员共享同一个存储空间。
C 语言还提供了枚举类型,使用关键字 enum 来定义,通过枚举类型的定义和使用,可以提

187
C 语言程序设计(第 2 版)

高程序的可读性。
为了方便程序的移植,并且使数据类型本身具有和业务相关的含义,进一步提高程序的可读
性,C 语言还提供了一种用户定义数据类型的手段,使用关键字 typedef,这种方式不会产生新的
数据类型,仅实现对已有数据类型重命名的作用。

习 题
【复习】
1.设有以下说明语句:
struct ex
{
int x ;
float y;
char z ;
}example;
则下面的叙述中不正确的是( )。
A.struct 是结构体类型的关键字 B.example 是结构体类型名
C.x,y,z 都是结构体成员名 D.struct ex 是结构体类型
2.下面的叙述中正确的是( )。
A.结构体一经定义,系统就给它分配了所需的内存单元
B.结构体变量和共用体变量所占内存长度是各成员所占内存长度之和
C.可以对结构类型和结构类型变量赋值、存取和运算
D.定义共用体变量后,不能引用共用体变量,只能引用共用体变量中的成员
3.结构体类型变量在程序执行期间( )。
A.所有成员驻留在内存中
B.只有一个成员驻留在内存中
C.部分成员驻留在内存中
D.没有成员驻留在内存中
4.判断下面的有关结构体的定义或引用方法是否正确。正确的用 T 表示,错误的用 F 表示。
struct student
{
int no;
int score;
}student1;
A.student.score = 99;( )
B.student1.score = 99;( )
C.struct LiMing; LiMing.score = 99;( )
D.struct student LiMing; LiMing.score = 99;( )
E.student LiMing; LiMing.score = 99;( )
5.若有以下说明和定义:
typedef int INTEGER;
INTEGER p,*q;

188
第7章 结构体、共用体和枚举

以下叙述正确的是( )

A.p 是 int 型变量
B.p 是基类型为 int 的指针变量
C.q 是基类型为 int 的指针变量
D.程序中可用 INTEGER 代替 int 类型名
6.枚举类型定义如下:
enum Color1{yellow,green,blue = 7,red,brown};
则枚举常量 yellow 和 red 的值分别是( )。
A.3,8 B.1,8 C.0,8 D.0,3
7.阅读下面的程序:
#include<stdio.h>
#define Person_1 struct person_1
struct person_1
{
char name[31];
int age;
char address[101];
};
typedef struct person_2
{
char name[31];
int age;
char address[101];
} Person_2;
int main( )
{
Person_1 a = {"zhao",31,"east street 49"};
Person_1 b = a;
Person_2 c = {"Qian",25,"west street 31"};
Person_2 d = c;
printf("%s,%d,%s\n",b.name,b.age,b.address);
printf("%s,%d,%s\n",d.name,d.age,d.address);
return 0;
}
该程序的运行结果是: 。
【应用】
1.建立一个职工情况统计表,包括职工的工作证号、姓名、年龄、工资等内容。输出该单位
职工的平均年龄、平均工资和各年龄段职工人数的分布情况。
2.已知有 a、b 两个链表,节点的数据域包括学号和成绩。要求把 2 个链表合并。
3.建立一个链表,每个节点包括学号、姓名和年龄。输入一个学号,如果链表中的节点所包
含的学号等于此学号,则将此节点删去。
【探索】
1.用结构体类型编写一个程序,完成生命游戏的功能:在一个由方格组成的矩形阵列上,每
个方格可包含一个机体。每个方格和 8 个方格相邻,用 occ(k)表示与方格 k 相邻的包含机体的
方格数。各机体生死的规则是:
如果 2≤occ(k)≤3,那么在方格 k 中的机体继续活下去,否则该机体死亡;

189
C 语言程序设计(第 2 版)

如果 occ(k)=3,那么在方格 k 中诞生一个新机体。
编写一个程序,读入初始机体配置,按规则计算一系列生成过程,并打印出每一时间的配置
情况。为处理方便,不考虑矩形阵列最外围方格中机体的生死问题,但它们对临近方格中机体的
生成起作用。
2.选美比赛问题。在选美比赛的现场,有一批选手参加比赛,比赛的规则是最后得分越高,
名次越低。当比赛结束时,要在现场按照选手的出场顺序(即选手的序号)宣布最后得分和最后
名次,获得相同分数的选手具有相同的名次,名次连续编号,不用考虑同名次的选手人数。编程
帮助大赛组委会完成比赛的评分和排名工作。

190
第 章 8
位运算

本章目标
◇ 了解位运算的用途
◇ 熟悉位运算的运算规则
◇ 明确位段的定义方法
◇ 掌握位运算符和位段的使用方法
C 语言创建之初主要用来编写系统程序,如调制解调程序、磁盘文件管理程序和打印机驱动
程序等。在编写这些系统程序时,经常需要对数据按位进行处理,因此 C 语言提供了位运算的功
能。位运算是对字节或字中的位(bit)进行测试、置位或移位处理的运算,这里的字节或字是针
对 ANSI C 标准中的 char 和 int 数据类型而言的。

8.1 位 运 算 符
C 语言提供了 6 种专门用于位运算的运算符,如表 8-1 所示。

表 8-1 位运算符
运 算 符 含 义 运 算 符 含 义

& 按位与 ~ 取反

| 按位或 >> 按位右移

^ 按位异或 << 按位左移

位运算符中,除“~(取反)
”是单目运算符以外,其余的均为双目运算符。位运算只适用于
字符型数据和整型数据,而不适用于其他类型的数据。

8.1.1 按位与运算符
1.按位与的真值表
按位与运算符用“&”表示,其功能是将参与运算的两个数以二进制形式表示后,按二进制
位进行按位与运算。按位与的运算规则是,只有对应的两个二进制位均为 1 时,结果位才为 1,
否则为 0。表 8-2 所示为按位与的真值表。

191
C 语言程序设计(第 2 版)

表 8-2 按位与的真值表
p q p&q
0 0 0
0 1 0
1 0 0
1 1 1

2.按位与的运算步骤
(1)将参与运算的数据用二进制形式表示,如果参与运算的数据是负数则以补码方式表示。
(2)对二进制的每一位均按照表 8-2 所示进行“与”运算。
例如,9 & 5 的算式如下:
0 0 0 01 0 0 1 (十进制数 9)
& 00000101 (十进制数 5)
0 0 0 00 0 0 1 (十进制数 1)
可见 9 & 5 = 1。
3.按位与的作用
(1)清零
如果想将一个二进制数的某些位清零,只要找一个合适的二进制数与原来的数进行按位与运
算即可。具体步骤如下:
① 将准备清零的数表示成二进制的形式;
② 按条件寻找新数,即原来的数中为 1 的位,新数中相应位为 0,其他位任意;
③ 将这 2 个数进行按位与运算。
例如,将 35 清零,具体的处理过程如下。
35 用二进制形式表示为:00100011
为它寻找的新数为:00000100
两数进行按位与运算:
0 0 1 00 0 11 (十进制数 35)
& 00000100 (十进制数 4)
0 0 0 00 0 0 0 (十进制数 0)
显而易见,为清零而寻找的新数不是唯一的,只要满足步骤②的条件即可。
(2)获取二进制数中的某些指定位
如果想将某个二进制数的某些指定位保留下来,则可以使该二进制数与一个指定的二进制数
进行按位与运算,该指定的二进制数在预保留位上取 1 即可。
例如,对于 01010100,如果要将该数从右数的第 0、1、3、4、5 位保留下来,具体的处理过
程如下:
01010100 (十进制数 84)
& 0 0 111 0 11 (十进制数 59)
00010000 (十进制数 16)
本例中,十进制数 84 与十进制数 59 进行按位与运算之后得到的是十进制数 16,它的第 0、1、
3、4、5 位没有发生变化,被保留下来,同样为保留指定位而寻找的数也不是唯一的,除预保留
的指定位外,其他位可以取任意值,通常取 0 值。利用这种方法可以取得某个整数的低字节或者

192
第8章 位运算

高字节。
获取整数 a 的低字节,只需将 a 与十进制数 255 进行按位与运算即可。
例如,
00 1 0 11 0 0 1 01 0 11 0 0 (十进制数 11436)
& 0 0 0 0 0 0 0 0 111 111 11 (十进制数 255)
00 0 0 0 0 0 0 1 01 0 11 0 0 (十进制数 172)
如果要获取整数 a 的高字节,只需将 a 与十进制数 65280 进行按位与运算即可。
例如,
0 0 1 0 11 0 01 0 1 0 11 0 0 (十进制数 11436)
& 1 111 111 1 0 0 0 0 0 0 0 0 (十进制数 65280)
0 0 1 0 11 0 00 0 0 0 0 0 0 0 (十进制数 11264)

8.1.2 按位或运算符
1.按位或的真值表
按位或运算符用“|”表示,其功能是将参与运算的两个数以二进制形式表示后,按二进制位
进行按位或运算。按位或的运算规则是,只要对应的两个二进制位有一个为 1 时,结果位就为 1,
否则为 0。表 8-3 所示为按位或的真值表。

表 8-3 按位或的真值表
P q p|q
0 0 0
0 1 1
1 0 1
1 1 1

2.按位或的运算步骤
(1)将参与运算的数据表示成二进制形式,如果参与运算的数据是负数则以补码方式表示。
(2)对二进制数的每一位均按照表 8-3 所示进行“或”运算。
例如,9 | 5 的算式如下:
0 0 0 0 1 0 01 (十进制数 9)
| 00000101 (十进制数 5)
0 0 0 0 11 01 (十进制数 13)
可见 9 | 5 = 13。
3.按位或的作用
按位或可以完成置位。如果想将某个二进制数的某些指定位置 1,就将该二进制数与一个指
定的二进制数进行按位或运算,该指定的二进制数在预置位上取 1。
例如,对于 01010100,想将其从左数的第 0、1、3、4、5 位置 1,具体的处理过程如下:
01010100 (十进制数 84)
| 0 01 11 0 1 1 (十进制数 59)
0 1 1 11 1 1 1 (十进制 127)
本例中,十进制数 84 与十进制数 59 按位或运算之后得到的是十进制数 127,它的第 0、1、3、
4、5 位被置 1,将指定位置 1 而寻找的数也不是唯一的,除预置位的指定位外,其他位可以取任

193
C 语言程序设计(第 2 版)

意值,通常取 1 值。利用这种方法可以将某个整数的低字节或者高字节置为 1。
如果想让整数 a 的低字节全置为 1,高字节保留原样,只需将 a 与十进制数 255 进行按位或运算。
例如:
0 0 1 0 11 0 0 1 0 1 011 0 0 (十进制数 11436)
| 0 0 0 0 0 0 0 01 111 111 1 (十进制数 15)
0 0 1 0 11 0 01 11 11 11 1 (十进制数 11519)
如果想让整数 a 的高字节全置为 1,低字节保留原样,只需将 a 与十进制数 65280 进行按位
或运算。
例如:
0 0 1 0 11 0 01 0 1 0 11 0 0 (十进制数 11436)
| 1 1 1 1 1 1 1 1 0 0 0 0 0 0 00 (十进制数 65280)
1 1 1 1 1 1 1 1 1 0 1 0 11 0 0 (十进制数 65452)

8.1.3 按位异或运算符
1.按位异或的真值表
按位异或运算符用“^”表示,其功能是将参与运算的两个数以二进制形式表示后,按二进制
位进行按位异或运算。按位异或的运算规则是,若参与运算的两个数二进制位相同,则结果为 0,
否则为 1。表 8-4 所示为按位异或的真值表。

表 8-4 按位异或的真值表
p q p^q
0 0 0
0 1 1
1 0 1
1 1 0

2.按位异或的运算步骤
(1)将参与运算的数据表示成二进制形式,如果参与运算的数据是负数则以补码方式表示。
(2)对二进制数的每一位均按照表 8-4 所示进行“异或”运算。
例如,9 ^ 5 的算式如下:
00001001 (十进制数 9)
^ 00000101 (十进制数 5)
0 0 0 0 11 0 0 (十进制数 12)
可见 9 ^ 5 = 12。
3.按位异或的作用
(1)使指定位翻转
如果想使一个二进制数的某些指定位翻转,即这些指定位上的 1 变为 0,0 变为 1,则将该二
进制数与一个指定的二进制数进行按位异或运算,该指定的二进制数在预置位上取 1。
例如,将 01111010 的低 4 位翻转,具体的处理过程如下:
0 1111 0 1 0 (十进制数 122)
^ 0 0 0 01111 (十进制数 15)
0 111 0 1 0 1 (十进制数 117)

194
第8章 位运算

本例中,0111010 中的低 4 位由 1010 翻转成了 0101,这是因为原数的 0 ^ 1 = 1,原数的 1 ^ 1 =


0,由此实现数的翻转。显然,使特定位翻转的数也不是唯一的,只要令指定位为 1 即可。
(2)与 0 异或,保留原值
例如,将十进制数 10 和 0 进行按位异或运算,将会保留原值 10,其处理过程如下:

00001010 (十进制数 10)


^ 00000000 (十进制数 0)
00001010 (十进制数 10)

可见 10 ^ 0 = 10。原数的 0 ^ 0 = 0,原数的 1 ^ 0 = 1,由此实现数的保留。


(3)交换两个值,不用临时变量
通过按位异或运算,可以使 2 个数的值互换。具体步骤如下:
① a = a ^ b;
② b = b ^ a;
③ a = a ^ b。
例如,a = 3,b = 4,将 a 与 b 的值互换,具体的处理过程如下:
a=0011 (十进制数 3)
^ b=0100 (十进制数 4)
a=0111 (十进制数 7)
^ b=0100 (十进制数 4)
b=0011 (十进制数 3)
^ a=0111 (十进制数 7)
a=0100 (十进制数 4)
上式等效于以下两步:
① 执行前 2 个赋值语句:
“a = a ^ b;b = b ^ a;”相当于 b = b ^ (a ^ b) = b ^ a ^b = a ^ b ^ b,其
中 b ^ b = 0,因为一个数与本身相异或的结果必为 0,因此,b = a ^ 0 = a = 3;
② 再执行第 3 个赋值语句:
“a = a ^ b;”由于 b = b ^ a ^ b,a = a ^ b,因此,a = a ^ b ^ b ^ a ^
b = a ^ a ^ b ^ b ^ b = b = 4。

8.1.4 按位取反运算符
1.按位取反的真值表
按位取反运算符用“~”表示,其功能是将一个二进制数按位取反,即 0 变为 1,1 变为 0。
表 8-5 所示为按位取反的真值表。

表 8-5 按位取反的真值表
p ~p
0 1
1 0

2.按位取反的运算步骤
(1)将要取反的数据表示成二进制形式,如果参与运算的数据是负数则以补码方式表示。
(2)对二进制数的每一位均按照表 8-5 所示进行“取反”运算。

195
C 语言程序设计(第 2 版)

例如,~5 的算式如下:
~ 00000101 (十进制数 5)
11111 01 0 (十进制数 250)
可见~5 = 250。
取反运算符的优先级别比算术运算符、关系运算符、逻辑运算符以及其他位运算符都高,尤
其是在进行复合运算时一定要注意。例如,~a&b 相当于(~a) & b。
3.按位取反的作用
如果想使某数的最低位为 0,可以用 a = a & 65534(对应二进制数是 1111111111111110)得到。
但这样做的缺点是如果将该程序移植到以 32 位存放一个整数的计算机系统上,由于一个整数用 4
个字节来存储,使最低位变成 0 用上式就不行了,因此程序的可移植性较差。如果改用 a = a & (~
1),则无论是对以 16 位或以 32 位存放一个整数的情况都适用,而不必做修改。

8.1.5 按位左移运算符
按位左移运算符用“<<”表示,其功能是将一个二进制数的各位逐一向左移动。
按位左移的运算步骤如下:
(1)将要左移的数据表示成二进制形式,如果参与运算的数据是负数则以补码方式表示;
(2)左移时,高位的数据将会丢失,低位补 0。
例如,将十进制数 7 左移一位,移位后结果变为 14。移位过程如图 8-1 所示。

0 0 0 0 0 1 1 1

丢失
0 0 0 0 1 1 1 0

补0

图 8-1 按位左移过程示意图

移位运算的每个操作数都应为整数类型。左移一位相当于该数乘以 2,左移 3 位相当于该数乘以


3
2 = 8。由于左移运算比乘法运算快得多,有些 C 编译系统自动将乘 2 的运算用左移一位来实现,将
乘 2n 的运算用左移 n 位来实现。
注意,
该结论只适用于该数左移时被溢出的舍弃位中不包含 1 的情况。
例如,数值 14 左移 3 位后是 112,相当于 14 × 8 = 112。而 112 左移 2 位时,由于溢出位中
含有 1,所以其结果已经不是 448 了。

8.1.6 按位右移运算符
按位右移运算符用“>>”表示, 其功能是将一个二进制数各位逐一向右移动。
按位右移的运算步骤如下:
(1)将要右移的数据表示成二进制形式,如果参与运算的数据是负数则以补码方式表示;
(2)将二进制数各位向右移动相应的位数,移位时,低位右移后舍弃,对于无符号数高位补
,则高位移入 0;如果符号位原来为 1(该
0。对于有符号数,如果原来的符号位为 0(该数为正)
数为负)
,则高位移入 0 还是 1,取决于所用的计算机系统。移入 0 的称为逻辑右移,即简单右移,
196
第8章 位运算

移入 1 的称为算术右移。Turbo C 和其他一些 C 编译系统采用的是算术右移,即对有符号数右移


时,如果符号位原来为 1,则高位移入的是 1。
例如,把十进制数 7 右移一位,结果变为 3。移位过程如图 8-2 所示。

0 0 0 0 0 1 1 1 1

0 0 0 0 0 0 1 1 1 丢失

补0
错误!
图 8-2 按位右移过程示意图

M提醒
对于按位移动运算来说,在不产生溢出的情况下,每左移一位相当于原数乘以 2,每右移
一位相当于原数除以 2。

左移之后再右移并不一定能得到原来的数,因为左移后,其高位值是被舍弃的。

例如,前面对 7 进行右移一位后,由于移位后丢失了第一位 1,所以结果变为 3。


【例 8-1】 编写程序实现对负数右移的功能。
程序:
#include<stdio.h>
int main()
{
int a = -9, x = 0;
x = a>>1;
printf("%d >> : %d\n", a, x);
return 0;
}
运行结果如下:

当对负数进行右移操作时,左端补 1 不补 0。

−9 右移一位的过程如下:
1)−9 的原码形式: 1000000000001001 最高位的 1 表示该数为负数
2)−9 的反码形式: 1111111111110110 最高位不变,其余各位取反
3)−9 的补码形式: 1111111111110111 对反码加 1

197
C 语言程序设计(第 2 版)

4)右移 1 位−9>>1: 11111111111110111 挤掉右端 1 位,左端补 1


5)求−9>>1 的反码: 1111111111111010 对补码减 1 而得
6)求−9>>1 的原码: 1000000000000101 反码最高位不变,其余取反
因此,a>>1 的值由−9 变为−5。

M提醒
若对不同类型的数据进行按位与、按位或、按位异或运算,由于两个数据的二进制位数可
能不相等,这时系统将自动按下面的规则进行转换:
(1)将两个运算量按低位对齐。
(2)给位数少的运算量左端补 0 或 1,其中正数和无符号型数据补 0,负数补 1。

8.2 位 段
通常,有些信息并不需要占用一个完整的字节,只需占一个或几个二进制位。例如,存放一
个开关量时,只有 0 和 1 两种状态,用一个二进制位即可。
怎样向一个字节中的一个或几个二进制位赋值或改变它的值呢?根据前面介绍的知识,可以
采用按位赋值的方法。这种方法是先清零,接着进行移位运算,然后进行或运算,其过程烦琐复
杂。C 语言提供了一个更加方便的方法,在结构体中以位为单位来指定其成员所占内存长度,这
种以位为单位的成员称为“位段”或“位域”。

8.2.1 位段的定义
位段定义的一般形式如下:
struct 位段结构名
{
类型说明符 位段名: 位段长度;

类型说明符 位段名:位段长度;
} 位段变量名;
其中,类型说明符必须指定为 unsigned 型或 int 型,位段中指明的位段长度不能大于存储单
元的长度。
例如,
struct cbit
{
unsigned a:2;
unsigned b:6;
unsigned c:4;
unsigned d:4;
unsigned i:16;
}data;
定义了一个结构体 cbit,包含 4 个成员,第一个成员占用 2 位,第二个成员占用 6 位,第三个成
员占用 4 位,第四个成员占用 4 位,第五个成员占用 16 位。并且在定义 cbit 的同时定义了该类型
的变量 data,变量 data 所占内存空间情况如图 8-3 所示。

198
第8章 位运算

图 8-3 说明,位段的数据类型虽然是 unsigned 类型,但它不占满一个字节,只占用指定的位数。

199
C 语言程序设计(第 2 版)

data a b c d i

2位 6位 4位 4位
16 位

图 8-3 变量 data 所占内存空间情况示意图

在位段中可以定义无名位段,该无名位段定义为几位,就表明有几位空间不用,如
果该无名位段的长度是 0,则表示让下一个位段从另一个字节开始存放。

例如:
struct cbit
{
unsigned a:2;
unsigned b:4;
unsigned :0; /*无名位段*/
unsigned c:4;
}data;

M提醒
只能定义位段变量,不能定义位段数组。

位段是定义在结构体中的成员,因此,位段的定义与结构体的定义类似,也有三种形式,这
里不再赘述。

8.2.2 位段的使用
位段引用的一般形式如下:
位段变量名.位段名
例如:
data.a = 0;
表示引用位段变量 data 的位段成员 a,并赋值 0。

对位段赋值时应注意位段允许的最大值范围。

例如,定义位段如下:
struct cbit
{
unsigned a:2;
unsigned b:6;
unsigned c:4;
unsigned d:4;
}data;
对位段 a 赋值为 8,写成语句“data.a = 8;”是错误的。因为位段 a 只占 2 位,最大值为(11)2,
即十进制数 3。如果强行赋值,则系统自动取该数最后 2 位的值。例如,执行语句“data.a = 8;”

系统只取十进制数 8 即(1000)2 的最后 2 位“00”为 data.a 赋值,结果为 0。

200
第8章 位运算

【例 8-2】 输出各个位段的值。
程序:
#include<stdio.h>
int main()
{
struct cbit
{
unsigned a:1;
unsigned b:3;
unsigned c:4;
} bit, *pbit;
bit.a = 1;
bit.b = 7;
bit.c = 15;
printf("%d,%d,%d\n", bit.a, bit.b, bit.c);
pbit = &bit;
pbit -> a = 0;
pbit -> b &= 3;
pbit -> c |= 1;
printf("%d,%d,%d\n", pbit -> a, pbit -> b, pbit -> c);
return 0;
}
运行结果如下:

程序定义了一个结构体 cbit,三个位段分别为 a、b 和 c,同时定义了 Struct cbit 类型的位


段变量 bit 和指向 Struct cbit 类型的指针 pbit。程序的第 10、11、12 三行分别给三个位段赋值。
程序第 13 行以整型量格式输出三个位段的内容。第 14 行把位段变量 bit 的地址送给指针变量
pbit。第 15 行用指针方式给位段 a 重新赋值,值为 0。第 16 行使用了复合的位运算符“&=”

该行相当于“pbit -> b = pbit -> b & 3;”
,位段 b 中原有值为 7,与 3 进行按位与运算的结果
为 3(111 & 011 = 011,十进制值为 3)
。同样,程序第 17 行中使用了复合位运算符“|=”
,相
当于“pbit -> c = pbit -> c | 1;”
,其结果为 15。程序第 18 行用指针输出了这三个位段的值。
需要强调的是,一个位段必须存放在同一个存储单元中,不能跨存储单元存放。也就是说,
如果前一个存储单元的剩余空间容纳不下一个位段,则该空间不用,从下一个存储单元起存放该
位段。这样,在上一个存储单元中留下的未用的空位,称为空穴。

M提醒
(1)位段只能作为结构体的成员,不用作为共用体的成员。
(2)位段没有地址,对位段不能进行取地址运算。
(3)位段可以在数值表达式中引用,它会被系统自动转换成 int 型。

特别提示:
在存储单元中,位段的空间分配方向因系统而异。在计算机使用的 C 编译系统中,一般是由
右到左分配的。这里谈到的“存储单元”可能是 1 个字节,也可能是 2 个字节,因不同的编译系
统而异。

201
C 语言程序设计(第 2 版)

8.3 综 合 实 例
【例 8-3】 字符串的加密和解密。
分析:
采用按位异或运算对字符串进行加密是一种比较简单的加密方法。该方法的基本思想是,先
确定一个用于加密的密码,然后将需要加密的字符串与该密码字符串进行按位异或操作,得到一
个新的字符串,从而实现对原字符串加密的目的,通常把这个字符串称为密文。对方接收到密文
之后,只需用该密文字符串与密码字符串再次进行按位异或操作,便可得到原字符串,从而实现
解密功能,通常把这个解密后得到的与原字符串相同的字符串称为明文。
通常,密码字符串的长度小于待加密字符串的长度。因此,加密时,待加密字符串与密码字
符串左端对齐后,进行按位异或运算。具体过程示意图如图 8-4 所示。

H e l l o . w o r l d !

c c u t c c u t

1次 2次

图 8-4 字符串加密过程示意图

解密的过程与加密相同,只是参与按位异或操作的字符串是密文。这里不再赘述。
算法描述:
(1)输入待加密字符串 s 和密码 m;
(2)计算 m 的长度 l_m 以及 s 的长度 l_s;
(3)循环变量 i ← 0,循环条件 i < l_s,i 的增量为 l_m,循环执行以下语句
循环变量 j ← 0,循环条件 j < l_m,j 的增量为 1,循环执行以下语句
如果 未到字符串 s 的末位,则
对 s[i+j]与 m[j]进行按位异或运算;
(4)输出加密后的字符串 s;
(5)再次执行上述循环过程,实现解密;
(6)输出解密后的字符串 s。
程序:
/* 字符串加密 */
#include<stdio.h>
#include<string.h>
int main()
{
char s[40], m[8];
int i, j, l_m,l_s;
printf("Please enter the string:");
gets(s);
printf("Now do the encryption,please enter the password:");
gets(m);

202
第8章 位运算

l_m = strlen(m); /* 计算密码长度 */


l_s=strlen(s);
for(i = 0; i < l_s; i += l_m) /* 按密码长度循环 */
for(j = 0; j < l_m; j++)
{
if(s[i + j] != '\0')
s[i + j] = s[i + j] ^ m[j]; /* 按位异或实现加密 */
}
printf("The string after encryption is:");
printf("%s\n", s);

for(i = 0; i < l_s; i += l_m) /* 与加密同样的运算完成解密 */


for(j = 0; j < l_m; j++)
{
if(s[i + j] != '\0')
s[i + j] = s[i + j] ^ m[j]; /* 对数组的每个字符进行加密 */
}
printf("The string after the decryption is:");
printf("%s\n", s);
return 0;
}

运行结果如下:

【例 8-4】 编写程序实现课程选修情况的存储与显示。具体要求:程序提示输入学生的学号,
然后给出具体的选修课程名称,由学生决定是否选修。最后打印各学生的学号和选修的结果。
分析:
为了节省空间,采用位段来分别存储每门课程是否选修,因此需要定义一个结构体,在该结
构体中定义位段,用来指明各门课程是否选修(1 为选修,0 为不选修)
。这样,实现课程选修功
能主要包括以下两个步骤:
第一步:定义结构体类型,用于描述学生的学号和课程选修情况。
第二步:编写主函数存储课程选修情况。分别将学生对各门课程的选修结果(1 或 0)存入各
位段中,然后输出学生的选课情况。
算法描述:
(1)定义结构体类型 choice,成员为学号和位段结构体;
(2)编写主函数。
1)定义结构体类型数组 stu,用于存储 N 个学生的选课情况;
2)循环变量 i ← 0,循环条件 i<N,i 的增量为 1,循环执行以下语句
ⅰ 输入学生学号;
ⅱ 显示各门课程;
ⅲ 输入学生的选课结果(1/0)
,存储到相应的位段中;
3)输出学生的选课情况。

203
C 语言程序设计(第 2 版)

程序:
#include<stdio.h>
#define N 2
struct choice
{
unsigned num;
struct
{
unsigned pro:1;
unsigned vc:1;
unsigned db:1;
unsigned ph:1;
} kc;
};
int main()
{
struct choice stu[N];
int i, s_no, sel;
for(i = 0; i < N; i++)
{
printf("Enter the no.:");
scanf("%d", &s_no);
stu[i].num = s_no;
printf("C programming(0/1):");
scanf("%d", &sel);
stu[i].kc.pro = sel;
printf("Visual C++(0/1):");
scanf("%d", &sel);
stu[i].kc.vc = sel;
printf("Net wrok(0/1):");
scanf("%d", &sel);
stu[i].kc.db = sel;
printf("Java programming(0/1):");
scanf("%d", &sel);
stu[i].kc.ph = sel;
}
printf("\n No C programming Visual C++ Net work Java Programming\n");
for(i = 0; i < N; i++)
printf("\n%d%10d%12d%12d%12d\n", stu[i].num, stu[i].kc.pro, stu [i]. kc.vc,
stu[i].kc.db, stu[i].kc.ph);
return 0;
}
运行结果如下:

204
第8章 位运算

如果在结构体 choice 中使用 unsigned 类型来存储课程选修标识,则每个结构体变量


需要 10 个字节的存储空间,但使用位段后,仅需 2 个字节。

8.4 深入研究:字段拼装问题
按位操作是计算机硬件所支持的基本操作,它提供了对数据内部细节的访问控制能力,是低
级编程工具(如机器指令、汇编语言等)所必须提供的,普通的应用程序一般不需要这种对数据
内部细节的访问控制能力,因此,有些高级语言,特别是面向应用的高级语言,不提供这种能力。
但是在系统软件的设计和实现过程中,这种对数据内部细节的访问控制能力是必不可少的。因此
了解和掌握一些按位操作的使用方法是必要的。
下面以字段拼装问题为例说明位运算的处理能力。
【例 8-5】 设计一个函数 setbits(x,p,n,y),对 x 从右数第 p 位开始,向左连续 n 位(含第 p 位)
置为 y 的最右边 n 位的值,其余各位保持不变。
分析:
要解决这个问题,可以分为以下三步:
(1)取出 y 中最右边的 n 位的值;
(2)将其左移 p−1
位;
(3)用左移过的内容取代 x 从右数第 p 位开始向左连续 n 位。
取出一个数据中指定位的值的方法是,建立一个掩码(各指定位对应的位为 1、其余位为 0)

使用该掩码和已知数据进行“按位与”运算。为取出 y 最右边 n 位的值,首先需要生成最右 n 位
全为−1 其余全为 0 的掩码:
mask = (1 << n) – 1;
使用这个掩码和 y 进行“按位与”运算,就取出了 y 中最右边 n 位的值,再使用按位左移操
作就完成了第一步和第二步的任务。具体语句如下:
t = (y & mask) << (p - 1);
“取代 x 从右数第 p 位开始向左连续 n 位”这项任务可以进一步分解为两步:
(1)将 x 从右数
第 p 位开始的向左连续 n 位清 0;
(2)将上述结果和被取出并移位后的 y 的最右边 n 位通过“按
位或”运算拼装在一起。将一个数据中的某些位清为 0,需要使用在这些位为 0 其余位为 1 的掩
码和这个数据“按位与”运算。这个掩码可以通过对前面生成并保存在变量 mask 中的掩码左移
并取反得到。因此将 x 从右数第 p 位开始的向左连续 n 位清 0 的操作如下:
tmp = x & ~(mask << (p - 1));
在完成上述分析之后,字段的拼装就很简单了。
程序:
#include<stdio.h>
int setbits(int x, int p, int n, int y);
int main()
{
int x, y, z;
printf("please input x and y:\n");
scanf("%d,%d", &x, &y);
z = setbits(x, 2, 4, y);
printf("z=%d\n", z);
return 0;

205
C 语言程序设计(第 2 版)

}
int setbits(int x, int p, int n, int y)
{
unsigned int tmp, mask;
mask = (1 << n)-1;
tmp = x & ~(mask << (p - 1));
return (tmp | ((y & mask) << (p - 1)));
}
运行结果如下:

整数 9 的二进制表示方式为 0000000000001001(假设用 2 个字节来存储整数)


,整
数 5 的二进制表示方式为 0000000000000101,对整数 9 从右数第 2 位开始,向左连续 4
位的值为 0010,将其置换为整数 5 的最右边 4 位的值 0101,结果为 0000000000001011,
即十进制数 11。

本章小结
位运算是以二进制位为单位进行的一种特殊运算,完成位运算的运算符分为逻辑运算符和移
位运算符两类,其中逻辑运算符包括按位与、按位或、按位异或和取反四种运算符,移位运算符
包括按位左移和按位右移两种,主要是对字节或字中的位(bit)进行测试、置位或移位等处理,
为实现调制解调程序、磁盘文件管理程序和打印机驱动程序等系统程序功能提供方便。
双目位运算符可以与赋值运算符一起组成复合赋值运算符,如&=、|=、^=、>>=和<<=等。
C 语言还提供了位段,也就是在结构体中以位为单位来指定其成员所占内存长度,这种以位
为单位的成员称为“位段”
,定义位段时要指明每个段的位数。利用位段存储某些数据可以节省存
储空间。要注意,位段中指明的位段长度不能大于存储单元的长度,另外,C 语言不允许定义位
段数组。
位运算是C语言与其他高级语言的重要区别之一,是 C 语言具有底层处理能力的体现。

习 题
【复习】
1.判断下列说法是否正确。
(1)位运算的操作数只能是整型或字符型的数据。
(2)位运算的操作数只能是实型数据。
(3)位运算中,除取反运算外,其余 5 个位运算符均可与赋值运算符一起构成复合赋值运算符。
(4)位运算符不能与赋值运算符组成复合赋值运算符。
(5)位段成员的类型必须指定为 unsigned 类型或 int 类型。

206
第8章 位运算

(6)位段成员的类型必须指定为 float 类型。


(7)一个位段必须存储在同一存储单元中,不能跨越两个存储单元。
(8)位段的长度不能大于存储单元的长度。
(9)在数值表达式中引用位段时,系统自动将位段转换为整型数。
(10)在程序中,只能用%u 格式字符,以整数形式输出位段成员的数值。
2.假设有以下语句:
char x = 3, y = 6, z;
z = x ^ y << 2;
则 z 的二进制值是( )
A.00010100 B.00011011 C.00011100 D.00011000
3.在 16 位机上,以下程序的运行结果是( )
#include<stdio.h>
int main()
{
struct st
{
unsigned a:10;
unsigned b:12;
unsigned c:2;
}x;
printf("%d\n", sizeof(x));
return 0;
}
A.2 B.3 C.4 D.不能通过编译
4.写出实现下列功能的表达式(操作数 x 为 int 型数据)

(1)取 x 的低字节,高字节置 0。
(2)保留 x 的第 3 位,其余各位置 0。
(3)将 x 的高 8 位均置为 1,其余各位不变。
(4)将 x 的最低位翻转,其余各位不变。
5.按位与的作用是什么?
6.C 语言提供位运算的作用是什么?其优势体现在哪里?
7.计算 8<<1、8<<4、21>>1、21>>4 的值,并说明在不溢出的情况下,x<<n、x>>n 分别是多少。
【应用】
1.编写函数用来实现左右循环移位。函数名为 move,调用方法为
move(value,n)
其中,value 为要循环移位的数,n 为移位的位数。如 n<0,表示为左移;n>0 为右移。如 n =
4,表示要右移 4 位;n =−3,表示要左移 3 位。
2.输入无符号短整数 k 和 p,将 k 的高 8 位作为结果的低 8 位,p 的高 8 位作为结果的高 8
位组成一个新的无符号短整数。
3.编写一个程序,输入一个短整数,以字符形式输出该短整数的高字节和低字节。
【探索】
编程实现某客户购买的商品(如电视、数码相机、沙发、吊灯)的存储,其中输入客户 ID
和购买商品(1:购买该商品,0:未购买该商品)
,存储客户购买的商品使用位段类型。

207
第 章 9
文件

本章目标
◇ 了解文件的概念和用途
◇ 熟悉文件操作的相关函数
◇ 掌握打开、关闭、读写文件和定位函数的使用方法
◇ 会用 C 语言的打开、关闭、读写文件和定位函数编写程序
前面各章的程序中所涉及的数据输入和输出操作都是以计算机终端(键盘、鼠标和显示器)
为对象,因此,随着程序运行的结束,用户从键盘输入的数据以及程序运行产生的结果都会随之
消失,无法得到长期保存,而且每次运行程序都要重新输入数据,显然这种数据输入输出的处理
方式无法适应批量数据处理的实际需要。为了提高数据输入输出的处理效率,可以将程序运行时
所需的原始数据事先存储到磁盘文件中,在需要的时候调入内存,并将程序运行的结果写入磁盘
文件中,以备查用。
C 语言提出了文件的概念以及相关机制解决数据的输入输出问题,在此基础上,ANSI C
标准定义了一系列文件操作函数方便用户的使用,这些函数存放在系统的标准函数库中,使用时
要包含头文件“stdio.h”。

9.1 文 件 概 述
在 C 语言中,无论输入输出的数据来源和去向是终端设备,还是存储在外存上的磁盘文件,
都被抽象成一个统一的概念,即文件,程序中的数据输入输出就被转化成对文件的读写操作。因
此,C 语言中的文件是一个逻辑概念,它涉及的对象很广,除了前面使用过的源程序文件、头文
件等磁盘文件外,凡是能进行输入输出的终端设备都被称为文件。
与用户程序密切相关的文件主要是终端文件和磁盘文件。终端文件中有 3 个特殊文件,分别
是标准输入文件(stdin)、标准输出文件(stdout)和标准出错信息文件(stderr)。每一个用户程
序运行时,计算平台的运行系统会自动地为其维护这 3 个文件,它们都是自动设置并打开的。默
认情况下,这 3 个文件的初始设置分别对应着计算机的终端键盘和显示器屏幕。因此,当程序调
用 scanf( )或 getchar( )时,就从标准输入文件(对应于键盘)读取信息;利用 printf( )或 putchar( )
打印输出结果时,就向标准输出文件(对应于显示器屏幕)输出信息。
C 语言提供了一种机制,在程序中的输入输出操作与文件之间建立一种关联,这种关联被映
射成逻辑数据流(stream)。C 语言支持两种形式的数据流:文本数据流和二进制数据流。文本数

207
C 语言程序设计(第 2 版)

据流是字符的有序序列,由字符组成行,每一行由 0 个或多个字符组成,最后一个字符为换行符。
二进制数据流被抽象成一个线性字节序列。因此,普通的磁盘文件分为文本文件和二进制文件。
文本文件又称为 ASCII 文件,与文本数据流相对应,这种文件在磁盘中存放时每个字符对应一个
字节,用于存放对应的 ASCII 码。例如,十进制数 5678,如果存放到文本文件中,文件内容将包
含 4 个字节,值分别为:53、54、55、56,它们分别是“5”
、“6”、
“7”
、“8”的 ASCII 码值。二
进制文件与二进制数据流相对应,是按照二进制编码方式来存放文件的。例如,十进制数 5678,
如果存放到二进制文件中,文件内容将包含 2 个字节,其二进制形式为 0001011000101110。当打
开一个文件时,就把文件与相应的数据流联系起来,内部通过对数据流的操作来达到处理其对应
文件的目的。实际使用时,经常用“文件”这个词代替“数据流”

9.2 文 件 指 针
根据问题的需要建立与文件相关联的某种数据流(文本数据流或二进制数据流)是对文件进
行操作的首要任务,该数据流应记录与文件操作相关的信息,如文件名、文件位置等。这些信息
保存在一个结构体类型的变量中,该文件结构体类型由系统定义,取名为 FILE,FILE 中记录了
控制一个数据流所需的各种信息。
FILE 结构体的定义如下:
typedef struct
{
short level; /* 缓冲区使用量 */
unsigned flags; /* 文件状态标识 */
char fd; /* 文件号 */
short bsize; /* 缓冲区大小缺省值 512 */
unsigned char *buffer; /* 缓冲区指针 */
unsigned char *curp; /* 当前活动指针 */
unsigned char hold; /* 无缓冲区取消字符输入 */
unsigned istemp; /* 草稿文件标识 */
short token; /* 做正确性检验 */
}FILE;
有了 FILE 类型之后,通过定义指向 FILE 类型的文件指针实施对文件的操作。
定义文件指针的一般形式如下:
FILE *文件指针名;
其中,文件指针名是用户定义的符合命名规则的标识符。例如:
FILE *fp;
表示 fp 是一个指向 FILE 结构体的指针。

9.3 文件的打开和关闭
文件的打开与关闭就是创建和销毁数据流的过程。打开文件是指创建一个数据流连接到一个

208
第9章 文件

磁盘文件的过程,在这个过程中,系统创建一个指向该文件的文件指针,以便进行其他操作。关
闭文件是指销毁一个已连接到磁盘文件的数据流的过程,也就是断开文件指针与文件之间的联系,
从而禁止对该文件进行读写。

9.3.1 文件打开函数 fopen( )


C 语言用函数 fopen( )实现文件的打开。
函数 fopen( )的一般形式如下:
FILE *fopen("文件名","使用文件方式")
其中,文件名是要打开的磁盘文件的名称,使用文件方式是指打开该文件后要对文件进行的
操作,如表 9-1 所示。该函数的返回值为指向 FILE 结构体的文件指针。
例如:
FILE *fp;
fp = fopen("d:\\vc\\test.txt", "r");
的功能是以“只读”方式打开 D 盘的 vc 目录下的文件“test.txt”,并返回一个指向文件 test.txt 的
文件指针 fp。

表 9-1 文件使用方式
文件使用方式 含 义
rt 以只读的方式打开一个文本文件
wt 以只写的方式打开或建立一个新的文本文件
at 以追加的方式打开一个文本文件,并在文件末尾写数据
rb 以只读的方式打开一个二进制文件
wb 以只写的方式打开或建立一个新的二进制文件
ab 以追加的方式打开一个二进制文件
rt+ 以读写的方式打开一个文本文件
wt+ 以读写的方式打开或创建一个新的文本文件
at+ 以读写的方式打开一个文本文件
rb+ 以读写的方式打开一个二进制文件
wb+ 以读写的方式打开或创建一个新的二进制文件
ab+ 以读写的方式打开一个二进制文件

表 9-1 中文件使用方式的详细含义与使用时应注意的问题有以下几个方面。
(1)文件使用方式由 r、w、a、t、b 和+这 6 个字符组成,各个字符的含义如下。
:读。
r(read)
w(write):写。
a(append):追加。
:文本文件,可省略不写。
t(text)
b(binary):二进制文件。
+:读和写。
(2)用“r”打开一个文件时,该文件必须已经存在,才能从该文件读取。
(3)用“w”打开的文件只能向该文件写入。若打开的文件不存在,则以指定的文件名建立

209
C 语言程序设计(第 2 版)

该文件;若打开的文件已经存在,则先将该文件删除,再重新创建。
(4)若要向一个已存在的文件追加新的信息,只能用“a”方式打开文件。但此时该文件必须
是存在的,否则将出错。打开时,位置指针移到文件末尾。
(5)用“r+”
、“w+”、
“a+”方式打开的文件可以进行读写操作。用“r+”方式打开时该文件
应该已经存在。用“w+”方式打开则新建立一个文件,先向此文件写数据,然后可以读此文件中
的数据。用“a+”方式打开的文件,原来的文件不被删除,位置指针移到文件末尾,既可以写,
也可以读。以“rt+”方式打开一个文件,作用是以读写的方式打开已存在的文本文件。以“at+”
方式打开一个文件,作用是以读写的方式打开一个文本文件,若该文件已经存在,则读写位置指
针在文件的末尾。以“wt+”方式打开一个文件,作用是以读写的方式建立一个新文本文件,如
果该文件已经存在,则先将原来的文件删除,再重新创建。

M技巧
(1)打开一个文件时,如果出错,函数 fopen()将返回一个空指针 NULL。在程序中可以利
用该信息判断是否成功打开文件,并做相应的处理。
(2)二进制文件读写操作速度比文本文件读写操作速度要快。因为把一个文本文件读入内
存时,要将 ASCII 码转换成二进制码,而把文件以文本方式写入磁盘时,也要把二进制码转换
成 ASCII 码。因此,如果可能的话,应尽量采用二进制文件进行读写操作。

9.3.2 文件关闭函数 fclose( )


文件一旦使用完毕,要用文件关闭函数 fclose( )关闭文件。
函数 fclose( )的一般形式如下:
int fclose(文件指针名)
fclose( )函数的返回值为 0 时表示关闭文件执行成功,否则返回 EOF。EOF 是头文件“stdio.h”
中定义的符号常量,值为−1。关闭文件的作用是让文件指针与被关闭的文件脱离,同时将未满的
输出缓冲区数据写入文件,将未满的输入缓冲区数据取出,以避免数据丢失。
例如:
fclose(fp);
的功能是关闭文件指针 fp 所指向的文件。

9.4 文件的读写
文件被打开后,可以对文件进行的最基本读写操作有两个:从文件中读取信息(读操作)和
将信息存放到文件中(写操作)

9.4.1 字符读写函数 fgetc( )和 fputc( )


1.读字符函数
函数 fgetc( )的一般形式如下:
int fgetc(文件指针名)
功能:从文件指针指向的磁盘文件中读取一个字符,将该字符的 ASCII 码值存放到 int 型变

210
第9章 文件

量中。其中,文件指针指向的文件应以只读或读写方式打开。
例如:
c = fgetc(fp);
的功能是从 fp 所指向的文件中读取一个字符并存入变量 c 中。
需要强调的是,读取的字符是通过该文件内部的位置指针来指定的,该指针用来指向文件当
前的读写字节。在文件打开时,该指针总是指向文件的第一个字节。使用 fgetc( )函数后,该位置
指针将向后移动一个字节。可连续多次使用 fgetc( )函数,读取多个字符。

文件指针和文件内部的位置指针是不同的。文件指针是指向整个文件的,必须在程
序中定义说明,只要不重新赋值,文件指针的值是不变的。文件内部的位置指针用以指
示文件内部的当前读写位置,每读写一次,该指针向后移动一个位置,它不需要在程序
中定义,而是由系统自动设置的。

M技巧
对文件进行读操作时,为了避免文件已结束还在进行读操作而导致错误,可以用系统函数
feof( )(详见 9.6 节)来判断文件是否结束,当该函数的返回值为 1(真)时表示文件结束,返
回值为 0(假)时表示文件还没有结束。

2.写字符函数
函数 fputc( )的一般形式如下:
int fputc(字符,文件指针名)
功能:把一个字符写入文件指针所指向的文件中。其中,字符可以是字符常量,也可以是字
符变量,文件指针所指向的文件可以用写、读写和追加的方式打开。字符写入的位置由文件位置
指针决定,文件位置指针的位置因打开方式不同而不同。函数的返回值为 int 型,如果写操作成
功,则返回写入字符的 ASCII 码值,否则返回 EOF。
例如:
fputc('a', fp);
的功能是把字符“a”写入 fp 所指向的文件中。
3.函数 fgetc( )和 fputc( )的使用
【例 9-1】 从键盘输入字符,存入文件 test.txt 中。
程序:
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp; /* 定义文件指针 */
char ch;
if((fp = fopen("test.txt","w")) == NULL) /* 以只写方式打开文件 */
{
printf("Cannot open file!\n");
exit(0);
}
printf("Please input a string: ");
while ((ch = getchar( )) != '\n') /* 只能输入字符,不能输入回车符 */
fputc(ch, fp);

211
C 语言程序设计(第 2 版)

fclose(fp);
printf("The string have been written into test.txt.\n");
return 0;
}
运行结果如下:

首先以只写方式打开磁盘文件 test.txt,并根据函数 fopen( )的返回值判断打开文件是


否成功。如果打开成功,则从键盘依次读入字符“I love China!”,每读入一个字符,就
利用函数 fputc( )将其写入 fp 所指向的磁盘文件中,以回车键作为结束字符,完成写入
操作,最后关闭文件。

M提醒
在 VC 6.0 环境下,当函数 fopen( )中的文件名以相对路径给出时,该文件存储在当前工作
区所在文件夹中。

9.4.2 字符串读写函数 fgets( )和 fputs( )


1.读字符串函数
函数 fgets( )的一般形式如下:
char *fgets(字符数组名,n,文件指针名)
功能:从文件指针所指向的文件中读入一个长度不超过 n−1 个字符的字符串,并将其存储到
字符数组中。其中,n 是一个正整数,用以指明字符串的长度。该函数的返回值是字符数组的首
地址。
例如:
fgets(str, 8, fp);
的功能是从 fp 所指向的文件中读入 7 个字符存储到字符数组 str 中,并且在该字符串后自动加
上“\0”。

M提醒
在使用函数 fgets( )读取字符串时,若在读入的过程中遇到了换行符或者文件结束标志
EOF,则读操作结束,无论该字符串是否已全部读入完毕。

2.写字符串函数
函数 fputs( )的一般形式如下:
int fputs(字符串,文件指针名)
功能:将一个字符串写入到指定的文件中。其中,字符串可以是字符串常量,也可以是字符
数组或字符指针。该函数的返回值为 int 型,如果成功写操作,则返回一个非 0 值,否则返回 EOF。
例如:
fputs("abcd", fp);
的功能是把字符串“abcd”写入 fp 所指向的文件中。
212
第9章 文件

3.函数 fgets( )和 fputs( )的使用


【例 9-2】 从文本文件 test1.txt 中读取字符串,再写入文本文件 test2.txt 中(假定 test1.txt 中
只有一行字符串“I love China!”)。
程序:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main()
{
FILE *fp1, *fp2;
char str[128];
if ((fp1 = fopen("test1.txt", "r")) == NULL) /* 以只读方式打开文件 1 */
{
printf("Cannot open file\n");
exit(0);
}
if((fp2 = fopen("test2.txt", "w")) == NULL) /* 以只写方式打开文件 2 */
{
printf("Cannot open file\n");
exit(0);
}
if((strlen(fgets(str,128,fp1))) > 0) /* 从文件中读取的字符串长度大于 0*/
{
fputs(str, fp2);
printf("%s", str);
}
fclose(fp1);
fclose(fp2);
return 0;
}
运行结果如下:

由于要操作两个文本文件 test1.txt 和 test2.txt,因此需要定义两个文件的指针 fp1 和



fp2。在读写这两个文件之前,可以把这两个文件以需要的方式同时打开(不分先后)
读写完成后,应关闭文件。

9.4.3 数据块读写函数 fread( )和 fwrite( )


1.数据块读函数
函数 fread( )的一般形式如下:
int fread(buffer,size,count,fp)
功能:从 fp 所指向的文件中每次读取大小为 size 个字节的数据,读取 count 次,数据保存在
buffer 中。其中,fp 是文件指针;buffer 是一个指针,用来存放输入数据块的首地址;size 表示一
个数据块的字节数;count 表示要读的数据块次数。函数 fread( )的返回值是 int 型,如果读取成功,
则返回 count 的值;如果遇到文件尾或遇到错误,则返回 0。

213
C 语言程序设计(第 2 版)

例如:
fread(fa, 4, 5, fp);
的功能是从 fp 所指向的文件中连续读 5 次,每次读 4 个字节的数据存储在指针 fa 所指向的缓冲
区中。

M技巧
当函数 fread( )的返回值为 0 时,可以用函数 feof( )或 ferror( )来判断是读取时遇到文件尾
还是遇到错误。

2.数据块写函数
函数 fwrite( )的一般形式如下:
int fwrite(buffer,size,count,fp)
功能:从 fp 所指向的文件中写入大小为 size 个字节的数据,写入 count 次,数据保存在 buffer
中。其中,fp 表示文件指针;buffer 是一个指针,用于存放写入数据块的首地址;size 表示一个
数据块的字节数;count 表示要写入的数据块次数。如果写入成功,则函数返回值等于 count,否
则返回 0。
需要指出的是,C 语言提供的数据块读写函数 fread( )和 fwrite( ),通常用来读写一个数组中
所有数组元素的值或一个结构体变量的值。

M提醒
使用函数 fread( )和 fwrite( )进行读写操作时,通常以二进制方式打开文件。

3.函数 fread( )和 fwrite( )的使用


【例 9-3】 应用 fwrite( )函数向文件写入数据,再用 fread( )函数从该文件读取数据,并显示在
屏幕上。
程序:
#include<stdio.h>
#include<stdlib.h>
int main()
{
FILE *fp1;
int i;
struct stu
{
char name[15];
char num[6];
float score[2];
} student;
if((fp1 = fopen("test.txt", "wb")) == NULL) /* 以二进制只写方式打开文件 */
{
printf("Cannot open file");
exit(0);
}
printf("Input data:\n");
for( i = 0; i < 2; i++)
{
scanf("%s%s%f%f", student.name, student.num, &student.score[0], &stud

214
第9章 文件

ent.score[1]);
fwrite(&student, sizeof(student), 1, fp1); /*成块写入文件 */
}
fclose(fp1);
if((fp1 = fopen("test.txt","rb"))==NULL) /*重新以二进制只写方式打开文件 */
{
printf("Cannot open file");
exit(0);
}
printf("Output from file:\n");
for(i = 0; i < 2; i++)
{
fread(&student, sizeof(student), 1, fp1); /*从文件成块中读取 */
printf("%s %s%7.2f%7.2f\n",student.name,student.num,student. score[0], student.
score[1]);
}
fclose(fp1);
return 0;
}
运行结果如下:

通常,当输入数据的格式较为复杂时,可将它们作为字符串处理,输出时将字符串转换为
所需的格式。ANSI C 提供 atoi、atof 和 atol 函数来实现字符串格式的转换,其一般形式如下:
int atoi(char *ptr)
float atof(char *ptr)
long int atol(char *ptr)
它 们 分 别 将 字 符 串 转 换 为 整 型 、 实 型 和 长 整 型 。 使 用 时 应 包 含 头 文 件 “ math.h” 或
“stdlib.h”。

9.4.4 格式化读写函数 fscanf( )和 fpintf( )


1.格式化读函数
函数 fscanf( )的一般形式如下:
int fscanf(文件指针名,格式字符串,输入列表)
功能:按格式字符串中指定的格式从文件指针指向的文件中读取内容,存储到输入列表所指
定的变量中。
例如:
fscanf(fp, "%d%c", &i, &c);
的功能是读取文件指针 fp 指向的文件中的内容,分别赋值给整型变量 i 和字符型变量 c。
2.格式化写函数
函数 fprintf( )的一般形式如下:
int fprintf(文件指针名,格式字符串,输出列表)

215
C 语言程序设计(第 2 版)

功能:将输出列表中的数据按指定的格式写入文件指针指向的磁盘文件中。
例如:
int a = 3;
float b = 9.80;
fprintf(fp, "%2d,%6.2f", a, b);
的功能是将变量 a 按“%2d”的格式、变量 b 按“%6.2f ”的格式写入文件指针 fp 所指向的文件
中,以逗号为分隔符。
需要指出的是,使用函数 fscanf( )和 fprintf( )对磁盘文件读写很方便,但由于输入时要将文本
文件转换为二进制文件,在输出时又要将二进制文件转换为字符,花费时间较多。因此在内存与
磁盘交换数据的情况下,最好使用函数 fread( )和 fwrite( )。
3.函数 fscanf( )和 fprintf( )的使用
【例 9-4】 将格式化数据写入文本文件,再从该文件中以格式化方法读取,并显示在屏幕上。
格式化数据是两个学生的相关信息,包括姓名、学号和两门课程的成绩。
程序:
#include<stdio.h>
#include<stdlib.h>
int main()
{
FILE *fp;
int i;
struct stu
{
char name[15];
char num[6];
float score[2];
} student;
if((fp = fopen("test1.txt", "w")) == NULL) /*以文本只写方式打开文件*/
{
printf("Cannot open file");
exit(0);
}
printf("Input data:\n");
for( i = 0;i<2;i++)
{
scanf("%s%s %f %f", student.name, student.num, &student.score[0],
&student.score[1]);
fprintf(fp, "%s %s %7.2f %7.2f\n", student.name, student.num,
student.score[0], student.score[1]); /* 写入文件 */
}
fclose(fp); /* 关闭文件 */
if((fp = fopen("test1.txt", "r")) == NULL) /* 以文本只读方式重新打开文件 */
{
printf("Cannot open file");
exit(0);
}
printf("Output from file:\n");
while(fscanf(fp,"%s%s%f%f\n", student.name, student.num,
&student.score[0], &student.score[1]) != EOF)
printf("%s %s%7.2f%7.2f\n", student.name, student.num,
student.score[0], student.score[1]);

216
第9章 文件

fclose(fp);
return 0;
}
运行结果如下:

程序中定义了一个文件指针 fp,两次以不同方式打开同一文件,写入和读取格式化
数据。

M提醒
对文件进行读写时,用什么格式写入文件,就一定要用什么格式从文件读取,否则,读取
的数据与格式控制符不一致,会造成数据出错。

在例 9-4 中,以二进制方式打开文件也能实现同样的功能。
if ((fp = fopen("test1.txt", "wb")) == NULL)
{ /* 以二进制只写方式打开文件*/
printf("Cannot open file");
exit(0);
}

9.4.5 自定义其他类型数据的读写函数
利用 ANSI C 提供的函数 fread( )和 fwrite( ),读写任何类型的数据都是十分方便的。但是,
如果所用的系统不提供这两个函数,那么用户可以自己定义所需要的函数。
例如,可以定义一个向磁盘文件写入一个实数(用二进制方式表示)的函数 putfloat( )。
void putfloat(float num,FILE *fp)
{
char *s;
int count;
s = (char *)&num;
for(count = 0; count < 4; count++)
putc(s[count], fp);
}
同样,用户可以编写出读写任何类型数据的函数。

9.5 文件的定位
读写文件中的数据时,要根据位置指针来确定读写的数据在文件中的位置,如果要随机
读写文件中的某些数据,则需要根据读写要求移动文件内部的读写位置指针,这称为文件的
定位。

217
C 语言程序设计(第 2 版)

1.函数 rewind( )
函数 rewind( )的一般形式如下:
int rewind(文件指针名)
功能:使文件的位置指针返回到文件头。其中,文件指针必须是已指向某一磁盘文件的有效
指针。
2.函数 fseek( )
函数 fseek( )的一般形式如下:
int fseek(文件指针名,位移量,起始点)
功能:将指定文件的位置指针,从起始点开始,移动指定的字节数。其中,位移量表示相对
于起始点位置指针移动的字节数。ANSI C 规定,位移量是 long 型数据。当用常量表示位移量时,
要求加后缀“L”,以便在读写大于 64KB 的文件时不致出错。起始点表示从何处开始计算位移
量,起始点通常是指文件首、当前位置或文件尾。起始点的表示方式如表 9-2 所示。

表 9-2 起始点的表示方式
起 始 点 表 示 符 号 数 字 表 示
文件首 SEEK_SET 0
当前位置 SEEK_CUR 1
文件尾 SEEK_END 2

例如:
fseek(fp, 100L, 0);
的功能是把位置指针移到离文件首 100 个字节的地方。
3.函数 ftell( )
函数 ftell( )的一般形式如下:
long ftell(文件指针名)
功能:得到位置指针在文件指针所指向的文件中的当前位置,用相对于文件首的位移量来表
示。如果函数 ftell( )返回值为−1L,表示出错。
例如:
i = ftell(fp);
if(i == −1L)
printf("errer\n");
的功能是用变量 i 存放位置指针的当前位置,如果函数 ftell( )出错(如不存在 fp 文件指针所指向
的文件),则输出“error”

9.6 文件的出错检测
对文件操作时,可能由于某些原因(如文件不存在)
,致使文件操作不能正常进行。因此,需
要对文件操作成功与否进行检测,根据检测的结果进行某种处理,可以避免程序因为错误的出现
而异常终止。
1.文件结束检测函数
对文件进行读操作时,可以通过函数 feof( )检测是否到达文件尾。

218
第9章 文件

函数 feof( )的一般形式如下:
int feof(文件指针名)
功能:判断文件是否处于文件尾,若是,则返回值为 1,否则返回值为 0。一般当该函数返回
值为 1 时,结束对文件的读取操作。
2.读写文件出错检测函数
函数 ferror( )的一般形式如下:
int ferror(文件指针名)
功能:检查文件读写函数对文件进行读写时是否出错。如果函数 ferror( )的返回值为 0,表示
未出错,否则表示出错。

9.7 综 合 实 例
【例 9-5】 文件合并。文件 addr.txt 中存储了某些人的姓名和地址,文件 tel.txt 中存储了这些
人的姓名与电话号码,但存储顺序与 addr.txt 中的不一致。要求:通过对比这两个文件,将同一
个人的姓名、地址和电话号码合并成一条记录存储到第三个文件 addrtel.txt 中。
文件 addr.txt 的内容如下:
hejie tianjing
liying shanghai
liming chengdu
wangpin chongqing

文件 tel.txt 的内容如下:
liying 12345
hejie 8764
wangpin 87643
liming 7654322

分析:
通过对上述两个文件的存储内容进行观察可以发现,这两个文件的存储格式基本一致,姓名字
段都占 14 个字节,地址或电话号码长度不超过 14 个字节,并以回车符结束。文件的最后一行只有
回车符,也可以说是长度为 0 的串。在两个文件中,由于存储的是同一批人的资料,则文件的记
录数是相等的,但存储顺序不同。可以以任何一个文件为基准,在另一个文件中顺序查找相同姓
名的记录,若找到,则合并记录存入第三个文件,然后将查找文件的指针移到文件头,以备下一
次顺序查找。
程序:
#include<stdio.h>
#include<stdlib.h>
#include<conio.h>
#include<string.h>
int main()
{
FILE *fptr1, *fptr2, *fptr3;
char temp[15], temp1[15], temp2[15];
if((fptr1 = fopen("addr.txt", "r")) == NULL)

219
C 语言程序设计(第 2 版)

{
printf("Cannot open file");
exit(0);
}
if((fptr2 = fopen("tel.txt", "r")) == NULL)
{
printf("Cannot open file");
exit(0);
}
if((fptr3 = fopen("addrtel.txt", "w")) == NULL)
{
printf("Cannot open file");
exit(0);
}
system("cls"); /*清屏幕*/

fgets(temp1, 15, fptr1);

while(strlen(temp1) > 1)
{
fgets(temp2, 15, fptr1);
fputs(temp1, fptr3);
fputs(temp2, fptr3);
strcpy(temp, temp1); /* 暂存姓名字段 */
do /* 查找姓名相同的记录 */
{
fgets(temp1, 15, fptr2);
fgets(temp2, 15, fptr2);
} while(strcmp(temp, temp1) != 0);
rewind(fptr2); /* 将文件指针移到文件头,以备下次查找 */
fputs(temp2, fptr3);
strcpy(temp1,"\0");
fgets(temp1, 15, fptr1);
}
fclose(fptr1);
fclose(fptr2);
fclose(fptr3);
return 0;
}
本例中,合并后的文件 addrtel.txt 的内容如下:

【例 9-6】 从键盘输入一个字符串(长度小于 80)


,写入文本文件 a.txt 中保存,再对该文件
进行操作。要求:把文件中的所有数字改成大写字母 A。

220
第9章 文件

分析:
由于要对文本文件 a.txt 中的内容进行处理,而文件内容是无法直接修改的,因此需要建立一
个临时文件 a.tmp,把对文件 a.txt 按要求处理后的结果暂时存入这个临时文件中,然后再利用文
件读写函数 fgetc( )和 fputc( )从 a.tmp 中读取数据再写入 a.txt 中。对文件 a.txt 和 a.tmp 都需要读
和写两种操作,打开方式应该用“wt+”。
判断一个文件的内容是否已全部读完,本题采用的方法是,首先计算该文件中存储的字符串
的长度,然后每读取一个字符,使该长度值减 1,若为 0 表示已读完,结束读取过程。因此,本
题定义了一个 int 型变量 counter 作计数器。
程序:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
FILE *fp, *fpt;
char c, str[80];
int counter;
if((fp = fopen("a.txt", "wt+")) == NULL)
{
printf("Can not open the file!\n");
exit(0);
}
if((fpt = fopen("a.tmp", "wt+")) == NULL)
{
printf("Can not open the file!\n");
exit(0);
}
printf(“Please input a string(<80):\n");
fscanf(stdin, "%s", str);
fputs(str, fp);
/* 读取 a.txt 内容,按要求处理后暂时存入 a.tmp 中 */
rewind(fp);
counter=strlen(str);
while(counter>0)
{
c = fgetc(fp);
if(c >= '0' && c <= '9') c ='A';
fputc(c, fpt);
counter--;
}
fclose(fp);
/* 重新以只写方式打开文件 a.txt */
if((fp = fopen("a.txt", "wt")) == NULL)
{
printf("Can not open the file!\n");
exit(0);
}
/* 将 a.tmp 中的内容写回到 a.txt 中 */
rewind(fpt);
counter=strlen(str);
while(counter>0)

221
C 语言程序设计(第 2 版)

{
fputc(fgetc(fpt), fp);
counter--;
}
fclose(fp);
fclose(fpt);
return 0;
}
运行结果如下:

程序运行结束后,文本文件 a.txt 的内容如下:

把程序第 31 行后的程序段替换成下列语句也能完成程序功能。

fclose(fpt);
remove("a.txt"); /* 删除文件 a.txt */
rename("a.tmp", "a.txt"); /* 将文件 a.tmp 改名为 a.txt */
return 0;
这里,函数 remove( )和 rename( )是两个标准库函数,可直接调用。

9.8 深入研究:读写效率问题
实际上,C 语言的标准函数库提供了在两个不同层面上对文件进行操作的机制和函数,一个
是基于系统调用的基础层面,另一个是字符流层面,因此,对同一个文件也有两种不同的描述方
法和操作机制。对文件操作的基础层面直接建立在操作系统所提供的基本功能之上,因此这一层
面上的操作也被称为系统调用。在这个层面上,对文件的描述所采用的机制是文件描述字。一个
文件描述字是一个整数,指明所对应文件的属性信息在用户程序打开文件表中的表项位置。在程
序开始运行时,系统的打开文件表中有 3 个表项。其中 0 号文件描述字对应着标准输入文件,1
号文件描述字对应着标准输出文件,2 号文件描述字对应着错误信息输出文件。这一层面上常用
的文件操作函数有:open( )、close( )、read( )、write( )、lseek( )等。
为了提高输入输出操作的效率和便于编程人员的使用,在 C 语言的标准函数库中提供了对基
本输入输出操作的进一步抽象和封装。在这一层面上,输入文件被抽象为一个可以顺序读入的
字符流,输出文件被抽象为一个可以接受字符流的容器,因此这一层面的输入输出文件也被称
为字符流。字符流是一个 FILE 类型的数据结构,与字符流相关的文件描述符是一个指向这一字
符流的具有 FILE *类型的指针。各种读写操作都是通过相应的字符流指针来指定输入输出所对
应的文件的。在 C 程序开始运行时,也有 3 个已经自动打开的字符流,其中 stdin 是标准输入,
222
第9章 文件

对应于基础层面中的 0 号文件描述字;stdout 是标准输出,对应于基础层面中的 1 号文件描述字;


stderr 是错误信息输出,对应于基础层面中的 2 号文件描述字。这一层面中常用的输入输出函数有:
fopen( )、fclose( )、fread( )、fwrite( )、ftell( )、feof( )、fgetc( )等。这些函数就是本章的主要内容。
基础输入输出与面向字符流的输入输出的主要区别有两点。第一,两者的实现机制不同。严
格地说,基础输入输出不属于库函数而属于系统调用,其功能由操作系统直接提供,而不是由函
数库提供,用户程序中使用基础输入输出功能就是直接调用操作系统的功能。面向字符流的输入
输出函数是在操作系统的基础输入输出功能之上进行了必要的抽象和封装以及功能扩展的函数,
用户程序在使用这些函数时不再直接与操作系统打交道。第二,面向字符流的输入输出是带缓冲
的,而基础输入输出操作自身是不带缓冲的。也就是说,在基础输入输出操作的层面上,任何一
次读写操作都会产生对操作系统数据读写功能的调用;而在面向字符流的输入输出层面上,读写
操作是通过缓冲区进行,并且这一缓冲过程是自动进行,并且对函数的使用者是透明的,只有在
缓冲区中的内容被读空或者被写满之后,对文件的读写操作才真正发生。这样可以减少对文件实
际读写的操作系统功能调用的次数,提高数据读写的效率。
【例 9-7】 采用两种方式进行数据的读写。
以基础读写方式将标准输入上的内容以指定的大小为单位读入到缓冲区,并以相同的大小为
单位写入标准输出。
char s[MAX_SIZE];

while (n = read (0, s, size) > 0)
write (1, s, n);
以面向字符流操作方式完成相同的功能:
char s[MAX_SIZE];

while (n = fread (s, size, 1, stdin) > 0)
fwrite (s, size, n, stdout);
在上面的代码中,变量 size 是每次读写操作单位的大小。

本章小结
C 语言中的文件是一个逻辑概念,它涉及的对象很广,凡是能进行输入输出的设备都称为文
件。与用户程序密切相关的文件主要是终端文件和磁盘文件。根据数据的组织形式不同,通常把
文件分为两类:文本文件和二进制文件。
C 语言中,对文件的使用遵循“先打开,再读写,后关闭”的原则。打开一个文件,就是
在程序与文件之间建立一个数据流,实际上就是在内存开辟一个缓冲区,用来存放文件的信息
(如文件的名称、状态和当前位置),这些文件信息保存在 FILE 类型的结构体变量中,FILE 结
构体类型由系统定义。通过定义 FILE 类型的指针来指向要操作的文件。当一个文件被打开后,
可获得该文件指针。关闭一个文件,就是断开程序与文件之间的这种关联,让文件指针与被关
闭的文件脱离,同时将未满的输出缓冲区数据写入文件,将未满的输入缓冲区数据取出,以避
免数据丢失。
C 语言提供了一系列标准函数来完成文件打开、文件读写和文件关闭操作。文件可按只读、
只写、读写、追加 4 种操作方式打开,同时还必须指定文件的类型是二进制文件还是文本文件。
223
C 语言程序设计(第 2 版)

文件读写方式有两种:顺序读写和随机读写。采用顺序读写文件方式,文件以字节、字符串和
数据块为单位,从当前文件内部的位置指针所指向的位置开始顺序进行;采用随机读写文件方
式,在读写文件之前,要通过移动文件内部的位置指针找到要读写的位置,然后进行读写操作。
对文件进行操作时,可以通过被调用函数的返回值来获取操作是否成功的信息,从而进行相应
的处理,也可以利用 C 语言提供的出错检测函数进行检测。
利用 C 语言中的文件功能,可以方便地对用户的批量数据进行存储管理,提高数据输入输出
的处理效率。

习 题
【复习】
1.什么是文件?C 语言的文件有什么特点?
2.什么是文件指针?通过文件指针访问文件有什么好处?
3.文件的打开和关闭的含义是什么?为什么要打开和关闭文件?
4.以下不能将文件位置指针重新移到文件开头位置的函数是( )

A.rewind(fp); B.fseek(fp,0,SEEK_SET);
C.fseek(fp,0,SEEK_END); D.fseek(fp,-(long)ftell(fp),SEEK_CUR);
5.下列程序运行后,输出结果是 。
#include<stdio.h>
int main( )
{
FILE *fp;
int i,a[4] = {2,4,7,9},t;
fp=fopen("data.dat","wb");
for(i = 0;i < 4;i++)
fwrite(&a[i],sizeof(int),1,fp);
fclose(fp);
fp = fopen("data.dat", "rb");
fseek(fp,−2L*sizeof(int),SEEK_END);
fread(&t,sizeof(int),1,fp);
fclose(fp);
printf("%d\n",t);
return 0;
}
【应用】
1.用反转文件的方法对文件进行加密处理(一个字节值,如把它的每个二进制位的 0 变 1,
1 变 0,称为反转)

2.创建一个包含 5 个学生成绩的文件“studbk.dat”
,然后查询成绩,在屏幕上显示出不及格
学生的成绩和优秀学生的成绩。
3.有 5 个学生,每个学生有 3 门课的成绩,从键盘输入以上数据(包括学号、姓名以及 3
门课的成绩)
,计算出平均分数,将原有的数据和计算出的平均分数存放在磁盘文件“stud”中。
【探索】
利用命令行参数,查找指定文件中某个单词出现的所有行的行号及其内容。

224
第9章 文件

提示:
查找指定文件中的某个单词出现的行号及该行的内容,可使用如下命令行:
example_9<查找文件名><查找单词>
查找子串 substr 在母串 string 中出现的索引由下面的函数确定:
int str_index(char substr [],char string[])
{
int i,j,k;
for(i = 0;string[i];i++)
for(j = i,k = 0;string[j] == substr[k];j++,k++)
if(!substr[k+1])
return i;
return −1;
}

225
第 章 10
编译预处理

本章目标
◇ 了解编译预处理的作用
◇ 熟悉编译预处理的三种类型
◇ 掌握宏定义、文件包含和条件编译命令的使用方法
◇ 会用宏定义、文件包含和条件编译命令编写程序
前面各章已多次使用过以符号“#”开头的编译预处理命令,如文件包含命令“#include”和
宏定义命令“#define”等。这些命令放在函数之外,并且一般放在源文件的前面,称为预处理命
令。这些预处理命令由编译预处理程序来处理,在编译的第一遍扫描(词法扫描和语法分析)之
前进行。当对一个源文件进行编译时,系统自动引用预处理程序对源文件中的预处理命令进行处
理,处理完成后自动进入编译过程。
C 语言提供了多种预处理命令,如宏定义、文件包含和条件编译等。恰当使用预处理命令编
写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。

10.1 宏 定 义
将一个标识符定义成一串符号的预处理命令称为“宏定义”
,这个标识符称为“宏名”
,这一
串符号称为“宏体”。完成了宏定义后,在源程序中就可以引用宏。对于带有宏的程序,源程序开
始编译前,预处理程序会把源程序中所引用的宏名替换成对应的一串字符,然后再开始编译过程。
替换的过程称为“宏替换”
,也称为“宏展开”
。宏定义分为两种:不带参数的宏定义和带参数的
宏定义。

10.1.1 不带参数的宏定义
不带参数的宏定义的一般形式如下:
#define 宏名 宏体
其中,
“#”表示是一条预处理命令,凡是以“#”开头的都是预处理命令。
“define”为宏定义
命令的关键字。宏名为合法的 C 标识符。宏体可以是任意合法的常量和表达式,也可以为空。
例如:
#define PI 3.14
定义了一个宏,宏名是 PI,宏体为一个常量 3.14,在预处理时,系统会自动地将程序中的 PI

226
第 10 章 编译预处理

替换成 3.14。

宏定义不是 C 语句,末尾不加分号,如果加了分号,系统会自动将分号作为表达式
的一部分,连同分号一起进行替换。

例如,如果宏定义为
#define PI 3.14;
则对语句
L = 2 * PI * r;
进行宏替换后,该语句变为
L = 2 * 3.14; * r;
显然出现了语法错误。

M技巧
编译预处理时只是对宏名进行简单的替换,因此,最好把宏体加上括号,以免在某些情况
下产生错误。

例如,下面的程序段:
#define X 5
#define Y (X + 2)
int main()
{
int s = 3 * Y;
return 0;
}
是正确的,在编译预处理时会将语句“s = 3 * Y;”替换为“s = 3 * (X + 2);”。如果去掉宏定义“#define
Y (X + 2)”中的括号,即
#define X 5
#define Y X + 2
int main()
{
int s = 3 * Y;
return 0;
}
则是错误的。编译预处理时会将语句“s = 3 * Y;”替换为“s = 3 * X + 2;”,由于运算符的优先级
不同,因此导致非预想的结果。
#define 命令通常出现在程序中函数的外面,宏名的有效范围为从定义开始到源文件结束或用
#undef 命令终止该宏定义为止。

M提醒
(1)宏名一般用大写字母表示,以便与变量相区别。但这不是规定,小写字母也是允许的。
(2)在预处理时宏体代替宏名,但它不对宏体进行解析,不做正确性检查。
(3)C 语言中的关键字不能作为用户标识符,但可以作为宏名,因为所有预处理发生在识
别这些关键字之前。

227
C 语言程序设计(第 2 版)

10.1.2 带参数的宏定义
#define 命令不仅可以进行字符串的替换,还可以进行参数的替换。
带参数的宏定义的一般形式如下:
#define 宏名(参数名) 宏体
其中,宏体中包含所定义的参数。
例如,
#define PI 3.14
#define S(r) (PI * (r) * (r))
area = S(3);
在第二条宏定义命令中,使用了带参数的宏定义,定义圆的面积为 S,r 为半径。在第三行的
赋值语句中使用了 S(3),用 3 代替宏定义中的参数 r,即用“3.14 * (3) * (3)”代替 S(3),因此上
述赋值语句经宏展开后变为“area = (3.14 * (3) * (3));”

【例 10-1】求两个数的最小值。
程序:
#include <stdio.h>
#define MIN(a, b) ((a) < (b)) ? (a) : (b)
int main()
{
int x, y;
x = 10;
y = 20;
printf("The minnumber is:%d\n", MIN(x, y));
return 0;
}
运行结果如下:

编译该程序时,MIN(a,b)定义的表达式用宏定义中的宏体替换,用 x 和 y 替换宏定
义中的参数 a 和 b。经宏替换后 printf 语句变成如下形式:
printf("The minnumber is:%d", ((x) < (y)) ? (x) : (y));

【例 10-2】计算并输出给定半径的圆的面积。
程序:
#include <stdio.h>
#define PI 3.14
#define area(r) (PI * (r) * (r))
int main()
{
printf("S(3) = %f\n", area(3));
return 0;
}
运行结果如下:

228
第 10 章 编译预处理

可以看出,带参数的宏在使用时,形式上与函数调用没有什么区别,特别是在宏名也用小写
字母表示的时候,并且在多数情况下,二者产生的结果也相同。但必须记住,带参数的宏定义与
函数的本质是不同的,不要把两者混淆起来。
(1)使用带参数的宏时,仅对宏体中的参数进行简单的替换。而函数调用是先求出表达式的
值,然后再代入形参。
(2)函数调用是在程序运行时处理的,为形参分配临时的内存单元。而宏替换是在编译前进
行的,在替换时并不分配内存单元,不进行值的传递处理,也没有“返回值”的概念。
(3)对函数中的实参和形参都要定义类型,而且二者的类型要求一致,如果不一致,应进行
类型转换。而宏定义不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号,展开时
代入指定的宏体即可。宏定义时,宏体可以是任何类型的数据。
(4)宏替换不占运行时间,只占编译时间,而函数调用则占运行时间。

10.1.3 宏定义的嵌套
宏定义的嵌套是指在宏体中使用已定义的宏名。在宏展开时由预处理程序逐层替换。
假设有如下程序:
#include <stdio.h>
#define R 3
#define PI 3.14
#define L (2 * PI * R)
#define S (PI * R * R)
int main()
{
printf("L = %.2f,S = %.2f\n", L, S);
return 0;
}
则程序的运行结果为:

本例中,经过宏展开后,函数 printf( )中的输出项 L 被替换为 2 * 3.14 * 3,S 被替换为 3.14 *


3 * 3。

不仅不带参数的宏定义可以嵌套,带参数的宏定义也可以嵌套。

10.1.4 取消宏定义
取消宏定义的一般形式如下:
#undef 宏名
#undef 的主要目的是将宏名局限在仅需要它们的代码段中。

229
C 语言程序设计(第 2 版)

例如:
#define LEN 100
#define WIDTH 100
...
char array[LEN][WIDTH];
...
#undef LEN
#undef WIDTH
在上面的程序段中,执行“#undef LEN”后,宏定义 LEN 将会被取消,如果以后再出现
LEN,系统将其视为一个未定义的变量名。

10.2 文 件 包 含
通常,一个大的程序可分为多个模块,由多个程序员分别编写,在这些模块中,经常会
出现一些公用的宏定义,对于这些公用的宏定义,应单独组成一个文件,在需要用到这些宏
定义的文件开头使用文件包含命令包含该文件。这样可避免在每个文件开头都书写那些公用
量,从而节省时间,减少差错,提高程序的可维护性。文件包含命令的功能是将指定的头文
件插入到此命令行位置取代该命令行,从而将指定的头文件和当前的源程序文件连成一个源
文件。
文件包含命令的一般形式如下:
#include "文件名"

#include <文件名>
其中,
“#”表示是一条预处理命令,
“include”为文件包含命令的关键字,文件名为合法的 C
标识符。
例如:
#include "file2.h"
表示在进行编译预处理时,将 file2.h 文件的内容与当前文件连在一起,具体地说,就是以
file2.h 文件的整个内容代替“#include "file2.h"”命令。
文件包含命令 include 的预处理过程如图 10-1 所示。

#include "file2.h" /*file2.h 的内容*/


/*file2.h 的内容*/
...
int main( )
...
{
... int main( )
...
{ ...
return 0;
return 0;
}
}

(a)源文件 file1.c (b)文件 file2.h (c)预处理后的 file1.c

图 10-1 文件包含命令 include 的预处理过程示意图

230
第 10 章 编译预处理

图 10-1(a)所示为源文件 file1.c,包含一条文件包含命令“#include "file2.h"”,图 10-1(b)


所示为文件 file2.h 的内容,在编译预处理时,对#include 命令进行“文件包含”处理,将 file2.h
的全部内容复制到“#include "file2.h"”命令处,得到“包含”后的源文件 file1.c,如图 10-1(c)
所示。编译时,将“包含”后的 file1.c 文件作为源文件进行编译。

在#include 命令中,使用尖括号和双引号都是合法的,区别在于使用尖括号时,系
统在存放 C 库函数头文件所在的目录中寻找被包含的文件;使用双引号时,系统先在用
户当前目录中寻找被包含的文件,如果找不到,再按照使用尖括号的方式寻找被包含的
文件。这样,如果被包含的文件是 C 库函数文件,则使用尖括号节省时间;如果被包含
的文件是自己编写的文件,则使用双引号节省时间。若被包含的文件不在当前目录中,
可在双引号中给出文件的路径。

【例 10-3】输出半径为 3 的圆的周长和面积。
程序:
/* EG10-3.h */
#define PI 3.14
#define S(r) (r * r * PI)
#define L(r) (2 * PI * r)

/* EG10-3.c */
#include <stdio.h>
#include ”EG10-3.h”
int main()
{
printf("S(3) = %f\n", S(3));
printf("L(3) = %f\n", L(3));
return 0;
}

运行结果如下:

包含的文件和被包含的文件在编译预处理之后成为一个文件。因此,两个文件中不
可以有重复的宏定义,被包含文件中的全局静态变量在包含的文件中也有效。

M提醒
(1)文件包含命令可以出现在文件的任何地方。为了醒目,多数放在靠近文件开头的地方。
(2)文件的包含可以嵌套。
(3)经常用在文件开头的被包含文件叫做“头文件”,常以“.h”为后缀,这只是为了说明
此文件的性质,用“.c”作为后缀或者无后缀也是合法的。
(4)一个#include 命令只能包含一个文件,包含多个文件,要使用多个#include 命令。

231
C 语言程序设计(第 2 版)

10.3 条 件 编 译
在某些情况下,希望源程序中的代码只在满足某种条件时才进行编译,这时就需要用到条件
编译。条件编译是指对程序源代码的各部分有选择地进行编译的过程。C 语言提供了丰富的条件
编译命令。
(1)#ifdef 标识符
程序段 1
#else
程序段 2
#endif
功能:如果标识符已被#define 命令定义过,则对程序段 1 进行编译;否则对程序段 2 进行编
译。如果没有程序段 2,上述格式中的#else 可以省略,写为
#ifdef 标识符
程序段
#endif
(2)#if 常量表达式
程序段 1
#else
程序段 2
#endif
功能:如果常量表达式的值为真,则对程序段 1 进行编译,否则对程序段 2 进行编译。
(3)#ifndef 标识符
程序段 1
#else
程序段 2
#endif
功能:如果标识符未被#define 命令定义过,则对程序段 1 进行编译,否则对程序段 2 进行编
译。这与第一种形式的功能正好相反。
【例 10-4】分析以下程序的运行结果。
程序:
#include <stdio.h>
#define RUBY 10
int main()
{
#ifdef RUBY
printf("Hello RUBY !\n");
#else
printf("Hello anyone!\n");
#endif
#ifndef BABY
printf("BABY is not defined!\n");
#endif
return 0;
}

232
第 10 章 编译预处理

运行结果如下:

【例 10-5】输入一行字母,根据需要设置条件编译,将字母全部改为大写输出。
程序:
#include<stdio.h>
#define LETTER 1
int main()
{
char str[20] = "CLanguage",c;
int i = 0;
while((c = str[i])!='\0')
{
i++;
#if(LETTER)
if(c >= 'a' && c <= 'z')
c = c - 32;
#else
if(c >= 'A' && c <= 'Z')
c = c + 32;
#endif;
printf("%c", c);
}
printf("\n");
return 0;
}
运行结果如下:

条件编译命令中的表达式为 LETTER,本例中它是一个宏,定义为常量 1,因此,


该条件为真,对程序段 1 中的语句(即第 11~12 行)进行编译,实现大写转换功能。

思考一下,如果将宏定义改为“#define LETTER 0”,会有怎样的运行结果?

10.4 其他预处理功能
C 语言中,除了上面介绍过的常用的预处理命令外,还有一些预处理命令,如#line 行控制命
令、#error 诊断控制命令和#pragma 字符序列等。
1.#line 行控制
#line 命令改变_LINE_与_FILE_的内容,它们是编译系统中预先定义的标识符。
#line 命令的一般形式如下:
#line 常量 "文件名"

233
C 语言程序设计(第 2 版)

功能:告诉编译系统,源程序中下一行的行号由该常量指定,当前正在处理的文件的名称
改为指定的文件名。其中,常量为任何正整数,表示源程序中当前行号,文件名为任意有效的
文件名。
#line 命令主要用于调试及其他特殊应用场合。
例如,
#line 20 "ABC"
则下一行的行号指定为 20,文件名改为“ABC”。
2.#error 诊断控制
#error 命令的一般形式如下:
#error 字符序列
功能:强迫编译程序停止编译,主要用于程序调试。
3.#pragma 字符序列
在标准 C 语言中对其含义未统一规定,由具体的编译系统去解释,如果对 pragma 不识别,
就忽略它。

10.5 预定义的宏名
标准 C 语言提供了 5 个预定义的宏名:
(1)_LINE_:当前源文件的行号。
(2)_FILE_:设定的源文件名。
(3)_DATE_:含有形式为“月/日/年”的串,表示源文件被翻译到代码的日期。
(4)_TIME_:源代码翻译到目标代码的时间作为串包含在_TIME_中,串形式为“时:分:秒”

(5)_STDC_:如果__STDC__已经定义,编译器将仅接受不包含任何非标准扩展的标准 C/C++
代码。如果实现是标准的,则宏_STDC_含有十进制常量 1;如果它含有任何其他数,则实现是非
标准的。
标准 C 语言中预定义的宏名由标识符与两边各一条下划线“_”构成,为了加以区别,用户
程序中的宏名最好不要采用这种形式。有的编译系统可能仅支持以上宏名中的几个,或根本不支
持,有的编译系统还可能提供其他预定义的宏名。

10.6 深入研究:头文件的重复引用问题
复杂的大型程序或者函数库有可能涉及复杂的数据结构,导致.h 文件在.c 文件中引用的方式
也可能是复杂的。有些.h 文件有可能通过间接的方式被多次引用。例如,假设在文件 a.h 中定义
了数据类型 x 和 y,文件 b.h 中定义了数据类型 u 和 v。如果数据类型 x 中的一个成员的类型是 u,
那么文件 a.h 就需要引用文件 b.h。如果在文件 s.c 中使用了文件 a.h 和文件 b.h 中说明的函数,那
么文件 s.c 就必须同时引用文件 a.h 和文件 b.h。这样,在文件 s.c 中文件 b.h 就会被以直接的方式
和间接的方式各引用一次,编译系统就会在编译文件 s.c 时两次遇到对数据类型 u 和 v 的定义,
并且报告数据类型的重复定义错误。即使在.h 文件中没有对数据类型的定义,大量重复引用.h 文

234
第 10 章 编译预处理

件也会使得编译的速度降低。为了避免.h 文件在编译一个.c 文件时被多次重复引用,可以在.h 文


件中配合使用条件编译命令和宏,根据特定的宏是否存在来判断.h 文件在当前的.c 文件中是否已
经被引用过。例如,<stdlib>文件中包含如下的命令行:
#if !defined(_STDLIB)
#define _STDLIB

#endif
上述命令的前两行出现在文件的开头,最后一行出现在文件的末尾,中间用省略号略去的是
<stdlib.h>中的正文内容。这样,只要<stdlib.h>在当前的.c 文件中被引用过一次,就定义了宏
_STDLIB_H。此后无论是再次直接引用<stdlib.h>,还是通过其他的.h 文件间接引用<stdlib.h>,
<stdlib.h>中第一行的#ifndef 就不再成立,<stdlib.h>中其余的内容也就不会再被处理了。

本章小结
编译预处理是在对源程序正式编译前由预处理程序执行的处理命令,这些命令称为预处理命
令,一般包括宏定义、文件包含和条件编译等。
宏定义是将一个标识符定义成一串符号,在宏替换时用该串符号去替换宏名。宏定义分为不
带参数的宏定义和带参数的宏定义两种。不带参数的宏常用来定义符号常量和配合条件编译,带
参数的宏常用于代码短小、使用频率高的程序段,以替代函数的作用提高运行效率。为了避免宏
替换时发生错误,一般应将宏定义中的宏体加括号,宏体中出现的参数也应加括号。
文件包含是预处理的一个重要功能,通过在一个文件中包含其他文件,可以把多个源文件连
接成一个源文件进行编译,最终生成一个目标文件。在程序设计时,利用这一功能在设计中共享
公共代码,避免重复劳动。
条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而降低内存的
消耗,提高程序的效率。
编译预处理是 C 语言的一个重要功能,使用预处理功能便于程序的修改、阅读、移植和调试,
也利于模块化程序设计。

习 题
【复习】
1.什么是编译预处理命令?C 语言提供编译预处理命令的作用是什么?
2.编译预处理命令主要分为哪几类?
3.请指出下列描述中哪些是正确的。
(1)C 源程序中的预处理命令不是 C 语言本身的组成部分。
( )
(2)C 语言预处理功能是指完成宏替换和包含文件的调用。
( )
(3)C 语言的编译预处理就是对源程序进行初步的语法检查。
( )
(4)凡是 C 源程序中行首以“#”标识的控制行都是预处理命令。
( )
(5)C 语言的编译预处理命令只能位于 C 源程序文件的首部。
( )
235
C 语言程序设计(第 2 版)

4.C 语言条件编译命令的基本形式是:
#××× 标识符
程序段 1
#else
程序段 2
#endif
这里×××可以是( )

A.define 或 include B.ifdef 或 include
C.ifdef 或 ifndef 或 define D.ifdef 或 ifndef 或 if
5.阅读下面的程序:
#define A 4
#define B(x) A * x / 2
int main( )
{
float c, a = 4.5;
c = B(a);
printf("%5.1f\n",c);
return 0;
}
该程序的运行结果是: 。
【应用】
1.定义一个带参数的宏 MYABS(x, y),求 x+y 的绝对值。
(1)一行输出 1 个实数;
2.设计输出实数的格式,包括: (2)一行内输出 2 个实数;
(3)一
行内输出 3 个实数。实数用%6.2f 格式输出。
3.编写程序,输入 8~10 位二进制数码,将其作为小数数码,并转换为十进制小数。
4.定义一个带参数的宏,用来判断一个字符是否为字母,编写主函数,从键盘输入一个字符,
调用上述过程输出判断结果。
5.定义一个带参数的宏,用来计算圆的面积。编写程序读入大圆半径 R 和小圆半径 r,调用
宏计算出大圆面积减去小圆面积而得到的圆环面积。
【探索】
1.用条件编译实现以下功能。
输入一行电报文字,可以任选两种输出:一种是原文输出;另一种是将字母变成下一个字符
(如 a 变为 b,其他字符不变)。用#define 命令来控制是否要译成密码。例如:
若#define CHANGE 1 则输出密码;若#define CHANGE 0 则输出原文。
2.设计一个任意凸五边形,将 5 个顶点的坐标(自定义)事先保存在一个文件中,编写程序
计算它的面积和周长。

236
附录 A
预备知识

C 语言是一种程序设计语言,而计算机硬件能够直接识别和执行的是机器语言,用 C 语言编
写的程序必须通过编译、链接等过程转换为机器语言后,计算机硬件才能根据其含义执行,完成
特定的问题求解要求。因此,要想学好 C 语言,必须了解计算机的基本组成结构及其工作原理,
明确数据在计算机中的表示方法以及程序在内存空间的总体布局等知识。同时,要编写出结构清
晰、易于理解的 C 程序,掌握良好的程序编写规则是至关重要的。

附录 A.1 计算机硬件系统的基本工作原理
1.计算机硬件的基本构成
计算机硬件系统的核心是由中央处理器(CPU)和可执行的主存储器(简称主存,也称
内存)组成的计算引擎。这里,主存用来存储程序(可执行的机器指令序列)和数据,是可
以直接寻址单元的线性序列。除主存外,计算机系统还有辅存,即辅助存储器(如硬盘),
用来保存当前没有载入主存的、部分或临时载入的程序和数据。同时,计算机系统还需要从
外界获取数据或者将处理后的数据通知外界,这就需要另外一个组件即一组通信设备(如键
盘、显示器等),使用它们可以在用户和计算机之间以及计算机和计算机之间进行数据和命
令交换。
基本的计算机硬件系统的组成如附图 A-1 所示。

主存(内存) CPU 通信设备

辅存

附图 A-1 基本的计算机硬件系统的组成

硬件是计算机求解问题的物质基础,软件是完成问题求解、指挥计算机硬件工作的关键。硬
件和软件的结合形成一个有机整体,统称为计算机系统。当前大多数计算机系统都是以数学家
约翰·冯·诺依曼在 20 世纪 40 年代末期提出的“存储程序控制原理”为基础的,存储程序控
制原理是指,计算机在执行程序和处理数据时必须首先将程序和数据存储在主存储器中,计算
机在工作时自动高速地从主存中取出指令并加以分析和执行。附图 A-2 给出了计算机系统的基
本工作原理。

237
C 语言程序设计(第 2 版)

外存储器
外部
主机
设备 读 写

程序/原始数据 程序数据
输入/输出
内存储器
设备 结果
运行结果
读 存 取 存
写 指 取 数 数
命 令 命 据 据
令 令

输入输出命令 操作命令
控制器 运算器

附图 A-2 计算机系统的基本工作原理

其工作过程如下:
(1)把表示计算步骤的程序和计算中需要的原始数据,在控制器输入命令的作用下,通过输
入设备送入计算机的存储器。
(2)当计算开始时,在取指令命令的作用下把程序指令逐条送入控制器。
(3)控制器对指令进行译码,并根据指令的操作要求向存储器和运算器发出存数、取数命令
和运算命令,经过运算器计算后把结果存放在存储器内。
(4)在控制器发出取数和输出命令的作用下,通过输出设备输出计算结果。
由于计算机的程序和数据都保存在存储器中,所以学习 C 语言的一个重点是关注存储器的逻
辑结构。计算机的内存空间主要分为三个区:系统程序区、应用程序区和数据区。数据区又分为
系统程序数据区和应用程序数据区两类。系统程序区主要存放操作系统程序,计算机启动时,主
要从该区取出程序分析执行;应用程序区主要存放应用程序设计者编制的应用程序;数据区则作
为上述两类程序运行时的变量和中间结果的存放空间。
C 程序属于应用程序,程序代码本身存放在应用程序区,程序运行时处理的数据存放在应用
程序数据区,该数据区又分为静态数据区和动态数据区(动态数据区又分为堆栈区和堆区)两种。
通常,程序运行时所需要的数据根据存储属性的不同分别分配在这几个数据区中。除此之外,计
算机内部还存在一个存储数据的空间—寄存器区,寄存器区在 CPU 内部,系统对该区数据的访
问速度最快,但是在 CPU 内部,寄存器非常有限,一般只有几个到几十个。C 数据存储分区与硬
件关系示意如附图 A-3 所示。
2.存储器的抽象结构
在程序运行时,程序本身以及要处理的数据都是存放在主存中的,为了便于理解,可以想象
把计算机的主存的每一个存储单元从上向下依次叠放起来,右边的每一个小方框表示一个存储单
元(在计算机中一般以一个字节为一个存储单元)
,该方框左边的数字串代表了该存储单元的编号,
实际上是该存储单元的地址。附图 A-4 所示为存储器相对抽象的内部结构。
在附图 A-4 中,0x20000000 就是其右边对应方框所代表的存储单元的地址。它采用十六进
制表示形式(这部分内容在附录 A.2 中介绍),表示 32 位地址;计算机的一个存储单元有 8 位,
计算机里的字节固定就是 8 位,即一个字节占据一个存储单元。也就是说,一个 32 位的地址
编码对应了一个字节(8 位)的存储空间。这里所说的“位”是计算机存储的最小单位。从附

238
附录 A 预备知识

图 A-4 可以看出,32 位地址 0x20000000 所编号的存储单元存放的 8 位数据是 0x16。

0x00000000

… …

内存 0x20
0x10000000
CPU 系统程序区
0x10000001

运算器 应用程序区 …

控制器 静态数据区
0x20000000
0x16
寄存器 堆 栈 区 0x20000001

0x20000002
堆 区

附图 A-3 C 数据存储分区与硬件关系示意 附图 A-4 存储器的抽象内部结构

由此可知,计算机内存由若干存储单元构成,地址是计算机存储单元的编号,每个存储单
元一般有 8 位,可存放 8 位二进制数,内存的每个地址对应了一个唯一的存储单元;反过来,
每个存储单元都对应了一个唯一的地址,即内存地址与存储单元是一一对应的。因此通过内存
地址可以有效地区分这些存储单元,这样计算机就可以通过这个内存地址(即编号)有效地识
别某一个存储单元。当计算机要存取这个存储单元时,必须通过这个编号先找到这个存储单元。
C 程序中的地址概念实际上是变量所在的存储单元的地址编号。不同的变量可能占据不同数量的
存储单元,存放变量的第一个存储单元的地址就是该变量的地址,通过这个地址就可以操作这个变量。

附录 A.2 进制与进制转换
在自然界中使用最广、人们最熟悉的是用 0~9 的不同组合来表示不同的数据。计算机完成的
各种运算最终都是通过电子电路来实现的,因此在计算机中用 0 和 1 的不同组合来表示不同的数
据,这就涉及进制与进制转换的问题。
1.数与进制
常用的数的表示法有以下 4 种:十进制表示法、二进制表示法、八进制表示法和十六进制表
示法。这里所说的数的表示法涉及进位计数制(简称进制)的问题,进位计数制是指用一组特定
的数字符号按照一定的进位规则来表示数的计数方法。不同的进制使用不同的符号。二进制只采
用 0 和 1 这两个符号,其进位规则为“逢二进一”
;十进制采用 0~9 这 10 个符号,其进位规则为
“逢十进一”
;八进制采用 0~7 这 8 个符号,其进位规则是“逢八进一”
;十六制采用 16 个基本符
号,分别是 0~9 和 A、B、C、D、E、F 六个英文字母,其进位规则是“逢十六进一”
,A 代表
10,B 代表 11,C 代表 12,D 代表 13,E 代表 14,F 代表 15。
任意进制的数都可以表示为它的各位数字与位权乘积之和。位权是在某种进制的编码中,
某一个位置的基本符号为 1 时,它所代表的数值的大小。例如,一个十进制数 1234.5 可表示为
1×103+2×102+3×101+4×100+5×10−1

239
C 语言程序设计(第 2 版)

类似地,一个二进制数 1010 可表示为


1×23+0×22+1×21+0×20=10(十进制表示)
一个十六进制数 4A5 可表示为
4×162+ A×161+5×160=1024+160+5 = 1189(十进制表示)
由此可以看出,任意进制的数都可以通过进制转换以其他进制来表示。
2.不同进制数间的转换
(1)二进制、八进制和十六进制数转换为十进制数
其转换规则如下:
将二进制、八进制或十六进制数的各位数字与位权相乘再求和,所得和数即为转换结果。
这种转换比较简单,此处不再举例。
(2)十进制数转换为二进制、八进制和十六进制数
十进制数转换为二进制、八进制或十六进制数,其整数部分和小数部分需要采用不同的方法
分别进行转换。如果该十进制数只有整数部分,则采用“除基取余法”进行转换;如果只有小数
部分,则采用“乘基取整法”进行转换;如果既有整数部分又有小数部分,整数部分和小数部分
分别采用上述方法进行转换,然后将两个转换结果拼接起来即为最终转换结果。这里,
“基”是在
一种进制中所使用的基本符号的个数。十进制的基为 10,二进制的基为 2,八进制的基为 8,十
六进制的基为 16。下面分别介绍除基取余法和乘基取整法。
1)除基取余法
除基取余法是用十进制数反复除以转换后进制的基,记下每次所得的余数,直到商为 0 为止。
将所得的余数按最后一个余数到第一个余数的顺序依次排列起来即为转换结果。
【例 A-1】 将一个十进制整数 194 转换为八进制数。
其具体过程如下:
① 194÷8=24……2
余数为 2
② 24÷8=3……0
余数为 0
③ 3÷8=0……3(商为 0,转换过程结束)
余数为 3
所以,十进制整数 194 转换为八进制数是 302,即
(194)10=(302)8
2)乘基取整法
乘基取整法是用十进制小数乘以转换后进制的基,得到一个乘积,将乘积的整数部分取出来,
再用余下的小数乘以该进制的基,重复以上过程,直到乘积的小数部分为 0 或满足转换精度要求
为止。将每次取得的整数按照从第一个整数到最后一个整数的顺序依次排列起来即为转换结果。
【例 A-2】 将一个十进制小数 0.78125 转换为十六进制数。
其具体过程如下:
① 0.78125×16=12.5
整数部分为 12,即为十六制的 C
② 0.5×16=8.0(小数部分为 0,转换过程结束)
整数部分为 8

240
附录 A 预备知识

所以十进制小数 0.78125 转换为八进制数是 0.C8,即


(0.78125)10=(0.C8)16
3.二进制数与八进制、十六进制数间的转换
由于二进制数与八进制、十六进制数具有特殊关系,即一个八进制位对应于 3 个二进制位,
一个十六进制位对应于 4 个二进制位,因此由二进制转换成八进制或十六进制,以及做反向转换
都可以采用下面分组的方法。
(1)二进制数与八进制数的相互转换其转换规则如下:
对于二进制数的整数部分,从最低位开始从右向左,每 3 位数字分成一组,最后一组若不足
3 位,高位补 0;把每组数转换成对应的八进制数码即得到转换结果。
对于二进制数的小数部分,其转换方法类似,只是分组方向是小数点后从左向右,分组时末
尾若不足 3 位,则低位补 0 即可。
【例 A-3】 将二进制数 11101111010.1011 转换为八进制数。
其具体过程如下:
以小数点为分界,分别向左右两侧进行分组,每 3 位为一组。
高位补一个 0 (011 101 111 010.101 100)2 低位补两个 0
(3 5 7 2. 5 4 )8
所以,二进制数 11101111010.1011 转换为八进制数是 3572.54,即
(11101111010.1011)2 = (3572.54)8
将八进制数转换为二进制数的方法与二进制数转换为八进制数的方法相反。
【例 A-4】 将八进制数 712.46 转换为二进制数。
其具体过程如下:
(7 1 2. 4 6)8
(111 001 010. 100 110)2
所以,八进制数 712.46 转换为二进制数是 111001010.10011,即
(712.46)8=(111001010.10011)2
(2)二进制数与十六进制数的相互转换。二进制数与十六进制数的相互转换方法和上述二进
制数与八进制数之间的转换类似,不同之处是每 4 位数字分成一组。
(3)八进制数与十六进制数的相互转换。转换可借助二进制数或十进制数作为桥梁来进行转
换,即先将八进制数转换为二进制数或十进制数,然后将其转换为十六进制数。
3.带符号数的表示方法
前面介绍的是不带符号的二进制数,但计算机中经常要处理带符号的二进制数。表示一个带
符号的二进制数有三种方法:原码法、反码法和补码法。
(1)原码法
在这种表示法中,一个带符号的二进制数由数的符号(正或负)和数的值构成,而数的符号
和数的值均由二进制数 0 和 1 来表示,一般用 0 表示正,用 1 表示负,数的值由多位二进制数表
示。为了避免混淆,在符号数的表示中还必须明确规定符号的位置。在大多数计算机中,都用二
进制数的最高位表示符号。
例如,把十进制符号数+45 和-45 分别表示为 8 位二进制符号数,结果如下:
(+45)10 = (0 0101101)2
符号位数 45 的值

241
C 语言程序设计(第 2 版)

(−45)10 = (1 0101101)2
符号位数 45 的值
可以看出,用原码来表示一个符号数是由符号位和数值凑到一起来实现的。这种表示正负数
的方法很好理解,但计算机在实现这种符号数的运算时很麻烦。因此,这种表示方法在微处理机
问世前就不为人们所使用了。
(2)反码法
在计算机的早期,曾采用反码法来表示带符号的数。对于正数,其反码与其原码相同。例如,
(+45)10 = (00101101)2
也就是说,正数用符号位与数值凑到一起来表示。
对于负数,用相应正数的原码各位取反来表示,包括将符号位取反,取反的含义是将 0 变为
1,将 1 变为 0。例如,(−45)10 的反码表示就是将(+45)10 对应的二进制数各位取反,即:
(+45)10 = (0 0 1 0 1 1 0 1)2
按位取反后的结果如下:
(−45) 10 = (1 1 0 1 0 0 1 0)2
同样,可以写出如下几个数的反码表示,以便读者对照:
(+4)10=(00000100)2
(−4)10=(11111100)2
(+7)10 = (00000111)2
(−7)10=(11111000)2

用原码法和反码法表示符号数时,数值 0 有两种表示方法,使用起来不方便。现在
计算机中的符号数不用这两种方法来表示,通常采用补码法来表示。

(3)补码法
在计算机中,符号数是用补码来表示的。用补码法表示带符号数的法则是:正数的表示方法
与原码法和反码法一样,负数的表示方法为该负数的反码表示加 1。例如,(−45)10 的补码表示就
是(−45)10 对应的反码表示加 1,即
(−45)10=(1 1 0 1 0 0 1 0)2 (为反码表示)
加 1 后的结果如下:
(−45)10=(1 1 0 1 0 0 1 1)2 (为补码表示)
同样,把前面提到的几个数的补码表示列在下面供参考:
(+4)10=(00000100)2
(−4)10=(11111110)2
(+7)10=(00000111)2
(−7)10=(11111001)2
如果用 8 位二进制数的补码来表示带符号数,则它所能表示的数值范围为(−128)10~(+127)10,
即(10000000)2~(0111111l)2。若数值的大小在上述范围之外,则必须采用更多位数的二进制编码
来表示符号数。例如,用 16 位二进制数或用浮点数来表示。
另外,在补码法的使用中,除了将带符号的十进制数表示为补码数外,还经常遇到将一个补
码数转换为带符号的十进制数。对于一个正数,可以直接利用将二进制数转换成十进制数的方法

242
附录 A 预备知识

来转换,并标上相应的符号。例如,补码数(01001001) 2 是一个正数,其十进制数值为+73。
对于负数,首先确认它是负数,然后用求其补码的方法,得到它的绝对值,即可求得该负数
的值。

附录 A.3 规范化编程
无论采用何种程序设计语言来编写程序,都要做到语法正确、逻辑正确,从而能够被计算机
所理解,正确地执行下去,得到预期的结果。同时,还应做到编码规范,具有充分的内部文档,
使程序的可读性强、可理解性强,从而使得程序被计算机所理解的同时,还能够更好地被人所理
解,便于程序的修改与维护。这就是规范化编程问题。
规范化编程的作用是使代码容易阅读,无论是对程序员本人,还是对其他人。好风格应该成
为一种习惯,初学编写程序时,需关心程序编写风格问题,逐渐养成一种好的编程习惯。随着计
算机硬件性能的提高,价格的下降,软件复杂性越来越高,编写的代码是否清晰易懂已成为衡量
程序好坏的一个重要标准。在软件产业化发展的时代,编码风格是衡量一个程序员水平的重要指
标之一,规范化编程已经成为一种应予以遵循的准则。因此,一定要充分重视规范化编程的问题。
虽然对于初学者来说,一开始也许会觉得难以体会其中的意义,但记忆是理解与掌握的前提,首
先记住这些原则,然后在学习过程中有意识地去尝试和领会,对养成一种良好的编程习惯来说是
非常有益的。
下面具体介绍编码风格的一般原则。
1.命名
选取含义鲜明的名字,使它能正确地提示程序对象所代表的实体,这对于帮助阅读者理解程
序是很重要的。匈牙利命名法是目前应用最广泛的一套变量命名规则,其形式为:[前缀]—数据
类型的缩写—程序对象名字。本书采用的命名规则是匈牙利命名法的一个简化版本,其要达到的
目标是“见名识义”和“见名识型”。
2.源程序书写格式
(1)一般采用缩进的方式来表示程序的层次结构,每一层代码都比上一层缩进固定数目的字
符位(2 个或 4 个字符),这样编写出来的代码层次非常清晰,便于阅读与理解。
(2)尽量做到一个声明或一个语句占一行。
3.算法设计
(1)进行算法设计时,尽量避免使用 goto 语句,设计出符合结构化编程思想的算法结构,因
为程序编写的目标是写出最清晰的代码,而不是最巧妙的代码。无论最终代码的长度如何,只要
做到清晰易懂就是好的设计。
(2)编写程序时不仅要考虑正常处理过程,还要考虑各种异常情况的处理,使得程序模块对
于各种可能的错误具有一定的容错性能。例如,检查输入数据的范围、检查函数的返回值、检查
指针变量是否为空、判断打开数据库和文件操作是否成功等。对异常情况的检查使得程序能够对
错误进行预先处理,避免程序崩溃,提高软件的可靠性。
4.程序注释
注释是帮助阅读和理解程序的一种有效手段,每一个程序都应该有一个表明程序用途的首部
注释。首部注释应该简洁地点明程序的突出特征,或提供一种概念,以帮助理解程序。具体内容

243
C 语言程序设计(第 2 版)

包括:
(1)程序的名称是什么?(what)
(2)程序是谁编写的?(who)
(3)程序在整个系统中处在什么位置?(where)
(4)为什么要编写这个程序?(why)
(5)程序是怎样使用它的数据结构、算法和控制结构的?(how)
使用注释还能够记录代码被修改的历史,包括修改时间、修改人、修改的原因和目的以及修
改内容。编写注释时,要注意的是应该提供那些不能马上从代码中看出来的信息,不需要用注释
的形式把每个语句都翻译成自然语言,应该利用注释提供一些额外的信息。

244
附录 B
ASCII 码字符集

十进制数值 十六进制数值 ASCII 字符 含义


0 00 NUL 空
1 01 SOH (start of heading) 标题开始
2 02 STX (start of text) 正文开始
3 03 ETX (end of text) 正文结束
4 04 EOT (end of transmission) 传输结束
5 05 ENQ (enquiry) 请求
6 06 ACK (acknowledge) 确认
7 07 BEL (bell) 响铃
8 08 BS (backspace) 退格
9 09 HT (horizontal tab) 水平制表符
10 0A LF (NL line feed, new line) 换行键
11 0B VT (vertical tab) 垂直制表符
12 0C FF (NP form feed, new page) 换页键
13 0D CR (carriage return) 回车键
14 0E SO (shift out) 关闭切换
15 0F SI (shift in) 启动切换
16 10 DLE (data link escape) 数据传送换码
17 11 DC1 (device constrol 1) 设备控制 1
18 12 DC2 (device constrol 2) 设备控制 2
19 13 DC3 (device constrol 3) 设备控制 3
20 14 DC4 (device constrol 4) 设备控制 4
21 15 NAK (negative acknowledge) 拒绝接收
22 16 SYN (synchronous idle) 同步空闲
23 17 ETB (end of trans. block) 传输块结束
24 18 CAN (cancel) 取消
25 19 EM (end of medium) 介质中断
26 1A SUB (ubstitute) 替补
27 1B ESC (escape) 退出
28 1C FS (file separator) 文件分隔符
29 1D GS (group separator) 组分隔符

245
C 语言程序设计(第 2 版)

续表
十进制数值 十六进制数值 ASCII 字符 含义
30 1E RS (record separator) 记录分隔符
31 1F US (unit separator) 单元分隔符
32 20 space
33 21 !
34 22 "
35 23 #
36 24 $
37 25 %
38 26 &
39 27 '
40 28 (
41 29 )
42 2A *
43 2B +
44 2C ,
45 2D -
46 2E .
47 2F /
48 30 0
49 31 1
50 32 2
51 33 3
52 34 4
53 35 5
54 36 6
55 37 7
56 38 8
57 39 9
58 3A :
59 3B ;
60 3C <
61 3D =
62 3E >
63 3F ?
64 40 @
65 41 A
66 42 B
67 43 C
68 44 D
69 45 E
70 46 F
71 47 G
72 48 H
73 49 I
74 4A J
75 4B K
76 4C L
77 4D M
78 4E N
79 4F O

246
附录 B ASCII 码字符集

续表
十进制数值 十六进制数值 ASCII 字符 含义
80 50 P
81 51 Q
82 52 R
83 53 S
84 54 T
85 55 U
86 56 V
87 57 W
88 58 X
89 59 Y
90 5A Z
91 5B [
92 5C \
93 5D ]
94 5E ^
95 5F _
96 60 `
97 61 a
98 62 b
99 63 c
100 64 d
101 65 e
102 66 f
103 67 g
104 68 h
105 69 i
106 6A j
107 6B k
108 6C l
109 6D m
110 6E n
111 6F o
112 70 p
113 71 q
114 72 r
115 73 s
116 74 t
117 75 u
118 76 v
119 77 w
120 78 x
121 79 y
122 7A z
123 7B {
124 7C |
125 7D }
126 7E ~
127 7F DEL 删除

247
附录 C
运算符的优先级与结合性

运 算 符 含 义 要求运算对象的个数 优 先 级 结 合 性
() 圆括号
[] 下标运算符
左结合
-> 指向结构体成员运算符
· 结构体成员运算符

! 逻辑非运算符
~ 按位取反运算符
++ 自增运算符
−− 自减运算符
+ 正值运算符
1(单目运算符) 右结合
− 负值运算符
(类型) 强制类型转换运算符
* 取内容运算符
& 取地址运算符
sizeof 求字节数运算符
* 乘法运算符
/ 除法运算符 2(双目运算符) 左结合
% 模运算符(求余运算符)
+ 加法运算符
2(双目运算符) 左结合
− 减法运算符
<< 左移运算符
>> 右移运算符
< <= > >= 关系运算符 2(双目运算符) 左结合
== 等于运算符
2(双目运算符) 左结合
!= 不等于运算符
& 按位与运算符 2(双目运算符) 左结合
^ 按位异或运算符 2(双目运算符) 左结合
| 按位或运算符 2(双目运算符) 左结合
&& 逻辑与运算符 2(双目运算符) 左结合
|| 逻辑或运算符 2(双目运算符) 左结合
?: 条件运算符 3(三目运算符) 低 右结合
= += −= *= /= %= 赋值运算符
2(双目运算符) 右结合
>>= <<= &= ^= |= (包括复合赋值运算符)
, 逗号运算符 2(双目运算符) 左结合

248
第1章 运算符的优先级与结合性

249
附录 D
C 库函数

1.数学函数
使用数学函数时,需要在源文件中使用以下命令行:
#include <math.h> 或 #include "math.h"

函数名 函数原型 功能 返回值 说明


abs int abs (int x) 求整数 x 的绝对值 计算结果
−1
acos double acos(double x); 计算 cos (x)的值 计算结果 x 应在−1 到 1 范围
−1
asin double asin(double x); 计算 sin (x)的值 计算结果 x 应在−1 到 1 范围
−1
atan double atan(double x); 计算 tan (x)的值 计算结果
atan2 double atan2 (double x, double y); 计算 tan−1(x/y)的值 计算结果
cos double cos (double x); 计算 cos(x)的值 计算结果 x 的单位为弧度
计算 x 的双曲余弦 cosh(x)
cosh double cosh (double x); 计算结果
的值
exp double exp(double x); 求 ex 的值 计算结果
fabs double fabs(double x); 求 x 的绝对值 计算结果
该整数的双精
floor double floor(double x); 求出不大于 x 的最大整数
度数
返回余数的双
fmod double fmod(double x, double y); 求整数 x/y 的余数
精度数
把双精度数 val 分解为数字
部分(尾数)x 和以 2 为底 返回数字部分
frexp double frexp(double val, int *eptr);
的指数 n,即 val=x*2n,n x 0.5≤x<1
存放在 eptr 指向的变量中
log double log(double x); 求 loge x,即 ln x 计算结果
log10 double log10(double x); 求 log10 x 计算结果
把双精度数 val 分解为整数
double modf(double
modf 部分和小数部分,把整数部 val 的小数部分
val, double *iptr);
分存到 iptr 指向的单元
pow double pow(double x, double y); 计算 xy 的值 计算结果
rand int rand(void); 产生 0~32767 的随机数 随机整数
sin double sin(double); 计算 sin(x)的值 计算结果 x 的单位为弧度

249
C 语言程序设计(第 2 版)

续表
函数名 函 数 原 型 功 能 返 回 值 说 明
计算 x 的双曲正弦函数
sinh double sinh(double x) 计算结果
的值

sqrt double sqrt(double x); 计算 x 的值 计算结果 x≥0


tan double tan(double x); 计算 tan(x)的值 计算结果 x 的单位为弧度
计算 x 的双曲正切函数
tanh double tanh(double x); 计算结果
的值

2.字符函数和字符串函数
ANSI C 标准要求在使用字符串函数时要包含头文件"string.h",在使用字符函数时要包含头文
件"ctype.h"。有的 C 编译器不遵循 ANSI C 标准的规定,而用其他名称的头文件。

函数名 函数原型 函数功能 返回值 包含文件


检查 ch 是否是字母(alpha)或数字 是字母或数字返回 1;
isalnum int isalnum (int ch); ctype.h
(number) 否则返回 0
是,返回 1;
isalpha int isalpha (int ch); 检查 ch 是否是字母 ctype.h
不是,返回 0
检查 ch 是否控制字符(其 ASCII 码 是,返回 1;
iscntrl int iscntrl (int ch); ctype.h
值在 0 和 0x1F 之间) 不是,返回 0
是,返回 1;
isdigit int isdigit (int ch); 检查 ch 是否是数字(0~9) ctype.h
不是,返回 0
检查 ch 是否可打印字符(其 ASCII 是,返回 1;
isgraph int isgraph (int ch); ctype.h
码在 0x21 到 0x7E 之间,
不包括空格) 不是,返回 0
是,返回 1;
islower int islower (int ch); 检查 ch 是否是小写字母(a~z) ctype.h
不是,返回 0
检查 ch 是否可打印字符(包括空格), 是,返回 1;
isprint int isprint (int ch); ctype.h
其 ASCII 码在 0x20 到 0x7E 之间 不是,返回 0
检查 ch 是否是标点字符(不包括空
是,返回 1;
ispunct int ispunct (int ch); 格),即除字母、数字和空格以外 ctype.h
不是,返回 0
的所有可打印字符
检查 ch 是否是空格、跳格符(制表 是,返回 1;
isspace int isspace (int ch); ctype.h
符)或换行符 不是,返回 0
是,返回 1;
isupper int isupper (int ch); 检查 ch 是否是大写字母(A~Z) ctype.h
不是,返回 0
检查 ch 是否是一个十六进制数字字 是,返回 1;
isxdigit int isxdigit (int ch); ctype.h
符(即 0~9,或 A~F,或 a~f) 不是,返回 0
char * strcat (char * 把字符串 str2 接到 str1 后面,str1
strcat str1 string.h
str1, char * str2) 最后的'\0'被取消
char * strchr (char * 找出 str 指向的字符串中第一次出现 返回指向该位置的指针;
strchr string.h
str, int ch); 字符 ch 的位置 如找不到,则返回空指针
str1<str2 , 返 回 负 数 ;
int strcmp (char *
strcmp 比较两个字符串 str1、str2 str1=str2,返回 0;str1> string.h
str1, char * str2);
str2,返回正数

250
附录 D C 库函数

续表
函数名 函数原型 函数功能 返回值 包含文件
char * strcpy (char *
strcpy 把 str2 指向的字符串复制到 str1 中去 返回 str1 string.h
str1, char * str2);
unsigned int strlen 统计字符串 str 中字符的个数(不包
strlen 返回字符个数 string.h
(char * str); 括终止符' \ 0')
找出 str2 字符串在 str1 字符串中第
char * strstr (char * 返回该位置的指针;如找
strstr 一次出现的位置(不包括 str2 的串 string.h
str1, char * str2); 不到,返回空指针
结束符)
返回 ch 所代表的字符的
tolower int tolower (int ch); ch 字符转换为小写字母 ctype.h
小写字母
toupper int toupper (int ch); 将 ch 字符转换成大写字母 与 ch 相应的大写字母 ctype.h

3.输入输出函数
使用输入输出函数时,需要在源文件中使用以下命令行:
#include <stdio.h>或#include "stdio.h"

函数名 函 数 原 型 函 数 功 能 返 回 值 说 明
使 fp 所指文件的错误标志和
clearerr void clearer (FILE * fp); 无
文件结束标志置 0
非 ANSI C
close int close (int fp); 关闭文件 关闭成功返回 0;不成功返回–1
标准

int creat ( char * filename , 非 ANSI C


creat 以 mode 所指定的方式建立文件 成功则返回正数;
否则返回–1
int mode); 标准
遇文件结束,返回 1;否则返 非 ANSI C
eof inteof ( int fd ); 检查文件是否结束
回0 标准
关闭 fp 所指向的文件,释放
fclose int fclose (FILE * fp); 有错则返回非 0;否则返回 0
文件缓冲区
遇文件结束符返回非零值;否
feof int feof (FILE * fp); 检查文件是否结束
则返回 0
从 fp 所指定的文件中取得下 返回所得到的字符;若读入出
fgetc int fgetc (FILE * fp);
一个字符 错,返回 EOF
从 fp 指向的文件读取一个长
char * fgets (char * 返回地址 buf;若遇到文件结
fgets 度为(n−1)的字符串,存入
buf, int n, FILE * fp); 束或出错,返回 NULL
起始地址为 buf 的空间
成功,返回一个文件指针(文
FILE * fopen (char * 以 mode 指定的方式打开名为
fopen 件信息区的起始地址);否则
filename, char * mode); filename 的文件
返回 0

int fprintf (FILE * fp, 把 args 的值以 format 指定的格


fprintf 实际输出的字符数
char * format, args,...); 式输出到 fp 所指定的文件中

int putc (char * str, 将字符 ch 输出到 fp 指定的文 成功,则返回该字符;否则返


fputc
FILE * fp ); 件中 回非 0

int fputs (char * str , 将 str 指定的字符串输出到 fp


fputs 返回 0;若出错返回非 0
FILE * fp ); 所指定的文件

251
C 语言程序设计(第 2 版)

续表
函数名 函 数 原 型 函 数 功 能 返 回 值 说 明

int fread (char * pt, 从 fp 指定的文件中读取长度


返回所读的数据项个数;如遇
fread unsigned size , unsigned 为 size 的 n 个数据项,存入到
n , FILE * fp ); 文件结束或出错返回 0
pt 所指向的内存区
从 fp 指定的文件中按 format 给
int fscanf (FILE * fp ,
fscanf 定的格式将输入数据送到 args 已输入的数据个数
char format , args, ...);
所指向的内存单元
(args 是指针)
将 fp 所指向的文件的位置指针
int fseek (FILE * fp ,
fseek 移到以 base 所指出的位置为基 返回当前位置;否则,返回−1
long offset , int base );
准、以 offset 为位移量的位置
返回 fp 所指向的文件中的读 返回 fp 所指向的文件中的
ftell long ftell (FILE * fp);
写位置 读写位置

int write (char * ptr , 把 ptr 所指向的 n*size 个字节 写到 fp 文件中的数据项的


fwrite
unsigned n , FILE * fp); 输出到 fp 所指向的文件中 个数
从 fp 所指向的文件中读入一 返回所读的字符;若文件结束
getc int getc (FILE * fp);
个字符 或出错,返回 EOF
从标准输入设备读取下一个 所读字符;若文件结束或出
getchar int getchar (void );
字符 错,则返回−1
从 fp 所指向的文件读取下一 输入的整数;如文件结束或出 非ANSI C 标
getw int getw (FILE * fp);
个字符(整数) 错,返回−1 准函数

int open ( char 以 mode 指出的方式打开已存 返回文件号(正数);如打开 非ANSI C 标


open
* filename , int mode); 在的名为 filename 的文件 失败,返回−1 准函数
format可以是
按 format 指向的格式字符串
int printf (char 输出字符个数;若出错,返 一个字符串,
printf 所规定的格式,将输出表列
* format,args , ...); 回负数 或字符数组
args 的值输出到标准输出设备
的起始地址

int putc (int ch , FILE 把一个字符 ch 输出到 fp 所指 输出的字符 ch;若出错,


putc
* fp ); 的文件中 返回 EOF

putchar int putchar (char ch ); 把字符 ch 输出到标准输出设备 输出的字符;若出错,返回 EOF


把 str 指向的字符串输出到标
返回非负数;若失败,返回
puts int puts (char * str ) ; 准输出设备,将‘\ 0’转换为
EOF
回车换行

int putw (int w, FILE* 将一个整数 w(即一个字)写 返回输出的整数;若出错, 非ANSI C标


putw
fp ); 到 fp 指向的文件中 返回 EOF 准函数

int read ( int fd , 从文件号 fd 所指示的文件中 返回真正读入的字节个数;


非ANSI C标
read char * buf , unsigned 读 count 个字节到由 buf 指示 如遇文件结束返回 0,出错
count); 准函数
的缓冲区中 返回−1
int rename (char * 把由 oldname 所指的文件名, 成功返回 0;
rename oldname , char *
newname); 改为由 newname 所指的文件名 出错返回−1

将 fp 指示的文件中的位置指
void rewind (FILE *
rewind 针置于文件开头位置,并清除 无
fp);
文件结束标志和错误标志

252
附录 D C 库函数

续表
函数名 函 数 原 型 函 数 功 能 返 回 值 说 明
从标准输入设备按 format 指向 读入并赋给 args 的数据个
int scanf (char * format,
scanf 的格式字符串所规定的格式, 数,遇文件结束返回 EOF; args 为指针
agrs );
输入数据给 args 所指向的单元 出错返回 0

int write (int fd , char * 从 buf 指示的缓冲区输出 count 返回实际输出的字节数;如 非 ANSI C


write
buf , unsigned count); 个字符到 fd 所标志的文件中 出错返回−1 标准函数

4.动态存储分配函数
ANSIC 标准建议设置 4 个有关动态存储分配的函数,即 calloc( )、free( )、malloc( )和 realloc( ),
使用时需要包含头文件"stdio.h",但许多 C 编译系统要求用"malloc.h"而不是"stdio.h",具体情况应
查阅相关手册。
ANSIC 标准要求动态分配系统返回 void 指针。void 指针具有一般性,它们可以指向任何类
型的数据,在使用时需要用强制类型转换将其转换成所需的类型。

函 数 名 函 数 原 型 函 数 功 能 返 回 值
分配 n 个数据项的内存连续空 分配内存单元的起始地
calloc void * calloc (unsigned n , unsign size );
间,每个数据项的大小为 size 址;如不成功,返回 0
free void free (void * p) 释放 p 所指向的内存区 无
所分配的内存区起始地
malloc void * malloc (unsigned size); 分配 size 字节的存储区
址;如内存不够,返回 0
将 p 所指向的已分配内存区的
realloc void * realloc (void * p, unsigned size); 大小改为 size,size 可以比原 返回指向该内存区的指针
来分配的空间大或小

253
参考文献

[1]Stephen G.Kochan. C 语言编程(第三版).张小潘译. 北京:电子工业出版社,2006.


[2]陈良银,等.C语言程序设计.北京:清华大学出版社,2006.
[3]孟庆昌,等.C 语言程序设计.北京:人民邮电出版社,2006.
[4]杨起帆,等.C语言程序设计教程.杭州:浙江大学出版社,2006.
[5]张树粹,孟佳娜.C语言程序设计.北京:人民邮电出版社,2006.
[6]蒋清明,等.C语言程序设计.北京:人民邮电出版社,2006.
[7]谭浩强.C程序设计(第三版).北京:清华大学出版社,2003.
[8]杨峰.妙趣横生的算法(C 语言实现).北京:清华大学出版社,2010.
[9]尹宝林.C 程序设计思想与方法.北京:机械工业出版社,2009.
[10]Samuel P.Harbison,Guy L.Steele.C语言参考手册.邱仲潘译.北京:机械工业出版社,
2003.
[11]Waite,S.Prata.新编C语言大全.范植华,樊莹,译.北京:清华大学出版社,1998.
[12]Herbert Schildt.ANSI C标准详解.王曦若,李沛,译.北京:学苑出版社,1998.

254

You might also like