真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

c++成員函數(shù)指針是什么

本篇文章給大家分享的是有關(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)度為您提供的服務

1前言

C++語言支持指向成員函數(shù)的指針這一語言機制。就像許多其它C++語言機制一樣,它也是一把雙刃劍,用得好,能夠提高程序的靈活性、可擴展性等等,但是也存在一些不易發(fā)現(xiàn)的陷阱,我們在使用它的時候需要格外注意,尤其是在我們把它和c++其它的語言機制合起來使用的時候更是要倍加當心。

關(guān)鍵字:成員函數(shù)指針,繼承,虛函數(shù),this指針調(diào)整,static_cast

2語法

C++成員函數(shù)指針(pointer
to member function)的用法和C語言的函數(shù)指針有些相似.

下面的代碼說明了成員函數(shù)指針的一般用法:

class ClassName {public: int foo(int); }

 

int (ClassName::*pmf)(int)
 = &ClassNmae::foo;

 

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ù)指針”的引用,呵呵)

3與其它語言機制的混合使用

C++是一種Multi-Paradigm的語言,各種語言機制混合使用也是平常的事。這里我們只提幾種會影響到成員函數(shù)指針實現(xiàn)和運行的語言機制。

3.1繼承

根據(jù)C++語言規(guī)定,成員函數(shù)指針具有contravariance特性,就是說,基類的成員函數(shù)指針可以賦值給繼承類的成員函數(shù)指針,C++語言提供了默認的轉(zhuǎn)換方式,但是反過來不行。

3.2虛函數(shù)

首先要說明,指向虛擬成員函數(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的行為就會異常

4實現(xiàn)

從上面一節(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)整。

4.1 Microsoft的實現(xiàn)

4.1.1內(nèi)部表示

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
 pmf_type1{

      void* vcall_addr;

};

 

struct
 pmf_type2{

      void* vcall_addr;

      int 
 delta;  //調(diào)整this指針用

};

      這兩種表示導致成員函數(shù)指針的大小可能不一樣,pmf_type1大小為4,pmf_type2大小為8。有興趣的話可以寫一段代碼測試一下。

4.1.2 Vcall_addr實現(xiàn)

上面兩個結(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
 nv_fun1(int) {return
 0;}

     virtual
 int v_fun(int)
 {return 0;}

     virtual
 int v_fun_2(int)
 {return 0;}

};

 

void foo(C *c)

{

     int
 (C::*pmf)(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

:0040100A
 8BCE                    mov ecx, esi ;this

:0040100CE81F000000              call 00401030

; 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

:0040102A
 C3                      ret

:00401030
 33C0    ; 函數(shù)實現(xiàn)       xor eax, eax    

:00401032
  C
20400                 
 ret 0004

:00401720
 8B01    ;
 vcall           mov eax, dword
 ptr [ecx]

:00401722 FF20                    jmp dword ptr [eax]

:00401730
 8B01    ;
 vcall          mov eax, dword
 ptr [ecx]

: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 index>

     void
vcall(void* this)

    {

        jmp this->vptr[index];
//pseudo asm code

    }

每種不同的index都要產(chǎn)生一個實例。

Microsoft就是采用這樣的方式實現(xiàn)了虛成員函數(shù)指針的調(diào)用。

4.1.3 This指針調(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
 b_fun(int)     {

         std::cout<<'B'<

         return
 0;

     }

private:

     int
 m_b;

};

 

class D : public B {

public:

     D():m_d(0x24681357){}

     virtual
 int foo(int)   {

         std::cout<<'D'<

         return
 0;

     }

private:

     int
 m_d;

};

// 注意這個例子中virtual的使用

void test_this_adjust(D *pd, int
 (D::*pmf)(int))

{

     (pd->*pmf)(0x12345678);

}

 

:00401000   mov eax, dword ptr [esp+04] ; this入?yún)?/p>

:00401004   mov ecx, dword ptr [esp+0C] ; delta入?yún)?/p>

:00401008   push 12345678 ;參數(shù)入棧

:0040100D   add ecx, eax
 ; this = ecx= this+delta

:0040100F   call [esp+0C] ; vcall_addr入?yún)?/p>

:00401013   ret

void test_main(D *pd)

{

     test_this_adjust(pd,
 &D::foo);

     test_this_adjust(pd,
 &B::b_fun);

}

 

; 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,004016A0 ;
 D::foo vcall地址

:0040102C  push ecx ; push
 delat = 0, ecx=0

:0040102D  push eax ; push vcall_addr

:0040102E  push esi 
 ; push this

:0040102F  call 00401000 ;
 call test_this_adjust

 

; test_this_adjust(pd,
 &B::b_fun);

:00401034  mov ecx, 00000004 ;和上面的調(diào)用不同了

:00401039  mov eax, 00401050 ;
 B::b_fun地址

:0040103E  push ecx ; push
 delta = 4, exc=4

:0040103F  push eax ; push vcall_addr, B::b_fun地址

:00401040  push esi ; push
 this

:00401041  call 00401000  ;
 call test_this_adjust

 

:00401046  add esp, 00000018

:00401049  pop esi

:0040104A  ret

注意這里和上面一個例子的區(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)整。

