多態與虛表詳解

引言:詳細講解了虛表的實現過程,有助於我們深入理解虛表,而不至於忘記。本文難免有所錯誤,如有問題歡迎留言

目錄:
1、多態
2、虛表的內存分佈

多態

示例一(多態):

#include <iostream>
using namespace std;

class AAA
{
public:
    virtual void OutPut() = 0;
};
class BBB:public AAA
{
public:
    void OutPut() { cout << "BBB" << endl; }
};
class CCC:public AAA
{
public:
    void OutPut() { cout << "CCC" << endl; }
};
class DDD:public AAA
{
public:
    void OutPut() { cout << "DDD" << endl; }
};

int main()
{
    AAA* p[3];
    p[0] = new BBB;
    p[1] = new CCC;
    p[2] = new DDD;
    for (int i = 0; i < 3; i++) {
        p[i]->OutPut();
        delete p[i];
        p[i] = NULL;
    }
    return 0;
}

輸出結果:
BB
CC
DD

通過示例一,我們可以大概的瞭解多態的含義,就是實現了同一個接口,呈現出多種狀態。就好似,充電寶上的USB接口,在不同的充電寶中(BBB、CCC、DDD),可能有的是1A的,有的是兩A的,有的是4A的,這就是生活中USB接口的多種不同實現。面向對象思想就是提取生活中的實例來抽象這個世界。
多態的含義:接口的多種不同實現方式即爲多態。

多態的實現原理?
這裏就需要了解虛表(虛函數列表)的實現
虛表的含義:每個有虛函數的類或者繼承具有虛函數的基類,編譯器都會爲它生成一個虛擬函數表。虛表中的每一個元素都分別指向一個虛函數的地址。
虛表的形成:虛表在編譯階段構造出來的表

虛表的內存分佈

示例二(虛表):

#include <iostream>
using namespace std;

#define interface struct

interface IX
{
    virtual void FX1() = 0;
    virtual void FX2() = 0;
};

interface IY
{
    virtual void FY1() = 0;
    virtual void FY2() = 0;
};

class CCom :public IX, public IY
{
public:
    virtual void FX1() override
    {
        cout << "FX1" << endl;
    }

    virtual void FX2() override
    {
        cout << "FX2" << endl;
    }

    virtual void FY1() override
    {
        cout << "FY1" << endl;
    }

    virtual void FY2() override
    {
        cout << "FY2" << endl;
    }

    int m_Num;

    void OutPutThis()
    {
        cout << "this 指針地址:" << hex << this << endl;
    }

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

    virtual void GetNum(int num)
    { 
        m_Num = num; 
    }
    virtual void OutPut() 
    { 
        printf("%d\n", m_Num);
    }
};

int main()
{
    CCom* pCom = new CCom;
    CCom* pCom2 = new CCom;
    IX* pIX = (IX*)pCom;
    IY* pIY = (IY*)pCom;

    pCom->OutPutThis();
    cout << "實例指針 pCom->" << hex << pCom << endl;
    cout << "虛表指針 pIX->" << hex << pIX << endl;
    cout << "虛表指針 pIY->" << hex << pIY << endl;
    pCom->GetNum(10);
    cout << "類成員變量 m_Num 地址:"<<hex<<&pCom->m_Num << endl;

    cout << endl;
    printf("CCom 類佔據的內存大小:%d\n", sizeof(CCom));
    cout << "虛表 IX 地址->" << hex << *(int*)pIX << endl;
    cout << "虛表 IY 地址->" << hex << *(int*)pIY << endl;
    cout << "成員變量 m_Num ->" << *((int*)pIY + 1) << endl;

    delete pCom;
    pCom = NULL;
    return 0;
}

運行結果:
this 指針地址:003BE380
實例指針 pCom->003BE380
虛表指針 pIX->003BE380
虛表指針 pIY->003BE384
類成員變量 m_Num 地址:003BE388

