多态实现原理剖析

Posted cucucudeblog

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多态实现原理剖析相关的知识,希望对你有一定的参考价值。

1. 虚函数表

C++的多态是通过一张虚函数表(virtual Table)来实现的,简称为V-Table,(这个表是隐式的,不需要关心其生成与释放)在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承,覆写的问题,保证其真实反应实际的函数,这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得尤为重要了,它就像一个地图一样,指明了实际所应该调用的函数

为了更加形象的对其进行理解,我们用一个实例来对其进行说明

#include <iostream>

using namespace std;

class Base
{
public:
    virtual void f(){cout <<"Base::f"<<endl;}
    virtual void g(){cout <<"Base::g"<<endl;}
    virtual void h(){cout <<"Base::h"<<endl;}
    int a;
};

int main()
{
    cout << "Base Size:" <<sizeof(Base)<< endl;
    return 0;
}

最终无论有几个虚函数,最终的大小都是8

为了对这个现象进行解释,我们用下图进行解释

技术分享图片

 所以说相当于int占了四个字节,然后另外四个字节放了一个地址,这个地址指向一张表,表中的三个就代表三个地址

为了对这个表进行说明,我们写了如下的代码进行说明

#include <iostream>

using namespace std;

class Base
{
public:
    virtual void f(){cout <<"Base::f"<<endl;}
    virtual void g(){cout <<"Base::g"<<endl;}
    virtual void h(){cout <<"Base::h"<<endl;}
    int a;
};

typedef void (*FUNC)();
int main()
{
    cout << "Base Size:" <<sizeof(Base)<< endl;
    Base b;
    cout<<"对象的起始地址"<<&b<<endl;
    cout<<"虚函数表的地址"<<(int **)*(int *)&b<<endl;
    cout<<"虚函数表中第一个函数的地址"<<*((int **)*(int *)&b)<<endl;
    cout<<"虚函数表中第二个函数的地址"<<*((int **)*(int *)&b+1)<<endl;
    FUNC pf=(FUNC)(*((int **)*(int *)&b));
    pf();
    pf=(FUNC)(*((int **)*(int *)&b+1));
    pf();
    pf=(FUNC)(*((int **)*(int *)&b+2));
    pf();
    return 0;
}

所得到的结果为

技术分享图片

对于多态发生的原理,我们可以用以下两段代码来进行比较

#include <iostream>

using namespace std;

class Base
{
public:
    virtual void f(){cout <<"Base::f"<<endl;}
    virtual void g(){cout <<"Base::g"<<endl;}
    virtual void h(){cout <<"Base::h"<<endl;}
};

class Derive:public Base
{
    virtual void f1(){cout <<"Derive::f1"<<endl;}
    virtual void g1(){cout <<"Derive::g1"<<endl;}
    virtual void h1(){cout <<"Derive::h1"<<endl;}
};
typedef void (*FUNC)();
int main()
{
    cout << "Base Size:" <<sizeof(Base)<< endl;
    Derive b;
    cout<<"对象的起始地址"<<&b<<endl;
    cout<<"虚函数表的地址"<<(int **)*(int *)&b<<endl;
    cout<<"虚函数表中第一个函数的地址"<<*((int **)*(int *)&b)<<endl;
    cout<<"虚函数表中第二个函数的地址"<<*((int **)*(int *)&b+1)<<endl;
    FUNC pf=(FUNC)(*((int **)*(int *)&b));
    pf();
    pf=(FUNC)(*((int **)*(int *)&b+1));
    pf();
    pf=(FUNC)(*((int **)*(int *)&b+2));
    pf();
    pf=(FUNC)(*((int **)*(int *)&b+3));
    pf();
    pf=(FUNC)(*((int **)*(int *)&b+4));
    pf();
    pf=(FUNC)(*((int **)*(int *)&b+5));
    pf();
    return 0;
}

得到的结果是

技术分享图片

但是若将继承的函数改为覆写,即

class Base
{
public:
    virtual void f(){cout <<"Base::f"<<endl;}
    virtual void g(){cout <<"Base::g"<<endl;}
    virtual void h(){cout <<"Base::h"<<endl;}
};

class Derive:public Base
{
    virtual void f(){cout <<"Derive::f1"<<endl;}
    virtual void g1(){cout <<"Derive::g1"<<endl;}
    virtual void h1(){cout <<"Derive::h1"<<endl;}
};

得到的结果为

技术分享图片

 

我们从表中可以看到下面几点

(1)覆写的f()函数被放到了虚表中原来父类虚函数的位置

(2)没有被覆盖的函数依旧

只要是虚函数,那么就先来遍历那张函数表,如果不是虚函数,则本来是什么就是什么

静态代码发生了什么

 当编译器看到这段代码时,并不知道b的真实身份,编译器能做的就是用一段代码代替这段语句

Base *b=new Derive();
b->f();

调用过程实际上是遍历虚函数表的过程,在编译的时候,b不知道自己的真实身份(因为运行的时候其不知道谁赋给她),但是它调用的时候有一个b->f(),类似于一个指向类成员的指针,其记录了一个偏移

技术分享图片

在这里,f偏移了0个单位,g()偏移了1个单位,h()偏移了2个单位,g1()偏移了3个单位

刚开始并不知道指向哪里,只是说会给他一张表,里面放了地址,需要时直接去表里找

(1)明确b类型

(2)然后通过指针虚函数表的指针vptr和偏移量,匹配虚函数的入口

(3)根据入口地址调用虚函数

对多态的评价

(1)实现了动态绑定

(2)些许的牺牲了一些空间和效率,也是值得的

为什么构造函数不能为虚函数

编译不过

注意,当基类的构造函数内部有虚函数时,会出现什么情况呢?结果是在构造函数中,虚函数机制不起作用了,调用虚函数如同调用一般的成员函数一样,当基类的析构函数内部有虚函数时,又如何工作呢?与构造函数相同,只有局部版本的被调用。但是,行为相同,原因是不一样的,构造函数只能调用局部版本,是因为调用时还没有派生类版本的信息,析构函数则是因为派生类版本的信息已经不可靠了。我们知道,析构函数的调用顺序与构造函数相反,是从派生类的析构函数到基类的析构函数,当某个类的析构函数被调用时,其派生类的析构函数已经被调用了,相应的数据也已被丢失,如果再调用虚函数的派生类的版本,就相当于对一些不可靠的数据进行操作,这是非常危险的,因此,在析构函数中,虚函数机制也是不起作用的

#include <iostream>

using namespace std;

class A
{
public:
    A()
    {
        p=this;
        p->func();
    }
    virtual void func()
    {
        cout<<"xxxxxxxxxxx"<<endl;
    }
private:
    A *p;
};

class B:public A 
{
public:
    void func()
    {
        cout<<"oooooooooooo"<<endl;
    }
};
int main()
{
    B b;
    return 0;
}

在这里最终的结果为xxxxxxxxxxxx,因为在生成的过程中,首先是A的构造函数进行创建,这就造成了其调用时生成的是xxxxxxxxxx

为了说明析构函数析构的顺序,我们举以下的例子

#include <iostream>

using namespace std;

class A
{
public:
    A()
    {
        p=this;
        p->func();
    }
    virtual void func()
    {
        cout<<"xxxxxxxxxxx"<<endl;
    }
    ~A()
    {
        p=this;
        p->func();
    }
private:
    A *p;
};

class B:public A
{
public:
    void func()
    {
        cout<<"oooooooooooo"<<endl;
    }
};
int main()
{
    B b;
    return 0;
}

最终也是生成xxxxxxxxxx的,因为在析构的过程中,会将首先调用B的析构函数,然后再调用A的析构函数,再会进行p->func的调用

 

以上是关于多态实现原理剖析的主要内容,如果未能解决你的问题,请参考以下文章

java多态理解和底层实现原理剖析

多态实现原理剖析

C++多态底层剖析

C++多态底层剖析

C++多态底层剖析

spring源码剖析AOP实现原理剖析