• 虚拟成员函数Virtual Member Functions


    首先,假设:

          基类为Point *ptr;

          ptr=new Point2d; Point2d为派生类。

          z()为基类的虚拟成员函数,那么在调用ptr->z()时,这种使用基类指针来调用正确的z()实体的多态是怎么实现的?或者说,需要什么信息才能在执行期调用正确的z()?

    要调用正确的z(),必须要知道两点:

          1.ptr所指对象的真实类型。ptr到底是指向的一个基类对象,还是某个派生类对象,这关系到到底应该调用基类的z()还是某个派生类的z()。

          2.z()实体的位置,即我们知道了ptr指向了对象的类型之后,要到哪里去寻找这个z()的实体。

        对于问题1,在编译期是得不到结果的,只有在执行期才能真正知道ptr所指对象,也就是所谓的积极的多态(eg.ptr->z())。另外提一嘴,消极的多态在编译期完成(eg. ptr=new Point2d)。到底执行期是怎么判断出所指对象类型的,现在还没看到,估计应该是通过虚拟表格中slot[0]的type_info也就是 RTTI机制实现的(RTTI在第七章)。

        对于问题2,通过在类中添加一个数据成员:一个指向虚拟函数表格的指针(vptr)。通过这个指针可以找到虚拟函数表格,虚拟函数表格中又通过slot索引,区分不同的虚拟成员函数。

    对于虚拟成员函数,分为不同情况有不同讨论:

    单一继承

        最简单的情况,在单一继承下,整个类只存在一个vptr,也就是说不管怎么派生,只有一个指向虚拟成员函数表格的指针vptr,派生类只会在这个表格上覆写,添加函数。内存布局按照Base1,Base2,Base3……这样的情况,且vptr一定存在于Base1那一块的空间中,有且只有一个vptr。

    对虚拟成员函数的处理一共有三种情况:

        1).继承base class的虚拟成员函数。

        2).覆写base class的虚拟成员函数。

        3).加入新的虚拟函数,这是表格增加一个slot,用于存放新的函数实体地址。

        在编译时期,对虚拟成员函数的处理:

        首先,编译时期并不知道ptr指向的到底是个什么鬼,可能是基类也可能是派生类,但有一点一定是确定的,那就是我们知道经过ptr可以经过vptr得到它的虚拟成员函数表格。

        其次,虽然不知道到底应该调用哪个z(),但我知道不管哪个z(),它一定存在于slot 4(本例是如此)。也就是说,ptr指向基类对象也好,它通过vptr调用slot4指向的函数地址;ptr指向派生类对象也好,它也还是通过vptr调用slo4指向的函数。

        不管ptr指向什么鬼,它一定被转换为:(*ptr->vptr[4])(ptr);(ptr)是当this指针传进去,因为成员函数是不存放在类的空间中的,因此需要传递一个this指针来区分到底是哪个对象调用了z(),是ptr1还是ptr2。

    这画质一言难尽。

    多重继承:

        相比于单一继承,多重继承的复杂度出现在第二个及后继出现的base class,这使我们需要在执行期对this指针进行调整

        为什么这两点会使多重继承区分于单一继承呢?

        首先,多重继承中,Base1,Base2,Base3......间是并列的关系,而在单一继承中Base1,Base2,Base3....间是继承关系,因此首先引入的问题就是在单一继承中我们只有一个vptr,而在多重集成中将出现n个vptr。在单一继承中,基类指针只有一个,只要有一个Base1就能代表它的后续派生类Base2,Base3......;而在多重集成中,它的基类指针多达n个,要实现多态,就要求这n个基类指针都能指向它们共同的派生类且都要可以实现多态。

        第二个及后继出现的base classes引入的复杂度

        如果我们用的基类指针是第一个,也就是最左边的基类指针Base1,不会带来太大的麻烦,因为内存布局中,Base1是放在第一个的,也就是说,Base1的地址与派生类Derived的地址是一致的,在编译时期不用对它进行调整,当然这不代表它与单一继承完全一致,后面还是可能会出现需要对this指针进行调整的情况,这个后面在讲。如果我们用的基类指针是第二个及后继的基类指针Base3,Base4....,这会在编译时期就带来问题:Base2 *pbase2=new Derived;这种消极地多态需要进行调整,不同于Base1,因为Base2在内存布局中没有放在第一个,因此pbase2指向的区域应该进行调整,应该调整为&(new Derived)+sizeof(Base1),这样pbase2才指向正确的地址,不然像pbase2->data_Base2将指向从Base1开始,往下(data_Base2-&Base2)处的位置,而这个位置不知道到底是个啥。

        然后是对于析构函数,如今在编译时期进行调整后,pbase2指向的不是Derived的地址(相同与Base1的地址),而是Derived地址往下偏移sizeof(Base1)个字节的地方,但是如果我们调用析构函数时,应该调用Derived的析构函数,则此时又应该调整this指针,使之回到Derived地址来调用析构。这种情况也就是通过一个指向第二个base class的指针,来调用derived class的虚拟成员函数(该虚拟成员函数不存在于第二个base class的虚拟表格而存在于第一个基类的vptr指向的虚拟表格,这才造成了区别)。

        还有一种情况与上面这种类似,就是通过一个指向derived class的指针,来调用第二个base class中继承而来的虚拟函数。即图中调用mumble()(mumble是Base2的虚拟成员函数,Base1中不存在但Derived class与Base1共享一个vtbr)时的情况,此时需要调整this指针到指向第二个基类区域Base2。

        第三种情况是图中的clone函数。Base1中有一个返回类型为Base1的clone,Base2中有一个返回类型为Base2的clone,而派生类Derived中,又覆写了clone,直接导致Base1的vptr指向的函数表格中,slot3位置原本属于Base1的返回值类型为Base1的clone被覆写。而如果我们通过第二个基类指针来调用clone,且第二个指针指向派生类对象时,在调用时会对this指针进行调整,也就是上面的情况1,即通过一个指向第二个base class的指针,来调用derived class的虚拟成员函数。

    虚拟继承:

        这里书上讲的比较少,只讲了Point2d->Point3d这样的一个虚拟继承。

        虽然是Point2d->Point3d,但还是不同于单一继承。首先,布局中Point2d位于底部而Point3d位于最开始,且Point3d通过他的vptr指向的虚拟函数表格中的负索引来找到Point2d区域。而单一继承中基类在顶层而继承类放在底层。其次,其中有两个vptr,而单一继承只有一个vptr。因为多个vptr的存在,所以也一定像多重继承那样需要对this指针进行调整。如ptr指向派生类,但要调用基类的虚拟成员函数时....作者在此并未细讲,他的建议是,不要在一个虚基类中声明nonstatic data members,这样将减少在vptr间的转换。

  • 相关阅读:
    X、Y轴抖动的动画
    ViewFlipper的简单用法
    让手机连接到指定的WIFI网络,适用于之前已经连过的网络
    Eclipse 离线汉化的方法
    自己写的SeekBarPreference,可以实现seekbar滑动监听和设置默认进度和最大进度
    录制Android屏幕软件——屏幕录像专家
    【转】解决Android因加载多个大图引起的OutOfMemoryError,内存溢出的问题
    Java中的线程实现
    获得手机当前的ip地址
    操作Wifi的工具类
  • 原文地址:https://www.cnblogs.com/lxy-xf/p/11026991.html
Copyright © 2020-2023  润新知