【C语言】—— 指针三 : 参透数组传参的本质
- 一、数组名的理解
- 二、使用指针访问数组
- 2.1、指针访问数组
- 2.2、[ ] 的深入理解
- 2.3、数组与指针的区别
- 三、一维数组的传参本质
- 四、数组指针变量
- 4.1、数组指针变量是什么
- 4.2、 数组指针的初始化
- 五、二维数组传参的本质
一、数组名的理解
如果我们想用指针来访问数组,我们可以怎么做呢?
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
之前我们都是通过 &
a
r
r
arr
arr[0] 的方式得到数组首元素的地址,因为数组地址在内存中是连续存放
的,得到首元素的地址就能访问整个数组。
但其实,数组名本身就是一个地址,是数组首元素的地址
,不信?我们可以通过代码来验证一下。
#include<stdio.h>
int main()
{
int arr[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);
return 0;
}
输出结果:
我们发现,他们的输出结果是一样的,进行指针运算,同样是跳过了四个字节
,所以数组名就是数组首元素的地址
。
这时,有小伙伴就要问了,那我之前用
s
i
z
e
o
f
sizeof
sizeof 为什么能求出整个数组的长度呢
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", sizeof(arr));
return 0;
}
输出的结果是 40,如果是地址的话,输出的结果应该是 4/8,为什么呢?难道前面说错了?
其实数组名是地址这本身并没有错,但是有两个例外:
- s i z e o f (数组名) sizeof(数组名) sizeof(数组名): s i z e o f sizeof sizeof 中
单独
放数组名,这里的数组名就表示整个数组
,计算的是整个数组的大小,单位是字节
- &数组名:这里的数组名
表示整个数组
,即取出整个数组的地址(整个数组的地址和数组首元素的地址在数值上相等的,但还是有区别的,下面详解)
除此之外,任何地方数组名就是数组首元素的地址
下面,我们可以通过代码来比较一下
#include<stdio.h>
int main()
{
int arr[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]);
printf("arr = %p\n", arr);
printf("arr+1 = %p\n", arr);
printf("&arr = %p\n", &arr);
printf("&arr+1 = %p\n", &arr);
return 0;
}
输出结果:
我们可以看到,&arr[0] 和 &arr[0]+1 相差 4 个字节
,同样
a
r
r
arr
arr 和
a
r
r
+
1
arr+1
arr+1 也是相差 4 个字节
&
a
r
r
arr
arr 和 &
a
r
r
arr
arr[0] 及
a
r
r
arr
arr 在数值上相等,但 &
a
r
r
arr
arr 和 &
a
r
r
+
1
arr+1
arr+1 相差 40 个字节
,这是因为 &
a
r
r
arr
arr 取出的是整个数组的地址,+1 跳过的是整个数组
,而该数组的大小为 40 个字节
看到这,相信大家对数组名有了更深层次的理解了吧。
二、使用指针访问数组
2.1、指针访问数组
学了前面的知识,我们就可以很方便地使用指针访问数组了:
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
//输入
int* p = arr;
for (i = 0; i < sz; i++)
{
scanf("%d", p + i);
//scanf("%d", arr + i);
}
//
for (i = 0; i < sz; i++);
{
printf("%d ", *(p + i));
}
return 0;
}
上述代码弄明白后,我们不妨想,数组名是首元素的地址
,可以赋值给
p
p
p,这里
p
p
p 和
a
r
r
arr
arr 是等价的,我们可以用
a
r
r
arr
arr [
i
i
i ] 来访问数组,那可不可以用
p
p
p [
i
i
i ] 访问数组呢?
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
//输入
int* p = arr;
for (i = 0; i < sz; i++)
{
scanf("%d", p + i);
//scanf("%d", arr + i);
}
//
for (i = 0; i < sz; i++);
{
printf("%d ", p[i]);
}
return 0;
}
可以看到,是可行
的,所以,本质上
p
p
p [
i
i
i ] 等价于
∗
(
p
+
i
)
*(p+i)
∗(p+i),同理,
a
r
r
arr
arr [
i
i
i ] 等价于
∗
(
a
r
r
+
1
)
*(arr+1)
∗(arr+1)
2.2、[ ] 的深入理解
这里,我们对下标引用操作符进行更深层次的理解
∗
(
a
r
r
+
1
)
*(arr+1)
∗(arr+1) 由加法交换律,应该与
∗
(
i
+
a
r
r
)
*(i+arr)
∗(i+arr) 等价,事实上也确实如此。这时,我们不妨做一个大胆的猜想:
- 既然 ∗ ( a r r + i ) *(arr+i) ∗(arr+i)等价于 a r r arr arr [ i i i ],那 ∗ ( i + a r r ) *(i+arr) ∗(i+arr) 是否等价于 i i i [ a r r arr arr ] 呢?
- 那这样的话,
a
r
r
arr
arr [
i
i
i ] 是不是与
i
i
i [
a
r
r
arr
arr ] 相等呢 ?那他们四种表示是不是相等呢?
实践出真知,我们直接上代码试试:
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
//arr[i]打印
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
//*(arr + i)打印
for (i = 0; i < sz; i++)
{
printf("%d ", *(arr + i));
}
printf("\n");
//*(i + arr)打印
for (i = 0; i < sz; i++)
{
printf("%d ", *(i + arr));
}
printf("\n");
//i[arr]打印
for (i = 0; i < sz; i++)
{
printf("%d ", i[arr]);
}
printf("\n");
return 0;
}
运行结果:
可以看到,猜想是 正确 的,其实下标引用操作符 [ ] 并没有那么神秘,它仅仅只是一个操作符而已
,就像 + 操作符一样,它的两个操作数可以互换位置,[] 也是如此
,事实上,
a
r
r
arr
arr [
i
i
i ] 在程序运行时,编译器都会以
∗
(
a
r
r
+
i
)
*(arr+i)
∗(arr+i) 的形式执行。当然,还是尽量别写成
i
i
i [
a
r
r
arr
arr ] 的形式,这样代码可读性不高
。
2.3、数组与指针的区别
讲了这么多,大家可能对数组与指针的区别有点糊涂了吧,这里我们来理一下
- 数组就是数组,是一块连续的空间。数组的大小和数组元素类型和数组大小都有关系。
- 指针(变量)就是指针(变量),大小 4/8 字节。
- 联系:数组名是地址,是首元素地址,可使用指针访问数组
三、一维数组的传参本质
在【C语言】—— 指针二 : 初识指针(下)一文中,我曾经提到,将变量名传递给函数,是传值调用,形参仅仅是实参的一份拷贝
,无法通过函数改变实参的值。
而将数组名传给函数,却可以通过函数改变数组的值。这是为什么呢,想到之前学过的传址调用(详情请看【C语言】—— 指针二 : 初识指针(下)),再结合刚刚所学到的知识:数组名是函数首元素的地址。
答案就不言而喻了:数组传参,传递的是数组名,本质上传递的是数组首元素的地址。
同时,形参中创建的数组不会再单独创建数组空间的,所以形参是可以省略数组大小的
这时,我们不妨想一个问题:之前我们都是在函数外面将数组的元素个数求好,再传递给函数,我们可以把数组传递给函数后,在函数内部求数组的元素个数吗?
#include<stdio.h>
void test(int arr[])
{
int sz2 = sizeof(arr) / sizeof(arr[0]);
printf("sz2 = %d\n", sz2);
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz1 = sizeof(arr) / sizeof(arr[0]);
printf("sz1 = %d\n", sz1);
test(arr);
return 0;
}
输出结果:
可见,函数内部并没有正确求出数组的元素个数
为什么呢?因为数组传参时,传递的是数组首元素的指针,虽然函数中
s
i
z
e
o
f
(
a
r
r
)
sizeof(arr)
sizeof(arr)的
a
r
r
arr
arr 是单独放在
s
i
z
e
o
f
sizeof
sizeof 中的,但前面函数传参时,
a
r
r
arr
arr 为指针,函数将
a
r
r
arr
arr 当做指针变量来接收,所以这里的
a
r
r
arr
arr 也被认为是指针,大小为 4/8,所以最终结果为 1/2。
所以在函数内部是无法求出数组元素个数的
#include<stdio.h>
void test(int arr[])//参数写成数组形式,本质上还是指针
{
printf("%d\n", sizeof(arr));
}
void test(int* arr)//参数写成指针形式
{
printf("%d\n", sizeof(arr));//计算一个指针变量的大小
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
test(arr);
return 0;
}
总结:一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式,当然,数组的形式本质上还是指针
。
四、数组指针变量
4.1、数组指针变量是什么
数组指针变量是什么,之前我们学过指针数组(详情请看【C语言】—— 指针二 : 初识指针(下)),指针数组是一种数组,存放的是指针变量。
那么数组指针呢?是数组还是变量?
前面,我们学习过:整形指针变量
i
n
t
int
int
∗
*
∗,存放的是整形变量的地址,能够指向整形数据的指针字符指针变量
c
h
a
r
char
char
∗
*
∗,存放字符形变量的地址,能够指向字符型数据的指针。
所以,数组指针变量应该是: 存放的是数组的地址,能够指向数组的指针变量
下面代码哪个是数组指针变量:
int* p1[10];
int (*p2)[10];
分析之前,我们先要知道: [ ] 的优先级比
∗
*
∗ 高
第一个,
p
p
p 先与 [ ] 结合,表示是一个数组
,剩下的
i
n
t
int
int
∗
*
∗ 表示数组存放的元素是
i
n
t
int
int
∗
*
∗,为指针数组
。
第二个,由于有(),
p
p
p 先与
∗
*
∗ 结合,表示是一个指针变量
,然后指针指向的是大小为 10 个整型的数组。
p
p
p 是一个指针,指向一个数组,为数组指针变量
。
4.2、 数组指针的初始化
数组指针变量是用来存放数组的地址的,那怎么获得数组的地址呢,这时就需要用到前面学的 &数组名 了
int arr[10] = { 10 };
&arr;//得到的是数组的地址
如果要存放数组的地址,就得存放在数组指针变量中
int(*p)[10] = &arr;
注:下标引用操作符 [ ] 中 10 不能省略,同时,要匹配得上。
数组指针类型解析:
五、二维数组传参的本质
有了前面对数组指针的了解,我们就可以了解二维数组传参的本质了。
过去,我们想传递二维数组时,可以这样写
#include<stdio.h>
void test(int a[3][5], int r, int c)
{
int i = 0;
int j = 0;
for (i = 0; i < r; i++)
{
for (j = 0; j < c; j++)
{
printf("%d ", a[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
test(arr, 3, 5);
return 0;
}
这里,实参是二维数组,形参也是二维数组的形式,那还有什么其他方法吗。
我们可以类比一下一维数组。对一维数组来说,除了可以传递一维数组的形式,还可以传递数组名,即首元素地址,那二维数组可不可以也传递数组名呢?
答案是 肯定 的,二维数组的数组名也是首元素的地址,但这之前,我们需再次理解二维数组:
二维数组可以看做每个元素是一维数组
的数组,即二维数组每个元素是一维数组,那么二维数组的首元素就是第一行
,即第一个一维数组。
所以,根据数组名就是数组首元素的地址这个规则,二维数组数组名是第一个一维数组的地址
,其类型为
i
n
t
[
5
]
int[5]
int[5] 。这样,在函数接收参数时,接收的是数组地址,所以形参应该为数组指针变量,类型为
i
n
t
(
∗
)
[
5
]
int(*)[5]
int(∗)[5].
二维数组传参本质上也是传递地址,传递的是第一行这个一维数组的地址
#include<stdio.h>
void test(int(*p)[5], int r, int c)
{
int i = 0;
int j = 0;
for (i = 0; i < r; i++)
{
for (j = 0; j < c; j++)
{
printf("%d ", *(*(p + i) + j));
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
test(arr, 3, 5);
return 0;
}
总结:二维数组传参,形参部分可写成数组形式,也可以写成指针形式。