C語言---回調函數------------->解析 收藏
什麼是回調函數?
簡而言之,回調函數就是一個通過函數指針調用的函數。如果你把函數的指針(位址)作為參數傳遞給另一個函數,當這個指針被用為調用它所指向的函數時,我們就說這是回調函數。
為什麼要使用回調函數?
因為可以把調用者與被調用者分開。調用者不關心誰是被調用者,所有它需知道的,隻是存在一個具有某種特定原型、某些限制條件(如傳回值為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()就稍微複雜一點:
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。當然,還有幾種方法可以解決這個問題,但限于篇幅,就不再論述了 .
2 補充BBS評論
回調到底層次的看法就是:
讓函數去"自主"調用函數,而不是由你決定.
typedef void (*VP)(void);
void Task1()
{
...
}
void Task2()
{
...
}
void EX_CallBack()
{
VP M = NULL;
if (condition)
{
M = Task1;
}
else
{
M = Task2;
}
M();
}
短歌說:它算是一種動态綁定的技術,
主要用于對某一事件的正确響應.
3.聲明函數指針并回調
程式員常常需要實作回調。本文将讨論函數指針的基本原則并說明如何使用函數指針實作回調。注意這裡針對的是普通的函數,不包括完全依賴于不同文法和語義規則的類成員函數(類成員指針将在另文中讨論)。
聲明函數指針
回調函數是一個程式員不能顯式調用的函數;通過将回調函數的位址傳給調用者進而實作調用。要實作回調,必須首先定義函數指針。盡管定義的文法有點不可思議,但如果你熟悉函數聲明的一般方法,便會發現函數指針的聲明與函數聲明非常類似。請看下面的例子:
void f();// 函數原型
上面的語句聲明了一個函數,沒有輸入參數并傳回void。那麼函數指針的聲明方法如下:
void (*) ();
讓我們來分析一下,左邊圓括弧中的星号是函數指針聲明的關鍵。另外兩個元素是函數的傳回類型(void)和由邊圓括弧中的入口參數(本例中參數是空)。注意本例中還沒有建立指針變量-隻是聲明了變量類型。目前可以用這個變量類型來建立類型定義名及用sizeof表達式獲得函數指針的大小:
// 獲得函數指針的大小
unsigned psize = sizeof (void (*) ());
// 為函數指針聲明類型定義
typedef void (*pfv) ();
pfv是一個函數指針,它指向的函數沒有輸入參數,傳回類行為void。使用這個類型定義名可以隐藏複雜的函數指針文法。
指針變量應該有一個變量名:
void (*p) (); //p是指向某函數的指針
p是指向某函數的指針,該函數無輸入參數,傳回值的類型為void。左邊圓括弧裡星号後的就是指針變量名。有了指針變量便可以指派,值的内容是署名比對的函數名和傳回類型。例如:
void func()
{
}
p = func;
p的指派可以不同,但一定要是函數的位址,并且署名和傳回類型相同。
傳遞回調函數的位址給調用者
現在可以将p傳遞給另一個函數(調用者)- caller(),它将調用p指向的函數,而此函數名是未知的:
void caller(void(*ptr)())
{
ptr();
}
void func();
int main()
{
p = func;
caller(p);
}
如果賦了不同的值給p(不同函數位址),那麼調用者将調用不同位址的函數。指派可以發生在運作時,這樣使你能實作動态綁定。
調用規範
到目前為止,我們隻讨論了函數指針及回調而沒有去注意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,盡管兩者有相同的傳回值和參數列
4。函數指針和回調函數
函數指針和回調函數
你不會每天都使用函數指針,但是,它們确有用武之地,兩個最常見的用途是把函數指針作為參數傳遞給另一個函數以及用于轉換表(jump table)。
【警告】簡單聲明一個函數指針并不意味着它馬上就可以使用。和其它指針一樣,對函數指針執行間接通路之前必須把它初始化為指向某個函數。下面的代碼段說明了一種初始化函數指針的方法。
int f(int);
int (*pf)(int)=&f;
第 2 個聲明建立了函數指針 pf ,并把它初始化為指向函數 f 。函數指針的初始化也可以通過一條指派語句來完成。 在函數指針的初始化之前具有 f 的原型是很重要的,否則編譯器就無法檢查 f 的類型是否與 pf 所指向的類型一緻。
初始化表達式中的 & 操作符是可選的,因為函數名被使用時總是由編譯器把它轉換為函數指針。 & 操作符隻是顯式地說明了編譯器隐式執行的任務。
在函數指針被聲明并且初始化之後,我們就可以使用三種方式調用函數:
int ans;
ans=f(25);
ans=(*pf)(25);
ans=pf(25);
第 1 條語句簡單地使用名字調用函數 f ,但它的執行過程可能和你想象的不太一樣。 函數名 f 首先被轉換為一個函數指針,該指針指定函數在記憶體中的位置。然後, 函數調用操作符調用該函數,執行開始于這個位址的代碼。
第 2 條語句對 pf 執行間接通路操作,它把函數指針轉換為一個函數名。這個轉換并不是真正需要的,因為編譯器在執行函數調用操作符之前又會把它轉換回去。不過,這條語句的效果和第1條是完全一樣的。
第 3 條語句和前兩條的效果是一樣的。間接通路并非必需,因為編譯器需要的是一個函數指針。
(一)回調函數
這裡有一個簡單的函數,它用于在單連結清單中查找一個值。它的參數是一個指向連結清單第 1 個節點的指針以及那個需要查找的值。
Node *
search_list(Node *node, int const value)
{
while(node!=NULL){
if( node->value == value )
break;
node = node->link;
}
return node;
}
這個函數看上去相當簡單,但它隻适用于值為整數的連結清單。如果你需要在一個字元串連結清單中查找,你不得不另外編寫一個函數。這個函數和上面那個函數的絕大部分代碼相同,隻是第 2 個參數的類型以及節點值的比較方法不同。
一種更為通用的方法是使查找函數與類型無關,這樣它就能用于任何類型的值的連結清單。我們必須對函數的兩個方面進行修改,使它與類型無關。
首先,我們必須改變比較的執行方式,這樣函數就可以對任何類型的值進行比較。這個目标聽上去好像不可能,如果你編寫語句用于比較整型值,它怎麼還可能用于其它類型如字元串的比較呢? 解決方案就是使用函數指針。調用者編寫一個比較函數,用于比較兩個值,然後把一個指向此函數的指針作為參數傳遞給查找函數。而後查找函數來執行比較。使用這種方法,任何類型的值都可以進行比較。
我們必須修改的第 2 個方面是向比較函數傳遞一個指向值的指針而不是值本身。比較函數有一個 void * 形參,用于接收這個參數。然後指向這個值的指針便傳遞給比較函數。(這個修改使字元串和數組對象也可以被使用。字元串和數組無法作為參數傳遞給函數,但指向它們的指針卻可以。)
使用這種技巧的函數被稱為回調函數(callback function),因為使用者把一個函數指針作為參數傳遞其它函數,後者将”回調“使用者的函數。任何時候,如果你所編寫的函數必須能夠在不同的時刻執行不同類型的工作或者執行隻能由函數調用者定義的工作,你都可以使用這個技巧。
【提示】
在使用比較函數的指針之前,它們必須被強制轉換為正确的類型。因為強制類型轉換能夠躲開一般的類型檢查,是以你在使用時必須格外小心,確定函數參數類型是正确的。
在這個例子裡,回調函數比較兩個值。查找函數向比較函數傳遞兩個指向需要進行比較的值的指針,并檢查比較函數的傳回值。例如:零表示相等的值,現在查找函數就與類型無關,因為它本身并不執行實際的比較。确實,調用者必須編寫必需的比較函數,但這樣做是很容易的,因為調用者知道連結清單中所包含的值的類型。如果使用幾個分别包含不同類型值的連結清單,為每種類型編寫一個比較函數就允許單個查找函數作用于所有類型的連結清單。
程式段01 是類型無關的查找函數的一種實作方法。 注意函數的第 3 個參數是一個函數指針。這個參數用一個完整的原型進行聲明。同時注意雖然函數絕不會修改參數 node 所指向的任何節點,但 node 并未被聲明為 const 。如果 node 被聲明為 const,函數将不得不傳回一個const結果,這将限制調用程式,它便無法修改查找函數所找到的節點。
#include
#include "node.h"
Node *
search_list( Node *node, void const *value, int (*compare)( void const *, void const *) )
{
while (node!=NULL){
if(compare(&node->value, value)==0)
break;
node=node->link;
}
return node;
}
指向值參數的指針和 &node->value 被傳遞給比較函數。後者是我們目前所檢查的節點值。
在一個特定的連結清單中進行查找時,使用者需要編寫一個适當的比較函數,并把指向該函數的指針和指向需要查找的值的指針傳遞給查找函數下面是一個比較函數,它用于在一個整數連結清單中進行查找。
int
compare_ints( void const *a, void const *b )
{
if( *(int *)a == *(int *)b )
return 0;
else
return 1;
}
這個函數像下面這樣使用:
desired_node = search_list ( root, &desired_value, compare_ints );
注意強制類型轉換:比較函數的參數必須聲明為 void * 以比對查找函數的原型,然後它們再強制轉換為 int * 類型,用于比較整型值。
如果你希望在一個字元串連結清單中進行查找,下面的代碼可以完成這項任務:
#include
...
desired_node = search_list( root, "desired_value", strcmp);
碰巧,庫函數 strcmp 所執行的比較和我們需要的完全一樣,不過有些編譯器會發出警告資訊,因為它的參數被聲明為 char * 而不是
void *。
(二)轉移表
轉換表最好用個例子來解釋。下面的代碼段取自一個程式,它用于實作一個袖珍式電腦。程式的其他部分已經讀入兩個數(op1和op2)和一個操作數(oper)。下面的代碼對操作符進行測試,然後決定調用哪個函數。
switch( oper ){
case ADD:
result = add( op1, op2);
break;
case SUB:
result = sub( op1, op2);
break;
case MUL:
result = mul( op1, op2);
break;
case DIV:
result = div( op1, op2);
break;
......
對于一個新奇的具有上百個操作符的電腦,這條switch語句将非常長。
為什麼要調用函數來執行這些操作呢? 把具體操作和選擇操作的代碼分開是一種良好的設計方法,更為複雜的操作将肯定以獨立的函數來實作,因為它們的長度可能很長。但即使是簡單的操作也可能具有副作用,例如儲存一個常量值用于以後的操作。
為了使用 switch 語句,表示操作符的代碼必須是整數。如果它們是從零開始連續的整數,我們可以使用轉換表來實作相同的任務。轉換表就是一個函數指針數組。
建立一個轉換表需要兩個步驟。首先,聲明并初始化一個函數指針數組。唯一需要留心之處就是確定這些函數的原型出現在這個數組的聲明之前。
double add (double,double);
double sub (double,double);
double mul (double,double);
double div (double,double);
......
double ( *oper_func[] )( double, double)={
add,sub,mul,div,...
};
初始化清單中各個函數名的正确順序取決于程式中用于表示每個操作符的整型代碼。這個例子假定ADD是0 ,SUB是1,MUL是2,依次類推。
第 2 個步驟是用下面這條語句替換前面整條 switch 語句!
result = oper_func[ oper ]( op1,op2 );
oper從數組中選擇正确的函數指針,而函數調用操作符執行這個函數。
本文來自CSDN部落格,轉載請标明出處:http://blog.csdn.net/hejianhua/archive/2008/11/07/3249586.aspx