本章重點: 利用其它特殊的 成員函式創造...

24
在第 6 章「利用建構式和解構式創造更好的抽象性」,我們知道成員函式可以: 在一開始就將物件初始化(預設建構式) 清除物件(解構式) 這一章將介紹另外三個成員函式,它們可幫助你創造更好的抽象性。這些成員函式 可以: 設定物件(設定運算子) 以其它物件為基礎,將物件初始化(copy 建構式) 顯示物件(顯示成員函式) 這些成員函式都將在其所屬的章節中討論。當談到設定和列印,我們也將看到在 C++ 中,兩種更常見的運算子多載的用法。 第九章 利用其它特殊的 成員函式創造 更好的抽象性 1 1 5 本章重點: 設定運算子 Copy 建構式 顯示一個物件 摘要

Upload: others

Post on 11-Nov-2020

2 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

在第 6 章「利用建構式和解構式創造更好的抽象性」,我們知道成員函式可以:

在一開始就將物件初始化(預設建構式)

清除物件(解構式)

這一章將介紹另外三個成員函式,它們可幫助你創造更好的抽象性。這些成員函式

可以:

設定物件(設定運算子)

以其它物件為基礎,將物件初始化(copy 建構式)

顯示物件(顯示成員函式)

這些成員函式都將在其所屬的章節中討論。當談到設定和列印,我們也將看到在

C ++ 中,兩種更常見的運算子多載的用法。

第九章

利用其它特殊的成員函式創造更好的抽象性

1 1 5

本章重點:

* 設定運算子

* Copy 建構式

* 顯示一個物件

* 摘要

Page 2: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

1 1 6 / 第九章

就像預設建構式和解構式一樣,你必須瞭解在這?所討論的成員函式與類別架構之

間的互動關係。我們將使用在第 6 章所介紹的類別架構,TextB ox 衍生自 R ect,並

擁有三個資料成員,分別是類別 Color、int 和 char*:

// 第 6 章的類別架構

class Rect {

// ...

private:

int top, left;

int width, height;

};

class Color {

// ...

private:

int data;

};

class TextBox : public Rect {

// ...

private:

Color textColor;

int frameThickness;

char *text;

};

設定運算子第一個討論的成員函式是設定運算子,它允許你使用標準的設定敘述,將一個類別

的實體設定給其它的實體:

main() {

TextBox source, destination;

// ...

destination = source; // 指定一個 TextBox 實體給其它實體

}

Page 3: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

利用其它特殊的成員函式創造更好的抽象性 / 1 1 7

對 C 的 struct 而言,設定被定義為將 source 的每個位元複製到 destination。但這

對 C++ 的物件而言並不適用。考慮圖 9-1 的例子,這是以 C 的 s truct 的方式來設

定一個 T extB ox 實體。在“設定之前”下面的是原始資料,而在“設定之後”下面

的則是將 source 設定給 destination 所得到的結果。

圖 9 -1 在 C 語言中的 TextBo x 設定

在圖中所顯示的設定之後,source 和 dest inat ion 皆指向同一個字串“hi”。如果其

中一個物件改變這個字串,就會造成問題。其中一個物件可能在其解構式釋放這個

字串,然而另一個物件仍要使用它。除此之外,dest inat ion 所指的字串“lo”已經

不見了,而且沒有被釋放 - 造成記憶體的垃圾。

C 程式設計者認為設定運算子無法用於絕大部份的 s truct ,因此他們通常撰寫一個

像 copy() 這類的函式來處理設定的問題。但在 C ++,你可以使用設定的語法,並

仍能獲得 copy() 函式的彈性。為了達到這個目的,你必須在類別的宣告中增加一

個成員函式:

設定之前

設定之後

Page 4: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

class TextBox {

public:

void operator=(TextBox &source); // 設定時啟動

//... // 注意:參數型態是參考位址

};

這個新成員函式的名稱是 operat or =,也就是關鍵字 operator 後面再接設定運算

子。它稱為設定運算子成員函式,或設定運算子(assi gnment operator)。這個成員

函式需要一個參數:此函式所屬之類別的物件的參考位址。當你將一個 T extB ox 設

定給另一個 TextBox 時,就會啟動這個函式:

main() {

TextBox source, destination;

// ...

destination = source; // 呼叫 Text::operator=()

}

main() 的最後一行啟動了 des tinati on 物件的 T extB ox::operator=( ),並以 s ource 做

為 引 數 。 這 個 設 定 敘 述 事 實 上 是 一 個 函 式 呼 叫 。 請 注 意 , 函 式

TextB ox::operator=(T extB ox &) 接受一個參考位址參數。但我們並不希望使用者自

己傳遞 s ource 的位址,也不想要以數值的方式來傳遞參數。使用參考位址參數就

