預處理(或稱預編譯)是指在進行編譯的第一遍掃描(詞法掃描和語法分析)以前所做的工做。預處理指令指示在程序正式編譯前就由編譯器進行的操做,可放在程序中任何位置。html
預處理是C語言的一個重要功能,它由預處理程序負責完成。當對一個源文件進行編譯時,系統將自動引用預處理程序對源程序中的預處理部分做處理,處理完畢自動進入對源程序的編譯。linux
C語言提供多種預處理功能,主要處理#開始的預編譯指令,如宏定義(#define)、文件包含(#include)、條件編譯(#ifdef)等。合理使用預處理功能編寫的程序便於閱讀、修改、移植和調試,也有利於模塊化程序設計。程序員
本文參考諸多資料,詳細介紹經常使用的幾種預處理功能。因成文較早,資料來源大多已不可考,敬請諒解。編程
C語言源程序中容許用一個標識符來表示一個字符串,稱爲「宏」。被定義爲宏的標識符稱爲「宏名」。在編譯預處理時,對程序中全部出現的宏名,都用宏定義中的字符串去代換,這稱爲宏替換或宏展開。c#
宏定義是由源程序中的宏定義命令完成的。宏替換是由預處理程序自動完成的。數組
在C語言中,宏定義分爲有參數和無參數兩種。下面分別討論這兩種宏的定義和調用。安全
無參宏的宏名後不帶參數。其定義的通常形式爲:less
#define 標識符 字符串ide |
其中,「#」表示這是一條預處理命令(以#開頭的均爲預處理命令)。「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。
無參宏注意事項:
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能夠定義多條語句,以替代多行的代碼,但應注意替換後的形式,避免出錯。宏定義在換行時要加上一個反斜槓」\」,並且反斜槓後面直接回車,不能有空格。
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(x++, y)時,x++被執行多少次取決於x和y的大小;當宏參數爲函數也會存在相似的風險。因此建議用內聯函數而不是這種方法提升速度。不過,雖然存在這樣的弊病,但宏定義很是靈活,由於x和y能夠是各類數據類型。
如下給出MAX宏的兩個安全版本(源自linux/kernel.h):
1 #define MAX_S(x, y) ({ \ 2 const typeof(x) _x = (x); \ 3 const typeof(y) _y = (y); \ 4 (void)(&_x == &_y); \ 5 _x > _y ? _x : _y; }) 6 7 #define TMAX_S(type, x, y) ({ \ 8 type _x = (x); \ 9 type _y = (y); \ 10 _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宏)。
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) + 'A' - 'a') : (c)) 2 #define LOCASE(c) (((c) >= 'A' && (c) <= 'Z') ? ((c) + 'a' - 'A') : (c))
注意,UPCASE和LOCASE宏僅適用於ASCII編碼(依賴於碼字順序和連續性),而不適用於EBCDIC編碼。
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 *);
可採用宏定義TSWAP (t,x,y)或SWAP(x, y)交換兩個整型或浮點參數:
1 #define TSWAP(type, x, y) do{ \ 2 type _y = y; \ 3 y = x; \ 4 x = _y; \ 5 }while(0) 6 #define SWAP(x, y) do{ \ 7 x = x + y; \ 8 y = x - y; \ 9 x = x - y; \ 10 }while(0) 11 12 int main(void){ 13 int a = 10, b = 5; 14 TSWAP(int, a, b); 15 printf(「a=%d, b=%d\n」, a, b); 16 return 0; 17 }
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中無效。
主要涉及C語言宏裏#和##的用法,以及可變參數宏。
在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時便會在標準錯誤流上輸出一個提示信息。
注意#宏對空格的處理:
##稱爲鏈接符(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];
@#稱爲字符化操做符(charizing),只能用於有傳入參數的宏定義中,且必須置於宏定義體的參數名前。做用是將傳入的單字符參數名轉換成字符,以一對單引號括起來。
1 #define makechar(x) #@x 2 a = makechar(b);
展開後變成a= 'b'。
...在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"的頭文件。文件包含命令把指定頭文件插入該命令行位置取代該命令行,從而把指定的文件和當前的源程序文件連成一個源文件。
在程序設計中,文件包含是頗有用的。一個大程序能夠分爲多個模塊,由多個程序員分別編程。有些公用的符號常量或宏定義等可單獨組成一個文件,在其它文件的開頭用包含命令包含該文件便可使用。這樣,可避免在每一個文件開頭都去書寫那些公用量,從而節省時間,並減小出錯。
對文件包含命令要說明如下幾點:
通常狀況下,源程序中全部的行都參加編譯。但有時但願對其中一部份內容只在知足必定條件才進行編譯,也就是對一部份內容指定編譯的條件,這就是「條件編譯」。有時,但願當知足某條件時對一組語句進行編譯,而當條件不知足時則編譯另外一組語句。
條件編譯功能可按不一樣的條件去編譯不一樣的程序部分,從而產生不一樣的目標代碼文件。這對於程序的移植和調試是頗有用的。
條件編譯有三種形式,下面分別介紹。
#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語句。
#ifndef 標識符 程序段1 #else 程序段2 #endif |
若是標識符未被#define命令定義過,則對程序段1進行編譯,不然對程序段2進行編譯。這與#ifdef形式的功能正相反。
「#ifndef 標識符」也可寫爲「#if !(defined 標識符)」。
#if 常量表達式 程序段1 #else 程序段2 #endif |
若是常量表達式的值爲真(非0),則對程序段1 進行編譯,不然對程序段2進行編譯。所以可以使程序在不一樣條件下,完成不一樣的功能。
【例7】輸入一行字母字符,根據須要設置條件編譯,使之能將字母全改成大寫或小寫字母輸出。
1 #define CAPITAL_LETTER 1 2 int main(void){ 3 char szOrig[] = "C Language", cChar; 4 int dwIdx = 0; 5 while((cChar = szOrig[dwIdx++]) != '\0') 6 { 7 #if CAPITAL_LETTER 8 if((cChar >= 'a') && (cChar <= 'z')) cChar = cChar - 0x20; 9 #else 10 if((cChar >= 'A') && (cChar <= 'Z')) cChar = cChar + 0x20; 11 #endif 12 printf("%c", cChar); 13 } 14 return 0; 15 }
在程序第一行定義宏CAPITAL_LETTER爲1,所以在條件編譯時常量表達式CAPITAL_LETTER的值爲真(非零),故運行後使小寫字母變成大寫(C LANGUAGE)。
本例的條件編譯固然也能夠用if條件語句來實現。可是用條件語句將會對整個源程序進行編譯,生成的目標代碼程序很長;而採用條件編譯,則根據條件只編譯其中的程序段1或程序段2,生成的目標程序較短。若是條件編譯的程序段很長,採用條件編譯的方法是十分必要的。
1. 屏蔽跨平臺差別
在大規模開發過程當中,特別是跨平臺和系統的軟件裏,能夠在編譯時經過條件編譯設置編譯環境。
例如,有一個數據類型,在Windows平臺中應使用long類型表示,而在其餘平臺應使用float表示。這樣每每須要對源程序做必要的修改,這就下降了程序的通用性。能夠用如下的條件編譯:
1 #ifdef WINDOWS 2 #define MYTYPE long 3 #else 4 #define MYTYPE float 5 #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文件中。
(摘自http://gcc.gnu.org/onlinedocs/cpp/Argument-Prescan.html)
Macro arguments are completely macro-expanded before they are substituted into a macro body, unless they are stringified or pasted with other tokens. After substitution, the entire macro body, including the substituted arguments, is scanned again for macros to be expanded. The result is that the arguments are scanned twice to expand macro calls in them.
宏參數被徹底展開後再替換入宏體,但當宏參數被字符串化(#)或與其它子串鏈接(##)時不予展開。在替換以後,再次掃描整個宏體(包括已替換宏參數)以進一步展開宏。結果是宏參數被掃描兩次以展開參數所(嵌套)調用的宏。
若帶參數宏定義中的參數稱爲形參,調用宏時的實際參數稱爲實參,則宏的展開可用如下三步來簡單描述(該步驟與gcc摘錄稍有不一樣,但更易操做):
1) 用實參替換形參,將實參代入宏文本中;
2) 若實參也是宏,則展開實參;
3) 繼續處理宏替換後的宏文本,若宏文本也包含宏則繼續展開,不然完成展開。
其中第一步將實參代入宏文本後,若實參前遇到字符「#」或「##」,即便實參是宏也再也不展開實參,而看成文本處理。
上述展開步驟示例以下:
1 #define TO_STRING(x) _TO_STRING(x) 2 #define _TO_STRING(x) #x 3 #define FOO 4
則_TO_STRING(FOO)展開爲」FOO」;TO_STRING(FOO)展開爲_TO_STRING(4),進而展開爲」4」。至關於藉助_TO_STRING這樣的中間宏,先展開宏參數,延遲其字符化。
1. 避免在無做用域限定(未用{}括起)的宏內定義數組、結構、字符串等變量,不然函數中對宏的屢次引用會致使實際局部變量空間成倍放大。
2. 按照宏的功能、模塊進行集中定義。即在一處將常量數值定義爲宏,其餘地方經過引用該宏,生成本身模塊的宏。嚴禁相同含義的常量數值,在不一樣地方定義爲不一樣的宏,即便數值相同也不容許(維護修改後極易遺漏,形成代碼隱患)。
3. 用只讀變量適當替代(相似功能的)宏,例如將#define PIE 3.14改成const float PIE = 3.14。這樣作的好處以下:
1) 預編譯時用宏定義值替換宏名,編譯時報錯不易理解;
2) 跟蹤調試時顯示宏值,而不是宏名;
3) 宏沒有類型,不能作類型檢查,不安全;
4) 宏自身沒有做用域;
5) 只讀變量和宏的效率一樣高。
注意,C語言中只讀變量不可用於數組大小、變量(包括數組元素)初始化值以及case表達式。
4. 用inline函數代替(相似功能的)宏函數。好處以下:
1) 宏函數在預編譯時處理,編譯出錯信息不易理解;
2) 宏函數自己沒法單步跟蹤調試,所以也不要在宏內調用函數。但某些編譯器(爲了調試須要)可將inline函數轉成普通函數;
3) 宏函數的入參沒有類型,不安全;
5) inline函數會在目標代碼中展開,和宏的效率同樣高;
注意,某些宏函數用法獨特,不能用inline函數取代。當不想或不能指明參數類型時,宏函數更合適。
5. 不帶參數的宏函數也要定義成函數形式,如#define HELLO( ) printf(「Hello.」)。
括號會暗示閱讀代碼者該宏是一個函數。
6. 帶參宏內定義變量時,應注意避免內外部變量重名的問題:
1 typedef struct{ 2 int d; 3 }T_TEST; 4 T_TEST gtTest = {0}; 5 #define ASSIGN1(_d) do{ \ 6 T_TEST t = {0}; \ 7 t.d = _d; \ 8 gtTest = t; \ 9 }while(0) 10 11 #define ASSIGN2(_p) do{ \ 12 int _d; \ 13 _d = 5; \ 14 (_p) = _d; \ 15 }while(0)
若宏參數名或宏內變量名不加前綴下劃線,則ASSIGN1(c)將會致使編譯報錯(t.d被替換爲t.c),ASSIGN2(d)會因宏內做用域而致使外部的變量d值保持不變(而非改成5)。
7. 不要用宏改寫語言。例如:
1 #define FOREVER for ( ; ; ) 2 #define BEGIN { 3 #define END }
C語言有完善且衆所周知的語法。試圖將其改變成相似於其餘語言的形式,會使讀者混淆,難於理解。
1. 函數中使用do{…}while(0)可替代goto語句。例如:
goto寫法 |
替代寫法 |
bOk = func1(); if(!bOk) goto errorhandle; bOk = func2(); if(!bOk) goto errorhandle; bOk = func3(); if(!bOk) goto errorhandle;
//… … //執行成功,釋放資源並返回 delete p; p = NULL; return true;
errorhandle: delete p; p = NULL; return false; |
do{ //執行並進行錯誤處理 bOk = func1(); if(!bOk) break; bOk = func2(); if(!bOk) break; bOk = func3(); if(!bOk) break;
// .......... }while(0);
//釋放資源 delete p; p = NULL; return bOk; |
2. 宏定義中使用do{…}while(0)的緣由及好處:
1) 避免空的宏定義產生warning,如#define DUMMY( ) do{}while(0)。
2) 存在一個獨立的代碼塊,可進行變量定義,實現比較複雜的邏輯處理。
注意,該代碼塊內(即{…}內)定義的變量其做用域僅限於該塊。此外,爲避免宏的實參與其內部定義的變量同名而形成覆蓋,最好在變量名前加上_(基於以下編程慣例:除非是庫,不然不該定義以_開始的變量)。
3) 若宏出如今判斷語句以後,可保證做爲一個總體來實現。
如#define SAFE_DELETE(p) delete p; p = NULL;,則如下代碼
1 if(NULL != p) 2 SAFE_DELETE(p) 3 else 4 DUMMY( );
就有兩個問題:
a) 由於if分支後有兩條語句,else分支沒有對應的if,編譯失敗;
b) 假設沒有else,則SAFE_DELETE中第二條語句不管if判斷是否成立均會執行,這顯然違背程序設計的原始目的。
那麼,爲了不這兩個問題,將宏直接用{}括起來是否能夠?如:
#define SAFE_DELETE(p) {delete p; p = NULL;}
的確,上述問題不復存在。但C/C++編程中,在每條語句後加分號是約定俗成的習慣,此時如下代碼
1 if(NULL != p) 2 SAFE_DELETE(p); 3 else 4 DUMMY( );
其else分支就沒法經過編譯(多出一個分號),而採用do{…}while(0)則毫無問題。
使用do{...} while(0)將宏包裹起來,成爲一個獨立的語法單元,從而不會與上下文發生混淆。同時由於絕大多數編譯器都可以識別do{...}while(0)這種無用的循環並優化,因此該法不會致使程序的性能下降。
C語言不只提供了豐富的數據類型,並且還容許由用戶本身定義類型說明符,也就是說容許由用戶爲數據類型取「別名」。類型定義符typedef便可用來完成此功能。
typedef定義的通常形式爲:
typedef 原類型名 新類型名 |
其中原類型名中含有定義部分,新類型名通常用大寫表示,以便於區別。
例如,有整型量int a,b。其中int是整型變量的類型說明符。int的完整寫法爲integer,爲增長程序的可讀性,可把整型說明符用typedef定義爲typedef int INTEGER。此後就可用INTEGER來代替int做整型變量的類型說明,如INTEGER a,b等效於int a,b。
用typedef定義數組、指針、結構等類型將帶來很大的方便,不只使程序書寫簡單並且意義更爲明確,於是加強了可讀性。
例如,typedef char NAME[20]表示NAME是字符數組類型,數組長度爲20。而後可用NAME 說明變量,如NAME a1,a2,s1,s2徹底等效於:char a1[20],a2[20],s1[20],s2[20]。
又如:
1 typedef struct{ 2 char name[20]; 3 int age; 4 char sex; 5 }STU;
而後可用STU來定義結構變量:STU body1,body2;
有時也可用宏定義來代替typedef的功能,可是宏定義是由預處理完成的,而typedef則是在編譯時完成的,後者更爲靈活方便。
此外,採用typedef從新定義一些類型,可防止因平臺和編譯器不一樣而產生的類型字節數差別,方便移植。如:
1 typedef unsigned char boolean; /* Boolean value type. */ 2 typedef unsigned long int uint32; /* Unsigned 32 bit value */ 3 typedef unsigned short uint16; /* Unsigned 16 bit value */ 4 typedef unsigned char uint8; /* Unsigned 8 bit value */ 5 typedef signed long int int32; /* Signed 32 bit value */ 6 typedef signed short int16; /* Signed 16 bit value */ 7 typedef signed char int8; /* Signed 8 bit value */ 8 //下面的不建議使用 9 typedef unsigned char byte; /* Unsigned 8 bit value type. */ 10 typedef unsigned short word; /* Unsinged 16 bit value type. */ 11 typedef unsigned long dword; /* Unsigned 32 bit value type. */ 12 typedef unsigned char uint1; /* Unsigned 8 bit value type. */ 13 typedef unsigned short uint2; /* Unsigned 16 bit value type. */ 14 typedef unsigned long uint4; /* Unsigned 32 bit value type. */ 15 typedef signed char int1; /* Signed 8 bit value type. */ 16 typedef signed short int2; /* Signed 16 bit value type. */ 17 typedef long int int4; /* Signed 32 bit value type. */ 18 typedef signed long sint31; /* Signed 32 bit value */ 19 typedef signed short sint15; /* Signed 16 bit value */ 20 typedef signed char sint7; /* Signed 8 bit value */