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 類別中反覆出現的 97 、 26 等數字都應該要定義成常數,最主要的原因不只是這些都是固定的數字,更重要的,假如有一天我們需要對不同的語言文字編碼,雖然很可能只是修改一下 Encrypt 類別,但要把每個 97 及 26 通通找出來修改會是件很麻煩的事情,只在一個地方修改的話,相對就更容易維護程式碼。
常數通常用大寫字母定義在標頭檔裡,我們將 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 |
重點整理
- 前置處理是編譯器在實際編譯前對程式檔案的處理工作,有引進標頭檔、巨集替換、條件編譯或除錯等。
- #include 為引進標頭檔的前置處理器指令。
- #define 為定義巨集的前置處理器指令。
問題與討論
- 為什麼要做前置處理?前置處理有什麼好處?
- 什麼是巨集?為什麼要設計巨集?
練習
- 承接上一個單元的猜數字遊戲,將類別宣告寫在 exercise2101.h 中,實作檔寫在 exercise2101.cpp 中,將原本的 main() 轉換成 Run() ,注意也要連帶實作 ArrayNumber() 及 ABCounter() 。
- 承上題,另寫一個程式 exercise2102.cpp ,裡頭含有 main() 函數,並建立 GuessGame 物件然後呼叫執行 Run() 。
the end
沒有留言:
張貼留言