<noframes id="hbptd">
      <ins id="hbptd"></ins>

      C語言預處理命令

      預處理(或稱預編譯)是指在進行編譯的第一遍掃描(詞法掃描和語法分析)之前所作的工作。預處理指令指示在程序正式編譯前就由編譯器進行的操作,可放在程序中任何位置。


      預處理是C語言的一個重要功能,它由預處理程序負責完成。當對一個源文件進行編譯時,系統將自動引用預處理程序對源程序中的預處理部分作處理,處理完畢自動進入對源程序的編譯。


      C語言提供多種預處理功能,主要處理#開始的預編譯指令,如宏定義(#define)、文件包含(#include)、條件編譯(#ifdef)等。合理使用預處理功能編寫的程序便于閱讀、修改、移植和調試,也有利于模塊化程序設計。


      二  宏定義


      C語言源程序中允許用一個標識符來表示一個字符串,稱為“宏”。被定義為宏的標識符稱為“宏名”。在編譯預處理時,對程序中所有出現的宏名,都用宏定義中的字符串去代換,這稱為宏替換或宏展開。


      宏定義是由源程序中的宏定義命令完成的。宏替換是由預處理程序自動完成的。


      在C語言中,宏定義分為有參數和無參數兩種。下面分別討論這兩種宏的定義和調用。


      2.1 無參宏定義


      無參宏的宏名后不帶參數。其定義的一般形式為:


      #define  標識符  字符串


      其中,“#”表示這是一條預處理命令(以#開頭的均為預處理命令)?!癲efine”為宏定義命令?!皹俗R符”為符號常量,即宏名?!白址笨梢允浅?、表達式、格式串等。


      宏定義用宏名來表示一個字符串,在宏展開時又以該字符串取代宏名。這只是一種簡單的文本替換,預處理程序對它不作任何檢查。如有錯誤,只能在編譯已被宏展開后的源程序時發現。


      注意理解宏替換中“換”的概念,即在對相關命令或語句的含義和功能作具體分析之前就要進行文本替換。


      【例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(r) ((r)*(r)),即宏定義時建議所有的層次都要加括號。


      【例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,因此表達式中第二次相乘時結果為3*3=9。同理,第三次相乘時結果為5*5=25,并在此次循環后j值變為7,不再滿足循環條件,停止循環。


      從以上分析可以看出函數調用和宏調用二者在形式上相似,在本質上是完全不同的。


      帶參宏注意事項:


      宏名和形參表的括號間不能有空格。

      宏替換只作替換,不做計算,不做表達式求解。

      函數調用在編譯后程序運行時進行,并且分配內存。宏替換在編譯前進行,不分配內存。

      函數只有一個返回值,利用宏則可以設法得到多個值。

      宏展開使源程序變長,函數調用不會。

      宏展開不占用運行時間,只占編譯時間,函數調用占運行時間(分配內存、保留現場、值傳遞、返回值)。

      為防止無限制遞歸展開,當宏調用自身時,不再繼續展開。

      如:#define TEST(x)  (x + TEST(x))被展開為1 + TEST(1)。


      2.3 實踐用例


      包括基本用法(及技巧)和特殊用法(#和##等)。


      #define可以定義多條語句,以替代多行的代碼,但應注意替換后的形式,避免出錯。宏定義在換行時要加上一個反斜杠”\”,而且反斜杠后面直接回車,不能有空格。


      2.3.1 基本用法

      1. 定義常量:


      #define PI   3.1415926

      將程序中出現的PI全部換成3.1415926。


      2. 定義表達式:

       #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. 得到指定地址上的一個字節或字:


      #define MEM_B(x)     (*((char *)(x)))

       #define MEM_W(x)     (*((short *)(x)))


      4. 求最大值和最小值:


      #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宏)。 


      5. 得到一個成員在結構體中的偏移量(lint 545告警表示"&用法值得懷疑",此處抑制該警告):


       #define FPOS(type, field) \

       /*lint -e545 */ ((int)&((type *)0)-> field) /*lint +e545 */


      6. 得到一個結構體中某成員所占用的字節數:


      #define FSIZ(type, field)    sizeof(((type *)0)->field)


      7. 按照LSB格式把兩個字節轉化為一個字(word):


      #define FLIPW(arr)          ((((short)(arr)[0]) * 256) + (arr)[1])


      8. 按照LSB格式把一個字(word)轉化為兩個字節:


      #define FLOPW(arr, val) \

          (arr)[0] = ((val) / 256); \

          (arr)[1] = ((val) & 0xFF)


      9. 得到一個變量的地址:


      #define B_PTR(var)       ((char *)(void *)&(var))

      #define W_PTR(var)       ((short *)(void *)&(var))


      10. 得到一個字(word)的高位和低位字節:


      #define WORD_LO(x)       ((char)((short)(x)&0xFF))

      #define WORD_HI(x)       ((char)((short)(x)>>0x8))


      11. 返回一個比X大的最接近的8的倍數:


      #define RND8(x)           ((((x) + 7) / 8) * 8)


      12. 將一個字母轉換為大寫或小寫:


      #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編碼。


      13. 判斷字符是不是10進值的數字:


       #define ISDEC(c)          ((c) >= '0' && (c) <= '9')


      14. 判斷字符是不是16進值的數字:


        #define ISHEX(c)          (((c) >= '0' && (c) <= '9') ||\


           ((c) >= 'A' && (c) <= 'F') ||\


           ((c) >= 'a' && (c) <= 'f'))



      15. 防止溢出的一個方法:


       #define INC_SAT(val)      (val = ((val)+1 > (val)) ? (val)+1 : (val))

      16. 返回數組元素的個數:


      #define ARR_SIZE(arr)     (sizeof((arr)) / sizeof((arr[0])))


      17. 對于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)))


      18. 使用一些宏跟蹤調試:


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

           

      19. 用do{…}while(0)語句包含多語句防止錯誤:


      #define DO(a, b) do{\

          a+b;\

          a++;\

       }while(0)


      20. 實現類似“重載”功能


      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;


      }


      21. 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))

      }


      22. 取消宏定義:


      #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 + 0x20;


       #endif


               printf("%c", cChar);


          }

           return 0;

       }


      在程序第一行定義宏CAPITAL_LETTER為1,因此在條件編譯時常量表達式CAPITAL_LETTER的值為真(非零),故運行后使小寫字母變成大寫(C LANGUAGE)。


      本例的條件編譯當然也可以用if條件語句來實現。但是用條件語句將會對整個源程序進行編譯,生成的目標代碼程序很長;而采用條件編譯,則根據條件只編譯其中的程序段1或程序段2,生成的目標程序較短。如果條件編譯的程序段很長,采用條件編譯的方法是十分必要的。


      4.4 實踐用例


      1. 屏蔽跨平臺差異


      在大規模開發過程中,特別是跨平臺和系統的軟件里,可以在編譯時通過條件編譯設置編譯環境。


      例如,有一個數據類型,在Windows平臺中應使用long類型表示,而在其他平臺應使用float表示。這樣往往需要對源程序作必要的修改,這就降低了程序的通用性??梢杂靡韵碌臈l件編譯:


       #ifdef WINDOWS


           #define MYTYPE long


       #else


           #define MYTYPE float


       #endif


      如果在Windows上編譯程序,則可以在程序的開始加上#define WINDOWS,這樣就編譯命令行    #define MYTYPE long;


      如果在這組條件編譯命令前曾出現命令行#define WINDOWS 0,則預編譯后程序中的MYTYPE都用float代替。這樣,源程序可以不必作任何修改就可以用于不同類型的計算機系統。


      2. 包含程序功能模塊


      例如,在程序首部定義#ifdef FLV:


       #ifdef FLV


          include"fastleave.c"


       #endif


      如果不許向別的用戶提供該功能,則在編譯之前將首部的FLV加一下劃線即可。


      3. 開關調試信息


      調試程序時,常常希望輸出一些所需的信息以便追蹤程序的運行。而在調試完成后不再輸出這些信息??梢栽谠闯绦蛑胁迦胍韵碌臈l件編譯段:


       #ifdef DEBUG


           printf("device_open(%p)\n", file);


       #endif


       如果在它的前面有以下命令行#define DEBUG,則在程序運行時輸出file指針的值,以便調試分析。調試完成后只需將這個define命令行刪除即可,這時所有使用DEBUG作標識符的條件編譯段中的printf語句不起作用,即起到“開關”一樣統一控制的作用。 


      4. 避開硬件的限制。


      有時一些具體應用環境的硬件不同,但限于條件本地缺乏這種設備,可繞過硬件直接寫出預期結果:


      #ifndef TEST


           i = dial();  //程序調試運行時繞過此語句


       #else


           i = 0;


       #endif


      調試通過后,再屏蔽TEST的定義并重新編譯即可。   


      5. 防止頭文件重復包含


      頭文件(.h)可以被頭文件或C文件包含。由于頭文件包含可以嵌套,C文件就有可能多次包含同一個頭文件;或者不同的C文件都包含同一個頭文件,編譯時就可能出現重復包含(重復定義)的問題。


      在頭文件中為了避免重復調用(如兩個頭文件互相包含對方),常采用這樣的結構:


       #ifndef  <標識符>


           #define  <標識符>


           //真正的內容,如函數聲明之類


       #endif


      <標識符>可以自由命名,但一般形如__HEADER_H,且每個頭文件標識都應該是唯一的。


      事實上,不管頭文件會不會被多個文件引用,都要加上條件編譯開關來避免重復包含。 


      6. 在#ifndef中定義變量出現的問題(一般不定義在#ifndef中)。


       

      #ifndef PRECMPL


           #define PRECMPL


          int var;

          

       #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中定義(推薦)


      //<x.h>


       #ifndef  __X_H


           #define  __X_H


           extern int var;


       #endif


       //<x.c>


       int var = 0;


      綜上,變量一般不要定義在.h文件中。



      五  小結


      預處理功能是C語言特有的功能,它是在對源程序正式編譯前由預處理程序完成的。程序員在程序中用預處理命令來調用這些功能。


      宏定義是用一個標識符來表示一個字符串,這個字符串可以是常量、變量或表達式。在宏調用中將用該字符串代換宏名。


      宏定義可以帶有參數,宏調用時是以實參代換形參。而不是“值傳遞”。

      為了避免宏替換時發生錯誤,宏定義中的字符串應加括號,字符串中出現的形式參數兩邊也應加括號。


      文件包含是預處理的一個重要功能,它可用來把多個源文件連接成一個源文件進行編譯,結果將生成一個目標文件。


      條件編譯允許只編譯源程序中滿足條件的程序段,使生成的目標程序較短,從而減少了內存的開銷并提高了程序的效率。


      使用預處理功能便于程序的修改、閱讀、移植和調試,也便于實現模塊化程序設計。

       


      六 附錄

      6.1 Argument Prescan

      (摘自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) 繼續處理宏替換后的宏文本,若宏文本也包含宏則繼續展開,否則完成展開。


      其中第一步將實參代入宏文本后,若實參前遇到字符“#”或“##”,即使實參是宏也不再展開實參,而當作文本處理。


      上述展開步驟示例如下:


      #define TO_STRING(x)    _TO_STRING(x)


      #define _TO_STRING(x)   #x


      #define FOO             4


      則_TO_STRING(FOO)展開為”FOO”;TO_STRING(FOO)展開為_TO_STRING(4),進而展開為”4”。相當于借助_TO_STRING這樣的中間宏,先展開宏參數,延遲其字符化。


      6.2 宏的其他注意事項


      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. 帶參宏內定義變量時,應注意避免內外部變量重名的問題:


       typedef struct{


           int d;


        }T_TEST;


        T_TEST gtTest = {0};


       #define ASSIGN1(_d) do{ \


           T_TEST t = {0}; \


            t.d = _d; \


            gtTest = t; \


        }while(0)


       #define ASSIGN2(_p) do{ \


           int _d; \


           _d = 5; \


           (_p) = _d; \


       }while(0)


      若宏參數名或宏內變量名不加前綴下劃線,則ASSIGN1(c)將會導致編譯報錯(t.d被替換為t.c),ASSIGN2(d)會因宏內作用域而導致外部的變量d值保持不變(而非改為5)。


      7. 不要用宏改寫語言。例如:


      #define FOREVER   for ( ; ; )


       #define BEGIN     {


       #define END       }


      C語言有完善且眾所周知的語法。試圖將其改變成類似于其他語言的形式,會使讀者混淆,難于理解。


      6.3 do{…}while(0)妙用


      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;,則以下代碼


       if(NULL != p)


           SAFE_DELETE(p)


       else


           DUMMY( );


      就有兩個問題:


      a) 因為if分支后有兩條語句,else分支沒有對應的if,編譯失??;


      b) 假設沒有else,則SAFE_DELETE中第二條語句無論if判斷是否成立均會執行,這顯然違背程序設計的原始目的。


      那么,為了避免這兩個問題,將宏直接用{}括起來是否可以?如:


      #define SAFE_DELETE(p)  {delete p; p = NULL;}


      的確,上述問題不復存在。但C/C++編程中,在每條語句后加分號是約定俗成的習慣,此時以下代碼


       if(NULL != p)


           SAFE_DELETE(p);


       else


           DUMMY( );


      其else分支就無法通過編譯(多出一個分號),而采用do{…}while(0)則毫無問題。


      使用do{...} while(0)將宏包裹起來,成為一個獨立的語法單元,從而不會與上下文發生混淆。同時因為絕大多數編譯器都能夠識別do{...}while(0)這種無用的循環并優化,所以該法不會導致程序的性能降低。


      6.4 類型定義符typedef


      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]。


      又如:


       typedef struct{


           char name[20];


           int  age;


           char sex;


       }STU;


      然后可用STU來定義結構變量:STU body1,body2;


      有時也可用宏定義來代替typedef的功能,但是宏定義是由預處理完成的,而typedef則是在編譯時完成的,后者更為靈活方便。


      此外,采用typedef重新定義一些類型,可防止因平臺和編譯器不同而產生的類型字節數差異,方便移植。如:



        typedef unsigned char boolean;       /* Boolean value type. */


        typedef unsigned long int uint32;    /* Unsigned 32 bit value */


        typedef unsigned short uint16;       /* Unsigned 16 bit value */


        typedef unsigned char uint8;         /* Unsigned 8 bit value */


        typedef signed long int int32;       /* Signed 32 bit value */


        typedef signed short int16;          /* Signed 16 bit value */


        typedef signed char int8;            /* Signed 8 bit value */




        //下面的不建議使用


       typedef unsigned char byte;          /* Unsigned 8 bit value type. */


       typedef unsigned short word;         /* Unsinged 16 bit value type. */


       typedef unsigned long dword;         /* Unsigned 32 bit value type. */


       typedef unsigned char uint1;         /* Unsigned 8 bit value type. */


       typedef unsigned short uint2;        /* Unsigned 16 bit value type. */


       typedef unsigned long uint4;         /* Unsigned 32 bit value type. */


       typedef signed char int1;            /* Signed 8 bit value type. */


       typedef signed short int2;           /* Signed 16 bit value type. */


       typedef long int int4;               /* Signed 32 bit value type. */


       typedef signed long sint31;          /* Signed 32 bit value */


       typedef signed short sint15;         /* Signed 16 bit value */


       typedef signed char sint7;           /* Signed 8 bit value */


      圖片加載中...

      在線留言

      ◎歡迎您的留言,您也可以通過以下方式聯系我們:

      ◎客戶服務熱線:021-51095123

      ◎郵箱:xin021@126.com

      展開

          <noframes id="hbptd">
          <ins id="hbptd"></ins>