C语言指针进阶(一)——深入详解“函数指针”与“指针函数”

前言:C语言最难的地方莫过于各种类型的指针,光听名字就把人给绕晕了,本文是在一些重要的基础概念之上进行说明的,需要一些知识储备,即:什么是数组指针、指针数组、函数指针、指针函数等,然后才能看得懂本文。

一、简单概述

1.1 函数指针

所谓函数指针即定义一个指向函数的指针变量,定义的格式如下:

int (*p)(int x, int  y);  //注意:这里的括号不能掉,因为括号()的运算优先级比解引用运算符*高

这个函数的类型是有两个整型参数,返回值是个整型。对应的函数指针类型

int (*) (int a, int b);  

对应的函数指针定义:

 
  1. int (*p)(int x, int y); //参数名可以去掉,并且通常都是去掉的。这样指针p就可以保存函数类型为两个整型参数,返回值是整型的函数地址了。

  2. int (*p)(int, int);

我们一般可以这么使用,通过函数指针调用函数:

 
  1. int maxValue (int a, int b) {

  2. return a > b ? a : b;

  3. }

  4. int (*p)(int, int) = NULL; //定义一个与maxValue兼容的指针

  5. p = maxValue;

  6. p(20, 45); //通过指针调用

1.2 指针函数

指针函数:指的是函数的返回值是一个指针,比如我的函数返回的是一个指向整数int的指针,定义格式如下:

int *p(int a,int b); //注意这里的*与P之间是没有括号的,所以含义是函数p(int,int)会返回一个(int *)指针

当然,函数本身也可能返回一个函数指针,后面会说到。

最重要的点:如何确定指针变量的类别是非常重要的,我们可以通过c++的

typeid(variable).name(); //查看变量的类别

总结如下:

名称(xx指针)含义(指向...的指针)定义形式指针p的类型typeid(p).name
变量指针指向变量的指针

int *p=&a

int (*p)=&a

int *
常量指针指向常量的指针

int const *p=&a(括号省略了)

const int *p=&a(括号省略了)

int const *
一维数组指针指向一维数组(首元素)的指针int  *p=a(括号省略了)int *
二维数组指针指向二维数组(第一行整体是首元素的指针)int (*p)[4]=aint (*)[4]
函数指针指向函数的指针int (*p)(int a,int b)=addint (*)(int,int)

显示详细信息

二、函数也可以作为参数

2.1 回调函数

上述内容是函数指针的基础用法,很多语言都支持函数作为参数和返回值,典型的像python动态语言,C语言当然也可以了,没错,其实函数指针更重要的意义在于函数回调。
举个例子:
现在我们有这样一个需求:实现一个函数,将一个整形数组中比50大的打印在控制台,我们可能这样实现:

 
  1. void compareNumberFunction(int *numberArray, int count, int compareNumber)

  2. {

  3. for (int i = 0; i < count; i++)

  4. {

  5. if (*(numberArray + i) > compareNumber)

  6. {

  7. printf("%d\n", *(numberArray + i));

  8. }

  9. }

  10. }

  11. int main()

  12. {

  13. int numberArray[5] = {15, 34, 44, 56, 64};

  14. int compareNumber = 50;

  15. compareNumberFunction(numberArray, 5, compareNumber);

  16. return 0;

  17. }

这样实现是没有问题的,然而现在我们又有这样一个需求:实现一个函数,将一个整形数组中比50小的打印在控制台。当然我么可以完全又把上面的代码copy一遍,将大于改写成小于。这样做当然可以,然而作为开发者,我们要未雨绸缪,要考虑到将来可能添加更多类似的需求,那么你将会有大量的重复代码,使你的项目变得臃肿,所以这个时候我们需要冷静下来思考,其实这两个需求很多代码都是相同的,只要更改一下判断条件即可,而判断条件我们如何变得更加灵活呢?

