C++基础 | 静态多态与动态多态

多态:顾名思义,多态就是多种形态,也就是对不同对象发送同一个消息,不同对象会做出不同的响应。

并且多态分为静态多态和动态多态。

静态多态就是在系统编译期间就可以确定程序执行到这里将要执行哪个函数,例如:函数的重载,对象名加点操作符执行成员函数等,都是静态多态,其中,重载是在形成符号表的时候,对函数名做了区分,从而确定了程序执行到这里将要执行哪个函数,对象名加点操作符执行成员函数是通过this指针来调用的。

函数的重载比较简单,不再赘述,这里我们通过一个简单的例子来看一下对象名加点操作符执行成员函数的静态多态:

class A
{
public:
    void Set(int a)
    {
        _a = a;
    }
public:
    int _a;
};

int main()
{
    A a1;
    a1.Set(15);
    return 0;
}

这里定义了一个A类,有一个成员函数和一个成员,我们将程序的部分汇编代码截取出来如下图: 
这里写图片描述 
我们可以看到这里直接是一个lea指令将a1对象的地址放入寄存器eax中,也就是对象的this指针,然后用call指令就可以跳转到Set函数,也就是说其汇编代码在此时就知道应该要去到哪个地方之行哪个函数,这就是静态多态,也叫编译时多态

动态多态则是利用虚函数实现了运行时的多态,也就是说在系统编译的时候并不知道程序将要调用哪一个函数,只有在运行到这里的时候才能确定接下来会跳转到哪一个函数的栈帧。

在说动态多态之前我们先来看一下什么是虚函数,虚函数就是在基类中声明该函数是虚拟的(在函数之前加virtual关键字),然后在子类中正式的定义(子类中的该函数的函数名,返回值,函数参数个数,参数类型,全都与基类的所声明的虚函数相同,此时才能称为重写,才符合虚函数,否则就是函数的重载),再定义一个指向基类对象的指针,然后使该指针指向由该基类派生的子类对象,再然后用这个指针来调用改虚函数,就能实现动态多态。

下面我们通过一个例子来看一下利用虚函数实现的动态多态:

class A
{
public:
    A(int a = 10)
        :_a(a)
    {}
     virtual void Get()
    {
         cout << "A:: _a=" << _a << endl;
    }
public:
    int _a;
};

class B :  public A
{
public:
    B(int b = 20)
        :_b(b)
    {}
    void Get()
    {
        cout << "B:: _b=" << _b << endl;

    }

    int _b;
};

int main()
{
    A a1;
    B b1;
    A* ptr1 = &a1;
    ptr1->Get();

    ptr1 = &b1;
    ptr1->Get();

    return 0;
}

在这里我们看到,基类A的Get函数声明为虚函数,在B类中进行了重写, 
然后在main函数中分别用基类的ptr1和指向子类的ptr2进行调用虚函数Get,我们得到了如下图的输出: 
这里写图片描述 
这说明确实是实现了不同调用,而且是在运行时,那么虚函数的底层到底是怎么实现的呢,我们来看一下汇编代码及其对象模型: 
这里写图片描述 
通过上图的汇编代码,我们看到这里做了一系列的指针解引用处理,最后确定了eax中应该存放的this指针的值,要搞清楚这个必须要搞清楚子类的对象模型。 
这里写图片描述 
用监视窗口查看b1可以看到如上图所示,这里的_vfptr是一个虚表指针,它指向一个存放该类对象的所有虚函数的地址的表,我们可以将该表理解为一个函数指针数组,在该数组的最后一个元素,编译系统会将其置为0,。 
对象模型如下图示: 
这里写图片描述 
其中红色为A类的成员,黑色为B类对象b1的成员,紫色就是一个虚函数表,存放着存放该类对象的所有虚函数的地址,汇编代码做了一系列的指针解引用处理就是为了从虚函数表中找到相应的虚函数进行调用,从而实现了动态多态。

菱形继承:

菱形继承问题和虚继承

0x01 菱形继承

   假设有类B和类C,它们都继承了相同的类A。另外还有类D,类D通过多重继承机制继承了类B和类C。

   如果直接继承会引发访问不明确(二义性),以及数据冗余。如果直接指定访问对象,可解决二义性,而要解决数据冗余,则要引入虚函数。

   因为图表的形状类似于菱形(或者钻石),因此这个问题被形象地称为菱形问题(钻石继承问题)。

   

   示例代码:

 

#include <Windows.h>
#include <iostream>
using namespace std;


class Life
{
public:
    Life() :LifeMeaning(5)
    {  }
public:
    int LifeMeaning;
};

class Bird :public Life
{
public:
    Bird() :BirdMeaning(0x50)
    {  }
public:
    int BirdMeaning;
};

class Human :public Life
{
public:
    Human() :HumanMeaning(0x100)
    {  }
public:
    int HumanMeaning;
};


class Angel :public Bird, public Human
{
public:
    Angel() :AngelMeaning(0x30)
    {  }
public:
    int AngelMeaning;
};

int main()
{
    Angel Angel;
    return 0;
}

  

   内存窗口观察Angel对象的基地址,可以看到有两个05(Life中的成员变量LifeMeaning的值05),这是因为子类对象会包父类的成员变量。对于Bird和Human来说,都会去包含Life类中LifeMeaning的值05。对于天使Angel来说,会同时包含Bird和Human的所有成员。故而LifeMeaning的这个变量在子类Angel中出现了两次,这是菱形继承问题。

  

  对于二义性,可以通过作用域符指定访问对象来消除(Angel.Bird::LifeMeaning),而数据冗余的问题,则要通过虚继承。

 

0x02  虚继承

     实例代码:

     

#include <Windows.h>
#include <iostream>
using namespace std;

class Life
{
public:
    Life() :LifeMeaning(0x5)
    {  }
public:
    int LifeMeaning;
};

class LifePropagate1 :virtual public Life
{
public:
    LifePropagate1() :LifePropagate1Meaning(0x50)
    {  }
public:
    int LifePropagate1Meaning;
};

class LifePropagate2 :virtual public Life
{
public:
    LifePropagate2() :m_B(0x60)
    {  }
public:
    int m_B;
};
class NewLife :public LifePropagate1, public LifePropagate2
{
public:
    NewLife() :NewLifeMeaning(0x100)
    {  }
public:
    int NewLifeMeaning;
};



int main()
{
    NewLife NewLifeObject;
    return 0;
}

 

 

  内存窗口观察NewLifeObject对象的基地址:

  LifePropagate1与LifePropagate2 共用一个虚基类。最终虚基类的数据只有一份,数据冗余和二义性问题不再。

     

1

<span style="color: #ff0000"> <br><br></span>

  那么,虚继承又是怎么解决这些烦人的问题的呢?

   可以看到在B和C中不再保存Life中的内容,保存了一份偏移地址,然后将最原始父类的数据保存在一个公共位置处这样保证了数据冗余性的降低同时,也消除了二义性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值