自学内容网 自学内容网

C语言复习第4章 数组

目录

一、一维数组的创建和初始化

1.1数组的创建

数组的创建方式:type_t arr_name [const_n]
type_t 是指数组的元素类型
const_n 是一个常量表达式 用来指定数组的大 比如int arr[5+6]

1.2 变长数组

int arr[5];int arr[5+6]都可以
可不可以是int arr[n]呢?
在这里插入图片描述

1.3 数组的初始化

1.不完全初始化 会根据指定的元素个数 把剩余部分初始化为0
2.char数组不完全初始化 剩余部分默认初始化为’\0’ '\0'的码值是0
3.如果不写元素个数 那就必须初始化 编译器会根据初始化的内容确定元素个数 在这里插入图片描述

所以下图的两个数组是不一样的
一个是arr[3] = {1,2,3}
一个是arr[10] = {1,2,3,0,0,0,0,0,0,0}
在这里插入图片描述

  • 注意字符串自带’\0’
    在这里插入图片描述

  • 一些离谱的写法:
    在这里插入图片描述

1.4 全局数组默认初始化为0

  • 在VS2022里 全局数组不初始化 默认都是0
  • 但是局部数组不初始化 整形数组是cccccccc char数组是问号
    在这里插入图片描述
    在这里插入图片描述

1.5 区分两种字符数组

  • "abc"自带’\0’
  • %s是打印到第一个’\0’才停止的
    在这里插入图片描述

1.6 用sizeof计算数组元素个数

这里sizeof(arr[0]) 也可以写成sizeof(int)
但是前者更合逻辑(一个元素的大小)
在这里插入图片描述

1.7 如何访问数组元素

在这里插入图片描述

1.8 一维数组在内存中的存储(连续存储)

● 内存单元的大小为1字节 而int是4个字节(则每个元素占4个内存单元)
● 一维数组在内存中是 连续存储
随着数组下标增长 地址由低到高变化(从低地址用到高地址)
在这里插入图片描述

再结合之前画过的图 推测:
下标越高的元素 其实是越先入栈的 在这里插入图片描述
在这里插入图片描述

1.9 访问数组元素的另一种方式:指针变量

● 所以 给我数组的首元素地址 根据连续存储特点 我就能顺藤摸瓜依次找到整个数组
● p是个整型指针 +1就跳过一个整型
*(p+i) == arr[i]在这里插入图片描述
在这里插入图片描述

p+i == &arr[i]
在这里插入图片描述

1.10 数组越界是运行时错误

  • 数组的下规定是从0开始的 如果数组有n个元素 最后一个元素的下标就是n-1
  • 所以数组的下标如果小于0 或者大于n-1 就是数组越界访问了
  • C语言本身是不做数组下标的越界检查 编译器也不一定报错
  • 编译器不报错 并不意味着程序就是正确的 所以写代码时 做好越界的检查

打印随机值:
可能是没有初始化 也可能是数组越界访问
下图编译器虽然不报错 但是程序明显是错误的
在这里插入图片描述

假如这里给arr[i]赋值的话 就会报错在这里插入图片描述

二、二维数组的创建和初始化

2.1 二维数组的创建

在这里插入图片描述

2.2 二维数组的初始化

  • 不完全初始化 默认也是0和’\0’
    在这里插入图片描述
  • 可以帮一维数组的每个元素看做一维数组
    在这里插入图片描述

2.3 行可以省略 列不可以省略

如果下面这个不给行数3 定义成[ ][4] 它也是可以知道是1234 5678 9000的
因为反正知道一行要放几个元素(列数已知)
如果定义成[4][ ] 编译器就不知道怎么办 一上来要怎么放?
所以
列不可以省略
行可以通过每列放几个来确定 所以行可以省略(前提是 后面有具体的初始化内容了)
在这里插入图片描述

2.4 二维数组在内存中的存储(连续存储)

  • 二维数组在内存中 其实也是连续存放的
    在这里插入图片描述
    在这里插入图片描述

2.5 二维数组的遍历方式

其实也一维数组类似
方式1 利用行号和列号
方式2 利用其连续存放的特性 把下图的二维数组直接看作一个12个元素的一维数组
在这里插入图片描述

方式3 利用2.6的理解
2.6说了:arr[i]是第i行一维数组的数组名
arr是二维数组的数组名
数组名 单独 放在sizeof里 求的是整个数组的大小!
sizeof(arr[0][0])是一个元素的大小 等价于sizeof(int)
整个二维数组大小/每一行一维数组大小=sizeof(arr)/sizeof(arr[0])=行数
每一行一维数组大小/每个元素大小=sizeof(arr[0])/sizeof(arr[0][0])=列数
在这里插入图片描述

2.6 arr[行号]可以理解为:该行一维数组的数组名

arr[3][4] = {1,2,3,4,5,6,7,8,7,7,7,7}: 三个一维数组 每个一维数组四个元素
1 2 3 4—>数组名arr[0] arr[0][1]=2
5 6 7 8—>数组名arr[1]
7 7 7 7—>数组名arr[2]

二维数组是一维数组的数组
二维数组的每个元素(每一行) 可以看做一行一维数组 且每一行的数组名就是arr[行号]
如果把1 2 0 0看成一个一维数组的话
类比:
arr[i]:arr数组名 i下标 则arr[0][j]:arr[0]数组名 j下标
其中**arr[0]指的就是1 2 0 0这个一维数组的数组名**
以此类推 第二行的数组名就是arr[1]
在这里插入图片描述

三、数组名

3.1 数组名是首元素地址 本质是指针变量

在这里插入图片描述
在这里插入图片描述

3.2 数组名代表整个数组的两个例外:sizeof和&

● sizeof(单独放一个数组名) 则这里的数组名表示整个数组 计算的是整个数组的大小 单位是字节
● &单独跟一个数组名 则这里的数组名也表示整个数组 取出的是整个数组的地址 是个数组指针
除了这两个例外 其他遇到的所有的数组名 都表示首元素地址

3.3 sizeof(arr)和sizeof(arr+0)的区别

sizeof(arr+0) 数组名不是单独放在sizeof里面 所以arr表示的还是首元素地址
arr如果参与运算 那肯定是个指针了 也就是看做首元素地址
在这里插入图片描述

3.4 arr和&arr类型的区别(指针+1)

在这里插入图片描述

arr和&arr虽然值相同 但类型是不一样的!!
arr是整型指针arr的类型是int*
&arr是数组指针&arr的类型是int (*)[10]
这就是本质区别
在这里插入图片描述

在这里插入图片描述

p+1跳过几个字节 取决于指针的类型
int* p是整型指针 p+1跳过一个int大小的空间 4字节-类型是int*
char* p是字符指针 p+1跳过一个char大小的空间 1字节-类型是char*
&数组名是数组指针 p+1跳过一个数组大小的空间 元素个数*元素大小个字节-类型是int(*)[10]
下图B08-AE0 = 28 = 十进制的40
在这里插入图片描述

3.5 &arr 和*(&arr) 数组指针解引用得到数组名

在这里插入图片描述

  • 所以都是求的整个数组的大小
    在这里插入图片描述

3.6 **(&arr) 解引用两次是什么?

再给p3解引用一次 发现就拿到了首元素1
但是p3可不是二级指针 传参的时候可不能写成二级指针
二级指针 一定是某个地址的地址
但是这里 p3是一整个数组的地址 p3应该是数组指针 而不是二级指针

在这里插入图片描述
其实我觉得 p3还真的可以理解成地址的地址
因为arr本身就是首元素地址 &arr不就是把地址的地址拿到了
(只能这么理解理解 但是这种说法是错的 前面已经提到过 &单独数组名拿的是整个数组的地址)
但是只能自己偷偷这么去理解 语法不是这样的

3.7 sizeof(*&arr) 和 sizeof(arr)

  • 求的都是整个数组的大小
    在这里插入图片描述

3.8 再理解一下 什么叫单独放在&或sizeof(含总结)

在这里插入图片描述

单独的情况下 arr[1]表示第二行数组的数组名
如果不是arr[1]"两个单独"的情况
那么数组名arr[1]表示首元素地址(就是第二行第一个元素的地址)

在这里插入图片描述

3.9 sizeof(数组名)和strlen(数组名)

  • strlen是专门针对字符串的函数
  • char arr[10]=“abc”;
    sizeof(arr)求出来就是10字节 放了什么内容没关系
    而strlen(arr)求到的就是\0之前的 求到3

四、数组(名)作为函数参数

4.1 数组(名)作为函数参数 本质传的是什么?

● arr不符合前面提到的"两个单独"这里arr就是首元素地址 本质上是指针
● 既然传过来的是地址 那么用指针接 才更规范
在这里插入图片描述
char ch[]本质就是char* ch
ch是指针变量 大小恒为8/4
在这里插入图片描述

4.2 冒泡排序函数的错误设计

下图是冒泡排序的一种经典错误
形参的int arr[] 本质上就是整型指针int* arr
既然本质是指针 sizeof(arr)计算的就是指针的大小 永远是8字节or4字节
则sizeof(arr)/sizeof(arr[0])恒为2或者1
在这里插入图片描述
在这里插入图片描述


● 把sz在main算好作为参数传进来 才是正确的做法
● 因为在main里 单独 把数组名arr给sizeof( ) 求的就是整个数组的大小
参考代码:

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

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

Sort(arr, sizeof(arr) / sizeof(int));
for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
printf("%d ", arr[i]);
return 0;
}

其实主要看sizeof( )里放的本质是什么
如果在函数里再新定义一个数组 还是一样可以正确求出该数组的大小(字节)的
在这里插入图片描述

4.3 理解形参int arr[ ] arr[j] &arr[j]的本质

● 把本质是int* arr的东西写成int arr[ ] 主要是为了用下标访问的时候可能更好理解
int arr[]从形式上好理解 但本质还是int* arr
arr[j]本质上是*(arr+j) 表示访问下标为j的元素
&arr[j]本质上是arr+j 表示下标为j的元素的地址
在这里插入图片描述

4.4 思考:为什么数组传参 本质要传首元素的地址?

思考为什么这么设计?
● 首先 前面提到 数组是连续存放在内存中的 所以只要知道起始地址 就可以顺藤摸瓜找到整个数组 只传一个首元素地址 可以"代表/找到整个数组"
● 其次 假设数组传参传的不是传址 而是一份数组的临时拷贝 那么假如数组100000个元素? 浪费空间和时间!!!

4.5 二维数组传参

  • 请回看3.6
    在这里插入图片描述

五、其他

怎么看数组的类型是什么

int num = 10; 去掉变量名 剩下的就是类型
那么 int num[10] = {1,2,3}; 去掉num 剩下的int [10] 就是类型
即数组num的类型就是int [10] 而且这个10不能被省略
在这里插入图片描述

算大小也不会这么算 因为[ ]写几 还得自己去数一下
只能说 就借此理解一下 数组也是有类型的 就跟int a = 10的a一样
在这里插入图片描述

用整数初始化字符数组会怎么样

在这里插入图片描述

&arr和&(arr+1)

首先必须要明确:&取地址符是把某个变量的地址取出来了

之前我们说过 arr就是一个指针 不是指针变量 arr的本质是常量(十六进制形式)
所以 &arr就是一个特例(按道理&arr应该报错的 因为arr根本不是变量)
但是 &arr直接把整个数组的地址取出来了
在这里插入图片描述

而除了&单独的数组名arr这个特殊情况
arr+1 得到的其实是第二个元素的地址 arr+1是个指针 不是指针变量 本质是个常量(和arr一样)
&(arr+1)就不存在特殊情况咯 按照&的要求 直接就报错了
在这里插入图片描述

其实前面也已经说了
&arr[j]本质上是arr+j 表示下标为j的元素的地址
&(arr+1) 相当于 &(&arr[1]) 介四嘛呀
在这里插入图片描述

六、模拟三子棋小游戏

6.1 效果演示

在这里插入图片描述

6.2 代码模块划分

  • 符合之前在函数章节所介绍的
  • test.c是逻辑测试
  • game.h和game.c是游戏的实现 其中game.h主要负责函数声明
    在这里插入图片描述

6.3 分析

  1. 棋盘的效果是printf打印出来的 其中空白部分才是真正落子的地方 用一个3*3的数组来维护
  2. 行数和列数在game.h用#define定义比较好
  3. game()玩游戏的逻辑:
  • 定义棋盘
  • 然后初始化棋盘InitBoard()
  • 必要的时候 打印一下棋盘的当前落子情况DisplayBoard
  • 然后在循环中开始电脑VS玩家的下棋
  • 开始下棋就想到 必须要引入一个函数来判断输赢IsWin()
  • 而且每次玩家下完棋或者电脑下完棋 都需要调用一次IsWin() 如果分数胜负就break 否则就继续下棋

6.4 先给出完整参考代码

test.c

  • 先给出完整代码 有一个大局
  • 后面会详细介绍一些核心的功能的实现
#define _CRT_SECURE_NO_WARNINGS
#include"game.h"
void menu()
{
printf("********************************************\n");
printf("******************  1. play   **************\n");
printf("******************  0. exit   **************\n");
printf("*******************************************\n");
}

void game()//游戏过程
{
char board[ROW][COL];//棋盘数组
InitBoard(board, ROW, COL);//初始化棋盘,都放入空格
DisplayBoard(board, ROW, COL);//打印棋盘

//下棋
char ret = 0;
while (1)
{
PlayerMove(board, ROW, COL);//让玩家选择落子
DisplayBoard(board, ROW, COL);//下完之后打印一下棋盘
ret = IsWin(board, ROW, COL);//每次下完就判断一次
if (ret != 'C')
{
//!=c  可能是平局 输了 赢了 那都不要继续下棋了
break;
}  

ComputerMove(board, ROW, COL);//让电脑随机落子
DisplayBoard(board, ROW, COL);//下完之后打印一下棋盘
ret = IsWin(board, ROW, COL);//每次下完就判断一次
if (ret != 'C')
{
break;
}
}

//由于ret的值是C 即游戏-不继续-了 break跳到这里
//所以在这看看游戏不继续是因为#*Q里的哪一种
if (ret == '*')
{
printf("玩家赢\n");
}
else if (ret == '#')
{
printf("电脑赢\n");
}
else
printf("平局\n");
}

int main()
{
srand((unsigned int)time(NULL));//注意只需设置一次随机数种子
int input = 0;

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

return 0;
}

game.h

#define ROW 3
#define COL 3
//由于行和列的3,用到的频率很高,假设我需要改的时候,只需要把头文件的数改掉即可


#include<stdio.h>
//由于发现两个.c文件中都引用了这个头文件
//所以把他放在了game.h中,这样只需要引用一次game.h即可
#include<stdlib.h>
#include<time.h>



//初始化棋盘的函数声明
void InitBoard(char board[ROW][COL], int row, int col);

//打印棋盘的函数声明
void DisplayBoard(char board[ROW][COL], int row, int col);

//玩家下棋的函数声明
void PlayerMove(char board[ROW][COL], int row, int col);

//电脑下棋的函数声明
void ComputerMove(char board[ROW][COL], int row, int col);

//判断游戏输赢的函数声明
//他应该返回         玩家赢  电脑赢  平    继续
//四种状态分别返回      *      #    'Q'    'C'
char IsWin(char board[ROW][COL], int row, int col);

game.c

#define _CRT_SECURE_NO_WARNINGS
#include"game.h"

void InitBoard(char board[ROW][COL], int row, int col)//初始化棋盘
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
board[i][j] = ' ';
}
}
}

void DisplayBoard(char board[ROW][COL], int row, int col)//打印棋盘
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf(" %c ", board[i][j]);
if (j < col - 1)
printf("|");
}
printf("\n");


if (i < row - 1)
{
for (j = 0; j < col; j++)
{
printf("---");
if (j < col - 1)
printf("|");
}
}
printf("\n");
}
}

void PlayerMove(char board[ROW][COL], int row, int col)//玩家下棋
{
int  x = 0;
int y = 0;
while (1)
{
printf("玩家走,请输入坐标>\n");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (board[x - 1][y - 1] == ' ')//表示该位置没落子,可以输入
{
//玩家下(1,2) 对于数组来说是(0,1)
board[x - 1][y - 1] = '*';
//玩家下完,出循环
break;
}

else
{
printf("该位置已经被落子,请重新输入\n");
}
}

else
{
printf("输入值超出范围\n");
}
}
}

void ComputerMove(char board[ROW][COL], int row, int col)//电脑随机下棋 程序员有多聪明 电脑就有多聪明
{
int x = 0;
int y = 0;
printf("电脑走\n");

while (1)
{
//对于电脑来说,可以让他直接落到数组里,范围也就是0-2
//由于row和col都是3,所以他们的余数肯定是0-2
x = rand() % row;
y = rand() % col;
if (board[x][y] == ' ')
{
//这里就不要减一了,电脑直接下到可以下的数组坐标上
board[x][y] = '#';
break;
}
}
//只要是被break跳出来的就说明电脑成功落子,否则就说明不能落子,直接回到while重新生成随机数
//棋盘满了 或者分出胜负了 
//就要结束游戏 要不然如果棋盘满了还让电脑下棋 就死循环了
}

//IsFull是不需要声明在.h中的 因为我这个IsFull是专门给本文件的IsWin用的
//所以IsFull不需要外部链接属性
static int IsFull(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
if (board[i][j] == ' ')
//没有满就返回0
return 0;
}
}
//当一直能走到这,说明没有空格了,也就是棋盘满了,返回1
return 1;
}

char IsWin(char board[ROW][COL], int row, int col)//判断输赢
{
//判断输赢(三行三列两个对角线)然后判断是否平局(是不是棋盘满了还没结束)都不是的话就游戏继续
int i = 0;
int j = 0;

//三行
for (i = 0; i < row; i++)
{
if ((board[i][0] == board[i][1] && board[i][1] == board[i][2]) && board[i][0] != ' ')
{
return board[i][0];
//三个谁相等就返回谁,跟自己设想的四种状态自洽
//所以刚开始想把四种状态一次给1234会变得复杂,三个相等还等判断是*还是#
}
}

//三列
for (i = 0; i < col; i++)
{
if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[0][i] != ' ')
return board[0][i];
}

//两个对角线
if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[0][0] != ' ')
return board[0][0];
if (board[0][2] == board[1][1] && board[1][1] == board[2][0] && board[1][1] != ' ')
return board[1][1];

//但凡前面发生了一次return 就说明分出胜负了
//如果能一直走到这 就说明还没分胜负 这里就看看是不是平局了?
//既然没有分出胜负 棋盘也满了 说明平局
//所以只要判断一下是不是棋盘满了 满了就是平局了
if (IsFull(board, ROW, COL))
{
return 'Q';
}

//如果还能走到这里 
//说明:既没有分出胜负 棋盘也没满也没打成平局  那就返回C 表示游戏继续
return 'C';
}

6.5 打印棋盘void DisplayBoard

首先提供一种写法
但是这种写法能针对的只有三子棋了
10*10咋办呢? 每次都手改吗?
那根本锻炼不到什么嘛!
下面介绍更好的写法
在这里插入图片描述

  • 这个函数的实现 主要还是要自己画图
  • 要学会在for循环里面 灵活使用if
  • 首先明确 i控制打印几部分 j控制每一部分打印的内容
  • 注意分隔符用的是’-’ 不是’_’ -是对着|中间的 这样美观一些
    在这里插入图片描述
  • 圈1这样的图形 其实有3行 也就是i<3的范围
  • 圈2这样的图像 只有2行 也就是i<2的范围
  • 单独来看每一行 字符’|‘也需要用if控制一下 只需要打印两次’|’ 也就是j<2的范围
  • 圈1和圈2整体看做一部分 用i控制
    在这里插入图片描述
    在这里插入图片描述

然后写代码 就很容易了:
最好就把3和3换成前面的COL和ROW
在这里插入图片描述

6.6 判断棋盘已满static int IsFull

  • IsFull是不需要声明在game.h中的 因为我这个IsFull是专门给本文件的IsWin用的
  • 所以 IsFull不需要外部链接属性 就使用static修饰

6.7 判断输赢char IsWin

首先要想好 什么字符代表赢 不能简单的就想用A B C来代表:

  • 因为 假设A算电脑赢 B算玩家赢的话 当我判断某一列相等了 我还要再看看那一列是全是*还是全是# 才知道是谁赢了
  • 如果直接判断到某一列相等 把那一列的#返回 直接算#电脑赢 更简单一些

其实返回什么就谁赢是最好的:

  • 电脑的符号是# 那么返回#就电脑赢了
  • 玩家的符号是* 那么返回*就玩家赢
  • 但是还是需要特别定义一下 返回Q就是平局 返回C就是没分出胜负 继续下棋

七、模拟扫雷小游戏

7.1 效果展示

在这里插入图片描述


原文地址:https://blog.csdn.net/qq_57030936/article/details/143058862

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!