C++ 入門指南

單元 21 - 前置處理

本書已有新版,請參考 C++ 入門指南 4.01 - 單元 21 - 前置處理

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

下達編譯指令 → 前置處理
                ↓
          開始實際編譯工作

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

#include <cstdlib>
#include <ctime>

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

#include "encrypt.h"

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

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

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

完整的 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 如下

// 從標準程式庫中引入 string
#include <string>
// 使用 std 中的 string 名稱
using std::string;

// 定義兩個巨集常數
#define DIFF 97
#define N 26

// 宣告 Encrypt 類別
class Encrypt {
public:
    // 宣告建構函數
    Encrypt();
    // 宣告 setter 與 getter 成員函數
    void set_code_array();
    void set_code_array(string);
    string get_code_array();
    // 宣告編碼、解碼的成員函數
    string ToEncode(string);
    string ToDecode(string);
    
private:
    // 密碼表字串
    string code_array;
};

/* 《程式語言教學誌》的範例程式
   http://kaiching.org/
   檔名:encrypt.h
   功能:Encrypt 類別的標頭檔
   作者:張凱慶 */

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

// 定義兩個巨集常數
#define DIFF 97
#define N 26

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

// 進行解碼工作的成員函數
string Encrypt::ToDecode(string s) {
    // 暫存解碼結果的字串
    string r;
    int i, j;
    // 第一層迴圈逐一取得每一個字元
    for (i = 0; i < s.size(); i++) {
        // 判斷該字元是否為英文小寫字母,若是英文小寫字母就進行解碼轉換
        if (s.at(i) >= DIFF && s.at(i) < DIFF + N) {
            // 第二層迴圈尋找該字元在密碼表中的索引值
            // 該索引值加上 DIFF 就可轉換回原本的字元
            for (j = 0; j < N; j++) {
                if (s.at(i) == get_code_array().at(j)) {
                    r += (char) j + DIFF;
                    break;
                }
            }
        }
        else {
            r += s.at(i);
        }
    }
    
    // 結束回傳解碼過的字串
    return r;
}

完整的實作檔案可以參考 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()

上一頁 單元 20 - 型態轉換問題
回 C++ 入門指南目錄
下一頁 單元 22 - 認識標準程式庫
回 C++ 教材
回程式語言教材首頁