+ All Categories
Home > Documents > Chap01 - Gotop

Chap01 - Gotop

Date post: 15-Oct-2021
Category:
Upload: others
View: 10 times
Download: 0 times
Share this document with a friend
51
12 由此可知,想要善用 C++ 的物件導向特性,首先需對 C 語言有基本瞭解,例如: 基本型態、運算子(operator )、控制結構(structure )和語法規則(syntax rule )。 如果你對 C 已有所瞭解,則較容易學會 C++,但這並不意味只要再多學幾個關鍵 字或結構,同時還要改正過去一些程式設計的習慣。倘若對 C 不了解,則需同時熟 C 語言,OOP 的程式設計觀念和 C++ 的泛型元件,但至少無須改正一些傳統 的程式設計習慣!現在你有心理準備了吧,本書將以一系列清楚,有效率的方法, 一步一步帶你走入 C++本書是假設諸位對 C 尚不瞭解,所以從教導 C 的基本原理與 C++ 新元件的 方式來學習 C++ 。你會從 C++ C 共用的特性開始學習,所以儘管你已學會 C本書這部分仍可提供 C 語言的複習。而且可從此瞭解二者的差異,甚至可以進一步 瞭解 C++ 是如何製作物件和類別並可學習樣版。 本書並非完整的 C++ 參考書,所以並不會探討語言非常細微的地方,但會學 習這語言的主要特色,包括樣版(template ),異常(exception )和最近加入的 名稱空間(namespace )。 接下來談談 C++ 語言的背景。 C++ 的簡史 近幾十年,電腦技術有長足進步,相較於 30 年前的大型電腦,今日的手提式 電腦跑得更快,儲存更多的資料(許多程式設計師可能還記得抱著一堆卡片,卻得 在不到 100KB 的主記憶體下跑程式 ── 如果是現在,這個環境甚至無法執行一個 電腦遊戲)。這段時間,程式語言也在改變,它的改變並非很快,但卻很重要。越 來越大、越有威力的電腦產生了更大,更複雜的程式,這結果造成程式管理和維護 上的新問題。 1970 年,像 C Pascal 之類的語言帶著我們走入結構化程式設計(structure programming )的領域,但是它的語法也限制了它的成效。C 語言不僅提供結構化 程式設計的工具,還可產生簡潔、快速執行的程式,而且可以處理硬體,例如管理 硬體通訊埠和磁碟機。這些優點使得 C 成為 1980 年最重要的語言。同時期物件導 向程式設計或是 OOP 的觀念也正在成長茁壯,具體化的語言包括 SmallTalk C++
Transcript
Page 1: Chap01 - Gotop

12

由此可知,想要善用 C++ 的物件導向特性,首先需對 C語言有基本瞭解,例如:

基本型態、運算子(operator)、控制結構(structure)和語法規則(syntax rule)。

如果你對 C已有所瞭解,則較容易學會 C++,但這並不意味只要再多學幾個關鍵

字或結構,同時還要改正過去一些程式設計的習慣。倘若對 C不了解,則需同時熟

悉 C語言,OOP的程式設計觀念和 C++ 的泛型元件,但至少無須改正一些傳統

的程式設計習慣!現在你有心理準備了吧,本書將以一系列清楚,有效率的方法,

一步一步帶你走入 C++。

本書是假設諸位對 C 尚不瞭解,所以從教導 C 的基本原理與 C++ 新元件的

方式來學習 C++。你會從 C++ 和 C共用的特性開始學習,所以儘管你已學會 C,

本書這部分仍可提供 C語言的複習。而且可從此瞭解二者的差異,甚至可以進一步

瞭解 C++ 是如何製作物件和類別並可學習樣版。

本書並非完整的 C++ 參考書,所以並不會探討語言非常細微的地方,但會學

習這語言的主要特色,包括樣版(template),異常(exception)和最近加入的

名稱空間(namespace)。

接下來談談 C++ 語言的背景。

C++ 的簡史 近幾十年,電腦技術有長足進步,相較於 30 年前的大型電腦,今日的手提式

電腦跑得更快,儲存更多的資料(許多程式設計師可能還記得抱著一堆卡片,卻得

在不到 100KB的主記憶體下跑程式 ── 如果是現在,這個環境甚至無法執行一個

電腦遊戲)。這段時間,程式語言也在改變,它的改變並非很快,但卻很重要。越

來越大、越有威力的電腦產生了更大,更複雜的程式,這結果造成程式管理和維護

上的新問題。

在 1970年,像C和 Pascal之類的語言帶著我們走入結構化程式設計(structure

programming)的領域,但是它的語法也限制了它的成效。C語言不僅提供結構化

程式設計的工具,還可產生簡潔、快速執行的程式,而且可以處理硬體,例如管理

硬體通訊埠和磁碟機。這些優點使得 C成為 1980年最重要的語言。同時期物件導

向程式設計或是 OOP 的觀念也正在成長茁壯,具體化的語言包括 SmallTalk 和

C++。

novia
螢光標示
Page 2: Chap01 - Gotop

38

iostream,實際上要稱為 std::cout;而 endl實際上為 std:endl。因此,若省略 using

指令的使用,則我們可以這樣撰寫:

std::cout << "Come up and C++ me some time."; std::cout << std::endl;

但是大部分的使用者都不想將名稱空間出現之前使用 iostream.h和 cout的程

式碼轉換成使用 iostream和 std::cout的名稱空間程式碼,除非他們可以容易地做

到。這就是 using指令出現的原因。下面的敘述表示你可以使用定義在 std名稱空

間中的名稱,無須使用 std:::

using namespace std;

using 指令使得 std 名稱空間中的所有名稱都是可用的。現代的作法認為這方

法有一點偷懶。較好的方法是將你需要的名稱用 using宣告變成可用:

using std::cout; // make cout available using std::endl; // make endl available using std::cin; // make cin available

若你在檔案頂端用這些取代這行

using namespace std; // lazy approach, all names available

你可以不用加上 std:: 就可使用 cin和 cout。但是若你需要使用 iostream中

的其他名稱,你需要個別地加入 using清單中。因為在學習 C++ 時還有許多事情

要作,並且對於簡單的程式而言,名稱空間的管理不是很重大的課題,這本書會採

取懶惰的方法,但是你可以練習其它的技巧。

用 cout的 C++ 輸出 現在來說明如何顯示訊息。myfirst.cpp使用下面的 C++ 敘述:

cout << "Come up and C++ me some time.";

這會印出雙引號之間的訊息。C++ 將雙引號之間的字元視為字元字串

(character string),這意味一個字串是由許多字元串接起來的。<< 符號表示將

字串送至 cout,這符號指出資訊流動的方向。那麼 cout又是什麼呢?cout是一個

預先定義好的物件,它知道如何顯示各種事物,包括字串,數字和單一字元(第 1

章提過,物件是類別的特定實體,而類別定義如何儲存和使用資料)。

至此是否有些心虛呢,都還沒學過物件就開始使用,事實上這就是物件的特

色,不一定要知道物件的內部作法,只要瞭解物件的介面 ── 也就是說,如何使

用它。cout物件的介面很簡單,若 string表示字串,顯示 string的敘述如下:

novia
螢光標示
Page 3: Chap01 - Gotop

81

// using the cout.put() member function to display a char cout << "Displaying char ch using cout.put(ch): "; cout.put(ch); // using cout.put() to display a char constant cout.put('!'); cout << endl << "Done" << endl; return 0; }

輸出如下:

The ASCII code for M is 77 Add one to the character code: The ASCII code for N is 78 Displaying char ch using cout.put(ch): N! Done

程式摘要 在範例程式 3.6中,'M' 代表 M字元的數值代碼,所以將 char變數 ch初始化

為 'M',表示將 ch指定為 77。然後程式再將 ch值指定給 int變數 i,所以 ch和 i

的值均為 77。接著 cout顯示變數 ch為字母 M,而顯示 i為數字 77,再次說明 cout

會因輸出值的型態而顯示不同的結果。

實際上,ch是一個整數,所以可以執行整數運算,例如,ch值加 1後變成 78。

然後將 i 值設為新值(同樣的,你可以將原 i 值加 1)。不變的 cout 仍顯示 char

型態為字元,而 int型態為數字。

C++ 將字元視為整數,可以容易的處理字元值,而不需要費力的在字元和

ASCII代碼間作轉換。

最後,程式用 cout.put() 函數顯示 ch和一個字元常數。

成員函數:cout.put() cout.put() 是什麼,為何其名稱有句點?cout.put() 是一個成員函數(member

function),為 C++ OOP觀念的第一個重要例子。類別定義如何表達和處理資料,

而成員函數屬於類別,描述處理類別資料的成員函數。例如,ostream 類別設計成

員函數 put() 為輸出字元。只有此類別的物件如 cout 才能使用此成員函數,表示

法為在物件名稱(cout)後加上句點和函數名稱(put())。句點稱為成員運算子

(membership operator)。cout.put() 表示類別物件 cout使用其成員函數 put()。

novia
螢光標示
novia
螢光標示
novia
螢光標示
Page 4: Chap01 - Gotop

95

及 long double 稱為浮點數型態。整數和浮點數型態則統稱為算術型態(arith-

metic type)。

C++ 算術運算子 C++ 用運算子(operator)執行數學運算,有 5種基本的算術運算子:加(+),

減(-),乘(*),除(/)和模數(%)。每個運算子均需兩個數值(稱為運算元

(operand))來求出最後的答案。運算子和其運算元組成運算式(expression);

如下列敘述:

int wheels = 4 + 2;

4和 2是運算元,符號 + 是加法運算子,4 + 2是運算式,其值為 6。

以下簡介 C++ 的 5個基本算術運算子:

+ 運算子計算運算元之和,如 4 + 20等於 24。

- 運算子用第一個運算元減去第二個運算元,如 12 - 3等於 9。

* 運算子計算運算元之積,如 28 * 4等於 112。

/ 運算子用第一個運算元除以第二個運算元,如 1000 / 5 等於 200。假使兩個

運算元皆為整數,則結果是商的整數部分,如 17 / 3等於 5,去掉其小數點及

小數部份。

% 運算子計算第一個運算元除以第二個運算元的餘數,如 19 % 6等於 1。兩個

運算元均須為整數型態。若有運算元為負數,則結果的正負視編譯程式而定。

運算元可為變數或常數,範例程式 3.10說明此點。因為 % 運算子只能處理整

數,留至後面的例子再說明。

範例程式 3.10 arith.cpp

// arith.cpp -- some C++ arithmetic #include <iostream> int main() { using namespace std; float hats, heads;

novia
螢光標示
Page 5: Chap01 - Gotop

112

cout << "Total yams = "; cout << yams[0] + yams[1] + yams[2] << endl; cout << "The package with " << yams[1] << " yams costs "; cout << yamcosts[1] << " cents per yam.\n"; int total = yams[0] * yamcosts[0] + yams[1] * yamcosts[1]; total = total + yams[2] * yamcosts[2]; cout << "The total yam expense is " << total << " cents.\n"; cout << "\nSize of yams array = " << sizeof yams; cout << " bytes.\n"; cout << "Size of one element = " << sizeof yams[0]; cout << " bytes.\n"; return 0; }

目前的 C++ 或 ANSI C版本都已允許在函數內初始化一般的陣列。然而有一

些使用 C++ 轉譯程式而非真正編譯程式的較舊系統,C++ 轉譯程式會產生

C程式碼給 C編譯程式,若此 C編譯不完全符合 ANSI C的話,如 SUN C++

2.0系統,就會產生以下的錯誤訊息:

"arrayone.cc", line 10: sorry, not implemented: initialization of yamcosts (automatic aggregate) Compilation failed

解決方法就是在陣列宣告中使用關鍵字 static:

// pre-ANSI initialization static int yamcosts[3] = {20, 30, 5};

關鍵字 static 將會使編譯程式使用不同的記憶體架構來儲存陣列,即使在

pre-ANSI C 之下,這架構都允許初始化陣列。有關 static 的用法會在第 9

章討論。

以下是範例程式 4.1的執行結果:

Total yams = 21 The package with 8 yams costs 30 cents per yam. The total yam expense is 410 cents. Size of yams array = 12 bytes. Size of one element = 4 bytes.

程式摘要 首先,程式產生擁有 3個元素的陣列 yams。因為 yams有 3個元素,所以編號

從 0至 2,arrayone.cpp用 0至 2的索引值為這三個元素指定值。每個 yams元素是

一個 int 型態,所以 arrayone.cpp 可以對陣列元素執行指定、乘法、除法和顯示

元素。

novia
螢光標示
Page 6: Chap01 - Gotop

116

char shirt_size = 'S'; // this is fine

將值 83指定給 shirt_size。但是 "S" 代表由二個字元組成的字串,S和 \0字

元。更嚴重的是 "S" 實際上表示儲存此字串的記憶體位址。所以如下的敘述

char shirt_size = "S"; // illegal type mismatch

想要將記憶體位址指定給 shirt_size!因為位址是 C++ 不同的型態,所以

C++ 編譯程式不接受這種指定(討論過指標後會再回到這主題)。

字串常數的結合 有時字串也許會太長而超出一行程式碼,C++ 提供連結(concatenate)字

串常數的功能,也就是說結合兩個字串變成一個字串。事實上,C++ 自動將兩個

隔著正常空白(空白鍵,tab 鍵和換行字元)的字串常數連結起來。所以以下面的

輸出敘述代表同一個意義:

cout << "I'd give my right arm to be" " a great violinist.\n"; cout << "I'd give my right arm to be a great violinist.\n"; cout << "I'd give my right ar" "m to be a great violinist.\n";

注意,結合兩個字串時,不會在結果字串中加入任何空白。第二個字串的第一

個字元緊接在第一個字串的最後一個字元之後,不包括第一個字串的 \0。第一個

字串的 \0會被第二個字串中的第一個字元所取代。

陣列與字串 將字串放入陣列有兩種最常見的方式,第一是以字串常數初始化陣列,第二是

將鍵盤或檔案的輸入讀入陣列。範例程式 4.2以上述二種方法輸入陣列資料,作法

是用字串常數初始化陣列,以及用 cin將輸入字串置於第二個陣列。這程式另外使

用標準函式庫函數 strlen() 來取得字串長度。標準的 cstring 標頭檔(或是舊系

統的 string.h)有此函數的宣告以及其他許多字串的相關函數。

範例程式 4.2 strings.cpp

// strings.cpp -- storing strings in an array #include <iostream> #include <cstring> // for the strlen() function int main() { using namespace std; const int Size = 15;

novia
螢光標示
Page 7: Chap01 - Gotop

124

字串與數字混合輸入 數字與行導向字串的混合輸入會產生一些問題,參考範例程式 4.6:

範例程式 4.6 numstr.cpp

// numstr.cpp -- following number input with line input #include <iostream> int main() { using namespace std; cout << "What year was your house built?\n"; int year; cin >> year; cout << "What is its street address?\n"; char address[80]; cin.getline(address, 80); cout << "Year built: " << year << endl; cout << "Address: " << address << endl; cout << "Done!\n"; return 0; }

範例程式 4.6的執行結果:

What year was your house built? 1966 What is its street address? Year built: 1966 Address Done!

你根本沒有機會輸入地址。這問題是當 cin讀入年度時,它將 <ENTER> 鍵

所產生的換行字元留在輸入佇列中。接下來 cin.getline() 讀入換行字元,視為空

白行,並指定空字串至 address陣列。解決的方法是讀取位址之前,先讀取並捨棄

換行字元。這做法有好幾種,包括使用無引數或一個 char引數的 get()。你可獨立

地呼叫函數:

cin >> year; cin.get(); // or cin.get(ch);

或利用運算式 cin >> year回傳 cin物件,連結這兩個呼叫:

(cin >> year).get(); // or (cin >> year).get(ch);

依據這方式修改範例程式 4.6,就可以正確地運作:

What year was your house built? 1966 What is its street address? 43821 Unsigned Short Street Year built: 1966

novia
螢光標示
Page 8: Chap01 - Gotop

131

以下是將一行資料讀取至 string物件的程式碼:

getline(cin,str);

這裡沒有使用點標記,而這即代表此 getline() 不是一個類別成員函數。因此,

它會將 cin當作是一個引數,以表示輸入資料在哪裡。此外,這裡並不需要一個表

示字串大小的引數,因為 string物件會自動地調整使用空間,以容納整個字串。

那麼,為什麼一個 getline() 是 istream類別成員函數,而另外一個 getline() 則不是?在 string類別尚未加入至 C++ 以前,istream類別就屬於 C++ 的一部

份。在設計上 istream是可以辨識基本的 C++ 型態,好比說 double與 int,但不

能夠識別 string 型態。所以這裡會有可以處理 double, int 以及其它基本型態的

istream類別成員函數,但是沒有處理 string物件的 istream類別成員函數。

由於沒有可以處理 string 物件的 istream 類別成員函數,你也許很好奇為什

麼這樣的程式碼仍可以運作:

cin >> str; // read a word into the str string object

它會將程式碼轉為類似這樣:

cin >> x; // read a value into a basic C++ type

實際上是使用 istream 類別的成員函數(以掩飾的方式達成)。然而 string

類別會使用 string類別(也是用掩飾的方式完成)的夥伴函數(friend function)。

我們會在第 11 章時說明什麼是夥伴函數,以及它的運作技巧。而在此之前,對於

string物件請直接使用 cin與 cout,並且不需瞭解其中是如何作用的。

現在我們來看另外一種複合型態,結構。

結構簡介 假設你想要儲存籃球運動員的資料,你可能要記錄他的姓名、薪水、身高、體

重、平均得分、罰球命中率、助攻等等。你會希望有一種資料格式可將所有資訊儲

存在一個單位中。陣列不能!雖然陣列可以儲存數個資料,但每個資料都必須是相

同型態。也就是說,一個陣列可以儲存 20 個 int,而另一個陣列可以儲存 10 個

float,但是單一陣列不能同時儲存 int和 float元素。

符合你需求(有關儲存籃球隊員資訊)的答案是 C++ 的結構(structure)。

結構比陣列有更多的資料格式變化,單一的結構可以儲存一種以上的資料型態。這

使你可以統一你的資料表示方式,將所有相關的籃球資訊都儲存在單一的結構變數

中。若你要記錄全隊的資訊,你可以用結構陣列(array of structure)。結構型態

是學習 C++ OOP類別的基石,學會結構就更接近 C++ 的 OOP核心。

結構是使用者可定義的型態,結構宣告就是定義型態的資料性質。在定義型態

之後,你就可以產生該型態的變數。因此建立結構分成二部份:第一是定義結構的

novia
螢光標示
Page 9: Chap01 - Gotop

143

指定的值必須是整數。你也可以只定義某些列舉值:

enum bigstep{first, second = 100, third};

在此例子中,first 預設為 0。之後未初始化的列舉值都比其前者大 1。所以

third的值為 101。

最後,可以有多個列舉值的值是相同的:

enum {zero, null = 0, one, numer0_uno = 1};

此例中,zero與 null均為 0;one與 numer0_uno均為 1。早期的 C++ 版本只

允許列舉值為 int值(或是可以升級至 int的值),但是這限制已經不存在,你可

以使用 long型態之值。

列舉值的範圍 對於列舉型態,最初只有在宣告內的名稱才是有效值。而目前的 C++ 允許將

型態轉換後的值指定給列舉變數。每個列舉型態都有範圍(range),你可以將此

範圍內的任何整數值,即使不是列舉值,都可經過型態轉換然後指定給列舉變數。

例如,假設 bits和 myflag定義如下:

enum bits{one = 1, two = 2, four = 4, eight = 8}; bits myflag;

然後,下列是有效敘述:

myflag = bits(6); // valid, because 6 is in bits range

此處 6雖然不是列舉值,但是落在列舉定義的範圍內,所以是正確的。

這範圍定義如下。首先是找出範圍的上限,先取最大的列舉值,找出大於此最

大值之最小的 2次方(power)數,將此值減 1即為範圍的上限(例如,最大的 bigstep

值,如之前的定義為 101,而大於 101之最小的 2次方數為 128,所以上限為 127)。

其次是找出範圍的下限,先取最小的列舉值,如果此值大於等於 0,則範圍的下限

為 0;如果最小的列舉值為負值,則參考上限算法並乘以負號(例如,若最小的列

舉值為 -6,則下一個 2次方數為 -8(乘以負號),所以下限為 -7)。

以上作法是考慮編譯程式應為列舉保留多少空間。對於小範圍保留一個位元組

或更少,對於具有型態 long值的列舉則保留 4個位元組。

novia
螢光標示
Page 10: Chap01 - Gotop

164

const char * bird = "wren"; // bird holds address of string

切記,"wren" 真正代表的是字串的位址,所以這敘述將 "wren" 的位址設給 bird

指標(一般來說,編譯程式會配置記憶體區域來儲存程式原始碼中用雙引號包住的

字串,然後將其位址結合至每個儲存的字串)。這意思是指標 bird 的用法就像是

字串 "wren" 的用法,例如:

cout << "A concerned " << bird << " speaks\n"。

字串文字是常數,這就是程式碼在宣告時用關鍵字 const 的原因。使用 const

表示你可用 bird 存取字串,但不能改變它。第 7 章會更詳細討論 const 指標的主

題。最後,指標 ps仍未初始化,所以 ps並未指向任何字串(這是不好的做法,本

例也不例外)。

接下來程式說明在 cout的敘述中,陣列名稱 animal和指標 bird的用法相同。

兩者都是字串的位址,而且 cout 會顯示儲存在這些位址的字串( "bear" 和

"wren")。若你想使用顯示 ps 的錯誤程式碼,你可能會得到空白行,也可能看到

一些亂碼,或是使程式當掉。產生未初值化的指標有點像送出空白的簽名支票,你

無法控制它會被怎麼用。

至於輸入,就有點不同。只要輸入比陣列 animal短,可以放進陣列中,用 animal

接收輸入都是安全的。但是用 bird接收輸入就不正確了:

一些編譯程式視字串文字為唯讀常數,若你要將新資料寫至這些字串則會導致執行期錯誤。在 C++ 中,字串文字規定是常數,但是不是所有的編譯程式都

已經改變舊版的行為。

一些編譯程式只是用一份字串文字來表示程式中所有出現該文字之處。

我們要詳述第二點。C++ 不保證每個字串文字只儲存一份。也就是說,若你

在程式中使用字串文字 "wren" 數次,則編譯程式會儲存數份此字串或是一份。若

是後者,則指定 bird指向 "wren" 會使其指向該字串的唯一拷貝。將值讀入此字串,

會影響到其他你認為是獨立字串之處。在任何情況,因為 bird指標宣告為 const,

編譯程式都會阻止想要改變 bird所指之內容的動作。

更糟的是想要讀資訊至 ps所指的位置。因為 ps未初始化,你不知道資訊要放

在何處。有可能覆寫已經存在該記憶體的資訊。幸好,很容易就可以避開這些問題

── 以足夠大的 char陣列接收輸入,切勿使用字串常數或是未初始化的指標接收

輸入(或許要避免所有可能的問題,使用 std::string物件來取代陣列)。

novia
螢光標示
novia
螢光標示
novia
螢光標示
Page 11: Chap01 - Gotop

170

程式摘要 首先看 getname() 函數。此函數用 cin 將輸入單字置於 temp 陣列。接著它用

new配置新的記憶體來儲存此單字。包括 null字元,程式需要 strlen(temp) + 1個

字元來儲存字串,所以這就是提供給 new之值。配置好記憶體後,getname() 用標

準函式庫函數 strcpy() 將字串從 temp複製到新區塊。這函數不會檢查此字串的大

小是否合適,因為 getname() 已經用 new產生正確的位元組數空間。最後,函數傳

回 pn,這是字串副本的位址。

在 main() 中,將回傳值(區塊位址)指定給指標 name。這指標定義在 main(),

但是它指向 getname() 函數配置的記憶體區塊。然後程式印出字串和字串的位址。

接著,在它釋放 name所指的區塊後,main() 第二次呼叫 getname()。C++ 不

保證剛釋放的記憶體是下一次 new首先選擇的記憶體,在此範例的執行結果中,也

是如此。

注意在此範例中,getname() 配置記憶體,而 main() 釋放之。通常將 new 和

delete 放在不同的的函數中並非良策,因為這樣比較容易忘記使用 delete。這範

例將 new和 delete分開的作法只是要說明這是可行的。

要領會此程式更細微的觀念,你應更深入瞭解 C++ 如何處理記憶體。所以我

們先簡介第 9章所涵蓋的內容。

自動空間,靜態空間,和動態空間 C++ 有 3種管理儲存資料之記憶體的方法,視配置記憶體的方法而定:自動

空間(automatic storage),靜態空間(static storage)和動態空間(dynamic

storage),有時稱為未用空間(free store)或堆積(heap)。以這三種方法配置

的資料物件其存在的時間並不相同,我們很快逐一討論。

自動空間 定義在函數內的一般變數會使用自動空間,並且稱為自動變數(automatic

variable)。當呼叫包含它們的函數時,它們就會自動存在,當函數結束時它們也

會終結。例如,在範例程式 4.17 中,只有在 getname() 函數執行時,temp陣列才

會存在。當程式控制權交回 main() 時,temp 所使用的記憶體就會自動釋放。若

getname() 回傳 temp的位址,則在 main() 中的 name指標會指向很快就會被再利用

的記憶體位置。這就是我們在 getname() 中必須使用 new的原因。

實際上,自動變數值是包含它之區塊(block)的區域值。區塊是一段由大括

號包住的程式碼。至目前為止,我們的區塊都是整個函數。但是在下一章你就會看

novia
螢光標示
novia
螢光標示
Page 12: Chap01 - Gotop

171

到在函數內可以包含區塊。如果在區塊內定義變數,則當程式執行這區塊的敘述

時,這變數才會存在。

靜態空間 靜態空間是在整個程式的執行期間都會存在的空間。有兩個方法可以使變數成

為靜態。一為將變數定義在外部,即函數之外;另一方法是變數宣告時加入關鍵字

static,如下所示:

static double fee = 56.50;

在 K&R C中,你只能初始化靜態陣列和結構,但是 C++ 2.0版(以及後續版

本)和 ANSI C,則也允許初始化自動陣列和結構。但是當今有許多 C++ 版本,

仍無法初始化自動陣列和結構。

第 9章會更詳細探討靜態空間。此處關於自動和靜態空間的主要重點是這些方

法會嚴格地定義變數的生命期。一種是程式執行的整個期間都存在的變數(靜態變

數),或是只有在特定函數執行時才存在的變數(自動變數)。

動態空間 比起自動與靜態變數,new與 delete運算子提供更具有彈性的方法。它們會管

理一份 C++ 認為是未用空間的共通記憶體空間。而這份記憶體空間,與靜態和自

動變數所使用的是獨立分開的。如同範例程式 4.22 所示,new 與 delete 讓我們可

以在一個函數中配置記憶體,並且在另外一個函數作釋放的動作。因此,資料的生

命週期並不會被程式的生命週期或者是函數的生命週期束縛住。在程式中,比起使

用一般的變數,new與 delete讓我們可以對記憶體空間作更多的控制。

堆疊,堆積,和記憶體缺口 在未用空間(或堆積)中用 new產生變數之後,若沒有呼叫 delete會如何?

