新闻详情

新闻详情

首页 / 资讯中心 / 详情

C语言扫雷项目实战:二维数组与递归算法的经典应用

发布时间:2026/10/1 18:53:49来源:尧图网络
C语言扫雷项目实战:二维数组与递归算法的经典应用
写这个项目之前先聊几句。扫雷是C语言初学者绕不开的一个经典练手项目它没有复杂的业务逻辑却能非常自然地串起数组、循环、函数封装、随机数、递归这几块C语言入门阶段的核心知识点。这篇文章就是给刚学完指针和二维数组、想找一个完整项目练手的新人准备的。我会按自己实际写过的思路把棋盘设计、布雷、统计雷数、翻开格子、递归展开、胜负判断一路讲下来代码可以整体复制到Dev C或VS里直接跑每一步都会解释为什么这么写希望你看完能自己把它复现出来而不是只会照着敲一遍。1. 为什么选扫雷做C语言练手项目1.1 扫雷项目能练到哪些核心知识点很多新人学完数组和函数之后最大的困惑是这些东西到底能干什么扫雷就是一个特别好的答案。它天然需要一个二维数组来承载棋盘数据需要有函数来拆分“初始化、布雷、计算、显示、玩家操作”这些模块需要用循环遍历来统计格子周围的雷数还需要用递归来实现“点开一片空白区域”的效果。一个项目把C语言入门阶段最重要的几块都覆盖到了。从难度上说扫雷又刚好卡在“跳一跳够得着”的位置。比九九乘法表、冒泡排序这种几行代码的练习复杂但又远没有学生管理系统、图书管理系统那种“业务逻辑堆砌”那么枯燥。如果你能独立完成扫雷再去写贪吃蛇、五子棋这类游戏思路会顺很多因为你已经知道“游戏循环”和“状态更新”是怎么回事了。1.2 这个版本做成了什么样子我这里选择的方案是10×10的棋盘布置10颗雷玩家依次输入坐标翻开格子也可以输入坐标加标记操作来插旗。翻开后如果该格子周围没有雷程序会自动展开周围一片区域直到碰到有数字的边界为止。踩到雷游戏结束翻完所有非雷格子则胜利。这套规则大家小时候基本都玩过好处是你不需要向任何人解释游戏玩法可以把全部精力放在C语言实现上。控制台版本不需要图形界面输出用字符和数字拼出棋盘保留了扫雷的核心玩法代码量又控制在200行左右新人可以完整读懂这是一个刚刚好的体量。有一点需要提前说清楚控制台版扫雷和你手机里的扫雷差在哪。它没有鼠标交互没有颜色图标也没有雷区动画但核心算法是完全一致的。你把这一版写明白了以后想套任何图形库都是顺手的事因为底层的逻辑就是那几张二维数组。2. 核心数据结构与设计思路2.1 用两个二维数组管理棋盘状态扫雷最核心的设计决策就是用一个棋盘还是两个棋盘。我的答案是必须用两个二维数组。第一个数组叫mine类型是int用来记录地雷分布。它里面只存两种值1代表有雷0代表无雷。这个数组对玩家是不可见的是程序内部用来判断踩没踩雷的。第二个数组叫show类型是char用来展示给玩家看。它里面存的是字符*表示未翻开1到8表示周围雷数!表示玩家手动标记的旗帜空格表示已经翻开且周围没有雷。为什么不能用同一个数组原因很简单。玩家看到的局面和真实的雷局天然就是两个不同的信息层。真实雷局是固定不变的而玩家界面是随着操作不断变化的。如果你只用一个数组每翻一次格子就要想办法隐藏真实信息代码会绕来绕去。用两个数组各司其职代码逻辑一下就清晰了。这种“数据层和展示层分离”的思路你在以后写任何项目都会遇到。哪怕是写个网页也是数据库一份数据前端展示一份视图。现在养成这种习惯后面受益。2.2 为什么棋盘要多拓一圈我定义棋盘大小的时候用的是这两个宏#define ROWS 10 #define COLS 10但在真正创建数组时我写成int mine[ROWS2][COLS2]; char show[ROWS2][COLS2];也就是实际分配的是12×12的空间但只使用中间10×10的区域作为有效游戏区外围一圈是不参与游戏、专门用来省麻烦的“边界缓冲带”。很多人第一次看到这个设计会问这不是浪费内存吗10×10的棋盘为什么非要多一圈出来原因在统计雷数的时候特别明显。假如某个格子正好在棋盘边缘比如第1行第1列它周围也是有8个邻居的但这8个邻居里有3个在数组外面。如果你用9×9的数组写统计函数时就得写一堆if (x-1 1)这样的边界判断代码瞬间变得很丑。而如果你给棋盘加一圈“空心边”原来边缘格子的坐标变成第2行第2列它的所有邻居都落在数组范围内直接遍历就行根本不用判断越界。代价是内存多了一点点换来的是代码的简洁和逻辑的安全。这个思路在以后做地图、做网格类应用时会反复用到属于那种吃过亏才会记住的技巧现在直接学会省得以后踩坑。2.3 数组下标从1开始还是从0开始顺带说一个新手容易困惑的点数组下标到底是0还是1。传统的C语言数组当然从0开始但在扫雷这个项目里我设计让有效游戏区域从下标1开始使用下标0整块区域都闲置不用。也就是棋盘上第1行第1列存在mine[1][1]里而不是mine[0][0]。为什么这么干还是因为统计周围雷数。当玩家输入坐标(1,1)时我直接去查mine[1][1]就行计算它的周边坐标就遍历mine[i][j]其中i从x-1到x1j从y-1到y1。因为每个格子周围8个邻居都有对应的数组位置就不需要任何坐标范围判断了。虽然数组从0开始用也能写但你每次操作坐标都要做一次index - 1的转换还会增加越界风险。对于新人来说用1到10作为有效坐标自己心里清楚下标0那圈是缓冲带这是最不容易错的做法。3. 完整代码实现与逐段拆解3.1 全局宏定义与函数声明先看整体代码框架。我把所有需要的头文件、宏定义和函数声明放在一起#include stdio.h #include stdlib.h #include time.h #include windows.h #define ROWS 10 #define COLS 10 #define MINE_NUM 10 void InitBoard(char show[ROWS2][COLS2], int mine[ROWS2][COLS2]); void SetMine(int mine[ROWS2][COLS2]); int CountMine(int mine[ROWS2][COLS2], int x, int y); void ShowBoard(char show[ROWS2][COLS2], int mine[ROWS2][COLS2]); void OpenCell(char show[ROWS2][COLS2], int mine[ROWS2][COLS2], int x, int y); int WinCheck(char show[ROWS2][COLS2], int mine[ROWS2][COLS2]); void GameLoop(char show[ROWS2][COLS2], int mine[ROWS2][COLS2]); void SetColor(int color);这里有个需要解释的细节windows.h和SetColor是给控制台设置颜色的让数字在屏幕上显示不同颜色纯属锦上添花不是核心逻辑。如果你在Linux或者Mac上编译把这部分去掉也不影响运行。不想折腾的可以直接忽略它。ROWS和COLS定义的是有效游戏区域的大小MINE_NUM定义的是雷的数量。对新手来说把这种魔法数字提取成宏是好习惯后续调难度只需要改这一处不用满篇找数字。3.2 棋盘初始化与布雷接下来是初始化函数void InitBoard(char show[ROWS2][COLS2], int mine[ROWS2][COLS2]) { int i, j; for (i 0; i ROWS2; i) { for (j 0; j COLS2; j) { show[i][j] *; mine[i][j] 0; } } }这个函数做的事非常直白把游戏数组全部置成初始状态。show里面全部填*表示所有格子都没有被翻开mine里面全部填0表示还没有任何雷。注意我用的是ROWS2而不是ROWS因为数组实际尺寸比游戏区域大一圈如果只初始化有效区域外围缓冲带会留下随机垃圾值后面统计雷数时这些垃圾值会被当成雷数算出来结果一片混乱。初始化永远要覆盖整个数组空间这是一个容易忽略但必须坚持的原则。布雷函数是这个项目的第一个重点void SetMine(int mine[ROWS2][COLS2]) { int count 0; while (count MINE_NUM) { int x rand() % ROWS 1; int y rand() % COLS 1; if (mine[x][y] 0) { mine[x][y] 1; count; } } }布雷的思路是死循环直到布够10颗雷为止。每次随机生成一个坐标判断这个位置之前有没有布过雷没有就布一颗。这里用rand() % ROWS 1来生成1到10之间的随机数注意这里必须写1因为如果只写rand() % ROWS得到的是0到9就会落到缓冲区里或者和玩家输入坐标对不上。但随机数还有个问题就是rand()默认情况下产生的序列是固定的不随机。所以主函数里必须先调用srand((unsigned)time(NULL))来设置随机种子。我后面在主函数部分会贴出来。3.3 统计周边雷数与数字面板生成统计雷数的函数是理解这个项目算法的关键之一int CountMine(int mine[ROWS2][COLS2], int x, int y) { int i, j; int count 0; for (i x-1; i x1; i) { for (j y-1; j y1; j) { if (mine[i][j] 1) { count; } } } return count; }这个函数的逻辑简单到让人怀疑就是数一下某个格子周围8个格子里有几颗雷。之所以敢这么直接写是因为我们定义棋盘时多加了一圈缓冲带。如果x和y的取值范围限定在1到10那么x-1最小是0x1最大是11都不会越界。如果当初棋盘是从0到9你写这个函数到处都是if判断看起来就没这么舒服了。这里我把传入坐标本身的雷也统计进去了。但要注意当你翻开一个格子时这个格子肯定是非雷的所以CountMine返回的就是它周围真正的雷数。我写这段代码时顺手把自身也加进去后续用的时候直接看返回值不会有问题。实际在玩家翻开格子的时候需要把这个统计结果转成字符存在show数组里show[x][y] CountMine(mine, x, y) 0;这里有个所有新手都会困惑的点为什么数字要加一个0因为show数组是字符数组里面存的是字符而不是数字。数字5在ASCII表里对应的是值53而字符5才是我要显示在屏幕上的东西。CountMine返回的是整数5我加上字符0ASCII值48就把5变成了53也就是字符5的值。这种数字 0转字符的技巧在C语言里非常常见一定要理解。3.4 玩家核心操作翻开与递归展开扫雷最精彩的部分就是当玩家翻开一个周围没有雷的格子时程序会自动“呼啦一片”展开。这个效果在C语言里就是用递归实现的。我先把代码贴出来再解释void OpenCell(char show[ROWS2][COLS2], int mine[ROWS2][COLS2], int x, int y) { if (x 1 || x ROWS || y 1 || y COLS) { return; } if (show[x][y] ! *) { return; } int count CountMine(mine, x, y); if (count 0) { show[x][y] count 0; return; } show[x][y] ; OpenCell(show, mine, x-1, y-1); OpenCell(show, mine, x-1, y); OpenCell(show, mine, x-1, y1); OpenCell(show, mine, x, y-1); OpenCell(show, mine, x, y1); OpenCell(show, mine, x1, y-1); OpenCell(show, mine, x1, y); OpenCell(show, mine, x1, y1); }这段代码的核心逻辑是这样的翻开一个格子之前先做两个判断。第一坐标是否越界越界直接返回。第二这个格子是不是*状态如果不是*说明已经翻过或者是旗帜标记直接返回防止重复递归。然后统计它周围的雷数。如果周围有雷那就在这个位置显示数字结束。如果周围没有雷那先把当前格子设成空格表示“已翻开且周围安全”然后递归调用OpenCell去翻开它周围8个格子。这样一路递归下去就会自动把一整片安全区域全部翻开直到碰到有数字的格子为止。这个递归最妙的地方在于它的自我终止条件第一是坐标越界直接返回第二是遇到的格子已经翻过直接返回。这两个条件保证了递归不会无限执行下去也保证了性能不会出问题。10×10的棋盘最多100个格子递归深度最深也是100层栈空间完全够用。有一个细节值得注意show[x][y] ! *这个条件。玩家可以在没翻开的地方插旗插了旗的格子显示为!。如果递归展开时碰到!show[x][y] ! *成立函数直接返回不会动它。这样旗帜就不会被一片空白区域覆盖掉玩家设置的标记始终可见体验是对的。3.5 胜负判定与主循环游戏循环的主角是GameLoop函数。逻辑是这样的玩家输入坐标程序判断这个坐标是否合法然后判断踩没踩雷没踩雷就翻开格子翻开后检查是否胜利然后继续循环void GameLoop(char show[ROWS2][COLS2], int mine[ROWS2][COLS2]) { int x, y; int op; while (1) { ShowBoard(show, mine); printf(输入操作(1翻开 / 2插旗 / 0退出): ); scanf(%d, op); if (op 0) { printf(游戏结束\n); break; } printf(输入坐标(行 列): ); scanf(%d %d, x, y); if (x 1 || x ROWS || y 1 || y COLS) { printf(坐标超出范围请重新输入\n); continue; } if (op 2) { if (show[x][y] *) { show[x][y] !; } else if (show[x][y] !) { show[x][y] *; } continue; } if (op 1) { if (show[x][y] ! || (show[x][y] 0 show[x][y] 8)) { printf(该位置不能翻开\n); continue; } if (mine[x][y] 1) { printf(踩雷了游戏结束\n); mine[x][y] -1; ShowBoard(show, mine); break; } OpenCell(show, mine, x, y); if (WinCheck(show, mine)) { printf(恭喜你扫雷成功\n); break; } } } }代码看起来长但流程很清晰。玩家有两种操作模式一种是普通的翻开一种是插旗。插旗的逻辑是如果当前位置是*就改成!相当于标记一个旗帜如果已经是!再按一次就取消旗帜。为什么要单独设插旗操作因为控制台游戏没有右键功能为了模拟鼠标右键的插旗效果只能通过输入命令来切换。这是控制台版扫雷的设计取舍。虽然比图形界面麻烦但它把“操作”这个维度引出来了以后你想做图形界面时你的逻辑层代码完全不用动只需要把输入方式换成鼠标点击就行。踩雷的处理也要说一下。当玩家踩雷时我把mine[x][y]设成-1然后用ShowBoard重新显示棋盘。ShowBoard里会检查mine[i][j] -1的格子把它显示成X这样就告诉玩家“你踩中的是这颗雷”。更重要的是踩雷后要把所有雷都显示出来不然玩家死得不明不白。这属于游戏体验的细节写的时候容易忽略但确实很重要。3.6 主函数与显示函数主函数是程序的入口做的事情不多但每件都很关键int main() { char show[ROWS2][COLS2]; int mine[ROWS2][COLS2]; srand((unsigned)time(NULL)); InitBoard(show, mine); SetMine(mine); printf(---- C语言扫雷 ----\n); printf(棋盘大小: %dx%d 雷数: %d\n, ROWS, COLS, MINE_NUM); GameLoop(show, mine); return 0; }main函数里的逻辑顺序是固定的先设置随机种子再初始化棋盘再布雷最后进入游戏循环。很多新人会把设置随机种子忘掉或者放在布雷之后结果就是每次运行游戏雷的位置都一样怎么调都不随机。这里要特别注意srand必须只调用一次必须在第一次使用rand之前调用。然后说显示函数。显示棋盘的功能在扫雷里看起来简单但要做好还是有讲究的。我一版一版改过三个心得第一要在棋盘上方打印列号方便玩家输入坐标。第二每行开头也要打印行号。第三相邻格子之间要有分隔不然数字挤在一起根本分不清是哪一列。void ShowBoard(char show[ROWS2][COLS2], int mine[ROWS2][COLS2]) { int i, j; printf(\n ); for (j 1; j COLS; j) { printf(%d , j); } printf(\n); for (i 1; i ROWS; i) { printf(%d , i); for (j 1; j COLS; j) { if (show[i][j] !) { SetColor(14); printf(! ); SetColor(7); } else if (show[i][j] *) { printf(* ); } else if (mine[i][j] -1) { SetColor(12); printf(X ); SetColor(7); } else { printf(%c , show[i][j]); } } printf(\n); } printf(\n); }这里有一行显示颜色的代码是配合之前提到的SetColor函数用的。SetColor(14)是黄色用来显示旗帜SetColor(12)是红色用来显示踩中的地雷位置。如果你用的是在线编译器或者Linux终端直接去掉这三行颜色相关代码即可不影响逻辑。实际显示的时候我还差一个WinCheck函数。这个函数判断玩家是否已经翻完了所有非雷格子int WinCheck(char show[ROWS2][COLS2], int mine[ROWS2][COLS2]) { int i, j; int count 0; for (i 1; i ROWS; i) { for (j 1; j COLS; j) { if (show[i][j] ! * show[i][j] ! !) { if (mine[i][j] ! 1) { count; } } } } if (count ROWS * COLS - MINE_NUM) { return 1; } return 0; }判断逻辑是遍历整个有效棋盘统计那些已经翻开又不是雷的格子数量。如果这个数量等于总格子数减雷数说明所有安全区域都已经翻开玩家胜利。这里有个小坑如果玩家插了旗而旗子下面是雷show[i][j]是!不会被统计进去。所以就算玩家把旗帜插在雷上也不会导致误判胜利。只有真正翻开的非雷格子才算数。这个逻辑一开始容易写错建议多测试几次。3.7 完整代码整合把上面的所有函数凑在一起就是可以直接编译的完整程序了。我把它整体贴出来方便你直接复制验证#include stdio.h #include stdlib.h #include time.h #include windows.h #define ROWS 10 #define COLS 10 #define MINE_NUM 10 void SetColor(int color) { HANDLE hConsole GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleTextAttribute(hConsole, color); } void InitBoard(char show[ROWS2][COLS2], int mine[ROWS2][COLS2]) { int i, j; for (i 0; i ROWS2; i) { for (j 0; j COLS2; j) { show[i][j] *; mine[i][j] 0; } } } void SetMine(int mine[ROWS2][COLS2]) { int count 0; while (count MINE_NUM) { int x rand() % ROWS 1; int y rand() % COLS 1; if (mine[x][y] 0) { mine[x][y] 1; count; } } } int CountMine(int mine[ROWS2][COLS2], int x, int y) { int i, j; int count 0; for (i x-1; i x1; i) { for (j y-1; j y1; j) { if (mine[i][j] 1) { count; } } } return count; } void OpenCell(char show[ROWS2][COLS2], int mine[ROWS2][COLS2], int x, int y) { if (x 1 || x ROWS || y 1 || y COLS) { return; } if (show[x][y] ! *) { return; } int count CountMine(mine, x, y); if (count 0) { show[x][y] count 0; return; } show[x][y] ; OpenCell(show, mine, x-1, y-1); OpenCell(show, mine, x-1, y); OpenCell(show, mine, x-1, y1); OpenCell(show, mine, x, y-1); OpenCell(show, mine, x, y1); OpenCell(show, mine, x1, y-1); OpenCell(show, mine, x1, y); OpenCell(show, mine, x1, y1); } void ShowBoard(char show[ROWS2][COLS2], int mine[ROWS2][COLS2]) { int i, j; printf(\n ); for (j 1; j COLS; j) { printf(%d , j); } printf(\n); for (i 1; i ROWS; i) { printf(%d , i); for (j 1; j COLS; j) { if (show[i][j] !) { SetColor(14); printf(! ); SetColor(7); } else if (show[i][j] *) { printf(* ); } else if (mine[i][j] -1) { SetColor(12); printf(X ); SetColor(7); } else { printf(%c , show[i][j]); } } printf(\n); } printf(\n); } int WinCheck(char show[ROWS2][COLS2], int mine[ROWS2][COLS2]) { int i, j; int count 0; for (i 1; i ROWS; i) { for (j 1; j COLS; j) { if (show[i][j] ! * show[i][j] ! !) { if (mine[i][j] ! 1) { count; } } } } if (count ROWS * COLS - MINE_NUM) { return 1; } return 0; } void GameLoop(char show[ROWS2][COLS2], int mine[ROWS2][COLS2]) { int x, y; int op; while (1) { ShowBoard(show, mine); printf(输入操作(1翻开 / 2插旗 / 0退出): ); scanf(%d, op); if (op 0) { printf(游戏结束\n); break; } printf(输入坐标(行 列): ); scanf(%d %d, x, y); if (x 1 || x ROWS || y 1 || y COLS) { printf(坐标超出范围请重新输入\n); continue; } if (op 2) { if (show[x][y] *) { show[x][y] !; } else if (show[x][y] !) { show[x][y] *; } continue; } if (op 1) { if (show[x][y] ! || (show[x][y] 0 show[x][y] 8)) { printf(该位置不能翻开\n); continue; } if (mine[x][y] 1) { printf(踩雷了游戏结束\n); mine[x][y] -1; ShowBoard(show, mine); break; } OpenCell(show, mine, x, y); if (WinCheck(show, mine)) { printf(恭喜你扫雷成功\n); break; } } } } int main() { char show[ROWS2][COLS2]; int mine[ROWS2][COLS2]; srand((unsigned)time(NULL)); InitBoard(show, mine); SetMine(mine); printf(---- C语言扫雷 ----\n); printf(棋盘大小: %dx%d 雷数: %d\n, ROWS, COLS, MINE_NUM); GameLoop(show, mine); return 0; }这套代码在Windows的Visual Studio和Dev C环境里可以直接编译运行。如果你用的是Linux把#include windows.h和SetColor相关的代码删掉再把ShowBoard里的颜色调用删掉剩下的逻辑是纯标准C一样能跑。4. 实操中的常见问题与排查技巧4.1 每次运行雷的位置都一样这是新手遇到最多的一个问题。现象是程序能编译能运行但每次布雷的结果都一样换一台机器也差不多。原因基本可以锁定为没有设置随机种子或者srand放的位置不对。解决办法是在main函数的最前面也就是第一次调用rand之前加上srand((unsigned)time(NULL))这一行。time(NULL)返回当前时间到秒的整数种子不一样rand生成的序列就不一样。但这里有个细节如果两次运行在同一个秒内种子的值是一样的雷的位置还是会一样。所以你会看到有些游戏程序为了更随机会用更高精度的时钟来生成种子比如clock()配合当前时间组合运算。对新人的扫雷项目来说用time(NULL)就足够了真遇到同秒运行的情况概率很低不影响体验。4.2 输入坐标后程序直接崩溃如果你在打开一个格子时程序崩溃闪退第一反应应该想到数组越界。最常见的越界原因是玩家输入的坐标不在有效范围内代码没做校验就直接往下执行。我在GameLoop里加了这么一段if (x 1 || x ROWS || y 1 || y COLS) { printf(坐标超出范围请重新输入\n); continue; }为什么必须加这段因为数组show和mine的实际可用坐标范围是1到10玩家要是输入0或者11代码就会访问到边界外的内存运气好是垃圾数据运气不好直接段错误崩溃。很多教材上的示例代码为了精简省略了这步校验但实际项目里这是必须写的。打个比方这就像开车转弯必须看后视镜不是每一次都需要看但养成习惯能救你一次。4.3 递归展开后屏幕卡顿或者莫名跳出递归展开本身一般不会出问题但如果你在OpenCell里漏掉了某个终止条件就可能出现无限递归后果是屏幕疯狂刷格子甚至程序僵死。常见的漏掉的条件是坐标越界判断。如果玩家翻开边界处的格子递归调用OpenCell(show, mine, x-1, y-1)时x-1 可能是0此时不判断就访问show[0][y]虽然因为缓冲区的存在数组不会真越界但如果你把系统改成9×9棋盘瞬间就炸了。我的建议是所有函数不论内部逻辑多简单只要涉及坐标访问第一行就写越界判断。这不是代码啰嗦而是防御式编程的习惯。第一次写递归的时候你可以在OpenCell里临时加个 static 计数变量每进入一次函数加1如果超过200次直接打印错误退出用来定位是否无限递归。调试完再删掉。4.4 输入坐标后出现“该位置不能翻开”但没反应这个问题比较隐蔽表现是玩家输入了坐标棋盘上对应的格子明明显示*但程序提示不能翻开。排查思路基本是检查show[x][y]当前的值到底是什么。常见原因是show数组里的格子其实是可见的但玩家没注意到。比如玩家已经翻过这个格子上面显着数字5这时候它当然不能再被翻开。这个判断本身是合理的。另一个容易踩的坑是scanf(%d %d, x, y)在读取坐标时如果玩家输入了非数字字符比如误输入了字母scanf就会读入失败变量保持原来的值程序看起来毫无反应甚至死循环。处理这个问题比较标准的办法是用fflush(stdin)清空输入缓冲区或者检查scanf的返回值。但fflush(stdin)在C标准里其实是未定义行为很多编译器支持有些不支持。更稳妥的方式是在读入后检查返回值是否为2如果不是就清空缓冲区重新输入。不过考虑到这是新人项目我不想把输入校验搞得太复杂。最简单的处理办法是在游戏开头打印一句“输入格式为操作 行 列”比如1 3 5表示翻开第3行第5列这样玩家不会因为输入顺序不清而出错。我在代码里拆成了两行输入先输入操作再输入坐标就是为了降低格式复杂度。4.5 显示界面错乱、数字叠在一起控制台版棋盘显示乱多半是输出格式问题。解决方案是固定每个格子占两位字符比如printf(%c , show[i][j])也就是字符后面带一个空格。同时行号列号前面也要对齐空格。如果还乱通常是你用的终端字体不是等宽字体或者终端窗口宽度不够导致自动换行。建议把终端窗口拉大一点或者用等宽字体如Consolas、Courier New。这个属于显示环境问题不是代码问题。5. 新手进阶建议与扩展玩法5.1 从10×10升级到自定义难度目前的代码固定是10×10、10颗雷。想做成三种难度只需要把宏改成变量然后在游戏开始时用随机数决定难度对应的参数就行。比如初级9×910颗雷中级16×1640颗雷高级30×1699颗雷改造的难点在于数组大小变成动态的。C语言里不能直接定义动态大小的二维数组至少标准C不行常见的替代方案是定义一个大数组比如#define MAX_SIZE 32然后用变量控制实际使用的行列数只利用数组的一部分。这个思路在大项目里很常见属于“预分配手动管理”的经典手法。5.2 加上计时功能和首翻保护扫雷游戏没有计时总觉得少了点味道。控制台下计时可以直接用time(NULL)在游戏开始时记一个时间戳每轮循环结束取当前时间减初始时间打印已用秒数。代码改动量很小。首翻保护是另一个很实在的改进。原来的布雷逻辑在游戏开始前就把雷全部布好了所以玩家第一次操作就可能踩雷。真实扫雷的规则是第一次翻开绝不允许是雷。实现方案是先记录玩家的第一次坐标如果这颗格子是雷就把这颗雷移走换到某个没雷的格子上同时要保证移动后其他格子周边的雷数重新计算所以需要把show棋盘的所有数字重算一遍。这个改动说难不难但需要你对数据流向有整体把握非常适合作为下一个练手题目。5.3 模块化拆分代码文件当主函数文件越来越长的时候你要学着把代码拆成多个文件。常见做法是拆成game.h、game.c、main.c三个文件。头文件里放宏定义和函数声明game.c里放函数实现main.c里放主函数。然后在game.c和main.c里都#include game.h。拆分文件的好处是逻辑层次清晰以后改动一个模块不会影响其他模块。但在Dev C或VS里创建多文件项目新人可能需要摸索一下工程配置建议等你把单文件版本跑通之后再做这个改动。5.4 最后说一个关于调试的真实体会我写这个扫雷项目时最耗时间的不是写逻辑而是调一个看似莫名其妙的bug有一次我翻开一片安全区域后边缘有几个格子永远保持*不展开。我盯着递归代码看了半天最后打印出整个棋盘的内部状态才发现这几个格子的show值不是*而是!——因为在测试时我插过旗子后来程序输出了我忘了这回事。这个经历其实很典型。很多“逻辑没错但结果不对”的问题根源是程序的内部状态和你脑补的状态不一样。调试扫雷项目最好的方法是写一个临时函数把mine数组整个打印出来对照show数组看每一步操作后的变化。等问题定位完再删掉这个调试函数。别怕写临时代码动手调试占写代码时间的一半这是非常正常的。扫雷这个项目写完之后你可以继续尝试给棋盘加颜色标记剩余雷数、加右键插旗功能如果有图形库、甚至把雷的形状改成随机生成。但无论加什么功能核心逻辑都没变两张二维数组、一个统计函数、一个递归展开函数。把这几样吃透C语言的地基就打得很稳了。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

