
- 意 思
- 通过函数指针调用的函数
- 作 用
- 对特定的事件或条件进行响应
- 中文名
- 回调函数
- 外文名
- Callback Functions
-
2021-05-07 22:34:54
1、什么是回调函数
回调函数本质上也是普通函数,只是调用机制有所区别——首先通过传参的形式将该函数的地址传递给其他函数,然后在其他函数中通过函数指针调用该函数。在其他函数中通过函数指针调用该函数的过程称为回调,而作为被调用的该函数则被称为回调函数。有人可能会疑惑何必多此一举,直接在其他函数中调用该函数就好,为何一定要通过指针中转。2、为什么需要回调函数
这就不得不提到项目联合开发带来的必然后果——接口的兼容性问题。举个超级简单的例子:程序员小A和程序员小B联合开发一个项目,要求小A开发的函数必须为小B开发的函数提供灵活可用的接口。 假如小A小B是好朋友,他们天天在一起,而且也不考虑开发效率问题,那就串行方式一起开发好了。如下例所示,在同一个文件中,小A先负责开发Add()函数,小B后负责开发add()函数,小A需要调用小B的函数,即小A为小B提供了一个接口,通过这个接口小B可以随意通过更改add()函数,间接更改了Add()函数。 由于小A在使用小B的函数前小B还没有实现,所以他们需要先一起商量一下小B需要实现的函数名称、函数返回值、参数类型,然后小A在调用前先声明。小A在完成自己的工作后就可以休息了,然后小B就按照之前商量好的定义函数,对于函数体中具体怎么写完全由小B自由发挥了:
延续上文的故事,但现在场景变了。为了提高效率需要小A和小B并行操作,但工作类容没有变。显然他们需要在两个文件中完成各自的任务。小A同样需要声明小B的函数。对于现实中的项目开发而言,这种方式最为常见。/*test.h*/ #include<iostream> using namespace std; int add(int,int); void Add(int a, int b) { cout << add(a, b) << endl; } int add(int a, int b) { return a + b; } /*main.cpp*/ #include"test.h" void main() { Add(1, 2); }
看到这里你肯定回想,这也太简单了,完全没有啰嗦的必要,其实我只是想一步步引出为什么要有回调函数的存在。通过上面的例子我们不难发现,小A为小B提供了接口,使得小B定义的函数能够在合适的时机被调用,这里必须有一个前提是小A和小B要提前商量好小B所需实现的函数的名称、返回值、参数列表。 而现在我们延续上面的故事,但场景又变了,小A和小B完全不认识,甚至小A不知道小B要用自己的接口,那么再按照上面的思路无论对小A还是小B来说都不那么友好了。首先对于小A来说,每次调用小B定义的函数前都需要声明,很是麻烦。而对于小B来说,由于小A定义的函数函数体对于小B来说是封装看不见的,小B对于自己定义的函数如何传到小A定义的函数中去很不直观,而且小B定义的函数名称、返回值类型、参数列表必须与小A在之前做出的声明保持完全一致。 因而,回调函数闪亮登场。如下例所示,小A不再需要每次调用小B定义的函数之前都要进行声明,小A只需要提供一个函数指针来接收小B传过来的函数的地址,而不用管函数名是什么。小A用这个指针就可以直接调用小B定义的函数。小B可以自己起函数名,只是返回类型和参数列表必须和小A的声明保持一致。/*test.h*/ #pragma once #include<iostream> using namespace std; int add(int, int); void Add(int a, int b) { cout << add(a, b) << endl; } /*test.cpp*/ #include"test.h" int add(int a, int b) { return a + b; } void main() { Add(1, 2); }
上面例子可以看出,回调函数必须通过函数指针进行传递和调用,为了简化代码,一般会将函数指针起个别名,格式为:/*test.h*/ #pragma once #include<iostream> using namespace std; void Add(int(*callbackfun)(int, int), int a, int b) { cout << callbackfun(a, b) << endl; } /*test.cpp*/ #include"test.h" int add(int a, int b) { return a + b; } void main() { Add(add, 1, 2); }
typedef 返回值类型 (*指针名) (参数列表) 则上面的例子部分代码可以修改如下。无论是pf(a,b)还是(*pf)(a,b)都是可以的,我打印了pf和*pf,发现它们都表示的是同一函数的地址。typedef int(*callbackfun)(int, int); void Add(callbackfun pf, int a, int b) { cout<<pf<<endl; cout<<*pf<<endl; cout << (*pf)(a, b) << endl; cout << pf(a, b) << endl; }
回调函数规避了必须在调用函数前声明的弊端,而且能够让用户直观地感受到自己定义的函数被调用。小A需要在声明函数指针时规定参数列表,小B在定义回调函数时需要与小A声明的函数指针保持相同的参数列表。当然小B可以自己决定在函数体中是否使用这些参数,前提是小A会提前为回调函数形参设置默认值。/*test.h*/ #pragma once #include<iostream> using namespace std; typedef int(*callbackfun)(int, int,int); void Add(callbackfun pf, int a=0, int b=0,int c=0) { cout << pf(a, b,c) << endl; } /*test.cpp*/ #include"test.h" int add(int a, int b,int c) { return a + b; } void main() { Add(add, 1, 2); }
3、有哪些函数可以做回调函数
可以做回调函数的函数在C++中目前我遇到过的有两种,第一种是C语言风格的函数,第二种是静态成员函数。第一种上面已经详细介绍过了,就不再赘述。第二种我在此做详细说明。3.1静态成员函数做回调函数
众所周知,类的非静态成员函数的参数列表中隐含了一个this指针,当用类的对象访问类的非静态成员函数时,C++编译器会将this指针指向该对象,从而保证了函数体中操纵的成员变量是该对象的成员变量。即使你没有写上this指针,编译器在编译的时候自动加上this指针。 而在调用回调函数的函数中,它会提供接受回调函数地址的函数指针,该函数指针严格规定了参数列表。由于this指针的存在,非静态成员函数的参数列表和函数指针的参数列表参数个数无法匹配。如下例所示,函数指针callbackfun有两个参数,非静态成员函数隐含this指针从而有三个参数,显然不匹配。
当然如果你执着于使用非静态成员函数,就只能通过普通全局函数做中转,如下例所示。虽然能够实现,但看起来总是傻傻的。class AddClass { private: int a, b; public: int add(int aa, int bb); }; int AddClass::add(int aa, int bb) { a = aa; b = bb; return a + b; } void Add(int(*callbackfun)(int, int), int a, int b) { cout << callbackfun(a, b) << endl; } void main() { Add(AddClass().add, 1, 2); }
类的静态成员函数属于类,为所有对象所共享,它没有this指针,因此这里我们采用静态成员函数作为回调函数。但这里我们又遇到了一个问题,就是静态成员函数是无法访问类的非静态成员的,因此如果用户有访问非静态成员的需求,那么需要在静态成员函数的参数列表中做一点小小的修改。在形参列表中加入万能指针void*作为一个形参,然后在函数体中再进行类型强转。class AddClass { private: int a, b; public: int add(int aa, int bb); }; int AddClass::add(int aa, int bb) { a = aa; b = bb; return a + b; } void Add(int(*callbackfun)(int, int), int a, int b) { cout << callbackfun(a, b) << endl; } int myAdd(int aa, int bb) { AddClass a; return a.add(aa,bb); } void main() { Add(myAdd, 1, 2); }
/*test.h*/ #include<iostream> using namespace std; typedef int(*callbackfun)(int, int, void*); void Add(callbackfun pf, int a, int b,void* p) { cout << pf(a, b,p) << endl; } /*test.cpp*/ #include<test.h> class AddClass { private: int a, b; public: static int add(int aa, int bb,void* temp); }; int AddClass::add(int aa, int bb,void* p) { AddClass* temp = (AddClass*)p; if (temp) { temp->a = aa; temp->b = bb; } return temp->a + temp->b; } void main() { AddClass* a = new AddClass; Add(AddClass::add, 1, 2,a); }
4、小结
①回调函数本质其实是对函数指针的一种应用,上面的例子都比较简单,还没有完全体现回调函数的威力。 ②回调函数是一种设计系统的思想,能够解决系统架构中的部分问题,但是系统中不能过多使用回调函数,因为回调函数会改变整个系统的运行轨迹和执行顺序,耗费资源,而且会使得代码晦涩难懂。 ③C++ STL中有大量使用回调函数的例子。如下例所示,遍历函数for_each()中的lamda表达式就是一个回调函数,当然我们也可以像上面那样定义全局函数或静态成员函数然后将地址传进去。
最后,我只是站在应用者的角度提出了我对于回调函数的一点浅显的见解,可能还有许多更高端更系统的观点,如果有能指出我的不足,将不胜感激。#include<iostream> #include<vector> #include<algorithm> using namespace std; void main() { vector<int> v{ 1,4,6,5,3 }; for_each(v.begin(), v.end(), [=](int val) { cout << val << " "; }); }
更多相关内容 -
C语言 —— 回调函数
2021-07-07 14:56:33void String(void* param) { char* str; str = (char*)param; printf("String:%s\n",str); } 该函数的形参表示可以接收任何类型指针 void *可以接受任何类型的指针什么是回调函数
回调并不是“你我”两方的互动,而是ABC的三方联动。A调用B,B在处理完之后调用C。
举个例子来说明回调函数:
打个比方,有一家旅馆提供叫醒服务,旅客可以自己决定叫醒方法,比如让客房打电话,或者敲门。
- 首先在主程序中调用库函数,主函数就是旅客,库函数就是旅店提供的叫醒服务
- 然后把回调函数指针传入库函数中
- 回调函数是我们自己写,就是我决定的叫醒方法后的工作。
- 比如旅客第一天告诉旅店使用打电话叫醒服务,并且自定义接到电话的回调函数就是接到电话后再继续睡一会儿。那么宾馆会在第二天进行回调旅客定义打电话的方法,旅客选择在回调函数中进行处理接到电话后要继续睡觉。
- 旅客第三天告诉旅店使用敲门方式叫醒。并且敲门的自定义回调函数为立即起床。那么宾馆会在第二天调用敲门的回调函数,旅客在敲门回调函数被调用后立即起床。
上面的例子说明了回调的两个作用,一个是延迟回调,也就是我告诉宾馆第二天叫我,我就可以干我自己的事儿了。另一个优势是选择方式,就是我可以在回调函数中定义不同的处理方式,敲门,和打电话的处理方式就不同。
回调函数作用
https://www.runoob.com/w3cnote/c-callback-function.html
1. 实现多态(解耦)
由于c语言没有面向对象的概念,所以使用回调来实现多态。也就是多态的作用其实就是解耦。
解耦,很多朋友可能会想,为什么不像普通函数调用那样,在回调的地方直接写函数的名字呢?这样不也可以吗?为什么非得用回调函数呢?
有这个想法很好,因为在网上看到解析回调函数的很多例子,其实完全可以用普通函数调用来实现的。要回答这个问题,我们先来了解一下回到函数的好处和作用,那就是解耦,对,就是这么简单的答案,就是因为这个特点,普通函数代替不了回调函数。所以,在我眼里,这才是回调函数最大的特点。来看看维基百科上面我觉得画得很好的一张图片。
#include<stdio.h> #include<softwareLib.h> // 包含Library Function所在读得Software library库的头文件 int Callback() // Callback Function { // TODO return 0; } int main() // Main program { // TODO Library(Callback); // TODO return 0; }
乍一看,回调似乎只是函数间的调用,和普通函数调用没啥区别,但仔细一看,可以发现两者之间的一个关键的不同:在回调中,主程序把回调函数像参数一样传入库函数。这样一来,只要我们改变传进库函数的参数,就可以实现不同的功能,这样有没有觉得很灵活?并且丝毫不需要修改库函数的实现,这就是解耦。再仔细看看,主函数和回调函数是在同一层的,而库函数在另外一层,想一想,如果库函数对我们不可见,我们修改不了库函数的实现,也就是说不能通过修改库函数让库函数调用普通函数那样实现,那我们就只能通过传入不同的回调函数了,这也就是在日常工作中常见的情况。现在再把main()、Library()和Callback()函数套回前面 F1、F2和F3函数里面,是不是就更明白了?
明白了回调函数的特点,是不是也可以大概知道它应该在什么情况下使用了?没错,你可以在很多地方使用回调函数来代替普通的函数调用,但是在我看来,如果需要降低耦合度的时候,更应该使用回调函数。
2. 延迟异步调用
这个应该是回调函数的最大重要的作用。
项目中使用的都是都是异步,我觉得最大的用处就是异步,延迟回调。
当一个操作非常耗时,而且可能出现崩溃的情况下,如果我们还使用同步回调,那么我们就要一直等这个操作执行完成才会回调。而这时我们可以使用异步回调函数。主函数调用这个耗时函数之后会继续往下走,而不会被阻塞住。这个耗时操作就自己去执行,等执行完成之后会调用回调函数,这样不影响主函数的其他操作。
异步的功能是底层提供给上层的, 我们自己写的回调函数,但是比如上面的例子中,是同步回调。
下面两幅图展示了同步回调与异步回调的区别:同步回调
异步回调
回调函数的使用
函数指针
https://www.cnblogs.com/zhengjiafa/p/5796283.html
作用:因为c语言中只有struct而struct不像c++中的class,他是没有成员函数的,所以使用函数指针,可以将函数作为成员函数
函数指针有两个用途:调用函数、做函数的参数(主要用途)
函数指针实例:#include <stdio.h> int Func(int x); /*声明一个函数*/ int (*p) (int x); /*定义一个函数指针*/ //函数定义 int Func(int x) { printf("asdf"); return 1; } char (*PTRFUN1)(int);//定义一个函数指针 char glFun(int a) { printf("out "); return 'd'; } int main() { p = Func; /*将Func函数的首地址赋给指针变量p*/ (*p)(2); //调用函数方式一 PTRFUN1 = glFun; //将glFun函数首地址给指针变量 PTRFUN1(2); //调用函数方式二 }
该实例并没有体现出函数指针的好处,反而使用起来更复杂。
但是通过下面的回调函数,我们就能看到函数指针的真正威力了,将函数作为参数传入另一个函数。回调函数示例代码
异步回调最本质上就是事件驱动编程。
特性:使用函数的指针最重要的作用就是作为函数的参数。这在回调函数中完美体现。
正常情况下我们无法将一个函数传入另一个函数中。但是使用函数指针,我们可以将一个函数传入另一个函数中,这个特性在回调函数中使用,体现的淋漓尽致**。//文件B.c //形参使用函数指针,传入的是函数的首地址,这样我们就可以实现将函数传入另一个函数中了 //这里形参是一个函数指针,他可以接收其他函数传入。 //回调类型和库函数名称类似 void Handle(int y, int (*HandleCallbackT)(int)) { printf("Entering Handle Function. "); printf("\r\n"); HandleCallbackT(y); //调用回调函数 //Callback; //调用回调函数 printf("Leaving Handle Function. "); printf("\r\n"); } //文件A.c int Callback_1(int x) // Callback Function 1 { printf("Hello, this is Callback_1: x = %d ", x); printf("\r\n"); return 0; } int Callback_2(int x) // Callback Function 2 { printf("Hello, this is Callback_2: x = %d ", x); printf("\r\n"); return 0; } int Callback_3(int x) // Callback Function 3 { printf("Hello, this is Callback_3: x = %d ", x); printf("\r\n"); return 0; } //文件A.c //callBack调用函数,调用函数和回调函数名称类似,因为他们是同一个文件,同一级 void CallBack() { int a = 2; int b = 4; int c = 6; //调用库函数,将回调函数传入,会在库函数中走回调函数 //在哪里调用这个库函数,就在哪里写回调函数定义。 Handle(a, Callback_1);//将callback1传入 Handle(b, Callback_2); Handle(c, Callback_3); } //test测试 int main() { printf("Entering Main Function. "); printf("\r\n"); CallBack(); //这里体现出优势,为何使用回调,其实就是为了解耦,我们可以将不同的回调函数传入handle //并且不需要改变handle函数,而我们可以传入不同的函数参数 printf("Leaving Main Function. "); printf("\r\n"); return 0; }
最重要的是要想清楚,在Handle函数调用之后,handle函数会调用回调函数,我们这时应该做什么,应该如何处理。
-
c语言回调函数的使用及实际作用详解
2021-07-16 23:49:20回调函数这个知识点其实并不是很难,难是难在网上很多讲解回调函数的都说的太学术化了化了,一点也不亲民。 很多人即使知道怎么写回调函数也根本就搞不懂它们在实际产品中也有什么用,什么时候用。 所以这节课呢...大家好,我是无际。
今天给大家讲一下芯片/模块厂家写SDK必须会使用的一种技术:回调函数。
回调函数这个知识点其实并不是很难,难是难在网上很多讲解回调函数的都说的太学术化了化了,一点也不亲民。
很多人即使知道怎么写回调函数也根本就搞不懂它们在实际产品中也有什么用,什么时候用。
所以这节课呢我们会以程序架构的需求为出发点,讲解回调函数是怎么满足它这个需求的。
为了方便大家理解,这篇内容也对应有一篇文章,大家可以找无际单片机编程获取。
一、通过这节课程你能掌握以下知识:
- 掌握程序架构的核心理念或需求。
- 掌握回调函数的作用
- 掌握回调函数的程序编写
- 掌握回调函数在产品中的应用
二、程序架构的核心理念和需求
很多人可能会说一个好的程序架构啊,就是代码很紧凑、执行效率也很高。
其实这个说的很片面,不完全对,这只能说明你程序算法写的好,但架构不一定做的好。
即然是架构,那自然是以从”大局”为重,思维不能局限于当下的产品功能,还要考虑到以后功能的增加和裁剪,那么对于单片机开发来说,我认为一个好的程序架构至少要达到以下要求:
硬件层和应用层的程序代码分开,相互之间的控制和通讯使用接口,而且不会共享的全局变量或数组。
这里呢,我就这个要求,别小看这一个要求,因为这个要求里面蕴藏着很多学问的,比如用专业称为可移植性、可扩展性。
那么我们来想象一下我们通常写单片机代码的方式啊,在51的时候基本一个.c文件解决,包括寄存器配置啊,产品功能啊。
这种就是没有架构的程序,然后我们进化到STM32这个单片机以后,程序大了,慢慢也会在工程文件里加几个文件夹目录把硬件层和应用层代码分开了。
于是我们会把一些不同的外设功能,比如Led、按键、串口等外设功能代码分别写在不同的.c文件里,然后统一用函数接口去调用它。
比方说控制一个LED灯亮,直接在led.c文件里写一个驱动led灯状态的函数然后给外部调用就好了。
那我们我们看这种Led的控制函数确实也是满足程序架构的需求的,硬件层和应用层代码分开,应用层用硬件层提供的接口来控制,而且又不会有硬件层和应用层共享的全部变量或数组。像这种是不是很简单?
那么不知道你们有没有碰到另外一种情况,就是应用程序需要采集硬件层的数据,比如串口接收数据,按键采集、ADC值采集。
这种硬件层的数据怎么通知应用层来拿,或者怎么主动给它?
我们以往最简单粗暴的方式是不是就是用一个全局变量,比方说硬件层串口接收到数据来了,那么我们把数据丢到数组里,然后把接收完成全局变量标志位置1。
比方说全局变量名为RcvFlag,然后应用层程序会轮询判断RcvFlag==1?是的话就开始把数组里的数据取出来解析。
很多人就会说了,你看我用这种方法照样能实现功能啊,为什么还要学习别的架构。
这样做当然可以实现功能,但是会存在移植性很差的问题。
比如说你们老板让你把这个串口的硬件层封装起来给客户用,但不能让客户看到你实现的源代码,只提供接口(函数名)给对方用。
那么这时候难道你要告诉客户先判断哪个变量为1,然后再取哪个数组的数据这么LOW的做法吗?
那么如果是懂行的客户一定会怀疑你们公司的技术实力是不是小学生水平。
那怎样做才会既方便又专业呢? 这里我们就需要用到回调函数啦。
三、回调函数的作用
那么在讲回调函数之前呢,对于函数调用呢我一般分为2种类型:
1.输出型
不知道大家有没有用过C语言自带的一些库函数,比如说sizeof()获取数据长度的函数,memcpy()是内存拷贝函数,我们调用这个函数之后呢就能完成相应的功能。
还有我们基于单片机的一些程序函数,比方说控制LED点亮熄灭、继电器吸合断开、LCD驱动等等。
那么这些呢,我一般称为输出型的函数。
输出型函数我们是主导的角色,我们知道什么时候该调用它。
2.输入型
输入型呢,也称为的是响应式的函数。
什么叫响应式的函数呢?
比方说接收串口的数据,我们不知道什么数据什么时候来。
再比方说,我们按键检测的函数,我们不知道什么时候会按下按键,那么这些就要定义成响应式函数来实现,而响应式函数就可以用回调函数来实现。
所以通过这两个种类型的分析啊,我们就可以知道,回调函数基本是用在输入型的处理中。
比方说串口数据接收,那么数据是输入到单片机里面的,单片机是处于从机角色。
按键检测,按键状态是输入到单片机里的。
再比方说ADC值采集,ADC值也是输入到单片机里的。
那么它们输入的时间节点都是未知的,这些就能够用回调函数来处理。
具体怎么处理后面我们会用代码来给大家举例。
回调函数还有一个作用就是为了封装代码。
比如说做芯片或者模组的厂家,我们拿典型的STM32来举例,像外部中断、定时器、串口等中断函数都是属于回调函数,这种函数的目的是把采集到的数据传递给用户,或者说应用层。
所以回调函数的核心作用是:
1.把数据从一个.c文件传递到另一个.c文件,而不用全局变量共享数据这么LOW的方法。
2.对于这种数据传递方式,回调函数更利于代码的封装。
四、掌握回调函数的程序编写
前面说了很多概念性的东西,可能大家也比较难理解,回调函数最终呢是靠函数指针来实现的。
那么我这里通过一些模拟按键的例子来演示下怎么回通过调函数来处理它们。
下面是我们的c-free工程,用这个来模拟方便点:
从模块化编程的思想来看,整个工程分为2个部分,应用层main.c文件,硬件层key.c和key.h文件。
不管再怎么复杂的程序,我们都要先从main函数一步步往下挖,main函数代码如下。
int main(int argc, char *argv[]) { KeyInit(); KeyScanCBSRegister(KeyScanHandle); KeyPoll(); return 0; }
KeyInit();是key.c文件的按键初始化函数
KeyScanCBSRegister(KeyScanHandle);是key.c的函数指针注册函数。
这个函数可能大家会有点蒙,请跟进我们的节奏,下面开始烧脑环节,也是写回调函数的必须步骤,
想理解这个回调函数注册函数,我们要先从硬件层(key.h)头文件的函数指针定义说起,具体看下图。
这里自定义了一个函数指针类型,带两个形参。
然后,我们在key.c这个文件里定义了一个函数指针变量。
重点来了,我们就是通过这个函数指针,指向应用层的函数地址(函数名)。
具体怎么实现指向呢?就是通过函数指针注册函数。
这个函数是在main函数里调用,使用这种注册函数的方式注册灵活性也很高,你想要在哪个.c文件使用按键功能就在哪里调用。
这里要注意,main.c这个文件要定义一个函数来接收硬件层(key.c)过来的数据。
这里定义也不是乱定义的,一定要和那个自定义函数指针类型返回值、形参一致。
然后把这个函数名字直接复制给KeyScanCBSRegister函数的形参就可以了。
这样调用后,我们key.c文件的pKeyScanCBS这个指针其实就是指向的KeyScanHandle函数。
也就是说执行pKeyScanCBS的时候,就是执行KeyScanHandle函数。
那具体检测按键的功能就是KeyPoll函数,这个在main函数里调用。
当检测到键盘有输入以后,最终会调用pKeyScanCBS。
最终执行的是main.c文件的KeyScanHandle函数。
所以,我们来看下输出结果。
如果还是有点模糊,下面我再给大家捋一捋编写和使用回调函数的流程:
- 自定义函数指针,形参作为硬件层要传到应用层的数据。
- 硬件层定义一个函数指针和函数指针注册函数。
- 应用层定义一个函数,返回值和形参都要和函数指针一致。
- 应用层调用函数指针注册函数,把定义好的函数名称作为形参传入。
Ok,这就是回调函数的使用。
如果还看不懂建议多看两遍。
下面请大家思考一下,这个程序虽然简单,但是不是架构还不错?应用层和硬件层完全独立?
-
C语言回调函数详解(全网最全)
2021-10-31 00:10:41什么是回调函数2 为什么要用回调函数?3 怎么使用回调函数?4.下面是一个四则运算的简单回调函数例子:5. 回调函数实例(很有用) 一、函数指针 在讲回调函数之前,我们需要了解函数指针。 我们都知道,C语言的灵魂...文章目录
一、函数指针
在讲回调函数之前,我们需要了解函数指针。
我们都知道,C语言的灵魂是指针,我们经常使用整型指针,字符串指针,结构体指针等
int *p1; char *p2; STRUCT *p3; //STRUCT为我们定义的结构体
但是好像我们一般很少使用函数指针,我们一般使用函数都是直接使用函数调用。
下面我们来了解一下函数指针的概念和使用方法。
1.概念
函数指针是指向函数的指针变量。
通常我们说的指针变量是指向一个整型、字符型或数组等变量,而函数指针是指向函数。
函数指针可以像一般函数一样,用于调用函数、传递参数。
函数指针的定义方式为:
函数返回值类型 (* 指针变量名) (函数参数列表);
“函数返回值类型”表示该指针变量可以指向具有什么返回值类型的函数;“函数参数列表”表示该指针变量可以指向具有什么参数列表的函数。这个参数列表中只需要写函数的参数类型即可。
我们看到,函数指针的定义就是将“函数声明”中的“函数名”改成“(*指针变量名)”。但是这里需要注意的是:“(*指针变量名)”两端的括号不能省略,括号改变了运算符的优先级。如果省略了括号,就不是定义函数指针而是一个函数声明了,即声明了一个返回值类型为指针型的函数。
那么怎么判断一个指针变量是指向变量的指针变量还是指向函数的指针变量呢?首先看变量名前面有没有“”,如果有“”说明是指针变量;其次看变量名的后面有没有带有形参类型的圆括号,如果有就是指向函数的指针变量,即函数指针,如果没有就是指向变量的指针变量。
最后需要注意的是,指向函数的指针变量没有 ++ 和 – 运算。
一般为了方便使用,我们会选择
typedef 函数返回值类型 (* 指针变量名) (函数参数列表);
比如
typedef int (*Fun1)(int);//声明也可写成int (*Fun1)(int x),但习惯上一般不这样。 typedef int (*Fun2)(int, int);//参数为两个整型,返回值为整型 typedef void (*Fun3)(void);//无参数和返回值 typedef void* (*Fun4)(void*);//参数和返回值都为void*指针
2,如何用函数指针调用函数
给大家举一个例子:
int Func(int x); /*声明一个函数*/ int (*p) (int x); /*定义一个函数指针*/ p = Func; /*将Func函数的首地址赋给指针变量p*/ p = &Func; /*将Func函数的首地址赋给指针变量p*/
赋值时函数 Func 不带括号,也不带参数。由于函数名 Func 代表函数的首地址,因此经过赋值以后,指针变量 p 就指向函数 Func() 代码的首地址了。
下面来写一个程序,看了这个程序你们就明白函数指针怎么使用了:
#include <stdio.h> int Max(int, int); //函数声明 int main(void) { int(*p)(int, int); //定义一个函数指针 int a, b, c; p = Max; //把函数Max赋给指针变量p, 使p指向Max函数 printf("please enter a and b:"); scanf("%d%d", &a, &b); c = (*p)(a, b); //通过函数指针调用Max函数 printf("a = %d\nb = %d\nmax = %d\n", a, b, c); return 0; } int Max(int x, int y) //定义Max函数 { int z; if (x > y) { z = x; } else { z = y; } return z; }
特别注意的是,因为函数名本身就可以表示该函数地址(指针),因此在获取函数指针时,可以直接用函数名,也可以取函数的地址。
p = Max可以改成 p = &Max c = (*p)(a, b) 可以改成 c = p(a, b)
3.函数指针作为某个函数的参数
既然函数指针变量是一个变量,当然也可以作为某个函数的参数来使用的。
示例:
#include <stdio.h> #include <stdlib.h> typedef void(*FunType)(int); //前加一个typedef关键字,这样就定义一个名为FunType函数指针类型,而不是一个FunType变量。 //形式同 typedef int* PINT; void myFun(int x); void hisFun(int x); void herFun(int x); void callFun(FunType fp,int x); int main() { callFun(myFun,100);//传入函数指针常量,作为回调函数 callFun(hisFun,200); callFun(herFun,300); return 0; } void callFun(FunType fp,int x) { fp(x);//通过fp的指针执行传递进来的函数,注意fp所指的函数有一个参数 } void myFun(int x) { printf("myFun: %d\n",x); } void hisFun(int x) { printf("hisFun: %d\n",x); } void herFun(int x) { printf("herFun: %d\n",x); }
输出:
4.函数指针作为函数返回类型
有了上面的基础,要写出返回类型为函数指针的函数应该不难了,下面这个例子就是返回类型为函数指针的函数:
void (* func5(int, int, float ))(int, int) { ... }
在这里,
func5
以(int, int, float)
为参数,其返回类型为void (\*)(int, int)
。在C语言中,变量或者函数的声明也是一个大学问,想要了解更多关于声明的话题,可以参考我之前的文章 - C专家编程》读书笔记(1-3章)。这本书的第三章花了整整一章的内容来讲解如何读懂C语言的声明。5.函数指针数组
在开始讲解回调函数前,最后介绍一下函数指针数组。既然函数指针也是指针,那我们就可以用数组来存放函数指针。下面我们看一个函数指针数组的例子:
/* 方法1 */ void (*func_array_1[5])(int, int, float); /* 方法2 */ typedef void (*p_func_array)(int, int, float); p_func_array func_array_2[5];
上面两种方法都可以用来定义函数指针数组,它们定义了一个元素个数为5,类型是
void (\*)(int, int, float)
的函数指针数组。6.函数指针总结
- 函数指针常量 :Max;函数指针变量:p;
- 数名调用如果都得如(*myFun)(10)这样,那书写与读起来都是不方便和不习惯的。所以C语言的设计者们才会设计成又可允许myFun(10)这种形式地调用(这样方便多了,并与数学中的函数形式一样)。
- 函数指针变量也可以存入一个数组内。数组的声明方法:int (*fArray[10]) ( int );
二、回调函数
1.什么是回调函数
我们先来看看百度百科是如何定义回调函数的:
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
这段话比较长,也比较绕口。下面我通过一幅图来说明什么是回调:
假设我们要使用一个排序函数来对数组进行排序,那么在主程序(Main program)中,我们先通过库,选择一个库排序函数(Library function)。但排序算法有很多,有冒泡排序,选择排序,快速排序,归并排序。同时,我们也可能需要对特殊的对象进行排序,比如特定的结构体等。库函数会根据我们的需要选择一种排序算法,然后调用实现该算法的函数来完成排序工作。这个被调用的排序函数就是回调函数(Callback function)。
结合这幅图和上面对回调函数的解释,我们可以发现,要实现回调函数,最关键的一点就是要将函数的指针传递给一个函数(上图中是库函数),然后这个函数就可以通过这个指针来调用回调函数了。注意,回调函数并不是C语言特有的,几乎任何语言都有回调函数。在C语言中,我们通过使用函数指针来实现回调函数。
我的理解是:把一段可执行的代码像参数传递那样传给其他代码,而这段代码会在某个时刻被调用执行,这就叫做
回调
。如果代码立即被执行就称为
同步回调
,如果过后再执行,则称之为异步回调
。回调函数
就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
2 为什么要用回调函数?
因为可以把调用者与被调用者分开,所以调用者不关心谁是被调用者。它只需知道存在一个具有特定原型和限制条件的被调用函数。
简而言之,回调函数就是允许用户把需要调用的方法的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。
int Callback() ///< 回调函数 { // TODO return 0; } int main() ///< 主函数 { // TODO Library(Callback); ///< 库函数通过函数指针进行回调 // TODO return 0; }
回调似乎只是函数间的调用,和普通函数调用没啥区别。
但仔细看,可以发现两者之间的一个关键的不同:在回调中,主程序把回调函数像参数一样传入库函数。
这样一来,只要我们改变传进库函数的参数,就可以实现不同的功能,这样有没有觉得很灵活?并且当库函数很复杂或者不可见的时候利用回调函数就显得十分优秀。
3 怎么使用回调函数?
int Callback_1(int a) ///< 回调函数1 { printf("Hello, this is Callback_1: a = %d ", a); return 0; } int Callback_2(int b) ///< 回调函数2 { printf("Hello, this is Callback_2: b = %d ", b); return 0; } int Callback_3(int c) ///< 回调函数3 { printf("Hello, this is Callback_3: c = %d ", c); return 0; } int Handle(int x, int (*Callback)(int)) ///< 注意这里用到的函数指针定义 { Callback(x); } int main() { Handle(4, Callback_1); Handle(5, Callback_2); Handle(6, Callback_3); return 0; }
如上述代码:可以看到,
Handle()
函数里面的参数是一个指针,在main()
函数里调用Handle()
函数的时候,给它传入了函数Callback_1()/Callback_2()/Callback_3()
的函数名,这时候的函数名就是对应函数的指针,也就是说,回调函数其实就是函数指针的一种用法。4.下面是一个四则运算的简单回调函数例子:
#include <stdio.h> #include <stdlib.h> /**************************************** * 函数指针结构体 ***************************************/ typedef struct _OP { float (*p_add)(float, float); float (*p_sub)(float, float); float (*p_mul)(float, float); float (*p_div)(float, float); } OP; /**************************************** * 加减乘除函数 ***************************************/ float ADD(float a, float b) { return a + b; } float SUB(float a, float b) { return a - b; } float MUL(float a, float b) { return a * b; } float DIV(float a, float b) { return a / b; } /**************************************** * 初始化函数指针 ***************************************/ void init_op(OP *op) { op->p_add = ADD; op->p_sub = SUB; op->p_mul = &MUL; op->p_div = &DIV; } /**************************************** * 库函数 ***************************************/ float add_sub_mul_div(float a, float b, float (*op_func)(float, float)) { return (*op_func)(a, b); } int main(int argc, char *argv[]) { OP *op = (OP *)malloc(sizeof(OP)); init_op(op); /* 直接使用函数指针调用函数 */ printf("ADD = %f, SUB = %f, MUL = %f, DIV = %f\n", (op->p_add)(1.3, 2.2), (*op->p_sub)(1.3, 2.2), (op->p_mul)(1.3, 2.2), (*op->p_div)(1.3, 2.2)); /* 调用回调函数 */ printf("ADD = %f, SUB = %f, MUL = %f, DIV = %f\n", add_sub_mul_div(1.3, 2.2, ADD), add_sub_mul_div(1.3, 2.2, SUB), add_sub_mul_div(1.3, 2.2, MUL), add_sub_mul_div(1.3, 2.2, DIV)); return 0; }
5. 回调函数实例(很有用)
一个
GPRS
模块联网的小项目,使用过的同学大概知道2G、4G、NB
等模块要想实现无线联网功能都需要经历模块上电初始化、注册网络、查询网络信息质量、连接服务器等步骤,这里的的例子就是,利用一个状态机函数(根据不同状态依次调用不同实现方法的函数),通过回调函数的方式依次调用不同的函数,实现模块联网功能,如下:/********* 工作状态处理 *********/ typedef struct { uint8_t mStatus; uint8_t (* Funtion)(void); //函数指针的形式 } M26_WorkStatus_TypeDef; //M26的工作状态集合调用函数 /********************************************** ** >M26工作状态集合函数 ***********************************************/ M26_WorkStatus_TypeDef M26_WorkStatus_Tab[] = { {GPRS_NETWORK_CLOSE, M26_PWRKEY_Off }, //模块关机 {GPRS_NETWORK_OPEN, M26_PWRKEY_On }, //模块开机 {GPRS_NETWORK_Start, M26_Work_Init }, //管脚初始化 {GPRS_NETWORK_CONF, M26_NET_Config }, /AT指令配置 {GPRS_NETWORK_LINK_CTC, M26_LINK_CTC }, //连接调度中心 {GPRS_NETWORK_WAIT_CTC, M26_WAIT_CTC }, //等待调度中心回复 {GPRS_NETWORK_LINK_FEM, M26_LINK_FEM }, //连接前置机 {GPRS_NETWORK_WAIT_FEM, M26_WAIT_FEM }, //等待前置机回复 {GPRS_NETWORK_COMM, M26_COMM }, //正常工作 {GPRS_NETWORK_WAIT_Sig, M26_WAIT_Sig }, //等待信号回复 {GPRS_NETWORK_GetSignal, M26_GetSignal }, //获取信号值 {GPRS_NETWORK_RESTART, M26_RESET }, //模块重启 } /********************************************** ** >M26模块工作状态机,依次调用里面的12个函数 ***********************************************/ uint8_t M26_WorkStatus_Call(uint8_t Start) { uint8_t i = 0; for(i = 0; i < 12; i++) { if(Start == M26_WorkStatus_Tab[i].mStatus) { return M26_WorkStatus_Tab[i].Funtion(); } } return 0; }
所以,如果有人想做个
NB
模块联网项目,可以copy
上面的框架,只需要修改回调函数内部的具体实现,或者增加、减少回调函数,就可以很简洁快速的实现模块联网。 -
回调函数
2019-09-09 17:36:28回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定... -
深入理解:回调函数
2019-06-21 15:36:09关于回调函数到底是什么,已经困扰了我很久了~ 在知乎上看到几位大神的帖子,才恍然大悟 作者:no.body 链接:https://www.zhihu.com/question/19801131/answer/27459821 来源:知乎 作者:常溪玲 链接... -
C++学习之回调函数
2021-07-28 11:42:041. 什么是回调函数 回调函数就是通过函数指针调用函数。如果把函数的指针或者地址作为参数传递给另一个参数,当这个指针被用来调用其所指向的函数时,那么这就是一个回调的过程,这个被回调的函数就是回调函数。回调... -
回调函数Callback
2021-11-21 18:18:36回调函数精讲 -
什么是回调函数?为什么要使用回调函数?如何使用回调函数?
2020-03-17 23:07:451. 回调函数: 函数指针的调用,即是一个通过函数指针调用的函数; 如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,就说这是回调函数。 In computer programming, ... -
【C语言】回调函数
2022-03-13 15:09:20回调函数 文章目录 前言 一、pandas是什么? 二、使用步骤 1.引入库 2.读入数据 总结 前言 随着我们对C语言的学习以及对指针更加深入的了解,我们避免不了接触到回调函数,以下是关于回调函数的... -
Java回调函数详解
2022-02-08 21:29:19什么是回调函数(CallBack) 在编写程序时,有时候会调用许多API中实现实现的函数,但某些方法需要我们传入一个方法,以便在需要的时候调用我们传入进去的函数。这个被传入的函数称为回调函数(Callback function)... -
JS中的 回调函数(callback)
2022-07-12 21:24:18什么是回调函数(callback)呢?2.回调函数有哪些特点?3.回调函数中this的指向问题4.为什么要用到回调函数?5.回调函数和异步操作的关系是什么?回调函数是异步么?把函数当作一个参数传到另外一个函数中,当需要用... -
阻塞式回调函数和延迟式回调函数
2020-04-30 18:16:57首先,有三种函数: 起始函数:大致可以等同于主函数 中间函数:中间函数把回调函数作为参数传递执行 ...在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做 触... -
C++回调函数(callback)的使用
2021-05-21 15:38:52什么是回调函数(callback)模块A有一个函数foo,它向模块B传递foo的地址,然后在B里面发生某种事件(event)时,通过从A里面传递过来的foo的地址调用foo,通知A发生了什么事情,让A作出相应反应。那么我们就把foo称为回... -
C语言如何实现回调函数
2021-05-20 06:39:57回调函数就是就是通过一个统一的接口实现不同的功能,C语言中的回调函数就是在代码中根据所传的参数不同而调用不同的回调函数回调函数是一个程序员不能显式调用的函数;通过将回调函数的地址传给调用者从而实现调用... -
javascript回调函数详解
2021-07-19 14:07:00<script type="text/javascript"> function add(a, b, fun) { let sum = a + b; fun(sum); } function log(sum) { document.write("我是... //方法2: 内嵌函数方法: add(2, 3, function (su. -
C语言中函数指针和回调函数的详解
2019-05-18 23:10:01函数指针:指向函数的指针变量。 因此“函数指针”本身首先应是指针变量,只不过该指针变量指向函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。如前所述,C在编译时,每一个函数都有一个... -
回调函数介绍和使用方法以及使用例子
2020-12-26 11:21:53回调函数在日常开发中使用广泛,什么是回调函数,为什么要使用回调函数,如何使用回调函数,本文将详细并结合一个例子说明。 什么是回调函数? 回调函数也是函数,通俗的来说回调函数是由开发者A定义,另个... -
vue 函数(二):callback回调函数
2021-09-09 15:17:33一、介绍:回调,顾名思义,回头再调。 -
Java回调函数
2021-10-24 23:41:58在 main 函数中,我们异步发送一个请求,并且指定处理响应的回调函数,接着 main 函数去做其他事,而当响应到达后,执行回调函数。 当我们想要换一种“处理响应”的方法时,将必须去修改 CallBack 类的 ... -
JS中什么是回调函数?
2022-03-21 07:25:37对于刚学JS的初学者来说(包括我现在的自己),对于这个回调函数真的是踩坑无数,于是乎想作为一个淋过雨的人,想为后面刚入门的人打一把伞。 本文不会用专业的知识词汇,只会用口语来简单让你有一个概念帮你浅浅的... -
VC++ DLL 实现回调函数的实例
2011-12-28 19:35:28VC 程序传函数到DLL里实现回调函数的实例。一个简单的例子。可以举一反三。 -
CANoe DLL编程(三)—— CANoe创建的DLL中的回调函数
2021-11-04 08:54:51CANoe创建DLL中的回调函数 -
什么是回调函数?
2022-04-12 18:31:07回调就是一种利用函数指针进行函数调用的过程。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,就说这是回调函数。 -
Java实现的回调函数
2021-02-12 21:25:16一个回调函数的例子,首先是我为了完成在工厂工作的任务,但是我不能一直在工厂工作,我必须知道工作完以后,就要下班回家。所以做了一个回调函数,通知我,今天工作做完了,可以下班回家啦。具体可以看代码的实现... -
详解C语言中回调函数的含义与使用场景[2]
2022-03-27 17:52:55引言:在上一篇详解C语言中回调函数的含义与使用场景[1]中介绍了回调函数的概念与使用方法,本节将深入地介绍回调函数典型的使用场景。通过使用回调函数可以实现驱动和应用程序的分离解耦,让程序更加地灵活。也可以... -
详解C语言中回调函数的含义与使用场景[1]
2022-03-27 16:06:34详解C语言中回调函数的含义与使用场景 引言:回调函数是 C 语言中 函数指针 的一种用法,在一个函数A中通过函数指针调用另一个函数B的过程称为回调(callback),通过函数指针被调用的函数B就是回调函数。回调让函数... -
C# 回调函数详解
2021-03-10 10:46:13回调和回调函数是什么 软件模块之间总是存在着一定的接口,回调是一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口。 对于一般的结构化语言,可以通过回调函数来实现回调。回调函数是一个函数... -
回调函数是异步吗?回调函数和异步操作的关系
2021-06-18 01:24:05定义:回调函数被认为是一种高级函数,一种被作为参数传递给另一个函数(在这称作"otherFunction")的高级函数,回调函数会在otherFunction内被调用(或执行)。回调函数的本质是一种模式(一种解决常见问题的模式),因此... -
Matlab GUI编程,如何在一个callback回调函数中调用另一个回调函数
2019-11-12 21:25:04一、如何在一个callback回调函数中调用另一个回调函数 网上找了好多帖子,都是在答非所问,我最终失去了耐心,自己去写个小demo碰碰运气吧,然后非常幸运,我解决了,哈哈,看下面的小案例。 图形界面非常简单,...