4.1.4結(jié)論

       Microsoft根據(jù)情況選用下面的結(jié)構(gòu)表示成員函數(shù)指針,使用Thunk技術(shù)(vcall_addr)實現(xiàn)虛擬函數(shù)/非虛擬函數(shù)的自適應,在必要的時候進行this指針調(diào)整(使用delta)。

      

struct pmf_type1{

     void*
 vcall_addr;

};

 

struct pmf_type2{

     void*
 vcall_addr;

     int  delta; 
 //調(diào)整this指針用

};

4.2 GCC的實現(xiàn)

        GCC對于成員函數(shù)指針的實現(xiàn)和Microsoft的方式有很大的不同。

4.2.1內(nèi)部表示

GCC對于成員函數(shù)指針統(tǒng)一使用類似下面的結(jié)構(gòu)進行表示:


struct

{

    void* __pfn; 
 //函數(shù)地址,或者是虛擬函數(shù)的index

    long __delta; // offset, 用來進行this指針調(diào)整

};

4.2.2實現(xiàn)機制

先來看看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
 (Base::*pmf)(int))

{

     return
 (pb->*pmf)(4);

}

//assembly

8048478:    push   %ebp

 8048479:  mov   
 %esp,%ebp

 804847b:  sub   
 $0x18,%esp

 804847e:  mov   
 0xc(%ebp),%eax   ;__pfn, 入?yún)?/p>

 8048481:  mov   
 0x10(%ebp),%edx  ;__delta, 入?yún)?/p>

 8048484:  mov   
 %eax,0xfffffff8(%ebp)  ; __pfn

 8048487:  mov   
 %edx,0xfffffffc(%ebp)  ; __delta

 804848a:  sub    $0x8,%esp          
 ;

 804848d:  mov   
 0xfffffff8(%ebp),%eax ; __pfn

 8048490:  and   
 $0x1,%eax             ; __test last 2 bits, 判斷是否為虛擬函數(shù)

 8048493:  test  
 %al,%al

 8048495:  je     80484b6
 <_Z8test_funP4BaseMS_FiiE+0x3e> ;不是虛函數(shù)就跳到
 non-virtual fun處

 

  ; virtual fun,是虛擬函數(shù),計算函數(shù)地址

 8048497:  mov   
 0xfffffffc(%ebp),%eax ;__delta

 804849a:  mov    0x8(%ebp),%ecx  ;get pb, 入?yún)?/p>

 804849d:  add   
 %eax,%ecx       ;ecx = this=pb+__delta

 804849f:  mov   
 0xfffffff8(%ebp),%eax ;eax=__pfn

 80484a2:  shr    $0x2,%eax             ;eax=__pfn>>2 (fun index)

 80484a5:  lea   
 0x0(,%eax,4),%edx     ;edx=eax * 4

 80484ac:  mov    (%ecx),%eax           ;eax=vtble

 80484ae:  mov   
 (%eax,%edx,1),%edx    ;edx為函數(shù)地址

 80484b1:  mov   
 %edx,0xfffffff4(%ebp)   ;存起來

 80484b4:  jmp    80484bc
 <_Z8test_funP4BaseMS_FiiE+0x44>

 

 ;
 non-virtual fun,不是虛擬函數(shù),直接取出函數(shù)地址

 80484b6:  mov   
 0xfffffff8(%ebp),%eax ;__pfn, fun addr

 80484b9:  mov   
 %eax,0xfffffff4(%ebp) ;__pfn, fun addr

 

                ; common invoking

                ;
 0xfffffff4(%ebp) contains fun address

 80484bc:  push  
 $0x4                 ;push parameters

 80484be:  mov   
 0xfffffffc(%ebp),%eax   ; delta

 80484c1:  add    0x8(%ebp),%eax        ; this = pb+delta,
 this指針調(diào)整

 80484c4:  push   %eax                  ;
 this

 80484c5:  call  
 *0xfffffff4(%ebp)     ;invoke

 80484c8:  add    $0x10,%esp

 80484cb:  leave 
 

 80484cc:  ret   
 

 80484cd:  nop     
 

5語言限制與陷阱

      按照C++語言的規(guī)定,對于成員函數(shù)指針的使用,有如下限制:

      不允許繼承類的成員函數(shù)指針賦值給基類成員函數(shù)指針。

      如果我們一定要反其道而行,則存在this指針調(diào)整的陷阱,需要注意。這一節(jié)我們通過兩個例子,說明為什么這樣操作是危險的。

5.1例子

先看一個單繼承的例子。

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,
 int (B::*pmf)(int))

{

         (pb->*pmf)(0x12345678);

}

void test_main(D *pd)

{

         typedef int (B::*B_PMF)(int);

         //test_consistent(pd,
 &D::foo);  error!

         test_consistent(pd,
 static_cast(&D::foo));

     // 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(&D::foo)

這里的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
     (B::*B_PMF)(int);

             //test_consistent(pd, &B2::b2_fun);                    //A

             //test_consistent(pd, static_cast(&B2::b2_fun));  // B


    網(wǎng)頁標題:c++成員函數(shù)指針是什么
    文章網(wǎng)址:http://weahome.cn/article/gojsos.html

    在線咨詢

    微信咨詢

    電話咨詢

    028-86922220(工作日)

    18980820575(7×24)

    提交需求

    返回頂部

    <ul id="cgomg"><pre id="cgomg"></pre></ul>
    <ul id="cgomg"></ul>
    <kbd id="cgomg"></kbd>