C语言数组(二)

前言

之前我们把一维数组的创建和简单的运用给学习了一遍,数组(一)而今天我们将要学习一个比一维数组稍微复杂一点的二维数组

一.二维数组的定义

和一维数组类似就是从一个方括号变成了俩方括号

int arr[3][4];
char arr[3][4];
double arr[3][4];

看,这就算创建好了但是这两个方框里的数字代表啥呢?我们知道一维数组int arr[3]其实是在内存中开辟了一个具有三个整型大小的空间,你也可以理解为一行有三个元素。而像刚才定义的那个二维数组你就可以理解为有一个空间上面有三行四列也就是12个元素。所以二维数组的两个[],前面的代表行,后面的代表列

二.二维数组的初始化

int arr[3][4] = {1,2,3,4};/*这是不完全初始化,也是连续赋值,这代表前四个元素1,2,
                          3,4其余的为0,赋值顺序是按照下面这个图片的来走的*/
int arr[3][4] = {{1,2},{4,5}};/*这里面每一个{}里都代表对一行的初始化,{1,2}
                              说明是对第一行的前两个元素初始化成1,2.同理
                              第二行初始化为4,5,这是分段赋值*/
int arr[][4] = {{2,3},{4,5}};/*二维数组如果有初始化,行可以省略,列不能省略
                             具体原因我在后面关于二维数组在内存中的存储中会
                             讲到*/

在这里插入图片描述

如果类似下面这些初始化:

	int arr[3][4] = { {1,2},3,4,5 };//第一行初始化为1,2
									//3,4,5放在第二行,多的就为第三行
	int arr[3][4] = { {1,2},{3,4},5 };//第一行初始化1,2
									//第二行初始化为3,4
									//最后一行初始化为5

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

我们之前接触过一维数组的存储,一维数组就是连续的,从低到高存放的。那二维数组会不会在内存中是以x行y列这样的方式存储的呢?答案当然不是,我们先看代码。

