2024-05-19|閱讀時間 ‧ 約 27 分鐘

物件導向中的多型、繼承概念與應用

物件導向(Object-Oriented Programming,OOP) 可以用來提高程式碼的可讀性、可維護性和可擴展性,同時還能夠促進程式碼的重用和組織。

多型 (Polymorphism)

以挖礦為例,當挖掘鑽石礦時,會獲得鑽石等物品。我們可以定義一個統一的礦物基類,並通過覆蓋其方法來確保挖掘特定礦物時會掉落相應的物品。這比傳統的 if-else 判斷方式更為簡潔,只需判斷挖掘的對象是否為礦物,無需關注具體的礦物類型。這樣的多型實現使程式碼更具靈活性和可擴展性。

多型與指標密切相關。通過改變指標來決定當前的礦物類型,指向不同的具體礦物類別(如鑽石礦或金礦),從而實現多型。利用 virtual 和 override 關鍵字,我們可以動態地選擇合適的函數實現。

純虛擬函數 (Pure Virtual Function)

在基類中聲明純虛擬函數,要求所有子類必須提供具體實現。這樣可以確保每個子類都有自己專屬的行為。

物件轉型 (Object Casting)

將衍生類別轉型為基類別是多型的一部分。我們可以利用基類別的指標操作不同類型的物件,實現通用處理。例如,一個人挖礦時,無論挖掘的是何種礦物,都會掉落相應的物品。這種方法可以省去大量的 if-else 判斷邏輯。

為什麼需要物件轉型?

物件轉型可以方便地替換為我們「想要的類型」,提高程式碼的靈活性和可重用性。

物件轉型的問題

如果將子類別轉型為基類別,而基類別中沒有相關的定義,則無法正常運行。

虛擬函數與覆蓋 (Virtual and Override)

如果基類中定義了 virtual 函數,而子類中對其進行了覆蓋(override),編譯器會在運行時自動選擇合適的子類實現。

不加 virtual 和 override 的情況

如果不使用 virtual 和 override,嘗試通過基類指標調用子類中的函數時,會出現調用基類函數的情況,導致返回不正確的結果。


提早綁定與延遲綁定 (Early Binding and Late Binding)

提早綁定

在編譯時期進行綁定,將所有函數綁定在一起。

延遲綁定

在運行時期進行綁定,允許程式在執行過程中動態綁定需要的函數定義。

沒有 virtual 的情況

使用提早綁定,直接在編譯時期綁定基類的定義。

加了 virtual 的情況

編譯器會使用延遲綁定,在運行時期動態尋找函數的定義,從而調用子類的相關函數實現。


虛擬解構函數 (Virtual Destructor)

對於具有衍生類別的物件,解構順序是從最底層子類的解構子開始,逐步釋放到最上層基類的解構子。這樣可以確保所有資源都能正確釋放,避免內存泄漏。

包含 (Inclusion)

以熔爐為例,減少同樣函數原型、不同類別、不同實作細節所造成的特殊判斷情況。例如,不同的礦物可以逐一燒成錠,這樣可以減少冗長的判斷邏輯。

問題

如果有多種類型的礦物,需要為每一種礦物編寫對應的處理邏輯。

解決方法

利用繼承和多型,只要繼承自礦物基類,並實現相應的燒製方法,就能大大減少重複的實現細節。


函數匹配與函數重載 (Function Matching and Function Overloading)

函數匹配

針對同名但參數不同的函數,C++ 會自動選擇對應的函數實現。例如,AddOre() 函數可以有不同的參數個數和類型,C++ 會自動匹配合適的函數。

函數重載

相同名稱但參數不同的函數。函數重載與函數匹配實現了相同的功能,使得同一個操作可以對多種類型的數據進行處理。

運算符重載 (Operator Overloading)

可以重載 C++ 內建的運算符,使得物件之間可以進行相加、相減等運算。例如,玩家的移動可以通過位置(三維空間的座標 x, y, z)的相加來實現。


類別模板 (Class Template)

使用模板讓類別使用通用型別,並將類別中通用型別變成指定型別。這樣可以避免為每種礦物創建不同的類別,而是使用模板來實現通用化。

模板綁定

通過提早綁定,在編譯時期自動綁定傳入型別的函數。


函數模板 (Function Template)

對特定函數使用通用型別 (Generic),使得函數可以處理不同型別的數據。

使用時機

根據 StackOverflow 上的專家回答,以下情況應該使用類別模板:

  • 如果某個成員的型態是 T。
  • 如果某個虛擬函數需要型別是 T。

鴨子類型 (Duck Typing)

不需要關心物件是否真的是礦物,只要能燒就可以處理。例如,木頭可以像礦物一樣燒,只要滿足最低需求即可運行。這種方式讓程式碼更具彈性。

最小需求

只要編譯能通過,無論是否符合語義,只要符合規則即可運行。


多型結論

通過抽象具體型別,變成通用處理,解決程式碼冗長的問題。

包含 (Inclusion)

減少同樣函數原型、不同類別、不同實作細節所造成的特殊判斷情況。

函數重載 (Function Overload)

相同名稱但不同參數的函數。

模板 (Template)

將不同型別的部分抽象成模板,根據使用者傳入的型別進行替換。


組合與全釋放 (Composition and Aggregation)

成員內具有其他類別成員,這個類別擁有「類別成員」的所有權。建構子或 Setter 中設定這些成員。例如,不同的食材組合可以創造不同的炒飯。當物件被解構時,所有成員都會被釋放。

組合的優點

  1. 維護方便。
  2. 新增道具簡單直觀。
  3. 擴展性好,只需新增類別即可。

組合的解構

成員內的所有成員都會被釋放。由於物件擁有其成員,當物件被解構時,其成員也應一同被釋放。


物件委派與聚合 (Delegation and Aggregation)

物件委派

組合可以實現類似「委派」的功能。例如,將裝備丟到附魔台中進行附魔。

物件的聚合

組合內的成員不會全部釋放。玩家有附魔台,裝備丟進去附魔,但附魔台消失時,裝備不會消失。委派是附魔的動作,組合是將附魔台和裝備結合。

介面

通過純虛擬函數和繼承實現「介面」,讓所有會攻擊玩家的對象都繼承一個介面,並讓子類實現細節。


耦合 (Coupling)

描述改變某些東西時造成的影響程度。

高耦合度

程式碼重複度低,但繼承鏈很長。

低耦合度

程式碼會重複,但繼承鏈短,維護方便。


組合與介面解決繼承問題

以怪物繼承為例,將重複的程式碼提到父類以提升程式碼整潔度,但容易增加耦合度。

傳統的類別繼承

將重複的程式碼提到父類以提升程式碼整潔度,但容易增加耦合度,導致維護困難。

新的繼承方式

不繼承 Mob,而是將 Mob 當成類別成員。初始化這些類別後,依然保有 Mob 的特性,通過組合與介面取代繼承類別。


The Diamond Problem

地獄幽靈和終界龍都繼承 Mob 的成員和函數,可能導致編譯時期的重複。可以利用組合與介面解決這個問題。

依賴注入 (Dependency Injection)

在建構子中將類別作為參數,根據傳入的物件做出對應行為。例如,熔爐丟肉燒會飄香,丟礦石燒得較慢。

結論

組合與介面解決繼承問題,扁平化繼承鏈,降低耦合度,更易於維護。

分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.