打印
[牛人杂谈]

回调函数

[复制链接]
856|17
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主

回调函数通俗的解释:

     普通函数:你所写的函数调用系统函数,你只管调用,不管实现。

     回调函数:系统调用你所写的函数,你只管实现,不管调用。

以下是使用C语言实现回调函数的一个例子:

#include <stdio.h>

void PrintNum(int n);
void ShowNum(int n,void (* ptr)());

void PrintMessage1();
void PrintMessage2();
void PrintMessage3();
void ShowMessage(void (* ptr)());

int main(){
   ShowNum(11111,PrintNum);
   ShowNum(22222,PrintNum);
   ShowMessage(PrintMessage1);
   ShowMessage(PrintMessage2);
   ShowMessage(PrintMessage3);
}

void PrintNum(int n){
   printf("Test1 is called,the number is %d\n",n);
}

void ShowNum(int n,void (* ptr)()){
   (* ptr)(n);
}


void PrintMessage1(){
   printf("This is the message 1!\n");
}

void PrintMessage2(){
   printf("This is the message 2!\n");
}

void PrintMessage3(){
   printf("This is the message 3!\n");
}

void ShowMessage(void (* ptr)()){
    (* ptr)();
}


沙发
wahahaheihei|  楼主 | 2016-4-27 22:36 | 只看该作者
运行结果:

使用特权

评论回复
板凳
heisexingqisi| | 2016-4-28 22:41 | 只看该作者
这种函数比较抽象,函数名是地址,把地址传递给系统函数来执行被调用函数

使用特权

评论回复
地板
wahahaheihei|  楼主 | 2016-4-28 22:56 | 只看该作者
对指针的应用是C语言编程的精髓所在,而回调函数就是C语言里面对函数指针的高级应用。简而言之,回调函数是一个通过函数指针调用的函数。如果你把函数指针(函数的入口地址)传递给另一个函数,当这个函数指针被用来调用它所指向的函数时,我们就说这个函数是回调函数。
  为什么要使用回调函数呢?我们先看一个小例子:
  Node * Search_List (Node * node, const int value)
  {
  while (node != NULL)
  {
  if (node -> value == value)
  {
  break;
  }
  node = node -> next;
  }
  return node;
  }
  这个函数用于在一个单向链表中查找一个指定的值,返回保存这个值的节点。它的参数是指向这个链表第一个节点的指针以及要查找的值。这个函数看上去很简单,但是我们考虑一个问题:它只能适用于值为整数的链表,如果查找一个字符串链表,我们不得不再写一个函数,其实大部分代码和现在这个函数相同,只是第二个参数的类型和比较的方法不同。

使用特权

评论回复
5
wahahaheihei|  楼主 | 2016-4-28 22:57 | 只看该作者
其实我们更希望令查找函数与类型无关,这样它就能用于查找存放任何类型值的链表了,因此必须改变比较的方式,而借助回调函数就可以达到这个目的。我们编写一个函数(回调函数),用于比较两个同类型的值,然后把一个指向这个函数的指针作为参数传递给查找函数,查找函数调用这个比较函数来执行比较,采用这个方法,任何类型的值得都可以进行比较。
  我们还必须给查找函数传递一个指向待比较的值的指针而不是值本身,也就是一个void *类型的形参,这个指针会传递给回调函数,进行最终的比较。这样的修改可以让我们传递指向任何类型的指针到查找函数,从而完成对任何类型的比较,这就是指针的好处,我们无法将字符串、数组或者结构体作为参数传递给函数,但是指向它们的指针却可以。
  现在,我们的查找函数就可以这样实现:
  NODE *Search_List(NODE *node, int (*compare)(void const *, void const *) ,
  void const *desired_value);
  {
  while (node != NULL)
  {
  if (compare((node->value_address), desired_value) == 0)
  {
  break;
  }
  node = node->next;
  }
  return node;
  }
  可以看到,用户将一个函数指针传递给查找函数,后者将回调这个函数。
  注意这里我们的链表节点是这样定义的:
  typedef struct list
  {
  void *value_address;
  struct list *next;
  }NODE;
  这样定义可以让NODE *类型的指针指向存储任何类型数据的链表节点。而value_address就是指向具体数据的指针,我们把它定义为void *,表示一个指向未知类型的指针,这样链表就可以存储任何类型的数据了,而我们传递给查找函数Search_List的第一个参数就可以统一表示为:NODE *,否则,还是要分别写查找函数以适应存储不同数据类型的链表。

使用特权

