國立屏東大學 資訊工程學系 程式設計(二)
前置處理係指在編譯之前所進行的程式碼處理動作,如figure 1所示,原始程式在編譯前可先經前置處理器(Preprocessor)進行程式碼的修改,然後才交由編譯器進行真正的編譯動作,最後才得到編譯好的目的檔(也就是可執行檔)。
同學通常不會發覺這個前置處理的動作,因為它是在我們對程式碼下達編譯命令時,和編譯的動作一併完成的。在C語言的程式碼中,存在以下三種前置處理的指令:
在開始介紹這些前置處理指令前,先說明相關的規定:
所謂的巨集(Macro)是一些程式碼的集合,由一個單一的巨集名稱所代表。一個已定義的巨集,可以在程式碼中用其名稱代表其程式碼的集合。就好比有時我們會以一些縮寫或代號,來替代較長的文字敘述,比方說我們可以用「USA」代替「The United States of American」一樣。不過在C語言中的巨集可以提供的功用遠超過縮寫的功用,我們將在以下的內容中加以介紹。
簡單巨集顧名思義就是其定義內容較為單純的巨集(其實我們已在第6單元的常數定義中使用過),其語法如下:
#define identifier replacement-list
其中identifier就是巨集的名稱,replacement-list則是我們要用以替換的內容。例如:
#define PI 3.1415
<note> 在定義簡單巨集時,常見的錯誤是加入了「=」與「;」。例如
#define PI=3.1415 或者 #define PI 3.1415;</note>
這種型式的巨集,因為其用途多是將特定的數值以具有意義的名稱來代替,因此又稱為「具意義的常數(manifest constants)定義」,或簡稱為「常數定義」。在程式碼中使用常數定義巨集有以下的好處:
參數式巨集可以讓巨集接收參數,在替換時能依參數的內容動態地產生不同內容,其語法如下:
#define identifier(x1, x2, ..., xn) replacement-list
其中在identifier後所接的「(x1, x2, …, xn)」即為該巨集的參數,與identifier間不可以有空白隔開。讓我們看看下面的例子:
#define MAX(x,y) ((x)>(y)?(x):(y)) #define IS_EVEN(n) ((n)%2==0) #define TOUPPER(c) ('a'<=(c)&&(c)<='z'?(c)-'a'+'A':(c)) #define newline() printf("\n")
上述的例子很像是將函式定義為巨集,好處是省略了函式呼叫所須的參數傳遞與記憶體配置等成本。有以下的注意事項:
#include <stdio.h> #define MAX(x,y) x>y?x:y int main() { int i=2; int j=-5; printf("%d\n",MAX(i,MAX(3,6))); // 2>3>6? 3: 6?2: 3>6? 3:6 return 0; }
又或者
#include <stdio.h> #define TWICE(x) 2*x int main() { int i=2; int j=-5; printf("%d\n",TWICE(3+5)); return 0; }
發現了嗎?其中的「TWICE(3+5)」被代換為「2*3+5」,這明顯是個錯誤。
巨集中的參數若冠以「#」運算子,則會將該參數轉換為字串常值(string literal),其內容為該參數的名稱。我們將「#」運算子稱為「字串化(stringization)」運算子。請參考以下的範例:
#define PRINT_INT(n) printf(#n "=%d\n", n)
在這個例子中,若在程式碼中出現「PRINT(i/j);」則會被代換為「printf(“i/j” “=%d\n”, i/j);」,其中#n所轉換成的字串常值以紅色標示。這裡要特別說明的是,編譯器會自動將連續兩個字串常值合併為一個。
這個「##」運算子被稱為「字符拼接(token-pasting)運算子」,可用以將參數的值與其它部份連接起來,例如:
#define MakeVar(n) var##n
在程式中,「MakeVar(1)」就會變成「var1」、「MakeVar(2)」變成「var2」等,依此類推,將其應用在變數的宣告,則以下的程式碼:
int MakeVar(1), MakeVar(2);
會轉換產生
int var1, var2;
我們也可以利用這個運算子,來設計可產生適用於不同型態的函式樣板(template),例如:
#define GENERIC_MAX(type) \
type type##_max(type x, type y) \
{ \
return x > y ? x : y; \
}
如果我們在程式中使用該巨集「GENERIC_MAX(double)」則可以產生以下的程式碼:
double double_max(double x, double y) { return x > y ? x : y; }
在程式碼中,如果使用「#undef identifier」,則是將其定義移除。
C語言已預先定義了一些巨集,例如:
我們可以在程式中使用這些預先定義好的巨集,例如:
printf("%d %s %s %s %d\n", __LINE__, __FILE__, __TIME__, __DATE__, __STDC__);
檔案引入即為我們時常使用的「#include」用以將所需的檔案載入。
我們可以將特別的程式碼標示在「#if」與「#endif」這兩個前置處理指令中,依「#if」的條件決定在編譯時是否要涵蓋在內。通常會先定義一個非0的常數做為條件,例如:
#define DEBUG 1 ... #if DEBUG printf("value of x = %d\n", x); printf("value of y = %d\n", y); #endif ...
在前置處理指令中,除了「#」與「##」外,還有一個運算子「defined」。「defined」通常與「#if」搭配使用,例如:
#if defined(DEBUG) 或者 #if defined DEBUG
再搭配「-D」的編譯器參數,就可以分別編譯除錯版與發佈版的程式,例如:
[04:16 user@ws home] cc -DDEBUG someprog.c
這兩個指令其實等同於「#if defined」與「#if defined !」。
如同「if]敘述一樣,「#if」與「#endif」前置處理指令也可以搭配「#elif」與「#else」指令,進行更複雜的條件式編譯。
當我們在函式定義的前面加以一個「inline」的修飾字時,在編譯時,其函式呼叫就會改以其函式內容代替,在執行時省去了函式呼叫的跳躍與返回的成本。inline的方式類似於巨集定義,但較具彈性。
#include <stdio.h> inline int foo(int x, int y) { if(x>y) return x*y; else return x+y; } int main() { printf("%d\n", foo(3,5)); return 0; }