C语言学习至今,今天我们来完成C语言的一个小项目:扫雷游戏。

        扫雷游戏是经典的网页小游戏,想要完成它我们需要几个步骤

扫雷游戏的预设效果

        如图所示,扫雷具有多个版本,这里作为演示,只演示最简单的10个雷的版本。

        扫雷游戏的玩法思路如下:

1找一个位置排雷,如果它是雷就被炸死,如果不是雷,统计它周围的雷的个数。

2如果找全所有的雷的个数,则游戏通关

        听着是不是很简单?

        但是实施起来的话,也是稍微有些许难度的。

        在本项目中,我们需要多个文件来运行该项目,它们分别为:

(1)test.c        主要用于项目的思路的实现

(2)game.h        用于定于宏和函数的声明

(3)game.c        用于具体的实现游戏的函数的定义

扫雷游戏的实现

扫雷游戏的代码骨架

        首先,我们需要打印菜单。预设的函数和效果如下

        接着,我们进入了游戏。在游戏中,我们要选择是否进入游戏,因此要运用switch语句来实现。如下图中的代码所示,当我们选择1时开始游戏,选择0时退出游戏,而选择其他数字时会进入default语句提示错误让你继续进行选择。在这里你输入字符而不是数字时,会陷入死循环。PS:作者实测

        这里之所以要用do……while语句,是因为无论循环变量是否符合条件,我们都要进入循环完成一次游戏的选择。

        srand的调用是为了后续随机布置雷的调用的。

        接下来核心的部分就是该项目的核心部分:game函数游戏实现。

扫雷游戏的函数实现

        游戏函数的代码实现如下:(在布置雷后将雷的信息打印出来是排查代码时候方便所致,如果为了游玩,建议删除或者注释该行代码)

头文件引用与宏定义与函数的声明

该项目中game.h入图二所示

        在这里我们可以进行函数的声明,声明就是:告诉编译器函数的名称返回值类型参数类型,但不提供具体实现。(在实际应用中函数的定义并不总是希望别人知道)

        初始化棋盘

        首先我们需要两个棋盘,即两个二维数组分别初始化,预期效果如下:(因为在这一阶段下我们还没有学习用户界面设计,所以只能用命令行来实现游戏)

其中1棋盘负责打印初始棋盘,2棋盘负责显示和布置雷。       

        在这里可能会有一个问题:ROWS和COLS的作用是什么呢?

        实际上,我们玩该项目的扫雷的时候只有9x9的棋盘,但是玩数组的时候我们要对所在左边周围一个的范围进行搜查,这样就会涉及到数组越界的问题,所以在该项目中,我们选择用更大的数组范围(即更高的内存)换更便利的写作方式。

        具体实现概念如下:图中黄色的部分是我们游玩的棋盘,而黄色和绿色加在一起的部分才是我们初始化期盼的全部内容。

        初始化棋盘的代码如下所示

        打印棋盘

        打印棋盘主要是打印棋盘最开始的样子,即如下图所示的样子

        打印棋盘的代码并不难实现,但是只有棋盘难以确定位置,所以我们需要打印行列号

        注意:打印列号时候如果i从1开始会导致行号的错位,即如下情况

布置雷

        在基础模式中,我们需要打印随机布置10个雷,并把他们布置在合法的位置

      图中所示的easycount是一个在game.h中新定义的宏变量,它表示在简单情况下雷个数量

        在布置雷的时候我们需要使用rand函数来随机布置雷的信息。

        初始化x和y之所以要加1是因为x和y表示的是实际的坐标而不是数组的下标,数组的下标相较于实际的坐标x和y均要-1。

        如图所示,我们布置雷的时候不允许雷越界。但是rand函数的变化范围非常的大,从0到2147483647(即 2^31 - 1)(常见的编译器,如gcc等)或者32767(即 2^15 - 1)(老版本编译器或者嵌入式系统),因此我们需要对其进行约束

rand函数的使用案例(选择性观看)

3. 生成特定范围的随机数

若需生成 [a, b] 之间的随机数,需对 rand() 的结果进行缩放:

公式

int num = a + rand() % (b - a + 1);

示例:生成 [1, 10] 的随机数

int num = 1 + rand() % 10;

注意

  • 取模运算的偏差:当 RAND_MAX 不是 (b - a + 1) 的整数倍时,结果可能分布不均。

  • 更精确的方法(减少偏差):

    int range = b - a + 1;
    int num = a + (int)((double)rand() / (RAND_MAX + 1.0) * range);
  •  伪随机性与种子初始化

  • rand() 是伪随机数生成器,依赖种子值(默认种子为 1)。

  • 使用 srand() 初始化种子以增强随机性(通常用当前时间):

  • #include <time.h>
    srand(time(0));  // 设置种子为当前时间

代码示范案例

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main() {
    srand(time(0));  // 初始化种子
    for (int i = 0; i < 5; i++) {
        int num = rand() % 100;  // 生成0~99的随机数
        printf("%d ", num);
    }
    return 0;
}

总结

特性说明
默认范围0 到 RAND_MAX(含)
典型 RAND_MAX2147483647(现代系统)或 32767(旧系统)
种子依赖需用 srand() 初始化种子,否则每次运行生成相同的随机数序列
均匀性保证直接取模可能引入偏差,建议使用浮点数缩放方法

检查雷并开始玩游戏

        在布置好雷后,我们要做的就是开始排查雷了。

        首先我们要初始化坐标量以及剩余次数。

        接下来我们要判断下坐标是否合法,因此便有了如下if语句的内容

        当坐标不合法时候,就会提示让你重新输入

        当棋盘2上的值为‘1’时,就会被炸死。如果没有被炸死的话,就会统计周围雷的个数并继续排查雷(排查雷的个数是另外的一个函数,它的原理如下图所示)

        该图中,所有的坐标均为字符而不是数字,但是仍然可以计算,因为数字的ASCII码的数值是连续的,‘0’的ASCII值为48,‘1’的ASCII值为49,‘1’-‘0’=1仍然成立,因此仍然可以计算周围有几个雷。

具体实现如下:(因为其计算过程比较繁琐因此需要函数的调用)

       

        当我们排除掉所有的雷的时候,游戏结束。

        感谢各位读者大大看到这里,感谢各位,在这里作者大大求一个小小的赞谢谢。

宏变量和一般变量(PS:之后的内容选择性观看)

在C语言中,宏变量(通过#define定义)和普通变量(通过数据类型声明)有本质区别。以下是两者的对比分析:


1. 定义方式

宏变量普通变量
通过预处理器指令#define定义通过数据类型声明(如intfloat
示例
#define PI 3.1415
示例
const float PI = 3.1415f;

2. 处理阶段

宏变量普通变量
预处理阶段被替换为字面量编译阶段分配内存地址
编译器看到的代码是替换后的文本编译器处理实际的变量操作

3. 内存与类型

宏变量普通变量
无内存占用(纯文本替换)占用内存空间(栈/堆)
无类型检查(直接替换)有严格的类型检查
示例问题
#define VALUE "123"
int x = VALUE;(编译报错)
正确写法
const char* VALUE = "123";

4. 作用域与生命周期

宏变量普通变量
从定义处到文件结束有效(无作用域)遵循作用域规则(局部/全局)
可用#undef取消定义生命周期由存储类型决定(自动/静态)

5. 调试与错误

宏变量普通变量
调试时不可见(替换后的代码)调试时可观察内存值和类型
错误可能难以追踪(如重复替换副作用)错误通常有明确提示

6. 典型应用场景

宏变量普通变量
定义常量(如数组大小)
#define MAX_SIZE 100
存储运行时数据
int count = 0;
条件编译标志
#define DEBUG_MODE
配置参数(需运行时修改)
int debug_mode = 1;
简化复杂表达式
#define SQUARE(x) ((x)*(x))
存储中间计算结果

7. 代码示例对比

宏变量的潜在问题:

#include <stdio.h>
#define SQUARE(x) x*x  // 有缺陷的宏

int main() {
    int a = 5;
    printf("%d\n", SQUARE(a+1));  // 展开为 5+1*5+1 = 11(非预期)
    return 0;
}

修正方法
#define SQUARE(x) ((x)*(x))

普通变量的安全实现:

#include <stdio.h>

int square(int x) {
    return x * x;
}

int main() {
    int a = 5;
    printf("%d\n", square(a+1));  // 正确输出36
    return 0;
}

总结:如何选择?

  • 使用宏

    • 需要跨文件共享常量

    • 需要条件编译控制

    • 简单代码片段复用(需谨慎)

  • 使用变量

    • 需要存储运行时数据

    • 需要类型安全和调试支持

    • 需要作用域控制

实际开发中应优先使用const常量或函数,仅在必要时使用宏。

函数的定义声明调用(上一篇博客遗漏的内容)

1.函数定义(Function Definition)

作用:提供函数的具体实现,包括函数体。

返回值类型 函数名(参数列表) {
    // 函数体
}

特点

  • 每个函数只能定义一次(否则链接错误)。

  • 定义必须与声明一致(返回值、参数类型)。

  • 若未定义但被调用,链接器报错(undefined reference)。

2. 函数声明(Function Declaration)

作用:告诉编译器函数的名称返回值类型参数类型,但不提供具体实现。
语法:返回值类型 函数名(参数类型列表);

示例

int add(int a, int b);  // 声明一个返回int、接受两个int参数的函数

特点

        可多次声明,通常放在头文件(.h)或代码文件开头。

        参数名可省略(但类型必须保留):int add(int, int);

        若未声明直接调用,编译器可能发出警告(取决于标准)。

        3. 函数调用(Function Call)

        作用:在代码中执行函数。        
        语法:函数名(实际参数)

        示例:int result = add(3, 5);  // 调用add函数,传入3和5

        特点


总结

  • 参数类型和数量必须与声明/定义一致。

  • 可嵌套调用(如 printf("%d", add(2,3)))。

  • 三者的区别对比

    特性声明定义调用
    目的告知编译器函数的存在提供函数的具体实现执行函数功能
    语法以分号;结尾包含函数体{}函数名加参数()
    次数可多次声明只能定义一次可多次调用
    位置头文件或代码文件开头代码文件中函数可访问的任何位置
    必需性调用前需声明(除非定义在前)必须定义才能使用
  • 常见错误及解决

  • 未声明直接调用(定义在调用之后):
    错误warning: implicit declaration of function 'add'
    解决:在调用前添加函数声明。

  • 重复定义
    错误redefinition of 'add'
    解决:确保每个函数只定义一次。

  • 参数类型不匹配
    错误conflicting types for 'add'
    解决:保持声明、定义、调用的参数类型一致。

  • 声明:是函数的“身份证”,告诉编译器如何检查调用。

  • 定义:是函数的“身体”,提供具体功能实现。

  • 调用:是函数的“使用”,触发函数执行。

  • 最佳实践:始终先声明函数(或通过头文件),再定义和调用,避免隐式声明导致的问题。

更多推荐