天天看點

C++ 指向類成員函數指針的用法(轉自維基百科)

類成員函數指針

類成員函數指針(member function pointer),是C++語言的一類指針資料類型,用于存儲一個指定​​類​​具有給定的形參清單與傳回值類型的成員函數的通路資訊。

目錄

  • ​​1 文法​​
  • ​​2 語義​​
  • ​​3 類成員函數指針的用途​​
  • ​4 例子​
  • ​​4.1 未知繼承的成員函數指針例子​​
  • ​​5 參考文獻​​

文法

使用​

​::*​

​聲明一個成員指針類型,或者定義一個成員指針變量。使用​

​.*​

​或者​

​->*​

​調用類成員函數指針所指向的函數,這時必須綁定(binding)于成員指針所屬類的一個執行個體的位址。例如:

struct X {
  void f(int){ };
  int a;
};
void (X::* pmf)(int); //一個類成員函數指針變量pmf的定義
pmf = &X::f;            //類成員函數指針變量pmf被指派

X ins, *p;
p=&ins;
(ins.*pmf)(101);       //對執行個體ins,調用成員函數指針變量pmf所指的函數
(p->*pmf)(102);      //對p所指的執行個體,調用成員函數指針變量pmf所指的函數      

由于​​C++運算符優先級清單​​中,函數調用運算符​

​()​

​的優先級高于​

​.*​

​與​

​->*​

​,是以成員函數指針所指的函數被調用時,必須把執行個體對象或執行個體指針、​

​.*​

​或​

​->*​

​運算符、成員函數指針用括号括起來,如上例所示。

C++标準規定,非靜态成員函數不是左值,是以非靜态成員函數不存在表達式中從函數左值到指針右值的隐式轉換,非靜态成員函數指針必須通過&運算符顯式獲得。是以上例中,pmf = X::f; 将編譯報錯。

語義

不同于普通函數,類成員函數的調用有一個特殊的不寫在形參表裡的隐式參數:類執行個體的位址。是以,C++的類成員函數調用使用​

​thiscall​

​調用協定。類成員函數是限定(qualification)于所屬類之中的。

同樣,類成員函數指針與普通函數指針不是一碼事。前者要用.*與->*運算符來使用,而後者可以用​

​*​

​運算符(稱為“解引用”dereference,或稱“間址”indirection)。普通函數指針實際上儲存的是函數體的開始位址,是以也稱“代碼指針”,以差別于C/C++最常用的資料指針。而類成員函數指針就不僅僅是類成員函數的記憶體起始位址,還需要能解決因為C++的​​多重繼承​​、​​虛繼承​​而帶來的類執行個體位址的調整問題。是以,普通函數指針的尺寸就是普通指針的尺寸,例如32位程式是4位元組,64位程式是8位元組。而類成員函數指針的尺寸最多有4種可能:

  • 單倍指針尺寸:對于非派生類、單繼承類,類成員函數指針儲存的就是成員函數的記憶體起始位址。
  • 雙倍指針尺寸:對于多重繼承類,類成員函數指針儲存的是成員函數的記憶體起始位址與this指針調整值。因為對于多繼承類的類成員函數指針,可能對應于該類自身的成員函數,或者最左基類的成員函數,這兩種情形都不需要調整this指針。如果類成員函數指針儲存的其他的非最左基類的成員函數的位址,根據C++标準,非最左基類執行個體的開始位址與派生類執行個體的開始位址肯定不同,是以需要調整this指針,使其指向非最左基類執行個體。
  • 三倍指針尺寸:對于多重繼承且虛繼承的類。類成員函數指針儲存的就是成員函數的記憶體起始位址、this指針調整值、虛基類調整值在虛基表(vbtable)中的位置共計3項。以常見的“菱形虛繼承”為例。最派生類多重繼承了兩個類,稱為左父類、右父類;兩個父類共享繼承了一個虛基類。最派生類的成員函數指針可能儲存了這四個類的成員函數的記憶體位址。如果成員函數指針儲存了最派生類或左父類的成員函數位址,則最為簡單,不需要調整this指針值。如果如果成員函數指針儲存了右父類的成員函數位址,則this指針值要加上一個偏移值,指向右父類執行個體的位址。如果成員函數指針儲存了虛基類的成員函數位址,由于C++類繼承的複雜多态性質,必須到最派生類虛基表的相應條目查出虛基類位址的偏移值,依此來調整this指針指向虛基類。
  • 四倍指針尺寸:C++标準允許一個僅僅是聲明但沒有定義的類(forward declaration)的成員函數指針,可以被定義、被調用。這種情況下,實際上對該類一無所知。這稱作未知類型(unknown)的成員函數指針。該類的成員函數指針需要留出4項資料位置,分别用于儲存成員函數的記憶體起始位址、this指針調整值、虛基表到類的開始位址的偏移值(vtordisp)、虛基類調整值在虛基表(vbtable)中的位置,共計4項。

