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++ 的學習是一個循序漸進的過程。多動手,多思考,多嘗試,你就能不斷突破,駕馭這門強大的語言,創造出更多精彩的作品!