前言
上一期讲了数组指针的原理,这一期接着上一期讲述数组指针的应用以及数组参数、函数参数。
首先看下面的代码进行上一期内容的复习,pc应该是什么类型?
char* arr[5] = {0};
xxx pc = &arr;
分析:
①首先判断arr是一个数组,数组的每一个元素类型是char*,所以arr是指针数组;
②&数组名表示整个数组的地址,pc一定是一个数组指针;
③既然是数组指针,可以先写(*pc)表明是一个指针,指向的是一个元素为char*类型的数组,一共有五个元素,即char* (*pc)[5] pc = &arr;
尝试使用数组指针遍历数组每一个元素;这里着重理解:若pc是数组指针,那么*pc是数组名,数组名其实就是数组首元素的地址,所以若需要打印数组元素,这里必须两次解引用。
但是正常的人类不会这么来遍历数组,直接使用一级指针就能完成遍历的操作,不建议这样去用。
1. 数组指针常见的用法
1.1 作为参数遍历二维数组:
使用数组指针遍历一维数组确实有些脱裤子放屁,但是使用数组指针却可以很轻易的遍历二维数组。
分析:
3*5的二维数组其实是三个int数组拼接而成,所以我们只需要使用数组指针指向第一行,数组指针+1就会跳转到下一行,每一行中需要对数组指针解引用,可以获得一维数组名,即一维数组首元素的地址,即类型为int*,此时再加上列j,最后再进行解引用就能获得每一个元素了。
1.2 下列是指针还是数组?
// 指针大全
int main()
{int i = 10;int* p1 = &i;int** p2 = &p1; char arr1[5] = { 0 };// p3,p5等价char* p3 = arr1;char p4 = arr1[0];char* p5 = &arr1[0];int* arr2[5] = { 0 }; char(*p6)[5] = &arr1; // ?int* (*p7)[5] = &arr2;// ??int(*parr[10])[5]; // ???return 0;
}
答案揭晓:
// 指针大全 int main() {int i = 10;int* p1 = &i;int** p2 = &p1; // 二级指针,存放一级指针变量的地址char arr1[5] = { 0 };// p3,p5等价char* p3 = arr1;char p4 = arr1[0];char* p5 = &arr1[0];int* arr2[5] = { 0 }; // 指针数组,每一个元素都是int*类型的char(*p6)[5] = &arr1; // 数组指针,指向数组char arr1[5]int* (*p7)[5] = &arr2;// 数组指针,指向数组int* arr2[5]int(*parr[10])[5]; // parr首先和[]结合,所以这是一个数组,去掉这两个部分,剩下int (*) [5]是一个数组指针// 所以这是一个可以容纳10个数组指针的数组return 0; }
最后一个 int(*parr[10])[5];有些难理解,我们可以采取的策略是:先定性,再去掉,最后判断。
①先定性:parr和【】首先结合,所以这一定是一个数组。
②parr[]去掉。
③判断剩下的部分:int(*)[5],这很显然是一个数组指针。
④下结论:这是一个可以存放10个数组指针的数组。
2. 数组参数和指针参数
写函数的时候,难免会把指针或者数组名传递给参数,我们该如何设计函数呢?
2.1 一维数组传参
下面的参数正确吗?
① 正确。一维数组传参的时候可以不指定数组长度。
②正确。形参和实参一致。
③正确。传入数组名,本质上就是首元素的地址,每一个元素是int*,可以使用指针接收。
④正确。形参和实参一致。
⑤正确。arr2是一个存放一级指针的数组,直接传入数组名,就是传递首元素的地址,首元素为一级指针,要存放一级指针的地址可以使用二级指针。
2.2 二维数组传参
下面的参数正确吗?
①、③正确,②错误原因图中已经标识。
二维数组传入数组名,在之前的例子已经讲过了,代表了首元素的地址,而首元素是一个数组,那么就是整个数组的地址,这个数组有五个元素,每一个元素是int类型,这里需要使用数组指针来接收地址。所以只有③正确。
2.3 一级指针传参
非常简单,下图可以直接概括。
2.4 二级指针传参
①正确。二级指针传参使用二级指针接受肯定是没有问题的。
②正确。使用一级指针的地址传参,当然也是没有问题的。
反过来想,如果形参使用二级指针,那么实参能传什么呢?除了刚刚讲的前两种情况,这里可以存储指针数组名。这是因为指针数组名是数组首元素的地址,首元素是一级指针,换言之也是一级指针的地址。
3. 函数指针介绍
顾名思义指向函数的指针。
这要牵扯到如何求函数的地址,我们知道&数组名是取出数组的地址,以此类推,&函数名可以取出函数的地址。
如何存放函数的地址呢?这就用到了函数指针,函数指针的定义也非常简单,首先确定这是一个指针*p,然后在后面加上大括号,确定这是一个函数指针,在大括号里面输入形参类型,在整个表达式前面标明返回类型。
利用函数指针调用函数也就顺理成章了,直接解引用之后按照函数的方法直接调用即可。
看似这一切都是脱裤子放屁,我为什么不直接调用函数呢?这是因为我们视野所限,后面还有更高级的玩法,例如将函数指针作为函数传递传给另外一个函数......
需要注意的一点是,这里的p可以不使用*解引用,因为add本质上是函数的地址,那么p存放的也是函数的地址,如果能够这样使用:add(),那么为什么不能这样使用:p();这里加上*只是为了明确这是一个指针。如果要加上*号,记得要加上括号。
3.1 函数指针的简单使用
作为另外一个函数的形参,在另外一个函数进行使用。
看到这里,你心中仍然觉得这是多此一举,那我为什么不直接调用呢?其实这涉及到面向对象的内容,试想一下:如果有四个方法,分别是加减乘除的功能,这四个方法除了方法名和方法体不一样,形参和返回值都是一样的,这时候我们只需要传入不同的方法名给这个函数指针,就能实现不同的方法,这就是多态!
3.2 函数指针的题目
首先来看一段来自《C陷阱和缺陷》这本书中非常有意思的代码。
①首先看void(*)(),如果这是void(*p)(),这就是函数指针,此时把变量名去掉,这就是变量的类型;类比int* p去掉p,int* 就是变量的类型一样。
②整体对这个变量类型大括号,后面再放一个0,这就是将0强制类型转换为函数指针类型,地址为0的地方存在一个函数,这个函数不需要返回值也不需要形参。
③然后对整体函数指针进行解引用,再进行函数的调用。
总结:以上代码就是一次函数的调用,调用的是0作为地址的函数,这个函数没有返回值没有形参。
以下是这本书的原话:
继续看一段来自这本书的一段代码。
分析:
①从signal入手,signal首先和()结合,()内部一个是整型类型,一个是函数指针类型,这是函数的声明,函数的声明的形参只需要注明形参的类型即可,例如:int add(int,int)。
②将signal(int,void(*)(int))去掉之后,看剩下的部分,void(*)(int),这毫无疑问是声明函数的返回值。
③所以总结一下,这个就是一个函数的声明,返回值是一个函数指针。
我们可以使用typedef简化一下:如此一来,一眼就能看出这是一个函数声明。
3.3 函数指针的用途
给出一个需求,计算两个数字的四则运算。
我们发现,在swich语句中代码存在大量冗余,只有一行不一样,这一行只是调用的方法不一样,剩下的部分都一样,我们该如何解决?
使用函数指针就可以完美解决,创建一个函数,形参是函数指针,根据传入的具体实参的不同,调用不同的方法,这个其实就是面向对象的多态。
回调函数也是这么做的,实现定义好函数指针,适时地进行调用函数。
今天的内容就到这了,下一期是最后一期关于指针的内容,如果对你有帮助,可以点赞收藏评论,一键三连,你的支持是我更新的最大动力!!