C++
- 1. 第9章
建構函式與解構函
式
1
著作權所有 ? 旗標出版股份有限公司
本章提要
9-1 建構函式 (Constructor)
9-2 複製建構函式
9-3 解构函式
9-4 物件的阵列
9-5 成员初始化串列
9-6 綜合演練
2
1
- 2. 9-1 建構函式 (Constructor)
當我們建立好一個類別之後, 便希望能將它當
成一般的基本型別來使用, 而要達到這個目的,
必須靠一些特殊的函式成員來實作某些功能。
其中建構函式的功用, 就是讓我們能像定義基
本資料型別的變數一樣, 可以在建立物件同時
就初始化資料成員的內容。
3
物件的初始化
當我們以基本型別來宣告變數時, C++ 的編譯
器會自動根據其型別配置好記憶空間, 同時若
有必要, 也會為它設定初值。而且我們也可在
宣告變數時即指定初值:
當我們用類別來定義物件時, 編譯器也會依照
類別的大小來配置記憶空間給這個物件。但用
前一章所學的方法, 我們無法對物件做類似的
初始值設定: 4
2
- 3. 物件的初始化
5
物件的初始化
如果物件的資料成員是公開的成員, 則雖可利
用大括號的方式設定物件初始值, 但這樣就失
去資料封裝的意義了:
6
3
- 5. 预设建构函式
9
预设建构函式
當我們宣告新的物件時, 這個预设建构函式就
會被呼叫, 只不過由於編譯器自動產生的預設
建構函式中並沒有執行什麼特別的動作, 所以
感覺好像根本沒有预设建构函式一樣。我們可
用以下的範例來檢視建構函式被呼叫的情形。
為了瞭解建構函式被呼叫的時點, 我們故意將
预设建构函式的內容重新定義成只輸出一段訊
息:
10
5
- 7. 预设建构函式
在第 14 行先宣告 2 個 Time 物件, 接著在第
16 行宣告指向物件的指標、第 18 行宣告指標
並用 new 運算子配置 1 個物件。由執行結果
可以發現, 只有第 16 行宣告指標的動作未引
發建構函式, 其它敘述由於都會建立實際的物
件, 所以都會自動呼叫建構函式來做初始化的
動作。因此我們可以瞭解, 系統會在建立物件
時自動呼叫建構函式, 我們只需將需要初始化
的敘述寫在建構函式中讓系統呼叫即可, 不需
自行呼叫建構函式。
13
预设建构函式
請注意第 6 行的 Time() 函式原型並未宣告傳
回值型別, 連 void 也未指定, 這是因為建構函
式本來就規定不能有傳回值, 所以連 void 都可
省了。如果在建構函式前面加上 void 關鍵字,
反而會造成編譯錯誤。
若類別中有其它類型的資料成員, 例如其它類
別的物件, 則在呼叫预设建构函式之前, 會先呼
叫成員物件的建構函式, 請參考以下的例子:
14
7
- 9. 预设建构函式
在 main( ) 函式並未建立 Time 物件, 但由執行
結果可以發現, Time 的建構函式被呼叫了 2
次!這是因為 Clock 類別有 2 個資料成員都是
Time 類別的物件, 所以編譯器會先呼叫 Time
的建構函式來建構這 2 個成員物件, 接著才呼
叫 Clock 自己的预设建构函式。因此當我們用
Clock 類別建立物件時, 就會引發 Time 的建構
函式被執行 2 次, 接著才會執行 Clock( ) 預設
建構函式。
17
预设建构函式
由於预设建构函式一定會在建立物件時被呼叫
(除非我們呼叫其它版本的建構函式), 所以最
適合用來做最基本的初始化動作, 例如將資料
成員都設定一個有意義的初始值等等。例如我
們就可將前述 Time 類別的预设建构函式改寫
成如下的樣子:
18
9
- 12. 预设建构函式
23
预设建构函式
上列程式在第 7 行宣告靜態資料成員 counter
以記錄物件總數, 並在第 15 行定義初始值為 0,
第 6 行的预设建构函式中, 則是將 counter 的
值遞增, 所以每建立一個物件, counter 的值就
會加 1 因此程式中建立含 10 個 Car 物件元素
的陣列後, counter 的值就變成 10 再用 new
運算子建立一個物件, counter 的值就變成 11
了。
24
12
- 13. 建构函式的多载
除了不含任何參數的预设建构函式外, 我們當
然也能設計含參數的建構函式, 透過參數來指
定資料成員的初始值, 這樣一來, 就能在建立物
件時即設好各物件的屬性, 不必像前一章的範
例, 還要在建立物件後, 用額外的函式呼叫來設
定物件的屬性。這類建構函式的設計方式和一
般成員函式沒有太大的不同, 只要記得建構函
式與類別同名, 且無傳回值即可。而在建立物
件時, 可用如下語法讓編譯器呼叫具有參數的
建構函式:
25
建构函式的多载
請參見以下的例子:
26
13
- 15. 建构函式的多载
1. 第 6 行定義预设建构函式, 將時間設為 12 點
整。第 7、8 行則宣告 2 種有參數列的建構函
式。
2. 第 17~21 行定義只有 1 個參數的建構函式內
容, 並以參數為小時的初值, 所以要檢查其值是
否超出小時的合理範圍 (本例採 24 小時制), 若
超出範圍, 則仍是設為 12 點。
3. 第 23~28 行定義有 3 個參數的建構函式內容,
同理, 分及秒的值都不能為負值或超過 59, 若參
數值超出此合理範圍, 則仍會將分或秒設為 0。
29
建构函式的多载
程式在第 32~34 行即以不同的方式建立新物
件, 編譯器會依我們建立物件時所指定的參數
多寡, 尋找參數數量相符的建構函式。第 33-
34 行的程式也可寫成:
30
15
- 16. 建构函式的多载
注意, 程式中未定義 2 個參數的建構函式, 因
此若建立物件時寫 "Time t4 (3,12);", 則編譯時
將會出現錯誤, 因為編譯器找不到簽名相符的
建構函式可以呼叫。要解決這個問題, 除了為
每一種可能狀況設計對應的建構函式版本外,
還有另一種較彈性的作法, 就是替建構函式的
參數設定預設值。
31
為建构函式的参数设定预设值
由於建構函式的基本用法和行為也和一般函式
相同, 所以我們也可為建構函式的參數加上預
設值, 這樣一來, 我們在建立新物件時就可以比
較有彈性, 有時用比較少的參數也能建立物件,
不必再另外定義參數較少的建構函式版本。
在為建構函式設定預設值時要注意一點, 如果
所有的參數都有預設值, 則類別中就不應再定
義不含參數的预设建构函式, 因為此舉將會造
成語意不明 (ambiguous) 的錯誤, 也就是編譯
器不知應呼叫哪一個建構函式, 因而產生錯
誤: 32
16
- 17. 為建构函式的参数设定预设值
33
為建构函式的参数设定预设值
以下我們就利用參數預設值的技巧, 讓 Time
類別的建構函式簡化成只有 1 個, 但使用者仍
是可依其需要, 在建立新物件時, 指定或多或少
的初始值:
34
17
- 19. 為建构函式的参数设定预设值
第 15 行的建構函式 3 個參數都有預設值, 所
以建構物件時可自由指定 0~3 個參數, 都會
呼叫到這個建構函式替物件的資料成員設定初
值。
37
9-2 複製建構函式
編譯器除了會自動產生预设建构函式外, 還會
產生一個特殊的複製建構函式 (Copy
Constructor)。當程式中定義新物件, 並以同類
別的其他物件來做初始值時, 編譯器會呼叫複
製建構函式 (Copy constructor) 來進行物件的
複製。例如:
38
19
- 22. 自动产生的复製建构函式
若稍後物件 a 先被刪除了, 將連帶使配置字串
的空間也被釋放。此時物件 b 也將失去所指的
字串資料:
43
自动产生的复製建构函式
被釋放出的空間可能隨後又被程式用來存放其
它的資料, 此時將導致物件 b 所存的字串資料
變成其它奇怪的內容。為避免這種情況, 對於
有資料成員是指標型別的類別, 就必須定義複
製建構函式, 以合理的方式複製指標的資料。
以字串類別為例, 我們可在複製建構函式中, 配
置新的空間給新物件, 然後複製字串內容到新
配置的空間, 請參考以下的範例:
44
22
- 24. 自动产生的复製建构函式
47
自动产生的复製建构函式
第 34~39 行即為複製建構函式, 其中第 37 行
用 new 配置新的空間, 再於第 38 行呼叫標準
函式庫的字串複製函式 strcpy() 將參數物件 s
的字串內容複製過來。
定義好 Str 類別的內容後, 我們可在程式中含
括這個 .h 檔, 然後在程式中以既有物件初始化
新物件, 以測試複製建構函式:
48
24
- 25. 自动产生的复製建构函式
49
9-3 解构函式
和建構函式相對的成員函式稱為解构函式
(Destructor), 建構函式是在物件建立時被呼叫,
而解构函式則是在物件的生命期結束時 (或是
以 delete 來將 new 配置的物件釋放時), 會由
編譯器自動呼叫以進行善後工作的成員函式。
舉例來說, 如果在建構函式中曾配置新的記憶
體空間, 那麼就必須利用解构函式來將之釋回
給系統。
50
25
- 27. 解构函式
請注意, 物件本身的空間是由系統負責建立和
釋回的, 以上面的 main() 來說, 在執行 "A i(5);"
時:
1. 系統先為 i 物
件配置空間:
2. 呼叫建構函式, 配置 5 個 int 空間, 並將其位址
存入資料成員 p 中:
53
解构函式
當 main() 執行到結尾的 }, 也就是 i 的生命期
結束, 這時候會做下面 2 個動作:
1. 呼叫解构函式將 p 所指的空間釋回。
2. 系統將 i 本身的空間釋放掉。此項處理與解構
函式無關。
54
27
- 28. 解构函式
如果是用 new/delete 配置 / 釋放物件的空間,
也會有類似的建構及解構過程, 例如:
執行 "A *a = new A(10);" 敘述時, 會進行如下
的初始化過程:
55
解构函式
1. 系統配置指標
變數 a 的空間:
2. 用 new 配置物件本身的空間, 並將其位址指定
給 a 指標:
56
28
- 29. 解构函式
3. 呼叫建構函式, 配置 10 個 int 空間, 並將其位址
存入資料成員 a->p 中:
最後執 行 “delete a;” 敘述時, 所做的善後工作
包括:
57
解构函式
1. 在將物件本身的空間釋回以前, 先呼叫解構函
式將其資料成員 p 所指的空間釋回。
2. 系統將物件本身的空間釋回 (指位器 a 的空間
要等到其生命期結束時, 才由系統將之釋放
掉) 。
然而, 對一個具有永久生命期的物件 (全域物件
或靜態物件) 來說, 它的解构函式則是在程式結
束時才被呼叫。以下範例簡單示範解构函式被
呼叫的情形:
58
29
- 31. 解构函式
61
解构函式
程式中有 3 個物件分別是全域、局部靜態、局
部物件, 由執行結果可發現全域變數會在程式
開始執行前就先建構, 它們的建構順序為:
解構的順序則是倒過來:
62
31
- 32. 解构函式
局部物件會在函式結束時就結束其生命期, 並
引發解构函式執行;而全域及局部靜態物件,
都是等程式結束後才結束其生命期, 所以在上
述執行結果中, 是在程式執行第 31 行的敘述
後, 才會執行 a、b 物件的解构函式。
63
用解构函式釋放記憶體空間
前一章提到類別中有指標型別的成員時, 有幾
項工件必須自行處理, 首先就是在建構函式及
複製建構函式中處理指標成員的初始化 (例如
配置新的記憶體空間), 另一項則是在物件生命
期結束時, 需用解构函式釋放原先所配置的記
憶體空間。
以前面的自訂字串類別為例, 應在解构函式中
釋放用來存放字串的動態配置記憶體空間, 修
改後的內容如下:
64
32
- 33. 用解构函式釋放記憶體空間
65
用解构函式釋放記憶體空間
第 11 行的解构函式 ~String() 中的 delete 的
敘述寫成 "delete [ ] data;", 是第 7 章介紹過的
釋放陣列空間的語法。因為我們在 Str 類別的
建構函式中, 是用 "new char[len+1]" 的方式配
置記憶體, 所以釋放時就要用上述的語法。
定義好 Str 類別的內容後, 我們可在程式中含
括這個 .h 檔, 然後在程式中測試解构函式:
66
33
- 36. 物件的阵列
71
物件的阵列
我們在第 7 行輸出用 sizeof() 取得的類別及陣
列大小, 分別是 8 和 32, 表示陣列恰好是 4 個
Str 物件的大小。因為物件中的資料成員只有
記錄字串長度的 int 及指向字串的指標, 所以陣
列大小為 (4+4)x4=32。請注意, 這個大小並不
包括建構函式所動態配置的記憶體空間, 所以
即使我們存放含 100 個字元的字串, 物件本身
的大小仍是固定的。
72
36
- 37. 物件的阵列
另外, 我們也可以用 new 來建立物件陣列, 不
過這時就不能設定各物件的初值了, 例如:
在物件陣列的生命期結束時, 編譯器會為每一
個元素分別呼叫解构函式, 如此才能保證所有
由建構函式配置的記憶體都被釋放掉。再次提
醒讀者, 如果使用:
73
物件的阵列
則將只有陣列的第一個元素會呼叫解构函式,
這是因為編譯器並不知道 p 所指的是一個陣列
我們必須在 delete 和指位器之間加上一 個 []
才行 (不必指明陣列的元素數目):
事實上, 加上 [] 的目的只是要編譯器為每一個
元素都呼叫解构函式, 所以如果類別內並沒有
定義解构函式的話, 在使用 delete 時加不加 []
都無所謂。
74
37
- 39. 成员初始化串列
這時要如何在 Account() 建構函式中初始化
name 的值呢?由本章開頭的介紹已知:在執
行 Account() 建構函式前, 編譯器會先呼叫
Str() 建構函式來建 構 name 成員, 所以我們最
多只能 用 Str 類別提供的設定字串成員函式
(假設有) 來設定 name 的字串值, 這樣一來又
無法享受到建構函式所提供的便利性。其次,
如果 Str 類別未提供無參數的预设建构函式,
則 Account 類別將無法使用, 因為編譯器將找
不到预设建构函式來建構 name 成員。
77
成员初始化串列
為解決這個問題, C++ 提供另一種物件初始化
的方法, 稱為成员初始化串列 (Member
initialization list)。成员初始化串列顧名思義,
可指定各資料成員在初始化時所用的初始值,
讓系統在初始化資料成員時, 即可先設定好初
始值, 不必再於建構函式中用指定的方式設定
其值。對物件成員而言, 成员初始化串列中所
設的初始值, 就會成為呼叫其建構函式的參
數。成员初始化串列需放在建構函式定義的參
數列後面, 其格式如下:
78
39
- 42. 成员初始化串列
83
成员初始化串列
除了物件成員外, 參考型別及 const 型別的資
料成員也必須使用成员初始化串列來初始化其
值。因為大家應還記得:參考型別及 const 變
數都必須在定義時即設定其值, 不能在宣告後
才指定新的值, 換言之這類資料成員都不能在
建構函式中指定其值, 而必須用成員初始化串
列在配置空間時, 即做好初始化其值的工作。
84
42
- 43. 成员初始化串列
85
成员初始化串列
這樣一來, 用上列建構函式建立 Test 的物件時,
系統就會在建立 ri 資料成員的同時, 即將它參
考到 b;在建立 ci 資料成員時, 就以 c 為其初
始值。
最後要提醒讀者, 每個資料成員在串列中只能
出現一次, 而且各資料成員在成员初始化串列
中的次序並不重要, 因為系統在為資料成員配
置空間時, 乃是依照它們在類別定義中的出現
順序來執行, 所以和串列中的排列順序完全無
關。
86
43
- 44. 成员初始化串列
例如:
87
9-6 綜合演練
複數類別的強化 (建構函式)
圆形类别的建构函式
88
44
- 46. 複數類別的強化 (建構函式)
91
複數類別的強化 (建構函式)
第 6 行的建構函式同時為兩個參數都設定預設
值, 所以建立物件時, 可以不加任何參數 (複數
值為 0)、只指定實部 (虛部為 0)、或是自行指
定實部與虛部的值。
在加減法的運算部分, 使用起來仍相當不便, 不
能像使用基本資料型別一樣, 直接以內建的運
算子進行基本的運算, 這些要留待下一章學會
運算子的多載後, 再來提升 Complex 類別的功
能。
92
46
- 47. 圆形类别的建构函式
大家在國中學習幾何時, 決定圓的方式通常是
用圓心座標 (x,y) 加上其半徑, 但在程式設計的
繪圖世界中, 要在畫面上畫出圓形時 (或橢圓),
通常是以指
定圓的外切
矩形的座標
來決定:
93
圆形类别的建构函式
如圖所示, 這種指定方式需指定外切矩形的左
上角及右下角座標, 然後程式會根據這個座標,
畫出在矩形內部的圓形 (若矩形不是正方形, 就
會畫出橢圓形)若我們要設計一代表圓的類別,
並讓使用者能以指定圓心座標及半徑或是圓的
外切正方形的方式來建立其物件, 則需設計可
應用於此兩種狀況的建構函式, 範例程式如
下:
94
47
- 49. 圆形类别的建构函式
97
圆形类别的建构函式
1. 第 11、12 行宣告 3 個資料成員:圓心座標
(x,y) 及半徑 r。
2. 第 8、9 行為計算圓面積及圓周長的成員函
式。
3. 第 15 行的建構函式是以傳入圓心座標及半徑
的方式建構物件, 其中半徑可省略, 預設值為
1。
98
49
- 50. 圆形类别的建构函式
4. 第 20 行的建構函式是以傳入外切矩形的兩個
點座標來定義圓, 因為怕使用者誤輸入的是長方
形或非正方形的座標點, 所以建構函式會先取較
小的一邊為正方形的邊長 (min( )C++ 內建函式,
會傳回 2 參數中的較小值), 再用此值計算圓心
座標及半徑。為方便使用, 函式未限制一定要將
較小的座標點當成第 1 對參數, 所以程式在計算
時, 需先比較座標點的大小, 以免計算出來的半
徑為負值。
99
50