详解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++中这个所谓的“随机”。文中的示例代码讲解详细,感兴趣的小伙伴可以了解一下
    2022-11-11
  • C语言中结构体偏移及结构体成员变量访问方式的问题讨论

    C语言中结构体偏移及结构体成员变量访问方式的问题讨论

    这篇文章主要介绍了C语言中结构体偏移及结构体成员变量访问方式的问题讨论,帮助大家理解struct的成员变量偏移,需要的朋友可以参考下
    2016-05-05
  • 适合初学者的C语言转义字符讲解

    适合初学者的C语言转义字符讲解

    转义字符是很多程序语言、数据格式和通信协议的形式文法的一部分。对于一个给定的字母表,一个转义字符的目的是开始一个字符序列,使得转义字符开头的该字符序列具有不同于该字符序列单独出现(没有转义字符开头)时的语义。因此转义字符开头的字符序列被叫做转义序列
    2022-04-04
  • C++17新特性个人总结

    C++17新特性个人总结

    这篇文章主要介绍了C++17新特性个人总结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-07-07
  • 你只用do-while来实现循环?太浪费了

    你只用do-while来实现循环?太浪费了

    这篇文章主要介绍了你只用do-while来实现循环?太浪费了,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-12-12
  • C++ 项目引入lib和dll的区别与使用实战

    C++ 项目引入lib和dll的区别与使用实战

    静态链接库与动态链接库都是共享代码的方式,本文主要介绍了C++项目引入lib和dll的区别与使用实战,具有一定的参考价值,感兴趣的可以了解一下
    2024-02-02
  •  C++模板template原理解析

     C++模板template原理解析

    这篇文章主要介绍了C++模板template原理,函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本
    2022-07-07
  • C++内存池的简单实现

    C++内存池的简单实现

    内存池是一种动态内存分配与管理技术。本文主要介绍了C++内存池的简单实现,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习吧
    2021-07-07
  • 原码, 反码与补码基础知识详细介绍

    原码, 反码与补码基础知识详细介绍

    这篇文章讲解了计算机的原码, 反码和补码. 并且进行了深入探求了为何要使用反码和补码, 以及更进一步的论证了为何可以用反码, 补码的加法计算原码的减法,需要的朋友可以参考下
    2016-12-12
  • C++11中匿名函数lambda的使用详解

    C++11中匿名函数lambda的使用详解

    我最早接触lambda的概念是在matlab中,那时候在做数值模拟的课题,lambda可以快速定义简单的函数,当时觉得好方便。任何语言都有这个功能,下面来看看C++11新引入的lambda是如何使用的吧
    2023-04-04

最新评论