物联网工程师技术教程综合案例
本章重点
• 控制台下的高级操作
• 顺序表的使用
• 链表的使用
首先祝贺大家完成了C语言基础知识的学习。俗语有云,“学而时习之,不亦乐乎。”要学习编程,复习的方法不是读课本、记忆知识点,而是动手完成自己的程序。只有通过大量的编程训练才能真正掌握C语言,进而造就优秀的程序员。
本章介绍三个C语言应用实例,意在展示C语言的强大功能,激发大家的兴趣和学习热情。这些应用尽管看上去功能强大、实现复杂,但做好程序的需求分析和框架设计之后,会发现它们写起来其实并不困难。
请大家做好准备,迎接C语言带来的挑战吧!
14.1 小应用:迷宫游戏
本节将完成迷宫游戏的设计。该游戏基于控制台操作,运行时在屏幕上输出一个由字符组成的迷宫。玩家通过键盘操纵迷宫中的小人从入口走到出口,游戏即结束。
为了方便游戏设计,规定迷宫的左上角为起点,右下角为终点。
迷宫游戏的开发过程主要涉及到如下知识点:
• 在控制台下开发小游戏的一般方法,包括直接获取用户输入、控制台文本输出高级控制、控制台界面重绘等知识。
• 游戏内部数据结构的设计。
• 游戏状态的存储与使用。
游戏运行时的截图如图14-1所示:
图 141 迷宫游戏截图
14.1.1 需求分析
本节仅分析功能性需求,省略非功能性需求。
迷宫游戏需要实现如下功能:
• 在控制台上用字符显示迷宫,遵循下面的规定:
空格表示通道;
井号“#”表示墙;
“@”符号表示玩家。
• 玩家使用键盘上的“ASWD”键位来控制小人的行进,无需通过回车来确认每一步输入(这一点和之前介绍的“井字游戏”的操作是不一样的)。输入无效时(例如下一步会撞墙)通过蜂鸣声对用户进行提示。
• 记录玩家走的步数,并进行提示;
• 玩家每走一步,要进行整个界面的重绘,即用新的内容覆盖原有内容,而不是直接在控制台底端输出新的画面;
• 为保护玩家的视力,要求使用不同的前景色和背景色输出游戏标题、操作说明和迷宫图。可考虑整个界面使用蓝色背景,游戏标题使用黄色,迷宫和操作说明的使用白色背景、黑色前景。
• 支持生成随机迷宫,而不是使用一个固定的迷宫,从而增强游戏的耐玩性。
14.1.2 程序设计
1. 数据结构
迷宫游戏中的主要数据结构有两个,一个是用来存储迷宫本身的数组,另一个是用来存放玩家信息的结构体。
程序中使用一个二维字符数组来存放迷宫的原始状态,数组每一个位置代表一个格子,每个元素就是格子中的内容(“#”或空格)。例如图14-2的迷宫可以由例程14-1来表示。
图 142 4 x 4迷宫示例
例程 141 用代码表示一个4 x 4 的迷宫
char maze[4][4] = {' ', ' ', '#', '#', /* 第一行 */
'#', ' ', '#', '#', /* 第二行 */
'#', ' ', ' ', '#', /* 第三行 */
'#', '#', '#', ' '}; /* 第四行 */
按照上面的表示方法,maze[0][0]是第一行、第一列的元素,maze[0][1]是第一行、第二列的元素,maze[2][0]则是第三行、第一列的元素。对应到常用的XY坐标系中,则是纵坐标y在前、横坐标x在后,如图14-3所示:
图 143 XY坐标系中的表示方法
上图中,第二行、第一列的格子对应于元素maze[1][0],即y = 1、x = 0,而不是x = 1、y = 0。这就需要大家在使用x、y坐标访问迷宫格子时做好转换工作。
完成了迷宫的数据结构设计之后,再来看看如何存储玩家的信息。要存储的玩家信息包括两类数据:
• 小人的当前位置(横纵坐标x、y);
• 小人在迷宫中的标记(本游戏中是“@”符号)。
之所以单独设定小人在迷宫中的标记而不是将其写死在程序中,是因为这样方便大家在未来对游戏进行进一步开发,例如实现增加一名玩家、增加一些敌人、添加宝箱等物品等特性。设计时进行一定的灵活性考虑,可以避免未来维护升级或二次开发时对程序的设计做过大改动,进而减少了成本投入,也降低了修改程序后带来新问题的可能性。
下面的代码定义了用来存储玩家当前状态的结构体Item。
例程 142 存储玩家信息的结构体Item
/* 结构体,用来存储玩家当前的状态 */
typedef struct Item_
{
/* 位置信息 */
int x;
int y;
const char symbol;
} Item;
2. 游戏流程
整个游戏的流程图如下所示:
图 144 迷宫游戏的流程图
3. 生成随机迷宫
经过之前几小节的工作,迷宫游戏已经开发完成了。但是游戏里使用的是一个预先设定好的迷宫,这使得每次运行时的迷宫都是一样的。如果一个游戏总是有相同的问题、相同的解法,那实在是太无聊了。借助深度优先搜索算法,可以实现生成任意大小的随机迷宫。本小节将介绍相关的算法,并给出参考实现。
首先简单介绍什么是搜索算法,以及深度优先算法的特点。
下面是一张由五个节点和四条边组成的图。搜索算法是指在一张由多个节点和连接节点的边的图中逐个访问每个节点,且每个节点只被访问一次的算法,也被称为遍历。大家可以按照任意的顺序来访问节点,例如“3->1->2->5->4”是一种有效的搜索顺序,“4->2->1->5->3”也是一种有效的搜索顺序。
图 145 由节点和边组成的图的示例
深度优先搜索是指从起始点出发,沿着边遍历图的每个节点尽可能深地搜索每个分支的一种搜索方法。当一个分支搜索完毕,再回到之前未搜索的另一个分支继续搜索,直到访问完所有的节点为止。下面以上图为例详细介绍深度优先搜索的方法。
1) 从第一个节点1出发,发现两个分支2和4。选择2号节点继续搜索,将4号节点暂时存起来;
2) 从2号节点继续搜索,只发现一个分支3;
3) 从3号节点继续搜索,发现了一个分支4;
4) 从4号节点继续搜索,发现一个分支5;
5) 从5号节点继续搜索,未发现任何分支。这时取出第一步时暂时跳过的4号节点,继续搜索;
6) 发现4号节点已经被搜索过了,因此跳过这个节点。
7) 搜索完毕。
这时图14-5的深度优先搜索顺序就是“1->2->3->4->5”。
利用第十章中介绍过的栈结构,可以很轻松地实现一个深度优先搜索算法。仍以图14-5为例介绍基本思路:
1) 初始化一个空栈Stack;
图 146 初始化一个空栈
2) 将起始点1号节点入栈;
图 147 1号节点入栈
3) 从栈中弹出一个节点,在图中找出它的后继节点,并放到栈中。这个步骤被称作展开节点。这里弹出节点1,并将节点4和2入栈。为了先访问左边的分支,需要将另一个分支先入栈,再将左边的分支入栈;
图 148 节点4、2入栈
4) 重复展开节点的步骤。从栈中弹出节点2,将它的后继节点——3号节点入栈;
图 149 节点3入栈
5) 从栈中弹出节点3,将它的后继4号节点入栈;
图 1410 节点4入栈
6) 从栈中弹出节点4,将它的后继5号节点入栈;
图 1411 节点5入栈
7) 从栈中弹出节点5。由于5号节点没有后继,因此这一步没有入栈的节点;
图 1412 节点5出栈
8) 从栈中弹出节点4。发现之前4号节点已经被访问过,跳过这个节点。
9) 栈空了,搜索随之结束。搜索顺序为“1->2->3->4->5”。
随机迷宫生成算法的基本思路如下:
1) 设定迷宫的左上角为起点,右下角为终点。出于迷宫显示上的考虑,要求整个迷宫除了起点和终点之外的所有位于边上的格子都是墙;
2) 将所有格子都标记为未访问;
3) 从第二行、第二列的方格出发,对整个迷宫进行深度优先搜索,但与普通的深度优先搜索不同的是,普通的深搜的步进为1,即每次展开节点时,会将节点的直接后继入栈;这里要求搜索的步进为2,即展开节点时,将那些和当前节点有一个节点的距离的节点入栈。最后将当前节点设置为已访问。
具体解释见图14-13:假设当前节点是0号格子,假设步进为1,那么展开0号格子后入栈的就是0号格子周围的四个格子,如左图所示(有网状底纹的格子);当深搜的步进为2时,入栈的是0号格子四周距离它一个格子距离的节点,如右图所示(同样是有网状底纹的格子)。
图 1413 从0号节点开始深搜的示意图
还应注意的是,四个节点应该按照随机的顺序入栈,否则整个迷宫看上去就不再随机了。
4) 将栈顶节点出栈。如果该节点没有被访问过,那么将该节点和它的直接前驱节点(即访问到这个节点之前访问的那个节点)之间的那个格子标记为通道,然后继续展开当前节点;否则忽略该节点。
图 1414 标记通道节点
5) 重复上述过程,直到栈变空为止。这时迷宫中应该包含三类格子:已访问、未访问和通道。
该算法要求迷宫的长和宽都是奇数,否则无法访问到终点方块。算法结束时,可保证至少有一条从起点到终点通道。最后只要将格子复制到新的迷宫内,将未访问的格子设置为墙,已访问的格子和通道格子都设置为通道即可。
14.1.3 代码实现
1. 常量定义
在Maze.h头文件中定义游戏开发过程中需要使用到的各种常量,如例程14-3所示。
例程 143 常量定义
1 /* 常量定义 */
1 /* 状态常量 */
2 #define RESULT_SUCCESS 0
3 #define RESULT_CANNOT_MOVE 1
4 #define RESULT_ARRIVED 2
5 /* 迷宫上的符号 */
6 #define PLAYER '@' /* 玩家 */
7 #define BLANK ' ' /* 道路 */
8 #define WALL '#' /* 墙 */
9 #define START '-' /* 起点 */
10 #define DESTINATION '+' /* 终点 */
11 /* 方向按键 */
12 #define UP 'W'
13 #define DOWN 'S'
14 #define RIGHT 'D'
15 #define LEFT 'A'
16 #define QUIT 'Q'
17 #define HEIGHT 5 /* 迷宫的宽度 */
18 #define WIDTH 5 /* 迷宫的高度 */
19 #define INDENT 8 /* 迷宫距离窗口左边界的空格数 */
2. 主函数
程序的主函数如下。
例程 144 迷宫游戏主程序
1 int main()
20 {
21 /* 生成的原始迷宫 */
22 char maze[HEIGHT][WIDTH];
23 /* 保存迷宫、起点、终点和玩家位置的数组 */
24 char grid[HEIGHT][WIDTH];
25 /* 用户的按键 */
26 int key;
27 /* 总步数 */
28 int steps = 0;
29 /* 是否已经到达终点的标志 */
30 int reachedDestination = 0;
31 /* 玩家对象 */
32 Item player = { 0, 0, PLAYER };
33 /* 提示信息 */
34 char* message;
35 /* 清屏 */
36 clearScreen();
37 /* 初始化迷宫 */
38 initializeMaze(grid, maze, &player);
39 /* 输出整个迷宫 */
40 printScreen(grid, steps, NULL);
41 while ((key = getKey()) != QUIT)
42 {
43 if (reachedDestination)
44 {
45 continue;
46 }
47 if (key == UP || key == DOWN || key == LEFT || key == RIGHT)
48 {
49 int dx = 0, dy = 0;
50 int ret;
51 ret = step(grid, &player, key);
52 updateGrid(grid, maze, &player);
53 switch (ret)
54 {
55 case RESULT_CANNOT_MOVE:
56 /* 发出蜂鸣声提示用户 */
57 printf("\a");
58 message = "无法移动到指定位置";
59 break;
60 case RESULT_ARRIVED:
61 message = "已经到达终点,按 Q 键退出游戏";
62 reachedDestination = 1;
63 break;
64 default:
65 ++steps;
66 message = NULL;
67 break;
68 }
69 }
70 else
71 {
72 /* 发出蜂鸣声提示用户 */
73 printf("\a");
74 message = "按键无效";
75 }
76 /* 输出整个迷宫 */
77 printScreen(grid, steps, message);
78 }
79 exitGame();
80 return 0;
81 }
主函数的第23行调用getKey()函数来获取用户的按键信息。getKey()的实现如下:
例程 145 getKey()函数
1 int getKey()
82 {
83 int key;
84 /* 使用 _getch() 函数获取一个按键,无需等待换行符 */
85 key = _getch();
86 /* 转换为大写字母之后返回 */
87 key = toupper(key);
88 return key;
89 }
3. 初始化迷宫
初始化迷宫时的逻辑如下:
首先调用setInitialMazeStructure()函数来生成一个新的迷宫,并存在maze数组中;
然后通过setInitialPlayerCoords()函数设置玩家的初始位置;
最后调用updateGrid()函数将玩家信息和原始迷宫的信息一起输出到真正的迷宫上。
下面提供了初始化迷宫的函数。
例程 146 初始化迷宫函数
1 /* 初始化迷宫 */
90 void initializeMaze(char grid[][WIDTH], char maze[][WIDTH], Item* player)
91 {
92 /* 建立迷宫的初始结构 */
93 setInitialMazeStructure(maze);
94 /* 设置玩家的初始位置 */
95 setInitialPlayerCoords(player);
96 /* 将原始迷宫和玩家合并到真正的迷宫数组中 */
97 updateGrid(grid, maze, player);
98 }
用来建立迷宫初始结构的setInitialMazeStructure()函数如下所示:
例程 147 setInitialMazeStructure()函数
1 void setInitialMazeStructure(char maze[][WIDTH])
99 {
100 int x;
101 int y;
102 char initialMaze[HEIGHT][WIDTH] =
103 { ' ', ' ', '#', '#', '#',
104 '#', ' ', '#', ' ', '#',
105 '#', ' ', ' ', '#', '#',
106 '#', '#', ' ', ' ', '#',
107 '#', '#', '#', ' ', ' ' };
108 /* 将生成的阵列填充到迷宫中 */
109 for (y = 0; y < HEIGHT; ++y)
110 {
111 for (x = 0; x < WIDTH; ++x)
112 {
113 maze[y][x] = initialMaze[y][x];
114 }
115 }
116 /* 设置起点和终点 */
117 maze[0][0] = START;
118 maze[HEIGHT - 1][WIDTH - 1] = DESTINATION;
119 }
setInitialPlayerCoords()函数用来设置玩家的起始位置。
例程 148 setInitialPlayerCoords()函数
1 void setInitialPlayerCoords(Item *player)
120 {
121 player->x = 0;
122 player->y = 0;
123 }
4. 更新迷宫状态
生成迷宫并设置玩家的起始位置之后,还要把初始迷宫和代表玩家的标记“@”画到真正的迷宫数组上。最后一个函数updateGrid()完成了这个工作。
例程 149 updateGrid()函数
1 /* 更新整个迷宫 */
124 void updateGrid(char grid[][WIDTH], const char maze[][WIDTH], Item *player)
125 {
126 setMaze(grid, maze);
127 setPlayer(grid, player);
128 }
updateGrid()函数调用了两个子函数,分别是setMaze()——将刚生成的迷宫复制到真正的迷宫数组中,和函数setPlayer()——将玩家标记复制到真正的迷宫数组中。
例程 1410 setMaze()函数
1 void setMaze(char grid[][WIDTH], const char maze[][WIDTH])
129 {
130 int x;
131 int y;
132 for (y = 0; y < HEIGHT; ++y)
133 {
134 for (x = 0; x < WIDTH; ++x)
135 {
136 grid[y][x] = maze[y][x];
137 }
138 }
139 }
在例程14-9中,第五行至第十一行完成了迷宫的复制。需要注意的是,复制是按照先行后列的顺序进行的,转换成XY坐标就是Y坐标在外循环,X坐标在内循环。当然对于复制来说,按照先列后行的顺序进行也没问题,但是输出数组时一定要按照先行后列的顺序完成。
函数setPlayer()的实现很简单,如下所示:
例程 1411 setPlayer()函数
1 /* 设置玩家在图中的位置 */
140 void setPlayer(char grid[][WIDTH], Item *m)
141 {
142 grid[m->y][m->x] = m->symbol;
143 }
事实上,玩家每走一步,都要调用updateGrid()函数来更新一遍迷宫数组,这是因为玩家的位置有了变化。另一种做法是每次更新小人位置时,使用另外的一组变量将该位置(X、Y坐标)记录下来;下次更新小人位置的时候,先根据之前记录的位置抹去迷宫数组相应位置上的标记,再将标记“@”记录到新的位置上。这样就避免了每次都要更新整个迷宫,可节约部分时间。
5. 处理按键信息
processKey()函数用来处理按键信息:根据用户输入的方向键改变小人的位置。实现如下:
例程 1412 processKey()函数
1 /* 处理玩家的按键 */
144 void processKey(int key, int* dx, int* dy)
145 {
146 switch (key)
147 {
148 case LEFT:
149 *dx = -1;
150 *dy = 0;
151 break;
152 case RIGHT:
153 *dx = 1;
154 *dy = 0;
155 break;
156 case UP:
157 *dx = 0;
158 *dy = -1;
159 break;
160 case DOWN:
161 *dx = 0;
162 *dy = 1;
163 break;
164 }
165 }
processKey()函数接受三个参数,分别是存储了用户按键信息的key以及两个指针dx和dy,分别代表小人在X方向和Y方向上的移动步数(可能的取值为-1、0和1)。容易知道移动之后,小人新位置(x', y')和原位置(x, y)之间有如下关系:
函数step()调用了processKey()来获取小人在X、Y方向上的移动步数,并更新小人的位置:如果新位置上没有墙,且在迷宫的范围内,就可以移动过去,并返回状态常量RESULT_SUCCESS;如果新位置是终点(右下角),则更新小人的位置,并返回状态常量RESULT_ARRIVED;如果新位置上有墙,则直接返回状态常量RESULT_CANNOT_MOVE。主函数根据step()的调用结果输出相应的提示信息。
step()函数的实现如下:
例程 1413 step()函数
1 /* 根据玩家的按键进行移动 */
166 int step(const char grid[][WIDTH], Item* m, int key)
167 {
168 int dx = 0;
169 int dy = 0;
170 int new_x = 0;
171 int new_y = 0;
172 int ret = RESULT_SUCCESS;
173 /* 根据按键获得玩家的步进 */
174 processKey(key, &dx, &dy);
175 /* 更新玩家的位置 */
176 new_x = m->x + dx;
177 new_y = m->y + dy;
178 /* 保证移动后的坐标仍在迷宫内 */
179 if (!(new_x >= 0 && new_x < WIDTH && new_y >= 0 && new_y < HEIGHT))
180 {
181 ret = RESULT_CANNOT_MOVE;
182 }
183 else
184 {
185 switch (grid[new_y][new_x])
186 {
187 case BLANK:
188 case START:
189 /* 没有障碍物,可以移动 */
190 m->x = new_x;
191 m->y = new_y;
192 break;
193 case DESTINATION:
194 m->x = new_x;
195 m->y = new_y;
196 ret = RESULT_ARRIVED;
197 break;
198 case WALL:
199 /* 有障碍物,不能移动到该位置 */
200 ret = RESULT_CANNOT_MOVE;
201 break;
202 }
203 }
204 /* 返回处理结果 */
205 return ret;
206 }
6. 输出迷宫状态
paintScreen()函数用来将更新过的迷宫数组输出到屏幕上,具体实现如下:
例程 1414 paintScreen()函数
1 void printScreen(const char grid[][WIDTH], int steps, char* message)
207 {
208 /* 计算右侧游戏信息的输出起始位置 */
209 const int RIGHT_START_POS = INDENT * 2 + HEIGHT;
210 setBackgroundColour(BACK_BLUE);
211 clearScreen();
212 /* 输出标题 */
213 setTextColour(FORE_YELLOW);
214 setCursorPos(HEIGHT / 2 + INDENT * 2, 1);
215 printf("迷宫游戏\n");
216 /* 输出整个迷宫 */
217 paintGrid(grid);
218 /* 输出操作指南 */
219 setBackgroundColour(BACK_WHITE);
220 setTextColour(FORE_BLACK);
221 setCursorPos(RIGHT_START_POS, 3);
222 printf("操作指南");
223 setCursorPos(RIGHT_START_POS, 4);
224 printf("A - 左 W - 上 D - 右 S - 下");
225 setCursorPos(RIGHT_START_POS, 5);
226 printf("Q - 退出");
227 /* 输出移动步数 */
228 if (steps > 0)
229 {
230 setCursorPos(RIGHT_START_POS, 7);
231 printf("已经移动了 %d 步。", steps);
232 }
233 /* 检查是否需要输出提示信息 */
234 if (message != NULL && strlen(message) > 0)
235 {
236 /* 输出提示信息 */
237 setBackgroundColour(BACK_WHITE);
238 setTextColour(FORE_RED);
239 setCursorPos(RIGHT_START_POS, 9);
240 printf("提示:%s\n", message);
241 }
242 }
在重绘屏幕之前,paintScreen()函数先调用了clearScreen()函数来清空整个屏幕缓冲区的内容。clearScreen()函数的实现很简单,只要调用cls命令即可。
例程 1415 clearScreen()函数
1 void clearScreen()
243 {
244 system("cls");
245 }
paintScreen()函数中还调用了setTextColor()和setBackgroundColor()两个函数来设置控制台的前景色和背景色。这两个函数调用了Windows API来改变控制台的颜色。Windows API是Windows提供的函数,可以完成大部分和系统关系密切的功能。当然由于这些函数是Windows提供的,因此并不能在其它平台(例如Linux)上使用。setTextColor()和setBackgroundColor()这两个函数的实现如下。
例程 1416 setTextColor()和setBackgroundColor()的实现
1 /* 上一次设置的文本颜色 */
246 static unsigned short recentTextColor = 0;
247 /* 上一次设置的背景色 */
248 static unsigned short recentBackgroundColor = 0;
249 /* 设置文本颜色 */
250 void setTextColour(unsigned short textColor)
251 {
252 CONSOLE_SCREEN_BUFFER_INFO csbInfo;
253 HANDLE stdoutput = GetStdHandle(STD_OUTPUT_HANDLE);
254 SetConsoleTextAttribute(stdoutput, textColor | recentBackgroundColor);
255 recentTextColor = textColor;
256 }
257 /* 设置背景颜色 */
258 void setBackgroundColour(unsigned short backgroundColor)
259 {
260 CONSOLE_SCREEN_BUFFER_INFO csbInfo;
261 HANDLE stdoutput = GetStdHandle(STD_OUTPUT_HANDLE);
262 SetConsoleTextAttribute(stdoutput, recentTextColor | backgroundColor);
263 recentBackgroundColor = backgroundColor;
264 }
另一个重要的函数是setCursorPos()。大家一定注意到了,之前在控制台上输出文本时,只能一行一行地顺序输出,而无法做到在屏幕任意位置输出。通过调用Windows提供的API SetConsoleCursorPosition,就可以实现在控制台任意位置处输出文本了。setCursorPos()函数封装了这个API,接受横坐标x和纵坐标y两个参数,具体实现如下:
例程 1417 setCursorPos()函数
1 void setCursorPos(int x, int y)
265 {
266 COORD loc;
267 HANDLE stdoutput = GetStdHandle(STD_OUTPUT_HANDLE);
268 loc.X = x;
269 loc.Y = y;
270 SetConsoleCursorPosition(stdoutput, loc);
271 }
将上面三个控制台相关的函数放在一个单独的文件ConsoleUtils.c中,并将函数声明放到ConsoleUtils.h里。之后大家写的控制台程序就可以直接引用相应的头文件,并使用这些函数了。
paintScreen()函数还调用了paintGrid()函数来输出整个迷宫。paintGrid()函数的实现很简单,首先设置前景色和背景色,然后通过循环将迷宫输出到屏幕上。需要注意的是,输出迷宫时要遵循先行后列的原则,这样才能将迷宫按原样输出。
例程 1418 paintGrid()函数
1 /* 向屏幕上输出整个迷宫 */
272 void paintGrid(const char grid[][WIDTH])
273 {
274 int y;
275 int x;
276 setBackgroundColour(BACK_WHITE);
277 setTextColour(FORE_BLACK);
278 for (y = 0; y < HEIGHT; ++y)
279 {
280 setCursorPos(INDENT, 3 + y);
281 for (x = 0; x < WIDTH; ++x)
282 {
283 setTextColour(FORE_BLACK);
284 printf("%c", grid[y][x]);
285 }
286 printf("\n");
287 }
288 }
7. 退出游戏
最后一个重要的函数是exitGame()。根据主函数中的逻辑,当用户按下“Q”键时,程序自动调用exitGame()退出游戏。退出游戏前需要恢复控制台的原始设置(黑底白字),否则之后控制台上输出的文本仍将保持之前的样式。
例程 1419 exitGame()函数
1 void exitGame()
289 {
290 setBackgroundColour(BACK_BLACK);
291 setTextColour(FORE_YELLOW);
292 setCursorPos(0, WIDTH + 5);
293 printf("\n");
294 system("pause");
295 /* 恢复原始控制台设置 */
296 setTextColour(FORE_WHITE);
297 }
8. 生成随机迷宫
根据14.2.3节给出的迷宫生成算法,下面的例程完成了迷宫的随机生成。
例程 1420 自动生成迷宫
1 /* 自动生成迷宫 */
298 void generateMaze(char maze[][WIDTH], int width, int height)
299 {
300 Coord *stack = (Coord*)malloc(sizeof(Coord)* width * height);
301 Coord initial = { 1, 1, 0, 0 };
302 /* 栈顶指针 */
303 int pos = 0;
304 /* 初始化随机数 */
305 srand(time(NULL));
306 /* 初始化迷宫 */
307 memset(maze, 0, width * height); /* 0 - 未被访问过 */
308 maze[0][0] = 2; /* 无墙 */
309 maze[0][1] = 2; /* 无墙 */
310 maze[1][0] = 2; /* 无墙 */
311 maze[WIDTH - 2][HEIGHT - 1] = 2; /* 无墙 */
312 maze[WIDTH - 1][HEIGHT - 2] = 2; /* 无墙 */
313 maze[WIDTH - 1][HEIGHT - 1] = 2; /* 无墙 */
314 /* 初始化栈结构 */
315 stack[pos++] = initial;
316 while (pos > 0)
317 {
318 /* 从栈中弹出一个元素 */
319 Coord current = stack[--pos];
320 /* 计算相邻格子的坐标 */
321 Coord coords[4];
322 int i;
323
324 if (maze[current.x][current.y] == 0)
325 {
326 maze[current.x][current.y] = 1; /* 标记为已访问 */
327 maze[current.tunnel_x][current.tunnel_y] = 2; /* 无墙 */
328 }
329 else if (maze[current.x][current.y] == 1)
330 {
331 /* 这个格子已经被访问过了 */
332 continue;
333 }
334 else
335 {
336 /* 不应该执行到这里 */
337 exit(-1);
338 }
339 /* 计算相邻格子的坐标 */
340 /* 左 */
341 coords[0].x = current.x - 2;
342 coords[0].y = current.y;
343 coords[0].tunnel_x = current.x - 1;
344 coords[0].tunnel_y = current.y;
345 /* 上 */
346 coords[1].x = current.x;
347 coords[1].y = current.y - 2;
348 coords[1].tunnel_x = current.x;
349 coords[1].tunnel_y = current.y - 1;
350 /* 下 */
351 coords[2].x = current.x;
352 coords[2].y = current.y + 2;
353 coords[2].tunnel_x = current.x;
354 coords[2].tunnel_y = current.y + 1;
355 /* 右 */
356 coords[3].x = current.x + 2;
357 coords[3].y = current.y;
358 coords[3].tunnel_x = current.x + 1;
359 coords[3].tunnel_y = current.y;
360 /* 随机变换四个格子的顺序 */
361 shuffleCoords(coords, 4);
362 for (i = 0; i < 4; ++i)
363 {
364 /* 将四个格子入栈 */
365 if (coords[i].x > 0 && coords[i].x < width - 1 && coords[i].y > 0 && coords[i].y < height - 1)
366 {
367 stack[pos++] = coords[i];
368 }
369 }
370 }
371 }
下面的例程用来进行四个格子入栈顺序的随机化。
例程 1421 随机化格子的入栈顺序
1 void shuffleCoords(Coord *coords, int size)
372 {
373 int *randomNumbers = (int*)malloc(sizeof(int)* size);
374 int *record = (int*)malloc(sizeof(int)* size);
375 Coord *tempArray;
376 int i, j, k, m;
377 /* 清空记录数组 */
378 memset(record, 0, sizeof(int)* size);
379 /* 生成 0 ~ size 的不重复随机数 */
380 for (i = 0; i < size; ++i)
381 {
382 m = rand() % (size - i);
383 j = 0;
384 k = 0;
385 while (j <= m)
386 {
387 if (record[j + k] == 0)
388 {
389 ++j;
390 }
391 else
392 {
393 ++k;
394 }
395 }
396 record[j + k - 1] = 1;
397 randomNumbers[i] = j + k - 1;
398 }
399 /* 根据随机数列重排序 coords */
400 tempArray = (Coord*)malloc(sizeof(Coord)* size);
401 memcpy(tempArray, coords, sizeof(Coord)* size);
402 for (int i = 0; i < size; ++i)
403 {
404 int pos = randomNumbers[i];
405 coords[i] = tempArray[pos];
406 }
407 /* 释放占用的内存 */
408 free(record);
409 free(randomNumbers);
410 }
最后还要修改用来建立迷宫初始结构的setInitialMazeStructure()函数:
例程 1422 修改后的setInitialMazeStructure()函数
1 void setInitialMazeStructure(char maze[][WIDTH])
411 {
412 int x;
413 int y;
414 char initialMaze[HEIGHT][WIDTH];
415 /* 生成随机迷宫阵列 */
416 generateMaze(initialMaze, WIDTH, HEIGHT);
417 /* 将生成的阵列填充到迷宫中 */
418 for (y = 0; y < HEIGHT; ++y)
419 {
420 for (x = 0; x < WIDTH; ++x)
421 {
422 maze[y][x] = (initialMaze[y][x] == 1 || initialMaze[y][x] == 2) ? BLANK : WALL;
423 }
424 }
425 /* 设置起点和终点 */
426 maze[0][0] = START;
427 maze[HEIGHT - 1][WIDTH - 1] = DESTINATION;
428 }
14.2 小应用:同学录
同学录是保存同窗回忆的非常好的选择。本节将介绍如何使用C语言实现一个同学录小程序。
14.2.1 需求分析
程序运行时如下图所示:
图 1415 同学录管理系统
同学录程序的功能性需求如下:
• 能够存储每名同学的个人信息和联系方式;
• 所有数据应以文件方式存储到磁盘上;
• 通讯录程序运行时,应及时将数据存储到文件中,每次更新都直接写入文件;
• 实现记录的添加、删除和修改功能;
• 实现记录的搜索功能。
14.2.2 程序设计与代码实现
1. 数据结构、枚举量与全局变量
同学录程序使用顺序表作为基本存储结构,使用到的数据结构的定义如下:
例程 1423 使用到的数据结构
1 /* 存储同学信息的结构体 */
429 typedef struct Student_
430 {
431 int student_id; /* 学号 */
432 char name[10]; /* 姓名 */
433 char telephone[30]; /* 电话 */
434 char address[30]; /* 地址 */
435 } Student;
程序中还为菜单项操作定义了枚举量和预定义宏:
1 /* 最大记录数 */
436 #define MAX_RECORDS 300
437 /* 存储数据的文件 */
438 #define DATA_FILE "student.dat"
439 /* 菜单项枚举量 */
440 enum MenuOperations
441 {
442 BatchInput = 1,
443 ReadAll,
444 Lookup,
445 Insert,
446 Remove,
447 Modify,
448 Exit
449 };
同学录程序使用全局变量存储学生信息的顺序表:
1 /* 全局变量 */
450 /* 存储学生信息的顺序表 */
451 Student studentsArray[MAX_RECORDS];
452 /* 当前学生信息的总记录数 */
453 int studentCount = 0;
2. 主函数
主函数的参考实现如下:
例程 1424 主函数
1 /* 主函数 */
454 int main()
455 {
456 /* 用户输入的选项 */
457 int option;
458 /* 用户是否要求退出的标记 */
459 int exiting = 0;
460 /* 打印欢迎信息 */
461 enterSystem();
462 /* 退出标记 */
463 while (exiting == 0)
464 {
465 menu();
466 printf("\n请选择操作:");
467 scanf("%d", &option);
468 system("cls");
469 switch (option)
470 {
471 case BatchInput:
472 batchInput(students);
473 break;
474 case ReadAll:
475 readIn(students);
476 break;
477 case Lookup:
478 lookup(students);
479 break;
480 case Insert:
481 insertStudent(students);
482 break;
483 case Remove:
484 removeStudent(students);
485 break;
486 case Modify:
487 modify(students);
488 break;
489 case Exit:
490 /* 设置退出标记为 1 */
491 exiting = 1;
492 /* 打印退出系统时的提示信息 */
493 exitSystem();
494 break;
495 default:
496 printf("无效的选项...");
497 break;
498 }
499 system("pause");
500 system("cls");
501 }
502 system("pause");
503 return 0;
504 }
3. 进入与退出系统
进入与退出同学录管理系统时,程序都会显示“请稍候”的提示,如图所示:
图 1416 “请稍候”
由于同学录管理系统启动与退出的速度很快,因此该提示并不是必要的。在这里实现此功能只是为了起到功能演示的作用。基本原理是输出每个“.”之后都调用Windows提供的API Sleep()函数停顿100毫秒,这样就能模拟出进度条前进的效果了。
进入系统时的代码如下:
例程 1425 enterSystem()函数
1 /* 进入系统 */
505 void enterSystem()
506 {
507 int i = 0;
508 printf("欢迎使用同学录管理系统!\n");
509 printf("正在进入系统,请稍候...");
510 fflush(stdout);
511 for (; i < 10; i++)
512 {
513 printf(".");
514 /* 延时函数,每次输出“.”之前停顿 0.1 秒 */
515 Sleep(100);
516 }
517 /* 清屏函数 */
518 system("cls");
519 /* 切换背景颜色 */
520 system("color 3f");
521 }
退出系统时的代码如下:
例程 1426 exitSystem()函数
1 /* 退出系统 */
522 void exitSystem()
523 {
524 int i;
525 printf("感谢使用本系统。\n");
526 printf("正在退出,请稍候");
527 fflush(stdout);
528 for (i = 0; i < 10; i++)
529 {
530 printf(".");
531 /* 延时函数,每次输出“.”之前停顿 0.1 秒 */
532 Sleep(100);
533 }
534 system("cls");
535 /* 恢复默认的背景颜色 */
536 system("color 07");
537 }
4. 系统菜单
menu()函数实现了显示系统操作菜单的功能和打印版权信息的功能,具体实现如下:
例程 1427 menu()函数
1 /* 输出系统菜单 */
538 void menu()
539 {
540 printf("================== 同学录管理系统 ==================\n");
541 printf("====================================================\n");
542 printf("当前共有 %d 条记录。\n", studentCount);
543 printf("====================================================\n");
544 printf("%d. 批量输入记录\n", BatchInput);
545 printf("%d. 读入全部记录\n", ReadAll);
546 printf("%d. 查找记录\n", Lookup);
547 printf("%d. 插入单条记录\n", Insert);
548 printf("%d. 删除记录\n", Remove);
549 printf("%d. 修改记录\n", Modify);
550 printf("%d. 退出\n", Exit);
551 printf("====================================================\n");
552 printf("版权信息:xxx 版权所有 2014\n");
553 }
5. 将同学信息存入文件
save()函数实现了将整个存储同学录的数组存入文件的功能。
例程 1428 save()函数
1 /* 将学生信息存入文件 */
554 void save(Student students[])
555 {
556 FILE * fp;
557 int i;
558 if ((fp = fopen(DATA_FILE, "wb")) == NULL)
559 {
560 printf("打开数据文件 %s 时发生错误。存储失败。\n", DATA_FILE);
561 return;
562 }
563 /* 通过循环将整个 students 数组写入文件 */
564 for (i = 0; i < MAX_RECORDS; i++)
565 {
566
567 if (fwrite(&students[i], sizeof(Student), 1, fp) != 1)
568 {
569 printf("向数据文件 %a 中写入数据时发生错误。存储失败。\n", DATA_FILE);
570 break;
571 }
572 }
573 /* 关闭文件 */
574 fclose(fp);
575 }
6. 操作一:批量录入同学信息
batchInput()函数实现了批量录入同学信息的功能。新录入的数据会覆盖已有的记录。batchInput()函数的实现如下:
例程 1429 batchInput()函数
1 /* 批量输入学生信息 */
576 void batchInput(Student students[])
577 {
578 int i;
579 printf("请输入要录入的学生的个数:\n");
580 scanf("%d", &studentCount);
581 for (i = 0; i < studentCount; ++i)
582 {
583 printf("请输入第 %d 个同学的学号:\n", i + 1);
584 scanf("%d", &students[i].student_id);
585 printf("请输入第 %d 个同学的姓名:\n", i + 1);
586 scanf("%s", students[i].name);
587 printf("请输入第 %d 个同学的电话:\n", i + 1);
588 scanf("%s", students[i].telephone);
589 printf("请输入第 %d 个同学的家庭住址:\n", i + 1);
590 scanf("%s", students[i].address);
591 }
592 save(students);
593 }
7. 操作二:从文件中读入全部记录
下面的函数readIn()实现了从文件中加载同学录数据的功能。
例程 1430 readIn()函数
1 /* 读入全部记录 */
594 void readIn(Student student[])
595 {
596 int elementsRead;
597 FILE * fp;
598 if ((fp = fopen(DATA_FILE, "rb")) == NULL)
599 {
600 printf("打开文件时发生错误。\n");
601 return;
602 }
603 studentCount = 0;
604 elementsRead = fread(&student[studentCount], sizeof(Student), 1, fp);
605 while (elementsRead == 1)
606 {
607 printf("学号 姓名 电话 家庭住址\n");
608 printf("----------------------------------------------------------\n");
609 printf("%d %s %s %s\n ", student[studentCount].student_id, student[studentCount].name, student[studentCount].telephone, student[studentCount].address);
610 printf("----------------------------------------------------------\n");
611 elementsRead = fread(&student[studentCount], sizeof(Student), 1, fp);
612 /* 增加总人数 */
613 ++studentCount;
614 }
615 fclose(fp);
616 printf("\n");
617 }
8. 操作三:查找记录
lookup()函数可以根据同学的姓名来查找对应的记录。函数中使用了strcmp()函数进行姓名匹配,这就要求待查找的姓名和原始记录中的姓名形成完整匹配。大家还可以使用strncmp()、strchr()等函数替换strcmp(),从而实现姓名的部分匹配。这个改进留为课后作业,请自行完成。
例程 1431 lookup()函数
1 /* 查找记录 */
618 void lookup(Student student[])
619 {
620 int i;
621 int studentFound = 0;
622 char name[10];
623 printf("请输入待查找同学的姓名:");
624 scanf("%s", name);
625 for (i = 0; i < studentCount; i++)
626 {
627 if (strcmp(student[i].name, name) == 0)
628 {
629 studentFound = 1;
630 printf("学号 姓名 电话 家庭住址\n");
631 printf("----------------------------------------------------------\n");
632 printf("%d %s %s %s\n ", student[i].student_id, student[i].name, student[i].telephone, student[i].address);
633 printf("----------------------------------------------------------\n");
634 }
635 }
636 if (studentFound == 0)
637 {
638 printf("查找失败,系统中不存在此同学的信息。\n");
639 }
640 }
9. 操作四:插入单条记录
insertStudent()函数可以在现有记录的基础上追加单条记录,而不会覆盖现有记录的值。实现如下:
例程 1432 insertStudent()函数
1 /* 插入记录 */
641 void insertStudent(Student students[])
642 {
643 printf("请输入需要插入同学的信息:\n");
644 printf("学号:");
645 scanf("%d", &students[studentCount].student_id);
646 printf("姓名:");
647 scanf("%s", students[studentCount].name);
648 printf("电话:");
649 scanf("%s", students[studentCount].telephone);
650 printf("家庭住址:");
651 scanf("%s", students[studentCount].address);
652 printf("插入记录成功,此同学的信息为:\n");
653 printf("学号 姓名 电话 家庭住址\n");
654 printf("----------------------------------------------------------\n");
655 printf("%d %s %s %s\n ", students[studentCount].student_id, students[studentCount].name, students[studentCount].telephone, students[studentCount].address);
656 printf("----------------------------------------------------------\n");
657 studentCount = studentCount + 1;
658 save(students);
659 }
10. 操作五:删除单条记录
例程 1433 removeStudent()函数
1 /* 删除记录 */
660 void removeStudent(Student students[])
661 {
662 int i, j, studentFound = 0;
663 char name[10];
664 printf("请输入需要删除同学信息的姓名:");
665 scanf("%s", name);
666 for (i = 0; i < studentCount; i++)
667 {
668 /* 判断输入姓名是否存在 */
669 if (strcmp(students[i].name, name) == 0)
670 {
671 /* 标记姓名存在 */
672 studentFound = 1;
673 /* 从待删除的记录开始,用后一条记录逐条覆盖前一条记录 */
674 for (j = i; j < studentCount - 1; j++)
675 {
676 students[j] = students[j + 1];
677 }
678 /* 将总记录数减一 */
679 --studentCount;
680 /* 退出循环 */
681 break;
682 }
683 }
684 if (studentFound == 0)
685 {
686 printf("删除失败,姓名 %s 不存在。\n", name);
687 }
688 if (studentFound == 1)
689 {
690 printf("删除记录成功。\n");
691 }
692 save(students);
693 }
11. 操作六:修改记录
最后一个操作是修改同学的信息,由modify()函数实现。
例程 1434 modify()函数
1 /* 修改同学信息 */
694 void modify(Student students[])
695 {
696 int i;
697 /* 循环的标记 */
698 int continueFlag = 1;
699 char name[10];
700 while (continueFlag)
701 {
702 printf("请输入需要修改同学的姓名:\n");
703 scanf("%s", name);
704 for (i = 0; i < studentCount; ++i)
705 {
706 if (!strcmp(students[i].name, name))
707 {
708 printf("学号 姓名 电话 家庭住址\n");
709 printf("----------------------------------------------------------\n");
710 printf("%d %s %s %s\n ", students[i].student_id, students[i].name, students[i].telephone, students[studentCount].address);
711 printf("----------------------------------------------------------\n");
712 /* 设置标记,结束外层循环 */
713 continueFlag = 0;
714 break;
715 }
716 }
717 if (continueFlag == 1)
718 {
719 printf("该姓名不存在,请重新输入。\n");
720 }
721 }
722 printf("------------------------------------------------------------\n");
723 printf("请选择要修改的项目:\n");
724 printf("1. 学号\n2. 姓名\n3. 电话\n4. 家庭住址\n");
725 printf("------------------------------------------------------------\n");
726 scanf("%d", &i);
727 switch (i)
728 {
729 case 1:
730 printf("请输入新的学号:");
731 scanf("%d", &students[i].student_id);
732 printf("修改成功!\n");
733 break;
734 case 2:
735 printf("请输入新的姓名:\n");
736 scanf("%s", students[i].name);
737 printf("修改成功!\n");
738 break;
739 case 3:
740 printf("请输入新的电话:\n");
741 scanf("%s", students[i].telephone);
742 printf("修改成功。\n");
743 break;
744 case 4:
745 printf("请输入新的家庭住址:\n");
746 scanf("%s", students[i].address);
747 printf("修改成功。\n");
748 break;
749 default:
750 printf("无效的选项。\n");
751 break;
752 }
753 /* 存储信息 */
754 save(students);
755 }
14.3 小应用:图书管理平台
随着当今图书市场竞争愈发激烈,中小图书企业迫切希望引入一款图书管理平台,从而加速图书流通信息的收集和反馈。最近大黄同学接到了一个小出版社的要求,希望他使用C语言开发一款图书管理平台。本节将介绍大黄同学的需求分析和架构设计的过程,最后给出代码的参考实现。
14.3.1 需求分析
本图书管理平台需要具备如下功能:
• 对图书信息进行集中管理,包括图书的增、删、改等功能;
• 实现图书的排序功能;
• 实现图书的查询功能。
程序的截图如下:
图 1417 图书管理系统截图
14.3.2 程序设计
1. 数据结构
与同学录程序不同,图书管理平台使用链表作为基本的存储结构。一本图书的属性包括图书编号(ID)、书名(bookname)、作者(author)、出版社(press)、图书类别(category)、出版日期(date)、价格(price)等信息。
2. 登录界面设计
为实现简单的身份认证机制,图书管理平台提供了登录功能,截图如下:
图 1418 登录界面
用户名和密码以常量的形式定义在程序中。一个重要的功能是,用户登录时输入的密码不能以明文形式显示在屏幕上,而是要变成星号(“*”)显示出来,从而保证密码的安全。程序中使用getch()函数实现了这个功能:
• 使用getch()获取用户的按键;
• 如果按键为正常的字母、数字或符号,则记录到一个数组中,并输出一个星号;
• 如果按键为退格键'\b',则删除记录数组中的最后一个元素,同时使用printf输出"\b \b",意为先倒退一格,再输出一个空格覆盖之前的星号,最后再倒退一格;
• 如果按键为'\n',则结束密码输入过程。
图 1419 密码输入界面
详细代码可参见下一节。
14.3.3 代码实现
1. 数据结构与枚举量
图书管理平台中使用到的枚举量与数据结构都很简单,参见下面的代码:
1 /* 预先设定好的用户名和密码 */
756 #define USERNAME "admin"
757 #define PASSWORD "drowssap"
758 /* 数据文件的文件名 */
759 #define DATA_FILE "bookinfo.db"
760
761 /* 书籍信息 */
762 typedef struct Book_
763 {
764 int ID;
765 char bookName[50];
766 char author[20];
767 char press[50];
768 char category[50];
769 char date[12];
770 float price;
771 struct Book_ *next;
772 } Book;
2. 辅助函数
程序中用到了以下几个辅助函数:getUserOption()、getUserChoice()、checkBook()和countBook()。前两个函数用来获取用户输入的选项(包括数字选项和“是/否”选项两种)。checkBook()函数用来检查图书编号是否已经存在,countBook()则用来获取当前数据库中书目的数量。这几个函数的具体实现见下。
例程 1435 getUserOption()函数
1 /* 辅助函数,获取用户输入的选项 */
773 int getUserOption(int maxOption)
774 {
775 int option;
776 printf("请输入您的选项(0-%d):", maxOption);
777 scanf("%d", &option);
778 getchar(); /* 吞掉后面的回车 */
779 while (option > maxOption)
780 {
781 printf("选项无效,请重新输入:");
782 scanf("%d", &option); /* 吞掉后面的回车 */
783 }
784 return option;
785 }
例程 1436 getUserChoice()函数
1 /* 辅助函数,获取用户的选择(y/n) */
786 int getUserChoice()
787 {
788 int op;
789 printf("请输入您的选择(y/n):");
790 op = tolower(getchar()); /* 吞掉后面的回车 */
791 while (op != 'y' && op != 'n')
792 {
793 printf("您的选择无效,请重新输入(y/n):");
794 op = tolower(getchar()); /* 吞掉后面的回车 */
795 }
796 return op;
797 }
例程 1437 checkBookID()函数
1 /* 辅助函数 */
798 /* 验证添加的图书编号是否已存在 */
799 int checkBookID(Book *head, int m)
800 {
801 Book *p;
802 p = head;
803 while (p != NULL)
804 {
805 if (p->ID == m)
806 {
807 break;
808 }
809 p = p->next;
810 }
811 if (p == NULL)
812 {
813 return 0;
814 }
815 else
816 {
817 return 1;
818 }
819 }
例程 1438 countBook()函数
1 /* 辅助函数 */
820 /* 统计图书数量 */
821 int countBook(Book *head)
822 {
823 int count = 0;
824 Book *p = head;
825 while (p != NULL)
826 {
827 ++count;
828 p = p->next;
829 }
830 return count;
831 }
3. 主函数(登录界面)
主界面实现了用户名和密码的获取与比较,且应用了之前介绍过了隐藏密码输入的技巧,参考实现如下:
例程 1439 主函数
1 int main()
832 {
833 /* 存储用户的选择 */
834 int choice;
835 int continueFlag = 1;
836 while (continueFlag)
837 {
838 clearScreen();
839 printIndexPage();
840 choice = getUserOption(1);
841 switch (choice)
842 {
843 case ACTION_EXIT:
844 continueFlag = 0;
845 break;
846 case ACTION_LOGIN:
847 {
848 char inputBufferUsername[100];
849 char inputBufferPassword[100];
850 char charInput = 0;
851 /* inputBufferPassword 的当前位置 */
852 int pos = 0;
853 printf("请输入您的用户名:");
854 gets(inputBufferUsername);
855 printf("请输入您的密码:");
856 /* 对于密码输入,不显示输入的字符 */
857 /* 使用 getch() 函数实现这个功能 */
858 charInput = _getch();
859 while (charInput != '\r')
860 {
861 if (charInput == '\b')
862 {
863 /* 退格键 */
864 if (pos > 0)
865 {
866 /* 将当前位置后移一位,相当于删除一个字符 */
867 --pos;
868 /* 用空格覆盖刚才的星号,并退格 */
869 printf("\b \b");
870 }
871 }
872 else
873 {
874 inputBufferPassword[pos] = charInput;
875 /* 将当前位置前移一位 */
876 ++pos;
877 /* 输出一个星号 */
878 printf("*");
879 }
880 charInput = _getch();
881 }
882 /* 使用空字符作为 inputBufferPassword 的字符串结束符 */
883 inputBufferPassword[pos] = 0;
884 /* 输出一个额外的换行 */
885 printf("\n");
886 /* 用户名不要求大小写完全一致,密码要求大小写一致 */
887 if (!_stricmp(inputBufferUsername, USERNAME) &&
888 !strcmp(inputBufferPassword, PASSWORD))
889 {
890 printf("验证通过!按任意键进入系统。\n");
891 _getch();
892 enterManagementInterface();
893 }
894 else
895 {
896 printf("验证失败,请检查用户名和密码是否正确输入。\n");
897 _getch();
898 }
899 break;
900 }
901 }
902 }
903 }
4. 主菜单
enterManagementInterface()函数负责打印欢迎信息、接收用户输入,并调用相应的操作处理函数。打印欢迎信息在函数printHeader()中实现,在此略去。enterManagementInterface()函数的实现如下:
例程 1440 enterManagementInterface()函数
1 /* 主菜单 */
904 void enterManagementInterface()
905 {
906 /* 链表 */
907 Book *bookList = NULL;
908 int continueFlag = 1;
909 int option;
910 int choice;
911 while (continueFlag)
912 {
913 clearScreen();
914 printHeader();
915 option = getUserOption(7);
916 system("cls");
917 switch (option)
918 {
919 case ACTION_EXIT:
920 continueFlag = 0;
921 break;
922 case ACTION_ADD_BOOK:
923 bookList = loadFromFile();
924 if (bookList == NULL)
925 {
926 printf("文件为空, 请先录入数据!\n");
927 getchar();
928 break;
929 }
930 else
931 {
932 bookList = insertBook(bookList);
933 printf("添加成功!\n");
934 printf("是否将新信息保存到文件?");
935 choice = getUserChoice();
936 if (choice == 'y')
937 {
938 writeToFile(bookList);
939 printf("保存成功!\n");
940 getchar();
941 }
942 }
943 case ACTION_REMOVE_BOOK:
944 bookList = loadFromFile();
945 if (bookList == NULL)
946 {
947 printf("文件为空,请先录入数据!\n");
948 getchar();
949 break;
950 }
951 else
952 {
953 removeBook(bookList);
954 getchar();
955 break;
956 }
957 break;
958 case ACTION_LIST_BOOK:
959 bookList = loadFromFile();
960 if (bookList == NULL)
961 {
962 printf("文件为空,请先录入数据!\n");
963 getchar();
964 break;
965 }
966 else
967 {
968 listBook(bookList);
969 getchar();
970 break;
971 }
972 case ACTION_SORT_BOOK:
973 bookList = loadFromFile();
974 if (bookList == NULL)
975 {
976 printf("文件为空,请先录入数据!\n");
977 getchar();
978 break;
979 }
980 else
981 {
982 sort(bookList);
983 getchar();
984 }
985 break;
986 case ACTION_QUERY_BOOK:
987 bookList = loadFromFile();
988 if (bookList == NULL)
989 {
990 printf("文件为空,请先录入数据!\n");
991 getchar();
992 break;
993 }
994 else
995 {
996 query(bookList);
997 getchar();
998 }
999 break;
1000 case ACTION_MODIFY_BOOK:
1001 bookList = loadFromFile();
1002 if (bookList == NULL)
1003 {
1004 printf("文件为空,请先录入数据!\n");
1005 getchar();
1006 break;
1007 }
1008 else
1009 {
1010 modifyBook(bookList);
1011 getchar();
1012 break;
1013 }
1014 break;
1015 case ACTION_INPUT_BOOK_INFO:
1016 bookList = inputBookInfo();
1017 printf("是否将输入的信息保存到文件以覆盖文件中已存在的信息?");
1018 choice = getUserChoice();
1019 if (choice == 'y')
1020 {
1021 writeToFile(bookList);
1022 printf("保存成功!\n");
1023 getchar();
1024 }
1025 default:
1026 printf("您的输入有误,请重新输入!\n");
1027 getchar();
1028 break;
1029 }
1030 }
1031 }
5. 批量录入图书信息
inputBookInfo()函数实现了批量录入图书信息的功能,实现如下:
例程 1441 inputBookInfo()函数
1 /* 录入图书信息,并存入链表中 */
1032 Book *inputBookInfo()
1033 {
1034 Book *head, *tail, *p;
1035 int bookID, n;
1036 char bookName[50], author[20], press[50], category[50], date[12];
1037 float price;
1038 int size = sizeof(Book);
1039 head = tail = NULL;
1040
1041 while (1)
1042 {
1043 int endFlag = 0;
1044 while (1)
1045 {
1046 printf("请输入图书编号,结束录入请输入 0:");
1047 scanf("%d", &bookID);
1048 endFlag = (bookID == 0);
1049 if (!endFlag)
1050 {
1051 n = checkBookID(head, bookID);
1052 if (n == 0)
1053 {
1054 break;
1055 }
1056 else
1057 {
1058 printf("您输入的编号已存在,请重新输入。\n");
1059 }
1060 }
1061 else
1062 {
1063 break;
1064 }
1065 }
1066 if (endFlag)
1067 {
1068 /* 退出循环 */
1069 break;
1070 }
1071 printf("请输入图书名:");
1072 scanf("%s", bookName);
1073 getchar();
1074 printf("请输入作者名:");
1075 scanf("%s", author);
1076 getchar();
1077 printf("请输入出版社:");
1078 scanf("%s", press);
1079 getchar();
1080 printf("请输入类别:");
1081 scanf("%s", category);
1082 getchar();
1083 printf("请输入出版时间:");
1084 scanf("%s", &date);
1085 getchar();
1086 printf("请输入价格:");
1087 scanf("%f", &price);
1088 getchar();
1089 p = (Book *)malloc(size);
1090 p->ID = bookID;
1091 strcpy(p->bookName, bookName);
1092 strcpy(p->author, author);
1093 strcpy(p->press, press);
1094 strcpy(p->category, category);
1095 strcpy(p->date, date);
1096 p->price = price;
1097 p->next = NULL;
1098 if (head == NULL)
1099 {
1100 head = p;
1101 }
1102 else
1103 {
1104 tail->next = p;
1105 }
1106 tail = p;
1107 }
1108 return head;
1109 }
6. 录入单本图书信息
录入单本图书信息的实现与批量录入图书信息的实现类似,请参考下面的给出的实现。
例程 1442 insertBook()函数
1 /* 录入单本图书的信息,并向链表中插入新的图书 */
1110 Book *insertBook(Book *head)
1111 {
1112 Book *ptr, *p1 = NULL, *p2 = NULL, *p = NULL;
1113 char bookName[50], author[20], press[50], category[50], date[12];
1114 int size = sizeof(Book);
1115 int bookID, n = 1;
1116 float price;
1117 while (1)
1118 {
1119 printf("请输入图书编号:");
1120 scanf("%d", &bookID);
1121 n = checkBookID(head, bookID);
1122 if (n == 0)
1123 break;
1124 else
1125 printf("您输入的编号已存在,请重新输入!\n");
1126 }
1127 printf("请输入图书名:");
1128 scanf("%s", bookName);
1129 getchar();
1130 printf("请输入作者名:");
1131 scanf("%s", author);
1132 getchar();
1133 printf("请输入出版社:");
1134 scanf("%s", press);
1135 getchar();
1136 printf("请输入类别:");
1137 scanf("%s", category);
1138 getchar();
1139 printf("请输入出版时间:");
1140 scanf("%s", &date);
1141 getchar();
1142 printf("请输入价格:");
1143 scanf("%f", &price);
1144 getchar();
1145 /* 创建新链表节点 */
1146 p = (Book *)malloc(size);
1147 p->ID = bookID;
1148 strcpy(p->bookName, bookName);
1149 strcpy(p->author, author);
1150 strcpy(p->press, press);
1151 strcpy(p->category, category);
1152 strcpy(p->date, date);
1153 p->price = price;
1154 /* 链表操作 */
1155 p2 = head;
1156 ptr = p;
1157 /* 将节点插入链表,同时保持链表中的节点按照ID升序排列 */
1158 /* 查找应当插入的位置,或链表的尾节点 */
1159 while ((ptr->ID > p2->ID) && (p2->next != NULL))
1160 {
1161 p1 = p2;
1162 p2 = p2->next;
1163 }
1164 if (ptr->ID <= p2->ID)
1165 {
1166 if (head == p2)
1167 {
1168 /* 插入链表开头的情况,需要让头节点指向新的节点 */
1169 head = ptr;
1170 }
1171 else
1172 {
1173 /* 插入链表中间的情况,需要让前一个节点 p1 的 next 域指向新的节点 */
1174 p1->next = ptr;
1175 }
1176 /* 让新节点的 next 域指向 p2 */
1177 p->next = p2;
1178 }
1179 else
1180 {
1181 /* 插入链表结尾的情况,让新节点的 next 域为 NULL */
1182 p2->next = ptr;
1183 p->next = NULL;
1184 }
1185 return head;
1186 }
7. 从数据库中读入图书信息
loadFromFile()函数实现了以下功能:从数据库文件中逐条读入图书信息,然后根据图书信息创建链表。
例程 1443 loadFromFile()函数
1 /* 从数据库中读取图书信息 */
1187 Book *loadFromFile()
1188 {
1189 FILE *fp;
1190 char ch;
1191 Book *head, *tail, *p1;
1192 head = tail = NULL;
1193 if ((fp = fopen(DATA_FILE, "r")) == NULL)
1194 {
1195 printf("打开数据库文件 %s 时发生错误。\n", DATA_FILE);
1196 return NULL;
1197 }
1198 ch = fgetc(fp);
1199 if (ch == '1')
1200 {
1201 while (!feof(fp))
1202 {
1203 p1 = (Book *)malloc(sizeof(Book));
1204 fscanf(fp, "%d%s%s%s%s%s%f\n", &p1->ID, p1->bookName, p1->author, p1->press, p1->category, p1->date, &p1->price);
1205 if (head == NULL)
1206 {
1207 head = p1;
1208 }
1209 else
1210 {
1211 tail->next = p1;
1212 }
1213 tail = p1;
1214 }
1215 tail->next = NULL;
1216 fclose(fp);
1217 return head;
1218 }
1219 else
1220 return NULL;
1221 }
8. 将链表写入数据库中
writeToFile()函数可以将整个图书信息链表转存到数据库中。
例程 1444 writeToFile()函数
1 /* 将整个链表写入数据库中 */
1222 void writeToFile(Book *head)
1223 {
1224 FILE *fp;
1225 char ch = '1';
1226 Book *p1;
1227 if ((fp = fopen(DATA_FILE, "w")) == NULL)
1228 {
1229 printf("打开数据库文件 %s 时发生错误。\n", DATA_FILE);
1230 return;
1231 }
1232 fputc(ch, fp);
1233 for (p1 = head; p1; p1 = p1->next)
1234 {
1235 fprintf(fp, "%d %s %s %s %s %s %.02f\n", p1->ID, p1->bookName, p1->author, p1->press, p1->category, p1->date, p1->price);
1236 }
1237 fclose(fp);
1238 }
9. 列出全部书目
listBook()函数通过遍历整张链表列出全部书目,实现如下:
例程 1445 listBook()函数
1 /* 列出所有书目 */
1239 void listBook(Book *head)
1240 {
1241 Book *ptr;
1242 if (head == NULL)
1243 {
1244 printf("\n没有信息!\n");
1245 return;
1246 }
1247 printf(" 全部图书信息\n");
1248 printf("---------------------------------------------------------------------------------------\n");
1249 printf(" 编号 图书名 作者名 出版社 类别 出版时间 价格\n");
1250 /* 遍历整个链表,输出每一项的信息 */
1251 for (ptr = head; ptr; ptr = ptr->next)
1252 {
1253 printf(" %d %s %s %s %s %s %.02f\n", ptr->ID, ptr->bookName, ptr->author, ptr->press, ptr->category, ptr->date, ptr->price);
1254 }
1255 printf("---------------------------------------------------------------------------------------\n");
1256 }
10. 删除单本图书
removeBook()可以实现从数据库中删除单本图书的功能。当数据库中只有一本图书信息时,会自动提示是否清空数据库。
例程 1446 removeBook()函数
1 /* 删除图书信息 */
1257 void removeBook(Book *head)
1258 {
1259 int a;
1260 char b, ch = '1';
1261 Book *p1, *p2 = NULL;
1262 FILE *fp;
1263 printf("请输入要删除的图书编号:");
1264 scanf("%d", &a);
1265 p1 = head;
1266 if (p1->ID == a && p1->next == NULL)
1267 {
1268 /* 当前只有一条数据,询问是否清空文件 */
1269 printf("是否清空数据库?");
1270 b = getUserChoice();
1271 switch (b)
1272 {
1273 case 'y':
1274 if ((fp = fopen(DATA_FILE, "w")) == NULL)
1275 {
1276 printf("打开数据库文件 %s 时发生错误。\n", DATA_FILE);
1277 return;
1278 }
1279 fclose(fp);
1280 printf("数据库已清空。\n");
1281 break;
1282 case 'n':
1283 break;
1284 }
1285 }
1286 else
1287 {
1288 while (p1->ID != a&&p1->next != NULL)
1289 {
1290 p2 = p1;
1291 p1 = p1->next;
1292 }
1293 if (p1->next == NULL)
1294 {
1295 if (p1->ID == a)
1296 {
1297 p2->next = NULL;
1298 printf("是否确定从数据库中彻底删除该图书?");
1299 b = getUserChoice();
1300 switch (b)
1301 {
1302 case 'y':
1303 writeToFile(head);
1304 printf("删除成功。\n");
1305 getchar();
1306 break;
1307 case 'n':
1308 break;
1309 }
1310 }
1311 else
1312 {
1313 printf("没有找到要删除的数据!\n");
1314 getchar();
1315 }
1316 }
1317 else if (p1 == head)
1318 {
1319 head = p1->next;
1320 printf("是否确定从文件中彻底删除该图书?");
1321 b = getUserChoice();
1322 switch (b)
1323 {
1324 case 'y':
1325 writeToFile(head);
1326 printf("删除成功。\n");
1327 getchar();
1328 break;
1329 case 'n':
1330 break;
1331 }
1332 }
1333 else
1334 {
1335 p2->next = p1->next;
1336 printf("是否确定从文件中彻底删除该图书?");
1337 b = getUserChoice();
1338 switch (b)
1339 {
1340 case 'y':
1341 writeToFile(head);
1342 printf("删除成功。\n");
1343 getchar();
1344 break;
1345 case 'n':
1346 break;
1347 }
1348 }
1349 }
1350 }
11. 图书信息查询示例
query()函数用来进行图书信息的查询,本图书管理系统支持按照图书编号、图书名称、类别、作者和出版时间进行查询。query()函数的实现如下:
例程 1447 query()函数
1 /* 图书查询 */
1351 void query(Book *head)
1352 {
1353 int a;
1354 printf(" == == == == == == == == == == == == == == == == == == == == == == == == == == == == == \n");
1355 printf(" ** 1 - 按编号查询 2 - 按书名查询 **\n");
1356 printf(" ** 3 - 按类别查询 4 - 按作者查询 **\n");
1357 printf(" ** 5 - 按出版时间查询 0 - 退出查询 **\n");
1358 printf(" == == == == == == == == == == == == == == == == == == == == == == == == == == == == == \n");
1359 printf("请输入所选择的编号:");
1360 scanf("%d", &a);
1361 getchar();
1362 switch (a)
1363 {
1364 case 0:
1365 break;
1366 case 1:
1367 queryByBookID(head);
1368 break;
1369 case 2:
1370 queryByName(head);
1371 break;
1372 case 3:
1373 queryByCategory(head);
1374 break;
1375 case 4:
1376 queryByAuthor(head);
1377 break;
1378 case 5:
1379 queryByDate(head);
1380 break;
1381 default:
1382 printf("您的输入有误!\n");
1383 break;
1384 }
1385 }
为节约篇幅,在此仅提供根据图书ID进行查询的示例queryByBookID(),如下:
例程 1448 queryByBookID()函数
1 /* 按编号查询图书信息 */
1386 void queryByBookID(Book *head)
1387 {
1388 int id;
1389 Book *p;
1390 printf("请选择您要查询的图书编号:");
1391 scanf("%d", &id);
1392 getchar();
1393 p = head;
1394 while (p != NULL)
1395 {
1396 if (p->ID == id)
1397 break;
1398 p = p->next;
1399 }
1400 if (p == NULL)
1401 {
1402 printf("没有找到编号为 %d 的图书。\n", id);
1403 }
1404 else
1405 {
1406 printf(" 您所查询的图书信息如下\n");
1407 printf(" == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == = \n");
1408 printf(" ** 编号 图书名 作者名 出版社 类别 出版时间 价格 **\n");
1409 printf(" ** %d %s %s %s %s %s %.02f **\n", p->ID, p->bookName, p->author, p->press, p->category, p->date, p->price);
1410 printf(" == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == = \n");
1411 }
1412 }
12. 图书信息排序示例
图书管理系统还提供根据不同信息进行排序输出的功能,示例如下:
例程 1449 排序输出示例
1 /* 图书排序 */
1413 void sort(Book *head)
1414 {
1415 int option;
1416 printf(" == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == \n");
1417 printf(" ** 1 - 按图书编号排序 2 - 按出版时间排序 **\n");
1418 printf(" ** 3 - 按图书价格排序 4 - 按图书名排序 **\n");
1419 printf(" ** 5 - 按作者名排序 0 - 取消排序操作 **\n");
1420 printf(" == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == \n");
1421 option = getUserOption(5);
1422 switch (option)
1423 {
1424 case 0:
1425 break;
1426 case 1:
1427 sortByID(head);
1428 break;
1429 case 2:
1430 sortByDate(head);
1431 break;
1432 case 3:
1433 sortByPrice(head);
1434 break;
1435 case 4:
1436 sortByName(head);
1437 break;
1438 case 5:
1439 sortByAuthor(head);
1440 break;
1441 default:
1442 printf("您的输入有误!\n");
1443 break;
1444 }
1445 }
1446 /* 按图书编号排序 */
1447 void sortByID(Book *head)
1448 {
1449 Book **books;
1450 Book *p, *p1, *temp;
1451 int i, k, index, n = countBook(head);
1452 char b;
1453 books = malloc(sizeof(Book*), n);
1454 p1 = head;
1455 for (i = 0; i < n; i++)
1456 {
1457 books[i] = p1;
1458 p1 = p1->next;
1459 }
1460 for (k = 0; k < n - 1; k++)
1461 {
1462 index = k;
1463 for (i = k + 1; i < n; i++)
1464 {
1465 if (books[i]->ID < books[index]->ID)
1466 {
1467 index = i;
1468 }
1469 }
1470 temp = books[index];
1471 books[index] = books[k];
1472 books[k] = temp;
1473 }
1474 printf("排序成功!\n");
1475 printf("是否显示排序结果?");
1476 b = getUserChoice();
1477 switch (b)
1478 {
1479 case 'n':
1480 break;
1481 case 'y':
1482 printf(" == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == \n");
1483 printf(" ** 编号 图书名 作者名 出版社 类别 出版时间 价格 **\n");
1484 for (i = 0; i < n; i++)
1485 {
1486 printf("** %d %s %s %s %s %d %.2f **\n", books[i]->ID, books[i]->bookName, books[i]->author, books[i]->press, books[i]->category, books[i]->date, books[i]->price);
1487 }
1488 printf(" == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == \n");
1489 break;
1490 default:
1491 printf("您的输入有误。\n");
1492 break;
1493 }
1494 free(books);
1495 }
- 点赞
- 收藏
- 关注作者
评论(0)