引言:詳細講解了虛表的實現過程,有助於我們深入理解虛表,而不至於忘記。本文難免有所錯誤,如有問題歡迎留言
目錄:
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的虛表地址
總結下來,類的基本內存分佈爲以下的形式
關於類調用函數的過程:
調用虛函數:在虛表中查找虛函數地址,然後調用該虛函數
調用成員函數:先查找子類自己的成員函數,然後是父類的成員函數