追蹤

宗宗大學

喜歡關注科學、哲學、社會、政治、教育,未來想當一位抱著理想的科學家,期許社會能夠進步。

[筆記]多型與繼承的關係|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. 物件導向程式設計|模擬真實世界的方式|江明朝
  2. AOOP Homework source code
  3. [筆記]109-2高等物件導向程式設計 期中考
  4. [筆記]介面與實作、運算子多載、左值右值、參數傳遞、回傳多值|C++
  5. [筆記]陣列與指標|C++
  6. [筆記]類別、特殊函式、內嵌函式、函式物件|C++
  7. [筆記]繼承模式與存取權限|C++
  8. [筆記]多型與繼承的關係|C++
  9. [筆記]static / const成員資料與函式|C++
  10. [筆記]夥伴函式與類別、不夠朋友問題|C++

本文章發表於:課程

加入85

宗宗大學

國立中山大學 生物科學系

追蹤 384 鼓勵作者

喜歡關注科學、哲學、社會、政治、教育,未來想當一位抱著理想的科學家,期許社會能夠進步。

鼓勵作者

目前持有 Blink Coin: Loading..

選擇禮物


愛心

(Coin 10)

幫高調

(Coin 20)

咖啡

(Coin 30)

掌聲鼓勵

(Coin 40)

崇拜眼神

(Coin 50)

驚呆了

(Coin 60)

神人4ni

(Coin 70)

花束

(Coin 100)

鑽石

(Coin 300)

紅寶石

(Coin 500)

藍寶石

(Coin 1000)

黃寶石

(Coin 3000)


送出鼓勵



發表匿名文章不會出現你的大頭圖與名稱,你可暢所欲言,但文章內容務必遵守「佈告欄使用規範」!


回應

送出回應


想回應這篇文章嗎?也想發表文章嗎?
馬上登入來發表文章、追蹤作者、收藏文章或回應文章吧!

註冊 登入