本篇文章主要讲解C语言中关于数组指针的相关内容,共有数组名的特殊作用,使用指针访问数组,数组指针,数组传参的本质,二级指针,指针数组以及用指针数组模拟二维数组七个板块。
目录
一 数组名的特殊作用
1 一维数组
我们先来看一段代码:
#include<stdio.h>
int main()
{
int arr[] = {1, 2, 3, 4, 5};
int* p1 = &arr[0];
printf("p1 = %p\n", p1);
printf("p1+1 = %p\n", p1 + 1);
printf("arr = %p\n", arr);
printf("arr+1 = %p\n", arr+1);
return 0;
}
运行结果是:
通过以上代码我们可以看到,在一维数组中,其实数组名代表的也是一个地址,而且代表的就是首元素的地址。
但是数组名有两种例外情况是不代表首元素地址的(二维数组相同),第一种情况就是在sizeof操作符中,数组名代表的是整个数组,如:
#include<stdio.h>
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
int sz = sizeof(arr);
printf("%d\n", sz);
return 0;
}
运行结果是:
sizeof操作符是求括号里参数在内存中所占的字节数,数组中是5个整型元素,由于此时的数组名代表整个数组,所以才能求出20。
第二种情况就是数组名放在了取地址操作符的后面(比如&arr),此时的数组名也是代表整个数组,所以&数组名取出的是整个数组的地址,如:
#include<stdio.h>
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
printf("arr+1 = %p\n", arr + 1);
printf("&arr+1= %p\n", &arr + 1);
return 0;
}
运行结果是:
我们可以看到虽然数组名与&数组名为同一个地址,但是数组名+1是跳过了4个字节,因为数组名代表首元素地址,所以其地址是int*类型,所以一次跳过4个字节,而&数组名+1却是跳过了20个字节,因为&数组名代表的是整个数组的地址,所以+1会跳过整个数组,它的指针类型我们到后面再说。
2 二维数组
在讲解二维数组的数组名之前,我们先来理解一下二维数组。
我们都知道一个二维数组可以这样创建:
int arr[3][5] = { 0 };
这代表创建了一个3行5列的二维数组,每行的下标是0,1,2,而每一样中又有5个元素,下标分别是0,1,2,3,4,那么我们是不是可以这样理解:二维数组中一共有3个元素,每个元素是一个一维数组,分别是arr[0],arr[1],arr[2],每个数组里面又包含5个元素,例如,第一行数组的第一个元素就是arr[0][0]。
这样来看,其实二维数组是一个一维数组的数组,在arr这个二维数组中,arr[0],arr[1],arr[2]就是二维数组的3个元素,也是二维数组中每个一维数组的数组名,表示每行数组中的首元素地址,也就是&arr[0][0],&arr[1][0],&arr[2][0],而二维数组的数组名arr其实是表示二维数组的首元素的地址,也就是arr[0]这个一维数组的地址(其类型到数组指针章节讲解),显然arr+1是跳过整个第一行的数组的,如:
#include<stdio.h>
int main()
{
int arr[3][5] = { 0 };
printf("arr = %p\n", arr);
printf("arr+1 = %p\n", arr + 1);
return 0;
}
运行结果是:
二 使用指针访问数组
1 一维数组
在以前,我们都是通过数组下标来访问数组的,如:
#include<stdio.h>
int main()
{
int arr[] = {1, 2, 3, 4, 5};
int sz = sizeof(arr)/sizeof(arr[0]);
int i = 0;
for (i = 0;i < sz;i++)
{
printf("%d ",arr[i]);
}
return 0;
}
现在学习了指针之后,我们就可以通过指针来访问数组了,如:
#include<stdio.h>
int main()
{
int arr[] = {1, 2, 3, 4, 5};
int* p = &arr[0];
int i = 0;
for (i = 0;i < 5;i++)
{
printf("%d ", *(p + i));
}
return 0;
}
运行结果是:
注意,上述代码中是通过指针加上一个偏移量来找到数组中的每个元素,所以代码中的p+i就相当于&arr[i],而且这里是把arr首元素的地址赋给了p,所以这里*(p + i) 是等价于 *(arr + i)的。
既然数组名也代表首元素地址,我们也可以通过数组名来进行数组元素的访问,如:
#include<stdio.h>
int main()
{
int arr[] = {1, 2, 3, 4, 5};
int i = 0;
for (i = 0;i < 5;i++)
{
printf("%d ", *(arr + i));
}
return 0;
}
运行结果是一样的。
到这我们就有一个问题了,既然arr[i]可以访问数组中的每个元素,*(arr+i)也可以访问每个元素,那么这两个是不是等价的呢?其实arr[i]这种方式,在内存中也是通过指针加减偏移量来找到每个元素的,所以i[arr]也是可以访问每个元素的,如:
#include<stdio.h>
int main()
{
int arr[] = {1, 2, 3, 4, 5};
int i = 0;
for (i = 0;i < 5;i++)
{
printf("%d ", i[arr]);
}
return 0;
}
运行结果是:
所以总结一下,如果把arr数组首元素的地址赋给指针变量p,这里会有几个等价关系:arr[i] =
i[arr] = *(arr + i) = *(p + i) = p[i]。
2 二维数组
通过指针访问二维数组元素代码如下:
#include<stdio.h>
int main()
{
int arr[2][5] = {1,2,3,4,5,6,7,8,9,10};
for (int i = 0;i < 2;i++)
{
for (int j = 0;j < 5;j++)
{
printf("%d ", *(*(arr + i) + j));//arr[i][j] == *(arr[i] + j)
}
printf("\n");
}
return 0;
}
运行结果:
三 数组指针
在二维数组数组名那一章节中,我们提到二维数组数组名其实是第一行数组的地址,是个数组指针,所以如果要把二维数组首元素的地址赋给一个指针变量,应该这样写代码:
#include<stdio.h>
int main()
{
int arr[2][5] = { 0 };
int(*p)[5] = arr;
return 0;
}
所以p数组指针的类型就是:
int(*)[5]
p先与*结合代表p是一个指针变量,左边的int和右边的[5]代表其是一个数组元素类型为int,数组元素个数为5的数组指针。
四 数组传参的本质
1 一维数组
以前我们对于数组传参是这样传参的:
#include<stdio.h>
void test(int arr[], int sz)
{
int i = 0;
for (i = 0;i < sz;i++)
{
arr[i] = i;
}
}
int main()
{
int arr[10] = { 0 };
int sz = sizeof(arr)/sizeof(arr[0]);
test(arr, sz);
int i = 0;
for (i = 0;i < sz;i++)
{
printf("%d ", arr[i]);
}
return 0;
}
而且我们知道数组传参是不会创建新的数组的,而且数组中的数组元素个数也不需要写,那么数组传参传递的真的是整个数组吗?我们再来看这么一段代码:
#include<stdio.h>
void test(int arr[])
{
printf("%d ",sizeof(arr)/sizeof(arr[0]));
}
int main()
{
int arr[10] = { 0 };
test(arr);
return 0;
}
运行结果是:
我们可以看到在x86环境下,运行结果是1,如果传递的arr表示整个数组的话,这里的运行结果应该是10,那么为什么是1呢?其实这里传递的arr是首元素的地址,在x86环境下是4个字节,而一个int类型的元素也是4个字节,所有运行结果就是1,所以形参中的数字是可以不写数组元素个数的。
既然传递的是数组首元素的地址的话,那么我们就可以这样写参数了:
#include<stdio.h>
void test(int* arr, int sz)
{
int i = 0;
for (i = 0;i < sz;i++)
{
arr[i] = i;
}
}
int main()
{
int arr[10] = { 0 };
int sz = sizeof(arr)/sizeof(arr[0]);
test(arr, sz);
int i = 0;
for (i = 0;i < sz;i++)
{
printf("%d ", arr[i]);
}
return 0;
}
运行结果是:
总结一下,一维数组传参函数的形参部分是既可以写成数组形式和指针形式的,写成数组形式会更好理解一点。
2 二维数组
类比一维数组,二维数组传参也是传递的首元素地址,但是二维数组首元素地址代表的是第一行数组的地址,所以二维数组形参部分也是可以写成指针形式的,只不过需要写成数组指针,如:
#include<stdio.h>
void test(int(*arr)[5], int sz)
{
int i = 0;
for (i = 0;i < sz;i++)
{
int j = 0;
for (j = 0;j < 5;j++)
{
arr[i][j] = j;
}
}
}
int main()
{
int arr[2][5] = { 0 };
test(arr, 2);
int i = 0;
for (i = 0;i < 2;i++)
{
int j = 0;
for (j = 0;j < 5;j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
运行结果是:
五 二级指针
在前面的讲解中,我们知道指针变量是用来存储其他变量的地址的,但是指针变量也是变量,其也是有地址呢,那么指针变量的地址该存放在什么地方呢?答案也是指针变量中,只不过是二级指针变量中,如:
#include<stdio.h>
int main()
{
int a = 10;
int* p = &a;
int** pp = &p;
printf("%d\n",**pp);
return 0;
}
运行结果是:
可以看到,图中的pp就是就是个二级指针,其类型是int**;类型,按照之前的理解,与pp变量名挨的近的那颗星代表pp是个指针变量,int*代表其所指向的变量类型为int*类型。
以此类推,三级指针变量就是用来存储二级指针变量的地址。所以,总结一下,就是二级指针是用来存储一级指针变量的地址,三级指针用来存储二级指针变量的地址。
六 指针数组
我们知道数组中的元素如果是整型的话,那么就叫做整型数组,如果是字符的话,那么就叫做字符数组,那么既然指针变量是个变量的话,那是不是也可以构成数组呢?答案是肯定的,一个数组中如果包含的变量是指针类型,那么就叫做指针数组,如:
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
int c = 30;
int* p[3] = {&a, &b, &c};
printf("%d\n", *p[0]);
return 0;
}
运行结果是:
当然这里也可以写成指针访问数组的形式:
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
int c = 30;
int* p[3] = {&a, &b, &c};
printf("%d\n", *(*(p + 0)));
return 0;
}
这里要注意指针数组与数组指针的区别,数组指针是一个数组类型的指针,比如一个5个元素的整形数组指针类型为int(*)[5],指针数组是一个指针变量的数组,比如上面那个代码,每个数组元素的类型为int*类型。
七 用指针数组模拟二维数组
我们可以发现一个指针数组中存放的是多个地址,所以是可以存放多个数组首元素地址的,当一个指针数组中包含多个一维数组首元素地址的时候就可以用指针数组模拟出二维数组了,如:
#include<stdio.h>
int main()
{
int arr1[] = {1, 2, 3, 4, 5};
int arr2[] = {2, 3, 4, 5, 6};
int arr3[] = {3, 4, 5, 6, 7};
int* prr[] = {arr1, arr2, arr3};
int i = 0;
for (i = 0;i < 3;i++)
{
int j = 0;
for (j = 0;j < 5;j++)
{
printf("%d ", prr[i][j]);//*(*(prr + i) + j)
}
printf("\n");
}
return 0;
}
运行结果是:
值得注意的是,这里只是用指针数组模拟出二维数组的效果,并不是真正的二维数组,因为真正的二维数组的内存空间是连续的,显然这里的三个一维数组之间的内存空间并不是连续的。