在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
转自https://www.cnblogs.com/findumars/p/9845429.html 首先第一点: 为什么运行时多态无法在编译期进行: 比如 class A { virtual void func(){...}; }; class A1:public A { void func(){...}; }; 对于这样一种最简单的形式,A1派生类对基类A中的func方法进行了重写。 在我们的程序中,假如有一个A*型指针pa: 对于pa->func(),我们并不知道它到底要调用哪个func,比如在某一行:pa=new A();pa->func();我们知道这时是调用的基类的func(); 但在下一行,很可能就是pa=new A1();pa->func();这下就是调用派生类A1的func方法了。 但对于编译器来说,这两个pa->func();对它来说它完全看不出任何区别,这样就无法在编译期将这个操作的函数地址定下来,因为func的地址既可能是A里的func的地址,也可能是A1中的func的地址,对于pa->func();这样有两种可能性,语义不详的语句,编译器当然不可能将它翻译成只有一个意思的机器指令,因此编译期是无法进行这种多态行为的。 但对于静态多态:重载与函数模板 对于重载func函数,由于是通过name magling的方式实现的,因此虽然都是func函数,但由于参数不同,实际上在name magling后,他们是两个名字完全不同的函数,这样在编译器看来,是完全可以把他们翻译成不同的机器指令的,因此是在编译期进行的。 对于模板来说,一个Add函数: 因此,对于编译器来说,它也是可以将这些调用翻译成不同的机器指令的。
其次第二点: 基类指针只能调用基类的方法(包括派生类重写的方法),而无法调用派生类新增的虚函数方法,尽管新增的虚函数很可能也写在这个基类的虚指针所指向的虚表中。 对于A* a=new B(); a->displaya()是可行的,但a->displayb()是不可行的,基类指针a只能调用A自己的方法和B重写的A中的方法。 这在逻辑上也是合理的,B是A的派生类,B是建立在A之上,再加上一部分特点所形成的的新的类,而A*型指针调用的方法应该是A与B的共性的方法,毕竟B是A的一种而A不是B的一种,A*型指针也正应该只能使用A与B的共性的部分,也就是A的方法。 class A { public: virtual void displaya() { cout<<"a"<<endl; } }; class B:public A { public: virtual void displayb() { cout<<"b"<<endl; } }; 关于运行时的多态的实现,猜测是不是这样进行的呢?(现在只是猜测,书还没看完) 还是以上面func()举例子: 对于A* pa=new A();pa->func(); 和A* pa=new A1();pa->func(); 虽然我们不知道pa到底指向谁,是A对象亦或是A1对象,但我们有一点是确定的,那就是它们的函数都是func函数 这样如果我们将虚指针vptr放置在固定的位置,则pa->func()就会能有两种功能了。 比如pa=new A();pa->func(); 这时pa指向A的起始地址,此时虚函数表中的func函数未被重写,因为是A对象,这样pa找到固定位置处的vptr,然后跳转到A的虚函数表,找到func(),然后执行。 对于pa=new A1();pa->func(); 这时pa还是指向A的起始地址(因为是先放基类,在存放派生类成员),此时虚函数表中的func已经被重写了,因为指向的是A1类型的对象,这样pa找到固定位置处的vptr,然后跳转到A的虚函数表(单一继承下派生类的虚函数还是写在A的虚表里),找到func,然后执行。 这样pa->func在运行期就可以根据不同的指向对象而执行不同的功能。 class A { virtual void func(){...}; }; class A1:public A { void func(){...}; };
虚函数表:单继承时的虚函数表:1、无虚函数覆盖 假如现有单继承关系如下: class Base
2、有虚函数覆盖 如果在继承关系中,子类重写了父类的虚函数: class Base
相比于无覆盖的情况,只是把 这时,如果通过绑定了子类对象的基类指针调用函数 x(),会执行 Derive 版本的 x(),这就是多态。 多重继承时的虚函数表1、无虚函数覆盖 现有如下的多重继承关系,子类没有覆盖父类的虚函数: class Base1 可以看出:
注意,这也就是为什么《深度探索C++对象模型里》对于多重继承的情况下要不断调整指针的指向的原因: 比如Base2* pb2=new Derive(); 由于Base2是与Base1平级的基类,因此pb2的指向要从new Derive()的起始地址调整到Base2 subobject处,因为pb2只能调用Derived与Base2的共有部分的函数,不能调用Base1部分的函数,因此要调整到Base2 subobject处,因为Base2的虚指针在此处,不然将使用Base1的虚指针进行跳转,从而不能调用Base2的方法。 而当delete pb2时又要跳转到new Derive()的起始处,因为派生类对析构函数是进行了重写的,而派生类中重写的函数将写入第一个基类,也就是Base1里的vptr所指向的虚表中,因此要调整到new Derive()首处,这样才能正确的调用析构函数。 2、有虚函数覆盖 将上面的多重继承关系稍作修改,让子类重写基类的 x() 函数: class Base1 相比于无覆盖的情况,只是将 注:若虚函数是 private 或 protected 的,我们照样可以通过访问虚函数表来访问这些虚函数,即上面的测试代码一样能运行。 附:编译器对指针的调整在多重继承下,我们可以将子类实例绑定到任一父类的指针(或引用)上。以上述有覆盖的多重继承关系为例: Derive b;
Base1* b1 = (Base1*)ptr2;
|
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论