ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C++小游戏教程之猜数游戏的实现

2026/10/8 8:51:31 拓冰建站 浏览量
C++小游戏教程之猜数游戏的实现 前言猜数游戏猜一个 1 到 100 之间的整数程序告诉你「大了」还是「小了」是学习一门语言时 最经典的小练习。它表面简单但要把三个环节都写对其实需要一点真功夫随机数随机种子怎么选、随机数怎么生成才没有偏置输入用户输入了abc或者直接按了 CtrlZ程序会不会卡死循环控制什么时候提示、什么时候结束。初学者的典型写法是rand() % 100 1再配一句srand(time(0))。这套组合能跑 但有偏置、可预测性差而且rand()的质量完全交给实现去决定。 本文用 C11 起标准的random库重写一遍并给出一个能正确处理非法输入的完整程序。 所有代码按C17标准编写目标编译器为 GCC 13 / Clang 17 / MSVC 19.3x 只用到标准库不含任何平台相关调用。一、随机数为什么不该继续用 rand()std::rand()返回[0, RAND_MAX]区间内的一个int而RAND_MAX是实现定义的 在 glibcLinux 上的 GCC/Clang 默认 C 库上它通常是 2147483647 而在 MSVC 的 C 运行时上它是 32767。你随手写的rand() % 100只有在(RAND_MAX 1)恰好是 100 的整数倍时才是均匀的当RAND_MAX是 32767 时32768 % 100 68于是0到67这几个余数出现的概率会比其它余数高一档。 这种偏差在游戏里无所谓但在需要严格均匀的场合抽样、模拟就是实打实的错误。random提供的设施则把算法钉死在标准里std::mt19937是 32 位的梅森旋转 Mersenne Twister算法它的周期是 2^19937 - 1这个数字是算法本身的性质 与你用哪家标准库无关。配合std::uniform_int_distribution取到的整数保证在闭区间上均匀。方案随机质量偏置可复现性建议rand() % N实现定义有除非N整除RAND_MAX 1靠srand不推荐std::mt19937uniform_int_distribution算法由标准规定无固定种子即可复现推荐std::random_device直接取值取决于实现无不可复现只用来做种子种子的选择也有讲究想要每局都不一样用std::random_device取值作为种子。想要可以复现调试时特别有用用固定常量例如std::mt19937 gen(20240501);。注意std::random_device能否给出真正的熵源是实现定义的libstdc 在某些 MinGW 构建里会退化成固定序列所以不要把它当成密码学安全的随机源。不要用time(nullptr)当种子。它的分辨率是秒级一秒内启动两次会得到完全相同的序列。二、完整实现下面这个程序包含了一个可复用的输入辅助函数。它对「解析失败」和「输入结束」做了区分 并且先判断 EOF、再清除错误标志——这个顺序很关键反过来写会把程序变成死循环 原因见「常见坑点」。#include iostream #include limits #include random namespace { enum class ReadResult { ok, // 成功读到一个整数 bad_input, // 输入内容不是整数已清空缓冲区 eof // 输入流结束 }; ReadResult read_int(int out) { if (std::cin out) { return ReadResult::ok; } if (std::cin.eof()) { return ReadResult::eof; // 先判断 EOF此时若 clear() 过会陷入反复报错的死循环 } std::cin.clear(); // 只清 failbit让流恢复可用 std::cin.ignore(std::numeric_limitsstd::streamsize::max(), \n); // 丢弃这一行剩余内容 return ReadResult::bad_input; } } // namespace int main() { std::random_device rd; std::mt19937 gen(rd()); // 32 位梅森旋转 std::uniform_int_distributionint dist(1, 100); // 闭区间 [1, 100] const int answer dist(gen); std::cout 我想好了一个 1 到 100 之间的整数来猜猜看吧。\n; int low 1; int high 100; int tries 0; for (;;) { std::cout 请输入一个 low 到 high 之间的整数; int guess 0; const ReadResult r read_int(guess); if (r ReadResult::eof) { std::cout \n输入结束游戏退出。\n; return 0; } if (r ReadResult::bad_input) { std::cout 这不是一个合法的整数请重新输入。\n; continue; // 非法输入不计入次数 } tries; if (guess answer) { std::cout 小了。\n; if (guess 1 low) { low guess 1; // 把已知的无效区间收窄 } } else if (guess answer) { std::cout 大了。\n; if (guess - 1 high) { high guess - 1; } } else { std::cout 猜对了你一共用了 tries 次。\n; break; } if (tries 10) { std::cout 十次都没猜中答案是 answer 下次再来。\n; break; } } return 0; }几处值得说明的地方std::uniform_int_distributionint dist(1, 100)的两个参数是闭区间端点也就是1和100都可能被取到。这一点与%的写法容易混淆也与很多语言里 「左闭右开」的习惯不同。dist(gen)每次调用都会消耗生成器状态所以dist和gen都可以反复复用不需要每次重新构造——重复构造反而会损失熵。low和high只是给玩家的提示范围不影响答案的判定逻辑即使某次输错了范围也不会导致判断出错。程序只用了std::cin/std::cout没有任何平台相关的调用所以在 Linux、macOS、Windows 上行为一致。三、让它更好玩三个小扩展扩展一难度分级。把猜测次数上限和数值上限做成参数#include iostream #include random #include string int max_number_for(const std::string level) { if (level easy) { return 50; } if (level normal) { return 100; } return 500; // hard } int max_tries_for(const std::string level) { if (level easy) { return 10; } if (level normal) { return 7; } return 9; }扩展二给计算机也出一道题。二分查找恰好是「猜数」的最优策略 每次猜区间中点最多 ⌈log2(100)⌉ 7 次就能锁定 1 到 100 之间的任意数。 这不只是游戏技巧它正是std::lower_bound所做的事——区间每轮至少缩小一半 所以复杂度是对数级的。扩展三让输入更宽容。玩家往往会输入多余的空格或直接回车。前面那个read_int已经能吃掉一整行的剩余字符所以42和42\n都能被正确解析。常见坑点场景❌ 错误写法✅ 正确写法随机数生成srand(time(0)); int x rand() % 100 1;std::mt19937 gen(std::random_device{}()); std::uniform_int_distributionint dist(1, 100);播种时机每轮循环都gen.seed(...)/ 每次循环重建生成器只在main开头播种一次输入失败处理std::cin guess;拿到非法输入后不管cin卡在 failbit循环变成死循环刷屏cin.clear(); cin.ignore(..., \n);后再继续判断顺序先cin.clear()再判断cin.eof()EOF 下会无限报错先判断cin.eof()再clear()区间含义以为uniform_int_distribution(1, 100)取不到 100它是闭区间两端都能取到输出暂停system(cls)/system(pause)用std::cin.get()或直接输出提示system依赖平台且会启动子进程类型选择把答案存成float再用比较用整型浮点相等比较本身就不该用结束条件让玩家一直猜到对没有上限体验很差设tries上限并给出答案清屏依赖依赖 ANSI 转义序列在旧版 Windows 控制台上「清屏」默认不生效想跨平台就用条件编译或干脆不清屏其中最阴的一个是输入失败后的死循环std::cin guess一旦因为字符无法解析而失败 流会置上failbit并保持失败状态后续所有读取立即返回失败且不消耗任何字符。 如果你在循环里只判断if (!(std::cin guess)) { continue; }就会变成 「错误提示刷屏、CPU 跑满」的经典事故。必须clear()ignore()才能恢复。另一个是浮点比较虽然这个游戏用整数就够了但很多初学者会把答案生成成rand() / (double)RAND_MAX * 100然后和用户输入做比较结果几乎永远猜不中。 浮点数的相等比较在绝大多数业务里都是错的用法。总结要点做法原因随机数randommt19937uniform_int_distribution算法由标准规定均匀无偏种子std::random_device或固定常量只播一次兼顾随机性与可复现性输入检查返回值失败时clearignore否则流会卡在失败状态结束条件设置次数上限并公布答案避免玩家无限猜下去经典算法二分查找是最优猜法对数级次数与std::lower_bound同一原理把随机数、输入校验、循环控制这三块写扎实这个 60 行的小程序就不再只是「能跑」 而是一个可以直接拿去教学、也能直接改成「猜单词」「猜牌」的骨架。 真正值得带走的不是游戏本身而是std::cin的失败状态处理和对「闭区间」这类 细节的敏感度——这两点在任何交互式程序里都会反复遇到。