C++ 入門指南 V2.00 - 單元 21 - 前置處理




C++ 程式在進行編譯時,會先檢查所有的前置處理器 (preprocessor) 指令,這些指令在實際編譯前會預先處理,例如引進標頭檔 (header file) 、定義巨集 (macro) 或常數 (constant) 等



前置處理器指令是以井字號開頭的,像是引進標準程式庫 (standard library) 的標頭檔


    #include <cstdlib>
    #include <ctime>

或是引進自己定義的標頭檔


    #include "encrypt.h"

#include 就是前置處理器指令。

定義巨集則是使用 #define 指令,基本上巨集就是簡單的文字替換,像是一行的程式碼就會直接替換到程式碼內,例如我們之前定義過 PrintInt() 函數 (function)


022 // 印出參數
023 void PrintInt(int a) {
024    cout << a << endl;
025 }

完整的 PrintInt() 定義,請參考單元 9 - 函數

PrintInt() 只有一行、簡單的程式碼,就可以改成巨集,例如


    // 定義巨集
    #define PrintInt(a) cout << a << endl

為什麼要定義巨集呢?因為編譯器 (compiler) 在實際編譯成執行檔之前,會先進行前置處理 (preprocess) ,這個處理就是直接把程式中有 PrintInt(a) 換成 cout << a << endl ,好處就是少掉額外進行的呼叫函數 (function) 。


呼叫函數的缺點在我們目前的小程式看不出來,倒是可以簡單說,呼叫函數就是要挪出處理器的時間,把函數的程式放到記憶體空間,然後才去執行,整個過程可能只需要幾微秒而已,可是當呼叫的次數越多,累積消耗的時間、空間也就越多囉!


節省時間、空間,就可以讓程式跑得更順,不是嗎?


我們也可以用 #define 指令定義巨集常數,像是 Encrypt 類別中反覆出現的 9726 等數字都應該要定義成常數,最主要的原因不只是這些都是固定的數字,更重要的,假如有一天我們需要對不同的語言文字編碼,雖然很可能只是修改一下 Encrypt 類別,但要把每個 9726 通通找出來修改會是件很麻煩的事情,只在一個地方修改的話,相對就更容易維護程式碼。


常數通常用大寫字母定義在標頭檔裡,我們將 97 定義成 DIFF ,而 26 定義成 NUM ,完整的 encrypt.h 如下


001 // 從標準程式庫中引入 string
002 #include <string>
003
004 // 定義兩個巨集常數
005 #define DIFF 97
006 #define NUM 26
007
008 // 使用 std 中的 string 名稱
009 using std::string;
010
011 // 宣告 Encrypt 類別
012 class Encrypt {
013    public:
014       // 宣告建構函數
015       Encrypt();
016       // 宣告 setter 與 getter 成員函數
017       void set_code_array();
018       void set_code_array(string);
019       string get_code_array();
020       // 宣告編碼、解碼的成員函數
021       string ToEncode(string);
022       string ToDecode(string);
023
024    private:
025       // 密碼表字串
026       string code_array;
027 };
028
029 /* 檔名: encrypt.h
030    作者: Kaiching Chang
031    時間: 2014-5 */

這裡,我們將常數定義的前置處理器指令 #define 以橘色的語法高亮度標示


004 // 定義兩個巨集常數
005 #define DIFF 97
006 #define NUM 26

對照實作如 ToDecode() ,有沒有感覺程式的語意更清楚了點呢?


055 // 進行編碼工作的成員函數
056 string Encrypt::ToEncode(string s) {
057    // 由參數字串取得字元的暫存變數
058    char c;
059    // 暫存編碼結果的字串
060    string r;
061    int i, m;
062    // 利用迴圈走完參數字串的所有字元
063    for (i = 0; i < s.size(); i++) {
064       // 判斷該字元是否為英文小寫字母
065       // 若是英文小寫字母就進行編碼轉換
066       if (s.at(i) >= DIFF && s.at(i) < DIFF + N) {
067          c = s.at(i);
068          m = c - DIFF;
069          r += get_code_array().at(m);
070       }
071       else {
072          r += s.at(i);
073       }
074    }
075
076    // 結束回傳編碼過的字串
077    return r;
078 }

完整的實作檔案可以參考 encrypt.cpp

我們簡單介紹了前置處理中的引進標頭檔、定義常數及巨集,其實前置處理還可以進行不少工作,例如避免重複引入或除錯等等,不過就目前的發展需求而言,就不詳細討論額外的題材了。

編密碼軟體的核心 Encrypt 類別 (class) 大體上發展完成,同時用了不少標準程式庫的內容,下面我們好好的介紹一下標準程式庫吧!


中英文術語對照


前置處理器 preprocessor
標頭檔 header file
巨集 macro
常數 constant
標準程式庫 standard library
函數 function
編譯器 compiler
前置處理 preprocess
函數 function
類別 class

重點整理


  1. 前置處理是編譯器在實際編譯前對程式檔案的處理工作,有引進標頭檔、巨集替換、條件編譯或除錯等。
  2. #include 為引進標頭檔的前置處理器指令。
  3. #define 為定義巨集的前置處理器指令。

問題與討論


  1. 為什麼要做前置處理?前置處理有什麼好處?
  2. 什麼是巨集?為什麼要設計巨集?

練習


  1. 承接上一個單元的猜數字遊戲,將類別宣告寫在 exercise2101.h 中,實作檔寫在 exercise2101.cpp 中,將原本的 main() 轉換成 Run() ,注意也要連帶實作 ArrayNumber()ABCounter()
  2. 承上題,另寫一個程式 exercise2102.cpp ,裡頭含有 main() 函數,並建立 GuessGame 物件然後呼叫執行 Run()

the end

沒有留言: