扫雷小游戏的核心部分,online judge 经典操作:动态内存管理模拟二维数组
创始人
2025-05-29 23:15:08

前言

书接上回,我们已经让玩家输入了扫雷棋盘的大小和雷的个数,接下来只需要实现扫雷的核心逻辑,即 MineSweeper 函数。希望大家享受编程的过程,并且有所收获。

void MineSweeper(size_t row, size_t col, size_t mineCount);

摆在我们面前的一个难题是:我可以直接创建一个二维数组,比如 int arr[3][5]; 就是一个三行五列的数组,但是创建数组时是不能用变量指定大小的!

说个题外话,C99 支持变长数组,可以使用变量来定义数组,但不能初始化。如:

int num = 10;
char ch[num];

但是绝大多数的编译器都不支持呀!所以我们要另辟蹊径。这里卖个关子,本篇博客会教会你如何使用动态内存管理的知识来管理二维数组,这个技巧会在很多 online judge (在线编程)题目中见到,还是很重要的。

任务

  1. 复习动态内存管理。

  1. 使用动态内存管理的知识来模拟二维数组。

  1. 对这块模拟出来的二维数组进行初始化。

  1. 打印这个模拟出来的二维数组。

  1. 回收这块动态开辟出来的空间。

动态内存管理

如果我们想一次性存储很多数据,可以使用数组。但是数组的大小在创建的时候就已经确定了,而且必须使用常量来初始化(C99 标准前不支持变长数组)。我们想实现扫雷,就必须用 row, col 这样的变量来初始化,这该怎么办呢?这就需要动态内存管理派上用场了!

C语言的动态内存管理有四大天王四个函数,分别是 malloc, calloc, realloc 和 free。这四个函数都非常重要,分别扮演了重要的角色,使用它们都得包含头文件 stdlib.h 。不过我们今天只会用到 malloc 和 free ,所以就先复习下这两个函数。

malloc 函数是用来开辟空间的。比如,我想开辟 10 个字节的空间,就只需要写 malloc(10) 就行了,malloc 函数会帮你开辟好 10 个字节的连续空间,并且把这块空间的起始位置返回给你。注意,返回的地址的类型是 void* 。void* 类型的特点是,不能解引用,不能加减整数,啥都不能干。所以,你如果想要使用这块空间,就必须使用非 void* 类型的指针来接收这个地址。

char* pch = (char*)malloc(10);

由于 malloc 是在堆上申请空间,需要程序员手动释放。当我们不需要这块空间时,必须手动 free 掉这块空间,否则会引起内存泄漏。

free(pch);
pch = NULL;

使用 malloc 模拟二维数组

接下来是重头戏!malloc 可以帮我们申请到一块空间,具体原理如下图。

既然 ptr 可以指向一块空间,这块空间里如果不存 char ,而是存 char* 呢?就像这样:

要是每个 char* 又指向一块 malloc 开辟的空间呢,就像这样:

如何访问用颜色标识出来的空间呢?是不是 ptr[0][1] 就行了?这样表示和二维数组访问元素是完全一样的!

回到扫雷小游戏。我们想要创建一个 row * col 大小的盘面来存储雷的信息(mine),同时还需要另一个数组来存储排查出来的雷的信息(show)。对于 show ,假设对于没有排查的位置都存储 * ,那么 show 应该是一个存储 char 类型元素的二维数组,为了方便管理,我们假设 show 也是存储 char 类型的二维数组。

但是,如果只开 row * col 大小的数组,对于边上或者角上位置,就很难统计周围 8 个位置雷的个数,因为直接访问会越界。我一开始想的是,可以判断一下,但后来想到了个更简单的办法,把 mine 和 show 都开大一圈,也就是说,开 (row+2)*(col+2) 大小的数组!

而只使用中间的 row*col 部分不就行了吗!其余位置就默认当没有雷,这样统计某个位置周围 8 个位置雷的个数时,就不需要考虑边角上的问题了。

阶段性总结一下:我们需要开两个 char 类型,大小是 (row+2)*(col+2) 的数组,而数组不能在初始化时用变量指定大小,所以可以使用 malloc 两层的方法,先 malloc 出一个存储元素类型是 char* 的指针数组(这个指针数组有 row+2 个空间),然后再让每一个 char* 指针指向一个存储元素类型是 char 的字符数组(这个字符数组有 col+2 个空间)。而具体访问每一个 char 的时候,就可以通过下标的方式,既 mine[i][j] 的方式来访问了,完美!

void MineSweeper(size_t row, size_t col, size_t mineCount)
{assert(row < 100 && col < 100);assert(mineCount < row * col);char** mine = NULL;char** show = NULL;InitBoard(&mine, row, col, '0');InitBoard(&show, row, col, '*');DisplayBoard(mine, row, col);DisplayBoard(show, row, col);DestroyBoard(mine, row, col);DestroyBoard(show, row, col);
}