C++标準并沒有明确規定類成員指針在派生類與基類之間的類型轉換。但不允許類成員函數指針與其它無繼承關系的類的成員函數指針互相轉換。不允許與普通函數指針互相轉換。

如果把基類的虛函數賦給派生類的成員函數指針,例如

DerivedClass_Func_to_Mem = & BaseClass::virtualFunc;      

實際上是把基類虛表中該虛函數條目對應到了派生類成員函數指針。調用該成員函數指針會執行到哪個函數,需要動态決定。

類成員函數指針可以用0指派;可以用==運算符、!=運算符。但不允許使用其他的指針算術與比較運算符,如>、<等等。

不能把類的靜态成員函數指派給類成員函數指針。類的靜态函數隻能指派給普通函數指針。因為類的靜态成員函數不具有this指針,不采用thiscall調用協定,實際上是限定于類作用域的普通函數。 是以,确切地說,應該稱“類非靜态成員函數指針”。

對于​​g++​​編譯器,不支援把虛基類的成員函數指針賦給派生類的成員函數指針。也即,g++不支援在虛繼承關系下的成員函數指針的upcast。這大大簡化了g++成員函數指針的實作難度。g++編譯出來的成員函數指針長度都是8位元組,其中的高4位元組是用于多重繼承時調整this指針的偏移值,單繼承時該值為0;低4位元組是個union結構,對于非虛成員函數就是函數體的記憶體起始位址,對于虛函數是該函數在虛表(vtable)中的位址位元組偏移量再加上1。這是因為,函數體的記憶體起始位址起碼是4位元組邊界對齊,是以該值是4的的倍數;而虛表中每個條目是4位元組長度(對于32位程式),虛函數所對應的虛表條目在虛表中的按位元組計算的偏移量也是4的倍數,加上1後就是個奇數。進而可以區分非虛函數與虛函數兩種情形。

​​Microsoft Visual C++​​編譯器支援在虛繼承關系下的成員函數指針的upcast。這大大複雜化了該編譯器的成員函數指針的實作。Visual C++定義了三個關鍵字:​

​__single、__multi、__virtual_inheritance​

​分别對應于類是單繼承、多重繼承、虛繼承關系;此外還有第四種情況:類在提前聲明(forward declaration)時的未知類型(unknown)成員函數指針。上述四種情況,Visual C++編譯出的32位程式的成員函數指針長度分别是4位元組、8位元組、12位元組、16位元組。上述3個繼承關系關鍵字用于在類定義時,顯式規定該類的成員函數指針的長度及儲存在其中的資訊類别。​​[1]​​如果在一個源檔案(編譯單元)中在沒有一個類的定義的情況下調用了該類的未知類型(unknown)成員函數指針,顯然必須在其他源檔案中對該未知類型(unknown)成員函數指針給出類型定義并指派,這就必須使用編譯選項​

​/vmg​

​來編譯此源檔案。​

​/vmg​

​編譯選項使得編譯單元中所有的類成員函數指針均為四倍尺寸。可以用上述3個Microsoft定義的繼承關系關鍵字把那些不是未知類型的成員函數指針顯式地給出其類繼承關系是單繼承、多繼承、虛繼承,進而使該類的成員函數指針分别是單倍、二倍、三倍的尺寸。

