指针(4):C 语言数组与指针深度剖析
- 前言
- 1. 数组名
- 1.1 数组名的理解
- 1.2 例外
- 1.2.1 sizeof(数组名)
- 1.2.2 &数组名
- 1.2.3 总结
- 2.使用指针访问数组
- 3.一维数组传参本质
- 总结
前言
很多人学 C 语言,数组和指针总是容易混淆,尤其是数组名到底是什么、指针怎么访问数组、数组传参到底发生了什么,这三块是 C 语言的重点,也是面试高频考点。本文围绕这三点把底层原理讲清楚。
1. 数组名
1.1 数组名的理解
我们知道,**“&”**能够取出一个元素的地址,那么如果我们想要取出一个数组中首元素的地址,我们就能够写出这样一个代码:
intarr[10]={1,2,3,4,5,6,7,8,9,10};int*p=&arr[0];通过这段代码,我们就能够将数组arr首元素的地址取出。但其实根本不需要这么麻烦,数组名它本来就是地址,并且还是数组首元素的地址,我们可以通过代码来观察一下:
#include<stdio.h>intmain(){intarr[10]={1,2,3,4,5,6,7,8,9,10};printf("&arr[0] = %p\n",&arr[0]);printf("arr = %p\n",arr);return0;}
通过代码及运行结果我们可以发现,数组首元素和数字名的地址的打印结果相同,这也就说明了数组名就是数组首元素的地址。
1.2 例外
1.2.1 sizeof(数组名)
通过上面的学习,我们知道了数组名就是数组首元素的地址,那么我们来看这样一段代码:
#include<stdio.h>intmain(){intarr[10]={1,2,3,4,5,6,7,8,9,10};printf("%d",sizeof(arr));return0;}这段代码的输出结果会是什么呢?,根据上面所学,数组名就是数组首元素的地址,那么它的输出结果就应该是4/8(一个地址的大小只可能是4或8),让我们把代码运行起来看看是否与我们的判断相符
这段代码的输出结果是40,并不是我们所预料中的4/8,那么这是为什么呢?难道说数组名其实并不是数组首元素的地址?
其实数组名就是数组首元素的地址,只不过这是个例外:sizeof(数组名),sizeof 中放数组名,这里的数组名表示的是整个数组,计算的是整个数组的大小,单位是字节,因此上面代码的输出结果为40(整个数组10个 int 类型整数的大小)。
此外还有一个例外,那就是“&数组名”。
1.2.2 &数组名
&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。(整个数组的地址和数组首元素的地址是有区别的)
通过文字描述可能不太好理解,接下来让我们通过一段代码来深入了解一下:
#include<stdio.h>intmain(){intarr[10]={1,2,3,4,5,6,7,8,9,10};printf("&arr[0] = %p\n",&arr[0]);printf("arr = %p\n",arr);printf("&arr = %p\n",&arr);return0;}让我们运行一下:
通过运行,我们可以发现,这三个的地址一摸一样,难道说整个数组的的地址和数组首元素的地址其实是一样的? 那么让我们再来看一段代码:
#include<stdio.h>intmain(){intarr[10]={1,2,3,4,5,6,7,8,9,10};printf("&arr[0] = %p\n",&arr[0]);printf("&arr[0]+1 = %p\n",&arr[0]+1);printf("arr = %p\n",arr);printf("arr+1 = %p\n",arr+1);printf("&arr = %p\n",&arr);printf("&arr+1 = %p\n",&arr+1);return0;}这段代码的运行结果会是什么呢?让我们来观察一下
这⾥我们发现&arr[0]和&arr[0]+1相差4个字节,arr和arr+1相差4个字节,是因为&arr[0]和arr都是⾸元素的地址,+1就是跳过⼀个元素。
但是&arr和&arr+1相差40个字节,这就是因为&arr取出的是数组的地址,+1操作是跳过整个数组的。
因此我们可以得出结论,&arr和arr所对应的地址值是相同的,但他们的类型是完全不同的,&arr所对应的是整个地址,而arr所对应的是数组首元素的地址。
1.2.3 总结
数组名就是数组⾸元素(第⼀个元素)的地址是对的,但是有两个例外:
sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩,单位是字节
&数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素的地址是有区别的)
除此之外,任何地⽅使⽤数组名,数组名都表⽰⾸元素的地址。
2.使用指针访问数组
有了前面知识的学习,接下来我们就能够轻松的实现用指针来访问数组。
数组下标访问本质就是指针运算。
让我们通过代码来深入了解一下:
#include<stdio.h>intmain(){intarr[10]={0};int*p=arr;inti=0;intsz=sizeof(arr)/sizeof(arr[0]);for(i=0;i<sz;i++){scanf("%d",p+i);}for(i=0;i<sz;i++){printf("%d ",*(p+i));}return0;}分析:
- 首先创建一个拥有10个元素的整形数组,并将其每个元素初始值赋值为0。
- 创建一个int类型的指针p,将数组arr首元素的地址赋值给该指针p。
- 创建一个int类型的变量sz,用来统计数组arr中元素的个数,sizeof(arr)算出数组整体的大小 ,sizeof(arr[0])计算数组首元素的大小(即数组每个元素的大小),两者相除算出数组中元素的个数。
- 创建循环,p+i(地址向后偏移i个元素类型大小)这本质上是一个地址,scanf(“%d”, p + i); 从键盘上读取一个数字存放在p+i地址所对应的内存单元当中,相当于存储在数组arr[ i ]中,循环总共读取10个数字。
- 循环打印 ,*(p+i)得到p+i地址所对应内存单元中存储的数据,其实也就等价于arr[ i ]。将数组arr中的元素全部打印出来。
这个代码搞明⽩后,我们再分析⼀下,数组名arr是数组⾸元素的地址,可以赋值给p,其实数组名arr和p在这⾥是等价的。那我们可以使⽤arr[ i ]可以访问数组的元素,那p[ i ]是否也可以访问数组呢?
让我们来看下面一段代码:
#include<stdio.h>intmain(){intarr[10]={1,2,3,4,5,6,7,8,9,10};int*p=arr;inti=0;intsz=sizeof(arr)/sizeof(arr[0]);for(i=0;i<sz;i++){printf("%d ",arr[i]);}printf("\n");for(i=0;i<sz;i++){printf("%d ",p[i]);}return0;}运行结果:
通过观察我们可以发现,p[ i ]和 *(p + i )是等价的,因此同理我们可以得到arr[i]应该等价于 *(arr+i)
总结:
数组下标访问本质就是指针运算。arr[i]等价于*(arr+i),也等价于*(i+arr),甚至可以写成i[arr]。
3.一维数组传参本质
之前我们学习过了,数组是可以传递给函数的(比如说sizeof(数组名)),本小节我们来探讨一下数组传参的本质。
⾸先从⼀个问题开始,我们之前都是在函数外部计算数组的元素个数,那么我们可以把数组传给⼀个函数后,从函数内部求数组的元素个数吗?
要想完成上面的问题,我们可能会写出这样一段代码:
#include<stdio.h>voidtest(intarr[]){intsz2=sizeof(arr)/sizeof(arr[0]);printf("sz2 = %d\n",sz2);}intmain(){intarr[10]={1,2,3,4,5,6,7,8,9,10};intsz1=sizeof(arr)/sizeof(arr[0]);printf("sz1 = %d\n",sz1);test(arr);return0;}我们来简单分析一下这个函数:
- 首先创建一个 int 类型的数组 arr ,里面存放十个元素,然后通过 sizeof 在主函数中算出 sz1(即数组总元素) 的大小,并将其打印。
- 将数组 arr 传参给 test 函数,test 函数进行与 “1” 中同样的计算,算出 sz2 的大小,并打印。
根据上述分析,我们得到的结果应该是 sz1 = 10,sz2 = 10,那么事实果真如此吗,让我们将代码运行起来观察一下:
这个结果出乎我们的意料,那么这个结果是怎么来的呢?
这就要学习数组传参的本质了,上个小节我们学习了:数组名是数组首元素的地址;那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参传递的是数组首元素的地址。
接下来让我们详细分析一下这段代码:
- 首先创建一个int类型的数组arr,里面存放十个元素,然后通过sizeof在主函数中算出sz1(即数组总元素) 的大小,并将其打印。
- 将数组 arr 传递给函数test(本质是将arr的首元素的地址传递给函数test)。
- 在test函数内部,sizeof (arr)计算的其实是 arr 这个指针的大小(在64位下大小为 8),sizeof( arr[0])计算的其实是arr [0]它自身类型的大小(即int类型的大小为4).
- 因此sizeof(arr) / sizeof(arr[0])结果应为8 / 4 = 2.
总结:函数形参的部分理论上应该使⽤指针变量来接收⾸元素的地址。那么在函数内部我们写sizeof(arr)计算的是⼀个地址的大小(单位字节)⽽不是数组的大小(单位字节)。正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。
总结
- 数组名:绝大多数场景代表首元素地址;仅
sizeof(数组名)、&数组名代表整个数组; - 指针访问数组:
arr[i]等价*(arr+i),下标是指针运算语法糖;数组≠指针,内存模型不一样; - 一维数组传参:数组名退化为首元素地址,形参本质是指针,必须手动传入元素个数。