以前自己也一直認為, 指針都一樣,32機器的尋址是32位,大小都是4,原來,我一直是錯的,成員函數(shù)指針的大小就不是4,不過,它應該不是普通的函數(shù)指針,看來自己以前對成員函數(shù)指針的理解太過于幼稚和膚淺了。
本文將對成員函數(shù)指針的一點點小小的總結(jié)。
自己只是作了下簡單的測試,如下
測試如下:
[cpp]
view plaincopy<span style="font-size:16px;">class A
{
};
class B
{
};
class C
{
};
class D:public A
{
};
class E:public A,B
{
};
class F:public A,B,C
{
};
int main()
{
cout<<"void (D::*)()的大小是:"<<sizeof( void (D::*)() )<<endl; // 4 指向成員函數(shù)的指針大小不一定等于sizeof(void*)
cout<<"void (E::*)()的大小是:"<<sizeof( void (E::*)() )<<endl; // 8 指向成員函數(shù)的指針大小不一定等于sizeof(void*)
cout<<"void (F::*)()的大小是:"<<sizeof( void (F::*)() )<<endl; // 8 指向成員函數(shù)的指針大小不一定等于sizeof(void*)
cout<<"void* 的大小是:"<<sizeof( void* )<<endl;
};</span>
下文是出自網(wǎng)上一牛人:
http://www.examda.com/ncre/three/pc/fudao/20071109/094943632.html尊重作者,我只是一字不漏的拷貝過來,方便自己和大家查閱
一。理論篇
在進行深入學習和分析之前,還是先看看書中是怎么介紹成員函數(shù)的??偨Y(jié)一下類成員函數(shù)指針的內(nèi)容,應該包含以下幾個知識點:
1。成員函數(shù)指針并不是普通的函數(shù)指針。
2。編譯器提供了幾個新的操作符來支持成員函數(shù)指針操作:
1) 操作符"::*"用來聲明一個類成員函數(shù)指針,例如:
typedef void (Base::*PVVBASEMEMFUNC)(void); //Base is a class
2) 操作符"->*"用來通過對象指針調(diào)用類成員函數(shù)指針,例如:
//pBase is a Base pointer and well initialized
//pVIBaseMemFunc is a member function pointer and well initialized
(pBase->*pVIBaseMemFunc)();
3) 操作符".*"用來通過對象調(diào)用類成員函數(shù)指針,例如:
//baseObj is a Base object
//pVIBaseMemFunc is a member function pointer and well initialized
(baseObj.*pVIBaseMemFunc)();
3。成員函數(shù)指針是強類型的。 typedef void (Base::*PVVBASEMEMFUNC)(void);
typedef void (Derived::*PVVDERIVEMEMFUNC)(void);
PVVBASEMEMFUNC和PVVDERIVEMEMFUNC是兩個不同類型的成員函數(shù)指針類型。
4。由于成員函數(shù)指針并不是真真意義上的指針,所以成員函數(shù)指針的轉(zhuǎn)化就受限制。具體的轉(zhuǎn)化細節(jié)依賴于不同的編譯器,甚至是同一個編譯器的不同版本。不過,處于同一個繼承鏈中的不同類之間override的不同函數(shù)和虛函數(shù)還是可以轉(zhuǎn)化的。 void* pVoid = reinterpret_cast<void*>(pVIBaseMemFunc); //error
int* pInt = reinterpret_cast<int*>(pVIBaseMemFunc); //error
pVIDeriveMemFunc = static_cast<PVIDERIVEMEMFUNC>(pVIBaseMemFunc); //OK
二。實踐篇
有了上面的理論知識,我們對類成員函數(shù)指針有了大概的了解,但是我們對成員函數(shù)指針還存在太多的疑惑。既然說成員函數(shù)指針不是指針,那它到底是什么東東? 編譯器為什么要限制成員函數(shù)指針轉(zhuǎn)化?老辦法,我們還是分析匯編代碼揭示其中的秘密。
首先,我寫了這樣兩個具有繼承關(guān)系的類:
[cpp]
view plaincopy<span style="font-size:16px;"> class Base
{
public:
//ordinary member function
void setValue(int iValue);
//virtual member function
virtual void dumpMe();
virtual void foobar();
protected:
int m_iValue;
};
class Derived:public Base
{
public:
//ordinary member function
void setValue(int iValue);
//virtual member function
virtual void dumpMe();
virtual void foobar();
private:
double m_fValue;
};
</span>
接著,我又定義了一些成員函數(shù)指針類型:
typedef void (Base::*PVVBASEMEMFUNC)(void);
typedef void (Derived::*PVVDERIVEMEMFUNC)(void);
typedef void (Base::*PVIBASEMEMFUNC)(int);
typedef void (Derived::*PVIDERIVEMEMFUNC)(int);
最后,在main函數(shù)寫了一些測試代碼:
[cpp]
view plaincopy<span style="font-size:16px;">int _tmain(int argc, _TCHAR* argv[])
{
PVIBASEMEMFUNC pVIBaseMemFunc = &Base::setValue;
PVIDERIVEMEMFUNC pVIDeriveMemFunc = static_cast<PVIDERIVEMEMFUNC>(pVIBaseMemFunc);
PVVBASEMEMFUNC pVVBaseMemFunc = &Base::foobar;
PVVDERIVEMEMFUNC pVVDeriveMemFunc = static_cast<PVVDERIVEMEMFUNC>(pVVBaseMemFunc);
Base baseObj;
(baseObj.*pVIBaseMemFunc)(10);
(baseObj.*pVVBaseMemFunc)();
Derived deriveObj;
(deriveObj.*pVIDeriveMemFunc)(20);
(deriveObj.*pVVDeriveMemFunc)();
return 0;
}</span>
成功編譯后生成匯編代碼。
老規(guī)矩,在分析匯編代碼的過程中還是只分析對解決問題有意義的匯編代碼,其他的就暫時忽略。
1。 成員函數(shù)指針不是指針。
從代碼看出,在main函數(shù)的調(diào)用棧(calling stack)中首先依次壓入四個成員函數(shù)指針,如果它們是普通指針的話,它們之間的偏移量應該是4個字節(jié),可是實際的情況卻是這樣的: _deriveObj$ = -88
_baseObj$ = -60
_pVVDeriveMemFunc$ = -44
_pVVBaseMemFunc$ = -32
_pVIDeriveMemFunc$ = -20
_pVIBaseMemFunc$ = -8
_argc$ = 8
_argv$ = 12
由此可以看出,他們之間的偏移量是12個字節(jié)。這12個字節(jié)中應該可以包含三個指針,其中的一個指針應該指向函數(shù)的地址,那另外兩個指針又指向那里呢?
在《C++ Common Knowledge: Essential Intermediate Programming》(中文譯名:C++必知必會)這本書的第16章對這部分的內(nèi)容做了說明,這個12個字節(jié)的偏移量正好印證了書中的內(nèi)容:
”The implementation of the pointer to member function must store within itself information as to whether the member function to which it refers is virtual or nonvirtual, information about where to find the appropriate virtual function table pointer (see The Compiler Puts Stuff in Classes [11, 37]), an offset to be added to or subtracted from the function’s this pointer (see Meaning of Pointer Comparison [28, 97]), and possibly other information. A pointer to member function is commonly implemented as a small structure that contains this information, although many other implementations are also in use. Dereferencing and calling a pointer to member function usually involves examining the stored information and conditionally executing the appropriate virtual or nonvirtual function calling sequence.“
2。 成員函數(shù)指針的轉(zhuǎn)化。本文所采用的代碼是想比較普通成員函數(shù)指針和虛函數(shù)指針在轉(zhuǎn)化的過程中存在那些差異 ;
PVIBASEMEMFUNC pVIBaseMemFunc = &Base::setValue;
mov DWORD PTR _pVIBaseMemFunc$[ebp], OFFSET FLAT:?setValue@Base@@QAEXH@Z ;
取出Base::setValue函數(shù)的地址,存放于變量pVIBaseMemFunc所占內(nèi)存的前4個字節(jié)(DWORD)中。
; PVVBASEMEMFUNC pVVBaseMemFunc = &Base::foobar;
mov DWORD PTR _pVVBaseMemFunc$[ebp], OFFSET FLAT:??_9@$B3AE ; `vcall’
取出符號”??_9@$B3AE“的值,存放于變量pVVBaseMemFunc所占內(nèi)存的前4個字節(jié)(DWORD)中。
對于符號”??_9@$B3AE“,我又找到了這樣的匯編代碼: _TEXT SEGMENT
_9@$B3AE PROC NEAR ; `vcall’, COMDAT
mov eax, DWORD PTR [ecx]
jmp DWORD PTR [eax+4]
_9@$B3AE ENDP ; `vcall’
_TEXT ENDS
符號
”??_9@$B3AE“代表的應該是一個存根函數(shù),這個函數(shù)首先根據(jù)this指針獲得虛函數(shù)表的指針,然后將指令再跳轉(zhuǎn)到相應的虛函數(shù)的地址。由此可以看出,對于虛函數(shù),即使是用過成員函數(shù)指針間接調(diào)用,仍然具有和直接調(diào)用一樣的特性。
; PVIDERIVEMEMFUNC pVIDeriveMemFunc = static_cast<PVIDERIVEMEMFUNC>(pVIBaseMemFunc);
mov eax, DWORD PTR _pVIBaseMemFunc$[ebp]
mov DWORD PTR _pVIDeriveMemFunc$[ebp], eax
直接將變量pVIBaseMemFunc所占內(nèi)存的前4個字節(jié)(DWORD)的值付給了變量_pVIDeriveMemFunc所占內(nèi)存的前4個字節(jié)中。
; PVVDERIVEMEMFUNC pVVDeriveMemFunc = static_cast<PVVDERIVEMEMFUNC>(pVVBaseMemFunc);
mov eax, DWORD PTR _pVVBaseMemFunc$[ebp]
mov DWORD PTR _pVVDeriveMemFunc$[ebp], eax
直接將變量pVVBaseMemFunc所占內(nèi)存的前4個字節(jié)(DWORD)的值付給了變量pVVDeriveMemFunc所占內(nèi)存的前4個字節(jié)中。由此可以看出,基類的成員函數(shù)指針轉(zhuǎn)化到相應的派生類的成員函數(shù)指針,值保持不變。當然這里的例子繼承關(guān)系相對來說比較簡單,如果存在多繼承和虛繼承的情況下,結(jié)果可能會復雜的多。
3。函數(shù)調(diào)用
下面的函數(shù)調(diào)用都大同小異,這里是列出其中的一個: ; (baseObj.*pVIBaseMemFunc)(10);
mov esi, esp
push 10 ; 0000000aH
lea ecx, DWORD PTR _baseObj$[ebp]
call DWORD PTR _pVIBaseMemFunc$[ebp]
cmp esi, esp
call __RTC_CheckEsp
這里的匯編代碼并沒有給我們太多新鮮的內(nèi)容:將對象的首地址(this指針)存放于寄存器ECX中,接著就將指令轉(zhuǎn)到變量_pVIBaseMemFunc所占內(nèi)存的前4個字節(jié)所表示的地址。
到了這里,我們應該對成員函數(shù)指針有了進一步的了解