前置處理器語言,顧名思義,並不是 C 語言的一部份,而是編譯過程中前置處理部份處理的簡單語言,以最簡單的 Hello, World 程式為例:
#include <stdio.h>
int main(void) {
printf("Hello! World!\n");
printf("哈囉!C 語言!\n");
return 0;
}
#include
是前置處理器的原始碼含括指令,表示將含括的檔案插入目前原始碼之中,使用 gcc
的話,可以指定 -E
表示只進行前置處理,例如:
gcc -E main.c -o main.i
開啟 main.i 的話,你會發現在 main
函式定義之前,安插了 stdio.h 的內容。
至目前為止,常使用到的另一個前置處理器指令是 #define
,它本質上是個字串取代(或說為擴展、展開),例如:
#define LEN 10
int arr[LEN];
被定義的內容稱為巨集(Macro),gcc
編譯時指定 -E
,會產生以下內容,LEN
被展開為 10:
# 1 "test.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "test.c"
int arr[10];
#define
常用來定義一個模版,以取代經常撰寫的程式片段,例如最常見的教學範例是交換兩變數:
#include <stdio.h>
int main(void) {
int x = 10;
int y = 20;
printf("%d %d\n", x, y);
{
int temp = x;
x = y;
y = temp;
}
swap(x, y)
printf("%d %d\n", x, y);
return 0;
}
temp
定義在區塊之中,因此不為區塊外所見,可以將其定義為模版:
#include <stdio.h>
#define swap(a, b) { \
int temp = a; \
a = b; \
b = temp; \
}
int main(void) {
int x = 10;
int y = 20;
printf("%d %d\n", x, y);
swap(x, y)
printf("%d %d\n", x, y);
return 0;
}
#define
的內容跨越多行時,每行結尾必須使用 \
,可以在 swap(x, y)
之後加上分號,這會令其看來像是函式呼叫,實際上是展開了 swap
的內容後加上分號,也會是合法的程式碼罷了,類似地,swap
的定義看來像是定義函式,實際上那對大括號只是定義了陳述句區塊,而不是函式區塊。
如果上例定義巨集時不加上大括號會如何呢?
#include <stdio.h>
#define swap(a, b) \
int temp = a; \
a = b; \
b = temp; \
int main(void) {
int x = 10;
int y = 20;
printf("%d %d\n", x, y);
swap(x, y)
printf("%d %d\n", x, y);
return 0;
}
就以上來說,結果是正確的,只不過 main
範疇中多了個 temp
變數,也就是說,如果同一範疇內也有 temp
變數,編譯就會失敗,另一個問題是以下也會編譯失敗:
#include <stdio.h>
#define swap(a, b) \
int temp = a; \
a = b; \
b = temp; \
int main(void) {
int x = 10;
int y = 20;
printf("%d %d\n", x, y);
if(x > y)
swap(x, y)
printf("%d %d\n", x, y);
return 0;
}
因為 if
的部份展開後會是:
if(x > y)
int temp = x;
x = y;
y = temp;
也就是 temp
只有 if
中可見,y = temp
該行也就編譯失敗了:
if(x > y)
int temp = x;
x = y;
y = temp;
如果是一開始有加上大括號的 swap
巨集就不會有問題:
if(x > y) {
int temp = x;
x = y;
y = temp;
}
#define
只是文字替代,因此要小心項目展開後計算先後順序的問題:
#include <stdio.h>
#define pow(a) a * a
int main(void) {
int x = 10;
printf("%d\n", pow(x));
printf("%d\n", pow(x + x));
return 0;
}
pow
目的是計算二次方,pow(x + x)
預期結果應該是 400,實際上顯示會是 120,因為展開後會是 x + x * x + x
,為了避免這個問題,可以在定義巨集時,將輸入項目加上括號:
#include <stdio.h>
#define pow(a) (a) * (a)
int main(void) {
int x = 10;
printf("%d\n", pow(x)); // (x) * (x)
printf("%d\n", pow(x + x)); // (x + x) * (x + x)
return 0;
}
#define
的輸入項目要避免副作用,例如:
#include <stdio.h>
#define pow(a) (a) * (a)
int main(void) {
int x = 10;
printf("%d\n", pow(x++));
return 0;
}
你覺得結果應該會是多少呢?若覺得是 100 就錯了,因為 pow(x++)
會被展開為 (x++) * (x++)
,結果會是 110;別在巨集中重複使用輸入項目,雖然可以解決問題,然而這有時無法做到,因此最重要的是記得,使用巨集時,輸入項目要避免副作用,上例應該寫為以下:
#include <stdio.h>
#define pow(a) (a) * (a)
int main(void) {
int x = 10;
printf("%d\n", pow(x));
x++;
return 0;
}
這就是為何有些開發者認為,應該避免使用巨集的原因,因為撰寫不易、除錯不易,然而使用上又容易出錯;然而有些功能又只有巨集辦得到,C 語言本身的標準實際上也包含了一些以巨集提供的功能,只能說巨集是把雙面刃、必要之惡了。
#define
用來定義巨集,相對地,#undef
用來取消巨集。
C 語言本身預先定義了 __STDC__
、__LINE__
等名稱,可以在〈Replacing text macros〉找到,例如,可以透過 __FILE__
、__LINE__
來寫個簡單的除錯資訊:
#include <stdio.h>
int main(void) {
int x = 10;
int y = 20;
fprintf(stderr, "(%s:%d) %s %d\n", __FILE__, __LINE__, "Shit happen!", 1);
return 0;
}
將 fprintf
定義為巨集是個不錯的主意,可以簡化程式的撰寫:
#include <stdio.h>
#define debug(fmt, ...) { \
fprintf(stderr, "(%s:%d) "fmt"\n", __FILE__, __LINE__, ##__VA_ARGS__); \
}
int main(void) {
debug("%s %d", "Shit happen!", 1);
return 0;
}
...
在巨集中表示其餘的項目,後續可以使用 __VA_ARGS__
來代表;#
會將項目加上雙引號含括,因此 #__VA_ARGS__
的話,表示將其餘項目展開為字串。
##
的話是合併項目,例如若項目是 a
與 b
,巨集中撰寫 ab
是不會分別展開的,因為項目必須使用空白區隔,這時可以撰寫 a##b
,這麼一來,a
與 b
會分別展開後合併,例如若 a
為 12、b
為 34,那麼 a##b
就會是 1234。
如果 ##
出現在逗號之後,有些編譯器(例如 gcc
)會在 __VAR_ARGS__
為空時,自動移除逗號,上面的範例若將 ##
拿掉,debug
時若沒有指定 fmt
外的引數,展開後編譯就會出錯。
那為什麼不把 debug
定義為函式就好,而是要定義為巨集?同樣的疑問應該也會發生在先前的 swap
、pow
巨集,畢竟它們也可以定義為函式!
在過去也許有個好理由將 swap
、pow
等定義為巨集:「不會產生函式呼叫,比較有效率」。不過在不用這麼斤斤計較的場合,將 swap
、pow
等定義為巨集的價值不大。
巨集的本質是文字替換,如果經常寫出某個 C 語言片段,而該片段不適合封裝為函式,或者封裝為函式時使用上突冗,才是適用巨集的場合,例如方才的 debug
定義為函式會比較麻煩,因為得使用到不定長度引數、字串串接等,相對來說,定義巨集反而容易得多,另一個情況是循序迭代陣列,這可以參考〈foreach 與陣列〉。
前置處理指令中,還有 #if
、#endif
、#ifdef
、#ifndef
、#elif
、#else
、#endif
,可用來判定巨集是否存在,根據條件進行不同的程式碼含括。例如:
#include <stdio.h>
#define __DEBUG__
#define debug(fmt, ...) { \
fprintf(stderr, "(%s:%d) "fmt"\n", __FILE__, __LINE__, ##__VA_ARGS__); \
}
int main(void) {
#ifdef __DEBUG__
debug("%s %d", "Shit happen!", 1);
#endif
return 0;
}
只要在 __DEBUG__
有定義的情況下,debug("%s %d", "Shit happen!", 1)
該行才會被納入原始碼,而後進行編譯的動作,如此一來,就可以透過 __DEBUG__
是否有定義,來決定要不要包含除錯資訊。
在〈Conditional inclusion〉有個範例,可以看到 defined
以及條件式中還可以進行簡單的運算:
#define ABCD 2
#include <stdio.h>
int main(void)
{
#ifdef ABCD
printf("1: yes\n");
#else
printf("1: no\n");
#endif
#ifndef ABCD
printf("2: no1\n");
#elif ABCD == 2
printf("2: yes\n");
#else
printf("2: no2\n");
#endif
#if !defined(DCBA) && (ABCD < 2*4-3)
printf("3: yes\n");
#endif
}