第 8 章 物件導向程式設計入門 1
本章提要 8-1 物件導向與類別 8-2 定義類別 8-3 類別與指標 8-4 friend 類別的夥伴 8-5 綜合演練 2
8-1 物件導向與類別 寫程式的最終目的是為了解決人們的問題, 而當電腦的應用愈來愈廣泛時, 所需設計的程式也變得愈來愈複雜, 而以傳統程序導向的程式設計方式, 要解決真實世界中各種問題也就顯得捉襟見肘 3
物件導向與類別 此時就有人提出一種新的觀念, 亦即程式本身要能模擬真實世界的運作方式, 設計程式時的思考方式便是以真實世界的運作方式來思考如何解決問題, 這種新的計計理念就稱為物件導向程式設計, 其中模擬真實世界的方式就是使用物件 (object) 4
物件 :C++ 舞台劇的演員 如果將 C++ 程式比擬為一齣舞台劇的話, 那麼要說明甚麼是物件導向程式語言就不難了 要上演一齣舞台劇, 首先必須要有導演, 先將自己想要表達的理念構思好 然後再由編劇將劇裡所需要的角色 道具描繪出來, 並且將這些元素搭配劇情撰寫出劇本 最後, 再由演員以及道具在舞台上依據劇本實際的演出 5
物件 :C++ 舞台劇的演員 每一個 C++ 程式也一樣可以分解為這些要素, 導演就是撰寫程式的您, 要先設想程式想要完成甚麼事情 接下來的編劇當然還是您, 必須先規劃出如何完成工作 以下就說明 C++ 程式中相對於一齣舞台劇的各個元素, 讓讀者瞭解如何運用這樣的觀念設計程式 6
類別 (Class) 與物件 一齣舞台劇有了基本的構思之後, 接下來就要想像應該有哪些角色來展現這齣劇, 每一種角色都會有它的特性以及要做的動作 舞台劇的角色與演員 C++ 的類別與物件 7
舞台劇的角色與演員 舉例來說, 如果演出西遊記, 不可缺少的當然是美猴王這位主角, 它有多種表情 還能夠進行 72 變 除了美猴王以外, 當猴子猴孫的小猴子也少不了, 這些小猴子可能有 3 種表情, 而且可以跑步 爬樹等等 想好了角色 編好劇本後, 還得找演員來演出這些角色, 否則只是空有劇本, 觀眾根本看不到 有些角色, 像是美猴王, 在整個劇中只有一個 ; 但有些角色, 像是小猴子, 可能就會有好幾個, 就得找多個演員來演出這些角色, 只是每一個猴子站的位置 表情不一樣 8
舞台劇的角色與演員 9
舞台劇的角色與演員 同樣的道理, 舞台上除了演員外, 可能還需要一些布景或道具, 每一種布景也有它的特性與可能的動作 舉例來說, 西遊記中美猴王騰雲駕霧, 舞台上可少不了雲 雲可能有不同顏色 形狀, 也可以飄來飄去, 而且同時間舞台上可能還需要很多朵雲 10
舞台劇的角色與演員 這些不論是要演員演的 還是要道具來表現的, 都可以通稱為角色 在劇本中就以文字來描述這些角色的特性以及行為, 並且描述整個劇的進行, 然後再找演員或是製作道具來實際演出 11
C++ 的類別與物件 每種角色都有其自我的屬性和行為, 這些角色在 C++ 中就稱為類別 (Class), 例如小猴子這個角色, 應該都具有相似的屬性及行為 ( 例如都會爬樹 ) 而實際演出的演員則稱為物件 (Object), 每個物件都是獨立的個體, 可以表現出與眾不同的行為 例如在一場戲中, 有的小猴子在玩水 有的小猴子在爬樹等等 當我們在設計程式時, 首先就是規劃出參與演出的有哪些類型的角色, 並定義出相對應的類 12 別
C++ 的類別與物件 接著劇本就是寫在 main() 函式中, 在 main() 函式中各演出者要一一上場或放置在特定的位置上, 這些演出者就是所謂的物件, 它們是根據事先定義好的類別所產生的 例如需要有 5 個小猴子上場, 要由小猴子的類別產生 5 個小猴子物件上場演出 當我們構思好故事大綱後, 就需擬定參與角色的特性, 此時就是要宣告類別, 因此以下就先看如何宣告及定義類別 13
8-2 定義類別 類別又稱為使用者自訂資料型別 (User Defined Data Type), 表示它是由程式設計人員自訂出的資料型別, 而編譯器也會把類別當成一種資料型別來處理 定義類別的語法 資料成員 成員函式 靜態成員 14
定義類別的語法 我們可以用 class 或 struct 關鍵字來定義類別, 但最常用到的則是 class 定義類別時的語法如下 : 15
定義類別的語法 以上就是定義類別的敘述架構, 例如以下就是定義一個空白的類別, 其名稱為 Car: 完成類別的定義, 就等於告訴編譯器我們已定義了一個新資料型別, 接著就能用它來建立物件了, 宣告的方式和使用內建資料型別宣告變數一樣 : 16
定義類別的語法 在此類別名稱就像是一個新的型別名稱, 並可用來定義出各個性質相同的物件, 這些物件就稱為該類別的個體 (Instance), 例如上例中的 mycar yourcar 就稱為 Car 類別的個體 此外我們也可在定義類別時, 即建立物件, 不過這種用法目前較少使用 : 17
定義類別的語法 以上雖完成一個自訂資料型別的定義, 不過這樣的空白類別不能做什麼事, 所以我們要在裏面加入一些內容來描述類別的屬性及行為 類別的屬性是透過資料成員 (Data Member) 來模擬, 類別的行為則是以成員函式 (Member Function) 來模擬, 以下就分別來看如何在類別中定義這 2 種成員 18
資料成員 資料成員的宣告方式和一般的變數相同, 只不過資料成員是被宣告在類別之內, 例如描述汽車的類別可能需要記錄載油量 耗油量等屬性, 此時可在類別中宣告 2 個資料成員來代表這些屬性 : 19
資料成員 請注意, 資料成員不能設定初始值, 因為基本上每個物件彼此都不盡相同, 若類別的資料成員可設定初始值, 就變成每個物件的該項屬性都相同, 並不合理, 所以在定義類別時, 不可將資料成員設定初始值 20
物件導向程式設計的特性 : 封裝 物件導向程式設計具有三項特性 : 封裝 (Encapsulation ) 繼承 (Inheritance) 多面性 (Polymorphism), 本章我們先來認識什麼是封裝 封裝的意思是指將類別的屬性及操作屬性的方法都包裝在其中, 而且只對外公開一些操作資料的方法, 外人無法直接存取類別 ( 物件 ) 未公開的屬性 21
物件導向程式設計的特性 : 封裝 封裝的目的是讓使用類別的人, 不需要理會 瞭解類別中包裝了什麼樣的內容, 他只需透過公開的方法來操作物件即可達到使用類別的目的 ( 就像只要會操作搖控器就能看電視, 而不需瞭解電視內部的設計 ) 這樣的理念讓設計好的類別有如軟體 IC, 程式設計人員可將既有的 IC ( 類別 ) 組合起來, 實作出程式的功能, 如此不但讓程式碼可重複使用, 更能提高工作效率 22
物件導向程式設計的特性 : 封裝 封裝的另一個意義則是資料隱藏 (Data Hiding), 類別中的成員預設都無法被外界任意存取 因為若能被外界任意存取 修改資料成員, 又失去了物件導向程式設計的意義 類別中不能被外部存取的成員, 統稱為私有 (private) 成員 ; 相對於私有成員則是公開 (public) 成員, 它們可被類別外部的程式任意存取 23
物件導向程式設計的特性 : 封裝 為了方便說明, 我們暫時先將範例類別的資料成員設為公開的成員, 設定的方式就是在定義資料成員之前, 加上 public 關鍵字及冒號 : 24
物件導向程式設計的特性 : 封裝 public 稱為存取修飾字 (Access Specifier), 存取修飾字共有 3 個, 分別是 : public: 在此段落中的成員都是公開的成員 若是用 struct 定義類別, 則類別中的成員預設是公開的成員 private: 在此段落中的成員都是私有的成員 用 class 定義類別時, 則成員預設都是私有的成員 protected: 在此段落中的成員對外部程式而言屬於私有的成員, 但對繼承的類別而言, 則是公開的成員 25
物件導向程式設計的特性 : 封裝 由於使用 struct 定義類別時, 預設所有的成員都是公開的成員, 所以上述的例子若改用 struct 定義需寫成 : 26
struct 與 class 的差別 用 struct 與 class 定義類別只有兩個差異 : struct 的成員預設為 public;class 的成員預設為 private 繼承 struct 時, 預設是以 public 的方式繼承 ; 繼承 class 時, 則是以 private 的方式繼承 27
struct 與 class 的差別 除了以上兩點外, 用 struct 和 class 是沒有差別的 但由於 struct 是承襲自 C 語言而來的, 只不過在 C++ 中增強 擴充為具有類別的功能 ; 再加上 struct 成員預設可公開存取又失去封裝的意義, 因此大部份的 C++ 程式設計人員都習慣用 class 來定義類別, 而較少使用 struct 28
存取資料成員 定義了公開的資料成員後, 在類別以外的程式中 ( 例如 main() 函式中 ), 可透過成員存取 (class member access) 運算子. 來存取各物件中的公開資料成員 : 例如 : 29
存取資料成員 透過這種方式, 我們可以用 Car 類別寫一個模擬汽車行駛狀況的程式如下 : 30
存取資料成員 31
存取資料成員 32
存取資料成員 33
存取資料成員 第 20 行的 while 迴圈會在油量大於 0 的狀況下持續執行, 當油量不足以走完指定里程時, 就會在第 31 行顯示相關資訊, 並於第 33 行跳出迴圈 在此要提醒讀者, 類別的公開成員可由外界自由存取, 所以我們也可在建構物件時, 即以設定陣列元素 結構體成員初始值的相同方式來設定其公開成員的初始值, 所以上例中第 12~14 行可簡化成一行敘述 : 34
存取資料成員 我們用 gas 及 eff 兩個資料成員來表現汽車物件的屬性及狀態, 這樣的表示方式雖然不錯, 但上述程式的寫法, 也等於只是將變數放在類別之中, 完全沒有發揮類別的資料封裝功效, 因此我們必須再加入可操作物件的成員函式, 讓汽車類別更能模擬真實汽車的行為 35
const 資料成員 資料成員也可加上 const 關鍵字成為常數, 由於常數一經宣告即無法更改其值, 所以 const 資料成員可在類別定義中設定初值, 這是它們與一般資料成員不同之處 舉例來說, 若汽車類別代表的是同款式的車型, 其耗油量是固定的, 即可將之宣告為 const 36
成員函式 成員函式的種類 定義成員函式 呼叫成員函式 37
成員函式的種類 成員函式 (member function) 乃是類別的靈魂所在, 也是設計類別時的成敗關鍵 它不僅要提供一個公開的類別操作界面, 有時還必須完成許多特殊的功能, 我們大致可以將成員函式依功能分成下列幾種 : 用來操作物件的函式 : 例如我們定義了一種字串類別, 則複製字串 搜尋字串 替代字串等函式都是所謂操作物件的方法 38
成員函式的種類 用來存取物件內資料成員的函式 : 由於我們通常都將資料成員封裝在類別之內 ( 設為 private), 所以外界必須經由這類函式才能間接取得代表物件屬性或狀態的資料成員 僅供類別內部使用的函式成員 : 第 6 章提過, 有一項功能 ( 或說一段程式碼 ) 會重複用到時, 即可將它寫成函式以提高程式撰寫效率 同理, 如果有多個成員函式需要用到同一個功能, 但又不希望公開給外界使用, 那麼就可以將這個功能獨立成一個函式, 並將之設為 private 而封裝在類別之內 39
成員函式的種類 類別的自動管理函式 : 當一個新物件被產生或生命期結束時, 我們可能需要做一些特別的處理, 例如新物件要做一些初始化或配置記憶體的工作 ; 而物件的生命期結束時, 可能也要釋放所配置的記憶體或是做一些其他的善後工作 前兩類成員函式是初學者最常接觸到的, 以下就來介紹如何定義成員函式, 以設計操作物件及存取資料成員的成員函式 40
定義成員函式 成員函式既然也是函式, 所以定義的方式和一般函式沒什麼不同 : 都要有函式名稱 傳回值 參數等等 定義函式成員有兩種方式, 第 1 種是直接將函式本體定義在類別的大括號中, 此法適用在內容只有 1 2 行敘述的函式 : 41
定義成員函式 42
定義成員函式 上例中的 geteff() 及 checkgas() 函式都只有一行敘述, 因此將函式本體定義在類別內即可 定義在類別內的函式有項特性 : 就是它們預設都是行內函式, 換言之, 上述的定義對編譯器來說, 就相當於我們有在函式前加上 inline 關鍵字 : 43
定義成員函式 另一種定義函式的方式, 則是只在類別內宣告函式原型, 但將函式本體定義在類別的大括號之外, 此方法適用在函式內敘述較多的情況 由於類別的成員函式可與程式中的一般函式 甚至其它類別的成員函式使用相同的名稱, 因此在定義成員函式時, 需以類別名稱及範圍解析運算子 :: 明確讓編譯器瞭解我們定義的是哪一個類別的成員函式 44
定義成員函式 45
定義成員函式 請注意, 成員函式與一般函式或其他類別中的函式同名並不算多載 (overloading), 因為它們都分屬不同的視野, 所以彼此互不相關, 根本不會形成多載 只有同一個類別內的同名函式, 才會型成多載 : 46
定義成員函式 47
呼叫成員函式 成員函式為類別的成員, 在使用上和一般函式有個最大的差異, 即成員函式必須透過類別的物件來呼叫, 例如 : 另外, 大家應會注意到, 在前述的成員函式定義中, 都直接存取資料成員, 而未透過成員存取運算子 其道理很簡單, 當我們用物件呼叫成員函式時, 函式所存取的資料成員, 就是該物件自己的資料成員 : 48
呼叫成員函式 49
呼叫成員函式 由於我們每次都必須以 " 物件. 函式名稱 " 的方式來呼叫成員函式, 所以成員函式自然不會與類別以外的其他函式混淆 認識成員函式的定義與呼叫方式後, 我們將前面的 Car 類別重新定義, 將資料成員設為私有, 而外部則可透過公開的成員函式來操作物件, 範例程式內容如下 : 50
呼叫成員函式 51
呼叫成員函式 52
呼叫成員函式 53
呼叫成員函式 1. 第 15~28 行的 go() 函式即為模擬汽車行走的函式, 參數為要行走的公里數, 函式中會檢查油量是否足夠, 並將車子原本的油量減掉此趟耗掉的部份 2. 第 30~34 行的 init() 函式則是用於初始化私有的資料成員, 因我們已將資料成員宣告於 private: 的段落中, 所以只能透過成員函式來設定其值 其實類別還有一種更佳的成員初始化方式 54
呼叫成員函式 類別經過一番重新設計後, main() 函式的內容變得相對簡單, 而且 main() 函式的功能就很容易一目瞭然 或許有讀者覺得這樣設計, 似乎使整個程式變得複雜, 其實這是因為我們未讓類別發揮重複使用的效果, 如果目前是在開發中大型的專案, 則只要類別都妥善定義好, 則開發各部份的程式就相對簡單許多, 這也是物件導向程式設計的最大好處 55
類別中成員的宣告慣例 在 Ch08-02.cpp 範例程式第 4~13 行 Car 類別的定義中, 將私有的成員也特地宣告在 private: 段落中 讀者或許會覺得奇怪, 為何不直接在類別定義的大括號一開始, 就先宣告私有的成員, 這樣不是可少寫一個 "private:" 嗎? 56
類別中成員的宣告慣例 57
類別中成員的宣告慣例 左邊的寫法看似要多寫一點字, 但它卻是常見的 C++ 程式的慣用寫法 (convention), 其寫法是在類別定義中, 依最公開的成員先出現的原則來宣告資料成員及成員函式, 所以採這種寫法時, 即是依 public protected private 三種成員的順序宣告 當然這只是一種寫作習慣, 並非強制的語法規則 58
靜態成員 靜態資料成員 靜態成員函式 59
靜態資料成員 我們也可將類別中的資料成員前面加上 static 關鍵字, 使其成為靜態的資料成員 一般的資料成員是每個物件都有自己的資料成員 ; 但靜態資料成員則是所有同類別的物件共用的, 換言之類別的靜態資料成員在記憶體中只有一份, 每個同類別物件所存取到的靜態資料成員都是相同的, 若有物件更改了靜態資料成員的值, 則所有物件存取到的值也跟著改變 由於這項特性, 靜態資料成員適用於存放類別的共通屬性 60
靜態資料成員 舉例來說, 前面範例的汽車類別, 若是用來代表同型的車款, 則基本的耗油量應該是相同的, 此時即可將代表耗油量的資料成員, 宣告為靜態成員, 讓每個物件都使用同一個資料 : 61
靜態資料成員 靜態資料成員會被視為獨立的個體, 並不專屬於哪一個物件, 所以我們必須額外定義它 ( 初始化其值 ), 定義的形式如下 : 62
靜態資料成員 雖然這樣的定義方式好像在定義全域變數, 但靜態資料成員並非全域變數, 這一點要分清楚 以下範例示範物件共用靜態資料成員的情形 : 63
靜態資料成員 64
靜態資料成員 第 9 行宣告的靜態資料成員於第 12 行定義, 因此在 main() 函式中建立物件後, 即可透過物件立即存取其值 在第 21 行時以 One 物件呼叫成員函式設定靜態資料成員的值, 在第 22 行用 Two 物件取得靜態資料成員的值已經是更動後的新數值, 印證了各物件共用同一靜態資料成員的事實 65
靜態成員函式 除了資料成員可加上 static 關鍵字變成靜態資料成員外, 成員函式也可用同樣的方法設定為靜態成員函式 (static member function) 靜態成員函式與一般成員函式最大的不同包括 : 不需透過物件即可呼叫, 但為了與一般非成員函式區分, 在呼叫時需以類別名稱 :: 函式名稱 () 的方式呼叫 在函式中只能存取靜態的資料成員, 不能存取非靜態的成員 66
靜態成員函式 使用靜態成員函式的目的主有有兩種 : 首先就是設計成用來修改靜態資料的函式 ; 此外基於不需物件即可呼叫的特性, 靜態成員函式也適合用來設計通用性 工具性的類別 舉例來說, 在台灣加油時是以公升為單位, 在美國等地則是以加侖為單位, 我們就可替汽車類別設計換算這兩種單位的成員函式, 並設為 static, 讓所有程式不建立物件, 也能呼叫該函式來進行單位換算 請參考以下範例 : 67
靜態成員函式 68
靜態成員函式 69
8-3 類別與指標 指標在類別上的應用包括 : 建立指向物件的指標, 以及在類別中使用指標型別的資料成員, 其中包括一個所有類別都有的 this 指標 指向物件的指標 指標型別的成員 this 指標 70
指向物件的指標 建立指向基本資料型別的指標時, 要存取變數值, 需使用間接存取運算子 * 但建立指向物件的指標時, 要用物件指標存取公開的成員時, 並不需使用間接存取運算子, 但需使用另一個成員存取運算子 "->" ( 橫線加大於符號 ) 來存取成員, 例如 : 71
指向物件的指標 宣告類別指標變數時, 可用 new 運算子建立新的物件, 讓指標可指向該物件, 請參考以下範例 : 72
指向物件的指標 73
指標型別的成員 我們知道, 指標變數在宣告時並未設定要指向何處, 所以若在類別中使用指標型別的資料成員時, 必須記得在物件建立即初始化其指標成員所指的位址, 例如我們可在初始化物件的成員函式中, 動態配置記憶體給物件的資料成員, 請參考以下的範例 : 74
指標型別的成員 75
指標型別的成員 76
指標型別的成員 Str 類別只是個很簡單的字串類別, 其唯一的資料成員就是指向字串的指標, 由於建立物件時不會讓指標指向任何字串, 所以設計了兩個多載的 set() 成員函式, 其中之一是傳入字串指標, 並將資料成員也指向相同的位址 ; 另一個不需參數的版本, 則是直接配置新的記憶體空間, 然後將 "Empty!" 這個字串內容複製進入 77
指標型別的成員 上述的 Str 類別設計方式仍不夠好, 因為要初始化字串內容相當不便, 且要具有實用功能的字串類別, 必須設計相當多處理字串的成員函式 所幸 C++ 已經幫我們設計好一個實用的 string 類別, 在第 11 章就會介紹如何使用 string 類別處理字串 78
this 指標 當我們建立物件時, 每個物件都會享有獨自的記憶體空間, 以存放各自擁有的非靜態資料成員, 所以每個物件的資料成員都是獨立互不干擾 但是類別的成員函式則是所有物件共用的, 在記憶體中只會存一份成員函式的程式碼, 每個物件呼叫成員函式時, 都是執行到同一份程式碼 79
this 指標 這時候就出現一個問題, 當我們透過各物件呼叫成員函式來存取資料成員時, 成員函式是如何得知目前呼叫它的是哪一個物件? 如何存取到正確物件的資料成員, 而不會存取到別的物件的資料成員? 答案很簡單, C++ 編譯器在編譯成員函式時, 偷偷做了幾個動作, 以便讓成員函式能存取到正確物件的資料成員 : 80
this 指標 1. 編譯器會自動在成員函式的參數列最前面加上一個該類別的指標參數, 並以 this 為參數名稱, 然後再將函式定義內所有出現資料成員的部份, 均改成 this-> 資料成員的形式 : 81
this 指標 2. 替程式中實際呼叫成員函式的敘述, 偷偷加上呼叫者物件的位址做為第一個參數, 例如 : 這樣一來, 就等於讓成員函式都能從 this 指標取得呼叫物件的資料成員, 所以就不會發生存取到錯誤的資料成員的情形 82
this 指標 為讓讀者體會這個隱含在程式中的 this 指標確實存在, 我們用以下的範例程式透過 this 指標來取得物件的位址 : 83
this 指標 84
this 指標 第 8 行的 showthis() 成員函式將程式未定義的 this 指標輸出到 cout, 也就是輸出指標所指的位址 結果程式仍可順利編譯 連結, 表示 this 指標確實存於程式之中 85
使用 this 指標的時機 在某些狀況下會需要用到 this 指標, 例如當成員函式的參數名稱與資料成員的名稱相同 或是要做整個物件的複製, 請參見以下的例子 : 86
使用 this 指標的時機 87
使用 this 指標的時機 88
使用 this 指標的時機 在第 7 行的 copy 函式中, 將參數物件 source 直接用指定運算子設定給 this 指標所指的物件, 此敘述的功用就是讓兩個物件的資料成員值變得完全一樣, 也就是把 source 物件的資料成員值複製給 this 物件的資料成員 89
使用 this 指標的時機 第 13 行的 Time::set() 成員函式其參數名稱與類別資料成員的名稱相同, 此時成員函式預設只會存取到它自己的局部變數, 也就是函式的參數 因此在函式中, 可用 "this-> 資料成員 " 的語法來存取物件的資料成員, 在第 15~17 行就是透過此方式將各參數的值指定給同名的資料成員 90
傳回物件的成員函式 另一種會應用到 this 指位器的情況, 就是希望類別的成員函式能以有如 cout 和 << 運算子的使用方式一樣, 可以串接在一起呼叫, 例如 : 91
傳回物件的成員函式 因為. 和 -> 運算子的結合順序都是由左到右, 所以 t.set() 會先執行, 執行完後的傳回值需為 Time 類別的物件, 然後再以傳回的物件來呼叫 show() 那麼, 要如何來設計 set() 呢?很簡單, 就是把它的傳回值設為 Time 類別的參考型別, 並在函式本體中傳回 this 指標所指的物件 : 92
傳回物件的成員函式 在執行 t.set(12,10,45).show(); 時, t.set(12,10,45) 會先執行並傳回 t ( 時 t 的值已被更改 ), 然後再以傳回的 t 來執行 t.show(): 請參見以下範例 : 93
傳回物件的成員函式 94
傳回物件的成員函式 95
傳回物件的成員函式 我們一直使用的 cout 及 << 運算子的串接輸出方式, 就是這種傳回物件本身的應用 因為在 C++ 中運算子也算是一種函式, 所以也可以設計成具有這種串接的特性 96
8-4 friend 類別的夥伴 類別是一個封裝良好的機構, 可以將內部私有的資料與外界完全隔離, 但這有時卻反而會造成一些困擾, 尤其是當我們想要在類別以外的 ( 成員或非成員 ) 函式來存取已封裝好的資料時 以字串類別為例, 我們知道在標準函式庫中有一個 strcmp() 函式可以做字串的比較, 如果我們想要另外寫一個 strcmp(str, Str) 的多載函式, 使得二個 Str 物件也可以用 strcmp() 來做比較, 這時除了將字串的成員設為公開成員外是否還有其它方法呢? 97
friend 類別的夥伴 答案就是將自訂的 strcmp( ) 宣告為 Str 類別的夥伴 (Friend) 被宣告為類別夥伴的函式具有存取私有成員的特權, 所以這樣一來就不必將類別的資料成員設為公開成員而破壞類別資料隱藏的特性 要將函式宣告為類別夥伴很簡單, 只需在類別中加上該函式的原型宣告, 並在宣告最前面加上 friend 關鍵字 : 98
friend 類別的夥伴 用關鍵字 friend 宣告以後, 在 strcmp(str&, Str&) 的函式本體中, 便可以直接存取 Str 類別中任何私有的成員 夥伴的宣告可以放在類別定義內的任何位置, 不過我們一般都將它放在最前面, 以方便閱讀 以下就是加入夥伴比較函式的 Str 類別 : 99
friend 類別的夥伴 100
friend 類別的夥伴 101
誰可以做為類別的夥伴 由於宣告為夥伴的對象並非類別內的成員, 所以沒有 private protected 或 public 之分 可以做為夥伴的對象有下列三種 : 1. 一般函式 : 例如前面提到的 strcmp() 函式 且同一個函式可以做為多個類別的夥伴, 例如 : 2. 其他類別內的成員函式 : 但這個類別必須是已定義好的, 例如 : 102
誰可以做為類別的夥伴 103
誰可以做為類別的夥伴 注意, copy() 函式本身的定義必須放在 String 類別的定義之後, 這是因為 copy() 要用到 Str 內的資料成員 所以如果我們想要將 copy() 的定義放在 MemBlock 類別之內的話, 必須用下面第三種方法 3. 其他的類別 : 這必須以 friend class 夥伴類別的名稱 ( 其中 class 可以省略 ) 來宣告 當甲類別被宣告為乙類別的夥伴時, 所有甲類別中的成員函式均可任意存取乙類別內的非共用資料 例如 : 104
誰可以做為類別的夥伴 105
8-5 綜合演練 複數類別 堆疊類別 106
複數類別 複數是在進階的數學應用中經常用到的數值, 但在內建資料型別中並沒有複數這樣的類別, 因此在 C++ 發展初期, 就有不少人都嘗試設計一個複數 (Complex) 類別來使用 不過到後來, C++ 標準組織就決定在標準類別庫中加入複數類別, 讓有需要的程式設計人員省下不少工夫 107
複數類別 在此我們還是利用自訂複數類別來做練習, 讓讀者能熟悉類別的定義及使用 複數的組成可分為實數的實部與虛數的虛部, 所以複數類別至少需有 2 個資料成員來儲存這兩個值, 而複數類別的四則運算也有些特別之處, 以下程式示範一個含加減法成員函式的陽春複數類別 : 108
複數類別 109
複數類別 110
複數類別 這個陽春的類別共有 4 個成員函式, 分別是設定數值的 set() 及顯示數值的 show(), 以及加減法的函式 加減法函式分別依照複數實部加實部 虛部加虛部的計算規則設計, 並傳回物件本身 由於我們還未學到一些實用的類別功能設計方式, 所以這幾個函式的設計方式都不太恰當, 例如加減法函式都改變呼叫物件的值, 當然我們也可修改成建立新複數物件, 並將計算結果設定給新物件, 最後再傳回新物件的方式 111
堆疊類別 初學程式設計者, 免不了會接觸到一些基本的資料結構 (Dat a Structure), 其中堆疊 (stack) 是初學者最常接觸到的 所謂堆疊是指一種後進先出 (LIFO, Last In First Out) 的資料結構, 也就是說, 放入此結構 ( 集合 ) 的物件 ( 變數 ) 要被取出時, 必須等比它後加入的物件全部被拿出來後, 才能將它拿出來 例如一端封閉的網球收納筒, 就是一種堆疊結構 : 112
堆疊類別 113
堆疊類別 關於資料結構的探討, 在此不擬深入, 有興趣者, 可參照其它相關主題的書籍 不過我們可以發現這種簡單的資料結構, 也很適合設計成類別, 不但能以更直覺的方式操作, 也能達到堆疊內資料不被外部任意存取 / 修改的目的 114
堆疊類別 舉一個整數堆疊的例子, 它可用來存放整數的資料, 我們可設計一個整數堆疊類別, 其中可存放一定數量的整數, 然後再設計用來將整數存入堆疊的成員函式 push() 將整數自堆疊取出的成員函式 pop(), 就可以很方便地將整數存入堆疊或由堆疊取出了 請看下面的程式 : 115
堆疊類別 116
堆疊類別 117
堆疊類別 以上是可存放整數資料的堆疊類別, 資料成員 buffer[] 陣列就是用來存放放入堆疊的整數, 而 sp 則記錄目前已存幾筆資料 push() pop() 函式分別模擬將資料存入堆疊 及從堆疊中取出資料的動作, 其內容很簡單, 都是先檢查 sp 是否已達最大值 ( 表示堆疊已滿 ) 或是 0 ( 表示堆疊是空的 ), 不是就進行存入或取出的動作 我們將程式存於.h 檔中, 這樣子別的程式要使用時, 只需含括這個.h 檔即可使用整數堆疊類別, 如以下範例所示 : 118
堆疊類別 119
堆疊類別 120
堆疊類別 在上例的 main() 函式中一共建立了二個堆疊物件, 並分別用它們來存放 3 個整數, 再取出之 程式最後故意要 st2 物件多 pop 一次資料, 結果就會執行到私有成員函式 Error(), 顯示堆疊出錯的訊息 121