【手把手带你入门】数组的创建与使用
一维数组的创建和初始化
数组的创建
数组是一组相同类型元素的集合。
数组的创建方式如下:
如果直接运行代码,会发现程序会报错或者警告:
将上面这段代码进行调试:
注意,这里的数组元素个数必须为常量表达式:
在 C99 之后以上代码才允许运行通过,成为变常数组。
数组的初始化
数组的初始化即指:在创建数组的同时,给数组的内容一些合理的初始值。
我们在对数组进行初始化时用 { } 。
在初始化数组时要注意一下两种写法的区别:
但是当指定了数组的大小时,就不能给出比指定的个数更多的元素了:
字符数组的初始化
在对字符数组进行初始化时,有以下三种方式:
前面提到,数组在不进行初始化时,编译器默认放入随机值,但是如果是作为全局变量的数组,在不进行初始化时,数组中的元素默认放的是0。
一维数组的使用
在了解了数组的创建和初始化之后,以及结合前面在
【手把手带你入门】初识C语言(下)中的介绍,我们对于一维数组的使用应该已经大致掌握。
对于数组的使用,我们用下标对数组进行访问
即: 数组名[ 下标 ]
例:arr[0] 访问arr数组中的第一个元素
而语法规定,数组的下标是从 0 开始的,如果我们要对数组中的元素进行访问,就要用到的是下标引用操作符:[ ],即对数组进行访问的操作符。
当我们知道数组的访问方式之后,我们可以访问其中的某个元素,也可以利用下标将数组中的元素打印出来。
但是这里的下标的数值是固定死的,如果我们对数组arr进行更改,加入了更多的元素,该代码还是只打印前十个元素。
所以这时候,我们可以用一个表示元素个数的变量来完成:
如果把数组稍作修改,该代码依然能完美打印数组中的每一个元素。
当然,我们以上的例子都是整型数组,但是实际上数组可以有很多种类型,如浮点型数组(数组元素的类型是浮点型)、结构体数组(数组元素的类型是就结构体类型)等等。
一维数组在内存中的存储
那么,一维数组在内存中到底是如何存储的呢?
接下来,我们通过一段代码来探讨:
首先猜测:数组的元素在内存中是连续存放的。
那么如果我们想知道它在内存中的布局是如何的,我们只需要把数组中的内一个元素的地址打印出来。
所以由此我们可以画出下图,并得出结论。
如果你觉得文章有用,记得点赞收藏关注一波哟!你的鼓励将是我巨大的动力!
二维数组的创建和初始化
接下来我们来看看二维数组的创建和初始化,二维数组其实是和一维数组非常相似的,但是它也有一些特别之处,下面我们一起来看看。
二维数组的创建
二维数组的创建其实就是比一维数组多了一维,这里大家可以类比一维坐标系和二维坐标系:一维坐标系的变量只有x,而二维坐标系的变量有x和y。
例如:
int arr[3][5];
char ch[5][6];
double d[4][7];
那么二维数组和一维数组到底有什么区别呢?
对比一维数组,我们可以通过画图来进行理解。
可以看到,一维数组通过画图表示出来就是一行,而二维数组在内存中则是多行的。
二维数组的初始化
同理,二维数组的初始化也是通过{ }来实现的。
第一种初始化
int arr1[3][5]={1,2,3,4,5,6,7,8,9,10,11};
画图如下:
从上图可以看出,初始化的值将依次一行一行地填入数组中,未进行初始化的部分则默认初始化为0。
第二种初始化
我们知道,一维数组就是只有一行的数组,而二维数组就是有多行的数组,因此,我们可以将二维数组看成是多个一维数组的组成,此时,我们可以这样对二维数组进行初始化:
int arr2[3][5] = { {1,2,3},{4,5},{6} };
我们可以通过调试来看看这些元素在内存中的存放。
我们依然画个图来看看~
我们知道,一维数组在初始化之后,元素个数可以省略掉,那么二维数组可省略吗?
所以对于二维数组,在初始化之后,行可以省略,而列不能省略。如果不对数组进行初始化,则行和列都不能省略。
二维字符数组的初始化
二维字符数字的初始化有一下几种:
char ch1[4][6] = { 'a','b' };
char ch2[4][6] = { {'a'},{'b'} };
char ch3[4][6] = {"abc","def","ghi"};
二维的字符数组未初始化的部分,编译器也会默认初始化为0,而0在ASCII码表中对应的字符就是‘\0’。
二维数组的使用
接下来我们看二维数组的使用,数组的访问都是通过下标来进行的,所以如果我们打印一个二维数组的值,同样要通过其下标来依次打印。
所以我们通过两层循环来依次找到二维数组中每一行每一列的下标。代码如下:
#include <stdio.h>
int main()
{
int arr[3][5] = { {1,2,3},{4,5,6},{7,8,9} };
int i = 0;
//二维数组每一行每一列的下标也是从0开始的
for (i = 0; i < 3; i++)//行 0~2
{
int j = 0;
for (j = 0; j < 5; j++)//列 0~4
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
程序运行结果如下:
二维数组在内存中的存储
二维数组在我们的想象中是一个有着几行几列的数组,那么它在内存中也是这样存储的吗?我们依然可以通过打印每个元素的地址来观察。
int main()
{
int arr[3][5] = { {1,2,3},{4,5,6},{7,8,9} };
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{ //打印每个元素的地址
printf("&arr[%d][%d] = %p\n", i,j,&arr[i][j]);
}
printf("\n");
}
return 0;
}
运行程序:
所以虽然在我们的想象中,二维数组是有行有列的,但是本质上,二维数组在内存中也是连续存放的,因为我们就可以理解为什么在初始化二维数组的时候,行可以省略而列不能省略的问题了。
因为如果列省略掉了,那在内存中就不知道一行该放几个元素,下一行的元素就不知道该放在那里了,而知道一列放几个元素,我们就可以根据给出的元素来确定一个数组可以放多少行。
这时候我们再来看看一维数组和二维数组,我们是否可以把为数组的每一行理解为一个一维数组呢?
其实是可以的。
我们可以把arr[3][5]理解为一个大的一维数组,包含3个元素,每个元素是1个一维数组。
数组越界
编译器会根据数组的下标在内存中开辟空间。如果我们访问数组前面或者后面的一块空间,就造成了数组的越界。
所以数组的下标是有范围限制的,语法规定下标从0开始,如数组中有n个元素,则最后一个元素的下标就是n-1.
而如果我们不按套路出牌,访问下标小于0或者下标大于n-1的元素,就会造成数组的越界访问,超出数组访问的合法空间。
虽然上面的代码越界访问了,但是依然可以正常打印,是因为C语言本身是没有在语法层面上做下标越界的检查的,编译器也不一定会报错,所以为了避免在写出bug,我们在写代码的时候要自己做好数组是否越界的检查。
这里我们可以看到VS2019中是给出了警告的。
同样,二维数组的行和列也可能存在越界的问题。
如果你觉得文章有用,记得点赞收藏关注一波哟!你的鼓励将是我巨大的动力!
数组作为函数参数
当我们对一个数组进行操作的时候,我们可能不一定全部的操作都在主函数中完成,有时候我们需要用函数来对数组进行操作,这时候数组就要作为函数参数被传递到某个函数中去。
那么数组在传参时时如何传的呢?
下面我们用一个冒泡排序的函数来说明问题。
冒泡排序函数
用冒泡排序的方法把一个降序排序的数组排成升序。
什么是冒泡排序?
冒泡排序的核心是:将两两相邻的元素进行比较
具体过程:
- 将第一个元素和第二个元素进行比较
- 如果满足我们想要的顺序,则比较第二个和第三个元素
- 如果不满足我们想要的顺序,则交换这两个元素,使其满足我们想要的顺序。然后再比较交换后的第二个和第三个元素。
- 依据第2条和第3条依次往下比较,直到比较完最后一个元素。
下面我们给出一个降序的数组:
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
利用冒泡排序将它排成升序的,即:
int arr[] = { 0,1,2,3,4,5,6,7,8,9 };
下面我们画图来分析一下:
我们发现最开始9依次和后面的元素进行比较,最终来到最后的位置,然后我们将8和剩下的元素依次进行比较,最后8来到9的前面,每次冒泡排序到最后,都有一个数字到它最终的位置上,我们把这样的一个过程称为一趟冒泡排序。
函数设计
接下来我们来进行冒泡排序函数的设计。
首先封装函数:
#include <stdio.h>
void Sort(int arr[])//冒泡排序函数
{
//冒泡排序的实现
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
Sort(arr);
return 0;
}
通过上面的分析,我们知道:
一趟冒泡排序可以让一个元素到它最终应该在的位置上,每一趟冒泡排序的内部是相邻两个元素之间的两两比较。
所以这里我们有10个元素,最终应该进行9趟冒泡排序。
如果我们有n个元素,则应该进行n-1趟的冒泡排序。
void Sort(int arr[])//冒泡排序函数
{
//冒泡排序的实现
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
//冒泡排序的趟数:sz-1
for (i = 0; i < sz-1; i++)
{
//实现一趟冒泡排序
}
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
Sort(arr);
return 0;
在第一趟冒泡排序中,是10个元素进行排序,则应该进行9次比较。
第二趟冒泡排序中,是剩下的9个元素进行比较,则应该比较8次。
依此类推,每一趟冒泡排序内部两两比较的次数依次递减。
void Sort(int arr[])//冒泡排序函数
{
//冒泡排序的实现
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
//冒泡排序的趟数:sz-1
for (i = 0; i < sz - 1; i++)
{
//实现一趟冒泡排序
int j = 0;
//一趟冒泡排序中两两比较的次数:sz-1-i
for (j = 0; j < sz - 1 - i; j++)
{
//两两比较
if (arr[j] > arr[j + 1])
{
//交换
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
Sort(arr);
return 0;
这样一个用冒泡排序的程序就写完了,接下来我们来测试一下这段代码。
因为没有在屏幕上对数组进行打印,所以我们在调试中用监视窗口在看arr数组的值。
函数调用完之后,我们发现数组的排序并没有改变!
这是为什么呢?
下面我们还是调试程序,用F11进入函数内部看看到底是哪里出了问题。
问题分析
当程序走到这里时,我们就发现sz的值是1而不是10,问题已经出现了!
那么为什么sz的值是1呢?
我们可以看到,在VS2019中,sizeof(arr)这一块下面划了一道波浪线,所以问题难道出现在了这儿?
没错,问题就出在这儿!
在上图中,我们可以看到arr的类型是int*,说明数组名arr的本质是一个地址,而sizeof(arr)计算的是一个地址的大小,值是4,所以最终计算出来的sz的值是1。
那么我们再来看看数组名arr,arr是一个地址,那它是谁的地址呢?是整个数组的地址吗?
从上图我们可以得出结论,数组名arr是首元素地址。
但如果数组名是首元素地址,那么我们打印出的sizeof(arr)应该是多少呢?
在这里打印的sizeof(arr)的值为什么不是4而是40呢?
40说明sizeof(arr)计算的是整个数组的大小,这里的数组名arr表示的是整个数组。
原来,在通常情况下,数组名就是数组首元素的地址。
但是有两个例外:
- sizeof(数组名),这里的数组名表示的是整个数组。
- &数组名,这里数组名也表示整个数组,取出的是数组的地址。
而在上面的代码中,我们传过去的参数arr本质上就是一个地址。
//实际参数的本质是一个地址
void Sort(int* arr);
//但是我们为了便于理解,写成了这样:
void Sort(int arr[]);
//数组传参,用数组接收
//但本质上我们传过去的是数组首元素的地址
所以虽然上面我们提到了两个例外,但是当数组名作为参数传给函数Sort时,函数内部就是用一个名为arr的指针接收的,这时候再在函数中用sizeof(arr)就只能是求一个指针的大小,因此我们不能直接在函数内用sizeof来求数组的大小。
一点改动
那么,上面那段实现冒泡排序的代码应该如何更改呢?
在弄清楚了问题的始末之后,一切就变得很简单啦!
我们只需要在函数外部把数组的个数求出来,再把它作为参数传过去就行啦!
最终代码如下:
#include <stdio.h>
void Sort(int arr[],int sz)//冒泡排序函数
{
//冒泡排序的实现
int i = 0;
//冒泡排序的趟数:sz-1
for (i = 0; i < sz - 1; i++)
{
//实现一趟冒泡排序
int j = 0;
//一趟冒泡排序中两两比较的次数:sz-1-i
for (j = 0; j < sz - 1 - i; j++)
{
//两两比较
if (arr[j] > arr[j + 1])
{
//交换
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
//在函数外部求数组个数,再把它作为参数传过去
int sz = sizeof(arr) / sizeof(arr[0]);
Sort(arr,sz);
return 0;
}
测试代码:
数组变为升序,说明冒泡排序成功!~
通过上面代码的实现,相信你对于数组的创建与使用,包括数组传参和数组名的本质一定已经有了更深的理解。正文到这里就暂时告一段落啦~
接下来就要用一些实例把数组真正应用起来~
届时将会写两个简单的小游戏,让我们的代码真正有意思起来!一起期待吧!
关注我,学习更多实用的C语言知识吧!~
本文所涉及的源代码均已整理上传至本人的gitee中,欢迎友友们按需自取~
https://gitee.com/fang-qiuhui/my-code/blob/2021.8.22/blog_2021_8_22_array/blog_2021_8_17_array.c