️作者:@malloc不出对象
⛺专栏:《初识C语言》
个人简介:一名双非本科院校大二在读的科班编程菜鸟,努力编程只为赶上各位大佬的步伐
目录
- 前言
- 数组的性质
- 1.1 数组的内存布局
- 1.1 数组越界
- 1.2 数组是一种类型吗
- 指针与数组的联系
- 数组名就是指针吗
- 数组名作为左值和右值的区别
- 以指针的形式和以数组的形式访问元素
- 数组传参
- 指针越界访问的经典案例
- 为什么要将指针和数组元素访问打通
- 指针与数组的区别
前言
终于来到重磅文章了,本篇文章我将带大家详细分析指针与数组之间的联系,我在网上也看过很多文章但是我个人感觉讲解不够到位又或者是不够详细(大佬们轻喷),,还有的文章甚至误导了一部分读者竟然说数组是一种特殊的指针??!也有很多读者经常将指针与数组混淆,那么我想说的是其实指针与数组完全不是一个概念,,只是在C/C++中为了方便将它们之间的关系打通了罢了,注意我说的话,有联系并不代表它们就是一个东西!!接下来我就给大家详细讲解数组与指针之间的关系与区别。
数组的性质
在讲指针与数组之间的区别与联系时,我们先来看看数组的各种性质。
概念:数组是具有相同数据类型的集合。
1.1 数组的内存布局
在讲数组的内存布局之前,大家先来看一个例子:
#includeint main(){int a = 10;int b = 20;int c = 30;printf("%p\n", &a);printf("%p\n", &b);printf("%p\n", &c);return 0;}
这个例子很简单,下面让大家看看结果:
从上图我们发现先定义的变量地址是比较大的,后续依次减小,这是为什么呢?
a,b,c都在main函数中定义,也就是在栈上开辟临时变量;a先定义意味着a先开辟空间,那么a就先入栈,所以a的地址最高,栈是由高地址向低地址生长的。如果有读者还是不明白的话可以看我的这篇博客讲解十分详细。
由此,我们这里可以得出一个小结论:先定义的变量先入栈,先入栈的地址高。
接下来我们看下面一个例子:
#include#define N 10int main(){int a[N] = { 0 };for (int i = 0; i < N; i++){printf("&a[%d]: %p\n", i, &a[i]);}return 0;}
下面我给出结果:
我们发现,数组的地址排布是:&a[0] < &a[1] < &a[2] < … < &a[9]。
该数组在main函数中定义,那么也同样在栈上开辟空间,数组有多个元素,那么肯定是a[0]先被开辟空间啊,那么肯定&a[0]地址最大啊,那为什么会出现这种相反的情况呢?
这是因为编译器会提前根据数组的大小开辟一整块连续的空间,,然后找到地址最低处,这个地址最低处就是数组下标(索引值)为0处,,依次类推数组元素地址呈线性连续增大。
由此我们得出一个结论:在开辟空间的角度,不应该把数组认为是一个个独立的元素,应该整体开辟空间,整体释放。数组元素从低地址开始向高地址增长。
下面是对应的数组简单内存分布图:
1.1 数组越界
讲到数组的内存布局,接下来我顺便给大家看一个关于数组越界的例子:
#includeint main(){int i = 0;int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };for (i = 0; i <= 12; i++){printf("hehe\n");arr[i] = 0;}return 0;}
很明显这是一个数组越界的例子,但我想让大家想想它会出现什么现象?数组越界通常会造成什么影响呢?下面我们一起来看看结果:
这里的数组越界造成了死循环了,这是为什么呢?下面我们进行Debug一下,调试也是一项很重要的技巧,一名合格的程序员一定要善用调试,,很多问题其实都是可以通过调试找出来的。
我们一直调试到i等于该数组的最后一个索引,此时都没发现什么问题,,但你注意到此时的arr[12]竟然跟i的值是相等的,这是一种巧合吗?实际上并不是,读者可以自行调试你会发现arr[12]其实一直是等于i的,,这会不会是造成死循环的因子呢?我们接下来继续看
接着我们的i一直到了11,此时其实是已经越界访问了,,但我们看到arr[10]与arr[11]的值还是被赋为了0,接下来我们继续下去
直到i等于12了,此时arr[12]还是等于i的,但下一步将0赋值给arr[12]时,同时我们也发现i也变为0了,此时i变为0了又进行13次循环,又重新变为了0,此时这样就导致了死循环。
那么造成死循环的原因我们已经找出来了,其实就是i与arr[12]使用的是一块空间,,本质上它们两个就是一个东西,改变i就是改变arr[12],改变arr[12]也就是改变了i。 那么请读者再联系一下上面我们讲过的内存分布图,你能准确的画出i与数组之间的内存分布图吗?
下面给出该例数组与i之间的内存分布图:
这里再说明一下,i在哪个位置是取决于编译器的,我本次采用的是VS2019当循环条件为for(i = 0; i <= 12; i++)时会发生死循环,在DevC++中当循环条件为for(i = 0; i <= 10; i++))时会发生死循环,在gcc中当循环条件为for(i = 0; i <= 11; i++))时会发生死循环;这些下来读者可以自行去测试一番这里就不带大家测试了。
如果i是较数组后入栈开辟空间的,那么就不会出现死循环的现象,此时当数组越界会出现程序崩溃的现象,我们来检测一下:
数组越界是件影响巨大的事情,它可以造成死循环、程序崩溃、数值异常溢出等问题,,这里再补充说明一点数组越界时程序崩溃是不确定的;如果越界访问的内存,系统“另有他用”,比如分配给了另外的对象,则会崩溃;如果越界的内存是“闲着”的,则有可能不崩溃,但是可能会出现其他异常的现象。无论如何都始终记住不要写出数组越界这样的错误代码,,在C/C++中出于效率等问题不会帮我检查数组是否越界等问题,,所以我们只能自己控制数组范围,不要写出数组越界的代码。
1.2 数组是一种类型吗
严格意义上来讲,它并不是一种数据类型,,它的本质就是一种数据结构,但我个人觉得为了方便理解某些问题把数组理解成一种数据类型也未尝不可,,在Java中你知道一个数组是如何被定义的吗?
例如:int[] arr是这样的,而我们通常的习惯也是把数据类型写在前面变量名写在后面,在Java中定义数组我认为它设计更为方便也更符合我们的习惯,,那么我们看起来这种写法比较怪的原因是因为你习惯在C/C++中定义数组了,所以习惯一时改不过来,,当然为何C/C++设计者为何要这样设计的原因我们就不得而知了,,接着我再来继续证明一下我的观点。
我们来看一个例子:sizeof(int[10]),大家觉得它会打印出什么呢?又或者它就是一个语法错误根本不成立呢?
再回答这个问题之前,我们首先大致的了解一下sizeof的功能,其作用是返回一个对象或类型所占的内存字节数。
下面给出答案:
通过上述结果我们发现它成功的打印出了结果,,那么int[10]到底是一个对象还是一个类型呢?显而易见,从这个角度我们可以认为它就是一种类型,它的数组长度为10,每个元素为int类型占四个字节所以得到的答案为40。 提到sizeof我就顺便再补充一点,它是C/C++中的一个关键字,而非函数,,因为它的使用方式跟函数非常相似,很多人也就认为它是一个函数,这一点一定要时刻将两者区分开来。
再者我们举一个例子,大家先想一下它们两个分别是什么类型:
#includeint main(){int* arr[5];int(*parr)[5];return 0;}
好了,我们通过调试来看看:
首先我们自行来分析一下,arr是一个指针数组,它的数组大小为5,数组中每个元素为int*型,,parr是一个数组指针,它指向一个数组大小为5,数组中每个元素的类型为int型.
好了,接下来把我们的目光看向编译器为我们分析的类型,arr的类型为int *[5],首先[ ]比*更靠近变量名,所以arr变量名先与[5]结合成为一个长度为5的数组,,剩下的int*是不是就为数组元素的类型了,,综合起来arr是一个指针数组,它的数组大小为5,数组中每个元素的类型为int*,,这跟我们上面自己分析的一模一样,你有没有感到不可思议呢嘿嘿
接着我们继续来分析一下parr,它的类型为int[5] *,这分析起来就更简单了,,首先最右边的*先与变量名parr结合,,表示它为一个指针,,剩下的就是这个指针的类型int[5]了,,综上parr是一个数组指针,,它所指向的对象的类型为int[5],即指向大小为5,数组中的每个元素为int型的数组。
综上,大家有没有觉得写成int *[5]arr与int[5] *parr这样的形式更能快速的帮我们判断出变量是一个什么类型呢?其实博主认为孰能生巧,哪一种都可以,,只不过编译器为我们分析的类型不是我们采用标准罢了,,但它也是正确的,,这也在一定程序上可以反应出我们的数组是可以有类型的。
假设这里我们存在数组类型哈,接下来还有一个问题,数组的元素的个数(索引)是否是数组类型的一部分?例如:int[10] arr,这个10也是我们数组类型的一部分吗?
答案:是的,数组的索引也是我们数组类型的一部分。
接下来我们来看一个例子:
这里虽然能编译通过但是有一个警告,它明确指出了两者数组下标不同,,至此我们也就明白了数组的索引也是我们的数组类型的一部分。
关于数组是否是一种类型,,我个人的理解是数组是可以有类型的,,我们从各个角度也验证了这一点,,这一点大家心里清楚就好,,但不要搬到现场进行实用,因为它们不认这种写法。
好了,恭喜你们!!这部分内容白看了,,哈哈皮一下很开心,在理解某些问题把数组理解成一种类型还是很有用的orz~
指针与数组的联系
指针与数组是C语言中很重要的两个概念,它们之间有着密切的关系,利用这种关系,可以增强处理数组的灵活性,加快运行速度。下面我们就来详细探究一下指针与数组之间的联系。
数组名就是指针吗
数组名就是指针吗?!也许大部分人会说是的,,甚至一些教材或者C语言书籍也是这么说的,,那么这些说法都是绝对不严谨的,的确,在大部分情况下数组名确实就是一个常量指针,,例如:在进行数组元素访问的时候,我们都是通过先找到起始地址,再找到其偏移量才能够访问到我们的数组元素的,,此时我们的数组名就可以被当做一个常量指针(数组首元素的地址),它的数值与&arr[0]是相等的;在一维数组进行函数传参时它传的也是数组名(常量指针)…
但有俩种情况是除外的,,所以出于严谨的说法,数组名并不就是指针!!
下面我们就来验证一下:
第一种情况:在sizeof中此时数组名为整个数组
来看一个例子:
#includeint main(){int arr[10] = { 0 };printf("%zd\n", sizeof(arr));return 0;}
arr数组有10个元素,每个元素的类型为int型,所以求出的大小为10 * 4 = 40,,而此时数组名为一个指针的话,那么在32位/64位平台下打印的应该是4or8,答案显然不是,所以证明此时数组名在sizeof中不是指针而是代表着整个数组。
第二种情况:&数组名此时取的是整个数组的地址
我们来看一个例子:
#includeint main(){int arr[10] = { 0 };printf("%p\n", arr); printf("%p\n", &arr[0]);printf("%p\n", &arr);return 0;}
arr数组名与&arr[0]地址相等我能理解,它们都是数组首元素的地址,而那你不是说&数组名取的是整个数组的地址吗?那为什么&arr它跟arr数组名的地址又是相等的呢?
我们继续看一个例子:
#includeint main(){int arr[10] = { 0 };printf("%p\n", arr);printf("%p\n\n", arr + 1);printf("%p\n", &arr[0]);printf("%p\n\n", &arr[0] + 1);printf("%p\n", &arr);printf("%p\n", &arr + 1);return 0;}
从图中我们发现arr与&arr[0]加1的变化是一样的,,而&arr + 1之后变化就大了,让我们来看看两者之间相差多少个字节吧。
借助我们的计算器,我们计算得出两者相差字节数为40,而sizeof(arr) = 40,这说明是不是&arr+1跳过的是一整个数组呢?我们说指针±整数移动多少步取决于它所指向的对象类型,arr它指向的首元素,而数组中的元素为int型,,所以arr + 1移动四字节,,也就是跳到下一个元素,,&arr[0]同理,,而&arr它所指向的对象为整个数组,,因此+1跳过40个字节,,也就是跳过整个数组(跳到最后一个元素的下一个位置).
好了,我们说明了&数组名此时取到是整个数组的地址,那么这个&arr与arr及&arr[0]的值相等又有什么联系呢?
还记得我们之前说过一句话吗?在C语言中,&地址得到的一定是纵多字节址当中地址最低的那个,,那么我们取出整个数组的地址,,那么在整个数组当中地址最低的那个是不是也是为起始地址(首元素的地址)啊,,因此&数组名与数组名(首元素的地址)在值上是相等的,,而在类型上的意义是完全不一样的。
有的读者说你这只是证明了在一维数组中满足这个特点啊,,那在多维数组中呢?其实还是一样的,下面我们来证实二维数组:
#includeint main(){int arr[3][5] = { 0 };printf("%p\n", arr);printf("%p\n\n", arr + 1);printf("%p\n", &arr[0]);printf("%p\n\n", &arr[0] + 1);printf("%p\n", &arr);printf("%p\n", &arr + 1);return 0;}
在给出答案之前我先对这题做下分析,首先这是一个二维数组,,那么我们该如何更好的理解其中的&arr、arr、arr[0]之间的各项关系呢?我们把二维数组看成是一个一维数组,,那么此时这个”一维数组”当中是不是有三个元素,分别为arr[0]、arr[1]、arr[2],,看成一维数组以后那么它的每个元素是不是又是一个一维数组,,也就是三行嘛每一行有5个元素,,此时arr[0]、arr[1]、arr[2]就是三个一维数组的数组名,,arr是数组名那么它就应该等于首元素的地址即&arr[0],,而&arr[0][0]呢就是第一行首元素的地址,,那么你想想arr[0]是数组名,那么&arr[0]在数值上是不是等于&arr[0][0]呢?而&arr取到的又是整个二维数组的地址,,那么它的整个数组的起始地址是不是为首元素的地址呢,,此时当成一维数组时首元素地址即为&arr[0]对不对,,,所以&arr == &arr[0] == arr == &arr[0][0]在数值上是相等的。
下面还是通过一张图展示一下把二维数组看成一个一维数组的关系图:
相信看完上面这关系图,对于理解怎么把二维数组看成一维数组就没什么问题了,下面我给出答案:
&arr[0][0] + 1得到的是下一个元素的地址,此时它所指向的对象类型为int型,所以偏移4字节;
arr + 1得到的是下一个元素的地址,此时它所指向的对象的类型为一个一维数组,所以偏移一个一维数组大小的字节,,4 * 5 = 20;&arr[0] + 1与它是一样的,这里不再多说了
&arr + 1,它取到的是整个二维数组的地址,+1跳过整个二维数组,偏移3 * 5 * 4 = 60个字节。
那么关于&数组名,我从一维数组和二维数组两个角度证明了&数组名此时数组名为整个数组,取出的是整个数组的例子。
最后,关于数组名是否是指针我做一个总结:首先严谨的说明一下数组名它不是一个指针,在除去以上两种情况时:一、sizeof(数组名) 二、&数组名,我们就可以认为数组名它就是一个指针,在大多数情况下数组名也就是经常作为常量指针(首元素地址)来进行使用的。
数组名作为左值和右值的区别
我们来看一个例子:
#includeint main(){int arr[5] = { 0 };arr = { 1, 2, 3, 4, 5 };int i = 0;for(i = 0; i < 5; i++){printf("%d ", arr[i]);}return 0;}
很明显这里报了一个错误,它说表达式必须是可修改的左值,这也就意味着arr数组名是一个指针(地址)常量,它不能改变。
这里再给大家讲下指针(地址)常量和指针(地址)变量的区别:
数据存储的空间中的数据可以被修改,这个空间称为变量,如果空间中的数据不能被修改,这个空间称为常量。地址常量就是地址不能被修改,例如:一维数组中的数组名,是一个常量指针,不可被运算和不可被改变。地址变量就是地址能修改,例如:一级指针,是一个指针变量,可以通过移动下标或移动指针来进行改变。
除了整体初始化数组我们还可以对数组元素一个个赋值
这里得到一个小结论:数组只能整体进行初始化,不能整体进行赋值,数组名不能作为左值,只能根据数组下标索引一个个进行赋值操作。
以指针的形式和以数组的形式访问元素
我们先来看一个例子:
#include #include #define N 10int main(){char *str = "abcdef";char arr[] = "abcdef";int len = strlen(str);for (int i = 0; i < len; i++){printf("%c\t", *(str + i));printf("%c \n", str[i]);}printf("\n"); len = strlen(arr);for (int i = 0; i < len; i++){printf("%c\t", *(arr + i));printf("%c \n", arr[i]);}printf("\n");return 0;}
那么大家也是可以发现以指针的形式或者以数组下标的方式访问数组元素两种方式都是可以的,我们也是经常看到以指针和数组的方式来访问一段连续空间的元素,那这是不是说明两者的访问方式就是一样的呢?
其实并不是,指针和数组的访问(寻址)方式是完全不一样的,只能说两者具有相似性,但并不是同一个东西。
下面通过一张图让大家体会一下这个过程,,其实上面这个例子讲的就是字符指针与字符串之间的关系,,我在这篇博客其实讲的很详细了,,下面我就照搬一下图嘿嘿:
数组访问元素的方式:数组名在这种情况下,代表的是首元素的地址,它代表的是字面常量,可以直接进行寻址,根据下标索引找到对应的元素。
指针访问元素方式:先找到指针变量的空间取出其保存的内容,然后再根据偏移量找到对应元素的地址,找到对应元素的地址之后就能取出对应的元素了。
我们可以看到这俩者的访问(寻址)方案是完全不同的。
数组传参
这里我们主要以一维数组传参为例:
#include void TravelGroups(int arr[], int sz){int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}}int main(){int arr[] = { 1,2,3,4,5,6,7,8,9,10 };int sz = sizeof(arr) / sizeof(arr[0]);TravelGroups(arr, sz);return 0;}
这里我们利用TralveGroup函数实现的是一个简单遍历数组元素的功能,不过本次我们的重点不是要研究这个函数,,而是看看我们的数组在传参时会出现的一系列现象。
在探究这个问题之前,我们先来看几个问题:
1.给TraveGroups函数传递实参时,我们传的是整个数组还是数组名(首元素的地址)?
2.TraveGroups函数形参接收到的是整个数组还是数组名(首元素的地址)?
3.如果形参接收到的是整个数组会产生什么样的影响?
4.如果形参接收到是是数组名(首元素的地址),那么本质上它是一个什么?它解决了什么问题?它是否形成了临时拷贝?
下面我们就一起来分析这些问题:
其实第一个问题和第二个问题本质上是一样的,都是针对我们再传递参数和接收参数时,传递或者接收的是整个数组还是数组名(首元素的地址),,其实哈第一个问题我们已经解决了,还记得我们刚刚讲过数组名只有在两种情况下不代表指针,其他情况下都是一个指针,,那么我们这里还是分析一下,,如果我们传递的是整个数组,,那么你想一想如果数组很大的情况下,我们是不是要进行很多次传递,,那么就会消耗大量时间,并且只要有形参实例化,必定会形成一份临时拷贝,,这样在时间和空间上都存在大量的消耗,,这样就大大影响了函数调用的效率,,所以不管是实参传递还是形参接收其实都是数组首元素的地址;
那么本质上我们的形参是什么呢?大家也可以看到我的形参写的也是数组的形式int arr[],那么此时它是一个数组名亦或者一个数组?显然都不是,此时数组在传参时降维成了一个指针,它接收了数组首元素的地址,降维成指针解决了什么问题?避免了拷贝整个数组元素,节省了大量的时间和空间的损耗,既然指针变量是一个变量,那么形参实例化时也是要进行一份临时拷贝的,,我们将原本的拷贝整个数组的成本降低到了拷贝一份指针变量,这样函数调用的效率就大大提升了。
至于为什么形参也可以使用数组的形式来表示后面我们再来进行分析,那么你说它此时降维成了一个指针,该如何证明呢?
我们知道数组名只有在俩种情况下它不是一个指针,,但此时我们在函数内部使用sizeof求出来的不是整个数组的大小,,这就足以说明此时的arr不是整个数组,,求出来的答案为4正好是32位平台下指针变量的大小为4。
关于这点我顺便提一下一个地方,你注意到为什么我传递了两个参数?一个参数是数组名一个参数是数组大小?这里我们再穿插讲解一下指针越界访问的经典案例。
指针越界访问的经典案例
如果数组大小不明确,再加上代码写的不严谨就可能导致数组越界,然而编译器不报错,这就造成了一个隐形错误。下面来看一段代码:
#includevoid test(int arr[]){int i = 0;for (i = 0; i < 6; i++){printf("%d ", *(arr + i));}}int main(){int arr[5] = { 1,2,3,4,5 };test(arr);return 0;}
这是一个非常经典的指针访问越界问题,在arr数组里面我们没有arr[5]这个数值,所以访问时会出现一个随机值,当出现这样的情况时,我们的程序还是能继续运行下去不报错,但是结果确是不可预知的。
所以我们在平时应该养成良好的习惯:将数组和数组长度一起作为参数传递给函数,做出明确的说明,这样就避免了因为粗心而产生的错误。
好了,那么接下来可能又会出现另外一种常见的错误了,也是我们刚刚测试的数组传参降维成指针了,而部分人在函数内部通过sizeof计算结果,把这个结果当做了数组元素个数。这也是一个很经典的错误案例:
#includevoid test(int arr[]){int sz = sizeof(arr) / sizeof(arr[0]);int i = 0;for (i = 0; i < sz; i++){printf("%d ", *(arr + i));}}int main(){int arr[5] = { 1,2,3,4,5 };test(arr);return 0;}
很多不知道数组传参会发生降维的小伙伴这里一定会出现错误,,这里的错误不是说指针访问越界或者数值出现异常等问题,而是只能打印1个or2个元素,在x86下指针变量为4,x64下为8,所以此时求出来的sz只能是1或者2,下面我们来测试一下(x86下):
这里我们发现sz计算出来为1,所以只会打印出第一个元素,因此我再次强调一定要将数组和数组长度一起作为参数传递给函数,这样避免了因为粗心等问题而产生的错误。
关于指针访问越界的问题我就讲到这儿了。
回到正题,再者我们知道指针变量也是一个变量,那么变量也有它的地址,,接着我们就来看看这个地址到底是什么?
我们发现此时arr与&arr的值竟然也不一样了,,这说明此时形参根本就不是一个数组而是一个指针变量了。
再者,如果此时的形参为数组名的话,在上面我们已经讲过数组名(首元素的地址),它是一个常量指针,它是不能被修改的,,那么此时我们对此时的形参进行测验一番:
我们发现arr可以进行++操作,这由此也可以说明此时它不是数组名而是一个指针变量。
再者,我们也可以调试一番看看形参类型:
在未进入函数之前,arr还是数组名,&arr的值也还是等于arr
F11进入函数后我们来看看此时arr与&arr的类型的变化,很明显arr此时就是一个指针,它的类型为int*接收到的是数组首元素的地址,,&arr它的类型为int**只不过它没有一个二级指针变量接收而已。
以上我们就已经在多方面证明了一维数组在进行传参时,数组发生了降维,降维成了指针,为什么要降维成指针?其根本是为了保证数组在进行传参时函数调用的效率。
好了,,我们经常看到形参有时候也写成数组带索引的,,既然我们说数组在传参时会降维成指针,,那么这个索引写不写其实都是无所谓的,我们来简单看下例子:
我们发现之下下标索引在正常范围之内都是可以随便取的,就算你越界了也没什么关系,,因为此时的索引压根没有任何意义,,并且平时我们要写成数组形式的话,也是经常写成[ ]里面不带任何东西的这种形式。
关于上述我们说到一维数组在进行传参时降维成了指针,那么多维数组呢?它们也遵循这个规则吗?
是的,所有数组在进行传参时,都会降维成指向其内部元素类型的指针。
下面我就标标准准的按照指针的形式给大家进行讲解:
二维数组传参时降维成一个数组指针,它所指向的是一个大小为5元素类型为int型的数组,这里我个人认为其实不需要知道它是一个什么类型的指针,,最简单的办法是将第一维写成指针的形式,后续就是其所指内部元素的类型了,int (*arr)[5],,如果是三维数组的话假设它写成int arr[1][2][5],,根据这种方法,我们将第一维直接写成指针的形式,,后续就是其所指内部元素的类型,int (*arr)[2][5]这样就是三维数组传参时降维成指针的形式了。这种方式应该是最简单最有效的方法了直接无脑写。
如果有读者还是不明白为什么将第一维写成指针的形式后续就是其所指内部元素的类型,,我们还有一种方法根据我们编译器解析的数组类型去理解,,
这样我觉得就能够很好的理解了,*靠近变量名代表它是一个指针,,它所指向的类型是int[5],,从这个角度我觉得也非常的通俗易懂。
为什么要将指针和数组元素访问打通
讲完了数组传参,下面我们就来讲讲之前遗留下来的问题,数组传参时不是已经降维成了一个指针了吗?那为什么形参还可以写成数组的形式呢?为什么既可以使用数组下标访问元素又可以通过指针偏移访问元素?为什么C语言要将指针和数组的访问方式打通?
假设没有将指针和数组元素访问打通,那么在C中(面向过程)如果有大量的函数调用且有大量数组传参,会要求程序员进行各种访问习惯的变化。下面举个例子看看:
我们发现如果没将指针和数组元素访问的方式打通的话,那么在函数内部就只能使用指针的方式访问数组元素,在函数外部就只能使用数组下标的形式来访问元素了,,这样让我们的访问方式来回进行切换一旦代码多了起来这是很容易出现错误的,只要是要求人做的,那么就有提升代码出错的概率和调试的难度。所以干脆,C将指针和数组的访问方式打通,让程序员在函数内,也好像使用数组那样进行元素访问,本质值减少了编程难度!
又或者我们之前没有听过数组传参会发生降维这个知识点时,大家知道会有这个特征吗?包括我以及部分人一开始都把它当做数组来看待了(大佬轻喷~),我也是根本没想到过这个问题。为何C不想让我们知道?很简单,因为C语言是被用的,那么不同的语法,使用上越统一,那么节省就是人的精力,这极大程度上方便了我们的使用。
最后再对这部分内容总结一下:其实指针和数组完全不是一个概念,但不可否认它们之间确实关联性很强,,将指针与数组的访问方式打通极大程度上提高了代码运行的效率、减少了我们的出错率、使我们使用起来更加的方便,而关联性归关联性千万不要把它们混为一谈了,,很多人把它们混淆的一个最大的原因是就是因为数组名,,数组名在这其中可谓真是两者之间的媒介啊,可以说指针与数组之间的能够串联起来大部分原因都是因为数组名,读者需要仔细斟酌区分。
指针与数组的区别
关于指针与数组之间的联系我之前也明确说明了它们两者完全就不是一个东西,只是它们具有很强的关联性,,要实在让我讲讲它们两者的区别的话,,接下来我总结了几个很容易理解的区分点。
1.概念
数组:数组是用于存储多个类型数据的集合。
指针:这里我强调一下,此”指针”在大部分情况下非指针,而是指的指针变量,指针变量它是一个变量,是专门保存地址的变量,既然是变量它也有自己的空间地址,而我们的讲的指针它就是地址,地址就是指针,它其实就是一个数据。
2.赋值
数组只能一个一个元素的赋值或拷贝。注意我说的是赋值而非初始化,并且数组名只能作为右值,不能作为左值提供空间进行赋值操作,它是一个常量指针,因此也不能进行±操作。
指针变量可以相互赋值。指针变量是可以相互赋值的,即使它们的类型不一样会出现警告,但由于它们都是指针变量,在x86 or x64环境下都是4or8字节,,接收到的也都是地址,当然是可以进行互相赋值的。
3.寻址方式
数组是直接访问访问,此时数组名即首元素地址,再根据数组下标索引直接访问数组元素,这种寻址方式更为迅速。
指针是间接访问方式,首先要取得指针变量的内容作为起始地址,再要根据偏移量得到元素的地址,最后通过解引用访问到这个元素。
4.分配方式
数组是开辟一块连续的内存空间,通常在堆区或者栈上分布。
指针则是只分配一个指针大小的内存,但它很灵活,它可以指向任意类型的数据,并可把它的值指向某个有效的内存空间。
5.传参
数组:在传参过程中,数组都会被降维成指针,指向其内部元素的类型。
指针:当函数参数部分是指针时,能接收很多类型的实参。
一级指针:一级指针变量、对应类型变量的地址、一维数组的数组名。
二级指针:二级指针变量、一级指针变量的地址、一维指针数组的数组名。
降维的意义:C语言在进行传值时会进行临时拷贝,如果这个数组过大的话,那么我们如果要拷贝整个数组效率会大大降低并且拷贝时开辟空间可能会导致栈溢出的问题,因此C语言将数组的传参进行了降维,将数组名(首元素地址)拷贝一份传入函数,后续再根据指针或者数组下标的访问方式进行寻址即可。
好了,关于指针与数组之间的关系与区别就讲到这了,相信肯定还会有其他我没提及到的指针与数组的区别点,也欢迎各位大佬们前来补充啊。最后博主也是花了很多时间来总结出这么一篇文章,如果觉得博主写的不错的话可以给博主一点支持哦,,同样的如果文章有任何错处或者有疑问的地方欢迎在评论区相互交流哦~