《C语言实例解析精粹》


C 语言实例解析精粹 曹衍龙 林瑞仲 徐 慧 编著 人 民 邮 电 出 版 社 图书在版编目(CIP)数据 C 语言实例解析精粹/曹衍龙,林瑞仲,徐慧编著.—北京:人民邮电出版社,2005.3 ISBN 7-115-13183-X Ⅰ.C… Ⅱ.①曹… ②林… ③徐… Ⅲ.C 语言-程序设计 Ⅳ.TP312 中国版本图书馆 CIP 数据核字(2005)第 017833 号 内容提要 本书共分 8 篇,分别为基础篇、数据结构篇、数值计算与趣味数学篇、图形篇、系统篇、 常见试题解答篇、游戏篇和综合实例篇,汇集了近 200 个实例,基本涵盖了目前 C 语言编程 的各个方面。 书中以具体的实例为线索,特别注重对例题的分析、对知识点的归纳、对求解方法的引 申,同时程序代码中融会了 C 语言的各种编程技巧,条理清晰,以方便读者举一反三,开发 出符合特定要求的程序。本书的配套光盘中涵盖了书中所有实例的源代码,以方便读者学习 和查阅。 本书适合具有初步 C 语言基础的读者阅读,可作为高校相关专业的辅导教材,也可作为 C 语言使用者进行程序设计的实例参考手册。 C 语言实例解析精粹 ‹ 编 著 曹衍龙 林瑞仲 徐 慧 责任编辑 汤 倩 ‹ 人民邮电出版社出版发行 北京市崇文区夕照寺街 14 号 邮编 100061 电子函件 315@ptpress.com.cn 网址 http://www.ptpress.com.cn 读者热线 010-67132692 北京密云春雷印刷厂印刷 新华书店总店北京发行所经销 ‹ 开本:787×1092 1/16 印张:25 字数:706 千字 2005 年 3 月第 1 版 印数:1 – 5 000 册 2005 年 3 月北京第 1 次印刷 ISBN 7-115-13183-X/TP·4513 定价:44.00 元(附光盘) 本书如有印装质量问题,请与本社联系 电话: (010)67129223 前前 言言 当今的计算机软件设计中,无论开发技术如何发展,C 语言作为一种基 本的程序语言,仍然是程序开发人员必须掌握的基本功。掌握了 C 语言,不 但可以对结构化的编程有全面的了解,而且能够深入理解操作系统的运作方 式、内存管理分配方式以及硬件编程控制方法等。 目前流行的许多开发工具,包括微软的 Visual C++和 Visual C++ .NET, Borland 公司的 C++ Builder 等开发工具都还遵循着标准 C 语言的基本语法。 在很多嵌入式系统的软件设计中,甚至都采用 C 语言来进行开发。 为什么写本书 对于 C 语言的学习者来说,最重要的是具备程序设计的能力。初学者往 往能读懂别人编写的代码,而自己编写程序时却无所适从,往往不清楚通过 程序语言的控制结构如何将简单的计算步骤串联起来完成一项复杂的计算。 提高程序设计能力的一个重要途径是学习别人编写的程序,从中掌握解决问 题的核心方法和关键步骤,循序渐进,直至自己能够找出算法并编写程序。 本书正是为了满足 C 语言学习者的这种需求而策划的,这也是书名“C 语言 实例解析精粹”的由来。 本书特色 书中以近 200 个实例的编程求解为线索,突出了对例题的分析、对知识 点的归纳、对求解方法的引申,力图通过对例题的详细分析,帮助学习者快 速提高 C 语言的“程序设计能力”。 主要内容 基础篇——介绍了 C 语言编程的基础知识,包括第一个程序的建立,基 本数据类型的转换,数组、函数、指针、文件、结构、联合的使用等。 数据结构篇 ——介绍了冒泡排序、堆排序、归并排序等各种排序算法, 顺序表、双链表、二叉树、图的建立和操作等。 数值计算和趣味数学篇 ——数值计算部分包括多项式求值,线性方程求 解以及矩阵运算等。趣味数学部分主要介绍了一些经典问题的求解,包括绘 制余弦曲线,计算高次方数的尾数,求解阿姆斯特朗数,歌德巴赫猜想,素数幻方,爱因斯坦的 数学题,三色球问题等。 图形篇——介绍如何使用 Turbo C 提供的图形函数,绘制基本的直线、圆弧,设置屏幕颜色、 线条类型、填充类型;绘制直线类图形、金刚石、飘带、肾形、心脏形、沙丘等图案;绘制正多 边形、递归三角形、抛物样条曲线等;实现图形变换,VGA 编程,分形图,动画设计等。 系统篇——介绍如何通过 Turbo C 中的系统调用函数,编写屏幕窗口程序、获取系统各类信 息程序以及硬件参数读取程序等。 常见试题解答篇 ——介绍一些常见的 C 语言考试试题的解答方法,比如水果拼盘问题、计算 方差、统计符合特定条件的数、字符串倒置、部分排序、产品销售记录处理、求解三角方程、统 计选票、数字移位等。 游戏篇——介绍了 DOS 环境下的 C 语言游戏编程,包括商人过河游戏、吃数游戏、解救人质 游戏、打字训练游戏、双人竞走游戏、迷宫探险游戏、迷你撞球游戏、模拟扫雷游戏、推箱子游 戏、五子棋游戏。 综合实例篇 ——介绍了综合 CAD 系统、功能强大的文本编辑器、图书管理系统和进销存管理 系统。 书中所有实例均在 Turbo C 2.0 或 Borland C++ 3.1 编译器中调试通过。如果某些程序需要显示 汉字,可以通过 Visual C++或 Visual C++ .NET 编译运行;如果在 DOS 下运行,需要先运行汉字 环境,比如 UCDOS。具体的使用方法参见书中的“光盘使用说明”以及光盘中的相关文档。 本书在编写过程中,参考了《The C Programming Language》等书籍和 Internet 上的相关资源, 在此对相关的作者和机构深表谢意。 技术支持 本书主要由曹衍龙、林瑞仲编写,参加写作的人员还有吴越、徐慧、续瑞瑞、张静、程立、 吴阳等。在写作过程中,我们力求精益求精,但难免存在一些不足之处,恳请读者批评指正。如 果您在使用本书时遇到问题,可以发 E-mail 至 zjulinruizhong@yahoo.com.cn 和 tangqian@ptpress.com.cn 与我们联系。 编 者 2005 年 3 月 目 录 X 11 目目 录录 第第一一部部分分 基基础础篇篇 实例 1 第一个 C 程序............................................2 实例 2 求整数之积 .................................................6 实例 3 比较实数大小 ............................................8 实例 4 字符的输出 ...............................................10 实例 5 显示变量所占字节数 ..............................11 实例 6 自增/自减运算 .........................................13 实例 7 数列求和 ...................................................14 实例 8 乘法口诀表 ...............................................17 实例 9 猜数字游戏 ...............................................19 实例 10 模拟 ATM(自动柜员机)界面 ..........22 实例 11 用一维数组统计学生成绩 ....................24 实例 12 用二维数组实现矩阵转置 ....................26 实例 13 求解二维数组的最大 /最小元素..........29 实例 14 利用数组求前 n 个质数 ........................31 实例 15 编制万年历.............................................33 实例 16 对数组元素排序 ....................................36 实例 17 任意进制数的转换 ................................37 实例 18 判断回文数.............................................39 实例 19 求数组前 n 元素之和 ............................41 实例 20 求解钢材切割的最佳订单 ....................42 实例 21 通过指针比较整数大小 ....................... 44 实例 22 指向数组的指针 .................................... 48 实例 23 寻找指定元素的指针 ........................... 50 实例 24 寻找相同元素的指针 ........................... 52 实例 25 阿拉伯数字转换为罗马数字............... 53 实例 26 字符替换 ................................................ 56 实例 27 从键盘读入实数 .................................... 57 实例 28 字符行排版 ............................................ 59 实例 29 字符排列 ................................................ 60 实例 30 判断字符串是否回文 ........................... 62 实例 31 通讯录的输入输出 ............................... 63 实例 32 扑克牌的结构表示 ............................... 68 实例 33 用“结构”统计学生成绩................... 69 实例 34 报数游戏 ................................................ 72 实例 35 模拟社会关系 ........................................ 73 实例 36 统计文件的字符数 ............................... 74 实例 37 同时显示两个文件的内容................... 80 实例 38 简单的文本编辑器 ............................... 81 实例 39 文件的字数统计程序 ........................... 82 实例 40 学生成绩管理程序 ............................... 85 第第二二部部分分 数数据据结结构构篇篇 实例 41 插入排序.................................................96 实例 42 希尔排序 ..............................................100 22 W C 语言实例解析精粹 实例 43 冒泡排序.............................................. 102 实例 44 快速排序.............................................. 105 实例 45 选择排序.............................................. 109 实例 46 堆排序...................................................111 实例 47 归并排序...............................................115 实例 48 基数排序...............................................119 实例 49 顺序表插入和删除............................. 123 实例 50 链表操作.............................................. 126 实例 51 双链表.................................................. 129 实例 52 二叉树遍历..........................................130 实例 53 浮点数转换为字符串 .........................132 实例 54 汉诺塔问题..........................................133 实例 55 哈夫曼编码..........................................135 实例 56 图的深度优先遍历 .............................138 实例 57 图的广度优先遍历 .............................139 实例 58 求解最优交通路径 .............................141 实例 59 八皇后问题..........................................143 实例 60 骑士巡游..............................................145 第第三三部部分分 数数值值计计算算与与趣趣味味数数学学篇篇 实例 61 绘制余弦曲线和直线的迭加 ............ 150 实例 62 计算高次方数的尾数......................... 151 实例 63 打鱼还是晒网 ..................................... 151 实例 64 怎样存钱以获取最大利息................. 154 实例 65 阿姆斯特朗数 ..................................... 155 实例 66 亲密数.................................................. 156 实例 67 自守数.................................................. 157 实例 68 具有 abcd=(ab+cd)2 性质的数 ........... 158 实例 69 验证歌德巴赫猜想............................. 159 实例 70 素数幻方.............................................. 161 实例 71 百钱百鸡问题 ..................................... 163 实例 72 爱因斯坦的数学题............................. 164 实例 73 三色球问题.......................................... 165 实例 74 马克思手稿中的数学题..................... 166 实例 75 配对新郎和新娘................................. 167 实例 76 约瑟夫问题.......................................... 168 实例 77 邮票组合.............................................. 169 实例 78 分糖果.................................................. 170 实例 79 波瓦松的分酒趣题............................. 172 实例 80 求 π 的近似值......................................173 实例 81 奇数平方的有趣性质 .........................175 实例 82 角谷猜想..............................................176 实例 83 四方定理..............................................177 实例 84 卡布列克常数......................................178 实例 85 尼科彻斯定理......................................179 实例 86 扑克牌自动发牌..................................180 实例 87 常胜将军..............................................181 实例 88 搬山游戏..............................................182 实例 89 兔子产子(菲波那契数列).............183 实例 90 数字移动..............................................184 实例 91 多项式乘法..........................................186 实例 92 产生随机数..........................................189 实例 93 堆栈四则运算......................................190 实例 94 递归整数四则运算 .............................196 实例 95 复平面作图..........................................199 实例 96 绘制彩色抛物线..................................200 实例 97 绘制正态分布曲线 .............................203 实例 98 求解非线性方程..................................206 目 录 X 33 实例 99 实矩阵乘法运算 ................................. 209 实例 100 求解线性方程 ....................................211 实例 101 n 阶方阵求逆 .................................... 215 实例 102 复矩阵乘法 ....................................... 219 实例 103 求定积分 ............................................220 实例 104 求满足特异条件的数列 ...................221 实例 105 超长正整数的加法 ...........................222 第第四四部部分分 图图形形篇篇 实例 106 绘制直线 ............................................ 226 实例 107 绘制圆 ................................................ 230 实例 108 绘制圆弧 ............................................ 231 实例 109 绘制椭圆 ............................................ 232 实例 110 设置背景色和前景色 ....................... 233 实例 111 设置线条类型 .................................... 235 实例 112 设置填充类型和填充颜色 ............... 237 实例 113 图形文本的输出 ............................... 238 实例 114 金刚石图案 ........................................ 240 实例 115 飘带图案 ............................................ 241 实例 116 圆环图案 ............................................ 242 实例 117 肾形图案 ............................................ 243 实例 118 心脏形图案 ........................................ 244 实例 119 渔网图案 ............................................ 245 实例 120 沙丘图案 ............................................ 246 实例 121 设置图形方式下的文本类型 .......... 246 实例 122 绘制正多边形 ................................... 248 实例 123 正六边形螺旋图案 ........................... 249 实例 124 正方形螺旋拼块图案 .......................251 实例 125 图形法绘制圆 ....................................252 实例 126 递归法绘制三角形图案 ...................254 实例 127 图形法绘制椭圆................................255 实例 128 抛物样条曲线 ....................................257 实例 129 Mandelbrot 分形图案 ........................259 实例 130 绘制布朗运动曲线 ...........................261 实例 131 艺术清屏 ............................................262 实例 132 矩形区域的颜色填充 .......................263 实例 133 VGA256 色模式编程 .......................265 实例 134 绘制蓝天图案 ....................................266 实例 135 屏幕检测程序 ....................................267 实例 136 运动的小车动画................................268 实例 137 动态显示位图 ....................................269 实例 138 利用图形页实现动画 .......................270 实例 139 图形时钟 ............................................271 实例 140 音乐动画 ............................................274 第第五五部部分分 系系统统篇篇 实例 141 读取 DOS 系统中的国家信息........ 278 实例 142 修改环境变量 ................................... 279 实例 143 显示系统文件表 ............................... 280 实例 144 显示目录内容 ................................... 282 实例 145 读取磁盘文件 ....................................284 实例 146 删除目录树 ........................................286 实例 147 定义文本模式 ....................................287 实例 148 设计立体窗口 ....................................290 44 W C 语言实例解析精粹 实例 149 彩色弹出菜单 ................................... 292 实例 150 读取 CMOS 信息.............................. 293 实例 151 获取 BIOS 设备列表 ........................ 294 实例 152 锁住硬盘 ............................................ 295 实例 153 备份/恢复硬盘分区表 ......................297 实例 154 设计口令程序 ....................................298 实例 155 程序自我保护 ....................................300 第第六六部部分分 常常见见试试题题解解答答篇篇 实例 156 水果拼盘 ............................................ 304 实例 157 小孩吃梨 ............................................ 305 实例 158 删除字符串中的特定字符 .............. 306 实例 159 求解符号方程 ................................... 307 实例 160 计算方差 ............................................ 308 实例 161 求取符合特定要求的素数 .............. 309 实例 162 统计符合特定条件的数................... 310 实例 163 字符串倒置 ....................................... 312 实例 164 部分排序 ............................................ 314 实例 165 产品销售记录处理 ........................... 316 实例 166 特定要求的字符编码 ....................... 318 实例 167 求解三角方程 ....................................320 实例 168 新完全平方数 ....................................321 实例 169 三重回文数 ........................................323 实例 170 奇数方差 ............................................324 实例 171 统计选票 ............................................326 实例 172 同时整除 ............................................328 实例 173 字符左右排序 ....................................329 实例 174 符号算式求解 ....................................331 实例 175 数字移位 ............................................333 实例 176 统计最高成绩 ....................................334 第第七七部部分分 游游戏戏篇篇 实例 177 商人过河游戏 ................................... 338 实例 178 吃数游戏 ............................................ 340 实例 179 解救人质游戏 ................................... 341 实例 180 打字训练游戏 ................................... 344 实例 181 双人竞走游戏 ................................... 346 实例 182 迷宫探险游戏 ....................................349 实例 183 迷你撞球游戏 ....................................351 实例 184 模拟扫雷游戏 ....................................353 实例 185 推箱子游戏 ........................................357 实例 186 五子棋游戏 ........................................359 第第八八部部分分 综综合合实实例例篇篇 实例 187 综合 CAD 系统................................. 362 实例 188 功能强大的文本编辑器................... 368 实例 189 图书管理系统 ....................................381 实例 190 进销存管理系统................................385 第第一一部部分分 基基 础础 篇篇 据结构篇 精彩导读 第一个 C 程序 数列求和 模拟 ATM 界面 编制万年历 任意进制数的转换 文件的字数统计 22 W C 语言实例解析精粹 实例 1 第一个 C 程序 实例说明 本实例将建立第一个 C 程序,通过使用 Borland 公司的 Turbo C 2.0 软件新建一个 C 程序, 在该程序中添加代码,使其能够输出“Hello World!”的问候语。本实例重点介绍如何创建、编 译、调试和运行 C 程序。 程序运行效果如图 1-1 所示。 C 语言程序的编译器有很多,如 Turbo C 2.0、 Turbo C 3.0、Borland C++ 3.1、Visual C++ 6.0 和 Visual C++.NET 等开发环境都可以编译运行 C 程序。其中, 最常用的是 Turbo C 2.0(简称 TC)。本书配套光盘上 的【Turbo C 2.0】目录下有该编译器的自解压文件 tc.exe。要在硬盘上安装 TC 编译器,可按如下步骤进行: 【1】将配套光盘【Turbo C 2.0】目录下的 tc.exe 文件拷贝到硬盘上,比如 C 盘根目录下。 【2】双击硬盘上的 tc.exe 文件,出现如图 1-2 所示的自解压文件向导。 【3】采用默认的安装目录【C:】,单击【安装】按钮,完成安装。 图 1-2 Turbo C 2.0 的安装向导 实例解析 安装好 TC 编译器之后,就可以按照如下步骤创建第一个 C 程序。 【1】双击 TC 安装目录(比如【C:\TC】目录)下的 tc.exe 程序,打开 TC 开发环境。 【2】此时 TC 编译器会自动创建一个 NONAME.C 的 C 程序,如图 1-3 所示。 【3】在该文件中添加【程序代码】中所示的内容。 【4】使用【Alt+F】组合键激活 File 菜单,通过上下光标键选择“Save”命令,或用快捷键“F2” 打开如图 1-4 所示的保存文件对话框,输入保存路径和文件名,按回车键,完成保存。 图 1-1 第一个 C 程序运行效果 第一部分 基础篇 X 33 图 1-3 TC 开发环境 图 1-4 保存程序文件 【5】执行菜单命令“Compile | Compile to OBJ”(见图 1-5),可将程序编译为 OBJ 对象文件, 此时编译器将对程序的语法语义进行检查,若有错误或警告,将给出提示。如图 1-6 所示是编译 成功的例子,图 1-7 所示是将 printf 语句后面的用于表示语句结束的分号“;”去掉后出现的错误 信息。 图 1-5 编译程序 44 W C 语言实例解析精粹 图 1-6 编译成功的信息 图 1-7 编译时报错的信息 【6】执行菜单命令“Compile | Make EXE File”,生成可执行文件,如图 1-8 所示。 图 1-8 生成可执行文件 【7】执行菜单命令“Run | Run”,或按快捷键【Ctrl + F9】,运行程序,如图 1-9 所示。程序 运行结果见图 1-1。 第一部分 基础篇 X 55 图 1-9 运行程序 【8】要查看程序运行结果,可以执行菜单命令“File | OS Shell”切换到命令行下查看结果, 如图 1-10 所示。在命令行下键入“exit”,可以返回 TC 开发环境。 图 1-10 查看运行结果 程序代码 【程序 1】 第一个 C 程序 /* The first C programme */ #include /* 包含标准输入输出头文件 */ main() /* 主函数 */ { printf("Hello World!\n"); /* 打印输出信息 */ } 归纳注释 #include 语句用于包含头文件 stdio.h,即定义标准输入输出函数的头文件。 每一个 C 程序必须有且只有一个 main()主函数,这样程序运行时才可以找到入口。 本程序通过 printf 函数向标准的显示终端打印输出字符“Hello World!”。该程序仅打印这些字 符,因此光标此时还停在同一行。通常程序打印输出一行后,需要回车换行,这里可以通过加入 控制符“\n”来实现,即 pprriinnttff((““HHeelllloo WWoorrlldd!!\\nn””));;。。 TC 编译器使用的其他问题,可以通过运行 TC 安装目录下的 Readme.exe 帮助文件来了解。 66 W C 语言实例解析精粹 基本上所有的功能都可以通过其菜单实现。相信读者只要多尝试,多使用这些菜单命令,很快就 可以掌握 TC 开发环境。 值得注意的是,TC 开发环境中,无法显示中文,而在 Visual C++ 6.0 及 Visual C++.NET 等编 译器中支持中文,且运行时也能够显示。读者若需要应用中文,可采用这些开发环境。比如上面 的 printf 语句改为“printf(“世界,您好!”);”,则显示在命令行下即为“世界,您好!”。本书的 实例程序中的代码尽量不使用中文,以便与 TC 编译器兼容,而其后的注释则采用中文,这样虽 然在 TC 编译器中无法看到中文(看到的是乱码),但用记事本等其他文本阅读工具打开,则可以 看到中文,以便增加程序的可读性。 实例 2 求整数之积 实例说明 从键盘输入两个整数,输出它们的积。通过本实例,读者可以理解从键盘读取输入的数据以 及输出整型变量等方法。程序运行结果如图 2-1 所示。 图 2-1 实例 2 程序运行结果 实例解析 C 语言提供的数据结构是以数据类型形式出现的。C 语言的数据类型可分为基本类型、构造 类型、指针类型 3 大类。其中基本类型包括整型(变量声明关键字为 int),字符型(变量声明关 键字为 char),实型(即浮点型,分为单精度浮点型 float 和双精度浮点型 double float),枚举类型 (变量声明关键字为 enum)。构造类型包括数组、结构体和共用体 3 种。 整型、字符型和实型都有常量和变量之分。比如 12、0、−3 为整型常量,4.6、−1.23 为实型 常量,'a'、'D'为字符常量。常量一般从其字面形式即可判别,也可以用一个标识符代表一个常量, 比如用预定义#define PRICE 30,这样可提高程序的可读性和可维护性。 值可以改变的量称为变量。变量名只能由字母、数字和下划线组成,且第一个字符必须为字 母或下划线。整型变量可分为基本型(用 int 表示)、短整型(用 short int 或 short 表示)、长整型 (用 long int 或 long 表示)和无符号型(包括无符号整型、无符号短整型、无符号长整型,分别用 unsigned int、unsigned short 和 unsigned long 表示)。 各种整型变量在不同的计算机机型上存放的内存字节数不同,典型的 IBM PC 中所占的位 (bit)和数的范围如表 2-1 所示。整型变量的定义为: int a,b; /* 定义变量 a,b 为整型 */ unsigned short c,d; /* 定义变量 c,d 为无符号短整型 */ 第一部分 基础篇 X 77 long e,f; /* 定义变量 e,f 为长整型 */ 表 2-1 整型变量所占位数和数的范围 数据类型 所占位数 数的范围 int 16 −32768~32768 即−215~(215−1) Short [int] 16 −32768~32768 即−215~(215−1) Long [int] 32 −2147483648~2147483647 即−231~(231−1) unsigned [int] 16 0~65535 即 0~(216−1) unsigned short 16 0~65535 即 0~(216−1) unsigned long 32 0~4294967295 即 0~(232−1) 在本例中,设两个整数分别为 x、y,它们的乘积为 m;程序首先调用 printf()函数,提示用户 输入数据,然后调用 scanf()函数,输入变量 x 和 y 的值,接着求 x 与 y 的积 m,最后输出结果。 上述过程用算法描述如下: 【算法】 读入两个整数,输出它们的积 { 提示用户输入数据; 输入变量 x 和 y 的值; 计算乘积; 输出乘积; } 程序代码 【程序 2】 求整数之积 /* Input two numbers, output the product */ #include main() { int x,y,m; /* 定义整型变量 x,y,m */ printf("Please input x and y\n"); /* 输出提示信息 */ scanf("%d%d",&x,&y); /* 读入两个乘数,赋给 x,y 变量 */ m=x*y; /* 计算两个乘数的积,赋给变量 m */ printf("%d * %d = %d\n",x,y,m); /* 输出结果 */ } 归纳注释 本实例程序实现的是两个整数的简单乘积,同样的,也可以通过修改,实现简单的整数四则 运算。整型变量包括整型、短整型、长整型和无符号整型,在 printf()和 scanf()函数中的格式说明 都可以是%d。 printf()输出函数(关键字中的 f 就是表示 format,格式化的意思)用于输出变量的值时,调 用格式为“printf(格式控制,输出表列);”,“格式控制”是用双引号括起来的字符串,包括格式字 符串和普通字符。格式字符串是以%开头的,在%后跟各种格式字符,以说明输出数据的类型、 形式、长度、小数位数等。如“%d”表示按十进制整型输出,“%ld”表示按十进制长整型输出等。 格式字符串。在 Turbo C 中,格式字符串的一般形式为:[标志][输出最小宽度][.精度][长度] 类型。其中方括号[]中的项为可选项。各项的意义如下:①类型字符用以表示输出数据的类型,d 88 W C 语言实例解析精粹 ——以十进制形式输出带符号整数(正数不输出符号);o——以八进制形式输出无符号整数(不输出 前缀 O);x——以十六进制形式输出无符号整数(不输出前缀 OX);u——以十进制形式输出无符 号整数;f——以小数形式输出单、双精度实数;e——以指数形式输出单、双精度实数;g——以 %f%e 中较短的输出宽度输出单、双精度实数;c——输出单个字符;s——输出字符串。②标志 字符为−、+、#、空格 4 种,−——结果左对齐,右边填空格;+——输出符号(正号或负号),输 出值为正时冠以空格,为负时冠以负号;#——对 c、s、d、u 类无影响;对 o 类,在输出时加前 缀 o;对 x 类,在输出时加前缀 0x;对 e、g、f 类当结果有小数时才给出小数点。③输出最小宽 度:用十进制整数来表示输出的最少位数。若实际位数多于定义的宽度,则按实际位数输出,若 实际位数少于定义的宽度则补以空格或 0。④精度格式符以“.”开头,后跟十进制整数。如果输 出数字,则表示小数的位数;如果输出的是字符,则表示输出字符的个数;若实际位数大于所定 义的精度数,则截去超过的部分。⑤长度格式符为 h、l 两种,h 表示按短整型量输出,l 表示按 长整型量输出。 scanf()为输入函数,即按用户指定的格式从键盘上把数据输入到指定的变量之中。scanf 函数 的一般形式为“scanf(“格式控制字符串”,地址表列);”其中,格式控制字符串的作用与 printf 函数相同,但不能显示普通字符串,也就是不能显示提示字符串。地址表列中给出各变量的地址。 地址是由地址运算符“&”后跟变量名组成的。例如,“&a,&b”分别表示变量 a 和变量 b 的地址。 这个地址就是编译器在内存中给 a 和 b 变量分配的地址。 实例 3 比较实数大小 实例说明 从键盘输入两个实数,输出它们中比较大的数。通过本实例,可以学习如何从键盘读取输入 的实数,如何实现实数的大小比较,以及实型变量的基本概念等。程序运行结果如图 3-1 所示。 图 3-1 实例 3 程序运行结果 实例解析 实型变量分为单精度(float 型)和双精度(double 型)两类。对于每一个实型变量在使用前 都应定义。例如: float x,y ; /* 指定 x、y 为单精度实数 */ double z ; /* 指定 z 为双精度实数 */ 在一般系统中,一个 float 型数据在内存中占用 4 个字节(32 位),一个 double 型数据占 8 个 字节。单精度实数提供 7 位有效数字,双精度实数提供 15~16 位有效数字,数值的范围随机器系 第一部分 基础篇 X 99 统而异。在 IBM PC 中,单精度实数的数值范围约为 10-38~1038,双精度实数范围约为 10-308~10308。 本实例先定义 3 个单精度浮点型变量,用于存放 x,y 和它们中的大数 c。接着,提示用户已 经输入的信息,通过条件运算符来比较大小,得到 c,最后输出结果。 条件运算符有 3 个操作对象,称三目(元)运算符,它是 C 语言中惟一的一个三目运算符。 条件表达式的一般形式为: 表达式 1?表达式 2:表达式 3 条件表达式的说明 (1)条件表达式的执行顺序:先求解表达式 1,若为真(非 0)则求解表达式 2,并将表达式 2 的值作为条件表达式的值,若表达式 1 为假(0),则求解表达式 3,并将表达式 3 的值作为条件 表达式的值。 max=(a>b)?a:b; 执行结果就是将条件表达式的值赋给 max,即将 a,b 中比较大的值赋给 max。 (2)条件运算符优先于赋值运算符,因此上面的赋值表达式的求解过程是先求解条件表达式, 再将它的值赋给 max。 条件表达式的优先级比关系运算符和算术运算符都低,因此, max=(a>b)?a:b; 可以写成 max=a>b?a:b; 又比如 a>b?a:b+1; 相当于 a>b?a:(b+1),而不等价于(a>b?a:b)+1。 (3)条件运算符的结合方向为“自右向左”。比如 a>b?a:c>d?c:d; 相当于 a>b?a:(c>d?c:d),如果 a=1,b=2,c=3,d=4,则条件表达式的值等于 4。 (4)条件表达式中,表达式 1 的类型可以与表达式 2 和表达式 3 的类型不同。如 x?’a’:’b’; x 是整型变量,若 x=0,则条件表达式的值为 b。表达式 2 和表达式 3 的类型也可以不同,此 时条件表达式的值的类型为二者中较高的类型。比如 x>y?1:1.5; 如果 x 小于等于 y,则条件表达式的值为 1.5,若 x 大于 y,则值为 1,由于 1.5 为实型,比整 型高,因此将 1 转换为实型 1.0。 程序代码 【程序 3】 比较实数大小 /* 输入两个浮点数,输出其中较大的数 */ #include main() { float x,y,c; /* 变量定义 */ printf("Please input x and y:\n"); /* 提示用户输入数据 */ scanf("%f%f",&x,&y); c=x>y?x:y; /* 计算 c=max(x,y) */ printf("MAX of (%f,%f) is %f",x,y,c); /* 输出 c */ } 1100 W C 语言实例解析精粹 归纳注释 在本例中,由于 x,y 变量定义为 float 型,在输出定义没有说明小数点后面有几位时,系统 的默认值为 6 位小数,因此,图 3-1 的输出结果为精确到 6 位。 由于实型变量的小数位具有默认值,而且系统会取近似值,因此在进行精确度要求较高的运 算时,必须指定小数位数。 条件表达式在计算最大值最小值时比较方便,还可以计算 3 个数的最值,比如: max(x,y,z)=(x>y?x:y)>z?(x>y?x:y):z。 实例 4 字符的输出 实例说明 从键盘输入字符,再输出它们。通过本实例,可以学习如何从键盘读取输入的字符,如何实 现字符的输出,以及字符串、字符常量、转义字符的基本概念等。程序运行结果如图 4-1 所示。 图 4-1 实例 4 程序运行结果 实例解析 字符变量的定义是“char c1,c2;”,字符变量的赋值可以用 cc11==''aa''来赋值,也可以从键盘输入, 其格式说明符为“%c”。 字符常量是用单引号括起来的一个字符。例如'a'、'b'、'='、'+'、'?'都是合法字符常量。在C语 言中,字符常量有以下特点。 (1)字符常量只能用单引号括起来,不能用双引号或其他括号。 (2)字符常量只能是单个字符,不能是字符串。 (3)字符可以是字符集中任意字符。但数字被定义为字符型之后就不能参与数值运算。如'5' 和 5 是不同的。'5'是字符常量,不能参与运算。 字符串常量是由一对双引号括起的字符序列。例如"CHINA"、"C program"、"$12.5"等都是合 法的字符串常量。字符串常量和字符常量是不同的量,它们之间主要有以下区别。 (1)字符常量由单引号括起来,字符串常量由双引号括起来。 (2)字符常量只能是单个字符,字符串常量则可以含一个或多个字符。 (3)可以把一个字符常量赋予一个字符变量,但不能把一个字符串常量赋予一个字符变量。 (4)字符常量占一个字节的内存空间。字符串常量占的内存字节数等于字符串中字节数加 1。 增加的一个字节中存放字符"\0"(ASCII 码为 0)。这是字符串结束的标志。例如,字符串"C program" 第一部分 基础篇 X 1111 在内存中所占的字节为 C program\0。字符常量'a'和字符串常量"a"虽然都只有一个字符,但在内存 中的情况是不同的。'a'在内存中占一个字节,可表示为 a。"a"在内存中占两个字节,可表示为 a\0。 转义字符是一种特殊的字符常量。转义字符以反斜线"\"开头,后跟一个或几个字符。转义字 符具有特定的含义,不同于字符原有的意义,故称“转义”字符。例如,在前面例题 printf()函数 的格式串中用到的“\n”就是一个转义字符,其意义是“回车换行”。转义字符主要用来表示那些 用一般字符不便于表示的控制代码。常用的转义字符及其含义如表 4-1 所示。 表 4-1 常用转义字符 转义字符 转义字符的意义 转义字符 转义字符的意义 \n 回车换行 \\ 反斜线符"\" \t 横向跳到下一制表位置 \' 单引号符 \v 竖向跳格 \a 鸣铃 \b 退格 \ddd 1~3 位八进制数所代表的字符 \r 回车 \xhh 1~2 位十六进制数所代表的字符 \f 走纸换页 广义地讲,C语言字符集中的任何一个字符均可用转义字符来表示,\ddd 和\xhh 正是为此而 提出的。ddd 和 xhh 分别为八进制和十六进制的 ASCII 代码。如\101 表示字母"A" ,\102 表示字 母"B",\134 表示反斜线,\XOA 表示换行等。 本例先定义 3 个字符变量,用于保存输入的字符,接着输出字符串、转义字符和字符变量。 程序代码 【程序 4】 字符的输出 /* 输入字符,输出通过转义字符控制的字符*/ #include main() { char c1,c2,c3; printf("Please input chars to c1,c2,c3:\n"); scanf("%c%c%c",&c1,&c2,&c3); printf("%s\n%c\n\t%c %c\n %c%c\t\b%c\n","The output is:",c1,c2,c3,c1,c2,c3); } 归纳注释 程序中,首先输出一个字符串,接着是“\n” 回车换行;在第一列输出 c1 值之后就是“\n”; 接着又是“\t”,于是跳到下一制表位置(设制表位置间隔为 8),再输出 c2 值;空一格再输出 c3 后又是“\n”,因此再回车换行;再空一格之后又输出 c1 值;接着输出 c2 的值,跳到下一制表位 置(与上一行的 c2 对齐),但下一转义字符“\b”又退回一格,故紧挨着 c2 再输出 c3 值。 实例 5 显示变量所占字节数 实例说明 通过程序显示 int、long、float、double 等变量在计算机系统中所占的位数。程序运行结果如 图 5-1 所示。 1122 W C 语言实例解析精粹 图 5-1 实例 5 程序运行结果 实例解析 前面已经说明整型变量在内存中所占的字节数(1 字节=8 位)。不同的编译器,变量所占的 字节数也不同。在 TC 编译器中,一般整型 int 变量占 2 字节,char 变量占 1 字节,short 整型占 2 字节,long 整型占 4 字节,float 型占 4 字节,double 型占 8 字节,long double 型占 8、10 或 12 字节(根据不同的系统)。 程序先输出提示信息,接着依次输出各个类型变量所占的字节数。 程序代码 【程序 5】 显示变量所占的字节数 /* 输出不同类型所占的字节数*/ #include void main() { printf("The bytes of the variables are:\n"); /*int 型在不同的 PC 机,不同的编译器中的字节数不一样,*/ /*一般来说在 TC2.0 编译器中字节数为 2,在 VC 编译器中字节数为 4 */ printf("int:%d bytes\n",sizeof(int)); /* char 型的字节数为 1 */ printf("char:%d byte\n",sizeof(char)); /* short 型的字节数为 2 */ printf("short:%d bytes\n",sizeof(short)); /* long 型的字节数为 4 */ printf("long:%d bytes\n",sizeof(long)); /* float 型的字节数为 4 */ printf("float:%d bytes\n",sizeof(float)); /* double 型的字节数为 8 */ printf("double:%d bytes\n",sizeof(double)); /* long double 型的字节数为 8 或 10 或 12 */ printf("long double:%d bytes\n",sizeof(long double)); } 归纳注释 sizeof()是保留字,它的作用是求某类型或某变量类型的字节数,括号中可以是类型保留字或 变量。比如,iinntt aa==11;;ssiizzeeooff((aa))==22。 第一部分 基础篇 X 1133 实例 6 自增/自减运算 实例说明 通过整型变量的自增运算结果来显示自增运算符的使用方法。程序运行结果如图 6-1 所示。 图 6-1 实例 6 程序运行结果 实例解析 自增 1 运算符记为“++”,其功能是使变量的值自增 1。自减 1 运算符记为“--”,其功能是 使变量值自减 1。自增 1,自减 1 运算符均为单目运算,都具有右结合性。可有以下几种形式: ++i i 自增 1 后再参与其他运算 --i i 自减 1 后再参与其他运算 i++ i 参与运算后,值再自增 1 i-- i 参与运算后,值再自减 1 在使用上容易出错的是 i++和 i--。特别是当它们出现在较复杂的表达式或语句中时,常常难 于弄清。 本实例程序先定义变量 a、b、c 和 i 并初始化 a 和 i,接着赋值给 b、c,输出 a、b、c,最后 输出 i 的各种自增自减运算结果。 程序代码 【程序 6】 自增/自减运算 /* 自增运算符的应用*/ #include main() { int a=5,b,c,i=10; /* 变量定义初始化 */ b=a++; /* a 赋给 b 后 a 自增 1 */ c=++b; /* b 自增 1,后赋给 c */ printf("a = %d, b = %d, c = %d\n",a,b,c); /* 输出 abc */ printf("i,i++,i++ = %d,%d,%d\n",i,i++,i++); /* 输出 i,i++,i++ */ printf("%d\n",++i); /* i 自增 1,输出 i,此时 i=12+1 */ printf("%d\n",--i); /* i 自减 1,输出 i,i=13-1=12 */ printf("%d\n",i++); /* 输出 i=12,i 自增 1,i=13 */ printf("%d\n",i--); /* 输出 i=13,i 自减 1,i=12 */ 1144 W C 语言实例解析精粹 printf("%d\n",-i++); /* -(i++)即 i 取出,加负号,输出-12,i 取出前已自增 1,i=13*/ printf("%d\n",-i--); /* i 取出,加负号,输出-13,i 取出前已再自减 1,i=12*/ } 归纳注释 自增自减运算符具有右结合性,在本实例程序中的第二个 printf 语句,运算顺序是自右向左, 计算最右边的 i++,即输出 i=10,再对 i 自增 1,此时 i=11,再计算中间的 i++,即输出 i=11, i 自增 1,此时 i=12,最后输出 i=12。 自增运算符通常运用与 for,while 等循环语句中,对循环变量进行自增或自减。 实例 7 数列求和 实例说明 计算 1+1+2+1+2+3+1+2+3+4+…+1+2+…+n 的值。通过该实例,可以学习 if 条件判断语句和 for 循环语句的应用。程序运行结果如图 7-1 所示。 图 7-1 实例 7 程序运行结果 实例解析 if 条件语句 if 语句有以下 3 种形式。 (1)if 语句 if(x>y) printf("%d",x); 其中,if 语句就是根据 x>y 这个表达式的值来决定是否要执行后面的 printf 语句,当表达式 的值为真(x>y)时,就执行,当表达式的值为假(x≤y)时,就跳过 printf 语句而直接执行后面 的语句。 (2)ifelse 语句 if(x>y) printf("%d",x); else printf("%d",y); 如果 x>y 成立,则打印出 x,否则打印出 y。与第一种 if 语句的不同之处在于,不论表达式的 值是真是假,if 语句都分别给它们安排了任务。也就是说无论 x,y 哪一个大,总能打印出一个数。 (3)if…elseif 语句 if(count>500) price = 0.15; else if(count>300) price = 0.10; else if (count>100) price = 0.05; 第一部分 基础篇 X 1155 else price = 0; 这段程序的意思是,如果 count 的值是在 500 以上,那么 price 等于 0.15;如果 count 的值在 300 到 500 之间(else if 可以理解为否则如果,即在不大于 500 的条件下如果满足大于 300 的条件, 也就是 300 到 500 之间),那么 price 等于 0.10;如果 count 的值在 100 到 300 之间,那么 price 等 于 0.05;如果 count 在 0 到 100 之间,那么 price 等于 0。这种形式的 if 语句比前两种都要复杂, 它能处理的分支也多,这个例子就能处理 4 个分支,而且分支的数目还可以无限扩展。 尽管 if 语句看起来很简单,但在实际的应用中还是有许多问题需要注意的。 (1)不要忘记在每个语句后面加分号。分号是 C 语句中不可缺少的部分,任何语句,不管它 是不是作为 if 语句的一部分,结尾都要加上分号表示一个语句的结束。如果忘记在语句末尾加分 号,则会出现语法错误。 (2)要注意 if 语句的嵌套问题。 在一个 if 语句中又包含一个或多个 if 语句时,就形成了嵌套 if 语句。嵌套 if 语句的一般形式 如下: if (表达式 1) if (表达式 2) 语句 1 else 语句 2 else if (表达式 3) 语句 3 else 语句 4 要注意嵌套 if 语句中 if 与 else 的匹配问题。记住一个原则:else 语句总是与前面最近的未匹 配的 if 语句相配对。按照这个原则,上面第 3 行的 else 跟第 2 行的 if 相匹配,第 4 行的 else 跟第 1 行的 if 相匹配,第 6 行的 else 跟第 5 行的 if 相匹配。而初学者往往容易忘记这个原则。容易出 错的形式一般是这样的: if (表达式 1) if (表达式 2) 语句 1 else if (表达式 3) 语句 2 else 语句 3 这种形式的程序很可能是想让第 3 行的 else 跟第 1 行的 if 相匹配,可事实上按照 else 总是跟 最近的 if 相匹配的原则,第 3 行的 else 是跟第 2 行的 if 相匹配的。那怎样实现原来的意图呢,可 以用花括号来确定配对关系。例如: if (表达式 1) {if (表达式 2) 语句 1} else if (表达式 3) 语句 2 else 语句 3 这时{ }说明了第 2 行是一个完整的内嵌 if 语句,不需要别的 else 来匹配了,因此第 3 行的 else 就与第 1 行的 if 相匹配了。 for 循环语句 C 语言中的 for 语句使用最为灵活,不仅可以用于循环次数已经确定的情况,而且可以用于 循环次数不确定而只给出循环结束条件的情况。for 循环的一般形式为: for(表达式 1; 表达式 2; 表达式 3) 循环语句 for 语句的执行过程是这样的:①先求解表达式 1;②再求解表达式 2,如果它的值为真,则 执行循环语句,然后执行下面第③步;③如果表达式的值为假,则循环结束,转到第④步;④求 1166 W C 语言实例解析精粹 解表达式 3,然后转到第②步;⑤执行 for 语句后面的语句。 for 循环的 3 个表达式各有不同的作用,因此也有不同的形式。最普通的,表达式 1 作为初始 化是个赋值语句,表达式 2 作为循环的测试条件一般是关系表达式,表达式 3 作为循环前进的动 力是个增量表达式。这 3 个表达式都是可以省略的,但后面的分号“;”不能省。表达式 1 被省略 相当于循环变量的初始化被省略了,那么一般来说在 for 语句之前应该给循环变量赋初值。执行 的时候,只是跳过“求解表达式 1”这一步,其他都不变。如果作为循环测试条件的表达式 2 省 略了,那么循环测试条件将被认为永远是 true,循环也就无休止地进行下去。如果表达式 3 省略 了,那么此时程序应该另外设法保证循环能正常结束。 本实例程序先输出提示信息,要求用户输入数列的项数 n,接着通过 if 语句判断所输入的 n 是否大于等于 1(只有不小于 1 数列才有意义),如果小于 1,则程序直接返回退出。实例通过两 个 for 循环语句实现数列的求和。 程序代码 【程序 7】 数列求和 /* 计算数列的和*/ #include void main() { int i,j,n; /* 定义循环变量 i,j,数列项数 n */ long int sum=0,temp=0; /* 定义数列的和及临时变量*/ printf("Please input a number to n:\n"); /* 提示输入数列项数*/ scanf("%d",&n); if(n<1) /* 如果输入的数小于 1*/ { printf("The n must be no less than 1!\n"); /* 提示输入有误*/ return; /* 程序返回,退出*/ } for(i=1;i<=n;i++) /* 循环计算数列的和*/ { temp=0; for(j=1;j<=i;j++) temp+=j; sum+=temp; } printf("The sum of the sequence(%d) is %d\n",n,sum); /* 输出数列的和*/ } 归纳注释 for 语句的表达式 1 可以是设置循环变量初值的赋值表达式,也可以是与循环变量无关的其他 表达式。比如“for( sum=0,i=1;i<=100;i++) sum=sum+I;”表达式可以是一个简单的表达式,也 可以是逗号表达式,即包含一个以上的简单表达式,中间用逗号间隔。这样在 for 语句前面就可 以不必为 sum 赋初值了,程序看起来就更紧凑了。 表达式 2 一般是关系表达式(如 i<=100)或逻辑表达式(如 a #include void main(void) { int i,j,x,y; clrscr(); /* 清屏 */ printf("\n\n***Pithy Formula Table of Multiplication***\n\n"); /*显示提示信息*/ x=9; y=5; /* 输出横轴数字 */ for(i=1;i<=9;i++) { gotoxy(x,y); /* 移到指定的光标位置 */ printf("%2d ",i); /* 打印横轴数字 */ x+=3; } x=7; y=6; /* 输出纵轴数字 */ for(i=1;i<=9;i++) { gotoxy(x,y); /* 移到指定的光标位置 */ printf("%2d ",i); /* 打印纵轴数字 */ y++; } x=9; y= 6; /* 计算并显示 1×1~9×9 */ for(i=1;i<=9;i++) { for(j=1;j<=9;j++) { gotoxy(x,y); /* 移到指定的光标位置 */ printf("%2d ",i*j); /* 打印乘法结果 */ y++; } y-=9; x+=3; } printf("\n\n Press any key to quit...\n"); getchar(); } 归纳注释 clrscr 函数是清屏函数(Clear Screen),用于清除屏幕上显示的所有信息,并把光标放在第一 行第一列。该函数的功能相当于在 DOS 操作系统下,或者在 Windows 操作系统下的命令行模式 第一部分 基础篇 X 1199 中的 clr 命令。 gotoxy(int x, int y)函数用于将光标移动到指定的(x,y)坐标位置。该函数通常用于控制在屏幕上 指定位置的内容显示。 实例 9 猜数字游戏 实例说明 实现一个简单的猜数字游戏,学习 while 循环语句的用法。程序运行结果如图 9-1 所示。 图 9-1 实例 9 程序运行结果 实例解析 while 循环语句 while 语句的一般形式为: while(表达式)语句; 其中表达式是循环条件,语句为循环体。 while 语句的语义是:计算表达式的值,当值为真(非 0)时,执行循环体语句。使用 while 语句应注意以下几点。 (1)while 语句中的表达式一般是关系表达或逻辑表达式,只要表达式的值为真(非 0)即可 继续循环。 (2)循环体如包含有一个以上的语句,则必须用{}括起来,组成复合语句。 (3)应注意循环条件的选择以避免死循环。 (4)允许 while 语句的循环体中包含 while 语句,从而形成双重循环。 do-while 语句 do-while 语句的一般形式为: 2200 W C 语言实例解析精粹 do 语句; while(表达式); 其中语句是循环体,表达式是循环条件。 do-while 语句的语义是:先执行循环体语句一次,再判别表达式的值,若为真(非 0)则继 续循环,否则终止循环。 do-while 语句和 while 语句的区别在于 do-while 是先执行后判断,因此 do-while 至少要执行 一次循环体。而 while 是先判断后执行,如果条件不满足,则一次循环体语句也不执行。while 语 句和 do-while 语句一般可以相互改写。 对于 do-while 语句还应注意以下几点。 (1)在 if 语句和 while 语句中,表达式后面都不能加分号,而在 do-while 语句的表达式后面 则必须加分号。 (2)do-while 语句也可以组成多重循环,而且也可以和 while 语句相互嵌套。 (3)在 do 和 while 之间的循环体由多个语句组成时,也必须用{}括起来组成一个复合语句。 (4)do-while 和 while 语句相互替换时,要注意修改循环控制条件。 do-while 语句也可与 while,for 语句相互嵌套,构成多重循环。以下 3 种都是合法的嵌套。 (1) for() { ... while() {...} ... } (2) do{ ... for() {...} ... }while(); (3) while() { ... for() {...} ... } 本程序主要思路是:先使用 while 循环语句控制输入密码的过程,如果 3 次输入错误,则给 出提示信息并退出程序。密码通过后,使用 while 语句控制程序流程,如果输入的数值不等于程 序给定的值,则程序一直循环运行下去,直到猜中给定的值。在这层 while 内部又用 do-while 语 句控制输入值的范围,如果输入值不在 1 和 100 之间,就要求重新输入;然后通过 if…else 语句 判断输入值的范围,并给出相应的提示信息,直到猜中给定值,程序结束。 程序代码 【程序 9】 猜数字游戏 /* 猜数字游戏 */ 第一部分 基础篇 X 2211 #include #include void main() { int Password=0,Number=0,price=58,i=0; clrscr();/* 清屏 */ printf("\n====This is a Number Guess Game!====\n"); /* 提示信息 */ while( Password != 1234 ) /* 当输入密码错误时 */ { if( i >= 3 ) /* 如果输入错误次数大于 3 就退出 */ { printf("\n Please input the right password!\n "); return; } i++; puts("Please input Password: "); scanf("%d",&Password); /* 要求重新输入密码 */ } i=0; while( Number!=price ) { do{ puts("Please input a number between 1 and 100: ");/* 提示猜数 */ scanf("%d",&Number); printf("Your input number is %d\n",Number); }while( !(Number>=1 && Number<=100) );/* 判断范围是否正确 */ if( Number >= 90 )/* 输入大于 90 的情况 */ { printf("Too Bigger! Press any key to try again!\n"); } else if( Number >= 70 && Number < 90 ) /* 比较大的情况 */ { printf("Bigger!\n"); } else if( Number >= 1 && Number <= 30 ) /* 太小的情况 */ { printf("Too Small! Press any key to try again!\n"); } else if( Number > 30 && Number <= 50 ) /* 比较小的情况 */ { printf("Small! Press any key to try again!\n"); } else { if( Number == price ) { printf("OK! You are right! Bye Bye!\n"); } else if( Number < price ) /* 相差不多的情况 */ { printf("Sorry,Only a little smaller! Press any key to try again!\n"); } 2222 W C 语言实例解析精粹 else if( Number > price ) printf(" Sorry, Only a little bigger! Press any key to try again!\n"); } getch(); } } 归纳注释 常见的循环应用有①计数循环。②输入验证循环:为了避免操作员按键错误,程序包含输入 验证循环是非常必须的,比如本例中的密码验证。③哨兵循环:循环程序不停地读、检查和处理 数据,直到遇到事先指定的表示结束的值,循环才终止。此值即为“哨兵值”,它控制着循环结束。 ④延时循环:循环中不实现任何功能,只是使 CPU 等待一定的时间后,再继续执行程序,即实现 计时器的功能。⑤查找循环:按给定的对象进行查找。⑥无限循环:有时程序需要永不停息地执 行下去,例如危险信号的监测中经常用到。 实例 10 模拟 ATM(自动柜员机)界面 实例说明 通过对自动柜员机(ATM)界面的模拟来学习 switch 语句的用法。程序运行结果如图 10-1~ 图 10-7 所示。 图 10-1 ATM 界面模拟程序运行主界面 图 10-2 在主界面中选【1. Quary】后的查询结果界面 图 10-3 在主界面中选【2. Credit】后的存款界面 第一部分 基础篇 X 2233 图 10-4 在存款界面中选【2. $100】后的提示 图 10-5 在主界面中选【3. Debit】后的取款界面 图 10-6 在取款界面中选【3. $500】后的提示 图 10-7 在主界面中选择【4. Return】后的提示 实例解析 switch 语句 switch 的中文意思是开关,因此 switch 语句又叫开关语句。switch 语句是专门用来处理多分 支的,例如,学生成绩分类(90 分以上为‘A’等;80~90 分为‘B’等;70~79 分为‘C’等); 人口统计分类(按年龄分为老、中、青、少、儿童);工资统计分类;银行存款分类等。当然这些 都可以用嵌套的 if 语句,也就是第 3 种形式的 if 语句来处理,但如果分支较多,就显得嵌套的层 数太多了,程序冗长且可读性降低。switch 语句就是专门为了解决多分支的问题而设计的。 switch 语句的一般形式如下: switch(变量或表达式) { case 常量表达式 1: 语句 1 case 常量表达式 2: 语句 2 . . case 常量表达式 n: 语句 n default: 2244 W C 语言实例解析精粹 语句 n+1 } 其中,default 是可有可无的。 switch 语句的执行过程是这样的:计算表达式的值,判断变量或表达式的值是否等于常量表 达式 1,如果相等,则执行语句 1,如果不等,则判断是否等于常量表达式 2,如果相等,则执行 语句 2,……依此类推,直到结束。如果没有相等的则执行 default 后面的语句 n+1(如果有的话)。 在语句 1~n 执行的过程中,若遇到 break,则直接跳出 switch,不再对下面的 case 语句进行判断。 本程序就是利用 switch 语句对各种选择进行判断和相应处理。本实例是自动柜员机功能菜单 的线性模拟程序。如果把程序中的各 case 语句的 puts 语句换成实际的 ATM 中的各功能子程序(函 数),并在主程序外加上各个子程序(函数),即可成为一个完整的 ATM 程序。 程序代码 【程序 10】 模拟 ATM 界面 //本实例源码参见光盘 归纳注释 本实例程序在每个 do-while 语句中都控制了输入的有效数值,保证 switch 语句判断时,变量 所包含的值不超出所有 case 的情况,因此没有用 default 语句。 本程序中大量用到 puts 语句,用于将一个字符串输出在屏幕上。也可以用 printf 语句实现。 本程序中的界面采用“=”、“|”来做成方框的形式,以增加美观的效果。也可以采用“*”、 “$”等其他符号来实现。 实例 11 用一维数组统计学生成绩 实例说明 记录并统计一个班级学生的成绩来学习一维数组的使用。运行本实例程序,可以对用户输入 的学生成绩进行统计,并输出统计结果。程序运行结果如图 11-1 所示。 图 11-1 实例 11 程序运行结果 第一部分 基础篇 X 2255 实例解析 一维数组的定义 一维数组的定义方式为: 类型说明符 数组名[常量表达式]; 例如: int a[10]; float b[15]; char c[20]; 其中,a[10]表示数组名为 a,此数组有 10 个元素。 在定义一维数组的同时,需要注意以下几点。 (1)数组名定名规则和变量名相同,遵循标识符定名规则。 (2)数组名后是用方括弧括起来的常量表达式,不能使用圆括弧,下面的用法是不正确的: int a(10); (3)常量表达式表示元素的个数,即数组长度。例如,a[10]中的 10 表示 a 数组中有 10 个元素, 下标从 0 开始,这 10 个元素分别是:a[0],a[1],a[2],a[3],a[4],a[5],a[6],a[7],a[8],a[9]。 (4)常量表达式中可以包括常量和符号常量,不能包括变量。也就是说,不允许对数组的大 小进行动态定义,即数组的大小不依赖于程序运行过程中变量的值。例如,下面的定义方法就是 不可以的: int num; scanf(“%d”,&num); int a[num]; …… 一维数组的初始化 可以用赋值语句或输入语句给数组中的元素赋值,但是这样做占用不少运行时间。可以使数 组在程序运行之前初始化,即在编译阶段得到初值。 对数组元素的初始化可以用以下方法实现。 (1)在定义数组时对数组元素赋值。例如: static int a[10]={0,1,2,3,4,5,6,7,8,9}; 将数组元素的初始值依次放在一对大括弧中。 经过上面的定义和初始化之后,a[0]=0,a[1]=1,a[2]=2,a[3]=3,a[4]=4,a[5]=5,a[6]=6, a[7]=7,a[8]=8,a[9]=9。 (2)也可以只给一部分元素赋值。例如: static int a[10]={0,1,2,3,4}; 定义 a 数组有 10 个元素,但是大括弧内只提供了 5 个初始值,这表示只给前面 5 个元素赋初 值,后 5 个元素的值默认都为 0。 (3)如果要使数组中全部元素值为 0,可以写成: static int a[10]={0,0,0,0,0,0,0,0,0,0}; 但不能写成: static int a[10]={0*10}; 这是与 FORTRAN 语言不同的。 (4)在对全部数组元素赋初值时,可以不指定数组长度。例如: static int a[5]={1,2,3,4,5}; 2266 W C 语言实例解析精粹 可以写成: static int a[]={1,2,3,4,5}; 在第 2 种写法中,大括弧中有 5 个数,系统会据此自动定义 a 数组的长度为 5。但若被定义 的数组长度与提供初值的个数不相同,则数组长度不能省略。例如,若定义数组长度为 10,就不 能省略数组长度的定义,而必须写为: static int a[10]={1,2,3,4,5}; 只初始化前 5 个元素,后 5 个元素为 0。 本实例利用一维数组存储班级学生的学号,语文、数学、外语等各科成绩,并求出各学生的 平均分。程序首先要求输入班级人数(最大为 50,可以在程序中修改该最大值),然后要求输入 每个学生的学号及各门成绩,并存入相应的一维数组中,最后计算每个学生的平均成绩及全班平 均成绩,并输出结果。 程序代码 【程序 11】 用一维数组统计学生成绩 //本实例源码参见光盘 归纳注释 本实例中,一维数组的操作基本依靠循环语句来实现,用得最多的是 for 循环。例如,可以 用 for 循环对一维数组初始化,利用 for 循环对一维数组的值进行运算等。在循环的基础上,可以 进行查找、插入、删除等操作。 实例 12 用二维数组实现矩阵转置 实例说明 本实例将输入的 3×4 矩阵转置为 4×3 矩阵,并输出结果。通过本实例,可以学习如何使用二 维数组。程序运行结果如图 12-1 所示。 图 12-1 实例 12 程序运行结果 实例解析 二维数组的定义 二维数组定义的一般形式为: 第一部分 基础篇 X 2277 类型说明符 数组名[常量表达式][常量表达式] 例如: int a[3][4],b[7][8]; 定义 a 为 3×4(3 行 4 列)的数组,b 为 7×8(7 行 8 列)的数组。 二维数组的引用 二维数组的元素也称为双下标变量,二维数组的元素的表示形式为: 数组名[下标][下标] 例如 a[3][4],下标可以是整型常量或是整型表达式,如 a[2*2-1][3+1]。特别强调不要写成: a[3,4]或者 a[2*2-1,3+1]的形式。 数组元素可以出现在表达式中,也可以被赋值,例如: b[1][2]=a[2][3]/3; 在使用数组元素时,应该注意下标值应在已定义的数组大小范围内。定义 a 为 3×4 的数组, 它可用的行下标值最大为 2,列坐标值最大为 3。用 a[3][4]则超过了数组的定义范围。 下标变量和数组说明在形式中有些相似,但两者具有完全不同的含义。数组说明的方括号中 给出的是某一维的长度,即可取下标的最大值;而数组元素中的下标是该元素在数组中的位置标 识。前者只能是常量,后者可以是常量、变量或表达式。 二维数组的初始化 可以用下面的方法对二维数组初始化。 (1)分行给二维数组赋初值。例如: static int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}}; 这种赋初值方法比较直观,把第一个大括弧内的数据赋给第一行的元素,第二个大括弧内的 数据赋给第二行的元素……。 (2)可以将所有数据写在一个大括弧中,按数组排列的顺序对各元素赋初值。例如: static int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12}; 效果与前种方法相同。但第一种方法比较好,一行对一行。用第二种方法,如果数据多,写 出来一大片,就比较容易遗漏,有错误也不容易检查出来。 (3)可以对部分元素赋初值,例如: static int a[3][4]={{1},{3},{5}}; 它的作用是只对各行第一列的元素赋初值,其余元素值自动设为 0。赋初值后数组各元素为: ⎟ ⎟ ⎟ ⎠ ⎞ ⎜ ⎜ ⎜ ⎝ ⎛ 0005 0003 0001 也可以对各行中的某一元素赋初值: static int a[3][4]={{1},{0,3},{0,0,5}}; 初始化后的元素如下: ⎟ ⎟ ⎟ ⎠ ⎞ ⎜ ⎜ ⎜ ⎝ ⎛ 0500 0030 0001 这种方法对非 0 元素少时比较方便,不必将所有的 0 都写出来,只需输入少量数据。 也可以只对某几行元素赋初值,例如: static int a[3][4]={{1},{3,5}}; 数组元素为: 2288 W C 语言实例解析精粹 ⎟ ⎟ ⎟ ⎠ ⎞ ⎜ ⎜ ⎜ ⎝ ⎛ 0000 0053 0001 以上对第 3 行不赋初值。也可以对第 2 行不赋初值,例如: static int a[3][4]={{1},{},{3}}; (4)如果对全部元素都赋初值(即提供全部初始数据),则定义数组时对第一维的长度可以不 指定,但第二维的长度不能省。如: static int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12}; 与下面的定义等价: static int a[][4]={1,2,3,4,5,6,7,8,9,10,11,12}; 系统会根据数据总个数分配存储空间,一共 12 个数据,每行 4 列,当然可确定为 3 行。 在定义时也可以只对部分元素赋初值而省略第一维的长度,但应当分行赋初值。如: static int a[][4]={{0,0,3},{},{0,10}}; 这样的写法,能通知编译系统数组共有 3 行。数组各元素为: ⎟ ⎟ ⎟ ⎠ ⎞ ⎜ ⎜ ⎜ ⎝ ⎛ 00100 0000 0300 本实例先输出提示信息,要求输入矩阵 A 的值,接着通过两个 for 嵌套来访问二维数组 A, 并将其转置存入数组 B,最后输出矩阵 B 的值。 程序代码 【程序 12】 用二维数组实现矩阵转置 /* 用二维数组实现矩阵的转置 */ #include #define ROW 3 /* 矩阵的行数 */ #define COL 4 /* 矩阵的列数 */ main() { int matrixA[ROW][COL],matrixB[COL][ROW];/* 矩阵的定义 */ int i,j; clrscr(); /* 清屏 */ printf("Enter elements of the matrixA,"); /* 提示信息 */ printf("%d*%d:\n",ROW,COL); for( i=0; imax)/*保存至 row 行的最小数*/ min=max; } printf("The minimum of maximum numbers is %d\n",min) ; } 从每行选出最小数,再从选出的 n 个小数中选出最大数,有如下的算法: [算法 2]在二维整数数组中,从每行选出最小数,再从选出的小数中选出最大数 {/*设最大元变量用 max 标记,各行的最小元变量用 min 标记*/ for(max=a[0][0],row=0;rowa[row][col]) min=a[row][col]; if(maxmax)/* 保存至 row 行的最小数 */ min=max; 第一部分 基础篇 X 3311 } printf("The minimum of maximum number is %d\n",min); for(max=a[0][0],row=0;rowa[row][col]) min=a[row][col]; if(maxm”成立。代码描述如下: k=0; while(primes[k]*primes[k]<=m) if(m%prime[k]==0) { /*m 是合数*/ m+=2;/*让 m 取下一个奇数*/ k=1;/*不必用 primes[0]=2 去测试 m*/ } else k++;/*继续用下一个质数去测试*/ 程序代码 【程序 14】 利用数组求前 n 个质数 #define N 50 main() { int primes[N]; int pc,m,k; clrscr(); printf("\n The first %d prime numbers are:\n",N); primes[0]=2;/*2 是第一个质数*/ pc=1;/*已有第一个质数*/ m =3;/*被测试的数从 3 开始*/ while(pc0) { a[i]与 a[high]交换; high--; } else if(a[i]==0) 略过该元素; else { a[i]与 a[low]交换,然后 low 增 1, i 增 1; } } 程序代码 【程序 16】 对数组元素排序 rest(int a[], int n) { int i,low,high,t; for(i=0,low=0,high=n-1;i<=high;) { 第一部分 基础篇 X 3377 if(a[i]>0) { /*a[i]与 a[high]交换,随之 high 减 1*/ t=a[i]; a[i]=a[high]; a[high]=t; high--; } else if(a[i]==0) i++; /* 略过该元素 */ else { /*a[i]与 a[low]交换,随之 low 增 1, i 增 1*/ t=a[i]; a[i]=a[low]; a[low]=t; low++; i++; } } } int s[]={8,4,0,-1,6,0,-5}; main() /* 主函数用于测试 rest 函数 */ { int i; clrscr(); /* 清屏 */ printf("\n The arry before rest is:\n"); for(i=0;i16) { s[0]='\0'; /* 不合理的进制,置 s 为空字符串 */ return 0; /* 不合理的进制,函数返回 0 */ } buf[i]='\0'; do { buf[--i]=digits[n%d]; /*得出最低位,将对应字符存入对应数组中*/ n/=d; }while(n); /* 将工作数组中的字符串复制到 s */ for(j=0;(s[j]=buf[i])!='\0';j++,i++); 第一部分 基础篇 X 3399 /* 其中控制条件可简写成 s[j]=buf[i] */ return j; } /* 主函数用于测试函数 trans() */ main() { unsigned int num = 0; int scale[]={2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,1}; char str[33]; int i; clrscr(); puts("Please input a number to translate:"); scanf("%d",&num); printf("The number you input is %d.\nThe translation results are:\n",num); for(i=0;i (%d) Error! \n",num,scale[i]); } printf("\n Press any key to quit...\n"); getch(); } 归纳注释 本实例将 0~F 等 16 个字符存储在静态字符数组里,通过该静态字符数组的引用来实现任意 进制数的转换。 实例 18 判断回文数 实例说明 判定正整数 n 的 d 进制表示形式是否是回文数。程序运行结果如图 18-1 所示。 图 18-1 实例 18 程序运行结果 4400 W C 语言实例解析精粹 实例解析 回文数就是顺着看和倒着看相同的数。例如,n=232,它的十进制表示是回文数;n=27,它 的二进制表示 11011(2)是回文数;n=851,它的 16 进制表示 353(16)是回文数。 设判断是否为回文数的函数为 circle(int n, ind d),它有两个参数 n 和 d。其中 n 为要判定是否 为回文数的整数,d 指将 n 转成 d 进制表示。为判定 n 是否是 d 进制表示形式中的回文数,有两 种办法:一是顺序译出 n 的 d 进制表示的各位数字,然后首末对应位数字两两比较,若对应位都 相同,则 n 是 d 进制表示形式中的回文数,否则 n 不是回文数;二是首先顺序译出 n 的 d 进制的 各位数字,然后把译出的各位数字将低位当高位按 d 进制的转换方法译成一个整数,若 n 是回文 数,则转换所得整数应与 n 相等;否则,n 不是回文数。下面的函数是参照方法二编写的,参照 方法一编写相应函数的工作留给读者完成。 程序代码 【程序 18】 判断回文数 /* 函数 circle 用于判断正整数 n 的 d 进制数表示形式是否是回文数 */ int circle(int n, int d) { int s=0,m=n; while(m) { s=s*d+m%d; m/=d; } return s==n; } /* main 函数用于测试 circle 函数 */ int num[]={232,27,851}; int scale[]={2,10,16}; main() { int i,j; clrscr(); for(i=0;i (%d) is a Circle Number!\n",num[i],scale[j]); else printf("%d -> (%d) is not a Circle Number!\n",num[i],scale[j]); printf("\n Press any key to quit...\n"); getch(); } 归纳注释 本实例中主函数中的待判断的数,也可以由用户从键盘输入,并要求输入相应的所要判断的 d 进制数。 第一部分 基础篇 X 4411 实例 19 求数组前 n 元素之和 实例说明 编制递归函数来求取数组的前 n 个元素的和,从而求取该数组的所有元素的和。程序运行结 果如图 19-1 所示。 图 19-1 实例 19 程序运行结果 实例解析 编制一个递归函数 sum(int a[ ],int n),求已知数组 a[ ]的前 n 个元素的和。数组 a[ ]的前 n 个 元素的和为 sum=a[n-1]+.....+a[0]。 数组 a[ ]的前 n 个元素之和是 a[n-1]加上数组 a[ ]的前 n-1 个元素的和,即当 n>0 时, sum(a,n)=a[n-1]+sum(a,n−1),当 n=0 时,sum(a,0)=0。 程序代码 【程序 19】 求数组前 n 个元素之和 int a[]={1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}; main() { int i; clrscr(); printf("\n The arry is:\n"); for(i=0;i #define N 20 #define DELTA 2 int bestlen; int bestsele[N]; int sele[N]; int n; int orderlen[N]; int total; main() { int i; clrscr(); printf("\n Please enter total length of the steel:\n");/* 输入钢材总长 */ scanf("%d",&total); printf("\n Please enter number of order:\n"); /* 输入订单数 */ scanf("%d",&n); printf("\n Please enter the orders:\n"); /* 输入各订单 */ for(i=0;i*yp) /* 通过指向变量的指针引用变量的值 */ { 4488 W C 语言实例解析精粹 t=*xp; /* 通过指向变量的指针引用变量的值 */ *xp=*yp;/* 通过指向变量 x 的指针 xp,引用变量 x 的值 */ *yp=t; /* 通过指向变量 y 的指针 yp,引用变量 y 的值 */ } if(*xp>*zp) /* 通过指向变量的指针,引用变量的值 */ { t=*xp; /* 通过指向变量 x 的指针 xp,引用变量 x 的值 */ *xp=*zp;/* 通过指向变量 x 的指针 xp,引用变量 x 的值 */ *zp=t; /* 通过指向变量 z 的指针 zp,引用变量 z 的值 */ } if(*yp>*zp) /* 通过指向变量的指针,引用变量的值 */ { t=*yp; /* 通过指向变量的指针,引用变量的值 */ *yp=*zp;/* 通过指向变量 y 的指针 yp,引用变量 y 的值 */ *zp=t;/* 通过指向变量 z 的指针 zp,引用变量 z 的值 */ } printf("x = %d\ty = %d\tz = %d\n",x,y,z); printf("\nPress any key to quit...\n"); getch(); } 归纳注释 本实例说明如何用指针实现数的大小比较。设 3 个变量分别为 x,y,z,与它们对应的 3 个指针 变量为 xp,yp,zp。两变量 x 和 y 之间的比较“x>y”,利用指向它们的指针 xp 和 yp 可以写成“*xp>*yp”。 与指针有关的类型说明如下所示(以整型为例)。 int i; /* 定义整型变量 i */ int *p; /* p 为指向整型数据的指针变量 */ int a[n]; /* 定义整型数组 a,它有 n 个元素 */ int *p[n]; /* 定义指针数组 p,它由 n 个指向整型数据的指针元素组成 */ int (*p)[n]; /* p 为指向含 n 个元素的一维数组的指针变量 */ int f(); /* f 为带回整型函数值的函数 */ int *p(); /* p 为带回一个指针的函数,该指针指向整型数据 */ int (*p)(); /* p 为指向函数的指针,该函数返回一个整型值 */ int **p; /* p 是一个指针变量,它指向一个指向整型数据的指针变量 */ 实例 22 指向数组的指针 实例说明 本实例实现通过指向数组的指针引用数组、利用数组名及下标引用数组等。 程序运行结果如图 22-1 所示。 第一部分 基础篇 X 4499 图 22-1 程序运行结果 实例解析 程序可预定义一个初始化的数组,然后用数组名和下标遍历数组。程序另定义一个指针变量, 让指针顺序指向数组的各元素,实现遍历数组;指针与游标变量结合,让指针指向数组中的某元 素,不改动指针,顺序改变游标变量也能遍历数组。 程序先输出提示说明信息,接着依次输出各个类型变量所占的字节数。 程序代码 【程序 22】 指向数组的指针 int a[ ]={1,2,3,4,5}; #define N sizeof a/sizeof a[0] main() { int j, /*游标变量*/ *p; /*指针变量*/ clrscr(); for(j=0;j=0;j--) printf("p[-%d]\t= %d\t",j,p[-j]); printf("\nPress any key to quit...\n"); getch(); } 归纳注释 指向数组的指针变量称为数组指针变量。一个数组是由连续的一块内存单元组成的。数组名 就是这块连续内存单元的首地址。一个数组也是由各个数组元素(下标变量)组成的。每个数组 元素按其类型不同占有不同连续的内存单元。一个数组元素的首地址是指它所占有的几个内存单 元的首地址。一个指针变量既可以指向一个数组,也可以指向一个数组元素,可把数组名或第一 5500 W C 语言实例解析精粹 个元素的地址赋予它。 若要使指针变量指向第 i 号元素,可以把 i 元素的首地址赋予它,或把数组名加 i 赋予它。设 有实数组 a,指向 a 的指针变量为 pa,则有以下关系:pa、a、&a[0]均指向同一单元,它们是数 组 a 的首地址,也是 0 号元素 a[0]的首地址。pa+1、a+1、&a[1]均指向 1 号元素 a[1]。类推可知 a+i、a+i、&a[i]指向 i 号元素 a[i]。应该说明的是 pa 是变量,而 a,&a[i]都是常量。 数组指针变量说明的一般形式为:类型说明符 * 指针变量名。其中类型说明符表示所指数组 的类型。从一般形式可以看出指向数组的指针变量和指向普通变量的指针变量的说明是相同的。 引入指针变量后,就可以用两种方法来访问数组元素了。第一种方法为下标法,即用 a[i]形式访 问数组元素。第二种方法为指针法,即采用*(pa+i)形式,用间接访问的方法来访问数组元素。 实例 23 寻找指定元素的指针 实例说明 在已知数表中找出第一个与指定值相等的元素的下标和指针。 程序运行结果如图 23-1 所示。 图 23-1 程序运行结果 实例解析 设要编制的函数为“int search(int *apt,int n,int key)”,其 中 apt 为给定的数表的首元素的指针; n 为数表中的元素个数;key 为要寻找的元素的值。函数返回的整型值为找到的元素在已知数表中 的下标,如数表中没有值为 key 的元素,则函数返回−1 值。 [函数 1]在已知数表中找第一个与指定值相等的元素 int search(int *apt,/*已知数表的首元指针*/ int n,/*数表中元素个数*/ int key)/*要寻找的值*/ { int *p; for(p=apt;p int search(int *apt,/*已知数表的首元指针*/ int n,/*数表中元素个数*/ int key)/*要寻找的值*/ { int *p; for(p=apt;p*cb)/*数表 1 的当前元素>数表 2 的当前元素*/ cb++;/*调整数表 2 的当前元素指针*/ else /*数表 1 的当前元素==数表 2 的当前元素*/ /*在前两个数表中找到相等元素*/ return ca;/*返回在这两个数表中找到相等元素*/ } return NULL; } main( )/*只是为了引用函数 search2( )*/ { int *vp,i; int a[ ]={1,3,5,7,9,13,15,27,29,37}; int b[ ]={2,4,6,8,10,13,14,27,29,37}; clrscr(); puts("The elements of array a is:"); for(i=0;i #define ROWS 4 #define COLS 4 第一部分 基础篇 X 5555 int nums[ROWS][COLS]={{1000,1000,1000,1000}, {900,500,400,100}, {90,50,40,10}, {9,5,4,1}}; char *roms[ROWS][COLS]={{"m","m","m","m"}, {"cm","d","cd","c"}, {"xc","l","xl","x"}, {"ix","v","iv","i"}}; main(int argc,char *argv[ ]) { int low,high; char roman[25]; if(argc<2) { printf("Usage:roman decimal_number\n");/*运行程序需带整数参数*/ exit(0); } high=low=atoi(argv[1]);/*将第一个参数转换成整数*/ checknum(low); if(argc>2) {/*带两个参数*/ high=atoi(argv[2]); checknum(high); if(low>high) { low=high; high=atoi(argv[1]); } } else low=1; for(;low<=high;low++) { to_roman(low,roman); printf("%d\t%s\n",low,roman); } } checknum(int val)/*检查参数合理性*/ { if(val<1||val>9999) { printf("The number must be in range 1..9999.\n"); exit(0); } } to_roman(int decimal,char roman[ ])/*将整数转换成罗马数字表示*/ { int power,index; roman[0]='\0'; for(power=0;power=nums[power][index]) { strcat(roman,roms[power][index]); decimal-=nums[power][index]; 5566 W C 语言实例解析精粹 } } 归纳注释 程序使用带命令行参数的 main 函数,将 1~n 的所有数都转换为罗马数字并输出。修改某些 语句,也可以实现仅转换一个整数的功能。 实例 26 字符替换 实例说明 编制一个字符替换函数 rep(char *s,char *s1,char *s2),实现将已知字符串 s 中所有属于字符串 s1 中的字符都用字符串 s2 中的对应字符代替。程序运行结果如图 26-1 所示。 图 26-1 实例 26 程序运行结果 实例解析 例如设字符串 s,s1,s2 分别为如下所示。 char s[ ]"= "ABCABC",s1[ ]= "AC",s2[ ]= "xy"; 则调用函数 rep(s,s1,s2)将使字符串 s 的内容变为"xByxBy"。 为了实现上述要求,可用一个循环顺序访问字符串 s 中的字符,并检查该字符是否在字符串 s1 中出现,若不在字符串 s1 中出现,则略过该字符;若在字符串 s1 中出现,则用字符串 s2 中的 对应字符代替 s 中的字符。 程序代码 【程序 26】 字符替换 #include #define MAX 50 /* 函数 rep 实现对 s 中出现的 s1 中的字符替换为 s2 中相应的字符 */ rep(char *s,char *s1,char *s2) { char *p; for(;*s;s++)/*顺序访问字符串 s 中的每个字符*/ 第一部分 基础篇 X 5577 { for(p=s1;*p&&*p!=*s;p++);/*检查当前字符是否在字符串 s1 中出现*/ if(*p)*s=*(p-s1+s2);/*当前字符在字符串 s1 中出现,用字符串 s2 中的对应字符代替 s 中 的字符*/ } } main( )/*示意程序*/ { char s[MAX];/*="ABCABC";*/ char s1[MAX],s2[MAX]; clrscr(); puts("Please input the string for s:"); scanf("%s",s); puts("Please input the string for s1:"); scanf("%s",s1); puts("Please input the string for s2:"); scanf("%s",s2); rep(s,s1,s2); puts("The string of s after displace is:"); printf("%s\n",s); puts("\n Press any key to quit..."); getch(); } 归纳注释 查找替换是许多文字处理软件,比如 word 必备的基本功能,本例程序实现了较为复杂的替 换功能,其基本原理是相同的。 实例 27 从键盘读入实数 实例说明 编制一个从键盘读入实数的函数 readreal(double *rp)。函数将读入的实数字符列转换成实数 后,利用指数参数 rp,将实数存于指针所指向的变量*rp。程序运行结果如图 27-1 所示。 图 27-1 实例 27 程序运行结果 5588 W C 语言实例解析精粹 实例解析 函数在返回之前,将最后读入的结束实数字符列的字符返还给系统,以便随后读字符时能 再次读入该字符。函数若能正常读入实数,函数返回整数 1;如果函数在读入过程中,未遇数 字符之前,遇到不能构成数字的情况,函数返回−1,表示未读到实数。 在输入实数时,在实数之前可以有个数不定的空白类字符,组成实数的字符列有数的符号字 符、实数的整数部分、小数点和实数的小数部分,其中某些部分可以缺省。设实数字符列有以下 几种可能形式: 数符 整数部分 数符 整数部分. 数符 整数部分.小数部分 数符 .小数部分 其中数符或为空,或为字符‘+’,或为字符‘−’,分别表示不带符号、带正号和带负号。整 数部分和小数部分至少要有一个数字符组成。 上述实数形式说明,在实数转换过程中,同一字符在不同情况下会有不同的意义。为标记当 前实数转换的不同情况,程序引入状态变量,由状态变量的不同值表示当前实数转换过程中的不 同情况。 共有以下多种不同情况:正准备开始转换、转换了数的符号字符、正在转换实数的整数部 分、正在转换实数的小数部分、发现输入错误、转换正常结束。设状态变量为 0 表示正准备开 始转换,还未遇到任何与实数有关的字符;状态变量为 1 表示已遇数的符号字符;状态变量为 2 表示正在转换实数的整数部分;状态变量为 3 表示在未遇数字字符之前先遇小数点,必须要 有小数部分;状态变量为 4 表示在转换整数部分之后遇小数点,这种情况可以没有小数部分; 状态变量为 5(ERR)表示转换发现错误;状态变量为 6(OK)表示转换正常结束。程序将输入字符 分成数的符号字符、数字符、小数点、其他字符等几类,各状态遇各类字符后,应变成的新状 态,如下所示。 数符 数字符 小数点 其他字符 0 1 2 3 ERR 1 ERR 2 3 ERR 2 OK 2 4 OK 3 ERR 4 ERR ERR 4 OK 4 OK OK 下面的程序将处理数的符号、转换整数部分、转换小数部分等工作都编写成函数,它们在读 入实数函数的控制下工作。读实数函数另有两张表:一是转换函数表,二是状态表。函数反复读 入字符,将字符分类,根据当前状态和当前字符类调用对应转换函数,根据当前状态和当前字符 类,从状态表取出新的状态。当新状态为 ERR 或 OK 时,转换结束,前者表示发现错误,后者表 示正确转换了一个实数。 程序代码 【程序 27】 从键盘读入实数 //本实例源码参见光盘 第一部分 基础篇 X 5599 归纳注释 本实例中的 readreal 函数采用指针作为函数的参数,对变量作实参时,与普通变量一样,也 是“值传递”。即将指针变量的值(一个地址)传递给被调用函数的形参(必须是一个指针变量)。 被调用函数不能改变实参指针变量的值,但可以改变实参指针变量所指向的变量的值。因此,为 了利用被调用函数改变的变量值,应该使用指针(或指针变量)作为函数实参。其机制为在执行 被调用函数时,使形参指针变量所指向的变量的值发生变化;函数调用结束后,通过不变的实参 指针(或实参指针变量)将变化的值保留下来。 本程序实现的是从键盘读入实数的功能,做相应的改动后,也可以实现从键盘读入整数、字 符等功能。 实例 28 字符行排版 实例说明 将字符行内单字之间的的空白符平均分配插入到单字之间,以实现字符行排版。程序运行结 果如图 28-1 所示。 图 28-1 实例 28 程序运行结果 实例解析 首先要统计字符行内单字个数、字符行内的空白字符数。然后计算出单字之间应平均分配的 空白字符数,另约定多余的空白字符插在前面的单字间隔中,前面的每个间隔多一个空白符,插 完为止。然后,将字符行复制到一个工作字符数组中,顺序扫视工作字符数组,略过其中的空白 符,将连续的空白符复制到字符行,接着复制平均个数的空白符,如余额空白符还未用完,再复 制一个空白符。在上述复制过程中,当复制了最后一个单字后,排版结束,函数返回。另外,因 字符行单字个数小于 2 不需要排版,函数发现这种情况将立即返回。 程序代码 【程序 28】 字符行排版 //本实例源码参见光盘 6600 W C 语言实例解析精粹 归纳注释 本实例中用到的转换函数表是一个函数指针数组。该数组是二维数组,其元素是指向 sign、 integer、decimal 等函数的指针。这些函数的返回值都是 int 型的。 应该特别注意函数指针变量和指针型函数这两者在写法和意义上的区别。如 int(*p)()和 int *p() 是两个完全不同的量。int(*p)()是一个变量说明,说明 p 是一个指向函数入口的指针变量,该函 数的返回值是整型量,(*p)两边的括号不能少。int *p() 则不是变量说明而是函数说明,p 是一个 指针型函数,其返回值是一个指向整型量的指针,*p 两边没有括号。作为函数说明, 在括号内 最好写入形式参数,这样便于与变量说明区别。 实例 29 字符排列 实例说明 用已知字符串 s 中的字符,生成由其中 n 个字符组成的所有字符排列。设 n 小于字符串 s 的 字符个数,其中 s 中的字符在每个排列中最多出现一次。例如,对于 s[ ]=“abc”,n=2,则所有字 符排列有:ba,ca,ab,cb,ac,bc。程序运行结果如图 29-1 所示。 图 29-1 实例 29 程序运行结果 实例解析 可采用递归方法来生成用字符串 s 中的字符生成由 n 个字符组成的字符排列。设程序用字符 数组 w[ ]存储生成的字符排列。令递归函数为 perm(int n,char *s),其功能是用字符串 s 中的字符 生成由 n 个字符组成的所有排列。其方法是从字符串 s 依次选用其中的每个字符填写到字符排列 的第 n 个位置(w[n-1])中,然后通过递归调用生成由 n-1 个字符组成的排列。当一个字符被选 用后,进一步递归调用时可选用的字符中应不再包含该字符。另外,当发现一个字符排列生成后, 就不再递归调用,而是输出生成的字符列。综合以上的思路,函数 perm(int n, char *s)的算法如下。 [算法]函数 perm(int n, char *s)的算法 第一部分 基础篇 X 6611 { if(一个排列已经生成)printf("%s\n",w); else { 保存本层可使用的字符串; 依次选本层可用字符循环完成以下工作; { 将选用字符填入正在生成的字符排列中; 形成进一步递归可使用的字符串; 递归调用; } } } 程序代码 【程序 29】 字符排列 #define N 20 char w[N]; perm(int n, char *s) { char s1[N]; int i; if(n<1) printf("%s\n",w); /* 一个排列生成输出 */ else { strcpy(s1,s); /* 保存本层可使用的字符 */ for(i=0;*(s1+i);i++) /* 依次选本层可用字符 */ { *(w+n-1)=*(s1+i);/* 将选用字符填入正在生成的字符排列中 */ *(s1+i)=*s1; *s1=*(w+n-1); perm(n-1,s1+1); /* 递归 */ } } } main() { int n=2; char s[N]; w[n]='\0'; clrscr(); printf("This is a char permutation program!\nPlease input a string:\n"); scanf("%s",s); puts("\nPlease input the char number of permuted:\n"); scanf("%d",&n); puts("The permuted chars are:\n"); perm(n,s); puts("\nPress any key to quit..."); getch(); } 6622 W C 语言实例解析精粹 归纳注释 在本例子中,当递归调用生成 0 个字符的排列时,一个字符排列已经生成。为保存本层次可 使用的字符串,可引入一个字符数组。为存储进一步递归可使用的字符串,可做如下处理,因进 一步递归可使用的字符总比本层次可使用的字符少一个字符,可把本层次正在选用的字符与其字 符串中的首字符交换,而进一步递归可使用的字符串就是从次字符开始的字符串。 实例 30 判断字符串是否回文 实例说明 “回文”是指顺读和反读内容均相同的字符串,例如,“121”、“ABBA”、“X”等。本例将编 写函数判断字符串是否是回文。程序运行结果如图 30-1 所示。 图 30-1 实例 30 程序运行结果 实例解析 引入两个指针变量,开始时,两指针分别指向字符串的首末字符,当两指针所指字符相等时, 两指针分别向后和向前移一个字符位置,并继续比较,直至两指针相遇,说明该字符串是回文。 若比较过程中,发现两字符不相等,则可以判断该字符串不是回文。 程序代码 【程序 30】 判断字符串是否回文 #include #define MAX 50 int cycle(char *s) { char *h,*t; for(h=s,t=s+strlen(s)-1;t>h;h++,t--) 第一部分 基础篇 X 6633 if(*h!=*t) break; return t<=h; } main() { char s[MAX]; clrscr(); while(1) { puts("Please input the string you want to judge (input ^ to quit):"); scanf("%s",s); if(s[0]=='^') break; if(cycle(s)) printf(" %s is a cycle string.\n",s); else printf(" %s is not a cycle string.\n",s); } puts("\nThank you for your using,bye bye!\n"); } 归纳注释 主程序采用 while 循环,对每个输入的字符串进行判断,直到输入“^”为止(只要输入的字 符串第一个是“^”,不管后面是什么字符都退出程序。 实例 31 通讯录的输入输出 实例说明 编制一个包含姓名、地址、邮编和电话的通讯录输入和输出函数。程序运行结果如图 31-1 所示。 图 31-1 实例 31 程序运行结果 6644 W C 语言实例解析精粹 实例解析 在实际问题中,一组数据往往具有不同的数据类型。例如,在学生登记表中,姓名为字符 型,学号为整型或字符型,年龄为整型,性别为字符型,成绩为整型或实型。显然不能用一个 数组来存放这一组数据。因为数组中各元素的类型和长度都必须一致,以便于编译系统处理。 为了解决这个问题,C语言中给出了另一种构造数据类型——结构。它相当于其他高级语言中 的记录。 “结构”是一种构造类型,它是由若干“成员”组成的。每一个成员可以是一个基本数据类型 或者又是一个构造类型。结构是一种“构造”而成的数据类型,在说明和使用之前必须先定义它, 也就是构造它。如同在说明和调用函数之前要先定义函数一样。 结构的定义 定义一个结构的一般形式为: struct 结构名 { 成员表列 }; 成员表由若干个成员组成,每个成员都是该结构的一个组成部分。对每个成员也必须进行类 型说明,其形式为: 类型说明符 成员名; 成员名的命名应符合标识符的书写规定。例如: struct stu { int num; char name[20]; char sex; float score; }; 在这个结构定义中,结构名为 stu,该结构由 4 个成员组成。第一个成员为 num,整型变量; 第二个成员为 name,字符数组;第三个成员为 sex,字符变量;第四个成员为 score,实型变量。 应注意在括号后的分号是不可少的。结构定义之后,即可进行变量说明。凡说明为结构 stu 的变 量都由上述 4 个成员组成。由此可见,结构是一种复杂的数据类型,是数目固定,类型不同的若 干有序变量的集合。 结构类型变量的说明 说明结构变量有以下 3 种方法,以上面定义的 stu 为例。 先定义结构,再说明结构变量。例如: struct stu { int num; char name[20]; char sex; float score; }; struct stu boy1,boy2; 以上说明了两个变量 boy1 和 boy2 为 stu 结构类型。也可以用宏定义用一个符号常量来表示 第一部分 基础篇 X 6655 一个结构类型,例如: #define STU struct stu STU { int num; char name[20]; char sex; float score; }; STU boy1,boy2; 在定义结构类型的同时说明结构变量。例如: struct stu { int num; char name[20]; char sex; float score; }boy1,boy2; 直接说明结构变量。例如: struct { int num; char name[20]; char sex; float score; }boy1,boy2; 第 3 种方法与第 2 种方法的区别在于第 3 种方法中省去了结构名,而直接给出结构变量。3 种方法中说明的 boy1,boy2 变量都具有相同的结构。说明了 boy1,boy2 变量为 stu 类型后,即 可向这两个变量中的各个成员赋值。在上述 stu 结构定义中,所有的成员都是基本数据类型或数 组类型。成员也可以又是一个结构,即构成了嵌套的结构。 在 ANSI C 中除了允许具有相同类型的结构变量相互赋值以外,一般对结构变量的使用,包 括赋值、输入、输出、运算等都是通过结构变量的成员来实现的。 表示结构变量成员的一般形式是“结构变量名.成员名”例如“boy1.num”即第一个人的学号, “boy2.sex”即第二个人的性别。如果成员本身又是一个结构,则必须逐级找到最低级的成员才能 使用。例如“boy1.birthday.month”即第一个人出生的月份成员,可以在程序中单独使用,与普通 变量完全相同。结构变量的赋值就是给各成员赋值。 如果结构变量是全局变量或为静态变量,则可对它进行初始化赋值。对局部或自动结构变量 不能进行初始化赋值。 结构数组 数组的元素也可以是结构类型的。因此可以构成结构型数组。结构数组的每一个元素都是具 有相同结构类型的下标结构变量。在实际应用中,经常用结构数组来表示具有相同数据结构的一 个群体。如一个班的学生档案,一个车间职工的工资表等。 结构数组的定义方法和结构变量相似,只需说明它为数组类型即可。例如: struct stu { int num; char *name; 6666 W C 语言实例解析精粹 char sex; float score; }boy[5]; 以上定义了一个结构数组 boy1,共有 5 个元素,boy[0]~boy[4]。每个数组元素都具有 struct stu 的结构形式。 结构指针变量 当使用一个指针变量指向一个结构变量时,称之为结构指针变量。结构指针变量中的值是所 指向的结构变量的首地址。通过结构指针即可访问该结构变量,这与数组指针和函数指针的情况 是相同的。结构指针变量说明的一般形式为: struct 结构名*结构指针变量名 例如,要说明一个指向 stu 的指针变量 pstu,可写为: struct stu *pstu; 当然也可在定义 stu 结构时同时说明 pstu。与前面讨论的各类指针变量相同,结构指针变量 也必须要先赋值后才能使用。赋值是把结构变量的首地址赋予该指针变量,不能把结构名赋予该 指针变量。如果 boy 是被说明为 stu 类型的结构变量,则“pstu=&boy”是正确的,而“pstu=&stu” 是错误的。 结构名和结构变量是两个不同的概念,不能混淆。结构名只能表示一个结构形式,编译系统 并不对它分配内存空间。只有当某变量被说明为这种类型的结构时,才对该变量分配存储空间。 因此上面“&stu”写法是错误的,不可能去取一个结构名的首地址。有了结构指针变量,就能更 方便地访问结构变量的各个成员。 其访问的一般形式为: (*结构指针变量).成员名 或为: 结构指针变量->成员名 例如“(*pstu).num”或者“pstu->num”。 应该注意(*pstu)两侧的括号不可少,因为成员符“.”的优先级高于“*”。如去掉括号写作 *pstu.num 则等效于*(pstu.num),这样,意义就完全不对了。 结构变量.成员名 (*结构指针变量).成员名 结构指针变量->成员名 这 3 种用于表示结构成员的形式是完全等效的。结构指针变量可以指向一个结构数组, 这 时结构指针变量的值是整个结构数组的首地址。 结构指针变量也可指向结构数组的一个元素, 这时结构指针变量的值是该结构数组元素的首地址。设 ps 为指向结构数组的指针变量,则 ps 也指向该结构数组的 0 号元素,ps+1 指向 1 号元素,ps+i 则指向 i 号元素。 这与普通数组的情 况是一致的。 结构指针变量作为函数参数 在 ANSI C 标准中允许用结构变量作为函数参数进行整体传送。但是这种传送要将全部成员 逐个传送,特别是成员为数组时将会使传送的时间和空间开销很大,严重地降低了程序的效率。 因此最好的办法就是使用指针,即用指针变量作为函数参数进行传送。这时由实参传向形参的只 是地址,从而减少了时间和空间的开销。 在本例中,设一个通信录由以下几项数据信息构成: 数据项 类型 姓名 字符串 地址 字符串 第一部分 基础篇 X 6677 邮政编码 字符串 电话号码 字符串 本例要求为通信录数据定义类型和定义通信录变量,并写出输入一个通信录和输出一个通信 录的函数。 在 C 程序中,通信录数据可用一个结构类型来描述,设其中的姓名、地址、邮政编码和电话 号码都用字符数组来存储,各数组的长度分别为 20,40,10 和 10。通信录数据类型如下: #define NAMELEN 20 #define ADDRLEN 40 #define ZIPLEN 10 #define PHONLEN 10 struct addr { char name[NAMELEN];/*姓名*/ char address[ADDRLEN];/*地址*/ char zip[ZIPLEN];/*邮政编码*/ char phone[PHONLEN];/*电话号码*/ }; 因其中的姓名、地址对不同的人可能需要不同数量的字符,更好的方法是对其中的姓名和地 址用字符指针来表示,实际存储姓名和地址的存储空间可向系统申请得到。重新说明通信录数据 类型如下: #define ZIPLEN 10 #define PHONLEN 10 struct addr {char *name;/*姓名*/ char *address;/*地址*/ char zip[ZIPLEN];/*邮政编码*/ char phone[PHONLEN];/*电话号码*/ }; 相应地以下变量定义都是通信录变量的定义: struct addr col,cdm[100]; 其中 col 是单个通信录变量,cdm[]是一个通信录变量数组。 设输入通信录的函数以指向通信录变量的指针为参数,当正确输入一个通信录时,函数返回 1,不能正常输入时,函数返回 0,并采用交互式方式输入通信录的每一项数据。 程序代码 【程序 31】 通讯录的输入和输出 //本实例源码参见光盘 归纳注释 关于动态内存分配问题:在数组中,数组的长度是预先定义好的,在整个程序中固定不变。 C语言中不允许动态数组类型。例如“int n;scanf("%d",&n);int a[n];”用变量表示长度,这是错误 的。但是在实际的编程中,往往会发生这种情况,即所需的内存空间取决于实际输入的数据,而 无法预先确定。对于这种问题,用数组的办法很难解决。为了解决上述问题,C语言提供了一些 内存管理函数,这些内存管理函数可以按需要动态地分配内存空间,也可把不再使用的空间回收 待用,为有效地利用内存资源提供了手段。常用的内存管理函数有以下 3 个。 6688 W C 语言实例解析精粹 (1)分配内存空间函数 malloc 调用形式: (类型说明符*)malloc (size) 在内存的动态存储区中分配一块长度为“size”字节的连续区域。函数的返回值为该区域的 首地址。“类型说明符”表示把该区域用于何种数据类型。“(类型说明符*)”表示把返回值强制转 换为该类型指针。“size”是一个无符号数。例如“pc=(char *) malloc (100);”表示分配 100 个字节 的内存空间,并强制转换为字符数组类型,函数的返回值为指向该字符数组的指针,把该指针赋 予指针变量 pc。 (2)分配内存空间函数 calloc calloc 也用于分配内存空间。调用形式: (类型说明符*)calloc(n,size) 在内存动态存储区中分配 n 块长度为“size”字节的连续区域。函数的返回值为该区域的首 地址。“(类型说明符*)”用于强制类型转换。calloc 函数与 malloc 函数的区别仅在于一次可以 分配 n 块区域。例如“ps=(struet stu*) calloc(2,sizeof (struct stu));”其中的 sizeof(struct stu)是计算 stu 的结构长度。因此该语句的意思是按 stu 的长度分配 2 块连续区域,强制转换为 stu 类型,并 把其首地址赋予指针变量 ps。 (3)释放内存空间函数 free 调用形式: free(void*ptr); 释放 ptr 所指向的一块内存空间,ptr 是一个任意类型的指针变量,它指向被释放区域的首地 址。被释放区应是由 malloc 或 calloc 函数所分配的区域。 实例 32 扑克牌的结构表示 实例说明 使用“结构”定义一副扑克牌,并对变量赋值。程序运行结果如图 32-1 所示。 图 32-1 实例 32 程序运行结果 实例解析 扑克牌有 4 种花色:草花、方块、红心和黑桃,可将花色说明为枚举类型。扑克牌的类型为 结构类型,包含两个成分,分别存储牌的花色和牌的面值,其中面值为字符数组。 第一部分 基础篇 X 6699 程序代码 【程序 32】 扑克牌的结构表示 enum suits{CLUBS,DIAMONDS,HEARTS,SPADES}; struct card { enum suits suit; char value[3]; }; struct card deck[52]; char cardval[][3]={"A","2","3","4","5","6","7","8","9","10","J","Q","K"}; char suitsname[][9]={"CLUBS","DIAMONDS","HEARTS","SPADES"}; main() { int i,j; enum suits s; clrscr(); for(i=0;i<=12;i++) for(s=CLUBS;s<=SPADES;s++) { j=i*4+s; deck[j].suit=s; strcpy(deck[j].value,cardval[i]); } for(j=0;j<52;j++) printf("(%s%3s)%c",suitsname[deck[j].suit],deck[j].value,j%4==3?'\n':'\t'); puts("\nPress any key to quit..."); getch(); } 归纳注释 在实际问题中,有些变量的取值被限定在一个有限的范围内。例如,一个星期内只有七天, 一年只有十二个月,一个班每周有六门课程等等。如果把这些量说明为整型,字符型或其他类型 显然是不妥当的。为此,C语言提供了一种称为“枚举”的类型。应该说明的是,枚举类型是一 种基本数据类型,而不是一种构造类型,因为它不能再分解为任何基本类型。 实例 33 用“结构”统计学生成绩 实例说明 设学生信息包括学号、姓名和五门功课的成绩,要求编写输入输出学生信息的函数。在输入 一组学生信息后,以学生成绩的总分从高到低顺序输出学生信息。程序运行结果如图 33-1 所示。 7700 W C 语言实例解析精粹 图 33-1 实例 33 程序运行结果 实例解析 学生信息的学号用 10 个字符来表示;学生的姓名在学生结构里只存储姓名字符串的指针,实 际存储学生姓名的空间向系统申请;成绩用一个整数数组来存储。存储学生信息的变量的数据类 型说明如下: #define SCORES 5 #define NUMLEN 10 struct std_type { char no[NUMLEN];/*学号*/ char *name;/*名字字符串指针*/ char scores[SCORES];/*五门功课的成绩*/ }; 设输入学生信息的函数以存储学生信息的结构变量的指针为参数,当正确输入一个学生信息 时,函数返回 1,不能正常输入时,函数返回 0,并采用交互方式输入学生信息的每一项数据。而 输出学生信息的函数的参数也是指向存储学生信息的变量的指针,一个学生信息的 3 项数据分别 输出在 3 行上。 程序引入一个结构数组依次存储输入的学生信息,为了在一组学生信息排序时避免交换整个 学生结构,另外引入一个存储下标的数组。开始时,该数组依次存储各学生结构在结构数组中的 下标,当排序过程中要改变两个学生结构的顺序时,就改变对应下标的顺序。此外,为了避免反 复求学生总分,又开设一个数组存储各位学生的总分。 程序代码 【程序 33】 用“结构”统计学生成绩 #include #define N 200 #define SCORES 5 #define NUMLEN 10 struct std_type{ char no[NUMLEN];/*学号*/ char *name;/*名字字符串指针*/ int scores[SCORES];/*五门功课的成绩*/ }; 第一部分 基础篇 X 7711 struct std_type students[N]; int order[N]; int total[N]; /*[函数]输入一个学生信息函数*/ int readastu(struct std_type *spt) { int len,j; char buf[120];/*输入字符串的缓冲区*/ printf("\nNumber : ");/*输入学号*/ if(scanf("%s",buf)==1) strncpy(spt->no,buf,NUMLEN-1); else return 0;/*Ctrl+Z 结束输入*/ printf("Name : ");/*输入姓名*/ if(scanf("%s",buf)==1) { len=strlen(buf); spt->name=(char *)malloc(len+1);/*申请存储姓名的空间*/ strcpy(spt->name,buf); } else return 0;/*Ctrl+Z 结束输入*/ printf("Scores : ");/*输入成绩*/ for(j=0;jscores+j)!=1) break; if(j==0)/*一个成绩也未输入*/ { free(spt->name);/*释放存储姓名的空间*/ return 0; } for(;jscores[j]=0; return 1; } /*[函数]输出一个学生信息的函数*/ int writeastu(struct std_type *spt) { int i; printf("Number : %s\n",spt->no);/*输出学号*/ printf("Name : %s\n",spt->name);/*输出姓名*/ printf("Scores : ");/*输出成绩*/ for(i=0;iscores[i]); printf("\n\n"); } main() { int n,i,j,t; 7722 W C 语言实例解析精粹 clrscr(); for(n=0;readastu(students+n);n++); /*采用冒泡法对学生信息数组排序*/ for(i=0;i main() { char fname[80];/*存储文件名*/ FILE *rfp; long count;/*文件字符计数器*/ clrscr(); printf("Please input the file's name:\n"); scanf("%s",fname); if((rfp=fopen(fname,"r"))==NULL) { printf("Can't open file %s.\n",fname); exit(1); } count=0; while(fgetc(rfp)!=EOF) count++; fclose(rfp);/*关闭文件*/ printf("There are %ld characters in file %s.\n",count,fname); puts("\n Press any key to quit..."); getch(); } 归纳注释 文件的随机读写 实际问题中常要求读写文件中某一指定的部分。为了解决这个问题可移动文件内部的位置指 针到需要读写的位置,再进行读写,这种读写称为随机读写。实现随机读写的关键是要按要求移 动位置指针,这称为文件的定位。 文件定位的函数主要有两个,rewind 函数和 fseek 函数。rewind 函数调用形式为: rewind(文件指针); 它的功能是把文件内部的位置指针移到文件首。fseek 函数用来移动文件内部位置指针,其调 用形式为: fseek(文件指针,位移量,起始点); 其中“文件指针”指向被移动的文件。“位移量”表示移动的字节数,要求位移量是 long 型 数据,以便在文件长度大于 64KB 时不会出错。当用常量表示位移量时,要求加后缀“L”。“起始 点”表示从何处开始计算位移量,规定的起始点有 3 种:文件首、当前位置和文件尾。 起始点 表示符号 数字表示 ────────────────────────── 文件首 SEEK—SET 0 当前位置 SEEK—CUR 1 文件末尾 SEEK—END 2 8800 W C 语言实例解析精粹 例如“fseek(fp,100L,0);”的意义是把位置指针移到离文件首 100 个字节处。 还要说明的是 fseek 函数一般用于二进制文件。在文本文件中由于要进行转换,故往往计算 的位置会出现错误。文件的随机读写在移动位置指针之后,由于一般是读写一个数据据块,因此 常用 fread 和 fwrite 函数。 实例 37 同时显示两个文件的内容 实例说明 编制一个程序,实现将两个文件的内容同时显示在屏幕上,并且最左边的第 1 至第 30 列显示文 件 1 的内容;右边第 41 至第 70 列显示文件 2 的内容;第 75 列至第 76 列显示两文件该行字符数总和; 其余列显示空白符。另外,每输出 20 行内容后,另输出 2 行空行。程序运行结果如图 37-1 所示。 图 37-1 实例 37 程序运行结果 实例解析 根据要求,主函数的主要工作包括:读入两文件的名字和打开文件;通过循环,控制在两个 文件中有文件还未结束时顺序读入两文件当前行的内容并输出;最后关闭文件。 对于读文件并输出的循环,有几种不同情况需要考虑:在某文件已经结束的情况下应不再读 该文件,其对应输出栏的内容都用空白符代替;另外,当前行的内容也不可能总是只有 30 个字符, 为此不足部分也需要用空白符代替,超过时需分批读入,每批最多读 30 个字符。 程序引入函数 readline()完成从指定的文件中读出字符并显示输出,对文件的当前行函数一次调用 最多读出 30 个字符,函数返回实际读出字符并显示的字符数,以便让主函数计算填充空白符的个数 并组织输出。主函数在一个循环中顺序以第一和第二文件调用函数 readline()实现两文件对应行内容的 左、右栏输出。另外,因每输出 20 行后还要输出 2 行空行,程序引入输出行计数器和函数 linecount()。 主函数在输出一行后,就调用该函数。由它完成对输出行的计数和一页满后输出 2 行空行。 程序代码 【程序 37】 同时显示两个文件的内容 //本实例源码参见光盘 第一部分 基础篇 X 8811 归纳注释 C语言中把文件当作一个“流”,按字节进行处理。本实例将两个文件格式化输出,相当于简 单的文字处理系统的“打印预览”功能。 实例 38 简单的文本编辑器 实例说明 编写一个简单的单行文本编辑器,设编辑命令有以下几种。 (1)E——指定所要编辑的文件。 (2)Q——结束编辑。 (3)R——用 R 命令后继的 K 行正文替代原始正文中的 M 行到 N 行的正文内容。命令格式为: R K M N K 行正文 其中 K、M、N 均为大于零的整数。 (4)I——将 I 命令后继的 K 行正文插入到原始正文第 M 行之后。命令格式为: I K M K 行正文 其中 K、M 均为大于零的整数。 (5)D——将原始正文中第 M 行至第 N 行的正文内容删去。命令格式为: D M N 其中 M、N 均为大于零的整数。 程序只限编辑较短的正文,每行不超过 80 个字符,总行数不超过 200 行,正文行从 0 开始编 号。程序运行结果如图 38-1 所示。 图 38-1 实例 38 程序运行结果 8822 W C 语言实例解析精粹 实例解析 程序将读入的正文行字符串存储在向系统申请来的空间中,正文行的字符指针存储在一个指 针数组中。 程序代码 【程序 38】 简单的文本编辑器 //本实例源码参见光盘 归纳注释 实例程序若加上可以实时显示的功能就是相当于 Linux 操作系统下 vi 编辑器的简单的行编辑 器的功能。 实例 39 文件的字数统计程序 实例说明 编程实现统计一个或多个文件的行数、字数和字符数。程序运行结果如图 39-1 所示(设该程 序文件名为 program.exe)。 图 39-1 实例 39 程序运行结果 实例解析 一个行由一个换行符限定,一个字由空格分隔(包括空白符、制表符和换行符),字符是指 第一部分 基础篇 X 8833 文件中的所有字符。要求程序另设 3 个任选的参数,让用户指定所要统计的内容。1——统计文 件行数;w——统计文件字数;c——统计文件字符数。若用户未指定任选的参数,则表示 3 个 统计都要。 运行本程序时的参数按以下格式给出: -l -w -c 文件 1 文件 2 … 文件 n 其中,前 3 个任选参数 l、w、c 的出现与否和出现顺序任意,或任意组合在一起出现,如-lwc, -cwl,-lw,-wl,-lc,-cl,-cw 等。 为实现上述功能,程序引入 3 个计数器,分别用于统计行数、字数和字符数。行计数器在遇 到字符行的第一个字符时增 1;字计数器在遇到每个字时增 1;字符计数器在遇到每个字符时增 1。 为标识一行的开始和结束,引入一个标志变量,遇换行符时,该标志变量置 0,遇其他字符时, 行计数器增 1,并置标志变量为 1;为表示一个字的开始和结束,也引入一个标志变量,遇空白类 字符时,该标志变量置 0,遇其他字符时,字计数器增 1,并置标志变量为 1。程序另引入 3 个标 志变量分别用于区分程序要统计的内容。 程序代码 【程序 39】 文件的字数统计程序 #include main(int argc, char **argv ) { FILE *fp; int lflg,wflg,cflg; /* l, w, c 3 个标志 */ int inline,inword; /* 行内和字内标志 */ int ccount,wcount,lcount; /* 字符,字,行 计数器 */ int c; char *s; lflg=wflg=cflg=0; if(argc<2) { printf("To run this program, usage: program -l -w -c file1 file2 ... filen \n"); exit(0); } while(--argc>=1&&(*++argv)[0]=='-') { for(s=argv[0]+1;*s!='\0';s++) { switch(*s) { case 'l': lflg=1; break; case 'w': wflg=1; break; case 'c': cflg=1; break; default: puts("To run this program, usage: program -l -w -c file1 file2 ... 8844 W C 语言实例解析精粹 filen"); exit(0); } } } if(lflg==0&&wflg==0&&cflg==0) lflg=wflg=cflg=1; lcount=wcount=ccount=0; while(--argc>=0) { if((fp=fopen(*argv++,"r"))==NULL) /* 以只读方式打开文件 */ { fprintf(stderr,"Can't open %s.\n",*argv); continue; } inword=inline=0; while((c=fgetc(fp))!=EOF) { if(cflg) ccount++; if(wflg) if(c=='\n'||c==' '||c=='\t') inword=0; else if(inword==0) { wcount++; inword=1; } if(lflg) if(c=='\n') inline=0; else if(inline==0) { lcount++; inline=1; } } fclose(fp); /* 关闭文件 */ } if(lflg) printf(" Lines = %d\n",lcount); if(wflg) printf(" Words = %d\n",wcount); if(cflg) printf(" Characters = %d\n",ccount); } 归纳注释 程序为了能同时对多个文件完成统计,把文件名参数定在最后,根据 main 函数的数组指针参 数依次对多个文件进行统计。 第一部分 基础篇 X 8855 实例 40 学生成绩管理程序 实例说明 编制一个统计存储在文件中的学生考试分数的管理程序。设学生成绩以一个学生一条记录的 形式存储在文件中,每个学生记录包含的信息有姓名、学号和各门功课的成绩。要求编制具有以 下几项功能的程序:求出各门课程的总分,平均分、按姓名,按学号寻找其记录并显示,浏览全 部学生成绩和按总分由高到低显示学生信息等。程序运行结果如图 40-1~40-5 所示。 图 40-1 实例 40 程序运行结果 图 40-2 输入 l 命令后的结果 图 40-3 输入 s 命令后的结果 8866 W C 语言实例解析精粹 图 40-4 按姓名和学号查找记录的结果 图 40-5 计算平均分和总分 实例解析 设每位学生学习语文、数学和英语 3 门课程,主程序输入文件名之后,进入接受命令、执行 命令处理程序的循环。 按问题的要求共设 5 条命令:求各门课程的总分、求各门课程的平均分、按学生名字寻找其 信息、按学号寻找其信息、结束命令。 为求各门课程的总分,从文件逐一读出学生记录,累计各门课程的分数,待文件处理完即可 得到各门课程的总分。为求各门课程的平均分,从文件逐一读出学生记录,累计各门课程的分数, 并统计学生人数,待文件处理完毕,将得到的各门课程的总分除以人数就得到各门课程的平均分。 按学生名字寻找学生信息的处理,首先要求输入待寻找学生的名字,顺序读入学生记录,凡名字 与待寻找学生相同的记录都在屏幕上显示,直到文件结束。按学生学号寻找学生信息的处理,首 先要求输入待寻找学生的学号,顺序读入学生记录,发现有学号与待寻找学生相同的记录就在屏 第一部分 基础篇 X 8877 幕上显示,并结束处理。浏览学生全部成绩,顺序读入学生记录,并在屏幕上显示,直到文件结 束。按总分由高到低显示学生信息,首先顺序读入学生记录并构造一个有序链表,然后顺序显示 链表上的各元素。 程序代码 【程序 40】 学生成绩管理程序 #include #define SWN 3 /* 课程数 */ #define NAMELEN 20 /* 姓名最大字符数 */ #define CODELEN 10 /* 学号最大字符数 */ #define FNAMELEN 80 /* 文件名最大字符数 */ #define BUFLEN 80 /* 缓冲区最大字符数 */ /* 课程名称表 */ char schoolwork[SWN][NAMELEN+1] = {"Chinese","Mathematic","English"}; struct record { char name[NAMELEN+1]; /* 姓名 */ char code[CODELEN+1]; /* 学号 */ int marks[SWN]; /* 各课程成绩 */ int total; /* 总分 */ }stu; struct node { char name[NAMELEN+1]; /* 姓名 */ char code[CODELEN+1]; /* 学号 */ int marks[SWN]; /* 各课程成绩 */ int total; /* 总分 */ struct node *next; /* 后续表元指针 */ }*head; /* 链表首指针 */ int total[SWN]; /* 各课程总分 */ FILE *stfpt; /* 文件指针 */ char stuf[FNAMELEN]; /* 文件名 */ /* 从指定文件读入一条记录 */ int readrecord(FILE *fpt,struct record *rpt) { char buf[BUFLEN]; int i; if(fscanf(fpt,"%s",buf)!=1) return 0; /* 文件结束 */ strncpy(rpt->name,buf,NAMELEN); fscanf(fpt,"%s",buf); strncpy(rpt->code,buf,CODELEN); for(i=0;imarks[i]); for(rpt->total=0,i=0;itotal+=rpt->marks[i]; return 1; } 8888 W C 语言实例解析精粹 /* 对指定文件写入一条记录 */ writerecord(FILE *fpt,struct record *rpt) { int i; fprintf(fpt,"%s\n",rpt->name); fprintf(fpt,"%s\n",rpt->code); for(i=0;imarks[i]); return ; } /* 显示学生记录 */ displaystu(struct record *rpt) { int i; printf("\nName : %s\n",rpt->name); printf("Code : %s\n",rpt->code); printf("Marks :\n"); for(i=0;imarks[i]); printf("Total : %4d\n",rpt->total); } /* 计算各单科总分 */ int totalmark(char *fname) { FILE *fp; struct record s; int count,i; if((fp=fopen(fname,"r"))==NULL) { printf("Can't open file %s.\n",fname); return 0; } for(i=0;itotal<=v->total) { u=v; v=v->next; } if(v==h) h=p; else u->next=p; p->next=v; p=(struct node *)malloc(sizeof(struct node)); } free(p); fclose(fp); return h; } /* 顺序显示链表各表元 */ void displaylist(struct node *h) { while(h!=NULL) { displaystu((struct record *)h); printf("\n Press ENTER to continue...\n"); 9900 W C 语言实例解析精粹 while(getchar()!='\n'); h=h->next; } return; } /* 按学生姓名查找学生记录 */ int retrievebyn(char *fname, char *key) { FILE *fp; int c; struct record s; if((fp=fopen(fname,"r"))==NULL) { printf("Can't open file %s.\n",fname); return 0; } c=0; while(readrecord(fp,&s)!=0) { if(strcmp(s.name,key)==0) { displaystu(&s); c++; } } fclose(fp); if(c==0) printf("The student %s is not in the file %s.\n",key,fname); return 1; } /* 按学生学号查找学生记录 */ int retrievebyc(char *fname, char *key) { FILE *fp; int c; struct record s; if((fp=fopen(fname,"r"))==NULL) { printf("Can't open file %s.\n",fname); return 0; } c=0; while(readrecord(fp,&s)!=0) { if(strcmp(s.code,key)==0) { displaystu(&s); c++; break; } } fclose(fp); if(c==0) 第一部分 基础篇 X 9911 printf("The student %s is not in the file %s.\n",key,fname); return 1; } main() { int i,j,n; char c; char buf[BUFLEN]; FILE *fp; struct record s; clrscr(); printf("Please input the students marks record file's name: "); scanf("%s",stuf); if((fp=fopen(stuf,"r"))==NULL) { printf("The file %s doesn't exit, do you want to creat it? (Y/N) ",stuf); getchar(); c=getchar(); if(c=='Y'||c=='y') { fp=fopen(stuf,"w"); printf("Please input the record number you want to write to the file: "); scanf("%d",&n); for(i=0;i #define MAX 255 int R[MAX]; void Insert_Sort(int n) { /* 对数组 R 中的记录 R[1..n]按递增序进行插入排序 */ int i,j; for(i=2;i<=n;i++) /* 依次插入 R[2],…,R[n] */ if(R[i]MAX) { printf("n must more than 0 and less than %d.\n",MAX); exit(0); } puts("Please input the elements one by one:"); for(i=1;i<=n;i++) scanf("%d",&R[i]); puts("The sequence you input is:"); for(i=1;i<=n;i++) printf("%4d",R[i]); Insert_Sort(n); puts("\nThe sequence after insert_sort is:"); for(i=1;i<=n;i++) printf("%4d",R[i]); puts("\n Press any key to quit..."); getch(); } 归纳注释 哨兵的作用:算法中引进的附加记录 R[0]称监视哨或哨兵(Sentinel)。哨兵有两个作用:① 进 行查找(插入位置)循环之前,它保存了 R[i]的副本,使不致于因记录后移而丢失 R[i]的内容; 110000 W C 语言实例解析精粹 ② 它的主要作用是在查找循环中“监视”下标变量 j 是否越界。一旦越界(即 j=0),因 为 R[0].key 和自己比较,循环判定条件不成立使得查找循环结束,从而避免了在该循环内的每一次均要检测 j 是否越界(即省略了循环判定条件“j>=1”)。 实际上,一切为简化边界条件而引入的附加结点(元素)均可称为哨兵。例如单链表中的头结 点实际上是一个哨兵。引入哨兵后可以使得测试查找循环条件的时间减少了一半,所以对于记录数 较大的文件节约的时间就相当可观。对于类似于排序这样使用频率非常高的算法,要尽可能地减少 其运行时间。所以不能把上述算法中的哨兵视为雕虫小技,而应该深刻理解并掌握这种技巧。 插入算法的时间性能分析:对于具有 n 个记录的文件,要进行 n-1 趟排序。各种状态下的时 间复杂度:初始文件状态为正序、反序和无序(平均)时,时间复杂度分别为 O(n)、O(n2)和 O(n2)。 算法的空间复杂度分析:算法所需的辅助空间是一个监视哨,辅助空间复杂度 S(n)=O(1),是 一个就地排序。 直接插入排序的稳定性:直接插入排序是稳定的排序方法。 实例 42 希尔排序 实例说明 用希尔排序方法对数组进行排序。程序运行结果如图 42-1 所示。 图 42-1 实例 42 希尔排序程序运行结果 实例解析 希尔排序(Shell Sort)是插入排序的一种。希尔排序基本思想是先取一个小于 n 的整数 d1 作为第一个增量,把文件的全部记录分成 d1 个组。所有距离为 dl 的倍数的记录放在同一个组中。 先在各组内进行直接插入排序;然后,取第二个增量 d20&&R[0].key0 do { increment=increment/3+1; //求下一增量 ShellPass(R,increment); //一趟增量为 increment 的 Shell 插入排序 }while(increment>1) } //ShellSort 注意:当增量 d=1 时,希尔排序和插入排序基本一致,只是由于没有哨兵而在内循环中增加 了一个循环判定条件“j>0”,以防下标越界。 程序代码 【程序 42】 希尔排序 #include #define MAX 255 int R[MAX]; void ShellPass(int d, int n) {/* 希尔排序中的一趟排序,d 为当前增量 */ int i,j; for(i=d+1;i<=n;i++) /* 将 R[d+1..n]分别插入各组当前的有序区 */ if(R[i]0&&R[0]0 */ do { increment=increment/3+1; /* 求下一增量 */ ShellPass(increment,n); /* 一趟增量为 increment 的 Shell 插入排序 */ }while(increment>1); } /* ShellSort */ void main() { int i,n; clrscr(); puts("Please input total element number of the sequence:"); scanf("%d",&n); 110022 W C 语言实例解析精粹 if(n<=0||n>MAX) { printf("n must more than 0 and less than %d.\n",MAX); exit(0); } puts("Please input the elements one by one:"); for(i=1;i<=n;i++) scanf("%d",&R[i]); puts("The sequence you input is:"); for(i=1;i<=n;i++) printf("%4d",R[i]); Shell_Sort(n); puts("\nThe sequence after shell_sort is:"); for(i=1;i<=n;i++) printf("%4d",R[i]); puts("\n Press any key to quit..."); getch(); } 归纳注释 增量序列的选择:希尔排序的执行时间依赖于增量序列。好的增量序列的共同特征:① 最后 一个增量必须为 1;②尽量避免序列中的值(尤其是相邻的值)互为倍数的情况。有人通过大量 的实验,给出了目前较好的结果:当 n 较大时,比较和移动的次数约在 nl.25~1.6n1.25 之间。 希尔排序的时间性能优于直接插入排序,因为:①当文件初态基本有序时直接插入排序所需 的比较和移动次数均较少。②当 n 值较小时,n 和 n2 的差别也较小,即直接插入排序的最好时间 复杂度 O(n)和最坏时间复杂度 O(n2)差别不大。③在希尔排序开始时增量较大,分组较多,每组 的记录数目少,故各组内直接插入较快,后来增量 di 逐渐缩小,分组数逐渐减少,而各组的记录 数目逐渐增多,但由于已经按 di-1 作为距离排过序,使文件较接近于有序状态,所以新的一趟排 序过程也较快。因此,希尔排序在效率上较直接插入排序有较大的改进。 稳定性:希尔排序是不稳定的。因为两个相同关键字在排序前后的相对次序发生了变化。 实例 43 冒泡排序 实例说明 用冒泡排序方法的数组进行排序。程序运行结果如图 43-1 所示。 图 43-1 实例 43 冒泡排序程序运行结果 第二部分 数据结构篇 X 110033 实例解析 交换排序的基本思想是两两比较待排序记录的关键字,发现两个记录的次序相反时即进行交 换,直到没有反序的记录为止。 应用交换排序基本思想的主要排序方法有冒泡排序和快速排序。 冒泡排序 将被排序的记录数组 R[1..n]垂直排列,每个记录 R[i]看做是重量为 R[i].key 的气泡。根据轻 气泡不能在重气泡之下的原则,从下往上扫描数组 R。凡扫描到违反本原则的轻气泡,就使其向 上“飘浮”。如此反复进行,直到最后任何两个气泡都是轻者在上,重者在下为止。 (1)初始,R[1..n]为无序区。 (2)第一趟扫描,从无序区底部向上依次比较相邻的两个气泡的重量,若发现轻者在下、重 者在上,则交换二者的位置。即依次比较(R[n],R[n-1]),(R[n−1],R[n-2]),…,(R[2],R[1]); 对于每对气泡(R[j+1],R[j]),若 R[j+1].key=i;j--) //对当前无序区 R[i..n]自下向上扫描 if(R[j+1].key #define MAX 255 int R[MAX]; void Bubble_Sort(int n) { /* R(l..n)是待排序的文件,采用自下向上扫描,对 R 做冒泡排序 */ int i,j; int exchange; /* 交换标志 */ for(i=1;i=i;j--) /* 对当前无序区 R[i..n]自下向上扫描 */ if(R[j+1]MAX) { printf("n must more than 0 and less than %d.\n",MAX); exit(0); } puts("Please input the elements one by one:"); for(i=1;i<=n;i++) scanf("%d",&R[i]); puts("The sequence you input is:"); for(i=1;i<=n;i++) printf("%4d",R[i]); Bubble_Sort(n); puts("\nThe sequence after bubble_sort is:"); for(i=1;i<=n;i++) printf("%4d",R[i]); puts("\n Press any key to quit..."); getch(); 第二部分 数据结构篇 X 110055 } 归纳注释 算法的最好时间复杂度:若文件的初始状态是正序的,一趟扫描即可完成排序。所需的关键字比 较次数 C 和记录移动次数 M 均达到最小值,即 Cmin=n−1,Mmin=0。冒泡排序最好的时间复杂度为 O(n)。 算法的最坏时间复杂度:若初始文件是反序的,需要进行 n−1 趟排序。每趟排序要进行 n−i 次 关键字的比较(1≤i≤n−1),且每次比较都必须移动记录 3 次。在这种情况下,比较和移动次数均 达到最大值,即 Cmax=n(n−1)/2=O(n2),Mmax=3n(n−1)/2=O(n2)。冒泡排序的最坏时间复杂度为 O(n2)。 算法的平均时间复杂度为 O(n2)。虽然冒泡排序不一定要进行 n-1 趟,但由于它的记录移动次 数较多,故平均时间性能比直接插入排序要差得多。 算法稳定性:冒泡排序是就地排序,且它是稳定的。 算法改进:上述的冒泡排序还可做如下的改进,①记住最后一次交换发生位置 lastExchange 的冒泡排序(该位置之前的相邻记录均已有序)。下一趟排序开始时,R[1..lastExchange-1]是有序 区,R[lastExchange..n]是无序区。这样,一趟排序可能使当前有序区扩充多个记录,从而减少排 序的趟数。②改变扫描方向的冒泡排序。冒泡排序具有不对称性。能一趟扫描完成排序的情况, 只有最轻的气泡位于 R[n]的位置,其余的气泡均已排好序,那么也只需一趟扫描就可以完成排序。 如对初始关键字序列 12,18,42,44,45,67,94,10 就仅需一趟扫描。需要 n−1 趟扫描完成 排序情况,当只有最重的气泡位于 R[1]的位置,其余的气泡均已排好序时,则仍需做 n−1 趟扫描 才能完成排序。比如对初始关键字序列:94,10,12,18,42,44,45,67 就需 7 趟扫描。造成 不对称性的原因是每趟扫描仅能使最重气泡“下沉”一个位置,因此使位于顶端的最重气泡下沉 到底部时,需做 n−1 趟扫描。在排序过程中交替改变扫描方向,可改进不对称性。 实例 44 快速排序 实例说明 用快速排序的方法对数组进行排序。程序运行结果如图 44-1 所示。 图 44-1 实例 44 快速排序程序运行结果 实例解析 快速排序( QuickSort) 快速排序是一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and- 110066 W C 语言实例解析精粹 ConquerMethod)。 (1)分治法的基本思想,将原问题分解为若干个规模更小但结构与原问题相似的子问题。递 归地解这些子问题,然后将这些子问题的解组合为原问题的解。 (2)快速排序的基本思想 设当前待排序的无序区为 R[low..high],利用分治法的基本思想如下。 ① 分解。在 R[low..high]中任选一个记录作为基准(Pivot),以此基准将当前无序区划分为左、 右两个较小的子区间 R[low..pivotpos-1]和 R[pivotpos+1..high],并使左边子区间中所有记录的关键 字均小于等于基准记录(不妨记为 pivot)的关键字 pivot.key,右边的子区间中所有记录的关键字 均大于等于 pivot.key,而基准记录 pivot 则位于正确的位置(pivotpos)上,无需参加后续的排序。 划分的关键是要求出基准记录所在的位置 pivotpos。划分的结果可以简单地表示为(注意 pivot=R[pivotpos]):R[low..pivotpos-1].keys≤R[pivotpos].key≤R[pivotpos+1..high].keys,其中 low ≤pivotpos≤high。 ② 求解。通过递归调用快速排序对左、右子区间 R[low..pivotpos-1]和 R[pivotpos+1..high]快 速排序。 ③ 组合。因为当“求解”步骤中的两个递归调用结束时,其左、右两个子区间已有序。对快 速排序而言,“组合”步骤无需做什么,可看作是空操作。 快速排序算法 void QuickSort(SeqList R,int low,int high) { //对 R[low..high]快速排序 int pivotpos; //划分后的基准记录的位置 if(low=pivot.key) //pivot 相当于在位置 i 上 j--; //从右向左扫描,查找第 1 个关键字小于 pivot.key 的记录 R[j] if(ipivot.key R[j--]=R[i]; //相当于交换 R[i]和 R[j],交换后 j 指针减 1 } //endwhile R[i]=pivot; //基准记录已被最后定位 return i; } //partition 程序代码 【程序 44】 快速排序 #include #define MAX 255 int R[MAX]; int Partition(int i,int j) {/* 调用 Partition(R,low,high)时,对 R[low..high]做划分,并返回基准记录的位置*/ int pivot=R[i]; /* 用区间的第 1 条记录作为基准 */ while(i=pivot) /* pivot 相当于在位置 i 上 */ j--; /* 从右向左扫描,查找第 1 个关键字小于 pivot.key 的记录 R[j] */ if(ipivot.key */ R[j--]=R[i]; /* 相当于交换 R[i]和 R[j],交换后 j 指针减 1 */ } /* endwhile */ R[i]=pivot; /* 基准记录已被最后定位*/ return i; } /* end of partition */ void Quick_Sort(int low,int high) { /* 对 R[low..high]快速排序 */ int pivotpos; /* 划分后的基准记录的位置 */ if(lowMAX) { printf("n must more than 0 and less than %d.\n",MAX); exit(0); } puts("Please input the elements one by one:"); for(i=1;i<=n;i++) scanf("%d",&R[i]); puts("The sequence you input is:"); for(i=1;i<=n;i++) printf("%4d",R[i]); Quick_Sort(1,n); puts("\nThe sequence after quick_sort is:"); for(i=1;i<=n;i++) printf("%4d",R[i]); puts("\n Press any key to quit..."); getch(); } 归纳注释 快速排序的时间主要耗费在划分操作上,对长度为 k 的区间进行划分,共需 k−1 次关键字的 比较。 最坏时间复杂度:最坏情况是每次划分选取的基准都是当前无序区中关键字最小(或最大) 的记录,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的另一个非空 的子区间中记录数目,仅仅比划分前的无序区中记录个数减少一个。因此,快速排序必须做 n−1 次划分,第 i 次划分开始时区间长度为 n−i+1,所需的比较次数为 n−i(1≤i≤n-1),故总的比较 次数达到最大值 Cmax = n(n−1)/2=O(n2)。如果按上面给出的划分算法,每次取当前无序区的第 1 个 记录为基准,那么当文件的记录已按递增序(或递减序)排列时,每次划分所取的基准就是当前 无序区中关键字最小(或最大)的记录,则快速排序所需的比较次数反而最多。 最好时间复杂度:在最好情况下,每次划分所取的基准都是当前无序区的“中值”记录,划 分的结果与基准的左、右两个无序子区间的长度大致相等。总的关键字比较次数为 O(nlgn)。 用递归树来分析最好情况下的比较次数更简单。因为每次划分后左、右子区间长度大致相 等,故递归树的高度为 O(lgn),而递归树每一层上各结点所对应的划分过程中所需要的关键字 比较次数总和不超过 n,故整个排序过程所需要的关键字比较总次数 C(n)=O(nlgn)。因为快速排 序的记录移动次数不大于比较的次数,所以快速排序的最坏时间复杂度应为 O(n2),最好时间复 杂度为 O(nlgn)。 基准关键字的选取:在当前无序区中选取划分的基准关键字是决定算法性能的关键。①“三 者取中”的规则,即在当前区间里,将该区间首、尾和中间位置上的关键字比较,以三者之中值 所对应的记录作为基准,在划分开始前将该基准记录和该区的第 1 个记录进行交换,此后的划分 过程与上面所给的 Partition 算法完全相同。②取位于 low 和 high 之间的随机数 k(low≤k≤high), 用 R[k]作为基准;选取基准最好的方法是用一个随机函数产生一个位于 low 和 high 之间的随机数 k(low≤k≤high),用 R[k]作为基准,这相当于强迫 R[low..high]中的记录是随机分布的。用此方法 所得到的快速排序一般称为随机的快速排序。随机的快速排序与一般的快速排序算法差别很小。 但随机化后,算法的性能大大地提高了,尤其是对初始有序的文件,一般不可能导致最坏情况的 发生。算法的随机化不仅仅适用于快速排序,也适用于其他需要数据随机分布的算法。 第二部分 数据结构篇 X 110099 平均时间复杂度:尽管快速排序的最坏时间为 O(n2),但就平均性能而言,它是基于关键字比 较的内部排序算法中速度最快的,快速排序亦因此而得名。它的平均时间复杂度为 O(nlgn)。 空间复杂度:快速排序在系统内部需要一个栈来实现递归。若每次划分较为均匀,则其递归 树的高度为 O(lgn),故递归后所需栈空间为 O(lgn)。最坏情况下,递归树的高度为 O(n),所需的 栈空间为 O(n)。 稳定性:快速排序是非稳定的。 实例 45 选择排序 实例说明 用直接选择排序方法对数组进行排序。程序运行结果如图 45-1 所示。 图 45-1 实例 45 程序运行结果 实例解析 选择排序(Selection Sort)的基本思想是:每一趟从待排序的记录中选出关键字最小的记录, 顺序放在已排好序的子文件的最后,直到全部记录排序完毕。 常用的选择排序方法有直接选择排序和堆排序。 直接选择排序( Straight Selection Sort) 直接选择排序的基本思想是 n 个记录的文件的直接选择排序可经过 n−1 趟直接选择排序得到 有序结果。 ① 初始状态:无序区为 R[1..n],有序区为空。 ② 第 1 趟排序 在无序区 R[1..n]中选出关键字最小的记录 R[k],将它与无序区的第 1 个记录 R[1]交换,使 R[1..1]和 R[2..n]分别变为记录个数增加 1 个的新有序区和记录个数减少 1 个的新无序区。 ③ 第 i 趟排序 第 i 趟排序开始时,当前有序区和无序区分别为 R[1..i−1]和 R[i..n](1≤i≤n−1)。该趟排序从 当前无序区中选出关键字最小的记录 R[k],将它与无序区的第 1 个记录 R[i]交换,使 R[1..i]和 R[i+1..n]分别变为记录个数增加 1 个的新有序区和记录个数减少 1 个的新无序区。 这样,n 个记录的文件的直接选择排序即可经过 n-1 趟直接选择排序得到有序结果。 直接选择排序的具体算法如下: void SelectSort(SeqList R) 111100 W C 语言实例解析精粹 { int i,j,k; for(i=1;i #define MAX 255 int R[MAX]; void Select_Sort(int n) { int i,j,k; for(i=1;iMAX) { printf("n must more than 0 and less than %d.\n",MAX); exit(0); } puts("Please input the elements one by one:"); for(i=1;i<=n;i++) scanf("%d",&R[i]); puts("The sequence you input is:"); for(i=1;i<=n;i++) printf("%4d",R[i]); Select_Sort(n); 第二部分 数据结构篇 X 111111 puts("\nThe sequence after select_sort is:"); for(i=1;i<=n;i++) printf("%4d",R[i]); puts("\n Press any key to quit..."); getch(); } 归纳注释 关键字比较次数:无论文件初始状态如何,在第 i 趟排序中选出最小关键字的记录,需做 n-i 次比较,因此,总的比较次数为 n(n−1)/2=O(n2)。 记录的移动次数:当初始文件为正序时,移动次数为 0。文件初态为反序时,每趟排序均要 执行交换操作,总的移动次数取最大值 3(n-1)。直接选择排序的平均时间复杂度为 O(n2)。 直接选择排序是一个就地排序。 稳定性分析:直接选择排序是不稳定的,反例比如[2,2,1]。 实例 46 堆排序 实例说明 用堆排序的方法对数组进行排序。程序运行结果如图 46-1 所示。 图 46-1 实例 46 堆排序程序运行结果 实例解析 堆排序 堆排序定义:n 个关键字序列 Kl,K2,…,Kn 称为堆,当且仅当该序列满足如下性质(简称 为堆性质):①ki≤K2i 且 ki≤K2i+1 或②Ki≥K2i 且 ki≥K2i+1(1≤i≤n)。 若将此序列所存储的向量 R[1..n]看做是一棵完全二叉树的存储结构,则堆实质上是满足如下 性质的完全二叉树:树中任一非叶结点的关键字均不大于(或不小于)其左右子结点(若存在) 的关键字。 例如,关键字序列(10,15,56,25,30,70)和(70,56,30,25,15,10)分别满足 111122 W C 语言实例解析精粹 堆性质①和②,故它们均是堆,其对应的完全二叉树比如小根堆示例和大根堆示例如图 46-2 和 46-3 所示。 图 46-2 小根堆示例 图 46-3 大根堆示例 大根堆和小根堆 根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最小者的堆称为小根堆。 根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最大者的堆称为大根堆。 注意:堆中任一子树亦是堆;以上讨论的堆实际上是二叉堆(Binary Heap),类似地可定 义 k 叉堆。 堆排序特点 堆排序(HeapSort)是一树形选择排序。在排序过程中,将 R[l..n]看成是一棵完全二叉树的 顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系,在当前无序区中选择关 键字最大(或最小)的记录。 堆排序与直接插入排序的区别 直接选择排序中,为了从R[1..n]中选出关键字最小的记录,必须进行n-1次比较,然后在R[2..n] 中选出关键字最小的记录,又需要做 n−2 次比较。事实上,后面的 n−2 次比较中,有许多比较可 能在前面的 n−1 次比较中已经做过,但由于前一趟排序时未保留这些比较结果,所以后一趟排序 时又重复执行了这些比较操作。 堆排序可通过树形结构保存部分比较结果,可减少比较次数。 堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前 无序区中选取最大(或最小)关键字的记录变得简单。 用大根堆排序的基本思想 ① 先将初始文件 R[1..n]建成一个大根堆,此堆为初始的无序区 ② 再将关键字最大的记录 R[1](即堆顶)和无序区的最后一个记录 R[n]交换,由此得到新 的无序区 R[1..n−1]和有序区 R[n],且满足 R[1..n−1].keys≤R[n].key 第二部分 数据结构篇 X 111133 ③ 由于交换后新的根 R[1]可能违反堆性质,故应将当前无序区 R[1..n−1]调整为堆。然后再 次将 R[1..n−1]中关键字最大的记录 R[1]和该区间的最后一个记录 R[n−1]交换,由此得到新的无序 区 R[1..n−2]和有序区 R[n−1..n],且仍满足关系 R[1..n−2].keys≤R[n−1..n].keys,同样要将 R[1..n−2] 调整为堆……直到无序区只有一个元素为止。 大根堆排序算法的基本操作 ① 初始化操作:将 R[1..n]构造为初始堆。 ② 每一趟排序的基本操作:将当前无序区的堆顶记录 R[1]和该区间的最后一个记录交换, 然后将新的无序区调整为堆(亦称重建堆)。 注意:只需做 n-1 趟排序,选出较大的 n-1 个关键字即可使得文件递增有序。用小根堆排序与 利用大根堆类似,只不过其排序结果是递减有序的。堆排序和直接选择排序相反,在任何时刻,堆 排序中无序区总是在有序区之前,且有序区是在原向量的尾部由后往前逐步扩大至整个向量为止。 堆排序的算法如下: void HeapSort(SeqList R) { //对 R[1..n]进行堆排序,用 R[0]做暂存单元 int i; BuildHeap(R); //将 R[1-n]建成初始堆 for(i=n;i>1;i--){ //对当前无序区 R[1..i]进行堆排序,共做 n-1 趟。 R[0]=R[1];R[1]=R[i];R[i]=R[0]; //将堆顶和堆中最后一个记录交换 Heapify(R,1,i-1); //将 R[1..i-1]重新调整为堆,仅有 R[1]可能违反堆性质 } //endfor } //HeapSort BuildHeap 和 Heapify 函数的实现 因为构造初始堆必须使用到调整堆的操作,先讨论 Heapify 的实现。 Heapify 函数思想:每趟排序开始前 R[l..i]是以 R[1]为根的堆,在 R[1]与 R[i]交换后,新的无 序区 R[1..i-1]中只有 R[1]的值发生了变化,故除 R[1]可能违反堆性质外,其余任何结点为根的子 树均是堆。因此,当被调整区间是 R[low..high]时,只需调整以 R[low]为根的树即可。 “筛选法”调整堆。R[low]的左、右子树(若存在)均已是堆,这两棵子树的根R[2low]和R[2low+1] 分别是各自子树中关键字最大的结点。若 R[low].key 不小于这两个孩子结点的关键字,则 R[low] 未违反堆性质,以 R[low]为根的树已是堆,无需调整;否则必须将 R[low]和它的两个孩子结点中 关键字较大者进行交换,即 R[low]与 R[large](R[large].key=max(R[2low].key,R[2low+1].key))交换。 交换后又可能使结点 R[large]违反堆性质,同样由于该结点的两棵子树(若存在)仍然是堆,故 可重复上述的调整过程,对以 R[large]为根的树进行调整。此过程直至当前被调整的结点已满足 堆性质,或者该结点已是叶子为止。上述过程就像过筛子一样,把较小的关键字逐层筛下去,而 将较大的关键字逐层选上来。因此,有人将此方法称为“筛选法”。 具体的算法如下: void Heapify(SeqList R,int s,int m) { //对 R[1..n]进行堆调整,用 temp 做暂存单元 int j; temp=R[s]; j=2*s; while (j<=m) { if (R[j]>R[j+1]&&j0;i--) Heapify(R,i,n); } 程序代码 【程序 46】 堆排序 #include #define MAX 255 int R[MAX]; void Heapify(int s,int m) { /*对 R[1..n]进行堆调整,用 temp 做暂存单元 */ int j,temp; temp=R[s]; j=2*s; while (j<=m) { if (R[j]>R[j+1]&&j0;i--) Heapify(i,n); } void Heap_Sort(int n) 第二部分 数据结构篇 X 111155 { /* 对 R[1..n]进行堆排序,用 R[0]做暂存单元 */ int i; BuildHeap(n); /* 将 R[1-n]建成初始堆 */ for(i=n;i>1;i--) { /* 对当前无序区 R[1..i]进行堆排序,共做 n-1 趟。 */ R[0]=R[1]; R[1]=R[i];R[i]=R[0]; /* 将堆顶和堆中最后一个记录交换 */ Heapify(1,i-1); /* 将 R[1..i-1]重新调整为堆,仅有 R[1]可能违反堆性质 */ } /* end of for */ } /* end of Heap_Sort */ void main() { int i,n; clrscr(); puts("Please input total element number of the sequence:"); scanf("%d",&n); if(n<=0||n>MAX) { printf("n must more than 0 and less than %d.\n",MAX); exit(0); } puts("Please input the elements one by one:"); for(i=1;i<=n;i++) scanf("%d",&R[i]); puts("The sequence you input is:"); for(i=1;i<=n;i++) printf("%4d",R[i]); Heap_Sort(n); puts("\nThe sequence after Big heap_sort is:"); for(i=1;i<=n;i++) printf("%4d",R[i]); puts("\n Press any key to quit..."); getch(); } 归纳注释 堆排序的时间主要由建立初始堆和反复重建堆这两部分的时间开销构成,它们均是通过调用 Heapify 实现的。 堆排序的最坏时间复杂度为 O(nlgn)。堆排序的平均性能较接近于最坏性能。 由于建初始堆所需的比较次数较多,所以堆排序不适于记录数较少的文件。 堆排序是就地排序,辅助空间为 O(1),堆排序是不稳定的排序方法。 实例 47 归并排序 实例说明 用归并排序的方法对数组进行排序。程序运行结果如图 47-1 所示。 111166 W C 语言实例解析精粹 图 47-1 实例 47 归并排序程序运行结果 实例解析 归并排序(Merge Sort)是利用“归并”技术来进行排序。归并是指将若干已排序的子文件 合并成一个有序的文件。 两路归并算法 算法基本思路:设两个有序的子文件(相当于输入堆)放在同一向量中相邻的位置上, R[low..m],R[m+1..high],先将它们合并到一个局部的暂存向量 R1(相当于输出堆)中,待合并 完成后将 R1 复制回 R[low..high]中。 ① 合并过程,设置 i,j 和 p 3 个指针,其初值分别指向这 3 个记录区的起始位置。合并时依 次比较 R[i]和 R[j]的关键字,取关键字较小的记录复制到 R1[p]中,然后将被复制记录的指针 i 或 j 加 1,以及指向复制位置的指针 p 加 1。 重复这一过程直至两个输入的子文件有一个已全部复制完毕(不妨称其为空),此时将另一非 空的子文件中剩余记录依次复制到 R1 中即可。 ② 动态申请 R1,因为申请的空间可能很大,故需加入申请空间是否成功的处理。 归并算法如下: void Merge(SeqList R,int low,int m,int high) {//将两个有序的子文件 R[low..m)和 R[m+1..high]归并成一个有序的子文件 R[low..high] int i=low,j=m+1,p=0; //置初始值 RecType *R1; //R1 是局部向量,若 p 定义为此类型指针速度更快 R1=(ReeType *)malloc((high-low+1)*sizeof(RecType)); if(! R1) //申请空间失败 Error("Insufficient memory available!"); while(i<=m&&j<=high) //两子文件非空时取其小者输出到 R1[p]上 R1[p++]=(R[i].key<=R[j].key)?R[i++]:R[j++]; while(i<=m) //若第 1 个子文件非空,则复制剩余记录到 R1 中 R1[p++]=R[i++]; while(j<=high) //若第 2 个子文件非空,则复制剩余记录到 R1 中 R1[p++]=R[j++]; for(p=0,i=low;i<=high;p++,i++) R[i]=R1[p];//归并完成后将结果复制回 R[low..high] } //Merge 归并排序 归并排序有两种实现方法:自底向上和自顶向下。 (1)自底向上的基本思想,第 1 趟归并排序时,将待排序的文件 R[1..n]看做是 n 个长度为 1 的 有序子文件,将这些子文件两两归并,若 n 为偶数,则得到 ⎥⎥ ⎤ ⎢⎢ ⎡ 2 n (表示大于等于 2 n 的最小整数)个 第二部分 数据结构篇 X 111177 长度为 2 的有序子文件;若 n 为奇数,则最后一个子文件轮空(不参与归并)。故本趟归并完成后, 前 ⎡⎤lgn 个有序子文件长度为 2,但最后一个子文件长度仍为 1;第 2 趟归并则是将第 1 趟归并所得 到的 ⎡⎤lgn 个有序的子文件两两归并,如此反复,直到最后得到一个长度为 n 的有序文件为止。 上述的每次归并操作,均是将两个有序的子文件合并成一个有序的子文件,故称其为“二路 归并排序”。类似地有 k(k>2)路归并排序。 (2)一趟归并算法,在某趟归并中,设各子文件长度为 length(最后一个子文件的长度可能 小于 length),则归并前 R[1..n]中共有 ⎡⎤lgn 个有序的子文件:R[1..length],R[length+1..2length]……。 注意:调用归并操作将相邻的一对子文件进行归并时,必须对子文件的个数(可能是奇数) 以及最后一个子文件的长度小于 length 这两种特殊情况进行处理:① 若子文件个数为奇数,则最 后一个子文件无需和其他子文件归并(即本趟轮空);② 若子文件个数为偶数,则要注意最后一 对子文件中后一子文件的区间上界是 n。 具体算法如下: void MergePass(SeqList R,int length) { //对 R[1..n]做一趟归并排序 int i; for(i=1;i+2*length-1<=n;i=i+2*length) Merge(R,i,i+length-1,i+2*length-1); //归并长度为 length 的两个相邻子文件 if(i+length-1 #define MAX 255 int R[MAX]; void Merge(int low,int m,int high) {/* 将两个有序的子文件 R[low..m]和 R[m+1..high]归并成一个有序的子文件 R[low..high] */ int i=low,j=m+1,p=0; /* 置初始值 */ int *R1; /* R1 是局部向量,若 p 定义为此类型指针速度更快 */ R1=(int *)malloc((high-low+1)*sizeof(int)); if(!R1) /* 申请空间失败 */ { puts("Insufficient memory available!"); return; } while(i<=m&&j<=high) /* 两子文件非空时取其小者输出到 R1[p]上 */ R1[p++]=(R[i]<=R[j])?R[i++]:R[j++]; while(i<=m) /* 若第 1 个子文件非空,则复制剩余记录到 R1 中 */ R1[p++]=R[i++]; while(j<=high) /* 若第 2 个子文件非空,则复制剩余记录到 R1 中 */ R1[p++]=R[j++]; for(p=0,i=low;i<=high;p++,i++) R[i]=R1[p];/* 归并完成后将结果复制回 R[low..high] */ } /* end of Merge */ void Merge_SortDC(int low,int high) {/* 用分治法对 R[low..high]进行二路归并排序 */ int mid; if(lowMAX) { printf("n must more than 0 and less than %d.\n",MAX); exit(0); } 第二部分 数据结构篇 X 111199 puts("Please input the elements one by one:"); for(i=1;i<=n;i++) scanf("%d",&R[i]); puts("The sequence you input is:"); for(i=1;i<=n;i++) printf("%4d",R[i]); Merge_SortDC(1,n); puts("\nThe sequence after merge_sortDC is:"); for(i=1;i<=n;i++) printf("%4d",R[i]); puts("\n Press any key to quit..."); getch(); } 归纳注释 稳定性:归并排序是一种稳定的排序。 存储结构要求:可用顺序存储结构,也易于在链表上实现。 时间复杂度:对长度为 n 的文件,需进行 ⎥⎥ ⎤ ⎢⎢ ⎡ 2 n 趟二路归并,每趟归并的时间为 O(n),故其时 间复杂度无论是在最好情况下还是在最坏情况下均是 O(nlgn)。 空间复杂度:需要一个辅助向量来暂存有序子文件归并的结果,故其辅助空间复杂度为 O(n), 显然它不是就地排序。 若用单链表做存储结构,则可以很容易给出就地的归并排序。 实例 48 基数排序 实例说明 用基数排序的方法对数组进行排序。程序运行结果如图 48-1 所示。 图 48-1 实例 48 基数排序程序运行结果 实例解析 分配排序的基本思想:排序过程无需比较关键字,而是通过“分配”和“收集”过程来实现。 112200 W C 语言实例解析精粹 它们的时间复杂度可达到线性阶 O(n)。 分配排序包括箱排序和基数排序。 箱排序( Bin Sort) 箱排序也称桶排序(Bucket Sort),其基本思想是设置若干个箱子,依次扫描待排序的记录 R[0],R[1],…,R[n−1],把关键字等于 k 的记录全都装入到第 k 个箱子里(分配),然后按序号 依次将各非空的箱子首尾连接起来(收集)。 例如,要将一副混洗的 52 张扑克牌按点数 A<2<…*d) *d=strlen(s); temp->k=atoi(s); if (head==NULL) { head=temp; terminal=temp; } else { terminal->next=temp; terminal=temp; } scanf("%s",s); } terminal->next=NULL; return head; } void my_output(lnode h) 112222 W C 语言实例解析精粹 { lnode t=h; printf("\n"); while (h!=NULL) { printf("%d ",h->k); h=h->next; } h=t; /* getch(); */ } lnode Radix_Sort(lnode head,int d) { lnode p,q,h,t; int i,j,x,radix=1; h=(lnode)malloc(10*sizeof(struct node)); t=(lnode)malloc(10*sizeof(struct node)); for (i=d;i>=1;i--) { for (j=0;j<=9;j++) { h[j].next=NULL; t[j].next=NULL; } p=head; while (p!=NULL) { x=((p->k)/radix)%10; if (h[x].next==NULL) { h[x].next=p; t[x].next=p; } else { q=t[x].next; q->next=p; t[x].next=p; } p=p->next; } j=0; while (h[j].next==NULL) j++; head=h[j].next; q=t[j].next; for (x=j+1;x<=9;x++) if (h[x].next!=NULL) { q->next=h[x].next; q=t[x].next; } q->next=NULL; radix*=10; 第二部分 数据结构篇 X 112233 printf("\n---------------------\n"); } return head; } void my_free(lnode h) { lnode temp=h; while (temp) { h=temp->next; free(temp); temp=h; } } void main() { lnode h; int d; clrscr(); h=my_input(&d); puts("The sequence you input is:"); my_output(h); h=Radix_Sort(h,d); puts("\nThe sequence after radix_sort is:"); my_output(h); my_free(h); puts("\n Press any key to quit..."); getch(); } 归纳注释 若排序文件不是以数组 R 形式给出,而是以单链表形式给出(此时称为链式的基数排序), 则可通过修改出队和入队函数使表示箱子的链队列无需分配结点空间,而使用原链表的结点空间。 入队出队操作亦无需移动记录而仅需修改指针。这样以来节省了一定的时间和空间,但算法要复 杂得多,且时空复杂度就其数量级而言并未得到改观。 基数排序的时间是线性的(即 O(n))。 基数排序所需的辅助存储空间为 O(n+rd)。 基数排序是稳定的。 实例 49 顺序表插入和删除 实例说明 通过顺序表的初始化、添加节点、删除节点等操作学习顺序表的用法。程序运行结果如图 49-1 所示。 112244 W C 语言实例解析精粹 图 49-1 实例 49 程序运行结果 实例解析 顺序表是各种数据结构中最简单却是应用很广的一种形式。学生成绩的序列或者按顺序排列 的多种产品属性数据都可以看成是顺序表。简而言之,顺序表就是一个广义的一维数组,这个数 组的元素可以是包含基本数据类型或者复杂数据类型在内的任何数据。在实际内存存储中顺序表 的元素是连续放置的,所以可以根据某个元素在顺序表中的序号来直接访问这个元素,也就是说 顺序表是可以随机访问(Random Access)的。 程序代码 【程序 49】 顺序表的操作 #define ListSize 100/* 假定表空间大小为 100 */ #include #include void Error(char * message) { printf("错误:%s\n",message); exit(1); } struct Seqlist{ int data[ListSize];/* 向量 data 用于存放表结点 */ int length; /* 当前的表长度 */ }; /* 以上为定义表结构 */ /* ------------以下为两个主要算法---------- */ void InsertList(struct Seqlist *L, int x, int i) { /* 将新结点 x 插入 L 所指的顺序表的第 i 个结点 ai 的位置上 */ int j; if ( i < 0 || i > L -> length ) Error("position error");/* 非法位置,退出 */ if ( L->length>=ListSize ) Error("overflow"); for ( j=L->length-1 ; j >= i ; j --) 第二部分 数据结构篇 X 112255 L->data[j+1]=L->data [j]; L->data[i]=x ; L->length++ ; } void DeleteList ( struct Seqlist *L, int i ) {/* 从 L 所指的顺序表中删除第 i 个结点 ai */ int j; if ( i< 0 || i > L-> length-1) Error( " position error" ) ; for ( j = i+1 ; j < L-> length ; j++ ) L->data [ j-1 ]=L->data [ j]; /* 结点前移 */ L-> length-- ; /* 表长减小 */ } /* ===========以下代码对算法验证======= */ void Initlist(struct Seqlist *L) { L->length=0; } /* 显示顺序表的内容 */ void DisplayList(struct Seqlist *L) { int i; puts("Now the list is:"); for(i=0;ilength;i++) printf("%d ",L->data[i]); printf("\n"); return; } void main() { struct Seqlist *SEQA; int i,n,t; SEQA = (struct Seqlist *)malloc(sizeof(struct Seqlist)); Initlist(SEQA); clrscr(); puts("Please input the size of the list:"); scanf("%d",&n); puts("Please input the elements of the list one by one:"); for (i=0;ilength-1); scanf("%d",&i); InsertList(SEQA,t,i); 112266 W C 语言实例解析精粹 DisplayList(SEQA); puts("\n Press any key to quit..."); getch(); } 归纳注释 程序首先调用子函数 InitList()对顺序表进行初始化,要求用户输入表的大小,再依次输入表 的结点元素。通过循环调用 InsertList()依次将元素插入到顺序表中,接着调用函数 DeleteList()删 去顺序表的一个元素,调用 InsertList()插入一个元素,使用 DisplayList()函数循环地把顺序表中的 元素逐个输出。 实例 50 链表操作 实例说明 通过链表的各种操作学习链表的概念、结构体的应用以及指针在链表中的应用。程序运行结 果如图 50-1~图 50-6 所示。 图 50-1 实例 50 程序主界面 图 50-2 创建链表 图 50-3 查找记录 第二部分 数据结构篇 X 112277 图 50-4 插入记录 图 50-5 删除记录 图 50-6 退出程序 实例解析 程序通过结构体数据类型模拟了链表的结构,并使用指针实现了对链表的各项操作。 链接方式存储的线性表简称为链表(Linked List)。链表的存储说明如下。 (1)用一组任意的存储单元来存放线性表的结点(这组存储单元既可以是连续的,也可以是 不连续的)。 (2)因为链表中结点的逻辑次序和物理次序不一定相同,所以为了能正确表示结点间的逻辑 关系,在存储每个结点值的同时,还必须存储指示其后继结点的地址信息。这个地址信息称为指 针(pointer)或链(link)。 程序代码 【程序 50】 链表的操作 #include #define N 10 typedef struct node { char name[20]; struct node *link; }stud; ... /* 其他操作函数见光盘 */ main() { int choose; stud *head,*searchpoint,*forepoint; char fullname[20]; 112288 W C 语言实例解析精粹 while(1) { menu(); scanf("%d",&choose); switch(choose) { case 1: clrscr(); head=creat(); puts("Linklist created successfully! \nPress any key to return..."); getch(); break; case 2: clrscr(); printf("Input the student's name which you want to find:\n"); scanf("%s",fullname); searchpoint=search(head,fullname); printf("The stud name you want to find is:%s",*&searchpoint->name); printf("\nPress any key to returen..."); getchar(); getchar(); break; case 3: clrscr(); insert(head); print(head); printf("\nPress any key to returen..."); getchar();getchar(); break; case 4: clrscr(); print(head); printf("\nInput the student's name which you want to delete:\n"); scanf("%s",fullname); searchpoint=search(head,fullname); forepoint=search2(head,fullname); del(forepoint,searchpoint); print(head); puts("\nDelete successfully! Press any key to return..."); getchar(); getchar(); break; case 5:print(head); printf("\nPress any key to return..."); getchar();getchar(); break; case 6:quit(); break; default: clrscr(); printf("Illegal letter! Press any key to return..."); menu(); getchar(); } 第二部分 数据结构篇 X 112299 } } 归纳注释 程序运行时,首先调用创建函数 creat(),根据给定的长度建立了一个关于学生信息的链表; 然后通过插入函数 InsertList()将指定的学生信息从链表的头部插入到链表中;随后要求输入要从 链表中删除的学生信息,调用 search()函数查询链表中是否有指定的学生信息,如果找到就调用删 除子函数 Deletelist()删除链表中的相关项;为了查看操作的效果,可以随时选择打印输出链表; 最后主函数调用退出函数 Quit()退出整个程序。 实例 51 双链表 实例说明 本实例实现通过双链表结构来查找学生的信息。程序运行时,要求输入学生信息,按顺序显 示出来,并可按姓名查找。程序运行结果如图 51-1 所示。 图 51-1 实例 51 程序运行结果 实例解析 双(向)链表中有两条方向不同的链,即每个结点中除 next 域存放后继结点地址外,还增加 一个指向其直接前趋的指针域 prior。这样在查找过程中,当指针处在中间某个结点时不仅可以像 单链表那样向后查找,而且可以返回之前找过的结点。这样的结构对于需要无规律反复读取链表 元素的操作是很有用的。 程序代码 【程序 51】 双链表 #include 113300 W C 语言实例解析精粹 #define N 10 typedef struct node { char name[20]; struct node *llink,*rlink; }stud;/*双链表的结构定义*/ /*双链表的创建*/ stud * creat(int n) { stud *p,*h,*s; int i; if((h=(stud *)malloc(sizeof(stud)))==NULL) { printf("cannot find space!\n"); exit(0); } h->name[0]='\0'; h->llink=NULL; h->rlink=NULL; p=h; for(i=0;irlink=s; printf("Please input the %d man's name: ",i+1); scanf("%s",s->name); s->llink=p; s->rlink=NULL; p=s; } h->llink=s; p->rlink=h; return(h); } /* 其他函数见光盘*/ 归纳注释 程序运行后,首先在创建过程中输入链表所需信息;接着调用子函数 print()把双链表的信息 打印输出;然后根据用户输入的学生姓名调用子函数 search()在建成的链表中查找对应节点。如果 这个节点存在,就调用删除子函数 delete()进行删除;最后再次调用输出子函数 print()将修改后的 双链表信息打印输出。 实例 52 二叉树遍历 实例说明 实现二叉树的建立和遍历。程序运行结果如图 52-1 所示。 第二部分 数据结构篇 X 113311 图 52-1 实例 52 运行结果 实例解析 树结构的形式在客观世界中是大量存在的,例如家谱、行政组织机构都可用树结构形象地表示。 树结构在计算机领域中也有着广泛的应用。例如在编译程序中,可以用树来表示源程序的 语法结构;在数据库系统中,可以用树来组织信息;在分析算法的行为时,可以用树来描述其 执行过程。 二叉树(Binary Tree)是树形结构的一个特殊类型。许多实际问题抽象出来的数据往往是二 叉树的形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为 简单,因此二叉树显得特别重要。 二叉树是 n(n≥0)个结点的一个有限集,它或者是空集(n=0),或者由一个根结点及两棵互不 相交的、分别称做这个根的左子树和右子树的二叉树组成。对二叉树上的节点访问有 3 种顺序, 即先序遍历、中序遍历和后序遍历。这 3 种遍历方法都是通过递归实现的,具体的算法可参考有 关的书籍。 程序代码 【程序 52】 二叉树的遍历 #include typedef struct bitnode { /* 定义二叉树 */ char data; struct bitnode *lchild, *rchild; }bitnode, *bitree; void visit(e) /* 访问树枝 */ bitnode *e; { printf(" Address: %o, Data: %c, Left Pointer: %o, Right Pointer: %o\n",e,e->data,e->lchild,e->rchild); 113322 W C 语言实例解析精粹 } void preordertraverse(t) /* 遍历二叉树 */ bitnode *t; { if(t) { visit(t); preordertraverse(t->lchild); preordertraverse(t->rchild); return ; }else return ; } .../* 其他程序见光盘 */ 归纳注释 在程序中,首先主函数通过调用子函数 creatbitree()创建了一个二叉树;接着对该二叉树进行 了中序遍历(调用子函数 preordertraverse()),输出其各个结点的内容;然后调用子函数 countleaf() 对该二叉树的叶子结点进行了计数,从而实现了对二叉树的基本操作。有兴趣的读者可以模仿此 例编写出二叉树的先序遍历和后序遍历程序。 实例 53 浮点数转换为字符串 实例说明 本实例通过数字金额转换为大写金额的程序来介绍将浮点数转换为字符串的方法。程序运行 结果如图 53-1 所示(此程序是用 Visual C++6.0 编译器编译运行的结果)。 图 53-1 实例 53 运行结果 实例解析 首先将用户输入的金额转换成“分”,即 扩 大 100 倍,然后逐位判断是否每一位为零。在不是 零的情况下,找出对应的货币单位和数量,从而生成相应的字符串。 第二部分 数据结构篇 X 113333 程序代码 【程序 53】 浮点数转换为字符串(数字金额转换为大写金额) //本实例源码参见光盘 归纳注释 在本例中,使用了 sprintf()函数将浮点数转换成字符串,在 C 语言中,sprintf()函数的定义如 下“int sprintf(char *buffer,const char *format [,argument]……)”,其中,第 1 个参数是转换后的字 符串。注意这个字符串一定要事先分配内存,不能是未分配内存的指针,否则将产生运行时错误。 第 2 个参数是格式控制参数,其格式与 printf()函数中的格式控制完全一样。在本例中,使用了形 如“sprintf(je,"%.01f",100*x)”这样的语句,就是将“100*x”这个浮点数按小数点后面保留两位 浮点精度的格式转换到字符串 je 中去。sprintf()函数的返回值是转换的字符个数。sprintf()会自动 在 buffer 字符串后面附加字符串结束标记符'\0',但是在返回值计数中不包含这个字符。 在使用 sprintf()函数的时候,另一个需要注意的问题是被转换的字串长度不要超过 buffer 的长 度,因为在 C 语言标准中没有定义如何处理这种情况,所以可能会发生莫名其妙的错误。 实例 54 汉诺塔问题 实例说明 汉诺塔问题的原始描述是有 3 个柱子 a,b,c,初始的时候在 a 柱上从上到下、从小到大放 了 3 个盘子 1,2,3,要求经过若干次移动后把盘子按照从上到下、从小到大的顺序放在 c 柱上。 在移动的过程中要求尺码小的盘子必须放在尺码大的盘子上面,b 柱子作为中转的柱子。后来, 汉诺塔问题扩展到了 3 个柱子,n 个盘子的情况。程序运行时,根据输入汉诺塔的层数,显示移 动的方案。程序运行结果如图 54-1 所示。 图 54-1 实例 54 汉诺塔程序运行结果 113344 W C 语言实例解析精粹 实例解析 递归策略是一个过程或函数,在其定义或说明中,又直接或间接调用自身的一种方法,这样 通常可以把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策 略只需少量的程序代码就可描述出解题过程所需要的多次重复计算,大大减少了程序的代码量。 下面首先分析汉诺塔问题的一般解法。 (1)将 a 柱子上的 n 个盘子从 a→(b)→c,可以用 b 作为过渡盘。 ① a柱上面的 n–1 个盘子(1 号~n–1 号)从 a→(c)→b。 ② 把 a 最下面的最大的那个盘子(第 n 个)从 a→c。 ③ 把 n–1 个(1 号~n–1 号)盘子从 b→(a)→c。 (2)解决步骤(1)中的②,把一个盘从一个柱子搬到另一个柱子,很容易解决。 (3)解决步骤(1)中的①。 将 a 柱上的 n–2 个(1 号~n–2 号)盘子从 a→( )→c。 把 a 柱此时最下面的最大的盘子(第 n–1 个)从 a→c。 把 n–2 个(1 号~n–2 号)盘子从 c→( )→b。 (4)解决步骤(1)中的③。 将 b 柱上的 n–2 个(1 号~n–2 号)盘子从 b→( )→a。 把 b 柱此时最下面的最大的盘子(第 n–1 个)从 b→c。 把 n–2 个(1 号~n–2 号)盘子从 a→( )→c。 …… 一直到递归的出口的条件——当盘子只有一个的时候。 程序代码 【程序 54】 汉诺塔问题 /*//////////////////////////////////////////////////////////////*/ /* 汉诺塔问题 */ /*//////////////////////////////////////////////////////////////*/ #include /* hanoil 子程序,实现将 n 个盘子从 a 移动到 c */ void hanoil(int n,char a,char b, char c) { if(n==1) /* 递归调用的出口,n=1 */ printf(" >> Move Plate No.%d from Stick %c to Stick %c.\n",n,a,c); else { hanoil(n-1,a,c,b); /* 递归调用 */ printf(" >> Move Plate No.%d from Stick %c to Stick %c.\n",n,a,c); hanoil(n-1,b,a,c); } } /****************************** 主程序******************************/ void main() { 第二部分 数据结构篇 X 113355 int n; char a='A'; char b='B'; char c='C'; clrscr(); printf("This is a hanoil program.\nPlease input number of the plates:\n"); scanf("%d",&n); if(n<=0) { puts("n must no less than 1!"); exit(1); } puts("The steps of moving plates are:"); hanoil(n,a,b,c); puts("\n Press any key to quit..."); getch(); } 归纳注释 在本例中,首先主函数定义了汉诺塔的高度,然后定义了 3 个字符型变量用来模拟汉诺塔问 题中的 3 根柱子,接着调用函数 haniol()模拟了将 n 个盘子从柱子 a 移到 c 上的过程。 实例 55 哈夫曼编码 实例说明 本实例实现哈夫曼树和哈夫曼编码的构造。程序运行后,首先生成了一个哈夫曼树,然后用 这颗哈夫曼树对所有字符进行了哈夫曼编码,并显示出来。程序运行结果如图 55-1 所示。 图 55-1 实例 55 程序运行结果 实例解析 哈夫曼编码是根据字符出现频率对数据进行编码解码,以便对文件进行压缩的一种方法,目 前大部分有效的压缩算法(比如 MP3 编码方法)都是基于哈夫曼编码的。具体的算法描述参见相 关数据结构书籍。 113366 W C 语言实例解析精粹 数据压缩过程称为编码,也就是把文件中的每个字符均转换为一个惟一的二进制位串。数据 解压过程称为解码,也就是把给定的二进制位字符串转换为对应的字符。 程序代码 【程序 55】 哈夫曼编码 #include #define MAX 1000 #define MAXSYMBS 30 #define MAXNODE 59 typedef struct { int weight; int flag; int parent; int lchild; int rchild; }huffnode; typedef struct { int bits[MAXSYMBS]; int start; }huffcode; main() { huffnode huff_node[MAXNODE]; huffcode huff_code[MAXSYMBS],cd; int i,j,m1,m2,x1,x2,n,c,p; /* char symbs[MAXSYMBS],symb; */ /*数组 huff_node 初始化*/ clrscr(); printf("please input the leaf num of tree:\n"); scanf("%d",&n); for(i=0;i<2*n-1;i++) { huff_node[i].weight=0; huff_node[i].parent=0; huff_node[i].flag=0; huff_node[i].lchild=-1; huff_node[i].rchild=-1; } printf("please input the weight of every leaf\n"); for(i=0;i #include struct node /* 图顶点结构定义 */ { int vertex; /* 顶点数据信息 */ struct node *nextnode; /* 指向下一顶点的指针 */ }; typedef struct node *graph; /* 图形的结构 */ struct node head[9]; /* 图形顶点数组 */ int visited[9]; /* 遍历标记数组 */ .../*其他程序见光盘*/ /********************** 图的深度优先搜寻法********************/ void dfs(int current) { graph ptr; visited[current] = 1; /* 记录已遍历过 */ printf("vertex[%d]\n",current); /* 输出遍历顶点值 */ ptr = head[current].nextnode; /* 顶点位置 */ while ( ptr != NULL ) /* 遍历至链表尾 */ { if ( visited[ptr->vertex] == 0 ) /* 如果没遍历过 */ dfs(ptr->vertex); ptr = ptr->nextnode; /* 下一个顶点 */ } } 归纳注释 本实例先用一个二维数组 node[20][2]存放图的信息,再调用函数 creategraph(int node[20][2],int num)将它转化成邻接表的形式,接着调用函数 dfs(int current)对图形进行深度优先遍历;函数 dfs() 在遍历的同时把整个邻接表中各个节点的信息输出到屏幕。 实例 57 图的广度优先遍历 实例说明 本实例实现图的广度优先遍历,程序先在屏幕上显示了整个图的邻接表结构,然后输出图按 照广度优先顺序遍历的结果。程序运行结果如图 57-1 所示。 114400 W C 语言实例解析精粹 图 57-1 实例 57 程序运行结果 实例解析 图的广度优先遍历的基本思想:从图中某个顶点 v1 出发,访问了 v1 之后依次访问 v1 的所有 邻接点;然后分别从这些邻接点出发按深度优先搜索递归访问图的其他顶点,直到所有顶点都被 访问到。它类似于树的按层次遍历,其特点是尽可能优先对横向搜索,故称之为广度优先搜索。 程序代码 【程序 57】 图的广度优先遍历(只给出了核心代码) /*//////////////////////////////////////////*/ /* 图的广度优先搜寻法 */ /* ///////////////////////////////////////*/ #include #include #define MAXQUEUE 10 /* 队列的最大容量 */ ... /* 其他程序见光盘 */ /*********************** 图的广度优先遍历************************/ void bfs(int current) { graph ptr; /* 处理第一个顶点 */ enqueue(current); /* 将顶点存入队列 */ visited[current] = 1; /* 已遍历过记录标志置 1*/ printf(" Vertex[%d]\n",current); /* 打印输出遍历顶点值 */ while ( front != rear ) /* 队列是否为空 */ { current = dequeue(); /* 将顶点从队列取出 */ ptr = head[current].nextnode; /* 顶点位置 */ while ( ptr != NULL ) /* 遍历至链表尾 */ { if ( visited[ptr->vertex] == 0 ) /*顶点没有遍历过*/ { enqueue(ptr->vertex); /* 将定点放入队列 */ visited[ptr->vertex] = 1; /* 置遍历标记为 1 */ 第二部分 数据结构篇 X 114411 printf(" Vertex[%d]\n",ptr->vertex);/* 打印遍历顶点值 */ } ptr = ptr->nextnode; /* 下一个顶点 */ } } } 归纳注释 与实例 56 相似,程序先利用一个二维数组 node[20][2]存放图形的信息,再通过调用函数 creategraph(int node[20][2],int num)把它转化成邻接表的形式;接着调用函数 bfs(int current)对图形 进行广度优先遍历;函数 bfs()在遍历的同时将邻接表的信息输出到屏幕。 实例 58 求解最优交通路径 实例说明 程序中预置了部分城市间的距离,然后由用户选择两个城市,计算出两城市之间的最短距离 以及相应的路线。程序运行结果如图 58-1 所示。此程序可在 Visual C++中编译(见光盘)。 图 58-1 实例 58 程序运行结果 实例解析 运行该程序后,首先调用 CreateUDN()函数生成一个拥有 25 个城市,30 条公路的交通图。然 后调用 narrate()函数输出提示信息,提示用户输入出发城市和终到城市的序号,再调用 ShortestPath()函数求出最短路径,由 Output()函数输出结果,最终退出 main()函数,程序结束。 CreateUDN()函数根据用户输入的顶点数和边数生成一个相应的交通图,其中顶点对应于城 市,边对应于城市间的直接通路,为了便于 ShortestPath()函数的计算,在生成的交通图中所有的 通路都是双向的,即如果 A 城市到 B 城市有直接通路,且里程为 K 千米,则 B 城市到 A 城市也 有直接通路,并且里程同样为 K 千米。 114422 W C 语言实例解析精粹 narrate()函数把能够进行计算的城市列表按简单的格式进行输出。 ShortestPath()利用了图论中的最短路径算法,有兴趣的读者可以参考离散数学或数据结构等 书,查阅其中的 Dijkstra 算法和 Floyd 算法。 Output()函数把计算的结果格式化输出。 程序代码 【程序 58】 求解最优交通路径(只给出核心代码) /*交通图最短路径程序*/ #include "string.h" #include "stdio.h" typedef struct ArcCell{ int adj; /*相邻接的城市序号*/ }ArcCell; /*定义边的类型*/ typedef struct VertexType{ int number; /*城市序号*/ char *city; /*城市名称*/ }VertexType; /*定义顶点的类型*/ typedef struct{ VertexType vex[25]; /*图中的顶点,即为城市*/ ArcCell arcs[25][25]; /*图中的边,即为城市间的距离*/ int vexnum,arcnum; /*顶点数,边数*/ }MGraph; /*定义图的类型*/ MGraph G; /*把图定义为全局变量*/ int P[25][25]; long int D[25]; /* 其他函数见光盘 */ void ShortestPath(num) /*最短路径函数*/ int num; { int v,w,i,t; int final[25]; int min; for(v=0;v<25;++v) { final[v]=0;D[v]=G.arcs[num][v].adj; for(w=0;w<25;++w) P[v][w]=0; if(D[v]<20000) {P[v][num]=1;P[v][v]=1;} } D[num]=0;final[num]=1; for(i=0;i<25;++i) { min=20000; for(w=0;w<25;++w) if(!final[w]) if(D[w]data 来访问 data 域。在本例中还用到了匿名 结构的声明方法,即使用“typedef struct{……}ObjName;”这种声明方法。在匿名结构中,只能 声明一个实例,以后再也不能声明该类型的实例。 实例 59 八皇后问题 实例说明 八皇后问题是指求解如何在国际象棋 8×8 棋盘上无冲突地放置八个皇后棋子。因为在国际象 棋里,皇后的移动方式是横竖交叉,所以在任意一个皇后所在位置的水平、竖直和斜 45 度线上都 不能有其他皇后棋子的存在。一个完整无冲突的八皇后棋子分布称为八皇后问题的一个解。本实 例实现了八枚皇后棋子在 8×8 棋盘上无冲突的放置。程序运行结果如图 59-1 所示。此程序可在 Visual C++中编译(见光盘)。 图 59-1 实例 59 程序运行结果 114444 W C 语言实例解析精粹 实例解析 本实例使用了回溯的方法来解决八皇后问题,也就是逐次试探的方法。这个方法通过函数 putchess()对自身的递归调用来实现。运行程序后,主函数调用 putchess()函数在棋盘第一行第一列上 放置棋子,开始向下一行递归。每一步递归中,首先检测待放位置有没有冲突出现。如果没有冲突 就放下棋子,并进入下一行递归,否则检测该行的下一个位置。如果整个一行中都没有可以放置的 位置,就退回上一层递归。最后,如果本次放置成功,并且递归调用深度为 7,就打印输出结果。 程序代码 【程序 59】八皇后问题 #include #include #define MAX 8 /* 棋子数及棋盘大小 MAX×MAX */ int board[MAX]; void show_result() { int i; for(i=0;i int f[11][11] ; /*定义一个矩阵来模拟棋盘*/ int adjm[121][121];/*标志矩阵,即对于上述棋盘,依次进行编号 1~121(行优先) */ void creatadjm(void); /*创建标志矩阵函数声明*/ void mark(int,int,int,int); /*将标志矩阵相应位置置 1*/ void travel(int,int); /*巡游函数声明*/ int n,m; /*定义矩阵大小及标志矩阵的大小*/ /******************************主函数***********************************/ int main() { int i,j,k,l; printf("Please input size of the chessboard: "); /*输入矩阵的大小值*/ scanf("%d",&n); m=n*n; creatadjm(); /*创建标志矩阵*/ puts("The sign matrix is:"); for(i=1;i<=m;i++) /*打印输出标志矩阵*/ { for(j=1;j<=m;j++) printf("%2d",adjm[i][j]); printf("\n"); } printf("Please input the knight's position (i,j): "); /*输入骑士的初始位置*/ scanf("%d %d",&i,&j); l=(i-1)*n+j; /*骑士当前位置对应的标志矩阵的横坐标*/ while ((i>0)||(j>0)) /*对骑士位置进行判断*/ { for(i=1;i<=n;i++) /*棋盘矩阵初始化*/ for(j=1;j<=n;j++) f[i][j]=0; k=0; /*所跳步数计数*/ travel(l,k); /*从 i,j 出发开始巡游*/ puts("The travel steps are:"); 第二部分 数据结构篇 X 114477 for(i=1;i<=n;i++) /*巡游完成后输出巡游过程*/ { for(j=1;j<=n;j++) printf("%4d",f[i][j]); printf("\n"); } printf("Please input the knight's position (i,j): ");/*为再次巡游输入起始位置*/ scanf("%d %d",&i,&j); l=(i-1)*n+j; } puts("\n Press any key to quit... "); getch(); return 0; } /*****************************创建标志矩阵子函数*************************/ void creatadjm() { int i,j; for(i=1;i<=n;i++) /*巡游矩阵初始化*/ for(j=1;j<=n;j++) f[i][j]=0; for(i=1;i<=m;i++) /*标志矩阵初始化*/ for(j=1;j<=m;j++) adjm[i][j]=0; for(i=1;i<=n;i++) for(j=1;j<=n;j++) if(f[i][j]==0) /*对所有符合条件的标志矩阵中的元素置 1*/ { f[i][j]=1; if((i+2<=n)&&(j+1<=n)) mark(i,j,i+2,j+1); if((i+2<=n)&&(j-1>=1)) mark(i,j,i+2,j-1); if((i-2>=1)&&(j+1<=n)) mark(i,j,i-2,j+1); if((i-2>=1)&&(j-1>=1)) mark(i,j,i-2,j-1); if((j+2<=n)&&(i+1<=n)) mark(i,j,i+1,j+2); if((j+2<=n)&&(i-1>=1)) mark(i,j,i-1,j+2); if((j-2>=1)&&(i+1<=n)) mark(i,j,i+1,j-2); if((j-2>=1)&&(i-1>=1)) mark(i,j,i-1,j-2); } return; } /*********************************巡游子函数*******************************/ void travel(int p,int r) { int i,j,q; for(i=1;i<=n;i++) for(j=1;j<=n;j++) if(f[i][j]>r) f[i][j]=0; /*棋盘矩阵的置大于 r 时,置 0*/ r=r+1; /*跳步计数加 1*/ 114488 W C 语言实例解析精粹 i=((p-1)/n)+1; /*还原棋盘矩阵的横坐标*/ j=((p-1)%n)+1; /*还原棋盘矩阵的纵坐标*/ f[i][j]=r; /*将 f[i][j]做为第 r 跳步的目的地*/ for(q=1;q<=m;q++) /*从所有可能的情况出发,开始进行试探式巡游*/ { i=((q-1)/n)+1; j=((q-1)%n)+1; if((adjm[p][q]==1)&&(f[i][j]==0)) travel(q,r); /*递归调用自身*/ } return; } /*************************赋值子函数***************************************/ void mark(int i1,int j1,int i2,int j2) { adjm[(i1-1)*n+j1][(i2-1)*n+j2]=1; adjm[(i2-1)*n+j2][(i1-1)*n+j1]=1; return; } 第第三三部部分分 数数值值计计算算 与与趣趣味味数数学学篇篇 精彩导读 验证歌德巴赫猜想 求π的近似值 堆栈四则运算 绘制彩色抛物线 求解线性/非线性方程 求定积分 115500 W C 语言实例解析精粹 实例 61 绘制余弦曲线和直线的迭加 实例说明 在屏幕上显示余弦曲线和直线的迭加图形。程序运行效果如图 61-1 所示。 图 61-1 实例 61 程序运行效果 实例解析 在屏幕上显示 0~360°的 cos(x)曲线与直线 f(x)=45× (y−1)+31 的迭加图形。其中 cos(x)图形用 “*”表示,f(x)用“+”表示,在两个图形相交的点上则用 f(x)图形的符号。 如果在程序中使用数组,这个问题则变得十分简单。但若规定不能使用数组,问题就变得不 容易解决了。问题的关键在于余弦曲线在 0~360°的区间内,一行中要显示两个点,而对一般的 显示器来说,只能按行输出,即输出第一行信息后,只能向下一行输出,不能再返回到上一行。 为了获得要求的图形就必须在一行中一次输出两个“*”。 为了同时得到余弦函数 cos(x)图形在一行上的两个点,考虑利用 cos(x)的左右对称性。将屏幕 的行方向定义为 x,列方向定义为 y,则 0~180°的图形与 180~360°的图形是左右对称的,若定 义图形的总宽度为 62 列,计算出 x 行 0~180°时 y 点的坐标 m,那么在同一行与之对称的 180~360 度的 y 点的坐标就应为 62−m。程序中利用反余弦函数 arcos 计算坐标(x,y)的对应关系。使用这 种方法编出的程序短小精炼,体现了一定的技巧。 程序代码 【程序 61】 绘制余弦曲线与直线的迭加 //本实例源码参见光盘 归纳注释 图形迭加的关键是分别计算出同一行中两个图形的列方向点坐标后,正确判断相互的位置关 系。为此,可以先判断图形的交点,再分别控制打印两个不同的图形。 第三部分 数值计算与趣味数学篇 X 115511 实例 62 计算高次方数的尾数 实例说明 计算形如 1313 高次方的计算结果的最后 3 个尾数。程序运行效果如图 62-1 所示。 图 62-1 实例 62 程序运行效果 实例解析 解本题最直接的方法是将 13 累乘 13 次,截取最后 3 位即可。但是由于计算机所能表示的整 数范围有限,用这种“正确”的算法不可能得到结果。事实上,题目仅要求最后 3 位的值,完全 没有必要求 13 的 13 次方的完整结果。 研究乘法的规律发现,乘积的最后 3 位的值只与乘数和被乘数的后 3 位有关,与乘数和被乘 数的高位无关。利用这一规律,可以大大简化程序。 程序代码 【程序 62】 计算高次方数的尾数 //本实例源码参见光盘 归纳注释 在进行数学运算时必须注意计算机所能表示的各种类型数的范围,超出该范围,计算结果将 会有很大变化,甚至是错误的。 实例 63 打鱼还是晒网 实例说明 中国有句俗语叫“三天打鱼两天晒网”。某人从 1990 年 1 月 1 日起开始“三天打鱼两天晒网”, 问这个人在以后的某一天中是“打鱼”还是“晒网”。程序运行效果如图 63-1 所示。此程序可在 Visual C++中编译(见光盘)。 115522 W C 语言实例解析精粹 图 63-1 实例 63 程序运行效果 实例解析 根据题意可以将解题过程分为 3 步: (1)计算从 1990 年 1 月 1 日开始至指定日期共有多少天; (2)由于“打鱼”和“晒网”的周期为 5 天,所以将计算出的天数用 5 去除; (3)根据余数判断他是在“打鱼”还是在“晒网”; 若余数为 1,2,3,则他是在“打鱼”,否则是在“晒网”。 在这 3 步中,关键是第 1 步。求从 1990 年 1 月 1 日至指定日期有多少天,要判断所经历的年 份中是否有闰年。闰年的判定方法可以用伪语句描述如下: 如果 ((年能被 4 除尽 且 不能被 100 除尽)或 能被 400 除尽) 则 该年是闰年; 否则 不是闰年。 C 语言中判断能否整除可以使用求余运算(即求模)。 程序代码 【程序 63】 打鱼还是晒网 #include struct date{ int year; int month; int day; }; int days(struct date day); void main() { struct date today,term; int yearday,year,day; puts("◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇"); 第三部分 数值计算与趣味数学篇 X 115533 puts("◇ 打鱼还是晒网 ◇"); puts("◇ 中国有句俗语叫【三天打鱼两天晒网】。 ◇"); puts("◇某人 20 岁从 1990 年 1 月 1 日起开始【三天打鱼两天晒网】,◇"); puts("◇问这个人在以后的某一天中是【打鱼】还是【晒网】? ◇"); puts("◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇\n"); while(1) { printf(" >> 请输入年/月/日【输入 1990 1 1 退出】:"); scanf("%d%d%d",&today.year,&today.month,&today.day); /*输入日期*/ if(today.year<1990) { if(today.year<1970) puts(" >> 对不起,那一年那还没出生呢!按任意键继续..."); else puts(" >> 对不起,那一年他还没开始打鱼呢!按任意键继续..."); getch(); continue; } if(today.year==1990&&today.month==1&&today.day==1) break; term.month=12; /*设置变量的初始值:月*/ term.day=31; /*设置变量的初始值:日*/ for(yearday=0,year=1990;year0&&day<4) printf(" >> %d 年%d 月%d 日,他正在打鱼。\n",today.year,today.month,today.day); /*打印结果*/ else printf(" >> %d 年%d 月%d 日,他正在晒网。\n",today.year,today.month,today.day); } puts("\n >> 请按任意键退出..."); getch(); } int days(struct date day) { static int day_tab[2][13]= {{0,31,28,31,30,31,30,31,31,30,31,30,31,}, /*平均每月的天数*/ {0,31,29,31,30,31,30,31,31,30,31,30,31,}, }; int i,lp; lp=day.year%4==0&&day.year%100!=0||day.year%400==0; /*判定 year 为闰年还是平年,lp=0 为平年,非 0 为闰年*/ for(i=1;i 第三部分 数值计算与趣味数学篇 X 115599 void main() { int n,a,b; clrscr(); puts("=========================================================="); puts("|| This program will find the four figures which have ||"); puts("|| the characteristic as follows: abcd=(ab+cd)^2. ||"); puts("|| e.g., 3025=(30+25)*(30+25). ||"); puts("=========================================================="); printf("\n >> There are following numbers with satisfied condition:\n\n"); for(n=1000;n<10000;n++) /*4 位数 N 的取值范围 1000~9999*/ { a=n/100; /*截取 N 的前两位数存于 a*/ b=n%100; /*截取 N 的后两位存于 b*/ if((a+b)*(a+b)==n) /*判断 N 是否为符合题目所规定性质的 4 位数*/ printf(" %d ",n); } puts("\n\n >> Press any key to quit..."); getch(); } 实例 69 验证歌德巴赫猜想 实例说明 验证 2000 以内的正偶数都能够分解为两个素数之和(即验证歌德巴赫猜想对 2000 以内的正 偶数成立)。程序运行结果如图 69-1 所示。 图 69-1 实例 69 程序运行结果 116600 W C 语言实例解析精粹 图 69-2 实例 69 程序运行结果 实例解析 为了验证歌德巴赫猜想对 2000 以内的正偶数都是成立的,要将整数分解为两部分,然后判断 分解出的两个整数是否均为素数。若是,则满足题意;否则重新进行分解和判断。 程序代码 【程序 69】 验证歌德巴赫猜想 #include #include int fflag(int n); void main() { int i,j,n; long max; clrscr(); puts("============================================================"); puts("|| This program will verify the Goldbach Guess. ||"); puts("|| That is any positive even number can be broken up into ||"); puts("|| the sum of two prime numbers. ||"); puts("|| e.g., 4=2+2, 6=3+3, 8=3+5, 10=3+7, 12=5+7,... ||"); puts("============================================================"); printf("\n >> Please input the scale n you want to verify : "); scanf("%ld",&max); printf("\n >> Now the program starts to verify the even number\n"); printf(" >> less than %ld equals to sum of two prime numbers.\n\n",max); for(i=4,j=0;i<=max;i+=2) { for(n=2;n> Press any key to quit..."); getch(); } int fflag(int i) /*判断是否为素数*/ { int j; if(i<=1)return 0; if(i==2)return 1; if(!(i%2))return 0; /*if no,return 0*/ for(j=3;j<=(int)(sqrt((double)i)+1);j+=2) if(!(i%j))return 0; return 1; /*if yes,return 1*/ } 归纳注释 程序中对判断是否为素数的算法进行了改进,对整数判断“用从 2 开始到该整数的一半”改 为“2 开始到该整数的平方根”。 实例 70 素数幻方 实例说明 求四阶的素数幻方。即在一个 4×4 的矩阵中,每一个格填入一个数字,使每一行、每一列和 两条对角线上的 4 个数字所组成的 4 位数,均为可逆素数。程序运行结果如图 70-1 所示。 图 70-1 实例 70 程序运行结果 116622 W C 语言实例解析精粹 实例解析 最简单的算法是穷举法,设定 4×4 矩阵中每一个元素的值后,判断每一行、每一列和两条对 角线上的 4 个数字组成的 4 位数是否都是可逆素数,若是,则求出了满足题意的一个解。 这种算法在原理是对的,也一定可以求出满足题意的全部解。但是,按照这一思路编出的 程序效率很低,在计算机上运行几个小时也不会结束。这一算法致命的缺陷是要穷举和判断的 情况过多。 充分利用题目中的“每一个 4 位数都是可逆素数”这一条件,可以放弃对矩阵中每个元素进 行的穷举的算法,先求出全部的 4 位可逆素数共(204 个),以矩阵的行为单位,在 4 位可逆素数 的范围内进行穷举,然后将穷举的 4 位整数分解为数字后,再进行列和对角线方向的条件判断, 改进的算法与最初的算法相比,大大地减少了穷举的次数。 考虑矩阵的第一行和最后一行数字,它们分别是列方向 4 位数的第一个数字和最后一个数 字,由于这些 4 位数也必须是可逆素数,所以矩阵的每一行和最后一行中的各个数字都不能为 偶数或 5。这样穷举矩阵的第一行和最后一行时,它们的取值范围是:所有位的数字均不是偶 数或 5 的 4 位可逆数。由于符合这一条件的 4 位可逆素数很少,所以这一限制又一次减少了穷 举的次数。 对算法的进一步研究会发现:当设定了第一和第二行的值后,就已经可以判断出当前的这 种组合是否一定是错误的(尚不能肯定该组合一定是正确的)。若按列方向上的 4 个两位数与 4 位可逆数的前两位矛盾(不是其中的一种组合),则第一、二行的取值一定是错误的。同理在 设定了前 3 行数据后,可以立刻判断出当前的这种组合是否一定是错误的,若判断出矛盾情况, 则可以立刻设置新的一组数据。这样就可以避免将 4 个数据全部设定好以后再进行判断所造成 的低效。 根据以上分析,可以用伪语言描述以上改进的算法: 开始 找出全部 4 位的可逆素数; 确定全部出现在第一和最后一行的 4 位可逆素数; 在指定范围内穷举第一行 在指定范围内穷举第二行 若第一、第二、第三行已出现矛盾,则继续穷举下一个数; 在指定范围内穷举第 4 行 判断列和对角方向是否符合题意 若符合题意,则输出矩阵; 否则继续穷举下一个数; 结束 程序代码 【程序 70】 素数幻方 //本实例源码参见光盘 归纳注释 本实例中采用了很多程序设计技巧,设置若干辅助数组,其目的就是要最大限度的提高程序 的执行效率,缩短运行时间。因此,该程序运行效率是比较高的。 第三部分 数值计算与趣味数学篇 X 116633 实例 71 百钱百鸡问题 实例说明 中国古代数学家张丘建在他的《算经》中提出了著名的“百钱买百鸡问题”:鸡翁一,值钱五,鸡 母一,值钱三,鸡雏三,值钱一,百钱买百鸡,问翁、母、雏各几何?程序运行效果如图 71-1 所示。 图 71-1 实例 71 程序运行效果 实例解析 设鸡翁、鸡母、鸡雏的个数分别为 x,y,z,题意给定共 100 钱要买百鸡,若全买公鸡最多买 20 只,显然 x 的值在 0~20 之间;同理,y 的取值范围在 0~33 之间,可得到下面的不定方程: 5x+3y+z/3=100 x+y+z=100 所以此问题可归结为求不定方程的整数解。 由程序设计实现不定方程的求解与手工计算不同。在分析确定方程中未知数变化范围的前提 下,可通过对未知数可变范围的穷举,验证方程在什么情况下成立,从而得到相应的解。 程序代码 【程序 71】 百钱百鸡问题 #include void main() { int x,y,z,j=0; clrscr(); puts("************************************************"); puts("* This program is to solve Problem of *"); puts("* Hundred Yuan Hundred Fowls. *"); puts("* Which is presented by Zhang Qiujiang, *"); puts("* a Chinese ancient mathematician, in his work *"); puts("* Bible of Calculation: 5 Yuan can buy 1 cock, *"); 116644 W C 语言实例解析精粹 puts("* 3 Yuan can buy 1 hen, 1 Yuan buy 3 chickens, *"); puts("* now one has 100 Yuan to buy 100 fowls, the *"); puts("* question is how many cocks, hens, chickens *"); puts("* to buy? *"); puts("************************************************"); printf("\n The possible plans to buy 100 fowls with 100 Yuan are:\n\n"); for(x=0;x<=20;x++) /*外层循环控制鸡翁数*/ for(y=0;y<=33;y++) /*内层循环控制鸡母数,y 在 0~33 变化*/ { z=100-x-y; /*内外层循环控制下,鸡雏数 z 的值受 x,y 的值的制约*/ if(z%3==0&&5*x+3*y+z/3==100) /*验证取 z 值的合理性及得到一组解的合理性*/ printf("%2d: cock=%2d hen=%2d chicken=%2d\n",++j,x,y,z); } puts("\n Press any key to quit..."); getch(); } 归纳注释 百钱百鸡问题是经典的数学问题,在用计算机求解这类问题时,可设定相应变量的范围,并 采用穷举法一一验证,找出符合条件的解。 实例 72 爱因斯坦的数学题 实例说明 爱因斯坦出了一道这样的数学题:有一条长阶梯,若每步跨 2 阶,则最后剩一阶,若每步跨 3 阶, 则最后剩 2 阶,若每步跨 5 阶,则最后剩 4 阶,若每步跨 6 阶则最后剩 5 阶。只有每次跨 7 阶,最后 才正好一阶不剩。请问这条阶梯共有多少阶?(求取得最小解)程序运行效果如图 72-1 所示。 图 72-1 实例 72 程序运行效果 实例解析 根据题意,阶梯数满足下面一组同余式: 第三部分 数值计算与趣味数学篇 X 116655 x≡1 (mod2) x≡2 (mod3) x≡4 (mod5) x≡5 (mod6) x≡0 (mod7) 通过程序对大于 1 的自然数一一进行判别,即可找到最小的解。 程序代码 【程序 72】 爱因斯坦的数学题 //本实例源码参见光盘 归纳注释 该题的解可能不止一个,程序所求取的只是最小的一个解。例如,可采用如下语句求取小于 10000 的解: for(i=0;i<10000;i++) if((i%2==1)&&(i%3==2)&&(i%5==4)&&(i%6==5)&&(i%7==0)) printf(“%d ”,i); 求出的解有:119,329,539,749,959 等。 实例 73 三色球问题 实例说明 若一个口袋中放有 12 个球,其中有 3 个红的,3 个白的和 6 个黒的,问从中任取 8 个共有多 少种不同的颜色搭配?程序运行效果如图 73-1 所示。 图 73-1 实例 73 程序运行效果 116666 W C 语言实例解析精粹 实例解析 设任取的红球个数为 i,白球个数为 j,则黒球个数为 8−i−j,根据题意红球和白球个数的取值 范围是 0~3,在红球和白球个数确定的条件下,黒球个数取值应为 8−i−j≤6。 程序代码 【程序 73】三色球问题 #include void main() { int i,j,count=0; clrscr(); puts("****************************************************************"); puts("* This program is to solve Problem of Three Color Ball. *"); puts("* The Problem is as follows: There are 12 balls in the pocket. *"); puts("* Amony them, 3 balls are red,3 balls are white and 6 balls *"); puts("* are black. Now take out any 8 balls from the pocket,how *"); puts("* many color combinations are there? *"); puts("****************************************************************"); puts(" >> The solutions are:"); printf(" No. RED BALL WHITE BALL BLACK BALL\n"); printf("-----------------------------------------------------\n"); for(i=0;i<=3;i++) /*循环控制变量 i 控制任取红球个数 0~3*/ for(j=0;j<=3;j++) /*循环控制变量 j 控制任取白球个数 0~3*/ if((8-i-j)<=6) printf(" %2d | %d | %d | %d\n",++count,i,j,8-i-j); printf("-----------------------------------------------------\n"); printf(" Press any key to quit..."); getch(); } 归纳注释 类似于三色球问题是数学中的排列(取出的先后顺序有区别)、组合(取出的先后顺序无区别) 问题,排列问题(从 n 个中取出 m 个,顺序有区别)的解是 )1()1( +−−= mnnnP m n Κ 个,组合问 题(从 n 个中取出 m 个,顺序无区别)的解是 !/)1()1( mmnnnC m n +−−=Κ 个。 实例 74 马克思手稿中的数学题 实例说明 马克思手稿中有一道趣味数学问题:有 30 个人,其中有男人、女人和小孩,在一家饭馆吃饭 第三部分 数值计算与趣味数学篇 X 116677 花了 50 先令;每个男人花 3 先令,每个女人花 2 先令,每个小孩花 1 先令。问男人、女人和小孩 各有几人?程序运行效果如图 74-1 所示。 图 74-1 实例 74 程序运行效果 实例解析 设 x,y,z 分别代表男人、女人和小孩。按题目的要求,可得到下面的方程: x+y+z=30 (1) 3x+2y+z=50 (2) 用程序求此不定方程的非负整数解,可先通过(2)−(1)式得: 2x+y=20 (3) 由(3)式可知,x 变化范围是 0~10。 程序代码 【程序 74】 马克思手稿中的数学题 //本实例源码参见光盘 归纳注释 该实例相当于求解具有边界条件的线性不定方程组。 实例 75 配对新郎和新娘 实例说明 3 对情侣参加婚礼,3 个新郞为 A、B、C,3 个新娘为 X、Y、Z。有人不知道谁和谁结婚, 116688 W C 语言实例解析精粹 于是询问了 6 位新人中的 3 位,但听到的回答是这样的:A 说他将和 X 结婚;X 说她的未婚夫是 C;C 说他将和 Z 结婚。这人听后知道他们在开玩笑,全是假话。请编程找出谁将和谁结婚。程 序运行效果如图 75-1 所示。 图 75-1 实例 75 程序运行效果 实例解析 将 A、B、C 3 人用 1,2,3 表示,将 X 和 A 结婚表示为“X=1”,将 Y 不与 A 结婚表示为“Y!=1”。 按照题目中的叙述可以写出表达式: X!=1 A不与 X 结婚 X!=3 X的未婚夫不是 C Z!=3 C不与 Z 结婚 题意还隐含着 X、Y、Z 3 个新娘不能结为配偶,则有: X!=Y且 X!=Z 且 Y!=Z 穷举以上所有可能的情况,代入上述表达式中进行推理运算,若假设的情况使上述表达式的 结果均为真,则假设情况就是正确的结果。 程序代码 【程序 75】 新郎和新娘 //本实例源码参见光盘 归纳注释 这类问题通常是通过逻辑变量(非真即假)的运算来求取的。 实例 76 约瑟夫问题 实例说明 这是 17 世纪的法国数学家加斯帕在《数目的游戏问题》中讲的一个故事:15 个教徒和 15 个 第三部分 数值计算与趣味数学篇 X 116699 非教徒在深海上遇险,必须将一半的人投入海中,其余的人才能幸免于难,于是想了一个办法:30 个人围成一圆圈,从第一个人开始依次报数,每数到第九个人就将他扔入大海,如此循环进行直到 仅余 15 个人为止。问怎样排法,才能使每次投入大海的都是非教徒。程序运行效果如图 76-1 所示。 图 76-1 实例 76 程序运行效果 实例解析 约瑟夫问题并不难,但求解的方法很多,题目的变化形式也很多。这里只给出一种实现方法。 题目中 30 个人围成一圈,因而启发我们用一个循环的链来表示。可以使用结构数组来构成一 个循环链。结构中有两个成员,其一为指向下一个人的指针,以构成环形的链;其二为该人是否 被扔下海的标记,为 1 表示还在船上。从第一个人开始对还未扔下海的人进行计数,每数到 9 时, 将结构中的标记改为 0,表示该人已被扔下海了。这样循环计数直到有 15 个人被扔下海为止。 程序代码 【程序 76】约瑟夫问题 //本实例源码参见光盘 归纳注释 本程序采用链表环来求解。这类问题也是经典问题,类似的还有海盗分金子等。 实例 77 邮票组合 实例说明 某人有 4 张 3 分的邮票和 3 张 5 分的邮票,用这些邮票中的一张或若干张可以得到多少种不 同的邮资?程序运行效果如图 77-1 所示。 117700 W C 语言实例解析精粹 图 77-1 实例 77 程序运行效果 实例解析 将问题进行数学分析,不同张数和面值的邮票组成的邮资可用下列公式计算: S=3×i+5×j 其中 i 为 3 分邮票的张数,j 为 5 分的张数 按题目的要求,3 分的邮票可以取 0、1、2、3 共 4 张,5 分的邮票可以取 0、1、2 共 3 张。 采用穷举方法进行组合,可以求出不同面值不同张数的邮票组合后的邮资。 程序代码 【程序 77】 邮票组合 //本实例源码参见光盘 归纳注释 在用穷举法求解这类问题时,必须注意其中可能有相同的解,不要重复列出。 实例 78 分糖果 实例说明 10 个小孩围成一圈分糖果,老师分给第一个小孩 10 块,第二个小孩 2 块,第三个小孩 8 块, 第四个小孩 22 块,第五个小孩 16 块,第六个小孩 4 块,第七个小孩 10 块,第八个小孩 6 块,第 九个小孩 14 块,第十个小孩 20 块。然后所有的小孩同时将手中的糖分一半给右边的小孩;糖块 数为奇数的人可向老师要一块。问经过这样几次后,大家手中的糖的块数将一样多,每人各有多 少块糖?程序运行效果如图 78-1 所示。 第三部分 数值计算与趣味数学篇 X 117711 图 78-1 实例 78 程序运行效果 实例解析 分析题目可知,题目描述的分糖过程是一个机械的重复过程,算法完全可以按照描述的过程 进行模拟。 程序代码 【程序 78】 分糖果 #include void print(int s[]); int judge(int c[]); int j=0; void main() { static int sweet[10]={10,2,8,22,16,4,10,6,14,20}; /*初始化数组数据*/ int i,t[10],l; clrscr(); printf(" Child No. 1 2 3 4 5 6 7 8 9 10\n"); printf("------------------------------------------------------\n"); printf(" Round No.|\n"); print(sweet); /*输出每个人手中糖的块数*/ while(judge(sweet)) /*若不满足要求则继续进行循环*/ { for(i=0;i<10;i++) /*将每个人手中的糖分成一半*/ if(sweet[i]%2==0) /*若为偶数则直接分出一半*/ t[i]=sweet[i]=sweet[i]/2; else /*若为奇数则加 1 后再分出一半*/ t[i]=sweet[i]=(sweet[i]+1)/2; for(l=0;l<9;l++) /*将分出的一半糖给右(后)边的孩子*/ sweet[l+1]=sweet[l+1]+t[l]; sweet[0]+=t[9]; print(sweet); /*输出当前每个孩子中手中的糖数*/ } printf("------------------------------------------------------\n"); 117722 W C 语言实例解析精粹 printf("\n Press any key to quit..."); getch(); } int judge(int c[]) { int i; for(i=0;i<10;i++) /*判断每个孩子手中的糖是否相同*/ if(c[0]!=c[i]) return 1; /*不相同则返回 1*/ return 0; } void print(int s[]) /*输出数组中每个元素的值*/ { int k; printf(" <%2d> | ",j++); for(k=0;k<10;k++) printf("%4d",s[k]); printf("\n"); } 归纳注释 本实例根据题目中分糖果的过程直接给出了实现的代码。在求解比较简单、直观的问题时, 可采用这种方法。 实例 79 波瓦松的分酒趣题 实例说明 法国著名数学家波瓦松(Poison)在青年时代研究过一个有趣的数学问题:某人有 12 品脱的 啤酒一瓶,想从中倒出 6 品脱,但他没有 6 品脱的容器,仅有一个 8 品脱和 5 品脱的容器,怎样 倒才能将啤酒分为两个 6 品脱呢?程序运行效果如图 79-1 所示。 图 79-1 实例 79 程序运行效果 第三部分 数值计算与趣味数学篇 X 117733 实例解析 将 12 品脱酒用 8 品脱和 5 品脱的空瓶平分,可以抽象为解不定方程: 8x−5y=6 其意义是:从 12 品脱的瓶中向 8 品脱的瓶中倒 x 次,并且将 5 品脱瓶中的酒向 12 品脱的瓶 中倒 y 次,最后在 12 品脱的瓶中剩余 6 品脱的酒。 分别用 a,b,c 代表 12 品脱、8 品脱和 5 品脱的瓶子,求出不定方程的整数解,按照不定方 程的意义则倒酒法为: a→b→c →a x y 倒酒的规则如下: (1)按 a→b→c→a 的顺序; (2)b 倒空后才能从 a 中取; (3)c 装满后才能向 a 中倒。 程序代码 【程序 79】 波瓦松的分酒趣题 //本实例源码参见光盘 归纳注释 这类约束分配问题采用递归求解比较方便。 实例 80 求 π 的近似值 实例说明 用两种方法编程实现求取 π 的近似值。程序运行效果如图 80-1 所示。 图 80-1 实例 80 程序运行效果 117744 W C 语言实例解析精粹 实例解析 利用“正多边形逼近”的方法求出 π 的近似值 利用“正多边形逼近”的方法求出 π 值在很早以前就存在,我们的先人祖冲之就是用这种方 法在世界上第一个得到精确度达小数点后第 6 位的 π 值的。 利用圆内接正六边形边长等于半径的特点将边数翻番,作出正十二边形,求出边长,重复这 一过程,就可获得所需精度的 π 的近似值。 假设单位圆内接多边形的边长为 2b,边数为 i,则边数加倍后新的正多边形的边长为: 2 bb122x ×−×−= 周长为: y=2ix 其中 i 为加倍前的正多边形的边数。 利用随机数法求 π 的近似值 随机数法求 π 的近似值的思路:在一个单位边长的正方形中,以边长为半径,以一个顶点为圆 心,在正方形上做四分之一圆。随机的向正方形内扔点,若落入四分之一圆内则计数。重复向正方 形内扔足够多的点,将落入四分之一圆内的计数除以总的点数,其值就是 π 值四分之一的近似值。 按此方法可直接进行编程。 程序代码 【程序 80】 求 π 的近似值 #include #include #include #include #define N 30000 void main() { double e=0.1,b=0.5,c,d; long int i; /*i: 正多边形边数*/ float x,y; int c2=0,d2=0; clrscr(); puts("***********************************************************"); puts("* This program is to calculate PI approximatively *"); puts("* in two methods. *"); puts("* One method is Regular Polygon Approximating, *"); puts("* the other is Random Number Method. *"); puts("***********************************************************"); puts("\n >> Result of Regular Polygon Approximating:"); for(i=6;;i*=2) /*正多边形边数加倍*/ { d=1.0-sqrt(1.0-b*b); /*计算圆内接正多边形的边长*/ 第三部分 数值计算与趣味数学篇 X 117755 b=0.5*sqrt(b*b+d*d); if(2*i*b-i*e<1e-15) break; /*精度达 1e-15 则停止计算*/ e=b; /*保存本次正多边形的边长作为下一次精度控制的依据*/ } printf("---------------------------------------------------------\n"); printf(" >> pi=%.15lf\n",2*i*b); /*输出π值和正多边形的边数*/ printf(" >> The number of edges of required polygon:%ld\n",i); printf("---------------------------------------------------------\n"); randomize(); while(c2++<=N) { x=random(101); /*x 坐标,产生 0~100 之间共 101 个的随机数*/ y=random(101); /*y 坐标,产生 0~100 之间共 101 个的随机数*/ if(x*x+y*y<=10000) /*利用圆方程判断点是否落在圆内*/ d2++; } puts("\n >> Result of Random Number Method:"); printf("---------------------------------------------------------\n"); printf(" >> pi=%f\n",4.*d2/N); /*输出求出的π值*/ printf("---------------------------------------------------------\n"); puts("\n Press any key to quit..."); getch(); } 归纳注释 从程序运行结果可看出,利用“正多边形逼近法”求取的π值比利用“随机数法”求出的π 值要更接近其真实值,这是因为,“随机数法”只有统计次数足够多时才可能准确。 实例 81 奇数平方的有趣性质 实例说明 编程验证大于 1000 的奇数其平方与 1 的差是 8 的倍数。程序运行效果如图 81-1 所示。 图 81-1 实例 81 程序运行效果 117766 W C 语言实例解析精粹 实例解析 本题是一个很容易证明的数学定理,我们可以编写程序验证它。 题目中给出的处理过程很清楚,算法不需要特殊设计。可以按照题目的叙述直接进行验证(程 序中验证的范围可由用户输入)。 程序代码 【程序 81】 奇数平方的有趣性质 //本实例源码参见光盘 归纳注释 由于本例中的运算结果可能超出 int 整型数的范围,因此采用长整型 long 来定义变量。 实例 82 角谷猜想 实例说明 日本一位中学生发现一个奇妙的“定理”,请角谷教授证明,而教授无能为力,于是产生角谷 猜想。猜想的内容是:任意给一个自然数,若为偶数则除以 2,若为奇数则乘 3 加 1,得到一个新 的自然数,然后按照上面的法则继续演算,若干次后得到的结果必然为 1。请编程验证。程序运 行效果如图 82-1 所示。 图 82-1 实例 82 程序运行效果 实例解析 本题是一个还未获得一般证明的猜想,但屡试不爽,可以用程序验证。 第三部分 数值计算与趣味数学篇 X 117777 题目中给出的处理过程很清楚,算法不需特殊设计,可按照题目的叙述直接进行验证。 程序代码 【程序 82】 角谷猜想 //本实例源码参见光盘 归纳注释 本实例程序在 while 循环中对输入的数进行验证,在输入为 0 时,则退出程序。 实例 83 四方定理 实例说明 数论中著名的“四方定理”讲的是:所有自然数至多只要用 4 个数的平方和就可以表示。请 编程验证此定理。程序运行效果如图 83-1 所示。 图 83-1 实例 83 程序运行效果 实例解析 对 4 个变量采用试探的方法进行计算,满足要求时输出计算结果。 程序代码 【程序 83】 四方定理 //本实例源码参见光盘 117788 W C 语言实例解析精粹 归纳注释 程序对任意输入的不为 0(为 0 则程序结束)的数进行试探,满足定理则输出结果。 实例 84 卡布列克常数 实例说明 任意一个 4 位数,只要它们各个位上的数字是不全相同的,就有如下的规律: (1)将组成该 4 位数的 4 个数字由大到小排列,形成由这 4 个数字构成的最大的 4 位数; (2)将组成该 4 位数的 4 个数字由小到大排列,形成由这 4 个数字构成的最小的 4 位(如果 4 个数中含有 0,则得到的数不足 4 位); (3)求两个数的差,得到一个新的 4 位数(高位零保留)。 重复以上过程,最后得到的结果是 6174,这个数被称为卡布列克数。请编程验证卡布列克常 数。程序运行效果如图 84-1 所示。 图 84-1 实例 84 程序运行效果 实例解析 题目中给出的处理过程很清楚,算法不需要特殊设计,可按照题目的叙述直接进行验证。 程序代码 【程序 84】 卡布列克常数 //本实例源码参见光盘 第三部分 数值计算与趣味数学篇 X 117799 归纳注释 程序通过 vr6174 函数的递归调用进行验证,通过 parse_sort 函数将 NUM 分解为数字,函数 max_min 将分解的数字组成最大整数和最小整数。 实例 85 尼科彻斯定理 实例说明 验证尼科彻斯定理,即任何一个整数的立方都可以写成一串连续奇数的和。程序运行效果如 图 85-1 所示。 图 85-1 实例 85 程序运行效果 实例解析 本题是一个定理,我们先来证明它是成立的。 对于任一正整数 a,不论 a 是奇数还是偶数,整数(a×a−a+1)必然为奇数。 构造一个等差数列,数列的首项为(a×a−a+1),等差数列的差值为 2(奇数数列),则前 a 项 的和为: a× ((a×a−a+1))+2×a(a−1)/2 =a×a×a−a×a+a+a×a−a =a×a×a 定理成立。 通过定理的证明过程可知所要求的奇数数列的首项为(a×a−a+1),长度为 a。编程的算法不 需要特殊设计,可按照定理的证明过程直接进行验证。 程序代码 【程序 85】 尼科彻斯定理 //本实例源码参见光盘 118800 W C 语言实例解析精粹 实例 86 扑克牌自动发牌 实例说明 一副扑克有 52 张牌,打桥牌时应将牌分给 4 个人。请设计一个程序完成自动发牌的工作。要 求:黑桃用 S(Spaces)表示;红桃用 H(Hearts)表示;方块用 D(Diamonds)表示;梅花用 C (Clubs)表示。程序运行效果如图 86-1 所示(T 代表 10)。 图 86-1 实例 86 程序运行效果 实例解析 按照打桥牌的规定,每人应当有 13 张牌。在人工发牌时,先进行洗牌,然后将洗好的牌按一 定的顺序发给每一个人。为了便于计算机模拟,可将人工方式的发牌过程加以修改:先确定好发 牌顺序 1、2、3、4;将 52 张牌顺序编号:黑桃 2 对应数字 0,红桃 2 对应数字 1,方块 2 对应数 字 2,梅花 2 对应数字 3,黑桃 3 对应数字 4,红桃 3 对应数字 5……然后从 52 张牌中随机的为 每个人抽牌。 程序代码 【程序 86】 扑克牌自动发牌 //本实例源码参见光盘 归纳注释 本实例采用 C 语言库函数的随机函数,生成 0~51 之间的共 52 个随机数,以产生洗牌后发牌 的效果。 第三部分 数值计算与趣味数学篇 X 118811 实例 87 常胜将军 实例说明 现有 21 根火柴,两人轮流取,每人每次可以取走 1~4 根,不可多取,也不能不取,谁取最 后一根火柴则谁输。请编写一个程序进行人机对弈,要求人先取,计算机后取;计算机一方为“常 胜将军”。程序运行效果如图 87-1 所示。此程序可在 Visual C++中编译(见光盘)。 图 87-1 实例 87 程序运行效果 实例解析 在计算机后走的情况下,要想使计算机成为“常胜将军”,必须找出取法的关键。根据本题的 要求加以总结,后走一方取子的数量与对方刚才一步取子的数量之和等于 5,就可以保证最后一 个子是留给先取子的那个人的。 据此分析进行算法设计就是很简单的工作,编程实现也十分容易。 程序代码 【程序 87】 常胜将军 //本实例源码参见光盘 118822 W C 语言实例解析精粹 归纳注释 本程序的关键在于后取一方的取法,即与先取一方所取的数目之和为 5,即可保证常胜。 实例 88 搬山游戏 实例说明 设有 n 座山,计算机与人为比赛的双方,轮流搬山。规定每次搬山的数止不能超过 k 座, 谁搬最后一座谁输。游戏开始时。计算机请人输入山的总数(n)和每次允许搬山的最大数(k)。 然后请人先开始,等人输入了需要搬走的山的数目后,计算机马上打印出它要搬多少座山,并 提示尚余多少座山。双方轮流搬山直到最后一座山搬完为止。计算机会显示谁是赢家,并问人 是否要继续比赛。若人不想玩了,计算机便会统计出共玩了几局,双方胜负如何。程序运行效 果如图 88-1 所示。此程序可在 Visual C++中编译(见光盘)。 图 88-1 实例 88 程序运行效果 第三部分 数值计算与趣味数学篇 X 118833 实例解析 计算机参加游戏时应遵循下列原则: (1)当剩余山数目−1≤可移动的最大数 k 时,计算机要移(剩余山数目−1)座,以便将最后 一座山留给人。 (2)对于任意正整数 x,y,一定有: 0≤x%(y+1)≤y 在有 n 座山的情况下,计算机为了将最后一座山留给人,而且又要控制每次搬山的数目不超 过最大数 k,它应搬山的数目要满足下列关系: (n−1)%(k+1) 如果算出结果为 0,即整除无余数,则规定只搬 1 座山,以防止冒进后发生问题。 程序代码 【程序 88】 搬山游戏 //本实例源码参见光盘 实例 89 兔子产子(菲波那契数列) 实例说明 从前有一对长寿兔子,它们每一个月生一对兔子,新生的小兔子两个月就长大了,在第二个 月的月底开始生它们的下一代小兔子,这样一代一代生下去,求解兔子增长数量的数列。程序运 行效果如图 89-1 所示。 图 89-1 实例 89 程序运行效果 实例解析 问题可以抽象成下列数学公式: Un=Un−1+Un−2 其中 n 是项数(n≥3)。 这就是著名的菲波那奇数列,该数列的前几个数:1,1,2,3,5,8,13,21… 菲波那契数列在程序中可以用多种方法进行处理。按照其通项递推公式,利用最基本的循环 118844 W C 语言实例解析精粹 控制就可以实现题目的要求。 程序代码 【程序 89】 兔子产子 //本实例源码参见光盘 归纳注释 在求解类似问题时,可将其归纳为通项公式,再利用通项公式求解会方便很多。 实例 90 数字移动 实例说明 在如图 90-1 中的 9 个点上,空出正中间的点,其余的点上任意填入数字 1~8。1 的位置固定 不动,然后移动其余的数字,使 1 到 8 顺时针从小到大排列。移动的规律是只能将数字沿线移向 空白的点。请编程显示数字移动过程。程序运行效果如图 90-2 所示。此程序可在 Visual C++中编 译(见光盘)。 图 90-1 九宫图 图 90-2 实例 90 程序运行效果 第三部分 数值计算与趣味数学篇 X 118855 实例解析 分析题目中的条件,要求利用中间的空白格将数字顺时针方向排列,且排列过程中只能借空 白的点来移动数字。问题的实质就是将矩阵外面的 8 个格看成一个环,8 个数字在环内进行排序, 同于受题目要求的限制——只能将数字沿线移向空白的点,所以要利用中间的空格进行排序,这 样要求的排序算法与其他的不同。 观察中间的点,它是惟一一个与其他 8 个点有连线的点,即它是中心点。中心点活动的空间 最大,它可以向 8 个方向移动,充分利用中心点这个特性是算法设计成功的关键。 在找到 1 所在的位置后,其余各个数字的正确位置就是固定的。可以按照下列算法从数字 2 开始,一个一个地来调整各个数字的位置。 (1)确定数字 i 应处的位置。 (2)从数字 i 应处的位置开始,向后查找数字 i 现在的位置。 (3)若数字 i 现在的位置不正确,则将数字 i 从现在的位置(沿连线)移向中间的空格,而 将原有位置空出;依次将现有空格前的所有元素向后移动……直到将 i 应处的位置空出,把它移 入空出的格。 从数字 2 开始使用以上过程,就可以完成全部数字的移动排序。 编程时要将矩阵的外边 8 个格看成一个环,且环的首元素是不定的,如果算法设计得不好, 程序中就要花很多精力来处理环中元素的前后顺序问题。将题目中的 3×3 矩阵用一个一维数组 表示,中间的元素(第 4 号)刚好为空格。设计另一个指针数组,专门记录指针外 8 个格构成 环时的连接关系。指针数组的每个元素依次记录环中数字在原来数组中对应的元素下标。这样 通过指针数组将原来矩阵中复杂的环型关系表示成了简单的线性关系,从而大大地简化了程序 设计。 程序代码 【程序 90】 数字移动 #include int a[]={0,1,2,5,8,7,6,3}; /*指针数组,依次存入矩阵中构成环的元素下标*/ int b[9]; /*表示 3×3 矩阵,b[4]为空格*/ int c[9]; /*确定 1 所在的位置后,对环进行调整的指针数组*/ int count=0; /*数字移动步数计数器*/ void main() { int i,j,k,t; void print(); clrscr(); puts("*******************************************************"); puts("* This is a program to Move Numbers. *"); puts("*******************************************************"); printf(" >> Input original order of digits 1~8: "); for(i=0;i<8;i++) scanf("%d",&b[a[i]]); /*顺序输入矩阵外边的 8 个数字,矩阵元素的顺序由指针数组的元素 a[i]控制*/ printf(" >> The sorting process is as follows:\n"); print(); 118866 W C 语言实例解析精粹 for(t=-1,j=0;j<8&&t==-1;j++) /*确定数字 1 所在的位置*/ if(b[a[j]]==1) t=j; /*t 记录数字 1 所在的位置*/ for(j=0;j<8;j++) /*调整环的指针数组,将数字 1 所在的位置定为环的首*/ c[j]=a[(j+t)%8]; for(i=2;i<9;i++) /*从 2 开始依次调整数字的位置*/ /*i 为正在处理的数字,i 对应在环中的正确位置就是 i-1*/ for(j=i-1;j<8;j++) /*从 i 应处的正确位置开始顺序查找*/ if(b[c[j]]==i&&j!=i-1) /*若 i 不在正确的位置*/ { b[4]=i; /*将 i 移到中心的空格中*/ b[c[j]]=0;print(); /*空出 i 原来所在的位置,输出*/ for(k=j;k!=i-1;k--) /*将空格以前到i的正确位置之间的数字依次向后移动一格*/ { b[c[k]]=b[c[k-1]]; /*数字向后移动*/ b[c[k-1]]=0; print(); } b[c[k]]=i; /*将中间的数字 i 移入正确的位置*/ b[4]=0; /*空出中间的空格*/ print(); break; } else if(b[c[j]]==i) break; /*数字 i 在正确的位置*/ printf("\n Press any key to quit..."); getch(); } void print(void) /*按格式要求输出矩阵*/ { int c; printf(" >> Step No.%2d ",count++); for(c=0;c<9;c++) if(c%3==2) printf("%2d ",b[c]); else printf("%2d",b[c]); printf("\n"); } 归纳注释 本实例程序采用循环从 2 开始依次调整数字的位置,调整完毕,利用 print 函数实现 输出。 实例 91 多项式乘法 实例说明 本实例实现多项式乘法。程序运行效果如图 91-1 所示。 第三部分 数值计算与趣味数学篇 X 118877 图 91-1 实例 91 程序运行效果 实例解析 求两个多项式: () 01 2 2 1 1 pxpxpxpxP m m m m ++⋅⋅⋅++= − − − − () 01 2 2 1 1 qxqxqxqxQ n n n n ++++= − − − − Λ 的乘积多项式 () 01 2 2 sxsxsxS nm nm ++= −+ −+ Λ 可以按照如下的迭加公式计算结果的各项系数: 1,,1,01,,1,0 −=−=+= ++ njmiqpss jijiji ΛΛ ,, 在程序中,先定义两个一维数组 p,q,从高到低来存储两个多项式的系数,并定义一维数组 result 来存储结果;然后调用 npmul()子函数按照上面的公式所示算法逐项来完成对结果多项式各 项系数的计算,最后在屏幕上计算结果。 程序代码 【程序 91】 多项式乘法 #include #include #define MAX 50 /* 下面的两个数组可以根据具体要求解的多项式来决定其值*/ static double p[6]={4,-6,3,1,-1,5}; /*表示多项式 4x^5 - 6x^4 + 3x^3 + x^2 - x + 5 */ static double q[4]={3,2,-5,1}; /*表示多项式 3x^3 + 2x^2 - 5x + 1 */ static double result[9]={0,0,0,0,0,0,0,0,0}; /*存放乘积多项式*/ void npmul(p,m,q,n,s) int m,n; double p[],q[],s[]; 118888 W C 语言实例解析精粹 { int i,j; for (i=0; i<=m-1; i++) for (j=0; j<=n-1; j++) s[i+j]=s[i+j]+p[i]*q[j]; /*迭代计算各项系数*/ return; } double compute(s,k,x) /*计算所给多项式的值*/ double s[]; int k; float x; { int i; float multip = 1; double sum = 0; for (i=0;i=0;i--) { sum = sum + s[i] * multip; /*依次从高到低求出相对应项的值*/ if (x!=0) multip = multip / x; } return sum; } void main() { int i,j,m,n; double px[MAX],qx[MAX],rx[MAX]; float x; clrscr(); for(i=0;i> Please input m (>=1): "); scanf("%d",&m); printf(" >> Please input P%d, P%d, ... P1, P0 one by one:\n",m-1,m-2); for(i=0;i> Please input n (>=1): "); scanf("%d",&n); printf(" >> Please input Q%d, Q%d, ... Q1, Q0 one by one:\n",n-1,n-2); for(i=0;i=1;i--) /*逐行逐项打印出结果多项式*/ { printf(" (%f*x^%d) + ",rx[m+n-1-i],i-1); if(j==2) { printf("\n"); j=0; } 第三部分 数值计算与趣味数学篇 X 118899 else j++; } printf("\n"); printf("Input the value of x: "); scanf("%f",&x); printf("\nThe value of the R(%f) is: %13.7f",x,compute(rx,m+n-1,x)); puts("\n Press any key to quit..."); getch(); } 归纳注释 本程序采用迭代法计算两个多项式乘积,并求出了最终结果。 实例 92 产生随机数 实例说明 编程实现产生随机数。程序运行效果如图 92-1 所示。 图 92-1 实例 92 程序运行效果 实例解析 众所周知,随机数在软件开发中是非常有用的。然而,在 DOS 系统中的很多编程语言中都不 能得出令人满意的随机数,这些随机数都有以下几个缺点:值域范围小,易重复,故随机度不高; 没有经过归一化,使用不便;其随机序列是固定的。 因此,虽然这些随机序列从内部看起来具有随机性,但就其整个序列而言却不是随机的,即 每次调用程序生成的随机序列结果都是惟一的,这样的随机数又称为伪随机数。 本实例讲解一种简单的方法,可以生成每次不同的随机序列。迭代方程大都具有随机性,考 虑如下迭代法: () () ( )[]nfnafnf −=+ 11 ( ) ( )之间,在之间,,在 4110)( anf 上面的方程是从 f(n) 到 f(n+1)上的映射。方程虽然看起来比较简单,但是当参数 a 发生变化 时,迭代的结果却表现出“倍周期分叉”的复杂特性。利用这一特性,很容易产生高随机度的随 机序列。因为方程对初值非常敏感,不同的初值对迭代的结果有很大的影响,所以可以让计算机 自动产生一个变化范围较大的初值,直接赋给方程参数 a 。这样处理后,不光是随机序列内部, 而且随机序列本身也具有了随机性。由此,即可得到高随机度的随机数序列。 119900 W C 语言实例解析精粹 程序代码 【程序 92】 产生随机数 //本实例源码参见光盘 归纳注释 本实例程序中的初值是利用 DOS 的系统时钟产生的,并经过处理而得到。初值和产生随机序 列的函数都被定义为 double 型。 实例 93 堆栈四则运算 实例说明 利用堆栈实现四则运算。程序运行效果如图 93-1 所示。 图 93-1 实例 93 程序运行效果 实例解析 运行程序,首先由 main()函数调用 Message()函数显示提示信息。然后调用 EvaluateExpression() 函数计算四则运算表达式。 EvaluateExpression()函数首先调用 InitStack()函数建立两个初始栈,一个用来存放操作符串, 另一个用来存放操作数串。然后循环调用 GetInput()函数得到用户输入并计算表达式的值。过程如 下:首先得到一个数,将这个数压入操作数栈。然后循环调用 GetInput()函数,如果输入是运算符, 而且优先级比操作符栈栈顶的优先级低,则将运算符压入操作符栈;如果是运算符,但优先级比 操作符栈顶的优先级高,则将操作数栈顶的两个数出栈,用该操作符进行运算,把得到的结果压 入操作数栈顶。当用户输入完后按回车键时,操作数栈栈顶的数就是最终的运算结果。 第三部分 数值计算与趣味数学篇 X 119911 程序代码 【程序 93】 堆栈四则运算 //#include "stdafx.h" #include "stdio.h" #include "string.h" #include "stdlib.h" #include "conio.h" #define TRUE 1 #define FALSE 0 #define STACK_INIT_SIZE 100/*存储空间初始分配量*/ #define STACKINCREMENT 20/*存储空间分配增量*/ typedef struct { int *pBase;/*在构造之前和销毁之后,base 的值为 NULL*/ int *pTop;/*栈顶指针*/ int StackSize;/*当前已分配的存储空间,以元素为单位*/ }Stack; typedef int BOOLEAN; char Operator[8]="+-*/()#";/*合法的操作符存储在字符串中*/ char Optr;/*操作符*/ int Opnd=-1;/*操作符*/ int Result;/*操作结果*/ /*算符间的优先关系*/ char PriorityTable[7][7]= { {'>','>','<','<','<','>','>'}, {'>','>','<','<','<','>','>'}, {'>','>','>','>','<','>','>'}, {'>','>','>','>','<','>','>'}, {'<','<','<','<','<','=','o'}, {'>','>','>','>','o','>','>'}, {'<','<','<','<','<','o','='}, }; //数据对象的操作方法 //构造一个空栈,如果返回值为 0,则表示初始化失败 Stack InitStack()/*这是个效率低的方法*/ { Stack S; S.pBase=(int*)malloc(STACK_INIT_SIZE*sizeof(int)); if(!S.pBase) {/*内存分配失败*/ printf("内存分配失败,程序中止运行\n"); exit(-1); } else 119922 W C 语言实例解析精粹 { S.pTop=S.pBase; S.StackSize=STACK_INIT_SIZE; } return S; } //销毁栈 S void DestoryStack(Stack *S) { if(S->pBase) { free(S->pBase); S->pTop=S->pBase=NULL; } } //若栈不空,则用 e 返回 S 的栈顶元素 //注:由于应用的特殊,可以不检查栈是否为空 int GetTop(Stack S) { return *(S.pTop-1); } //插入元素 e 为新的栈顶元素,如果成功则返回 1,否则返回 0 int Push(Stack *S,int e) { if(S->pTop-S->pBase==S->StackSize) {//栈满,追加存储空间 S->pBase=(int*)realloc(S->pBase,S->StackSize+STACKINCREMENT*sizeof(int)); if(!S->pBase) return 0;//存储分配失败 S->pTop=S->pBase+S->StackSize; S->StackSize+=STACKINCREMENT; } *(S->pTop++)=e; return 1; } int Pop(Stack *S,int *e) {//若栈不空,则删除 S 的栈顶元素,用 e 返回其值,并返回 1,否则返回 0 if(S->pTop==S->pBase) return 0; *e=*--(S->pTop); return 1; } //主函数及其他函数的实现 //比较两个数学符号 operator_1,operator_2 的计算优先权 //在算符优先关系表中查找相应的关系并返回'<','=',或'>' char CheckPriority(char operator_1,char operator_2) { int i,j;//用来查询算符间优先关系表的下标 //char *ptr; i=strchr(Operator,operator_1)-Operator; 第三部分 数值计算与趣味数学篇 X 119933 //找到传入操作符在字符串 Operators 中的相对位置 j=strchr(Operator,operator_2)-Operator; //返回算符优先关系表中相应值 return PriorityTable[i][j]; } BOOLEAN IsOperator(char ch) {//判断一个字符是否为操作符 if(strchr(Operator,ch)) return TRUE; else return FALSE; } //从键盘获得输入 void GetInput(void) { char Buffer[20];//键盘输入缓冲区,用来处理输入多位数的情况 char ch;//存放键盘输入 int index;//存放 Buffer 的下标 index=0; ch=getch();//从键盘读入一个字符 while(ch!=13&&!IsOperator(ch)) {//如果输入的字符是回车符或是操作符,循环结束 if(ch>='0'&&ch<='9') {//将字符回显到屏幕 printf("%c",ch); Buffer[index]=ch; index++; } ch=getch(); } if(ch==13) Optr='#';//输入的表达式以回车符结束 else { Optr=ch; printf("%c",ch); } if(index>0) { Buffer[index]='\0'; Opnd=atoi((Buffer)); } else Opnd=-1;//程序不支持输入负数,当 Opnd 为负数时,表示输入的字符为操作符 } //计算形如 a+b 的表达式,theta 为操作符,a、b 为操作数 int Calc(int a,char theta,int b) { switch(theta) { 119944 W C 语言实例解析精粹 case '+': return a+b; case '-': return a-b; case '*': return a*b; default: if(b==0)//除数为零的情况 { printf("除数不能为"); return 0;//返回 0 用以显示 } else return a/b; } } /*表达式求值*/ BOOLEAN EvaluateExpression() { int temp;//临时变量 char theta;//存放操作符的变量 int itheta;//存放出栈的操作符的变量 int a,b;//存放表达式运算时的中间值 int topOpnd;//栈顶操作数 char topOptr;//栈顶操作符 Stack OPTR=InitStack();//操作符栈 Stack OPND=InitStack();//操作数栈 if(!Push(&OPTR,'#'))//操作符栈中的第一个为#字符 return FALSE; GetInput();//从键盘获得输入 while(Optr!='#'||GetTop(OPTR)!='#') {//如果 Optr 大于等于 0,表示有操作数输入 if(Opnd>=0)Push(&OPND,Opnd); switch(CheckPriority(GetTop(OPTR),Optr)) { case '<'://栈顶元素优先权低 if(!Push(&OPTR,Optr))return FALSE; GetInput(); break; case '='://去掉括号并接收键盘输入 Pop(&OPTR,&temp);GetInput(); break; case '>'://退栈并将运算结果入栈 //先用 itheta 得到操作符再赋给 theta Pop(&OPTR,&itheta); Pop(&OPND,&b); Pop(&OPND,&a); theta = (char)( itheta ); Push(&OPND,Calc(a,itheta,b)); Opnd=-1; break; 第三部分 数值计算与趣味数学篇 X 119955 } } //本算法中,当输入只有一个操作数时就输入回车符 //OPND.pTop==OPND.pBase //如果 OPND.pTop==OPND.pBase 并且 Opnd<0 //则说明用户未输入任何操作和操作符而直接输入[回车] //程序直接退出运行 if(OPND.pTop==OPND.pBase&&Opnd<0) { printf("\n\n 感谢使用!\n"); exit(1); } else if(OPND.pTop==OPND.pBase) Result=Opnd; else { Result=GetTop(OPND); DestoryStack(&OPND); DestoryStack(&OPTR); } return TRUE; } void Message(void) { printf("\n 四则运算表达式求值演示\n"); printf("-------------------------------\n"); printf("使用方法:请从键盘上直接输入表达式,以回车键结束.如 45*(12-2)[回车]\n"); printf("注 0:不输入任何数而直接按[回车]键,将退出程序.\n"); printf("注 1:本程序暂时不接受除数字键及四则运算符之外的任何其他键盘输入.\n"); printf("注 2:本程序暂时只能处理正确的表达式,不支持输入负数.\n"); printf("-------------------------------\n\n"); } void main(void) { int i;//用来一些说明性信息 Message(); for(i=1;;i++) { printf("表达式%d:",i); if(EvaluateExpression()) printf("=%d\n",Result); else printf("计算中遇到错误\n"); } } 归纳注释 堆栈是一种先进后出的数据结构,对堆栈的操作有 pop 和 push 两种。pop 是将堆栈栈顶的数取 119966 W C 语言实例解析精粹 出来,同时栈中元素减少一个,栈顶元素的下一个成为新的栈顶元素。push 是将一个元素压入堆栈 中,该元素成为新的栈顶元素,原来栈顶元素成为栈顶元素的下一个元素,栈中元素增加一个。 实例 94 递归整数四则运算 实例说明 利用递归方法实现整数的四则运算。程序运行效果如图 94-1 所示。 图 94-1 实例 94 程序运行效果 实例解析 本程序实现了带括号的四则运算的功能,可以把它看作一个简单的计算器。 编程的基本思想如下:把四则运算的优先级做排列,最低的优先级是加减运算,较高的是乘 除运算。为了方便起见,可以把运算数看作是和括号一样拥有最高优先级的运算符。在这里,用 表示一个可以计算加减运算的表达式,表示一个可以计算乘除的表达式,而 则是括号表达式或一个数字。然后可以得到下面的表达式(其中“→”表示左边由右边构成,“|” 表示或者): +|- *|/ → ()|Number 根据上面的表达式,可以很容易地写出相应的程序。 程序代码 【程序 94】 递归整数四则运算 /* 对四则混合运算所提取的形式化表达式(生成式) -> { } -> + | - -> { } -> * | / -> ( ) | Number 第三部分 数值计算与趣味数学篇 X 119977 */ #include #include char token; /*全局标志变量*/ /*递归调用的函数原型*/ int exp( void ); int term( void ); int factor( void ); void error( void ) /*报告出错信息的函数*/ { fprintf( stderr, "错误\n"); exit( 1 ); } void match( char expectedToken ) /*对当前的标志进行匹配*/ { if( token == expectedToken ) token = getchar(); /*匹配成功,获取下一个标志*/ else error(); /*匹配不成功,报告错误*/ } void Message(void) { printf("=============================================================\n"); printf("* 递归实现的四则运算表达式求值程序 *\n"); printf("*************************************************************\n"); printf("使用方法:请从键盘上直接输入表达式,以回车键结束.如 45*(12-2)[回车]\n"); printf("***********************************************************\n\n"); } main() { int result; /*运算的结果*/ Message(); printf(" >> 请输入表达式: "); token = getchar(); /*载入第一个符号*/ result = exp(); /*进行计算*/ if( token == '\n' ) /* 是否一行结束 */ printf( " >> 表达式的计算结果为 : %d\n", result ); else error(); /* 出现了例外的字符 */ puts("\n\n 请按任意键退出 ...\n"); getch(); return 0; } int exp( void ) { int temp = term(); /*计算比加减运算优先级别高的部分*/ while(( token == '+' ) || ( token == '-' )) switch( token ) { case '+': match('+'); /*加法*/ temp += term(); 119988 W C 语言实例解析精粹 break; case '-': match('-'); temp -= term(); /*减法*/ break; } return temp; } int term( void ) { int div; /*除数*/ int temp = factor(); /*计算比乘除运算优先级别高的部分*/ while(( token == '*' ) || ( token == '/' )) switch( token ) { case '*': match('*'); /*乘法*/ temp *= factor(); break; case '/': match('/'); /*除法*/ div = factor(); if( div == 0 ) /*需要判断除数是否为 0*/ { fprintf( stderr, "除数为 0.\n" ); exit(1); } temp /= div; break; } return temp; } int factor( void ) { int temp; if( token == '(' ) /*带有括号的运算*/ { match( '(' ); temp = exp(); match(')'); } else if ( isdigit( token )) /*实际的数字*/ { ungetc( token, stdin ); /*将读入的字符退还给输入流*/ scanf( "%d", &temp ); /*读出数字*/ token = getchar(); /*读出当前的标志*/ } else error(); /*不是括号也不是数字*/ return temp; } 归纳注释 本实例采用的方法叫做递归下降法。所谓下降,就是从最宏观的部分开始,逐步细化。 程序执行的时候,由用户输入一个表达式,然后主函数调用 exp()函数来计算表达式的值,最 第三部分 数值计算与趣味数学篇 X 119999 终输出结果。本程序只是一个最简单的实例,没有处理表达式中有空格的情况和非整数的情况。 如果读者理解了这种编程的思想,增加这些功能是轻而易举的。 实例 95 复平面作图 实例说明 DOS 文件模式下的标准窗口为 25 行,每行 80 个字符。在本例中,将使用文本模式,在复平 面上打印输出给定的复数点集。程序运行效果如图 95-1 所示。 图 95-1 实例 95 程序运行效果 实例解析 设给定了 n 个复数 1,,1,0, −=+= nkiyxz kkk Λ 其中 1−=i 。 首先在屏幕上建立一个直角坐标系 xOy,其 x 值与 y 值的范围分别为[-12,12]和[−40,40]。注 意 这里是将 x 轴作为纵轴,y 作为横轴。即所有的复数点均打印在宽 25,长 80 的屏幕上,坐标原点 在屏幕中心。 本例中要绘制的图形为 θθρ ie+= 2 ,在绘制的时候,用“−”,“|”表示坐标轴,“*”表示函 数点。为了能以熟悉的直角坐标系来描绘复平面,在这里使用隶莫佛定理: θθθ sincos irrrei += 当使用 x 表示实部,y 表示虚部的时候,就得到了直角坐标与复坐标之间的关系: ⎩ ⎨ ⎧ = += θθ θθ sin cos2 y x 220000 W C 语言实例解析精粹 程序代码 【程序 95】 复平面作图 //本实例源码参见光盘 归纳注释 在程序中,为了实现在屏幕上的输出,定义了二维数组 screen[25][80],用来对应屏幕上的点, 并且使用 menset(screen,'',25*80)这个函数将所有的字符都设置成空格,这样在屏幕上就是空白显 示,只要将需要显示出来的点设置成可视字符即可。 实例 96 绘制彩色抛物线 实例说明 本程序实现了根据给定的起点及旋转的角度,用指定的颜色绘制抛物线的功能。程序运行结 果如图 96-1 所示。 图 96-1 实例 96 程序运行结果 实例解析 抛物线插补法的过程简述如下(有兴趣的读者可参阅“数值分析方法”类书籍): 假设抛物线方程是 2axy = ,要画出从起点 ( )11, yx 到终点 ( )11, yx− 的部分。其中 ()11, yx 为相 对于抛物线顶点的坐标。 首先设置插补的初始信息:当 01 x 时水平方向的移 动增量 dx=−1;同样 y 也类似。 第三部分 数值计算与趣味数学篇 X 220011 其他插值信息如下: ( ) 。0 ,d ,2 ,10.12 2 1 1 1 = = −= ×+−= f yxfy yrx yxxfx d 进行插补时,首先显示起点(x1,y1),并 令 x=x1,y=y1。如 果 f≥0,则 x=x+dx,就显示像点(x,y), 并且令 f=f−| fx |,fx=fx+rx。此时如果 fx×(fx−rx)≤0,则 dy=−dy,fy=−fy,f=−f。如果 f<0,则 y=y+dy, 显示像点(x,y),且 f=f+| fy |。 重复进行以上过程,直到到达终点结束。 绘制旋转的抛物线时,要根据旋转角度 t 对像点进行调整,经过调整后的实际显示的像点方 程为 tytxytytxx cossinsincos 22 +=−= , 运行该程序后,首先由主程序调用绘制抛物线函数依次画 4 个互成 90°的抛物线,然后依次 画出互成 30°的各色抛物线 12 个,最后退出主函数,程序结束。 程序代码 【程序 96】 绘制彩色抛物线 #include #include /*画抛物线的子函数 spara()*/ /*row,col 代表抛物线顶点的坐标,x1,y1 是抛物线起点相对顶点的坐标*/ /*t 为抛物线绕顶点旋转的角度*/ void spara(row,col,x1,y1,t,color) int row,col,x1,y1,t,color; { int n,dx,dy,x,y,x2,y2; double ct,st; double f,fx,fy,b,a,rx; st=(double)t*3.1415926/180.0; /*把角度转化为弧度*/ ct=cos(st); st=sin(st); n=abs(x1)+abs(y1); n=n+n; dx=1; dy=1; f=0.0; /*初始化工作*/ if (x1>0) dx=-1; if (y1>0) dy=-1; a=y1; b=x1; b=b*b; rx=-a-a; fx=2.0*x1*dx+1.0; fx=-a*fx; fy=b; if (dy<0) fy=-fy; x=x1; y=y1; x2=(double)x*ct-(double)y*st+2000.5; y2=(double)x*st+(double)y*ct+2000.5; x2=x2-2000; y2=y2-2000; putpixel(row-y2,col+x2,color); while (n>0) /*具体的运算法则见上面的公式*/ { n=n-1; if (f>=0.0) { x=x+dx; 220022 W C 语言实例解析精粹 x2=(double)x*ct-(double)y*st+2000.5; y2=(double)x*st+(double)y*ct+2000.5; x2=x2-2000; y2=y2-2000; putpixel(row-y2,col+x2,color); if (fx>0.0) f=f-fx; else f=f+fx; fx=fx+rx; if (fx==0.0||(fx<0.0&&fx-rx>0.0)||(fx>0.0&&fx-rx<0.0)) { dy=-dy; fy=-fy; f=-f;} } else { y=y+dy; x2=(double)x*ct-(double)y*st+2000.5; y2=(double)x*st+(double)y*ct+2000.5; x2=x2-2000; y2=y2-2000; putpixel(row-y2,col+x2,color); if (fy>0.0) f=f+fy; else f=f-fy; } } return; } void main() { int i,color; int gdriver = DETECT , gmode; color = 1; registerbgidriver(EGAVGA_driver); initgraph(&gdriver,&gmode,"..\\bgi"); /*初始化图形界面*/ for (i=1;i<=4;i++) /*先画出 4 个互成 90°的抛物线*/ { spara(200,200,100,100,i*90,color); color+=3; getch(); } color = 1; for (i=1;i<=11;i++) /*再画 12 个互成 30°的抛物线*/ { spara(200,200,100,100,i*30,color); color++; } getch(); closegraph(); return; } 归纳注释 本例中首次使用了 Turbo C 的图形库函数,编程环境具体的配置和图形函数的详细使用方法 参见【第四部分 图形篇】中的说明。 第三部分 数值计算与趣味数学篇 X 220033 实例 97 绘制正态分布曲线 实例说明 实现绘制正态分布曲线。程序运行结果如图 97-1 所示,其中 Alpha 为 5,θ为 80。 图 97-1 实例 97 程序运行结果 实例解析 正态分布是实际应用中极为广泛的分布函数之一,也称为高斯分布。正态函数的定义如下: () ( ) dtexaP x at ∫ ∞− − = 2 2 2 π2 1,, σ σ σ 其中 a 为随机变量的数学期望(平均值);σ(σ>0)是随机变量方差的根。但是在实际使用过 程中,因为这是一个广义积分式,所以很难用程序计算,而且计算量较大。通常使用误差函数来计算: () ⎟⎟ ⎠ ⎞ ⎜⎜ ⎝ ⎛ −+= σ σ 22 1 2 1,, axerfxaP 其中误差函数定义如下: () dtexerf x t∫ −= 0 2 π 2 程序代码 【程序 97】 绘制正态分布曲线 #include "stdio.h" #include "math.h" #include "graphics.h" 220044 W C 语言实例解析精粹 double lgam1(x) /*Gamma 函数的计算*/ double x; { int i; double y,t,s,u; static double a[11]={ 0.0000677106,-0.0003442342, 0.0015397681,-0.0024467480,0.0109736958, -0.0002109075,0.0742379071,0.0815782188, 0.4118402518,0.4227843370,1.0}; if (x<=0.0) { printf("err**x<=0!\n"); return(-1.0);} y=x; if (y<=1.0) { t=1.0/(y*(y+1.0)); y=y+2.0;} else if (y<=2.0) { t=1.0/y; y=y+1.0;} else if (y<=3.0) t=1.0; else { t=1.0; while (y>3.0) { y=y-1.0; t=t*y;} } s=a[0]; u=y-2.0; for (i=1; i<=10; i++) s=s*u+a[i]; s=s*t; return(s); } double lgam2(a,x) /*不完全 Gamma 函数*/ double a,x; { int n; double p,q,d,s,s1,p0,q0,p1,q1,qq; if ((a<=0.0)||(x<0.0)) { if (a<=0.0) printf("err**a<=0!\n"); if (x<0.0) printf("err**x<0!\n"); return(-1.0); } if (x+1.0==1.0) return(0.0); if (x>1.0e+35) return(1.0); q=log(x); q=a*q; qq=exp(q); if (x<1.0+a) { p=a; d=1.0/a; s=d; for (n=1; n<=100; n++) { p=1.0+p; d=d*x/p; s=s+d; if (fabs(d)=0.0) y=lgam2(0.5,x*x); else y=-lgam2(0.5,x*x); return(y); } double lgass(a,d,x) /*正态分布函数*/ double a,d,x; { double y; if (d<=0.0) d=1.0e-10; y=0.5+0.5*lerrf((x-a)/(sqrt(2.0)*d)); return(y); } main() { int i; double j; double a, d; int gdriver = DETECT, gmode; 220066 W C 语言实例解析精粹 clrscr(); printf("This program will draw the Normal Distribution Graph.\n"); printf("Please input the mathematical expectation (Alpha): "); scanf("%lf", &a ); printf("Please input the variance (Sita >0): "); scanf("%lf", &d ); /*registerbgidriver( EGAVGA_driver );*/ initgraph( &gdriver, &gmode, "e:\\tc\\bgi" ); setbkcolor( BLUE ); moveto( 50, 430 ); lineto( 590, 430 ); outtextxy( 600, 425, "X"); moveto( 200, 50 ); lineto( 200, 450 ); outtextxy( 200, 30, "Y" ); outtextxy( 185, 435, "O"); setcolor( RED ); moveto( 51, 430 - 100 * lgass( a, d, -150.0 ) ); for( i = 51; i <= 590; i++ ) { j = 430 - 360 * lgass( a, d, (double)(i-200) ); lineto( i, j ); } getch(); closegraph(); } 归纳注释 本实例根据输入的数学期望和方差进行正态分布曲线的绘制。在绘制曲线时,采用图形模式。 实例 98 求解非线性方程 实例说明 用二分法求解非线性方程 f(x)=0 在指定区间[a,b]内的实根。程序运行结果如图 98-1 所示。 图 98-1 实例 98 程序运行结果 第三部分 数值计算与趣味数学篇 X 220077 实例解析 从端点 x0=a 开始,以 h 为步长,逐步往后进行搜索。对于每一个子区间[xi,xi+1](其中 xi+1=xi+h) 如果 f (xi)=0,那么 xi 为一个实根,并且从 xi+h/2 开始再往后搜索。如果 f (xi+1)=0,那么 xi+1 为一个实根,并且从 xi+1+h/2 开始再往后搜索。如果 f (xi)f (xi+1)>0,那么说明当前子区间内无实根, 从 xi+1 开始再往后搜索。如果 f (xi)f (xi+1)<0,则说明当前子区间内有实根,这时要反复将子区间减 半,直到发现一个实根,或者子区间长度划分到了小于 eps 为止。在后一种情况下,取子区间的 中点作为方程的一个实根,然后再从 xi+1 开始往后搜索,其中 eps 为预先给定的精度要求。 以上过程一直进行到区间右端点 b 为止。 函数 BinSearchRoot 实现了上述功能。需要指出的是,程序中还需要一个函数来计算对于每 个给定的点,多项式对应的值。这里使用函数 Equation 进行求值。在求值的时候,为了提高效率, 没有使用直接带入求值,而是使用霍纳法求值。 举例说明霍纳法:要求 3x3−5x2+x+1,就先把表达式转化为((3x−5)x+1)x+1。在使用第一种方 法时需要作 3+2+1=6 次乘法,3 次加减法;而第二种方法只需要做 4 次乘法,3 次加减法,大大 减少了计算量。 程序代码 【程序 98】 求解非线性方程 #include "math.h" #include "stdio.h" int BinSearchRoot(a,b,h,eps,x,m) /*用二分法计算非线性方程的实根*/ int m; /*参数意义 a 所求的根的下界 b 所求的根的上界,即所求的根落在区间 [a,b]之内 h 递进的步长 eps 精度 x 根的值 m 预计的根的个数*/ double a,b,h,eps,x[]; { extern double Equation(); /*求解的非线性方程*/ int n,js; double z,y,z1,y1,z0,y0; n=0; z=a; y=Equation(z); while ((z<=b+h/2.0)&&(n!=m)) /*对给定步长的子区间进行搜索*/ { if (fabs(y)0.0) /*该区间内无根*/ { y=y1; z=z1;} else /*该区间内有根*/ { js=0;/*标志,0 表示未找到根,1 表示已经确定了根*/ while (js==0) { if (fabs(z1-z)> Solve successfully!\n >> The results are:"); printf(" >> The function has %d roots, they are:\n",n);/*输出根的个数*/ for (i=0; i<=n-1; i++) printf(" >> x(%d)=%13.7e\n",i,x[i]); printf("\n Press any key to quit...\n"); getch(); } double Equation(x) double x; { double z; z=(((((x-5.0)*x+3.0)*x+1.0)*x-7.0)*x+7.0)*x-20.0; return(z); } 归纳注释 在使用二分发时,要注意步长 h 的选择,如果步长取得过大,可能会导致某些实根的丢失; 如果选得过小,会增加计算工作量。 实例 99 实矩阵乘法运算 实例说明 求 m×n 阶实矩阵 A 与 n×k 阶实矩阵 B 的乘积矩阵 C=AB。程序运行结果如图 99-1 所示。 图 99-1 实例 99 程序运行结果 221100 W C 语言实例解析精粹 实例解析 矩阵相乘遵循如下法则: (1)A 的列数与 B 的行数必须相同; (2)Am×n 与 Bn×k 的乘积为 m×k 的矩阵; (3)结果矩阵 C 中每个元素 cij 的值是 A 中 i 行的元素与 B 中 j 列的元素的乘积和。用公式表 示就是 。,, 1,,1,01,,1,0 1 0 −=−== ∑ − = kjmibaC n t tjitij ΛΛ 程序中最主要的函数是 void MatrixMul(a,b,m,n,k,c),其形式参数说明如下: (1)a 为双精度实型二维数组,体积为 m×n,存放矩阵 A 的元素; (2)b 为双精度实型二维数组,体积为 n×k,存放矩阵 B 的元素; (3)m 为整型变量,矩阵 A 的行数,也是乘积矩阵 C=AB 的行数; (4)n 为整型变量,矩阵 A 的列数,也是 B 矩阵的行数; (5)k 为整型变量,矩阵 B 的列数,也是乘积矩阵 C=AB 的列数; (6)c 为双精度实型二维数组,体积为 m×k,存放乘积矩阵 C=AB 的元素。 程序代码 【程序 99】 实矩阵乘法运算 #include "stdio.h" #define MAX 255 void MatrixMul(a,b,m,n,k,c) /*实矩阵相乘*/ int m,n,k; /*m 矩阵 A 的行数, n 矩阵 B 的行数, k 矩阵 B 的列数*/ double a[],b[],c[]; /*a 为 A 矩阵, b 为 B 矩阵, c 为结果,即 c = AB */ { int i,j,l,u; /*逐行逐列计算乘积*/ for (i=0; i<=m-1; i++) for (j=0; j<=k-1; j++) { u=i*k+j; c[u]=0.0; for (l=0; l<=n-1; l++) c[u]=c[u]+a[i*n+l]*b[l*k+j]; } return; } main() { int i,j,m,n,k; double A[MAX]; double B[MAX]; double C[MAX]; for(i=0;i> Please input the number of rows in A, m= "); scanf("%d",&m); printf(" >> Please input the number of cols in A, n= "); scanf("%d",&n); printf(" >> Please input the number of cols in B, k= "); scanf("%d",&k); printf(" >> Please input the %d elements in A one by one:\n",m*n); for(i=0;i> Please input the %d elements in B one by one:\n",n*k); for(i=0;i> The result of C(%d*%d)=A(%d*%d)B(%d*%d) is:\n",m,k,m,n,n,k); for (i=0; id) { d=t; js[k]=j; is=i;} } if (d+1.0==1.0) l=0; /*主元为 0*/ /*主元不为 0 的时候*/ else { if (js[k]!=k) for (i=0;i<=n-1;i++) { p=i*n+k; q=i*n+js[k]; t=a[p]; a[p]=a[q]; a[q]=t; } if (is!=k) { for (j=k;j<=n-1;j++) { p=k*n+j; q=is*n+j; t=a[p]; a[p]=a[q]; a[q]=t; } t=b[k]; b[k]=b[is]; b[is]=t; } } if (l==0) { free(js); printf("fail\n"); return(0); } d=a[k*n+k]; /*下面为归一化部分*/ for (j=k+1;j<=n-1;j++) { p=k*n+j; a[p]=a[p]/d; } b[k]=b[k]/d; /*下面为矩阵 A,B 消元部分*/ for (i=k+1;i<=n-1;i++) { for (j=k+1;j<=n-1;j++) { p=i*n+j; a[p]=a[p]-a[i*n+k]*a[k*n+j]; } b[i]=b[i]-a[i*n+k]*b[k]; } } d=a[(n-1)*n+n-1]; 221144 W C 语言实例解析精粹 /*矩阵无解或有无限多解*/ if (fabs(d)+1.0==1.0) { free(js); printf("该矩阵为奇异矩阵\n"); return(0); } b[n-1]=b[n-1]/d; /*下面为迭代消元*/ for (i=n-2;i>=0;i--) { t=0.0; for (j=i+1;j<=n-1;j++) t=t+a[i*n+j]*b[j]; b[i]=b[i]-t; } js[n-1]=n-1; for (k=n-1;k>=0;k--) if (js[k]!=k) { t=b[k]; b[k]=b[js[k]]; b[js[k]]=t;} free(js); return(1); } main() { int i,n; double A[MAX]; double B[MAX]; clrscr(); puts("This is a program to solve N order linear equation set Ax=B."); puts("\n It use Guass Elimination Method to solve the equation set:"); puts("\n a(0,0)x0+a(0,1)x1+a(0,2)x2+...+a(0,n-1)xn-1=b0"); puts(" a(1,0)x0+a(1,1)x1+a(1,2)x2+...+a(1,n-1)xn-1=b1"); puts(" ......"); puts(" a(n-1,0)x0+a(n-1,1)x1+a(n-1,2)x2+...+a(n-1,-1)xn-1=bn-1\n"); printf(" >> Please input the order n (>1): "); scanf("%d",&n); printf(" >> Please input the %d elements of matrix A(%d*%d) one by one:\n",n*n,n,n); for(i=0;i> Please input the %d elements of matrix B(%d*1) one by one:\n",n,n); for(i=0;i> The solution of Ax=B is x(%d*1):\n",n); for (i=0;i #include #include #define MAX 255 void MatrixMul(a,b,m,n,k,c) /*实矩阵相乘*/ int m,n,k; /*m 矩阵 A 的行数, n 矩阵 B 的行数, k 矩阵 B 的列数*/ double a[],b[],c[]; /*a 为 A 矩阵, b 为 B 矩阵, c 为结果,即 c = AB */ { int i,j,l,u; /*逐行逐列计算乘积*/ for (i=0; i<=m-1; i++) for (j=0; j<=k-1; j++) { u=i*k+j; c[u]=0.0; for (l=0; l<=n-1; l++) c[u]=c[u]+a[i*n+l]*b[l*k+j]; } return; } int brinv(a,n) /*求矩阵的逆矩阵*/ int n; /*矩阵的阶数*/ double a[]; /*矩阵 A*/ { int *is,*js,i,j,k,l,u,v; double d,p; is=malloc(n*sizeof(int)); js=malloc(n*sizeof(int)); for (k=0; k<=n-1; k++) { d=0.0; for (i=k; i<=n-1; i++) /*全选主元,即选取绝对值最大的元素*/ for (j=k; j<=n-1; j++) { l=i*n+j; p=fabs(a[l]); if (p>d) { d=p; is[k]=i; js[k]=j;} } /*全部为 0,此时为奇异矩阵*/ if (d+1.0==1.0) { 第三部分 数值计算与趣味数学篇 X 221177 free(is); free(js); printf(" >> This is a singular matrix, can't be inversed!\n"); return(0); } /*行交换*/ if (is[k]!=k) for (j=0; j<=n-1; j++) { u=k*n+j; v=is[k]*n+j; p=a[u]; a[u]=a[v]; a[v]=p; } /*列交换*/ if (js[k]!=k) for (i=0; i<=n-1; i++) { u=i*n+k; v=i*n+js[k]; p=a[u]; a[u]=a[v]; a[v]=p; } l=k*n+k; a[l]=1.0/a[l]; /*求主元的倒数*/ /* a[kj]a[kk] -> a[kj] */ for (j=0; j<=n-1; j++) if (j!=k) { u=k*n+j; a[u]=a[u]*a[l]; } /* a[ij] - a[ik]a[kj] -> a[ij] */ for (i=0; i<=n-1; i++) if (i!=k) for (j=0; j<=n-1; j++) if (j!=k) { u=i*n+j; a[u]=a[u]-a[i*n+k]*a[k*n+j]; } /* -a[ik]a[kk] -> a[ik] */ for (i=0; i<=n-1; i++) if (i!=k) { u=i*n+k; a[u]=-a[u]*a[l]; } } for (k=n-1; k>=0; k--) { /*恢复列*/ if (js[k]!=k) for (j=0; j<=n-1; j++) { u=k*n+j; v=js[k]*n+j; p=a[u]; a[u]=a[v]; a[v]=p; } /*恢复行*/ if (is[k]!=k) for (i=0; i<=n-1; i++) 221188 W C 语言实例解析精粹 { u=i*n+k; v=i*n+is[k]; p=a[u]; a[u]=a[v]; a[v]=p; } } free(is); free(js); return(1); } print_matrix(a,n)/*打印的方阵 a 的元素*/ int n; /*矩阵的阶数*/ double a[]; /*矩阵 a*/ { int i,j; for (i=0; i> Please input the order n of the matrix (n>0): "); scanf("%d",&n); } printf(" >> Please input the elements of the matrix one by one:\n >> "); for(i=0;i”。 TC 2.0 的图形库函数主要有 6 大类:图形系统管理、屏幕管理、绘图函数、图形属性控制、 填充和图形方式下的文本操作等。 (1)图形系统管理 在一般缺省情况下,屏幕为 80 列 50 行的文本方式,此时,所有的图形函数均不能操作,因 此在使用图形函数绘图之前,必须将屏幕显示适配器设置为一种图形模式,即所谓的“图形方式 初始化”。在绘图完毕后,要回到文本方式,必须关闭图形方式。TC 2.0 提供了 14 个函数对图形 系统进行控制和管理工作。 ① 图形方式初始化通过函数 initgraph 来完成。其调用格式为: initgraph(*gdriver, *gmode, *path); 函数 initgraph 是通过从磁盘上装入一个图形驱动程序来初始化图形系统,并将系统设置为图 形方式。其中 3 个参数的含义如下。 gdriver 是一个整型值,用来指定要装入的图形驱动程序,该值在头文件 graphics.h 中定义, 常用的是 DETECT、EGA、VGA 和 IBM8514。使用 DETECT,由系统自动检测图形适配器的最 高分辨率模式,并装入相应的图形驱动程序。 gmode 是一个整型值,用来设置图形显示模式,不同的图形驱动程序有不同的图形显示模式,即 使是同一个图形驱动程序下,也有几种图形显示模式。图形显示模式决定了显示的分辨率、可同时显 示的颜色的多少、调色板的设置方式以及存储图形的页数。常用的几种显示模式如表 106-1 所示。 path 是一个字符串,用来指明图形驱动程序所在的路径。如果驱动程序就在用户当前目录下, 则该参数可以为空字符串,否则应给出具体的路径。一般情况下,Turbo C 安装在 C 盘的 TC 目 录中,则该路径为 C:\TC,如果写在参数中则为“C:\\TC”。 表 106-1 几种常用的显示模式 图形驱动程序( gdriver) 图形显示模式( gmode) 值 分辨率 颜色数 页 EGA EGALO EGAHI 0 1 640×200 640×350 16 16 1 2 VGA VGALO VGAMED VGAHI 0 1 2 640×200 640×350 640×480 16 16 16 2 2 1 IBM8514 IBM8514LO IBM8514HI 0 1 640×480 1024×768 256 256 例如,在程序中使用 VGA 图形驱动程序,图形显示模式为 VGAHI,即 VGA 高分辨率图形 模式,分辨率为 640×480,则 initgraph 函数的调用方式如下: int gdriver=VGA,gmode=VGAHI; initgraph(&gdriver,&gmode,”C:\\TC”); 222288 W C 语言实例解析精粹 也可以用整型常数代替符号常数,例如: int gdriver=9,gmode=2; initgraph(&gdriver,&gmode,”C:\\TC”); 这两种方式是等效的。 另外,可使用 DETECT 模式,由系统自动检测,并把图形显示模式设置为检测到的驱动程序 的最高分辨率。例如: int gdriver=DETECT,gmode; initgraph(&gdriver,&gmode,”C:\\TC”); ② 关闭图形方式。在运行图形程序绘图结束后,要回到文本方式,以进行其他工作,这时应 关闭图形方式。关闭图形方式要调用函数 closegraph。其调用格式为: closegraph(); 其作用是释放所有图形系统分配的存储区,恢复到调用 initgraph 之前的状态。 (2)屏幕管理 TC 2.0 提供了 11 个函数用于对屏幕和视图区进行控制管理。常用的有以下 3 种。 ① 设置视图区 setviewport(x1,y1,x2,y2,c); 该函数在屏幕上定义一个以(x1,y1)为左上角坐标,(x2,y2)为右下角坐标的视图区。c 为裁剪 状态参数,当 c=1 时,超出视图区的图形部分被自动裁剪掉,当 c=0 时,则对超出视图区的图 形不做裁剪处理。 视图区建立后,所有的图形输出坐标都是相对于当前视图区的,即视图区的左上角点为坐标 原点(0,0),而与图形在屏幕上的位置无关。 ② 清除视图区 清除视图区函数为 clearviewport,它的作用是清除当前的视图区,将当前点位置设置于屏幕 的左上角(0,0)点。执行后,原先的视图区不再存在。调用格式为: clearviewport(); ③ 清屏 清屏使用函数 cleardevice,它的作用是清除全屏幕,并将当前点位置设置为原点(0,0)。但是 其他的图形系统设置保持不变,如线型、填充模式、文本格式和模式等,如果设置了视图区,则 视图区的设置保持不变,包括当前点位置设置在视图区的左上角。调用格式为: cleardevice(); (3)绘图函数 绘图函数是编写绘图程序的基础,也是任何一种图形软件的核心内容。Turbo C 的 BGI (Borland Graphics Interface)提供了大量的基本绘图函数,以方便图形设计。这些绘图函数可分为 直线类、圆弧类、多边形类等。 本实例主要说明直线类绘图函数的用法。 直线类绘图函数 直线类绘图函数用于绘制直线,可以用两种坐标,绝对坐标和相对坐标。 (1)line 函数 line 函数用于在指定两点之间画一条直线段: line(int x1, int y1, int x2, int y2); 参数 x1、y1、x2、y2 使用绝对坐标,(x1,y1)和(x2,y2)分别为直线的两个端点坐标。用 line 函数画直线时,当前点的位置不变。 (2)lineto 函数 第四部分 图形篇 X 222299 lineto 函数用于从当前点位置到指定位置(x,y)画一条直线,并改变当前点的位置。所以在 画一条直线的同时,当前点的位置也移到了指定点,即直线的终点。调用格式为: lineto(int x, int y); (3)moveto 函数 函数 moveto(int x, int y)用于将当前点移动到(x,y)。 (4)linerel 函数 linerel 函数使用相对坐标画直线。其功能是从当前点位置开始画线到指定点位置,指定点位 置的坐标不是以绝对坐标形式给出的,而是以其相对于当前点(即直线的起点)位置的坐标增量 给出的。调用格式: linerel(int dx, int dy); 假设当前点位置坐标(x,y),则“linerel(dx,dy);”等效于“lineto(x+dx, y+dy);”。 (5)moverel 函数 该函数的功能与 moveto 函数相似,但它使用的是相对坐标。moverel(int dx, int dy)用于将当 前点位置在 x 和 y 方向上分别移动增量 dx 和 dy。 程序代码 【程序 106】 绘制直线 #include void main() { int gdriver=DETECT,gmode; initgraph(&gdriver,&gmode,"c:\\tc"); cleardevice(); printf("\n Draw lines with function 'line'."); line(160,120,480,120); line(480,120,480,360); line(480,360,160,360); line(160,360,160,120); getch(); cleardevice(); getch(); printf("\n Draw lines with function 'lineto'."); moveto(160,120); lineto(480,120); lineto(480,360); lineto(160,360); lineto(160,120); getch(); cleardevice(); getch(); printf("\n Draw lines with function 'linerel'."); moveto(160,120); linerel(320,0); linerel(0,240); linerel(-320,0); linerel(0,-240); getch(); closegraph(); 223300 W C 语言实例解析精粹 } 归纳注释 要使用 Turbo C 2.0\3.0 中的画图模式,首先应该在 Option 菜单中的 Linker→Libraries 选项里 选中 Graphic Library。然后每次使用时应该在程序中进行初始化,即在代码中加入两条语句“int gdriver=DETECT,gmode; initgraph(&gdriver,&gmode,“xxxxx”);”,其中 xxxxx 是用户的 Turbo C 的图形库目录所在路径。退出程序前,应该调用 closegraph()函数退出图形模式,回到文本模式。 本实例通过绘制矩形来说明直线类绘图函数的用法。 实例 107 绘制圆 实例说明 本实例实现圆形的绘制。程序运行效果如图 107-1 所示。 图 107-1 实例 107 程序运行效果 实例解析 圆的绘图函数是 circle。该函数用于以指定圆心和半径画圆。其调用格式为: circle(int x, int y, int r); 其中(x,y)为指定圆心的坐标,r 为圆的半径。例如: circle(320,240,100); 执行结果是以点(320,240)为圆心,以 100 为半径画一个圆。 程序代码 【程序 107】 绘制圆 /* draw circles */ #include 第四部分 图形篇 X 223311 void main() { int b; int gdriver=DETECT,gmode; initgraph(&gdriver,&gmode,"c:\\tc"); cleardevice(); printf("\n\n\n This program shows the circle graph.\n"); for(b=10;b<=140;b+=10) circle(320,240,b); getch(); closegraph(); } 归纳注释 程序实现了画一组同心圆。首先初始化图形模式,再清除屏幕,然后打印提示信息,通过 for 循环语句画一组圆心相同,半径依次变大的同心圆,最后,按任意键退出图形模式,程序结束。 实例 108 绘制圆弧 实例说明 本实例实现圆弧的绘制。程序运行效果如图 108-1 所示。 图 108-1 实例 108 程序运行效果 实例解析 画圆弧的函数是 arc,其调用格式是: arc(int x, int y, int angs, int ange, int r); 其中,(x,y)为圆弧所在圆心的坐标,angs,ange 分别为圆弧的起始角和终止角,单位为“度”, r 为圆弧的半径。 例如,调用“arc(320,240,90,180,100);”的结果是以点(320,240)为圆心,100 为半径,从 90° ~180°画了四分之一圆的圆弧。 223322 W C 语言实例解析精粹 当圆弧的起始角 angs=0,终止角 ange=360 时,则可以画出一个整圆。 程序代码 【程序 108】 绘制圆弧 /* draw arc */ #include void main() { int b; int gdriver=DETECT,gmode; initgraph(&gdriver,&gmode,"c:\\tc"); cleardevice(); printf("\n\n\n This program shows the arc graph.\n"); for(b=10;b<=140;b+=10) arc(320,240,0,150,b); getch(); closegraph(); } 归纳注释 程序首先初始化图形模式,再打印出提示信息,然后通过 for 循环画一组半径逐渐增大的从 0°~150°的同心圆弧,最后按任意键退出图形模式,结束程序。 实例 109 绘制椭圆 实例说明 本实例实现椭圆的绘制。程序运行效果如图 109-1 所示。 图 109-1 实例 109 程序运行效果 第四部分 图形篇 X 223333 实例解析 画椭圆的函数是 ellipse。该函数用于画椭圆弧或椭圆。其调用格式为: ellipse(int x, int y, int angs, int ange, int xr, int yr); 其中,(x,y)为椭圆的中心坐标,angs,ange 为椭圆弧的起始角和终止角,单位为“度”,xr, yr 分别为椭圆的水平半轴和垂直半轴。 如果 angs=0,ange=360,则可以画出一个完整的椭圆。 xr>yr,则画出长轴为水平方向的椭圆或椭圆弧; xr void main() { int a=150,b; int gdriver=DETECT,gmode; initgraph(&gdriver,&gmode,"c:\\tc"); cleardevice(); for(b=10;b<=140;b+=10,a+=10) ellipse(320,240,0,360,a,b); getch(); closegraph(); } 归纳注释 本实例程序先初始化图形模式,再打印出提示信息,然后通过 for 循环画一组半轴逐渐增大 的从 0°~360°的同中心椭圆,最后按任意键退出图形模式,结束程序。 多边形绘图函数主要有 rectangle(int x1, int y1, int x2, int y2),用于绘制以(x1,y1)为左上角 点,以(x2,y2)为右下角点的矩形;drawpoly(int nps, int *pxy),用于绘制具有 nps 个顶点的多边 折线,数组 pxy 用于存放这些顶点的坐标,比如,有一个名为 xy 的整型数组中存放了 4 个顶点的 坐标为[x1,y1, x2, y2, x3, y3, x4, y4],则调用格式为 drawpoly(4,xp),如果最后一个点的坐标与第一 个点的坐标相同,则可以画一个封闭的多边形。 实例 110 设置背景色和前景色 实例说明 本实例实现对背景色和前景色的设置。程序运行效果如图 110-1 所示。 223344 W C 语言实例解析精粹 图 110-1 实例 110 程序运行效果 实例解析 图形的属性控制包括颜色和线型。颜色包括背景色和前景色。背景色指的是屏幕的颜色,即 绘图时的底色。前景色指的是绘图时图形线条所用的颜色。任何绘图函数都是在当前的颜色(包 括背景色和前景色)和线型状态下进行绘图的。在前面的例子中没有提到颜色和线型,是因为用 了系统的缺省值。系统的缺省值是:背景色为黑色,前景色为白色,线型为实型。 如果要用到系统缺省值以外的颜色和线型,则可以利用图形颜色和线型控制函数来设置。 (1)背景色设置 设置背景色函数为 setbkcolor,其功能是设置绘图时的背景颜色。调用格式为: setbkcolor(int color); 参数 color 代表所取的颜色,可以为整型常数,也可以用符号常数表示。TC 2.0 定义的颜色 如表 110-1 所示。 表 110-1 颜色表 符 号 名 数 值 颜 色 符 号 名 数 值 颜 色 BLACK 0 黑 色 DARKGRAY 8 深灰色 BLUE 1 蓝 色 LIGHTBLUE 9 浅蓝色 GREEN 2 绿 色 LIGHTGREEN 10 浅绿色 CYAN 3 青 色 LIGHTCYAN 11 浅青色 RED 4 红 色 LIGHTRED 12 浅红色 MAGENTA 5 紫红色 LIGHTMAGENTA 13 淡紫色 BROWN 6 棕 色 YELLOW 14 黄 色 LIGHTGRAY 7 浅灰色 WHITE 15 白 色 例如,要把背景色设置成浅蓝色,可以调用:“setbkcolor(LIGHTBLUE);”或“ setbkcolor(9);”。 (2)设置前景色 函数 setcolor 用于设置前景色,即绘图用的颜色。调用格式为“setcolor(int color);”其参数 color 的含义与 setbkcolor 的参数相同。 程序代码 【程序 110】 设置背景色和前景色 /* set color */ #include 第四部分 图形篇 X 223355 void main() { int cb,cf; int gdriver=DETECT,gmode; initgraph(&gdriver,&gmode,"c:\\tc"); cleardevice(); printf("\n This program shows how to set color of graph."); printf("\n Press any ket to change color..."); for(cb=0;cb<=15;cb++) { setbkcolor(cb); for(cf=0;cf<=15;cf++) { setcolor(cf); circle(100+cf*25,240,100); } getch(); } getch(); closegraph(); } 归纳注释 程序首先初始化图形方式,清除屏幕,打印输出提示信息,然后在第一个 for 循环中依次设 置 0~15 号颜色,在每种背景色下,依次设置 0~15 号前景色,并画一个该颜色的圆,按任意键, 换一种背景色,如此循环,最后按任意键,关闭图形方式,结束程序。 实例 111 设置线条类型 实例说明 本实例实现对线条类型的设置。程序运行结果如图 111-1 所示。 图 111-1 实例 111 程序运行结果 223366 W C 语言实例解析精粹 实例解析 设置线条类型采用函数 setlinestyle 来实现。该函数用于设置当前绘图所用的线型和宽度,这 些设置仅限于对直线类图形有效。其调用格式为: setlinestyle(int sty, int pat, int b); 该函数所用的 3 个参数含义如下。 (1)sty 用来定义所画直线的类型。包括 SOLID_LINE、DOTTED_LINE、CENTER_LINE、 DASHED_LINE 和 USERBIT_LINE,其对应数值依次为 0~4,含义为实线(缺省)、点线、中心 线、虚线和用户自定义类型。 (2)pat 用于用户自定义线型。如果是使用前 4 种系统预定义的线型,则该参数可取 0 值。 (3)b 指定所画直线的粗细,以像素为单位。可取 NORM_WIDTH 和 THICK_WIDTH 两种值 (对于数值为 1 和 3),含义是 1 个像素宽(缺省)和 3 个像素宽。 当函数 setlinestyle 的第一个参数为 USERBIT_LINE(或 4)时,可以由用户自己定义直线类 型。此时,第 3 个参数的意义不变。在第二个参数中定义直线的类型,该参数是一个 16 位二进制 码,每一位(bit)表示一个像素。某一位(bit)置 1 时表示直线上相应位置有显示,置 0 时为空。 例如,1111 1111 1111 1111,16 位全置 1,因此是画一条实线。而 1010 1010 1010 1010,隔位置 1, 因此是画一条点线。 在实际编写程序时,一般把 16 位二进制数转换为 4 位十六进制数,每 4 位二进制数转换为 1 位十六进制数。故上面的两个例子转换为十六进制数为 FFFF 和 AAAA。函数的调用方法为 “setlinestyle(4,0xAAAA,1);”,用这种方法,可以根据需要定义各种线型。 程序代码 【程序 111】 设置线条类型 /* set line style */ #include void main() { int i,j,c,x=50,y=50,k=1; int gdriver=DETECT,gmode; initgraph(&gdriver,&gmode,"c:\\tc"); cleardevice(); setbkcolor(9); setcolor(4); for(j=1;j<=2;j++) { for(i=0;i<4;i++) { setlinestyle(i,0,k); line(50,50+i*50+(j-1)*200,200,200+i*50+(j-1)*200); rectangle(x,y,x+210,y+80); circle(100+i*50+(j-1)*200,240,100); } k=3; x=50; 第四部分 图形篇 X 223377 y=250; } getch(); closegraph(); } 归纳注释 程序用于演示系统预定义的 4 种线型。程序首先初始化图形方式,清屏,设置背景色和前景色, 在 for 循环中设置两种不同的线型宽度,在第 2 个 for 循环中设置 4 种不同的线型,并分别绘制直线、 矩形和圆。可以看出,直线会产生变化,而矩形和圆只在不同像素的线型粗细时会有变化。 实例 112 设置填充类型和填充颜色 实例说明 本实例实现对填充类型以及填充颜色的设置。程序运行结果如图 112-1 所示。 图 112-1 实例 112 程序运行结果 实例解析 (1)setfillstyle 函数用来设置当前填充模式和填充颜色,以便用于填充一个指定的封闭区域, 调用格式为: setfillstyle(int pattern, int color); 其中,参数 pattern 用于指定填充模式。系统有 13 种预定义的填充模式,见表 112-1。参数 color 用于指定填充颜色。 223388 W C 语言实例解析精粹 表 112-1 系统预定义的填充模式 宏 值 含 义 宏 值 含 义 EMPTY_FILL 0 用背景颜色填充 HATCH_FILL 7 网格线填充 SOLDI_FILL 1 实填充 XHATCH_FILL 8 斜网格线填充 LINE_FILL 2 用线“-”填充 INTERLEAVE_FILL 9 间隔点填充 LTSLASH_FILL 3 用斜线填充(阴影线) WIDE_DOT_FILL 10 稀疏点填充 SLASH_FILL 4 用粗斜线填充(粗阴影线) CLOSE_DOT_FILL 11 密集点填充 BKSLASH_FILL 5 用粗反斜线填充(粗阴影线) USER_FILL 12 用户定义的模式 LTBKSLASH_FILL 6 用反斜线填充(阴影线) (2)floodfill 函数用于对一个指定区域进行填充操作,其填充模式和颜色由 setfillstyle 函数指 定。其调用格式为: floodfill(int x, int y, int bcolor); 参数(x,y)指位于填充区域内任一点的坐标,该点作为填充的起始点。bcolor 为填充区域的 边界颜色。例如: setcolor(RED); circle(320,240,150); setfillstyle(SOLID_FILL,GREEN); floodfill(320,240,RED); 该段程序的作用是,用红颜色画一个圆,然后用绿色色块填充该圆。 (3)其他填充函数。以下所列的几个填充函数,均需事先由函数 setfillstyle 指定当前的填充 模式和颜色。 ① fillellipse(int x, int y, int rx, int ry)函数,用于画一个填充的实椭圆,用当前颜色画出边线。 其参数(x,y)为椭圆中心坐标,rx,ry 分别为椭圆的水平和垂直半轴长。 ② sector(int x, int y, int angs, int ange, int rx, int ry)函数,用于画一个填充的椭圆扇区,用当前 颜色画出边线。其参数意义与 ellipse 函数相同。 ③ fillpoly(int nps, int *pxy)函数,用于画并填充一个多边形(必须使首末两点重合以确保多 边形封闭),边线用当前颜色画出。其参数意义与 drawpoly 函数相同。 程序代码 【程序 112】 设置填充类型和填充颜色 //本实例源码参见光盘 归纳注释 程序首先初始化图形方式,清屏,设置背景色,在 for 循环中设置不同的前景色,画矩形,并设 置不同的填充颜色,填充该矩形,形成一系列的填充矩形。最后按任意键,关闭图形方式,结束程序。 实例 113 图形文本的输出 实例说明 本实例实现图形模式下的文本输出。程序运行结果如图 113-1 所示。 第四部分 图形篇 X 223399 图 113-1 实例 113 程序运行结果 实例解析 (1)outtext 函数 outtext 函数用于在当前位置输出一个文本字符串。其调用格式: outtext(char *text); 参数 text 是一个字符串。例如: outtext("Hello world"); 将在当前位置输出一个字符串“Hello world”。 (2)outtextxy 函数 outtextxy 函数用来在(x,y)位置输出一个字符串。其调用格式为: outtextxy(int x, int y, char *text); 参数(x,y)为指定位置的坐标,text 为待输出的字符串。 程序代码 【程序 113】 图形文本的输出 //本实例源码参见光盘 归纳注释 本实例演示了图形方式下简单的文本输出。程序先初始化图形方式,清屏,设置前景色,移 动当前点位置,输出提示信息,再设置前景色,移动当前点位置到下一行,再输出一串字符串, 如此反复。接着在 for 循环中画一系列的直线,形成窗口图形,并在窗口中输出“Hello world!”。 最后提示按任意键,关闭图形方式,结束程序。 224400 W C 语言实例解析精粹 实例 114 金刚石图案 实例说明 将半径为 R 的圆周等分成 n 份,然后用直线将各等分点两两相连,这样形成的图案称之为“金 刚石”图案。本实例将实现金刚石图案的绘制。程序运行结果如图 114-1 所示。 图 114-1 实例 114 程序运行结果 实例解析 程序要求输入等分的份数 n 和圆周的半径 r,并根据这两个参数计算圆周上等分点的坐 标,存入 x,y 数组,最后对这些等分点用 line 函数进行两两连线,形成所要的“金刚石” 图案。 程序代码 【程序 114】 金刚石图案 //本实例源码参见光盘 归纳注释 在本实例程序中,简单的应用 line 函数进行连线,在等分份数 n 为奇数时,可以用 moveto 函数把当前点移到一个等分点上,再用 lineto 一笔绘完整个图案。 第四部分 图形篇 X 224411 实例 115 飘带图案 实例说明 直线段的起点和终点坐标如果均按余弦和正弦函数的规律变化,则形成具有不同长度和不同 位置的线段组成的图案,该图案看起来好像是随风飘扬起来的一条飘带。本实例将实现飘带图案 的绘制。程序运行结果如图 115-1 所示。 图 115-1 实例 115 程序运行结果 实例解析 绘制这样的图案,可采用如下算法:通过 for 循环计算直线段的起点 x 轴坐标、y 轴坐标,终 止点的 x 轴坐标、y 轴坐标(与起点的 y 轴坐标相同),这些坐标满足余弦和正弦函数的规律。 )max(2 1)8.1cos()80)(max(2 12 ))2max()5.2cos()8sin(90()max(1 )max(2 1)6.1cos()80)(max(2 11 xaxx yaayy xaxx +×−= +××−= +×−= 其中,max(x)是指水平方向的最大分辨率(通常为 640),max(y)是指垂直方向的最大分辨率 (通常为 480),a∈(a,π),并以 π/380 的步长递增。 程序代码 【程序 115】 飘带图案 //本实例源码参见光盘 归纳注释 通过改变 x1,y1,x2 计算公式中 cos 和 sin 函数前面的幅值的大小,可以改变飘带的宽度和 样式,读者可以试着编写类似的程序。 224422 W C 语言实例解析精粹 实例 116 圆环图案 实例说明 把一个半径为 Rlarge 的圆周等分成 n 份,然后以每个等分点为圆心,以 Rsmall 为半径画 n 个圆,将形成环的图案。本实例将实现圆环图案的绘制。程序运行效果如图 116-1 所示。 图 116-1 实例 116 程序运行效果 实例解析 画圆环图案的算法是在 for 循环中,依次计算 n 等分点的坐标,并以该坐标作为圆心,以 Rsmall 为半径画圆。这样的圆环有 3 种情况,Rl>Rs、Rl=Rs 和 RlRs 情况的提示,调用 circles 函数画 Rl>Rs 情况下的圆 弧图案,再依次改变前景色,画 Rl=Rs 和 Rl #include #include /*这是根据给出的圆心坐标和点坐标分别在 8 个象限画点的子程序*/ void circlePoint( int xCenter, int yCenter, int x, int y ) { putpixel( xCenter + x, yCenter + y, YELLOW ); putpixel( xCenter - x, yCenter + y, YELLOW ); putpixel( xCenter + x, yCenter - y, YELLOW ); putpixel( xCenter - x, yCenter - y, YELLOW ); putpixel( xCenter + y, yCenter + x, YELLOW ); putpixel( xCenter - y, yCenter + x, YELLOW ); putpixel( xCenter + y, yCenter - x, YELLOW ); putpixel( xCenter - y, yCenter - x, YELLOW ); } void myCircle(int xCenter,int yCenter,int radius) { int x, y, p; /*初始化各个参数*/ x = 0; y = radius; p = 1 - radius; circlePoint(xCenter, yCenter, x, y); /*循环中计算圆上的各点坐标*/ while( x < y ) { x++; if( p < 0 ) p += 2*x+1; else { y--; p+=2*(x-y)+1; } 225544 W C 语言实例解析精粹 circlePoint( xCenter, yCenter, x, y); } } void main() { int gdriver=DETECT, gmode; /*这是用 c 画图时必须要使用的图像入口*/ int i; int xCenter, yCenter, radius; printf("Please input center coordinate :(x,y) "); scanf("%d,%d", &xCenter, &yCenter ); printf("Please input radius : "); scanf("%d", &radius ); /*这条语句初始化整个屏幕并把入口传给 gdriver,注意引号中是 tc 中 bgi 目录的完整路径*/ registerbgidriver(EGAVGA_driver); initgraph(&gdriver, &gmode, "c:\\tc"); setcolor( BLUE ); myCircle(xCenter, yCenter, radius); getch(); closegraph(); return; } 归纳注释 本实例采用图形学的方法画圆,而不是调用 Turbo C 的 circle 函数来画。可以说,应用 putpixel(int x, int y, int color)函数可以画出所有的图形(该函数是在点(x,y)画一个颜色为 color 的点)。但是,这个函数是一个像素一个像素的画,效率太低。因此,Turbo C 提供了众多的库函 数来画各种图形。在这些库函数不能满足需要时,可采用 putpixel 函数来画所需要的图形。 实例 126 递归法绘制三角形图案 实例说明 本实例用递归方法绘制三角形图案。程序运行效果如图 126-1 所示。 图 126-1 实例 126 程序运行效果 第四部分 图形篇 X 225555 实例解析 C 语言允许函数之间的递归调用,利用这种特点,也可以设计出构思巧妙的图案。整个递归三 角形图案是由大小不同的许多三角形组成的。除了最外面那个大三角形外,其余的三角形都由连接 包含它的三角形的三边中点构成的。但是,这种连接三角形 的三边中点是有选择的,因为如果没有选择地全部连线,那 未必会形成如图 126-1 所示的那样许多大小不等的三角形。 这种选择的方法如图 126-2。在三角形 ABC 中,连结三边中 点形成三角形 PQR,这样就最终产生 4 个小三角形:△PQR, △APR,△PBQ,和△PQC。在这 4 个小三角形中,只将其 中 3 个位于角上的小三角形(即△APR,△PBQ,△PQC) 连接其三边中点形成新的三角形。 程序中,主要使用一个绘制三角形的函数 tria(int xa, int ya, int xb, int yb, int xc, int yc, int n)。 该函数用来连接一个三角形的三边中点绘制一个三角形,同时又递归调用自身,在形成的 3 个角 上的三角形中连接三边中点构成三角形。 程序代码 【程序 126】 递归法绘制三角形 //本实例源码参见光盘 归纳注释 主程序首先要求输入递归调用的深度,接着初始化图形方式,清屏,设置背景色和前景色, 画最外面的大三角形,调用函数 tria 递归绘制三角形,最后按任意键,关闭图形方式,结束程序。 函数 void tria(int xa, int ya, int xb, int yb, int xc, int yc, int n)中参数(xa,ya),(xb,yb),(xc,yc)为三 角形 3 个顶点坐标,n 为递归深度。 实例 127 图形法绘制椭圆 实例说明 本实例实现用图形法绘制椭圆。程序运行效果如图 127-1 所示。 图 127-1 实例 127 程序运行效果 图 126-2 连点选择 225566 W C 语言实例解析精粹 实例解析 绘制曲线的基本方法 平面解析几何中,把一条曲线中的点(x,y)满足的关系 y=f(x)函数式称为曲线的方程,同 样,该曲线即为这个方程的曲线。例如圆的方程为 x2+y2=R2,椭圆的方程为 x2/ a2+y2/b2=1。在绘 制这些曲线时,可以借助各种标志工具,比如画圆可以用圆规,画椭圆可以用椭圆规,但是对于 非圆曲线,绘制时的更一般的方法是借助于曲线板。先在平面上确定一些满足条件的、位于曲线 上的坐标点,然后借用曲线板把这些点分段光滑地连接成曲线。绘出的曲线的精确程度,取决于 所选择的数据点的精度和数量,坐标点的精度越高、点的数量取得越多,连成的曲线越接近于理 想曲线。 其实,这个方法也是用计算机来绘制各种曲线的基本原理,即把曲线离散化,把它们分割 成很多短直线段,用这些短直线段组成的折线来逼近曲线。至于这些短直线段取多长,则取决 于图形输出设备的精度和绘制的曲线所要求的精度,但所要求的精度不能逾越图形设备实际具 备的精度。 椭圆曲线的绘制 椭圆的标准方程为: 12 2 2 2 =+ b y a x 其中,a 和 b 分别为椭圆的长、短半轴。在实际绘图中,采用参数方程来表示椭圆更为方便,即: ⎩ ⎨ ⎧ ×== ×== tbtgy tatfx sin)( cos)( (0≤t≤2π) 式中,t 为参数变量,它的取值范围从 0~2π,即一个圆周。可以看出,这个参数的实际意义 是椭圆上的点所对应的中心角。这样,根据参数方程,每确定一个 t 值,就可以计算得到对应于 t 值的位于该椭圆上的一个确定点(x,y)。当 t=ti 时,可以得到椭圆上一点(xi,yi): ⎩ ⎨ ⎧ ×= ×= ii ii tby tax sin cos 让参变量 t 增加一个增量Δt,使 ti+1=ti+Δt,代入参数方程,得另一点(xi+1,yi+1): ⎩ ⎨ ⎧ ×= ×= ++ ++ 11 11 sin cos ii ii tby tax 连接两点(xi,yi)和(xi+1,yi+1),便可以近似地认 为绘制了椭圆上的一段弧,如图 127-2 所示。由于计算机 的运算速度很快,并且运算精度极高,所以可以很容易地 把Δt 值取得很小,以便能获得更多的坐标点,这样就可 以使绘制出的椭圆更为精确,看上去是一条极为光滑的曲 线。 图 127-2 椭圆弧 第四部分 图形篇 X 225577 程序代码 【程序 127】 图形法绘制椭圆 //本实例源码参见光盘 归纳注释 程序用自定义的椭圆绘制函数 ellipse1(int x0, int y0, int a, int b, int dt)绘制了椭圆群的星形图 案。该函数的参数(x0,y0)为椭圆的中心坐标,a、b 分别为椭圆的长、短半轴,dt 为参变量的 增量,即Δt。 实例 128 抛物样条曲线 实例说明 本实例实现抛物样条曲线的绘制。程序运行效果如图 128-1 所示。 图 128-1 实例 128 程序运行效果 实例解析 抛物样条曲线 工程上通常需要把一系列的离散的测量点用一条光滑的曲线连接起来,绘制成曲线图形。在 拟合生成曲线的众多方法中,一般总要选择一种简单一些的曲线,作为拟合生成其他曲线的基本 曲线,然后对这种基本曲线做一些适当的数学处理,来生成完整的拟合曲线。抛物样条曲线,顾 名思义,就是选择抛物线这样一种较为简单的二次曲线作为基本曲线来拟合给定离散型值点生成 的曲线。 设有不在同一直线上的 3 点:P1(x1,y1),P2(x2,y2),P3(x3,y3),则通过该给定的 3 点 定义的一条抛物线的方程为: 225588 W C 语言实例解析精粹 2 321)( tAtAAtP ++= 3 2 2 2 1 2 )2()44()132( PttPttPtt −+−++−= (0≤t≤1) 写成矩阵的参数方程形式为: [][] ⎥ ⎥ ⎥ ⎦ ⎤ ⎢ ⎢ ⎢ ⎣ ⎡ ⎥ ⎥ ⎥ ⎦ ⎤ ⎢ ⎢ ⎢ ⎣ ⎡ −− − = 33 22 11 2 001 143 242 1)()( yx yx yx tttytx 这样,根据参变量 t 的取值,就可以一一计算出位于曲线上的数据点,然后顺次连线绘出图形。 抛物线加权合成 设有一离散型值点列 Pi(i=1,2,… ,n),则可以按上面的方程每经过相邻 3 点作一段抛物线, 一共可以作 n−2 条抛物线段。设其中第 i 条抛物线段为经过 Pi,Pi+1 ,Pi+2 3 点,其表达式为: 2 2 1 22 )2()44()132()( ++ −+−++−= iiiiiiiiiii PttPttPtttS (0≤t≤1) 同理,第 i+1 条抛物线为经过 Pi+1,Pi+2 ,Pi+3 3 点,其表达式为(见图 128-2): 31 2 12 2 1111 2 111 )2()44()132()( +++++++++++ −+−++−= iiiiiiiiiii PttPttPtttS (0≤t≤1) 一般来说,每两段曲线之间的搭接区间,两条抛物线是不可能重合的。为了用一条光滑的曲 线拟合整个型值点列,必须有一个办法让这些抛物线段按照一 定的法则结合成一条曲线,这种结合的办法就是加权合成。 在加权合成的过程中,要选择两个合适的权函数 f(T)和 g(T),则加权合成后的曲线为 Pi+1(t): )()()()()( 111 +++ •+•= iiiii tSTgtSTftP 取权函数为 f(T)=1−T,g(T)=T,0≤T≤1,另取 T=2t,ti=0.5+t, ti+1=t,则有: 1 2323 1 )11012()44()( ++ +−+−+−= iii PttPttttP 3 23 2 23 )24()812( ++ −+++−+ ii PttPttt (i=1,2,…,n−3) (0≤t≤0.5) 上式表达了每相邻的 4 个点可以决定中间的一段抛物样条曲线。 抛物样条曲线的端点条件 n 个型值点 Pi 可以加权合成生成 n−3 段抛物样条曲线,但 n 个型值点之间有 n−1 个区段,其 首尾两段曲线 P1P2 和 Pn-1Pn 段,由于缺乏连续相邻的 4 点这样的条件而无法产生。为此,必须在 两端各加一个辅助点 P0 和 Pn+1。问题是这两点是如何加上去的,它依据什么原则,这就是所谓的 “端点条件”。这里,仅介绍两种常用的方法。 (1)自由端条件。这种方法的原理比较简单,它让所补之点 P0 和 Pn+1 与原两端点 P1 和 Pn 分 别重合,即 P0=P1,Pn+1=Pn。这样的补点方法称为自由端条件,这种方法一般适用于对曲线的两 端没有什么特殊的要求的情况。 (2)形成封闭曲线。为了在 n 个型值点之间形成封闭曲线,就要生成 n 段曲线段,而不是原 图 128-2 Si 和 Si+1 第四部分 图形篇 X 225599 理的 n−1 段。所以,在补点工作中要加上 3 个点,首先让首尾两点重合,然后各向前后延长一点, 即 Pn+1=P1,P0=Pn,Pn+2=P2。 程序代码 【程序 128】 抛物样条曲线 //本实例源码参见光盘 归纳注释 程序通过读取文件中点的坐标来绘制抛物样条曲线。抛物样条曲线的绘制函数 void parspl(int p[][2], int n, int k, int e)的参数 p 是型值点的坐标数组,n 是型值点数,k 是插值数,即把参变量 t 区间细分的份数,e 是端点条件,e=1 时,为自由端点,e=2 时为画封闭曲线。函数 marking 的 作用是对型值点进行标记,在调试程序时便于核对曲线程序是否正确,在实际应用中是不需要的。 实例 129 Mandelbrot 分形图案 实例说明 本实例实现 Mandelbrot 分形图案的绘制。程序运行效果如图 129-1 所示。 图 129-1 实例 129 程序运行效果 实例解析 分形图形是数学中一种很奇妙的现象,它们一般都可以通过简单的数学公式描述产生无比复 杂和美妙的图形。而且分形图形的一个最为奇妙之处就是它可以被无限细分,把它的某一个细节 放大后会得到和原来图形一样的效果。Mandelbrot 分形就是最著名的分形图形之一,它是通过使 用复数的迭代运算来得到图形的形状和颜色,虽然公式很简单,但是得到的结果却让人惊叹不已。 Mandelbrot 分形的原理如下。 用迭代公式: 226600 W C 语言实例解析精粹 [] [][][][]⎩ ⎨ ⎧ +−×−= = 011 0 zkzkzkz zInitz 其中 z[i]是复数,计算时要使用复数的运算法则。 Mandelbrot 图形集的初始化要求: −2.25≤Re(zInit)≤0.75 −1.25≤Im(zInit)≤1.25 其中 Re(z)表示 z 的实部,Im(z)表示 z 的虚部。 为了方便起见,最终的图形是画在复平面上的,以 x 轴为实部,y 轴为虚部,并用该点的迭 代次数作为该点的颜色。当图像上每一个点的迭代次数越多时,整个图形也就越清晰。 程序代码 【程序 129】 Mandelbrot分形图案 #include typedef struct { float x, y; } complex; /*定义复数的结构,x 表示实部,y 表示虚部*/ complex complexSquare( complex c ) /*计算复数的平方 (x+yi)^2 = (x^2-y^2) + 2xyi */ { complex csq; csq.x = c.x * c.x - c.y * c.y; csq.y = 2 * c.x * c.y; return csq; } int iterate( complex zInit, int maxIter ) /*迭代计算颜色,maxIter 是最多迭代的次数,*/ { complex z = zInit; int cnt = 0; /* 当 z×z > 4 的时候退出 */ while((z.x * z.x + z.y * z.y <= 4.0) && (cnt < maxIter)) { /*迭代公式:z[k] = z[k-1]^2 + zInit, cnt 是迭代次数*/ z = complexSquare( z ); z.x += zInit.x; z.y += zInit.y; cnt++; } return cnt; } void mandelbrot( int nx, int ny, int maxIter, float realMin, float realMax, float imagMin, float imagMax ) /*画 Mandelbrot 图形的主程序,参数意义如下 第四部分 图形篇 X 226611 nx: x 轴的最大值 ny: y 轴的最大值 maxIter: 迭代的最大次数 realMin: 初值 zInit 的实部最小值 realMax: 初值 zInit 的实部最大值 imagMin: 初值 zInit 的虚部最小值 imagMax: 初值 zInit 的虚部最大值 */ { float realInc = (realMax - realMin) / nx; /*x 轴迭代的步长*/ float imagInc = (imagMax - imagMin) / ny; /*y 轴迭代的步长*/ complex z; /*初值 zInit*/ int x, y; /*点(x,y)的横纵坐标*/ int cnt; /*迭代的次数*/ for( x = 0, z.x = realMin; x 226644 W C 语言实例解析精粹 #include void main() {int gd=VGA,gm=VGALO; /*registerbgidriver(EGAVGA_driver);*/ initgraph(&gd,&gm,"e:\\tc\\bgi"); /*设置图形模式*/ setcolor(YELLOW); rectangle(105,105,175,135); /*画正方形*/ full(120,120,YELLOW); /*调填充函数*/ getch(); /*等待*/ closegraph(); /*关闭图形模式*/ } #define DELAY_TIME 5/*填充点后延长的时间,用来观看填充的过程,单位为毫秒*/ int full(int x,int y,int color1)/*递归的填充函数*/ {int color2,x1,y1; x1=x; y1=y; if(kbhit())return; color2=getpixel(x1,y1); /*读(x,y)点颜色值*/ if(color2!=color1) /*判断是否与填充色相等*/ {putpixel(x1,y1,color1); /*画点(x1,y1) */ delay(DELAY_TIME); getch(); x1++; full(x1,y1,color1); /*递归调用*/ } x1=x; y1=y; color2=getpixel(x1-1,y1); /*读(x1-1,y1)点颜色值*/ if(color2!=color1) /*判断是否与填充色相等*/ {putpixel(x1,y1,color1); /*画点(x1,y1) */ delay(DELAY_TIME); x1--; full(x1,y1,color1); /*递归调用*/ } x1=x; y1=y; color2=getpixel(x1,y1+1); /*读(x1,y1+1)点颜色值*/ if(color2!=color1) /*判断是否与填充色相等*/ {putpixel(x1,y1,color1); /*画点(x1,y1) */ delay(DELAY_TIME); y1++; full(x1,y1,color1); /*递归调用*/ } x1=x; y1=y; color2=getpixel(x1,y1-1); /*读(x1,y1+1)点颜色值*/ if(color2!=color1) /*判断是否与填充色相等*/ {putpixel(x1,y1,color1); /*画点(x1,y1) */ delay(DELAY_TIME); y1--; full(x1,y1,color1); /*递归调用*/ } return; } 第四部分 图形篇 X 226655 归纳注释 程序中的常量 DELAY_TIME 可以控制填充速度,单位是毫秒。也可以将 delay(DELAY_TIME) 语句换成 getch()语句,实现手动填充。 要注意填充面积不要过大,否则递归层次太多,消耗大量内存,将导致 DOS 崩溃。图形库的 路径在编译运行程序时需要在 initgraph()函数中指出,具体路径可根据用户安装 TC 的位置来决定。 实例 133 VGA256 色模式编程 实例说明 本实例程序通过 DOS 中断调用实现直接写视频缓冲区的功能,并进行 VGA256 色模式编程。 程序运行效果如图 133-1 所示。 图 133-1 实例 133 程序运行效果 实例解析 运行该程序后,首先由主程序通过 InitScr()函数进行 13H 中断调用,再由 LineV()和 Rect()函 数子程序在屏幕上绘出如图 133-1 所示的效果图,然后等待用户输入任意键,系统通过调用 RstScr() 退出 13H 中断调用,最后退出 main()函数,程序结束。 程序代码 【程序 133】 VGA256 色 //本实例源码参见光盘 归纳注释 putpoint()函数是通过该程序中的常指针变量“char far *p”直接取得视频缓冲区的首址,然后 直接在缓冲区内填充颜色。当然,也可以利用 VGA BIOS 中断在屏幕上画点,但是这种方法的速 度要比直接写视频缓冲区的速度慢得多。 值得注意的是,由于采用直接写视频缓冲区的做法,也使得该程序的可移植性较差。这是因 为不同的系统,其缓冲区位址也各不相同。 226666 W C 语言实例解析精粹 实例 134 绘制蓝天图案 实例说明 实现随机蓝天图案的绘制。程序运行效果如图 134-1 所示。 图 134-1 实例 134 程序运行效果 实例解析 本例使用了 BIOS 中断 INT 10H 中的 10H 号子功能调用来填充屏幕,得以生成美丽的图案。 INT 10H 的 10H 号 INT 10H 的 10H 号功能调用是设置调色板寄存器的,多用于 VGA/EGA 系统中。调用 INT 10H 中断服务器时,AL 寄存器中的子功能号决定操作的结果。 00H 设定一个调色板寄存器,BH 中存放要设置的值,BL 中存放目的寄存器的代号。 01H 设置“过扫描”(Overscan)寄存器,BH 中存放要设置的值。 02H 设置所有的调色板寄存器和过扫描寄存器。“ES:DX”指向一个 17 字节的表,其中前 16 字节为调色板值,最后一个字节存放过扫描寄存器的值。 03H 切换“聚集/闪烁”位,BL 为 0 时,激活聚焦功能,为 1 时激活闪烁功能。 07H 读取指定调色板寄存器的值。 08H 读取过扫描寄存器的值。 09H 读取所有的调色板寄存器和过扫描寄存器的值。 10H 设定指定的颜色寄存器。 12H 设定一组颜色寄存器。 13H 选择颜色页。 15H 读取指定的颜色寄存器的值。 17H 读取一组颜色寄存器的值。 1AH 读取颜色页的状态。 程序代码 【程序 134】 美丽的随机图案 //本实例源码参见光盘 第四部分 图形篇 X 226677 归纳注释 在程序执行过程中,首先用 set_mode()函数指定屏幕分辨率为 320×280 像素,然后使用 set_pattern()函数设置调色板,将颜色寄存器设置成需要的值。此处就调用了 INT 10H 的 12H 号子 功能,即设定一组颜色寄存器的值。接下来调用 plot()函数和 Sub_device()函数递归地填充屏幕区 域。最后在程序结束的时候将显示模式调整回文本模式。 实例 135 屏幕检测程序 实例说明 本实例将实现使用多种显示填充模式进行屏幕检测的功能。运行该程序后,将使用各种不同的 填充模式来对屏幕的中心区域进行检测,并且伴随有背景音乐。程序运行效果如图 135-1 所示。 图 135-1 实例 135 程序运行效果 实例解析 在本例中使用了设置中断向量的方法来实现 PC Speaker 的发声。 中断向量是一个地址值,指向某一个特定的系统服务程序的开始地址(也称入口地址)。 在 DOS 环境下,系统内存的最开始的部分保存着系统的中断向量表,每个中断向量占用 4 字节的空间。例如最常用 DOS 功能服务 INT 21H 的入口地址就存放在中断向量表的 0x84 位置上。 本例中使用的中断向量是 0x1C,因为这个中断向量是空闲的,所以对它的修改不会带来错误。 getvect()和 setvect()函数是 Turbo C 提供的对中断向量进行操作的函数。在程序中使用了 “handler=getvect(0x1c)”这样的语句,就是把中断向量表中原有的内容存放在 handler 中。然后使 用 setvect()函数把 music()函数的入口地址写入到中断向量 0x1C 中。在程序结束的时候,还应该 把 handler 重新写回到中断向量 0x1C 中,以便恢复系统的默认设置。 outportb()与 iniportb()函数是对端口进行读写的功能函数,本例中使用了 0x41,0x42 与 0x61 端口。其中 0x40~0x42 端口是系统的计时器与计数器端口,而 0x61 端口则是系统扬声器的端口。 在程序的开始部分声明了一个枚举类型的 NOTE,表示每个音符的频率高低,song 则是一个数组, 记录了一个曲子的音符。用户可以自行设定背景音乐,方法是修改 song 数组的内容。 226688 W C 语言实例解析精粹 程序代码 【程序 135】 屏幕检测 //本实例源码参见光盘 归纳注释 在主函数中使用 setfillstyle()函数设置各种填充模式,在屏幕的正中画出一个边长 100 个像素 的正方形,并进行填充,测试屏幕。 实例 136 运动的小车动画 实例说明 本实例实现运动的小车动画的绘制。程序运行效果如图 136-1 所示。 图 136-1 实例 136 程序运行效果 实例解析 在 Turbo C 中使用 putimage()函数进行动画绘制的功能,基本思路如下。 首先分配一块内存用来存放需要显示在屏幕上的小车的图像。然后用程序更新这块内存区域, 也就是改变小车运动的状态,同时把它显示到屏幕上。显示的时候使用 putimage()函数。 Turbo C 中的 graphics 图形库提供了丰富的画图函数,通过简单的形状可以组合出丰富多彩的 图案来。在本例中用到的函数如下。 rectangle()函数的作用是在屏幕上指定的位置画一个矩形,声明如下: void far rectangle(int left,int top,int right,int bottom); 其中 left,top 是矩形左上角的 x,y 坐标;right,bottom 是矩形右下角的 x,y 坐标。 circle()函数的作用是在屏幕上画一个圆,声明如下: void far circle(int x,int y,int radius); 其中 x,y 是圆心的坐标;radius 是圆的半径。 pieslice()函数的作用是在屏幕上画一个填充的扇形,声明如下: void far piesliece(int x,int y,int start,int end,int radius); 其中 x,y 是圆心坐标;radius 是扇形的半径;start 是扇形起始的角度,end 是扇形终止的 角度。 第四部分 图形篇 X 226699 程序代码 【程序 136】 运动的小车 //本实例源码参见光盘 归纳注释 本实例中的小车就是使用 rectangle()、circle()和 pieslice()几个函数进行组合得到的。通过矩形、 圆、圆弧、椭圆等图形的组合可以得到更多复杂的图形,这也是计算机图形设计的基础。 实例 137 动态显示位图 实例说明 实现动态显示 16 色位图的功能。程序运行效果如图 137-1 所示。 图 137-1 实例 137 程序运行效果 实例解析 程序中首先要求用户选择要显示的 16 色位图文件,注意只能是 16 色的才能产生正确的结果。 读入用户文件名后,主程序调用 bmp_to_dat()函数先在磁盘上搜索到用户输入的文件,然后把位 图读入内存并转存为.dat 文件。这是因为 Turbo C 提供的 putimage()函数不能直接将位图显示在屏 幕上,所以只能先把位图文件转成 putimage()函数能够识别的数据格式。 位图文件的首部包含了位图的信息,例如文件的大小、位图的长度、宽度和颜色的数量等。 首部后面放置的是整个图像每个点的颜色信息,bmp_to_dat()读出其中的颜色信息,把整个位图转 存成.dat 文件。 接下来主程序循环 100 次,在不同的位置上显示出这个图片,给用户一种动态的感觉。 程序代码 【程序 137】 动态显示位图 //本实例源码参见光盘 227700 W C 语言实例解析精粹 归纳注释 putimage()函数的功能是将一幅位图显示到屏幕上,其原型如下: void far putimage(int left,int top,void far *bitmap,int op); 其中,参数 left,top 是该图片左上角的 x,y 坐标值。Bitmap 是一个指向存放图片的内存指 针,其中前两个字分别存储图片的长和宽,余下的部分存放位图颜色信息。op 是显示的模式,即 图片中的像素与屏幕上已有的像素之间是如何影响的。 实例 138 利用图形页实现动画 实例说明 本实例利用图形页来实现一个较复杂的动画。程序运行效果如图 138-1 所示。 图 138-1 实例 138 程序运行效果 实例解析 计算机图形的动画显示实际上是一系列静止图像在不同位置的重放。大部分动态显示程序模 拟运动的基本方法是相同的,即在屏幕某一显示位置上先擦除一个静止图像,然后在临近的位置 上绘出下一幅图,程序重复地执行擦除和绘制的过程,就产生所需要的动画效果。这种动画方式 对于简单的图形效果是很好的,但对于较为复杂的图形来说,效果就不是很好了,因为复杂图形 的重画时间较长。为了解决这一问题,本例展示了如何使用多页方式显示动画。 在 Turbo C 的图形子系统中,提供了两个重要的页面设置函数,即设置图形输出活动页函数 setactivepage()和设置可见图形页函数 setvisualpage(),其函数声明为: void far setactivepage(int); void far setvisualpage(int); 多个图形页交替显示的过程如下:在所用的两个页面中,当一个可见页面用于显示时,另一 第四部分 图形篇 X 227711 个关闭页同时用于绘图。当新的画面绘成后,就把两页进行互换,原来作为显示用的页面现在关 闭用来绘制新的图形,而原来的绘图页面被激活作为可见页。 一般可把画面显示顺序做如下安排:第 1 页用于显示动画过程的 1,3,5……画面,第 2 页 用于显示 2,4,6……画面,如此交替下去,利用页面转换技术进行动态显示。因为图形的擦除 和重画过程都在后台进行,屏幕上出现的仅仅是整幅画面的瞬间切换,所以动态效果十分平滑。 程序代码 【程序 138】 利用图形页实现动画 //本实例源码参见光盘 归纳注释 图形页实际上是一个虚假页面,是内存中开辟的一块内存缓冲区。活动图像既可以是当前显 示页,也可以是非显示页。当用函数 setactivepage()选定某一页作为活动页后,其后所有的图形输 出都针对该页。有了多个图形页,程序就可以先将图形输出到一个非显示屏幕页上,然后调用 setvisualpage()改变可见页来快速显示、关闭图形页面中的画面。 实例 139 图形时钟 实例说明 本实例实现走动的图形时钟的绘制。程序运行效果如图 139-1 所示。 图 139-1 实例 139 程序运行效果 实例解析 运行该程序后,首先由 DrawClock()子函数画出表盘,再由 gettime(&newtime)检测系统时间, 每秒钟调用 DrawClock()更新一次表盘,用户按任意键退出 main()函数,程序结束。Setfillstyle() 函数设置绘图的填充模式和颜色;fillpoly()函数画一个填充的多边形。 227722 W C 语言实例解析精粹 程序代码 【程序 139】 图形时钟 #include #include #include #define CENTERX 320 /*表盘中心位置*/ #define CENTERY 175 #define CLICK 100 /*喀嗒声频率*/ #define CLICKDELAY 30 /*喀嗒声延时*/ #define HEBEEP 10000 /*高声频率*/ #define LOWBEEP 500 /*低声频率*/ #define BEEPDELAY 200 /*报时声延时*/ /*表盘刻度形状*/ int Mrk_1[8]={-5,-160,5,-160,5,-130,-5,-130, }; int Mrk_2[8]={-5,-160,5,-160,2,-130,-2-130, }; /*时针形状*/ int HourHand[8]={-3,-100,3,-120,4, 10,-4,10}; /*分针形状*/ int MiHand[8]={-3,-120,3,-120,4, 10,-4,10}; /*秒针形状*/ int SecHand[8]={-2,-150,2,-150,3, 10,-3,10}; /*发出喀嗒声*/ void Click() { sound(CLICK); delay(CLICKDELAY); nosound(); } /*高声报时*/ void HighBeep() { sound(HEBEEP); delay(BEEPDELAY); nosound; } /*低声报时*/ void LowBeep() { sound(LOWBEEP); } /*按任意角度画多边形*/ void DrawPoly(int *data,int angle,int color) { 第四部分 图形篇 X 227733 int usedata[8]; float sinang,cosang; int i; sinang=sin((float)angle/180*3.14); cosang=cos((float)angle/180*3.14); for(i=0;i<8;i+=2) { usedata[i] =CENTERX+ cosang*data[i]-sinang*data[i+1]+.5; usedata[i+1]=CENTERY+sinang*data[i]+cosang*data[i+1]+.5; } setfillstyle(SOLID_FILL,color); fillpoly(4,usedata); } /*画表盘*/ void DrawClock(struct time *cutime) { int ang; float hourrate,minrate,secrate; setbkcolor(BLACK); cleardevice(); setcolor(WHITE); /* 画刻度*/ for(ang=0;ang<360;ang+=90) { DrawPoly(Mrk_1,ang,WHITE); DrawPoly(Mrk_2,ang+30,WHITE); DrawPoly(Mrk_2,ang+60,WHITE); } secrate=(float)cutime->ti_sec/60; minrate=((float)cutime->ti_min+secrate)/60; hourrate=(((float)cutime->ti_hour/12)+minrate)/12; ang=hourrate*360; DrawPoly(HourHand,ang,YELLOW);/*画时针*/ ang=minrate*360; DrawPoly(MiHand,ang, GREEN);/*画分针*/ ang=secrate*360; DrawPoly(SecHand,ang, RED);/*画秒针*/ } main() { int gdriver=EGA, gmode=EGAHI; int curpage; struct time curtime ,newtime ; initgraph(&gdriver,&gmode,"c:\\tc"); setbkcolor(BLUE); cleardevice(); gettime(&curtime); curpage=0; DrawClock(&curtime); while(1) 227744 W C 语言实例解析精粹 { if(kbhit()) break; /*按任意键退出*/ gettime(&newtime); /*检测系统时间*/ if(newtime.ti_sec!=curtime.ti_sec)/*每 1 秒更新一次时间*/ { if(curpage==0) curpage=1; else curpage=0; curtime=newtime; /*设置绘图页*/ setactivepage(curpage); /*在图页上画表盘*/ DrawClock(&curtime); /*设置绘图页为当前可见页*/ setvisualpage(curpage); /*0 分 0 秒高声报时*/ if(newtime.ti_min==0&&newtime.ti_sec==0) HighBeep(); /* 59 分 55 至 60 秒时低声报时*/ else if(newtime.ti_min==59&& newtime.ti_sec<=59) LowBeep();/*其他时间只发出喀嗒声*/ else Click(); } } closegraph(); } 归纳注释 在实现发声功能时调用了系统函数 sound()。为了减少程序规模,定义了函数 DrawPoly()来画 表针,DrawPoly 调用 TC 的绘图函数 setfillstyle(),fillpoly()来实现。 实例 140 音乐动画 实例说明 本实例实现音乐动画的绘制。程序运行效果如图 140-1 所示。 图 140-1 实例 140 程序运行效果 第四部分 图形篇 X 227755 实例解析 (1)动画部分的实现。动画部分主要使用了 Turbo C 实现的多页面功能。Turbo C 在 VGA 模 式下可以设置多个页面。利用页面间的切换,可以体现出动画的效果。主要使用的函数有 setactivepage(),setvisualpage(),cleardevice()。Setactivepage()的功能是设置当前活动的页面,当 设置以后,可以对活动页面进行各种属性的设置以及绘图,默认的活动页面是 0 号页面。本例中 在两个页面上分别绘制出两对位置、大小不同的扇形和三维立方体。 (2)音乐部分的实现。本例使用设置中断向量的方法来实现 PC Speaker 的发声。 程序代码 【程序 140】 音乐动画 //本实例源码参见光盘 归纳注释 setvisualpage()函数的功能是将某一个页面设置成最前,也就是可视的页面。使用 setvisualpage() 函数在 0 号与 1 号显示页面之间切换,就出现了动画的效果。Cleardevice()函数的功能是在图形模 式下清除屏幕,相对应地在文本模式下可以用 system(“cls”)来清除屏幕。 第第五五部部分分 系系 统统 篇篇 据结构篇 精彩导读 修改环境变量 读取 CMOS 信息 获取 BIOS 设备列表 锁住硬盘 备份/恢复硬盘分区表 设计口令程序 227788 W C 语言实例解析精粹 实例 141 读取 DOS 系统中的国家信息 实例说明 本实例通过系统调用获取 DOS 系统中的国家信息并将其显示。程序运行效果如图 141-1 所示。 图 141-1 实例 141 程序运行效果 实例解析 country 函数与 country 结构 country 函数用于获取国家信息,其函数原型(在 dos.h 中定义)如下: country *country (int code, struct country *info); 调用成功,则返回一个指向 country 型结构的指针。参数中,code 值是指定了要选择的国家 代码。如果 code 参数的值为−1,country 函数将把当前国家代码设置成用户指定的代码。如果 code 参数的值不是−1,country 函数将当前国家代码设置存入缓冲区。 country 结构是在 dos.h 中定义的,如下所示: struct country { int co_date; /*日期格式*/ char co_cuur[5]; /*流通货币符号*/ char co_thsep[2]; /*千位分隔符*/ char co_desep[2]; /*小数分隔符*/ char co_dtsep[2]; /*数据分隔符*/ char co_tmsep[2]; /*时间分隔符*/ char co_currstyle; /*流通货币方式*/ char co_digits; /*有效数字*/ char co_time; /*时间*/ char co_case; /*大小写*/ char co_dasep; /*数据分隔符*/ char co_fill[10]; /*补白*/ } 第五部分 系统篇 X 227799 程序代码 【程序 141】 获取国家信息 //本实例源码参见光盘 实例 142 修改环境变量 实例说明 本实例通过系统调用实现对环境变量的读取和修改。程序运行效果如图 142-1 所示。 注意:运行本程序时,将对系统环境变量进行修改。 图 142-1 实例 142 程序运行效果 实例解析 环境变量的获取和修改 环境变量是包含诸如驱动器、路径或文件名之类的字符串。环境变量控制着多种程序的行为。 例如,TEMP 环境变量指定程序放置临时文件的位置。最常用的系统变量还有 PATH 环境变量, PATH 指定了系统搜索的路径。例如用户输入 fdisk 命令,系统将依次在 PATH 变量指定的各个路 径中寻找fdisk,直到找到第一个名字叫做fdisk的可执行文件为止(可执行文件是指扩展名位EXE, CMD,COM,VBS 等的文件)。 getenv(const char* name)函数的功能是获取名为 name 的环境变量,返回值是一个字符串指 针。putenv(const char* name)函数的功能是设定指定名为 name 的环境变量。由于系统不允许在 程序中直接更改环境变量,所以对环境变量的更改只能依赖于 putenv()函数。putenv()函数也可以 删除一个环境变量,例如语句 putenv(”myEnv=”)即可删除 myEnv 环境变量。 environ 是 Turbo C 内置 (Build-in)的全局变量,可以在任何的程序中访问这个变量。environ 是一个字符型数组,其含义是系统中所有的环境变量。可以用循环的方式,将其全部输出。 程序代码 【程序 142】 修改环境变量 #include 228800 W C 语言实例解析精粹 #include #include #include #include int main(void) { char *path, *ptr; int i = 0; clrscr(); puts(" This program is to get the Path and change it."); /* 获得当前环境变量中的 path 信息 */ ptr = getenv("PATH"); /* 更新 path */ path=(char *)malloc(strlen(ptr)+15);/*分配内存空间,比 path 的长度多 15 个字节*/ strcpy(path,"PATH="); /*复制 path*/ strcat(path,ptr); strcat(path,";c:\\temp"); /*在当前的 path 后追加一个路径*/ /* 更新路径信息并显示所有的环境变量 */ putenv(path); /*设置环境变量*/ while (environ[i]) /*循环输出所有的环境变量*/ printf(" >> %s\n",environ[i++]); printf(" Press any key to quit..."); getch(); return 0; } 归纳注释 程序先清屏,输出提示信息,接着调用 getenv 函数获取环境变量,在当前环境变量中追加 temp 路径,并更新路径,最后输出所有的环境变量,按任意键结束程序。 实例 143 显示系统文件表 实例说明 本实例通过系统调用实现系统文件表的显示。程序将把系统文件表的详细内容(主要是各个 文件的文件句柄)显示出来。通过本例可以学习系统文件表的结构和 intdos(),intdosx()等函数的 应用。程序运行效果如图 143-1 所示。 图 143-1 实例 143 程序运行效果 第五部分 系统篇 X 228811 实例解析 系统文件表 文件句柄是进入进程文件表的索引值,这些值又指向系统文件表。系统文件表存放着每个文 件的信息,文件可以是 DOS、设备驱动器、驻留内存的程序,或用户程序打开的文件。如表 143-1 所示显示了 DOS 系统文件表的内容。 表 143-1 DOS 系统文件表 00H 指向下一个表的远端指针 16H 日期标记 04H 本表的入口数 18H 文件长度 06H 本入口的句柄 1CH 当前指针偏移量 08H 文件打开模式 20H 相关簇 0AH 文件属性 22H 目录入口扇区 0BH 局部/远端设备 26H 目录入口偏移量 0DH 驱动程序头文件或 DPB 27H 可执行文件名 12H 起始簇 34H 保留 14H 时间标记 实际上 DOS 把系统文件表分成两段。第一段包含 5 个入口,第二段为用户在 config.sys 文件 中 FILES 参数指定的入口数提供足够的空间。 程序首先根据表 143-1 所示的系统文件表内容定义了相对应的结构型 SystemTableEntry: struct SystemTableEntry { struct SystemTableEntry far *next; /*下一个系统文件表入口*/ unsigned file_count; /*表中文件的个数*/ unsigned handle_count; /*对此文件所进行操作的个数*/ unsigned open_mode; /*文件的打开模式*/ char file_attribute; /*文件的属性字节*/ unsigned local_remote; /*本地/远端设备*/ unsigned far *DPD; /*驱动器参数块*/ unsigned starting_cluster; /*起始簇*/ unsigned time_stamp; /*时间戳*/ unsigned date_stamp; /*数据戳*/ long file_size; /*文件大小*/ long current_offset; /*当前偏移*/ unsigned relative_cluster; /*相对簇*/ long directory_sector_number; /*路径扇区数*/ char directory_entry_offset; /*路径入口偏移*/ char filename_ext[11]; /*可执行文件名*/ }; 然后获取本机 DOS 版本号,这是通过 intdos()函数执行 0x21 中断调用 ax=0x3001 函数得到 的。由此版本号,通过调用 int 21H 的 0x52 中断功能获取内部 DOS 列表地址,该地址偏移 4 位 的地址即是系统文件表的地址,由此便可以导出系统文件表的内容。接下来就是遍历此系统文件 表,依次访问文件表中记录的每个文件,如果有对该文件的操作,那么在屏幕上输出相对应的可 执行文件名,然后指向下一个文件,进行同样的操作,直到遍历完整个文件表。 228822 W C 语言实例解析精粹 程序代码 【程序 143】 显示系统文件表 //本实例源码参见光盘 归纳注释 intdos 函数和 intdosx 函数的原型为: int intdos(union REGS *inregs, union REGS *outregs); int intdosx(union REGS *inregs, union REGS *outregs, struct SREGS *segregs); 这两个函数执行 DOS 的 int 21H 中断,调用一个指定的 DOS 函数。Inregs->h.ah 的值指定了 要调用的 DOS 函数。intdosx 在调用前,还要对 segregs->ds 和 segregs->es 进行赋值。 程序中用到的宏 FP_OFF,FP_SEG 和 MK_FP 声明如下: unsigned FP_OFF(void far *fp); unsigned FP_SEG(boid far *p); void far *MK_FP(unsigned seg, unsigned ofs); FP_OFF 作用于远指针*p,得到一个无符号的整数值,即远地址偏移;FP_SEG 作用于远指针 *p,得到一个无符号整数值,即远地址段的值;MK_FP 由段号和偏移生成一个远指针。 实例 144 显示目录内容 实例说明 本实例通过系统调用显示一个目录下所有文件和文件夹,目的是让读者学会使用 opendir()函 数和 readdir()函数打开并读取一个文件目录,利用链表进行排序等内容。程序运行效果如图 144-1 所示(在 Borland C++ 3.1 中编译运行)。 图 144-1 实例 144 程序运行效果 第五部分 系统篇 X 228833 实例解析 程序在声明部分,首先定义了一个DIR型的指针directory_pointer和dirent结构型的指针entry, 把这两个指针作为对象目录的入口指针,然后定义了本程序进行排序的链表结构。在程序中用 opendir 函数打开以命令行参数为名称的文件夹,返回目录指针到 directory_pointer,然后读取该目 录下文件名到 entry 中,按照名称字典将其插入到链表中。最后打印整个链表内容,也就完成了 显示该目录下排序好的所有文件及子目录的名称。 程序代码 【程序 144】 显示目录内容 #include #include #include #include #include void main(int argc, char *argv[]) { DIR *directory_pointer; struct dirent *entry; struct FileList { char filename[64]; struct FileList *next; } start, *node, *previous, *new; if ((directory_pointer = opendir(argv[1]))==NULL) /*取 argv[1]文件夹的指针赋于*/ printf("Error opening %s\n", argv[1]); else { start.next = NULL; /*将 directory_pointer 指向的文件名列表做成一个以 FileList 类型为结点的链*/ while(entry=readdir(directory_pointer)) /*读取 directory_pointer 指向的文件名*/ { // Find the correct location previous = &start; node = start.next; while ((node) && (strcmp(entry, node->filename) > 0)) /* 以字典顺序搜索在链表中此文件名应该插入的位置*/ { node = node->next; previous = previous->next; } new = (struct FileList *) 228844 W C 语言实例解析精粹 malloc(sizeof(struct FileList)); if (new == NULL) /*内存分配失败*/ { printf("Insufficient memory to store list\n"); exit(1); } /*完成插入*/ new->next = node; previous->next = new; strcpy(new->filename, entry); } closedir(directory_pointer); node = start.next; /*输出整个链表结点的文件名*/ while (node) { printf("%s\n", node->filename); node = node->next; } } printf(" Press any key to quit..."); getch(); return; } 归纳注释 程序主要是通过链表完成查询和删除、排序等操作。链表是 C 语言中常用的一种数据结 构,通常有单向、双向、单向循环、双向循环链表等类型。相对于数组,它更容易完成数据 的插入和删除,也就更容易完成数据的排序和查找,并且会更经济地使用内存空间,不会造 成空间闲置。 本例定义的文件链表类型为: struct FileList { char filename[64]; struct FileList *next;}。 为了完成文件名按字典排序输出,应该在创建链表时把所选路径中的所有文件名有序地插入 到链表中。 实例 145 读取磁盘文件 实例说明 本实例通过系统调用递归读取磁盘文件,并滚屏显示整个磁盘中的所有文件名,以便使读者 掌握使用磁盘文件目录操作库 dirent.h 中相关函数,进行磁盘文件目录的访问和操作。程序运行 效果如图 145-1 所示(在 Borland C++ 3.1 中编译运行)。 第五部分 系统篇 X 228855 图 145-1 实例 145 程序运行效果 实例解析 dirent.h 是 Turbo C 中主要针对磁盘文件进行操作的库函数头文件,包括 opendir、closedir、 readdir 等常用的目录读取或操作函数,这些函数说明如下。 DIR *opendir(char *dirname); opendir 函数用于打开一个目录串以便读取。当要从一个目录串读取连续的目录入口时,使用 readdir 函数。 struct dirent readdir(DIR *dirp); 该函数从目录串*dirp 中读取当前目录入口,然后将入口指针移向目录串下一个目录入口。它 返回一个指向 dirent 结构的指针。 void closedir(DIR *dirp); 该函数用于关闭一个目录链表。 此外,本例程序还用到了基本输入输出库 io.h 中的函数_chmod,其具体声明如下: int _chmod(const char *path, int func [, int attrib]); 此函数获得或设置*path 所指向文件的 DOS 文件属性,当 func=0 时,该函数返回当前 DOS 属性;当 func=1 时,函数设置 attrib 的属性。属性 FA_DIREC 表明指向的是一个文件夹。 程序代码 【程序 145】 读取磁盘文件 //本实例源码参见光盘 归纳注释 程序首先由 opendir 函数得到当前目录的一个目录串指针,然后沿此指针向下搜索,判断此 指针中的目录入口文件的属性,如果入口文件为文件夹,就递归调用主函数继续搜索;如果入口 文件是文件,则打印文件名。 228866 W C 语言实例解析精粹 实例 146 删除目录树 实例说明 本实例使用系统调用来删除目录树,即将用户指定的整个目录连同其文件和子目录全部删除。 程序运行效果如图 146-1 所示(在 Borland C++ 3.1 中编译运行)。 图 146-1 实例 146 程序运行效果 实例解析 在 MS-DOS 6.0 及以上版本中,Microsoft 引进了 Deltree 命令,允许用户直接删除目录和它的 文件以及该目录的子目录。本实例实现的正是 Deltree 命令的功能。 程序首先分析输入的目录,用 fnsplit()函数进行分解。函数 fnsplit()的定义在 dir.h 头文件中, 具体声明为: int fnsplit (const char *path, char *drive, char *dir, char* name, char *ext); 它将一个文件的全路径名作为一个字符串进行分解,分为 4 个部分:盘符、目录、文件名和 扩展名。 另外,程序中还用 getcwd 函数: char *getcwd(char *buf, int buflen); 以获得当前工作目录的全路径名,并把它存放在 buffer 中。用 intdosx 指向 DOS int 21H 的中 断功能。 第五部分 系统篇 X 228877 程序代码 【程序 146】 删除目录树 //本实例源码参见光盘 归纳注释 程序在判断该目录是否存在以及是否为当前工作目录后,再进入目录中进行递归删除,直到 这个目录的所有文件和子目录都被删除,然后退到上一层目录,删除此目录。由此完成对整个目 录树的删除。 实例 147 定义文本模式 实例说明 本实例通过系统调用自定义文本模式,通过本实例读者可以掌握有关 BIOS INT 10H 的功能 和 window(),geninterrupt(),textmode()等函数的使用方法。程序运行效果如图 147-1 所示。 图 147-1 实例 147 程序运行效果 实例解析 BIOS 中断 程序主要使用了 BIOS 中提供的 INT 10H 中断的各种功能。BIOS 的 INT 10H 中断主要提供 了视频操作的各种功能,是系统提供的一组功能强大的函数。这些函数可以用以下语句来调用: _AH=0xXX; geninterrupt(VIDEO_BIOS); 其中,_AH 表示了调用的 INT 10H 中断的具体功能函数号 XX,geninterrupt()函数执行一个 228888 W C 语言实例解析精粹 软中断,在上面的语句中,VIDEO_BIOS 表示 10H 中断。 在本例中,主要使用其中的文本模式功能。 0FH——获得当前的视频模式,返回值 AH 中存放每行的字符数,AL 中存放视频模式。 11H——为 VGA、EGA 模式生成模式集合,并重置视频环境,生成可显示的点阵字符。 12H——为 VGA、EGA 模式选择显示器程序,执行后返回值。 BH:0——彩色模式,1——单色模式。 BL:0——64K,1——128K,2——192K,3——256K。 CH:适配器位。 CL:开关设置。 1AH——获取当前正在使用的显示模式鉴别码。 另外,函数 setfont8x8()是将显示模式设置成 8×8 像素的点阵格式,其参数位显示模式,即每 行可以显示的字符数。 程序代码 【程序 147】 定义文本模式 #include #include #include #define VIDEO_BIOS 0x10 /*int 10h 是 BIOS 中对视频函数的调用*/ int setfont8x8(int); /*设置不同的显示模式*/ void setstdfont(int); /*恢复成系统默认的显示模式*/ void main(void) { int lines,i; lines = setfont8x8(C80); /*设置 8×8 点阵,每行 80 字符的显示模式,并获取可显示的最大行数*/ textattr(WHITE); /*textattr()函数设置字符模式下窗口的前景色和背景色*/ clrscr(); /*清除屏幕*/ if (lines < 43) { textattr(LIGHTRED); cprintf("\n\r Drivers of EGA or VGA not found...\n\r"); /*cprintf()的功能是向窗口输出文本*/ exit(1); } window(20,15,70,35); /*画字符模式窗口,4 个参数依次为左,上,右,下的位置*/ textattr((RED<<4)+WHITE); /*把窗口设置成前景色为白色,背景色为红色*/ clrscr(); for (i=1;i<=lines;i++) { /*循环输出最多能输出的行数*/ cprintf("\n\r No. %d ",i); delay(200); /*每输出一行,等待 200ms*/ } getch(); /*等待用户输入一个字符*/ window(1,1,80,lines); /*重新设置窗口*/ textattr(LIGHTGRAY<<4); /*将窗口背景色设置为灰色*/ clrscr(); cprintf("\n\r Full screen 80x%d display mode.\n\r",lines); getch(); lines = setfont8x8(C40); /*将窗口设置为每行 40 个字符的显示模式*/ textattr((BLUE<<4)+LIGHTGREEN); /*设置窗口,前景为绿色,背景为蓝色*/ 第五部分 系统篇 X 228899 clrscr(); cprintf("\n\r Can be also set as 40x%d mode.\n\r",lines); getch(); setstdfont(C80); /*重新设置成标准的每行 80 字符的显示模式*/ clrscr(); cprintf("\n\r Back to normal mode...\n\r"); printf(" Press any key to quit..."); getch(); exit(0); } int setfont8x8(mode) int mode; { int maxlines,maxcol; char vtype,displaytype; textmode(mode); /*设置文本格式,mode 含义为每行可以显示的字符数*/ _AH = 0x0F;/*int 10h 的 0fh 功能为获取当前的显示模式,执行后每行可显示字符数保存在 ah 中*/ geninterrupt(VIDEO_BIOS); /*geninterrupt()函数执行一个软中断,调用 int 10h*/ maxcol = _AH; /*获取每行可以显示的字符数*/ _AX = 0x1A00; /*int 10h 的 1ah 功能为获取当前的显示代码*/ geninterrupt(VIDEO_BIOS); displaytype = _AL; /*int 10h 返回后,al 中为显示类型*/ vtype = _BL; /*bl 中为显示器的类型*/ if (displaytype == 0x1A) { /*可以直接获取最大行数*/ switch (vtype) { case 4: case 5: maxlines = 43; break; case 7: case 8: case 11: case 12: maxlines = 50; break; default: maxlines = 25; break; } } else { /*无法读取显示器的类型 */ _AH = 0x12; /*int 10h 的 12h 功能为选择显示器程序*/ _BL = 0x10; geninterrupt(VIDEO_BIOS); if (_BL == 0x10) maxlines = 25; else maxlines = 43; } if (maxlines > 25) { /*如果可以设置更多的行*/ _AX = 0x1112; /*以下的部分都是 int 10h 的 11h 号功能调用,作用是生成相应的显示字符*/ _BL = 0; geninterrupt(VIDEO_BIOS); _AX = 0x1103; _BL = 0; geninterrupt(VIDEO_BIOS); } 229900 W C 语言实例解析精粹 *((char *) &directvideo - 8) = maxlines; /*设置显示行数*/ window(1,1,maxcol,maxlines); /*画出相应大小的窗口*/ return(maxlines); /*返回可以设置的最大行数*/ } void setstdfont(mode) int mode; { if (mode != LASTMODE) _AL = mode; else { _AH = 0x0F; /*获取当前显示模式*/ geninterrupt(VIDEO_BIOS); mode = _AL; } _AH = 0; /*恢复成系统标准模式*/ geninterrupt(VIDEO_BIOS); *((char *) &directvideo - 8) = 25; /*行数设置成 25 行*/ textmode(mode); } 归纳注释 程序先把显示模式设置为每行 80 个字符,并将屏幕设置成白色,然后清屏,画一个窗口,并 将其前景色设为白色,背景色设为红色,在窗口中输出能够显示的最大行数,按任意键后,把显 示模式改为每行 80 个字符,刷新屏幕,再改成每行 40 个字符的显示模式,最后还原成系统最初 的标准设置。 实例 148 设计立体窗口 实例说明 本实例通过系统调用设计立体投影窗口,即在不同的位置上显示 3 个不同颜色的立体投影窗 口,以便读者掌握 window()函数的使用方法。程序运行效果如图 148-1 所示。 图 148-1 实例 148 程序运行效果 第五部分 系统篇 X 229911 实例解析 立体投影窗口 立体投影窗口的原理是在设定的窗口区域内以投影色(一般为黑色)进行涂色,然后在原窗 口位置上错位后,再用一种不同的颜色画出一个窗口,将这个新画的窗口叠加在原投影窗口上, 就形成了立体投影窗口的效果。 Turbo C 提供了窗口函数 window(int left, int top, int right, int bottom),允许在屏幕范围内画出 任意大小的矩形窗口。其中 left、top 是窗口左上角的坐标,right、bottom 是窗口右下角的坐标。 该函数在屏幕上定义一个文本窗口,并且使屏幕上这个区域成为激活窗口。 函数 cprintf(),cputs(),clscr()是窗口 I/O 函数,它们自动激活定义的窗口,并在该窗口中完 成输出、清屏等操作。但是因为 Turbo C 默认显示方式是直接传送字符串到屏幕显示缓冲区,因 此必须设置全局变量 drectvideo=0,以通过 BIOS 调用使用这些窗口 I/O 函数。 程序代码 【程序 148】 设计立体窗口 #include void window_3d( int, int, int, int, int, int ); int main(void) { directvideo = 0; textmode(3); textbackground( WHITE ); textcolor( BLACK ); clrscr(); window_3d( 10,4,50,12, BLUE, WHITE ); gotoxy( 17,6); cputs("The first window"); window_3d(20,10,60,18,RED, WHITE ); gotoxy(17,6); cputs("The second window"); window_3d(30,16,70,24,GREEN, WHITE ); gotoxy(17,6); cputs("The third window"); getch(); return 0; } void window_3d( int x1, int y1, int x2, int y2, int bk_color, int fo_color) /*立体投影窗口的显示 x1,y1, x2, y2 是窗口的大小 bk_color 是背景色 fo_color 是前景色,即文本的颜色*/ { textbackground(BLACK); window(x1, y1,x2, y2); /*画背景窗口*/ clrscr(); textbackground(bk_color); /*设置背景色*/ 229922 W C 语言实例解析精粹 textcolor(fo_color); /*设置前景色*/ window(x1-2, y1-1, x2-2, y2-1); /*画实际的窗口*/ clrscr(); } 归纳注释 显示 3 个立体投影窗口的操作主要由函数 window_3d(int x1, int y1, int x2, int y2, int bk_color, int fo_color)来完成,其参数 x1,y1,x2,y2 分别是投影窗口的左上角和右下角坐标,bk_color, fo_color 是投影窗口的背景色和前景色(即窗口中显示字符的颜色)。该函数先设置背景色为黑色, 然后根据坐标参数开一个窗口(此时屏幕上无窗口显示),并调用清屏函数 clscr(),使得屏幕上显 示一个黑色窗口,接着设置立体投影窗口的背景色和前景色,再把黑色投影窗口的 x 轴和 y 轴坐 标分别减 2 和减 1,得到的坐标值作为新的窗口坐标打开一个窗口,即将此窗口叠加到黑色窗口 上,再清屏,就得到一个立体投影窗口。 实例 149 彩色弹出菜单 实例说明 本实例通过系统调用实现彩色弹出式菜单的设计。程序运行效果如图 149-1 所示。 图 149-1 实例 149 程序运行效果 实例解析 弹出式菜单设计 弹出式菜单被广泛应用于各类软件或操作系统中,作为显示某些功能的工具。弹出式菜单被 设计成“弹出”并显示的一个窗口,功能选项在窗口中垂直排列,并且第一个选项为高亮反显。 用户可以用快捷键或单击方式进行菜单功能选择。 设计弹出式菜单有以下几个要点。 第五部分 系统篇 X 229933 (1)保存菜单出现前的部分屏幕的函数 save_video。要保存屏幕内容,必须读取并存储屏幕 上各位置的当前值。用 INT 10H 的 08 号子功能从屏幕上某一个位置读一个字符,返回该字符的 ASCII 字符码及其在当前光标处的属性。其调用参数为“AH=0x08,BH=显示页”,返回时“AH= 属性字节,AL=ASCII 字符码”。 (2)显示菜单边框的函数 chineseputs。由于本例开发的彩色汉字弹出式菜单函数基于中文文 本操作系统,所以菜单的边框是中文方式下的双字节汉字制表符。构成菜单边框的制表符有 6 种, 系统把这些制表符作为汉字对待,因而需先构造一个显示汉字字符串的函数 chineseputs(),调用 BIOS INT 10H 中断的 09 号功能显示汉字字符串,该调用的入口参数为 AH=09H,AL=ASCII 字 符码,BH=显示页,BL=属性(字符方式下)或颜色(图形方式下),CX=写字符计数。 (3)显示菜单正文的函数 display_menu,为显示单个字符串,只要把指针像数组一样编程索 引,数组的每一项,均为指向相应菜单项的指针。 (4)等待用户响应的函数 get_resp。 (5)处理用户响应的主函数 main。 (6)恢复屏幕为原始状态的函数 restore_video。 (7)popup 函数,用于弹出所选中的菜单项。 程序代码 【程序 149】 彩色弹出菜单 //本实例源码参见光盘 归纳注释 本实例中,popup 函数是弹出式菜单程序中最主要的函数,它负责调用其他函数,popup 首先 判断菜单起始点坐标是否超出屏幕范围,然后根据菜单正文最长项来决定菜单终止点的坐标。基 于菜单起始点和终止点的坐标,popup 函数计算出保存将要被弹出式菜单覆盖的部分屏幕所需的 内存,然后把屏幕内容保存到该内存区域中。如果边框标志置位,则显示边框;否则不显示边框。 接着显示菜单正文项,并准备接受用户的选择。一旦用户选择了某选项,则 popup 函数恢复屏幕, 并释放保存屏幕所需的那块内存区域,返回用户的选项值给调用 popup 函数的主函数 main()。 实例 150 读取 CMOS 信息 实例说明 本实例通过系统调用来读取 CMOS 的信息。程序运行效果如图 150-1 所示。 图 150-1 实例 150 程序运行效果 229944 W C 语言实例解析精粹 实例解析 CMOS 信息与 CMOS 结构 PC 把系统配置信息存放在 CMOS 存储器中,包括用户驱动器类型、系统日期等。PC 机不使 用标准的段加偏移地址方式访问 CMOS,而是通过 PC 端口地址与 CMOS 联系。 本实例程序通过调用 inportb 和 outportb 函数获取 CMOS 信息,并显示有关内容。程序先定 义了 CMOS 结构(详见程序代码,各项含义可通过其名称得知),接着声明了一个此结构的 CMOS 变量,然后通过 outportb 和 inportb 函数对 CMOS 内容进行以 byte 为单位的访问,依次读取 CMOS 信息,并显示其中的部分内容。 程序代码 【程序 150】 读取 CMOS 信息 //本实例源码参见光盘 归纳注释 程序用到的 inportb 和 outportb 函数在头文件 dos.h 中定义如下: unsigned char inportb(int portid); 实现从硬件端口读取一个字节。 void outportb(int portid, unsigned char value); 实现向硬件端口输出一个字节的 value 值。 在本实例中,70H 和 71H 分别是针对 CMOS 进行写入和读取的端口,以便返回系统的 CMOS 信息,将其赋予 CMOS 结构变量中。 实例 151 获取 BIOS 设备列表 实例说明 本实例通过系统调用来获取 BIOS 设备列表。程序运行效果如图 151-1 所示(在 Borland C++ 3.1 中编译运行)。 图 151-1 实例 151 程序运行效果 第五部分 系统篇 X 229955 实例解析 _bios_equiplist 函数 本实例程序通过调用 bios.h 头文件中定义的_bios_equiplist 函数来获取系统的 BIOS 设备列表 并输出。此函数的原型为: unsigned _bios_equiplist(void); 此函数通过使用 BIOS INT11H 中断来返回一个整型数据,描述连接在系统上外围设备的信 息。此整型数据的具体位(Bit)含义如下。 Bit 15 14——并行打印机数目,00=0,01=1,10=2,11=3。 Bit 13——附加的打印机序列号。 Bit 12——附加的游戏输入输出设备。 Bit 11 10 9——COM 口的个数,000=0,001=1,…,111=7。 Bit 8——系统是否可以 DMA,0——可以,1——不可以。 Bit 7 6——磁盘驱动器数目,00=0,01=1,10=2,11=3。 Bit 5 4——初始化视频格式,00=未使用,01=40×25BW 彩色,10=80×25BW 彩色, 11=80×25BW 黑白。 Bit 3 2——主板 RAM 大小,00=16K,01=32K,10=48K,11=64K。 Bit 1——是否具有浮点运行协处理器。 Bit 0——是否从磁盘启动。 程序中定义了与此相对应的位域型作为列表信息的存储类型(具体声明见程序中 struct Equip 的定义),然后定义了 Equipment 的联合类型,并声明了变量 equip 以存储 BIOS 设备列表。 程序代码 【程序 151】 BIOS 设备列表 //本实例源码参见光盘 归纳注释 程序中使用了“联合”类型(union)来实现对设备列表信息的位访问。在“union Equipment { unsigned list; struct Equip list_bits;}”中 ,list 和 list_bits 存放的地址是相同的,只是前者只能作为 无符号整型变量访问,后者则可以访问其中的某些位,这样方便于操作。 实例 152 锁住硬盘 实例说明 本实例通过系统调用实现对硬盘加软锁。程序运行效果如图 152-1 所示。 注意:因为对硬盘分区表的操作关系到系统的安全问题,比较危险,本实例程序仅作为了解 和学习使用,上机实践时应有专业人员指导。 229966 W C 语言实例解析精粹 图 152-1 实例 152 程序运行效果 实例解析 硬盘主引导扇区 给硬盘加锁的方法很多,最简单的方法就是改变主引导记录或者改变分区表中某些关键字, 以达到不能使用硬盘的目的。硬盘主引导扇区内容如表 152-1 所示。 表 152-1 硬盘主引导扇区内容 000H 主引导记录(240 字节) 1CEH 第二分区表(16 字节) 0F0H 全 0(206 字节) 1DEH 第三分区表(16 字节) 1BEH 第一分区表(16 字节) 1EEH 第四分区表(16 字节) 55H AAH 主引导扇区最后两个字节 55H 和 AAH 是硬盘自举记录的有效标志。每个分区表为 16 个字节, 具体内容见表 152-2。其中,Boot ind 是自举标志字节,其值为 80H 时,表示可自举分区,其值为 00H 时,表示不可自举分区。SYS ind 是 DOS 系统标志字节,其值为 01H 时,表示分区是 DOS 分区,为 00H 时,表示未知。H.S.CYL 表示分区的起始地址:H 是磁头号,S 是扇区号,CYL 是柱面号的低 8 位,高 2 位在 S 字节的高 2 位。REL seet 表示该分区的相对扇区号,#of sects 表示该分区使用的扇区 数。例如,某硬盘的一个分区表为 80 00 02 00 01 03 51 30 01 00 00 00 03 51 00 00。第一字节 80H 是自 举分区标志,第五字节 01H 是 DOS 分区标志,若改变 01H 为 00H,可达到加密硬盘的目的。 表 152-2 分区表字节内容 Boot ind H S CYL SYS ind H S CYL REL seet # of sects 程序仅仅修改主引导扇区最后两个字节 55H 和 AAH,修改后,如果用硬盘启动,则系统提 示:Disk Boot Failure,Insert System Disk AND Press Enter。 程序代码 【程序 152】 锁住硬盘 #include #include #include int main(void) { int result; char a='N'; char buffer[512]; clrscr(); 第五部分 系统篇 X 229977 printf(" This is a hard disk lock program.\n"); printf(" Do you want to lock your hard disk? (Y/N): "); scanf("%c",&a); if (a == 'Y'||a=='y') { result = biosdisk(2,0x80,0,0,1,1,buffer); if(result) { buffer[510] = 0x0; buffer[511] = 0x0; printf(" Fail to read main boot sector!\n"); } if(!result) result = biosdisk(3,0x80,0,0,1,1,buffer); (!result)?(printf(" Writing main boot sector successfully!\n")):(printf(" Fail to write main boot sector!\n")); } printf(" Press any key to quit..."); getch(); return 0; } 归纳注释 程序中用到的主要函数是 biosdisk 函数,其定义为“char biosdisk(int cmd, int drive, int head, int track, int sector, int nsects, void *buffer)”。该函数使用中断 13H,把磁盘操作直接转给 BIOS。cmd 参 数指示待执行的操作,cmd=2 时是读盘操作,cmd=3 时是写盘操作。drive 为 0 时代表第一个软驱, 为 1 时表示第二个软驱,为 0x80 则表示第一个硬盘驱动器。函数返回值为 00 时,表示操作成功。 实例 153 备份/恢复硬盘分区表 实例说明 本实例通过系统调用实现对硬盘分区表的备份和恢复。程序运行效果如图 153-1 所示(在 Borland C++ 3.1 中编译运行)。 注意:由于程序是对硬盘的分区表进行操作,对系统来说比较危险,容易产生丢失文件甚至 系统崩溃的情况,所以必须谨慎再谨慎。 图 153-1 实例 153 程序运行效果 229988 W C 语言实例解析精粹 实例解析 为了实现对硬盘分区表的保存与恢复,本例采用了 biosdisk()函数对硬盘的分区表进行了读取 和写入。一般来说,硬盘的分区表都保存在硬盘 0 柱面 0 磁道 0 扇区 0 簇,biosdisk 函数通过 INT 13H 中断直接向 BIOS 发出磁盘操作命令,从指定的该盘区位置读取 512 字节数据并保存在 buffer 中。然后使用文件操作函数 fopen,fwrite 等进行保存。恢复分区表则相反,调用 biosdisk 函数将 以前保存过的分区表文件的内容恢复到分区表即可。 程序代码 【程序 153】 备份/恢复硬盘分区表 //本实例源码参见光盘 归纳注释 本实例程序中用到的文件读写操作函数 fread 和 fwrite 定义如下: fread (void *buffer, int size, int count, FILE *fp); fwrite (const void *buffer, int size, int count, FILE *fp); 其中,buffer 是一个指针,用于指向读/写数据的地址,size 是要读写的字节数,count 是要进 行读写 size 字节的数据项,fp 为指向文件的文件指针。 实例 154 设计口令程序 实例说明 本实例通过系统调用实现口令程序设计。程序运行效果如图 154-1 所示。 图 154-1 实例 154 程序运行效果 第五部分 系统篇 X 229999 实例解析 在许多程序中要求输入用户名和密码,本实例程序采用输入并禁止回显的系统控制台,实现 了一个简单的密码输入程序。程序采用 Turbo C 提供的 getpass()函数,实现从控制台读入口令, 并禁止回显,最多可以支持 8 个字符的输入。 程序中用到的主要函数如下。 (1)char *getpass(char *prompt),该函数从键盘读入一个口令并禁止回显,返回一个字符指针 指向获得的字符串,最多为 8 个字符(不包括作为该字符串结束标志的空终结符)。 (2)void getdate(struct date *datep)和 void gettime(struct time *timep),这两个函数包含在 dos.h 头文件中,分别用来获得系统当前的日期和时间。其指针型参数 datep 和 timep 指向取到的结果。 调用这两个函数后,返回值分别使用 datep.da_year,datep.da_mon,datep.da_day 以及 timep.ti_hour, timep.ti_min,timep.ti_sec 来表示具体的日期和时间。 程序代码 【程序 154】 设计口令程序 #include #include #include void main(void) { struct date today; struct time now; getdate(&today); /*把系统当前日期存入 today 所指向的 date 结构中*/ gettime(&now); /*把系统当前时间存入 now 所指向的 time 结构中*/ /*设定字符颜色和背景色*/ textcolor(LIGHTGREEN); textbackground(MAGENTA); /*当输入口令不对时,反复进行以下循环*/ do{ clrscr(); /*调用清屏函数*/ gotoxy(25,10); printf("Today's date is %d-%d-%d\n",today.da_year,today.da_mon,today.da_day); gotoxy(25,12); printf("Current time is %02d:%02d:%02d\n",now.ti_hour,now.ti_min,now.ti_sec); gotoxy(1,1); printf(" ====== Welcome to this password program! ======\n Please input "); } while (atoi((char *) getpass("password:")) != today.da_mon+now.ti_hour); /*如果输入正确,则显示正确信息并退出*/ textcolor(WHITE); textbackground(BLACK); clrscr(); gotoxy(1,1); printf(" Password Correct!!\n"); printf(" Press any key to quit...\n"); getchar(); 330000 W C 语言实例解析精粹 } 归纳注释 程序先在屏幕上显示当前日期和时间,并要求用户输入 password(本例设定为屏幕上显示的 月和小时之和),如果输入不对,则不停地循环。当然,也可以设计成输入 3 次密码不对,即退出 程序,这是程序设计中经常用到,以便防止非授权人恶意使用程序。 实例 155 程序自我保护 实例说明 本实例通过系统调用来实现程序的自我保护——“程序自杀”。程序运行效果如图 155-1 所示 (在 Borland C++ 3.1 中编译运行)。 图 155-1 实例 155 程序运行效果 实例解析 在编制软件时,一般使用的安全检测保护技术是在系统开始运行前,对用户加以口令校对或 使用期限检测,在发现非法使用时,系统立即终止运行,以此来限制软件的使用权限。但这一方 法也有缺陷,就是非法使用者可以在系统退出之后,用其他软件工具对系统运行期间留在内存中 的数据进行跟踪分析并修改,然后得到跳过这一安全检测保护的方法,从而可以直接进入系统。 为了增加非法使用者的破解难度,可以使系统在一旦检测到非法使用时,立即把自身的内存 数据毁灭,这样就能大大加强软件的自我保护能力。 程序代码 【程序 155】 程序自我保护 #include #include #include #include 第五部分 系统篇 X 330011 #include void main( int argc, char* argv[] ) { struct time now; FILE* fp; int errno; gettime( &now ); if( now.ti_min > 30 ) /*如果非法使用系统则删除程序*/ { errno = chmod( argv[0], S_IWRITE ); if((errno)&& ( fp != NULL )) { fclose( fp ); /*将文件长度截为 0*/ unlink( argv[0] ); exit(0); /*删除本文件退出*/ } else { /*如果不能删除,则打印错误退出*/ printf( "\n You are an illegal user to run this program!\n System run error!\n" ); exit(1); } } printf(" You are a legitimate user to run this program!\n"); getch(); return; } 归纳注释 程序实现了简单的自我保护技术——“程序自我删除”。该程序规定必须在机器时间处在每个 小时的前半小时才能启动系统,否则该使用为非法。一旦检测到非法使用时,它立即将该程序删 除。在删除前,该程序首先将它本身的执行程序的长度截止为 0,这样即使非法使用者恢复了被 删掉的程序,得到的也只是一个长度为 0 的空文件,毫无用处。 第第六六部部分分 常常见见试试题题 解解答答篇篇 精彩导读 水果拼盘问题 计算方差 字符串倒置 部分排序 求解三角方程 统计选票 330044 W C 语言实例解析精粹 实例 156 水果拼盘 实例说明 现有苹果、桔子、香蕉、菠萝、梨 5 种水果用来做水果拼盘,每个水果拼盘一定有 3 个水果, 且这 3 个水果的种类不同,问可以制作出多少种水果拼盘?本实例使用 C 语言中的“枚举类型” 来解决水果拼盘问题。程序运行后显示出可能的拼盘类型,效果如图 156-1 所示。 图 156-1 实例 156 程序运行效果 实例解析 使用枚举类型定义变量 x、y、z,其中,x、y、z 是 5 种水果中的任一种,设定由水果 x、y、 z 制作一种水果拼盘,并满足 x≠y≠z。使用三重循环语句来组合它们,使用穷举法来计算总共有多 少种水果拼盘可以制作。 程序代码 【程序 156】 水果拼盘 #include void main() { enum fruit {apple, orange, banana, pineapple, pear};/* 定义枚举结构*/ enum fruit x,y,z,pri;/*定义枚举变量*/ int n,loop; n=0; for(x=apple;x<=pear;x++) for(y=apple;y<=pear;y++) if(x!=y) 第六部分 常见试题解答篇 X 330055 { for(z=apple;z<=pear;z++) if((z!=x)&&(z!=y)) { n=n+1; printf("\n %-4d",n); for(loop=1;loop<=3;loop++) { switch(loop) { case 1:pri=x;break; case 2:pri=y;break; case 3:pri=z;break; default: break; } switch(pri) { case apple: printf(" %-9s","apple");break; case orange: printf(" %-9s","orange");break; case banana: printf(" %-9s","banana");break; case pineapple: printf(" %-9s", "pineapple");break; case pear: printf(" %-9s","pear");break; default: break; } } } } printf("\n\n There are %d kinds of fruit plates.\n",n); puts(" Press any key to quit..."); getch(); return; } 实例 157 小孩吃梨 实例说明 小孩买了一些梨,当即吃了一半,还不过瘾,又多吃了一个;第二天早上又将剩下的梨吃掉 一半,又多吃了一个。以后每天早上都吃了前一天剩下的一半,并又多吃一个。到第 18 天只剩下 一个梨了,问小孩共买了多少个梨?本实例使用“倒推法”来解决小孩吃梨问题。程序运行效果 如图 157-1 所示。 图 157-1 实例 157 程序运行效果 330066 W C 语言实例解析精粹 实例解析 解本题用到一种重要的计算方法,即“倒推法”。知道最后一天的梨数,可以一天一天倒推到 第一天的梨的个数。假设第 n 天的梨个数为 Xn,则前一天的梨个数为 Xn−1,那么,Xn=Xn-1− ((Xn−1)/2+1)=(Xn−1)/2−1。因此,迭代公式 Xn−1=2(Xn+1),初始条件 X18=1。据此,可以一步一 步倒推到第一天的梨的个数。 程序代码 【程序 157】 小孩吃梨 #include /*小孩吃梨问题*/ void main() { long x=1,y=18; /* 定义两个变量,并赋值*/ clrscr(); puts(" This program is to solve"); puts(" The Problem of Child's Eating Pears.\n"); while(y>0) /*y>0 是循环控制条件*/ { x=2*(x+1); y--; } printf(" In the first day, the child bought %ld pears.\n",x); puts("\n Press any key to quit..."); getch(); return;} 实例 158 删除字符串中的特定字符 实例说明 编写函数 func 实现从字符串中删除指定的字符。同一字母的大、小写按不同字符处理。 例如原字符串为“turbo c and borland c++”,从键盘上输入字符“n”,则输出后变为“turbo c ad borlad c++”。如果输入的字符在字符串中不存在, 则字符串照原样输出。程序运行效果如图 158-1 所示。 图 158-1 实例 158 程序运行效果 程序代码 【程序 158】 删除字符串中的特定字符 #include int func(char s[],int c) { char *q=s; 第六部分 常见试题解答篇 X 330077 for(; *q; q++) if(*q != c) *(s++)=*q; *s='\0'; } void main() { static char str[]="Turbo c and borland c++"; char ch; clrscr() ; printf(" The string before delete is %s.\n",str); printf(" Please input the char to delete : "); scanf("%c",&ch); func(str,ch); printf(" The string after delete is %s.\n",str); printf(" Press any key to quit..."); getch(); return; } 实例 159 求解符号方程 实例说明 要求根据输入的字符方程求出未知字符,即“97ab=8ab×cd+1”,其中 a、b 代表 0~9 的数字, cd 为两位数,8×cd 的结果为两位数,9×cd 的结果为 3 位 数,求 a、b、cd 代表的数,及 8ab×cd 的结果。程序可以 利用 i 来表示 cd(10≤i<100),再根据 8×cd 为两位数和 9×cd 为 3 位数来进行限定,求出 cd 的值。程序运行效果 如图 159-1 所示。 程序代码 【程序 159】 求解符号方程 #include void main() { long int a,b,c,i,j,k; clrscr(); for(i=10;i<100;i++) { for(j=0;j<=9;j++) for(k=0;k<=9;k++) { a=8*100+j*10+k; b=97*100+j*10+k; if((b==i*a+1)&&b>=1000&&b<=10000&&8*i<100&&9*i>=100) printf("\n %ld=%ld*%ld+%ld",97*100+j*10+k,800+j*10+k,i,b%i); } } printf("\n Press any key to quit..."); getch(); 图 159-1 实例 159 程序运行效果 330088 W C 语言实例解析精粹 return; } 实例 160 计算方差 实例说明 本实例要求编写函数计算并输出给定 10 个数的方差 ∑ = −= 10 1 2 0 )(10 1 k k XXs ,其中 ∑ = = 10 1 0 10 1 k kXX 为 10 个数的平均值。例如,给定的 10 个数为 95.0、89.0、76.0、65.0、88.0、72.0、 85.0、81.0、90.0、56.0,则输出为 s=11.730729。程序运行效果如图 160-1 所示。 图 160-1 实例 160 程序运行效果 程序代码 【程序 160】 计算方差 #include #include double fun(double x[10]) { int i; double fc,avg=0.0,sum=0.0,abs=0.0; for (i=0;i<10;i++) sum+=x[i]; avg=sum/10; for (i=0;i<10;i++) abs+=(x[i]-avg)*(x[i]-avg); fc=sqrt(abs/10) ; return fc; } void main() { double s, x[10]={95.0,89.0,76.0,65.0,88.0,72.0,85.0,81.0,90.0,56.0}; int i; clrscr(); printf(" The original data is:\n"); for(i=0;i<10;i++) printf(" %3.1f",x[i]); printf("\n The variance of the data is %lf.\n",fun(x)); 第六部分 常见试题解答篇 X 330099 printf(" Press any key to quit..."); getch(); return; } 实例 161 求取符合特定要求的素数 实例说明 编写函数 num(int m,int k,int xx[])将大于整数 m 且紧靠 m 的 k 个素数存入数组中,最后调用 函数 readwriteDAT 从 IN161.DAT 文件中读取 10 对 m、k,分别求取符合要求的素数,并把结果输 出到文件 OUT161.DAT 中。例如若输入 17,5,则应输出 19,23,29,31,37。程序运行效果如 图 161-1 所示。 图 161-1 实例 161 程序运行效果 程序代码 【程序 161】 求取符合特定要求的素数 #include #include int isP(int m) { int i ; for(i = 2 ; i < m ; i++) if(m % i == 0) return 0 ; return 1 ; } void num(int m,int k,int xx[]) { int i=0; for(m=m+1;k>0;m++) if(isP(m)) { xx[i++]=m; k--; } } void readwriteDAT() { int m, n, xx[1000], i; FILE *rf, *wf ; 331100 W C 语言实例解析精粹 rf = fopen("IN161.DAT", "r"); wf = fopen("OUT161.DAT", "w"); for(i = 0 ; i < 10 ; i++) { fscanf(rf,"%d%d",&m,&n); num(m,n,xx) ; for(m=0;m < n ; m++) fprintf(wf, "%d ", xx[m]); fprintf(wf, "\n"); } fclose(rf); fclose(wf); } void main() { int m, n, xx[1000] ; clrscr() ; puts(" This program is to get k prime numbers which are larger than m."); printf(" >> Please input two integers to m and k : ") ; scanf("%d%d", &m, &n ) ; num(m, n, xx) ; printf(" >> The %d prime numbers which are larger than %d are:\n ",n,m); for(m = 0 ; m < n ; m++) printf(" %d ", xx[m]); readwriteDAT(); printf("\n Press any key to quit...") ; getch(); return; } 实例 162 统计符合特定条件的数 实例说明 已知数据文件 IN162.DAT 中存有 200 个 4 位数, 并已调用读函数 readDat()把这些数存入数组 a 中。编制一个函数 jsVal(),其功能是如果 4 位数各位上的数字均是 0 或 2 或 4 或 6 或 8,则统计 出满足此条件的个数 cnt,并 把 这 些 4 位数按从大到小的顺序存入数组 b 中。最后 main()函数调用 写函数 writeDat( )把结果 cnt 以及数组 b 中符合条件的 4 位数输出到 OUT162.DAT 文件中。程序 运行效果如图 162-1 所示。 图 162-1 实例 162 程序运行效果 第六部分 常见试题解答篇 X 331111 程序代码 【程序 162】 统计符合特定条件的数 #include #define MAX 200 int a[MAX], b[MAX], cnt = 0; void jsVal() { int bb[4]; int I,j,k,flag; for (I=0;I<200;I++) { bb[0]=a[I]/1000; bb[1]=a[I]%1000/100; bb[2]=a[I]%100/10; bb[3]=a[I]%10; for (j=0;j<4;j++) { if (bb[j]%2==0) flag=1; else { flag=0; break; } } if (flag==1) { b[cnt]=a[I]; cnt++; } } for(I=0;I=index;j--) { ch=xx[I][strl-1]; for(k=strl-1;k>0;k--) 331144 W C 语言实例解析精粹 xx[I][k]=xx[I][k-1]; xx[I][0]=ch; } } } void main() { clrscr(); if(ReadDat()) { printf("Can't open the file!\n"); return; } StrOR(); WriteDat(); system("pause"); } int ReadDat(void) { FILE *fp;int i=0;char *p; if((fp=fopen("in163.dat","r"))==NULL) return 1; while(fgets(xx[i],80,fp)!=NULL) { p=strchr(xx[i],'\n'); if(p) *p=0; i++; } maxline=i; fclose(fp); return 0; } void WriteDat(void) { FILE *fp; int i; fp=fopen("out163.dat","w"); for(i=0;i #include #include int aa[200],bb[10]; void jsSort() { int I,j,data; for(I=0;I<199;I++) for(j=I+1;j<200;j++) {if (aa[I]%1000>aa[j]%1000) {data=aa[I];aa[I]=aa[j];aa[j]=data;} else if(aa[I]%1000==aa[j]%1000) if(aa[I] #include #include #include #include #define MAX 100 typedef struct{ char dm[5]; /*产品代码*/ char mc[11]; /*产品名称*/ int dj; /*单价*/ int sl; /*数量*/ long je; /*金额*/ }PRO; PRO sell[MAX]; void ReadDat(); void WriteDat(); void SortDat() { int I,j; PRO xy; for(I=0;I<99;I++) for(j=I+1;j<100;j++) if(strcmp(sell[I].dm,sell[j].dm)<0) {xy=sell[I];sell[I]=sell[j];sell[j]=xy;} else if(strcmp(sell[I].dm,sell[j].dm)==0) if(sell[I].je 第六部分 常见试题解答篇 X 331199 #include #include #include unsigned char xx[50][80]; int maxline=0;/*文章的总行数*/ int ReadDat(void); void WriteDat(void); void encryptChar() { int I; char *pf; for(I=0;I130||*pf*11%256<=32); else *pf=*pf*11%256; pf++; } } } void PressKeyToQuit() { printf("\n Press any key to quit..."); getch(); return; } void main() { clrscr(); if(ReadDat()) { printf(" Can't open file IN166.DAT!\n"); return; } encryptChar(); WriteDat(); PressKeyToQuit(); } int ReadDat(void) { FILE *fp; int i=0; unsigned char *p; if((fp=fopen("in166.dat","r"))==NULL) return 1; while(fgets(xx[i],80,fp)!=NULL) { p=strchr(xx[i],'\n'); if(p)*p=0; i++; } maxline=i; fclose(fp); return 0; } 332200 W C 语言实例解析精粹 void WriteDat(void) { FILE *fp; int i; fp=fopen("out166.dat","w"); for(i=0;i #include #include float countvalue() { float x0,x1=0.0; while(1) { 第六部分 常见试题解答篇 X 332211 x0=x1; x1=cos(x0); if(fabs(x0-x1)<1e-6) break; } return x1; } void PressKeyToQuit() { printf("\n Press any key to quit..."); getch(); return; } main() { clrscr(); puts(" This program is to find the real root of"); puts(" function cos(x)-x=0."); printf(" >> The real root is %f.\n",countvalue()); printf(" >> The result of cos(%f)-%f is %f.\n",countvalue(),countvalue(), cos(countvalue())-countvalue()); writeDat(); PressKeyToQuit(); } writeDat() { FILE *wf; wf=fopen("OUT167.DAT","w"); fprintf(wf,"%f\n",countvalue()); fclose(wf); } 实例 168 新完全平方数 实例说明 本实例实现在 3 位整数(100~999)中寻找符合条件的整数并依次从小到大存入数组中,特 定条件为它既是完全平方数,又有两位数字相同,例如 144、676 等。要求满足该条件的整数的个 数通过所编制的函数返回。最后调用函数 writeDat()把结果输出到文件 out168.dat 中。 程序运行效果如图 168-1 所示。 图 168-1 实例 168 程序运行效果 332222 W C 语言实例解析精粹 程序代码 【程序 168】 新完全平方数 #include int jsvalue(int bb[]) { int I,j,k=0; int hun,ten,data; for(I=100;I<=999;I++) { j=10; while(j*j<=I) { if (I==j*j) { hun=I/100;data=I-hun*100; ten=data/10;data=data-ten*10; if(hun==ten||hun==data||ten==data) { bb[k]=I; k++; } } j++; } } return k; } void PressKeyToQuit() { printf("\n Press any key to quit..."); getch(); return; } main() { int b[20],num; clrscr(); puts(" This program is to find the Perfect Square Numbers."); puts(" which have 3 digits, and 2 of them are the same."); puts(" These numbers are as follows:"); num=jsvalue(b); writeDat(num,b); PressKeyToQuit(); } writeDat(int num,int b[]) { FILE *out; int i; out=fopen("out168.dat","w"); fprintf(out,"%d\n",num); for(i=0;i int jsvalue(long n) { int I,strl,half; char xy[20]; ltoa(n,xy,10); strl=strlen(xy); half=strl/2; for(I=0;I=half) return 1; else return 0; } void PressKeyToQuit() { printf("\n Press any key to quit..."); getch(); return; } 332244 W C 语言实例解析精粹 main() { long m; FILE *out; clrscr(); puts(" This program is to find the Palindrome Numbers."); puts(" whose square and cubic are also Palindrome Numbers."); puts(" >> These numbers less than 1000 are:"); out=fopen("out169.dat","w"); for(m=11;m<1000;m++) { if(jsvalue(m)&&jsvalue(m*m)&&jsvalue(m*m*m)) { printf(" m=%4ld,m*m=%6ld,m*m*m=%8ld \n",m,m*m,m*m*m); fprintf(out,"m=%4ld,m*m=%6ld,m*m*m=%8ld \n",m,m*m,m*m*m); } } fclose(out); PressKeyToQuit(); } 实例 170 奇数方差 实例说明 函数 ReadDat()用于从文件 IN170.DAT 中读取 1000 个十进制整数到数组 xx 中。要求编制函 数 Compute()分别计算出 xx 中奇数的个数 odd,奇数的平均值 ave1,偶数的平均值 ave2 以及所有 奇数的方差 totfc 的值,最后调用函数 WriteDat()把结果输出到 OUT170.DAT 文件中。 计算方差的公式为 ∑ = −= N 1i 2avel)(xx[i]N 1totfc ,其中 N 为奇数的个数,xx[i]为奇数,ave1 为 奇数的平均值。 原始数据文件存放的格式为每行存放 10 个数,并用空格隔开(每个数均大于 0 且小于等于 2000)。程序运行效果如图 170-1 所示。 图 170-1 实例 170 程序运行效果 程序代码 【程序 170】 奇数方差 #include 第六部分 常见试题解答篇 X 332255 #include #include #define MAX 1000 int xx[MAX],odd=0,even=0; double ave1=0.0,ave2=0.0,totfc=0.0; void WriteDat(void); int ReadDat(void) { FILE *fp; int i,j; if((fp=fopen("IN170.DAT","r"))==NULL) return 1; for(i=0;i<100;i++) { for(j=0;j<10;j++) fscanf(fp, "%d ", &xx[i*10+j]); fscanf(fp, "\n"); if(feof(fp)) break; } fclose(fp); return 0; } void Compute(void) { int I, yy[MAX]; for(I=0;I<1000;I++) if(xx[I]%2) { odd++; ave1+=xx[I]; yy[odd-1]=xx[I]; } else { even++; ave2+=xx[I]; } ave1/=odd; ave2/=even; for(I=0;I> The results are:"); for(i=0;i char xx[100][11]; int yy[10]; int ReadDat(void); void WriteDat(void); void CountRs(void) 图 171-1 实例 171 程序运行效果 第六部分 常见试题解答篇 X 332277 { int i,j,k; for(i=0;i<100;i++) { k=0; for(j=0;j<10;j++) if(xx[i][j]=='1') k++; if(k==0||k==10) continue; for(j=0;j<10;j++) if(xx[i][j]=='1') yy[j]++; } } void main() { int i; for(i=0;i<10;i++) yy[i]=0; if(ReadDat()) { printf("Can't open file IN171.DAT!\n\007" ); return; } CountRs(); WriteDat(); system("pause"); } int ReadDat(void) { FILE *fp; int i; char tt[13]; clrscr(); if((fp=fopen("in171.dat","r" ))==NULL) return 1; for(i=0;i<100;i++) { if(fgets(tt,13,fp)==NULL)return 1; memcpy(xx[i],tt,10); xx[i][10]=0; } fclose(fp); return 0; } void WriteDat(void) { FILE *fp; int i; fp=fopen("out171.dat","w" ); for(i=0;i<10;i++) { fprintf(fp," %d\n",yy[i]); printf(" NO.%2d notes=%d\n" ,i+1,yy[i]); } fclose(fp); } 332288 W C 语言实例解析精粹 实例 172 同时整除 实例说明 要求编写函数 void countvalue(int *a, int *n),求 出 1~1000 之内能被 7 或 11 整除,但不能同时 被 7 和 11 整除的所有整数,将它们放在数组 a 中,并通过 n 返回满足条件的数的个数。程序运行 效果如图 172-1 所示。 图 172-1 实例 172 程序运行效果 程序代码 【程序 172】 同时整除 #include #include void countvalue(int *a,int *n) { int I; *n=0; for(I=1;I<=1000;I++) if(I%7==0&&I%11) { *a=I; *n=*n+1; a++; } else if(I%7&&I%11==0) { *a=I; *n=*n+1; a++; } } 第六部分 常见试题解答篇 X 332299 void PressKeyToQuit() { printf("\n Press any key to quit..."); getch(); return; } main() { int aa[1000],n,k; clrscr(); puts(" This program is to find numbers\n which can be divided exactly by 7 or 11,\n but can not be divided exactly both by 7 and 11."); puts(" These numbers less than 1000 are:"); countvalue(aa,&n); for(k=0;k #include #include char xx[20][80]; void jsSort() { int i,strl,half,j,k; char ch; for(i=0;i<20;i++) /*行循环*/ { strl=strlen(xx[i]); /*每行长度*/ half=strl/2; for(j=0;jxx[i][k]) { ch=xx[i][j]; /*每次将最小数赋给 xx[i][j]*/ xx[i][j]=xx[i][k]; xx[i][k]=ch; } for(j=half-1,k=strl-1;j>=0;j--,k--) { ch=xx[i][j]; xx[i][j]=xx[i][k]; xx[i][k]=ch; } } } 第六部分 常见试题解答篇 X 333311 void PressKeyToQuit() { printf("\n Press any key to quit..."); getch(); return; } void main() { readDat(); jsSort(); writeDat(); PressKeyToQuit(); } readDat() { FILE *in; int i=0; char *p; in=fopen("in173.dat","r"); while(i<20&&fgets(xx[i],80,in)!=NULL) { p=strchr(xx[i],'\n'); if(p) *p=0; i++; } fclose(in); } writeDat() { FILE *out; int i; clrscr(); out=fopen("out173.dat","w"); for(i=0;i<20;i++) { printf("%s\n",xx[i]); fprintf(out,"%s\n",xx[i]); } fclose(out); } 实例 174 符号算式求解 实例说明 计算出满足的条件 SIX+SIX+SIX=NINE+NINE 的自然数 SIX 和 NINE 的个数 cnt,以及满足此 条件的所有的 SIX 与NINE 的和 sum。编写函数 countvalue()实现程序的要求,最后调用函数 writedat() 把结果 cnt 和 sum,输出到文件 out187.dat 中,其中 S,I,X,N,E 各代表一个十进制数字。 333322 W C 语言实例解析精粹 程序运行效果如图 174-1 所示。 图 174-1 实例 174 程序运行效果 程序代码 【程序 174】 符号算式求解 #include int cnt,sum; void countvalue() { int i; for(i=666;i<=999;i=i+2) if((i/10%10==(3*i/2)/100%10)&&((3*i/2)/1000==(3*i/2)%100/10))/*以 I 为准*/ { cnt++; sum+=i+3*i/2; } } void PressKeyToQuit() { printf("\n Press any key to quit..."); getch(); return; } void main() { cnt=sum=0; clrscr(); puts(" This program is to find the numbers of SIX and NINE,"); puts(" which satisfy the formula SIX+SIX+SIX=NINE+NINE,"); puts(" where S,I,X,N,E stand for digits between 0 and 9. "); countvalue(); printf(" >> The Number of satisfied is %d.\n",cnt); printf(" >> The Sum of all SIX and NINE is %d.\n",sum); writeDat(); PressKeyToQuit(); } writeDat() { FILE *fp; fp=fopen("OUT174.DAT","w"); fprintf(fp,"%d\n%d\n",cnt,sum); fclose(fp); } 第六部分 常见试题解答篇 X 333333 实例 175 数字移位 实例说明 已知在文件 in175.dat 中存有若干个(小于 200 个)4 位数字的正整数,函数 readdat()读取这 若干个正整数并存入数组 xx 中。要求编写函数 calvalue(),①求出文件中共有多少个正整数 totnum; ②求这些数右移 1 位后,产生的新数中为偶数的数的个数 totcnt,以及满足此条件的这些数(右 移前的值)的算术平均值 totpjz,最后调用函数 writedat()把所求的结果输出到文件 out175.dat 中。 程序运行效果如图 175-1 所示。 图 175-1 实例 175 程序运行效果 程序代码 【程序 175】 数字移位 #include #include #define MAXNUM 200 int xx[MAXNUM]; int totnum=0; int totcnt=0; double totpjz=0.0; int readdat(void); void writedat(void); void calvalue(void) { int i,data; for(i=0;i0) totnum++; data=xx[i]>>1; if(data%2==0) { totcnt++; totpjz+=xx[i]; } } totpjz/=totcnt; } 333344 W C 语言实例解析精粹 void PressKeyToQuit() { printf("\n Press any key to quit..."); getch(); return; } void main() { int i; clrscr(); puts(" This program is to execute Bit Shift Operation."); puts(" >> Now reading datas from file......Succeeded!"); puts(" >> Calculating......Completed!"); puts(" >> Results are:"); for(i=0;i> The total number of data in the file is %d.\n",totnum); printf(" >> The even number of data after bit shift is %d.\n",totcnt); printf(" >> The variance of these even numbers is %.2lf\n",totpjz); writedat(); PressKeyToQuit(); } int readdat(void) { FILE *fp; int i=0; if((fp=fopen("in175.dat","r"))==NULL) return 1; while(!feof(fp)) fscanf(fp,"%d",&xx[i++]); fclose(fp); return 0; } void writedat(void) { FILE *fp; fp=fopen("out175.dat","w"); fprintf(fp,"%d\n%d\n%.2lf\n",totnum,totcnt,totpjz); fclose(fp); } 实例 176 统计最高成绩 实例说明 已知学生的记录由学号和学习成绩构成,N 名学生的数据已存入 a 数组中。找出成绩最高的学 第六部分 常见试题解答篇 X 333355 生记录(假定最高成绩的记录是惟一的)并通过形参返回。 要求编写函数 MMM(STU a[], int n, STU *S)实现程序 的要求,并编写 sort(STU a[], int n)函数对记录数组 a 的成 绩进行排序(从高到低),最后调用函数 READWRITEDAT() 把结果和排序后的成绩记录输出到文件 OUT176.DAT 中。 例如: KS01 87 KS09 97 KS11 67 则最后输出: KS09 97 程序运行效果如图 176-1 所示。 程序代码 【程序 176】 统计最高成绩 #include #include #define N 10 void readwritedat(); typedef struct ss{ char num[10]; int s; }STU; mmm(STU a[],int n,STU *s) { int i; s->s=a[0].s; for(i=1;is->s) *s=a[i]; } void PressKeyToQuit() { printf("\n Press any key to quit..."); getch(); return; } void sort(STU a[],int n) { STU t; int i,j; for(i=0;i [F=font file] [D=device] 是用户要打印的包含图形的文件。“.DWG”是默认的扩展名。 指明用户拥有何种类型的打印机。如果用户没有修改打印程序,那么应该是 LASERJET 和 EPSON 其中之一。 [font file] 是保存字符集的字体文件,用它在做图文件中显示文本。如果不指定字体文件,那 么默认的“MICROCAD.FNT”作为缺省的字体文件 [device]指定打印机的输出端口。如果没有指定,“LPT1”是默认的。用户也可以把这个端口 指定为一个文件,这样就把图形重定向到文件中了。 (4)做图文件的格式 MICROCAD 产生的文件由 8 位的字节和 16 位的字组成。字是高位在前存储的。文件的最前 面的几个字节是用来存储“S)etup”的值,如下所示。 Word = Grid 大小(0 表示禁止) Word = Snap 大小(0 表示禁止) Word = 文本比例 (100 = 1:1) Word = 光标原点的 X 值 Word = 光标原点的 Y 值 Byte = 对象的基点显示与否,0 = ON, 1=OFF 设置参数的后面是 0 个或者多个对象,代表直线、圆等,每种类型的使用格式如下。 ① 结束符——Byte = 0x00 这个不是 MICROCAD 必须的,而且程序通常也不产生这个结束符。但是程序遇到一个包含 单一 0x00 字节的对象时就会停止绘制对象。它有两个作用:第一个作用是如果想要用类似 XMODEN 的协议传输文件,传输过程中,文件的尾部有可能被添加一些垃圾信息。用户可以首 先自己添加一个结束符来避免文件出错;第二个作用是一旦碰到结束符,后来的内容虽然可以添 加到文件,但是图形重绘或者重新载入时,结束符后面的对象将不会被绘出。 第八部分 综合实例篇 X 336655 ② 直线——Byte = 0x01 Word = 第一个点绝对 X 值 Word = 第一个点绝对 Y 值 Word = 第二个点相对 X 值 Word = 第二个点相对 Y 值 ③ 矩形——Byte = 0x02 Word = 第一个点绝对 X 值 Word = 第一个点绝对 Y 值 Word = 第二个点相对 X 值 Word = 第二个点相对 Y 值 ④ 圆——Byte = 0x03 Word = 中心点绝对 X 值 Word = 中心点绝对 Y 值 Word = 圆的半径 ⑤ 文本——Byte = 0x04 Word = 文本的绝对 X 值 Word = 文本的绝对 Y 值 Word = 文本比例 (100 = 1:1 = 16×24) Bytes= 文本串 Byte = 0 (结束符) ⑥ 圆弧——Byte = 0x05 Word = 中心点绝对 X 值 Word = 中心点绝对 Y 值 Word = 圆弧的半径 Word = 起点的角度(0-255) Word = 终点的角度(0-255) ⑦ 组——Byte = 0x06 Word = 组位置的绝对 X 值 Word = 组位置的绝对 X 值 Word = 下面包含对象的字节数 (in bytes) ... 下面是 0 个或者多个对象... X/Y 的坐标都是相对于组的起点的。 ⑧ 绝对拷贝——Byte = 0x07 Word = 复制对象的的绝对 X 值 Word = 复制对象的的绝对 Y 值 Word = 源对象在文件中距文件头的偏移值 ⑨ 相对拷贝——Byte = 0x08 Word = 复制对象的的绝对 X 值 Word = 复制对象的的绝对 Y 值 Word = 复制对象和源对象的偏移值(负值) 当对象通过命令菜单的“D)up”复制时候,使用绝对拷贝。当处理离散对象的时候,这个形 式非常有用。 当然,对象被分组后(插入一幅图),任何绝对拷贝的对象将被改成相对拷贝。但是用户不用 336666 W C 语言实例解析精粹 担心使用“E)rase”等命令后,对象的位置会改变。MICROCAD 把一个组当成一个对象来处理。 因为绝对拷贝一定要参照相同组的对象并且一个组的内容不会被改变,这样做是非常安全的。 (5)字符字体 下面的字符字体文件包含在系统中。 MICROCAD.FNT——这是默认的字体文件。 HIGHRES.FNT——这个字体的质量相当好,但是当它缩小的时候不如 MICROCAD.FNT 的效果。 MATRIX.FNT——一种(8×8)低分辨率的字体。效果看起来像点阵打印机的输出。 THINLINE.FNT——一种细长的字体。 系统同样包含一个字体编辑器(FE.COM),通过这个程序用户可以创造和编辑自己需要的字 符字体文件。用户只要以要编辑的字体文件名为参数即可运行这个程序。 (6)其他注意事项 系统所有内部的计算都是 16 位的。所以圆和圆弧如果半径太大,很可能显示出错。因为这些 对象需要平方运算,结果可能超出 16 位。 网格不是作为对象的一部分被记录的。如果用户执行了 M)ove、 E)rase、U)ndo 等在网格上 的操作,有的时候网格就会消失。每次重绘屏幕的时候,这些网格又会被重建。 程序代码 【程序 187】 综合 CAD 系统 //本实例源码参见光盘 由于本系统比较大,涉及到的文件比较多,为了节省篇幅,实例的源代码将在光盘中给出。 下面只介绍部分核心代码的情况。 (1)程序头 程序头主要定义了有关显示器、鼠标等设备的常量和全局变量。定义了与图形文件相关的缓 冲区。对图形的编辑,首先保存在缓冲区中,只有用户选择了保存文件命令,在缓冲区的数据才 会被写入磁盘的文件。程序头还定义了一些必须初始化的做图参数。 (2)分发文件操作命令 function 函数用来显示 S)etup 命令项的子功能,并且接收用户输入的命令。分发子命令到相 应的处理函数。 (3)插入图形函数 执行这个函数可以把一个做图文件的图形插入到当前图形中。一个图形是以一个组的形式插 入到另外一个文件中的,并且组中的每个对象基点表示相对组基点的偏移值,在插入前的绝对拷 贝对象都要转换为相对拷贝。 (4)图形绘制函数 draw_line()函数在屏幕上绘制出一条或者多条直线。函数提示用户选择一个起点和一系列的 终点。每次用户选择一个新的点,MICROCAD 将经过上一个点和这个点绘制一条直线。按鼠标 的右键可以取消画线操作。函数调用了 line()函数,可以通过这个函数经过两个指定端点绘制一条 直线。函数将提示用户选择两个顶点,当第 2 个顶点被选择后,MICROCAD 将以这两个顶点为 对角线绘制一个矩形。用户可以通过点击鼠标右键重新选择顶点。 提示用户选择一个中心点和圆弧上的任意一点。MICROCAD 以中心点为圆心,以中心点到 另一点的距离为半径,绘制一个圆。 提示用户选择一个中心点、一个起点、一个终点。这个圆弧是一个圆的一部分。程序通过 find_vector()函数找到离起点和终点最近的矢量。过起点矢量顺时针到终点矢量绘制一段圆弧。 第八部分 综合实例篇 X 336677 把一个文本串插入做图文件中,系统提示输入字符串,之后要求用户选择一个插入位置。显 示的文本的字体是在 fnt 文件中定义的。 程序通过从头搜索做图缓存,找到最后一次绘制的图形在缓存的位置。这样的效率比较低, 有兴趣的读者可以在程序头中加入一个全局变量,来记录最后绘制的图形元素。加入这个变量可 以避免每次查找最后的图形元素时,要重新搜索一次缓存。 (5)删除、移动、拷贝对象函数 上面是操作对象的几个函数,包括删除、移动、拷贝。这些操作都是建立在前面定义的数据 结构上,通过仔细研读本例,可以发现好的算法都是以精巧的数据结构定义为前提的。 (6)绘制对象函数 绘制对象函数把 dpos 变量指示的对象偏移(xoffset,yoffset)绘制出来。程序首先得到源对 象的绝对位置,把绝对位置加上偏移量后作为绘制位置。 (7)绘制对象函数 通过上面的函数,文件指针可以毫无偏差的在对象间移动,大大简化了用户调用接口,上层 程序不用理会后面的对象的类型,只需调用函数就可以跳过一个对象。 (8)对象选择函数 当使用 C)opy、E)rase、M)ove 命令的时候,都要先选择要操作的对象。MICROCAD 实时监 控鼠标的光标的位置,并且选择任何基点处于鼠标光标下的对象。一旦一个对象被选择, MICROCAD 通过暂时隐藏它来表示选中状态。用户可以按下鼠标的左键来确定自己的选择,这 时候就把对象从做图文件中删除了。 (9)find_vector()函数 把一个圆在每个象限分成 64 等份,这样每个圆在每个象限都有 64 条矢量。用户给出的任意 一点有可能不过其中的任意一条矢量。这时候就用一条距离所求点最近的矢量代表端点的角度。 (10)底层图像绘制函数 通过在屏幕上写像素的方式绘制各种图元。 (11)缓存和文件操作函数 这些函数嵌入了一些汇编语言,大大提高了访问硬件的速度。 (12)设备相关函数 设备相关函数主要用于图形界面的初始化、鼠标的初始化和对鼠标的调用。它们中间也嵌入 了汇编代码,提高了对硬件访问的效率。 归纳注释 本书使用了汇编指令,而内嵌汇编指令的 C 程序只能采用 TCC 命令行的编译连接方法,用 TCC 命令行实现编译连接方法是: TCC –B-L: \LIB 文件名 库文件名 其中“-L”选项指定了连接所需的库文件路径,文件名指有内嵌汇编指令的 C 程序名,库文 件指程序中要用的库函数所在的库文件(Turbo C 标准库可以省略)。 含有内嵌汇编指令的 C 程序进行编译时,必须有“-B”选择项,否则编译时,一旦遇到汇编 代码,便立刻给出警告信息,并以“-B”选择重新编译,若在 C 程序中加上“#program inline” 语句,则可以省略“-B”选择项。 由于汇编时 TCC 要调用 TASM.EXE 程序,若无此程序,可以将 MASM(3.0 以上)改名为 TASM.EXE 以代替之。 336688 W C 语言实例解析精粹 实例 188 功能强大的文本编辑器 实例说明 在前面的章节中,介绍了一个简单的文本编辑器,本章将介绍一个功能强大的文本编辑器— —EDITOR。EDITOR 是一个多文档/多窗口的文本/二进制编辑器,适合编辑批处理文件、二进制 文件、文本文件和多种编程语言。惟一的限制就是编辑的文件数目和大小受制于内存的大小。同 样,最大的窗口数目也受制于内存的大小。程序运行后的界面如图 188-1 所示。 图 188-1 编辑器界面 EDITOR 可用于处理日常的数据文件、计算机代码和各式各样的文本文件。EDITOR 提供的 窗口界面和光标操作命令使得对文件的编辑非常直观和方便。特别是块操作命令大大方便了对源 代码的编辑和文本文件的格式化。此外,EDITOR 还有一些简洁的命令格式化文本和段落。 实例解析 (1)运行 EDITOR editor [ [-f search_pattern] file name(s)] editor [ [-g regular_expression] file name(s)] editor [ [-b] file name] (2)使用 editor editor editor foo.bar editor c:\c70\TDE\main.c editor *.c \qc25\TDE\*.h editor foo.bar foobar f* foo.* f??b?? (3)匹配查找字符串或者正则表达式 editor -f find_me_load_me foo.* editor -f find_me_load_me foo.* *.bar editor -g "this|that" foo.* <=== 找 "this"或者"that" editor -g [a-zA-Z0-9_]+\( foo.bar <=== 查找 C 语言函数 如果符号“|”需要在正则表达式中出现,搜索字符串必须用引号括起来。否则 C 的命令行解 释器会把“|”理解成管道操作符(参见 DOS 的命令帮助),而不是正则表达式中的“或”运算符。 第八部分 综合实例篇 X 336699 (4)编辑二进制文件 editor -b tde.exe editor -b80 tde.exe 如果用户在命令行中不指定文件名,editor 将提示用户输入一个文件名。如果指定的文件不 存在,那么系统将建立一个以这个名字为文件名的新文件。点击两次“Enter”键将出现一个目录 列表,如图 188-2 所示。 图 188-2 目录列表界面 (5)文件操作命令 文件操作有以下快捷命令。 F2 表示保存文件。当前的文件将被修改后的文件覆盖。系统默认覆盖原文件,所以不会提示 输入新的文件名。 F3 表示关闭当前文件。如果用户正在编辑多个文件或者打开了多个窗口,editor 将搜索其中 一个不可见的文件在当前窗口显示出来。如果没有不可见的窗口,editor 就会重新组合窗口。如 果被关闭的文件是当前惟一被编辑的文件,F3 命令将关闭 editor 程序。如果关闭前对文件做了修 改,系统会提示用户是否保存修改,如图 188-3 所示。如果修改当前文件的时候又加载了一个新 的文件,系统会创建一个不可见的窗口。 图 188-3 关闭当前文件界面 F4 表示保存并且关闭当前文件。这个命令的效果相当于 F2 和 F3 组合。 #F2 表示另存为。如果不想覆盖原文件但又想保存当前的文件内容,可以选择另外一个名字 保存文件。这个命令提示用户输入一个文件名以创建一个新的文件来保存当前对文件的修改(可 以按 escape 键放弃这个命令)。 #F4 表示编辑新文件。这个命令把新文件加载为当前窗口,使现在的窗口变得不可见。用户 337700 W C 语言实例解析精粹 甚至可以加载同一个文件的几份副本。如果在修改过程中,需要参考未改变前的源文件,这个命 令就非常有用。 @F2 表示设置文件属性。属性包括 A=存档文件,S=系统文件,H=隐藏文件,R=只读文件。 如果想要编辑一个只读文件,那么就要修改文件属性:编辑文件;把文件属性设置为存档;保存 对文件的修改;把文件属性设置回只读。 @F4 表示编辑下一个文件。如果在命令行中输入了多个文件名或者通配符,通过这个命令可 以把下一个匹配的文件加载到编辑器中来。 -f pattern file(s) (DefineGrep 的命令行形式) -g pattern file(s) (DefineRegXGrep 的命令行形式) ^F12 = DefineRegXGrep #F12 = DefineGrep Grep 将搜索文件来查找一个模式,并且仅仅加载那些匹配到的文件。命令行形式的 Define Grep 和功能键定义的 Grep 有一些细微的区别。在命令行下,空格键被 DOS 命令行解释器理解成 分隔符。因此在命令行方式下,模式串不能嵌入空格。但是在功能键的方式下,模式中可以含有 任何字符。定义 Grep 同时定义了搜索模式,当一个文件已经被加载,按 RepeatFind 可以跳转到 模式出现的下一个位置。这里实际上有两个查找结构,一个为标准搜索函数定义,一个为 Grep 定义。虽然定义 Grep 同时定义了搜索模式,但是重新定义标准的搜索结构对 Grep 模式却没有影 响。除非重新定义一个 Grep,Grep 模式是不会发生变化。 F12 表示 RepeatGrep,在 Grep 被定义后,通过按此键来搜索下一个文件。 (6)查找、替换命令 EDITOR 使用了 Boyer-Moore 搜索算法来查找字符串。通常,要查找的模式越长,搜索速度 越快。所以,如果想加快搜索速度,应该尽量搜索更长的字符串,比如词组。 ^F5 表示大小写敏感与否。用户可以在任何时候定义搜索是否大小写敏感。即使在用户定义 了搜索模式后,这个命令依然起作用。 #F5 表示向前查找。用户输入定义模式后,EDITOR 就会向前搜索文件来匹配。 #F6 表示 find backward。用户输入定义模式后,EDITOR 就会向后搜索文件来匹配。 F5 表示重复向前查找。一旦用户通过 Shift+F5 定义好向前查找的模式,可以通过 F5 来查找 下一个匹配的位置。如果到了文件的尾部,就会翻转到头部继续向前查找。 F6 表示重复向后查找。一旦用户通过 Shift+F6 定义好向后查找的模式,可以通过 F6 来查找 下一个匹配的位置。如果到了文件的头部,就会翻转到尾部继续向后查找。 #F8 表示替换。这个命令要求用户指定查找和要替换为的内容。它同样会提示用户指定替换 的方向。如果想要设定大小写敏感,一定要翻转搜索标志。可以选择提示替换或者非提示替换。 不管提示还是非提示,这个函数实际上把光标移动到匹配到的位置,如图 188-4 所示。 #F7 表示 FindRegX,提示用户输入一个正则表达式作为查找条件,可以通过 F1 获得帮助。 F7 表示 RepeatFindRegX,一旦定义了一个正则表达式,通过这个键来查找下一个。 @F7 表示 RepeatFindRegXBackward,一旦定义了一个正则表达式,通过这个键来查找上一个。 (7)比较命令 #F11 表示 DefineDiff,这个命令初始化 diff,它提示用户指定要比较的窗口号和字母。命令 将忽略领头的空格、空行,忽略大小写,忽略行尾。任何两个窗口都可以被比较,但是要求两个 都是可见的。 第八部分 综合实例篇 X 337711 图 188-4 替换操作界面 Diff 命令将会提示用户输入如下内容: DIFF: Enter first window number and letter (e.g. 1a) : DIFF: Enter next window number and letter (e.g. 2a) : DIFF: Start diff at (B)eginning of file or (C)urrent position? (b/c) DIFF: Ignore leading spaces (y/n)? DIFF: Ignore all space (y/n)? DIFF: Ignore blank lines (y/n)? DIFF: Ignore end of line (useful with reformatted paragraphs) (y/n)? F11 表示重复比较命令。如果找到了一个差别,按 F11 可以查找下个不一样的位置。 (8)窗口命令 F8 表示垂直拆分窗口。当前的文件将在多个窗口被显示出来。见图 188-5,对于文件的修改 会在相同文件拷贝的每个窗口显示出来。在一个窗口对文件做的块标记,也会在另外的窗口显示 出来。每个窗口最多可以有 15 行,所有可以显示的窗口数目被显示能力所限制。 图 188-5 垂直拆分窗口界面 F9 表示水平拆分窗口。当前的文件在多个窗口被显示出来。见图 188-6,对于文件的修改会在 相同文件拷贝的每个窗口显示出来。在一个窗口对文件做的块标记,也会在另外的窗口显示出来。 337722 W C 语言实例解析精粹 图 188-6 水平拆分窗口界面 #F9 表示调整窗口的大小。使用向上和向下的光标键可以把窗口调整到合适的大小。但是不 允许调整顶端窗口的大小。 ^F9 表示最大化当前窗口。使当前的窗口充满屏幕,除了当前的窗口,其他的窗口都变得不 可见。 F10 表示下一个窗口。如果显示了不止一个窗口,可以按 F10 使光标移到下一个窗口。 #F10 表示前一个窗口和 F10 命令相反。 ^F10 表示下一个隐藏窗口,在当前窗口的位置显示下一个隐藏或者不可见的窗口。当前的窗 口变得不可见。 (9)块命令 块操作可以在文件内部也可以在文件间执行。行块的移动操作对象是光标所在行文本。矩形 块的和 stream 块移动操作从光标所在的列开始。按 Control+Break 可以阻止绝大多数的矩形块操 作。其他的块操作因为执行速度很快,几乎没有机会停止它们。 @B 表示标记矩形块——用来标记矩形的左上角和右下角,如图 188-7 所示。 图 188-7 标记矩形块界面 @L 表示标记行块——和上面的类似,但是整个行都被标记了。 @X 表示标记 steam 块——在开始行和结束行之间的文本被标记。 @U 表示取消对块的标记——如果用户不小心错误的标记了一个块,可以用这个命令取消。 注意,对标记的块进行几次操作后,块可能还是被标记的。 第八部分 综合实例篇 X 337733 @G 表示归并要删除的块——有点像自我扩展,这个函数将删除在块中的文本。 @M 表示移动块——把光标移动合适的位置,按@M 把选中的块引导到这个位置。在行块模 式下,文本被移动到当前光标所在的行的下面。在矩形块和 stream 块模式下,块被移动到光标所 在行。 @C 表示拷贝块——在行块模式下,文本被拷贝到当前光标所在的行的下面。在矩形块和 stream 块模式下,块被拷贝到光标所在行。 @K 表示拷贝块——和上个命令相同,除了块还是被标记。 @O 表示覆盖块——只能覆盖矩形块。原来的块保持标记状态。 @F 表示填充矩形块——用字符填充被标记的矩形块。 @N 表示给矩形块标号——EDITOR 提示用户输入开始数字,增量。EDITOR 只处理整数。 @P 表示打印块——把选中的块输出到打印设备并打印出来。在运行此命令之前,首先要选 定一个块,可以按 Control+Break 中止打印。 @S 表示矩形块排列 矩形块中的行按照内容做升序或者降序排列。用户可以按 Control+Break 中止排序。 #@S 表示交换块——交换块只对矩形块有效。被交换的区域假定和矩形块的宽度相同。要保 证光标在要交换的区域的左上角。 @W 表示把块写入文件中——EDITOR 提示用户输入文件名。 @E 表示用 Tab 键移动当前块——必须是行块,移动的距离是 Tab 键设置的空格数。 @T 表示裁减块尾的空格 @<表示转化为大写字母——把一个块中所有小写字母转换为大写字母。 @>表示转化为小写字母——把一个块中所有大写字母转换为小写字母。 #@>表示 BlockFixUUE——解决 Email 传输中 EBCDIC 码到 ASCII 码的转换问题。 (10)字处理命令 @V 表示翻转换行模式——在关闭换行、固定换行和动态换行间切换。在固定换行模式下,左 面的空白在页面的设置被明确的指定。在动态换行模式下,左面的空白被当前的行的排版所决定。 在 EDITOR 中,除了空白,字换行和格式化段落是没有任何关联的。字换行主要是用来当光 标或者字符接近右边空白的时候回车换行的。格式化的段落和文本在任何时候都可以使用,他们 不依赖于换行模式。 ^F6 表示设置左面的空白——可以设置为大于等于 1 列,小于右空白的任意列数。 ^F7 表示设置右面的空白——可以设置为大于左空白,小于 1040 的任意列数。 ^F8 表示设置段落空白——段落的空白可以设置为任何小于右空白的列数。 格式化段落——当前没有给任何键赋予此功能 文本从段落的开始就根据当前设置的左右空白和段落空白被格式化。如果光标没有在段落的 开始,EDITOR 会搜索到段落头,并且从头开始格式化段落。 ^B 表示格式化文本——文本从光标的位置开始被格式化。这个命令并不搜索段落的头。 @F8 表示左对齐——左对齐当前行。 @F9 表示右对齐——右对齐当前行。 @F10 表示居中——把当前行居中。 (11)TAB 键 Tab——在插入模式下,按下 Tab 键会插入几个空格,并且向右移动光标。如果在改写模式下, 仅仅向右移动光标。 #Tab——把光标移回先前的位置。 ^Tab——设置逻辑和物理 Tab 间隔。 337744 W C 语言实例解析精粹 @Tab——打开 Smart Tab 模式(这个命令在 Windows 自带的模拟 DOS 环境中可能无法正常 工作)。通过寻找在光标上面的第一个非空格行来确定 Smart Tab 的位置。 #@T——打开 TabInflate 模式(这个命令在 Windows 自带的模拟 DOS 环境中可能无法正常工 作),在这个模式下,Tab 不会被物理扩展。 (12)开关命令 ^F1——开关光标同步标志。只有光标移动命令才被同步。 ^F2——开关 Eol 显示。Eol 是一个特殊的标识行结束的符号。 ^F3——决定在行尾写什么字符: 或者什么都不写。EDITOR 可以读任何 一种格式的文件。但是用户可以通过这个命令指定写文件的时候采用哪种方法。 ^F4——决定是否裁减行尾的空格。如果这个功能打开,那么在屏幕下面的状态栏的右方会 显示一个 T,如图 188-8 所示。 图 188-8 裁减行尾的空格界面 ^F5 表示决定搜索或者排序的时候是否大小写敏感——可以在任何时候翻转这个标志,即使 在定义过搜索模式后。 Ins——在插入和改写模式下切换。 @I——决定是否采用缩进模式。在缩进模式下,EDITOR 在前面的行中搜索第一个非空格的 字符,当按下回车的时候,把光标移到这列。 @R 表示标尺开关——决定是否在可见的窗口显示标尺,如图 188-9 所示。 图 188-9 标尺开关界面 @V 表示换行方式。 第八部分 综合实例篇 X 337755 @Z 决定是否在文件的尾部写入“Control+Z”。对于某些程序,如果文件的尾部没有 “Control+Z”,它们将不会正常工作。 (13)其他命令 Enter——在光标的位置插入一行,如果光标在一行的中间,从光标的位置把行一分为二。如 果是缩进模式,还会匹配缩进。 #Enter——把光标向下移一行。光标被放置到下一行中第一个非空格字符的位置。这个按键 不会在文件中添加行。 ^Enter——把光标向下移一行。光标被放置到下一行中第一列的位置。这个按键不会在文件 中添加行。 Up (箭头)——把光标向上移。 Down (箭头)——把光标向下移。 Left (箭头)——把光标向左移。 Right (箭头)——把光标向右移。 Home——使光标在一行中的第一个分空格字符和第一列中间跳转。 End——把光标移到一行行尾。 Page Up——把光标向上移一页。 Page Down——把光标向下移一页。 ^Right——把光标移到一个单词的右端。 ^Left——把光标移到一个单词的左端。 ^#Right (Control+Shift+Right)——把屏幕向右移动一个字符。 ^#Left (Control+Shift+Left)——把屏幕向左移动一个字符。 ^Home——把光标移动到屏幕上的第一行。 ^End——把光标移动到屏幕上的最后一行。 ^Page Up——把光标移到文件的第一页。 ^Page Down——把光标移到文件的最后一页。 ESC=撤销对一行的修改——使行的状态回到未修改前的状态。当光标离开此行后,所有对这 行的修改将生效,且没有快捷的途径恢复到原来的内容。 Del——删除光标上面的字符。 ^Del——删除一个文件中的所有字符,就好像他们在一个流中,文件的中的所有字符最终都 会被删除。 Backspace——删除光标前面的字符,并且左移光标。如果光标在行的第一列,那么当前的行 就会和上面一行合并。 @=——复制当前行 @-——删除从当前光标位置到行尾的文本 ^Y——删除当前行,光标不动。 @D——删除当前行,光标不动(和^Y 一样)。 @Y 或者^U——取消最近一次的删除行操作。重做缓存最大可以容纳 200 个被删除的行,如 果删除的行大于 200 个,那么最早删除的行就被移出缓存为新删除的行空出空间。用户可以重复 按^U 来恢复在缓存中的所有行。 上面介绍的删除命令,删除的行都会保存在缓存中。 @A——在光标的下面加入一个空白行。 ^_或者 Control underline——在当前的光标位置拆分一行。 @J——把当前行和下一行合并。 337766 W C 语言实例解析精粹 ^]——匹配括号。 @1~@3——设置文件标号,每个文件最多可以设置 3 个文件标号。 #@1~#@3 (Shift+Alt+1~Shift+Alt+3)——跳转到定义的文件标号位置中。 ^@(Control+At 符号) 在光标所在位置写入当前系统的日期和时间。用户可以通过 editorcfg 实用程序来配置日期和时间的格式。 Control+Break——在 EDITOR 中,按下 Control+Break 将停止诸如打印、排序、查找/替换和 递归宏的执行。 程序代码 【程序 188】 文本编辑器 (1)结构性函数 结构性函数主要在 main.c 文件中定义。整个主函数的流程非常简单,如图 188-10 所示。 这些结构性函数用于初始化图形界面和各个内存区域,定义程序的行为,并且接收用户的输 入、格式化命令、操作命令等,最后在程序结 束的时候释放创建的内存区域和结构。 (2)文件操作函数 文件操作函数在 FILE.C 文件中定义。文 件操作包括了关于文件的 I/O 的函数。在 EDITOR 中,文件通过一个双向链表管理。每个链表中节点都有指向前一个和后一个节点的指针。 每个节点还有一个指针指向一行文本,一个行宽度变量,还有一个脏位指示器。用一个精确的计 数器统计每行文本的字符数。 (3)查找替换函数 查找替换函数在 FINDREP.C 文件中定义。 在程序中使用的是 Boyer-Moore 文本查找算法。具体的算法可以参见论文 Robert S. Boyer and J Strother Moore,"A fast string searching algorithm.",Communications of the ACM_ 20 (No. 10): 762-772,1977。 /* * 作用: 建立并且执行查找操作 * 参数: window 为当前窗口的指针 */ int find_string( WINDOW *window ) { int direction; int new_string; char pattern[MAX_COLS]; /* 想要查找的文本 */ long found_line; long bin_offset; line_list_ptr ll; register WINDOW *win; /* 把文件指针放到一个寄存器中 */ int rc; int old_rcol; int rcol; switch (g_status.command) { case FindForward : direction = FORWARD; 初始化 处理用户 的输入和 命令 结束程序 图 188-10 程序流程图 第八部分 综合实例篇 X 337777 new_string = TRUE; break; case FindBackward : direction = BACKWARD; new_string = TRUE; break; case RepeatFindForward1 : case RepeatFindForward2 : direction = FORWARD; new_string = bm.search_defined != OK ? TRUE : FALSE; break; case RepeatFindBackward1 : case RepeatFindBackward2 : direction = BACKWARD; new_string = bm.search_defined != OK ? TRUE : FALSE; break; default : direction = 0; new_string = 0; assert( FALSE ); break; } win = window; entab_linebuff( ); if (un_copy_line( win->ll, win, TRUE ) == ERROR) return( ERROR ); /* 得到搜索文本,上次的搜索文本做为缺省值*/ if (new_string == TRUE) { *pattern = '\0'; if (bm.search_defined == OK) { assert( strlen( (char *)bm.pattern ) < MAX_COLS ); strcpy( pattern, (char *)bm.pattern ); } /* * 查找 */ if (get_name( find4, win->bottom_line, pattern, g_display.message_color ) != OK || *pattern == '\0') return( ERROR ); bm.search_defined = OK; assert( strlen( pattern ) < MAX_COLS ); strcpy( (char *)bm.pattern, pattern ); build_boyer_array( ); } rc = OK; if (bm.search_defined == OK) { old_rcol = win->rcol; if (mode.inflate_tabs) win->rcol = entab_adjust_rcol( win->ll->line, win->ll->len, win->rcol); 337788 W C 语言实例解析精粹 update_line( win ); show_search_message( SEARCHING, g_display.diag_color ); bin_offset = win->bin_offset; if (direction == FORWARD) { if ((ll = forward_boyer_moore_search( win, &found_line, &rcol )) != NULL) { if (g_status.wrapped && g_status.macro_executing) rc = ask_wrap_replace( win, &new_string ); if (rc == OK) find_adjust( win, ll, found_line, rcol ); else win->bin_offset = bin_offset; } } else { if ((ll = backward_boyer_moore_search( win, &found_line, &rcol )) != NULL) { if (g_status.wrapped && g_status.macro_executing) rc = ask_wrap_replace( win, &new_string ); if (rc == OK) find_adjust( win, ll, found_line, rcol ); else win->bin_offset = bin_offset; } } if (g_status.wrapped) show_search_message( WRAPPED, g_display.diag_color ); else show_search_message( CLR_SEARCH, g_display.mode_color ); if (ll == NULL) { /* * 没有找到 */ if (mode.inflate_tabs) win->rcol = old_rcol; combine_strings( pattern, find5a, (char *)bm.pattern, find5b ); error( WARNING, win->bottom_line, pattern ); rc = ERROR; } show_curl_line( win ); make_ruler( win ); show_ruler( win ); } else { /* * 没有定义查找模式 */ error( WARNING, win->bottom_line, find6 ); rc = ERROR; } return( rc ); } (4)窗口函数 窗口函数在 WINDOW.C 文件中定义。在窗口函数定义了窗口的行为,比如对窗口的拆分、 合并,对窗口的大小进行调整,在不同窗口中切换等。 第八部分 综合实例篇 X 337799 (5)块函数 块函数在 BLOCK.C 文件中定义。 /* * 作用: 记录块的起始位置 * 参数: window 为指向当前窗口的指针 * 注意: 假定用户将会定义块的开始和结束。若采用混和方式,那么块的类型被认为是当前的块类型 */ int mark_block( WINDOW *window ) { int type; int num; long lnum; register file_infos *file; /* 临时文件 */ register WINDOW *win; /* 把当前窗口指针放到一个临时寄存器里面 */ int rc; win = window; file = win->file_info; if (win->rline > file->length || win->ll->len == EOF) return( ERROR ); if (g_status.marked == FALSE) { g_status.marked = TRUE; g_status.marked_file = file; } if (g_status.command == MarkBox) type = BOX; else if (g_status.command == MarkLine) type = LINE; else if (g_status.command == MarkStream) type = STREAM; else return( ERROR ); rc = OK; /* * 仅对于一个文件定义块,用户可以在这个文件的任何窗口进行操作 */ if (file == g_status.marked_file) { /* * 不管块的模式,标识块的起始和中止位置 */ if (file->block_type == NOTMARKED) { file->block_ec = file->block_bc = win->rcol; file->block_er = file->block_br = win->rline; } else { if (file->block_br > win->rline) { file->block_br = win->rline; if (file->block_bc < win->rcol && type != STREAM) file->block_ec = win->rcol; else file->block_bc = win->rcol; } else { 338800 W C 语言实例解析精粹 if (type != STREAM) { file->block_ec = win->rcol; file->block_er = win->rline; } else { if (win->rline == file->block_br && win->rline == file->block_er) { if (win->rcol < file->block_bc) file->block_bc = win->rcol; else file->block_ec = win->rcol; } else if (win->rline == file->block_br) file->block_bc = win->rcol; else { file->block_ec = win->rcol; file->block_er = win->rline; } } } /* * 如果用户标识的块的终止位置在起始位置前,那么交换两个位置 */ if (file->block_er < file->block_br) { lnum = file->block_er; file->block_er = file->block_br; file->block_br = lnum; } /* * 如果用户标识的块的终止列在起始列前,那么交换两个位置 */ if ((file->block_ec < file->block_bc) && (type != STREAM || (type == STREAM && file->block_br == file->block_er))) { num = file->block_ec; file->block_ec = file->block_bc; file->block_bc = num; } } /* * 如果块类型已经被定义,但是如果用户使用混和模式,那么块的类型被置为当前块的类型 */ if (file->block_type != NOTMARKED) { /* * 如果块的类型是矩形块,那么要保证左上角小于右下脚 * 如果块的类型是 stream 块,那么保证起始列小于中止列 */ if (type == BOX) { if (file->block_ec < file->block_bc) { num = file->block_ec; file->block_ec = file->block_bc; file->block_bc = num; } } 第八部分 综合实例篇 X 338811 } assert( file->block_er >= file->block_br ); file->block_type = type; file->dirty = GLOBAL; } else { /* * 已经定义好块 */ error( WARNING, win->bottom_line, block1 ); rc = ERROR; } return( rc ); } 块操作可以在文件内部也可以在文件间执行。行块的移动操作对象是光标所在行文本。矩形 块和 stream 块的移动操作从光标所在的列开始。按 Control+Break 可以阻止绝大数的矩形块操作。 其他的块操作因为执行速度很快,几乎没有机会停止它们。 归纳注释 EDITOR 编辑器是一个功能非常强大的文本编辑器,它支持目前流行的文本编辑器的绝大部 分功能。读者可以在该程序的基础上加以改进,做出功能更加完善的程序。 前面介绍的几个函数都是 EDITOR 编辑器中几个比较重要的函数,系统是由近一百个函数组 成,有兴趣的读者可以仔细阅读光盘里的源程序。 实例 189 图书管理系统 实例说明 本实例将介绍如何通过 C 语言开发一个图书管理系统。程序运行界面如图 189-1 所示。 图 189-1 图书管理系统主界面 用户通过上下方向键可以选择具体的功能菜单。系统提供了 5 个功能菜单,分别是“Member”、 “Book”、“Reports”、“Help”和“Exit”。 338822 W C 语言实例解析精粹 “Member”表示会员管理。选中该选项,单击回车,可以进入到其子菜单中,如图 189-2 所示。 图 189-2 会员管理功能菜单 “Member”选项提供了 4 个子选项,分别是“Add New Member”(添加新会员)、“Renew Existing Member”(继续使用户账号有效)、“Issue Duplicate I-card”(签发借书卡副本)以及 “Back”(返回)。 选中“Add New Member”进入添加新会员界面,如图 189-3 所示。 在主界面中选中“Book”选项,可以进行图书管理,如图 188-4 所示。 图 189-3 添加新会员信息 图 189-4 图书管理功能菜单 在图书管理功能中,提供了 4 个功能选现,分别是“Add New Books”(添加新图书)、“Issue Book”(借出图书)、“Return Book”(还书)以及“Back”(返回)。 选中“Add New book”,进入添加新图书界面,如图 189-5 所示。 图 189-5 添加新图书界面 选中“Issue book”选项后后按回车,出现如图 189-6 所示的借书界面。 注意:“Book id”和“Membership id”要到“Reports”(报表)中查询,这些编号由系统自动 实现。在输入“Book id”时,首先需要输入“2005”,回车后输入“1”,然后再次回车输入“1”。 选中“Return book”选项后按回车,出现如图 189-7 所示的界面。 第八部分 综合实例篇 X 338833 图 189-6 借书界面 图 189-7 还书界面 回到主界面,选择“Reports”选项,可以查看各种报表,如会员报表、图书报表以及借书还 书报表等,报表界面如图 189-8 所示。 图 189-8 报表管理界面 在报表管理中,提供了 3 种报表查看,“Member Details”(查看会员报表)、“Books Details” (图书报表)以及“Transactions Details”(业务报表)。 会员报表如图 189-9 所示。 图 189-9 会员报表 图书报表如图 189-10 所示。 图 189-10 图书报表 业务报表如图 189-11 所示。 图 189-11 业务报表 实例解析 由于 C 语言中操作数据库不是很方便,因此大部分的数据存储都是通过文件来进行的。在本实例 338844 W C 语言实例解析精粹 中,数据存储采用了 3 个文件来实现。程序第一次运行的时候便会自动在目录下创建。①BOOK.DAT, 用于保存图书信息;②MEMBER.DAT,用于保存会员信息;③TRANS.DAT,用于保存业务信息,即 结束还书信息。 在实例中,会员信息、图书信息以及业务信息分别由结构体来表示,如下所示: struct member //会员管理结构体 { int mid; char mname[20],madd[30]; struct msince { int day,mon,year; } ms; struct mexpir { int day,mon,year; } me; } M; struct book //图书管理结构体 { struct bkid { int gno,bno,no; } b; char bname[20],author[15],pub[20]; int price; } B; struct transaction //业务管理结构体 { int mid,tid; struct bookid { int gno,bno,no; } b; struct issued { int day,mon,year; } i; struct returned { int day,mon,year; } r; float fine; char reason; } T; 而菜单的定义是通过字符串数组定义的: 第八部分 综合实例篇 X 338855 char *mainmenu[]={ "Member", "Book", "Reports", "Help", "Exit" }; char *memmenu[]={ "Add New Member", "Renew Existing Member", "Issue Duplicate I-Card", "Back" }; char *bookmenu[]={ "Add New Books", "Issue Book", "Return Book", "Back" }; char *rptmenu[]={ "Members Details", "Books Details", "Transactions Details", "Back" }; 程序代码 【程序 189】 图书馆管理系统 由于本系统比较大,涉及到的文件比较多,读者可参考光盘中的源代码进行学习。 归纳注释 本实例采用了多个文件来保存数据结构,用户可以改进程序,采用单个文件或者数据库来实 现各项功能。另外在设计数据库时候,可以将会员编号或者图书编号设计为自定义的,这样用户 就可以很方便地借书或者还书了。 实例 190 进销存管理系统 实例说明 本实例将编制一个功能比较完善的进销存管理程序。运行该程序,系统主界面如图 190-1 所示。 338866 W C 语言实例解析精粹 图 190-1 进销存管理系统主界面 在系统的主界面中提供了 11 个功能选项,同时在主界面上会显示当前的时间。下面依次演示 系统中的重要模块。 选择功能菜单“1”进入查看商品信息界面,如图 190-2 所示。 图 190-2 进销存管理系统主界面 在界面中会显示各种商品,可以单击商品编号,查看该类商品的详细信息,如图 190-3 所示。 图 190-3 商品详细清单界面 在主界面中选择“2”,进入商品信息录入界面,如图 190-4 所示。 商品信息录入完毕,会提示是否继续录入,选择“n”,回到主界面。 在主界面上选择“3”,进入商品销售界面,如图 190-5 所示。 第八部分 综合实例篇 X 338877 图 190-4 商品信息录入界面 图 190-5 商品销售界面 在主界面中选择“4”,进入商品查询界面,如图 190-6 所示。 图 190-6 商品查询界面 1 系统提供了两种查询方式,即根据商品编号查询以及根据商品描述查询,如果选择“1”表示 根据商品编号查询,在查询界面上输入查询编号,可以显示出查询结果,如图 190-7 所示。 图 190-7 商品查询界面 2 338888 W C 语言实例解析精粹 在主界面上选择“5”,可以删除商品信息,同样可以根据商品描述或者根据商品编号删除商 品信息,界面类似商品查询界面。 在主界面上选择“6”,可以查看销售、入库以及利润报表,如图 190-8 所示。 图 190-8 报表显示界面 在图 190-8 所示中,选择“1”可以查看销售报表,选择“2”可以查看入库报表,选择“3” 可以查看利润报表。销售报表如图 190-9 所示。 图 190-9 销售报表界面 在主界面中选择“7”可以打印记录。 在主界面中选择“8”可以图形化显示一些数据,如库存数量,销售利润等,如图 190-10 所示。 图 190-10 图形显示界面 在图 190-8 中选择“1”可以图形化显示商品库存 ,如图 190-11 所示。 第八部分 综合实例篇 X 338899 图 190-11 库存的图形显示界面 实例解析 设产品信息以一个产品一条记录的形式存储在文件中,每个产品记录包含的信息有:产品名 称、描述、数量和价格等。程序将具有以下几项功能。 (1)显示产品的信息。 (2)增加商品库存,相当于进货。 (3)减少商品库存,相当于销售。 (4)查找商品库存内容。 (5)从数据库中删除商品信息。 (6)察看销售,购买,利润报告。 (7)打印报告。 (8)数量或者利润值的图示。 程序代码 【程序 190】 进销存管理系统 //本实例源码参见光盘 归纳注释 除了主函数以外,将各个模块细化,编写了一个个小的具有独立功能的函数。从而让主函数 显得整洁。通过函数的调用,也方便了对每个功能模块的修改。如果程序需要添加功能,或者删 除功能也可以仅对每个小函数进行改进。
还剩392页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 15 金币 [ 分享pdf获得金币 ] 16 人已下载

下载pdf

pdf贡献者

zsm2015

贡献于2012-05-02

下载需要 15 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf