回調函數設計方法


引入:
 
    你顯示器不亮了,你不知道怎么弄,那你就問在外地干IT的大表哥,你大表哥告訴你修理的方法,然后需要你自己來操作。
    你大表哥知道怎么弄,但是自己不去弄,而是由你去弄。
換句話說,你大表哥實現了修理你顯示器的方法,但他不會自己去調用,而是由你去調用。那么你大表哥告訴你的修機器的方法就是回調函數。
    在這個比喻里,你自己 作為主調方,有實際的需求——修顯示器,但是沒有方法,求教表哥的時候,表哥給你的方法 就是一個 函數地址,當你按照大表哥的方法執行的時候,就是 執行了一個回調函數了。
 
  在工程設計中,尤其在底層庫設計的時候,很多時候,庫的開發者並不能預測今后使 用這段代碼的程序員需要這個函數做具體什么工作,這時候,就需要使用回調設計。
    C 和 C++都提供這類回調支持,C 的建議是使用函數指針,回調實現,C++則通過對基類的繼承,對基類中虛函數的再設計來實現。不過,根據筆者經驗,在這點上,C 的方式比C++方式要輕靈,並且更加靈活。因此,在筆者的工程開發中,一般使用回調函數設計,不太使用虛函數機制。
    回調函數其實就是函數指針的應用,在 C 中,一切數據均可以指針化表示,函數本身 其實也可以,當我們以正確的構型調用一個函數指針時,其效果和直接調用函數本身,完全一樣。
    另外,由於現代操作系統的 C 親密性,很多操作系統級的 api 設計都可以看到回調函數 ,比如我們常見的線程函數,甚至進程本身,其實都是操作系統的回調函數,beginthread 這類啟動線程的調用,一般就是把指定的線程函數指針,在系統的線程表中,注冊一個新的表項,系統下一輪時間循環,自動會根據這個表項,回調該指針,進而實現應用程序線程對時間片的獲取。並且,這個過程,一般都是純 C 的,和 C++無關。
    從某種意義上說,現代並行計算,是建立在 C 的回調模型上的。作為程序員,對於回調函數,應該有很深入的認識,並能熟練應用。
    回調函數的設計非常簡單,不過,這里面首先要搞清楚兩個身份,一個是回調函數的
設計者,一個是使用者,但二者都是程序員。
    在后文中,使用 回調模型設計者和使用者來區分這兩個身份。
 

回調模型設計者:

    作為回調模型的設計者,首先需要定義一個回調函數構型,因為 C 語言就算再靈活, 也需要知道函數原型是怎樣的,才能確保使用者是正確調用,避免崩潰。
typedef void(*_APP_INFO_OUT_CALLBACK)(char* szInfo,void* pCallParam);

 

1、typedef,這是我們顯式定義一種新的變量類型,這個變量類型,就是這一個回調函 數指針的類型。以后使用這個指針的設計者和使用者,都可以使用 _APP_INFO_OUT_CALLBACK 這個變量類型來定義自己的指針變量。
2、本回調函數使用 void 作為返回值,是因為這個特殊應用。其實很多時候,有個約定 ,一般回調函數使用 bool 作為返回值,這在某些循環遍歷的場合,當使用者感到自己的數據已經找到,循環無需繼續,可以返回個 false,設計者就知道,可以不再循環了。這體現出使用者不是完全被動的接受回調,也可以通過返回值影響回調發起方的邏輯。
3、char* szInfo 這是業務數據,這里不再細說。
4、void* pCallParam,這個非常關鍵,所有回調函數的設計者,一定要幫助使用者傳遞 一根 void*的指針,並透傳到每一次回調調用中。
例子:
創建一個支持回調的 類
classCStultzLowDebug
{
public:
CStultzLowDebug(char* szPathName,
char* szAppName,
//構造函數傳入回調函數和參數,可以是 null
_APP_INFO_OUT_CALLBACK pInfoOutCallback=null,
void* pInfoOutCallbackParam=null);
//保存在對象內部,方便 Debug 等功能函數調用
_APP_INFO_OUT_CALLBACK m_pInfoOutCallback;
void* m_pInfoOutCallbackParam;
};

 

構造函數 的具體實現:
CStultzLowDebug::CStultzLowDebug(char* szPathName,
char* szAppName,
_APP_INFO_OUT_CALLBACK pInfoOutCallback,
void* pInfoOutCallbackParam)
{
  m_pInfoOutCallback=pInfoOutCallback;//回調函數指針保存
  m_pInfoOutCallbackParam=pInfoOutCallbackParam;//參數指針保存
//
}

 

設計者 對回調函數 的調用方式
intCStultzLowDebug::Debug2File(char*szFormat,...)
{
//
if(m_pInfoOutCallback)//標准寫法,先判斷指針有效性
{
  m_pInfoOutCallback(szInfoOut,//像函數一樣調用
  m_pInfoOutCallbackParam);//這里在幫助透傳指針
}
//
}

 

 
總結 回調函數的設計的特點:
1、先定義回調函數原型,順便定義一個新的指針變量類型。
2、設計者以該回調函數指針變量類型定義新的變量,實現參數傳遞和數據保存。
3、調用前先檢查指針有效性,避免跳到空指針處,造成崩潰。
 
 

回調模型使用者

 
    作為使用者來說,如果回調函數設計者均基於上述方法設計,其調用程序設計也可以
形成簡單規律和套路。
使用者首先必須以回調函數構型構建一個函數,這就是將來的回調函數實體,設計者
的模塊會跳至此處運行。使用者在這個函數內部,直接使用傳來的變量 szInfo 即可,這就是每次 Debug 模塊輸出的字符串。
void ApplicationInfomationOutCallback(char* szInfo,void* pCallParam);

 

    但有一點注意,如果是 C 里面,可以這樣直接聲明和實現函數即可。但在 C++的類中 ,不能這樣直接寫。這是由於 C++的編譯器,為每一個類成員函數,提供了一個默認的隱含指針 this作為參數,指向本次實例化的對象,其類型就是這個類本身。因此,如下所述,這個函數就不對了。
classCStultzLowDebug
{
private:
    voidApplicationInfomationOutCallback(char* szInfo,void* pCallParam);
};
此時的回調函數原型,由於是類成員函數,有隱含指針,因此相當於如下原型
voidApplicationInfomationOutCallback(
CStultzLowDebug*this,//這是 C++編譯器在編譯時強行添加的
char* szInfo,
void* pCallParam);

 

這時,我們再和回調函數原型比較,發現多了一個 this 指針。 兩個函數不是一個構型,函數指針類型不匹配,調用將會失敗。
因此,所有的回調函數,一旦寫在類里面,必須用 static 修飾為靜態成員函數。
classCStultzLowDebug
{
private:
//請注意這里的 static 修飾
    static void ApplicationInfomationOutCallback(char* szInfo,void* pCallParam);
};

 

    C++規定,對於靜態類成員函數,將不提供隱含的 this 指針,因此,函數的編譯后本體和書寫時的聲明完全一樣,這樣就可以把這個函數作為回調函數。
    但這隨之帶來另外一個問題,就是沒有了 this 指針,使用起來很不方便。我們知道 , C++的面向對象設計中,其對象的核心定義就是“一批數據和針對該數據的所有方法的集 合。這是面向對象程序設計的精髓。
 一個類的成員函數方法,一般說來,都和這個類實例化的對象所包含的數據密
切相關,程序中需要不斷訪問本對象的成員變量或其他成員函數,也就需要頻繁訪問本對象指針 this。   
因此 在C++里,我們可以有如下的解決方案,即 傳參。而 回調函數設計者有義務為使用者透傳一根  void* 的參數指針
 
   在 實際使用時,將this指針 作為 參數,傳給 回調函數,那么 就可以直接使用this ,從而操作 類中的數據了。
 
另外,如果 有 多個參數 需要 傳遞,就需要使用 傳入結構體 指針的方式。
 
 
 
參考文獻:《0 bug C/C++商用工程之道》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM