天天看點

什麼是回調函數(callback)

  什麼是回調函數(callback)

子產品A有一個函數foo,它向子產品B傳遞foo的位址,然後在B裡面發生某種事件(event)時,通過從A裡面傳遞過來的foo的位址調用foo,通知A發生了什麼事情,讓A作出相應反應。 那麼我們就把foo稱為回調函數。

例子:

      回調函數是一個很有用,也很重要的概念。當發生某種事件時,系統或其他函數将會自動調用你定義的一段函數。回調函數在windows程式設計使用的場合很多, 比如Hook回調函數:MouseProc,GetMsgProc以及EnumWindows,DrawState的回調函數等等,還有很多系統級的回調 過程。本文不準備介紹這些函數和過程,而是談談實作自己的回調函數的一些經驗。

      之是以産生使用回調函數這個想法,是因為現在使用VC和Delphi混合程式設計,用VC寫的一個DLL程式進行一些時間比較長的異步工作,工作完成之後,需 要通知使用DLL的應用程式:某些事件已經完成,請處理事件的後續部分。開始想過使用同步對象,檔案影射,消息等實作DLL函數到應用程式的通知,後來突 然想到可不可以在應用程式端先寫一個函數,等需要處理後續事宜的時候,在DLL裡直接調用這個函數即可。   

       于是就動手,寫了個回調函數的原形。在VC和 Delphi裡都進行了測試

一:聲明回調函數類型。

        vc版

               typedef int (WINAPI *PFCALLBACK)(int Param1,int Param2) ;

        Delph版

               PFCALLBACK = function(Param1:integer;Param2:integer):integer;stdcall;

        實際上是聲明了一個傳回值為int,傳入參數為兩個int的指向函數的指針。

        由于C++和PASCAL編譯器對參數入棧和函數傳回的處理有可能不一緻,把函數類型用WINAPI(WINAPI宏展開就是__stdcall)或stdcall統一修飾。

二:聲明回調函數原形

        聲明函數原形

       vc版

                int WINAPI CBFunc(int Param1,int Param2);

        Delphi版

           function CBFunc(Param1,Param2:integer):integer;stdcall;             

       以上函數為全局函數,如果要使用一個類裡的函數作為回調函數原形,把該類函數聲明為靜态函數即可。

三: 回調函數調用調用者

          調用回調函數的函數我把它放到了DLL裡,這是一個很簡單的VC生成的WIN32 DLL.并使用DEF檔案輸出其函數名 TestCallBack。實作如下:

               PFCALLBACK   gCallBack=0;

             void WINAPI TestCallBack(PFCALLBACK Func)

            {

                   if(Func==NULL)return;

                   gCallBack=Func;

                   DWORD ThreadID=0;

                   HANDLE hThread = CreateThread(   NULL,   NULL,   Thread1,    LPVOID(0),           &ThreadID );

                    return;

              }

       此函數的工作把傳入的 PFCALLBACK Func參數儲存起來等待使用,并且啟動一個線程。聲明了一個函數指針PFCALLBACK gCallBack儲存傳入的函數位址。

四: 回調函數如何被使用:

           TestCallBack函數被調用後,啟動了一個線程,作為示範,線程人為的進行了延時處理,并且把線程運作的過程列印在螢幕上.

本段線程的代碼也在DLL工程裡實作

       ULONG   WINAPI Thread1(LPVOID Param)

      {

              TCHAR Buffer[256];

              HDC hDC = GetDC(HWND_DESKTOP);

              int Step=1;

              MSG Msg;

               DWORD StartTick;

         //一個延時循環

              for(;Step<200;Step++)

              {

                         StartTick = GetTickCount();

                        for(;GetTickCount()-StartTick<10;)

                          {

                                  if(PeekMessage(&Msg,NULL,0,0,PM_NOREMOVE) )

                                  {

                                    TranslateMessage(&Msg);

                                    DispatchMessage(&Msg);

                                    }

                            }                                

            sprintf(Buffer,"Running %04d",Step);

                          if(hDC!=NULL)

                                   TextOut(hDC,30,50,Buffer,strlen(Buffer));

                    }

                 (*gCallback)(Step,1);

                    ::ReleaseDC (HWND_DESKTOP,hDC);

                   return 0;

       }

五:萬事具備

         使用vc和Delphi各建立了一個工程,編寫回調函數的實作部分

        VC版

      int WINAPI CBFunc(int Param1,int Param2)

        {

                int res= Param1+Param2;

              TCHAR Buffer[256]="";

             sprintf(Buffer,"callback result = %d",res);

             MessageBox(NULL,Buffer,"Testing",MB_OK);   //示範回調函數被調用

              return res;            

        }   

          Delphi版

           function CBFunc(Param1,Param2:integer):integer;

           begin

                   result:= Param1+Param2;

                   TForm1.Edit1.Text:=inttostr(result);     / /示範回調函數被調用

            end;

        使用靜态連接配接的方法連接配接DLL裡的出口函數 TestCallBack,在工程裡添加 Button( 對于Delphi的工程,還需要在Form1上放一個Edit控件,預設名為Edit1)。

         響應ButtonClick事件調用 TestCallBack

               TestCallBack(CBFunc) //函數的參數CBFunc為回調函數的位址

         函數調用建立線程後立刻傳回,應用程式可以同時幹别的事情去了。現在可以看到螢幕上不停的顯示字元串,表示dll裡建立的線程運作正常。一會之後,線程延 時部分結束結束,vc的應用程式彈出MessageBox,表示回調函數被調用并顯示根據Param1,Param2運算的結果,Delphi的程式 edit控件裡的文本則被改寫成Param1,Param2 的運算結果。

         可見使用回調函數的程式設計模式,可以根據不同的需求傳遞不同的回調函數位址,或者定義各種回調函數的原形(同時也需要改變使用回調函數的參數和傳回值約 定),實作多種回調事件處理,可以使程式的控制靈活多變,也是一種高效率的,清晰的程式子產品之間的耦合方式。在一些異步或複雜的程式系統裡尤其有用 -- 你可以在一個子產品(如DLL)裡專心實作子產品核心的業務流程和技術功能,外圍的擴充的功能隻給出一個回調函數的接口,通過調用其他子產品傳遞過來的回調函數 位址的方式,将後續處理無縫地交給另一個子產品,随它按自定義的方式處理。

       本文的例子使用了在DLL裡的多線程延時後調用回調函數的方式,隻是為了突出一下回調函數的效果,其實隻要是在本程序之内,都可以随你高興可以把函數位址傳遞來傳遞去,當成回調函數使用。

        這樣的程式設計模式原理非常簡單單一:就是把函數也看成一個指針一個位址來調用,沒有什麼别的複雜的東西,僅僅是程式設計裡的一個小技巧。至于回調函數模式究竟能為你帶來多少好處,就看你是否使用,如何使用這種程式設計模式了。

另外的解釋:cdxiaogan

msdn上這麼說的:

有關函數指針的知識

使用例子可以很好地說明函數指針的用法。首先,看一看 Win32 API 中的 EnumWindows 函數:

Declare Function EnumWindows lib "user32" _

(ByVal lpEnumFunc as Long, _

ByVal lParam as Long ) As Long

EnumWindows 是一個枚舉函數,它能夠列出系統中每一個打開的視窗的句柄。EnumWindows 的工作方式是重複地調用傳遞給它的第一個參數(lpEnumFunc,函數指針)。每當 EnumWindows 調用函數,EnumWindows 都傳遞一個打開視窗的句柄。

在代碼中調用 EnumWindows 時,可以将一個自定義函數作為第一個參數傳遞給它,用來處理一系列的值。例如,可以編寫一個函數将所有的值添加到一個清單框中,将 hWnd 值轉換為視窗的名字,以及其它任何操作!

為了表明傳遞的參數是一個自定義函數,在函數名稱的前面要加上 AddressOf 關鍵字。第二個參數可以是合适的任何值。例如,如果要把 MyProc 作為函數參數,可以按下面的方式調用 EnumWindows:

x = EnumWindows(AddressOf MyProc, 5)

在調用過程時指定的自定義函數被稱為回調函數。回調函數(通常簡稱為“回調”)能夠對過程提供的資料執行指定的操作。

回調函數的參數集必須具有規定的形式,這是由使用回調函數的 API 決定的。關于需要什麼參數,如何調用它們,請參閱 API 文檔。

回複人:zcchm

我談一下自己對回調函數的一點了解, 不對的地方請指教.

     我剛開始接觸回調時, 也是一團霧水.很多人解釋這個問題時, 總是拿API來舉例子, 本來菜鳥最懼怕的就是API, ^_^. 回調跟API沒有必然聯系.

     其實回調就是一種利用函數指針進行函數調用的過程.

     為什麼要用回調呢?比如我要寫一個子子產品給你用, 來接收遠端socket發來的指令.當我接收到指令後, 需要調用你的主子產品的函數, 來進行相應的處理.但是我不知道你要用哪個函數來處理這個指令,   我也不知道你的主子產品是什麼.cpp或者.h, 或者說, 我根本不用關心你在主子產品裡怎麼處理它, 也不應該關心用什麼函數處理它...... 怎麼辦?

     使用回調.

     我在我的子產品裡先定義回調函數類型, 以及回調函數指針.

     typedef void (CALLBACK *cbkSendCmdToMain) (AnsiString sCmd);

     cbkSendCmdToMain     SendCmdToMain;

     這樣SendCmdToMain就是一個指向擁有一個AnsiString形參, 傳回值為void的函數指針.

     這樣, 在我接收到指令時, 就可以調用這個函數啦.

     ...

     SendCmdToMain(sCommand);

     ...

     但是這樣還不夠, 我得給一個接口函數(比如Init), 讓你在主子產品裡調用Init來注冊這個回調函數.

     在你的主子產品裡, 可能這樣

     void CALLBACK YourSendCmdFun(AnsiString sCmd);   //聲明

     ...

     void CALLBACK YourSendCmdFun(AnsiString sCmd);   //定義

     {

         ShowMessage(sCmd);

     }

     ...

     調用Init函數向我的子產品注冊回調.可能這樣:

     Init(YourSendCmdFun, ...);

     這樣, 預期目的就達到了.

     需要注意一點, 回調函數一般都要聲明為全局的. 如果要在類裡使用回調函數, 前面需要加上 static   , 其實也相當于全局的.

繼續閱讀