【C++ 筆記】前置處理器(preprocessor) - part 33 === 目錄(Table of Contents): [TOC] --- 很感謝你點進來這篇文章。 你好,我並不是什麼 C++、程式語言的專家,所以本文若有些錯誤麻煩請各位鞭大力一點,我極需各位的指正及指導!!本系列文章的性質主要以詼諧的口吻,一派輕鬆的態度自學程式語言,如果你喜歡,麻煩留言說聲文章讚讚吧! Introduction --- preprocessor 是在 C / C++ 編譯前對 Source Code(`.cpp` files)進行處理的一種工具。 > It does many tasks such as including header files, conditional compilation, text substitution, removing comments, etc. > From [GeeksForGeeks](https://www.geeksforgeeks.org/cpp/cpp-preprocessors-and-directives/) preprocessor 可以做到如下這些事情: 1. 引入標頭檔 2. 條件編譯 3. 文本替換 4. 移除註解 5. 等等 另外也可以讓開發者去選擇說,哪些程式需要被保留(included)或是不需要被保留(excluded)的。 經 preprocessor 處理過後的程式碼,通常都被稱為是「已被展開的程式碼(expanded code)」,都由 `.i` 這個副檔名去做儲存的動作。 前置處理主要是在編譯器編譯前,生出一份乾淨的 `.c` / `.cpp` 檔案給它,方便日後處理。 我們所有的前置處理指令(directives)都由 `#` 符號作為開頭,如我們最常見的 `#include` 就是之一。然後這個指令不是 C++ 語法的原因,所以不需要加上分號以示結束。 ## #include 這個指令就是把其他檔案的內容包含到目前的檔案之中。 常見的就是我們把 `iostream` 這個 header file 標頭檔包含到目前檔案中。 Syntax : ```cpp #include <file_name> #include "file_name" ``` 用雙引號或是兩個大於小於號也可以。 ## #define 這個指令常被用來定義一個巨集(marco),巨集也稱為宏(中國大陸音譯)。 巨集是由 preprocessor 執行的文字替換機制。 Syntax : ```cpp #define macro_name value ``` 如下範例,透過 `#define` 將 3.14159 這個常數用 PI 表示,PI 就是巨集,其後若出現的任何巨集都會被替換為 3.14159 常數。 ```cpp= #include <iostream> #define PI 3.14159 using namespace std; int main(){ cout << PI; return 0; } ``` ### #define 的四種語法 前面介紹的屬於下面的常數巨集。 1. Constant Macros(常數巨集) 2. Chain Macros(鏈式巨集) 3. Macro Expressions(巨集運算式) 4. Multiline Macros(多行巨集) ### Chain Macros 簡單來說就是巨集再套一個巨集。 它的具體定義如下: ```cpp= #define MACRO1_NAME value1 #define value1 final_value ``` 範例: ```cpp= #include <iostream> #define MA MB #define MB MC #define MC MD #define MD 100 using namespace std; int main(){ cout << "MA : " << MA << endl; cout << "MB : " << MB << endl; cout << "MC : " << MC << endl; cout << "MD : " << MD << endl; return 0; } ``` 這支程式碼的作用主要就是 MA MB MC MD 這些巨集都可以代表 100 的意思。 可以無限串下去,只要 preprocessor 處理得來的話。 ### Macro Expressions 也稱為類函數巨集。 通常可接受參數並展開成一段運算式或是函數呼叫的程式碼片段。 Syntax : ```cpp #define MACRO_NAME (expression within brackets) ``` or ```cpp #define MACRO_NAME(parameters) (expression) ``` 範例: ```cpp= #include <bits/stdc++.h> #define add(a, b) a + b using namespace std; int main(){ int a, b; cin >> a >> b; cout << add(a, b) << endl; return 0; } ``` 然後也可以寫成這樣: ```cpp= #include <bits/stdc++.h> #define add a + b using namespace std; int main(){ int a, b; cin >> a >> b; cout << add << endl; return 0; } ``` ### Multiline Macros 就是可以建立多行的巨集。 這部分要用到反斜線 `\` 去做到這件事。 定義如下: ```cpp= #define MACRO_NAME value \ value2 \ value3 \ ``` 範例: ```cpp= #include <bits/stdc++.h> #define SWAP(a, b) \ { \ int tmp = a; \ a = b; \ b = tmp; \ } using namespace std; int main(){ int a, b; cin >> a >> b; cout << "a = " << a << ", b = " << b << endl; SWAP(a, b); cout << "a = " << a << ", b = " << b << endl; return 0; } ``` ## #undef 這指令用於取消先前用 `#define` 定義的巨集,使用方法也很簡單,如下: ``` #undef macro_name ``` 範例: ```cpp= #include <iostream> #define PI 3.14159 #undef PI using namespace std; int main(){ cout << PI; return 0; } ``` 輸出結果: ``` main.cpp: In function ‘int main()’: main.cpp:10:13: error: ‘PI’ was not declared in this scope 10 | cout << PI; | ^~ ``` ## 條件編譯(Conditional Compilation) 以下這些指令都是條件預處理器指令: - `#if` - `#elif` - `#else` - `#endif` - `#error` Syntax : ```cpp= #if constant_expr // Code to be executed if constant_expression is true #elif another_constant_expr // Code to be excuted if another_constant_expression is true #else // Code to be excuted if none of the above conditions are true #endif ``` 使用上與 C++ 的條件語句是差不多的,`#elif` 就是 `else if`,差別在於需要用 `#endif` 表示條件編譯的結束。 範例(https://www.geeksforgeeks.org/cpp/cpp-preprocessors-and-directives/): ```cpp= #include <iostream> using namespace std; #define PI 3.14159 int main() { // Conditional compilation #if defined(PI) cout << "PI is defined"; #elif defined(SQUARE) cout << "PI is not defined"; #else #error "Neither PI nor SQUARE is defined" #endif return 0; } ``` 輸出結果: ``` PI is defined ``` ### #ifdef & #ifndef `#ifdef` 判斷巨集是否定義;`#ifndef` 有個 n,表示判斷巨集是否未定義。 Syntax : ```cpp= #ifdef macro_name // Code to be executed if macro_name is defined #ifndef macro_name // Code to be executed if macro_name is not defined #endif ``` 範例: ```cpp= #include <iostream> using namespace std; #define DEBUG int main() { #ifdef DEBUG printf("除錯模式\n"); #endif #ifndef RELEASE #define RELEASE printf("發佈模式\n"); #endif return 0; } ``` 輸出結果: ``` 除錯模式 發佈模式 ``` ### 為什麼需要條件編譯? 在編譯前,可以選擇性包含或排除某些程式碼,使得編譯器只編譯所需部分的程式碼,這叫做條件編譯。 這樣做有什麼好處? 1. 跨平台(Cross-platform)兼容性佳 2. 節省資源 3. 減小可執行檔(.exe)體積 ## #error 用來自訂編譯錯誤時的訊息。 Syntax : ``` #error error_message ``` 範例改自 GeeksForGeeks: ```cpp= #include <iostream> using namespace std; // not defining PI here // #define PI 3.14159 int main() { #if defined(PI) cout << "巨集 PI 已被定義" << endl; #else #error "巨集 PI 和 SQUARE 都沒被定義" #endif return 0; } ``` 輸出結果: ``` main.cpp:12:10: error: #error "巨集 PI 或 SQUARE 沒被定義" 12 | #error "巨集 PI 或 SQUARE 沒被定義" | ^~~~~ ``` ## #warning 在編譯前可以透過這個指令自訂先行警告訊息。 跟 `#error` 有什麼差別?`#error` 是自訂編譯錯誤時的訊息,`#warning` 會在編譯前警告。 Syntax : ``` #warning message ``` 範例: ```cpp= #include <iostream> using namespace std; #ifndef PI #warning "巨集 PI 未被定義!" #endif int main(){ cout << "The Program is running now."; return 0; } ``` 輸出結果: ![image](https://hackmd.io/_uploads/r1q49OF5ee.png) 需注意的是,警告訊息會由編譯器不同而有所不一樣的格式。 ## #pragma `#pragma` 用於告訴編譯器針對特定需求執行特殊行為,其語法和功能並不屬於 C++ 語言標準,而是由各編譯器自行定義和支援。 在使用上需要看編譯器怎麼定義 `#pragma` 的行為,不然會因為不相容問題產生一些錯誤。 Syntax : ``` #pragma directive ``` 以下是 `#pragma` 常用的 Flags: - `#pragma once`:用於保護 header files - `#pragma message`:用於在編譯期間打印自訂訊息 - `#pragma warning`:用於控制警告行為(如啟用或停用警告)。 - `#pragma optimize`:用於控制最佳化設定(管理最佳化等級)。 - `#pragma comment`:用於在 .o 檔中含一些附加資訊(或指定 linker 選項)。 範例: `#pragma once` 在以下範例中,就是要確保 `MyHeader.h` 不要被多次包含,只要包含一次就好。 ```cpp= // MyHeader.h #pragma once struct MyStruct { int value; }; ``` `#pragma message` 範例: ```cpp= #pragma message("這是編譯期間的提示訊息") int main() { return 0; } ``` `#pragma warning` 用來開啟或關閉指定的編譯器警告(僅部分編譯器支援),以下是舉 MSVC 編譯器為例製作的範例: ```cpp= #pragma warning(disable:4100) void func(int unusedParam) { // 不會出現未使用參數的警告 } ``` 要重新啟用就可這樣打:`#pragma warning(default:4100)` `#pragma optimize` 範例(MSVC): ```cpp= #pragma optimize("g", on) // g: 全優化 int foo() { int a = 1; int b = 2; return a + b; } #pragma optimize("", on) // 還原到預設優化設定 ``` ## # 和 ## 運算子 `#` 運算子拿來字串化,就是把巨集中的參數轉字串。 範例: ```cpp= #include <iostream> #define STR(x) #x using namespace std; int main(){ cout << STR(Hello World); return 0; } ``` 輸出結果: ``` Hello World ``` 以上範例的 STR 巨集透過 `#x`,可將輸入的引數 Hello World 變為字串 `"Hello World"`。 --- `##` 運算子則是把巨集中的兩個參數連接(concatenate)成一個識別字。 範例: ```cpp= #include <iostream> #define CONCAT(x, y) x ## y using namespace std; int main(){ int CONCAT(var, 1) = 123; cout << var1 << endl; return 0; } ``` 輸出結果: ``` 123 ``` 可以看到真的可以拿來作為識別字,這個在對於生成函數跟變數名稱會很有用。 總結 --- ### 前置處理器作用 用於在編譯器處理之前,先對程式碼進行「展開與清理」。 功能: 1. 引入標頭檔 2. 條件編譯 3. 文字替換 4. 移除註解等 前置處理後的程式碼稱為「展開後程式碼」,通常存為 `.i` 檔案。 前置處理指令以 `#` 開頭,非 C++ 語法,不需要分號。 ### 常見指令與功能 `#include`:引入其他檔案(常用於 header files)。 `#define`:定義巨集(文字替換),包含: #### 常數巨集 1. 鏈式巨集(巨集套巨集) 2. 巨集運算式(類函數巨集,可傳參數) 3. 多行巨集(可用反斜線延伸多行) `#undef`:取消已定義的巨集。 ### 條件編譯 指令:`#if`、`#elif`、`#else`、`#endif`、`#ifdef`、`#ifndef`。 透過根據巨集是否定義,或條件是否成立,來選擇性保留或排除程式碼。 優點: 1. 跨平台兼容 2. 節省資源 3. 縮小可執行檔體積 `#error`:自訂錯誤訊息,強制中斷編譯。 `#warning`:在編譯前顯示警告訊息。 `#pragma`:提供編譯器專屬的特殊指令,非標準 C++。 常用 flags: `#pragma once`:避免標頭檔重覆引入。 `#pragma message`:編譯期間顯示提示訊息。 `#pragma warning`:控制警告行為。 `#pragma optimize`:調整最佳化設定。 ### 運算子 `#`:字串化,把巨集參數轉成字串。 `##`:連接參數,用於生成識別字(如動態生成變數或函數名稱)。 參考資料 --- [你所不知道的 C 語言:前置處理器應用篇 - HackMD](https://hackmd.io/@sysprog/c-preprocessor) [[C 語言] 程式設計教學:如何使用巨集 (macro) 或前置處理器 (Preprocessor) | 開源技術教學](https://opensourcedoc.com/c-programming/preprocessor/) [C++ Preprocessor And Preprocessor Directives - GeeksforGeeks](https://www.geeksforgeeks.org/cpp/cpp-preprocessors-and-directives/) [#define in C++ - GeeksforGeeks](https://www.geeksforgeeks.org/cpp/define-preprocessor-in-cpp/) [C++ 预处理器 | 菜鸟教程](https://www.runoob.com/cplusplus/cpp-preprocessor.html)