C++ 实战:从零手写控制台五子棋(深入解析内存管理与核心算法)

摘要 :本文不仅是一个五子棋游戏的实现教程,更是一次对 C++ 底层内存管理指针操作 以及程序健壮性设计的深度实践。我们将摒弃简单的"功能堆砌",从工程化角度出发,重点解决动态二维数组的内存泄漏问题、非法输入导致的死循环陷阱,并优化胜负判定算法。适合希望从"写代码"进阶到"做工程"的开发者阅读。


🎯 1. 项目背景与工程价值

在面试和实际开发中,五子棋往往被视为一个经典的综合性测试题。它看似简单,实则考察了候选人对以下核心能力的掌握程度:

  1. 内存管理能力 :能否正确使用 malloc/freenew/delete 管理动态资源,避免内存泄漏(Memory Leak)和悬空指针。
  2. 边界控制意识:在处理数组访问时,是否具备严格的边界检查习惯,防止段错误(Segmentation Fault)。
  3. 异常处理思维:面对用户的非法输入(如输入字符而非数字),程序是否会崩溃或陷入死循环?
  4. 算法效率优化:是每次全盘扫描 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 "两次分配"策略与内存布局

很多初学者容易混淆一维数组映射和真正的二维指针数组。标准的动态二维数组创建需要两步:

  1. 一级分配:创建一个指针数组,每个元素指向一行。
  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++ 中,动态分配的内存必须手动释放。对于二维指针数组,释放顺序必须与分配顺序严格相反

  1. 先释放内部的数据行。
  2. 再释放外部的指针数组。

❌ 常见错误

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')时:

  1. cin 进入 fail 状态。
  2. 非法字符保留在缓冲区中。
  3. 下次循环再次读取时,立即读到同一个非法字符,再次失败,形成死循环

✅ 解决方案

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_ptrstd::vector 替代裸指针,利用析构函数自动管理内存,从根本上杜绝内存泄漏。
  • 智能指针std::shared_ptrstd::unique_ptr 的使用体现了对现代 C++ 资源管理哲学的理解。

6.3 架构设计

  • MVC 模式:将视图(ShowBoard)、模型(ChessBoard 类)、控制器(GameLoop)分离,提高代码的可维护性和可扩展性。

📝 结语

本项目的核心价值不在于游戏本身,而在于通过一个简单的案例,串联起了 指针操作、内存生命周期、异常流处理 等 C++ 核心知识点。

在面试中,能够清晰阐述**"为什么这样分配内存"** 、"如何处理非法输入" 以及**"如何保证程序不崩溃"**,往往比单纯实现功能更能赢得面试官的青睐。

希望这篇博文能为你的 C++ 学习之路提供助力。如果觉得有用,欢迎点赞、收藏并分享给更多正在奋斗的同学!👍

相关推荐
zandy10114 小时前
体验家 XMPlus 问卷答题体验优化与完成率提升引擎:从交互设计到行为心理学的工程化实践
windows·microsoft·交互
码银7 小时前
放弃了豆包,我使用Python做了一个桌面宠物
python·microsoft·宠物
大郭鹏宇17 小时前
基于 LangGraph 构建智能分诊系统(一):项目概述与环境搭建
大数据·人工智能·microsoft·langchain
DisonTangor1 天前
【微软开源】Mage-Flow 深度解读:微软4B参数图像生成与编辑模型,单卡A100仅需0.59秒
人工智能·microsoft·ai作画·开源·aigc
大模型码小白1 天前
企业级检索增强后端集成:Java 服务如何管理知识库版本
java·服务器·开发语言·人工智能·python·microsoft
会飞的小新2 天前
C 标准库之 <fenv.h> 详解与深度解析
c语言·开发语言·microsoft
编码者卢布2 天前
【Azure Policy】Policy修正任务为Azure资源添加诊断日志报错问题的调查
microsoft·flask·azure
海盗12342 天前
微软技术周报——2026-07-22
microsoft·c#·.net
海盗12342 天前
微软技术日报 ——2026-07-21
microsoft·c#·.net