十一 組合和繼承
OOD(Object Oriented Design)
(1)基于對象:單一類的寫法;
(2)面向對象:類與類之間的關系,包含三種:Composition(復合)、Delegation(委托)、 Inheritance(繼承)。
11.1 Composition表示has-a
如上圖所示,queue類中有deque類,稱為queue has-a deque即類的復合。
這也是一種設計模式Adapter,queue類擁有deque類,而且queue所有的函數功能都借用deque類來實現。deque功能非常全,queue根據用戶開放部分的功能,queue是一種adapter。
這是復合在內存中的表現,queue類有deque類,deque類有Itr類,sizeof的計算如圖所示:
類之間在復合關系下構造函數與析構函數的順序為:
(1)構造由內而外。container的構造函數首先調用Component的default構造函數,然后才執行自己。
Container::Container(...):Component( ){ ... };
(2) 析構由外而內。container的析構函數首先執行自己,然后才調用component的析構函數。
Container::~Container(...){ ... ~Component( ) };
11.2 Delegation(委托) (Composition by reference)
(1)如圖所示左邊有指向右邊的指針,這種叫委托。它和復合的區別是復合內外部是一起出現,兩者生命周期同步;而委托則是允許外部先創建出來,等需要的時候再把內部寫好,兩者生命周期不同步。
(2) 圖示寫法叫做Handle/Body或者pointer implementation(pimpl)。左邊是對外的接口,右邊是功能的實現,右邊不影響左邊,右邊可以更改甚至指向別的類,具有良好的彈性。
11.3 Inheritance(繼承)表示is-a
子類從父類繼承了數據和函數(函數繼承的是調用權)。
類之間在繼承關系下構造函數與析構函數的順序為:
(1)構造由內而外。Derived的構造函數首先調用Base的default構造函數,然后才執行自己。
Derived::Derived(...):Base( ){ ... };
(2) 析構由外而內。Derived的析構函數首先執行自己,然后才調用Base的析構函數。Base class的析構必須是virtual,否則會出現undefined behavior!
Derived::~Derived(...){ ... ~Base( ) };
十二 虛函數與多態
12.1 虛函數及其應用
繼承最有價值的是和虛函數的搭配使用。成員函數從虛函數的角度出發分為三種:
(1)non-virtual函數:你不希望derived class重新定義(override,重寫)它;
(2)virtual函數:你希望derived class重新定義(override,重寫)它,且你對它已有默認定義;
(3)pure virtual函數:你希望derived class一定要重新定義(override,重寫)它,且你對它沒有默認定義。
虛函數應用實例如下:
舊文件的開啟與讀取,check file name 、search file 、open file任何人寫的都差不多,可以事先寫,但是因為每個人讀取的文件類型不一樣,所以讀取這個動作不能事先寫。框架如下:
應用框架CDocument中已經事先寫好check file name 、search file等函數,CMyDoc是CDocument的子類,沒法事先寫的Serialize()設計為虛函數(可能為空函數,也可能為純虛函數),使用流程如圖箭頭所示,創建一個子類的對象,通過子類的對象調用父類的函數,遇到Serialize()時父類函數去找子類的定義,然后再將剩余的動作完成,將Serialize()延緩到子類去定義,這種用法就叫做Template Method(23種設計模式之一,Method是java中的函數)。
這種設計模式適合做框架,MFC大量用到Template Method。
具體代碼實現(仿真):
12.2 繼承+復合關系下的構造與析構
在圖示繼承+復合關系下構造函數與析構函數的順序為:
(1)構造由內而外。Derived的構造函數首先調用Base的default構造函數,然后調用Component的構造函數,最后才執行自己。
Derived::Derived(...):Base( ),Component(){ ... };
(2) 析構由外而內。Derived的析構函數首先執行自己,然后調用Component的析構函數,最后調用Base的析構函數。
Derived::~Derived(...){ ... ~Component(),~Base( ) };
在圖示繼承+復合關系下構造函數與析構函數的順序為:
(1)構造由內而外。Derived的構造函數首先調用Component的default構造函數,然后調用Base的構造函數,最后才執行自己。
Derived::Derived(...):Component(),Base( ){ ... };
(2) 析構由外而內。Derived的析構函數首先執行自己,然后調用Base的析構函數,最后調用Component的的析構函數。
Derived::~Derived(...){ ... ~Base( ) ,~Component()};
12.3 多態實例
功能最強大的是委托+繼承設計方式。
委托+繼承設計實例1(Observer):
左邊為放數據的class,右邊為觀察的class,左邊可以有很多的右邊,左邊數據裝有指向右邊指針的容器,右邊可以被繼承,將來創建的子類is a Observer都可以放在容器里面,所以可以產生不同的Observer。
左邊要提供注冊和注銷的功能,如圖attach傳入Observer放到容器里頭。左邊還應該有notify把所有Observer進行遍歷,去通知Observer,內容由Observer寫好,左邊調用。
如圖所示有多個窗口看同一份文件或者不同角度看同一份數據。實現代碼如下:
委托+繼承設計實例2(Composite):
文件系統:Primitive代表文件,Composite是一個容器,容納很多個Primitive和Composite,所以設計一個父類Component,Primitive和Composite都is a Component,Component和Composite是委托的關系。add函數和容器類似,add不能設計為純虛函數,因為Primitive不能定義。
委托+繼承設計實例3(Prototype):
我需要一個樹狀繼承體系,子類未來才被派生,不知道未來子類的名稱。讓派生的子類創建一個自己當成原型Prototype,讓我有辦法去看到子類創建出來的的原型放在什么位置上。
在LandSatImage類里面放一個靜態的對象LAST,他的類型是LandSatImage(自己),構造函數寫成私有的,通過私有的構造函數調用addPrototype,將自己掛上去,addPrototype是父類寫的,它將得到的指針放到容器里頭去,這樣就可以使破折號下面創建的原型放到上面去,可以被上面看到。子類準備一個函數clone,它new一個自己。破折號以上可以通過原型(這是一個對象)可以調用clone這個函數,做出一個副本,如果沒有原型則不能。
clone不能是靜態函數,因為靜態函數的調用需要class name,這里沒有。
實現代碼如下: