Uploaded by bqwwwqwq

ch9

advertisement
9
類別:深入了解;
拋出異常
My object all sublime
I shall achieve in time.
—W. S. Gilbert
Is it a world to hide virtues in?
—William Shakespeare
Have no friends not equal to
yourself.
—Confucius
學習目標
在本章中,您將學到:
] 使用標頭檔防護,避免重複
引用標頭檔。
] 透過物件名稱、參考或指標
存取類別成員。
] 使用解構子執行物件結尾工
作。
] 了解建構子和解構子被呼叫
執行的順序。
] 回傳 private 物件參考所引發
的危險。
] 物件資料成員間的指派。
] 組合其他物件來產生新物件。
] 使 用 夥 伴 (friend) 函 式 與 類
別。
] 在 成 員 函 式 中 使 用 this 指
標,存取非靜態類別成員。
] 使用 static 資料成員與成員函
式。
9-2
C++程式設計藝術
本章綱要
9.1 簡介
9.9 預設逐成員指派
9.2 個案研究:類別 Time
9.10 const 物件和 const 成員函式
9.3 類別範圍及存取類別成員
9.11 組合:將物件當作類別成員
9.4 Access 函式與 Utility 函式
9.12 夥伴函式與夥伴類別
9.5 類別 Time 個案研究:建構子與預設引數
9.13 使用 this 指標
9.6 解構子
9.14 static 類別成員
9.7 建構子與解構子的呼叫時機
9.15 總結
9.8 類別 Time 個案研究:微妙陷阱,
傳回 private 資料成員的參考
摘要︱自我測驗題︱自我測驗題解答︱習題︱創新進階題
9.1 簡介
本章將深入探討類別。透過一個整合的範例 Time 類別,搭配其他範例,呈現幾個類別
的建構功能。我們使用 Time 類別,重溫前幾章提到的類別特性。範例中也會介紹使用標頭
檔防護,避免在同一個原始碼檔案中,重複引用標頭檔。
我們將示範類別的顧客端程式,可使用三種方式,來存取類別的 public 成員,分別是:
(1) 透過物件名稱 (name of an object)。
(2) 透過物件的參考 (reference to an object)。
(3) 透過指向物件的指標 (pointer to an object)。
物件參考是物件的別名,所以,透過物件和物件參考時,使用相同的點號 (.) 成員選擇
運算子 (member selection operator)。透過指標時,用的是箭號 (->) 成員選擇運算子。
我們會介紹可以讀取或寫入物件成員資料的存取函式 (access functions)。存取函式常
見的用途之一,是測試條件的真偽,因此又稱為判斷函式 (predicate functions)。我們還會
介紹工具函式 (utility functions,又稱助手函式 helper functions),這種函式是類別的 private
成員函式,唯一的作用是幫助類別的 public 函式完成工作。使用類別的外部程式無法直接呼
叫它們,因為它們是私有的 (private)。
我們會介紹如何將引數傳入建構子 (constructor),以及如何在建構子中使用預設引
數,使顧客端程式可運用各種引數為物件進行初始化。接下來,我們要討論一種稱為解構子
(destrcutor) 的特殊成員函式,用來在物件被摧毀之前進行資源回收工作。我們還會說明建
構子與解構子呼叫的順序。
我們也展示了回傳 private 資料的參考,會破壞物件的封裝 (encapsulation),讓外部
程式得以直接存取物件的資料,引發危險。我們還將展示,使用預設物件的逐成員指派
(memberwise assignment) 功能,可讓屬性同一類別的物件,彼此使用指派運算子。
我們可透過 const 物件與 const 成員函式,防止物件被修改,強制實行最小權限原則。討
論軟體再利用的一種形式,稱為組合 (composition),讓類別用其他類別的物件作為其資料
第九章
類別:深入了解;拋出異常
成員。接下來,會討論類別的夥伴關係 (friendship),讓非成員函式也能存取類別的非 public
成員,這是運算子多載的效能因素,常用的一個技術 ( 第 10 章 )。我們還會討論一個特別的
指標 this,類別中任何非 static 的成員函式,都默默 ( 隱式:implicite) 以此指標作為引數,以
便存取正確物件的資料成員,以及其他非 static 的成員函式。我們會說明為何需要 static 類別
成員,並示範如何在類別中使用 static 資料成員與成員函式。
9.2 個案研究:類別 Time
我們的第一個範例,建立類別 Time。我們展示一個 C++ 軟體工程中重要的觀念,在標
頭檔中使用標頭檔防護,防止標頭檔在同一個原始檔中被引用多次。因為類別的定義只能出
現一次,故此技巧可避免重複定義的錯誤。
類別 Time 的定義
類別 Time 的定義 ( 圖 9.1) 包括成員函式 Time、setTime、printUniversal 和 printStandard 的原
型 ( 第 13-16 行 ),以及 hour、minute 和 second 三個整數 priviate 成員 ( 第 18-20 行 )。Time 類別的
private 資料成員,只有類別的四個成員函式才能存取它們。第 11 章討論繼承 (inheritance) 和它
在物件導向程式設計中所扮演的角色時,我們會介紹第三種成員存取修飾詞 protected。
良好的程式設計習慣 9.1
為增進程式的清晰性與可讀性,在類別的定義中,通常每個存取修飾詞只會出現一
次。並將常用的 public 成員放在最前面,這是較容易看到的位置。
軟體工程觀點 9.1
類別的函式與資料成員的權限應為 private,除非該元件的確需要 public 權限。這是
另一個最小權限原則的實踐。
1 // Fig. 9.1: Time.h
2 // Time class definition.
3 // Member functions are defined in Time.cpp
4
5 // prevent multiple inclusions of header
6 #ifndef TIME_H
7 #define TIME_H
8
9 // Time class definition
10 class Time
11 {
12 public:
13
Time(); // constructor
14
void setTime( int, int, int ); // set hour, minute and second
15
void printUniversal() const; // print time in universal-time format
16
void printStandard() const; // print time in standard-time format
17 private:
圖 9.1
Time 類別的定義 (1/2)
9-3
9-4
C++程式設計藝術
18
unsigned int hour; // 0 - 23 (24-hour clock format)
19
unsigned int minute; // 0 - 59
20
unsigned int second; // 0 - 59
21 }; // end class Time
22
23 #endif
圖 9.1
Time 類別的定義 (2/2)
在圖9.1中,類別定義包含在標頭檔防護 (include guard) 之中,( 第6、7和23行 ),如下所示:
// prevent multiple inclusions of header
#ifndef TIME_H
#define TIME_H
...
#endif
在建構較大型的程式時,通常會將其他定義和宣告也放到標頭檔中。如果程式已經定義
過 TIME_H,則以上前置標頭檔防護不會重複含括 #ifndef ( 意思為「如果尚未定義」) 和 #endif
之間的程式碼。如果某個檔案先前未嵌用 ( 引用:include) 此標頭檔,則 #define 指令會定義
TIME_H,並嵌用標頭檔中的敘述。如果程式先前已經加入標頭檔,因為 TIME_H 己於上次
嵌用時定義,所以不會再次嵌用。大型的程式通常有好幾個標頭檔,甚至標頭檔本身還嵌用
其他的標頭檔,因此很容易造成重複嵌用的狀況。
錯誤預防要訣 9.1
利用 #ifndef、#define 和 #endif 等前置處理指引,形成標頭檔防護,可避免程式重複
嵌用標頭檔。
良好的程式設計習慣 9.2
依慣例,在標頭檔的前置處理指引 #ifndef 和 #define 中,使用大寫的標頭檔名稱,
並將點號 "." 替換成底線 "_"。
類別 Time 的成員函式
在圖 9.2 中,建構子 Time ( 程式第 11-14 行 ) 會將所有資料成員的初始值設為 0 ,相當於
通用格式時間的 12 AM。無效值不會存入資料成員中。因為,建立 Time 物件時,程式會自
動呼叫建構子,且後續所有對資料成員的修改,均得透過 setTime 函式來執行,setTime 函式
執行有效性檢查與修正,故 Time 物件的資料成員不可能為無效值。最後,值得一提的是,
程式設計師可以為類別定義不同的多載建構子 (overloaded constructor),我們已在 6.18 節學
過多載函式 (overloaded functions)。
1
2
3
4
5
// Fig. 9.2: Time.cpp
// Time class member-function definitions.
#include <iostream>
#include <iomanip>
#include <stdexcept> // for illegal_argument exception class
圖 9.2
Time 成員函式的定義 (1/2)
第九章
類別:深入了解;拋出異常
6 #include "Time.h" // include definition of class Time from Time.h
7
8 using namespace std;
9
10 // Time constructor initializes each data member to zero.
11 Time::Time()
12
: hour( 0 ), minute( 0 ), second( 0 )
13 {
14 } // end Time constructor
15
16 // set new Time value using universal time
17 void Time::setTime( int h, int m, int s )
18 {
19
// validate hour, minute and second
20
if ( ( h >= 0 && h < 24 ) && ( m >= 0 && m < 60 ) &&
( s >= 0 && s < 60 ) )
21
{
22
23
hour = h;
24
minute = m;
25
second = s;
} // end if
26
27
else
28
throw invalid_argument(
29
"hour, minute and/or second was out of range" );
30 } // end function setTime
31
32 // print Time in universal-time format (HH:MM:SS)
33 void Time::printUniversal() const
34 {
35
cout << setfill( '0' ) << setw( 2 ) << hour << ":"
36
<< setw( 2 ) << minute << ":" << setw( 2 ) << second;
37 } // end function printUniversal
38
39 // print Time in standard-time format (HH:MM:SS AM or PM)
40 void Time::printStandard() const
41 {
42
cout << ( ( hour == 0 || hour == 12 ) ? 12 : hour % 12 ) << ":"
43
<< setfill( '0' ) << setw( 2 ) << minute << ":" << setw( 2 )
44
<< second << ( hour < 12 ? " AM" : " PM" );
45 } // end function printStandard
圖 9.2
Time 成員函式的定義 (2/2)
在 C++11 之前,只有 static const int 的資料成員 ( 在第 7 章看到的 ) 可以在類別主體中宣
告並初始化。基於這個原因,資料成員通常應該由類別的建構子初始化,尤其是對沒有預設
初始化的基本型態資料成員。有了 C++11,您現在可以在類別定義中宣告資料成員時,使用
類別內初始值,初始化任何資料成員。
Time 類別的成員函數 setTime 和拋出異常
函式 setTime ( 第 17-30 行 ) 是一個 public 函式,接收三個 int 參數,用它們來設定時間。
第 20-21 行,測試每一個引數,判斷它們的值是在有效範圍內,如果有效,則將這些有效
值,指派給資料成員 hour、minute 和 second。hour 的值必須大於等於 0 且小於 24,因為在通
9-5
9-6
C++程式設計藝術
用時間格式中,時的範圍必須為 0 到 23 間的整數 ( 例如:1 PM 以 13 時表示,11 PM 以 23 時
表示,0 表示午夜,12 表示中午 )。
同樣的,minute 和 second 的值也必須大於等於 0 且小於 60。若遇到超出範圍的值,函式
setTime 會拋出異常 (throws an exception) ( 第 28-29 行 ),異常型態為 invalid_argument ( 定
義在 <stdexcept> 標頭檔中 ),此異常目的在告訴顧客端程式,收到不合理的引數。正如您在
7.10 節中所學,使用 try ...catch 捕捉異常,並試圖從異常中復原,這部分在圖 9.3 的程式執
行。敘述 throw ( 第 28-29 行 ) 建立一個新物件,型態為 invalid_argument。在建立異常物件
後,throw 敘述立即終止 setTime 函式的執行,異常回到設定時間的處理程式。
。
Time 類別的成員函數 printUniversal
函式 printUniversal ( 圖 9.2,第 33-37 行 ) 不接收任何引數,以通用時間格式印出時間,此
格式為三個以冒號隔開的數字,分別代表時、分和秒。舉例來說,假設時是 1:30:07 PM,函
式 printUniversal 會印出 13:30:07。我們在第 35 行使用參數化串流操作子 setfill 設定填充字元 (fill
character),當印出的數字位數小於輸出欄位時,用這個字元把欄位填滿。在預設情況下,填
充字元會出現在數字的左邊。以這個例子來說,如果 minute 的值為 2,會顯示成 02,因為填充
字元設定成零 ('0')。如果數字的位數夠多,就不會顯示填充字元。一旦使用 setfill,其後所有
輸出欄位都會使用該函式所設定的填充字元;這稱為粘著性設定 (sticky setting)。相對於另一
個操作子,setw,就只會對下一個欄位作用,這稱為非粘著性設定 (non-sticky setting)。
錯誤預防要訣 9.2
各種粘著性設定 ( 如填充字元或浮點數精準度 ) 使用完畢後,應把它恢復為原來的設
定,否則會使後面的程式產生不正確的輸出格式。我們會在第 13 章討論如何重置填
充字元和浮點數精準度的設定。
Time 類別的成員函數 printStandard
函式 printStandard ( 第 40-45 行的 ) 同樣不接收任何引數,將日期按標準時間格式輸出,
包括 hour ( 時 )、minute ( 分 ) 和 second ( 秒 ) 的值,中間以冒號隔開,最後加上 AM 或 PM 表
示上午或下午 ( 如 1:27:06 PM)。和函式 printUniversal 一樣,函式 printStandard 透過 setfill ('0')
用兩位數字印出 minute 和 second 的值,必要時在數值前面補零。第 42 行用條件運算子 (?:)
決定如何顯示 hour 的值,若其值為 0 或 12 ( 無論是 AM 或 PM),都顯示 12;否則以 1 至 11 顯
示之。第 44 行的條件運算式決定要顯示 AM 還是 PM。
在類別定義之外定義成員函式,類別範圍
即使我們在類別的定義之內宣告成員函式,並將其定義置於類別的主體之外 ( 並透過二
元範圍解析運算子將此二者綁定 (tied)),成員函式仍屬於類別範圍 (class scope);意思是
說,只有此類別的其他成員能夠直接使用函式名稱,否則,程式必須透過此類別的物件、此
類別物件的參考 ( 也就是別名 ) 或指向此類別物件的指標,或是二元範圍解析運算子,才能
夠取用它的名稱。我們稍後會更深入討論類別範圍的議題。
第九章
類別:深入了解;拋出異常
成員函式若定義於類別定義中,則此函式默默變成是內嵌 (inline) 函式。記住,編譯器
有權決定是否要讓該函式成為內嵌函式。
效能要訣 9.1
請將小型的成員函式定義放在類別定義之中 ( 若得到編譯器認可 ),就可讓此函式成
為內嵌函式,提高程式的效能。
軟體工程觀點 9.2
在類別的標頭檔中,最好只定義最簡單和最穩定 ( 不太會變更實作 ) 的成員函式。
成員函式與全域函式 ( 又稱自由函式 )
printUniversal 和 printStandard 這兩個成員函式沒有任何引數,這是因為成員函式已經隱
含知道所要印出的內容,是被呼叫的 Time 物件的資料成員。這麼做可讓成員函式呼叫,相
較於程序化程式設計的傳統函式呼叫,顯得簡潔許多。
軟體工程觀點 9.3
物件導向程式設計可藉由減少參數數目,達到簡化函式呼叫目的。物件導向程式設
計的優點,來自於透過物件封裝資料成員和成員函式,且讓成員函式擁有存取資料
成員的權限。
軟體工程觀點 9.4
成員函式的程式碼和非物件導向程式設計的函式相比,通常較為精簡,因為儲存在
資料成員的資料,已透過建構子或存放新資料的成員函式驗證過了。
因為資料已經存放在物件中,使用該資料的成員函式通常不需要使用引數;就算需
要,也會比非物件導向程式設計的函式少。因此,函式呼叫,以及成員函式的原型
和定義就比較精簡。這對程式發展的許多面向都很有幫助。
錯誤預防要訣 9.3
呼叫類別的成員函式時,通常不需使用引數,就算需要,通常也會比非物件導向程
式語言中的傳統函式呼叫少,故傳遞錯誤引數、錯誤引數型態和錯誤引數個數的可
能性大為降低。
使用類別 Time
定義完類別 Time 之後,就可把它當作一種資料型態來用,如:
Time sunset; // object of type Time
array< Time, 5 > arrayOfTimes; // array of 5 Time objects
Time &dinnerTime = sunset; // reference to a Time object
Time *timePtr = &dinnerTime; // pointer to a Time object
9-7
9-8
C++程式設計藝術
圖 9.3 使用類別 Time,第 11 行產生類別 Time 的物件,名為 t。物件產生時 ( 實體化,
instantiated),Time 的建構子會被呼叫,將每個 private 資料成員的初始值設為 0。接下來,第
15 和 17 行分別將時間以通用和標準格式印出,確認資料成員已正確設定初始值。第 19 行透
過呼叫成員函式 setTime 設定新的時間,第 23 和 25 行再次以兩種格式列印時間。
1 // Fig. 9.3: fig09_03.cpp
2 // Program to test class Time.
3 // NOTE: This file must be compiled with Time.cpp.
4 #include <iostream>
5 #include <stdexcept> // for invalid_argument exception class
6 #include "Time.h" // include definitionof class Time from Time.h
7 using namespace std;
8
9 int main()
10 {
11
Time t; // instantiate object t of class Time
12
13
// output Time object t's initial values
14
cout << "The initial universal time is ";
15
t.printUniversal(); // 00:00:00
16
cout << "\nThe initial standard time is ";
17
t.printStandard(); // 12:00:00 AM
18
19
t.setTime( 13, 27, 6 ); // change time
20
21
// output Time object t's new values
22
cout << "\n\nUniversal time after setTime is ";
23
t.printUniversal(); // 13:27:06
24
cout << "\nStandard time after setTime is ";
25
t.printStandard(); // 1:27:06 PM
26
27
// attempt to set the time with invalid values
28
try
{
29
30
t.setTime( 99, 99, 99 ); // all values out of range
} // end try
31
32
catch ( invalid_argument &e )
{
33
34
cout << "\n\nException: " << e.what() << endl;
} // end catch
35
36
37
// output t's values after specifying invalid values
38
cout << "\nAfter attempting invalid settings:"
39
<< "\nUniversal time: ";
40
t.printUniversal(); // 13:27:06
41
cout << "\nStandard time: ";
42
t.printStandard(); // 1:27:06 PM
43
cout << endl;
44 } // end main
The initial universal time is 00:00:00
The initial standard time is 12:00:00 AM
Universal time after setTime is 13:27:06
Standard time after setTime is 1:27:06 PM
Exception: hour, minute and/or second was out of range
圖 9.3
類別 Time 的測試程式 (1/2)
第九章
類別:深入了解;拋出異常
After attempting invalid settings:
Universal time: 13:27:06
Standard time: 1:27:06 PM
圖 9.3
類別 Time 的測試程式 (2/2)
以無效值呼叫 setTime
為展示 setTime 方法是如何驗證它的參數,第 30 行呼叫 setTime 函式並傳入無效的時、
分、秒值:99。此敘述被放置在一個 try 的區塊中 ( 第 28-31 行 ),在該區塊,setTime 會拋出
invalid_argument 異常,它會這麼做,因為引數均無效。當這種情況發生時,異常在第 32-35
行被捕獲,第 34 行藉由呼叫異常的成員函式 what,顯示錯誤訊。第 38-42 再次以兩種格式輸
出時間,以確認雖提供無效的引數,但 setTime 沒變更時間。
展望:組合 (composition) 與繼承 (inheritance)
我們不需完全從頭建立新的類別。類別可以包含其他類別的物件作為其成員,或衍
生 (derived) 自其他類別,讓其他類別提供新類別的屬性和行為。這稱為軟體的再利用
(software reuse),可大大提高程式設計者的生產力,並讓程式碼更容易維護。類別以其他類
別的物件作為資料成員,稱為組合 (composition,又稱聚合 aggregation),我們將在第 9.11
節討論這個主題。類別衍生自其他類別,稱為繼承 (inheritance),我們將在第 11 章討論。
物件大小
剛接觸物件導向程式設計的新手常會認為,物件一定很大,因為它包含資料成員和成員
函式。邏輯上來說,這沒有錯:程式設計者很自然地會把物件想成是包含資料和函式的集合
體 ( 特別是,我們的解說也許更增強了這個信念 ),但實際上,這個觀念是錯誤的。
效能要訣 9.2
物件如果包含函式的話,的確是蠻大的;但它只包含資料,所以其實小很多。編譯器
只會為整個類別建立一套成員函式,該類別所有物件共享此成員函式。每個物件擁有
屬於自己的資料。函式的程式碼是不可修改的,因此可讓類別的所有物件共用。
9.3 類別範圍及存取類別成員
類別的資料成員和成員函式都隸屬類別範圍 (class scope)。非成員函式,預設屬於全域
命名空間範圍 (global namespace scope)。( 第 23.4 節詳細介紹命名空間 )。
在類別範圍中,類別成員函式可直接使用名稱存取類別成員。在類別範圍之外,可透
過物件的 handle ( 控制碼 ) 取用類別的 public 成員。物件名稱、物件的參考、指向物件的指
標,都可稱作是物件的 handle。有了物件 handle 才可操控物件。顧客端程式只要掌控了物件
的 handle,如參考和指標,就可透過介面 ( 指的是成員函式 ) 使用物件。[ 在第 9.13 節中會談
到,每當取用物件內的資料成員或成員函式時,編譯器會插入隱式 handle。]
9-9
9-10
C++程式設計藝術
類別範圍和區塊範圍
在成員函式中宣告的變數具有區塊範圍 (block scope),只能在該函式中使用。如果某
成員函式定義了一個變數,其名稱與同類別的類別變數名稱相同,則在此函式範圍中,類別
範圍的變數會被隱藏。此時,可以在變數前面放置類別名稱,以及範圍解析運算子 (::),存
取被隱藏的變數。被隱藏的全域變數可以透過單元範圍解析運算子存取 ( 請參閱第 6 章 )。
點號 (.) 和箭號 (->) 成員選擇運算子
將點號 (.) 成員選擇運算子 (member selction operator) 置於物件名稱或物件的參考之
後,就可存取該物件的成員。箭號成員選擇運算子 (->,arrow member selection operator)
則可搭配指向物件的指標,存取物件的成員。
使用物件、參考、指標存取 public 類別成員
一個類別 Account,有一個 public 成員函式 setbalance,已知下列宣告:
Account account; // an Account object
// accountRef refers to an Account object
Account &accountRef = account;
// accountPtr points to an Account object
Account *accountPtr = &account;
您可使用點號 (.) 和箭號 (->),執行下列運算
// call setBalance via the Account object
account.setBalance( 123.45 );
// call setBalance via a reference to the Account object
accountRef.setBalance( 123.45 );
// call setBalance via a pointer to the Account object
accountPtr->setBalance( 123.45 );
9.4 Access 函式與 Utility 函式
存取函式
存取函式 (access functions) 可以用來讀取或顯示類別的資料。存取函式另一項常用的
功能是用來測試條件式的真偽,這類函式又稱為判斷函式 (predicate function)。舉例來說,
許多容器類別 (container class) 都有一個叫做 isEmpty 的成員函式;所謂容器類別,是指包含
許多物件的類別,例如 vector。程式嘗試由容器物件讀取項目之前,會先以 isEmpty 測試此
容器是否為空的。同樣的,判斷函式 isFull 可用來測試容器類別物件,是否已無額外的空間
存放資料。對 Time 類別來說,有用的判斷函式應該是 isAM 和 isPM。
工具函式
工具函式 (utility function) 又稱為助手函式 (helper function),是 private 型的成員函式,
專供類別的其他函式使用。工具函式之所以宣告為 private,是不想讓類別的顧客端程式呼
第九章
類別:深入了解;拋出異常
叫。在一個類別中,若數個成員函式都會用到一些共同的程式碼,那麼,就該將此通用程式
碼組合成工具函式。
9.5 類別 Time 個案研究:建構子與預設引數
在圖 9.4-9.6 中,強化了 Time 類別,展現其如何將引數默默 (implictly) 傳遞給建構子。
圖 9.2 定義的建構子,將 hour、minute、second 初始化為 0 ( 午夜時分,通用格式 )。
與一般函式相同,建構子可以擁有預設引數。圖 9.4 的第 13 行宣告 Time 建構子,其中
包含預設引數,將每個引數的預設值設定為 0,然後傳給建構子。建構子以 explicit 方式宣
告。我們會在第 10.13 節詳述 explicite 建構子。
1 // Fig. 9.4: Time.h
2 // Time class containing a constructor with default arguments.
3 // Member functions defined in Time.cpp.
4
5 // prevent multiple inclusions of header
6 #ifndef TIME_H
7 #define TIME_H
8
9 // Time class definition
10 class Time
11 {
12 public:
13
explicit Time( int = 0, int = 0, int = 0 ); // default constructor
14
15
// set functions
16
void setTime( int, int, int ); // set hour, minute, second
17
void setHour( int ); // set hour (after validation)
18
void setMinute( int ); // set minute (after validation)
19
void setSecond( int ); // set second (after validation)
20
21
// get functions
22
unsigned int getHour() const; // return hour
23
unsigned int getMinute() const; // return minute
24
unsigned int getSecond() const; // return second
25
26
void printUniversal() const; // output time in universal-time format
27
void printStandard() const; // output time in standard-time format
28 private:
29
unsigned int hour; // 0 - 23 (24-hour clock format)
30
unsigned int minute; // 0 - 59
31
unsigned int second; // 0 - 59
32 }; // end class Time
33
34 #endif
圖 9.4
類別 Time,具預設引數的建構子
圖 9.5 中第 10-13 行,定義新版本的 Time 建構子,接收 hour、minute 和 second 等參數,
分別用來初始化 private 資料成員 hour、minute 和 second。建構子的預設引數能確保即使呼叫
時未提供引數,建構子仍能為類別物件的資料成員設定初始值。每個引數都有預設值的建構
9-11
9-12
C++程式設計藝術
子又稱預設建構子 (default constrtutor);這種建構子呼叫時不需任何引數。類別最多只能有
一個預設建構子。這個版本中的 Time 類別,為每個資料成員提供 set 和 get 函式。Time 建構
子呼叫 setTime,而該函式又呼叫 setHour、setMinute 和 setSecond 檢查和設定資料成員的值。
軟體工程觀點 9.5
具預設引數的函式,其預設值若有變更,使用該函式的顧客端程式碼均需重新編譯
( 確保程式能夠正確執行 )。
1 // Fig. 9.5: Time.cpp
2 // Member-function definitions for class Time.
3 #include <iostream>
4 #include <iomanip>
5 #include <stdexcept>
6 #include "Time.h" // include definition of class Time from Time.h
7 using namespace std;
8
9 // Time constructor initializes each data member
10 Time::Time( int hour, int minute, int second )
11 {
12
setTime( hour, minute, second ); // validate and set time
13 } // end Time constructor
14
15 // set new Time value using universal time
16 void Time::setTime( int h, int m, int s )
17 {
18
setHour( h ); // set private field hour
19
setMinute( m ); // set private field minute
20
setSecond( s ); // set private field second
21 } // end function setTime
22
23 // set hour value
24 void Time::setHour( int h )
25 {
26
if ( h >= 0 && h < 24 )
27
hour = h;
28
else
29
throw invalid_argument( "hour must be 0-23" );
30 } // end function setHour
31
32 // set minute value
33 void Time::setMinute( int m )
34 {
35
if ( m >= 0 && m < 60 )
36
minute = m;
37
else
38
throw invalid_argument( "minute must be 0-59" );
39 } // end function setMinute
40
41 // set second value
42 void Time::setSecond( int s )
43 {
圖 9.5
Time 類別的成員函式 (1/2)
第九章
類別:深入了解;拋出異常
44
if ( s >= 0 && s < 60 )
45
second = s;
46
else
47
throw invalid_argument( "second must be 0-59" );
48 } // end function setSecond
49
50 // return hour value
51 unsigned int Time::getHour() const
52 {
53
return hour;
54 } // end function getHour
55
56 // return minute value
57 unsigned int Time::getMinute() const
58 {
59
return minute;
60 } // end function getMinute
61
62 // return second value
63 unsigned int Time::getSecond() const
64 {
65
return second;
66 } // end function getSecond
67
68 // print Time in universal-time format (HH:MM:SS)
69 void Time::printUniversal() const
70 {
71
cout << setfill( '0' ) << setw( 2 ) << getHour() << ":"
72
<< setw( 2 ) << getMinute() << ":" << setw( 2 ) << getSecond();
73 } // end function printUniversal
74
75 // print Time in standard-time format (HH:MM:SS AM or PM)
76 void Time::printStandard() const
77 {
78
cout << ( ( getHour() == 0 || getHour() == 12 ) ? 12 : getHour() % 12 )
79
<< ":" << setfill( '0' ) << setw( 2 ) << getMinute()
80
<< ":" << setw( 2 ) << getSecond() << ( hour < 12 ? " AM" : " PM" );
} // end function printStandard
81
圖 9.5
Time 類別的成員函式 (2/2)
圖 9.5,第 12 行,建構子呼叫成員函式 setTime,並將傳入建構子的參數作為函式的
引數 ( 這些值就是預設值 )。setTime 呼叫 setHour,確保 hour 落在 0-23 範圍內,然後呼叫、
setMinute 和 setSecond,確保 minute 和 second 落在 0-59 範圍內。函式 setHour ( 第 24-30 行 )、
setMinute ( 第 33-39 行 ) 和 setSecond ( 第 42-48 行 ),若收到超出範圍的值,各自拋出異常。
圖 9.6 的 main 函式會初始化 5 個 Time 物件,其中第一個物件建構子使用三個隱式的預
設值呼叫建構子 ( 第 10 行 ),第二個物件只傳入一個引數 ( 第 11 行 ),第三個物件傳入兩個引
數 ( 第 12 行 ),第四個物件傳入三個引數 ( 第 13 行 ),而最後一個物件傳入三個無效的引數
( 第 38 行 )。然後,程式以通用和標準時間格式,顯示每個物件的內容。Time 物件 t5 ( 第 38
行 ),程式會顯示錯誤訊息,因為建構子的引數超出範圍。
9-13
9-14
C++程式設計藝術
1 // Fig. 9.6: fig09_06.cpp
2 // Constructor with default arguments.
3 #include <iostream>
4 #include <stdexcept>
5 #include "Time.h" // include definition of class Time from Time.h
6 using namespace std;
7
8 int main()
9 {
Time t1; // all arguments defaulted
10
Time t2( 2 ); // hour specified; minute and second defaulted
11
Time t3( 21, 34 ); // hour and minute specified; second defaulted
12
Time t4( 12, 25, 42 ); // hour, minute and second specified
13
14
cout << "Constructed with:\n\nt1: all arguments defaulted\n ";
15
t1.printUniversal(); // 00:00:00
16
cout << "\n ";
17
t1.printStandard(); // 12:00:00 AM
18
19
cout << "\n\nt2: hour specified; minute and second defaulted\n ";
20
t2.printUniversal(); // 02:00:00
21
cout << "\n ";
22
t2.printStandard(); // 2:00:00 AM
23
24
cout << "\n\nt3: hour and minute specified; second defaulted\n ";
25
t3.printUniversal(); // 21:34:00
26
cout << "\n ";
27
t3.printStandard(); // 9:34:00 PM
28
29
cout << "\n\nt4: hour, minute and second specified\n ";
30
t4.printUniversal(); // 12:25:42
31
cout << "\n ";
32
t4.printStandard(); // 12:25:42 PM
33
34
// attempt to initialize t6 with invalid values
35
36
try
{
37
Time t5( 27, 74, 99 ); // all bad values specified
38
} // end try
39
40
catch ( invalid_argument &e )
{
41
cerr << "\n\nException while initializing t5: " << e.what() << endl;
42
} // end catch
43
} // end main
44
Constructed with:
t1: all arguments defaulted
00:00:00
12:00:00 AM
t2: hour specified; minute and second defaulted
02:00:00
2:00:00 AM
t3: hour and minute specified; second defaulted
21:34:00
9:34:00 PM
t4: hour, minute and second specified
12:25:42
12:25:42 PM
Exception while initializing t5: hour must be 0-23
圖 9.6
使用預設引數的建構子
第九章
類別:深入了解;拋出異常
Time 類別的 Set、Get 函式與建構子
類別 Time 的 set 和 get 函式在類別主體中被呼叫。尤其是 setTime 函式 ( 圖 9.5 第 16-21 行 )
又呼叫 setHour、setMinute 和 setSecond 等函式,而函式 printUniversal 和 printStandard 分別在
第 71-72 行和第 78-80 行呼叫 getHour、getMinute 和 getSecond 三個函式。其實,這些函式都
能直接存取類別的 private 資料,無需呼叫 set 及 get 函式。然而,請考慮改變內部時間表示法
的情況:原來我們用三個 int 表示時、分、秒 ( 共需 12 個位元組,系統使用 4 個位元組表示一
個整數 ),假設現在改用一個 int,表示自午夜以來經過的秒數 ( 只需四個位元組 )。在這個情
況下,只有直接存取 private 資料的函式需要修改,即存取 hour、minute 和 second 的 set 和 get
函式,但 setTime、printUniversal 和 printStandard 等函式並不需改變,因為它們未直接存取資
料。當改變類別的實作方式時,以這種方式設計類別,可降低程式發生錯誤的可能。
同樣的,我們也可以在 Time 建構子中直接加入函式 setTime 中的敘述。這麼做能稍微
提高執行效率,因為可消除呼叫建構子和 setTime 的額外負擔。不過,若把敘述複製到建構
子或函式中,稍後若想改變內部的資料表示法,就會比較困難。如果 Time 建構子直接呼叫
setTime,以後 setTime 的程式碼若有任何變更,只需修改程式碼一次即可。當改變類別的實
作方式時,這種方式可降低發生錯誤的可能。
軟體工程觀點 9.6
若類別的成員函式已經提供建構子 ( 或其他成員函式 ) 所需的全部或部分的功能,則
可讓建構子 ( 或其他的成員函式 ) 直接呼叫該成員函式。這麼做能讓程式碼更容易維
護,並降低因修改程式碼實作而發生錯誤的可能性。總之,避免重複的程式碼。
程式中常犯的錯誤 9.1
建構子可以呼叫類別的其他成員函式,如 set 或 get 函式,但建構子原是用來設定物
件的初始值,此時資料成員可能尚未處於合法的狀態。在資料成員正確設定初始值
之前使用其值,會造成邏輯錯誤。
C++11:使用初始化列表呼叫建構子
回想一下 4.10 節,C++11 現在提供了一個可用於初始化任何變數,名為初始化列表的統
一初始化語法。圖 9.6 的第 11-13 行,可使用初始化列表如下:
Time t2{ 2 }; // hour specified; minute and second defaulted
Time t3{ 21, 34 }; // hour and minute specified; second defaulted
Time t4{ 12, 25, 42 }; // hour, minute and second specified
或
Time t2 = { 2 }; // hour specified; minute and second defaulted
Time t3 = { 21, 34 }; // hour and minute specified; second defaulted
Time t4 = { 12, 25, 42 }; // hour, minute and second specified
沒有等號 (=) 的語法較好。
9-15
9-16
C++程式設計藝術
C++11:多載建構子和委派建構子
第 6.18 節展示了如何用多載函數。類別的建構子和成員函式也可以多載。多載建構子
主要目的在提供不同數量,或不同型態的引數。要多載建構子,需在類別的定義中,為每
個多載版本提供原型,同時也要為每個多載版本分別定義建構子。這也適用於該類別的成
員函數。
在圖 9.4-9.6,Time 類別的建構子帶三個參數,對每個參數都有預設的引數與之對應。
我們可以定義四個建構子來取代原先的一個建構子,四個建構子原型如下:
Time(); // default hour, minute and second to 0
Time( int ); // initialize hour; default minute and second to 0
Time( int, int ); // initialize hour and minute; default second to 0
Time( int, int, int ); // initialize hour, minute and second
正如一個建構子可以呼叫類別的其他成員函數來執行任務,C++11 現在允許建構子呼叫
同類別的其他建構子。執行呼叫的建構子就是所謂的委託建構子 (delegating constructor),
代表它將工作委託給其他其他的建構子。這個機制相當有用,尤其當幾個多載函式有一些共
同的工作要執行,這些工作已經定義由 private 的工具函式執行,當所有的多載函式大都需
要呼叫此函式時,委託建構子的功能就顯現出來了。
以上便宣告了 Time 類別的四個建構子,前三個建構子可將工作委派給有三個整數引數
的建構子,對額外的參數,傳遞 0 作為預設值。要做到此,可以使用類別名稱執行成員初始
化,如下所示:
Time::Time()
: Time( 0, 0, 0 ) // delegate to Time( int, int, int )
{
} // end constructor with no arguments
Time::Time( int hour )
: Time( hour, 0, 0 ) // delegate to Time( int, int, int )
{
} // end constructor with one argument
Time::Time( int hour, int minute )
: Time( hour, minute, 0 ) // delegate to Time( int, int, int )
{
} // end constructor with two arguments
9.6 解構子
解構子 (destructor) 是一種特別的成員函式。類別解構子的名稱,是類別名稱前面加上
波浪符號 (~,tilde character)。如此命名有直覺上的意義:在後面的章節中,讀者將會瞭解
波浪符號是位元運算的補數運算子 (complement),解構子和建構子也有互補的意義存在。解
構子可能沒有參數與回傳型態。
物件清除時,程式會默默自動呼叫類別的解構子。舉例來說,程式離開該物件的範圍
時,此範圍指的是物件被實體化的區塊。解構子本身不會釋放物件占用的記憶體;它負責執
行結尾工作 (termination housekeeping),結束之後,系統才會收回物件占用的記憶體,供
其他物件使用。
第九章
類別:深入了解;拋出異常
目前為止,雖然我們尚未在類別定義任何解構子,實際上每個類別都有一個解構子。若
未定義解構子,編譯器會自動產生空的解構子。[ 請注意:稍後我們會討論到,這種自動建立
的解構子,對由組合 ( 第 9.11 節 ) 和繼承 ( 第 11 章 ) 而成的物件,會執行重要的運算。] 在第
10 章中,我們對那些會動態配置記憶體或使用其他系統資源 ( 如磁碟檔案,在第 14 章中會學
到的類別物件,建立適當的解構子 )。我們在第 10 章將討論如何動態配置和回收記憶體空間。
9.7 建構子與解構子的呼叫時機
建構子和解構子會由編譯器默默自動呼叫。它們的呼叫順序,與程式的執行進入和離開
物件被建立的程式範圍的順序有關。一般來說,解構子的呼叫順序,與建構子的呼叫順序相
反,但如圖 9.7-9.9 所見,物件的儲存類別會影響解構子的呼叫順序。
全域範圍物件的建構子和解構子
在任何函式 ( 包括 main) 開始執行之前,程式首先呼叫全域範圍物件的建構子。( 在多
個檔案之中,多個全域物件建構子間的執行順序無法確定 )。相對應的解構子會在 main 結束
時被呼叫。函式 exit 會強迫程式立刻終止執行,不呼叫區域物件的解構子。程式輸出發生錯
誤,或程式無法開啟要處理的檔案時,我們常會呼叫 exit 終止程式執行。函式 abort 的功能
和 exit 很像,不同處在於呼叫 abort 時,程式會立刻終止,不會呼叫任何物件的解構子。函式
abort 通常用來表示程式不正常結束執行。若想更深入瞭解 exit 和 abort 函式,請參閱附錄 F。
區域物件的建構子和解構子
程式執行到定義區域物件處時,會呼叫其建構子。相對地,當程式離開物件的範圍時
( 離開定義該物件的區塊 ),會呼叫對應的解構子。對區域物件來說,程式會在進入和離開其
範圍時,呼叫其建構子和解構子各一次。但我們若透過呼叫 exit 或 abort 終止程式執行,就不
會呼叫區域物件的解構子。
靜態區域物件的建構子和解構子
當程式第一次執行 static 區域物件的定義處時,會唯一呼叫其建構子一次,而於 main 結
束或程式呼叫 exit 時,呼叫對應的解構子。全域和 static 物件的清除順序,恰與建立的順序
相反。程式若呼叫 abort 函式終止程式,就不會呼叫 static 物件的解構子。
示範建構子和解構子呼叫時機
圖 9.7-9.9 的 程 式 示 範,CreateAndDestroy 類 別 ( 圖 9.7 和 9.8) 物 件 建 構 子 和 解 構 子
的 呼 叫 順 序。 該 類 別 在 main 中, 有 各 種 不 同 範 圍 及 不 同 儲 存 類 別 的 物 件 變 數。 每 個
CreateAndDestroy 類別的物件都包含一個整數 (objectID) 和一個字串 (message),作為識別之
用 ( 圖 9.7,第 16-17 行 ) 。這個簡單的類別是為教學目的而設計的。圖 9.8 中解構子的第 19
行,會判斷被清除物件的 objectID 值是否為 1 或 6 ( 第 19 行 ),若是,則輸出換行字元。這讓
程式的輸出清晰且容易追蹤結果。
9-17
9-18
C++程式設計藝術
1 // Fig. 9.7: CreateAndDestroy.h
2 // CreateAndDestroy class definition.
3 // Member functions defined in CreateAndDestroy.cpp.
4 #include <string>
5 using namespace std;
6
7 #ifndef CREATE_H
8 #define CREATE_H
9
10 class CreateAndDestroy
11 {
12 public:
13
CreateAndDestroy( int, string ); // constructor
14
~CreateAndDestroy(); // destructor
15 private:
16
int objectID; // ID number for object
17
string message; // message describing object
18 }; // end class CreateAndDestroy
19
20 #endif
圖 9.7
CreateAndDestroy 類別的定義
1 // Fig. 9.8: CreateAndDestroy.cpp
2 // CreateAndDestroy class member-function definitions.
3 #include <iostream>
4 #include "CreateAndDestroy.h" // include CreateAndDestroy class definition
5 using namespace std;
6
7 // constructor sets object's ID number and descriptive message
8 CreateAndDestroy::CreateAndDestroy( int ID, string messageString )
9
: objectID( ID ), message( messageString )
10 {
11
cout << "Object " << objectID << "
constructor runs
"
12
<< message << endl;
13 } // end CreateAndDestroy constructor
14
15 // destructor
16 CreateAndDestroy::~CreateAndDestroy()
17 {
18
// output newline for certain objects; helps readability
19
cout << ( objectID == 1 || objectID == 6 ? "\n" : "" );
20
21
cout << "Object " << objectID << "
destructor runs
"
22
<< message << endl;
23 } // end ~CreateAndDestroy destructor
圖 9.8
CreateAndDestroy 的成員函式定義
圖 9.9 在全域定義物件 first ( 第 10 行 )。程式會在執行 main 中任何敘述之前,呼叫其建構
子。程式結束時,等其他所有自動儲存類別物件的建構子都呼叫後,才呼叫其解構子。
第九章
類別:深入了解;拋出異常
1 // Fig. 9.9: fig09_09.cpp
2 // Demonstrating the order in which constructors and
3 // destructors are called.
4 #include <iostream>
5 #include "CreateAndDestroy.h" // include CreateAndDestroy class definition
6 using namespace std;
7
8 void create( void ); // prototype
9
10 CreateAndDestroy first( 1, "(global before main)" ); // global object
11
12 int main()
13 {
14
cout << "\nMAIN FUNCTION: EXECUTION BEGINS" << endl;
15
CreateAndDestroy second( 2, "(local automatic in main)" );
16
static CreateAndDestroy third( 3, "(local static in main)" );
17
18
create(); // call function to create objects
19
20
cout << "\nMAIN FUNCTION: EXECUTION RESUMES" << endl;
21
CreateAndDestroy fourth( 4, "(local automatic in main)" );
22
cout << "\nMAIN FUNCTION: EXECUTION ENDS" << endl;
23 } // end main
24
25 // function to create objects
26 void create( void )
27 {
28
cout << "\nCREATE FUNCTION: EXECUTION BEGINS" << endl;
29
CreateAndDestroy fifth( 5, "(local automatic in create)" );
30
static CreateAndDestroy sixth( 6, "(local static in create)" );
31
CreateAndDestroy seventh( 7, "(local automatic in create)" );
32
cout << "\nCREATE FUNCTION: EXECUTION ENDS" << endl;
33 } // end function create
Object 1 constructor runs (global before main)
MAIN FUNCTION: EXECUTION BEGINS
Object 2 constructor runs (local automatic in main)
Object 3 constructor runs (local static in main)
CREATE FUNCTION: EXECUTION BEGINS
Object 5 constructor runs (local automatic in create)
Object 6 constructor runs (local static in create)
Object 7 constructor runs (local automatic in create)
CREATE FUNCTION: EXECUTION ENDS
Object 7 destructor runs (local automatic in create)
Object 5 destructor runs (local automatic in create)
MAIN FUNCTION: EXECUTION RESUMES
Object 4 constructor runs (local automatic in main)
MAIN FUNCTION: EXECUTION ENDS
Object 4 destructor runs (local automatic in main)
Object 2 destructor runs (local automatic in main)
Object 6 destructor runs (local static in create)
Object 3 destructor runs (local static in main)
Object 1 destructor runs (global before main)
圖 9.9
建構子和解構子的呼叫順序
9-19
9-20
C++程式設計藝術
函式 main ( 第 12-23 行 ) 宣告了三個物件。物件 second ( 第 15 行 ) 和 fourth ( 第 21 行 ) 都是區
域自動物件,而物件 third ( 第 16 行 ) 則是 static 區域物件。這些物件的建構子都是在程式進入這
些物件的宣告處時,才進行呼叫。物件 fourth 和 second 的解構子會在 main 函式結束時以呼叫建
構子相反的順序呼叫 。物件 third 是 static 物件,它的生命週期會一直延續到程式終止。程式會
在呼叫全域物件 first 的解構子之前,在其他物件解構子執行後,呼叫物件 third 的解構子。
函式 create ( 第 26-33 行 ) 宣告三個物件,區域自動物件 fifth ( 第 29 行 ) 和 seventh ( 第 31
行 ) 以及 static 區域物件 sixth ( 第 30 行 )。seventh 和 fifth 的解構子會在 create 函式結束時,按
建構子的相反順序被呼叫。sixth 是 static 物件,所以會一直存在,直到程式結束為止。因
此,程式會在呼叫 third 和 first 的解構子之前,清除完畢其他物件之後,呼叫 sixth 的解構子。
9.8 類別Time個案研究:微妙陷阱,傳回private資料成員的參考
物件的參考是該物件的別名,因此可用在指派敘述的左邊,接受設定的數值。在此情況
下,參考會作為 lvalue,接受要設定的數值。有一個使用這項功能的方法,那就是透過類別
的 public 成員函式,傳回指向該類別 private 資料成員的參考。若傳回 const 參考,就無法用
作可修改的 lvalue。
圖 9.10-9.12 的 程 式 藉 簡 化 過 的 Time 類 別 ( 圖 9.10 和 9.11) , 示 範 使 用 成 員 函 式
badSetHour ( 宣告於圖 9.10,第 15 行,定義於圖 9.11,第 37-45 行 ) 傳回指向 private 資料成
員的參考。這個傳回動作,會讓函式 badSetHour 產生 private 資料成員 hour 的別名。這麼做
會暴露 private 資料成員,甚至讓它成為指派敘述中的 lvalue,讓外部程式碼任意修改類別的
private 資料。函式若傳回指向 private 資料成員的指標,也會有同樣的問題。
1 // Fig. 9.10: Time.h
2 // Time class declaration.
3 // Member functions defined in Time.cpp
4
5 // prevent multiple inclusions of header
6 #ifndef TIME_H
7 #define TIME_H
8
9 class Time
10 {
11 public:
12
explicit Time( int = 0, int = 0, int = 0 );
13
void setTime( int, int, int );
14
unsigned int getHour() const;
15
unsigned int &badSetHour( int ); // dangerous reference return
16 private:
17
unsigned int hour;
18
unsigned int minute;
19
unsigned int second;
20 }; // end class Time
21
22 #endif
圖 9.10 Time 類別的宣告
第九章
類別:深入了解;拋出異常
1 // Fig. 9.11: Time.cpp
2 // Time class member-function definitions.
3 #include <stdexcept>
4 #include "Time.h" // include definition of class Time
5 using namespace std;
6
7 // constructor function to initialize private data; calls member function
8 // setTime to set variables; default values are 0 (see class definition)
9 Time::Time( int hr, int min, int sec )
10 {
11
setTime( hr, min, sec );
12 } // end Time constructor
13
14 // set values of hour, minute and second
15 void Time::setTime( int h, int m, int s )
16 {
17
// validate hour, minute and second
18
if ( ( h >= 0 && h < 24 ) && ( m >= 0 && m < 60 ) &&
( s >= 0 && s < 60 ) )
19
{
20
21
hour = h;
22
minute = m;
23
second = s;
} // end if
24
25
else
26
throw invalid_argument(
27
"hour, minute and/or second was out of range" );
28 } // end function setTime
29
30 // return hour value
31 unsigned int Time::getHour() const
32 {
33
return hour;
34 } // end function getHour
35
36 // poor practice: returning a reference to a private data member.
37 unsigned int &Time::badSetHour( int hh )
38 {
39
if ( hh >= 0 && hh < 24 )
40
hour = hh;
41
else
42
throw invalid_argument( "hour must be 0-23" );
43
44
return hour; // dangerous reference return
45 } // end function badSetHour
圖 9.11 Time 成員函式的定義
圖 9.12 宣告型 Time 物件 t ( 第 10 行 ) 和參考 hourRef ( 第 13 行 ) ,並將呼叫 t.badSetHour (20)
傳回的參考,指定給 hourRef。程式第 15 行顯示別名 hourRef 的值。這顯示 hourRef 破壞了類別
的封裝性 (encapsulation),因為 main 中的敘述應不得存取類別的 private 資料。接下來,第 16 行
透過這個別名,把 hour 的值設成 30 ( 非有效值 ),而第 17 行顯示 getHour 傳回的這個值,顯示
透過 hourRef 的確更改了物件 t 的 private 成員的值。最後第 21 行使用 badSetHour 函式本身作為
lvalue,將回傳的參考設為 74 ( 另一個不合法的值 )。第 26 行再次顯示 getHour 函式傳回的值,
確認將值指定給第 21 行函式呼叫的傳回值結果,的確修改了 Time 物件 t 的 private 資料。
9-21
9-22
C++程式設計藝術
軟體工程觀點 9.7
傳回指向 private 物件的參考或指標,會破壞類別的封裝,讓使用類別的程式碼依賴
物件的內部資料表示方式。但有時候,這麼做卻是恰當的,在第 10.10 節,我們會
以建立自訂義的陣列為例來說明。
1 // Fig. 9.12: fig09_12.cpp
2 // Demonstrating a public member function that
3 // returns a reference to a private data member.
4 #include <iostream>
5 #include "Time.h" // include definition of class Time
6 using namespace std;
7
8 int main()
9 {
10
Time t; // create Time object
11
12
// initialize hourRef with the reference returned by badSetHour
13
unsigned int &hourRef = t.badSetHour( 20 ); // 20 is a valid hour
14
15
cout << "Valid hour before modification: " << hourRef;
16
hourRef = 30; // use hourRef to set invalid value in Time object t
17
cout << "\nInvalid hour after modification: " << t.getHour();
18
19
// Dangerous: Function call that returns
20
// a reference can be used as an lvalue!
21
t.badSetHour( 12 ) = 74; // assign another invalid value to hour
22
23
cout << "\n\n*************************************************\n"
24
<< "POOR PROGRAMMING PRACTICE!!!!!!!!\n"
25
<< "t.badSetHour( 12 ) as an lvalue, invalid hour: "
26
<< t.getHour()
27
<< "\n*************************************************" << endl;
28 } // end main
Valid hour before modification: 20
Invalid hour after modification: 30
*************************************************
POOR PROGRAMMING PRACTICE!!!!!!!!
t.badSetHour( 12 ) as an lvalue, invalid hour: 74
*************************************************
圖 9.12 傳回指向 private 資料成員的參考
9.9 預設逐成員指派
指派運算子 (=) 可將某個物件指定給另一個相同類別的物件。預設情況下,該運算子會
執行逐成員指派 (memberwise assignment) 又稱作拷貝指派 (copy assignment):將指派運
算子右邊物件的每個成員,逐一指定給運算子左邊物件的每個成員。圖 9.13-9.14 定義類別
Date。圖 9.15 的第 18 行透過預設的逐成員指派 (default memberwise assignment),把 Date
物件 date1 的資料成員設給型態同為 Date 的物件 date2 的對應資料成員。意思是說,date1 的
第九章
類別:深入了解;拋出異常
成員 month 會設給 date2 的成員 month;date1 的成員 day 設給 date2 的成員 day;date1 的成員
year 會設給 date2 的成員 year。[ 請注意:類別的資料成員若為指向動態配置記憶體的指標,
逐成員指派可能會造成嚴重錯誤;我們將在第 10 章討論這些問題和解決方法 )。]
1 // Fig. 9.13: Date.h
2 // Date class declaration. Member functions are defined in Date.cpp.
3
4 // prevent multiple inclusions of header
5 #ifndef DATE_H
6 #define DATE_H
7
8 // class Date definition
9 class Date
10 {
11 public:
12
explicit Date( int = 1, int = 1, int = 2000 ); // default constructor
13
void print();
14 private:
15
unsigned int month;
16
unsigned int day;
17
unsigned int year;
18 }; // end class Date
19
20 #endif
圖 9.13 Date 類別的宣告
1 // Fig. 9.14: Date.cpp
2 // Date class member-function definitions.
3 #include <iostream>
4 #include "Date.h" // include definition of class Date from Date.h
5 using namespace std;
6
7 // Date constructor (should do range checking)
8 Date::Date( int m, int d, int y )
9
: month( m ), day( d ), year( y )
10 {
11 } // end constructor Date
12
13 // print Date in the format mm/dd/yyyy
14 void Date::print()
15 {
16
cout << month << '/' << day << '/' << year;
17 } // end function print
圖 9.14 Date 成員函式定義
1
2
3
4
5
6
// Fig. 9.15: fig09_15.cpp
// Demonstrating that class objects can be assigned
// to each other using default memberwise assignment.
#include <iostream>
#include "Date.h" // include definition of class Date from Date.h
using namespace std;
圖 9.15 類別物件可利用逐成員指派執行物件指派 (1/2)
9-23
9-24
C++程式設計藝術
7
8 int main()
9 {
Date date1( 7, 4, 2004 );
10
Date date2; // date2 defaults to 1/1/2000
11
12
cout << "date1 = ";
13
date1.print();
14
cout << "\ndate2 = ";
15
date2.print();
16
17
date2 = date1; // default memberwise assignment
18
19
cout << "\n\nAfter default memberwise assignment, date2 = ";
20
date2.print();
21
cout << endl;
22
23 } // end main
date1 = 7/4/2004
date2 = 1/1/2000
After default memberwise assignment, date2 = 7/4/2004
圖 9.15 類別物件可利用逐成員指派執行物件指派 (2/2)
物件可以作為引數傳入函式,也可以從函式傳回。在預設情況下,物件的傳入和傳回動
作以傳值呼叫進行,即程式會傳入和傳回物件的副本。因此,C++ 會建立新的物件,透過複
製建構子 (copy constructor) 將原物件的值設給新的物件。編譯器會為所有類別提供預設的
複製建構子,用來把原物件的每個成員,複製到新物件的對應成員。和逐成員指派一樣,類
別資料成員若包含指向動態配置記憶體的指標,複製建構子可能會造成嚴重的問題。我們將
在第 10 章談到如何自行定義複製建構子,以便適當處理指向動態配置記憶體的指標成員。
9.10 const 物件和 const 成員函式
在這一節中,我們要探討如何對物件套用最小權限原則。有些物件需要被修改,有些則
不需要。程式設計師可利用關鍵字 const,指出該物件是不可修改的,任何嘗試修改的動作
都會造成編譯的錯誤。以下敘述
const Time noon( 12, 0, 0 );
宣告類別 Time 的 const 物件 noon,並將其初始值設為中午 12 點。此類別可用來建立常
數物件,也可用來建立非常數物件。
軟體工程觀點 9.8
任何嘗試修改 const 物件的動作,都會在編譯期被發現,故不會造成執行期錯誤。
效能要訣 9.3
適當地將變數或物件宣告為 const 能提高程式的執行效能,因為編譯器能對常數進行
無法對一般變數使用的最佳化處理。
第九章
類別:深入了解;拋出異常
除非成員函式亦宣告為 const,否則 C++ 編譯器不允許 const 物件呼叫任何的成員函式。
即使是不能修改物件內容的 get 成員函式,也是如此。
正如您在第 3 章 GradeBook 類別中所看到,宣告為 const 的函式必須出現在原型與定義
中,並分別於函式參數列之前,以及函式本體的左大括號前面,寫出 const 關鍵字。
程式中常犯的錯誤 9.2
定義為 const 的成員函式,若嘗試修改物件的資料成員,會造成編譯錯誤。
程式中常犯的錯誤 9.3
定義為 const 的成員函式,若呼叫同物件中的其他非 const 成員函式,會造成編譯錯
誤。
程式中常犯的錯誤 9.4
對 const 物件呼叫非 const 成員函式,會造成編譯錯誤。
建構子和解構子會引發一個有趣的問題,建構子和解構子經常需要修改物件,建構子必須
被允許修改物件的內容,以便正確設定物件的初始值。解構子能在系統清除物件之前,執行物
件結尾的工作。若把建構子和解構子宣告為 const,會產生編譯錯誤。一個 const 物件的常態性
質 (constness),必須在建構子初始化物件後,才會出現,一直到解構子執行前都持續不變。
使用常數和非常數成員函式
圖 9.16 的程式對圖 9.4-9.5 中的類別 Time 進行修改,但在 printStandard 的原型和定義移除
了關鍵字 const,這樣我們才能看出錯誤所在。我們實體化兩個 Time 物件,一個是非 const 物
件 wakeUp ( 第 7 行 ),而另一個則是 const 物件 noon ( 第 8 行 )。此程式嘗試呼叫 const 物件 noon
的非 const 成員函式 setHour ( 第 13 行 ) 和 printStandard ( 第 20 行 )。此時,編譯器會產生錯誤訊
息。程式也說明其他作用於物件的三種不同成員函式,即呼叫 const 物件的非 const 成員函式
( 第 11 行 )、呼叫非 const 物件的 const 成員函式 ( 第 15 行 ) 以及呼叫 const 物件的 const 成員函式
( 第 17-18 行 )。對 const 物件呼叫非 const 成員函式產生的錯誤訊息顯示在輸出視窗之中。
1 // Fig. 9.16: fig09_16.cpp
2 // const objects and const member functions.
3 #include "Time.h" // include Time class definition
4
5 int main()
6 {
Time wakeUp( 6, 45, 0 ); // non-constant object
7
8
const Time noon( 12, 0, 0 ); // constant object
9
// OBJECT
MEMBER FUNCTION
10
wakeUp.setHour( 18 ); // non-const
non-const
11
12
圖 9.16 const 物件和 const 成員函式 (1/2)
9-25
9-26
C++程式設計藝術
13
noon.setHour( 12 );
// const
14
15
wakeUp.getHour();
// non-const
16
17
noon.getMinute();
// const
18
noon.printUniversal(); // const
19
20
noon.printStandard(); // const
21 } // end main
non-const
const
const
const
non-const
Microsoft Visual C++ compiler error messages:
C:\examples\ch09\Fig09_16_18\fig09_18.cpp(13) : error C2662:
'Time::setHour' : cannot convert 'this' pointer from 'const Time' to
'Time &'
Conversion loses qualifiers
C:\examples\ch09\Fig09_16_18\fig09_18.cpp(20) : error C2662:
'Time::printStandard' : cannot convert 'this' pointer from 'const Time' to
'Time &'
Conversion loses qualifiers
圖 9.16 const 物件和 const 成員函式 (2/2)
建構子必須是 non-const 的成員函式,但可用來初始化 const 物件 ( 圖 9.16,第 8 行 )。回
想圖 9.5,Time 建構子的定義呼叫了另一個非 const 成員函式 setTime,執行 Time 物件的初始
值。在 const 物件的建構子中呼叫非 const 成員函式是合法的。
圖 9.16,第 20 行雖然類別 Time 的成員函式 printStandard 不會修改呼叫它的物件,仍會
產生編譯錯誤。不對物件進行修改,並不代表它就自動是 const,必須明確宣告才行。
9.11
組合:將物件當作類別成員
類 別 物 件 AlarmClock 需 知 道 何 時 應 該 發 出 響 鈴 聲 音, 因 此, 何 不 讓 Time 物 件 成 為
AlarmClock 的 一 個 成 員 呢 ? 這 就 是 組 合 (composition), 又 稱 為「 有 一 個 」 關 係 (has-a
relationship)。類別可以包含其他類別的物件作為自己的成員。
軟體工程觀點 9.9
組合是最常見的軟體再利用方式,使類別包含其他類別的物件。
軟體工程觀點 9.10
成員物件會按照在類別內宣告的順序建立 ( 而非成員初始值列表中的順序 );並且,
成員物件會在包圍它們的類別物件 ( 又稱宿主物件 host objects) 之前建立。
下 一 個 程 式 以 類 別 Date ( 圖 9.17-9.18) 和 類 別 Employee ( 圖 9.19-9.20) 為 例, 示 範 組
合。 類 別 Employee 的 定 義 ( 圖 9.19) 包 含 private 資 料 成 員 firstName、lastName、birthDate
和 hireDate。 成 員 birthDate 和 hireDate 是 類 別 Date 的 const 物 件, 此 類 別 含 有 private 資 料 成
員 month、day 和 year。Employee 建構子的標頭 ( 圖 9.20,第 10-11 行 ) 指出,建構子接受四
第九章
類別:深入了解;拋出異常
個參數 (first、last、dateOfBirth 和 dateOfHire)。前兩個參數透過成員初始值列表,傳遞給
String 類別的建構子。後兩個參數則透過成員初始值列表,傳遞給 Date 類別的建構子,初始
化 birthDate 和 hireDate 兩個資料成員。
1 // Fig. 9.17: Date.h
2 // Date class definition; Member functions defined in Date.cpp
3 #ifndef DATE_H
4 #define DATE_H
5
6 class Date
7 {
8 public:
9
static const unsigned int monthsPerYear = 12; // months in a year
10
explicit Date( int = 1, int = 1, int = 1900 ); // default constructor
11
void print() const; // print date in month/day/year format
12
~Date(); // provided to confirm destruction order
13 private:
14
unsigned int month; // 1-12 (January-December)
15
unsigned int day; // 1-31 based on month
16
unsigned int year; // any year
17
18
// utility function to check if day is proper for month and year
19
unsigned int checkDay( int ) const;
20 }; // end class Date
21
22 #endif
圖 9.17 Date 類別定義
1 // Fig. 9.18: Date.cpp
2 // Date class member-function definitions.
3 #include <array>
4 #include <iostream>
5 #include <stdexcept>
6 #include "Date.h" // include Date class definition
7 using namespace std;
8
9 // constructor confirms proper value for month; calls
10 // utility function checkDay to confirm proper value for day
11 Date::Date( int mn, int dy, int yr )
12 {
13
if ( mn > 0 && mn <= monthsPerYear ) // validate the month
14
month = mn;
15
else
16
throw invalid_argument( "month must be 1-12" );
17
18
year = yr; // could validate yr
19
day = checkDay( dy ); // validate the day
20
21
// output Date object to show when its constructor is called
22
cout << "Date object constructor for date ";
23
print();
24
cout << endl;
圖 9.18 Date 成員函式定義 (1/2)
9-27
9-28
C++程式設計藝術
25 } // end Date constructor
26
27 // print Date object in form month/day/year
28 void Date::print() const
29 {
30
cout << month << '/' << day << '/' << year;
31 } // end function print
32
33 // output Date object to show when its destructor is called
34 Date::~Date()
35 {
36
cout << "Date object destructor for date ";
37
print();
38
cout << endl;
39 } // end ~Date destructor
40
41 // utility function to confirm proper day value based on
42 // month and year; handles leap years, too
43 unsigned int Date::checkDay( int testDay ) const
44 {
45
static const array< int, monthsPerYear + 1 > daysPerMonth =
{ 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
46
47
48
// determine whether testDay is valid for specified month
49
if ( testDay > 0 && testDay <= daysPerMonth[ month ] )
50
return testDay;
51
52
// February 29 check for leap year
53
if ( month == 2 && testDay == 29 && ( year % 400 == 0 ||
( year % 4 == 0 && year % 100 != 0 ) ) )
54
55
return testDay;
56
57
throw invalid_argument( "Invalid day for current month and year" );
58 } // end function checkDay
圖 9.18 Date 成員函式定義 (2/2)
1 // Fig. 9.19: Employee.h
2 // Employee class definition showing composition.
3 // Member functions defined in Employee.cpp.
4 #ifndef EMPLOYEE_H
5 #define EMPLOYEE_H
6
7 #include <string>
8 #include "Date.h" // include Date class definition
9
10 class Employee
11 {
12 public:
13
Employee( const std::string &, const std::string &,
14
const Date &, const Date & );
15
void print() const;
16
~Employee(); // provided to confirm destruction order
17 private:
圖 9.19 示範組合的 Employee 類別定義 (1/2)
第九章
類別:深入了解;拋出異常
18
std::string firstName; // composition: member object
19
std::string lastName; // composition: member object
20
const Date birthDate; // composition: member object
21
const Date hireDate; // composition: member object
22 }; // end class Employee
23
24 #endif
圖 9.19 示範組合的 Employee 類別定義 (2/2)
1 // Fig. 9.20: Employee.cpp
2 // Employee class member-function definitions.
3 #include <iostream>
4 #include "Employee.h" // Employee class definition
5 #include "Date.h" // Date class definition
6 using namespace std;
7
8 // constructor uses member initializer list to pass initializer
9 // values to constructors of member objects
10 Employee::Employee( const string &first, const string &last,
11
const Date &dateOfBirth, const Date &dateOfHire )
12
: firstName( first ), // initialize firstName
13
lastName( last ), // initialize lastName
14
birthDate( dateOfBirth ), // initialize birthDate
15
hireDate( dateOfHire ) // initialize hireDate
16 {
17
// output Employee object to show when constructor is called
18
cout << "Employee object constructor: "
19
<< firstName << ' ' << lastName << endl;
20 } // end Employee constructor
21
22 // print Employee object
23 void Employee::print() const
24 {
25
cout << lastName << ", " << firstName << " Hired: ";
26
hireDate.print();
27
cout << " Birthday: ";
birthDate.print();
28
cout << endl;
29
30 } // end function print
31
32 // output Employee object to show when its destructor is called
33 Employee::~Employee()
34 {
cout << "Employee object destructor: "
35
<< lastName << ", " << firstName << endl;
36
37 } // end ~Employee destructor
圖 9.20 Employee 類別的成員函式定義
Employee 建構子的成員初始值串列
建構子標頭之後的冒號 (:)( 圖 9.20,第 12 行 ) 接著成員初始值串列。成員初始值串列
指 出,Employee 建 構 子 的 參 數 會 傳 給 string 和 Date 資 料 成 員 的 建 構 子。 參 數 first、last、
dateOfBirth 和 dateOfHire 會 分 別 被 傳 遞 給 物 件 firstName ( 第 12 行 )、lastName ( 第 13 行 )、
9-29
9-30
C++程式設計藝術
birthDate ( 第 14 行 ) 和 hireDate ( 第 15 行 ) 的建構子。成員初始值串列中的項目以逗號分隔。
成員初始化的順序並不重要。它們是按成員物件在 Employee 類中宣告的順序執行。
良好的程式設計習慣 9.3
為了清楚起見,成員初始化列表中的初始值,應依類別資料成員宣告順序列出。
Date 類別的預設複製建構子
檢視 Date 類別 ( 圖 9.17) 程式,您應該注意到,Date 類別並未提供接受參數為 Date 型態
的建構子。那麼,類別 Employee 建構子的成員初始值串列為何把 Date 物件傳入 Date 建構
子,來初始化 birthDate 和 hireDate 物件呢? 請回想我們在第 9.9 節提過,編譯器會為每個類
別提供預設的複製建構子,此建構子會將建構子的引數物件的每個成員,複製到要初始化物
件的對應成員。第 10 章會討論如何定義自己的複製建構子。
測試類別 Date 和 Employee
圖 9.21 建 立 兩 個 Date 物 件 ( 第 10-11 行 ), 並 把 它 們 當 作 引 數, 傳 入 於 第 12 行 產 生 的
Employee 物件的建構子。程式於第 15 行輸出 Employee 物件的資料。程式在第 10-11 行建立
Date 物件時,圖 9.18 的第 11-25 行會顯示一行輸出,指出建構子被呼叫了 ( 見範例輸出的前
兩行 )。[ 請注意:程式圖 9.21 中第 12 行進行兩次額外的 Date 建構子呼叫,它們並沒有出現
在程式的輸出中。當 Employee 的 Date 成員物件在其建構子的成員初始值串列 ( 圖 9.20,第
14-15 行 ) 中設定初始值時,會呼叫 Date 的預設複製建構子。因為這個建構子是編譯器自動
定義的,所以呼叫它時,不會輸出任何訊息。]
1 // Fig. 9.21: fig9_21.cpp
2 // Demonstrating composition--an object with member objects.
3 #include <iostream>
4 #include "Employee.h" // Employee class definition
5 using namespace std;
6
7 int main()
8 {
9
Date birth( 7, 24, 1949 );
10
Date hire( 3, 12, 1988 );
11
Employee manager( "Bob", "Blue", birth, hire );
12
13
cout << endl;
14
manager.print();
15 } // end main
Date object constructor for date 7/24/1949
Date object constructor for date 3/12/1988
Employee object constructor: Bob Blue
Blue, Bob Hired: 3/12/1988 Birthday: 7/24/1949
Employee object destructor: Blue, Bob
Date object destructor for date 3/12/1988
Date object destructor for date 7/24/1949
Date object destructor for date 3/12/1988
Date object destructor for date 7/24/1949
圖 9.21 組合,包含物件成員的物件
There are actually five constructor
calls when an Employee is
constructed—two calls to the
string class's constructor (lines
12–13 of Fig. 9.20), two calls to the
Date class's default copy
constructor (lines 14–15 of
Fig. 9.20) and the call to the
Employee class's constructor.
第九章
類別:深入了解;拋出異常
類別 Date 和 Employee 各自擁有解構子 ( 分別是圖 9.18 的第 34-39 行,以及圖 9.20 的第
33-37 行 ),當其物件被清除時,程式會列印出一個訊息。我們就可以從程式的輸出確認物件
是由內而外建構起來,而解構則是以相反的順序,由外而內進行 ( 即先清除 Employee 物件
後,才清除成員物件 Date)。
請注意圖 9.21 的最後四行輸出。最後兩行分別是 Date 物件 hire ( 圖 9.21,第 11 行 ) 和
birth ( 圖 9.21,第 10 行 ) 解構子的輸出。這些輸出說明,main 中的三個物件會以它們建構的
相反順序,進行解構。Employee 解構子的輸出是倒數五行。輸出視窗的倒數第 4 和第 3 行,
顯示 Employee 的成員物件 hireDate ( 圖 9.19,第 21 行 ) 和 birthDate ( 圖 9.19,第 20 行 ) 執行的
解構子。最後一行的輸出對應到圖 9.21,第 10-11 行,Date 物件的建立。
我們可以從這些輸出確認,Employee 物件是由外而內進行解構,也就是說,程式會先
執行 Employee 的解構子 ( 輸出視窗的倒數五行 ),然後,成員物件會以它們建構的相反順序
進行解構。string 類別的解構子中沒有輸出敘述式,因此我們不會看到 firstName 和 lastName
物件被解構。同樣地,圖 9.21 的輸出沒有顯示 birthDate 和 hireDate 物件執行的建構子,因為
它們都是由 C++ 編譯器提供的預設 Date 類別複製建構子執行初始化。
未使用成員初始值串列時,會發生什麼事?
未提供成員初始值給成員物件時,系統會自動呼叫該物件的預設建構子。由預設建構子
所設定的數值,可透過 set 函式來重新設定。不過較複雜的初始化,會需要可觀的額外工作
和時間。
程式中常犯的錯誤 9.5
成員物件若未用成員初始值串列設定初始值,且其類別未提供預設建構子 ( 即類別
定義一或多個建構子,但未提供預設建構子 ),會產生編譯器錯誤。
效能要訣 9.4
在成員初值串列中明確初始化成員物件。這可以避免「重複初始化」成員物件的額
外負擔。重複初始化:第一次是呼叫成員物件的預設建構子,而第二次是稍後在建
構子的本體中,用 set 函式設定成員物件的值。
軟體工程觀點 9.11
類別成員若為其他類別的物件,將其設成 public,不會影響該物件的 private 成員之
資料封裝性和資訊隱藏性。不過,這會破壞包含該物件所屬類別的封裝性和隱藏
性,因此類別的成員物件,應該像其他資料成員一樣設為 private。
9-31
9-32
C++程式設計藝術
9.12 夥伴函式與夥伴類別
類別的夥伴函式 (friend function) 不是成員函式,但它可以存取類別的 public 和非 public
成員。獨立的函式、其他類別、其他類別的成員函式,都可以宣告成另一個類別的夥伴。
本節會舉出如何使用夥伴函式機制的範例。在稍後的章節中,在第 10 章我們會介紹如
何用夥伴函式多載類別物件的運算子,到時您會明白,在某些環境下,成員函式不能用於運
算子多載。
宣告夥伴
要將函式宣告為類別的夥伴函式,必須在類別定義中,在函式原型前面加上關鍵字
friend。若要將類別 ClassTwo 宣告成類別 ClassOne 的夥伴,請將以下的宣告
friend class ClassTwo;
置於類別 ClassOne 的定義中。
夥伴關係是由其他類別主動賦予,而非自行取得的權限,也就是說,類別 B 若想成
為類別 A 的夥伴,則類別 A 必須明確宣告類別 B 是它的夥伴。另外,夥伴關係沒有對稱性
(symmetric),也不具遞移性 (transitive),意思是說,若類別 A 是類別 B 的夥伴,且類別 B 是
類別 C 的夥伴,則您不可以說類別 B 是類別 A 的夥伴或類別 C 是類別 B 的夥伴,或類別 A 是
類別 C 的夥伴。
用夥伴函式修改類別的 private 資料
圖 9.22 是個說明夥伴機制的範例,定義 friend 函式 setX 修改類別 Count 的 private 資料成
員 x。此處夥伴宣告 ( 第 9 行 ) 依慣例放在類別定義的最前端,甚至在 public 成員函式宣告之
前。但其實它可出現在類別定義中任何位置。
函式 setX ( 第 29–32 行 ) 是一個獨立 ( 全域 ) 函式,並不是類別 Count 的成員。因此,當
我們為 counter 物件呼叫 setX 時,程式第 41 行會將 counter 當成引數傳給 setX,而不是將它作
為物件的 handle ( 如物件名稱 ) 呼叫函式如下:
counter.setX( 8 );
// error: setX not a member function
假如您移除第 9 行的 friend,就會看到一個錯誤訊息,表示 setX 函式不能修改 Count 類
別的 private 資料成員 x。
1
2
3
4
5
6
7
8
9
// Fig. 9.22: fig09_22.cpp
// Friends can access private members of a class.
#include <iostream>
using namespace std;
// Count class definition
class Count
{
friend void setX( Count &, int ); // friend declaration
圖 9.22 夥伴函式能夠存取類別的 private 成員 (1/2)
第九章
類別:深入了解;拋出異常
10 public:
11
// constructor
12
Count()
13
: x( 0 ) // initialize x to 0
{
14
15
// empty body
} // end constructor Count
16
17
18
// output x
19
void print() const
{
20
21
cout << x << endl;
} // end function print
22
23 private:
24
int x; // data member
25 }; // end class Count
26
27 // function setX can modify private data of Count
28 // because setX is declared as a friend of Count (line 9)
29 void setX( Count &c, int val )
30 {
31
c.x = val; // allowed because setX is a friend of Count
32 } // end function setX
33
34 int main()
35 {
36
Count counter; // create Count object
37
38
cout << "counter.x after instantiation: ";
39
counter.print();
40
41
setX( counter, 8 ); // set x using a friend function
42
cout << "counter.x after call to setX friend function: ";
43
counter.print();
44 } // end main
counter.x after instantiation: 0
counter.x after call to setX friend function: 8
圖 9.22 夥伴函式能夠存取類別的 private 成員 (2/2)
前面說過,圖 9.22 是為了示範夥伴機制。實際應用上,我們通常會將函式 setX 定義為
類別 Count 的成員函式,且將圖 9.22 的程式分割成三個檔案:
1. 標頭檔 ( 如 Count.h),內含類別 Count 的定義,其中包含 friend 函式 setX 的原型。
2. 實作檔 ( 如 Count.cpp),內含類別 Count 成員函式及 friend 函式 setX 的定義。
3. 包含 main 的測試程式 ( 如 fig09_22.cpp)。
多載 friend 函式
多載函式亦可成為類別的夥伴函式。每個想要成為夥伴函式的多載函式,都必須在類別
的定義中,明確宣告為該類別的夥伴。
9-33
9-34
C++程式設計藝術
軟體工程觀點 9.12
雖然夥伴函式的原型出現在類別的定義中,但它不是成員函式。
軟體工程觀點 9.13
成員函式的存取修飾詞 private、protected 和 public 與夥伴宣告 (friend declaration)
無關,所以夥伴宣告可以放在類別定義中任何位置。
良好的程式設計習慣 9.4
請將所有夥伴宣告放在類別定義的最前面,且在宣告的前面不應該出現任何成員存
取修飾詞。
9.13 使用 this 指標
物件的成員函式可以處理物件的資料,但成員函式要怎麼知道它處理的是哪個物件?每
個物件可以透過 this 指標 (C++ 關鍵字 ),存取自己的記憶體位址。this 指標不是物件自己的
一部分,也就是說,對 this 執行 sizeof 運算的結果,與對物件執行 sizeof 運算的結果不同。編
譯器會將 this 指標作為隱含的引數傳給物件的非 static 成員函式。第 9.14 節會介紹 static 類別
成員,並解釋為何 this 指標不傳入 static 成員函式。
使用 this 指標避免命名衝突
成員函式可以隱式 (implicitly) 或顯式 (explicitly) 兩種方式使用 this 指標 ( 截至目前為
止我們都這麼做 ),參用物件成員和成員函式。以顯式方式明確使用 this 指標是為了避免不
同類別間資料成員和成員函式參數間的命名衝突 ( 或其他區域變數 )。考慮圖 9.4-9.5 中類別
Time 的資料成員和 setHour 資料成員,我們可定義 setHour 如下:
// set hour value
void Time::setHour( int hour )
{
if ( hour >= 0 && hour < 24 )
this->hour = hour; // use this pointer to access data member
else
throw invalid_argument( "hour must be 0-23" );
} // end function setHour
在此函數定義,setHour 的參數 hour 和資料成員 hour 同名。在 setHour 的範圍內,參數隱
藏了資料成員。但是,您可以還是透過 this-> 存取資料成員 hour。所以,以下的敘述將參數
hour 的值,指派給資料成員 hour
this->hour = hour; // use this pointer to access data member
錯誤預防要訣 9.4
為了使程式更為清楚和容易維護,並且避免錯誤,不要讓區域變數名稱隱藏了資料
成員。
第九章
類別:深入了解;拋出異常
this 指標的型態
this 指標的型態與物件的型態,以及使用 this 指標的成員函式是否宣告成 const 有關。例
如,在類別 Employee 的非 const 成員函式中,this 指標的型態是 Employee *const。在常數成
員函式中,其型態則為 const Employee*。
隱式和顯式地使用 this 指標存取物件的資料成員
圖 9.23 說明如何在程式中以隱式和顯式兩種方式使用 this 指標,讓類別 Test 的成員函式
印出 Test 物件的 private 資料 x。在下一個範例和第 10 章,我們將展示使用 this 指標的一些重
要又難捉摸的的範例。
1 // Fig. 9.23: fig09_23.cpp
2 // Using the this pointer to refer to object members.
3 #include <iostream>
4 using namespace std;
5
6 class Test
7 {
8 public:
9
explicit Test( int = 0 ); // default constructor
10
void print() const;
11 private:
12
int x;
13 }; // end class Test
14
15 // constructor
16 Test::Test( int value )
17
: x( value ) // initialize x to value
18 {
19
// empty body
20 } // end constructor Test
21
22 // print x using implicit and explicit this pointers;
23 // the parentheses around *this are required
24 void Test::print() const
25 {
26
// implicitly use the this pointer to access the member x
27
cout << "
x = " << x;
28
29
// explicitly use the this pointer and the arrow operator
30
// to access the member x
31
cout << "\n this->x = " << this->x;
32
33 // explicitly use the dereferenced this pointer and
34
// the dot operator to access the member x
35
cout << "\n(*this).x = " << ( *this ).x << endl;
36 } // end function print
37
38 int main()
39 {
圖 9.23 使用 this 指標存取物件的成員 (1/2)
9-35
9-36
C++程式設計藝術
40
Test testObject( 12 ); // instantiate and initialize testObject
41
42
testObject.print();
43 } // end main
x = 12
this->x = 12
(*this).x = 12
圖 9.23 使用 this 指標存取物件的成員 (2/2)
為了教學示範,成員函式 print ( 第 24-36 行 ) 首先隱含地使用 this 指標 ( 第 27 行 ) 印出 x,
即只指定資料成員的名稱。接下來,函式 print 會利用 this 指標,以兩種不同的符號來存取 x,
即箭號 (->) 運算子加上 this 指標 ( 第 31 行 ),以及點號 (.) 運算子配合解參照的 this 指標 ( 第 35
行 )。請注意,使用點號成員選擇運算子 (.) 時,*this ( 第 35 行 ) 必須加上小括號,點號運算
子的優先權比 * 運算子的優先權更高,所以此處的小括號不能省略。若省略小括號,運算式
*this.x 的結果會與 * (this.x) 相同,這是一種語法錯誤,因為點號運算子不能與指標一起使用。
this 指標的另一項有趣用法,是防止物件賦值給它自己。在第 10 章我們會談到,當物件
包含指向動態配置記憶體的指標時,自我賦值會造成嚴重的錯誤。
使用 this 指標進行連續函式呼叫
另一個 this 指標的用法是進行串接成員函式呼叫 (cascaded member-function calls),
在同一個敘述中進行多個函式呼叫 ( 如圖 9.26 中第 12 行 )。圖 9.24-9.26 的程式對類別 Time 的
set 函式,如 setTime、setHour、setMinute 和 setSecond 進行修改,使它們傳回 Time 物件的參
照,以便進行串接成員函式呼叫。請注意在圖 9.25 中,這些函式的本體最後一個敘述都是傳
回 *this ( 第 23、34、45 和 56 行 ),且傳回值的型態為 Time &。
圖 9.26 的程式建立 Time 物件 t ( 第 9 行 ),並於用它進行串接成員函式呼叫 ( 第 12 和 24
行 )。讀者可能要問,為何回傳 *this 就可以執行串接呼叫?因為,點號運算子 (.) 的結合性是
由左至右,故第 12 行首先計算 t.setHour(18) 的值,再傳回物件 t 的參考,作為函式呼叫的傳
回值。然後,剩餘的運算式會成為
t.setMinute( 30 ).setSecond( 22 );
函式呼叫 t.setMinute(30) 執行後,傳回物件 t 的參考。然後,剩下的運算式會成為
t.setSecond( 22 );
1
2
3
4
5
6
7
// Fig. 9.24: Time.h
// Cascading member function calls.
// Time class definition.
// Member functions defined in Time.cpp.
#ifndef TIME_H
#define TIME_H
圖 9.24 修改 Time 類別的定義,進行串接成員函式呼叫 (1/2)
第九章
類別:深入了解;拋出異常
8
9 class Time
10 {
11 public:
12
explicit Time( int = 0, int = 0, int = 0 ); // default constructor
13
14
// set functions (the Time & return types enable cascading)
15
Time &setTime( int, int, int ); // set hour, minute, second
16
Time &setHour( int ); // set hour
17
Time &setMinute( int ); // set minute
18
Time &setSecond( int ); // set second
19
20
// get functions (normally declared const)
21
unsigned int getHour() const; // return hour
22
unsigned int getMinute() const; // return minute
23
unsigned int getSecond() const; // return second
24
25
// print functions (normally declared const)
26
void printUniversal() const; // print universal time
27
void printStandard() const; // print standard time
28 private:
29
unsigned int hour; // 0 - 23 (24-hour clock format)
30
unsigned int minute; // 0 - 59
31
unsigned int second; // 0 - 59
32 }; // end class Time
33
34 #endif
圖 9.24 修改 Time 類別的定義,進行串接成員函式呼叫 (2/2)
1 // Fig. 9.25: Time.cpp
2 // Time class member-function definitions.
3 #include <iostream>
4 #include <iomanip>
5 #include <stdexcept>
6 #include "Time.h" // Time class definition
7 using namespace std;
8
9 // constructor function to initialize private data;
10 // calls member function setTime to set variables;
11 // default values are 0 (see class definition)
12 Time::Time( int hr, int min, int sec )
13 {
14
setTime( hr, min, sec );
15 } // end Time constructor
16
17 // set values of hour, minute, and second
18 Time &Time::setTime( int h, int m, int s ) // note Time & return
19 {
20
setHour( h );
21
setMinute( m );
22
setSecond( s );
23
return *this; // enables cascading
24 } // end function setTime
圖 9.25 修改 Time 類別成員函式的定義,進行串接成員函式呼叫 (1/3)
9-37
9-38
C++程式設計藝術
25
26 // set hour value
27 Time &Time::setHour( int h ) // note Time & return
28 {
29
if ( h >= 0 && h < 24 )
30
hour = h;
31
else
32
throw invalid_argument( "hour must be 0-23" );
33
34
return *this; // enables cascading
35 } // end function setHour
36
37 // set minute value
38 Time &Time::setMinute( int m ) // note Time & return
39 {
40
if ( m >= 0 && m < 60 )
41
minute = m;
42
else
43
throw invalid_argument( "minute must be 0-59" );
44
45
return *this; // enables cascading
46 } // end function setMinute
47
48 // set second value
49 Time &Time::setSecond( int s ) // note Time & return
50 {
51
if ( s >= 0 && s < 60 )
52
second = s;
53
else
54
throw invalid_argument( "second must be 0-59" );
55
56
return *this; // enables cascading
57 } // end function setSecond
58
59 // get hour value
60 unsigned int Time::getHour() const
61 {
62
return hour;
63 } // end function getHour
64
65 // get minute value
66 unsigned int Time::getMinute() const
67 {
68
return minute;
69 } // end function getMinute
70
71 // get second value
72 unsigned int Time::getSecond() const
73 {
74
return second;
75 } // end function getSecond
76
77 // print Time in universal-time format (HH:MM:SS)
78 void Time::printUniversal() const
79 {
圖 9.25 修改 Time 類別成員函式的定義,進行串接成員函式呼叫 (2/3)
第九章
類別:深入了解;拋出異常
80
cout << setfill( '0' ) << setw( 2 ) << hour << ":"
81
<< setw( 2 ) << minute << ":" << setw( 2 ) << second;
82 } // end function printUniversal
83
84 // print Time in standard-time format (HH:MM:SS AM or PM)
85 void Time::printStandard() const
86 {
87
cout << ( ( hour == 0 || hour == 12 ) ? 12 : hour % 12 )
88
<< ":" << setfill( '0' ) << setw( 2 ) << minute
89
<< ":" << setw( 2 ) << second << ( hour < 12 ? " AM" : " PM" );
90 } // end function printStandard
圖 9.25 修改 Time 類別成員函式的定義,進行串接成員函式呼叫 (3/3)
程式第 24 行 ( 圖 9.26) 也使用連續呼叫的機制。注意,我們不能將其他成員函式的呼
叫,鏈加在 printStandard 呼叫後面,因為 printStandard 並不傳回指向 t 的參考。我們在第 24
行在 setTime 之前呼叫 printStandard,會產生編譯錯誤。第 10 章會提供幾個使用連續函式呼
叫的實用範例。其中之一是在單一敘述中對 cout 連續使用 << 運算子輸出多個數值。
1 // Fig. 9.26: fig09_26.cpp
2 // Cascading member-function calls with the this pointer.
3 #include <iostream>
4 #include "Time.h" // Time class definition
5 using namespace std;
6
7 int main()
8 {
9
Time t; // create Time object
10
11
// cascaded function calls
12
t.setHour( 18 ).setMinute( 30 ).setSecond( 22 );
13
14 // output time in universal and standard formats
15
cout << "Universal time: ";
16
t.printUniversal();
17
18
cout << "\nStandard time: ";
19
t.printStandard();
20
21
cout << "\n\nNew standard time: ";
22
23
// cascaded function calls
24
t.setTime( 20, 20, 20 ).printStandard();
25
cout << endl;
26 } // end main
Universal time: 18:30:22
Standard time: 6:30:22 PM
New standard time: 8:20:20 PM
圖 9.26 利用 this 指標進行串接成員函式呼叫
9-39
9-40
C++程式設計藝術
9.14 static 類別成員
類別的每個物件都擁有其資料成員的副本,這個通則,有個重要的例外:static 資料成
員 (static data member)。在某些情況下,類別的所有物件必須共享某個變數的單一副本。
此時,就該是 static 資料成員出現的時候。靜態變數是全類別範圍共用的資訊,由此類別的
所有實體物件共用,也不是由哪一個物件來設定。回憶我們在第 7 章中的類別 GradeBook,
就使用 static 資料成員儲存常數,代表所有 GradeBook 物件能儲存的成績個數。
使用全類別共用資料的動機
我 們 再 用 一 個 例 子 說 明 使 用「 全 類 別 」static 資 料 的 動 機 與 理 由。 假 設 在 視 訊 遊 戲
中, 有 Martian 和 其 他 的 太 空 生 物 等 角 色。 當 Martian 知 道 有 五 個 以 上 的 Martian 時, 每
個 Martian 就 會 變 得 很 勇 敢, 並 且 想 要 攻 擊 其 他 的 太 空 生 物。 如 果 少 於 五 個 Martian,
則 每 個 Martian 就 會 變 得 十 分 膽 小。 所 以, 每 個 Martian 都 必 須 知 道 Martian 的 數 目, 以
martianCount 的 值 表 示。 我 們 可 以 賦 予 類 別 Martian 的 每 個 物 件 一 個 martianCount 資 料 成
員。如此,每個 Martian 都會有一個該資料成員的副本。每當建立新的 Martian 物件時,
就 必 須 更 新 所 有 Martian 物 件 的 資 料 成 員 martianCount。 這 會 讓 每 個 Martian 物 件 必 須 處
理 ( 存取 ) 記憶體中所有其他的 Martian 物件。如此一來,這些重複的副本就會浪費空間及
更 新 每 一 個 martianCount 副 本 的 時 間。 比 較 好 的 作 法 是 將 martianCount 宣 告 為 static, 讓
martianCount 變成全類別共用的資料。每個 Martian 都可以存取 martianCount 的值,好像它
是每個 Martian 的資料成員一樣,但是 C++ 只維護一份靜態變數 martianCount 的副本。這樣
就可以節省空間。我們透過 Martian 建構子,遞增靜態變數 martianCount 的值,且用 Martian
解構子減少 martianCount 的值。這樣可以節省時間。因為該類別只有一份副本,所以不需
對每個 Martian 物件遞增 martianCount 個別副本的值。
效能要訣 9.5
當一份資料就可滿足類別所有物件時,請使用 static 資料成員。
static 資料成員的使用範圍以及初始化
類別的 static 資料成員具有類別使用範圍。static 資料成員只能初始化一次。
static 成員可以宣告成 public、private 或 protected。基本型態的 static 資料成員的預設初
始值為 0。在 C++11 之前,只有型態為 int 或 enum 的 const static 資料成員可以在類別定義中
宣告時設定其初始值,其他的 static 資料成員則必須在全域範圍 (global namespace scope) 中
定義 ( 即類別定義本體之外 )。C++11 的類別內初始化,則允許您將這些變數在類別定義的宣
告中做初始化。若 static 資料成員的型態為類別,且該類別有預設建構子,則此 static 資料成
員不需設定初始值,因為程式會自動呼叫其預設建構子。
第九章
類別:深入了解;拋出異常
存取 static 資料成員
類別的 private 和 protected static 成員,只能透過類別的 public 成員函式或類別的夥伴函
式存取。即使類別沒有任何物件存在時,該類別的 static 成員仍然存在。類別沒有物件存
在時,若要存取其 public static 成員,只要在成員名稱前面寫出類別名稱後接二元範圍解析
運算子 (::) 即可。例如,如果先前變數 martianCount 是 public,則程式沒有建立 Martian 物
件時,可以用 Martian::martianCount 運算式存取該變數。( 當然,一般來說我們不鼓勵使用
public 資料成員 )。
我們還可以提供 public static 成員函式 (static member function),如此就可在沒有物件
時,用類別名稱、二元範圍解析運算子以及函式的名稱,存取 private 或 protected static 資料
成員。static 成員函式可視為整個類別,而非單一物件,對外界提供的服務。
軟體工程觀點 9.14
若某類別沒有產生任何物件,則 static 資料成員和 static 成員函式仍然可以單獨使用。
static 資料成員的使用範例
圖 9.27-9.29 的程式示範用 private static 資料成員 count ( 圖 9.27,第 24 行 ) 及 public static
成員函式 getCount ( 圖 9.27,第 18 行 )。在圖 9.28 中的第 8 行,程式在全域範圍定義 count 資
料成員並將初始值設為零,而第 12-15 行則定義 static 成員函式 getCount。請注意在第 8 或第
12 行中,程式都沒有寫出關鍵字 static,但它們都是 static 類別成員。關鍵字 static 不能用在
類別定義的外部,譬如,成員定義程式中。
資料成員 count 會維護已經建立的類別 Employee 的物件個數。當類別 Employee 物件存
在時,Employee 物件的任何成員函式都可存取成員 count,在圖 9.28 中,第 22 行的建構子和
第 32 行的解構子都會參照 count。
1 // Fig. 9.27: Employee.h
2 // Employee class definition with a static data member to
3 // track the number of Employee objects in memory
4 #ifndef EMPLOYEE_H
5 #define EMPLOYEE_H
6
7 #include <string>
8
9 class Employee
10 {
11 public:
12
Employee( const std::string &, const std::string & ); // constructor
13
~Employee(); // destructor
14
std::string getFirstName() const; // return first name
15
std::string getLastName() const; // return last name
16
17
// static member function
圖 9.27 Empolyee 類別定義,用 static 資料成員記錄記憶體中 Empolyee 物件數量 (1/2)
9-41
9-42
C++程式設計藝術
18
static unsigned int getCount(); // return # of objects instantiated
19 private:
20
std::string firstName;
21
std::string lastName;
22
23
// static data
24
static unsigned int count; // number of objects instantiated
25 }; // end class Employee
26
27 #endif
圖 9.27 Empolyee 類別定義,用 static 資料成員記錄記憶體中 Empolyee 物件數量 (2/2)
1 // Fig. 9.28: Employee.cpp
2 // Employee class member-function definitions.
3 #include <iostream>
4 #include "Employee.h" // Employee class definition
5 using namespace std;
6
7 // define and initialize static data member at global namespace scope
8 unsigned int Employee::count = 0; // cannot include keyword static
9
10 // define static member function that returns number of
11 // Employee objects instantiated (declared static in Employee.h)
12 unsigned int Employee::getCount()
13 {
14
return count;
15 } // end static function getCount
16
17 // constructor initializes non-static data members and
18 // increments static data member count
19 Employee::Employee( const string &first, const string &last )
20
: firstName( first ), lastName( last )
21 {
22
++count; // increment static count of employees
23
cout << "Employee constructor for " << firstName
24
<< ' ' << lastName << " called." << endl;
25 } // end Employee constructor
26
27 // destructor deallocates dynamically allocated memory
28 Employee::~Employee()
29 {
30
cout << "~Employee() called for " << firstName
31
<< ' ' << lastName << endl;
32
--count; // decrement static count of employees
33 } // end ~Employee destructor
34
35 // return first name of employee
36 string Employee::getFirstName() const
37 {
38
return firstName; // return copy of first name
39 } // end function getFirstName
40
41 // return last name of employee
42 string Employee::getLastName() const
43 {
44
return lastName; // return copy of last name
45 } // end function getLastName
圖 9.28 Employee 類別成員函式的定義
第九章
類別:深入了解;拋出異常
圖 9.29 在程式的不同位置使用靜態成員函數 getCount,取得當時在記憶體中 Employee
物件的個數。程式在 Employee 物件建立 ( 第 12 行 ) 前、在兩個 Employee 建立後 ( 第 23 行 )、
在的 Employee 物件破壞後 ( 第 34 行 ) 呼叫 Employee:: getCount 破壞。第 16-29 行在 main 中嵌
入一個區塊範圍。回想一下,局部變數只在定義的區塊中存在,程式離開區塊,區域變數也
隨著消失。在這個例子中,第 17-18 行,我們在內嵌區塊建了兩個 Employee 物件。物件建構
子的執行,會將 Employee 類別的靜態資料成員 count 加 1。當程式執行到第 29 行,代表離開
了兩個物件定義的區域,Employee 物件會被摧毀。此時,每個物件的解構子開始執行,將
靜態資料成員 count 的值減 1,表示記憶中少了一個物件。
1 // Fig. 9.29: fig09_29.cpp
2 // static data member tracking the number of objects of a class.
3 #include <iostream>
4 #include "Employee.h" // Employee class definition
5 using namespace std;
6
7 int main()
8 {
9
// no objects exist; use class name and binary scope resolution
10
// operator to access static member function getCount
11
cout << "Number of employees before instantiation of any objects is "
12
<< Employee::getCount() << endl; // use class name
13
14
// the following scope creates and destroys
15
// Employee objects before main terminates
{
16
17
Employee e1( "Susan", "Baker" );
18
Employee e2( "Robert", "Jones" );
19
20
// two objects exist; call static member function getCount again
21
// using the class name and the binary scope resolution operator
22
cout << "Number of employees after objects are instantiated is "
23
<< Employee::getCount();
24
25
cout << "\n\nEmployee 1: "
26
<< e1.getFirstName() << " " << e1.getLastName()
27
<< "\nEmployee 2: "
28
<< e2.getFirstName() << " " << e2.getLastName() << "\n\n";
} // end nested scope in main
29
30
31
// no objects exist, so call static member function getCount again
32
// using the class name and the binary scope resolution operator
33
cout << "\nNumber of employees after objects are deleted is "
34
<< Employee::getCount() << endl;
35 } // end main
Number of employees before instantiation of any objects is 0
Employee constructor for Susan Baker called.
Employee constructor for Robert Jones called.
Number of employees after objects are instantiated is 2
Employee 1: Susan Baker
Employee 2: Robert Jones
~Employee() called for Robert Jones
~Employee() called for Susan Baker
Number of employees after objects are deleted is 0
圖 9.29 static 資料成員 count 記錄類別物件個數
9-43
9-44
C++程式設計藝術
成員函式若不存取類別的非 static 資料成員或成員函式,則應宣告為 static。static 成員函
式沒有 this 指標 ( 這與非 static 成員函式不同 ),因為 static 資料成員和成員函式獨立存在於任
何類別物件之外。this 指標必須指到類別的某特定物件,但 static 成員函式被呼叫時,記憶體
中可能尚未產生任何類別的物件。
程式中常犯的錯誤 9.6
在 static 成員函式中使用 this 指標,會造成編譯錯誤。
程式中常犯的錯誤 9.7
將 static 成員函式宣告成 const,會造成編譯錯誤。限定詞 const 表示,該函式不能對
其操作的物件進行修改,但 static 成員函式的存在和物件無關,限定詞 const 套用的
對象並不存在。
9.15 總結
本章加深了對類別的介紹,使用Time類別為例,為大家介紹幾個新功能。我們使用標頭檔
防護,防止標頭檔 (.h) 在程式中被重複引用。您也學到了怎麼使用箭號運算子,透過物件類別型
態的指標,來存取物件的成員。您學到了成員函式具有類別使用域,成員函式的名稱只有同類
別中的其他成員才能使用,除非透過此類別的物件、參考此類別的物件、指向此類別物件的指
標,或使用二元範圍解析運算子。我們也討論了存取函式 ( 通常用來讀取資料成員的值,或測試
條件的真偽 ),以及工具函式 (private成員函式,用來協助類別的public成員函式的操作 )。
您學到了,建構子可以指定預設引數,以各種方式呼叫。您也學到了,呼叫時不需任何
引數的建構子稱為預設建構子,類別最多只能有一個預設建構子。我們討論了解構子,它的
用途是在類別物件被清除之前,執行物件結尾的工作。我們也示範了物件的建構子和解構子
被呼叫的順序。
我們說明了,當成員函式回傳一個指向 private 資料成員的參考時,可能會破壞類別的
封裝。我們也展示了同一個型別的物件,可以透過預設的逐成員指派指派給另一個物件,在
第 10 章我們會討論,當物件有指標成員時,執行物件指派所會產生的問題。
您也學到了透過 const 物件與 const 成員函式,防止物件被修改,強制實行最小權限原
則。您也學到透過組合,類別可以使用另一個類別的物件作為成員。我們介紹了夥伴關係,
並使用一個範例來介紹如何使用 friend 函式。
您學到了一個特別的指標 this,類別中任何非 static 的成員函式,都隱含此指標作為引
數,以便在函式內正確存取物件的資料成員,以及其他非 static 的成員函式。您也知道如何
明確使用 this 指標,存取類別的成員以及進行串接成員函式呼叫。我們說明了為何需要 static
資料成員,並示範如何在類別中使用 static 資料成員與成員函式。
在第 10 章中,我們繼續深入探討類別和物件的運用,如運算子多載,將 C++ 的運算子
套用在自定義類別的物件。例如,我們將會見到如何「多載」運算子 <<,讓它可以不需要
明確地使用重覆結構,即能輸出一個完整的陣列。
第九章
類別:深入了解;拋出異常
摘要
9.2 案例研究:類別 Time
y 前置處理指引 #ifndef ( 意思是「若未定義」
,if not defined) 和 #endif 可用來避免重複嵌用
同一標頭檔。若這兩個指令中間的程式碼未曾嵌用過,#define 會定義一個名稱,避免程
式碼以後被再度嵌用,並將程式碼嵌用進原始碼檔案中。
y 在 C++11 之前,只有型態為 int 或 enum 的 const static 資料成員可以在類別定義中宣告時設定其
初始值,其他的 static 資料成員則必須在全域範圍 (global namespace scope) 中定義 ( 即類別定
義本體之外 )。C++11 的類別內初始化,則允許您將這些變數在類別定義的宣告中做初始化。
y 類別的函式可拋出異常 ( 譬如,遇到無效的引數 ),表示資料是無效的。
y 串流操作子 setfill 可用來設定填充字元,當要輸出的整數位數小於欄位寬度時,用來填滿
欄位。
y 如果某成員函式定義了一個變數,其名稱與同類別的類別變數名稱相同,則在此函式範圍
中,類別範圍的變數會被隱藏。
y 預設情況下,填充字元會出現在數字之前。
y 串流操作子 setfill 是一種粘著性設定 (sticky setting),意思是一旦設定,就會對之後所有的
輸出欄位發生作用。
y 即使我們在類別的定義之內宣告成員函式,將其定義置於類別的主體之外 ( 並透過二元範
圍解析運算子結合二者 ),成員函式仍屬於類別使用域 (class scope)。
y 成員函式若定義於類別定義中,C++ 編譯器會嘗試將該成員函式變成內嵌函式。
y 類別可以包含其他類別的物件作為其成員,或衍生自其他類別,讓其他類別提供新類別的
屬性和行為。
9.3 類別使用域及存取類別的成員
y 類別的資料成員與成員函式屬於類別使用域。
y 非成員函式屬於全域命名空間使用域 ( global namespace scope)。
y 在類別使用域中,類別的所有成員可透過成員函式存取,且可透過名稱取用。
y 在類別使用域之外,可透過物件的代表 (handle) 參考類別成員,如物件名稱、以及指向物
件的參考或指標。
y 在成員函式中宣告的變數具有區域使用域,只能在該函式中使用。
y 將點號 (.) 成員選擇運算子 (member selction operator) 置於物件名稱或指向物件的參考之
後,就可存取該物件的 public 成員。
y 箭號成員選擇運算子 (->) 則可配合指向物的指標使用,存取物件的 public 成員。
9.4 存取函式與工具函式
y 存取函式可以用來讀取或顯示類別的資料。存取函式另一項常用的功能是用來測試條件式
的真偽,這類函式又稱為判斷函式。
9-45
9-46
C++程式設計藝術
y 工具函式用來幫助類別其他 public 成員函式完成工作的 private 成員函式,工具函式不能讓
使用類別的外部程式呼叫。
9.5 類別 Time 案例研究:建構子與預設引數
y 與一般函式相同,建構子可以擁有預設引數。
9.6 解構子
y 物件消除時,系統會自動呼叫其類別的解構子。
y 類別解構子的名稱,是類別名稱前面加上波浪符號 (~)。
y 解構子不會釋放物件的儲存空間,它會在系統收回物件所占記憶體之前,執行物件結尾工
作,讓新物件得以利用清出來的記憶體。
y 解構子不接收任何參數,也沒有傳回值。類別至多可擁有一個解構子。
y 若程式設計者未指定類別解構子,編譯器會自動提供,所以每個類別都有解構子。
9.7 建構子與解構子的呼叫時機
y 建構子和解構子的呼叫順序與建立物件時,程式執行權進出每個物件範圍的順序有關。
y 一般來說,解構子的呼叫順序,與建構子的呼叫順序相反,但物件的儲存類別會影響解構
子的呼叫順序。
9.8 類別 Time 案例研究:小心陷阱,傳回 private 資料成員的參考
y 指向物件的參考,相當於物件名稱的別名,因此可用在指派敘述的左邊,接受設定的數
值。在此情況下,參考會作為 lvalue,接受要設定的數值。
y 若函式傳回 const 參考,則該參考就不能用為可修改的 lvalue。
9.9 預設逐成員指派
y 指派運算子 (=) 可將某個物件指定給另一個相同型別的物件。預設情況下,該運算子會執
行逐成員指派 (memberwise copy)。
y 物件可以用傳值呼叫的方式傳入函式,也可以從函式傳回。C++ 會建立新的物件,透過複
製建構子 (copy constructor) 將原物件的值設給新的物件。
y 編譯器會為所有類別提供預設的複製建構子,用來把原物件的每個成員,複製到新物件的
對應成員。
9.10 const 物件和 const 成員函式
y 關鍵字 const 可用來指稱物件不可被修改;程式若試圖修改物件內容,會產生編譯錯誤。
y C++ 編譯器不允許在 const 物件上呼叫非 const 成員函式。
y 定義為 const 的成員函式,若嘗試修改物件的資料成員,會造成編譯錯誤。
y 成員函式若為 const,則必須在原型和定義中都寫出 const。
第九章
類別:深入了解;拋出異常
y const 物件需於初始化時賦值。
y 建構子和解構子不能宣告為 const。
9.11
組合:將物件當作類別成員
y 類別可以擁有其他類別的物件作為成員,這個觀念稱為組合。
y 成員物件按照它們在類別定義中的順序進行建構,並且比包含它們的類別物件更先建立。
y 未提供成員初始值給成員物件時,系統會自動呼叫該物件的預設建構子。
9.12 夥伴函式與類別
y 類別的夥伴函式 (friend function) 定義於類別範圍的外部,它可以存取類別的所有成員。
獨立的函式或整個類別可以宣告成另一個類別的夥伴。
y 夥伴宣告可以寫在類別的任何地方。
y 夥伴關係沒有對稱性,也沒有遞移性。
9.13 使用 this 指標
y 所有物件都能透過 this 指標存取自己的位址空間。
y this 指標不是物件自己的一部分,意思是說,對 this 執行 sizeof 運算的結果,與對物件執行
sizeof 運算的結果不一樣。
y 編譯器會將 this 指標作為隱含的引數,傳給物件的非 static 成員函式。
y 物件會自動使用 this 指標 ( 截至目前為止我們都這麼做 ),也可明確使用之以參照其資料成
員和成員函式。
y 透過 this 指標,我們能在單一敘述中藉串接成員函式呼叫多個函式。
9.14 static 類別成員
y static 資料成員代表全類別共用的資訊,意思是類別所有物件共通的特性,而非特定物件
的個別屬性。
y static 資料成員具有類別範圍,且可宣告為 public、private 或 protected。
y 即使某類別沒有任何物件存在時,該類別的 static 成員仍然存在。
y 類別沒有物件存在時,若要存取其 public static 成員,只要在成員名稱前面寫出類別名稱
後接二元範圍解析運算子 (::) 即可。
y 關鍵字 static 不能用在類別定義外,定義成員函式的程式中。
y 成員函式若不存取類別的非 static 資料成員或成員函式,則應宣告為 static。static 成員函式
沒有 this 指標 ( 這與非 static 成員函式不同 ),因為 static 資料成員和成員函式獨立存在於任
何類別物件之外。
9-47
9-48
C++程式設計藝術
自我測驗題
9.1
請填寫以下空格:
a) 類別成員可透過
運算子配合類別的物件名稱,或用
運算子,配合指向此
類別物件的指標存取之。
b) 類別成員若為
,則只能由類別的成員函式或夥伴函式存取。
c) 類別的成員若為
d)
,則程式可以在此類別物件使用域內的任何地方存取之。
可用來把類別物件指派給另一個相同類別的物件。
e) 非類別成員的函式必須宣告成類別的
f) 常數物件必須被
g)
;一旦建立之後,便不能夠再修改。
資料成員代表全類別共用的資訊。
h) 物件的非 static 成員函式可藉
指標存取自己。
i)
關鍵字
j)
如果沒有為類別的成員物件,提供成員初始值串列,則程式會呼叫該物件的
可以用來指出,一個物件或變數不能夠更改。
k) 如果成員函式沒有存取
l)
9.2
,才能夠存取該類別的 private 資料成員。
成員物件必須在外圍物件
類別成員,則應該宣告為 static。
建立。
找出下列敘述中的錯誤,並說明如何更正:
a) 假設在類別 Time 中有下列原型宣告:
void ~Time( int );
b) 假設以下原型定義在 Employee 類別中
int Employee( string, string );
c) 以下是一個類別的定義:
class Example
{
public:
Example( int y = 10 )
: data( y )
{
// empty body
} // end Example constructor
int getIncrementedData() const
{
return ++data;
} // end function getIncrementedData
static int getCount()
{
cout << "Data is " << data << endl;
return count;
} // end function getCount
private:
int data;
static int count;
}; // end class Example
。
第九章
類別:深入了解;拋出異常
自我測驗題解答
9.1
a) 點號運算子 (.),箭號運算子 (->)。b) private。c) public。d) 預設的逐成員指派 ( 由指
派運算子執行 )。e) 夥伴。f) 初始化。g) static。h) this。i) const。j) 預設建構子。k) 非
static。l) 之前。
9.2
a) 錯誤:解構子不能傳回任何值 ( 甚至不能有傳回值型別 ) 或接收任何引數。
更正:移除宣告中的傳回值型別 void 和參數 int。
b) 錯誤:建構子不可以有傳回值。
更正:從宣告中移除傳回值型別 int。
c) 錯 誤: 類 別 Example 的 定 義 有 兩 個 錯 誤。 第 一 個 錯 誤 發 生 在 函 式
getIncrementedData,此函式宣告為 const,但它會修改物件的內容。
更 正: 要 更 正 第 一 個 錯 誤, 須 將 const 關 鍵 字 從 getIncrementedData 的 定 義
中移除。
錯 誤: 第 二 個 錯 誤 發 生 在 函 式 getCount。 此 函 式 宣 告 為 static, 因 此 不 能 存
取類別中任何的非 static 成員。
更正:要更正第二個錯誤,須將輸出敘述式從 getCount 定義中移除。
習題
9.3
( 範圍域解析運算子 ) 範圍解析運算子的功用為何?
9.4
( 加強 Time 類別 ) 請撰寫建構子 Time,使用於 C++ 標準函式庫標頭檔 <ctime> 中宣告的
time 和 localtime 函式,取得目前時間,用來設定 Time 物件的初始值。
9.5
( 類 別 Complex) 請 撰 寫 名 為 Complex 的 類 別, 用 來 表 示 數 學 中 的 複 數 (complex
number),並寫一個程式測試之。複數的格式為
realPart + imaginaryPart * i
其中 i 為
請使用 double 變數表示此類別的 private 資料。提供一個建構子,於此類別的物件被宣
告時,設定其初始值。此建構子應具預設引數,供未提供參數時使用。此外,請提供
public 成員函式執行以下工作:
a) 兩個複數相加:實部和實部相加,虛部和虛部相加。
b) 兩個複數相減:先將左運算元的實部減去右運算元的實部,再將左運算元的虛部
減去右運算元的虛部。
c) 用形如 (a,b) 的格式,列印 Complex 的值;其中 a 表示實部,b 表示虛部。
9.6
( 類別 Rational) 請撰寫名為 Rational 的類別,用來執行非整數算術,並寫一個程式測試
之。
9-49
9-50
C++程式設計藝術
請使用整數變數表示類別的 private 資料,即分母和分子。提供一個建構子,於此類別
的物件被宣告時,設定其初始值。建構子應具預設值,供未提供參數時使用,並應以
最簡分數的形式儲存分數。例如,以下分數
會在物件內將分子儲存為 1,而將分母儲存成 2。請提供執行以下工作的 public 成員
函式:
a) 將兩個 Rational 數字相加,結果存成最簡分數。
b) 將兩個 Rational 數字相減,結果存成最簡分數。
c) 將兩個 Rational 數字相乘,結果存成最簡分數。
d) 將兩個 Rational 數字相除,結果存成最簡分數。
e) 以形如 a/b 的格式印出 Rational 的值,其中 a 為分子,b 為分母。
f) 以浮點數格式印出 Rational 的值。
9.7
( 加強 Time 類別 ) 修改圖 9.4-9.5 的類別 Time,加入成員函式 tick,其功能是把 Time 物
件儲存的時間增加一秒。Time 物件的狀態應永遠保持合法。撰寫一個測試程式,利用
迴圈測試 tick 成員函式,每執行一次迴圈操作,以標準時間格式列印其內容,確認 tick
成員函式運作正確。請測試以下的情況:
a) 進位到下一分鐘。
b) 進位到下一小時。
c) 進位到下一日 ( 即 11:59:59PM 進位到 12:00:00AM) 。
9.8
( 加強 Date 類別 ) 修改圖 9.13-9.14 的 Date 類別,檢查用來設定資料成員 month、day 和
year 的初始值。另外,請提供成員函式 nextDay,用來遞增日數。Date 物件的狀態應
該永遠維持合法。撰寫一個測試程式,利用迴圈測試 nextDay 函式,每跑一次迴圈,
就印出時間日期,以確認 nextDay 成員函式運作正確。請測試以下的情況:
a) 進位到下個月。
b) 進位到明年。
9.9
( 結 合 類 別 Time 與 Date) 請 結 合 習 題 9.7 和 9.8 的 類 別 Time 和 Date, 設 計 名 為
DateAndTime 的類別。( 第 12 章會討論繼承,到時就可快速完成這項任務,不需修改
現有的類別定義 )。請修改 tick 函式,當時間遞增到明天時,呼叫 nextDay 函式。並請
修改函式 printStandard 和 printUniversal,輸出日期和時間,並撰寫一個程式測試新類
別 DateAndTime,特別是時間遞增到明天的狀況。
9.10 ( 藉類別 Time 的 set 函式的傳回值指出錯誤 ) 修改圖 9.4-9.5 中類別 Time 的 set 函式,當
傳入的值無效時,會傳回一個特別的值,指出發生了錯誤。請撰寫程式測試新版的
Time 類別;當 set 函式傳回錯誤值時,顯示錯誤訊息。
9.11 ( 類別 Rectangle) 請設計類別 Rectangle,此類別包含 length 和 width 兩個屬性,預設值均
為 1。請提供成員函式,計算周長和面積。此外,請提供處理 length 和 width 屬性的 set
和 get 函式。set 函式應檢查 length 及 width 是否為浮點數,且其值介於 0.0 和 20.0 之間。
第九章
類別:深入了解;拋出異常
9.12 ( 加強類別 Rectangle) 請改良前一個習題設計的類別 Rectangle,使其儲存矩形四個頂
點的直角座標。建構子呼叫一個接收四組座標的 set 函式,並驗證每個座標都位於第
一象限內,且沒有任何一個 x 或 y 座標大於 20.0。此外,set 函式亦須確認所提供的 4
組座標值的確形成一矩形。成員函式須計算 length ( 長 )、width ( 寬 )、perimeter ( 周
長 ) 和 area ( 面積 )。較長的邊為長 (length)。最後,請提供判斷函式 square 判斷該矩
形是否為正方形。
9.13 ( 加強類別 Rectangle) 請修改習題 9.12 的 Rectangle 類別,加入 draw 函式,於 25x25 的
四方形區域 ( 第一象限 ) 中繪出矩形,並加入 setFillcharacter 函式,用來指定填入矩形
內部的字元。另外,請加入 setPerimeterCharacter 函式,指定描繪矩形邊框的字元。讀
者若有興趣,還可加入函式縮放矩形的大小,或在第一象限指定的區域內,對它進行
移動和旋轉。
9.14 ( 類 別 HugeInteger) 請 建 立 使 用 具 40 個 元 素 陣 列 的 HugeInteger 類 別, 用 來 表 示 最
高 可 達 40 位 數 的 整 數。 請 提 供 成 員 函 式 input、output、add 和 substract。 為 比 較
HugeInteger 物 件, 請 提 供 isEqualTo、isNotEqualTo、isGreaterThan、isLessThan、
isGreaterThanOrEqualTo 和 isLessThanOrEqualTo 等函式。它們都是判斷函式,如果兩
個大整數之間的關係成立,則傳回 true,否則傳回 false。此外,請提供另一個判斷函
式 isZero。若有興趣,讀者還可加入 multiply、divide 和 modulus 等成員函式。
9.15 ( 類 別 TicTacToe) 請 建 立 類 別 TicTacToe 及 一 個 完 整 的 程 式, 實 作 井 字 遊 戲 (tic-tactoe)。類別應包含一個 private 的 3x3 二維整數陣列。建構子將每個方格的初始值設為
0。此遊戲可以有兩位玩家;第一位玩家指定的方格置入 1,第二位玩家置入 2。每步
只能放到空白的方格中。每進行一步,必須判斷是否有人贏了或是和局。讀者如果有
興趣,可讓人和電腦比賽。此外,您可以讓遊戲者決定誰先下誰後下。若還意猶未
盡,可以再寫一個程式,用 4x4x4 的陣列玩三度空間的井字遊戲。[ 請注意:此專案極
具挑戰性,完成它可能要花好幾個星期。]
9.16 (this 指標 ) 解釋甚麼是 this 指標。將 sizeof 用在 this 指標結果為何,解釋當中意涵觀念。
9.17 ( 建構子多載 ) Time 類別的定義可否包括下列兩個建構子:
Time( int h = 0, int m = 0, int s = 0 );
Time();
作為預設建構子?若答案為否,請解釋為什麼?
9.18 ( 複製建構子 ) 當一類別物件的參考傳遞給建構子,會發生甚麼事?
9.19 ( 修改 Date 類別 ) 請修改圖 9.17 的 Date 類別,讓它具有以下功能:
a) 以多種格式來輸出日期,例如
DDD YYYY
MM/DD/YY
June 14, 1992
b) 使用多載建構子產生 Date 物件,並按 a) 的格式設定初始值。
c) 請建立一個 Date 建構子,使用 <ctime> 定義的標準程式庫函式,讀取系統時間,
用來設定 Date 的成員。請參考您手上編譯器的文件,或參考 www.cplusplus.com/
9-51
9-52
C++程式設計藝術
ref/ctime/index.html 網 站, 取 得 <ctime> 標 頭 檔 的 相 關 資 訊。 您 可 能 也 想 檢 視
C++11 新的 chrono 程式庫 : en.cppreference.com/w/cpp/chrono
在第 10 章中,我們會建立運算子,測試兩個日期是否相等,並且比較兩個日期決定其
前後順序。
9.20 (SavingsAccount 類別 ) 建立 SavingsAccount 類別,用 static 資料成員 annualInterestRate
儲存帳戶的年利率。類別的每個物件都包含一個 private 資料成員 savingsBalance,指
出每位帳戶內的目前存款餘額。請提供成員函式 calculateMonthlyInterest,計算每月的
存款利息,其計算方式是將 balance 乘以 annualInterestRate 再除以 12;計算後的利息
必須再加上 savingsBalance。請提供 static 成員函式 modifyInterestRate,用來設定 static
annualInterestRate ( 年 利 率 ) 的 新 值。 請 撰 寫 一 個 程 式 來 測 試 SavingsAccount, 產 生
兩個不同的 SavingsAccount 類別的物件:saver1 和 saver2,其餘額分別為 $2000.00 和
$3000.00。把 annualInterestRate 的值設為 3%,計算每個月的利息,並且印出每位存款
者的新餘額。接下來,請將 annualInterestRate 設定為 4%,計算下個月的利息,並印出
每個存戶的新存款。
9.21 (IntegerSet 類別 ) 建立類別 IntegerSet,其物件能儲存 0 至 100 範圍內的整數集合。用
bool 型態的 vector 表示集合。如果整數 i 位在該集合內,則陣列元素 a[i] 的值為 true。
如果整數 j 不在此集合中,則陣列元素 a[j] 為 false。預設建構子會將集合的初始值設成
空集合,意思是說,其元素全部為 false。
a) 請為一般的集合運算,提供對應的成員函式。舉例來說,請提供 unionOfSets 成員
函式,用來建立兩個集合的聯集 ( 意思是,如果目前兩個集合的元素之中任一個
或兩個都是 true,則請將聯集的元素也設定為 true;如果目前兩個集合的元素都是
false,則請將聯集的元素都設定為 false。)
b) 提供 intersectionOfSets 成員函式,建立兩個集合的交集 ( 意思是,如果目前兩個集
合的元素之中任一個或兩個元素都是 false,則交集的元素也都必須設定為 false;
如果目前兩個集合的元素都是 true,則將交集的元素都設定為 true。)
c) 提供 insertElement 成員函式,將一個新的整數 k 加入集合中 ( 即將 a[k] 設定為 true)。
請提供 deleteElement 成員函式,將整數 m 從集合中刪除 ( 即將 a[m] 設定為 0)。
d) 提供一個 printSet 成員函式,將集合中的數字,以空白隔開後列印出來。請列印出
現在集合中的元素即可 ( 即所在 vector 位置的值是 true 的元素 )。如果遇到空集合,
請印出 ---。
e) 提供 isEqualTo 成員函式,判斷二個集合是否相等。
f) 請 提 供 接 受 整 數 陣 列 和 陣 列 大 小 的 額 外 建 構 子, 並 用 陣 列 設 定 集 合 物 件 的 初
始值。
現在,撰寫一個驅動程式測試 IntegerSet 類別。建立幾個 IntegerSet 物件,並測試所有
成員函式是否能正確運作。
9.22 ( 修改 Time 類別 ) 我們可以讓圖 9.4-9.5 定義的 Time 類別,用自午夜開始計時的秒數,
作為類別內部的時間表示法,代替 hour、minute 和 second 三個整數。用戶端可以使用
第九章
類別:深入了解;拋出異常
相同的 public 函式,並獲得相同的結果。請修改圖 9.4 的類別 Time,依據從午夜開始
計時的秒數實作類別 Time,並說明此類別的使用者不會察覺功能上的改變。[ 請注意:
本習題優雅地示範隱藏實作的好處。]
9.23 ( 洗牌和發牌 ) 建立一個程式,能將一副撲克牌洗牌和發牌。本程式應該包含類別
Card、類別 DeckOfCards 和一個測試程式。Card 類別應該提供:
a) int 型態的資料成員 face 和 suit。
b) 一個建構子,接收代表花色和點數的兩個 int 值,並用它們來初始化資料成員。
c) 兩個字串型態的 static 陣列,分別代表花色和點數。
d) toString 函式,將 Card 以字串“face of suit”的格式回傳。您可以用 + 運算子來串
接字串。
DeckOfCards 類別應該包含:
a) Card 型態的 vector,其名稱為 deck,用來儲存 Card。
b) 整數 currentCard,代表下一張要發的牌。
c) 預設建構子,用來初始化 deck 中的 Card。在每張 Card 建立並初始化之後,建構子
應該使用 vector 函式 push_back 將這張牌加入 vector 的尾端。52 張牌都應該執行上
述動作。
d) shuffle 函式,用來洗牌。洗牌演算法應該對 vector 中的每張牌執行一次:針對每張
牌,隨機選另一張牌與它交換。
e) dealCard 函式會回傳這副牌中的下一張 Card。
f) moreCards 函式會回傳 bool 值,指出是否還有牌要發。
測試程式應該建立一個 DeckOfCards 物件,將一副牌洗牌,並發出 52 張牌。
9.24 ( 洗牌和發牌 ) 修改習題 9.23 的程式,使發牌函式能夠一次發出 5 張牌。請撰寫函式完
成下列工作:
a) 判斷這 5 張牌裡是否有對子。
b) 判斷這 5 張牌裡是否有雙對子。
c) 判斷這 5 張牌裡是否有三條。( 如三張傑克 )
d) 判斷這 5 張牌裡是否有四梅。( 如四張 A)
e) 判斷這 5 張牌裡是否有同花。( 即五張同樣花色的牌 )
f) 判斷這 5 張牌裡是否有順子。( 即五張連續的牌 )
洗牌和發牌的專題
9.25 ( 洗牌和發牌 ) 使用習題 9.24 所發展的函式撰寫一個程式,讓它能夠發出兩家,每家五
張牌的一局牌,然後檢視每付牌,並且判斷哪一付牌較好。
9.26 ( 洗牌和發牌 ) 修改習題 9.25 的程式,模仿莊家發牌。五張牌發出時必須面朝下,玩家
無法看到牌。然後,程式應檢視莊家手上的牌,按照這付牌,使莊家再抽一、二或三
張牌,取代手上不需要的廢牌。然後,程式應再檢視莊家手上的牌。
9-53
9-54
C++程式設計藝術
9.27 ( 洗牌和發牌 ) 請修改習題 9.26 的程式,自動處理莊家的牌,但參加者可決定要換掉手
中的哪幾張牌。然後,程式應檢視兩付牌,判定誰贏。請使用這個新程式,與電腦進
行 20 場比賽。誰贏得比較多場次的比賽,您或電腦?讓您的夥伴與電腦進行 20 場比
賽。誰贏的場次較多?請依據比賽的結果,適度修改這個撲克牌遊戲程式。再玩 20 場
遊戲。修改後的遊戲程式有比較好的成績嗎?
創新進階題
9.28 ( 項目:應急類別 ) 北美應急響應服務,9-1-1,呼叫者連接到本地公共服務應答點
(PSAP)。傳統上,PSAP 會詢問呼叫者身分識別資訊,包括呼叫方的地址,電話號碼
和緊急情況的性質,然後派遣適當的緊急救援人員 ( 如警察,救護車或消防部門 )。增
強型 9-1-1 ( 或 E9-1-1) 使用計算機和資料庫確定呼叫者的實際地址,引導呼叫者到最
近的 PSAP,並顯示來電者的電話號碼和地址給接收人員。無線增強型 9-1-1 提供呼叫
接收者一種用於無線呼叫的身分識別。識別信息在兩個階段推出,第一階段通訊頻道
須提供無線電話號碼和傳輸站的位置或基地台位置。第二階段通訊頻道必須提供呼叫
者的位置 ( 使用技術如 GPS)。要了解更多關於 9-1-1 資訊,請造訪 www.fcc.gov/pshs/
services/911-services/Welcome.html 和 people.howstuffworks.com/9-1-1.htm 兩個網站。
建立一個類別,最重要的就是確定類別的屬性 ( 實體的變數 )。對於本範例類別的設
計,請先在互聯網上研究 9-1-1 服務。然後,設計出一個名為 Emergency 的類別,可在
9-1-1 應急響應物件導向系統中使用。列出能表達緊急事件的所有屬性。例如,類別的
一些必要屬性,包括:通報者資訊 ( 包括他們的電話號碼 )、緊急事件發生的位置、通
報時間、緊急情況性質、響應類型和響應的狀態。類別屬性應該能完整地描述問題的
性質和發生時間等緊急事件來龍去脈詳細資訊,以便於後續單位接手解決問題。
Download