这时候我们就用到回调函数的知识了,我们可以定义一个函数,这个函数需要两个int型参数,函数内部实现代码是将两个整形数字做比较,将比较结果的bool值作为函数的返回值返回出来,以大于被比较数字的情况为例:

 
  1. BOOL compareGreater(int number, int compareNumber) {

  2. return number > compareNumber;

  3. }

同理,小于被比较的数字函数定义如下:

 
  1. BOOL compareLess(int number, int compareNumber) {

  2. return number < compareNumber;

  3. }

接下来,我们可以将这个函数作为compareNumberFunction的一个参数进行传递(没错,函数可以作为参数),那么我们就需要一个函数指针获取函数的地址,从而在compareNumberFunction内部进行对函数的调用,于是,compareNumberFunction函数的定义变成了这样:

 
  1. void compareNumberFunction(int *numberArray, int count, int compareNumber, BOOL (*p)(int, int))

  2. {

  3. for (int i = 0; i < count; i++)

  4. {

  5. if (p(*(numberArray + i), compareNumber)) //通过函数指针调用比较函数

  6. {

  7. printf("%d\n", *(numberArray + i));

  8. }

  9. }

  10. }

具体使用时代吗如下:

 
  1. int main() {

  2. int numberArray[5] = {15, 34, 44, 56, 64};

  3. int compareNumber = 50;

  4. // 大于被比较数字情况:

  5. compareNumberFunction(numberArray, 5, compareNumber, compareGreater);

  6. // 小于被比较数字情况:

  7. compareNumberFunction(numberArray, 5, compareNumber, compareLess);

  8. return 0;

  9. }

所以,函数回调本质为函数指针作为函数参数,函数调用时传入函数地址,这使我们的代码变得更加灵活,可复用性更强。

说了这么多,其实函数指针作为函数参数很简单,我们只要能知道函数指针的类型即可,一般格式如下:

void MyFunction(..., int (*p)(int,int),....)

下面是一些常见的函数指针,注意函数的返回值和参数类型要匹配哦!!!另外注意解引用运算符*上面的括号不能掉啊!!

 
  1. int (*p)(int,int) //有参数,有返回值的函数

  2. void (*p)(int,int) //有参数,无返回值的函数

  3. void (*p)() //无参数,无返回值的函数

  4. void (*p)(void)

2.2 借助于函数指针作为参数实现“动态排序”

首先我们应该理解动态这个词,我的理解就是不同时刻,不同场景,发生不同的事,这就是动态。动态排序就是根据不同的排序指标进行排序,不用书写很多重复性的代码,话不多说,直接上案例。

需求: 有30个学生需要排序
按成绩排
按年龄排

这种无法预测的需求变更,就是我们上文说的动态场景,那么解决方案就是函数回调:

 
  1. //定义一个结构体

  2. typedef struct student

  3. {

  4. char name[20];

  5. int age;

  6. float score;

  7. }Student;

  8. //比较两个学生的年龄

  9. BOOL compareByAge(Student stu1, Student stu2)

  10. {

  11. return stu1.age > stu2.age ? YES : NO;

  12. }

  13. //比较两个学生的成绩

  14. BOOL compareByScore(Student stu1, Student stu2)

  15. {

  16. return stu1.score > stu2.score ? YES : NO;

  17. }

  18. void sortStudents(Student *array, int n, BOOL(*p)(Student, Student))

  19. {

  20. Student temp;

  21. int flag = 0;

  22. for (int i = 0; i < n - 1 && flag == 0; i++)

  23. {

  24. flag = 1;

  25. for (int j = 0; j < n - i - 1; j++)

  26. {

  27. if (p(array[j], array[j + 1]))

  28. {

  29. temp = array[j];

  30. array[j] = array[j + 1];

  31. array[j + 1] = temp;

  32. flag = 0;

  33. }

  34. }

  35. }

  36. }

  37. int main() {

  38. Student stu1 = {"小明", 19, 98};

  39. Student stu2 = {"小红", 20, 78};

  40. Student stu3 = {"小白", 21, 88};

  41. Student stuArray[3] = {stu1, stu2, stu3};

  42. sortStudents(stuArray, 3, compareByScore);

  43. return 0;

  44. }