评论回复
6
wahahaheihei|  楼主 | 2016-4-28 22:58 | 只看该作者
现在,查找函数与类型无关,因为它不进行实际的比较,因此,我们必须编写针对不同类型的比较函数,这是很容易实现的,因为调用者知道链表中所包含的值的类型,如果创建几个分别包含不同类型值的链表,为每种类型编写一个比较函数就允许单个查找函数作用于所有类型的链表。
  下面是一个比较函数,用于在一个整型链表中查找:
  注意强制类型转换,比较函数的参数必须被声明为void *以匹配查找函数的原型,然后强制转换为(int *)类型用于比较整型。
  int int_compare(void const *a, void const *b)
  {
  if (*(int *)a == *(int *)b)
  {
  return 0;
  }
  else
  {
  return -1;
  }
  }
  这个函数可以这样被使用:
  desired_node = Search_List(root, int_compare, &desired_int_value);
  如果你希望在一个字符串链表中进行查找,下面的代码就可以完成任务:
  desired_node = Search_List(root, strcmp, “abcdefg”);
  正好库函数strcmp所执行的比较和我们需要的一样,不过gcc会发出警告信息:因为strcmp的参数被声明为const char *而不是void const *。
  上面的例子展示了回调函数的基本原理和用法,回调函数的应用是非常广泛的。通常,当我们想通过一个统一接口实现不同内容的时候,用回调函数来实现就非常合适。任何时候,如果你所编写的函数必须能够在不同的时刻执行不同的类型的工作或者执行只能由函数调用者定义的工作,你都可以用回调函数来实现。许多窗口系统就是使用回调函数连接多个动作,如拖拽鼠标和点击按钮来指定调用用户程序中的某个特定函数。

使用特权

评论回复
7
wahahaheihei|  楼主 | 2016-4-28 22:58 | 只看该作者
1 什么是回调
软件模块之间总是存在着一定的接口,从调用方式上,可以把他们分为三类:同步调用、回调和异步调用。同步调用是一种阻塞式调用,调用方要等待对方执行完毕才返回,它是一种单向调用;回调是一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口;异步调用是一种类似消息或事件的机制,不过它的调用方向刚好相反,接口的服务在收到某种讯息或发生某种事件时,会主动通知客户方(即调用客户方的接口)。回调和异步调用的关系非常紧密,通常我们使用回调来实现异步消息的注册,通过异步调用来实现消息的通知。同步调用是三者当中最简单的,而回调又常常是异步调用的基础,因此,下面我们着重讨论回调机制在不同软件架构中的实现。

使用特权

评论回复
8
wahahaheihei|  楼主 | 2016-4-28 22:59 | 只看该作者
对于不同类型的语言(如结构化语言和对象语言)、平台(Win32、JDK)或构架(CORBA、DCOM、WebService),客户和服务的交互除了同步方式以外,都需要具备一定的异步通知机制,让服务方(或接口提供方)在某些情况下能够主动通知客户,而回调是实现异步的一个最简捷的途径。
对于一般的结构化语言,可以通过回调函数来实现回调。回调函数也是一个函数或过程,不过它是一个由调用方自己实现,供被调用方使用的特殊函数。
在面向对象的语言中,回调则是通过接口或抽象类来实现的,我们把实现这种接口的类成为回调类,回调类的对象成为回调对象。对于象C++或Object Pascal这些兼容了过程特性的对象语言,不仅提供了回调对象、回调方法等特性,也能兼容过程语言的回调函数机制。
Windows平台的消息机制也可以看作是回调的一种应用,我们通过系统提供的接口注册消息处理函数(即回调函数),从而实现接收、处理消息的目的。由于Windows平台的API是用C语言来构建的,我们可以认为它也是回调函数的一个特例。
对于分布式组件代理体系CORBA,异步处理有多种方式,如回调、事件服务、通知服务等。事件服务和通知服务是CORBA用来处理异步消息的标准服务,他们主要负责消息的处理、派发、维护等工作。对一些简单的异步处理过程,我们可以通过回调机制来实现。
下面我们集中比较具有代表性的语言(C、Object Pascal)和架构(CORBA)来分析回调的实现方式、具体作用等。

使用特权

评论回复
9
wahahaheihei|  楼主 | 2016-4-28 23:00 | 只看该作者

2 过程语言中的回调(C)

2.1 函数指针

回调在C语言中是通过函数指针来实现的,通过将回调函数的地址传给被调函数从而实现回调。因此,要实现回调,必须首先定义函数指针,请看下面的例子:



void Func(char *s);// 函数原型void (*pFunc) (char *);//函数指针



可以看出,函数的定义和函数指针的定义非常类似。

一般的化,为了简化函数指针类型的变量定义,提高程序的可读性,我们需要把函数指针类型自定义一下。



typedef void(*pcb)(char *);



回调函数可以象普通函数一样被程序调用,但是只有它被当作参数传递给被调函数时才能称作回调函数。

被调函数的例子:



void GetCallBack(pcb callback){/*do something*/}用户在调用上面的函数时,需要自己实现一个pcb类型的回调函数:void fCallback(char *s) {/* do something */} 然后,就可以直接把fCallback当作一个变量传递给GetCallBack,GetCallBack(fCallback);



如果赋了不同的值给该参数,那么调用者将调用不同地址的函数。赋值可以发生在运行时,这样使你能实现动态绑定。


使用特权

评论回复
10
wahahaheihei|  楼主 | 2016-4-28 23:03 | 只看该作者

2.2 参数传递规则

到目前为止,我们只讨论了函数指针及回调而没有去注意ANSI C/C++的编译器规范。许多编译器有几种调用规范。如在Visual C++中,可以在函数类型前加_cdecl,_stdcall或者_pascal来表示其调用规范(默认为_cdecl)。C++ Builder也支持_fastcall调用规范。调用规范影响编译器产生的给定函数名,参数传递的顺序(从右到左或从左到右),堆栈清理责任(调用者或者被调用者)以及参数传递机制(堆栈,CPU寄存器等)。

将调用规范看成是函数类型的一部分是很重要的;不能用不兼容的调用规范将地址赋值给函数指针。例如:



// 被调用函数是以int为参数,以int为返回值__stdcall int callee(int); // 调用函数以函数指针为参数void caller( __cdecl int(*ptr)(int)); // 在p中企图存储被调用函数地址的非法操作__cdecl int(*p)(int) = callee; // 出错



指针p和callee()的类型不兼容,因为它们有不同的调用规范。因此不能将被调用者的地址赋值给指针p,尽管两者有相同的返回值和参数列

2.3 应用举例

C语言的标准库函数中很多地方就采用了回调函数来让用户定制处理过程。如常用的快速排序函数、二分搜索函数等。

快速排序函数原型:



void qsort(void *base, size_t nelem, size_t width, int (_USERENTRY *fcmp)(const void *, const void *));二分搜索函数原型:void *bsearch(const void *key, const void *base, size_t nelem,                                 size_t width, int (_USERENTRY *fcmp)(const void *, const void *));



其中fcmp就是一个回调函数的变量。

下面给出一个具体的例子:



#include <stdio.h>#include <stdlib.h>int sort_function( const void *a, const void *b);int list[5] = { 54, 21, 11, 67, 22 };int main(void){   int  x;   qsort((void *)list, 5, sizeof(list[0]), sort_function);   for (x = 0; x < 5; x++)      printf("%i\n", list[x]);   return 0;}int sort_function( const void *a, const void *b){   return *(int*)a-*(int*)b;}



2.4 面向对象语言中的回调(Delphi)

Dephi与C++一样,为了保持与过程语言Pascal的兼容性,它在引入面向对象机制的同时,保留了以前的结构化特性。因此,对回调的实现,也有两种截然不同的模式,一种是结构化的函数回调模式,一种是面向对象的接口模式。

2.4.1 回调函数

回调函数类型定义:



type   TCalcFunc=function (a:integer;b:integer):integer;



按照回调函数的格式自定义函数的实现,如



function Add(a:integer;b:integer):integerbegin  result:=a+b;end;function Sub(a:integer;b:integer):integerbegin  result:=a-b;end;



回调的使用



function Calc(calc:TcalcFunc;a:integer;b:integer):integer



下面,我们就可以在我们的程序里按照需要调用这两个函数了



c:=calc(add,a,b);//c=a+bc:=calc(sub,a,b);//c=a-b



2.4.2 回调对象

什么叫回调对象呢,它具体用在哪些场合?首先,让我们把它与回调函数对比一下,回调函数是一个定义了函数的原型,函数体则交由第三方来实现的一种动态应用模式。要实现一个回调函数,我们必须明确知道几点:该函数需要那些参数,返回什么类型的值。同样,一个回调对象也是一个定义了对象接口,但是没有具体实现的抽象类(即接口)。要实现一个回调对象,我们必须知道:它需要实现哪些方法,每个方法中有哪些参数,该方法需要放回什么值。

因此,在回调对象这种应用模式中,我们会用到接口。接口可以理解成一个定义好了但是没有实现的类,它只能通过继承的方式被别的类实现。Delphi中的接口和COM接口类似,所有的接口都继承与IInterface(等同于IUnknow),并且要实现三个基本的方法QueryInterface, _AddRef, 和_Release。

  • 定义一个接口
    type IShape=interface(IInterface)        procedure Draw;end
  • 实现回调类
    type TRect=class(TObject,IShape)        protected      function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;      function _AddRef: Integer; stdcall;function _Release: Integer; stdcall;    public          procedure Draw;end;type TRound=class(TObject,IShape)        protected      function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;      function _AddRef: Integer; stdcall;function _Release: Integer; stdcall;    public          procedure Draw;end;
  • 使用回调对象
    procedure MyDraw(shape:IShape);var shape:IShape;beginshape.Draw; end;

如果传入的对象为TRect,那么画矩形;如果为TRound,那么就为圆形。用户也可以按照自己的意图来实现IShape接口,画出自己的图形:



MyDraw(Trect.Create);MyDraw(Tround.Create);


使用特权

评论回复
11
wahahaheihei|  楼主 | 2016-4-28 23:05 | 只看该作者

2.4.3 回调方法

回调方法(Callback Method)可以看作是回调对象的一部分,Delphi对windows消息的封装就采用了回调方法这个概念。在有些场合,我们不需要按照给定的要求实现整个对象,而只要实现其中的一个方法就可以了,这是我们就会用到回调方法。

回调方法的定义如下:



TNotifyEvent = procedure(Sender: TObject) of object; TMyEvent=procedure(Sender:Tobject;EventId:Integer) of object;



TNotifyEvent 是Delphi中最常用的回调方法,窗体、控件的很多事件,如单击事件、关闭事件等都是采用了TnotifyEvent。回调方法的变量一般通过事件属性的方式来定义,如TCustomForm的创建事件的定义:



property OnCreate: TNotifyEvent read FOnCreate write FOnCreate stored IsForm;



我们通过给事件属性变量赋值就可以定制事件处理器。

用户定义对象(包含回调方法的对象):



type TCallback=Class    procedure ClickFunc(sender:TObject);end;procedure Tcallback.ClickFunc(sender:TObject);begin  showmessage('the caller is clicked!');end;



窗体对象:



type TCustomFrm=class(TForm)  public        procedure RegisterClickFunc(cb:procedure(sender:Tobject) of object);end;procedure TcustomFrm..RegisterClickFunc(cb:TNotifyEvent);begin  self.OnClick=cb;end;



使用方法:



var  frm:TcustomFrm;begin  frm:=TcustomFrm.Create(Application);  frm.RegisterClickFunc(Tcallback.Create().ClickFunc);end;


使用特权

评论回复
12
wahahaheihei|  楼主 | 2016-4-28 23:06 | 只看该作者

3 回调在分布式计算中的应用(CORBA)

3.1 回调接口模型

CORBA的消息传递机制有很多种,比如回调接口、事件服务和通知服务等。回调接口的原理很简单,CORBA客户和服务器都具有双重角色,即充当服务器也是客户客户。

回调接口的反向调用与正向调用往往是同时进行的,如果服务端多次调用该回调接口,那么这个回调接口就变成异步接口了。因此,回调接口在CORBA中常常充当事件注册的用途,客户端调用该注册函数时,客户函数就是回调函数,在此后的调用中,由于不需要客户端的主动参与,该函数就是实现了一种异步机制。

从CORBA规范我们知道,一个CORBA接口在服务端和客户端有不同的表现形式,在客户端一般使用桩(Stub)文件,服务端则用到框架(Skeleton)文件,接口的规格采用IDL来定义。而回调函数的引入,使得服务端和客户端都需要实现一定的桩和框架。

下面给出了一个使用回调的接口文件,服务端需要实现Server接口的框架,客户端需要实现CallBack的框架:



module cb{        interface CallBack;        interface Server;interface CallBack {            void OnEvent(in long Source,in long msg);};          interface Server {            long RegisterCB(in CallBack cb);                void UnRegisterCB(in long hCb);};};



客户端首先通过同步方式调用服务端的接口RegistCB,用来注册回调接口CallBack。服务端收到该请求以后,就会保留该接口引用,如果发生某种事件需要向客户端通知的时候就通过该引用调用客户方的OnEvent函数,以便对方及时处理。


使用特权

评论回复
13
wahahaheihei|  楼主 | 2016-4-28 23:16 | 只看该作者

在层次化程序设计中,上层模块可以直接调用下层模块的函数,而下层模块一般不能直接调用上层模块的函数。而实际情况中却常常存在层间相互依赖的情况,即层间相互调用函数,例如,层B的状态变化需要通知层A或者引起层B的状态变化,为了避免这种相互依赖,可以使用回调函数。假设层A位于层B的上层,层A调用层B的函数,称层A为caller,层B中被调用的函数被称为callee,层A中被callee回调的函数称为callbacker。

1. 回调函数

    回调函数是通过caller向callee传递callbacker的函数指针实现,当在callee中callbacker被调用时,称为发生回调,而callbacker则称为回调函数。callee无需关心callbacker的实现细节和所处理的具体的数据类型,仅需知道callbacker的原型即可,而callbacker的实现由caller负责,其中包括实现细节(算法)和数据类型。

    回调函数可以实现动态绑定,即通过在运行时向callee传递不同的函数指针,从而调用不同的函数。例如,排序算法中需要按某种规则比较数据,callee无需知道数据比较的方法以及数据的类型,而仅仅关心比较数据的个数以及比较结果的含义,具体的比较操作由callbacker负责,数据类型可以是原始数据类型也可以是结构体类型。

    回调可以实现消息通知和事件驱动,比如callee中发生某个事件时,需要通知caller或者需要caller完成某种功能,则可以通过回调机制实现。

2. 函数指针

    回调机制是通过传递函数指针实现,而函数指针则是指向函数的指针,函数指针的定义可以使用两种形式:

(1)直接定义

    函数返回类型 (*函数指针名)(形参表);

(2)使用typedef

    typedef 函数返回类型 (*新类型名)(形参表);

    新类型名 函数指针名;

int add_int(int a, int b){return a+b;}

int (*pfunc)(int x, int y);

pfun = add_int;//或者&add_int

typedef int (*PFUNC)(int x, int y);

PFUNC pfunx;

pfunx = &add_int;

3. 返回函数指针的函数

    即函数的返回值是一个函数指针,也可以有两种定义形式:

(1)直接定义

    函数返回类型 (*函数名(形参表1))(形参表2){......}

    定义了一个名称由“函数名”标识的函数,其参数由“形参表1”标识,该函数返回一个函数指针,指向一个由“函数返回类型”标识返回类型、参数个数以及类型符合“形参表2”的函数。

(2)使用typedef

    typedef 函数返回类型 (*新类型名)(形参表2);

    新类型名 函数名(形参表1){......}


实例:
void (*signal(int sig, void (*func)(int)))(int);
函数名:signal
功能:指定处理信号的函数,sig指定信号,func为处理该信号的函数,具有一个整型的参数
返回值:为一个函数指针,指向一个具有一个整型参数、返回值类型为void的函数,该函数参数类型以及返回值类型与func函数一致;即返回该信号之前的处理函数

使用特权

评论回复
14
wahahaheihei|  楼主 | 2016-4-28 23:17 | 只看该作者
#include <signal.h>

char tmpfilename [L_tmpnam];

void terminate (int param)
{
  printf ("Terminating program...\n");
  remove (tmpfilename);//移除临时文件
  exit(1);
}

int main(void)
{
        void (*prev_fn)(int);//定义函数指针
        FILE *fp;

        prev_fn = signal (SIGTERM,terminate);//为SIGTERM信号指定处理函数terminate

        tmpnam (tmpfilename);//生成临时文件名并保存在tmpfilename中

        fp = fopen(tmpfilename, "w+");
        fprintf(fp, "test");
        fclose(fp);

        raise(SIGTERM);//生成SIGTERM信号并通知进程

        if (prev_fn==SIG_IGN) signal (SIGTERM,SIG_IGN);//重新设定默认处理函数

        return 0;
}
4. 函数指针数组
    指向一组有相同返回类型以及参数个数和顺序的函数,常用来替换switch/if结构,也可以有两种定义形式:
(1)直接定义
     函数返回类型 (*函数指针数组名[N])(形参表);
(2)使用typedef
    typedef 函数返回类型 (*新类型名)(形参表);
    新类型名 函数指针数组名[N];


使用特权

评论回复
15
zhuotuzi| | 2016-4-28 23:26 | 只看该作者
回调函数可以象普通函数一样被程序调用,但是只有它被当作参数传递给被调函数时才能称作回调函数。

使用特权

评论回复
16
zhuomuniao110| | 2016-4-28 23:43 | 只看该作者
回调在C语言中是通过函数指针来实现的,通过将回调函数的地址传给被调函数从而实现回调。

使用特权

评论回复
17
gejigeji521| | 2016-4-29 21:01 | 只看该作者
通常,当我们想通过一个统一接口实现不同内容的时候,用回调函数来实现就非常合适。

使用特权

评论回复
18
wahahaheihei|  楼主 | 2016-4-30 10:31 | 只看该作者
中断发生后进入ADC_IRQHandler 函数,最终会调用到HAL_ADC_ConvCpltCallback

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

217

主题

3063

帖子

12

粉丝