c语言修炼秘籍【第三章】数组

【C++修炼秘籍】Vector深度剖析 vector的数据安排以及操作方式,与array非常相似,就像数组一样,vector采用连续的存储空间,可以像数组一样的随机访问,但是又不像数组一样,它的大小是可以随意改变的,而且是自动处理;有了vector后,相比于C语言的数组使用要方便许多,接下来让我们深入了解一下;vector动态开辟空间,是可变大变小的数组序列容器;像数组一样随机访问;分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。 阅读详情

c语言修炼秘籍【第三章】数组

【心法】
【第零章】c语言概述
【第一章】分支与循环语句
【第二章】函数
【第三章】数组
【第四章】操作符
【第五章】指针
【第六章】结构体
【第七章】const与c语言中一些错误代码
【禁忌秘术】
【第一式】数据的存储
【第二式】指针
【第三式】字符函数和字符串函数
【第四式】自定义类型详解(结构体、枚举、联合)
【第五式】动态内存管理
【第六式】文件操作
【第七式】程序的编译



前言

本文将会介绍常用到的一维数组和二维数组是如何使用及它们在内存中是如何存储,说明何为数组越界,并讨论数组在作为函数参数时应该如何使用。在最后附带三子棋和扫雷的代码实现详解。


一、数组是什么?为什么要有数组?

数组是一组相同类型的元素的集合。

思考一个问题,此时需要从输入流中获得10个整型值,如何在标准输出流中全部输出,并计算它们之和的值?
输入示例:1 2 3 4 5 6 7 8 9 10
输出示例:
1 2 3 4 5 6 7 8 9 10
55
如果没有数组的话,你想要保存这十个数值,就需要创建10个整型变量,虽然比较复杂但好像并非难以接受;那如果,此时需要获取100个整型值呢?如果要创建100个变量的话,这是否有点…?
所以,为了要临时保存多个相同类型的变量的值时,数组就能发挥它的作用了。

二、一维数组

1.数组的创建

type_t array_name[const_n];

// type_t 是指数组的元素类型
// array_name 是数组名
// const_n 是一个常量表达式,表示数组能保存的最大元素个数,即数组的大小

// 一个大小为5的char类型数组,能保存5个char类型元素
char arr1[5];
// 一个大小为10的int类型数组,能保存10个int类型元素
int arr2[10];

// error 错误的数组创建方式
int count = 10;
int arr3[count]; // 数组的大小必须是常量,const修饰的变量也不行,因为它只是不能被修改,具有常量的特性,但本质上仍是变量

2.数组的初始化

数组的初始化是指在创建的同时给数组的内容一些合理的初始值

看代码:

int arr1[10] = { 1, 2, 3, 4 }; // 不完全初始化
int arr2[] = { 1, 2, 3, 4 };
int arr3[4] = { 1, 2, 3, 4 };

char str1[6] = "hello";
char str2[] = "hello";
char str3[] = { 'h', 'e', 'l', 'l', 'o' };
char str4[] = { 'h', 101, 'l', 'l', 'o' };
char str5[10] = "hello";
char str6[10] = { 'h', 'e', 'l', 'l', 'o' };

arr1中有10个元素,在初始化时,只给了4个元素,这种做法叫做不完全初始化,剩余的元素全部默认初始化为0;
arr2中没有指定数组的元素个数,此时初始化列表有几个元素,该数组就有多少个元素;
arr3中指定了数组中有4个元素,初始化列表中包含了4个元素,这被称为完全初始化;
其中arr2和arr3等价;


str1完全初始化;
str2与str1等价;
str3与str4等价,其中str4中的第二个元素'e'用ASCII码值101表示;
str5用"hello"这个字符串进行不完全初始化;
str6用 { 'h', 'e', 'l', 'l', 'o' }这五个字符进行不完全初始化;
str5和str6在内存中的值虽然相同,但初始化过程并不相同:
str5是用 { 'h', 'e', 'l', 'l', 'o' , '\0'}这六个字符进行不完全初始化,对剩余的4个元素默认初始化为0;
str5是用{ 'h', 'e', 'l', 'l', 'o' }这五个字符进行不完全初始化,对剩余的5个元素默认初始化为0;


内存空间:
arr
在这里插入图片描述

注意

一定要能够区分下面这类代码,
arr1中有4个元素,‘a’, ‘b’, ‘c’, ‘\0’,数组大小为4
arr2中只有3个元素,‘a’, ‘b’, ‘c’,数组大小为3

char arr1[] = "abc";
char arr2[3] = { 'a', 'b', 'c' };

3.一维数组的使用

我们要如何使用数组中呢?需要用到下标引用操作符[]

看代码

#include <stdio.h>

int main()
{
	// 创建一个大小为10的int类型数组,不完全初始化,数组内容为全0
	int a[10] = { 0 };
	// 计算数组大小(求数组中元素个数)
	// 整个数组所占内存空间的大小 / 数组中一个元素所占内存空间的大小 == 数组中元素个数
	int sz = sizeof(a) / sizeof(int);

	// 对数组内容进行赋值,数组是使用下标来访问的,下标从0开始
	// i作下标
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		a[i] = i;
	}

	// 输出数组的内容
	for (i = 0; i < sz; i++)
	{
		printf("%d ", a[i]);
	}

	return 0;
}

总结

  1. 数组中的元素是用下标来访问的,下标从0开始;还可以使用指针访问,后面章节会介绍。
  2. 数组的大小可以通过计算得到。

4.一维数组在内存中的存储

看代码:

#include <stdio.h>

int main()
{
	int arr[10] = { 0 };
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);

	for (i = 0; i < sz; i++)
	{
		printf("arr[%d]`s address == %p\n", i, &arr[i]);
	}

	return 0;
}

在这里插入图片描述
可以看出,数组中元素在内存空间中的地址是连续的,数组的内部的空间使用是从低位到高位。
在这里插入图片描述

三、二维数组

1.数组的创建

type_t array_name[const_x][const_y]
// type_t 数组元素类型
// array_name 数组名
// const_x,const_y 数组大小

// 可以将其看作int (arr1[2])[3],
// arr1[]是一个一维数组,数组中元素类型为int,大小为3
// arr1也是一个一维数组,数组的元素是一个数组,类型为int[3],大小为2
int arr1[2][3];
// 可以将其看作char (arr2[2])[3],
// arr2[]是一个一维数组,数组中元素类型为char,大小为4
// arr2也是一个一维数组,数组的元素是一个数组,类型为char[4],大小为2
char arr2[2][4];

在这里插入图片描述

2.数组的初始化

int arr1[][3] = { 1,2,3,4 }; // 二维数组如果有初始化,行数可以省略,但列不行
int arr2[][3] = { { 1, 2 }, { 3, 4 } };
int arr3[2][3] = { { 1, 2 }, { 3, 4 } };
// arr2 和 arr3 等价

在这里插入图片描述

通过观察arr1可知,为什么行可以省略,而列不行
因为列的大小确定了,一行能有几个元素就确定了,给出了初始化列表,就能有唯一的数组;
但如果只给行的大小,不知道列的大小,一行可以只放一个元素,也可以放10个元素,相同的元素总数,此时能够出现的行和列的组合有很多种;
所以列不能省略。

3.二维数组的使用

二维数组的使用也是通过下标访问

#include <stdio.h>

int main()
{
	// 不完全初始化
	int arr[3][5] = { 0 };
	int row = sizeof(arr) / sizeof(arr[0]);
	int col = sizeof(arr[0]) / sizeof(arr[0][0]);

	int i = 0;
	int j = 0;
	// 对数组内容进行赋值
	for (i = 0; i < row; i++)
	{
		for (j = 0; j < col; j++)
		{
			arr[i][j] = i + j;
		}
	}

	// 输出数组内容
	for (i = 0; i < row; i++)
	{
		for (j = 0; j < col; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}

	return 0;
}

在这里插入图片描述

4.二维数组在内存中的存储

打印int arr[2][3]的地址

#include <stdio.h>

int main()
{
	int arr[2][3] = { 0 };

	int i = 0;
	int j = 0;
	for (i = 0; i < 2; i++)
	{
		for (j = 0; j < 3; j++)
		{
			printf("arr[%d][%d]`s address == %p\n", i, j, &arr[i][j]);
		}
	}

	return 0;
}

在这里插入图片描述

可以看到,arr中元素的地址是连续的,即二维数组的内存空间如下图所示:
在这里插入图片描述

四、数组越界

数组是有大小的,那么数组其实是有界限的,使用下标时可能会出现超限越界的情况。
例如,

  1. int arr[10],该数组的大小为10,下标从0开始,最大的下标为9,当使用arr[10]时就出现越界了,此时编译器并不会报错;
  2. 初始化时,使用的初始化列表中元素个数超过数组大小,也会越界,但这种情况编译器会报错;
  1. 程序执行越界
    在这里插入图片描述
  2. 初始化越界
    在这里插入图片描述

二维数组也存在越界问题

c语言并不提供,数组的越界检测,需要程序员自己做好越界的检查

五、数组作为函数参数

我们在写代码时,往往会遇到需要将数组作为函数参数的情况,比如,写一个函数实现对整型数组进行冒泡排序
在这里插入图片描述
上图展示了冒泡排序的一轮交换的过程,每一轮排序都会找到数组中的最大值将其放置到数组末尾,
有n个元素,最多就需要进行n - 1轮排序。

1.冒泡排序函数的错误设计

#include <stdio.h>

void bubble_sort(int arr[])
{
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	int j = 0;
	for (i = 0; i < sz - 1; i++)
	{
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = 0;
				tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}

int main()
{
	int arr[10] = { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 };
	bubble_sort(arr);
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");

	return 0;
}

观察结果,看看是否成功将数组排序
在这里插入图片描述
并未排序
这到底是为什么呢?
调试发现,bubble_sort中的sz == 1
要搞清楚这个问题,我们必须先弄清传参时的数组名表示什么。

2.数组名是什么?

打印以下数值

int main()
{
	int arr1[10];
	printf("arr1 == %p\n", arr1);
	printf("&arr1[0] == %p\n", &arr1[0]);
	printf("arr1 + 1 == %p\n", arr1 + 1);
	printf("&arr1[0] + 1 == %p\n", &arr1[0] + 1);
	printf("sizeof arr1 == %d\n", sizeof(arr1));
	printf("sizeof &arr1[0] == %d\n", sizeof(&arr1[0]));

	return 0;
}

运行结果:
在这里插入图片描述

其中arr1和&arr1[0]打印的内容相同,arr1 + 1和&arr1[0] + 1打印的内容相同
由此可以看出,arr1和&arr1[0]等价
即数组名所代表的含义是首元素地址


sizeof(arr1)打印的值为40 - - 10个整型数值所占内存空间的大小。
sizeof(&arr1[0])打印的值为4,这是因为&arr1是一个地址,32位平台中地址所占内存空间大小就是4字节。
既然arr1表示首元素地址,为什么sizeof(arr1) == 40呢?
这是因为数组名的使用有两个特例:

  1. sizeof()中的数组名表示整个数组 - - 求整个数组所占内存空间的大小
  2. &后的数组名,&arr1表示整个数组的地址。

用代码验证&<数组名>是表示整个数组

int main()
{
	int arr[10];

	printf("&arr[0] == %p\n", &arr[0]);
	printf("&arr == %p\n", &arr);
	printf("&arr[0] + 1 == %p\n", &arr[0] + 1);
	printf("&arr + 1 == %p\n", &arr + 1);

	return 0;
}

运行结果:
在这里插入图片描述
从结果中可以看出,虽然&arr[0]和&arr打印内容相同,
但&arr[0] + 1的值只比&arr[0]增大了4字节;
而&arr + 1的值比&arr增大了40字节;

说明&arr中的arr此时表示整个数组

3.冒泡排序函数的正确设计

当数组传参时,并不属于上述两种特例,所以仅是将数通的首元素地址传给了函数。
所以即使函数参数部分写成数组的形式:int arr[],其本质上依然是一个指针int* arr
那么函数内部的sizeof(arr),仅是计算一个地址的内存空间大小 - - 4 / 8(32位 / 64位)

将代码修改如下

#include <stdio.h>

void bubble_sort(int arr[], int sz)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < sz - 1; i++)
	{
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = 0;
				tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}

int main()
{
	int arr[10] = { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 };
	// sizeof(arr)中的arr表示整个数组
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz);
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");

	return 0;
}

运行结果:
在这里插入图片描述
注意

当前代码虽然成功的完成了冒泡排序所需的功能,但是仍存在优化空间,举一个比较极端的例子
当要排序的数组如下时
在这里插入图片描述
虽然原代码最终也能输出正确的结果,但是很显然,原数组本来就是有序的,直接输出即可,但原代码还是会吭哧吭哧的反复执行很多次,在这个过程中我们能够发现,当一轮排序中没有交换任何两个元素,此时的数组元素就已经处于有序状态,排序可以直接结束。
所以我们可以在每轮交换中增加一个变量flag,用于记录当前是否进行了交换

优化代码如下:

void bubble_sort(int arr[], int sz)
{
	int i = 0;
	int j = 0;
	int flag = 0;
	for (i = 0; i < sz - 1; i++)
	{
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = 0;
				tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
				flag++;
			}
		}
		if (!flag)
		{
			break;
		}
		flag = 0;
	}
}

六、数组使用实例

这两个数组的使用实例,代码中作出了详细注释,就不再赘述。

1.三子棋 – 可拓展成n子棋

tictactoe.h

#pragma once

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

// 棋盘大小为 ROW x COL
#define ROW 3
#define COL 3

// 游戏状态
// 游戏过程中总共有4种状态
// 1. 玩家获胜
// 2.电脑获胜
// 3.平局
// 4.继续对局
typedef enum
{
	PlayerWin, // 玩家获胜
	ComputerWin, // 电脑获胜
	Equality, // 平局
	Continue // 继续对局
} Status;

// 函数声明
// 棋盘初始化
void initBoard(char [ROW][COL]);

// 三子棋盘游戏
void game(char [ROW][COL]);

// 玩家下棋
void Player(char [ROW][COL]);

// 判断落子位置是否合法:1 - 合法,0 - 非法
int islegal(char [ROW][COL], int, int);

// 打印棋盘
void printBoard(char[ROW][COL]);

//电脑下棋
void Computer(char[ROW][COL]);

// 判断胜负
Status isWin(char [ROW][COL]);

tictactoe.c

#define _CRT_SECURE_NO_WARNINGS

#include "tictactoe.h"

// 初始化棋盘 - 初始化为空格' '
void initBoard(char board[ROW][COL])
{
	int i = 0;
	int j = 0;
	for (i = 0; i < ROW; i++)
	{
		for (j = 0; j < COL; j++)
		{
			board[i][j] = ' ';
		}
	}
}

// 静态函数,只能在当前文件中使用
// 判断谁先手
// 1 - 玩家先,0 - 电脑先
static int whoisFirst()
{
	int input = 1;
	while (1)
	{
		printf("请选择谁先手(0 - 电脑, 1 - 你):>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			return 1;
		case 0:
			return 0;
		default:
			printf("选项不存在\n");
			break;
		}
	}
}

// 用于判断平局的全局变量
static int count;

// 游戏函数
void game(char board[ROW][COL])
{
	// 初始化棋盘
	initBoard(board);
	// 棋盘中总共有ROW * COL个位置可以落子
	count = ROW * COL;
	// 选择谁先手
	int ret = whoisFirst();
	// 保存对局当前的状态
	Status status = Continue;

	// 双方轮流下棋
	if (ret)
	{
		// 打印棋盘
		printBoard(board);
		while (1)
		{
			// 玩家下棋
			Player(board);
			// 判断胜负
			status = isWin(board);
			// 只有Continue才会继续下棋
			if (status != Continue)
			{
				break;
			}
			// 电脑下棋
			Computer(board);
			// 判断胜负
			status = isWin(board);
			// 只有Continue才会继续下棋
			if (status != Continue)
			{
				break;
			}
		}
	}
	else
	{
		while (1)
		{
			// 电脑下棋
			Computer(board);
			// 判断胜负
			status = isWin(board);
			// 只有Continue才会继续下棋
			if (status != Continue)
			{
				break;
			}
			// 玩家下棋
			Player(board);
			// 判断胜负
			status = isWin(board);
			// 只有Continue才会继续下棋
			if (status != Continue)
			{
				break;
			}
		}
	}
	if (status == PlayerWin)
	{
		printf("玩家赢了\n");
	}
	else if (status == ComputerWin)
	{
		printf("电脑赢了\n");
	}
	else
	{
		printf("平局\n");
	}
	printf("终盘为:\n");
	printBoard(board);
}

void Player(char board[ROW][COL])
{
	int x = 0;
	int y = 0;
	printf("你的回合\n");
	// 玩家下棋
	while (1)
	{
		printf("输入落子坐标(输入示例:(1, 2) -- > 1 2):>", ROW, COL);
		scanf("%d %d", &x, &y);
		// 判断落子位置是否合法
		if (islegal(board, x, y))
		{
			board[x - 1][y - 1] = 'P';
			count--;
			// 打印棋盘,便于玩家选择落子点
			printBoard(board);
			break;
		}
		else
		{
			printf("非法位置\n");
		}
	}
}

int islegal(char board[ROW][COL], int row, int col)
{
	// 落子位置位于棋盘内
	if (row >= 1 && row <= ROW && col >= 1 && col <= COL)
	{
		if (board[row - 1][col - 1] == ' ')
		{
			return 1;
		}
		return 0;
	}
	// 落子位置位于棋盘外
	else
	{
		return 0;
	}
}

void printBoard(char board[ROW][COL])
{
	int i = 0;
	int j = 0;
	// 打印列号 - 0 1 2 ..... COL
	for (i = 0; i <= COL; i++)
	{
		if (i == 0)
		{
			printf("  ", i);
		}
		else
		{
			printf("%d ", i);
		}
	}
	printf("\n");

	// 打印一行 - <行号> *|*|.....|COL
	for (i = 0; i < ROW; i++)
	{
		printf("%d ", i + 1); // 打印行号
		// 打印一行棋盘
		for (j = 0; j < COL; j++)
		{
			printf("%c", board[i][j]);
			if (j != COL - 1)
			{
				printf("|");
			}
		}
		printf("\n");
		// 打印行间的分隔线
		if (i != ROW - 1)
		{
			printf("  "); // 空出行号位置
			for (j = 0; j < COL; j++)
			{
				printf("- ");
			}
			printf("\n");
		}
	}
}

// 以生成的随机数作为电脑的落子点
void Computer(char board[ROW][COL])
{
	int x = 0;
	int y = 0;
	printf("电脑的回合\n");
	while (1)
	{
		x = (rand() % ROW) + 1;
		y = (rand() % COL) + 1;
		if (islegal(board, x, y))
		{
			board[x - 1][y - 1] = 'C';
			count--;
			printBoard(board);
			break;
		}
	}
}

Status isWin(char board[ROW][COL])
{
	int i = 0;
	int j = 0;
	// 判断是否平局 -- 棋盘下满
	if (!count)
	{
		return Equality;
	}

	// 判断ROW行
	for (i = 0; i < ROW; i++)
	{
		for (j = 0; j < COL - 1; j++)
		{
			if (board[i][j] != board[i][j + 1])
			{
				break;
			}
		}
		if (j == COL - 1)
		{
			if (board[i][j] == 'P')
			{
				return PlayerWin;
			}
			else if (board[i][j] == 'C')
			{
				return ComputerWin;
			}
		}
	}
	
	// 判断COL列
	for (i = 0; i < COL; i++)
	{
		for (j = 0; j < ROW - 1; j++)
		{
			if (board[j][i] != board[j + 1][i])
			{
				break;
			}
		}
		if (j == ROW - 1)
		{
			if (board[j][i] == 'P')
			{
				return PlayerWin;
			}
			else if (board[j][i] == 'C')
			{
				return ComputerWin;
			}
		}
	}
	
	// 主对角线
	for (i = 0, j = 0; i < ROW - 1 && j < COL - 1; i++, j++)
	{
		if (board[i][j] != board[i + 1][j + 1])
		{
			break;
		}
	}
	if (j == ROW - 1)
	{
		if (board[i][j] == 'P')
		{
			return PlayerWin;
		}
		else if (board[j][i] == 'C')
		{
			return ComputerWin;
		}
	}
	
	// 副对角线
	for (i = ROW - 1, j = 0; i >= 0 && j < COL - 1; i--, j++)
	{
		if (board[i][j] != board[i - 1][j + 1])
		{
			break;
		}
	}
	if (j == ROW - 1)
	{
		if (board[i][j] == 'P')
		{
			return PlayerWin;
		}
		else if (board[j][i] == 'C')
		{
			return ComputerWin;
		}
	}
	
	return Continue;
}

test.c

#include "tictactoe.h"

void menu()
{
	printf("********************\n");
	printf("*****  1.Play  *****\n");
	printf("*****  0.Exit  *****\n");
	printf("********************\n");
}

int main()
{
	int input = 0;
	printf("********************\n");
	printf("*****  三子棋  *****\n");

	// 创建棋盘
	char board[ROW][COL];
	// 设置随机数
	srand((unsigned int)time(NULL));

	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game(board);
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("选项错误\n");
			break;
		}
	}while(input);

	return 0;
}

2.扫雷 - - 实现了雷区发散查找

假设布雷区域的大小为9x9,内部黑框部分,在它的外围,增加了一圈格子用以辅助,
使得计算布雷区域的任意一个格子周围的布雷数量的逻辑相同
在这里插入图片描述

mine_sweeping.h

#pragma once

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

// 布雷区域大小
#define MAX_ROW 9
#define MAX_COL 9

// 保存数据空间大小
// 排雷时,需要判断一个格子周围的8个格子中有几个地雷
// 为了保证整个布雷区域的判断逻辑相同,在该区域外增加了一圈辅助格子
#define MAX_ROWS MAX_ROW + 2
#define MAX_COLS MAX_COL + 2

// 地雷数量
#define MAX_MINE 10

typedef enum
{
	Unknown, // 未知
	Sweep, // 已排雷
	Mark // 标记为有雷
} Status;

typedef struct
{
	char mine; // 'm' - 有地雷,' ' - 无地雷
	Status status;
	int numofMine;
} Cell;

// 布置雷区
void initBoard(Cell [MAX_ROWS][MAX_COLS]);

// 打印雷区 -- 测试用函数
void displayMine(Cell [MAX_ROWS][MAX_COLS]);

// 打印玩家看到的雷区
void displayBoard(Cell [MAX_ROWS][MAX_COLS]);

// 排雷
int sweep(Cell [MAX_ROWS][MAX_COLS]);

// 判断玩家排雷位置是否合法,1 - 合法,0 - 非法
int islegal(Cell [MAX_ROWS][MAX_COLS], int, int);

// 计算周围的地雷数量
void findMine(Cell [MAX_ROWS][MAX_COLS], int, int);

// 放置标记
void putMark(Cell[MAX_ROWS][MAX_COLS], int, int);

// 玩家选择操作 - 排雷,放置标记
int operation();

// 游戏逻辑
void game();

mine_sweeping.c

#define _CRT_SECURE_NO_WARNINGS

#include "mine_sweeping.h"

// 随机布雷
void initBoard(Cell board[MAX_ROWS][MAX_COLS])
{
	int i = 0;
	int j = 0;
	int x = 0;
	int y = 0;

	// 数组元素初始化
	for (i = 0; i < MAX_ROWS; i++)
	{
		for (j = 0; j < MAX_COLS; j++)
		{
			// 雷区重置 - 没有雷
			board[i][j].mine = ' ';
			board[i][j].status = Unknown;
			board[i][j].numofMine = 0;
		}
	}

	i = MAX_MINE;
	// 布雷,每布一颗,i--,i == 0时,循环结束 
	while (i)
	{
		// 雷区的下标范围是 (1, 1) 到 (MAX_ROW, MAX_COL)
		x = (rand() % MAX_ROW) + 1;
		y = (rand() % MAX_COL) + 1;
		// 该位置没有雷
		if (board[x][y].mine != 'm')
		{
			board[x][y].mine = 'm';
			i--;
		}
	}
}

void displayMine(Cell board[MAX_ROWS][MAX_COLS])
{
	int i = 0;
	int j = 0;

	printf("  ");
	// 打印列号
	for (i = 1; i <= MAX_COL; i++)
	{
		printf("%d ", i);
	}
	printf("\n");

	// 打印雷区
	for (i = 1; i <= MAX_ROW; i++)
	{
		// 行号
		printf("%d ", i);
		// 打印一行
		for (j = 1; j <= MAX_COL; j++)
		{
			printf("%c ", board[i][j].mine);
		}
		printf("\n");
	}
}

void displayBoard(Cell board[MAX_ROWS][MAX_COLS])
{
	int i = 0;
	int j = 0;

	printf("  ");
	// 打印列号
	for (i = 1; i <= MAX_COL; i++)
	{
		printf("%d ", i);
	}
	printf("\n");

	// 打印雷区
	for (i = 1; i <= MAX_ROW; i++)
	{
		// 行号
		printf("%d ", i);
		// 打印一行
		for (j = 1; j <= MAX_COL; j++)
		{
			if (board[i][j].status == Unknown)
			{
				printf("%c ", '*'); 
			}
			else if (board[i][j].status == Mark)
			{
				printf("%c ", '#'); // 标记为有雷
			}
			else
			{
				printf("%d ", board[i][j].numofMine);
			}
		}
		printf("\n");
	}
}

// 返回1 - 排除该位置,返回0 - 踩到地雷,你被炸死了,游戏结束
int sweep(Cell board[MAX_ROWS][MAX_COLS])
{
	int x = 0;
	int y = 0;
	while (1)
	{
		printf("请输入排雷位置 (1, 2) -> 1 2:>");
		scanf("%d %d", &x, &y);
		// 当前位置合法 -- (x, y)指向的格子的status为Unknown
		if (islegal(board, x, y))
		{
			// 未踩到地雷
			if (board[x][y].mine != 'm')
			{
				findMine(board, x, y); // 寻找(x, y)附近的地雷数量
				return 1;
			}
			else
				return 0;
		}
	}
}

int islegal(Cell board[MAX_ROWS][MAX_COLS], int row, int col)
{
	if (row >= 1 && row <= MAX_ROW && col >= 1 && col <= MAX_COL)
	{
		if (board[row][col].status == Unknown)
		{
			return 1;
		}
		return 0;
	}
	return 0;
}

// 保存雷区中目前需要排雷的格子数
static int num;

// 如果(row, col)周围没有地雷,会向周围发散寻找,
void findMine(Cell board[MAX_ROWS][MAX_COLS], int row, int col)
{
	int count = 0;
	int i = 0;
	int j = 0;
	// 将当前格子的状态转为已排雷
	board[row][col].status = Sweep;
	num--;
	
	// 遍历周围
	// 上、中、下三行
	for (i = -1; i <= 1; i++)
	{
		// 前、中、后三列
		for (j = -1; j <= 1; j++)
		{
			if (board[row + i][col + j].mine == 'm')
			{
				count++;
			}
		}
	}
	// 当前位置没有雷,向周围发散寻找
	if (count == 0)
	{
		for (i = -1; i <= 1; i++)
		{
			for (j = -1; j <= 1; j++)
			{
				// 格子的状态为Sweep就不用计算了,边界的辅助区域也不用计算
				if (board[row + i][col + j].status != Sweep 
						&& row + i >= 1
						&& row + i <= MAX_ROW
						&& col + j >= 1
						&& col + j <= MAX_COL)
				{
					findMine(board, row + i, col + j);
				}
			}
		}
	}
	board[row][col].numofMine = count;
}

// 可以选择已排雷区域,相当于什么也没干
void putMark(Cell board[MAX_ROWS][MAX_COLS])
{
	int x = 0;
	int y = 0;

	printf("请输入标记位置 (1, 2) -> 1 2:>");
	scanf("%d %d", &x, &y);
	// 当前位置合法 -- (x, y)指向的格子的status为Unknown
	if (islegal(board, x, y))
	{
		if (board[x][y].status != Sweep)
		{
			board[x][y].status = Mark;
		}
	}
}

int operation()
{
	int input = 0;
	printf("1.排雷\n");
	printf("2.放标记\n");
	printf("选择操作:>");
	scanf("%d", &input);
	switch (input)
	{
	case 1:
		return 1;
	case 2:
		return 0;
	default:
		printf("选项错误\n");
		break;
	}
}

void game()
{
	// 用一个二维数组保存地雷信息
	// 每个元素有几个属性
	// 1.是否有地雷
	// 2.当前的状态
	//   a.未知
	//	 b.已排雷
	//	 c.标记为有雷
	// 3.周围地雷的数量
	Cell board[MAX_ROWS][MAX_COLS];
	// 初始化 -- 往雷区中埋入MAX_MINE颗地雷
	initBoard(board);
	num = MAX_ROW * MAX_COL - MAX_MINE;
	// 打印雷区 -- 测试用函数
	//displayMine(board);
	// 打印排雷用雷阵
	displayBoard(board);

	// 游戏持续进行,直到待排雷区域数 == 0
	while (num)
	{
		if (operation())
		{
			// 排雷
			int ret = sweep(board);
			if (ret == 0)
			{
				printf("踩到地雷,你被炸死了,游戏结束\n");
				break;
			}
		}
		else
		{
			putMark(board);
		}
		// 打印雷区当前状态
		displayBoard(board);
	}
	if (num)
	{
		printf("游戏失败\n");
	}
	else
	{
		printf("游戏胜利\n");
	}
	printf("雷区情况:\n");
	displayBoard(board);
	printf("地雷分布:\n");
	displayMine(board);
}

test.c

#include "mine_sweeping.h"

void menu()
{
	printf("********************\n");
	printf("*****  1.Play  *****\n");
	printf("*****  0.Exit  *****\n");
	printf("********************\n");
}

int main()
{
	int input = 0;
	srand((unsigned int)time(NULL));

	do
	{
		printf("********************\n");
		printf("*****   扫雷   *****\n");
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			//printf("mine_sweeping\n");
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("没有这个选项\n");
			break;
		}
	} while(input);

	return 0;
}

雷区发散查找,函数实现如下

// 如果(row, col)周围没有地雷,会向周围发散寻找,
void findMine(Cell board[MAX_ROWS][MAX_COLS], int row, int col)
{
	int count = 0;
	int i = 0;
	int j = 0;
	// 将当前格子的状态转为已排雷
	board[row][col].status = Sweep;
	num--;
	
	// 遍历周围
	// 上、中、下三行
	for (i = -1; i <= 1; i++)
	{
		// 前、中、后三列
		for (j = -1; j <= 1; j++)
		{
			if (board[row + i][col + j].mine == 'm')
			{
				count++;
			}
		}
	}
	// 当前位置没有雷,向周围发散寻找
	if (count == 0)
	{
		for (i = -1; i <= 1; i++)
		{
			for (j = -1; j <= 1; j++)
			{
				// 格子的状态为Sweep就不用计算了,边界的辅助区域也不用计算
				if (board[row + i][col + j].status != Sweep 
						&& row + i >= 1
						&& row + i <= MAX_ROW
						&& col + j >= 1
						&& col + j <= MAX_COL)
				{
					findMine(board, row + i, col + j);
				}
			}
		}
	}
	board[row][col].numofMine = count;
}

在这里插入图片描述


总结

本文对一维和二维数组的初始化、创建、使用,作出的简要的介绍,并通过冒泡排序算法的实现,详细说明了当数组作为函数参数时的性质 - - 传递的是首元素地址。在最后,给出了两个数组的使用实例,附带了完整的实现代码,并配有详细注释。

RK3568-Linux-麒麟系统---更新制作麒麟rootfs RK3568 Linux系统 麒麟国产麒麟镜像 阅读详情

相关推荐

Altium AD20的PCB板框绘制、定义板子形状、重新设置原点、放置尺寸标注

AD20的板框绘制与以前的版本不同,以前板框是在KeepOut层,而在AD20中没有这个层,绘制板框需要到机械层Mechanical1。 板框大小评估 首先进行板框大小评估。 工具 - 器件摆放 - 在矩形区域摆放。 之后通过鼠标框选矩形区域,元件会自动按照框选的范围进行摆放。这个操作可以重复多次,最后大致得出需要的板框尺寸。确定好后右键。 板框绘制,重新定义板子形状 点击下方的 Mechanical1,切换到机械层。放置线条,根据元件摆放来绘制PCB大致的形状,将元件完全包裹。

Mark_md的博客 10万+

C语言修炼秘籍【第零章】c语言概述

c语言是一门编程语言。语言的作用就是交流,人们可以利用它与计算机进行交流,让计算机知道你需要它完成什么工作。本系列文章是对笔者在c语言学习的总结记录。本文会先粗略的介绍c语言中需要学习的内容。

PLKDTHFS的博客 1663

泰森多面体Voronoi 3D-V5.0 功能介绍

Abaqus Voronoi3D

qq_34840441的博客 3364

c语言修炼秘籍【第五章】指针

本文会从介绍指针是什么开始,循序渐进地介绍指针的使用、指针使用时会遇到的问题、指针能进行哪些运算、指针与数组之间的关系,以及指针数组这些内容。本文对c语言中使用的指针进行了较简略的介绍,包括什么是指针,指针类型有什么用,指针的一些运算,什么是野指针,如何利用指针访问数组,二级指针使用中会出现的问题,以及指针数组。

PLKDTHFS的博客 1763

乐企增值税及附加税费(一般纳税人适用)申报表填报能力说明文档V3.009

申报表填报,是指企业在自有业务系统中将算税结果数据按预填规则要求加工汇总后,进行申报表填报的过程。申报表填报前对纳税申报所需的前提条件进行校验。校验通过后,应用前序算税结果数据及预填规则进行纳税申报表预填,生成申报表数据。申报表数据通过申报业务规则校验后,经纳税人确认即完成申报表填报业务。

记录点滴 344

COMSOL介电弹性体数值仿真指南:预拉伸圆形DEA的稳态变形分析(II)

本期博客是《COMSOL介电弹性体数值仿真指南》系列博客中的一期。聚焦于介电弹性体驱动器(DEA)中最为经典的预拉伸圆形DEA驱动器构型,对其在COMSOL平台中的具体建模步骤进行介绍与讨论。

忘了面孔的Batou 353

c语言修炼秘籍【第二章】函数

本文会从函数是什么开始,逐一介绍函数的语法结构,如何声明和定义一个函数,函数的参数类型,调用函数时的传参类型,函数的调用方式,函数递归,并在最后对汉诺塔、青蛙跳台阶和八皇后问题进行详细分析(包含全部代码)

PLKDTHFS的博客 1442

点云去噪:基于点云拟合的方法

点云处理是计算机视觉中的重要任务之一,而点云去噪是点云处理的关键步骤之一。在实际应用中,由于采集设备的噪声、运动估计误差或者环境干扰等因素,点云数据中常常包含有噪声点,这些噪声点会对后续的点云分析和处理造成不良影响。一种简单的方法是计算点到拟合曲面的距离,并将距离超过某个阈值的点定义为噪声点,进行过滤。一种简单的方法是计算点到拟合曲面的距离,并将距离超过某个阈值的点定义为噪声点,进行过滤。该方法通过拟合点云数据的曲面模型,对噪声点进行识别和过滤,从而得到更加干净的点云数据。

AuSwift的博客 638

C语言修炼秘籍之——函数递归

函数递归是什么玩意儿?有啥用啊?来个例题看看啊?和迭代之间有啥区别啊? 这位客官,咱里边儿请~坐下来听我娓娓道来~

2301_80029060的博客 1534

基于YOLOv5的人脸识别系统:从构建到应用的全流程指南

本文详细介绍了基于YOLOv5的人脸识别系统开发全流程。首先阐述了YOLOv5模型的高效性和准确性优势,随后分步骤讲解了深度学习环境配置、数据集准备(推荐WIDER FACE数据集)和模型训练方法。重点展示了如何将训练好的模型集成到Tkinter开发的GUI界面中,实现图片上传、人脸检测和结果可视化功能。文章提供完整的代码示例和飞书文档链接(密码:946m22&8),帮助开发者快速构建可交互的人脸识别系统,适用于安防、门禁等多种应用场景。通过该项目,读者可掌握YOLOv5的实际应用和界面集成技术。

FJN110的博客 168

LangChain .NET:C#开发者构建AI应用的RAG框架与实战指南

大语言模型(LLM)作为当前人工智能的核心技术,通过深度学习海量文本数据,掌握了强大的语言理解和生成能力。其原理基于Transformer架构,通过自注意力机制捕捉长距离依赖关系,从而实现对复杂语义的建模。这一技术价值在于能够将非结构化的自然语言转化为可计算、可推理的表示,为各类智能应用提供认知基础。在实际工程实践中,LLM常被应用于构建检索增强生成(RAG)系统和AI智能体(Agent)。RAG通过结合外部知识库检索与LLM生成,有效提升了问答的准确性和事实性,是构建企业知识库、智能客服等场景的关键技术。

weixin_42538175的博客 704

c语言修炼秘籍【第七章】const与c语言中一些错误代码

而数组内部的内存使用分配是从低到高的,arr[0]的地址在最低位,当循环进行到第11次 - - arr[11]设置的其实是i的值,此时将 i 的值设为了0,循环继续。这是因为,程序要进行报错也是需要执行时间的,访问arr[11]时程序仍处于循环中,没有时间报错就又进入了下一次循环。test1()可以正常使用指针对n为进行修改,也可以对p指向的位置进行修改。test2()不能通过指针对n进行修改,可以对p指向的位置进行修改。,arr后分配,地址会比i的地址低,分配到的地址为。,i先分配空间,地址为。

PLKDTHFS的博客 674

10、智慧变电站 - 加载vue大屏元素(整体效果)

这一篇文章非常简单,但是也非常出效果阅读前面几篇文章,都可以看到有顶部、左侧、右侧的面板,主要我不会录gif图片,大屏元素面板还有一些动态加载的效果。 vue的transition标签 Vue 提供了 transition 的封装组件,可以给任何元素和组件添加进入/离开过渡;在进入/离开的过渡中,会有 6 个 class 切换:v-enter 、v-enter-active 、v-enter-to 、v-leave 、v-leave-active 、v-leave-to。我们正好使用这个特性来达到面板进入

github_39988036的博客 1039

c语言修炼秘籍【第四章】操作符

本文会对c语言中使用到的操作符进行介绍,包括算术操作符移位操作符位操作符赋值操作符单目操作符关系操作符逻辑操作符条件操作符逗号表达式下标引用、函数调用和结构成员表达式求值单目操作符的种类!// 逻辑反操作- // 负值+ // 正值& // 取地址sizeof // 操作数的类型长度(以字节为单位)~ // 按二进制位取反++ // 前置、后置++-- // 前置、后置--* // 间接访问操作符(解引用操作符) -- 指针的使用。

PLKDTHFS的博客 1136

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

c语言修炼秘籍【第一章】分支与循环语句

在处理现实中的问题时,我们总会遇到需要作出选择或是需要重复多次的完成一种事件。你去图书馆学习,需要日复一日的坚持,每多学一天,你的代码功力就能+1,当你的代码功力,达到了某一程度,你就能够迎娶白富美,走上人生巅峰。为了实现这些目的,c语言提供了分支语句,用以实现选择;提供循环语句,用以实现重复多次的机械活动;表达式语句函数调用语句控制语句复合语句空语句分支、循环语句属于控制语句。控制语句用于控制程序的执行流程,以实现程序的各种结构方式,它们由各种特定的语句定义符构成;

PLKDTHFS的博客 1244

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

c语言修炼秘籍【第六章】结构体

将结构体与数组进行对比:数组是一组相同类型的元素集合;结构体是一些值的集合,它们类型可以不同,这些值被称为成员变量。使用结构体类型定义变量int x;int y;} p1;// 声明类型的同时定义变量p1// 定义结构体变量 p2// 初始化:定义变量的同时赋初值struct Stu// 名字int age;// 年龄int data;// 结构体嵌套的初始化// 结构体嵌套的初始化本文对c语言中使用到的结构体进行了简单介绍,更详细的会在后续攻略中详解。