无锡靠谱的微波炉变压器温控器/汽车加热垫温控器厂家质量参考评选 2026/10/1 19:49:24

无锡靠谱的微波炉变压器温控器/汽车加热垫温控器厂家质量参考评选

温控器选型核心原理入门:从功能逻辑到落地标准 微波炉变压器温控器、汽车加热垫温控器是工业与民用场景里的安全阀门,其核心作用是通过感知温度变化自动切断/接通电路,避免因过热引发短路、起火等安全事故。很多人对这类器件的认知仅停留在温…

阅读更多 →
北京汉臣健身器材 朝阳区门店 商用椭圆机与动感单车适配不同面积场馆 2026/10/1 19:49:24

北京汉臣健身器材 朝阳区门店 商用椭圆机与动感单车适配不同面积场馆

健身器材选购的行业科普:商用椭圆机与动感单车的本质区别商用椭圆机与动感单车虽然同属有氧器械,但在结构原理、适用人群、场地需求上存在显著差异。椭圆机通过滑轨与飞轮联动,实现脚步不离踏板的椭圆轨迹运动,对膝关节冲击小&…

阅读更多 →
机器人租赁的生死线:现场确认全流程风险与落地工具 2026/10/1 19:49:24

机器人租赁的生死线:现场确认全流程风险与落地工具

