close
三、物件導向
以物件為基礎作延伸,多型(Polymorphism)操作為設計時的核心,於執行時期動態繫結(Dynamic binding)以決定物件的行為。
繼承(Inheritance)
您可以繼承某個(父)類別的定義並加以擴充,而制訂出一個新的(子)類別定義。

多型(Polymorphism)
多型操作在物件導向設計中伴演著極重要的角色,使用的適當與否,足以影響程式的架構與未來的可維護性。



公開(public)繼承
假設您先前已經撰寫了一些2D繪圖相關類別,現在打算將之擴充為適用於3D繪圖,3D的觀念是2D的延伸,許多2D繪圖時所使用的功能都可以留下來加以擴充,在C++中您不用重寫所有的類別,您可以「繼承」(Inherit)原先已定義好的類別,然後加以擴充。

例如繪圖中最基本的點類別,您原先已定義好一個Point2D類別,您繼承它並將之擴充為Point3D類別,在繼承的關係中,稱被繼承的類別為「父類別」(Parent class)或「基礎類別」(Base class),而繼承父類別的就稱之為「子類別」(Child class)或「衍生類別」(Derived class),在繼承時您使用 : 運算子,並指定其繼承方式,在繼承的權限關係上,公開繼承是最常見的,先由這個開始說明繼承的概念。

當您公開繼承某個類別時,該成員的所有public成員都可以在衍生類別中被呼叫使用,而private成員則不可以直接在衍生類別中被呼叫使用;在這個例子中,Point2D中已經有_x, _y兩個成員,而繼承之後新增_z成員,在函式上您新增public的z()方法,而x()與y()直接繼承父類別中已定義的內容。

在繼承某個類別之後,您可以一併初始父類別的建構函式,以完成相對應的初始動作,這可以使用 : 運算子並加上父類別建構函式名稱,至於無參數的預設建構函式,在生成衍生類別物件時,基底類別的無參數建構函式會自動被呼叫。

父類別的public成員可以直接在衍生類別中使用,而private成員則不行,private類別只限於定義它的類別來存取,如果您想要與父類別的 private成員溝通,就只能透過父類別中繼承下來的public函式成員。

公開繼承時,父類別與子類別之間是"is-a"的關係,白話來說,假設父類別是A類的話,公開繼承的子類也「是一種」A 類。



受保護的(protected)成員
在之前的範例中,類別的資料成員多設定為private成員,也就是私用成員,私用成員只能在類別物件中使用,不能直接透過物件來呼叫使用,而即使是繼承了該類別的衍生類別也是如此,您只能透過該類別所提供的public函式成員來呼叫或設定私用成員。

然而有些時候,您希望繼承了基底類別的衍生類別,能夠直接存取呼叫基底類別中的成員,但不是透過public函式成員,也不是將它宣告為public,因為您仍不希望這些成員被物件直接呼叫使用。

您可以宣告這些成員為「受保護的成員」(protected member),保護的意思表示存取它有條件限制以保護該成員,當您將類別成員宣告為受保護的成員之後,繼承它的類別就可以直接使用這些成員,但這些成員仍然受到類別的保護,不可被物件直接呼叫使用。

要宣告一個成員成為受保護的成員,就使用"protected"關鍵字,並將成員宣告於它的下方。

直接使用繼承下來的受保護成員確實比較方便,函式成員也可以宣告為受保護的成員,對象通常是僅適用於類別中使用的一些內部處理函式,這些函式對類別外部來說,可能是呼叫它並沒有意義或是有危險性,但您在衍生類別中仍可能使用到這些函式,所以通常會將之宣告為受保護的成員。



成員函式的重新定義
如果基底類別中定義了一個函式,當您繼承了基底類別之後,您可以「重新定義」(Override)這個函式,以讓它適用於衍生類別。

當同一個成員函式在衍生類別中被重新定義,使用此衍生類別所生成的物件來呼叫該函式,所執行的會是衍生類別中所定義的函式,而基底類別中的同名函式並不受影響。

如果在重新定義某個函式,打算先執行父類別中該函式的內容,則可以使用this關鍵字結合::運算子,指定父類別中該函式來執行



繼承後的建構函式與解構函式
若您繼承了某個類別之後,當您在生成衍生類別的物件時若不指定參數,無參數的預設建構子會被執行,而基礎類別的無參數預設建構子也會被執行,所以基於這種特性,通常預設建構子中會撰寫一些通用的成員狀態初始,例如設定一些預設值。

如果繼承之後,您要使用衍生類別生成物件,在生成物件時指定參數,並同時執行基底類別中的某個參數建構子,您可以使用 : 運算子。

如果您使用衍生類別生成物件,則建構函式的執行順序會從基底類別的建構函式開始執行起,這是可以理解的,因為基底類別是衍生類別的基礎,一些基礎的參數或初始狀態必須先完成,再來再完成衍生類別中的建構函式。

而在物件被消滅時,解構函式的執行順序則正好相反,是從衍生類別的解構函式開始執行,再來才是基礎類別的建構函式,因為若基底類別的解構函式如果先執行,則衍生類別相依於基底類別的一些狀態也會被解構(例如指標),則此時再行衍生類別的解構函式,將存在著相依問題而造成錯誤。



保護(protected)繼承、私用(private)繼承
在繼承時採公開(public)繼承的方式來繼承一個類別時,父類別與子類別為"is-a"的關係,子類別繼承父類別的公開(public)介面及受保護(protected)的成員,子類別是父類別的細化型態。

保護(protected)繼承可以改變繼承下來的基底類別成員權限,保護的意思就是讓這些成員繼承下來之後,保護它們僅能在類別與衍生類別中使用。

保護繼承時使用protected來繼承基底類別,繼承下來的成員在衍生類別中的權限會跟著改變。
簡單的說,原來的權限在protected以下的保留其原來權限,而在protected以上的就降為protected,子類別protected繼承的目的在只希望保留父類別中已實作的公開成員與受保護的成員為己用或接下來的衍生類別使用,並提供自己的公開介面。

您也可以在繼承基底類別之後,將它所有的成員一律改為私用(private),使用私用(private)繼承可以達到這個目的。

private繼承被稱為「實作繼承」,意味著子類別只想保留父類別中已實作的公開與受保護的成員為己用,並提供自己的公開介面與接下來會被繼承的受保護的成員。



虛擬函式(Virtual function)
之前曾經介紹過函式與運算子的重載(Overload),重載可以使用一個函式名稱來執行不同的實作,這是一種「編譯時期」就需決定的方式,這是「早期繫結」(Early binding)、「靜態繫結」(Static binding),因為在編譯時就可以決定函式的呼叫對象,它們的呼叫位址在編譯時就可以得知。

「虛擬函式」(Virtual function)可以實現「執行時期」的多型支援,是一個「晚期繫結」(Late binding)、「動態繫結」(Dynamic binding),也就是指必須在執行時期才會得知所要調用的物件或其上的公開介面。

在談虛擬函式之前必須先知道,一個基底類別的物件指標,可以用來指向其衍生類別物件而不會發生錯誤,多型與動態繫結的基礎從這開始,它們只有在使用指標或參考時才得以發揮它們的特性。

注意將衍生類別型態的指標指向基底類別的物件基本是不可行的(雖然可以使用型態轉換的方式來勉強達成,但並不鼓勵),衍生類別的指標並不能存取基底類別的成員。

虛擬函式是一種成員函式,它在基底類別中使用關鍵字"virtual"宣告(定義),並在衍生類別中重新定義虛擬函式,這將成員函式的操作決議(Resolution)推遲至執行時期再決定。

虛擬函式可以實現執行時期的「多型」,也就是「一個介面,多種函式」,一個含有虛擬函式的類別被稱為「多型的類別」(Polymorphic class),當一個基底類別型態的指標指向一個含有虛擬函式的衍生類別,您就可以使用這個指標來存取衍生類別中的虛擬函式。

衍生類別中重新定義虛擬函式時,virtual可以根據需求加上,如果再接下來的衍生類別仍想進行多型操作,則加上virtual,如果不打算進行多型操作,則可以不加上。



純虛擬函式、抽象類別(Abstract class)
C++預設函式成員都不是虛擬函式,如果要將某個函式成員宣告為虛擬函式,則要加上"virtual"關鍵字,然而C++提供一種語法定義「純虛擬函式」(Pure virtual function),指明某個函式只是提供一個介面,要求繼承的子類別必須重新定義該函式,定義純虛擬函式除了使用關鍵字"virtual"之外,要在函式定義之後緊跟著'='並加上一個0。

一個類別中如果含有純虛擬函式,則該類別為一「抽象類別」(Abstract class),該類別只能被繼承,而不能用來直接生成實例,如果試圖使用一個抽象類別來生成實例,則會發生編譯錯誤。



執行時期型態資訊(RTTI)
在C++這種可以進行多型機制的語言中,您可以將基底類別指標指向衍生類別物件,這種指定通常在執行時期後發生,您並無法在編譯時期即得知指標所指向的物件型態,而必須在執行時期取得物件的執行時期資訊。

RTTI 全名為Run-Time Type Information,也有人作Run-Time Type Identification,C++中用來取得指標或參考所實際代表的物件,您可以使用typeid()來取得物件於執行時期的資訊,要使用 typeid(),您必須包括標頭檔,typeid()使用時傳入一個物件:
typeid(object);

typeid()會傳回一個type_info物件,其擁有幾個成員可以描述或進行物件的比較:
const char *name(); // 取得物件型態名稱
bool before(const type_info &ob); // 當物件的名稱順序位於ob之前時,傳回true
bool operator==(const type_info &ob); // 比較物件型態是否相同
bool operator!=(const type_info &ob); // 比較物件型態是否不同


==與!=運算子在這邊被重載為可以比較兩個物件的型態是否相同;typeid()也可以使用型態名稱作為引數,這通常是用來取得一個type-info 物件,並與一個物件作比較時使用:
typeid(type-name);

使用共同的基底類別指標來指向基底類別物件與衍生類別物件,雖然如此,還是利用typeid()取回的type-info物件仍可以得知物件的型態名稱。

RTTI的使用時機之一,就是當您將物件以參考方式傳遞給函式時,函式的參數使用共同的基底類別指標或參考,但在函式中有必須操作衍生類別中的某個方法,由於函式事先並不知道您傳入的物件型態名稱,所以您必須利用RTTI來進行判斷。

傳統的C風格轉型語法也是可以使用的,例如:
Derived1 *derived1 = (Derived1*) base;
當然不建議使用這種方式強制轉型,事實上使用static_cast也不是很適合,C++為了支援RTTI還提供有dynamic_cast,這在下一個主題中介紹。



使用 dynamic_cast
您可以使用static_cast(甚至是傳統的C轉型方式)將基底類別指標轉換為衍生類別指標,這種轉型方式是強制轉型,在執行時期使用強制轉型有危險性,因為編譯器無法得知轉型是否正確,執行時期真正指向的物件型態是未知的,透過簡單的檢查是避免錯誤的一種方式:
if(typeid(*base) == typeid(Derived1)) {
Derived1 *derived1 = static_cast(base);
derived1->showOne();
}


為了支援執行時期的型態轉換動作,C++提供了dynamic_cast用來將一個基底類別的指標轉型至衍生類別指標,稱之為「安全向下轉型」(Safe downcasting),它在執行時期進行型態轉換動作,首先會確定轉換目標與來源是否屬同一個類別階層,接著才真正進行轉換的動作,檢驗動作在執行時期完成,如果是一個指標,則轉換成功時傳回位址,失敗的話會傳回0,如果是參考的話,轉換失敗會丟出 bad_cast例外。

由於dynamic_cast轉換失敗的話會傳回0,因而運算的結果不可使用,必須先行對轉換結果作檢查才可以。

如果使用參考的話,dynamic_cast在轉換失敗之後會丟出bad_cast例外,所以您必須使用try...catch來處理例外。



多重繼承(一)
C++允許讓衍生類別同時直接繼承兩個以上的父類別,例如有A、B兩個類別,您可以使用下面的方式來讓C類別直接繼承這兩個類別。

多重繼承之後的建構函式執行順序,是依您撰寫程式時的順序由左而右決定,最後才是繼承後衍生類別,例如上面的例子中,會先執行A類別定義的建構函式,然後再執行B類別的建構函式,最後執行C類別的建構函式,而解構函式的執行則正好相反,先執行衍生類別的解構函式,然後再右向左執行。



多重繼承(二)
多重繼承時通常其中一個基底類別作為private實作體,而其它的用以表現完全的抽象介面。

考慮您有一個方法doRequest(),您事先並無法知道什麼型態的物件會被傳進來,或者是這個方法可以接受任何類型的物件,您想要操作物件上的某個特定方法,例如doSomething()方法,問題是傳進來的物件是任意的,除非您定義一個抽象類別並宣告doSomething()抽象方法,然後讓所有的類別都繼承這個抽象類別,否則的話您的doRequest()方法似乎無法實作出來,實際上這麼作也沒有價值。

您可以藉由多重繼承來解決這個問題,例如先定義一個抽象類別:
#ifndef IREQUEST
#define IREQUEST

class IRequest {
public:
virtual void execute() = 0;
};

#endif


假設您的類別是以下的SomeObject類別的子類別:
#ifndef SOMEOBJECT
#define SOMEOBJECT

#include
using namespace std;

class SomeObject {
public:
virtual void someFunction() {
cout }

private:
void otherFunction() {
cout }
};

#endif


為了滿足之前所提供的doRequest()的需求,您讓衍生類別同時繼承IRequest與SomeObject類別,例如:
#include "IRequest.h"
#include "SomeObject.h"
#include
using namespace std;

class Welcome : public SomeObject, public IRequest {
public:
void execute() {
cout }
};


#include "IRequest.h"
#include "SomeObject.h"
#include
using namespace std;

class Hello : public SomeObject, public IRequest {
public:
void execute() {
cout }
};


假設您設計了一個doRequest()方法,雖然不知道Hello與 Welcome是什麼型態,但它們都繼承了IRequest,所以doRequest()只要知道IRequest定義了什麼公開介面,就可以操作Hello與Welcome的實例,而不用知道傳入的物件到底是什麼類別的實例,使用下面這個程式來測試:
#include
#include "IRequest.h"
#include "Welcome.h"
#include "Hello.h"
using namespace std;

void doRequest(IRequest *request) {
request->execute();
}

int main() {
Welcome welcome;
Hello hello;

doRequest(&welcome);
doRequest(&hello);

return 0;
}




虛擬繼承(Virtual Inheritance)
多重繼承時,會有一種模擬兩可的情況,就是當兩個類別都繼承同一個基底類別,而這兩個類別又同時被另一個類別,以平行多重繼承的方式同時繼承。

此時,C類別將會擁有兩個A類別的複本,一個來自B1所繼承下來的,一個來自B2所繼承下來的,那麼C類別到底要用B1所繼承下來的?還是B2所繼承下來的?您可以使用「虛擬繼承」(Virtual Inheritance)來解決這個問題。

虛擬繼承是在繼承基底類別時使用"virtual"關鍵字。
如此B1與B2以虛擬繼承的方式繼承了A類別,這個好處是當有類別多重繼承了某個基底類別時,在該類別中將會只有一個基底類別存在,而不會有多個複本,例如在上例中,類別C中將只會有一個基底類別A的存在。
arrow
arrow
    全站熱搜
    創作者介紹

    silverfoxkkk 發表在 痞客邦 留言(0) 人氣()