[筆記]多型與繼承的關係|C++

多型的出現是為了要解決什麼問題?多型觸發的條件是什麼?為什麼多型要和繼承一起使用?多型能保有繼承的優點嗎?多型比繼承又多了什麼功能?特殊函式也能多型嗎?

還是很困惑嗎?文章裡有答案喔~😎

一、多型 (polymorphism)

多型想要解決C語言switch的問題,虛擬函式 (virtual functions) + 繼承 (inheritance) + override可以實做多型。override的前提是有多個虛擬函式簽章 (signature)一樣,才會是override,如果不是虛擬函式,則會變成redefine。另一個相近的概念overload則是要在函式名一樣,但簽章不一樣的狀況。

Same function name

Same signature

Virtual

Override

Non-virtual

Redefine

Different signature

Non-virtual

Overload

(一)純虛擬函式 (pure virtual functions)

純虛擬函式 (=0)在基礎類別中,不需要先給予實作內容,等到衍生類別才需要實作內容,並在後面宣告override,因為有時候我無法先給它定義;而如果是一般的虛擬函式 ,基礎類別就必須給出實作細節。

只要函式被宣告為virtual,以後的子類別的此函式都是虛擬的,即使子類別沒有顯示宣告virtual。也就是一日virtual,終生virtual。不過為了增加可讀性,建議還是都要寫virtual。

任何類別中如果有一個以上的純虛擬函式,就是扮演抽象類別 (abstract class)。抽象類別無法被直接實例化,除非能補足抽象類別所缺的實作,故抽象類別只能當作介面 (interface)的角色。直接實例化抽象類別會導致不完整的實例而失敗,但依舊可以使用抽象類別作為parameter type / return type / data member type,因為使用者會賦值 (assign)衍生類別物件給這些抽象類別,以補足抽象類別所缺乏的實作

(二)多型與繼承關係

多型要和繼承一起使用,因為透過繼承,不同函式的scope才會一樣,scope一樣才能override。因此如果兩個沒有繼承關係的類別,有著同樣函式簽章的虛擬函式,兩者是不會有多型關係,顯然多行和繼承的關係十分密切。總之多型需要和繼承與虛擬函式一起使用,才會有多型的的效果。

1. 多型仍保有繼承優點

繼承關係:基礎類別 (b) <----- 衍生類別1 (d1) <------- 衍生類別2 (d2) <---------- 衍生類別3 (d3)

如果基礎類別中宣告一個虛擬函式(vf),基礎類別已經有給予定義,而d1已經override,d2也已經override,則這3個類別的vf是獨立的。所以如果我想在d2中重複使用d1的vf是可以的,只要使用::去明示scope即可,如:d1::vf。而如果d3沒有override,則d3的vf是繼承d2::vf,故如果沒有override就繼承上面有定義的定義。因此,在多型中,要麻繼承親代定義,要麻override重新定義。即使是override虛擬函式,仍保有繼承重複使用程式碼的特性,只不過使用上要使用base::virtual_function,已表明我是呼叫基礎的虛擬函式,而不是呼叫自己的虛擬函式。

2. 多型的功用

多型

所以多型的override有什麼的功能呢?顯然不是只有單純重新定義虛擬函數,不然這就和redefine沒差別。它的目的是為了處理C switch的問題,如果以C switch去模擬上圖的功能,我們會用一個變數Animal存放Dog, Cat or Rat,並且用switch去偵測Animal的值是哪一個?如果是狗 Dog,則印出coin值;如果是貓 Cat,則印出luck和rainbow值;如果是鼠 Rat,則印出good和shock值。

顯然C switch的方法如果種類更多時,會很難維護。而多型也可以來實作同樣的功能,同時在維護上更方便。此時會有一個基礎類別寵物 (Pet),而旗下繼承者有狗 (Dog)、貓 (Cat)、鼠 (Rat),且每個類別都有虛擬函式getInfo(),getInfo()會印出各別獨有的成員資料。因此getInfoe()有四種版本,分別為Pet::getInfo()、Dog::getInfo()、Cat::getInfo()、Rat::getInfo()。

  • Pet *pet;
  • pet = new Pet(); pet.getInfo() == pet.Pet::getInfo()
  • pet = new Dog(); pet.getInfo()== pet.Dog::getInfo(),印出coin值
  • pet = new Cat(); pet.getInfo() == pet.Cat::getInfo(),印出luck和rainbow值
  • pet = new Rat(); pet.getInfo() == pet.Rat::getInfo(),印出good和shock值

我可以建構一個指向寵物的指標 (pet),並且分別配置狗物件、貓物件、鼠物件,在呼叫pet.getInf(),神奇的是居然不是呼叫到基礎版本Pet::getInf(),而是呼叫到衍生類別的版本。這方法有助於,如果今天我不確定pet會配置到哪個衍生類別時,但又想要呼叫衍生類別的getInfo(),這時多型就非常有用。

多型可以讓基礎類別,向下存取衍生類別的虛擬函式;而繼承可以讓衍生類別,向上存取基礎類別的函式。不過多型向下存取的前提是,基礎類別要配置衍生類別物件,因此基礎類別物件是不可能靠多型向下存取。上面pet的例子,即使pet配置狗物件,pet本身仍是Pet,Pet也無法存取Dog非虛擬函式。

3. 成員函式使用多型

(1) 不使用多型

class A{

public:

    void print(){printC();}

    void printC() {cout << "AA" << endl;}

};

 

class B : public A{

public:

    void print(){

        A::print();

        cout << "BBBBB" << endl;

    }

    void printC()  {cout << "CCCCC" << endl;}

};

 

 

class C : public A{

public:

    void print(){

        A::print();

        cout << "BBBBB" << endl;

    }

    void printC()  {cout << "DDDD" << endl;}

};

(2) 使用多型

class A{

public:

    void print(){printC();}

    virtual void printC() {cout << "AA" << endl;}

};

 

class B : public A{

public:

    void print(){

        A::print();

        cout << "BBBBB" << endl;

    }

    void printC() override {cout << "CCCCC" << endl;}

};

 

 

class C : public A{

public:

    void print(){

        A::print();

        cout << "BBBBB" << endl;

    }

    void printC() override {cout << "DDDD" << endl;}

};

(3) man()函式

int main() {

    A a;

    a.print();

    cout << "-------" << endl;

    B b;

    b.print();

    cout << "-------" << endl;

    C c;

    c.print();

    return 0;

}

(4) 結果

void printC()

virtual void printC()

AA

-------

AA

BBBBB

-------

AA

BBBBB

AA

-------

CCCCC

BBBBB

-------

DDDD

BBBBB

不用多型的結果:因為printC()沒使用多型,所以B和C在重複使用A::print()時,A::print()裡的printC()一樣是A::printC(),即使B和C有redefinition printC(),A仍無法存取B和C的printC(),只能存取A::printC()。

用多型的結果:因為printC()有使用多型,所以B和C在重複使用A::print()時,A::print()裡的printC()會判斷是A::printC(), B::printC(), C::printC()。如果是B呼叫A::print(),則printC()為B::printC()。

除非在A::print()裡的printC()寫成A::printC(),則即使使用多型,也只會呼叫A::printC()。

3. 多型耗時

多型這麼棒,為什麼不預設為虛擬函式?

  1. 使用多型很耗時間,故在C++中必須顯式宣告virtual,而在JAVA中,預設所有函式成員都是virtual,所以JAVA比較耗時。
  2. 如果C++預設函式為虛擬函式,C與C++在資料型態上會有不相容的問題。

(三)虛擬解構子 (virtual destructor)

有個情境一定要使用虛擬解構子:

  • 有一個沒有虛擬解構子的基礎類別
  • 有一個繼承它的衍生類別
  • 有一個基礎類別的指標指向衍生類別
  • 例如:Pet *pet; pet = new Dog(); delete pet;

以上述Pet–Dog的例子,如果Pet::~Pet()不是virtual,則當執行delete pet時,編譯器只會呼叫Pet::~Pet(),這樣會造成記憶體遺失,因為Pet沒權利存取Dog的解構子;而如果Pet::~Pet()是virtual,執行delete pDog時,編譯器會先呼叫Pet::~Dog()再呼叫Pet::~Pet()。

編譯器會自動呼叫兩次的原因是,雖然解構子不會被繼承,但也不用在Dog::~Dog()實作中呼叫Pet:: ~Pet(),因為編譯器會自動呼叫。

虛擬解構子不能是純虛擬解構子,也就是不能宣告為=0,因為這樣base::destructor會沒有定義!故虛擬解構子只能為一般虛擬函式,虛擬解構子本體可以為空,因為編譯器會自動建立,故還是有定義。

所以結論是,如果類別中有使用到虛擬函式,建議再加上虛擬解構子,以避免記憶體遺漏。然後沒有虛擬建構子這玩意!

二、相關文章

留言

  1. 謝謝您的文章,惟讀者發現幾處有錯字,建請修正。
    1. 「(二)多型與繼承關係」這段有一處「多型」誤繕為「多行」
    2. 「2. 多型的功用」這段有多處「pet.getInfo()」誤繕為「pet.getInf()」或「pet.getInfoe()」

    回覆刪除

張貼留言