文章程式碼顯示

2018年3月14日 星期三

《筆記》談談C++語言 - 6:類別的獨立、介面與實作分開

類別的獨立

將類別獨立出來的好處在於若套件設定得當,程式設計者變可以重覆使用我們的類別。但想使用 GradeBook 類別的使用者,並無法在別的程式中直接 include 前一章節我們寫的程式碼,其原因在於一個完整的程式裡面並不能存在兩個 main 函式。所以我們需要將他獨立出來

以前我們的程式碼都是在 *.cpp 裡面完成,此檔也叫做原始碼檔案,它包含 GradeBook 類別定義以及一個 main 函式。建構物件導向 C++ 程式時,通常會將可再利用的原始碼(如GradeBook 類)定義在負檔名為 *.h 的標頭檔文件中(header file)。接著用 #include 前置處理器以含入標頭檔,借此運用可再利用的程式碼。

我們新增一個 GeadeBook.h 裡面內含以下

#include "stdio.h"
#include "stdlib.h"

class GradeBook {

public:

 //constructor
 GradeBook(char* str){
  setCourseName(str);
 }

  void setCourseName ( char* name){
   courseName = name;
  }

  char* getCourseName (){
   return courseName;
  }

  void displayMessage(){
   printf("Welcome to [%s] GradeBook", getCourseName() );
  }

private:
 char* courseName;

};


接著在主要的 .cpp 檔案裡面如下

#include "stdio.h"
#include "stdlib.h"
#include "GradeBook.h"

int main() {

 GradeBook myGradeBook("GradeBook TEST");
 myGradeBook.displayMessage();

 return 0;
}




註:

現在 GeadeBook 類別已經被定義在 GeadeBook.h 標頭檔中,可被簡單的再利用了。不幸的是將類別定義放在如上的 GeadeBook.h 中,仍然會把類別的所有「實作」暴露給用戶端。因為 GeadeBook.h 只是一個文字檔,任何人都可以輕易的讀取內部文字內容。

但類別物件有個很重要的概念,就是「用戶端」只要知道該呼叫哪些成員函式、該餵哪些引數給成員函式以及每個成員函式的回傳型別就可以了。用戶端不須知道這些函式的實作方式。

若用戶端知道類別的實作方式,該使用者就有可能會依照類別實作的細節來寫程式。理想上,若類別的實作細節有所變更,用戶端不應跟著改變。

將類別實作隱藏起來,未來較易修改類別的實作方式,也可將用戶端的變動幅度降到最低。

所以我們還需要更進一步將「介面與實作分開」

介面與實作分開

前一節是把類別定義(*.h)與使用該類別的用戶端程式碼(*.cpp)分開來,現在則是要進一步把「介面與實作分開」(separating interface from implementation)

類別的介面

介面(interface)定義一種標準化方式,讓事物(如人類)與系統之間進行互動。例如,收音機控制器就是使用者與內部元件之間的介面,控制器會與收音機外殼上的按鈕連接起來(藉由電線 ... 等等),使用者只要旋轉旋鈕或是按下按鍵就可以跟收音機內的控制器進行溝通,並且並不會知道控制器內部的實作方式是什麼。

同樣的,類別的介面說明類別提供了「什麼」服務給用戶端,以及如何使用這些服務,但並不會說明「如何」實作這些服務的細節。

類別的 public 介面由類別的 public 成員函式組成,也叫做類別的 public 服務(public services)。例如 GeadeBook 的介面有一個建構子以及 serCourseName、getCourseName以及displayMessage 成員函式。

GeadeBook 的用戶端(也就是上一節中的 *.cpp) 使用這些函式以要求類別的服務。

為了達到分解開來的目的,我們將成員函式的定義與類別的定義格開來,如此一來成員函式的實作便能被隱藏起來不讓用戶端看到。如此可確保你所寫的用戶端程式碼(*.cpp)不會依附於類別實作細節上。

總共分解為三個部分

標頭檔 GeadeBook.h  : 此為 GeadeBook 類別的定義

#include "stdio.h"
#include "stdlib.h"

class GradeBook {

public:
 GradeBook( char* str );
 void setCourseName ( char* name );
 char* getCourseName ();
 void displayMessage();

private:
 char* courseName;

};


此又是另一個版本的 GeadeBook 類別定義,此版本跟上一節的很像,但只有留下函式原型(function prototype) ,它描述了類別的 public 介面,但沒有暴露成員函式的實作細節。

使用者可以藉由這個檔案得知 GeadeBook 類有哪些成員函式(服務),並且知道每個函式的型態是什麼。


原始檔 GeadeBook.cpp  : 此為 GeadeBook 成員函式的標頭(原文書在這邊說的是成員函式的定義,但我認為用標頭或是實作內容會更貼切一些)

#include "stdio.h"
#include "stdlib.h"
#include "GradeBook.h"

//constructor
GradeBook::GradeBook(char* str){
 setCourseName(str);
}

void GradeBook::setCourseName ( char* name){
 courseName = name;
}

char* GradeBook::getCourseName (){
 return courseName;
}

void GradeBook::displayMessage(){
 printf("Welcome to [%s] GradeBook", getCourseName() );
}


習慣上,定義成員函式的原始碼(*.cpp) 檔名跟標頭檔(*.h) 的檔名相同(如GeadeBook),但檔名為 .cpp

在這個文件裡面存放的是 GeadeBook 類別裡面的成員函式實作細節

有幾個重點要注意

1. 需要 include 標頭檔進來

2. 直接將建構子以及成員函式放上來,並且在「函式名稱前面加上類的名稱(GradeBook) 及 "雙冒號" 」

每個函式標頭的成員函式名稱前面都有類別名稱及雙冒號,這叫做「二元使用域解析運算子(binary scope resolution operator)」它們會把每個成員函式「綁」到宣告成員函式與資料成員的 GeadeBook  類別定義(GeadeBook.h) 上面。如果沒有在函式名稱前面加上這個,編譯器就不知道這些函式是 GeadeBook  類別的成員函式,而會把它當成一般的函式(例如 main 這種)。

如此一來,這種一般的函式因為沒有指定一個物件就不能存取 GeadeBook  的 private 資料,或呼叫類別內的成員函式。所以編譯器會出現錯誤

主原始檔 main.cpp

#include "stdio.h"
#include "stdlib.h"
#include "GradeBook.h"

int main() {

 GradeBook myGradeBook("myGradeBook Class lesson");
 myGradeBook.displayMessage();

 return 0;
}



如此一來我們就將介面跟實作分開來了。

什麼? 你說使用者還是看的到 GeadeBook.cpp 的內容啊

實際上在真正應用的過程中我們會把 GeadeBook.h 的內容原封不動給使用者(加些註解來說明這些成員函式該如何正確應用)。而 GeadeBook.cpp 部分則編譯成機器語言指令交給使用者。

↓↓↓ 連結到部落格方針與索引 ↓↓↓

Blog 使用方針與索引