C++ 入門指南 4.01

單元 21 - 前置處理

-unit21-

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) 在實際編譯成執行檔之前,會先進行前置處理,這個處理就是直接把程式 (program) 中有 PrintInt(a) 換成 cout << a << endl ,好處就是少掉額外進行的呼叫函數。

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

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

我們也可以用 #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 類別的標頭檔
   作者:張凱慶 */

對照實作如 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.cxx

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

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

中英文術語對照
類別class
編譯器compiler
常數constant
函數function
標頭檔header file
巨集macro
前置處理器preprocessor
程式program
標準程式庫standard library
重點整理
1. 前置處理是編譯器在實際編譯前對程式檔案的處理工作,有引進標頭檔、巨集替換、條件編譯或除錯等。
2. #include 為引進標頭檔的前置處理器指令。
3. #define 為定義巨集的前置處理器指令。
問題與討論
1. 為什麼要做前置處理?前置處理有什麼好處?
2. 什麼是巨集?為什麼要設計巨集?
練習
1. 標準程式庫中的 deque 是雙端佇列的集合體型態,可以在頭尾進行資料操作,字面常數一樣是用大括弧,寫一個程式 exercise2101.cxx ,用字面常數建立一個整數的 deque ,然後用 push_front() 在頭插入整數, push_back() 在尾插入另一個整數,最後印出所有元素內容。 參考程式碼
2. C++11 新增了 unordered_map ,這是未排序的配對型集合體型態,使用方式與 map 類似,寫一個程式 exercise2102.cxx ,宣告建立一個 unordered_map ,然後印出元素內容。 參考程式碼
3. C++11 新增了 unordered_set ,這是未排序的配對型集合體型態,使用方式與 set 類似,寫一個程式 exercise2103.cxx ,宣告建立一個 unordered_set ,然後印出元素內容。 參考程式碼
4. 標準程式庫中的 priority_queue 是預設以由大到小排序的集合體型態,利用 push() 可以加入資料,寫一個程式 exercise2104.cxx ,宣告建立一個整數的 priority_queue ,利用 push() 加入數個不同整數,然後印出元素內容。 參考程式碼
5. 標準程式庫中的 list 是雙向鏈結串列的集合體型態,字面常數一樣是用大括弧,寫一個程式 exercise2105.cxx ,用字面常數建立一個整數的 list ,然後用 find() 查找元素, insert() 插入元素, remove() 移除元素,最後印出操作過後的所有元素內容。 參考程式碼
6. 標準程式庫中 cmathsin() 用以計算正弦函數,寫一個程式 exercise2106.cxx ,利用 sin() 計算正弦值。 參考程式碼
7. 標準程式庫中 cmathcos() 用以計算餘弦函數,寫一個程式 exercise2107.cxx ,利用 cos() 計算餘弦值。 參考程式碼
8. 標準程式庫中 cmathtan() 用以計算正切函數,寫一個程式 exercise2108.cxx ,利用 tan() 計算正切值。 參考程式碼
9. 標準程式庫中 cmathfloor()ceil()round() 用以計算最接近整數,寫一個程式 exercise2109.cxx ,比較三者差異。 參考程式碼
10. 標準程式庫中 cmathisgreater()isgreaterequal()isless()islessequal()islessgreater() 用以比較兩個整數參數之間的關係,用法如函數識別字的字面意義,寫一個程式 exercise2110.cxx ,比較五者差異。 參考程式碼

相關教學影片

上一頁 單元 20 - 型態轉換問題
回 C++ 入門指南 4.01 目錄
下一頁 單元 22 - 重構
回 C++ 教材
回程式語言教材首頁