目錄表

國立屏東大學 資訊工程學系 程式設計(二)

20. 前置處理指令


前置處理係指在編譯之前所進行的程式碼處理動作,如figure 1所示,原始程式在編譯前可先經前置處理器(Preprocessor)進行程式碼的修改,然後才交由編譯器進行真正的編譯動作,最後才得到編譯好的目的檔(也就是可執行檔)。

Fig. 1: 前置處理流程

同學通常不會發覺這個前置處理的動作,因為它是在我們對程式碼下達編譯命令時,和編譯的動作一併完成的。在C語言的程式碼中,存在以下三種前置處理的指令:

在開始介紹這些前置處理指令前,先說明相關的規定:

20.1 巨集定義(Macro Definition)

所謂的巨集(Macro)是一些程式碼的集合,由一個單一的巨集名稱所代表。一個已定義的巨集,可以在程式碼中用其名稱代表其程式碼的集合。就好比有時我們會以一些縮寫或代號,來替代較長的文字敘述,比方說我們可以用「USA」代替「The United States of American」一樣。不過在C語言中的巨集可以提供的功用遠超過縮寫的功用,我們將在以下的內容中加以介紹。

20.1.1 簡單巨集(Simple Macros)

簡單巨集顧名思義就是其定義內容較為單純的巨集(其實我們已在第6單元的常數定義中使用過),其語法如下:

#define identifier replacement-list

其中identifier就是巨集的名稱,replacement-list則是我們要用以替換的內容。例如:

#define PI 3.1415

<note> 在定義簡單巨集時,常見的錯誤是加入了「=」與「;」。例如

#define PI=3.1415
或者
#define PI 3.1415;
</note>

這種型式的巨集,因為其用途多是將特定的數值以具有意義的名稱來代替,因此又稱為「具意義的常數(manifest constants)定義」,或簡稱為「常數定義」。在程式碼中使用常數定義巨集有以下的好處:

20.1.2 參數式巨集(Parameterized Macros)

參數式巨集可以讓巨集接收參數,在替換時能依參數的內容動態地產生不同內容,其語法如下:

#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__);

20.2 檔案引入(File Inclusion)

檔案引入即為我們時常使用的「#include」用以將所需的檔案載入。

20.3 條件式編譯(Conditional Compilation)

我們可以將特別的程式碼標示在「#if」與「#endif」這兩個前置處理指令中,依「#if」的條件決定在編譯時是否要涵蓋在內。通常會先定義一個非0的常數做為條件,例如:

#define DEBUG 1
 
...
 
#if DEBUG
printf("value of x = %d\n", x);
printf("value of y = %d\n", y);
#endif
 
...

20.3.1 defined運算子

在前置處理指令中,除了「#」與「##」外,還有一個運算子「defined」。「defined」通常與「#if」搭配使用,例如:

#if defined(DEBUG)
 
或者
 
#if defined DEBUG

再搭配「-D」的編譯器參數,就可以分別編譯除錯版與發佈版的程式,例如:

[04:16 user@ws home] cc -DDEBUG someprog.c

20.3.2 #ifdef/#ifndef

這兩個指令其實等同於「#if defined」與「#if defined !」。

20.3.3 #elif/#else

如同「if]敘述一樣,「#if」與「#endif」前置處理指令也可以搭配「#elif」與「#else」指令,進行更複雜的條件式編譯。

20.4 inline函式

當我們在函式定義的前面加以一個「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;
}