ARTICLE DETAIL

建站实战干货

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

5个C++控制台小游戏源码免费分享:从语法入门到实践进阶

2026/9/21 0:19:44 拓冰建站 浏览量
5个C++控制台小游戏源码免费分享:从语法入门到实践进阶 很多学C的朋友都有过这种体验语法书啃了半个月for循环、数组、指针都认识你了但合上书想写点东西还是不知道从哪下手。我自己的经验是学编程最快的路径就是做小项目而小游戏是性价比最高的切入点——它不依赖复杂框架输出反馈快还能把各个语法点串起来。这篇文章整理了5个可以直接运行的C控制台小游戏源码免费分享出来猜数字、井字棋、扫雷、贪吃蛇、俄罗斯方块。全部用C配合Windows控制台API写成Visual Studio、Dev-C、VS Code MinGW都能编译。每一个游戏我都会拆开讲设计思路、核心难点和完整代码还会分享我实际调试时踩过的坑。适合刚开始学C的学生、想复习基础语法的在职开发以及正在为课程设计找素材的朋友。1. 5个C小游戏的设计思路难度阶梯与知识点覆盖1.1 为什么选这5个游戏难度递增与知识点的刻意安排写C小游戏核心目的不是做出多炫的画面而是用最小的复杂度把语言基础吃透。这5个游戏我刻意做了一个难度阶梯你按顺序练习基本上能把C的入门语法完整过一遍。猜数字是门槛最低的只需要随机数、while循环、if分支和基本的cin/cout这相当于你写的第一个“完整程序”。井字棋引入了二维数组和函数拆分还带一个简单的人机AI让你第一次体会到“程序做决策”。扫雷的核心是二维数组和递归展开这里面有随机布雷、数字生成和边界处理逻辑密度比前两个高不少。贪吃蛇开始涉及实时输入和状态刷新要用到STL的vector来管理蛇身还要处理方向控制和碰撞检测。俄罗斯方块则是控制台小游戏里面综合难度最高的一档7种方块的形状表示、旋转矩阵、墙踢、消行、游戏结束判定每一个点都能单独拿出来问“为什么”。有不少人一上来就想写飞机大战、RPG我劝你先别。控制台程序最大的优势是没有图形库的干扰每一行代码都在为“游戏逻辑”服务。等你能把这5个游戏从零写一遍再去碰EasyX、Qt、Unity这些带界面的东西会轻松非常多。1.2 开发环境准备Visual Studio、Dev-C还是VS Code MinGW代码是.cpp源文件不是文本文档。有人把代码复制到记事本里然后改后缀名那肯定跑不起来必须用编译器。环境优点缺点适合人群Visual Studio Community免费开箱即用调试功能强安装包大占用空间多新手、学生上课用Dev-C体积小启动快自带编译器界面老旧调试一般临时用、老电脑VS Code MinGW-w64轻量可自定义体验现代需要手动配置编译器有命令行基础的开发者我自己的建议是如果你在Windows上学习直接装Visual Studio Community新建项目时选“控制台应用”然后把代码粘贴进.cpp文件按CtrlF5就能跑。如果你用的是Dev-C新建源文件、保存成.cpp、编译运行就行。VS Code的话需要先装MinGW-w64并配置好环境变量难度高一点但也不是什么大问题。还有一个很多人忽略的点以下代码里贪吃蛇和俄罗斯方块用到了windows.h和conio.h这两个Windows专属头文件所以只能在Windows下编译。macOS和Linux用户不是不能跑但需要自己改造键盘输入和屏幕刷新部分的代码这对初学者来说工作量不小建议先在Windows上跑通。2. 猜数字用最短代码学会随机数与循环2.1 游戏规则与代码结构拆解猜数字的规则很简单程序随机想一个1~100之间的整数你有10次机会去猜每次输入之后程序会告诉你“太大了”还是“太小了”猜中就赢。这个游戏虽然简单但麻雀虽小五脏俱全。我用了外层while循环来实现“再玩一次”的选项内层while来限制猜测次数srand(time(nullptr))配合rand() % 100 1生成随机数输入校验保证了玩家不会输入1~100之外的数字。代码里还用了一个win标志来记录是否猜中这样如果10次用完还没猜对就能给出不同的结束提示。2.2 完整源代码可直接复制运行的猜数字#include iostream #include cstdlib #include ctime using namespace std; int main() { srand(time(nullptr)); char again y; while (again y || again Y) { int target rand() % 100 1; int guess, count 0; bool win false; cout 猜数字游戏 endl; cout 我已经想好了一个 1~100 之间的数字。 endl; while (count 10) { cout 还剩 10 - count 次机会请输入你的猜测; cin guess; if (guess 1 || guess 100) { cout 输入范围是 1~100 endl; continue; } count; if (guess target) { cout 太大了 endl; } else if (guess target) { cout 太小了 endl; } else { cout 恭喜你猜中了你用了 count 次猜对。 endl; win true; break; } } if (!win) { cout 很遗憾10次机会用完了。正确答案是 target 。 endl; } cout 再玩一次吗(y/n); cin again; } return 0; }2.3 源码解读随机数、循环与边界处理rand() % 100 1这段很多新手会问为什么不是rand() % 101。因为rand() % 100的结果是0~99加1之后才是1~100。如果你想要一个[a, b]区间的随机整数通用公式是rand() % (b - a 1) a这个规律后面几个游戏也一直在用。time(nullptr)作为随机数种子也很关键。如果你不写srand每次运行程序rand()生成的序列都是固定的玩起来就像背答案一样没意思。用当前时间当种子每次运行才不一样。代码里我用continue跳过非法输入但这里有个小瑕疵如果玩家输错这次不消耗猜测次数但提示信息会在下一次重绘时被清掉。控制台游戏经常有这种体验问题不影响功能但你在做自己的项目时可以把提示信息显示在固定区域。扩展方向也很明确加难度选择简单1~50、普通1~100、困难1~200或者把猜测次数改成和数字范围挂钩。理论上1~100的数字用二分法最多7次一定能猜中你可以把机会设置成7次让游戏更有挑战性。3. 井字棋人机对战与AI决策思路拆解3.1 游戏规则与AI优先级设计井字棋是3x3的棋盘玩家用X电脑用O谁先把任意一行、一列或一条对角线连成三个谁就赢。这个游戏代码量不大但它第一次引入了“AI”的概念——电脑怎么决定下一步落子。我给电脑设计了一个五级优先级的决策方案第一能赢就赢第二玩家马上要赢就堵第三占中心位置第四占角落第五随便找一个空位。你从这个规则能看出这其实是一种“贪心策略”每一手都选当下最优解。对于井字棋这种局面有限的游戏这个策略已经能让电脑变得很难被击败但还不是完美AI后手玩家仍然有机会平局或获胜。3.2 完整源代码可直接复制运行的井字棋#include iostream using namespace std; char board[3][3] {{1,2,3},{4,5,6},{7,8,9}}; void drawBoard() { system(cls); cout 玩家: X 电脑: O\n\n; for (int i 0; i 3; i) { cout board[i][0] | board[i][1] | board[i][2] endl; if (i 2) cout ---|---|--- endl; } cout endl; } bool checkWin(char player) { for (int i 0; i 3; i) { if (board[i][0] player board[i][1] player board[i][2] player) return true; if (board[0][i] player board[1][i] player board[2][i] player) return true; } if (board[0][0] player board[1][1] player board[2][2] player) return true; if (board[0][2] player board[1][1] player board[2][0] player) return true; return false; } bool boardFull() { for (int i 0; i 3; i) for (int j 0; j 3; j) if (board[i][j] ! X board[i][j] ! O) return false; return true; } void computerMove() { // 1. 直接赢 for (int i 0; i 3; i) { for (int j 0; j 3; j) { if (board[i][j] ! X board[i][j] ! O) { char tmp board[i][j]; board[i][j] O; if (checkWin(O)) return; board[i][j] tmp; } } } // 2. 堵玩家 for (int i 0; i 3; i) { for (int j 0; j 3; j) { if (board[i][j] ! X board[i][j] ! O) { char tmp board[i][j]; board[i][j] X; if (checkWin(X)) { board[i][j] O; return; } board[i][j] tmp; } } } // 3. 占中心 if (board[1][1] ! X board[1][1] ! O) { board[1][1] O; return; } // 4. 占角落 int corners[4][2] {{0,0},{0,2},{2,0},{2,2}}; for (int k 0; k 4; k) { int r corners[k][0], c corners[k][1]; if (board[r][c] ! X board[r][c] ! O) { board[r][c] O; return; } } // 5. 任意空位 for (int i 0; i 3; i) for (int j 0; j 3; j) if (board[i][j] ! X board[i][j] ! O) { board[i][j] O; return; } } int main() { drawBoard(); while (true) { int pos; cout 请输入位置(1-9); cin pos; if (pos 1 || pos 9 || board[(pos-1)/3][(pos-1)%3] X || board[(pos-1)/3][(pos-1)%3] O) { cout 无效位置重新输入 endl; continue; } board[(pos-1)/3][(pos-1)%3] X; if (checkWin(X)) { drawBoard(); cout 恭喜你赢了 endl; break; } if (boardFull()) { drawBoard(); cout 平局 endl; break; } computerMove(); if (checkWin(O)) { drawBoard(); cout 电脑赢了 endl; break; } if (boardFull()) { drawBoard(); cout 平局 endl; break; } drawBoard(); } return 0; }3.3 AI决策的逻辑核心与后续升级方向仔细看computerMove的前两步它们的实现思路是“模拟落子再还原”。比如判断“电脑能不能赢”就把当前空位临时填上O然后调用checkWin(O)赢了就保留落子并返回不赢就把这个位置恢复成原来的字符。这种“试走一步看看结果不行就撤销”的思路很多围棋程序、象棋程序的底层也是同一个套路只不过它们试的深度更深。为什么中心优先于角落因为井字棋里中心位置能同时参与4条胜利线横、竖、两条对角线而角落只参与3条边格只参与2条。占住中心既方便自己连线也能限制对方的发展空间。如果想升级这个AI最简单的方向是引入minimax算法。井字棋的状态空间很小9个格子每个格子只有三种状态就算全量枚举也才不到2万种局面用递归做搜索毫无压力。如果做到那一步电脑就永远不会输了。再往后可以加“玩家先手还是后手”的选项、难度分级新手模式让电脑随机走、以及记录胜负次数。4. 扫雷二维数组与递归展开的经典练手项目4.1 游戏规则与核心数据结构扫雷的规则不用多说棋盘里随机埋着雷翻开一个格子如果是雷就炸如果是数字就显示周围8格有几颗雷如果是0就会自动展开连片的空白区域。这个游戏最典型的编程考点有三个布雷算法、数字统计、递归展开。我额外做了一个经典的处理——把二维数组上下左右各扩大一圈声明成mine[ROWS2][COLS2]。这样在遍历周围8个格子的时候就不需要每次都判断是不是数组边界因为地图外圈永远是0。这个思路很多二维数组相关的项目都能用比如生命游戏、俄罗斯方块的边界地图非常实用。4.2 完整源代码可直接复制运行的扫雷#include iostream #include cstdlib #include cstring #include ctime using namespace std; const int ROWS 9, COLS 9, MINES 10; int mine[ROWS 2][COLS 2]; bool opened[ROWS 2][COLS 2]; void init() { memset(mine, 0, sizeof(mine)); memset(opened, 0, sizeof(opened)); int placed 0; while (placed MINES) { int r rand() % ROWS 1; int c rand() % COLS 1; if (mine[r][c] ! -1) { mine[r][c] -1; placed; } } for (int i 1; i ROWS; i) { for (int j 1; j COLS; j) { if (mine[i][j] -1) continue; int cnt 0; for (int di -1; di 1; di) for (int dj -1; dj 1; dj) if (mine[i di][j dj] -1) cnt; mine[i][j] cnt; } } } void openCell(int r, int c) { if (r 1 || r ROWS || c 1 || c COLS) return; if (opened[r][c]) return; opened[r][c] true; if (mine[r][c] 0) { for (int di -1; di 1; di) for (int dj -1; dj 1; dj) openCell(r di, c dj); } } void display() { system(cls); cout ; for (int j 1; j COLS; j) cout j ; cout endl; for (int i 1; i ROWS; i) { cout i ; if (i 10) cout ; for (int j 1; j COLS; j) { if (!opened[i][j]) { cout * ; } else if (mine[i][j] -1) { cout M ; } else { cout mine[i][j] ; } } cout endl; } } int main() { srand(time(nullptr)); init(); bool gameOver false, win false; int openedCount 0; while (!gameOver) { display(); int r, c; cout 输入要翻开的格子(行 列空格分隔); cin r c; if (r 1 || r ROWS || c 1 || c COLS) { cout 输入错误 endl; continue; } if (opened[r][c]) { cout 这个格子已经打开过 endl; continue; } if (mine[r][c] -1) { opened[r][c] true; gameOver true; cout 踩雷了游戏结束。 endl; continue; } openCell(r, c); openedCount 0; for (int i 1; i ROWS; i) for (int j 1; j COLS; j) if (opened[i][j]) openedCount; if (openedCount ROWS * COLS - MINES) { gameOver true; win true; display(); cout 恭喜你赢了 endl; } } if (!win) { for (int i 1; i ROWS; i) for (int j 1; j COLS; j) if (mine[i][j] -1) opened[i][j] true; display(); } return 0; }4.3 最容易踩的坑数组越界与递归边界写扫雷最容易出的问题就是数组越界。如果你不把地图扩大一圈统计某个格子周围雷数的时候就要写一堆if (i 0 j 0)之类的判断非常容易漏而且越界访问带来的错误往往不报错只会在运行到某个奇怪的时候崩溃排查起来相当折磨。递归函数openCell是另一个容易出问题的地方。它的逻辑是翻开当前格子如果当前格子数字是0就递归翻开周围8个格子。这里必须加上opened[r][c]的判断否则会出现两个格子互相递归调用停不下来的情况。另外r ROWS || c COLS的边界判断也是必须的否则边缘的0区域会把程序带出数组范围。这个版本没有实现右键标记地雷的功能你可以自己加一个“标记”模式。解析输入的时候让玩家选择是“翻开”还是“标记”标记过的格子用?显示再选一次取消标记。这个功能对赢下游戏其实不是必须的但加上之后更接近真实扫雷。5. 贪吃蛇实时输入与碰撞检测的控制台实践5.1 游戏规则与几个关键设计点贪吃蛇是很多人写的第一个“动态”控制台游戏。蛇会自己往前移动你用WASD控制方向吃到食物就变长撞到墙壁或者撞到自己就结束。这里最关键的设计是用一个vectorPoint来保存蛇身头部在snake[0]尾部在snake.back()。每次移动在头部插入新位置如果吃到食物就不删尾部蛇就变长一格没吃到食物就删掉尾部蛇整体向前平移。这个“插入头部 删除尾部”的思路很简洁比用数组存每个格子的状态好写很多。还有一个新手容易忽视的点蛇不能180度掉头。如果不加限制蛇向右走的时候你按了左键蛇头会直接撞到自己的脖子然后游戏结束体验很糟糕。我在代码里加了一个方向判断只有新方向和当前方向不冲突时才会更新。5.2 完整源代码可直接复制运行的贪吃蛇#include iostream #include cstdlib #include ctime #include conio.h #include windows.h #include vector using namespace std; const int W 20, H 20; int dx[4] {0, 1, 0, -1}; int dy[4] {-1, 0, 1, 0}; struct Point { int x, y; }; vectorPoint snake; int dir 1; Point food; int score 0; void draw() { system(cls); for (int y 0; y H; y) { for (int x 0; x W; x) { if (x 0 || y 0 || x W - 1 || y H - 1) { cout #; } else { bool isHead (x snake[0].x y snake[0].y); bool isBody false; for (size_t i 1; i snake.size(); i) { if (x snake[i].x y snake[i].y) { isBody true; break; } } if (isHead) cout O; else if (isBody) cout o; else if (x food.x y food.y) cout *; else cout ; } } cout endl; } cout 得分 score 方向键: W/A/S/D endl; } void generateFood() { while (true) { food.x rand() % (W - 2) 1; food.y rand() % (H - 2) 1; bool onSnake false; for (size_t i 0; i snake.size(); i) { if (food.x snake[i].x food.y snake[i].y) { onSnake true; break; } } if (!onSnake) return; } } int main() { srand(time(nullptr)); snake.clear(); snake.push_back({W / 2, H / 2}); snake.push_back({W / 2 - 1, H / 2}); snake.push_back({W / 2 - 2, H / 2}); generateFood(); while (true) { if (_kbhit()) { char ch _getch(); int newDir dir; if (ch w || ch W) newDir 0; else if (ch d || ch D) newDir 1; else if (ch s || ch S) newDir 2; else if (ch a || ch A) newDir 3; if ((newDir 0 dir ! 2) || (newDir 1 dir ! 3) || (newDir 2 dir ! 0) || (newDir 3 dir ! 1)) { dir newDir; } } Point newHead {snake[0].x dx[dir], snake[0].y dy[dir]}; if (newHead.x 0 || newHead.y 0 || newHead.x W - 1 || newHead.y H - 1) break; bool hitSelf false; for (size_t i 0; i snake.size(); i) { if (newHead.x snake[i].x newHead.y snake[i].y) { hitSelf true; break; } } if (hitSelf) break; snake.insert(snake.begin(), newHead); if (newHead.x food.x newHead.y food.y) { score 10; generateFood(); } else { snake.pop_back(); } draw(); Sleep(150); } cout 游戏结束得分 score endl; return 0; }5.3 控制台刷新与节奏控制的实战细节_kbhit()是控制台游戏里很重要的一个函数它“非阻塞”地检测有没有键被按下。如果用cin等待输入蛇就不会自动移动用_kbhit()你才能做到“不按键时蛇一直往前走按键时改变方向”。每次循环读一次按键蛇就在那个方向上前进一格节奏由Sleep(150)控制这个值越小游戏越快。很多人第一次运行贪吃蛇会觉得画面闪烁严重原因是system(cls)每次把整个屏幕清掉再重绘速度一快就会有闪烁感。一个比较简单的优化办法是用SetConsoleCursorPosition把光标定位到左上角再逐行覆盖重绘而不是清屏。你可以自己封装一个gotoxy函数把draw()里的system(cls)改成光标定位闪烁会明显改善。关于游戏的节奏我建议把速度等级做成分段式每吃到5个食物Sleep的时间减少20毫秒最低降到30毫秒。这样游戏有成长曲线而不是从开头到结尾一个速度。6. 俄罗斯方块旋转、墙踢与消行的进阶挑战6.1 游戏规则与整体架构俄罗斯方块是这5个游戏里最复杂的。玩法是7种方块不断从顶部落下你可以左右移动、旋转、加速下落落到底部就会固定住。固定的方块堆满一整行就会消失如果方块堆到顶部游戏结束。我从一开始就决定把方块用4x4矩阵来表示每一种方块是一组01数字。比如I型方块在矩阵里是1111一行T型方块是第二行一个1加上第三行111。为什么不用坐标数组因为旋转的时候矩阵直接做转置特别方便坐标数组反而要在代码里写死各种旋转后的样子。游戏的主要数据结构是board[H][W]保存已经固定的方块当前下落方块用Tetromino结构体保存里面有形状矩阵和两个坐标。每次循环做三件事读取输入、尝试下落、绘制画面。落不下去就固定当前方块、消行、生成新方块。6.2 完整源代码可直接复制运行的俄罗斯方块#include iostream #include cstring #include cstdlib #include ctime #include conio.h #include windows.h using namespace std; const int W 10, H 20; int board[H][W] {0}; int shapes[7][4][4] { {{0,0,0,0},{1,1,1,1},{0,0,0,0},{0,0,0,0}}, // I {{0,0,0,0},{0,1,1,0},{0,1,1,0},{0,0,0,0}}, // O {{0,0,0,0},{0,1,0,0},{1,1,1,0},{0,0,0,0}}, // T {{0,0,0,0},{0,1,1,0},{1,1,0,0},{0,0,0,0}}, // S {{0,0,0,0},{1,1,0,0},{0,1,1,0},{0,0,0,0}}, // Z {{0,0,0,0},{1,0,0,0},{1,1,1,0},{0,0,0,0}}, // J {{0,0,0,0},{0,0,1,0},{1,1,1,0},{0,0,0,0}} // L }; struct Tetromino { int shape[4][4]; int x, y; Tetromino(int type) { for (int i 0; i 4; i) for (int j 0; j 4; j) shape[i][j] shapes[type][i][j]; x 3; y 0; } }; bool collide(const Tetromino t) { for (int i 0; i 4; i) { for (int j 0; j 4; j) { if (t.shape[i][j]) { int bx t.x j; int by t.y i; if (bx 0 || bx W || by H) return true; if (by 0 board[by][bx]) return true; } } } return false; } void place(const Tetromino t) { for (int i 0; i 4; i) { for (int j 0; j 4; j) { if (t.shape[i][j]) { int by t.y i; int bx t.x j; if (by 0 by H bx 0 bx W) board[by][bx] 1; } } } } void clearLines() { for (int i H - 1; i 0; i--) { bool full true; for (int j 0; j W; j) { if (board[i][j] 0) { full false; break; } } if (full) { for (int k i; k 0; k--) for (int j 0; j W; j) board[k][j] board[k - 1][j]; for (int j 0; j W; j) board[0][j] 0; i; } } } void draw(const Tetromino cur) { system(cls); for (int i 0; i H; i) { cout #; for (int j 0; j W; j) { bool hasBlock false; if (board[i][j]) hasBlock true; for (int r 0; r 4; r) for (int c 0; c 4; c) if (cur.shape[r][c] cur.y r i cur.x c j) hasBlock true; cout (hasBlock ? [] : ); } cout # endl; } cout a/d:左右移动 w:旋转 s:加速下落 q:退出 endl; } int main() { srand(time(nullptr)); Tetromino cur(rand() % 7); int fallSpeed 500; while (true) { if (_kbhit()) { char ch _getch(); if (ch a) { cur.x--; if (collide(cur)) cur.x; } else if (ch d) { cur.x; if (collide(cur)) cur.x--; } else if (ch w) { int tmp[4][4]; for (int i 0; i 4; i) for (int j 0; j 4; j) tmp[j][3 - i] cur.shape[i][j]; memcpy(cur.shape, tmp, sizeof(tmp)); if (collide(cur)) { cur.x; if (collide(cur)) cur.x - 2; if (collide(cur)) { cur.x; for (int i 0; i 4; i) for (int j 0; j 4; j) tmp[j][3 - i] cur.shape[i][j]; memcpy(cur.shape, tmp, sizeof(tmp)); } } } else if (ch s) { fallSpeed 30; } else if (ch q) { return 0; } } cur.y; if (collide(cur)) { cur.y--; place(cur); clearLines(); cur Tetromino(rand() % 7); if (collide(cur)) { system(cls); cout 游戏结束 endl; break; } fallSpeed 500; } draw(cur); Sleep(fallSpeed); } return 0; }6.3 旋转、墙踢与消行的高频坑位旋转的公式是tmp[j][3 - i] cur.shape[i][j]这是一个顺时针旋转90度的矩阵变换。你不需要死记这个公式理解方式很简单原来在第i行第j列的元素旋转后跑到第j行第(3-i)列。如果方块不是正方形这个公式也成立因为这里的4x4矩阵本质上是正方形。墙踢wall kick是指方块靠着墙旋转时旋转后的形状可能超出边界。我在代码里用的策略是旋转后如果碰撞先尝试右移一格不行就左移一格还是不行就放弃这次旋转。这个策略实现简单实战够用。真正的俄罗斯方块游戏墙踢规则要复杂得多SRSSuper Rotation System系统里对不同方块类型有不同的墙踢数据表但那是进阶话题了。消行代码里有个容易忽略的细节消掉一行后上面的所有行都要往下移然后我写了i来抵消for循环里的i--相当于重新检查这一行。为什么要这样因为如果连续两行都满了上面的行下移之后当前行可能又变成满的必须再查一次。这个i的写法非常经典也是很多新手调试半天才想明白的难点。7. 常见编译错误与运行环境问题排查7.1 编译报错速查表我根据平时帮别人看代码的经验把这几个游戏最容易遇到的编译错误整理成了一张表。报错信息原因解决方法system was not declared缺少cstdlib或stdlib.h在文件开头加#include cstdlib_kbhit was not declared缺少conio.h或者不是Windows环境加#include conio.hSleep was not declared缺少windows.h加#include windows.hmemcpy was not declared缺少cstring加#include cstringSetConsoleCursorPosition was not declared缺少windows.h加#include windows.herror C4996: strcpy: This function or variable may be unsafeVS的安全检查在文件开头加#define _CRT_SECURE_NO_WARNINGS中文乱码源文件编码和控制台编码不一致用GBK或UTF-8 BOM保存源文件这些报错基本都是“头文件没包含全”导致的。C不像Java那样自动导入所有库你不包含头文件编译器就不认识这个函数所以养成“用哪个函数就查哪个头文件”的习惯很重要。7.2 运行环境问题MSVCP140.dll与Visual C Redistributable很多人把自己编译好的exe发给别人别人双击时报错“找不到MSVCP140.dll”这是因为目标电脑缺少Visual C运行库。MSVCP140.dll对应的就是Visual C 2015-2022 Redistributable它是运行Visual C程序时必需的动态链接库。解决方案很简单去微软官网搜索“Visual C Redistributable”下载x64版本安装就行。如果电脑是32位系统还要同时装x86版本。这个安装包是免费的装完基本能解决大多数“缺dll”的问题。另外还有个常见情况在编译或安装某个Python扩展、C第三方库时报错“Microsoft Visual C 14.0 or greater is required. Get it with Microsoft C Build Tools”。这个不是缺运行库而是缺完整编译工具链。解决方法是安装VS Build Tools或者在Visual Studio Installer里勾选“使用C的桌面开发”工作负载把C编译环境补上。这个问题我见得太多了很多人卡了半天以为自己写的代码有问题其实纯粹是环境缺组件。7.3 我的调试与排查经验控制台小游戏的bug其实很好查因为你能看到每一步的输出。我自己的习惯是先用小数据把逻辑跑通再扩展到完整场景。比如扫雷我会先把雷数设置成1棋盘设成3x3手动输入坐标确认布雷、数字、递归展开都正常再改成9x9和10雷。贪吃蛇就先让蛇撞墙确认游戏结束逻辑正确再判断撞自己。俄罗斯方块的旋转算法我建议先在纸上把4x4矩阵写出来手动转一次再用代码验证不然光对着屏幕看很容易头晕。数组越界这种问题是最难查的因为它不一定崩溃有时候只是输出一些莫名其妙的值。遇到这种情况最有效的办法是把所有数组下标打印出来或者用调试器逐步走。不要靠猜控制台程序不像图形程序那样信息多但打印大法是永远的神。如果你运行exe后窗口一闪而过别急着怀疑代码那是程序正常跑完退出了。在main函数最后加一行system(pause)或者cin.get()让程序停在结束前就能看到结果了。这个坑很多人踩过但它其实不是程序错误。写这几个小游戏我最大的体会是代码比想象中容易写难的是把逻辑拆清楚。拿俄罗斯方块来说如果一开始就想着把所有功能一次性写完很容易写到一半就乱了。但当你把碰撞检测、旋转、消行拆成独立的函数每个函数只做一件事整个程序的思路就清晰多了。最后分享一个我练手时的做法不满足于代码跑通而是给每个游戏加一个自己设计的功能。比如给猜数字加难度选择给井字棋加胜负统计给扫雷加计时器。当你发现自己加功能时需要改动原来的结构说明你对程序的理解已经不止于语法层面了。这5个代码免费拿走但收获多少取决于你愿意动手改多少。C这门语言看十遍不如敲一遍希望你能从第一个猜数字开始把这条路走通。