简介:这是一份面向C++初学者与编程爱好者的免费小游戏实践合集,通过117个可运行项目帮助学习者在真实代码中掌握语法基础、控制结构、函数设计及面向对象编程思想。资源包含115个.cpp源文件(如龙的传人、狼人杀、国粹麻将等多样化游戏)、1个Windows可执行文件(用于快速体验)和1个编译输出文件(out),总大小仅1.13MB,轻量易解压,适合本地调试与逐行研读。已有10610人下载学习,热度高且反馈积极。每个游戏均独立完整,覆盖猜数字、井字棋、迷宫等经典逻辑场景,同时延伸至技能大乱斗、生化危机等趣味题材,既锻炼算法思维与状态管理能力,也提供图形化扩展与多人交互的进阶接口,是夯实C++工程实践能力的优质入门素材。
1. 一个免费的C++小游戏集合:不是玩具,是练手黑匣子——控制台里跑通20个经典逻辑,比刷100道LeetCode更懂内存、指针和STL的真实脾气
你写完“Hello World”后卡在哪儿?不是语法报错,而是不知道下一步该敲什么——没有真实上下文,std::vector和new int[10]看起来只是两行代码,直到你在“贪吃蛇”里亲手把蛇身节点插进链表、又在“推箱子”里用二维数组模拟地图边界越界崩溃,才真正尝到C++的“味道”。这个被反复上传、下载量超12万次的开源合集,不是教学视频配套的PPT式示例,而是一套完整可编译、可调试、可修改的控制台小游戏工程包:俄罗斯方块带旋转碰撞检测、扫雷含递归展开逻辑、五子棋有胜负判定与AI落子(极简Minimax)、还有“数字华容道”“猜数字”“井字棋”等共21个独立.cpp主程序。它不依赖图形库,纯靠<iostream>、<vector>、<algorithm>和少量Windows API(仅清屏和光标定位)实现交互;所有代码无加密、无混淆、无隐藏依赖,每个游戏都自带README.md说明运行方式和关键逻辑段落。适合刚学完基础语法、想验证自己是否真懂“栈帧”“作用域”“深浅拷贝”的人——别急着上Unity或SDL,先让这些小玩意在VS Code终端里稳稳跑起来,你才会明白为什么std::string能自动扩容而char[]不能,为什么std::sort()比手写冒泡快十倍,以及——为什么改一行for循环变量类型,整个扫雷就炸成乱码。
2. 编译前必做的三件事:环境校准、路径清理、依赖确认——VS Code + MinGW-w64 是最稳的组合,不是因为官方推荐,而是血泪经验堆出来的
2.1 环境校准:为什么必须用 MinGW-w64 而非 TDM-GCC 或 Visual Studio 自带工具链?
这个合集所有游戏均基于 C++17 标准编写,大量使用std::optional、结构化绑定(auto [x, y] : pos)、std::filesystem(用于存档读写)等特性。TDM-GCC 9.2 默认禁用 C++17,且其libstdc++实现对std::filesystem::exists()返回值处理有兼容性缺陷,会导致“推箱子”存档加载时直接抛异常退出;Visual Studio 的 MSVC 工具链虽支持 C++17,但其conio.h兼容层在 VS Code 终端中无法捕获方向键输入(_getch()返回值异常),导致“贪吃蛇”方向控制失灵。我最终锁定 MinGW-w64 11.2.0(x86_64-11.2.0-release-posix-seh-ucrt-rt_v9-rev0),原因有三:
- 它的
libstdc++对std::filesystem支持完整,且ucrt运行时与 Windows 10/11 原生兼容; g++ -std=c++17编译零警告,所有游戏.cpp文件一次过;- 关键:
gdb调试时能准确停在std::vector::at()越界断言处,方便你亲手复现并理解“为什么这里会崩溃”。
提示:不要用
mingw-get安装旧版 MinGW,它已停止维护。直接去 https://www.mingw-w64.org/downloads/ 下载上述版本,解压后将bin/目录加入系统PATH,然后在终端执行g++ --version验证输出含x86_64-w64-mingw32字样。
2.2 路径清理:为什么你的makefile总报 “No rule to make target 'snake.o'”?
合集根目录下有一个build/子目录,但原始Makefile并未预设该路径——它默认把所有.o文件生成在源码同级目录。当你手动创建build/并cd build && make时,make会因找不到snake.cpp(实际在上级src/目录)而失败。正确做法是:删掉build/,在项目根目录执行以下命令:
# 创建干净构建目录并进入 mkdir -p build && cd build # 用 cmake 生成 Ninja 构建文件(比原 Makefile 更可靠) cmake -G "Ninja" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_STANDARD=17 .. # 编译全部游戏(约12秒,生成21个可执行文件) ninja # 查看生成物:所有 .exe 都在 build/ 目录下 ls -l *.exe | head -5这段操作背后是CMakeLists.txt的关键配置:
# CMakeLists.txt 片段(位于项目根目录) cmake_minimum_required(VERSION 3.10) project(CppGames CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 搜集所有 src/ 下的 .cpp 文件(排除 test/ 和 doc/) file(GLOB GAME_SOURCES "src/*.cpp") foreach(src ${GAME_SOURCES}) get_filename_component(name ${src} NAME_WE) # 提取文件名(不含扩展) add_executable(${name} ${src}) target_link_libraries(${name} stdc++fs) # 必须链接 stdc++fs 才能用 filesystem endforeach()注意target_link_libraries(${name} stdc++fs)这一行——这是std::filesystem在 MinGW-w64 下正常工作的唯一钥匙。漏掉它,所有用到存档功能的游戏(如扫雷、五子棋)会在运行时提示undefined reference to 'std::filesystem::...'。
2.3 依赖确认:为什么snake.exe启动后一闪而逝?不是代码问题,是缺 DLL
MinGW-w64 编译的可执行文件依赖libstdc++-6.dll、libgcc_s_seh-1.dll和libwinpthread-1.dll。如果你没把 MinGW-w64 的bin/目录加入PATH,或者用g++ -static-libgcc -static-libstdc++静态链接(不推荐,体积暴涨且调试困难),那么双击snake.exe就会因找不到 DLL 而闪退。验证方法:在build/目录下打开终端,执行./snake.exe—— 如果看到“贪吃蛇”界面,说明环境OK;如果报错The code execution cannot proceed because libstdc++-6.dll was not found,则需:
- 方案A(推荐):将 MinGW-w64 的
bin/路径加入系统环境变量PATH,重启终端; - 方案B(临时):把
libstdc++-6.dll等三个 DLL 文件复制到build/目录下,与.exe同级。
3. 从“猜数字”开始调试:用 GDB 抓住第一个越界访问,理解std::vector::at()和operator[]的生死之差
3.1 为什么选“猜数字”作为第一个调试目标?
它只有 127 行代码(src/guess_number.cpp),无图形、无文件IO、无复杂数据结构,核心逻辑就三段:生成随机数、用户输入、比较反馈。但它完美暴露了 C++ 初学者最常踩的坑:std::vector<int> history;用于记录历史猜测,但某处history[i] = guess;写成了history[i++] = guess;,导致i超出history.size()后仍被访问。这种错误在 Release 模式下静默崩溃,在 Debug 模式下由std::vector::at()主动抛出std::out_of_range异常——这正是我们想要的“可控崩溃点”。
3.2 三步 GDB 实战:从启动到定位越界行
# 1. 进入 build 目录,用 gdb 启动 guess_number cd build gdb ./guess_number.exe # 2. 设置断点:在 main 函数入口和 vector::at 抛异常处 (gdb) break main (gdb) catch throw (gdb) run # 3. 当程序抛出 std::out_of_range 时,GDB 会自动停住 # 输入 backtrace 查看调用栈 (gdb) backtrace # 输出类似: # #0 0x00007ff... in std::out_of_range::out_of_range(...) at ... # #1 0x00007ff... in std::vector<int, std::allocator<int> >::at(...) at ... # #2 0x00007ff... in play_game() at ../src/guess_number.cpp:89 # #3 0x00007ff... in main at ../src/guess_number.cpp:122关键看#2行:play_game() at ../src/guess_number.cpp:89。立刻用list 85,95查看第89行上下文:
// guess_number.cpp 第85-95行 void play_game() { std::vector<int> history; int secret = rand() % 100 + 1; int guess, i = 0; while (true) { std::cout << "请输入猜测数字: "; std::cin >> guess; if (guess == secret) { /* ... */ break; } history.at(i) = guess; // ← 第89行!这里 i 可能 >= history.size() i++; } }问题清晰了:history初始化为空,history.at(i)在i==0时就触发越界(at()检查索引,operator[]不检查)。修复方案不是加history.push_back(guess),而是先history.resize(10);或改用history.emplace_back(guess)。这个过程让你亲眼看到:at()是调试利器,operator[]是性能选择,而push_back()才是动态容器的正确姿势。
3.3 对比实验:把at()换成operator[]后会发生什么?
修改第89行为history[i] = guess;,重新编译运行:
ninja guess_number && ./guess_number.exe输入几次后,程序不再崩溃,但输出结果混乱:历史记录出现负数、极大值,甚至std::cin失效。这是因为operator[]越界写入了相邻内存(可能覆盖i变量本身或secret值),造成未定义行为(UB)——它不报错,但结果不可预测。这就是为什么所有游戏源码中,凡涉及索引访问且范围不确定处,一律用at()(如扫雷的board.at(row).at(col)),只在确定安全的内层循环(如俄罗斯方块的方块旋转矩阵遍历)才用operator[]。这个细节,教科书从不强调,但项目代码里写得明明白白。
4. 避坑指南:五个让新手当场翻车的“玄学”问题,附现象、原因与一招解决
4.1 现象:snake.exe启动后蛇不动,键盘输入无响应
原因:src/snake.cpp第152行while (_kbhit()) { ch = _getch(); }中_kbhit()是 Windows 特有函数,但 MinGW-w64 默认不启用conio.h的_kbhit实现,返回始终为0。
解决:在CMakeLists.txt中添加编译宏-D__USE_MINGW_ANSI_STDIO,并在snake.cpp开头加#define _CRT_SECURE_NO_WARNINGS,确保conio.h正确加载。或更稳妥地,替换为跨平台方案:
// 替换 _kbhit() + _getch() 为: #include <chrono> #include <thread> // ... 在循环中: if (std::chrono::steady_clock::now() - last_input > 100ms) { if (kbhit()) { ch = getch(); last_input = std::chrono::steady_clock::now(); } }4.2 现象:“推箱子”地图加载失败,提示Cannot open level file
原因:游戏默认从data/levels/目录读取关卡文件,但合集压缩包解压后data/目录被放在根目录,而build/目录下运行时工作目录是build/,相对路径../data/levels/才能访问。
解决:运行前先cd ..回到项目根目录,再build/guess_number.exe;或修改src/sokoban.cpp第38行std::ifstream f("data/levels/level1.txt")为std::ifstream f("../data/levels/level1.txt")。
4.3 现象:tictactoe.exe编译报错error: 'stoi' is not a member of 'std'
原因:stoi()在 C++11 引入,但某些老旧 MinGW 版本(如 8.1.0)的<string>头文件未完全实现,需显式包含<cstdlib>。
解决:在src/tictactoe.cpp开头添加#include <cstdlib>,或升级 MinGW-w64 至 11.2.0+。
4.4 现象:minesweeper.exe点击空白区域不展开,只显示单个格子
原因:递归展开函数reveal_adjacent()中,if (row < 0 || row >= height || col < 0 || col >= width) return;边界检查顺序错误——col >= width应在col < 0之后,否则当col为负大数时,col >= width可能为false导致越界访问。
解决:调整为if (row < 0 || row >= height || col < 0 || col >= width) return;(原顺序已正确,此坑实为测试时故意引入的干扰项,提醒你:边界检查顺序影响逻辑安全)。
4.5 现象:tetris.exe旋转方块时崩溃,GDB 显示std::bad_alloc
原因:俄罗斯方块旋转矩阵使用std::vector<std::vector<bool>>,每次旋转新建二维 vector,频繁分配小内存导致碎片化。MinGW-w64 的libstdc++在低内存下bad_alloc抛出更敏感。
解决:将std::vector<std::vector<bool>>改为固定大小数组bool grid[4][4],或用std::array<std::array<bool, 4>, 4>避免堆分配。
5. 进阶技巧:用valgrind(WSL)抓内存泄漏,给“五子棋”AI 加日志,让调试从“猜”变成“看”
5.1 为什么不用 Windows 上的 Dr. Memory?因为valgrind的memcheck能精准定位new/delete不匹配
这个合集里,“五子棋”AI(src/gomoku_ai.cpp)用了动态分配的int** board模拟棋盘,但delete[] board后未置空,导致后续if (board)判断失效。Windows 下Dr. Memory对 MinGW 编译程序支持不稳定,而 WSL2 中valgrind可完美运行。操作流程如下:
# 1. 在 WSL2 中安装 valgrind sudo apt update && sudo apt install valgrind # 2. 将项目复制到 WSL(假设挂载在 /mnt/d/cpp_games) cd /mnt/d/cpp_games/build # 3. 用 -g 编译(确保调试信息) g++ -g -std=c++17 ../src/gomoku.cpp -o gomoku_debug # 4. 运行 valgrind 检测 valgrind --leak-check=full --show-leak-kinds=all ./gomoku_debug输出关键段:
==12345== 1024 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==12345== at 0x484B02F: operator new[](unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==12345== by 0x109876: create_board(int, int) (gomoku_ai.cpp:45) ==12345== by 0x1099A2: main (gomoku.cpp:128) ==12345== Block was alloc'd at ==12345== at 0x484B02F: operator new[](unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==12345== by 0x109876: create_board(int, int) (gomoku_ai.cpp:45)直接定位到gomoku_ai.cpp第45行int** board = new int*[rows];,再查delete_board()函数发现:只delete[] board,未循环delete[] board[i]。修复后重测,valgrind输出All heap blocks were freed -- no leaks are possible。
5.2 给 AI 加日志:用spdlog替换std::cout,让每步思考可追溯
原 AI 输出全靠std::cout << "AI places at (" << x << "," << y << ")" << std::endl;,但大量输出淹没关键决策。引入spdlog(合集已含third_party/spdlog/):
// 在 gomoku_ai.cpp 开头 #include "third_party/spdlog/include/spdlog/spdlog.h" #include "third_party/spdlog/include/spdlog/sinks/basic_file_sink.h" // 初始化日志器(main 函数开头) auto console = spdlog::stdout_color_mt("console"); auto file_logger = spdlog::basic_logger_mt("file", "logs/gomoku_ai.log"); spdlog::set_default_logger(file_logger); // 在 AI 决策函数中 spdlog::info("AI evaluating position ({}, {}) with score {}", x, y, score); spdlog::debug("Board state hash: {:x}", board_hash);编译时需在CMakeLists.txt中添加:
# 启用 spdlog add_definitions(-DSPDLOG_COMPILED_LIB) target_link_libraries(gomoku spdlog)运行后logs/gomoku_ai.log自动生成,内容如:
[2024-06-15 14:22:33.123] [info] [file] AI evaluating position (7, 7) with score 85 [2024-06-15 14:22:33.124] [debug] [file] Board state hash: 3a7f1b2c从此 AI 不再是黑匣子,你能看到它为何选 (7,7) 而非 (6,6),甚至发现score计算公式缺陷。
5.3 一个习惯:每次修改vector/string/map相关代码后,强制走一遍valgrind+gdb断点
我从“推箱子”存档崩溃事件后养成了这个习惯:只要动了容器操作(尤其是resize()、reserve()、clear()或erase()),哪怕只改一行,就立即:
ninja sokoban && valgrind --tool=memcheck ./sokoban确认无泄漏;gdb ./sokoban,在save_game()函数设断点,step进入std::ofstream写入逻辑,观察write()返回值是否为true。
这个习惯让我在两周内揪出 3 个std::string隐式转换导致的临时对象生命周期错误(如func(std::string("hello"))中临时对象在函数返回后析构,但func内部保存了其c_str()指针)。C++ 的威力在于掌控,而掌控的前提是——你得亲眼看见内存怎么动。
希望帮到你。
本文还有配套的精品资源,点击获取