CH15 虛擬函式與抽象類別

Download Report

Transcript CH15 虛擬函式與抽象類別

第十五章 虛擬函式與抽象類別
課前指引
C++ 會自動提供預設的複製建構式,稱為「預設複製建構式」完成物
件間的複製。當類別中的成員包含有指標,必須使用動態記憶體配置
時,若使用預設的複製建構式就會隱含一些無法預期的錯誤,這時就
得自訂複製建構式加以處理。如果子類別僅單純的繼承一個父類別,
這樣的繼承稱為單一繼承 (single inheritance),像一父對一子的繼
承,或「父-> 子-> 孫」的垂直繼承,都是單一繼承。而如果一個子
類別同時繼承多個父類別,就稱為多重繼承,就像人類都擁有繼承自
父、母的多重繼承 (multiple inheritance)。
章節大綱
15-1 複製建構式
15-2 虛擬函式
15-3 抽象類別
15-4 多重繼承與虛擬基底類別
15-5 虛擬結構式
備註:可依進度點選小節
15.1複製建構式
當建立物件時,建構式會做初始化的動作,
設定物件的初值。
那麼是否可以使用一個已經建立完成的物件
當初值,來快速建立另一個新的物件呢?C++
提供的複製建構式就是用以完成物件的複製
。
3
15.1 複製建構式
預設的複製建構式
假設 Car 類別的建構式含有兩個參數,可以建立
物件如下:
Car BMW(1,100); // 建立 BMW 物件 編號 id=1 速度
Speed=100
有了 BMW 的物件,就可以再以 BMW 當初值,建
立另一個 Benz 的物件,並將 BMW 的所有成員初
值複製給 Benz。
Car Benz(BMW); // 物件複製
也可以寫成下列的語法:
Car Benz=BMW; // 物件複製
4
15.1 複製建構式
預設的複製建構式
這樣的動作就是物件複製,也就是把 BMW 複製給
Benz,如此就可以很輕易地複製出很多成員初值
相同的物件。完成物件複製動作的機制是由「複
製建構式」來達成的,C++ 會自動提供預設的複
製建構式,稱為「預設複製建構式」完成物件間
的複製。
5
15.1 複製建構式
範例時間:預設的複製建構式
詳細步驟,請翻閱課本15-3頁說明。
執行結果
6
15.1 複製建構式
如何自訂複製建構式
上面的例子中,我們並沒有自訂複製建構式,執
行結果也都正常,編譯器會自動提供預設的複製
建構式,看起來好像也沒有撰寫自訂複製建構式
的必要。
然而,當類別中的成員包含有指標,必須使用動
態記憶體配置時,若使用預設的複製建構式就會
隱含一些無法預期的錯誤,這時就得自訂複製建
構式加以處理。
複製建構式和建構式、解構式相似,若在程式中
提供了自訂複製建構式,則編譯器將不再提供預
設的複製建構式。
7
15.1 複製建構式
自訂複製建構式語法
例如:
const 關鍵字表示傳入的物件僅能讀取不能
修改,參數的傳遞方式必須是物件的參考
(&MyCar)。
8
15.1 複製建構式
範例時間:自訂複製建構式
詳細步驟,請翻閱課本15-5至15-6頁說明。
執行結果
9
15.1 複製建構式
未使用自訂複製建構式產生的錯誤
如果類別中的成員包含有指標,並使用動態記憶
體配置時,使用預設的複製建構式就會隱含一些
無法預期的錯誤,這時就得仰賴自訂複製建構式
加以處理。
我們先證明當類別中的成員包含有指標,並以一
個已存在的物件初始化另一個新建立的物件,若
末使用自訂複製建構式處理動態記憶體配置,將
會產生錯誤,讓讀者有動機研究這個較深入的單
元。
10
15.1 複製建構式
範例時間 :類別中的成員包含有指標,未使
用自訂複製建構式產生的錯誤
詳細步驟,請翻閱課本15-7至15-8頁說明。
執行結果
11
15.1 複製建構式
12
15.1 複製建構式
自訂複製建構式修正錯誤
以上的錯誤是由預設的複製建構式產生的錯誤,
因為在上一個範例中,並未自訂複製建構式,所
以編譯器便自動提供預設的複製建構式。這告訴
我們一件事,當類別成員含有指標並作動態記憶
體配置時,如果有使用物件複製的動作,就必須
自行處理自訂的複製建構式。
上一個例子中,為避免太複雜,故意不使用動態
方式建立指標物件,在這個例子中,我們將之加
入,動態指標物件的存取語法是「->」,並且必
須自行以delete 處理解構。
13
15.1 複製建構式
範例時間:類別中的成員包含有指標,使用自
訂複製建構式修正錯誤
詳細步驟,請翻閱課本15-10至15-12頁說明
執行結果
14
15.1 複製建構式
15
15.1 複製建構式
自訂複製建構式的另一個錯誤
您可能在思索,類別成員不要使用指標,不要以
一個已存在的物件初始化方式建立另一個新的物
件,就可以避開預設複製建構式產生的錯誤。
事實上,當我們呼叫函式,將「物件」當作參數
並以「傳值方式」傳遞時,也會執行預設複製建
構式,而這個時候如果處理不當,也會隱含一些
無法預期的錯誤,以例子來說明。
16
15.1 複製建構式
範例時間:自訂複製建構式的另一個錯誤
詳細步驟,請翻閱課本15-14至15-15頁說明
執行結果
17
15.1 複製建構式
將物件當作參數並以傳值方式傳遞錯誤的處
理
上面的錯誤是因為將物件當作參數並以傳值方式
傳遞給函式,此時函式中會將物件複製一份,這
個複製的動作是由複製建構式來處理,因為程式
中並未自訂複製建構式,編譯器便以預設複製建
構式來處理之。有兩個方法可以修正上面的錯誤
1.將 show() 函式改用傳參考方式傳遞物件參數,這樣
就不會複製物件,也就不會執行複製建構式了,同時以
const 宣告傳遞的傳遞參數只能讀取,可以避免破壞原
來的物件。
只列出不同部份的程式碼,完整程式可參考
CopyStructor6.cpp。
18
15.1 複製建構式
2.在程式中自訂複製建構式 (可參考CopyStructor7.cpp)
修正後正確的執行結果
19
15.2 虛擬函式
虛擬函式
在第 13 章繼承曾提到,子類別會繼承父類別中
的所有成員,子類別也可以相同名稱但不同參數
方式多載父類別的成員函式,同時,也可以使用
相同參數、相同函式名稱及相同傳回值的方式將
父類別中的成員函式覆寫。
20
15.2 虛擬函式
未使用虛擬函式造成的錯誤
然而實際情況並不如預期,看看課本15-18頁的例
子,子類別 Triangle 繼承 Rectangle父類別,
並覆寫 GetArea() 計算三角形面積。
範例時間:未使用虛擬函式造成的錯誤
詳細步驟,請翻閱課本15-19至15-20頁說明
執行結果
21
15.2 虛擬函式
使用虛擬函式
上例的錯誤是因為編譯器將 showArea() 和
GetArea() 在連結時以早期繫結(early binding)
的方式編譯在一起了,所以會造成子類別呼叫的
GetArea() 仍是父類別的函式。
解決的方法,就是將 GetArea() 函式改為虛擬函
式,告訴編譯器,別太早連結配對,等到實際實
行時才加以連結,這種連結稱為晚期繫結 (late
binding) 或動態繫結 (dynamic binding)。
虛擬函式的語法:
virtual 傳回值資料型別 函式名稱();
例如: virtual int GetArea(); // 宣告虛擬函式
22
15.2 虛擬函式
範例時間:使用虛擬函式作動態繫結
詳細步驟,請翻閱課本15-21至15-22頁說明
執行結果
23
15.2 虛擬函式
指向基底類別的指標
指標物件可以更改指標指向另一個物件的位址,
它可以指向父類別物件的位址,也可以改變指標
物件指向子類別的位址。
例如:以父類別建立的指標物件 p,並將指標指
向父類別物件 Rect 的位址,再將指標物件 p 指
向子類別的位址 Tri 的位址。
24
15.2 虛擬函式
指向基底類別的指標
然而如果這個指標是由父類別所建立的物件指標
,也會因為編譯器的早期繫結而產生錯誤,以例
子來說明更改指向基底類別指標的錯誤。
範例時間:指向基底類別的指標
詳細步驟,請翻閱課本15-23至15-24頁說明
執行結果
25
15.2 虛擬函式
使用虛擬函式改進指向基底類別的指標的錯
誤
上例的錯誤一樣是因為編譯器將 showArea() 和
GetArea() 在連結時以早期繫結 (early
binding) 的方式編譯在一起了,所以會造成指向
子類別物件 Tri 的指標物件 p 呼叫的
GetArea() 仍是父類別的函式。
解決的方法,一樣是將 GetArea() 函式改為虛擬
函式,告訴編譯器,別太早連結配對,等到實際
執行時才以動態方式繫結 (dynamic binding)。
26
15.2 虛擬函式
範例時間:以虛擬函式改進指向基底類別的指
標的錯誤
詳細步驟,請翻閱課本15-25至15-26頁說明
執行結果
27
15.3 抽象類別
有些時候,可能希望先以父類別定義出架構
,然後再由其子類別繼承後使用,這樣所有
繼承的子類別就會長得像父類別,具有一致
性的介面。這有點像是中央政府頒訂政策,
再委由地方政府策劃細節後實施。
這個父類別只需訂出大的架構,不必訂出實
施細節,而且也不可以直接以此類別來建立
物件,這樣較特殊的類別稱為「抽象類別」
(abstract class)。
28
15.3 抽象類別
純虛擬函式
以 virtual 宣告的虛擬函式若將初值設為 0,即
為純虛擬函式。
純虛擬函式的語法:
Virtual 傳回值資料型別 成員函式=0;
例如:
virtual int GetArea()=0; // 定義 GetArea() 為純
虛擬函式
29
15.3 抽象類別
認識抽象類別
當類別成員只要擁有一個 ( 或一個以上) 純虛擬
函式,就會變成抽象類別。
抽象類別有幾個特性和一般的類別不同:
1. 抽象類別不能建立物件,而是透過子類別的繼承和
實作後,以子類別建立物件。
2. 父類別中的純虛擬函式只需定義,不必實作,然後
再由子類別實作純虛擬函式。
30
15.3 抽象類別
如果子類別中未將父類別的純虛擬函式實作
,則此子類別仍然是一個抽象類別。
此外,抽象類別和一般類別相同,也可以定
義資料成員和成員函式,只是不能以抽象類
別建立物件。
例如:Base 類別含有純虛擬函式「virtual
int Show()=0」,即為抽象類別。
31
15.3 抽象類別
範例時間:抽象類別
詳細步驟,請翻閱課本15-28至15-30頁說明
執行結果
32
15.3 抽象類別
範例時間:改變父類別 (抽象類別) 的指標變
數,分別指向不同的子類別
詳細步驟,請翻閱課本15-30至15-31頁說明
執行結果
33
15.4 多重繼承與虛擬基底類別
了解最簡單的「父-> 子」繼承後,開始將繼
承多樣化,類別繼承的方式還有「父-> 子->
孫」稱為垂直繼承,也可有一父多子、一子
多父,甚至是更複雜的多重繼承。
34
15.4 多重繼承與虛擬基底類別
多重繼承
如果子類別僅單存的繼承一個父類別,這樣的繼
承稱為單一繼承 (single inheritance),像一父
對一子的繼承,或「父-> 子-> 孫」的垂直繼承
,都是單一繼承。
而如果一個子類別同時繼承多個父類別,就稱為
多重繼承,就像人類都擁有繼承自父、母的多重
繼承 (multiple inheritance)。多重繼承時,子
類別會同時繼承所有父類別的所有成員。
35
15.4 多重繼承與虛擬基底類別
多重繼承的語法如下:
class 子類別:public 父類別一,public 父類別二
例如:子類別 child 同時繼承 father、mother
兩個父類別。
class child:public father,public mother
{
};
36
15.4 多重繼承與虛擬基底類別
範例時間:多重繼承
詳細步驟,請翻閱課本15-34至15-35頁說明
執行結果
37
15.4 多重繼承與虛擬基底類別
相同父類別成員的多重繼承
上面的範例中,因為兩個父類別的成員名稱都不
相同,所以不會有任何的問題,但如果父類別中
成員使用相同名稱,將會有繼承錯亂的問題。
以例子來說明:
38
15.4 多重繼承與虛擬基底類別
上例故意將兩個父類別的成員都定為相同的
名稱,這樣編譯器仍會將所有父類別的成員
繼承給子類別,如果沒有存取這些名稱相同
的成員,在編譯時並不會產生錯誤。
不過,問題會發生在對這些相同名稱成員存
取時,編譯器將因不知要存取的對象而產生
最簡單的方法就是明確以範圍解
錯誤。
析運算子「::」加上指定的父類
別名稱來存取,即「父類別:: 成
員」,例如:
「Baby.father::showArea()」,執
行 father 父類別的 showArea() 函
式。
39
15.4 多重繼承與虛擬基底類別
範例時間:相同父類別成員的多重繼承
詳細步驟,請翻閱課本15-37至15-38頁說明
執行結果
40
15.4 多重繼承與虛擬基底類別
虛擬基底類別
在多重繼承時,如果父類別成員名稱相同,會造
成存取無法明確辨識的困擾,同樣的,如果是像
下圖的多重繼承,也會有繼承錯亂的困擾
袓父類別含有一個成員函式 showArea(),所以他
的兩個孩子 A、B 都會繼承showArea(),而子類
別 C 則繼承兩個父類別 A、B ( 間接繼承自袓父
類別—或稱隔代遺傳),當子類別 C 要存取
showArea() 將發生辨識的困擾,因為它分別繼承
了父類別 A、B 的 showArea() 函式(間接繼承自
袓父類別的)
41
15.4 多重繼承與虛擬基底類別
上面的繼承圖說明,如果子類別擁有兩個袓父類
別相同的成員,將無法正確存取此袓父類別的成
員,因為它擁有兩個相同成員。要解決此問題,
必須透過虛擬基底類別 (virtual base class)
的機制來解決。
42
15.4 多重繼承與虛擬基底類別
只要在父類別以 virtual 將袓父類別宣告為虛擬
基底類別,如此一來,繼承的子類別將只擁有一
份袓父類別的成員,這樣就可以正確取代袓父類
別相同的成員。
虛擬基底類別的語法:
class 父類別:public virtual 袓父類別
{
…程式碼;
};
例如:在父類別 father 定義 grandfather 袓父
類別為虛擬基底類別
class father:public virtual grandfather
43
15.4 多重繼承與虛擬基底類別
範例時間:多重繼承--虛擬基底類別
詳細步驟,請翻閱課本15-40至15-41頁說明
執行結果
44
15.5 虛擬解構式
虛擬解構式
15.2.3 節中提到,更改指標指向另一個子類別物
件的位址,如果這個指標是由父類別所建立的物
件指標,則會因為編譯器的早期繫結,而產生錯
誤。
同樣地的情形,也會發生在解構上,看看下列的
問題,以父類別 Base 建立指標物件 p,再將 p
指向子類別 Sub 以動態配置記憶體建立的物件。
45
15.5 虛擬解構式
虛擬解構式
這樣也會使得以 Base 類別建立的指標物件 p,
因為前期繫結,而繫結至Base 類別,所以以
delete p 解構時執行的是父類別 Base 的解構式
,而不是子類別 Sub 的解構式。
解決的方法是將父類別的解構式以 virtual 關鍵
字定義為虛擬解構式。
46
15.5 虛擬解構式
虛擬解構式
上面的例子提醒我們,當使用動態記憶體配置方
式,將子類別物件位址指派給父類別物件的指標
時,別忘了要將父類別的解構式設為虛擬解構式
。
範例時間:虛擬解構式
詳細步驟,請翻閱課本15-43至15-44頁說明
執行結果
47
15.5 虛擬解構式
虛擬解構式
下列程式碼,以父類別 Base 建立指標物件 p,
再將 p 指向另一子類別 Sub物件的位址,delete
釋放配置記憶體時將會發生解構錯誤。
必須在父類別的解構式以 virtual 關鍵字定義為
虛擬解構式。
48
本章結束
Q&A討論時間
49