物联网工程师技术教程综合案例

举报
tea_year 发表于 2024/01/22 17:02:51 2024/01/22
【摘要】 本章重点• 控制台下的高级操作• 顺序表的使用• 链表的使用首先祝贺大家完成了C语言基础知识的学习。俗语有云,“学而时习之,不亦乐乎。”要学习编程,复习的方法不是读课本、记忆知识点,而是动手完成自己的程序。只有通过大量的编程训练才能真正掌握C语言,进而造就优秀的程序员。本章介绍三个C语言应用实例,意在展示C语言的强大功能,激发大家的兴趣和学习热情。这些应用尽管看上去功能强大、实现复杂,但做好...


本章重点

控制台下的高级操作

顺序表的使用

链表的使用

首先祝贺大家完成了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 = 1x = 0,而不是x = 1y = 0。这就需要大家在使用xy坐标访问迷宫格子时做好转换工作。

完成了迷宫的数据结构设计之后,再来看看如何存储玩家的信息。要存储的玩家信息包括两类数据:

小人的当前位置(横纵坐标xy);

小人在迷宫中的标记(本游戏中是“@”符号)。

之所以单独设定小人在迷宫中的标记而不是将其写死在程序中,是因为这样方便大家在未来对游戏进行进一步开发,例如实现增加一名玩家、增加一些敌人、添加宝箱等物品等特性。设计时进行一定的灵活性考虑,可以避免未来维护升级或二次开发时对程序的设计做过大改动,进而减少了成本投入,也降低了修改程序后带来新问题的可能性。

下面的代码定义了用来存储玩家当前状态的结构体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出发,发现两个分支24。选择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,并将节点42入栈。为了先访问左边的分支,需要将另一个分支先入栈,再将左边的分支入栈;


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()函数来更新一遍迷宫数组,这是因为玩家的位置有了变化。另一种做法是每次更新小人位置时,使用另外的一组变量将该位置(XY坐标)记录下来;下次更新小人位置的时候先根据之前记录的位置抹去迷宫数组相应位置上的标记,再将标记“@”记录到新的位置上。这样就避免了每次都要更新整个迷宫,可节约部分时间。

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以及两个指针dxdy,分别代表小人在X方向和Y方向上的移动步数(可能的取值为-101)。容易知道移动之后,小人新位置(x', y')和原位置(x, y)之间有如下关系:



函数step()调用了processKey()来获取小人在XY方向上的移动步数,并更新小人的位置:如果新位置上没有墙,且在迷宫的范围内,就可以移动过去,并返回状态常量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 APIWindows提供的函数,可以完成大部分和系统关系密切的功能。当然由于这些函数是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 }


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。