没错,动态排序就是这么简单!

 

三、函数指针作为函数的返回值

函数既然可以作为参数,自然也可以作为返回值。

比如我们有一个函数AFunction,这个函数的参数为一个字符串,即char类型的指针,还有一个函数指针参数接受一个函数作为参数;

要返回这样一个函数BFunction,这个函数有一个int类型的返回值,有两个int类型的参数,那指向这个函数的指针定义为如下:

int (*p)(int a,int b)=BFunction;

按照第一节的内容,这个指针的类型应改为:

int (*)(int a,int b)   //这就是BFunction的类型

那我们怎么去定义AFunction呢?

按照我们面向对象的思想,知道了函数的返回类型,我们这样定义是不是就可以了:

 
  1. int (*)(int, int) AFunction(char *ch,int (*p)(int,int))

  2. {

  3. }

  4. //前面的 int (*)(int, int) 就是我要返回的函数的指针

然而:这看起来很符合我们的理解,然而,这并不正确,编译器无法识别两个完全并行的包含形参的括号(int, int)和char *ch,int (*p)(int,int),

那到底该怎么做呢?真正的形式其实是这样:

int (*AFunction(char *ch,int (*p)(int,int)))(int, int);  

这种声明从外观上看更像是脸滚键盘出来的结果一团乱糟糟的,现在让我们来逐步的分析一下这个声明的组成步骤:AFunction() 是一个函数

  • (1)AFunction(char *ch,int (*p)(int,int)) 函数接受一个类型为char *的参数和一个函数指针int (*p)(int,int)
  • (2)*AFunction(char *ch,int (*p)(int,int)) 函数返回一个指针,这不就是“指针函数(返回一个指针的函数)”的通用形式吗?这不过这里返回的指针本神又指向一个函数而已,所以类比于通用形式:
int *p(int,int)  //指针函数的通用形式
  • 我们将这里的  *AFunction(char *ch,int (*p)(int,int))  这个整体看成是通用形式里面的  p
  • (3)(*findFunction(char *ch,int (*p)(int,int)))()  这个指针指向一个函数
  • (4)(*findFunction(char *ch,int (*p)(int,int)))(int, int)  指针指向的函数接受两个整形参数
  • (5)int (*findFunction(char *ch,int (*p)(int,int)))(int, int)指针指向的函数返回一个整形

现在我们的分析已经完成了,编译器可以通过了,我们来看一个简单的例子,要实现的功能如下:

函数AFunction同上面不变,他接受的参数是一个BFunction函数,然后根据给AFunction传递的参数信息,选择性的返回这个BFunction函数,如下:

 
  1. #include <iostream>

  2. #include <stdlib.h>

  3. using namespace std;

  4. //即上面需要返回的BFunction函数,执行加法操作

  5. int add(int a, int b)

  6. {

  7. return a + b;

  8. }

  9. int (*AFunction(const char * ch, int(*p)(int, int)))(int a,int b) //实际上就是 int (*p)(int,int)

  10. {

  11. if (ch == "add") //只有传入“add”的时候才返回加法函数,否则返回null

  12. {

  13. return p;

  14. }

  15. else

  16. {

  17. return NULL;

  18. }

  19. }

  20. int main()

  21. {

  22. //返回的类型要与定义的BFunction兼容

  23. int(*p)(int, int) = AFunction("add", add);

  24. int result = p(1000, 2000);

  25. printf("the result is : %d\n", result);

  26. getchar();

  27. return 0;

  28. }

我们可能会疑惑,这用一个简单的条件判断,然后直接调用BFunction还不是一样的,何必多此一举,为什么我要以函数去获取函数呢,直接使用BFunction不就好了么,其实在以后的编程过程中,很有可能maxValue和minValue被封装了起来,类的外部是不能直接使用的,那么我们就需要这种方式,如果你学习了Objective-C你会发现,所有的方法调用的实现原理都是如此。

但是,上面的这个定义是在是太过于难看,括号那么多,看的不清楚,有没有简单一些的方法,当然是有的,借助于typedef即可完成。

我们说有下面的关系:

int (*p)(int,int) 实际上等价于 int (*)(int,int)  p

前者是正确的书写,后者是面向对象更直观的展现,方便人看,最然并不能通过编译,我们借助于typedef可以完成这一转变。

 
  1. typedef int(*FUNC)(int, int);

  2. //这就相当于自定定义一个 “类型 对象” 的转换

  3. //等价于 int(*)(int, int) 这个类型用FUNC来简短表示

等价于  int(*)(int, int) 这个类型FUNC来简短表示 ,FUNC在这里就有了类型的含义了,注意理解,这个很重要;

现在我们来重新实现上面的代码,如下:

 
  1. #include <iostream>

  2. #include <stdlib.h>

  3. using namespace std;

  4. typedef int(*FUNC)(int, int); //定义一个FUNC代表 int(*)(int, int) 类型

  5. //即上面需要返回的BFunction函数,执行加法操作

  6. int add(int a, int b)

  7. {

  8. return a + b;

  9. }

  10. FUNC AFunction(const char * ch, int(*p)(int, int)) //这实际上就是 “类型名称 对象名称”,看起来就比较自然了

  11. {

  12. if (ch == "add") //只有传入“add”的时候才返回加法函数,否则返回null

  13. {

  14. return p;

  15. }

  16. else

  17. {

  18. return NULL;

  19. }

  20. }

  21. int main()

  22. {

  23. //返回的类型要与定义的BFunction兼容,也是“类型名称 对象名称”的表示

  24. FUNC p = AFunction("add", add);

  25. int result = p(1000, 2000);

  26. printf("the result is : %d\n", result);

  27. getchar();

  28. return 0;

  29. }


是不是觉得原来C语言也可以这么灵活,也可以这么玩啊。

四、C语言使用typedef 简化指针定义

typedef的作用就是专门给类型名起一个别名的,如下:

 
  1. typedef int HaHa;

  2. typedef double Hello;

所以我们同样可以给指针类型起一个别名。

(1)变量指针

 
  1. int a = 100;

  2. typedef int * Pointer; //Pointer就是类型 int *,int *是类型名,Pointer是别名

  3. Pointer p = &a;

(2)常量指针

 
  1. int const a = 100;

  2. typedef int const * Pointer; //Pointer的类型就是 int const *,int const *是类型名,Pointer是别名

  3. Pointer p = &a;

(3)数组指针(二维的)

 
  1. int a[][4] = { {1,2,3,4},{5,6,7,8} };

  2. typedef int(*Pointer)[4]; // Pointer等价于类型 int (*)[4],int (*)[4]是类型名,Pointer是别名

  3. Pointer p = a;

(4)函数指针

 
  1. typedef int (*Pointer)(int,int); //Pointer等价于类型 int (*)(int,int),int (*)(int,int)是类型名,Pointer是别名

  2. Pointer p = add; //但是这里由于C语言语法的关系,我们不能写成 int (*)(int,int) Pointer 这样的形式

  3. //函数本身又返回一个指向int的指针

  4. typedef int *(*Pointer)(int,int); //Pointer等价于类型 int *(*)(int,int),int *(*)(int,int)是类型名,Pointer是别名

  5. Pointer p = add;

总结:通过typedef我们可以将C语言晦涩难懂的各种指针统一成一样的格式,即

类型  变量

这样的规范格式,方便查看。

C语言指针进阶(一)——深入详解“函数指针”与“指针函数”-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值