C++回撥函式(callback)的使用

weixin_30788239發表於2013-03-18

什麼是回撥函式(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;             
       以上函式為全域性函式,假如要使用一個類裡的函式作為回撥函式原形,把該類函式宣告為靜態函式即可。 [Page]

三: 回撥函式呼叫呼叫者
          呼叫回撥函式的函式我把他放到了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);

[NextPage]

         int Step=1;
              MSG Msg; [Page]
               DWORD StartTick;
         //一個延時迴圈
              for(;Step<200;Step++)
              {
                         StartTick = GetTickCount();
                  
                        for(;GetTickCount()-StartTick<10;)
                          {
                                  if(PeekMessage(&Msg,NULL,0,0,PM_NOREMOVE) )
                                  {
                                    TranslateMessage(&Msg);
                                    DispatchMessage(&Msg);
                                    }
                            }                                 [Page]
                      
            sprintf(Buffer,/"Running d/",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);

[NextPage]


             MessageBox(NULL,Buffer,/"Testing/",MB_OK);   //演示回撥函式被呼叫
              return res;             [Page]
        }   
          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裡的多執行緒延時後呼叫回撥函式的方式,只是為了突出一下回撥函式的效果,其實只要是在本程式之內,都能夠隨您高興能夠把函式地址傳遞來傳遞去,當成回撥函式使用。
        這樣的程式設計模式原理很簡單單一:就是把函式也看成一個指標一個地址來呼叫,沒有什麼別的複雜的東西,僅僅是程式設計裡的一個小技巧。至於回撥函式模式究竟能為您帶來多少好處,就看您是否使用,怎樣使用這種程式設計模式了。 [Page]
另外的解釋: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, 或說, 我根本不用關心您在主模組裡怎麼處理他, 也不應該關心用什麼函式處理他...... 怎麼辦?

[NextPage]


     使用回撥.
     我在我的模組裡先定義回撥函式型別, 連同回撥函式指標.
     typedef void (CALLBACK *cbkSendCmdToMain) (AnsiString sCmd);
     cbkSendCmdToMain     SendCmdToMain;
     這樣SendCmdToMain就是個指向擁有一個AnsiString形參, 返回值為void的函式指標.
     這樣, 在我接收到命令時, 就能夠呼叫這個函式啦. [Page]
     ...
     SendCmdToMain(sCommand);
     ...
     但是這樣還不夠, 我得給一個介面函式(比如Init), 讓您在主模組裡呼叫Init來註冊這個回撥函式.
     在您的主模組裡, 可能這樣
     void CALLBACK YourSendCmdFun(AnsiString sCmd);   //宣告
     ...
     void CALLBACK YourSendCmdFun(AnsiString sCmd);   //定義
     {
         ShowMessage(sCmd);
     }
     ...
     呼叫Init函式向我的模組註冊回撥.可能這樣:
     Init(YourSendCmdFun, ...);
     這樣, 預期目的就達到了.

     需要注意一點, 回撥函式一般都要宣告為全域性的. 假如要在類裡使用回撥函式, 前面需要加上 static   , 其實也相當於全域性的.

轉載於:https://www.cnblogs.com/zjoch/p/4237197.html

相關文章