1 .什么是指针
在探究之前我们要弄清楚指针的概念,
- 指针做什么
int *p;
- 1
学过C的人都应该知道,这定义了一个指针,在这里p到底是个什么东西呢?
其实,p也就是一个变量,而对于变量,就可以理解为一个左值,会开辟一块内存空间,然后在这块空间中储存内容。
- 理解指针
int *p;
int a=10;
p=&a;
- 1
- 2
- 3
在这几句话,也不难理解,正因为像咱们在上面说的。开辟好了p这块空间,所以咱们现在就要在这块空间中,存放内容,而这个内容,在这里&a,也就是a的地址了。
如果你还不能理解,那么我用图来给你解释:
我想这样你应该会理解指针的作用,即存放地址。对于上面那个例子,p这块存放地址的空间我们叫做指针变量,p中存放的内存地址处的内存我们就称为p所指向的内存,通常我们也会说p指向了a。
-
“*”的作用
对于上面的理解,我们留下一个问题,就是“* ”,在定义一个指针时,我们需要加“ *”,而在使用时,我们也会加“ *”。
比如下面这段程序:
int *p;
int a=10;
p=&a;
*p=20;
- 1
- 2
- 3
- 4
“* ”我们就可以理解为,当一个基本的数据类型加上它时,这样就会构成一个数据类型的指针,这个指针我们要记住,大小永远是4个字节。而当我们写成 p 这样的形式时,我们在这里,称呼 *为解引用。就好像你现在手中会有一把钥匙,可以去通过指针来改变它所指向的那块空间的内容。
2.什么是数组
- 数组概念
数组,我们通常会理解为一组类型相同元素的集合。
比如:
int arr[]={1,2,3,4,5};
- 1
学过C的人应该都知道,这意思就是定义了一个数组,其中包含了5个int类型的元素。
当定义数组时,编译器会根据元素的个数和元素类型确定大小开辟空间,比如,上面的arr,我们就会分配20字节大小个空间。在这里,这块空间是不能再次改变的。
- 乱乱的&arr和&arr[0]
对于数组,我们一定要清楚&arr和&arr[0]这两个概念,&arr,这里说的就是对整个数组取地址。而&arr[0]在这里所说的也就是对数组的首个元素的地址。&arr和&arr[0]这两个值是相等的,虽然他们相等,但是意义是不一样的。这个的区别就好比陕西省政府与西安市政府,他们的所在地都一样,都是在西安,但是意义不一样。 -
数组名的左值右值
左值和右值,简单说就是可以出现在“=”右边的就是右值,可以出现在“=”左边的就是左值。左值,他要是一块可以被修改的空间,右值,所说的值一块空间中所带的内容。接下来我们要探讨一个问题了,数组名可以做左值吗?右值呢?
当arr作为右值时,我们要清楚,arr就是代表首元素的地址,在这里的arr就相当于arr[0]。
而当arr作为左值呢,在这我们就要根据左值右值的概念考虑了,左值必须是一块可以修改的空间,而这里,arr待变的是arr数组首元素的地址,地址不能被修改,所以,arr是不能作为左值的。
3.指针数组之间的联系
在我当初学习指针和数组时,总会觉得他们之间有种关系,并且好像彼此之间是一回事,在这里,我要告诉你,指针和数组之间是没有任何关系的!!!
很多人容易把这两个概念弄混淆很大的原因就是应为在访问时,指针和数组他们总是可以达到一样的目的。
例如:
char *p="abcdef";
- 1
- 指针形式和下标形式访问指针
对于上面的的指针变量p,根据前面咱们分析的可以知道,p是一块4个字节大小的空间,里面存放了首字符的地址。
比如我们需要访问’c’:
指针形式:*(p+2);这个意思就是先取出p的地址,然后给它按照字符型进行偏移2次,然后把所指向的地址进行解引用,访问其中所存放的内容。
数组下标形式:p[2];这里编译器会把下标形式解析成为指针形式的操作。这里的意思就是先取出p中存储的地址值,再加上其中括号中2个元素的偏移,得出新地址,然后再取出内容。
这两种方法最后使得我们所得到的结果都是一样的,两种方式本质上都是以指针为形式进行访问。
- 指针形式和下边形式访问数组
char arr[]="123456";
- 1
在这里我们需要访问’3’
指针形式:*(arr+2);这里就是得到arr的首字符地址,然后偏移两个字符,然后得到所指向地址,然后解引用,得到其中的内容。
数组下标形式:arr[2];这里编译器会把下标形式解析成为指针形式的操作。首先得到arr首元素的地址,然后偏移两个字符,得到新的地址,再取出内容。
经过了上述两个过程的分析,我们应该都知道指针和数组就是两个不一样的东西,他们只是可以”以指针的形式“和”以数组下标形式“来进行访问。
- 区分a与&a
首先我们通过牛客网的一道题来进行分析:
#include<stdio.h>
int main()
{
int a[5]={1,2,3,4,5};
int *ptr=(int *)(&a+1);
printf("%d,%d,*(a+1),*(ptr-1)");
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
在这里我们要分析:
&a+1:这个说的就是取了a的地址,然后向后偏移一个int,也就是到了这个数组最后一个位置的下一个地址。
(int )(&a+1):这个意思是将上述的地址强制转化为int 类型的地址。
所以执行完第二句话后,这里的ptr中存放的就是a这个数组的下一个数组的首地址。
*(a+1):这个的意思参考上面咱们分析的,所以也就可以得到这个的结果就是数组第二个元素。
所以,最终输出的结果也就是2和5;
这里我们依然强调一下&a说的是数组的地址,对他+1指的是偏移整个数组。
a这里代表的是a[0],首元素的地址。
4.指针数组和数组指针
指针数组:这里强调的是数组,然后数组中的元素都是指针,数组的大小是通过数组本身来决定。
数组指针:这里强调的是指针,就是说它还是一个指针变量,只不过他指向数组,至于他所指向的数组多大,这个就不知道了。
int *p1[5];
int (*p2)[5];
- 1
- 2
在这里我总结下来,指针数组和数组指针主要的区别主要是看优先级结合。”()”的优先级是最高的,下来是”[ ]”,再下来是”*“;
int* p1[5]:我们可以理解为,先于[10]结合,就是一个数组p1,然后是和”int*“结合,就表示数组中的内容全部都是int*类型的,所以这就是一个指针数组,数组中包含的是10个指向int类型数据的指针。所以他就是一个指针数组。
int (*p2)[5]:这里首先优先级最高的是”()“,所以先结合”()“,就是强调了这是个指针,然后和”[10]”进行结合,所以p2就是一个指向一个包含10个int大小的数组的指针。所以他就是一个数组指针。
5.函数指针,函数指针数组和函数指针数组指针
通过刚才对于指针数组和数组指针的理解,接下来我们来探讨一下函数数组和函数指针。
函数指针
- 什么是函数指针?
函数指针:顾名思义,这里强调的依然是指针,函数指针也就是说函数的指针,他是一个指向函数的指针。
char *fun3(char *p1,char *p2);
- 1
- 2
这个我想大家再熟悉不过了,函数为fun3,两个char* 类型的形参 p1,p2,函数返回值char *。
char * *fun2(char *p1,char *p2);
- 1
这里函数为fun2,两个char 类型的形参 p1,p2,函数返回值char **。
char *(*fun1)(char * p1,char *p2);
- 1
在这,我们就可以用优先级来进行判断,它先于()结合,可以知道fun1是一个指针,然后它指向的是一个函数。而这个函数呢,就是一个有两个char* 类型的形参 p1,p2,函数返回值char *的函数。
- 函数指针的使用
例:
#include<stdio.h>
int max(int x,int y)
{
return (x>y? x:y);
}
int main()
{
int (*ptr)(int, int);
int a, b, c;
ptr = max;
scanf("%d%d", &a, &b);
c = (*ptr)(a,b);
printf("a=%d, b=%d, max=%d", a, b, c);
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
在这一段程序中,ptr是指向函数的指针变量,所以可把函数max()赋给ptr作为ptr的值,即把max()的入口地址赋给ptr,以后就可以用ptr来调用该函数。
接下来看下一个例子:
void fun()
{
printf("CALL Fun!\n");
}
int main()
{
void (*p)();
*(int *)&p=(int)fun;
(*p)();
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
分析:
void (*p)():这一句说的就是我在这里定义了一个指针变量p,p用来指向函数,函数的返回值和参数都是void类型的。
*(int )&p=(int)fun:在这,就是说将fun函数的地址强制转换成int类型,然后赋给了p指针变量。
(*p)():表示对函数fun进行调用。
所以我们要清楚,函数指针还是一个指针,里面存放的是函数的首地址,然后我们通过首地址来调用函数。
(*(void (*)())0)();
- 1
这个是什么呢?
对于这个,其实咱们也可以通过分析进行解决。
void (*)():这个是一个void类型的一个函数指针,这个函数返回值为空,并且参数也为空。
(void (*)())0:这个就是将0强制转换成这个函数指针的类型,也就是说一个函数保存在首地址为0的一段区域。
(* ( void (*)())0):这是取得首地址为0的的内容,就是保存在首地址为0的的函数。
(* (void (*)())0)():这是最终对这个函数进行调用,因为函数的参数为空,所以调用时参数也是空的。
接下来我们再进行分析一个类似的:
(*(char * *(*)(char **,char **))0)(char * *,char * *);
- 1
是不是看这有些晕呢,咱们继续一层一层进行分析。
char * * ( * )(char * * ,char * * ):这个所说的就是一个函数指针,这个函数指针指向返回值为char * * * ,参数为两个char * *的参数。
(char * * ( * )(char * * ,char * * ))0:这是将0强制转换为这个函数指针的类型,也就是说一个函数保存在首地址为0的一段区域。
( * (char * * ( * )(char * * ,char * * ))0 ):这是取得首地址为0的的内容,就是保存在首地址为0的的函数。
( * (char * * ( * )(char * * ,char * * ))0)(char * * ,char * * ):这是最终对这个函数进行调用因为函数的参数为(char * * ,char * * ),所以调用也是给(char * * ,char * * )参数。
函数指针数组
函数指针数组:我们也可以参考前面的分析方法,它是一个数组,这个数组中的元素就是我们前面所提到的函数指针。
char *(* p[3])(char *p);
- 1
上面这个就是一个函数指针数组,首先它是有3个元素的一个数组,里面存放的是指向函数的指针,这些指针指向一些返回值类型为指向字符的指针。
在这里的关键你要知道这个是数组,数组中的元素是函数指针。
函数指针数组指针
到这里,我想许多人看见以后肯定非常晕了,一层套一层的,其实,没什么复杂的,万变不离其宗,咱们还是一层一层来进行分析。
首先给出一个函数指针数组指针
char *(*(*p)[3])(char *p);
- 1
在这里,首先按照优先级从个最里面看,最里面的(*p)这是一个指针。
(*( *p)[3])这个说了这个指针是指向含有三个元素的数组的指针。
char*( *(*p)[3])(char *p)这个说了这个数组中存放的元素是3个函数指针,这些指针指向一些返回值为指向字符的指针。然后这个函数的参数是指向字符的指针。