C/C++ 學習筆記 004 – polymorphism(多型) 與 virtual function(虛擬函數)
參考書: C++ 程式設計與應用 (張耀仁 著 清蔚科技出版) 的筆記
前言
polymorphism(多型) 與 virtual function(虛擬函數) 的學習筆記。請注意筆記論述並不夠嚴謹,各位讀者務必小心。很多「動作」、「稱呼」都不夠正式、不夠嚴謹,只是根據自己的思考跟意象來描述。
因此請注意不要被這篇筆記弄混了,例如、下面第 3. 點我就還不確定那樣的動作可不可以稱為「呼叫」、「調用」,其實最好還是以原文為主….
0. 基本名詞
- Base Class = 基礎類別
- Derived Class = subclass = 衍生類別
1. cast
重新賦予某變數資料型態,例如:
int a; float(a); (double)a;
2. upcast
衍生類別內的物件,被賦予了基礎類別的資料型態 。例如、有一個基礎類別 BasedZero ,一個衍生類別 DerivedOne :
DerivedOne a,b; BasedZero *c = &a; BasedZero &d = b;
3. Polymorphism 可以達成如下動作:
「在程式內呼叫基礎類別內的某函數,但最終卻能調用衍生類別內的同名函數。」
此外,不同衍生類別內可以存在同名函數,他們可以分別執行不同的事情,或是進行不同的動作。 對應到實際上來說,就好像是去作用到不同的實體對象上,譬如、去按下不同的電燈開關。
- 按電燈開關這個動作可以定義為一個基礎類別。
- 按某一層樓的電燈開關可以定義為一個衍生類別。
- 我們可以宣告 「按下台北101 電燈開關」 ,是上述基礎類別的物件。
- 我們可以宣告「按下台北101 一樓電燈開關」 ,是上述衍生類別的物件。
4.大概過程
4.1 基礎類別Operate 裡,有著所有動作的基本定義,譬如、輕觸、滑動、按下、敲擊、撞擊、拳打腳踢 … 等等。
4.2 基礎類別Operate 內,其中有個基本動作是「按下開關」 ,我們訂此函數為 press(),並定義其為 virtual function (虛擬、虛構的函數,但不是不存在的意思)
4.3 衍生類別則有 SW_01、SW_02、SW_03 ,代表對三個不同開關的操作,例如:去按下開關一、開關二、開關三 。這三個開關,當然就是三個衍生類別內的資料成員。
三個衍生類別內都自訂了自己的 press ,會分別去按下開關一、開關二、開關三等三個不同的實體開關,如下:
/* 耍懶惰,省略所有建構解構函數,省略宣告資料成員 ~ :P */ /* 在 .h 內宣告基礎類別 Operate ,內有 press () */ class Operate { public: virtual void press() { 按下不特定開關; } } /* 在 .h 內宣告衍生類別 SW_01 ,內有 press () */ class SW_01 : public Operate { public: void press(){ 按下實體開關一; } }
4.4 main與主程式內
4.4.1 先宣告
/* 先宣告個基礎類別物件 */ Operate O1, O2; /* 宣告三個衍生類別物件 */ SW_01 s1; SW_02 s2; SW_03 s3;
4.4.2 現在來制定實際上有某種功能的函數,例如 開燈、關燈:
TurnON(Operate &O1) { O1.press; } TurnOFF(Operate *O2) { O2->press; /* 或也可以寫 {(*O2).press;} */ }
也就是在 TurnON 與 TurnOFF 內,調用基礎類別的 Press()
注意這裡只是為了示範,所以才分別把 TurnON 傳遞 O1 的 reference 、 TurnOFF 傳遞O2 的 address ,事實上自己撰寫時不必這麼花俏XD
4.4.3 最後 main 內這樣用:
TurnON (s1); // 傳入函數後,雖然我們在上面 4.4.2 的 TurnON 函數內
// 寫的是基礎類別的 press // 但是最後會改呼叫SW_01內的press,於是最後按下的是開關一 TurnON (s2); TurnON (s3); TurnOFF (s1); // 同樣的道理,最後是呼叫 SW_01 內的 press ,因此最終是按下開關一 TurnOFF (s2); TurnOFF (s3);
若沒宣告virtual 那就是呼叫到基礎類別內的 press() ,也就是說,會去執行基礎類別內訂的 press ,而不會分別去按下三個開關 。 (也許就想做是只作個按開關的動作,卻沒真正的按下哪個開關)
5. 名詞
- redefinition(重訂):衍生類別中,定義一個跟基礎類別相同名稱的函數,稱為 redefinition
- overidding(覆寫、覆蓋):而如果重訂的對象是虛擬函數,那可稱為 overidding(
- object slicing(物件遭切割):函數傳遞變數(基礎類別)的方式採用 by value 的話,會被切掉衍生類別的相關資料
6. VPTR
基礎類別中有虛擬函數時 編譯器會幫這些基礎類別衍生類別內的同名函數加上 VPTR 來識別 此指標也會稍微增加衍生類別的大小 +1
編譯器會幫這些基礎類別衍生類別內的同名函數加上 VPTR 來識別 此指標也會稍微增加衍生類別的大小 +1
7. VTABLE
編譯器還為每個類別加上一個VTABLE,VPTR指向VTABLE的開頭。 VTABLE 紀錄了基礎類別內被宣告為 virtual function 的城員函數。
- 基礎類別的 VTABLE 就是紀錄自己的成員函數。
- 衍生類別內,假如自己有overriding 基礎類別的virtual function ,那就會儲存衍生類別的成員函數的位址。
- 如果沒overidding,那就儲存基礎類別的成員函數的位址。
- 所以VPTR、VTABLE 就是幫助late binding(暫譯 晚點兒連結XD )的達成
8. pure virtual function (純虛擬函數)
基礎類別內這樣寫
virtual void rotate() = 0 ;
就是說,基礎類別內的虛擬函數並不打算真正的對應到真實動作,而基礎類別也可能不打算對應真實物件。
一個類別內,有純虛擬函數,就叫做抽象類別 abstract class,若所有虛擬函數都是純的,那就是純抽象類別。 抽象類別就不能拿來訂實體物件(instance),編譯不會過,這可以防止抽象類別對應到實體。
9.
但既然把函數定義為: virtual void rotate()=0;
上述的函數並沒有內容在其中。 那萬一我們既需要抽象類別,但卻又要有函數內容給大家共用,how to ???
嗯 … class 內依舊寫:
class 基礎類別{ virtual void rotate()=0; }
並另外撰寫出函數的定義:
void Shape::rotate() { /*實際內容*/ }
然後各個衍生類別內,都應該重寫一下:
/* 衍生類別內 */
void rotate() { Shape::rotate(); }
這樣就可以啦!
10.
或者也可以另外訂個新函數,使用基礎類別的參照來接收衍生類別的物件就可以了。
例如 這麼宣告:
衍生類別 D1; /* 宣告衍生類別的物件 D1 */ MakeRot(D1); /* 呼叫函數 傳遞 D1 為參數 */
這麼定義:
MakeRot(基礎類別 &B) /*用 基礎類別的 reference 接收 D1*/ { B.Rotate(); }
所以,若 pure virtual function 有定義,可衍生類別內虛擬函數沒有去覆蓋 (overide) 同名者,編譯器會出錯,除非你這麼寫:
void rotate() {Shape::rotate();}
11.
基礎類別內,如果成員函數有 overloading 的情形,那麼衍生類別內倘若重定義(redefinition)了其中一個,則另外一個overloading 的函數會失效。
如果這些是 virtual function ,即便同樣的有 overloading,並在衍生類別內有 overide。雖然一樣會有 name hidoing的狀況, 但是可以技巧性繞過,使用 upcast
例如、有一個基礎類別的物件,一個衍生類別的物件:
基礎類別 B1;
衍生類別 D1;
在基礎類別內,有個函數 CCC() ,而且此函數還有 overloading 的函數 CCC(int)
倘若我們在衍生類別內重訂函數 CCC()。 那麼, D1.CCC() 可以工作,但 D1.CCC(8) 不能工作。
可是若我們這樣宣告 (指標、參照):
基礎類別 *B2 = &D1;
基礎類別 &B3 = D1;
那麼,下面這兩個做法:
B2->CCC(6);
B2.CCC(6);
就都可以用,也就是 upcasr 技巧, 用基礎類別的宣告去賦予一個衍生類別物件的新資料型態。
12. 虛擬解構函數
建構時,還沒編排 VPTR VTABLE,所以不必在意。
但解構時必須把解構函數宣告成virtual function ,才能釋放 VPTR VTABLE
像這樣: virtual ~base()
反正,基礎類別內有宣告虛擬,那你就把解構也虛擬,就可以了。否則記憶體可能會一直沒有釋放, memory leakage 洩漏到無法使用之處。
Leave a Reply