详解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);
}

相关文章

  • undefined reference to `SetPduPowerConsumptionCnt''错误的解决方法

    undefined reference to `SetPduPowerConsumptionCnt''错误的解决方法

    编译时出现undefined reference to `SetPduPowerConsumptionCnt'错误要如何解决呢?有没有什么好的解决方法?下面小编就为大家解答吧,如果你也遇到了这种情况,可以过来参考下
    2013-07-07
  • 利用c++和easyx图形库做一个低配版扫雷游戏

    利用c++和easyx图形库做一个低配版扫雷游戏

    这篇文章主要介绍了用c++和easyx图形库做一个低配版扫雷游戏,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-01-01
  • C++ 排序插入排序实例详解

    C++ 排序插入排序实例详解

    这篇文章主要介绍了C++ 排序插入排序实例详解的相关资料,需要的朋友可以参考下
    2017-06-06
  • C++数据结构之搜索二叉树的实现

    C++数据结构之搜索二叉树的实现

    了解搜索二叉树是为了STL中的map和set做铺垫,我们所熟知的AVL树和平衡搜索二叉树也需要搜索二叉树的基础。本文将详解如何利用C++实现搜索二叉树,需要的可以参考一下
    2022-05-05
  • C++ AVL树插入新节点后的四种调整情况梳理介绍

    C++ AVL树插入新节点后的四种调整情况梳理介绍

    AVL树是高度平衡的而二叉树,它的特点是AVL树中任何节点的两个子树的高度最大差别为1,本文主要给大家介绍了C++如何实现AVL树,需要的朋友可以参考下
    2022-08-08
  • C++11中条件标量和互斥锁应用出现死锁问题

    C++11中条件标量和互斥锁应用出现死锁问题

    这篇文章主要介绍了C++11中条件标量和互斥锁应用出现死锁思考,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-06-06
  • Opencv下载和导入Visual studio2022的实现步骤

    Opencv下载和导入Visual studio2022的实现步骤

    本文主要介绍了Opencv下载和导入Visual studio2022的实现步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-05-05
  • C++11中的可变参数模板/lambda表达式

    C++11中的可变参数模板/lambda表达式

    C++11的新特性可变参数模板能够让我们创建可以接受可变参数的函数模板和类模板,相比C++98和C++03,类模板和函数模板中只能含固定数量的模板参数,可变参数模板无疑是一个巨大的改进,这篇文章主要介绍了C++11中的可变参数模板/lambda表达式,需要的朋友可以参考下
    2023-03-03
  • C++中名称空间namespace的使用方法示例

    C++中名称空间namespace的使用方法示例

    namespace中文意思是命名空间或者叫名字空间,下面这篇文章主要给大家介绍了关于C++中名称空间namespace使用的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起看看吧。
    2017-12-12
  • C++中std::optional的使用指南分享

    C++中std::optional的使用指南分享

    C++ 17 引入了std::optional,表示一个可能有值的对象,这篇文章主要来和大家聊聊std::optional的使用,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一下
    2023-06-06

最新评论