详解C语言编程中的函数指针以及函数回调

 更新时间:2016年04月20日 15:24:58   作者:鑫有灵犀  
这篇文章主要介绍了C语言编程中的函数指针以及函数回调,函数回调实际上就是让函数指针作函数参数、调用时传入函数地址,需要的朋友可以参考下

函数指针:

就是存储函数地址的指针,就是指向函数的指针,就是指针存储的值是函数地址,我们可以通过指针可以调用函数。

我们先来定义一个简单的函数:

//定义这样一个函数
void easyFunc()
{
  printf("I'm a easy Function\n");
}
//声明一个函数
void easyFunc();
//调用函数
easyFunc();

//定义这样一个函数
void easyFunc()
{
  printf("I'm a easy Function\n");
}
//声明一个函数
void easyFunc();
//调用函数
easyFunc();

上面三个步骤就是我们在学习函数的时候必须要做的,只有通过以上三步我们才算定义了一个完整的函数。

如何定义一个函数指针呢?前面我们定义其他类型的指针的格式是 类型 * 指针名 = 一个地址,比如:

int *p = &a;//定义了一个存储整形地址的指针p

也就是说如果我们要定义什么类型的指针就得知道什么类型,那么函数的类型怎么确定呢?函数的类型就是函数的声明把函数名去掉即可,比如上面的函数的类型就是:

void ()

我们再来声明一个有参数和返回值的函数:

int add(int a, int b);

上面函数的类型依旧是把函数名去掉即可:

int (int a, int b)

既然我们知道了函数的类型那么函数指针的类型就是在后面加个 * 即可,是不是这样呢?

int (int a, int b) * //这个是绝对错误的

上面这么定义是错误的,绝对是错误的,很多初学者都这样去做,总觉得就应该这样,其实函数指针的类型的定义正好比较特殊,它是这样的:

int (*) (int a, int b);//这里的型号在中间,一定要用括号括起来

int (*) (int a, int b);//这里的型号在中间,一定要用括号括起来

我们定义函数指针只需在 * 后面加个指针名称即可,也就是下面这样:

int (*p)(int a, int b) = NULL;//初始化为 NULL

int (*p)(int a, int b) = NULL;//初始化为 NULL

如果我们要给 p 赋值的话,我们就应该定义一个返回值类型为 int ,两个参数为 int 的函数:

int add(int a, int b)
{
  return a + b;
}
p = add;//给函数指针赋值

int add(int a, int b)
{
  return a + b;
}
p = add;//给函数指针赋值

经过上面的赋值,我们就可以使用 p 来代表函数:

p(5, 6);//等价于 add(5, 6);
printf("%d\n", p(5, b));

p(5, 6);//等价于 add(5, 6);
printf("%d\n", p(5, b));

输出结果为:11

通过上面的指针函数来使用函数,一般不是函数的主要用法,我们使用函数指针主要是用来实现函数的回调,通过把函数作为参数来使用。

函数指针的值

函数指针跟普通指针一样,存的也是一个内存地址, 只是这个地址是一个函数的起始地址, 下面这个程序打印出一个函数指针的值(func1.c):

#include <stdio.h>

typedef int (*Func)(int);

int Double(int a)
{
  return (a + a);
}

int main()
{
  Func p = Double;
  printf("%p\n", p);
  return 0;
}

编译、运行程序:

[lqy@localhost notlong]$ gcc -O2 -o func1 func1.c
[lqy@localhost notlong]$ ./func1
0x80483d0
[lqy@localhost notlong]$ 

然后我们用 nm 工具查看一下 Double 的地址, 看是不是正好是 0x80483d0:

[lqy@localhost notlong]$ nm func1 | sort
08048294 T _init
08048310 T _start
08048340 t __do_global_dtors_aux
080483a0 t frame_dummy
080483d0 T Double
080483e0 T main
...

  不出意料,Double 的起始地址果然是 0x080483d0。

函数回调

函数回调的本质就是让函数指针作为函数参数,函数调用时传入函数地址,也就是函数名即可。

我们什么时候使用回调函数呢?咱们先举个例子,比如现在小明现在作业有个题不会做,于是给小红打电话说:我现在作业有个题不会做,你能帮我做下吗?然后把答案告诉我?小红听到后觉得这个题也不是立刻能做出来的,所以跟小明说我做完之后告诉你。这个做完之后告诉小明就是函数的回调,如何告诉小明,小红必须有小明的联系方式,这个联系方式就是回调函数。接下来我们用代码来实现:

小明需要把联系方式留给小红,而且还得得到答案,因此需要个参数来保存答案:

void contactMethod(int answer)
{
  //把答案输出
  printf("答案为:%d\n", answer);
}

void contactMethod(int answer)
{
  //把答案输出
  printf("答案为:%d\n", answer);
}
小红这边得拿到小明的联系方式,需要用函数指针来存储这个方法:


void tellXiaoMing(int xiaoHongAnswer, void (*p)(int))
{
  p(xiaoHongAnswer);
}
//当小红把答案做出来的时候,小红把答案通过小明留下的联系方式传过去
tellXiaoMing(4, contactMethod);

void tellXiaoMing(int xiaoHongAnswer, void (*p)(int))
{
  p(xiaoHongAnswer);
}
//当小红把答案做出来的时候,小红把答案通过小明留下的联系方式传过去
tellXiaoMing(4, contactMethod);


上面的回调有人会问为什么我们不能直接 tellXiaoMing 方法中直接调用 contactMethod 函数呢?因为小红如果用函数指针作为参数的时候,不仅可以存储小明的联系方式,还可以存储小军的联系方式,这样的话我这边的代码就不用修改了,你只需要传入不同的参数就行了,因此这样的设计代码重用性很高,灵活性很大。

函数回调的整个过程就是上面这样,这里有个主要特点就是当我们使用回调的时候,一般用在一个方法需要等待操作的时候,比如上面的小红要等到答案做出来的时候才通知小明,不如当小明问小红时,小红直接能给出答案,就没必要有回调了,那执行顺序就是:
2016420152222457.png (458×514)
回调的顺序是:

2016420152256870.jpg (904×850)

上面的小红做题是个等待操作,比较耗时,小明也不能一直拿着电话等待,所以只有小红做出来之后,再把电话打回去才能告诉小明答案。

因此函数回调有两个主要特征:

函数指针作为参数,可以传入不同的函数,因此可以回调不同的函数
函数回调一般使用在需要等待或者耗时操作,或者得在一定时间或者事件触发后回调执行的情况下
我们使用函数回调来实现一个动态排序,我们现在个学生的结构体,里面包含了姓名,年龄,成绩,我们有个排序学生的方法,但是具体是按照姓名排?还是年龄排?还是成绩排?这个是不确定的,或者一会还会有新需求,因此通过动态排序写好之后,我们只需传入不同的函数即可。

定义学生结构体:

//定义个结构体 student,包含name,age 和 score
struct student {
  char name[255];
  int age;
  float score;
};
//typedef struct student 为 Student
typedef struct student Student;

定义比较结果的枚举:

//定义比较结果枚举
enum CompareResult {
  Student_Lager = 1, //1 代表大于
  Student_Same = 0,// 0 代表等于
  Student_Smaller = -1// -1 代表小于
};
//typedef enum CompareResult 为 StudentCompareResult
typedef enum CompareResult StudentCompareResult;

定义成绩,年龄和成绩比较函数:

/*
  通过成绩来比较学生
*/
StudentCompareResult compareByScore(Student st1, Student st2)
{
  if (st1.score > st2.score) {//如果前面学生成绩高于后面学生成绩,返回 1
    return Student_Lager;
  }
  else if (st1.score == st2.score) {//如果前面学生成绩等于后面学生成绩,返回 0
    return Student_Same;
  }
  else { //如果前面学生成绩低于后面学生成绩,返回 -1
    return Student_Smaller;
  }
}
 
/*
  通过年龄来比较学生
*/
StudentCompareResult compareByAge(Student st1, Student st2)
{
  if (st1.age > st2.age) {//如果前面学生年龄大于后面学生年龄,返回 1
    return Student_Lager;
  }
  else if (st1.age == st2.age) {//如果前面学生年龄等于后面学生年龄,返回 0
   return Student_Same;
  }
  else {//如果前面学生年龄小于后面学生年龄,返回 -1
    return Student_Smaller;
  } 
}
 
