虚拟成员函数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间的转换。

原文地址:https://www.cnblogs.com/lxy-xf/p/11026991.html