{%hackmd BJrTq20hE %} ###### 作者:void ###### 修改時間:2021/11/09 # C++:預處理器指示詞 ###### tags: `Programming Language` `C++` `Tutorial` ## 前言 > [color=#1f7c84]恩恩,最近閒來無事看了幾篇跟預處理器指示詞有關的寫法,我只能說令我震驚到了,原來 C++ 有這麼好用的東西我都不知道阿。從我第一次在高中碰到的時候,學到有關預處理器指示詞也只有 `#include` 跟 `#define` ,直到上研究所學長提起才知道預處理器指示詞不只這兩個,但當時~~懶惰~~忙碌沒有仔細看看這些指示詞,慚愧慚愧 ~ 。 > > 好了,廢話少說直接開看! 基本上預處理器指示詞是編譯器在看的,編譯器根據預處理器指示詞進行條件編譯。這個部分可以幫助我們在 Debug 、版本控管、處理相容性問題的時候,會有更靈活的運用。 ## 概述 `#define`:定義一個巨集 `#undef`:取消定義一個巨集 `#if`:根據後方的條件句,決定是否編譯這段 `#ifdef`:如果後方的巨集名稱有被定義,則編譯以下程式碼 `#ifndef`:如果後方的巨集名稱沒有被定義,則編譯以下程式碼 `#elif`:之前的條件句判斷失敗,則繼續判斷此條件 `#else`:之前的條件句判斷失敗,則編譯以下程式 `#endif`:條件句的結尾 基本上這些不需要想的太難,其實也可以把它當成一般的程式讀,像是`#if`就如同 C 中的`if`,指示他們判斷的東西跟執行的動作不同而已,下面我們再繼續做介紹。 ## 基本使用 > [color=#2db76e]我們高中資訊老師說:「想學個東西最好的方法是用他完成一個簡單的東西,你就有想學的動力了」 ### #define 看看這段程式碼吧 ```cpp= #include <iostream> #define TEST 1 using namespace std; int main(){ cout << TEST << endl; } ``` 對,就是這們簡單。基本上`#define`就很像定義一個變數一樣,我們稱它為識別碼。你說這個識別碼是啥,他就是啥,編譯的時候會把它直接把程式中出現識別碼`TEST`替換成`1`。所以你編譯的東西相當於: ```cpp= #include <iostream> using namespace std; int main(){ cout << 1 << endl; } ``` 但是並不是所有你`#define`過的識別碼都會被編譯器取代,這部分請參考 [Microsoft 的 docs](https://docs.microsoft.com/zh-tw/cpp/preprocessor/hash-define-directive-c-cpp?view=msvc-160),不過今天寫的東西不會碰到這個部分,有興趣的可以自行鑽研。 ### #undef 既然剛才有說定義`define`,那相反的就會有取消定義`undef`,基本上也十分好懂,就是把你定義過的識別碼取消,來看個例子。 ```cpp= #include <iostream> #define TEST 1 #undef TEST //添加此行 using namespace std; int main(){ cout << TEST << endl; } ``` 你按下執行後,你可能會很疑惑為啥就跳出了`error: 'TEST' was not declared in this scope`,不過想想也很合理,畢竟是沒定義的`TEST`就只被當成一般的變數嘛 (摸頭燦笑,`#undef`非常簡單就只是把你定義過的識別碼刪掉。 ## 條件編譯 ### #if #elif #else #endif 下一個要討論的是條件編譯,這個部分我們更深入討論其他的預處理器指示詞。 讓我們來看個簡單的例子 ```cpp= #include <iostream> #define TEST 1 using namespace std; int main(){ cout << TEST << endl; #if TEST == 1 cout << "TEST = 1" << endl; #elif TEST == 2 cout << "TEST = 2" << endl; #elif TEST == 3 || TEST == 4 cout << "Maybe TEST = 3 or 4" << endl; #else cout << "TEST != 1, 2, 3, 4" << endl; #endif } ``` `#if` `#elif` `#else`這幾個指示詞的邏輯就像是一般 C/C++ 中的`if` `else`一樣,識別碼進行條件判斷,判斷成功就編譯,失敗就跳過。`#endif`就沒甚麼好說的,基本上只要`#if`指示詞,後面就必須跟著`#endif` > [color=#FF0000]:bulb: 不過要記住一點,指示詞是給編譯器看得,不是給程式看得,所以後面的條件句再進行判斷的時候,你只能使用`#define`過的識別碼,不能使用程式中的變數。 ### #ifdef #ifndef 介紹這兩個指令之前先來看看以下程式碼 ```cpp= #include <iostream> #define TEST using namespace std; int main(){ cout << "Is TEST be defined ?" << endl; #if defined(TEST) cout << "TEST is defined" << endl; #endif #if !defined(TEST) cout << "TEST isn't defined" << endl; #endif } ``` 我們很常會出現像是上面的這種程式碼,用於檢測識別碼是否已經被定義了。也因為過於常用就被額外設定了兩個個預處理器指示詞`#ifdef` `#ifndef`。 所以上面的指示詞可以被寫成下面這樣: ```cpp= #include <iostream> #define TEST using namespace std; int main(){ cout << "Is TEST be defined ?" << endl; #ifdef TEST cout << "TEST is defined" << endl; #endif #ifndef TEST cout << "TEST isn't defined" << endl; #endif } ``` 是不是簡潔多了呢?基本上也是可以搭配`#elif` `#else`一起使用,可以搞出很堆變化。 ## 應用 大致上介紹完了幾個比較重要的指示詞後,後續介紹幾個比較常用的應用,看看能不能迸出什麼新滋味 ( ? ### Debug Mode #### 情況 有的時候你程式碼寫的一長串,然後編譯過關是一回事,程式有沒有照自己想像的跑又是另一回事。這時候就會一直打`cout`或是`printf`,Debug完後,要留也不是;不留也不是,這時候預處理器指示詞可以提供一個比較乾淨的管理方式。我們來看看下面的程式碼: ```cpp= //main.cpp #include <iostream> #define _A_DEBUG_ 0 #define _B_DEBUG_ 0 using namespace std; int main(){ int a = 0, b = 1; for(int i=0;i<=10;i++){ a++; b *= 2; #if _A_DEBUG_ == 1 cout << a << " "; #endif #if _B_DEBUG_ == 1 cout << b << " "; #endif } cout << "a: " << a << endl; cout << "b: " << a << endl; } ``` 這個程式應該難不倒各位吧?如果說想看說`a` `b`在迴圈的情況,你可以透過調整最上面`#define`後識別碼的值開關是否要要進入 Debug 模式。 #### 改良 那既然他是給編譯器看的,那我們何不將它集中在一個 header file 中呢? ```cpp= //config.h #define _A_DEBUG_ 0 #define _B_DEBUG_ 0 ``` ```cpp= //main #include <iostream> #include "config.h" using namespace std; int main(){ int a = 0, b = 1; for(int i=0;i<=10;i++){ a++; b *= 2; #if _A_DEBUG_ == 1 cout << a << " "; #endif #if _B_DEBUG_ == 1 cout << b << " "; #endif } cout << "a: " << a << endl; cout << "b: " << a << endl; } ``` 現在還只是小程式,當你有很多 Debug 陳述式的時候,`#define` 多到爆炸,那妳可以用這方法讓程式簡潔一點。 #### 再改良版 那如果你除了個別控制外,希望可以有個總開關的話也可以這麼寫: ```cpp= #define _DEBUG_MODE_ 0 #define _A_DEBUG_ 0 #define _B_DEBUG_ 0 #if _DEBUG_MODE_ == 1 #undef _A_DEBUG_ #undef _B_DEBUG_ #define _A_DEBUG_ 1 #define _B_DEBUG_ 1 #endif ``` 這麼寫可以覆蓋掉所有的設定,直接讓編譯器多編譯兩段 Debug 用的程式碼。也可以透過類似的方式控制你的 Debug 資訊想要顯示多少。 不過很可惜的就是並不存在重新定義的指示詞,所以我們只能先用`#undef`清掉之前的識別碼重新定義。 ### 多檔案編譯 #### 情況 多檔案編譯的時候,每個不同的檔案都會匯入不同的 header ,但是很可能會這些 header 很可能會重複匯入,導致編譯器跳出`XXX is defined`的錯誤,我們用上面的程式小小修改一下: ```cpp= //Foo.h #include <vector> class Foo{ vector<int> x; Foo(){} int head(){ if(!x.empty()) return x[0]; return -1; } }; ``` ```cpp= //Foo2.h #include <vector> class Foo2{ vector<char> x; Foo2(){} char head(){ if(!x.empty()) return x[0]; return '#' } }; ``` ```cpp= //main.cpp #include <iostream> #include <vector> #include "Foo.h" #include "Foo2.h" using namespace std; int main(){ Foo test; Foo2 test2; vector<bool> test3; } ``` #### 解決方法 上面的 `#include <vector>` 被重複匯入了,所以執行這個程式肯定是報錯的。但我們可以利用下面的技巧來迴避這個問題。 ```cpp= #ifndef _FOO_H_ #define _FOO_H_ ... //程式碼 #endif ``` 這個命名方式就是檔名全大寫,前後加 `_`,副檔名的 `.` 也換成 `_` #### 解決 上面的寫法代表如果這裡面有沒定義 (匯入) 的,則定義 (匯入)。所以要解決上面的問題很簡單: ```cpp= //Foo.h #ifndef _FOO_H_ #define _FOO_H_ #include <vector> class Foo{ vector<int> x; Foo(){} int head(){ if(!x.empty()) return x[0]; return -1; } }; #endif ``` ```cpp= //Foo2.h #ifndef _FOO2_H_ #define _FOO2_H_ #include <vector> class Foo2{ vector<char> x; Foo2(){} char head(){ if(!x.empty()) return x[0]; return '#' } }; #endif ``` ```cpp= //main.cpp #ifndef _MAIN_CPP_ #define _MAIN_CPP_ #include <iostream> #include <vector> #include "Foo.h" #include "Foo2.h" using namespace std; int main(){ Foo test; Foo2 test2; vector<bool> test3; } #endif ``` 這樣就不會再發生重複定義或匯入的問題了。 > [color=#FF0000]:bulb: 隨手加上這三行程式碼是個好習慣,不管事你在做大專案,或是要程式交接,可以讓下一位接手你程式的人不會發生一堆相容性的問題。 ## 本文網址 https://hackmd.io/@voidgeary/BkpVyzHvF ## 參考 Reference ➜ https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/81826/ Reference ➜ https://www.gushiciku.cn/pl/gL7M/zh-tw Reference ➜ https://docs.microsoft.com/zh-tw/cpp/preprocessor/preprocessor?view=msvc-160
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up