可以利用與內建型態相同的語法來設定類別,而不像使用數值參數時,必須複製整

個類別,因而造成效率的瓶頸。

請不要因為設定運算子成員函式的奇怪名稱、或是不尋常的啟動時機而感到困惑;

它只是一個普通的函式。如果你要的話,也可以像一般的函式一樣呼叫它:

main() {

TextBox source, destination;

// ...

destination.operator=(source); // 也可以這樣呼叫設定運算子,但通常不會

}

在上面這個例子中,我們使用標準的成員函式語法來啟動設定運算子。除了較難理

解之外,它和之前的例子完全相等。稍後你將會知道,某些時候你必須使用這種自

行啟動函式的語法。

1 1 8 / 第九章

Page 5: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

設定運算子的定義

現在已經準備好定義 TextB ox 的設定運算子 我們必須設定所有的成員,而且別忘

了為 text 配置屬於自己的記憶體。下面的程式碼就是其定義:

void TextBox::operator=(TextBox &source) { // TextBox 指定運算子的定義

// 1:不能指定給自己

if (this == &source) return;

// 2:啟動基礎類別的指定運算子

Rect::operator=(source);

// 3:啟動資料成員的指定運算子

textColor = source.textColor;

frameThickness = source.frameThickness

// 4:記憶體管理和指定給指標成員

delete[] text;

if (source.text != 0) {

text = new char[strlen(source.text)+1];

strcpy(text, source.text);

}

else {

text = 0;

}

}

這個範例的程式碼可分為四組,我們已為各組加上註解。如果來源和目標相同,那

麼藉由第一組的程式,這個函式就會結束執行。我們藉由比較 source 和 thi s(代表

目標的位址)的位址來避免自我設定。這不僅是一個簡單的最佳化,它還可以避免

因為釋放了記憶體,但在第四組程式碼中仍必須使用它所造成的悲劇。即使在一個

良好的程式碼中,設定給自己的情況仍然可能出現,因此做個檢查是個不錯的主

意。

利用其它特殊的成員函式創造更好的抽象性 / 1 1 9

Page 6: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

第二組程式碼將來源的基礎類別(R ect)部份設定給目標。做法是呼叫 R ect 的設

定運算子,也就是讓此物件的基礎類別設定給自己。我們必須在函式名稱前加上

R ect ::,因為 R ect 基礎類別和 TextB ox 衍生類別都有一個 operator= 函式。(我們

在第 5 章「利用合成與衍生所建立的階層架構」曾討論過名稱限制的問題。)如果

我們這麼寫:

operator=(source); // 和第二組程式碼不同

就會重複不斷地啟動 Text Box: :operator=( )。如果使用型態轉換,就可以使用標準的

設定敘述:

*(Rect*)this = *(Rect*)&source; // 和第二組程式碼相同,但較難看懂且不安全

在這個例子中,我們將來源和目標的型態轉換成它們的 R ect 部份,藉以設定 R ect

部份。當然這種作法行得通,但是卻很醜陋,更重要的是,型態轉換並不安全,如

果有比較好的解決方式,就應該避免使用它。

請注意 T extB ox::operator=(R ect &) 需要一個 R ect 的參考位址,但在第二組程式碼

所傳的是 R ect::operator=(source),它的型態是 T extB ox。這種作法不會發生問題,

因為就如在第 8 章「參考位址」所討論自動指標轉換,參考位址可以繫結到一個衍

生類別物件的基礎類別部份。因此我們可以將一個 T extB ox 實體傳給一個預期

R ect 參考位址的函式,而編譯器將會正確地繫結這個參考位址。

第三組程式碼設定對應的 textC ol or 和 frameT hickness 資料成員。對絕大多數的資

料成員而言,這麼做就已經足夠了,它們的設定運算子就可以處理所有的細節了。

除了所用的語法之外,第二組和第三組程式碼非常類似。第二組程式碼會連接到基

礎類別的設定運算子,第三組程式碼會連接到資料成員的設定運算子。對在第三組

程式碼中的內建型態(像是在這個例子中的 frameThickness)而言,連接到資料成

員的設定運算子就表示使用內建的設定方式,也就是複製每個位元。

第四組程式碼在 text 資料成員(使用你已經學過的 new[] 和 delet e[])之間複製字

串。在配置記憶體以及由 s ource 複製新字串之前,必須先釋放舊字串所佔據的空

間,這是指標資料成員最常見的處理方式。一般來說,我們必須先釋放舊記憶體,

然後再配置及設定新的記憶體。

1 2 0 / 第九章

Page 7: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

我們可以讓設定運算子做任何我們希望它做的事。你可以把它定義為:

void TextBox::operator=(TextBox &source) {

cout << "forget it, I'm not assigning anything\n";

}

當我們試著將一個 TextB ox 設定給另一個 Text B ox 時,它只會印出一段使人不耐

煩的訊息,甚至連設定的動作都不會執行。雖然我們並不建議你這麼做,但編譯器

並不會產生任何錯誤訊息。

神奇的設定

設定運算子實際上比我們先前所介紹的更有彈性,它可以接受任何型態的參

數、可以被多載⋯⋯等等。想定義一個能用於運算式(像是 a=b=c)的設定運

算子也是可能的。必須讓設定運算子回傳一個值,再將這個值傳到下一個設

定運算子。

隱含式設定運算子

如果你並沒有為一個類別宣告設定運算子,那麼此類別將包含一個隱含的設定運算

子。隱含式設定運算子只會串連類別成員的設定運算子,因此通常也稱為成員型式

的設定。對於內建型態而言,設定只是將每個位元由來源複製到目標 - 位元型式

的設定。也就是說,指標成員將以圖 9-1 的方式來設定,然而類別無法接受這種設

定方式。

如果宣告了一個設定運算子,那麼就必須定義它,否則當你試著啟動這個設定運算

子時,就會產生一個連結錯誤。如果類別並不包含任何指標成員,通常使用隱含式

設定運算子就夠了。舉例來說,類別 R ect 就可以使用它的隱含式設定運算子:

利用其它特殊的成員函式創造更好的抽象性 / 1 2 1

Page 8: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

class Rect { // 使用隱含式設定運算子

// 不需要定義設定運算子

// ...

private:

int left, top;

int width, height;

};

R ect 的隱含式設定運算子會複製四個資料成員,而這也正是我們希望它做的事。

然而,如果我們在 R ect 中加入了 uniqueID,藉以辨識各個不同的 R ect,那麼就不

能使用隱含式設定運算子了:

class Rect { // 不能使用隱含式設定運算子

void operator=(Rect &src);

// ...

private

int left, top;

int width, height;

int uniqueId; // 每個 Rect 的 uniqueID 值都不同

};

當設定 Rect 時,我們並不希望這個欄位被複製,因此必須改寫隱含式設定運算子

以避免這個問題。然而,雖然獲得了由新的設定運算子所帶來的優點,但使用者並

不需要改變程式碼,這就是良好抽象性的完美之處。

不幸地,你並不能只為某些有必要的成員重新定義其設定。隱含式設定運算子會串

連所有類別成員的設定運算子。若將隱含式設定運算子改寫,那麼就完全不會串連

任何類別成員的設定運算子。如果你不希望串連所有的類別成員,那麼你就必須處

理整個設定的過程。

運算子多載和參考位址

運算子的改寫是 C ++ 運算子多載的一個例子。就像函式多載一樣,運算子多載允

許相同的運算子作用於許多不同的型態。對於我們在如此鄰近的章節中連續介紹

C ++ 兩個龐大的主題 - 參考位址和運算子多載,請不要太驚訝,因為在這個語言

中引入參考位址的目的,主要就是為了支援運算子多載。

1 2 2 / 第九章

Page 9: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

因為 C ++ 以函式呼叫的方式來實作運算子多載,因此有時必須以隱含式的方式傳

遞物件作為引數。此時物件必須以指標的方式來傳遞,但這種作法看起來卻很拙

劣,然而複製整個物件又太耗時間。如果是預設的設定,那麼你不能認為 dest =

src 能夠行得通,當你將它改寫之後,也不能預期 dest = &src 是正確的。這將會破

壞抽象性,而運算子多載正是為了解決這個問題所設計的。

進一步的運算子多載

在預設的情況,只有少數的運算會作用於類別實體 - 例如 =(設定)、&(物

件的位址)。運算子多載讓你可以重新定義像 + 和 -> 這類的運算子。舉例來

說,你可以在 C ++ 中建立一個 T ime 類別,並且為此類別定義 +,這樣就不必

受限於一定要將 Time 實作為 int。你甚至可以讓一個陣列類別像內建陣列一

個使用括號。這一章將涵蓋兩個最常見的運算子多載的使用法:一個是先前

所討論的設定運算子,另一個則是稍後要討論的顯示運算子。

Copy 建構式本章所要介紹的第二個成員函式是 copy 建構式。在第 6 章,我們知道預設建構式

可以從無到有建立一個物件,並將它放到預設的狀態。另一種方法就是根據現有的

物件來建立新的物件,而這也就是 copy 建構式所做的事。舉例來說:

main() {

TextBox t1; // 建立 t1

TextBox t2 = t1; // 利用 t1 來建立 t2

// ...

}

main() 的第二行看起來像是一個指標,但是在宣告時所使用的設定運算子,實際上

是一個初始化。在 C 語言中,設定和初始化之間的差異並不大,兩者都只是將各