在未用空間動態配置的變數或結構,即使包含此指標的記憶體已經根據範疇

和物件生命期規則而釋放,但是在未呼叫 delete 之前都會一直存在。基本

上,你無法存取在未用空間上的結構,因為包含指標的記憶體已經不見。你

會產生記憶體缺口(memory leak)。缺口的記憶體在整個程式的生命週期都

不能再使用,它已經被配置但是無法被釋放。在極端的例子中(雖然不常見),

記憶體缺口會嚴重到用光程式可用的記憶體,致使程式因用光記憶體而當

掉。此外,這些記憶體缺口會負面地影響一些作業系統或是其他執行在相同

記憶體空間的應用程式,可能輪流使它們失敗。

最好的程式設計師和軟體公司都可能產生記憶體缺口。要避免之,最好是養

成習慣立即結合 new和 delete運算子,當你一動態配置未用空間,就馬上規

劃並進入結構的刪除。

novia
螢光標示
novia
螢光標示
Page 13: Chap01 - Gotop

173

指標與陣列的關係非常密切。若 ar為陣列名稱,則運算式 ar[i] 會解釋為 *(ar

+ i),將陣列名稱解釋為陣列第一個元素的位址。因此陣列名稱所扮演的角色與指

標一樣。結果,你可用指標名稱和陣列表示法來存取用 new配置之陣列的元素。

new和 delete運算子使你可以明確地控制資料物件何時產生和何時歸還至記憶

體區。自動變數就是宣告在函數內的變數。而靜態變數是定義在函數之外或是具有

關鍵字 static 的變數,是較無彈性的。自動變數在進入包含它的區塊(通常是函

數定義)時會自動產生,而離開區塊後,它就結束。靜態變數在程式整個執行期間

都是存在的。

問題回顧 1. 如何宣告下列變數?

a. actors為 30個 char的陣列。

b. betsie為 100個 short的陣列。

c. chunk為 13個 float的陣列。

d. dipsea為 64個 long double的陣列。

2. 宣告一個擁有 5個 int的陣列,並且以前 5個正奇整數初始這一陣列。

3. 寫一行敘述將問題 2 之陣列的第一個元素和最後一個元素之和指定給變數

even。

4. 寫一行敘述顯示 float陣列 ideas的第二個元素。

5. 宣告 char陣列,並將它初始化為字串 "cheeseburger"。

6. 建立一個描述魚的結構,這結構應包含魚的種類、重量(單位為盎司)、長度(單位為吋,為一小數)。

7. 宣告定義在問題 6之型態的變數,並對其初始化。

8. 用 enum定義名為 Response的型態,其值包括 Yes,No,及 Maybe。Yes應為 1,

No應為 0而 Maybe應為 2。

9. 假設 ted為 double變數,宣告一個指標指向 ted,並用此指標輸出 ted值。

10. 假設 treacle為 10個 float的陣列。宣告一個指標指向 treacle的第一個元素,

並用指標顯示陣列的第一個和最後一個元素。

novia
螢光標示
Page 14: Chap01 - Gotop

220

在此 cin.get(char) 在測試條件中呼叫了一次,而不是原本的兩次 ── 一次

是在迴圈前面,一次是在迴圈後面。要對迴圈測試求值,程式首先必需執行

cin.get(ch),若成功,則它會在 ch中放置一個值。之後程式從函數呼叫取得傳回

值,即 cin。然後對 cin應用 bool轉換,若輸入正常,則產生 true,否則為 false。

三項指導原則(標示結束條件,初始化條件,和更新條件)全部濃縮在一個迴圈測

試條件中。

另一種 cin.get() 若仍喜歡C的字元 I/O函數 getchar() 和 putchar(),則依然可用標頭檔 stdio.h

(或使用較新的 cstdio),一切如使用 C的樣子。或是可用類別 istream和 ostream

的成員函數,其功能和此類似,我們現在就來看這方法。

一些較舊的編譯程式並沒有提供此處討論的 cin.get() 成員函數(無引數)。

無引數之 cin.get() 成員函數回傳輸入的字元。使用方式如下:

ch = cin.get();

(還記得 cin.get(ch) 回傳物件,不是讀入的字元)。這函數的功能和 C 的

getchar() 很像,以 int值回傳字元代碼。同樣的,你可用 cout.put() 函數顯示此

字元(參考第 3章):

cout.put(ch);

這和 C的 putchar() 很類似,差別處是引數為 char型態,而不是 int型態。

原先,put() 成員函數的單一函數原型為 put(char)。你可傳遞 int 引數,

然後它會自動轉型成 char。ANSI C++ 仍然要求單一原型。但是,目前許多

C++ 編譯程式提供三種原型: put(char), put(signed char),和

put(unsigned char)。在這些工具中,若使用具 int 引數的 put() 會產生

錯誤訊息,因為轉換 int 有一種以上的選擇。明確地型態轉換,如

cin.put(char) 可以處理 int型態。

要成功地使用 cin.get(),你需要知道它如何處理檔案終點的狀況。當此函數

遇到檔案終點時,不會回傳任何字元。相反的,cin.get() 會回傳符號常數 EOF表

示此特殊值。這常數定義在標頭檔 iostream。EOF值必須異於任何有效的字元值,

novia
螢光標示
novia
螢光標示
Page 15: Chap01 - Gotop

285

使用函數原型的方法是是將函數定義置於第一次使用之前。這不一定都是可行。因

為 C++ 的程式設計格式是先放置 main(),因為它通常提供整個程式所需的結構。

函數原型的語法 函數原型是一個敘述,所以需要以分號作結束。取得函數原型的最簡單方法是

複製函數定義的函數標題,然後加上分號即可。這就是範例程式 7.2中,cube() 的

函數原型作法:

double cube(double x); // add ; to header to get prototype

在函數原型中,你不一定要提供變數的名稱,只要型態清單就夠了。只有引數

型態的 cheers() 其函數原型如下:

void cheers(int); // okay to drop variable names in prototype

一般而言,在函數原型中,引數清單的變數名稱可有可無。函數原型的變數名

稱只是用作位置保留符號(placeholder),所以若使用名稱,不一定要與函數定

義的名稱一致。

C++ 和 ANSI C的函數原型

ANSI C 的原型製作源自 C++,但兩種語言仍有些差異。最重要的是 ANSI C 保

有與古典 C的相容性,所以函數原型是可以省略的,但 C++ 是必須的。例如,

函數宣告如下:

void say_hi();

在 C++ 中,括號內空白和括號內使用關鍵字 void是相同的,表示函數沒有引數。

在 ANSI C中,括號內空白表示沒有將引數寫入,而非無引數。C++ 對於未標示

引數清單的同等寫法是使用省略符號:

void say_hi(...); // C++ abdication of resposibility

通常這只用在具有變動引數個數之 C函數的介面,如 printf()。

函數原型的功能 前面已討論過原型和編譯程式的關係,那對你有何助益呢?最大的好處是降低

程式出錯的機率,尤其是原型能做到下列事情:

使編譯程式能正確地處理函數回傳值。

使編譯程式能檢查呼叫程式所使用的引數個數是否正確。

使編譯程式能檢查呼叫程式的引數型態是否正確。若不正確,則盡其所能轉成正確的型態。

novia
螢光標示
Page 16: Chap01 - Gotop

287

此處 side是變數,範例執行時其值為 5。cube() 的函數標題為:

double cube(double x)

當此函數被呼叫時,它會產生新的 double變數 x,且將 5指定給它。這會隔離

main() 的資料和 cube() 的動作,因為 cube() 是處理 side的副本而不是原始的資

料。你很快就會看到這種保護的範例。接收傳遞值的變數稱為形式引數( formal

argument)或形式參數(formal parameter),傳遞給函數的值稱為實際引數(actual

argument)或實際參數(actual parameter)。為了簡化說明,ANSI C++ 使用

引數(argument)表示實際引數或參數,而用參數(parameter)表示形式引數或

參數。在此術語下,引數傳遞是將引數指定給參數(參考圖 7.2)。

圖 7.2 以值傳遞

在函數內宣告的變數,包括參數都是函數私有的變數。當函數被呼叫時,電腦

就會配置這些變數所需的空間。當函數結束時,就會釋放這些變數使用的記憶體(有

些 C++ 文獻稱配置記憶體和釋放記憶體為產生和終結變數)。這種變數稱為區域

變數(local variable),因為它們區域化於函數中。此法有助於資料的完整性。這

也表示若在 main() 中宣告變數 x,則在某個函數中亦宣告變數 x時,這兩個是不同

且無關的變數,就如同加州的 Albany 不同於紐約的 Albany(參考圖 7.3)。這種

變數也稱為自動變數,因為是在程式執行時自動配置和釋放。

novia
螢光標示
Page 17: Chap01 - Gotop

300

這訊息提醒我們,C++ 將 const double ar[] 解釋成 const double *ar。所

以,此宣告實際上是說明 ar為指向常數值。以下為 show_array() 函數的程式碼:

void show_array(const double ar[], int n) { using namespace std; for (int i = 0; i < n; i++) { cout << "Property #" << (i + 1) << ": $"; cout << ar[i] << endl; } }

陣列的修改 第三個陣列運算是將每個元素乘上相同的評估倍數。要傳遞 3 個引數給這函

數:倍率,陣列和元素個數,毋須回傳值,所以函數的樣子如下:

void revalue(double r, double ar[], int n) { for (int i = 0; i < n; i++) ar[i] *= r; }

因為這函數會改變陣列值,所以不用 const宣告 ar。

整合 現在已決定資料型態(陣列)及如何使用資料(3個函數),因此可以完成一

個使用這種設計的程式。因為我們已經建構所有處理陣列的工具,所以可以簡化許

多在 main() 中的工作。剩下的程式設計工作大部分就是在 main() 中呼叫這些函

數,範例程式 7.7 為整合的結果。在其中,只有使用 iostream 功能的函數有放入

using指令。

範例程式 7.7 arrfun3.cpp

// arrfun3.cpp -- array functions and const #include <iostream> const int Max = 5; // function prototypes int fill_array(double ar[], int limit); void show_array(const double ar[], int n); // don't change data void revalue(double r, double ar[], int n);

novia
螢光標示
Page 18: Chap01 - Gotop

320

程式摘要

我們已經討論過範例程式 7.12 的這兩個函數,所以我們來複習程式如何使用

cin控制 while迴圈:

while (cin >> rplace.x >> rplace.y)

cin是 istream類別的物件,萃取運算子(>>)的設計方式是 cin >> rplace.x

也是此型態的物件。你在第 11 章會看到類別運算子以函數的方式完成。cin >>

rplace.x實際上程式呼叫可以回傳 istream型態值的函數。將萃取運算子應用在 cin

>> rplace.x 物件上(如 cin >> rplace.x >> rplace.y),就會再次得到 istream

類別的物件。因此,整個 while 迴圈的測試運算式最後會求出 cin,這用在測試運

算式中會轉成 bool值的 true或 false,視輸入是否成功而定。例如,在此迴圈中,

cin希望使用者輸入兩個數字。若你輸入 q,cin >> 會識得 q不是數字,它將留在

輸入佇列中,而回傳值會轉成 false,使迴圈結束。

將此讀入數字的方法與下列較簡單的方法作一比較:

for (int i = 0; i < limit; i++) { cout << "Enter value #" << (i + 1) << ": "; cin >> temp; if (temp < 0) break; ar[i] = temp; }

要早早結束迴圈,只需輸入負值,此法限制輸入需為非負值。這是因應程式的

需要。但是一般性的方法是不要排除任何特殊的數字而能結束迴圈。使用 cin >> 為

測試條件可以消除這種限制,因為它可以接受所有有效的數字輸入。所以需要用輸

入迴圈讀進數字時,要記住此技巧。當讀入非數字時會指定錯誤狀況,拒絕再讀進

入任何輸入。若你的程式需要再讀入輸入,你必須用 cin.clear() 重設輸入,然後

讀入不對的輸入並去除之。範例程式 7.7說明了這些技術。

傳遞結構位址 若你以傳遞結構位址取代傳遞整個結構來節省時間和空間,就要重寫這些函

數,使它們使用指向結構的指標。你需要作三項改變:

呼叫函數時,需傳遞結構位址(&pplace),非結構本身(pplace)。

novia
螢光標示
Page 19: Chap01 - Gotop

357

它會使用兩個 string引數並且使用 string類別的成員函數,建立一個可以完

成我們想要的新字串。請注意,這兩個函數引數值都是 constreference。如果使用

的引數是 string物件,此函數還是會產生相同的結果:

string version4(string s1, string & s2) // would work the same

在這種情況下,s1與 s2都是全新的 string物件。因此,使用 reference會較

有效率,因為函數不需要建立新的物件以及將舊有物件上的資料複製到新的物件。

使用 const修飾元代表函數會使用原有的字串,但是不會修改它們。

這裡的 temp物件是一個新物件,並且屬於在 version1() 函數內,當該函數執

行結束後,使用空間也會跟著釋放。因此將 temp的 reference回傳是沒有用的,所

以這裡的函數型態要是 string。這表示 temp 的內容將會被複製到一個暫時性的回

傳空間。接著,在 main() 中,回傳空間的內容會被複製到名稱為 result的字串:

result = version1(input, "***");

將 C-格式字串引數值傳遞給 string物件 reference參數

對於 version1() 函數,也許你會注意到一個有趣的現象:兩個參數(s1 與 s2)

都是 const string & 型態,不過實際的引數值(input與 "***")分別屬於 string

與 const char * 型態。因為 input屬於 string型態,讓 s1指向它是沒有問題的。

不過要如何讓程式接受一個 char指標之引數值作為 stringreference?

這裡有兩個地方要注意。一個是 string類別有定義一個 char * 轉成為 string的轉換機制,讓 string 物件能夠初始化為一個 C-格式字串。另外一個要注意的則是本章稍早有討論到之有關 constreference參數的特性。假設實際引數值型態與

