c++回撥函式(下)

pamxy發表於2013-07-02

轉自:http://blog.csdn.net/xinpo66/article/details/8179898

四無題

    軟體模組之間總是存在著一定的介面,從呼叫方式上,可以把他們分為三類:同步呼叫、回撥和非同步呼叫。同步呼叫是一種阻塞式呼叫,呼叫方要等待對方執行完畢才返回,它是一種單向呼叫;回撥是一種雙向呼叫模式,也就是說,被呼叫方在介面被呼叫時也會呼叫對方的介面;非同步呼叫是一種類似訊息或事件的機制,不過它的呼叫方向剛好相反,介面的服務在收到某種訊息或發生某種事件時,會主動通知客戶方(即呼叫客戶方的介面)。回撥和非同步呼叫的關係非常緊密,通常我們使用回撥來實現非同步訊息的註冊,通過非同步呼叫來實現訊息的通知。同步呼叫是三者當中最簡單的,而回撥又常常是非同步呼叫的基礎。

   

    對於不同型別的語言(如結構化語言和物件語言)、平臺(Win32JDK)或構架(CORBADCOMWebService),客戶和服務的互動除了同步方式以外,都需要具備一定的非同步通知機制,讓服務方(或介面提供方)在某些情況下能夠主動通知客戶,而回撥是實現非同步的一個最簡捷的途徑。

 

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

 

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

 

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

 

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

 

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

 

四常見程式語言的callback分析

 

    1 N/A

 

    2 過程語言中的回撥(C

 

 

    2.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,

    GetCallBackfCallback;

 

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

 

    2.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; //出錯

 

    指標pcallee()的型別不相容,因為它們有不同的呼叫規範。因此不能將被呼叫者的地址賦值給指標p,儘管兩者有相同的返回值和引數列

 

    2.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;

 }

 

 2.4 面嚮物件語言中的回撥(Delphi

 

 DephiC++一樣,為了保持與過程語言Pascal的相容性,它在引入物件導向機制的同時,保留了以前的結構化特性。因此,對回撥的實現,也有兩種截然不同的模式,一種是結構化的函式回撥模式,一種是物件導向的介面模式。

 

附錄博文

 

  簡介

 

  對於很多初學者來說,往往覺得回撥函式很神祕,很想知道回撥函式的工作原理。本文將要解釋什麼是回撥函式、它們有什麼好處、為什麼要使用它們等等問題,在開始之前,假設你已經熟知了函式指標。

 

  什麼是回撥函式?

 

  簡而言之,回撥函式就是一個通過函式指標呼叫的函式。如果你把函式的指標(地址)作為引數傳遞給另一個函式,當這個指標被用為呼叫它所指向的函式時,我們就說這是回撥函式。

 

  為什麼要使用回撥函式?

 

  因為可以把呼叫者與被呼叫者分開。呼叫者不關心誰是被呼叫者,所有它需知道的,只是存在一個具有某種特定原型、某些限制條件(如返回值為int)的被呼叫函式。

 

  如果想知道回撥函式在實際中有什麼作用,先假設有這樣一種情況,我們要編寫一個庫,它提供了某些排序演算法的實現,如氣泡排序、快速排序、shell排序、shake排序等等,但為使庫更加通用,不想在函式中嵌入排序邏輯,而讓使用者來實現相應的邏輯;或者,想讓庫可用於多種資料型別(intfloatstring),此時,該怎麼辦呢?可以使用函式指標,並進行回撥。

 

  回撥可用於通知機制,例如,有時要在程式中設定一個計時器,每到一定時間,程式會得到相應的通知,但通知機制的實現者對我們的程式一無所知。而此時,就需有一個特定原型的函式指標,用這個指標來進行回撥,來通知我們的程式事件已經發生。實際上,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()就稍微複雜一點:

 

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(),同時還傳遞了一個指向回撥函式的指標。

 

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);

 

  如果使用微軟的編譯器,將會得到下面這個編譯錯誤:

 

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。當然,還有幾種方法可以解決這個問題,但限於篇幅,就不再論述了。


相關文章