個位元由來源複製到目標。但在 C ++ 語言中,這個差異就非常重要,設定會改變

一個已經建構之物件的值,而初始化則會建構一個新的物件,並同時給它一個值。

利用其它特殊的成員函式創造更好的抽象性 / 1 2 3

Page 10: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

這種初始化其實可以用這種方式來實作:先呼叫預設建構式,接著再呼叫設定運算

子【註】。這種作法將使得初始化和設定之間的界限變得非常模糊。然而,對一個

巨大的物件而言,這將是一種浪費,因為它將物件放入預設狀態之後,立刻就改變

它的狀態。C + + 藉由一個新增的建構式來改善這個問題:copy 建構式。這個建構

式就像這樣:

class TextBox {

public:

TextBox(TextBox &source); // copy 建構是的宣告

//... // 注意參數型態是參考位址

};

接受參數的建構式

事實上,一個類別可擁有接受任意參數的建構式。舉例來說,一個字串類別

可能擁有:

class String {

public:

String(); // 預設建構式

String(String &src); // copy 建構式

String(char *src); // 由內建字串建構

String(char src[], size_t len); // 由字元陣列建構

// ...

};

每個建構式都可用於各種不同的狀況。你可以用以下的方式來啟動它們:

main() {

String s1; // 啟動預設建構式

String s2 = s1; // 啟動 copy 建構式

String s3("hello"); // 啟動上面第三個建構式

String s4("hello", 5); // 啟動上面第四個建構式

// ...

}

本書只介紹前兩種建構式:預設建構式和 copy 建構式。

1 2 4 / 第九章

註 C++ 的前身(使用類別的 C)就是用這種方式來實作初始化。

Page 11: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

copy 建構式或許是 C ++ 中最微妙的成員函式。它有一點像預設建構式,也有一點

像設定運算子。舉例來說,它的名稱就像預設建構式,也像設定運算子一樣接受一

個參數。當一個物件根據同類別的另一個物件來建構時,就會啟動 copy 建構式:

main() {

TextBox t1; // 呼叫預設建構式

TextBox t2 = t1; // 呼叫 copy 建構式

t2 = t1; // 呼叫指定建構式

}

main() 的每一行會啟動一個不同的 Text Box 成員函式。第一行啟動預設建構式;第

三行啟動設定運算子。而在第二行,t2 同時被宣告和設定,這是由 copy 建構式所

處理的。

copy 建構式的定義

我們現在必須定義 copy 建構式。在這一節中,我們將教你比較簡單的方法,而不

是最有效率的做法,因為你必須先瞭解 copy 建構式要做什麼,以及它如何完成這

些工作。以比較有效率的方法來實作 copy 建構式是本章稍後的進階主題。

就像第 6 章所討論的預設建構式一樣,copy 建構式也會自動串連屬於類別實體之

類別成員的預設建構式。因此當 copy 建構式的程式本體開始執行時,這些成員皆

已經由預設建構式而建構完成了。知道這些成員會自動被預設建構式所建構之後,

我們就可以用設定運算子來複製它們:

TextBox::TextBox(TextBox &source) { // copy 建構式的定義

// 1:此時,屬於類別實體的成員已經被預設建構式所建構。現在藉由指定運算子,完成 copy 建構過程。

// 2:啟動基礎類別的指定運算子

Rect::operator=(source);

// 3:啟動資料成員的指定運算子

frameThickness = source.frameThickness;

textColor = source.textColor;

// 4:記憶體管理和設定指標成員,注意:不需要釋放記憶體

if (source.text != 0) {

text = new char[strlen(source.text)+1];

利用其它特殊的成員函式創造更好的抽象性 / 1 2 5

Page 12: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

strcpy(text, source.text);

}

else {

text = 0;

}

}

沒錯,這個定義的第一行真的有三個 T extB ox。T extB ox 是這個函式所屬的類別、

這個函式的名稱、以及這個函式的參數型態。將這個定義和預設建構式的定義、前

一節所討論的設定運算子之間做個比較,是件很有意義的事。就像預設建構式,屬

於類別實體的成員都自動被預設建構式所建構。就像設定運算子,藉由設定其成員

的方式來複製物件。請注意在為 text 配置空間之前,並不需要使用 del ete[] 來釋放

原本的 t ext,因為我們建立了一個新的物件。如果你忘了 Rect: :operator=(source)

這個語法的意義,請回頭看看前一節對設定運算子的討論,或是參考第 5 章的內

容。

copy 建構式是一個複雜的函式;因此讓我們來複習一下基本的觀念。當一個物件

被宣告,而且同時根據一個同型態的物件來初始化時,就會啟動 copy 建構式。我

