多态与虚表详解

引言:详细讲解了虚表的实现过程,有助于我们深入理解虚表,而不至于忘记。本文难免有所错误,如有问题欢迎留言

目录:
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的虚表地址
这里写图片描述


总结下来,类的基本内存分布为以下的形式
这里写图片描述

关于类调用函数的过程:
调用虚函数:在虚表中查找虚函数地址,然后调用该虚函数
调用成员函数:先查找子类自己的成员函数,然后是父类的成员函数

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值