C++——多態總結

在博客多態&虛函數中主要對多態的一些基本概念和虛函數做了介紹,下面,我們來探究一下【虛表】。

含有虛函數的類

  • 先來看看含有虛函數的類的大小吧!

class B
{
public:
    virtual void Show()
    {
        cout << _b << endl;
    }
public:
    int _b;
};

一眼看過去,這個類中只有一個int類型的變量_b,那麼他的大小是不是隻有4個字節呢?我運行了之後發現並不是,它的大小是8個字節。運行結果我這就不看了,下面我們用內存和監視來分析一下對象b的對象模型。

這裏寫圖片描述

可以看到,當執行完b._b = 1; 時,對象b的成員_b與對象b的地址偏移了4個字節。而這4個字節裏存放的是一個地址,我們把這個地址叫做虛表指針,它指向一個存放虛函數地址的內存塊。這個內存塊就是虛表。

由此,我們可得到b的對象模型

這裏寫圖片描述

有虛函數的類相比普通類多了4個字節,用來存放虛表指針。在對象模型中,類中的成員變量存放在虛表指針之後。

  • 當類中有多個對象時,這些對象共用同一虛表。(可同時創建兩個對象,在內存裏查看其虛表指針是否相同來驗證)

  • 當類中有多個成員變量時,對象模型中各變量的存放順序按其在類中的聲明順序存放。

  • 當類中有多個虛函數時,虛表中各虛函數存放順序按照其在類中的聲明順序存放。

直接來看例子吧:

class B
{
public:
    virtual void Fun3()
    {}
    virtual void Fun1()
    {}
    virtual void Fun2()
    {}
public:
    int _b3;
    int _b1;
    int _b2;
};

下圖是我根據監視內存畫出來的b的存儲結構

這裏寫圖片描述

再來看看它的對象模型是怎樣的?

這裏寫圖片描述

  • 需要注意一點
  • 類的構造函數在這裏起填充虛表指針的作用

下面我們來看一下繼承(參見博客【繼承】)體系中虛函數的結構如何?

單繼承中的虛函數

  • 虛函數無覆蓋
class B
{
public:
    B()
        :_b(1)
    {}
    virtual void Fun1()
    {
        cout << "B::Fun1()";
    }
    virtual void Fun2()
    {
        cout << "B::Fun2()";
    }
    virtual void Fun3()
    {
        cout << "B::Fun3()";
    }
public:
    int _b;
};
class D :public B
{
public:
    D()
        :_d(2)
    {}
    virtual void Fun4()
    {
        cout << "D::Fun4()";
    }
    virtual void Fun5()
    {
        cout << "D::Fun5()";
    }
    virtual void Fun6()
    {
        cout << "D::Fun6()";
    }
public:
    int _d;
};

typedef void(*FUN_TEST)();
void FunTest()
{
    B b;
    cout<<sizeof(b)<<endl;
    cout << "B vfptr:" << endl;
    for (int iIdx = 0; iIdx < 3; ++iIdx)
    {
        FUN_TEST funTest = (FUN_TEST)(*((int*)*(int *)&b + iIdx)); funTest();
        cout << ": " << (int *)funTest << endl;
    }
    cout << endl;
    D d;
    cout << sizeof(d) << endl;
    cout << "D vfptr:" << endl;
    for (int iIdx = 0; iIdx < 6; ++iIdx)
    {
        FUN_TEST funTest = (FUN_TEST)(*((int*)*(int *)&d + iIdx));
        funTest();
        cout << ": " << (int *)funTest << endl;
    }
}

其中FunTest()函數用於打印各對象中的函數。

先來看看結果吧

這裏寫圖片描述

d中有兩個成員變量以及一個虛表指針,所以大小爲12.

這裏寫圖片描述

可以發現,在派生類中,也完全繼承了基類的虛函數,且遵循繼承的存儲結構。
派生類中繼承的基類的虛函數地址與基類中的相同。

  • 虛函數有覆蓋

同樣拿上面例子來說,我把派生類中的函數名Fun5改爲Fun3,把Fun6改爲Fun2,並且派生類中的循環次數改爲4,其餘保持不變。再次運行代碼得到如下結果:

這裏寫圖片描述

基類無變化,但是在派生類中,只打印了派生類的函數Fun2和Fun3.但是我的定義順序明明是Fun3在Fun2前面的呀。怎麼打印結果卻是反的?這是怎麼一回事呢?

這裏寫圖片描述

當對象d被創建的時候,其虛表指針已經形成,但此時,虛表中存放的是從基類繼承來的虛函數,當系統檢測到派生類中已經對基類的虛函數進行重寫的函數時,就拿該函數去替換虛表中基類對應的虛函數。所以雖然,Fun3定義在Fun2之前,但是替換時,系統從基類的Fun1開始,依次向下檢測,當檢測到Fun2被重寫時,直接拿派生類中的Fun2去替換當前位置上的Fun2。如下圖:

這裏寫圖片描述

這裏寫圖片描述

由此,可得出單繼承的對象模型:

這裏寫圖片描述

虛表的形成:

這裏寫圖片描述

菱形繼承

前面在繼承中,爲了解決二義性問題,我們引入了虛擬繼承。在虛擬繼承中,派生類中前4字節是偏移量的地址。但引入虛函數之後,我們看到虛表指針也存放於派生類的前4字節。那麼,我們來看看,在菱形繼承中,派生類的對象模型如何?

  • 菱形繼承
class B
{
public:
    virtual void FunTest1()
    {
        cout << "B::FunTest1()" << endl;
    }

    int _b;
};

class C1 :public B
{
public:
    void FunTest1()
    {
        cout << "C1::FunTest1()" << endl;
    }

    virtual void FunTest2()
    {
        cout << "C1::FunTest2()" << endl;
    }

    int _c1;
};

class C2 :public B
{
public:
    virtual void FunTest1()
    {
        cout << "C2::FunTest1()" << endl;
    }

    virtual void FunTest3()
    {
        cout << "C2::FunTest3()" << endl;
    }

    int _c2;
};

class D :public C1, public C2
{
public:
    virtual void FunTest1()
    {
        cout << "D::FunTest1()" << endl;
    }

    virtual void FunTest2()
    {
        cout << "D::FunTest2()" << endl;
    }

    virtual void FunTest3()
    {
        cout << "D::FunTest3()" << endl;
    }

    virtual void FunTest4()
    {
        cout << "D::FunTest4()" << endl;
    }

    int _d;
};

typedef void(*Fun)();

void Printvpf()
{
    D d;
    cout << sizeof(d) << endl;
    d.C1::_b = 1;
    d.C2::_b = 2;
    d._c1 = 3;
    d._c2 = 4;
    d._d = 5;
    C1& c1 = d;
    int* vpfAddr = (int*)*(int*)&c1;
    Fun* pfun = (Fun*)vpfAddr;
    while (*pfun)
    {
        (*pfun)();
        pfun = (Fun*)++vpfAddr;
    }

    cout << endl;

    C2& c2 = d;
    vpfAddr = (int*)*(int*)&c2;
    pfun = (Fun*)vpfAddr;
    while (*pfun)
    {
        (*pfun)();
        pfun = (Fun*)++vpfAddr;
    }
}

來看看結果吧!

這裏寫圖片描述

結果可見,系統將派生類自己的虛函數放在了其第一個基類C1後面,並且派生類中重寫的虛函數覆蓋了基類的虛函數。

其對象模型如下:

這裏寫圖片描述

  • 菱形虛擬繼承
class B
{
public:
    virtual void FunTest1()
    {
        cout << "B::FunTest1()" << endl;
    }

    int _b;
};

class C1 :virtual public B
{
public:
    void FunTest1()
    {
        cout << "C1::FunTest1()" << endl;
    }

    virtual void FunTest2()
    {
        cout << "C1::FunTest2()" << endl;
    }

    int _c1;
};

class C2 :virtual public B
{
public:
    virtual void FunTest1()
    {
        cout << "C2::FunTest1()" << endl;
    }

    virtual void FunTest3()
    {
        cout << "C2::FunTest3()" << endl;
    }

    int _c2;
};

class D :public C1, public C2
{
public:
    virtual void FunTest1()
    {
        cout << "D::FunTest1()" << endl;
    }

    virtual void FunTest2()
    {
        cout << "D::FunTest2()" << endl;
    }

    virtual void FunTest3()
    {
        cout << "D::FunTest3()" << endl;
    }

    virtual void FunTest4()
    {
        cout << "D::FunTest4()" << endl;
    }

    int _d;
};

typedef void(*Fun)();

void Printvpf()
{
    D d;
    cout << sizeof(d) << endl;
    d._b = 1;
    d._c1 = 2;
    d._c2 = 3;
    d._d = 4;
    C1& c1 = d;
    int* vpfAddr = (int*)*(int*)&c1;
    Fun* pfun = (Fun*)vpfAddr;
    while (*pfun)
    {
        (*pfun)();
        pfun = (Fun*)++vpfAddr;
    }

    cout << endl;

    C2& c2 = d;
    vpfAddr = (int*)*(int*)&c2;
    pfun = (Fun*)vpfAddr;
    while (*pfun)
    {
        (*pfun)();
        pfun = (Fun*)++vpfAddr;
    }
    B& b = d;
    vpfAddr = (int*)*(int*)&b;
    pfun = (Fun*)vpfAddr;
    while (*pfun)
    {
        (*pfun)();
        pfun = (Fun*)++vpfAddr;
    }
}
int main()
{
    Printvpf();
    return 0;
}

結果如圖:

這裏寫圖片描述

表示偏移量的地址緊隨虛表指針之後,其次纔是成員變量。各類成員存放遵循繼承規則。
上圖中表示偏移量的第一個數可能有人無法理解爲什麼幾乎是一串f,其實它是負數在內存中的存儲形式。

由此可得對象模型爲:

這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章