回调函数

来源:互联网 发布:plc编程和c 编辑:程序博客网 时间:2024/05/21 06:44

       在说回调函数之前,首先要搞清楚什么是函数指针:它就是一个函数在编译时被分配的入口地址,可以将该地址赋给一个指针,这样指针地址变量持有函数入口地址,它就指向了该函数,所以称这种指针为指向函数的指针,简称函数指针。在说明函数指针时,同时也要描述指针所指向的函数的参数类型和个数,如 :

     int (*funp)(int a , int b) ; 其中funp就是一个函数指针,它指向带有两个int 类型参数的函数。
    在C++中,单独的一个函数名(其后不跟圆括号),被自动的转化为该函数的入口地址,也就是该函数第一条指令的地址。因此,当把一个函数的地址赋给一个指针变量时,对该指针的操作就等同于调用该函数。
    说了那么多,总结一下就是:
    函数指针是一个指向函数的指针变量,它是专门来存放函数入口地址的,在程序中给它赋予哪个函数的入口地址,它就指向哪个函数,因此在一个程序中,一个函数的指针可被多次赋值,指向不同的函数。
    接下来我们来分析一下回调函数。
    下面请看一个例子:
     设一个函数process,在调用它的时候,每次实现不同的功能。输入a和b两个数,第一次调用的时找出其中的大者,第二次调用的时找出其中的小者。第三次调用求两者之和。
#include "iostream.h"
int max(int x , int y);
int min(int x , int y);
int add(int x , int y);
void process(int x , int y , int(*fun)(int , int));
//客户程序C
void main()
{
 int a,b;
 process(a,b,max);//注册回调函数
 process(a,b,min);
 process(a,b,add);
}
int max(int x , int y)
{
 return x>y?x:y;
}
int min(int x , int y)
{
 return x<y?x:y;
}
int add(int x, int y)
{
 return x + y;
}
//服务程序S
void process(int x, int y, int(* fun)(int, int))
{
 int result;
 result = (*fun)(x , y);
 return result;
}
    按照我们刚才的逻辑,其实所声明的三个功能函数:max ,min ,add 就是回调函数。
     请看:
     使用回调函数实际上就是在调用某个函数时将自己的一个函数(这个函数就是回调函数)的地址作为参数传递给那个函数。而那个函数在需要的时候,利用传递的地址调用回调函数,这是你可以利用这个机会,在回调函数中处理消息或完成一定的操作。
    也可以这样理解:
     所谓回调,就是客户程序C(main)调用服务程序S中的某个函数A(process),然后S又在某个时候反过来调用C中的某个函数B(max),对于C来说,这个B便叫做回调函数。例如Win32下的窗口过程函数就是一个典型的回调函数。
     一般说来,C不会自己调用B,C提供B的目的就是让S来调用它,而且是C不得不提供。由于S并不知道C提供的B叫什么,所以S会约定B的接口规范(函数原型),然后由C提前通过S的一个函数R(process)告诉S,自己将要使用B函数,这个过程称为回调函数的注册,R称为注册函数。
      下面举个通俗的例子:
      某天,我打电话向你请教问题,当然是个难题,:),你一时想不出解决方法,我又不能拿着电话在那里傻等,于是我们约定:等你想出办法后打手机通知我,这样,我就挂掉电话办其它事情去了。过了XX分钟,我的手机响了,你兴高采烈的说问题已经搞定,应该如此这般处理。故事到此结束。
 这个例子说明了“异步+回调”的编程模式。其中,你后来打手机告诉我结果便是一个“回调”过程;我的手机号码必须在以前告诉你,这便是注册回调函数;我的手机号码应该有效并且手机能够接收到你的呼叫,这是回调函数必须符合接口规范。       

      2. 什么情况下使用回调
      如果你是SDK的使用者,一旦别人制定了回调机制,那么你被迫得使用回调函数,因此这个问题只对SDK设计者有意义。
 从引入的目的看,回调大致分为三种:
 1) SDK有消息需要通知应用程序,比如定时器被触发;
 2) SDK的执行需要应用程序的参与,比如SDK需要你提供一种排序算法;
 3) SDK的操作比较费时,但又不能让应用程序阻塞在那里,于是采用异步方式,让调用函数及时返回,SDK另起线程在后台执行操作,待操作完成后再将结果通知应用程序。
 经上面这样一总结,你也许会恍然大悟:原来“回调机制”无处不在啊!
 是的,不光是Win32 API编程中你会用到,也不光是其它SDK编程中会用到,平时我们自己编写程序时也可能用到回调机制,这时,我们既是回调的设计者又是回调的使用者。
      3. 传统SDK回调函数设计模式
     Win32 SDK是这方面的典型例子,这类SDK的函数接口都是基于C语言的,SDK或者提供专门的注册函数,用于注册回调函数的地址,或者是在调用某个方法时才传入回调函数的地址,回调函数的原型也由于注册函数中的函数指针定义而受到约束。
 以Win32中的多媒体定时器函数为例,其原型为:
MMRESULT timeSetEvent(
  UINT uDelay,  // 定时器时间间隔,以毫秒为单位             
  UINT uResolution,          
  LPTIMECALLBACK lpTimeProc,  // 回调函数地址
  DWord dwUser,  // 用户设定的数据            

  UINT fuEvent               
);
 其中第三个参数便是用于注册回调函数的,第四个参数用于设定用户自定义数据,
        下面是回调函数的具体使用方法:
#include "stdio.h"
#include "windows.h"

#include "mmsystem.h" // 多媒体定时器需要包含此文件
#pragma comment(lib, "winmm.lib") // 多媒体定时器需要导入此库
void CALLBACK timer_proc(UINT uTimerID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2) // 定义回调函数
{
 printf("time out.\n");
}
int main()
{
 UINT nId = timeSetEvent( 1000, 0, timer_proc, 0, TIME_CALLBACK_FUNCTIONTIME_PERIODIC); // 注册回调函数
 getchar();
 timeKillEvent( nId );
 return 0;
}
     运行程序,我们会看到,屏幕上每隔一秒将会打印出“time out.”信息。同时我们也应该注意到,这里的timeSetEvent是异步执行的,timeSetEvent很快执行完毕,主线程继续执行,操作系统在后台负责检查timer是否超时。
 前面已经说过,本文的是站在SDK设计者的角度来看待问题,这里,我们就把前面那个通俗的例子变成程序,如下:
/// sdk.h (SDK头文件)
#ifndef __SDK_H__
#define __SDK_H__
typedef void (*HELP_CALLBACK)(const char*); // 回调函数指针
void help_me( const char* question, HELP_CALLBACK callback ); // 接口声明
#endif//__SDK_H__
/// sdk.cpp (SDK源文件,为方便,没有使用.c文件)
#include "sdk.h"
#include "stdio.h"
#include "windows.h"
HELP_CALLBACK g_callback;
void do_it() // 处理函数
{
 printf("thinking...\n");
 Sleep( 3000 );
 printf("think out.\n");
 printf("call him.\n");
 g_callback( "2." );
}
void help_me( const char* question, HELP_CALLBACK callback ) // 接口实现
{
 g_callback = callback; // 保存回调函数指针
 printf("help_me: %s\n", question);
 do_it(); // 如果采用异步方式的话,这里一般采用创建线程的方式

/// app.cpp (应用程序源文件)
#include "sdk.h"
#include "stdio.h"

void got_answer( const char* msg ) // 定义回调函数
{
 printf("got_answer: %s\n", msg);
}
int main()
{
 help_me( "1+1=?", got_answer ); // 使用SDK,注册回调函数
 return 0;
}

4. C++中回调函数的设计
     C++的类中也可以使用类似上面的设计方式。如果SDK采用C语言接口,应用程序使用C++编程方式,那么类成员函数由于具有隐含的this指针而不能赋值给普通函数指针,解决方法很简单,就是为其加上static关键字
 以上面的程序为例,这里我们只看应用程序的代码:
/// app.cpp ( C++风格 )
#include "sdk.h"
#include "stdio.h"
class App
{
public:

 void ask( const char* question )
 {
  help_me( question, got_answer );
 }

 static void got_answer( const char* msg )
 {
  printf("got_answer: %s\n", msg);
 }

}; 
int main()
{
 App app;
 app.ask( "1+1=?");
 return 0;
}
    上面这种方式有个明显的缺点:由于got_answer是静态成员函数,所以它不能访问类的非静态成员变量,这可不是一件好事情。为了解决此问题,作为回调函数的设计者,你有必要为其增添一个参数,用于传递用户所需的值,如下:
/// sdk.h (SDK头文件)
#ifndef __SDK_H__
#define __SDK_H__
typedef void (*HELP_CALLBACK)(const char*, unsigned long); // 回调函数指针
void help_me( const char* question, HELP_CALLBACK callback, unsigned long user_value ); // 接口声明
#endif//__SDK_H__
/// sdk.cpp (SDK源文件,为方便,没有使用.c文件)
#include "sdk.h"
#include "stdio.h"
#include "windows.h"
HELP_CALLBACK g_callback;
unsigned long g_user_value;
void do_it()
{
 printf("thinking...\n");
 Sleep( 3000 );
 printf("think out.\n");
 printf("call him.\n");
 g_callback( "2.", g_user_value ); // 将用户设定的数据传入
}
void help_me( const char* question, HELP_CALLBACK callback, unsigned long user_value )
{
 g_callback = callback;
 g_user_value = user_value; // 保存用户设定的数据
 printf("help_me: %s\n", question);
 do_it();
}
/// app.cpp (应用程序源文件)
#include "sdk.h"
#include "stdio.h"
#include "assert.h"

class App
{
public:
 App( const char* name ) : m_name(name)
 {
 }
 void ask( const char* question )
 {
  help_me( question, got_answer, (unsigned long)this ); // 将this指针传入
 }
 static void got_answer( const char* msg, unsigned long user_value )
 {
  App* pthis = (App*)user_value; // 转换成this指针,以访问非静态数据成员
  assert( pthis );
  printf("%s got_answer: %s\n", pthis->m_name, msg);
 }
protected:
 const char* m_name;
};
int main()
{
 App app("ABC");
 app.ask( "1+1=?");
 return 0;
}
     这里的user_value被设计成unsigned long,它既可以传递整型数据,也可以传递一个地址值(因为它们在32位机器上宽度都为32),有了地址,那么像结构体变量、类对象等都可以访问了。

5. C++中回调类编程模式
     时代在不断进步,SDK不再是古老的API接口,C++面向对象编程被广泛的用到各种库中,因此回调机制也可以采用C++的一些特性来实现。
 通过前面的讲解,其实我们不难发现回调的本质便是:SDK定义出一套接口规范,应用程序按照规定实现它。这样一说是不是很简单,
想想我们C++中的继承,想想我们亲爱的抽象基类......于是,我们得到以下的代码:
/// sdk.h
#ifndef __SDK_H__
#define __SDK_H__
class Notifier // 回调类,应用程序需从此派生
{
public:
 virtual ~Notifier() { }
 virtual void got_answer(const char* answer) = 0; // 纯虚函数,用户必须实现它
};
class Sdk // Sdk提供服务的类
{
public:
 Sdk(Notifier* pnotifier); // 用户必须注册指向回调类的指针
 void help_me(const char* question);
protected:
 void do_it(); 
protected:
 Notifier* m_pnotifier; // 用于保存回调类的指针
};
#define//__SDK_H__
/// sdk.cpp
#include "sdk.h"
#include "windows.h"
#include <iostream>
using namespace std;
Sdk::Sdk(Notifier* pnotifier) : m_pnotifier(pnotifier)
{
}
void Sdk::help_me(const char* question)
{
 cout << "help_me: " << question << endl;

 do_it();
}

void Sdk::do_it()
{
 cout << "thinking..." << endl;
 Sleep( 3000 );
 cout << "think out." << endl;
 cout << "call him." << endl;
 m_pnotifier->got_answer( "2." );
}
/// app.cpp
#include "sdk.h"
class App : public Notifier // 应用程序实现一个从回调类派生的类
{
public:
 App( const char* name ) : m_sdk(this), m_name(name) // 将this指针传入
 {
 }
 void ask( const char* question )
 {
  m_sdk.help_me( question );
 }
 void got_answer( const char* answer ) // 实现纯虚接口
 {
  cout << m_name << " got_answer: " << answer << endl;
 }
protected:
 Sdk m_sdk;
 const char* m_name;
};
int main()
{
 App app("ABC");
 app.ask( "1+1=?");
 return 0;
}
        哈哈,是不是很爽?Notifier将用户必须实现的回调函数都以纯虚函数的方式定义,这样用户就不得不实现它,当然如果不是非实现不可,那么我们也可以将其定义成一般的虚函数,并像析构函数那样提供一个“空”的实现,这样用户可以在关心它时才去实现它。由于这个类的作用是实现回调,所以我们不妨称之为回调类。
 上面这种方式一简化,可以将Sdk与Notifier合并在一起。
       至此,回调函数的主要应用已经差不多写完了,关键是在程序中具体的应用,只有在具体的应用中才能真正的掌握它。

文章出处:www.diybl.com):http://www.diybl.com/course/3_program/c++/cppjs/2008920/143907_2.html

 

原创粉丝点击