【C++ 筆記】前置處理器(preprocessor) - part 33

很感謝你點進來這篇文章。

你好,我並不是什麼 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

preprocessor 可以做到如下這些事情:

  1. 引入標頭檔
  2. 條件編譯
  3. 文本替換
  4. 移除註解
  5. 等等

另外也可以讓開發者去選擇說,哪些程式需要被保留(included)或是不需要被保留(excluded)的。

經 preprocessor 處理過後的程式碼,通常都被稱為是「已被展開的程式碼(expanded code)」,都由 .i 這個副檔名去做儲存的動作。

前置處理主要是在編譯器編譯前,生出一份乾淨的 .c / .cpp 檔案給它,方便日後處理。

我們所有的前置處理指令(directives)都由 # 符號作為開頭,如我們最常見的 #include 就是之一。然後這個指令不是 C++ 語法的原因,所以不需要加上分號以示結束。

#include

這個指令就是把其他檔案的內容包含到目前的檔案之中。

常見的就是我們把 iostream 這個 header file 標頭檔包含到目前檔案中。

Syntax :

1
2
#include <file_name>
#include "file_name"

用雙引號或是兩個大於小於號也可以。

#define

這個指令常被用來定義一個巨集(marco),巨集也稱為宏(中國大陸音譯)。

巨集是由 preprocessor 執行的文字替換機制。

Syntax :

1
#define macro_name value

如下範例,透過 #define 將 3.14159 這個常數用 PI 表示,PI 就是巨集,其後若出現的任何巨集都會被替換為 3.14159 常數。

1
2
3
4
5
6
7
8
9
10
#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

簡單來說就是巨集再套一個巨集。

它的具體定義如下:

1
2
#define MACRO1_NAME  value1
#define value1 final_value

範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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 :

1
#define MACRO_NAME (expression within brackets)

or

1
#define MACRO_NAME(parameters)  (expression)

範例:

1
2
3
4
5
6
7
8
9
10
11
12
#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;
}

然後也可以寫成這樣:

1
2
3
4
5
6
7
8
9
10
11
12
#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

就是可以建立多行的巨集。

這部分要用到反斜線 \ 去做到這件事。

定義如下:

1
2
3
#define MACRO_NAME value \
value2 \
value3 \

範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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 定義的巨集,使用方法也很簡單,如下:

1
#undef macro_name

範例:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

#define PI 3.14159

#undef PI

using namespace std;

int main(){
cout << PI;
return 0;
}

輸出結果:

1
2
3
4
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 :

1
2
3
4
5
6
7
#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/):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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;
}

輸出結果:

1
PI is defined

#ifdef & #ifndef

#ifdef 判斷巨集是否定義;#ifndef 有個 n,表示判斷巨集是否未定義。

Syntax :

1
2
3
4
5
#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

範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

using namespace std;

#define DEBUG

int main() {

#ifdef DEBUG
printf("除錯模式\n");
#endif

#ifndef RELEASE
#define RELEASE
printf("發佈模式\n");
#endif

return 0;
}

輸出結果:

1
2
除錯模式
發佈模式

為什麼需要條件編譯?

在編譯前,可以選擇性包含或排除某些程式碼,使得編譯器只編譯所需部分的程式碼,這叫做條件編譯。

這樣做有什麼好處?

  1. 跨平台(Cross-platform)兼容性佳
  2. 節省資源
  3. 減小可執行檔(.exe)體積

#error

用來自訂編譯錯誤時的訊息。

Syntax :

1
#error error_message

範例改自 GeeksForGeeks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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;
}

輸出結果:

1
2
3
main.cpp:12:10: error: #error "巨集 PI 或 SQUARE 沒被定義"
12 | #error "巨集 PI 或 SQUARE 沒被定義"
| ^~~~~

#warning

在編譯前可以透過這個指令自訂先行警告訊息。

#error 有什麼差別?#error 是自訂編譯錯誤時的訊息,#warning 會在編譯前警告。

Syntax :

1
#warning message

範例:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

using namespace std;

#ifndef PI
#warning "巨集 PI 未被定義!"
#endif

int main(){
cout << "The Program is running now.";
return 0;
}

輸出結果:

image

需注意的是,警告訊息會由編譯器不同而有所不一樣的格式。

#pragma

#pragma 用於告訴編譯器針對特定需求執行特殊行為,其語法和功能並不屬於 C++ 語言標準,而是由各編譯器自行定義和支援。

在使用上需要看編譯器怎麼定義 #pragma 的行為,不然會因為不相容問題產生一些錯誤。

Syntax :

1
#pragma directive

以下是 #pragma 常用的 Flags:

  • #pragma once:用於保護 header files
  • #pragma message:用於在編譯期間打印自訂訊息
  • #pragma warning:用於控制警告行為(如啟用或停用警告)。
  • #pragma optimize:用於控制最佳化設定(管理最佳化等級)。
  • #pragma comment:用於在 .o 檔中含一些附加資訊(或指定 linker 選項)。

範例:

#pragma once 在以下範例中,就是要確保 MyHeader.h 不要被多次包含,只要包含一次就好。

1
2
3
4
5
6
// MyHeader.h
#pragma once

struct MyStruct {
int value;
};

#pragma message 範例:

1
2
3
4
5
#pragma message("這是編譯期間的提示訊息")

int main() {
return 0;
}

#pragma warning 用來開啟或關閉指定的編譯器警告(僅部分編譯器支援),以下是舉 MSVC 編譯器為例製作的範例:

1
2
3
4
5
#pragma warning(disable:4100)

void func(int unusedParam) {
// 不會出現未使用參數的警告
}

要重新啟用就可這樣打:#pragma warning(default:4100)

#pragma optimize 範例(MSVC):

1
2
3
4
5
6
7
8
9
#pragma optimize("g", on)  // g: 全優化

int foo() {
int a = 1;
int b = 2;
return a + b;
}

#pragma optimize("", on) // 還原到預設優化設定

# 和 ## 運算子

# 運算子拿來字串化,就是把巨集中的參數轉字串。

範例:

1
2
3
4
5
6
7
8
9
10
#include <iostream>

#define STR(x) #x

using namespace std;

int main(){
cout << STR(Hello World);
return 0;
}

輸出結果:

1
Hello World

以上範例的 STR 巨集透過 #x,可將輸入的引數 Hello World 變為字串 "Hello World"


## 運算子則是把巨集中的兩個參數連接(concatenate)成一個識別字。

範例:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

#define CONCAT(x, y) x ## y

using namespace std;

int main(){
int CONCAT(var, 1) = 123;
cout << var1 << endl;
return 0;
}

輸出結果:

1
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

[C 語言] 程式設計教學:如何使用巨集 (macro) 或前置處理器 (Preprocessor) | 開源技術教學

C++ Preprocessor And Preprocessor Directives - GeeksforGeeks

#define in C++ - GeeksforGeeks

C++ 预处理器 | 菜鸟教程