📖 适合人群:C语言初学者、有编程基础想巩固的读者
⏱️ 阅读时长:约15-20分钟
💡 学习收获:掌握随机数生成、循环结构、分支语句、模块化编程思想
📌 一、前言
大家好!今天我们来聊聊一个经典又有趣的小项目——猜数字游戏。这个游戏的核心逻辑很简单:由程序随机生成一个1到100之间的整数,玩家通过不断输入数字来猜测这个神秘数字。每次猜测后,程序会给出"猜大了"或"猜小了"的提示,直到玩家猜对为止。别看这个游戏简单,它涉及的知识点可不少:
- 随机数的生成(
rand和srand) - 循环结构(
while、do-while) - 分支语句(
switch) - 函数的封装与调用
- 用户交互与输入处理 通过这个小小的游戏,我们可以把这些知识点串联起来,真正理解它们是如何协同工作的。话不多说,让我们先看看完整的代码:
📌 二、完整代码展示
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// 游戏主体逻辑函数
void game() {
// 生成随机数:1~100
int r = rand() % 100 + 1;
// 存储玩家输入的猜测值
int guess = 0;
// 无限循环,直到猜对后 break 退出
while (1) {
printf("请输入你猜的数字:");
scanf("%d", &guess);
if (guess < r) {
printf("猜小了,再试试!\n");
} else if (guess > r) {
printf("猜大了,降降温!\n");
} else {
printf("恭喜你,猜对了!\n");
break; // 猜对了,退出循环
}
}
}
int main() {
int input = 0; // 存储用户菜单选择
// 初始化随机数种子(只需执行一次)
srand((unsigned int)time(NULL));
// do-while:先执行一次,再判断条件
do {
printf("\n========== 猜数字游戏 ==========\n");
printf(" 1. 开始游戏\n");
printf(" 0. 退出游戏\n");
printf("================================\n");
printf("请输入您的选择:");
scanf("%d", &input);
switch (input) {
case 1:
game(); // 调用游戏函数
break;
case 0:
printf("感谢游玩,再见!\n");
break;
default:
printf("输入错误,请输入 0 或 1\n");
break;
}
} while (input); // input 为真(非0)时继续循环
return 0;
}
📌 三、代码逐行解析
3.1 预处理指令
#define _CRT_SECURE_NO_WARNINGS
作用:在 Visual Studio 等编译器中禁用安全警告。在 VS 中使用
scanf会报警告,加这行代码可以让编译更清爽。
#include <stdio.h> // 标准输入输出库
#include <stdlib.h> // 包含 rand()、srand()、malloc() 等函数
#include <time.h> // 包含 time() 函数,用于获取时间戳
3.2 game() 函数解析
void game() {
int r = rand() % 100 + 1; // 生成 1~100 的随机数
int guess = 0;
while (1) {
printf("请输入你猜的数字:");
scanf("%d", &guess);
if (guess < r) {
printf("猜小了,再试试!\n");
} else if (guess > r) {
printf("猜大了,降降温!\n");
} else {
printf("恭喜你,猜对了!\n");
break;
}
}
}
| 行号 | 代码 | 解析 |
|---|---|---|
| 1 | void game() | 定义一个无返回值类型的函数,名为 game |
| 2 | int r = rand() % 100 + 1 | 生成 1~100 的随机整数(详见下文知识点) |
| 4 | while (1) | 死循环,持续让玩家输入直到猜对 |
| 6 | scanf("%d", &guess) | 读取用户输入的整数,& 取地址符 |
| 7-14 | if-else if-else | 比较猜测值与随机数,给出反馈 |
| 12 | break | 猜对时跳出 while 循环,结束游戏 |
3.3 main() 函数解析
int main() {
int input = 0;
srand((unsigned int)time(NULL)); // 初始化随机数种子
do {
printf("\n========== 猜数字游戏 ==========\n");
printf(" 1. 开始游戏\n");
printf(" 0. 退出游戏\n");
printf("================================\n");
printf("请输入您的选择:");
scanf("%d", &input);
switch (input) {
case 1:
game();
break;
case 0:
printf("感谢游玩,再见!\n");
break;
default:
printf("输入错误,请输入 0 或 1\n");
break;
}
} while (input);
return 0;
}
| 行号 | 代码 | 解析 |
|---|---|---|
| 2 | int input = 0 | 定义整型变量存储用户菜单选择 |
| 3 | srand((unsigned int)time(NULL)) | 用当前时间初始化随机数种子 |
| 5 | do { ... } while (input) | 先执行菜单,再判断是否继续 |
| 11-19 | switch(input) | 根据用户选择执行对应操作 |
📌 四、核心知识点深入剖析
🔑 知识点一:随机数生成(rand 与 srand)
这是很多初学者容易困惑的地方,让我们详细讲解:
1. rand() 函数
int r = rand(); // 生成一个随机整数
rand() 会返回一个 伪随机整数,范围通常是 0 ~ RAND_MAX(RAND_MAX 在大多数系统上是 32767)。
2. 为什么需要 srand()?
关键点:rand() 生成的随机数并不是真正随机的!
它实际上是伪随机数生成器,基于一个初始值(种子)来计算序列。如果种子相同,每次运行程序,rand() 生成的序列都是一样的!
// 不设置种子的情况
rand(); // 第一次运行:72, 45, 23...
rand(); // 第二次运行:72, 45, 23... (完全相同!)
3. srand() 设置种子
srand((unsigned int)time(NULL)); // 用当前时间作为种子
time(NULL)返回 自1970年1月1日以来经过的秒数- 由于每次运行程序的时间不同,种子就不同
- 这样每次运行程序,生成的随机数序列就不同了
4. 生成指定范围的随机数
int r = rand() % 100 + 1; // 生成 1~100 的随机数
原理拆解:
rand() % 100→ 得到 0~99 的余数+ 1→ 最终范围变成 1~100
通用公式:
rand() % (最大值 - 最小值 + 1) + 最小值
例如,生成 50~100 的随机数:
int r = rand() % 51 + 50; // 50~100
💡 小贴士:
srand()只需要调用一次,通常放在main()函数开头。多次调用反而可能导致随机数"不够随机"。
🔑 知识点二:do-while 循环
语法结构
do {
// 循环体语句
} while (条件表达式);
执行流程
┌─────────────────────────────────┐
│ 执行循环体代码 │
└───────────────┬─────────────────┘
│
▼
┌───────────────────┐
│ 判断条件表达式 │
└─────────┬─────────┘
│
┌──────┴──────┐
│ 条件为真? │
└──────┬──────┘
│
是 │ 否
┌────┴────┐
│ │
▼ ▼
继续执行 循环结束
为什么这里用 do-while?
do {
// 显示菜单
// 获取用户输入
} while (input);
选择 do-while 而不是 while 的原因:
| 特性 | while | do-while |
|---|---|---|
| 首次执行 | 先判断条件,可能一次都不执行 | 先执行,至少执行一次 |
| 本例适用性 | ❌ 如果用 while,需要额外处理首次菜单显示 | ✅ 完美匹配菜单场景 |
// 用 while 实现会这样(更繁琐):
int input;
printf("显示菜单"); // 需要先手动显示一次
scanf("%d", &input);
while (input != 0) {
// 处理逻辑
printf("显示菜单"); // 每次循环都要显示
scanf("%d", &input);
}
🔑 知识点三:switch 分支语句
语法结构
switch (表达式) {
case 常量1:
// 语句块1
break;
case 常量2:
// 语句块2
break;
default:
// 默认语句块
break;
}
执行流程
┌─────────────────┐
│ 计算 switch 表达式 │
└────────┬────────┘
│
▼
┌─────────────────────┐
│ 匹配到哪个 case ? │
└────────┬────────────┘
│
┌────────┼────────┐
│ │ │
▼ ▼ ▼
case 1 case 0 default
│ │ │
▼ ▼ ▼
执行语句 执行语句 执行语句
│ │ │
▼ ▼ ▼
break break break(结束)
本例中的 switch
switch (input) {
case 1:
game(); // 1 -> 开始游戏
break;
case 0:
printf("感谢游玩,再见!\n"); // 0 -> 退出
break;
default:
printf("输入错误,请输入 0 或 1\n"); // 其他 -> 报错
break;
}
⚠️ 重要提醒:
break语句不能省略!如果没有break,程序会"贯穿"执行下一个 case 的代码(这有时是有意为之,但大多数时候是 bug)。
// 没有 break 的"贯穿"效果示例:
switch (score) {
case 5:
case 4:
printf("优秀\n"); // score 为 4 或 5 都输出"优秀"
break;
case 3:
case 2:
printf("及格\n");
break;
}
🔑 知识点四:输入处理与取地址符 &
scanf("%d", &guess);
这里有两个关键点:
1. scanf 函数
%d表示读取一个十进制整数scanf返回成功读取的项数
2. & 取地址符
这是初学者非常容易犯错的地方!
int guess; // 定义整型变量
scanf("%d", &guess); // &guess 表示变量 guess 的内存地址
为什么要加 &?
scanf 需要知道把读取到的数据存到哪里去,所以要提供变量的地址(内存位置)。
| 写法 | 含义 | 结果 |
|---|---|---|
scanf("%d", guess) | ❌ 错误 | 编译器可能不报错,但运行时会崩溃或产生不可预测行为 |
scanf("%d", &guess) | ✅ 正确 | 把输入的值存到 guess 变量所在的内存地址 |
📌 五、代码优化建议
虽然原代码能正常运行,但作为"合格程序员",我们还应该考虑更多细节。以下是几个优化方向:
✏️ 优化一:添加边界校验
问题:当前代码接受任意范围的整数输入,可能导致:
你猜:-10000
猜大了
你猜:99999999
猜小了
优化后:
void game() {
int r = rand() % 100 + 1;
int guess = 0;
printf("我已经想好了一个 1~100 之间的数字,请开始猜!\n");
while (1) {
printf("请输入你猜的数字(1-100):");
// 检查 scanf 返回值,确保成功读取
if (scanf("%d", &guess) != 1) {
// 清空输入缓冲区
while (getchar() != '\n');
printf("输入无效,请输入数字!\n");
continue;
}
// 边界校验
if (guess < 1 || guess > 100) {
printf("请输入 1~100 范围内的数字!\n");
continue;
}
// 游戏逻辑
if (guess < r) {
printf("猜小了,再试试!\n");
} else if (guess > r) {
printf("猜大了,降降温!\n");
} else {
printf("恭喜你,猜对了!\n");
break;
}
}
}
✏️ 优化二:添加猜测次数统计
void game() {
int r = rand() % 100 + 1;
int guess = 0;
int attempts = 0; // 猜测次数计数器
printf("我已经想好了一个 1~100 之间的数字,请开始猜!\n");
while (1) {
printf("请输入你猜的数字:");
scanf("%d", &guess);
attempts++; // 计数 +1
if (guess < r) {
printf("猜小了!\n");
} else if (guess > r) {
printf("猜大了!\n");
} else {
printf("恭喜你,猜对了!\n");
printf("你总共猜了 %d 次\n", attempts);
break;
}
}
}
✏️ 优化三:添加猜测次数限制(简单模式)
void game() {
int r = rand() % 100 + 1;
int guess = 0;
int attempts = 0;
const int MAX_ATTEMPTS = 7; // 最多猜7次
printf("【简单模式】你有 %d 次机会,加油!\n", MAX_ATTEMPTS);
while (attempts < MAX_ATTEMPTS) {
printf("还剩 %d 次机会,请输入:", MAX_ATTEMPTS - attempts);
scanf("%d", &guess);
attempts++;
if (guess < r) {
printf("猜小了!\n");
} else if (guess > r) {
printf("猜大了!\n");
} else {
printf("恭喜你,猜对了!用了 %d 次\n", attempts);
return;
}
}
printf("很遗憾,你用完了所有机会!正确答案是 %d\n", r);
}
✏️ 优化四:使用 getchar 防止换行符干扰
当用户输入字符后按回车,\n 会留在输入缓冲区,可能干扰下一次输入:
// 改进的菜单输入函数
int getMenuChoice() {
int choice;
printf("\n========== 猜数字游戏 ==========\n");
printf(" 1. 开始游戏\n");
printf(" 0. 退出游戏\n");
printf("================================\n");
printf("请输入您的选择:");
while (scanf("%d", &choice) != 1) {
// 如果输入不是数字,清空缓冲区
while (getchar() != '\n');
printf("输入错误,请输入数字:");
}
return choice;
}
✏️ 优化五:代码重构与模块化
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// 函数声明
void printMenu();
int getUserChoice();
void playGame();
void game();
// ==================== 主函数 ====================
int main() {
srand((unsigned int)time(NULL));
while (1) {
printMenu();
int choice = getUserChoice();
switch (choice) {
case 1:
playGame();
break;
case 0:
printf("感谢游玩,再见!\n");
return 0; // 直接退出程序
default:
printf("输入错误,请输入 0 或 1\n");
}
}
return 0;
}
// ==================== 打印菜单 ====================
void printMenu() {
printf("\n========== 猜数字游戏 ==========\n");
printf(" 1. 开始游戏\n");
printf(" 0. 退出游戏\n");
printf("================================\n");
printf("请输入您的选择:");
}
// ==================== 获取用户选择 ====================
int getUserChoice() {
int choice;
while (scanf("%d", &choice) != 1) {
while (getchar() != '\n'); // 清空输入缓冲区
printf("输入错误,请输入数字:");
}
return choice;
}
// ==================== 单局游戏 ====================
void playGame() {
int r = rand() % 100 + 1;
int guess;
int attempts = 0;
printf("\n游戏开始!我已经想好了一个 1~100 之间的数字。\n");
while (1) {
printf("请输入你猜的数字:");
// 输入校验
if (scanf("%d", &guess) != 1) {
while (getchar() != '\n');
printf("⚠️ 输入无效,请输入数字!\n");
continue;
}
// 边界校验
if (guess < 1 || guess > 100) {
printf("⚠️ 请输入 1~100 范围内的数字!\n");
continue;
}
attempts++;
if (guess < r) {
printf("⬆️ 猜小了!\n");
} else if (guess > r) {
printf("⬇️ 猜大了!\n");
} else {
printf("🎉 恭喜你,猜对了!用了 %d 次\n", attempts);
break;
}
}
}
📌 六、扩展思考:让游戏更完善
如果你想让这个猜数字游戏更有趣,可以考虑以下扩展方向:
🎮 扩展一:难度等级系统
void selectDifficulty(int *range, int *maxAttempts) {
int level;
printf("\n请选择难度等级:\n");
printf("1. 简单模式(1-50,10次机会)\n");
printf("2. 普通模式(1-100,7次机会)\n");
printf("3. 困难模式(1-200,8次机会)\n");
printf("4. 地狱模式(1-500,9次机会)\n");
printf("请选择:");
scanf("%d", &level);
switch (level) {
case 1:
*range = 50; *maxAttempts = 10; break;
case 2:
*range = 100; *maxAttempts = 7; break;
case 3:
*range = 200; *maxAttempts = 8; break;
case 4:
*range = 500; *maxAttempts = 9; break;
default:
*range = 100; *maxAttempts = 7;
}
}
🎮 扩展二:历史记录与排行榜
typedef struct {
char name[50];
int attempts;
int range;
} ScoreRecord;
void saveScore(ScoreRecord *records, int *count, int attempts, int range) {
printf("恭喜通关!请输入你的名字:");
scanf("%s", records[*count].name);
records[*count].attempts = attempts;
records[*count].range = range;
(*count)++;
}
void showLeaderboard(ScoreRecord *records, int count) {
printf("\n========== 排行榜 ==========\n");
for (int i = 0; i < count; i++) {
printf("%d. %s - %d次(1-%d范围)\n",
i+1, records[i].name, records[i].attempts, records[i].range);
}
}
🎮 扩展三:支持重新开始与统计
typedef struct {
int totalGames; // 总游戏局数
int totalAttempts; // 总猜测次数
int bestRecord; // 最佳记录
} GameStats;
void updateStats(GameStats *stats, int attempts) {
stats->totalGames++;
stats->totalAttempts += attempts;
if (stats->bestRecord == 0 || attempts < stats->bestRecord) {
stats->bestRecord = attempts;
}
}
void showStats(GameStats *stats) {
printf("\n========== 游戏统计 ==========\n");
printf("总游戏局数:%d\n", stats->totalGames);
printf("总猜测次数:%d\n", stats->totalAttempts);
if (stats->totalGames > 0) {
printf("平均每局:%.1f 次\n",
(float)stats->totalAttempts / stats->totalGames);
}
printf("最佳记录:%d 次\n", stats->bestRecord);
}
📌 七、常见问题 FAQ
Q1: 为什么猜数字游戏总是猜到同一个随机数?
很可能是因为 srand() 没有正确调用或放在了错误的位置。
错误示例:
void game() {
srand((unsigned int)time(NULL)); // ❌ 每次游戏都重新设置种子
int r = rand() % 100 + 1;
// ...
}
正确做法:
int main() {
srand((unsigned int)time(NULL)); // ✅ 只调用一次
// ...
}
Q2: while (input) 和 while (input != 0) 等价吗
是的! 在 C 语言中,非零值表示"真"(true),零值表示"假"(false)。所以:
while (input)等价于while (input != 0)while (!input)等价于while (input == 0)
Q3: 如何让随机数更"随机"?
除了 time(NULL),还可以使用:
#include <stdlib.h>
srand(rand()); // 用 rand() 的初始值作为种子(不推荐)
// 或者使用更好的随机数生成器
#include <random>
std::random_device rd; // C++ 的随机设备(需要 C++11)
Q4: break 和 continue 的区别?
| 关键字 | 作用 | 适用场景 |
|---|---|---|
break | 跳出整个循环 | 猜对数字、找到目标、发生错误需要退出 |
continue | 跳过本次循环剩余语句,进入下次迭代 | 输入无效需要重新输入、跳过某些特殊情况 |
while (1) {
scanf("%d", &guess);
if (guess < 1 || guess > 100) {
continue; // 无效输入,跳过下面的判断,重新输入
}
// 正常游戏逻辑...
}
📌 八、总结
通过这篇教程,我们从零开始学习了:
| 知识点 | 掌握程度 | 应用场景 |
|---|---|---|
#define 预处理指令 | ✅ | 禁用警告、定义常量 |
srand() + rand() | ✅ | 生成伪随机数 |
do-while 循环 | ✅ | 菜单驱动的循环程序 |
switch-case 分支 | ✅ | 多分支选择逻辑 |
break / continue | ✅ | 循环控制 |
scanf 与 & 取地址符 | ✅ | 用户输入处理 |
| 函数封装与模块化 | ✅ | 提升代码可维护性 |

245

被折叠的 条评论
为什么被折叠?



