天天看點

類成員函數的指針

今天在Qt中使用unix函數庫時,需要傳遞一個類的成員函數的指針,遂報錯。。。google之,淺嘗成員函數指針一詞。

原文出處https://kelvinh.github.io/blog/2014/03/27/cpp-tutorial-pointer-to-member-function/,原文内容有一些錯誤,已修正。

關于成員函數指針

成員函數指針是C++最少用到的文法之一,甚至有經驗的C++碼農有時候也會被它搞暈。這是一篇針對于初學者的教程,同時也給有經驗的碼農分享了一些我個人對底層機制的挖掘。在開始之前,讓我們先看一段代碼:

//mem_fun1.cpp
#include <iostream>

class Foo {
public:
    Foo(int i = 0) { _i = i; }
    void f() {
        std::cout << "Foo::f()" << std::endl;
    }
private:
    int _i;
};

int main() {
    Foo *p = 0;
    p->f();
}

// Output:
// Foo::f()      

為什麼我們能通過一個空指針調用成員函數?看起來好像編譯器根本不鳥p的值是什麼,隻介意p的類型。别着急,咱先吊吊胃口,把答案留到後面的章節。現在,我們所能知道的是,編譯器準确地知道要調用哪個函數。這就是所謂的“靜态綁定”。因為成員函數可以靜态綁定(并不是總能靜态綁定,待會兒讨論),是以它們的位址是在編譯階段決定的(同樣并不是永遠如此)。直覺地講,應該有一種方法可以儲存成員函數的位址。而且,真有這麼一種方法,那就是——成員函數指針。

C++文法

下面的文法展示了如何聲明一個成員函數指針:

Return_Type (Class_Name::* pointer_name) (Argument_List);

Return_Type:   member function return type.
Class_name:    name of the class in which the member function is declared.
Argument_List: member function argument list.
pointer_name:  a name we'd like to call the pointer variable.      

例如,我們定義一個類 

Foo

 和一個成員函數 

f

 :

int Foo::f(string);      

我們可以給這個成員函數指針起一個“高大上”的名字 

fptr

 ,是以我們就有了下面的内容:

Return_Type:   int
Class_Name:    Foo
Argument_List: string

declaration of a  pointer-to-member function named "fptr":
  int (Foo::*fptr) (string);      

現在,指定一個成員函數給我們“高大上”的 

fptr

 :

fptr = &Foo::f;      

當然,就連腦殘都知道可以将聲明和初始化結合起來:

int (Foo::*fptr) (string) = &Foo::f;      

為了通過函數指針來調用成員函數,我們使用成員指針選擇操作符(翻譯君表示也不知道該怎麼翻譯,原文是pointer-to-member selection operators), 

.*

 或者 

->*

 。下面的代碼示範了基本用法:

#include <iostream>
#include <string>

using namespace std;

class Foo {
public:
    int f(string str) {
        cout << "Foo::f()" << endl;
        return 1;
    }
};

int main(int argc, char *argv[]) {
    int (Foo::*fptr) (string) = &Foo::f;
    Foo obj;
    (obj.*fptr)("str"); // 通過對象來調用 Foo::f()
    Foo *p = &obj;
    (p->*fptr)("str"); // 通過指針來調用 Foo::f()
}      

注意: 

.*fptr

 綁定fptr到對象obj,而 

->*fptr

 則綁定fptr到指針p所指向的對象。(還有一個 重要的差別 是:我們可以重載後者,卻不能重載前者)。在 

(obj.*fptr)

 和 

(p->*fptr)

 兩邊的括号是文法所強制要求的。

成員函數指針不是正常指針

成員函數指針不像正常指針那樣儲存某個“準确”的位址。我們可以把它想像成儲存的是成員函數在類布局中的“相對”位址。讓我們來展示一下二者的不同。我們隻對類 

Foo

 做一個小手術:将成員函數 

f

 變成 

static

 :

#include <iostream>
#include <string>

using namespace std;

class Foo {
public:
    static int f(string str) {
        cout << "Foo::f()" << endl;
        return 1;
    }
};

int main(int argc, char *argv[]) {
    // int (Foo::*fptr) (string) = &Foo::f; // 錯誤
    int (*fptr) (string) = &Foo::f; // 正确
    (*fptr)("str"); // 調用 Foo::f()
}      

一個靜态成員函數沒有 

