前置處理器 = 進行「前置處理」的文字編輯器。
或許我們得先解決什麼是「前置處理」。
從說文解字的角度出發,它做的是某件工作的事前處理,在C和C++中,我們指的是進行編譯前的處理工作,大致上可分為:
- 引入標頭檔(Header file)
- 進行文字的取代
- 巨集、常數的定義……
- 條件式編譯
具體而言,在程式中前有 #
的部分就涉及了前置處理,比如#define
、#Pragma
與#include
等等。
要注意,前置處理器並不屬於C編譯器的一部分,它也不了解C家族的語法,如果你用C的觀點來審視它可能會碰上大麻煩。
一些常見的用法
include
#include
用於引入外部的標頭檔,比如以下程式碼:1
2
3
4
5
6
int main(){
printf("Hello World!");
return 0;
}
便引入stdio.h,明確的宣告(explicit declaration)了printf,並放入目前的程式中,所以我們能使用標準輸出的printf函式,而不必自己實作它的細節。
順代一提,若你使用的是C編譯器而非C++編譯器,就算沒有引入對應的標頭檔,編譯器仍會為函式建立一個隱性宣告(implicit declaration),當聯結至標準函式庫時,若「湊巧」在庫中發現了一個同名的函式,便會把它們聯結在一塊兒,編譯時跳出的就不會是 Error 而是報 Warning,比如:
waring: incompatible implicit declaration of built-in function ‘printf’
而在c++的編譯器上則是:
error: ‘printf’ was not declared in this scope
關於C標準上更詳細的資訊,可以看看這篇文章
在使用include上有兩種格式:
#include <file_name>
file_name為系統內建的標頭檔,如stdio、stdlib等等,通常放於預設目錄底下。
#include "file_name"
當用雙引號包覆時,file_name 是我們自定義的標頭檔。引用方式可採相對路徑或絕對路徑,此外,我們也可以在追加 gcc flag -i 來告知標頭檔所在目錄,編譯時 gcc 會先查找系統目錄,再從 -i 的引數中由左至右尋找檔案。
define
define
的功能是進行文字上的取代:
define NAME Substitute-text
便會將NAME替代為Substitute-text,我們稱NAME為巨集,常會全以字母大寫呈現巨集,當作和普通變數的辨別標準:1
2
3
4
5
6
int main(){
printf("%d",TEN);
}
我們可以用gcc -E 來觀察前置處理器的輸出。完成前置處理後,TEN
便被定義(define)為“10”
:1
2
3
4
5
6
int main(){
printf("%d",10);
}
透過適當地調用巨集,能讓起來程式碼看起來更加友善,即便不使用string
,我們也能很形象化的描述各種情形:1
2
3
4
5
6
7
8
9
10
11
12
13
...
switch(type){
case MEDICINE:
...
break;
case SCIENCE:
...
break;
}
define
也能夠引入參數:1
2
3
4
5
6
7
8
int main(){
int a = 5;
int b = 7;
swap(a,b);
}
其中swap後請記得「緊接」(
,否則會跳脫出取代範圍,還記得嗎?我們是用空白來做區分的。上述程式碼在完成前置處理後會變成這樣:1
2
3
4
5
6
7
8
int main(){
int a = 5;
int b = 7;
{int t; t=a; a=b; b=t;}
}
你可能覺得這麼做跟使用行內涵式(inline
)沒什麼兩樣,不過早期的C編譯器並沒有常數與行內涵式的功能,才衍生出這樣的解決方案。
此外,正如前文所提到的,前置處理器並不了解 C 家族的語法,所以他所做的僅僅是取代而已,諸如要去取代的變數型別、運算元的優先順序上等涉及 C 語法的細節都不會加以檢查,所以使用上須格外小心,否則會在除錯上造成很大的盲點。比方說,變數的組合建議以括弧包覆,不然可能發生這種情形:1
2
3
4
5
6
7
8
9
10
int main() {
cout << "The square of all the parts is " <<
ALL_PARTS * ALL_PARTS << '\n';
return (0);
}
預期的輸出為 12 * 12 = 144 ,但是真正的輸出是 7+5*7+5 =47,這便是沒考慮到運算順序導置的缺失,應該修正為:#define ALL_PARTS ((FIRST_PART) + (LAST_PART))
條件式編譯
前置處理器提供了一些if判斷指令,我們能透過它來控制某些功能是否實現,最常見的莫過於include guard了,這避免了引入了某些程式碼導至的錯誤,我們能用#ifndef
與endif
來達成,引用wiki的例子:1
2
3
4
5
6
7
8
struct foo {
int member;
};
既然有#ifndef
,當然也有#ifdef
、#if
、#else
囉,像上述的功能也可以用#ifdef
配合#else
或使用#if defined(H_GRANDFATHER)
達成。
除了include guard,條件式編譯也有不少用途,比方說程式碼的除錯,我們可以透過定義一個巨集DEBUG,藉由#define
及判斷式來開關除錯模式:1
2
3
4
5
6
printf("Debug version.\n");
//insert your debug code or function.
printf("Production version.\n")