C++ 深入

C++ 語言教學課程

這個教學課程的前幾節涵蓋最後兩個單元中已介紹的基本內容,並提供更多進階概念的相關資訊。本單元的重點在於動態記憶體,以及物件和類別的詳細資料。也會介紹一些進階主題,例如繼承、多態性、範本、例外狀況和命名空間。稍後我們會在進階 C++ 課程中詳細探討。

物件導向設計

這是物件導向設計方面的絕佳教學課程。我們會在本單元的專案中,套用這裡提供的方法。

按範例學習 #3

本單元的重點為使用指標、物件導向設計、多維度陣列和類別/物件來取得更多練習。並逐一瀏覽下列範例。我們沒辦法強調成為優秀程式設計師的關鍵,就是練習、練習、練習!

練習 #1:運用指標提供更多練習

如果您需要額外練習使用指標,請參閱這項資源,其中涵蓋指標的所有層面,同時提供許多程式範例。

下列程式的輸出內容為何?請勿執行程式,但必須繪製記憶體相片來判斷輸出內容。

void Unknown(int *p, int num);
void HardToFollow(int *p, int q, int *num);

void Unknown(int *p, int num) {
  int *q;

  q = #
  *p = *q + 2;
  num = 7;
}

void HardToFollow(int *p, int q, int *num) {
  *p = q + *num;
  *num = q;
  num = p;
  p = &q;
  Unknown(num, *p);
}

main() {
  int *q;
  int trouble[3];

  trouble[0] = 1;
  q = &trouble[1];
  *q = 2;
  trouble[2] = 3;

  HardToFollow(q, trouble[0], &trouble[2]);
  Unknown(&trouble[0], *q);

  cout << *q << " " << trouble[0] << " " << trouble[2];
}

手動決定輸出內容之後,請執行程式,查看是否正確無誤。

練習 #2:進一步練習類別和物件

如需其他類別和物件練習,請參考這裡的資源,瞭解如何實作兩個小型類別。花點時間運動

練習 #3:多維度陣列

不妨試試下列計畫:

const int kStudents = 25;
const int kProblemSets = 10;

// This function returns the highest grade in the Problem Set array.
int get_high_grade(int *a, int cols, int row, int col) {
  int i, j;
  int highgrade = *a;

  for (i = 0; i < row; i++)
    for (j = 0; j < col; j++)
      if (*(a + i * cols + j) > highgrade)  // How does this line work?
        highgrade = *(a + i*cols + j);
  return highgrade;
}

int main() {
 int grades[kStudents][kProblemSets] = {
   {75, 70, 85, 72, 84},
   {85, 92, 93, 96, 86},
   {95, 90, 83, 76, 97},
   {65, 62, 73, 84, 73}
 };
 int std_num = 4;
 int ps_num = 5;
 int highest;

 highest = get_high_grade((int *)grades, kProblemSets, std_num, ps_num);
 cout << "The highest problem set score in the class is " << highest << endl;

 return 0;
}

這個程式中有一行標示「這行程式碼如何運作?」的一行。 - 你能理解嗎? 請參閱這裡的說明

編寫初始化 3Dim 陣列的程式,並在第 3 個維度值中填入三個索引的總和。請參閱這裡的解決方案。

運動 #4:豐富的 OO 設計範例

查看詳細的物件導向設計範例,必須全程完成整個程序。最終程式碼是以 Java 程式設計語言編寫,但您仍可以已閱讀的段落來閱讀完整程式碼。

請花點時間看完整個範例。此圖片有助於說明整個程序,以及支援這項功能的設計工具。

Unit Testing (單元測試)

簡介

測試是軟體工程流程中相當重要的一環。單元測試是一種特定的測試類型,可檢查單一原始碼的小型模組。單元測試一律由工程師完成,且通常會在編寫模組時完成。用來測試 Composer 和資料庫類別的測試驅動程式為單元測試的範例。

單元測試具有以下特性。這些...

  • 獨立測試元件
  • 具確定性
  • 通常會對應至單一類別
  • 避免仰賴外部資源,例如資料庫、檔案、網路
  • 快速執行
  • 可以依任何順序執行

您可以運用自動化架構和方法,為大型軟體工程機構的單元測試提供支援和一致性。 有些精密的開放原始碼單元測試架構,稍後將在本課程中介紹。

以下為單元測試中的測試內容。

在理想情況下,我們會測試下列項目:

  1. 模組介面會經過測試,確保資訊正確進出。
  2. 本機資料結構會經過檢查,確保資料儲存無誤。
  3. 系統會測試邊界條件,確保模組在限製或限制處理作業的邊界能正確運作。
  4. 我們會測試模組中的獨立路徑,確保每個路徑都至少執行一次。 
  5. 最後,我們必須檢查是否正確處理錯誤。

程式碼涵蓋率

實際上,我們無法在測試中獲得完整的「程式碼涵蓋範圍」。程式碼涵蓋率是一種分析方法,可以判斷測試案例套件已經執行 (涵蓋) 軟體系統的哪些部分,以及尚未執行哪些部分。如果嘗試並得到 100% 的涵蓋率,我們會花更多時間編寫單元測試,而不是編寫實際程式碼!請考慮針對下列項目的所有獨立路徑進行單元測試。這可能會成為指數問題。

此圖中的紅色線條未經測試,而未彩色線條則測試。

與其嘗試 100% 的涵蓋率,而是著重於測試,以提升模組是否正常運作。我們會測試下列項目:

  • 空值案件
  • 範圍測試,例如陽性/負值的測試
  • 極端案例
  • 失敗案例
  • 測試多數時間最有可能執行的路徑

單元測試架構

大部分的單元測試架構會在路徑執行期間使用斷言來測試值。「斷言」是檢查條件是否為 True 的陳述式。斷言的結果可能是成功、不嚴重失敗或嚴重失敗。斷言執行完成後,只要結果是成功或非嚴重失敗,程式仍會繼續運作。如果發生嚴重錯誤,系統會取消目前的函式。

測試包含設定狀態或操控模組的程式碼,以及多項可驗證預期結果的斷言。如果測試中的所有斷言都成功,也就是傳回 true,測試就會成功,否則測試會失敗。

測試案例包含一或多個測試。我們會將測試分成反映測試程式碼結構的測試案例。在本課程中,我們將使用 CPPUnit 做為單元測試架構。透過這個架構,我們可以以 C++ 撰寫單元測試,並自動執行這些測試,並提供測試成功或失敗的報告。

CPP 單元安裝

SourceForge 下載 CPPUnit 程式碼。找出合適的目錄,並將 tar.gz 檔案放置於該處。接著,輸入以下指令 (在 Linux 或 Unix 中),但替換適當的 cppunit 檔案名稱:

gunzip filename.tar.gz
tar -xvf filename.tar

如果您使用的是 Windows,可能需要尋找用來擷取 tar.gz 檔案的公用程式。下一步是編譯程式庫。變更為 cppunit 目錄。 其中包含提供特定指示的 INSTALL 檔案。通常您需要執行:

./configure
make install

如果遇到問題,請參閱 INSTALL 檔案。程式庫通常位於 cppunit/src/cppunit 目錄中。如要確認編譯是否正常運作,請前往 cppunit/examples/simple 目錄,然後輸入「make」。如果一切都編譯好,就表示一切就緒。

這裡提供一個絕佳教學課程。請按照這個教學課程的說明,建立複數類別及其相關的單元測試。cppunit/examples 目錄中還有其他範例。

為什麼要這麼做?

單元測試對於業界至關重要,原因有幾個。您已經熟悉其中一個原因:我們需要在開發程式碼時檢查工作內容。即使我們正在開發規模相當小的程式,也會刻意寫出某種檢查工具或驅動程式,確保程式符合預期。

從長遠的經驗來看,工程師都知道,第一次嘗試程式的成效非常小。單元測試是以這個概念為基礎,讓測試計畫自行檢查和可重複執行。斷言會取代手動檢查輸出內容。此外,由於結果很容易解讀 (測試通過或失敗),因此測試可不斷重複執行,確保程式碼能在變更方面更具彈性。

讓我們具體說明一下:首次將完成的程式碼提交至 CVS 時,其運作方式會一切完美。而且持續完美地運作一陣子。之後,有一天,有人變更您的程式碼。不久之後或之後有人會破壞程式碼。請問他們會自行注意到嗎?不太可能。不過,當您編寫單元測試時,仍有可每天自動執行單元測試的系統。稱為「持續整合」系統。如此一來,當工程師 X 中斷您的程式碼時,系統就會寄送電子郵件給他們,直到他們修正為止。就算 X 工程師就是您,也沒問題!

單元測試除了可協助您開發軟體,還可在變動期間確保軟體安全無虞,這些單元包括:

  • 建立可執行檔的規格,以及與程式碼保持同步的說明文件。也就是說,您可以讀取單元測試,瞭解模組支援哪些行為。
  • 有助於區隔需求與導入作業。由於您宣告了外部可見行為,因此有機會明確思考,而非混雜如何實作該行為。
  • 支援實驗。如果您設有安全網路,可在模組行為中斷時收到快訊,您就較有可能嘗試測試及重新設定設計。
  • 改善設計。編寫完整的單元測試通常需要讓您更容易測試程式碼。與無法測試的程式碼相比,可測試的程式碼通常較模組化。
  • 維持高品質。關鍵系統中的小錯誤可能導致公司損失數百萬美元,甚至更糟的是使用者的滿意度或信任度。單元測試的安全網可降低這種情況。透過及早找出錯誤,品質確保團隊也能將時間花在更複雜且困難的故障情境上,而非回報明顯的失敗情形。

請花點時間為 Composer 資料庫應用程式使用 CPPUnit 編寫單元測試。相關說明請見 cppunit/examples/ 目錄。

《Google 模式》

簡介

假設有一位中世紀僧侶在他的修道院中看了數千個手稿,「Aristotle 這個...」

Monastary Library

幸好,指令碼是按照內容分類,並使用特殊符號來擷取,以便擷取各項資訊。如果沒有這種組織,就很難找到相關手稿。

儲存及擷取大型集合寫入資訊的活動稱為資訊擷取 (IR)。過去幾世紀以來,這項活動更加重要,尤其是紙張和印刷媒體等發明。過去只有少數幾個人如今,每天使用搜尋引擎或搜尋桌面時,會有數億人會進行資訊檢索。

開始使用資訊擷取

戴帽子的貓

蘇斯博士在 30 年內寫了 46 本童書。他向他介紹的貓、牛和大象,包括誰、露齒和氯。你還記得哪個生物是哪些生物?除非是家長,否則只有孩子才能知道哪些故事含有各種生物:

(COW 和 BEE) 或 CROWS

我們會套用一些傳統的資訊擷取模型,協助解決這個問題。

顯而易見的做法就是暴力:收集 46 Dr. Seuss 博士的故事,然後開始閱讀。查看每本書,哪一本書包含 COW 和 BEE 字詞,並同時尋找包含 CROWS 字的書籍。如今電腦執行速度遠勝過我們的工作。如果蘇斯博士的所有書籍都以數位形式呈現 (如為文字檔案),我們可以直接透過檔案進行回覆。但對於蘇斯醫師等小收藏品來說,這項技巧十分實用。

但是在許多情況下,我們需要更多。舉例來說,目前線上所有資料的收集作業過於龐大,G 代表無法妥善處理。我們也不想只希望文件符合自身條件,並已習慣根據關聯性排名。

除了 grep 以外,還有一種方法是在搜尋前先在集合中為文件建立索引。IR 中的索引與教科書背面的索引類似。我們會列出每個蘇斯博士故事中的所有字詞 (或字詞),並抽出「the」、「and」和其他連接點、介詞等 (統稱為「禁用字詞」)。然後以方便尋找條款及識別相關報導的方式呈現這項資訊。

其中一種可能的呈現方式是矩陣,其最上方的故事,以及每列列出的字詞。資料欄中的「1」表示字詞出現在該欄的故事中。

書籍和字詞的表格

我們可以用位元向量檢視每個資料列或資料欄。資料列的位元向量會指出該字詞出現在哪些報導中。欄的位元向量會指出故事中出現的字詞。

回到原始問題:

(COW 和 BEE) 或 CROWS

我們會擷取這些字詞的位元向量,先執行位元向量 AND,然後對結果進行位元的 OR 運算。

(100001 和 010011) 或 000010 = 000011

答案是:「Brown Can Moo 先生!你能嗎?」和「Lorax」。這張插圖是布林值擷取模式的「完全比對」模式。

假設我們要展開矩陣,在故事中納入所有蘇斯博士的故事和所有相關字詞,此矩陣會大幅擴張,而重要的觀測結果大多是 0。矩陣可能不是索引的最佳表示法。我們需要找出可以只保存 1 數量的方法,

部分強化項目

紅外線 (IR) 用來解決這個問題的結構稱為「反向索引」。 我們保留了字詞字典,每個字詞都會有一份清單,當中記錄出現該字詞的文件。這份清單稱為「發布清單」。單獨連結的清單很適合用來代表這個結構,如下所示。

如果您不熟悉連結的清單,只要針對「C++ 中的連結清單」進行 Google 搜尋,即可找到許多資源,說明建立方式和使用方式。我們會在後續單元中進一步說明。

請注意,我們會使用文件 ID (DocIDs) 而不是故事名稱。我們也會將這些 DocID 排序,以便處理查詢。

我們如何處理查詢?針對原始問題,我們先找出 COW 張貼清單,再找出 BEE 張貼清單。然後,我們會把它們「合併」在一起:

  1. 將標記保留在兩份清單內,然後同時完成兩份張貼清單。
  2. 在每個步驟中,比較兩個指標指向的 DocID。
  3. 如果兩者相同,請將該 DocID 放在結果清單中,否則將指標推指向較小的 docID。

以下說明如何建構反轉的索引:

  1. 為每份關注的文件指派 DocID。
  2. 為每份文件找出相關字詞 (權杖化)。
  3. 針對每個字詞建立記錄,其中應包含字詞、找到該字詞的 DocID,以及文件中的頻率。請注意,如果特定字詞出現在多份文件中,則可能會有多個記錄。
  4. 按照字詞排序記錄。
  5. 處理字詞的單一記錄,以及合併出現在多份文件中的字詞多筆記錄,藉此建立字典和貼文清單。建立 DocID 的連結清單 (依排序順序)。每個字詞也都有頻率,即某個字詞所有記錄的頻率總和。

專案

找出多份長文的明文文件,和你大力嘗試運用。專案是使用上述演算法,從文件中建立反向索引。此外,您也需要建立查詢輸入內容的介面,以及處理查詢的引擎。您可以在論壇中找到專案合作夥伴。

以下是完成這項專案的可能程序:

  1. 首先,您需要定義文件中識別字詞的策略。列出您想想到的所有停用字詞,並編寫函式,讀取檔案中的文字、儲存字詞並刪除停用字詞。查看疊代字詞清單時,您可能需要新增更多停用字詞。
  2. 編寫 CPPUnit 測試案例來測試函式,再編寫 makefile 來整合建構作業的所有項目。請將檔案檢查成 CVS,尤其是要與合作夥伴合作時。建議您研究如何向遠端工程師開放 CVS 執行個體。
  3. 新增處理程序以納入位置資料;是指在檔案中,哪個檔案和字詞位置?您可以算出一段計算來定義頁碼或段落編號。
  4. 編寫 CPPUnit 測試案例以測試這項額外功能。
  5. 建立反轉的索引,並將位置資料儲存在每個字詞的記錄中。
  6. 編寫更多測試案例。
  7. 設計可讓使用者輸入查詢的介面。
  8. 使用上述搜尋演算法處理反轉的索引,並將位置資料傳回給使用者。
  9. 請務必一併加入最後一個部分的測試案例。

所有專案都已完成,請透過論壇和即時通訊尋找專案合作夥伴,交流想法。

額外功能

許多 IR 系統的常見處理步驟稱為「詞幹」。這個詞源背後的主要概念是,如果使用者搜尋「擷取」資訊,就會對含有「擷取」、「擷取」和「擷取」等資訊的文件感興趣。系統可能會因繁重因素而容易發生錯誤,因此這有點困難。舉例來說,對「資訊擷取」有興趣的使用者可能會因為這麼多,因而得到標題為「黃金獵犬的資訊」的文件。Porter 演算法是實用的演算法進行擷取。

應用程式:隨時隨地!

請參閱 Panoramas.dk 瞭解這些概念的應用方式。