尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C语言分支与循环怎么练?用猜数字游戏打通控制流

C语言分支与循环怎么练?用猜数字游戏打通控制流 1. 为什么我建议拿猜数字游戏当分支循环的练手项目接触过C语言的人都知道分支结构和循环结构是语法学习里避不开的两道坎。很多初学者在教材上做了一堆if-else嵌套、while循环打印星号的练习结果一到自己写程序还是不知道怎么下手。我当年学C语言时也经历过这个阶段语法都认识每行代码都能读懂可真要自己写一个带逻辑的小程序脑子就是一片空白。后来回头想想问题的根源在于大部分练习题是“为了用语法而用语法”缺少一个完整的、能激发兴趣的载体。比如“输入一个数判断奇偶”“输出1到100的累加和”做完也就做完了完全不知道自己写的东西有什么用。猜数字游戏就是我在这个阶段找到的最合适的练手项目没有之一。它的规则足够简单——程序随机生成一个1到100之间的整数你输入数字它告诉你猜大了还是猜小了直到你猜对为止。但就是这么一个看着不起眼的小游戏几乎把分支结构和循环结构的核心用法全部串起来了判断玩家猜的数字和答案的大小关系这是典型的分支结构应用场景玩家猜错之后要反复猜这是典型的循环结构应用场景循环里还藏着一个if-else分支两个结构嵌套配合恰好模拟了真实程序里最常见的控制流组织方式。这篇文章我打算用一个完整项目的思路来拆解这个游戏从规则设计、关键字选择、代码实现到调试排错全部过一遍。如果你是刚学完C语言分支和循环语法、正愁找不到练手项目的新手或者已经能看懂代码但还没独立写过完整程序这篇文章应该能给你不少实在的东西。2. 先把游戏规则拆清楚逻辑设计先行于代码很多初学者有个很常见的毛病——拿到了题目就打开编辑器开始写代码写到一半发现逻辑没想清楚只能删掉重来。我在带学生做这个项目时会先让他们在纸上把游戏流程画出来哪怕只是写几行伪代码也比直接动手写强得多。2.1 一次完整的猜数字流程是什么样的我们先站在玩家的角度走一遍这个游戏最核心的一次对局流程程序随机生成一个1到100之间的整数这个数不能让玩家看到玩家从键盘输入一个数字作为自己的猜测程序比较玩家的猜测和正确答案的大小关系如果猜大了程序提示“猜大了再来一次”如果猜小了程序提示“猜小了再来一次”如果猜对了程序提示“恭喜你猜对了”本轮游戏结束。把这六步写成C语言风格的伪代码大概是这个样子生成随机数 secret 循环: 输入猜测值 guess 如果 guess secret: 输出猜大了 否则如果 guess secret: 输出猜小了 否则: 输出猜对了 结束循环这个伪代码里已经出现了一对重要的逻辑关系循环负责“反复让玩家输入”分支负责“每次输入后给出不同反馈”。循环体里嵌套分支分支的结果又反过来决定循环是否继续。这两个结构的关系本质上就是循环给了程序重复执行的能力分支给了程序每次执行时做出不同判断的能力。2.2 边界情况要先想清楚我让初学者在动手前先把规则问清楚其实还有一个目的——逼他们思考边界情况。比如这个问题里你会遇到几个非常现实的问题玩家猜的数字如果超出了1到100的范围程序要不要管玩家输入的不是整数比如输入了abc怎么办游戏结束后玩家想再玩一局程序要不要支持对于练手项目我的建议是第一版先把核心流程跑通上面这些问题能处理就处理处理不了也不要有压力。先把“猜数字”这一个循环走通后面再逐步加功能这个思路才是健康的工程习惯。一上来就想把输入校验、重复游玩、难度选择全做进去反而容易被细节困住主线逻辑反而写不扎实。3. 分支结构让程序学会“判断”分支结构在C语言里的三个关键词是if、else if、else外加一个用得相对少的switch。猜数字游戏里最核心的判断逻辑就是拿玩家的输入和答案做比较然后决定往哪个分支走。3.1 if-else if-else 在这个游戏里怎么用猜数字游戏里的判断逻辑是三选一猜大了、猜小了、猜对了。用if-else if-else写出来如下if (guess secret) { printf(猜大了\n); } else if (guess secret) { printf(猜小了\n); } else { printf(猜对了\n); }这里有一个C语言初学者非常容易踩的坑判断顺序和边界条件的覆盖。如果你把else if条件写成“guess secret”那么当guess secret时所有条件都为假程序什么都不会输出。很多人的逻辑好像没问题结果运行时发现猜对了却没有反应就是因为没考虑相等的情况。另一种常见错误是把条件写反比如把else if (guess secret)误写成else if (guess secret)导致提示永远不对应。这种错误在纸上推演一遍就能暴露出来也正说明了上一节“先画流程”的重要性。3.2 关系运算符和逻辑运算符判断的底层工艺写好if条件绕不开关系运算符。C语言里的关系运算符有六个、、、、、!。猜数字游戏里用到的大于、小于和等于就是前两个和后三个中的。另外要注意逻辑运算符的优先级问题。如果你后面扩展功能比如要判断guess是否在合法范围内可能会写出这样的条件if (guess 1 guess 100) { // 合法输入 }这里的是逻辑与运算符表示两个条件同时成立。优先级方面关系运算符的优先级高于逻辑与所以上面这个表达式会先算guess 1和guess 100再用把它们连接起来结果符合直觉。但如果你把括号漏掉硬写成guess 1 guess 100其实也是正确的因为C语言保证了这个结合顺序。还有一个初学者特别容易和赋值混淆的地方判断相等要用而不是。在C语言里if (guess secret)是合法代码但它做的不是判断相等而是把secret的值赋给guess然后检查这个值是否为真非零。这种错误编译器通常不会报错程序却会表现出完全意想不到的行为。用猜数字游戏来体会这个坑比单纯背语法印象深十倍。3.3 分支结构在这个游戏里的两个层次猜数字游戏里其实藏着两层分支逻辑很多新手只看到第一层。第一层是“判断大小”也就是我们上面写的if-else if-else它在每次玩家输入后执行一次。第二层是“判断是否继续游戏”。如果我们扩展功能让玩家在猜对之后可以选择再来一局这里又需要一个分支printf(是否再来一局(y/n)); scanf( %c, choice); if (choice y || choice Y) { // 重新开始游戏 } else { printf(再见\n); }第二层分支通常嵌套在第一层循环的外面用来决定整个游戏要不要从头再来。两层分支的逻辑虽然单看都不难但它们和循环嵌套在一起之后控制流的走向会变得复杂。这正是我想通过这个游戏传达的分支结构单独拿出来确实简单真正的挑战在于多个结构叠加时大脑还能不能理清程序实际的执行路径。4. 循环结构让程序学会“重复”猜数字游戏里最自然的需求就是——玩家猜错了不能直接结束得允许他再猜一次。这个“再猜一次”的背后就是循环结构在发挥作用。4.1 C语言三种循环的对比与选择C语言提供了三种循环结构while、do-while、for。形式上看它们都能实现重复执行但适用场景有微妙的差异循环类型判断时机最少执行次数典型场景while先判断后执行0次不知道要循环几次且可能一次都不执行do-while先执行后判断1次至少要执行一次比如“先猜一次再判断”for先判断后执行0次循环次数已知或由一个变量控制猜数字这个场景玩家至少要猜一次天然适合do-while。如果你用while实现就得先给guess赋一个永远不会猜对的值或者用一个标志变量强行进入循环写起来别扭得多。do { printf(请输入你猜的数字1-100); scanf(%d, guess); if (guess secret) { printf(猜大了\n); } else if (guess secret) { printf(猜小了\n); } else { printf(恭喜你猜对了\n); break; } } while (1);上面这段代码里do-while的循环条件我写的是字面量1也就是条件永远为真。那这个循环怎么结束呢靠的是循环体里的break语句也就是猜对之后跳出循环。4.2 break、continue 和退出循环的几种方式break的作用是跳出当前循环continue的作用是跳过本次循环剩余的语句、进入下一次迭代。猜数字游戏里break是必须的因为总不能真的让程序无限循环下去。推荐的做法是猜对时用break跳出这样代码最简洁} else { printf(恭喜你猜对了\n); break; }很多人会问能不能不用break而是用循环条件本身来退出当然可以。你可以定义一个标志变量flag初始为1猜对时把flag改成0然后while (flag)来判断。两种写法都能跑区别在于用break代码更短但逻辑上的“出口”藏在循环体中间用flag的方式循环条件本身就能说明“还在猜”的状态可读性有时候更好。我在教这个项目时一般建议新手先熟悉break的写法因为它的逻辑更直观——猜对就跳出去符合直觉。等写熟练之后再用flag变量去重构一遍体会一下两种退出机制的不同味道。4.3 死循环是怎么产生的又该怎么避免“循环必须能退出”这句话说一百遍不如让新手实际掉一次坑。猜数字游戏里最容易产生的死循环之一是scanf读取失败后输入缓冲区的残留问题。比如玩家不小心输入了一个字母”a”scanf(%d, guess)会读取失败此时guess保持之前的值而输入缓冲区里还残留着字符’a’。下一次循环scanf再读还是读到’a’这个非数字字符再次失败。结果就是程序一遍遍提示“请输入你猜的数字”但玩家再怎么输入数字也无法让程序正常响应看起来就像死循环。这个问题我在后面讲坑的时候会专门展开现在先记住一个结论scanf读整数遇到非数字字符时不会自动跳过它你需要手动清空输入缓冲区。否则哪怕你的while条件写对了程序也可能因为输入问题卡在循环里出不来。5. 完整代码一个能直接跑起来的猜数字游戏前面把分支和循环分开讲清楚了现在把它们组装起来。很多人学C语言卡在一个点上单个语法都懂组合起来就懵。下面我给出一个完整可运行的代码然后一行一行讲它为什么这么写。#include stdio.h #include stdlib.h #include time.h int main() { int secret, guess; char choice; do { // 初始化随机数种子 srand((unsigned)time(NULL)); // 生成 1~100 之间的随机数 secret rand() % 100 1; printf(我已经想好了一个 1~100 之间的数字来猜猜看吧\n); // 猜数字主循环 while (1) { printf(请输入你的猜测); scanf(%d, guess); if (guess secret) { printf(猜大了\n); } else if (guess secret) { printf(猜小了\n); } else { printf(太棒了猜对了答案就是 %d\n, secret); break; } } printf(还想再玩一局吗(y/n)); scanf( %c, choice); } while (choice y || choice Y); printf(感谢游玩再见\n); return 0; }这段代码把两层循环嵌套起来外层do-while控制“要不要再来一局”内层while(1)控制“猜错就继续猜”。二者之间靠break和choice变量衔接。有几个细节新手写的时候特别容易出错我逐一说明第一外层用do-while而不是while。因为无论玩家玩不玩下一局游戏至少要先完整地跑一次do-while的“先执行后判断”恰好匹配这个需求。如果写while你就需要额外定义一个标志变量初始化还要费心思。第二内层用while(1)搭配break而不是用do-while。其实do-while也能实现同样的效果比如do { printf(请输入你的猜测); scanf(%d, guest); ... } while (guess ! secret);这段代码用while (guess ! secret)作为循环条件逻辑上也能跑通。乍一看它比break版本更“纯粹”——没有用break循环条件直接反映了“没猜对就继续”的语义。但这里有个坑程序会重复使用guess这个变量而scanf(%d, guess)每次都会覆盖它的值所以while (guess ! secret)确实能正常工作。我之所以推荐break版本是因为它在逻辑上更好读。循环条件一旦写成1读者的精力就不会被“这个条件什么时候为假”的问题带走可以专注理解循环体内部的判断逻辑。等代码写多了你会发现逻辑清晰比代码短更重要。第三注意scanf( %c, choice)里面格式字符串%c前有一个空格。这个空格的作用是跳过输入缓冲区里可能残留的换行符。如果不加这个空格上一次scanf(%d, guess)之后玩家按回车产生的换行符还留在缓冲区里紧接着的scanf(%c, choice)会直接读到一个换行符choice就变成了’\n’而不是玩家实际输入的y或n游戏会直接退出。这个坑我用红色高亮也不过分因为它极其隐蔽排查起来非常痛苦。6. 随机数的生成容易被忽略的底层细节猜数字游戏的“题眼”是随机数。代码里只有一行secret rand() % 100 1但这一行背后的门道比看上去多得多。6.1 rand() 和 srand()随机数其实不随机C语言标准库里的rand()函数会返回一个0到RAND_MAX之间的整数RAND_MAX在绝大多数环境下是32767。每次调用rand()它返回的是序列里的下一个数。这个序列看起来是随机的但它其实是固定的——只要程序的种子不换每次运行生成的序列一模一样。所谓的“种子”就是srand()设置的那个起点。如果不调用srand()程序默认种子是1那么每次运行时生成的随机数序列完全一样。这会导致一个尴尬的情况你每运行一次游戏答案都跟在第一次运行时相同的位置上完全不“随机”。所以代码里必须在生成随机数之前调用一次srand((unsigned)time(NULL))。time(NULL)返回当前系统时间用时间做种子每次运行的时间不同种子就不同产生的随机数序列也随之变化这才达到了“每次玩都是新答案”的效果。这里有个重要细节srand只需要调用一次。我把srand放在外层do-while循环里每次进入新一轮游戏都会重新播种。严格来说这种做法效率并不高因为time(NULL)以秒为单位如果玩家手速极快在一秒内完成了两局游戏两局随机数种子相同生成的答案也可能相同。更规范的做法是把srand放在main函数最开头调用一次就够之后rand()会持续生成后续序列。初学者经常写错的是把srand放进内层的while(1)循环里导致每次猜数字都重置种子。这样一来每轮循环生成的随机数都是同一个数因为循环执行时间远小于1秒玩家一旦猜对范围就会陷入“刚才告诉你猜大了现在同一个值却变了”的诡异现象。如果你遇到这种Bug第一优先怀疑srand的位置。6.2 rand() % 100 1 到底做了什么先看一个反例如果用rand() % 100生成的是0到99之间的数包含0不包含100。猜数字游戏的答案范围是1到100所以需要在取模后加1即rand() % 100 1最终范围变成1到100。取模运算在这里有两个作用一是把rand()的返回值压缩到目标区间二是保证区间内的每个数都有机会出现。不过严格地讲rand() % 100并不是绝对均匀的。rand()返回0到32767共32768个数。把它分成100组会发现前68个数0到67会多一次出现机会因为32768除以100的余数是68。所以在真正的随机数应用场景下这种取模方式会有微小的偏差。但对于猜数字游戏来说这个偏差完全可忽略你不可能在玩的过程中感知到68这个区间比别的区间多了1/32768的出现概率。如果你按教程做进阶涉及更高精度的随机数需求比如模拟掷骰子、随机抽奖建议学习一个技巧——拒绝采样int get_random_number(int min, int max) { int range max - min 1; int limit RAND_MAX - (RAND_MAX % range); int result; do { result rand(); } while (result limit); return min result % range; }这段逻辑先过滤掉会产生偏差的部分然后再取模。猜数字游戏用不上它但作为知识点它反映了“随机数生成不是简单取模”的工程师思维。6.3 边界范围写错的几种典型表现假设你要做的是1到100猜数字那么rand() % 100 1是正确的。但如果你不小心写成rand() % 101这个空子可就大了——生成的数范围是0到100包括0一百个数之外的0也会出现玩家如果猜0也会中奖这就不符合游戏规则了。另一种常见错误是自己把起止范围搞混。比如想生成10到20之间的数正确写法是rand() % 11 10。这个例子里% 11是因为闭区间的元素个数是20-10111个。很多新手在没吃透“取模结果是0到N-1”这个概念前会直接写成rand() % 10 10结果范围变成10到19少了20。用猜数字游戏熟悉这个计算之后再遇到“生成特定范围随机数”的需求你就能直接套公式了。7. 我踩过的坑以及帮你省时间的调试技巧说实话猜数字游戏代码量不大但它埋坑的密度绝对不小。这一节我专门讲自己教这个项目时反复见到的错误每条都是真实案例。7.1 “scanf 分号后置”引起的代码逻辑错乱在C语言里if、while、for这些语句后面跟分号会产生独立的空语句导致后面的代码块脱离控制结构。比如while (guess ! secret); { printf(请输入); scanf(%d, guess); }这里while后面直接跟了个分号整个循环体变成了空语句程序会在这一行原地死循环。括号里的printf和scanf只是普通代码块永远不会被执行。这种错误编译不报错、运行无提示只有程序卡住不动了才知道出事了。排查方法很简单先检查每个控制语句后面有没有多出来的分号。7.2 输入缓冲区残留字符那个让你“无法退出”的真凶前面说过scanf(%d)读不到字母会失败这里细讲一下。运行程序后如果玩家不小心输了个字母你会看到程序状态变成提示“请输入你的猜测”但无论你输什么数字它都没反应最后你只能强制关闭窗口。原因拆解scanf(%d, guess)遇到非数字字符时读取失败guess保持原值而那个非数字字符留在缓冲区里。下一次循环scanf再读又读到同一个非数字字符再次失败无限循环。要解决需要在读取失败时清空缓冲区#include stdio.h // 清空输入缓冲区 void clear_input_buffer() { int c; while ((c getchar()) ! \n c ! EOF) { // 逐个读走并丢弃 } } int main() { int guess; printf(请输入); while (scanf(%d, guess) ! 1) { clear_input_buffer(); printf(输入无效请重新输入); } // 正常使用 guess return 0; }这段代码用getchar()把缓冲区里的残留字符一个个吃掉直到遇到换行符或文件结束标志为止。加上这个函数即使玩家乱输程序也能恢复响应。7.3 区分“调试打印”和“正式输出”写C语言程序时很多人习惯在关键位置临时加一行printf来打印变量的值这很正常但用完要记得删掉。我在猜数字游戏里见过最哭笑不得的事情是调试时打印了随机数的值忘了删结果整个游戏变成了看着答案猜数字。调试不是不能说而是要控制在一个不干扰玩家体验的层面。建议用条件编译的方式控制调试输出// #define DEBUG #ifdef DEBUG printf([调试信息] 答案是%d\n, secret); #endif需要调试时把#define DEBUG的注释去掉调试完再注释回来。这样既方便排查又不影响正式版本。7.4 编译不通过的检查顺序新手写代码错误一半来自拼写和语法。如果编译器报错按这个顺序检查头文件有没有包含齐全。猜数字游戏需要stdio.hprintf、scanf、stdlib.hrand、srand、time.htime。每个左括号{是否都有对应的右括号}。C语言不会像Python那样用缩进决定结构括号错了编译器可能会在文件末尾报“expected ‘}’”。每个语句是否都有分号结尾。要注意的是函数定义后面不能加分号而变量声明和普通语句必须加分号。变量名是否拼写一致。secret和sceret这种低级错误编译器会报“undefined identifier”一眼就能看出问题。8. 玩法升级给猜数字游戏加入更多花样把基础版本跑通之后这个项目的价值才刚刚体现出来。下面这几个扩展方向每一个都指向新的知识点你可以根据自己的进度逐个尝试。8.1 记录猜测次数引入“计分”概念定义一个计数器count每猜一次加1。玩家最终猜对后输出总次数int count 0; ... count; if (guess secret) { printf(你一共猜了 %d 次\n, count); break; }这个需求虽然简单但它教会了你“如何在循环中维护状态变量”。很多更复杂的算法本质都是在循环里不断更新几个状态变量猜数字是这个思路最简单的演练场。8.2 用for循环限制最大尝试次数在玩法上加上“最多猜7次”的限制这时for循环比while更合适因为循环上限是确定的for (int i 1; i 7; i) { printf(第 %d 次猜测, i); scanf(%d, guess); if (guess secret) { printf(猜大了\n); } else if (guess secret) { printf(猜小了\n); } else { printf(恭喜你%d 次就猜对了\n, i); break; } if (i 7) { printf(很遗憾7次都没猜中答案是 %d\n, secret); } }这里要注意for循环和break的配合。break会直接跳出for循环所以如果玩家猜对了就不会执行后面的“7次都没猜中”提示。这是分支和循环嵌套的又一次实战训练。8.3 用switch重构判断逻辑switch通常比if-else更适合多路分支的场景但猜数字的大小判断很难直接用switch表达因为它需要比较运算。你可以换一个思路用switch处理游戏菜单比如难度选择printf(请选择难度1.简单 2.中等 3.困难); scanf(%d, difficulty); switch (difficulty) { case 1: range 50; break; case 2: range 100; break; case 3: range 200; break; default: printf(输入无效默认中等难度\n); range 100; break; }这样写的好处是default分支能让程序对非法输入有兜底行为。对比if-else实现switch的可读性会更清晰因为每一种case都直白对应一个选项。8.4 保存历史战绩体会“持久化”思想如果玩家玩了很多局能不能记录下他的最佳成绩最少猜测次数这就需要在主循环外定义一个min_count变量每局结束后对比int min_count 9999; ... if (count min_count) { min_count count; printf(新纪录最少 %d 次猜中\n, min_count); }把每局的猜测次数记下来程序运行期间就拥有了跨局“记忆”。虽然这只是内存中的临时变量程序关闭就没了但它为你将来接触文件读写、数据库存储这些“持久化”技术打下了概念基础。我在实际带项目时的体会是猜数字游戏最适合的学习路径不是“一次写完就完事”而是“先跑通基础版再像搭积木一样一个功能一个功能往上加”。每加一个功能你对分支和循环的理解就深一层。等你把上面几个扩展都做完你会发现自己已经不是那个只会照抄书上例子的初学者了而是能真正读懂控制流、设计控制流的入门C程序员了。
返回列表