C语言的底层逻辑剖析数组(其一),保姆式解析,一维数组和二维数组,数组在内存中的存储,数组的创建和使用
开篇语
数组这章可以说相对来说比较简单,当然仅限于理解使用简单,对于数组后期的一些经典案例还是比较吃力的,比如后面我们数组最后会去实现三子棋,还有扫雷(这个一般是最最痛苦的)。所以即使这期内容比较简单,还是希望大家能够静下心来,即使是基础我们就打扎实了了。
一维数组的创建和初始化
#include<stdio.h>
//数组的几种创建方式
int main()
{
int arr1[3] = {1,2,3};//第一种,完全初始化;
int arr2[] = { 1,2,3 };//第二种,与第一种等价;
int arr3[3] = {1,};//不完全初始化;
return 0;
}
当我们这样去定义和初始化我们的几个数组时,我们可以调出监视窗口来看一下我们的数组里面到底都有些什么。
可以看到当我们即使不给数组赋值的时候,编译器会自动根据初始化的内容去为数组设置长度。当我们对数组不完全初始化的时候,编译器默认向数组中放的就是0。
注:数组创建,在C99标准之前, [] 中要给一个常量才可以,不能使用变量。在C99标准支持了变长数组的概念,数组的大小可以使用变量指定,但是数组不能初始化。(感兴趣的可以在Linux等环境下尝试一下变长数组,就是可以用变量来创建数组大小,但是这样就不支持初始化了,一定不要认为变长数组就是可以变长的数组,不要被名字误导。)
理解了上面,我们接下来重点来看一下字符类型的数组,
int main()
{
char arr1[] = {'a','b','c'};
char arr2[] = "abc";
return 0;
}
可以看到,当我们以字符串的形式在末尾会自动补上一个‘\0’,这时候如果我们来打印一下看会发生什么呢?
#include<stdio.h>
int main()
{
char arr1[] = {'a','b','c'};
char arr2[] = "abc";
printf("%s\n",arr1);
printf("%s\n",arr2);
return 0;
}
还是这两个数组,看起来内容只是差了一个‘\0’,但是就是这个\0影响了这个结果,对于字符串来说,\0是结束标志,当我们没有这个结束标志时,编译器就不知道什么时候停下来,就会打印出了很多随机值,我们就看到了烫烫烫烫等等。解决方法就是在arr1中加一个\0或者0,因为\0的ASCII码值就是0,所以放0也是可以的。
另外还是float类型,double类型的数组等等,这些都是类似的,我们这里只重点学习int和char类型,我们基本用到的也就这两种。
一维数组在内存中的存储
虽然教材中一般都是先讲使用最后再讲存储,或者压根就不讲,但是我们还是要先讲一下数组的存储是为了能够更好的理解数组,然后才能更好的去使用数组。
那么一维数组是怎样存储的呢?
我们可以把数组的元素地址一个个的打印出来看一下,
#include<stdio.h>
int main()
{
int arr[5] = {1,2,3,4,5};
int i = 0;
for (i = 0; i < 5; i++)
{
printf("&arr[%d]=%p\n",i,&arr[i]);
}
return 0;
}
如果看不懂代码没关系,下面数组使用我们就会讲到,重点我们是要看一下地址:
可以看到每一个地址之间都是相隔4,那么4是多少呢,不就是一个整形的大小吗?也就是4个字节,
所以我们就可以想象出一维数组在内存中的存储方式就是连续的,并且每一个元素都相隔一个整形大小,我们可以画个图来理解:
注意:存储是从低地址向高地址依次存储的。后面在一期函数栈帧里面会详细解释。
这样就清晰多了。只要我们拿到第一个元素的地址,就可以顺藤摸瓜找到后面的元素,这样就可以访问我的数组了。
一维数组的使用
数组访问方式一般有两种,一种是通过下标访问,另一种就是通过指针直接找到数组中的元素。我们举例来说明:
#include<stdio.h>
int main()
{
int arr[5] = {1,2,3,4,5};
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%d ",arr[i]);
}
return 0;
}
这是第一种通过数组下标的方式来访问,[] 叫做下标访问操作符。需要重点注意的就是数组下标是从0开始的,一定一定要记住!!!
我们再来看第二种,
int main()
{
int arr[5] = {1,2,3,4,5};
int* pa = arr;
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%d ", *pa);
pa++;
}
return 0;
}
这里我们通过将数组第一个元素的地址传过去然后用指针来依次访问我的各个元素。
以上介绍的两种数组访问的方式都很重要,也都是需要掌握的。关于一维数组我们就到这里,接下来是二维数组,其实与一维数组大体上是一样的。
二维数组的创建和初始化
类比一维数组,我们只需要掌握一下创建和初始化的基本格式即可。
int main()
{
//3和4意味着数组3行4列;
//完全初始化;
int arr1[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
//不完全初始化;
int arr2[3][4] = { 1,2,3,4 };
return 0;
}
其他类型的数组格式相同,
重点要注意的是行数是可以省略的,但是列数绝对不能省略(原因在后面二维数组的存储会讲到);
这点一定一定一定要记住。
我们来看一下这两个数组里放的是什么,
可以看到我们确实是三行四列的形式,并且不完全初始化自动补充的是0。如果我们想要只在前两个元素放呢?那么就有了二维数组特有的初始化方式:
int main()
{
//特殊的初始化方式
int arr3[3][4] = { {1,2},{3,4},{4,5} };
return 0;
}
我们这时候来看一下数组中存放是怎样的,
可以看到,我们的元素是只存放在前两个元素的。
以上就是关于二维数组的创建和初始化。
二维数组的使用
与一维数组相同,二维数组也是通过下标和指针方式。不要忘记数组不管是行还是列也都是从0开始的,举例如下:
#include<stdio.h>
int main()
{
//特殊的初始化方式
int arr3[3][4] = { {1,2},{3,4},{4,5} };
//访问2行2列元素
printf("%d ",arr3[1][1]);
return 0;
}
指针的方式:
#include<stdio.h>
int main()
{
//特殊的初始化方式
int arr3[3][4] = { {1,2},{3,4},{4,5} };
printf("%d ", * (arr3 + 1)[1]);//访问2行2列的元素
return 0;
}
关于数组的使用就两种,只要理解掌握即可。我们重点来看下面二维数组的存储:
二维数组在内存中的存储
#include<stdio.h>
int main()
{
int arr[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 4; j++)
{
printf("&%d:%p \n",arr[i][j],&arr[i][j]);
}
}
return 0;
}
我们分析一下每个元素的地址,会发现依旧是每个元素相隔4个字节,所以我们可以得出结论即使是二维数组在内存中存储也是连续的,不是你想的方格那种形式,当我们还是连续存储的话,你就很容易理解上面为什么在二维数组初始化的时候不能省略列了。
我们只有理解数组在内存中是怎么存储的即可,这样我们在使用数组的时候脑海中就有一个非常清晰的思路,而不是死记硬背。
数组越界
1.数组的下标是有范围限制的。
2.数组的下规定是从0开始的,如果数组有n个元素,最后一个元素的下标就是n-1。 所以数组的下标如果小于0,或者大于n-1,就是数组越界访问了,超出了数组合法空间的访问。
3.C语言本身是不做数组下标的越界检查,编译器也不一定报错,但是编译器不报错,并不意味着程序就 是正确的,所以程序员写代码时,最好自己做越界的检查。
数组越界的一个经典问题就是如下图;
#include<stdio.h>
int main()
{
int arr[5] = {1,2,3,4,5};
int i = 0;
for (i = 1; i <= 5; i++)
{
printf("%d ",arr[i]);
}
return 0;
}
注意这是错误示例,首先这段代码并没有注意到数组下标是从0开始的,其次它最后i=5,本身也就数组越界了,但是编译器并没有报错,所以在使用数组时必须重点注意数组越界的问题;
另外还有人可能看到这样的问题,不用犹豫,直接去检查数组越界问题,编译器已经说明的很清楚了,一般程序员都特别犟,总是不认为自己的代码出错,希望大家谦虚一些,认真检查自己的代码。
好了,以上就是这期数组(一)的内容了,即使这期内容比较简单,还是希望大家能够静下心来,即使是基础我们就打扎实了了。在下一期数组(二)我们会去实现三子棋,还有扫雷这个一般是最最痛苦的三子棋。希望大家多多点赞支持!!