int main()
{
	int arr[2][2] = { 1 , 2 , 3 , 4 };//二维数组初始化
	int i = 0, j = 0;
	
	//打印二维数组的值
	for (i = 0; i < 2; i++)
	{
		for (j = 0; j < 2; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
	printf("\n");
	
	//打印二维数组的地址
	for (i = 0; i < 2; i++)
	{
		for (j = 0; j < 2; j++)
		{
			printf("%p ", &arr[i][j]);
		}
		printf("\n");
	}
	return 0;
}

我们再来看结果:
在这里插入图片描述
我们在这里可以看到1,2,3,4分别对应的地址末尾两位的变化是78 7C 80 84.你们可以看到虽然是二维数组,但他在内存中和一维数组一样地址由低到高连续存放的
像这样:
在这里插入图片描述

在我们看题目或者别人的代码的时候,经常会看到这样的代码:

int arr[] = { 1, 2, 3, 4, 5, 6 };
int arr[6] = { 1, 2, 3, 4, 5, 6 };

int arr[][2] = {{1, 2}, {3, 4}, {5, 6}};
int arr[3][2] = {{1, 2}, {3, 4}, {5, 6}};

为什么一维数组的大小可以不写,而二维数组只有行可以不写
我们先来看一维数组int arr[] = { 1, 2, 3, 4, 5, 6 };:
在这里插入图片描述
在这里我们看到我们虽然没有给数组大小,但是通过初始化,已经把这个数组划好了6个区域。所以一维数组你只要给它初始化好值,你就可以不用确定他的大小,因为它不可能是5个元素,7个元素之类的。如果他是5个元素,那6这个值应该放哪呢?难道编译器以下犯上,把你想要的元素自己清除吗?或这如果是7个元素或者更多,那多出来的位置放什么值?这就多此一举了。所以在定义一维数组时,只要给他初始化值就可以不用给大小
二维数组:int arr[][2] = {{1, 2}, {3, 4}, {5, 6}};
在这里插入图片描述

此时如果你没有给行数,但是给了列数的时候。编译器就会明白,每隔两个元素就是一行,你在后面初始化的时候,他就会根据初始化内容来判断行数,编译器不会自动给你加一行或者少一行。
但是你给了行数,却没有给列数会怎么样呢?这样编译器就会出错,因为此时编译器不知道什么时候到下一行,虽然你给了列数,它也不好判断,是5个元素之后是下一行?还是6个元素是下一行?
总结

一维数组在初始化给定元素个数时,可以不用写数组大小
二维数组在给定元素个数时,列数一定要写,行数可写可不写

四.二维数组的使用

当我们了解完二维数组在内存中的存储时,有没有发现二维数组和一维数组有一些微妙的关系?
一维数组可以这样写:

int arr[] = { 1, 2, 3, 4, 5, 6 };

二维数组可以这样写:

int arr[][2] = {{1, 2}, {3, 4}, {5, 6}};

都是第一个括号里的大小可以省略,我们可不可以大胆的猜想一下。二维数组可以是由几个一维数组组成的数组?对于二维数组的行数能不写的这种情况,我们也可以这样理解。

你必须要知道包含一维数组的个数也就是有几列,但可以不知道,你一个一维数组含有多少元素,一维数组在初始化时确实可以不知道它的大小

这样子讲是不是就可以理解清楚了?

我们在看看在表示二维数组的一个元素时,是怎么表示的?假如要表示第二行的第三个元素,可以这样写:

a[1][2]//和一维数组一样,下标都是从0开始

那a [ 1 ] [ 2 ] 可以说它是二维数组中,第二个一维数组里的第三个元素。这样我们也可以认为a[ 1 ]其实是一个一维数组的数组名。数组名+下标可以找到一个元素,这是在一维数组中讲到的,在这里a[ 1 ] [ 2 ]就相当于a[ 1 ] + 第2个元素.
如果能理解上面这些,在使用起来也不会特别麻烦了。
现在我们在来看如何打印一个二维数组:

int main()
{
	int arr[3][3] = { 0 };
	//这里将二维数组全部初始化为0

	int i = 0;
	for (i = 0; i < 3; i++)
	//i=0时打印第一行,i=1时打印第二行...
	{
		int j = 0;
		//当i=0时我们可以认为此时我们在打印第一个数组里的元素
		for (j = 0; j < 3; j++)
		{
			printf("%d ", arr[i][j]);
		}
		//打印完一行之后我们换行
		printf("\n");
	}
	return 0;
}

和一维数组一样,我们在打印二维数组的时候用两个循环嵌套就可以表示出来了。我们看看结果:
在这里插入图片描述

五.二维数组的数组名

我们已经知道了一维数组的数组名是数组首元素的地址,但是二维数组呢?二维数组的数组名也是首元素地址吗?是的话,首元素是什么呢?是第一行的第一个元素?还是其他的?
我们通过代码来理解一下:

int main()
{
	int arr[3][2] = { {1,2},{3,4},{5,6 } };
	printf("%p\n", arr);
	printf("%p", arr + 1);
	return 0;
}

我们来看看二维数组+1的地址增加了多少
在这里插入图片描述

因为地址是按16进制计算的,所以B8-C0共增加了8个字节(逢十六进1).这里发现首元素+1跳过了两个整型的空间,也就是跳过了一整行。既然是跳过一整行,就是说明二维数组的首元素其实是第一行的地址
总结

一维数组的名字是首元素的地址
二维数组的名字是整个第一行元素的地址

六.数组作为函数参数

我们常常会在写一个函数时要用到和数组有关的内容,但是数组传过去后,形参用什么来接收呢?通常我们有两种方法

第一种:用数组来接收

void print1(int arr[5])//这里5也可以不写
{
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		printf("%d ", arr[i]);
	}
}

void print2(int arr[3][2])//3也可以不写
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 2; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}

int main()
{
	int arr1[5] = { 1, 2, 3, 4, 5 };
	int arr2[3][2] = {1, 2, 3, 4, 5, 6};

	print1(arr1);
	printf("\n");
	print2(arr2);
	return 0;
}

这里就是将arr1,arr2整个传过去,函数在用数组接收。到这里可能就有人问了,欸?数组名不应该是数组首元素的地址?你这传过去一个地址,你形参在用数组接收,这不乱套了吗?这其实不要紧的,因为这样写也是支持的。上面函数形参虽然人模狗样的写个数组,但其实本质上还是个指针,加个[]是为了说明它接收的是一个数组。

