1.4x1.1x1.8猜数字的游戏游戏

设计一个猜数字的游戏的游戏系统随机生成一个1~100之间的整数,每猜一次系统就会提示玩家该数字是偏大还是偏小如果猜中了,则告知玩家并提前结束游戏

用面向对潒的编程方式来实现,把问题抽象化这个游戏有3个属性,分别是随机数的取值范围min和max,目标值 target还有一个方法用于如何猜游戏,随着需求的变化我们可以不断地加其他属性和方法。需要用到的知识点:

  • 导入random包:生成随机数

本章中要编写的是“猜数游戏”程序我们先来做一个测试版本,这个测试版的程序只能比较玩家输入的数值和计算机准备的数值之后再逐渐为其追加其他功能。

本章Φ会编写一个“猜数游戏”的程序首先我们要做的是一个测试版本,用来显示玩家从键盘输入的值和计算机事先准备好的“目标数字”嘚比较结果

通过if 语句实现条件分支

List 1-1 所示的程序是测试版的“猜数游戏”。

先运行程序因为程序提示输入0~9 的数值,所以我们就在键盘仩键入数值这样一来,程序就会把键入的数值和“目标数字”进行比较并显示出比较后的结果。

本游戏中的“目标数字”是7用变量ans 表示,从键盘输入的值则用变量no 表示

程序通过阴影部分的if 语句来判断no 和ans 两个变量值的大小关系,然后如Fig.1-1 所示根据判断结果显示“再小┅点。”“再大一点”“回答正确。”

输出的字符串中包含两种转义字符。一个是我们很熟悉的\n表示换行;另一个是\a,表示警报茬大多数环境下,一旦输出警报就会响起蜂鸣音因此本书在运行示例中采用符号表示警报。

▲关于转义字符我们会在第2 章中详细介绍。

下面让我们来了解一下比较no 和ans 这两个变量值的if语句的结构

if 语句是通过对名为控制表达式的表达式进行求值(专栏1-1),再根据求值结果紦程序流程分为不同的分支的语句它包含两种语句结构,如右图所示

▲() 中的表达式为控制表达式。

本程序中的if 语句采用以下形式

当嘫,这里并不是为了把程序流程分成三个分支才特意采用这种语句结构的从字面意思 可知,if 语句是一种语句因此else 控制的语句也可以是if 語句。如Fig.1-2 所示程序采 用了在if 语句中嵌套if 语句的结构。

为了跟本程序的if 语句1 达到同样效果笔者编写了2 和3 中的if 语句。

下面让我们一起来比較并讨论一下这三个程序以加深对if 语句的理解。

最末尾的else 语句后面追加了阴影部分的内容只有当两个判断(no > ans) 和(no < ans) 都不成立,也就是no 和ans 相等時程序才会运行这部分。

在阴影部分进行的判断是肯定会成立的条件

这里有三个if 语句并列。无论变量no 和ans 之间的大小关系如何程序都會进行这三个条件判断。

笔者根据变量no 和ans 的大小关系对这三个程序中都进行了什么判断(对哪个控制表达式进行了求值)进行了总结,洳Table 1-1 所示

Table 1-1 三个程序所进行的判断

比如,我们假设no 大于ans则程序1 和程序2 都只会进行①的判断,即(no > ans)

▲因为如果no 大于ans,那么在执行完printf("\a 再小一點\n") 后,整个if 语句就执行完毕了 而在程序3 这种有三个独立的if 语句并列的情况下,则会执行三次判断即①的(no >ans)、②的(no < ans),还有③的(no == ans)这种实現方法效率最低。

不管在何种条件下判断次数最少的都是程序1 的if 语句。

程序1 的if 语句的优点不光只有判断次数少而已为了让大家理解这┅点,这里通过Fig.1-3 来说明

图a 的if 语句跟程序1 中的if 语句结构相同,程序流程都分为三个分支执行的不是“处 理A”就是“处理B”,再不然就是“处理C”

▲不会出现没有执行任何处理或者执行了两项和三项处理的情况。

图b 的if 语句是根据变量x 的值进行分支的

看上去程序似乎执行叻“处理X”“处理Y”“处理Z”三者中的一项处理,然而变量x 的值如果不是1、2、3那么程序就不会进行任何处理。

如Fig.1-4 所示程序流程实质上汾为四个分支。

如此图b 与图a 的if 语句的结构完全不同,因此不能省略最后的判断if(x == 3)

▲如果省略了,那么即使x 的值不是3而是4 或5 等,“处理Z”也会被运行

程序1 的if 语句的结构如图a ,在末尾的else 语句后面是没有if 语句的因此一看就明白不存在更多分支。

就程序的易读性而言程序1 吔要优于程序2 ,因为程序2 在末尾的else 语句后放了个“多余”的判断

▲如果一定要对程序的读者强调“当no 等于ans 时这么做”,则可以按照程序2 那样来实现 通常,编译器的优化技术会内部删除程序2 的这种“多余”的判断因此我们没必要太在意效率问题。

1-2 重复到猜对为止

如果“猜数游戏”只允许玩家输入一次数值那未免太无趣了。我们把程序改良一下让玩家可以一直重复输入直到猜对为止。

如果玩家只能输叺一次数值那么想要猜对数值的话,就需要不停地重启程序直到猜对为止这样一来不只是没意思,还麻烦得要命

下面我们把程序改良一下,让玩家能够反复输入数值直到猜对为止改良后的程序如List 1-2所示。

List 1-2 中删除了List 1-1 中if 语句的后半截并在此基础上追加了阴影部分的do 语句。

do 语句是通过先循环后判断(后述)重复进行处理的语句其结构如右图所示。

▲和之前学习的if 语句以及接下来要学习的while 语句和for 语句等語句的结构不同,do 语句 的末尾带有分号“;”

do 和while 围起来的语句叫作循环体。只要() 中的表达式也就是控制表达式的求值结果不为0,那么循環体就会被一直重复运行下去直到控制表达式的求值结果为0,才会结束重复运行

下面参照Fig.1-5 来理解如何通过本程序的do 语句实现循环。

运算符!= 对左边和右边的操作数的值是否不相等这一条件进行判断如果这个条件成立, 程序就会生成int 型的1不成立则会生成int 型的0。

如果读取嘚值no 和目标数字ans 不相等那么对控制表达式no != ans 进行求值,得到的值就是1因此需要通过do 语句来重复运行程序,再次运行用{} 括起来的代码块吔就是循环体。

当程序读取到的no 和目标数字ans 是同一个值时控制表达式的求值结果就是0,循环就结束了此时画面显示“回答正确。”程序运行结束。

相等运算符和关系运算符

▲int 型的1 表示“真”0 表示“假”(专栏1-2)。

相等运算符“==”“!=”

判断两个操作数是否相等

判断两個操作数的大小关系

除了do 语句外,C 语言的循环语句 还有while 语句和for 语句

我们用先判断后循环的while 语句试着写出之前的程序,如List 1-3 所示

while 语句嘚结构如右图所示。

只要控制表达式的求值结果不为0那么作为循环体的语句就会永远重复运行下去。但是求值结果一旦为0就不再循环叻。

因为本程序的while 语句的控制表达式是1所以循环会永远进行下去。这样的循环一般称为“无限循环”

① 声明一组要反复执行的命令,直到满足某些条件为止——译者注

一直重复的话,程序会永无止境为了强制跳出循环语句,我们在本程序中使用了break 语句 因为break 语句昰在no 和ans 相等时运行的,所以通过while 语句进行的循环会被强制中断

▲使用break 语句的程序往往不容易读也不容易理解,我们只在“某个特殊的条件成立时因为某种原因想强制结束循环语句”的情况下使用break 语句就好。这里所举的“猜数游戏”的循环 结构很简单因此实现这个程序鈈需要用到break 语句,像List 1-2 那样通过do 语句(不使用break 语句)就能实现

很难看出程序中的while 是属于do 语句的一部分还是属于while 语句的一部分,下面我们结匼右图中所示的程序来思考一下

首先把0赋给变量x,然后通过do 语句对变量x 的值进行增量操作直到x 等于5 为止。

接下来在while 语句中对x 的值进荇减量操作,并显示其结果

▲关于增量(increment)运算符++ 和减量(decrement)运算符--,在1-4 节中会详细为大家讲解

如右图所示,我们把{} 括起来的代码块當作do 语句的循环体

这样一来,只要看每一行的开头就能区分while 属于哪一部分了

while:如果开头是while,则属于while 语句的开头部分
}while:如果开头是},則属于do 语句的结尾部分

不管是do 语句还是while 语句抑或是for 语句,只要其循环体是单一的语句那么就没 必要特意导入代码块。

话虽如此对do 语呴来说,其循环体如果是单一的语句导入代码块则会增加程序的易读性。

先判断后循环和先循环后判断

根据何时判断是否继续处理循環可以分为两种。

先判断后循环(while 语句和for 语句)

在进行处理前先判断是否要继续处理。会出现循环体一次也没有运行的情况

先循环后判断(do 语句)

在进行处理后,再判断是否要继续处理循环体至少会被运行一次。

1-3 随机设定目标数字

在前面的“猜数游戏”中“目标数芓”都是事先在程序里设置好的,所以我们事先是知道***的为了提升游戏的趣味性,我们来让这个值自动变化

rand 函数:生成随机数

为叻每次游戏时都能改变“目标数字”,我们需要一个随机数用于生成随机数的就是rand 函数,如下所示

这个函数生成的随机数是int 型的整数。在所有编程环境中其最小值都为0但最大值则 取决于编程环境,所以我们用<stdlib.h> 头文件将其定义成一个名为RAND_MAX 的对象宏 (object-like macro)其定义的示例如丅所示。

下面我们来尝试实际生成并显示随机数请运行List 1-4 所示的程序。

首先显示的是能够生成的随机数的“范围”最小值是0,最大值是RAND_MAX 嘚值(值取决于编程环境)

然后显示的是rand() 返回的随机数值,当然这个值在0 ~ RAND_MAX 的范围内

对于“再运行一次?”的问题如果选择了“是”,那么就能重复生成并显示随机数

请多运行几次程序,结果如Fig.1-7 所示总会生成一个相同的随机数序列。这很令人费解rand 函数生成的值真嘚是随机的吗?

srand 函数:设置用于生成随机数的种子

rand 函数是对一个叫作“种子”的基准值加以运算来生成随机数的之所以先前每次运行程序都会生成同一个随机数序列,是因为rand 函数的默认种子是常量1要生成不同的随机数 序列,就必须改变种子的值

负责执行这项任务的就昰srand 函数,如下所示

比如,假设我们调用了srand(50)这样一来,之后调用的rand 函数就会利用设定的新种子值50来生成随机数

Fig.1-8 所示为在某个编程环境Φ生成的随机数序列的示例。

当种子值为1 时在最初调用rand 函数时生成的是41,再调用时生成18467接下来是6334……

如果种子值是50,则会依次生成201、20851、6334……

如上图所示一旦决定了种子的值,之后生成的随机数序列也就确定了因此如果想要每次运行程序时都能生成不同的随机数序列,就必须把种子值本身从常量变成随机数

然而,为了生成随机数而需要随机数这本身很矛盾。

▲rand 函数生成的是叫作伪随机数的随机数伪随机数看起来像随机数,却是基于某种规律生成的

因为能预测接下来会生成什么数值,所以才叫作伪随机数真正的随机数是无法預测接下来会生成什么数值的。

我们一般使用的方法是把运行程序时的时间当作种子List 1-5 的程序中就使用了这个方法。

请运行一下程序如Fig.1-9 所示,每次启动都会生成不同的随机数序列

▲关于获取当前时间所使用的time 函数,我们会在第6 章详细学习在此之前,只需把程序中的阴影部分当成是固定的一部分即可(#include

rand 函数生成的值范围是0 ~ RAND_MAX,话虽如此但我们需要的随机数不会每次都恰好在这个范围內。

一般情况下我们需要的是某个特定范围内的随机数。如果我们需要“大于等于0 且小于等于10”的随机数可以像下面这样求出。

这里使用的方法是把非负整数值除以11就得到余项(余数)为0, 1, …, 10。

▲大家注意不要把非负整数值错除以10用10 除得到的余数是0, 1, …, 9,无法生成10

现茬大家已经掌握了如何生成随机数,那么我们就来把猜数游戏中的“目标数字”设定为0 ~ 999 的随机数吧对应的程序如List 1-6 所示。

▲笔者没有以while 語句版本的List 1-3 为基准而只在do 语句版本的List 1-2 的基础上做了一些细微的修改,增加了部分内容

阴影部分把生成的随机数除以1000后得到的余数赋给叻变量ans。

仅仅是把目标数字变成了随机数就大大地提升了猜数游戏的趣味性。大家可以多运行几次感受一下

话说回来,大家知道怎么財能最快猜中吗一开始输入499,然后根据程序的判定结果(是大还是小)再输入749 或者249每次都把范围缩小到一半。

目标数字的范围很容易變更下面举两个具体的例子。

将程序的阴影部分改写成下面这样

把目标数字定为3 位数的整数(100 ~ 999)

将程序的阴影部分改写成下面这样。

呮要不断输入数值终会猜对。为了给玩家以紧张感我们把玩家最多可输入的次数限制 在10 次之内。变更后的程序如List 1-7 所示

变量max_stage 表示玩家朂多可输入的次数,在这里是10 次

另一个新的变量remain 表示还能够输入多少次。当然其初始值是max_stage,也就是 10如Fig.1-10 所示,玩家每次输入数值时嘟会对remain 的值进行减量操作(如10, 9, 8, …), 即在原基础上减去1

当这个值为0时,游戏就结束了因此do 语句的判断不仅包含表达式no != ans,还要 加上阴影蔀分的remain > 0

连接两个表达式的逻辑与运算符&& 只会在两边的操作数都不为0时生成int 型的1,否则 便生成0

因此,不仅当玩家猜中时(图a )循环会结束当玩家输入10 次都没猜中,remain 变成0(图 b )时循环也会结束。

▲关于循环结束的条件和&& 运算符我们会在专栏 1-2 中学到。

此外用max_stage 减去remain 就可鉯知道玩家是在第几次猜中了目标数字。如图a所示游戏结束时的remain 值是7,所以用max_stage 减去remain也就是用10减去7,***为3

▲因为max_stage 的声明中已经指定叻const,所以max_stage 的值无法变更这样一来,如果 把应该写成remain-- 的部分写成了max_stage--就会发生编译错误(防止遗漏)。

如果程序能保存玩家输入的值玩镓就能在游戏结束时确认自己猜的数字距离目标数字有多近(或者有多远)。

下面我们来把程序改良一下令其能保存玩家已输入的数值,并在游戏结束时显示这些数值改良后的程序如List 1-8 所示。

▲程序的运行示例可以在第26 页看到

本程序利用数组(array)来存储已输入的值。数組是一种将同一类型的变量排成一列的数据结构数组内的各个变量就是数组元素。

在声明数组时数组元素的个数必须是常量表达式。吔就是说下面这样的声明会引起编译错误。

因此本程序中没有采用变量max_stage而设了一个对象宏MAX_STAGE,将其声明为1

▲编译初期,要把宏MAX_STAGE(程序嘚3 处灰色阴影部分)替换成10

接下来把存储所输入数值的数组num 声明为2 。如Fig.1-11 所示数组num 的元素类型是int 型,元素个数是10

在数组的声明中,[] 里嘚值是元素个数而[] 里的值是下标(subscript),用于访问(读取)各个元素

首个元素的下标是0,之后的下标逐一递增因此可以用表达式num[0],num[1]…,num[9] 依次访问数组num 的元素由于末尾元素的下标值等于元素个数减1,因此不存在num[10] 这个元素

数组num 的各个元素和一般的(非数组的单独的)int 型对象具有相同的性质,能够赋值和获取值

 本书中将前者用[] 表示,后者用粗体的[] 表示

让我们结合Fig.1-12 来理解如何把玩家输入的值存入数組的元素中。

本程序中新引入的变量是stage这个变量用于代替List 1-7 中表示剩余输入次数的变量remain。游戏开始时其初始值为0之后玩家每输入一个数徝,stage 的值都会逐次递增当值等MAX_STAGE,也就是等于10时游戏结束。

负责把读取的值存入数组的正是图中的阴影部分这里共有三个运算符,即[]、++、=

增量运算符++(也称为递增运算符)包括前置形式的++a 和后置形式的a++ 两种形式。我们先来了解一下它们都有哪些不同之处

前置形式的++a 會在对整个表达式进行求值之前,先对操作数的值进行增量因此当a 的值为3 时,运行以下代码的话a 首先被增量成4,然后程序会把表达式++a 嘚求值结果4 赋给b最终a 和b 都等于4。

后置形式的a++ 会在对整个表达式进行求值之后再对操作数的值进行增量。因此当a 的值为3 时运行以下代碼的话,表达式a++ 的求值结果3 首先被赋给b然后程序会对a 进行增量,增量结果为4最后的结果a 等于4,b 等于3

▲这里所说的前置和后置的求值時间也同样适用于进行减量操作的减量运算符--。

本程序的阴影部分中使用了后置形式的增量运算符下面我们来了解一下玩家输入的值是洳何一个一个地保存到数组元素中的。

▲1-4 节的Fig.1-11 把数组的各个元素纵向排列方框中写有访问各个元素的“表达式”。这次 Fig.1-12 则把各个元素横姠排列方框中写着各个元素的“值”,各个元素的下标是方框上面的小数字

  另外,实心圆符号●中所写的下标值和变量stage 的值是一致嘚

a 玩家输入500,因为变量stage 的值是0所以程序会把500赋给num[0],再把stage 的值增量为1

b 玩家输入250,因为变量stage 的值是1所以程序会把250赋给num[1],再把stage 的值增量為2

通过反复进行上述处理,即可把玩家输入的值按顺序依次存入数组

通过for 语句来显示输入记录

一旦游戏结束程序就会显示出玩家的输叺记录。负责进行这项操作的就是下面这个for 语句

可以像下面这样解释这个for 语句所进行的循环。

首先把i 的值设为0当i 的值小于stage 时,就不断往i 的值上加1以此来让循环体运行stage 次。

猜数游戏的主体do 语句结束时变量stage 的值等于玩家输入数值的次数。如果玩家输入到第7 次就猜对了那么stage 的值就是7。此时通过for 语句循环的次数是7 次

如Fig.1-13 所示,在各个循环中数组num 内元素num[i] 的下标是i。实心圆符号●内的下标和变量i 的值是一致嘚

我们在循环体内通过printf 函数来显示3 个值。

1 表示的是变量i 加上1 之后的值下标是从0开始的,而我们数数是从1 开始的加上1 是为了弥补变量嘚值和显示的值之间的差距。

▲如图c 变量i 的值是2,加上1 后显示结果就是3

2 则会直接显示出玩家输入的值 num[i]。

3 表示的是玩家输入的值和正确***之差如果玩家输入的值大于正确***,就在显示结果中加上符号“+”来表示如果玩家输入的值小于正确***,就在显示结果中加仩符号“-”来表示

▲如图c ,因为num[i] 的值为125减去正确***116,得出差值为9显示结果就是“+9”。 大家都知道(通过平日的积累也应该有所了解)在使用格式字符串"%d" 表示int 型的数 值时,只有当数值为负值时才会在数值前加上符号“-”

一旦将格式字符串设为"%d",那么数值即使是正徝和0 也会带有符号

我们将会在第2 章详细学习printf 函数和格式字符串。

按顺序逐个访问数组内的各个元素就叫作遍历(traverse)这是一个基础术语,还请大家务 必牢记

接下来,for 语句会在变量i 的值小于stage 的期间一直循环因此,for 语句结束时变量i 的值就等于stage而不是stage - 1。

▲把本程序的for 语句妀写成while 语句时的代码如下所示

 循环体会在变量i 的值为0,1…,stage - 1 时运行共运行stage 次。最后调用printf 函数时变量i 的值为stage - 1。当这个值增量后等於stage 时控制表达式 i < stage 不成立,循环结束

我们再来详细学习一下数组。首先是用于初始化的声明

要将元素初始化,需要对应各个元素把初始值按顺序依次排列并用逗号“,”一一隔开再用“{}”把它们括起来。

例如像下面这样一旦进行了声明,元素a[0]、a[1]、a[2]、a[3]、a[4] 就会依次被初始囮为1、2、3、4、5

下面是把所有元素初始化为0的声明。

但是在给出了“{}”形式的初始值的数组声明中,没有被赋予初始值的元素会被初始囮为0因此如果我们像下面这样声明的话,a[1] 之后没有被赋予初始值的所有元素都会被初始化为0这样看上去会更简洁一些。

▲对有静态存儲期(5-3 节)的数组(包括在函数外定义的数组和在函数内加上static 定义的数组)而言即使不赋予该数组初始值,所有的元素也都会被初始化為0

在赋予了初始值的数组的声明中,可以省略元素个数

此时根据初始值的个数,数组a 的元素个数被视为3 个也就是说,上面的声明和丅面的声明是一样的

另外,如果初始值的个数超过了数组的元素个数程序就会报错。

此外初始值{1, 2, 3} 不能作为右侧表达式用于赋值,因此以下赋值会导致程序报错

▲关于初始值我们还会在后面的章节继续学习。

List 1-8 在声明数组以前把该数组的元素个数定义成了宏

在一些不呔适合用宏定义元素个数的情况下,首先要声明数组再求元素个数。

求数组元素个数最常用的方法是使用sizeof 运算符List 1-9 中的程序就采用了这個方法。

int 型的大小根据编程环境的不同而有所不同但通过sizeof(a) / sizeof(a[0])求出的值是数组的元素个数,跟int 型的大小无关例如,如果int 型是2 字节那么sizeof(a) 就昰10,sizeof(a[0]) 就是2因此可求出元素个数等于10 / 2,也就是5此外,如果int 型是4 字节那么通过计算20 / 4,可得到元素个数仍为5

由前文可知,变量na 会被初始囮为数组a 的元素个数5如果把数组a 的声明进行如下变更,那么变量na 就会被初始化为6实际的运行示例也是如此(如右图所示)。

不需要随著初始值的增减去修改程序的其他地方

 各位想象一下,如果因为某种原因要变更元素类型的话那我们该怎么办?假设“因为要存入數组元素中的数值超出了int 型的范围所以需要将元素类型变更成long 型”,在这种情况下就必须把表达式sizeof(a) / sizeof(int) 改成sizeof(a) / sizeof(long)。

建议大家不要满足于读懂本書中的程序还要试着解答下述问题,自己来设计和开发程序锻炼自己的编程能力。

  • 因为是自由演练所以没有***。

编写一个“抽签”的程序生成0~6 的随机数,根据值来显示“大吉”“中吉”“小吉”“吉”“末吉”“凶”“大凶”

把上一练习中的程序加以改良,使求出某些运势的概率与求出其他运势的概率不相等(例如可以把求出“末吉”“凶”“大凶”的概率减小)

编写一个“猜数游戏”,让目标数字是一个在-999 和999 之间的整数

同时还需思考应该把玩家最多可输入的次数定在多少合适。

编写一个“猜数游戏”让目标数字是一個在3 和999 之间的3 的倍数(例如3, 6, 9, …, 999)。编写以下两种功能:一种是当输入的值不是3 的倍数时游戏立即结束;另一种是当输入的值不是3 的倍数時,不显示目标数字和输入的数值的比较结果直接让玩家再次输入新的数值(不作为输入次数计数)。

同时还需思考应该把玩家最多可輸入的次数定在多少合适

编写一个“猜数游戏”,不事先决定目标数字的范围而是在运行程序时才用随机数决定目标数字。打个比方如果生成的两个随机数是23 和8124,那么玩家就需要猜一个在23 和8124之间的数字

另外,根据目标数字的范围自动(根据程序内部的计算)选定一個合适的值作为玩家最多可输入的次数。

编写一个“猜数游戏”让玩家能在游戏开始时选择难度等级,比如像下面这样

使用List 1-8 的程序時,即使玩家所猜数字的游戏和正确***的差值是0输入记录的显示结果也会带有符号,这样不太好看请大家改进一下程序,让差值0不帶符号

每次如果输入的数不对可给出夶小提示。如果猜正确给出恭喜信息,游戏结束;如果十次猜数不正确游戏结束,给出失败信息判断游戏者所猜数与所产生随机数の间关系的功能由函数/usercenter?uid=f">神QQ

不过楼主的python经验少了, 如果上面输入的不是数字 那么程序会报错的, 别有用心的人就能知道程序是如何编写的叻 这样不好啊

以后遇到python方面的问题, 可以帮忙搞定

这是老师布置的作业,我写的不好请问你可以这个程序我借鉴一下吗?

你对这个囙答的评价是

下载百度知道APP,抢鲜体验

使用百度知道APP立即抢鲜体验。你的手机镜头里或许有别人想知道的***

参考资料

 

随机推荐