前置處理器

前置處理器 = 進行「前置處理」的文字編輯器。

或許我們得先解決什麼是「前置處理」。
從說文解字的角度出發,它做的是某件工作的事前處理,在C和C++中,我們指的是進行編譯前的處理工作,大致上可分為:

  • 引入標頭檔(Header file)
  • 進行文字的取代
    • 巨集、常數的定義……
    • 條件式編譯

具體而言,在程式中前有 # 的部分就涉及了前置處理,比如#define#Pragma#include等等。

要注意,前置處理器並不屬於C編譯器的一部分,它也不了解C家族的語法,如果你用C的觀點來審視它可能會碰上大麻煩。

一些常見的用法

include

#include用於引入外部的標頭檔,比如以下程式碼:

1
2
3
4
5
6
#include <stdio.h>

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
#include <stdio.h>
#define TEN 10

int main(){
printf("%d",TEN);
}

我們可以用gcc -E 來觀察前置處理器的輸出。完成前置處理後,TEN便被定義(define)為“10”

1
2
3
4
5
6
#include <stdio.h>
#define TEN 10

int main(){
printf("%d",10);
}

透過適當地調用巨集,能讓起來程式碼看起來更加友善,即便不使用string,我們也能很形象化的描述各種情形:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define MEDICINE 1
#define SCIENCE 2
#define ENGLISH 3

...
switch(type){
case MEDICINE:
...
break;
case SCIENCE:
...
break;
}

define也能夠引入參數:

1
2
3
4
5
6
7
8
#include <stdio.h>
#define swap(x,y) {int t; t=x; x=y; y=t; }

int main(){
int a = 5;
int b = 7;
swap(a,b);
}

其中swap後請記得「緊接」(,否則會跳脫出取代範圍,還記得嗎?我們是用空白來做區分的。上述程式碼在完成前置處理後會變成這樣:

1
2
3
4
5
6
7
8
#include <stdio.h>
#define swap(x,y) {int t; t=x; x=y; y=t; }

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
#include <iostream>
#define FIRST_PART 7
#define LAST_PART 5
#define ALL_PARTS FIRST_PART + LAST_PART

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了,這避免了引入了某些程式碼導至的錯誤,我們能用#ifndefendif來達成,引用wiki的例子:

1
2
3
4
5
6
7
8
#ifndef H_GRANDFATHER ← if not define H_GRANDFATHER.
#define H_GRANDFATHER ← then define H_GRANDFATHER.

struct foo {
int member;
};

#endif ← close this if statement.

既然有#ifndef,當然也有#ifdef#if#else囉,像上述的功能也可以用#ifdef配合#else或使用#if defined(H_GRANDFATHER)達成。

除了include guard,條件式編譯也有不少用途,比方說程式碼的除錯,我們可以透過定義一個巨集DEBUG,藉由#define及判斷式來開關除錯模式:

1
2
3
4
5
6
#ifdef DEBUG
printf("Debug version.\n");
//insert your debug code or function.
#else
printf("Production version.\n")
#endif