因为本质上是个指针,函数参数是一维数组的话,int arr[5]和int arr[]是没啥区别的,所以5写不写都是没问题的。而二维数组的话还是要把列数写上去。

这里传的arr是一个数组,但是实际传过去的是一个指针。这种情况有一个名词叫做:数组降级

第二种:用指针来接收

void print1(int *arr)
{
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		printf("%d ", arr[i]);
	}
}

int main()
{
	int arr1[5] = { 1, 2, 3, 4, 5 };
	print1(arr1);
	return 0;
}

好,看到这里,问题来了。二维数组呢?二维数组传数组名过去也是拿一个int* 的指针接收吗?上面我们了解过,二维数组名其实是第一行元素也就是一个一维数组的地址,那一个数组的地址你拿一个整型的指针接收肯定说不过去。既然传过去的是一个数组,肯定要用一个指向数组的指针,这个我在后面会讲,叫数组指针

这里看不懂先不要急,讲数组指针的时候我会再讲一遍

我们来看代码:

void print(int(*arr)[2])
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 2; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}

int main()
{
	int arr[3][2] = { {1, 2},{3, 4},{5, 6} };
	print(arr);
	return 0;
}

再来看输出:
在这里插入图片描述
我们既然知道数组在传参的时候,既可以用数组接收,又可以用指针接收。其实这两种本质上都是接收一个地址。但是看到这里我又想既然二维数组使用数组指针来接收可以,还挺好的,我一维数组也想用数组指针来接收,但这样要怎么做呢?其实很简单,在传参的时候直接传整个数组的地址即可

void print(int(*arr)[3], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		//printf("%d ", *(arr + i));
		//这样写肯定是不行的,arr是指向整个数组的地址
		//arr+1就相当于跳过一个数组,肯定是打印不出来的

		printf("%d ", (*arr)[i]);
		//因为arr指向整个数组,解引用arr就相当于拿到这个数组

		//printf("%d ", *(*arr + i));
		//这样写也可以
	}
}

int main()
{
	int arr[] = { 1, 2, 3 };
	print(&arr, 3);
	return 0;
}

一维数组还是不要这样写了,虽然这符合规定,但是太麻烦。

七.指针数组和数组指针

7.1指针数组

指针数组其实是数组,是一个里面元素是指针的数组

我们在学数组的时候,当数组里元素是int型的就叫整型数组,同理还有字符型数组,浮点型数组。如果一个数组里的元素是一种类型的指针,这个数组就叫指针数组。

指针数组的表达

在此之前,我们要先熟悉几个操作符的优先级
[] > () > *

int* arr[3];
//因为[]的优先级要大于*,所以arr先于[]结合变成了数组
//在我们把arr[3]拿走之后发现剩下的是int*
//这就说明这个数组里的元素是int*类型的指针

这里我们通过拆解这个代码,来更清楚的认识什么是指针数组。

int* arr1[3];//一个含有三个int*类型指针元素的数组
char* arr2[3] = {地址1,地址2,地址3};
//...

指针数组的应用

int main()
{
	int a = 0;
	int b = 0;
	int c = 0;
	int d = 0;

	int* p1 = &a;
	int* p2 = &b;
	int* p3 = &c;
	int* p4 = &d;

	int* arr[] = { p1, p2, p3, p4 };

	*arr[0] = 10;
	*arr[1] = 20;
	*arr[2] = 30;
	*arr[3] = 40;
	//将数组里的元素也就是指针解引用之后在操作

	printf("%d\n", a);
	printf("%d\n", b);
	printf("%d\n", c);
	printf("%d\n", d);

	return 0;
}

7.2数组指针

数组指针其实是一个指针,指向的是一个数组的地址

可能在刚接触数组指针和指针数组时可能会分不清,其实很简单,看名字后两个就行,是数组还是指针,一下子就能知道了。

数组指针的定义
我们回想一下指针数组的表达:

int* arr[3];

