首页资源分类嵌入式开发其它 > [C程序设计].张长海.文字版

[C程序设计].张长海.文字版

已有 454746个资源

下载专区

上传者其他资源

文档信息举报收藏

标    签: C语言

分    享:

文档简介

[C程序设计].张长海.文字版

文档预览

高等学校教材 C程序设计 张长海 陈 娟 编著 高等教育出版社 内容提要 本 书以 国 际 标 准 ISO/IEC9899:1999和 国 家 标 准 GB/T15272-94定 义的 C语 言 为 载 体,阐 述 基 本 的程 序 设计 方 法 , 并 对 相 关 的 C语 言 成 分进 行较 严 格的 介绍 。用 BNF表 示 C语 言 的 语法,引进 PAD 图表 示程 序 逻 辑 。 全书 共分 十 四 章 ,主 要 内容 包括 :BNF、PAD 图、程 序 设计方 法、程序 开发 和 结 构 化 程序 设计 以 及 C语 言的 各 种 词 法 单 位 、数据 类 型 、语 句、函 数 等。 每章 都 包 含 大 量例 题,并附 有大量 习 题 ,以 利于 读 者 提 高程序 设 计 能 力和 学习 掌握 相 关 语言 概念 。 本 书最 大 的 特 点 是以 “程 序设 计”为主 线,把重 点放 在 讲 述 程 序 设 计方 法 上 。 摈 弃 了 目 前 各 种 程 序 设计 书 中 流 行 的 以 “解 释程 序设 计语 言 ”为主 的 做 法。 全书 整体 结 构 良 好,图 文并 茂,知识 体 系 新 颖 完整 ,概 念 准 确 ;注 重对 读 者 进 行 程 序 设 计方 法及 算 法 的 训 练,力 求 体现 “结 构化 程序 设 计 ”思 想 ,注 重培 养 和训 练读 者良 好 的 程 序 设计 风格 。 本 书可 作 为 高 等 院校 计算 机系 各 专 业 “高 级 语 言程序 设计 ”、“C语言程 序设 计 ”、“程 序 设计 基础 ”等课 程 的 教 材 和 参 考 书,也 可 供 其 他 专业 学生 以 及从 事计算 机 工 作 的有 关人 员阅 读 参 考 。 策划编辑 倪文慧 责任编辑 武林晓 市场策划 陈 振 封面设计 王凌波 责任印制      出版发行 高等教育出版社 社  址 北京市西城区德外大街 4号 邮 政编 码  100011 总    机  010-58581000 购 书 热 线  010-64054588 免 费 咨 询  800-810-0598 网     址   http://www.hep.edu.cn           http://www.hep.com.cn 经  销 新华书店北京发行所 印  刷 开    本  787×1092  1/16 印    张  28.25 字    数  590000 版  次   年 月第 1版 印  次   年 月第 次印刷 定     价  30.00元 本书 如 有缺 页 、倒 页 、脱 页等 质 量 问 题 ,请 到 所 购 图书 销 售 部 门 联 系调 换 。 版权所有 侵权必究 物料 号:15116-00 前  言 本书适 用于“高级 语言程 序 设 计”或“程 序 设 计 基 础 ”课 程。该 课程 是计 算 机系 的专 业 基础 课,在计算 机专业 教学中占 有重 要 地 位。 学 好该 课程 既 可以 为后 续 课 程 打 下良 好的 基 础,又会 对学生 一生的 程序设计 技术、技 巧、风 格和习 惯产生 深远影 响。 本书重点 在于程 序设计 ,而 对 C语言 本身则采 取有所 取、有所不 取的策略 。 对 于那些 常 用的 语言成 分,直接讲 述与程序 设 计 方 法 有 关的 语 言 成 分,详细 准 确 地 介 绍;对 于那 些与 程 序设 计方法 联系不 太紧要 ,但是 还 常 用 的 部 分,放 在 最 后 简 单介 绍;而 对 于 那 些 与讲 述程 序 设计 方法关 系不太 大,也不常用 的部分 则根本 不涉及 。 本书力图 在深度 、广度和知 识结构 上作出 合理的 安排。 试图在 既训练 学生 的编 程 能力, 又培 养学生 的抽象 思维能 力上下 功夫;使学生 既具有 较强 的编 程 能力,又能 掌握 高 级语 言 C 本身 的语法 和语义 ,同 时在知识 结构、知 识面上尽 量做到 广泛、深入 。 本书作 者从事 计算机 教学已 经 20余年 ,讲 授过 10多门 计算 机方 面 的课 程。曾 十余 次 为吉 林大学 计算机 系本科 生主讲 “高 级语 言程序设 计”课 。对 C语 言进行 了深 入研究 ,仔 细 研究 了国际标 准 ISO/IEC9899∶1999和中 华人民 共和国 国家标 准 GB/T15272-94。本书 是 作者二十余年教学实践的总结。 作为大 学本科 计算机 专业基 础课教 材,本 书具有 如下特 点: 1.全书 整体结 构好,知识 体系新 颖完整,章节 安排 合 理,并 注 意由浅 入深 地介绍 程序 设 计知 识。比如 有关函 数的知 识,由浅入 深地分 三章介 绍;有关指 针的知 识分散 到 各 个章节 介 绍,免得 集中在 一章,使学 生学起 来枯燥 乏味,接受困 难。 2.注重 对学生 进行严格 的抽象 思维训 练。严 格按照 国际标 准 ISO/IEC9899∶1999和 国 家标 准 GB/T15272-94介绍 C语言,并使 用 BNF表示语 法,使 用自然 语言叙 述语义 。对 C 语言 语法、语义 的描述 严格、细致、准确 ,并 且 形式 化,为后 续 课 程 (例如 编译 原 理)打 下了 良 好的基础。 3.本书 最大的 特点是以 “程 序 设计 ”为主 线 ,重 点 放 在讲 述 程 序 设计 方法 上 ,摈 弃了 目 前各 种程序 设计书 中流行 的以“解释 程序设 计语言 ”为主 的做 法;注重对 学生 进行程 序设 计 方法 及算法 的训练 ,力 图 做 到严 格 的理 论 与 具 体 方 法 、算 法有 机 结 合。 全 书配 备 有 大 量 例 题,一方 面,在讲解 例题时 着重于 算 法的 构 造 ,以 便 训 练学 生 的 编 程能 力 ;另一 方 面,概念 的 介绍 都以例题 作引导 ,从具体实 例出发 ,使 概念引 进得自 然且 容 易理 解。本书 绝 大 部分例 题 不是 单纯地 为了解 释语言 概念,而 是 从 构 造 算法 出 发 ,以 训 练学 生 的 实 际 编程 能 力为 目标。 · 2· 前  言 另外 ,本 书还配 备有大 量的习题 ,以便学 生做课后 练习和 进一步 提高。 4.全书 自始至 终贯穿结 构化程 序设计 思想,所有 例题 都具 有 良好 的结构 和程序 设计 风 格。 目的是 给学生 一个示 范,使 学生 从开 始学 习 程序 设计 就 养成 一个 良 好 的 程 序设 计习 惯 和风格。 5.本书 图文并 茂,引进 PAD图表 示程序 逻辑。 PAD图的结 构比传 统的流程 图、NS图等 都好 ,同 时也比 直接用 程序表示 算法更 直观,易于 理解。 全书共 十四章 ,大 致分为四 部分。 第一部 分基本 知识,包括 第 一 、二 、三章。 第一 章 介 绍程 序 设计 基 本概 念 、BNF和 PAD 图;第二 章介绍 C语 言基本 符号、单词、数 据及 其类型 ;第 三章 介绍常 量、变量 、表 达式 、简 单 程序 、赋 值和输 入 /输出。 第二部 分程序 设计,包括第 四、五、八、九章 。 第 四章 简 单 介 绍模 块化 程序 设 计 思 想,引 进子 程序和函 数概念 ;第五章讲 述结构 化程序 设计的 顺序、分 支 、重 复三种 程序 逻辑,并介 绍 实现 这三种 程序逻 辑的 C语言流 程控制 语句 ;第 八章 进一步 介绍 函数,讲 述参数 、作 用域 以 及递 归程序 设计;第九 章介绍程 序 开 发 和 结 构化 程 序 设 计,包括 结 构 化 程 序设 计 原则 、程 序 风格 、自 顶向下 逐步求 精的 程 序 设 计 技 术、程 序 正 确 性、可 移 植 性 、文 档 以 及穷 举 法 和 试 探 法。 第三部 分数据 组织,包 括第 六 、七、十、十 一 、十 二章 。 第 六 章 讲 述 数 组;第 七 章 介 绍 指 针;第十 章讲述 文件及 其操作;第十 一章讲 述对复 杂数据 的描 述 ,引 进结构 体和 共用 体 ;第 十 二章讲述动态数据结构及其在程序设计中的应用。 第四部 分,包括第 十三、十四 章。第 十 三章 进一 步 介绍 函数 ,讲 述函 数作 参 数和 函数 副 作用 等较深 入的 问 题;第 十 四章 介绍 存 储类 别、位操 作、break和 continue语句、编译 预处 理 等一 些 C语言独 有的特 色。 本书的 第七、十一 、十 二、十 四章由 陈娟 执笔,附 录 四由 崔燕 提供 素材,其 余 各章 节由 张 长海 执笔。 全书由 张长海 统稿。 陈娟分 别使 用 TurboC3.0和 MicrosoftVisualC++6.0调 试并通过了所有例题程序。 在本书 的编写 过程中 ,作者 参阅 并引 用了国 内外 诸多同 行的 文章、著 作,在 此向 他们 致 意,并恕 不一一 列举、标明 ;在本 书的 成书 和出 版 过程 中得 到 高等 教育 出 版 社 的 大力 支持 和 帮助 ,作 者在此 表示感 谢。 由于作 者学术 水平有 限,书 中难 免存 在错误 和不 足,敬请各 位读 者批 评指 正 ,作 者表 示 由衷的感谢。 作 者   2004年于 长春 目  录 第一章 基本知识 …………………………… (1)  1.1 程序设计语言 ………………………… (1)   1.1.1 机器语言 ………………………… (1)   1.1.2 汇编语言 ………………………… (2)   1.1.3 高级语言 ………………………… (2)   1.1.4 程序的执行 ……………………… (3)  1.2 C语言简况 …………………………… (3)  1.3 程序设计语言的形式描述 …………… (5)   1.3.1 语法、语义 ……………………… (5)   1.3.2 BNF……………………………… (5)   1.3.3 文法的其他表示法 ……………… (8)  1.4 C程序结构 ………………………… (10)  1.5 算法及其描述工具 PAD图 ………… (11)   1.5.1 算法 …………………………… (11)   1.5.2 PAD图 ………………………… (12)   1.5.3 PAD实例 ……………………… (16)  本章小结 …………………………………… (18)  习题一 ……………………………………… (18) 第二章 数据信息 …………………………… (23)  2.1 基本符号 …………………………… (23)   2.1.1 字符集 ………………………… (23)   2.1.2 标识符 ………………………… (24)   2.1.3 保留字 ………………………… (25)   2.1.4 分隔符 ………………………… (25)   2.1.5 运算符 ………………………… (25)   2.1.6 常量 …………………………… (26)   2.1.7 间隔符 ………………………… (29)   2.1.8 注释 …………………………… (29)  2.2 数据 ………………………………… (30)   2.2.1 浮点类型 ……………………… (31)   2.2.2 整数类型 ……………………… (32)   2.2.3 字符类型 ……………………… (33)   2.2.4 布尔类型 ……………………… (33)   2.2.5 枚举类型 ……………………… (33)  2.3 混合运算 …………………………… (35)  2.4 关系运算 …………………………… (36)  本章小结 …………………………………… (36)  习题二 ……………………………………… (37) 第三章 简单程序 …………………………… (39)  3.1 常量及常量定义 …………………… (39)  3.2 变量及变量声明 …………………… (39)   3.2.1 变量 …………………………… (39)   3.2.2 变量声明 ……………………… (40)   3.2.3 变量形态 ……………………… (41)   3.2.4 变量地址 ……………………… (42)   3.2.5 变量初始化 …………………… (42)  3.3 表达式 ……………………………… (43)   3.3.1 表达式的结构 ………………… (43)   3.3.2 表达式的计算 ………………… (45)  3.4 语句 ………………………………… (46)  3.5 表达式语句 ………………………… (47)  3.6 赋值 ………………………………… (47)  3.7 类型转换 …………………………… (51)  3.8 输入 /输出 …………………………… (54)   3.8.1 字符输入 ……………………… (54)   3.8.2 字符输出 ……………………… (55)   3.8.3 格式输入 ……………………… (55)   3.8.4 格式输出 ……………………… (56)  本章小结 …………………………………… (59)  习题三 ……………………………………… (59) 第四章 函数 ………………………………… (63)  4.1 带子程序的 C程序 ………………… (63)  4.2 函数 ………………………………… (66)   4.2.1 函数调用 ……………………… (66) · 2· 目  录   4.2.2 函数定义 ……………………… (67)   4.2.3 函数原型 ……………………… (71)  4.3 程序设计实例 ……………………… (72)  本章小结 …………………………………… (78)  习题四 ……………………………………… (78) 第五章 流程控制 …………………………… (80)  5.1 顺序结构 …………………………… (80)  5.2 分支程序设计 ……………………… (80)   5.2.1 逻辑值控制的分支程序设计 … (81)    5.2.2  算术 值 控 制 的 多 分支 程 序 设计 (85)  5.3 循环程序设计 ……………………… (88)   5.3.1 先判断条件的循环程序设计 … (89)   5.3.2 后判断条件的循环程序设计 … (91)   5.3.3 for语句 ………………………… (95)  5.4 程序设计实例 ……………………… (99)  本章小结 ………………………………… (110)  习题五 …………………………………… (111) 第六章 数组 ……………………………… (120)  6.1 结构型数据类型 …………………… (120)  6.2 数组类型 …………………………… (120)   6.2.1 数组声明 ……………………… (120)   6.2.2 下标表达式 ………………… (122)   6.2.3 应注意的问题 ……………… (122)  6.3 多维数组 …………………………… (123)  6.4  程 序 设计 实 例 ———数 组在 程 序 设 计 中的应用 (124)  6.5 数组初值 …………………………… (142)  6.6 字符数组 …………………………… (144)  6.7 类型定义 …………………………… (145)  本章小结 ………………………………… (147)  习题六 …………………………………… (147) 第七章 指针 ……………………………… (157)  7.1 基本概念 …………………………… (157)   7.1.1 指针类型和指针变量 ………… (158)   7.1.2 指针所指变量 ………………… (160)   7.1.3 空指针与无效指针 …………… (162)   7.1.4 通用指针 ……………………… (162)  7.2 指针运算 …………………………… (164)  7.3 指针与数组 ………………………… (166)   7.3.1 用指针标识数组 ……………… (167)   7.3.2 多维数组与指针 ……………… (171)   7.3.3 指针数组 ……………………… (176)   7.3.4 指针与数组总结 ……………… (180)  7.4 指针与字符串 ……………………… (181)  7.5 指向指针的指针 …………………… (185)  7.6 命令行参数 ………………………… (187)  本章小结 ………………………………… (189)  习题七 …………………………………… (189) 第八章 再论函数 ………………………… (192)  8.1 参数 ………………………………… (192)   8.1.1 C参数传递规则 ……………… (192)   8.1.2 指针作参数 …………………… (194)   8.1.3 数组作参数 …………………… (200)     8.1.4 其 他 程 序 设 计 语言 的 参 数 类别 (204)  8.2 返回指针的函数 …………………… (207)  8.3 作用域 ……………………………… (210)   8.3.1 作用域 ……………………… (210)   8.3.2 生存期 ………………………… (212)   8.3.3 局部量和全局量 …………… (213)  8.4 递归 ………………………………… (215)   8.4.1 递归程序 …………………… (215)   8.4.2 递归程序设计 ……………… (216)   8.4.3 间接递归 …………………… (221)   8.4.4 递归程序执行过程 ………… (227)  本章小结 ………………………………… (238)  习题八 …………………………………… (238) 第九章 程序开发和结构化程序设计 …… (246)  9.1 goto和标号 ………………………… (246)   9.1.1 带标号的语句 ……………… (246)   9.1.2 goto语句 ……………………… (246)  9.2 空语句 ……………………………… (247)  9.3 结构化程序设计原则 ……………… (248)  9.4 程序风格 …………………………… (249)   9.4.1 良好的行文格式 …………… (250) 目  录 ·3·    9.4.2  用合 适 的 助 记 名 来命 名 标 识符 (252)   9.4.3 注释 …………………………… (252)   9.4.4 对程序说明的建议 ………… (253)  9.5 程序的正确性 ……………………… (253)   9.5.1 错误种类 …………………… (253)   9.5.2 程序测试和验证 …………… (254)   9.5.3 测试方法 …………………… (255)  9.6 可移植性 …………………………… (255)  9.7 文档 ………………………………… (256)  9.8  自 顶 向下 逐 步 求 精的 程序 设计技术 (257)   9.8.1 自顶向下、逐步求精 ………… (257)   9.8.2 求精过程的表示 …………… (259)   9.8.3 求精实例 …………………… (260)  9.9  受 限 排列 组 合 ———穷 举法 与 试探法 (269)  本章小结 ………………………………… (281)  习题九 …………………………………… (281) 第十章 文件 ……………………………… (288)  10.1 文件概述 ………………………… (288)  10.2 文件操作 ………………………… (290)   10.2.1 打开、关闭文件 ……………… (291)   10.2.2 字符读 /写 …………………… (292)   10.2.3 字符串读 /写 ………………… (293)   10.2.4 数据块读 /写 ………………… (293)   10.2.5 格式化读 /写 ………………… (294)   10.2.6 文件定位 …………………… (294)  10.3 文件操作实例 …………………… (296)  本章小结 ………………………………… (301)  习题十 …………………………………… (301) 第十一章 结构体与共用体 ……………… (305)  11.1 结构体 …………………………… (305)   11.1.1 结构体类型 ………………… (305)   11.1.2 结构体类型名 ……………… (307)   11.1.3 结构体变量 ………………… (308)   11.1.4 指向结构体变量的指针 …… (309)   11.1.5 结构体变量的成分 ………… (309)  11.2 共用体 …………………………… (313)   11.2.1 带共用体的结构体实例 …… (313)   11.2.2 共用体类型 ………………… (314)   11.2.3 限制 ………………………… (318)   11.2.4 switch语句与共用体 ………… (318)  11.3 结构体与函数 …………………… (318)   11.3.1 返回结构体值的函数 ……… (319)   11.3.2 结构体作函数参数 ………… (320)  11.4 程序设计实例 …………………… (322)  本章小结 ………………………………… (327)  习题十一 ………………………………… (327) 第十二章 动态数据结构 ………………… (331)  12.1 管理动态变量 …………………… (332)  12.2 动态数据结构 …………………… (334)   12.2.1 栈(stack) …………………… (334)   12.2.2 队列(queue)………………… (336)   12.2.3 链表(linkagetable) ………… (337)   12.2.4 树(tree)……………………… (340)  12.3 程序设计实例 …………………… (346)  本章小结 ………………………………… (361)  习题十二 ………………………………… (361) 第 十 三章   三 论 函 数 ———几个 较 深 入 的 问题 (367)  13.1 函数指针 ………………………… (367)  13.2 函数作参数 ……………………… (369)  13.3 函数副作用 ……………………… (372)  13.4 形式参数作实在参数 …………… (373)  13.5 参数结合顺序 …………………… (374)  13.6 可变长度数组 …………………… (376)   13.6.1 可变长度数组 ……………… (376)   13.6.2 可变长度数组作参数 ……… (377)  本章小结 ………………………………… (378)  习题十三 ………………………………… (378) 第十四章 C语言独有的特性 …………… (383)  14.1 运算 ……………………………… (383)   14.1.1 sizeof………………………… (383)   14.1.2 赋值运算 …………………… (384)   14.1.3 顺序表达式 ………………… (384) · 4· 目  录   14.1.4 条件表达式 ………………… (384)   14.1.5 位运算 ……………………… (385)  14.2 位段 ……………………………… (387)  14.3 存储类别 ………………………… (388)   14.3.1 数据在内存中的存储 ……… (389)   14.3.2 自动存储类别 ……………… (389)   14.3.3 寄存器存储类别 …………… (390)   14.3.4 变量的静态存储类别 ……… (391)   14.3.5 变量的外部存储类别 ……… (393)   14.3.6 函数的存储类别 …………… (394)   14.3.7 类型定义符 ………………… (395)  14.4 const指针 ………………………… (395)    14.4.1  指 向 常 量的 指 针 (常 量 指 针 ) (396)   14.4.2 指针常量 …………………… (396)    14.4.3  指 向 常 量的 指 针 常 量 (常 量 指 针 常 量 ) (397)  14.5 有关指针的总结 ………………… (397)  14.6 语句 ……………………………… (399)   14.6.1 break ………………………… (399)   14.6.2 continue……………………… (400)   14.6.3 for的延伸 …………………… (401)  14.7 编译预处理 ……………………… (401)   14.7.1 宏定义 ……………………… (401)   14.7.2 文件包含 …………………… (405)   14.7.3 条件编译 …………………… (405)  本章小结 ………………………………… (408) 附录一 ACSII字符集 …………………… (409) 附录二 C语言语法 ……………………… (412) 附录三 标准库头文件表 ………………… (422) 附录四 实验指导书 ……………………… (423)  F4.1 使用 TurboC …………………… (423)   F4.1.1 启动 TurboC ……………… (423)   F4.1.2 选择工作目录 ……………… (423)   F4.1.3 建立工作环境 ……………… (425)   F4.1.4 编辑源文件 ………………… (426)   F4.1.5 编译、连接 …………………… (426)   F4.1.6 运行 ………………………… (427)  F4.2 visualc++ 集成开发环境 ……… (427)   F4.2.1 启动 VC++ ………………… (427)   F4.2.2 建立环境 …………………… (427)   F4.2.3 录入、编辑源程序 …………… (429)   F4.2.4 编译 ………………………… (429)   F4.2.5 连接 ………………………… (430)   F4.2.6 运行 ………………………… (430)  F4.3 实验 ……………………………… (431)   F4.3.1 实验一 C环境基本操作 … (431)   F4.3.2 实验二 模块化程序设计 … (432)   F4.3.3 实验三 程序的流程控制 … (432)     F4.3.4  实 验 四  数 组 的 概 念 和 应用 (433)     F4.3.5  实 验 五  指 针 及 其 在 程 序设 计 中 的应 用 (434)   F4.3.6 实验六 递归程序设计 …… (434)   F4.3.7 实验七 数据组织 ………… (434)   F4.3.8 实验八 文件及其应用 …… (435)  F4.4 课程设计 ………………………… (436) 参考文献 …………………………………… (441) 第一章 基 本 知 识 现代计算 机从出 现至今 不过 50多年 时间,但其 发展速度 是任何 一种新 技术都 不可比 拟 的,目前 ,计算 机已经 渗透到 各个领 域。本 书将以 C语 言为背 景向大 家揭示如 何 编 制计算 机 程序 ,即 如何使 用计算 机解 决 科 技 、生 产、事 务处 理 等方 面 的 问 题 ,介 绍 程 序设 计 的 基 本 方 法、技术 和技巧 。在具 体介绍程 序设计 之前,先简 略介绍 一下有 关计算 机程序 设 计 和程序 设 计语言的基本知识。 1.1  程序 设计 语言 一个庞 大的计 算机系 统是怎 样有 条 不 紊 地工 作 的 呢 ?答案 是:计算 机系 统 的工 作是 由 事先 设计好的 程序来 控制的 。人们 首先按 自己的 需要把 让计算 机做的 工作编 写 成 计算机 程 序,并把 程序送 入计算 机,然 后启动 计算机 执行程 序。计 算机的 控制器 从程序 的 第 一条指 令 开始 ,顺 序地逐 条取出 指令 进 行解 释,然 后 按指 令 的 规 定 和要 求 指挥 整 个 计算 机 系 统 的 工 作,从而 完成人 们设想 的要计算 机完成 的工作 。 程序是一 个指令 序列,也就 是用指 令序列 排成的 一个工 作顺 序 、工作步骤 。 人 们平常 也 使用 程序这 个名词 ,例 如运动 会程序 等。计算 机程 序是用 计 算机 指 令 为计算 机排定 的工 作 顺序 、工 作步骤 。 为计算机编写程序的过程称为程序设计。 描述程 序必须 使用一 种 语 言。 程 序设 计 语 言 是 指用 于 编 写、描 述 计 算 机程 序 的 语言。 一般 的,人们将 程序设 计语言分 成三类 :机器语言 、汇 编语言 和高级 语言。 1.1.1 机器 语言 机器语 言由能 被计算 机 直 接执 行 的机 器 指 令 组 成,每 条 机器 指 令 是 一 串二 进 制 代码。 用机器语言编出的程序是一串二进制代码序列。例如计算 Y = X+15, 若  X <Y X-15, 若  X≥ Y 用 Pentium 机 器语言 可编出如 下程序 片段(设 程序从 100号 单元开 始,X、Y分 别占 用 116、 118号单 元):    01010010001011000000001 · 2· 第一章 基 本 知 识 001111000001100000000001 0111110000000101 001011010001010100000000 1110101000000011 000001010001010100000000 101000110001100000000001 ⁝ ⁝ ⁝ ⁝ ⁝ ⁝ ⁝ 0000000000000000 0000000000000000 用机器 语言编 程序显 然十分 困难,编 出的程 序不 但容易 出错,调 试极 为困 难 ,而 且程 序 本身 也极不 好读。 基于上 述原因 ,人们 引进了 汇编语 言。 1.1.2 汇编 语言 汇编语言是符号化了的机器语言。 也就是 引进 一些 助记符 表示 机器 指令中 的操 作码、 地址 等。完 成 1.1.1节例子 中同样 计算的 Pentium 汇 编语言 程序片 段如下 :      OV  X,X CMP AX,Y JL S1 SUB AX,15 JMP S2 S1:ADD AX,15 S2:MOV Y,AX   ⁝ X DW ? Y DW ? 汇编语 言程序 显然比 机器语 言程 序 进了 一步,它 比较好 读、好懂,写 起来 也 显然 比二 进 制代码程序方便得多。其原因在于用符号助记符代替了单调的二进制代码。 虽然汇编 语言比 机器语 言前进 了一步 ,但 是它仍 然十分 烦琐,并且 仍然依 赖 于 具体的 计 算机 ,程 序不便 于移植 ,因 此人们 又进一 步引进了 高级语 言。 1.1.3 高级 语言 高级语 言程序 不依赖 于具体 的计 算 机,它以 较接 近于自 然语 言或专 业语 言 的方 式描 述 操作 。例如 ,使 用 C语言 完成 1.1.1节 例子中 同样的 计算,可用如 下语句 :   if( <Y) Y=X+15; 1.2  C语 言 简 况 ·3· else Y=X-15; 显然,这种 程序十 分好读,它 几乎 类 似于 英语 句 子。这 种程 序编 写起 来自 然 很轻 松,而 且还 具有通 用性,可以 在不同机 器上运 行,十分便 于移植 。 1.1.4 程序 的执行 尽管用汇 编语言 和高级 语言编 写程序 比用机 器语言 方便 得 多,但不幸 的是 ,计 算机只 认 识二 进制代 码 (机 器 语 言 ),既 不 懂 汇 编 语 言 的 符号 代 码,也不 懂高级 语言近 似自然语 言的语 句。为 了使计 算 机懂 得汇编 语言及 高级 语言 程 序,人们 设计 了 翻 译 器。 由翻 译器把 用 汇 编 语 言 或高 级 语 言 编写 的 程 序 (称 为 源程 序)翻译成 等价的 机 器 语言程 序(称为 目标 程 序)。 翻译 器也是一 个程序 ,人们称汇 编语言 的翻译 器为汇 编 程序 ,高 级语言 的翻译 器为编译 程序。 如图 1.1所示,用 汇编语言 或高级 语言编 写程序 解 题的 过程是 :首 先用汇 编 语 言 或高 级语 言编 写 出 程 序; 然后 把源程 序录入 计算 机;接 着启 动翻 译器,由 翻 译 器 将相应的汇编语言程序 或高级语言程序翻 译成机器语 言程 序;接着再 启动连 接程 序,由 连 接 程序 将 机 器 语 言 程序 连接生成 计算机 可执行 的程序 ;最 后再把 可执行 程 序送 入计算 机,并启动 计算 机 执行 ,完 成人 们 要 计 算 机 图 1.1 汇编语言、高级语言解题过程 做的工作。 1.2  C语言 简况 本书以 C语言 为背景讲 述程序 设计,因 此有 必要 简 单介 绍一 下 C语 言,包括它 出现 的 历史 背景、它的 主要特 性以及优 点和缺 点。 20世纪 70年 代初,C语 言 在美 国贝 尔实 验室 诞 生。 它 的 前 身 可以 追 朔到 ALGOL60、 CPL、BCPL和 B语言 。 1960年公 布的 ALGOL60语 言称为 算法语 言,是一种 面向算法 的高级 程序 设计 语 言,在 程序 设计语言 理论、编 译理论、形式 语言理 论方面 起到里 程碑 的 作用。 它的优 点 是 语法严 谨 且形 式化,并完 全脱离 具体计算 机硬 件;缺点 是 不适 于编 写计 算机 系统 程 序。1963年,英国 剑桥 大学设计了 CPL语言,与 ALGOL60相比 ,它更 接近硬 件,但是规 模较大。1967年,Martin · 4· 第一章 基 本 知 识 Richard对 CPL进 行了简化,推出了 BCPL,可以称其为基本 CPL语言。1970年,美 国贝尔 实验 室的 KenThompson又 对 BCPL做了进一步的简 化,设 计出简单且 很接近 计算机硬 件的 B语言 (取 BCPL的第 一个字母),并用 B语言 编写了 UNIX操作系统,但是 B语言又 过于简单 了。 1972年,贝 尔 实 验 室 的 DennisRitchie在 B语 言 的 基 础 上 设 计 并 实 现 了 C语 言 (取 BCPL的第二个 字母 ),C语言 既 保持 了 B语 言的 优 点 ,又 克 服 了 B语 言的 缺 点。1973年, KenThompson和 DennisRitchie用 C语言 改写了 UNIX,从此,C语 言和 UNIX 紧密地 联系 在 一起 。C语言 和 UNIX的 突出优 点引 起 计算 机界 的 广 泛 重 视,UNIX 日 益广 泛 地 被使 用,C 语言也得到迅速推广。 C语 言 的 标 准 化 工 作 从 1982年 开 始,当 时 美 国 国 家 标 准 协 会 (American National StandardsInstitute,ANSI)认 识到标 准化将 有助于 C语 言在商业 化编程 中的普 及,因此成 立了 以 Jim Brodie为 主席的 一个委 员 会 (X3J11),该 委员 会 工 作 的结 果是 制定 了 一 个 C 语言 标 准,并在 1989年被正 式采用 (即美国 国家标 准 X3.159-1989),称 这个标 准为“ANSIC”。 国际标 准化组 织(InternationalOrganizationforStandardization,ISO)考虑到 编 程 工作是 国 际化 的,C的标准化 工作应 该列为 ISO的 工作日 程,因 此成立 了一个 以 P.J.Plauger为组 长的 工作 小组:ISO/IECJTC1/SC22/WG14。 该小组只 做了少 量的编 辑性修 改,即把 ANSIC 变成 了国 际标准 :ISO/IEC9899∶1990,此 后 ISO/IEC标准 又被 ANSI采 用。 人们把 这个公 共标 准 称为 “标准 C语言 ”,简 称“C89”。 1995年,WG14小 组对 C89做了 两处技 术修改 和一个 补充,称这 个版本为 “C95”。 从 1995年开 始,WG14开始对 C进 行更大 的修订 ,最 终于 1999年 完成并获 得批准 ,新标 准的 标准号 为 ISO/IEC9899∶1999,称该新 标准为 “C99”。 我国于 1994年 12月 4日 公 布了“中华人 民 共和 国 国 家标准 GB/T15272-94程 序设 计 语言 C”,该标 准实际 上是 C89的 翻版。 C的优点 可以概 括为以下 几点: (1)语 言简洁 、紧 凑,使用方 便、灵 活; (2)C本 身是模 块式,便于集 体分工 合作开 发大型 程序; (3)运 算符丰 富; (4)数 据结构 丰富; (5)具 有结构 化控制 结构; (6)与 计算机 硬件联 系紧密 ,可以 直接访 问计算 机内存 ,具 有位操 作; (7)生 成目标 代码质 量高。 同时 C也有 许多缺 点: (1)语 法不严 格; (2)类 型机制 不严密 ,比如 字符类 型与整 数类型 没有区 别,不检查 下标超 界; (3)程 序设计 自由度 太大,不利于 保证程 序的正 确性; 1.3 程 序 设计 语 言 的 形 式 描 述 (4)若 程序与 计算机 硬件联 系太密 切,则 可移植 性不好 ; (5)有 些语言 成分太 复杂,比如运 算符; (6)语 言本身 不能保 证程序 设计的 结构化。 1.3 程 序设 计语 言的 形式 描述 ·5· 1.3.1 语法 、语义 一个程 序设计 语言是 一个记 号系统 ,如同 自然语 言一样 ,由 语法和 语义两 方面组 成。 1.语法 语法是一 组规则。它 描述程序 的结构形式 及构成规 律。使用 这组规 则可以产 生和形 成一 个合 法的程序,即语法 上正确的程 序,人 们上机时 看到的 编译能通 过的程 序。为了 表示方 便及 叙述 严谨,本书将采用 BNF(巴科斯 -脑 尔范式,简称巴科 斯范式)来 描述 C的语法。 2.语义 语义也 是一组 规则,它定义 一个程 序的意 义。例 如怎样 理解语 句   fori stepbuntil10do begin b+1; a[i] 0; end 的意 义。这 就是语 义要描 述的问 题。本 书将采用 自然语 言来描 述 C语 言的语义 。 1.3.2  BNF 为了描 述 C语言的 语法,下边 讲述有 关形式语 言方面 的一些 知识。 在自然 语言中 ,语 言是句 子的集 合,句子是 语言的 基本成 分。 在程序 设计 语言中 ,句 子 就是程序。 在自然 语言中 ,句 子由单 词构成 ,单 词又由 字母构 成。在 程序 设计语 言中,程序 由词 法 单位 构成,词法 单位又 是由字符 构成的 。 我们的目 的是给 出一种 表示方 法,用它精 确地表 示一个 语言,使得 凡是按 此 方 法表示 的 符号 串都是 相应语 言的句 子,反 之,任 意 给 一 个该 语 言 的 句 子都 能 用 相 应 的方 法 表示 出来。 巴科 斯范式 (BNF)表示法 是描述 语言的 强有力的 工具。 下面介 绍巴科 斯范式 表示法 。 文法 考虑英语句子: · 6· 第一章 基 本 知 识   The  big elephant ate  the peanut 由英语 知识,可以 把它图解 成 图 1.2所 示的 形式 。这 种 图解 式把 一 个 英 语 句子 分解 成 它的 各个组 成部分 ,并 以此来描 述一个 英语句 子的语 法。 图 1.2 一个英语句子的图解式 由图 1.2的图 解式可以 看出:一 个 英 语 句 子是 由 主语 后 跟 随谓 语,再 后 跟随 宾语 组成 的;主语是 由冠词后跟 随形容词,再后跟 随名词组成的;等等。可 以用符号“ ”表 示“由……组 成的 ”,并以书 写顺序表示 跟随,则可以把 上述“一个英 语句子”的构成规则写 成下述形式 :   G: <一个英语句子 > <主语 ><谓语 ><宾语 > <主 语 > <冠词 ><形容词 ><名词 > <冠 词 > the <形容词 > big <谓 语 > <动词 > <动 词 > ate <宾 语 > <冠词 ><名词 > <名 词 > peanut <名 词 > elephant 在上述 表示法 中,每一行称 为一条 规则式 (或 规 则);在每条 规则式 中,符号 “ ”表示 它 左端 的符号由 它右端 的符号 串组成 ,或 者说左 端的符 号产生 右端 的 符号 串。非 形式 地 ,人 们 称这 种描述 形式为 “文 法”,“G”是 给 该文 法起 的 名字 ,标在 第一 条规 则 式前 。 称规 则式 左 端的 符号为非 终极符 ;只出现在 文法规 则式右 端的符 号为终 极符;第一 条规则 式 的 左端符 号 为文 法的开 始符。 从文法 的开始 符出发 不断用右 端符 号串替 换 左端 符 号 ,直 至所有 符号 都 不能 再替换(全 部为终 极符)为 止,得到 的符号串 称该文 法的句 子。文 法的所 有 句 子构成 的 集合 称为该 文法定 义的语 言。 <一个英 语句子 >是文法 的开始 符,该文法 的句子 有:    hebigelephantatethepeanut Thebigpeanutatetheelephant Thebigpeanutatethepeanut Thebigelephantatetheelephant 1.3 程 序 设计 语 言 的 形 式 描 述 ·7· 这些句子 构成的 集合称 为 文 法 G 定 义 的 语言 。 当 然,有 些 句子 的意 义是 不 对 的 ,称 语 义上 是错误 的,但在形 式上或 语法上 都是正确 的。 上述描 述 一个 语 言 语法的 方法称 为巴 科 斯范 式(简称 BNF)表 示法,它是 由 J.W.Backus和 P.Naur于 1960年在 ALGOL60标准 报告 中首先提出来的。下边举几个具体文法的例子。 例 1.1 无符 号整数 集合的 文法如 下:   G(整数): D N ND D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 用符号“m ”表示“或 者”,则描述 同一左 端符号 的不同规 则式就 可以简 写。上 述整数 文 法可简写成:   G(整数): DmND D 0m1m2m3m4m5m6m7m8m9 例 1.2 下述 文法 G1定义 集合 {0n1nm n≥ 1}   G1:S 01m 0S1 递归定义 注意前 述两个 例子,其中有 规则:   N ND S 0S1 这种在 定义一 个符号 时又利 用该符 号本身(例 N,S)的 现 象称 为递归。 递归 是定义 无 穷集 合的一 种常用 手段。 在实际 应用中 ,递归 定义的 例子很 多。 例 1.3 标识 符可以 如下定 义:    <标识符 > <字母 >|<标识符 ><字母 >|<标识符 ><数字 > 表示:一个 字母是 标识符;一个 标识符 后边跟 一个字 母也是标识 符;一个标识 符后边 跟一 个数 字还是标识 符。即标 识符是由字 母开头的后 边跟任 意多个字母或 数字组成的字符串 。 例 1.4 标识 符表可 以如下 定义:    <标识符表 > <标识符 >m <标识符表 >, <标识符 > · 8· 第一章 基 本 知 识 表示:一个标识符是标识符表;一个标识符表后边跟一 个逗号和一个标识符还是标识符表。 例 1.5 整数 表达式 可以定 义成:    <表达式 > <运算分量 >m <运算分量 ><运算符 ><表达式 > <运算分量 > <整数 >m(<表达式 >) <运算符 > +m -m ×m / 表示:一个 运算分 量是表达 式;一个 运算分 量后 边跟 一个运 算符,再 跟一 个 表达 式还 是 表达 式。而 运算分 量是一 个整数 或一个 括号括起 来的表 达式;运算 符包括 :+、-、×、/。 1.3.3 文法 的其他表示 法 除使用 BNF表示 法表示文 法外,在 文献 中 还使 用其 他 一 些 方法 表 示 文 法,它 们 与 BNF 具有相同的表达能力。下边介绍几种其他的文法表示法。 1.花括 号 花括号 {}的定义 如下: {β }表示 ε或 β或 ββ或 βββ或 … 其中 ,ε表示空 符号串 ;β是任意 符号 串。即 花 括 号 “{”和 “}”括起 来的 部分 表 示这 部分 可 以重 复 0次或 任意有 限次。 这样    a}表示 ε或 a或 aa或 aaa或 … {,a}表示 ε或 ,a或,a,a或,a,a,a或 … 在标准 BNF表示 法中,为了表 示任 意长 度 的串,是 通 过递 归来 定 义 的,例 如 ,假 定有 非 形式化规则式   A a,a,a,…,a 用标准 BNF表示 为   A am A,a 将花括 号用于 BNF表示法 中,上式可 以表示 为   A a{,a} 这里就 没有递 归了。 可以将 花括号 看成循 环标 志,表示花 括号 括起 来的 部 分要 循环 出 现若 干次,直到 满足需 要为止。 用这种 表示法 ,整 数的文法 可以写 成    D{D } D 0m1m2m3m4m 5m6m7m8m9 标识符的文法可以写成    <标识符 > <字母 >{ <字母 >|<数字 > } 还可以 在花括 号的闭 括号后 标以 上 下角 标,下角 标表示 花括 号中的 符号 串 至少 重复 的 1.3 程 序 设计 语 言 的 形 式 描 述 ·9· 次数 ,上 角标表 示花括 号中的符 号串 至多重 复的 次数。 例如,FORTRAN 标识 符只有 前 6个 字符 有效,则其 文法可 以定义成 :    <标识符 > <字母 >{ <字母 >|<数字 > }50 2.方括 号 方括号 “[”和“]”用来 表示可 选择的符 号串,定义 如下:   [β ]表示 ε或 β 即方 括号“[”和“]”括 起来的 部分 表 示这 部分 可 以出 现一 次或 不 出 现。例 如,标 准 BNF规 则式   E Tm T + E 可表示为   E T[ + E ] 花括号“{”和“}”及方 括号“[”和“]”都 是元语 言 符 号,其地位 与 “ ”是 一 样的, 它们 在规则 式右端 出现,表示被 括起来 的符号 串的重 复,而不属 于符号 串的一 部分。 3.语法 图 语法图 以图的 方式表 示语法 ,例如 ,前 述标 识符 的 语 法可 以 用 语 法 图 表 示,如 图 1.3所 示。 整数的 语法图 如图 1.4所示。 语法图 的描述 方法不 是形 式 化的。 语法图 可 理 解成沿 箭 头方 向,从入口 到出口 ,能 在该图 中通过 的任意一 个字符 序列都 是该语 言的句 子。 图 1.3 标识符 图 1.4 整数 4.替代 符号 由于 C语言 本身使 用 BNF的元语 言符号: “ |”、“[”、“]”、“{”、“}” 本书 避免使 用这些 符号作 为元语 言符号 ,而是 采取如 下措施 : (1)以 全角黑 竖直符 号“x ”代替 “|”; (2)以 全角空 心方括 号“ ”、“ ”分别代替 方括号 “[”、“]”; (3)不 使用花 括号“{”、“}”。 又由于 印刷及 书写上 的原因 ,本书 以全角 右箭头 “→ ”替代 符号“ ”。 另外,为了 清晰和 避免混淆 ,本书把 所有非终 极符都 用全角 尖括号 “<”、“>”括起 来。 · 10· 第一章 基 本 知 识 如此,前述 表达式 的文法可 以写成 如下形 式:    <表达式 > → <运算分量 >m <运算分量 > <运算符 ><运算分量 > <运算分量 > → <整数 > x (<表达式 >) <运算符 > → +x -x ×x / 1.4  C程序 结构 作为上 一节 BNF的例子,现在 给出 C 程序的 语法定 义如下 :    <程序 > → <编译单元表 > <编译单元表 > → 编译单元 >x <编译单元 ><编译单元表 > <编译单元 > → <顶层声明表 > <顶层声明表 > → 顶层声明 >x <顶层声明表 > <顶层声明 > <顶层声明 > → 声明 >x <函数定义 > <函数定义 > → <函数定义说明符 ><复合语句 > 按上述 语法定 义,一个 C程序 由一个 或若干 个编译 单元组 成,每个编 译单元 是一个 源程 序文 件;一个编 译单元 由若干顶 层 声 明 组 成,每个 顶 层 声 明 是一 个 声 明 或 函数 定 义,其中 主 要为外部函数定义。 需要指 出的是 ,C语言 的语法 十分不 严密,它不 能保证 严格的 文法条 件:“凡 是 按此方 法 表示 的符号串 都是相 应语言 的句子 ,反 之,任意给 一个该 语言的 句子都 能用相 应 方 法表示 出 来”。 在 C的语法 中,只能满 足后 边 的 条 件:“任意 给一 个 该语 言的 句子 都能 用 相应 方法 表 示出 来”。尽管 如此,人 们还是用 BNF表示 C的语法 ,因 为除此 之外没 有一个 更 好 的形式 化 的表示方法。 图 1.5所 示为一 个完整 的 C程 序。该 程序由 两个编 译单 元组成 ,每 个编 译单元 保存 在 一个源程序文件中。 图 1.5 一个完整的 C程序 文件“hello.c”中定 义函数 hello,函数 hello打印字 符串 “Hello”,在该 文件中 还括入 一个 头文 件“stdio.h”,该头 文件中 包含所 有 I/O函数的 定义。 1.5 算法及其描述工具 PAD图 · 11· 文件 startup.c中定 义一个函 数 “main”,这是 必 须的 ,任 何 C程 序必 须包 含 且仅 包含 一 个以 main命名的 函数,该函数 称为 该 C程序 的主 函数,C程 序从 这个 函 数开 始执 行。在 该 程序 中,main调用 函数 hello,hello的 声明 不在本文 件中,而在 另一 个文件 hello.c中,因此 使 用一个外部声明来指明。 运行该程序的过程是: (1)使 用 C编译 程序分 别编译 两个 C源 程 序文件 “hello.c”和 “startup.c”,生成 两个 目 标代 码模块 文件“hello.obj”和 “startup.obj”; (2)使 用连接 程序进 行连接 ,把“hello.obj”和“startup.obj”以及需 要的库 函 数 连接到 一 起,生成 可执行 的机器 语言程序 “startup.exe”; (3)执 行“startup.exe”,得到的 运行结果 是在屏 幕上显 示字符 串 hello。 1.5 算 法及 其描 述工 具 PAD图 1.5.1  算 法 按照软件 工程学 的观点 ,软 件的生 产过程 大致经 过可行 性研 究 、需求分析 、概要 设 计、详 细设 计、实现、组装测 试、确认测 试、使用和 维护等 阶段。 程序设 计要解 决的是 详 细 设计阶 段 的问 题,对概要 设计中 的每个模 块进行 功能分 析和实 现。详 细设计 阶段一 般经过如 下步骤 : (1)建 立数学 模型———把实 际问题 转化为数 学问题 ; (2)找 出计算 方法———为数 学问题 的求解找 出方法 ; (3)进 行算法 分析———为实 现计算 方法给出 具体算 法。 经过详 细设计 阶段后 ,进入 实现阶 段,该阶段 包括: (4)选 择一种 程序设 计语言 ,编出 计算机 程序———写程 序; (5)调 试程序 ———保 证程序的 正确性 。 然后进 入测试 阶段,该阶段 包括: (6)上 计算机 运行,测试程 序的正 确性———组装 测试、确认 测试。 最后交付使用并维护。 我们的 任务是 找出算 法(算法分 析)、编出计 算 机 程序(实 现)、测试和 运 行 ,至 于建立 数 学模 型、找出计 算方法 则可能做 不出太 大贡献 。虽然 不可避 免地要 参与这 方面 的工 作 ,将 来 也要 学习“计算 方法”课 程,但是真 正解决 此类 问题 可 能还 要靠 数 学 家 。从算 法分析 阶段 开 始是 我们的 工作。 找出算 法,本 课程要 解决一 部 分 ,将 来还 要 学 习 “算 法 分 析”课 ,但 是由 于 现实 世界是十 分复杂 的,真正要 解决算 法问题 还要靠 长期的 积累 和 悟性。 选择 一种 语 言,要 根据 具体问题 来决定 ,20年前有人 统计 过,当时 世界 上 有两 千余 种 程序 设计 语 言,本 书 以 C · 12· 第一章 基 本 知 识 语言 为背景讲 授“程序 设计”。 千万不 要忽略 上计算 机调 试阶 段,因为一 方面 任何程 序不 经 过调 试都是不 能保证 其正确 性的,另一 方面程 序设计 是实践 性很强 的课程 ,计 算 机 专业更 是 实践性极强的学科。 算法是 一个计 算过程 ,具体 指明 应该 进行的 操作,描 述了解 决问 题的 方法 和 途径 ,它 是 程序设计的基础和精髓。一个有效的算法具有如下特点: 1.有穷 性 一般情况 下,一个 算法应该 在有限 的时间 内终止 ,不 应该是无 限的(当 然也 有例 外 ,比 如 操作 系统)。进 一步,有 穷性往往 指 在 合 理的 时间 范围 内,比 如 后 文 介 绍的 例 题 “Hanoi塔 ” 问题 ,若 1s进行 一次,大约 需要 5849亿 年。虽 然是有 穷的,但是显 然是无 意义的 。 2.确定 性 算法中 的每一 个步骤 都 应 该是 确 定的 ,含 义 是 惟 一的,不 应该 是 模 糊 的、模 棱 两 可的。 例如 求解一 元二次 方程,如果算 法直接 写成 x = -b± b2 2a -4ac 显然 是不确 定的,不能 构成确定 的算法 步骤。 例如当 a等 于 0时怎 么办 ?当 b2 -4ac小于 0 时怎么操作?都没有确定的描述。 3.有效 性 算法中的 每一个 步骤都 应该是 有效的 ,都 能够被 有效地 执行并 得到确 定的 结果,不应 该 存在 无效的操 作。例 如在一 个算法 中存在 操作“x→ x”,显 然 是无 意义的,也 是无 效 的;又 例如 操作“a÷0”也是 不能被 有效执 行的。 算法由 某些基 本的成 分组成 ,这 些基 本成 分 是一 些基 本 的操 作及 控 制结 构。构 成算 法 的基本操作包括: (1)表 达式以 及给变 量赋值 ; (2)读 (输入); (3)写 (输出)。 基本的控制结构包括: (1)顺 序控制 结构; (2)分 支控制 结构; (3)循 环控制 结构; (4)函 数调用 ; (5)函 数返回 。 1.5.2  PAD 图 为了描述 一个算 法,有多种 多样的 表示方 法,例如流 程图、NS图 、程 序,等 等。 本书采 用 1.5 算法及其描述工具 PAD图 · 13· 问题 分析图 (Problem AnalysisDiagram ,PAD)来 描述算 法。 PAD有许多 优点,它使 用二维 的树形 结构描述 程序 的 逻辑 ,比 直接 用程 序 (可以 说程 序 的表 现形式 是一维 的)表示算 法更清 晰直 观;PAD 使用了 结构 化的概 括的 抽象 的记号 系统, 它比 用流程 图表示 算法 更 简 练、紧 凑 、层 次 分 明;PAD是 开 放 的,它 比 封闭 式 的 NS图 更 清 晰、分明 ,也 更便于 修改。 下面具 体给出 PAD 图的 记号系 统 (为 简单 明了 起见,也为 了适 应 C语言 的一些 特点,本书将 标准 PAD的 记号系 统做了 一定的修 正 )。 1.基本 操作序 列用方框 括起来 基本操 作序列 表示成 图 1.6所 示的形 式。 2.顺序 控制结 构(顺序执 行的操 作) 用一条 竖线将 每个操 作成分 按执行 顺序连接 起来,表示 成如图 1.7所 示的形式 。       图 1.6 基本操作序列 图 1.7 顺序控制结构    3.分支 控制结 构 分支控 制结构 可以按 指定条 件有选 择地执行 操作,分成 如下两 种类型 : (1)由 逻辑条 件控制 的单分 支条件 选择。 由逻辑 条件控 制的分 支由如 图 1.8所 示的 流 程 图 表示 ,分 别表 示如 果条 件 成立 则执 行 某些 操作,否则 (条件不 成 立)什 么也 不 执行 或 执行 另 一 些操 作。在 PAD图中 分 别 用 如 图 1.9所示的 形式表 示。 图 1.8 由条件控制的分支结构的流程图 图 1.9 由条件控制的分支结构的 PAD图 (2)标 准的序 数值控 制的多 分支条 件选择。 图 1.10所示 的流程 图表示 的操作 ,在 PAD中用 图 1.11所 示的 形式表示 。 该 结构的 含 义是 :计 算表达 式的值 ;若表 达式值 等于某 个条件 的值,则执 行列在 该条件 后的 操作,然后 跳 过所 有其他 操作,本结 构执行结 束。 (3)适 应 C的序数 值控制 的多分 支条件 选择。 · 14· 第一章 基 本 知 识 图 1.10 多分支条件选择的流程图Ⅰ 图 1.11 多分支条件选择的 PAD图Ⅰ 图 1.12所示 的流程 图表示 的操作 ,在 PAD中用 图 1.13所 示的 形式表示 。 该 结构的 含 义是 :计 算表达 式 e的值 ;若 e等于某 个 c的 值,则 执行 列在 该表达 式后的 操 作 ,然 后顺序 执 行后 面的所 有操作 ,直 到结束。   图 1.12 多分支条件选择的流程图Ⅱ 图 1.13 多分支条件选择的 PAD图Ⅱ   单分支 及两分 支情况 的条件 为布 尔 表 达 式。上 分支 为真值 出口 的操 作,下 分支 为假 值 出口 的操作 。多分 支情况 的条件 为一 般 序 数 类型 表 达 式 ,这 时可 以在 各 分 支 的 出口 处标 上 本分支的具体执行条件。 4.重复 控制结 构 该控制 结构包 含一个 重复执 行的条 件(称 循环 控制 部分)和 一个要 重复 执 行的 操作 (称 循环 体)。重复 条件的 判断有 先判断 和后判 断之分 。 (1)先 判断重 复条件 的重复 控制结 构。 这种重 复控制 结构是 :先判 断 重 复条 件,以决 定 是否 执行 循 环 体 。在 这种 结 构中 ,循 环 体可 能一次 也不被 执行。 图 1.14所示 的流程 图表示 的操 作,在 PAD中用 图 1.15所 示的 形 式表示。 (2)后 判断重 复条件 的重复 控制结 构。 这种重 复控制 结构是 :后判 断 重 复条 件,以决 定 是否 执行 循 环 体 。在 这种 结 构中 ,循 环 1.5 算法及其描述工具 PAD图 · 15· 体至 少被执 行一次 。图 1.16所 示 的流 程 图 表 示 的操 作,在 PAD中用 图 1.17所 示 的 形 式 表示。 图 1.14 先判断条件的重复结构的流程图 图 1.15 先判断条件的重复结构的 PAD图  图 1.16 后判断条件的重复结构的流程图 图 1.17 后判断条件的重复结构的 PAD图 5.函数 定义、函数 入口和 函数出 口 函数定义 、函数入 口和函数 出口用 图 1.18所 示的形 式表 示 。在出 口处还 可 标 上返回 值 和各 形式参 数带回 的值,形式参 数表上 也可以 声明每 个形式 参数的 具体性 质。 图 1.18 函数定义、函数入口和函数出口 6.函数 调用 函数调 用采用 如图 1.19所 示的形 式表示,其含 义是:此处 执行一 段子算法 ,执行后 还返 回到 这里,继续 向下执 行。 7.GOTO 和标 号 GOTO语句 表示成 如图 1.20所 示的形 式。标 号 标 在 相应 图 的 线 上,例如 ,图 1.21所 示 的两 种形式 都表示 标号 L1标在操 作 2之 前。若有 转向语 句 gotoL1则转向 操作 2处向 下执 行。 在结构 化程序 设计原 则中,不主张 并限制 使用 GOTO语 句和标 号,本书也 不 使 用该控 制 结构 ,甚 至不介 绍 GOTO语句和 标号。 · 16· 第一章 基 本 知 识  图 1.19 函数调用 图 1.20 GPTP语句   图 1.21 标号  在程 序开发过 程中,复杂 一些的 程序不可 能一步 写出具 体、详细的 算法 PAD图,经常 要 采用 自顶向 下、逐步求 精的技术 。这 时一 些处 理 框可 以写 成 模糊 的函 数 调 用 或 模糊 的用 自 然语 言描述 的操作 及函数 ,然后 再引进 对这些 函数的 求精 PAD图。 1.5.3 PAD实 例 例 1.6 用 PAD图 表示图 1.22所示的 流程图 。 图 1.22 流程图 1.5 算法及其描述工具 PAD图 · 17· 解:首先,该流 程图可 以分成 五 段,包 括 三 个简 单 操 作 框 和两 个 循环 部 分,用 PAD表 示 如图 1.23所示 。 第二,条件“非 (i<=2)”的循环 体有两个简 单操作框 ,可以表 示成如 图 1.24所示的形式。 图 1.23 流程图的五个部分 图 1.24 第一个循环体 第三,条件 “非 (IMA== 9)”的 循 环体 可以 分 成 三 段,包括 两个 简 单操 作 框和 一个 条 件控 制的分 支部分 ,可 以表示成 如图 1.25所示的 形式。 第四,分支 框的“操 作”部分可 以分成 两段,包括 一个 循环 和一个 简单 操作框 ,可 以表 示 成如 图 1.26所示 的形式 。 图 1.25 第二循环体 图 1.26 分支框的操作 最后把 这几部 分合并 起来,得到如 图 1.27所 示的形 式。 图 1.27 最后结果 · 18· 第一章 基 本 知 识 本章小结 本章讲 述了计 算机、程序设 计以及 C语言的 基本 知识,包 括程序 设计 语言、高级 语言 程 序的 执行过 程、C简况、BNF、C程 序结构、算 法概念 和 PAD。重点 掌握 BNF和 PAD。 习 题 一 1.1  什 么叫 机 器 语 言、汇 编 语 言 、高 级 语 言 ?它 们 各 有 什 么特 点 ? 1.2  使 用高 级 语 言 上机 解题 的 过 程 是 什 么 ? 1.3  写 一文 法 ,使其 语 言 是 偶 整 数集 合 。 1.4 写一文法,使其语言是偶整数集合,但不允许有 0打头的偶整数。 1.5 写一文法,使其语言为第二位数字不是 5的整数。 1.6 写一文法,使其语言为字母表 {A,B}上回文字符串集合。回文字符串是指从左向右读和从右向 左读 都 一样 的 字 符 串 ,例 如 :ABABBBABBBABA、ABABBABA都 是 回 文 字 符 串。 1.7 写一文法,使其语言为所有匹配括号组成的字符串集合,即字符串中 每个左括号 “(”都有一 个相 应的 右 括号 “)”与 其 配 对 。 1.8 扩充习题 1.7,字母表上的括号还包括方括号“[”、“]”和花括号 “{”、“}”,并且 要求在字符 串中 各类括号互不交叉。 1.9 写一文法,使其语言为字母表{A,B}上所有 ww形式的字符串集合,其中 w为一个字符串。 1.10 写一文法,使其语言为字母表{A,B}上所有不是 ww形式的字符串集合,其中 w为一个字符串。 1.11 写一文法 G,使得 L(G)={anbm |n≥0,m≥0} 1.12 试写出下面文法所确定的语言 L(G):    (1)G: → AB A→ x|y B→ z|w (2)G: → xA A→ z|yA (3)G: → A|B A→ xA|y B→ xB|z (4)G: → C|xS C→ z|y (5)G: → SaS S→ b S→ d (6)G: → x|(B) B→ AC C→ {+A} (7)G: → D|D*A A→ x|(C) C→ +x|-x 习 题 一 · 19· 1.13  设 有 文 法    : → A A→ B|ifAthenAelseA B→ C|B+C|+C C→ D|C*D|*D D→ x|(A)|-D 判断下述符号串是否该文法的句子:    +x (x+x)* (+-x) (x* -+x) ifx+xthenx*xelse -x ifxthenif-xthenxelsex+xelsex* x if-xthenxelseifx+xelsex 1.14  设 有 文 法 :    函数定义 > → <函数定义说明符 ><复合语句 > <函数定义说明符 > → <声明说明符 > <声明符 > <声明说明符 > → <类型说明符 > <声明符 > → <直接声明符 > <直接声明符 > → 函数声明符 >x <简单声明符 > <函数声明符 > → 直接声明符 > (<参数类型列表 >) x <直接声明符 > ( <标识符列表 > ) <简单声明符 > → <标识符 > <参数类型列表 > → <参数列表 > <参数 表 > → <参数声明 >x <参数列表 >,<参数声明 > <参数声明 > → 声明说明符 ><声明符 >x <声明说明符 > <复合语句 > → { <声明或语句列表 > } <声明 > → <声明说明符 ><初始化声明符列表 > ; <初始化声明符列表 > → 初始化声明符 > x <初始化声明符列表 >,<初始化声明符 > <初始化声明符 > → <声明符 > <声明或语句列表 > → <声明或语句 > x <声明或语句 ><声明或语句列表 > <类型说明符 > → intx float <标识符 > → id <声明或语句 > → <声明 > x S; (1)如果 <函数定义 >是文法开始符,下述符号串 是否该文法 推导出的符 号串?如果 是,给 出推导 过 程;如 果 不 是 ,说 明 为 什 么。    n id(intid,floatid) {S; intid(int,float); S; · 20· 第一章 基 本 知 识 } ② id(floatid,floatid){S;S;S;} ③ floatid(){} ④ id(){} (2)如果 <声明 >是文法开始符,下述符号串是否该文法推导出的符号串?如果是,给出推导过程;如 果不 是 ,给 出 为什 么 。    floatid(floatid,floatid); ② intid(int,float); ③ id(intid,floatid); ④ id(float,float); ⑤ intid(); ⑥ int(); 1.15 判断符号串 (i*i+i)*i是否下述文法的句子,并写出从文法开始符 E推导出该句子的推导过 程。 把 文法 用 方 括 号 、花 括 号 改 写成 不含 直 接 递 归的 形 式 。    : → E + Tm T T→ T* Pm P P→ im (E) 1.16  把 下 述 文 法改 成 不 含 递归的 形 式 。    : → VarI:T I→ im I,i T→ t 1.17 用标准 BNF改写下述文法,使之不含花括号和方括号。    : → [D[;V]m V] D→ r{;r} V→ casevofb{;b} 1.18 有两个瓶子 A和 B,分别盛装有酱油和醋,使用 PAD设计一个把它们盛装物互换的算法。 1.19 有三个数 A、B、C,使用 PAD设计算法,求三个数中最大的数并输出。 1.20 使用 PAD设计算法,当输入数字 0、1、… 、9时,显 示相应英文 数字。例如当输入 1时输出 one, 当输入 5时输出 five,等等。 1.21 使用 PAD设计算法,求 N个整数的平均值。 1.22 使用 PAD设计算法,求正整数 M、N的最大公约数。 1.23 设 x1 和 x2 分别是 n维空间的两个点,使用 PAD设计求 x1 和 x2 距离的算法。 1.24 矩阵 Amn的元素全部为整数,使用 PAD设计算法调整 A,使所有正数全部放在 A的左侧,所有负 数全部放在 A的右侧。 1.25 使用 PAD设计算法,把 1、2、… 、10摆成一个环,使任意相邻两数的和为素数。 1.26 用 PAD图表示下述流程图。 习 题 一 · 21·    · 22· 第一章 基 本 知 识 第二章 数 据 信 息 2.1  基 本 符 号 一个 C程序 由若干 词法单位 (相 当于 单 词,下文 也称 词 法 单 位为 单词 )构 成 ,每 个词 法 单位 由一些 基本字 符构成 。本节 将介绍 构成 C程序的 各种词 法单位 (包括:保留 字、运 算符、 分隔 符、标识符 和常量 )以及构成 词法单 位的基 本字符 集。 2.1.1 字符 集 任何计 算机系 统都使 用一个 可被 本 系统 识别 的 字符 集,该字 符集包 括了 人 们常 用的 字 母、数字 以及诸 如句号 、逗 号、括 号 之 类 的 特殊 字 符。 字符集 被用 于人与 计算 机 之间 以及 计 算机 与计算 机之间 、计 算机与外 部设备 之间进 行信息 交换。 国际上 较通用 的字 符 集 是 “美 国 标 准 信 息交 换 代 码”(TheAmericanStandardCodefor InformationInterchange,ASCII)字符集 。该字 符集是 ISO建 议 的标 准 字 符集 的 美 国 版。它 已 被计 算机行 业广泛 接受为 标准。 许多型 号的计算 机、终端都 使用 ASCII字 符集或其 子集。 每个字符有如下两种形态: (1)可 视形态 :表 现为刻在 键盘上 、印在纸上 、显 示在屏 幕上的 字符符 号; (2)存 储形态 :表 现为存储 在计算 机中的 二进制 代码。 一个字符 在计算 机内是 以二进 制代码 形式(存 储形态 )保 存的,只 有在显示 、打 印时才 表 现为 可视形 态。ASCII字 符集及每 个字符 的代码 见附录 一。 在 C程序中 ,为 构成单 词而使 用的字 符限于 ASCII字 符 集 的 一个 子 集 。该 子 集包 括 52 个大 小写拉 丁字母 、10个 数字以 及 29个特殊 字符。 <字母 > → x Bx Cx D x Ex Fx Gx H x Ix Jx K x Lx M x Nx Ox Px Q x R x Sx Tx U x Vx W x X x Yx Z x ax bx cx dx ex fx gx hx ix jx kx lx m x nx ox px qx rx sx tx ux vx wx xx yx z <数字 > → 0x 1x 2x 3x 4x 5x 6x 7x 8x 9 <特殊 字符 > → x #x % x ^x & x * x (x _x )x - x + x = x ~ x [x ]x ’x |x \x ;x : · 24· 第二章 数 据 信 息 x ”x {x }x ,x .x < x > x /x ? 除字符集 上的符 号以外 ,其他符 号都不允 许在 C程 序中 出 现。例 如数学 中 常 用的一 些 符号 :α、β、∑、δ、Φ、π、Γ、ε、Ψ 等是 绝对不 允许在 C程 序中使 用的。 某些字 符看起 来很近 似。例 如,数 字 2与 字母 Z十 分相 像 ;字母 “O”与数 字“0”几乎 无 法区 别。为 了避免 混淆,在书写 程 序 时 应 强制 地 将 类 似的 字 符 区 别 开来 。空 格 字符 是没 有 任何 印刷符号 的空白 ,书写时,一 般都 空开 一块 位 置表 示空 格。本 书有 时为 了 表示 空格 数 目,或为 了强调 一个空 格的存在 ,必要时 将用“□”表 示空格。 2.1.2 标识 符 在程序 中出现 的任何 对象,例如:类 型、变量、函 数等必 须有 个名 字 。标识 符(identifier) 就是 用来表 示这些 对 象 名 字 的 符号 。 C标 识 符 的构 成 规 则是:以 字 母 开头 的 字 母、数 字 序 列,并且 在 C中,下划线 被作为字 母看待 。其语 法是:    <标识符 > → <非数字字符 > x <标识符 ><非数字字符 > x <标识符 ><数字 > <非数字字符 > → 字母 > x_ C99限定标 识符最 长可以 达到 63个 字符。 下述符 号串是合 法的标 识符:   r               lterHeatSetting ex3page29 time Inquire_Work_station_Transformation r1 readinteger Inquire_Work_station_identification eps WG4 the_total 下述符号串不是标识符:   2forthemoney       不是字母开头) caseonhand (内 部有空 格 ) over&under (包 含非字 母 、数 字符 号 ) C标识符 分成两 大类:标准标 识符和 用户自 定义标 识符。 标准标识 符是标 准库中 使用的 标识符 ,这 些标识 符的含 义已经 由 C语言预 先定 义 好了, 也称预定义标识符。不允许程序员在程序中使用它们。 用户自 定义标 识符是 用户依 据需 要 给自 己使 用 的 类 型、变量、函 数等 对象 起 的名 字,它 类似于数学中的符号名。 除标准 标识符 外,一个标识 符在 未声 明它 之 前没 有固 定 的意 义。具 体用 它 表记 什么 对 象要 由程序员 在程序 中加以 声明,C语言 不允许 出 现无 定义 的标识 符,因此对 程 序 中出现 的 一切 标识符 都必须 给予声 明,指 出该标 识符的 具体意 义。 2.1 基 本 符 号 · 25· 在使用自定义标识符时需要注意以下几点: (1)不 能与保 留字重 名; (2)不 能与标 准标识 符重名 ,程 序员 要注 意 不 允 许用 标 准 库 中的 标 识 符 为 自己 的对 象 命名; (3)任 何标识 符都必 须声明 且必须 先声明后 使用; (4)不 允许重 复声明 ,在同 一使用 范围内 ,任 何标识 符都不 能声明 两次或 两次以 上; (5)拼 写完全 一致的 两个标 识符是 相同的; (6)字 母是区 分大小 写的,所以 identifier与 Identifier是完 全不同 的两个 标识符 ; (7)为 了使程 序清晰 、易读 ,应尽量 使每个标 识符的 拼写与 它所代 表对象 的含义 相符。 2.1.3 保留 字 C语言包 括 37个保留 字。这 些保留 字与标 识符 具 有同样 的结 构,但是 它 们的拼 写是 固 定的 ,并 且具有 特殊、独立 的含义 及作用 ,不能 把保留 字作为 普通标 识符使 用。 <保留 字 > → utox boolx breakx casex charx _Complexx const x continuex defaultx restrictx dox double x elsex enum x externx floatx forx gotox if x _Imaginaryx inlinex intx longx registerx return x shortx signedx sizeofx staticx structx switch x typedefx unionx unsignedx voidx volatilex while 2.1.4 分隔 符 分隔符是 由一个 或两个 字符组 成的特 殊符号 。由两 个字符 组成的 分隔符 中 间 不允许 夹 有任 何其他 符号(包括 空格)。    <分隔符 > → x (x )x [x ]x ’x |x ;x : x ”x {x }x ,x \ 2.1.5 运算 符 运算符也 是由一 个或两 个字符 组成的 特殊符 号。由 两个字 符组成 的运算 符 中 间仍然 不 允许夹有任何其他符号。 <运算 符 > → x % x ^x & x * x - x + x = x ~ x |x .x < x > x /x ? x += x -= x *= x /= x % = x <<= x >>= x &= x ^=x |= x -> x ++ x -- x << x >> x <= x >= x ==|!= x && x || 保留字、分 隔符和 运算符都 是一些 有特 殊意 义 的记 号,在 C程序 中起 关键字 的作 用,定 · 26· 第二章 数 据 信 息 义程序各部分及整个程序的含义。例如: (1)“/”是除 法,“7/2”表 示 7整 数除以 2,结果 为 3;7.0/2表 示 浮点 数 7.0浮 点除 以 2,结 果为 3.5; (2)“% ”是 整数求 余数,“7% 2”表示求 7整除 2后的 余数,结果 为 1; (3)for表示循 环语句的 开始; (4)int是整数 类型说明 符; (5)“{”、“}”分别 表示复合 语句的 开始和 结束,等等 。 每个保留字、分隔符和运算符都有固定的含义,其具体 意义在以后涉及到的地方将逐步介绍。 2.1.6  常 量 常量是 C程序处 理的数据 。作为词法 单位存在 的 C常量是 指字面 常量,符号 常量在 下一 章介 绍。C常量包 括整数类型、浮点 类型、字符类型和 字符串类型,每种常量都 具有值和类 型。 <常量 > → 整数类 型常量 > x <浮点类 型常量 > x <字 符类型常 量 > x <字符 串类型 常量 > 1.整数 类型常 量 整数类型 常量是 一个数 字序列 。可以 用十进 制、八进制 和十六 进制表 示,还 可 以用一 个 后缀表示它的存储长度。 (1)如 果整数 类型常 量以 0x或 0X开头 ,则 是十六 进制表 示,用 字 母 a到 f(或 A到 F) 表示 10到 15; (2)如 果整数 类型常 量以 0开头则 是八进制 表示; (3)否 则是十 进制表 示。 下面不 考虑存 储长度 ,并且 只考虑 十进制 。十进 制表示 的整数 类型常 量的语法 是:    <十进制整数类型常量 > → <非负数字 > x <十进制整数类型常量 ><数字 > <非负数字 > → 1x 2x 3x 4x 5x 6x 7x 8x 9 <数字 > → 0x <非负数字 > 按照语 法规则 ,十 进制整数 类型 常量 是一 个 非零 开头 的 数字 序列。 其值 是 相应 整数 在 十进 制计数法 中表示 的整数 值。在 整数中 不允许 有逗号 及其 他 非数 字字符出 现,例 如 ,数 学 上一 个数可 以加隔 点“,”写成 17,409,而 在 C语言中 必须写 成 17409。 其他进制表示的整数类型常量请查阅附录二有关语法。 2.浮点 类型常 量 浮点类型 常量可 以写成 带小数 点的、指数 的或同 时包含 两者的 十进制 数,还 允 许使用 后 缀指定数据表示的长度。不带后缀的浮点类型常量的语法是:    < 浮点类型常量 > → 数字序列 ><指数部分 > 2.1 基 本 符 号 · 27· x <带点数据 > x <带点数据 ><指数部分 > <带点数据 > → 数字序列 >. x .<数字序列 > x <数字序列 >. <数字序列 > <指数部分 > → 指数标志 ><数字序列 > x <指数标志 >+<数字序列 > x <指数标志 >-<数字序列 > <指数标志 > → ex E <数字序列 > → 数字 > x <数字序列 ><数字 > 按上述语 法,浮点类型常量 的一种形式是所谓 定点表示法 ,其结 构是下述三 种形式 之一: (1)数 字序列 ,后 边跟一个 小数点 ; (2)数 字序列 ,前 边加一个 小数点 ; (3)数 字序列 ,后 边跟一个 小数点 ,再跟一个 数字序 列。 其值 就是字 面意义 下它在 十进制 表示法 中的 一个浮 点数 值。按 照 语 法 规 则,下 列是 合法 的 定点表示法的浮点数:   12.34   0.25  71.0  7777.   .8888 下列是非法的定点表示法的浮点数:   5,204.65      (不 应有逗 号) 浮点类 型常量 的另一 种形式 是所谓 浮点表 示法(或称指 数表 示法、科 学表示 法)。在 浮 点表 示法中 ,一 个浮点 类型 常 量被 表 示 成 某 一个 称 做 尾数 的 基础 值 乘 以 10的 某 一 整 数 次 幂。尾数可以 是一个 十进制 整数或 定点表 示形式 的浮点 类型 常 量,放在最 前边 ,后 边紧跟 着 字符 E或 e和称 做指数 的一个整 数。指 数给出 适当的 10的幂次。 例:    4.789E4    表示 34.789乘 以 10的 4次方 .29e-5 表示 0.29乘以 10的负 5次方 534E+5 表示 534乘以 10的 5次方 下面是合法的浮点表示的浮点类型常量:   218E1  4.7E-3   0.0E0  755.E4   .777E-3 下面是非法的浮点表示的浮点数:    5 (E前无数 字序列 ) 234E (E后无指 数部分 ) 不论是浮 点表示 的浮点 类型常 量,还是定 点表示 的浮点 类型常 量,它们在 计 算 机内部 的 表示 形式都 是一样 的。C99中 还引 进 了 十 六进 制表 示形 式 的 浮 点类 型常 量,还 可以 用一 个 后缀表示它的存储长度。 3.字符 类型常 量 字符类 型常量 是由单 引号(左撇 )括起来 的一串 字符。例 如: · 28· 第二章 数 据 信 息    T  r  Thisisacharacter 都是字符型常量。这里单引号 作为字符型常量的括号使用。 4.字符 串类型 常量 字符串类型常量是由双引号括起来的一串字符。例如:   "Thisisastring"   ""   "Totalexpenditures:" 都是 字符串 型常量 。这里 双引号 "作 为字符 串的串 括号使 用。 5.字符 转义符 可以在字 符型和 字符串 型常量 中使用 字符转 义符表 示源程 序中无 法或很 难 直 接输入 的 字符 ,例 如回车 符、空白符 等。转 义符的 结构是 反斜 杠后紧 跟一 个字 符 或整 数,在 C语言 中 反斜杠作为转义符的专用符号使用。下述都是字符转义符。    \r  \0  \377  \\  Thi\sastring 反斜杠 后跟一 个八进 制或十 六进制 整数,看做 ASCII码值 为相应 整数的 字符。 例如    52    看 做字符 * \101 看 做字符 A \141 看 做字符 a \x41 看 做字符 A \x61 看 做字符 a \0 看 做字符 null,该 字符在 C程 序中作 为字符串 结束符 ,使用十分 频繁。 反斜杠 后跟一 个字符 ,代表 的符号 含义如 表 2.1所示,除了 该表上 所列字 符 以 及表示 十 六进 制整数 的“x”有 特殊意 义以外 ,其 他字符 放在反 斜杠后,仍然 表示它 本身。 转义符 \a \b \t \n \v \f \r \" \ \? \\ 表 2.1 字符转义符 ASCII码 值 (十 进制 ) 7 警 报 ,如铃 声 8 退格符 9 水平制表符 10 换行符 11 垂直制表符 12 换页符 13 回车符 34 双引号 39 单引号 63 问号 92 反斜杠 含义 2.1 基 本 符 号 · 29· 2.1.7 间隔 符 在 C程序中 ,若 有些词 法单位 相邻,可能 发生 混淆 。例 如 :保 留 字 int和 标识 符 i相邻, 若写成   inti 则会 被理解 成一个 标识符 ,而不 是一个 保 留 字 int后 跟 一 个标 识 符 i。这 种混 淆经常 发生 在 两个 相邻的 标识符 、保 留字、常量 之间。 为了 分隔 这 些 易 混淆 的词 法单 位 ,C语 言引 进了 间 隔符 ,并 且规定 任何由 标识符、保 留字、字 面常 量 组成 的两 个 相邻 词法 单 位 之 间 至少 应有 一 个间 隔符。 C语言的 间隔符包 括空格 、行 结束符 、水 平制表 符、垂 直制表 符、换页符 等。 间隔符除 了分隔 两个相 邻的词 法单位 外,在程序 中没有 任何实 际意义 ,它 们 在 编译时 被 忽略 (除非在字 符型常 量中、字符 串型 常量 中、#include文 件名 中)。 在程 序中,每个 编译 单 元正文的第一个词法单位之前或在任何两个相邻的 词法单位之 间可以加任意多 个间 隔符, 这些 间隔符起 的作用 是一样 的,并且可 以互相 连用、混用 ,多 个间隔 符一起 连用 相当 于 一个。 在任何词法单位之内不允许含有任何间隔符。例如 317写 成 317 <= 写成 <  = 都是错误的。 2.1.8  注 释 C注释有 两种形 式。 (1)一 种形式 是由“/*”和“*/”,括起来 的任意 一串字 符; (2)另 一种形 式是由 两个正 斜线“//”开 始,直到该 对斜线 所在文 本行结束 。 注释是 给程序 加注解 用的,对程 序的 实际意 义没 有任何 影响,只 增加 程序 的 可读 性,编 译程序把注释作为空白符处理。在程序中适当加注释是一个好习惯。 可以利用 间隔符 和注释 适当组 织程序 的印刷 格式,使得 程序 更 加清 晰、易 读。 除了极 个 别的 几个例外 ,标准 C没对 程序的 书写格 式做任何 规定。 这就是 说,程序的书 写 格 式是自 由 的。例如下述程序:   #include <stdio.h> inti;                 /声明整型变量 i void ain(){ //主 函 数 i= 25+38; /* 求和运算 */ printf("25+38=% d",i); /* 打印 */ } 完全可以写成: · 30· 第二章 数 据 信 息   #include <stdio.h> inti;main() {  i= 25+38; printf("25+38=% d",i); } 但是,这不 是一个 好的习惯 。读者 在书写 程序以 及向计 算机录 入程序 时,应 尽 量使程 序 看起 来结构 清晰,层次 分明。 2.2 数    据 数据是 计算机 处理的 对象。 不同数 据是有 区别 的,因此程 序设 计语 言应 该 具有 区分 各 种类 型数据 的能力 ,于 是引进了 类型的 概念。 类型是 数据的 一种属 性。 在程序设 计语言 中,一个数 据类型 包含两 层含义 :第 一,它确定 一个值 集合(属 于该类 型 的数 据能够 取值的 范围);第二,它还 确定一 个运算 集合(能够 施于该 类型数 据上的 运算)。 一般来讲 ,在程序 设计语言 中,每个值 (即每个 数据)都属 于一种 数据类 型;每个常 量、变 量、函数 、表 达式都 有一种 数据类 型。 为了处 理各种 各样的 数据及 表示各 种数 据结 构 ,C语 言向 用户 提供 了十 分 丰富 的数 据 类型 和数据 结构,并且 还为用户 提供 一 些 定 义新 数据 类 型的 手段 。从 而 可 以 使 用户 面对 并 使用 极为丰 富的数 据结构 。C语言关 于数据 类型的 分类如 下:    <数据类型 > → 标量类型 > x <组合类型 > x <联合体类型 > x <函数类型 > x <void类型 > <标量类型 > → <算术类型 > x <指针类型 > <算术类型 > → <浮点类型 > x <整数类型 > <整数类型 > → <整型 > x <字符型 > x <布尔型 > x <枚举型 > <组合类型 > → <数组类型 > x <结构体类型 > 由上述 语法可 知,C语言 数 据 类型 分 为标 量 类 型、组合 类 型 、联合 体 类 型、函 数 类 型 和 void类 型。其 中标量 类型又分 为算 术类型 和指 针类 型;算 术类 型又 分为 浮点 类 型和 整数 类 型;整数 类型包 括整型 、字符 型、布尔型 和枚举 型;组合类 型包 括 数组 类型和结 构体 类型。 具 体分 类如图 2.1所示 。 2.2  数   据 · 31· 图 2.1 C数据类型 2.2.1 浮点 类型 在 C语言中 ,浮 点类型 包括各 种长度 的浮点类 型(float、double、longdouble)、各种长 度 的复 数类型(float_complex、double_complex、longdouble_complex)和 各种长 度 的 虚数类 型 (float_imaginary、double_imaginary、longdouble_imaginary)。这里 仅介绍 float型。上 一节 已经 介绍过浮 点类型 常量。 在 float型 中,有两个 重要特 性往往 容易忽 视,因 此要提 请读者 注 意,其一 是值域 ,其 二是精 度。 抽象地 讲,float型的 值域是 全体实 数,但是 由于 计算 机 表 示 方面 的原 因,其 实际 的值 域 只能 是实数 的一个 子集,并且不 同种 类 计 算 机上 所能 表 示的 最大 实 数也 不同 。 读者 在应 用 C语言 解题时 要注意 了解所用 的计算 机上实 数的表 示范围 。 仍是由于 计算机 表示方 面的原 因,实数在 计算机 内部的 表示有 时是不 精确 的,例如圆 周 率 π是 无穷小数 ,而计算 机内只能 表示有 限 位 小数,所 以就 只 能表 示成 近 似 的 。一般 的,任 何一 种计算 机表示 的实数 都在某 一精度 之内,超过这 个精度 的部分 是近似 的。 好在 C语言 有 double和 longdouble类型 ,读 者在应 用 C语言解 题时可以 根 据 所用的 计 算机系统以及被求解问题的要求选择浮点类型。 封闭于 浮点类 型的运 算(运算分 量和结 果都是 浮点类 型)包括:    +(加 法)   -(减法 )  * (乘法)    /(除 法) 它们 的意义 就是通 常数学 意义 下 的 加 、减 、乘 、除。 另 外,如同 在数 学中 一 样,+、- 还可 以 用做单目运算符使用。例如: +5.5  就是   0.0+5.5  得     5.5 -5.5  就是   0.0-5.5  得   负的 5.5 · 32· 第二章 数 据 信 息 为了照 顾人们 的习惯 ,C语言 在浮点 型意义 下 允许 浮点 型与整 型进行 混 合 运算,最后 结 果定为浮点型。 在浮点 类型运 算中,要特别 注意 由于 计算 机 表示 方面 的 原因 引起 的 精度 损失。 请读 者 注意以下几点: (1)两 个几乎 相等的 值相减 ,或两 个绝对 值几乎 相等而 符号相 反的值 相加 ,会 引起很 大 的误 差。在 这种情 况下,高位的 数字互 相抵消 了,从而丢 失了一 些或全 部有效 数字。 (2)除 法操作 时,也有一种 潜在 的 危 险。 若 除数 的绝 对 值很 小,结果 很可 能 引起 溢出。 因此 应避免 用 0或接 近于 0的值做 除数。 (3)避 免做两 个浮点 数相等 或 不相 等的 比 较。 由 于 计算 机 表 示 方面 的近 似 性,会使 得 本来 应该相 等的两 个值不 等。建 议读者 在比较 X、Y两个浮点 值是 否相等 时使用 |X-Y|<ε 进行 。其中 ε是一 个绝对 值较小 的正浮 点数。 2.2.2 整数 类型 在 C语言中 ,整 数类型 包括各 种长度 的带符号 与不带 符号的 整数类 型(short、int、long、 longlong、unsigen、signed等以及 它们的 组合 )。int类型 是 最 基 本的 整数 类型,除了 特殊 需 要外 ,本 书仅使 用 int型。 整数类 型的常 量上一节 也已经 介绍过 。 抽象地 讲,整数类 型的值域 是全 体整 数,但 是由 于计 算机表 示方 面的 原因,其实 际的 值 域只 能是整 数的一 个子集 。每种 计算 机 能 表 示的 整 数 都 在 某一 范 围 之 内 ,不 同 计算 机的 范 围也 不同。 读者在 编写 C语言程 序时,可 以 根据 需要 和 使用 的计 算机系 统选 择使用 不同 的 整数 类型,一定 注意不 要超界,尤 其是经 过运算之 后,超界往 往是容 易被忽 略的。 封闭于 整数类 型的运 算(运算分 量和结 果都是 整数类 型)包括:    +(加 法)   -(减法 )  * (乘法)    /(除 法)   %(取模 ) 它们的意义如下: (1) + 、- 、* :就 是通常数 学意义 下的加 、减 、乘 。在此 不再赘 述。另外 ,如同在 数学 中一 样,+ 、- 还 可以用 做单目运 算符使 用。例 如:    +5  等价于   0+5 得 5;   -5  等价于   0-5   得负 5 (2) /:除法 。如果 两个操作 数都是 整数类 型,则 “/”操作即 整数除 法,只 求 其商而 舍 去余 数,这里只 是简单 地丢掉余 数,不进 行四舍五 入。显 然,以零作 为除数 是错误的 。例如 :   5/3得 1;         -7 /2得 -3   3/5得 0;         -7 /-2得 3 (3)% :取余数。i% j的结果是 i/j后得到的余数,即 i% j等价于 i-(i/j)* j。因此   7% 3得 1;        7% -3得 1   3% 7得 3;       -7% -3得 -1    -7% 3得 -1;       7% 0是 错误的 2.2  数   据 · 33· 显然,对任 意整数 i及 j≠0,永远有 :i=(i/j)*j+i% j。 2.2.3 字符 类型 字符类 型常量 就是单 个字符 ,它的 值域是 2.1.1节 中所述 的一个 由实现 定义的 字符集 。 在 C语言中 把字符 类型看成 整数类 型,其整数 值是在 计算机 系统字 符集中 的编 码 ,也 就 是相 应字符存 储形态 的编码 值。另 外,由于 C语言 把字符 类型看 成整数 类型,还 定 义了带 符 号和不带符号的字符类型。 在 C语言中 ,所 有关于 在整数 类型的 运算定义 ,自 然也都 适用于 字符类型 。 2.2.4 布尔 类型 布尔类 型仅有 两个值 :false(假)和 true(真)。在 C语 言中把 bool类型也看 成整数 类型, 分别 用 0和 1表示 false(假 )和 true(真)。显 然,布尔类 型的值 域就是 由 0和 1构成的 集合。 可施于布尔类型上的运算有: !(非)    &&(与)     ||(或 ) 设 b1和 b2分 别是两 个布尔 类型量 ,上述 运算的 定义如 表 2.2所示: b1 false false true true b2 false true false true 表 2.2 布尔运算的定义 !b1 true false b1&& b2 false false false true b1 ||b2 false true true true 直观上讲: !为 取反运 算。true的 反就是 false,false的反就 是 true。 && 可理解成 “并且”。 只有两 分量都 是 true时结果 才是 true,否则 结果为 false。 ||可 理解成“或 者”。两分 量只要 有一个 为 true,结果为 true,否 则结果为 false。 2.2.5 枚举 类型 世界上 的事物 是复杂 的,例 如,一个 房间 里的同 学,一周 里的 七天,一 年的 十 二个 月,等 等。 计算机 在处理 这些问 题时,用标准 的数据 类型 (例如 整数类 型)表 示它 们 很 不 直观。 可 以用 枚举类 型(enumerated-type)表 示这些 不方便 用数值 表示的 一组特 殊对象的 集合。 枚举类型 通过枚 举表记 值的标 识符确 定一个 类型的 值的 有 序集 合。其形 式 是 将表示 值 的标 识符顺 序列出 来,并用花括 号把它 们括上 ,语 法如下 : · 34· 第二章 数 据 信 息    <枚举类型说明符 > → 枚举类型定义 > x <枚举类型引用 > <枚举类型引用 > → enum <枚举标签 > <枚举类型定义 > → enum <枚举标签 > { <枚举定义列表 > } <枚举定义列表 > → 枚举常量定义 > x <枚举定义列表 >,<枚举常量定义 > <枚举常量定义 > → 枚举常量 > x <枚举常量 > = <表达式 > <枚举常量 > → <标识符 > <枚举标签 > → <标识符 > 这个 语法比 较复杂 ,它 定义了枚 举类型 各种可 能的结 构。最 基本的 枚举类 型说明符 形式是 :     enum {id,id,id,…,id} 或     enum id_tag{id,id,id,…,id} 其中 ,每 个标识 符 id都是 一个枚 举常 量,也是 相应枚 举类 型中 的 一个 值。括 号中的 全部 标 识符集合构成相应枚举类型的值域。例如:     enum {sunday,monday,tuesday,wednesday,thursday,friday,saturday} enum month{Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec} enumcolor{red,yellow,green,blue}      * 红、黄、绿、蓝 */ enum {club,diamond,heart,spade} /* 梅花、方块、红桃、黑桃 */ 都是 枚举类 型说明 符。利 用这些 类型说 明符可以 声明枚 举类型 变量,也可 以定义枚 举类型 。 在 C语言中 仍然把 枚举类型 看成 整数 类 型,每个 枚举 常量 对 应一 个整 数 值。一 般情 况 下,枚举 表的第 一个标 识符对应 整数值 0,其他 标识符 对应前一 个标识 符整数 值 +1。上 述例 子中 red对应 0、yellow对应 1、green对 应 2、blue对应 3。还 可以在 声明 枚举 类型时 ,在 枚 举表 中的枚 举常量 标识符 后标识 上该常 量对应的 整数值 ,例 如:   enumcolor{red =10,yellow = red+2,green = 15,blue} 表示 red对应 10、yellow对应 red+2即 12、green对应 15、blue对应 green+1即 16。 在 C语言中 ,所 有关于 在整数 类型的 运算定义 自然也 都适用 于枚举 类型。 使用枚举类型应注意: (1)括 在花括 号中的 枚举常 量表中 的标识符 都是常 量; (2)枚 举常量 表中的 标识符 不可重 复,不 论是在 同一个 枚举常 量表中 ,还 是 在 同一使 用 范围内的不同枚举常量表中; (3)枚 举常量 表中的 标识符 不能与 其他标识 符重复 ; (4)不 能用保 留字作 枚举常 量; (5)尽 管 C语言把 枚举类 型看做 整数类 型,枚 举常量 也与整 数对应 ,但 是还是 建议读 者 区别开枚举常量和整数。 2.3 混 合 运 算 下列是非法的枚举常量表:   {a,b,c,d,b}      标识符 b重复 {x,y,z,c} 标识符 c与别的枚举常量表的 c重复 {if,e,f,g} if是 保 留字 · 35· 2.3  混 合 运 算 前面介绍 了 C语言 的各种简 单数据 类型,包括 浮点类 型、整数类 型、字符类 型、布尔类 型 和枚 举类型 ,也 介绍了 可 以施 与各 种类 型上 的 运算 。事 实 上,在 C语 言 中 只有 两 种 简 单 类 型:浮 点 类 型 和 整 数 类 型 ,所 谓 字 符 类 型 、布 尔 类 型、枚 举 类 型 都 是 整 数 类 型 的 不 同 表 现 形式。 C语言这 样处理 类型,更接近 计算机 数据表 示的实 质(本来 在计算 机内部 所 有 数据都 是 二进 制代码 ,也 都是整 数),也 简化了 概念。 但是 这 样 做 的后 果 是 使 得 C语言 的类型 概念 太 弱,有时 一些不 可思议 的运算也 是合法 的(例如 ,字 母 A和表示 一个人 年龄的 整数 28相 加简 直不 可理解 ,但 是在 C语 言中 却 可以 得 到 结果 92),并 且 不利 于 类型 检 查 和保 证 程 序 的 正 确性。 通常称 浮点类 型和整 数类型 为算 术 型,称 可 施于 算术 型 上的 运算 为 算术 运算。 相应 运 算符 称为算 术运算 符。按 照 日 常习 惯,人 们 往往 不 区 分算 术 型中 的 浮 点 类 型和 整 数 类型。 为了 照顾这种 习惯,C语言 允许在 浮点类 型和整 数 类型 之间 进行混 合运算 ,混 合 运 算后结 果 为浮 点类型 。表 2.3列出了 算术运 算符、分量 类型和 结果类 型。 运算符 + - + - * / % 表 2.3 算术运算符、分量类型、结果类型 运算 单目恒等 单目取负 加 减 乘 除 求余数 运 算 分 量 类型 浮点类型 或 整数类型 整数类型 结果类型 同运算分量类型 若两个分量类型都是 整 数 类 型 ,则 为 整 数 类型 ; 否 则 为 浮 点 类 型。 整数类型 · 36· 第二章 数 据 信 息 2.4  关 系 运 算 本章介 绍 的 浮 点 类 型、整 数 类 型、字 符 类 型 、布 尔 类 型、枚 举 类 型 都 属 于 简 单 类 型 (simple-type),也称标 量类型 。 简单类型 有一个 共同的 特性,即值 是可比 的(也可 以认为 是有序 的)。并且 规定,不论 是 整数 类型(包括 了字符 类型、布尔 类型、枚举 类型)还是浮 点类 型,都按实 数意 义下的 数值 大 小进 行比较 和排序 。数值 小的排 在前边 ,数值 大的排 在后边 。 1.关系 运算 定义了值 的顺序 之后,就可 以对两 个值进 行大小 关系的 比较,称 为进行关 系运 算。所 有 关系 运算都产 生布尔 类型结 果。在 C语 言中有 四个关 系运算 符可用 来对标量 类 型 进行关 系 运算 ,它 们是: <(小于)  >(大于 )  <=(小 于等于 )  >=(大 于等于 ) 2.相等 比较运 算 还可以 对两个 值进行 相等关 系的 比 较,称 为 进行 相等 比 较运 算。所 有相 等 比较 运算 也 都产 生布尔类 型结果 。在 C语言 中有两 个相等 比较运 算符可 用来对 标量类型 进 行 相等比 较 运算 ,它 们是: ==(等于)  !=(不等于 ) 这些 关系运 算和相 等比较 运算的 意义都 是明显的 ,例 如:    ==3      得 true      A > C     得 false 3!=3 得 false true< false 得 false 5.5 < 8.1  得 true true<= true 得 true C语言的 bool类型 没有定义 通常逻 辑代数 中的等 价、蕴含运 算,但是这两 种 运 算可以 通 过关系运算和相等比较运算来实现: 等价 运 算 显 然 可 以 用 相 等 比 较 运 算 “==” 来 代 替,而 蕴 含 运 算 则 可 以 用 关 系 运 算 “<=”来代替。 本章小结 本章讲 述构成 C语 言程 序 的 基 本单 位、数 据 ,包 括基 本 字 符集、词 法 单 位、简 单 数 据 类 型。重点掌握简单类型及其运算。 习 题 二 · 37· 习 题 二 2.1  什 么是 计 算 机 系统 字符 集 ? 什 么 是 程 序设 计 语 言 字 符集 ? 它 们 之间是 什 么 关 系 ? 2.2 查阅有关资料,找出 C语言、Pascal语言和 FORTRAN语言的字符集、词法单位种类以及标识符的 文法规则。 2.3 构成 C语言程序的词法单位有几种?各是什么?每种词法单位的构成规则是什么?每种词法单 位在 C语言程序中起什么作用? 2.4 下列符号哪些可以作为 C语言用户标识符?     data、6day、j5、VAX、program 、abs、sin、A.B、M.D.Jean        AANDB 、last、DJS_130、X 、ZZZZZZ、IBM-PC、case、Do、do        IBM-PC/XT、A AND B、f(x)、1th、"while"、FLOAT、m -n、a[5] 2.5  试 述保 留 字 和 标识 符的 相 同 点 和 不 同 点。 2.6 下列哪些是合法的 char类型常量?      A 、 ! 、 、 78、STAND、 \ 、 .      thissaboy、that\sadog 2.7  下 列数 据 哪 些 是整 数,哪 些 是浮 点 数 ,哪些 是 非 法 的 ?     256、2.321、33E+7、E+88、 -1.2E70、0.0、0.0E+0、E5、       00033-5、00033E0001、27、103、-256、0.3×105、00000、.375E2、      -625.E+9、-2.321、5.5E-6.6、0.05E-39、0.05-E39、2E5、256.、.256 2.8 为下列量写出 C语言表示。     e、88.675、字符 A、字符 5、真 、- 2、101000、π、ρ、α、Ω 2.9  求 下列 运 算 的 结果 ,并 说 明 类型 或 指 出 错 误 。      1)15.1+8          7)367/2 (2)15.0+8.0 (8)15 <8 (3) A //BCD (9)33>=33 (4) A + B (10) G >= A (5)true<false (11)25.0 /5.0 (6)367% 2 (12)25.0% 2.0 2.10  说 明 数 据 类型 的 意 义 。 2.11  什 么 是 简 单类 型 ?C语 言中 有 哪 些 简单 类 型 ? 各类型 上 都 定 义 了 什 么运 算 ? 2.12  字 符 常 量 和字 符 串 常 量有什 么 区 别 ? 2.13  枚 举 类 型 有什 么 特 点 ?在使 用 中 应 注 意 哪 些问题 ? 2.14  分 别 定 义 枚举 类 型 : (1)描 述 婚 姻 状 况 :已 婚 (marrid)、离 婚(divorced)、孀居 (widowed)、单 身 (single); (2)描 述 寝 室 全 部 同 学; · 38· 第二章 数 据 信 息 (3)描 述 某 农 民 今 年 播种的 庄 稼 品 种; (4)描述 C语言具有的全部简单数据类型; (5)描述 C语言具有的全部词法单位种类; (6)描 述 本 学 期 所 学 的课程 。 2.15 从下列 C语言程序段中删除所有注释后剩下的符号串是什么?    /**/*/*“*/*/*”//*//**/*/ 第三章 简 单 程 序 本章介 绍最简 单的 C语言程 序,包括 常量、变量、表达 式、赋值、输入 /输出 等。 3.1 常 量及 常量 定义 经常有 一些数 据 在 程序的 执行期 间不变 ,例如 圆 周率 π 的近似 值 3.142,自 然 对数底 e 的近 似值 2.7183等。这 些在程 序执 行 期 间 不变 的 数据 称为 常量。 常量 可以 直 接书 写在 程 序中 ,称 为字面 常量。 程序   #include <stdio.h> void ain(){ printf("% f\n",2.7183); } 打印 出 2.7183。 2.1.6节介绍 的常量 都可以 作为字 面 常 量写 在 程序中 。 有时 在程 序中直 接 书写 一个常 量的字 面值不 能表明 该常量 的 明确 含 义 。例如,2.7183就不 一定 被理解 成自 然 对数 的底 e;另 一方面 ,有 时一个常 量可能 在程 序中多 处用 到,一旦要 对该 常量 进行修 改,就 有可 能产生 遗漏。 可以引 进标识 符来代 表常量(constant),称表 示常量 的标识 符 为 常量标 识 符(constant-identifier)。 枚举常 量表中 的标识 符就是 常量 标 识 符 。除此 之外,还可 以在 程序 的说 明 部分 使用 宏 定义 来定义 某标识 符为常 量标识 符,并 用它代 表某一 常量。 格式如 下:   #define标识符 常量 这种格式定义相应标识符为其后面的常量字符串。例如   #defineSPEED55 定义 标识符 SPEED代 表整数 55。在 该行之后 的程序 中凡是 出现 SPEED都代表 55。 3.2 变 量及 变量 声明 3.2.1  变 量 与常量 相对应 ,变 量(variable)是 在程序执 行期间 其 值 可变 的 数 据 对象。 变量的 概念 在 · 40· 第三章 简 单 程 序 数学中是大家熟悉的。程序设计语言中变量的概念与数学中变量的概念类似。 从存储 分配角 度看,变量分 为静 态变 量和动 态变 量,这里只 讨论 静态 变量,第十 一章 再 讨论动态变量。 在程序设 计语言 中,一个变 量包含 两个方 面。其 一是值 ,即 在程序 执行的 某 一 时刻变 量 所具 有的值 ;其 二是属 性,包括变 量 的名 字、它 所 属 的 数据 类 型、它 的 作用 域、它 所占 有的 存 储区以及该存储区地址等一些信息。 编译程 序在把 高级语 言程序 翻译 成 机器 语言 程 序时,给 每个 变量都 分配 一 块适 当的 存 储空 间,以便随 时保存 变量的值 。这块 存储区 就是变 量的存 储区,变量 的地址 就 是 这块存 储 区的 首地址 ,变 量的值 就是这块 存储 区中 现行保 存的 数据。 设 有 变 量 v,分 配在 内 存 0F000 开始 的一块 存储区 中,现行值为 2.7183,则如 图 3.1所示。 图 3.1 变量 3.2.2 变量 声明 变量的属 性由变 量声明 规定,程序 员可以 在变量 声明中 引进变 量,并且规 定 所 引进的 变 量的 属性。C语言 变量声 明的语法 如下:    <变量声明 > → <类型 > <变量项表 > ; <变量项表 > → 变量项 > x <变量项 >,<变量项表 > <变量项 > → <标识符 > x <标识符 > = <初值 > 暂时先 不考虑 初值,按上述 语法规 则,一个变 量声明 的形式 如下:   Tid,id,…,id; 其中: (1)每 个 id是一个 标识符 ,是 由该变 量声明 引进的 变量,也是相 应变量 的名字 。 (2)T是类 型,可以是任 何一种 类型符 ,包 括已 经学过 的 各种 浮 点 类型、整 数类型 、字 符 类型 、布 尔类型 、枚举 类型以 及以后 将学习 的各种 类型。 它决定 了列在 它后面 标 识 符表中 的 标识符所代表变量的类型属性。 (3)每 个变量 声明所 处的位 置决定 了它所声 明变量 的作用 域。 如下一 些变量 声明引 进了 9个变 量: 3.2  变量 及 变 量 声 明 · 41·   inti,j,k; charc; intt; floatamount,total; boolflag; enum {red,blue,green}color; 其中 ,i、j、k、t为 int型变 量;c为 char型变量;amount、total为 float型变量 ;flag为 bool型 变 量;color为一个枚 举型变 量。 3.2.3 变量 形态 一个变 量作为 程序语 言 的 一 个 成分,在 程 序 中 有 三 种 出现 状 态 (注 意,这 里说 的 是 变 量,不是 标识符 )。 1.在变 量声明 中出现 一个标 识符在 某变量 声明的 标识 符 表中 出现 便 引进 了一 个 变 量 ,该 变量 的 类型 由该 标 识符 表前边 的类型 决定。 编译程 序会 给 该 变 量分 配 一 块 存 储空 间,但 是 从 程 序 开始 执行 的 时刻 一直到给 该变量 赋值之 前,该变量 是没有 值的,这时 称该变量 为“值无 定义的 ”。例如 有 变量声明:   charc; 则引进变量 c:       它为 字符型 。在程 序开始 执行时 c是“值 无定义的 ”,它还没有 值。 2.为变 量赋值 后出现 这种出现 状态为 变量赋 予一个 它值域 之内的 (即所属 类型的)一 个数据 。一旦 为变量 赋 值,该变 量就变 成了“值 有定义的 ”,也 就是说 它有了 值,其值就 是刚刚 赋给它的 那个数 据量。 给一 个变量 赋值可 以通过 表达式 的赋值 运算、读语句 、初 值、参数结 合、初值等 方式实 现。 一个变 量被赋 值后,便成为 “值有 定义的 ”变 量 ,对 “值 有 定 义 的 ”变 量可 以 再一 次重 新 给它 赋予新 值。变 量一旦 被赋予 新值 便 失 去 了它 原 来 的 值,并且 一般 情 况 下 那 个原 来的 值 就丢 失了。 例如上 述变量 c,若给 它赋值 P后 ,它 便是“值 有定义 的”,并 且值为 P 。 c:    P    若再 给它赋 值 S,则 c的 值就变 成了 S,这时原 来的值 P便 已丢失 。 c:   S    这是因为 只能给 c分配一块 存储 区,该区 只能存 放一 个字 符 型值。 向该 区存入 一个 新 值,其旧 值必然 被冲掉 。 · 42· 第三章 简 单 程 序 3.引用 性出现 引用性 出现分 为两种 :一种 引用 变量 当前 值 ;一 种引 用变 量 本 身 ,包 括 它 的名 字、地址、 存储 区、类型等 一切属 性。引用 值的出 现 一 般 在表 达 式 中,这 时 被 引用 的 变 量 必须 是 “值 有 定义 的”,这种引 用性出 现不会改 变变 量的 值 。引用 变量 本身 多出现 在参 数结合 中,这时 被 引用 的变量可 以是“值 有定义 的”,也可以 是“值无 定义的 ”。 这种 引用性 出现 可能会 改变 变 量的值。 3.2.4 变量 地址 在 C语言程 序中,经常 使用一 个变量 的存储区 及其 地址。 因 此 C语 言给 出一个 运算 符 “&”,该运 算符是 一个单 目运算 符,把它 缀 在一 个变 量 前,求 相应 变量 的 内存 地址。 例如 对 于本 节开始 的变量 v而言,运算   &v 将得 到 v的地 址 0F000。该地 址也称 为 v的指 针,运算 符 & 也称 为求 指针运 算符,运 算 &v 也称 为求 v的指针 。 3.2.5 变量 初始化 从前面 关于变 量声明 的语法 可以看 出,在 声明变 量时还 可以给 变量赋 予初值,形 式是:   id = 初 值 即在变量标识符后加一个等号再跟以相应初值。例如:   inti=0,j=1,k=100*2; charc= A ; enum{red,blue,green}color= red; 变量在 声明时 被赋予 初值,则在程 序开始 运行时 ,它 便取得 了相应 值,它是值有 定义的 。 现在看 来,初值是 一个常量 表达 式,将来学 过构 造型 类型后 ,初 值可 能是 由 花括 号括 起 来的常量表达式表。 注意 在使用变量时要注意以下几点: (1)程 序中使 用的一 切变量 都必须 声明; (2)在 同一作 用域内 ,任何 两个变 量不允 许重名 ,也 不允许 与其他 类标识 符重名 ; (3)从 程序的 静态行 文顺序 看,变 量的声 明必须 出现在 所有使 用之前 ; (4)从 程序的 动态执 行顺序 看,为 变量赋 值的出 现必须 在所有 使用值 之前; (5)从 程序的 静态行 文顺序 看,在 变量声 明中给 变量赋 初值处 ,相 应常量 表 达 式必须 是 可计算的。 3.3 表   达   式 · 43· 3.3  表  达  式 3.3.1 表达 式的结构 表达式 (expression)是 C语 言 程 序完 成 各 类 运 算的 主 要 部 分,由 运 算 符 和 运 算 分 量 组 成。 C语言表 达式及 其语法 十分复 杂,下面只 给出 表达式 分类 语 法,有 关表 达式的 详细 语法 请参阅附录二或有关资料。    <表达式 > → 基本表达式 > x <后缀表达式 > x <一元表达式 > x <二元表达式 > x <条件表达式 > x <逻辑表达式 > x <赋值表达式 > x <顺序表达式 > <基本表达式 > → 标识符 > x <字面常量 > x <(表达式)> 基本表 达式是 C语 言表达式 的最底 层,包括 标识 符、字面 常量和 括号 表达式 ,可 以说 基 本表 达式就 是 C语言表 达式的运 算分量 。 标识符 包括变 量标识 符和常 量标 识 符,它们 的类 型由相 应声 明给 出 ,当表 达 式计 算时, 变量 应该是 值有定 义的。 字面常 量在 2.1.6节中 已经介 绍过。 一个表 达式用 括 号 括起来 后 仍作为一个运算分量。 由基本 表达式 (运 算分量 )和运算 符相互 组合可 以构成各 种复杂 的表达 式。例 如:   3*6 u>0 r=(x+y)*5<=(r-s)/8 这里 ,3、6、u、r、x、y、5、s、8等都 是基本 表达式 ;经 过与运 算符组 合后的 x+y、r-s是 表 达式; 用括 号括上 的表达 式(x+y)、(r-s)又变成 基本表 达式;最终若 干基本 表达式 与 运 算符组 合 得到 的 r=(x+y)*5<=(r-s)/8仍是 表达式。 C语言 运 算 符 十 分 复 杂,表 3.1列 出了 C语言 所有 运 算 符 、它 们的 优先 级以 及 结 合 规 律。 这些运 算符有 的已经 介绍过 ,有 的将 在后 续 章 节 的适 当 位 置 讲授,有 的可 能 不讲 授,请 读者查阅有关资料。 · 44· 第三章 简 单 程 序 表 3.1 C运算符(按优先级从高到低) 记号 运算符 类别 结合关系 优先级 高 标 识 符 、字 面 常 量 、(… ) a[k] F(… ) . -> ++ -- 简单记号 基 本表 达 式 无 下标 函数调用 直接选择 间接选择 后缀 16 从左到右 自 增 、自 减 (类 型名){初 始 化列 表 } ++ -- Sizeof ~ ! -+ & * (类型 名 ) * /% 复合字面值 自 增 、自 减 长度 按位取反 逻辑非 算 术 负 、正 地址 间接访问 类型转换 算 术 乘、除 、求 余数 前缀 一元 15 从右到左 14 13 +- << >> < > <= >= == != & ^ | 算 术 加 、减 左 移 、右 移 关系运算 判等运算 按位与 按位异或 按位或 二元 12 11 10 从左到右 9 8 7 6 && 逻辑与 5 || 逻辑或 4 ?: 条件表达式 三元 3 =  +=  -= *=  /= %= 赋值 <<=  >>= &= ^=  |= 二元 从右到左 2 , 顺序表达式 从左到右 1 低 在书写 C语 言表达 式时必须 注意与 通常数 学表达 式的区 别,请注意 : (1)所 有字符 必须写 在一条 水平线 上,不 允许出 现上、下角 标和分 数线等 。 (2)a乘 b不 能写成 ab,也 不能写 成 a·b,必须写 成 a* b。 (3)除 了下标 使用方 括号以 外,所 有括号 必须用 圆括号 。 (4)注 意连续 的关系 运算,数 学 中 a < y < b一 类的 关 系 表 达 式 ,在 C语 言中 虽然 允 3.3 表   达   式 · 45· 许,但是 其意义 与数学 中完全不 同。数 学中的 该类关 系表达 式在 C语 言中应该 写成 (a< y)&& (y < b) 这里 的括号 不是必 须的,但是加 上括号 使得意 义更清 晰。 (5)由 两个或 两个以 上字符 构成的 运算符,其各 字符中 间不允 许夹有 空格等间 隔符。 (6)由 于 C语言表 达式、运算 符以及 运算符 优先 级的复 杂性 ,初 学者 很难掌 握,就是 熟 练的 程序员 也难免 出错,所以在 容易出 错的地 方按照 自己的 本意适 当加括 号是好习 惯。 3.3.2 表达 式的计算 在 C语言中 ,表 达式的 计算顺 序是不 确定的。 大致遵 循如下 规则: (1)若 两个不 同优先 级的运 算符相 邻,则 应先计 算优先 级高的 运算符 规定的运 算。 (2)若 两个运 算符同 级且相 邻,则 应按运 算符结 合律的 规定从 左向右 或从右向 左计算 。 前边已经 介绍了 算术运 算符(+ 、- 、* 、/、%)、关系 运算符 (< 、> 、<= 、>=)、判 等运 算符(= 、!=)、逻辑 运算符(!、&& 、||)、求变量 指针 运 算符( & )等。 经 过 算术运 算 符进 行运算得 到的表 达式称 算术表 达式,算术 表达式 产生算 术型结 果;经过关 系 运 算符和 判 等运 算符进行 运算得 到的表 达式称 关系表 达式,关系 表达式 产生布 尔型结 果;经 过 逻辑运 算 符进 行运算 得到的 表达式 称逻辑 表达式 ,逻辑 表达式 产生布 尔型结 果。 由表 3.1可以看 出,算术表 达式的 优先级 高于关 系表达 式,关系表 达式的 优 先 级高于 判 等表 达式,判等 表达式 的优先级 高 于逻 辑 表 达 式。 在 算 术 表 达 式中,乘 法运 算 (* 、/、%) 高于 加法运 算(+ 、- );在关 系表达 式和判等 表达 式 中 ,运 算符的 优 先级 是 一 样的;在逻 辑 表达 式中,逻辑 非运算 (!)优先 级最高 ,然 后是 逻辑与 运算 (&&),逻 辑或 运 算( ||)优先 级 最低。 在二元运算符两端的 部分 称为该运 算符的 运算 分量。为 了实现 二 元运算 符规定的 运 算,首先 要计算 运算符 两端运算 分量的 值。两 个运算 分量值 的计算 次序是 依赖于 实现的 (为 了优 化代码 ,编 译程序 有时重新 安排运 算分 量的 次序)。 这意 味着:运算 分量 的计算 可能 按 正文 顺序计算 ;或按正 文的逆序 计算;或两 者并行 计算;或不 必两 者 都计 算。请 读者 注 意,由 于运 算对象计 算次序 的不同 ,程 序可能 得到完 全不同 的结果 。为了 保证正 确性 和可 移 植性, 在程序中任意运算的值都不应该依赖运算分量的计算次序。 为说明 一个运 算的值 可能依 赖于运 算对象的 计算次 序,举例如 下:   #include <stdio.h>              * 1*/ intx,y; /* 2*/ intf(intz){ /* 3*/    x=x+1; /* 4*/    return(z*z); /* 5*/ } /* 6*/ voidmain(){ /* 7*/ · 46· 第三章 简 单 程 序    x=0;    printf("% d\n",x+f(2)); } /* 8*/ /* 9*/ /* 10*/ 该程 序的第 3行到第 6行是一 个函数 定义,该函 数的功 能是先 将 x加 1,再送回 x中,然 后带 着函 数值 z*z返回 。 执行该 程序,请注 意第 9行 的输出 。当二 元运算 符 的 运算 分 量从 左向右 计算 时输出 4, 而从 右向左 计算时 输出 5。 再举一 个表达 式计算 的例子 。设 u=20、v=30、w=40、x=20、y=50,计 算如下 算式: u+20>v||w==0 ||x<y||v+w/3>x||x+y>u+v+w 从左 向右计 算该表 达式,当计算 出 u+20>v的值 为 true后,就可以 得出结 论:不 论 后面诸 条 件的 值是 true还是 false,该 表达式 的值都 是 true,所 以该表 达式的 后边部 分可以不 必计算 。 最后总结表达式值的计算规则如下: (1)括 号内的 表达式 先计算 ; (2)运 算符分 16个优先级 ,不 同级运 算符相 邻,先计算 优先级 高的运 算; (3)同 级运算 符相邻 ,按结 合律的 规定从 左向右 或从右 向左进 行计算 ; (4)二 元运算 符和三 元运算 符的各 个运算分 量的计 算次序 是依赖 于实现 的。 3.4 语    句 程序的一 个侧面 是描述 对数据 的操作 ,操 作由一 个个动 作组 成 。在程 序设 计语 言 中,表 示动 作的是 语句(statement),语句 的执 行表 示执 行 有关 动作。 各种 程序 设计 语 言中 语句 的 种类 不一,但是 一个好 的程序设 计语言 中提供 的语句 应该能 够方便 地描述 各种 动作,并且 用 这些 语句编 写出的 程序应 该是结 构 清晰 合 理 、风 格优 美和 执行 高 效的 。 C语 言 提供 了十 二 种语句。有关语句分类的语法如下:    <语句 > → 无标号语句 > x <标号语句 > <标号语句 > → < 标号 > :<语句 > <无标号语句 > → 简单语句 > x <结构语句 > <简单语句 > → 空语句 > x <表达式语句 > x <返回语句 > x <break语句 > x <continue语句 > x <goto语句 > 3.6  赋   值 · 47· <结构语句 > → 复合语句 > x <分支语句 > x <重复性语句 > <分支语句 > → <if语句 >x <switch语句 > <重复性语句 > → < do语句 >x < while语句 >x < for语句 > 若不考 虑 标 号 语 句 ,C语 句 分 为 简 单 语 句 (simple-statement)和 结 构 语 句 (structured- statement,也称 构造语 句)两类,如 图 3.2所示。 图 3.2 C语句分类 3.5  表达 式语 句 C语言中 最简单 的语句莫 过于表 达式语 句,表达式 语句的语 法是:    <表达式语句 > → <表达式 > ; 即,一个 表达式 后跟一 个分号“;”就构 成表达 式语句 ,它 的语义 是计算 表达式的 值。例 如:   3+5; b = a*c-3 > x+y&&c+d < 0; 3.6 赋    值 给变量 赋值的 操作是 最基本 的操作 。C语言用 带赋 值运算 符的表 达 式语 句描 述赋值 操 作,这种 表达式 语句一 般具有形 式:   v = e; · 48· 第三章 简 单 程 序 其中: (1) = 是赋值 运算符 ,由 一个等 号组成; (2)v是一 个左值 (现在看 就是一 个变量),作为 赋值运 算符左 端的运 算分量; (3)e是一 个表达 式,作 为赋值 运算符 右端的 运算分 量,e能计 算出一 个值。 表达式语 句被执 行时,赋值 运算符 右端的 e被 计算并 得到一 个值,然后把 该 值 送入赋 值 运算 符左端 运算分 量的变 量 v中 (即送入 v所代 表的存 储区中 )。例如 语句   days=2+5; 执行 结果是 将值 7赋给 变量 days。 若 days原来 是值 无定义 的,现在 它变 成值有 定义 的,并 取值 7;若 days原 来是值 有定义 的,则不论 days原来值 是什 么 ,都 将被 7所取 代 ,days仍 是 值有 定义的 ,值 是 7。最后整 个表达 式值也 为 7。 在赋值 表达式 中,赋值运算 符“=”右 端是计 算 值 的表达 式,左 端回 答“该 值交给 什么 对 象”,因此 下述语 句都是 正确的。   firstnumber=1; circumference=2* pi* radius 甚至数学上显然不成立的   nextnumber=nextnumber+1 也是 正确 的。 其 执 行 结 果 是 将 变 量 nextnumber的 值 加 1,得 到 一 个 新 值 ,再 送 入 变 量 nextnumber中 去。但 下述形式 是错误 的,因为 左端不 是变量。   1=firstnumber tength* width=area 例 3.1 设 v1、v2、v是同一 类型的 三个变 量,且 v1、v2都 是值有 定义 的,则下述 语句 将 使 v1与 v2的 值互相 交换。   v=v1; v1=v2; v2=v 例 3.2 做一 个长 50cm,宽 30cm,面积 500cm2的矩 形木 框 ,且各 边 等宽 。 编写 计算 该 木框每边应做多宽的程序。 如图 3.3所示,若 设木框各 边宽为 x,框长 为 length,宽为 width,面积为 area。由 数学 知 识可知:   area=2(length×x+(width-2x)×x) 即   4x2 - 2(length+width)x + area =0 这是一 元二次 方程,使用求 根公式 编程序 解该方 程。 3.6  赋   值 x = -b± b2 2a -4ac 该方 程求解 步骤如 图 3.4所 示,得程序 如下: · 49·      图 3.3 木框 图 3.4 解二次方程的 PAD图     #include"stdio.h" #include"math.h" #definelength50.0 #definewidth30.0 #definearea500.0 void ain(){ floatx1,x2,b,d; b=-2.0* (length+width); d=sqrt(b* b-4.0*4.0*area); x1=(-b+d)/(2*4); x2=(-b-d)/(2*4); printf("x1=%.2f  x2=%.2f\n",x1,x2); } 该程序运行结果将输出: x1=36.58      x2=3.42 1.左值 赋值运算 符“=”左 端的运 算分量 是一个 变量。 广义的讲 ,在 C语言 中赋值 运算符 “=” 左端 的运算分 量是一 个“左值 ”,也 就是一 个可以 求得 左值的表 达式 。所谓 “左值 ”实质就 是 内存 某个存 储区的 地址。 左值的 概念在 C语 言中十 分重 要,许多 地方在 解释 语义时 都用 到 左值 。通俗的 讲,左值 就是允许 在赋值 运算符 左端出 现的表 达式。 最基本 的左 值就 是 变量, 另外 还有下 标变量 、结 构成分等 ,目前仅 接触变量 。表 3.2列出 了可以 作为左 值 的 非数组 表 · 50· 第三章 简 单 程 序 达式 ,以 后将逐 步介绍 。为了称 呼 方 便,如果 没有 特 殊需 要,以后 还笼统 地称 左 值表 达式 为 变量。 表 3.2 可以作为左值的非数组表达式 表达式 标识符 e[k] (e) e->名 称 e.名 称 *e 字符串型常量 附加条件 应为变量名 无 e应 为 左值 e应 为 左值 无 无 无 2. ++ 和 -- 前边举过一个赋值运算的例子 nextnumber=nextnumber+1 该运 算的含 义是 把变量 nextnumber的 值取 出,加 1后再送 回 nextnumber中。 在程 序设计 过 程中 ,这 类运算 特别多 ,为了 简化书 写,C语言 引 进两 个运 算符 “++”和 “--”,分别 表示 把 一个 整 数 类 型 变 量 中 内 容 取 出 加 1(++)或 减 1(--)后 再 送 回 原变 量 中 去。“++”和 “--”既 可以缀 在变量 的前边 ,属 于一元 运算符 ,也 可以缀 在变量 的后 边,属于 后缀运 算符。 以变 量 v为例 ,设 v中原 有值 v0,这两个 运算符的 含义如 表 3.3所 示。 表达式 v++ ++v v-- --v 表 3.3 运算符 ++和 --的语义 类别 后缀 一元 后缀 一元 运算后 v的值 v0 +1 v0 - 1 运算后表达式的值 v0 v0 + 1 v0 v0 - 1 可以看 出,v++ 和 ++v对 v的操作 都相当 于 v=v+1,运算后 v的值都 是 v的原值 加 1,即 v0 +1。但 是,整个 表达式的 结果有 区别: (1)表 达式 v++ 是 先求表 达式的 值,后对 v加 1。结果 表达式 的值是 对 v进 行加 1之 前的 v值,即 v0。 (2)表 达式 ++v是 先对 v加 1,后 求表达式 的值。 结果表 达 式 的值是 对 v进 行加 1之 后的 v值,即 v0 +1。 3.7 类 型 转 换 v-- 和 --v的 意义与 v++ 和 ++v类 似,在此不 再赘述 。 · 51· 3.7  类 型 转 换 前边已经 讲过几 种数据 类型及 其运算 ,这 些类型 都是简 单类型 ,以 后还要 介 绍 构造型 类 型和 指针类型 等。从 前边的 讲述可 知,各种不 同类型 及其数 据是有 区别的 。对 C语 言 而言, 在表 达式中 对参与 运算的 运算分 量 类型 ,以及 在 函 数 的形 式 参 数 与 实在 参 数 结 合时 对参 数 的类 型都有一 定的限 制或进 行一些 必要的 转换。 弄清这 些限 制 以及 转换规则 是必 要的。 本 节介绍参与运算的运算分量的类型转换。 实质上 ,C语言 的简单 类型只 有两类 :各 种浮点 类 型 和各 种 整数类 型。但 是每 一类型 中 根据 数据存 储长度 以及表 示形式 不同,又 分 成若 干 类 型 。在 C语 言中各 种类 型数据 进行 混 合运 算时遵循 一个隐 式转换 规则进 行自动 类型转 换,除此 之外,C语言 还提供 显 式 的强制 类 型转换运算。 1.隐式 转换规 则 C语言隐 式自动 类型转换 的总体 原则如 下: (1)单 目运算 (一元运 算、后 缀运算 )。 ① 所有浮点类型不转换; ② 长度大 于 int类型 的整数 类型不 转换; ③ 长度小 于 int类型 的带符 号整数 类型转 换成 int类 型; ④ 长度小 于 int类型 的无符 号整数 类型: (a)值 可以用 int类型表 示,转换成 int类型 ; (b)值 不能用 int类型 表示,转换成 unsignedint类型。 (2)双 目运算 (两个操 作数的 运算)和三 目运算 (三个操作 数的运 算)。 向类型 高的运 算分量 转换,包括 把短 类型转 换成 长类型、把 整数 类型 转换 成 浮点 类型、 把有 符号类 型转换 成无符 号类型 。图 3.5给出这 种转换 的规律。 在图 3.5中: ① 横向的箭头表示只要遇到 相应 类型 一定按 箭头 方向 转换 成左方 的类 型。比 如有 运 算 A + 0,这是 两个字 符相加 ,得 到整 数结果 112。 但这 个 112是 int类 型 的 ,而 不 是 char 类型 或 short类型 的,原因是在 运算之 前就把 A 和 0分别 转换成 了 int类型的 64和 48,相加 后的 结果自 然是 int类型的 了。 ② 纵向箭头表示按箭头方向 从下 向上 类型一 个比 一个 高。参 与运 算的 诸运算 分量 向 所有 运算分量 中类型 最高的 运 算 分 量 的 类型 转换 。 比 如 有运 算3.14*2,首先 3.14是浮 点 类型 ,没 有指明 是哪种 浮点类型 ,按省缺 应该为 float类型 ;然 后 2虽 然是 int类 型,但 是由 于 要与 3.14进行运 算,被转换 成 float类 型;最后运 算结果 6.28为 float类 型。 · 52· 第三章 简 单 程 序 图 3.5 自动类型转换规则 2.强制 类型转 换 有时程 序中需 要进行 强制的 类型转 换,比 如运算 5/2,根据 需要应 该进行 浮点运 算,得到 结果 2.5,而按 运算规 则,这个运算 是整数 类型运 算,得到结 果 是 整数类 型的 2。 该 问题可 以 用强 制类型 转换来 解决,把算式 写成 (float)5/2或者 5/(float)2。强 制类型转 换是 C语 言的 一个 一元运 算,优先级 为 14,它的形 式是: (类型 名) 该运算把它右侧的运算分量强制转换为括号内的类型名规定的类型。比如:   (char)(A + 0)     运算 结果为 char类型 小写的 p   (int)5.2/2 运算 结果为 int类型 的 2 使用强制类型转换要注意: (1)类 型名必 须用括 号括上 ,该运 算符就 是这种 形式。 (2)该 运算符 的级别 比较高 ,用括 号把它 的运算 分量括 上是一 种好的 习惯 ,否 则容易 出 错。 比如若 把 (char)(A + 0 )写成 (char)A + 0,则得 int类 型的 112。 3.赋值 转换 设有赋值运算   v=e 3.7 类 型 转 换 · 53· 若该 运算合 法,要求 e的 类型与 v的类 型赋值兼 容。赋 值兼容 的条件 如表 3.4所示 。 左端操作数类型 算术类型 结构体类型 联合体类型 T的 指针 void* 类型 T的 指针 任何类型 表 3.4 赋值兼容条件 右端操作数类型 算术类型 兼容的结构体类型 兼容的联合体类型 T的指针,其中 T与 T兼容 T的 指针 void* 类 型 null指 针常 量 在表 3.4中,有些 概念现在 还没有 接触 ,有 些以 后 本书 也不 会涉 及,全部 列 在这 里是 为 了读 者查阅 方便。 从赋值 兼容条 件可 以 看 出 ,简 单赋 值运 算 不能 直接 把 一 个 数 组全 部送 入 另一 个数组 中,但是却 可以把一 个 结 构 体 或联 合 体 送 入另 一 个 与 它 类型 兼 容 的 结构 体或 联 合体 中。当 e与 v赋 值兼容时 ,相应赋 值运算可 以进行 。赋值 运算的 动作是 : (1)计 算赋值 运算符 右端表 达式 e的 值; (2)若 e与 v赋 值兼容 ,把 e的值 按赋值转 换规则 转换成 赋值运 算符左 端 v的类 型; (3)把 转换后 的值送 入 v中。 经过赋 值运算 后,表达式“v = e”的值 为最后 送入 v中 的 值。在 赋值 运算 过程中 ,要 把 e的 类型转 换成 v的类型 。在上述 赋值兼 容条件 中,兼容的结 构体类 型和兼 容的联 合体类 型 最后 在成分一 级上还 是归于 算术类 型。所 以归根 结底赋 值类型 转换是 算术类 型 的 赋值类 型 转换 ,算 术类型 的赋值 类型转换 规则如 表 3.5所示。 v的 类 型 整数类型 整数类型 浮点类型 浮点类型 表 3.5 算术类型的赋值转换规则 e的 类 型 转换规则 整数类型 适应 v的长度,按补码方式直接转换 e的值为 v的类型 若 v的长度 >e的长度,则 e扩展到 v的长度; 若 v的长度 <e的长度,则从 e的尾部截 取合适的 长度数 据,并保持数值 e的符号,送入 v中 浮点类型 整数类型 浮点类型 把 e的值截尾取整,转换成 整数类 型;再 按照 v和 e都是 整数类型的规则进行转换 把 e的值直接转换成 v的浮点类型,送入 v中 适应 v的长度(精度),把 e的值转换成 v的类型 · 54· 第三章 简 单 程 序 3.8  输入 /输出 目前读 者已经 学习了 常 量、变 量、表达 式 和 表 达 式语 句 等 ,并 能 编 写 一 些简 单 的 程序。 但是 这些程 序不能 随机地 改变初 始数据 ,执 行结 果也 不 能以 理想 的 格式 输出 。 一个 计算 机 程序 应该能 对不同 数据进 行加工 ,并 能按用户 要 求 用理 想的格式 输出 加 工结 果。本 节介 绍 C语言 的输入 和输出 (简称 I/O),将能圆 满地解 决这一 问题。 输入是指 把数据 从外部 设备(例 如磁盘 、键 盘、磁带、传感 器等)上 读入计算 机内,针对 高 级语 言也可 以认为 读入程 序中某 变量内 。输 出 是 指 把计 算 机 内 部 的数 据 送 到 外部 设 备 (例 如磁 盘、显示器 、打 印机等 )上 去。 为了概 念的简 化和处 理的标 准化,C语言把 一切外 部设备全 部定 义为 文件,所以 C语 言 的 I/O全部 是针对 文件的 。输入 是 从文 件 将 数 据读 入 到 计 算机 内;输 出 是 将 计 算机 中的 计 算结 果送到文 件上。 普通用 于输入 的键盘 和用于 输出的 显示器在 C语言中分 别 被 定义为 标 准输 入文件 和标准 输出文 件。本 节 介绍 的 I/O全部 针 对 标 准输 入文 件 和 标 准输 出文 件,也 就是针对键盘和显示器。 C语言不 提供 I/O语句,而 是通过 标准函数 库中若 干标准 函数实 现 I/O。以下 讲述的 有 关 C语言 中实现 I/O的 操作看似 I/O语 句,实际都 是表达 式语句 ,相 应表达式 语句调 用 C语 言标准函数库中的标准函数。 C语言标 准包括 语言本身 和一组 标准函 数库,每个 标准 函数库 包含若 干 标 准函 数 ,这 些 标准 函数包 括字符 和字符 串处理 函数、输 入 /输出 函 数、数学 函数、日 期与 时间 函 数、动态 存 储分 配函数、控 制函数 ,等等 ,同 时各计 算机厂 商还为 自己推 出的系 统配备 大量 的标 准 函数, 放在 他们自 己定义 的各种 标准函 数库中 。 每 个标 准 函 数 库 都与 一 个 标 准 头文 件 相对 应,在 标准 头文件 中定义 相应库 中各个 函数的 特性,标 准头 文件 以 “.h”为 扩 展名 。 若想 使用 某 标准 函数库 中的函 数,必须使用 编译预 处理 命 令“#include”引 用相应 标准 函数 库的头 文件。 该命令的格式是:   #include<文件 名 > 包含标 准 I/O函数的 标准函 数库对 应的标 准头文 件是 “stdio.h”,所 以任 何 C语 言程 序 如果 使用 I/O函数 ,必 须引用该 标准头 文件,也就 是在程 序中必 须包含 如下程 序行:   #include<stdio.h> 该行 一般都 放在程 序的最 前边,这也是 前边例 题程序 都含有 这一行 的原因 。 3.8.1 字符 输入 C语言提 供的字 符输入函 数是一 个无参 函数,调用 该函数的 一般格 式是: 3.8 输入 /输出 · 55·   getchar() 其操 作是从 标准输 入设备 (也 就是键 盘)上读 入一个 字符,作为 函数值 。 读入的 字符一 般要保 存到一 个字符 型变量 中,若变 量 ch是 字符 类型 的,经 常以 如下 形 式使用该函数:   ch=getchar(); 这是 一个带 赋值运 算的表 达式语 句,作 用是读 入一个 字符送 入变量 ch中 。 3.8.2 字符 输出 C语言提 供字符 输出函数 putchar,调用 该函数 的一般 格式是:   putchar(int型表达式 ) 其操 作是把 int型表达 式 计 算 出的 值转 换成 字 符 类 型值 输出 到标 准 输 出 设备 (也 就 是 显 示 器)上 。如果操 作正 确,把 输 出 的 int型 整 数 作 为 函 数 值 ;如 果 错 误,则 EOF(-1)作 为 函 数值。 3.8.3 格式 输入 getchar和 putchar只能用 于输入 /输 出一个 字 符 ,以 下 介绍 的格 式输 入和 格 式输 出能 用 于各种类型数据。 标准函 数 scanf是 C语 言提供 的格式 输入函数 ,调用它的 一般格 式是:   scanf( <格式控制 >,<输入列表 > ) 其操 作是从 标准输 入设备 (键 盘)上读 入一系 列数 据,按 格式 控制 的要求 进行 转换并 送入 输 入列 表所列 的诸变 量中。 如 果输 入 操 作正 确,则 函 数值 为 输 入 的 数 据 个 数 ,否 则 函 数 值 为 EOF。 1.输入 列表 输入列 表由逗 号“,”分隔开的 若干输 入表项 组成,每个 输入 表 项是 一个 变 量的 指针 (变 量的 地址)。运 算符“&”是求变 量指针 的运算 。所以 输入列 表一般 应该有 形式 &v1,&v2,&v3,…,&vn 其中 v1,v2,… ,vn是 n个 变量。 2.格式 控制 格式控制 是一个 常量字 符串,其中 含有各 种以百 分号开 始的格 式控 制 符,表 3.6列出 了 常用 的 scanf函 数格式 控制符。 下述是 一个格 式控制 : “% d% c% f%d” 执行 scanf函数时 ,计 算机按 照格式 控制 中 控 制 符的 要 求 转 换 外部 设 备 上 的输 入 流 (实 际是 字符流 ),把 它变成 计算机内 部数 据。上 述格式 控制 将这 样转换 外部 设备上 的输 入:首 · 56· 第三章 简 单 程 序 先按 照整数 的语法 转换出 一 个整 数;再 转 换 出一 个 字符 ;再按 浮 点数 语 法 转换 出 一 个 浮 点 数;最后 再按整 数的语 法转换出 一个整 数。 显然,格式 控制符 要与输入 列表上 的变量 匹配,事实 上格式 符是根 据输入 列 表 上相应 变 量的 需要来 安排的 ;输 入数据要 满足输 入列表 上变量 和格式 控制的 要求。 输入数据类型 整数 单个字符 字符串 浮点数 表 3.6 常用的 scanf函数格式控制符 输入要求 带符号 10进制整数 无符号 10进制整数 以小数形式或指数形式 格式控制符 %d %u %c %s %f %e %g 设 i为 int类 型变量 、ch为 char类型 变量、v为 float类型变 量、k为 int类型 变量,键盘 上 输入数据为   1234 123e+2  987 则函数调用   scanf("% d% c%f% d",&i,&ch,&v,&k) 的结 果是,变量 i得 到 整数 数 据 1234;变量 ch得 到 字 符 型 数 据 空 格 (ASCII码 32,是 紧 跟 1234的 4后边 的那个 空格字 符);v得到 浮点型数 据 12300;k得 到整数 数据 987。 3.8.4 格式 输出 标准函 数 printf是 C语言提 供的格 式输出 函数,调用 它的一 般格式 是:   printf( <格式控制 >,<输出列表 > ) 其操 作是按 照格式 控制的 要 求,把 输 出 列 表 上的 数 据转 换 成 字 符 串,并 送 入标 准 输 出 设 备 (显示 器)上输出 ,如果输 出操作正 确,则函数 值为输 出的字符 个数,否 则函数值 为 EOF。 1.输出 列表 输出列表 由逗号 “,”分隔开的 若干表 达式组 成。每 个表达 式计算 出一个值 ,该 值将被 按 照格 式控制 中相应 控制项 格式符 的要求 进行转换 ,变 成字符 流被输 出。输 出列表有 形式   e1,e2,e3,…,en 其中 e1,e2,… ,en是 n个 表达式 。 2.格式 控制 printf的格式控 制与 scanf的格 式控 制一 样 ,也 是一 个常 量字 符串。 其中 含 有任 意普 通 3.8 输入 /输出 · 57· 字符 和各种 以百分 号开始 的格式 控制符 。表 3.7列出常 用的 printf函 数格式控 制符。 格式符 d u c s f e 表 3.7 常用的 printf函数格式控制符 使用形式 %d % md % -md %u % mu % -mu %c %s % ms % -ms %f % m.nf % -m.nf %e % m.ne % -m.ne m省缺值 1 字符串 长度 N省 缺 值 说明 以带符 号 十 进 制 形 式 输 出 整 数 (正 数 不 输 出 符号) 以 无 符号 十 进 制 形 式输 出 整 数 以 字 符 形 式 输出 一 个 字 符 :c 输 出 字 符 串 :cc… c 6 以 小 数形 式 输 出 实 数 :±xx… x.xx…x 6 以 指 数形 式输 出 实 数:±x.xx… xe±xxx 在格式符 的各种 使用形 式中,d、u、c、s、f、e是 格 式符 ;m 和 n是无 符号整 数常 量,表 示输 出宽度和 小数位 数;负号“-”是对齐 方式,它们 共同确定 输出格 式。一 般格式 转换都 把 数据 转换成 m 个字符 的字符 串,并按右 对齐的 方式输 出。 (1)负 号“-”表示 该输出 项以左 对齐方 式输出 。 (2)m称 字段宽 度,表示相 应输出 项所占 字符个 数。 ① 若 m大 于数据 长度,则以 空格补 齐。 (a)不带 负号“-”在左端 补空格 ,数 据按右 对齐的 方式输 出; (b)带 负号“-”在右端 补空格 ,数据 按左对 齐的方 式输出 。 ② 若 m小 于数据 长度,则突 破 m的限制 ,输 出足够 表示数 据的字 符串。显 然,字段 宽度 m 值不应 该小于 数据长 度,这 样会使 输出格 式难看 。 (3)n表示小 数部分 占用的 字符位 数。 下述是一个格式控制:   "num1=%2d flag= %c\n area=%10.3f num2=%5d\n" 执行 printf函 数时,格式 控制中 的普通 字符将 原封不 动地 输出 到外部 设备 上去,格式 控制 中 的格 式符用于 控制对 输出列 表上数 据的转 换。计 算机按 照格式 控制中 控制符 的 要 求转换 输 · 58· 第三章 简 单 程 序 出列 表上的 诸表达 式,把它们变 成字符 流送到 标准 输 出设 备(显 示器 )上输 出 。上述 格式 控 制应 该对应的 输出列 表是:一个 整数表 达式、一个 字符表 达式、一个 浮点表 达式 ,再 一个整 数 表达 式。该 格式控 制将按 如下步 骤执行 输出和转 换各个 表达式 的值,并送 到显示器 上: (1)输 出字符 串“num1=”; (2)按 整数格 式转换 输出列 表上的 第一个表 达式的 值,转换成 一个 2字符长的 字符串 ; (3)输 出字符 串“flag= ”; (4)按 字符格 式转换 输出列 表上的 第二个表 达式的 值,转换成 一个字 符; (5)输 出字符 串“ \narea=”; (6) 按 浮 点 格 式 转 换 输 出 列 表 上 的 第 三 个 表 达 式 的 值 ,转 换 成 一 个 10字 符 长 的 字 符串; (7)输 出字符 串“num2=”; (8)按 整数格 式转换 输出列 表上的 第四个表 达式的 值,转换成 一个 5字符长的 字符串 。 显然,格式 控制符 要与输出 列表上 的表达 式匹配 ,事 实上是 根据输 出列表 上 输 出的需 要 来安排相应格式符的。函数调用   printf(" um1=%2d flag= %c\n  area=%10.3f  num2=%5d\n" ,25, A ,123.0/2,987); 将产 生如下 输出结 果(“\n”是转 义字符 ,表 示行结 束):   ‖□□num=25□□flag= A ‖□ □ area=□ □ □ □61.500□ □ num2=□ □987 例 3.3 修改 3.6节 例 3.2,使之 适用于 任意长 、宽 、面 积,并产生 一个较 好的输 出格式 。   #include"stdio.h" #include"math.h" void ain(){ floatx1,x2,b,d; floatlength,width,area; printf("pleaseinputlength,width,area:\n"); scanf("% f%f% f",&length,&width,&area); b=-2.0* (length+width); d=sqrt(b* b-4.0*4.0*area); x1=(-b+d)/(2*4); x2=(-b-d)/(2*4); printf("x1=%.1f  x2=%.1f\n",x1,x2); } 该程序与 3.6节中例 3.3的 程序相 比,它用三 个变量 代替了 三个常 量,并 且 应 用了输 入 语句。执行过程是: 首先将在终端屏幕上显示一行提示: 习 题 三 · 59·   Pleaseinput:lenth,width,area: 这时 计算机处 于等待 输入状 态。操 作员应 顺次从 键盘键 入矩 形 方框 的长、宽和 面积;当键 入 结束 后(比如已 经键入 了:503050),计 算机继 续向下 运行,产生 如下输 出:    x1=36.6□□□x2=□□3.4 ‖■ 到此 程序执 行结束 。显然 该程序 每次运 行可以针 对不同 的长、宽、面积 进行计 算。 在输入 语句之 前 的 输 出 语 句 产 生 一 行 提 示 信 息 ,这 不 是 必 须 的 ,但 确 是 一 个 良 好 的 习惯。 本章小结 本章讲述 最简单 的 C语 言程序 ,包 括常量 、变 量、表达式、表 达式语 句、赋值、I/O以及 混 合运算中的类型转换。重点掌握说明格式和表达式的构成。 习 题 三 3.1  什 么是 常 量 ?常 量 定义 的 实 质 是 什 么 ?下 述 常 量 定 义哪 些 是 错 误的? 为 什 么 ?    definebase =173.5; #defineratio1/3; #define1st_character S| #definecount999 #definevowelsratio #definewhile7 3.2  什 么是 变 量 ?变 量 的“名 ”、“地 址”、“存 储 区 ”和 “值 ”各是 什 么 意 思 ? 有 什么 区 别 ? 3.3  怎 样区 分 变 量 和常 量? 它 们 有 什 么 不 同? 3.4  试 述类 型 和 变 量的 关系 。 3.5 以 int类型为例,说明类型本身、类型变量、类型常量之间的区别。 3.6  找 出下 列 变 量 说明 的错 误 :    loatrateofinterest A,differencc:float; charleate,dig,spa; chardigit,while; char A , B , C; chardig,real; 3.7  写 一个 变 量 说 明部 分,说 明 : · 60· 第三章 简 单 程 序 两个 integer型变量 multiplier和 error; 一个 char型变量 signal; 三个 real型变量 langth,width,height。 3.8 三条线段长分别为 a、b、c,写出它们构成三角形条件的逻辑表达式。 3.9  写 出下 列 逻 辑 表达 式: (1)i被 j整除; (2)m 是 偶 数 ; (3)y∈/[-100,-10],且 y∈/[10,100]; (4)实数 x等于圆周率 π; (5)n是小于正整数 g的偶数。 3.10  用 逻 辑 表 达式 描 述 下 述线性 方 程 组 无 解 、有 惟 一 解 、有 无 穷多 组 解 的 条 件 。 ax+by=c        dx+ey=f 3.11  x为 何 值 时 ,可 能 怀疑 下 述 表 达 式 的 有效 性 和 精 确 度 。 (1)exp(x)-exp(-x) (2)(x-1)/(x+1) (3)1-x+x2 /2! -x3 /3! +… +(-1)n xn/n! 3.12  改 写 下 述 表达 式 ,使表 达 式 意 义 不 变,所 含括 号 最 少 。 (1)(d+w)* (e/u) (2)(x*y* z)/(u/v) (3)(a*(b/(c* (d/(e*f))))) (4)(((((a1x+a2)x+a3)x+a4)x+a5)x+a6)x+a7 3.13  计 算 下 述 表达 式 值 。 (1)x+a%3* (int)(x+y)% 2/4             设 :x=2.5、y=4.7、a=7 (2)(float)(a+b)/2+(int)x% (int)y 3.14  用 赋 值 表 达式 表 示 下 列计算 。 设 :a=2、b=3、x=3.5、y=2.5 (1)s=n(n2-1) (2)y=xa+bc (3)s= p(p-a)(p-b)(p-c) (4)x=(ln a+d2 -e26)5/2 (5)y=sainxx+ cosπ2x (6)y=23πh[3(α2 +β2)+h2] (7)R= 1 1 R1 +R12 +R13 (8)y= x 1+3+5+(7x2(+x2)(x42)x3)2 3.15  编 写 程 序 ,输 入 一 个字 符 ,然 后 顺 序 输 出 该 字符的 前 驱 字 符、该 字 符本 身 和 它 的 后 继 字符。 3.16 编写程序,将输入的任意英文单词翻译成密 码文。翻译 规则是,把所 有字母 用它后 面第 3个 字 符替换,并假设字符 a接在字符 z后边。例如 zero将被翻译成 chur。 习 题 三 · 61· 3.17  找 出 你 使 用的 计 算 机 系统的 下 列 值 : (1)允 许 的 最 大 整 数 ; (2)最 小 的 负 整 数 ; (3)允 许 的 最 大 实 数 ; (4)实 数 的 近 似 精 确 度; (5)不等于 0的mXm的最小值,其中 X是实数; (6)使计算机能区别 1和 1+ε的 ε最小值; (7)能 表 示 的 字 符 和 它们的 编 码 。 3.18  下 述 语 句 输出 什 么 ?     printf("%d = %dis%d\n",32+18,50,32+18=50) 3.19  编 写 一 个 按如 下 格 式 打印通 信 地 址 的 程 序 。        JENNIFER J.JONES       496W.CEDAR STREET.        HOMETOWN,IDWA42599 3.20  编 写 一 程 序,读 入 角度 值 ,输 出 弧 度 值 。 3.21  编 写 一 程 序,读 入 弧度 值 ,输 出 角 度 值 。 3.22 不用中间变量,交换 A、B两整数型变量的值。 3.23 用如下公式计算 π值。 π 4 =4arctg15 -arctg2139 3.24  编 写 程 序 ,输 入 底 的半 径 和 高 ,求 圆 柱 体 的体 积 和 表 面 积,并 输 出 。 3.25 已知摄氏温度(℃ )与华氏温度(F)的转换关系式是: C= 95(F-32) 编 写一 个 摄 氏 温 度 与 华氏温 度 的 转 换程 序 ,输入 摄 氏 温 度 (℃ ),输 出 华 氏 温 度(F)。 3.26  编 写 程 序 ,输 入 一 个英 寸 数 ,把 它 换 算 成     #哩 #码 #英尺 #英寸 形 式,并 输出 。 3.27 从点(x0,y0,z0)到平面 Ax+By+Cz+D=0的距离 d的公式是 d=|Ax0 +By0 +Cz0 +D| A2 +B2 +C2    编写程序,定义平面方程系数 A、B、C、D为常量,输入点的坐标 x0、y0、z0,计算并输出 d。 3.28 绝对温度 T的黑体辐射源在波长 λ处的发射功率 E为 E=e2cπh/cBλhTλ--15 其中 c=2.997924×108是光速; h=6.6252×10-34是 普 朗 克 常 数; B=5.6687×10- 8是 波兹 曼 常数 。 编写程序,在常量定义部分定义常量,给出恰当的标识符并在变量说明部分说明,输入波长 λ,计算并 · 62· 第三章 简 单 程 序 输出发射功率 E。 3.29 设表达式可以包含指数运算 a↑b = ab,它是一个算术运算,其优先级在 13级和 14级之间,即 高于乘法运算低于类型转换运算。给出适合这种改动的表达式的 BNF。 3.30 编写程序,求正棱台 的对 角线 长。要求 在键 盘上 随机 输入正 棱台 的上 底边 长、下底 边 长和 斜 边长。 第四章 函  数 4.1 带 子程 序的 C程 序 例 4.1 编程 序计算 y(x) = p2(x) +5x ·p(x+2) p(x+5)- x 其中 p(u) =f(u×0.3,2xx+u)+u/2 f(v,w) = w+v 7v 解:根据前 边学过 的知识,编 写程序 如下:    /* PROGRAM example401*/ #include"stdio.h" #include"math.h" void ain(){ floaty,f,p0,p2,p5,x; printf("pleaseinputx:"); scanf("% f",&x); f=((x+x)+(x*0.3))/(7* (x*0.3)); p0=(f+x/2)/(2* x); f=((x+(x+2))+((x+2)* 0.3))/(7* ((x+2)* 0.3)); p2=(f+(x+2)/2)/(2* x); f=((x+(x+5))+((x+5)* 0.3))/(7* ((x+5)* 0.3)); p5=(f+(x+5)/2)/(2* x); y=((p0* p0+5* x)/(p5-sqrt(x)))* p2; printf("y(x)=%8.4f\n",y); } 可以看出 ,程序 中求 p(u)和 f(v,w)时几乎 相同的部分 照写了三 次,使程序加长,这是 一种 浪费 。分析这三 部分可知 :其基本 形式一 致,只是参与运算 的数据分别 为 x、x+2、x+5。 为了 简化 程序,一般程序设 计语言都为 这种情况 (计 算过程一致,而 参与运算 的数据不 同)提供 一种 · 64· 第四章 函  数 机制 ———子程 序。在 C语言 中子程序 体现为函数 (function)。下面 用函数来写 这段程序 。    /* PROGRAM example401A*/ #include"math.h"                        * 1*/ #include"stdio.h" /* 2*/ floatx,y; /* 3*/ floatf(floatv,floatw ){ /* 4*/  return(w+v)/(7* v); /* 5*/ } /* 6*/ floatp(floatu){ /* 7*/  return(f(u* 0.3,x+u)+(u/2))/(2* x); /* 8*/ } /* 9*/ voi main(){ /* 10*/ printf("pleaseinputx:"); /* 11*/ scanf("% f",&x); /* 12*/ y=(p(x)* p(x)+(5* x))/(p(x+5)-sqrt(x))* p(x+2); /* 13*/ printf("y(x)=%8.4f\n",y); /* 14*/ } /* 15*/ 这个程 序与 example401对照起 来,显 然干 净、利索、清晰 ,既好读 又好 看,并且与 原题 目 有相当高的可对照性。具体分析该程序: 第 7~9行 是一个 函数定义 ,定 义函数 p。 它有自 变量 u。函数 的自变 量称 为形 式 参数, 简称形参。 第 13行 的 p(x)称 为函数 调用。 它计算当 自变量 u取 x值 时,函数 p的值 。在计 算函 数值 时,替换自 变量的 部分称为 实在参 数,简称实 参。这 个 计算 过 程 称为以 x作 实 在参数 调 用函 数 p。显然,p(x+5)和 p(x+2)都是函 数 调用 ,它们分 别以 x+5和 x+2作为 实在 参 数调 用函数 p,计算 当自变量 u取 x+5、x+2时函数 p的值。 在函数 p内 : 第 7行的 “floatp(floatu)”称为 函数定 义说明 符。“float”定义本 函数的 类型为 浮点型 ;p 是函 数名字 ;括 号括起 来的部分 称为形 式参数 表。在 形式参 数表中 ,u为形 式参数 ;“float”声 明形 式参数 u为 浮点型 。 从第 7行 的“{”到第 9行 的“}”,是 函数 p的 函数 体。函 数体由 一个 复合语 句构 成,为 函数 的操作 部分,具体 规定函数 p的操 作及其 值的计 算。 第 8行是 返回语 句“return(f(u*0.3,x+u)+(u/2))/(2* x);”,它带着 表 达 式的值 作 为函 数值返 回。该 语句中 的 f(u* 0.3,x+u)也是 一个函 数调用,它以 u*0.3和 x+u为 实 在参 数调用 函数 f,计算 f的值 。 第 4~6行 定义函 数 f。 程序 example401A的执行 过程是 : (1)从 主函数 的第一 个语句 (第 11行)开始,执行 “printf("pleaseinputx:");”输 出一 4.1 带子程序的 C程序 · 65· 行提示信息: pleaseinputx: (2)执 行函数 调用“scanf("%f",&x);”进 行输入 ,等 待操作 员键入 x值 。 (3)当 操作员 键入一 个 x值并 回车 后 (为 叙 述 方便 ,设操 作 员 键 入 1.0),执 行第 13行 的表 达式语 句。在 执行表 达式语 句过程 中,将 分别以 x、x+5、x+2为 实在参 数调用 函数 p, 分别 计算出 当 p的 自变量 取这几 个值时 的函数值 。 以 x+5为例 说明调 用函数 p的 执行过程 :   (3.1)计算 x+5得 6.0。   (3.2)将 6.0送入 p的形 式参数 u中,这时 u的值 为 6.0。   (3.3)进入 函数 p,执行 p的 操作部分 ,即返回 语句“return… ;”,这时要 先计算 返 回语句中表达式的值。      (3.3.1)以 u*0.3和 u+x为实在 参数调 用函数 f。          (3.3.1.1)顺 序计算 u*0.3、u+x得 1.8、7.0。           3.3.1.2)将 1.8、7.0顺 序送入 f的形式 参数 v、w中,这时 v=1.8、w =7.0。          (3.3.1.3)进 入函数 f执 行 f的 操作部分 ,执行返 回语句“return… ”。          (3.3.1.4)计 算出表 达式的 值为 0.698413。          (3.3.1.5)带 着函数 值 0.698413返 回调用处 :p中 的返回语 句内。      (3.3.2)计算 出返回 语句中 表达式 的值为 1.84921,也就是 函数 p的值。   (3.4)带着 函数值 1.84921返 回调用处 :主 程序第 13行 的表达 式语句 内。 计 算 p(x)得 0.797619。 计 算 p(x+2)得 1.13889。 (4)计 算出表 达式语 句的赋 值运算 符右端表 达式值 7.55882,送入 变量 Y中。 (5)执 行输出 函数 printf在屏 幕显示 出 Y=7.5588 调用函 数,称 为激活 (活化)一 个函数 ,并且 称一个 正在 执行 的函数 为该函 数 的 一 个活 动 (activation)。在 函数 的一 个活 动开始 后,形 式 参 数 用实 在 参 数 带 进来 的 信 息 参 与进 一步 的运算 。调用 函数的 执行过 程如图 4.1所示 。 在 C语言中 ,使 用函数 由两 部 分 组 成:函数 定义 和 函数 调用 。本 例 第 4~6 行 和 7~9行 是 函 数 定 义。 本 例 的   图 4.1 调用函数的执行过程 p(x)、p(x+2)、p(x+5)和 sqrt(x)等 是函数调 用。 sqrt()是 C系 统 提供 的标 准函数 ,该 函数的 定义在 数 学 函数 库中,在本程序中 使用了该函 数,所 以用“#include”命 令把数 学函 数库的头 文件 “math.h” · 66· 第四章 函  数 括入程序之中。 在软件发 展史上 ,引进子程 序概念 是一个 重大成 就,对程序 设计技 术的发 展 有 着重大 影 响,它是 模块化 、分 块编译 、逐步 求精 等技 术 的基 础。 子 程序 技术 起源 于 前述 的原 因———简 化代 码、缩小代 码长度 以及数学 中的函 数概念 。但后 来随着 子程序 技术的 发展 ,尤 其是在 结 构化 程序设 计中,它已 不仅仅是 为 了 简 化 代 码,而 被 提 升 到 程序 设 计 抽 象 的高 度 来认 识了。 因此 ,不 但简化 代码时 使用子程 序,而 且 为 了 程序 概 念 的 抽 象和 程 序 的 可 读性 及 清晰 性,有 时看 起来操 作极简 单,不 必 使用 子 程序 ,甚至 只 有一 小 段 代码 的 地 方 也 引进 子 程序 。 例 如 example401A中的 函数 f,只计算 一个表 达式,而且 只有一 个地 方调用 它,完全 可以不 引进 函 数。 但是引 进函数 后,使得程序 与 问 题本 身具 有 极 强 的可 对 照 性 ,十 分清 晰,从 而增 加了 程 序的 可读性 ,也 为程序 的正确性 打下了 良好的 基础。 子程序 技术将 “做 什么”与 “怎么做 ”分离 开 来。在 程序 设计过 程中,当为 了要 完成某 一 操作 而调用 一个子 程序时 ,程序 员 只 关 心 要 做什 么 即 可,而 对于 怎 样 去 完 成这 一 操作 ,则 完 全不 必操心 。程序 员可以 集中精 力考 虑 高 一 层次 的 算 法 ,不 必为 具体 的 某 一 微 小细 节而 困 扰住 。然后 ,当 回过头 来(或由别 人)设 计 子程 序本 身时 再具 体关 心 “怎么 做”,设计 具体 算 法、具体 考虑怎 样去实 现总体上 的要求 。使用 子程序 技术将 “做什 么”与 “怎么做 ”分离开 来 具有如下优点: (1)程 序的逻 辑结构 清晰,程序易 写、易读、易懂 。 (2)程 序的设 计、调试和维 护变得 更加容 易。 4.2 函    数 在数学 中可定 义函数 ,例如 f(x,y)=xy 若计算 2的 4次方则 表示成 f(2,4)。 程序设计语言中函 数的概念来 源于数学 中的函数。 在 C语言中 ,使 用函数 必须先 定义该 函数(标 准库函 数除外 )———类 似于 数学中 的函 数 定义 。然后再 在表达 式中调 用该函 数——— 类似于 数学中 计算 某 函数 的一个特 定值 。下面 介 绍 C函数的 定义及 调用形式 。 4.2.1 函数 调用 在 C语言中 ,当 调 用 一 个 函数 时,首 先把 函 数 调 用 (function-designator)中 实 在 参 数 表 (actual-parameter-list)的各实 在参数 (actual-parameter)信 息传入 函数形 式参数 表 的 形式参 数 中去 ,然 后进入 函数执 行复合语 句。这 与数学 中计算 一个函 数值时 ,首 先用一 些 值 替换函 数 定义 中的函数 自变量 ,然后再计 算函数 值十分 类似。 函数调 用本身 是后缀 表达 式,它的简 单 4.2  函   数 · 67· 语法是:    <函数调用 > → <后缀表达式 >( <表达式列表 > ) <后缀表达式 > → <基本表达式 > <基本表达式 > → <标识符 > <表达式列表 > → 表达式 > x <表达式 >,<表达式列表 > 由上述 语法可 知,函数调用 的形式 是:   F(U,U,…,U) 或   F() 其中: (1)函 数标识 符 F是欲调用 的函数 的名字 ; (2)每 个 U都是表 达式,分别 为一个 实在参 数; (3)实 在参数 表 U,U,…,U列出调 用函数时 传入相 应函数 中的信 息。 函数调 用的目 的是计 算一个 函数 值 ,然 后将 这个 值用于 表达 式中参 与进 一 步的 运算 及 操作 ,调 用标准 函数也 是函数调 用。程 序 example401A中的 p(x+5)和 f(u*0.3,x+u)以 及 exp(u)等都是 函数调 用。 4.2.2 函数 定义 除标准 库函数 外,程序中 使用 函 数 必 须先 定 义,然 后 再用 “函数 调 用 ”调 用它。 在程 序 example401A中,4~11行定义 函数 p,5~8行 定义函 数 f。标 准函数 是系 统 已经定 义好的 函 数,不必 定义即 可直接 调用。下 边介绍 C函数 定义的 形式。 1.语法 简单的 函数定 义(function-declaration)的语法 如下:    <函数定义 > → <函数定义说明符 ><复合语句 > <函数定义说明符 > → <声明说明符 > <声明符 > <声明说明符 > → <类型说明符 > <声明符 > → <直接声明符 > <直接声明符 > → 函数声明符 > x <简单声明符 > <函数声明符 > → 直接声明符 > (<参数类型列表 >) x <直接声明符 > ( <标识符列表 > ) <简单声明符 > → <标识符 > <参数类型列表 > → <参数列表 > <参数列表 > → 参数声明 > · 68· 第四章 函  数 x <参数列表 >,<参数声明 > <参数声明 > → 声明说明符 ><声明符 > x <声明说明符 > <复合语句 > → { <声明或语句列表 > } 这里给 出了简 单的 C函 数定义 的语法,完整 的 C函数 语法太 复 杂 ,这 里不 详细介 绍,有 兴趣 的读者 可参见 附录二 。由上 述语法 可知,一个函 数定义 的形式 是:   类型说明符 标识符 (参数列表 ) 复合语句 2.函数 定义说 明符 上述形 式的第 一行称 为“函数定 义说明 符”(function-def-specifier),具 有形式 :   TTF(参数列表 ) 具体指明以下几点: (1)函 数的结 果类型 ———由 “类型说 明符”(TT)标明; (2)函 数的名 字——— 由类型 说明符 后的“标识 符”(F)标明 ; (3)函 数的形 式参数 个数和 每个形 式参数的 特性———由“参 数列表 ”标 明。 3.函数 类型 函数类 型不能 是数组 类型、函数类 型。 有些函数 是无值 的,也可以 说是无 类型的 ,这 可能是 问题 的 算法 本身决定 的。 无类型 函 数在 函数定义 时,其结 果类型的 “类型说 明符”使用 空 类 型 符 “void”。前边 的例 题中 把 main 函数 看成是 无类型 函数,所以函 数类型 都使用 类型符 void。 4.参数 列表 从语 法 可 知,函 数 定 义 说 明 符 的 参 数 列 表 (parameter-list)由 一 个 个 参 数 声 明 (parameter-declaration)组 成,各个参 数声明 之间 以 逗号 “,”分隔。 每个 参数 声 明具 体说 明 一个 形式参 数的特 性(类型),参数 声明的 一种形 式如下 :   类型说明符 标识符 显然 ,参 数列表 具有形 式   Tid,Tid,…,Tid 其中: (1)id是标 识符,为一 个形式 参数; (2)T是 类型说 明,它指出 形式参 数 id的类 型。 C语言允 许使用 无参函数 ,无 参函 数 的参 数 列表 为 空 ,或 使用 “空类 型 ”的 类 型 说 明 符 “void”。 从某种 意义上 讲,C语 言参数 种类十 分简单 ,只 有 值 参数。值 参数 表 示形 式参 数是一 个 4.2  函   数 · 69· 局部 于函数的 变量;当 调用函数 时,把实在 参数的 值传入 形式 参 数表 示的变量 中去 。前文 讲 函数 调用时 讲到,“在 C语言 中,当 调用一 个函数 时,首先把 函数调 用中实 在参数 表的各 实在 参数 信息传 入被调 用函数 的形式 参数表 的形式 参数中 去”。实 际上 ,就 是“计 算各个 实在 参 数表 达式的 值,把它们 传送到对 应的形 式参数 变量中 去”。 5.复合 语句 复合语句 (compound-statement)由 声明和语 句列表组成 。其声明部分具体 说明本函数 内使 用的 其他量;语句部分 规定在本函 数中要 执行的算法动 作,即描述本函 数的具体 实现算法。 综上所 述,函数定 义具有形 式:   TTF( id,Tid,…,Tid){ … } 其中: (1)TT是类 型说明 符,具体说 明函数 的类型 ; (2)F是 函数名 字; (3)Tid,Tid,…,Tid是 参数列 表; (4){… }是复合 语句。 6.参数 结合 C语言规 定,从 程序静 态行文 上看,函数 调用中 实在 参数表 的实在 参 数与 相应 被调用 函 数的 函数定 义中形 式参数 表的形 式参数 ,按 位置 从左 向 右依 次一 一 对应 。 并 且 还要 求对 应 位置上的实在参数表达式与形式参数之间要赋值兼容。 当程序 运行时 ,调 用函数,进 行参数 结合的动 作是: (1)计 算实在 参数表 达式的 值。 (2)把 实在参 数的值 按赋值 转 换规 则转 换 成形 式参 数的类 型。 如果 不能 完 成该 转换, 则称 函数参 数不一 致,产生错误 。 (3)把 转换后 的实在 参数值 送入形 式参数中 。 C语言没 有规定 当实在参 数不止 一个时 ,计 算 实在 参数 的次序 ,甚 至 没有 规定 函数名 和 实在 参数之 间的计 算次序 。 这意 味 着,函 数 名字、各 个 实 在参 数 的计 算 次 序是 依 赖 于 实 现 的。可能从左 向右计 算,也可能 从右向 左计算 ,还 可能同 时 并行 计 算 ,等 等。计 算次 序 不同, 可能 导致各 个实在 参数的 值不同 ,也 可能 导 致调 用的 函 数不 同。 读者 要 注 意 ,函 数名 、各 个 实在 参数的 值千万 不要依 赖于计 算它们 的次序。 下述例 4.2说明 这个问 题。 例 4.2 实在 参数值 可能依 赖于计 算次序 的实例 。   #include"stdio.h"           * 1*/ intx,y; /* 2*/ intf(intz){ /* 3*/ · 70· 第四章 函  数    x=x+1;    return(z*z); } intg(intu,intv){    ⁝ } voidmain(){    x=0;    g(x,f(2)); } /* 4*/ /* 5*/ /* 6*/ /* 7*/ /* 8*/ /* 9*/ /* 10*/ /* 11*/ /* 12*/ /* 13*/ 该程序 第 12行调 用函数 g,当 实在参 数从左 向右计 算时,g的 两个参 数 u、v分别得 到值 0、4;当实在 参数从 右向左 计算时 ,g的两个参 数 u、v分别得 到值 1、4;当实 在 参 数并行 计 算时 ,g的两个 参数 u、v分别得 到的值 是不确 定的,这要 看哪边 计算得 快了。 7.函数 返回 函数调 用进入 函数执 行后,到一定 步骤应 该返回 到调用 处。C函数 返回有 两种方 式: (1)执 行返回 语句,返回语 句的形 式是   return; 或   return表 达式 ; (2)函 数运行 到复合 语句末 尾。如 果在复合 语句中 没有返 回语 句,或 虽然 有返 回 语句, 但是 在函数执 行期间 没有执 行到任 何一个 返回语 句,则当函 数执行 到复合 语句末 尾(最后 那 个闭 花括号 “}”)后 ,即返回到 调用处 。 本章例 4.1程序 example401A 中第 5行语句 “return(w+v)/(7*v);”是函 数 f中的 返 回语 句,第 8行 语句 “return(f(u*0.3,x+u)+(u/2))/(2*x);”是 函数 p中 的返回 语句。 8.函数 值 如果函 数有值 ,应 该把函数 值带 回调 用 处。 C语言 使 用带 表达 式的 返回 语 句向 调用 函 数的 主程序 传递函 数值。 为了将函 数值传 回函数 调用处 (为 了带 回一个 值),在 复合 语句中 , 应该 至少有一个 (当 然可以有 多个)带表达式的返回 语句,带表达式的 返回语句的 执行过 程是: (1)计 算表达 式的值 ; (2)把 表达式 值按赋 值转换 规则转 换成函数 的结果 类型; (3)用 类型转 换后的 值作为 函数值 ,并带 着它返 回到调 用该函 数处。 在函数调 用处,作为运算分 量,函数调用的 值是带回 的函数值,并用 它参与进一步 的运算 。 如果函数 执行不 带表 达 式的 返 回 语句 ,或者 没 有执 行 返 回 语 句,而 执 行到 复 合 语 句 最 后,遇到 闭花括 号“}”,返 回 函 数 调 用处,这 两种 情 况函 数都 无值 可以 带 回。 如 果是 无类 型 函数 ,可 能在函 数调用 处也不需 要函数 值,这种返 回是正 常的;如果 是有类 型函 数,在函数 调 4.2  函   数 · 71· 用处 极可能 正需要 函数值 参加进 一步运 算,这 将带来 不可预 料的结 果,读者一 定要注 意。 如果函数 有返回值,显然要 求返回语句中表达 式的类型与 函数的结 果类型必须 赋值兼容。 4.2.3 函数 原型 前面举 的一切 程序例 子,从 行文 上看,任何 函数 的函 数调用 都在 相应 函数 定 义之 后,这 是有 意安排的 。因为 C语言为了 给编译 系统提 供方便 ,规 定任何 标识符 都必须 声明,而且 必 须先 声明后 使用。 这样做 正 好可 以满 足 C语 言的 这个 规 定。 但 不是 所 有 程序 都 能 做 到 这 点,即使 经过精 心安排 能够做到 这点,对于 大程序 也十分 困难。 这就有 可能使 得 有 一些函 数 的调 用在其 定义之 前出现 。为了 解决这 个 问题 ,C语言 引 进 “函 数原型 ”的 概 念。函 数原 型 放在 函数调用 之前先 声明相 应函数 的特性 ,满 足了 C标识 符先定 义后使 用的要 求,并向编 译 系统 提供所 调用函 数的类 型、参 数 个 数、每 个 参数 的 特 性 等 一切 信 息 ,以 便 编 译 系统 在处 理 相应函数调用时使用。这 样相 应函数的 定义就 可以 放在 任何 位置了。 函数原 型是一个 声 明,在程 序中可 以放在 任何能够 放 置 声 明 的 位置 ,可 以 作 为 顶层 声 明 ,也 可 以 作 为一 个函 数 的内 部声明 放在复 合语句 之内,总之 只 要 放 在相 应函 数 调用 之前 即 可。 从 程 序 设计 风格 考 虑,最好 把所有 函数原 型集中一 起,放在 主函数之 前。函 数原型 的语法 是:    <声明 > → <声明说明符 ><初始化声明符列表 > ; <初始化声明符列表 > → <初始化声明符 > x <初始化声明符列表 >,<初始化声明符 > <初始化声明符 > → <声明符 > 这个 语法结 合前述 函数定 义中的 语法,可以推 出函数 原型一 般具有 的形式 :   TTF(T,T,… ,T); 或   TTF(Tid,Tid,…,Tid); 其中: (1)TT是类 型说明 符,说明函 数的类 型; (2)F是 函数名 字; (3)id是标 识符; (4)T是 类型说 明,它指出 相应位 置上形式 参数的 类型。 简单的说 ,函数原 型就是在 函数定 义说明 符后加 一个分 号,并作为 一个声 明 出 现在程 序 中。一般情况 下,函数 原型使用 第一种 形式。 第二种 形式的 函数原 型,相应参 数 标 识符也 不 起作 用,因为函 数原型 只需要说 明参数 个数和 每个参 数的特 性,而不关 心相应 参 数 是什么 名 字。如下两个函数原型等价:   floatf(int,float,int,char); floatf(intz,floatu,intv,charw); · 72· 第四章 函  数 4.3  程序 设计 实例 例 4.3 生成 伪随机 数。 实际应 用中经 常用到 随机数 序列,一般 计算机 上都 有随 机数发 生器,并 且在一 般程 序 设计 语言的 实现中 都有一 个产生 随机数 的函数,用 户在使 用时不 断 调 用该 函 数即可 得到 一 个随 机数序 列。下 面脱离 机器的 随机数 发生器,编 写一个 产生随 机数 的函 数 。用于 产生 随 机数 的最普 遍 的 方 法 是 线 性 同 余 法,该 方 法 基 于 如 下 公 式 计 算 一 个 随 机 数 序 列 的 第 k 项 rk:   rk = (multiplier* rk-1 + increment)MODmodulus 其中 ,rk-1是随机 数序列 的第 k-1项;multiplier、increment、modulus是常 数 。如果 给定 一 个初 始值 r0,通过反 复使用该 公式进 行计算 ,便可以 得到一个 随机数 序列。 事实上 ,由 于给 定 r0后,以后 的任意 一项 rk总 能预先 知道,所以 用这个 公式产 生的序 列只能 称 为 伪随机 数 序列 ,序 列中的 一项也 只能称 为 伪随 机数 ,但 对 于 实 际 应 用来 讲,这 就 足够 用了。 如下 给 定三 个常数 的值,能产 生 65536个 随机数。     multiplier= 25173 increment= 13849 modulus=216 = 65536 产生伪 随机数 的 C函数 如下:   #definemultiplier25173 #defineincrement13849 #definemodulus65536 intseed=0; intandom (void){ seed = (multiplier* seed + increment)% modulus; returnseed; } 其中 ,seed是 外部变 量,在第一 次调用 该函数 之前必 须给 seed赋予 初值。 对于大 多数应 用来讲 ,应 该用某 种比 例调 整 结果。 例如 ,用于 模拟 掷 骰 子 ,可 以令 结 果取 6为模,上 述程序 段的函数 部分可 以修改 成:   intandom (void){ seed = (multiplier* seed + increment)% modulus; returnseed% 6 +1; } 4.3  程 序设计 实 例 · 73· 而对 于欲产 生 0与 1之间的 实数类 型随机 数序列 ,上 述程序 段的函 数部分 可以修 改成:   floa random(void){ seed = (multiplier* seed + increment)% modulus; return(float)seed/modulus; } 例 4.4 验证 Pascal定理 :圆的内 接六 边 形三双 对边 延 长线 的交 点在一 条直线 上,如图 4.2所示。 解:这 是编 写的 第 一 个 有意 义 的 程 序,读者 应该 从 中 认 真体 会“自 顶向下、逐 步求精 ”的程序设 计思想 。 不失一 般性,假设 所有 直线 都 不平 行于 y轴,下边 用 斜 截式 方程 y=kx+b表 示直线 。 为了确 保输入 的六个 顶点准 确地在 圆周上,采 用极 坐 标。设   图 4.2 Pascal定理 六个 顶点的 极角 分别为 θ1、θ2、θ3、θ4、θ5、θ6 (0≤θ1≤ θ2≤ θ3≤ θ4 ≤θ5≤ θ6 <2π),矢 径为 r。 设:θ1 ~θ6用 theta1、theta2、theta3、theta4、theta5、theta6表 示; 六个顶 点 A、B、C、D、E、F在 直 角坐 标 系 中 的 坐 标 分 别 用 xa、ya,xb、yb,xc、yc,xd、 yd,xe、ye,xf、yf表 示; 六条边 S1、S2、S3、S4、S5、S6的截斜 式 直 线 方 程的 系 数 分别 用 s1_a、s1_b,s2_a、s2_b, s3_a、s3_b,s4_a、s4_b,s5_a、s5_b,s6_a、s6_b表 示; 三双对 边交点 B1、B2、B3在直角 坐标系 中的坐 标分别 用 b1_x、b1_y,b2_x、b2_y,b3_x、 b3_y表示; B1B2直 线方程 的系数 用 b12_a、b12_b表 示; 矢径为 r,由 于定理 的验证与 矢径无 关,固可令 矢径 r为常 量 1.0。 分析验证 过程,首 先把问题 分解成 :读入 六 个极 角的 值;求六个 顶点 A、B、C、D、E、F在 直 角坐 标 系 中的 坐标;求三 双 对边 交点 B1、B2、B3 的坐 标 ;验 证 B1、B2、B3 是 否 在 一 条 直 线 上 共 四 步 完 成 。得 到 如 图 4.3所示的 PAD。 进一步 求精图 4.3中 的算法 : (1)读 入六个 极角的 值,只 需一条 scanf即可 。 (2)求 六个顶 点 A、B、C、D、E、F在直 角坐 标系 中的 坐标。 假设有 一 个 函数 coordinate,可 以在 已知 极角 、矢 径的条 件下,计算出 该点在 直 角 坐标 系 中的 坐标 。则该 问题可 以求 精成 对 函数 的 六次 调 用,得   图 4.3 第一层验证的 PAD · 74· 第四章 函  数 到如 图 4.4所 示的 PAD。 继续求 精图 4.4中的“转 换”。已 知极角 、矢 径,求相 应点 在直角 坐标 系中的 坐标 ,直 接 有公 式,求精如 图 4.5所示。    图 4.4 已知极角、矢径,求一点的直角坐标 图 4.5 计算顶点坐标    (3)图 4.3中求 三对对 边交点 坐标。 可以 分 解 成 分别 求 AB边 和 DE边 交点 、求 BC边 和 EF边交 点、求 CD边和 FA边交 点,即三次 调用函 数 intersection,如 图 4.6所示。 函数 intersection。已 知每条 直线上 两点坐 标,求两 条直 线 的交 点。可 以求精 为:先求 两 条直 线方程 l1、l2,再求 交点 B。 图 4.7所示的函 数 intersection完成该 步骤。 ① 进 一 步 求 精 图 4.7 所 示 的 步 骤,假 设 已 知 两 点,求 过 该 两 点 直 线 方 程 的 函 数 straightline,则求 两条直 线方 程 的 系 数,只 要 两次 分别 调用 该 函 数 即可。 求 精 成图 4.8所 示 的步骤。 图 4.6 求三对对边  图 4.7 已知四点,求 两条直线 交点 图 4.8  已知 四点,求 两 条直 线方程 4.3  程 序设计 实 例 · 75· 图 4.8中 ,已知两 点计算过 该两点 的直线 方程系 数,有现成的 数学公 式,被描述 成图 4.9 所示的步骤。 ② 在 图 4.7中 ,已 知两 条直 线,求 该 两 条直 线的 交 点,也有 现成 的 数 学 公式 ,被 描述 成 图 4.10所示的 步骤。 (4)下 边求精 ,验 证 B1、B2、B3 是否 在一条直 线上。 验证三点 是否在 一条直 线上,可以 先求过 两点的 直线方 程,然后再 判断第 三 点 是否在 该 直线 上,如图 4.11所 示。 图 4.9  已 知 两 点求 过 该 两 点的直线方程系数 图 4.10 已知两条直线,求它们的交点 图 4.11 验 证 B1、B2、B3 是 否在一条直线上 最后求 精图 4.11,求 过 B1、B2 的直 线方程 可以直 接调用图 4.9所 示的函 数 straightline; 验证 B3是否在 直线 B1B2上,只是 一个布 尔表达式 。 最后编写出程序如下:    /*PROGRAM Pascaltheorem*/ #include"math.h" #include"stdio.h" #definePI3.1415927 #defineeps1e-5 floatradius;                    * 圆的半径 */ floattheta1,theta2,theta3,theta4,theta5,theta6; /* 六个极角的度数 */ floatxa,ya,xb,yb,xc,yc,xd,yd,xe,ye,xf,yf; /* 六个顶点的直角坐标 */ floatb1_x,b1_y,b2_x,b2_y,b3_x,b3_y; /* 三个交点的直角坐标 */ floatb12_a,b12_b; /* 主程 序 之 前 这 段 为“函数 原 型 ”以 及 各 个函数 返 回 结 果 所 用 变量 */ voidtrans_abcdef(); floatpx,py;        * 用来保存 coordinate()转换的直角坐标 */ voidcoordinate(float,float); voidthree_inter(); voidintersection(float,float,float,float,float,float,float,float); floatl1_a,l1_b,l2_a,l2_b; /* 两条直线的斜率和截距 */ · 76· 第四章 函  数 voidequation(float,float,float,float,float,float,float,float); floata,b; /* 直线方程的斜率和截距 */ voidstraightline(float,float,float,float); floatwx,wy; /* 直线交点的直角坐标 */ voidinter(float,float,float,float); inttest(float,float,float,float,float,float); /* 主函 数 */ void ain(){ /* 读 入 圆 的 半 径*/ printf("pleaseinputtheradiusofthecircle:"); scanf("% f",&radius); /* 读 入 六 个 角 */ printf("pleaseinputsixangle:"); scanf("% f% f%f% f% f%f",&theta1,&theta3,&theta3,&theta4,&theta5,&theta6); trans_abcdef();              *计算六个定点坐标*/ three_inter(); /*求 三 个 交 点 */ if(test(b1_x,b1_y,b2_x,b2_y,b3_x,b3_y)) /*验证*/     printf("ok"); else{ printf("Thereisanerrorwhen:\n"); printf("theta1=% dtheta2=% d\n",theta1,theta2); printf("theta3=% dtheta4=% d\n",theta3,theta4); printf("theta5=% dtheta6=% d\n",theta5,theta6); } } /* 计算 一 个 顶 点 坐 标*/ void oordinate(floatr,floattheta){ px=r*cos(PI*theta/180);   /* 先把“角度”转换成“弧度”,再转换成直角坐标 */ py=r* sin(PI*theta/180); } /* 计算 六 个 顶 点 坐 标*/ void rans_abcdef(){ coordinate(radius,theta1); xa=px; ya=py; coordinate(radius,theta2); xb=px; yb=py; coordinate(radius,theta3); xc=px; yc=py; coordinate(radius,theta4); xd=px; 4.3  程 序设计 实 例 yd=py; coordinate(radius,theta5); xe=px; ye=py; coordinate(radius,theta6); xf=px; yf=py; } /* 求三 个 交 点 */ void hree_inter(){ intersection(xa,ya,xb,yb,xd,yd,xe,ye); b1_x=wx; b1_y=wy; intersection(xb,yb,xc,yc,xe,ye,xf,yf); b2_x=wx; b2_y=wy; intersection(xc,yc,xd,yd,xf,yf,xa,ya); b3_x=wx; b3_y=wy; } /* 已知 四 点 ,求两条 直 线 交 点*/ void ntersection(loatrx,floatry,floatsx,floatsy, floattx,floatty,floatux,floatuy){ equation(rx,ry,sx,sy,tx,ty,ux,uy); inter(l1_a,l1_b,l2_a,l2_b); } /* 已知 四 点 ,求两条 直 线 方 程*/ void quation(loatrx,floatry,floatsx,floatsy, floattx,floatty,floatux,floatuy){ straightline(rx,ry,sx,sy); l1_a=a; l1_b=b; straightline(tx,ty,ux,uy); l2_a=a; l2_b=b; } /* 计算 由 两 点 确 定 直线 方 程 的 斜率 (a)和 截 距 (b)*/ void traightline(floatex,floatey,floatfx,floatfy){ a=(fy-ey)/(fx-ex);   * 斜率 */ b=ey-a*ex; /* 截距 */ } /* 已知 两 个 直 线 方 程的 斜 率 和 截距 ,求 它 们 的交 点 */ void nter(floatma,floatmb,floatna,floatnb){ · 77· · 78· 第四章 函  数 wx=(nb-mb)/(ma-na); wy=ma* wx+mb; } /* 检验 */ boolest(loatb1_x,floatb1_y, floatb2_x,floatb2_y, floatb3_x,floatb3_y){ straightline(b1_x,b1_y,b2_x,b2_y); if(abs(b3_y-(a* b3_x+b))<eps) returntrue; else returnfalse; } 本章小结 本章讲 述 C函 数,包 括定义 及调用 。重点 掌握使 用函数的 程序设 计思想 。 习 题 四 4.1  指 出下 列 函 数 定义 说明 符 的 错 误 。 (1)intFUNCTIONf();        9)f(charx):float; (2)floatf(x,y); (10)floatp(); (3)intf; (11)realp(); (4)floatf(x:char); (12)floatf(floatx,y;intz); (5)floatf(charx) (13)floatf(floatx,y); (6)floatf(charx,); (14)floatf(floatx,y,intz); (7)floatf(charx); (15)floatf(intx-1,intz); (8)realf(intx); 4.2  什 么是 函 数 的 类型 ?是 否 所 有 函 数 都 有类 型 ? 什 么 是函 数 值 ?是 否所 有 函 数 都 有 值 ? 4.3 编写程序,输入实数 a、b、c的值,分别计算并输出下列算式的值:    S=cos(c)+17c+ln(4c+2c+41c) 4.25(a+b)+ln(a+b+    T= a+b+a1+b) 4.25+ln(c+ c+ 1c) 习 题 四 · 79· 4.4  分 别编 写 一 个 函数 ,计 算 下 面双 曲 函数 :   sh(x)=12(ex -e-x)   ch(x)=21(ex +e-x)   th(x)=(ex -e-x) (ex +e-x)    sh(x+y)=sh(x)ch(y)+ch(x)sh(y)   sh-1(x)=ln(x+ x2 +1) 4.5 编写一个函数,计算 f(x,y)=xy。 4.6 分别编写一个函数,计算复数加法、减法、乘法、除法,复数 Z表示成:Z=a+bi 4.7 编写一个函数,求解一元一次方程 Ax+B=0。 4.8 编写一个函数,求解二元一次方程组 A1x+B1y+C1 =0 A2x+B2y+C2 =0 4.9 编写一个函数 f(n),求任意四位正整数 n的逆序数。例当 n=2345时,函数值为 5432。 4.10  编 写 程 序 ,输 入 平 面上 三 个 点 ,求 过 该 三 点 的圆的 圆 心 和 半径 。 4.11 如图 4.12所示,已 知△ABC的两 个角 ∠A、∠B的 角度 值 及边 c, 编写一个函数,利用正弦定理求另两条边 a、b。 4.12 编写函数 area(xa,ya,xb,yb,xc,yc),代入 三 个顶 点 坐标,求 △ABC 的面积。   图 4.12 三角形 第五章 流 程 控 制 描述算 法的最 基本成 分是一 些基 本 操作 和基 本 控制 结构,使 用这些 基本 成 分应 该能 描 述任 何一个 问题的 解题算 法。第 三、四 章已经 介绍了 怎样用 C语句来 表示基本 操作,本 章介 绍怎 样用 C语句来 表示基本 控制结 构。 5.1  顺 序 结 构 程序设 计过程 中经常 遇到这 样一种 情况:若执 行算 法的 某部分 ,则 顺序 的语句 一个 接 一个 地全部 被执行 ;若 不执行,则 全 部语 句 一 个 也不 被执 行。 即 这 部 分是 一个 整 体,在该 整 体内 ,一 个个语 句顺序 排列。这 就 是 结构 化 程序 设计 中的 顺序 控 制结 构。C语 言用 复合 语 句(compound-statement)来描 述这种 结构。复 合语句 的语法 是:    <复合语句 > → { <声明或语句列表 > } <声明或语句列表 > → 声明或语句 > x <声明或语句列表 ><声明或语句 > <声明或语句 > → <声明 > x <语句 > 从上述 语法可 以看出 :复合 语句是 由花括 号 {、}括 起来 的一系 列语 句和 声明构 成的。 它的一般形式如下:   {  DS DS ⁝ DS } 其中 每个 DS是 一个“语 句”或者 是一个 “声明”。函 数的可 执行部 分是一 个复合 语句。 5.2  分支 程序 设计 前边讲的 语句都 是顺序 执行的 ,但 实际问 题并不 这样简 单,常常遇 到按不 同 情 况进行 不 同处 理或加 工的情 况,本节讲述 的分支 结构就 是为描 述这一 类问题 而设计 的。例如 ,计算 5.2  分 支程序 设 计 · 81· A,   当 A≥ B MAX(A,B)= B,  当 A<B 5.2.1 逻辑 值控制的分 支程序设 计 像上述问 题那样 ,按某条件 成立与 否而决 定具体 操作的 问题,应该 用逻辑 控 制 的分支 程 序来 描述。 if语 句是为 描述逻 辑控制 的分支程 序而设 计的。 if语句(if-statement)有 单分支 if 语句 和双分 支 if语句,其语法 是    <if语句 > → f( <表达式 > ) <语句 > x if( <表达式 > ) <语句 > else <语句 > 第一条 规则式 定义的 是单分 支 if语句,其 形式是   if(B)   S 其中: (1)B是 一个布 尔表达 式; (2)S是一 个语句 。 它的语 义是:计算 B的值,若 B为 true,则执行 语 句 S,否 则 什么 也 不 执 行。 这种 if语 句对 应如图 5.1所示 的流程 图;在 PAD 图中用如 图 5.2所示的形 式表示 。 图 5.1 单分支 if语句的流程图 图 5.2 单分支 if语句的 PAD图 第二条 规则式 定义的 是双分 支 if语 句,其形式 是:   if( ) S1 else S2 其中: (1)B是 布尔表 达式; (2)S1、S2都是语 句。 双分支 if语 句的语 义是:计算 B的值。若 B为 true,则执行 语句 S1,然后 跳过 else及其 后边 的语句 S2,向下执 行本 if语 句的后 继语句;若 B为 false,则先跳 过语句 S1,向下去 执行 · 82· 第五章 流 程 控 制 else后的 语句 S2,然后再 向下执行 if语句 的后继 语句。 这种 if语句 对应如 图 5.3所示的 流 程图 ,在 PAD图中 用如图 5.4所 示的形 式表示。 图 5.3 双分支 if语句的流程图 图 5.4 双分支 if语句的 PAD图 例 5.1 前述 求 A、B极大 值的问 题,可 以描述 成   if >b) max=a; else max=b; 例 5.2 数学 中的符 号函数 1, 当 x>0 sign(x)= 0, 当 x=0 -1, 当 x<0 用 PAD 可描述 成如图 5.5所示 的形式 ;用 if语 句则写 成如图 5.6所示 的形式 。 图 5.5 sign函数的 PAD图 if( >0) sign=1 else f(x= ) sign=0 elsesign= -1 图 5.6 sign函数的程序片段 使用与 书写 if语句 时应注意 以下几 个问题 : (1)B是 布尔表 达式。 (2)嵌 套是允 许的。 从语法 上看,“(表 达式) ”后以 及 else后都 是 一个 语句。 当然, 它应 允许是任 何语句 ,包括复合 语句、条件 语句以 及其 他 各类 语句。 例 5.2中 图 5.6所示 的 程序 ,第 一个 if语句 else后 的语句 就仍是 一个 if语句。 5.2  分 支程序 设 计 · 83·    (3)else的归属 问题,要特别 注意。 请看语 句   if(a>b)if(b>c)x=0;elsex=1; 这个语句怎样执行?考虑如下问题: ① 若 a<=b,执行 什么? ② 若 a>b且 b<=c,执行 什么? 这涉及 到最后 的 elsex = 1的归 属问题 ,即它 属于哪 个 if语句 。可以 有两种 解释: (1)else属于最 前边的 if,则上 述语句 相当于   if( >b) {if(b>c x=0; } elsex =1; 上述的问题的答案是: ① 若 a<=b,执行 x= 1; ② 若 a>b且 b<=c,什么 也不执 行。 (2)else属于第 二个 if,则上述 语句相 当于   if(a> ) {if( b>c) x= 0; elsex = 1; } 上述的问题的答案是: ① 若 a<=b,什么 也不执 行 ; ② 若 a>b且 b<=c,执行 x = 1。 这就产 生了两 义性。 C标 准规定 :“否 则部分 与前面 最邻近的 一个没 有配对 的 if配对”, 这就 是说,该语 句应按 第二种方 案解释 。若想 描述第 一种方 案的结 构只好 用 {、}将中间 的 “if()语 句”括起来 ,构 成复合 语句。 若不括起 来,按 if语义 ,就是按 第二种方 案解释 。 例 5.3 求一 元二次 方程 ax2 +bx+c=0的 根。 从数学 上看,该问 题很简单 ,按公式 其解为: x= -b± b2 2a -4ac 但是要 编程序 来解,就要保 证程序 的健壮 性。必 须考虑 各种可 能发生 的情况,例 如 a=0 怎么 办?下 面按自 顶向下 、逐步 求精的 原则来 开发该 程序,如图 5.7所示 。 考虑到 每步带 回的 信 息量 太 大 ,不 如 把 打 印 与 求 解 合并 ,在 求 解 结 束 处 打 印 ,得 如 下 程序: · 84· 第五章 流 程 控 制 图 5.7 解一元二次方程 注:Δ=b2 -4ac    /* PROGRAM quadraticequation*/ #include <stdio.h> #include <math.h> void ain(){ floata,b,c,delta; /*读入二次方程的 3个系数*/ printf("inputthethreecoefficientsoftheequation(A,B,C):"); scanf("% f%f% f",&a,&b,&c); if(a =0){ delta=b* b-4* a* c; if( elta>0) printf("x1 %f,x2=%f\n", (-b+sqrt(delta))/(2* a),(-b-sqrt(delta))/(2* a)); else if( elta==0) printf("x1=x2=% f\n",-b/(2* a)); else printf("x1 %f+%fi,x2=%f-%fi\n", -b/(2* a),sqrt(-delta)/(2* a), -b/(2* a),sqrt(-delta)/(2* a)); }else if(b! 0) 5.2  分 支程序 设 计 printf("x=%f\n",-c/b); else if(c =0) printf("0=0! \n"); else printf("% f=0\n",c); } · 85· 5.2.2 算术 值控制的多 分支程序 设计 if语句用 于逻辑 值控制的 两个分 支的 分支 结构 程序 设 计,而在 实际 应用 中 经常 遇到 多 分支 结构。 多分支 结构可 以用 switch语 句(switch-statement)控制,其语 法定义 是:    <switch语句 > → switch( <表达式 > ) <语句 > <标号语句 > → <标号 > :<语句 > <标号 > → 命名标号 > x <case标号 > x <缺省标号 > <case标号 > → case<常量表达式 > <缺省标号 > → default <break语句 > → break; switch语 句的语 法看 起来十 分简 单,只有 一条 BNF,但是 真 正应 用起 来,却要 配 合下 面有 关 的标 号语句 和 break语句 。 经 常使 用 的 switch语句 的格 式 有如 图 5.8和图 5.9所示 的两 种 形式 ,而 且最常 用的是 图 5.8中 的形式 。每种 形式中 的 default部分都 可以省 略。   swith(表达式 ){ case常 量 表 达 式:语 句 break; case常量表达式 :语句 break; ⁝ case常 量 表 达 式:语 句 break; default:语 句   }   swith(表达式 ){ case常量表达式 :语句 case常量表达式 :语句 ⁝ case常量表达式 :语句 default:语 句   } 图 5.8 switch语句的使用形式之一 图 5.9 switch语句的使用形式之二 图 5.8所 示形式 的多分 支结构 执行过 程是(语 义是): (1)计 算 switch后表达 式的值 ,设 值为 e。 · 86· 第五章 流 程 控 制 (2)根 据 e值决 定下一 步操作 。 ① 若 在诸常量 表达式 中,有某一 个表达式 的值等 于(1)中计算 出的 e值,则执 行列在 该 表达 式后的 语句;然后 执行 break语 句,跳 到 }后 ,该 switch语 句执 行 结 束。向 下执 行 swich 语句的后继语句。 ② 否 则,若 在诸表 达式中 没有一 个表达式 的值等 于(1)中计算 出的 e值,则 (a)若该 switch语句含 有 default缺 省标号 ,则 执行 缺 省标 号 default后 的 语句;执 行后, 跳到 }后,该 switch语句 执行结束 ,执行该 switch语 句的后继 语句。 (b)若 该 switch语句不 含 default缺 省标号 ,则 该 switch语句执 行结束 ,跳 到 }后执 行该 switch语句 的后继 语句。 设,有 switch语 句   swith(e){ casec :S1 break; casec :S2; break; casec :S3; break; casec :S4; break; casec :S5; break; defualt:S6 } 则按 图 5.10所示 的流程 图执行 ,对 应用图 5.11所示的 PAD图 表示。 图 5.10 多分支流程图之一 图 5.11 多分支 PAD之一 图 5.9所 示形式 的多分 支结构 执行过 程是(语 义是): (1)计 算 switch后表达 式的值 ,设 值为 e。 5.2  分 支程序 设 计 · 87· (2)根 据 e值决 定下一 步操作 。 ① 若 在诸常量 表达式 中,有某一 个表达式 的值等 于(1)中计算 出的 e值,则执 行列在 该 表达 式后的 语句;然后 顺序执行 该语句 后面的 所有语 句,直到 该 switch语句最 后,遇到 “}” 为止 ,该 switch语 句执行 结束。 向下执 行 swich语句 的后继语 句。 ② 若 在诸表达 式中没 有一个 表达式 的值等 于(1)中计 算 出 的 e值,则 与 图 5.8使用 形 式执 行动作 一样,在此 不再赘述 。 可以看 出,该形式 与图 5.8所示的 形式比 ,仅 差“若在 诸常量表 达式中 ,有 某 一 个表达 式 的值 等于(1)中计 算出的 e值 ”这部分 的处理,但是 这却是 本质的 差异。 设有 switch语句   swth(e){ casec1:S1 casec2:S2; casec3:S3; casec4:S4; casec5:S5; defualt:S6 } 则按 图 5.12所示 的流程 图执行 ,对 应用图 5.13所示的 PAD图 表示。 图 5.12 多分支流程图之二 图 5.13 多分支 PAD之二 例 5.4 高速 公路每 公里的 收费标 准按不 同种类 汽车如下 。编程 序,为某高 速公路 收费 站计算各种车辆的收费额。 · 88· 第五章 流 程 控 制   小 汽车(car)     .50元   卡 车(truck) 1.00元   大 客车(bus) 1.50元 算法如 图 5.14所 示,程序如 下:    /*PROGRAM calculatecost*/ #include"stdio.h" emuntsort(car,truck,bus)sort; inti; floatmileage; floatprice; voidman(){ printf("pleacechoose(1.car2.truck3.bus):"); scanf("%d",& i); switch i){ case1:sort= car;   break; case2:sort= truck;   break; case3:sort= bus; } printf("pleaceinputmileage:"); scanf("% f",&mileage); switch sort){ casecar:price = 0.50;   break; casetruck:price= 1.00;   break; casebus:price = 1.50; } printf("cost= %.2f\n",price*mileage); } 图 5.14 算法 5.3  循环 程序 设计 前面介 绍了顺 序结构 程序设 计和 分 支程 序设 计 ,现实问 题中 还有算 法的 某 部分 需要 反 复执行多次的情况。例如计算数列 ak =1 k(k+1) 5.3  循 环程序 设 计 · 89· 的前 n项和。显 然,用流 程图该问 题将被 描述成 图 5.15所示 的形式 。 该流程图 的“s=s+ak”和“k=k+1”部分要被 执行 n次 ,这就 是循环。循环是 指程序 的某 部分 被重复执行 多次。编 写重复执行 的程序称循 环程序 设计,被重复执行 的部分称为 循环体。 一般来 讲,循环程 序分为两 类: (1)先 判断条 件的循 环; (2)后 判断条 件的循 环。 图 5.15就是 后判断 条件的 循环,同样 问题还 可以被 描述成 图 5.16所 示 的 流程 图 ,这 就 是先判断条件的循环。 图 5.15 计算数列(后判断循环条件) 图 5.16 计算数列(先判断循环条件) C语言用 重复性 语句描述 重复 控制 结构,规 定 程序 的某 些部 分被 重 复执 行。重 复性 语 句是 描述循环 程序的 有力手 段。C语 言 有三 种 不 同 的重 复性 语 句 (repetitive-statement),它 们是: while语句———先判 断条件 的循环 ; do语句 ———后 判断条件 的循环 ; for语 句———也 是先判 断条件 的循环 。 一般 来讲,掌握 和使用 一种重复 性语句 应该明 确如下 问题: (1)重 复执行 部分(称 为“循环体 ”)是什么 ? (2)循 环控制 方式是 什么? (3)控 制条件 是什么 ? 下面分别讲述这三种重复性语句。 5.3.1 先判 断条件的循 环程序设 计 while语句被用 来描述 先判断 条件的 循环程 序。 · 90· 第五章 流 程 控 制 1.语法 while语句(while-statement)的语法 是:    < while语句 > → while(<表达式 > ) <语句 > 按上 述语法 ,while语 句的形式 是:   while( ) S 其中: (1)e是一 个 bool型表达式 ,是循环 控制条件 ,起 控制循 环继续 进行和结 束的作 用; (2)while是 保留字 ,起 引导 while语 句的作 用; (3)S是一 个语句 ,是 需要重 复执行 的部分 ,即 while的循环 体。 注意,作为 while循环体 的 S是一 个 语句 ,若重 复执 行 部 分 由多 个语 句组 成 ,则 应该 用 {、}把其 括起来 ,构成 一个复 合语句 。 2.语义 while语句是先 判断条 件的循 环,它表示 的控制 结构对 应图 5.17所 示的流程 图,在 PAD 图中 用图 5.18所 示的形 式表示 。其语 义是: 图 5.17 while语句的流程图 图 5.18 while语句的 PAD图 (1)计 算 e值; (2)若 e值 为 true则 转向 (3),否则转向 (5); (3)执 行语句 S; (4)转 向 (1); (5)while语 句执行 结束,向后 执行其 后继语 句。 按 while语句语义 ,显 然循环 体 S有可 能一次也 执行不 到。 设 e值依赖 于变量 组 V,在 进 入 while之 前一 定 有为 V 设 置初 值 的 部分 。 进 入 while 后,若 e值为 true,将进入 循环体 S执 行 ,这 时 要 求经 过 S的有 限 次 执 行 能 使 e的 值变 为 false,使 while语 句在有 限的时间 内终止 。也就 是说,S中一定 要有改 变 e值的 操作,否则 将 陷入 死循环 。由此 可知,一般使 用 while语句 的程序 应该呈 如下模 式:   V=V0; while e(V)) 5.3  循 环程序 设 计 · 91· V=S(V) 其中: (1)V = V0表示 程序控 制进入 while前为变 量组 V设置的 初值; (2)e(V)表 示 e的 计算依 赖于变 量组 V 的值 ; (3)V=S(V)表示 执行语 句 S时对变 量组 V 值的修 改。 例 5.5 编写 一个函 数,求自然 对数底 e的 近似值 。 算法:e值的计 算公式 如下: ∑∞    e=1+ 1 i=1 i! 由于 不能进 行无穷 项的计 算,所 以只能 进行近 似计算 ,当 余项 r=n1!<ε时停止 计算。可 利用 ∑ 前后 项之间 的递推 关系 xn =xn-1* n1计 算 的一项 。算 法如图 5.19所示 ,程 序如下 。     float aturallogarithmbase(floateps){ intn; floate,r; e=1.0; n=1; r=1.0; while( >eps){ e=e+r; n=n+1; r=r/n;   } returne; } 图 5.19 计算 e的近似值 5.3.2 后判 断条件的循 环程序设 计 do语句被 用来描 述后判 断条件 的循环程 序。 1.语法 do语句 (do-statement)的语 法是:    <do语句 > → do <语句 > while( <表达式 > ); 按上 述语法 ,do语 句的形 式是:   do   S while(e); · 92· 第五章 流 程 控 制 其中: (1)e是 bool型表 达式,是循 环控制 条件,起控制 循环继 续进行 和结束的 作用; (2)do和 while是两 个保留 字,起引导 和分隔 作用,指明此 语句是 do循环 语句; (3)S是一 个语句 ,是 重复执 行部分 ,构成 循环体 。 2.语义 do语句 是后判 断条件的 条件循 环,它表 示 的控 制 结 构 对 应图 5.20所 示 的 流 程 图,在 PAD中 用图 5.21所 示的形 式表示 。其语 义是: 图 5.20 do语句的流程图 图 5.21 do语句的 PAD图 (1)执 行语句 S; (2)计 算 e值 ; (3)若 e值 为 true,则转 向(1),否 则转向(4); (4)do语句执 行结束,向后 执行其 后继语 句。 按 do语 句的语 义,显然循环 体 S起码 被执行 一次,并且 在 S中也一 定包含 改变 e值 的 操作。 例 5.6 对本 节开始 的问题 ,编 一个函数 s(n),计算 数列 ak =1 k(k+1) 的前 n项和。 解:用 do语句描 述该问 题,算法如 图 5.22所示 ,程 序如下 :   float (intn){ intk; floatsum ; sum = 0; k =1; do sum = sum +1.0/(k*(k+1)); k++ ; }while(k<=n); returnsum ; } 图 5.22 计算数列前 n项和 5.3  循 环程序 设 计 · 93· 例 5.7 编写 程序,输入 一个年 份,求该年 以后的 n个 闰年。 某年(yy)是 闰年的 条件是 :yy能 被 4整除,但 不能被 100整除;或 yy能被 400整除。 写出布尔表达式是:   (yy% 4 ==0)&& (yy% 100!=0) ||(yy% 400 ==0) 先找到 第一个 闰年,然后每 隔 4年判 断一下是 否是闰 年,直到找 到 n个闰年 为止。 算法 如图 5.23所示 。 图 5.23 求第 n个闰年 程序:    /*PROGRAM findleapyear*/ #include"stdio.h" void ain(){ intyy,n; printf("pleaseinputbeginyear:"); scanf("% d",&yy); printf("pleaseinputthenumberoftheleapyear:"); scanf("% d",&n); yy=(yy/4+1)*4; do{ if((y%4==0)&&(yy%100!=0)||(yy%400==0)){ printf("year%d\n",yy); n--; } yy=yy+4; }while(n>0); } 例 5.8 编写 程序,解方 程 2X3 +0.5X2 - X + 0.093 = 0 把方 程变一 下形,成为 : X=2X3 + 0.5X2 +0.093 可以 想像,若某 个 X代入 右端后 ,计 算结果 正好是 X,则这个 X值就 是方程的 根。 可以采 用 · 94· 第五章 流 程 控 制 如下方法求解该方程的根: (1)选 定一个 X的初值 X0; (2)以 X0代 入右端 计算出 一个值 X1; (3)若 X1等 于 X0,显然 X0为根 ,转 向(5); (4)否 则,若 X1≠X0,则 令 X0=X1,转向(2); (5)结 束,停止计 算。 这个 方法称 为“迭代 法”。当 方程满 足一 定 条件时 ,该 方法是 可行 的(若方 程 X=f(x) 在根 X0附近满 足mP′(X0)m < 1,也 就是在 根附近,函 数 f导 数的 模小于 1,则上 述计算 过程 一 定收 敛)。 在实际工 作中,绝 大部分计 算都是 近似计 算,求 方 程 根不 一 定 (也不 可 能)要求很 准确。 只要 求得的 X1≈ X0即 可。上 述算法 可以描述 成图 5.24所 示的流 程图,把该算 法改造 一下, 用 PAD表 示成图 5.25所示的 形式。 图 5.24 选代法的流程图 图 5.25 选代法的 PAD图 循环终 止条件 X1≠ X0应 该表示 成mX1-X0m >ε。 设 X1初值为 0.09,ε=0.000001, 得到如下迭代法求解该方程根的程序:    /*PROGRAM findsolution*/ #include"stdio.h" #include"math.h" #defineeps1e-6 void ain(){ floatx0,x1; x0=0.0; x1=0.09; do{  x0=x1; x1=2*x0*x0*x0 +0.5*x0*x0 +0.093; }while(fabs(x1-x0)>eps); printf("x=% f\n",x0); } 5.3  循 环程序 设 计 · 95· 3.比较 前面已 经讲了 两种重 复性语 句,它 们的相 同点是 : (1)都 是由条 件控制 的循环 ; (2)在 循环体 内都必 须有改 变循环 条件的操 作。 它们的区 别在于 ,while语 句是先 判断循 环终止 条 件 ,后 进入循 环体,因此 循 环 体有可 能 一次 也不执 行;而 do语 句则是先 进入循 环体 执行后 再判 断循 环终止 条件 ,因 此循环 体至 少 被执 行一次 ———这 是最重 要的区 别。 5.3.3  for语 句 研究先判断条件的循环程序模式   V=V0; while(e(V))    V=S(V) 比如 本章例 5.5求自 然对数 底 e近似值 的问题 ,程 序被描 述成   e=1.0; n=1; r=1.0; while r>eps){ e=e+r; n=n+1; r=r/n;   } 其中 ,“r=1.0”是循环 控制的初 值部 分;“r>eps”是 循 环 控制 部 分;“r=r/n”是 对循 环控 制 条件 的修正 部分。 由于这 种程序 模式 在 程序 设计 中 经 常 出现,C语 言为 这种 程 序模 式提 供 一种 简写形 式———for语 句 (for-statement)。 用 for语 句 描 述 该 问 题 ,可 以 写 成 如 下 的 程 序 片段:   e=1.0; n=1; for( =1.0;r>eps;r=r/n){ e=e+r; n=n+1;    } 本段 程序与前 一段程 序意义 完全一 样。在 这个程 序片段 中,循环的 初值部 分、控 制 部分和 控 制条 件的修 正部分 全部集 中起来 写 在括 号 中 ,让 人 一 眼就 可 以 看 出 这些 部 分 的 内容 以及 操 作。 使程序 紧凑、好读 、易 于理解 。 · 96· 第五章 流 程 控 制 另外,for语 句最经 常被用 于描述 循环次数 已知的 循环 。比如 本章开 始求 序 列前 n项 和 的问 题,用 while语句可 以描述成 图 5.26所示 的程序 片段,用 for语句 可以描 述成图 5.27所 示的程序片段。两段程序的意义完全相同。 sum =0; k=1; while(k= <n){     sum =sum +1.0/(k* (k+1));     k+ +; } 图 5.26 while语句描述求序列和 sum=0; for(k=1;k<=0;k++);    sum=sum+1.0/(k* (k+1)) 图 5.27 for语句描述求序列和      for语 句是 while语句的 一种简 写方式 ,经 常被用 于描述 循环次 数已知 的循环。 1.语法 for语 句的语法 定义如 下:    < for语句 > → for( 表达式 1> ;<表达式 2> ;<表达式 3> ) <语句 > 按上 述语法 for语 句有如 下形式:              for(e1;e2;e3)   S   在 for语句 中: (1)for是保 留字,指明 for语 句开始 。 (2)S是一 个语句 ,是 for循 环的循 环体。 (3)e1、e2、e3都是 表达式 ,一 般解释 是,它 们可以 是 任 意表 达 式。但 是,这 三 个表达 式 在 for语句中起 循环控 制作用 ,它 们分担 的角色分 别是: ① e1,称为初 值表达 式,经常用 于设置 该循环 开始时 的一些 初值; ② e2,称为终 值表达 式,经常用 于控制 循环结 束; ③ e3,称为增 量表达 式,经常用 于每次 执行循 环体后 对循环 控制条 件的修正 。 2.语义 for循 环语句语 义如图 5.28的流 程图所示 ,并用图 5.29所示的 PAD图 表示。 例 5.9 编写 程序求 向量内 积。由 终端输 入两个 n维向量 X、Y,计 算其内积 X·Y。 解:用 for语句 描述该 问题,算法 如图 5.30所 示,程序如 下:   void ain(intn){ intn,k; 5.3  循 环程序 设 计 · 97· 图 5.28 for循环语句语义 floatxy,xi,yi; printf("pleaceinputn \n"); scanf("% d",&n); xy = 0; for( =1 i<=n;i=i+1){ scanf("% f% f",&xi,&yi); xy= xy+ xi* yi; } printf("xy=% f\n",xy); } 图 5.29 for循环的 PAD图 图 5.30 计算向量 X、Y内积 例 5.10  编写程 序,画函数 y=f(x)=e-xsin(2πX)在[0,2]区 间上的 图形。 分析:在屏 幕上显 示该函 数 曲 线。定 义 垂 直 方向 为 X轴 ,水 平方 向 为 Y轴。 绘 图 区 间 [0,2],定义显示 的第一 行为 X轴零 点。把 区间[0,2]分 成 40等份 ,则 图像共 显 示 40行,每 行相 当于 X值 0.05。由于 该函数 的值域 在 ±1之间,定义 Y轴在 屏幕中 间第 40列 ,每 20列 为 Y值 1,由 此,第 20列为 -1,第 60列 为 +1,每 列对 应 Y值 0.05。算 法如 图 5.31所 示,程 序如下。 · 98· 第五章 流 程 控 制 图 5.31 算法       /* ROGRAM graph*/ #include"stdio.h" #include"math.h" #definedx0.05 #definedy0.05 #definepi3.14159265 #definey040 #defineendx40 viod ain(){ intn,i,j; floatx,y; x= 0; for( =0;i<=endx;i++ ){ y= exp(-x)* sin(2* pi* x); n = y0+ (int)(y/dy+0.5); for(j=0;j< n;j++) printf(""); printf("* \n"); x= x + dx; } } 执行该 程序,将在 屏幕上输 出图 5.32所示的 图形。 3.广义 语义 图 5.32 函数图形 上述几个 例题是 for循环 语句最 普通、最典型 的用法 ,同时也是 最简单 的用 法。从 for语 句的 语义可以 看出,e1、e2、e3三个 表达式 可以是任 意表达 式,没有 什么 限制。同 时,for循 环 是 while循环的 一 个 变种。 反过来 ,图 5.33所 示的 for循环,可以 用 图 5.34所 示 的 while循 环表示。 可以使用 for循环 语句编 写出各 种灵活 的循环 程序结 构,只 要能 保证循环 正确 执行。 比 如 e1、e2、e3中的任 意一个 ,甚 至全部 都可以为 空;e1、e2、e3中的任 意一个 ,甚 至 全 部都可 以 为逗 号表达 式,从而在 循环过程 中描述 多项操 作和控 制。 5.4  程 序设计 实 例 · 99· for(e1;e2;e3)   S 图 5.33 for循环模式 e1; while(e2){   S     e3; 图 5.34 while循环模式 5.4  程序 设计 实例 本章介绍了三种基本 的控 制结构。这 些控 制结构内 的语 句部分 都 允许任 意形式的 语 句。 这些控 制结构 之间可 以互相 嵌套,而且这 种 嵌 套没有 层次限 制。 比如,if语句内 可以 包 含循 环语句 ,循 环语句 内又可以 继续 包含 if语 句,等 等 。又 比 如 ,一 个循 环语 句 内可 以含 有 循环 语句,内层 循环语 句内还可 以再包 含循环 语句,形成 所谓的 多重循 环,等等。 例 5.11  编写程 序,打印 99表 。 分析:99表的 形式如 图 5.35所 示。打 印 图 5.35 所示 的 99表,应 该 考 虑 先 打印 前 9行,再 打印 底 行, 如图 5.36(a)所示;打 印 前 9行 应该 一 行 一 行 地 打 印,如图 5.36(b)所示;打印 第 i行应 该先 打 印行 标, 再打 印本行 数值,如图 5.36(c)所示;打 印本行数 值, 是一 个 1到 i的 循环 ,如 图 5.36(d)所 示;打印底 行, 先打 印一个 “* ”,再打印 1到 9,如图 5.36(e)所示。 程序:   图 5.35 99表    /* ROGRAM printtable9*9*/ #include"stdio.h" void ain(){ inti,j; for(i= ;i<10;i++){ printf("% 4d",i); for(j= ;j<=i;j++) printf("%4d",i* j); printf("\n"); } printf("%4c",* ); for(i 1;i<10;i++) printf("%4d",i); · 100· printf("\n"); } 第五章 流 程 控 制 图 5.36 打印 99表的 PAD图 例 5.12  编写程 序,打印 100以 内的素数 (如图 5.37所示)。 图 5.37 打印 100以内的素数 程序:    /* ROGRAM writeprime*/ #include"stdio.h" 5.4  程 序设计 实 例 · 101· boolprime(intn){ intj; boolflag; flag=true; for( = n/2;j>= 2;j-- ) if( %j==0) flag=false; returnflag; } void ain(){ inti; for(i= ;i<=100;i++ ) if( rime(i)) printf("%5d\n",i); } 本程序 中的 prime函 数更可 以简写 成:   boolprme(intn){ intj; for( = n/2;j>=2;j-- ) if(n%j== 0) returnfalse; returntrue; } 例 5.13  模拟计 算器。 设计一 个模拟 计算器 的程序 ,设 该计 算器 只 能做 加、减、乘、除运 算,并且 运 算符 不带 优 先级 ,也 没有括 号。 解:算法如 图 5.38所示,程 序如下 (请 读者 思考:本 程 序 仅 能 对 一位 整 数进 行 计 算,若想 使它 适 应 多 位整 数、浮 点 数,程 序 应该 怎样修 改?):    /*PROGRAM Calculator*/ #include"stdio.h" vodmain(){ floata,b; charw; scanf("% f",&a); 图 5.38 模拟计算器 scanf("% c",&w); whil((w== + )||(w== - )||(w== * )||(w== /)){ scanf("%f",&b); · 102· 第五章 流 程 控 制 swith(w){ case + :{ a=a+b; break; } case - :{ a=a-b; break; } case * :{ a=a*b; break; } case /:a=a/b; } printf("% .2f",a); scanf("%c",&w); } } 例 5.14  十六进 制数翻 译。 编写一 个函数 convert,从终端 读入一 个十六进 制数,把它 翻译成 十进制数 。 若翻译 正确,则 convert返 回 true;否则,若翻译 错误,则 convert返回 false。 若该十 六进制 数为整 数,则 标 志 单元 intflag值 为 true,翻 译 后 得 到 的整 数保 存 在 变 量 n 中;否则 ,若该 十六进 制数为 实数,则标 志单元 intflag值 为 false,翻译后 得到的 实 数 保存在 变 量 f中。 十六进 制数的 语法图 如图 5.39所 示。 图 5.39 十六进制数的语法图 其中,X表 示非十 六进制数 字字符 ,十六进制 数字字 符有: 0、1、2、3、4、5、6、7、8、9、A、B、C、D、E 、F 假设 scanf不能直 接读入十 六进 制数,所 以只能 一个 字符 一 个字 符地 读入,然后 再把 它 们拼 起来,并翻 译成十 进制数。 解 1:按上 述语法 图,一个十 六进制 数具有 如下三 种形式: 5.4  程 序设计 实 例 · 103· nn n r r-1 r-2…n1n0· n-1n-2… n-s nn n r r-1 r-2…n1n0· · n-1n-2… n-s 第一 种形式 是最普 通的一 般形式 ,它的 含义是 : nr ×16r +nr-1 ×16r-1 +nr-2 ×16r-2 +… +n1 ×161 +n0 ×160       +n-1 ×16-1 +n-2 ×16-2 +… +n-s ×16-s 考虑翻 译算法 ,按 十六进制 语法,翻 译过程可 以写成 如图 5.40和 图 5.41所示 的 PAD。 图 5.40 十六进制数的翻译过程 图 5.41 十六进制数的详细翻译过程 分别求精各个部分: 滤掉前 导空白 部分可 以求精 成图 5.42,注 意 按 该算 法滤 掉前 导 空 白 结束 后,在变 量 ch 中应该是十六进制数的第一位数字。 把整数 部分表 达式整 理一下 ,可以 改写成 :   (…(((nr)×16+nr-1)×16+nr-2)×16+… +n1)×10+n0 从该 表达式 出发,并考 虑滤掉前 导空白 结束后 ,整 数部分 的第一 位数 字在 变 量 ch中 的 事实。 翻译 整数部 分可以 求精成 图 5.43所 示的 PAD。 同时 也 要注 意,按该 算 法 翻 译整 数结 束后, 在变 量 ch中应该 是整数 部分的 下一个 字符(后继 符)。 小数点 的处理 比较简 单,不 论从“滤掉前 导空 白”还 是从 “翻 译整 数”结束,到这 步时 当 前字 符已经在 变量 ch中 。如果 该字符 是小数 点“.”,则转 入小数 部分 处理;否 则翻译 结束。 所以 这部分 应该求 精成图 5.44所示的 PAD图 。 考虑翻 译小数 部分:小数部 分表示 成: n-1 ×16-1 +n-2 ×16-2 +… +n-s ×16-s 翻译算 法实际 就是计 算该表 达式。 使 用一 个 “位 值”变 量 g,小 数部 分翻 译 可以 求精 成 · 104· 第五章 流 程 控 制 图 5.42 滤掉前导空白 图 5.43 翻译整数部分 图 5.45所 示的 PAD。 整理上 述求精 过程 产生 的 PAD图,并考 虑题 目 要求 和翻译 细 节 ,改 写十 六 进 制 数 翻译 的 总控 PAD 图 5.40,得 到图 5.41所 示的 PAD图 。   图 5.44 小数点处理 最后该 题目要 求编一 个函 数 ,按上 述 图 5.41的 PAD图编 出函 数 convert。 该函 数是 一 个 bool类型函数 ,如 果翻译 正确,则返 回 true;否 则翻译 错误,则返 回 false。该 函 数 可以应 用 在需 要翻译 十六进 制数的 任何程 序中。 例如,可以配 备如下 主程序 调用该 函数 convert。    /* converthexadecimalnumbertodecimalnumber*/ #include"stdio.h" #defineradix16 intn; floatf; boolintflag; void ain(){ if( onvert()){ printf("Successful:"); if(intlag) printf("% d\n",n); else printf("% g\n",f); } else printf("Fail! \n"); } 图 5.45 翻译小数部分 按求精 到的上 述 PAD图,编出 convert函 数如下 :   intcalculte_char(charch){      /计算合法数字字符的数值 if((ch>= 0)&&(ch<= 9))    return(int)ch- 0; 5.4  程 序设计 实 例 · 105· else if((ch>= A )&&(ch<= F))    return (int)ch- A +10; else    return (int)ch- a +10; } boolisdigit(charch){ //判断 ch是否十六进制数字的函数   return(ch>=0)&&(ch<=9)||(ch>= A)&&(ch<= F)||(ch>=a)&&(ch<= f); } /* 翻译函数 */ bool onvert(){ charch; floatg; intflag=true; ch=getchar(); /* 滤 掉 前 导 空 白字 符 */ while(ch = ) ch=getchar(); if(! isdigit(ch))    returnfalse; /* 翻 译 整 数 部 分*/ n=0; whil(isdigit(ch)){ n=n*radix + calculate_char(ch); ch=getchar(); } if(ch = .){ intflag=false; /* 翻 译 小数 部 分 */ f=0.0; g=1.0; ch=getchar(); whil(isdigit(ch)){ g=g/radix; f=f+g* calculate_char(ch); ch=getchar(); } /* 整 数 部分 与 小 数 部分 合并 */ f=f+n; } if(ch== \r||ch== \n)    returntrue; else · 106· 第五章 流 程 控 制    returnfalse; } 解 2:状态 矩阵方 法。 从十六 进制数 的语法 图可以 看出,一个十 六进制 数由以 下几部 分组成 : (1)前 导字符 ———这 部分不 是数的 成分; (2)整 数部分 ; (3)小 数点; (4)小 数部分 。 解法 1中 的程序 把十六 进制 数翻译 的处 理分成 上述 几个 阶 段,如果 称每 个 阶段 为一 个 状态 ,则 十六进 制数的 处理过程 分别在 如下四 个状态 下进行 : (1)处 理前导 字符状 态(s0); (2)处 理整数 部分状 态(s1); (3)处 理小数 点状态 (s2); (4)处 理小数 部分状 态(s3)。 为方便 起见,再加 一个结束 状态 s4,状态 间的转 换与当前 处理的 字符有 关: 处理开 始,当前状 态设为 s0。 在 s0状态 下,遇到空 白字符 ,则 越过,状态不 变; 遇到数 字字符 ,则 进行整 数部分 的翻译 拼数,并转到 状态 s1; 遇到小 数点“.”,则转到状 态 s2,该数 为实数; 遇到其 他字符 ,则 错误,设为 1号错误 ,转到结束 状态 s4。 在 s1状态 下,遇到数 字字符 ,则 进行整 数部分 的翻译 拼数,状态不 变; 遇到小 数点“.”,则转到状 态 s2,该数 为实数; 遇到空 白字符 ,则 翻译结 束,转 到结束 状态 s4,该数 为整数; 遇到其 他字符 ,则 错误,设为 2号错误 ,转到结束 状态 s4。 在 s2状态 下,遇到数 字字符 ,则 进行小 数部分 的翻译 拼数,并转到 状态 s3; 遇到小 数点“.”,则错误,设 为 3号 错误,转到结 束状态 s4; 遇到空 白字符 ,则 翻译结 束,转 到结束 状态 s4,该数 为实数; 遇到其 他字符 ,则 错误,设为 4号错误 ,转到结束 状态 s4。 在 s3状态 下,遇到数 字字符 ,则 进行小 数部分 的翻译 拼数,状态不 变; 遇到小 数点“.”,则错误,设 为 3号 错误,转到结 束状态 s4; 遇到空 白字符 ,则 翻译结 束,转 到结束 状态 s4,该数 为实数; 遇到其 他字符 ,则 错误,设为 4号错误 ,转到结束 状态 s4。 可以用如 表 5.1所示的 一个矩 阵(称状 态矩阵 )表示上述 陈述,矩阵 的行标 为状 态 ;列 标 为当 前处理字 符;矩阵 元素为相 应状态 与字符 相遇时 的操作 和状 态 转换。 该状 态矩 阵 的行、 列均 从 0开始 编号。 5.4  程 序设计 实 例 · 107· 表 5.1 十六进制数翻译状态矩阵 数 字 :sort=0 “.”:sort=1 空 白 :sort=2 其 他:sort=3 s0 拼整数 /s1 /s2 /s0 错误(1) /s4 s1 拼整数 /s1 /s2 整数,结束 /s4 错误(2) /s4 s2 拼小数 /s3 错误(3) /s4 实数,结束 /s4 错误(4) /s4 s3 拼小数 /s3 错误(3) /s4 实数、合并,结束 /s4 错误(4) /s4 以 s保存当 前所处 的状态: (1)s==0:处理前 导空格状 态; (2)s==1:处理整 数部分状 态; (3)s==2:处理小 数点状态 ; (4)s==3:处理小 数部分状 态; (5)s==4:结束状 态。 以 sort保存 当前处 理的字符 类。 (1)sort==0:当前 处理(遇到 )字符为 数字字 符; (2)sort==1:当前 处理(遇到 )字符为 小数点 “.”; (3)sort==2:当前 处理(遇到 )字符为 空白字 符; (4)sort==3:当前 处理(遇到 )字符为 其他字 符。 状态矩 阵方法 翻译十 六进制 数的算 法如图 5.46所 示。 采用状 态矩阵 方法控 制,翻 译程序 显然比 前述方 法更有 条理。 程序如 下:    /* 十六进制翻译函数 2*/ #include"stdio.h" #defineradix16 intn;            * 整数部分 */ floatf; /* 小数部分 */ boolintflag,errflag; /* 整数标志、错误标志 */ charch; /* 当前读入字符 */ intnum ; /* ch对应的数值 */ floatg; /* 翻译小数部分权值 */ ints,sort; /* 状态、字符类 */ voidcom_integer(void); /* 翻译整数部分 */ voidcom_real(void); /* 翻译小数部分 */ intsort_char(char); /*字符分类,形成 sort*/ intcalculate_char(charch); /*计算合法数字字符的数值,与方法 1相同,函数体略 */ boolisdigit(charch); /* 判断 ch是否十六进制数字,与方法 1相同,函数体略 */ · 108· 第五章 流 程 控 制 图 5.46 十六进制数翻译的状态矩阵方法 voidcom_error(int); /* 错 误处 理 */ boolonvert2(){ /* 状态矩阵方法翻译函数 */ intflag= true; /* 首先假设翻译的是整数 */ errflag = true; /* 首先假设翻译正确 */ s= 0; /* 状态初值 */ g= 1.0; /* 小数部分权值 */ n =0; /* 整数单元 */ f= 0; /* 实数单元 */ whil (s<4){ ch=getchar(); /* 读 入字 符 */ sort=sort_char(ch); /*为字符分类*/ if(sort==0)   num=calculate_char(ch);         *计算字符对应的数值*/ /* 状态操作 */ swith(s){ 5.4  程 序设计 实 例 · 109· case0: wtch(sort){ case0: om_integer(); s= 1; break; case1: = 2; break; case2:break; case3:com_error(1); } break; case1: witch(ort){ case0: om_integer(); break; case1: =2; break; case2: =4; break; case3:com_error(2); } break; case2: with(sort){ case0: om_real(); s= 3; break; case2: =4; intflag = false; break; case1: om_error(3); break; case3:com_error(4); } break; case3: with(sort){ case0: om_real(); break; case2: =4; intflag = false; f= n+f; break; case1: om_error(3); break; case3:com_error(4); } /* 遇到数字,翻译整数 */ /* 遇到小数点 */ /* 前导字符 */ /* 翻译整数 */ /* 翻译整数时遇到小数点“.”*/ /* 翻译整数时遇到空白字符 */ /* 翻译整数时遇到其他字符 */ /* 翻译小数 */ /* 翻译整数时遇到空白字符 */ /* 存在两个以上小数点 */ /* 翻译小数时遇到非法字符 */ /* 翻译小数 */ /* 翻译整数时遇到空白字符 */ /* 存在两个以上小数点 */ /* 翻译小数时遇到非法字符 */ · 110· 第五章 流 程 控 制 }/* switch(s)*/ }/* while(s<4)*/ returnerrflag; }/* boolconvert2*/ void om_integer(void){ /* 翻译整数部分 */ n = n*radix+num; } voidcm_real(void){ /* 翻译小数部分 */ g= g/radix; f= f+num*g } /*字符分类、形成 sort*/ int ort char(charch){ if(isdigit(ch)){ /* 数字 字 符 */ return0; }els{ if( h== .) /* 小数 点 */ return1; else if(ch= ||ch== \t||ch== \n) return2; /* 空白 字 符 */ elsereturn3; /* 非法 字 符 */ } } /* 错误 处 理 */ void om_error(interrnum){ errflag = false; s=4; swith(errnum){ case1: rintf("#error1:数开始符非法! \n"); break; case2: rintf("#error2:整数部分输入非法字符! \n"); break; case3: rintf("#error3:输入两个或两个以上小数点 \n!"); break; case4: rintf("#error4:小数部分输入非法字符! \n"); break; } } 本章小结 本章讲 述结构 化的标 准流程 控制,包括顺 序结 构、分 支结 构、重 复 结构 的 流 程 及其 C表 示格式。 习 题 五 · 111· 习 题 五 5.1  已 知有 说 明    inti,j,k;    floatr,s,t,u,v;    boolb1,b2,b3;    charc1,c2; 指出下述语句中的错误。    (1)if(i=1)r=1; (2) f( >j) r=1; s=1; else t=1; (3) f b1) if(b2)r=1 elseif(b3)r=2 (4) witch(r+s) { ase1:i=1; case2:j=1; } (5) wtch(j) {1:r=1;  2:t=1; } (6) wich(j) { ase1.0:r=1; case2.0:s=1; }   5.2 一个写得不好的 C程序中有如下语句:    f(a<b)if(c<d)x=1; elseif(a<c)if(b<d) =2; elsex=3; elseif(a<d)if(b<c) =4; elsex=5; (7) witch(j) { ase1:r=1; case2:s=1; case1:t=1; } (8) witch(j) { ase1:r=1; case2,3:s=1; } (9)while(r+s)s=1+s (10)while(r>s)t=t+1; (11)for(j= a;j< z;)c1=c2 (12)for( =1;r<=10;r++) s=s+r (13) or( =1;j<10;j++) {k=k+j;   for j=10;j<20;j++) r=k } · 112· 第五章 流 程 控 制 elsex=6; elsex=7; (1)用 更 好 的 格 式 重 写此语 句 ; (2)有 没 有 多 余 或 矛 盾的条 件 ,若 有 ,请 化 简 ; (3)写 一 个 效 果 相 同 但更简 单 的 语 句。 5.3  如 下程 序 是 否 正确 ?它 产 生 什 么 输 出 ?为 什么 ?    ooltrue; voidmai (int){ true= 1==2; if(rue==false) printf("success\n"); elseprintf("failure\n"); } 5.4  如 下函 数 完 成 什么 操作 ?    ntr,x,y; intpo er(intp;intn){ r=1; x=p; y=n; whil (y<>0){ while(y 2==0){ y=y/2; x=x*x } y=y-1; r=r*x; } power=r; } 5.5 下面函数有错误吗?请给予解释。若该函数是计算 n!,应如何修改?    ntact(intn;intans) ans=1; WHIE n<>1DO ans=n* ans; n=n-1 } } 5.6 编写程序,输入三个实数 a、b、c,然后按递增顺序把它们输出。 5.7 编写程序,输入一个整数,判断它能否被 3、5、7整除,并输出如下信息: (1)能同时被 3、5、7整除。 习 题 五 · 113· (2)能 同 时 被 两个 数整 除 ,并指 明 是 哪 两 个 数? (3)能 被 一 个 数整 除,并 指 明是 哪 个 数 ? (4)不 能 被 所 有数 整除 。 5.8 编写程序,统计以 100为结束符的整数输入流中 -1、0、+1的出现次数并输出。 5.9 编写程序,当输入数值月份时,显示相应英文月份名称。例如,当输入 1时输出 January,当输入 5 时输出 May,等等。 5.10 编写程序,打印如图 5.47所示形式的数字金字塔。 5.11 用循环语句控制打印图 5.48所示的字符图形(分别用 integer型和 char型作控制变量)。 图 5.47 数字金字塔 图 5.48 字符图形 5.12 编写函数,输入一组数 z1,z2,…,zn,并计算这组数的偏差值 ∑ σ = 1 N (珋y-yi)2 其中 yi =max{1+cos2zi,1+sin2zi}   (i=1,2,…,N) ∑ 珋y= 1 N N yi l=1 5.13  编 写 一 个 函数 ,求 给定 句 子 中 最 长 单词 长 度 。 5.14 编写一个函数,计算所有小于 n的完全平方数之和。 5.15  编 写 一 个 函数 求 1- 1 2 + 1 3 - 1 4 + 15 -… +919-1100 5.16 编写一个函数 intsum(charc,intn),其中 c为数字字符,n为整数。求 n个 c   Sn=c+cc+ccc+… +ccc…c ccc…c是由数字 c组成的整数。 5.17 编写一个函数,计算 f(x,n)=xn。 5.18 编写一个函数,计算 f(N)=r1! +r2! +… +rn!。其中 N=r1r2...rn。 5.19 编写一个函数,判断给定自然数 n是否为降序数。降序数是指对于 n=d1d2d3…dk有 di>=di+ 1(i=1,2,… ,k-1) 5.20 设有正整数 n=d1d2…dr(0<=di<=9,1<=i<=r)。定义    maxdig(n)=max{d1,d2,… ,dr} · 114· 第五章 流 程 控 制 试写出对任意给定的正整数 n求 maxdig(n)的函数。 5.21 一个皮球从 100m高处落下,每次落地 后反弹 到原来 高度的 一半。编写 程序,求第 100次反 弹 时弹起的高度。 5.22 编写一个 bool型函数,判断两个整数是否互质。 5.23 编写一个 bool型函数,判断一个整数是否为素数。 5.24 编写程序,求所有四位可逆素数。可逆素数是指正序和反序读都是素数的整数,例如 9679是素 数、9769也 是 素 数 。 5.25 编写程序,求所有四位对称数。对称数是正序和反序读相等的整数,例如 96769就是对称数。 5.26 编写函数,求给定两个正整数 u、v的最大公因数。提示:采用欧几里德辗转相除算法。 5.27 编写程序,打印所有小于 100的可以被 11整除的自然数。 5.28 编写程序,打印所有个位数为 6且能被 3整除的全部五位自然数。 5.29 编写程序,打印前 10对孪生素数。若两个素数之差为 2,则称孪生素 数,例如 (3、5),(11、13), 等等。 5.30 编写程序,分别用 for和 while循环实现顺序输出 12、42、72、102、… 、222 5.31 编写一个函数 p(x,n),计算 Legendre多项式的第 n项。 1, n=0   Pn(x) x, n=1 2nn-1*x*Pn-1(x)-n-n1*Pn-2(x), n>1 5.32 编写程序,输入任意正整数 N,验证数列 ak =k(k1+1) 前 N项和为 n 。 n+1 5.33 编写程序,验证nl→im∞ axn =0。要求输入任意 x和任意较小正整数 ε,并找到 n,当 N>n时,axN <ε, 从而验证极限成立,求 n。 5.34 编写函数,判断给定字符序列中 (与 ),[与 ],{与 }是否配对(个数相等)。 5.35 编写程序,输入 100个整数,输出第二、第三、第四大和第二、第三、第四小的数。 5.36 编写程序,输出如下序列的前 100项。该序列的第一项为 0,第二项为 1,以后的奇数项为其前 两项 之 和,偶 数项 为 其 前 两 项 之 差。 5.37 甲、乙、丙同时放鞭炮,甲每隔 A秒放一个,乙每隔 B秒放一个,丙每隔 C秒放一个,他们各放 D 个。编写一个函数,对任意给定的 A、B、C、D,求能听到多少声声响。 5.38  编 写 程 序 ,输 入 任 意正 整 数 ,把 它 分 解 成 质因 数 乘 积 。例如 ,60=2×2×3×5。 5.39 我国古代数学家祖冲之采用正多边形逼近的割圆法求出圆周率 π的近似值到小数点后第 8位。 编写程序,用祖冲之的方法求圆周率 π的近似值,要求精确到小数点后第 16位。 5.40  验 证 哥 德 巴赫 猜 想 : (1)任意一个大偶数都可以分解为两个素数之和。随机产生 10个大于 6的偶数进行验证,并对每个 习 题 五 · 115· 偶数输出分解结果。 (2)任意一个大奇数都可以分解为三个素数之和。随机产生 10个大于 5的奇数进行验证,并对每个 奇数输出分解结果。 5.41 已知 1899年 12月 31日是星期一。编写程序,输入 1900年以后的任意一天的日期,以英文形 式输出这一天是星期几。 5.42 若 α是 x的一个近似值,则 β=α+2αx 是一个更好的近似值。编写一个函数 f(x),用迭代法求 x的平方根。 5.43 若 α是3x的一个近似值,则 2α+ β= 3 x α 是一个更好的近似值。编写一个函数 f(x),用迭代法求 x的立方根。 5.44  编 写 程 序 ,用 牛 顿 莱蒲 生 公 式 通 过 逐步 逼 近 解 方 程 x* sin(x)=1 迭代公式是 xn+1 = 1+x2ncos(xn) sin(xn)+xncos(xn) 5.45 解非线性方程 f(x)=0的牛顿迭代法的迭代公式是 xn+1 =xn -ff′((xxnn)) 编写程序,用该方法求方程 f(x)=x41 +x3 +1=0在 x0 =-1附近的根,如图 5.49所示。 5.46 设 f(x)是 x的连 续函 数,且 f(a)<0,f(b)>0;则 可 以用 两 分 法求 方 程 f(x)=0在 区 间 [a,b]上的一个根。其思路是,不断重复如下过程,直到 |a-b|<ε为止。如图 5.50所 示,则(a+b)/2 即为方程的根。 (1)令 x0 = (a+b)/2。 (2)计算 y0 = f(x0)。 (3)若 y0 >0,则令 b=y0;否则令 a=y0。 图 5.49 牛顿迭代法 图 5.50 两分法 · 116· 第五章 流 程 控 制 5.47  编 写 程 序 模拟 石 头 、剪 子 、布 游 戏 。 规 则 是 剪子 剪 布 ;石 头 克 剪 子;布 包 住 石头 。 游戏 者 自己 分 别输 入 自己 的 选 择 ,由 计 算 机 判 断输 赢 。 5.48 爱因斯坦阶梯问题。设有阶梯,不知其数,但知:每步跨 2阶,最后剩 1阶;每步跨 3阶,最后剩 2 阶;每步跨 5阶,最后剩 4阶;每步跨 7阶,正好到楼顶。编写程序求最少共有多少阶。 5.49 我国古代有一道著名难题———“百钱百 鸡”问题:“鸡 翁一,值钱 五;鸡母 一,值钱 三;鸡 雏三, 值钱 一 。百 钱 买 百 鸡 ,问 鸡 翁 、鸡 母 、鸡 雏 各几 何”。编写程 序 ,解 该 题 。 5.50 100匹马驮 100担货,大马驮 3担,中马驮 2担,小马驮 0.5担。编 写程序,计算大、中、小马 的数量。 5.51 某食堂管理员带 1000元人民币去市场买鸡,市场价每只小鸡 5元,每只公鸡 10元,每只母鸡 15元。该管理员打算正好买 100只鸡,并且尽可能多买母鸡。请编写程序,替他制定采购方案。 5.52 鸡兔同笼问题。若干鸡兔同关在一个笼子里,共有 36个头,100只脚。编写程 序求共有多 少只 鸡,多 少 只 兔 子 。 5.53 有一张面值 100元的人民币,要把它兑换成面值 10元 、5元 、2元 、1元 、0.5元的零钱,每种 面值 至 少换 一 张 。 编 写 程序 ,给 出兑 换方 案 。 5.54 小华有 6角 9分钱,准备全部购买铅笔。文具店里有 8分、6分、5分、4分一支的铅笔四种。在 必须至少买一只 8分铅笔的前提下,使买的铅笔数最多且又刚好 把钱用完。编写 程序,为 小华选择购 买方 案,输 出 购 买 每 种铅 笔的 数 量 。 5.55 编写一个函数,H(n)计算调和级数前 n项和 1+ 1 1 + 1 2 + 13 + 1 4 +… + 1 n 5.56  利 用 展 开 式 ex =x0 +x1 +x2 +x3 +x4 +… +xn +… 0! 1! 2! 3! 4! n! 分别计算 ex到第 100项和到余项小于 10-8(考虑当 0<x<1;x<0;x>1时各应该如何计算)。 5.57  设 计 一 个 用如 下 四 种 不同方 法 计 算 和 的 程 序,计 算 S=1- 1 2 + 1 3 - 1 4 + 1 5 - 1 6 +… +99199-101000 (1)从 左 向 右 各 项 相 加; (2)从 右 向 左 各 项 相 加; (3)从 左 向 右 各 正 项 和各负 项 分 别 相加 ; (4)从 右 向 左 各 正 项 和各负 项 分 别 相加 。 比 较得 到 的 四 个 结 果 ,说 明 出现 这 种 现象 的 原因 。 5.58 分别用如下展开式计算圆周率 π到 10-5,并选出收敛最快的展开式。 (1) π 4 =1- 1 3 + 1 5 - 1 7 + 1 9 -…      (格里 高 里 展开 式 ) (2) π 2 = 21· 23· 43· 45· 65· 67· 87· …·2n2n-1·2n2n+1· … 5.59 编写程序,对给定的自然数 n,验证 Nicomachus定理。任意自然数 n的立方 n3都可以分 解成 n 个奇数之和,且这 n个奇数中最大的一个在奇数序列中的序号为: 习 题 五 · 117· 1+2+3+… +n=n(n+1) 2 5.60 编写一个函数 f(a,b),参数 a、b是两个正整数,其中 a<31,b为三位数。函数值为 a在左,b 在右拼成的一个整数。例如,a=25,b=356,则函数值为 25356。 5.61 编写程序,打印所有三位的 Armstrong数。所谓 Armstrong数是指其值等于它本身每位数字立方 和的数。例如 153就是一个 Armstrong数。 153=13 +53 +33 5.62 编写程序,打印所有除以 11后所得商正好是它的各个数字平方和的三位数。 5. 3 求满足如下两个条件的最小的正整数 n: (1)n的个位数字是 6; (2)若把个位数字 6移到最高位,得到的数正好是 n的四倍。 5.64 设 x是十进制整数,p(x)为 x的所有数码之积,例如 p(123)=1*2*3=6。求使 p(x)=x2 - 10x-22成立的最小正整数 x。 5.65 精确计算 ab,其中 a、b为正整数。 5.66 编写程序,验证 100以内的奇数平方除以 8都余 1。 5.67 编写程序,输入整数 k,求满足如下条件的整数偶 m、n。 (1)0 < m、n < k; (2)(n2 -mn-m2)2 =1; (3)m2 +n2最大。 5.68 编写程序,求由 1、2、3、4可以组成多少个每位数字均互不相同的四位 数,并 输出所有这 样的 四位数。 5.69  斐 波 纳 契 序列 问 题 。 有 一对 小 兔 子 ,出生 一 个 月后 变 成 大 兔 子 开 始怀 孕 ; 两 个月 后 ,生 出 一 对 小 兔 子,这 时 共有 两 对 兔 子(一 对 大 兔 子 ,一 对 小 兔子 ),同 时 大 兔 子又 再 次 怀孕 ; 三个月后,以前出生的小兔子变成大兔子,以前怀孕的大 兔子又生出 一对小兔子,这 时共有三对 兔子 (两 对大 兔 子 ,一 对 小 兔 子 ),所有 大 兔 子 又 全 部 怀孕 ; 四个月后,以前出生的小兔子变成大兔子,以前怀孕的大 兔子又各生 出一对小兔 子,这时共有五 对兔 子(三对 大 兔 子 ,两 对 小 兔 子 ),所 有 大 兔 子 又 全 部怀 孕; 五个月后,以前出生的小兔子变成大兔子,以前怀孕的大 兔子又各生 出一对小兔 子,这时共有八 对兔 子(五对 大 兔 子 ,三 对 小 兔 子 ),所 有 大 兔 子 又 全 部怀 孕; ⁝ 假设在兔子的生养过程中没有死亡。编写程序,输入 n,计算 n个月后有多少对兔子,并输出。 5.70 某中学举行数学竞赛。一年级组及格生是不及格生的 3倍多 1人;二年级组及格生是不及格生 的 6倍;二年级组及格生比一年级组及格生多 5人。编程序,求一、二年级组共有多少学生参加竞赛。 5.71 五个球队 A、B、C、D、E举行一次比赛,甲、乙两球迷预测比赛结果。甲预测名次顺序为 A、B、C、 D、E,结果没有预测对任何一个队的名次,甚至也没预测对任何两队的名次是相邻 的;乙 预测名次顺 序为 D、A、E、C、B,结果预测对了两队的名次,并且还预测对两对球队的名次 是相邻的。编写程 序,求比 赛的 实际结果。 · 118· 第五章 流 程 控 制 5.72 两个乒乓球队进行对抗赛,甲队出 A、B、C三人;乙 队出 X 、Y、Z三人。部分抽 签结果是:A 不与 X比赛;C不与 X、Z比赛。编写程序,给出全部抽签结果。 5.73 一个猴子摘了一堆桃子,第一天吃了一半零一个,第二天吃了 剩余桃子的 一半零一个,第三 天 又吃了剩余桃子的一半零一个,如此下去,第十天吃完后还剩余 一个桃子。编写 程序,求 最初猴子摘 了几 个桃 子 ;若 要 第十 天 正 好 吃 完 ,最 初 应 该 摘几 个 桃 子 。 5.74 古印度有一位国王要奖励为他发明国际象棋的宰相达依尔,问他要什么。达依尔 回答:“陛下, 只要在国际象棋棋盘的第一个格子中放一粒麦子,第二个格子中 放两粒麦子,第 三个格子中 放四粒麦子, 第四个格子中放八粒麦子,如此下去,以后每个格子中都放它前一个格子中麦子数的两倍,这样放满 棋盘 的 64个格子即可。”。国王觉得这很容易,于是开始奖赏,没想到一袋 麦子很快就 用完了,下一袋也 很快 就用完了,最后一算,全 印 度 的 麦 子 全 部用 上 也 不 够。请 编 写 程 序 计 算 所 需麦 子 的 体 积 (1m3 麦 子 约 1.4×108粒 )。 5.75 某厂 有八位 厂级领 导 A、B、C、D、E、F、G、H,上级 要求每 天晚上 安排一 人值夜 班。根据 工作 需 要,一把手 A不排班机动,B紧接着在 D的后边值班,E比 F晚两天值班,C比 H早三天值班,G星期四值 班,且在 B、C之间。编写程序,打印值班表。 5.76 一辆汽车装满油料可以行驶 300km。从存有 n车油料的油库出发,通过在途中建立加油站的方 法,它 可 以 行 驶 L=3000×(1+1 +1 +… + 1 ) 35 2n-1 km。编写一个函数,给定 L计算 n。 5.77  某 货 运 公 司按 如 下 公 式计算 运 费 : 运费 =里程 ×货物重量 ×单价 除此之外还按表 5.2所示给予折扣,编写一个计算运费的程序。 重量 里程 <500km 500km≤里程 <1000km 里 程 ≥1000km >6 0% 1% 2% 表 5.2 折扣表 [6,12) 1% 2% 5% [12,18) 2% 5% 8% [18,24) 5% 8% 12% ≥24 8% 12% 16% 5.78 一个质点按如下规则在平面上随机游动:开始质点在原点,x=0,y=0;以后每次它随机地 沿以 下四个方向之一游动一步    向 左 :x=x-1    向 右 :x=x+1    向 下 :y=y-1    向 上 :y=y+1 对给定的 R,当 x2 +y2≥R2时游动结束。编写程序,输入 R,求游动次数。 5.79 编写程序,该程序输入一个 Pascal源程序,删 除该源 程序中 的注释 部分;检查 该源程 序中的 圆 括号 “(、)”和方括号“[、]”是否配对。输出结果源程序和检查结果。 习 题 五 · 119· 5.80 编写函数,对任意一元二次多项式 ax2 +bx+c进行因式分解,并以如下格式输出: a*x*x+b*x+c= (a1*x+c1)(a2*x+c2) 5.81 编写程序,顺序生成如下序列的前 100项。 (1)序列的第一、二两项分别为 2和 3; (2)序 列 后继 项 如 下 生成: ① 若序列的最后两项乘积为一位数,则该一位数即为后继项; ② 若序列的最后两项乘积为两位数,则该两位数的十位 数字和个位 数字分别为 后继项 的 连续两项。 5.82 已知平面上四点 (x1,y1),(x2,y2),(x3,y3),(x4,y4)。编 写函数 判断任 意给定 的点(x,y)是 否 在上述四点围成的四边形内。 第六章 数  组 6.1 构 造型 数据 类型 前边讲述 了 C语言 的简单数 据类型 。简单 数据类 型的特 点是在 一个类型 值 域 之内的 每 个值 都是单值 ,即在一 个值内不 包含其 他的值 。但是 世界上 的事物 不是这 样简 单的,比如 描 述数 学上的 一个 n维向量 、一个 m×n的 矩阵 、一个 人的 自 然 情 况表、一 个班 级 的学 生登 记 表,等等 。又比 如一万 个分量的 向 量 不 可 能用 一 万 个 基本 类 型 的 变 量表 示。 类 似问 题都 必 须用 一个数 据来表 示和处 理,前 面介 绍 的 基 本数 据类 型 显然 不能 满 足要 求。 构 造型 数据 类 型将能满足描述这类问题的要求。 本章将 开始学 习 C语言的构 造型 数据类 型 (structured-type)。 所谓 构造型数 据类型 是指 一个 数据类型值域 之内的 一个值是由 若干其 他类型 的值构 成的。C语言提 供了三 种构造 型数 据类 型:数 组、结构 和联合 。学习和使用 构造型数据类型应 注意掌握每 个构造型 类型的特点 : (1)它的 基础类 型是什么? ———该构 造型类型 是以什么类 型为基点出发构造 新类型的。 (2)构 造的方 法是什 么?———不同 的构造方 法形成 了不同 的构造 型数据 类型。 (3)一 个成分 的存取 方式和 使用方 法———不 同的类 型使用 不同形 式。 6.2  数 组 类 型 6.2.1 数组 声明 数组是最 常见的 一种 数 据结 构,它 是 变 量的 一 个有 序 集 合,其中 所 有 变量 具 有 同 一 类 型。例如,一行 正文可 以表示为 由若干 字符组 成的数 组;一个向 量可以 表示为 由 若 干实数 组 成的 数组;一个 矩阵可 以表示为 由若干 向量组 成的数 组。 数组类 型(array-type)的基 础类 型——— 称为 成分 类 型,可 以是 任 意类 型 (包 括 以 前 讲 的 简单 类型,以后 要讲的 结构、联合 、指针 以及正 在讲的 数组等 类型)。 数组类型 的构造 型方法 是把固 定数目 的成分 类型的 数据 顺 序排 成一个表 。 每 个数据 是 成分 类型的 一个值 ,所 有成分顺 序排成 的值表 是数组 类型的 一个值 。 6.2 数 组 类 型 · 121· 每个成 分数据 在数组 中都有 一个 惟 一 的 下标。 下标 从 0开 始顺 序增 加,即 第一 个成 分 下标 为 0;第二 个成分 下标为 1;第三个 成分下 标为 2……最 后一个 成分下 标显然 为数组 成分 个数 减 1。访问 数组的 一个成 分使用 该成分在 数组中 的下标 。 C语言中 涉及数 组类型的 语法是 :    <声明 >→ <声明说明符 ><初始化声明符列表 >; <初始化声明符列表 >→ <初始化声明符 > x <初始化声明符列表 >,<初始化声明符 > <初始化声明符 >→ <声明符 > x <声明符 >=<初始算子 > <声明说明符 >→ <类型说明符 > <声明符 >→ <直接声明符 > <直接声明符 >→ <简单声明符 > x <数组声明符 > <简单声明符 >→ <标识符 > <数组声明符 >→ <直接声明符 >[<赋值表达式 >] 这个文 法太复 杂,实际上简 单的 C数 组声 明是 一个 数组 声 明符 (arraydeclarator),它 的 结构如下:    <标识符 >[<赋值表达式 >] 其中 (1)标 识符是 要声明 的数组 的名字 ,也就 是要声 明的数 组变量 的名字 ; (2)赋 值表达 式被暂 时看做 常量表 达式,它是要 声明的 数组的 尺寸,也就 是 相 应数组 由 多少个成分组成。 把这个 声明符 放在一 个声明 中,便 可以声 明一个 数组。 例如   intm,n,v[10]; 除了 声明两 个 int类型 变量 m 、n以外 ,还 声明了一 个 int类型 数组变 量 v,v有 10个 成 分,每 个成 分都是 int类型的 。这里 的类型 说明 符 int就是 要声 明的 数组的 类型 ,也 就是数 组中 每 个成分的类型。 前文说的一万个元素的向量可以如下声明   floatvector[10000]; 数组 vector是 10000个实 数的线 性表。 该类型的 每个值 都是由 10000个 实数组 成的一 个向 量。 在向量 中 10000个元素 (成 分)的编 号 顺序 为 0、1、2、3、…、9999,即 它们 的下标 分别 是 0、1、2、3、…、9999。下述 数组声 明都是 合法的:   intt1[10],t0[10],w[10]; floatt2[2]; boolt3[26]; chart4[8]; · 122· 第六章 数  组 它们分别声明如下数组: t1是 10个 元素的 整型数组 ,成分的 下标编号 从 0到 9; t0和 w也是 10个元素 的整型 数组,成分 的下标 编号从 0到 9; t2是 2个元素 的实型 数组,成分的 下标编 号分别 为 0和 1; t3是 26个 元素的 bool型数组 ,成 分的下 标编号 从 0到 25; t4是 8个元素 的字符 型数组 ,成分 的下标 编号从 0到 7。 请读者注 意数组 类型变 量与已 学过的 简单类 型变量 之间 的 区别。 简单类 型 变 量只表 示 一个 单值(有时 称之为 简单变 量),而数组 类型变 量则表 示一组 值,它 由一组 变量组 成。进 一 步,说一 个数组 变量值 有定义,当 且仅当 该数组变 量中每 一个成 分变量 均为值 有定义 的。 6.2.2 下标 表达式 若要具 体标明 (访 问)数 组 变 量的 某一 个成 分 ,使 用下 标 表 达式 (subscript-expression), 下标 表达式 的 BNF是:    <下标表达式 >→ <后缀表达式 >[ <表达式 >] 其中 (1)后 缀表达 式最终 表现为 一个数 组变量,指出 访问哪 个数组 的成分 ; (2)方括 号中表 达式的类型 必须是整 数类型,它具体指 明访问的是 数组的哪一个成分 。 下标表达 式实际 是一个 变量,它是 相应数 组成分 类型的 一个 变 量。在 程序 中,该变量 的 地位 、作 用与相 应数组 成分类型 的一般 变量的 地位、作用 完全 相 同。即 凡是可 以 使 用数组 成 分类 型变量 的地方 都可以 使用下 标表达 式,有 时也称 下标表 达式为 “下标变量 ”。 在上述 声明的 前提下 ,下述 下标表 达式是 合法的 。   vector[255]    ector的编号为 255(第 256个)的成分,为 float型变量; v[2+3] v的编号为 5(第 6个)的成分,为 int型变量; t3[i+j*k] 若 i+j*k落在 0~25之内,则为 t3的编号为 i+j*k(第 i+j*k+1个)的成 分,是一个 bool型变量;若 i+j*k落在 0~25之外,则引起错误; t4[0] t4的编号为 0(第 1个)的成分,为 char型变量。 6.2.3 应注 意的问题 1.运算 C语言没 有定义 施于数组 类型上 的运算 。数组 类 型 的运 算 都是通 过其成 分 实 现的。 例 如,求数 组 t0、t1的差并 送入数组 w中,应 该如下 实现:   for m=0;m<=9;m++) w[m]=t0[m]-t1[m]; 而不能写成 6.3 多 维 数 组 · 123·   w=t0-t1; 2.I/O 数组变 量不能 作 I/O函数的 实 在参 数。 就是说 不能 整个 读 入一 个数 组,也 不能 整个 输 出一个数组。下述语句都是错误的:     scanf("% f",&w); printf("% f",w); 若想读 入一批 数据送 入数组 w中,或 想把数组 w中 的数据 全部输 出,可以分 别用如 下方 法实现:   for m=0;m<=9;m++) scanf("%f",&(w[m])); for(m= ;m<=9;m++) printf("%f",v[m]); 6.3  多 维 数 组 1.二维 下面看数组声明符的 BNF    直接声明符 >→ <简单声明符 > x <数组声明符 > <简单声明符 >→ <标识符 > <数组声明符 >→ <直接声明符 >[ <赋值表达式 >] 在数组声 明符中 ,方括号前 的直接 声明符 又可以 是数组 声明符 ,这 样就构 成 了 数组的 数 组。其形式是:    <标识符 >[<赋值表达式 >][<赋值表达式 >] 例:   floata[10][5]; 称如上 形式的 数组为 二维数 组,a是一 个二维 数组。 之所以 引进二 维数组 ,是因 为实 际问 题中 有 时需 要描 述 这种 数组 的 数组 。 例如 矩阵 就 是向 量的数 组,而向量 本身也是 数组。 上例的 a就可 以认为 是一个 10行 ×5列的矩 阵。 二维数组的下标表达式形式如下:   数组变量[表达式 1][表达式 2] 矩阵 a的第 3行、第 4列元素表 示为   a[3][4] · 124· 第六章 数  组 2.多维 继续扩充 上述数 组的维 数,二维数 组声明 中的直 接声明 符仍然 可以是 一个 数组 声 明符, 从而 构成三 维数组 。进一 步还可 以构造 四 维数 组、五 维 数 组,等 等。 若 需要,在 C程 序中 可 以声 明任意 维数的 数组,多维数 组的数 组声明 符形式 是:   数组标识符[赋值表达式 1][赋值表达式 2]…[赋值表达式 n] 多维数组的下标表达式形式是:   数组变量[表达式 1][表达式 2]…[表达式 n] 每个 表达式 与数组 说明符 中相应 维 按顺 序 一 一 对应 ,每个 表 达 式 的 值表 示 与 之 相对 应的 那 维上 的下标 值。例 如,有说明:   intx[5][2][26][5][3]; 则 x是一个五 维数组 。它的 成分类 型是 int型 ;第 一维 5个成分 ;第 二维 2个成分;第 三维 26 个成 分;第四维 5个成 分;第 五维 3个成分 。 下述形式   x[0][1][3][4][2] 是 x的一个下 标变量 ,类 型是 int型。 按语法 ,下 标表达 式中的表 达式可 以少写 若干,例如 :   x[0][1] 也是 一个下标 表达式 ,它的类型 (即 x数 组的成分 类型)是 一个三 维数组 。也就 是说,这时 把 它看成是类型   intx[5][2]<成分类型 > 的一个下标表达式。而它的成分类型是一个三维数组类型:   intt[26][5][3]; 6.4 程 序设 计实 例———数 组在 程序 设计 中的 应用 例 6.1 编写 一个函 数,求两向 量的内积 。 设有向 量 An、Bn 则 其内积为 n ∑ aibi i=1    求内积 的算法 可 以 描述 如 图 6.1所 示 ,程 序 片 段如下:   #definen100; floata[n],b[n];   图 6.1 求内积 6.4 程 序 设 计实 例 ——— 数 组 在 程 序 设 计中的 应 用 · 125· floa innerproduct(void){ floate; inti; e=0; for(i= ;i<n;i++ ) e=e+a[i]* b[i]; returne; } 例 6.2 编写 一个函 数,求两矩 阵的乘积 。 设有矩 阵 Amp、Bpn,则 其乘积 矩阵 Cmn的元 素 Cij为 p ∑ a b ik kj k=1 若把 矩阵 A、B分别 放在二 维数组 a、b中。则求 积矩阵 C的算法可 以描述 为: 求积矩 阵 C,首先考 虑一行 一行地 求,得 到 i循环 ;然后 对 于一行 ,应 该一 个元素 一个 元 素地 求,得到 j循 环;最 后 求 一 个 元 素,就 是上 面 例 6.1的 求 向 量 内 积。 最终 得 到 PAD如 图 6.2所示 ,程 序如下 :   #definem 0; #definen 20; #definep 30; floata[m ][p],b[p][n],c[m][n]; vodm trixproduct(void){ floate; inti,j,k; for( =0;i<m;i++ ) for(j=0 j<n;j++ ){ e=0; for( =0;k<p;k++ ) e=e+a[i][k]* b[k][j]; c[i][j]=e; } } 图 6.2 求矩阵积的算法 例 6.3 打印 杨辉三 角形的 前 10行。 · 126· 第六章 数  组 杨辉三角 形亦称 Pascal三角 形,它 是我国 宋代 著名 数 学 家 杨辉 发 现 的 ,其 形 式 如图 6.3 所示。该三角形的特点是: (1)从 第 0行 开始编 号,第 n行诸 数正好 是二项 式(a+b)n展 开式各 项的系 数。 (2)从 第二行 开始,每行数 字除两 端均是 1外,其余 数字正 好是它 上面一 行 与 它左右 相 邻的两数字之和。 打印该 三角形 ,显 然应该一 行行地 生成打 印,如图 6.4所示。 图 6.3 杨辉三角形 图 6.4 打印杨辉三角形 把第 i行数据 保存在 数组 A中 ,求 精上述 PAD图 。 先求精 生成 第 i行。 现在 的 问题 是如 何 生 成 ?使用 二项 式系数 定理 显然不 好,因为 这 要计算组合组数: Cm n = m! n!(n-m)! n稍大一 点,运算量 就会极大 。下面 利用杨 辉三角 形 的 特性,从第 i-1行生成第 i行。设 第 i-1行在 数 组 B中,并 考 虑 到 将 来 生 成 第 i+1行 时,数 组 B中 应 为 第 i行 ,可 以 得 到 如 图 6.5所示 的算法 。该算 法当 i>1时显 然正确,当 i≤1时也正 确,请读 者自己考 虑为什 么。 再求精 “打印第 i行”。 把第 0行 的 1输出 在屏幕 的第 40列 ;各行 数据中 每个数 占 6位; 第 i行应 从 40-i* 3处 开始显示 。得到 打印 PAD如图 6.6所 示,程序如 下。 图 6.5 生成第 i行    /*PROGRAM yanghui*/ #include"stdio.h" #definen10 #definewideword6 图 6.6 打印 6.4 程 序 设 计实 例 ——— 数 组 在 程 序 设 计中的 应 用 · 127· voidm in(){ inta[11],b[11],i,j; for(i=0; <n;i++){ /*生成第 i行*/ for(j= ;j<i;j++) a[j]=b[j-1]+b[j]; a[i]=1; /*形成下一次的第 i-1行*/ for(j= ;j<=i;j++) b[j]=a[j]; /*打印第 i行*/ for(j= ;j<=40-i*(wideword/2);j++) printf("%c",  ); for(j= ;j<=i;j++) printf("%6d",a[j]); printf("\n"); } } 还可以不 使用数 组 B,只用一 个数组 A来生成 和打印 该杨辉三 角形,这个 算 法 不但比 上 述算 法节省 存储空 间,而且运行 速度也 快。程 序如下 :    /* PROGRAM yanghui0*/ #include"stdio.h" #definen10 #definewideword6 voidman(){ inta[11],i,j; for(i=0; <n;i++){ /*生成第 i行*/ a[i]=1; for( =i-1;j>0;j--) a[j]=a[j-1]+a[j]; /*打印第 i行*/ for( =0;j<=40-i*(wideword/2);j++) printf("%c",  ); for( =0;j<=i;j++) printf("%6d",a[j]); printf("\n"); } } · 128· 第六章 数  组 例 6.4 编写 一个函 数,用“主元 排序”法对 整数组 A进行排序 。 “排序”亦称 “分类”,是计 算机科 学中 研究 的一 类重 要 课 题 ,现 在已 经有 很 多有 效的 算 法,下边 以整数 组 A为背 景介绍三 种较常 见的算 法。数 组 A的说明 如下:   #definen100 inta[n]; “主元排序 ”的思想 是: 首先在 a[0]、a[1]、…、a[n-1]中选 一个最 小元素 a[j],把 a[j]与 a[0]交 换; 然后在 a[1]、a[2]、…、a[n-1]中选 一个最 小元素 a[j],把 a[j]与 a[1]交 换; 然后再 在 a[2]、a[3]、… 、a[n-1]中选一个 最小元 素 a[j],把 a[j]与 a[2]交换; 如此 继续,一直 进行到 在 a[n-2]、a[n-1]中选一 个最小 的元素,放 在 a[n-2]中 为止。 按 该算 法,得 PAD如图 6.7所示 ,并 编写函 数 sort如下 :   voidsrt(intn,inta[]){ inti,j,k,r; for(i=0 i<n-1;i++ ){ j=i; for k=i+1;k<n;k++ ) if(a k] < a[j]) j=k; r=a[i]; a[i]=a[j]; a[j]=r; } } 图 6.7 主元排序 例 6.5 编写 一个函 数,用“冒泡 法”对整数 组 A进行 排序。 “冒泡”排 序的思 想是: 从头至 尾扫描 被排 序 数 组 A(i从 0到 n-2),比 较 数 组 A 的 所 有 相 邻 元 素 a[i]、 a[i+1],若 a[i]>a[i+1],则交 换 a[i]、a[i+1]。如此 反复 进行,直到 某一 次扫描 没有 数 据交 换为止 ,便 完成了 对数组 A的 排序。 至多扫描 n次就 可以使数 组 A完成排 序,所以 该算 法能够终止。 6.4 程 序 设 计实 例 ——— 数 组 在 程 序 设 计中的 应 用 · 129· 用一个 bool型 变 量 flag标 志 一 次 扫 描 过 程 中 是 否 有 数 据 交 换。 每 次 扫 描 开 始 时 令 flag=flase,假设没 有数据 交换;然后,当 有数据交 换时令 flag=true;最 后在一遍 扫描结 束后, 若flag==true,则 说明本 次扫描 有数据 交换,还应进 行下一 次扫描 ,否 则扫描 终止。 上述思 想如图 6.8所示 ,编 出程序 如下:   void ortofup(intn,inta[]){ inti,r; boolflag; flag=true; while(flg){ flag=false; for i=0;i<n-1;i++ ) if(a i] > a[i+1]){ r=a[i]; a[i]=a[i+1]; a[i+1]=r; flag=true } } } 图 6.8 冒泡排序 例 6.6 编写 一个函 数,用“逐步 增加递增 子序列 ”法对整 数组 A进行 排序。 “逐步增加 递增子 序列”排 序方法 的思想 是: 把数组 A看 成一个 序列: a[0]、a[1]、…、a[i]、… 、a[n] 假设子序列 a[0]、a[1]、… 、a[i-1] 已经 是递增 的,给上述 子序列再 加一个 元素 a[i];若有办 法使子 序列 a[0]、a[1]、… 、a[i-1]、a[i] 仍是 递增的 ,便 可以完 成对 A的排 序。由 于 当 子序 列 只 有 一个 元 素 a[0]时,它自然 是递 增 的;这样 ,一 个一个 地向该 子序列 加 元素 ,每加 一 个 元 素后,都 使 新 的 子序 列仍 保 持递 增;直 到将 最后一 个元素 a[n-1]加入 子序 列 为 止 ,便 完成 了对 序列 A的 排 序。该 算法 用图 6.9 · 130· 第六章 数  组 描述。 下面求 精“使 a[0]、a[1]、… 、a[i]递增”。 由于 a[0]、a[1]、… 、a[i-1]递增 ,若要 使 a[0]、a[1]、… 、a[i-1]、a[i]递增,则 应首 先找 到一个 j,使 a[j]≤a[i]<a[j+1]然 后,把 a[i]插到 序列   a[0]、a[1]、… 、a[j]、a[j+1]、…、a[i-1] 的 a[j]和 a[j+1]之 间。该算 法用图 6.10描述 。   图 6.9 逐步增加递增子序列 图 6.10 使 a[1]~a[i]递增   下面求 精“求位置 j,使 a[j]≤ a[i]<a[j+1]”,这 只要从 a[i-1]开始 ,向前 用 a[i]与 序列 上的元 素逐一 进行比 较,直 到找 到满 足条 件 的位 置为 止(当然 ,j不 能超 过 序 列 A的 第 一个 位置),如图 6.11所示 。 “把 a[i]插到 a[j]和 a[j+1]之 间”,只 要把 a[j+1]、a[j+2]、… 、a[i-1]顺 序向 后 串,分别 送入 a[j+2]、a[j+3]、… 、a[i]中,然后把 a[i]送 入 a[j+1]中即 可。 为了向 后 串,必须 先记录 a[i],用图 6.12描述。 综合上 述分析 可得完整 的算法 如图 6.13所示。 图 6.11 求位置   图 6.12 插入 a[i] 图 6.13 逐步增加递增子序列排序的完整算法 6.4 程 序 设 计实 例 ——— 数 组 在 程 序 设 计中的 应 用 · 131· 按上述 PAD图 ,编 写程序 如下:     voidsor (intn,inta[]){ inti,j,k,r; for(i=1;i<n i++ ){ j=i-1; while (a[j]>a[i])&&(j>=0)) j=j-1; r=a[i] for(k i-1;k>=j+1;k-- ) a[k+1]=a[k]; a[j+1]=r; } } 例 6.7 顺序 检索。 “检索 ”与“分类 ”是互 相联 系 在 一 起 的 ,也 是 计 算 机 科 学 研 究 的 一 类 重 要课 题 ,下 面 仍然 以 整 数 组 A为 背 景 介 绍两 种 较 常 见 的 算 法。设 A已 经 按递 增 排 序,其 说 明与 排 序 中 一样。 “顺序检索 ”的思想 最简单 : 顺次用 欲检索 的数 key与 数组 A中 元素 a[0]、a[1]、…、a[n-1]逐一 进 行 比 较。有 两 种可能的结果: (1)找 到一个 j使 key==a[j]:找到,位置为 j,函 数 search带着 j返 回; (2)直 到结束 ,没 有使 key==a[j]:未找 到,函数 search带 回值 -1。 该思想 如图 6.14所 示,编写函 数如下 : 图 6.14 顺序检索   intserch(intn,inta[],intkey){   * key为检索关键字 */ intj; /* 返回位置 */ for(j 0;j<n;j++) if(key =a[j]) · 132· 第六章 数  组 returnj; return -1; } 例 6.8 对半 检索。 “对半检索 ”亦称“两 分法检 索”。在 检索过 程中用 到三个变 量: lower:记录 检索区 间下界 ,初 值是 0; upper:记录检 索区间 上界,初值 是 n-1; j:标 记当前 检索位 置。 假设 a[0]≤key≤ a[n-1]。对 半检索 的思想是 ,令 j=(lower+upper)/2,判断:   key==a[j]:已经找到 ,位置为 j;函数 search带着 j返回;   key>a[j]:key在 a[j]与 a[upper]之 间,检索区 间缩小 一半,lower=j+1;   key<a[j]:key在 a[lower]与 a[j]之间,检 索区间缩 小一半 ,upper=j-1。 图 6.15、图 6.16和 图 6.17示 意该过 程如下。 图 6.15 中点 j=(lower+upper)/2 图 6.16 key在 a[j]与 a[upper] 之 间 ,lower=j+1 重复上述 过程,重 复的终止 条件为 upper-lower<0。 当循环终 止时表 示未 找到,函数 返 回 -1。 该算法 如图 6.18所 示,编写函 数如下 : 图 6.17 key在 a[lower]与 a[j] 之 间,upper=j-1 图 6.18 对半检索   inthaf_search(intn,inta[],intkey){/* key为检索关键字 */ intlower,upper,j; lower=0; upper=n-1; 6.4 程 序 设 计实 例 ——— 数 组 在 程 序 设 计中的 应 用 · 133· while(up er-lower>=0){ j=(lower+upper)/2; /* 两分 */ if( ey==a[j]) returnj; /*已经找到,位置为 j*/ elsef(ky> a[j]) lower=j+1; /* key在 a[j+1]与 a[upper]之间,lower=j+1*/ elseupper=j-1; /* key在 a[lower]与 a[j-1]之间,upper=j-1*/ } return -1; } 例 6.9 栈。 栈和队列 都是线 性表,是 两 种 重 要 的 数 据结 构,应 用 中经 常 使用 数 组 实现 这 两 种 线 性 表。下边通过两个例题分别介绍这两种数据结构。 在数据管 理上,栈 的特点是 “后进先 出”,即最后 进入栈的 数据元 素应最 先取出 使用。 数 据的 进入(称“压 入”)和退 出(称“弹 出”)均在 栈 顶进 行。这 种结 构和操 作有 些类似 自动 步 枪的 弹夹,装弹 时子弹 是从顶部 一 个 个 压 入 的;射 击 时 子 弹 是从 顶 部 一 个 个弹 出 的;子弹 进 入弹 夹的顺 序和弹 出的顺 序正好 相反,最 后 进入 的是 最 先弹 出的 。设 栈 中 每 个 数据 元素 的 类型 为 typeofelement并有如下 说明:   #definesize… typeofelementstack[size]; inttop; /* 栈的尺寸 */ /* 栈 */ /* 栈顶指针 */ 栈顶 指针 top初值 是 0,它始 终指向 栈顶第 一 个空 单元 (第一 个可用 位置 ),如 图 6.19所示。 栈的操作有: 压入:将一 个 typeofelemet类 型的数 据送入 栈中; 弹出:从栈 中取出 一个数据 ,并将该 数据从栈 中移掉 (删除)。 下面 分别给 出这两 种操作 的 C函 数: 压入   boolpus (typeofelementx){ if(tp>size-1) returnfalse; else{  stack[top]=x; top=top+1; returntrue; } }   图 6.19 栈 · 134· 第六章 数  组 弹出   typeofelementx; boolpo (void){ ifto <0 returnfalse; else{  tp=top-1; x=stack[top]; returntrue; } } 例 6.10  队列。 在数据 管理上 ,队 列的特点 是“先进 先出”,即 最后进 入队 列的数 据元 素最 后取出 使用, 而最 先进入 队列的 数据元 素最先 取出使 用。数据 的 进 入 在队 尾 进 行 (排 在 队尾 ),而 取出 在 队头 进行。 这种结 构和操 作有些 类似 于 人 们 排着 长 队 通 过 一个 狭 窄 的 走 廊,最 先进 入走 廊 的人一定最先从走廊的另一头走出去。 队列经 常用于 组织各 种缓冲 区(例如 打印缓 冲区 )。 由于 对一般 缓冲 区来说 ,数 据总 是 不断 产生送 入缓冲 区,并不断被 取 出 使 用 ,所 以经 常 把 缓 冲 区组 织 成 一 个 环形,也就 是把 缓 冲区 的首尾接 起来。 送入数 据时,当已 经到达 缓冲区 末尾时 ,下 一个数 据便送 到 缓 冲区首 部 第一 个位置 上;取出数 据时,当已 经到 达 缓 冲 区末 尾 时,下 一 个数 据便 从 缓 冲 区 首部 第一 个 位置 上取出。 显然,当 欲送入数 据时,缓冲 区中应 有空位 置才 能 送入;当欲 取出 数据 时 ,缓 冲 区中 应有数 据才能 取出。 这样,管理环 形缓冲 区应有 如下几 个计数 器: (1)缓 冲区尺 寸(size):常 量,记录 缓冲区的 长度; (2)缓 冲区中 现存数 据数(count):初值是 0,始终 记录队 列中现 存数据个 数; (3)送 入指针 (inpointer):初值是 0,始终 指向队 列中第一 个空单 元; (4)取 出指针 (outpointer):初 值 0,始终 指向队 列中第 一个可 取出的 数据。 设队列 中每个 数据元 素的类 型为 typeofelement,并有如下 说明:   #definesize… typeofelementstack[size]; intnpointer, outpointer, count; /* 队列的尺寸 */ /* 队列 */ /* 送入指针 */ /* 取出指针 */ /* 队列中现存数据数计数器 */ 队列 如图 6.20所示,有关队 列的操 作有: 入队:将一 个 typeofelemet类 型的数 据送入 队列的 队尾; 出队:从队 头取出 一个数据 使用,并 将该数据 从队列 移掉(删除 )。 6.4 程 序 设 计实 例 ——— 数 组 在 程 序 设 计中的 应 用 · 135· 如果操 作 成 功,则 函 数返 回 true;否 则操 作失败 ,函 数返 回 false。下 面分 别给出 这两 种 操作 的 C函数: 入队   bool utx(typeofelementx){ if( ount> size) returnfalse; else{  queue[inpointer]=x; inpointer=(inpointer+1)% size; count=count+1; returntrue; } }   图 6.20 队列 出队   typeofelementx; bool etx(void){ if( ount<1) returnfalse; else{  x=queue[outpointer] outpointer=(outpointer+1)% size; count=count-1; returntrue; } } 例 6.11  编写一 个函数 ,用 Gaoss消去法 解线性 方程组 :C* X=b。 把系数 矩阵 C和常 数向量 b合并 成一个 n行、n+1列的矩阵 A,得: a00 a01 a02 … a0 n-1 a0n a10 a11 a12 … a1 n-1 a1n a20 a21 a22 … a2 n-1 a2n ⁝ ⁝ ⁝ ⁝⁝ ⁝ a a a … a a n-10 n- 1 1 n - 12 n-1 n-1 n-1 n Gaoss消去 法的步 骤是:先把 矩阵化 成如下形 式: · 136· 第六章 数  组 a a a … a 00 01 02 0 n-1 aon a a … a 11 12 1 n-1 a1n a … a 22 2 n-1 a2n 0 … a a n -1n -1 n-1 n 然后 ,从 最后一 个 方 程开 始,逐 步回 代 ,求 出 根向 量。 这 一步 骤描 述如图 6.21所示。   图 6.21 消去法 求精“消去 A的下三 角部分 ”: 先这样 考虑:从第 0列开始 ,一 列一 列地把 每列 对角 线以 下 的 部 分 消 去,一 直到 第 n-2 列。 进行到 第 i步 时 A的格 式和算 法如图 6.22所示。 图 6.22 消去对角线以下部分 再考虑 消去第 i列的主 对角线 以下部 分。从 本列第 i+1个元 素开始 ,直到 第 n-1个元 素,一个 一个元 素地消 去。进行 到第 j步时 A的格 式和算 法分别如 图 6.23所 示。 图 6.23 消去第 i列 下面考 虑消去 a[j][i]。根据 线性方 程组的 性 质 ,消 去 a[j][i]应该 把 第 j个方 程减 去 第 i个方 程的 a[j][i]/a[i][i]倍。第 j个方 程 a[j][i]以 前的元 素全 部为 “0”,只 要 从 i开 始,一直 到 n,每项 都减去 第 i个方 程 的相 应 项 乘 以 (a[j][i]/a[i][i])倍 即 可,如 图 6.24 所示。 回代求 根部分 的出发 点是:方程已 经是上 三角形 式了。 可以 看 出 x[n-1]能立 即 求出, 它等 于 a[n-1][n]/a[n-1][n-1];得 到 x[n-1]代 入 第 n-2个 方 程,立 即 可 以 求 出 x[n-2],如此等 等,最后求 出 x[0]。从 而可以把 回代求 根过程 描述如 图 6.25所示 。 求 x[i]时的 条件是 :x[i+1]、x[i+2]、…、x[n-1]已经求 出;第 i个方程 是: 6.4 程 序 设 计实 例 ——— 数 组 在 程 序 设 计中的 应 用 · 137· 图 6.24 消去 a[j][i]   a[i][i]*x[i]+a[i][i+1]*x[i+1]+… +a[i][n-1]*x[n-1]=a[i][n] 所以该方程的解应该是:   x[i]=(a[i][n]-(a[i][i+1]*x[i+1] + … + a[i][n-1]*x[n-1]))/a[i][i] 计算 这个公式的 过程如图 6.26所 示。再考虑 回代过 程中当 i=n-1时 的边界 情况,这时 中间 的求 和循环不作 ,r=0。最 下面的公式正好变成 x[n-1]=a[n-1][n]/a[n-1][n-1],也是 正确 的,程序如 下: 图 6.25 回代求根 图 6.26 求 x[i]   #definen10 #definen111 floata[n][n1],x[n]; * 注意数组从 0开始编下标 */ void aoss(void){ floatr; inti,j,k; /* 消去下三角部分 */ for i=0;i<n-1;i++) /* 列控制:0到 n-2列 */ for(j=i 1;j<n;j++ ){ /* 行控制:i+1到 n-1行(最后一行)*/ r=a[j][i] /a[i][i]; for( =i;k<=n;k++) /* 第 j个方程(行)运算,直到第 n列 */ a[j][k]=a[j][k]-a[i][k]* r; } /* 回 代 求 根 */ for(i= -1;i>=0;i-- ){ /* 从最后一个方程(第 n-1行)开始 */ r=0; · 138· 第六章 数  组 } } for j=i+1;j<=n-1;j++ )/* 从第 i+1列到第 n-1列求和 */ r=r+a[i][j]* x[j]; x[i]=(a[i][n]-r) /a[i][i]; 2 2 2 例 6.12  编写程 序,打印如图 6.27所 示的星 形线图 形 x3 +y3 =a3 。 2 2 2 由于函 数 x3 +y3 =a3 不是 单调的 ,因此 不 能计 算一 点 打 印 一点。 本程 序用一 个 25行 79列 的二维 字符数 组 chargrid[25][79]表 示屏 幕。计 算过 程中,把函 数值 的坐标 信息 保 存在 该数组 内。待 全部计 算结束 后,再 打印该 数组。 2 2 2 函数 x3 +y3 =a3 还可以表 示成参 数方程 的形式 x=acos3 t y=asin3 t 为了 方便,使用 参数方 程来解该 题。把 整个 圆周 0~2π分 割 成 360个 等分 点 ,计算函 数值, 并把这些值转存放在数组中。 设坐标 原点在 数组的 第 13行第 39列 上,数 组列标 表示 X坐标 ,行标表示 Y坐 标。考 虑 屏幕 的行列 比例,计算 函数值时 X坐标与 Y坐标 的 比 例应设 为 2∶1,即 横方 向 的两个 字符 代 表的 数值应 与竖方 向一行 代表的 数值一 样。最后 ,定 a=12,程 序如下 :    /* PROGRAM rose*/ #include <stdio.h> #include <math.h> #definepi3.1415926 #defineaspectratio2 #definea12 voidmain ){ floatt; intx,y,i;   图 6.27 星形线 chargrid[25][79]; for y=0;y<25;y++ ) for( =0;x<79;x++ ) grid[y][x]=  ; for(x=0;x<79;x++ )grid[13][x]= - ; for(y=0;y<25;y++ )grid[y][39]= |; grid[13][39]= + ; for(i=0 i<360;i++ ){ t=i* pi/180; /*i* (2π/360)*/ x=39+(int)(a* cos(t)* cos(t)* cos(t))* aspectraio; y=13+(int)(a* sin(t)* sin(t)* sin(t)); 6.4 程 序 设 计实 例 ——— 数 组 在 程 序 设 计中的 应 用 · 139· grid[y][x]= * } for(y=0 y<25;y++ ){ for( =0;x<79;x++ ) printf("% c",grid[y][x]); printf("\n"); } } 例 6.13  已知有 n个 整数组 成的序 列放在 数组 a中 。顺序 从 a中 任意抽出 k个元 素构 成的 序列 u称为从 a中抽取 的长度 为 k的 子序列 ;进 一 步 ,若 u为 递增 的 ,则 称 u为从 a中 抽取 的长度 为 k的递 增子序 列。编 写程序 ,求从 a中可 能抽取 的最长 的递增 子序列 的长度 。 为了叙述 方便,使 用数 组 a[n+1]的 元 素 a[1]~a[n]保 存 n长 的 整 数 序 列,而 空 余 a[0]不 用。解该 题可以 有如下 两种思 路: (1)枚 举出所 有可能 从 a中抽取 的递增 子序列 ,其中 那个最 长者的 长度即 为所求 。 (2)令 i从 1变到 n;对每 个 i,逐 个考察 a中元素 ,求 从 a[1]~a[i]中抽取 的最长 的递 增子 序列的 长度 k;最后当 i=n时,从 a[1]~a[i]中 抽 取 的最长 的递增 子序 列 的长 度 k即 为所求。 考虑第 二个算 法。该 算 法 基 于 这 样 一 种 思 想 :设 已 经 考 察 了 i-1长 的 序 列 a[1]~ a[i-1],并求 得从它 中抽取 的最长 的递增 子 序列 的长度 为 k。当向 序 列 a[1]~a[i-1]再 加一 个元素 a[i]后,若有 办法重新 确定 k,使 k为 从 i长的序列 a[1]~a[i-1]、a[i]中抽 取 的最 长的递 增子序 列的长 度,则 该问题 可解。 因为,当 i=2时,i-1=1长 的 序 列 只 有 一 项 a[1],当 然 从 它 中 抽 取 的 最 长 的 递 增 子 序 列 就 是 a[1],其长 度 k=1。 这 样 从 i=2开 始,逐 步 增 加 i 值,作 i++;每 当 i增 加时都 重新确 定 k值;最后 当 i=n时,所 求 得 的 k值 就 是 所 求 的 从 n长 的 序 列 a[1]~a[n]中抽取 的最长 的递 增子序 列的 长度。 按 该思 想,得图 6.28。 下面求 精重新 确定 k:   图 6.28 抽取 设从 i-1长的序 列 a[1]~a[i-1]中抽取的 最长的 递增子 序列的 长度为 k。现在 考虑, 当再 加一个 元素 a[i]后 ,如何重新 确定 k,使 k为 从 i长的序列 a[1]~a[i-1]、a[i]中抽 取 的最长的递增子序列的长度。 加入 a[i]后 ,序列 a[1]~a[i-1]、a[i]对 k值的各 种影响情 况是: (1)引 起 k值 变化,使 k=k+1; (2)k值不变 。 下面分别考察这两种情况: · 140· 第六章 数  组 (1)由 于加入 a[i]可能 引 起 k值 变 化,使 k=k+1。 引起 k变 化的 条 件 是 a[i]大 于 a[1]~a[i-1]中的长 度为 k的某递 增子 序列的最 末元 素。在 a[1]~a[i-1]中 ,长 度 为 k 的递 增子序 列可能 不止一 个,显 然,只要 a[i]大于所 有这些递 增子序 列中末 元素最 小的那 个 递增 子序列 的末元 素,就可以使 k变化 。 为了判 断 a[i]是 否 大 于 这 个 末 元 素,就 必 须 保 存 它。 所 以 必 须 引 进 变 量 bk,保 存 a[1]~a[i-1]中 长 度 为 k的 递 增子 序列 中 末 元 素 最 小者 的末 元 素 。由此 可 见 ,当 向 序 列 a[1]~a[i-1]加入 a[i],得 到 序 列 a[1]~a[i]后,引 起 k变 化而 作 k=k+1的 条 件 是 a[i]>=bk。 (2)k值不变 。以上 考虑了 a[i]>=bk 的 情况。 但若 a[i]<bk 将产生 什么情 况呢? 若 a[i]<bk,说明 从 i长的序 列 a[1]~a[i]中抽取 的最长 递增子 序 列 的长 度 仍为 k,这 是确 定无疑的 。但是 ,所有长度 为 k的 递增子 序列中 末元 素最小 者的末 元 素是 否 还 是 bk 就 不一 定了。若 a[i]大于 某一个 抽取的 (k-1)长 的递增 子序 列 x的末 元素,则(x,a[i])就 是 一个 长度为 k的递 增子序 列,并 且 a[i]<bk。所以 ,从 i长 的序列 a[1]~a[i]中 抽取的 长度 为 k的递增子 序列中 ,末元素最 小者的 末元素 应为 a[i],而不 应该是 原来的 bk。 为了以 后加 入 a[i+1]等后继 元素的 需要,必须以 a[i]替换 bk,而使 bk =a[i]。 问题是 怎样判 断 a[i]大 于等于 某个(k-1)长 的递增 子序列 a的末 元素,即如 何确 定这 件事 实的存 在。与 前述分 析一样 ,a[1]~a[i-1]中长 度为 (k-1)的递 增子 序 列仍 可能 有 多个 ,当 然只要 判断 a[i]是否大于 所有这 些子序 列中末 元素最 小者的 末元素即 可。由 此,就 必须 保存这 个末元 素最小 的递 增 子 序 列 的末 元 素,也 就是 必 须 再引 进 一个 变 量 bk-1,保 存 (k-1)长的 末 元 素 最小 的 递 增 子序 列 的 末 元 素。 当 a[i]>=bk-1时,则 以 a[i]替 换 bk,使 bk =a[i]。 再进一 步,若 a[i]<bk-1,那么 a[i]是 否可以 成为 某个 抽取 的 长 度 为(k-1)的 递增 子 序列 的末元素 ,而取代 bk-1呢? 这就要 依赖于 长度为 (k-2)的 子序列 ,从而 又要保 存抽取 的 长度 为(k-2)的 递增子 序列中 末元素 最小者 的末元 素。 如此等等 ,依此类 推。抽取 的长度 为 1,2,… ,k-2,k-1,k的这一 系列递 增 子 序列中 的 末元 素最小 者的末 元素都 要保存 。引进 数组 b,使 b[j]保存抽 取的长 度为 j的递 增子序 列中 末元素最小者的末元素。 显然,b数组有 性质: b[1]<=b[2]<=b[3]<=… <=b[k-1]<=b[k] 由以上分析得如下第二步算法: (1)若 a[i]>=b[k],则 {k=k+1;b[k]=a[i]}; (2)若 a[i]在 某 b[j-1]与 b[j]之间 ,即 b[j-1]<=a[i]<b[j] 则 b[j]=a[i]。 这表明 :a[i]加 上以 b[j-1]为末 元 素的 抽 取 长 度 为 j-1的 递增 子序 列 后,就是 抽取长 度为 j的递增 子序列 ,并 且 a[i]<b[j],即 a[i]应为 抽取 长度为 j的 递增 子 6.4 程 序 设 计实 例 ——— 数 组 在 程 序 设 计中的 应 用 · 141· 序列 中末元 素最小 者的末 元素,所以作 b[j]=a[i]。而 b数组 中其他 元素不变 ,因为: ① 对 于 j以 后的元 素,a[i]不大 于前一 个元素 ,构 不成长 一点的 序列; ② 对 于 j-1以 前的元 素,a[i]不小 于下一 个元素 ,不 是末元素 最小者 。 (3)最 后考虑 ,当 j=1时,若 a[i]<b[1],则应 该 用 a[i]替换 b[1],作 b[1]=a[i]。 可以 引进 b[0],并令 b[0]=最小 整数,设为 MINNUM 来 由上述 条件统 一控制。 按上述 分析,得如 图 6.29所示 的算法 ,程序 如下: 图 6.29 求抽取最长递增子序列的长度    /*PROGRAM length*/ #defineMINNUM -2147483648 #definem 1000 #definenm-1 inta[m]; intleth(void){ intb[m],i,j,k; b[0]=MINNUM; b[1]=a[1]; k=1; for( =2;i<=n;i++) if(a[i >=b[k]){ k=k+1; b[k]=a[i] } else for( =k;j>0;j-- ) · 142· } 第六章 数  组 if((b j-1]<=a[i])&&(a[i]<b[j])) b[j]=a[i]; returnk; 6.5  数 组 初 值 从本章 6.2.1节 有关数 组声明 的语法 可以看 出,数 组声明 符还有 如下形 式: 标 识 符 [常量 表 达 式 ]… [常 量 表达 式 ]=初始 化 算 子 使用 该形式声 明一个 数组的 同时,还给 相应数 组赋以 初值。 与变量 赋初值 一样 ,被 赋以初 值 的数 组在程 序开始 执行时 (准 确地说 ,是 相应数 组生 存期 开始时 )便 取 得了 相 应 初 值。本 节 讲述 数组赋 初值的 形式,也就是 初始化 算子的 形式。 C语言关于 初始化 算子的 BNF如 下:    <初始化算子 >→ <赋值表达式 > x <初始化算子列表 >} <初始化算子列表 >→ <初始化算子 > x <初始化算子列表 >,<初始化算子 > 1.一维 数组 对应一 维数组 初始化 算子形 式是由 一对花括 号“{”、“}”括起 来的常 量 表 达式 表 ,各 个 常量表达式间用逗号分隔。例如   inta[5]={0,1,2,3,4}; 声明 5个元素 的 int类 型数组 a,并且 a中 各个元 素的初 值分别 是 0、1、2、3、4。 2.多维 数组 对应二 维数组 初始化 算子形 式是由 一对 花 括 号 “{”、“}”括起 来的 一维 数 组初 始化 算 子表。各个一维数组初始化算子间用逗号分隔。例如   inta[3][5]={ 0,1,2,3,4}, {1,2,3,4,5}, {2,3,4,5,6}}; 声明 3行 5列 的 int类 型二维 数组 a,并 且 a中各 个元素 的初值 分别是 : 01234 001234 112345 223456 6.5 数 组 初 值 · 143· 对应三 维数组 初始化 算子形 式是由 一对 花 括 号 “{”、“}”括起 来的 二维 数 组初 始化 算 子表。各个二维数组初始化算子间用逗号分隔。 对应四 维数组 初始化 算子形 式是由 一对 花 括 号 “{”、“}”括起 来的 三维 数 组初 始化 算 子表。各个三维数组初始化算子间用逗号分隔。 如此等等。 3.数据 个数与 数组长度 不一致 下面以 一维数 组为例 ,多维 数组的 情况可 以从一 维数组 类推。 (1)若 数据个 数小于 数组长 度,则 数组中 剩余元 素填 0,例 如:   inta[5]={1,2}; 则数 组 a中各 个元素 的初值 分别是 1、2、0、0、0。 (2)若 数据个 数大于 数组长 度,则 剩余数 据丢掉 ,例 如:   inta[5]={1,2,3,4,5,6,7,8,9}; 则数 组 a中各 个元素 的初值 分别是 1、2、3、4、5,剩余 数据 6、7、8、9被丢掉 。 4.数组 长度为 空 数组声 明的最 前一维 可 以 不指 明 本维 元 素 个 数 ,而由 初 始化 算 子 中 元 素的 个 数 决定。 例如:   inta[]={0,1,2}; 声明 一维数 组 a,a有 三个元素 (虽然在 声明中 没有指 出 a的 长度),初值分 别是 0、1、2。   intb[][4]={ 0,1,2,3}, {1,2,3,4}, {2,3,4,5}, {3,4,5,6}, {4,5,6,7}}; 声明 二维数 组 b,b有五行 (虽然在 声明中 没有指出 b数组 有几行 ),每 行四个元 素,并且 b中 各个元素的初值分别是: 0 1 2 3 0 0 1 2 3 1 1 2 3 4 2 2 3 4 5 3 3 4 5 6 4 4 5 6 7 注意,多维 数组声 明中,若省 缺长度 ,只 能是 最左边 的一 维,也就 是最 外层 一 维,下述 写 法都是错误的。   inta[3][]= … · 144· intb[][][4]= … intc[2][][5]= … 第六章 数  组 6.6  字 符 数 组 事实上 ,字 符数组 没有什么 特殊的 地方。 元素类 型是 char类型的 数组称为 字符数 组,字 符数 组用来 存放字 符数据 ,通常 使用的 字符数 组一维 的居多 。 字符数 组与字 符串有 着密切 的关 系 。通常 用字 符数 组保存 字符 串,可以 说 字符 数组 与 字符 串是一 个概念 。也可 以认为 字符串 是常量字 符数组 ,字 符数组 是字符 串变量。 如下是一个字符数组声明。   charst[10]; 如下是 相应数 组 st的一些 下标表 达式。   st[0]、st[i+j]、st[9] 1.初始 化 可以使 用上节 讲的一 般方法 对字符 数组初始 化,例如:   charst[]={ t,h,i,s, ,i,s, ,a, ,s,t,r,i,n,g, \0} 定义 17个元素 的字符 数组 st,并且赋 以初值   thisisastring\0 其中 “\0”是代码 值为 0的字符 ,在 C语 言中,定义 该字符 为字符 串结束 符。 也可以 直接使 用字符 串为字 符数组 赋初值,如下 声明与 上述声 明等价 。   charst[]= "thisisastring"; 也定 义 17个元 素的字 符数组 st,并且 赋以同 样的初 值,其中字 符串结 束符“\0”是 自动加 的。 事实上,在 C语 言中,把字符 串作为 字符 数组来 处理 ,反 之字 符数组 也被 当作字 符串 来 看。 可以认 为,字符串 是常量,字 符数组 是字符串 变量。 2.I/O C语言用 格式符 %s控 制字符 串的输 入 /输 出。把 %s用在 printf和 scanf函 数的格 式 串中 ,可 以直接 控制输 入或输出 一个字 符串。 当然,字符 串以字 符“\0”为结 束符。 在上述 说 明下 ,函 数调用     scanf("% s",st); 输入 一个字 符串,送到 字符数组 st中。假 如执行该 语句时 ,键 盘输入 china,则 st数组得 到值 如下: st: c h i n a \0 0 0 0 0 0 0 0 0 0 0 0 6.7 类 型 定 义 · 145· 可以 看到,操作 结果是 把 china送入字 符数组 st中 ,st数组不足 的元素 补 0。 函数调用     printf("% s",st); 把 st中 保存的 字符串 输出,当 然该 字 符串以 “\0”为 结束 符。如 果 st中仍 然是 上述结 果,则 在终端屏幕显示   china 3.运算 C语言定 义了字 符串上的 关系 运算、判 等运 算 和赋 值运 算。并 且在 运算 过 程中 可以 不 必管参与运算的字符串长度是否相等。 两个字 符串比 较大小 或比较 是否相 等,按 字典顺 序进行 。例如 ,在 上述操 作结果 下有:   st> "chin" st== "china" st< "0123456789" "thisisastring" > "thissastring" 得 true 得 true 得 false 得 false 字符串的赋值 运算是把一个字符串值赋值给一个字符型数组(即一个字 符串变量)。例如:   st="beijing"; 得到结果: st: b e i j i n g \0 0 0 0 0 0 0 0 0 0 6.7  类 型 定 义 C语言提 供了丰 富的数据 类型,包括 简单数 据类型 、构造 型 数据类 型等。 这些 数据类 型 有些 是基本数 据类型 ,有些是用 户自定 义的数 据类型 。到目 前为止 ,对 用户自 定 义 的数据 类 型,都是 直接定 义它的 结构,并直接 说明相 应类型 的变量 。类型 定义可 以给用 户 自 定义类 型 定义 名字,或给 已经有 名 字的 类型 定 义 别 名。 类型 定 义 以 保留 字 typedef为引 导,C语 言 把 typedef归 于存储 类说明 符。已经 学过的 各种数 据类型 使用 typedef的 形式如 下: 1.给已 有名字 起别名 给已经 有的类 型名(不管 是 C语言原 有的,还是 用户自 定义的 )定义一 个别名 ,形式 是:   typedef类型名 标识符; 该形 式为“类型 名”定义 别名,由“标 识符”代 表。例 如   typedefintINTEGER; typedeffloatREAL; · 146· 第六章 数  组 分别 为类型 int和 float定 义别名 INTEGER和 REAL,如此     INTEGER m,n; 相当于   intm,n; 而   REALu,v,w; 相当于   floatu,v,w; 2.定义 数组类 型名 定义数组类型名使用形式:     typedef  数 组   说 明符 该形 式中,数组 说明符 中的标识 符就是 给相应 数组类 型起的 名字,例如   typedefintta[10]; 说明 了类型 ta,它是一 个 10个元素 的 int类型 的数组 类型,如下 声明   taa; 相当于   inta[10]; 3.定义 枚举类 型名 定义枚举类型名有两种形式   typedefenum 名 {…}标识符; 和   typedefenum {…}标识符; 都定义标识符为一个枚举类型名。如下两个类型定义是等价的。   typedefenum color{red,yellow,blue}tcolor; 和   typedefenum {red,yellow,blue}tcolor; 都是 定 义 tcolor为 一 个枚 举 类 型 类型 名 。不 管 有 上 述 哪 个 类 型 定 义,如下 四 个 声 明 是 等 价的。   tcolorc; enum colorc; enum {red,yellow,blue}c; enum color{red,yellow,blue}c; 都是 说明一 个枚举 类型变 量 c。 这里要 提请读 者注意 :在 C语 言中一 定要 把类型 名与 变量 名 区别 开。一 个类型 可以 有 习 题 六 · 147· 名字 ,它 只是表 示一种 数据结构 的一 个框 架,而 不存 在一 个 实体 ,不 给它 分配 存储 空间。 只 有变 量才是 一个实 体,它具有一 块 存 储 空 间,并且 该 块 存 储 空间 的 结 构 是 相应 数 据类 型的。 任何 一个类 型可以 有多个 变量,每个变 量都具 有一块 存储空 间。 类型定义 定义一 个标识 符是某 类型的 名字,只定 义了相 应框架 的一个 同义 语,即所定 义 的标 识符具有 相应类 型表示 的框架 结构。 但它没 有一个 实体,没有 一块存 储空 间,亦即没 有 具体表示一个变量。变量在变量声明中声明。 本章小结 本章讲述 数组,开 始接触构 造型数 据类型 。包括 数组声 明、下标 表达式、多 维数 组 、数 组 初值 和字符 串,还讲述 了类型定 义。重 点掌握 数组的 结构和 应用,以及 类型与 变量的 区别。 习 题 六 6.1  设 有矩 阵 分别说明执行下述语句后 a和 c的结果是什么?   (1)for j=1;j<=3;j++) for k=1;k<=3;k++) c[j][k]=a[a[j][k]][a[k][j]];   (2)for( =1;j<=3;j++) for( =1;k<=3;k++) a[j][k]=a[a[j][k]][a[k][j]]; 6.2  用 两个 元 素 的 数组 表示 一 个 复 数 ,分 别 编 写 复数 的 加 、减 、乘、除 函数。 6.3  编 写函 数 ,求二 维 整 数 组 中 第二 大 元 素 。 6.4  编 写函 数 ,判断 任 意 给 定 的 二维 整 数 组 (100*100)中 是 否 有相 同元 素 。 6.5 如果一个关系是自 反、对称、传递 的,则称 它 为等 价 关系。 现在 给 定 域 D={d1,d2,…,d100}和 D*D上的关系 R,试编写一个函数,判断 R是否为等价关系。 6.6  编 写函 数 ,判断 给 定 整 数 矩 阵是 否 是 上 三 角 矩阵 。 6.7  编 写函 数 ,判断 给 定 整 数 矩 阵是 否 关 于 主 对 角线 对 称 。 6.8 编写函数,把矩阵 A的转置矩阵 A 存入 A中。 6.9 编写一个函数,把给定一维数组的诸元素循环右移 j位。 · 148· 第六章 数  组 6.10 编写一个函数,把给定一维数组的诸元素右移 j位,左边出现的空位以 0补足。 6.11  编 写 一 个 函数 ,把 整数 组 中 最 大 元 素与 第 一 个 元 素 对调 。 6.12 编写函数 strchange,把给定的字符串反序。 6.13 编写函数 strcat,把给定的两个字符串连接起来。 6.14 数列 x顺序由 2000以内各个相邻素数之差组成。求 x中所有和为 1898的子序列。 6.15  编 写 函 数 ,把 整 数 组中 值 相 同 的 元 素删 除 得 只 剩 一 个,并 把剩 余 元 素 全 部 串到 前 边 。 6.16 编写函数,找出给定二维整数组 A中所有鞍 点。若 一个数组元 素 A[i,j]正好 是矩阵 A第 i行 的最小值,第 j列的最大值则称其为 A的一个鞍点。 6.17 编写函数,对给定的字符串 判断 它是 否回 文字。回 文字 是指 正读和 反读 都相 同的 单词。例 如 rotor就 是 回 文字 。 6.18 编写函数,判断给定的十进制正整数在二进制中是否是回文数。回文数是指正读和反读都 相同 的数,例如 768464867就是十进制中的回文数。 6.19 编写程序,从键盘上输入两个字符串,比较这两个字符 串,输出 它们中第一 个不同 字符的 ASCII 码之差以及符号位置。例如,若输入 abcdef和 abcefgh,则输出 s1[4]-s2[4]=-1。 6.20 精确计算 300!。 6.21 二维数组 A10*10的每行最大元素构成向量 B,每列最小元素构成向量 C,求 B·C。 6.22 编写一个函数,判断对于任意给定的两个正整数 A、B,判断是否 A的因子和等于 B,且 B的因 子和等于 A。例如   220的因子和 =1+2+4+5+10+11+20+22+44+55+110=284   284的因子和 =1+2+4+71+142=220 6.23 编写程序,对任意给定的由字符“+”、“-”组成的字符串生成一 个由字符 “+”、“-”组成 的直 角三角形。生成规则是: (1)第 i行比第 i-1行少一个字符; (2)第 i行的第 j个字符由第 i-1行的第 j、j+1两字符生成; (3)当第 i-1行的第 j、j+1两字符相同时,第 i行字 符的第 j个字符 为“+”,否 则第 i行字 符的第 j 个字 符 为“-”。 例如,字符串“++----+-”将生成如图 6.30所示的三角形。 6.24  已 知 在 数 组    inta[20] 中按任意顺序存放着整数    1,2,3,… ,20 编写 一 个函 数 ,调 整 两 维 数 组    floatb[20][20] 图 6.30 字符三角形 使调整后的 b[i][j]中正好保存调整前 b[a[i]][a[j]]的值。 6.25 设整数集合 M 定义如下: (1)1∈ M; (2)若 x∈M,则 2x+1∈M,3x+1∈M; (3)没有别的整数属于集合 M。 习 题 六 · 149· 编写程序,按递增顺序生成并输出集合 M的前 30项。再编写 一个函 数,对 任意给 定整数 z,判断 它是否 属 于集合 M。 6.26 用实数数组存储多项式,数组的 i个元素存储多项式的 i次幂的系数,如多项式 5.7x5 -10.8x3 +0.49x2 +2.7 表示为 0 2.7 1 2 3 4 0 0.49 10.8 0 5 5.7 分别 编 写一 个 函 数 ,实 现 上 述 存 储方 式 的 多项 式 的加 法 、乘 法 和 除 法 。 6.27  设 多 项 式 的系数和幂次存于如下表中: P(x)=anxn +an-1xn-1 +… +a1x+a0 例 保存成 幂次 幂次 幂次 … 幂次 系数 系数 系数 … 系数 P(x)=3.0x5 +4.2x3 +2.1x2 +7 5 3 2 0 3.0 4.2 2.1 7 设计 保 存多 项 式 的 数 据 结 构,并 编写 一 个 函数 计 算多 项 式 的 值 。 6.28 编写函数,计算 2n阶多项式 anx2n +an-1x2(n-1) +… +a2x4 +a1x2 +a0 的值。其中系 数 an、an-1、…、a2、a1、a0 保 存 在 浮点 数 组 之中 ,以参 数 的 形式 带 入 函数 ,函 数值 为 多 项 式 的值。 6.29 为了实现高精度计算,可以把正整数 m存储在有 n个元素的数组中。例如    m =198874306923 可以存储为 00 0 0 00 0 1 98 874 3 0 692 3 编写 函 数,分 别求 如 上 表 示 方 法 的两 个 正 整数 的 加法 、减法 、乘 法 和除 法 。 6.30 设函数 y=f(x)以如下表的形式给出: x1 x2 x3 … xn y1 y2 y3 … yn · 150· 第六章 数  组 编写 一 个函 数 ,用 插 值 公 式 y1 +yx22 --yx11(x-x1), x< x1 计算点 x处的函数值。 y= yi +yxii++11 --yxii(x-xi), xi≤ x≤ xi+1 yn +yxnn--11 --yxnn(x-xn), x> xn 6.31 编写一个函数,统计给定二维整数组中有多少 个互不 相同的 数,以及每 个数的 出现频 率。最 后 按出现频率的递增顺序输出。 6.32 设 x1、x2 分别是 n维空间的两个点,编 写函数,求该 两点间 距离。提示:n维空 间两点 间距离 公 式是 n ∑(x1i -x2i)2 i=1 其中,x1i和 x2i分别是点 x1、x2 在第 i维上的坐标值。 6.33 编写函数,按下述方法对数组 A进行排序:首先把数组 A的前半和后半分别进行排序,然后再将 排好序的两半按序合并。 6.34 Shell排序是于 1959年由 DenaldL.Shell提 出的,它的思想是:首先把要排序的序列分 成前后两 组,比较并交换这两组数据的对应元素,使前一组的元素总是大于后一组的对应元素;然后使组的长度 缩小 一半,把数据分成四组,比较并交换相邻 两组数 据的对 应元素,使 前一组 的元 素总 是大 于后一 组的 对应 元 素;然后再使组的长度缩小一半,把数据分成八组,如此等等,不断缩小数据组的长度,直到组的长度缩 小到 一,并使前一组的元素总是大于后一组的对应元素为止。即完成了对序 列的排序。编写 一个函数,用 Shell 排序法实现对整数组的排序。 6.35 编写函 数,先 对任意给定的 m×n阶整 数矩阵的每 行按递增顺 序排序,然后再按行 以每行第 一 个元素为关键字以递增顺序排序。使    a11 <a12 <… <a1n    a21 <a22 <… <a2n       ⁝    am1 <am2 <… <amn 且    a11 <a21 <… <am1 6.36 编写函数,对任意给定的 m×n阶整数矩阵按行进行递增排序,使 a11 <a12 <… <a1n <a21 <a22 <… <am1 <am2 <… <amn 6.37 改进本章例 6.6的“逐步增加递增子序列”排序方法,在寻找新加入项的 位置 j时采用对半 检索 方法进行。 6.38  摇 动 排 序 是冒 泡 排 序 方法的 一 个 改 进 算 法 。 在冒泡排序过程中,若每次扫描时都记录第一个发生数 据交换的位 置,显然这 个位置 以前的 元素是 已 经排好序的,在下一次扫描时从这个位置开始即可;另外,若 再把扫描方 向改成一次 从前向 后,一 次从后 向 习 题 六 · 151· 前,还 可 以 提 高排 序 速 度 。 编 写 一个 实现 数 组 摇 动排 序 的 函 数 。 6.39  整 理 名 字 表。 编 写程 序 ,输 入 任意顺 序 的 名 字 表 ,将 其 按 字典 顺 序 排 序 并输出 。 6.40 设有两个 n元整数数组 a1、a2。保存的 2n个值 互不相同,且都已经 按递增排序。编写 函数,求 这 2n个数中值为中间的两个数。 6.41 编写 函数,对 给定整数数 组 A的元 素从小到大 进行编号并 输出。要求不改变 数组 A的元 素位 置,相 同 元 素 编号 相 同 。 例 如 : 7364836对应编号为 4132513 6.42 数组 A未排序,现有一个索引数组 B保存 A的下标。编写程序,不改变数组 A,只改变数组 B,完 成对 A的排序,如图 6.31所示。 6.43 编写函数,把给定的整数数组中 0元素全部移到后部,且所有非 0元素的顺序不变。 6.44 设有编号为 1到 20的 20个城市,d(i,j)表示城市 i到 j之间的距离,令 md(i)=min{d(i,j) 1≤j≤n,i≠j} 表示城市 i的孤立半径。用二维数组存储城市间的 距离,编写 程序输 入各个 城市间 的距离,求孤 立半径 最 大的那些城市。 6.45 将矩阵 Ann按顺时针方向旋转 90°。 6.46 编写函数,求给定二维整数组中出现频率最高的 数。例如在 (2,3,4,3,5,7,5,5)几个数中 5出 现的频率最高。 6.47 编写程序,把自然数列 1、2、3、4、5、…成螺旋形放入方 阵 Amm中,并打印该 方阵。例如,五阶 方阵 A55的形式如图 6.32所示。若所有主对角线和副对角线上的元素不排列数字,程序算法如何改进?   图 6.31 索引数组 图 6.32 五阶方阵 A55 6.48 用高斯-塞德尔迭代法求解线性方程组 AX=b的解向量。提示:把方程组变形为 X=AX+b的 形式。 6.49 编写函数,把给定 n×n长的一维数组 A按蛇形存入 n阶方阵中。例如当 n=4时,得图 6.33所 示的数组。 6.50  奇 数 阶 魔 方阵 问 题 。 设 n为奇数,编程序把前 n2 个自然数填入 n阶 方阵中,使方 阵的每 行、每 列、主 对角线、次对 角线上 诸 元素之和均相等。 可以采用如下算法:首先把 1放在第一行中间的那个元素上,然后总是向右上方排列 下一个数(即,若 当前数 m放在元素 Aij上,则下一个数 m+1放在元素 Ai-1,j+1上),但当遇到 下述四 种特殊 情况时 要进行 相 应特殊处理: (1)当 填 完 一 列 的 第 一个元 素 (A1,j)后 ,下一个 元 素 是 下一 列 的 最 后 一 个元 素 (An,j+1); · 152· 第六章 数  组 (2)当 填 完 一 行 的 最 后一个 元 素 (Ai,n)后 ,下 一 个 元 素 是 上 一行 的 第 一 个 元素 (Ai-1,1); (3)当填 完 一 个 元素 (Ai,j)后 ,下一 个 元素 (Ai-1,j+1)已 经填 入 数 了 ,则 下 一 个 数 填入 当 前 元 素 下 边一 行 的那 个 元素 (Ai+1,j)上 ; (4)当 填 完 元 素 (An,n)后,则 下一 个 数 填 入 元 素 (An-1,n)上 。 如图 6.34所示的五阶魔方阵描述了上述过程。 图 6.33 蛇形数组 图 6.34 五阶魔方阵 6.51 如图 6.35所示,有 20个数呈圆盘形摆 放。用数组 表示该 圆盘,并分别 求出圆 盘中的 最大数 和 最小 数 。进 一 步 分 别 求 出连 续 四 个 数 相 加之 和 最 大 、最 小 的 四 个 数及 其 和 。 6.52 编写程序,打印如图 6.36所示的四叶玫瑰线图形(ρ=a*sin2θ)。  图 6.35 呈圆盘形摆放的 20个数 图 6.36 四叶玫瑰线   6.53 平面上有 100个点,任意三个点均可构成三角形。编写程序,求面积最大的三角形及其面积。 注:若 a、b、c是三角形的三条边,则 SΔ = S-(S-a)(S-b)(S-c) S= 12(a+b+c)    6.54  求 最 大 和 子序 列 。设 有 整数 序 列 习 题 六 · 153· A=a1,a2,… ,an 编写程序,输入该序列 A,并在 A中找一个子序列 S=ai,ai+1,… ,aj 使子序列 S的和在 A序列的所有子序列中最大。 6.55 学生成绩档案管理。设有 m个学生,n门课程,用一个 m×n矩 阵存储学生 成绩(可能某门 课程 有某 学 生不 选 )。编写 程 序 作 如 下 统 计: (1)每 个 学 生 所 选 的 课程总 数 及 平 均成 绩 ; (2)一 门 课 程 的 平 均 成绩; (3)一 门 课 程 不 及 格 学生的 名 单 ; (4)各 个 学 生 不 及 格 课程的 清 单 ; (5)各 个 学 生 优 秀 课 程的清 单 ; (6)按 如 下 加 权 成 绩 公式排 序 并 输 出加 权 排 序 的 成 绩单 。 每个学生所选的课程平均成绩 +10×所选的课程总数 6.56 判断一个字符序列中(与 ),[与 ],{与}是否配对且互不相交。 6.57  把 五 个 长 度不 等 的 递 增序列 合 并 成 一 个 序 列。 6.58 求以 M ×N矩阵中四个最大元素的下标为坐标构成的四边形的面积。 6.59 编写一个函数,把给定的整数翻译成长度为 10的字符串。 6.60 编写一个函数,把给定的实数按定点形式翻译成保留三位小数长度为 15的字符串。 6.61 编写一个函数,把给定的实数按浮点形式翻译成长度为 20的字符串。 6.62 设 a1,a2,…,an 是由 n个 0到 9的数字组成的一个排列,分别编写一个函数,对给定的排列 A生 成新的排列 b1,b2,…,bn,使 (1)整数 b1b2…bn 是所有大于整数 a1a2…an 中的最小者; (2)整数 b1b2…bn 是所有小于整数 a1a2…an 中的最大者; (3)整数 b1b2…bn 是所有整数中的最大者; (4)整数 b1b2…bn 是所有整数中的最小者。 6.63 编写一个函数,对给定的正整数 m,n,输出 n的十进制小数,直到出现循环为止,并指明循环节。 m 6.64 编写一个函数,求出给定序列 A中所有递增和递减子序列的总数。 6.65 编写程序,读入一篇正文,统计并输出字长度的频率分布 (一 个字母的字 有多少,两个 字母的 字 有多 少 ),以 及字 长 度 的 平 均 值和平 均 偏 差 。 6.66 一个正文包含的每个字不超过 10个字符,全 文不超过 1000个 字。编写程序处 理该正 文,统 计 并输 出 每个 字 的 出 现 次 数 ,并 找 出及 输 出 出现 次 数最 高 的 字 。 6.67 把一副 52张扑克牌按红桃、黑桃、草花、方块以及 A、2、…、9、10、J、Q、K的 顺序全部正 面朝 上排 成 一列 ,然后 从第 2张牌开始,把凡是 2的倍数的牌全部翻转; 从第 3张牌开始,把凡是 3的倍数的牌全部翻转; ⁝ 从第 52张牌开始,把凡是 52的倍数的牌全部翻转。 · 154· 第六章 数  组 求最后有多少张牌正面朝上。 6.68  编 写 程 序 ,模 拟 用 扑克 牌 摆 十 二 月 的游 戏 。 取一副扑克牌的 A、2、3、…、10、J、Q共 48张牌,洗牌 后把它们随机 地摆成十二 摞,每 摞 4张。从第 一 摞的最下边取出一张牌并翻开,该牌是几就把它放在第几 摞的上边;然后再从 该摞的最下 边取出 一张牌 并 翻开,该牌是几就把它放在第几摞的上边;如此等等。直到第一摞的 4张牌全部被翻开,从其他摞翻出 的某 一张牌是 1,放在第一摞上后,又要求翻第一 摞的最 下边的 牌为止,游戏 结束。这 时若 48张牌全 部翻开 并 归位 ,则 游 戏 成功 ,否 则 游戏 失 败 。 要 求 洗牌 和 分 牌用 伪随 机 数 发 生 器 实 现。 6.69 一个有 n个顶点的无向图可以用一个数组表示:    boolgraph[n][n] 若顶点 u、v之间有边相连,则 graph[u][v]为 true,否则为 false。若 graphn[u][v]为 true,表示从顶点 u至多 经过 n步可以到达顶点 v。编写一个程序,读入一个无向图,计算并打印 graphn。若图中最长路径的长 度小 于 n,怎样结束程序使计算量减少?graphn 由 n次布尔矩阵乘法得到,该乘法中以布尔运算 ||和 &&代 替加 和乘。 6.70 分别产生 两 个 0到 50间 的 由 30个 数 组 成 的 随 机 数 集 合 a、b。 再 分 别 求 出 {a}-{b}及 {b} -{a}。 6.71  下 列 竖 式 中每 个 不 同 字母代 表 不 同 数 字 : send +) more m oney 编写 程 序,破 译每 个 字 母 所 代 表 的数 字 。 6.72  约 瑟 夫 (Josephus)问 题 。 古代某法官要判决 n个犯人死刑,他有一条荒唐的逻辑,将犯人首尾相接排成圆圈,然后从第 s个 人开 始数起,每数到第 m个犯人,则拉出来处决;然 后再数 m个,数到的 犯人再 处决;依此 类推,但剩 下的最 后 一个 犯 人可 以 赦 免 。 编 写程 序 ,给出 处决 顺 序 ,并 告知 哪一 个 人 活 下 来 。 6.73 设有一头母牛,它每年生一头小母牛;每头小母牛 从 4岁开 始,它 每年也 生一头 小母牛;编写 程 序,求第 20年时共有多少头母牛。 6.74 模拟生命游戏(该游戏由英国剑桥大学 JohnHortonConway发明)。 在一个由方格组成的矩形阵列上,每个方格可以包含一个机体。每个方格 和 8个方格 相邻,用 occ(k) 表示与方格 k相邻的被机体占有的方格数。生命游戏由如下两条规则从前一格局产生新的格局: (1)若 2≤occ(k)≤3,则在方格 k中的机体存活到下一格局,否则它死亡,该方格变为空; (2)若一个空方格 k的 occ(k)=3,则在该方格中生成一个机体。 编写 程 序,用 数组 表 示 该 生 命 游 戏的 矩 形 阵列 并 完成 如 下 工 作 : ① 读入初始配置; ② 按上述规则生成并打印一系列配置。 图 6.37所示为由 7个方格的 U形模式生成的前八步配置。注意,从一个配置到下一个配置时,所有改 变是同时发生的。 6.75  密 码 文 (密 文 )解 密。 习 题 六 · 155· 图 6.37 模拟生命游戏 密文由字符序列组成,解密后产生的字符序列称原文。解密算法是把密文 s1,s2,…,sn 看成一 个环(如 图 6.38所示)。解密时先按 s1 的 ASCII码 n,从 s1 开始在环上按顺时针方向数到第 n个字符,即 为原文的 第一个字符,从环上去掉该字符;然后取下一个字符的 ASCII码 n,并从下一个 字符开始在 环上按顺时 针方 向数到第 n个字符,即为原文的第二个字符,从环上去掉该字符;如此等等,直到环上没有字符为止,即 可得 到原文。用字符数组 A存放密文环,不允许使用其他工作数组,编写程 序,对给 定的密文进 行解密,密后 的 原文仍存放在数组 A中。 6.76 稀疏矩阵可以用一个 n行 3列矩阵只存储非零元素,每行存储一个非零元素,第一 列为行标;第 二列为列标;第三列为非零元素本身。例如,如图 6.39所 示的矩 阵存储 为如图 6.40所示 的形式。编写 程 序,实 现 上 述 存储 方 案 的 稀 疏 矩 阵的 乘 法 。 图 6.38 密文环 图 6.39 稀疏矩阵 图 6.40 存储结果   6.77 因为对称矩 阵 有性 质 aij =aji。为 了节 省 存 储,可 以 只存 储 对 称矩 阵 的 下三 角 元 素,例 如,如 图 6.41所示的矩阵,可以以行主序存储成如图 6.42所示的形式。分别编写函数实现如下功能: (1)把 给 定 的 下 三 角 形式的 以 行 主 序存 储 的 对 称 矩 阵还 原 成 原 形 ; (2)实 现 下 三 角 形 式 的以行 主 序 存 储的 对 称 矩 阵 的 加法 和 乘 法 ; (3)把行主序存储形式改成列主序存储形式,即将如图 6.42所示的矩阵存成如图 6.43所示的形式。 · 156· 第六章 数  组 图 6.41 对称矩阵 图 6.42 行主序存储结果 图 6.43 列主序存储结果   6.78 罗马数是采用罗马表示法书写的数,只包含字符 I、V、X、L、C、D、M,其个位数、十位数、百位数、 千位数分别如下:                             I II III IV V VI VII VIII IX 10 20 30 40 50 60 70 80 90 X XX XXX XL L LX LXX LXXX XC 100 200 300 400 500 600 700 800 900 C CC CCC CD D DC DCC DCCC CM 1000 M 按千 位 、百 位 、十 位 、个 位 的 次 序 把若 干罗 马 数 字 连接 起 来 ,就 构 成 罗 马 数 ,例 如    9   0   02   29 XIX LX DII CMXXIX 编写 一 个函 数 ,把 给 定 的 罗 马 数 字符 串 (假 设 没有 错 误 )翻译 成 阿 拉 伯 数 。 第七章 指  针 指针是 高级程 序设计 语言中 一个 重 要 的 概念。 正确 灵活地 运用 指针,可 以 有效 地表 示 和使 用复杂 的数据 结构;并可动 态分 配内 存 空间 ,节 省程 序 运行 空间 ,提 高运 行效 率。但 是 如果 不能正确 理解和 使用指 针,指针将 是程序 中最危 险的成 分,由此带 来的后 果 可 能是无 法 估量的。 7.1  基 本 概 念 为了理 解指针 的含义 ,必须 弄清楚 数据在 内存中 是怎样 存储的 ,又 是怎样 被访问 的。 在第三章 中读者 已经 了 解到 ,一 个 变 量 有它 的 值,同 时也 在 计算 机 内 存占 用 一 块 存 储 区,该存 储区的 地址就 是相应变 量的地 址,该存储 区保存 的内 容 就是 相应变量 的值 。例如 有 变量声明:   charc= S; intv=27,u=32; int* p=&v; 则编 译程序 分别给 变量 c、v、u、p分 配存储 空间,如图 7.1 所示。 必须理 解清楚 给 变 量 分配 的内 存区 域、该 内 存 区域 的 地址 、该 内存区 域 保存 的 内容 以 及 它 们 之间 的 关 系。 一个 变量 variable的指针 (或 者 称指 向 变 量 variable的 指 针 )就   图 7.1 变量存储分配 是给 它分配 的内存 区域的 地址(或者 说是给 它分配 的内存 区域地址 的首地 址)。 变量 c被 分配在 B8FF开始的 一个字 节的 内存区 域中 。 B8FF是 该存 储区域 的地 址,同 时也 是变量 c的地址 ,也 称 B8FF是变 量 c的 指针;该内存 区域中 存储的 字 符 S是 该存储 区 域的 内容,也是 变量 c的 值。 变量 v被 分配在 B800开始的 两 个字 节的内 存区 域中。B800是 该 两个 字节的 存储 区域 的地 址,同时 也 是 变 量 v的地 址 ,也 称 B800是 变 量 v的指针 ;该 内存 区域中 存储 的整 数 27 是该 存储区 域的内 容,也是变量 v的值 。 变量 u被分配 在 B802开始的 两个字 节的内存 区域中 。B802是该 两个字 节 的 存储区 域 的地 址,同时也 是变量 u的地址 ,也 称 B802是 变 量 u的 指 针;该 内存 区域 中 存 储 的 整 数 32 · 158· 第七章 指  针 是该 存储区 域的内 容,也是变量 u的值 。 变量 p被分配 在 B804开始的 四个字 节的内存 区域中 。B804是该 四个字 节 的 存储区 域 的地 址,同时也 是变量 p的地址 ,也称 B804是变 量 p的指 针 ;该 内存 区域 中存储 的 B800是 该存 储区域 的内容 ,也 是变量 p的值 。 p是指 向 int类型 变 量 的 指针 类型 变量,目前 它指 向 int类 型变量 v,所以变 量 p的 值是 int类 型变量 v的地址 ,也就是 B800,称 p指向 v。 访问变 量 v的内 容(使用 值),一 般 直 接使 用变 量 v的名 字,例 如 v*10表 示 :用 变量 v 的值 (27)乘以 10,得 270。 还可以 通过间 接方式 访问一 个变量 的内容 (使用 值),即通 过 指 向 相应 变 量 的 指针。 比 如访 问变量 v(使用 变量 v)可以用 方式   *p 来实 现。算式 (*p)*10同样 得到值 270。它通过 指 向 v的指 针 变量 p,采用 间 接 访问的 方 式实 现对变 量 v的访 问,取出变 量 v的 值参与运 算。 可以认为 地址与 指针是 同义语 。变量 的指 针 就 是 变量 的 地 址 ,存 放 变 量地 址 (指针 )的 变量是指针变量。 7.1.1 指针 类型和指针 变量 在 C语言中 ,任 何一个 类型都 伴随着 一个指向 本类型 变 量 的指 针 类型。 设 有 类型 T,则 指向 T类 型变量 的指针 类型用   T* 表示 。T称为该 指针类 型的基 类型。 指针变 量简称 指针,是一种 特殊的 变量,它里 面 存 储 的“值 ”被解 释为 一个 变量的 地址, 确切的说是计算机内存的一个地址。 说明指 向 T类型变 量的指针 变量使 用形式 :   T*p,*p,… *p; 其中 每个 p都是标 识符,是被说 明的指 针类 型的变 量,确切 的说是 “指向 T类 型变量 的指 针 变量 ”。例如   int*iptr1,*iptr2;       * 说明指向 int类型变量的指针变量 iptr1和 iptr2*/ char* cptr; /* 说明指向 char类型变量的指针变量 cptr*/ intx,y; charch= a; 指针 类型所 指向的 类型既 可 以是 基 本 数据 类 型 ,也 可以 是 构 造 型 数 据 类 型 ,甚 至 是 指 针 类 型,还可 以是函 数。经 常简称“指 向 T类型变 量的 指 针变量 v”为 “v指向 T类 型”或 “T类 型 的指 针 v”等 。 指针变量 的值是 内存地 址(宏观 上讲是 变量的 地址)。求 取不同 类型变 量或常 量地址 的 7.1 基 本 概 念 · 159· 表达 方式不 同。基 本类型 变量、数组 成员、结 构体变 量、联合 体变 量 等用 求地 址运 算符 “&” 获取 变量的地 址;数组 的地址与 其第一 个元素 (成员 )的地 址相 同 ,用 数组 名字 本身表 示;函 数的 地址为 函数的 入口地 址,用 函数名 字表示 。设有 操作:   iptr1=&x; iptr2=&y; cptr=&ch; x=5; y=8; //iptr1指向变量 x //iptr2指向变量 y //cptr指向字符变量 ch,ch有初值 a 如果 指针 iptr1所占 内存单 元的地 址为 E990,则编译 程序会 根据变 量声明 的先后 顺序为 其在 内存 中分配 空间,如图 7.2所示 。 如果执行 iptr1=iptr2;则 iptr1和 iptr2指 向同一 内存单 元,如图 7.3所示,其中 虚线标 出 指针 iptr1的变化。 图 7.2 内存分配示意图Ⅰ 图 7.3 内存分配示意图Ⅱ 要特别 注意指 针与数 组的关 系。数 组名(数组 变量 )本身 就是一 个指 针,可以认 为它 是 一个 常量指 针。另 外,还可以定 义 指 向 数 组的 指 针 和 指针 数 组 等 等。下 面是 与 数组 有关 的 一些 指针应 用,以后还 要详细介 绍和解 释这些 表达方 式。   intw[5]; int* pw=w; int* q=&w[2]; intrr[10][5],(* pr)[5]; pr=rr; int* qr[5]; //pw表示数组 w,或者说指向变量 w[0] //q指向变量 w[2] /* r是指向一维数组的指针, 该一维数组有 5个元素,每个元素是一个整数 */ //pr指向一维数组 rr[0],该 rr[0]有 5个元素 /* r是指针数组,有 5个元素,每个元素是一个指向 int类型变 量的指针 */ · 160· int* (tr[5]); qr[1]=&y; 第七章 指  针 /* 同 qr。tr是指针数组,有 5个元素,每个元素是一个 指向 int 类型变量的指针 */ //qr[1]指向变量 y 7.1.2 指针 所指变量 指针变 量和指 针所指 变量是 两个 不 同 的 概念。 指针 变量即 指针,它 本身 存 储某 个内 存 地址 (某个变量 的地址 )。指针 所指变 量为指 针变量 中所保存 的内存 地址对 应的变 量。设 有 程序片段:   int* iptr; inti=3,j; iptr=&i; 则其 内存分 配如图 7.4所示 。 在图 7.4中,系统 为 int型 指 针 变 量 iptr分 配的 内 存 单 元地 址为 EAB0,iptr本 身 值 为内 存 地 址,即 EAB4。 而 地 址 EAB4对应的 是 int型变 量 i,则 int型指 针变量 iptr所指变 量   图 7.4 内存分配示意图Ⅲ 为 int型变量 i,即 iptr指向 i。 运算符 “*”访问 指针所指 变量的 内容,称为 间接引 用运算 符。若 有语句     j=* iptr; 将把 iptr所指 内存单 元(EAB4)中的内 容送入 变量 j,此时 j的 值为 3。 * iptr表示指 针变 量 iptr所 指变量 的内容 。 一定要 区分开 指针变 量和指 针所 指 的变 量;进一 步一定 要区 分开指 针值 和 指针 所指 变 量的值。 例 7.1 指针 变量与 指针所 指变量 。   #include <stdio.h> void ain(){ inti,j; charch; int* pi,* pj; char* pch; printf("Inputaninteger:"); scanf("% d",&i); printf("Inputanotherinteger:"); scanf("% d",&j); printf("Inputachar:"); scanf("% c",&ch); pi=&i; * 1*/ /* 2*/ /* 3*/ /* 4*/ /* 5*/ /* 6*/ /* 7*/ /* 8*/ /* 9*/ /* 10*/ /* 11*/ /* 12*/ /* 13*/ 7.1 基 本 概 念 · 161· pj=&j; /* 14*/ pch=&ch; /* 15*/ printf("i=% d  j=% d  ch=% c\n",i,j,ch); /* 16*/ printf("*pi=%d *pj=%d *pch=%c\n",*pi,*pj,*pch);   /* 17*/ } /* 18*/ 该程序运行结果为:   Inputaninteger:23 Inputanotherinteger:45 Inputachar:r i=23j=45ch=r *pi=23* pj=45*pch=r //这时在键盘输入 23 //这时在键盘输入 45 //这时在键盘输入 r //这是程序第 16行输出的 //这是程序第 17行输出的 其中变 量 i、j、ch在内 存中存 储的内 容,分别通 过对指 针 pi、pj、pch的间接 引用而 获得。 例 7.2 用指 针变量 实现:输入 两个整数 ,按从大 到小顺序 输出。   #include <stdio.h> void ain(){ inti,j; int* pmax,* pmin,* p; printf("Inputaninteger:"); scanf("% d",&i); printf("Inputanotherinteger:"); scanf("% d",&j); pmax=&i; pmin=&j; if(i<j { p=pmax; pmax=pmin; pmin=p; } printf("max=% d,min=%d\n",*pmax,* pmin); } /* 1*/ /* 2*/ /* 3*/ /* 4*/ /* 5*/ /* 6*/ /* 7*/ /* 8*/ /* 9*/ /* 10*/ /* 11*/ /* 12*/ /* 13*/ /* 14*/ /* 15*/ /* 16*/ /* 17*/ 假设输入 25、38,该程序 运行到 第 10行结束 时,各个变 量的状 态 如 图 7.5所示,当程 序 执行 到第 15行 结束时 ,各 个变量 的状 态 如图 7.6所示 。可以 看 出变 量 i、j的内容 并没 有交 换,而指 向它们 的指针 变量 pmax和 pmin的 内容交 换了。 请读者 注意符 号“*”的用 法: (1)“* ”放在表 达式中的 指针变 量之 前,表 示“对 指针所 指变 量的 访问”,称为 间接 引 用操 作符。 例如例 7.1第 17行 打印语 句中的“* pi”、“*pj”、“*pch”。 (2)“* ”放在指 针定义中 时,表示 指针类型 ,称 为指针 定义 符 ,用来说明 指针 变量。 例 如例 7.1第 5行 的 “int* pi,* pj;”说 明 指 向 int类 型 的 指 针 变 量 pi、pj;第 6行 的 · 162· 第七章 指  针 “char*pch;”说 明指向 char类型 的指针 变量 pch。 图 7.5 到第 10行结束 图 7.6 到第 15行结束 7.1.3 空指 针与无效指 针 NULL是 C指 针类型 的一个 特殊值 ,称 为“空 ”,表示 指 针 变 量的 值为 空,不 指向 任何 变 量或 函数。 NULL值 属于所 有指针 类型。 判断指针 变量 iptr的值是 否为空 可以使用   iptr!=NULL 或 iptr== NULL 保证 指针在 没有指 向有效 对象或 函数时 取值为 NULL是一 种良好 的编程 风格。 有时可能 不小心 生成无 效 指 针(invalidpointer)。无 效 指 针 是指 一个 指针 变 量 无 值,它 既没 有指向确 定的变 量或函 数,也不是 NULL。程序 中存 在无 效指针 ,不 是好 的程序 设计 风 格。 产生无 效指针 的原因 很多,例如 (1)说 明指针 变量后 还没有 给它赋 值; (2)把 整型变 量转换 成指针 ; (3)回 收为指 针所指 对象分 配存储 空间; (4)指 针运算 超出数 组范围 。 访问空指 针(指针 变量的 值为 NULL)或无 效指针 (指针变 量无值 )所指向 的 内 容当然 是 错误的。 7.1.4 通用 指针 通用指针 类型的 值是一 个指针 值,但是该 值不属 于任何 类型。 具体来 讲,通 用 指针所 指 向的 内存空 间没有 被作为 任何一 个数据 类型 的变量 。 在 实 际应 用 当 中,对 事 先 无法 确定 类 型的 空间,可以 先使用 通用指针 标识,然后 在具体 使用之 前,对其进 行强制 类型 转换,得到 相 应类 型的变量 ,再对其 进行操作 。通用 指针所 指向的 空间可 以转换 成任何 类型 的变 量 ,通 用 指针说明符是:   void* 通用 指针变 量可以 被赋以 任何一 个 指针 类 型 的 值;同 时任 何 类 型 指 针变 量 也 可 以被 赋以 通 用指针值。例如:   void* general_ptr; /* 声明通用指针变量 */ 7.1 基 本 概 念 · 163· int* int_ptr; char* char_ptr; general_ptr=int_ptr; int_ptr=general_ptr; general_ptr=char_ptr; char_ptr=general_ptr; int_ptr=char_ptr; char_ptr=int_ptr; int_ptr=(int* )char_ptr; /* 声明 int类型指针变量 */ /* 声明 char类型指针变量 */ /* OK*/ /* OK*/ /* OK*/ /* OK*/ /* 错误 ,类 型 不一致 */ /* 错误 ,类 型 不一致 */ /* OK*/ 例 7.3 分析 下述程 序片段 的执行 结果。 该例题 主 要请 读者 体会指 针变 量和指 针所 指 变量 的概念 及其区 别,同时也体 会值为 NULL的 指针不 指向任 何变量 。     int* p,* q,u,v; q=&u; *q=3; p=&v; *p=* q; *p=5; p=q; *p=7; p=NULL; *p=3 解 首 先执行 q=&u;得 图 7.7;再 执行 * q=3;得图 7.8;再执 行 p=&v;得图 7.9;再 执行 *p=* q;得 图 7.10;再 执 行 * p=5;得 图 7.11;再 执 行 p=q;得 图 7.12;再 执 行 *p=7;得图 7.13;再 执行 p=NULL;得图 7.14;再 执行 * p=3;发生 错误,因为 p不指向 任 何变量。 图 7.7 执行 q=&u 图 7.8 执行*q=3 图 7.9 执行 p=&v 图 7.10 执行*p=*q 图 7.11 执行*p=5 图 7.12 执行 p=q · 164· 第七章 指  针       图 7.13 执行*p=7 图 7.14 执行 p=NULL       7.2  指 针 运 算 涉及指针 的运算 包括前 边已经 接触过 的 赋值 运算 “=”、求 变 量 地 址运 算 “&”和 访问 指 针所 指变量 内容的 运算“*”。 本节还 将介绍 算术运 算“+”、“-”、判等运 算和关 系运算 。 1.求地 址 & “&”运算 符用来 求被操作 对象的 地址。 例如:&x表示变 量 x在 内存 中的存放 地址 ,若 x 的地 址为 ABD0,则 &x的值为 ABD0。 “&”的优 先级为 15。 2.取内 容 * “*”与“&”互为逆 运算。“* ”运算访 问地址 表达式 所指变 量。例 如:“x=*p”是将 指 针 p所指变 量的值 送入变 量 x中;“*p=5”是将 5送入 指 针 p所指 变 量 中。“* ”的优先 级 也为 15。 3.赋值 = 可以把一 个指针 值赋值 给某个 指针变 量。所 谓指针 值是指向 某变量 指针(变 量的地 址) 或函数的指针。例如:     int* px,x; px=&x; px=NULL; px=4800; * 指针 px指向变量 x*/ /* 为 px赋空指针*/ /* 将地址 4800赋给 px,而不是整数类型 4800*/ 4.加 +(++) 如果指 针值指 向数组 的一个 成分,C语 言允 许 对相 应指 针值 加上 一 个整 数表 达式。 设 指针 p指向 数组变 量 a的一 个成分 ,把 指针 p与一个 整数 k相加,得到 的结果 值 仍 然是一 个 指针 值,该值指 向的是 “数组 a从 p原 来 所 指成 分 开 始,向 数 组 尾 部 移动 k个 成 分 后 的 成 分”。 例如:     int* p,* q,* r,a[100]; p=&(a[10]); 则 7.2 指 针 运 算 · 165·   p+3 指向的实际地址是     &(a[10])+3* sizeof(int) 即 a[13]的地 址,若 有   q=p+3; 将使得 q指向 a[13]。 与前文 一样“p++”表 示“p=p+1”。 5.减 -(--) 指针的 减法运 算包括 :指针 值减去 一个整 数表达 式和两 个相容 的指针 值相减。 (1)如 果指针 值指向 数组的 一个成 分,C语 言允许 对 相 应指针 值减去 一个 整数 表 达式。 设指 针 p指 向数组 变量 a的 一个成 分,把指针 p与一 个整数 k相减 ,得 到的结 果 值 仍然是 一 个指 针值,该值 指向的 是“数组 a从 p原 来 所 指成 分 开 始 ,向 数 组 首部 移 动 k个成分 后的 成 分”。 例如:     int* p,* q,* r,a[100]; p=&(a[10]); 则   p-3 指向的实际地址是     &(a[10])-3* sizeof(int) 即 a[7]的 地址,若有   q=p-3; 将使 得 q指 向 a[7]。 与前文 一样,“p--”表示 “p=p-1”。 (2)如 果两个 指针值 相容,即 它 们 指 向的 对 象 是 同一 个 类 型 的,则可 以进 行 相减 运算。 得到 的结果 是整数 类型的 值,为 两个指 针值之 间的距 离。例 如     int* p,* q,* r,a[100]; p=&(a[10]); q=&(a[15]); 则 p-q得 5;而 q-p得 -5。 指针的加减运算有如下限制: ① 若 p指向的 不是数组 成分,或者 p+k或 p-k后超出数 组 a的 范围,其行 为是未 定义 的,将产 生不可 预料的 结果; ② 不 能针对函 数指针 或 void* 类型指 针进行 加减运 算; ③ 不允许两个指针间进行加法运算。 · 166· 第七章 指  针 6.判等 运算和 关系运算 C语言还 可以针 对兼容类 型的指 针进 行判 等运算 和关 系运算 ,得 到的 结果 是 bool类 型 的 true或 false。 针对指 针的判等 运算和 关系运 算包括 : (1)判 断两个 指针值 是否相 等或不 相等(==或!=); (2)比 较两个 指针值 的大小 关系(>、>=、<、<=)。 例如: px<py判断 px所指向 的存储 单元地 址是否小 于 py所指 向的存 储单元地 址。 px==py判 断 px与 py是否 指向同 一个存 储单元 。 px==0、px!=0、px==NULL、px!=NULL都是判 断 px是否为 空指针 。 一定要 注意参 与关 系运 算 的指针 值是否 是兼 容类 型的,如果 p指 向一个 int类 型 变量, 而 q指向一 个 float类 型变量 ,进行 p与 q的比 较是错 误的。 例 7.4 指针 运算例 。   #include <stdio.h> void ain(){ charstr[255],* p; intv; scanf("% s",str); p=str; whil(*p!= \0) p++; printf("Thestringlengthis% d \n";p-str); } 程序运行若输入:   abcdef 则输出结果为   Thestringlengthis6 7.3  指针 与数 组 指针与 数组有 着密切 的关系 。数组 名是数组 的首 地址,也就 是 a[0]的地 址。 指针值 也 是一 个地址,如 果一个 指针 p指 向数组 的首地 址,即指向 a[0],则 p与 a表示 的 是 同一个 对 象。事实上,在 C语 言中把 指针和 数组当 做同一个 概念看 待,数组名 是指针,指 针也 是 数组。 可以认为数组名是常量指针。 7.3 指 针 与 数 组 · 167· 7.3.1 用指 针标识数组 在 C语言中 ,数 组名是 指针,指向数 组的首 元素 (下标 为 0的元 素),也就 是数组 第一 个 元素的地址。可以把这个指针值送入指针变量中。例如:   inta[5]; int* iptr iptr=a; //也可以使用 iptr=&(a[0]) 则 a、&a[0]、iptr均 表示同 一内存 地址,即存 放数 组第 0个 元 素的 地址。 图 7.15示意了 a 与 iptr以及它 们之间 的关系。 图 7.15 指向数组元素的指针 请读者 注意,数组 名不 代 表整 个 数 组,而 是 数 组 第一 个 元 素的 地 址 ,即 数组 的 首 地址。 上述 “iptr=a”不是 把整个 数组 a全 部送入 iptr中,而是 把数组 a的首 地址(即 a[0]的地 址) 送入 iptr中。 访问数 组 a的 第 i个 元 素 既 可 以 使 用 数 组 名 a,也 可 以 使 用 指 针 变 量 iptr。 在 执 行 “iptr=a”操 作后,如下 表示方式 相互等 价。   a[i] *(a+i) iptr[i] *(iptr+i) 例 7.5展示了 指针与 数组之 间的关 系。 例 7.5    void ain(){ inta[5]; inti,* p; for(i= ;i<5;i++ ){ printf("a[%d]= ",i); scanf("% d",&(a[i])); } for(i= ;i<5;i++ ) printf("%2d",a[i]); printf("\n"); · 168· 第七章 指  针 for(i= ;i<5;i++ ) printf("%2d",* (a+i)); printf("\n"); for(p= ;p<a+5;p++ ) printf("%2d",* p); printf("\n"); } 程序 运行在 输入阶 段,结果是:   a[0]=1 a[1]=2 a[2]=3 a[3]=4 a[4]=5 其中 等号后 的数字 是输入 的。在 输出阶 段,输 出结果 是:   12345 12345 12345 通过该例题可以看出几种访问数组元素的方式是等价的。 使用指 针表示 数组应 该特别 小 心。 C语 言 的 指 针 十 分灵 活 ,也最 容 易出 错。请 特别 注 意如下几点。 1.数组 名是指 针常量 指针变 量可以 参与运 算,比 如例 7.5中的 语句:   for(p= ;p<a+5;p++) printf("%2d",* p); 该语 句中,对指 针变量 p不断进 行增 1运算。 虽然数 组也是 指针,但是 对数组 变 量 却不能 这 样,因为 数 组变 量是 指针 常 量。 下述 语句 是 错 误 的,对数 组变 量 a的 增 1运 算 “a++”是 非 法的。   for(a= ;a<p+5;a++) printf("%2d",* a); 2.指针 变量的 当前值 经常用指针变量表示数组。比如有     inta[10],* iptr; iptr=a; 若访 问 a数组 的第 i个元 素,如下几 种写法都 是等价 的,这在前 边已经 介绍过 。   a[i] 7.3 指 针 与 数 组 · 169· *(a+i) iptr[i] *(iptr+i) 但是 是否任何 时刻这 四种写 法都等 价呢? 回答 是 否 定 的,比 如 ,若 语句 “iptr=&(a[2])”被 执行 后,则 上 述 iptr[i]、* (iptr+i)访 问 的 是 数 组 元 素 a[i+2],而 不 是 a[i],它 们 与 a[i+2]、*(a+i+2)等 价。写 程序时 要特别 注 意 指针变 量的当 前值,有 时 由于忽 略指针 变 量的 当前值,会 使程序 产生严重 错误,而且 这种错 误还十 分 难于 查 找 。下述例 7.6说明该 问 题。 该程序 原想输 入 10个整数 ,再把 它 们打 印出 来 ,但是程 序运 行结 果 却不 然。原 因就 在 于忽 略了指 针变量 当前指 向的位 置。请 读者找到 程序错 误,并改正 。 例 7.6 输入 10个整数 ,再把 它们打 印出来 ,指 针变量 错误使 用的例 子。   #include"stdio.h" void ain(){ inti,* p,a[10]; p=a; for(i=0 i<10;i++) scanf("% d",p++); for(i=0;i<10 i++){ printf("%d\n",*p); p++; } } 使用指 针形式 访问 数组 元 素,是 从指 针 变量 当 前 所 指 的 位 置 开 始“向前 ”或 “向后 ”计 算,而不 是当初 给指针 变量赋的 初值。 3.数组 超界 数组超界是指访问不存在的数组元素。例如有如下声明:     inta[10],* aptr; 声明 数组 a有 10个元素 ,下 标分别 为 0、1、… 、9。 但是如 果要访 问 a[-5]或者 a[10]显 然 是错 误的。这 种明显 的错误 任何人 都不容 易犯,问题 是有时 错误是 十分隐 蔽的 ,尤 其是涉 及 指针运算时。例如有如下操作序列:   aptr=a+5; ⁝ … *(aptr+v)… 当 v的值大于 4时,就会 出错。 但是有 时程序员 意识不 到 会 出现这 种错误 ,这 种 错 误也是 最 隐蔽 、最 难于查 找的,希望 读者注 意。 C系统根 本不检 查数组 超界,比如 目 前 变 量 v的值 为 20,则 C系统 会针 对 “* (aptr+ · 170· 第七章 指  针 v)”,去 访问下标 变量 a[25],尽管 前边 声 明 a只 有 10个 元 素。 这种 访问 会得 到 什么 结果, 当然不得而知。 4.指针 变量的 运算 指针的应 用给程 序员带 来极大 的灵活 和方便 ,同 时也带 来极大 的错误 隐患 ,尤 其是使 用 指针 参与运算 时。“++”和“--”运算又 是指针 运算中 最常用 和最容 易出错的 运算,下面 解 释几 个有关 “++”和“--”运算的 应用模 式。 设有数 组 a[10]、指针 变量 ptr和简 单变量 v,首先令 v的值 为 5;指针 ptr指 向 a[5],并 且 a[5]的 值为 50,a[4]的值 为 40,a[6]的值为 60。即 首先有说 明和运 算     inta[10],* ptr,v,* q,u; v=5; ptr=&(a[v]); a[4]=40; a[5]=50; a[6]=60; (1)ptr++。 运算“ptr++”的 意义是 把 ptr的 值加 1,运 算结 束后 ptr指向 a[6]。但是 该表达 式的 值 是 ptr加 1之 前时 ptr的值,即 a[5]的地 址。若 有   q=ptr++; 则结 果 q指 向 a[5],ptr指向 a[6]。 (2)* (ptr++)。 按(1)中的解 释,ptr++的值是 a[5]的地址 ,*(ptr++)是求 a[5]的值,为 50。该运 算 相当 于“a[v++]”。 若有   u=*(ptr++); 则结 果 u的 值为 50。 (3)(* ptr)++。 *ptr是求 ptr所 指变量 的值,即 a[5]的值 ,为 50;“(*ptr)++”是 a[5]++,最 后 a[5] 的值 为 51,而该表 达式的 值为加 1之前的 值仍然 是 50。 若有     u=(* ptr)++; 则结 果 u的 值为 50,a[5]的值 为 51。 (4)* ptr++。 由于运 算符“*”和 “++”优 先级相 同,结合方 向 是从 右向 左,因 此 该 表达 式 相 当 于(2) 中的 “*(ptr++)”,它的运 算与(2)完全相 同。 (5) ++ptr。 与(1)类似,运算“++ptr”的意义 是把 ptr的值加 1,运算结 束后 ptr指向 a[6]。该表 达 7.3 指 针 与 数 组 · 171· 式的 值是 ptr加 1之 后 ptr的 值,即 a[6]的地址 。若有     q= ++ptr; 则结 果 q指 向 a[6],ptr指向 a[6]。 (6)* (++ptr)。 按(5)的叙述 ,“++ptr”的 值是 a[6]的地 址,则“*(++ptr)”是 求 a[6]的值,得 60。该 运算 相当与 “a[++v]”。若 有   u=*(++ptr); 则结 果 u的 值为 60。 (7)* ++ptr。 与(4)类似,由于运 算符“* ”和“++”优 先级相 同,结合方 向是从 右向左,因 此该表 达式 相当 于“*(++ptr)”。 有关它 的运算 与(6)完 全一致 ,最 后 u的 值为 60。 7.3.2 多维 数组与指针 下面以 int类型二 维数组 为例,说明 多 维 数 组与 指针 之 间的 关系 ,以 及 怎 样 用指 针表 示 多维 数组及 其元素 。其他 类型和 多于二 维的情况 ,读 者可以 从这里 的叙述 中扩展推 得。 1.二维 数组元 素的地址 第六章 已经介 绍过,二维数 组   inta[m][n]; 可以看做是由 m个一维数组   a[0]、a[1]、… 、a[m-2]、a[m-1] 构成 。这 m个 一维数 组都有 n个元 素,即每个 a[i]都是 由 n个 int类型的 变量   a[i][0]、a[i][1]、… 、a[i][n-1] 组成 的 int类型 的数组 。 按前述 关于数 组与指 针的说 法,如 图 7.16所 示。从 一维角度 看,a表示 一维数 组的首 地 址,该一 维数组 的数组 元素仍然 是数组 ;进一步,每个 a[i]也都 表示一 维数组的 首地址 ,该 一 维数 组是 a数 组的第 i行 ,它 的各个 元素是 int类型。 图 7.16 二维数组 a的地址示意 · 172· 第七章 指  针 实际上 ,不 存在 a、a[0]、a[1]、… 、a[m -1]的 存储 空间,C系统 不给 它们分 配内 存,只 分配 m ×n个 int类 型变量的 内存空 间。图 7.16是 一 个 示 意图,给 出的 a、a[0]、a[1]、… 、 a[m -1]只是 一个示 意,读 者不要 误会 。事 实 上,a、a[0]、a[1]、a[2]、… 、a[m -1]都是 指 针常量。 进一步 解释 a[i]。 a[i]是 a数组第 i个元 素。如 果 a是一 维数组 ,则 a[i]代 表数组 a的 第 i个元 素,它是一 个变量并 且可以 有值,C系统 会给它 分配相 应的存 储空间,它实 实在在 地 占用 计算机内 存;如果 a是二维 数组,则 a[i]代表一 维数组,它是 一 个 指针常 量,C系统不 给 它分 配存储 区,它不占 用内存空 间,仅仅 是一个地 址。 读者已经 知道数 组名实 际是一 个指针 ,可 以用指 针形式 访问 数 组元 素。针 对二 维 数组, 用指 针方式 访问数 组元素 经常有 如表 7.1所示的 几种形 式。 表 7.1 指针访问数组元素形式 形 式 意 义 a 二维数组名,指向一维数组 a[0],即第 0行首地址。相当于:&(a[0]) 也可以说是数组 a的首地址,指向 a[0][0]。相当于:&(a[0][0]) a+i 第 i行首地址,即 a[i]地址。相当于:&(a[i]) *a 第 0行第 0列元素首地址,即 a[0][0]地址。相当于:a[0]、*(a+0)、&(a[0][0]) * a+i 第 0行第 i列元素首地址,即 a[0][i]地址。相当于:a[0]+i、&(a[0][i]) *(a+i) 第 i行第 0列元素地址,即 a[i][0]地址。相当于:a[i]、&(a[i][0]) *(a+i)+j 第 i行第 j列元素地址,即 a[i][j]地址。相当于:a[i]+j、&(a[i][j]) *(*(a+i)+j) 第 i行第 j列元素值,即 a[i][j]值。相当于:*(a[i]+j)、a[i][j] 读者要 仔 细 理 解 表 7.1中 各 种 表 示 形 式 的 意 义。 在 该 表 的 各 种 表 示 形 式 中,只 有 &(a[i][j])、a[i]+j、*(a+i)+j是实 际计算 机内存 的物理地 址;a[i][j]、* (a[i]+j)、 *(* (a+i)+j)是它们 各自的 值,占用 计算机内 存;其他 形式都 是表示 地址的 指 针常 量,没 有被 分配具 体内存 空间。 例如,并不存 在 a[i]这样一 个实 际的 变 量,它只 是一 个指针 常量, 运算 “&(a[i])”只 是一 种 地 址 计 算 方 式 而 已,并 不 是 求 内 存实 际 存在 的 变量 a[i]的 内 存 地址。 2.指向 二维数 组元素的 指针变 量 现在可以解释如何用指针变量表示二维数组的元素了。 (1)使 用成分 类型指 针。 数组 a的 成分类 型是 int类 型,可 以 直 接 使用 int类型指 针变 量访 问数 组 a的 成分。 设 有声明     int* aptr,a[m][n],x; 则可 以直接 用 aptr访问 a的成分 。使用 方法是首 先使 aptr指向 a的某 个成分 a[i][j],然 后 7.3 指 针 与 数 组 · 173· 以该 成分的地 址为基 点,计算所 要访问 的数组 成分的 相对地 址,并进 行访问。 最 常 用的地 址 基点 是 a数组 的第一 个成分 a[0][0],例如     aptr=* (a[0][0]) 由于 a[0]指向 a[0][0],所 以这个 运算还 可以写 成   aptr=a[0] 在上述赋 值运 算 的 前 提 下 ,a的 成 分 a[u][v]的 地 址 为 “aptr+u* n+v”。 若 想 把 a[u][v]的值送 入变量 x中,可以使 用赋值 运算:     x=* (aptr+u* n+v) 这个运算等价于   x=a[u][v] 若想 把某表 达式 e的 值送入 数组 a的 成分 a[u][v]中,可以 使用赋 值运算 :   (aptr+u*n+v)=e 这个运算等价于   a[u][v]=e 有必要解释一下地址计算公式     aptr+u* n+v 图 7.16所示 的示 意 图 已 经 暗示 了 C数 组 的 存 储分 配方式 。在 C语 言 中,按 “行 优先 ”原 则 分 配 数 组元 素的存储 空间。 也就 是说,对 数组 a来说,它 的 各个 成分被分 配的内 存空间 的顺序 是:首先分 配第 0 行元 素;然后分 配第 1行元素;…;最后 再分配 第 n- 1行元素 ,如此 等等。 每行元 素的分 配当然是 按下 标 值从 小到大 进行。 设 m=3,n=4,并从 首 地址 A000 开始 分配内 存空间 ,则 数 组 a的 存 储 分 配 如 图 7.17 图 7.17 数组 a[3][4]存储分配示意 所示 。具体 来讲,计算 公式“aptr+u* n+v”是 :   基 点 +行 数*每 行元素 个数 +剩余行的 零头元 素个数 例如 ,a[2][1]的地 址是:基点 aptr;加上 整行数 (行 标为 2,前 面有第 0行和第 1行,所以 整行 数为 2);乘以每 行元素 个数(每 行元素 个数就是 a的声 明中的列 标 4);加 上最后 一行剩 余行 的零 头元素 个数(下标 为 1,前 面有第 0个元素 ,所 以零头 数为 1)。得到 a[2][1]的位 置是:     aptr+2* 4+1 再考 虑每个 元素占 用的内 存尺寸 ,所以 2*4+1还要 乘以 int类 型一个 变量占 用 的 内存空 间 数 2,最 后 a[2][1]对应的具 体内存 地址是 :   A000 + (2*4+1)*2 为 A012。 · 174· 第七章 指  针 当然,在用户程 序中只要写 地址计算公式“aptr+u*n+v”即 可,没 有必要也不 允许考 虑每 个元 素占用的存 储空间尺 寸。更不可 能写出具体 的地址 计算算式“A000 + (2*4+1)*2”。 例 7.7 编写 函数,求 m×n个 元素的 给定 float型数组各 个元素 之乘积 。   floa arrmul(intm,intn,float*arr){ intu,v; floatmul; mul=0; for(u=0 u<m;u++) for v=0;v<n;v++){ mul=mul* (*(arr+u*n+v)); } returnmul; } 设有声明   floata[10][15]; 则可以用如下任何一种形式调用该函数:   arrmul(10,15,&(a[0][0])) arrmul(10,15,a[0]) arrmul(10,15,* a) 读者 可以从 中体会 数组和 指针作 函数参 数时,参数传 递的信 息及其 作用。 在本例 中,形式参 数是一个 指向 float类 型 的 指针 变量,实 在 参 数把 数 组 a第 一 个 元 素 a[0][0]的指针 (地址)送 入形式 参数 arr之 中。&(a[0][0])、a[0]、*a都 是 a[0][0]的地 址。 实际上 本例是 把二维 数组作 为一维 数组对待 的,地址计 算是采 用一维 方式进行 的。 (2)使 用行指 针。 还可以 声明指 向二维 数组行 标的指 针变量 ptr。也 就是让 指针变 量 ptr指 向 二 维数组 的 某行 a[i],而 a[i]是 一个一维 数组,本身 也是一 个指针 (指针常量 )。这种 情况 下,指针变 量 ptr以数组 a的行为 单位进行 操作。 指向二 维数组 行标的 指针变 量(指向一 维数组 的指针 变量)的说 明形式 是   T(*p)[n] 其中 ① T是类型 ; ② p是标识 符,是被声 明的指 向二维 数组行标 (指向一维 数组)的 指针变 量; ③ n是被指 向的数 组的尺寸 。 该声明的意义应该如下解释: “*p”是 n个 元素的 数组,该 数 组 的 元素 是 T类型;“p”是 指 向 该数 组的 指针。 这个 声 7.3 指 针 与 数 组 · 175· 明形 式声明 了一个 指针变 量 p,p指向 n个元 素的 数 组,数组 成分 是 T类型 的 。在这 种形 式 中,元素 个数 n可以省 略,省略 n后,该 声明可 以写成 “T(*p)[]”,表 示 p是“指向 T类 型 数组 ”的指针变 量。 请读者 注意说 明形式 “T(* p)[n]”,它仅仅 说明一 个指 针 变量,而不 是一 个一 维 数组; 另外 ,p是变 量,而 不是常 量。这 里给出 的数 组形式 仅 仅 表 示 “p指 向 数 组,p的 值是 一个 一 维数 组的首 地址”,而不 表示“在 这里就 实际 说明一 个数 组,存在一 个数 组的 内存 空 间,p是 指向 这个数 组的指 针常量 ”。 例如有     int(* ptr)[m],a[m][n],x,(* ptr0)[m]; 则“* ptr”是 一个有 m个元素的 int类型 的数组 ,ptr是指 向该数组 的指针 变量。 若有   ptr=a[i] 则 ptr指向数 组 a的第 i行 元素组 成的一 维数组 ,即 a的 第 i行 第 一 个 元 素 a[i][0]。进 一 步,若有 运算   ptr0=ptr+1 则 ptr0指 向 数 组 a的 第 i+1行 元 素 组 成 的 一 维 数 组。 即 a的 第 i+1行 第 一 个 元 素 a[i+1][0],而不是 第 i行的下 一个元 素 a[i][1]。 也可以 使用指 向数组 行标的 指针变 量访问数 组 a的 成分 ,例 如使 用 ptr。 使用方 法是 首 先使 ptr指向 a的某行 a[i],即该行 的第一 个元素 a[i][0],然后 以该行 为基点 ,计 算所要 访 问的 数组成 分的相 对地址 ,并进 行访问 。最常 用的地 址基点 是数组 a的第一 行 a[0],例如   ptr=a[0] 由于 a指向 a[0],所以 这个运 算还可 以写成     ptr=* a 在上 述赋值 运算的 前提下 ,a的成分 a[u][v]的地 址为“* (ptr+u)+v”。若想 把 a[u][v] 的值 送入变 量 x中,可以 使用赋 值运算 :     x=* (* (aptr+u)+v) 这个运算等价于   x=a[u][v] 还等价于   x=(aptr+u)[v] 若想 把某表 达式 e的 值送入 数组 a的 成分 a[u][v]中,可以 使用赋 值运算 :     (* (aptr+u)+v)=e 这个运算等价于   a[u][v]=e 例 7.8 编写 函数,求给 定 float型 m ×n矩阵各 个元素之 积。 · 176· 第七章 指  针 该问题 在例 7.7中已经 解决。 现在用 指针变 量访问 数组元素 ,重新编 写该函数 。   floa arrmul(intm,intn,float(*a)[15]){ intu,v; floatmul; mul=0; for(u=0 u<m;u++) for v=0;v<n;v++){ mul=mul* (*(*(a+u)+v)); } returnmul; } /也可以使用 mul*a[u][v] //或使用 mul*(*(a[u]+v)) 设有声明   floatarr[10][15]; 则可以用如下任何一种形式调用该函数:   arrmul(10,15,arr) arrmul(10,15,&(arr[0])) 读者可以从中体会指向数组的指针作函数参数时参数传递的信息及其作用。 在本例 中,形式参 数是一个 指针变 量,该变量 指向 一 个 float类型 的一 维数组 ,实 在参 数 把 arr数组的第 一行 arr[0]的 指针(地 址)送入 形式参 数 a之中。 &(arr[0])、arr都是 arr[0] 的地 址。由 于地址 计算的 需要,形式参 数 a说明 中的数 组尺寸 15是必须 的。 7.3.3 指针 数组 一维指针数组的声明形式是     T* p[n] 其中 (1)T是类 型; (2)p是标识 符,是被声 明的数 组变量 ; (3)n是数组 尺寸。 该声明的意义应该解释如下: 由于运 算符“*”的 优先级 低于运 算符“[]”,所以 该声明相 当于声 明     T* (p[n]) 按此 格式,“p[n]”表 示 p是 n个元 素的数 组,数 组元素 是“T*”类 型,即指向 T类 型变量 的 指针 类型。 所以 p是 n个 元素的 指针 数 组,数组 成分 是指向 T类 型变 量的 指 针。也 可以 认 为 p本身 是一个 指 针常 量,指向 n个 元素的 指针数 组。 这个声 明形式 声 明了 一个 指针数 组 p,如 图 7.18所 示。 7.3 指 针 与 数 组 · 177· 图 7.18 指针数组 指针数 组没什 么特殊 的,是 由指 针构 成的数 组。数 组中 每个 元素 都 是 指 针。例 7.9的 程序 中定义 了指针 数组 n,n包含 5个元 素,其中每 个元素 都是整 型指针 。 例 7.9 指向 指针的 指针、指针 数组与数 组的关 系。   #include <stdio.h> void ain(){ inta[5]={23,24,25,26,27}; int* n[5],i; int** p=n; for(i=0 i<5;i++) n[i]=&(a[i]); for(i=0 i<5;i++,p++) printf("%4d",** p); } 程序运行结果将输出: 图 7.19 指向指针的指针变量 p、指针 数组 n与数组 a的关系   2324252627 图 7.19展示 了“指向 指针的 指针变 量 p”、“指针 数组 n”与“整型 数组 a”之间的 关系。 例 7.10  二维数 组、指针数组 和指针 间的关 系。   #include <stdio.h> inta[4][4]={{1,2,3,4},{11,12,13,14},{21,22,23,24},{31,32,33,34}}; int* pa[4]={a[0],a[1],a[2],a[3]}; int* p=a[0]; inti; void ain(){ for(i 0;i<3;i++) printf("%d% d% d\n",a[i][2+i],*(a[i]),* (* (a+i)+i)); for(i 0;i<4;i++) printf("%d% d\n",* (pa[i]),p[i])); } 程序运行结果将输出: · 178· 第七章 指  针   3        14 11 12 31 21 23 1 1 11 2 21 3 31 4 图 7.20展示 了 int类 型二维 数组 a、指针 数组 pa和 指针 p之间 的关系。 用指针 数 组 处 理 多 个 字 符 串 十 分 方 便。   图 7.20 二维数组 a、指针数组 pa和指针 p之 请读 者认真 体 会 例 7.11程 序 中 的 指 针 数 组 间的关系 作参 数、字符串 指 针操 作,以 及 它 们 给 程序 设 计带来的方便。 例 7.11  把若干 给定的 字符串 按字母顺 序排序 并输出 。   #include <stdio.h> voidsort_string(char* arr_str[],intn); voidout_string(char* arr_str[],intn); intstr_cmp(int* str1,int* str2); char* name[]={"basic","programming","greatwall","language","computer"}; main ){ sort_string(name,5); out_string(name,5); } void ort_string(char*arr_str[],intn){ char* temp; inti,j,k; for i=0;i<n-1;i++){ k=i; for( =i+1;j<n;j++) if(str cmp(arr_str[k],arr_str[j]) >0) k=j; temp= arr_str[i]; arr_str[i]= arr_str[k]; arr_str[k]=temp; } } void ut_string(char*arr_str[],intn){ intj; for( =0;j<n;j++) 7.3 指 针 与 数 组 · 179· printf("% s\n",arr_str[j]); } /* 下面是比较两个字符串大小的函数,本程序没有使用系统标准函数 */ int tr_cmp(char*str1,char* str2){ do{ if(*st1>* str2) /比 较 当 前 字 符 return1; elseif *str1 < *str2) return -1; }while(*(str1++)!= \0&&*(str2++)!= \0) //注意此处“++”运算的作用 if(*sr1== \0&& *str2== \0) //两 个 串 全 部 结束 ,长 度相 同 return0; else f(*sr1!= \0) //str1长 return1; elsereturn -1; //str2长 } 该程序 的声明 部分产 生如图 7.21所示的数 据结构 。 图 7.21 例 7.11中程序声明部分产生的数据结构 经过 sort_string处理后 的结果 如图 7.22所 示。 图 7.22 经过 sort_string处理后的结果 最后,运行 结束产 生如下输 出:   basic computer greatwall language programming · 180· 第七章 指  针 7.3.4 指针 与数组总结 注意有 关数组 声明的 几种形 式,以 及它们 的区别 。     T  a[n] T  * p[n] T  (* p)[n] T  * (p[n]) 其中 T是 类型符 ,以 int类型为例 ,并设 n=5,举 例来说 明这几种 形式的 意义。 声明“inta[5]”表示 a是 5个 元素的 int类型数 组,如图 7.23所 示。 图 7.23 数组 a 声明“int*p[5]”,由于 运算符 “*”的 优 先 级 低于 运 算 符 “[]”,所 以该 声明相 当于 声 明“int*(p[5])”。 按此格式 ,“p[5]”表示 p是 5个元 素的数 组,数 组元素 是“int* ”类型, 即指 向 int类型 变量的 指针类 型。所 以 p是 5个 元素 的指针 数组,数 组成 分是指 向 int类 型 变量 的指针 。也可 以认为 p本身 是一个 指针 常量,指 向 5个 元 素 的 指针 数组 。 这个 声明 声 明了 一个指 针数组 p,如图 7.24所示。 图 7.24 指针数组 下标变 量 p[2]是数 组 p的 编号为 2(第 三 个 )的 成 分 ,它 是一 个 指 针 ,可 以指 向一 个 int 类型变量。 声明“int(*p)[5]”表 示(*p)是 5个 元素的 int类 型 数组,则 p是“指 向 5个 元 素的 int 类型 数组”的指 针变量 。这里 只声明 一个指 针类型 变量 p,p可以 指向 5个元素 的 int类型 数 组,但是 目前 还 没有 该数组 。这 个 声明 不产生 5个元 素 的 int类型 数 组,只声 明一 个指向 数 组的 指针变量 p。 事实上 ,元 素个数 5可以省 略,该声 明可以写 成“int(* p)[]”,表示 p是 “指向 int类型数 组”的指 针变量 ,如 图 7.25所示 。 声明“int*(p[5])”与 “int* p[5]”等 价 。p是 5个 元 素的 指针 数 组,p本身是 一个 指 针常量。 7.4  指 针与字 符 串 · 181· 图 7.25 指向数组的指针 7.4  指针 与字 符串 字符串 实质上 是常量 字符数 组,同时 还可 以 使用 字符 数 组变 量保 存 字符 串。指 针与 数 组有 着密切 的关系 ,数 组名是指 针,指针 也 可 以 指向 数组 。显 然 ,指针与 字符 串 也有 着密 切 的关 系。在 C语 言中,除了 用字符 数组保 存并标识 字符串 外,还可以 使用字符 类 型 指针指 向 一个 字符串,从 而使用 指针访问 它所指 的字符 串。该 字符串 既可能 是字符 数组 ,也 可能是 字 符串常量。例如     char* sp="Ilovechina",* sv; charstring[]="Iam astudent"; 定义 两个字 符型指 针变量 sp、sv以及 一个字 符 数组 变量 string;并对 变 量 进 行初 始 化,使 sp 指向 字符串 “Ilovechina”,而 string初 始化为 “Iam astudent”。 设 变量 从 A000开始 分配 内 存空 间,常量从 AA00开始分 配内存 空间,这个 声明产 生如图 7.26所示 的内存分 配结果 。 编译系 统为字 符类型 指针变 量 sp、sv分配 指针 类型空 间;为字符 串数组 string分配 字符 型空 间,并初始 化,把字符 串“Iam astudent”保存 在相应 数组 string的 内存空间 中。 为了保存 字符串 常量“Ilovechina”,编译系 统在常 量区 给它开 辟存储 空 间 ,这 块存储 空 间的 结构与 字符数 组相同 ;并初 始化字 符类型 指针变 量 sp,使 它指向 字符串 “Ilovechina”。 字符串“Ilovechina”是 常量字 符串,只有通 过指向 它的 指针变 量才能 访 问 它,它本身 没 有名 字,sp绝 对不是 它的名 字。目 前 sp指向它,当然 也可以 用其他 指针变 量指向它 ,sp也 可 以用 于其他 用途。 例如,下述赋 值运算   sv=sp; sp=string; 使得 sv指 向字符串 “Ilovechina”,而 sp又去 指向另 一个字符 串,如图 7.27所示。 使用字 符 数组 sp可 以访问相 应字符 串,也可以 参与一 切与字 符串有 关的运 算。例 如     printf("% s\n",sp); 将打印出     Iam astudent 而   sp[3] *(sp+3) · 182· 第七章 指  针 的值 都是字 符“m”。 下面分析 字符串 指针与 字符数 组之间 的关系 。 图 7.26 字符串与指针 图 7.27 赋值运算后的字符串与指针 1.从字 符串角 度看 (1)字 符串指 针是一 个指针 类型变 量,被 分配指 针类型 存储空 间,它虽然 可 以 指向一 个 字符 串,但是字 符串指 针变量的 存储空 间不能 保存 一 个字 符 串 。比 如 sp地 址是 A000,占 用 指针 类型空 间 A000~A003;sv地 址是 A004,占用 指针类 型 空 间 A004~A007。 开始 sp指 向 字符 串“Ilovechina”,sv无值 。 (2)字 符数组 是一个 数组类 型变量 ,被分 配数组 类型存 储空间 ,该 空间可 以 保 存一个 字 符串 。比如 string地 址 是 A008,占 用 空 间 A008~A016。 数 组 string保 存 字 符 串 “Iam a student”。 2.从指 针角度 看 (1)字 符串指 针名字 是一个 指 针变 量,它可 以 指 向 任何 字 符 串。 可 以把 任 意字 符串 赋 值给 字符串指 针,让字 符串指针 变量指 向相应 字符串 ,但 是决不 是字符 串指针 变 量 本身存 储 了整 个字符 串。例 如,sp是 字符 串指 针 变 量,变 量 声 明 初始 化 时它 指 向 常量 字 符 串 “Ilove china”,后来经 过赋值 运算,它又 指向字 符串“Iam astudent”。还 可以让 字符 串指针 变量 指 向任意一个字符串。例如下述运算都是正确的。 7.4  指 针与字 符 串 · 183·   sp=string;       /sp指向字符数组 string sp="Iamateacher"; //sp重新指向一个新的字符串常量 (2)字 符数组 名字是 一个指 针常量 ,它只 能指向 分配给 它的那 块内存 空间 ,不 能指向 其 他字 符串或 数组。 也不能 用一个 赋 值运 算 把 整 个字 符 串赋 值 给 一 个 字符 数 组 。例 如 string 地址 是 A008,占 用 空 间 A008~A016。 初始 化时,数 组 string保 存 字符 串 “Iam astudent”。 如果做运算   string=sp 或   string="Iam ateacher" 都是 错误的 。当然 ,数 组的内容 是可以 改变的 。 3.从使 用角度 看 (1)不论 字符串 指针还是字 符数组 ,访问 整个字 符串时 要十分 小心,有些操 作是 正确的 , 有些 操作是错误 的。举例 如下,请 读者从 中深刻体会字 符串指针和字符数 组的各种用 法。   ① charstr[20]="Iam ateacher"; /正确,初始化数组 str ② charstr0[20],str[20]="Iam ateacher";   str0=str; //错 误 ,给指 针 常 量 赋 值,数 组 不能 整 体 赋 值 ③ charstr[20];   str="Iam ateacher"; //错 误 ,给指 针 常 量 赋 值,数 组 不能 整 体 赋 值 ④ charstr[20];   str[]="Iam ateacher"; //错 误 ,给指 针 常 量 赋 值 //“str[]”出 现 在“=”左 端,意 义不 明 确 ⑤ char*str;   str="Iam ateacher"; //正 确 ,给指 针 变 量 赋 值,str指 向 常 量 字 符 串 ⑥ charstr[20];   scanf("%s",str); //正 确 ,输入 字 符 串 ⑦ char*str;   scanf("%s",str); //错 误 ,str不 指 向任 何 变 量 ,输 入 字 符 串 无 处存 放 ⑧ char*str,str0[20];   str=str0;   scanf("%s",str); //正确,输入字符串数据从 str0[0]开始存放 ⑨ char*str,str0[20];   str=str0+5;   scanf("%s",str); //正确,输入数据将从 str0[5]开始存放 瑏瑠 charstr[20]= "Iamateacher";   printf("%s",str); //正确,打印:Iam ateacher 瑏瑡 char*str="Iam ateacher";   printf("%s",str); //正确,打印:Iam ateacher 瑏瑢 char*str="Iam ateacher";   str=str+5; · 184· 第七章 指  针   printf("%s",str); //正 确 ,打印 :ateacher 瑏瑣 char*str= "x=%d  y=%f\n";   printf(str,x,y); //正 确 ,相当 于 :printf("x=% d  y=% f\n",x,y); (2)不 论字符 串指针 还是字 符数组 ,都可 以用相 同的方 法访问 其分量 。例如,设 有说明     char* sp="Ilovechina",sv[20],* p,* q; intv; 如下三 个语句 列完成 同样功 能,都 是把 sp所指 字符串 复制到 字符数 组 sv之 中。    ① =-1; do{  v++; sv[v]=sp[v]; }while(sp[v]!=0);    ② =-1; do{ v++; *(sv+v)=* (sp+v); }while(*(sp+v)!=0);    ③ =sp-1;q=sv-1; do{ p++;q++; *q=* p; }while(*p!=0); 第一个语句列完全使用数组和下标表达式形式。 第二个语句列使 用指针和一 个整型变 量 v,每 次循环 v的值增 加,指针变量 始终指向字符 串和 字符数组的 首位。在 赋值时,先把指 针值加一个整 数,然后用间接 寻址达到 赋值目的。 第三个 语句列 使用两 个指针 变量 p、q,开始 p、q分别 指向源 字符串 和目标 字符数 组首 位,在循 环过程 中 p、q不 断移动 ,也 使用间 接寻址 达到赋 值目的。 表 7.2列 出了字 符数组 与字符 指针变 量的比 较。 表 7.2 字符数组与字符指针变量的对比 字符数组 字符指针变量 存储空间 分配数组空间 分配指针空间 初始化 可以 用 字 符 串 初 始化 。 例 如 :  chara[]="string" 数组 a有 7个元素,把 string\0这 7个字符顺 次分别放入数组 a各个元素中 可以用字符串初始化。例如:    char* p="string" 这时 "string"是 常量,p指 向 该 常量 首 地 址 7.5  指向 指 针 的 指 针 · 185· (续 表 ) 赋值 字符数组 字符指针变量 可以 对 数 组 元 素 单个 赋 值 ,例如 :   a[2]= y 不允许对数组名赋值,因为 数组名 是指针 常 量,下 述运 算 是 错 误 的    a=p 可以让指针变 量 指 向 任 意 字 符串。 设 有说 明 “chara[10]”,如 下赋 值运 算 都 是正 确 的 : p="Hello"; p=a; 地址值 可变性 数组 名 是 指 针 常 量,总 是 指向数 组 首 地 址 数组名是指 针 常 量,值 不可 变,常用 如 下 形 式访问分量: a[v]      下 标 变 量 *(a+v) 基址 +位移 是指 针 变 量 ,可 以 指 向 任 意 字符 串 指针 变 量 值 可 变 ,可 以 通 过 各种 运 算 改变 值 例如: p=…      赋值 p=p+i 与整数运算 联系 指针变量可以指向数组。 函 数 参数 数组名作实在参数,其对应 形式参 数必须 是 指针或数组名。 参 数结 合 时 传 递 实 在 参 数 数组首地址。不 能 把整 个 数 组 全 部 传 入 函 数 指针 变量作实在 参数,把指针变量 值送入形式 参数变量中。其 对应 形 式 参 数 可 以 是 指针 或 数组名 7.5 指 向指 针的 指针 如果一个 指针变 量指向 的变量 仍然是 一个指 针变量 ,就 构成指 向指针 变量 的指 针 变量, 简称 指向指针 的指针 。在图 7.28中,变量 p指向 一个变 量,该变量 仍然是 指针 类型 的 ,它 指 向一 个 int类型 变量。 图 7.28 指向指针的指针 P的 声明形 式是:   类型符 **p 如下程 序片段 构造图 7.28所示的 结构。     int** p,* s,v; p=&s; s=&v; v=300; · 186· 第七章 指  针 其中 声明符 “**p”声明了 指向指 针变量 的指针 变量 p。使用 s访问 v内容的 形式是   *s 这是 读者已 经熟悉 的,使用 p访 问 v内 容的形 式是     ** p 上述间接寻址运算的意义是: (1)p是指 针变量 ,它 的值是 指向“指 向 int类型 的指针 变量”; (2)* p取 上述 p的内容 ,得 到一个 指针值 ,该 指针值 “指向 int类 型变量 ”; (3)**p即“*(* p)”,再取上 述“*p”的内 容,得到一 个 int类 型的值。 对“int**p”的解释 是:由于“* ”是从右向 左结合 的,所以该 声明相 当于     int* (* p) 该形 式表示 “*p”是“int*”类型 ,即指 向 int类型 变量的 指针类 型,而 p是 指向它 外层类 型 (即“int*”类型 )变量的 指针类 型变量 。 上述给 v赋值的 语句   v=300; 可以 使用指 针变量 s实现,也可 以使用 指向指针 的指针 变量 p实现,下述 三个语句 等价。     v =300; *s =300; ** p=300; 指向指 针的指 针在实 际程序 中有 很 大用 处,程序 的命令 行参 数就使 用了 指 向指 针的 指 针。经常使用指针数组实现指向指针的指针。设有程序片段   charc1[]="copy"; charc2[]="jilin.dat",c3[]="changchun.dat",c4[]="beijing"; char* aptr[4],** ptr1,** ptr2; ptr1=aptr; *ptr1=c1; *(ptr1+1)=c2; aptr[2]=c3; ptr1[3]=c4; ptr2=&(aptr[0]); 图 7.29描述 了上述 程序片 段产生 的结果。 (1)数 组 aptr是一个 指针数 组,它的每 个元素 值是 一个指 针,指向 一个 char型变 量,数 组名 aptr是 一个指 向指针 的指针 (指针常 量); (2)变 量 ptr1、ptr2也是 指向指 针的指 针(指针 变量)。 如下操 作都是 访问 c3的第 8个元 素,得到字 符值“u”。 7.6 命 令 行 参 数 · 187· 图 7.29 指向指针的指针     c3[7]、* (c3+7) aptr[2][7]、ptr1[2][7]、ptr2[2][7] *(aptr[2]+7)、* (ptr1[2]+7)、* (ptr2[2]+7) (* (aptr+2))[7]、(* (ptr1+2))[7]、(* (ptr2+2))[7] *(* (aptr+2)+7)、* (*(ptr1+2)+7)、* (* (ptr2+2)+7) 7.6  命令 行参 数 到目前为 止,编写 的一切程 序都没 有涉及 与操作 系统之 间的 联 系。考 虑问 题:编写程 序 实现 操作系 统的 copy命令。 命令格 式是:   copy 文件名 1 文件名 2 copy命令把 一个 文 件 的 内 容 拷 贝 到 另 一 个 文 件 中,当 然 要 针 对 一 切 可 能 的 文 件 名。 例如   copyjilin.datchangchun.dat 把文 件 jilin.dat的内容 复制到 文件 changchun.dat中 ,问题是如 何把两 个文件 名传入 程序。 使用命 令行参 数可以 解决该 问题。 C语 言程序 从 主 函数 main开 始执 行,当操作 系统 启 动 C语言 程序执 行时,一定 把三个 信息传 入程序。 这三个 信息是 : (1)命 令行中 字符串 的个数 ; (2)命 令行中 每个字 符串的 内容; (3)表 示程序 运行环 境的各 个字符 串的内容 。 第一个信 息是一 个整数 ;第 二个信 息由一 个个字 符串组 成,用一个 指针数 组 保 存这些 字 符串 的首指 针,操作系 统传递给 程 序 的 是 相 应指 针 数 组 的首 地 址 ;第 三 个 信息 不 常用 ,这 里 不涉及它。 为了与 操作系 统联系 ,传递 上述信 息,一般 main函 数有 两个形 式参数 ,第 一 个 对应操 作 系统 传递过 来的“命令 行中字 符 串 的 个 数”,经 常 用 argc表 示;第二 个对 应操 作 系统 传递 过 来的 “指针数组 的首地 址”,经常用 argv表示。 带参数 的 main函 数的常 用形式为 : · 188· 第七章 指  针   intmain(intargc,char* argv[]) 其中 的 int型参 数 argc标 识数组 argv的 元 素 个数 ,字符 指 针 数 组 argv的 各个 元 素分 别指 向 命令 行中可 执行文 件名和 各个参 数的字 符串,argv[0]总是指 向可执 行文件 名。上 述命令 行     copy  jilin.dat  changchun.dat 产生 如图 7.30所 示的信 息。 图 7.30 命令行参数信息 显然,argv是 指 向 指 针 数 组 的 指 针 变 量;argv[1]是 字 符 型 指 针 变 量 ,指 向 字 符 串 “jilin.dat”;而 argv[1][4]是 字符类 型变量,值 是字符 “n”。 带参数 的主程 序非常 有用,几乎所 有实用 程序 都涉 及程序 参数。 C语 言 程序 在 DOS操 作系统下执行命令的形式是:   可执行文件名 参数 1参数 2…… 参数 n 例 7.12  编写程 序,输出命令 行的参 数内容 。   #include <stdio.h> void ain(intargc,char* argv[]){ printf("argc=% d\n",argc); printf("commandname:% s\n",argv[0]); for(in i=1;i<argc;i++) printf("Argument% d:% s\n",i,argv[i]); } 如果 执行该 程序,在 DOS下键 入命令 行:   C> ttse.txthopeeeefe <CR> 程序运行结果为:   argc=5 commardname:tt Argument1:se.txt Argument2:hope Argument3:ee Argument4:efe 习 题 七 · 189· 本章小结 本章主 要介绍 指针的 概念与 操作,并 对指 针 与数 组的 关 系进 行了 详 细介 绍。重 点掌 握 指针 变量与 指针所 指变量 之间以 及指针 与数 组之间 的关 系。只 有 掌 握 好 这两 种 关系 ,才 能 够正 确理解 指针概 念及其 操作,从而正 确使用 指针。 习 题 七 7.1  什 么是 指 针 ?说 明 指针 变 量 与 指 针 所 指变 量 的 区 别 ? 7.2  执 行下 述 语 句 列后 ,m的 值是 什 么 ?    ntm=1; int* n=&m; m =* n+m; 7.3  给 出所 有 可 施 于指 针上 的 运 算 ,并 说 明 它 们 的意 义 。 7.4  下 列关 于 指 针 运算 的描 述 ,哪几 个 是 错 误 的 ? (1)比 较 两 个 指 针 的 大小; (2)把 空 指 针 值 送 入 另一个 指 针 变 量; (3)一 个 指 针 加 上 两 个整数 之 差 ; (4)两 个 指 针 相 加 。 7.5  设 有如 下 声 明    ntival=1024,*iptr; float* fptr; 判 断下 列 运 算 的 合 法 性,并 说明 理 由 。      val=*iptr; ival=iptr; * iptr=ival; iptr=ival;    7.6  举 例说 明 下 述 几个 声明 的 意 义 。    iptr=&ival; iptr=&ival; fptr=iptr; fptr=* iptr;      nt* p; int** p; inta[]; inta[5]; int* p[];    nt*(p[]); int(* p)[]; int* p[5]; int* (p[5]); int(* p)[5] 7.7  说 明下 列 声 明 的意 义。 · 190· 第七章 指  针    hars[6]="pascal"; chars[]="pascal"; char* s="pascal"; chars[]={ p,a,s,c,a,l,0}; char* s[2]={"pascal","fortran"}; 7.8  设 有声 明    inta[10][20],i=2; 下 述形 式 各 表 示 什 么 意义,它们 之 间 有什 么 关系 ,各访 问 的 是 哪 个 变 量。    +i、a[i]、*(a+i)、&a[i]、&a[i][0] a[3][2]、*(* (a+2)+1)、* (* (a+4))、(a[3]+2) 7.9  设 有声 明    inta[10],i; 下述各种访问数组 a的元素的形式是否正确。    a[3+i]、*(a+i)、* (i+a)、* ((a++)+i) 7.10  下 述 程 序 执行 后 ,输出 结 果 是 什 么 ?    include<stdio.h> char* p[2][3]={"abc","defgh","ijkl","mnopqr","stuvw","xyz"}; main(){ printf("% c\n",*** (p+1)); printf("% c\n",** p[0]); printf("% c\n",(* (* (p+1)+1))[4]); printf("% c\n",* (p[1][2]+2)); printf("% s\n",** (p+1)); } 7.11  说 明 如 下 程序 片 段 的 输出结 果 。    hara[]="pascal",ptr; for(ptr=a;ptr<a+6;ptr++)printf("% s\n",ptr); 7.12  编 写 程 序 ,求 给 定 字符 串 的 长 度 。 7.13 编写比较两个字符串 s1、s2大小的程序。当 s1>s2时,输出 1;当 s1<s2时,输出 -1;当s1==s2 时,输出 0。 7.14  编 写 程 序 ,分 别 求 给定 字 符 串 中 大 写字 母 、小写字 母 、空 格 、数 字 和 其 他 符号的 数 目 。 7.15 编写程序,把给定字符串的从 m开始以后的字符复制到另一个指定的字符串中。 7.16 编写程序 insert(char*s1;char*s2;intv),在字符串 s1的第 v个字符处插入字符串 s2。 7.17 编写程序,用指针作参数,实现把字符串 str1复制到字符串 str2。 7.18 编写程序 str_delete(char*s;intv,w),从字符串 s的第 v个字符开始删除 w个字符。 7.19 编写程序,用指针作参数,实现把字符串 str反向。 7.20  编 写 程 序 ,分 别 利 用指 针 传 递 参 数 ,实 现 两 个字符 串 变 量 的交 换 。 7.21 编写程序,用指向指针的指针,实现对给定的 n个整数按递增顺序输出,要求不改变这 n个 数原 来的顺序。 习 题 七 · 191· 7.22 编写程序,用指向指针的指针实现输入 n个字 符串,然后对 它们进 行排序,最后 按序输 出它们。 要分别字符串等长和不等长两种情况。 7.23 编写程序,对给定的 n个整数进 行位置 调整。调整 方案是:后面 m个数 移到 最前 面,而 前面 的 n-m个 数 顺 序 向 后 串。 7.24  编 写 程 序 ,输 入 一 个字 符 串 ,如    123bc456d7890*12///234ghjj987 把字符串中连续数字合并,作为整数存入 int类型数组中,并输出。 7.25 编写程序,把 1、2、3、4、5、6、7、8、9组合成三个三位数,要求每个数 字仅用一次,使每个 3位 数为 完全平方数。 7.26 编写程序,把 1、2、3、4、5、6、7、8、9组合成三个三位 数 m1、m2、m3,要求每个数 字仅用一 次,使 得 m2=2*m1,并且 m3=3*m1。例如:m1=192;m2=384;m3=576。 7.27 编写程序,用指针形式访问数组元素,把给定的 int类型矩阵转置。 7.28 编写程序,把给定的 int类型的 5×5矩阵中最大 元素放在 中间;按序分 别把四 个最小 元素放 在 四个 角 上,顺 序是 :左 上 、右 上 、右 下 、左 下 。 7.29 编写程序。建立两个 4×4的 整型 数组,其 中的 元素 可以 由随 机函 数生 成,并将这 两个 数组 按 行、列 输 出 ;并 编 写 函 数 ,将 两 个 数组 作为 参 数 ,计 算矩 阵的 乘 积 并 输 出 结 果。 7.30 编写程序。建立一个学生姓名的序列。用字符数组来保存每个学生的姓名,要求在程序运 行时 能够 随 意增 加 或 删 除 学 生 姓名的 记 录 ,同 时要 求 此序 列 能 够 按 照 姓 名的字 母 顺 序 排列 打 印 出 来 。 7.31 已知有指针数组形式给出的字符串列表 table。编 写程序,输入 一个字 符串,查 table表,若查 到 输出该串位置,否则输出 0。 7.32 有一个成绩管理程序,管理 30个学生,20门课程。编写如下函数: (1)求 某 给 定 课 程 的 平均分 数 ; (2)针 对 某 给 定 课 程 ,打 印 本课 程 不 及格 的 所有 学 生 的 情 况 ,包 括 学 号、每 人 全部 课 程 成 绩、平 均 成 绩 ; (3)输出所有平均成绩在 90分以上的学生名单; (4)输出所有各科成绩均在 80分以上的学生名单。 第八章 再 论 函 数 本章将 讲述一 些与函 数有关 的 较深 入 的 内 容,包 括参 数 、作 用 域 、递 归等 。 最后 第十 三 章还将介绍有关函数的更深入的内容。 8.1 参    数 8.1.1 C参数传 递规则 第四章 中已经 介绍过 ,C语言 只有 值参 数一 种 参数 类别。 函数 调用 时把 实 在参 数的 值 传送到形式参数中。 从一般 程序设 计语言 参数规 则来看 ,值参 数意味 着: (1)函 数调用 中,与值参数 对应的 实 在 参 数是 一 个 表 达式 (单个 变量 ,常 量是表 达式 的 特例 ),并 且要求 实在参 数表达式 与形式 参数变 量之间 赋值兼 容。 (2)当 程序运 行时,调用函 数,进行 参数结合 的动作 是: ① 首先计算实在参数表达式的值; ② 把实在参数的值按赋值转换规则转换成形式参数的类型; ③ 把转换后的实在参数表达式值送入形式参数变量中。 (3)在 整个函 数活化 期间,值 参 数 表 示形 式 参 数 本身,它 是 函 数 内的 一个 局 部变 量,已 经与 实在参 数脱离 关系,与实在 参数无 关了。 在函数 内对形 式参数 的赋值 不影响实 在参数 。 (4)当 函数执 行结束 返回后 ,实在 参数值 无任何 变化,还是 调用函 数之前 的值。 例 8.1 本例 用以说 明 C函数内 对形式参 数的操 作不影 响实在 参数。    /* ROGRAM checkout*/ #include"stdio.h" intu,v; voidp(ntx,inty){ y=x+y; printf("% d% d\n",x,y); } void ain(void){ u=3; v=4; * 1*/ /* 2*/ /* 3*/ /* 4*/ /* 5*/ /* 6*/ /* 7*/ /* 8*/ /* 9*/ 8.1  参   数 · 193· p(u,v); printf("% d% d\n",u,v); p(6,u+v); printf("% d% d% d\n",u,v,u+v); } /* 10*/ /* 11*/ /* 12*/ /* 13*/ /* 14*/ 该程序 有全局 变量 u、v,同时函 数 p还 有形式 参数 x、y,分析该 程序的执 行过程 。 (1)开 始执行 第 8、9行的 赋值,得:     u:    v: 4 (2)执行 第 10行函 数调用 p(u,v);进 行参数 结合,计算实 在参数 u、v值,分 别送入 形式 参数 x、y,得   x: 3 y: 4 (3)进 入函数 执行第 4行赋 值 y=x+y;给 y赋值 与主程 序中 v没有任何 关系,得   y: 7 (4)执 行第 5行输 出函数 printf("%d%d\n",x,y);打印   3 7 (5)函 数执行 结束,返回主 程序第 10末尾。 (6)执 行第 11行 输出函数 printf("%d%d\n",u,v);这 时主程 序 中的 v值还 是调 用 函数 p之前 的值 4,虽 然函数 内给形 式参数 y赋值 7,但 是不影 响主程 序。本 语句打 印   3 4 (7)执行 第 12行函 数调用 p(6,u+v);进 行参数 结合,计算 实在参 数 6、u+v值,分 别送 入值 参数 x、y,得   x: 6 y: 7 (8)进 入函数 执行第 4行赋 值 y=x+y;给 y赋值 与主程 序中 u+v没有 任何关 系,得   y: 13 (9)执 行第 5行输 出函数 printf("%d%d\n",x,y);打印   6 13 (10)函数 执行结 束,返回主 程序第 12末 尾。 (11)执行 第 13行 输出函 数 printf("%d%d% d\n",u,v,u+v);这 时 主程 序中 u+v 的值 还是用 调用函 数 p之 前的 u、v值 3、4来 计算,得 7,虽 然函数内 给形式 参数 y赋 值 13,但 是不 影响主 程序中 实在参 数 u+v。 本语句打 印   3 4  7 · 194· 第八章 再 论 函 数 8.1.2 指针 作参数 从一般 概念上 讲,函数某个 “形式参 数”是指 针类 型。对应 的函 数调 用,其 相应 “实在 参 数”也 是一个指 针类型 表 达式。 在调 用函 数 时,把 实 在参 数指 针 值送 入 形 式参 数 指 针 变 量 中,没有 什么新 的特殊 概念。例 如,函数 f有一 个 int类 型的指 针参 数,它的 声 明和调 用形 式 可能是:   void (int*x){ ⁝ } void ain(void){ int* v;  ⁝ … f(v)…  ⁝ } 但是真正 应用指 针参数 ,其 作用是 相当大 的。由 于在函 数内部 ,指 针参数 变 量 可以指 向 它的 调用处 (外 层程序 )的其他 变量,它起 到了其 他程序 设计语 言中变 量参数的 作用。 如下例 8.2中 程 序 的 功 能 是 对 随 意 输 入 的 两 个 整 数,按 由 大 到 小 的 顺 序 输 出 。 函 数 swap的 功能是交 换两个 整数变 量的值 。 例 8.2 指针 作参数 的作用 。   #include <stdio.h> void wap(int*xx,int*yy){ inttemp; temp=* xx; * xx=* yy; * yy=temp; } void ain(){ intx,y; int* px,* py; scanf("% d% d",&x,&y); px=&x; py=&y; if(x y) swap(px,py); printf("\n%d\t% d\n",x,y); } * 位置 2*/ /* 位置 3*/ /* 位置 1*/ 当输入 8.1  参   数 · 195·    3   6 时,程序 运行结 果为:    6   3 下面分析该程序的运行过程。 (1)当 程序执 行 到 位 置 1时,系 统 在内 存 为 主 函 数 main()开 辟一段 存储空 间,并给变 量 px、py赋值,使它 们 分别 指向 x、y,如 图 8.1所示。   图 8.1 例 8.2中程 序执 行 到位 (2)程 序继续 运行,调 用 函数 swap。 系 统内 存又 为 置 1时的内存状态 函数 swap()分配一 段空间 。参 数结合 后,当程 序执行 到 位置 2时,内存 状态如 图 8.2所 示。 (3)从 函数 swap()返回 前,当程 序执行到 位置 3时,内存状 态如图 8.3所示 。因为 指针 参数 所传递 的 是 地 址值 ,可 以通 过 xx、yy访问 到主 程 序中 的变 量 x、y,所 以 交 换了 变量 x、y 的值 。变量 px和 py所指变 量依 然 是 x和 y,但变 量 x、y的内 容 却 发 生 了变 化。 函 数 返 回 后,实现 了 x、y值 的交换 。 图 8.2 例 8.2中程序执行到位置 2时的内存状态 图 8.3 例 8.2中程序执行到位置 3时的内存状态 该例甚 至可以 不引进 指针变 量 px、py,直 接使用 变量 x、y的 地址调 用函数 swap。主 程 序如 下,函数 swap与 前面相同 。   void ain(){ intx,y; scanf("%d%d",&x,&y);/* 位置 1*/ if(x y) swap(&x,&y); printf("\n%d\t% d\n",x,y); } 该程序运行过程如下: (1)当程 序执行 到位置 1时,系统 在内存为主 函数 main()分配一段空间,如图 8.4所示。 · 196· 第八章 再 论 函 数 (2)程 序继续 运行,调 用函数 swap。 系 统 在 内存 又 为函 数 swap()分配 一 段 空 间,参 数 结 合 后,当 程 序 执行 到 位 置 2 时,内存 状态如 图 8.5所示。注 意,这时通 过形 式 参数 与实在 参数 的结合 ,把 实在 参 数 表达 式 “&x”和 “&y”的值 分 别 送 入   图 8.4 程 序 执 行到 位 置 1 形式 参数变 量 xx、yy中 。 &x和 &y的 值分 别是 变 量 x、y的 时的内存状态 地址 ,从 而使 xx、yy分别指 向主程 序中的 变量 x、y。 (3)从 函数 swap()返回 前,当程 序执行到 位置 3时,内存如 图 8.6所 示。因 为指针 参数 所传 递的是 地址值 ,可 以通过形 式 参 数 xx、yy访 问到 主程 序中的 变量 x、y,所 以 交换 了变 量 x、y的值。 图 8.5 程序执行到位置 2时的内存状态 图 8.6 程序执行到位置 3时的内存状态 请读者 注意交 换 x、y的值 是如何 实现的 ,从 而体会 指针作 参数产 生的作 用。 第四章验 证 Pascal定 理例题 4.4的程 序,传递信 息时让 人感到 十分别 扭,函 数 不能把 多 个计 算结果 带回调 用处。 使用指 针参数 可以解决 这个问 题,例题 4.4的程 序可以改 写。 例 8.3 改写 例 4.4。验证 Pascal定 理,圆 的内接 六边形 三双对 边延线的 交 点 在一条 直 线上。    /*PROGRAM Pascaltheorem*/ #include"math.h" #include"stdio.h" #definePI3.1415927 #defineeps1e-5 floatradius;    * 圆的半径 */ floattheta1,theta2,theta3,theta4,theta5,theta6; /* 六个极角的度数 */ floatxa,ya,xb,yb,xc,yc,xd,yd,xe,ye,xf,yf; /* 六个顶点的直角坐标 */ floatb1_x,b1_y,b2_x,b2_y,b3_x,b3_y; /* 三个交点的直角坐标 */ /* 主程 序 之 前 这 段 为“函数 原 型 ”以 及 各 个函数 返 回 结 果 所 用 变量 */ voidtrans_abcdef(); /* 计 算 六个 顶 点 直 角 坐 标 */ voidcoordinate(float,float,float*,float* ); /* 计 算 一个 顶 点 直 角 坐 标 */ voidthree_inter(); /* 求 三 个交 点 */ voidintersection(flat,float,float,float,float,float,float,float ,float* ,float* ); /* 已 知 四点 ,求 两 条直 线 交 点 */ 8.1  参   数 · 197· voidequation(float,loat,float,float,float,float,float,float ,float*,float*,float*,float*); /*已知四点,求两条直线方程*/ voidstraightline(flat,float,float,float, float* ,float*); /* 已 知 两点 ,求 直线 方 程 斜 率 (a)和 截 距 (b)*/ voidinter(float,flot,float,float, float*,float* ); /* 已 知 两个 直 线 方 程 的斜 率 和 截 距 ,求 它 们 的 交 点*/ inttest(float,float,float,float,float,float); /* 检验 */ /* 主函 数 */ void ain(){ /* 读 入 圆 形 的 半径 */ printf("pleaseinputtheradiusofthecircle:"); scanf("% f",&radius); /* 读 入 六 个 角 */ printf("pleaseinputsixangle:"); scanf("% f% f%f% f% f%f",&theta1,&theta3,&theta3,&theta4,&theta5,&theta6); trans_abcdef(); /* 计 算 六个 定 点 坐 标 */ three_inter(); /* 求 三 个交 点 */ if(tst(b1_x,b1_y,b2_x,b2_y,b3_x,b3_y)) /*验证*/ printf("ok"); else{ printf("Thereisanerrorwhen:radius=% f\n",radius); printf("theta1=% ftheta2=% f\n",theta1,theta2); printf("theta3=% ftheta4=% f\n",theta3,theta4); printf("theta5=% ftheta6=% f\n",theta5,theta6); } } /* 计算 六 个 顶 点 直 角坐 标 */ void rans_abcdef(){ coordinate(radius,theta1,&xa,&ya); coordinate(radius,theta2,&xb,&yb); coordinate(radius,theta3,&xc,&yc); coordinate(radius,theta4,&xd,&yd); coordinate(radius,theta5,&xe,&ye); coordinate(radius,theta6,&xf,&yf); } /* 计算 一 个 顶 点 直 角坐 标 */ void oordinate(floatr,floattheta,float*px,float*py){ * px=r* cos(PI*theta/180); /* 先把“角度”转换成“弧度”,再转换成直角坐标 */ * py=r* sin(PI*theta/180); } /* 求三 个 交 点 */ void hree_inter(){ intersection(xa,ya,xb,yb,xd,yd,xe,ye,&b1_x,&b1_y); · 198· 第八章 再 论 函 数 intersection(xb,yb,xc,yc,xe,ye,xf,yf,&b2_x,&b2_y); intersection(xc,yc,xd,yd,xf,yf,xa,ya,&b3_x,&b3_y); } /* 已知 四 点 ,求两条 直 线 的 交点 */ void ntersection(foatrx,floatry,floatsx,floatsy, floattx,floatty,floatux,floatuy, float* hx,float* hy){ floatl1_a,l1_b,l2_a,l2_b; /* 两条直线的斜率和截距 */ equation(rx,ry,sx,sy,tx,ty,ux,uy,&l1_a,&l1_b,&l2_a,&l2_b); inter(l1_a,l1_b,l2_a,l2_b,hx,hy); } /* 已知 四 点 ,求两条 直 线 方 程*/ void quation(loatrx,floatry,floatsx,floatsy, floattx,floatty,floatux,floatuy, float*ma,float*mb,float* na,float* nb){ straightline(rx,ry,sx,sy,ma,mb); straightline(tx,ty,ux,uy,na,nb); } /* 计算 由 两 点 确 定 直线 方 程 的 斜率 (a)和 截 距 (b)*/ void traightline(floatex,floatey,floatfx,floatfy,float*a,float*b){ * a=(fy-ey)/(fx-ex); /* 斜率 */ * b=ey-a* ex; /* 截距 */ } /* 已知 两 个 直 线 方 程的 斜 率 和 截距 ,求 它 们 的交 点 */ void nter(floatma,floatmb,floatna,floatnb,float* wx,float* wy){ * wx=(nb-mb)/(ma-na); * wy=ma* wx+mb; } /* 检验 */ boolest(foatb1_x,floatb1_y, floatb2_x,floatb2_y, floatb3_x,floatb3_y){ floatpa,pb; /* B1、B2连线方程系数 */ straightline(b1_x,b1_y,b2_x,b2_y,&pa,&pb); if(fbs(b3_y - (pa* b3_x+ pb))<eps) returntrue; else returnfalse; } 在该程 序中,计算 一个顶 点在 直角 坐 标 系 中坐 标 的 函数 coordinate用指 针 参数 px、py 代替了原来的全局量传递计算结果坐标。使用形式是: (1)在 函数声 明中,增加形 式参数 px、py,并 把它声 明成指 针类型 ,构 成指针参 数: 8.1  参   数 · 199·     voidcoordinate(floatr,floattheta,float*px,float* py) (2)在函 数调用 中,用变量的指针 (地 址)作实在 参数,对应相应形 式参数。对 应点 A有:   coordinate(r,theta1,&xa,&ya); (3)在 函数声 明中,对形式 参数 px、py以间 接寻址 方式赋 值,把值直 接送入 实在参 数指 针表 示的变 量中。 对应点 A是 xa、ya。     *px=r* cos(PI* theta/180); *py=r* sin(PI* theta/180); 在这 种参数 中,注意如 下事项: ① 函 数调用时 实在参 数把变 量 xa、ya的指 针分别 送入形 式参数 px、py中 ,因此 px、py 分别 指向 xa、ya。 ② 函 数内部,以间 接 寻址 方 式 赋值,分别 通过 * px、* py把 值送 入 px、py所 指的变 量 中,也就 是 xa、ya中 。 ③ 当 函数返回 后,xa、ya中自 然是函 数内部 给它们 赋的值 。 在本程 序中,求两 条 直线 交 点 函 数 intersection的 参 数 hx、hy;计 算 直 线 方 程 系 数 函 数 straightline的参 数 a、b;求两个 直线方 程交点 函数 inter的参 数 wx、wy等都是 指针 参数。 它 们都以上述方式传递函数的计算结果。 特别指 出函数 three_inter、intersection和 inter之间的 参数 传 递:函 数 intersection有 指针 参数 hx、hy;函 数 inter有指针 参数 wx、wy;函数 intersection调用 inter时 直接 使用形 式参 数 hx、hy对应函 数 inter的形式 参数 wx、wy,那么 wx、wy指向 什么?   void hree_inter(void){ intersection(… ,&b1_x,&b1_y); ⁝ } void ntersection(…,float*hx,float*hy) { inter(… ,hx,hy); } floatnter(… ,float* wx,float* wy) { * wx=(nb-mb)/(ma-na); * wy=ma* wx+mb; } 该参数传递过程是: 函数 three_inter中的 函数调 用 intersection(… ,&b1_x,&b1_y)把 b1_x、b1_y的地址 送 入函 数 intersection的形式 参数 hx、hy中,形式 参数 hx、hy本身 是指 针类 型,它 们分 别指 向 b1_x、b1_y。 · 200· 第八章 再 论 函 数 函数 intersection中的 函数调 用 inter(… ,hx,hy)直接使用 hx、hy对 应函数 inter中 的形 式参 数 wx、wy;wx、wy是 指 针 参 数,参 数 结 合 时 应 该 得 到 指 针 值。实 在 参 数 使 用 函 数 intersection的形式 参数 hx、hy,它们 正好是指 针类型 ,分别保存 b1_x、b1_y的 指针 值。调 用 inter函数后 ,wx、wy分别 指向 b1_x、b1_y。函 数 inter中的两 个赋值 结果分别 给 b1_x、b1_y 赋值。 请读者 认真体 会并思 考在如 下几种 情况下,实在 参数表 达式应 该是怎 样的形式 。 (1)单 独变量 作实在 参数,包括: ① 一般非指针类型变量作一般非指针类型形式参数的实在参数; ② 一般非指针类型变量作指针类型形式参数的实在参数; ③ 指针变量作指针类型形式参数的实在参数; ④ 指针变量作一般非指针类型形式参数的实在参数。 (2)单 独形式 参数变 量作实 在参数 ,包括 : ① 一般非指针类型形式参数变量作一般非指针类型形式参数的实在参数; ② 一般非指针类型形式参数变量作指针类型形式参数的实在参数; ③ 指针类型形式参数变量作一般非指针类型形式参数的实在参数; ④ 指针类型形式参数变量作指针类型形式参数的实在参数。 8.1.3 数组 作参数 数组作参数 我们已经 知道,数 组名实际 是一个 指针,所以 数组名 作实在 参数传 送给形 式 参 数的信 息 实质 上是一 个指针 值,当然相应 形式 参 数 应 该是 指针 类 型的 。使 用数 组 作 函 数 参数 的形 式 一般是: (1)形 式参数 用数组 声明符 说明,如下例 子说明 形式参 数 x是 10个 元素的 数组。   intf(floatx[10]) (2)实 在参数 用数组 名对应 形式参 数数组,例如 ,若 有声明   floata[10]; 则用如下形式调用函数   f(a) 函数调 用 f(a)把数 组 a的 首地址 送入函 数 f的形式 参数 x中,在函数 执 行 期间,形式 参 数 x指向实在 参数数 组 a,用 a参 与进 一步 运 算。也 就是 说,形式 参数和 实在 参数使 用一 个 数组。 数组与 指针有 极其密 切的关 系。数 组作参 数,传递 给形式 参数 的实 际是 实 在参 数数 组 的首 地址,也就 是把实 在参数数 组名的 值送入 形式参 数中。 在函数 内,使用形 式 参 数数组 实 际是 使用实 在参数 数组名 字开始 的 那片 存 储 区。 事 实 上 ,C语 言是 把数 组参 数 当作 指针 来 8.1  参   数 · 201· 处理的。 也就是 说,数组作 参数,形式 参数是 一个指 针类 型变 量;实 在参 数数 组名 实 际也 是一 个 指针 值;参数结 合时,把实 在参数 指针 值 送 入 形式 参 数 中 ,实 际是 把数 组 首 地 址 送入 指针 变 量中 ,然 后形式 参数指 向实在参 数数 组的 第 一个 元素 。在 上 述 例 子中,经 过函 数 调用 、参 数 结合 后,形式参 数 x指向 a[0]。 需要特 别强调 的是,C参数都 是值方 式的,但是 数组 参数传 递给函 数 的值 不是 整个数 组 的值 ,而 是数组 名的值 ,也就 是实在 参数数 组的首 地址。 在函数 内不给 形式参 数 开 辟数组 存 储空 间,只给它 开辟一 个指针空 间,参数 传递的是 实在参 数数组 名字的 指针值 。 数组参数可以有各种变形。 1.省略 数组形 式参数最 外层的 尺寸 最外层的 尺寸对 计算数 组元素 的地址 不起作 用,在对数 组参数 的形式 参数 声明 时 ,可 以 省略 最外层 尺寸。 事实上 即使声 明最 外层 尺 寸,这个 尺寸 也 不 起 作 用。 前 述 f的函 数定 义 说明符可以使用形式   intf(floatx[]) 这种 形式的 形式参 数声明 与前面 所 述形 式 等 价,形 式 参数 的 具 体 尺 寸由 函 数 调 用时 的实 在 参数数组决定。多维情况也是这样。例如   intq(floaty[][20]) 声明 二维数 组参数 y,y每行 20个元素 ,具体 多少行 由实在 参数数 组决定 。设有声 明   floatu[10][20],v[15][20]; 则函 数调用 q(u),进 入函数 q执行 时,形式参 数 y表记 实 在 参 数 数组 u,它是 10行 20列 的 数组 ;而 函数调 用 q(v),进入同 一个函 数 q执行 时,形式参 数 y表记 实在参 数数组 v,它是 15 行 20列 的数组 。 与数组 初值一 样,省略尺寸 的只能 是最外 层,下述函 数定义 说明符 都是错 误的。   intr(floatz[][]) ints(floatz[10][]) intt(floatz[10][][20]) 2.形式 参数用 指针形式 前述函 数 f的 函数定 义说明 符还可 以用形 式     intf(float* x) 这种 形式的 形式参 数声明 与上述 1中的 形 式 等 价,形 式参 数 的 具 体尺 寸 由 函 数 调用 时的 实 在参数数组决定。设有声明   floatc[20],b[15]; 则函 数调用 f(c),进入函 数 f执 行时,形 式参数 x表记 实 在参 数数 组 c,它 是 20个元 素的 一 维数 组;而函数 调用 f(b),进 入同一 个函数 f执 行时,形式 参 数 x表记 实 在参 数数 组 b,它 是 · 202· 第八章 再 论 函 数 15个元 素的一 维数组 。 3.使用 指针作 实在参数 上述 2中 ,形式参 数是一个 指针,对 应实在 参数 可以 是数 组 ;反之,形 式参 数 是数 组,对 应实在参数也可以是指针。下述应用是正确的。   floata[15],b[20]; float* p; int (floatx[]){ ⁝ } int (… ){  ⁝   p=a; … f(p)…   p=b; … f(p)… } * 赋值后,指针 p就是数组 a*/ /* 以 p作实在参数,函数 f内 x表记数组 a*/ /* 赋值后,指针 p就是数组 b*/ /* 以 p作实在参数,函数 f内 x表记数组 b*/ 4.形式 参数和 实在参数 都使用 指针形 式 数组参数 形式还 可以是 形式参 数和实 在参数 都使用 指针 形 式,即形式 参数 是一 个 指针, 对应实在参数也是指针。下述应用是正确的。   floata[15],b[20]; float* p; int (float*x){ … x[i]… } int (… ){  ⁝   p=a; … f(p)…   p=b; … f(p)… } /* 赋值后,指针 p就是数组 a*/ /* 以 p作实在参数,函数 f内 x表记数组 a*/ /* 赋值后,指针 p就是数组 b*/ /* 以 p作实在参数,函数 f内 x表记数组 b*/ 第六章 的许多 例题都 是使用 全局 量 传 递 计算 结 果 数 组,显得 很别 扭 。使 用 数组 参数 可 以解决该问题。 例 8.4 利用 数组参 数重写 第六章 例 6.4,用 “主 元排 序”法 对整数 组 A进 行排序 ,使 之 能对任意整数数组排序。 把被排 序数组 和数组 长度都 作为函 数的参数 。调 用时,被 排序 数 组对 应形 式 参数 a;数 组长 度对应 形式参 数 n。 8.1  参   数 · 203·   void ort(inta[],intn){ inti,j,k,r; for(i=0 i<n-1;i++ ){ j=i; for k=i+1;k<n;k++ ) if(a k] < a[j]) j=k; r=a[i]; a[i]=a[j]; a[j]=r; } } 下面再 举几个 数组参 数的例 题,请 读者从 中体会 各种参 数形式 的用法 和意义。 例 8.5 编写 函数,把给 定数组 中的 n个 整数反 序存放 。 解 该 程序的 逻辑结 构比较 简单,就不再 画 PAD图了,下面 的例子 也是如 此。   void nv(intx[],intn){ inttemp,i,j; for( =0;i<=(n-1)/2;i++){ j=n-1-i; temp=x[i]; x[i]=x[j]; x[j]=temp; } } 设有 声明“inta[10]”,则 可 以 用 “inv(a,10)”调用 该 函数 。 若 还有 声 明“int* ptr”和 操 作 “ptr=a”,则也 可以用 “inv(ptr,10)”调 用该函数 。该函 数还可 以用指 针形式 编写如 下:     void nv(int* x,intn){ inttemp,*i,*j; //j从 后向 前 变 化 ,i从 前 向后 变 化 for( =x,j=x+n-1;i<=x+(n-1)/2;i++,j--){ temp=*i; //temp保存 i所指数组元素 x[i]的值 * i=*j; //x[j]的值送入 x[i] * j=temp; //把 temp中保存的 x[i]的值送入 x[j] } } 这个 函数与 前一个 完成同 样功能 ,可以 使用相 同的方 式调用 。 例 8.6 编写 函数,从 n个整数 中找出最 大和最 小的数 。   void ax_min_value(intx[],intn,int*max,int*min){ int* p; · 204· 第八章 再 论 函 数 * max=* min=* x; for( =x+1;p<x+n;p++){ if( p > *max) *max=* p; if( p < *min) *min=* p; } } //把 x[0]送入 max、min所指变量中 //p从 x[1]开始到 x[n-1]循环 //求极 大 值 ,p指 向 当 前 数 组元 素 //max指 向 极 大 值 单 元 //求极 小 值 设有声明   inta[10],max_value,min_value; 则可以用   max_min_value(a,10,&max_value,&min_value); 调用该函数。若还有声明     int* ptr; 和操作   ptr=a; 则也可以用   max_min_value(ptr,10,&max_value,&min_value); 调用该函数。 8.1.4 其他 程序设计语 言的参数 类别 C语言只 有值参 数,函 数调用 时,一律把 实在参 数 的 值送 入 形式参 数变量 中。 这种参 数 机制 的优点 是简化 了参数 概念,也 降 低 了 编 译系 统 的 复 杂度 ;缺 点 是 给 程 序设 计 带来 不便, 程序 员要关心 十分细 致的函 数之间 的信息 传递。 许多程 序设计 语言还 引进其 他 各 类参数 类 别。 下边简 单介绍 Pascal的变量 参数和 ALGOL60的换名 参数。 首先郑 重重申 ,所 谓变量参 数、换名 参数都 不是 C语 言的 内容,它们 分别 属于不 同程 序 设计 语言。在 此介绍 它们,只是 为了开 阔读者 的眼界 ,不 感兴趣 的读者 完全可 以 不 看此节 内 容。 尤其不 要在 C语言 中试图使 用这些 成分。 1.变量 参数 Pascal语言参 数类别 有两种 :值 参数和变 量 参 数,下 面 用前 述 验 证 Pascal定理中 求六 个 顶点 在直角 坐标系 中坐标 部分的 Pascal程序片段 来介绍 它的变 量参数 。 例 8.7 求六 个顶点 直角坐 标的 Pascal程 序片段 。   VAR ,theta1,theta2,theta3,theta4,theta5,theta6:real;   1} xa,ya,xb,yb,xc,yc,xd,yd,xe,ye,xf,yf:real; {2} {以知矢径和极角,求一点在直角坐标系中的坐标} {3} 8.1  参   数 · 205· PROCE UREcoordinate(r,theta:real;VARpx,py:real);{4} BEGIN 5} px r*cos(PI*theta/180); {6} py r*sin(PI*theta/180); {7} END; {8} {计 算 六 个顶 点 坐 标 } {9} PROCE UREtrans_abcdef; {10} BEGIN {11} coordinate(r,theta1,xa,ya); {12} coordinate(r,theta2,xb,yb); {13} coordinate(r,theta3,xc,yc); coordinate(r,theta4,xd,yd); {14} {15} coordinate(r,theta5,xe,ye); {16} coordinate(r,theta6,xf,yf); {17} END {18} 该程序 片段从 过程(相当 于 C语言的 函数)trans_abcdef开始执 行。无 需解 释,读者应 该 能大致读懂这个程序片段。 在这个 程序片 段中,trans_abcdef六次调 用过程 coordinate。coordinate有四 个形式 参数, px、py是变量 参数,r、theta是 值参数 。调用过 程 coordinate时 程序执 行如下 : 从第 12行 开始执 行,以 r、theta1、xa、ya为实 在 参 数 对应 形 式 参 数 r、theta、px、py调用 过 程 coordinate。r、theta为值 参数,分别 作为独 立变量 将被赋值 以实在 参数表 达式 r、theta1的 当前 值,在 coordinate执行 开始时 ,r、theta分 别具有 r、theta1的 当前值 ; 而 px、py是 变 量 参 数,实 在 参 数 变 量 xa、ya将 替 换 变 量 型 形 式 参 数 px、py。 在 coordinate的 整个执 行期间,px、py将分 别表 记 实在参 数变 量 xa、ya。执 行 coordinate的语 句 部分 时,第 6、7两行的 赋值语 句   px r*cos(PI*theta/180); py r*sin(PI*theta/180); 给 px、py赋值 ,就 是给 xa、ya赋值,从而 把 coordinate的执 行结果 带回主 程序。 由该例题 可以看 出,值参数 与变量 参数是 不一样 的。读 者已经 很熟悉 值参 数,变量参 数 的特性是: (1)与 变量参 数对应 的实在 参数是 一个变量 访问,且实 在参数 与形式 参数要类 型相同 。 (2)变 量参数 的结合 过程是 : ① 首先确是实在参数变量; ② 以实在参数变量代替形式参数变量。 (3)在 整个函 数活化 期间,变 量 参 数 始 终表 记 与 其 相对 应 的 实 在参 数变 量。所 以在 函 数内 ,对 形式参 数的一 切操作实 际上都 是对相 应的实 在参数 的操 作 。显然 ,若 在 函 数内改 变 形式参数的值将直接影响实在参数变量。 · 206· 第八章 再 论 函 数 (4)当 函数执 行结束 返回后 ,实 在参 数值 依 赖 于 在函 数 活 化 期间 是 否 改 变 了相 应形 式 参数 ,并 且其值 就是在 函数活化 期间对 相应形 式参数 的最后 一次 赋 值操 作的值 。因 此 ,可 以 利用变量参数将函数的执行结果带回其调用处。 按前述 变量参 数结合 过程可 知,对变 量参数 而言,使 用哪个 变量 代替 形式 参 数,在进 入 函数 之前已 经确定 ,在 整个 函 数活 化 期 间,相应 形 式 参 数 始终 表 记那 个 确 定的 实 在 参 数 变 量,不会 改变了 。 例 8.8 有如 下程序 片段。在 函数 p中,x是 变量参 数,它的赋 值语句 x 5是给下 标变 量 v[2]赋 值 5,还是 给下标 变量 v[3]赋 值 5?   VAR :integer; v:ARRAY [1..10]OFreal; PROCE UREp(VARx:real); BEGIN i 3; x5 END; BEGIN i 2; p(v[i]) END. 答案 是:给 v[2]赋值 5,而不是给 v[3]。该程 序片段 的执行 过程是 : (1)i 2; (2)执 行函数 调用 p(v[i]): 先确定 实在参 数变量 ,这时 i等 于 2,所 以 v[i]是 v[2]; 再以 v[2]替换 函数 p的形 式参数 x,因而函 数 p中的所 有 x都表 记 v[2]; (3)执 行函数 p的函 数体: i 3;这 时全局 量 i等于 3。 x 5;由于 x表记 v[2]而 不是 v[i],所以 i等于 3对本语 句无 影响;这里 仍是 给 v[2] 赋值 5,而不是 给 v[3]赋值 5;相当于 执行赋 值语句 v[2] 5。 2.换名 参数 ALGOL60语言 参数类 别有两种 :值 参数和 换名参 数。下面 用类似 前述例 题 8.6的程序 片 段介绍它的换名参数。 例 8.9 有如 下 ALGOL60程序 片段。 在函数 p中 ,形 式参 数 x是换 名参 数,y是 值 参数。 P中赋值 语句 x 5是 给哪个 变量赋 值 5?     INTEGER i,k; INTEGER ARRAY v[1:10]; 8.2  返回 指 针 的 函 数 · 207· PROCE UREp(x,y); VALUEy; INTEGERx,y; BEGIN i 3; y 0; x5 END; BEGIN i 2;k 10; p(v[i],k) END 答案是 :x 5给 v[3]赋值 5,而不是 给 v[2]。 本例题 结合上 述例 8.6还 可 以 看 出 变量 参 数和 换 名 参数 的 区 别。 该 程序 片 段 执 行 过 程是: (1)i 2; (2)执 行函数 调用 p(v[i]): 以 v[i]替换 函数 p的形 式参数 x,因而函 数 p中的所 有 x都被 替换成 v[i]。 (3)执 行函数 p的 函数体 : i 3;这 时全局 量 i等于 3。 x 5;由于 x是 v[i],执行的 是赋值 语句 v[i] 5;而此 时 i等于 3,所 以这里 是给 v[3] 赋值 5。 可以看出 ,换名参 数是“用整 个实在 参数原 封不动 地替换 形式参 数”,在 函数内 部再确 定 具体操作。 y是值 参数,函数 内给 y 0不 影响实 在参数 k的值 。函数返 回后,k值仍然 是 10。 8.2 返 回指 针的 函数 函数返回 类型不 允许是 数组类 型和函 数类型 ,除 此之外 允许一 切类型 ,当 然 允 许指针 类 型,而且 还要依 仗指针 来返回数 组和函 数。“返 回指针 的函数 ”这个问 题太复杂 ,这 里不准 备 全部 介绍,请读 者参阅 有关资料 。 本 节 仅 介绍 函 数 结 果类 型 是 一 般 指针 类型 的形 式。带 回 指针值的函数的函数定义说明符形式是:   类型名 *函数名(形式参数表) 例如     float* f(intx,inty) · 208· 第八章 再 论 函 数 该函数定义说明符指明: (1)f是函 数名; (2)x和 y是 两个形 式参数,都是 int类型 ; (3)该 函数的 返回类 型是指 向 float类型 的指针 ,即 “float* ”。 可以这 样解释 该函数 定义说 明符。 按运算符 优先级 规定,“*”的 级别低于 “()”,因 此   f(intx,inty) 是一 个函数,它 的类型 是“float* ”,即函数 f的类 型是 float类型指 针。 函数 f是 返 回指针 值 的函数。 在函数 内,return语 句后边的 表达式 当然应 该是“类 型名 * ”类型的 。比如 若有声 明   floatu,*v; 则下 述 return语 句都是 正确的   return&u; rerurnv; 而语句   returnu; 是错误的。 下面举一个返回指针值的函数例子。 例 8.10  编写程 序,读入月份 数,输出该 月份的 英文名 称。   #include <stdio.h> char* month_name(int); intm in(void){ intn; char* p; printf("Inputanumberofamonth:"); scanf("% d",&n); p=month_name(n); printf("Itis% s\n",p); } char*name[]= "illegalmonth", "January","February","March", "April","May","June", "July","August","September", "October","November","December" }; char*m nth_name(intm){ if(m< ||m>12) returnname[0]; 8.2  返回 指 针 的 函 数 · 209· elsereturnname[m]; } 函数 month_name有一个 int类型参 数,带入月 份数;返回 相应月 份英文名 称 字 符串的 首 指针。程序运行在输入阶段显示如下信息:   Inputanumberofamonth: 如果这时输入   2 程序输出   ItisFebruary 下面 把该问 题复杂 化,进一步说 明指针 参数。 例 8.11  编写程 序,读入月份 数,输出该 月 份 的英 文名 称 。要求 用 4*3指针数 组保 存 一年 12个月每 个月份 名称的英 文字符 串指针 。 解 用 4行 3列 的指针 数组保 存表示 12个 月的 12个字符串 指针。 程序如 下:   #include"stdio.h" char* mp(char*[4][3],int); /* 函数 mp的原型声明 */ int ain(void){ char* ame[][3]= {"January","February","March"}, {"April","May","June"}, {"July","Auguest","September"}, {"October","November","December"}}; char* p; intm; printf("Inputeranumberofmonth:"); /* 读入月份 */ scanf("%d",&m); p=mp(name,m ); /* 求月份字符串指针 */ printf("the% dthmonthis% s\n",m,p); /* 输出 */ } char* mp( har* cp[4][3],intn){ introw,col; row =(n-1)/3; col=(n-1)%3; return*(* (cp+row)+col); } 程序运行在输入阶段时显示如下:   Inputanumberofamonth: 如果这时输入 /* 求月份字符串指针函数 */ /* 行标 */ /* 列标 */ /* 返回月份字符串指针 */ · 210· 第八章 再 论 函 数   2 则程序输出   the2thmonthisFebruary 在该程序 中,每个 数组元素 都是指 针,指向相 应月份 的字符串 。这 样,二 维数 组 name既 有行 指针,又有 列指针 ,且 每个元 素又是 指针。因 此有 (1)行 指针 name+row指向 name的第 row行; (2)列 指针 * (name+q)+ col指向 name的第 row行第 col列; (3)元 素指针 *(*(name+row)+col)表示 name的 第 row行 col列元素 值,指向 具体 字符串。 函数 mp把字 符串指 针作为函 数值。 参数 cp的 类型和二 维数组 name的类 型是等 价的, 可以 用 name与 它对应 ,传送二维 数组 name的首 地址。 于是 cp指向 name的 第一行 ,即 第一 行的 首地址 。函数 中首先 根据月 份求 出 相应 月份 在 数 组 中的 行 (row)和列 (col),最 后求 出 相应 数组元 素中保 存的指 针后返 回。对 返回值“* (*(cp+row)+col)”解释 如下: (1)cp:name第 1行的首地 址; (2)cp+row:name第 row行的首 地址; (3)* (cp+row):name第 row行第 1列的地 址; (4)* (cp+row)+col:name第 row行第 col列的 地址; (5)* (*(cp+row)+col):name第 row行 第 col列 的元素 (即字符 指针)。 C指针函 数只允 许返回全 局变量 指针、静态 变 量指 针和 堆内空 间地址 ,不 允许 把函数 内 部声 明的局 部变量 指针(地址 )作为返 回 值。 在例 8.10中,如 果 把 字 符串 数组 name放在 函 数 mp之内 声明就 错了。 因为函数 内部声 明的 局部变 量在 栈区 分 配空 间,函数 调用结 束时, 释放 栈区中 函数运 行空间 ,会造 成返回 指针所 指存储 空间已 经不存 在。 8.3  作  用  域 8.3.1 作用 域 所谓作 用域,就是 使程序中 声明 的标 识符 有 定义 的区 域 。在一 个标 识符 的 作用 域内 使 用它 是合法 的,并且所 使用的就 是相应 声明的 标识符 。 作用域是 一个静 态概念 ,描 述从程 序静态 行文上 看,程序中 一个被 声明的 标 识 符起作 用 的范 围(一段程 序行文 )。本节 所涉及 的内容 都是从 程序的静 态行文 上看。 1.声明 点 C程序中 出现的 每一个 标识符 都有一 个声明 点,标 识符的 声明点 就 是 “包 含 相 应标识 符 8.3 作   用   域 · 211· 词法 记号的 声明符 的结尾 处”。例如   intx/* 此处为 x的声明点 */,u[10][20]   /* 此处为 u的声明点 */; 2.作用 域规则 每个声 明点都 有作为 程序正 文的 一 部 分 的一 个 区 域 ,称 为声 明的 作 用域 。 声明 的作 用 域是 “相应声明 有效的 程序文 本区域 ”,表 8.1列出 各种 C标识 符的作 用域。 类别 顶层标识符 函数定义中的形式参数 函数原型中的形式参数 复合语句中声明的标识符 语句标号 预处理器的宏 表 8.1 C标识符作用域 声明的作用域 从声明点到本源程序编译单位文本结束 从声明点到函数体结束 从声明点到函数原型结束 从复合语句中声明点到复合语句结束 相应标号声明所在的整个函数体 从相应宏定义的#define命 令到 本源 程序 编译单 位文 本结 束,或第 一 个 取消 相 应宏 定 义 的 #undef命令 在 C语言中 ,每 个源程 序编译 单位、每个函 数定义 、函 数原型 、复 合语句都 各 自 构成一 个 作用 域区域 。C语言 规定: (1)在 嵌套结 构中,若里层 区域的 一个标 识符与 外层区 域的某 标识符 同名 ,则 外层标 识 符的作用域不包括里层那个同名标识符的作用域区域。 (2)C程 序中使 用的任 意一个 标识符 必须声 明,并且必 须先声 明后使 用,在 同 一区域 内 任何标识符不得重复声明。 例 8.12  下述应 用都是 错误的 : (1) /* 标识符 k没有声明 */ inti,j; int(void){ ⁝ k=i+j; } (2) /* 标识符 x在同一作用域区域中重复声明 */ floa f(intx){ intx; ⁝ } (3) /* 标识符 c在同一作用域区域中重复声明 */ #definec2.0 typedefintt; t ,d,e; · 212· 第八章 再 论 函 数 ⁝ (4) /* 标识符 index和 t2先使用,后声明 */ #definec2.0 t2y[index]; typedeft2float; #defneindex10 ⁝ 8.3.2 生存 期 生存期是 一个动 态概念 ,描 述当程 序运行 起来后 ,一 个对象(标 识符)是 否有效 存在。 所 谓对象主要是指变量和函数。 与类型不 同,变量 和函数在 程序运 行时要 分配存 储空间 ,也 就是 具有存在 性。 对象的 生 存期是保持所分配存储空间的那段程序的运行期间。 如果变 量有一 个初始 化语句 ,则每 次进入 它的生 存期创 建它的 同时,对它 重新初 始化。 1.静态 生存期 如果所分 配的存 储空间 在程序 执行前 开始,并保 持到程 序执行 终止,则称 此 对 象具有 静 态生 存期。 在 C语言中 : (1)所 有函数 都具有 静态生 存期; (2)所 有顶层 声明的 变量都 具有静 态生存期 ; (3)复 合语句 中声明 的变量 是否具 有静态生 存期,取决 于具体 声明的 存储类别 ; (4)函 数形式 参数是 否具有 静态生 存期,取决于 具体声 明的存 储类别 。 具有静态 生存期 的对象 (变量、函数 )在程序 的整个 运行期 间全都 存在。也 就是说 ,程 序 开始 运行,它的 生存期 开始并创 建它;一 直到程序 运行结 束,它的生 存期结 束并删除 它。 2.本地 生存期 如果对象 在进入 复合语 句或函 数时生 成,在退出 复合语 句或函 数时删 除,则 称 为具有 本 地生 存期。 具有本 地生存 期的对 象当 程 序 运 行进 入 复 合 语 句或 函 数 时 创 建它,并且 它的 生 存期 开始;当程 序运行 退出复 合 语 句或函 数时删 除它,并 且 它的生 存期结 束。C语 言中 把具 有本地生存期的变量称为自动变量。 (1)复 合语句 中声明 的变量 是否具 有本地生 存期,取决 于具体 声明的 存储类别 ; (2)函 数形式 参数是 否具有 本地生 存期,取决于 具体声 明的存 储类别 。 3.动态 生存期 C语言中 的数据 对象还可 以具 有动 态生 存 期,可以 随意 显式 地创 建 和删 除。动 态对 象 的创 建和释 放通过 动态存 储分配 函数(malloc、free等)显 式实现 。 复合语 句中声 明的变 量、函 数形 式参数 具有 本地 生存期 还是 具有静 态生 存 期依 赖于 它 8.3 作   用   域 · 213· 的存 储类别 。有关 存储类 别的概 念不 在 这 里 介绍,有 兴趣 的 读者 可以 查 阅 第 十 三章 或有 关 资料。目前使用它们的省缺存储类别。 (1)所有 复合语 句中声明的 变量、函数形式参 数全部具 有本地生存 期,全部为自动 变量; (2)所 有顶层 声明的 变量具 有静态 生存期,全部 为静态 变量; (3)所 有函数 都具有 静态生 存期。 8.3.3 局部 量和全局量 由作用域 规则可 知,一个函 数或复 合语句 引入的 标识符 只在本 函数或 复合 语句 内 有效, 在本 函数或复 合语句 外便失 去了它 的意义 。称它 们是局 部于声 明它们 的那个 函 数 或复合 语 句的 ,或 称该标 识符相 对于声明 它们的 那个函 数或复 合语句 来讲是 局部量 。 由作用域规则还可知: (1)任 何顶层 声明的 标识符 在 所有 函 数 内 部以 及 复 合 语句 都可 以 使 用,只 要在 函数 内 部和 复合语句 中没有 与它同 名的其 他声明 。称顶 层声明 的标识 符相对 于函数 和 复 合语句 来 讲是全局量。 (2)若 一个复 合语句 嵌套在 一 个函 数 或 另 一个 复 合 语 句之 内,且 某 一 个 标 识符 在函 数 或其 外层复合 语句中 声明,而且 在内层 复合语 句中没 有与相 应标识 符同名 的声 明,则函数 或 外层 复合语 句中关 于这个 标识符 的 声明 在 内 层 复合 语句 中 仍然 有效 ,即 在 内 层 复合 语句 中 仍可 使用这个 标识符 。称在 函数或 外层复 合语句 中声明 的标识 符相对 于其内 层 复 合语句 来 讲是全局量。 全局量 和局部 量是一 个相对 概念。 某标识 符相 对于 某复合 语句 是局 部的,相对 于其 内 层复 合语句 却是全 局的;反之,某 标识 符 相 对 于内 层 某 复 合 语句 是 全 局 的 ,但 是 相对 于声 明 它本身的那个复合语句来讲却是局部的。 全局量 在内层 具有外 层同样 的意义 ,在内 层对全 局量的 操作直 接反应 在外层程 序中。 下面再 举一 例,说 明 指 针 作 参 数、作 用 域、局 部 量 和 全 局量。 例 8.13  编 写 程序,计 算 调 合 级 数 的 前 N项 和。 要 求 结果 是一个 准确的 分数 BA形式。 Hn =11+ 21+31 +… + 1 n 算法 如图 8.7所示,C程序 如下:   图 8.7 计算调合级数    /* ROGRAM thesummationofaseriers*/ #include"stdio.h" inta,b,n; /* 1*/ /* 2*/ · 214· 第八章 再 论 函 数 voidadd int*e,int*f,intii){ *e=(*e)* ii+ (*f); *f=(*f)* ii; } /* 3*/ /* 4*/ /* 5*/ /* 6*/ intgd(intu,intv){ intr; r=v; while r!=0){ r=u% v; u=v; v=r; } returnu; } /* 7*/ /* 8*/ /* 9*/ /* 10*/ /* 11*/ /* 12*/ /* 13*/ /* 14*/ /* 15*/ /* 16*/ void educe(int*x,int*y){ intg; g=gcd(x,y); * x=(* x) /g; * y=(* y) /g; } /* 17*/ /* 18*/ /* 19*/ /* 20*/ /* 21*/ /* 22*/ int ain(void){ inti; printf("pleaseinputthevalueofn:"); scanf("%d",&n); a=0; b=1; for(i=1 i<=n;i++ ){ add(&a,&b,i); reduce(&a,&b); } printf("% d\n",a); printf("Hn= ——— \n"); printf("% d\n",b); } /* 23*/ /* 24*/ /* 25*/ /* 26*/ /* 27*/ /* 28*/ /* 29*/ /* 30*/ /* 31*/ /* 32*/ /* 33*/ /* 34*/ /* 35*/ /* 36*/ 在该程序中 (1)变 量 a、b、n,函数 main、add、reduce、gcd在顶层 声明,是全 局量。 它们的 作用域 是 它们各自的声明符以后直到整个程序文件结束。 (2)变 量 i在主函 数 main中声 明,是 main的 局部 量。它 们的作 用域 是第 24行 它的 声 明符 以后直 到第 36行 。 8.4  递   归 · 215· (3)add的形 式参数 e、f、ii在 add中声 明,是 add的 局部量 。它们 的作用域 是第 3行它 们各 自的声 明符以 后直到 第 6行。 (4)gcd的形式 参数 u、v和在 gcd内部 复合 语 句中 声明 的 变量 r是 局 部于 gcd的局 部 量。 它们的 作用域 是第 7、8行它们 各自的 声明符 以后直 到第 16行。 (5)reduce的形 式参数 x、y和 在 reduce内 部复合 语句中 声明的 变量 g是 局部于 reduce 的局 部量。 它们的 作用域 是第 17、18行它 们各自 的声明 符以后直 到第 22行 。 在本程序的执行过程中: (1)第 30行调 用函数 add作 加法。 ① 以 &a、&b作实 在参数 ,对 应函数 add的形 式参数 e、f,分 别把 a、b的地 址送入 指针 形式 参数 e、f中 。在 add中,采用间 接寻址 为 *e、*f赋值,因 为 e、f分 别 指向 a、b,就 是 给 a、b赋值 。 ② 以 i作实 在参数 对应形式 参数 ii,把 i的值送 入 ii之中,参与 运算。 ③ 另 外,这 个函数 中还通 过间接 寻址 * e、*f访问 a、b,用 a、b的 值参与运 算。 (2)第 31行调 用函数 reduce作 约分,参数 结合以 及信息传 递类似 add。 (3)第 19行 reduce函数体 中调用 函数 gcd,以 x、y作实在 参数对 应形式 参数 u、v,把两 个整 数值传 入函数 gcd。在 gcd内,虽然 反复给形 式参数 变量 u、v赋 值,但是由 于 C参数 的 值参 数特性 ,这 些 赋 值 不 影响 实 在 参 数 ,不 改 变 实 在 参 数 x、y的 值 。 所 以 当 从 gcd返 回 reduce后,x、y的 值没有 变化,保证了 程序的 正确性 。 8.4 递    归 8.4.1 递归 程序 例 8.14  编写一 个函数 factorial计 算阶乘 n!。 按过去 的程序 设计思 想,该 函数应 该写成 :   intactorial(intn){ inti,p; p=1; for(i= ;i<=n;i=i+1) p=p* i; returnp; } 现在 换一个 角度考 虑问题 ,n!不仅是   1×2×3×… ×n · 216· 第八章 再 论 函 数 还可以定义成 1, 当 n=0 n! = n×(n-1)!, 当 n>0 按照 该定义 ,n!就 是一个 简单的 条件语 句和表达 式计算 ,可 以编写 函数如 下:   intactorial(intn){ if( ==0) return1; else returnn* factorial(n-1); } 问题 是该函 数是否 正确? 可以在 函 数 factorial内 又调用 函数 factorial本身 吗 ?回答 是肯 定 的。首先按作 用域规 则,在函数 factorial内 又调用 函数 factorial本身 是合法 的;其次,C系 统 保证 上述调 用过程 执行的 正确性 ,这就 是递归 。 从静态 行文角 度看,在定义 一个 函数 时,若 在定 义它 的内部 又出 现对 它本 身 的调 用,则 称该函数是递归的或递归定义的。 从动态执 行角度 看,当调用 一个函 数时,在 进 入 相 应函 数 且 还 没 退 出 (返回 )之前 ,又 再 一次 地调用 它本身 ,而再 一 次 进 入 相应 函 数,则 称 之 为 递 归,或 称 之 为 对 相 应 函 数 的 递 归 调用。 称递归 定义的 函数 为递 归 函数。 上述函 数 factorial就是 递 归 函数。 若 计算 5!,使用 函 数调 用 factorial(5),计 算过程 如图 8.8所示 。 图 8.8 farctorial(5)计算过程 8.4.2 递归 程序设计 有许多 实际问 题可以 递归定 义,采 用递归 方法来 编写程 序十分 方便和 简单。 例 8.15  x的 n次幂,可以 定义为 xn = 1, 当 n=0 x*xn-1, 当 n >0 计算它的递归函数是: 8.4  递   归 · 217·     float ower(floatx,intn){ if(n= 0) return1; else    returnx* power(x,n-1); } 例 8.16  n次勒让 德多项式 ,定 义为 1, 当 n=0 pn(x) = x, 当 n=1 ((2n-1)xpn-1(x)-(n-1)pn-2(x))/n, 当 n>1 计算它的递归函数是:   float (intn,floatx){ if( ==0) return1; elseif(n== ) returnx; else        return((2*n-1)*x*p(n-1,x) - (n-1)*p(n-2,x))/n; } 递归程 序设计 的思想 体现在 :用 逐步 求精原 则,首先 把一个 问题 分解 成若 干 子问 题,这 些子 问题中有 的问题 与原始 问题具 有相同 的特征 属性,至多 不过是 某些参 数不 同,只是规 模 比原 来小了。 此时,就 可以对这 些子问 题实施 与原始 问题同 样的分 析算法 ,直 到 规 模小到 问 题容 易解决或 已经解 决时为 止。也 就是说 ,若 将整个 问题的 算法设 计成一 个函 数,则解决 这 个子问题的算法就表现为对相应函数的递归调用。 再看 n!的例 子。该 问题是:当 n>0时,若能 计算出   w=(n-1)! 则     r=n!=n* w 因此 ,得 如下抽 象算法 : (1)计 算:w=(n-1)!; (2)r=n*w。 由于 子算法 (1)的 特征属 性与 整 个 算 法完 全相 同 ,只 是参 数不 同,w代 替了 r,n-1代替 了 n。这时 若将整 个算法 表示为 f(n,r)则 子算法 (1)就是   f(n-1,w) 即是 对 f的递归调 用。再 考虑 n=0时,这时 r=0!=1,从 而得到计 算 n!的 函数 fact。若 把该 函数 的参数 r用 函数值 表示,就是 前面例 8.14的函数 factorial。 · 218· 第八章 再 论 函 数   void act(intn,int*r){ intw; if n==0) * r=1; else{  factorial(n-1,&w); *r=n* w } } 这里讲 的只是 一般规 律和程 序 设计 思 想 。实际 使用 时,设计 递归 函 数要 复杂 得多。 编 写递归程序要注意: (1)递 归程序 好看、好读、风 格优美 ,但执 行效率 低。 (2)计 算 n!的 函数既 可以写 成循环 形式,也可以 写成递 归形式 。但是有 些 循 环程序 写 成递 归很困 难。反 之,有些递归 程序写 成循环 也很困 难,甚至是 不可能 的。 (3)终 结条件 。程序 一定要 能终止 ,不能 无限递 归下去 。 (4)使 用全局 量要特 别小心 。用不 好,单 元发生 冲突,将导 致程序 出错。 例 8.17  汉诺(Hanoi)塔游 戏。 该问题 又称世 界 末 日问 题。 相 传,古 印 度 布 拉 玛 婆 罗 门 神 庙 的 僧 侣 们 做 一 种 被 称 为 Hanoi塔的游 戏。该 游戏是 :在一 个平板 上有 三根钻 石针 ;在 其中 一根针 上有 成塔型 落放 的 大小 不等的 64片金片 ;要求 把这 64片 金片全部 移到另 一根钻 石针上 。移动 规则是 : (1)每 次只允 许移动 一片金 片; (2)移 动过程 中的任 何时刻 ,都不 允许有 较大的 金片放 在较小 的金片 的上面; (3)移动 过程中 ,三根钻石针都可 以利用,但是金片 不许放在除钻石针 以外的任何 地方。 不论白天 黑夜都 有一个 僧侣在 移动,据说 当 64片金 片全部 从一根 钻石针 移 到 另一根 钻 石针 上的那 天就是 世界的 末日。 到那时 他们的虔 诚信徒 可以升 天,而其他 人则要下 地狱。 当然这 只是传 说,按照规则 把 64片 金 片全 部从 一 根针 移到 另一 根针 上,总 的移 动次 数 是 264 -1次,若一 秒 移动 一次,不 发生错 误,日 夜 不停 地 移动,约需 5849亿年 。 而 太阳系 的 寿命 仅有 100~150亿年 而已。 请编写 程序,打印 金片的移 动顺序 。 不妨设 三根钻 石针顺 次编号 为 a、b、c,开始 所有 64片金片全 部在 a针上 ,现在要 把它 们移 动到 b针上 ,移动过 程中 c针可 以利用,如图 8.9所 示。 怎样进行 移动?开始 就遇到把 a针最 上边的金 片先 移到 b针上,还是 先移到 c针 上?没 有任 何根据作出 决定,按一般方法 是不好 解决这个问题 的。下面换一个角 度来考虑该 问题。 试想,若 能 够 把 a针 上 的 64片 金 片 全 部 移 动 到 b针 上 ,则 在 移 动 过 程 中 一 定 有 如 图 8.10所 示的一 种格局 出现。 8.4  递   归 · 219· 图 8.9 汉诺塔游戏初始状态 图 8.10 汉诺塔游戏中间状态Ⅰ 就是说 ,必 须能够 先把 a针 顶部的 63片 金片移 到 c针上。 现在,可 以很容易 地把 a针上 的一 片金片 移到 b针上,如图 8.11所示。 图 8.11 汉诺塔游戏中间状态Ⅱ 然后,再按 照把 a针 上的 63片 金片移 到 c针上 的算 法,把 c针 上 的 63片 金片全 部移 到 b针上。 从而,完成 了题目要 求的工 作,如图 8.12所 示。 图 8.12 汉诺塔戏结果状态 按照这 个想法 ,移 动 64片金片 的问题 可以被 分解成 : (1)把 a针上的 63片金片 从 a针 移到 c针 上,移动过 程中 b针可以 利用; (2)把 a针上的 一片金 片移到 b针上 ; (3)把 c针上的 63片金片 ,从 c针移到 b针 上,移 动过程 中 a针 可以利 用。 · 220· 第八章 再 论 函 数 到此 ,问 题虽然 没有解 决,但 是已经 向解的 方向前 进了一 步,移动 64片金片 的问题 变成移 动 63片 金片的 问题了 。这一步 虽然很 小,甚至是 微不足 道的,但却 是十分 重要的。 步骤(2)很容 易完成 ;步骤 (1)、(3)的问 题与原 始问 题具 有相同 的特 征属性 ,只 是规 模 小了 ,移 动 64片金片 的问题 变 成 了 移动 63片金 片的 问题,另 外 还 有 一 些 参数 不同。 设想, 若有一个函数   move(n,x,y,z) 能够 完成:“把 x针 上的 n片金片 移动到 y针上,移 动过程 中可以利 用 z针 。”,则 上 述原始 问 题可 以描述 成对 move的调用 :   move(64,a,b,c) 分解 算法中 的步骤 (1)、(3)也 可以分 别描述成 对 move的 调用:   move(63,a,c,b) 和   move(63,c,b,a) 现在 ,考 虑 move函 数。按 分解移 动 64片金片 问题的 思路,问题 :“把 x针上的 n片 金片移 动 到 y针上,移动过 程中可 以利用 z针”可以 被分解 成: (1)把 x针上的 n-1片金 片从 x针移到 z针上,移动过 程中 y针可以 利用; (2)把 x针上的 一片金 片移到 y针上 ; (3)把 z针上的 n-1片金 片从 z针移 到 y针 上,移动过程 中 x针 可以利 用。 按该 分解算 法,并考虑 递归出口 (显 然 ,当 移动 金片 的片 数为 0时 ,便 不用 移 动 了 ),最后 得 到完 整的 Hanoi塔 问题解 法程序 如下:    /* PROGRAM hanoi*/ #include"stdio.h" voidm veone(charu,charv){ printf("%c-> %c\n",u,v); } voidm ve(intn,charx,chary,charz){ if( >0){ move(n-1,x,z,y); moveone(x,y); move(n-1,z,y,x) } } intman(void){ intn; printf("pleaceinputn:"); scanf("%d",&n); move(n,a,b,c) } 8.4  递   归 · 221· 执行 hanoi程序 时不要 输入太 大的 n。当输 入 3时 ,输 出结果 如下:   a->b a->c b->c a->b c->a c->b a->b 它给出了当有三片金片时金片的移动顺序。 8.4.3 间接 递归 前面讲的 递归程 序都是 在函数 本身的 函数体 内调用 自己。 而递归 的动态 含 义 是在调 用 函数 进入函数 后,没有 退出之前 ,又 再一次 调用本 函 数。 可能 存在图 8.13所示 情况,这显 然 也是 进入 P后,没退出 P之 前又 再一 次调 用 P,这 种情 况称 为“间 接 递 归”。相 应 前 面 讲 的 “在 P中直接 调用 P”称 为“直接递 归”。 显然,在间 接递归 情况下,各函 数之间 的位置 关系不 论怎样排 列都会 违背 C语 言的标 识 符必须先声明后使用的原则。例如   intp(… { ⁝ … q(…)… ⁝ } intq(… { ⁝ … p(…)… ⁝ } 图 8.13 间接递归 不论 怎样排 列 p与 q的 位 置 ,都 解决不 了问题 。 为 了解决 这个问 题,必 须 使用 函数原 型,对 一个函数进行提前声明。     intq(… ); intp(… { ⁝ … q(…)… ⁝ } intq(… { ⁝ · 222· 第八章 再 论 函 数 … p(…)… ⁝ } 第一 行说明 函数标 识符 q及其 特性,但具 体有关 q的算法 部分在 后边给 出。 例 8.18  编写程 序,从终端读 入表达 式,计算表 达式的 值。表 达式遵 守先乘 除后加 减的 运算 规则,并且 允许有 括号。它 的语法 是:   E→T+Ex T-Ex T T→F*Tx F/Tx F F→ <数字 >x (E) <数字 >→0x 1x 2x 3x 4x 5x 6x 7x 8x 9 解 把 每个语 法单位 的处理 编 写成 一个 函数,全 局 量 w放 当前 字符 ,算 法 如 图 8.14~ 图 8.17所示。 图 8.14 计算表达式 图 8.15 计算Ⅰ 图 8.16 计算Ⅱ    /* ROGRAM calculateexprission*/ #nclude"stdio.h" charw; 图 8.17 计算 f /* 1*/ /* 2*/ 8.4  递   归 floatv; voidt(float* vt); voidf(float* vf); void (float*ve){ floatv2,v1; charw1; t(&v1); if(w== ) ||(w== - )){ w1=w; scanf("% c",&w); e(&v2); if(w1 = + ) * ve=v1+v2; else * ve=v1-v2; } else*ve=v1; } void (float*vt){ floatu1,u2; charw1; f(&u1); if((w== * )||(w== /)){ w1=w; scanf("% c",&w); t(&u2); if(w1= * ) * vt=u1* u2; else  * vt=u1/u2; } else* vt=u1 } void (float*vf){ if( == (){ scanf("% c",&w); e(vf); scanf("% c",&w); }els { *vf=(int)w -(int)0; scanf("% c",&w); } } /* 3*/ /* 4*/ /* 5*/ /* 6*/ /* 7*/ /* 8*/ /* 9*/ /* 10*/ /* 11*/ /* 12*/ /* 13*/ /* 14*/ /* 15*/ * 16*/ /* 17*/ /* 18*/ /* 19*/ /* 20*/ /* 21*/ /* 22*/ /* 23*/ /* 24*/ /* 25*/ /* 26*/ /* 27*/ /* 28*/ /* 29*/ /* 30*/ * 31*/ /* 32*/ /* 33*/ /* 34*/ /* 35*/ /* 36*/ /* 37*/ /* 38*/ /* 39*/ /* 40*/ /* 41*/ /* 42*/ /* 43*/ /* 44*/ /* 45*/ · 223· · 224· 第八章 再 论 函 数 int ain(void){ scanf("%c",&w); e(&v); printf("\n=% f\n",v); } /* 46*/ /* 47*/ /* 48*/ /* 49*/ /* 50*/ 该程 序的第 4、5行分 别是 函 数 t、f的 函 数 原 型;第 21~35行 是 函 数 t的具 体说 明 部 分;第 36~45行是函 数 f的具体 说明部 分。 例 8.19  画 n次希 尔伯特 (D.Hilbert1891)曲 线。 图 8.18所示 分别是 1次、2次 、3次希 尔伯特 曲线 图 8.18 希尔伯特曲线 可以看 出,2次曲 线是由 四个不 同方向的 1次曲 线用 三 条线 段连 接而成 的 (粗线 部分 就 是连 接线段 );3次曲线 又是由 四个不 同方向 的 2次 曲线 用三 条 线段 连接 而成的;若 定义 0 次曲 线为空 ,则 1次曲 线也是由 四 个 不 同 方 向的 0次 曲线 用 三 条 线 段连 接而 成的。 把四 个 方向 分别定 义为 A、B、C、D,图 8.19所示分别 是四个 不同方 向的 2次曲线 。把画 连接线 段的 函数 记为指 向相应 方向的 箭头,则 A方向 按顺序 “ ”画 出,由 j-1次曲线生 成 j次曲线 的过程 是:D←A↓ A→B; B方向 按顺序 “ ”画 出,由 j-1次曲线生 成 j次曲线 的过程 是:C↑B→B↓ A; C方向 按顺序 “ ”画 出,由 j-1次曲线生 成 j次曲线 的过程 是:B→C↑C← D; D方向 按顺序 “ ”画 出,由 j-1次曲线生 成 j次曲线 的过程 是:A↓D→ D↑C。 图 8.19 四个不同方向的希尔伯特曲线 8.4  递   归 · 225· 假设(这个 假设是 TurboC图 形函数 库中的 函数): (1)标准 函数 initgraph设置 终端显 示器为图 形方式 ,屏 幕由 1024*768个 点组成 ,左下 角为 (0,0),右 上角为 (1024,768); (2)标 准函数 closegraph关 闭终端 显示器 的图形 方式,重新设 置它为 常规的 字符方 式; (3)标 准函数 moveto(x,y)把画笔 从当前 点移动 到点(x,y),不画线 ; (4)标 准函数 lineto(x,y)把画 笔从当前 点移动 到点(x,y),并画一 条线段; (5)设 每条线 段的长 度为 len个 点; (6)曲 线画在 屏幕中 间; (7)用 x、y记录当 前画笔所 在位置 。 利用递 归技术 很容易 写出画 每个方 向 j次 曲线的 函数 。最后 画前 n次希 尔伯特 曲线 的 程序如下:    /*PROGRAM Hilbert*/ #include"graphics.h" #include"stdlib.h" #include"conio.h" #include"stdio.h" #defineL400 intx,y,len; /* 函数原型 */ voidA(int); voidB(int); voidC(int); voidD(int); voidstart(); voidset(int,int); /* 函数 */ void (inti){ if(i 0){ D(i-1);x- =len;lineto(x,y); A(i-1);y- =len;lineto(x,y); A(i-1);x+ =len;lineto(x,y); B(i-1); } } void (inti){ if(i 0){ C(i-1);y+ =len;lineto(x,y); B(i-1);x+ =len;lineto(x,y); B(i-1);y- =len;lineto(x,y); A(i-1); /开 启 绘 图仪 //设 置 画 笔初 始 位 置 · 226· 第八章 再 论 函 数 } } void (inti){ if(i 0){ B(i-1);x+ =len;lineto(x,y); C(i-1);y+ =len;lineto(x,y); C(i-1);x- =len;lineto(x,y); D(i-1); } } void (inti){ if(i 0){ A(i-1);y- =len;lineto(x,y); D(i-1);x- =len;lineto(x,y); D(i-1);y+ =len;lineto(x,y); C(i-1); } } void ain(){ //主 函 数 inti,cx,cy,n; printf("pleaseinputthen(1-7):"); scanf("% d",&n); if(n> ||n<=0) exit(0); start(); //开 绘 图 仪 i=0; len=L;cx= len/2;cy=cx; do{ i++; len=len/2;cx+ =len/2;cy+ =len/2; set(cx,cy); A(i); }while(i!=n); //分别画出第 i次希尔伯特曲线 getch(); closegraph(); } void tart(){ //开 启 绘 图仪 intdriver=DETECT,mod,errcode; initgraph(&driver,&mod,"F:\\tc30\\bgi"); //初始化绘图仪 errcode=graphresult(); if(ercode!=grOk){ //绘 图 仪 无法 正 确 打 开,则 退 出 程序 printf("Graphicserror:% s\n",grapherrormsg(errcode)); exit(1); 8.4  递   归 · 227· } setcolor(WHITE); setbkcolor(BLACK); } void et(intcx,intcy){ x=cx;y=cy; moveto(x,y); } //正 确 打 开绘 图 后 ,设 置 画 笔 颜 色为 白 色 //设 置 背 景颜 色 为 黑 色 //设 置 画 笔初 始 位 置 //画 笔 移 动到 点 (x,y),不 画 线 希尔伯 特曲线 可以用 来作 美 工设 计,把 前 n次 曲线 迭 加 在 一 起,各 次 曲线 的 中 心 点 相 同,并且 让 j次曲线 的线段长 度取 j-1次 曲 线的 线段 长度 的 一 半 ,则 可 以 做出 很 好 看 的 图 案。 图 8.20和图 8.21所示 分别是 由五次 和前五 次希尔 伯特曲线 构成的 图案。 图 8.20 五次希尔伯特曲线 图 8.21 前五次希尔伯特曲线构成的图案 8.4.4 递归 程序执行过 程 递归程 序写起 来简单 ,风格 也优 美,但是 执行 时 能得 到正 确 的 结 果 吗 ?回 答 是肯 定的。 这是 由 C系 统保证 的。C系统 在执行 函数调 用 时,按 图 8.22所 示 步骤 进行 (凡是允 许递 归 的程 序设计 语言在 执 行 函数 调 用 时都 按 这 个 步骤 进 行 )。其 中,关 键 是 “开 辟 新 的 运 行 环 境”和 “释放本函 数的运 行环 境”两 步。所 谓运 行环 境,就 是本 函数 运行 时所 需 要的 存储 空 间(形 式参数单 元、局部 变量单元 、临时 变 量 单 元等 )以 及为 了保 存 (调 用 本 函数 之 前 )程 序 运行 的现场 所需要 的空间 等。为 了简洁 ,目前 仅涉及 : (1)本 函数值 单元; (2)返 回地址 单元; · 228· 第八章 再 论 函 数 (3)本 函数的 形式参 数单元 区; (4)本 函数的 局部变 量单元 区; (5)本 函数的 临时变 量单元 区; 由于每 次调 用 函数 时都 在新的 运行 环境下 运行 ,保 证 了存 储单元 不发生 冲突,从 而 也就 保证 了递 归 程 序 执行 的 正确 性。为 了加深 对递 归程 序 的理 解,再举 一 个 递 归程 序 的例 题,并观察 它的执 行过程。 例 8.20  从前 n个 自然数 1,2,3,…,n中 取 r个数 作 组合 ,打 印全部 所有 组合。 例如,n=5、r=3,便有 图 8.23 所示 的 10种组 合。 解本 题的一般 想法是 使用 r重循环   for(1 =1;i1 <=n-r+1;i1 ++ ) for 2 =i1 +1;i2 <=n-r+2;i2 ++ ) ⁝ for(ir =ir-1 +1;ir<= n;ir++) 印 (i1,…,ir)   图 8.22 递归程序执行过程 但是,由 于 r是 可 变 的 ,所 以循 环嵌 套层 数 亦 是 可变 的,因 此 不 知道 应该用 多少个 i。 只好从 另一角 度来想 问题,自然想 到递归 。 从前 n个自然 数   1,2,3,…,n 中取 r个 数作组 合的问 题可以 分解成 :先分 别取这 n个数 中的一 个数 i(只需 要在前 n-r+1个 数中取 i);然后 从数 i后边 的 n-i+1个 数 图 8.23 10种组合     i+1,i+2,… ,n 中取 r-1个数作组 合;最后把 每组 r-1个 数 的 组 合与 前 边的 i合 并,从 而构 成 r个 数的 组 合。 当然,从 i后取 r-1个 数作组 合的问 题还可以 用同样 的方法 来分解 。 设想,若有 一个函 数   combination(s,j) 能够完成从自然数子序列     s,s+1,s+2,… ,n 中取 j个 数作组合 ,则原始问 题可以 写成函 数调用 :   combination(1,r) 而第一步分析算法则可以写成:   fori 1TOn-r+1DO combination(i+1,r-1) 把该 算法抽 象化,并考 虑递归出 口 (当 j=1时,就不 必再 继 续递 归下 去,只需 打 印已 经形 成 8.4  递   归 · 229· 的组 合),得到函 数:   void ombination(ints,intj){ inti; for(i= ;i<=n-j+1;i++ ) if(j> ) combination(i+1,j-1) else打印每层递归进入本函数时的 i } 主程 序中说 明变量 n、r;输入 n、r后,以 语句 combination(1,r)调用 上述 函数,就是 分析 算 法,也就 得到前 边的 r重循 环程序 。 上述程 序无法 在第 r层递 归调用 时访问(打 印)前边 诸层调 用时的 局部 量 i。显 然 ,每 层 的 i必须 满足: (1)或 者在本 层调用 时打印 ; (2)或 者用一 个全局 数组变 量,每 层保存 它,最后打 印。 使用每层 递归调 用时,打印 本层 i的方法,请 读者自 己完 成 相应 程序。用 全 局 数组变 量 a保 存每层 的 i,最 后打印 ,解 该问题 的完整程 序如下 :    /* ROGRAM printcombination*/ #include"stdio.h" * 1*/ #definenn10 /* 2*/ intn,r; /* 3*/ inta[nn],k; /* 4*/ void ombination(ints,intj){ /* 5*/ inti;      /* △ */ /* 6*/ for(i= ;i<=n-j+1;i++){ /* 7*/ a[r-j]=i;   /* r-j不能用 s*/   /* ◇ */ /* 8*/ if(j>1 /* 9*/ combinatien(i+1,j-1);   /* ☆*/ /* 10*/ else{ * 11*/ for(k 0;k<r;k++) /* 12*/ printf("%6d",a[k]); /* 13*/ printf("\n"); /* 14*/ } /* 15*/ /* ¤ */ /* 16*/ } /* 17*/ /* $ */ /* 18*/ } /* 19*/ intm in(void){   /* ⊙ */ /* 20*/ printf("Entern,r:"); /* 21*/ scanf("%d%d",&n,&r);   /* ◎ */ /* 22*/ · 230· 第八章 再 论 函 数 combination(1,r);   /* ※ */ } /* 23*/ /* 24*/ 下面观察 该程序 的执行 过程以 及在该 程序执 行过程 中计 算 机内 存的变化 过程 。希望 读 者能 认真体 会,并从中 加深对递 归程序 的理解 。为了 叙述方 便,在程序 中夹入 若干标 志。 (1)程 序开始 执行,到第 20行 ⊙处。 如图 8.24所 示。 (2)执 行到第 22行 ◎ 处 ,输 入 5、3。如 图 8.25所 示。 (3)执 行第 23行 函数调用 ,为 函数 combination开 辟新的 运行环 境,进行参数 结合   s=1——— s=1; j=r——— j=3 进入函 数后,开始 执行前,到 第 6行 △ 处。如 图 8.26所 示。 (4)进 入第 7行循环 语句,执行 i=s和第 8行 a[r-j]=i,即 a[3-3]=1,到 ◇处。 如 图 8.27所示。 图 8.24 步骤(1) 图 8.25 步骤(2) 图 8.26 步骤(3) 图 8.27 步骤(4)    (5)执 行第 10行 函数调用 ,为 函数 combination开辟新 的运行 环境,进行 参数结 合   s=i+1——— s=2; j=j-1——— j=2 进入函 数后,开始 执行前,到 第 6行 △ 处。如 图 8.28所 示。 (6)进 入第 7行循环 语句,执行 i=s和第 8行 a[r-j]=i,即 a[3-2]=2,到 ◇处。 如 图 8.29所示。 (7)执 行第 10行 函数调用 ,为 函数 combination开辟新 的运行 环境,进行 参数结 合   s=i+1——— s=3; j=j-1——— j=1 进入函 数后,开始 执行前,到 第 6行△ 处。如 图 8.30所示 。 (8)进 入第 7行循环 语句,执行 i=s和第 8行 a[r-j]=i,即 a[3-1]=3,到 ◇处。 如 图 8.31所示。 (9)第 9行判 断 j>1为 假,执行第 12~14行打 印数组 a,打印出 :   123 然后执 行到第 16行 ¤ 处 : 8.4  递   归 · 231· 先执行 第 7行的 i++ ——— i=4; 再进行 第 7行的 循环判 断,计算“i<=n-j+1”即“4<=5”,为 true。 继续循 环。执 行第 8行 a[r-j]=i,即 a[3-1]=4,到 ◇处。 如图 8.32所 示。 图 8.28 步骤(5) 图 8.29 步骤(6) 图 8.30 步骤(7) 图 8.31 步骤(8)    (10)第 13行判 断 j>1为假,执行 第 12~14行 打印数 组 a,打印 出:   124 然后执 行到第 16行 ¤ 处 : 先执行 第 7行的 i++ ——— i=5; 再进行 第 7行的 循环判 断,计算“i<=n-j+1”即“5<=5”,为 true。 继续循 环。执 行第 8行 a[r-j]=i,即 a[3-1]=5,到 ◇处。 如图 8.33所 示。 (11)第 13行判断 j>1为 假,执行第 12~14行打 印数组 a,打印出 :   125 然后执 行到第 16行 ¤ 处 : 先执行 第 7行的 i++ ——— i=6; 再进行 第 7行的 循环判 断,计算“i<=n-j+1”即“6<=5”,为 false。 退出循环 。执行 到第 18行 $ 处 ,退 出函数并 释放运 行环境 。返回 到第 10行 ☆处。 如 图 8.34所示。 (12)在第 10行 跳过 else部 分,退 出 IF语句,到第 16行¤处 : 先执行 第 7行的 i++ ——— i=3; 再进行 第 7行的 循环判 断,计算“i<=n-j+1”即“3<=4”,为 true。 继续循 环。执 行第 8行 a[r-j]=i,即 a[3-2]=3,到 ◇处。 如图 8.35所 示。 · 232· 第八章 再 论 函 数 图 8.32 步骤(9) 图 8.33 步骤(10) 图 8.34 步骤(11) 图 8.35 步骤(12)    (13)执行 第 10行函数 调用,为函 数 combination开辟新的 运行环 境,进行参 数结合   s=i+1——— s=4; j=j-1——— j=1 进入函 数后,开始 执行前,到 第 6行 △ 处。如 图 8.36所 示。 (14)进入 第 7行循 环语句 ,执 行 i=s和第 8行 a[r-j]=i,即 a[3-1]=4,到◇处 。如 图 8.37所示。 (15)第 9行判断 j>1为假 ,执 行第 12~14行打 印数组 a,打 印出:   134 然后执 行到第 16行¤处: 先执行 第 7行的 i++ ——— i=5; 再进行 第 7行的 循环判 断,计算“i<=n-j+1”即“5<=5”,为 true。 继续循 环。执 行第 8行 a[r-j]=i,即 a[3-1]=5,到 ◇处。 如图 8.38所 示。 (16)第 9行判断 j>1为假 ,执 行第 12~14行打 印数组 a,打 印出:   135 然后执 行到第 16行¤处: 先执行 第 7行的 i++ ——— i=6; 再进行 第 7行的 循环判 断,计算“i<=n-j+1”即“6<=5”,为 false。 退出循环 。执行 到第 18行 $ 处 ,退 出函数并 释放运 行环境 。返回 到第 10行 ☆处。 如 图 8.39所示。 (17)在第 10行 跳过 else部 分,退 出 IF语句,到第 16行¤处 : 先执行 第 7行的 i++ ——— i=4; 再进行 第 7行的 循环判 断,计算“i<=n-j+1”即“4<=4”,为 true。 8.4  递   归 · 233· 继续循 环。执 行第 8行 a[r-j]=i,即 a[3-2]=4,到 ◇处。 如图 8.40所 示。 图 8.36 步骤(13) 图 8.37 步骤(14) 图 8.38 步骤(15) 图 8.39 步骤(16)    (18)执行 第 10行函数 调用,为函 数 combination开辟新的 运行环 境,进行参 数结合   s=i+1——— s=5; j=j-1——— j=1 进入函 数后,开始 执行前,到 第 6行 △ 处。如 图 8.41所 示。 (19)进入 第 7行循 环语句 ,执 行 i=s和第 8行 a[r-j]=i,即 a[3-1]=5,到◇处 。如 图 8.42所示。 (20)第 9行判断 j>1为假 ,执 行第 12~14行打 印数组 a,打 印出:   145 然后执 行到第 16行 ¤ 处 : 先执行 第 7行的 i++ ——— i=6; 再进行 第 7行的 循环判 断,计算“i<=n-j+1”即“6<=5”,为 false。 退出循环 。执行 到第 18行 $ 处 ,退 出函数并 释放运 行环境 。返回 到第 10行 ☆处。 如 图 8.43所示。 (21)在第 10行 跳过 else部 分,退 出 IF语句,到第 16行¤处 : 先执行 第 7行的 i++ ——— i=5; 再进行 第 7行的 循环判 断,计算“i<=n-j+1”即“5<=4”,为 false。 退出循环 。执行 到第 18行 $ 处 ,退 出函数并 释放运 行环境 。返回 到第 10行 ☆处。 如 图 8.44所示。 (22)在第 10行 跳过 else部 分,退 出 IF语句,到第 16行¤处 : 先执行 第 7行的 i++ ——— i=2; 再进行 第 7行的 循环判 断,计算“i<=n-j+1”即“2<=3”,为 true。 · 234· 第八章 再 论 函 数 继续循 环。执 行第 8行 a[r-j]=i,即 a[3-3]=2,到◇处 。如图 8.45所示。 图 8.40 步骤(17) 图 8.41 步骤(18) 图 8.42 步骤(19) 图 8.43 步骤(20)    (23)执行 第 10行函数 调用,为函 数 combination开辟新的 运行环 境,进行参 数结合   s=i+1——— s=3; j=j-1——— j=2 进入函 数后,开始 执行前,到 第 6行 △ 处。如 图 8.46所 示。 (24)进入 第 7行 循环语句 ,执 行 i=s和 第 8行 a[r-j]=i,a[3-2]=3,到◇ 处。如 图 8.47所 示。 图 8.44 步骤(21) 图 8.45 步骤(22) 图 8.46 步骤(23) 图 8.47 步骤(24)    (25)执行 第 10行函数 调用,为函 数 combination开辟新的 运行环 境,进行参 数结合   s=i+1——— s=4; j=j-1——— j=1 进入函 数后,开始 执行前,到 第 6行△ 处。如 图 8.48所示 。 (26)进入 第 7行循 环语句 ,执 行 i=s和第 8行 a[r-j]=i,即 a[3-1]=4,到◇处 。如 8.4  递   归 · 235· 图 8.49所示。 (27)第 13行判 断 j>1为假,执行 第 12~14行 打印数 组 a,打印 出:   234 然后执 行到第 16行¤处,判断 第 7行 循环, 先执行 第 7行的 i++ ——— i=5; 再进行 第 7行的 循环判 断,计算“i<=n-j+1”即“5<=5”,为 true。 继续循 环。执 行第 8行 a[r-j]=i,即 a[3-1]=5,到 ◇处。 如图 8.50所 示。 (28)第 13行判 断 j>1为假,执行 第 12~14行 打印数 组 a,打印 出:   235 然后执 行到第 16行¤处,判断 第 7行 循环, 先执行 第 7行的 i++ ——— i=6; 再进行 第 7行的 循环判 断,计算“i<=n-j+1”即“6<=5”,为 false。 退出循 环。执 行到第 18行 $处,退 出 函数 并释 放 运 行 环境 。 返 回 到 第 10行 ☆处。 如 图 8.51所示。 图 8.48 步骤(25) 图 8.49 步骤(26) 图 8.50 步骤(27) 图 8.51 步骤(28)    (29)在第 10行 跳过 else部 分,退 出 IF语句,到第 16行¤处 : 先执行 第 7行的 i++ ——— i=4; 再进行 第 7行的 循环判 断,计算“i<=n-j+1”即“4<=4”,为 true。 继续循 环。执 行第 8行 a[r-j]=i,即 a[3-2]=4,到◇处 。如图 8.52所示。 (30)执行 第 10行函数 调用,为函 数 combination开辟新的 运行环 境,进行参 数结合   s=i+1——— s=5; j=j-1——— j=1 进入函 数后,开始 执行前,到 第 6行 △ 处。如 图 8.53所 示。 (31)进入 第 7行循 环语句 ,执 行 i=s和第 8行 a[r-j]=i,即 a[3-1]=5,到◇处 。如 图 8.54所示。 (32)第 9行判断 j>1为假 ,执 行第 12~14行打 印数组 a,打印出 : · 236· 第八章 再 论 函 数   245 然后执 行到第 16行¤处,判断 第 7行 循环, 先执行 第 7行的 i++ ——— i=6; 再进行 第 7行的 循环判 断,计算“i<=n-j+1”即“6<=5”,为 false。 退出循 环。执 行到第 18行 $处,退 出 函数 并释 放 运 行 环境 。 返 回 到 第 10行 ☆处。 如 图 8.55所示。 图 8.52 步骤(29) 图 8.53 步骤(30) 图 8.54 步骤(31) 图 8.55 步骤(32)    (33)在第 10行 跳过 else部 分,退 出 IF语句,到第 16行 ¤ 处 : 先执行 第 7行的 i++ ——— i=5; 再进行 第 7行的 循环判 断,计算“i<=n-j+1”即“5<=4”,为 false。 退出循 环。执 行到第 18行 $处,退 出 函数 并释 放 运 行 环境 。 返 回 到 第 10行 ☆处。 如 图 8.56所示。 (34)在第 10行 跳过 else部 分,退 出 IF语句,到第 16行 ¤ 处 : 先执行 第 7行的 i++ ——— i=3; 再进行 第 7行的 循环判 断,计算“i<=n-j+1”即“3<=3”,为 true。 继续循 环。执 行第 8行 a[r-j]=i,即 a[3-3]=3,到◇处 。如图 8.57所示。 (35)执行 第 10行函数 调用,为函 数 combination开辟新的 运行环 境,进行参 数结合   s=i+1——— s=4; j=j-1——— j=2 进入函 数后,开始 执行前,到 第 6行△ 处。如 图 8.58所示 。 (36)进入 第 7行循 环语句 ,执 行 i=s和第 8行 a[r-j]=i,即 a[3-2]=4,到◇处 。如 图 8.59所示。 (37)执行 第 10行函数 调用,为函 数 combination开辟新的 运行环 境,进行参 数结合   s=i+1——— s=5; j=j-1——— j=1 进入函 数后,开始 执行前,到 第 6行△ 处。如 图 8.60所示 。 8.4  递   归 · 237· 图 8.56 步骤(33) 图 8.57 步骤(34) 图 8.58 步骤(35) 图 8.59 步骤(36)    (38)进入 第 7行循 环语句 ,执 行 i=s和第 8行 a[r-j]=i,即 a[3-1]=5,到◇处 。如 图 8.61所示。 (39)第 9行判断 j>1为假 ,执 行第 12~14行打 印数组 a,打 印出:   345 然后执 行到第 16行¤处,判断 第 7行 循环, 先执行 第 7行的 i++ ——— i=6; 再进行 第 7行的 循环判 断,计算“i<=n-j+1”即“6<=5”,为 false。 退出循 环。执 行到第 18行 $处,退 出 函数 并释 放 运 行 环境 。 返 回 到 第 10行 ☆处。 如 图 8.62所示。 (40)在第 10行 跳过 else部 分,退 出 IF语句,到第 16行¤处 : 先执行 第 7行的 i++ ——— i=5; 再进行 第 7行的 循环判 断,计算“i<=n-j+1”即“5<=4”,为 false。 退出循 环。执 行到第 18行 $处,退 出 函数 并释 放 运 行 环境 。 返 回 到 第 10行 ☆处。 如 图 8.63所示。 图 8.60 步骤(37) 图 8.61 步骤(38) 图 8.62 步骤(39) 图 8.63 步骤(40) · 238· 第八章 再 论 函 数    (41)在第 10行 跳过 else部 分,退 出 IF语句,到第 16行¤处 : 先执行 第 7行的 i++ ——— i=4; 再进行 第 7行的 循环判 断,计算“i<=n-j+1”即“4<=3”,为 false。如图 8.64所示 。 退出循 环。执 行到第 18行 $ 处 ,退 出函数并 释放运 行环境 。返回 到第 23行※ 处。 (42)程序 执行结 束,返回操 作系统 。如图 8.65所示。 图 8.64 步骤(41) 图 8.65 步骤(42) 本章小结 本章再 论函数 ,分 三部分。 第一部 分讲述 C参数 、指针作 参数、数组 作参 数以及 其他 程 序设 计语言 的变量 参数、换 名参 数。第 二 部 分讲 述 作 用 域。 第三 部 分 讲 述 递归 程 序 设计。 本章 讲述内 容除其 他程序 设计语 言参数 外,都 十分重 要。读 者一定 要掌握 好本章的 内容。 习 题 八 8.1 确定 下述程 序的输出 结果。   #include"stdio.h" inta,b,c; voidp(itx,inty,int* z){ *z=x+ y + *z; printf("%d% d% d\n",x,y,*z); }; int ain(void){ a=5;b=8;c=3; p(a,b,&c); p(7,a+b+c,&a); p(a*b,a/b,&c); } 8.2 如下函数 f是否有意义?若有意义,说明它的功能;若无意义,说明理由。    1)intf(har*s){    2)voidf(car*s,char*t){ 习 题 八 · 239· char* p=s; while(* p)p++; returnp-s; }   8.3 如下函数 fun完成什么功能。 while(*t++ =* s++); }    oidfun int*c,intm,intn){ intu=m,v=n,t; while(u v){ t=* (c+u); *(c+u)= *(c+v); * (c+v)=t; u++;v--; } } 8.4 使用命令“名 uvwxyzmnopqrsabcdeffg”执行下述程序后,输出结果是什么?    include<string.h> #include <stdio.h> main(int rgc,char*argv[]){ char** p; sort(argv+1,argc-1); for(p=arv+1;--argc;p++) printf("% s",* p); } voidsor(char* w[],intn){ inti,j; charword[20]; for( =0;i<n;i++) for( =i+1;j<n;j++) if(strcmp w[i],w[j])>0){ strcpy(word,w[i]); strcpy(w[i],w[j]); strcpy(w[j],word); } } 8.5 设有 int类型变量 x、y和如下三种 swap函数。它们能否实 现 x、y值的交换?为什么?不改变各 个函数中三个表达式语句,能否对程序进行修改,使之能够实现 x、y值的交换? (1) oidsap(int*xx,int*yy){   * 主程序中用 swap(&x,&y)调用 */ int*temp; * temp=* xx; * xx=* yy; yy=*temp; } · 240· 第八章 再 论 函 数 (2) oidswap int*xx,int*yy){ /* 主程序中用 swap(&x,&y)调用 */ int* temp; temp=xx; xx=yy; yy=temp; } (3) oidswap intxx,intyy){ inttemp; temp=xx; /* 主程序中用 swap(x,y)调用 */ xx=yy; yy=temp; } 8.6  阅 读下 列 函 数 ,指 出 它 们 的 功能 。 (1)flat(floata[],intn,floatx){ if(n== ) returna[0]; elsereturnp(a,n-1,x* x+a[n]); } (2)flot (inta[],intn){ if( ==1) returna[1]; elseif(m(a n-1)>a[n]) returnm(a,n-1); elsereturna[n]; } (3) oidfun intn,int*s){ intf1,f2; if(n==1||n==2)* s=1; else{  fun(n-1,&f1); fun(n-2,&f2); * s=f1+f2; } } 8.7  在 如下 几 种 情 况下 ,实 在 参 数应 该 是 怎 样 的 形式 ? (1)一 般 非 指 针 类 型 变量作 一 般 非 指针 类 型 形 式 参 数的 实 在 参 数 ; (2)一 般 非 指 针 类 型 变量作 指 针 类 型形 式 参 数 的 实 在参 数 ; (3)指 针 变 量 作 指 针 类型形 式 参 数 的实 在 参 数 ; (4)指 针 变 量 作 一 般 非指针 类 型 形 式参 数 的 实 在 参 数; (5)一 般 非 指 针 类 型 形式参 数 变 量 作一 般 非 指 针 类 型形 式 参 数 的 实 在参 数 ; (6)一 般 非 指 针 类 型 形式参 数 变 量 作指 针 类 型 形 式 参数 的 实 在 参 数 ; (7)指 针 类 型 形 式 参 数变量 作 一 般 非指 针 类 型 形 式 参数 的 实 在 参 数 ; 习 题 八 · 241· (8)指 针 类 型 形 式 参 数变量 作 指 针 类型 形 式 参 数 的 实在 参 数 。 8.8 除了“值参数”、“变量参数”和“换名参数”以外,你还 能为函数设 计出其 他种类 的参数 吗?给 出 你设计的参数的特性。 8.9 函数的嵌套调用和递归调用有什么区别?各应该注 意什么 问题?在 C语言 中 main函 数可不 可 以递归调用? 8.10  编 写 函 数 ,求 给 定 字符 串 的 长 度 。 8.11 编写函数,比 较 两 个 给 定 字 符 串 s1、s2的 大 小。 当 s1>s2时,返 回 “1”;当 s1<s2时,返 回 “-1”;当 s1==s2时,返回“0”。 8.12  编 写 函 数 ,分 别 求 给定 字 符 串 中 大 写字 母 、小写字 母 、空 格 、数 字 和 其 他 符号的 数 目 。 8.13 编写函数,用指针作参数,实现把字符串 str1复制到字符串 str2。 8.14 编写函数,把给定字符串的从 m开始以后的字符复制到另一个指定的字符串中。 8.15 编写函数 str_delete(char*s,intv,intw),从字符串 s的第 v个字符开始删除 w个字符。 8.16 编写函数 insert(char*s1,char*s2,intv),在字符串 s1的第 v个字符处插入字符串 s2。 8.17 编写函数,用指针作参数,实现把字符串 str反向。 8.18  编 写 函 数 ,分 别 利 用指 针 传 递 参 数 ,实 现 两 个字符 串 变 量 的交 换 。 8.19 编写函数,用指向指针的指针实现对给定的 n个 整数按递 增顺序 输出,要求不 改变这 n个数 原 来的顺序。 8.20 编写函数,对给定的 n个整数进 行位置 调整。调整 方案是:后面 m个数 移到 最前 面,而 前面 的 n-m个 数 顺 序 向 后 串。 8.21 编写函数,使得只用该函数就能实 现对 不同 类型 数组 的排序。 例如,可 以对 int型 数组、double 型数组、float型数组或 char型数组进行排序。 8.22 编写显示动态菜单信息,并带回用户选择结果 的函数。用 户以指 针数组 形式给 出菜单 信息,并 作为函数参数。 8.23  编 写 函 数 ,使 得 仅 通过 此 函 数 ,便 可 计 算 两 个整数 的 加 、减 、乘 、除 四种 运 算 。 8.24 编写函数,用指针形式访问数组元素,把给定的 int类型矩阵转置。 8.25 编写函数,把给定的 int类型的 5*5矩阵中最大 元素放在 中间;按序分 别把四 个最小 元素放 在 四个 角 上,顺 序是 :左 上 、右 上 、右 下 、左 下 。 8.26  编 写 程 序 ,利 用 指 针参 数 ,实 现 对 读 入 的 某 个角度 计 算 它 的正 弦 、余弦 、正切。 8.27  编 写 一 个 函数 ,计 算 1, 当 n=0 m, C(m,n) = Cnm = Cmm-n, 当 n=1 当 n<2m Cn-1 m-1 +Cnm-1, 当 n≥ 2m    8.28  用 递 归 实 现第 五 章 习 题中斐 波 纳 契 序 列 问 题。该 序 列 可 以表 示 成 1, f(n) = 1, 当 n=1 当 n=2 f(n-1)+f(n-2), 当 n>2   8.29 分别用递归和递推编写出计算 Hermite多项式的函数。Hermite多项式定义为 · 242· 第八章 再 论 函 数 1, 当 n=0 Hn(x) = 2x, 当 n=1 2xHn-1(x)-2(n-1)Hn-2(x), 当 n>1   8.30 编写一个计算 Ackerman函数的递归函数。然后 加一个输出 函数参数 m 、n的输出 语句作为 函 数体的第一个语句,并写一个程序输入 m、n后调用该函数,对于 m=5,n=3的情况 执行该 程序,观察递 归 调用 层 次及 状 况 。 Ackerman函 数 定义 为 n+1, Ack(m,n) = Ack(m -1,1), 当 m =0 当 n=0 Ack(m -1,Ack(m,n-1)), 当 m >0且 n>0   8.31 编写函数,用递归方法求 n个元素数组 a的最大值。 8.32 设有数组声明 inta[100],试编写一个 递归 函数,求 a的反 序数 组并仍 保存 在 a中,即 a[1]与 a[100]交换,a[2]与 a[99]交换,a[3]与 a[98]交换,…,a[50]与 a[51]交换。 8.33  分 别 编 写 一个 函 数 ,实 现 正 整 数 的 乘、乘 幂 运算。 只 准 使 用运 算 符 “+”。 8.34 用递归实现求 m、n最大公约数的函数 gcd(m,n)。 8.35 用递归实现函数 digint(n,j),它回送整数 n的从右边开始的第 j位数字。例如:    igit(25367,4)= 5; digit(31833,6)= 0。 8.36 编写一个递归函数,计算下述序列 x0、x1、…、xn 的出现周期。   x0 =d xn+1 =(ax2n +bxn +c)% m 8.37 执行例 8.18的程序,给出表达式 4+3*(5+6+6)/7+8的计算过程。 8.38 例 8.18的程序计算表达式的顺序是从右向左。编写出从 左向右 计算表 达式的 程序,并且运 算 分量 允 许是 多 位 整 数 和 实 数;进 一步 ,在 计算 过 程 中 要检查 表 达 式 的 语 法 错误 。 8.39 编写程序验证卡布列克运算。该运算是指对任意各位数字不完全相同的四位数 n: (1)把四位数字从小到大排列,形成由原来四位数字组成的最小的四位数 n1; (2)把四位数字从大到小排列,形成由原来四位数字组成的最大的四位数 n2; (3)作 n2 -n1 重新得到四位数 n。 重复上述步骤,最后总能得到整数 6174。 8.40  利 用 递 归 方法 计 算 常 系数多 项 式 值 (提 示:参 考 第五 章整 数 翻 译算 法 )。 amxm +am -1xm-1 +am -2xm -2 +… … +a2x2 +a1x+a0 8.41 编写程序,计算下式,直到两次计算之差的绝对值小于 10-5。    y=arctgcosractgcos…ractancos(π/6) 8.42  编 写 一 个 递归 函 数 ,实 现 在 整 数 组 中进 行 顺 序 检 索 。 8.43  编 写 一 个 递归 函 数 ,实 现 在 整 数 组 中进 行 两 分 法 检 索。 8.44 编写一个递归函数,实现把有序整数组 A和 B合并成一个有序数组 C。 8.45 从功能上考虑,支持递归的程序设计语言可以不必再提供重复性语句。怎样用递归函数来 代替 C语言中的 while、do循环。 8.46 Lisp语言由 S表达式组成,S表达式定义如下: 习 题 八 · 243· (1)任意字母 S是 S表达式; (2)若 u、v分别都是 S表达式,则(u,v)也是 S表达式。 编写函数,判断给定的字符串 S是否为 S表达式。 8.47 不允许显式保存字符串 W,编写程序,判断以 S|为结束 符的输入字符 串是否 关于字 符 & 对称。 即输入字符串成如下形式:    W&MS| 其中子字符串 M 是子字符串 W 的反序。例如    bc&cbaS|  输出 yes 11&12S| 输出 no &S| 输出 yes 8.48  编 写 一 个 函数 ,判 断给 定 的 字 符 串 是否 符 合 语 法    L→ ab|aLb 8.49 编写一个递归函数判断字符串 X=x1x2…xm 是否在字符串 Y=y1y2…yn 中出现,其中 n≥m。 8.50 试写一个函数,计算由字符串参数形式给出的分子式的分子量。其中分子式由括号括 住,例如: (Na2SO4)、(Al2(SO4)3)。 8.51 使用每层递归调用时,打印本层 i的方法,编写出例题 8.20的“从前 n个自然数 1,2,3,…,n中 取 r个数作组合”的 程序。打 印 格 式是 每 个 数占 6个 格,每 行最 前 边 占 一 个格。 当 n=5、r=3时,对 应 图 8.66所示的每种组合打印出图 8.67所示的相应格式。 图 8.66 10种组合 图 8.67 打印结果 8.52 图 8.68所示是 int型 函数 F的 PAD,x、y为 形式 参数,写 出 F的 递归函数说明。 8.53  不 用 递 归 ,编 写 如 下函 数 。    1 =1 Fn =Fn-1(2x+1)* x 8.54 设有如下函数,求 f(1,10)的值;并 给出在 计算f(1,10)时函 数 f 的执 行 过程 。 该 函 数 能 否转 化 成 非 递 归 函数 。   图 8.68 整型函数 F · 244· 第八章 再 论 函 数    loat(floatx,floaty){ if(x> y) return(x+y)/2; else  returnf(f(x+2,y-1),f(x+1,y-2)); } 8.55 如下函数称 Macrarthy函数,分别以 105、95、90、85调 用该 函数,求函 数值。 并把它 化成 无递 归 形式。    ntm intx){ if(x> 00) returnx-10; else  returnm(m(x+11)); } 8.56 设有如下函数。求 f(-1,2)、f(0,0)、f(10,10)的值,并把该函数转化成递归函数。    ntf(ntx,inty){ intt; if((x< 0)||(y<=0)) return0; else   t=y; do{ =y+t; x=x-1; while(x>0); returny; } } 8.57  分 别 把 如 下函 数 转 化 成非递 归 函 数 。 (1) ntf intx,inty){ intt; if(x 0) returnx+y; elsereturnf(x-1,x+y)+x/y; } (2) oid (intx,inty){ if( <y) a=a-1; else   =a+1; s(x+y,y-x); } } 习 题 八 · 245· (3) oidq void){ if(y<0){    x=x-y; q(); y=y+1; } elsey=0; } 8.58 不用递归计算 f(n),f(n)定义如下:    (0)=0 f(1)=1 f(2n)=f(n) f(2n+1)=f(n)+f(2n-1) 8.59 编写一个计 算 Ackerman函 数 的 非 递 归 函 数,并 写 一 个 程 序,输 入 m、n后 调 用 该 函 数,对 于 m=5,n=3的 情 况 执 行该 程 序 。 8.60  设 有 程 序 如下 ,分 别说 明 它 们 的 执 行过 程 。 (1) include"stdio.h" intt; intq(ntr,int*s){ r=2* (* s); printf("% d%d% d\n",t,r,* s) returnr* (* s); } intman(void){ t=1; t=q(t,&t); printf("% d% d%d\n",t,q(t,&t),q(t,&t)); } (2) include"stdio.h" intt; intq(it* r,ints){ * r=2* s; printf("%d% d% d\n",t,*r,s); return(* r)* s; } intm in(void){ t=1; t=q(&t,t); printf("% d% d% d\n",t,q(&t,t),q(&t,q(&t,t))); } 第九章 程序开发和结构化程序设计 编写程 序并不 难,只要有解 决问 题的 算法,掌握 某种 程序设 计语 言,任何 人 都可 以编 出 程序 ,但 是不同 的人编 写出的程 序却 大不 相 同。 针 对 同一 个 问 题 ,有 人编 写的 程 序风 格好、 易读 、易 维护、易重用 、可靠性高 、运 行得既 快又节 省存储 空间;有人 编写的 程序 风格 差 、晦 涩 难懂 、难 于维护 、冗 长、正 确 性和 可 靠性 极 低 、运 行起 来 既 慢又 占 用 空 间 。 因此 编 写 程 序 容 易,编写 好程序 难,编写出 满足用 户需求 、各方 面均优 秀的程 序更难 。 要想编 写出一 个风格 优美、正确 可靠、各方 面均 优秀 的好程 序,必须 按照 现 代软 件工 程 的规 范进行 。程序 模块的 划分、算 法 的选 择与 设 计、编码 的风 格 、测试等 都应 该 在软 件工 程 规范 的约束 下,按照标 准的软件 开发 过 程 进 行。 同时 也必 须 遵循 好的 程 序 设 计 原则 和使 用 好的程序设计方法。 本章简 单介绍 程序开 发技术 、结 构化 程序 设 计原 则和 方 法。为 读者 能够 编 写出 好程 序 和优秀程序打下一个良好的基础。 在介绍 程序开 发技术 、结构 化程序 设计原 则和方 法之前 ,先 介绍两 种 C语 句。 9.1 goto和标 号 有讽刺 意味的 是,结构化程 序设计 原则限 制使用 goto语句,而本 书偏偏把 它 放 在这一 章 来讲 。目的 是提请 大家注 意,尽 量不要 使用它 。 goto语句 是强制 改变程 序正常 执 行 顺 序的 手 段 。但是 事先 声明,频 繁使 用 goto语句 不 是好 的程序 设计习 惯,不符合结 构化程 序设计 原则。 所以尽 管介绍 了 goto语句 和标 号 ,还 是 希望 读者在 编写程 序时除 有特殊 需要外 ,尽量 不要使 用 goto语句 和标号 。 9.1.1 带标 号的语句 带标号的语句形式是:     标号 :语 句 其中 标号(lable)是一个 标识符。 9.1.2  goto语 句 goto语句 (goto-statement)的 形式是 : 9.2 空   语   句 · 247·     goto标 号 其中: (1)标 号就是 带标号 语句中 用的标 号,是 一个标 识符; (2)goto是 保留字 ,表 示转向 。 goto语句的 意义是 中断正 常的程 序执行顺 序,转到 本函数 内 标 号标 出 的语 句处,并从 其 开始 继续向 下执行 。goto语句与带标号 语句配合使 用,可 达到改变程序正常执 行顺序的目的。 例 9.1 前述 第五章 例 5.8中 迭代法 求解方 程根的 图 9.1所示的 流程图,可以 用图 9.2 所示的程序片段表示。 图 9.1 迭代法求解方程 图 9.2 程序 当第 3行 的条件 为 true时,执行 第 4行的 goto语 句,则转 到标 号 r2标 出 的 第 7行去 执 行,从而 结束迭 代过程 。 当程序执 行到第 6行的 goto语句后 ,则 无条件 强制控 制转到 标 号 r1标出 的第 2行去 执 行,继续 进行迭 代。 虽然 C语 言 允许使 用 goto语句转 向本函 数内 任何语 句,但是下 述 转向 是极其 不好 的程 序设 计习惯 。这类 goto语句使程 序逻辑 混乱,同时 也给编 译器优 化程序 带来麻 烦。 (1)从 if语 句外转 入 if语 句的“then”或“else”子 句之中; (2)在 if语 句的“then”或“else”子句 之间转 向; (3)从 switch语 句之外 转入 switch语 句之内 ; (4)从 循环语 句之外 转入循 环语句 之内; (5)从 复合语 句之外 转入复 合语句 之内。 9.2  空  语  句 空语句 (empty-statement)只由一 个分号 “;”组 成,表记没有 任何操 作动作 。其语 法是:    <空语句 > → ; 空语句 也是一 种语句 ,只不 过 在 程序 正文 上 看,它只 有一 个 分 号 。在 语义 上 看,它没 有 · 248· 第九章 程序开发和结构化程序设计 任何 操作动作 ,其作用 只是占据 一个语 句的位 置。作 为一个 语句,空语 句的地 位 和 其他语 句 一样 ,任 何可以 放置语 句的地方 都可以 写空语 句。空 语句有 时起很 大作用 ,例 如:   for(… { ⁝ gotor0; ⁝    r0:;           ← 当 转 向 循 环 末 尾时 ,如 果没 有 空 语 句,就 不 好 解决 }; 9.3  结构 化程 序设 计原 则 由于计算 机硬件 技术 的 不断 发 展,以 及 计算 机 的广 泛 应 用,计算 机 软 件系 统 也 日 益 发 展,并且 软件在 计算机 系统中所 占比重 也越来 越大,使得 作为软 件主要 组成部 分 的 程序系 统 越来 越庞大,复 杂度越 来越高,造价 也越来 越昂贵 ,同 时出错 率也不 断增加 ,系 统 的 可靠性 越 来越 难以保证 ,维护也 越来越困 难。比 如,当年 著名的 IBM 360操 作 系 统 出错 率是 3‰。 经 过几 年运行 后,征集了 广大用户 的意 见,集中 进 行 一 次 修 改,修 改完 成后,最 后 测试 出错 率 仍是 3‰ 。此类例 子很多 ,随 着情况 的不断发 展,最后 终于 在 20世纪 60年代 引起了 一场 所 谓的 软件危 机。在 该背景 下,1968年 Dijkstra提 出 了结 构化 程序 设计 思 想。 这 种思 想的 基 点是 :“清 晰、易 懂地书 写程序 逻辑,使程 序结 构表 现 得简 单、明快 ”。 从这 点出发 ,人 们经 过 艰苦 实践,总结 出了一 套结构化 程序 设 计 原 则。 这套 原则 要 求程 序员 写 出 的 程 序应 该是 结 构良 好的,即: (1)易 于保证 和验证 程序的 正确性 ; (2)易 于阅读 、维 护和调试 。 这种良 好结构 的程序 具体体 现在,对任意 程序段 来讲: (1)仅 有一个 入口,一个出 口; (2)没 有死循 环; (3)没 有死码 区。 为了达 到上述 目的,强调程 序员在 编写程 序时应 该注意 如下几 点: (1)利 用自顶 向下、逐步求 精的技 术设计 程序; (2)具 有良好 的程序 设计风 格; (3)尽 量利用 标准的 顺序、分支、重 复控制结 构。保 证程序 仅有一 个入口 、一个 出口; (4)限 制使用 goto语句 。可能 一个坏 程序的 缺点都 是由 goto语句引 起的。 结构化程 序设计 的发展 ,使 程序设 计从技 艺走向 工程,为软 件工程 学的发 展 奠 定了有 力 的基础。使软件生产由个体作坊式的艺术创作方式发展成为千千万万人参加的 工程 方式, 9.4 程 序 风 格 · 249· 达到 了“系列化 、产 品化 、工 程 化 、标准 化 ”。“软 件 工 程 ”也 从 这 一 时 期 开 始 逐 步 发 展 起 来了。 能够反 映结构 化程序 设计要 求,便于书 写结 构化 程序的 程序 设计语 言称 为 结构 化程 序 设计 语言。 可以认 为 C语言是结 构化程 序设计 语言。 结构化 程序设 计方法 是 20世纪 60年 代末 70年代 初逐 渐发展 起来的 ,目 前 程 序设计 领 域的 热点是 “面 向对象 程序设 计方 法”和 “基于 构 件 的 程 序设 计方 法”等 。从 程 序编 码角 度 看,它们 的主要 特长在 于程序的 组织、信息 封装、软件 重用等 ,而 最终对 于足够 小 的 程序模 块 的编 码,它们没 有给人 们带来益 处。 结构 化程 序 设计 方法 针 对面 向对 象 程 序 设 计中 每个 小 模块 的设计起 着十分 关键的 作用。 所以尽 管目前 面向对 象程序 设计技 术和基 于 构 件的程 序 开发 技术受 到人们 广泛关 注与重 视,但 是 结构 化 程 序 设计 技 术 仍 然 是十 分 重 要 和不 可缺 少 的。 可以说 结构化 程序设 计是一 切程 序 设 计 技术 的 基 础 ,是 任何 软件 工 作 者 必 须掌 握的 技 术。 本书的 目标是 讲授程 序设计 基 础,主 要介 绍 结 构 化程 序 设 计 技 术。 至于 面 向对 象程 序 设计 技术和 基于构 件的程 序开发 技术,读者可 以进一 步参阅 有关资 料和书 籍。 9.4  程 序 风 格 程序风格 没有严 格定义 。一般 提到程 序风格 是指程 序的 书 写格 式等与易 读性 、清 晰性、 互相 交流有 关的,而与 程序执行 无关或 关系不 大的一 些的问 题。 编写程 序不仅 仅是为 了完成 某些 预 定的 任务 而 与计 算机 进 行 交 流,通知 计 算机 按某 一 个规 定好的步 骤或算 法做某 些具体 工作,而且 也是为 了与人 进行交 流,进一步 还 为 了给自 己 或别 人阅读。 同时,在 程序的编 制和调 试以及 程序交 付使用 后的维 护过程 中,程 序 员自己 也 需要 不断地 查阅自 己编出 的程序 ,以 便 做 出 必要 的修 改 和补 充。 更何 况 程 序 的 维护 不一 定 由编 程者自 己进行 ,很 可能由别 人来 做。 所以,在 编 写程 序时 就 要 考 虑到:该 程 序既 是为 了 要在 计算机 上运行 ,也 是 为 了今 后 的交 流 和 阅 读 ,同 时 还 是为 了 留 下 有 用的 参 考文 件。 为 此,程序 必须是 宜于阅 读的,也就是 必须是 结构良 好或风 格优 美 的。程 序设计 风 格 不好不 利 于产 生正确 、高 效、易读、易维护 的 程 序。风 格不 好的 程序会 使整 个程序 的维 护 费用 与时 间 明显 增加,甚至 导致整 个编程过 程失败 。所以 ,程 序设计 风格 是 程序 员必备的 修养 。良好 的 程序 设计风格 是程序 员在长 期的编 程实践 中逐步 发展、积累 和提 炼 出来 的。它 是产 生 正确、 高效 、易 读、易维护 程序的 一种重 要手段 。 如何书写 计算机 程序,以及 关于程 序风格 没有一 个确定 的统 一 标准。 很多 组织,尤其 是 西方 一些大 的计算 机公司 都建 立他 们 自己 的 一 些 标 准,并 要 求他 们 的 程 序 员遵 守 和 使用。 同时 也有不 少个人 发表他 们自己 的标准 ,希 望别 人效 仿。 如 果程 序员 能 养 成 良 好的 程序 设 计风 格,大家按 统一的 标准进行 程 序 设 计 和 书写 程 序 ,则 有 助于 彼 此 交 流 ,有 助 于别 人理 解 · 250· 第九章 程序开发和结构化程序设计 他们 所编写 的程序 。本节 对程序 设计 风 格 也 提出 一 个 建 议,希望 能给 读 者 一 个 有益 的提 示 和帮 助。程序 的风格 主要涉 及程序 的行文 格式、注释 和空白 的合适 用法、尽量 使 用 合适的 助 记名 来命名 标识符 、明 白地表示 出程序 结构和 数据结 构等。 9.4.1 良好 的行文格式 程序的 行文格 式不好 直接影 响程 序 的 可 读性、清 晰性 和 外观。 第二 章曾 经 给出 一个 这 方面 的例子,计 算 25和 38之和 。那个 程序写成 如下 A和 B的格 式显然 都没有 写 成 C的 格 式好 ,而 且 A和 B的格 式还相 当坏,因为 它们十分 不好读 。    /* A*/    includestdio.h         inti;main(){i=25+38;printf("25+38=% d",i);} /* B*/ #includestdio.h inti;main()                  {          i=25+38;                printf( "25+38=% d", i                                           );} /* C*/ #includestdio.h inti; int ain(void){ i=25+38; printf("25+38=% d",i); } /* 声明整型变量 i*/ /* 主函数 */ /* 求和运算 */ /* 打印 */ 第三种 写法的 C程 序十分好 读,因 为 它层 次分 明 ,格式 清 晰,意义 明确。 这只是 一个 很 小的 例子,程序 越大越 显示出良 好的 行 文 格 式的 重要 性。 很 难想 像如 何 去 读 一 个几 千行 或 上万 行的像 A或 B那 样格式 混乱、杂乱无 章的程 序。要 产生具 有良好 行文格式 的程序 ,主要 是合 理地利用 空格、换 行。使程 序的各 个函数 之间、各类 说明之 间以及 每个函 数 内 的各个 功 能模 块之间 分成明 显的段 落;并 按照程 序的嵌 套层次 使程序 呈现出 锯齿形 的缩头格 式。 下面给 出“适当使 用空行 、空 格”一般 方法的 建议: (1)在 #编 译预处 理命令 、typedef类型 定义、变 量声 明 之 前 加一 个空 行,并 把这 些部 分 分别集中写在一起。 (2)在 函数定 义之前 加两个 或更多 的空行。 (3)每 个常量 定义占 一行。 9.4 程 序 风 格 · 251· (4)每 个类型 声明均 另起一 新行。 (5)每 个变量 声明均 另起 一 新 行。 并 注意 一 个 变 量 声明 中 的每 个 变 量说 明 符 之 间 的 排列。 (6)在 结构和 联合声 明中,各成员 声明向 右缩进 几列。 (7)复 合语 句的 左 花括 号“{”放 在函数 定义 说明 符 if、else、switch、while、for、do、struct、 union的起 始行末 尾;而右花 括号“}”与 相应语 句或声 明引导词 的第一 个字符 对齐,并以 注释 标明 ,例 如“/* if*/”;各 成分语 句或声 明向右 缩进几 列并对齐 。 (8)函 数定义 中,函数 定 义 说 明 符 另 起 一行,函 数 体 部分 各 个语 句 和 声明 向 右 缩 进 几 列,如图 9.3所 示的格 式。 (9)IF语句 把 if、else对 齐,并且内 含的子 语句向 右缩进 几列,如图 9.4所示 的格式 。 (10)SWITCH 语 句 内 部各 个 case标 号 分别 占一 行,“:”互 相对 齐,并比 switch缩 进 几 列;“:”后的语句 开始符 对齐,如图 9.5所 示的格 式。 图 9.3 函数定义 图 9.4 IF语句 图 9.5 SWITCH语句 (11)WHILE语句 把 while、条 件 b写在一行 上,而 把 循环 体 S另 起一 行 并相 对于 while  缩 进 几列,如图 9.6所 示的格式 。 (12)FOR语 句把循 环描述部 分的 for表 达式写 在一行 上,而把循 环体 S另 起一行 ,并相 对于 for缩进 几列,如图 9.7所示 的格式 。 (13)DO语 句把 do和 while对 齐,循 环 体中 的 各个 成 分 语 句 向 右 缩 进 几列 并 对 齐,如 图 9.8所示的 格式。 图 9.6 WHILE语句 图 9.7 FOR语句 图 9.8 DO语句 另外,在运 算符的 两边、赋值 运算符 “=”的 两 边,以 及在 注释 的 “/* ”之 后 和 “*/”之 前  可 以 各加一 个空格 。 以上只是 建议,当 然还有其 他方法 。读者 也可以 创造自 己的一 套方法 ,世 界 上 各大软 件 公司 都有自 己的一 套标准 。总之 ,要以 提高程 序的清 晰性、可读 性为目 的。 · 252· 第九章 程序开发和结构化程序设计 9.4.2 用合 适的助记名 来命名标 识符 标识符 是程序 员给自 己引进 的 常量 、类型、变 量 、函数等 起的 名字。 程序 设 计语 言对 如 何命 名标识符 没有限 制,标识符 也没有 固定的 含义。 但是从 使用的 角度看 ,标 识 符 表记的 每 个对 象都有 具体的 含义。 为了提 高可 读 性 和 有助 于 记 忆 ,应 该使 标识 符 在 拼 写 上尽 量和 它 所标 记对象 的物理 、数 学 等 含义 相 一致 ,并且 要 避免 与 系 统预 定 义 的 标 准标 识 符重 名。 例 如,表示 圆周率 π用 pai就比用 一般 的 a要好 ;表示 面积 用 area就 比用 s要好;表示 长度 用 length就比 用 l要 好;表示某 一角度 a的正弦 值用 sinofa就比用 sin要好 ,等等。 9.4.3  注 释 注释是 间隔符 的一种 ,在程 序中 的作 用相 当 于一 个空 格 。注释 的存 在不 影 响程 序的 意 义,但是 它有助 于人们 阅读和理 解程序 ,使原来 模糊 的 、意 义不清 的部 分变 得 清 晰 明了。 因 此,在程 序中适 当加入 注释是一 个好的 程序设 计习惯 。但是 也不要 在不需 要加 注释、意义 十 分明 显的地方 加注释 。究竟 应该在 程序的 什么地 方加注 释,以及注 释应该 如何 来写,并没 有 一个 统一的 标准,这里 也只是提 一些建 议。通 常: (1)所 有程序 都应该 从注释 开始,该注释 至少应 该包含 如下一 些信息 :   程序的名称;   编写和调试人员的姓名;   版本编号;   完成时间或最后一次修改时间;   程序所做工作描述;   使用注意事项;   要求的输入数据及其数据格式;   产生的输出数据及其数据格式;    ⁝ (2)所 有函数 也都应 该从注 释开始 ,该注 释至少 应该包 含如下 一些信 息:   函数的名称;   每 个形式 参数的 名称、作用、特 性及其对 实在参 数的要 求;   编写和调试人员的姓名;   版本编号;   完成时间或最后一次修改时间;   该函数的功能以及用到的主要算法;   该函数产生的结果以及该结果对主程序的影响;   使用注意事项; 9.5  程 序的正 确 性 · 253·    ⁝ (3)也 可以对 一个程 序段、一个语 句、一个声 明等加 注释,以注 明某程 序段 的功 能 、一 个 语句 的作用 、一 个常量 或变量的 意义等 。 (4)当 修改有 注释的 程序时 ,若程 序内容 被修改 ,则 相应的 注释 也必须进 行修 改。错 误 的注释往往比没有注释效果更坏。 9.4.4 对程 序说明的建 议 一般的 ,程 序中使 用的全部 常量都 要引进 一个常 量标识 符,在程序 中不应 该出现 除了 0、 1等极其 简单的常 量以外 的其他 字面常 量。并 且常 量 应该 是全 程 的。在 程序 一开始 用宏 定 义把 本程序中 使用的 全部常 量定义 ,并 加注释 标明每 个常量 的意 义 、使用位置 等。 而在每 个 函数中一般不应该再包含常量定义。 大多数情 况下,类 型名也应 该是全 程的。 但是,对类 型的要 求要比 对常量 的 要 求宽松 一 些,也可 以把类 型说明 成局部的 。 应该按 照作用 和用途 来选择 变量的 说明位置 ,并 且应该 尽量把 变量说 明成局部 的。 函数一般 应该只访问 它的形式参数和局部 量,如果必须访 问全局量 ,应该加 必要的 注释。 9.5  程序 的正 确性 编写程序 的目的 是把需 要计算 机解决 的问题 描述出 来,交给计 算机,使计 算 机 能够按 要 求解 决问题或 完成运 算。这 就要求 程序能 正确地 表达 问 题——— 程序 正确。但 人 们 还是避 免 不了 经常犯 错误。 一个程 序不能 产生正 确结果的 原因是 多方面 的,例如: (1)问 题说明 的不正 确; (2)所 选的算 法不是 问题的 真正解 ; (3)程 序没有 精确的 实现算 法; (4)用 了错误 的数据 去执行 。 9.5.1 错误 种类 编写一个 计算机 程序,一般 不会一 次就正 确,尤其是 初学 者 。许多 人不愿 意 承 认自己 的 程序 有错误 ,而 抱怨计 算机,这样 是 解决 不 了 问 题的 。科 学的 态 度 是 承认 现实,设法 纠正 错 误,并最 终得到 一个正 确的程序 ,使计算 机能够按 要 求 正确运 行。如 上所 述,一个程 序没 有 产生 正确的 结果,可能 存在各式 各样的 原因。 这些错 误可大 致分成 如下三 类: 1.语法 错误 编写出的 程序不 符合 C语言 的语法 ,称为有语 法错误 。例如 标识符 未说明 、语 句结构 不 · 254· 第九章 程序开发和结构化程序设计 对、标识 符重复 说明,等等。 避免这 类错误 的一个 根本办 法是精确 掌握 C语言 的 严 格语法 定 义(BNF表示 式),严格地 按语法 编写程 序。 对这一类 错误,上 机时编译 程序会 马上指 出来,并大 致给 出 出错 位置和错 误性 质。只 要 按照计算机的提示找出错误并修改即可。查出和修改这类错误是比较容易的。 2.语义 错误 解释程 序语义 时发现 的错误 称为 语 义错 误。例 如:以零作 除 数、运算 分量 类 型不 匹配、 死循 环、形式参 数与实 在参数不 匹配、变 量没赋值 之前就 使用其 值,等等。 这类错误 中的一 部分可 由编译 程序检 查出来 ,例 如形式 参数与 实在参 数不 匹配;有一 部 分错 误编译 程序检 查不出 来。编 译程 序 能 查 出来 的 错 误 比 较容 易 解 决,而 那 些 编译 程序 查 不出来的错误就不好办了。 3.逻辑 错误 逻辑错 误是程 序没有 真实反 映算 法 或 算 法本 身 就 有 错 误,没 有反 映 所求 的问 题。例 如 错误 地运用 全局量 、错 写运算 符,等等。 这 类 错误 的 出 现 是 最大 量 的 ,也 是最 难对 付的。 程 序的 调试和 查错主 要是检 查这类 错误,而这也 是往往 最容易 被初学 者忽视 的。 9.5.2 程序 测试和验证 前述语义错误的一部 分以 及逻辑错 误都要 靠本 节介 绍的 方法来排 除。和 其他产品 一 样,计算 机程序 在投入 使用之前 ,一 定要保 证它的 正确性 。看一个 产品(程 序)是否 正确有 两 条途径: (1)证 明——— 若能证 明一个 程序正 确,是最 理想的 。 目前 在软 件领 域内 有 一个 研究 分 支———“程序正 确性证 明”,专门研 究程序 正确 性证明 的理 论和 方 法。不 过这 已超出 本书 的 范围 ,本 书不涉 及它,有兴 趣的读 者可以 参阅有关 资料。 (2)测 试——— 给定一 切可能 的数据 运 行程 序,看结 果 是否 正确 ,若结 果正 确 ,则 说明 相 应程序是正确的。下面着重介绍测试。 若试图给 出一切 可能的 数据运 行程序 ,事 实上是 不可能 的。例 如,最简单 的 两 个整数 相 加的 运算,为测 试其正 确性,就得给 出所有 两个整 数的不 同组 合 。连最 低档的 微 型 机也可 以 表示 65536个整 数,这就 有 65536×65536种组合 。每种 组合都 进行一 次测试 是做 不 到的, 也是不必要的。 有效并实 用的方 法是:选取 若干组 有代表 性的数 据进行 测试,这些 数据的 组 合 能代表 整 个数 据的全 貌。例 如,上述两个 整 数 相加 的问 题 ,可 以分 别取 两 个 正 整数 相加、两个 负整 数 相加 、一 个正整 数和一 个负整数 相加,等等 ,看 是否都 正确。 若这些 情况都 正确 ,则 基本能 代 表两 个整数 相加的 全貌,也就基 本上能 说明两 个整数 相加的 正确性 。 一般来 讲,这里所 说的有代 表性的 若干组 数据,大致 应考虑 如下情 况: 9.6 可 移 植 性 · 255· (1)正 确的数 据,应能得到 正确的 结果; (2)错 误的数 据,应能报告 数据出 错,或得到 错误结 果; (3)边 界上的 数据,应能按 数据特 性,取得合 理的结 果。 这是从 数据本 身来讲 。若从 程序逻 辑上看 ,选 取的 数据组 应能 遍历 程序 的 一切 分支 和 循环 。在 PAD图上 看,应能达 到 PAD图最右 端的一 切可执 行框,即每 个框都能 执行到 。 需要提 醒读者 的是,不论验 证还 是测 试,只 能发 现错 误,不 能证 明或 说明 程 序正 确没 有 错误 。使用 精心设 计的数 据进行 了周密 测试过的 程 序 ,仅 能说“没 有发 现 错误 ”,或 说“发 现 的错 误都已经 改正,改 正后再测 试没有 发现错 误”。 而 不能 说 也 不 能保 证“这 个程序 是正 确 的”。 当然,习惯 上也经 常说“这 个程序 是正 确的”。 这时 “正确”这个概 念不 是 指理 论上 的 “正确 ”概念,而应 该理解 成习惯上 的“正确 ”,即 “没有发 现错误”。 9.5.3 测试 方法 测试一 个程序 ,目 的 是为 了 找 出程 序 中 的 语 义、逻 辑 错误 并 修 改 之 。 一般 有 两 种 如 下 途径: (1)自 顶向下 ; (2)自 底向上 。 自底向上 的方法 是先从 程序的 最底层 的基本 模块开 始调 试 ,给 出适当 数据 进行 测 试,得 到正 确的模 块(保证 达到 对本 模 块 的 要求);然 后向 上,调 试高 一层 次的 模 块 ;正 确 后 ,再 向 上,如此 等等,直到 最顶层 。 自顶向 下的方 法是从 程序的 最顶 层 开始,先 假设 底层模 块正 确,调试 顶层 的 整体 逻辑, 使之 正确;然后 到低一 层次的各 模块,再假 设更低 层次中 的模 块 正确,调试 本层 各模 块 ,每 一 模块 保证达 到其上 层的要 求;然 再 调 试 更 底 一层 的 模 块,如 此等 等,直 到 最 底 层 的每 个模 块 都调试正确为止。 显然,自顶 向下的 方法比较 合理。 因为这 种方法 可以有 效地定 位错误 ,不 致 因 为底层 错 误而 影响整 个程序 逻辑。 而且与 算法 分 析 及 程序 设 计 时 采 用的 自 顶 向 下 、逐 步 求精 技术 有 紧密 的对应 关系。 所以应 该使用 这 种方 法 来 调 试程 序。 当然 这 不 是 绝对 的,事 实上 经常 使 用的也比较有效的手段是两种方法互相结合。 9.6  可 移 植 性 如果一 个程序 在任何 计算机 上以 及 在任 何编 译 系统 中都 能 运 行 ,则 说这 个 程序 是可 移 植的 。这就要 求程序 不依赖 于具体 的计算 机以及 具体的 编译 系 统。在 某计算 机 系 统上开 发 一个程序时要充分考虑到将来它 可能被 用到 其他计 算机 或其 他编译 系统 中。如 果有 可能, · 256· 第九章 程序开发和结构化程序设计 应该 尽量使 用不依 赖于具 体计算 机和具 体编 译 系 统 的方 式 编 写 程 序。 不幸 的是,标 准 C语 言中 有 若 干 处 由 实 现 定 义 的 (Implementation-defined)和 依 赖 于 实 现 的 (Implementation- dependent)地 方;各个编 译系统 也在不 同程度 上对 C语 言进行 了扩充 ;有 些编 译系统 又在 不 同程 度上对标 准 C语言 进行了一 些限 制。为 了使程 序具 有可 移植性 ,建 议编 写程序 时注 意 以下 诸点(但它 们并不 能完全 保证可 移植性 ): (1)尽 量避免 “依赖于 实现的 ”和 “由实现 定义的 ”部分造成 的影响 ; (2)不 要依赖 字符集 合的各 种性质 ,可靠 的特性 只有 a < b <… < z和 0 < 1 <… < 9; (3)不 论何时 输出整 数类型 和浮点 类型值时 ,一 定包括 宽度和 精度参 数; (4)不 要为浮 点类型 计算假 设不现 实的精度 ; (5)尽 量不使 用具体 编译系 统对标 准 C语言进 行的扩充 部分; (6)同 样尽量 不使用 那些可 能被某 些编译系 统限制 的部分 ; (7)用 文件仔 细说明 程序中 可能依 赖于机器 的部分 。 9.7 文    档 程序文 档详细 记录了 有关本 程序 的 一 切 资料。 按照 我国的 国家 标准,软 件 产品 文档 应 包括如下内容: (1)可 行性报 告:一般应该 在项目 开始前 ,进 行可行 性论证 时提出 ; (2)开 发计划 :一 般应该在 项目开 始实施 前,做工作 计划时 提出; (3)需 求说明 书:一般应该 在项目 刚开始 进行,做需 求分析 后提出 ; (4)数 据要求 说明书 :给出 本系统 对数据 的要求 ; (5)概 要设计 说明书 :给出 本系统 的总体 设计; (6)详 细设计 说明书 :给出 本系 统 的 详 细设 计说 明,包 括每 个程 序的 功 能 、输 入 /输出、 算法 、流 程逻辑 (流程图 、PAD图 等),等等; (7)数 据设计 说明:给出本 系统使 用的数 据结构 ; (8)用 户手册 :包 括本系统 的功能 、支撑环境 、支 持软件 、安 装及 初始化、使 用方 法 步骤、 输入 /输 出数据 说明、操作 说明、出错处 理,等等; (9)操 作手册 :包 括本系统 的运行 说明、操作 说明、操作 步骤等 ; (10)模块 开发卷 宗:每个模 块的标 题、功 能说明 、设 计说明 、代 码清单 、测 试说明 ,等等 ; (11)测试 计划:包括 测试内 容、测 试条件 、测 试数据 、应 得结果 等; (12)测试 分析报 告:一般由 用户来 写,包 括测试 概要、测试 结果、测试 结论等; (13)总结 报告。 9.8  自 顶向 下 逐步 求 精 的 程 序 设 计技术 · 257· 以上简 单地列 出 了 一 个 软 件 产 品 文 档 应 该 包 括 的 内 容,详 细 资 料 可 查 阅 国 家 标 准: GB8567-88《计算 机软件 产品开 发文件 编制指 南》。 文档是 操作人 员的“指南 ”,维 护人 员 的 “参 考 手 册”,必 须认 真对 待 。文 档 必须 严格 正 确反 映软件产 品本身 ,尤其当修 改程序 时,一定要 马上随 着修 改 相应 文档。一 个 与 程序不 匹 配的文档比没有文档更糟糕。 9.8 自 顶向 下逐 步求 精的 程序 设计 技术 9.8.1 自顶 向下、逐步求精 计算机 是一台 没有创 造力和 普通 感 觉 的 机器,它 不会 自 己解 决问 题 。若 想 让计 算机 解 题,必须 用清晰 而无两 义性的方 式给它 提供算 法。这 就要求 : (1)对 所要解 决的 问 题 找 出 一个 算 法 ,它 能 提 供 所 解决 问 题 的 从 输 入 到 输 出 所 需 的 映像。 (2)选 择一种 表达方 式(一个 程序语 言)编写出 程序,用计 算机能 接受的方 式表述 算法。 问题的 关键是 如何找 出算法 。因为 编写出程 序,只是表 述算法 ,应 该没有 太大困 难。 “自顶向下 、逐步求 精”的程序 设计技 术是目 前 较 为流行 的(当然 也 是 较为 合 理的)找 出 一个 问题的解 题算法 的一种 思维方 法。该 技术的 核心思 想是:对于 某一个 要解 决的 问 题,在 寻求 它的解法 过程中 ,首先从问 题的整 体(最顶 层)出发 ,将它 分解成 独立而 互不交 叉的若 干 个子问题。每个子问题解决整体问题的一部分或一 种情况。这几个子问 题若能 正确 解决, 则它 们的总 和就是 整体问 题的解 ,当 然 每 个 子问 题不 一 定马 上就 能 解决 。 向 下 再一 个个 地 具体 考虑下 一层的 各个子 问题,针 对 每 个 子 问题 ,仍 采 用 对 待整 体 问 题 解 的思 路 ,继 续对 其 进行 分解(求精 ),得到该 子问题解 法的分 解 步 骤,即更 低一 层 次的 子问 题 。如此 下去 ,直 到 最低 层的每 个子问 题都能 用计算 机 语言 的 一 个 语句 表示 出 来或 都能 明 显 写 出解 法为 止,便 找到解决整体问题的解题算法了。 显然,开始 对整体 问题的求 精以及 对低层 某一子 问题的 求精过 程中,不一 定 对 相应算 法 十分 清楚,程序 员头脑 里可能是 一些 模糊 的 概念 。 但 是经 过 这 种 不断 的 细 化 、求 精过 程,问 题的 解法会 一层层 地不断 清晰、明了,最 终得到一 个十分 清楚、明确 的解题 算法。 上述的 求精过 程中的 每一步 ,即分 解某一 具体问 题时,主要 用到如 下四种 求精技 术: (1)顺 序连接 的求精 ; (2)分 支、选择的 求精; (3)循 环的求 精; (4)递 归的求 精。 · 258· 第九章 程序开发和结构化程序设计 当问题的 子解具 有前后 关系时 ,采 用第一 种顺序 连接的 求精技 术,将问题 分 解 成互不 相 交的几个子问题的顺序执行。 当问题 是分别 不同情 况而应 该进 行 不同 处理 时 ,采用第 二种 分支、选 择的 求 精技 术,构 造分支。这时要注意分支的条件一定要正确。 当问题的 子解具 有特性 :如 果具有 向解的 方向前 进一步 的方法 ,且 不断重 复 该 步骤即 能 解决 问题,最终 达到完 全解,则应该 采用循 环的求 精技术 (构造循环 )。这时 一定要 弄清循 环 的初 始条件 、结 束条件 和有限进 展的一 步都是 什么。 当问题的 某步解 法与前 边高层 次的某 步解法 具有相 同性 质 ,只 是某些 参数 不同 时 ,可 采 用递归的求精技术。这时应注意递归的参数变化规律以及递归出口。 由此可 知,所谓自 顶向下,逐 步求精 的分析技 术实质 上是如 图 9.9所 示过程 的反复 。 在图 9.9中,求解 的问题可 能是整 体问题 ,也 可能是 求精过程 中某一 层的某 一步。 图 9.9 求精过程 这种自 顶向下 、逐 步求精 的 思 维方 式不 是 计 算 机 程 序员 独 有 的 。事 实上,在日 常生 活 和工 作中也经 常使用 该技术 ,只 不过不 自觉或 没意识 到罢了 。例如 写一本 书或 一篇 文 章,总 要作 一个提 纲,全书分 成几章;然 后 每一 章 又 分 为几 节;每 一 节 又 分为 几 小 节 ,等 等;最后 再 具体 着手写每 个小节 。又如 ,设 计生产 某产品 的一个 工厂(例 如汽车 厂):首 先应考 虑全厂 应 该分 成几个车 间(例如 ,生产发动 机的发 动机车 间、生产底 盘的底盘 车间、生产 车 轮 的车轮 车 间、总装 车间,等等);然 后再考虑 每个车 间应分 成几个 工段(例 如,发 动机车 间应分 成生产 机 壳的 机壳工 段、生产活 塞的活塞 工段、负 责 工件 热 处 理 的热 处理 工 段,等 等 );然后再 考虑 每 个工 段应该 配备多 少种设 备,每 种设备 应配备 多少台 ,等 等。这 就是自 顶向下 、逐步 求精。 9.8  自 顶向 下 逐步 求 精 的 程 序 设 计技术 · 259· 采用自 顶向下 、逐 步求精方 法构造 程序有 如下优 点: (1)程 序的层 次分明 、结构 清晰。 (2)便 于集体 开发程 序。对 于大型 程序来讲 ,可 以每组 负责一 个模块 (一 个子部 分),在 一个 组内又可 以每个 人负责 一个子 模块(更小的 子部 分),等等。 而各个 模块 之间以 及各 个 子模 块之间相 对独立 ,互相之间 没有制 约,各个模 块的负 责人员 可以独 立地进 行 各 自的程 序 设计。 (3)便 于调试 。若程 序有错 误,可以 很容 易 地将 错误 局 部于 某一 子 部 分 ,找 出错 误,同 时每 一部分 的错误 是独立 的,也 不至于 影响其 他的部 分。 9.8.2 求精 过程的表示 有各种 方法表 示上述 求精过 程,比 较常见 的方法 有如下 几种: 1.传统 的流程 图方法 传统的 流程图 方法是 把每步 求精 过 程 用 流程 图 表 示 。这种 方法 的优 点是 直 观,它以 二 维图 来表示 程序的 流程;缺点是 不能体 现结构 化程序 设计思 想。 2.函数 方法 函数表 示方法 是:在求精过 程中,对 每个子 问题 都以 一个函 数调 用来 表示,然后 再具 体 求精 相应的函 数本身 。在这 种方法 中,函数调 用体现 了高一 层次的 模糊的 求解 步骤,即做 什 么;而对 具体函 数的求 精体现了 低 一 层 次 的 求精 过 程 ,包 含 了一 个 函 数 的 具体 实 现细 节,解 决怎 么做的问 题。该 方法不 主张画 框图,而主 张直接 书写程 序。这 种方法 的优 点是:可以 直 接写 出程序 ,程 序的层 次比较分 明,能体 现结 构 化 程 序设 计 思 想 (直接使 用结 构化程 序设 计 的控 制结构)。 缺点是 :由于程序 设计语 言以一 维方式 表示逻 辑结构 ,所以 在视 觉上 看 ,逻 辑 结构 和算法 不直观 ,表 示能力也 不足。 3.PAD 方法 本书推 荐使用 PAD方 法。PAD方 法利用 PAD图 表 示 求精 过 程。它 具有 上述两 种方 法 的共 同优点 ,同 时又摈 弃了上述 两种方 法的缺 点,具有如 下特点 : (1)处 理的概 括性:在 PAD 中,与 一 条纵 线 相 连 的处 理 可 以 作 为一 个概 括。而 在流 程 图和 程序中 ,没 有特别 的表示处 理概括 的记法 。 (2)执 行顺序 的表示 :在 PAD 中,从 上至 下 用 纵 线连 接 处 理 顺 序。 而在 流 程图 中用 箭 头表 示处理 流向;在程 序中则没 有表示 处理顺 序的方 法。 (3)嵌 套层次 的表示 :在 PAD中 ,树 结构可 以 清楚 地表 示嵌套 层次。 而在 流程图 中,没 有表 示嵌套 的记号 ;在 程序中则 靠的是 语句的 嵌套。 它们都 没有 PAD那样 清晰、明了 。 对于人们 来说,画 出图更直 观、易读、易记 、易 于理解 。特 别 是二 维的平面 图,更 容 易看、 容易 说明问题 。所以 即使不 是几何 学的问 题,如果能 给出适 当的几 何表示 ,也 会 使 问题变 得 · 260· 第九章 程序开发和结构化程序设计 更容 易处理 。PAD正是 使用二维 树形结 构图描 述程序 的逻辑 ,使 程序逻 辑直观 、清楚 。 9.8.3 求精 实例 下面用几 个例子 一方面 说明自 顶向下 、逐 步求精 的程序 设计方 法,另一方 面 给 出求精 过 程的 PAD表示方法 。 例 9.2 编写 程序,测定 字母偶 的出现频 率。 该例子 是测定 小写字 符串中 相邻字 母偶的出 现频率 。例如 ,它 给出 在一 个 字符 串中 ea 比 ie出现得 多还是 少。程 序只对 在单字 中的字母 偶计 数,针 对 thecat,它只对 th 、he、 ca和 at计数,但不对 ec计 数。计 数器使 用一个 二维数 组 conmat。把 字母偶 ie的出 现次 数放 入下标 变量 conmat[(int)i-(int) a][(int) e -(int) a]中(事实 上可以 不加强 制 类型 转换,下面 程序中 将省略强 制类型 转换)。 设有说 明:   intconmat[26][26]; 首先可 以 考 虑 把 该 问 题 分 解 成 如 下 几 步:初 始 化 计 数 器 数 组 conmat;统计 每个字 母偶的 出现频 率;输 出统计 结 果 。表示 成如图 9.10 所示 的 PAD。 下面考 虑求精 上述 PAD中的 每一个 步骤。 初始化 数组 conmat,显 然 应 该一 行 一行 地 初 始化 ,对 于每 行,显 然 又应 该一个元 素一个 元素地 初始化 ,分 别用如 图 9.11和 图 9.12所示 的   图 9.10 总控 PAD表示 。 图 9.11 初始化 图 9.12 初始化第 c1行 考虑统计部分: 假设被统 计的字 符串是 从终端 输入的 ,则 显然应 该把该 字符串 全部读 入,并 在 读入的 过 程中 边读边 统计,用如 图 9.13所示 的 PAD表示。 再考虑具 体统计 算法,为统 计字母 偶的出 现频率 ,显 然在读 入字符 串的过 程 中 应该始 终 保存 两个字 符 prevchar、thischar,并 且当该 两个字 符都是 字 母时 ,相 应计 数单元加 1;最 后在 读入 下一个 字符之 前还应 该把保 存的两 个字符向 前串,得如 图 9.14所示 的 PAD。 最后考 虑输出 部分,把结 果输出 成两个 26×13的表 如下: 9.8  自 顶向 下 逐步 求 精 的 程 序 设 计技术 · 261· 图 9.13 统计(EOF代表回车或换行) 图 9.14 统计一次      * abcde… m a b c ⁝ z * nopqr… z a b c ⁝ z 每个表 元素是 相应字 母偶的 出现次 数。显然 应该一 个表一 个 表 地打印,由 如图 9.15所 示的 PAD表示。打 印 一 个 表 (例 第 一 个 表),显 然 应 该 先 打 印 表 头 再 打 印 下 面 各 行,由 如 图 9.16所 示的 PAD表 示。 图 9.15 输出 图 9.16 输出一个表 打印表头 可以求 精成如 图 9.17所 示的 PAD。 打印表 体应该一 行一行 地打 印,每行应 该 先打 印行头 ,再 一个元 素一个元 素地打 印,得如图 9.18所示的 PAD。 最后编写出该程序如下: · 262· 第九章 程序开发和结构化程序设计 图 9.17 打印表头 图 9.18 打印表体    /*PROGRAM contingecies*/ #include"stdio.h" intconmat[26][26]; /* 初始 化 函 数 */ voidintial(){ inti,j; for( =0;i<26;i++) for(j=0;<26;j++) conmat[i][j]=0; } /* 统计 函 数 */ voidsttistical(){ charprechar,thischar; scanf("% c",&prechar); do{    /* 读 下 一 个 字 符*/ scanf("% c",&thischar); /* 统 计 */ if((thischar< z&&thischar>= a) && (prechar<= z&&prechar>= a))    conmat[prechar- a][thischar- a]++; prechar=thischar; 9.8  自 顶向 下 逐步 求 精 的 程 序 设 计技术 · 263· }while(thischar!= \n&&thischar!= \r); } /* 输出 函 数 */ voidsu out(charbeginch,charendch){ charc1,c2; /* 打 印表 头 */ printf("%5c",* ); for( 1=beginch;c1<=endch;c1++) printf("%5c",c1); printf("\n"); /* 打 印表 体 */ for(c1= a;c1<= z;c1++){ printf("%5c",c1); for(c2=beginh;c2<=endch;c2++) printf("%5d",conmat[c1- a][c2- a]); printf("\n"); } } voidou(){ subout(a,m ); printf("\n"); subout( n,z); } /* 主函 数 */ voidman(){ initial();      *初始化*/ statistical(); /* 统 计 */ out(); /* 输 出 */ } 例 9.3 三个 齿轮啮 合问题 。 设有三 个齿轮 互相衔 接,求 当三 个齿轮 的某 两对 齿互相 衔接 后到下 一次 这 两对 齿再 次 互相 衔接,每个 齿轮最 少各转多 少圈。 解:这是求 最小公 倍数的问 题。每 个齿 轮需转 圈数 是三个 齿轮 齿数 的最 小 公倍 数除 以 自己 的齿数。 设三个 齿轮的 齿数分 别为 na、nb、nc;啮合最 小圈 数分 别 为 ma、mb、mc;三 齿 轮齿 数的最 小公倍 数为 k3。 计算步 骤如图 9.19所示。 读入三 齿轮齿 数和输 出结果 ,分别 只是一 次调用 读或写 函数,已经 不必求 精。 求精计 算三齿 数的最 小公倍 数 k3,可以把 该问题 分解成:先求 两个齿 数 na与 nb的 最小 公倍 数 k2,然后再 求 k2与第 三个齿 数 nc的最 小公倍 数 k3,k3即为 na、nb、nc三 个齿轮 齿数 · 264· 第九章 程序开发和结构化程序设计 的最 小公倍 数。设 已经有 求两个 数的最 小公倍数 的函数 intlowestcm(intx,inty),则该 求 精过 程如图 9.20所示。 图 9.19 三个齿轮啮合 图 9.20 三个数的最小公倍数 继续 求精求两 个数的 最小公 倍数的 函数 lowestcm。x、y的最 小 公倍 数是 x、y的 积除 以 x、y的最 大公约数 。设已 经有求 两个数 的最大 公约数 的函数 intgcd(intx,inty),则该求 精 过程 如图 9.21所 示。 采用辗 转相除 法求两 个数的 最大公 约数,函数 intgcd(intx,inty)可 以如下 定义: x, y=0 gcd(x,y)= gcd(y,x%), y≠0 函数 gcd是 一 个 递 归 函 数,先 采 用 分 支 求 精 过 程,再 采 用 递 归 求 精 过 程,可 以 求 精 成 如 图 9.22所 示的形 式。 图 9.21 两个数的最小公倍数 图 9.22 最大公约数 最后,分别 计算啮 合的 最 小 圈 数可 以被 求 精 成 如 图 9.23所 示的形式。 按 PAD图,编写程 序如下 :    /* PROGRAM mesh*/ #include"stdio.h" /* 计算 x,y的最大公约数 */ intgcd intx,inty){ if(y== )   图 9.23 最小圈数 9.8  自 顶向 下 逐步 求 精 的 程 序 设 计技术 · 265· returnx; else    returngcd(y,x% y); }/* gcd*/ /* 计算最小公倍数 */ intlowescm(intx,inty){ returnx* y/gcd(x,y); }/* lowestcm */ /* beginofmesh*/ intman(void){ intna,nb,nc,k3; printf("pleaseinputn1n2n3:"); scanf("%d% d% d",&na,&nb,&nc); k3=lowestcm(lowestcm(na,nb),nc); printf("FOR meshthefirstmnstrotateabout%4drings.\n",k3 /na); printf("FOR meshthesecondmnstrotateabout% 4drings.\n",k3 /nb); printf("FOR meshthethirdmnstrotateabout%4drings.\n",k3/nc); } 例 9.4 验证 三角形 外心定 理。 一个三 角形三 条边的 垂直平 分线交 于一点,且该 点是三 角形外 接圆的 圆心。 解:不失一 般性,假设三 角形的 任意一 条边都 不平行 于任 意 一个 坐标轴。 依 据 平面几 何 知识 ,该 问题的 验证步 骤应该是 :读入三 点 a、b、c的坐标 (x1,y1)、(x2,y2)、(x3,y3);检 验三 点是 否构成 一个三 角形;若三点 构成三 角形,则验 证外心 定理,如图 9.24所示。 读入三 点坐标 只是一 个读语 句,不 必 求 精。检 验三 点是 否 构成 三角 形使用 一个 bool型 函数 isTriange,可以 求精 成:求 两点 p1、p2确 定 的 直线 方 程 L12;判 断 若 p3在 L12上,则 isTriange为 false,否则 isTriange为 true,如图 9.25所示 。 图 9.24 验证外心定理 图 9.25 三点是否构成三角形 · 266· 第九章 程序开发和结构化程序设计 求两点间 直线方 程可以 编写一 个函数 line(x1,y1,x2,y2,*a,* b),并 求精 如图 9.26 所示。 验证外 心定理 可以如 下进行 :求 每条 边的垂 直平 分线;验证 该三 条直 线是 否 交于 一点; 若该 三条直 线交于 一点,则验证 该点到 三角形 各顶点 距离是 否相等 ,如 图 9.27所示 。 图 9.26 直线方程 图 9.27 验证外心定理 求每条 边的垂 直平分 线方程 可以 编 写一 个求 一 条线 段的 垂 直 平 分线 方程 的 函数 ,然 后 分别 三次调 用该函 数,如图 9.28所 示。 求一条 边的垂 直平分 线方程 可以先 调用前述 函数 line,求 该边 的直线 方程;再求 该边 的 中点 ,然 后求过 该中点 的与该边 垂直的 直线方 程,如图 9.29所 示。 图 9.28 垂直平分线 图 9.29 一条边的垂直平分线 验证该 三条直 线是否 交于一 点,编 写一个 函数 isOnePoint,可以 先求两 条直 线的 交 点,然 后再 判断第 三条直 线是否 过该点 ,如图 9.30所示 。 设有一 个函数 distance(x1,y1,x2,y2)可以 计算 两 点间的 距离 ,验 证三 条 垂直平 分线 的 交点 到三角 形各顶 点距离 是否相 等,是 一 个 布尔 表达 式。 计 算两 点间 距 离 的 函 数被 求精 如 9.8  自 顶向 下 逐步 求 精 的 程 序 设 计技术 图 9.31所 示。最 后按上 述 PAD,编写 验证三 角形外 心定理程 序如下 : · 267· 图 9.30 三条直线交于一点 图 9.31 两点间的距离    /*PROGRAM test*/ #include"stdio.h" #include"math.h" #defineeps1e-5                /控制精度 #definemax1e30 /*求由两点所确定的直线方程系数 y=a*x+b*/ void ine(floatx1,floaty1,floatx2,floaty2,float*a,float*b){ * a=(y1-y2)/(x1-x2); * b=y1-(* a)* x1; } /* 判断 三 点 是 否 在 同一 直 线 上 ,即 三 点 是 否 构成 三 角 形 */ boolsTriangle(floatx1,floaty1,floatx2,floaty2,floatx3,floaty3){ floata,b; line(x1,y1,x2,y2,&a,&b); //第 1,2点连线的斜率和截距 if(fab(a* x3+b-y3)<eps) //带入第 3点,判断是否在同一直线 returnfalse; //在,则 不 是三 角 形 else returntrue; //不在 ,则 是三 角 形 } /* 求由 两 点 所 确 定 直线 的 中 垂 线的 方程 */ void line(floatx1,floaty1,floatx2,floaty2,float*a,float*b){ floatta,tb; floatx,y; x=(x1+x2)/2; y=(y1+y2)/2; //两点 所 确 定 直 线 的 中点 line(x1,y1,x2,y2,&ta,&tb); //两点 所 确 定 直 线 的 方程 * a=-1/ta; * b=y-(* a)* x; //中垂 线 方 程 · 268· 第九章 程序开发和结构化程序设计 } /* 根据 三 条 直 线 方 程判 断 三 条 直线 是否 交 于 一 点 ,并 返 回 交 点坐 标 */ boolisOnePoint(flota1,floatb1,floata2,floatb2, floata3,floatb3,float* x,float* y){   x=(b2-b1)/(a1-a2); * y=a1*(* x)+b1; //交点 坐 标 if(fabs(a3 (*x)+b3-(*y))<eps) returntrue; //位于 同 一 直 线 else      returnfalse; //否则 } /* 计算 两 点 距 离 */ float istance(floatx1,floaty1,floatx2,floaty2){ returnsqrt((x1-x2)* (x1-x2)+(y1-y2)* (y1-y2)); } /* 主程 序 */ void ain(){ floatax,ay,bx,by,cx,cy; //三点 坐 标 floata_a,a_b,b_a,b_b,c_a,c_b; //三角 形 三 条 中 垂 线 的直 线方 程 floatcenterx,centery; //中垂 线 交 点 坐 标 printf("pleaseinputcoordinatofpointa:"); scanf("% f% f",&ax,&ay); printf("pleaseinputcoordinatofpointb:"); scanf("% f% f",&bx,&by); printf("pleaseinputcoordinatofpointc:"); scanf("% f% f",&cx,&cy); //输入 三 角 形 三 点 坐 标 if(! isriangle(ax,ay,bx,by,cx,cy)) printf("thethreepointsareonthesameline! \n"); else{ vline(ax,ay,bx,by,&a_a,&a_b); vline(bx,by,cx,cy,&b_a,&b_b); vline(cx,cy,ax,ay,&c_a,&c_b); //求三 条 中 垂 线 的 斜 率和 截距 if(isOnePoint(a_a,a b,b_a,b_b,c_a,c_b, &centerx,&centery)) //三条中垂线是否交于一点   f(fas(distance(centerx,centery,ax,ay)-distance(centerx,centery,bx,by))<eps&& fabs( istance(centerx,centery,ax,ay)-distance(centerx,centery,cx,cy))<eps) printf("thecenterofthecircumcircleis:(% g,% g)\nOK! \n",centerx, centery); //中垂 线 交 点 是 三 角 形外 接圆 的 圆 心 else    printf("theintersectionisnotthecenterofthecircumcircle! \n"); else  printf("thethreeperpendularbisectorsdontintersectinonepoint! \n"); } } 9.9  受 限排 列 组合 ——— 穷 举 法 与 试探法 · 269· 9.9 受 限排 列组 合———穷 举法 与试 探法 有这样 一类问 题,从 若干 元 素 的 所有 排 列 (或 组 合 )中 选 取 符 合 某 种 条 件 的 一 些 排 列 (或组 合)。 例 9.5 著名 的八皇 后问题 。 这是 一个古 老 而 有 趣 的 游 戏。高 斯 (C.F.Gauss)最早 在 1850年 提出 并 研 究 过 这 个 问 题 ,但 是 没 有 完 全 解 决 。 该 问 题 是:在一 个 8×8格的 国际 象 棋棋 盘上 放置 八个 皇 后,使 任 意两 个皇 后都不 能互相 攻击。 按 国际 象 棋 规则,两 个皇 后 若 在 同一 行上 ,或在 同 一 列 上,或在 同一 条斜 线 上,则 它 们 可以 互 相攻 击。 如图 9.32放 置的八 个皇后 便不能 互相攻击 。 编写程序 ,求出所 有符合 要求的 布局(共有 92种满 足条件 的布 局,若除去 对称 的 等 类 同布 局,有 12种 完全 不 同 的 布 局。  图 9.32  不 能 互 相攻 击 这里 不考虑 剔除类 同布局 ,求出 全部 92种 布局)。 的八个皇后 解这类问题有如下两条途径: (1)穷 举法; (2)试 探法。 下面以八 皇后问 题为例 ,分 别介绍 这两种 方法。 在具体 介绍这 两种方 法之 前,先考虑 八 皇后 问题的 表示方 法。用 如图 9.33所 示的一 维数组 表示棋 盘。 图 9.33 表示棋盘的一维数组 A[i]表示棋盘的 第 i列,若 A[i]中 存放有数 k,表示第 i列中第 k行上放置 了皇后 。例如   A[3]=6 表示 在棋盘的第 3列第 6行上放置一 个皇后。A[0]是根据下面算法 的需要多设 的一个元 素。 1.穷举 法 本方法 的思路 是,按某种顺 序枚 举出 全部 排 列或 组合。每 当枚 举出 一种 排 列或 组合 之 后,便用 给定条 件判断 该种排列 或组合 是否符 合条件 。若符 合条件 ,则 本种排 列 或 组合被 选 中,可以 输出或 记录下 来。若 不 符 合条 件,则 把 本种 排 列 或组 合 舍 掉。 八 个皇 后 的 排 列 问 · 270· 第九章 程序开发和结构化程序设计 题,最简 单的方 法 是 八 重 循环,可 以 编 出 如 下 穷 举 法 程 序。为 简 单 起 见 ,先 省 略 检 验 函 数 check与输出子 程序 out的具 体实现 细节,晚些 时候再 开发它们 。    /* PROGRAM eightqueenl*/ inta[9]; boolcheck();    * 检查某格局是否合格的函数,函数分程序暂略 */ voidout(void); /* 输出子程序,分程序部分暂略 */ intman(void){ inti1,i2,i3,i4,i5,i6,i7,i8; for i1=1;i1<=8;i1++ ) for i2=1;i2<=8;i2++ ) for i3=1;i3<=8;i3++ ) for i4=1;i4<=8;i4++ ) for i5=1;i5<=8;i5++ ) for i6=1;i6<=8;i6++ ) for i7=1;i7<=8;i7++ ) for(i8 1;i8<=8;i8++ ){ a[1]=i1; a[2]=i2; a[3]=i3; a[4]=i4; a[5]=i5; a[6]=i6; a[7]=i7; a[8]=i8; if(check())out(); } } 2.试探 法 本方法的 思路是 ,按一定规 律,从某一 满足 条 件 的 初始 状 态 (初 始 排 列 )出发 ,逐 步生 成 满足 条件的子 排列,不 断增加 子排列 长度。在 增加子 排列长 度的过 程中,每增加 一 位 ,生 成 一个 子排列 后,便检验 它是否 满足条 件。若该 子排列 不满足 条件,则换 一个子 排列(即 修改 当前 位置——— 在当前 位置上 换一个 元素);若该 子排列 满足条 件,则延长 子排列 ,增加子 排 列长 度。直 到子排 列的长 度满足 问题的 要求长度 为止,便找 到了一 个解。 若要求找 到一个 解即可 ,这 时便可 以结束 。若要 求找到 所有解 ,这 时可以 输 出 或记录 本 解,然后 按前述 思路,继续 修改排 列,继 续试探 ,找 下一个 解。下 面只考 虑找全 部解的 问题。 在上述 试探过 程中,修改 当前位 置时要考 虑: (1)若当 前位置 上还有没被 试探过的 元素,则 应该取下 一个没被试 探过的元素进行试 探。 (2)若 当前位 置上所 有元素 都被 试 探 过 了 (例如 八皇 后 问 题中,某 一列 的 八个 位置 都 9.9  受 限排 列 组合 ——— 穷 举 法 与 试探法 · 271· 已经 考虑过 了),则在当 前位置 上没办 法再修改 了。这 说明 前边的 某个位 置 有 问题,放置 的 元素 不对,显然 应该向 回退一位 (即回 溯到 上一 个位置 上)。回 溯后 ,原 来 的 上 一个 位置 变 成了 当前位 置,则又变 成修改 当前位 置的 问 题了。 这时 ,同 样还 应该考 虑取 下一个 没被 试 探过 的元素 进行试 探,以及是 否所有 元素在当 前位置 上都被 试探过 了并回 溯的问 题。 如图 9.34所 示,设要生 成一个 n位的 串 A;在 每个位 置上可 选择的 符号有 k个 ;A 应该 满足 条件 r。 图 9.34 图示修正当前位置 则: (1)初 始时,从第 一个位置 开始试 探,令 m=1。 (2)然后在第 m位逐次取可选符号:B[1],B[2],…,B[k]。对某一个 B[i]有两种可能: ① 若 B[i]使 A[1],A[2],…,A[m]满足 r,则进入 A的 下一个位 置,作 m=m+1。 ② 若 B[i]不能 使 A[1],A[2],…,A[m]满 足 r,则还有 两种情 况: (a)i<k,取 B中下 一个符 号继续 试探,即作 i=i+1。 (b)i=k,说 明在当 前位 置上 所 有符 号都 已经 被 试 探 过;应 该 回溯 到 上 一 个 位 置,作m=m-1;然后 再继续 试探。 (3)直 到找到 解或 m==0,全 部情况 都已经被 试探过 为止。 试探法 的思想 如图 9.35所 示。 图 9.35 试探法 · 272· 第九章 程序开发和结构化程序设计 上述试 探法具 体应用 于八皇 后问题 ,如图 9.36所示 。 图 9.36 八皇后试探法 其中 ,check(m)检 验 A[1],A[2],…,A[m]是否 满足 条 件,若 满 足条 件 则 返 回 true, 否则 返回 false。上 述算法 的 C程序如 下:    /* P OGRAM eightqueen2*/ intA[9],m ; boolcheck(intm);       *检查某格局是否合格的函数 */ voidout(); /* 输出子程序 */ void hange(void){ /* 修正 */ while( [m]==8) m =m-1; A[m]=A[m]+1; } voidextnd(void){ /* 延长 */ m =m+1; A[m]=1; }; int ain(void){ /* 主程序 */ A[0]=0; A[1]=1; m=1; whil (m>0) if(chck(m)){ if(m= 8){ out(); 9.9  受 限排 列 组合 ——— 穷 举 法 与 试探法 · 273· change(); } elseextend(); } elsechange(); } 从上例 可以看 出,穷举法 与试探 法的着眼 点及主 要区别 在于: 穷举法 是举出 全部可 能的格 局,对 每种格 局进行 检验,使合 格者入 选,不合格 者淘汰 。 试探法 是从初 始满足 条件的 格局 开 始,逐步 前进,每 前进一 步都 判断 子格 局 是否 合适。 若当 前子格 局合适 则进入 下一级 ;若 当前子 格局不 合适 则选 同一级 的下 一个子 格局;但 是 若同 级子格 局已全 部试探 完毕,则回 溯到上一 级,直到 当前子格 局够长 为止。 显然,穷举 法的运 算量比试 探法 要大 的多,因为 它要 考虑一 切情 况的 排列,而试 探法 可 以在 中间丢 掉大量 的不合 格排列 。事实 上许多问 题的 穷举法 是 不适 用 的 ,八 皇后问 题穷 举 法有 88 种 排列,把每 一种情况 都排出 来,并检验 其是否 合格显然 是不可 能的。 在上述算 法中,不 论是穷 举 法 还是 试探 法 ,都 是用 循环 选代 的方 式 给出 的解 法。众 所 周知 ,循 环程序 可以用 递归来表 示,不论 穷举法还 是试探 法都可 以写成 递归形 式。 3.穷举 法的递 归实现 用穷举 法实现 八皇后 问题,可以 抽象地描 述为在 数组 A 的 8个 位置 上分 别排列 一个 1 到 8的整数。 设想,如 果有一 个函数 queen(r)能 够完 成“在 数 组 A 后部 的 r个 位 置 (从 第 8-r+1到 8)上分 别排列 一个 1到 8的整 数”,则八皇 后问题便 是 queen(8)。 “在数组 A后 部的 r个 位置上 ,分 别排列一 个 1到 8的整 数”可以 分解成 : (1)先 在第 8-r+1位上 分别排 列一个 1到 8的 整数; (2)然 后再在 剩余后 部的 r-1个位 置上分 别排列 一个 1到 8的整 数。 步骤(2)便是 对 queen的 递归调 用 queen(r-1)。最后考 虑递归 出口,显 然应该 在 r=0 时检 验输出 并退出 递归。 由此得 如图 9.37所示 的递归 实现 八皇后 问题 的穷 举 算法 及递 归 程序。 图 9.37 递归实现八皇后穷举法    /* P OGRAM eightqueen3*/ · 274· 第九章 程序开发和结构化程序设计 intA[9]; boolcheck();    * 检查某格局是否合格的函数,分程序略 */ voidout(); /* 输出子程序,分程序略 */ void ueen(intr){ inti; for(i 1;i<=8;i++ ){ A[8-r+1]=i; if(r>0 queen(r-1); elseif(chek()) out(); } } int ain(void){ queen(8) } 4.试探 法的递 归实现 试探方法 的思路 是,按一定 规律,从 某一 满足 条 件 的 初始 状态 (初 始 排列 )出 发 ,逐 步 试探 生成满 足条件 的子排 列,并且不 断增加 子排列 长度 ,直 到子 排列的 长度 满足问 题的 要 求长 度 n为 止,便找到 了一个解 。设想 ,如 果有一 个函数 queen(r)能 够“合理地 排列数 组 A 后部 的 r个(从第 n-r+1到 n)元 素 ”,并 保证 序 列满 足 给 定 条 件,则 八 皇后 问 题 便 是 对 queen的调用 queen(8)。下面 开发 queen(r)。 “合理地排 列数组 A后部的 r个(从第 n-r+1到 n)元素 ”可以分 解成: (1)首 先在第 n-r+1位 上排列 一个合 格的元 素; (2)然 后再合 理地排 列从 n-(r-1)+1开始 的后部 的 r-1个元素 。 步骤(1)“在第 n-r+1位 上 排 列 一 个 合 格 的 元 素 ”,即 把 8个 元 素 顺 次 地 排 列 在 第 n-r+1位 上,并 对每个 元素检 验是否 合格,使合 格者入 选。 步骤(2)“合理地 排列后 部的 r-1个 元素”,显然与 “合理地 排列后 部的 r个 元素”具 有 相同 的特征 属性,只是 排列的 元素个 数减少了 ,也就是 说它表现 为对 queen的递 归调用 。 按上述 思想得 如图 9.38所 示的 queen算 法。在 该算法 中,试探法 的“延长 ”体现在 继续 递归 调用上;“修 正本位 ”体现在从 1到 8作 i的 循环上 ;而“回溯”则 体现在 递归出 口的返 回 上。 程序如 下(这个程 序是最 短最精 练的了 )。 图 9.38 递归实现试探法 9.9  受 限排 列 组合 ——— 穷 举 法 与 试探法 · 275·    /* ROGRAM eightqueen4*/ intA[9]; boolcheck(intm );      *检查某格局是否合格的函数 */ voidout(void); /* 输出子程序 */ voidquen(intr){ /* 试探法 */ inti; for(i=1 i<=8;i++ ){ A[8-r+1]=i; if(ceck(8-r+1)) if(r 0) queen(r-1); elseout(); } } int ain(void){ /* 主程序 */ queen(8) } 5.检验 函数 check 下面只 开发试 探法的 检验程 序。对 穷举法来 讲,则应该 多加一 层循环 :     for(m =2;m <=n;m ++) 对每 个 m检验 A[m]是 否满足条 件。 分析检验条件: (1)纵 向,每列只 有一个 皇后:由数 据结构 保证每 列只能 放一个 皇后。 (2)横 向,每行放 置一个皇 后:显然 要求数 组 A 中不 能有 重 复 的 数值。 设当 前为第 m 步,由于 前 m-1步是 合格的 ,所 以只要 检验 A[m]不等于 所有的 A[1]、… 、A[m-1]。 (3)左 高右低 对角线 (共 有 2*n-1条,即 15条):这样 对角 线上不 同位 置的行 列号 之 差相 等。设当 前为第 m步 ,由于 前 m-1步是合格的 ,所以 只要对一切 i<m检验 A[m]-m≠ A[i]-i即 可。 (4)左 高右低 对 角 线:与 左 高 右 低 对 角 线 类 似。 只 不 过 这 里 该 检 查 A[m]+m ≠ A[i]+i。 检验函 数 check如下:   boolheck(intm ){ inti; for i=1;i<m;i++ ){ if(A[m]==A[i])returnfalse; if(A[m]-m ==A[i]-i)returnfalse; if(A[m]+m ==A[i]+i)returnfalse; · 276· 第九章 程序开发和结构化程序设计 } returntrue; } 例 9.6 再举 一个这 类问题 的例子 ,Debruijn问 题。如 图 9.39所示,由 23 个二 进制 数 字 0、1组成一 个环,使 23 个 3位的二 进制数:    000           0 001           1 010           2 011           3 100           4 101           5 110           6 111           7 图 9.39 Debruijn环 正好 在环中 各出现 一次。 本例 目 前 的 顺 序是 0、1、2、5、3、7、6、4。设 计 生 成 这样 环 的 程序。 环由 2n 个 二进制数 字组成 ,恰 好包含 2n 个互 不相同 的 n位二进 制数。 解:还是分别用 试探法和穷举法来 解该题。先 考虑环的表示,对任 意 n,环上 有 2n 个 0、1。 用一 维数组 A保存环 上的数据 ,A的长 度 nn=2n,并 且认为 A[nn-1]与 A[0]相接构 成环。 6.穷举 法 用递归 来实现 ,算 法如图 9.40所示,程序如 下: 图 9.40 Debruijn穷举法    /* ROGRAM Debruijn1*/ #include"stdio.h" intnn; intA[256]; boolcheck(intu); voidout(); int ower(intw){ inti,r; r=1; for i=1;i<=w;i++ ) //记录环的长度 2n /* 保存环,限制输入 n最大为 8*/ /* 检查 A[0]~A[u]的 2u 个 n位整数是否不同,略 */ /* 输出数组 A,略 */ /* 求 2n*/ 9.9  受 限排 列 组合 ——— 穷 举 法 与 试探法 · 277· r=r*2; returnr; } voidnet(intm){ /* 穷举 */ inti; if(m nn) for(i=0;i =1;i++ ){ A[m]=i; next(m+1); } elseif(check nn-1))//检验合格 out(); } int ain(void){ /* 主程序 */ intn; printf("\npleaceinputn="); scanf("%d",&n); nn=power(n); /* 以上输入 n,要求 n≤8*/ /* 求 nn=2n*/ next(0); } 7.试探 法迭代 实现 环上一 定有 n个连续 0组成 的 n位 二进制数 0。 作为 初值,把 n个 0放在 最前边 ,从 第 m=n-1位开始试 探。找 到一个 解输出 后,令 m =0,使 循环 终 止 。算 法如 图 9.41所 示,程 序如下:    /* ROGRAM Debruijn2*/ #include"stdio.h" intnn;       /记录环的长度为 2n intA[256]; /* 保存环,限制 n最大为 8*/ boolcheck(intu); /* 检查 A[0]~A[u]的 2u 个 n位整数是否不同,分程序略 */ voidout(); /* 输出数组 A,分程序略 */ intpower(intw); /* 求 2n,同前,略 */ /* 延 长 函 数*/ voidexted(int* m){ (* m)++; A[* m]=0; } /* 修正本位函数 */ voidbck(int* m){ while(A[*m]==1)(*m)-- ;   * 回溯 */ A[* m]=1; /* 修正本位 */ · 278· 第九章 程序开发和结构化程序设计 } intm in(void){ intn,i; intm;     /m记录当前处理位置。注意 m是局部量,为了在子程序中修正它,使 //用 指针 参数 。 printf("\npleaseinputn="); scanf("%d",&n); /* 以上输入 n,要求 n<=8*/ nn=power(n); /* 求 nn=2n*/ for( =0;i<n;i++ ) A[i]=0; m=n-1; /* 以上试探初值 */ whil (m>=n-1) if(ceck(m)) if(m= nn-1){ //若 A[0]到 A[m]正确,且长度够 out(); //输 出 m=0; //退 出 程 序 标记 }elseextend(&m); /* 延长 * elseback(&m); } 图 9.41 Debruijn试探法迭代实现 8.试探 法递归 实现 初始状 态仍用 前 n个 0,从第 n+1位开始 递归试 探。算法 如图 9.42所示,程序 如下:    /* ROGRAM Debruijn3*/ #include"stdio.h" intn;                   * 限制 n最大为 8*/ intnn; /* 2n*/ boolflag; /* 标志试探完毕 */ intA[256]; /* 保存环 */ 9.9  受 限排 列 组合 ——— 穷 举 法 与 试探法 · 279· boolcheck(intu); voidout(); intpower(intw); voidnet(intm ){ if(fla ){ if(m<n ){ A[m]=0; if(chec(m)) next(m +1); A[m]=1; if(chec(m)) next(m +1); }else{ out(); flag=false; } } } int ain(void){ printf("\npleaceinputn="); scanf("%d",&n); nn=power(n); for( =0;i<n;i++ ) A[i]=0; /* 以上试探初值 */ flag=true; next(n); } /*2u 个 n位整数是否不同,分程序略 */ /* 输出数组 A,分程序略 */ /* 求 2n,同前,略 */ /* 试探结束?*/ /* 延长 */ /* 修正本位 */ /* 延长 */ /* 以上输入 n,要求 n<=8*/ /* 求 nn=2n*/ 图 9.42 Debruijn试探法递归实现 · 280· 第九章 程序开发和结构化程序设计 9.检验 函数 check 为了检 验环上 已存在 哪些 n位二进 制数,用 一个 数组 B保 存 已 检 验过 的互 不相 同 的 n 位二 进制数 。check的 参数 u为当前 放入数 组 A中的 “0、1”个数。 在 穷举 法中 用 nn作实 在 参数 对 应 该 u,在 试 探 法 中 用 每 步 试 探 时 的 序 列 长 度 m 为 实 在 参 数 对 应 该 u。 PAD 如 图 9.43所示,check函数如 下: 图 9.43 Dbdruijn检验函数   inttras(inte,intf){           * A[e]~A[f]翻译成 10进制整数 */ intkk,j; kk=0; for(j e;j<=f;j++ ) kk=kk*2 + A[j]; returnkk; } booltet_b(intkk,intb[],intv){ /* 检验 b[0] ~b[v]中有 kk*/ intj; for(j 0;j<v;j++ ) if(k= b[v]) returnfalse; returntrue; } boolchck(intu){ inti,j,k; intb[256],v; v=0; for( =n;i<=u;i++ ){ k=trans(i-n+1,i); /* A[i-n+1]~A[i]翻译成 10进制 =>k*/ iftest b(k,v) /* 检验 B中有 k*/ 习 题 九 · 281· returnfalse; b[v]=k; v++; } if( ==nn){    /* 以下处理 A[i]~A[u]加后边 n-(u-i+1)个 0*/ for(i u-n+2;i<=u;i++ ){    /* 初始位置是 u-n+2而不是 u-n+1*/ k=trans(i,u);         * 翻译 A[i]~A[u]部分 */ for(j= ;j<=n-(u-i+1);j++)/* 后边补 n-(u-i+1)个 0*/ k=k*2; iftestb(k,b,v) /* 检验 B中有 k*/ returnfalse; b[v]=k; v++; } } returntrue; /* 检验结果值 */ } 本章小结 本章讲 述不太 重要的 两种 C语 句以及 极其重 要 的 结构 化 程序设 计思 想,最后讲 述一 类 问题 的程序设 计方法 ,包括 goto语句 和标 号、空语 句、结构化 程序 设计 原则、程序 风格 、自 顶 向下 逐步求精 的程序 设计技 术、程序正 确性、可移 植性、文档 、穷 举法 和试探法 。 重 点掌握 自 顶向下逐步求精的程序设计技术。 习 题 九 9.1 不用 goto语句实现如图 9.44所示的框图。 9.2 如图 9.45所示,正三角形 ABC的三条边 BC、CA、AB上分别有点 r、s、t,使 At= 13AB、Br= 13BC、Cs= 13CA 线段 Ar、Bs、Ct两两相交于点 u、v、w。验证三角形 uvw也是正三角形,且三角形 uvw的 面积等于三 角形 ABC面 积 的七 分之 一 。 9.3  图 9.46所示为排球场平面图,一、二、三、四、五、六为位置号。某球队参加比赛时,一、四号 位放 主攻手;二、五号位放二传手;三、六号位放副 攻手。已 知一、六 号球 员不 在后 排;三 、四号球 员不 在同 一 排;二 、三 号 球 员 不 是 二 传手 ;五 、六 号 球员 不 是 副 攻 手 。编 写 程序 推 算每 个 球 员 的 位 置 。 · 282· 第九章 程序开发和结构化程序设计 图 9.44 框图 图 9.45 三角形 图 9.46 排球场平面图 9.4 从终端输入四个数字(0~9之间),求这四个数字与 1997最接近的排列。 9.5 编写程序,打印 n个元素 a1、a2、…、an 的全部 n!个排列。 9.6 求集合 M的所有子集合。 9.7 已知字母表 ( 1,2,3),生成该字母表上的 所有含有 n个字符的序列。要 求生成的序 列中 没有任意两个子序列是相同的。 9.8 定义:在由 0、1组成的 n位序列中,任意两个 m位子序列均不相等者称为绝对独立序列。编写 函数,对给定的 n求所有的绝对独立序列。 9.9 输入整数 n,构造由整数 1、2、… 、n组成的数字圆环,使环上 任意两个相邻 数之和都为 素数。例 如当 n=8时,该环可以是:1、2、3、8、5、6、7、4。 9.10  编 写 程 序 ,求 所 有 满足 如 下 条 件 的 四位 数 : (1)它 是 完 全 平 方 数 ; (2)千 位 数 字 与 十 位 数字之 和 等 于 百位 数 字 与 个 位 数字 之 积 。 例如 3136满足条件: 3136==562 3+3==1×6 9.11  编 写 程 序 ,求 所 有 满足 如 下 条 件 的 两位 分 数 : (1)分 子 的个 位 数 与 分母的 十 位 数 相 同 ; (2)划 去 分子 的 个 位 数与分 母 的 十 位 数 后 得到 的 分数 正 好是 原 分 数 约 分 的 结果。例如 : 1664= 1 4 9.12 若一个整数 a满足条件 a2 的尾数等于 a,则称 a为自守数,例如 252 =625、762 =5776、93762 =87909376 都是自守数。编写程序,求 10000以内的所有自守数。 9.13 对于两个整数 a、b,如果存在 x使 a+x、b+x都是 完全平 方数,则 x称为相 对于 a、b的 奇特数。 编写程序,输入 100以内任意一对整数,判断在 10000以内它们是否有奇特数,若有则输出。 9.14 编写程序,选择四个整数 a、b、c、d,使得对小于 40的任意一个数都可以由 a、b、c、d中某几个 数相 加 得到 (1、3、9、27)。 9.15 编写程序,把 100分解成 4个整数 a、b、c、d之和,并且满足条件: (a+4)==(b-4)==(c*4)==(d/4) 9.16 编写程序把 1、2、3、4、5、6、7、8、9分成三组,每组三个数码,并且三个数码可以组成一个完 全平方数。 习 题 九 · 283· 9.17 编写程序把 1、2、3、4、5、6、7、8、9分成 3个三位数 a、b、c,并且满足条件: a==(2* b)==(3*c) 9.18 编写程序证明,在 1、2、3、4、5、6、7、8、9之间插入 7个 “+”、“-”符号,使运算结果为 100 成立的惟一方案是: 1+2+3-4+5+6+78+9=100 9.19 对于给定的 n、m,求所有满足方程式 i1 +i2 +… +in =m (i1≥i2≥…≥in) 的正整数序列 i1,i2,…,in。 9.20 对于正整数 n,输出其和等于 n的所有不增的正整数和式。例如,对于 n=4,程序输出:    =3+1 4=2+2 4=2+1+1 4=1+1+1+1 9.21  求 具 有 下 列特 征 的 一 个六位 数 : (1)该 六 位数 的 各 位 数字互 不 相 同 ; (2)该数分别乘以 2、3、4、5、6后得到的新的六位数都是由原来的六个数字组成。 9.22 设有数字串 (d1,d2,…,dn),0<=di <=9,称形 如 (di,di+1,…,di+k)的子 串为 原串的 子片, 其中 1<=i且 i+k<=n。每个子片可以构成一个数 didi+1…di+k,称之为该 子片的基数。编 写程序,计算 并打印给定数字串的所有子片的基数。 9.23 把 1、2、3、4、5、6填入图 9.47所示的各个圆圈内,使三角形各边数字和相等,成为正三角形。 9.24 编写程序,把图 9.48所示的 表填 满,使 1、2、… 、16正好 各占 一 格,并且 使 各行 各列 之 积都 是 偶数。 9.25 图 9.49是一个 4*4的 整数 方阵,其 中 数值 相等 的 元素 均相 邻。由此 便形 成 了一 个个 的 “平 台”,其中最大的平台面 积 是 8,高 度是 6。编 写 函数,对任 意 给 定的 n* n矩 阵,求 其最 大 平 台面 积 和 高度。 9.26 在半径为 r的圆内有两个互相正交的内接矩形,如图 9.50所示。编写程序,确定 a、b(a、b均为 整数 ),使 阴 影部 分 面 积 最 大 。 图 9.47 三角形 图 9.48 表 图 9.49 整数方阵 图 9.50 圆内的正交内接矩形 9.27 今有资金 6万元,拟投资 A、B、C三种产品,投资额与利润的关系如表 9.1所示。求最佳投资 方案。 · 284· 第九章 程序开发和结构化程序设计 利润 投资 额 0 产品 A 0 B 0 C 0 表 9.1 投资额与利润的关系 1 2 3 4 5 1.2 1.5 1.85 2.4 2.8 1.8 2.0 2.25 2.4 2.5 1.3 1.9 2.2 2.45 2.7 6 3.3 2.6 3.0 9.28 修改八皇后程序,使之只输出 12个本质上不同的解。 9.29 五皇后问题。在一个 8×8格的国际象棋棋盘上放 置五个 皇后,使得再 在棋盘 的任意 一个位 置 上摆放棋子都将被某个皇后吃掉。 9.30  骑 士 游 历 问题 。 设 有一块 n×n=n2 个格子的棋盘,如图 9.51所示,一位骑士从初始位置 x0,y0 出发,按国际象棋“马走 日”规则移动,找到所有可以走遍整个棋盘的方案(经过 n2 -1次移动使每个格子恰好被访问一次)。 9.31 九宫排序问题。这是中国的一个古老游戏。如图 9.52所示,一个方框被分成 9个方格,任意放 上 8个标有序号的方块,并留有一个空格,通过若干次移动把它排成固定的格式,移动过程中的任何 一步 都只允许把与空格相邻的方块移入空格内。编写程序,对任意给定的初始格局判 断是否能移 动成功,若能 成功则给出移动步骤。 图 9.51 骑士游历问题 图 9.52 九宫排序 9.32 庙里和尚分苹果,若两个一堆则多余一个;若三个一 堆则多 余两个;若四 个一堆 则多余 三个;若 五个 一 堆则 正 好 。 编 写 程序 ,求 最少 有多 少 苹 果 。 9.33 小明有一个最多能装 10kg的网袋,现 在有白 菜 5kg、猪肉 2kg、鱼 3.5kg、酱 油 1.7kg、白糖 1 kg、土豆 5.1kg。编写程序,为小明选择方案,使他网袋中所装东西的总重量最大。 9.34 农夫、山羊、卷心菜和狼。一个农夫带一只狼、一只山羊和一棵卷心菜想过一条河。问题是 任意 时刻 当 农夫 不 在 场 时 ,狼 要 吃 羊、羊要 吃卷 心 菜 。 编 程 序 ,为 农 夫 设计 渡河 方 案 。 9.35 传教士和吃人生番问题。有 m个传教士和 c个生番准备渡河,只有一只船,每 次最多只能 载 p 个人。在穿梭运载过程中,无论此岸、船上 还是对 岸,生 番均 不能 多于 传教 士,否则 传教 士将 被生番 吃掉。 编写 程 序,为 传教 士 设 计 渡 河 方 案。 9.36 某人去邮局寄东西,邮资大于 8角,目前 邮局只 有 1角、3角 和 5角 面值 的三 种邮 票。编写 程 序,为 他 设 计 购买 邮 票 的 方 案 : (1)给 出 所 有 的 购 买 方案; 习 题 九 · 285· (2)给 出 所 需 张 数 最 少的方 案 。 9.37 有 n个火车站 s1、s2、… 、sn,列车时刻表以适当形式给出(并非所有车站间都通车)。编写程序, 当输入任意起、终点车站 sA、sB 时,程序给出旅途时间最短的乘车方案。 9.38 有 A、B、C、D、E五本书,要分给张、王、李、赵、钱五位同学,每人一本。每人喜爱的书如表 9.2 所示 。 编写 程 序 ,给出 分 配 方 案使 每 个 人 都 满 意。 表 9.2 每人喜爱的书 A B C D E 张 1 1 王 1 1 1 李 1 1 赵 1 钱 1 1 9.39  稳 定 婚 姻 问题 。 设分别有 n个男人和 n个女人,都未婚。每个男人对 n个女人都有不同程度的 偏爱,这些信息保 存在 一张表内;同时,每个女人对 n个男人也都有不同程度的偏爱,这些信息也保 存在一张表 内(例如,对 n=8 的情况,表 9.3和表 9.4给出了一种偏爱样本)。为他们配婚,配好 n对婚姻后,若发现某个男人和某个女 人没 有 成为 夫 妻 ,但 他 们 彼 此 相 爱程 度 更 甚于 他 们的 实 际 配 偶 ,即 : (1)或者存在一个女人 w,某 男人 m对 她比 对自 己的 妻子 更 中意,并且 w对 m 比对 自己 的 丈夫 更 中意; (2)或者存在一个男人 m,某女 人 w对 他比 对自 己的 丈夫 更 中意,并且 m 对 w比对 自己 的 妻子 更 中意。 则 称这 种 分 配 是 不 稳 定的,否 则 称 为 稳 定 的。 编 写程 序 找出 所 有 稳 定 分 配 方案,进 一 步 找 出 最 稳定 的 分配方案。 表 9.3 男人对女人的偏爱表 等级 1 2 3 4 5 6 7 8 男人序号 1 7 2 6 5 1 3 8 4 2 4 3 2 6 8 1 7 5 3 3 2 4 1 8 5 7 6 4 3 8 4 2 5 6 7 1 5 8 3 4 5 6 1 7 2 6 8 7 5 2 4 3 1 6 7 2 4 6 3 1 7 5 8 8 6 1 4 2 7 5 3 8 · 286· 第九章 程序开发和结构化程序设计 表 9.4 女人对男人的偏爱表 等级 1 2 3 4 5 6 7 8 女人序号 1 4 6 2 5 8 1 3 7 2 8 5 3 1 6 7 4 2 3 6 8 1 2 3 7 4 5 4 3 2 4 7 6 8 5 1 5 6 3 1 4 5 7 2 8 6 2 1 3 8 7 4 6 5 7 3 5 7 2 4 1 8 6 8 7 2 8 4 5 6 3 1 9.40 图 9.53所示是一个迷宫示意图。设计迷宫存储方案,并编写程 序,对 任意给定的 迷宫,给 出从 入口到出口的出路。 9.41 设有三种棋子 a、b、c和五个格的棋盘如图 9.54所示。在每 个格里放一 个棋子,但不允许 相邻 格子 里 放相 同 的 棋 子 。 放满 棋 子 的 棋 盘 称为 一 种 格 局。编 写 程 序 ,输 出 所 有 格 局 。 图 9.53 迷宫 图 9.54 棋盘 9.42  巧 填 数 。 (1)在下述空格内填入 1~9,使等式成立。  2□□□□□□ ÷□ ÷□□2=1997(2963548÷7÷212=1997) (2)在下述空格内填入 1、3~6使等式成立。  □□□□22÷22□ =1997(451322÷226=1997)  □2□2□□ ÷2□2=1997(523214÷262=1997) (3)在 下 述 空 格 内 填 入适当 的 数 使 等式 成 立 。  2□2□2□2÷□2□□ =1997(2528202÷1266=1997)  □7□7□7□ ÷□7□ =1997(1747375÷875=1997) (4)在下述空格内填入 1~8使等式成立。  □□□□66÷□□□ ÷□ =1997(754866÷126÷3=1997) 9.43 在图 9.55所示的圆盘中填入 1~9(其中 9、7已经填上)使等式按顺时针方向相等。 习 题 九 · 287· 图 9.55 圆盘 9.44 改进例 9.2。编写 程序,读入一个保 存在 text文 件上的正文,测定 并输出该正 文中各个字母 偶 出现的次数。例如,输入正文为:thecat,则 th、he、e□ 、□c、ca、at各出现一次。打印两个统计 表,一个表按出现次数递增顺序输出各个 字母偶 及其出 现次数;另 一个表 按字典 顺序输 出各个 字母偶 及 其出现次数。 9.45 在由 n(n≤7)个单个数字组成的 n*n数字点 阵中,每行 元素由 不同的 n个数 字组成。若第 一 行确定一个数字,然后每行选择一个数字,使得所选数字 总合为 n(n+1)/2,求 出所有 这样的 选择。例如, 在图 9.56所示点阵中,斜体标出的选择满足条件。 图 9.56 数字点阵 第十章 文  件 文件是 为了某 种目的 系统地 把数 据 组 织 起来 而 构 成 的 数据 集 合 体。 从实 现 角度 看,文 件往 往与外 部设备 、磁 盘上 的 文件 联 系 在 一 起,也 就 是 与 计算 机 操作 系 统 的文 件 联 系 在 一 起。随着计算 机硬件 技术的 不断发 展,计算机 应用范 围的不 断扩展 ,计 算机应 用 技 术水平 的 不断 提高,人们 往往需 要加工处 理各式 各样的 数据,连接 各种 各 样的 外部设备 。 这 些数据 和 设备 是千差万 别的。 为了处 理的统 一与概 念的简 化,操作系 统把这 些外部 数据 、外 部设备 一 律作 为文件 来管理 。程序 设计语 言中管 理的文件 ,就 是计算 机操作 系统中 的文件。 10.1 文 件 概 述 文件是程 序设计 中的一 个重要 概念,从不 同的角 度看,文 件 可以 分成不同 的类 别。从 操 作角 度看,文件 分为顺 序文件和 随机文 件;从用户 角度看 ,文 件分为 普通文 件和 设备 文 件;从 文件 内部编 码方式 看,文件分为 ASCII文件和二 进制文 件。 1.文件 名 文件名 是文件 的惟一 标识,它的一 般结构 是: 主文件 名.扩展名 其中 的扩展名 可以省 略,但通常 都保留 。因为 通过扩 展名,可 以 判断 文件类型 。 例 如下列 扩 展名:   .c     语言的源程序文件 .txt 文本文件 .doc word文 档 文 件 在文件 名上还 可以附 加磁盘 目录的 路径信息 ,如 :   E:\doc\programing\test.c 表示存 储在磁 盘 E上 doc文 件夹下 programing文件夹 下的 test.c文件。 文件名 分为绝 对文件 名和相 对文件 名。 上 述 从 磁盘 盘 符 开 始 描述 的 文 件 名称 为 “绝 对 文件 名”。而从 计算机 操作系 统中文 件系 统 的某 个文 件 夹开 始描 述的文 件名 称为相 对文 件 名,例如 :   programing\test.c test.c 10.1 文 件 概 述 · 289· 分别 表示相对 于文件 夹 E:\doc的相对 文件名 和相对 于文 件 夹 E:\doc\programing的相对 文 件名。 2.顺序 文件和 随机文件 顺序文件 的特点 是:文件分 成两种 模式———读模 式和写 模式。 在任意 时刻 ,一 个顺序 文 件只 能处于两 种模式 之一。 当一个 顺序文 件处于 读模式 时,只能 从 该文 件读数 据。 反 之,当 一个 顺序文 件处于 写模式 时,只 能 向该 文 件 写 数 据。从 操 作 角 度 看,顺 序 文件 只 能 顺 序 操 作。即对于读 来讲,顺 序文件只 能从文 件第一 成分开 始顺序 地,一个成 分接一 个 成 分地读 数 据。而对于写 来讲,顺 序文件只 能在文 件尾一 个成分 接一个 成分地 向文件 里写 数据,每次 写 进的成分都放在文件末尾。 而随机 文件的 特点是 :对文 件 的 操作 是随 机 的。在 同一 时刻,既 可以 向文 件 中写 ,也 可 以从 文件中读 (文件没 有读 /写模 式之分 )。另外 ,读 /写 操作可 以针对 文件中任 意成分 进行。 例如 ,第 一次读 了第 100个成分 ;然 后再读 第 3个 成分;然后 再用 一 个新 的数据 修 改第 50个 成分 ,将 其写入 第 50个成分 中;然后又 读第 200个成分 ,等等。这 是允许 的,并且是 正确的 。 3.普通 文件和 设备文件 普通文件 是指驻留在 磁盘或其他外部介质 上的一个 有序数 据集,可以是 源程序文 件、目标 程序 文件、可执行程序 文件;也可以是一 组待输入处理的原 始数据,或者是一 组输出的结果。 设备文件 是指与 主机相 联的各 种外部 设备,如显 示器、打印 机、键盘 等。在 操作 系 统中, 把外 部设备 也作为 文件来 进 行 管 理 ,把 它 们 的 输 入、输 出 等同 于 对 磁 盘 文件 的 读和 写。 通 常,显示 器定义 为标准 输出文件 ,键盘是 标准输入 文件。 4.ASCII文件 和二进制 文件 ASCII文 件就是 ASCII码 文件,也称 为文本 文件、TEXT文 件。这 种文件 的每个 字符对 应 一个 字节,用于 存放相 应字符的 ASCII码,也 就是存 放字符 的存储 形态的 编码。字 符 1、2、3、 4的 ASCII码 分别为 49、50、51、52(十 六进制的 31、32、33、34),字符 串“1234”的 存储形 式为: 共占 用 4个字 节。ASCII文 件可 以 在屏 幕上 按 字符 显示,例 如源程 序文 件就 是 ASCII文件, 用 DOS命令 TYPE可以 显示文 件的内 容。由 于是按 字符显示 ,因此能 读懂文件 内容。 二进制 文件就 是二进 制码 文 件 ,它 把 数 据 按 二进 制 编码 方 式 存放 到 文件 中 。 例 如,数 1234的 存储形 式为:   0000010011010010 只占 二个字 节。用 TYPE命令 显示二 进制文件 是无意 义的,其内 容无法 读懂。 5.流式 文件 C系统的 文件操 作不区分 文件类 别,不论顺 序 文件 还是 随机文 件、普 通文 件还 是设备 文 · 290· 第十章 文  件 件、ASCII文件还 是二进 制文件 ,C系 统把文 件一律 都 看成 是 “字 节流”,以字 节 (每个 字节 可 能是 一个字符 ,也可能 是一个二 进制代 码)为单 位进行 操作处 理。而 不像有的 程 序 设计语 言 (Pascal)那 样,以 记录为 单位对 文件进 行操作 。对字 节流的操 作,其输 入 /输 出的开 始和结 束 都由 程序控 制,不受物 理符号(如 回车符 )的影响 。把这 种文件 操作方 式称为“流 式文件 ”。 6.文件 指针 C系统为 了处理 文件,为每个 文 件 在 内 存中 开 辟 一 个 区域 ,用来 存放 文件 的 有关 信息, 如文 件名、文件 状态以 及文件当 前位 置等 。这 个 区 域 被作 成 一 个 称 为 FILE类 型 的结 构体。 FILE的类型 由系统 定义,保存在 头文 件 stdio.h中,它 的具 体结 构暂时 不用 关心。 C程序 中 用指 向 FILE类 型变量 的指针变 量(简称 “文件指 针”)来标 识具体 文件。 声明     FILE* fp; 声明 了一个 文件指 针变量 fp,以后 fp可以用来 标识具 体文件 。 7.标记 C文件是 一个流 式文件,在该 字节流 上有一 个 隐含 的暗 标记,该标 记 总是 指向 文件中 正 要操 作的字 节,即下一 个字节,称 该标记 为文件读 /写 位置指 针。例 如:   □ □□ … □.        指向文 件首,即指 向第一 个字节 ;  ↑    □□□ … □. 指向第四个字节;    ↑ □□□ … □□. 指向文件尾;        ↑ 8.几个 常量 C系统引 进几个 常量标志 文件处 理状态 。 最 常 用的 是 EOF和 NULL,它们 是 stdio.h中 预定义的常量。 EOF:值 为“-1”,习惯上表 示文件 结束或 文件操 作出错 。 NULL:值 为“0”,习 惯上表示 打开文 件失败 等。 10.2 文 件 操 作 C语言没 有文件 操作语句 ,C文 件操作全 部通 过系 统定义 的库函 数 来 实现。所 谓“库 函 数”是 指系统已 经定义 好的,存放 在“函数 库”文 件内的 ,可 以被 用 户 直 接调 用 的 函 数。这 些 库函 数根据其 功能的 不同,存放 在不同 的函数 库中。 库函数 本身并 不属于 语言 ,它 是系统 根 据需 要提供 给用户 使 用 的函 数。C标 准 定 义了 常 用 的 函 数库 和 每个 函 数 库中 常 用 的 库 函 10.2 文 件 操 作 · 291· 数。 但是不 同的编 译系统 提供的 函数 库 不 同 ,不 同编 译系 统 在每 个函 数 库 中 提 供的 库函 数 也不 同。为 了提高 程序的 可移植 性,用 户 应 该只 使 用 C标 准定 义 的函 数库 和 库函 数。对 应 每个 函数库,有 一个头 文件,在头文 件中包 含相应 函数库 中所 有 函数 的函数原 型。 用户使 用 库函 数时,需要 把相应 的头文件 用 #include命令 括入 到自己 的程 序文 件 中。 文 件操 作函 数 库的 头文件 是“stdio.h”,所以 在用户 程序中 只要涉 及文件 操作,具体 说只要 涉及输 入 /输出, 就应 该把该 文件括 入程序 ,使用 的程序 行是:   #include"stdio.h" 这就是为什么前述各个章节的程序都含有这一行的原因。 任何高 级语言 ,对 文件操作 都应 该遵 循:打 开文 件→ 操作文 件→ 关闭 文件 这 样的 过程。 下面就遵循这个规则对文件的操作进行说明。 10.2.1  打开、关闭文件 1.打开 文件 打开文 件使用 函数 fopen。 fopen的 函数原 型是:   FILE* fopen(constchar* filename,constchar* mode); 它的调用方式是:   fp=fopen(filename,mode); 其中 filename是 一个字 符串,具体给 出要打 开的文 件的文 件名; mode也是 一个字 符串,具体 给出文件 的打开 模式,表 10.1列出各 种打开 模式; fp是文件 指针变 量,以后程 序中使 用该指 针变量 标识由文 件名给 出的文 件。 fopen的功能 是根据 mode指定 的模式 ,打 开由 filename指定 的文件 。 指 向文件 filename的 文件指 针, 操作 成功 返回值 = NULL, 出错 例如:   fp=fopen("c:\user\file.txt","r"); 以“只 读”方式打 开 c盘 user结点下 的 file.txt文件 ,如 果成功 ,则 fp就是文 件 file.txt的文 件 指针 变量,并且 只允许 对文件进 行读操 作;否则 fp的值是 NULL。 序号 1 2 mode “r” “w” 表 10.1 文件打开模式 含义 以只读方式打开一个 ASCII文件 以只写方式打开或新建一个 ASCII文件,原有文件内容全部删除 · 292· 序号 3 4 5 6 7 8 9 10 11 12 第十章 文  件 mode “a” “r+” “w+” “a+” “rb” “wb” “ab” “rb+” “wb+” “ab+” (续 表 ) 含义 同 “w”,但是 不 删 除 原有 文 件 内 容 以可读可写方式打开一个 ASCII文件 以可读可写方式打开或新建一个 ASCII文件,原有文件内容全部删除 同 “w+”,但 是 不 删 除原 有 文 件 内 容 以只读方式打开一个二进制文件 以 只 写 方式 打 开或 新 建一 个 二 进 制 文 件 ,原 有 文 件内 容 全部 删 除 同 “wb”,但 是 不删除 原 有 文 件内 容 以可读可写方式打开一个二进制文件 以 可 读 可写 方 式打 开 或新 建 一 个 二 进 制 文件,原 有文 件 内容 全 部删 除 同 “wb+”,但 是不删 除 原 有 文件 内 容 2.关闭 文件 任何文 件在操 作结束 之后,都应该 执行文 件关闭 操作。 关闭文 件使用 函 数 fclose。fclose 的函数原型是:   intfclose(FILE* stream ); 它的调用方式是:   fclose(fp); 其中 fp是 文件指针 。fclose的 功能是 关闭由文 件指针 fp指向 的文件 。 0, 操作成 功 返回值 = EOF, 出错 10.2.2  字符读 /写 读 /写字符 (或字节 )数据时,选用 fgetc()和 fputc()函 数。 1.读字 符:fgetc (1)函 数原型 :intfgetc(FILE* fp); (2)功能 :从 fp指向 的文件 中读取 一个字 符,同 时将读 /写 位置指 针向前 移动 1个 字节。 读取字 符的 ASCII码, 操作 成功 (3)返 回值 = EOF, 出错或遇到文件结束 2.写字 符:fputc (1)函 数原型 :intfputc(intch,FILE* fp); (2)功 能:把字符 ch写 入 fp指 向的文件 ,同时将 读 /写 位置指 针向前 移动 1个字节 。 所写字 符的 ASCII码, 操作 成功 (3)返 回值 = EOF, 出错 10.2 文 件 操 作 · 293· 10.2.3  字符串读 /写 读 /写字符 串时,选用 fgets()和 fputs()函数。 1.读字 符串:fgets (1)函 数原型 :char*fgets(char* str,intnum,FILE* fpointer); (2)功 能:从 fpointer指向的 文件中 读取一个 字符串 ,并 将此 串 保存在 str指 向 的字符 数 组中 。字符串 的自然 结束符 是“换行 符”和“文 件结束 符”。若读 到 num-1个 字 符 后还没 遇 到结 束符,则也 强制结 束,这时把 num-1个 读 入的 字 符 送 入数 组 str中。 读入结 束后 ,在 数 组 str的 字符串 末尾加 字符串终 止字符 NULL,并将 文件读 /写 位置指 针向前 移动实 际读取 的 字节个数。 str所指的 字符数 组首地址 , 操作 成功 (3)返 回值 = NULL, 否则 2.写字 符串:fputs (1)函 数原型 :int*fputs(char* str,FILE* fpointer); (2)功 能:把 str所指 字符串 (不 包括字 符串结 束符 NULL)写入 fpointer指 向的文 件,同 时将 读 /写位置 指针向 前移动 num(字符 串长度)个 字节。 非负值, 操作成功 (3)返 回值 = EOF, 否 则 10.2.4  数据块读 /写 读 /写多个 不含格 式的数据 块时,选 用 fread()和 fwrite()函 数。 读数据 块:fread (1)函 数原型 :intfread(void*buf,intsize,intcount,FILE*fp); (2)功 能:从 fp所指 的文件 中 读 取 count个字 段,每 个字 段 为 size个 字节,把它 们送 到 buf所指 的缓冲 数组中 ,同 时,将 读 /写位 置指针 向前 移动 size* count个 字 节。一 般来 讲,数 组 buf每 个元素的 尺寸为 size,每 个字段 正好对应 数组 buf的 一 个 元素,即读 入 count个字 段 送入 数组 buf的 count个 元素中 。 实际读取的字段个数, 操作成功 (3)返 回值 = EOF, 错误或文件结束 写数据 块:fwrite (1)函 数原型 :intfwrite(void*buf,intsize,intcount,FILE *fp); (2)功 能:从 buf所指的 数组中 ,把 count个 字段写 到 fp所 指 的 文件中 ,每 个字 段为 size · 294· 第十章 文  件 个字 节,同时,将读 /写 位置指针 向前移 动 size*count个 字节。 一 般来 讲,数组 buf每 个元 素 的尺 寸为 size,每个字 段正好 对应数 组 buf的一个 元素,即 把数组 buf的 count个 元 素写到 文 件中。 实际写入的字段个数, 操作成功 (3)返 回值 = 其他, 操作错误 10.2.5  格式化读 /写 读 /写多个 含格式 的数据时 选用 fscanf()和 fprintf()函数。 函 数 fscanf()和 fprintf()与 函数 scanf()和 printf()的 功能 相 似 ,区 别 在于 函 数 fscanf()和 fprintf()操 作对 象 是 一 般 文 件,而 scanf()和 printf()操作对 象是标 准输入 /输 出文件 。格式 化读 /写是 把数据按 fscanf() 和 fprintf()函数 中格式 控制字 符串中 控制字 符的要 求 进 行转 换 ,然 后再进 行读 /写 。格式 转 换在 第三章 的 3.8节 已经介 绍过,这里 不再赘 述。 1.格式 化输入 :fscanf (1)原 型:intfscanf(FILE * fp,char* format,arg_list); (2)功 能:从 fp所指 文件,按 format规定 的格式 进行转 换,读取 arg_list对应的 数据。 实际读取的参数个数, 操作成功 (3)返 回值 = 一个负数, 否则 2.格式 化输出 :fprintf (1)原 型:intfprinft(FILE* fp,char* format,arg_list); (2)功 能:将 arg_list内 的各参 数值按 format格 式进行 转换,输出 到 fp所指 的文件 。 实际写入的参数个数, 操作成功 (3)返 回值 = 一个负数, 否则 10.2.6  文件定位 读者已 经知道 ,C文件 是一个 流式文 件,在该字 节流上 有一 个 隐含的 暗标 记 (文件 读 /写 位置 指针),该标 记总是 指向文件 中正要 操作的 字节。 (1)当 以读模 式(“r”)打开 文件时 ,文件 读 /写位 置指针指 向文件 开始; (2)当 以写模 式(“w”)打开 文件时 ,文件 读 /写位置 指针指 向文件 开始; (3)当 以追加 写模式 (“a”)打开 文件时,文 件读 /写位置 指针指 向文件 尾; (4)当 以各种 随机模 式(“a+”、“w+”、“a+”)打 开文 件 时 ,文 件 读 /写 位 置指 针指 向 文件开始。 在对文件 进行任 何读 /写操 作时,位置 指针都 自动向 下移 动 相应 个数的字 节。 如果要 打 破这 种规律,就 必须使 用定位函 数对位 置指针 重新定 位。函 数 rewind和 fseek用于 位置指 针 10.2 文 件 操 作 · 295· 定位 ;函 数 ftell和 feof用于测试 文件位 置指针 当前所 处位置 。 1.rewind()函数 (1)原 型:voidrewind(FILE*fp); (2)功 能:使 fp所指 文件的 位置指 针重新 指向文 件开始。 (3)返 回值:无。 2.fseek()函数 (1)原 型:intfseek(FILE*fp,longoffset,intorigin); (2)功 能:使 fp所指 文件的 指针指 向 origin+offset的 位置。 0, 操 作成功 (3)返 回值 = 非 0, 否 则 其中 (1)origin指的是 起始位置 ,表 10.2给出 了 C定义的 表示起 始位置 的 3个 宏。 (2)offset是指 相对于 初始位 置移动 的字节 数。当 偏 移量 是正 数时,从初 始位置 向前 移 动 offset个字节 ;当偏移 量是负数 时,从初始 位置向 后退 offset个字节 。 起始位 置 (origin) 文件开始 文件当前位置 文件结尾 表 10.2 表示起始位置的宏 宏定义 SEEK_SET SEEK_CUR SEEK_END 数字代表 0 1 2 3.ftell()函数 (1)原 型:longintftell(FILE*fp); (2)功 能:给出 fp所 指文件 的位置 指针当 前所处 位置。 位置指针值, 操作成功 (3)返 回值 = -1(EOF), 否则 4.feof()函 数 (1)原 型:intfeof(FILE*fp); (2)功 能:fp是一 个输入 流,标志是 否“读”到 fp所指文 件末尾 ,即 文件是 否结束 。 非 0(EOF), 若读 操作后,文件暗标记(文件读 /写位置指针)到 达文件尾 (3)返 回值 = 0, 否则 · 296· 第十章 文  件 10.3  文件 操作 实例 例 10.1  实现文 本文件 复制的 功能。   #include <stdlib.h> #include"stdio.h" voidman(intargc,char*argv[]){   * 执 行 方 式 :可 执行文件名 源文件名 目标文件名 */ FILE* input; /* 源 文 件 指 针 */ FILE* output; /* 目标文件指针 */ charch; if(arg!=3){ /* 参 数 个 数 不 对*/ printf("thenumberofargumentsnotcorrect\n"); printf("\nUsage:可执行文件名 source-filedest-file"); exit(0); /* 退 出 */ } if((iput=fopen(argv[1],"r"))==NULL){ /*打开源文件失败*/ printf("cannotopensourcefile\n"); exit(0); } if((otput=fopen(argv[2],"w"))==NULL){ /*创建目标文件失败*/ printf("cannotcreatedestinationfile\n"); exit(0); } while !feof(input)){ /* 复 制 源 文 件 到目 标 文 件 中 */ fputc(fgetc(input),output); } fclose(input); /* 关 闭 源 文 件 */ fclose(output); /* 关 闭 目 标 文 件*/ } 例 10.2  创建某 文本文 件的副 本,要 求副本 文件要 有行号 。   #include <stdlib.h> #include"stdio.h" #defineSIZE256 voidman(intargc,char* argv[]){ FILE* input; FILE* output; charch; intline=1; if(arg!=3){ /* 执行方式:可执行文件名 源文件名 目标文件名 */ /* 源 文 件 指 针 */ /* 目标文件指针 */ /* 参 数 个 数 不 对*/ 10.3  文 件操 作 实例 · 297· printf("thenumberofargumentsnotcorrect\n"); printf("\nUsage:可执行文件名 source-filedest-file"); exit(0); /* 退 出 */ } if((iput=fopen(argv[1],"r"))==NULL){ /*打开源文件失败*/ printf("cannotopensourcefile\n"); exit(0); } if((otput=fopen(argv[2],"w"))==NULL){ /*创建目标文件失败*/ printf("cannotcreatedestinationfile\n"); exit(0); } /* 复制 */ fprintf(output,"%5d",line); /* 写 入 第 一 行 行号 */ while(( h=fgetc(input))!=EOF){ fputc(ch,output); /* 写入当前字符 */ if(ch= \n||ch== \r) fprintf(output,"%5d",line++ );    /*写入行号,行号增 1*/ } fclose(input); /* 关 闭 源 文 件 */ fclose(output); /* 关 闭 目 标 文 件*/ } 还可以 使用 fgets和 fputs字符 串 I/O函数,编出 更简洁 的程序 。假设 源文件 每行最 长不 超过 256个字 符。编 写程序 如下(省 略了与 前一个 程序相 似的部分 ):   ⁝ #defineSIZE256 void ain(intargc,char*argv[]){ charbuf[SIZE]; ⁝ while(fgts(buf,SIZE,input)!=NULL){ fprintf(output,"%5d",line++ ); fputs(buf,output); } ⁝ } /* 复 制 源 文 件 到目 标 文 件 中 */ /*写入行号,行号增 1*/ /* 写 入 字 符 串 */ 例 10.3  编写一 个函数 ,合并 两个已 按递增 排序的 整数文 件成一 个按递增 排序的 文件。 PAD图如 图 10.1所示 。 程序:   #include <stdlib.h> #include"stdio.h" · 298· 第十章 文  件 图 10.1 合并文件 /*执行方式:可执行文件名 已排序源文件名 已排序源文件名 目标文件名*/ void ain(intargc,char*argv[]){ FILE*f1; /*已排序整数源文件 1指针*/ FILE*f2; /*已排序整数源文件 2指针*/ FILE*f3; /* 合并后的目标文件指针 */ intv1,v2; if(arg!=4){ /* 参 数 个 数 不 对*/ printf("thenumberofargumentsnotcorrect\n"); printf( "\nUsage:可执行文件名 source-filesource-filedest-file"); exit(0); /* 退 出 */ } if(( 1=fopen(argv[1],"r"))==NULL){ /*打开源文件 1失败*/ printf("cannotopensourcefile\n"); exit(0); } if(( 2=fopen(argv[2],"r"))==NULL){ /*打开源文件 2失败*/ printf("cannotopensourcefile\n"); exit(0); } if((f =fopen(argv[3],"w"))==NULL){ /* 创 建 目 标 文 件失 败 */ printf("cannotcreatedestinationfile\n"); exit(0); } fread(&v1,sizeof(int),1,f1); fread(&v2,sizeof(int),1,f2); while( feof(f1)&& !feof(f2)){ if(v1 < 2){ /* 取较小元素存入 f3文件 */ fwrite(&v1,sizeof(int),1,f3); 10.3  文 件操 作 实例 · 299· fread(&v1,sizeof(int),1,f1); }else{ fwrite(&v2,sizeof(int),1,f3); fread(&v2,sizeof(int),1,f2); } } while(! eof(f1)){ fwrite(&v1,sizeof(int),1,f3); fread(&v1,sizeof(int),1,f1); } while(! eof(f2)){ fwrite(&v2,sizeof(int),1,f3); fread(&v2,sizeof(int),1,f2); } fclose(f1); fclose(f2); fclose(f3); } /* 处理 f1文件尾部 */ /* 处理 f2文件尾部 */ /* 关 闭 文 件 */ 例 10.4  在磁盘 中建立 一个正 弦 函数 表 文 件 “sin.tab”,其 格 式如 下(角 度 a的 取值 范 围是 0°~359°,由于 篇幅限 制,只 列出部 分角度 及其正 弦值): THELISTOFSIN(X) a SIN(a) a SIN(a) a SIN(a) a SIN(a) a SIN(a) 0 0.0000 1 0.0175 2 0.0349 3 0.0523 4 0.0698 5 0.0872 6 0.1045 7 0.1219 8 0.1392 9 0.1564 ⁝ ⁝ ⁝ ⁝ ⁝ ⁝ ⁝ ⁝ ⁝ ⁝ 算法如 图 10.2所示 ,程 序如下 。   #include <stdlib.h> #include <math.h> #include <stdio.h> 图 10.2 正弦函数表 · 300· 第十章 文  件 #definePAI3.14159 void ain(){ intu,v; FILE*f; if(( =fopen("sin.tab","w"))==NULL){          *打开文件*/ printf("cannotopenfile\"sin.tab\"\n"); exit(0); } fprintf(f,"%40cTHELISTOFSIN(X)\n", ); /* 表头 */ fprintf(f,"aSIN(a)aSIN(a)aSIN(a)"); fprintf(f,"aSIN(a)aSIN(a)\n"); for( =0;v<=71;v++ ){ /* 表体 */ for(u=0; <=4;u++ ) fprintf(f,"%5d%6.4f",v* 5+u,sin((v* 5+u)*PAI/180)); fprintf("\n"); } fclose(f); } 例 10.5 设 磁盘上 有两个 text文件 ,NAME.DAT上为 一个人 员名单 ;ADDRESS.DAT上 是对 应 NAME.DAT文 件上每 个人的 家 庭 地 址。 编写 一个 程 序,在磁 盘上 生成 一 个姓 名、地 址、电 话 号 码 表 文 件 NAMEADDR.TAB,其 中 每 个 人 的 电 话 号 码 在 终 端 上 随 机 录 入 ,如 图 10.3所示。 图 10.3 姓名、地址、电话号码表 该程序 总体上 应该一 个人一 个 人地 处 理 ,对 每 个 人来 讲 :先 从 NAME.DAT上读 入一 个 姓名 ;然 后从 ADDRESS.DAT上读入 相应家 庭地址 ;然 后在终 端屏幕 上显示正 处 理 的人员 姓 名,要求 操作员 键入其 电话号码 ,并 读入 该 电 话 号码 ;最后 把 姓 名 、地 址、电话 号 码作 为一 行 送入 文件 NAMEADDR.TAB上。   #include <stdio.h> #include <stdlib.h> 习 题 十 · 301· void ain(){ FILE*name; /* 名字 源 文 件 指 针 */ FILE*address; /* 地址 源 文 件 指 针 */ FILE*nameaddr; /*目标文件指针 */ charname0[8],addr[30],tel[10]; if((nam =fopen("NAME.DAT","r"))==NULL){ /* 打 开 名字 源 文 件 失 败 */ printf("cannotopensourcefile NAME.DAT \n"); exit(0); } if((addess=fopen("ADDRESS.DAT","r"))==NULL){ /*打开地址源文件失败*/ printf("cannotopensourcefile ADDRESS.DAT \n"); exit(0); } if((nam addr=fopen("NAMEADDR.DAT","w"))==NULL){ /*创建目标文件失败*/ printf("cannotcreatedestinationfile NAMEADDR \n"); exit(0); } while(! eof(name)&&!feof(address)){ /* 控制 全 部 处 理 */ /* 控制读 */ fscanf(name,"%8s",&name0); /* 读入姓名 =>name0*/ fscanf(address,"%32s",&addr);/* 读入地址 =>addr*/ printf("name% spleaseinputtel:",name0); /* 输出 提 示 信 息 */ scanf("% s",&tel); /* 终端输入电话号码 =>tel*/ /* 姓名、地址、电话号码写入文件 NAMEADDR.DAT一行 */ fprintf(nameaddr,"% 12s%32s%10s\n",name0,addr,tel); } fclose(name); fclose(address); fclose(nameaddr); } 本章小结 本章主 要介绍 了文件 的概念 及其操 作。重点 掌握文 件打开 、关 闭、读 /写等操作 。 习 题 十 10.1 什么是文件指针?什么是文件读 /写位置指针?它们之间有什么区别? · 302· 第十章 文  件 10.2  打 开 、关闭 文 件 的 含义 是 什 么 ?为什 么 要 打 开 和 关 闭文 件 ? 10.3  编 写 一 个 统计 文 本 文 件中字 符 个 数 的 程 序 。 10.4  编 写 一 个 统计 文 本 文 件中行 数 的 程 序 。 10.5 统计某给定的 ASCII文件中各字母的出现频率。 10.6 一个文件保存整数,修改该文件,使偶数加 2,奇数乘 2。 10.7  编 写 程 序 ,分 别 求 出给 定 整 数 文 件 上等 于 及 大 于 某 给定 整 数 值 的元素 个 数 。 10.8  编 写 程 序 ,把 给 定 整数 文 件 上 所 有 大于 某 值 的 数 拷 贝到 另 一 个 给定文 件 上 去 。 10.9 分解实数文件 f到 g1、g2。g1保存所有小于 f中平均值的数,g2保存其他数。 10.10 分解整数文件 f到 g1、g2。g1保存所有素数,g2保存其他数。g1、g2每行五个数。 10.11 编写程序,判断任意给定的两个 ASCII文件是否相等。 10.12 编写程序,把 ASCII文件 f的所有奇数行拷贝到文件 g中去。 10.13 编写程序,把 ASCII文件 f的所有单词 bad改为 good。 10.14 编写程序,把给定的不带行号的 ASCII文件加上行号。 10.15 编写程序,把给定的带行号的 ASCII文件的行号删除。 10.16 编写程序,分别计算给定 ASCII文件的行数以及行长度的平均值、最大值和最小值。 10.17 文件 f1、f2、f3 是按字典顺序排列的名字表文 件(每个名字最 长 10个字符)。编写程 序,求第 一 个在 三 个文 件 上 都 出 现 的 名字(对 每 个文 件 至 多 扫描一 次 )。 10.18 格式化给定 ASCII文件。每行最多 50字符,每页 40行,在每页顶端加页号。 10.19 设有按递增排序的实数文件 f,其中数据个数未知,对 f的操作只允许从头 读到尾。编写程序, 把文件 f中数据按递减排序存入文件 g中。(分别考虑两种 情况:内存可 以放下 f中 数据;内存放 不下 f中 数据 )。 10.20 编写程序,把 ASCII文件 f的所有行变成中间对齐。设每行不超过 41个字符。 10.21 ASCII文件 g1、g2每行最长不超过 30字符,并列打印该两个文件(g2从第 40列开始打印)。 10.22 设计 C程序印刷格式,编写一个 C程序格式化 程序,该程 序读入 任意格 式的 C程序,按标 准 格式打印读入的 C程序。 10.23 ASCII文 件 记 录了 一 些 书 名 ,每 个 书 名 占一 行。 编 写 程 序,按 给定 关 键 字 检 索 ,列 出 含 给 定关 键 字的书名,且把给定关键字列在一列上。例关键字为 programming时得如下结果:      computer rogrammingforengineers programminginpascal    systematic rogramming programmingandprogramming programmingand programming 10.24 编写程序,处理一个只由字母、逗号、句号、空格、换行组成的 ASCII文件。从键盘 输入文件名, 统计并输出结果:文件中包含单词总数、不同单词个数、按字典顺序排列的各个单词及其出现次数。输 入一 个单 词 ,判 断 该单 词 是 否 在 文 件 中出 现 。 10.25 两个 给定的复数文件(复数以相邻的实部、虚部两个实数偶的形式给出)均已按 模递增方式排 序。 编 写程 序 ,合 并 该 两 文 件 为 一个 仍按 递 增 排 序的 复 数 文 件 。 10.26 已知一个仓库文件保存如下信息:物品代码、仓库编号、单价。编写程序,把给 定仓库文件 的前 习 题 十 · 303· 100个 记录 按递 增 排 序 ,其 余 记 录 不 变 。 10.27  在文 件 上 实 现 冒 泡分 类 。 10.28  在文 件 上 实 现 归 并分 类 。 给定一个整数文件 A,要求把它按递增顺序重排。可以如下实现: (1)引进两个文件 B、C; (2)把 A分成两等分放入 B、C上; (3)把 B、C归并到 A,归并原则是把 B、C上对应元素合并成有序偶放到 A上。 (4)重复(2)、(3),这次是把 B、C上对应两个元素合并成有序四元组放到 A上。 (5)继续不断重复(2)、(3),每次都把 B、C上对应元素个数及合并的有序组长 度加倍,直到 A排好 序为止。 上述算法中的(2)对文件进行了一次复写工作,但没有对排序作出贡献。改进 上述算法,以减少 对文 件的复写工作。 10.29  假设 每 个 学 生 记 录的 数 据 结 构如下 :    tuctstudent{ intno; char* name; intage; boolsex; } 有 文件 stu.txt保存 M 个学生记录,对此文件中的 M个数据按学生学号从大到小排序存放在原文件中。 10.30 若有两个文件 st1.txt和 st2.txt分别保存 M、N个未排序的学生记录。编写程序,将两个文件按 照学生学号从大到小合并放在一个新文件中,并在屏幕上显示出“年龄 >11”的学生姓名。 10.31 编写程序,按不同方法处理给定 ASCII文件,使其每行的长度都等于 50:    (1)每 行 字 符 不 足者补 空 格 ,多 余者 截 掉;    (2)重 新 安 排 换 行,不 改变 原 来 有效 字 符。 10.32 编写程序,把给定全部保存实数的二进制文件上的实数翻译成字符形 式,存入 给定的 ASCII文 件上。要求每行 5个数,每个数占 20位,保留 5位小数。 10.33 给定单词转换对照表 A、B,编写程序对 ASCII文件 T进行转换,结果存入 ASCII文件 S中。转 换规则是,把 T中所有在表 A中出现的单词用表 B中相对应的单词替换(Aj用 Bj 替换)。 10.34 行编辑程序:编写程序,对给定的 ASCII文件进行编辑,编辑结果送入另一 个给定的 ASCII文 件中去。编辑的含义是删除或替换一些行;在某行后插入若干行;复制某些行到另一行后,等等。编辑 过程 由终端键盘输入的如下编辑命令控制:    ,m,<欲插入的行 ><Ctrl-Z>      在第 m行后插入 <欲插入的行 > d,m,n. 删除第 m行到第 n行 r,m,n,<欲替换的行 ><Ctrl-Z> 把第 m行到第 n行替换成 <欲替换的行 > c,m,n,j. 把第 m行到第 n行拷贝至第 j行后 e. 结束编辑 10.35 由 BNF · 304· 第十章 文  件   E ->d|* (E,E)|+ (E,E) 定义的表达式称为前缀表达式,其中 d是数字字符。现在定义:    (d)=d β(+ (E,E))=(β(E),β(E) +) β(* (E,E))=(β(E),β(E)*) 设运算符不超过 50个,试编写一个函数,从 ASCII文件读入 上述 BNF定义的 前缀表达式,输出 经过 β变换的 表 达 式 。 第十一章 结构体与共用体 世界上的 事物是 复杂的 ,计 算机处 理的数 据也是 各种各 样的。 现在考 虑这 样一 类 数据: 一个 数据项由 多个子 数据项 组成,而且 每个子 数据项 的类型 可能 不 一样。 例如 ,在 人事档 案 管理 中,每个人 的自 然 情 况 表 可能 包 含 姓 名 (字 符 串 型 )、年龄 (整 型 )、出生 时 间 (三 个 整 型)、性别 (枚举),等等。 姓名: 年龄: 出生时间  年  月  日 性别: 再如,研究 人造卫 星,每个人 造卫星 的信息可 能包 括:名 字(字 符型 )、发 射 时间 (三个 整 数)、重量 (实型)、直径(实 型)、轨道半 径(实型)与 赤道夹 角(实型 ),等 等。 名字 重量 发射时间     年   月    日 直径 轨道半径 与赤道夹角 使用结构体可以描述这一类由不同类型子数据项组成的数据。 11.1 结  构  体 11.1.1  结构体类型 结构体 类型 (structure)是 分 量 (components)的 集 合。 分 量 也 称 成 员 (members)、成 分 (element)、域(fields),分量类 型可以 不同。 有关结 构体类 型的语法 如下:    <结构体类型说明符 >→ <结构体类型引用 >x <结构体类型定义 > <结构体类型引用 >→struct<结构体标签 > <结构体类型定义 >→struct <结构体标签 > { <字段列表 > } <结构体标签 > → <标识符 > <字段列表 > → <成员声明 > x <字段列表 ><成员声明 > <成员声明 > → <类型声明符 ><成员声明符列表 > ; <成员声明符列表 >→ <成员声明符 > x <成员声明符列表 >,<成员声明符 > <成员声明符 > → <简单成员 > x <位字段 > <简单成员 > → <声明符 > <位字段 > → <声明符 > :<宽度 > <宽度 > → <常量表达式 > · 306· 第十一章 结构体与共用体 按这个 语法,不考 虑位字段 ,“结 构体类 型定义 ”呈如下两 种形式 之一。   结构体类型定义形式 A   结构体类型定义形式 B   strut{ tid,… ,id;    ⁝ tid,… ,id; }    struc sid{ tid,… ,id;   ⁝ tid,… ,id; } 其中: (1)struct是 保留字 ,引 导一个 结构体 类型定 义。 (2)每个 t是 一个类 型说明符 ,可以是任 意类型 任何形 式的类 型说明 符。它 说明后 边诸 标识 符 id的 类型。 (3)每个 id是一 个成员 声明符 ,具体 声明结 构体类 型的一 个分量 ,它 最终涉 及的标 识符 是该 分量的 名字;要求 在整个结 构体类 型定义 内,诸 id中声 明 的各个 分量 的名 字互不 相同; 每个 id的类 型是它 前边的 t表记 的类型 。 (4)sid是一 个标识 符,称 结构体 标签,起标 记该结 构体类 型的作 用。 例 11.1  一个人 的自然 情况表 及卫星数 据类型 可以说 明成:     enum sext{male,female}; structdat { intyear,month,day; }; structpreon{ charname[10]; intage; enum sextsex; structdatebirthdate; }; structmanstellite{ charname[10]; structdatelounchdate; floatweight,diameter,orbitrad,angle; }; 从结构体 类型定 义的语 法可以 看出,与其 他构造 型类型 一样,结构 体类型 定 义 也可以 嵌 套。 在例 11.1中 : (1)date是一个 结构体类 型,包含 三个成分 。结构 如图 11.1所示。 (2)preson有四个 成分,成分 name为数 组类型 ,成 分 age为 int类型,成分 sex为枚举 类 型,成分 birthdate仍为 一个结构 体类型 。结构 如图 11.2所示。 (3)mansatellite有六个 成分。 结构如 图 11.3所 示。 11.1  结   构   体 · 307· 图 11.1 date 图 11.2 preson 图 11.3 mansatellite 在 struct后跟 以结构 体标签,称为 “结构体 类 型引 用”。在 例 11.1的结构 体类型 定义 的 意义下   structdate structpreson structmansatellite 都是 结构体 类型引 用,使用它们 将分别 标记相 应结构 体定义 。 结构体类 型定义 和结构 体类型 引用统 称“结构 体类型 说明符”。 使用结 构体类 型说明 符 可以 定义结 构体类 型的类 型名,还可以 声明结 构体类 型变量 。 11.1.2  结构体类型 名 与一般 类型一 样,使用 typedef可 以定义结 构体类 型名,形式 是     typedef  结 构 体 类 型说 明 符   标 识 符 按这个 形式,在上 节结构体 说明的 基础上 ,例 11.2是一 些正确 的结构 体类型名 字定义 。 例 11.2  定义结 构体类 型名。     typedefstruct ate{ intyear,month,day; }datetype; typedefstrct{ tstring10name; intage; enum sextsex; structdatebirthdate; }    presontype; typedefstructmansatellitemansatellitetype; 第一个 类型定 义使用 完整形 式的结 构体类型 定义,定义 类型标 识符 datetype; 第二个 类型定 义使用 不带结 构标签 的结构体 类型定 义,定义类 型标识 符 presontype; · 308· 第十一章 结构体与共用体 第三个 类型定 义使用 结构体 类型引 用,定 义类型 标识符 mansatellitetype。 11.1.3  结构体变量 结构体类型变量声明可以采取如下三种形式之一。 (1)使 用结构 体类型 引用; (2)直 接使用 结构体 类型定 义; (3)使 用 typedef定义 的结构 体类型 名。 在前两 节类型 定义基 础之上 ,例 11.3声明了 四个结 构体变量 。 例 11.3  结构体 变量声 明。   structpresonzhang; struct ate{ intyear,month,day; }dateofbirth; struct charauthor[10]; datetypepublish_date; intpage_number; }programming; mansatellitetypefirst_east; (1)变 量 zhang用结 构体类 型引用 声明,是 structpreson类 型,具有 图 11.2所示的 结构; (2)变 量 dateofbirth使用完整 的结构 体类型 定义声 明,具有图 11.1所示的 结构; (3)变 量 programming使 用不 带结 构 体 标 签 的结 构 体类 型定 义声 明,包 含 3个 成分,分 别 为 字 符 数 组 类型 的 author、 datetype类型的 publish_date、int类型 的 page_number,结 构如 图 11.4所 示; (4) 变 量 first_east使 用 typedef定 义 的 类 型 标 识 符 mansatellitetype声明,具有 图 11.3所示 的结构 。 不要忘 记,类型定 义不分配 存储空 间,只说 明一 个数据类   图 11.4 programming结构 型的框架结构。只有到变量声 明时才给变量分 配 存储空 间, 并且 使得被 声明的 变量具 有相应 类型的 结构 。也 就 是 说 ,到 目前 为止 本 章 定 义 的所 有标 识 符只 有本节声 明的四 个变量 zhang、dateofbirth、programming、first_east具有 实体,被分 配存 储 空间 。类型标 识符只 是定义 了一个 数据类 型的框 架,不占用 存储空 间,只给相 应 类 型起一 个 名字。 11.1  结   构   体 · 309· 11.1.4  指向结构体 变量的指 针 可以定义 指向任 何类型 的指针 类型,并声 明相应 指针类 型的变 量,结构体 类 型 当然不 例 外。 在前 几 节 声 明 的 基 础 上 ,例 11.4声 明 了 4个 指 向 不 同 结 构 体 类 型 变 量 的 指 针 变 量 pointer_preson、dateofpointer、p、p_east。 例 11.4  指向结 构体类 型变量 的指针变 量。     structpreson* pointer_preson; struct ate{ intyear,month,day; }* dateofpointer; struct charauthor[10]; datetypepublish_date; intpage_number; }* p; mansatellitetype* p_east; 该声明片段声明: (1)变 量 pointer_preson为指向 标签为 preson的结 构体类 型变量 的指针 变量; (2)变 量 dateofpointer为 指向标 签为 date的结构 体类型变 量的指 针变量 ; (3)变 量 p为 指向无 标签结 构体类 型变量的 指针变 量; (4)变 量 p_east为 指向结构 体类型 mansatellitetype变 量的指 针变量 。 上述指针变量可以指向相应结构体类型的变量。例如:   pointer_preson=&zhang; p_east=&first_east; p=&programming; dateofpointer=&dateofbirth; 11.1.5  结构体变量 的成分 访问结 构体变 量的一 个 成分 ,使 用成 员 选 择 表达 式(component-selection-expresion)。 其 语法是:    <成员选择表达式 > → 直接成员选择 > x <间接成员选择 > <直接成员选择 > → <后缀表达式 >.<标识符 > <间接成员选择 > → <后缀表达式 > -><标识符 > · 310· 第十一章 结构体与共用体 1.直接 成员选 择 直接成 员选择 表达式 针对一 般的结 构体变量 ,形 式是   r.w 其中: (1)r是 后缀表 达式,最终计 算出一 个结构 体变量 ; (2)w是 r所 属结构 体类型 中的一 个成员 名字。 下述成员选择表达式是合法的:   zhang.name programming_pascal.author 2.间接 成员选 择 间接成 员选择 表达式 针对指 向结构 体变量的 指针变 量,形式是   p->w 其中: (1)p是后缀 表达式 ,最 终计算 出一个 指向结 构体变 量的指 针变量 ; (2)w是 p所指 向结构体 变量所 属类型 中的一 个成员 名字。 下述成员选择表达式是合法的:   pointer_preson ->name p_east->weight p_east->lounchdate 当然也 可以首 先对指 针变量 进行 求 地 址 运算,然 后使 用 直接 成员 选 择。 例 如上 述三 个 选择 表达式 还可以 写成如 下形式 。由于 优先级的 原因,这里 的括号 是必须 的。     (* pointer_preson).name (* p_east).weight (* p_east).lounchdate 由于成员 选择表 达式本 身也是 一个变 量访问 ,它 是相应 成分类 型的一 个变 量,它与成 分 类型 的其他 变量一 样,凡是可以 使用 那 些 变 量的 地方 都 可以 使用 成 员选 择表 达式。 对于 嵌 套结 构体,可以 认为“成 员选择表 达式”仍 然是一 个“后缀 表达式 ”,所 以 可 以继续 应用“成 员 选择 表达式 ”的 规则访 问里层 的成分 。例如 :   zhang.birthdate.mouth p_east->lounchdate.year 例 11.5  设计表 示复数 的结构 体类型,给出 复数加 法和乘 法函数 。 解:/* 复数类 型 */   typedefstrctcomplex{ 11.1  结   构   体 · 311· floatreal_part,imaginary_part; }complex_type; /* 复数加法 */ complex_ypecomplex_add(complex_typex,complex_typey){ complex_typeadd; add.real_part=x.real_part+ y.real_part; add.imaginary_part=x.imaginary_part+ y.imaginaty_part; returnadd; } /* 复数乘法 */ complex_ypecomplex_mul(complex_typex,complex_typey){ complex_typeproduct; product.real_part x.real_part* y.real_part + x.imaginary_part* y.imaginaty_part; product.imaginary_part x.real_part* y.imaginary_part + x.imaginaty_part* y.real_part; returnproduct; } 例 11.6  已知 图书 检索卡 的结 构 如图 11.5所 示 ,建 立 该 卡 片 的 数 据 结 构,并编 写 出 根 据 书号 检 索 相 应 书名、 作者 名、语种、摘要 的函数 。 解:显 然 应 该 用 结 构 体 类 型 来 描 述该卡片。 设每 个结 构体 变量 为一 张 卡片 ,全 部 卡 片 存 放 在 文 件 card.dat 中。 函数先 读入书 号;对欲 检 索的 书号   图 11.5 图书检索卡 采用 顺序检 索 方 式 检 索 ;检 索 到 后 输 出 书 名、作 者 名、语 种 和 摘 要;最 后 输 出 提 示 信 息 “searchend!”。数据 结构和 程序如 下。 假设主程序中有下述一系列声明   FILE* cardpointer;           * 文件指针 */ structbookn { //书 号 结 构 体 charcatalogue; charorder[8];     }; structdate //日 期 结 构 体 intyear,month,day;    }; structbookcrd{ //检 索 卡 结 构体 charname[32],author[16],languge[16]; · 312· 第十一章 结构体与共用体 structdatepublishingdate; structbooknono; charabstract[256]; }; 并且在主函数中已经用   cardpointer=fopen("card.dat","r"); 打开 了文件 card.dat,检 索函数 searchbook以及输 出检索 结果的 函数 out_anser如下:    /* 输出检索结果函数 */ out_anse(structbookcardcard){ inti,j,k; printf("NAME:%s\n",card.name); printf("AUTHOR:% s\n",card.author); printf("LANGUGE:% s\n",card.languge); printf("Pubishdate:% d-% d-% d" ,card.publishdate.year ,card.publishdate.month ,card.publishdate.day); printf("ABSTRACT:\n"); for(i= ;i<3;i++ ){ printf(""); for(k 1;k<=64;k++ ) printf("%c",card.abstract[64* i+j]); printf("\n"); } } /* 检索函数 */ void earchbook(void){ charcatalogue0; charorder0[8]; structbookcardcard; /* 输入欲检索的类别、书号 */ printf( pleaceinputcatalogue,order:); scanf("%c",&catalogue0); scanf("%s",order0); /* 检索*/ rewind(cardpointer); while(! fof(cardpointer)){ fread(&card,sizeof(structbookcard),1,cardpointer); if((card.no.ctalogue==catalogue0) && (card.no.order==order0))    out_anser(card); } printf("searchend! \n"); } 11.2  共   用   体 11.2 共  用  体 · 313· 11.2.1  带共用体的 结构体实 例 在实际 应用中 ,经 常遇到一 个结构 体的结 构随某 种情况 不同而 不同。 例 11.7  学校的 职工登 记卡可 能包含如 下内 容:姓 名 、出 生时 间、性别、参 加工作 时间、 职别 ;然 后对于 不同职 别的人员 则包含 如下不 同信息 :   工 人:工种(技 术、服 务 )、 类别(临 时工、合同工 、固定工 ); 干 部:级别(校 、处、科 、其他 ); 教 师:最后学 历(硕士 、博士 、其他 )、 职称(教 授、讲 师、助教 )、 专业(数 学、物 理、化学 、计算 机 )。 描述职工登记卡的数据结构。 解:三种人 员卡片 的形式分 别如图 11.6、图 11.7和 图 11.8所示 。 图 11.6 工人 图 11.7 干部 图 11.8 教师 C语言为 适应描 述这种可 变表格 数据结 构的 需要,提供 了共 用体(union)类 型 。可以 采 取共 用体与 结构体 结合的 方式描 述 这种 结 构 可 变的 表格 ,上 述 职 工 登记 卡 的 数 据类 型可 以 定义 成结构 体类型 typecardperson如下 。 · 314· 第十一章 结构体与共用体   typedefunion{ orkerfieldtypeworkerfield; cadrefieldtypecadrefield; teacherfieldtypeteacherfield; }category_tab_type;          * 描述不同类人员的共用体 */ typedefstuctcardperson{ charname[8]; /* 姓名 */ datetypebirthdate; /* 出生时间 */ sextypesex; /* 性别 */ datetypeworkdate; /* 参加工作时间 */ categorytypecategory; /* 职别 */ category_tab_typecategory_tab; /* 不同职别的不同信息 */ }typecardperson; 假设这 个声明 中涉及 到的诸 枚举类 型和结构 体类型 已经被 提前声 明,它们是:   typedefenum {male,female}sextype; //性 别:男 、女 typede struct{ //日 期:年 、月 、日 intyear,month,day; }datetype; typedefenum {worker,cadre,teacher}categorytype; //职别:工人、干部、教师 typedefenum {technology,serve}worktype; /* 工人工种:技术、服务 */ typedefenum {emporary,contract,fixed }classtype; /*工人 类 别 :临时 工 、合 同 工、固 定 工 */ typedefenum {chool,department,section,general }jobtype; /*干部级别:校、处、科、一般 */ typedefenum {doctor,master,others}degreetype; /*学位:硕士、博士、其他 */ typedefenum professor,lecturer,assistant }titletype; /*职称:教授、讲师、助教 */ typedefenum mathematics,physics,chemistry,computer }fieldtype; /*专业:数学、物理、化学、计算机 */ typedefstruct{ orkypework; classtypeclass;       }workerfieldtype; /* 工人记录的信息 */ typedefstruct jobtypejob; }cadrefieldtype; /* 干部记录的信息 */ typedefstruct{ egreetypedegree; titleypetitle; fieldtypefield;       }teacherfieldtype; /* 教师记录的信息 */ 11.2.2  共用体类型 在例 11.7中使用 了共用体 类型。 共用体 类型的 类型说 明符语 法与结 构体类似 : 11.2  共   用   体 · 315·    <共用体类型说明符 > → 共用体类型引用 > x <共用体类型定义 > <共用体类型引用 > → union <共用体标签 > <共用体类型定义 > → union <共用体标签 > { <字段列表 > } <共用体标签 > → <标识符 > 当然 ,在 共用体 中,任何一 个字段 仍然还 可以是共 用体。 共用体类 型声明 、变量声明 都与结 构体类 似,访问共 用体类 型变量 的成分 也 与 结构体 类 似———使用 成员选 择表达 式,在 此不再 赘述。 表面上 看,共 用 体 类 型 的 类 型 说 明 符 与 结 构 体 类 型 的 类 型 说 明 符 仅 差 一 个 关 键 字 “union”和 “struct”,可 是事实 上它们 有本质差 别。它 们的差 别在于 : (1)结 构体类 型中所 有成员 一个接 一个地顺 序分配 存储空 间,互相不 冲突; (2)共 用体类 型中所 有成员 占 用公 共的 存储空 间, 也就 是说,它们 从 同 一个 地 址开 始 分 配 存 储,各 个 成员 的存储空间是重叠的。 上节声明的 typecardperson类型变 量的结构 如图 11.9 所示 。它的 分量中 有结构 体类型 的,例 如 birthdate;也有 共用 体类型的 ,例如 category_tab。 为了说 明结构 体与共 用体 的区别 ,请 注意这 两个分量 的存储 影像。 (1)birthdate的 类 型 datetype是 结构 体 类 型,它 的 存储 影 像 如 图 11.10所 示 ,它 有 三 个 字 段 year、month、 day,它 们串行 地一个 接一个地 顺序分 配存储 空间。   图 11.9  ypecardperson类 型 变量 (2)category_tab的 类型 category_tab_type是 共用体 结构 类型 ,它 的存储 影像如 图 11.11所示,它 也有 三 个字 段 workerfield、cadrefield、teacherfield,它 们并行地从内存同一个地址开始分配存储空间。 图 11.10 datetype类型变量存储影像 例 11.8  在例 11.7职工登记 卡中 工人再加如下信息: 临时工 :空 ; 合同工 :定 合同时 间、合同期 限; 固定工 :空 。 图 11.11 category_tab_type类型变量存储影像 · 316· 第十一章 结构体与共用体 教师再加如下信息: 学士:毕业 学校; 硕士:毕业 专业、得学 位时间 ; 博士:导师 、论 文题目 、得学 位时间 。 增加上 述信息 后,重新定义 例 11.7中 职工登 记卡 cardperson类型。 解:关于人 员的类 型 typecardperson和 描 述 不 同 类人 员 的共 用 体类 型 category_tab_type 仍然 与例 11.7中一样 。利用 typecardperson类 型声明 变量 zhang如下:   typecardpersonzhang; 但是为 了适应 变化后 的表格 需要,共用体 类型 category_tab_type的各个成 员 workerfield、 cadrefield、teacherfield的类型 workerfieldtype、cadrefieldtype、teacherfieldtype与例 11.7中就 有 区别 了,与例 11.7中 不同的 部分重 新声明 如下:        ⁝ /* 合同工记录信息 */ typedefstrut{datetypebegindate; intlenth; }contracttype; /* 博士记录信息 */ typedefstrut{chardoctorteacher[8],paper[256]; datetypegraduationdate; }doctortype; /* 硕士记录信息 */ typedefstrut{fieldtypebeginfield; datetypebegindate; }mastertype; /* 学士记录信息 */ typedefstruct{charschool[64]}othertype; /* 工人记录信息 */ typedefstruct{ orkypework; classtypeclass; union contracttypecontractinf; }classinf;       }workerfieldtype;      ⁝ /* 教师记录信息 */ typedefstruct{ egreetypedegree; union{dctortypedoctorinf; mastertypemasterinf; othertypeotherinf;    }degreeinf; /* 不同类工人记录信息 */ /* 不同学位教师记录信息 */ 11.2  共   用   体 · 317· titleypetitle; fieldtypefield;       }teacherfieldtype;      ⁝ 重新 说 明 后 的 职 工 登 记 表 类 型 变 量 zhang的 结 构 仍 然 如 图 11.9所 示。 其 中 分 量 category_tab的 类 型 category_tab_type是 共 用 体 类 型,它 的 存 储 影 像 有 别 于 例 11.7中 的 图 11.11,变 成 如 图 11.12所 示 的 结 构,category_tab有 三 个 字 段 workerfield、cadrefield、 teacherfield,它们 并行地 从内存同 一个地 址开始 分配存 储空间 。 图 11.12 zhang的分量 category_tab的存储影像 category_tab分量的 workerfield字段 仍然是 一个结 构体,该结构 体有三 个分 量,它们串 行 分配 存储空 间。其 中 classinf分 量又是 一个共用 体,该共 用体 记 录不 同种类工 人的 信息。 对 合同 工记录合 同开始 时间和 合同期 限,对固定 工和临 时工则 不记录 任何信 息,它 的 存储影 像 如图 11.13所 示。 图 11.13 分量 zhang.category_tab.workerfield.classinf的存储影像 category_tab分量 的 teacherfield字段 也仍 然 是 一 个结 构 体 ,该 结 构 体 有 四个 分 量 ,它 们 串行 分配存 储空间 。其中 degreeinf分量 又是 一个共 用体 ,该 共用 体记录 不同 学位教 师的 信 息。 学士记 录毕业 学校;硕士记 录 毕 业专 业和 得 学 位 时间;博 士 记 录 导师、论 文 题目 和得 学 位时 间。它 的存储 影像如 图 11.14所示。 图 11.14 分量 zhang.category_tab.teacherfield.degreeinf的存储影像 · 318· 第十一章 结构体与共用体 11.2.3  限制 使用共用体有如下限制: (1)共 用体使 得在同 一个内 存 区域 可 以 存 储不 同 类 型 的数 据,但 是 程 序 运 行的 每一 个 局部 时刻,只能 存储其 中一种类 型数据 。并且 该数据 是最后 存入的 数据,其他 以 前 存入的 数 据被覆盖。 (2)共 用体变 量的地 址与它 的各个 成员的地 址是同 一个地 址。 (3)不 能对共 用体变 量整体 赋值,也不能 通过引 用共用 体变量 来得到 一个共用 体的值 。 (4)共 用体类 型不能 作函数 的 参数 类 型 和 函数 的 返 回 类型 ;但是 指 向 共 用 体类 型的 指 针类 型属于 一般指 针类型 ,当然 可以作 函数的 参数类 型和函 数的返 回类型 。 (5)给 共用 体类 型 变 量 赋 初 值,仅 对 应 相 应 共 用 体 类 型 的 第 一 个 字 段 (从 静 态 行 文 上看 )。 11.2.4  switch语句与 共用体 可以使 用 switch语句方 便地处 理共用 体。例 如针对 例 11.7学 校职工 登 记 表,可以采 用 如下语句结构处理:    /* 处理姓名 name*/ /* 处理出生时间 birthdate*/ /* 处理性别 sex*/ /* 处理参加工作时间 workdate*/ /* 处理职别*/ swith(category){     /* 处理不同职别的不同信息 category_tab*/ caseworker:处理工人信息语句 ; break; casecadre:处理干部信息语句 ; break; caseteacher:处 理教 师 信 息 语 句 ; } 11.3  结构 体与 函数 本节介绍 结构体 与函数 的关系 ,包 括返回 结构体 值的函 数、函 数的结 构体 参数。 在 C语 言中 允许函数 类型为 结构体 类型,即函 数可以 返回一 个结构 体值,还允 许结构 体 作 为函数 的 参数 ,用 参数的 方式向 函数传递 结构体 类型的 值。 11.3  结 构体 与 函数 · 319· 但是上 述叙述 对共用 体不适 用。不 允许函 数类 型是 共用体 类型,函 数不 可 以返 回一 个 共用 体值。 也不允 许共用 体作为 函数 的 参 数 ,不 允许 用参 数 的方 式向 函 数 传 递 共用 体类 型 的值 。若要实 现这些 功能,只能 通过指 针形式 ———返 回指针 的函数 带回一 个共 用体 指 针;或 指向 共用体 的指针 作函数 参数,向 函 数 传 递 一个 共 用 体 指针 ,通 过 该 指 针 找到 相 应共 用体, 并使用其值。 11.3.1  返回结构体 值的函数 函数的 计算结 果可能 是一个 结构体 值。在 C语 言中 ,有 两种 途径能 够把 该结构 体值 通 过函数名字带回调用函数的主程序。 (1)使 用指针 。函数 的结果 类型是 指向结构 体类型 变量的 指针类 型。 (2)直 接使用 结构体 类型。 函数 的 结果 类型 是 结 构 体类 型 ,直接 把 一个 结 构体 值带 回 调用函数的主程序。 第一种 方式就 是返回 指针的 函数,只 不过 相 应指 针是 指 向结 构体 类 型变 量的 指针。 与 其他 类型返 回指针 的函数 没有任 何区别 ,本 书在 第八 章 8.2节已 经介 绍 过,此 处 不再 赘述。 本章 的例 11.5已经使 用了第二 种方式 ,下 边再举 例介绍 这种方式 。 例 11.9  一个人 事档案 管理系 统中,职 工 登 记卡 包 含 姓 名、性 别、出生 时 间 等 信息。 为 该人 事档案 管理系 统编写 输入一 个职工 卡片的函 数,供主管 理系统 使用。 解:职工登 记卡的 类型定义 如下。   typedefenum {male,female}sextype; typede struct{ intyear,month,day; }datetype; typedefstuctcardperson{ charname[8]; datetypebirthdate; sextypesex; }typecardperson; /* 姓名 */ /* 出生时间 */ /* 性别 */ 在该类 型定义 下,编写输入 函数如 下。   typeardpersonreadcard(void){ typecardpersoncard; //说 明一 个 卡 片 类 型 变量 intsex_tag; printf("pleaceinputname:"); //以 下开 始 输 入 scanf("% s",card.name); //姓 名 printf("pleaceinputbirthdate:year、month、day"); scanf("% d% d% d",&( ard.birthdate.year), &(card.birthdate.month), · 320· 第十一章 结构体与共用体 &(card.birthdate.day)); //出生时间 printf("pleaceinputsex(0:male,1:female)"); scanf("% d",&sex_tag); //性 别 if(sex tag==0) card.sex=male; elsecard.sex=female; returncard; //带着一张卡片 card值返回 } 函数 readcard的 函 数 类 型 是 typecardperson,函 数 内 说 明 一 个 typecardperson类 型 变 量 card,返 回语句 “returncard”将 使函数 带着 card的 值返回 到调用 处。 在主程序 中,将使 用函数 readcard带 回的 card值。例 如主程 序中具 有功能 :输 入所有 职 工卡 片、填加一 张职工 卡片等,这些 功能的 实现都 将调用 该函 数 。如果 用数组 保 存 所有卡 片 并且 设不超 过 100个 职工,并有 声明:   #definen100 intv,flag; typecardpersoncard_arr[n]; 可以设计实现输入功能的程序片段如下:   v=0; flag=1; while(fag){ card_arr[v]=read_card(); v++; printf("pleacechoose0_end1_continue:"); scanf("% d",&flag); } 其中 语句“card_arr[v]=read_card();”调用函 数 read_card,函数带 回的值 是结 构体 值 ,直 接 送入 数组成 分变量 card_arr[v]中。 11.3.2  结构体作函 数参数 在函数 之间,通过 参数传送 结构体 值也有 如下两 种方法 : 用指向结构体变量的指针作函数参数; 直接用结构体变量作函数参数。 第一种 方式就 是指针 作函数 参数,只 不过 相 应指 针是 指 向结 构体 类 型变 量的 指针。 与 指向 其他 类型 变 量 的 指 针 没有 任何 区 别,本书 在第 八章 8.1.2节 已 经介 绍过,此处 不再 赘 述。 本章的 例 11.6已 经使用了 第二种 方式,下边 再举例 介绍这种 方式。 例 11.10 还 是上节 的人事档 案管理 问题。 为该人 事档案 管理系 统编写一 个查询 函数, 11.3  结 构体 与 函数 · 321· 供主 管理系统 使用。 该函数 带入被 检索人 员的全 部信息 ,在 档案库 中检索 ,若 找 到 则返回 相 应卡 片的序 号,否则返 回“-1”。 解:有关职 工登记 卡的类型 定义和 卡片档 案的数 据类型 及其变 量说明 同例 11.9,本 例不 再重复。编写检索函数如下:     int earch_card(typecardpersoncurrent_card){ intr; r=0; whil (r<n){ if( urrent_card.name==card_arr[r].name && current_card.birthdate.year==card_arr[r].birthdate.year && current_card.birthdate.month==card_arr[r].birthdate.month && current_card.birthdate.day==card_arr[r].birthdate.day && current_card.sex==card_arr[r].sex )returnr; r++; } return -1; } 函数 search_card形式参 数 current_card的 类型是 typecardperson,它是 一个 结构体 类型。 在主 函数中 ,当 需要检 索一张卡 片 是 否 存 在 时调 用 该 函 数,例如 检 索 功 能 、输 入 之前 的查 询 功能 等。主 函数调 用本函 数时将 通 过实 在 参 数 带给 形式 参 数 current_card一 个 结构 体类 型 值,该值 是一个 构造型 类型的 复合值 。 进 入 函数 内 部 ,用 current_card值 与档 案 库中 各个 卡 片逐 一对照 ,如 果找到 相同者,函 数带着 该卡 片 所处 位置 r返 回 到调 用处;如 果 到最 后还 找 不到 ,说 明库中 无此人 ,函 数带着 值“-1”返回到 调用处 。 在主程序中可能有如下程序片段:   typecardpersoncard; inti;   ⁝ card.name=… card.birthdate.year=… card.birthdate.month=… card.birthdate.day=… card.sex=… if(i=sarch_card(card)) printf("exist!thepositionis% d\n",i); else   printf("notexist! \n");   ⁝ · 322· 第十一章 结构体与共用体 其中 if语句的 条件表 达式“i=search_card(card)”以 结构 体 类 型 变 量 card的值 作实 在 参数 调用函 数 search_card。 函数内 形式参 数变量 current_card将取 得该值 ,用 该 值 参与进 一 步运 算,进行检 索。 11.4  程序 设计 实例 例 11.11 编 写程序 为某图形 处理程 序保 存图形 数据,数 据从终 端上 读入,保存 到文 件 中。 对不同 图形保 存不同 数据:直 线,保 存两 个 端 点 的 坐 标;三 角形 ,保 存三 个 顶 点 的 坐 标;矩形 ,保存左下 角坐标 以及横 边长和 竖边长 ;椭 圆,保存 圆心 坐标以 及横 方向半 轴长 和 竖方向半轴长。 解:由于各 类图形 保存的数 据不同 ,用 结构体 和共用 体结 合 描述 一个图形 。 程 序逻辑 比 较简 单,只是顺 次依据 不同图形 读入不 同数据 并保存 到文件 上。     /* PROGRAM storegraphics*/ enum shapekind{triangle,rectangle,ellipse,line}; uniongraphicskin { struct{floatinex1,liney1, linex2,liney2; }linekind; struct{floatrianglex1,triangley1, trianglex2,triangley2, trianglex3,triangley3; }trianglekind; struct{floa rectanglex,rectangley, rectanglehorizontal, rectanglevertical; }rectanglekind; struct{floa ellipsex,ellipsey, ellipsehorizontal, ellipsevertical; }ellipsekind; }; structshapetype enum shapekindshape; uniongraphicskindgraphicsstruct; }; charredkind(void){ charch; printf("pleaceinputkindofshape"); 11.4  程 序设 计 实例 printf("(t_triangle,r_rectangle,"); printf("e_ellipse,l_line,x_exit):"); scanf("% c",&ch); returnch; } int ain(void){ FILE * fg; charkind; structshapetypegraphics; if((fg=fpen("fileofgraphics.dat","w"))==NULL){ printf("cannotopenfile:fileofgraphics.dat\n"); exit(0); } kind=readkind(); whil (kind!= x){ switc (kind){ case l:{ graphics.shape=line; printf("pleaceinputx1,y1,x2,y2:"); scanf( %f%f%f%f", &(graphics.graphicsstruct.linekind.linex1), &(graphics.graphicsstruct.linekind.liney1), &(graphics.graphicsstruct.linekind.linex2), &(graphics.graphicsstruct.linekind.liney2)     ); break; } case t:{ graphics.shape=triangle; printf("pleaceinputrianglex1,triangley1,"); printf(" trianglex2,triangley2,"); printf(" trianglex3,triangley3:"); scanf( %f%f%f%f%f%f", &(graphics.graphicsstruct.trianglekind.trianglex1), &(graphics.graphicsstruct.trianglekind.triangley1), &(graphics.graphicsstruct.trianglekind.trianglex2), &(graphics.graphicsstruct.trianglekind.triangley2), &(graphics.graphicsstruct.trianglekind.trianglex3), &(graphics.graphicsstruct.trianglekind.triangley3) ); break; } · 323· · 324· 第十一章 结构体与共用体 case r:{ graphics.shape=rectangle; printf("pleaceinputectanglex,rectangley,"); printf(" rectanglehorizontal,"); printf(" rectanglevertical:"); scan ("%f%f%f%f", &(graphics.graphicsstruct.rectanglekind.rectanglex), &(graphics.graphicsstruct.rectanglekind.rectangley), &(graphics.graphicsstruct.rectanglekind.rectanglehorizontal), &(graphics.graphicsstruct.rectanglekind.rectanglevertical) ); break; } case e:{ graphics.shape=ellipse; printf("pleaceinputellipsex,ellipsey,"); printf(" ellipselehorizontal,"); printf(" ellipselevertical:"); scanf "%f%f%f%f", &(graphics.graphicsstruct.ellipsekind.ellipsex), &(graphics.graphicsstruct.ellipsekind.ellipsey), &(graphics.graphicsstruct.ellipsekind.ellipsehorizontal), &(graphics.graphicsstruct.ellipsekind.ellipsevertical) ); } } fwrite(&graphics,sizeof(structshapetype),1,fg); kind=readkind(); } } 例 11.12 编 写程序 ,进 行文本 统计。程 序读 入一篇 英文 文章,统计 各种 单词和 标点 符 号(句 号、逗号、分号、冒 号)出现的 次数。 解:由于单 词要记 录单词 本身及 其长度,而 标点符 号 不用 记录 这些信 息,因此用 包含 共 用体 的结构 体来保 存一个 符号或 单词的 信息 。设单 词前 16个 字符有 效,且 全部 不同单 词及 标点 符号总数不 超过 1000个,用一个结 构体数组保 存每种 单词及符号 的信息。程 序如下:     /* PROGRAM total*/ #definemaxwordlength16 #definemaxwordnum 1000 enum tokenkind{word,period,comma,semicolon,colon}; structypetoken{ intnum; 11.4  程 序设 计 实例 · 325· enum tokenkindkind; union{  struct{ ntlenth; charname[maxwordlength+1]; }theword; }aword; }token[maxwordnum ],token0; intii; charch; FILE* fg; structtypetokenreadtoken(void);              * 读符号 */ voidfindtoken(typetoken); /* 在符号表上检索 */ voidouttoken(void); /* 输出 */ intman(void){ if((fg fopen("wordtext.dat","r"))==NULL){ printf("cannotopenfile:wordtext.dat\n"); exit(0); } ii=0; ch=(char)fgetc(fg); while( feof(fg)){ token0=readtoken(); findtoken(token0); } fclose(fg); outtoken(); } boolsalpha(charc){ return( a <=c&&c<= z)||(A <=c&&c<= Z); } structypetokenreadtoken(void){ structtypetokenformtoken; inti; //判断字符 c是否字母 /* 函数值是结构体类型 */ while(!(isalpha(ch) |ch== .||ch== , ||ch== ; ||ch== :)&& !feof(fg)) ch=(char)fgetc(fg); if(isapha(ch)){ formtoken.kind=word; i=0; while(isapha(ch)&& i<maxwordlength){ · 326· 第十一章 结构体与共用体 formtoken.aword.theword.name[i]=ch; ch=(char)fgetc(fg); i=i+1; } formtoken.aword.theword.lenth=i; formtoken.aword.theword.name[i]= \0; //字 符 串 结束 符 while(isapha(ch)) //读 掉 单 词的 超 长 部 分 ch=(char)fgetc(fg); }else{ switch ch){ case .:formtoken.kind=period;break; case ,:formtoken.kind=comma;break; case ;:formtoken.kind=semicolon;break; case ::formtoken.kind=colon;break; } ch=(char)fgetc(fg); } returnformtoken; }/* readtoken*/ void indtoken(typetokenformtoken){ inti; boolflag; /* 形式参数是结构体类型 */ flag=flase; /* 检索 */ for( =-1;i<ii-1&& !flag;){ i++ ; switch formtoken.kind){ caseword:if((frmtoken.kind==token[i].kind) &&(strmp(formtoken.aword.theword.name, token[i].aword.theword.name)==0) &&(for token.aword.theword.lenth ==token[i].aword.theword.lenth)            )flag=ture;         break; caseperiod:; casecomma:; casesemicolon:; casecolon:if(formoken.kind==token[i].kind) flag=ture; } } if(flg) /* 计数 */ 习 题十一 · 327· token[i].num=token[i].num +1; else{ token[ii].num =1; token[ii].kind=formtoken.kind; if(frmtoken.kind==word){ token[ii].aword.theword.name=formtoken.aword.theword.name; token[ii].aword.theword.lenth=formtoken.aword.theword.lenth; } ii=ii+1; } }/* findtoken*/ void uttoken(void){ /* 输出 */ inti; for(i 0;i<ii;i++ ){ switc (token[i].kind){ caseword:printf( word:%slenth=%d:" ,token[i].aword.theword.name ,token[i].aword.theword.lenth             );         break; caseperiod:printf("period(.):");break; casecomma:printf("comma(,):");break; casesemicolon:printf("semicolon(;):");break; casecolon:printf("colon(:):"); } printf("num=%d\n",token[i].num ); } }/* outtoken*/ 本章小结 本章讲 述构造 数据类 型——— 结构体 类 型和 共 用 体 类 型。包 括结 构体 和共 用 体 类 型、结 构体 和共用 体变量 、结 构体和 共用体 成分 变量———成 分选择 表达 式。重 点掌 握 结构 体和 共 用体应用。 习题十一 11.1 建立 1990年到 2000年日历数组,每天一个成分,记录年、月、日、星期等信息。 · 328· 第十一章 结构体与共用体 11.2 声明描述日期(年、月、日)的结构体类型。编写函数,以参数方 式带入某日 期,计算 相应日期 在 相应 年 是第 几 天 ,并 以 函 数 值 形 式带 回 。说 明 所 编写 函 数 的 调 用 方式 和 使 用 方 法 。 11.3 某单位进行选举,有 5位 候选人:zhang、wang、zhao、liu、miao。编写一个统计每人得票数的程序。 要求 每 个人 的 信 息 使 用 一 个结构 体 表 示 ,五个 人 的信 息 使 用 结 构 体 数组。 11.4 某县欲掌握本县在校就读的大学生情况,以便引进人才。描述某县近几年在校就读大学生 情况 的统计表有如下说明和定义:    tructcampuesidence{ charaddress[128]; chartelephone[16]; }; structregisata{ charcollege[12]; intclass; floatgradeaverage; charadviser[6]; }; structstudet{ charname[8]; structcampusesidencecampus; structregisdataregistration; }; structstudentstudenttab[50]; //学 校 信 息 //地 址 //电 话 //登 记 资 料 //学 校 //年 级 //平 均 成 绩 //导 师 //学 生 情 况表 //姓 名 //学 校 信 息 //登 记 资 料 //学 生 情 况表 数 组 分别编写如下函数: (1)输 入 该 表 ; (2)按学校 +学生姓名排序; (3)按 平 均 成 绩 排 序 ; (4)按 电 话 号 码 排 序 。 11.5  编 写 函 数 ,求 多 边 形的 周 长 。 多边形 各 顶 点 坐 标 以 结构 体 形式 给 出。 11.6 利用结构体类型描述扑克牌。编写函数,对任 意给定 的一副 牌排序。(去 掉王牌;假 定梅花 < 方块 <红桃 <黑桃) 11.7 利用结构体类型描述扑克牌。编写程序,实现摆 12个月的游戏。 11.8 为例题 11.6编写输入一张卡片、修改指定书号卡片的函数。 11.9  设 计 描 述 学生 成 绩 单 (包 括 学 号 、姓 名 、4门课 程 )的 数 据类 型 ,编写 如 下 函 数 : (1)统计每个人各门功课(设只有 4门课)的成绩及总成绩; (2)统 计 全 班 每 门 课 程的平 均 分 ; (3)输 入 一 个 学 生 的 信息; (4)输 出 一 个 学 生 的 信息。 11.10  平面 上 的 点 由 直 角坐 标 系 给 出,建 立 描 述平 面 上 一点 位置 的 数 据 类 型 ,并编 写 一个 函 数 ,求 任 意三点构成的三角形的外接圆。 11.11  某仓 库 库 存 管 理 程序 保 存 如 下信息 : 习 题十一 · 329· (1)产 品 编 号 ; (2)产 品 名 称 ; (3)产 地 ; (4)计 量 单 位 ; (5)单 价 ; (6)数 量 。 说 明描 述 一 种 商 品 的 数据类 型 ;若 把 库 存 信 息保 存 在 数 组 上 ,编 写 程序 实 现 如 下 功 能: (1)输 入 数 据 ,建立 库 存 商品 数 组 ; (2)统 计 库 存 商 品 总 价值; (3)打 印 库 存 商 品 明 细表; (4)修 改 指 定 商 品 信 息。 11.12 银行账目数组包含:账号、姓名、单位、地址、当笔交易额、交易时 间、余额。编 写程序完成 如下 功能: (1)输 入 账 目 信 息 ,建 立 银 行账 目 数 组; (2)按 账 号 显 示 每 个 账户的 账 目 ; (3)显 示 指 定 时 间 内 的所有 交 易 。 11.13 学生成绩表包含如下信息:学号、姓名、考试科 目、平 时作业 成绩、期中成 绩、期 末成绩、课程 成 绩。 建 立描 述 一 个 学 生 一门 科 目 成 绩 的 数据 类 型 。若 课程成绩 =平时作业成绩*10% +期中成绩*30% +期末成绩*60% 且全 部 学生 的 全 部 科 目 的 考试成 绩 都 以 这种 形 式 保 存 ,分 别 编 写 实 现如下 功 能 的 函数 : (1)输 入 平 时 作 业 成 绩、期 中成 绩 、期末 成 绩 ,计 算 课 程 成 绩 ; (2)某 个 学 生 的 成 绩 单,该成 绩 单 包 括 该 学 生所 有 考 试 科 目 的课 程 成 绩 ; (3)某 课 程 的 成 绩 单 ,该 成绩 单 包 括 所 有 参 加本 课 程 考 试 的 学生 的 成 绩 ; (4)某 课 程 的 不 及 格 学生名 单 及 其 成绩 ; (5)平 均 成 绩 统 计 表 。该 表 把 所 有 学生 按 平 均 成绩递 减 顺 序 输 出 。 11.14 一家公司用计算机管理会计账目,其 中应收 款账文 件记录 所有欠 款单位 的欠款 明细账,每 一 条记录按发货日期顺序记载着如下信息:   欠款单位 发货单号 发货日期 金额 编写程序,为该公司打印出图 11.15所示形式的账龄分析统计表。 图 11.15 账龄分析统计表 · 330· 第十一章 结构体与共用体 11.15 设有如下 C程序:    include"stdio.h" inti; floatr; charb; intman(void){ scanf("% d",&i);printf("% d\n",i); scanf("% f",&r);printf("% f\n",r); scanf("% c",&b);printf("% c\n",b); } 请修改上述程序,使它在说明部分仅说明一 个变量,且 该变量 占用的 存储空 间不超 过 i、r、b三变量 中 占用空间最大的那个变量占用的空间。要求修改后的程序与上述程序等价。 11.16 定义表示教师和学生信息的统一的数据类型 person,该 类型除了记录 姓名、出生时间、地址、身 份证 号 码外 ,对于 学 生 还 存 储 已 获得 学 分 总数 和 所学 专 业 ;对 于 教 师 还 存 储职 称 、工资 和 科 研 方向。 设计同时保存学生和教师信息的数据结构;分别设计输入、输出一个人员信息的函数;利用这两个 函数 构造人员管理系统,该系统具有一般人事管理系统的录入、修改、查询、删除、统计功能。查询要求可以 按姓 名、学 生 学 号 查询 ;统 计 要求 可 以 按 学 生 所学 专 业 、学 分 统 计 ;教 师 可 以 按 职称 、工 资统 计 。 11.17 为例 11.9编写求线段长度、三角形重心坐标、矩形周长和椭圆面积的函数。 11.18 如图 11.16所示,平面上一点的坐标位置既可能以笛卡儿直角坐标的 形式给出,也可能以 极坐 标的形式给出(坐标原点相同)。建立描述平面上一点位置 的数据类型,并 给出在这种类 型定义 下的计 算 平面上 A、B两点间距离 d的函数。 图 11.16 平面两点间的距离 第十二章 动态数据结构 考虑上 一章的 职工卡 片问题 ,用计 算机管 理这些 卡片,要把 卡片保 存在计 算机内 。 首先,用什 么数据 结构存储 :一张卡 片是一个 结构体 ,所 有卡片 自然用 结构体数 组。 第二,数组 多大:为保 存全部 卡片,并且人 数不固 定,就应该 给一个 足够大 的数组 。 第三,操作 问题: (1)若 增加一 个人,应该在 数组中 加一个 元素,会产 生数组 不够大 的可能 ; (2)若 增加一 张卡片 在数组 中间,应该把 加入位 置以后 的其他 元素依 次向后移 动; (3)若 在中间 删除一 张卡片 ,会 在数 组中 间 留下 一个 “洞 ”,应该 把 “洞”以 后的 元素 依 次向前移动。 使用数组带来的问题是: (1)操 作不方 便; (2)数 组尺寸 不好确 定。 最好把 这些卡 片存储 成动态 的,需 要多大 存储 量 (有 多少张 卡片 )就用 多 大。中 间加 一 张卡 片时不要 向后串 别的卡 片,删除一 张卡片 时不要 留下“洞 ”。如图 12.1所示的 链式结 构 正好可以满足这些要求。 图 12.1 链式结构 在这种 结构中 ,链 上 的一 节 是 一张 卡 片 ,有 多少 张 卡 片就 有 多 少 节 。 当增 加 一 张 卡 片 时,只需 要向计 算机系 统申请一 块空间 ,联 到链的 适当位 置上。例 如,要增 加一张卡 片 50插 入到 2、3之间 ,则只 需要修 改指针 ,如 图 12.2所 示。 图 12.2 增加一张卡片 · 332· 第十二章 动态数据结构 若删除 一节,只需 要将其从 链上 摘下来 即可。 例在 图 12.2基础 上删 除节 2,如 图 12.3 所示。 图 12.3 删除节 2 链上已 经没有 节 2了,删 掉 的 节 所 占 的 存储 空 间还 可 以 还 回 计 算 机 系 统,以 便 作 其 他 用途。 这就是 一种动 态数据 结构———链表 。动 态 数据 结构 上 的 一 项是 一个 动态 变 量,指针 是 标识动态变量的有力手段。动态变量与静态变量的区别在于: 静态变量 是程序 中由程 序员“显 式”说明 的变量 。它有一 个名字 ,在编译时 ,编 译程序 已 经给它分配存储空间。这块存储空间用变量的名字来标识。 动态变量 在程序 中没有 “显式”说 明,它没 有名字 ,在 编译 时编译 程序 不知 道有该 变量, 不给 (也不可能 给)它分 配空间 。动态 变量是 在程序 运行时,随 程序存 储数据的 需要,由申 请 空间 函数(例如 malloc,当然也 是由程 序员安 排的)随 机地动 态申 请 来的 空间。 它没 有 名字, 一般 动态变量 都由指 针标识 。当使 用完毕 后,由释放 空间函 数 (例如 free)释 放 ,还 回计算 机 存储 管理系 统,以备他 用。 注意:这里 所说的 静态变量 不是 C语 言中 由 静态 存储 类 别 static声明 的变量 ;动 态变 量 也不 是 C语言中 由自动 存储类别 auto声明 的变量 。 而 是一般 程序设 计概 念中 的静态 变量、 动态变量。 在上述 职工卡 片管理 问题中 ,每 个节 中包含 基本 数据部 分,这是 必须 的,还 应包 含一 个 指针 ,标 识及指 出下一 节的位置 。 12.1  管理 动态 变量 动态变量 在程序 运行时 ,随 程序存 储数据 的需要 向计算 机系统 申请;使用 完 后 还回计 算 机系 统。本 节介绍 申请计 算机存 储空间 函数 malloc和 释放存 储空间 函数 free。 1.内存 程序运 行时,涉及 用户程序 的内 存存 储结 构 如 图 12.4所 示 。首 先是 目标 代 码 区 ;然 后 12.1  管 理动 态 变量 · 333· 是静 态存储 区,用 于存 放 那 些可 用 绝 对 地 址标 识 的 ,主 要 是 具 有 静态 存储 类别的 数据和 变量;接着是 目标代 码运行 时用到 的库程 序代码 区; 最后 剩余空 间是栈 区和堆 区,栈区 和堆区 从剩 余空间 的两 端动 态 地向 中间增长。栈区用来存储程序中声明的函数局部变量等具有自动存储 类别 的数据 和变量 ;堆 区用来存 储经过 动态申 请空间 函数申 请的变 量。 2.sizeof运算符 单目运 算符 sizeof的 操作数是 类型。 运算 结果是 求得 相应 类 型的 尺寸 ,即 存储相 应类型 数据所需 要的字 节数。 例如   图 12.4 内 存存 储结构   sizeof(int)        * 结果是 2*/ sizeof(char) /* 结果是 1*/ sizeof(structdate) /* structdate是第十一章定义的日期类型,结果是 6*/ 3.malloc函数 malloc函数 的原型 是     void* malloc(unsignedlongsize); malloc函数的 功能是:申请足 够大内存区域 用来存储长度为 size的 数据对 象,返回该 区域 的首 指针,并保证该区 域符合任 何数据 类型对 存储区 域开始地 址和对 齐的 要求。 返回指 针是 void类型 的,调用者必须使用显示 强制类型转 换把该指 针转换成所 需要类型的指针。 例如     float* p; p=(float* )malloc(sizeof(float)); 由于要 保证该 区域符 合任何 数据 类 型对 存储 区 域开 始地 址 和 对 齐的 要求,所以 实际 申 请的 存储区 域可能 大于 size。 4.free函 数 动态申 请的内 存如果 不再使 用,应 当适时 释放 ,这 样 可以 提高 程 序 的 运行 效率 。free函 数用 来释放 经过 malloc申请的动 态空间 。free的函数 原型是     voidfree(void* ptr); free函数的功 能是:释放 由 malloc申请 的内存 区域。 free的 参数 ptr是一个 指针,指向 以 前由 malloc申请 的一个 内存区域 。例如   free(p) /* 释放 p所指向的,以前由 malloc申请的内存空间 */ 一块存 储区域 一经释 放,便 不能再 使用。 使用 free函数 时要特 别注意 ,操 作 不 当会产 生 不可 预料的 结果。 如下情 况使用 free函数 都会造 成灾难 性后果。 (1)ptr无值; (2)ptr的值为 NULL; (3)ptr所指向 的空间 不是经 过 malloc申请 来的; · 334· 第十二章 动态数据结构 (4)对 一次申 请的存 储区进 行多次 释放(实际 可能是 ptr无值 或值为 NULL)。 5.实用 问题 若指针变 量指向 的由 malloc申请来 的动态 变量是 孤立的 ,不能 与其他 变量 相联 系 ,显 然 作用 不大。引 进动态 变量的 目的是 构造动 态数据 结构,例如 像本章 开始介 绍的 那样,构造 一 个链 表等。 这就要 求一个 数据项 上除 基 本 的 数据 信 息 外 ,还 应包 含与 其 他 数 据 项相 联系 的 信息 ,也 就是包 含指针 ,呈图 12.5所示 的形式。 该结构 必 须 用结构 体类型 描述 ,链 表上一 节 的类 型定义 形式如 图 12.6所示 。 图 12.5 一个数据项 图 12.6 类型定义形式 12.2  动态 数据 结构 利用指 针和动 态变量 ,可以 构造各 种动态 数据结 构。 12.2.1  栈(stack) 在第六 章已经 用数组 实现过 栈和 队 列,但 是 数组 有一 定 的局 限性。 可以 用 单向 链表 实 现栈 ,指 针变量 top指 向栈顶 ,如图 12.7所示 。栈的 操作有: (1)初 始化:stackintial; (2)压 栈:stackpush; (3)弹 栈:stackpop。 设有声明:   typedef… items; typedefstructtackcell{ itemsdata; structstackcell* predocessor;      }stackcelltype; typedefstackcelltype* pstacktype; pstacktypetop;   图 12.7 用单向链表实现栈 如下实现栈的操作: 12.2  动 态数 据 结构   void tackinitial(void){ top=NULL; } void tackpush(itemsx){ pstacktypep; p=(pstacktype)malloc(sizeof(stackcelltype)); p->data=x; p->prodocessor=top; top=P; } void tackpop(items*x){ pstacktypep; if(top >NULL){ * x=top->data; p=top; top=top->predecessor; free(p); }elseprintf("栈下溢 \n"); } 看如下三个操作: (1)初 始化后 (调用一 次 stackinitail),如 图 12.8所 示。 (2)调 用一次 stackpush(1),如图 12.9所 示。 · 335· 图 12.8 初始化 图 12.9 调用一次 stackpush(1) 再调用 一次 stackpush(2),如图 12.10所 示。 (3)调 用一次 stackpop(&b),如图 12.11所示 。 图 12.10 再调用一次 stackpush(2) 图 12.11 调用一次 stackpop(&b) · 336· 第十二章 动态数据结构 12.2.2  队列(queue) 如图 12.12所示,也 可 用 单 向 链 表 实 现 队 列,现 在 要用 两 个 指 针 变量,一 个 指 向 队 头 (front),一个指 向队尾 (rear)。队列 的操作有 : (1)初 始化(queueinitial); (2)进 队——— 排在队 尾(inqueue); (3)出 队——— 从队头 删一项 (outqueue)。 设有如下说明: 图 12.12 用单向链表实现队列   typedef… items; typedefstruct ueue{ itemsdata; structqueue* next;        }queuetype; typedefqueuetype*pqueuetype; pqueuetypefront,rear; 如下实现队列的操作:   voidquueinital(void){ front=NULL; rear=NULL; } voidnqueue(item x){ pqueuetypep; p=(pqueuetype)malloc(sizeof(queuetype)); p->data=x; p->next=NULL; if(rer==NULL){ rear=p; front=p; }else rear->next=p; rear=p; } } void utgueue(item *x){ pqueuetypep; if(font==NULL) printf("队空 \n"); else{ 12.2  动 态数 据 结构 · 337· } } *x=front->data; p=front; front=front->next; if(frnt==NULL) rear=NULL; free(p); 看如下三个操作: (1)调 用初始 化后 (调 用 一次 queueinitail),如 图 12.13 所示。   图 12.13 初始化 (2)调 用一次 ingueue(1),如图 12.14所 示;再调用 一次 ingueue(2),如 图 12.15所示; 再调 用一次 ingueue(3),如图 12.16所示。 图 12.14 ingueue(1) 图 12.15 ingueue(2) (3)调 用一次 outgueue(&a),如 图 12.17所示;再 调 用一 次 outgueue(&b),如 图 12.18 所示 ;再 调用一 次 outgueue(&a),如 图 12.19所示。 图 12.16 ingueue(3) 图 12.17 outgueue(&a) 图 12.18 outgueue(&b) 图 12.19 outgueue(&a) 12.2.3  链表(linkagetable) 如图 12.20所示 ,链 表有各 种各样 的结构 ,这里 不能 全 部 介 绍这 些链 表的 操 作,只介 绍 一下 单向链表 的操作 。实际 上前边 讲的栈 、队 列都是 单向链 表,但是栈 和队列 只 是 单向链 表 的两 种特殊 应用———操作 只在头 尾进行 。下面介 绍单向 链表的 一般操 作: · 338· 第十二章 动态数据结构 (1)创 建单向 链表; (2)遍 历单向 链表; (3)在 单向链 表上检 索; (4)向 单向链 表插入 一项; (5)从 单向链 表删除 一项; (6)交 换单向 链表中 两项的 位置。 图 12.20 链表 1.创建 单向链 表 创建单向 链表,是 指用一项 一项的 数据逐 步建立 、形 成一 个 链表。 可以分 成 向 链头加 入 数据 和向链 尾加入 数据两 种方式 。新 项 加 入 链头 就 是 压 栈 的算 法;新 项 加 入 链 尾就 是队 列 中入 队的算 法。只 要反复 调用那 里的函 数或将函 数体放 在循环 语句中 即可,这里不 再赘述 。 2.遍历 单向链 表 遍历是 指从头 到尾将 链表上 的数 据 全 部 加 工一 遍,可 用图 12.21所 示的 算法。 在遍 历 链表 的过程 中,经常在 加工一项 数 据 后,当 链 指针 前 进 时 ,另 外再 使用 一 个 指 针 保留 其前 一 项的 位置,以备 其他加 工时使用 ,所以经 常用图 12.22所 示的算法 来遍历 单向链 表。    p=base; while(p =NULL){ 加工 P-> p=p->next;  }    p0=NULL; p=base; while(p =NULL){ 加工 P->  } p0=p; p=p->next;    图 12.21 遍历算法Ⅰ 图 12.22 遍历算法Ⅱ 3.在单 向链表 上检索 检索是指 在单向链表 上查找关键字等于某 给定值的结点,若找到则带 回相应结点的指针 ; 12.2  动 态数 据 结构 · 339· 否则 带回 NULL。设关键字 域域名为 key;欲检索的 关键字值为 key0,如下 算法实现检 索:   p0=NULL; p=base; while(p! NULL&&p->key!=key0){ p0=p; p=p->next; } 4.向单 向链表 插入一项 设有如 图 12.23所示的 链表,现在 要把如图 12.24所示的 数据项 插入到 p0、p所指两 项 之间 ,得 如图 12.25所示 的结果 。操作 是:   r->next=p; p0->next=r;      * 已经完成图 12.25所示的操作 */ p0=r /* 使 p0仍为 p的前一项 */ 图 12.23 链表 图 12.24 插入的数据项 图 12.25 插入后结果 5.从单 向链表 上删除一 项 设有如 图 12.26所示的 链表,现在 要删 除 p所 指项,得 如 图 12.27所示 的链表 。删除 算 法是: 图 12.26 链表 图 12.27 删除 p所指项后的结果 · 340· 第十二章 动态数据结构   q=p; p=p->next; p0->next=p; free(q) 6.交换 单向链 表上的两 项 设有如 图 12.28所示的 链表,现在 要 把 p所 指 的 项 与 q所指 的项 交 换。 最 简单 的方 法 是把 p所指 项与 q所指项 中的基 本数据 部分交换 。但是 如果数 据量 大,这 样做 会浪 费 时间。 可以 通过修 改指针 来交换 单向链 表上两 项,该 算法通 过如下 七步完 成: 图 12.28 链表    /*交换 p->next、q->next*/ g=p->next;        * 1*/ p->next=q->next; /* 2*/ q->next=g; /* 3*/ /*交换 p0->next、q0->next*/ p0->next=q; /* 4*/ q0->next=p; /* 5*/ /*交换 p、q*/ p=p0->next; /* 6*/ q=q0->next; /* 7*/ 经过 1、2、3、4、5后,得图 12.29,链表 中的两项 已经完 成 交 换 ;再经 过 6、7步整 理 指针, 得图 12.30。 图 12.29 前五步 图 12.30 后两步 12.2.4  树(tree) 如图 12.31所示 ,数 据可以 组织 成 树形 结构。 用动 态 变量 和指 针来 存储 和 表示 一个 树 形结 构是方便 的。只 考虑二 叉树,二叉 树的每 个数据 项附带 两个指 针,分别指 向 它 的两个 分 12.2  动 态数 据 结构 · 341· 支。二叉树的定义是: (1)空 是树; (2)一 个结点 连接两 个不相 交的树 ,仍为 树; (3)所 有结点 具有相 同的数 据类型 。 树的优 点是数 据组织 灵活,并且 检索快 。 设 ti为 二叉 树的一 个结点 ,一 般 ti由 两部分 组成:基本 数据部 分和指针部分。基本数据部分保存本结点上的基本数 据;指针 部分连 接本 结 点 以 下 的其 他结 点。结 点 ti的 指针 连接的 结点称 为 ti的子 结点,相应 ti称 为其 子结 点的 父结点 。ti的指针 部分有 两 个指 针:左 指 针、右指   图 12.31 树形结构 针。 称 ti的左指 针连 接 的 部 分 为 ti的 左 子 树,ti的右 指针 连接的 部分为 ti的 右子树。 若左、右子 树均空 ,则 称 ti为 叶结点 。 为了检索 操作方 便,一般把 二叉树 组织成 二叉检 索树。 二叉检 索树的 定义 是:设树中 每 个结 点的数 据部分 有一个 数据项 key是有序 的,称 该 数据 项为 关 键 字 。一个 二叉树 称为 检 索树 ,如 果对每 个结点 ti,它的 左子树 中所有结 点的 key值都 小 于 ti的 key值 ;ti的 右子树 中 所有 结点的 key值都 大于 ti的 key值 。树的操 作有: (1)遍 历; (2)检 索; (3)插 入一个 结点; (4)删 除一个 结点。 由于树 是递归 定义的 ,所以 树的操 作用递 归算法 十分简 洁。设 有说明 部分:   typedef… keytype; typedef… datatype; typedefstruc tree{ keytypekey; datatypedata; structtree*left; structtree*right; }treetype; typedeftreetype* treepointer; treepointerroot; 1.遍历 遍历二叉 树(traversingbinarytrae)是 指按一定 规律走 遍树的 每个结 点,使 每 一 结点被 访 问一 次,而且只 被访问 一次。在 访问 一 个 结 点时 ,可 以 做 任 何信 息加 工 工 作。 例 12.1打 印 结点 的 data域,并设 该域为 char型的 。 · 342· 第十二章 动态数据结构 遍历算 法可分 为前序 遍历、中序遍 历和后 序遍历 三种。 (1)前 序遍历 是指:对任意 一个结 点 ti来讲 ,先 访问 及处 理该结 点的 数据域 ;然 后遍 历 左子 树;最后遍 历右子 树。 (2)中 序遍历 是指:对任意 一个结 点 ti来讲 ,先 遍历 左子 树;然后访 问及 处理该 结点 的 数据 域;最后遍 历右子 树。 (3)后 序遍历 是指:对任意 一个结 点 ti来讲,先遍 历左子 树;然 后遍历 右子 树;最后访 问 及处理该结点的数据域。 例 12.1  设有 图 12.32所示 的 树 。这 是 由表 达 式 (a+ b/c)*(d-e*f)生成 的树,这 棵树反 映了 表达 式的结 构,同 时也反映了表达式的运算次序。 前序遍 历,得到:*+a/bc-c*ef是 表 达 式 的波 兰 表 示 式(运 算符在两 个运算 分量之 前)。 中序遍 历,得到:a+b/c*d-e* f是表达 式的原 形式,只 是没有括号。   图 12.32  a+b/c)*(d- 后序遍历 ,得到:abc/+def*-* 是表 达 式 的 逆波 兰表 示 e* f) 式(运 算符在两 个运算 分量之 后)。 三种遍历算法分别如下:   voidprerder(treepointerp){ /*前序遍历*/ if(p! NULL){ printf("% c",p->data); preorder(p->left); preorder(p->right) } } voidinoder(treepointerp){/* 中序 遍历 */ if(p! NULL){ inorder(p->left); printf("% c",p->data); inorder(p->right) } } voidposorder(treepointerp){/* 后序 遍历 */ if(p! NULL){ postorder(p->left); postorder(p->right) printf("% c",p->data); } } 12.2  动 态数 据 结构 · 343· 2.检索 检索是 按给定 关键字 值 C在树上 找一个 结点 ti,且 ti的 关键字 值 key恰好 等于 C。 若检 索到 ,函 数将带 回相应 结点指针 ;若没检 索到,函数将 带回 NULL。   treepoinersearch(typekeyc,treepointerp){ if(( ->key==c)||(p==NULL)) returnp; elseif(c< ->key) returnsearch(c,p->left);     else  returnsearch(c,p->right); } 3.插入 一个结 点 插入是指 将一个 数据项 插入到 树中某 一恰当 位置,使树 仍保 持 检索 树的性 质。 显 然,首 先要 按 key值 查出应 该插入的 位置,然后 再插入 。     voidinert(keytypec,datatyped,treepointer* p){ if(*p =NULL){ *p=(treepointer)malloc(sizeof(structtree)); (*P) ->key=c; (*p) ->data=d; (*p) ->left=NULL; (*p) ->right=NULL; }elseif(c (*p) ->key) insert(c,d,&((*p)->left));     elseinsert(c,d,&((* p)->right)); } 函数 insert的 参数 p是 指 向 指 针 的 指 针 类 型。 在 “(* P) ->key=c”等 运 算 中 把 “*P”用 括号括上 是必须 的,因为运 算符“->”的级别 比“*”高 。 由于函 数 insert的参 数 p是指 向指针 的指针 类型,在函 数 insert内 P指向 指 针 类型的 实 在参 数。所以 在执行 “*p=(treepointer)malloc(sizeof(structtree))”时,将使 实在参 数指 针 变量指向新申请来的结点。 (1)若调 用 insert时,root为空树 ,如 图 12.33所示。 以 &root作实 在参数 调用 insert,即 insert(c,d,&root)。insert的 形 式 参 数 p指 向 root,而 root值 为 NULL。 转 插 入 功 能,执 行 “*p=(treepointer)malloc(sizeof(structtree))”,如 图 12.34所示 ;再 执 行 后边 的 几 个 赋 值 语句 ,如 图 12.35所示。 · 344· 第十二章 动态数据结构 图 12.33 空树 图 12.34 执行*p=(…)malloc(…) 图 12.35 执行赋值语句 (2)若 调用 insert时 ,root非空 ,在 中间 某 一 个 结 点 查到 空指 针,如 图 12.36所 示 ;设 查 到该 结点后 ,应 该 继 续 向 右 查,以 “&((* p) ->right)”作 实 在 参 数 递 归 调 用 insert,即 insert(c,d,&((* p)->right))。insert的形式参 数 p指向 本步 的 (* p)->right,而 (* p) ->right值为 NULL。转插 入功能 ,执 行“* p=(treepointer)malloc(sizeof(structtree))”,如 图 12.37所示 ;再 执行后 边的几 个赋值 语句,如图 12.38所 示。 图 12.36 查到空指针 图 12.37 执行*p=(…)malloc(…) 图 12.38 执行赋值语句 4.删除 一个结 点 设欲删 除结点 为 r,则可能有 如图 12.39、图 12.40和图 12.41所示的 几种情 况。对 不同 情况删除算法如下: 图 12.39 r是叶结点 图 12.40 r只有一个子树 图 12.41 r两个方向都有子树 (1)r是 叶结点 。 简单删 掉 r结点,并把 r的父结 点连接处 改成 NULL即可 ,从 图 12.39得图 12.42。 (2)r只 有一个 子树。 把 r以下部 分接入 r的父结点 连接 r处 。然后 删掉 r结点 ,从 图 12.40得图 12.43。 (3)r两 个方向 都有子 树。 在 r的左子 树上找 到关键 字 key值 最大的 结点 s,把 s结点的 数据 data及 关 键 字 key复 制到 r结 点上,然后 删除掉 s结点。 12.2  动 态数 据 结构 · 345· 当然也可 以在 r的 右子树 上找到 关键字 key值最 小的结点 s,把 s结 点 的数 据 data及 关 键字 key复制 到 r结点上 ,然 后删除 掉 s结点 。 使用在 左子树 上找最 大结点 的方法 ,按如 下步骤 进行: (1)沿 r左 子树的 右方向 ,向 下找一 个没有 右子树 的结点 s,图 12.41中 结点(12)。 (2)把 该结点 s的值复制 到结点 r(即欲删 除的结 点,图 12.41中为结 点(4))。 (3)把 s的左子 树连在 s的父结 点的右链 上,在图 12.41中即 连到结 点(9)的右链 上。 (4)删 除 s结点 ,即 图 12.41中的结 点(12)。 最后形 式如图 12.44所 示 。 其 中,r结 点 中 存 储 的 是 原 s结 点 的 数 据 data和 关 键 字 key(12),而 原 s结点 被删掉 了。 图 12.42 r是叶结点 图 12.43 r只有一个子树 图 12.44 r两个方向都有子树 综合上 述三种 情况,下述函 数 deletenode完成删 除一个 结点。 deletenode的 调用形 式是:   deletenode(valueofkey,&root) 其中 (1)value_of_key是欲删除 结点的 关键字 值; (2)root是指 针类型 (treepointer)变量,指向树 根。这 里用指 向指针 的指针 作参数 。   treepointerdel(treepointer*,treepointer* );    * 处理第三种情况的函数的函数原型 */ void eletenode(keytypec,treepointer*p){ /* 删除关键字值等于 c的结点 */ treepointerr; if(* ==NULL) printf("notfound:% d\n",c); elseif(c (*p) ->key) /* c<当前结点的 key值,被删结点在左子树上 */ deletenode(c,&((*p) ->left));   elsef(c>(*p) ->key) /* c>当前结点的 key值,被删结点在右子树上 */ deletenode(c,&((*p) ->right));       else{  * c==当前结点的 key值,删除该结点 *p*/ r=* p; if(r-> ight==NULL) * p=r->left /* 右子树空,接左分支 */ · 346· 第十二章 动态数据结构 elseif(r-> eft==NULL) *p=r->right; /* 左子树空,接右分支 */     else  r=del(&(r->left),p );  /* 左右均非空 */ free(r); /* 释放 r*/    }  }; treeponterdel(treepointer*s,treepointer*p){  * 处理第三种情况,仅第三种情况调用 */ treepointerr; if((*s ->right!=NULL) /* 右分支非空?*/ r=del(&((* s)->right),p) /* 右分支非空,在右方向以下继续查找 */ else{ * 找到右分支为空的结点 *s*/ (* p)->key =(* s)->key; /* 复制 *s的关键字、数据到 *p结点 */ (* p)->data=(* s)->data; r=* s; /* r记载该 *s结点,为 free做准备 */ *s=(*s)->left;   * 删除 *s所指结点。由于 s参数是指向指针的变量 */ /* 本语句把 *s左分支接到 *s的父结点上*/ /* 从而在树上摘下了 *s所指向的结点。*/ } returnr; //把将 释 放 的 变 量 指针 带 回 主 程序 } 12.3  程序 设计 实例 例 12.2  一个排 序算法 。数组 排序已 经很熟 悉,而 且有各 种各 样的算法 。 下 面用逐 步 增加递增子序列的方法实现链表排序。 该算法的思想是: (1)开 始假设 空序列 是递增 的。 (2)若 i个元 素的子 序列 A1 ~Ai 已经 递增 ,则 加一 个元素 Ai+1,把 Ai+1插 入 到 A1 ~Ai 序列 中一个 适当位 置使 i+1个元素 的子序 列 A1 ~Ai+1也递增。 (3)直 到 i=n为止。 设有说明:   typedef… datatype; struc item{ datatypedata; intkey; structitem *next; }; 12.3  程 序设 计 实例 · 347· typedefstructitem *pt; 以 base为链 首的链表 如图 12.45所示;基于该 链表排 序的算 法如图 12.46所 示。程 序运 行中 ,各个 参数、变 量、链 表状态如 图 12.47所示。函 数 sort调 用形式 是“base=sort(base)”, sort返回后 带回结 果链表 首指针。 图 12.45 以 base为链首的链表 图 12.46 链表排序 图 12.47 以 base为链首的链表排序,程序运行中,各个参数、变量、链表状态    /* 链表排序,base为链首 */ ptsot(ptbase){ ptp,p0,r,r0,q; p0=NULL; p=base; while(p!= ULL){  /* 逐项处理,把 p加入到子序列中,并保持“base~p”递增 */ · 348· 第十二章 动态数据结构 } returnbase; }   /* sort*/ /* 寻找位置 */ r=base; while(( ->key<p->key)&& (r!=p)){ r0=r; r=r->next; } /* p插入到 r0、r之间 */ if(r!= ){ /* 若 r==p,在链尾,则不用插入 */ /* 把 p独立出来,令 q指向它 */ q=p; p0->next=p->next; p=p0; /* 插入 */ if(r= base){ /* 插在链首 */ q->next=base; base=q; }else{ * 插在链中 r0、r之间 */ q->next=r; r0 ->next=q; } } /* 前进一项 */ p0=p; p=p->next; 例 12.3  法雷序 列。 对任意 给定的 自然数 n,把所 有如下 形式的 不可约 分数 ij    (0<i<=n;0<=j<=i) 按递 增顺序 排列起 来,称该序列 为 n级法 雷序列 Fn。 例如 F8为: 01,81,71,61,51,41,72,31,83,52,73,21,74,53,85,32,75,43,54,76,87,11 编写函 数,对任意 给定的正 整数 n,求 n级 法雷序 列,并带回 n级法 雷序列链 指针。 分析:法雷 序列的 每项是个 分数,不 能用实 数精 确地 表示,而且 这些 数的 排 列顺 序是 不 规则 的。用 图 12.48所示形 式的链 表来 表示它 。 显 然法 雷 序 列 的 各项 均 在 区 间[0/1,1/1] 之内 。生成 法雷序 列的算 法是:先 把 一阶 法雷 序列:0/1,1/1放入 链 表 中;然后 顺 序 分 别 以 i=2,3,…,n作分母 ,对 任意 i以 j=1,2,…,i-1作分 子,作成 分数 j/i;若 j/i是 不可约 分 数,则该 j/i必然 是法雷 序列的一 项;把该 j/i插入 到链表 的合 适位置 上,并使 链表仍 保持 按 12.3  程 序设 计 实例 递增 排序,算法 如图 12.49所示 。 图 12.48 法雷序列 · 349· 图 12.49 生成法雷序列的算法   structfalei_item { intnumerator,denominator; structfarlei_item *next; }; typedefstructfarlei_item * farleipointer; intgcd intx,inty){ if( ==0) returnx; elsereturngcd(y,x% y); } /* 构造 法 雷 序 列 ,并 返 回 序 列头 指 针 */ farleiponterfarlei(intn){ inti,j; farleipointerfn,r,r0,p; /* 求最大公约数 */ if( <1); //如果 n<=0,则没有法雷序列 returnNULL; /* 0/1,1/1送入序列,fn指向链首 */ fn=(farleipointer)malloc(sizeof(structfarlei_item));   //构造 0/1 fn->numerator=0; fn->denominator=1; fn->next=(farleipointer)malloc(sizeof(structfarlei_item));  //构造 1/1 fn->next->numerator=1; fn->next->denominator=1; fn->next->next=NULL; · 350· 第十二章 动态数据结构 /* 生成序列 */ for i=2;i<=n;i++) for j=1;j<i;j++) if gcd(i,j)==1){ /* 查 j/i位置 r0,r*/ r=fn; while(j* ->denominator>i*r->numerator){ r0=r; r=r->next; } /* j/i插入到 r0,r之间 */ p=(farleipointer)malloc(sizeof(structfarlei_item)); p->numerator=j; p->denominator=i; r0->next=p; p->next=r; } returnfn; } 例 12.4  多项式 加法。 多项式 可以用 如图 12.50所示 形式的 链表来 存储,例如多 项式 p(x)=6.5x5 +3.4x2 +x+0.5 可以 表示成如图 12.51所示的 形式。编 写一个函数 ,实 现多项式加法 :p(x)+q(x)=>s(x)。 图 12.50 链表存储多项式 图 12.51 多项式 6.5x5 +3.4x2 +x+0.5 设有类型说明:     structiem { floatcoef; intexp; structitem * next; }; typedefstructitem *polynome; 解:在本程 序的算 法中,在链表 上利用 了一个 哨兵项 。所谓 哨兵是 在链表 的 链 首多加 一 节,该节 不存储 有效的 链表项的 值,而保 存 一 个 边界 值或 空 值,本 程 序的 哨兵 项是 空值。 由 于利 用了哨 兵变量 ,所 以处理统 一了。 多项式 加法的 算法如 图 12.52所示,程序 如下: 12.3  程 序设 计 实例 · 351· 图 12.52 多项式加法   voidads(intexp0,floatcoef0,polynomer){ /* 向 s加入一项 */ polynomer0; r0=(polynome)malloc(sizeof(structitem)); r0->exp=exp0; r0->coef=coef0; r0->next=NULL; r->next=r0; } polinome ddpolynome(polinomep,polinomeq){ polinomes,r     //s为结果多项式链首;r始终指向结果多项式 s的最低次幂项 /* 申请一个哨兵变量,以便算法统一 */ s=(polynome)malloc(sizeof(structitem)); r=s; /* 相加 */ while (p!=NULL)&&(q!=NULL)) if(p->ep>q->exp){ adds(p->exp,p->coef,r); p=p->next; }elseif(p->exp<q->exp){ adds(q->ex,q->coef,r); q=q->next;    else{  adds(p->exp,p->coef+q->coef,r); q=q->next; p=p->next; } r=r->next; /* p多项式尾 */ while(p =NULL){ adds(p->exp,p->coef,r); p=p->next; r=r->next; · 352· 第十二章 动态数据结构 } /* q多项式尾 */ while(q =NULL){ adds(p->exp,p->coef,r); q=q->next; r=r->next; } /* 释放哨兵变量 */ r=s; s=s->next; free(r); returns; } 例 12.5  找路径 。 已知一 个网 g=(v,e)。 其中,v=(v1,v2,…,vn)为 网 g的 结 点 集 合,vi 为网中 结点。 e={(vi,vj)}是 网中边 的集合 ,(vi,vj)表示连 接结点 vi 到结 点 vj 的边。找 路 径 是指求 从 结点 m 到结点 n的所 有路径 。 这是一个 十分有 应用价 值的问 题。例 如,每个结 点可以 看成一 个城市 ,每 条 边 可以看 成 城市 之间的 路。该 问题就 是求从 m 城到 n城 之间所 有可能 的路。 解:从 m点出 发沿所 有可能 的路 向前走一 步;然后 再站在 新的点 上,再向前 走一 步;如 此重 复,直到走 到结点 n为止。 在走的 过程中 ,保 证不走 重复点 ,可 以得到 图 12.53的算法 。 在该算 法中,关键 在于找出 m点的 所有后 继 点。 这涉 及到 网 的 表 示方 法 。网有 各种 各 样的 表示方 法。以 图 12.54所示的 网为例 ,下面 分别给 出三种 表示方 法。 图 12.53 找路径 图 12.54 网 1.邻接 链表方 法 设网中 有 k个 结点,使用一 个有 k个元素 的一维 指针数 组 G,数组 G的 第 i个元素 对应 网中 第 i个结点。 以它为 链首,把与 结点 i直接相 连的结 点链成 一个链 。图 12.54所示 的网 可表 示成图 12.55所 示的邻 接链表 。 2.邻接 表方法 设网中有 k个 结点,使用一 个 k×k的 int型矩 阵 g表 示网,矩阵 g的第 i行顺 序列出 与 12.3  程 序设 计 实例 · 353· 图 12.55 邻接链表 结点 i直 接相连的 结点编 号,最后 以“-1”结尾。 则图 12.54所示 的网可 表示成 图 12.56所 示的邻接表。 3.邻接 矩阵方 法 设网中 有 k个 结点,则使用 一个 k×k的 bool类 型矩阵 g表示网 ,矩阵元 素 ture, 表 示从结 点 i到结点 j有 直接路 g[i,j]= false, 表 示从结 点 i到结点 j没 有直接 路 利用这 种表示 法,则图 12.54所示 的网可表 示成图 12.57所示的 8×8的 bool矩 阵。 图 12.56 邻接表 图 12.57 邻接矩阵 使用邻 接链表 方法,编写函 数 route如 下。route的调用 方式是   route(mm,nn,0) 其中 ,mm是开始 结点的 结点号 ;nn是结束 结点的 结点号 ;0是路 迹数组 起点。   #defineh10 struct ode{ intno; structnode* link; }; intp[h];             * 路迹数组 */ structnode* g[h]; /* 网的邻接链表 */ · 354· 第十二章 动态数据结构 voidprintp(int,int); //函 数 原型 :输 出 booliinp(int,int,int); //函数原型:判断点 i是否已经走过(在 P中) voidrote(intm,intn,intr){ //开始结点、结束结点、路迹数组 p的尾 structnode*hh; inti; p[r]=m; if(m== ) printp(0,r); else{  hh=g[m]; while hh!=NULL){ i=hh->no; if(! iip(i,0,r)) route(i,n,r+1); hh=hh->link; } } } booliinp intii,intu,intv){ intj; for(j u;j<=v;j++ ) if(i==p[j]) returntrue; returnfalse; } voidprinp(inte,intf){ intj; for(j=e; <=f;j++) printf("%4d",p[j]); printf("\n"); } 本例给 出的网 是一个 无向图 。在图 论中还有 有向 图,有 向图 的 边 是带 箭 头 的 。有向 图 也可 以用上 述方法 表示,上述程 序照样 适用。 在图论中 有的问 题还涉 及 到 边 长 ,即 两 结 点 间 的距 离。可 能这 样提 出 问 题 :求 结点 m 到结 点 n的 最短距 离。只 要把上 述表示 法和算法 稍加改 动,把边长 信息加 入即可。 例 12.6  程序填 空。 下列程 序的功 能是:读入源 程序 f,收 集并 按字母 顺序 输出 f内的标 识符 及其出 现的 行 号序 列(按行号 递增顺 序)。在 收集标 识符 的 过 程 中,生成 一棵 检 索 树 。树结 点的关 键字 值 是标 识符的前 10个字 符。树的 每个结 点与一 个链表 相联系 ,链 表结点 的值部 分 是 相应标 识 符在 f内 出现的行 号。阅 读该程 序,在其中 方框处 填上适 当字句 ,使该 程序完 成上述 功能。 12.3  程 序设 计 实例 · 355· 程序   #include <malloc.h>                        * 1*/ #include <stdlib.h> /* 2*/ #include <string.h> /* 3*/ #inclde"stdio.h" /* 4*/ structlnde{ /* 5*/ intlno; /* 6*/ structlnode* next; /* 7*/ }; /* 8*/ typedefstructlnode* lpt; /* 9*/ structwnde{ /* 10*/ charw[11]; /* 11*/ lptfirst,last; /* 12*/ structwnode*left,* right; /* 13*/ }; /* 14*/ typedefstructwnode* wpt; /* 15*/ wptroot; /* 16*/ intn,k; /* 17*/ charid[11],ch; /* 18*/ /* 19*/ voidwite(wptp){ /* 20*/ lptx; /* 21*/      { /* ① */ /* 22*/    rite(p->left); printf("% s\n",p->w); /* 23*/ /* 24*/      ;/* ② */ /* 25*/ do{  * 26*/ pintf("%8d",x->lno); x=x->next; }while(x!=NULL); printf("\n"); write(p->right); } }/* write*/ voidbtee(wpt* p){ wptv; lptq; v=* p; if(v== ULL){ /* 27*/ /* 28*/ /* 29*/ /* 30*/ /* 31*/ /* 32*/ /* 33*/ /* 34*/ /* 35*/ /* 36*/ /* 37*/ /* 38*/ /* 39*/ /* 40*/ · 356· 第十二章 动态数据结构 v=(wpt)malloc(sizeof(structwnode)); q=(lpt)malloc(sizeof(structlnode)); strcpy(v->w,id); v->left=NULL; v->right=NULL; /* 41*/ /* 42*/ /* 43*/ /* 44*/ /* 45*/      ;  /* ③ */ /* 46*/ v->last=q; /* 47*/      ;  /* ④ */ /* 48*/ q->next=NULL; * p=v; }elseif(strc p(id,v->w)<0) /* 49*/ /* 50*/ /* 51*/      ;  /* ⑤ */ /* 52*/    lseif(strc p(id,v->w)>0) /* 53*/      ;  /* ⑥ */ /* 54*/ else{ q=(lpt)malloc(sizeof(structlnode)); q->lno=n; q->next=NULL; * 55*/ /* 56*/ /* 57*/ /* 58*/      ;/* ⑦ */ /* 59*/ v->last=q /* 60*/ } /* 61*/ }/* btree*/ /* 62*/ /* 63*/ intmain void){/* main*/ /* 64*/ FILE* f; /* 65*/ if((f=foen("program.dat","r"))==NULL){ /* 66*/ printf("cannotopenfile:program.dat\n"); /* 67*/ exit(0); /* 68*/  } /* 69*/ ch=fgetc(f); /* 70*/ n=0; /* 71*/ root=NULL; /* 72*/ while(!feof(f)){ /* 73*/ n=n+1; /* 74*/ whil (ch!= \n){ /* 75*/ if(A <= h&& ch<= Z ||a <=ch&& ch<= z){ /* 76*/ k=0; /* 77*/ do{  * 78*/ if(k<10) /* 79*/ id[k]=ch; /* 80*/ k=k+1; /* 81*/ 12.3  程 序设 计 实例 · 357· } ch=fgetc(f);      ;  /* ⑧ */      ;  /* ⑨ */ btree(&root) }elsech=fgetc(f); } } write(root); } /* 82*/ /* 83*/ /* 84*/ /* 85*/ /* 86*/ /* 87*/ /* 88*/ /* 89*/ /* 90*/ /* 91*/ 解:该题要 求读懂 一个不完 整的程 序,并把其 填写完 整。读 这种程 序一般 需 要 反复读 多 遍。按照顺序 应该先 读说明 部分,确定 数据结 构;然后再 读主 程 序,读主程 序时 ,先 跳过子 程 序调 用部分 ,并 承认子 程序的功 能;最后 再一个一 个地读 子程序 。下面 逐步地 进行分 析: 1.数据 结构 由题意 及程序 说明部 分可以 看出,该程 序将 构造 一个 如 图 12.58所 示形 式 的以 标识 符 为关键字的检索树: 图 12.58 检索树 2.主程 序 (1)变 量使用 。 从第 17行 n的 说 明 和 第 71、74行 n的 变 化,看 出 n记 录 当 前 正 处 理 的 源 程 序 行 的 行号。 从第 18行 id的说明 和第 80行 id的变 化,看 出 id保 存当前 正处理 的标识符 。 从第 17行 k的说 明和第 77、81行 k的变化 ,看出 k记录标识 符长度 和放入 id中的 字符 位置。 (2)控 制结构 。 可以明 显地看 出:第 73行的 while语 句 控制 读完 (处理完 )整 个 源 程 序,该 语 句在 第 89 行结 束;第 90行的调 用函数 write打印 处理结果 ,即 打印生 成的检 索树。 · 358· 第十二章 动态数据结构 在第 73行 到第 89行的 循环语 句内:第 75行 的 while语句控制 读(处理)一 行源程 序,该 语句 在第 88行 结束。 分析第 75行到第 88行 的处理 一行源 程序的 循环语 句:条 件语句   if(A <=ch&& ch<= Z ||a <=ch&&ch<= z) 表示 ch当 前值是 标识符 的开始 符,所以第 77行到 第 86行应 该是 处理 一个标 识符;而 第 87 行的 fgetc则是越过 那些非 标识符 组成部 分的字 符。 分析第 77到第 86行处 理一个 标识 符部分 :第 78行是 一 个 do循 环 ,没 有 while与其 对 应。 从第 77到 第 86行处理 一个标 识符,应该有 一个给 标识符 填结束 符的操 作。所 以第 84、 85行的 两个框 ⑧、⑨应该 有一个 是与 do对应 的 while,一 个是给 标识 符填结束 符。 给标识 符 填结 束符应 该在读 入标识 符之后 进行,因此第 84行的方 框 ⑧应 该 是 对应第 78行 do语句 的 while部 分,第 85行 的 框 ⑨ 应 该 是 给 标 识 符 填 结 束 符 。 最 后,第 86行 的 函 数 调 用 btree(&root)显然是 把当前 读入的 标识符 存入树 中。这 样第 77行到第 85行就是 读入一 个标 识符部分。 继续分析 第 77行 到第 85行读 入一个 标识符 部分:k记 录放入 id中 的字符 个 数。 这样, 第 78到 第 84行的循 环 语 句 是 读入 一 个标 识 符 ;第 85行 是向 标 识符 结 尾 填字 符 串 结 束 符 “ \0”。因此 ,第 84行 的方框 ⑧应该 是控制读 到一个 标识符 结束符 ,其 内容应 该是   }while( A <=ch&&ch<= Z ||a <=ch&& ch<= z|| 0 <=ch&&ch<= 9) 而第 85行 的方框 ⑨应该为   id[k]= \0 3.函数 btree 通过对 主程序 的分析 ,函数 btree应 该 完成 把 标 识 符 id的信 息保 存入 树 中。 btree的 参 数 p是指向 指针的 指针 类型,在 主 程序 中以 “&root”对 应 p带 入 整 棵 树 调 用函 数 btree。 在 btree中 变量 p指向 root,p的 值是 root的地址 ,“*p”的 值就是 root的 值,指向树 的根结 点。 每次进入 函数 btree时,都用参 数“*p”带入 一棵树 ,进入 函数 后便用 “v=* p”使 v指 向 *p所 指的结 点,分以下 四种情 况: (1)v==NULL:也就 是说 * p带 入的是 空树,第 41到第 50行处理 这种情 况; (2)id<v->w :标识 符 id应 该在树 *p的左子 树上,第 52行处 理这种 情况; (3)id>v->w :标识 符 id应 该在树 *p的右子 树上,第 54行处 理这种 情况; (4)id==v->w :当 前结点 就是标 识符 id所在结 点,第 56到第 60行 处理这 种情况 。 下面分别分析上述四种情况: 第(2)、(3)两种情 况: 显然应 该分别 在 p的 左、右 子树 上 继 续 查找 标识 符 id。 由递 归调 用 btree来 完成 ,所 以 第 52、54行的 ⑤、⑥分别 应该填 入“btree(&(v->left))”和“btree(&(v->right))”。 12.3  程 序设 计 实例 · 359· 第(1)种情况 : 这时可能是: (1)最 开始处 理,遇到第一 个标识 符,如图 12.59所 示,树 为空树 。 (2)遇 到一个 树中没 有的新 标识符 而查到某 结点的 空分支 上了。 不妨设 目 前 查到某 结 点的 右子树 ,而 该右子 树为空。 如图 12.60所示 。 图 12.59 空树 图 12.60 遇到新标识符 不论怎 样,在 btree函 数 中处 理 是一 样 的。 下 面 用后 一 种可 能 为 背 景,叙 述 处 理 过 程。 经过 第 41到第 50行 诸已知 语句的 处理后 ,得到 如图 12.61所 示的结 果。现 在需要 : (1)把 v->first链 到 q所 指的行 号链头 ,也 就是 q所指变 量; (2)在 q->lno中填上行 号。 所以第 46、48行 的③、④分 别 填入 “v->first=q”和“q->lno=n”,得如 图 12.62所 示 的结果。 图 12.61 经过第 42到第 53行处理后 图 12.62 第(1)种情况最后结果 第(4)种情况 : 这时开 始状态 如图 12.63所示 。 新标识符 id与 该结点的 名字相 等,这时应 该把 id所在行 的行号 插入到本 结 点 的行号 链 · 360· 第十二章 动态数据结构 图 12.63 第(4)种情况开始状态 表的 链尾去 。经过 第 56到 第 60行 的 诸 已知 语 句的 处 理 后,得 到 如 图 12.64所 示 的 结 果。 在v->last指向 q所指 变量之前 ,显然应 该先使行 号链的 链尾接 到 q所 指变量 上。所 以第 59 行的 方框⑦ 应该填 入“v->last->next=q”,得 如图 12.65所示 的结果 。 图 12.64 经过第 53到第 57行工作后 图 12.65 加入第 56行语句后再工作 4.函数 write 通过对主 程序的 分析,函数 write应该完 成把保 存在 树中 的 标识 符信息打 印出 来。在 主 程序 中以 root对应参 数 p,带 入整棵 树,调用函 数 write。 每次进 入函数 write时,都 用变量 参数 p带入 一棵树 ,然后 按中序 遍历该 树。 中序遍 历采用 的是递 归算法 ,体 现在 第 23到第 31行上,所 以 显 然 第 22行 的方 框① 应 该是 控制递 归出口 ,填 入“if(p!=NULL)”。 再看第 23行到第 31行 :第 23行和第 31行 分别是 为遍 历左、右子 树而递 归 调 用本函 数 write;第 24行 到第 30行便 是处理 本结点 数据,即打 印本结 点标识 符及其 行号。 第 24行到第 30行:第 24行是打 印标识 符名字 ;第 26行 到第 29行 打印 x链 上的行 号序 列;显然 第 25行的方 框②应 该给 x赋值行 号链表 的表头 ,填入 “x=p->first”。 5.最后 结果 ① if(p!=NULL) ② x=p->first 习 题十二 · 361· ③ v->first=q ④ q->lno=n ⑤ btree(&(v->left)) ⑥ btree(&(v->right)) ⑦ v->last->next=q ⑧ }while(A <=ch&& ch<= Z ||a <=ch&& ch<= z|| 0 <=ch&& ch<= 9) ⑨ id[k]= \0 本章小结 本章讲 述十分 重要的 动态数 据结 构 。包括 动态 数据 结构概 念、各种 简单 的 动态 数据 结 构:栈、队列 、链 表、树。重 点掌握 动态数 据结构的 操作。 习题十二 12.1 设有变量说明 int*p1,*p2,*p3,指出下列程序段的执行结果。 (1) 1=(int*)malloc(sizeof(int)); p2=(int* )malloc(sizeof(int)); * p1=9; *p2=5*(*p1% 5); p3=p1; p1=p2; p2=p3; printf("%5d%5d%5d\n",* p1,* p2,* p3); (2) 1=(int*)malloc(sizeof(int)); * p1=25; p3=(int* )malloc(sizeof(int)); * p3=* p1; p2=(int* )malloc(sizeof(int)); * p2=3; * p3=* p3/*p2; * p2=* p1/*p3; printf("%5d%5d%5d\n",* p1,* p2,* p3); (3) 1=(int*)malloc(sizeof(int)); · 362· 第十二章 动态数据结构 * p1=5; p2=(int* )malloc(sizeof(int)); * p2=8; *p1=*p1 + *p2; printf("d% 5d%5d\n",* p1,* p2); (4) 1=(int*)malloc(sizeof(int)); * p1=12; p2=p1; p1=(int* )malloc(sizeof(int)); * p1=* p2+7; printf("d% 5d%5d\n",* p1,* p2); 12.2  设 有 数 据 类型 如 下 :    tructr{ intd; structtr* n; } 分别用循环和递归方法编写函数,求结点类型为 tr的链表长度。 12.3 分别用循环和递归方法编写函数,把给定的结点类型为 tr的单向链表倒过来。 12.4  编 写 一 个 函数 ,把 任意 给 定 的 单 向 环形 链 改 成 反 方 向的 单 向 环 形链。 12.5 编写函数,把给定的结点类型为 tr的一个单向链表接在另一个单向链表之后。 12.6  删 除 单 向 链表 中 所 有 值为素 数 的 结 点 。 12.7 有两个链表 a、b,结点包含信息学号、姓 名、4科成绩。编 写函数,带入 两个链 表,并从 a链上 删 除所有学号、姓名与 b链重复的结点。 12.8 把单向链表 c分别拆成两个链表 d、e。d中存放偶数值的结点;e中存放其他结点。 12.9 设 a、b是已按递增排序的单向链表。把既在 a中出现又在 b中出现的数从 a、b上摘下来,按递 增顺序生成链表 c。 12.10 生成 tr类型单向链表 b,b由 tr类型链表 a中那些数值不等的所有数组成。 12.11 编写函数,判断链表 a是否是链表 b的子链。 12.12  设字 符 串 由 链 表 形式 给 出 。编 写函 数 ,判 断 给 定 字 符 串 是否 是 回 文串 。 12.13  编写 函 数 ,用 冒泡 法 实 现单 向 链 表 排 序 。 12.14  分别 编 写 函 数 ,实 现 双 向链 表 的 创 建 、遍 历 、插 入 、删除 、交换 两节 。 12.15  分别 编 写 函 数 ,实 现 单 向环 形 链 表 的 创 建 、遍 历 、插 入 、删 除 、交换 两 节 。 12.16  分别 编 写 函 数 ,实 现 双 向环 形 链 表 的 创 建 、遍 历 、插 入 、删 除 、交换 两 节 。 12.17  考虑 单 向 链 表 和 双向 链 表 的 优缺点 。 12.18  考虑 怎 样 组 织 环 形链 能 使 操 作更方 便 ,以 及环 形 链 表 和 一般 链 表 的 优 缺点。 12.19  分别 使 用 循 环 和 递归 实 现 按 读入的 反 序 建 立 整 数 链表 。 12.20  已知 整 数 链 表 已 经按 递 增 排 序,编 写 函 数求 最 先 在给 定的 三 个 链 表 上 均 存在 的整 数 。 12.21 已知一个以单向链表为存储结构的线性表 A,表元素为 字符型。编写一 个函数 P实现把 表中 习 题十二 · 363· 的数字、字母、其他符号分离,形成三个链表 N(数字)、C(字符 )、O(其他)。要求 分离后 链表中 字符的 顺 序与原链表 A中的顺序一致。 12.22 一个整数可以表示成如图 12.66所示形式,设计这种表示法的数据类型,并编写函数,判 断任 意给定的两个整数的大小。 图 12.66 链表表示的整数 12.23 分别编写实现如图 12.66所示形式的两个整数的四则运算函数。 12.24 二进制数 b=b1b2…bn 可 以表示为如图 12.67所示的形式。分别编写函数实现如上 表示法的 任意 二 进制 数 的 加 法 、乘 法 和 除 法(用 递 归 和 迭代 分 别 实 现 )。 图 12.67 二进制数 12.25 按例 12.4的格式组织多项式的存储,实现两个多项式的乘法、除法。 12.26 三变元 x、y、z的多 项式 可以 表示 成如 图 12.68所 示 的形 式。例,3x6 +5x5 +6y6z表 示成 如 图 12.69所示的形式。设计这种表示法的数据结构,并编写函数实现两多项式的加法。 图 12.68 三变元多项式 图 12.69 3x6 +5x5 +6y6z 12.27 围绕山顶一圈有十个 山洞,有一只 狐狸 和一 只兔 子在 洞中 居住。狐 狸总 想找 到兔 子 以吃 掉 它,狐 狸 找 兔 子 的方法 是 先 到 第一 个 洞 找 ;再 隔 一 个 洞 ,即 到 第三 个 洞 找 ;再 隔 两 个 洞,到 第 六个 洞找 ;下 次再隔三个洞;再隔四个洞;依此类推。假定狐狸找 10次,请为兔子选择一个安全的住处。 12.28 编写程序选猴王。有 n个猴子站成一圈,从某指 定的第 m个猴子 开始报 数,当报到 数 r时,该 猴子被淘汰;然后再从 1开始重新报数,当 报到 数 r时,该猴 子被 淘汰;然 后再 从 1开始重 新报 数,依此 类 推。 求 哪个 猴 子 当 选 为 猴王 。 12.29 我不下地狱谁下地狱问题:15个和尚和 15个商人在沙漠中迷路,必须让一半人死掉,剩余的水 和粮食才能勉强维持其余人活命,走出沙漠。当时设定一个规则,30人 围成一圈,从第一个人 开始报数,每 数到 9时,该人自杀;然后继续报数,直到剩余 15个人为止。和尚们想“我不下地狱谁下地狱”,于是都决定 自己献身。编写程序,为和尚们找到位置,保证自己献身,保全 15名商人活命。 12.30 编写程序,根据输入的等价对,形成并输出相应的等 价类。等价对(a,b)表示元 素 a与 b等 价。若存在等价对 (a,b),且存在等价对 (c,b)或等价对 (b,c),则表示 a,b,c属于同一等价类。 12.31  模拟 动 态 存 储 分 配。 · 364· 第十二章 动态数据结构 采 用链 表 模 拟 实 现 动 态存储 分 配 ,分 配策 略 如下 : (1)设立一个可用空间表,该表连接每块可用空间为 一个链表,并记录每 块可用 空间的 长度、首地址。 初值是整块的可以用于动态存储分配的存储空间。 (2)当申请空间时,调用申请空间函数。申请空间函数在可用空间表上查找到最小的一块满足要 求的 空间,分配足量的空间给用户,并把剩余部分仍然保留在可用空间表上。类似 malloc函数功能。 (3)当回收空间时,用户调用回收空间函数。回收空间函 数把收回的 空间链入 可用空 间表上,并且 合 并相邻的可用空间块。类似 free函数。 12.32  编写 一 个 函 数 ,撤 销 (free)一棵 二 叉 树 的所 有 结 点 。 12.33  设二 叉 树 的 关 键 字是 整 数 ,编 写 函 数 ,删 除 (free)所有 键 值 小 于 给定 值 的 结 点 。 12.34  分别 编 写 用 如 下 顺序 遍 历 树 的函数 :    (1)访 问 根 结 点 ;         (2)遍 历 右子 树 ;          (3)遍历 右 子树 ; 遍历右子树; 访问根结点; 遍历左子树。 遍历左子树。 遍历左子树。 访问根结点。 并对例 12.1的树考察用如上遍历得到的结果。这些结果与那里得到的结果有什么关系? 12.35 定义一个数据结构代表 n元树,然后 编写函 数,遍历 n元树并 生成一 个包含 同样元 素的二 叉 树(设 n=4)。 12.36 按如下方法 存储名字表:用一个 26元素的 指针数组,数组 的每个元素 指向一棵二 叉检索树, 相应每棵树上所有名字的首字母相同。说明这种数据结构,并编写基于 这种结构的 名字表 管理程 序,其 功 能包括: (1)建 立 名 字 表 ; (2)检 索 一 个 名 字 ; (3)插 入 一 个 名 字 ; (4)删 除 一 个 名 字 ; (5)按 字 典 顺 序 打 印 名字表 ; (6)按 字 典 顺 序 打 印 以某字 母 开 头 的名 字 表 。 12.37 设计一个模拟家庭关系的数据结构。每个人用一个结构体表示,包含有他(她)的姓 名、性别、 年龄和指向其父亲、母亲、配偶、子女链的指针,如图 12.70所示。试编写以下函数: (1)增 加 一 个 新 人 ; (2)检 查 某 两 人 之 间 关系的 函 数 ; (3)检 查 某 两 人 是 否 堂兄弟 姐 妹 的 布尔 函 数 ; (4)检 索 某 人 ,并输 出 他 的直 系 亲 属 的 情 况 ; (5)遍 历 整 个 结 构 。 12.38 可以用二叉树来表示表达式,例如将(5-3)*2表示成如图 12.71所示的形式。设表达式中 分量类型为 int型: (1)设 计 这 种 表 达 式 树的结 点 类 型 ; (2)编 写 一 个 函 数 ,计 算 给 定表 达 式 的值 。 12.39 设前缀表达式及后缀表达式的文法 G1 和 G2 分别如下:   G1:E→ +(E,E)|*(E,E)|j 习 题十二 · 365· 图 12.70 模拟家庭关系的数据结构 图 12.71 用二叉树表示表达式   G2:E→(E,E)+|(E,E)* |j 其中 j为整数。现在有一个前缀表达式以字符方式保 存在文 件 f上,假定无 语法错 误,且运算 符不超 过 50 个。分别使用树和数组各编一个程序,把文件 f上的前缀表达式翻译成后缀表达式放在文件 g上。 12.40 由 BNF   E->d|(E,E)* |(E,E)+ 定义的表达式称为后缀表达式,其中 d是数字字符。现在定义    β(d)=d    β((E,E)+)= +(β(E),β(E))    β((E,E)* )=* (β(E),β(E)) 试编写一个函数,从 text文件读入上述 BNF定义的后缀表达式,输出经过 β变换的表达式。 12.41 设 d是数字(0~9),定义 (1)d是 β串; (2)若 M1、M2 是 β串,则 d(M1,M2)也是 β串。 再定义变换:    F(d)=d    F(d(M1,M2))=F(M1),F(M2),d 编写程序,对任意输入的 β串施行 F变换并输出。 12.42  定义 1]在树中,根结点的层次为 0;其他任意结点的层次为其父结点的层次 +1; [定义 2]一棵树的深度是该树中最大的层次号; [定义 3]一棵树的内部路径长度是该树的所有边的总数。 编写下列各函数: (1)找 出 树 中 指 定 结 点的层 次 ; (2)找 出 给 定 树 的 深 度; (3)找 出 给 定 树 的 内 部路径 长 。 12.43  可以 用 括 号 表 达 式表 示 树 ,例 如    E(B(A,D(C,)),F(,H(G,I))) 可以表示如图 12.72所 示的 树。编写 函数,把 以 字符 串给 出 的括 号 表达式形式的树处理成指针形式的树。 12.44  编写 函 数 ,把 给定 二 叉 树每 个 结 点 上 的 左 、右 子 树 全部交 换 。   图 12.72 树 · 366· 第十二章 动态数据结构 12.45  定义 :若两 棵 树 满 足 如下 条 件 ,则 称它 们 是 相 似 的。 (1)它 们 都 是 空 树 ; (2)它 们 都 不 是 空 树 ,但 它们 的 左 、右 子 树 分 别 相 似 。 编写一个 bool函数,判断任意给定的两棵树是否相似。 12.46  定义 :若两 棵 树 满 足 如下 条 件 ,则 称它 们 是 镜 像 相似 的 。 (1)它 们 都 是 空 树 ; (2)它 们 都 不 是 空 树 ,但 一棵 树 的 左 子 树 与 另一 棵 树 的 右 子 树镜 象 相 似 。 编写一个 bool函数,判断任意给定的两棵树是否镜象相似。 12.47  设有 三 叉 树 ,其 结 点 的数 据 结 构 如 下 :    truct ode{ charinfo; structnode* next1,* next2,*next3; }; 编写函数,以自顶向下,自左向右地顺序(层次遍历)打印给定三叉树各结点的 info值。图 12.73所示的三 叉树 打 印结 果 为 :ABCDEFGHIJKL。 12.48 设给定有向图 G,编写函数,求从任意给定结点 i开始第二步所能到达的所有结点。 12.49 网上找路径。在例 12.5中,网上没有边长,在图论中有的问题还涉及边长。图 12.74所 示就 是一个带边长的网。把例 12.5中网的三种表示方法分别改造成适应存储这种形式的结构。还能再设 计其 他表示方法吗?分别用改造过的例 12.5的三种表示方法和新设计的表示方法实现: (1)编写一个函数,求结点 r到结点 s的所有路径; (2)编写一个函数,求结点 r到结点 s的最短路径。 图 12.73 三叉树 图 12.74 带边长的网 第 十 三 章   三 论 函 数 ——— 几 个 较 深 入 的 问 题 本章讲述与函数有关的一些较深入的问题。 13.1  函 数 指 针 在数组 与指针 一节中 曾指出 数组 名 表示 数组 首 地址,若 将数 组名赋 值给 一 个类 型兼 容 的指 针变量 ,那 么这个 指针变量 也指 向这 个 数组 。 同 样,函数 名 也 具 有上 述相 同 的特 性,即 函数 名表示 函数控 制块的 首地址 ,函 数 控 制 块中 包括 函 数入 口地 址 等信 息。 如 果用 一个 指 针变 量来标识 函数控 制块的 首地址 ,则 称这个 指针变 量为指 向函数 的指针 变量 ,简 称指向 函 数的指针或函数指针。函数指针声明形式是:   类型符 (*标识符)(形式参数表); 其中 (1)标 识符是 被声明 的“指向 函数的 指针变量 ”名; (2)类 型符给 出函数 指针变 量所指 向函数的 类型信 息; (3)形 式参数 表给出 函数指 针变量 所指向函 数的形 式参数 信息。 例如     int(* f)(floatd,charc); 声明 指向“返回 int类型 值的函 数”的函 数指针 变量 f,f所指向 的函 数 有两 个形 式参数 ,第 一 个参 数是 float类型,第二 个参数是 char类型 。 函数指针 声明中 用括号 把星号 “*”和“标 识符”括 起来是必 须的,例 如 上述 的“(* f)”, 原因 是由优 先级造 成的。 引进函 数指针 概念不是 凭空臆 造的,它的 作用在 于: (1)使 用函数 指针调 用函数 ; (2)实 现其他 程序设 计语言 中函数 参数的功 能。 本节先 介绍利 用函数 指针调 用函数 ,下一 节介绍 函数参 数。 可以把 函数名 赋值给 一个函 数指针 变量,然后通 过函数 指针变 量调用 函数。形 式是:   函数指针变量 =函数名; 要求函 数指针 的特性 与函数 名的特 性一致,这种 一致性 体现在 : (1)它 们的返 回类型 相同; (2)它 们的参 数个数 相同; · 368· 第 十 三 章  三论 函 数 ——— 几 个 较 深 入 的 问题 (3)对 应位置 上,每个形式 参数的 类型相 同。 例 13.1  编写函 数,对给定整 数数组 排序,递增 或递减 按给定 参数决 定。   void ort(inta[],intn,char*flag){ boolascending(int,int);         * 函数原型 /*/ booldescending(int,int); voidswap(int* ,int*); bool(* ad)(int,int); /* 函数指针 /*/ intpass,c; if(flg=="ascending") /* 根据排序方向给函数指针赋值具体函数 */ ad=ascending; elsead=&descending; /* 函数名前加“&”和不加“&”意义相同, 这里也可以写成 “ad=descending”*/ for( ass=0;pass<n;pass++) /* 冒泡排序 */ for( =0;c<n-1;c++) if(( ad)(a[c],a[c+1]) )     /* 比较,此处使用函数指针 */ swap(&(a[c]),&(a[c +1 ] ) );  /* 交换 */ } boolascending(inta,intb){returna>b;} /* 具体说明函数 */ booldescending(inta,intb){returna<b;} voids ap(int* a,int* b){ inttemp; temp=* a; * a=* b; * b=temp; } 在本例题中: (1)函 数 sort有 三个参 数,a是被 排序的 数组,n是 数组长 度,flag是 递增或 递减标 记; (2)函 数 swap交换 两个变 量的值 ; (3)函 数 ascending判 断是否 a>b; (4)函 数 descending判断 是否 a<b。 当调用 sort时,sort根 据 flag的值 是否 “ascending”,决 定按 递增 或 递减 排序,把 函 数 名 ascending或 descending赋 值给函 数指针 变量 ad。 通过调 用 ad所指向 的函数 ,判 断是否 需要 交换 数组两个相 邻成分。 当需要交换 时,调用函数 swap。经过多次 扫描,最终达到排 序目的。 函数 sort使用了 指向函数 的指针 调用函 数 ascending或 descending。 使用函数指针要注意: (1)给 函数指 针赋值 时,右 端只是 一个函 数名,不允 许带参 数 表 。“ad=ascending”是 正 确的 ,而 “ad=ascending(int,int)”是 错误的 。 13.2  函 数 作 参 数 · 369· (2)不 能对函 数指针 变量进 行任何 运算。“ad+n”、“ad++”、“ad--”等都 是错误 的。 (3)利 用函数 指针调 用 函数 时,必 须 把 “* ”和 函 数 名 用 括号 括 起 来,成 “(* 函 数 名 ) (…)”形 式。因为 “()”的优先 级高于 “*”。在 例 13.1中 ,调 用 函 数 指针 ad所指函 数的 形 式是 “(*ad)(a[c],a[c+1])”,不 能写成“* ad(a[c],a[c+1])”。 13.2  函数 作参 数 一个函 数可以 调用其 他函数 ,这是 大家熟 知的 事实。 有时 遇 到这 种情 况 ,在 一个 函数 P 内要 调用另 一个函 数,但到底调 用哪一 个函数 要到执 行函数 P时才 能确定 。 例 13.2  编写程 序,用梯形公 式计算 并打印 定积分 ∫ ∫ ∫ 1 1 2 x3dx     sin2xdx   x3 + ex dx 0 -1 0 解:最好能 有一个 计算定积 分的函 数 integrate ∫b f(x)dx a 能够 计算任 意函数 f的定 积分,然后 分别以 函数 x3、sin2x、…为参 数调用 函数 integrate。 人们希 望 integrate能计算 任意函 数 f在任 意区 间 [a,b]上的 定积 分 。而 具 体计 算哪 个 函数 的 积 分 在 调 用 integrate时 确 定 。显 然 f作 为 integrate的 一 个 参 数 比 较 合 适 。 调 用 inegrate的 函数调用 可以写 成: integrate(g,a,b) 其中 g为被积 分函数 。这就 要求函 数 integrate带有函 数 参 数,integrate的函数 定 义 说明符 可 以写成: floatintegrate(float(* f)(float),floata,floatb) 函数作参 数时,在 形式参数 表中应 列出作 参数函 数的函 数原型 ,目 的是为 了 说 明该形 式 参数函数的特性。 下面继 续开发 函数 integrate。 设 Y0 =f(a),Y1 =f(a+h),Y2 =f(a+2h),…,Yn =f(b); 其中 h=(b-a)/n。 计算函 数 f在区间 [a,b]上 定积分 的梯形 公式是 : ∫b S = f(x)dx≈ h((Y0 +Yn)/2+Y1 +Y2 +Y3 +… +Yn-1 a 编写求积分的函数如下:   floatntegrate( loat(*f)(float),         * f为被积分函数 */ floata,floatb, /* a,b分别为积分区间的下、上界 */ intn){ /* n为积分区间分割个数 */ floath,s; inti; · 370· 第 十 三 章  三论 函 数 ——— 几 个 较 深 入 的 问题 h=(b-a)/n; s=((* f)(a)+(* f)(b)) /2.0; for(i=1;i<=n-1;i++)         s=s+(* f)(a+i* h); returns*h; } //调 用 函 数 “* f”,是 被 积 分 函 数,由 使 用 者决 定 另外分别编出 的函数说明: x3     sin2x       x3 + ex   floatcube(floatx){        eturnx*x*x; }; floatsin2(floatx){ returnsin(x)* sin(x); }; floatr(floatx){ returnsqrt(x* x* x+sqrt(exp(x))); }; 主程序如下:   mian(){ } printf("% f",integrate(cube,0.0,1.0,100)); printf("% f",integrate(sin2,-1.0,1.0,100)); printf("% f",integrate(r,0.0,2.0,100)); 其中 ,实 在参数 0、1和 -1.0、1.0以 及 0.0、2.0分 别 表 示积 分 区 间 [0、1],[-1.0、1.0], [0.0、2.0];100为把 积分区 间等分 成 100份 ,即 步长为 0.01;cube、sin2、r分 别为被 积函 数 的函 数标识 符。被积 函数 cube、sin2、r都 是一元 函数,其参 数是 float型的,结果类 型也是 float 型的 。即被积函 数 cube、sin2、r的函 数定义说明 符与 integrate形式参数表中的 函数参数说明   float(*f)(float) 是一致的。 函数作 参数就 是“指向函 数的指 针”作参 数,它的形 式是: (1)形 式参数 表中函 数形式 参 数以 函数 指 针的 函数 原型形 式说 明。 其中,函数 形式 参 数的 函数名 使用指 针并用 括号括 上,函 数 形式 参 数 的 形式 参 数 表 使 用函 数 原 型 形式 的形 式 参数 表,形式是   类 型名 (* 函数形式 参数的 形式参 数名)(函 数原型 形式的 形式参数 表) 例如:floatintegrate(float(*f)(float),… ) 13.2  函 数 作 参 数 · 371· (2)函 数调用 中对应 函数指 针形式 参数的实 在参数 直接使 用函数 名或函 数指针 。 (3)在 函数内 部,调用函数 指针形 式参数 表示的 函数,使用 形式 (* 形式参 数函数 名)(实在 参数表 ) 例如:s=s+(* f)(a+i*h) 使用函 数参数 应该注 意,函 数调 用中的 实在 参数 函数与 函数 定义中 的函 数 指针 形式 参 数必须一致。这种一致性体现在: (1)实 在参数 函数和 形式参 数函数 的返回类 型一致 ; (2)实 在参数 函数和 形式参 数函数 的参数个 数相同 ; (3)实 在参数 函数和 形式参 数函数 相对应的 每个参 数类型 一致。 例 13.3  用指向 函数的 指针作 函数参数 ,实 现例 13.1同 样的问 题:编写一 个排序 函数, 该函 数对给 定整数 数组既 可以按 递增排 序,也 可以按 递减排 序。 解:程序片 段如下 :     voidb bsort(ints[],intsize,bool(* p)(int,int)){ intu,v; voidswap(int* ,int* ); for( =0;u<size;u++) for v=0;v<size-1;v++) if( *p)(s[v],s[v+1])) swap(&s[v],&s[v+1]); } voidswap(int*r1;int*r2)       /与例 13.1同,略 boolascending(inta;intb){returna>b;} booldescending(inta;intb){returna<b;} 函数 bubsort有三个 形式参 数,s是 欲 排序 的 整数 数 组,size是 数 组 s的 尺 寸,p是 一 个 bool型 函数,调用 bubsort时,以 ascending函数或 以 descending函数 作实在 参数 对应 它 ,具 体 指明 是按递 增排序 还是按 递减排 序。swap函 数完成 交换两个 整数变 量中保 存的值 。 例 13.4  编写程 序,以 0.1为 间 隔,计算区 间 [0,1]内所有 正 弦 函数 和 所有余 弦函 数 之和。   #include <stdio.h> #include <math.h> double um(double(*func)(double),     * 参数 func是函数指针 */      doubled1,doubled2){ doubledt=0.0,d; for( =d1;d<d2;d+=0.1) dt+=(* func)(d); /* 用函数指针调用函数 */ returndt; } · 372· 第 十 三 章  三论 函 数 ——— 几 个 较 深 入 的 问题 void ain(){ doubles; s=sum(sin,0.1,1.0); /* 求 sin函数之和 */ printf("Thesum ofsinfor0.1to1.0is%e\n",s); s=sum(cos,0.5,3.0); /* 求 cos函数之和 */ printf("Thesum ofcosfor0.5to3.0is%e\n",s); } 程序运行结果为:   Thesum ofsinfor0.1to1.0is5.01388 Thesum ofcosfor0.5to3.0is-2.44645 函数 sum()的第一个参数为函数指针,该指针 指向的是带有一个 double型参 数并返回 double 类型数据的函数。而在头文 件 math.h中定义的函数 sin()与 cos()正是 这样的函 数,分别 以它们 作实在参数对应 sum函数的函数指针参数 func,达到函数 sum分别求 sin和 cos之和。 13.3  函数 副作 用 所谓函 数副作 用是指 ,当调 用函 数时,被调 用函 数除 了返回 函数 值之 外,还 对主 调用 函 数产 生附加 的影响 。例如 ,调用 函数时 在被调 用函数 内部进 行如下 操作: (1)修 改全局 量的值 ; (2)修 改主调 用函数 中声明 的变量 的值(一般 通过指 针参数 实现)。 函数副作 用会给 程序设 计带来 不必要 的麻烦 ,给 程序带 来十分 难以查 找的 错误,并且 降 低程 序的可 读性。 第三章 介绍表 达式 值 的 计 算时 曾 经 举 过 一个 例 子 ,由 于 双 目 运算 的两 个 运算 分量的计 算次序 不同,而带 来运算 结果不 同,就是由 函数 副 作用 引起的。 对 函 数副作 用 的看 法与对 GOTO语句的 看法一 样,在程 序设 计 语言 界一 直 有分 歧,有人 主张 保 留,有人 主 张取 消。我 们认为 ,可 以保留函 数 副 作用,但 是应 该 限制 程序 员 尽 量 不要 使用 函 数副 作用。 由于函数副作用的影响: (1)会 使双目 运算的 结果依 赖于两 个运算分 量的计 算次序 ; (2)还 可能使 某些在 数学上 明显成 立的事实 ,在 程序中 就不一 定成立 。 例如,在数 学上乘 法符合交 换律,a*f(x)与 f(x)* a显 然相等 。但 是 在程 序中,若函 数 f改变 全局量 a的值,则上 述交换 律就不 成立。设 有函数 :   floatf(loatu){ a=a*2; return2* u } 13.4  形式 参 数 作 实 在参 数 · 373· 假定,计算 时开始 a=3,x=5;双目 运算符的 运算对 象从左 向右计 算。计 算 a* f(x): 第一步 ,求 运算分 量 a的值 ,为 3; 第二步 ,求 运算分 量 f(x)的值 ,调 用函数 f,u取 x值 为 5,进入 f执 行“a=a* 2”,a得 6, 再执 行“return2* u”,得函 数值为 10,返 回; 第三步 ,计 算表达 式值为 3*10,得 30。 而在同 样条件 下计算 f(x)*a: 第一步 ,求 运算分 量 f(x)的值 ,调 用函数 f,u取 x值 为 5,进入 f执 行“a=a* 2”,a得 6, 再执 行“return2* u”得函数值 为 10,返回 ; 第二步 ,求 运算分 量 a的值 ,为 6; 第三步 ,计 算表达 式值为 10*6,得 60。 计算结果 显然不 一样,使乘 法交换 率不成 立。这 就是因 为副作 用的影 响造 成的,因为 在 函数 f内 改变了全 局量 a的值。 若函数 有指针 参数,在 函 数 分 程 序 内 修 改指 针 参数 所 指 变 量 的 值 ,也 将产 生 函 数 副 作 用,并引 起同样 的问题 。例如有 函数声 明:   floatf(nt*a,floatu){ *a=*a* 2; return2* u } 该函数存 在副作 用,调用该 函数将 使用“f(&z,e)”的 形 式。 其 中,z是 一个 变 量,e是 一 个表 达式。该 函数在 计算表 达式“z*f(&z,e)”时,将 产 生与 第三 章讲述 过的 例子相 同的 问 题,表达 式的值 依赖于 运算分量 的 计 算 次 序;即使 计 算 次 序 固定 ,也同 样 会 产 生 与上 述相 同 的问 题,使“z* f(&z,e)”不 等于“f(&z,e)*z”。 希望读 者在编 写程序 时,尽 量不要 使用带 副作用 的函数 。 13.4  形式 参数 作实 在参 数 一个标 识符只 要在作 用域内 就可 以 随意 使用,形 式参数 标识 符在作 用域 内 当然 可以 用 来作 实在参 数。下 述例 13.5程 序片断 是合法的 。在例 13.5中: 第 7行的 函数调 用以函 数 g的 形式参 数 x2独立作 为一个 表达式 ,作 函数 f的参数 x1的 实在 参数,带入 值 2.5。 第 8行的 函数调 用,函数 g的 形式 参 数 y2在表 达式 “* y2”中 ,表达 式“*y2”作函 数 f 的参 数 x1的实在 参数,把表 达式“* y2”的值(a的值)3.5带 入 x1。 第 9行的 函数调 用以函 数 g形式 参数 x2的 地址 &x2作 函数 f形式 参数 y1的实在 参数。 · 374· 第 十 三 章  三论 函 数 ——— 几 个 较 深 入 的 问题 在函 数 f内,形式参 数 y1指向 函数 g的形 式参数 x2,第 3行 赋值 语句 给 函 数 g的形 式参 数 x2赋值 1.0。在从 函数 f返回时 ,函 数 g的形 式参数 x2的值是 1.0。 第 10行的 函数调 用以函数 g的形 式参数 y2作 函数 f形式 参数 y1的实 在参 数,把 y2的 值送 入 y1中。在 f内,形式参 数 y1指 向函数 g的 形 式参数 y2指向 的变量;而 在 函数 g中, 形式 参数 y2指向 主程序 中的变 量 a;所以 在函数 f内 ,形 式参 数 y1也指 向主程序 中的 a,第 3行赋值 语句给主 程序中 变量 a赋 值 1.0,在从函 数 f返回时,主程 序中 a的 值是 1.0。 例 13.5  形式参 数作实 在参数 。   floata,d;                    * 1*/ voidf(loatx1,float* y1){ /* 2*/ * y1=x1* 2; /* 3*/ } /* 4*/ voidg(loatx2,float* y2){ /* 5*/ floatc; /* 6*/ f(x2,&c); /* 7*/ f(* y2,&c); /* 8*/ f(0.5,&x2); /* 9*/ f(0.5,y2) /* 10*/ f(0.5,y2) /* 11*/ } /* 12*/ intm in(void){ /* 13*/ d=2.5; /* 14*/ a=3.5; /* 15*/ g(d,&a) /* 16*/ } 13.5 参 数结 合顺 序 C语言没 有对调 用函数时 的参数 结合 顺序 作规 定,甚 至没 有规 定函 数名 与 各个 实在 参 数间 的计算 次序,它们 是依赖于 实现 的。 这就 是 说,对不 同机 器 或 不 同编 译系 统 ,当 调用 函 数时 ,函 数名的 计算、各个参 数值的 计算以 及参数 的结合 顺序 可 能是 不同的。 这 就 给用户 程 序带 来某些不 确定性 ,不便于程 序移植 。如下 例 13.6说 明参数 结合顺 序不同 带 来 的程序 执 行的 不确定性 。同时 ,这个例子 也说明 了函数 副作用 带来的 问题。 希望大 家编 写程 序 时,一 定尽 量避免函 数副作 用,不要使 程序的 结果依 赖于函 数名、各个 实在参 数的求 值 顺 序以及 参 数的结合顺序等。 例 13.6  参数结 合顺序 不同带 来的程序 执行的 不确定 性。    /* P OGRAM s*/ #include"stdio.h"                 * 1*/ 13.5  参 数结 合 顺序 · 375· intz; intd(itx,inty){ returnx-y } intq( ntx){ z=z-x; returnx* x } int ain(void){ z=10; printf("%d",d(q(z),q(z))) } /* 2*/ /* 3*/ /* 4*/ /* 5*/ /* 6*/ /* 7*/ /* 8*/ /* 9*/ /* 10*/ /* 11*/ /* 12*/ /* 13*/ 1.当参 数的求 值与结合 顺序是 从左向 右时,该程 序的执 行过程 是: (1)z=10。 (2)计 算函数 d。 (2.1)调用 函数 q,求 d的第 一个实 在参数 的值。 (2.1.1)把全 局量 z的值 10送入 q的形式 参数 x中 。 (2.1.2)在 q中作 z=z-x,得 全局变 量 z的值 为 0。 (2.1.3)在 q中作 returnx* x,得函数 值为 100,返回 。 (2.2)函数 q带回 函数值 100作为 函数 d的第一 个参数 x的实在 参数值 。 (2.3)调用 函数 q,求 d的第 二个实 在参数 的值。 (2.3.1)把全局 量 z的值 0送入 q的形式 参数 x中。 (2.3.2)在 q中 作 z=z-x,得 z的值仍为 0。 (2.3.3)在 q中 作 returnx* x,得函数 值为 0,返回。 (2.4)函数 q带回 函数值 0作为函 数 d的 第二个 参数 y的实在 参数值 。 (2.5)进入 d计算 returnx-y,得 100。 (2.6)函数 d带回 函数值 100。 (3)打 印 100。 2.而当 参数求 值与结合 顺序是 从右向 左时,该程 序的执 行过程 是: (1)z=10。 (2)计 算函数 d。 (2.1)调用 函数 q,求 d的第 二个实 在参数 的值。 (2.1.1)把全局 量 z的值 10送 入 q的形式 参数 x中 。 (2.1.2)在 q中 作 z=z-x,得 z的值为 0。 (2.1.3)在 q中 作 returnx* x,得函数 值为 100并返 回。 (2.2)函数 q带回 函数值 100作为 函数 d的第二 个参数 y的实在 参数值 。 · 376· 第 十 三 章  三论 函 数 ——— 几 个 较 深 入 的 问题 (2.3)调用 函数 q,求 d的第 一个实 在参数 的值。 (2.3.1)把全 局量 z的值 0送入 q的形 式参数 x中。 (2.3.2)在 q中作 z=z-x,得 z的值仍 为 0。 (2.3.3)在 q中作 returnx* x,得函数 值为 0并返回 。 (2.4)函数 q带回 函数值 0作为函 数 d的 第一个 参数 x的实在 参数值 。 (2.5)进入 d计算 returnx-y,得 -100并返回 。 (2.6)函数 d带回 函数值 -100。 (3)打 印 -100。 3.进一 步,当参数 求 值与 结 合顺 序 是并 行 时 ,即 两 个实 在 参数 同 时 求 值 (可 能 在 不 同 CPU上 ),同时 与形式 参数结 合。这 就要看微 观上哪 个参数 执行得 快些了 。 (1)若 左边执 行得快 些,先 作了 z=z-x,右边才 取 z值,则 与从左 向右结 合 时 的结果 一 样,打印 100; (2)若 右边执 行得快 些,先 作了 z=z-x,左边才 取 z值,则 与从右 向左结 合 时 的结果 一 样,打印 -100; (3)若 两边速 度差不 多,一 边取出 z值,正在计 算 z-x,还没 送回 z里 ,另 一 边 也取出 了 z值。 计算结 束后,打印 0。 13.6 可 变长 度数 组 13.6.1 可变长 度数组 到目前为 止,数组 尺寸都是 固定的 。问题 是,如果数 组的 尺 寸不 固定怎么 办? 如果第 六 章例 6.2求任 意大小 两个矩 阵 Amn、Bnp的积 Cmp怎 么办?可 变长度 数组(相 当于 ALGOL 60的动 态数组 )可以解 决这个 问题,例 6.2的程序 可以编 写成例 13.7的形式 : 例 13.7  矩阵乘 积——— 使用可 变长度数 组。   voidm trixproduct(void){                  * 1*/ intm,n,p; /* 2*/ printf("\npleaceinputsizeofmatrix:m,n,p="); /* 3*/ scanf("%d% d%d",&m,&n,&p); /* 4*/ {    * 5*/ floate,a[m][p],b[p][n],c[m][n]; /* 6*/ inti,j,k; /* 7*/ for( =1;i<m ;i++) /* 8*/ for(j= ;j<n;j++ /* 9*/ e=0; /* 10*/ 13.6  可 变长 度 数组 · 377· } } } for( =1;k<p;k++) e=e+a[i][k]* b[k][j]; c[i][j]=e; /* 11*/ /* 12*/ /* 13*/ /* 14*/ /* 15*/ /* 16*/ 这个函数采用 的就是可变长度数组。数组 a、b、c在复合语句(第 5行 ~第 15行)中说明,有 关它每维的长度 m、n、p在外层复合语句中说明并 输入。当程序执行到数组所在复合语句时(第 5 行),创 建数组并按当时 m、n、p的实际值为数组分 配存储空间;当程序 执行到 第 15行,退出 该复 合语句时,删除数组 并释放它所占用的存储空间。有关可变长度数组有如下说明和规定: (1)可 变长度 数组在 复合语 句作用 域中声明 。 (2)可 变长度 数组变 量作用 域从声 明点延续 到最近 包含它 的声明 的复合 语句结 束。 (3)数 组各维 的长度 可以使 用 非常 量 表 达 式,不 过当 程 序 执 行到 相 应 数 组 声明 符开 始 位置 时,该表达 式的值 必须是可 计算的 。 (4)可 变长度 数组只 有到程 序运行 时才能动 态确定 其长度 。 (5)当 程序执 行遇到 可变长 度数组 声明时,对 长度 表 达 式 求 值(长 度值 必 须是 正整 数) 并生成相应长度的数组。 (6)可 变长度 数组变 量的生 存期是 :当程 序运行 到达它 的声明 点并生 成该 数组 开 始,直 到程 序运行 离开它 的作用 域为止 。当生 存期结束 时,删除相 应的可 变长度 数组。 (7)可 变长度 数组一 旦生成 ,在它 的生存 期内不 能改变 它的长 度。 (8)不 能说明 可变长 度数组 为 static和 extern存 储类别 。 (9)不 允许把 结构成 员和联 合成员 说明成可 变长度 数组。 13.6.2 可变长 度数组作参 数 例 13.7的程 序每次 计算矩 阵 乘积 时 都 必 须输 入参 与计 算的 矩阵 的 尺寸 。 还可 以使 用 可变 长度数 组 作 函 数 参 数 (类 似 Pascal的 可 调 维 数 组 参 数 ),实 现 上 述 矩 阵 乘 积 问 题。 例 13.8比上述 例 13.7的 程序更灵 活。 例 13.8  矩阵乘 积——— 使用可 变长度数 组作参 数.   voidmatrixproduct intm,intn,intp, floata[m][p],floatb[p][n],floatc[m][n]){  /* 1 */                loate; /* 2*/ inti,j,k; /* 3*/ for( =1;i<m ;i++) /* 4*/ for(j=1 j<n;j++){ /* 5*/ e=0; /* 6*/ for( =1;k<p;k++) /* 7*/ · 378· 第 十 三 章  三论 函 数 ——— 几 个 较 深 入 的 问题 e=e+a[i][k]* b[k][j]; /* 8*/ c[i][j]=e; /* 9*/ } /* 10*/ } /* 11*/ 在该程 序片段 中,数组的尺 寸和 参与 运算 的 数组 全部 作 为函 数的 形 式参 数。当 调用 函 数时 ,以 实在参 数形式 给定相应 尺寸 和 参 与 运算 的实 在 参数 数组 。有 关 可 变 长 度数 组作 函 数参数有如下说明和规定: (1)可 变长度 数组作 函数参 数 和前 述 可 变 长度 数 组 的 本质 区别 在 于 :可 变 长度 数组 在 它生 存期开 始时生 成,被分配存 储 空 间,而 可 变长 度 数 组 参 数不 被 分 配 存 储空 间 ,它 以实 在 参数 数组参 与运算 ,在 函数内,它 表记实 在参数数 组; (2)数 组尺寸 可以不 作为形 式参数 ,例如   voidf(intm,inta[r][s]) 当调 用函数 时,以其他 地方声明 的 变 量 r、s计算 形 式 参 数 a的尺 寸,当然 r、s必 须在 作用 域 之内 ,也 必须在 生存期 之内,并且 必须有 值。 (3)如 果数组 尺寸也 作为形 式参数 ,在形 式参数 表中,按程 序行文 次序它 必 须 放在可 变 长度 数组参 数之前 (如 同例 13.8一样),下述 函数定 义说明符 是错误 的   voidf(inta[s][r],intr,ints) (4)在 函数原 型声明 中,由 于 只 关 心 形式 参 数 数 组维 数 ,不 关 心 每维 的尺 寸 ,可 以不 指 明每 维尺寸 而以星 号“*”代替 ,下 述函数 原型都 是等价 的:     voidf(int,int[* ][*]); voidf(int,int[* ][m]); voidf(int,int[n][n]); voidf(int,int[][* ]); 尽管第三 个声明 似乎表 达了该 参数是 一个方 阵,但是实 际上,它还 是依据 函 数 定义时 形 式参数数组说明和相应实在参数给定的尺寸决定每维的尺寸。 本章小结 本章三 论函数 ,讲 述一些涉 及函 数的 较为 深 入的 问题。包 括 函数 作 参 数 、函 数副 作用、 形式 参数作 实在参 数、参数结合 顺序等 。重点 掌握函 数作参 数。 习题十三 13.1 下 述 声 明 是否 正 确 ? 正确的 举 例 说 明 意 义 ,错 误 的 说明 原 因 。 习 题十三 · 379· (1)intfunc(int,int); (2)intfunc(inti,intj); (3)int* func(int,int); (4)int* func(inti,intj); (5)int(* func)(int,int); (6)int!(* func)(inti,intj); (7)typedefint* func(int,int); (8)typedefint* func(inti,intj); (9)typedefint(* func)(int,int); (10)typedefint(*func)(inti,intj); (11)int* p(); (12)int* (p()); (13)int(* p)(); (14)int* (* p)(); 13.2 下 述 程 序 是否 正 确 ,说 明 它 的 运 行 结 果。    include"stdio.h" intsum(intm,intn){returnm+n;} intsub(intm,intn){returnm -n;} intmul(intm,intn){returnm* n;} intdiv(intm,intn){returnm/n;} intopration(intu,intv,intop){ int(*p[])(int,int)={sum,sub,mul,div} return(* p[op])(u,v) } voidm in(){ printf("%d\n",operation(40,20,0)); printf("%d\n",operation(40,20,1)); printf("%d\n",operation(40,20,2)); printf("%d\n",operation(40,20,3)); } 13.3 编写函数,实现数组元素的查找。被查找元素的匹配规则由本函数的使用者按统一的接口 规则 定义 (使 用 函 数 指 针 )。 13.4 编写一个函数,用割线法求解方程 f(x)=0的根。 13.5 编 写 一 个 函数 ,求 任意 给 定 一 元 整 型 函数 (参 数 也为 整 型 )在区 间 [a,b]上的 最 大 值 。 13.6 编写一个函数 max(f,a,b),求函数 f(x)在区间[a,b]上的极大值。 13.7 编写一个函数 tabulate,打印任意 给定的 一元实 型函数 (参数也 为实型 )在区间 [a,b]上 步长 为 step的 函 数 表 。 13.8 编 写 程 序 ,用 梯 形 公式 求 如 下 定 积 分 。 ∫ ∫ ∫ 1 1 2 sinxdx     cosxdx     exdx 0 -1 0 · 380· 第 十 三 章  三论 函 数 ——— 几 个 较 深 入 的 问题 13.9 用函数指针实现,输入整数 n,当 n为奇数时计算 1+ 1 3 + 15 +… + 1 n    当 n为偶数时计算 1+ 1 2 + 14 +… + 1 n 13.10 试用随机数方法计算圆周率 π的近似值。      提 示 :以 原 点 为 圆 心 作单 位 圆 和单 位 正方 形 。 13.11  试用 随机 数 方 法 计算 积 分 ∫2 1 -2 2 x2 -4dx 13.12 给定一个铁路系统若干条铁路线上每日车 次的时候 表,分别用 数组和 文件表 示这些 数据。编 写一 个 查询 函 数 ,对 给 定 的 车 站 和列 车 行 使方 向 ,能 查出各 列 车 到 达 和 发 车时 间 。 13.13 按递增顺序生成并打印前 20个自然数 x,使 x= a3 + b3 13.14 找一个最小的自然数 x,使 x=a3 +b3 =c3 +d3 其中 a、b、c、d都是自然数,且 a≠b、c≠d。 13.15 按递增顺序生成并打印前 20个自然数 x,使 x=a3 +b3 +c3 13.16 找一个最小的自然数 x,使 x=a3 +b3 +c3 =d3 +e3 +f3 其中 a、b、c、d、e、f都是自然数,且[a,b,c]≠ [d,e,f]。 13.17 求前 10个满足 i3 +j3 +k3 =m3 的 m,且诸 m互不相关。a1、b1、c1 与 a2、b2、c2 相关是指同 时满 足 a1% a2 =0,b1% b2 =0,c1% c2 =0,a1/a2 =b1 /b2 =c1 /c2。 13.18 编写程序,任意输入 8个整数,把这 8个整数放在一个立方体的 8个顶点 上。找到使该立 方体 每个面上四个数之和互相相等的摆放方案并输出。 13.19 编写程序,求九位累进可除数。九位累进可 除数是 指:该九位 整数由 1、2、3、…、9这 九个数 字 组成,每个数字只出现一次;并且,该整数的前 一位组 成的整 数可以 被 1整 除,前 二位组 成的整 数可以 被 2 整除,前三位组成的整数可以被 3整除,依此类推,前九位组成的整数可以被 9整除(例如 381654729)。 13.20 设计算法,从 n项不同价值不同重量的邮件中选取一部分,在不超过规定重量的情况下该 部分 价值最高。 13.21 某乐团要在 50首歌曲(编号 1~50)中选择一些受欢迎的曲子在演唱 会上演出,进行一次 民意 测验。每个被调查人按顺序提出 5首他所喜爱的歌曲填入如下表格内: 姓名: 性别: 年龄: 1. 2. 3. 4. 5. 最后 调 查数 据 存 入 一 个 文 件中。 以 该 文 件作 为 输 入 ,编 写 计 算 并 输出 下 列 结 果 的 程序 : (1)一 张 按 受 欢 迎 程 度排列 的 歌 曲 表。 每 项 包 含歌曲 编 号 及 其 被 提 名次 数 。 习 题十三 · 381· (2)把被调查人分别按性别和年龄(20及以下和 20以上的)分成四组,给出每组人的 名单及第一 位提 名的 最 受本 组 人 欢 迎 的 三 个歌曲 之 一 (某 人 的 这 栏可能 为 空 )。 13.22 猜数游戏。计算机选择一个四位数,由游戏 者来猜 这个数,游戏 者在终 端不停 地输入 四位数, 每输入一次,计算机都指出该数猜对几位数字及有几位正 确数字的位 置也正确,直到游戏 者全部 猜正确 为 止,计 算 机 输 出游 戏 者 所 猜 的 次 数。 编写 程 序 ,实 现该 游戏 。 13.23 有一堆火柴共 n根,两人轮流拿取,每次最少拿 1根,且不许超过 k根,谁 最后拿完谁 输。编写 程序 为 其中 一 人 提 供 咨 询 ,使 他 经常 立 于 不败 之 地。 13.24 如图 13.1所示,摆有 n行火柴,第 j行摆 j个 火柴。编写 一个人 机游戏 程序,游戏规 则是每 次 只能 从 一行 上 取 走 部 分 或 全部火 柴 ,最 后 一次 取 火柴 者 胜 利 。 13.25 求图 13.2中 A点到 B点的所有路径和所有最短路径。 图 13.1 火柴游戏 图 13.2 最短路径 13.26  哥尼 斯堡 桥 问 题 (欧 拉 一笔 画)。 这是图论中的一个著名问题,在平面上给出一图后,能否把它一笔画出。设计图的表示方法,并编 写程 序,对 给 定 的 图判 断 是 否 能 把 它 一笔 画 出 ,若 能 则 给 出路径 ;否 则 给出 无 解 信 息 。 13.27 有 n张卡片,记为 1、2、3、…、n。编写程 序给其 排定一 个顺序,使得 翻开第 一张卡 片是 1,然 后 把下两张卡片依次放在末尾;再翻开第一张卡片是 2,然后再把下两张卡片依次放 在末尾;再翻开第一 张卡 片是 3,然后再把下两张卡片依次放在末尾;依此类推;直到翻开第 n张卡片为止。 13.28 已知一叠卡片共 2n张,求经过多少次相叉才能使第 p张变成第 q张。相叉是 指分别取前 n张 和后 n张把卡片分成两摞,然后依次每摞各取一张,直到取完为止,构成新的一叠。 13.29 已知有序列 A、B。如下序列 C称为 A、B的一个相叉序列:在 C的开头先顺序放入某序列 的若 干元素,然后把 A、B两序列的元素一个个交替放入 C序 列中;最后若 某序列 元素用 尽,则 把另一 个序列 的 剩余元素顺序全部放入 C序列末尾。编写程序,求任意两字符序列的所有相叉。 13.30 若上题中相叉定义修改成在把 A、B两序列 的元素 交替放 入 C序列中 时允许 分别取 若干个 不 等数 量 的元 素 ,应 该 怎 样 编 写 程 序? 13.31 画 n次 Sierpinski曲线。1次、2次、3次 Sierpinski曲线如图 13.3所示。 13.32 稀疏矩阵是绝大多数元素为 0的矩阵,为了节省存储空间,经常只保存那些非 0元素。如 下多 重链表是保存稀疏矩阵的一种方案: (1)每个非 0元素是一个基本数据结构,其结构如图 13.4所示; (2)把每行非 0元素构成一个环形链表,left为其链指针; (3)把每列非 0元素亦构成一个环形链表,up为其链指针; (4)在 每 个 基 本 数 据 结构上 :row为 行标 ;col为 列 标 ;val为 值 ; · 382· 第 十 三 章  三论 函 数 ——— 几 个 较 深 入 的 问题 图 13.3 Sierpinski曲线 (5)每个环链上加一个哨兵结点,哨兵的 col、row值为 -1。 图 13.5所示的矩阵存储为图 13.6所示的形式。    图 13.4 稀疏矩阵非 0元素数据结构 图 13.5 稀疏矩阵 在图 13.6中,colpt、rowpt分别为指针数组,数组元素分别指向稀疏矩阵各列、各行的环形链。 图 13.6 稀疏矩阵的存储形式 设 计矩 阵 的 外 部 数 据 格式,编写 读 入 稀疏 矩 阵以 及 实 现 稀 疏 矩 阵加法 、乘 法 的函 数 。 第 十 四章   C语 言 独有 的 特 性 14.1  运   算 C语言的 运算符 非常丰富 ,表 3.1列出 了 所 有 的 C语言 运算 符,包括 运算 符 的记 号、运 算、类别 、优先 级、结合 关系等。 常用的 运算符 及其意 义已经 在前面 相关章 节中 介绍 过 ,本 节 介绍 那些 C语言 独有的 特色。 14.1.1  sizeof sizeof用于求 操作数 的尺寸 ,其格 式是:       izeof(类 型名) 或 sizeof(表 达式) (1)第 一种格 式求指 定类型 的数据 对象长度 ,即 存储相 应类型 数据所 需要的字 节数; (2)第 二种格 式求指 定表达 式计算 结果类型 的对象 长度。 表 14.1列出 了在微 型机上 各种数 据类型、数 据对象 占用的存 储量。 表 14.1  各种数 据类型、数 据对象 占用的存 储量 操作数 sizeof(char) shorts;… sizeof(s) sizeof(int) sizeof(long) sizeof(float) doubles;… sizeof(s) shorts;… sizeof(s+0.0) doublea[10];… sizeof(a) doublea[10];… sizeof(a[1]) 结果 1 1 2 4 4 8 4(结果类型为 float) 80 8 · 384· 第十 四 章   C语 言 独 有 的 特 性 14.1.2 赋值运 算 前述章 节仅介 绍一个 最 基 本 的 赋值 运 算符 “=”,事 实 上 C语 言 的 赋 值 运 算 符 十 分 丰 富,“=”可 以和一 些双目 运算符结 合,形成新 的附 加运算 意义 的赋值 运算 符,称为复 合赋 值 运算符。复合赋值运算符包括:    +=  -= *=  /= %=  <<=  >>= &= ^=  |= 使用这些复合赋值运算符的格式是:   操 作数 1 赋值 运算符  操作 数 2 复合赋值运算   aop=b 与简单赋值运算   a=aopb 等价 。例如 ,设 x=3则   x+=x*5; 相当于   x=x+(x*5); 结果 x中为整 数 18。 14.1.3 顺序表 达式 用逗号运 算符“,”分 隔开的若 干个表 达式称 为逗号 表达式 ,又称 为顺序 表达式 。逗号 表 达式 按行文顺 序从左 向右计 算各个 子表达 式的值 。表达 式的结 果类型 是最右 端 表 达式的 类 型,表达 式的结 果值是 最右端表 达式的 值。例 如   j=(x=0.5,y=10,15+x,y=(int)x+y*2) 将顺序地进行如下操作:   先 计算“x=0.5”,给 x赋值 0.5,得 float类型的 0.5;   再 计算“y=10”,给 y赋 值 10,得 int类 型的 10;   再 计算“15+x”,得 float类型的 15.5;   再 计算“y=(int)x+y*2”,给 y赋值 20,得 int类型的 20; 最终 ,括 号内表 达式的 结果值是 20,结果类型 是整数 类型,j被赋 值 20。 14.1.4 条件表 达式 C语言条 件表达 式格式是 : 操作 数 1  ? 操作 数 2  :  操作数 3 14.1  运    算 · 385· 该表 达式含 有两个 运 算 符 “?”和 “:”以 及 三个 操 作数。 条 件 表 达 式是 三 元 表 达 式,运 算 符 “?”和“:”合并使用 称为三 元运算 符。条 件表达 式的计 算步骤是 : (1)计 算“操作 数 1”; (2)若 “操作数 1”的值 为 true,则计算“操 作数 2”,表 达式的 值为“操 作数 2”的值; (3)否 则“操作 数 1”的 值为 false,计算“操 作数 3”,表达式的 值为“操 作数 3”的值。 条件表 达式是 右结合 的,优 先级别 高于赋 值运算 符,低于二 元操作 符。 表达式 语句“x=a?b:c;”相 当于如下 条件语 句:   if( !=0) x=b; else x=c; 由于右结 合的特性,表达式 “u=a>b?x:c>d?y:z”相当于“u=a>b?x:(c>d?y:z)”,用 条 件语句表示如下:   if(a b) u=x; elsei(c>d) u=y; elseu=z; 14.1.5  位 运 算 C语言可 以直接 针对二进 制位 进行 操作,这 使 得用 它描 述系 统程 序 十分 方便。 位运 算 的所 有操作 数必须 为整数 类型,表 14.2列出 C语言 的位 运算 符,下面分 别介 绍它们 定义 的 运算。 记号 ~ << >> & ^ | 表 14.2 C语言的位运算符(按优先级从高到低) 运算符 按位取反 左 移 、右 移 按位与 按位异或 按位或 类别 一元 二元 结合关系 从右到左 从左到右 优先级 15 11 8 7 6 1.按位 取反 按位取反运算的格式是: · 386· 第十 四 章   C语 言 独 有 的 特 性    ~操作数 该运算 “~”对操作 数结果 值的二 进制表 示的每 一位取反 码。 例 14.1  如果 X是一 个 int类 型的整 数,十 六进制 表示为   0XF0F0 它的二进制表示为   1111000011110000 ~X结 果的二 进制表 示为   0000111100001111 十六进制的表示为   OX0F0F 2.位移 运算 位移运算的格式是:   操作数 1 位移运算符 操作数 2 C语 言有两个位移运算 符“<<”和“>>”。其中“<<”为左移 ;“>>”为 右移;“操作数 1” 是要 进行位 移的整 数;“操作数 2”指定 移动的位 数。 位移运 算的操 作是:按运算 符的要 求把 “操 作 数 1”移 动 “操作 数 2”指定 的位 数。在 进 行位 移运算 过程中 ,移 到边界之 外的多 余位放 弃舍掉 ;另 一侧产 生的缺 位以“0”补足。 例 14.2  设变量 x值为 0x1B4F,计 算表达式 “x <<5 >>2”的值 。 解:首先 x先 左移 5位,所得结 果为 0x69E0,如图 14.1所示。 然后再 对所得 结果向 右移 2位,结 果为 0x1A78,如 图 14.2所示 。 图 14.1 x<<5 3.按位 逻辑运 算 按位逻 辑运算 是双目 运算,它的格 式是:   操作数 1 运算符 操作数 2 具体操作是: (1)首 先将两 个操作 数都转 换成二 进制数; (2)然 后根据 运算符 的要求 以二进 制位 为单 位,按 位对 其进 行“位 与”、“位 异 或”、“位 14.2  位    段 · 387· 图 14.2 (x<<5)>>2 或”运 算。 表 14.3给出 全部三 个位逻 辑运算 符以及它 定义的 操作。 表 14.3 位逻辑运算符以及它定义的操作 x& y x^y x|y x y (按 位 与 ) (按位 异 或 ) (按 位 或 ) 0 0 0 0 0 0 1 0 1 1 1 0 0 1 1 1 1 1 0 1 例 14.3  设整数 x值为 0x1B4F,y值为 0x1A78,它 们按位逻辑运算结果 如图 14.3所示。 图 14.3 x与 y的按位运算 14.2  位   段 为了适 应系统 程序设 计的需 要,通 过使用 位段,C语 言允许 在结构 体 中把 整数 类型成 员 存储 在比通 常使用 的空间 更小的 空间 内 。例 如在微 型计 算机 内 一般 把 int类 型数据 存储 成 两个 字节(16bit),使用位 段可以把 它存储 在比两 个字节 少的 bit内。例 如      struts{ · 388· 第十 四 章   C语 言 独 有 的 特 性 unsigneda:4; unsignedb:5,c:7; }u; 结构体 变量 u有三个 成员 a、b、c,分别占 用 4bit、5bit、7bit,一共两 个字节 。 u的 成员 a、b、c称为 位段。位 段 通 过 在结 构体 成员 名 后 加 上冒 号“:”和 一 个整 数类 型 常量 表达式 来声明 ,整 数类型常 量表达 式指明 相应位 段的宽 度(占用 bit数)。 位段一般 依赖于 具体计 算机系 统,例如计 算机系 统一个 机器字 的宽度 、计 算 机 系统存 储 数据 是采用 “高 位存储 法”还是 “低位存 储法”,等等 。 使用位段要注意: (1)位 段仅允 许声明 为各种 整数类 型; (2)位 段长度 不允许 超越特 定计算 机的自然 字长; (3)位 段占用 的空间 不能跨 越特定 计算 机 的 地 址分 配 边 界 (该边界 与特 定计算 机的 自 然字 长有关 ),出 现跨越 ,将移 到下一 个机器 字; (4)位 段通常 用于与 具体计 算机相 关的程序 中,因此破 坏程序 的可移 植性。 14.3  存 储 类 别 C语言中 每个变 量和函数 都具 有两 个属 性 :类 型和 存储 类别。 读者 已经 十 分熟 悉类 型 属性 ,存 储类别 指的是 数 据 的 存 储方 式。 存 储 方 式分 为 两大 类:静 态 和 动态。 具 体 包 括 五 种:自动 的 (auto)、静 态 的 (static)、寄 存 器 的 (register)、外 部 的 (extern)和 类 型 定 义 符 (typedef)。 所谓静态 存储方 式是指 在程序 运行期 间分配 固定的 存储空间 (在静态 区)。而 动态存 储 方式 则是在 程序运 行期间 根据函 数调用 (函 数被激 活 )和 复合 语 句开始 执行 (复合语 句被 激 活)的 需要进行 动态存 储分 配(在栈 区)。 自动 和寄 存器 存 储 类 别属 于动 态存 储 方 式 ,外 部 和静 态存储类 别属于 静态存 储方式 ,类 型定义 符则是 用来定 义类型 名的。 这里所 说的“动 态 存储 方式”和“静 态存储 方式”要 和第十 二 章动 态数 据结 构 中所 讲的 “动态 变量 ”和 “静态 变 量”区 别开。 按第十二 章的概 念,本节所 有存储 类别的 变量全 部都是 静态变 量,它们由 系 统 在栈区 或 静态 区分配存 储空间 ;而动态变 量没有 显示的 名字,是通 过执行 由程序 员安排 的 申 请空间 函 数(例 如 malloc)在 堆区分 配存储 空间,并由指 针变量 标识。 本节的 概念是 指程序 中显示 声明 的 变量 的存 储 分配 方式,表 明它们 是在 栈 区还 是在 静 态区 分配存 储空间 ,及 其分配方 式。 C语言通 过在类 型符前缀 以存储 类别关 键字来 声明变 量和函 数的存 储类别。 例如:     autofloatx,y; 14.3 存 储 类 别 · 389· 声明两 个浮点 类型变 量 x、y,并且 它们的 存储类 别是自动 的。 14.3.1 数据在 内存中的存 储 数据的存 储类别 规定了 数据的 存储区 域,同时也 说明了 数据 的 生存 期。计 算机 中 ,用 于 存放 程序和 数据的 物理单 元有寄 存器和 内存 。寄 存 器 速 度 快但 空 间 小,常 常 只 存放 经常 参 与运 算的少数 数据。 内存速 度慢但 空间大 ,可 存放程 序和数 据。内 存中又 分为 系统 区 、用 户 程序 区和数 据区(包括 堆区、栈区 、静 态存储 区),如图 14.4所 示。 图 14.4 计算机存储区域 (1)寄 存器:用于 存放立即 参加运 算的数 据,它可以 随时更 新。 (2)系 统区:用于 存放系统 软件,如 操作系统 、语 言编译 器。只 要计算 机运 行,这一部 分 空间就必须保留给系统软件使用。 (3)用 户程序 代码区 :用于 存放用 户程序 的程序 代码。 (4)库 程序代 码区:用于存 放库函 数的代 码。 (5)数 据区:用来 存储用户 程序数 据,包括堆 区、栈区和 静态存 储区。 ① 堆区:用于 存储动 态变量 ;经 过 malloc申请 来的动 态变量 经常存 储在堆 区。 ② 栈区:具有 先进后 出特性 。用于存 储程序 中显式 声明的 自动存 储方式 的变量 。 ③ 静态存 储区:用于 存储程 序中显式 声明的 静态存 储方式 的变量 。 14.3.2 自动存 储类别 具有自动 存储类 别的变 量简称 自动变 量。C语言 用 auto表 示自 动 存储 类别,它 是 C语 言中 使用最 广泛的 一种存 储类别 。C语言 规定,在 函数 内 凡未 加存 储类 别说 明 的变 量均 视 为自 动变量 ,也 就 是 说,自 动 变 量 可 省 去 说 明 符 auto。图 14.5所 示 的 程 序 片 段 等 价 于 图 14.6所示的 程序片 段。 自动变 量的作 用域仅 限于定 义它的 相应个体 (函 数、复合语 句)内 。如果 是在函 数内 定 · 390· 第十 四 章   C语 言 独 有 的 特 性 义的 ,则 只在函 数内有 效;若是在 复 合语 句 中 定 义的 ,则只 在 相 应 语 句中 有效 。 自动 变量 具 有动 态生存 期,当定义 自动变量 的 相 应 个 体 开始 执 行 时,自 动变 量 生 存 期 开始;当定 义自 动 变量 的相应 个体执 行结束 时,自 动变 量也 离 开它 的生 存 期,不再 存在 。 例 14.4说明 动态 变 量作用域以及生存期。 {  inti,j,k;         booltest;         ⁝ } 图 14.5 自动存储类型Ⅰ { autointi,j,k;       autobooltest;        ⁝ } 图 14.6 自动存储类型Ⅱ    例 14.4  动态变 量作用 域以及 生存期。   intexam le_auto(intx,inty){          * L1*/ autointw,h; /* L2*/ ⁝ /* L3*/ { /* L4*/ autocharc; /* L5*/ ⁝ /* L6*/ } /* L7*/ ⁝ /* L8*/ } /* L9*/ 在该程序片段中, (1)函数 example_auto的 两个参 数 x和 y的 作用域 是在 L1和 L9之间,生存 期是程 序执 行进 入函数 example_auto开始 ,直 到该函 数执行 完毕返 回。 (2)自动 变量 w和 h的作用 域在 L2和 L9之间,生存 期是程 序进入 复合语 句 L2~L9执 行开 始,直到该 复合语 句执行完 毕退出 。 (3)自 动变量 c的作用 域则局 限于 L5和 L7之间 。如果 在 L8有引 用变量 c的 语 句,则 错误 。生存 期是程 序进入 复合语 句 L4~L7执行开 始,直到该 复合语 句执行完 毕退出 。 14.3.3 寄存器 存储类别 一般情 况下,不论 动态还是 静态 存储 的变 量 都存 放在 内 存中。 当程 序中 用 到哪 个变 量 时,由控 制器发 出指令 从内存中 获取该 变量的 值送到 运算器 中。经 过运算 器进 行运 算 ,如 果 需要 存储,再从 运算器 将数据送 到内存 中存储 。因此 当对一 个变量 频繁使 用时 ,将 反复访 问 内存 ,从 而花费 大量的 存取时间 。 14.3 存 储 类 别 · 391· 为此,C语言提 供了另 一种存 储 类 别变 量 ———寄 存 器 变 量。 寄存 器变 量 分配 在 CPU的 寄存 器中,使用 时不访 问内存,直 接 在寄 存 器 中 进行 ,提高 了 程 序 运 行效 率。 寄 存器 变量 的 存储 类别说 明符是 register。 寄存器 的 个 数 与 CPU相 关,十分 有限,所 以寄存 器变 量的 个 数必 然也 有 限。现 代编 译 系统 一般自 动分配 CPU寄存 器,所以 程序员说 明的寄 存器变 量不起 作用。 例 14.5  求 1000以 内可以 被 3整 除的所有 整数的 积,并打印 出来。   voidman(){ registerinti,s=1; for( =1;i<=1000;i++){ if( %3==0) s=s* i; } printf("s=%d",s); } 本程序 循环 1000次 ,i和 s都将频 繁使用 ,因此 可定义 为寄存 器变量 。 14.3.4 变量的 静态存储类 别 静态存 储类别 使用关 键字 static声明 ,静 态 存储 类别 的 变量 简称 静 态 变 量,静态 存储 类 别的函数称为静态函数。 C语言规 定,静 态变量 必须使 用 static声明。 静态变 量分为 静态全 局变量 和 静 态局部 变 量。 静态全 局变量 和静态 局部变 量的 生 存 期 都是 贯 穿 于 整 个程 序 的 运 行 期间;它们 的不 同 点在 于:静态全 局变量 的作用域 是包含 它的声 明的整 个源程 序文件 ,而 静态局 部 变 量的作 用 域是声明它的复合语句或函数。 1.静态 局部变 量 在局部 变量的 声明前 再加上 static说明 符就构 成静态 局部变量 。例如 :   { staticcharx,y; staticintstr[3]={0,1,2}; ⁝ } 复合语 句内的 局部变 量 x、y、str被 声明 成 static存 储 类别 的 ,是静 态 局部 变量。 静态 局 部变 量采用静 态存储 方式,被分 配在静 态存储 区。它 的生存 期为整 个程序 ,但 是 它 的作用 域 与自 动变量相 同,即只 能在定义 该变量 的复合 语句或 函数中 使用。 离开复 合语 句和 函 数后, 静态局部变量仍然存在却不能使用。 虽然静 态局部 变量在 离开声 明它 的 函数 或复 合 语句 后不 能 使 用 ,但 是如 果 再次 调用 声 · 392· 第十 四 章   C语 言 独 有 的 特 性 明它 的函数或 再一次 进入声 明它的 复合语 句时,又可 以继续 使用它 ,而 且这时 还 保 存了前 次 被使 用后留 下的值 。因 此,当多次 调 用 一 个函 数并 且要 求 在各 次调 用之 间保 留 某些 变量 的 值时 ,或 当多次 执行一 个复合语 句并且 要求在 各次执 行之间 保留某 些变量 的值 时,可考虑 采 用静 态局部 变量。 虽然用 全局变 量也 可 以 达 到上 述 目 的 ,但 全局 变量 有 时 会 造 成意 外的 副 作用 ,因 此仍以 采用静 态局部变 量为宜 。 例 14.6  静态局 部变量 使用举 例。 图 14.7定 义函数 not_test,其中 变量 test说 明 为 自 动 变量 并赋 予 初 始 值 false。当 main 中多 次调用 not_test时 ,test均赋初 值为 false,故每 次输出 值均为 true。而把函 数not_test说 明 成图 14.8所示 的形式 ,由于 test为 静 态 变 量,能在 每次 调 用后 保留 其值 并在 下 一次 调用 时 继续 使用,同样 的 main程序,输 出结果 为:truefalsetruefalse。     voidman(){                   for(int =0;i<4;i++) not_test(); } //主 程 序 /* 函数调用 */ void ot_test(){ booltest=false; test=! test; if(tst) printf("true\n"); els printf("false\n") } 图 14.7 函数 not_test(test为自动变量) void ot_test(){     /* 函数定义 * / staticbooltest=false; test=!test; if(tst) printf("true"); else printf("false") } 图 14.8 函数 not_test(test为静态变量) 2.静态 全局变 量 如果全 局变量 之前冠 以 static就构成 了静态全 局变量 ,此 种变量 同全局 变量的 存储方 式 一样 ,都 是静态 存储方 式。不同 点 是 ,当 源程 序由 多 个文 件组 成 时,非静 态全 局 变量 的作 用 域是 整个源程 序,可以 被程序中 的所有 文件所 共享;而静 态全局 变量只 在声明 它 的 源程序 文 件内 有效,不是 整个源 程序。 例 14.7  静态全 局变量 使用举 例。 图 14.9所 示的程 序 example由 两个文件 构成,每个 文 件中 都定 义了 char类型 变量 chr。 文件 ch14_09_01.c中以“charchr”声 明变量 chr,文 件 ch14_09_02中以 “staticcharchr”声明 变量 chr。两个 源程序 文件分别 编译 ,当 连 接 程 序为 变 量 分 配存 储空 间 时 ,两 个 变量 互不 干 扰,各分 配各的 存储空 间,形成一 个可执 行文件。 程序运 行输出 结果为 : 14.3 存 储 类 别 · 393·   chr_in_14_09_01=a chr_in_14_09_02=b 但是如果 将文件 ch14_09_02.c中的 chr声 明改为 “charchr”,那 么两个 文件虽 然都能 各 自通 过编译 ,但 是在连 接时会出 现错误 ,同一变量 被声明 了两次 。 /* 文件 ch14_09_01.c* / /* 文件 ch14_09_02.c* / charchr; voidfn(); void ain(){ chr= a; printf("cr_in_14_09_01=%c\n" ,chr); fn(); } staticcharchr; void n(){ chr= b; printf("cr_in_14_09_02=% c\n" ,chr); } 图 14.9 程序 example 14.3.5 变量的 外部存储类 别 C语言用 extern表示 外部存 储类别 ,包 括外部 变量和 外部函 数。 在 C语言中 ,所 有未加 存储类 别说明 的全局变 量均视 为外部 变量。 外部变 量意 味 着,变 量在 一个源 程序文 件中被 声明,在 其 他 所 有 源程 序文 件中 都可 以 使用 它。C语 言使 用外 部 变量采用如下程序结构: (1)在 一个源 程序文 件中声 明该变 量,不 附加 extern存储 类别说 明符,例如   intx; (2)在其 他所有 使用 x的源程 序文件 中以 extern存储类别说 明符声明同 一个变量 ,例如   externintx; 如此结 构,各个源 程序文件 分别编 译,每个 文件 中变 量 x都 有 定 义。 当连 接 时,连接 程 序把 各个文 件中的 x分配在 同一个 存储区 ,占用 相同存 储空间 。 在图 14.9所 示 的 程 序 中 ,如果 将 文 件 ch14_09_02.c中 chr声 明 的 static去 掉,改 为 “charchr”,两个文 件虽都 能各自 通过编 译,但 是 在 连 接时 会 出 现 “同一 变量 被 声明 了两 次” 的错 误。原 因在于 chr在 ch14_09_01.c和 ch14_09_02.c中 都被 声明 成 全局 变量。 如果 在 某个 文件中 把 chr声 明 成 外 部 的 (例 如在 ch14_09_02.c中 把 chr的 声明 改 成 “externchar chr”)就不 会出现 错误,这时 两个文 件中的 chr是同一 个变量。 · 394· 第十 四 章   C语 言 独 有 的 特 性 14.3.6 函数的 存储类别 C函数只 能被定 义成 static和 extern两种存 储类别 。 被定 义成 static存储 类 别 的函数 称 为静 态函数 ,也 称为内 部函数;被 定义成 extern存储 类别 的函 数 称为 外部函数 。 函 数的省 缺 存储类别是外部存储类别。 1.内部 函数 若在一 个源文 件中定 义的函 数只 能 被本 文件 中 的函 数调 用 ,而不能 被同 一 源程 序其 他 文件 中的函 数调用 ,这 种函数称 为内部 函数。 定义内 部函数 的一般 形式是 :   static类型说明符 函数名(形参表)…… 此处静态 static的含义 已不是 指存储 方式,而 是指对 函数 的调用 范围 只局 限于本 文件。 因此在不同的源文件中定义同名的静态函数不会引起混淆。 2.外部 函数 在 C语言中 ,所 有未加 存储类 别说明 的函数均 视为外 部函数 。外部 函数意 味着,函数 在 一个 源程序 文件中 被定义 ,在其 他 所 有 源 程 序文 件中 都可 以使 用 它。 C语言 使 用外 部函 数 采用如下程序结构: (1)在 一个源 程序文 件中声 明该函 数,附 加或不 附加 extern存储 类别说 明符,例如          intf(floatx){ ⁝ } (2)在 其他所 有使用 f的 源程 序 文 件 中用 函数 原型 说 明 同 一个 函数,并 且 在前 面附 加 extern存储 类别说 明符,例如    externintf(floatx); 对此结 构,各 个 源 程 序文 件 分 别 编译,每个 文 件 中 函 数 f都有 定义。 连接时 ,由 连接 程 序实 现各个 文件中 函数 f的协调 和统一 。 在一个 源文件 的函数 中要调 用其 他 源 文 件 中定 义的 外 部 函 数 时,必 须用 extern说明 被 调函数为外部函数。 例 14.8  外部函 数 的 使 用 。在 如 下 程 序 中,源 文 件 max.c中 声 明 函 数 max;源 文 件 main.c中 调 用 函 数 max。max的 函 数 原 型 声 明 被 指 定 为 extern类 别 的 ,保 证 了 在 源 文 件 main.c中调用的 max就是在 max.c中定义 的函数 max,并 且不会 发生声 明冲突 。    /* 源文件 main.c*/ externintmax(inta,intb);      //函数原型,外部的,表示 max在其他源文件中 voidm in(){             * 主函数 */ intx,y,r; x=9; 14.4  const指针 y=6; r=max(x,y); /* 调用函数 max*/ printf("Themaxofx=% dandy=% dis% d\n",x,y,r); } · 395·    /* 源文件 max.c*/ extrnintmax(inta,intb){   //外部函数定义,其中 extern可以省略 if( >b) returna; ele returnb; } 14.3.7 类型定 义符 类型定 义符以 前已经 接触过 。类型 定义符 的实 质是 定义类 型的 同义 词,用 来把 标识 符 定义 为类型 名。把 一个标 识符定 义为 类 型 名 之后,它 就可 以 出现 在允 许 使 用 类 型说 明符 的 任何地方。这样就可以用简单的名字替代复杂的类型声明。 例 14.9  类型定 义举例 。   typedefbool* BP;         * BP是布尔型指针类型 */ typedefint(* IFP)(); /* IFP是指向返回值为整型的函数的指针类型 */ typedefintIF2I(int,int); /* IF2I是有两个整型参数且返回整型值的函数类型*/ typedefintIA[5]; /*IA是长度为 5的一维整型数组类型 */ 有了这些定义之后就可以进行如下的声明:   BPbp;           * bp是一个布尔型指针 */ BPfbp(); /* fbp是一个返回布尔型指针的函数 */ IFPifp; /* ifp是一个返回类型为整型的函数指针 */ IF2I* fp; /* fp是一个返回类型为整型且有两个整型参数的函数指针 */ IA ia; /* ia是一个长度为 5的一维整型数组 */ IA ia2[2]; /* ia2是一个 2行 5列的二维整型数组 */ 类型定 义符不 能同其 他类型 说明符 一起使用 ,例 如   typedeflongintlint; unsignedlintx;        * 错误,unsigned和 lint都是类型名 */ 14.4  const指 针 const与 指针结 合,可 以产生 如表 14.4所 示的四 种组合 。 · 396· 第十 四 章   C语 言 独 有 的 特 性 表 14.4 const与指针结合形式 格式 <类型名 > * <指针变量名 > const<类型名 > * <指针变量名 > <类型名 > * const<指针名 > const<类型名 > * const<指针变量名 > 实例 意义 int*p constint* p 指针和数据都不是常量 指 针 不 是 常 量 ,指 向 的数 据 为常 量 int* constp 指 针 是 常 量 ,指 向 的 数据 为 变量 constint* constp 指针是常量,指向的数据也为常量 第一类 是读者 熟悉的 ,本 节 介 绍 后三 类。 const与 指针 结合 使用,可 以 限定 对 数 据 的 修 改权 限。把 它用于 函数参 数中,可以给 程序的 安全性 带来好 处。把 const用于 函 数 形式参 数 中,不影 响参数 传递时 把实 在 参数 值 传 入 形 式参 数,但 是 影响 在 函数 内 部 对形 式 参 数 的 赋 值,从而 为程序 的安全 性提供一 定的保 证。 14.4.1 指向常 量的指针(常量指针 ) 指向常 量的指 针声明 的形式 是,在 指针声 明的类 型前加 类型限 定符 const,形 式如下 :   const类型 * id 其中 ,标 识符 id是 被声明 的指针 变量。 const限 定符表 示: (1)id是常量 指针。 但是它 本身不 是常量,而 是指针 变量,可以对 它进行 修改。 (2)不 能通过 常量指 针来修 改它所 指对象的 值,不论该 对象是 常量还 是变量。 例如有声明:   constchar*cptr; 表示 cptr是 常量指 针,将指向 字符类 型量(变 量或常 量),若 有说明   constcharch1= a; constcharch2= b; charch3= c; 则   cptr=&ch1;   /指针变量 cptr指向字符型常量 ch1 cptr=&ch2; //指针变量 cptr指向字符型常量 ch2 *cptr= d; //cptr指向常量 ch2,不能给常量赋值 cptr=&ch3; //指针变量 cptr指向字符型变量 ch3 *cptr= f; //虽然 ch3本身是变量,但是由于 cptr是常量指针,也不能用这种方式修改 ch3 ch3= g; //这 是 正 确 的 14.4.2 指针常 量 指针常 量的声 明形式 是,在 指针 声明的 星号 和表 示指针 名字 的标识 符之 间 加类 型限 定 14.5 有 关 指 针 的 总结 · 397· 符 const,形式如下 :   类型 * constid=对象指针 其中 ,标 识符 id是 被声明 的指针 常量。 同常量 指针相 反,const限定 符表示 :: (1)指 针常量 id本身是 常量,id将固定指 向相应 的对象 ,不 能修改 id本身 。 (2)指 针常量 id所指对 象既可 是变量 ,也 可是常 量,即 id所指 对象的 内容可 以修改 。 例如:   charch3="xyz"; char* constcptr="abc";     /cptr是指针常量,将固定指向字符串 "abc" char* constcptr0=&ch3; //cptr0也是指针常量,将固定指向 char类型变量 ch3 *cptr0= g; //cptr0所指向的对象 ch3为"gyz" *(cptr+1)= h; //cptr0所指向的对象 ch3的内容为"ghz" cptr="edf"; //错误 ,不 能给 指 针 常 量 赋 值 ,不 能 修 改指 针 常量 *(cptr++); //错误 ,不 能改 变 指 针 常 量 的 值 *cptr= g; //也错误,但是不涉及指针常量 cptr, //而是因为 cptr所指向的对象"abc"本身是常量字符串 14.4.3 指向常 量的指针常 量(常量 指针常量) 指向常量 的指针 常量的 声明形 式是:在指 针声明 的类型 前、在星号 和表示 指 针 名字的 标 识符 之间分 别加类 型限定 符 const,形式 如下:   const类型 * constid=对象指针 其中 ,标 识符 id是 被声明 的常量 指针常 量。两 个 const限 定符表 示: (1)id是指针 常量,将固 定指向 相应的 对象,不能被 修改。 (2)id所指向 的对象 也是常 量,也不能 修改。 使用常 量指针 常量要 记住:在完 成对 其初始 化后,不 论是指 针值,还 是指 针 所指 对象 的 值,都是 不能改 变的。 例如:   intb,a;           /声明 int类型变量 b、a constintc=2; //c是 int类型常量,它的值只能是 2,不能改变 constint* constipa=&c; //ipa是常量指针常量,即 ipa本身是常量,它的值不能改变 //只能指向常量 c;当然 c也是常量,它的值也不能改变 constint* constipa0=&a; //ipa0是常量指针常量,指向变量 a ipa=&b; //ipa是 常 量 指针,不 能 修改 它 的 值 *ipa0=22; //错 误 ,ipa所 指 的 对 象 虽然 是 变 量 ,也不 能 修 改 它的 值 14.5 有 关指 针的 总结 本节总 结涉及 指针的 各 种 声明 和 运算 以 及 它 们 的 意 义,目的 是 给 读 者 一个 总 体 概念。 · 398· 第十 四 章   C语 言 独 有 的 特 性 以 int类型为例 ,设 有声明     int* p,* q,v,u,a[10],(* fp)(); intf(); 表 14.5给出 涉及指 针的各 种声明 及其意义 。表 14.6说明 涉及指 针的各 种 运 算的合 法 性及其意义。 表 14.5 涉及指针的声明形式及其意义 声明形式 意义 intv; int类型变量 v inta[5] a是 5个元素的 int类型数组;可以认为 a是指针常量 inta[] a是数组,元素个数未知,数组元素是 int类型;相当于“int*p”,但是 a是指针常量 int* p p是指向 int类型数据的指针变量 int* *p int* p[5] 指向指针的指针。p是指针变量,它指向另一个指针,相应指针指向 int类型数据 p是 5个元素的数组,该数 组的 每个 元素 是指 针类 型,是 指向 int类 型数 据的 指 针变 int* (p[5]) int* p[] 量 。 p本 身 是 一个 指针 常 量 p是数组,数组长度未知,该数组的每个元 素是指 针类型,是指 向 int类 型数据 的指针 int* (p[]) int(* p)[5] 变 量 ;该 形式 与 “int* * p”等价 。p是 指 针 变 量 (*p)是 5个元素的 int类型数组;p是指向该数组的指针变量 int(* p)[] (*p)是数组,数组长度未知,该数组 的每 个元 素是 int类型;p是指向 该数 组的 指针 变量 intf(); 函数 f,返回值为 int类型 int* f() int(* f)() f是函数,函数类型是“int*”,即 f是返回指针的函数 int* (f()) (* f)是函 数 ,函 数 类 型 是 “int”,f是 指 向 函 数 的指 针 int* *f() f是函数,函数类型是“int**”、即 f是“返回指针的指针”的函数 int*(*f)() (*f)是函数,函数类型是“int*”,即 f是指向函数的指针;所指函数是“int*”类型, int*((*f)()) 即返回 int类型指针的函数 constint*cptr; 常量指针 cptr,cptr是变量,指向一个 int类型常量 int*constcptr; 指针常量 cptr,cptr是常量,指向一个 int类型常量或变量 constint* const 指向常量的指针常量 cptr,cptr是常量,指向一个 int类型常量 cptr; 形式 p+n p++ p-- 合法性 表 14.6 涉及指针的各种运算 解释 正确 加减一个整数 14.6  语    句 形式 p=NULL p=&v; p=a; p=&(a[5]) p=q fp=f fp=&f p=1000 v=p p-q p+q p==q、p!=q p>q、p>=q P<q、p<=q 合法性 正确 正确 空赋值给指针 变量地址赋值给指针 数组首地址赋值给指针 数组元素地址赋值给指针 指针赋值给指针 解释 正确 函数首地址赋值给指针 错误 正确 错误 正确 正确 整型数据赋值给指针 指针赋值给整型变量 两个指针相减,结果是一个整数,为 p、q之间的距离 要求 p、q指向同一个数组的元素 两个指针相加 两个指针比较,结果 bool类型 两个指针比较,结果 bool类型 要求 p、q指向同一个数组的元素 · 399· (续 表 ) 14.6  语   句 本节深 入介绍 三个语 句 break、continue和 for。 break和 continue语句 是一 种受 限 制的 goto语 句,用 来 改 变 循 环 或 分 支 语 句 的 控 制 流 程。在达到相 同目的 的情况 下,使用 break和 continue语 句比使 用 goto语句 具有更 好的风 格 和结 构。但 与全部 用标准 控制流 程编写 的程序相 比,break和 continue语句的 结构较 差。 读者已 经很熟 悉 for循环 语句,本节 将延伸它 。 14.6.1  break 执行 break语 句使包 含它的 最内层 while、do、for、switch语 句终止 执行 ,立 即转移 到所 终 止语 句之外 的程序 点。在 没有循 环或 switch语句 的场合 使用 break是 错误的。 例 14.10 迭 代中使 用 break。   intx=0; while x< 10){ printf("Looping"); x++; if x==5) · 400· 第十 四 章   C语 言 独 有 的 特 性 break; else ⁝      //其他代码 }  ⁝       //后续代码 在该程 序片段 中,循环将在 x==5时 停止,去 执行 后续代 码,尽 管 循 环 控 制 当 x<10时都 执 行 循环 体。用 流程图 来表 示 该 程 序片 段如 图 14.10 所示。 特别请 读者 注意 break在 switch语 句中 的 作 用。 如图 14.11和图 14.12所示 的两段 代码 执行 的结 果是不 一样的 ,请 读者认真 体会。 在图 14.11中,当 x==2时,打印结 果为   图 14.10 例 14.10程序的流程图   2 3 no_meaning 因为 switch语 句将控 制转 移到 case2处打 印 2之 后,接 着执 行 printf(“3”),最 后执 行 printf (“no_meaning”)。 如果需 要在每 次调用 printf之 后都终 止 switch体,则要像 图 14.12一样使 用 break语句。 在图 14.12所 示的程 序片段 中,当 x==2时 ,执 行结果 只打印 出一个 数字 2。 swith(x){ case1:printf("1"); case2:printf("2"); case3:printf("3"); default:printf("no_meaning"); } 图 14.11 switch语句中使用 break之一 swich(x){ case1 printf("1"); break; case2 printf("2"); break; case3 printf("3"); break; default:printf("no_meaning"); } 图 14.12 switch语句中使用 break之二 14.6.2  continue continue语 句终止 它所在 的最内 层 while、do、for语句 循环 体的执 行,跳过 循环体 余下 的 代码 ,立 即转移 到循环 体末尾,受 其影响 的循环语 句 从 “重新求 值循环 测 试 点 ”开始 执行 (对 for语 句为“表 达式 3”)。在没 有循环 迭代语句 的场合 使用 continue语 句是错误 的。 在例 14.11的程 序片段 中,不 可能 打 印 “Looping2”,程 序执 行流 程如 图 14.13所 示,该 14.7  编 译 预 处 理 程序片段执行结果是:   Looping0 Looping1 Looping3 Looping4 例 14.11 continue语句 示例。   for x=0;x <5;x++){ if( ==2) continue; els printf("Looping% d\n",x); } · 401· 图 14.13 例 14.11程序的流程图 14.6.3 for的延 伸 大家知 道 for语句 包括 for关 键字和 括在 括 号中 的用 分号分 开的 3个 表达式 ,然 后是 语 句。 通常,第一 个表达 式初始化 循 环 控制 变量,第 二 个表 达式 测 试 循 环是 否终 止 ,第 三个 表 达式 更新循 环控 制变 量 。如果 使 用 逗号 表 达式,就可 以书 写带 有 多个 控制变 量 的 for语句。 例 14.12比较 两个字 符串是 否相等 就利用 了 for语句 的这个 功能。 例 14.12 编 写函数,判断两个 字符串 str1和 str2是 否相等,相等则返 回真,否则返回 假。   bolstr_equal(constchar* str1,constchar*str2){ char* t1,* t2; for( 1=str1,t2=str2;*t1&& *t2;t1++,t2++) if( t1!=*t2) returnfalse; return*t1==* t2; } 14.7  编译 预处 理 C语言的 预处理 器是一个 简单的 宏处 理器,源 程序 必 须经 过这 个宏 处理 器 处理 之后 才 能让编译器正确处理。 14.7.1  宏 定 义 C语言源 程序中 允许用一 个标识 符来表 示一 个字符 串,称 为“宏 ”。被 定 义为 “宏”的 标 · 402· 第十 四 章   C语 言 独 有 的 特 性 识符 称为“宏名 ”。在编 译预处 理 时 ,对 程 序中 所有 出现 的“宏 名 ”,都用 宏定 义 中的 字符 串 去代 换,称为“宏 代换”或 “宏 展开”。 事实上,第 三章第 3.1节 介绍的 所 谓 常量定 义就是 “宏 定义 ”。 宏定义由 源程序 中的宏 定义命 令完成 。宏展 开由编 译预处理 程序自 动完 成。在 C语 言 中,“宏”分 为有参 数宏和 无参数宏 两种,简称 有参宏 和无参宏 。 1.无参 数宏 读者已 经很熟 悉无参 宏,它 的定义 形式为 :     #define 标 识 符  字 符串 其中 (1)#代表 本行是 编译预处 理命令 ; (2)define是 宏定义 命令; (3)标 识符是 所定义 的宏名 ; (4)字 符串是 宏名所 代替的 内容,可以是 常数、表达 式,等等。 例 14.13 计 算半径 为 10米 的圆的 周长,其中 用宏定 义了圆 周率 PI。当编 译预处 理时, 将用 3.1415926来 替 代 程 序 中 出 现 的 所 有 PI。 相 当 于 在 所 有 出 现 PI的 地 方 全 部 写 3.1415926一样。 程序运 行时自 然使用 3.1415926参与 运算。   #definePI3.1415926 mai (){ intr=10; intl; l=2* PI* r; printf("Theperimeterof circlewith%dmeterradiusis%d\n" ,r,l); } 如要终 止宏定 义作用 域,可 使用 “#undef”命 令。例 如   #definePI3.1415926 man(){ ⁝ #undefPI        /* 终止 PI的作用定义 */ ⁝ } 说明: (1)宏 定义是 用宏名 来代替 一个字 符串,编译预 处理程 序对它 不做任 何检 查,如果有 错 误,只能 在已经 展开宏 的源程序 中发现 。 (2)习 惯上宏 名用大 写字母 表示,但也允 许用小 写字母 。 14.7  编 译 预 处 理 · 403· (3)宏 定义必 须写在 函数之 外,作 用域从 宏定义 命令开 始直到 源程序 结束。 (4)宏 定义允 许嵌套 ,在宏 定义的 字符串 中可以 使用已 经定义 的宏名 ,例 如:   #definePI3.1415926 #defineCIRCLE_L2*PI*r    /* PI是已定义的宏名 */ 2.有参 数宏 有参数 宏十分 类似于 有参函 数,它 的定义 形式为 :   #define标识符 (形式参数表 )字符串 其中 ,#、define、标 识符、字符串 的概念 与无参 宏相同 ;除 此之 外,形式参 数表由 逗 号 分隔开 的 标识 符组成 ,这 些标识 符在字符 串中出 现。 与有参数宏对应的宏调用形式是:     标识 符 (实在 参 数 表 ) 宏定义 中的参 数称为 形式参 数,宏调 用中 的 参数 称为 实 在参 数。带 参宏 调 用不 仅要 宏 展开 ,而 且要用 实在参 数去代换 形式参 数。宏 展开时 ,一 方面用 宏定义 中的字 符 串 替换宏 调 用中 的 标 识 符;另 一 方 面,同 时 用 诸 实 在 参 数 替 换 字 符 串 中 的 形 式 参 数 标 识 符。 下 面 以 例 14.14说明 该过程 。 例 14.14 判 断 a、b的大小,用“宏 ”定义比 较表达 式。   #defineMAX(x,y)(x>y?x:y)        * 1*/ mai (){ /* 2*/ inta,b,max; /* 3*/ printf("inputtwonumbers:"); /* 4*/ scanf("% d%d",&a,&b); /* 5*/ max=MAX(a,b); /* 6*/ printf("max=% d\n",max); /* 7*/ } /* 8*/ 程序的 第 1行是 带参宏 定义,用宏 名 MAX表示条 件表 达式 “x > y?x:y”,形 式参 数 x、y均出 现在条件 表达式 中。程 序第 6行“max=MAX(a,b)”为宏调 用,实 在参数 a、b对 应形 式参数 x、y。宏 展开后 该语句 为:“max=(a > b)?a:b”;用 于求 a、b中较大 的数。 要特别注意宏展开过程中的参数替换。 例 14.15 宏 展开过 程中的参 数替换 例题。   #defineMULT(x,y)x*y main() inta,b,r; printf("inputanumber:"); scanf("% d% d",&a,&b); r=MULT(a+1,b+1); · 404· 第十 四 章   C语 言 独 有 的 特 性 } 在该程 序中,宏 MULT的形 参是 x和 y,宏调用 的实在 参数是 a+1和 b+1。 宏展开 时用 x* y代 换 MULT,得到语 句 r=a+1*b+1。可 能与预 期的   r=(a+1)* (b+1) 不一 样。在 该程序 中,如果求形 式参数 x、y的积 x* y,必须用 形式   #defineMULT(x,y)(x)*(y) 定义 宏。宏 展开 MULT(a+1,b+1)才 能得到 正确结 果   (a+1)*(b+1) 宏展开 就是简 单的符 号替换 ,与函 数调用 有本质 的 不 同。在例 14.15中,若 把 MULT定 义为 函数,则调 用“MULT(a+1,b+1)”的 操作 步骤 是首 先 计 算 实在 参数 表达 式 值,然后 把 相应 值送入 形式参 数。产 生的结 果相当 于 r=(a+1)*(b+1)。可以 看出宏 调 用 很容易 出 错,因此 使用有 参宏时 必须十分 小心 ,否 则 就 会 出现 意想 不 到的 错误 。建 议尽 量 使用 函数, 避免不必要的麻烦。使用带参数宏时要注意: (1)带 参宏定 义中,宏名和 形参表 的圆括 号之间 不能有 空格出 现,否则会 被 认 为是无 参 宏定 义。例 如在例 14.14中   #defineMAX(x,y)(x > y?x:y) 若写成   #defineMAX  (x,y)(x> y?x:y) MAX变成无参 宏,宏展开 时将用 “(x,y)(x > y?x:y)”替 换 MAX。 (2)在 宏定义 中的形 式参数 是标识 符,而 宏调用 中的实 在参数 可以是 任意字 符串(包 括 表达 式)。例如 可以用 “MAX(a+b,a-b)”调用宏 MAX,求 a+b和 a-b中 的较大 者。 (3)在 宏定义 中,字符串内 的形式 参数最 好用括 号括起 来,以避免 出错。 (4)带 参宏和 函数很 相似,但有本 质上的 不同,主要 的不同 点有表 14.7所示的 几方面 。 表 14.7 带参数宏和函数的比较 带参数宏 函 数 形 形式参数标 识 符不 是变 量,不 分 配内 存 单 元,不 形式参数 是局 部 于 函数 的 变 量,分 配内 存 参 作类型说明 单 元 ,必 须 作 类 型说 明 实 实在参数是一个字符串,用它 去代换 形式参 数标 实在参数 是表 达 式,它 的 值被 传 入 形式 参 参 识符 数 结 带参宏调用 十分 类似 第 八章 8.1.4节 介绍 的 换 函数是值 参数,调用 时 把 实在 参 数 的值 赋 合 名参数,只作符号代换,不存在值传递的问题 予 形 式 参数,进 行“值 传 递 ” 14.7  编 译 预 处 理 · 405· 14.7.2 文件包 含 读者比较 熟悉预 处理命 令#include,它可 以把指 定 源 文件的 全部内 容括入 现 有 源程序 文 件中 ,它 的一般 形式是 :     #include"文件 名 " 或   #include <文件名 > 文件包含 命令的 功能是 把指定 文件的 全部内 容括进 来,插入到 该命令 行所 在位 置 ,取 代 该命 令行。 由当前 源程序 文件和 指定文 件组成一 个文件 ,一 起编译 。 一个大 的程序 可以被 分为多 个模 块 ,由多 个 程序 员分 别 编写。 公用 信息 可 以单 独组 成 一个 文件,在其 他文件 的开头用 文件 包 含 命 令将 其括 入。 这 样既 可避 免 在 每 个 文件 开头 都 去书 写那些 公用量 ,节 省时间,又 可避免 书写手误 ,减 少出错 。 文件包 含命令 中的文 件名既 可以 用 双引 号也 可 用尖 括号 括 起 来 ,它 们的 区 别在 于查 找 指定 文件位 置的不 同。尖 括号只 在省 缺 目 录 中查 找 指 定 文 件,省 缺目 录 由 用 户 设置 编程 环 境时 设定。双 引号表 示首先 在当前 源文件 所在文 件目录 中查 找 ,如 果没有 找到 ,则 在省缺 目 录中查找。 一个 include只能包 含一个 文件,要包含 多个文 件,则需要 多个包 含命令 。 14.7.3 条件编 译 条件编 译命令 使编译 器能够 按照 不 同条 件编 译 不同 的程 序 部 分 ,产 生不 同 的目 标代 码 文件 。表 14.8列 出了条 件编译 命令。 命令 #if #ifdef #ifndef #elif #else #endif 表 14.8 条件编译命令 含义 根据常量表达式的值有条件地包含文本 根据是否定义宏名有条件地包含文本 与 #ifdef命 令 相 反 的 测 试,有 条件 包 含 文 本 在 #if、#ifdef、#ifndef、#elif测 试 失 败 时根据 另 一 常 量 表 达 式的 值 有条 件 地包 含 文 本 在 #if、#ifdef、#ifndef、#elif测 试 失 败 时包含 的 文 本 结束条件编译 这些命 令的一 般组合 使用的 方式有 两种:使用常 量表达 式判断 、使 用宏定 义名判 断。 1.使用 常量表 达式判断 使用常量表达式判断的条件编译形式是下述三种形式之一。 · 406· 第十 四 章   C语 言 独 有 的 特 性 #if整型 量表达式 文本 1 #else  其余文本 #endif #if整型 量表达式 文本 1 #endif #if整型 量表达式1 文本 1 #elif整 常量表达式2 文本 2 #else  其余文本 #endif 使用常量 表达式 判断的 条件编 译的功 能是,首先 求常量 表达式 值,然后根 据 常 量表达 式 值是 否为 0进 行下面 的条件 编译。 2.使用 宏定义 名判断 使用宏定义名判断的形式是: #ifdef标识 符 文本 1 #else  文本 2 #endif #ifndef标 识 符 文本 #endif 这种组 织方式 测试标 识符是 否定义 为宏。 “#ifdef标 识符 ”的意 义是 :如 果定义 了标 识 符为 宏(即使宏 体为空 ),则为真,编译 #if后边 的文本 ;否 则如果 没有定 义标识 符 为 宏或者 已 经用 “#undef”命令取 消了标识 符的宏 定义,则为 假,编译#else后面的 文本。 例 14.16 条 件编译 例。   #include"stdio.h" #defineR1 #defineMAX(a,b)(a>=b? a:b) #defineMIN(a,b)(a< =b? a:b) mai(){ intx=0,y=0,t=0; printf("Pleaseinput3differentintegers:"); scanf("% d% d% d",&x,&y,&t); #ift   =MAX(x,y); printf("MAX(% d,% d)=% d\n",x,y,t); #else   =MIN(x,y); printf("MIN(% d,%d)=%d\n",x,y,t); #endif *1* / /*2* / /*3* / /*4* / /*5* / /*6* / /*7* / /*8* / /*9* / /*10* / /*11* / /*12* / /*13* / /*14* / /*15* / 14.7  编 译 预 处 理 · 407· #if3   =MAX(x,y); printf("MAX(% d,% d)=% d\n",x,y,t); #else   =MIN(x,y); printf("MIN(% d,%d)=%d\n",x,y,t); #endif #undefR #ifdefR   printf("Theresultis% d\n",t); #else   printf("cannotoutput\n"); #endif } /*16* / /*17* / /*18* / /*19* / /*20* / /*21* / /*22* / /*23* / /*24* / /*25* / /*26* / /*27* / /*28* / /*29* / 在例 14.16中使 用了上 面介绍 的两种 条件编 译形式 。程序的 第 9行 ~15行,用变 量(注 意这 里是变 量)t作为条 件编译 的判断 条 件;第 16行 ~22行 用 常 量 3作 条件 编 译的 判断 条 件;第 24行 ~28行,用宏 R作 为条件 编译的 判断条 件。变 量 t的值是 在程 序运 行 时由 scanf 函数 确定的 ,在 编译预 处理时 t的值 并不起 作用,因此 编译了 13行 和 14行。根 据规则 ,由常 量 3控制编译 了 17行 和 18行。由 于第 23行#undef语 句取消了 宏 R的定 义,所 以 编译了 第 27行。 经过编 译预处 理后,等价 的程序 如下:   #include"stdio.h" #defineR1 #defineMAX(a,b)(a>=b? a:b) #defineMIN(a,b)(a< =b? a:b) mai(){ intx=0,y=0,t=0; printf("Pleaseinput3differentintegers:"); scanf("% d% d% d",&x,&y,&t); t=MIN(x,y); printf("MIN(%d,% d)=% d\n",x,y,t); t=MAX(x,y); printf("MAX(% d,% d)=% d\n",x,y,t); printf("cannotoutput\n"); } /*1* / /*2* / /*3* / /*4* / /*5* / /*6* / /*7* / /*8* / /*13* / /*14* / /*17* / /*18* / /*27* / /*29* / 程序运行结果形式如下:   Pleaseinput3differentintegers:120 MIN(1,2)=1 MAX(1,2)=2 cannotoutput · 408· 第十 四 章   C语 言 独 有 的 特 性 条件编译 当然可 以用条 件语句 来实现 ,但 是用条 件语句 将会对 整个源 程序 进行 编 译,生 成的 目标代 码程序 很长,而采用 条 件 编 译 ,则 根据 条 件 只 编 译其 中 的 某 段 程序,生成 的目 标 程序 较短。 如果条 件选择 的程序 段很长 ,采用 条件编 译的方 法是十 分必要 的。 读者已 经简单 地 了 解 了 C语 言 中 编 译 预 处 理 的 指 令 及 用 法 ,此 外 还 有 如 预 定 义 宏、 #line、#program、#error等预 处理指 令,在 这里就 不一一 介绍了 。 本章小结 本章主要 介绍了 C语言独有 的特性 :break和 continue语 句、运算、for语句的 延伸 、存 储 类别 、编 译预处 理等。 重点掌握 break语句和 存储类 别。 附 录 一   ACSII字 符 集 ACSII字 符集 (按 十 进 制编 码 ) 0 1 2 3 4 5 6 7 8 9 0 nul soh stx etx eot enq ack bell bs ht 1 lf vt ff cr so si dle dc1 dc2 dc3 2 dc4 nak syn etb can em sub esc fs gs 3 rs us sp ! " # S| % & ’ 4 ( ) * + , - . / 0 1 5 2 3 4 5 6 7 8 9 : ; 6 < = > ? @ A B C D E 7 F G H I J K L M N O 8 P Q R S T U V W X Y 9 Z [ \ ] ^ _ a b c 10 d e f g h i j k l m 11 n o p q r s t u v w 12 x y z { | } ~ del ACSII字 符集 (按 八 进 制编 码 ) 0 1 2 3 4 5 6 0 nul soh stx etx eot enq ack 1 bs ht lf vt ff cr so 2 dle dc1 dc2 dc3 dc4 nak syn 3 can em sub esc fs gs rs 4 sp ! " # S| % & 5 ( ) * + , - . 6 0 1 2 3 4 5 6 7 8 9 : ; < = > 10 @ A B C D E F 11 H I J K L M N 12 P Q R S T U V 13 X Y Z [ \ ] ^ 14 a b c d e f 7 bell si etb us ’ / 7 ? G O W _ g · 410· 附 录 一   ACSII字 符 集 (续 表 ) 0 1 2 3 4 5 6 7 15 h i j k l m n o 16 p q r s t u v w 17 x y z { | } ~ del ACSII字符 集 (按十 六 进 制编 码) 0123456789ABCDE 0 nul soh stx etx eot enq ack bell bs ht lf vt ff cr so 1 dle dc1 dc2 dc3 dc4 nak syn etb can em sub esc fs gs rs 2 sp ! " # S| % & ’ ( ) * + , - . 3 0123456789:;<=> 4 @ A B C D E FG H I JK LM N 5 PQ R S TU VW X Y Z [ \] ^ 6 a b c d e fg h i jk lm n 7 p q r s tu vw x y z{ |} ~ F si us / ? O _ o del 按 十进 制 编 码 ,各 个 字 符 意 义如 下 : 0~31控 制 字 符 ; 32~127可打印字符(91~95、123~126九个字符 ISO标准未定义)。 控制字符含义如下: 文本控制符 08:bs(backspace)退 格 09:ht(horizontaltabulation)横 向列 表 10:lf(linefeed)换行 11:vt(verticaltabulation)纵 向 列 表 12:ff(form feed)换 页 13:cr(carriagereturn)回 车 00:nul(nullcaracters)空 白 24:can(cancel)作 废 26:sub(substitute)置 换 127:del(delete)删 除 31:us(unitseparator)单位 分 隔 符 换码符 14:so(shiftout)换 档 15:si(shiftin)换档 27:esc(escape)扩 展 介质控制符 07:bel(ringbell)响 铃 17、18、19、20:dc?(devicecontrol)设 备 控 制 25:em (endofmedium)介质结束 分隔符 28:fs(fileseparator)文 件分 隔 符 29:gs(groupseparator)组 分隔 符 30:rs(recordseparator)记 录 分 隔 符 通信控制符 01:soh(startofheading)标 题 开始 02:stx(startoftext)正 文开 始 03:etx(endoftext)正 文 结 束 附 录 一   ACSII字 符 集 · 411· 04:eot(endoftransmission)传 输 结 束 05:enq(enquiry)询 问 06:ask(asknowledgment)确 认 21:nak(negativeasknowledgment)不 确 认 16:dle(datalineescape)数 据 链 扩 展 22:syn(synchronousidle)同 步 字 符 23:etb(endoftransmissionblock)传 输 块 结束 附录 二   C语 言语 法    <编译单元表 >→ <编译单元 > x <编译单元 ><编译单元表 > <翻译单元 >→ <顶层声明表 > <顶层声明表 >→ <顶层声明 > x <顶层声明表 > <顶层声明 > <顶层声明 >→ <函数定义 > x <声明 > <函数定义 >→ <函数定义说明符 ><复合语句 > <函数定义说明符 >→ <声明说明符 > <声明符 > <声明列表 > <声明说明符 >→ <存储类说明符 > <声明说明符 > x <类型说明符 > <声明说明符 > x <类型限定符 > <声明说明符 > x <函数说明符 > <声明说明符 > <存储类说明符 >→autox externx registerx staticx typedef <类型限定符 >→constx volatilex restrict <函数说明符 >→inline <声明符 >→ <指针声明符 > x <直接声明符 > <指针声明符 >→ <指针 ><直接声明符 > <直接声明符 >→ <简单声明符 > x (<声明符 >) x <函数声明符 > x <数组声明符 > <指针 >→* <类型限定符列表 > x * <类型限定符列表 > <指针 > <类型限定符列表 >→ <类型限定符 > x <类型限定符列表 ><类型限定符 > <简单声明符 >→ <标识符 > <函数声明符 >→ <直接声明符 > (<参数类型列表 >) x <直接声明符 > ( <标识符列表 > ) 附 录 二   C语 言 语 法 · 413· <参数类型列表 >→ <参数列表 > x <参数列表 >,… <参数列表 >→ <参数声明 > x <参数列表 >,<参数声明 > <参数声明 >→ <声明说明符 ><声明符 > x <声明说明符 > <抽象声明符 > <声明列表 >→ <声明 > x <声明列表 ><声明 > <声明 >→ <声明说明符 ><初始化声明符列表 > <初始化声明符列表 >→ <初始化声明符 > x <初始化声明符列表 >,<初始化声明符 > <初始化声明符 >→ <声明符 > x <声明符 >=<初始化算子 > <初始化算子 >→ <赋值表达式 > x { <初始化算子列表 > , } <初始化算子列表 >→ <初始化算子 > x <初始化算子列表 >,<初始化算子 > x <指示 ><初始化算子 > x <初始化算子列表 >,<指示 ><初始化算子 > <指示 >→ <指示符列表 >= <指示符列表 >→ <指示符 > x <指示符列表 ><指示符 > <指示符 >→ [<常量表达式 >] x <标识符 > <数组声明符 >→ <直接声明符 >[<常量表达式 >]   (直到 C99) x <直接声明符 >[ <数组限定列表 > <数组尺寸表达式 > ] (C99) x <直接声明符[ <数组限定列表 > *] <数组限定符列表 >→ <数组限定符 > x <数组限定列表 ><数组限定符 > <数组限定符 >→staticx restrictx constx volatile <数组尺寸表达式 > → <赋值表达式 > x* <抽象声明符 >→ <指针 > x <指针 > <直接抽象声明符 > <直接抽象声明符 > → ( <抽象声明符 > ) x <直接抽象声明符 > [ <常量表达式 > ] x <直接抽象声明符 > [ <表达式 > ] x <直接抽象声明符 > [*] x <直接抽象声明符 > ( <参数类型列表 > ) · 414· 附 录 二   C语 言 语 法 <类型说明符 >→ <typedef名字 > x <void类型说明符 > x <整数类型说明符 > x <浮点类型说明符 > x <枚举类型说明符 > x <结构体类型说明符 > x <共用体类型说明符 > <typedef名字 >→ <标识符 > <void类型说明符 >→void <整数类型说明符 >→ <signed类型说明符 > x <unsigned类型说明符 > x <字符类型说明符 > x <布尔类型说明符 > <signed类型说明符 >→ shortx shortint x signedshortx signedshortint x intx signedintx signed x longx longintx signedlongx signedlongint x longlongx longlongintx signedlonglong x signedlonglongint <unsigned类型说明符 > → unsignedshort int x unsigned int x unsignedlong int x unsignedlonglong int <字符类型声明符 >→char x signedchar x unsignedchar <浮点类型说明符 >→flat x double x longdouble x <复数类型说明符 > <复数类型声明符 >→float_Complex x double_Complex x longdouble_Complex <枚举类型说明符 >→ <枚举类型定义 > x <枚举类型引用 > <枚举类型引用 >→enum <枚举标签 > <枚举类型定义 >→enum <枚举标签 > { <枚举定义列表 > } x enum <枚举标签 > { <枚举定义列表 >,}  C99 <枚举定义列表 >→ <枚举常量定义 > x <枚举定义列表 >,<枚举常量定义 > <枚举常量定义 >→ <枚举常量 > x <枚举常量 >=<表达式 > <枚举常量 >→ <标识符 > 附 录 二   C语 言 语 法 · 415· <枚举标签 >→ <标识符 > <结构体类型说明符 >→ <结构体类型定义 > x <结构体类型引用 > <结构体类型引用 >→struct<结构体标签 > <结构体标签 >→ <标识符 > <结构体类型定义 >→struct <结构体标签 > { <字段列表 > } <共用体类型说明符 >→ <共用体类型定义 > x <共用体类型引用 > <共用体类型引用 >→union <共用体标签 > <共用体类型定义 >→union <共用体标签 > { <字段列表 > } <共用体标签 >→ <标识符 > <字段列表 >→ 成员声明 > x <字段列表 ><成员声明 > <成员声明 >→ <类型说明符 ><成员声明符列表 > <成员声明符列表 >→ <成员声明符 > x <成员声明符列表 >,<成员声明符 > <成员声明符 >→ <简单成员 > x <位字段 > <简单成员 >→ <声明符 > <位字段 >→ <声明符 > :<宽度 > <宽度 >→ <常量表达式 > <标识符列表 >→ <标识符 > x <参数列表 >,<标识符 > <标识符 >→ <无数字标识符 > x <标识符 ><无数字标识符 > x <标识符 ><数字 > <无数字标识符 >→ <非数字符号 > x <通用字符名 > x 其他实现定义的字符 <非数字符号 >→ Ax Bx Cx Dx Ex Fx Gx H x Ix Jx Kx Lx M x N x Ox Px Qx Rx Sx Tx Ux Vx W x Xx Yx Z x ax bx cx dx ex fx gx hx ix jx kx lx m x n x ox px qx rx sx tx ux vx wx xx yx z x_ <数字 >→1x 2x 3x 4x 5x 6x 7x 8x 9 <语句 >→ <表达式语句 > x <标号语句 > x <复合语句 > · 416· 附 录 二   C语 言 语 法 x <条件语句 > x <循环语句 > x <开关语句 > x <break语句 > x <continue语句 > x <返回语句 > x <goto语句 > x <空语句 > <表达式语句 >→ <表达式 > <标号语句 >→ <标号 > :<语句 > <复合语句 >→{ <声明或语句列表 > } <条件语句 >→ <if语句 > x <if-else语句 > <循环语句 >→ <while语句 > x <do语句 > x <for语句 > <开关语句 >→switch( <表达式 > ) <语句 > <break语句 >→break <continue语句 >→continue <返回语句 >→return <表达式 > <goto语句 >→goto <命名标号 > <空语句 >→ <标号 >→ <命名标号 > x <case标号 > x <缺省标号 > <命名标号 >→ <标识符 > <case标号 >→case<常量表达式 > <缺省标号 >→default <声明或语句列表 >→ <声明或语句 > x <声明或语句列表 ><声明或语句 > <声明或语句 >→ <声明 > x <语句 > <if语句 >→if( <表达式 > ) <语句 > <if-else语句 >→if( <表达式 > ) <语句 > else<语句 > <while语句 >→while( <表达式 > ) <语句 > <do语句 >→do<语句 > while(<表达式 >) <for语句 >→for<for表达式 > <语句 > <for表达式 >→( <初始子句 > ; <表达式 > ; <表达式 > ) <初始化子句 >→ <表达式 > x <声明 > 附 录 二   C语 言 语 法 <常量表达式 >→ <条件表达式 > <逗号表达式 >→ 赋值表达式 > x <逗号表达式 >, <赋值表达式 > <赋值表达式 >→ <条件表达式 > x <一元表达式 > <赋值操作符 > <赋值表达式 > <条件表达式 >→ <逻辑或表达式 > x <逻辑或表达式 >? <表达式 >:<条件表达式 > <逻辑或表达式 >→ <逻辑与表达式 > x <逻辑或表达式 > ||<逻辑与表达式 > <逻辑与表达式 >→ <位或表达式 > x <逻辑与表达式 >&&<位或表达式 > <位或表达式 >→ <位异或表达式 > x <位或表达式 >|<位异或表达式 > <位异或表达式 >→ <位与表达式 > x <位异或表达式 >^<位与表达式 > <位与表达式 >→ <相等表达式 > x <位与表达式 >&<相等表达式 > <相等表达式 >→ <关系表达式 > x <相等表达式 > <相等关系操作符 > <关系表达式 > <关系表达式 >→ <位移表达式 > x <关系表达式 > <关系运算符 > <位移表达式 > <位移表达式 >→ <加法表达式 > x <位移表达式 > <位移操作符 > <加法表达式 > <加法表达式 >→ <乘法表达式 > x <加法表达式 ><加法操作符 ><乘法表达式 > <乘法表达式 >→ <目标表达式 > x <乘法表达式 > <乘法操作符 > <目标表达式 > <目标表达式 >→ <一元表达式 > x (<类型名 >)<目标表达式 > <一元表达式 >→ <后缀表达式 > x <sizeof表达式 > x <一元减法表达式 > x <一元加法表达式 > x <逻辑非表达式 > x <位取反表达式 > x <地址表达式 > x <间接表达式 > x <前缀自增表达式 > x <前缀自减表达式 > <后缀表达式 >→ <基本表达式 > x <下标表达式 > x <成员选择表达式 > x <函数调用 > · 417· · 418· 附 录 二   C语 言 语 法 x <后缀自增表达式 > x <后缀自减表达式 > x <复合文字 > <sizeof表达式 >→ sizeof( <类型名 > ) x sizeof( <一元表达式 > ) <一元减法表达式 >→ -<目标表达式 > <一元加法表达式 >→ +<目标表达式 > <逻辑非表达式 >→! <目标表达式 > <位取反表达式 >→ ~ <目标表达式 > <地址表达式 >→& <目标表达式 > <间接表达式 >→ <目标表达式 > <前缀自减表达式 >→ --<一元表达式 > <前缀自增表达式 >→ ++<一元表达式 > <基本表达式 >→ <标识符 > x <常量 > x <括号表达式 > <括号表达式 >→( <表达式 > ) <下标表达式 >→ <后缀表达式 > [ <表达式 > ] <成员选择表达式 >→ <直接成员选择 > x <间接成员选择 > <直接成员选择 >→ <后缀表达式 > .<标识符 > <间接成员选择 >→ <后缀表达式 >-> <标识符 > <函数调用 >