C語言預處理命令詳解
本文參考諸多資料,詳細介紹常用的幾種預處理功能。因成文較早,資料來源大多已不可考證,敬請諒解。全文字數2萬,閱讀時間50分鐘,建議先收藏。
一 前言
預處理(或稱預編譯)是指在進行編譯的第一遍掃描(詞法掃描和語法分析)之前所作的工作。預處理指令指示在程式正式編譯前就由編譯器進行的操作,可放在程式中任何位置。
預處理是C語言的一個重要功能,它由預處理程式負責完成。當對一個原始檔進行編譯時,系統將自動引用預處理程式對源程式中的預處理部分作處理,處理完畢自動進入對源程式的編譯。
C語言提供多種預處理功能,主要處理#開始的預編譯指令,如巨集定義(#define)、檔案包含(#include)、條件編譯(#ifdef)等。合理使用預處理功能編寫的程式便於閱讀、修改、移植和除錯,也有利於模組化程式設計。
二 巨集定義
C語言源程式中允許用一個識別符號來表示一個字串,稱為“巨集”。被定義為巨集的識別符號稱為“巨集名”。在編譯預處理時,對程式中所有出現的巨集名,都用巨集定義中的字串去代換,這稱為巨集替換或巨集展開。
巨集定義是由源程式中的巨集定義命令完成的。巨集替換是由預處理程式自動完成的。
在C語言中,巨集定義分為有引數和無引數兩種。下面分別討論這兩種巨集的定義和呼叫。
2.1 無參巨集定義
無參巨集的巨集名後不帶引數。其定義的一般形式為:
#define 識別符號 字串
其中,“#”表示這是一條預處理命令(以#開頭的均為預處理命令)。“define”為巨集定義命令。“識別符號”為符號常量,即巨集名。“字串”可以是常數、表示式、格式串等。
巨集定義用巨集名來表示一個字串,在巨集展開時又以該字串取代巨集名。這只是一種簡單的文字替換,預處理程式對它不作任何檢查。如有錯誤,只能在編譯已被巨集展開後的源程式時發現。
注意理解巨集替換中“換”的概念,即在對相關命令或語句的含義和功能作具體分析之前就要進行文字替換。
【例1】定義常量:
#define MAX_TIME 1000
若在程式裡面寫if(time < MAX_TIME){…},則編譯器在處理該程式碼前會將MAX_TIME替換為1000。
注意,這種情況下使用const定義常量可能更好,如const int MAX_TIME = 1000;。因為const常量有資料型別,而巨集常量沒有資料型別。編譯器可以對前者進行型別安全檢查,而對後者只進行簡單的字元文字替換,沒有型別安全檢查,並且在字元替換時可能會產生意料不到的錯誤。
【例2】反例:
#define pint (int*)
pint pa, pb;
本意是定義pa和pb均為int型指標,但實際上變成int* pa,pb;。pa是int型指標,而pb是int型變數。本例中可用typedef來代替define,這樣pa和pb就都是int型指標了。
因為巨集定義只是簡單的字串代換,在預處理階段完成,而typedef是在編譯時處理的,它不是作簡單的代換,而是對型別說明符重新命名,被命名的識別符號具有型別定義說明的功能。
typedef的具體說明見附錄6.4。
無參巨集注意事項:
巨集名一般用大寫字母表示,以便於與變數區別。巨集定義末尾不必加分號,否則連分號一併替換。巨集定義可以巢狀。
可用#undef命令終止巨集定義的作用域。
使用巨集可提高程式通用性和易讀性,減少不一致性,減少輸入錯誤和便於修改。如陣列大小常用巨集定義。預處理是在編譯之前的處理,而編譯工作的任務之一就是語法檢查,預處理不做語法檢查。巨集定義寫在函式的花括號外邊,作用域為其後的程式,通常在檔案的最開頭。字串" "中永遠不包含巨集,否則該巨集名當字串處理。
巨集定義不分配記憶體,變數定義分配記憶體。
2.2 帶參巨集定義
C語言允許巨集帶有引數。在巨集定義中的引數稱為形式引數,在巨集呼叫中的引數稱為實際引數。
對帶引數的巨集,在呼叫中,不僅要巨集展開,而且要用實參去代換形參。
帶參巨集定義的一般形式為:
#define 巨集名(形參表) 字串
在字串中含有各個形參。
帶參巨集呼叫的一般形式為:
巨集名(實參表);
在巨集定義中的形參是識別符號,而巨集呼叫中的實參可以是表示式。
在帶參巨集定義中,形參不分配記憶體單元,因此不必作型別定義。而巨集呼叫中的實參有具體的值,要用它們去代換形參,因此必須作型別說明,這點與函式不同。函式中形參和實參是兩個不同的量,各有自己的作用域,呼叫時要把實參值賦予形參,進行“值傳遞”。而在帶參巨集中只是符號代換,不存在值傳遞問題。
【例3】
#define INC(x) x+1 //巨集定義
y = INC(5); //巨集呼叫
在巨集呼叫時,用實參5去代替形參x,經預處理巨集展開後的語句為y=5+1。
【例4】反例:
#define SQ(r) r*r
上述這種實參為表示式的巨集定義,在一般使用時沒有問題;但遇到如area=SQ(a+b);時就會出現問題,巨集展開後變為area=a+b*a+b;,顯然違背本意。
相比之下,函式呼叫時會先把實參表示式的值(a+b)求出來再賦予形參r;而巨集替換對實參表示式不作計算直接地照原樣代換。因此在巨集定義中,字串內的形參通常要用括號括起來以避免出錯。
進一步地,考慮到運算子優先順序和結合性,遇到area=10/SQ(a+b);時即使形參加括號仍會出錯。因此,還應在巨集定義中的整個字串外加括號,
綜上,正確的巨集定義是#define SQ® (®*®),即巨集定義時建議所有的層次都要加括號。
【例5】帶參函式和帶參巨集的區別:
#define SQUARE(x) ((x)*(x))
int Square(int x){
return (x * x); //未考慮溢位保護
}
int main(void){
int i = 1;
while(i <= 5)
printf("i = %d, Square = %d\n", i, Square(i++));
int j = 1;
while(j <= 5)
printf("j = %d, SQUARE = %d\n", j, SQUARE(j++));
return 0;
}
執行後輸出如下:
i = 2, Square = 1
i = 3, Square = 4
i = 4, Square = 9
i = 5, Square = 16
i = 6, Square = 25
j = 3, SQUARE = 1
j = 5, SQUARE = 9
j = 7, SQUARE = 25
本例意在說明,把同一表示式用函式處理與用巨集處理兩者的結果有可能是不同的。
呼叫Square函式時,把實參i值傳給形參x後自增1,再輸出函式值。因此迴圈5次,輸出1~5的平方值。呼叫SQUARE巨集時,SQUARE(j++)被代換為((j++)(j++))。在第一次迴圈時,表示式中j初值為1,兩者相乘的結果為1。相乘後j自增兩次變為3,因此表示式中第二次相乘時結果為33=9。同理,第三次相乘時結果為5*5=25,並在此次迴圈後j值變為7,不再滿足迴圈條件,停止迴圈。
從以上分析可以看出函式呼叫和巨集呼叫二者在形式上相似,在本質上是完全不同的。
帶參巨集注意事項:
-
巨集名和形參表的括號間不能有空格。
-
巨集替換隻作替換,不做計算,不做表示式求解。
-
函式呼叫在編譯後程序執行時進行,並且分配記憶體。巨集替換在編譯前進行,不分配記憶體。
-
函式只有一個返回值,利用巨集則可以設法得到多個值。
-
巨集展開使源程式變長,函式呼叫不會。
-
巨集展開不佔用執行時間,只佔編譯時間,函式呼叫佔執行時間(分配記憶體、保留現場、值傳遞、返回值)。
-
為防止無限制遞迴展開,當巨集呼叫自身時,不再繼續展開。
如:#define TEST(x) (x + TEST(x))被展開為1 + TEST(1)。
2.3 實踐用例
包括基本用法(及技巧)和特殊用法(#和##等)。
#define可以定義多條語句,以替代多行的程式碼,但應注意替換後的形式,避免出錯。巨集定義在換行時要加上一個反斜槓”\”,而且反斜槓後面直接回車,不能有空格。
2.3.1 基本用法
- 定義常量:
#define PI 3.1415926
將程式中出現的PI全部換成3.1415926。
- 定義表示式:
#define M (y*y+3*y)
編碼時所有的表示式(yy+3y)都可由M代替,而編譯時先由預處理程式進行巨集替換,即用(yy+3y)表示式去置換所有的巨集名M,然後再進行編譯。
注意,在巨集定義中表達式(yy+3y)兩邊的括號不能少,否則可能會發生錯誤。如s=3M+4M在預處理時經巨集展開變為s=3*(yy+3y)+4*(yy+3y),如果巨集定義時不加括號就展開為s=3yy+3y+4yy+3y,顯然不符合原意。因此在作巨集定義時必須十分注意。應保證在巨集替換之後不發生錯誤。
- 得到指定地址上的一個位元組或字:
#define MEM_B(x) (*((char *)(x)))
#define MEM_W(x) (*((short *)(x)))
- 求最大值和最小值:
#define MAX(x, y) (((x) > (y)) ? (x) : (y))
#define MIN(x, y) (((x) < (y)) ? (x) : (y))
以後使用MAX (x,y)或MIN (x,y),就可分別得到x和y中較大或較小的數。
但這種方法存在弊病,例如執行MAX(x++, y)時,x++被執行多少次取決於x和y的大小;當巨集引數為函式也會存在類似的風險。所以建議用行內函數而不是這種方法提高速度。不過,雖然存在這樣的弊病,但巨集定義非常靈活,因為x和y可以是各種資料型別。
以下給出MAX巨集的兩個安全版本(源自linux/kernel.h):
#define MAX_S(x, y) ({ \
const typeof(x) _x = (x); \
const typeof(y) _y = (y); \
(void)(&_x == &_y); \
_x > _y ? _x : _y; })
#define TMAX_S(type, x, y) ({ \
type _x = (x); \
type _y = (y); \
_x > _y ? _x: _y; })
Gcc編譯器將包含在圓括號和大括號雙層括號內的複合語句看作是一個表示式,它可出現在任何允許表示式的地方;複合語句中可宣告區域性變數,判斷迴圈條件等複雜處理。而表示式的最後一條語句必須是一個表示式,它的計算結果作為返回值。MAX_S和TMAX_S巨集內就定義區域性變數以消除引數副作用。
MAX_S巨集內(void)(&_x == &_y)語句用於檢查引數型別一致性。當引數x和y型別不同時,會產生” comparison of distinct pointer types lacks a cast”的編譯警告。
注意,MAX_S和TMAX_S巨集雖可避免參數副作用,但會增加記憶體開銷並降低執行效率。若使用者能保證巨集引數不存在副作用,則可選用普通定義(即MAX巨集)。
- 得到一個成員在結構體中的偏移量(lint 545告警表示"&用法值得懷疑",此處抑制該警告):
#define FPOS(type, field) \
/*lint -e545 */ ((int)&((type *)0)-> field) /*lint +e545 */
- 得到一個結構體中某成員所佔用的位元組數:
#define FSIZ(type, field) sizeof(((type *)0)->field)
- 按照LSB格式把兩個位元組轉化為一個字(word):
#define FLIPW(arr) ((((short)(arr)[0]) * 256) + (arr)[1])
- 按照LSB格式把一個字(word)轉化為兩個位元組:
#define FLOPW(arr, val) \
(arr)[0] = ((val) / 256); \
(arr)[1] = ((val) & 0xFF)
- 得到一個變數的地址:
#define B_PTR(var) ((char *)(void *)&(var))
#define W_PTR(var) ((short *)(void *)&(var))
- 得到一個字(word)的高位和低位位元組:
#define WORD_LO(x) ((char)((short)(x)&0xFF))
#define WORD_HI(x) ((char)((short)(x)>>0x8))
- 返回一個比X大的最接近的8的倍數:
#define RND8(x) ((((x) + 7) / 8) * 8)
- 將一個字母轉換為大寫或小寫:
#define UPCASE(c) (((c) >= 'a' && (c) <= 'z') ? ((c) + 'A' - 'a') : (c))
#define LOCASE(c) (((c) >= 'A' && (c) <= 'Z') ? ((c) + 'a' - 'A') : (c))
注意,UPCASE和LOCASE巨集僅適用於ASCII編碼(依賴於碼字順序和連續性),而不適用於EBCDIC編碼。
- 判斷字元是不是10進值的數字:
#define ISDEC(c) ((c) >= '0' && (c) <= '9')
- 判斷字元是不是16進值的數字:
#define ISHEX(c) (((c) >= '0' && (c) <= '9') ||\
((c) >= 'A' && (c) <= 'F') ||\
((c) >= 'a' && (c) <= 'f'))
- 防止溢位的一個方法:
#define INC_SAT(val) (val = ((val)+1 > (val)) ? (val)+1 : (val))
- 返回陣列元素的個數:
#define ARR_SIZE(arr) (sizeof((arr)) / sizeof((arr[0])))
- 對於IO空間對映在儲存空間的結構,輸入輸出處理:
#define INP(port) (*((volatile char *)(port)))
#define INPW(port) (*((volatile short *)(port)))
#define INPDW(port) (*((volatile int *)(port)))
#define OUTP(port, val) (*((volatile char *)(port)) = ((char)(val)))
#define OUTPW(port, val) (*((volatile short *)(port)) = ((short)(val)))
#define OUTPDW(port, val) (*((volatile int *)(port)) = ((int)(val)))
- 使用一些巨集跟蹤除錯:
ANSI標準說明了五個預定義的巨集名(注意雙下劃線),即:LINE、__FILE __、DATE、TIME、__STDC __。
若編譯器未遵循ANSI標準,則可能僅支援以上巨集名中的幾個,或根本不支援。此外,編譯程式可能還提供其它預定義的巨集名(如__FUCTION__)。
__DATE__巨集指令含有形式為月/日/年的串,表示原始檔被翻譯到程式碼時的日期;原始碼翻譯到目的碼的時間作為串包含在__TIME__中。串形式為時:分:秒。
如果實現是標準的,則巨集__STDC__含有十進位制常量1。如果它含有任何其它數,則實現是非標準的。
可以藉助上面的巨集來定義除錯巨集,輸出資料資訊和所在檔案所在行。如下所示:
#define MSG(msg, date) printf(msg);printf(“[%d][%d][%s]”,date,__LINE__,__FILE__)
- 用do{…}while(0)語句包含多語句防止錯誤:
#define DO(a, b) do{\
a+b;\
a++;\
}while(0)
- 實現類似“過載”功能
C語言中沒有swap函式,而且不支援過載,也沒有模板概念,所以對於每種資料型別都要寫出相應的swap函式,如:
IntSwap(int *, int *);
LongSwap(long *, long *);
StringSwap(char *, char *);
可採用巨集定義TSWAP (t,x,y)或SWAP(x, y)交換兩個整型或浮點引數:
#define TSWAP(type, x, y) do{ \
type _y = y; \
y = x; \
x = _y; \
}while(0)
#define SWAP(x, y) do{ \
x = x + y; \
y = x - y; \
x = x - y; \
}while(0)
int main(void){
int a = 10, b = 5;
TSWAP(int, a, b);
printf(“a=%d, b=%d\n”, a, b);
return 0;
}
- 1年中有多少秒(忽略閏年問題) :
#define SECONDS_PER_YEAR (60UL * 60 * 24 * 365)
該表示式將使一個16位機的整型數溢位,因此用長整型符號L告訴編譯器該常數為長整型數。
注意,不可定義為#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL,否則將產生(31536000)UL而非31536000UL,這會導致編譯報錯。
以下幾種寫法也正確:
#define SECONDS_PER_YEAR 60 * 60 * 24 * 365UL
#define SECONDS_PER_YEAR (60UL * 60UL * 24UL * 365UL)
#define SECONDS_PER_YEAR ((unsigned long)(60 * 60 * 24 * 365))
}
- 取消巨集定義:
#define [MacroName] [MacroValue] //定義巨集
#undef [MacroName] //取消巨集
巨集定義必須寫在函式外,其作用域為巨集定義起到源程式結束。如要終止其作用域可使用#undef命令:
#define PI 3.14159
int main(void){
//……
}
#undef PI
int func(void){
//……
}
表示PI只在main函式中有效,在func1中無效。
2.3.2 特殊用法
主要涉及C語言巨集裡#和##的用法,以及可變引數巨集。
2.3.2.1 字串化操作符#
在C語言的巨集中,#的功能是將其後面的巨集引數進行字串化操作(Stringfication),簡單說就是將巨集定義中的傳入引數名轉換成用一對雙引號括起來引數名字串。#只能用於有傳入引數的巨集定義中,且必須置於巨集定義體中的引數名前。例如:
#define EXAMPLE(instr) printf("The input string is:\t%s\n", #instr)
#define EXAMPLE1(instr) #instr
當使用該巨集定義時,example(abc)在編譯時將會展開成printf(“the input string is:\t%s\n”,“abc”);string str=example1(abc)將會展成string str=“abc”。
又如下面程式碼中的巨集:
define WARN_IF(exp) do{ \
if(exp) \
fprintf(stderr, "Warning: " #exp"\n"); \
} while(0)
則程式碼WARN_IF (divider == 0)會被替換為:
do{
if(divider == 0)
fprintf(stderr, "Warning" "divider == 0" "\n");
} while(0)
這樣,每次divider(除數)為0時便會在標準錯誤流上輸出一個提示資訊。
注意#巨集對空格的處理:
忽略傳入引數名前面和後面的空格。如str= example1( abc )會被擴充套件成 str=“abc”。
當傳入引數名間存在空格時,編譯器會自動連線各個子字串,每個子字串間只以一個空格連線。如str= example1( abc def)會被擴充套件成 str=“abc def”。
2.3.2.2 符號連線操作符##
##稱為連線符(concatenator或token-pasting),用來將兩個Token連線為一個Token。注意這裡連線的物件是Token就行,而不一定是巨集的變數。例如:
#define PASTER(n) printf( "token" #n " = %d", token##n)
int token9 = 9;
則執行PASTER(9)後輸出結果為token9 = 9。
又如要做一個選單項命令名和函式指標組成的結構體陣列,並希望在函式名和選單項命令名之間有直觀的、名字上的關係。那麼下面的程式碼就非常實用:
struct command{
char * name;
void (*function)(void);
};
#define COMMAND(NAME) {NAME, NAME##_command}
然後,就可用一些預先定義好的命令來方便地初始化一個command結構的陣列:
struct command commands[] = {
COMMAND(quit),
COMMAND(help),
//...
}
COMMAND巨集在此充當一個程式碼生成器的作用,這樣可在一定程度上減少程式碼密度,間接地也可減少不留心所造成的錯誤。
還可以用n個##符號連線n+1個Token,這個特性是#符號所不具備的。如:
#define LINK_MULTIPLE(a, b, c, d) a##_##b##_##c##_##d
typedef struct record_type LINK_MULTIPLE(name, company, position, salary);
這裡這個語句將展開為typedef struct record_type name_company_position_salary。
注意:
當用##連線形參時,##前後的空格可有可無。
連線後的實際引數名,必須為實際存在的引數名或是編譯器已知的巨集定義。
凡是巨集定義裡有用’#‘或’##'的地方,巨集引數是不會再展開。如:
#define STR(s) #s
#define CONS(a,b) int(a##e##b)
則printf(“int max: %s\n”, STR(INT_MAX))會被展開為printf(“int max: %s\n”, “INT_MAX”)。其中,變數INT_MAX為int型的最大值,其值定義在<climits.h>中。printf("%s\n", CONS(A, A))會被展開為printf("%s\n", int(AeA)),從而編譯報錯。
INT_MAX和A都不會再被展開,多加一層中間轉換巨集即可解決這個問題。加這層巨集是為了把所有巨集的引數在這層裡全部展開,那麼在轉換巨集裡的那一個巨集(如_STR)就能得到正確的巨集引數。
#define _STR(s) #s
#define STR(s) _STR(s) // 轉換巨集
#define _CONS(a,b) int(a##e##b)
#define CONS(a,b) _CONS(a,b) // 轉換巨集
則printf(“int max: %s\n”, STR(INT_MAX))輸出為int max: 0x7fffffff;而printf("%d\n", CONS(A, A))輸出為200。
這種分層展開的技術稱為巨集的Argument Prescan,參見附錄6.1。
2.3.2.3 字元化操作符@#
@#稱為字元化操作符(charizing),只能用於有傳入引數的巨集定義中,且必須置於巨集定義體的引數名前。作用是將傳入的單字元引數名轉換成字元,以一對單引號括起來。
#define makechar(x) #@x
a = makechar(b);
展開後變成a= ‘b’。
2.3.2.4 可變引數巨集
在C語言巨集中稱為Variadic Macro,即變參巨集。C99編譯器標準允許定義可變引數巨集(Macros with a Variable Number of Arguments),這樣就可以使用擁有可變引數表的巨集。
可變引數巨集的一般形式為:
#define DBGMSG(format, ...) fprintf (stderr, format, __VA_ARGS__)
省略號代表一個可以變化的引數表,變參必須作為引數表的最右一項出現。使用保留名__VA_ARGS__ 把引數傳遞給巨集。在呼叫巨集時,省略號被表示成零個或多個符號(包括裡面的逗號),一直到到右括號結束為止。當被呼叫時,在巨集體(macro body)中,那些符號序列集合將代替裡面的__VA_ARGS__識別符號。當巨集的呼叫展開時,實際的引數就傳遞給fprintf ()。
注意:可變引數巨集不被ANSI/ISO C++所正式支援。因此,應當檢查編譯器是否支援這項技術。
在標準C裡,不能省略可變引數,但卻可以給它傳遞一個空的引數,這會導致編譯出錯。因為巨集展開後,裡面的字串後面會有個多餘的逗號。為解決這個問題,GNU CPP中做了如下擴充套件定義:
#define DBGMSG(format, ...) fprintf (stderr, format, ##__VA_ARGS__)
若可變引數被忽略或為空,##操作將使編譯器刪除它前面多餘的逗號(否則會編譯出錯)。若巨集呼叫時提供了可變引數,編譯器會把這些可變引數放到逗號的後面。
同時,GCC還支援顯式地命名變參為args,如同其它引數一樣。如下格式的巨集擴充套件:
#define DBGMSG(format, args...) fprintf (stderr, format, ##args)
這樣寫可讀性更強,並且更容易進行描述。
用GCC和C99的可變引數巨集, 可以更方便地列印除錯資訊,如:
#ifdef DEBUG
#define DBGPRINT(format, args...) \
fprintf(stderr, format, ##args)
#else
#define DBGPRINT(format, args...)
#endif
這樣定義之後,程式碼中就可以用dbgprint了,例如dbgprint (“aaa [%s]”, FILE)。
結合第4節的“條件編譯”功能,可以構造出如下除錯列印巨集:
#ifdef LOG_TEST_DEBUG
/* OMCI除錯日誌巨集 */
//以10進位制格式日誌整型變數
#define PRINT_DEC(x) printf(#x" = %d\n", x)
#define PRINT_DEC2(x,y) printf(#x" = %d\n", y)
//以16進位制格式日誌整型變數
#define PRINT_HEX(x) printf(#x" = 0x%-X\n", x)
#define PRINT_HEX2(x,y) printf(#x" = 0x%-X\n", y)
//以字串格式日誌字串變數
#define PRINT_STR(x) printf(#x" = %s\n", x)
#define PRINT_STR2(x,y) printf(#x" = %s\n", y)
//日誌提示資訊
#define PROMPT(info) printf("%s\n", info)
//除錯定位資訊列印巨集
#define TP printf("%-4u - [%s<%s>]\n", __LINE__, __FILE__, __FUNCTION__);
//除錯跟蹤巨集,在待日誌資訊前附加日誌檔名、行數、函式名等資訊
#define TRACE(fmt, args...)\
do{\
printf("[%s(%d)<%s>]", __FILE__, __LINE__, __FUNCTION__);\
printf((fmt), ##args);\
}while(0)
#else
#define PRINT_DEC(x)
#define PRINT_DEC2(x,y)
#define PRINT_HEX(x)
#define PRINT_HEX2(x,y)
#define PRINT_STR(x)
#define PRINT_STR2(x,y)
#define PROMPT(info)
#define TP
#define TRACE(fmt, args...)
#endif
三 檔案包含
檔案包含命令列的一般形式為:
#include "檔名"
通常,該檔案是字尾名為"h"或"hpp"的標頭檔案。檔案包含命令把指定標頭檔案插入該命令列位置取代該命令列,從而把指定的檔案和當前的源程式檔案連成一個原始檔。
在程式設計中,檔案包含是很有用的。一個大程式可以分為多個模組,由多個程式設計師分別程式設計。有些公用的符號常量或巨集定義等可單獨組成一個檔案,在其它檔案的開頭用包含命令包含該檔案即可使用。這樣,可避免在每個檔案開頭都去書寫那些公用量,從而節省時間,並減少出錯。
對檔案包含命令要說明以下幾點:
包含命令中的檔名可用雙引號括起來,也可用尖括號括起來,如#include "common.h"和#include<math.h>。但這兩種形式是有區別的:使用尖括號表示在包含檔案目錄中去查詢(包含目錄是由使用者在設定環境時設定的include目錄),而不在當前原始檔目錄去查詢;
使用雙引號則表示首先在當前原始檔目錄中查詢,若未找到才到包含目錄中去查詢。使用者程式設計時可根據自己檔案所在的目錄來選擇某一種命令形式。
一個include命令只能指定一個被包含檔案,若有多個檔案要包含,則需用多個include命令。檔案包含允許巢狀,即在一個被包含的檔案中又可以包含另一個檔案。
四 條件編譯
一般情況下,源程式中所有的行都參加編譯。但有時希望對其中一部分內容只在滿足一定條件才進行編譯,也就是對一部分內容指定編譯的條件,這就是“條件編譯”。有時,希望當滿足某條件時對一組語句進行編譯,而當條件不滿足時則編譯另一組語句。
條件編譯功能可按不同的條件去編譯不同的程式部分,從而產生不同的目的碼檔案。這對於程式的移植和除錯是很有用的。
條件編譯有三種形式,下面分別介紹。
4.1 #ifdef形式
#ifdef 識別符號 (或#if defined識別符號)
程式段1
#else
程式段2
#endif
如果識別符號已被#define命令定義過,則對程式段1進行編譯;否則對程式段2進行編譯。如果沒有程式段2(它為空),#else可以沒有,即可以寫為:
#ifdef 識別符號 (或#if defined識別符號)
程式段
#endif
這裡的“程式段”可以是語句組,也可以是命令列。這種條件編譯可以提高C源程式的通用性。
【例6】
#define NUM OK
int main(void){
struct stu{
int num;
char *name;
char sex;
float score;
}*ps;
ps=(struct stu*)malloc(sizeof(struct stu));
ps->num = 102;
ps->name = "Zhang ping";
ps->sex = 'M';
ps->score = 62.5;
#ifdef NUM
printf("Number=%d\nScore=%f\n", ps->num, ps->score); /*--Execute--*/
#else
printf("Name=%s\nSex=%c\n", ps->name, ps->sex);
#endif
free(ps);
return 0;
}
由於在程式中插入了條件編譯預處理命令,因此要根據NUM是否被定義過來決定編譯哪個printf語句。而程式首行已對NUM作過巨集定義,因此應對第一個printf語句作編譯,故執行結果是輸出了學號和成績。
程式首行定義NUM為字串“OK”,其實可為任何字串,甚至不給出任何字串,即#define NUM也具有同樣的意義。只有取消程式首行巨集定義才會去編譯第二個printf語句。
4.2 #ifndef 形式
#ifndef 識別符號
程式段1
#else
程式段2
#endif
如果識別符號未被#define命令定義過,則對程式段1進行編譯,否則對程式段2進行編譯。這與#ifdef形式的功能正相反。
“#ifndef 識別符號”也可寫為“#if !(defined 識別符號)”。
4.3 #if形式
#if 常量表達式
程式段1
#else
程式段2
#endif
如果常量表達式的值為真(非0),則對程式段1 進行編譯,否則對程式段2進行編譯。因此可使程式在不同條件下,完成不同的功能。
【例7】輸入一行字母字元,根據需要設定條件編譯,使之能將字母全改為大寫或小寫字母輸出。
#define CAPITAL_LETTER 1
int main(void){
char szOrig[] = "C Language", cChar;
int dwIdx = 0;
while((cChar = szOrig[dwIdx++]) != '\0')
{
#if CAPITAL_LETTER
if((cChar >= 'a') && (cChar <= 'z')) cChar = cChar - 0x20;
#else
if((cChar >= 'A') && (cChar <= 'Z')) cChar = cChar + 0