Post view

C++ 物件導向程式設計:3. 資料結構和類別

物件導向程式設計 蘇言霖

3. 資料結構和類別

在正式介紹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++ 為您做了一點改變:宣告structunionenum 時,不再需要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 createlistsort 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 類別指令來宣告物件。雖然structunion 也可以宣告物件。因此請改用底下的方式來宣告 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 的目標還很遠。其實在物件導向領域,要設計一個好的物件比定義一個資料結構,所面臨的問題會更複雜艱深。

通常一個物件除了基本的裝封、介面還必須擁有誕生(建立)和消失(釋放)的生命周期,以及物件的行為(功能)。沒有一個物件是不會或不需要消失的,只要結束程式執行!

或許您無法了解究竟什麼是物件?其實常用的intcharfloat等就是物件!當您定義一個 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++ 的基本類別有intcharlongfloat等。

· 案例(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 等級的ab 變數一般。由於解構函式是自動執行的,因此不能有傳回值。解構函式名稱同建構函式,但前面加上 ~。由於這個程式不需要使用動態記憶體配置來建立物件,因此我們設計了空的建構和解構函式。

為了可以和一般型別混合使用,我們為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 可以換成您熟悉的 longfloat等,比如 long x,y=5; ,只是使用 long 型別C++ 會自動處理好相關的細節,您完全不必知道C++ 是怎麼辦到的。這就是所謂抽象資料型別。

現在我們自定的myint ,希望像內建型別一般使用,以往由C++ 自動處理的問題,變成您必須告訴(教)C++ 要如何處理myint ,如此而已。而目前myint 所做的也不過是內建型別的基本工作罷了,以前您不了解內建型別的行為,現在拿出來討論後,自然會覺得有點陌生。

蘇言霖 2013/09/10 0 3380
Comments
Order by: 
Per page:
 
  • There are no comments yet
Rate
0 votes
Post info
蘇言霖
「超級懶貓級」社群網站站長
2013/09/10 (2083 days ago)
Actions