数组指针和指针数组的区别
学习指针真的很让人头疼,最近几天无时不刻在想怎么使用它,现在又学了数组指针和指针数组,老实说,明明只是几个字调换了个位置就能有这么大差别,于是我想分享一下关于我对它们俩的看法int main()//指针数组int a = 10;int b = 20;int c = 30;//类比整型数组//数组指针//类比整型指针return 0;
前言
学习指针真的很让人头疼,最近几天无时不刻在想怎么使用它,现在又学了数组指针和指针数组,老实说,明明只是几个字调换了个位置就能有这么大差别,于是我想分享一下关于我对它们俩的看法
一、数组和指针的类型区别
指针数组和数组指针,顾名思义,谁在后面,谁就是重点,因为写出来过于繁杂,所以我想先从类型的区别说起,类型嘛,就是区别这个变量和那个变量代表什么,就比如int a和int* a,他们的类型就是int 和 int*;那类比来看,int* a[ ]和int(*a)[ n],他们的类型分别就是int*[ ]和int(*)[ n](注意,这里的n不能省略,这也与指针类型的作用有关)因为从操作符的优先级来看,' * '的优先级是低于' [ ] '的,所以用 ' () '将' * '单独提出,也是为了告诉我们,这个类型重点是指针。(倒是可以想成去掉变量名就是类型,不过这并不符合概念)
接着看int*a[ ]和int(*a)[n],从意义上我们可以分辨出,int*a[ ]中,a为指针数组的数组名,
[ ]为数组包含的元素个数,而int*代表数组中包含元素的类型为指针;int(*a)[n]中,a为它的数组指针变量名,而其中的“ * ”只有一个作用,就是说明修饰的变量是指针变量[n]为该指针指向数组包含的元素个数,而int为指向数组中的元素的类型。
二、具体区分二者
1.定义和初始化
代码如下:
int main()
{
//指针数组
int a = 10;
int b = 20;
int c = 30;
int* p[]={&a, &b, &c};
//类比整型数组
int p_arr = {a, b, c};
//数组指针
int arr[] = {1,2,3};
int(*p)[3] = &arr;
//类比整型指针
int* p_int = arr;
return 0;
}
可以看到,因为指针数组就是一个数组里面放了几个指针,数组指针就是一个指针指向了整个数组,所以此处的&arr并不是arr数组的首地址的地址什么奇怪的值,而是取出了整个数组的地址。但要注意,虽然如此,p的初始指向还是和p_int相同,指向数组首地址。
(p和p_int地址的具体区别留在之后说)
2.数组指针的指向
上文的例子说到,数组指针和整型数组指向一个数组时初始指向相同,那么怎么区分它们呢?
这里用实例说明更方便,代码如下:(运行结果是VS2022中拷贝过来,x86环境)
#include <stdio.h>
int main()
{
int arr[]={1, 2, 3};
int(*p) [3] = &arr;
int* p_int = arr;
//比较数值
printf("%d\n", *(*p));
printf("%d\n", *p_int);
//比较地址
printf("%p\n", &arr);//或arr
printf("%p\n", &arr + 1);//或p + 1
printf("%p\n", arr + 1);//或p_int + 1
return 0;
}
我们先从比较数值来直观证明两者的指向问题。首先,由于数组指针中存放的是整个数组的地址,若我们要得到指针的内容就必须先找到数组首元素地址(第一次解引用),再找到首元素地址对应的元素(第二次解引用),接下来比较结果,可以看到都为‘1’,这就是因为初始指向相同,解引用出对应元素自然就是相同的。
那么我们再通过比较地址来说明指针类型的具体作用,也是为什么上文说int(*a)[n]中的n不能省略的原因。从类型中我们知道,该数组指针指向一个int[3]类型的数组,而类型的作用之一就是决定指针加减时一次的跨度,比如此时,数组指针加一就会跨过整个数组,以下用简单的草图演示以下:
应该是比较直观能看出来p+1后的位置变化,而p+1的位置显然已经超出数组了,如果此时再解引用就造成数组越界访问,使用野指针,所以我们比较地址。能看到,数组首元素地址(第一个地址)与p+1后的结果(第二个地址)相差12byte,这说明其跨越了整个数组(3*4byte=12byte)的长度。而最后一个地址是p_int+1后的地址,根据上图能看出指向第二个元素的地址,所以与首元素地址相差4byte(虽然数据的存储用的是二进制,但为了方便显示会使用16进制打印出来)
注:由于p指向一个int[3]类型的数组,p_int指向int类型的数组首元素,所以+1后跨度被指向的类型决定为跨越int[3]或int的长度,这就是为什么int(*p)[3]的元素个数不能省略的原因,否则编译器不知道往后访问的具体长度。
3.指针数组的包含
数组指针可以通过指针位置的移动来实现上述效果,那么指针数组呢?代码如下:
#include <stdio.h>
int main()
{
int a = 1;
int b = 2;
int c = 3;
int* p[] = {&a, &b, &c};
int p_int[] = {a, b, c};
//比较数值
printf("%d\n", *(*p));
printf("%d\n", *p_int);
//比较地址
printf("%p\n", *p);
printf("%p\n", p);
printf("%p\n", p_int);
return 0;
}
我们依旧类比整型数组,奇怪的是,明显类型不同,但是比较方式却和数组指针的几乎一样,我的理解是数组名代表首元素地址,而指针的初始指向也是首元素地址,所以有很多相似之处,但它们有个根本区别就是,数组名是一个常量若想直接让它自增自减是不行的,但指针是变量,操作会灵活很多,修改指向的数组、指针算数等。回到这个例子,(*p)的结果应该是数组的首元素,
*(*p)的结果应该是将首元素指针解引用得到a的值,而(*p_int)自然就是首元素a的值。我们检验一下:
可以看出,结果确实无误。
我们继续比较地址,其实这里的比较地址并没有意义,为什么呢?因为我们知道,定义一个变量需要向内存申请一个空间,而这里的p和p_int两个变量实际上是两个单独开辟的空间,没有任何关系,之所以上文中的指针可以比较是因为它们指向同一个地址,若指针指向也不一样,比较也毫无意义。同理,p中的&a也与p的地址无关,因为在创建变量a时,就已经向内存申请了一个空间了,也就是说它们开辟空间时有明显的先后顺序,在栈区中的位置不一样,完全无关。
4.使用场景
说了那么多,还是不知道创建出这两个复杂类型的作用,那么我们来看看具体的使用场景:
1.指针数组
#include <stdio.h>
#include <stdlib.h>
int main()
{
//1.存储字符串
char result[] = "hello";
char* p_result[] = {"hello","world"};
for (int i = 0; i < 2; i++)
{
printf("%s\n", p_result[i]);//或*(p_result+i)
}
//2.管理申请的动态空间
char* arr[] = (char*)malloc(20*sizeof(char));
arr[0]='\0';
//3.存放数组
int arr1[] = {1, 2, 3, 4};
int arr2[] = {2, 3, 4, 5};
int arr3[] = {3, 4, 5, 6};
int* p_arr[3] = {arr1, arr2, arr3};
int i = 0;
int j = 0;
for(i = 0; i < 3; i++)
{
for(j = 0; j < 4; j++)
{
printf("%d ", p_arr[i][j]);//或(*(*p_arr+i)+j)
}
printf("\n");
}
return 0;
}
这里列举了三种常见的使用场景,我们一一解释。首先存储字符串,为什么一定要用指针数组呢,因为字符类型只能存储一个字符,字符数组只能存储一个字符串,若要存储多个字符串需要用字符指针数组(自然不是唯一方法),这样我们想访问多个字符串就会方便很多。
再是管理malloc申请的动态空间,首先我们的目的还是为了管理多个字符串,但不知道具体长度无法初始化,此时最好的方法就是使用malloc申请一个动态空间,而从c++官网我们可以知道malloc函数的具体参数和返回类型:
不仅需要包含头文件stdlib.h,还需要一个指针来接收,所以此时我们可以使用(字符)指针数组来接收一个存放多个字符串的空间(不是唯一方法)。
最后是存放数组,这其实和二维数组类似,但有根本区别,这里我们还是用草图演示:
依图,我认为两者的根本区别就是,二维数组中的元素是连续存放的,打印二维数组时只需要给出首元素地址就能顺藤摸瓜找到所有元素,但指针数组中只有每个一维数组是连续存放,其中每个一维数组需要解引用之后才能得到元素,显然,元素的内存并不是连续存放的,所以在打印指针数组时需要解引用两次(与二维数组的第二次解引用概念不同),访问不同的内存,效率与二维数组不同。
2.数组指针
代码如下:
#include<stdio.h>
void print(int (*p)str[5], int len)//int *p[]
{
int i=0;
for(i = 0; i < len; i++)
{
int j = 0;
for(j = 0; j < 5; j++)
{
printf("%d ", *(*(p+i)+j));
}
}
}
int main()
{
int arr[3][5] = {{1, 2, 3, 4, 5},{2, 3, 4, 5, 6},{3, 4, 5, 6, 7}};
print(arr, 3);
return 0;
}
这是数组指针常用的场景:作为形参,接收二维数组的首地址,这也是由于数组指针的特性,即指向整个数组,能够通过两次解引用找到数组每个元素。如果此处使用指针数组,虽然从形式上似乎很合理,但实际上指针数组里存放的是许多个指针,若我们把二维数组传给它会导致类型不匹配,这是根本上的错误。
若此处想用指针数组作为形参就是另外一个场景,此处用代码说明:
#include <stdio.h>
void print(int* p[], int len)//int** p
{
int i = 0;
for(i = 0;i < len; i++)
{
printf("%d ", *(*(p+i)));
}
}
int main()
{
int a = 10;
int b = 20;
int c = 30;
int* arr[3]={&a, &b, &c};
print(arr, 3);
return 0;
}
此处若想打印指针数组,形参在此处为了类型匹配只能写指针数组或二级指针,因为二级指针是指向数组首元素的,这可以类比整型数组传参,可以使用数组名也可以使用一级指针,这也是因为一级指针指向了数组首元素,只不过前者多了一次解引用。
总结
这就是一些我对数组指针和指针数组的一些浅薄的看法,其实最主要的只需要区分哪个是数组哪个是指针,数组名是常量,指针是变量,这是最明显的区别。
更多推荐
所有评论(0)