混沌 IN C++::Pointers-to-Member functions 解迷

難度:star.gifstar.gifstar.gifstar.gifstar.gif

文前說明:下面涉及到的內容討論了在GCC 3.2MS Visual C++6/.NET中,指向成員函數的指針的實現。如果您將本文讀完,別忘了文章最後的一點說明。<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />

以前有過將指向成員函數的指針轉換成一個long而被編譯器拒絕的經歷嗎?這裏將說出真相。先來一段頗爲“神奇”的代碼

struct Base1

{

   int i;

   Base1():i(1){}

   void fun1(){   cout<<i<<endl; }

};

 

struct Base2

{

   int i;

   Base2():i(2){}

   void fun2(){   cout<<i<<endl; }

};

 

struct Derived: public Base1, public Base2

{

   int i;

   Derived():i(3){}

   void fun3(){   cout<<i<<endl;}

};

 

typedef void (Derived::*MEM_PTR)();

 

int main(){

   MEM_PTR mem_ptr = &Derived::fun2;

   Derived d;

   *(reinterpret_cast<int*>(&mem_ptr) + 1) = 0;

   (d.*mem_ptr)();

   *(reinterpret_cast<int*>(&mem_ptr) + 1) = 4;

   (d.*mem_ptr)();

   *(reinterpret_cast<int*>(&mem_ptr) + 1) = 8;

   (d.*mem_ptr)();

}

 

程序輸出是多少呢?

 

我們來剖析一下這個Derived

o_mem_fun_ptr01.gif


Derived

this指針,存在兩個情況

1、指向Base1部分

當發生d.fun1()d.fun3()這兩個調用時,這兩個成員函數得到的this指針都是指向Base1部分的。

2、指向Base2的部分

當發生d.fun2()的調用時,這個成員函數得到的this指針是指向Base2部分的。

從上面這兩種情況可以看出,在對多重繼承的對象調用成員函數時,會對this指針進行調整。d.fun1()/d.fun2()/d.fun3()在編譯時,對於對象d和這三個成員函數來說有足夠的類型信息,編譯器會自動對this指針進行調整。那麼,如果對成員函數取地址,在進行(obj.*mem_ptr)()(ptr->*mem_ptr)()調用時,編譯器無法完全知道mem_ptr是指向哪個成員函數,所以編譯器無法對這類的調用進行直接調整,而是放在運行期,根據環境進行調整。那麼在運行期,這調整的依據是什麼呢?

cout<<sizeof(MEM_PTR)<<endl;  //MEM_PTR是上面代碼中的typedef-name

發現了嗎?MEM_PTR這個指向成員函數的指針的大小是8-Byte,而不是我們常說的指針大小是4-ByteMEM_PTR的前4-Byte就是函數的地址,而後4-Byte就是需要調整的量。我們可以把通過指向成員函數的指針調用模型看作下面這樣

((對象地址+調整量).*函數地址)(); ((對象地址+調整量)->*函數地址)();

在上面的代碼中*(reinterpret_cast<int*>(&mem_ptr) + 1)其實就代表了後4-Byte的內存,即調整量。

mem_ptr = &Derived::fun2; mem_ptr 指向的是Derived::fun2

*(reinterpret_cast<int*>(&mem_ptr) + 1) = 0;  把調整量設定爲0

(d.*mem_ptr)(); 僞碼:((&d - 0).*mem_ptr)(); this指針未改變,所以fun2中訪問的i其實是Base1::i

*(reinterpret_cast<int*>(&mem_ptr) + 1) = 4;  把調整量設定爲4

(d.*mem_ptr)(); 僞碼:((&d - 4).*mem_ptr)(); this指針改變了,所以fun2中訪問的i其實是Base2::i

*(reinterpret_cast<int*>(&mem_ptr) + 1) = 8;

 (d.*mem_ptr)();

其中調整量分別是48其本質是sizeof(Base1)sizeof(Base1)+sizeof(Base2)

 

現在我們再來一段“神奇”的代碼。

把上面代碼的每個成員函數裏的cout<<i<<endl;改爲cout<<i<<’/t’<<this<<endl; ,然後再在main()的最後面加上d.fun1(); d.fun2(); d.fun3(); 最後編譯運行,會得到兩組輸出,但是在3 的那一組,我們會發現兩個輸出的this指針不同,爲什麼呢?也許你已經想到。線索就在上面的文字裏。

 

當成員函數被定義爲virtual這個世界會變成怎麼樣呢?

將最先那段頗爲“神奇”的代碼中的Base1::fun1()Base2::fun2()定義爲虛函數。然後將上面的調整量48分別設定爲sizeof(Base1)sizeof(Base1)+sizeof(Base2),最後編譯運行。程序會在輸出12之後被中斷。而輸出3時卻出錯,爲什麼呢?

我們先來了解一下這時Derived的對象模型

o_mem_fun_ptr02.gif


其中多了兩個

vptrvptr1是由Base1部分和Derived派生出來的這部分使用,vptr2是由Base2部分使用。

mem_ptr = &Derived::fun2; 打算取Derived::fun2的地址(:由於Base2::fun2是虛函數,它的實際地址只能在運行期才能決定。所以這裏用了“打算”二字)。有一點我們可以肯定Base2::fun2被安插在Base2vtable中的第一個位置。

*(reinterpret_cast<int*>(&mem_ptr) + 1) = 0;

(d.*mem_ptr)(); 由調整量確定了this指針指向的是Base1部分,然後通過vptr1試圖獲得第一個vtbl中的第一個虛函數地址,所以,事實上得到的是Base1::fun1的地址,Base1的指針,調用Base1::fun1,固然不會出錯,所以輸出爲1

*(reinterpret_cast<int*>(&mem_ptr) + 1) = sizeof(Base1);

(d.*mem_ptr)(); 和上面同理,由調整量確定了this指向Base2部分,由vptr2得到Base2::fun2地址。所以輸出爲2

*(reinterpret_cast<int*>(&mem_ptr) + 1) = sizeof(Base1) + sizeof(Base2);

(d.*mem_ptr)();

爲什麼最後一個會出錯呢?注意一個特點,this指針被調整後,會訪問調整後的this指針所指向的vptr。而對於class Derived而言,我們可以通過上面的對象模型得知Derived派生出來的部分並沒有安插vptr,而是與Base1同用的一個vptr1,所以,由於在被調整的this所指的位置並不存在(正確的)vptr,所以導致尋找了一個錯誤的地址而誤認爲是Base1vtbl,故發生訪問錯誤。而此時,我們可以爲Derived手動安插一個數據成員來模擬一個vptr來達到原來的目的。

struct Derived: public Base1, public Base2

{

   int vptr;  //安插一個數據成員

int i;

   Derived():i(3)

{

   vptr = *(reinterpret_cast<int*>(this));  //模擬一個vptr

}

   void fun3(){   cout<<i<<endl;}

};

其餘代碼不變,然後編譯運行。現在程序正確了,訪問的是fun1(),但是this指針卻不是指向的Base1的部分,換句話說,雖然我們成功了,但是卻避免不了這樣危險的代碼。

 

所以,如果我們試圖通過某些非語言提供的機制對 指向成員函數的指針 進行轉換,是非常危險的。

 

最後我們再做一個實驗。還是用第一個“神奇”的代碼。在代碼中加入

struct Base3{};

struct Derived2:public Base3, public Derived{};

 

然後將main中的代碼改爲

int main()

{

void (Base3::*pfunc1)();

void (Derived2::*pfunc2)();

pfunc2 = pfunc1;

pfunc1 = pfunc2;

}

 

試想一下爲什麼pfunc2 = pfunc1; 可以通過編譯,而pfunc1 = pfunc2;卻不能?

 

最後的一點說明:GCC MS Visual C++ 對實現指向成員函數的指針的區別。MSVC中,單繼承中的指向成員函數的指針的大小仍然是4-Byte,也就是說,上面pfunc1的大小是4-Byte,而只有在多重繼承中的指向成員函數的指針的大小是8-Byte,也就是說pfunc2的大小是8-Byte。而GCC中,都是8-Byte,也就是在單繼承中,它的調整量是0

 

關於上面提到的虛函數地址的獲得,可以參考

http://blog.csdn.net/jinhao/archive/2004/01/17/4798.aspx

http://blog.csdn.net/jinhao/archive/2004/01/17/4799.aspx

 

//THE END

發佈了32 篇原創文章 · 獲贊 15 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章