C++編譯期多型與執行期多型

melonstreet發表於2016-01-15

前言

今日的C++不再是個單純的“帶類的C”語言,它已經發展成為一個多種次語言所組成的語言集合,其中泛型程式設計與基於它的STL是C++發展中最為出彩的那部分。在物件導向C++程式設計中,多型是OO三大特性之一,這種多型稱為執行期多型,也稱為動態多型;在泛型程式設計中,多型基於template(模板)的具現化與函式的過載解析,這種多型在編譯期進行,因此稱為編譯期多型或靜態多型。在本文中,我們將瞭解:

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

執行期多型

執行期多型的設計思想要歸結到類繼承體系的設計上去。對於有相關功能的物件集合,我們總希望能夠抽象出它們共有的功能集合,在基類中將這些功能宣告為虛介面(虛擬函式),然後由子類繼承基類去重寫這些虛介面,以實現子類特有的具體功能。典型地我們會舉下面這個例子:

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是哪個具體型別的介面。不同的推斷結果呼叫不同的函式,這就是編譯器多型。這類似於過載函式在編譯器進行推導,以確定哪一個函式被呼叫。

執行期多型與編譯期多型優缺點分析

執行期多型優點

  • OO設計中重要的特性,對客觀世界直覺認識。
  • 能夠處理同一個繼承體系下的異質類集合。

執行期多型缺點

  • 執行期間進行虛擬函式繫結,提高了程式執行開銷。
  • 龐大的類繼承層次,對介面的修改易影響類繼承層次。
  • 由於虛擬函式在執行期在確定,所以編譯器無法對虛擬函式進行優化。
  • 虛表指標增大了物件體積,類也多了一張虛擬函式表,當然,這是理所應當值得付出的資源消耗,列為缺點有點勉強。

編譯期多型優點

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

編譯期多型缺點

  • 程式可讀性降低,程式碼除錯帶來困難。
  • 無法實現模板的分離編譯,當工程很大時,編譯時間不可小覷。
  • 無法處理異質物件集合。

關於顯式介面與隱式介面

所謂的顯式介面是指類繼承層次中定義的介面或是某個具體類提供的介面,總而言之,我們能夠在原始碼中找到這個介面.顯式介面以函式簽名為中心,例如

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

我們稱shout為一個顯式介面。在執行期多型中的介面皆為顯式介面。

而對模板引數而言,介面是隱式的,奠基於有效表示式。例如:

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

對於anim來說,必須支援哪一種介面,要由模板引數執行於anim身上的操作來決定,在上面這個例子中,T必須支援shout()操作,那麼shout就是T的一個隱式介面。

相關文章