1. 引言
介于身边有特别多没有学习过编程,或者有一定C语言、python或是Java基础的但是没有接触过C++的新手朋友,我想可以通过一个很简单的小项目作为挑战,帮助大家入门C++。
今天,我们将挑战一个对新手来说稍微复杂一点,但非常适合新手的经典小游戏——**井字棋 **!通过这个项目,你将学习到 C++ 中更重要的概念,让你的编程能力再上一个台阶!
这个项目将带你:
- 深入理解函数: 如何定义、调用函数,以及参数传递(包括引用传递)。
- 掌握二维数据结构: 使用
std::vector
来模拟棋盘。 - 设计更复杂的程序逻辑: 判断输赢、平局等游戏状态。
- 提升代码组织能力: 将不同功能封装到独立的函数中。
如何学习
在编写的过程中可能会遇到之前没有接触过的知识点,这时候不要只是跟着案例敲,一个优秀的程序员应该理解自己所写的代码,而不是一味地对着抄,或是复制粘贴。对于不懂的代码:
- 使用AI工具(例如deepseek)解读
- 查阅相关手册或博客文章
准备好了吗?让我们开始这场新的编程冒险吧!
2. 项目概览:井字棋游戏
游戏规则:
- 井字棋在 3x3 的棋盘上进行。
- 两名玩家轮流落子,一个用 ‘X’,一个用 ‘O’。
- 玩家选择一个空位进行落子(通常通过输入行和列的数字)。
- 第一个在横线、竖线或对角线上连成三个自己的棋子的玩家获胜。
- 如果棋盘填满,但没有人获胜,则游戏平局。
游戏界面示例(命令行):
1 | 2 | 3
---+---+---4 | 5 | 6
---+---+---7 | 8 | 9玩家 X 的回合。请输入你的选择 (1-9):
3. 环境准备
你需要:
- 一个 C++ 编译器: GCC (MinGW)、MSVC (Visual Studio)、Clang 等。(如果不知道什么是编译器记得先简单了解一下)
- 一个代码编辑器或 IDE: VS Code、Visual Studio、CLion 等。(推荐:Visual Studio)
VisualStudio2022使用教程与安装
确保你的环境能够编译和运行 C++ 程序。
4. 逐步实现:井字棋游戏
我们将把游戏的不同功能拆分成独立的函数,这样代码会更清晰、更容易维护。
4.1 核心数据结构:棋盘
井字棋棋盘是一个 3x3 的网格。在 C++ 中,我们可以使用二维 std::vector
来表示它。std::vector
是 C++ 标准库提供的动态数组,比 C 风格数组更安全、更灵活。
#include <iostream> // 用于输入输出
#include <vector> // 用于 std::vector
#include <limits> // 用于 std::numeric_limits (处理输入错误)// 为了方便,我们在这里使用整个 std 命名空间
using namespace std;// 定义棋盘
// vector<vector<char>> 表示一个二维字符向量
// 初始时,每个格子都用 ' ' (空格) 表示空位
vector<vector<char>> board = {{' ', ' ', ' '},{' ', ' ', ' '},{' ', ' ', ' '}
};// 当前玩家 ('X' 或 'O')
char currentPlayer = 'X';// 游戏是否结束
bool gameOver = false;int main() {// 游戏主逻辑将在这里实现return 0;
}
4.2 函数一:显示棋盘 displayBoard()
这个函数负责将当前的棋盘状态打印到控制台,让玩家看到。
// 函数:显示棋盘
void displayBoard() {system("cls"); // Windows 清屏命令,Linux/macOS 可以用 system("clear");// 或者注释掉,不清屏也行cout << "--- 井字棋 ---" << endl;cout << "-------------" << endl;for (int i = 0; i < 3; ++i) {for (int j = 0; j < 3; ++j) {cout << " " << board[i][j]; // 打印棋子if (j < 2) {cout << " |"; // 打印列分隔符}}cout << endl;if (i < 2) {cout << "---+---+---" << endl; // 打印行分隔符}}cout << "-------------" << endl;
}
4.3 函数二:玩家落子 playerMove()
这个函数负责接收玩家的输入,并更新棋盘。它需要处理用户输入、检查输入是否合法(是否在范围内、是否为空位)。
// 函数:玩家落子
// 参数 board 用引用传递 (&),这样函数内部对 board 的修改会直接反映到外部的 board 变量
void playerMove(vector<vector<char>>& currentBoard, char player) {int choice;int row, col;while (true) {cout << "玩家 " << player << " 的回合。请输入你的选择 (1-9): ";cin >> choice;// 检查输入是否为数字,以及是否在有效范围内if (cin.fail() || choice < 1 || choice > 9) {cout << "无效输入!请输入 1 到 9 之间的数字。" << endl;cin.clear(); // 清除错误标志// 忽略输入缓冲区中剩余的字符,直到换行符cin.ignore(numeric_limits<streamsize>::max(), '\n');continue; // 继续循环,要求重新输入}// 将 1-9 的选择转换为二维数组的行和列row = (choice - 1) / 3;col = (choice - 1) % 3;// 检查选择的格子是否为空if (currentBoard[row][col] == ' ') {currentBoard[row][col] = player; // 落子break; // 输入合法,跳出循环} else {cout << "这个位置已经被占用了!请选择其他位置。" << endl;}}
}
新知识点:
- 引用传递 (
&
):playerMove(vector<vector<char>>& currentBoard, char player)
中的&
符号表示currentBoard
是一个引用。这意味着函数内部对currentBoard
的修改,会直接影响到main
函数中传入的board
变量,而不是它的一个副本。这对于需要修改外部变量的函数非常有用。 cin.fail()
和cin.clear()
: 用于处理非数字输入错误。cin.fail()
检查输入流是否处于错误状态,cin.clear()
清除错误标志,cin.ignore()
丢弃错误输入。numeric_limits<streamsize>::max()
: 表示流的最大尺寸,配合cin.ignore
丢弃所有剩余输入。
4.4 函数三:检查胜利 checkWin()
这个函数判断当前棋盘上是否有玩家获胜。
// 函数:检查是否有玩家获胜
// 参数 currentBoard 用常量引用传递 (const &),表示函数只读取 board 的内容,不修改它
bool checkWin(const vector<vector<char>>& currentBoard, char player) {// 检查行for (int i = 0; i < 3; ++i) {if (currentBoard[i][0] == player && currentBoard[i][1] == player && currentBoard[i][2] == player) {return true;}}// 检查列for (int j = 0; j < 3; ++j) {if (currentBoard[0][j] == player && currentBoard[1][j] == player && currentBoard[2][j] == player) {return true;}}// 检查主对角线if (currentBoard[0][0] == player && currentBoard[1][1] == player && currentBoard[2][2] == player) {return true;}// 检查副对角线if (currentBoard[0][2] == player && currentBoard[1][1] == player && currentBoard[2][0] == player) {return true;}return false; // 没有获胜
}
新知识点:
- 常量引用 (
const &
):checkWin(const vector<vector<char>>& currentBoard, char player)
中的const &
表示currentBoard
是一个引用,但函数内部不能修改currentBoard
的内容。这是一种很好的编程习惯,既避免了不必要的拷贝,又保证了数据的安全性。 bool
返回值: 函数返回true
或false
,表示是否获胜。
4.5 函数四:检查平局 checkDraw()
这个函数判断棋盘是否已满,且没有玩家获胜。
// 函数:检查是否平局
bool checkDraw(const vector<vector<char>>& currentBoard) {for (int i = 0; i < 3; ++i) {for (int j = 0; j < 3; ++j) {if (currentBoard[i][j] == ' ') {return false; // 还有空位,不是平局}}}return true; // 没有空位,是平局
}
4.6 游戏主逻辑 main()
现在,我们将所有函数组合起来,构建游戏的主循环。
int main() {cout << "-----------------------------------" << endl;cout << " 欢迎来到井字棋! " << endl;cout << "-----------------------------------" << endl;// 游戏主循环while (!gameOver) {displayBoard(); // 显示棋盘// 玩家落子playerMove(board, currentPlayer);// 检查胜利if (checkWin(board, currentPlayer)) {displayBoard(); // 胜利后再次显示最终棋盘cout << "恭喜玩家 " << currentPlayer << " 获胜!" << endl;gameOver = true; // 游戏结束}// 检查平局(只有在没有胜利者的情况下才检查平局)else if (checkDraw(board)) {displayBoard(); // 平局后显示最终棋盘cout << "游戏平局!" << endl;gameOver = true; // 游戏结束}// 切换玩家else {currentPlayer = (currentPlayer == 'X') ? 'O' : 'X'; // 三元运算符:如果当前是X,则切换到O,否则切换到X}}cout << "游戏结束!感谢游玩!" << endl;return 0;
}
5. 完整代码
现在,将所有代码片段组合起来,你的 main.cpp
文件应该长这样:
#include <iostream> // 用于标准输入输出
#include <vector> // 用于 std::vector
#include <limits> // 用于 std::numeric_limits (处理输入错误)
#include <string> // 用于 std::string (如果需要)
// #include <windows.h> // 如果使用 system("cls") 且在 Windows 环境下// 为了方便,我们在这里使用整个 std 命名空间
using namespace std;// 定义棋盘 (全局变量,简化示例)
vector<vector<char>> board = {{' ', ' ', ' '},{' ', ' ', ' '},{' ', ' ', ' '}
};// 当前玩家 ('X' 或 'O')
char currentPlayer = 'X';// 游戏是否结束
bool gameOver = false;// --- 函数声明 (良好的编程习惯,先声明再定义) ---
void displayBoard();
void playerMove(vector<vector<char>>& currentBoard, char player);
bool checkWin(const vector<vector<char>>& currentBoard, char player);
bool checkDraw(const vector<vector<char>>& currentBoard);// --- 函数定义 ---// 函数:显示棋盘
void displayBoard() {// Windows 清屏命令,Linux/macOS 可以用 system("clear");// 如果不想清屏,可以注释掉这行// system("cls"); cout << "\n--- 井字棋 ---" << endl;cout << "-------------" << endl;for (int i = 0; i < 3; ++i) {for (int j = 0; j < 3; ++j) {cout << " " << board[i][j]; // 打印棋子if (j < 2) {cout << " |"; // 打印列分隔符}}cout << endl;if (i < 2) {cout << "---+---+---" << endl; // 打印行分隔符}}cout << "-------------" << endl;
}// 函数:玩家落子
void playerMove(vector<vector<char>>& currentBoard, char player) {int choice;int row, col;while (true) {cout << "玩家 " << player << " 的回合。请输入你的选择 (1-9): ";cin >> choice;// 检查输入是否为数字,以及是否在有效范围内if (cin.fail() || choice < 1 || choice > 9) {cout << "无效输入!请输入 1 到 9 之间的数字。" << endl;cin.clear(); // 清除错误标志// 忽略输入缓冲区中剩余的字符,直到换行符cin.ignore(numeric_limits<streamsize>::max(), '\n');continue; // 继续循环,要求重新输入}// 将 1-9 的选择转换为二维数组的行和列// 1 -> (0,0), 2 -> (0,1), 3 -> (0,2)// 4 -> (1,0), 5 -> (1,1), 6 -> (1,2)// 7 -> (2,0), 8 -> (2,1), 9 -> (2,2)row = (choice - 1) / 3;col = (choice - 1) % 3;// 检查选择的格子是否为空if (currentBoard[row][col] == ' ') {currentBoard[row][col] = player; // 落子break; // 输入合法,跳出循环} else {cout << "这个位置已经被占用了!请选择其他位置。" << endl;}}
}// 函数:检查是否有玩家获胜
bool checkWin(const vector<vector<char>>& currentBoard, char player) {// 检查行for (int i = 0; i < 3; ++i) {if (currentBoard[i][0] == player && currentBoard[i][1] == player && currentBoard[i][2] == player) {return true;}}// 检查列for (int j = 0; j < 3; ++j) {if (currentBoard[0][j] == player && currentBoard[1][j] == player && currentBoard[2][j] == player) {return true;}}// 检查主对角线 (左上到右下)if (currentBoard[0][0] == player && currentBoard[1][1] == player && currentBoard[2][2] == player) {return true;}// 检查副对角线 (右上到左下)if (currentBoard[0][2] == player && currentBoard[1][1] == player && currentBoard[2][0] == player) {return true;}return false; // 没有获胜
}// 函数:检查是否平局
bool checkDraw(const vector<vector<char>>& currentBoard) {for (int i = 0; i < 3; ++i) {for (int j = 0; j < 3; ++j) {if (currentBoard[i][j] == ' ') {return false; // 还有空位,不是平局}}}return true; // 没有空位,是平局
}int main() {cout << "-----------------------------------" << endl;cout << " 欢迎来到井字棋! " << endl;cout << "-----------------------------------" << endl;// 游戏主循环while (!gameOver) {displayBoard(); // 显示棋盘// 玩家落子playerMove(board, currentPlayer);// 检查胜利if (checkWin(board, currentPlayer)) {displayBoard(); // 胜利后再次显示最终棋盘cout << "恭喜玩家 " << currentPlayer << " 获胜!" << endl;gameOver = true; // 游戏结束}// 检查平局(只有在没有胜利者的情况下才检查平局)else if (checkDraw(board)) {displayBoard(); // 平局后显示最终棋盘cout << "游戏平局!" << endl;gameOver = true; // 游戏结束}// 切换玩家else {// 三元运算符:如果当前是X,则切换到O,否则切换到XcurrentPlayer = (currentPlayer == 'X') ? 'O' : 'X';}}cout << "游戏结束!感谢游玩!" << endl;return 0; // 程序正常退出
}
6. 编译和运行你的游戏
6.1 使用命令行编译 (以 GCC 为例)
-
打开你的命令行工具(Windows 下可以是 Git Bash、CMD、PowerShell,macOS/Linux 下是 Terminal)。
-
使用
cd
命令进入你存放main.cpp
的文件夹。cd path/to/your/TicTacToeGame
-
编译
main.cpp
:g++ main.cpp -o tictactoe
g++
是 C++ 编译器的命令。main.cpp
是你的源文件。-o tictactoe
指定编译生成的可执行文件名为tictactoe
(Windows 下会自动添加.exe
后缀)。
-
运行你的游戏:
./tictactoe # Linux/macOS tictactoe.exe # Windows
6.2 使用 IDE 编译和运行
如果你使用 Visual Studio、VS Code 或其他 IDE,通常可以直接点击“运行”或“构建并运行”按钮,IDE 会自动帮你完成编译和运行的步骤。
7. 运行效果示例
-----------------------------------欢迎来到井字棋!
-------------------------------------- 井字棋 ---
-------------| |
---+---+---| |
---+---+---| |
-------------
玩家 X 的回合。请输入你的选择 (1-9): 5--- 井字棋 ---
-------------| |
---+---+---| X |
---+---+---| |
-------------
玩家 O 的回合。请输入你的选择 (1-9): 1--- 井字棋 ---
-------------O | |
---+---+---| X |
---+---+---| |
-------------
玩家 X 的回合。请输入你的选择 (1-9): 9--- 井字棋 ---
-------------O | |
---+---+---| X |
---+---+---| | X
-------------
玩家 O 的回合。请输入你的选择 (1-9): 2--- 井字棋 ---
-------------O | O |
---+---+---| X |
---+---+---| | X
-------------
玩家 X 的回合。请输入你的选择 (1-9): 8--- 井字棋 ---
-------------O | O |
---+---+---| X |
---+---+---| X | X
-------------
玩家 O 的回合。请输入你的选择 (1-9): 3--- 井字棋 ---
-------------O | O | O
---+---+---| X |
---+---+---| X | X
-------------
恭喜玩家 O 获胜!
游戏结束!感谢游玩!
8. 知识点回顾与进阶
通过这个井字棋项目,我们学习并实践了以下 C++ 核心概念:
std::vector
: 使用vector<vector<char>>
来表示二维棋盘,这是 C++ 中处理动态数组和多维数据的重要方式。- 函数(Function): 将程序拆分成
displayBoard
、playerMove
、checkWin
、checkDraw
等独立函数,提高了代码的模块化、可读性和可维护性。 - 函数参数传递:
- 值传递: 默认方式,传递参数的副本。
- 引用传递 (
&
): 允许函数修改外部变量(如playerMove
修改board
),避免不必要的拷贝。 - 常量引用 (
const &
): 允许函数高效地读取外部变量,但不允许修改(如checkWin
、checkDraw
读取board
),兼顾效率和安全性。
bool
类型与逻辑判断:checkWin
和checkDraw
函数返回bool
值,用于控制游戏流程。- 健壮的输入处理: 使用
cin.fail()
、cin.clear()
和cin.ignore()
来处理非数字输入和输入缓冲区问题,使程序更稳定。 - 三元运算符 (
? :
): 简洁地实现玩家切换currentPlayer = (currentPlayer == 'X') ? 'O' : 'X';
。
进阶挑战:自己尝试完成
- 重置游戏: 在游戏结束后,询问玩家是否“再玩一次”,如果选择是,则重置棋盘和玩家状态。
- 人机对战 (AI): 实现一个简单的电脑玩家。
- 初级 AI: 随机选择一个空位落子。
- 中级 AI: 优先选择能赢的位置,其次选择能阻止对手赢的位置。
- 统计分数: 记录玩家 X 和玩家 O 的胜场次数。
- 优化棋盘显示: 可以考虑用数字 1-9 来表示每个格子,方便玩家输入。
- 结构体/类封装: 将棋盘、当前玩家、游戏状态等数据封装到一个
Game
结构体或类中,进一步提升代码的面向对象特性。
9. 总结:C++ 进阶的里程碑!
恭喜你!你已经成功完成了你的第一个 C++ 进阶小游戏项目——井字棋!这标志着你对 C++ 的理解又深入了一步。
通过这个项目,你不仅掌握了 std::vector
、函数的运用、引用传递等重要概念,更重要的是,你学会了如何将一个相对复杂的程序拆解成更小、更易于管理的部分,这是软件开发中非常重要的能力。
C++ 的学习是一个循序渐进的过程。多动手,多思考,多尝试,你就能不断突破,驾驭这门强大的语言,创造出更多精彩的作品!