1. 程式人生 > >C++語言學習(十四)——C++類成員函數調用分析

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

不可訪問 ring error: 兩種 cout list 空間 splay 示例代碼

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

一、C++成員函數

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

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

2、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在成員函數的開始執行前構造,在成員函數執行結束後清除。

二、C++成員函數指針

1、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關鍵字。

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指針所指向的對象。
成員函數指針不是常規指針(保存的是某個確切地址),成員函數指針保存的是成員函數在類布局中的相對地址。

2、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類型。

#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;
}

3、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對於成員函數指針統一使用下面的結構進行表示:

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簡單,在所有場合其實現方式都是一樣的。

4、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;
}

5、靜態成員函數指針

對於靜態成員函數,函數體內部沒有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;
}

6、普通成員函數指針

非靜態、非虛的普通成員函數指針不能直接調用,必須綁定一個類對象。
普通函數指針的值指向代碼區中的函數地址。如果強制轉換為普通函數指針後調用,成員函數內部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;
}

7、虛成員函數指針

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。
當一個對象調用虛函數時,首先通過獲取指向虛函數表指針的值得到虛函數表的地址,然後將虛函數表的地址加上虛函數離表頭的偏移量即為虛函數的地址。?

8、成員函數指針示例

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

#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!

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

1、成員函數調用簡介

類中的成員函數存在於代碼段。調用成員函數時,類對象的地址作為參數隱式傳遞給成員函數,成員函數通過對象地址隱式訪問成員變量,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;
}

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

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

#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

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

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

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

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

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