【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;
}
```
輸出結果:

需注意的是,警告訊息會由編譯器不同而有所不一樣的格式。
## #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)