類成員函數指針的用途

類成員函數指針的主要用途是把資料與相關代碼結合在一起。這與​​委托​​(delegate)、​​函子​​(functor)、​​閉包​​(closure)等概念很像。雖然C++對此支援的并不太好。

​​MFC​​類體系中,​​Windows消息​​傳遞處理機制是基于CCmdTarget類及其派生類的靜态資料成員與靜态成員函數GetThisMessageMap()。使用者所寫的類中的Windows消息處理函數(例如OnCommand)必須轉換為CCmdTarget::*的成員函數指針類型AFX_PMSG,儲存在該使用者類的_messageEntries靜态數組中。

typedef void (CCmdTarget::*AFX_PMSG)(void);      

調用使用者類中該消息處理函數時,根據該函數儲存在_messageEntries中的signature(一個無符号整型表示的函數的形參類型清單與傳回值類型),把類型為void (CCmdTarget::*AFX_PMSG)(void)的成員函數指針強制轉為其它類型的CCmdTarget成員函數指針(例如void (AFX_MSG_CALL CWnd::*pfn_v_i_i)(int, int),目前在union MessageMapFunctions中列出了近百種CCmdTarget成員函數指針),然後調用轉換後的成員函數指針。這是基于Visual C++編譯器把單繼承的成員函數指針編譯為隻儲存了函數的記憶體起始位址,是以可以在同一個單繼承類中把一種類型的成員函數指針強制轉換為另一種成員函數指針,或者把單繼承派生類的成員函數指針強制轉換為基類成員函數指針。這是打破了C++标準的違例辦法。例如,對于CWnd::OnCommand函數,轉換過程是:

BOOL (CWnd::*)(WPARAM, LPARAM lParam) => void (CWnd::*)() => void (CCmdTarget::*)()      

例子

#include <iostream>

class Test; //一個未定義的類。

class Test2 
{
       int i;
public:
  void foo(){ }
};

class Test3
{
  int i;
public:
        void foo(){ }
};


class Test4:public Test2 , public Test3 //多繼承的類 
{
  int i;
public:
         void foo(  ) { }
};

class Test5:virtual public Test4 //虛繼承的類 
{
  int i;
public:
         void foo(  ) { }
};

int main()
{ 
std::cout <<"Test3類成員函數指針長度="<<sizeof(void(Test3::*)()) <<'\n';
std::cout <<"Test4類成員函數指針長度="<<sizeof(void(Test4::*)()) <<'\n';
std::cout <<"Test5類成員函數指針長度="<<sizeof(void(Test5::*)()) <<'\n';  
std::cout <<"Test類成員函數指針長度="<<sizeof(void(Test::*)()) <<'\n';

//以下可以打開IDE的反彙編(Disassembly)視窗觀察成員函數指針的指派與調用
Test5 a;                                                             //定義一個執行個體
void (Test5::* pfunc)()=&Test5::foo;                //定義類成員函數指針并指派
pfunc=&Test5::Test2::foo;
pfunc=&Test2::foo;
pfunc=&Test5::Test3::foo;

(a.*pfunc)();  //調用類成員函數指針,同時使用了虛基表(vbtbl)索引值與this指針調整值
}      

未知繼承的成員函數指針例子

使用Microsoft Visual C++編譯32位程式:

//main.cpp 不需要任何特殊的編譯選項

class Test;                                         //一個forward declaration、未定義的類

typedef void(Test::*NULLFUNCPTR)(); //未知繼承的類成員函數指針的類型定義
extern Test  var;                                 //外部定義的全局對象
extern  NULLFUNCPTR pfunc;           //外部定義的類成員函數指針的變量
void set();                                      //外部定義的對類成員函數指針pfunc初始化

void Helper(Test &var, NULLFUNCPTR pf)
{
  (var.*pf)();
}

int main()
{ 
  size_t ss=sizeof(NULLFUNCPTR);
        set();  
  Helper(var, pfunc  );
}      

繼續閱讀