第15章 繼承與多重繼承 15-1 繼承的基礎 15-2 覆寫與隱藏父類別的成員 15-3 子類別的建構與解構子 15-4 多重繼承 15-5 軟體工程與繼承 15-6 類別的型態轉換與檢查
15-1 繼承的基礎 15-1-1 類別繼承的基礎 15-1-2 實作繼承 15-1-3 父類別的存取控制
15-1-1 類別繼承的基礎-圖例 「繼承」(Inheritance)是物件導向程式設計的一種進階觀念,繼承就是物件的再利用,當定義好一個類別後,其他類別可以繼承這個類別的成員資料和函數。 類別繼承也是在模擬真實世界,例如:學生和老師都是人,我們可以先定義Person類別來模擬人類,然後擴充Person類別建立Student類別來模擬學生,如右圖所示:
15-1-1 類別繼承的基礎-圖例說明 Person類別是Student類別的「父類別」(Superclass)或「基礎類別」(Base Class),反之Student類別是Person類別的「子類別」(Subclass)或「延伸類別」(Derived Class)。 UML類別圖的繼承是使用空心的箭頭線來標示兩個類別間的關係。
15-1-1 類別繼承的基礎-類別架構 繼承不只可以多個子類別繼承同一個父類別,還可以擁有很多層的繼承,如果將整個類別關聯性(Relationships)的樹狀結構都繪出來,稱為「類別架構」(Class Hierarchy),如下圖所示:
15-1-1 類別繼承的基礎-兄弟類別 Truck、Car和Motorcycle類別是「兄弟類別」(Sibling Classes),因為擁有相同Vehicle父類別。當然我們可以繼續繼承類別Car,類別SportsCar和Jeep也是類別Vehicle的子類別,不過並不是直接繼承的子類別。 簡單的說,Car類別是SportsCar和Jeep的直接父類別(Direct Base Class),Vehicle類別則是SportsCar和Jeep的間接父類別(Indirect Base Class)。
15-1-2 實作繼承-父類別的宣告 父類別vehicle定義車輛的基本資料,如下所示: class vehicle { private: int engineNo; string owner; public: void setNumber(int no) { engineNo = no; } void setOwner(string owner) { this->owner = owner; } void printVehicle() { } };
15-1-2 實作繼承-子類別的宣告 (語法) 類別如果是繼承自存在的其他類別,其宣告語法,如下所示: class 子類別名稱 : 存取修飾子 父類別名稱 { // 擴充的成員資料和函數 }; 上述類別宣告使用「:」運算子後跟著父類別,表示擴充父類別的宣告,在父類別前方的存取修飾子可以定義繼承父類別的存取範圍,其值可以是private、protected和public,詳細說明請參閱下一節。
15-1-2 實作繼承-子類別的宣告 (範例) 因為車輛可以分成很多種,例如:卡車、機車和轎車等,以轎車car子類別為例的類別宣告,如下所示: class car : public vehicle { private: int doors; public: car(string owner, int no, int doors) { } void printCar() { } };
15-1-2 實作繼承-UML類別圖 UML類別圖,如下圖所示:
15-1-2 實作繼承-繼承的成員種類 在C++語言宣告的子類別可以繼承父類別的所有成員資料和函數,但是並不包含: 父類別的建構子和解構子。 父類別的朋友關係。 父類別的指定運算子=。
15-1-3 父類別的存取控制-說明 在C++語言的子類別使用哪一種存取控制來繼承父類別,將影響成員的存取範圍,如下所示: 父類別名稱 { private: …… protected: public: }; class 子類別名稱 : 存取修飾子 父類別名稱 {
15-1-3 父類別的存取控制-父類別的存取控制 在子類別是使用存取修飾子private、protected和public來繼承父類別。父類別各種存取控制的說明,如下表所示:
15-1-3 父類別的存取控制-子類別的存取控制 子類別是否能夠存取父類別指定區塊的成員函數和資料,需視它屬於類別的public、protected和private成員而定,筆者整理如下表所示:
15-2 覆寫與隱藏父類別的成員 15-2-1 覆寫父類別的成員函數 15-2-2 隱藏父類別的成員資料
15-2-1 覆寫父類別的成員函數-說明 在父類別的成員函數如果不符合需求,子類別可以宣告同名、同參數列和傳回值的函數來取代父類別的成員函數,稱為「覆寫」(Override)。
15-2-1 覆寫父類別的成員函數-父類別 在父類別vehicle擁有一個靜態成員函數和成員函數需要覆寫,如下所示: class vehicle { private: ……… public: static void showBrand() { } void printVehicle(int index) { } }; 上述showBrand()和printVehicle()是需要覆寫的成員函數。
15-2-1 覆寫父類別的成員函數-子類別 子類別car使用public繼承父類別vehicle,如下所示: class car : public vehicle { private: ……… public: static void showBrand() { } void printVehicle(int no) { } }; 在程式碼呼叫car物件的成員函數時,就是呼叫子類別car的函數,而不是父類別的同名函數。
15-2-2 隱藏父類別的成員資料-說明 除了父類別的成員函數外,子類別也可以隱藏父類別成員資料的變數,只需變數名稱相同,就算變數型態不同也一樣可以隱藏。
15-2-2 隱藏父類別的成員資料-父類別 例如:父類別vehicle的成員變數owner是宣告在public區塊的string字串型態,如下所示: class vehicle { private: ……… public: string owner; };
15-2-2 隱藏父類別的成員資料-子類別 在子類別car使用public存取修飾子繼承父類別vehicle,如下所示: class car : public vehicle { private: int owner; ……… public: }; car物件的成員變數owner是整數int,不再是string字串,原來父類別public區塊的owner成員變數被隱藏起來。
15-3 子類別的建構與解構子 15-3-1 子類別的建構與解構順序 15-3-2 子類別傳遞參數給父類別
15-3-1 子類別的建構與解構順序 當建立子類別的物件呼叫子類別的建構子時,它會先初始化父類別的成員,也就是呼叫父類別的建構子。如果子類別沒有建構子,在建立物件時,預設建構子(Default Constructor)就會呼叫父類別的預設建構子。 簡單的說,在呼叫子類別的建構子前,會先呼叫父類別的建構子,而解構子剛好與建構子是相反順序,也就是子類別的建構子是在父類別的建構子之前呼叫。
15-3-2 子類別傳遞參數給父類別 當父類別擁有建構子時,在子類別可以傳遞參數給父類別的建構子。例如:car類別是繼承自vehicle,此時car子類別的建構子,如下所示: car(int owner, int no, int doors) : vehicle(owner, no) { this->doors = doors; } 上述建構子的「:」運算子後是傳遞給父類別vehicle建構子的參數,如果父類別不只一個,請使用「,」號分隔。 其中傳遞給父類別參數的值,就是傳入子類別建構子的參數值,以此例,owner和no也是子類別建構子的參數。
15-4 多重繼承-說明 「多重繼承」(Multiple Inheritance)是指一個類別能夠繼承多個父類別,如下圖所示:
15-4 多重繼承-語法 多重繼承。其宣告語法如下所示: class 子類別名稱 : 存取修飾子 父類別名稱, 存取修飾子 父類別名稱….. { …… // 額外的成員資料和函數 }; 上述類別和繼承類別的語法相似,因為繼承類別不只一個,需要使用「,」逗號來分隔。
15-4 多重繼承-範例 例如:繼承自truck和driver類別的子類別driven_tuck,如下所示: class driven_truck : public truck, public driver { public: driven_truck(float pl, float fl, float pay, int pd): truck(pl, fl), driver( pay, pd) { } float costPerTom( float cost_of_gas ) { } void printData() { } };
15-5 軟體工程與繼承-圖例 軟體工程的繼承是類別關聯性(Relationships),它是指不同類別間的關係。例如:繼承是Is-a類別關聯性,稱為一般關係(Gereralization)。成品和零件(Whole-Part)的類別關聯性,即Part-of和Has-a關係,如下圖所示:
15-5 軟體工程與繼承-圖例說明 Part-of和Has-a關係的說明,如下所示: Part-of關係:指此類別是其他類別的零件,以上圖為例Wheel車輪和Door車門是Car車類別的零件。 Has-a關係:相反於Part-of關係,Car類別Has-a擁有Wheel和Door類別。 在UML的上述關係稱為「聚合關係」(Aggregation),或是另一種更強調Whole-part關係稱為「組成關係」(Composition)。
15-6 類別的型態轉換與檢查-說明 在ANSI-C++定義reinterpret_cast、static_cast、dynamic_cast和const_cast四種新的型態轉換運算子,可以使用在類別型態的轉換,其基本語法如下所示: static_cast<類別*>(運算式); const_cast<類別*>(運算式); dynamic_cast<類別*>(運算式); reinterpret_cast<類別*>(運算式); ANSI-C++還定義全新typeid運算子,可以檢查指定運算式或類別的型態,其語法如下所示: typeid(運算式)
15-6 類別的型態轉換與檢查-static_cast運算子 C++的static_cast運算子可以將類別型態指標轉換成其他類別型態的指標,只需是編譯程式能夠自動轉型的型態迫換,都可以使用static_cast運算子來明確表示所需的型態轉換,如下所示: class A {}; class B: public A {}; A *a = new A; B *b = static_cast<B*>(a); 上述程式碼使用static_cast<B*>運算子,將父類別A物件指標a,型態轉換成子類別B的物件指標b。
15-6 類別的型態轉換與檢查-const_cast運算子 C++的const_cast運算子可以取消常數物件指標成為一般物件指標,如下所示: class C {}; const C *c1 = new C; C *c2 = const_cast<C*>(c1); 上述程式碼使用const_cast<C*>運算子,將常數物件指標c1型態轉換成非常數物件指標c2。
15-6 類別的型態轉換與檢查-dynamic_cast運算子 C++的dynamic_cast運算子主要是在類別架構中,安全的進行型態轉換,static_cast和dynamic_cast運算子的差異,在於dynamic_cast會進行檢查,這是在執行期檢查型態轉換的指標是否指向合法的需求型態,如果不是,就傳回NULL指標,如下所示: class D { virtual void dummy() {}; }; class E: public D {}; D *d1 = new E; D *d2 = new D; E *e1 = dynamic_cast<E*>(d1); E *e2 = dynamic_cast<E*>(d2); // 傳回NULL指標
15-6 類別的型態轉換與檢查-reinterpret_cast運算子 C++的reinterpret_cast運算子可以在完全無關的類別型態間進行轉換,其操作是二進位複製,從一個指標複製至另一個指標,如下所示: class F {}; class G {}; F *f = new F; G *g = reinterpret_cast<G*>(f); 上述程式碼使用reinterpret_cast<G*>運算子,將類別F物件指標f,型態轉換成類別G的物件指標g。
15-6 類別的型態轉換與檢查- typeid運算子 C++的typeid運算子可以在執行期傳回運算式的型態資訊,我們可以使用關係運算子「==」和「!=」來比較其傳回值,如下所示: class H {}; H *h1; H h2; if ( typeid(h1) != typeid(h2) ) { cout << "h1與h2型態不同\n"; } 上述程式碼使用typeid運算子檢查物件變數h2和物件指標變數h1是否是相同型態。