☰
C语言项目实战第三弹:输入校验与区间提示,完善你的猜数字游戏
2026/10/12 4:43:52 网站建设 项目流程

输入字母就死循环?加上输入校验和区间提示,才算真正好用的猜数字游戏

在前两弹中,我们基于游戏的核心逻辑,初步实现了游戏的基础版;接着,我们通过函数的调用与封装,先后完成了游戏功能的扩展和程序结构的优化。但到目前为止,仍有一些功能尚未实现,代码中也还存在漏洞,这些问题都可能导致程序异常甚至崩溃。接下来,我们将针对这些漏洞对代码进行修正或重构;同时,我们会先将新增功能用代码实现,调试通过后再分别封装起来。

第二弹结尾我们总结了几个待解决的问题,其中最关键的两个是:

  1. 输入校验不完整:玩家输入字母(如a)时,scanf读取失败,残留字符留在缓冲区,导致死循环。
  2. 缺少区间提示:只提示“太大”或“太小”,玩家无法有效缩小猜测范围。

本弹我们就先解决这两个问题,让游戏真正好用起来。


一、输入校验:让程序不再死循环

1. 问题复现

在原代码中,所有输入都直接使用scanf("%d", &var);。如果玩家输入字母,scanf会返回 0,并且字母会留在输入缓冲区中,下一次循环仍然读取到同一个字母,于是无限循环打印错误提示。

2. 原理分析

scanf的返回值是成功读取并赋值的项数,而不是读取到的值本身。

  • 成功读取一个整数,返回1;
  • 遇到字母、符号等无法转换为整数的字符,返回0;
  • 遇到文件结束符(EOF),返回-1。

当我们从键盘输入内容时,数据先进入输入缓冲区,然后scanf再从缓冲区中按格式读取。以输入a后按回车为例,缓冲区里是'a' '\n'。scanf尝试把a解释为整数失败,返回 0,但没有从缓冲区中取走a。进入下一次循环时,scanf又看到缓冲区里的a,再次失败,如此反复,程序陷入死循环。

3. 解决方案

核心思路有两条:

  1. 检查scanf的返回值,判断本次读取是否成功;
  2. 清空输入缓冲区,把残留的非法字符扔掉。

完整处理模板:

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 ~ 5015 次
普通1 ~ 10010 次
困难1 ~ 2005 次
自定义玩家自己定玩家自己定
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语言项目实战第四弹:成绩持久化与多文件编程,打造更专业的猜数字游戏》,敬请期待!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询