回調函數 callback

簡介

對於不少初學者來講,每每以爲回調函數很神祕,很想知道回調函數的工做原理。本文將要解釋什麼是回調函數、它們有什麼好處、爲何要使用它們等等問題,在開始以前,假設你已經熟知了函數指針。

什麼是回調函數?

簡而言之,回調函數就是一個經過函數指針調用的函數。若是你把函數的指針(地址)做爲參數傳遞給另外一個函數,當這個指針被用爲調用它所指向的函數時,咱們就說這是回調函數

爲何要使用回調函數?

由於能夠把調用者與被調用者分開。調用者不關心誰是被調用者,全部它需知道的,只是存在一個具備某種特定原型、某些限制條件(如返回值爲int)的被調用函數

若是想知道回調函數在實際中有什麼做用,先假設有這樣一種狀況,咱們要編寫一個庫,它提供了某些排序算法的實現,如冒泡排序、快速排序、shell排序、shake排序等等,但爲使庫更加通用,不想在函數中嵌入排序邏輯,而讓使用者來實現相應的邏輯;或者,想讓庫可用於多種數據類型(int、float、string),此時,該怎麼辦呢?能夠使用函數指針,並進行回調

回調可用於通知機制,例如,有時要在程序中設置一個計時器,每到必定時間,程序會獲得相應的通知,但通知機制的實現者對咱們的程序一無所知。而此時,就需有一個特定原型的函數指針,用這個指針來進行回調,來通知咱們的程序事件已經發生。實際上,SetTimer() API使用了一個回調函數來通知計時器,並且,萬一沒有提供回調函數,它還會把一個消息發往程序的消息隊列。

另外一個使用回調機制的API函數是EnumWindow(),它枚舉屏幕上全部的頂層窗口,爲每一個窗口調用一個程序提供的函數,並傳遞窗口的處理程序。若是被調用者返回一個值,就繼續進行迭代,不然,退出。EnumWindow()並不關心被調用者在何處,也不關心被調用者用它傳遞的處理程序作了什麼,它只關心返回值,由於基於返回值,它將繼續執行或退出。

無論怎麼說,回調函數是繼續自C語言的,於是,在C++中,應只在與C代碼創建接口,或與已有的回調接口打交道時,才使用回調函數。除了上述狀況,在C++中應使用虛擬方法或函數符(functor),而不是回調函數

一個簡單的回調函數實現

下面建立了一個sort.dll的動態連接庫,它導出了一個名爲CompareFunction的類型--typedef int (__stdcall *CompareFunction)(const byte*, const byte*),它就是回調函數的類型。另外,它也導出了兩個方法:Bubblesort()和Quicksort(),這兩個方法原型相同,但實現了不一樣的排序算法。算法

 

 

 

 

 

 

 

 

 

void DLLDIR __stdcall Bubblesort(byte* array,int size,int elem_size,CompareFunction cmpFunc);

void DLLDIR __stdcall Quicksort(byte* array,int size,int elem_size,CompareFunction cmpFunc);

這兩個函數接受如下參數:

·byte * array:指向元素數組的指針(任意類型)。

·int size:數組中元素的個數。

·int elem_size:數組中一個元素的大小,以字節爲單位。

·CompareFunction cmpFunc:帶有上述原型的指向回調函數的指針。

這兩個函數的會對數組進行某種排序,但每次都需決定兩個元素哪一個排在前面,而函數中有一個回調函數,其地址是做爲一個參數傳遞進來的。對編寫者來講,沒必要介意函數在何處實現,或它怎樣被實現的,所需在乎的只是兩個用於比較的元素的地址,並返回如下的某個值(庫的編寫者和使用者都必須遵照這個約定):

·-1:若是第一個元素較小,那它在已排序好的數組中,應該排在第二個元素前面。

·0:若是兩個元素相等,那麼它們的相對位置並不重要,在已排序好的數組中,誰在前面都無所謂。

·1:若是第一個元素較大,那在已排序好的數組中,它應該排第二個元素後面。

基於以上約定,函數Bubblesort()的實現以下,Quicksort()就稍微複雜一點:shell

void DLLDIR __stdcall Bubblesort(byte* array,int size,int elem_size,CompareFunction cmpFunc)
{
for(int i=0; i < size; i++)
{
for(int j=0; j < size-1; j++)
{
//回調比較函數
if(1 == (*cmpFunc)(array+j*elem_size,array+(j+1)*elem_size))
{
//兩個相比較的元素相交換
byte* temp = new byte[elem_size];
memcpy(temp, array+j*elem_size, elem_size);
memcpy(array+j*elem_size,array+(j+1)*elem_size,elem_size);
memcpy(array+(j+1)*elem_size, temp, elem_size);
delete [] temp;
}
}
}
}

注意:由於實現中使用了memcpy(),因此函數在使用的數據類型方面,會有所侷限。

對使用者來講,必須有一個回調函數,其地址要傳遞給Bubblesort()函數。下面有二個簡單的示例,一個比較兩個整數,而另外一個比較兩個字符串:編程

int __stdcall CompareInts(const byte* velem1, const byte* velem2)
{
int elem1 = *(int*)velem1;
int elem2 = *(int*)velem2;

if(elem1 < elem2)
return -1;
if(elem1 > elem2)
return 1;

return 0;
}

int __stdcall CompareStrings(const byte* velem1, const byte* velem2)
{
const char* elem1 = (char*)velem1;
const char* elem2 = (char*)velem2;
return strcmp(elem1, elem2);
}

下面另有一個程序,用於測試以上全部的代碼,它傳遞了一個有5個元素的數組給Bubblesort()和Quicksort(),同時還傳遞了一個指向回調函數的指針。windows

int main(int argc, char* argv[])
{
int i;
int array[] = {5432, 4321, 3210, 2109, 1098};

cout << "Before sorting ints with Bubblesort\n";
for(i=0; i < 5; i++)
cout << array[i] << '\n';

Bubblesort((byte*)array, 5, sizeof(array[0]), &CompareInts);

cout << "After the sorting\n";
for(i=0; i < 5; i++)
cout << array[i] << '\n';

const char str[5][10] = {"estella","danielle","crissy","bo","angie"};

cout << "Before sorting strings with Quicksort\n";
for(i=0; i < 5; i++)
cout << str[i] << '\n';

Quicksort((byte*)str, 5, 10, &CompareStrings);

cout << "After the sorting\n";
for(i=0; i < 5; i++)
cout << str[i] << '\n';

return 0;
}

若是想進行降序排序(大元素在先),就只需修改回調函數的代碼,或使用另外一個回調函數,這樣編程起來靈活性就比較大了。數組

調用約定

上面的代碼中,可在函數原型中找到__stdcall,由於它以雙下劃線打頭,因此它是一個特定於編譯器的擴展,說到底也就是微軟的實現。任何支持開發基於Win32的程序都必須支持這個擴展或其等價物。以__stdcall標識的函數使用了標準調用約定,爲何叫標準約定呢,由於全部的Win32 API(除了個別接受可變參數的除外)都使用它。標準調用約定的函數在它們返回到調用者以前,都會從堆棧中移除掉參數,這也是Pascal的標準約定。但在C/C++中,調用約定是調用者負責清理堆棧,而不是被調用函數;爲強制函數使用C/C++調用約定,可以使用__cdecl。另外,可變參數函數也使用C/C++調用約定。

Windows操做系統採用了標準調用約定(Pascal約定),由於其可減少代碼的體積。這點對早期的Windows來講很是重要,由於那時它運行在只有640KB內存的電腦上。

若是你不喜歡__stdcall,還可使用CALLBACK宏,它定義在windef.h中:安全

#define CALLBACK __stdcallor

#define CALLBACK PASCAL //而PASCAL在此被#defined成__stdcall

做爲回調函數的C++方法

由於平時極可能會使用到C++編寫代碼,也許會想到把回調函數寫成類中的一個方法,但先來看看如下的代碼:服務器

class CCallbackTester
{
public:
int CALLBACK CompareInts(const byte* velem1, const byte* velem2);
};

Bubblesort((byte*)array, 5, sizeof(array[0]),
&CCallbackTester::CompareInts);

若是使用微軟的編譯器,將會獲得下面這個編譯錯誤:app

error C2664: 'Bubblesort' : cannot convert parameter 4 from 'int (__stdcall CCallbackTester::*)(const unsigned char *,const unsigned char *)' to 'int (__stdcall *)(const unsigned char *,const unsigned char *)' There is no context in which this conversion is possible

這是由於非靜態成員函數有一個額外的參數:this指針,這將迫使你在成員函數前面加上static。固然,還有幾種方法能夠解決這個問題,但限於篇幅,就再也不論述了。異步

 

_____________________________函數


        調用(calling)機制從彙編時代起已經大量使用:準備一段現成的代碼,調用者能夠隨時跳轉至此段代碼的起始地址,執行完後再返回跳轉時的後續地址。 CPU爲此準備了現成的調用指令,調用時能夠壓棧保護現場,調用結束後從堆棧中彈出現場地址,以便自動返回。借堆棧保護現場真是一項絕妙的發明,它使調用者和被調者能夠互不相識,因而纔有了後來的函數和構件,使吾輩編程者如此輕鬆愉快。若評選對人類影響最大之發明,在火與車輪以後,筆者當推壓棧調用。
       話雖這樣說,此調用機制並不是完美。回調函數就是一例。函數之類本是爲調用者準備的美餐,其烹製者應對食客瞭如指掌,但實情並不是如此。例如,寫一個快速排序函數供他人調用,其中必包含比較大小。麻煩來了:此時並不知要比較的是何類數據--整數、浮點數、字符串?因而只好爲每類數據製做一個不一樣的排序函數。更通行的辦法是在函數參數中列一個回調函數地址,並通知調用者:君需本身準備一個比較函數,其中包含兩個指針類參數,函數要比較此二指針所指數據之大小,並由函數返回值說明比較結果。排序函數借此調用者提供的函數來比較大小,借指針傳遞參數,能夠全然無論所比較的數據類型被調用者回頭調用調用者的函數(夠咬嘴的),故稱其爲回調(callback)。
        回調函數使程序結構亂了許多。Windows API 函數集中有很多回調函數,儘管有詳盡說明,仍使初學者一頭霧水。恐怕這也是無奈之舉。不管何種事物,能以樹形結構單向描述畢竟讓人舒服些。若是某家族中孫輩又是某祖輩的祖輩,恐怕無人能理清其中的頭緒。但數據處理之複雜每每須要構成網狀結構,非簡單的客戶/服務器關係能窮盡。
       Windows 系統還包含着另外一種更爲普遍的回調機制,即消息機制。消息本是 Windows 的基本控制手段,乍看與函數調用無關,實際上是一種變相的函數調用。發送消息的目的是通知收方運行一段預先準備好的代碼,至關於調用一個函數。消息所附帶的 WParam 和 LParam 至關於函數的參數,只不過比普通參數更通用一些。應用程序能夠主動發送消息,更多狀況下是坐等 Windows 發送消息。一旦消息進入所屬消息隊列,便檢感興趣的那些,跳轉去執行相應的消息處理代碼。操做系統本是爲應用程序服務,由應用程序來調用。而應用程序一旦啓動,卻要反過來等待操做系統的調用。這分明也是一種回調,或者說是一種廣義回調。其實,應用程序之間也能夠造成這種回調。假如進程 B 收到進程 A 發來的消息,啓動了一段代碼,其中又向進程 A 發送消息,這就造成了回調。這種回調比較隱蔽,弄很差會搞成遞歸調用,若缺乏終止條件,將會循環不已,直至把程序搞垮。如果故意編寫成此遞歸調用,並設好終止條件,卻是頗有意思。但這種程序結構太隱蔽,除非十分必要,仍是不用爲好。
       利用消息也能夠構成狹義回調。上面所舉排序函數一例,能夠把回調函數地址換成窗口handle。如此,當須要比較數據大小時,不是去調用回調函數,而是借 API 函數 SendMessage 向指定窗口發送消息。收到消息方負責比較數據大小,把比較結果經過消息自己的返回值傳給消息發送方。所實現的功能與回調函數並沒有不一樣。固然,此例中改成消息純屬畫蛇添腳,反倒把程序搞得很慢。但其餘狀況下並不是老是如此,特別是須要異步調用時,發送消息是一種不錯的選擇。假如回調函數中包含文件處理之類的低速處理,調用方等不得,須要把同步調用改成異步調用,去啓動一個單獨的線程,而後立刻執行後續代碼,其他的事讓線程慢慢去作。一個替代辦法是借 API 函數 PostMessage發送一個異步消息,而後當即執行後續代碼。這要比本身搞個線程省事許多,並且更安全。
       現在咱們是活在一個 object 時代。只要與編程有關,不管何事都離不開 object。但 object 並未消除回調,反而把它發揚光大,弄獲得處都是,只不過大都以事件(event)的身份出現,鑲嵌在某個結構之中,顯得更正統,更容易被人接受。應用程序要使用某個構件,總要先弄清構件的屬性、方法和事件,而後給構件屬性賦值,在適當的時候調用適當的構件方法,還要給事件編寫處理例程,以備構件代碼來調用。何謂事件?它不過是一個指向事件例程的地址,與回調函數地址沒什麼區別。
    不過,此種回調方式比傳統回調函數要高明許多。首先,它把讓人不太舒服的回調函數變成一種天然而然的處理例程,使編程者頓覺氣順。再者,地址是一個危險的東西,用好了可以使程序加速,用很差到處是陷阱,程序隨時都會崩潰。現代編程方式老是想法把地址隱藏起來(隱藏比較完全的如 VB 和 Java),其代價是下降了程序效率。事件例程使編程者無需直接操做地址,但並不會使程序減速。更妙的是,此一改變,本是有損程序結構之奇技怪巧變成一種嶄新設計理念,不只免去被人抨擊,並且逼得吾等凡人淨手更衣,細細研讀,仰慕至今。只是偶然靜心思慮,發覺不過一瓶舊酒而已,故引得此番議論,讓諸君見笑了。 事件驅動程序設計是圍繞着消息基礎造成的,發生一個事件,伴隨着一大堆的消息。
       我理解「回調機制」是window 在執行某個API函數的過程當中,調用指定的一個函數。咱們能夠模擬一下:
假設 ms 提供一個函數叫作  EnumFont ,該函數是獲得全部的字體,假設它的實現是
EnumFont()
{
  while ( (f =FindNextFont()) !=NULL)
  {
       printf("fontname: " + f.name);
  }
}
這樣就循環顯示出全部的字體名稱。可是,開發者可能對字體信息另有用處,那麼如何才能讓開發者能使用這些信息呢,因而作改進:
EnumFont( void*  userFunc )
{
  while ( (f =FindNextFont()) !=NULL)
  {
       printf("fontname: " + f.name);
       if ( userFunc!=NULL)  userFunc( f) ;
  }
}
假設userFunc 是一個函數 void f( FontObject font).這樣使用者只須要定義一個函數:
  void myfunc( FontObject font)
             {
                 listCtrl.Addstring ( font.name);
              }
經過使用 EnumFont ( myfunc) 就能夠將全部額字體信息添加到一個列表框中。那麼咱們稱 myfunc是一個回調函數,即讓某個系統函數調用的函數。所以能夠得出結論:
1 回調函數是由開發者按照必定的原型進行定義的函數
2 回調函數並不禁開發者直接調用執行
3 回調函數一般做爲參數傳遞給系統API,由該API來調用。
4 回調函數可能被系統API調用一次,也可能被循環調用屢次。
好比 函數int EnumFontFamilies(
  HDC hdc,             // handle to device control
  LPCTSTR lpszFamily,  // pointer to family-name string
  FONTENUMPROC lpEnumFontFamProc,
                       // pointer to callback function
  LPARAM lParam        // pointer to application-supplied data
);
其中的   FONTENUMPROC lpEnumFontFamProc就是一個回調函數,該函數遵守格式
int CALLBACK EnumFontFamProc( ENUMLOGFONT FAR *lpelf,  NEWTEXTMETRIC FAR *lpntm, int FontType,  LPARAM lParam )進行定義。
如同mutant所說,回調函數主要用於一些比較費時的操做,或響應不知道什麼時候將會發生的事件,回調函數提供了一種異步的機制,相對於同步執行,提升了效率.前者的例子如WriteFileEx,ReadFileEx等,函數的最後一個參數是一個回調函數的指針,程序中調用WriteFileEx之後,就直接返回了,能夠繼續進行其餘工做,系統在讀寫操做完成後通知程序做善後處理.後者的例子就是windows的事件機制回調函數的另外一個用途,是用於一些枚舉函數,如EnumDisplayModes等,每找到一種支持的顯示模式,就通知回調函數,由回調函數具體處理,這是由於 EnumDisplayModes自己並不知道用戶要如何處理.能,用戶提供回調函數,定製系統的功能,這樣,不一樣的用戶提供不一樣的回調函數,可使系統具備不一樣的功能.這就是所謂的plugin.使用回調函數實際上就是在調用某個函數(一般是API函數)時,將本身的一個函數(這個函數爲回調函數)的地址做爲參數傳遞給那個函數。而那個函數在須要的時候,利用傳遞的地址調用回調函數,這時你能夠利用這個機會在回調函數中處理消息或完成必定的操做。至於如何定義回調函數,跟具體使用的API函數有關,通常在幫助中有說明回調函數的參數和返回值等。

 

 

  歸納起來,回調機制包括兩部分:服務執行者和服務方式制定者。
             1. 服務執行者先制定服務規範
             2.服務方式制定者而後按照規範制定服務方式
             3.而後執行者按照這個方式提供服務
       回調函數的方式是把函數指針的做爲參數傳遞進去,因此規範就是約定函數的參數類型個數。

相關文章
相關標籤/搜索