第五章,C语言的数组
文章目录
前言
我们前面学习了一些C语言的分支和循环语句以及函数相关的知识点。今天我们来学习一个比较适用并重要的知识点,那就是数组。它对数据处理更加方便快捷,也能让代码更加简洁。我们准备从以下几个方面去学习数组,1、一维数组的创建使用以及在内存中的存储。2、二维数组的创建和使用以及内存中的数组。3、数组的一些错误使用方式。下面我们就一步一步的去学习数组。
一、一维数组
1、一维数组的创建以及初始化
1.1 数组的创建
数组就是一组相同元素的集合,就像是把一群志同道合的同志们聚集在一起。
//数组的创建
array_type array_name[const_n];
//array_type:数组里元素的数据类型,比如:int、char、double等等,只要是C语言的数据类型都可以。
//array_name:数组名称,数组命名建议也要见名知意。
//const_n:数组的元素的个数,必须是一个常量表达式,不能是变量(在C99标准之前)
//数组创建的一些例子
//1、创建一个整型的数组
int arr_int[5] = { 1,2,3,4,5 };
//2、创建一个字符型数组
char arr_char[3] = { 'a','b','c' };
//3、创建一个浮点型数组
double arr_double[2] = { 13.14,5.20 };
特殊数组:变长数组
//变长数组
int a = 0; //定义一个变量a,并初始化为0
scanf("%d",&a); //从标准输入流输入一个整型数据,并把数据放在a变量的内存中
int arr[a]; //定义一个变长数组
//给变长数组每个元素赋值
for (int i=0;i<a;i++)
{
arr[i] = 10 + i;
}
变长数组的概念就是数组的长度可以是变化的,但也不是随时随地都可以变化,它变化是在变量(a)离他最近那个赋值的时候,这个时候数组的长度就被确定了,之后就不能被修改。
为什么要使用变长数组?
因为有时候我们写代码最开始也不确定数组的具体大小,如果假设的数组太大就浪费内存了,太小又装不下我们想要存放的数据,为了解决这种左右为难的情况,就创造了一个变长数组的概念。
变长数组为什么在创建的时候不能初始化内容?
因为变长数组的长度是在运行的时候才知道,如果我们在创建的时候就初始化,那么编译器在编译的时候,就不知道数组的大小,所以不能初始化。
下面关于变长数组的错误使用:
//变长数组的错误使用
int a = 20; //定义一个变量a,并初始化为20
int arr[a]; //定义一个变长数组,数组的长度为20
//int arr[a] = {1,2,3,4,5,6}; //错误的写法,定义变长数组的时候不能初始化
//这里我想把数组的长度改成30。
//这种做法是错误的,因为变长数组的长度是在离变长数组定义之前离得最近的那次变量赋值。
//所以编译器会把a = 20这次的值给变长数组里的变量,这时候就确定下来数组的长度为20
a = 30;
PS:变长数组仅在C99标准和以后的标准上支持,C99之前的不支持。
1.2 数组的初始化
什么叫初始化?
就是在数组定义的同时就给数组一些合理确定的初始值(这些值后期可以根据需要更改)。
//一维数组的初始化
//1、完全初始化
//根据我们指定的数组长度去对应的赋初始值,
//比如:数组有5个元素,那我们就把5个元素全部都给赋初始值
int arr1[5] = { 1,2,3,4,5 };
//2、未完全初始化
//给数组的元素赋一部分初始值
//比如:数组有3个元素,我们就只给前两个元素赋初始值
char arr2[3] = { 'a','b' };
//3、不指定数组长度的初始化
//我们不指定数组的长度,让编译器编译的时候,根据初始值的个数去决定数组的长度。
//比如:我们给一个数组赋4个初始值,那么编译器在编译的时候,就确定数组的长度为4
double arr3[] = { 1.2, 2.2, 3.5, 5.2 };
基本上数组的初始化都是上面的三种方式。其中第一种使用的最多,对别人阅读代码很友好。
2、一维数组的使用
我们访问数组通过“[]”下标符号。下标的起始位置从0开始,结束位置到字符数组长度-1的位置。
//一维数组的使用
//1、创建一个字符数组,并不完全初始化
char arr[10] = { 'a','b','c','d','e' };
//2、计算数组的长度
//sizeof(arr):求整个数组的字节大小
//sizeof(arr[0]):求数组的第一个元素的字节大小
//两个相除得到整个数组的长度
int len = sizeof(arr) / sizeof(arr[0]);
//3、给数组的赋值
for (int i=0;i<len;i++)
{
arr[i] = 'a' + i; //在字符‘a’的ASCII基础上+1
}
//4、打印数组元素
for (int i = 0; i < len; i++)
{
printf("%c ",arr[i]);
}
3、一维数组在内存中的存储
我们要更好的使用数组,那我们就要先理解数组在内存中是如何存储的。
//数组在内存中的存储
int arr[5] = { 1,2,3,4,5 };
int len = sizeof(arr) / sizeof(arr[0]);
for (int i=0;i<len;i++)
{
//这里打印每个元素在内存中的地址
printf("&arr[%d] = %p\n",i,arr+i);
}
看上面的结果,为什么每个元素之间的间隔是4个字节呢?因为定义了一个整形数组,数组里面的元素是整形,每个整形的数据大小为4个字节,所以每个元素依次增加4个字节。
访问数组的几种方式:
//访问数组的方式
//1、通过[]符号访问元素
int arr[5] = { 1,2,3,4,5 };
int len = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < len; i++)
{
printf("%d\n",arr[i]);
}
printf("\n");
//2、通过地址解引用访问
for (int i = 0; i < len; i++)
{
printf("%d\n", *(arr+i));
}
二、二维数组
1、二维数组的创建以及初始化
1.1 数组的创建
上面我们学习了一维数组,形式如下:
那像下面的二维数组我们应该怎样去创建和使用呢?
那我们下面就来学习一下。
二维数组中重要的是二维,我们初中就是学过二维的概念。我们初中学习的就是二维坐标,每个坐标都是有两个点组成(X,Y)。那我们上面的二维数组就是行和列组成,也可以把行看成X,列看成Y。
//二维数组的创建
//创建一个整形的二维数组
int arr[3][4]; //3行4列
//创建一个字符的二维数组
char arr2[2][2]; //2行2列
1.2 数组的初始化
//二维数组的初始化
//1、完全初始化
int arr[2][2] = { {1,2},{3,4} };
//2、未完全初始化
int arr2[3][4] = { {1,2},{3,4} };
//3、编译器根据列的个数去确定二维数组的行数
int arr3[][3] = { 1,2,3,4,5,6 };//这里编译器会去识别数组元素的个数,通过列的个数去分配元素
//这种写法是错误的。如果只写行的个数的话,编译器就不知道给列分配多少个元素。
int arr4[2][] = { 1,2,3,4,5 };
2、二维数组的使用
//二维数组的使用
//1、定义一个整形的二维数组
int arr[3][4] = { 1,2,3,4,5 }; //不完全初始化
//打印数组的元素
//因为二维数组是二维的,所以我们需要使用两层循环去遍历数组
for (int i =0;i<3;i++) //这里打印二维数组中的行
{
for (int j=0;j<4;j++) //这里打印二维数组中的列
{
printf("%d ",arr[i][j]);
}
printf("\n");
}
3、二维数组在内存中的存储
//二维数组在内存中的存储
//定义一个3*4的整形的二维数组
int arr[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 }; //不完全初始化
//打印数组的元素
for (int i =0;i<3;i++)
{
for (int j=0;j<4;j++)
{
printf("&arr[%d][%d] = %p\n",i,j,&arr[i][j]); //打印每个元素的地址
}
}
其实二维数组的存储方式也是跟一维数组存储的方式一样的,只不过二维数组的元素更多而已。
下面用画图的方式剖析一下数组在内存的存储。
三、数组作为函数的参数
我们上面只学习了如何定义和使用数组,没有学习当参数使用,数组作为参数传参也是一个很常见的写法。因为数组是一个相同类型元素的集合,我们要同时操作很多数据,那么就要使用数组。
下面看看数组传参的案例(冒泡排序):
冒泡排序的思想:一组数据,从前向后(数组的下标从小到大)两个相邻的数据进行比较,如果发现逆序,则交换比较两个数据的位置,交换后继续向后比较,直到最大的数据到了数组的最后一个元素位置就停止比较。
下面我们就通过代码实现一下:
void bubbleSort(int arr[10], int len)
{
int i = 0;
int j = 0;
for (i=0;i<len-1;i++)
{
for (j=0;j<len-1-i;i++)
{
if (arr[j] > arr[j+1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
//数组作为函数的参数
//我们实现一个冒泡排序函数,将一组数据传进函数进行排序
int arr[10] = { 2,1,4,3,5,6,8,7,9,10 };
int len = sizeof(arr) / sizeof(arr[0]);
bubbleSort(arr, len);
for (int i=0;i<len;i++)
{
printf("%d ",arr[i]);
}
如果我们把上面的代码改成下面的形式能实现吗?
void bubbleSort(int arr[10])
{
int len = sizeof(arr) / sizeof(arr[0]);
int i = 0;
int j = 0;
for (i=0;i<len-1;i++)
{
for (j=0;j<len-1-i;i++)
{
if (arr[j] > arr[j+1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
int main()
{
int arr[10] = { 2,1,4,3,5,6,8,7,9,10 };
bubbleSort(arr);
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
}
但是结果并不如意,看结果就2 1交换了位置,就像只比较了一次。为什么呢?下面我们看下bubbleSort()函数中这条代码。
这条代码不就是我们想求数组长度的方法吗。NO,这条代码只针对sizeof操作符里面是数组名的时候才是求数组的大小。大家这里一定不要被形参给迷惑了,不要看见int arr[10]这个参数就觉得它是一个数组,然后直接用那个公式计算数组大小。其实函数形参为数组的时候,int arr[10] == int *arr,所以这里函数里面只把这个形参当成一个指针变量arr仅此而已。
之前我们不是学过sizeof()计算各种类型的指针变量大小,所以的指针变量的大小都是8字节。
代码中的sizeof(arr) / sizeof(arr[0]) ===》8/4 = 2,最终len的值为2。
所以最后知道为什么只交换了两个数据了吧。
PS:大家一定要牢记,数组作为参数传给函数,不管形参写成什么形式,函数内部都会当成一个指针变量。
四、数组的错误使用
上面,我们一直在讲正确的使用,但是也不可避免一些错误的使用方式。使用数组最大的错误使用就是数组越界。
首先,我们要明白数组的下标是有范围限制的。 数组的下标规定是从0开始的,如果数组有n个元素,最后一个元素的下标就是n-1。 所以数组的下标如果小于0,或者大于n-1,就是数组越界访问了,超出了数组合法空间的访问。
C语言本身是不做数组下标的越界检查,编译器也不一定报错,但是编译器不报错,并不意味着程序就是正确的。
下面举两个列子:
这里本来数组只有5个元素,但是偏偏要去访问6个元素,但是编译器不会给你报错,因为C语言不做数组下标的越界检查。你强行要访问数组最后一个元素下一个地址的值,编译器只能让你访问了,谁叫你是我的铁子,那编译器就把下个地址的值拿给你,但是这个值是多少谁都不确定的。
所以身为程序员的铁子们,编译器觉得数组下标对不对无所谓,但是我们需要重视起来,因为你处理数据,突然多了一个很奇葩的数据,这会导致你的结果也很奇葩。为了防止这种情况的发生,那我们就需要自己去做越界的检查。
最后给铁子们再讲一下数组名这个概念:
首先我们不知道数组名arr是什么意思,那我们看&arr[0],这个知道是数组中第一个元素的地址,吧,那我们上面打印arr和&arr[0]的地址都是同一个地址,从这里就可以看出来,其实数组名就是数组首元素地址。但是有几个特殊情况,请看下面的代码。
上面图片中,如果arr是数组首元素地址,那它就是一个地址,我们上面提过sizeof()求指针变量的大小不是8吗,为什么这里打印的大小是12。所以这就是数组的特殊情况。
下面还有一个种特殊情况:
图片中,arr没放在sizeof()操作符中吧,所以它是首元素地址吧,但是,,,,铁子们请看,如果它是首元素地址,那么arr+1应该是第二元素的地址,那&arr和&arr+1也是不是相差一个元素的地址。那上面图片中,为什么arr+1与arr相差4,而&arr与&arr+1却相差12字节,这12是不是刚好就是一个数组的大小呢,所以这也是数组名的特殊的一种存在。
两种特殊存在的结论:
1、如果数组名放在sizeof操作符里,计算的就是数组的整个字节大小。
2、如果&数组名,这样就是取得整个数组得地址,并不是单纯得首元素地址。
3、除了以上两种情况,其他都表示首元素地址。
总结
又是一篇学习日记,和大家一起学习,一起成长,是一件很开心得事情。所以大家不要落下学习哦~,这章主要讲了一维数组和二维数组以及一些注意点。大家没事多去敲敲,敲敲有益于身体健康,工资加倍,哈哈哈哈。
在此谢谢铁子们,你们的建议就是我们成长路上的动力。