输入字母就死循环?加上输入校验和区间提示,才算真正好用的猜数字游戏
在前两弹中,我们基于游戏的核心逻辑,初步实现了游戏的基础版;接着,我们通过函数的调用与封装,先后完成了游戏功能的扩展和程序结构的优化。但到目前为止,仍有一些功能尚未实现,代码中也还存在漏洞,这些问题都可能导致程序异常甚至崩溃。接下来,我们将针对这些漏洞对代码进行修正或重构;同时,我们会先将新增功能用代码实现,调试通过后再分别封装起来。
第二弹结尾我们总结了几个待解决的问题,其中最关键的两个是:
- 输入校验不完整:玩家输入字母(如
a)时,scanf读取失败,残留字符留在缓冲区,导致死循环。 - 缺少区间提示:只提示“太大”或“太小”,玩家无法有效缩小猜测范围。
本弹我们就先解决这两个问题,让游戏真正好用起来。
一、输入校验:让程序不再死循环
1. 问题复现
在原代码中,所有输入都直接使用scanf("%d", &var);。如果玩家输入字母,scanf会返回 0,并且字母会留在输入缓冲区中,下一次循环仍然读取到同一个字母,于是无限循环打印错误提示。
2. 原理分析
scanf的返回值是成功读取并赋值的项数,而不是读取到的值本身。
- 成功读取一个整数,返回
1; - 遇到字母、符号等无法转换为整数的字符,返回
0; - 遇到文件结束符(EOF),返回
-1。
当我们从键盘输入内容时,数据先进入输入缓冲区,然后scanf再从缓冲区中按格式读取。以输入a后按回车为例,缓冲区里是'a' '\n'。scanf尝试把a解释为整数失败,返回 0,但没有从缓冲区中取走a。进入下一次循环时,scanf又看到缓冲区里的a,再次失败,如此反复,程序陷入死循环。
3. 解决方案
核心思路有两条:
- 检查
scanf的返回值,判断本次读取是否成功; - 清空输入缓冲区,把残留的非法字符扔掉。
完整处理模板:
if(scanf("%d",&var)!=1){printf("输入无效,请输入数字!\n");while(getchar()!='\n');// 清空缓冲区continue;// 重新输入}注意:while (getchar() != '\n');最好写成同时检查 EOF,更安全:
intc;while((c=getchar())!='\n'&&c!=EOF);我们可以将清空缓冲区的逻辑封装成一个函数:
voidclear_buffer(){intc;while((c=getchar())!='\n'&&c!=EOF);}4. 改造位置
(1)main()中读取菜单选项
do{menu();if(scanf("%d",&input)!=1){printf("输入无效,请输入数字!\n");clear_buffer();input=-1;// 设为非0值,保证循环继续(不能设为0,否则会退出循环)continue;}switch(input){...}}while(input);(2)DifficultySelection()中读取难度
while(1){if(scanf("%d",&select)!=1){printf("输入无效,请输入数字!\n");clear_buffer();selectmenu();// 重新显示难度菜单continue;}switch(select){...}}建议把selectmenu();直接放在do-while循环内部,这样每次循环都会重新显示菜单,代码更简洁。
(3)DifficultySelection()中读取自定义次数
case4:while(1){printf("请输入允许的猜测次数:\n");if(scanf("%d",&count)!=1){printf("输入无效,请输入数字!\n");clear_buffer();continue;}// 检查是否有多余字符(如小数点)intc=getchar();if(c!='\n'&&c!=EOF){printf("输入无效,请输入整数!\n");clear_buffer();continue;}if(count<=0){printf("次数必须为正整数,请重新输入。\n");continue;}returncount;}(4)game()中读取猜测数字
printf("请输入你猜测的数字:\n");if(scanf("%d",&guess)!=1){printf("输入无效,请输入数字!\n");clear_buffer();continue;// 不计入次数}attempts++;// 输入合法后才计次二、区间提示:让玩家知道范围
1. 思路
维护两个变量low和high,表示当前目标数字可能的范围。初始为1和100。每次猜测后:
- 如果猜大了,说明目标数字小于
guess,更新high = guess - 1; - 如果猜小了,说明目标数字大于
guess,更新low = guess + 1; - 提示玩家当前有效范围。
同时,如果玩家输入的数字不在当前有效范围内,提示重新输入,且不计次数。
2. 代码实现
intlow=1,high=100;// 初始范围// 检查是否在有效范围内if(guess<low||guess>high){printf("您输入的数字不在当前有效范围内,请重新输入。\n");continue;// 不计次数}attempts++;remaining=count-attempts;used_time=(double)(clock()-start)/CLOCKS_PER_SEC;if(guess>target){high=guess-1;printf("抱歉,你猜的数字太大了。\n");printf("已用次数:%d\n",attempts);printf("剩余次数:%d\n",remaining);printf("当前有效范围:%d ~ %d\n",low,high);printf("用时:%.2f秒\n",used_time);}elseif(guess<target){low=guess+1;printf("抱歉,你猜的数字太小了。\n");printf("已用次数:%d\n",attempts);printf("剩余次数:%d\n",remaining);printf("当前有效范围:%d ~ %d\n",low,high);printf("用时:%.2f秒\n",used_time);}else{printf("恭喜你,猜对了!\n");printf("已用次数:%d\n",attempts);printf("用时:%.2f秒\n",used_time);break;}三、记录最佳成绩(单次运行)
1. 思路
在main()中定义best_attempts和best_time,初始化为极大值。每局结束后,如果猜对且成绩更好,则更新并提示。
2. 实现方式
为了在game()中更新这些变量,简单起见可以使用全局变量:
intbest_attempts=9999;doublebest_time=9999.0;然后在game()猜对时:
if(attempts<best_attempts){best_attempts=attempts;best_time=used_time;printf("🎉 新纪录!最少次数:%d\n",best_attempts);}每次开始游戏前,可以在menu()中或main()中显示历史最佳:
printf("历史最佳:%d次 / %.0f秒\n",best_attempts,best_time);注意:全局变量虽然简单,但会污染命名空间。更规范的做法是用指针参数或结构体,我们会在后续文章中介绍。
四、让难度梯度更明显
1. 思路
目前所有难度都是 1~100,只是次数不同。我们可以让不同难度对应不同的数字范围:
| 难度 | 数字范围 | 次数上限 |
|---|---|---|
| 简单 | 1 ~ 50 | 15 次 |
| 普通 | 1 ~ 100 | 10 次 |
| 困难 | 1 ~ 200 | 5 次 |
| 自定义 | 玩家自己定 | 玩家自己定 |
2. 实现方式:全局变量
定义全局变量max_range,在难度选择中修改它:
intmax_range=100;// 默认值intDifficultySelection(){intselect=0;intcount=0;selectmenu();while(1){if(scanf("%d",&select)!=1){printf("输入无效,请输入数字!\n");clear_buffer();selectmenu();continue;}switch(select){case1:max_range=50;return15;case2:max_range=100;return10;case3:max_range=200;return5;case4:// 自定义模式:让玩家输入次数和范围// ...(输入校验略)returncount;default:printf("输入错误,请重新输入。\n");break;}}}在game()中:
intcount=DifficultySelection();inttarget=rand()%max_range+1;// 使用全局变量这样,难度梯度就真正实现了:范围越大,猜的空间越大,越难猜中,而次数的减少进一步提升了挑战性。
五、将项目拆分成多个文件
到目前,代码已经超过 100 行,全部堆在一个文件中不利于维护和扩展。我们可以根据功能拆分成:
game.h:函数声明、宏定义game.c:游戏逻辑实现main.c:主函数,只负责调用
示例:
// game.h#ifndefGAME_H#defineGAME_Hvoidmenu();voidselectmenu();intDifficultySelection();voidgame();#endif// game.c#include"game.h"#include<stdio.h>#include<time.h>#include<stdlib.h>// ... 函数实现 ...// main.c#include"game.h"intmain(){// ... 调用 ...return0;}多文件编程能让代码结构更清晰,也便于团队协作。由于篇幅限制,这里只给出框架,完整代码可到 Gitee 仓库查看。
六、小结与下一步
通过本弹的修改,我们解决了:
- 输入字母导致死循环的问题;
- 缺少区间提示的问题;
- 单次运行内记录最佳成绩;
- 难度梯度不明显的问题;
- 初步引入了多文件编程。
但仍有一些可以继续优化的地方:
- 最佳成绩只在单次运行内有效,关掉程序就丢失了(需要文件操作持久化);
- 输入校验还可以更健壮,比如处理小数、超长输入等;
- 多文件编程只给出了框架,尚未完整实现;
- 代码中仍有重复的清空缓冲区代码,可以进一步封装。
这些我们会在下一弹中继续完善。
📦源码获取与专栏说明
为了方便大家对比各个阶段的代码实现,学长把每一阶段的代码都整理并传到了 Gitee 仓库。
👉 Gitee 仓库地址:点击跳转
遇到任何问题,或者代码有可以优化的地方,欢迎在仓库提 Issue 哦!如果觉得有帮助,点个 Star 鼓励一下学长吧~
下一篇预告:
我们将把游戏成绩保存到文件,让最佳纪录持久化;同时完善多文件编程,并进一步优化输入校验和代码结构。下一篇《C语言项目实战第四弹:成绩持久化与多文件编程,打造更专业的猜数字游戏》,敬请期待!