C++語言學習(十四)——C++類成員函數調用分析

C++語言學習(十四)——C++類成員函數調用分析

1、C++成員函數

一、C++成員函數的編譯

C++中的函數在編譯時會根據命名空間、類、參數簽名等信息進行從新命名,造成新的函數名。函數重命名的過程經過一個特殊的Name Mangling(名字編碼)算法來實現。Name Mangling算法是一種可逆的算法,既能夠經過現有函數名計算出新函數名,也能夠經過新函數名逆向推導出原有函數名。
Name Mangling算法能夠確保新函數名的惟一性,只要命名空間、所屬的類、參數簽名等有一個不一樣,那麼產生的新函數名也不一樣。
不一樣的編譯器有不一樣的 Name Mangling 算法,產生的函數名也不同。ios

二、this指針

this指針屬性以下:
A、名稱屬性:標識符this表示。
B、類型屬性:classname const
C、值屬性:表示當前調用該函數對象的首地址。
D、做用域:this指針是編譯器默認傳給類中非靜態函數的隱含形參,其做用域在非靜態成員函數的函數體內。
E、連接屬性:在類做用域中,不一樣類的非靜態成員函數中,this指針變量的連接屬性是內部的,但其所指對象是外部的,即this變量是不一樣的實體,但指向對象是同一個。
F、存儲類型:this指針是由編譯器生成,當類的非靜態成員函數的參數個數必定時,this指針存儲在ECX寄存器中;若該函數參數個數未定(可變參數函數),則存放在棧中。
this指針並非對象的一部分,this指針所佔的內存大小是不會反映在sizeof操做符上的。this指針的類型取決於使用this指針的成員函數類型以及對象類型。
類的成員函數默認第一個參數爲T
const register this。
this在成員函數的開始執行前構造,在成員函數執行結束後清除。算法

2、C++成員函數指針

一、C++成員函數指針簡介

C++語言規定,成員函數指針具備contravariance特性,即基類的成員函數指針能夠賦值給派生類的成員函數指針,C++語言提供了默認的轉換方式,但反過來不行。
C++編譯器在代碼編譯階段會對類對象調用的成員函數進行靜態綁定(虛函數進行動態綁定),類成員函數的地址在代碼編譯時就肯定,類成員函數地址可使用成員函數指針進行保存。
成員函數指針定義語法以下:數組

ReturnType (ClassName::* pointerName) (ArgumentLList);
ReturnType:成員函數返回類型
ClassName: 成員函數所屬類的名稱
Argument_List: 成員函數參數列表
pointerName:指針名稱
class Test
{
public:
    void print()
    {
        cout << "Test::print" << endl;
    }
};

成員函數指針語法極其嚴格:
A、不能使用括號:例如&(Test::print)不對。
B、 必須有限定符:例如&print不對,即便在類ClassName做用域內也不行。
C、必須使用取地址符號:直接寫Test::print不行,必須寫:&Test::print。
Test類的成員函數print的函數指針聲明以下:
void (Test::*pFun)();
初始化以下:
pFunc = &Test::print;
Test類的成員函數print的函數指針聲明及初始化以下:
void (Test::* pFunc)() = &Test::print;
一般,爲了簡化代碼,使用typedef關鍵字。ide

typedef void (Test::*pFunc)();
pFunc p = &Test::print;

能夠經過函數指針調用成員函數,示例代碼以下:函數

#include <iostream>

using namespace std;

class Test
{
public:
    void print()
    {
        cout << "Test::print" << endl;
    }
};

int main(int argc, char *argv[])
{
    void (Test::* pFunc)() = &Test::print;
    Test test;
    //經過對象調用成員函數
    (test.*pFunc)();//Test::print
    Test* pTest = &test;
    //經過指針調用成員函數
    (pTest->*pFunc)();//Test::print
    //pFunc();//error
    //error: must use '.*' or '->*' to call pointer-to-member
    //function in 'pFunc (...)', e.g. '(... ->* pFunc) (...)'

    return 0;
}

上述代碼中,.*pFunc將pFunc綁定到對象test,-&gt;*pFunc綁定pFunc到pTest指針所指向的對象。
成員函數指針不是常規指針(保存的是某個確切地址),成員函數指針保存的是成員函數在類佈局中的相對地址。佈局

二、C++成員函數地址

C++成員函數使用thiscall函數調用約定。C++靜態成員函數、普通成員函數的函數地址在代碼區,虛成員函數地址是一個相對地址。學習

#include <iostream>

using namespace std;

class Parent
{
public:
    Parent(int i, int j)
    {
        m_i = i;
        m_j = j;
        cout << "Parent(int i, int j): " << this << endl;
    }
    virtual void print()
    {
        cout << "Parent::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
    }
    virtual void sayHello()
    {
        cout << "Parent::sayHello()" << endl;
    }
    virtual void func()
    {
        cout << "Parent::func()" << endl;
    }
    virtual ~Parent()
    {
        cout << "~Parent(): " << this << endl;
    }
    static void display()
    {
        cout << "Parent::display()" << endl;
    }
    int add(int v)
    {
        return m_i + m_j + v;
    }
protected:
    int m_i;
    int m_j;
};

int main(int argc, char *argv[])
{
    cout <<&Parent::display<<endl;
    cout <<&Parent::print<<endl;
    cout <<&Parent::sayHello<<endl;
    cout <<&Parent::func<<endl;

    return 0;
}

上述代碼中,打印出的全部的成員函數的地址爲1。緣由在於輸出操做符<<沒有對C++成員函數指針類型進行重載,C++編譯器將C++成員函數指針類型轉換爲bool類型進行了輸出,因此全部的輸出爲1。所以,C++成員函數地址進行打印時不能使用cout,能夠用printf輸出,由於printf能夠接收任意類型的參數,包括__thiscall類型。this

#include <iostream>

using namespace std;

class Parent
{
public:
    Parent(int i, int j)
    {
        m_i = i;
        m_j = j;
        cout << "Parent(int i, int j): " << this << endl;
    }
    virtual void print()
    {
        cout << "Parent::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
    }
    virtual void sayHello()
    {
        cout << "Parent::sayHello()" << endl;
    }
    virtual void func()
    {
        cout << "Parent::func()" << endl;
    }
    virtual ~Parent()
    {
        cout << "~Parent(): " << this << endl;
    }
    static void display()
    {
        cout << "Parent::display()" << endl;
    }
    int add(int v)
    {
        return m_i + m_j + v;
    }
protected:
    int m_i;
    int m_j;
};

int main(int argc, char *argv[])
{
    //靜態成員函數
    cout << "static member function addree:" << endl;
    printf("0x%p\n", &Parent::display);
    printf("0x%p\n", Parent::display);
    //普通成員函數
    cout << "normal member function addree:" << endl;
    printf("0x%p\n", &Parent::add);
    cout << "virtual member function addree:" << endl;
    //虛成員函數
    printf("%d\n", &Parent::print);//1
    printf("%d\n", &Parent::sayHello);//5
    printf("%d\n", &Parent::func);//9

    return 0;
}

三、C++編譯器成員函數指針的實現

C++編譯器要實現成員函數指針,必須解決下列問題:
A、成員函數是否是虛函數。
B、成員 函數運行時,需不須要調整this指針,如何調整。
不須要調整this指針的狀況以下:
A、繼承樹最頂層的類。
B、單繼承,若全部類都不含有虛函數,那麼繼承樹上全部類都不須要調整this指針。
C、單繼承,若最頂層的類含有虛函數,那麼繼承樹上全部類都不須要調整this指針。
可能須要進行this指針調整的狀況以下:
A、多繼承
B、單繼承,最頂的base class不含virtual function,但繼承類含虛函數,繼承類可能須要進行this指針調整。
Microsoft VC對C++成員函數指針的實現採用的是Microsoft一向使用的Thunk技術。Microsoft將成員函數指針分爲兩種:編碼

