详解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 函数呢?因为小红如果用函数指针作为参数的时候,不仅可以存储小明的联系方式,还可以存储小军的联系方式,这样的话我这边的代码就不用修改了,你只需要传入不同的参数就行了,因此这样的设计代码重用性很高,灵活性很大。

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

回调的顺序是:

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

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

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

定义学生结构体:

//定义个结构体 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);
}
(0)

相关推荐

  • 深入学习C语言中的函数指针和左右法则

    通常的函数调用     一个通常的函数调用的例子: //自行包含头文件 void MyFun(int x); //此处的申明也可写成:void MyFun( int ); int main(int argc, char* argv[]) { MyFun(10); //这里是调用MyFun(10);函数 return 0; } void MyFun(int x) //这里定义一个MyFun函数 { printf("%d\n",x); } 这个MyFun函数是一个无返回值的函数,它并不完成

  • 详解C语言结构体中的函数指针

    结构体是由一系列具有相同类型或不同类型的数据构成的数据集合.所以,标准C中的结构体是不允许包含成员函数的,当然C++中的结构体对此进行了扩展.那么,我们在C语言的结构体中,只能通过定义函数指针的方式,用函数指针指向相应函数,以此达到调用函数的目的. 函数指针 函数类型 (*指针变量名)(形参列表):第一个括号一定不能少. "函数类型"说明函数的返回类型,由于"()"的优先级高于"*",所以指针变量名外的括号必不可少.  注意指针函数与函数指针表示

  • c语言:基于函数指针的两个示例分析

    第一个:------------------------------------------------------ 复制代码 代码如下: #include <stdio.h>#include <string.h>void tell_me(int f(const char *, const char *));int main(void){   tell_me(strcmp);   tell_me(main);   return 0;}void tell_me(int f(const

  • C语言中的函数指针基础学习教程

    顾名思义,函数指针就是函数的指针.它是一个指针,指向一个函数.看例子: A) char * (*fun1)(char * p1,char * p2); B) char * *fun2(char * p1,char * p2); C) char * fun3(char * p1,char * p2); 看看上面三个表达式分别是什么意思? C)这很容易,fun3是函数名,p1,p2是参数,其类型为char *型,函数的返回值为char *类型. B) 也很简单,与C)表达式相比,唯一不同的就是函数的

  • 深入解析C语言中函数指针的定义与使用

    1.函数指针的定义     函数是由执行语句组成的指令序列或者代码,这些代码的有序集合根据其大小被分配到一定的内存空间中,这一片内存空间的起始地址就成为函数的地址,不同的函数有不同的函数地址,编译器通过函数名来索引函数的入口地址,为了方便操作类型属性相同的函数,c/c++引入了函数指针,函数指针就是指向代码入口地址的指针,是指向函数的指针变量. 因而"函数指针"本身首先应该是指针变量,只不过该指针变量指向函数.这正如用指针变量可指向整形变量.字符型.数组一样,这里是指向函数.C在编译时

  • C++中函数指针详解及代码分享

    函数指针 函数存放在内存的代码区域内,它们同样有地址.如果我们有一个int test(int a)的函数,那么,它的地址就是函数的名字,如同数组的名字就是数组的起始地址. 1.函数指针的定义方式:data_types (*func_pointer)( data_types arg1, data_types arg2, ...,data_types argn); c语言函数指针的定义形式:返回类型 (*函数指针名称)(参数类型,参数类型,参数类型,-); c++函数指针的定义形式:返回类型 (类名

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

    函数指针: 就是存储函数地址的指针,就是指向函数的指针,就是指针存储的值是函数地址,我们可以通过指针可以调用函数. 我们先来定义一个简单的函数: //定义这样一个函数 void easyFunc() { printf("I'm a easy Function\n"); } //声明一个函数 void easyFunc(); //调用函数 easyFunc(); //定义这样一个函数 void easyFunc() { printf("I'm a easy Function\n

  • 详解C语言编程中预处理器的用法

    预处理最大的标志便是大写,虽然这不是标准,但请你在使用的时候大写,为了自己,也为了后人. 预处理器在一般看来,用得最多的还是宏,这里总结一下预处理器的用法. #include <stdio.h> #define MACRO_OF_MINE #ifdef MACRO_OF_MINE #else #endif 上述五个预处理是最常看见的,第一个代表着包含一个头文件,可以理解为没有它很多功能都无法使用,例如C语言并没有把输入输入纳入标准当中,而是使用库函数来提供,所以只有包含了stdio.h这个头文

  • 详解C 语言项目中.h文件和.c文件的关系

    详解C 语言项目中.h文件和.c文件的关系 在编译器只认识.c(.cpp))文件,而不知道.h是何物的年代,那时的人们写了很多的.c(.cpp)文件,渐渐地,人们发现在很多.c(.cpp)文件中的声明语句就是相同的,但他们却不得不一个字一个字地重复地将这些内容敲入每个.c(.cpp)文件.但更为恐怖的是,当其中一个声明有变更时,就需要检查所有的.c(.cpp)文件. 于是人们将重复的部分提取出来,放在一个新文件里,然后在需要的.c(.cpp)文件中敲入#include XXXX这样的语句.这样即

  • 详解C++模板编程中typename用法

    typename的常规用法 typename在C++类模板或者函数模板中经常使用的关键字,此时作用和class相同,只是定义模板参数:在下面的例子中,该函数实现泛型交换数据,即交换两个数据的内容,数据的类型由_Tp决定. template <typename _Tp> inline void swap(_Tp& __a, _Tp& __b) { _Tp __tmp = __a; __a = __b; __b = __tmp; } typename的第二个用法:修饰类型 限定名和

  • 详解C语言内核中的链表与结构体

    Windows内核中是无法使用vector容器等数据结构的,当我们需要保存一个结构体数组时,就需要使用内核中提供的专用链表结构LIST_ENTRY通过一些列链表操作函数对结构体进行装入弹出等操作,如下代码是本人总结的内核中使用链表存储多个结构体的通用案例. 首先实现一个枚举用户进程功能,将枚举到的进程存储到链表结构体内. #include <ntifs.h> #include <windef.h> extern PVOID PsGetProcessPeb(_In_ PEPROCES

  • 详解C语言内核中的自旋锁结构

    提到自旋锁那就必须要说链表,在上一篇<驱动开发:内核中的链表与结构体>文章中简单实用链表结构来存储进程信息列表,相信读者应该已经理解了内核链表的基本使用,本篇文章将讲解自旋锁的简单应用,自旋锁是为了解决内核链表读写时存在线程同步问题,解决多线程同步问题必须要用锁,通常使用自旋锁,自旋锁是内核中提供的一种高IRQL锁,用同步以及独占的方式访问某个资源. 首先以简单的链表为案例,链表主要分为单向链表与双向链表,单向链表的链表节点中只有一个链表指针,其指向后一个链表元素,而双向链表节点中有两个链表节

  • 详解JavaScript异步编程中jQuery的promise对象的作用

    Promise, 中文可以理解为愿望,代表单个操作完成的最终结果.一个Promise拥有三种状态:分别是unfulfilled(未满足的).fulfilled(满足的).failed(失败的),fulfilled状态和failed状态都可以被监听.一个愿望可以从未满足状态变为满足或者失败状态,一旦一个愿望处于满足或者失败状态,其状态将不可再变化.这种"不可改变"的特性对于一个Promise来说非常的重要,它可以避免Promise的状态监听器修改一个Promise的状态导致别的监听器的行

  • 详解Java多线程编程中的线程同步方法

    1.多线程的同步: 1.1.同步机制: 在多线程中,可能有多个线程试图访问一个有限的资源,必须预防这种情况的发生.所以引入了同步机制:在线程使用一个资源时为其加锁,这样其他的线程便不能访问那个资源了,直到解锁后才可以访问. 1.2.共享成员变量的例子: 成员变量与局部变量: 成员变量: 如果一个变量是成员变量,那么多个线程对同一个对象的成员变量进行操作,这多个线程是共享一个成员变量的. 局部变量: 如果一个变量是局部变量,那么多个线程对同一个对象进行操作,每个线程都会有一个该局部变量的拷贝.他们

  • 详解Java设计模式编程中的策略模式

    定义:定义一组算法,将每个算法都封装起来,并且使他们之间可以互换. 类型:行为类模式 类图: 策略模式是对算法的封装,把一系列的算法分别封装到对应的类中,并且这些类实现相同的接口,相互之间可以替换.在前面说过的行为类模式中,有一种模式也是关注对算法的封装--模版方法模式,对照类图可以看到,策略模式与模版方法模式的区别仅仅是多了一个单独的封装类Context,它与模版方法模式的区别在于:在模版方法模式中,调用算法的主体在抽象的父类中,而在策略模式中,调用算法的主体则是封装到了封装类Context中

  • 详解Python核心编程中的浅拷贝与深拷贝

    一.问题引出浅拷贝 首先看下面代码的执行情况: a = [1, 2, 3] print('a = %s' % a) # a = [1, 2, 3] b = a print('b = %s' % b) # b = [1, 2, 3] a.append(4) # 对a进行修改 print('a = %s' % a) # a = [1, 2, 3, 4] print('b = %s' % b) # b = [1, 2, 3, 4] b.append(5) # 对b进行修改 print('a = %s'

随机推荐