多态是什么?怎么实现的?

  多态是什么?怎么实现的?

  C++的多态是通过虚函数(virtual function)和指向基类的指针或引用来实现的。在基类中声明虚函数,派生类中重写该函数,通过基类指针或引用调用该函数,就可以实现运行时多态。
  多态的实现原理主要涉及到两个概念:虚函数表(vtable)和虚函数指针(vptr)。每个含有虚函数的类,以及从这样的类派生的类,都有一个虚函数表。这个表中存储了虚函数的地址。类的对象中包含一个虚函数指针,指向这个虚函数表。当我们通过基类的指针或引用调用虚函数时,实际上是通过这个虚函数指针找到虚函数表,然后在表中查找并调用相应的函数。这个过程是在运行时完成的,所以可以实现运行时多态。
  多态性的实现主要依靠两个机制:继承和虚函数。

  1. 继承:派生类可以继承基类的属性和方法。通过继承,派生类可以具有基类的行为和特征。
  2. 虚函数:在基类中声明一个虚函数,派生类可以对该虚函数进行重写。通过使用虚函数,可以在运行时根据实际对象的类型来调用相应的函数,而不是根据指针或引用的类型。
  多态的步骤如下:
  1. 定义基类:定义一个基类,并在其中声明一个或多个虚函数。
  2. 派生类:从基类派生出一个或多个派生类,并在派生类中重写基类的虚函数。
  3. 使用基类指针或引用:使用基类类型的指针或引用来引用派生类对象。这样做可以根据实际对象的类型来调用相应的函数。
  通过多态性,我们可以根据实际对象的类型来调用相应的函数,而不需要显式地判断对象的类型。这样可以提高代码的灵活性和可维护性。

  虚函数表指针分析,及多重继承虚函数表的分布

  C++如果想满足动态绑定,及基类指针或引用调用派生类函数,需要满足三个条件:

  1.基类存在虚函数;

  2.基类指针或 引用指向派生类对象;

  3.派生类需要重写基类的虚函数。

#include <iostream>

using namespace std;
class A {
public:
    virtual void func()
    {
        cout << "A:func" << endl;
    }
    int a;
};
class B:public A 
{
public:
    int b;
};
int main()
{
    A* p = new B;
    p->func();//此时会调用A::func函数
}

 

  此时A* p指针会指向B类对象内存布局中的A类的基类子对象,从而找到vptr(虚函数表指针),接着找到B类中的虚函数表,由于B类中并未重写A类虚函数,所以使用A *p指针调用func函数会调用到A::func。

  

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

  如果B类重写了基类A中的虚函数,那么B类的虚函数表中对应的func函数的地址就会被改为&B::func,所以此时用A* p指针调用func函数,就会调用到B::func,调用流程大概是:

  p指针先指向了A类的基类子对象->接着找到了vptr指针->接着找到B类中的虚函数表->调用func函数。

  需要注意的有以下几点:

  1.如果是单继承,派生类中仅含有一个vptr(虚函数表指针),该指针继承于基类;

  2.每个类仅有一个虚函数表(如果是多继承的话为多个),派生类的虚函数表中的数据(也就是函数地址)复制于基类的虚函数表,如果派生类中重写了基类虚函数,那么该派生类的虚函数表中对应的基类虚函数地址会更改为派生类重写后的函数地址,也就是派生类的函数地址。

  3.基类和派生类的虚函数表不是同一个,每个类有属于自己的虚函数表,派生类虚函数表只是复制于基类。

  下面讨论多继承的情况下,虚函数表是怎样生成的。

  

#include <iostream>

using namespace std;
class A {
public:
    virtual void funcA()
    {
        cout << "A:funcA" << endl;
    }
    int a;
};
class B :public A
{
public:
    virtual void funcB()
    {
        cout << "B:funcB" << endl;
    }
    int b;
};
class C :public A, public B
{
public:
    int c;
};
int main()
{
    C c;
    A* a_p = &c;
    B* b_p = &c;
    a_p->funcA);
    b_p->funcB();
}

   

  此时C类有两个基类A和B,所以C中有两个vptr,这两个vptr分别指向两个虚函数表,这两个虚函数表都属于C类,其中的数据分别复制于A和B的虚函数表,如果此时使用A* a_p指针调用func函数,由于c类中并未重写该函数,导致虚函数表中的地址还是&A::func,所以会调用A::funcA,如果使用B* b_p指针调用func函数也是如此,它们会指向C类对象内存布局中属于该指针类型的基类子对象,从而找到vptr,调用虚函数表中对应的函数。

  如果C类重写了虚函数,那么重写了哪一个基类的虚函数,则该基类子对象内的vptr指针指向的虚函数表中的函数地址就会改变为重写后的函数地址,以下就不各个举例了。

  还有一种情况就是,如果C类自身拥有虚函数,那么会生成一个新的虚函数表吗?答案是不会的。如果C类自身拥有虚函数的话,那么这个函数地址会被添加到复制基类的虚函数表最后一个位置中,如果是多继承的情况下,会被 添加到继承顺序最先继承的基类虚函数表中。

  如果有小伙伴想测试我上述所说的结论,但不知道怎么测试的话,下面这个范例供参考。

  

#include <iostream>

using namespace std;
class A {
public:
    virtual void func()
    {
        cout << "A:func" << endl;
    }
    int a;
};
typedef void (*func_p)(void);
int main()
{
    A* p = new A;
    int** m = (int**)p;
    ((func_p)m[0][0])();//此处会调用A::func函数,m[0]就是虚函数表的地址,m[0][0]就是虚函数表中第一个函数的地址,也即是A::func的地址,
               //可以把虚函数表理解为一个数组,里面存放函数指针。
return 0; }

  需要注意的有以下几点:

  1.如果用基类指针指向了派生类对象并不是指向了该派生类对象的首地址,而是会指向该派生类中基类子对象的首地址

  2.多继承的情况下,会按继承顺序生成基类子对象,如果是上述例子中也就是先生成A后生成B,最后生成C类自己的成员。

  3.一般情况下vptr指针会在基类子对象的类成员上方(有一种解释是为了效率,不用进行偏移就可以找到vptr指针从而找到虚函数表)。

  4.如果基类虚函数中含有缺省值(默认值),这个缺省值并不会实现多态,即便调用了派生类函数,但是这个缺省值还是来自于基类函数(为了执行的效率,缺省值并不实现多态),这里就不测试了。

  5.以上所有测试结论来自于我本身所用环境vs2019 32位环境下,各个编译器实现不一定相同,这里不做多的探究了。

posted @ 2023-08-16 07:52  justloving  阅读(366)  评论(0编辑  收藏  举报