做机器人租赁这几年,我越来越觉得“现场确认”这四个字才是真正的生死线。合同签约只是把合作关系定下来,设备进场之后那一连串“开箱—部署—调试—演示—签字”的动作,才是决定这笔租赁到底是赚钱还是赔钱、客户是回头复购还是拉黑你的分水…

阅读更多 →
产品经理的本质:价值交换的设计师,从功能交付到可持续增长 2026/10/1 19:49:10

产品经理的本质:价值交换的设计师,从功能交付到可持续增长

说个产品经理都见过的场景:你花了三周画原型、磨交互、排期、盯开发,功能终于上线了。数据一看,核心按钮点击率不到3%,一周后更低了。这时候很多人会骂用户不懂产品,怪运营不给力,嫌投放量不够。但我做了几…

阅读更多 →
大厂Java面试:从Spring Boot到微服务架构的实战思维 2026/10/1 19:49:10

大厂Java面试:从Spring Boot到微服务架构的实战思维

1. “八股文”不是背答案:大厂Java面试的真正筛选逻辑先聊个很多人不愿意面对的事实:网上铺天盖地的“大厂Java面试题合集”“Java八股文背诵手册”,大家刷得都很努力,但真正能拿到Offer的人,往往不是背得最熟的那个。…

阅读更多 →
DeepTumorVQA2026——3D 腹部 CT 的分层式医学 VQA 基准之腹部器官全分割 2026/10/1 19:49:03

DeepTumorVQA2026——3D 腹部 CT 的分层式医学 VQA 基准之腹部器官全分割

今天将分享3D 腹部 CT 的分层式医学 VQA 基准之腹部器官全分割挑战赛实现版本,为了方便大家学习理解整个流程,将整个流程步骤进行了整理,并给出详细的步骤结果。感兴趣的朋友赶紧动手试一试吧。 一、DeepTumorVQA2026介绍 近年来医学视觉语…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