您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關c/c++如何回調函數,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
1:函數名為指針
首先,在C語言中函數是一種function-to-pointer的方式,即對于一個函數,會將其自動轉換成指針的類型.如:
1 #include<stdio.h> 2 3 void fun() 4 { 5 } 6 7 int main() 8 { 9 printf("%p %p %p\n", &fun, fun, *fun);10 return 0;11 }
這三個值的結果是一樣的. 其實對于最后的那個*fun, 即使前面加上很多個*號, 其結果也不變, 即**fun, ***fun的結果都是一樣的. 對于這個問題, 因為之前講過函數是一種function-to-pointer方式, 其會自動轉換成指針的類型, &fun是該函數的地址, 為指針類型, fun是一個函數, 會轉換成其指針類型, 而對于*fun, 由于fun已經變成了指針類型,
指向這個函數, 所以*fun就是取這個地址的函數, 而又根據function-to-pointer, 該函數也轉變成了一個指針, 所以以此類推, 這三個值的結果是相同的.
2:回調函數
通過將回調函數的地址傳給調用者從而實現動態調用不同的函數。因此當我們想通過一個統一接口實現不同的內容,這時用回掉函數非常合適。
若要實現回調函數,最關鍵的是要把調用函數的參數定義為函數指針類型。函數指針的定義這里稍
微提一下。比如:
int (*ptr)(void); 這里ptr是一個函數指針,其中(*ptr)的括號不能省略,因為括號的優先級高于星號,那樣就成了一個返回類型為整型的函數聲明了。int為返回類型,括號內為函數的參數。
下面通過一個例子來解釋回調函數的用法:
1 #include <stdlib.h> 2 #include <stdio.h> 3 int Test1(int num) 4 { 5 printf("i am test1,the data is %d \n",num); 6 return 0; 7 } 8 int Test2(int num) 9 { 10 printf("i am test2,the data is %d\n",num);11 return 0; 12 } 13 14 int Caller(int (*ptr)(int n),int n)//指向函數的指針作函數參數,這里第二個參數是函數指針的參數 15 { //不能寫成void Caller2(int (*ptr)(int n)),這樣的定義語法錯誤。 16 int a=(*ptr)(n); 17 return a; 18 } 19 int main() 20 { 21 22 Caller(Test1,20); 23 printf("************************\n"); 24 Caller(Test2,10);25 26 return 0; 27 }
下面介紹幾種比較容易混淆的指針概念:
1:函數指針
1:函數指針的定義方式:
返回值類型 (* 指針變量名)(形參列表);
返回值為指針的函數定義: 返回指針類型 * 函數名(形參列表);
2:函數指針的賦值:
在賦值時,可以直接將函數指針指向函數名(函數名即代表該段代碼的首地址),但是前提是:函數指針和它指向的函數的參數個數以及類型必須一致。函數指針的返回值類型與函數的返回值類型必須一致。
3:通過函數指針調用函數:
加上指針f指向函數func。(*f ) 和 func代表同一函數。
使用方法如下:
聲明函數指針:int (*f)(int x);
函數指針賦值: f=func ( int func(int x));
函數指針調用函數: (*f)(x) (x為整型變量)
2:函數指針數組
函數指針數組是一個其元素是函數指針的數組。即,此數據結構是是一個數組,且其元素是一個指向函數入口地址的指針。
定義方式: 返回值 ( *數組名[個數]) (參數列表)
3:指向數組的指針
類型 (*變量名)[元素個數]
4: 指針數組
類型 *變量名[元素個數]
因為[] 比*具有更好的優先級。所以如果是變量a先和*結合則表示其為一個指針,如果a先和[]結合,則表示是一個數組。
帶參數的回調函數:
//定義帶參回調函數void PrintfText(char* s) { printf(s);}//定義實現帶參回調函數的"調用函數"void CallPrintfText(void (*callfuct)(char*),char* s){ callfuct(s);}//在main函數中實現帶參的函數回調int main(int argc,char* argv[]){ CallPrintfText(PrintfText,"Hello World!\n"); return 0;}
非靜態成員函數作回調函數
當然如果是靜態成員函數就好辦跟全局函數是類似,到此為止世界還沒有變亂,如在VC編程中用AfxBeginThread開啟一個線程,就經常將參數AFX_THREADPROC pfnThreadProc定義為一個全局函數或靜態成員函數,可是這兩個都不方便訪問類的非靜態成員,之所以鄭重其事地寫這篇文章,就是以前靜態回調用起來非常不爽。
回調函數是非靜態成員函數呢?我們可不能簡單地設為這樣:
class CCallback這樣編譯就不會通過的,因為非靜態的成員函數必須通過對象來訪問,好,我們稍稍改進一下:
class CCallbackint main(int argc, char* argv[])
{
CCallback obj;
Caller(&obj,&CCallback::Func);
}
即給Caller多傳個對象進去,好吧,貌似問題解決了,可是,調用者(如庫的提供商)只知道回調函數接口長這樣而已,事先全然不知客戶的類是如何定義,終于模板登上場了:
template<typename T>其他不變的,把調用者這里換成模板就OK了,當然這個Caller也可以是成員函數,現在用這個方法寫個小應用是沒什么問題了,但是限制多多,如調用者一次只調用了一個實現,但現實情況往往是產生某個事件時,應該依次調用多個行為,即把掛在這個事件上的所有回調函數通通臨幸一遍,還有回調是如此的重要,以至于C#不用庫在語言本身層面就實現了它,我們也不可以到此草草了事,而是按照組件化的思維提供一套完善的回調機制,所謂完善,如上個例子中Caller只能接收一個參數為int,返回值為void的成員函數指針,等等,必須是這樣的接口嗎,想想參數為double行不行,如void (T::*p)(double)這樣的函數傳給它可以嗎,int不是可自動轉換為double嗎,那這個函數指針也能自動轉換嗎,就像C#中的協變與逆變一樣,不行,C++不允許,當然我們可以強制轉換,不過要在十分清楚類型的情況下才能這么做,否則因為不是類型安全的很容易引起程序錯誤甚至崩潰。所以要支持各種參數,多個參數,還得模板,嗯嗯,努力尚未成功,同志還需革命!
多態回調
甭管什么名詞,總之我們的目的是:產生某個事件時,調用某個待客戶實現的行為,調用者什么時候調用確定了,關鍵是客戶按照規定接口實現這個行為,這聽起來有點像多態了,是的,有時候被調用者與調用者是繼承關系,這就不需要其它理論了,就多態唄,不過多態不一定非得用虛函數來實現,就像MFC一樣,考慮到每個類背負一個龐大的虛函數表會帶來很大的性能損失,換做用幾個結構體和強大的宏而實現消息映射。在wincore.cpp中,CWnd::OnWndMsg源碼里,當來了消息,在事先建立的鏈表中從派生類依次向上查找第一個實現了這個消息的類的AFX_MSGMAP結構體,再取得它的AFX_MSGMAP_ENTRY成員,即真正的消息入口地址,
struct AFX_MSGMAP_ENTRY 就類似于寫一個普通的鏈表結構:struct list_node{list_node* next; int data},只不過這里的鏈表的next不能再隨便指,要指向基類的節點,根據next指針找到對應的節點后取出數據data成員即可,在這里,data就是AFX_MSGMAP_ENTRY,如上圖,AFX_MSGMAP_ENTRY里定義了消息標號即各種附加參數,還有最關鍵的成員pfn,代表了事先派生類通過宏填充好的回調成員函數地址。但是pfn的類型即AFX_PMSG定義為typedef void (AFX_MSG_CALL
CCmdTarget::*AFX_PMSG)(void); 只能代表一種類型,而客戶的派生類的為響應消息的回調函數的類型有很多種,在框架中如何保證以正確的形式調用呢?原來客戶在填充消息標號和函數地址時,也順便填充好了函數類型交給nSig成員保存,根據nSig,如前文所說,將pfn強制轉換到相應的類型就OK了,不過這成員函數指針轉換來轉換去,代碼非常難看啊可讀性不強,于是使用union進行類型轉換:
// specific type safe variants for WM_COMMAND and WM_NOTIFY messages
void (AFX_MSG_CALL CCmdTarget::*pfn_COMMAND)();
BOOL (AFX_MSG_CALL CCmdTarget::*pfn_bCOMMAND)();
void (AFX_MSG_CALL CCmdTarget::*pfn_COMMAND_RANGE)(UINT);
BOOL (AFX_MSG_CALL CCmdTarget::*pfn_COMMAND_EX)(UINT);
...
}
//wincore.cpp CWnd::OnWndMsg
union MessageMapFunctions mmf;
mmf.pfn = lpEntry->pfn;
nSig = lpEntry->nSig;
switch (nSig)
{
default:
ASSERT(FALSE);
break;
case AfxSig_bD:
lResult = (this->*mmf.pfn_bD)(CDC::FromHandle((HDC)wParam));
break;
case AfxSig_bb: // AfxSig_bb, AfxSig_bw, AfxSig_bh
lResult = (this->*mmf.pfn_bb)((BOOL)wParam);
break;
case AfxSig_bWww: // really AfxSig_bWiw
lResult = (this->*mmf.pfn_bWww)(CWnd::FromHandle((HWND)wParam),
(short)LOWORD(lParam), HIWORD(lParam));
break;
...
}
當然這里只是一個小插曲而已,它只是MFC為滿足于自己應用設計這么一套機制,派生類的回調函數類型是有限的,再則要求與框架類是繼承關系,如果沒有繼承關系怎么辦,例如當產生串口或者網口收到數據的事件時,需要更新UI界面,UI界面與串口類可是沒有絲毫繼承關系的,呃...鐵人王進喜說:有條件要上,沒條件創造條件也要上,我們大不了專門定義一個回調抽象類,讓UI界面繼承自它,實現類里的回調函數,然后串口類通過抽象類型對象指針就可以多態地調用到UI的真正回調實現。COM/ATL的回調,Java的回調就是這么干。不過在C++中,情形有些不一樣,這樣實現很勉強,它需要多重繼承,仍然不能直接實現同時調用多個行為,耦合性高,每個回調都需要單獨定義一個類(只要接口不一樣),效率也不夠高,我們想直接調用到綁定好的回調,基于這些缺點,還得尋找更好的方法。
信號與槽(Signal/Slots)
說了這么多,終于來到正題了,在C++中,信號與槽才是回調的完美解決方案,其實本質上是一個觀察者模式,包括其它的叫法:delegate,notifier/receiver,observer,C#中的delegate也是一個觀察者的實現。Qt中提供了信號與槽的整套機制,任何對象的槽可以綁定到另一個對象的信號上,一個信號可以擁有多個槽,經典的圖例如下:
可是qt中的實現用了signal slot關鍵字,不是C++標準的啊,其它編譯器不能隨便編譯(好像先經過qmake生成標準的代碼就可以了),直接上源碼不妥得搞清楚為什么,一切從最簡單的入手,我們先來用標準C++實現一個簡易的signal/slots,如何實現呢,說白了,就是想方設法把回調函數信息保存起來,必要時利用它就OK了,回調函數信息就兩個,類對象指針與成員函數地址,我們將這對信息存儲到名叫slot的類中,而在signal類中,維護多個slot即可,仍然用帶一個int參數,返回值為void的函數接口:
#include <vector>template<typename T, typename T1>
class slot
{
public:
slot(T* pObj,void (T::*pMemberFunc)(T1))
{
m_pObj=pObj;
m_pMemberFunc=pMemberFunc;
}
void Execute(T1 para)
{
(m_pObj->*m_pMemberFunc)(para);
}
private:
T* m_pObj;
void (T::*m_pMemberFunc)(T1);
};
template<typename T, typename T1>
class signal
{
public:
void bind(T* pObj,void (T::*pMemberFunc)(T1 para))
{
m_slots.push_back(new slot<T,T1>(pObj,pMemberFunc));
}
~signal()
{
vector<slot<T,T1>* >::iterator ite=m_slots.begin();
for (;ite!=m_slots.end();ite++)
{
delete *ite;
}
}
void operator()(T1 para)
{
vector<slot<T,T1>* >::iterator ite=m_slots.begin();
for (;ite!=m_slots.end();ite++)
{
(*ite)->Execute(para);
}
}
private:
vector<slot<T,T1>* > m_slots;
};
class receiver
{
public:
void callback1(int a)
{
cout<<"receiver1: "<<a<<endl;
}
void callback2(int a)
{
cout<<"receiver2: "<<a<<endl;
}
};
class sender
{
public:
sender(): m_value(0) {}
int get_value()
{
return m_value;
}
void set_value(int new_value)
{
if (new_value!=m_value)
{
m_value=new_value;
m_sig(new_value);
}
}
signal<receiver,int> m_sig;
private:
int m_value;
};
int main(int argc,char** arg)
{
receiver r;
sender s;
s.m_sig.bind(&r,&receiver::callback1);
s.m_sig.bind(&r,&receiver::callback2);
s.set_value(1);
return 0;
}
程序在VC6下順利通過,這個版本相比前面所說的繼承手法耦合性低了,被調用者receiver與規定函數接口的slot類沒有任何關系,但仔細以觀察這個程序在概念上是有問題的,signal類有兩個模板參數,一個是類的類型,一個是函數參數類型,如果把這個signal/slots組件提供出去,使用者如上面的sender類不免會有個疑慮:在實例化signal類型時,必須提供這兩個模板參數,可是調用方事先哪就一定知道接收方(receiver)的類型呢,而且從概念上講事件發送方與接收方只需遵循一個共同的函數接口就可以了,與類沒什么關系,上個程序要求在實例化時就得填充receiver的類型,也就決定了它與receiver只能一對一,而不能一對多,于是作此改進:將signal的參數T去掉,將T類型的推導延遲到綁定(bind)時,signal沒有參數T,signal的成員slot也就不能有,那slot的成員也就不能有,可是,參數T總得找個地方落腳啊,怎么辦?有個竅門:讓slot包含slotbase成員,slotbase沒有參數T的,但slotbase只定義接口,真正的實現放到slotimpl中,slotimpl就可以掛上參數T了,boost中any、shared_ptr就是用此手法,改進后全部代碼如下:
#include <vector>template<typename T1>
class slotbase
{
public:
virtual void Execute(T1 para)=0;
};
template<typename T,typename T1>
class slotimpl : public slotbase<T1>
{
public:
slotimpl(T* pObj,void (T::*pMemberFunc)(T1))
{
m_pObj=pObj;
m_pMemberFunc=pMemberFunc;
}
virtual void Execute(T1 para)
{
(m_pObj->*m_pMemberFunc)(para);
}
private:
T* m_pObj;
void (T::*m_pMemberFunc)(T1);
};
template<typename T1>
class slot
{
public:
template<typename T>
slot(T* pObj,void (T::*pMemberFunc)(T1))
{
m_pSlotbase=new slotimpl<T,T1>(pObj,pMemberFunc);
}
~slot()
{
delete m_pSlotbase;
}
void Execute(T1 para)
{
m_pSlotbase->Execute(para);
}
private:
slotbase<T1>* m_pSlotbase;
};
template<typename T1>
class signal
{
public:
template<typename T>
void bind(T* pObj,void (T::*pMemberFunc)(T1 para))
{
m_slots.push_back(new slot<T1>(pObj,pMemberFunc));
}
~signal()
{
vector<slot<T1>* >::iterator ite=m_slots.begin();
for (;ite!=m_slots.end();ite++)
{
delete *ite;
}
}
void operator()(T1 para)
{
vector<slot<T1>* >::iterator ite=m_slots.begin();
for (;ite!=m_slots.end();ite++)
{
(*ite)->Execute(para);
}
}
private:
vector<slot<T1>* > m_slots;
};
#define CONNECT(sender,signal,receiver,slot) sender.signal.bind(receiver,slot)
class receiver
{
public:
void callback1(int a)
{
cout<<"receiver1: "<<a<<endl;
}
};
class receiver2
{
public:
void callback2(int a)
{
cout<<"receiver2: "<<a<<endl;
}
};
class sender
{
public:
sender(): m_value(0) {}
int get_value()
{
return m_value;
}
void set_value(int new_value)
{
if (new_value!=m_value)
{
m_value=new_value;
m_valueChanged(new_value);
}
}
signal<int> m_valueChanged;
private:
int m_value;
};
int main(int argc,char** arg)
{
receiver r;
receiver2 r2;
sender s;
CONNECT(s,m_valueChanged,&r,&receiver::callback1);
CONNECT(s,m_valueChanged,&r2,&receiver2::callback2);
s.set_value(1);
return 0;
}
這個版本就比較像樣了,一個signal可與多個slots連接,增加了類似QT的connect,用宏實現#define CONNECT(sender,signal,receiver,slot) sender.signal.bind(receiver,slot),這樣使用者就非常方便,而且現在已完全解耦,sender只管定義自己的signal,在恰當時機用仿函數形式調用即可,而receiver只管實現callback,互不影響,可獨立工作,如果需要再通過CONNECT將它們連接起來即可,已經很組件化了,可是離真正的工程應用尚有一段距離,如它不能接收全局函數或靜態成員函數或仿函數為回調函數,不能帶兩個或更多的函數參數,最后一步了。
關于c/c++如何回調函數就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。