摘要 :本文不仅是一个五子棋游戏的实现教程,更是一次对 C++ 底层内存管理 、指针操作 以及程序健壮性设计的深度实践。我们将摒弃简单的"功能堆砌",从工程化角度出发,重点解决动态二维数组的内存泄漏问题、非法输入导致的死循环陷阱,并优化胜负判定算法。适合希望从"写代码"进阶到"做工程"的开发者阅读。
🎯 1. 项目背景与工程价值
在面试和实际开发中,五子棋往往被视为一个经典的综合性测试题。它看似简单,实则考察了候选人对以下核心能力的掌握程度:
- 内存管理能力 :能否正确使用
malloc/free或new/delete管理动态资源,避免内存泄漏(Memory Leak)和悬空指针。 - 边界控制意识:在处理数组访问时,是否具备严格的边界检查习惯,防止段错误(Segmentation Fault)。
- 异常处理思维:面对用户的非法输入(如输入字符而非数字),程序是否会崩溃或陷入死循环?
- 算法效率优化:是每次全盘扫描 O(N2)O(N2) ,还是基于落子点的局部检测 O(1)O(1) ?
本项目目标 :构建一个健壮、无内存泄漏、交互友好的控制台五子棋引擎。
运行效果预览:
text
bash
1 2 3 4 5
1 * * * * *
2 * * A * *
3 * * B A *
4 * * * B *
5 * * * * *
现在是玩家 [A] 执棋阶段
请玩家 [A] 输入棋子位置【行 列】:
🛠️ 2. 核心数据结构:动态二维数组的深度解析
2.1 为什么选择 char** 而非固定数组?
在实际工程中,棋盘大小往往是可配置的。使用固定数组 char board[15][15] 缺乏灵活性。我们采用指针的指针 char** 来模拟动态二维数组,这能让我们更直观地理解堆区内存的布局。
2.2 "两次分配"策略与内存布局
很多初学者容易混淆一维数组映射和真正的二维指针数组。标准的动态二维数组创建需要两步:
- 一级分配:创建一个指针数组,每个元素指向一行。
- 二级分配:为每一行分配具体的数据空间。
cpp
cpp
char** CreateArray(int row, int col) {
// 1. 分配行指针数组 (row 个 char* 指针)
char** chess = (char**)malloc(row * sizeof(char*));
if (!chess) return NULL;
for (int i = 0; i < row; i++) {
// 2. 为每一行分配数据空间 (col 个 char)
chess[i] = (char*)malloc(col * sizeof(char));
if (!chess[i]) {
// 异常安全:如果中间某行分配失败,释放之前已分配的行
for (int k = 0; k < i; k++) free(chess[k]);
free(chess);
return NULL;
}
// 初始化棋盘
for (int j = 0; j < col; j++) {
chess[i][j] = '*';
}
}
return chess;
}
💡 面试官考点 :这种结构在内存中是不连续的。
chess[0]和chess[1]指向的内存块可能相距很远。这与vector<vector<char>>类似,但比一维大数组char* board = new char[row*col]访问速度稍慢(因为多了一次指针解引用),但代码可读性更好。
⚔️ 3. 核心算法: O(1)O(1) 局部胜负判定
3.1 算法优化思路
naive 做法 :每次落子后遍历整个棋盘,时间复杂度 O(R×C)O(R×C) 。
优化做法 :只检查最后落子的位置。因为新形成的五连珠必然包含最后这一步。我们以落子点 (a,b)(a,b) 为中心,向四个方向辐射检测。
3.2 四方向辐射检测实现
我们定义四个轴向:水平、垂直、主对角线、副对角线。对于每个轴,分别向正负两个方向统计连续相同棋子的数量。
cpp
cpp
int IsWin(char** arr, int row, int col, int a, int b) {
char win = arr[a][b];
int count;
// 方向1:水平 (左右)
count = 0;
for (int i = b - 1; i >= 0 && arr[a][i] == win; i--) count++;
for (int i = b + 1; i < col && arr[a][i] == win; i++) count++;
if (count >= 4) return 1; // 加上自身共5个
// 方向2:垂直 (上下)
count = 0;
for (int i = a - 1; i >= 0 && arr[i][b] == win; i--) count++;
for (int i = a + 1; i < row && arr[i][b] == win; i++) count++;
if (count >= 4) return 1;
// 方向3:副对角线 / (左下-右上)
count = 0;
for (int i = a + 1, j = b - 1; i < row && j >= 0 && arr[i][j] == win; i++, j--) count++;
for (int i = a - 1, j = b + 1; i >= 0 && j < col && arr[i][j] == win; i--, j++) count++;
if (count >= 4) return 1;
// 方向4:主对角线 \ (左上-右下)
count = 0;
for (int i = a - 1, j = b - 1; i >= 0 && j >= 0 && arr[i][j] == win; i--, j--) count++;
for (int i = a + 1, j = b + 1; i < row && j < col && arr[i][j] == win; i++, j++) count++;
if (count >= 4) return 1;
return 0;
}
🧹 4. 工程难点突破:健壮性与内存安全
4.1 内存泄漏的"铁律"
在 C/C++ 中,动态分配的内存必须手动释放。对于二维指针数组,释放顺序必须与分配顺序严格相反:
- 先释放内部的数据行。
- 再释放外部的指针数组。
❌ 常见错误:
cpp
cpp
free(chess); // 仅释放了指针数组,导致 row 个数据行内存泄漏!
✅ 正确实现:
cpp
cpp
void FreeArray(char** arr, int row) {
if (arr == NULL) return;
for (int i = 0; i < row; i++) {
free(arr[i]); // 1. 释放每一行
}
free(arr); // 2. 释放总指针
}
4.2 输入流的"死循环"陷阱
当 cin >> int_var 遇到非数字字符(如 'a')时:
cin进入fail状态。- 非法字符保留在缓冲区中。
- 下次循环再次读取时,立即读到同一个非法字符,再次失败,形成死循环。
✅ 解决方案:
cpp
cpp
if (!(cin >> a >> b)) {
cout << "输入格式错误,请输入整数" << endl;
cin.clear(); // 1. 清除错误标志位,恢复 cin 可用状态
// 2. 丢弃缓冲区中直到换行符的所有内容
cin.ignore(numeric_limits<streamsize>::max(), '\n');
continue;
}
💻 5. 完整代码实现(工程化分文件结构)
为了体现模块化设计思想,我们将项目拆分为头文件、实现文件和主函数。
5.1 头文件 (Gobang.h)
定义接口与常量,实现声明与实现分离
Gobang.h
cpp
#pragma once
#ifndef GOBANG_H
#define GOBANG_H
#include <iostream>
#include <limits> // 用于 numeric_limits
const char Q = '*';
const char PLAYER_A = 'A';
const char PLAYER_B = 'B';
// 接口声明
char** CreateArray(int row, int col);
void ShowBoard(char** arr, int row, int col);
int IsWin(char** arr, int row, int col, int a, int b);
void FreeArray(char** arr, int row);
#endif // GOBANG_H
5.2 功能实现 (Gobang.cpp)
封装核心逻辑,隐藏实现细节
Gobang.cpp
cpp
#include "Gobang.h"
#include <iostream>
using namespace std;
// 动态分配二维数组
char** CreateArray(int row, int col) {
char** chess = (char**)malloc(row * sizeof(char*));
if (!chess) return NULL;
for (int i = 0; i < row; i++) {
chess[i] = (char*)malloc(col * sizeof(char));
if (!chess[i]) {
// 回滚释放
for (int k = 0; k < i; k++) free(chess[k]);
free(chess);
return NULL;
}
for (int j = 0; j < col; j++) {
chess[i][j] = Q;
}
}
return chess;
}
// 释放内存
void FreeArray(char** arr, int row) {
if (arr == NULL) return;
for (int i = 0; i < row; i++) {
free(arr[i]);
}
free(arr);
}
// 棋盘展示
void ShowBoard(char** arr, int row, int col) {
cout << "\n ";
for (int i = 1; i <= col; i++) cout << i << " ";
cout << endl;
for (int i = 0; i < row; i++) {
cout << i + 1 << " ";
for (int j = 0; j < col; j++) {
cout << arr[i][j] << " ";
}
cout << endl;
}
}
// 胜负判定
int IsWin(char** arr, int row, int col, int a, int b) {
char win = arr[a][b];
int count;
// 水平
count = 0;
for (int i = b - 1; i >= 0 && arr[a][i] == win; i--) count++;
for (int i = b + 1; i < col && arr[a][i] == win; i++) count++;
if (count >= 4) return 1;
// 垂直
count = 0;
for (int i = a - 1; i >= 0 && arr[i][b] == win; i--) count++;
for (int i = a + 1; i < row && arr[i][b] == win; i++) count++;
if (count >= 4) return 1;
// 副对角线 /
count = 0;
for (int i = a + 1, j = b - 1; i < row && j >= 0 && arr[i][j] == win; i++, j--) count++;
for (int i = a - 1, j = b + 1; i >= 0 && j < col && arr[i][j] == win; i--, j++) count++;
if (count >= 4) return 1;
// 主对角线 \
count = 0;
for (int i = a - 1, j = b - 1; i >= 0 && j >= 0 && arr[i][j] == win; i--, j--) count++;
for (int i = a + 1, j = b + 1; i < row && j < col && arr[i][j] == win; i++, j++) count++;
if (count >= 4) return 1;
return 0;
}
5.3 主函数 (main.cpp)
负责流程控制与异常处理
main.cpp
cpp
#include <iostream>
#include "Gobang.h"
using namespace std;
int main() {
cout << "=== 欢迎来到五子棋游戏 ===" << endl;
int row, col;
// 【健壮性1】初始输入校验
while (true) {
cout << "请输入棋盘大小 [行 列]: ";
if (cin >> row >> col && row > 0 && col > 0 && row <= 20 && col <= 20) break;
cout << "输入无效,请输入正整数。" << endl;
cin.clear();
cin.ignore(numeric_limits<streamsize>::max(), '\n');
}
char** chess = CreateArray(row, col);
if (!chess) {
cerr << "内存分配失败,程序退出。" << endl;
return -1;
}
cout << "=== 棋盘开辟成功,游戏开始 ===" << endl;
ShowBoard(chess, row, col);
char role = PLAYER_A;
while (true) {
int a, b;
while (true) {
cout << "玩家 [" << role << "] 请输入坐标 [行 列]: ";
// 【健壮性2】防止非法输入死循环
if (!(cin >> a >> b)) {
cout << "格式错误,请输入数字!" << endl;
cin.clear();
cin.ignore(numeric_limits<streamsize>::max(), '\n');
continue;
}
// 【健壮性3】边界与占用检查
if (a >= 1 && a <= row && b >= 1 && b <= col) {
if (chess[a-1][b-1] == Q) {
chess[a-1][b-1] = role;
break; // 合法落子,跳出内层循环
} else {
cout << "该位置已有棋子,请重新输入。" << endl;
}
} else {
cout << "坐标超出范围,请重新输入。" << endl;
}
}
ShowBoard(chess, row, col); //打印棋盘
// 胜负判断
if (IsWin(chess, row, col, a-1, b-1)) {
cout << "🎉 恭喜玩家 [" << role << "] 获胜!" << endl;
break;
}
// 切换角色
role = (role == PLAYER_A) ? PLAYER_B : PLAYER_A;
}
// 【健壮性4】确保内存释放
FreeArray(chess, row);
cout << "游戏结束,资源已释放。" << endl;
return 0;
}
🚀 6. 进阶探讨:从"作业"到"工程"
如果你想在面试中进一步展示你的技术深度,可以提及以下优化方向:
6.1 性能与算法
- 位运算优化:在嵌入式或高性能场景下,可以使用位图(Bitboard)来表示棋盘,利用位运算快速判断连线,极大提升计算速度。
- AI 算法 :引入 Minimax 算法 配合 Alpha-Beta 剪枝,实现具有一定智能的 AI 对手,这是算法岗位的经典面试题。
6.2 现代 C++ 特性
- RAII 机制 :使用
std::unique_ptr或std::vector替代裸指针,利用析构函数自动管理内存,从根本上杜绝内存泄漏。 - 智能指针 :
std::shared_ptr或std::unique_ptr的使用体现了对现代 C++ 资源管理哲学的理解。
6.3 架构设计
- MVC 模式:将视图(ShowBoard)、模型(ChessBoard 类)、控制器(GameLoop)分离,提高代码的可维护性和可扩展性。
📝 结语
本项目的核心价值不在于游戏本身,而在于通过一个简单的案例,串联起了 指针操作、内存生命周期、异常流处理 等 C++ 核心知识点。
在面试中,能够清晰阐述**"为什么这样分配内存"** 、"如何处理非法输入" 以及**"如何保证程序不崩溃"**,往往比单纯实现功能更能赢得面试官的青睐。
希望这篇博文能为你的 C++ 学习之路提供助力。如果觉得有用,欢迎点赞、收藏并分享给更多正在奋斗的同学!👍