深入理解C++中的多态

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/m0_37962600/article/details/81585557

一、多态的分类

1.静多态:在编译期间就可以确定函数的调用地址,并产生代码。也就是说地址是早早绑定的;其往往是通过函数重载和模板来实现;

2.动多态:函数调用的地址不能在编译期间确定,必须在运行时才确定;其主要是通过虚函数来实现;

二、动多态

1、什么是动多态?

简而言之就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数;

2、动多态的实现

虚函数主要实现了多态的机制。虚函数是通过一张虚函数表来实现的,简称V-Table。在这个表中,主要是一个类的虚函数地址表,这张表解决了继承、覆盖的问题,保证其能真实反映实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

虚函数表指针:类中除了定义的函数成员,还有一个成员是虚函数表指针(占四个基本内存单元),这个指针指向一个虚函数表的起始位置,这个表会与类的定义同时出现,这个表存放着该类的虚函数指针,调用的时候可以找到该类的虚函数表指针,通过虚函数表指针找到虚函数表,通过虚函数表的偏移找到函数的入口地址,从而找到要使用的虚函数

虚函数表(在类外,存放在.rodata段)

C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的最高的性能--如果有多层继承或者多重继承的情况下)。这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数;

假设有下面这样一个类:

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

按照上面的说法,我们可以通过Base的实例来得到虚函数表:

typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
/*
可以通过强行把&b转成int*,取得函数表的地址。然后,再次取址就可以得到第一个虚函数的地址,也就是Base::f();
*/
cout<<"虚函数表的地址"<<(int*)(&b)<<endl;
cout<<"虚函数表第一个函数地址:"<<(int*)*(int*)(&b)<<endl;
//第一个虚函数
pFun = (Fun)*((int*)*(int*)(&b));
pFun();

最终的运行结果:

虚函数表地址:0012FED4
虚函数表第一个函数地址:0044F148
Base::f

如果要调用Base::g()和Base::h(),其代码如下:

(Fun)*((int*)*(int*)(&b)+0); //Base::f()
(Fun)*((int*)*(int*)(&b)+1); //Base::g()
(Fun)*((int*)*(int*)(&b)+2); //Base::h()

参考下图会更容易理解哦!

注:虚函数表最后的那个节点是虚函数表的结束节点,就像字符串的结束符“\0”一样,标志了虚函数表的结束。

继承时的虚函数表

(1)一般继承(无虚函数覆盖)

                                                                

在这个继承关系中,子类没有重载任何父类的函数,那么在派生类的实例中,其虚函数表如下所示:

对于实例:Derived d的虚表如下:

          

我们可以看到以下几点:a、虚函数按照其声明顺序放在表中;

                                            b、父类的虚函数在子类的虚函数前面;

(2)一般继承(有虚函数覆盖)

如图所示:

                                                     

对于一个派生类的实例,其虚函数表如下所示:

可以看到如下几点:a、覆盖的f()函数放到了虚表中原来父类虚函数的位置;

                                    b、没有被覆盖的函数依旧;

我们就可以看到如下面的程序:

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

  由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数所取代,于是在实际调用时,是Derive::f()被调用了。这就实现了多态;

(3)多重继承(无虚函数覆盖)

假设有如下的继承关系:

                                                       

对于子类实例中的虚函数表,是下面这个样子:

我们可以看到:a、每个父类都有自己的虚表;

                            b、子类的成员函数被放到了第一个父类的表中(所谓的第一个父类是按照声明顺序来判断的);

(4)多重继承(发生虚函数覆盖的情况)

如下图所示,我们在子类中覆盖了父类的f()函数:

                                                                 

下面是对应子类实例中的虚函数表:

我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以以任一静态类型的父类来指向子类,并调用子类的f(),如下:

Derive d;
Base *b1 = &d;
Base *b2 = &d;
Base *b3 = &d;

b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()

b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()

多态中存在的问题

[-:>内存泄漏

例如上面的程序中,如果在圆形的类中定义一个圆心的坐标,并且坐标是在堆中申请的内存,则在mian函数中通过父类指针操作子类对象的成员函数的时候是没有问题的,可是在销毁对象内存的时候则只是执行了父类的析构函数,子类的析构函数却没有执行,这会导致内存泄漏。

解决办法:将析构函数声明为虚函数。

纯虚函数

在成员函数的形参后面写上=0,则成员函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。纯虚函数必须在派生类中重新定义以后,派生类才能实例化出对象。

抽象类往往用于这样的情况,它可以方便我们使用多态特性,且在很多情况下,基类本身生成对象是不合情理的,我们知道所有的对象都是通过类来描绘的,但是反过来却不是这样。并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就要使用抽象类,就像一个水果类可以派生出橘子香蕉苹果等等,但是水果类本身定义对象并不合理也没有必要。

vfptr与vbptr的区别

https://blog.csdn.net/beibiannabian/article/details/80497012

 

阅读更多
换一批

没有更多推荐了,返回首页