/*
   通过名字来比较学生
*/
StudentCompareResult compareByName(Student st1, Student st2)
{
  if (strcmp(st1.name, st2.name) > 0) {//如果前面学生名字在字典中的排序大于后面学生名字在字典中的排序,返回 1
    return Student_Lager;
  }
  else if (strcmp(st1.name, st2.name) == 0) {//如果前面学生名字在字典中的排序等于后面学生名字在字典中的排序,返回 0
    return Student_Same;
  }
  else {//如果前面学生名字在字典中的排序小于后面学生名字在字典中的排序,返回 -1
    return Student_Smaller;
  }  
}

定义排序函数:

/*
  根据不同的比较方式进行学生排序
  stu1[]:学生数组
  count :学生个数
  p :函数指针,来传递不同的比较方式函数
*/
void sortStudent(Student stu[], int count, StudentCompareResult (*p)(Student st1, Student st2))
{
  for (int i = 0; i < count - 1; i++) {
    for (int j = 0; j < count - i - 1; j++) {
      if (p(stu[j], stu[j + 1]) > 0) {
        Student tempStu = stu[j];
     stu[j] = stu[j + 1];
       stu[j + 1] = tempStu;
      }
    }
  }
}

定义结构体数组:

//定义四个学生结构体
Student st1 = {"lingxi", 24, 60.0};
Student st2 = {"blogs", 25, 70.0};
Student st3 = {"hello", 15, 100};
Student st4 = {"world", 45, 40.0};
//定义一个结构体数组,存放上面四个学生
Student sts[4] = {st1, st2, st3, st4};

输出排序前的数组,排序和排序后的数组:

//输出排序前数组中的学生名字
printf("排序前\n");
for (int i = 0; i < 4; i++) {
  printf("name = %s\n", sts[i].name);//输出名字
}
//进行排序
sortStudent(sts, 4, compareByName);
//输出排序后数组中的学生名字
printf("排序后\n");
for (int i = 0; i < 4; i++) {
  printf("name = %s\n", sts[i].name);
}

相关文章

  • C语言字符串原地压缩实现方法

    C语言字符串原地压缩实现方法

    这篇文章主要介绍了C语言字符串原地压缩实现方法,包含了字符串的遍历与转换等操作,是很实用的操作技巧,需要的朋友可以参考下
    2014-09-09
  • C语言使用广度优先搜索算法解决迷宫问题(队列)

    C语言使用广度优先搜索算法解决迷宫问题(队列)

    这篇文章主要介绍了C语言使用广度优先搜索算法解决迷宫问题,结合迷宫问题分析了C语言队列广度优先搜索算法的相关使用技巧,需要的朋友可以参考下
    2017-09-09
  • C++动态加载so/dll库的实现

    C++动态加载so/dll库的实现

    本文主要介绍了C++动态加载so/dll库的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-07-07
  • VC++中HTControl控制类使用之CHTDlgBase对话框基类实例

    VC++中HTControl控制类使用之CHTDlgBase对话框基类实例

    这篇文章主要介绍了VC++中HTControl控制类使用之CHTDlgBase对话框基类,是比较丰富而实用的功能,需要的朋友可以参考下
    2014-08-08
  • C语言巧用二分查找实现猜数游戏

    C语言巧用二分查找实现猜数游戏

    二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列,本篇文章教你用二分查找编写猜数字游戏
    2022-02-02
  • C++布隆过滤器的使用示例

    C++布隆过滤器的使用示例

    宁可错杀一千,也不放过一个,这是布隆过滤器的特点,本文主要介绍了C++布隆过滤器的使用示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-09-09
  • 二叉查找树的插入,删除,查找

    二叉查找树的插入,删除,查找

    以下是对二叉查找树的插入与删除以及查找进行了详细的介绍,需要的朋友可以 过来参考下
    2013-09-09
  • C语言 typedef:给类型起一个别名

    C语言 typedef:给类型起一个别名

    本文主要介绍C语言 typedef,这里整理了相关资料及简单示例代码帮助大家学习理解,有兴趣的小伙伴可以参考下
    2016-08-08
  • C++消息队列(定义,结构,如何创建,发送与接收)

    C++消息队列(定义,结构,如何创建,发送与接收)

    这篇文章主要介绍了C++消息队列(定义,结构,如何创建,发送与接收),消息队列是一种先进先出的队列型数据结构,实际上是系统内核中的一个内部链表
    2022-08-08
  • 关于C++面向对象设计的访问性问题详解

    关于C++面向对象设计的访问性问题详解

    这篇文章主要给大家介绍了关于C++面向对象设计的访问性问题的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
    2017-09-09

最新评论