C++中虛函數功能的實現機制

 

C++中虛函數功能的實現機制

要理解C++中虛函數是如何工作的,需要回答四個問題。

1、  什麼是虛函數。

虛函數由於必須是在類中聲明的函數,因此又稱爲虛方法。所有以virtual修飾符開始的成員函數都成爲虛方法。此時注意是virtual修飾的成員函數不是virtual修飾的成員函數名。

例如:基類中定義:

                            virtual void show();           //由於有virtual修飾因此是虛函數

                            voidshow(int);          //雖然和前面聲明的show虛函數同名,但不是虛函數。

所有的虛函數地址都會放在所屬類的虛函數表vtbl中。另外在基類中聲明爲虛函數的成員方法,到達子類時仍然是虛函數,即使子類中重新定義基類虛函數時未使用virtual修飾,該函數地址仍會放在子類的虛函數表vtbl中。

 

2、  正確區分重載、重寫和隱藏。

注意三個概念的適用範圍:處在同一個類中的函數纔會出現重載。處在父類和子類中的函數纔會出現重寫和隱藏。

重載:同一類中,函數名相同,但參數列表不同。

重寫:父子類中,函數名相同,參數列表相同,且有virtual修飾。

隱藏:父子類中,函數名相同,參數列表相同,但沒有virtual修飾;

                    或:函數名相同,參數列表不同,無論有無virtual修飾都是隱藏。

例如:

                   基類中:(1)    virtual void show();           //是虛函數

                                     (2)    void show(int);          //不是虛函數

                   子類中:(3)    void show();                        //是虛函數

                                     (4)    void show(int);          //不是虛函數

1,2構成重載,3,4構成重載,1,3構成重寫,2,4構成隱藏。另外2,3也會構成隱藏,子類對象無法訪問基類的void show(int)成員方法,但是由於子類中4的存在導致了子類對象也可以直接調用void show(int)函數,不過此時調用的函數不在是基類中定義的void show(int)函數2,而是子類中的與3重載的4號函數。

 

3、  虛函數表是如何創建和繼承的。

基類的虛函數表的創建:首先在基類聲明中找到所有的虛函數,按照其聲明順序,編碼0,1,2,3,4……,然後按照此聲明順序爲基類創建一個虛函數表,其內容就是指向這些虛函數的函數指針,按照虛函數聲明的順序將這些虛函數的地址填入虛函數表中。例如若show放在虛函數聲明的第二位,則在虛函數表中也放在第二位。

 

對於子類的虛函數表:首先將基類的虛函數表複製到該子類的虛函數表中。若子類重寫了基類的虛函數show,則將子類的虛函數表中存放show的函數地址(未重寫前存放的是子類的show虛函數的函數地址)更新爲重寫後函數的函數指針。若子類增加了一些虛函數的聲明,則將這些虛函數的地址加到該類虛函數表的後面。

 

4、  虛函數表是如何訪問的。

當執行pBase->show()時,要觀察show在Base基類中聲明的是虛函數還是非虛函數。若爲虛函數將使用動態聯編(使用虛函數表決定如何調用函數),若爲非虛函數則使用靜態聯編(根據調用指針pBase的類型來確定調用哪個類的成員函數)。此處假設show爲虛函數,首先:由於檢查到pBase指針類型所指的類Base中show定義爲虛函數,因此找到pBase所指的對象(有可能是Base類型也可能是Extend類型。),訪問對象得到該對象所屬類的虛函數表地址。其次:查找show在Base類中聲明的位置在Base類中所有虛函數聲明中的位序。然後到pBase所指對象的所屬類(有可能是Extend哦,多態)的虛函數表中訪問該位序的函數指針,從而得到要執行的函數。

         例如:

                   基類Base::virtualvoid show();                 (1)

                   子類Extend::virtualvoid show();             (2)

                   Externext;

                   Base*pBase=&ext;

                   pBase->show();

 

當執行pBase->show();時首先到Base中查看show(),發現其爲虛函數,然後訪問pBase指向的ext對象,在對象中得到Extend類的虛函數表,在Base類聲明中找到show()聲明的位序0,訪問Extend類的虛函數表的位置0,得到show的函數地址。注意若只有基類定義了virtual void show();而子類未重寫virtual void show();即上面的函數(2),則Extend虛函數表中的位序0中存放的地址仍然是Base類中定義的virtual void show()函數,而若Extend類中重寫了Base類中的virtual void show()方法,則Extend的虛函數表中位序0的函數地址將被更新爲Extend中新重寫的函數地址。從而調用pBase->show()時將產生多態的現象。

 

總結:當調用pBase->show();時,執行的步驟:

1,  判斷Base類中show是否爲虛函數。

2,  若不是虛函數則找到pBase所指向的對象所屬類Base。執行Base::show()。若是虛函數則執行步驟3.

3,  訪問pBase所指對象的虛函數表指針得到pBase所指對象所在類的虛函數表。

4,  查找Base中show()在聲明時的位序爲x,到步驟3得到的虛函數表中找到位序x,從而得到要執行的show的函數地址。

5,  根據函數地址和Base中聲明的show的函數類型(形參和返回值)訪問地址所指向的函數。

以上爲虛函數的工作機制。

注意只有用virtual修飾的成員方法纔會放到虛函數表中去。

子類對父類函數的隱藏將導致無法通過子類對象訪問基類的成員方法。

因此給出以下建議:

1、  若要在子類中重新定義父類的方法(有virtual爲重寫,無virtual爲隱藏),則應確保子類中的函數聲明和父類函數聲明中的形參完全一樣。但返回值類型是基類引用/指針的成員函數在重新定義時可以返回子類的引用/指針(返回值協變),這是由於子類的對象可以賦給基類引用/指針。

2、  若基類中聲明瞭函數的重載版本,則在派生類中重新定義時應該重新定義所有基類的重載版本。這是因爲,重新定義一個函數,其他的基類重載版本將被隱藏,導致子類無法使用這些基類的成員方法。所以需要每個都重新定義。若一些父類的重載版本,子類確實不需要修改,則由於重新定義了一個重載版本,即使有些重載版本不需要修改也要重新定義,在定義體中直接調用基類的成員方法(使用作用於限定符訪問)。

3、  從虛函數的實現機制可以看到要想在子類中實現多態需要滿足三個重要的條件。(1)在基類中函數聲明爲虛函數。(2)在子類中,對基類的虛函數進行了重寫。(3)基類的指針指向了子類的對象。

隱藏測試: 子類中只定義了show(),那麼子類對象將不能訪問父類的show(int)

#include<iostream>
using namespace std;
class Parent{
public:
    void show()
    {
        cout<<"Parent::show()"<<endl;
    }
    void show(int t)
    {
        cout<<"Parent::show()"<<t<<endl;
    }
};
class Child: public Parent{
public:
    void show()
    {
        cout<<"Child::show()"<<endl;
    }
};
int main()
{
    Child c;
    c.show();

    getchar();
    return 0;
}
Child::show()

多態 父類中沒有定義虛函數show,則使用靜態聯編,根據指針p的類型來決定調用哪個類的函數
#include<iostream>
using namespace std;
class Parent{
public:
    void show()
    {
        cout<<"Parent::show()"<<endl;
    }
    void show(int t)
    {
        cout<<"Parent::show()"<<t<<endl;
    }
};
class Child: public Parent{
public:
    void show()
    {
        cout<<"Child::show()"<<endl;
    }
};
int main()
{
    Child c;
    Parent *p=&c;
    p->show();
    p->show(2);

    getchar();
    return 0;
}

Parent::show()

Parent::show()2

多態 父類中定義了虛函數show,查找show在Base類中聲明的位置在父類中所有虛函數聲明中的位序。然後找到p所指對象的所屬類的虛函數表中訪問該位序的函數指針,從而得到要執行的函數。

#include<iostream>
using namespace std;
class Parent{
public:
    virtual void show()
    {
        cout<<"Parent::show()"<<endl;
    }
    void show(int t)
    {
        cout<<"Parent::show()"<<t<<endl;
    }
};
class Child: public Parent{
public:
    void show()
    {
        cout<<"Child::show()"<<endl;
    }
};
int main()
{
    Child c;
    Parent *p=&c;
    p->show();
    p->show(2);

    getchar();
    return 0;
}

Child::show()

Parent::show()2

多態 如果子類中沒有重新父類方法,那麼複製父類的虛函數表

#include<iostream>
using namespace std;
class Parent{
public:
    virtual void show()
    {
        cout<<"Parent::show()"<<endl;
    }
    void show(int t)
    {
        cout<<"Parent::show()"<<t<<endl;
    }
};
class Child: public Parent{
public:
    void show()
    {
        cout<<"Child::show()"<<endl;
    }
};
class GrandChild:public Child{

};
int main()
{
    GrandChild c;
    Parent *p=&c;
    p->show();
    p->show(2);

    getchar();
    return 0;
}

Child::show()

Parent::show()2

多態 如果子類重寫了父類的虛函數,那麼通過p指針所在類的該虛函數的位序訪問子類中的對應的函數,如果子類重寫的是非虛函數,那麼通過指針p只能訪問p所在類的該函數

#include<iostream>
using namespace std;
class Parent{
public:
    virtual void show()
    {
        cout<<"Parent::show()"<<endl;
    }
    void show(int t)
    {
        cout<<"Parent::show()"<<t<<endl;
    }
};
class Child: public Parent{
public:
    void show()
    {
        cout<<"Child::show()"<<endl;
    }
    void show(int t)
    {
        cout<<"Child::show()"<<t<<endl;
    }
};
class GrandChild:public Child{
    void show()
    {
        cout<<"GrandChild::show()"<<endl;
    }
    void show(int t)
    {
        cout<<"GrandChild::show()"<<t<<endl;
    }
};
int main()
{
    GrandChild c;
    Parent *p=&c;
    p->show();
    p->show(2);

    getchar();
    return 0;
}

GrandChild::show()

Parent::show()2

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章