C++回撥函式 用法

Mobidogs發表於2020-04-04

一回撥函式

我們經常在C++設計時通過使用回撥函式可以使有些應用(如定時器事件回撥處理、用回撥函式記錄某操作進度等)變得非常方便和符合邏輯,那麼它的內在機制如何呢,怎麼定義呢?它和其它函式(比如鉤子函式)有何不同呢?

使用回撥函式實際上就是在呼叫某個函式(通常是API函式)時,將自己的一個函式(這個函式為回撥函式)的地址作為引數傳遞給那個函式。

而那個函式在需要的時候,利用傳遞的地址呼叫回撥函式,這時你可以利用這個機會在回撥函式中處理訊息或完成一定的操作。至於如何定義回撥函式,跟具體使用的API函式有關,一般在幫助中有說明回撥函式的引數和返回值等。C++中一般要求在回撥函式前加CALLBACK(相當於FAR PASCAL),這主要是說明該函式的呼叫方式。

至於鉤子函式,只是回撥函式的一個特例。習慣上把與SetWindowsHookEx函式一起使用的回撥函式稱為鉤子函式。也有人把利用VirtualQueryEx安裝的函式稱為鉤子函式,不過這種叫法不太流行。

也可以這樣,更容易理解:回撥函式就好像是一箇中斷處理函式,系統在符合你設定的條件時自動呼叫。為此,你需要做三件事:

1.       宣告;

2.       定義;

3.       設定觸發條件,就是在你的函式中把你的回撥函式名稱轉化為地址作為一個引數,以便於系統呼叫。

宣告和定義時應注意:回撥函式由系統呼叫,所以可以認為它屬於WINDOWS系統,不要把它當作你的某個類的成員函式。

二回撥函式、訊息和事件例程


    呼叫(calling)機制從彙編時代起已經大量使用:準備一段現成的程式碼,呼叫者可以隨時跳轉至此段程式碼的起始地址,執行完後再返回跳轉時的後續地址。CPU為此準備了現成的呼叫指令,呼叫時可以壓棧保護現場,呼叫結束後從堆疊中彈出現場地址,以便自動返回。借堆疊保護現場真是一項絕妙的發明,它使呼叫者和被調者可以互不相識,於是才有了後來的函式和構件。

    此呼叫機制並非完美。回撥函式就是一例。函式之類本是為呼叫者準備的美餐,其烹製者應對食客瞭如指掌,但實情並非如此。例如,寫一個快速排序函式供他人呼叫,其中必包含比較大小。麻煩來了:此時並不知要比較的是何類資料--整數、浮點數、字串?於是只好為每類資料製作一個不同的排序函式。更通行的辦法是在函式引數中列一個回撥函式地址,並通知呼叫者:君需自己準備一個比較函式,其中包含兩個指標類引數,函式要比較此二指標所指資料之大小,並由函式返回值說明比較結果。排序函式借此呼叫者提供的函式來比較大小,借指標傳遞引數,可以全然不管所比較的資料型別。被呼叫者回頭呼叫呼叫者的函式(夠咬嘴的),故稱其為回撥(callback)。

    回撥函式使程式結構亂了許多。Windows API 函式集中有不少回撥函式,儘管有詳盡說明,仍使初學者一頭霧水。恐怕這也是無奈之舉。

無論何種事物,能以樹形結構單向描述畢竟讓人舒服些。如果某家族中孫輩又是某祖輩的祖輩,恐怕無人能理清其中的頭緒。但資料處理之複雜往往需要構成網狀結構,非簡單的客戶/伺服器關係能窮盡。

    Windows 系統還包含著另一種更為廣泛的回撥機制,即訊息機制。訊息本是 Windows 的基本控制手段,乍看與函式呼叫無關,其實是一種變相的函式呼叫。傳送訊息的目的是通知收方執行一段預先準備好的程式碼,相當於呼叫一個函式。訊息所附帶的 WParam 和 LParam 相當於函式的引數,只不過比普通引數更通用一些。應用程式可以主動傳送訊息,更多情況下是坐等 Windows 傳送訊息。一旦訊息進入所屬訊息佇列,便檢感興趣的那些,跳轉去執行相應的訊息處理程式碼。作業系統本是為應用程式服務,由應用程式來呼叫。而應用程式一旦啟動,卻要反過來等待作業系統的呼叫。這分明也是一種回撥,或者說是一種廣義回撥。其實,應用程式之間也可以形成這種回撥。假如程式 B 收到程式 A 發來的訊息,啟動了一段程式碼,其中又向程式 A 傳送訊息,這就形成了回撥。這種回撥比較隱蔽,弄不好會搞成遞迴呼叫,若缺少終止條件,將會迴圈不已,直至把程式搞垮。若是故意編寫成此遞迴呼叫,並設好終止條件,倒是很有意思。但這種程式結構太隱蔽,除非十分必要,還是不用為好。

    利用訊息也可以構成狹義回撥。上面所舉排序函式一例,可以把回撥函式地址換成視窗 handle。如此,當需要比較資料大小時,不是去呼叫回撥函式,而是借 API 函式 SendMessage 向指定視窗傳送訊息。收到訊息方負責比較資料大小,把比較結果通過訊息本身的返回值傳給訊息傳送方。所實現的功能與回撥函式並無不同。當然,此例中改為訊息純屬畫蛇添腳,反倒把程式搞得很慢。但其他情況下並非總是如此,特別是需要非同步呼叫時,傳送訊息是一種不錯的選擇。假如回撥函式中包含檔案處理之類的低速處理,呼叫方等不得,需要把同步呼叫改為非同步呼叫,去啟動一個單獨的執行緒,然後馬上執行後續程式碼,其餘的事讓執行緒慢慢去做。一個替代辦法是借 API 函式 PostMessage 傳送一個非同步訊息,然後立即執行後續程式碼。這要比自己搞個執行緒省事許多,而且更安全。

    如今我們是活在一個 object 時代。只要與程式設計有關,無論何事都離不開 object。但 object 並未消除回撥,反而把它發揚光大,弄得到處都是,只不過大都以事件(event)的身份出現,鑲嵌在某個結構之中,顯得更正統,更容易被人接受。應用程式要使用某個構件,總要先弄清構件的屬性、方法和事件,然後給構件屬性賦值,在適當的時候呼叫適當的構件方法,還要給事件編寫處理例程,以備構件程式碼來呼叫。何謂事件?它不過是一個指向事件例程的地址,與回撥函式地址沒什麼區別。

    不過,此種回撥方式比傳統回撥函式要高明許多。首先,它把讓人不太舒服的回撥函式變成一種自然而然的處理例程,使程式設計者頓覺氣順。再者,地址是一個危險的東西,用好了可使程式加速,用不好處處是陷阱,程式隨時都會崩潰。現代程式設計方式總是想法把地址隱藏起來(隱藏比較徹底的如 VB 和 Java),其代價是降低了程式效率。事件例程(?)使程式設計者無需直接操作地址,但並不會使程式減速。
(例程似乎是程式的臺灣翻譯。)

三精妙比喻:回撥函式還真有點像您隨身帶的BP機:告訴別人號碼,在它有事情時Call您。

       回撥用於層間協作,上層將本層函式安裝在下層,這個函式就是回撥,而下層在一定條件下觸發回撥,例如作為一個驅動,是一個底層,他在收到一個資料時,除了完成本層的處理工作外,還將進行回撥,將這個資料交給上層應用層來做進一步處理,這在分層的資料通訊中很普遍。其實回撥和API非常接近,他們的共性都是跨層呼叫的函式。但區別是API是低層提供給高層的呼叫,一般這個函式對高層都是已知的;而回撥正好相反,他是高層提供給底層的呼叫,對於低層他是未知的,必須由高層進行安裝,這個安裝函式其實就是一個低層提供的API,安裝後低層不知道這個回撥的名字,但它通過一個函式指標來儲存這個回撥,在需要呼叫時,只需引用這個函式指標和相關的引數指標。    其實:回撥就是該函式寫在高層,低層通過一個函式指標儲存這個函式,在某個事件的觸發下,低層通過該函式指標呼叫高層那個函式。

四呼叫方式
    軟體模組之間總是存在著一定的介面,從呼叫方式上,可以把他們分為三類:同步呼叫、回撥和非同步呼叫。同步呼叫是一種阻塞式呼叫,呼叫方要等待對方執行完畢才返回,它是一種單向呼叫;回撥是一種雙向呼叫模式,也就是說,被呼叫方在介面被呼叫時也會呼叫對方的介面;非同步呼叫是一種類似訊息或事件的機制,不過它的呼叫方向剛好相反,介面的服務在收到某種訊息或發生某種事件時,會主動通知客戶方(即呼叫客戶方的介面)。回撥和非同步呼叫的關係非常緊密,通常我們使用回撥來實現非同步訊息的註冊,通過非同步呼叫來實現訊息的通知。同步呼叫是三者當中最簡單的,而回撥又常常是非同步呼叫的基礎。
   
    對於不同型別的語言(如結構化語言和物件語言)、平臺(Win32、JDK)或構架(CORBA、DCOM、WebService),客戶和服務的互動除了同步方式以外,都需要具備一定的非同步通知機制,讓服務方(或介面提供方)在某些情況下能夠主動通知客戶,而回撥是實現非同步的一個最簡捷的途徑。

    對於一般的結構化語言,可以通過回撥函式來實現回撥。回撥函式也是一個函式或過程,不過它是一個由呼叫方自己實現,供被呼叫方使用的特殊函式。

    在物件導向的語言中,回撥則是通過介面或抽象類來實現的,我們把實現這種介面的類成為回撥類,回撥類的物件成為回撥物件。對於象C++或Object Pascal這些相容了過程特性的物件語言,不僅提供了回撥物件、回撥方法等特性,也能相容過程語言的回撥函式機制。

    Windows平臺的訊息機制也可以看作是回撥的一種應用,我們通過系統提供的介面註冊訊息處理函式(即回撥函式),從而實現接收、處理訊息的目的。由於Windows平臺的API是用C語言來構建的,我們可以認為它也是回撥函式的一個特例。

    對於分散式元件代理體系CORBA,非同步處理有多種方式,如回撥、事件服務、通知服務等。事件服務和通知服務是CORBA用來處理非同步訊息的標準服務,他們主要負責訊息的處理、派發、維護等工作。對一些簡單的非同步處理過程,我們可以通過回撥機制來實現。

    下面我們集中比較具有代表性的語言(C、Object Pascal)和架構(CORBA)來分析回撥的實現方式、具體作用等。

   過程語言中的回撥(C)
    (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);

    如果賦了不同的值給該引數,那麼呼叫者將呼叫不同地址的函式。賦值可以發生在執行時,這樣使你能實現動態繫結。

    (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,儘管兩者有相同的返回值和引數列

    (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;
 }

 

相關文章