zoukankan      html  css  js  c++  java
  • 虚方法的调用是怎么实现的(单继承VS多继承)

    我们知道通过一个指向之类的父类指针可以调用子类的虚方法,因为子类的方法会覆盖父类同样的方法,通过这个指针可以找到对象实例的地址,通过实例的地址可以找到指向对应方法表的指针,而通过这个方法的名字就可以确定这个方法在方法表中的位置,直接调用就行,在多继承的时候,一个类可能有多个方法表,也就有多个指向这些方法表的指针,一个类有多个父类,怎么通过其中一个父类的指针调用之类的虚方法?

    其实前面几句话并没有真正说清楚,在单继承中,父类是怎么调用子类的虚方法的,还有多继承又是怎么实现这点的,想知道这些,请认真往下看。

    我们先看单继承是怎么实现的。先上两个简单的类:

    #include <iostream> 
    using namespace std; 
    
    class A
    {
    public:
        A():a(0){}
    
        virtual ~A(){}
        
        virtual void GetA()
        {
            cout<<"A::GetA"<<endl; 
        }
    
        void SetA(int _a)
        {
            a=_a; 
        } 
        int a;
    };
    
    class B:public A
    {
    public:
        B():A(),b(0){}
    
        virtual ~B(){}
         
        virtual void GetA()
        { 
            cout<<"B::GetA"<<endl; 
        }
    
        virtual void GetB()
        { 
            cout<<"B::GetB"<<endl; 
        }
    private:
        int b;
    };
    
    typedef int (*Fun)(void);
    
    void TestA()
    {
        Fun pFun;
        A a; 
        cout<<"类A的虚方法(第0个是A的析构函数):"<<endl;
        int** pVtab0 = (int**)&a;
        for (int i=1; (Fun)pVtab0[0][i]!=NULL; i++){ 
            pFun = (Fun)pVtab0[0][i]; 
            cout << "    ["<<i<<"] "; 
            pFun(); 
        }
        cout<<endl;
        B b ;
        A* b1=&b;
    
        cout<<"类B的虚方法(第0个是B的析构函数)通过类B的实例:"<<endl;
        int** pVtab1 = (int**)&b;
        for (int i=1; (Fun)pVtab1[0][i]!=NULL; i++){ 
            pFun = (Fun)pVtab1[0][i]; 
            cout << "    ["<<i<<"] "; 
            pFun(); 
        }
        cout<<endl;
        cout<<"类B的虚方法(第0个是B的析构函数)通过类A的指针:"<<endl;
        int** pVtab2 = (int**)&*b1;
        for (int i=1; (Fun)pVtab2[0][i]!=NULL; i++){ 
            pFun = (Fun)pVtab2[0][i]; 
            cout << "    ["<<i<<"] "; 
            pFun(); 
        }
        cout<<endl;
        cout<<"     b的地址:"<<&b<<endl;
        cout<<"b1指向的地址:"<<b1<<endl<<endl;
    }

    运行结果如下:

    通过运行结果我们知道:通过父类指向子类的指针调用的是子类的虚方法。在单一继承中,虽然父类有父类的虚方法表,子类有子类的虚方法表,但是子类并没有指向父类虚方法的指针,在子类的实例中,子类和父类是公用一个虚方法表,当然只有一个指向方法表的指针,为什么可以公用一个虚方法表呢,虚方法表的第一个方法是析构函数,子类的方法会覆盖父类的同样的方法,子类新增的虚方法放在虚方法表的后面,也就是说子类的虚方法表完全覆盖父类的虚方法表,即子类的每个虚方法与父类对应的虚方法,在各种的方法表中的索引是一样的。

    但是在多继承中就不是这样了,第一个被继承的类使用起来跟单继承是完全一样的,但是后面被继承的类就不是这样了,且仔细往下看。

    还是先上3个简单的类

    #include <iostream> 
    using namespace std; 
    
    class A
    {
    public:
        A():a(0){}
    
        virtual ~A(){}
        
        virtual void GetA()
        {
            cout<<"A::GetA"<<endl; 
        }
         
        int a;
    };
    
    class B 
    {
    public:
        B():b(0){}
    
        virtual ~B(){}
         
        virtual void SB()
        { 
            cout<<"B::SB"<<endl; 
        } 
    
        virtual void GetB()
        {  
            cout<<"B::GetB"<<endl; 
        }
    
    private:
        int b;
    };
    
    class C:public A,public B 
    {
    public:
        C():c(0){}
    
        virtual ~C(){}
    
        virtual void GetB()//覆盖类B的同名方法
        { 
            cout<<"C::GetB"<<endl; 
        }
    
        virtual void GetC()
        { 
            cout<<"C::GetC"<<endl; 
        }
    
        virtual void JustC()
        {
            cout<<"C::JustC"<<endl; 
        }
    private:
        int c;
    };
    
    typedef int (*Fun)(void);
    
    void testC()
    {
        C* c=new C();
        A* a=c;
        B* b=c;
        Fun pFun;
        cout<<"sizeof(C)="<<sizeof(C)<<endl<<endl;
        cout<<"c的地址:"<<c<<endl;
        cout<<"a的地址:"<<a<<endl;
        cout<<"b的地址:"<<b<<endl<<endl<<endl;
         
        cout<<"类C的虚方法(第0个是C的析构函数)(通过C类型的指针):"<<endl;
        int** pVtab1 = (int**)&*c;
        for (int i=1; (Fun)pVtab1[0][i]!=NULL; i++){ 
            pFun = (Fun)pVtab1[0][i]; 
            cout << "    ["<<i<<"] "<<&*pFun<<"    "; 
            pFun(); 
        }
        cout<<endl<<endl;
        cout<<"类C的虚方法(第0个是C的析构函数)(通过B类型的指针):"<<endl;
        pVtab1 = (int**)&*b;
        for (int i=1; (Fun)pVtab1[0][i]!=NULL; i++){ 
            pFun = (Fun)pVtab1[0][i]; 
            cout << "    ["<<i<<"] "<<&*pFun<<"    "; 
            pFun(); 
        }
    }

    运行结果如下:

    从结果说话:

    Sizeof(C)=20,我们并不意外,在单继承的时候,父类和子类是公用一个指向虚方法表的指针,在多继承中,同样第一个父类和子类公用这个指针,而从第二个父类开始就有自己单独的指针,其实就是父类的实例在子类的内存中保持完整的结构,也就是说在多重继承中,之类的实例就是每一个父类的实例拼接而成的,当然可能因为继承的复杂性,会加一些辅助的指针。

    指针a与指针c指向同一个地址,即c的首地址,而b所指的地址与a所指的地址相差8字节刚好就是类A实例的大小,也就是说在C的内存布局中,先存放了A的实例,在存放B的实例,sizeof(B)=8(字段int b和指向B虚方法表的指针),在家上C自己的字段int c刚好是20字节。

    让我有点意外的是:方法B::SB,C::GetB并没有出现在类C的方法表中,而且C::GetB是C覆写B中的GetB方法,怎么没有出现在C的方法表中呢?在《深入探索C++对象模型》一书中讲到,这两个方法同时应该出现在C的方法表中,同样也会覆盖B的虚方法表。可能是不通的编译器有不同的实现,我用的是VS2010,那本书上讲的是编译器cfront

    OK,我们不用管不同的编译器实现上的区别,这点小区别无伤大雅,虚方法的调用机制还是一样的。

    先来分析几个小例子,看看虚方法的实现机制。

           C* c=new C();

           A* a=c;

           a->GetA();

           c->GetA();

           c->GetC();

    上面已经说了,a与c指向的是同一个地址,且公用同一个虚方法表,而方法GetA,GetC的地址就在这个方法表中,那么调用起来就简单多了,大致就是下面这个样子:

    a->GetA()   ->   (a->vptr1[1])(a);   // GetA在方法表中的索引是1

    c->GetA()  ->  (c->vptr1[1])(c);   // GetA在方法表中的索引是1

    c->GetC()   ->   (a->vptr1[2])(c);   // GetC在方法表中的索引是2

    vptr1表示指向类C第一个方法表的指针,这个指针实际的名字会复杂一些,暂且将指向类C的第一个方法表的指针命名为vptr2,下面会用到这个指针。

    再来分析几行代码:

         B* b=c;

           c->GetB();

           b->GetB();

    指针b和指针c指向的不是同一个地址,那么B* b=c;到底是做了啥呢?大致是会转换成下面这个样子:

    B* b=c+sizeof(A);

    c所指的地址加上A的大小,刚好是b所指的地址。

    c->GetB();同样需要转换,因为方法GetB根本不在c所指的那个方法表中,可能转换成这个样子(实际转换成啥样子我真不知道):

    this=c+sizeof(A);

    (this->vptr2[2])(c);

    如果像编译器cfront所说的那样,方法GetB在vptr1所指的方法表中,那么就不用产生调整this指针了,如果在vptr1所指的方法表中,就让方法表变大了,且跟别的方法表是重复的。

    b->GetB();就不需要做过多的转换了,因为b正好指向vptr2,可能转换成下面这个样子:

    b->GetB()   ->   (b->vptr2[2])(b);   // GetB在方法表中的索引是2

    总之指针所指的方法表如果没有要调用的方法,就要做调整,虚方法需要通过方法表调用,相对于非虚方法,性能就慢那么一点点,这也是别人常说的C++性能不如C的其中一点。

    虚多继承就更麻烦了,不熟悉可能就会被坑。《深入探索C++对象模型》这本书是这样建议的:不要在一个virtual base class中声明nonstatic data members,如果这样做,你会距复杂的深渊越来越近,终不可拔。

    virtual base class还是当做接口来用吧。

  • 相关阅读:
    直接插入排序学习笔记
    选择排序学习笔记
    冒泡排序学习笔记
    阿里云ssl证书申请及域名绑定流程
    Nginx user_agent、if指令及全局变量
    rewrite和return笔记
    rewrite和return的简单需求
    Nginx Rewrite正则表达式案例
    linux下WordPress伪静态规则设置
    集群前后端分离(api接口)
  • 原文地址:https://www.cnblogs.com/hlxs/p/3214062.html
Copyright © 2011-2022 走看看