this

 指針。除了它和其它的類成員共享命名空間Foo(在我們的例子中命名空間是 

Foo::

 )之外,它和正常全局函數是一樣的。是以,靜态成員函數不是類的一部分,成員函數指針的文法對正常函數指針并不成立,例如上面例子中的靜态成員函數指針。

int (Foo::*fptr) (string) = &Foo::f;      

上面這行代碼在g++ 4.2.4中編譯的錯誤資訊為:“不能将 

int (*)(std::string)

 轉化成 

int (Foo::*)(std::string)

 ”。這個例子證明了成員函數指針不是正常指針。另外,為什麼C++如此費心地去發明這樣的文法?很簡單,因為它和正常指針是不同的東西,而且這樣的類型轉換也是違反直覺的。

C++類型轉換規則

非虛函數情形

我們在前面一節看到,成員函數指針并不是正常指針,是以,成員函數指針(非靜态)不能被轉換成正常指針(當然,如果哪個腦殘真想這麼做的話,可以使用彙編技術來暴力解決),因為成員函數指針代表了 偏移量 而不是 絕對位址 。但是,如果是成員函數指針之間互相轉換呢?

//memfunc4.cpp
#include <iostream>

class Foo {
public:
    int f(char *c = 0) {
        std::cout << "Foo::f()" << std::endl;
        return 1;
    }
};

class Bar {
public:
    void b(int i = 0) {
        std::cout << "Bar::b()" << std::endl;
    }
};

class FooDerived : public Foo {
public:
    int f(char *c = 0) {
        std::cout << "FooDerived::f()" << std::endl;
        return 1;
    }
};

int main(int argc, char *argv[]) {
    typedef int (Foo::*FPTR) (char*);
    typedef void (Bar::*BPTR) (int);
    typedef int (FooDerived::*FDPTR) (char*);

    FPTR fptr = &Foo::f;
    BPTR bptr = &Bar::b;
    FDPTR fdptr = &FooDerived::f;

    // bptr = static_cast<void(Bar::*)(int)>(fptr); // 錯誤
    fptr = static_cast<int(Foo::*)(char*)>(fdptr); // 正确,向上轉型

    Bar obj;
    ( obj.*(BPTR) fptr )(1); // 調用 Foo::f()
}

// Output:
// Foo::f()      

在上面的代碼中,首先使用 

typedef

 。它讓這些繁瑣的定義變得清晰起來。fptr的類型是:

int (Foo::*) (char*);      

或者等價地說——FPTR。如果我們仔細看上面的代碼:

bptr = static_cast<void(Bar::*)(int)>(fptr);      

這一行會出錯,因為 不同的非靜态非虛成員函數具有強類型是以不能互相轉化 ,但是:

fptr = static_cast<int(Foo::*)(char*)>(fdptr);      

這一行卻是正确的!我們可以将一個指向派生類的指針指派給一個指向其基類的指針(即"is-a"關系),這個規則提供了将 

FooDerived::*

 應用到任何 

Foo::*

 能被應用的地方的基本保證。在代碼最後兩行:

Bar obj;
( obj.*(BPTR) fptr)(1);      

盡管我們想要調用的是 

Bar::b()

 ,但是 

Foo::f()

 卻被調用了,因為fptr是靜态綁定(翻譯君注:這裡的靜态綁定,即指在編譯階段,fptr的值已經确定了,是以即使進行強制轉換,依然調用的是Foo類的f()函數)。(請圍觀成員函數調用和 

this

指針)

虛函數情形

我們隻将前例中的所有成員函數變成虛函數,其它都不動:

#include <iostream>

class Foo {
public:
    virtual int f(char *c = 0) {
        std::cout << "Foo::f()" << std::endl;
        return 1;
    }
};

class Bar {
public:
    virtual void b(int i = 0) {
        std::cout << "Bar::b()" << std::endl;
    }
};

class FooDerived : public Foo {
public:
    int f(char *c = 0) {
        std::cout << "FooDerived::f()" << std::endl;
        return 1;
    }
};

int main(int argc, char *argv[]) {
    typedef int (Foo::*FPTR) (char*);
    typedef void (Bar::*BPTR) (int);

    FPTR fptr = &Foo::f;
    BPTR bptr = &Bar::b;

    FooDerived objDer;
    (objDer.*fptr)(0); // 調用 FooDerived::f(),而不是 Foo::f()

    Bar obj;
    ( obj.*(BPTR) fptr )(1);// 調用 Bar::b(),而不是 Foo::f()
}

// Output:
// FooDerived::f()
// Bar::b()      

如我們所看到的,當成員函數是虛函數的時候,成員函數能夠具有多态性并且現在調用的是 

FooDerived::f()

 ,而且 

Bar::b()

 也能被正确調用了。因為 “一個指向虛成員的指針能在不同位址空間之間傳遞,隻要二者使用的對象布局一樣” (C++ Bjarne Stroustrup 的 《C++程式設計語言》 )。當成員函數是虛函數的時候,編譯器會生成虛函數表,來儲存虛函數的位址。隻要成員函數在類中的相對位址一樣,即使是在不同類之間,成員函數指針也能互相轉換。這是和非虛函數之間的最大不同,是以,運作時的行為也是不同的。

成員函數指針數組及其應用

成員函數指針的一個重要應用就是根據輸入來生成響應事件,下面的 

Printer

 類和指針數組 

pfm

 展示了這一點:

#include <stdio.h>
#include <string>
#include <iostream>

class Printer { // 一台虛拟的列印機
public:
    void Copy(char *buff, const char *source) { // 複制檔案
        strcpy(buff, source);
    }

    void Append(char *buff, const char *source) { // 追加檔案
        strcat(buff, source);
    }
};

enum OPTIONS { COPY, APPEND }; // 菜單中兩個可供選擇的指令

typedef void(Printer::*PTR) (char*, const char*); // 成員函數指針

void working(OPTIONS option, Printer *machine,
             char *buff, const char *infostr) {
    PTR pmf[2] = { &Printer::Copy, &Printer::Append }; // 指針數組

    switch (option) {
    case COPY:
        (machine->*pmf[COPY])(buff, infostr);
        break;
    case APPEND:
        (machine->*pmf[APPEND])(buff, infostr);
        break;
    }
}

int main() {
    OPTIONS option;
    Printer machine;
    char buff[40];

    working(COPY, &machine, buff, "Strings ");
    working(APPEND, &machine, buff, "are concatenated!");

    std::cout << buff << std::endl;
}

// Output:
// Strings are concatenated!      

在上述代碼中, 

working

 是一個用來執行列印工作的函數,它需要幾個參數:1. 菜單選項;2. 可用的列印機;3. 字元串目的地;4. 字元串來源。上述代碼中字元串來源是兩個字元串常量"Strings "和"concatenated!",而成員函數指針數組被用來根據菜單選項執行相應的列印動作。

成員函數指針另外一個重要的應用可以在STL的 

mem_fun()

 中找到。(翻譯君去看了一下 

mem_fun()

 的源代碼,原來是用成員函數來構造仿函數functor的。)

成員函數調用和 

this

 指針

現在我們回到文章最開始的地方。為什麼一個空指針也能調用成員函數?對于一個非虛函數調用,例如: 

p->f()

 ,編譯器會生成類似如下代碼:

Foo *const this = p;
void Foo::f(Foo *const this) {
    std::cout << "Foo::f()" << std::endl;
}      

是以,不管p的值是神馬,函數 

Foo::f

 都可以被調用,就像一個全局函數一樣!p被作為 

this

 指針并當作參數傳遞給了函數。而在我們的例子中 

this

 指針并沒有被解引用,是以,編譯器放了我們一馬(翻譯君表示,這其實跟編譯器沒有關系,即使我們在成員函數中使用this指針,編譯照樣能通過,隻不過在運作時會crash)。假如我們想知道成員變量 

_i

 的值呢?那麼編譯器就需要解引用 

this

 指針,這隻有一個結果,那就是我們的好兄弟——未定義行為(undefined behavior)。對于一個虛函數調用,我們需要虛函數表來查找正确的函數,然後, 

this

 指針被傳遞給這個函數。

這就是非虛函數、虛函數、靜态函數的成員函數指針使用不用實作方式的根本原因。

結論

簡單總結一下,通過上述文章,我們學到了:

  1. 成員函數指針聲明和定義的文法
  2. 使用成員指針選擇操作符來調用成員函數的文法
  3. 使用 

    typedef

     寫出更加清晰的代碼
  4. 非虛成員函數、虛函數、靜态成員函數之間的差別
  5. 成員函數指針和正常指針的對比
  6. 不同情形下的成員函數指針轉換規則
  7. 如何使用成員函數指針數組來解決特定的設計問題
  8. 編譯器是如何解釋成員函數調用的

我衷心希望這篇教程能打開通往上述要點的相關進階技巧的大門,例如多重繼承、虛繼承下的成員函數指針,以及編譯器的相關實作。。

繼續閱讀