CCom 類佔據的內存大小:12
虛表 IX 地址->e0ab58
虛表 IY 地址->e0ab70
成員變量 m_Num ->a

分析程序的運行結果:
CCom類的大小爲12,首地址爲0x003BE380(也是pIX的地址),接下來是pIY,接下來是成員變量m_Num地址。我們可以得出類實例的內存分佈:
0x003BE380 -> this,pIX (虛表指針與實例指針)
0x003BE384 -> pIY (pIY虛表指針地址)
0x003BE388 -> m_Num (成員變量)


類的繼承關係:
這裏寫圖片描述


實例地址的內存分佈(兩個虛表(__vfptr)指針加一個成員變量):
內存
this、pIX的值爲0x00e0ab58,pIY的值爲0x00e0ab70,m_Num的值爲0x0000000a
不難發現:內存中是根據繼承書寫的先後順序排布的(依次IX、IY)。


虛表(__vfptr)結構
虛表
pIX與pIY分別指向了兩個虛表的首地址。虛表中分別存放着我們的虛函數FX1、FX2與FY1、FY2的函數地址,通過該地址訪問函數。

跟進虛表(_vfptr)IX的地址 0x00E0AB58
虛表的內存分佈
虛表一有四個虛函數地址,對應着FX1,FX2,GetNum,OutPut函數的地址
爲什麼GetNum與OutPut在虛表一中?
1、子類的虛函數在父類虛函數的末尾,如果有多個虛函數列表,子類的虛函數在第一個虛表的後面
2、虛函數是根據聲明的順序放入虛表中的


調用基類純虛函數的反彙編示例,瞭解虛函數的調用過程
這裏寫圖片描述
mov eax,dword ptr [pIX]//取出pIX指針的值 0051E380 (虛表指針的值)存入 EAX 寄存器中
mov edx,dword ptr [eax]//取出 EAX 寄存器處的DWORD值 013EAB58 (虛表首地址)存入EDX寄存器
mov esi,esp//將棧頂指針存放到ESI寄存器,方便後面進行堆棧溢出檢查
mov ecx,dword ptr [pIX]//取出pIX指針的值 0051E380 存入 ECX 寄存器中
mov eax,dword ptr [edx]//取出 EDX 寄存器處的DWORD值 013E11D1 (虛表中FX1的地址)存入EAX寄存器
call eax//調用函數地址爲EAX中的DWORD值(013E11D1)處的函數,也就是FX1函數的首地址

//以下兩句爲檢查棧頂指針以及進行RTC錯誤動態檢查(VS編譯器中的優化)
cmp esi,esp
call __RTC_CheckEsp (013E111DBh)


對比繼承的純虛函數、子類自己的虛函數和普通成員函數的調用過程
這裏寫圖片描述
1)不難看出,調用基類的純虛函數(或虛函數)與調用子類自己的彙編實現是在編譯時就實現的,在實現過程中均是先取出虛表指針值,然後找到虛表指針所指向虛表的首地址,再在首地址中找到所需要調用的函數的首地址,進行call指令調用
2)調用普通的成員函數,直接將數據進行壓棧,然後調用成員函數地址即可(成員函數的地址在編譯階段就固定)
3)對照反彙編代碼,我們可以清楚的看到虛表的調用過程遠比調用普通的成員函數複雜,這也是虛表的一個小缺點。
注:虛表在編譯時就已經形成,同一個類的不同實例使用的是同一張虛表,我們看下我聲明兩個實例pCom與pCom2的虛表地址
這裏寫圖片描述


總結下來,類的基本內存分佈爲以下的形式
這裏寫圖片描述

關於類調用函數的過程:
調用虛函數:在虛表中查找虛函數地址,然後調用該虛函數
調用成員函數:先查找子類自己的成員函數,然後是父類的成員函數

發佈了54 篇原創文章 · 獲贊 38 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章