C++編譯期多態與運行期多態

前言

今日的C++不再是個單純的“帶類的C”語言,它已經發展成爲一個多種次語言所組成的語言集合,其中泛型編程與基於它的STL是C++發展中最爲出彩的那部分。在面向對象C++編程中,多態是OO三大特性之一,這種多態稱爲運行期多態,也稱爲動態多態;在泛請添加鏈接描述型編程中,多態基於template(模板)的具現化與函數的重載解析,這種多態在編譯期進行,因此稱爲編譯期多態或靜態多態。在本文中,我們將瞭解:

  • 什麼是運行期多態
  • 什麼是編譯期多態
  • 它們的優缺點在哪

運行期多態

運行期多態的設計思想要歸結到類繼承體系的設計上去。對於有相關功能的對象集合,我們總希望能夠抽象出它們共有的功能集合,在基類中將這些功能聲明爲虛接口(虛函數),然後由請添加鏈接描述子類繼承基類去重寫這些虛接口,以實現子類特有的具體功能。典型地我們會舉下面這個例子:
C++編譯期多態與運行期多態

class Animal
{
public :
    virtual void shout() = 0;
};
class Dog :public Animal
{
public:
    virtual void shout(){ cout << "汪汪!"<<endl; }
};
class Cat :public Animal
{
public:
    virtual void shout(){ cout << "喵喵~"<<endl; }
};
class Bird : public Animal
{
public:
    virtual void shout(){ cout << "嘰喳!"<<endl; }
};

int main()
{
    Animal * anim1 = new Dog;
    Animal * anim2 = new Cat;
    Animal * anim3 = new Bird;

   //藉由指針(或引用)調用的接口,在運行期確定指針(或引用)所指對象的真正類型,調用該類型對應的接口
    anim1->shout();
    anim2->shout();
    anim3->shout();

    //delete 對象
    ...
   return 0;
}

運行期多態的實現依賴於虛函數機制。當某個類聲明瞭虛函數時,編譯器將爲該類對象安插一個虛函數表指針,併爲該類設置一張唯一的虛函數表,虛函數表中存放的是該類虛函數地址。運行期間通過虛函數表指針與虛函數表去確定該類虛函數的真正實現。

運行期多態的優勢還在於它使處理異質對象集合稱爲可能:

//我們有個動物園,裏面有一堆動物
int main()
{
    vector<Animal*>anims;

    Animal * anim1 = new Dog;
    Animal * anim2 = new Cat;
    Animal * anim3 = new Bird;
    Animal * anim4 = new Dog;
    Animal * anim5 = new Cat;
    Animal * anim6 = new Bird;

    //處理異質類集合
    anims.push_back(anim1);
    anims.push_back(anim2);
    anims.push_back(anim3);
    anims.push_back(anim4);
    anims.push_back(anim5);
    anims.push_back(anim6);

    for (auto & i : anims)
    {
        i->shout();
    }
    //delete對象
    //...
    return 0;
}

總結:運行期多態通過虛函數發生於運行期

編譯期多態

對模板參數而言,多態是通過模板具現化和函數重載解析實現的。以不同的模板參數具現化導致調用不同的函數,這就是所謂的編譯期多態。相比較於運行期多態,實現編譯期多態的類之間並不需要成爲一個繼承體系,它們之間可以沒有什麼關係,但約束是它們都有相同的隱式接口。我們將上面的例子改寫爲:

class Animal
{
public :
    void shout() { cout << "發出動物的叫聲" << endl; };
};
class Dog
{
public:
     void shout(){ cout << "汪汪!"<<endl; }
};
class Cat
{
public:
     void shout(){ cout << "喵喵~"<<endl; }
};
class Bird
{
public:
     void shout(){ cout << "嘰喳!"<<endl; }
};
template <typename T>
void  animalShout(T & t)
{
    t.shout();
}
int main()
{
    Animal anim;
    Dog dog;
    Cat cat;
    Bird bird;

    animalShout(anim);
    animalShout(dog);
    animalShout(cat);
    animalShout(bird);

    getchar();
}

在編譯之前,函數模板中t.shout()調用的是哪個接口並不確定。在編譯期間,編譯器推斷出模板參數,因此確定調用的shout是哪個具體類型的接口。不同的推斷結果調用不同的函數,這就是編譯器多態。這類似於重載函數在編譯器進行推導,以確定哪一個函數被調用。

運行期多態與編譯期多態優缺點分析

運行期多態優點
  1. OO設計中重要的特性,對客觀世界直覺認識。
  2. 能夠處理同一個繼承體系下的異質類集合。

    運行期多態缺點

    1.運行期間進行虛函數綁定,提高了程序運行開銷。
    2.龐大的類繼承層次,對接口的修改易影響類繼承層次。
    3.由於虛函數在運行期在確定,所以編譯器無法對虛函數進行優化。
    4.虛表指針增大了對象體積,類也多了一張虛函數表,當然,這是理所應當 值得付出的資源消耗,列爲缺點有點勉強。

    編譯期多態優點

    5.它帶來了泛型編程的概念,使得C++擁有泛型編程與STL這樣的強大武器。
    6.在編譯器完成多態,提高運行期效率。
    7.具有很強的適配性與鬆耦合性,對於特殊類型可由模板偏特化、全特化來處理。

    編譯期多態缺點

  3. 程序可讀性降低,代碼調試帶來困難。
  4. 無法實現模板的分離編譯,當工程很大時,編譯時間不可小覷。
  5. 無法處理異質對象集合。

    關於顯式接口與隱式接口

    所謂的顯式接口是指類繼承層次中定義的接口或是某個具體類提供的接口,總而言之,我們能夠在源代碼中找到這個接口.顯式接口以函數簽名爲中心請添加鏈接描述,例如:

    void AnimalShot(Animal & anim)
    {
    anim.shout();
    }

    我們稱shout爲一個顯式接口。在運行期多態中的接口皆爲顯式接口。

而對模板參數而言,接口是隱式的,奠基於有效表達式。例如:

template <typename T>
void AnimalShot(T & anim)
{
    anim.shout();
}

對於anim來說,必須支持哪一種接口,要由模板參數執行於anim身上的操作來決定,在上面這個例子中,T必須支持shout()操作,那麼shout就是T的一個隱式接口。請添加鏈接描述

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