本篇文章給大家分享的是有關(guān)c++成員函數(shù)指針是什么,小編覺得挺實用的,因此分享給大家學習,希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。
創(chuàng)新互聯(lián)專注于谷城企業(yè)網(wǎng)站建設(shè),響應式網(wǎng)站,商城網(wǎng)站制作。谷城網(wǎng)站建設(shè)公司,為谷城等地區(qū)提供建站服務。全流程按需求定制制作,專業(yè)設(shè)計,全程項目跟蹤,創(chuàng)新互聯(lián)專業(yè)和態(tài)度為您提供的服務
C++語言支持指向成員函數(shù)的指針這一語言機制。就像許多其它C++語言機制一樣,它也是一把雙刃劍,用得好,能夠提高程序的靈活性、可擴展性等等,但是也存在一些不易發(fā)現(xiàn)的陷阱,我們在使用它的時候需要格外注意,尤其是在我們把它和c++其它的語言機制合起來使用的時候更是要倍加當心。
關(guān)鍵字:成員函數(shù)指針,繼承,虛函數(shù),this指針調(diào)整,static_cast
C++成員函數(shù)指針(pointer
to member function)的用法和C語言的函數(shù)指針有些相似.
下面的代碼說明了成員函數(shù)指針的一般用法:
class ClassName {public: int foo(int); }
int (ClassName::*pmf)(int)
ClassName c; //.*的用法,經(jīng)由對象調(diào)用 (c.*pmf)(5); // A
ClassName *pc = &c; //->*的用法,經(jīng)由對象指針調(diào)用 (Pc->*pmf)(6); // B
|
使用typedef可以讓代碼變得略微好看一點:
typedef int (ClassName::*PMF)(int); PMF pmf = &ClassName::foo; |
注意獲取一個成員函數(shù)指針的語法要求很嚴格:
1)
不能使用括號:例如&(ClassName::foo)不對。
2)
必須有限定符:例如&foo不對。即使在類ClassName的作用域內(nèi)也不行。
3)
必須使用取地址符號:例如直接寫ClassName::foo不行。(雖然普通函數(shù)指針可以這樣)
所以,必須要這樣寫:&ClassName::foo。
C++成員函數(shù)的調(diào)用需要至少3個要素:this指針,函數(shù)參數(shù)(也許為空),函數(shù)地址。上面的調(diào)用中,->*和.*運算符之前的對象/指針提供了this(和真正使用this并不完全一致,后面會討論),參數(shù)在括號內(nèi)提供,pmf則提供了函數(shù)地址。
注意這里成員函數(shù)指針已經(jīng)開始顯示它“異類”的天性了。上面代碼中注釋A和B處兩個表達式,產(chǎn)生了一個在C++里面沒有類型(type)的“東西”(這是C++語言里面唯一的例外,其它任何東西都是有類型的),這就是.*和->*運算符:
(c.*pmf)
(Pc->*pmf)
這兩個運算符求值生成的“東西”我們只知道可以把它拿來當函數(shù)調(diào)用一樣使喚,別的什么也不能干,甚至都不能把它存在某個地方。就因為這個原因,Andrei Alexandrescu在他那本著名的《Modern c++ design》里面就說,成員函數(shù)指針和這兩個操作符號是“curiously half-baked concept in c++”。(5.9節(jié))
C++里面引入了“引用”(reference)的概念,可是卻不存在“成員函數(shù)的引用”,這也是一個特殊的地方。(當然,我們可以使用“成員函數(shù)指針”的引用,呵呵)
C++是一種Multi-Paradigm的語言,各種語言機制混合使用也是平常的事。這里我們只提幾種會影響到成員函數(shù)指針實現(xiàn)和運行的語言機制。
根據(jù)C++語言規(guī)定,成員函數(shù)指針具有contravariance特性,就是說,基類的成員函數(shù)指針可以賦值給繼承類的成員函數(shù)指針,C++語言提供了默認的轉(zhuǎn)換方式,但是反過來不行。
首先要說明,指向虛擬成員函數(shù)(virtual function
member)的指針也能正確表現(xiàn)出虛擬函數(shù)的特性。舉例說明如下:
class B { public:
virtual int
foo(int) {/* B's
implementation */return 0; } };
class D : public
B { public: virtual
int foo(int) { /* D's implementation */ return
0; } };
int
(B::*pmf)(int) = &B::foo;
D d;
B* pb = &d;
(d.*pmf)(0); //這里執(zhí)行D::foo
(pb->*pmf)(0); //這里執(zhí)行D::foo,多態(tài)
C++借由虛函數(shù)提供了運行時多態(tài)特性,虛函數(shù)的實現(xiàn)和普通函數(shù)有很大的不同。一般編譯器都是采用大家都熟悉的v-table (virtual function table)的方式。所有的虛函數(shù)地址存在一個函數(shù)表里面,類對象中存儲該函數(shù)表的首地址(vptr_point)。運行時根據(jù)this指針、虛函數(shù)索引和虛函數(shù)表指針找到函數(shù)調(diào)用地址。
3.2多繼承因為這些不同,所以成員函數(shù)指針碰上虛函數(shù)的時候,也需要作特殊的處理,才能正確表現(xiàn)出所期望的虛擬性質(zhì)。
這里扯上多繼承,是因為多繼承的存在導致了成員函數(shù)指針的實現(xiàn)的復雜性。這是因為編譯器有時候需要進行”this”指針調(diào)整。
舉例說明如下:
class B1{};
class B2{};
class D: public
B1, public B2{}
假設(shè)上面三個對象都不涉及到虛函數(shù),D在內(nèi)存中的典型布局如下圖所示(如果有虛函數(shù)則多一個vptr指針, 差別不大)。
現(xiàn)在假設(shè)我們經(jīng)由D對象調(diào)用B2的函數(shù),
D d;
d.fun_of_b2();
這里傳給fun_of_b2的this指針不能是&d,而應該對&d加上一個偏移,得到D內(nèi)含的B2子對象的首地址處。
成員函數(shù)指針的實現(xiàn)必須考慮這種情況。
多繼承總是不那么受歡迎。不過即使是單繼承,上面的情況也會出現(xiàn)??紤]下面的例子:
class B{}; //non-virtual
class
class D :public B{}; //virtual class
假設(shè)B是一個普通的類,沒有虛擬成員函數(shù)。而D加上了虛擬成員函數(shù)。
因為D引入了vptr指針,而一般的實現(xiàn)都將vptr放在對象的開頭,這就導致經(jīng)由D對象訪問B的成員函數(shù)的時候,仍然需要進行this指針的調(diào)整。
D d;
d.fun_of_b(); //this指針也需要調(diào)整,否則fun_of_b的行為就會異常
從上面一節(jié)我們可以看到,編譯器要實現(xiàn)成員函數(shù)指針,有幾個問題是繞不過去的:
1) 函數(shù)是不是虛擬函數(shù),這個涉及到虛函數(shù)表(__vtbl)的訪問。
2) 函數(shù)運行時,需不需要調(diào)整this指針,如何調(diào)整。這個涉及到C++對象的內(nèi)存布局。
事實上,成員函數(shù)指針必須記住這兩個信息。為什么要記住是否為虛函數(shù)就不用解釋了。但是this指針調(diào)整為什么要記住呢?因為在.*和->*運算符求值時必須用到。 考慮上面那個多繼承的例子:
int (D::*pmf)(int) = &B2::foo_of_b2; //A
D d;
(d.*pmf)(0);
//B
看看上面的代碼,其實我們在A處知道需要進行this指針調(diào)整,也知道該怎么調(diào)整。但是這時候this還沒出世呢,還不到調(diào)整的時候。到了B處終于有了This指針了,可是又不知道該怎樣調(diào)整了。所以pmf必須記住調(diào)整方式,到了B處調(diào)用的時候,再來進行調(diào)整。
Microsoft VC的實現(xiàn)采用的是Microsoft一貫使用的Thunk技術(shù)(不知道這個名字怎么來的,不過有趣的是把它反過來拼寫就變成了大牛Knuth的名字,呵呵)。
對于Mircosoft來說,成員函數(shù)指針實際上分兩種,一種需要調(diào)節(jié)this指針,一種不需要調(diào)節(jié)this指針。
先分清楚那些情況下成員函數(shù)指針需要調(diào)整this指針,那些情況下不需要?;貞浬弦还?jié)討論的c++對象內(nèi)存布局的說明,我們可以得出結(jié)論如下:
如果一個類對象obj含有一些子對象subobj,這些子對象的首地址&subobj和對象自己的首地址&obj不等的話,就有可能需要調(diào)整this指針。因為我們有可能把subobj的函數(shù)當成obj自己的函數(shù)來使用。
根據(jù)這個原則,可以知道下列情況不需要調(diào)整this指針:
1)
繼承樹最頂層的類。
2)
單繼承,若所有類都不含有虛擬函數(shù),那么該繼承樹上所有類都不需要調(diào)整this指針。
3)
單繼承,若最頂層的類含有虛函數(shù),那么該繼承樹上所有類都不需要調(diào)整this指針。
下列情況可能進行this指針調(diào)整:
1)
多繼承
2)
單繼承,最頂?shù)腷ase class不含virtual function,但繼承類含虛函數(shù)。那么這些繼承類可能需要進行this指針調(diào)整。
Microsoft把這兩種情況分得很清楚。所以成員函數(shù)的內(nèi)部表示大致分下面兩種:
struct void* vcall_addr; };
|
struct void* vcall_addr; int }; |
這兩種表示導致成員函數(shù)指針的大小可能不一樣,pmf_type1大小為4,pmf_type2大小為8。有興趣的話可以寫一段代碼測試一下。
上面兩個結(jié)構(gòu)中出現(xiàn)了vcall_addr,它就是Microsoft的Thunk技術(shù)核心所在。簡單的說,vcall_addr是一個指針,這個指針隱藏了它所指的函數(shù)是虛擬函數(shù)還是普通函數(shù)的區(qū)別。事實上,若它所指的成員函數(shù)是一個普通成員函數(shù),那么這個地址也就是這個成員函數(shù)的函數(shù)地址。若是虛擬成員函數(shù),那么這個指針指向一小段代碼,這段代碼會根據(jù)this指針和虛函數(shù)索引值尋找出真正的函數(shù)地址,然后跳轉(zhuǎn)(注意是跳轉(zhuǎn)jmp,而不是函數(shù)調(diào)用call)到真實的函數(shù)地址處執(zhí)行。
看一個例子。
//源代碼 class C { public: int virtual virtual };
void foo(C *c) { int
pmf = &C::nv_fun1; (c->*pmf)(0x12345678);
pmf = &C::v_fun; (c->*pmf)(0x87654321);
pmf = &C::v_fun_2; (c->*pmf)(0x87654321); }
|
; foo的匯編代碼,release版本,部分地方進行了優(yōu)化 :00401000 56 push esi :00401001 8B742408 mov esi, dword ptr [esp+08] ; pmf = &C::nv_fun1; ; (c->*pmf)(0x12345678); :00401005 6878563412 push 12345678 : : ; pmf = &C::v_fun; ; (c->*pmf)(0x87654321); :00401011 6821436587 push 87654321 :00401016 8BCE mov ecx, esi ;this :00401018 E803070000 call 00401720 ; pmf = &C::v_fun_2; ; (c->*pmf)(0x87654321); :0040101D 6821436587 push 87654321 :00401022 8BCE mov ecx, esi ;this :00401024 E807070000 call 00401730 :00401029 5E pop esi : |
:00401030 : |
:00401720 :00401722 FF20 jmp dword ptr [eax] |
:00401730 :00401732 FF6004 jmp [eax+04] |
從上面的匯編代碼可以看出vcall_addr的用法。00401030,00401720, 00401730都是vcall_addr的值,其實也就是pmf的值。在調(diào)用的地方,我們不能分別出是不是虛函數(shù),所看到的都是一個函數(shù)地址。但是在vcall_addr被當成函數(shù)地址調(diào)用后,進入vcall_addr,就有區(qū)別了。00401720,
00401730是兩個虛函數(shù)的vcall,他們都是先根據(jù)this指針,計算出函數(shù)地址,然后jmp到真正的函數(shù)地址。00401030是C::nv_fun1的真實地址。
Microsoft的這種實現(xiàn)需要對一個類的每個用到了的虛函數(shù),都分別產(chǎn)生這樣的一段代碼。這就像一個template函數(shù):
template
void
vcall(void* this)
{
jmp this->vptr[index];
//pseudo asm code
}
每種不同的index都要產(chǎn)生一個實例。
Microsoft就是采用這樣的方式實現(xiàn)了虛成員函數(shù)指針的調(diào)用。
不過還有一個this調(diào)整的問題,我們還沒有解決。上面的例子為了簡化,我們故意避開了this指針調(diào)整。不過有了上面的基礎(chǔ),我們再討論this指針調(diào)整就容易了。
首先我們需要構(gòu)造一個需要進行this指針調(diào)整的情況。回憶這節(jié)開頭,我們討論了哪些情況下需要進行this指針調(diào)整。我們用一個單繼承的例子來進行說明。這次我們避開virtual/non-virtual function的問題暫不考慮。
class B { public: B():m_b(0x13572468){} int std::cout<<'B'< return } private: int }; class D : public B { public: D():m_d(0x24681357){} virtual std::cout<<'D'< return } private: int }; | // 注意這個例子中virtual的使用 |
void test_this_adjust(D *pd, int { (pd->*pmf)(0x12345678); }
| :00401000 mov eax, dword ptr [esp+04] ; this入?yún)?/p> :00401004 mov ecx, dword ptr [esp+ :00401008 push 12345678 ;參數(shù)入棧 :0040100D add ecx, eax : :00401013 ret |
void test_main(D *pd) { test_this_adjust(pd, test_this_adjust(pd, }
| ; test_this_adjust(pd, &D::foo); :00401020 xor ecx, ecx :00401022 push esi :00401023 mov esi, dword ptr [esp+08] ; pd, this指針 :00401027 mov eax, : :0040102D push eax ; push vcall_addr :0040102E push esi :
; test_this_adjust(pd, :00401034 mov ecx, 00000004 ;和上面的調(diào)用不同了 :00401039 mov eax, 00401050 ; :0040103E push ecx ; push : :00401040 push esi ; push :00401041 call 00401000 ;
:00401046 add esp, 00000018 :00401049 pop esi : |
注意這里和上面一個例子的區(qū)別:
在調(diào)用test_this_adjust(pd,
&D::foo)的時候,實際上傳入了3個參數(shù),調(diào)用相當于
test_this_adjust(pd, vcall_address_of_foo, delta(=0));
調(diào)用test_this_adjust(pd,
&B::b_fun)的時候,也是3個參數(shù)
test_this_adjust(pd, vcall_address_of_b_fun, delta(=4));
兩個調(diào)用有個明顯的不同,就是delta的值。這個delta,為我們后來調(diào)整this指針提供了幫助。
再看看test_this_adjust函數(shù)的匯編代碼,和上一個例子的不同,也就是多了一句代碼:
:0040100D add ecx, eax ; this
= ecx= this+delta
這就是對this指針作必要的調(diào)整。
Microsoft根據(jù)情況選用下面的結(jié)構(gòu)表示成員函數(shù)指針,使用Thunk技術(shù)(vcall_addr)實現(xiàn)虛擬函數(shù)/非虛擬函數(shù)的自適應,在必要的時候進行this指針調(diào)整(使用delta)。
struct pmf_type1{ void* };
|
struct pmf_type2{ void* int delta; }; |
GCC對于成員函數(shù)指針的實現(xiàn)和Microsoft的方式有很大的不同。
GCC對于成員函數(shù)指針統(tǒng)一使用類似下面的結(jié)構(gòu)進行表示:
struct { void* __pfn; long __delta; // offset, 用來進行this指針調(diào)整 }; |
先來看看GCC是如何區(qū)分普通成員函數(shù)和虛擬成員函數(shù)的。
不管是普通成員函數(shù),還是虛擬成員函數(shù),信息都記錄在__pfn里面。這里有個小小的技巧。我們知道一般來說因為對齊的關(guān)系,函數(shù)地址都至少是4字節(jié)對齊的。這就意味這一個函數(shù)的地址,最低位兩個bit總是0。(就算沒有這個對齊限制,編譯器也可以這樣實現(xiàn)。) GCC充分利用了這兩個bit。如果是普通的函數(shù),__pfn記錄該函數(shù)的真實地址,最低位兩個bit就是全0,如果是虛擬成員函數(shù),最后兩個bit不是0,剩下的30bit就是虛擬成員函數(shù)在函數(shù)表中的索引值。
使用的時候,GCC先取出最低位兩個bit看看是不是0,若是0就拿這個地址直接進行函數(shù)調(diào)用。若不是0,就取出前面30位包含的虛擬函數(shù)索引,通過計算得到真正的函數(shù)地址,再進行函數(shù)調(diào)用。
GCC和Microsoft對這個問題最大的不同就是GCC總是動態(tài)計算出函數(shù)地址,而且每次調(diào)用都要判斷是否為虛擬函數(shù),開銷自然要比Microsoft的實現(xiàn)要大一些。這也差不多可以算成一種時間換空間的做法。
在this指針調(diào)整方面,GCC和Mircrosoft的做法是一樣的。不過GCC在任何情況下都會帶上__delta這個變量,如果不需要調(diào)整,__delta=0。
這樣GCC的實現(xiàn)比起Microsoft來說要稍簡單一些。在所有場合其實現(xiàn)方式都是一樣的。而且這樣的實現(xiàn)也帶來多一些靈活性。這一點下面“陷阱”一節(jié)再進行說明。
GCC在不同的平臺其實現(xiàn)細節(jié)可能略有不同,我們來看一個基于Intel平臺的典型實現(xiàn):
//source code int test_fun(Base *pb, int { return } //assembly 8048478: push %ebp 8048479: mov 804847b: sub 804847e: mov 8048481: mov 8048484: mov 8048487: mov 804848d: mov 8048490: and 8048493: test 8048495: je 80484b6
; virtual fun,是虛擬函數(shù),計算函數(shù)地址 8048497: mov 804849d: add 80484ae: mov 80484b1: mov 80484b4: jmp 80484bc
; 80484b6: mov 80484b9: mov
; common invoking ; 80484bc: push 80484be: mov 80484cb: leave 80484cc: ret 80484cd: nop |
按照C++語言的規(guī)定,對于成員函數(shù)指針的使用,有如下限制:
不允許繼承類的成員函數(shù)指針賦值給基類成員函數(shù)指針。
如果我們一定要反其道而行,則存在this指針調(diào)整的陷阱,需要注意。這一節(jié)我們通過兩個例子,說明為什么這樣操作是危險的。
先看一個單繼承的例子。
class B { public: B():m_b(0x13572468){} /* virtual */ int b_fun(int) { //A std::cout<<'B'< return 0; } private: int m_b; }; class D : public B { public: D():m_d(0x24681357){} virtual int foo(int) { // B std::cout<<'D'< return 0; } private: int m_d; }; void test_consistent(B* pb, { (pb->*pmf)(0x12345678); } void test_main(D *pd) { typedef int (B::*B_PMF)(int); //test_consistent(pd, test_consistent(pd, // crash in MSVC } int main() { D d; test_main(&d); return 0; } |
這句話在Microsoft Visual C++6.0下面一運行就crash。 表面上看我們傳的指針是D的指針,函數(shù)也是D的函數(shù)。但實際上不是那么簡單。函數(shù)調(diào)用的時候,pd賦值給pb,編譯器會進行this指針調(diào)整,pb指向pd內(nèi)部B的子對象。這樣到了test_consistent函數(shù)內(nèi)部的時候,就是用D::B對象調(diào)用D::foo函數(shù),this指針不對,所以就crash了。
|
上面這個問題,GCC能正確的進行處理。其實錯誤的原因不在于pb=pd指針賦值的時候,編譯器將指針進行了調(diào)整,而在于在test_consistent內(nèi),成員函數(shù)指針被調(diào)用的時候,應該將this指針再調(diào)整回去!這個問題又是由static_cast的行為不適當引起的。
static_cast
這里的static_cast,
是將D的成員函數(shù)指針強制轉(zhuǎn)換為給B的成員函數(shù)指針。因為它是D的函數(shù),雖然會經(jīng)由B的指針或者對象調(diào)用,但是調(diào)用時this指針應該根據(jù)B的地址調(diào)整成D的首地址。所以經(jīng)過static_cast之后,這個成員函數(shù)指針應該為{__pfn, __delta= -4 }。(B被包含在D內(nèi)部,所以這里是-4!) GCC正確的執(zhí)行了這個cast,并且每次使用成員函數(shù)指針調(diào)用時都進行this指針調(diào)整, 所以沒有問題。可是Microsoft的實現(xiàn)在這個地方卻無能為力,為什么呢?就算static_cast正確,在test_consistent里面根本就不會進行this指針調(diào)整! 因為它使用的其實是 struct{void *vcall_address;}這個結(jié)構(gòu),根本不知道要進行this指針調(diào)整。
Microsoft在這里要做的是將一個struct pmf_type2類型的對象,通過static_cast轉(zhuǎn)換成一個struct pmf_type1的對象。這種轉(zhuǎn)換根本不能成功,因為struct pmf_type1要少一個成員delta.這樣的轉(zhuǎn)換會丟失信息。
當然我們不能怪Microsoft,C++語言本來就規(guī)定了不能這樣用。不過Microsoft可以做得更好一點,至少可以不允許這樣的static_cast。(這樣的用法, VC2005能夠給出一個告警,提示有可能產(chǎn)生不正確的代碼!)
我們可以很簡單的解決這個問題,在上面的代碼中A處,把注釋掉的virtual打開,也可以把B處的virtual注釋掉,使得所有地方都無需進行this調(diào)整,問題也就不再出現(xiàn)了。
這個例子可能有些牽強,我們把上面的代碼稍做修改,再舉一個涉及到多繼承的例子。
class B { public: B():m_b(0x13572468){} virtual int b_fun(int) { std::cout<<"B return 0; } private: int m_b; }; class B2 { public: B2():m_b2(0x24681357){} int b2_fun(int) { std::cout<<"B2 return 0; } private: int m_b2; }; class D :public B , public B2 { public: D():m_d(0x24681357){} int foo(int) { std::cout<<"D return 0; } private: int m_d; }; void test_consistent(B* pb, int (B::*pmf)(int)) { (pb->*pmf)(0x12345678); } void test_main(D *pd) { typedef int //test_consistent(pd, &B2::b2_fun); //A //test_consistent(pd, static_cast |