PLKDTHFS的博客 761

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

C语言修炼秘籍之——动态内存分配

客官可是在寻找动态内存分配秘方?快快请进,且听我娓娓道来~

2301_80029060的博客 1240

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第二式】指针

字符指针数组指针指针数组数组传参和指针传参几种数组、指针之间的比较函数指针函数指针数组指向函数指针数组的指针回调函数指针和数组的笔试题指针是一个存储地址的变量,地址是一块内存空间的唯一标识;指针的大小是固定的(4字节或8字节);指针是有类型的,它的类型决定它 + - 整数时移动的步长、*解引用操作时的权限;指针是能够进行运算的,+ - 整数、指针相减、指针的关系运算;接下来,就让我们继续探讨指针的进阶主题。对于普通的指针,我们都已经很熟悉了。

PLKDTHFS的博客 1518

JAVA修炼秘籍第四章《闭关修炼》

往期: JAVA 修炼秘籍第一章:《痛苦的折磨》 JAVA 修炼秘籍第二章:《逐渐魔化》 JAVA 修炼秘籍第三章:《绝地反击》 文章目录一、方法方法的重载方法递归二、数组获取数组长度与访问数组拷贝数组与以字符串的形式打印数组 一、方法 在JAVA中什么是方法呢,方法其实与C语言中的函数是大相径庭的,方法可以帮助我们更清晰的理解代码,并且可以重复使用重复调用,大大减少了代码的重复性。 方法的书写格式如下: public class Prog{ public static int ad

Coach_zhang的博客 657

C语言指针修炼手册:从“失控野指针”到“空间掌控大师”的修炼之路

本文聚焦 C 语言指针进阶,解析从 “野指针” 到 “指针大师” 的修炼路径。首先揭示野指针成因(未初始化、释放未置空、越界 / 作用域失效)及规避策略(定义即初始化、释放即置空、管控生命周期)。接着深入指针核心知识,包括类型本质、二级指针、与数组的深层关联(数组名双重身份、指针数组与数组指针辨析)、函数指针及回调机制。实战部分结合结构体(柔性数组设计)与底层编程(内存映射、类型转换)展现指针应用。最后给出系统化学习路径,强调借助 GDB、Valgrind 等工具调试,破除 “指针万能论” 等误区,倡导通过

科技改变人类,技术成就未来 2万+

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第一式】数据的存储

本章节的重点是数据类型详细介绍整型在内存中的的存储:原码、反码、补码大小端字节序介绍及判断浮点型在内存中的存储解析char // 字符数据类型short // 短整型int // 整型long // 长整型long long // 更长的整型float // 单精度浮点数double // 双精度浮点数// c语言有没有字符串类型?// 没有以及他们所占存储空间的大小。使用这个类型开辟内存空间的大小(大小决定了使用范围)如何看待内存空间的视角什么是大端小端?

PLKDTHFS的博客 1487

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第五式】动态内存管理

大家在使用数组时肯定都会有过这样的疑问,如果一个数组的长度不足了,可以延长这个数组吗?使其能够满足存储的需求。又或者是现在不确定数组应该设为多长,先把它放着,等之后再设置它的长度。上面的这些需求,数组显然是无法实现的,那么c语言中有什么东西能够做到这一点呢?动态内存分配!你现在不知道需要多大的空间,没关系,之后你明确了的时候,使用动态内存分配,将你要的空间分配给即可。为什么存在动态内存分配动态内存函数的介绍mallocfreecallocrealloc常见的动态内存错误。

PLKDTHFS的博客 1293

C语言修炼秘籍之——结构体、位段、联合体、枚举

这位客官是在寻找最全面的结构体、位段、联合体与枚举的文章吗?不妨到里头坐坐,听我娓娓道来~

2301_80029060的博客 1169

c语言中switch语句流程图_C语言专题:最全C语言笔记,修炼高人的武林秘籍,网友:我也想要...

电子产品单片机设计通过自学C语言,现在将自己总结出来的学习笔记分享给大家,希望大家喜欢,持续分享优质内容,我们将终身学习进行到底,活到老,学到老,学习永不止境。如果大家对学习笔记有不明白的地方,欢迎给我留言或者私信我,我会保证对所有人都一一进行回复的。第一章 C语言概述1、语言的发展与特点:语言的发展与特点2、语言程序的结构和语法法则:语言程序的结构和语法法则3、程序开发过程:程序开发过程第二章:...

weixin_39883433的博客 385

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第三式】字符函数和字符串函数

c语言中对字符和字符串的处理非常频繁,但c语言本身是没有字符串类型的,字符串通常存储在常量字符串或字符数组中。常量字符串适用于不需要对其进行修改的字符串函数。strlen函数的作用是获得一个字符串的长度;接收一个const修饰的字符指针变量,这个指针指向的字符串必须有结束符\0;返回一个无符号(易错点)的int类型的数值,表示这个字符串的长度(不包括结束符\0如果没有返回值,则表示出错;// 使用示例int main()printf("a的长度 = %d\n", strlen(a));

PLKDTHFS的博客 926

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第六式】文件操作

为什么使用文件什么是文件文件的打开和关闭文件的顺序读写文件的随机读写文本文件和二进制文件文件读取结束的判定文件缓冲区磁盘上的文件就是文件。但是在程序设计上,我们谈到的文件一般有两种:程序文件、数据文件;本节详细的对c语言中数据文件的使用进行了详细的介绍,包括对文件读写操作使用的函数,以及通过文件位置指针的操作函数来对文件进行随机读写,并指出了feof函数可能出现的使用错误,在最后,介绍了文件缓冲区的概念;

PLKDTHFS的博客 1226
上一篇: c语言修炼秘籍【第二章】函数
下一篇: c语言修炼秘籍【第四章】操作符
YKPG
博客等级 码龄6年 344粉丝 37原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值