在正式介紹C++ 的類別之前,讓我們先談談C 和 C++ 對於結構的看法。
如果我們希望為一本書定義一個新的資料型別,在 C 語言可以寫:
typedef struct book
{
char writer[20]; // 作者
char name[100]; // 書名
unsigned int price; // 售價
} book; // book 是新的資料結構型別
book c1, c2, c3; // 定義 3 本書
其實,在宣告資料結構的目的,除了希望把不同的資料結合在一起,更重要的是希望像標準型別一般使用。比如結構變數、陣列、指標、參數、傳回值等。所以 struct 之前常用 typedef 前置詞。因為這個原故,C++ 為您做了一點改變:宣告struct、union、enum 時,不再需要typedef 前置詞,預設新的資料結構名稱同時就是新的資料型別名稱!所以在 C++ 只要寫:
struct book
{
char writer[20]; // 作者
char name[100]; // 書名
unsigned int price; // 售價
};
book c1, c2, c3; // 定義 3 本書
或許您認為可省略typedef 並不是很重要的事,在物件導向卻是很重要的一個特性。只是這裡的重點並非 typedef。如果新的資料結構型別也可以參與數學運算、邏輯判斷式等則更好,但是C 語言不允許這麼做(= 設定例外)。比如: if (c1 == c2)。您必須寫許多的操作函式來處理新的資料型別,即使是 +-*/ 。我們把這個問題留待以後介紹 C++ 的 overloading operator 時再討論。
通常在宣告 book 結構之後,就會幫 book寫幾個操作函式,如資料輸入、顯示、印表、排序、索引、開檔、存檔等。或許您會認為這種設計方式是天經地義的事,但問題就是出在這裡,而且隨隨便便就可以列舉出一大堆問題:
· book 不屬於任何函式,所有函式都可使用book,包括完全不相干的函式。
· 為 book 所設計的函式只能操作 book,不能操作其他結構。譬如現在 Photo CD相當流行,如果要寫一個相片管理程式,您就必須重寫一個新程式,即使新程式還是相片輸出、顯示印表排序等。
· 如果 book 要新增或修訂,比如新增條碼功能。您就必須忙於改程式和抓蟲。
以前,資料結構在實際的軟體發展過程中,可說是相當重要的一環!好的資料結構有助於程式的設計、發展、維護、變更、加強功能等。簡單的說,資料結構的好壞常會影響軟體品質!如果您曾經寫過某些程式或軟體,相信能夠體會資料結構的重要性。所以「資料結構」很自然的成為程式設計必修的一門課。
後來發現,好的資料結構加上好的演算法,不見得可以得到一個好的程式,即使能寫出一個好程式,只要談到程式的變更、重組、再利用等依舊問題重重。
雖然C++ 擁有C 全部的變數等級,甚至更強(可以存取同名的全域變數),但仍然不夠。我們的目的是希望設計出與應用軟體無關的程式,因此程式的每一個部份都應該儘可能的彼此獨立。適當的資訊隱藏,就成為物件導向的一個重要觀念,我們稱為 encapsulated 「裝封」。簡單的說,就是把資料和方法(操作函式)結合在一起,並隱藏了不需要或沒有必要公開的重要資料。
以前面 book 的例子來說,當我們把資料和函式綁在一起之後, book 將不再是任何函式均可使用的結構變數,而是一個類別。也可以說是一個新的資料型別。隱藏不等於完全看不到,我們必須為物件定義出適當而正確的操作方式,供其他物件或程式使用。我們把物件對外溝通的管道稱為「介面」。
只要您裝入物件是正確可用的程式,介面功能齊全並且有變更的彈性(比如可升級 CD 或可升級電腦,就一定比無法改變吸引人),這就完成了物件裝封的第一步。若萬一不小心裝了錯誤的程式?別擔心,C++ 的裝封機制可以使問題的漣漪效應減到最低。
假設 book 有create、list、sort、 close 等操作函式,我們可以用裝封的觀念設計新的 book 結構,例如:
struct book
{
char writer[20]; // 作者
char name[100]; // 書名
unsigned int price; // 售價
int create(); // 建立函式
int list(); // 顯示函式
int sort(); // 排序函式
int close(); // 關閉函式
};
book c1, c2, c3; // 定義 3 本書
看,一點也不難吧。不過在C++ 裡我們習慣使用新的class 類別指令來宣告物件。雖然struct、union 也可以宣告物件。因此請改用底下的方式來宣告 book:
class book
{
char writer[20]; // 作者
char name[100]; // 書名
unsigned int price; // 售價
int create(); // 建立函式
int list(); // 顯示函式
int sort(); // 排序函式
int close(); // 關閉函式
};
book c1, c2, c3; // 定義 3 本書
class 是標準的物件宣告指令,只是 C++ 同時擴充了 struct 的語法,允許在 struct 宣告 book 成員函式。其實class 和 struct 還是有點差異,最大不同是預設的存取等級。class 預設是私有等級,而 struct 預設是公用等級。
C++ 有三種不同的存取等級或使用等級:
· public :公用等級。資訊完全公開,歡迎任何物件或函式來使用,甚至可以透過繼承機制繼承下去。
· private:私有等級。資訊完全隱藏,這是本物件專用的資料或函式,不提供給其他物件使用,也無法繼承給子孫,但在同一個物件則沒有影響。
· protected:保護等級。基本性質同私有等級,但可以過繼給子孫享用。
所謂物件的介面就是指 public 和子孫專用的 protected 而言。以前面 book 類別來說,因為沒有指定存取等級,所以所有的資料和函式都設定為私有等級,換句話說 book 沒有提供物件的操作介面!
我們很少設計沒有介面的物件,因為一旦沒有介面,物件裡所有的變數和函式都無法使用供他人使用,也不能繼承,這個物件就幾乎沒有實質的意義。就像C 語言可以宣告 const int a; 一個沒有值的常數,常數又不能改變內容(正常管道的話)那麼這種常數就沒有用處了。
因此 book 類別比較合理的設計是:
class book
{
private:
char writer[20]; // 作者
char name[100]; // 書名
unsigned int price; // 售價
public:
int create(); // 建立函式
int list(); // 顯示函式
int sort(); // 排序函式
int close(); // 關閉函式
};
C++ 並沒有規定存取等級的先後順序或使用次數,您可以依個人的喜愛的風格寫程式,但一般習慣把私有等級寫在類別的前面,之後才是保護和公用等級。然而一個真正好用的 book 物件自然不是只有這些東東,這只能算是最簡單的基本元件,離軟體 IC 的目標還很遠。其實在物件導向領域,要設計一個好的物件比定義一個資料結構,所面臨的問題會更複雜艱深。
通常一個物件除了基本的裝封、介面還必須擁有誕生(建立)和消失(釋放)的生命周期,以及物件的行為(功能)。沒有一個物件是不會或不需要消失的,只要結束程式執行!
或許您無法了解究竟什麼是物件?其實常用的int、char、float等就是物件!當您定義一個 int a; 之後,C++ 就會配置一個整數空間,該空間的辨識名稱就叫做 a (稱為案例 instance)。
定義好變數之後您就可以用物件的介面 +-*/%=、==、!= 等來操作案例,比如 a++; 或 a = b + c; 或 if (a == 0)。如果a 是一個自動等級的變數,當函式返回時就會自動釋放所有自動等級的物件(變數 a)。因此 int a; 就是一個標準的物件,只是在物件導向對於同一件事有不同的看法。以 int a,b; 來說,包含了類別和案例兩個名詞:
· 類別(class): int 就是類別,也就是物件a 和 b 的 int 類別。C++ 的基本類別有int、char、long、float等。
· 案例(instance): a 和 b 就是案例。我們稱為a 和 b 是 int 類別的案例。也可以說 a 和 b 是 int 物件。
如此解釋應該很清楚了。只是不要太走火入魔,在C++ 裡我們仍習慣的把 int a; 稱為 a 是整數變數。而使用者自定類別才使用物件導向名詞來稱呼,比如前面的 book c1; 稱為 c1 是 book 類別的案例,而不是說 c1 是 book 型別的變數。
從另一個角度來看, book 類別同時是一個新的資料型別, C++ 把 class 視為一種特殊的資料型別。因此定義案例(即物件),就像定義一般變數。甚至可以把新定義的class 像基本型別一樣的使用。譬如C 和C++ 一直沒有提供字串型別,我們可以自己設計一個新的 string 類別,再加上覆載運算子的功能,就可以產生一個新的 string 資料型別,而且在使用上就好像C++ 的基本型別(或稱為內建型別)一般無二。更好的是C++ 溶合了 C,您還可以把 string 拿回到C 使用。
底下我們以一個非常簡單的例子,說明要如何設計一個整數類別的物件。我們把新的類別叫做 myint:
// myint.cpp
#include <iostream.h>
class myint //myint 類別
{
private: //私有資料成員
int integer;
public: //公用的物件介面
myint() { } //建構函式 1
myint(int x) //建構函式 2
{
integer = x * 10;
}
~myint() { } //解構函式
operator int() //型別轉換函式
{
return integer;
}
};
void main()
{
myint x, y = 5; //x、y 物件
int a, b = 2; //a、b 變數
a = y + b; //myint 可參與運算
x = ++b + y; //也可以設定內容
// 底下印出 a、b、x、y 的結果
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "x = " << x << endl;
cout << "y = " << y << endl;
}
執行結果
a = 52
b = 3
x = 530
y = 50
我們設計的myint 用法幾乎和內建的 int 相同,但是存入的數值會幫您生利息,乘上 10 。
建構函式(constructor) 是專門負責建立物件,C++ 會在建立物件時自動執行建構函式,如果您沒有指定初值則會呼叫沒有參數的建構函式,如果有指定初值,如 y=5; 則叫用有int 型別的建構函式。建構函式的名稱必須同類別名稱。
由於建構函式是自動執行的,因此函式傳回值就沒有意義,不能寫函式的傳回值,即使宣告 void 也不對,保持空白就對了。當程式執行myint x 時,因 x 沒有指定初值,所以會執行myint() 建構函式。而 myint y=5; 則會執行 myint(int x) 函式,並且把 5 傳給 x ,乘 10 之後設定給私有變數 integer。
myint y=5; 的可讀性較高,如果覺得有點不可思議,也可以寫 myint y(5); ,但還是前面的式子比較優美易懂。
有參數的建構函式可同時做「自動型別」轉換的功能,C++ 會在需要時自動找尋適當的轉換函式做正確的型別轉換。建構函式轉換的方向是,由內建型別轉換為使用者自定型別(即類別)。
解構函式(destructor) 是用來刪除物件,當物件不再使用時,C++ 會自動執行解構函式來刪除物件。換句話說,物件的建立和刪除可做到「全自動」,就像 auto 等級的a、b 變數一般。由於解構函式是自動執行的,因此不能有傳回值。解構函式名稱同建構函式,但前面加上 ~。由於這個程式不需要使用動態記憶體配置來建立物件,因此我們設計了空的建構和解構函式。
為了可以和一般型別混合使用,我們為myint 設計了一個型別轉換函式,由使用者自定型別(類別)轉換為內建型別。這就是 operator int 函式。cout 是 C++ 的標準輸出入物件,就像printf 一般,只是在 C++ 裡,我們很少用 printf,因為printf 在設計上可傳入任何型別的參數,如果想利用 printf 列印物件,就會發生問題。
用 cout 只要把希望顯示的資料用新定義的 << 運算子 (<< 是位移運算子)傳給 cout 物件即可,不必擔心型別的問題C++ 會自動判斷,如果傳給 cout 是不認識的類別,C++ 會自動做型別轉換,或尋找 cout 的夥伴函式。
我們這裡提供了型別轉換函式,因此 C++ 會自動把 myint 換成 int 後再執行 cout 印出正確的內容,用 Turbo C++ 的 F7 追蹤功能,追幾次程式即知。如果C++ 無法轉換也找不到夥伴函式,編譯程式就會提出適當的錯誤訊息。
初學者往往對新的名詞以及myint 的行為難以理解,其實myint 可以換成您熟悉的 long、float等,比如 long x,y=5; ,只是使用 long 型別C++ 會自動處理好相關的細節,您完全不必知道C++ 是怎麼辦到的。這就是所謂抽象資料型別。
現在我們自定的myint ,希望像內建型別一般使用,以往由C++ 自動處理的問題,變成您必須告訴(教)C++ 要如何處理myint ,如此而已。而目前myint 所做的也不過是內建型別的基本工作罷了,以前您不了解內建型別的行為,現在拿出來討論後,自然會覺得有點陌生。