關於C++中的非靜態類成員函數指針


  昨天發現了一個問題,就是使用對類中的非靜態成員函數使用std::bind時,不能像普通函數一樣直接傳遞函數名,而是必須顯式地調用&(取地址),於是引申出我們今天的問題:非靜態類成員函數指針和普通函數指針有什么區別?

一.C++中對函數到指針的隱式轉換

  以前在C語言程序設計課上,老師都會說:“函數名就是指向這個函數的指針”。實際上通過查閱cppreference中的隱式轉換規則,其中有這么一句關鍵的話道出了玄機:

函數類型 T 的左值能隱式轉換成指向該函數的指針純右值。這不作用於非靜態成員函數,因為不存在指代非靜態成員函數的左值。

  這一句話表明,實際上我們在把普通函數的函數名當作右值使用的過程中,C++隱式地將其轉換成了指向該函數的的指針。這也就是我們在使用std::bind的過程中可以直接傳遞普通函數的函數名的原因,例如:

int foo(int a,int b)
{
    return (a+b);
}

int main()
{
    auto function = std::bind(foo,std::placeholders::_1,std::placeholders::_2);
    std::cout << function(3,4) << std::endl; //7
    return 0;
}

  但是,這一條隱式轉換規則不作用於非靜態成員函數,因為不存在指代非靜態成員函數的左值。因此我們能看出,指向非靜態成員函數的指針和指向普通函數的指針是有區別的。接下來我們將會討論類成員函數指針(除非有特別說明,下文中的“類成員”都指代“非靜態類成員”)。

 

二.類成員函數指針的聲明和調用

  類成員函數指針的聲明有些類似普通函數指針。但是區別在於,類成員函數是類成員的一部分,所以需要將類成員函數指針使用 ::* 聲明成 成員指針類型。這也表示了類成員函數指針不能單獨地被調用,需要 該類型的實例使用.*(對象的成員指針)指向該類型的指針使用->*(指針的成員指針) 進行調用。同時由於上文說到的C++隱式轉換的規則,我們需要顯示地調用 &(取地址) 獲取非靜態成員函數指針:

class A
{
public:
    void FunA()
    {
        std::cout << "Function A" << endl;
    }
};

int main()
{
    void(A::*p1)() = &A::FunA;
    //p1(); Error
    A a;
    A *p2 = &a;
    (a.*p1)(); // Succ
    (p2->*p1)(); // Succ
    return 0;
}

 

三.探究類成員函數指針

  我們都知道,普通函數指針實際上指向的是函數代碼的起始地址。然而類成員函數指針不僅僅是類成員函數的內存起始地址,它還解決C++中涉及到的多重繼承、虛函數等導致的地址偏移問題。因此,普通函數指針的內存大小是普通指針的大小(32位系統4字節,64位系統8字節),但是成員函數指針卻不一定。

1.多繼承中的使用

  現在有三個類A B C,其中類C public繼承 了A B兩個類。它們的聲明和定義如下:

class A
{
public:
  int a;
  void funA()
  {
    std::cout << "function A" << std::endl;
  }
};

class B
{
public:
  int b;
  void funB()
  {
    std::cout << "function B" << std::endl;
  }
};

class C : public A, public B
{
public:
  int c;
  void funC()
  {
    std::cout << "function C" << std::endl;
  }
};

  現在我們聲明一個函數指針:

int main()
{
  void (C::*mfpC)() = &B::funB;
  return 0;
}

  這個指針很特別,它是一個指向C的類成員的指針,但實際上它指向了C的基類B的成員函數B::funB。現在我們編譯代碼並調用gdb調試查看mfpC的信息:

(gdb) p mfpC
$1 = (void (C::*)(C * const)) 0x8000966 <B::funB()>, this adjustment 4
(gdb) p sizeof(mfpC)
$2 = 16
(gdb) x/16xb &mfpC
0x7ffffffee830: 0x66    0x09    0x00    0x08    0x00    0x00    0x00    0x00
0x7ffffffee838: 0x04    0x00    0x00    0x00    0x00    0x00    0x00    0x00

  可以看見,mfpC實際占用了16個字節(運行環境為64位操作系統),這16個字節中不僅包含了它所指向的函數的起始地址,還包含了this指針的調整值。因為對於多重繼承來說,如果類成員函數指針保存的是非最左基類的成員函數地址,根據C++標准,非最左基類實例的開始地址肯定不同於派生類實例的開始地址,所以需要調整this指針,使其指向非最左基類實例。

2.有虛函數的使用

  現在修改代碼,在類B中定義一個虛函數funC,並在類C中重寫它:

class A
{
public:
  int a;
  void funA()
  {
    std::cout << "function A" << std::endl;
  }
};

class B
{
public:
  int b;
  void funB()
  {
    std::cout << "function B" << std::endl;
  }

  virtual void funC()
  {
    std::cout << "function C in class B" << std::endl;
  }
};

class C : public A, public B
{
public:
  int c;
  void funC()
  {
    std::cout << "function C in class C" << std::endl;
  }
};

  然后我們實例化一個類C的對象c和一個指向B::funC的成員函數指針mfpB,最后通過c調用它:

int main()
{
  C c;
  void (B::*mfpB)() = &B::funC;
  (c.*mfpB)();
  return 0;
}

  結果顯而易見,輸出的不是"function C in class B",而是"function C in class C"。

  由此可見,成員函數指針雖然看起來和普通的函數指針有些類似,但實際上兩者還是有很大的差別的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM