預先處理過程掃描原始碼,對其進行初步的轉換,產生新的原始碼提供給編譯器。可見預先處理過程先於編譯器對原始碼進行處理。
在C語言中,並沒有任何內在的機制來完成如下一些功能:在編譯時間包含其他源檔案、定義宏、根據條件決定編譯時間是否包含某些代碼。要完成這些工作,就需要使用預先處理程式。儘管在目前絕大多數編譯器都包含了預先處理程式,但通常認為它們是獨立於編譯器的。預先處理過程讀入原始碼,檢查包含預先處理指令的語句和宏定義,並對原始碼進行響應的轉換。預先處理過程還會刪除程式中的注釋和多餘的空白字元。
預先處理指令是以#號開頭的程式碼。#號必須是該行除了任何空白字元外的第一個字元。#後是指令關鍵字,在關鍵字和#號之間允許存在任意個數的空白字元。整行語句構成了一條預先處理指令,該指令將在編譯器進行編譯之前對原始碼做某些轉換。下面是部分預先處理指令:
指令用途
#空指令,無任何效果
#include包含一個原始碼檔案
#define定義宏
#undef取消已定義的宏
#if如果給定條件為真,則編譯下面代碼
#ifdef如果宏已經定義,則編譯下面代碼
#ifndef如果宏沒有定義,則編譯下面代碼
#elif如果前面的#if給定條件不為真,當前條件為真,則編譯下面代碼
#endif結束一個#if……#else條件編譯塊
#error停止編譯並顯示錯誤資訊
一、檔案包含
#include預先處理指令的作用是在指令處展開被包含的檔案。包含可以是多重的,也就是說一個被包含的檔案中還可以包含其他檔案。標準C編譯器至少支援八重嵌套包含。
預先處理過程不檢查在轉換單元中是否已經包含了某個檔案並阻止對它的多次包含。這樣就可以在多次包含同一個標頭檔時,通過給定編譯時間的條件來達到不同的效果。例如:
#defineAAA
#include"t.c"
#undefAAA
#include"t.c"
為了避免那些只能包含一次的標頭檔被多次包含,可以在標頭檔中用編譯時間條件來進行控制。例如:
/*my.h*/
#ifndefMY_H
#defineMY_H
……
#endif
在程式中包含標頭檔有兩種格式:
#include<my.h>
#include"my.h"
第一種方法是用角括弧把標頭檔括起來。這種格式告訴預先處理程式在編譯器內建的或外部庫的標頭檔中搜尋被包含的標頭檔。第二種方法是用雙引號把標頭檔括起來。這種格式告訴預先處理程式在當前被編譯的應用程式的原始碼檔案中搜尋被包含的標頭檔,如果找不到,再搜尋編譯器內建的標頭檔。
採用兩種不同包含格式的理由在於,編譯器是安裝在公用子目錄下的,而被編譯的應用程式是在它們自己的私人子目錄下的。一個應用程式既包含編譯器提供的公用標頭檔,也包含自訂的私人標頭檔。採用兩種不同的包含格式使得編譯器能夠在很多標頭檔中區別出一組公用的標頭檔。
二、宏
宏定義了一個代表特定內容的標識符。預先處理過程會把原始碼中出現的宏標識符替換成宏定義時的值。宏最常見的用法是定義代表某個值的全域符號。宏的第二種用法是定義帶參數的宏,這樣的宏可以象函數一樣被調用,但它是在調用語句處展開宏,並用調用時的實際參數來代替定義中的形式參數。
1.#define指令
#define預先處理指令是用來定義宏的。該指令最簡單的格式是:首先神明一個標識符,然後給出這個標識符代表的代碼。在後面的原始碼中,就用這些代碼來替代該標識符。這種宏把程式中要用到的一些全域值提取出來,賦給一些記憶標識符。
#defineMAX_NUM10
intarray[MAX_NUM];
for(i=0;i<MAX_NUM;i++)/*……*/
在這個例子中,對於閱讀該程式的人來說,符號MAX_NUM就有特定的含義,它代表的值給出了數組所能容納的最大元素數目。程式中可以多次使用這個值。作為一種約定,習慣上總是全部用大寫字母來定義宏,這樣易於把程式紅的宏標識符和一般變數標識符區別開來。如果想要改變數組的大小,只需要更改宏定義並重新編譯器即可。
宏表示的值可以是一個常量運算式,其中允許包括前面已經定義的宏標識符。例如:
#defineONE1
#defineTWO2
#defineTHREE(ONE+TWO)
注意上面的宏定義使用了括弧。儘管它們並不是必須的。但出于謹慎考慮,還是應該加上括弧的。例如:
six=THREE*TWO;
預先處理過程把上面的一行代碼轉換成:
six=(ONE+TWO)*TWO;
如果沒有那個括弧,就轉換成six=ONE+TWO*TWO;了。
宏還可以代表一個字串常量,例如:
#defineVERSION"Version1.0Copyright(c)2003"
2.帶參數的#define指令
帶參數的宏和函數調用看起來有些相似。看一個例子:
#defineCube(x)(x)*(x)*(x)
可以時任何數字運算式甚至函數調用來代替參數x。這裡再次提醒大家注意括弧的使用。宏展開後完全包含在一對括弧中,而且參數也包含在括弧中,這樣就保證了宏和參數的完整性。看一個用法:
intnum=8+2;
volume=Cube(num);
展開後為(8+2)*(8+2)*(8+2);
如果沒有那些括弧就變為8+2*8+2*8+2了。
下面的用法是不安全的:
volume=Cube(num++);
如果Cube是一個函數,上面的寫法是可以理解的。但是,因為Cube是一個宏,所以會產生副作用。這裡的擦書不是簡單的運算式,它們將產生意想不到的結果。它們展開後是這樣的:
volume=(num++)*(num++)*(num++);
很顯然,結果是10*11*12,而不是10*10*10;
那麼怎樣安全的使用Cube宏呢?必須把可能產生副作用的操作移到宏調用的外面進行:
intnum=8+2;
volume=Cube(num);
num++;
3.#運算子
出現在宏定義中的#運算子把跟在其後的參數轉換成一個字串。有時把這種用法的#稱為字串化運算子。例如:
#definePASTE(n)"adhfkj"#n
main()
{
printf("%s ",PASTE(15));
}
宏定義中的#運算子告訴預先處理程式,把原始碼中任何傳遞給該宏的參數轉換成一個字串。所以輸出應該是adhfkj15。
4.##運算子
##運算子用於把參數串連到一起。預先處理程式把出現在##兩側的參數合并成一個符號。看下面的例子:
#defineNUM(a,b,c)a##b##c
#defineSTR(a,b,c)a##b##c
main()
{
printf("%d ",NUM(1,2,3));
printf("%s ",STR("aa","bb","cc"));
}
最後程式的輸出為:
123
aabbcc
千萬別擔心,除非需要或者宏的用法恰好和手頭的工作相關,否則很少有程式員會知道##運算子。絕大多數程式員從來沒用過它。
三、條件編譯指令
條件編譯指令將決定那些代碼被編譯,而哪些是不被編譯的。可以根據運算式的值或者某個特定的宏是否被定義來確定編譯條件。
1.#if指令
#if指令檢測跟在製造另關鍵字後的常量運算式。如果運算式為真,則編譯後面的代碼,知道出現#else、#elif或#endif為止;否則就不編譯。
2.#endif指令
#endif用於終止#if預先處理指令。
#defineDEBUG0
main()
{
#ifDEBUG
printf("Debugging ");
#endif
printf("Running ");
}
由於程式定義DEBUG宏代表0,所以#if條件為假,不編譯後面的代碼直到#endif,所以程式直接輸出Running。
如果去掉#define語句,效果是一樣的。
3.#ifdef和#ifndef
#defineDEBUG
main()
{
#ifdefDEBUG
printf("yes ");
#endif
#ifndefDEBUG
printf("no ");
#endif
}
#ifdefined等價於#ifdef;#if!defined等價於#ifndef
4.#else指令
#else指令用於某個#if指令之後,當前面的#if指令的條件不為真時,就編譯#else後面的代碼。#endif指令將中指上面的條件塊。
#defineDEBUG
main()
{
#ifdefDEBUG
printf("Debugging ");
#else
printf("Notdebugging ");
#endif
printf("Running ");
}
5.#elif指令
#elif預先處理指令綜合了#else和#if指令的作用。
#defineTWO
main()
{
#ifdefONE
printf("1 ");
#elifdefinedTWO
printf("2 ");
#else
printf("3 ");
#endif
}
程式很好理解,最後輸出結果是2。
6.其他一些標準指令
#error指令將使編譯器顯示一條錯誤資訊,然後停止編譯。
#line指令可以改變編譯器用來指出警告和錯誤資訊的檔案號和行號。
#pragma指令沒有正式的定義。編譯器可以自訂其用途。典型的用法是禁止或允許某些煩人的警告資訊。
補充:
預先處理就是在進行編譯的第一遍詞法掃描和文法分析之前所作的工作。說白了,就是對源檔案進行編譯前,先對預先處理部分進行處理,然後對處理後的代碼進行編譯。這樣做的好處是,經過處理後的代碼,將會變的很精短。
關於預先處理命令中的檔案包含(#include),宏定義(#define),書上已經有了詳細的說明,在這裡就不詳述了。這裡主要是對條件編譯(#ifdef,#else,#endif,#if等)進行說明。以下分3種情況:
1:情況1:
#ifdef _XXXX
...程式段1...
#else
...程式段2...
#endif
這表明如果標識符_XXXX已被#define命令定義過則對程式段1進行編譯;否則對程式段2進行編譯。
例:
#define NUM
.............
.............
.............
#ifdef NUM
printf("之前NUM有過定義啦!:) ");
#else
printf("之前NUM沒有過定義!:( ");
#endif
}
如果程式開頭有#define NUM這行,即NUM有定義,碰到下面#ifdef NUM的時候,當然執行第一個printf。否則第二個printf將被執行。
我認為,用這種,可以很方便的開啟/關閉整個程式的某項特定功能。
2:情況2:
#ifndef _XXXX
...程式段1...
#else
...程式段2...
#endif
這裡使用了#ifndef,表示的是if not def。當然是和#ifdef相反的狀況(如果沒有定義了標識符_XXXX,那麼執行程式段1,否則執行程式段2)。例子就不舉了。
3:情況3:
#if 常量
...程式段1...
#else
...程式段2...
#endif
這裡表示,如果常量為真(非0,隨便什麼數字,只要不是0),就執行程式段1,否則執行程式段2。
我認為,這種方法可以將測試代碼加進來。當需要開啟測試的時候,只要將常量變1就好了。而不要測試的時候,只要將常量變0。