C++ 類成員函數(shù)指針語法的友好指南
一旦你理解了一般原則,C++ 類成員函數(shù)指針不再那么令人生畏。
如果你正在尋找性能、復雜性或許多可能的解決方法來解決問題,那么在涉及到極端的情況下,C++ 總是一個很好的選擇。當然,功能通常伴隨著復雜性,但是一些 C++ 的特性幾乎難以分辨。根據(jù)我的觀點,C++ 的 類成員函數(shù)指針 也許是我接觸過的最復雜的表達式,但是我會先從一些較簡單的開始。
文章中的例子可以在我的 Github 倉庫 里找到。
C 語言:函數(shù)指針
讓我們先從一些基礎(chǔ)開始:假設你有一個函數(shù)接收兩個整數(shù)作為參數(shù)返回一個整數(shù):
int sum(int a, int b) {
return a+b;
}
在純 C 語言中,你可以創(chuàng)建一個指向這個函數(shù)的指針,將其分配給你的 sum(...)
函數(shù),通過解引用來調(diào)用它。函數(shù)的簽名(參數(shù)、返回類型)必須符合指針的簽名。除此之外,一個函數(shù)指針表現(xiàn)和普通的指針相同:
int (*funcPtrOne)(int, int);
funcPtrOne = ∑
int resultOne = funcPtrOne(2, 5);
如果你使用指針作為參數(shù)并返回一個指針,這會顯得很丑陋:
int *next(int *arrayOfInt){
return ++arrayOfInt;
}
int *(*funcPtrTwo)(int *intPtr);
funcPtrTwo = &next;
int resultTwo = *funcPtrTwo(&array[0]);
C 語言中的函數(shù)指針存儲著子程序的地址。
指向類成員函數(shù)的指針
讓我們來進入 C++:好消息是你也許不需要使用類成員函數(shù)指針,除非在一個特別罕見的情況下,比如說接下來的例子。首先,你已經(jīng)知道定義一個類和其中一個成員函數(shù):
class MyClass
{
public:
int sum(int a, int b) {
return a+b;
}
};
1、定義一個指針指向某一個類中一個成員函數(shù)
聲明一個指針指向 MyClass
類成員函數(shù)。在此時,你并不知道想調(diào)用的具體函數(shù)。你僅僅聲明了一個指向 MyClass
類中任意成員函數(shù)的指針。當然,簽名(參數(shù)、返回值類型)需要匹配你接下想要調(diào)用的 sum(...)
函數(shù):
int (MyClass::*methodPtrOne)(int, int);
2、賦值給一個具體的函數(shù)
為了和 C 語言(或者 靜態(tài)成員函數(shù))對比,類成員函數(shù)指針不需要指向絕對地址。在 C++ 中,每一個類中都有一個虛擬函數(shù)表(vtable)用來儲存每個成員函數(shù)的地址偏移量。一個類成員函數(shù)指針指向 vtable 中的某個條目,因此它也只存儲偏移值。這樣的原則使得 多態(tài) 變得可行。
因為 sum(...)
函數(shù)的簽名和你的指針聲明匹配,你可以賦值簽名給它:
methodPtrOne = &MyClass::sum;
3、調(diào)用成員函數(shù)
如果你想使用指針調(diào)用一個類成員函,你必須提供一個類的實例:
MyClass clsInstance;
int result = (clsInstance.*methodPtrOne)(2,3);
你可以使用 .
操作符來訪問,使用 *
對指針解引用,通過提供兩個整數(shù)作為調(diào)用函數(shù)時的參數(shù)。這是丑陋的,對吧?但是你可以進一步應用。
在類內(nèi)使用類成員函數(shù)指針
假設你正在創(chuàng)建一個帶有后端和前端的 客戶端/服務器 原理架構(gòu)的應用程序。你現(xiàn)在并不需要關(guān)心后端,相反的,你將基于 C++ 類的前端。前端依賴于后端提供的數(shù)據(jù)完成初始化,所以你需要一個額外的初始化機制。同時,你希望通用地實現(xiàn)此機制,以便將來可以使用其他初始化函數(shù)(可能是動態(tài)的)來拓展你的前端。
首先定義一個數(shù)據(jù)類型用來存儲初始化函數(shù)(init
)的指針,同時描述何時應調(diào)用此函數(shù)的信息(ticks
):
template<typename T>
struct DynamicInitCommand {
void (T::*init)(); // 指向額外的初始化函數(shù)
unsigned int ticks; // 在 init() 調(diào)用后 ticks 的數(shù)量
};
下面一個 Frontend
類示例代碼:
class Frontend
{
public:
Frontend(){
DynamicInitCommand<Frontend> init1, init2, init3;
init1 = { &Frontend::dynamicInit1, 5};
init2 = { &Frontend::dynamicInit2, 10};
init3 = { &Frontend::dynamicInit3, 15};
m_dynamicInit.push_back(init1);
m_dynamicInit.push_back(init2);
m_dynamicInit.push_back(init3);
}
void tick(){
std::cout << "tick: " << ++m_ticks << std::endl;
/* 檢查延遲初始化 */
std::vector<DynamicInitCommand<Frontend>>::iterator it = m_dynamicInit.begin();
while (it != m_dynamicInit.end()){
if (it->ticks < m_ticks){
if(it->init)
((*this).*(it->init))(); // 這里是具體調(diào)用
it = m_dynamicInit.erase(it);
} else {
it++;
}
}
}
unsigned int m_ticks{0};
private:
void dynamicInit1(){
std::cout << "dynamicInit1 called" << std::endl;
};
void dynamicInit2(){
std::cout << "dynamicInit2 called" << std::endl;
}
void dynamicInit3(){
std::cout << "dynamicInit3 called" << std::endl;
}
unsigned int m_initCnt{0};
std::vector<DynamicInitCommand<Frontend> > m_dynamicInit;
};
在 Frontend
完成實例化后,tick()
函數(shù)會被后端以固定的時間時間調(diào)用。例如,你可以每 200 毫秒調(diào)用一次:
int main(int argc, char* argv[]){
Frontend frontendInstance;
while(true){
frontendInstance.tick(); // 僅用于模擬目的
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
}
Fronted
有三個額外的初始化函數(shù),它們必須根據(jù) m_ticks
的值來選擇調(diào)用哪個。在 ticks 等于何值調(diào)用哪個初始化函數(shù)的信息存儲在數(shù)組 m_dynamicInit
中。在構(gòu)造函數(shù)(Frontend()
)中,將此信息附加到數(shù)組中,以便在 5、10 和 15 個 tick 后調(diào)用其他初始化函數(shù)。當后端調(diào)用 tick()
函數(shù)時,m_ticks
值會遞增,同時遍歷數(shù)組 m_dynamicInit
以檢查是否必須調(diào)用初始化函數(shù)。
如果是這種情況,則必須通過引用 this
指針來取消引用成員函數(shù)指針:
((*this).*(it->init))()
總結(jié)
如果你并不熟悉類成員函數(shù)指針,它們可能會顯得有些復雜。我做了很多嘗試和經(jīng)歷了很多錯誤,花了一些時間來找到正確的語法。然而,一旦你理解了一般原理后,方法指針就變得不那么可怕了。
這是迄今為止我在 C++ 中發(fā)現(xiàn)的最復雜的語法。