1. 程式人生 > >淺談C++回撥函式

淺談C++回撥函式

  1.什麼是回撥函式?

       回撥函式就是一個通過函式指標呼叫的函式。如果你把函式的指標(地址)作為引數傳遞給另一個函式,當這個指標被用來呼叫其所指向的函式時,我們就說這是回撥函式。回撥函式不是由該函式的實現方直接呼叫,而是在特定的事件或條件發生時由另外的一方呼叫的,用於對該事件或條件進行響應。

      簡單總結:什麼是回撥函式?——就是由宣告函式的類來呼叫的函式叫做回撥函式。普通函式可以讓任何類呼叫。“回撥”的主語是誰?——宣告“回撥函式”的那個類。Block、委託、通知、回撥函式,它們雖然名字不一樣,但是原理都一樣,都是“回撥機制”的思想的具體實現!

2.回撥函式的機制

      (1)定義回撥函式原型;(2)提供函式實現的一方在初始化的時候,將回調函式註冊給呼叫者;(3)當特定的事件或條件發生的時候,呼叫者使用函式指標呼叫函式對事件進行處理。

3.回撥函式的意義

       因為可以把呼叫者和被呼叫者分開,所以呼叫者不關心誰是被呼叫者。它只需知道存在一個具有特定原型和限制條件的被呼叫函式。簡而言之,回撥函式就是允許使用者把需要呼叫的方法的指標作為引數傳遞給一個函式,以便該函式在處理相似事件的時候可以靈活的使用不同的方法。

4.回撥函式的作用

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

       回撥可用於通知機制。例如,有時要在A程式中設定一個計時器,每到一定時間,A程式會得到相應的通知,但通知機制的實現者對A程式一無所知。那麼,就需一個具有特定原型的函式指標進行回撥,通知A程式事件已經發生。實際上,API使用一個回撥函式SetTimer()來通知計時器。如果沒有提供回撥函式,它還會把一個訊息發往程式的訊息佇列。

        再比如,假設有A、B兩個類。
(1)A類有多種形態,要在B類中實現回撥函式。如假設A類是網路請求開源類ASIHttpRequest,它可能請求成功,也可能請求失敗。這個時候,B類就要針對以上兩個情況,作不同的處理。
(2)A類的形態由B類決定時,要在B類中實現回撥函式。如UITableView類就會提供很多回調函式(iOS專業術語稱“委託”方法)

(3)A類需要向B類傳遞資料時,可以在B類中實現回撥函式(A類一般是資料層比較耗時的操作類)。如舉的那個發工資的例子。在實際程式設計中,這樣的機制有個好處就是可以提升使用者的操作體驗。比如使用者從X頁面跳轉到Y頁面,需要向網路請求資料,而且比較耗時,那我們怎麼辦?有三種方案:第一種就是在X頁面展示一個旋轉指示器,當收到網路傳回的資料時,在展現Y頁面。第二種就是使用回撥函式。使用者從X頁面直接跳轉到Y頁面,Y頁面需要到資料讓資料層去執行,當收到資料時,再在Y頁面展現。第三種就是在Y頁面中開啟多執行緒。讓一個子執行緒專門到後臺去取資料。綜合來說,第二種更加簡介易懂,而且程式碼緊湊。

         不管怎麼說,回撥函式是繼承自C語言的。在C++中,應只在與C程式碼建立介面或與已有的回撥介面打交道時,才使用回撥函式。除了上述情況,在C++中應使用虛方法或仿函式(functor),而不是回撥函式。

5.程式碼示例

typedef int(__stdcall*CompareFunction)(constbyte*,constbyte*)

     它就是回撥函式的型別,負責用同樣的引數形式將引數傳遞給相應的具體元素比較函式。另外,通過它,兩個不同的排序演算法,可以呼叫和具體元素相關的比較函式,實現和元素型別無關的排序:Bubblesort()和Quicksort(),這兩個方法都用同樣的引數原型,但實現了不同的排序演算法。

void DLLDIR__stdcallBubblesort(byte*array,intsize,intelem_size,CompareFunctioncmpFunc);
void DLLDIR__stdcallQuicksort(byte*array,intsize,intelem_size,CompareFunctioncmpFunc);
這兩個函式接受以下引數:
·byte * array:指向元素陣列的指標(任意型別)。
·int size:陣列中元素的個數。
·int elem_size:陣列中一個元素的大小,以位元組為單位。

·CompareFunction cmpFunc:帶有上述原型的指向回撥函式的指標。

這兩個函式都會對陣列進行某種排序,但每次都需決定兩個元素哪個排在前面,而函式中有一個回撥函式,其地址是作為一個引數傳遞進來的。對編寫者來說,不必介意函式在何處實現,或它怎樣被實現的,所需在意的只是兩個用於比較的元素的地址,並返回以下的某個值(庫的編寫者和使用者都必須遵守這個約定):
·-1:如果第一個元素較小,那它在已排序好的陣列中,應該排在第二個元素前面。
·0:如果兩個元素相等,那麼它們的相對位置並不重要,在已排序好的陣列中,誰在前面都無所謂。
·1:如果第一個元素較大,那在已排序好的陣列中,它應該排第二個元素後面。

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

void DLLDIR__stdcall Bubblesort(byte*array,intsize,intelem_size,cmpFunc)
{
for(inti=0;i<size;i++)
{
for(intj=0;j<size-i-1;j++)
{
//回撥比較函式
if(1==(*cmpFunc)(array+j*elem_size,array+(j+1)*elem_size))
{
//兩個相比較的元素相交換
byte* temp=newbyte[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(constbyte*velem1,constbyte*velem2)
{
int elem1=*(int*)velem1;
int elem2=*(int*)velem2;
if(elem1<elem2)
return-1;
if(elem1>elem2)
return1;
return0;
}
int __stdcall CompareStrings(constbyte*velem1,constbyte*velem2)
{
const char* elem1=(char*)velem1;
const char* elem2=(char*)velem2;
return strcmp(elem1,elem2);
}

下面另有一個程式,用於測試以上所有的程式碼,它傳遞了一個有5個元素的陣列給Bubblesort()和Quicksort(),同時還傳遞了一個指向回撥函式的指標。(使用byte型別需包含標頭檔案windows.h,或:

typedef unsignedchar bute;
int main(intargc,char*argv[])
{
int i;
int array[]={5432,4321,3210,2109,1098};
cout<<"BeforesortingintswithBubblesort\n";
for(i=0;i<5;i++)
cout<<array[i]<<'\n';
Bubblesort((byte*)array,5,sizeof(array[0]),&CompareInts);
cout<<"Afterthesorting\n";
for(i=0;i<5;i++)
cout<<array[i]<<'\n';
const char str[5][10]={"estella","danielle","crissy","bo","angie"};
cout<<"BeforesortingstringswithQuicksort\n";
for(i=0;i<5;i++)
cout<<str[i]<<'\n';
Quicksort((byte*)str,5,10,&CompareStrings);
cout<<"Afterthesorting\n";
for(i=0;i<5;i++)
cout<<str[i]<<'\n';
return0;
}
如果想進行降序排序(大元素在先),就只需修改回撥函式的程式碼,或使用另一個回撥函式,這樣程式設計起來靈活性就比較大了。

上面的程式碼中,可在函式原型中找到__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 CALLBACKPASCAL//而PASCAL在此被#defined成__stdcall
作為回撥函式的C++方法

因為平時很可能會使用到C++編寫程式碼,也許會想到把回撥函式寫成類中的一個方法,但先來看看以下的程式碼:

class CCallbackTester
{
public:
int CALLBACKCompareInts(constbyte*velem1,constbyte*velem2);
};
Bubblesort((byte*)array,5,sizeof(array[0]),&CCallbackTester::CompareInts);
如果使用微軟的編譯器,將會得到下面這個編譯錯誤:

errorC2664:’Bubblesort’:cannotconvertparameter4from’int(__stdcallCCallbackTester::*)(constunsignedchar*,constunsigne)

這是因為非靜態成員函式有一個額外的引數:this指標,這將迫使你在成員函式前面加上static。

6.使用回撥函式的好處

(1)可以讓實現方,根據回撥方的多種形態進行不同的處理和操作。(ASIHttpRequest)
(2)可以讓實現方,根據自己的需要定製回撥方的不同形態。(UITableView)
(3)可以將耗時的操作隱藏在回撥方,不影響實現方其它資訊的展示。
(4)讓程式碼的邏輯更加集中,更加易讀。