昨天發現了一個問題,就是使用對類中的非靜態成員函數使用std::bind時,不能像普通函數一樣直接傳遞函數名,而是必須顯式地調用&(取地址),於是引申出我們今天的問題:非靜態類成員函數指針和普通函數指針有什么區別?
一.C++中對函數到指針的隱式轉換
以前在C語言程序設計課上,老師都會說:“函數名就是指向這個函數的指針”。實際上通過查閱cppreference中的隱式轉換規則,其中有這么一句關鍵的話道出了玄機:
函數類型
T
的左值能隱式轉換成指向該函數的指針純右值。這不作用於非靜態成員函數,因為不存在指代非靜態成員函數的左值。
這一句話表明,實際上我們在把普通函數的函數名當作右值使用的過程中,C++隱式地將其轉換成了指向該函數的的指針。這也就是我們在使用std::bind的過程中可以直接傳遞普通函數的函數名的原因,例如:
int foo(int a,int b) { return (a+b); } int main() { auto function = std::bind(foo,std::placeholders::_1,std::placeholders::_2); std::cout << function(3,4) << std::endl; //7 return 0; }
但是,這一條隱式轉換規則不作用於非靜態成員函數,因為不存在指代非靜態成員函數的左值。因此我們能看出,指向非靜態成員函數的指針和指向普通函數的指針是有區別的。接下來我們將會討論類成員函數指針(除非有特別說明,下文中的“類成員”都指代“非靜態類成員”)。
二.類成員函數指針的聲明和調用
類成員函數指針的聲明有些類似普通函數指針。但是區別在於,類成員函數是類成員的一部分,所以需要將類成員函數指針使用 ::* 聲明成 成員指針類型。這也表示了類成員函數指針不能單獨地被調用,需要 該類型的實例使用.*(對象的成員指針) 或 指向該類型的指針使用->*(指針的成員指針) 進行調用。同時由於上文說到的C++隱式轉換的規則,我們需要顯示地調用 &(取地址) 獲取非靜態成員函數指針:
class A { public: void FunA() { std::cout << "Function A" << endl; } }; int main() { void(A::*p1)() = &A::FunA; //p1(); Error A a; A *p2 = &a; (a.*p1)(); // Succ (p2->*p1)(); // Succ return 0; }
三.探究類成員函數指針
我們都知道,普通函數指針實際上指向的是函數代碼的起始地址。然而類成員函數指針不僅僅是類成員函數的內存起始地址,它還解決C++中涉及到的多重繼承、虛函數等導致的地址偏移問題。因此,普通函數指針的內存大小是普通指針的大小(32位系統4字節,64位系統8字節),但是成員函數指針卻不一定。
1.多繼承中的使用
現在有三個類A B C,其中類C public繼承 了A B兩個類。它們的聲明和定義如下:
class A { public: int a; void funA() { std::cout << "function A" << std::endl; } }; class B { public: int b; void funB() { std::cout << "function B" << std::endl; } }; class C : public A, public B { public: int c; void funC() { std::cout << "function C" << std::endl; } };
現在我們聲明一個函數指針:
int main() { void (C::*mfpC)() = &B::funB; return 0; }
這個指針很特別,它是一個指向C的類成員的指針,但實際上它指向了C的基類B的成員函數B::funB。現在我們編譯代碼並調用gdb調試查看mfpC的信息:
(gdb) p mfpC $1 = (void (C::*)(C * const)) 0x8000966 <B::funB()>, this adjustment 4 (gdb) p sizeof(mfpC) $2 = 16 (gdb) x/16xb &mfpC 0x7ffffffee830: 0x66 0x09 0x00 0x08 0x00 0x00 0x00 0x00 0x7ffffffee838: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00
可以看見,mfpC實際占用了16個字節(運行環境為64位操作系統),這16個字節中不僅包含了它所指向的函數的起始地址,還包含了this指針的調整值。因為對於多重繼承來說,如果類成員函數指針保存的是非最左基類的成員函數地址,根據C++標准,非最左基類實例的開始地址肯定不同於派生類實例的開始地址,所以需要調整this指針,使其指向非最左基類實例。
2.有虛函數的使用
現在修改代碼,在類B中定義一個虛函數funC,並在類C中重寫它:
class A { public: int a; void funA() { std::cout << "function A" << std::endl; } }; class B { public: int b; void funB() { std::cout << "function B" << std::endl; } virtual void funC() { std::cout << "function C in class B" << std::endl; } }; class C : public A, public B { public: int c; void funC() { std::cout << "function C in class C" << std::endl; } };
然后我們實例化一個類C的對象c和一個指向B::funC的成員函數指針mfpB,最后通過c調用它:
int main() { C c; void (B::*mfpB)() = &B::funC; (c.*mfpB)(); return 0; }
結果顯而易見,輸出的不是"function C in class B",而是"function C in class C"。
由此可見,成員函數指針雖然看起來和普通的函數指針有些類似,但實際上兩者還是有很大的差別的。