解释下,assert 函数是用来断言的,也就是说,assert 后面跟的括号里的表达式如果为真,就什么都不会发生,如果为假,就会直接终止掉程序。assert 函数的使用需要包含头文件 assert.h 。这里判断的是上期讲解的,row 和 col 不能超过两位数且雷的个数不能多于总数。

mine 数组中,默认用字符 0 表示该位置不是雷,字符 1 表示该位置是雷。show 数组中,用 * 表示该位置未排查,数字字符(字符 1 ~ 字符 8 )表示该位置周围 8 个位置雷的个数。当然还有其他的设计,比如空格和 ? 字符,这些留着后面讲解。一开始的初始化函数 InitBoard 负责把 mine 和 show 模拟的二维数组分别初始化成全 0 和全 * 。由于改变了 mine 和 show 两个指针变量,所以需要传它们的地址,而 char** 的地址是 char*** 。接下来是 InitBoard 函数的实现,采取前面说的两层 malloc 的方式。

static void InitBoard(char*** pBoard, size_t row, size_t col, char set)
{assert(pBoard != NULL);*pBoard = (char**)malloc((row + 2) * sizeof(char*));if (*pBoard == NULL){perror("InitBoard: 开辟空间失败");return;}for (size_t i = 0; i < row + 2; ++i){(*pBoard)[i] = (char*)malloc((col + 2) * sizeof(char));if ((*pBoard)[i] == NULL){perror("InitBoard: 开辟空间失败");for (size_t j = 0; j < i; ++j){free((*pBoard)[j]);(*pBoard)[j] = NULL;free(*pBoard);*pBoard = NULL;}return;}else{memset((*pBoard)[i], set, col + 2);}}
}

函数前用 static 关键字修饰,函数成为静态的函数,只能在本源文件内部使用,其他源文件内不能使用,这是一个很好的封装。

每次 malloc 之后,都必须检查是否开辟空间成功,如果失败,malloc 会返回空指针 NULL 。

如果空间都开辟成功,使用 memset 函数把这块空间都初始化成 set 。memset 函数对应的头文件是 string.h ,可以初始化一块内存,三个参数分别是这块空间的起始地址,要设置的值,这块空间的大小(单位是字节)。

使用 free 函数回收空间

空间开辟完,使用完后需要使用 free 函数回收。写个循环挨个挨个回收就行了。

static void DestroyBoard(char** board, size_t row, size_t col)
{assert(board != NULL);for (size_t i = 0; i < row + 2; ++i){free(board[i]);board[i] = NULL;}free(board);board = NULL;
}

打印模拟的数组

最后聊聊如何打印。其实这非常简单,因为两层 malloc 出来的数组的访问方式和二维数组一模一样,所以直接当成二维数组来打印就行了。为了打印的好看,我把行标和列标也打印上去了。

需要注意的是,这个数组的有效区间不包括最外面这一圈,所以打印时需要控制范围是 [1, row] * [1, col] 。

static void DisplayBoard(char** board, size_t row, size_t col)
{assert(board != NULL);// 有效区间 [1, row] * [1, col]for (size_t i = 0; i <= col; ++i){printf("%2zd ", i);}printf("\n\n");for (size_t i = 1; i <= row; ++i){printf("%2zd ", i);for (size_t j = 1; j <= col; ++j){printf("%2c ", board[i][j]);}printf("\n\n");}
}

打印出来的效果如下:

现在我们就把框架搭好了,接下来就要写如何布置雷和排查雷的逻辑了。欲知后事如何,且听下回分解。

相关内容

热门资讯

[必备科技]“新蛮王炸金花是不... [必备科技]“新蛮王炸金花是不是有挂!”!原来真的有挂亲.新蛮王炸金花这款游戏是可以开挂的,确实是有...
玩家实测“四川麻将有挂没有”开... 您好:四川麻将这款游戏可以开挂,确实是有挂的,需要软件加微信【8700483】,很多玩家在四川麻将这...
必备盘点"大圣大厅究... 您好:大圣大厅这款游戏是可以开挂的,究竟有没有挂确实能开挂,了解请添加《75638038》(加我们微...
「玩家推荐」风风棋牌.有没有挂... 您好:风风棋牌这款游戏可以开挂,确实是有挂的,需要了解加客服微信【9951342】很多玩家在这款游戏...
实测分享“新卡农大厅到底是不是... 您好:新卡农大厅这款游戏可以开挂,确实是有挂的,需要软件加微信【69174242】,很多玩家在新卡农...