在C裡面,經常需要提供一個函數地址,注冊到結構裡,然後在程序執行到特定階段時,回調該函數。創建線程,注冊線程運行的主函數就是一個典型的例子。這裡以簡單的回調實例,說明C++中回調函數為成員函數時有關this指針的問題。由於C++對C的繼承關系,C++沒有自己的線程封裝技術,一般而言我們創建線程時,還是用C的回調函數機制。類似的例子也挺多的。在Java等純粹的面向對象語言,則不一樣,不光有自己的獨立的線程類型,對於回調,也是注冊整個對象,而不是注冊一個方法,如常用的觀察者模式。這裡,在網上查閱了大量關於this指針、類成員函數和靜態成員函數的相關知識點,結合自己的理解作一些總結。
關於回調函數,類的成員函數作為回調函數,一般而言大家已經形成了編程范式,討論一些生僻的用法,可能被認為是腐朽的,無價值的。這裡只想客觀分析一下技術點,思想可能在類似的場景中遇到也說不准。
通常我們理解的成員函數和this指針是:
《深入探索C++對象模型》中提到成員函數時,當成員函數不是靜態的,虛函數,那麼我們有以下結論:
(1) &類名::函數名 獲取的是成員函數的實際地址;
(2) 對於函數x來講obj.x()編譯器轉化後表現為x(&obj),&obj作為this指針傳入;
(3) 無法通過強制類型轉換在類成員函數指針與其外形幾乎一樣的普通函數指針之間進行有效的轉換。
通常我們理解的是類的普通成員函數有一個隱藏的參數,即第一個參數,其值是this。如果希望一個成員函數既能訪問類的數據成員,又能作為回調函數,有如下幾種方法:
1、靜態成員函數作為回調函數
為了不失封裝性,可以將需要作為回調的函數聲明為靜態的。靜態的成員函數,可以直接在類的外部調用。我們知道靜態成員函數是不能直接訪問類的非靜態數據和接口的。那麼此時需要知道具體的對象地址或者引用才能訪問具體的對象成員。又有兩個方法能實現這個:
1)將對象的地址用全局變量記錄,在靜態成員函數中通過該全局變量訪問數據成員和方法。來看具體的代碼實例:
#include <stdio.h> #include <stdlib.h> typedef void (*func)(void*); class CallBack; class CallBackTest; CallBack* g_obj = NULL; CallBackTest* g_test = NULL; class CallBackTest { public: CallBackTest() { m_fptr = NULL; m_arg = NULL; } ~CallBackTest() { } void registerProc(func fptr, void* arg = NULL) { m_fptr = fptr; if (arg != NULL) { m_arg = arg; } } void doCallBack() { m_fptr(m_arg); } private: func m_fptr; void* m_arg; }; class CallBack { public: CallBack(CallBackTest* t) : a(2) { if (t) { t->registerProc((func)display); } } ~CallBack() { } static void display(void* p) { if (g_obj) { g_obj->a++; printf("a is: %d", g_obj->a); } } private: int a; }; int main(int argc, char** argv) { g_test = new CallBackTest(); g_obj = new CallBack(g_test); g_test->doCallBack(); return 0; }
如上代碼,實現對CallBack成員函數的回調。在callback類的構造函數中注冊靜態的成員函數到callbacktest類中。如果對該代碼稍加該井,可以將g_obj變量放在callback類裡面,作為一個靜態成員,這樣就更好了。更優雅的,將g_obj作為display的參數傳入,就更好了。於是有了我們通常的做法,將成員函數聲明為靜態的,帶一個參數,是其所在的類的對象指針,這樣我們可以在注冊的時候將this指針傳遞給靜態成員函數,使用起來就好像是靜態的成員函數有了this指針一樣。
#include <stdio.h> #include <stdlib.h> typedef void (*func)(void*); class CallBack; class CallBackTest; class CallBackTest { public: CallBackTest() { } ~CallBackTest() { } void registerProc(func fptr, void* arg = NULL) { m_fptr = fptr; if (arg != NULL) { m_arg = arg; } } void doCallBack() { m_fptr(m_arg); } private: func m_fptr; void* m_arg; }; class CallBack { public: CallBack(CallBackTest* t) : a(2) { if (t) { t->registerProc((func)display, this); } } ~CallBack() { } static void display(void* _this = NULL) { if (!_this) { return; } CallBack* pc = (CallBack*)_this; pc->a++; printf("a is: %d", pc->a); } private: int a; }; int main(int argc, char** argv) { CallBackTest* cbt = new CallBackTest(); CallBack* cb = new CallBack(cbt); cbt->doCallBack(); return 0; }
最常用和正統的解決方法,借助於static成員函數對類數據成員的可見性,可以很方便的利用:
pc->a++; printf("a is: %d", pc->a);
這樣的語句來操作類的成員函數和成員數據。但是仍然不能像普通成員函數那樣利用隱藏的this指針就直接操作類的成員函數。肯定有很多“好事”的同學希望直接像普通的成員函數那樣訪問類的成員。接下來就探討一下這個方法。
2、非靜態成員函數作為回調函數
既然我們知道,非靜態成員函數有一個隱藏的參數,那麼能否注冊的時候,多傳入一個參數,然後隱藏的那個指向對象的參數默認就轉為this指針的值了,相當於在調用時給this賦值。可以做一個嘗試,代碼如下:
#include <stdio.h> #include <stdlib.h> typedef void (*func)(void*); class CallBack; class CallBackTest; class CallBackTest { public: CallBackTest() { } ~CallBackTest() { } void registerProc(func fptr, void* arg = NULL) { m_fptr = fptr; if (arg != NULL) { m_arg = arg; } } void doCallBack() { m_fptr(m_arg); } private: func m_fptr; void* m_arg; }; class CallBack { public: CallBack(CallBackTest* t) : a(2) { if (t) { t->registerProc((func)display, this); } } ~CallBack() { } void display() { a++; printf("a is: %d", a); } private: int a; }; int main(int argc, char** argv) { CallBackTest* cbt = new CallBackTest(); CallBack* cb = new CallBack(cbt); cbt->doCallBack(); return 0; }
嘗試失敗了,提示編譯錯誤。在附錄的引用[1]文中,作者采用了更直接的給指針變量賦值的方式,避開了編譯錯誤的問題,但調用時仍然會報錯。因此this指針並不是簡單的在函數調用時以第一個參數的方式傳遞進去的,在理解成員函數訪問數據的過程可以這樣去理解,但是實際上的運行過程並不是這樣的。在引文1、2中給出了一些可行的辦法,進一步找了一下,這個也就是thunk技術,由於與平台和編譯器的行為強相關。大體思路是,首先將this指針填寫到指定的寄存器或者指定的地方,當調用成員函數名時,會自動根據寄存器的地址值加上偏移量實現跳轉。這裡不詳細介紹了,有興趣的同學可以參考鏈接。
使用靜態成員函數加上參數傳入this指針的方式應該說是目前比較完善的解決辦法。不失封裝性,又不失易用性