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 洩漏到無法使用之處。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步瞭解 Akismet 如何處理網站訪客的留言資料