難度:
文前說明:下面涉及到的內容討論了在GCC 3.2和MS 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
Derived
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-Byte。MEM_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)();
其中調整量分別是4和8其本質是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()定義爲虛函數。然後將上面的調整量4和8分別設定爲sizeof(Base1)和sizeof(Base1)+sizeof(Base2),最後編譯運行。程序會在輸出1和2之後被中斷。而輸出3時卻出錯,爲什麼呢?
我們先來了解一下這時Derived的對象模型
其中多了兩個
mem_ptr = &Derived::fun2; 打算取Derived::fun2的地址(注:由於Base2::fun2是虛函數,它的實際地址只能在運行期才能決定。所以這裏用了“打算”二字)。有一點我們可以肯定Base2::fun2被安插在Base2的vtable中的第一個位置。
*(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,所以導致尋找了一個錯誤的地址而誤認爲是Base1的vtbl,故發生訪問錯誤。而此時,我們可以爲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