C++類的繼承概念辨析:虛函數,虛函數表,抽象基類,純虛函數,虛基類,虛繼承

虛函數和虛函數表

在瞭解什麼是虛函數之前,首先要理解什麼是動態綁定。

動態綁定
  • 動態綁定是C++類指針或引用的特性,當編譯器遇到一個基類指針或引用時,並不直接確定其類型,而是在運行時根據其具體指向來調用對應的函數。
  • 爲什麼基類指針和引用可以指向派生類呢?因爲類的繼承關係是一種is a的關係,即派生類是特殊的基類,因此基類指針和引用可以指向派生類,但是此時的基類指針和引用,只能訪問基類的對象和成員函數。如果想要在不發生強制類型轉換的情況下訪問派生類的成員函數,就要用到虛函數的概念。
虛函數應用示例
  • 虛函數的實現前提是,派生類和基類中存在同樣的函數,名字和形參列表都一致,在基類中該函數有virtual關鍵字聲明,派生類中無所謂。
  • 於是當程序執行遇到基類指針或者引用時,先判斷指向的類型,再根據類型調用相關的函數。注意:只要是基類指針指向的虛函數調用,均會實行動態綁定,容易忽略的一點是在基類的成員函數內部發生的調用,隱含了this指針,所以同樣會發生動態綁定。可以觀察以下代碼,猜測其輸出,然後和運行結果進行對比。
#include <iostream>

using namespace std;
class base
{
public:
    virtual void display()
    {
        cout<<"I am  base display\n";
    }
    void display(int i)
    {
        cout<<"base i="<<i<<endl;
    }
    void show()
    {
        cout<<"this is base show"<<endl;
        display();
    }
};
class derived:public base
{
public:
    void display( )
    {
        cout<<"I am derived display\n";
    }
    void display(int i)
    {
        cout <<"derived i ="<<i<<endl;
    }
    void show()
    {
        cout<<"this is derived show"<<endl;
        display();
    }
};
int main()
{
    base *b;
    derived d;
    b=&d;
    b->base::display();
    b->display(10);
    b->show();
    return 0;
}
/*運行結果:
I am  base display
base i=10
this is base show
I am derived display
*/
  • 在上述例子中同時涉及了函數重載和虛函數的動態綁定。可以看到即使是在基類的非虛函數中調用虛函數,也會發生動態綁定。
  • 想要直接,確定的調用基類的成員函數,必須使用類限定符。而如果不使用虛函數,使用派生類限定符直接調用派生類的成員函數,此時會進行靜態綁定,編譯器檢測到調用與指針類型不符合,會報錯。
虛函數的實現:虛函數表

虛函數具體是如何實現的呢?如果一個成員函數是虛函數,那麼在基類指針發生虛函數調用的地方,編譯器不會給出函數調用地址,而是檢索基類的派生鏈,找到其所有派生類,建立一個虛函數表,表的內容是對象類型,和對應的虛函數的調用地址。在程序運行時,根據對象類型找到對應的虛函數入口,從而實現了動態綁定。

虛函數的意義

虛函數有什麼作用呢?虛函數使得我們在編寫程序時不需要考慮對象的具體類型,可以使用統一的代碼來完成對不同類型的對象的處理。相當於對上層函數隱藏了各種複製派生類的具體實現,而提供了統一的接口,就像交通工具類可以派生出輪船,飛機,汽車火車,每個派生類都有move函數,move的方式也不一樣,但是需要調用交通工具類進行move操作的函數則不需要關心被調用的對象是如何實現move的,它只需要使用一個交通工具類對象調用move函數就可以了。

抽象類和純虛函數

在虛函數的基礎上,進一步設想,假如我現在要設計一個基類來派生出不同的子類,我要求每種子類都要實現某個功能。但是基類不需要或者沒有辦法實現這個功能,只是提供了一個大致框架方便統一調用,我該怎麼辦呢?

  • 學過java的同學會想到java中的接口類型,只需給出函數的聲明而不需要實現。在C++中類似的功能由抽象類實現。
  • 就像上述提到的交通工具類派生出輪船,飛機,汽車、火車類,每個類都要有move函數,所以move函數應該是虛函數才能實現動態綁定。但是沒有具體指明交通工具類型時,我們無法對move函數進行定義,所以此時可以把交通工具類聲明爲抽象類。
  • 抽象類就是其中的虛函數只有聲明,沒有定義的類型,我們用虛函數聲明=0的形式來進行表示。這種虛函數,稱爲純虛函數。
  • 抽象類,顧名思義,抽象的,沒有具體實現,因此無法生成其對象,而抽象類的派生類必須對抽象類中的純虛函數進行實現,否則仍然是抽象類。

虛基類和虛繼承

虛基類和虛繼承主要是用來解決交叉繼承的問題。

  • 在普通繼承方式中,派生類繼承基類的同時,派生類的對象會生成一份基類對象,如果- 基類對象又有基類,派生類同時還會擁有一個間接基類對象。問題來了,派生類可能會擁有兩個或者多個直接基類,如果這些直接基類又是從同一個基類繼承而來,那麼派生類就會擁有許多個間接基類的對象。
  • 而在有些情況下,我們希望所有的間接基類繼承都只會指向同一個間接基類對象,在這種情況下就要用到虛繼承,而虛繼承對應的間接基類,也就是那個我們只需要一份對象的間接基類,稱爲虛基類。
  • 所以事實上,虛基類不是類本身的特性,而是在某個類被虛繼承之後,我們給他的稱呼。任何一個非抽象類,都可以用來聲明虛繼承,都可以是一個虛基類。
  • 同時,由於虛基類必須保證其對象的唯一性,因此虛基類的對象實例化必須由最底層的派生類顯式給出,否則就會產生衝突。也即是說,如果一個類聲明瞭虛繼承,則虛基類對象的構造需要也必須由這個類的派生類實現,除非這個類沒有派生類。
虛基類的代碼實現
#include <iostream>
using namespace std;

class transportation
{
public:
    transportation(int i)
    {
        cout<<"transportation "<<i<<" born"<<endl;
    }
};
class car:virtual public transportation
{
public:
    car(int i):transportation(i)
    {
        cout<<"car "<<i<<" born"<<endl;
    }
};
class plane:virtual public transportation
{
public:
    plane(int i):transportation(i)
    {
        cout<<"plane "<<i<<" born"<<endl;
    }
};
class carplane:public car,public plane
{
public:
    carplane(int i ):car(i+2),plane(i+3)
    {
        cout<<"carplane "<<i<<" born"<<endl;
    }
};
int main()
{
    carplane t(0);
    return 0;
}

/*程序輸出
transportation 1 born
car 2 born
plane 3 born
carplane 0 born
*/
/*如果沒有使用虛繼承的方式,則程序輸入爲:
transportation 2 born
car 2 born
transportation 3 born
plane 3 born
carplane 0 born
*/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章