虛方法的調用是怎麼實現的(單繼承VS多繼承)

咱們知道經過一個指向之類的父類指針能夠調用子類的虛方法,由於子類的方法會覆蓋父類一樣的方法,經過這個指針能夠找到對象實例的地址,經過實例的地址能夠找到指向對應方法表的指針,而經過這個方法的名字就能夠肯定這個方法在方法表中的位置,直接調用就行,在多繼承的時候,一個類可能有多個方法表,也就有多個指向這些方法表的指針,一個類有多個父類,怎麼經過其中一個父類的指針調用之類的虛方法?ios

其實前面幾句話並無真正說清楚,在單繼承中,父類是怎麼調用子類的虛方法的,還有多繼承又是怎麼實現這點的,想知道這些,請認真往下看。函數

咱們先看單繼承是怎麼實現的。先上兩個簡單的類:佈局

#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;
}

運行結果以下:性能

經過運行結果咱們知道:經過父類指向子類的指針調用的是子類的虛方法。在單一繼承中,雖然父類有父類的虛方法表,子類有子類的虛方法表,可是子類並無指向父類虛方法的指針,在子類的實例中,子類和父類是公用一個虛方法表,固然只有一個指向方法表的指針,爲何能夠公用一個虛方法表呢,虛方法表的第一個方法是析構函數,子類的方法會覆蓋父類的一樣的方法,子類新增的虛方法放在虛方法表的後面,也就是說子類的虛方法表徹底覆蓋父類的虛方法表,即子類的每一個虛方法與父類對應的虛方法,在各類的方法表中的索引是同樣的。this

可是在多繼承中就不是這樣了,第一個被繼承的類使用起來跟單繼承是徹底同樣的,可是後面被繼承的類就不是這樣了,且仔細往下看。spa

仍是先上3個簡單的類3d

#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(); 
    }
}

運行結果以下:
指針

從結果說話:code

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仍是當作接口來用吧。

相關文章
相關標籤/搜索