大家好,很高兴又和大家见面了。经过前面两个篇章的习题演练,我相信大家的编码能力都是有所提升的,今天我们将来编写咱们的第一个游戏——猜数字游戏。本篇章内容涉及知识点会比较多,建议大家反复观看,确保自己能够完全消化这些内容。
猜数字游戏
功能要求:
1.用户可以决定是开始游戏还是退出游戏;
2.开始游戏后,程序在运行时会生成一个随机数;
3.游戏运行的过程中,用户可以猜数字,系统会给出相应的提示;
4.游戏可以反复运行。
看到这些条件,是不是感觉很头疼啊,既然他们加在一起很困难,那我们就逐个击破。接下来,按照要求开始进行编码:
1.用户可以决定是开始游戏还是退出游戏
第一个功能,用户可以决定开始还是退出,我们来换一种说法——用户如果选择……就开始游戏,如果选择……就退出游戏。通过这种表述,思路是不是就很清晰了,这是在让我们编写一个选择语句——if语句或者switch语句。仅仅知道这条信息是远远不够的,所以在编写前,我们先思考几个问题:
(1)用户通过什么样的方式去选择?
选择的方式有输入和点击两种方式,因为咱们现在学的是C语言,目前我们只学习了一种方式——输入,这里我们首先就是想到的是输入函数。我们现在接触到了哪些输入函数呢?第一个输入函数scanf函数——输入字符后通过换行进行确认;之后我们还在探讨while语句时接触了第二个输入函数getchar——读取字符缓冲区的一个字符。接下来我们来探讨一下这两个函数的区别。我们在探讨前要先弄明白什么是字符缓冲区?这里我们可以理解为就是程序运行后的输入界面,这个输入界面就好比一个生成的空间,我们在界面输入的内容都会储存到这个空间里面,scanf函数会在这个空间里读取空格或者是回车前的所有内容,而getchar函数只能读取这个空间内的一个内容,这个内容包括空格和换行:
这个光标所在的地方就是输入界面,这时计算机内部会开辟一个空间来存储这个界面里输入的全部内容;
我们在界面输入1234空格abcd回车后,经过打印可以知道,scanf读取了空格前的字符1234,到getchar时则读取了空格这个字符;
通过这个代码我们可以更加直观的看到getchar是怎么运作的了,一个getchar,它会主动在缓冲区提取一个字符,这个字符包含空格与回车,在scanf提取完1234后,后面还有6个字符,分别是空格、a、b、c、d、回车,这里的6个getchar会依次提取这些字符。我们通过这个测试能够得到结论:
输入函数scanf提取的是输入缓冲区里空格前的所有字符,而输入函数getchar是提取输入缓冲区里的一个字符。
(2)用户选择什么内容?
知道了这两个输入函数的用法后,接下来我们要解决的是用户选择什么内容?也就是说用户需要在界面输入的内容我们是不是应该给个提示啊,就像玩游戏时一样,你现在要玩什么游戏玩什么模式,要不要开始,我们都可以在屏幕上获得这些信息,也就是说,我们需要给用户提供一个可以选择的界面,也就是菜单栏。有了思路之后,我们开始编写第一个功能,菜单栏以及选择功能:
到这里我们的第一个功能就做好了,这里输入函数和选择语句选择其中一个就可以了,这里我将scanf函数与switch语句的代码放在下面供大家参考:
//猜数字游戏
//功能一:菜单与选择
void menu()
{
printf("****************************\n");
printf("***1.开始游戏 2.退出游戏 ***\n");
printf("****************************\n");
}
int main()
{
menu();//菜单界面;
printf("请输入:>(1,2)");//提示输入页面;
int ch = 0;//定义变量存放输入内容;
scanf("%d", &ch);//scanf输入函数;
//switch语句
switch (ch)
{
case 1:
printf("开始游戏\n");
game();//引用函数,完成游戏;
break;
case 2:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
return 0;
}
下面我们试运行一下看看:
很完美,那第一个功能解决了,我们接下来解决第二个功能;
2.开始游戏后,程序在运行时会生成一个随机数
第二个功能生成一个随机数,这个内容好像在我们目前所学的知识里并没有涉及到,接下来我们要重点探讨一下这个内容了。
C语言中为了帮助我们生成随机数,提供了一个库函数——rand——生成随机数函数,使用这个库函数需要引用头文件<stdlib.h>,知道这个函数之后,咱们来使用一下:
我们可以看到,成功生成了一个随机数,那现在问题来了,这个随机生成的随机数它的数值有多大呢?下面我们来通过软件MSDN来学习一下这个库函数:
这里我们可以看到几个关键信息:(1)rand返回的范围是0—RAND_MAX;(2)rand返回的是一个伪随机整数;(3)在标定也就是调用rand函数之前,需要使用srand函数为rand函数设置伪随机数生成器。看到这三个信息,我有以下几个问题:
(1)RAND_MAX的值是多少?
这个RAND_MAX是什么?我们继续查阅一下MSDN:
现在我们知道了,原来RAND_MAX是一个常数,它的值为0X7fff。那这个0X7fff又是多大呢?我们要先知道0x开头的数值是16进制的数值,它的大小是7fff,接下来我们借助计算器来换算一下:
通过计算器我们得知了RAND_MAX的十进制的值是32767。接下来我们看下一个问题。
(2)什么是伪随机数?
伪随机数的意思是不是代表它并不是随机的?我们再来运行一下:
这里为了区分上一次测试的内容,我在打印界面加入了随机数三个字,但是打印结果大家可以看到,和第一次的一模一样,也就是说虽然是随机生成的,但是它也并不是真正的随机。怎么解决这个问题呢?接下来我们看一下第三个问题:
(3)srand函数是什么?该如何使用?
srand函数又是一个陌生的函数,我们继续查阅MSDN来了解它:
看着感觉很难理解对吧!这里咱们就简单点把这个srand理解为初始化rand函数的一个函数,在前面的测试中我们看到了,rand函数在开始运行时,它的值是固定不变的——41,那我们在调用rand函数之前如果调用srand函数的话,那rand运行时的值就会随机产生。那srand又是如何使用的呢?我们接着查阅MSDN:
从软件中举的例子我们可以看到它在调用的时候使用了一个函数——time,这个函数又是什么呢?我们接着查找:
从这张图中我们可以知道几个信息,一是time函数是一个时间函数,它的作用就是获取系统时间,二是在使用time函数时我们需要引用头文件<time.h>。这个时间函数具体有什么作用呢?这里我们可以简单的理解为:time——时间函数通过获取系统时间之后,将这个时间与1970.1.1(00:00:00)相减,得到的值为从1970年那时其到现在经历的秒数,然后将这个数值存储起来,如果参数为NULL,则不存储返回值。下面我们要介绍一个新的知识点——时间戳,什么是时间戳呢?
所谓的时间戳就是:当前计算机的时间与计算机的起始时间之间的差值,单位为秒,计算机的起始时间就是1970.1.1.0:0:0。这么一看是不是就跟时间函数的介绍一样啊,所以这个时间函数也被称为时间戳。这个函数的数据类型我们通过介绍可以知道是time_t
那time_t又是什么类型呢?我们借助VS来看一下:
不知道大家还记不记得关键字typedef——数据类型重命名,这里我们可以看到在使用32位系统时,它是将__time32_t重命名后的一个数据类型,我们现在使用的是64位系统,所以它这里是将__time64_t重命名的一个数据类型,那这个__time64_t又是什么数据类型呢?我们接着查找:
这里我们可以看到32位时它是long,64位时它是int,也就是说它本身是一个整型数据类型。这里我们再回过来看一下srand的使用方式,srand((unsigned)time(NULL))这行代码怎么理解呢?不知道大家还记不记得操作符(数据类型)——强制转换数据类型的操作符。这行代码是不是就用到了呀,time的数据类型我们现在知道了,要么是int要么是long,也就是有符号的整型,这里的整型是有正有负的,我们现在通过强制转换数据类型将有符号整型(signed int)转换成了无符号整型(unsigned int),使time的值永远为正。后面这个NULL是来干什么的呢?我们在看到time的介绍,里面提到了,如果参数为NULL则不存储返回值。也就是说这里我们在srand里使用time时这个返回值我们并不需要存储起来。这里就奇怪了,为什么在srand介绍里说要使用1作为参数呢?下面我们来测试一下:
诶!这不还是41吗?也没随机呀!那我们使用2/3/4分别测试一下:
通过这个测试我们就可以发现,当srand里的参数发生变化时,rand的初始值也会发生变化,那我们如果在里面放入一个会变化的参数,是不是就能解决我们的问题了呢?下面我们再思考一下time函数在这里的作用,time是获取系统时间,我们的系统时间是不是一直在变化呀,啊!突然就对应上了,原来使用time是为了srand一个随时变化的值,那为什么是无符号的整型呢?我们回过头来看一下srand的介绍图片:
从这里我们可以看到,使用srand函数是,srand的参数必须是无符号整型。最后为什么time的参数是NULL呢?这里我们可以简单的理解为我们只是需要一个会变化的无符号整型值,这个值是多少并不重要,所以我们并不需要将这个值给存储起来,因此才会选用NULL作为time函数的参数。到这里我相信大家都能理解了吧,下面我们来测试一下:
这里我们可以看到,这个值确实以经开始变化了那说明咱们的功能二也完成了呀,但是有个问题,数字是从0—32767,那怎么去猜呀,如果它是0-100是不是就更加合理一点了,接下来我们怎么操作呢?有朋友就提到了跟100取模,为什么呀?这里我们举个例子:32756/100=327……56、13402/100=134……02。有没有发现,如果跟100取模的话那这个余数是不是在0-99之间呀。那我们要到100是不是可以取模后+1就行了,接下来我们将代码改写一下:
现在功能2是不是也完成了呀,接下来我们来看看功能三:
3.游戏运行的过程中,用户可以猜数字,系统会给出相应的提示
这个功能相比于功能二是不是就简单很多了,我们只需要加个条件语句是不是就可以了,这里因为值是在变化的,判断条件是个范围,所以我们使用if语句:
现在我们的游戏雏形已经完成了,接下来我们继续完善功能四;
4.游戏可以反复运行
说道游戏反复运行,那肯定是需要循环语句,既然是游戏整体反复运行,那我们的循环语句就需要加在主函数里面:
现在我们可以看到,游戏确实正常运行了。像这样代码已经没问题了,但是我们把功能三先屏蔽掉,来进行测试:
这里我们可以看到,如果没有功能三的话,这里的随机数生成其实也没有那么随机,那我们应该怎么调整呢?首先我们要明确为什么会出现这种情况,这里我们要分析一下srand它的作用是,是生成随机起点,也就是说我们现在通过时间戳生成的数都只是随机的起点而已,为什么会这样呢?因为此时srand是在循环内部,我每进行一次循环,它就会从新启动一次,那是不是我只要让它只启动一次就OK了呢?下面我们修改一下代码:
现在我们可以看到此时生成的数值就没有规律可言了,它真正做到了随机。到这一步咱们的这个游戏才算完成了。这里有朋友就会说了,刚刚在测试的时候也看到了数值并没有规律呀,会出现这种情况的原因其实是因为我们每完成一次游戏的时间是不可把控的,有时长,有时短,但是我们在屏蔽掉功能三后,我们会发现在时间间隔很短的情况下,它产生的数就不是随机的了,这样解释,大家应该都能理解吧。下面附上咱们的完整代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <stdlib.h>
#include <time.h>
//猜数字游戏
//功能一菜单页面
void menu()
{
printf("******\n");
printf("1.开始游戏 2.退出游戏 \n");
printf("\n");
}
//功能二创建随机数
void game()
{
int rn = rand() % 100 + 1;//这里定义变量rn——random number随机数,来存储函数rand产生的值;
//跟100取模为了缩小随机数的范围,将0-32767缩小到0-99,+1是为了让范围变成0-100;
//功能三游戏运行并给出提示;
int guess = 0;//定义变量接收用户猜测数字;
while (1)//循环语句为了让用户能一直猜,直到猜对为止;
{
printf("请输入猜测数字(0-100)>:");//游戏提示;
scanf("%d", &guess);//输入猜测数字;
//输入完开始进入判断;
if (guess < rn)
{
printf("猜小了\n");
}
else if (guess > rn)
{
printf("猜大了\n");
}
else
{
printf("猜对了\n");
printf("随机数为:%d\n", rn);
break;//猜对后游戏停止,跳出循环;
}
}
}
int main()
{
srand((unsigned int)time(NULL));
//这里使用unsigned int是因为如果是32位操作系统,time的数据类型是long,有符号的长整型;
//为了避免更换操作系统而出现报错,所以直接强制转换成unsigned int
//功能四——重复多次玩游戏;
do
{
menu();//菜单界面;
printf("请输入(1,2)>:");//提示输入页面;
int ch = 0;//定义变量存放输入内容;
//输入函数
scanf("%d", &ch);//scanf输入函数;
//switch语句
switch (ch)
{
case 1:
printf("开始游戏\n");
game();//引用函数,完成游戏;
break;
case 2:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while(1);//此时的循环判别我们只需要通过循环内部的break终止循环就行,循环只需要在未终止前能一直运行就可以了
return 0;
}
结语
到这里咱们今天的内容就全部结束了,我希望通过这篇内容能够帮助大家更好的理解猜数字游戏的编码逻辑,如果这篇内容对你有帮助的话,还请支持一下博主来个关注、点赞、分享三连招!!接下来随着学习的深入,我会继续给大家分享我在学习过程中的感受,感谢大家的翻阅,咱们下一篇再见。