一 前言
預處理(或稱預編譯)是指在進行編譯的第一遍掃描(詞法掃描和語法分析)之前所作的工作。預處理指令指示在程序正式編譯前就由編譯器進行的操作,可放在程序中任何位置。
預處理是C語言的一個重要功能,它由預處理程序負責完成。當對一個源文件進行編譯時,系統將自動引用預處理程序對源程序中的預處理部分作處理,處理完畢自動進入對源程序的編譯。
C語言提供多種預處理功能,主要處理#開始的預編譯指令,如宏定義(#define)、文件包含(#include)、條件編譯(#ifdef)等。合理使用預處理功能編寫的程序便於閱讀、修改、移植和調試,也有利於模塊化程序設計。
本文參考諸多資料,詳細介紹常用的幾種預處理功能。因成文較早,資料來源大多已不可考,敬請諒解。
二 宏定義
C語言源程序中允許用一個標識符來表示一個字符串,稱為“宏”。被定義為宏的標識符稱為“宏名”。在編譯預處理時,對程序中所有出現的宏名,都用宏定義中的字符串去代換,這稱為宏替換或宏展開。
宏定義是由源程序中的宏定義命令完成的。宏替換是由預處理程序自動完成的。
在C語言中,宏定義分為有參數和無參數兩種。下面分別討論這兩種宏的定義和調用。
2.1 無參宏定義
無參宏的宏名後不帶參數。其定義的一般形式為:
#define 標識符 字符串
其中,“#”表示這是一條預處理命令(以#開頭的均為預處理命令)。“define”為宏定義命令。“標識符”為符號常量,即宏名。“字符串”可以是常數、表達式、格式串等。
宏定義用宏名來表示一個字符串,在宏展開時又以該字符串取代宏名。這只是一種簡單的文本替換,預處理程序對它不作任何檢查。如有錯誤,只能在編譯已被宏展開後的源程序時發現。
注意理解宏替換中“換”的概念,即在對相關命令或語句的含義和功能作具體分析之前就要進行文本替換。
【例1】定義常量:
1 #define MAX_TIME 1000
若在程序裡面寫if(time < MAX_TIME){.........},則編譯器在處理該代碼前會將MAX_TIME替換為1000。
注意,這種情況下使用const定義常量可能更好,如const int MAX_TIME = 1000;。因為const常量有數據類型,而宏常量沒有數據類型。編譯器可以對前者進行類型安全檢查,而對後者只進行簡單的字符文本替換,沒有類型安全檢查,並且在字符替換時可能會產生意料不到的錯誤。
【例2】反例:
1 #define pint (int*)
2 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】
1 #define INC(x) x+1 //宏定義
2 y = INC(5); //宏調用
在宏調用時,用實參5去代替形參x,經預處理宏展開後的語句為y=5+1。
【例4】反例:
1 #define SQ(r) r*r
上述這種實參為表達式的宏定義,在一般使用時沒有問題;但遇到如area=SQ(a+b);時就會出現問題,宏展開後變為area=a+b*a+b;,顯然違背本意。
相比之下,函數調用時會先把實參表達式的值(a+b)求出來再賦予形參r;而宏替換對實參表達式不作計算直接地照原樣代換。因此在宏定義中,字符串內的形參通常要用括號括起來以避免出錯。
進一步地,考慮到運算符優先級和結合性,遇到area=10/SQ(a+b);時即使形參加括號仍會出錯。因此,還應在宏定義中的整個字符串外加括號,
綜上,正確的宏定義是#define SQ(r) ((r)*(r)),即宏定義時建議所有的層次都要加括號。
【例5】帶參函數和帶參宏的區別:
復制代碼
1 #define SQUARE(x) ((x)*(x))
2 int Square(int x){
3 return (x * x); //未考慮溢出保護
4 }
5
6 int main(void){
7 int i = 1;
8 while(i <= 5)
9 printf("i = %d, Square = %d\n", i, Square(i++));
10
11 int j = 1;
12 while(j <= 5)
13 printf("j = %d, SQUARE = %d\n", j, SQUARE(j++));
14
15 return 0;
16 }
復制代碼
執行後輸出如下:
復制代碼
1 i = 2, Square = 1
2 i = 3, Square = 4
3 i = 4, Square = 9
4 i = 5, Square = 16
5 i = 6, Square = 25
6 j = 3, SQUARE = 1
7 j = 5, SQUARE = 9
8 j = 7, SQUARE = 25
復制代碼
本例意在說明,把同一表達式用函數處理與用宏處理兩者的結果有可能是不同的。
調用Square函數時,把實參i值傳給形參x後自增1,再輸出函數值。因此循環5次,輸出1~5的平方值。
調用SQUARE宏時,SQUARE(j++)被代換為((j++)*(j++))。在第一次循環時,表達式中j初值為1,兩者相乘的結果為1。相乘後j自增兩次變為3,因此表達式中第二次相乘時結果為3*3=9。同理,第三次相乘時結果為5*5=25,並在此次循環後j值變為7,不再滿足循環條件,停止循環。
從以上分析可以看出函數調用和宏調用二者在形式上相似,在本質上是完全不同的。
帶參宏注意事項:
宏名和形參表的括號間不能有空格。
宏替換只作替換,不做計算,不做表達式求解。
函數調用在編譯後程序運行時進行,並且分配內存。宏替換在編譯前進行,不分配內存。
宏的啞實結合不存在類型,也沒有類型轉換。
函數只有一個返回值,利用宏則可以設法得到多個值。
宏展開使源程序變長,函數調用不會。
宏展開不占用運行時間,只占編譯時間,函數調用占運行時間(分配內存、保留現場、值傳遞、返回值)。
為防止無限制遞歸展開,當宏調用自身時,不再繼續展開。如:#define TEST(x) (x + TEST(x))被展開為1 + TEST(1)。
2.3 實踐用例
包括基本用法(及技巧)和特殊用法(#和##等)。
#define可以定義多條語句,以替代多行的代碼,但應注意替換後的形式,避免出錯。宏定義在換行時要加上一個反斜槓”\”,而且反斜槓後面直接回車,不能有空格。
2.3.1 基本用法
1. 定義常量:
1 #define PI 3.1415926
將程序中出現的PI全部換成3.1415926。
2. 定義表達式:
1 #define M (y*y+3*y)
編碼時所有的表達式(y*y+3*y)都可由M代替,而編譯時先由預處理程序進行宏替換,即用(y*y+3*y)表達式去置換所有的宏名M,然後再進行編譯。
注意,在宏定義中表達式(y*y+3*y)兩邊的括號不能少,否則可能會發生錯誤。如s=3*M+4*M在預處理時經宏展開變為s=3*(y*y+3*y)+4*(y*y+3*y),如果宏定義時不加括號就展開為s=3*y*y+3*y+4*y*y+3*y,顯然不符合原意。因此在作宏定義時必須十分注意。應保證在宏替換之後不發生錯誤。
3. 得到指定地址上的一個字節或字:
1 #define MEM_B(x) (*((char *)(x)))
2 #define MEM_W(x) (*((short *)(x)))
4. 求最大值和最小值:
1 #define MAX(x, y) (((x) > (y)) ? (x) : (y))
2 #define MIN(x, y) (((x) < (y)) ? (x) : (y))
以後使用MAX (x,y)或MIN (x,y),就可分別得到x和y中較大或較小的數。
但這種方法存在弊病,例如執行MAX(a++, b)時,a++被執行多少次取決於a和b的大小!所以建議用內聯函數而不是這種方法提高速度。不過,雖然存在這樣的弊病,但宏定義非常靈活,因為a和b可以是各種數據類型。
5. 得到一個成員在結構體中的偏移量(lint 545告警表示"&用法值得懷疑",此處抑制該警告):
1 #define FPOS( type, field ) \
2 /*lint -e545 */ ((int)&((type *)0)-> field) /*lint +e545 */
6. 得到一個結構體中某成員所占用的字節數:
1 #define FSIZ(type, field) sizeof(((type *)0)->field)
7. 按照LSB格式把兩個字節轉化為一個字(word):
1 #define FLIPW(arr) ((((short)(arr)[0]) * 256) + (arr)[1])
8. 按照LSB格式把一個字(word)轉化為兩個字節:
1 #define FLOPW(arr, val ) \
2 (arr)[0] = ((val) / 256); \
3 (arr)[1] = ((val) & 0xFF)
9. 得到一個變量的地址:
1 #define B_PTR(var) ((char *)(void *)&(var))
2 #define W_PTR(var) ((short *)(void *)&(var))
10. 得到一個字(word)的高位和低位字節:
1 #define WORD_LO(x) ((char)((short)(x)&0xFF))
2 #define WORD_HI(x) ((char)((short)(x)>>0x8))
11. 返回一個比X大的最接近的8的倍數:
1 #define RND8(x) ((((x) + 7) / 8) * 8)
12. 將一個字母轉換為大寫:
1 #define UPCASE(c) (((c) >= 'a' && (c) <= 'z') ? ((c) - 0x20) : (c))
13. 判斷字符是不是10進值的數字:
1 #define ISDEC(c) ((c) >= '0' && (c) <= '9')
14. 判斷字符是不是16進值的數字:
1 #define ISHEX(c) (((c) >= '0' && (c) <= '9') ||\
2 ((c) >= 'A' && (c) <= 'F') ||\
3 ((c) >= 'a' && (c) <= 'f'))
15. 防止溢出的一個方法:
1 #define INC_SAT(val) (val = ((val)+1 > (val)) ? (val)+1 : (val))
16. 返回數組元素的個數:
1 #define ARR_SIZE(arr) (sizeof((arr)) / sizeof((arr[0])))
17. 對於IO空間映射在存儲空間的結構,輸入輸出處理:
1 #define INP(port) (*((volatile char *)(port)))
2 #define INPW(port) (*((volatile short *)(port)))
3 #define INPDW(port) (*((volatile int *)(port)))
4 #define OUTP(port, val) (*((volatile char *)(port)) = ((char)(val)))
5 #define OUTPW(port, val) (*((volatile short *)(port)) = ((short)(val)))
6 #define OUTPDW(port, val) (*((volatile int *)(port)) = ((int)(val)))
18. 使用一些宏跟蹤調試:
ANSI標准說明了五個預定義的宏名(注意雙下劃線),即:__LINE__、__FILE __、__DATE__、__TIME__、__STDC __。
若編譯器未遵循ANSI標准,則可能僅支持以上宏名中的幾個,或根本不支持。此外,編譯程序可能還提供其它預定義的宏名(如__FUCTION__)。
__DATE__宏指令含有形式為月/日/年的串,表示源文件被翻譯到代碼時的日期;源代碼翻譯到目標代碼的時間作為串包含在__TIME__中。串形式為時:分:秒。
如果實現是標准的,則宏__STDC__含有十進制常量1。如果它含有任何其它數,則實現是非標准的。
可以借助上面的宏來定義調試宏,輸出數據信息和所在文件所在行。如下所示:
1 #define MSG(msg, date) printf(msg);printf(“[%d][%d][%s]”,date,__LINE__,__FILE__)
19. 用do{…}while(0)語句包含多語句防止錯誤:
1 #define DO(a, b) do{\
2 a+b;\
3 a++;\
4 }while(0)
20. 實現類似“重載”功能
C語言中沒有swap函數,而且不支持重載,也沒有模板概念,所以對於每種數據類型都要寫出相應的swap函數,如:
1 IntSwap(int *, int *);
2 LongSwap(long *, long *);
3 StringSwap(char *, char *);
可采用宏定義SWAP(t,x,y)以交換t類型的兩個參數(要使用程序塊結構):
復制代碼
1 #define SWAP(t, x, y) do{\
2 t temp = *y;\
3 *y = *x;\
4 *x = temp;\
5 }while(0)
6
7 int main(void){
8 int a = 10, b = 5;
9 SWAP(int, &a, &b);
10 printf("a=%d, b=%d\n", a, b);
11 return 0;
12 }
復制代碼
21. 1年中有多少秒(忽略閏年問題) :
1 #define SECONDS_PER_YEAR (60UL * 60 * 24 * 365)
該表達式將使一個16位機的整型數溢出,因此用長整型符號L告訴編譯器該常數為長整型數。
注意,不可定義為#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL,否則將產生(31536000)UL而非31536000UL,這會導致編譯報錯。
以下幾種寫法也正確:
1 #define SECONDS_PER_YEAR 60 * 60 * 24 * 365UL
2 #define SECONDS_PER_YEAR (60UL * 60UL * 24UL * 365UL)
3 #define SECONDS_PER_YEAR ((unsigned long)(60 * 60 * 24 * 365))
22. 取消宏定義:
#define [MacroName] [MacroValue] //定義宏
#undef [MacroName] //取消宏
宏定義必須寫在函數外,其作用域為宏定義起到源程序結束。如要終止其作用域可使用#undef命令:
復制代碼
1 #define PI 3.14159
2 int main(void){
3 //……
4 }
5 #undef PI
6 int func(void){
7 //……
8 }
復制代碼
表示PI只在main函數中有效,在func1中無效。
2.3.2 特殊用法
主要涉及C語言宏裡#和##的用法,以及可變參數宏。
2.3.2.1 字符串化操作符#
在C語言的宏中,#的功能是將其後面的宏參數進行字符串化操作(Stringfication),簡單說就是將宏定義中的傳入參數名轉換成用一對雙引號括起來參數名字符串。#只能用於有傳入參數的宏定義中,且必須置於宏定義體中的參數名前。例如:
1 #define EXAMPLE(instr) printf("The input string is:\t%s\n", #instr)
2 #define EXAMPLE1(instr) #instr
當使用該宏定義時,example(abc)在編譯時將會展開成printf("the input string is:\t%s\n","abc");string str=example1(abc)將會展成string str="abc"。
又如下面代碼中的宏:
1 define WARN_IF(exp) do{ \
2 if(exp) \
3 fprintf(stderr, "Warning: " #exp"\n"); \
4 }while(0)
則代碼WARN_IF (divider == 0)會被替換為:
1 do{
2 if(divider == 0)
3 fprintf(stderr, "Warning" "divider == 0" "\n");
4 }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就行,而不一定是宏的變量。例如:
1 #define PASTER(n) printf( "token" #n " = %d", token##n)
2 int token9 = 9;
則運行PASTER(9)後輸出結果為token9 = 9。
又如要做一個菜單項命令名和函數指針組成的結構體數組,並希望在函數名和菜單項命令名之間有直觀的、名字上的關系。那麼下面的代碼就非常實用:
1 struct command{
2 char * name;
3 void (*function)(void);
4 };
5 #define COMMAND(NAME) {NAME, NAME##_command}
然後,就可用一些預先定義好的命令來方便地初始化一個command結構的數組:
1 struct command commands[] = {
2 COMMAND(quit),
3 COMMAND(help),
4 //...
5 }
COMMAND宏在此充當一個代碼生成器的作用,這樣可在一定程度上減少代碼密度,間接地也可減少不留心所造成的錯誤。
還可以用n個##符號連接n+1個Token,這個特性是#符號所不具備的。如:
1 #define LINK_MULTIPLE(a, b, c, d) a##_##b##_##c##_##d
2 typedef struct record_type LINK_MULTIPLE(name, company, position, salary);
這裡這個語句將展開為typedef struct record_type name_company_position_salary。
注意:
當用##連接形參時,##前後的空格可有可無。
連接後的實際參數名,必須為實際存在的參數名或是編譯器已知的宏定義。
凡是宏定義裡有用'#'或'##'的地方,宏參數是不會再展開。如:
1 #define STR(s) #s
2 #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)就能得到正確的宏參數。
1 #define _STR(s) #s
2 #define STR(s) _STR(s) // 轉換宏
3 #define _CONS(a,b) int(a##e##b)
4 #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。
【'#'和'##'的一些應用特例】
1. 合並匿名變量名
1 #define ___ANONYMOUS1(type, var, line) type var##line
2 #define __ANONYMOUS0(type, line) ___ANONYMOUS1(type, _anonymous, line)
3 #define ANONYMOUS(type) __ANONYMOUS0(type, __LINE__)
例:ANONYMOUS(static int)即static int _anonymous70,70表示該行行號。
第一層:ANONYMOUS(static int) → __ANONYMOUS0(static int, __LINE__)
第二層: → ___ANONYMOUS1(static int, _anonymous, 70)
第三層: → static int _anonymous70
即每次只能解開當前層的宏,所以__LINE__在第二層才能被解開。
2. 填充結構
復制代碼
1 #define FILL(a) {a, #a}
2
3 enum IDD{OPEN, CLOSE};
4 typedef struct{
5 IDD id;
6 const char * msg;
7 }T_MSG;
復制代碼
則T_MSG tMsg[ ] = {FILL(OPEN), FILL(CLOSE)}相當於:
1 T_MSG tMsg[] = {{OPEN, "OPEN"},
2 {CLOSE, "CLOSE"}};
3. 記錄文件名
1 #define _GET_FILE_NAME(f) #f
2 #define GET_FILE_NAME(f) _GET_FILE_NAME(f)
3 static char FILE_NAME[] = GET_FILE_NAME(__FILE__);
4. 得到一個數值類型所對應的字符串緩沖大小
1 #define _TYPE_BUF_SIZE(type) sizeof #type
2 #define TYPE_BUF_SIZE(type) _TYPE_BUF_SIZE(type)
3 char buf[TYPE_BUF_SIZE(INT_MAX)];
4 //--> char buf[_TYPE_BUF_SIZE(0x7fffffff)];
5 //--> char buf[sizeof "0x7fffffff"];
這裡相當於:char buf[11];
2.3.2.3 字符化操作符@#
@#稱為字符化操作符(charizing),只能用於有傳入參數的宏定義中,且必須置於宏定義體的參數名前。作用是將傳入的單字符參數名轉換成字符,以一對單引號括起來。
1 #define makechar(x) #@x
2 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的可變參數宏, 可以更方便地打印調試信息,如:
1 #ifdef DEBUG
2 #define DBGPRINT(format, args...) \
3 fprintf(stderr, format, ##args)
4 #else
5 #define DBGPRINT(format, args...)
6 #endif
這樣定義之後,代碼中就可以用dbgprint了,例如dbgprint ("aaa [%s]", __FILE__)。
結合第4節的“條件編譯”功能,可以構造出如下調試打印宏:
復制代碼
1 #ifdef LOG_TEST_DEBUG
2 /* OMCI調試日志宏 */
3 //以10進制格式日志整型變量
4 #define PRINT_DEC(x) printf(#x" = %d\n", x)
5 #define PRINT_DEC2(x,y) printf(#x" = %d\n", y)
6 //以16進制格式日志整型變量
7 #define PRINT_HEX(x) printf(#x" = 0x%-X\n", x)
8 #define PRINT_HEX2(x,y) printf(#x" = 0x%-X\n", y)
9 //以字符串格式日志字符串變量
10 #define PRINT_STR(x) printf(#x" = %s\n", x)
11 #define PRINT_STR2(x,y) printf(#x" = %s\n", y)
12
13 //日志提示信息
14 #define PROMPT(info) printf("%s\n", info)
15
16 //調試定位信息打印宏
17 #define TP printf("%-4u - [%s<%s>]\n", __LINE__, __FILE__, __FUNCTION__);
18
19 //調試跟蹤宏,在待日志信息前附加日志文件名、行數、函數名等信息
20 #define TRACE(fmt, args...)\
21 do{\
22 printf("[%s(%d)<%s>]", __FILE__, __LINE__, __FUNCTION__);\
23 printf((fmt), ##args);\
24 }while(0)
25 #else
26 #define PRINT_DEC(x)
27 #define PRINT_DEC2(x,y)
28
29 #define PRINT_HEX(x)
30 #define PRINT_HEX2(x,y)
31
32 #define PRINT_STR(x)
33 #define PRINT_STR2(x,y)
34
35 #define PROMPT(info)
36
37 #define TP
38
39 #define TRACE(fmt, args...)
40 #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】
復制代碼
1 #define NUM OK
2 int main(void){
3 struct stu{
4 int num;
5 char *name;
6 char sex;
7 float score;
8 }*ps;
9 ps=(struct stu*)malloc(sizeof(struct stu));
10 ps->num = 102;
11 ps->name = "Zhang ping";
12 ps->sex = 'M';
13 ps->score = 62.5;
14 #ifdef NUM
15 printf("Number=%d\nScore=%f\n", ps->num, ps->score); /*--Execute--*/
16 #else
17 printf("Name=%s\nSex=%c\n", ps->name, ps->sex);
18 #endif
19 free(ps);
20 return 0;
21 }
復制代碼
由於在程序中插入了條件編譯預處理命令,因此要根據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】輸入一行字母字符,根據需要設置條件編譯,使之能將字母全改為大寫或小寫字母輸出。
復制代碼
1 #define LETTER 1
2 int main(void){
3 char str[20]="C Language",c;
4 int i="0";
5 while((c=str[i])!='\0'){
6 i++;
7 #if LETTER
8 if(c>='a'&&c<='z') c="c-32";
9 #else
10 if(c>='A'&&c<='Z') c="c"+32;
11 #endif
12 printf("%c",c);
13 }
14 return 0;
15 }
復制代碼
在程序第一行定義宏LETTER為1,因此在條件編譯時常量表達式LETTER的值為真(非零),故運行後使小寫字母變成大寫(C LANGUAGE)。
本例的條件編譯當然也可以用if條件語句來實現。但是用條件語句將會對整個源程序進行編譯,生成的目標代碼程序很長;而采用條件編譯,則根據條件只編譯其中的程序段1或程序段2,生成的目標程序較短。如果條件編譯的程序段很長,采用條件編譯的方法是十分必要的。
4.4 實踐用例
1. 屏蔽跨平台差異
在大規模開發過程中,特別是跨平台和系統的軟件裡,可以在編譯時通過條件編譯設置編譯環境。
例如,有一個數據類型,在Windows平台中應使用long類型表示,而在其他平台應使用float表示。這樣往往需要對源程序作必要的修改,這就降低了程序的通用性。可以用以下的條件編譯:
#ifdef WINDOWS
#define MYTYPE long
#else
#define MYTYPE float
#endif
如果在Windows上編譯程序,則可以在程序的開始加上#define WINDOWS,這樣就編譯命令行 #define MYTYPE long;如果在這組條件編譯命令前曾出現命令行#define WINDOWS 0,則預編譯後程序中的MYTYPE都用float代替。這樣,源程序可以不必作任何修改就可以用於不同類型的計算機系統。
2. 包含程序功能模塊
例如,在程序首部定義#ifdef FLV:
1 #ifdef FLV
2 include"fastleave.c"
3 #endif
如果不許向別的用戶提供該功能,則在編譯之前將首部的FLV加一下劃線即可。
3. 開關調試信息
調試程序時,常常希望輸出一些所需的信息以便追蹤程序的運行。而在調試完成後不再輸出這些信息。可以在源程序中插入以下的條件編譯段:
1 #ifdef DEBUG
2 printf("device_open(%p)\n", file);
3 #endif
如果在它的前面有以下命令行#define DEBUG,則在程序運行時輸出file指針的值,以便調試分析。調試完成後只需將這個define命令行刪除即可,這時所有使用DEBUG作標識符的條件編譯段中的printf語句不起作用,即起到“開關”一樣統一控制的作用。
4. 避開硬件的限制。
有時一些具體應用環境的硬件不同,但限於條件本地缺乏這種設備,可繞過硬件直接寫出預期結果:
1 #ifndef TEST
2 i = dial(); //程序調試運行時繞過此語句
3 #else
4 i = 0;
5 #endif
調試通過後,再屏蔽TEST的定義並重新編譯即可。
5. 防止頭文件重復包含
頭文件(.h)可以被頭文件或C文件包含。由於頭文件包含可以嵌套,C文件就有可能多次包含同一個頭文件;或者不同的C文件都包含同一個頭文件,編譯時就可能出現重復包含(重復定義)的問題。
在頭文件中為了避免重復調用(如兩個頭文件互相包含對方),常采用這樣的結構:
1 #ifndef <標識符>
2 #define <標識符>
3 //真正的內容,如函數聲明之類
4 #endif
<標識符>可以自由命名,但一般形如__HEADER_H,且每個頭文件標識都應該是唯一的。
事實上,不管頭文件會不會被多個文件引用,都要加上條件編譯開關來避免重復包含。
6. 在#ifndef中定義變量出現的問題(一般不定義在#ifndef中)。
1 #ifndef PRECMPL
2 #define PRECMPL
3 int var;
4 #endif
其中有個變量定義,在VC中鏈接時會出現變量var重復定義的錯誤,而在C中成功編譯。
(1) 當第一個使用這個頭文件的.cpp文件生成.obj時,var在裡面定義;當另一個使用該頭文件的.cpp文件再次(單獨)生成.obj時,var又被定義;然後兩個obj被第三個包含該頭文件.cpp連接在一起,會出現重復定義。
(2) 把源程序文件擴展名改成.c後,VC按照C語言語法對源程序進行編譯。在C語言中,遇到多個int var則自動認為其中一個是定義,其他的是聲明。
(3) C語言和C++語言連接結果不同,可能是在進行編譯時,C++語言將全局變量默認為強符號,所以連接出錯。C語言則依照是否初始化進行強弱的判斷的(僅供參考)。
解決方法:
(1) 把源程序文件擴展名改成.c。
(2) .h中只聲明 extern int var;,在.cpp中定義(推薦)
復制代碼
1 //<x.h>
2 #ifndef __X_H
3 #define __X_H
4 extern int var;
5 #endif
6 <x.c>
7 int var = 0;
復制代碼
綜上,變量一般不要定義在.h文件中。