在这里我们发现,因为优先级的影响,arr总是先和[]相结合变成数组,但是数组指针他是一个指针呀,我们希望arr它可以先和*结合,所以我们用括号把 * 和arr括起来。像这样

int (*arr)[2];

在这里就可以认为arr先和*结合变成指针,我们把 * arr拿掉,剩下的是int [2],这就说明指针指向的是一个数组有两个元素,而这个数组里的元素是int类型的元素。

int (*)[];
//这就是一个数组指针类型

应用

int main()
{
	char str[] = "hello word";
	//先定义一个数组

	char(*strr)[11] = &str;
	//把数组str的地址取出来,放到数组指针变量strr里

	printf("%s\n", strr);
	return 0;
}

在这里插入图片描述

看到这里,我们基本了解了什么是数组指针,现在我们在回头看一下之前讲的二维数组传参,加深一下印象:

void print(int(*arr)[2])
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 2; j++)
		{
			//printf("%d ", arr[i][j]);
			//我们知道arr其实是二维数组的首元素(第一行数组)的地址
			//所以arr+i跳过的是一行
			//*(arr+i)拿到的就是这一行
			//*(arr+i) + j就是这一行的第j个元素
			//*(*(arr+i) + j)拿到的就是第i行第j列的元素
			printf("%d ", *(*(arr + i) + j));
		}
		printf("\n");
	}
}

int main()
{
	int arr[3][2] = { {1, 2},{3, 4},{5, 6} };
	print(arr);
	return 0;
}

但是*(*(arr + i) + j))为什么等价于arr[i][j]呢?

我们知道数组名就相当于地址,我们假设一个一维数组名是arr。
arr+i就是这个数组第i个元素的地址,然后解引用就拿到了这个元素。但是我们发现arr[i]也是拿到的这个数组第i个元素
这是不是可以说*(arr + i) 等价于 arr[i]
既然这样*(*(arr + i) + j)) --> *(arr[i] + j) --> arr[i][j]

7.3指针数组和数组指针套娃

int (*parr[10])[5];

在我们学完数组指针和指针数组后,能不能说一下这个代码它是个啥?

首先,parr先和[]结合成为数组,所以这个代码肯定表示的是一个数组,但是数组里的元素是什么呢?
我们把parr[10]取出来,剩下的就是这个数组的元素:
int (*)[5],看到这里应该明白了,这是一个数组指针的类型,这个类型是一个指针,指向了一个含有5个Int型变量的数组。

经过分析就明白了,他是一个数组,数组里的元素是个指针,这个指针是数组指针,指向的是含有5个Int型变量的数组

八.数组越界

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

九.数组名总结

9.1数组名的含义

数组名代表首元素地址
一维数组名代表第一个元素的地址。
二维数组名代表第一行元素的地址。

但是有两个例外:

  1. sizeof(arr)计算的是整个数组的大小
  2. &arr得到的是整个数组的地址

整个数组的地址和首元素地址的区别:

int main()
{
	int arr[10] = { 0 };
	printf("%p\n", arr);
	printf("%p\n", &arr);
}

发现它们的值是一样的:
在这里插入图片描述

区别就是指针个走一步的步长:

int main()
{
	int arr[10] = { 0 };
	printf("%p\n", arr);
	printf("%p\n", arr + 1);

	printf("%p\n", &arr);
	printf("%p\n", &arr + 1);

}

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

发现arr向后走一步是增加一个元素的大小也就是4个字节。而&arr向后走一步是增加一个数组的大小,这里就是40个字节。

9.2数组名与下标

找到一个数组的元素是通过操作符[]来表示:

int arr[10] = { 0 };
printf("%d", arr[1])

这样确实可以访问到数组的第二个元素,但是arr是个首元素地址,也就是1个指针,那arr+1指向的位置应该也是数组第二个元素的位置,那此时在解引用:

*(arr + 1);

这样也能得到数组的第二个元素。那是不是可以说明:

arr[i] == *(arr + i);

然后这个式子也满足加法交换律,所以:

*(arr + i) == *(i + arr)

那也可以说明:

*(i + arr) == i[arr] == arr[i] == *(arr + i)

最后一种写法i[arr]只是为了向你们证明这样可以写,但这里不推荐,因为太过于奇怪。

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值