reference參數型態不一致,但是可以透過轉換,成為可接受之 reference型態。那麼接著程式會建立一個正確型態的暫時變數,將其初始為轉換後的數值,然後

傳遞指向此暫時變數的 reference。舉例來說,本章稍早應該有看過的,const

double & 參數可以這種方式接受 int型態的引數值。相同的,const string & 參數可以用這樣的方式接受 char * 或 const char * 引數值。

這樣便利的特性使得如果正式的參數是 const string & 型態,函數呼叫中使用的實際引數可以是 string物件或者是 C-格式字串,如用雙引號括起來的字串常數、用 null表示結尾的 char 陣列,或者是指向 char 的指標變數。因此,下面這行程

式碼可以正確的執行:

result = version1(input, "***");

version2() 函數不會建立暫時性的字串。取而代之的是,它會直接修改原有的

字串:

const string & version2(string & s1, const string & s2) // has side effect { s1 = s2 + s1 + s2; // safe to return reference passed to function return s1; }

novia
螢光標示
Page 20: Chap01 - Gotop

362

函數只使用傳遞值而不改變它:

如果資料物件很小,如內建的資料型態或是小型結構,則以值傳遞。

如果資料物件是陣列,使用指標,這是唯一的選擇。將指標宣告成 const。

如果資料物件是適當大小的結構,則使用 const 指標或是 constreference,以

增進程式執行效率。你可以節省複製結構或是類別的時間和空間。將指標或

reference宣告成 const。

如果資料物件是類別物件,使用 constreference。類別設計常常需要用到

reference,這就是 C++ 加入 reference特性的主要原因。因此,傳遞類別物

件引數的標準方式是以 reference傳遞。

函數會修改呼叫函數中的資料:

如果資料物件是內建的資料型態,使用指標。例如,fixit(&x),x為 int型態,

一看就知道這函數會修改 x。

如果資料物件是陣列,唯一的選擇是使用指標。

如果資料物件是結構,使用 reference或指標。

如果資料物件是類別物件,使用 reference。

以上只是一些建議原則,還有其他理由可作不同的選擇。例如,cin使用基本

型態的 reference,如此你可以用 cin >> n而非 cin >> &n。

預設引數 我們來看 C++ 的另一個新技術 ── 預設引數(default argument)。預設引

數是函數呼叫時若省略對應的實際引數會自動使用的值。例如,若你建立 void

wow(int n) 函數,使 n有預設值 1,則函數呼叫 wow() 等於呼叫 wow(1)。這使你在

函數的用法上有較大的彈性。假設有一個函數 left() 會回傳字串的前 n 個字元,

字串和 n皆為引數。更精確的說法是函數傳回指向由部分原始字串組成之新字串的

指標。例如,呼叫 left("theory", 3) 建構新字串 "the" 並回傳其指標。如果第二

個引數的預設值為 1,則呼叫 left("theory", 3) 與之前一樣,3會覆蓋預設值。但

是呼叫 left("theory") 不會產生錯誤,而是假設第二個引數為 1 並回傳指向字串

"t" 的指標。若你的程式常常需要萃取一個字元字串,但是偶而要萃取較長的字串,

則這種預設值是有益的。

novia
螢光標示
novia
螢光標示
Page 21: Chap01 - Gotop

366

當你使用 print() 函數時,編譯程式從函數原型找一個符合函數呼叫的相同

簽名:

print("Pancakes", 15); // use #1 print("Syrup"); // use #5 print(1999.0, 10); // use #2 print(1999, 12); // use #4 print(1999L, 15); // use #3

例如,print("Pancakes", 15) 使用一個字串和一個整數當作引數,所以符合

原型 #1。

當你使用多載的函數時,要確定函數呼叫使用了正確的引數型態。以下列的敘

述為例:

unsigned int year = 3210; print(year, 6); // ambiguous call

哪一個函數原型與這個 print() 呼叫相符呢?沒有函數原型符合!如果沒有

函數原型符合,不會自動放棄找一個函數來使用,因為 C++ 會嘗試使用標準的型

態轉換強迫找出符合者。例如,假設 print() 只有原型 #2,則函數呼叫 print(year,

6) 會將 year值轉成 double。但是上述程式碼有三個原型其第一個引數都是數字,

對於轉換 year有三種不同的選擇。對於這種模稜兩可的情況,C++ 無法分辨故產

生錯誤。

有些函數簽名似乎不同,但卻不能同時存在。例如下面兩個原型:

double cube(double x); double cube(double & x);

以上兩個函數好像可以當作函數多載,因為函數簽名看起來不同。但是從編譯

程式的觀點來考慮,若有如下的程式碼:

cout << cube(x);

x引數同時符合 double x以及 double &x的原型。因此編譯程式無法分辨應選

用那個函數。為了避免產生混淆,編譯程式在檢查函數簽名時,將 reference所參

考的型態和此型態本身視為相同的簽名。

函數配對的程序會區別出 const和非-const變數。想想下列原型:

void dribble(char * bits); // overloaded void dribble (const char *cbits); // overloaded void dabble(char * bits); // not overloaded void drivel(const char * bits); // not overloaded

novia
螢光標示
Page 22: Chap01 - Gotop

376

07/20/1969 Swapped arrays: 07/20/1969 07/04/1776

特定化 假設定義一個結構如下:

struct job { char name[40]; double salary; int floor; };

現想要交換兩個結構的內容。原始樣版使用下面的程式碼完成交換的功能:

temp = a; a = b; b = temp;

因為 C++ 允許將一個結構指定給另一個結構,所以雖然型態 Any是 job結構,

這作法是可行的。但是如果你只想交換 salary和 floor成員,而維持 name成員不

變,這需要不同的程式碼,但是傳入 Swap() 的引數會與第一種情況相同(兩個 job

結構的 reference),所以不能利用樣版多載來提供另一種程式碼。

但是你可以提供特定化的函數定義,稱為顯式特定化(explicit specialization)。

如果編譯程式發現特定化的定義與函數呼叫相符,則會使用此定義而不再檢查樣版。

特定化的機制隨著 C++ 的演變而有所修改。我們會看 C++ Standard 規定

的目前格式,然後再看兩個較舊編譯程式支援的舊格式。

第三代特定化(ISO/ANSI C++ Standard)

在 C++ 測試過本章稍後描述的方法後,C++ Standard決定了下面的方法:

對於某一個函數名稱,你可以有非-樣版函數,樣版函數,和顯式特定化的樣版

函數,並且它們都有多載的版本。

顯式特定化的原型和定義應前置 template<>,並要提到特定的型態名稱。

特定化會覆寫一般的樣版,而非-樣版函數會覆寫這兩者。

novia
螢光標示
novia
螢光標示
Page 23: Chap01 - Gotop

第九章 記憶體模式和名稱空間

本章將學習以下的主題:

獨立編譯

儲存期間,範疇,和連結性

定位放置 new

名稱空間

將資料儲存在記憶體中,C++ 提供許多選擇。你可以選擇資料要在記憶體中

停留多久(儲存期間),或是選擇程式的哪些部分可以存取資料(範疇和連結性)。

我們可以使用 new動態地配置記憶體空間,而定位放置 new(placement new)更

可以在其中提供使用上的變化。C++ 的名稱空間功能提供存取的控制權。較大的

程式一般由數個原始碼檔案組成,這些會共享某些資料。這種程式需要獨立編譯程

式檔案,所以本章從此開始。

獨立編譯 C++ 與 C一樣,允許你甚至鼓勵你將程式的函數元件置於獨立的檔案中。在

第 1 章中,你可以獨立編譯檔案,然後將它們連結成最終的執行檔(一般而言,

C++ 編譯程式是用來編譯原始程式而且也管理連結程式)。若你只修改一個檔案,

你可以只重新編譯此檔案,然後與其他先前已編譯好的檔案連結即可。這方法使得

大型程式較容易管理。而且,大部分的 C++ 環境均提供額外的功能幫助程式管

理。例如,UNIX和 Linux系統提供 make程式;它記錄程式與哪些檔案相關,以及

這些檔案的最後修改時間。若執行 make 而且它檢查自最後一次編譯後你已經修改

過一個或多個原始檔案,則 make 會執行正確的步驟重建程式。Borland C++,

Microsoft Visual C++,和 Metowerks CodeWarrior 的 IDE(整合發展環境)以

Project選單提供類似的功能。

novia
螢光標示
Page 24: Chap01 - Gotop

405

暫存器變數 與 C一樣,C++ 支援關鍵字 register宣告區域變數。暫存器變數是另一種形

式的自動變數,所以有自動儲存期間,區域範疇,而且沒有連結性。關鍵字 register

是建議編譯程式你希望它快速存取此變數,最好是用 CPU 暫存器而不要用堆疊來

處理這變數。這個構想是 CPU 存取暫存器之值比存取堆疊上的記憶體更快。要宣

告 register變數,在變數型態前加上關鍵字 register:

register int count_fast; // request for a register variable

你可能已經注意到修飾用語“建議"和“要求"。編譯程式不一定會承諾此要

求。例如,暫存器也許已經都佔滿,CPU 暫存器已被佔滿,或是你要求的型態不

適於暫存器。許多程式設計師覺得現在的編譯程式常常都夠聰明,並不需要建議。

例如,若撰寫 for迴圈,編譯程式也許自己就會用暫存器處理迴圈索引。

若變數儲存在暫存器中,它並無記憶體位址,因此你不能將位址運算子用在暫

存器變數上。在下面的程式碼中,我們可以使用變數 x的位址值,但不能夠使用暫

存器變數 y的:

void gromb(int *); // function that expects an address int main() { int x; register int y; gromb(&x); // ok gromb(&y); // not allowed ...

在宣告中使用 register 就足以造成此限制,即使編譯程式實際上不是用暫存

器處理此變數。

簡言之,一般的區域變數,用 auto宣告的區域變數,和用 register宣告的區

域變數都有自動儲存期間,區域範疇,和無連結性。以下的程式碼代表這三種情況:

int main() { short waffles; // auto variable by default auto short pancakes; // explicitly auto register int muffins; // register variable

不具指示元(specifier)的區域變數宣告與用 auto 宣告結果相同,而且這種

變數一般的處理方式是放在堆疊中。使用 register 指示詞是暗示此變數會被大量

使用,而編譯程式也許會選用記憶體堆疊以外的方法(如 CPU暫存器)來儲存它。

novia
螢光標示
Page 25: Chap01 - Gotop

446

紹 this 指標,這是一些類別程式設計的重要元件。接下來各章會繼續討論運算子

多載(另一種同名異式的變形)以及繼承,這是程式碼再利用的基礎。

程序式及物件導向的程式設計 雖然我們偶而在程式設計時會探究 OOP 的觀念,但通常主要仍偏向標準程序

語言如 C,Pascal和 BASIC的方法。我們會用一個例子說明 OPP和程序式語言有

何不同。

你是 Genre Giants 壘球隊的新進成員,你的工作是記錄團隊的統計資料。若

採程序式程式設計的方法,思考方式可能如下:

輸入每位球員的名字、打擊次數、擊出次數、打擊率(打擊率是擊出次數除以

球員的正式打擊次數,正式打擊次數是當球賽結束時球員上壘或是出局的次

數,但如某些事件如代跑,都不算在內),以及其它的基本統計數字。因為電

腦應該可以使我們的生活更簡單,所以有些數字要由電腦算出,如打擊率。而

且我也希望程式可以顯示結果。我應該如何組織?我想我應該使用函數。首

先,我會利用 main() 呼叫一個函數取得輸入,呼叫另一函數計算,最後呼叫

第三個函數來輸出結果。當我又取得下一場比賽的資料時,該如何呢?我不希

望從頭開始。對了,我可以加入函數異動統計資料。也許,在 main() 中我需

要選單功能選擇輸入、計算、更新及輸出資料。我該如何表現資料呢?我可以

用字串陣列儲存球員的名字,另一個陣列儲存每個球員的打擊次數,還要另一

個陣列儲存擊出次數等。這似乎不是明智的作法。我可以設計結構儲存一個球

員的資訊,然後用結構陣列是全隊資料。

簡言之,你會先思考程序,再考慮如何表達資料(注意:為了不必一年到頭都

執行程式,你可能也希望將資料儲存至檔案,再從檔案讀入資料。)

接著我們來看採取 OOP 的方法時,你的觀點如何改變。此時要從資料開始思

考。而且思考時不只是想如何表示之,同時也要考慮如何使用:

novia
螢光標示
Page 26: Chap01 - Gotop

447

想想看我要記錄些什麼?當然是一個球員。所以需要一個能表達球員完整資料

的物件,而不只是打擊率或是打擊次數。這就是我的基本資料單元,可以表示

球員名稱和統計資料的物件。我需要一些成員函數來處理這個物件。我想我需

要一個方法將基本資訊置於此單元中。電腦應計算一些資料,像打擊率 ── 我

可以加入成員函數作計算。而且程式應自動作這些計算,不需使用者要求。我

還需要成員函數異動並顯示資訊。所以使用者有 3個方式可以和資料互動:初

始化,異動,和回報資料。這就是使用者介面。

簡言之,使用物件導向程式設計,你的重點是使用者瞭解的物件,思考你要描

述物件的資料及描述使用者與資料互動的操作。在完成介面描述後,你就要決定如

何實作介面和資料儲存方式。最後按照設計撰寫程式。

抽象化與類別 生活充滿複雜性,而我們處理複雜性的一個方式是表達簡化的抽象概念。人是

千百億原子的集合體,有些研究頭腦的人會說你的頭腦是一群半自動的代理人,但

是將你想成單一的個體比較簡單。在電腦領域,抽象化是以使用者介面定義資訊表

示方式的重要步驟。也就是說,你萃取問題的基本操作特性,並以這些特性表示解

決方法。在壘球的例子中,這介面描述使用者如何初始化,異動及顯示資料。從抽

象化至使用者自訂型態是小小一步,在 C++ 是實作此介面的類別設計。

何謂型態? 我們要進一步思考型態的組成元素。例如,什麼叫討厭的人?若你受制於一般

的想法,你也許會以視覺的方式思考討厭的人 ── 戴著厚且黑邊的眼鏡,口袋中

裝滿鉛筆,等等。稍稍想過之後,你的結論會是討厭的人最好以功能來定義,例如,

他如何應付棘手的社交情況。我們會有類似的情況,我們就用程序式語言如 C 為

例。起初你會以資料型態的外表來判斷 ── 它如何存在記憶體中。例如,char佔

記憶體的 1 個位元組,而 double 佔用 8 個位元組。但再想一下你就會推論出資料

型態也可以定義為可對其執行的操作。例如,int 型態可適用所有的數學運算,你

可以加、減、乘、除整數,以及模數。

對於指標,它所需的記憶體大小與 int一樣。也許內部是以整數表示。但是指

標的操作與整數不同。例如,你不能將兩個指標相乘,這想法沒有意義,所以 C++

novia
螢光標示
novia
螢光標示
Page 27: Chap01 - Gotop

449

每股價格

股票總值

再來定義類別。一般而言,類別規格分兩部分:

類別宣告(class declaration),描述資料組成元素,稱資料成員(data member)

以及公用介面,稱為成員函數(member function)。

類別成員函數的定義,描述成員函數的實作方式。

大致來說,類別宣告提供類別的概觀,而成員函數的定義提供細節部分。

什麼是介面?

對於兩個要作互相溝通的系統 ── 好比說,一台電腦與一台印表機之間,或者是

一個使用者與一個電腦程式之間,介面可以作為其間共同使用的架構。舉例來說,

前述所指的使用者可以是你,而程式則可能是一個文書編輯程式。當你使用文書

編輯程式時,並不會直接地將文字由我們的意念轉換至電腦的記憶體當中。而實

際上會作的是,我們會以電腦所提供的介面與其互動與溝通。當按下一個鍵時,

電腦會在螢幕上顯示一個字元。當我們移動滑鼠的時候,電腦則會移動螢幕上的

游標。若我們按下滑鼠的按鈕時,電腦可能也會對正在輸入的文章作一些特別的

事情。所以我們可以這麼說,對於我們的想法該如何轉換為電腦中的資訊,程式

的介面可以作這方面的相關管理。

說到類別,我們來討論公有介面。以這裡的情況來說,公有的是指使用這個類別

的程式,而互動系統是由這個類別的物件所構成的,至於介面則是由撰寫該類別

的人所提供的成員函數構成的。使用介面可以讓我們撰寫能夠與類別物件互動的

程式碼,因此讓程式可以使用這個類別物件。舉例來說,要在一個 string物件中

算出字元的數目,我們不需要深入瞭解該物件的內容有什麼,而只需要使用該類

別之撰寫者所提供的 size() 成員函數即可。這樣所代表的意義在於,在此類別的

設計裡,不允許一般使用者對其直接作存取。不過,這個類別允許一般使用者使

用 size() 成員函數得到其中的資料。由此看來,size() 成員函數屬於在使用者

與 string類別物件間之公有介面的一部份。類似的情形,getline() 成員函數屬

於 istream類別公有介面的一部份;所以程式會使用 cin,而不會直接由 cin物件

的內部,讀取一行輸入資料;這份工作就直接交由 getline() 處理。

若以更為個人的角度來看,我們可以跳脫出使用類別的這個程式為該使用者的想

法,而將撰寫這個會使用此類別之程式的人看成是實際的使用者。不過在任何的

情況下,要使用一個類別,就需要知道它的公有介面;要撰寫一個類別,我們就

需要建立它的公有介面。

要建立一個類別以及使用它的程式,需要數個步驟。我們先不要一次說明,先

將開發的步驟分成幾個小階段;之後這些階段的程式碼會在範例程式 10.3 中加以

novia
螢光標示
Page 28: Chap01 - Gotop

454

因此,範疇運算子解決了成員函數的定義可用於什麼類別的標示,我們稱識別

字 update() 具有類別範疇(class scope)。Stock類別的其它成員函數可直接使

用 update(),無須使用範疇運算子。這是因為它們屬於同一類別,所以 update() 會

在範疇中。在類別宣告和成員函數定義之外使用 update() 都需要使用特殊的方式,

很快就會介紹。

成員函數名稱的一個看法是完整的類別成員函數名稱包括類別名稱。我們說

Stock::update() 是函數的標示名稱(qualified name)。另一方面,簡單的 update()

是完整名稱的的縮寫(未標示的名稱),這是只能用在類別範疇中的名稱。

成員函數的第二個特徵是可以存取類別的 private成員。例如,成員函數 show()

可以使用如下的程式碼:

cout << "Company: " << company << " Shares: " << shares << endl << " Share Price: $" << share_val << " Total Worth: $" << total_val << endl;

此處 company,share等皆為 Stock類別的私有資料成員。若使用非成員函數存

取這些資料成員,則編譯程式會阻止你(但是,第 11 章討論的夥伴函數會提供例

外情況)。

記住這兩點,我們就可以實作如範例程式 10.2 所示的類別成員函數。這些成

員函數的定義可以置於獨立的檔案或是與類別宣告放在相同的檔案。因為我們需要

簡單,所以假設這些定義與類別宣告放在相同的檔案中。這是最簡單,雖然不是最

好的方法,使得類別宣告可為成員函數的定義所用(最好的方法,本章稍後會用到,

就是使用類別宣告的標頭檔以及含有類別成員函數定義的獨立原始碼檔案)。為了

得到更多名稱空間的體驗,在有些成員函數中的程式碼會使用 std:: 修飾元,而在

其它地方則是使用 using-宣告。

範例程式 10.2 stocks.cpp續

// more stocks.cpp -- implementing the class member functions void Stock::acquire(const char * co, int n, double pr) { std::strncpy(company, co, 29); // truncate co to fit company company[29] = '\0'; if (n < 0) { std::cerr << "Number of shares can't be negative; " << company << " shares set to 0.\n"; shares = 0; } else shares = n; share_val = pr;

novia
螢光標示
novia
螢光標示
Page 29: Chap01 - Gotop

490

料。例如,C++ 程式用堆疊來管理自動變數。當自動變數產生時,從頂端加入,

它們消失時亦從頂端移除。

我們以一般的、抽象的方法來描述堆疊的性質。第一是堆疊儲存多個項目(此

性質使其成員收納器(container),是一種更通用的抽象化)。第二是你可對其

執行下面的操作。

可以產生空堆疊。

可新增資料項至堆疊頂端(push資料項) 。

可從頂端移除資料項(pop資料項)。

可以檢查堆疊是否已滿。

可以檢查堆疊是否為空。

你可以用類別宣告匹配這些描述,其中 public成員函數提供堆疊操作的介面。

private資料成員負責堆疊資料的儲存。類別的概念相當適用 ADT的方法。

private 區必須負責描述如何儲存資料。例如,你可以用一般的陣列,動態配

置的陣列,或是一些更高階的資料結構,如鏈結串列。而 public 介面要隱藏真正

的表示方式,而以通用的方式表現之,如產生堆疊,加入資料項,等等。範例程式

10.10顯示一個方法,它假設編譯程式支援 bool型態。若你的系統尚未支援,你可

以用 int,0和 1取代 bool,false和 true。

範例程式 10.10 stack.h

// stack.h -- class definition for the stack ADT #ifndef STACK_H_ #define STACK_H_ typedef unsigned long Item; class Stack { private: enum {MAX = 10}; // constant specific to class Item items[MAX]; // holds stack items int top; // index for top stack item public: Stack(); bool isempty() const; bool isfull() const; // push() returns false if stack already is full, true otherwise bool push(const Item & item); // add item to stack // pop() returns false if stack already is empty, true otherwise bool pop(Item & item); // pop top into item }; #endif

novia
螢光標示
Page 30: Chap01 - Gotop

517

result.hours = totalminutes / 60; result.minutes = totalminutes % 60; return result; }

有了這宣告和定義,敘述

A = 2.75 * B;

會轉成

A = operator*(2.75, B);

並呼叫我們剛剛定義的非成員夥伴函數。

總之,類別的夥伴函數是非成員函數,其存取權力與成員函數相同。

夥伴函數是否破壞了 OOP﹖

乍看之下,夥伴函數似乎違反了 OOP資料隱藏的原則,因為夥伴的機制允許非成

員函數存取私有資料。但這是比較狹窄的看法,相反的,可將夥伴函數視為類別延

伸介面的一部份。例如,在觀念上,double 乘以 Time 值與 Time 值乘以 double

是很相似的。前者需以夥伴函數完成,而後者可用成員函數完成,這是 C++ 語

法的結果,不是深層觀念的差異。不論是使用夥伴函數或類別成員函數,你都可

以用相同的使用者介面表達任一種操作。而且記住,唯有類別宣告才能決定函數

是否為夥伴函數,所以類別宣告仍控管哪些函數可以存取私有資料。簡言之,夥

伴函數與類別成員函數是表示類別介面的兩個不同的機制。

實際上。這特殊的夥伴函數可以寫成非夥伴函數,只要將其定義加以修改,使

其能夠把乘法運算中,把位置應該在前面的數值交換過來即可:

Time operator*(double m, const Time & t) { return t * m; // use t.operator*(m) }

原來的版本明確地存取 t.minutes 和 t.hours,所以必須是夥伴。這版本只是

完整的使用 Time物件 t,讓成員函數處理私有值,所以這版本不需要是夥伴。然而

將此版本宣告為夥伴仍然是不錯的想法。最重要的是它將函數變成正式類別介面的

一部份。其次,若你稍後發現函數需要直接存取私有資料,你只要改變函數定義而

不用修改類別原型。

如果你想多載類別的運算子,而且還希望使用非類別項作為運算子的第一個

運算元,你可以用夥伴函數維持運算元的順序。

novia
螢光標示
Page 31: Chap01 - Gotop

534

} else { cout << "Incorrect 3rd argument to Vector() -- "; cout << "vector set to 0\n"; x = y = mag = ang = 0.0; mode = 'r'; } }

若第三個引數是 'r' 或是省略(原型指定預設值 'r'),這輸入會解釋為直角

座標,若是 'p' 值則會解釋為極座標。

Vector folly(3.0, 4.0); // set x = 3, y = 4 Vector foolery(20.0, 30.0, 'p'); // set mag = 20, ang = 30

注意,若你提供 x和 y值,則建構函數用私有的成員函數 set_mag() 和 set_ang()

指定長度和角度值;若你提供長度和角度值,則用私有的成員函數 set_x() 和 set_y()

指定 x和 y值。還要注意,若不是指定 'r' 或 'p',則建構函數會送出警告訊息,

並將狀態設為 'r'。

同樣的,operator<<() 函數使用 mode決定如何顯示值:

// display rectangular coordinates if mode is r, // else display polar coordinates if mode is p ostream & operator<<(ostream & os, const Vector & v) { if (v.mode == 'r') os << "(x,y) = (" << v.x << ", " << v.y << ")"; else if (v.mode == 'p') { os << "(m,a) = (" << v.mag << ", " << v.ang * Rad_to_deg << ")"; } else os << "Vector object mode is invalid"; return os; }

可以指定 mode的各個運算函數都會小心地只用有效值 'r' 和 'p',所以這函

數最後的 else 應該都不會到達。但是檢查是不錯的作法,因為這檢查有助於攔截

其他難解的程式設計錯誤。

多種表示和類別

量有許多不同但同義的表示法是很常見的。例如,在美國用一加侖跑多少英里計

算耗油量;在歐洲則用一公升跑多少公里計算。你可以用字串或數值表示數目,

你可以用 IQ或是大笨蛋來表示智慧。類別本身可以在單一物件中包含一個資料項

novia
螢光標示
Page 32: Chap01 - Gotop

557

當運算子函數是成員函數時,第一個運算元是呼叫此函數的物件。例如,在前

面的敘述中,up物件是呼叫的物件。若你想將運算子函數定義為第一個運算元不是

類別物件,則你必須使用夥伴函數。然後你可以任意順序將運算元傳入函數定義。

運算子多載最常用到的地方就是定義 << 運算子,使其可以連結 cout物件顯示

物件的內容。為了讓 ostream物件成為第一個運算元,將運算子函數定義為夥伴函

數。而為了使重新定義的運算子可以串接起來,所以其回傳型態是 ostream &。下

面是滿足這些需求的一般格式:

ostream & operator<<(ostream & os, const c_name & obj) { os << ... ; // display object contents return os; }

但是,若類別有成員函數可以傳回欲輸出之資料成員的值,則你可以使用這些

成員函數而不是在 operator<<() 中直接存取之。此時,這函數不一定是(而且不

應該是)夥伴函數。

C++ 允許建立不同類別型態之間的轉換。首先,有單一引數的建構函數就像

是轉換函數,將引數型態值轉成類別型態。如果你將引數型態的值指定給物件,則

C++ 會自動呼叫建構函數。例如,假設 string類別的建構函數以 char * 為單一

引數,則若 bean為 string物件,則可使用下面的敘述:

bean = "pinto"; // converts type char * to type String

但若在建構函數宣告之前加上關鍵字 explicit,則此建構函數只能用在明確的

轉換。

bean = String("pinto"); // converts type char * to type String explicitly

要將類別轉成其他型態,你必須定義轉換函數提供有關如何轉換的指示。轉換

函數必須是成員函數;如果要轉成 typename,其原型如下:

operator typeName();

注意它沒有宣告函數回傳型態,沒有引數,而且必須回傳轉換後的值(雖然沒

有宣告函數回傳型態)。例如,將 Vector型態轉換成 double型態的函數格式為:

Vector::operator double() { ... return a_double_value; }

novia
螢光標示
novia
螢光標示
Page 33: Chap01 - Gotop

583

宣告回傳型態為 char & 使你可以將值指定至特定的元素。例如,你可以:

String means("might"); means[0] = 'r';

第二個敘述會轉成多載的運算子函數呼叫:

means.operator[](0) = 'r';

將 'r' 設給成員函數的回傳值。但是此函數回傳 means.str[0] 的 reference,

所以此程式碼等於

means.str[0] = 'r';

最後一行程式碼違反私有資料的存取,但是因為 operator[]() 是類別成員函

數,所以可以修改陣列內容。最終的結果是 "might" 變成 "right"。

假設有一常數物件:

const String answer("futile");

若你剛剛看過的 operator[]() 定義是唯一可用的定義,則下面的程式碼會標

示為錯誤:

cout << answer[1]; // compile-time error

這原因是 answer是 const,而且此成員函數不保證不會修改資料(事實上,有

時此成員函數的工作是修改資料,所以它不能作任何保證)。

但是 C++ 會區別多載時的 const和非-const函數簽名,所以我們可以提供第

二個版本的 operator[](),只是用於 const String物件:

// for use with const String objects const char & String::operator[](int i) const { return str[i]; }

有了這些定義,你可以讀-寫存取一般的 String物件並唯讀存取 const String

資料:

String text("Once upon a time"); const String answer("futile"); cout << text[1]; // ok, uses non-const version of operator[]() cout << answer[1]; // ok, uses const version of operator[]() cin >> text[1]; // ok, uses non-const version of operator[]() cin >> answer[1]; // compile-time error

靜態類別成員函數 也可以將成員函數宣告為靜態(若函數宣告和定義是獨立的,則關鍵字 static

應出現在函數宣告中,而不是在函數定義中)。這產生兩個重要的結果。

novia
螢光標示
Page 34: Chap01 - Gotop

606

Memory contents: 00320AD0: Better Idea, 6 00320EC8: Heap2, 10 Heap1 destroyed Heap2 destroyed Better Idea destroyed Just Testing destroyed Done

範例程式 12.9 中的程式將兩個定位放置 new 的物件放在相鄰的地方,並且呼

叫適當的解構函數。

技術回顧 討論至此,你已經看過處理各種與類別相關之問題的程式設計技術,以下總結

這些技術和它們的使用時機。

多載 << 運算子 重新定義 << 運算子,使其結合 cout顯示物件的內容,其夥伴運算子函數的定

義格式如下:

ostream & operator<<(ostream & os, const c_name & obj) { os << ... ; // display object contents return os; }

此處的 c_name 表示類別名稱,若此類別提供公用成員函數回傳所需的內容,

則可以在運算子函數中使用這些成員函數,就不需使用夥伴的作法。

轉換函數 欲將單一值轉成類別型態,其類別建構函數的函數原型格式如下:

c_name(type_name value);

此處 c_name表示類別名稱,而 type_name表示欲轉換的型態名稱。

要將類別型態轉成其它型態,需要一個有下述原型的類別成員函數:

operator type_name();

雖然此函數沒有宣告回傳型態,但它應回傳目的型態之值。

記住,使用轉換函數要小心。你可用關鍵字 explicit 宣告建構函數以避免它

用於隱含式的轉換。

novia
螢光標示
novia
螢光標示
novia
螢光標示
novia
螢光標示
novia
螢光標示
Page 35: Chap01 - Gotop

607

建構函數中用 new的類別 類別用 new運算子為其成員配置記憶體,在設計上有幾點要注意(沒錯,我們

剛剛總結過這些注意事項,但這是非常重要不可忘記的規則,尤其是因為編譯程式

不知道這些問題,所以無法發現錯誤)。

任何用 new配置空間的類別成員,須在解構函數中對其使用 delete運算子釋放

此記憶體。

若解構函數中用 delete釋放類別成員指標所指的記憶體,則此類別的所有建構

函數都應用 new初始化此指標或將其設為空指標。

建構函數不能混用有無中括號的 new,只能選定一種。若建構函數使用 new[] 則

解構函數就要用 delete[];若建構函數使用 new則解構函數就要用 delete。

定義複製建構函數時要配置新的記憶體,而非複製指向已存在之記憶體的指標值。這使程式可將類別物件初始化為另一個類別物件。這建構函數的原型格

式為:

className(const className &)

應定義類別的成員函數多載指定運算子,而且函數定義的格式如下(此處c_pointer 是類號 c_name 的成員,型態為指向 type_name 的指標)。以下的範

例假設建構函數是使用 new [] 來初始變數 c_pointer。

c_name & c_name::operator=(const c_name & cn) { if (this == & cn_) return *this; // done if self-assignment delete [] c_pointer; // set size number of type_name units to be copied c_pointer = new type_name[size]; // then copy data pointed to by cn.c_pointer to // location pointed to by c_pointer ... return *this; }

模擬佇列 我們試試將對類別的瞭解應用程式設計的問題上。Bank of Heather想在 Food

Heap 超市放置一台自動櫃員機,但超市經理擔心自動櫃員機前排隊的人會影響超

市的流量,而欲限制自動櫃員機前排隊的人數。所以 Bank of Heather想估計顧客

需排隊排多久。你的任務是準備一個程式模擬排隊情形,使超市經理可以了解自動

櫃員機所造成的影響。

novia
螢光標示
Page 36: Chap01 - Gotop

612

類別的私有區域,則宣告的型態只能用在此類別中。若宣告置於公用區域,則在

類別之外可用範疇運算子使用此宣告的型態。例如,若 Node宣告在 Queue類別的

公用區域,則可以在 Queue類別之外宣告型態為 Queue::Node的變數。

解決資料的表示方式後,下一步為撰寫類別的成員函數。

類別的成員函數 類別的建構函數要指定每個類別成員之值。因為佇列一開始為空的狀態,所以

front 和 rear 指標應該都設為 NULL(或 0),item 設為 0。而且要將佇列的元素個

數上限 qsize設為建構函數引數 qs之值。下面是一個不可行的寫法:

Queue::Queue(int qs) { front = rear = NULL; items = 0; qsize = qs; // not acceptable! }

這問題是 qsize 是 const,只能初始化成某一個值,而不能指定值。基本上,

呼叫建構函數產生物件是在執行大括號內的程式碼之前。因此,呼叫 Queue(int qs)

建構函數會先為 4個成員變數配置空間,然後程式進入大括號內,用一般指定將值

放入配置的空間中。因此要初始化 const資料成員,須在物件產生時,即未執行建

構函數的主體前執行初始化。C++ 為此提供一種特殊的語法,稱成員初始串列

(member initializer list),此串列以冒號開始,初始元之間以逗號隔開,置於引

數串列的右小括號之後、函數主體的左大括號之前。若資料成員的名稱為 mdata,

初始值為 val,其初始元的格式為 mdata(val)。以此表示法,Queue的建構函數為:

Queue::Queue(int qs) : qsize(qs) // initialize qsize to qs { front = rear = NULL; items = 0; }

一般來說,初始值可為常數或是取自建構函數之引數串列中的引數。這方法不

只是初始化常數,你也可以用下面的方式撰寫 Queue的建構函數:

Queue::Queue(int qs) : qsize(qs), front(NULL), rear(NULL), items(0) { }

只有建構函數能用成員初始串列的語法。此語法除了處理上例的 const類別成

員之外,還可以用來處理 reference的類別成員,如:

novia
螢光標示
novia
螢光標示
novia
螢光標示
Page 37: Chap01 - Gotop

689

使用轉換函數必須小心,若是合理的才使用它們。此外,某些類別設計在定義

轉換函數後,容易使人寫出模稜兩可的程式碼。例如,假設你定義了將第 11 章的

vector型態轉換成 double型態的轉換函數,而且有下面的程式碼:

vector ius(6.0, 0.0); vector lux = ius + 20.2; // ambiguous

編譯程式會將 ius 轉換成 double 型態,然後執行 double 加法;或是將 20.2

轉換成 vector(利用建構函數),然後執行 vector 加法?答案都不是,編譯程式

會通知你,使用了模糊的建構。

以值傳遞物件與以 reference傳遞物件 一般來說,如果你設計一個函數需要物件引數,則你應該採取傳遞物件

reference,而非物件值。一個原因是基於效率,以值傳遞物件牽涉到呼叫複製建構

函數,產生一個臨時副本,之後還要呼叫解構函數。呼叫這些函數必須花費時間,而

且複製一個大物件較傳遞 reference更慢。如果函數不會修改物件,則將引數宣告成

const reference。

以 reference傳遞物件另一原因是當繼承使用虛擬函數時,若函數定義接受基

礎類別 reference 的引數,則也能正確地接收衍生類別的 reference,如本章之前

所提(另外也請參考稍後會看到之「虛擬成員函數」的討論)。

傳回物件與傳回 reference 有些類別成員函數會傳回物件。你可能已經注意到一些成員直接傳回物件,而

有些傳回 reference。若成員函數必須回傳物件,而且不是一定是物件時,你應使

用 reference取代物件。我們更進一步討論這個問題。

首先,直接傳回物件與傳回 reference的程式碼差異在於函數原型和標頭:

Star nova1(const Star &); // returns a Star object Star & nova2(const Star &); // returns a reference to a Star

其次,你應回傳 reference而不回傳物件的原因是,回傳物件會產生回傳物件

的暫時副本。這是呼叫程式可以使用的副本。因此傳回物件需花費時間呼叫複製建

構函數建立副本,呼叫解構函數清除副本。回傳 reference則省下時間和記憶體。

直接回傳物件與以值傳遞物件類似:都需要處理暫時副本。同樣的,回傳 reference

與以 reference傳遞類似:呼叫函數和被呼叫函數皆使用相同的物件。

但是不是永遠皆適合傳回 reference。函數不能傳回在函數中產生的暫時物件

reference,因為當函數結束後物件就會消失,reference會變成無效。在此情況,

程式必須回傳物件以產生呼叫程式可以使用的副本。

novia
螢光標示
Page 38: Chap01 - Gotop

696

函數 是否可以繼承 成員或是夥伴 產生預設形式 可否為虛擬 有無回傳型態

[] Yes 成員 No Yes Yes

-> Yes 成員 No Yes Yes

op= Yes 兩者皆可 No Yes Yes

new Yes 靜態成員 No No void *

delete Yes 靜態成員 No No void

其他運算子 Yes 兩者皆可 No Yes Yes

其他成員 Yes 成員 No Yes Yes

夥伴 No 夥伴 No No Yes

總結 繼承特性讓你可以依據需求,從既存類別(基礎類別)定義出新類別(衍生類

別)。公用繼承建構 is-a的關係,意義是衍生類別的物件也是一種基礎類別的物件。因為是 is-a關係的一部份,所以衍生類別繼承了基礎類別的資料成員和大部分的成員函數。但是衍生類別並不會繼承基礎類別的建構函數、解構函數和指定運算子。

衍生類別能夠直接存取基礎類別的公有和保護成員,而只能藉由公用和保護的基礎

類別成員函數才能存取基礎類別的私有成員。然後,你可以在類別中加入新的資料

成員和成員函數,而且可以將此衍生類別再當做基礎類別,衍生出更多類別。程式

建立衍生類別物件時,最先呼叫基礎類別建構函數,再呼叫衍生類別建構函數。每

個衍生類別都需要有自己的建構函數。當程式產生衍生類別物件時,它先呼叫基礎

類別的建構函數,然後才是衍生類別的建構函數。當程式刪除物件時,先呼叫衍生

類別的解構函數,再呼叫基礎類別的解構函數。

如果類別要作為基礎類別,則你可以選擇使用保護的成員而不是私有的成員,

如此衍生的類別可以直接存取這些成員。但是一般而言,使用私有成員可以降低程

式設計出錯的範圍。如果你希望衍生類別能夠重新定義基礎類別的成員函數,則此

成員函數應宣告成 virtual。這使得指標或 reference 存取的物件是根據物件型態

而不是 reference或指標型態來處理。特別是基礎類別的解構函數必須是虛擬函數。

你可以定義 ABC(抽象基礎類別),表示只定義函數介面而不設計函數作法。

例如,你可以從特殊的形狀類別中定義抽象的 Shape 類別,Circle 和 Square 則是

從此衍生的類別。抽象基礎類別必須包含至少一個純虛擬成員函數。定義純虛擬成

員函數是在函數宣告的分號之前放置 = 0:

novia
螢光標示
Page 39: Chap01 - Gotop

696

函數 是否可以繼承 成員或是夥伴 產生預設形式 可否為虛擬 有無回傳型態

[] Yes 成員 No Yes Yes

-> Yes 成員 No Yes Yes

op= Yes 兩者皆可 No Yes Yes

new Yes 靜態成員 No No void *

delete Yes 靜態成員 No No void

其他運算子 Yes 兩者皆可 No Yes Yes

其他成員 Yes 成員 No Yes Yes

夥伴 No 夥伴 No No Yes

總結 繼承特性讓你可以依據需求,從既存類別(基礎類別)定義出新類別(衍生類

別)。公用繼承建構 is-a的關係,意義是衍生類別的物件也是一種基礎類別的物件。因為是 is-a關係的一部份,所以衍生類別繼承了基礎類別的資料成員和大部分的成員函數。但是衍生類別並不會繼承基礎類別的建構函數、解構函數和指定運算子。

衍生類別能夠直接存取基礎類別的公有和保護成員,而只能藉由公用和保護的基礎

類別成員函數才能存取基礎類別的私有成員。然後,你可以在類別中加入新的資料

成員和成員函數,而且可以將此衍生類別再當做基礎類別,衍生出更多類別。程式

建立衍生類別物件時,最先呼叫基礎類別建構函數,再呼叫衍生類別建構函數。每

個衍生類別都需要有自己的建構函數。當程式產生衍生類別物件時,它先呼叫基礎

類別的建構函數,然後才是衍生類別的建構函數。當程式刪除物件時,先呼叫衍生

類別的解構函數,再呼叫基礎類別的解構函數。

如果類別要作為基礎類別,則你可以選擇使用保護的成員而不是私有的成員,

如此衍生的類別可以直接存取這些成員。但是一般而言,使用私有成員可以降低程

式設計出錯的範圍。如果你希望衍生類別能夠重新定義基礎類別的成員函數,則此

成員函數應宣告成 virtual。這使得指標或 reference 存取的物件是根據物件型態

而不是 reference或指標型態來處理。特別是基礎類別的解構函數必須是虛擬函數。

你可以定義 ABC(抽象基礎類別),表示只定義函數介面而不設計函數作法。

例如,你可以從特殊的形狀類別中定義抽象的 Shape 類別,Circle 和 Square 則是

從此衍生的類別。抽象基礎類別必須包含至少一個純虛擬成員函數。定義純虛擬成

員函數是在函數宣告的分號之前放置 = 0:

Page 40: Chap01 - Gotop

702

何謂學生?有在學校註冊的人?參加思考研究的人?真實世界中從殘酷事件

中逃離的難民?具有一個識別名稱和一組考試成績的人?很顯然的,最後的定義完

全不適用於人的特性,但對電腦來說卻是很適合的簡單表達方式。所以我們根據此

定義來發展 Student類別。

將學生簡化成名字和一組考試成績意味可用一個具有兩個成員的類別:一個用

以表示名字,另一個則表示成績。對於名字來說,可以使用字元陣列,但這會使名

字的長度有所限制。除此之外,我們還可以使用 char 指標以及動態記憶體配置。

或者,就如第 12 章:「類別和動態記憶體配置」所說的,這會需要許多其它的程

式碼。除了這些,我們還可以使用一種類別的物件,而這個類別已經將所有該作的

都完成了。舉例來說,我們可以使用 String類別(第 12章),或者是標準 C++ 之

string 類別的物件。較為簡單的選擇是使用 string 類別,因為 C++ 函式庫已經

能夠提供所有實作的程式碼(要使用 String類別,我們還必須讓 string1.cpp實作

檔案成為我們程式專案的一部分)。

要表示成績同樣會遇到相似的選擇方式。當使用固定大小的陣列,就會有長度

的限制。使用動態記憶體配置就需要提供許多其它的程式碼。我們可以使用自己設

計的類別,以動態記憶體配置來表示一個陣列。我們還可以在標準 C++ 函式庫中

尋找可以表示資料的類別。

第三種選擇的問題在於我們尚未開發出這樣的類別。要寫出一個這樣的精簡版

本類別並不困難,因為 double型態的陣列與 char型態的陣列有許多相似的地方,

因此我們可以基於 String類別的思維,來設計一個 double陣列的類別。而事實上,

這也是本書先前版本所做的事情。

當然了,如果函式庫已經有提供合適的類別,那麼這個就會更簡單了。確實,

我們可以使用 valarray類別。

valarray類別:快速導覽 valarray 類別是由 valarray 標頭檔所提供的。如同它的名稱,這個類別是用

來處理數字型態的數值(或者是處理具有類似屬性的類別),所以它能夠支援好比

內容加總的運算,以及找出陣列中的最大值以及最小值。而為了要能夠處理不同種

類的資料型態,valarray定義為一個樣版類別。在本章稍後,會談到如何定義樣版

類別,不過現在更需要的是知道如何使用樣版類別。

以樣版的角度來說,當我們宣告一個物件時,必須提供它一個特定的型態。而

當我們宣告一個物件時,要做到這點,可以在識別字 valarray 後面加上角括號,

並且將希望使用的型態放入其中:

valarray<int> q_values; // an array of int valarray<double> weights; // an array of double

novia
螢光標示
novia
螢光標示
novia
螢光標示
Page 41: Chap01 - Gotop

732

若 Worker 是虛擬基礎類別,則自動傳遞資訊的方法不可行。例如,多重繼承

的建構函數如下:

SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other) : Waiter(wk,p), Singer(wk,v) {} // flawed

這問題是自動將 wk傳給 Worker物件可經兩條不同的路徑(Waiter和 Singer),

要避免潛在的衝突,若為虛擬基礎類別,C++ 不能自動透過中間類別傳遞資訊。

因此上述建構函數會初始化 panache和 voice成員,但是 wk引數中的資訊不會到達

Waiter子物件。無論如何,編譯程式需在產生衍生物件前建構基礎物件,所以此例

會用預設的 Worker建構函數。

若不用虛擬基礎類別的預設建構函數,你需要明確地引用適當的基礎建構函

數。因此,這建構函數如下:

SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other) : Worker(wk), Waiter(wk,p), Singer(wk,v) {}

此程式碼會明確的引用 Worker(const Worker &) 建構函數。對虛擬基礎類別這

種用法是合法且必須的,但對非虛擬基礎類別則是不合法的。

若類別有間接的虛擬基礎類別,則此類別需清楚的呼叫虛擬基礎類別的建構

函數,除非只需要虛擬基礎類別的預設建構函數。

那一個成員函數? 除了修改建構函數的規則外,MI仍需作其它程式碼的調整。將 Show() 成員函

數延伸至 SingingWaiter 類別。因 SingingWaiter 物件沒有新的資料成員,所以你

會認為此類別使用繼承的成員函數即可。這會產生第一個問題。若你省略新版本的

Show() 並用 SingingWaiter物件呼叫繼承的 Show() 成員函數:

SingingWaiter newhire("Elise Hawks", 2005, 6, soprano); newhire.Show(); // ambiguous

若為單一繼承且沒有重新定義 Show() 會使用最近祖先的定義。在此情況,每

個直接的祖先都有 Show() 函數,此呼叫會造成混淆。

novia
螢光標示
Page 42: Chap01 - Gotop

746

template <class Type> // or template <typename Type> bool Stack<Type>::push(const Type & item) { ... }

若在類別宣告中定義成員函數(內嵌定義),則可省略樣版宣告和類別修飾元。

範例程式 14.13結合類別和成員函數樣版。但需了解這些樣版並不是類別和成

員函數的定義,它們只是告訴 C++ 編譯程式如何產生類別和成員函數定義。實現

特定樣版,如處理 string物件的堆疊類別,稱為實體化(instantiation)或特定化

(specialization)。除非編譯程式已經支援關鍵字 export,否則將樣版成員函數

置於不同檔案時會不能執行,因為樣版不是函數,不能個別編譯。樣版必須與要求

特定樣版實體化的程式碼放在一起,最簡單的方法是將所有的樣版資訊置於標頭

檔,在使用樣版的檔案中引入此標頭檔。

範例程式 14.13 stacktp.h

// stacktp.h -- a stack template #ifndef STACKTP_H_ #define STACKTP_H_ template <class Type> class Stack { private: enum {MAX = 10}; // constant specific to class Type items[MAX]; // holds stack items int top; // index for top stack item public: Stack(); bool isempty(); bool isfull(); bool push(const Type & item); // add item to stack bool pop(Type & item); // pop top into item }; template <class Type> Stack<Type>::Stack() { top = 0; } template <class Type> bool Stack<Type>::isempty() { return top == 0; }

novia
螢光標示
Page 43: Chap01 - Gotop

778

多重繼承(MI)在類別設計上可以重複利用一個以上的類別。私有或保護的

MI表現 has-a的關係,但公用 MI表現 is-a 的關係。在多重繼承中,多重定義的名稱和多重繼承的基礎類別都會產生問題,可用類別修飾元解決名稱的不明確性,用

虛擬基礎類別避免繼承多重基礎類別。但是使用虛擬基礎類別在其建構函數的初始

化串列及解決不確定性問題上引進新規則。

類別樣版可以產生通用的類別設計,其中型態,通常是成員型態,是用型態參

數表示。典型的樣版格式如下:

template <class T> class Ic { T v; ... public: Ic(const T & val) : v(val) { } ... };

此處的 T是型態參數,而且其行為就像是之後會指定之真正型態的替身(此參

數可為任何有效的 C++ 名稱,T和 Type都是常見的選擇)。在此例中可用 typename

取代 class:

template <typename T> // same as template <class T> class Rev {...} ;

以一特殊型態宣告類別物件就會產生類別定義(實體化)。例如,宣告

class Ic<short> sic; // implicit instantiation

會使編譯程式產生類別宣告,樣版中每個型態參數 T都會置換成類別宣告中的

實際型態 short。此時的類別名稱是 Ic<short>,而非 Ic。Ic<short> 稱為樣板特定

化,這是隱式實體化。

當你用關鍵字 template宣告類別的明確特定化時會產生顯式實體化:

template class IC<int>; // explicit instantiation

在此情況即使未要求此類別的任何物件,編譯程式仍會使用通用的樣版產生

int特定化 Ic<int>。

你可以提供顯式特定化的類別宣告覆蓋樣版定義,就像是定義類別,以

template<> 開頭,之後是樣版類別名稱,後面再接著角括號,內含你要特定化的型

態。例如,你可以用字元指標將 Ic類別特定化:

novia
螢光標示
novia
螢光標示
Page 44: Chap01 - Gotop

830

class LabeledSales : public Sales { public: static const int STRLEN = 50; // could be an enum class nbad_index : public Sales::bad_index { private: char lbl[STRLEN]; public: nbad_index(const char * lb, int ix, const char * s = "Index error in LabeledSales object\n"); const char * label_val() {return lbl;} }; explicit LabeledSales(const char * lb = "none", int yy = 0); LabeledSales(const char * lb, int yy, const double * gr, int n); virtual ~LabeledSales() { } const char * Label() const {return label;} virtual double operator[](int i) const throw(std::logic_error); virtual double & operator[](int i) throw(std::logic_error); private: char label[STRLEN]; };

我們來稍微探討一下範例程式 15.14。首先,符號常數 MONTHS是在 Sales的保

護區;這使得此數值可以在衍生類別,如 LabeledSales內使用。

接下來,bad_index 類別以巢狀類別的形式,放置在 Sales 的公有區;這使得

該類別可以成為 catch區塊所使用的型態。請注意,要在外部辨識這個類別時,要

使用 Sales::bad_index。這個類別是由標準 logic_error 類別所衍生出來的。

bad_index類別具有儲存及報告超出範圍的陣列索引值。

nbad_index 類別以巢狀類別的形式,放置在 LabeledSales 的公有區,在外部

的程式碼,可以 LabeledSales::nbad_index加以使用。它是由 bad_index衍生過來

的,並且再加上儲存與報告 LabeledSales 物件之標籤的能力。由於 bad_index 由

logic_error衍生,nbad_index也算是由 logic_error衍生出來的。

這兩個類別都有多載 operator[]() 成員函數,用以存取在物件中的個別陣列

元素,以及當索引值超出範圍時,丟出一個異常。請注意這裡的異常規格:

// Sales version virtual double operator[](int i) const throw(std::logic_error); // LabeledSales version virtual double operator[](int i) const throw(std::logic_error);

因為異常規格型態在規則上可以用於衍生類別,std::logic_error 型態可與

bad_index型態和 nbad_index型態匹配。

novia
螢光標示
novia
螢光標示
Page 45: Chap01 - Gotop

863

當遇到檔案結尾時,也就是輸入資料流的 eofbit被指定,此時 fail() 與 eof()

成員函數都會回傳 true。

當遇到作為界定的字元(預設是使用 \n),此時該字元會由輸入資料流移出,

並且不會儲存。

當讀到可以讀取之最多字元個數(此個數會比 string::npos,以及能夠指派使

用之記憶體位元組個數小)時,也就是輸入資料流的 failbit 會被指定,並且

fail() 成員函數會回傳 true。

(輸入資料流有一個描述狀態的制度,用以記錄資料流發生的錯誤狀態。在此

制度中,eofbit會用來檢查是否讀取至檔案結尾;failbit會檢查是否有輸入錯誤;

badbit可用以檢查一些無法辨識的錯誤,如硬體錯誤;goodbit則表示一切都完好

順利。第 17章會對這些作更深入的討論。)

string物件的 operator>>() 函數的特性與上述的類似,除了對於界定輸入資料

的字元,不是原本先讀取然後捨棄的方式,而是讀取到空白字元,然後將該字元留

在輸入佇列中。空白字元可以是一個空白字元、換行字元,或者是 tab字元,或者

更廣泛地來說,就是 isspace() 會回傳 true的任何字元。

到目前為止,我們已經看過數個以 console 方式作 string 輸入的範例。由於

string物件的輸入函數會與資料流一起使用,並且可以識別是否讀到檔案結尾,因

此我們也可以在檔案輸入中使用它們。範例程式 16.2 是一個簡短的程式,可由檔

案中讀取字串。程式會假設檔案內容所包含的字串會由冒號加以區隔,並且使用

getline() 成員函數來指定輸入資料界定字元。接著程式會以一個字串使用一行輸

出的方式,以數字編號和字串內容加以呈現資料。

範例程式 16.2 strfile.cpp

// strfile.cpp -- read strings from a file #include <iostream> #include <fstream> #include <string> #include <cstdlib> int main() { using namespace std; ifstream fin; fin.open("tobuy.txt"); if (fin.is_open() == false) { cerr << "Can't open file. Bye.\n"; exit(EXIT_FAILURE); } string item; int count = 0;

novia
螢光標示
Page 46: Chap01 - Gotop

872

多載 C函數使用 string物件

你可以用多載的 == 運算子比較 string物件。但是因大小寫相異,所以 == 運

算子於執行相等性的比較時在某些情況會產生問題。例如,一個程式也許會

比較使用者的輸入和常數值,而且使用者也許不會使用相同的大小寫。範例

程式 16.3將所有輸入的內容轉為小寫來避免這樣的問題。這裡還有其它的方

法。許多 C函式庫提供 stricmp() 或者 _stricmp() 函數,它們可以作忽略

字母大小寫的比較測試(然而,這種函數沒有列在 C 標準之中,所以並非所

有環境都可以找到使用)。我們可以建立這個函數的多載版本,成為這裡可

以使用的:

#include <cstring> // for stricmp() on many systems #include <string> // string object inline bool stricmp( const std::string& strA, const std::string& strB ) // overloaded function {

return stricmp( strA.c_str(), strB.c_str() ) == 0; // C function } string strA; cin >> strA; // assume user enters Maplesyrup string strB = "mapleSyrup"; // stored constant bool bStringsAreEqual = stricmp( strA, strB );

使用簡化的語法,現在你可以不管大小寫即可比較兩個字串的相等性。而

c_str() 成員函數則提供一種方式,將 C-格式字串函數轉換為 string 物件

函數。

這節將 string類別視為是以 char型態為基礎。事實上,如先前所提,字串函

式庫其實是以樣版類別為基礎:

template<class charT, class traits = char _traits<charT>, class Allocator = allocator<charT> > basic_string {...};

此類別包含下列兩個 typedef:

typedef basic_string<char> string; typedef basic_string<wchar_t> wstring;

因此你可以使用以 wchar_t 和 char 型態為基礎的字串。甚至你可以發展一些

類似字元的類別,並搭配 basic_string類別樣版,使此類別滿足某些需求。traits

類別是用來說明所選定字元型態的性質,像是如何比較值。對於 char 和 wchar_t

型態,已經定義 char_traits 樣版的特定化,而且這些都是 traits 的預設值。

Allocator類別是管理記憶體配置的類別。對於 char和 wchar_t型態,已經定義了

allocator 樣版的特定化,而且這些都是預設值。它們以一般的方式使用 new 和

delete,但你也可以自行保留一塊記憶體並使用自己的配置方法。

novia
螢光標示
Page 47: Chap01 - Gotop

936

範例程式 16.16的輸出結果如下:

Original list contents: 4 5 4 2 2 3 4 8 1 4 After using the remove() method: la: 5 2 2 3 8 1 After using the remove() function: lb: 5 2 2 3 8 1 4 8 1 4 After using the erase() method: lb: 5 2 2 3 8 1

從輸出可以看出,成員函數 remove() 將 list la從 10個元素減少為 6個元素。

但是 list lb在使用 remove() 函數之後,仍含有 10個元素。後面 4個元素是可以不

需要的,因為它們要不是數值 4,不然就是移至 list前面之數值的副本。

雖然這些成員函數通常比較適合,但是非成員函數的函數比較通用。正如你所

見,你可以將它們用在陣列和 string 物件以及 STL 收納器,而且你可以用它們處

理混合的收納器型態,例如,將 vector收納器的資料存至 list或是 set。

使用 STL STL是一個函式庫,其部分的設計是可以一起運作的。STL元件不只是工具,

還可以用來建構其他的工具。我們用一個例子來說明。假設要寫一個讓使用者輸入

單字的程式。結束時,你希望能紀錄所輸入的單字,以字母排序這些單字(忽視大

小寫差異),以及每個單字輸入的次數。為了簡化程式,假設輸入不包含數字或標

點符號。

輸入以及儲存單字清單是很簡單的。依照範例程式 16.5,你可以建立

vector<string> 物件並用 push_back() 將輸入單字加入 vector:

vector<string> words; string input; while (cin >> input && input != "quit") words.push_back(input);

那要怎麼取得依字母順序排列的單字串列呢?可以在 unique() 之後使用

sort(),不過這個方法會覆蓋原來的資料,因為 sort() 是一個 in-place 演算法。

有一個更簡單的方法可以避免這個問題。建立一個 set<string> 物件,再從 vector

將單字複製(使用插入迭代器)到 set。set會自動對其內容排序而非呼叫 sort(),

且 set 中每一個關鍵值只有一份,所以就取代 unique() 了。等一下!還有忽略大

小寫差異的規格。其中一種處理的方法是用 transform() 取代 copy() 將資料從

vector複製到 set。而轉換函數則使用一個可以將字串轉換成小寫字體的函數。

set<string> wordset; transform(words.begin(), words.end(), insert_iterator<set<string> > (wordset, wordset.begin()), ToLower);

novia
螢光標示
Page 48: Chap01 - Gotop

958

能自動辨識這種重新導向的語法(Unix,Linux,和 DOS 3.0以後的版本也接受重

新導向運算子和檔案名稱之間可以選擇性的放置空白)。

標準輸出串流,以 cout表示,是程式輸出的一般管道。標準錯誤串流(以 cerr

和 clog 表示)的目的是處理程式的錯誤訊息。預設上,這三種一般都送至螢幕。

但是重新導向標準輸出不會影響 cerr 或 clog;因此,若你使用其中一個物件印出

錯誤訊息,即使一般的 cout 輸出重新導向至他處,則程式依然會在螢幕上顯示錯

誤訊息。以下面的程式片段為例:

if (success) std::cout << "Here come the goodies!\n"; else { std::cerr << "Something horrible has happened.\n"; exit(1); }

若沒有重新導向,則不管哪一個訊息都會顯示在螢幕上。但是,若輸出已經重

新導向至檔案,則 if部分的訊息會輸出至檔案,但是第二個訊息會輸出至螢幕上。

有些作業系統也允許重新導向標準錯誤。例如,在 Unix和 Linux中,2> 運算子可

以重新導向標準錯誤。

以 cout 輸出 我們已經提過 C++ 將輸出視為位元組串流(依實作和平台而定,這也許是

16-位元或 32-位元的位元組,然而都是位元組)。但是程式中有許多種資料的組成

單位都大於單一位元組。例如,int型態也許可以表示為 16-位元或 32-位元的二進

位值。double值也許可以表示為 64位元的二進位資料。但是當你將位元組串流送

至螢幕時,你會希望每個位元組都代表一個字元值。也就是說,要在螢幕上顯示數

字-2.34,你應送出 5 個字元 -,2,.,3,和 4 至螢幕上,而不是此值的內部 64-

位元浮點數表示法。因此對於 ostream類別最重要的任務之一是轉換數字型態,如

int或是 float,轉成以文字格式表示此值的字元串流。也就是說,ostream類別將

資料的二進位位元模式的內部表示方式轉換成字元位元組的輸出串流。要執行這些

轉換工作,ostream 類別提供數個類別成員函數。我們現在就來看看,這會總結本

書用過的成員函數以及描述其他在輸出外觀上提供較佳控制的成員函數。

多載 << 運算子 本書經常將 cout與所謂的插入運算子(insertion operator)一起使用:

int clients = 22; cout << clients;

novia
螢光標示
novia
螢光標示
novia
螢光標示
Page 49: Chap01 - Gotop

962

此處 cout是呼叫物件,而且 put() 是類別成員函數。就像 << 運算子函數,此

函數回傳呼叫物件的 reference,所以你可以用它連結輸出:

cout.put('I').put('t'); // displaying It with two put() calls

函數呼叫 cout.put('I') 回傳 cout,之後這會是 put('t') 的呼叫物件。

從這正確的原型中,你可以使用具有數字型態引數的 put(),如 int,而不是

char,並使函數原型自動將引數轉成正確的 char值。例如,你可以這樣做:

cout.put(65); // display the A character cout.put(66.3); // display the B character

第一個敘述將 int 值 65 轉成 char 值,然後顯示其 ASCII 碼為 65 的字元值,

第二個敘述將型態 double值 66.3轉成型態 char值 66,並顯示對應的字元。

在 Release 2.0 C++ 之前,這功能遲早都會用到,那時候,這語言以型態 int

表示字元常數。因此,敘述如

cout << 'W';

會將 'W' 解釋為 int值,然後將它顯示為整數 87,即此字元的 ASCII值。但是

敘述

cout.put('W');

則沒有問題。因為目前的 C++ 以 char型態表示 char常數,所以你可以用任

一種方法。

實作的問題是有些編譯程式會多載 put() 使其具有三種引數型態,分別是:

char,unsigned char,和 signed char。這會與具一個 int引數的 put() 產生混淆,

因為 int可以轉換成這三種型態。

write() 成員函數寫出整個字串並具有下列的樣版原型:

basic_ostream<charT, traits>& write(const char_type* s, streamsize n);

write() 的第一個引數是要顯示之字串的位址,第二個引數是要顯式的字元

數。使用 cout 呼叫 write() 會產生 char 特定化,所以回傳型態是 ostream &。範

例程式 17.1說明 write() 如何運作。

範例程式 17.1 write.cpp

// write.cpp -- using cout.write() #include <iostream> #include <cstring> // or else string.h int main() {

novia
螢光標示
Page 50: Chap01 - Gotop

1158

4. 建構函數的呼叫順序是依據類別衍生的順序,首先呼叫最早祖先的建構函數。解構函數的呼叫順序是相反的。

5. 是的,每個類別需要有自己的建構函數,如果衍生類別沒有新增成員,則建構函數可以只是一個空的主體,但一定要存在。

6. 只會呼叫衍生類別的成員函數,它取代基礎類別的定義。只有當衍生類別沒有重新定義成員函數,或是使用範疇運算子,才會呼叫基礎類別的成員函數。因

此會被重新定義的所有函數都應宣告為 virtual。

7. 若衍生類別的建構函數使用 new或 new [] 運算子來初始化類別成員中的指標,

則應定義指定運算子。更一般性的說法是,若預設的指定對於衍生的類別成員

是不正確的,就要定義指定運算子。

8. 是的,你可以將衍生類別物件的位址指定給基礎類別的指標。唯有利用明確的型態轉換,才能將基礎類別物件的位址指定給衍生類別指標(向下轉型),而

且使用這種指標並不一定安全。

9. 是的,你可以將衍生類別物件指定給基礎類別物件。任何衍生類別的新資料成員不會傳給基礎類別。程式會使用基礎類別的指定運算子。唯有衍生類別定義

轉換運算子,程式才可以將基礎類別物件指定給衍生類別物件。所謂轉換運算

子是以基礎類別的 reference 作為建構函數的唯一引數,或是定義以基礎類別

為參數的指定運算子。

10. 它可以這樣做,因為 C++ 的基礎類別的 reference 可以參考從此基礎類別衍

生的任何類別。

11. 傳遞物件值會呼叫複製建構函數。因為形式引數為基礎類別物件,所以會呼叫基礎類別的複製建構函數。複製建構函數的引數為基礎類別的 reference,所以

此 reference 可以參考以引數傳入的衍生物件。最後的結果是建立一個新的基

礎類別物件,其成員是衍生物件的基礎類別部分。

12. 傳遞物件的 reference 而不是物件值可使函數利用虛擬函數的功能。此外,傳

遞物件 reference 比較節省時間與記憶體,尤其是大物件。以值傳遞的優點是

可以保護原始資料,但以 reference傳遞加上 const就能達到同樣的目的。

13. 如果 head() 為一般成員函數,則 ph->head() 會呼叫 Corporation::head()。如

果 head() 為虛擬函數,則 ph->head() 會呼叫 PublicCorporation::head()。

14. 第一,這情況不適用 is-a 模式,所以公用繼承是不適當的。第二,House 中的

area() 定義會遮蔽 kitchen的 area() 版本,因為這兩個成員函數有不同的簽名。

novia
螢光標示
Page 51: Chap01 - Gotop

1162

2. 你可以將 string 物件設給另一個 string 物件。string 物件提供自己的記憶體

管理,所以通常不需要擔心字串會超過其容量。

3. #include <string> #include <cctype> using namespace std; void ToUpper(string & str) { for (int i = 0; i < str.size(); i++) str[i] = toupper(str[i]); }

4. auto_ptr<int> pia= new int[20]; // wrong, use with new, not new[] auto_ptr<string>(new string); // wrong, no name for pointer int rigue = 7; auto_ptr<int>(&rigue); // wrong, memory not allocated by new auto_ptr dbl (new double); // wrong, omits <double>

5. 堆疊的 LIFO觀念是在找到你要的資料之前,必須移除許多高爾夫球桿。

6. 因為 set只會儲存每個值的一份資料,如 5個 5分只會存成單一的 5。

7. 使用迭代器可在處理物件時,用類似指標的介面走訪資料,而不以陣列的方式(如在雙向鏈結串列中的資料)。

8. STL的方法使 STL函數可以用於一般陣列的指標以及指向 STL收納器類別的迭

代器,而增加其一般性。

9. 你可以將 vector 物件設給另一個 vector 物件。vector 管理它自己的記憶體,

所以可以將資料項插入 vector,它會自己自動調整大小。利用成員函數 at() 可

以取得自動的邊界檢查。

10. sort() 函數和 random_shuffle() 函數需要一個隨機存取的迭代器,而 list物

件恰好有一個雙向的迭代器。你可以用 list樣版類別的 sort() 成員函數(請參

考附錄 G)而非一般功能的函數來作排序,但是沒有一個成員函數等於

random_shuffle()。但是你可將 list 複製到 vector,對 vector 任意排列後,再

將結果複製回 list。

第 17 章 1. iostream檔案定義類別,常數,和一些處理輸出入的運作子。這些物件管理用

於 I/O 的串流和緩衝區。這檔案同時建立連接程式與標準輸出入串流的標準物

件(cin,cout,cerr,clog和寬字元的同等物件)。

novia
螢光標示

Recommended