回調心的

最近,忙着搞個回調函數,但是我連回調函數是什麼都不知道,好在經過一番修煉,略知一二,分享如下:

 

     在說回調函數之前,首先要搞清楚什麼是函數指針:它就是一個函數在編譯時被分配的入口地址,可以將該地址賦給一個指針,這樣指針地址變量持有函數入口地址,它就指向了該函數,所以稱這種指針爲指向函數的指針,簡稱函數指針。 在說明函數指針時,同時也要描述指針所指向的函數的參數類型和個數, 如

     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合併在一起。

 

       至此,回調函數的主要應用已經差不多寫完了,關鍵是在程序中具體的應用,只有在具體的應用中才能真正的掌握它。


本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/xuxinshao/archive/2010/03/12/5373187.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章