指针可以说是C语言最强大的功能之一,通过指针可以直接访问内存。然而指针也是C语言中非常难掌握的知识点之一。如果可以灵活地使用指针,基本上可以称得上是编程老手了。
本文通过我对于C语言指针的一些了解,对指针中容易让人混淆的几个概念进行一些分析和讲解。如有不足之处,还请大家帮忙指出。
C语言里,我们常用的一些数据类型有:char型,int型,float型,double型等,它们分别用来存放字符型、整型、单精度浮点型、双精度浮点型。和这些数据类型相比,指针实际上也是一种数据类型,不过与一般的数据类型不同的是,指针变量中存放的内容,是另一个变量的地址。因此也可以把指针理解为地址。
C语言一般通过变量来存储数据,用函数来定义一段可以重复使用的代码。无论是数据还是函数,最终都要被放到内存中,然后CPU通过地址来获取内存当中的代码和数据然后进行执行。可以说,指针就是C语言的灵魂。
正如前文所说,数据和代码都是存放在内存中,而CPU去访问它们的时候,都要通过地址去进行访问。
例如我们创建一个变量:int a = 0;(int占2或4个字节,我们以4字节为例)
如下图所示,整个大方格为内存的一片空间,每个小格代表一个字节,我们假设a的地址是0X00FFFFFF(16进制),当cpu需要访问a的时候,就通过访问0X00FFFFFF来访问a。
我们可以做一个比喻,假设你想找某一个你不认识的人,你知道他的名字和住址,请问你是通过名字来找他还是通过地址来找他呢?很明显,通过地址可以很方便的找到这个人,而通过名字则很难。
如果把* 和&看做双目运算符的话,它们分别表示乘和按为与,这里我们不作重点介绍。当它们作单目运算符时,分别表示( * )解引用运算符和 (&)取地址运算符。
&(取地址运算符)很好理解,顾名思义就是取地址。而*(解引用运算符)可能就不太好理解,我们可以把* 理解成取内容,取出某个地址里存放的内容。
#include
int main()
{
int a = 0; //创建int型的变量a
a = 10;
int* p = &a; //创建一个int型的指针p
printf("%d", *p); //打印a的值
return 0;
}
这是一段很简单的代码,我们先创建了一个整型变量a,并初始化为0,然后将它的值改为10,最后打印在屏幕上。我们主要看第六行代码: int* p = &a;
首先&a,是取a的地址,然后将它赋给p,所以p当中存放的就是a的地址,而int*则表明p是一个指向整型的指针变量。
关于int *p的解释,很多人会说这是语法规定的,指针变量就应该这么定义,没有什么要解释的,下面我将给出一种不同的解释。
正如前面所说, *是解引用运算符,或者说是取内容,那么我们这么理解int *p:
将 int *p拆分成两部分,int 和 * p; *p为取p的内容,即取出p中存放的地址的内容,取出这个内容之后,它是int型。总上所述,首先p中存放了地址 ,然后对p进行 *操作后,取出的数据是一个int型的数据,所以说它是int型的指针。
可能很多人看到这两个词在一起就一脸懵逼,刚开始的时候我也是这样,分不清什么是数组指针,什么是指针数组,后来通过老师的讲解才慢慢理解。希望我的理解能够对大家有帮助。
我们在这两个词之间分别加上一个“的”,即数组的指针和指针的数组。将前半部分其实是对主语的修饰。我们再举个例子:它是小明的书。请问这个“它”的本质内容是小明还是书呢?很明显“它”是一本书,而小明只是对这本书的修饰,表明它的所属。
类似地,我们再来分析数组指针和指针数组。数组指针即数组的指针,首先它是一个指针,然后它是属于数组的指针,因此数组指针就是一个指向数组的指针。
指针数组即指针的数组,首先它是一个数组,然后它是属于指针的数组,因此指针数组就是一个数组,这个数组的元素是指针。
先给出数组指针和指针数组的定义方式再进行解释:
int *arr1[10] ————————————————指针数组
int (*arr2)[10] ————————————————数组指针
再看一段代码,我们通过三种方式输出了数组arr[10]中的元素
#include
int main()
{
int arr[10] = {
1,2,3,4,5,6,7,8,9,10 };
int* arr1[10] = {
&arr[0],&arr[1],&arr[2],&arr[3],&arr[4],&arr[5],&arr[6],&arr[7],&arr[8],&arr[9]};
int(*arr2)[10] = &arr;
int i = 0;
//用arr输出每个元素
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
//用arr1输出每个元素
for (i = 0; i < 10; i++)
{
printf("%d ", *(arr1[i]));
}
printf("\n");
//用 arr2 输出每个元素
for (i = 0; i < 10; i++)
{
printf("%d ", *(*arr2 + i));
}
printf("\n");
return 0;
}
首先解释数组指针和指针数组的定义方式:
int *arr1[10] ————————————————指针数组
int (*arr2)[10] ————————————————数组指针
看起来指针数组和数组指针很像,只是第二个比第一个多了一个括号。我们要看变量名先和哪一个符号结合,如果先和[]结合,那么它就是数组,如果先和 * 结合,它就是指针。因为[]的优先级比 *高,所以,如果不加括号,变量名就先和[]结合,那它就是数组,加了括号,括号的优先级更高,因此先和 *结合,它就是指针。所以说区别指针数组和数组指针的关键就是看变量名先和哪一个符号结合。当弄清楚它先和谁结合之后,把变量名以及和它结合的部分删掉,剩下的部分就是它的类型。
例如:
int *arr1[10],arr1先和[10]结合,所以它首先是一个数组,其次这个数组有10个元素,剩下的部分是int * ,所以数组的每个元素的类型是int *,即每个元素都是一个整型指针。
int ( *arr2)[10],arr2先和 *结合,所以arr2是一个指针,剩下的部分是 int [10],所以它指向了一个有10个整型元素的数组。
我们再用 *运算符的方法分析:
int * arr1[10],arr1先和[10]结合,所以它首先是一个数组,其次这个数组有10个元素,然后对这个数组的元素进行 *(取内容操作),最后得到的类型是int,因此arr1是一个有10个元素的数组,数组的每个元素是整型指针。
int ( *arr2)[10],对arr2进行 *(取内容操作),得到的是int [10]类型(有10个整型元素的数组),所以arr2指向了一个有10个整型元素的数组。
int arr[10] = {
1,2,3,4,5,6,7,8,9,10 };
int* arr1[10] = {
&arr[0],&arr[1],&arr[2],&arr[3],&arr[4],&arr[5],&arr[6],&arr[7],&arr[8],&arr[9]};
int(*arr2)[10] = &arr;
arr是一个数组,每个元素是1,2,3,4,5,6,7,8,9,10;
分别对应:arr[0],arr[1]…arr[9];
arr1也是一个数组,但它的每个元素是一个指针,
分别指向arr[0],arr[1],arr[2]…arr[9]
arr2是一个指针,它指向了数组arr
//用arr1输出每个元素
for (i = 0; i < 10; i++)
{
printf("%d ", *(arr1[i]));
}
printf("\n");
//用 arr2 输出每个元素
for (i = 0; i < 10; i++)
{
printf("%d ", *(*arr2 + i));
}
printf("\n");
因为arr1中的每个元素分别指向了arr[0],arr[1]…arr[9],引起对arr1[i]进行*(解引用/取内容操作)就得到了arr[i];
因为arr2指向数组arr,* arr2得到了arr首元素的地址,* arr2 +i得到了arr[i]的地址,
再对* arr2 +i 进行* 操作,就得到了arr[i]。
printf("%p\n", arr2); //输出数组arr的地址
printf("%p\n", *arr2); //输出输出arr首元素的地址
可以看到,数组arr的地址和arr首元素的地址数值上相同,但是它们的含义不一样,一个是整个数组的地址,一个是数组首元素的地址。(%p是输出地址)我们再输出arr2+1,和* arr2 +1以及&arr[1]
printf("%p\n", arr2+1);
printf("%p\n", *arr2+1);
printf("%p\n", &arr[1]); //输出arr[1]的地址
arr2 +1和*arr2+1差了0x24,也就是36个字节,正好是9个整型变量的大小。
当对arr2加1时,会跳过整个数组,而对*arr2+1只会越过
当我们弄清楚了数组指针和指针数组之后,再来理解函数指针。和前面类似,函数指针就是函数的指针,首先它是一个指针,然后它指向了一个函数。
一个函数的定义是用 返回类型 + 函数名 +(函数参数)构成,那么函数指针是如何定义呢?
int Fun(int x, int y)
{
return 0;
}
Fun就是一个函数,它有两个整型参数,返回类型是int
int* Fun1(int x, int y)
{
return NULL;
}
Fun1也是一个函数,它有两个整型参数,返回类型是int *;
因为()优先级比较高,所以Fun1先和(int x,int y)结合,表明它是一个函数,返回类型是int *;
那么我们如何定义一个函数指针呢,由于 *比()优先级低,为了让函数名先和 *结合,我们应该给函数名和 *加一个括号,即:
int Fun(int x, int y)
{
return 0;
}
int main()
{
int (*p)(int x, int y) = &Fun;
return 0;
}
p先和 *结合,因此p是一个指针,将 *p删除,剩下的int (int x, int y),就是它所指向的变量的类型,即一个有两个整型参数,返回类型为int的函数。
我们也可以用另一种解释方法,对p进行 *(解引用/取内容)操作,得到了一个int (int x, int y)的函数,所以p就是一个指向有两个int型参数,返回类型为int的函数。
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 3;
int b = 2;
int sum1 = 0;
int sum2 = 0;
int sum3 = 0;
int (*p1)(int x, int y);
int (*p2)(int x, int y);
sum1 = Add(a, b);
printf("%d\n", sum1);
p1 = &Add;
sum2 = p1(a, b);
printf("%d\n", sum2);
p2 = Add;
sum3 = (*p2)(a,b);
printf("%d\n", sum3);
return 0;
}
对函数指针的赋值,可以对函数名&,然后赋值给函数指针,即p1 = &Add;
也可以直接将函数名赋值给函数指针,即p2 = Add;
通过函数指针调用函数时,同样有两种方式:(* p1)(a,b)或p2(a,b)。
即:指针名(参数);或(* 指针名)(参数)
当我们理解了数组指针、指针数组以及函数指针后,我们就可以开始疯狂套娃了
1、一个整型数: int a;
2、一个指向整型数的指针 int *p;
3、一个指向指针的指针 int **p;
4、一个有10个整型数的数组: int arr[10];
5、一个有10个指针的数组,该指针指向一个整型数 : int *arr[10];
6、一个指向数组的指针,该数组有10个整型元素:int ( *arr)[10];
7、一个指向数组的指针,该数组有10个指向整型数的指针:int *( *arr)[10];
9、一个指向函数的指针,该函数有一个整型参数并返回一个整型数: int ( *p)(int );
10、一个指向函数的指针,该函数有一个整型参数并返回一个整型指针:int * (*p)(int)
11、一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型:
int ( *arr[10])(int);
12、一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型指针:
int* ( *arr[10])(int);
8和11比较复杂,故只解释8和11。
首先看8:
arr先和[10]结合,表明arr是一个数组,该数组有10个元素,把arr[10]删除,剩下的部分为:
int ( *)[10]即为数组元素的类型,每个元素是一个数组指针,该指针指向了一个有10个整型元素的数组。
11:arr先和[10]结合,表明arr是一个数组,该数组有10个元素;剩下int ( *)(int),剩下的部分是函数指针的类型,因此arr数组的每个元素是一个函数指针,指向了一个有一个整型参数,返回值是int类型的函数。
是不是一脸懵逼
我们先将这个表达式通过加上空格,让它变得更加清晰
( * ( void( *) (void) ) 0 ) () ;
这样是不是更加清楚了一点?
再加上颜色:
( * ( void( *) (void) ) 0 ) ()
( * ( void( *) (void) ) 0 ) ()
蓝色部分void( *)(void)是一个函数指针类型,该函数没有参数,返回类型为void,我们将其用“类型”代替:
即void( *)(void)=类型
那么原式变为:( * ( 类型 ) 0 ) ()
(类型)0,就是强制类型转换,将0强制类型转换成一个函数指针,将(类型)0,替换为函数指针
原式变为( *函数指针)(),就是调用该函数
因此
( *(void( *)(void))0)();是先将0强制类型转换为一个函数指针,该指针指向一个无参数,返回类型为void类型的函数,然后再对这个函数进行调用。