struct pmf_type1{
    void* vcall_addr;
};

struct pmf_type2{
    void* vcall_addr;
    int  delta;  //調整this指針用
};

vcall_addr是Microsoft 的Thunk技術核心所在。vcall_addr是一個指針,隱藏了它所指的函數是虛擬函數仍是普通函數的區別。若是所指的成員函數是一個普通成員函數,vcall_addr是成員函數的函數地址。若是所指的成員函數是虛成員函數,那麼vcall_addr指向一小段代碼,這段代碼會根據this指針和虛函數索引值尋找出真正的函數地址,而後跳轉到真實的函數地址處執行。
Microsoft根據狀況選用函數指針結構表示成員函數指針,使用Thunk技術(vcall_addr)實現虛擬函數/非虛擬函數的自適應,在必要的時候進行this指針調整(使用delta)。
GCC對於成員函數指針統一使用下面的結構進行表示:spa

struct        
{    
    void* __pfn;  //函數地址,或者是虛擬函數的index    
    long __delta; // offset, 用來進行this指針調整   
};

不論是普通成員函數,仍是虛成員函數,信息都記錄在__pfn。通常來講由於對齊的關係,函數地址都至少是4字節對齊的。即函數地址的最低位兩個bit老是0。 GCC充分利用了這兩個bit。若是是普通的函數,__pfn記錄函數的真實地址,最低位兩個bit就是全0,若是是虛成員函數,最後兩個bit不是0,剩下的30bit就是虛成員函數在函數表中的索引值。
GCC先取出函數地址最低位兩個bit看看是否是0,如果0就使用地址直接進行函數調用。若不是0,就取出前面30位包含的虛函數索引,經過計算獲得真正的函數地址,再進行函數調用。
GCC和Microsoft對成員函數指針實現最大的不一樣就是GCC老是動態計算出函數地址,並且每次調用都要判斷是否爲虛函數,開銷天然要比Microsoft的實現要大一些。
在this指針調整方面,GCC和Mircrosoft的作法是同樣的。不過GCC在任何狀況下都會帶上__delta變量,若是不須要調整,__delta=0
GCC的實現比Microsoft簡單,在全部場合其實現方式都是同樣的。

四、C++成員函數指針的限制

C++語言的規定,基類的成員函數指針能夠賦值給派生類的成員函數指針,不容許繼承類的成員函數指針賦值給基類成員函數指針。
 C++規定編譯器必須提供一個從基類成員函數指針到繼承類成員函數指針的默認轉換。C++編譯器提供的默認轉換最關鍵的就是this指針調整。
所以,通常狀況下不要將繼承類的成員函數指針賦值給基類成員函數指針。不一樣C++編譯器可能有不一樣的表現。
解決方案:
A、不要使用static_cast將繼承類的成員函數指針賦值給基類成員函數指針,若是必定要使用,首先肯定沒有問題。
B、若是必定要使用static_cast,注意不要使用多繼承。
C、若是必定要使用多繼承的話,不要把一個基類的成員函數指針賦值給另外一個基類的函數指針。
D、單繼承要麼所有不使用虛函數,要麼所有使用虛函數。不要使用非虛基類,卻讓子類包含虛函數。

#include <iostream>
#include <string>

using namespace std;

class Parent
{
public:
    Parent(int i, int j)
    {
        m_i = i;
        m_j = j;
    }
    void print()
    {
        cout << "Parent::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
    }
    double sum()
    {
        cout << "Parent::" << __func__<< endl;
        double ret = m_i + m_j;
        cout <<ret << endl;
        return ret;
    }
    void display()
    {
        cout << "Parent::display()" << endl;
    }
    int add(int value)
    {
        return m_i + m_j + value;
    }
protected:
    int m_i;
    int m_j;
};

class ChildA : public Parent
{
public:
    ChildA(int i, int j, double d):Parent(i, j)
    {
        m_d = d;
    }
    void print()
    {
        cout << "ChildA::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
        cout << "m_d = "<< m_d << endl;
    }
    double sum()
    {
        cout << "ChildA::" << __func__<< endl;
        double ret = m_i + m_j + m_d;
        cout << ret << endl;
        return ret;
    }
private:
    void display()
    {
        cout << "ChildA::display()" << endl;
    }
private:
    double m_d;
};

int main(int argc, char *argv[])
{
    Parent parent(100,200);
    ChildA childA(1,2,3.14);
    Parent* pTestA = &childA;
    typedef void (Parent::*pPrintFunc)();
    pPrintFunc pPrint = &Parent::print;
    typedef double (Parent::*pSumFunc)();
    pSumFunc pSum = &Parent::sum;
    typedef void (Parent::*pDisplayFunc)();
    pDisplayFunc pDisplay = &Parent::display;

    printf("0x%X\n",pPrint);
    printf("0x%X\n",pSum);
    printf("0x%X\n",pDisplay);
    //不能將派生類的成員函數指針賦值給基類的函數指針
    //pPrint = &ChildA::print;//error
    //能夠將基類的成員函數指針賦值給派生類
    void (ChildA::*pChildPrintFunc)() = pPrint;
    (childA.*pChildPrintFunc)();//Parent::print
    void (*p)() = reinterpret_cast<void (*)()>(pPrint);
    p();

    return 0;
}

五、靜態成員函數指針

對於靜態成員函數,函數體內部沒有this指針,與類的其它成員函數共享類的命名空間,但靜態成員函數並非類的一部分,靜態成員函數與常規的全局函數同樣,成員函數指針的語法針對靜態成員函數並不成立。
靜態成員函數的函數指針定義語法以下:

ReturnType (* pointerName) (ArgumentLList);
ReturnType:成員函數返回類型
Argument_List: 成員函數參數列表
pointerName:指針名稱

靜態成員函數的函數指針的使用與全局函數相同,但靜態成員函數指針保存的仍舊是個相對地址。

#include <iostream>

using namespace std;

class Test
{
public:
    static void print()
    {
        cout << "Test::print" << endl;
    }
};

int main(int argc, char *argv[])
{
    void (* pFunc)() = &Test::print;
    cout << pFunc << endl;//1
    //直接調用
    pFunc();//Test::print
    (*pFunc)();//Test::print
    Test test;
    //(test.*pFunc)();//error
    Test* pTest = &test;
    //(pTest->*pFunc)();//error

    return 0;
}

六、普通成員函數指針

非靜態、非虛的普通成員函數指針不能直接調用,必須綁定一個類對象。
普通函數指針的值指向代碼區中的函數地址。若是強制轉換爲普通函數指針後調用,成員函數內部this指針訪問的成員變量將是垃圾值。

#include <iostream>
#include <string>

using namespace std;

class Parent
{
public:
    Parent(int i, int j)
    {
        m_i = i;
        m_j = j;
    }
    void print()
    {
        cout << "Parent::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
    }
    double sum()
    {
        cout << "Parent::" << __func__<< endl;
        double ret = m_i + m_j;
        cout <<ret << endl;
        return ret;
    }
    void display()
    {
        cout << "Parent::display()" << endl;
    }
    int add(int value)
    {
        return m_i + m_j + value;
    }
protected:
    int m_i;
    int m_j;
};

class ChildA : public Parent
{
public:
    ChildA(int i, int j, double d):Parent(i, j)
    {
        m_d = d;
    }
    void print()
    {
        cout << "ChildA::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
        cout << "m_d = "<< m_d << endl;
    }
    double sum()
    {
        cout << "ChildA::" << __func__<< endl;
        double ret = m_i + m_j + m_d;
        cout << ret << endl;
        return ret;
    }
private:
    void display()
    {
        cout << "ChildA::display()" << endl;
    }
private:
    double m_d;
};

int main(int argc, char *argv[])
{
    Parent parent(100,200);
    ChildA childA(1,2,3.14);
    Parent* pTestA = &childA;
    typedef void (Parent::*pPrintFunc)();
    pPrintFunc pPrint = &Parent::print;
    typedef double (Parent::*pSumFunc)();
    pSumFunc pSum = &Parent::sum;
    typedef void (Parent::*pDisplayFunc)();
    pDisplayFunc pDisplay = &Parent::display;

    printf("0x%X\n",pPrint);
    printf("0x%X\n",pSum);
    printf("0x%X\n",pDisplay);
    //綁定類對象進行調用
    (pTestA->*pPrint)();
    (pTestA->*pSum)();
    (pTestA->*pDisplay)();
    //強制轉換爲普通函數指針
    void (*p)() = reinterpret_cast<void (*)()>(pPrint);
    p();//打印隨機值

    return 0;
}

七、虛成員函數指針

C++經過虛函數提供了運行時多態特性,編譯器一般使用虛函數表實現虛函數。

#include <iostream>
#include <string>

using namespace std;

class Parent
{
public:
    Parent(int i, int j)
    {
        m_i = i;
        m_j = j;
    }
    virtual void print()
    {
        cout << "Parent::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
    }
    virtual double sum()
    {
        cout << "Parent::" << __func__<< endl;
        double ret = m_i + m_j;
        cout <<ret << endl;
        return ret;
    }
    virtual void display()
    {
        cout << "Parent::display()" << endl;
    }
    int add(int value)
    {
        return m_i + m_j + value;
    }
protected:
    int m_i;
    int m_j;
};

class ChildA : public Parent
{
public:
    ChildA(int i, int j, double d):Parent(i, j)
    {
        m_d = d;
    }
    virtual void print()
    {
        cout << "ChildA::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
        cout << "m_d = "<< m_d << endl;
    }
    virtual double sum()
    {
        cout << "ChildA::" << __func__<< endl;
        double ret = m_i + m_j + m_d;
        cout << ret << endl;
        return ret;
    }
private:
    void display()
    {
        cout << "ChildA::display()" << endl;
    }
private:
    double m_d;
};

class ChildB : public Parent
{
public:
    ChildB(int i, int j, double d):Parent(i, j)
    {
        m_d = d;
    }
    virtual void print()
    {
        cout << "ChildB::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
        cout << "m_d = "<< m_d << endl;
    }
    virtual double sum()
    {
        cout << "ChildB::" << __func__<< endl;
        double ret = m_i + m_j + m_d;
        cout << ret << endl;
        return ret;
    }
private:
    void display()
    {
        cout << "ChildB::display()" << endl;
    }
private:
    double m_d;
};

int main(int argc, char *argv[])
{
    Parent parent(100,200);
    ChildA childA(1,2,3.14);
    //childA.display();//error,編譯時private不可訪問
    ChildB childB(100,200,3.14);
    Parent* pTestA = &childA;
    Parent* pTestB = &childB;
    typedef void (Parent::*pVPrintFunc)();
    pVPrintFunc pPrint = &Parent::print;

    (parent.*pPrint)();//Parent::print
    (pTestA->*pPrint)();//ChildA::print,多態
    (pTestB->*pPrint)();//ChildB::print,多態

    typedef double (Parent::*pVSumFunc)();
    pVSumFunc pSum = &Parent::sum;

    (parent.*pSum)();//Parent::sum
    (pTestA->*pSum)();//ChildA::sum,多態
    (pTestB->*pSum)();//ChildB::sum,多態

    typedef void (Parent::*pVDisplayFunc)();
    pVDisplayFunc pDisplay = &Parent::display;

    (parent.*pDisplay)();//Parent::display
    (pTestA->*pDisplay)();//ChildA::display,多態
    (pTestB->*pDisplay)();//ChildB::display,多態

    printf("0x%X\n",pPrint);
    printf("0x%X\n",pSum);
    printf("0x%X\n",pDisplay);

    return 0;
}

虛成員函數指針的值是一個相對地址,表示虛函數在虛函數表中,離表頭的偏移量+1。
當一個對象調用虛函數時,首先經過獲取指向虛函數表指針的值獲得虛函數表的地址,而後將虛函數表的地址加上虛函數離表頭的偏移量即爲虛函數的地址。 

八、成員函數指針示例

成員函數指針的一個重要應用是根據輸入來生成響應事件,使用不一樣的處理函數來處理不一樣的輸入。

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

using namespace std;

//虛擬打印機
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!

3、C++類成員函數的調用分析

一、成員函數調用簡介

類中的成員函數存在於代碼段。調用成員函數時,類對象的地址做爲參數隱式傳遞給成員函數,成員函數經過對象地址隱式訪問成員變量,C++語法隱藏了對象地址的傳遞過程。因爲類成員函數內部有一個this指針,類成員函數的this指針會被調用的類對象地址賦值。所以,若是類成員函數中沒有使用this指針訪問成員,則類指針爲NULL時仍然能夠成功對該成員函數進行調用。static成員函數做爲一種特殊的成員函數,函數內部不存在this指針,所以類指針爲NULL時一樣能夠成功對靜態成員函數進行調用。

#include <iostream>
#include <string>

using namespace std;

namespace Core {

class Test
{
public:
    Test(int i)
    {
        this->i = i;
    }
    void print()
    {
        cout << "i = " << i << endl;
    }
    void sayHello()
    {
        cout << "Hello,Test." << endl;
    }
    static void printHello()
    {
        cout << "Hello,Test." << endl;
    }
private:
    int i;
};

}

int main()
{
    using namespace Core;
    Core::Test* ptest = NULL;
    ptest->sayHello();//Hello,Test.
    ptest->printHello();
    //定義函數指針類型
    typedef void (Test::*pFunc)();
    //獲取類的成員函數地址
    pFunc p = &Test::print;
    Test test(100);
    //調用成員函數
    (test.*p)();//i = 100

    return 0;
}

二、普通成員函數調用機制分析

普通成員函數經過函數地址直接調用。

#include <iostream>

using namespace std;

class Test
{
public:
    void print()
    {
        cout << "Test::print" << endl;
    }
};

int main()
{
    Test test;
    Test* p = &test;
    p->print();
}

對於非虛、非靜態成員函數的調用,如p->print(),C++編譯器會生成以下代碼:

Test* const this = p;
void Test::print(Test* const this)
{
    cout << "Test::print" << endl;
}

無論指針p是任何值,包括NULL,函數Test::print()均可以被調用,p被做爲this指針並看成參數傳遞給print函數。所以,當傳入print函數體內的p指針爲NULL時,只要不對p指針進行解引用,函數就能正常調用而不發生異常退出。

#include <iostream>

using namespace std;

class Test
{
public:
    void print()
    {
        cout << "Test::print" << endl;
        sayHello();
    }
    void sayHello()
    {
        cout << "Test::sayHello" << endl;
    }
};

int main()
{
    Test* p = NULL;
    p->print();
}

// output:
// Test::print
// Test::sayHello

三、靜態成員函數調用機制分析

靜態成員函數經過函數地址進行調用,其調用方式同全局函數。

四、虛成員函數調用機制分析

虛成員函數的調用涉及運行時多態。當一個對象調用虛函數時,首先經過運行時對象獲取指向虛函數表指針的值獲得虛函數表的地址,而後將虛函數表的地址加上虛函數離表頭的偏移量即爲虛函數的地址。 基類對象內部的虛函數表指針指向基類的虛函數表,派生類對象的虛函數表指針指向派生類的虛函數表,確保運行時對象調用正確的虛函數。

相關文章
相關標籤/搜索