C語言關於回撥函式和this指標探討
在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指標的方式應該說是目前比較完善的解決辦法。不失封裝性,又不失易用性。
相關文章
- C語言函式指標與回撥用函式C語言函式指標
- 回撥函式(c和指標)函式指標
- C語言函式指標與回撥函式使用方法C語言函式指標
- 【不在混淆的C】指標函式、函式指標、回撥函式指標函式
- 函式指標&回撥函式Callback函式指標
- C 語言回撥函式詳解函式
- C語言 函式指標C語言函式指標
- C++定義函式指標,回撥C#C++函式指標C#
- C語言關於指標,gets()和gets_s()函式的理解C語言指標函式
- 函式指標之回撥函式和轉移表函式指標
- python高階函式和C語言函式指標Python函式C語言指標
- C語言函式指標基礎C語言函式指標
- 宣告函式指標並實現回撥 (轉)函式指標
- C語言語法基礎--S2函式和指標C語言函式指標
- 關於函式指標函式指標
- [C++] 成員函式指標和函式指標C++函式指標
- 函式回撥(C++)函式C++
- 函式指標的重要用途——回撥函式函式指標
- C語言函式手冊:c語言庫函式大全|C語言標準函式庫|c語言常用函式查詢C語言函式
- 關於 js 中的回撥函式 callbackJS函式
- C語言 非同步回撥C語言非同步
- [JS]回撥函式和回撥地獄JS函式
- 函式指標、回撥函式、動態記憶體分配、檔案操作函式指標記憶體
- 指標函式 和 函式指標指標函式
- 關於C語言結構體對齊問題的探討C語言結構體
- C++回撥函式 用法C++函式
- c++回撥函式(下)C++函式
- C++回撥函式示例C++函式
- c#之回撥函式C#函式
- C語言指標(三):陣列指標和字串指標C語言指標陣列字串
- C語言 C語言野指標C語言指標
- C語言(指標)C語言指標
- C語言指標C語言指標
- c語言函式指標的定義C語言函式指標
- C語言標準函式庫C語言函式
- 函數語言程式設計 - 玩轉高階回撥函式函數程式設計函式
- 關於C++引用做為函式引數和指標作為函式引數C++函式指標
- C++中的回撥函式C++函式