們使用成員函式來製作 copy 建構式,這個成員函式實作了預設建構式和設定運算

子。在 copy 建構式的程式本體執行之前,屬於類別實體的成員將自動被預設建構

式所建立,接著將串連每個類別成員的設定運算子。

隱含式 copy 建構式

如果你並沒有為某個類別定義一個 copy 建構式,那麼這個類別將包含一個隱含式

copy 建構式。這個隱含式 copy 建構式只會串連每個類別成員的 copy 建構式。內

建型態之實體的 copy 建構式會將每個位元由來源複製到目標。

如果類別沒有指標成員,那麼用隱含式 copy 建構式就可以了。舉例來說,Rect 就

可以使用隱含式 copy 建構式:

class Rect { // 可以使用隱含式 copy 建構式

// ...

private:

int left, top;

int width, height;

};

1 2 6 / 第九章

Page 13: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

就像其它的成員函式一樣,如果你宣告了一個 copy 建構式,那麼你必須定義它,

否則當你啟動它時,將會產生一個連結錯誤。

請注意在隱含式 copy 建構式和明確 copy 建構式之間,串連行為的差異。隱含式

copy 建構式會串連所有類別成員的 copy 建構式。明確式 copy 建構式只會串連屬

於類別實體之類別成員的預設建構式。沒錯,這確實讓人覺得困惑,你必須花一些

時間才能瞭解 copy 建構式。

利用成員初始化串列撰寫較有效率的 copy 建構式

我們說 copy 建構式被創造的目的,是為了避免在預設建構式執行之後,再做

一次設定。但我們卻仍然以這種方法來實作 copy 建構式:先讓類別成員由預

設建構式來建構,然後再設定它們的值。為什麼呢?因為要讓它儘量保持簡

單。尚未考慮最佳化的情況,copy 建構式本身就非常的複雜。

你可以讓 copy 建構式串連所有成員的 copy 建構式,而不是所有成員的預設

建構式,也不是只串連屬於類別實體之成員的 copy 建構式。這牽涉到一種稱

為成員初始化串列的連階主題。成員初始化串列可使用於任何一個建構式,

以說明該如何建構此類別的成員。因此,我們可以這麼做:

TextBox::TextBox(TextBox &source) // 成員初始化陣列:

: Rect(source), // 基礎類別

textColor(source.textColor), // 資料成員

frameThickness(source.frameThickness), // 資料成員

{

if (source.text != 0) {

text = new char[strlen(source.text)+1];

strcpy(text, source.text);

}

else {

text = 0;

}

}

這個串列在函式名稱以及一個冒號的後面,但在函式的程式本體之前。它將

告訴編譯器啟動某些特定類別成員的 copy 建構式。copy 建構式之程式本體所

剩下的唯一工作就是處理 text 資料成員。

利用其它特殊的成員函式創造更好的抽象性 / 1 2 7

Page 14: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

避免隱含式預設建構式的啟動

如果你改寫了隱含式 copy 建構式,那麼你也必須同時改寫隱含式預設建構式,因

為編譯器將不再為你提供這個隱含式預設建構式。下面是一個 Foo 類別,它有一

個 copy 建構式,但並沒有預設建構式:

class Foo {

public:

Foo(Foo &source); // 只改寫隱含式 copy 建構式

};

main() {

Foo f; // 錯誤:Foo 沒有預設建構式

// ...

}

當你試著建立一個 F oo 的實體時,編譯器將告訴你,它並沒有預設建構式,因此

無法建立它的實體。這個錯誤可以確定你不會無意間忘了改寫隱含式預設建構式。

少數類別需要一個特殊的 copy 建構式,但並不需要一個特殊的預設建構式。請注

意你可以改寫預設建構式,並繼續使用隱含式 copy 建構式。

以數值的方式傳遞物件

當一個物件根據另一個相同型態之物件來初始化時,就會啟動 copy 建構式。我們

曾舉過一個關於初始化的例子,就是當宣告一個物件時,也同時設定它的值。同樣

地,當我們以數值的方式傳遞參數時,也是根據另一個物件來建構一個物件。舉例

來說,我們已經宣告了一個函式 draw(),它接受一個以數值的方式傳遞的 Text B ox

參數:

void draw(TextBox tbox);

main() {

TextBox my_tbox;

// ...

draw(my_tbox);

}

1 2 8 / 第九章

Page 15: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

當在 main() 之中啟動這個函式時,參數 tbox 將根據引數 my_tbox 來初始化。這個

初始化的過程將使用 copy 建構式。函式 draw() 自己複製了一個 T extB ox,因此它

對 t box 所做的任何改變都不會反映到 my_tbox。對數值參數而言,這是正確的行

為。因此 copy 建構式允許我們以傳值的方式來傳遞物件,而且就像傳遞內建型態

的實體一樣容易。

請注意,只有當我們傳遞數值時,才會呼叫物件的 copy 建構式。在下面的例子

中,我們以三種不同的方式來傳遞 TextB ox:

void vfunc(TextBox t); // 以數值方式傳遞物件

void rfunc(TextBox &t); // 以參考位址方式傳遞物件

void pfunc(TextBox *t); // 以指標方式傳遞物件

main() {

TextBox tbox;

vfunc(tbox); // 啟動 TextBox 的 copy 建構式

rfunc(tbox); // 不啟動 TextBox 的 copy 建構式

pfunc(&tbox); // 不啟動 TextBox 的 copy 建構式

}

只有呼叫 vfunc() 才會啟動 TextB ox 之 copy 建構式,因此 vfunc() 擁有自己的物

件,而且可以做任何它想做的事。另外兩個函式都沒有建立新的 TextB ox 物件,它

們對 TextB ox 物件所做的任何改變,都將反映到原本的物件。

這可能會讓你對 copy 建構式感到困惑。copy 建構式將接受一個物件的參考位址,

然而,若物件是以數值的方式來傳遞,copy 建構式仍然會被啟動。因此,參考位

址參數的使用法之一,就是讓類別實體能夠像數值參數一樣傳遞。

如果你忘了 copy 建構式的參數必須是一個參考位址,那麼你將會看到 C++ 的一些

奇怪行為。正如我們曾說過的,當參數是以數值的方式來傳遞時,copy 建構式也

會被啟動。但是,如果不小心忽略了 copy 建構式的參數的 &,那麼這個函式將會

得到一個數值參數:

class TextBox {

public:

TextBox(TextBox &source); // 正確:取得參考位址參數

TextBox(TextBox source); // 錯誤:沒有取得數值參數

//...

};

利用其它特殊的成員函式創造更好的抽象性 / 1 2 9

Page 16: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

當這個錯誤的 copy 建構式被啟動時,會建構屬於自己的參數,而且很快就會造成

堆疊的滿溢。許多編譯器都能找出這種錯誤。

以數值的方式回傳物件

當某個函式以數值的方式回傳其結果時,也會使用 copy 建構式。舉例來說,下面

這個函式將會回傳一個 TextB ox 類別的實體:

TextBox makeTbox() {

TextBox tbox;

// ...

return tbox;

}

在這個例子中,tbox 是用來當作 copy 建構式的來源,藉以建構保存此函式之結果

的暫存物件。為什麼要這麼做呢?因為當此函式回傳時,tbox 就不再存在了,但是

呼叫者卻需要這個函式的值。下面這個敘述呼叫函式 makeT box()(假設 print() 是

TexB ox 的一個成員函式):

makeTbox().print();

當執行這個敘述時,會依序發生下列的事件:

執行 makeTbox(),啟動 T extBox 的預設建構式以建立 tbox。

makeTbox() 回傳 tbox,啟動 T extB ox 的 copy 建構式以建立此物件的一個

複製。

呼叫剛才複製的 TextBox 物件的成員函式 print( )。

避免被複製

在結束 copy 建構式的討論之前,我們將告訴你如何關閉 copy 建構式和設定運算

子。對於實作基本抽象性 ─ 分數、點、時間值 ─ 的類別而言,複製是非常常見

而且重要的。這些類別都需要一個正確定義的設定運算子和 copy 建構式,才能算

是一個完整的實作。

1 3 0 / 第九章

Page 17: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

但是你也可能建立了一個類別,而且你不希望這個類別的實體被複製。通常是因為

這種複製極為罕見,因此你並不想做精確的定義或實作。舉例來說,把一個檔案系

統的實體指定給另一個實體,或是將一個飛行模擬物件以數值的方式來傳遞,該有

什麼樣的結果呢?

對於不希望被複製的類別,你並不需要提供設定運算子或是 copy 建構式。但若你

沒有提供它們,則此類別會包含一個隱含式的版本。當然,這個版本並不會做你希

望它做的事。你真正希望做的,是要求編譯器完全不允許設定和 copy 建構式的啟

動。

要避免設定和 copy 建構式的啟動其實很簡單。你只要將這些成員函式宣告為私有

的,而且不提供定義的部份就可以了。以下的例子示範如何避免一個雜湊表被複

製:

class HashTable {

public:

void insert(char *key, int value);

// ...

private:

void operator=(HashTable &src); // 不提供定義

HashTable(HashTable &src); // 不提供定義

};

這個類別的設定運算子和 copy 建構式都是私有的,而且未定義其程式本體。將它

們宣告為私有的,就可以避免使用者以這種方式複製 HashTable:

main() {

HashTable h1; // OK:呼叫預設建構式

HashTable h2 = h1; // 違法存取:copy 建構式是私有的

h2 = h1; // 違法存取:設定運算子是私有的}

在 main() 最後兩行的敘述中,當它們試著複製 h1,會造成違法存取。省略私有設

定運算子和 copy 建構式的程式本體,不僅比提供無用的定義來得容易,而且如果

你無意中試著在 Has hT abl e 的成員函式(不管這個函式是不是私有的)中啟動它

們,將會造成連結錯誤。

利用其它特殊的成員函式創造更好的抽象性 / 1 3 1

Page 18: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

顯示一個物件本章所要介紹的第三個成員函式可以讓類別實體顯示自已。在第 2 章「不使用類別

的 C ++ 中」,我們說明如何利用 C++ 的 iost ream 函式庫來顯示內建型態的實體。

你可以這麼寫:

#include <iostream.h>

main () {

cout << "recall printing: " << 2 << '+' << 2 << " = " << 2+2 << '\n';

}

並得到以下的輸出結果:

recall printing: 2+2 = 4

現在要學的是要如何以這種方式來顯示你自己的類別。雖然提供成員函式來顯示一

個物件並不是必要的,但如果所有類別實體都擁有顯示自己的能力,就可以簡化除

錯的過程。事實上,你必須定義兩個函式:顯示成員函式 ─ 稱為 print ( ),負責真

正的顯示工作,以及非成員顯示運算子函式 ─ 稱為 operator<<(),讓顯示的過程

可用類似前述例子的方式來啟動。接下來兩節將討論這些函式,第三節將說明如何

藉由串連來顯示複雜類別的實體。

顯示成員函式

要讓一個類別可以顯示自己,必須先增加一個成員函式 pri nt ():

class Rect {

public:

void print(ostream *os);

// ...

};

這個函式負責顯示一個 Rect 的實體。print() 的參數是一個指向 ostream 的指標,

os tream 就是 iost ream 函式庫中負責輸出的類別。舉例來說,我們可以將一個指向

cout 的指標傳遞到此函式,然後用以下這種方式來定義 print ():

void Rect::print(ostream *os) {

*os << "Rect{" << top << ", " << left << ", "

<< width << ", " << height << "}";

}

1 3 2 / 第九章

Page 19: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

它將顯示此物件的類別及其四個資料成員。我們可以用下面這種方式來顯示 R ect

物件:

#include <iostream.h>

main() {

Rect r;

// ...

cout << "the rect is: "

r.print(&cout); // prints r to cout

cout << '\n';

}

將獲得以下的輸出結果:

the rect is: Rect{1, 2, 3, 4}

顯示運算子(printing operator)

要用顯示運算子來顯示你自己的類別,就必須定義一個非成員函式:

ostream &operator<<(ostream &os, Rect &r) {

r.print(&os);

return os;

}

使用全域函式 operator<<(),可以讓顯示 R ect 的工作變得容易。此函式接受一個

ostream 的參考位址,以及一個要顯示之 R ect 的參考位址。它會呼叫 Rect 的成員

函式 print( ),並傳回所顯示之 ostream 的參考位址。

請注意,operator<<() 是一個全域函式,而不像 operator=() 是一個成員函式。每個

類別都有自己的全域 operator<< (),這些 operator< <() 函式之間是以第二個參數

(所要顯示之物件的型態)做為區別。這是另一個函式多載的例子【註】。

利用其它特殊的成員函式創造更好的抽象性 / 1 3 3

註 為什麼 operator=() 是一個成員函式,但 operator<<() 卻是一個全域函式呢?基本

上來說,operator<<() 不可以是一個成員函式,因為我們想撰寫 cout << my_object

這樣的敘述,把要顯示的物件放在第二個而不是第一個。如果二元運算子(像是 =

或 <<)是一個成員函式,那麼它必須是在其左邊之類別的成員。

Page 20: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

現在我們就可以像內建型態一樣顯示 R ect:

main() {

Rect r;

// ...

cout << "the rect is: "

cout << r; // prints r to cout

cout << '\n';

}

編譯器會將 cout << r 這個敘述解釋為:

operator<<(cout, r); // 編譯器對“cout << r”的解釋

事實上,我們可以這麼寫:

cout << "the rect is: " << r << '\n';

而此敘述將被解釋成:

operator<<(operator<<(operator<<(cout, "the rect is: "), r), '\n');

由內開始向外讀,我們看到這個多載的 operator<<() 函式被啟動了三次。這也說明

了為什麼 operator<<(ostream &, R ect &) 會回傳 ost ream 的參考位址。這個參考位

址將成為下一次呼叫 operator<<() 的 ostream 參數。這種作法讓我們可以用 R ect 當

作一連串顯示的一部份。

請注意,我們在 operator << ( ost r eam & , R ect &) 所用的是參考位址,但在

R ect: :p r in t(ostream *) 所用的卻是指標。我們當然也可以在 pri nt () 中使用參考位

址,但除非必要,我們寧可選擇指標,因為和參考位址相比,指標比較不會讓初學

者感到困惑(如果需要的話,請參考第 8 章對參考位址的討論)。

1 3 4 / 第九章

Page 21: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

串連顯示成員函式

對於本章以及第 6 章所討論的絕大部份成員函式,編譯器都能給我們很大的幫助。

舉例來說,如果我們沒有撰寫預設建構式,那麼此類別將包含一個隱含式預設建構

式。即使我們自己撰寫了預設建構式,編譯器也可以自動將此預設建構式串連到類

別成員的預設建構式。但 print( ) 卻得不到這樣的協助。在這一節的內容中,我們

將討論如何利用成員的顯示能力來顯示一個龐大的類別。

舉例來說,既然可以顯示 R ect,我們就可以用它來顯示一個 Text Box。TextB ox 的

定義是:

class TextBox : public Rect { // 類別 TextBox 的定義

// ...

private:

Color textColor;

int frameThickness;

char *text;

};

T extB ox 衍生自 R ect,並擁有三個資料成員:C olor、i nt、char*。iost ream 函式庫

可以顯示 int 和 char* 的實體,我們已在前兩節將它擴充,讓它可以顯示一個 R ect

的實體。一旦我們使用類似的方法,讓它也擁有顯示 C olor 實體的能力,那麼下面

這段程式碼就可以讓 T extB ox 顯示自己:

void TextBox::print(ostream *os) {

*os << "TextBox{";

Rect::print(os);

*os << ", " << textColor << ", " << frameThickness << ", "

<< '"' << text << '"' << "}";

}

我們使用顯示運算子來顯示每個資料成員。然而對基礎類別 Rect 而言,必須呼叫

R ect::p rint( )。這是因為 Text Box 不能只用 *os << * this 或 prin t (os) 來顯示它的基

礎部份;這麼做只會重複不斷地顯示整個 T extBox。你當然可以呼叫 cout < <

*(R ect *) thi s 來顯示基礎類別,但是如果非必要,最好不要使用型態轉換。當我們

在第 12 章「利用虛擬函式創造多型」討論多型時,避免型態轉換的使用就更為重

要。使用 Rect::print ( ) 就可以啟動正確的函式來顯示基礎類別,而且不需要使用型

利用其它特殊的成員函式創造更好的抽象性 / 1 3 5

Page 22: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

態轉換。

現在,我們所需要的就只剩這個非成員函式:

ostream &operator<<(ostream &os, TextBox &tbox) {

tbox.print(&os);

return os;

}

這個函式接受一個 os tream 的參考位址,以及一個要顯示之 Text B ox 的參考位址。

它會呼叫 TextB ox 的成員函式 print(),並回傳 ostream 的參考位址。我們現在可以

顯示一個 T extB ox 物件了:

main() {

TextBox tbox;

// ...

cout << "the text box is: " << tbox << '\n';

}

並得到類似這樣的結果:

the text box is: TextBox{Rect{10, 30, 94, 12}, Color{1023}, 8, "Hello?"}

至於我們用來顯示這個類別實體的語法,其實沒有什麼特別之處。你並不一定要顯

示類別的名稱,或是將類別的資料放在括號內,只是我們覺得這種表示法很有用。

你可以自行選擇另一種比較簡潔,或是更為詳細的表示法。

顯示函式與這兩章所介紹的其它函式並不同,它與編譯器之間並沒有特殊的關係。

所有的顯示過程都是由 iost ream 函式庫所處理的。即使是內建型態的顯示,也都被

實作成標準的函式,甚至可以在使用者層次實作可擴充式的 I/O,展現了 C ++ 的一

部份威力。

1 3 6 / 第九章

Page 23: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

摘要在結束這一章之前,我們將介紹一個類別,這個類別包含了本書所介紹的所有特殊

成員函式(以及重要的全域函式):

class TextBox { // 特殊的成員函式

public:

TextBox(); // 預設建構式

~TextBox(); // 解構式

void operator=(TextBox &source); // 指定運算子

TextBox(TextBox &source); // copy 建構式

void print(ostream *os); // 顯示成員函式

// ...

};

// 重要的成員函式

ostream &operator<<(ostream &os, TextBox &tb); // 顯示運算子

利用其它特殊的成員函式創造更好的抽象性 / 1 3 7

Page 24: 本章重點: 利用其它特殊的 成員函式創造 更好的抽象性epaper.gotop.com.tw/pdf/a031.pdf · 利用其它特殊的成員函式創造更好的抽象性 / 1 1 7 對

1 3 8 / 第九章