zoukankan      html  css  js  c++  java
  • C++多态与虚函数表

    转自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
    {
    public:
    virtual void x() { cout << "Base::x()" << endl; }
    virtual void y() { cout << "Base::y()" << endl; }
    virtual void z() { cout << "Base::z()" << endl; }
    };

    class Derive : public Base
    {
    public:
    virtual void x1() { cout << "Derive::x1()" << endl; }
    virtual void y1() { cout << "Derive::y1()" << endl; }
    virtual void z1() { cout << "Derive::z1()" << endl; }
    };


    在这个单继承的关系中,子类没有重写父类的任何方法,而是加入了三个新的虚函数。Derive类实例的虚函数表布局如图示:

    • Derive class 继承了 Base class 中的三个虚函数,准确的说,是该函数实体的地址被拷贝到 Derive 实例的虚函数表对应的 slot 之中。
    • 新增的 虚函数 置于虚函数表的后面,并按声明顺序存放。

    2、有虚函数覆盖

    如果在继承关系中,子类重写了父类的虚函数:

    class Base
    {
    public:
    virtual void x() { cout << "Base::x()" << endl; }
    virtual void y() { cout << "Base::y()" << endl; }
    virtual void z() { cout << "Base::z()" << endl; }
    };

    class Derive : public Base
    {
    public:
    virtual void x() { cout << "Derive::x()" << endl; } // 重写
    virtual void y1() { cout << "Derive::y1()" << endl; }
    virtual void z1() { cout << "Derive::z1()" << endl; }
    };


    则Derive类实例的虚函数表布局为:

    相比于无覆盖的情况,只是把 Derive::x() 覆盖了Base::x(),即第一个槽的函数地址发生了变化,其他的没有变化。

    这时,如果通过绑定了子类对象的基类指针调用函数 x(),会执行 Derive 版本的 x(),这就是多态。

     

    多重继承时的虚函数表

    1、无虚函数覆盖

    现有如下的多重继承关系,子类没有覆盖父类的虚函数:

    class Base1
    {
    public:
    virtual void x() { cout << "Base1::x()" << endl; }
    virtual void y() { cout << "Base1::y()" << endl; }
    virtual void z() { cout << "Base1::z()" << endl; }
    };

    class Base2
    {
    public:
    virtual void x() { cout << "Base2::x()" << endl; }
    virtual void y() { cout << "Base2::y()" << endl; }
    virtual void z() { cout << "Base2::z()" << endl; }
    };

    class Derive : public Base1, public Base2
    {
    public:
    virtual void x1() { cout << "Derive::x1()" << endl; }
    virtual void y1() { cout << "Derive::y1()" << endl; }
    };


    对于 Derive 实例 d 的虚函数表布局,如下图:

    可以看出:

    • 每个基类子对象对应一个虚函数表
    • 派生类中新增的虚函数放到第一个虚函数表的后面

    注意,这也就是为什么《深度探索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
    {
    public:
    virtual void x() { cout << "Base1::x()" << endl; }
    virtual void y() { cout << "Base1::y()" << endl; }
    virtual void z() { cout << "Base1::z()" << endl; }
    };

    class Base2
    {
    public:
    virtual void x() { cout << "Base2::x()" << endl; }
    virtual void y() { cout << "Base2::y()" << endl; }
    virtual void z() { cout << "Base2::z()" << endl; }
    };

    class Derive : public Base1, public Base2
    {
    public:
    virtual void x() { cout << "Derive::x()" << endl; } // 重写
    virtual void y1() { cout << "Derive::y1()" << endl; }
    };


    相比于无覆盖的情况,只是将Derive::x()覆盖了Base1::x()Base2::x()而已,你可以自己写测试代码测试一下,这里就不再赘述了。

    注:若虚函数是 private 或 protected 的,我们照样可以通过访问虚函数表来访问这些虚函数,即上面的测试代码一样能运行。

    附:编译器对指针的调整

    在多重继承下,我们可以将子类实例绑定到任一父类的指针(或引用)上。以上述有覆盖的多重继承关系为例:

    Derive b;
    Base1* ptr1 = &b; // 指向 b 的初始地址
    Base2* ptr2 = &b; // 指向 b 的第二个子对象
    • 因为 Base1 是第一个基类,所以 ptr1 指向的是 Derive 对象的起始地址,不需要调整指针(偏移)。
    • 因为 Base2 是第二个基类,所以必须对指针进行调整,即加上一个 offset,让 ptr2 指向 Base2 子对象。
    • 当然,上述过程是由编译器完成的。
    Base1* b1 = (Base1*)ptr2;  
    b1->y(); // 输出 Base2::y()
    Base2* b2 = (Base2*)ptr1;
    b2->y(); // 输出 Base1::y()

    这里,由于ptr2指向的是Base2子对象的地址,因此尽管b1是Base1型指针,同时也将ptr2转换成了Base1,但ptr2的地址仍然是Base2的地址,因此调用y()也将调用Base2中的y();
    同理,由于ptr1指向的是Base1的地址,因此尽管b2是Base2型指针,同时也将ptr1转换成了Base2,但ptr1的地址仍然是Base1的地址,因此调用y()也将调用Base1中的y()
    其实,通过某个类型的指针访问某个成员时,编译器只是根据类型的定义查找这个成员所在偏移量,用这个偏移量获取成员。由于 ptr2 本来就指向 Base2 子对象的起始地址,所以b1->y()调用到的是Base2::y(),而 ptr1 本来就指向 Base1 子对象的起始地址(即 Derive对象的起始地址),所以b2->y()调用到的是Base1::y()
    
    
  • 相关阅读:
    TP
    vim manual 个人笔记
    关于动画属性
    过渡
    关于 css3 的filter属性
    html 中行内元素和块级元素区别
    JS以不同的格式保存文件内容
    64位Kali无法顺利执行pwn1问题的解决方案
    鱼龙混杂 · 数据结构学习笔记(01)
    Terminal(终端) 在 OS X下如何快速调用
  • 原文地址:https://www.cnblogs.com/lxy-xf/p/11418676.html
Copyright © 2011-2022 走看看