天天看點

轉載:宏定義的一些使用技巧總結

轉自:http://dxf206.blog.163.com/blog/static/4227861200952511813462/ 

我在寫代碼的時候喜歡使用宏,不僅使代碼看起來整潔,而且用好了還能極大的減輕編碼的工作量,但是如果使用不當的話,出了問題查找起來就就非常的難了,下面的總結大部分是從網上看到的,也有一些是我自己在工作中總結出來的。

宏使用中的常見的基礎問題

1. 防止一個頭檔案被重複包含

   #ifndef BODYDEF_H

   #define BODYDEF_H

   //頭檔案内容

   #endif

2. 重新定義一些類型,防止由于各種平台和編譯器的不同,而産生的類型位元組數差異,友善移植。

   typedef  unsigned char      boolean;    

   typedef  unsigned long      uint32;     

   typedef  unsigned short     uint16;     

   typedef  unsigned char      uint8;      

   typedef  signed long int    int32;      

   typedef  signed short       int16;      

   typedef  signed char        int8;       

   //下面的不建議使用

   typedef  unsigned char     byte;        

   typedef  unsigned short    word;        

   typedef  unsigned long     dword;       

   typedef  unsigned char     uint1;       

   typedef  unsigned short    uint2;       

   typedef  unsigned long     uint4;       

   typedef  signed char       int1;        

   typedef  signed short      int2;        

   typedef  long int          int4;        

   typedef  signed long       sint31;      

   typedef  signed short      sint15;      

   typedef  signed char       sint7;       

3. 得到指定位址上的一個位元組或字

   #define  MEM_B(x)  (*((uint8*)(x)))

   #define  MEM_W(x)  (*((uint16*)(x)))

4. 得到一個field在結構體(struct)中的偏移量

   #define FPOS(type,field) ((uint32) &((type *)0)->field)

5. 得到一個結構體中field所占用的位元組數

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

6. 求最大值和最小值

   #define  MAX(x,y) (((x)>(y))?(x):(y))

   #define  MIN(x,y) (((x)<(y))?(x):(y))

7. 得到一個變量的位址

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

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

8. 按照LSB格式把兩個位元組轉化為一個Word

   #define  FLIPW(ray) ((((word) (ray)[0]) * 256) + (ray)[1])

9. 按照LSB格式把一個Word轉化為兩個位元組

   #define  FLOPW( ray, val ) /

      (ray)[0] = ((val) / 256); /

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

10.得到一個字的高位和低位位元組

   #define  WORD_LO(***)  ((byte) ((word)(***) & 0xFF))

   #define  WORD_HI(***)  ((byte) ((word)(***) >> 8))

11.将一個字母轉換為大寫

   #define  UPCASE(c) (((c) >= 'a' && (c) <= 'z') ? ((c) - 0x20) : (c))

12.判斷字元是不是10進值的數字

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

13.判斷字元是不是16進值的數字

   #define  HEXCHK(c) (((c) >= '0' && (c) <= '9') || /

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

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

14.防止溢出的一個方法

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

15.傳回數組元素的個數

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

16.傳回一個比X大的最接近的8的倍數

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

17.傳回一個無符号數n尾的值MOD_BY_POWER_OF_TWO(X,n)=X%(2^n)

   #define MOD_BY_POWER_OF_TWO(val, mod_by) /

            ((dword)(val) & (dword)((mod_by)-1))

18.對于IO空間映射在存儲空間的結構,輸入輸出處理

   #define inp(port)         (*((volatile byte *) (port)))

   #define inpw(port)        (*((volatile word *) (port)))

   #define inpdw(port)       (*((volatile dword *)(port)))

   #define outp(port, val)   (*((volatile byte *) (port)) = ((byte) (val)))

   #define outpw(port, val)  (*((volatile word *) (port)) = ((word) (val)))

   #define outpdw(port, val) (*((volatile dword *) (port)) = ((dword) (val)))

19.使用一些宏跟蹤調試

   ANSI标準說明了五個預定義的宏名。它們是:

   __LINE__   (兩個下劃線),對應%d

   __FILE__   對應%s

   __DATE__   對應%s

   __TIME__   對應%s

   __STDC__

   如果編譯不是标準的,則可能僅支援以上宏名中的幾個,或根本不支援。記住編譯程式也許還提供其它預定義的宏名。

   __LINE__宏指令表示目前指令所在的行,是個整型數

   __FILE__宏指令表示目前指令所在檔案,包含完整路徑

   __DATE__宏指令含有形式為月/日/年的串,表示源檔案被翻譯到代碼時的日期。

   __TIME__宏指令包含源代碼翻譯到目标代碼的時間。串形式為時:分:秒。

   如果實作是标準的,則宏__STDC__含有十進制常量1。如果它含有任何其它數,則實作是非标準的。

   可以定義宏,例如:

   當定義了_DEBUG,輸出資料資訊和所在檔案所在行

   #ifdef _DEBUG

       #define DEBUGMSG(msg,date) printf(msg);printf("%d%d%d",date,_LINE_,_FILE_)

   #else

       #define DEBUGMSG(msg,date)

   #endif

20.宏定義防止使用時錯誤

   用小括号包含。

   例如:#define ADD(a,b) (a+b)

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

   例如:#define DO(a,b)      /

                   a+b;        /

                   a++;

   應用時:if(...)

             O(a,b); //産生錯誤

           else

   解決方法: #difne DO(a,b)       /

                 do{              /

                    a+b;          /

                    a++;          /

                 }while(0)

宏中"#"和"##"的用法

在C語言的宏中,#的功能是将其後面的宏參數進行字元串化操作(Stringfication),簡單說就是在對它所引用的宏變量通過替換後在其左右各加上一個雙引号。

而##被稱為連接配接符(concatenator),用來将兩個Token連接配接為一個Token.注意這裡連接配接的對象是Token就行,而不一定是宏的變量。

1. 一般用法

   我們使用#把宏參數變為一個字元串,用##把兩個宏參數貼合在一起.

   #include <stdio.h>

   #include <stdLib.h>

   #define STR(s)     #s

   #define CONS(a,b)  int(a##e##b)

   int main()

   {

       Printf(STR(vck));           // 輸出字元串"vck"

       printf("%d/n", CONS(2,3));  // 2e3 輸出:2000

       return 0;

   }

2. 當宏參數是另一個宏的時候

   需要注意的是凡宏定義裡有用"#"或"##"的地方宏參數是不會再展開.

(1)非'#'和'##'的情況

   #define TOW      (2)

   #define MUL(a,b) (a*b)

   printf("%d*%d=%d/n", TOW, TOW, MUL(TOW,TOW));

   這行的宏會被展開為:

   printf("%d*%d=%d/n", (2), (2), ((2)*(2)));

   MUL裡的參數TOW會被展開為(2).

(2)當有'#'或'##'的時候

   #define A          (2)

   #define STR(s)     #s

   #define CONS(a,b)  int(a##e##b)

   printf("int max: %s/n",  STR(INT_MAX));    // INT_MAX #include<climits>

   這行會被展開為:

   printf("int max: %s/n", "INT_MAX");

   printf("%s/n", CONS(A,A));               // compile error

   這一行則被展開為:

   printf("%s/n", int(AeA));

   INT_MAX和A都不會再被展開, 然而解決這個問題的方法很簡單. 加多一層中間轉換宏.

   加這層宏的用意是把所有宏的參數在這層裡全部展開, 那麼在轉換宏裡的那一個宏(_STR)就能得到正确的宏參數

   #define A           (2)

   #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,int型的最大值

   輸出為: int max: 0x7fffffff

   STR(INT_MAX) -->  _STR(0x7fffffff) 然後再轉換成字元串;

   printf("%d/n", CONS(A, A));

   輸出為:200

   CONS(A, A)  -->  _CONS((2), (2))  --> int((2)e(2))

3. "#"和"##"的一些應用特例

(1)合并匿名變量名

   #define  ___ANONYMOUS1(type, var, line)  type  var##line

   #define  __ANONYMOUS0(type, line)  ___ANONYMOUS1(type, _anonymous, line)

   #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)填充結構

   #define  FILL(a)   {a, #a}

   enum IDD{OPEN, CLOSE};

   typedef struct MSG{

     IDD id;

     const char * msg;

   }MSG;

   MSG _msg[] = {FILL(OPEN), FILL(CLOSE)};

   相當于:

   MSG _msg[] = {{OPEN,  "OPEN"},

                 {CLOSE, "CLOSE"}};

(3)記錄檔案名

   #define  _GET_FILE_NAME(f)   #f

   #define  GET_FILE_NAME(f)    _GET_FILE_NAME(f)

   static char  FILE_NAME[] = GET_FILE_NAME(__FILE__);

(4)得到一個數值類型所對應的字元串緩沖大小

   #define  _TYPE_BUF_SIZE(type)  sizeof #type

   #define  TYPE_BUF_SIZE(type)   _TYPE_BUF_SIZE(type)

   char  buf[TYPE_BUF_SIZE(INT_MAX)];

     -->  char  buf[_TYPE_BUF_SIZE(0x7fffffff)];

     -->  char  buf[sizeof "0x7fffffff"];

   這裡相當于:

   char  buf[11];

......符号的使用

   ......在C宏中稱為Variadic Macro,也就是變參宏。比如:

   #define myprintf(templt, ......) fprintf(stderr,templt,__VA_ARGS__)

   // 或者

   #define myprintf(templt, args......) fprintf(stderr,templt,args)

   第一個宏中由于沒有對變參起名,我們用預設的宏__VA_ARGS__來替代它。第二個宏中,我們顯式地命名變參為args,那麼我們在宏定義中就可以用args來代指變參了。同C語言的stdcall一樣,變參必須作為參數表的最後一項出現。當上面的宏中我們隻能提供第一個參數templt時,C标準要求我們必須寫成:myprintf(templt,);的形式。這時的替換過程為:

   myprintf("Error!/n",);

   替換為:

   fprintf(stderr,"Error!/n",);

   這是一個文法錯誤,不能正常編譯。這個問題一般有兩個解決方法。首先,GNU CPP提供的解決方法允許上面的宏調用寫成:

   myprintf(templt);

   而它将會被通過替換變成:

   fprintf(stderr,"Error!/n",);

   很明顯,這裡仍然會産生編譯錯誤(非本例的某些情況下不會産生編譯錯誤)。除了這種方式外,c99和GNU CPP都支援下面的宏定義方式:

   #define myprintf(templt,......) fprintf(stderr,templt, ##__VAR_ARGS__)

   這時,##這個連接配接符号充當的作用就是當__VAR_ARGS__為空的時候,消除前面的那個逗号。那麼此時的翻譯過程如下:

   myprintf(templt);

   被轉化為:

   fprintf(stderr,templt);

   這樣如果templt合法,将不會産生編譯錯誤。

   另外,在vxworks中,還可以允許下面的宏定義:

   #define myprintf(arg...)        printf(arg)

   宏的第一個參數就設定為變參,是以下面的幾種使用方式都是正确的:

   myprintf("number");

   myprintf("number %d",2);

   myprintf("number %d %d",2,3);

宏使用中的陷阱

   這裡列出了一些宏使用中容易出錯的地方,以及合适的使用方式。

   錯誤的嵌套-Misnesting

   宏的定義不一定要有完整的、配對的括号,但是為了避免出錯并且提高可讀性,最好避免這樣使用。

   由操作符優先級引起的問題-Operator Precedence Problem

   由于宏隻是簡單的替換,宏的參數如果是複合結構,那麼通過替換之後可能由于各個參數之間的操作符優先級高于單個參數内部各部分之間互相作用的操作符優先級,如果我們不用括号保護各個宏參數,可能會産生預想不到的情形。比如:

   #define ceil_div(x, y) (x + y - 1) / y

   那麼

   a = ceil_div( b & c, sizeof(int) );

   将被轉化為:

   a = ( b & c + sizeof(int) - 1) / sizeof(int);

   // 由于+/-的優先級高于&的優先級,那麼上面式子等同于:

   a = ( b & (c + sizeof(int) - 1)) / sizeof(int);

   這顯然不是調用者的初衷。為了避免這種情況發生,應當多寫幾個括号:

   define ceil_div(x, y) (((x) + (y) - 1) / (y))

   消除多餘的分号-Semicolon Swallowing

   通常情況下,為了使函數模樣的宏在表面上看起來像一個通常的C語言調用一樣,通常情況下我們在宏的後面加上一個分号,比如下面的帶參宏:

   MY_MACRO(x);

   但是如果是下面的情況:

   #define MY_MACRO(x) { /

   /

   /

   }

   //……

   if (condition())

       MY_MACRO(a);

   else

       {......}

   這樣會由于多出的那個分号産生編譯錯誤。為了避免這種情況出現同時保持MY_MACRO(x);的這種寫法,我們需要把宏定義為這種形式:

   #define MY_MACRO(x) do {   /

   /

   /

   } while(0)

   這樣隻要保證總是使用分号,就不會有任何問題。

   Duplication of Side Effects

   這裡的Side Effect是指宏在展開的時候對其參數可能進行多次Evaluation(也就是取值),但是如果這個宏參數是一個函數,那麼就有可能被調用多次進而達到不一緻的結果,甚至會發生更嚴重的錯誤。比如:

   #define min(X,Y) ((X) > (Y) ? (Y) : (X))

   //......

   c = min(a,foo(b));

   這時foo()函數就被調用了兩次。為了解決這個潛在的問題,我們應當這樣寫min(X,Y)這個宏:

   #define min(X,Y) ({ /

   typeof (X) x_ = (X); /

   typeof (Y) y_ = (Y); /

   (x_ < y_) ? x_ : y_;})

   ({......})的作用是将内部的幾條語句中最後一條的值傳回,它也允許在内部聲明變量(因為它通過大括号組成了一個局部Scope)。

http://blog.pfan.cn/lingdlz/31278.html

寫好C語言,漂亮的宏定義很重要,使用宏定義可以防止出錯,提高可移植性,可讀性,友善性 等等。下面列舉一些成熟軟體中常用得宏定義。。。。。。

1,防止一個頭檔案被重複包含

#ifndef COMDEF_H

#define COMDEF_H

  //頭檔案内容

#endif

2,重新定義一些類型,防止由于各種平台和編譯器的不同,而産生的類型位元組數差異,友善移植。

typedef  unsigned char      boolean;     

typedef  unsigned long int  uint32;      

typedef  unsigned short     uint16;      

typedef  unsigned char      uint8;       

typedef  signed long int    int32;       

typedef  signed short       int16;       

typedef  signed char        int8;        

//下面的不建議使用

typedef  unsigned char     byte;         

typedef  unsigned short    word;         

typedef  unsigned long     dword;        

typedef  unsigned char     uint1;        

typedef  unsigned short    uint2;        

typedef  unsigned long     uint4;        

typedef  signed char       int1;         

typedef  signed short      int2;         

typedef  long int          int4;         

typedef  signed long       sint31;       

typedef  signed short      sint15;       

typedef  signed char       sint7;        

3,得到指定位址上的一個位元組或字

#define  MEM_B( x )  ( *( (byte *) (x) ) )

#define  MEM_W( x )  ( *( (word *) (x) ) )

4,求最大值和最小值

   #define  MAX( x, y ) ( ((x) > (y)) ? (x) : (y) )

   #define  MIN( x, y ) ( ((x) < (y)) ? (x) : (y) )

5,得到一個field在結構體(struct)中的偏移量

#define FPOS( type, field ) /

( (dword) &(( type *) 0)-> field )

6,得到一個結構體中field所占用的位元組數

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

7,按照LSB格式把兩個位元組轉化為一個Word

#define  FLIPW( ray ) ( (((word) (ray)[0]) * 256) + (ray)[1] )

8,按照LSB格式把一個Word轉化為兩個位元組

#define  FLOPW( ray, val ) /

  (ray)[0] = ((val) / 256); /

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

9,得到一個變量的位址(word寬度)

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

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

10,得到一個字的高位和低位位元組

#define  WORD_LO(***)  ((byte) ((word)(***) & 255))

#define  WORD_HI(***)  ((byte) ((word)(***) >> 8))

11,傳回一個比X大的最接近的8的倍數

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

12,将一個字母轉換為大寫

#define  UPCASE( c ) ( ((c) >= 'a' && (c) <= 'z') ? ((c) - 0x20) : (c) )

13,判斷字元是不是10進值的數字

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

14,判斷字元是不是16進值的數字

#define  HEXCHK( 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( a )  ( sizeof( (a) ) / sizeof( (a[0]) ) )

17,傳回一個無符号數n尾的值MOD_BY_POWER_OF_TWO(X,n)=X%(2^n)

#define MOD_BY_POWER_OF_TWO( val, mod_by ) /

           ( (dword)(val) & (dword)((mod_by)-1) )

18,對于IO空間映射在存儲空間的結構,輸入輸出處理

  #define inp(port)         (*((volatile byte *) (port)))

  #define inpw(port)        (*((volatile word *) (port)))

  #define inpdw(port)       (*((volatile dword *)(port)))

  #define outp(port, val)   (*((volatile byte *) (port)) = ((byte) (val)))

  #define outpw(port, val)  (*((volatile word *) (port)) = ((word) (val)))

  #define outpdw(port, val) (*((volatile dword *) (port)) = ((dword) (val)))

[2005-9-9添加]

19,使用一些宏跟蹤調試

A N S I标準說明了五個預定義的宏名。它們是:

_ L I N E _

_ F I L E _

_ D A T E _

_ T I M E _

_ S T D C _

如果編譯不是标準的,則可能僅支援以上宏名中的幾個,或根本不支援。記住編譯程式

也許還提供其它預定義的宏名。

_ L I N E _及_ F I L E _宏指令在有關# l i n e的部分中已讨論,這裡讨論其餘的宏名。

_ D AT E _宏指令含有形式為月/日/年的串,表示源檔案被翻譯到代碼時的日期。

源代碼翻譯到目标代碼的時間作為串包含在_ T I M E _中。串形式為時:分:秒。

如果實作是标準的,則宏_ S T D C _含有十進制常量1。如果它含有任何其它數,則實作是

非标準的。

可以定義宏,例如:

當定義了_DEBUG,輸出資料資訊和所在檔案所在行

#ifdef _DEBUG

#define DEBUGMSG(msg,date) printf(msg);printf(“%d%d%d”,date,_LINE_,_FILE_)

#else

      #define DEBUGMSG(msg,date)

#endif

20,宏定義防止使用是錯誤

用小括号包含。

例如:#define ADD(a,b) (a+b)

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

例如:#difne DO(a,b) a+b;/

                   a++;

應用時:if(….)

          DO(a,b); //産生錯誤

        else

C語言中如何使用宏

C(和C++)中的宏(Macro)屬于編譯器預處理的範疇,屬于編譯期概念(而非運作期概念)。下面對常遇到的宏的使用問題做了簡單總結。

宏使用中的常見的基礎問題

       #符号和##符号的使用

       ...符号的使用

       宏的解釋方法

  我們能碰到的宏的使用

  宏使用中的陷阱

常見的基礎性問題

       關于#和##

在C語言的宏中,#的功能是将其後面的宏參數進行字元串化操作(Stringfication),簡單說就是在對它所引用的宏變量通過替換後在其左右各加上一個雙引号。比如下面代碼中的宏:

#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的時候便會在标準錯誤流上輸出一個提示資訊。

而##被稱為連接配接符(concatenator),用來将兩個Token連接配接為一個Token。注意這裡連接配接的對象是Token就行,而不一定是宏的變量。比如你要做一個菜單項指令名和函數指針組成的結構體的數組,并且希望在函數名和菜單項指令名之間有直覺的、名字上的關系。那麼下面的代碼就非常實用:

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;

       關于...的使用

...在C宏中稱為Variadic Macro,也就是變參宏。比如:

#define myprintf(templt,...) fprintf(stderr,templt,__VA_ARGS__)

// 或者

#define myprintf(templt,args...) fprintf(stderr,templt,args)

第一個宏中由于沒有對變參起名,我們用預設的宏__VA_ARGS__來替代它。第二個宏中,我們顯式地命名變參為args,那麼我們在宏定義中就可以用args來代指變參了。同C語言的stdcall一樣,變參必須作為參數表的最有一項出現。當上面的宏中我們隻能提供第一個參數templt時,C标準要求我們必須寫成:

myprintf(templt,);

的形式。這時的替換過程為:

myprintf("Error!/n",);

      替換為:

fprintf(stderr,"Error!/n",);

這是一個文法錯誤,不能正常編譯。這個問題一般有兩個解決方法。首先,GNU CPP提供的解決方法允許上面的宏調用寫成:

myprintf(templt);

而它将會被通過替換變成:

fprintf(stderr,"Error!/n",);

很明顯,這裡仍然會産生編譯錯誤(非本例的某些情況下不會産生編譯錯誤)。除了這種方式外,c99和GNU CPP都支援下面的宏定義方式:

#define myprintf(templt, ...) fprintf(stderr,templt, ##__VAR_ARGS__)

這時,##這個連接配接符号充當的作用就是當__VAR_ARGS__為空的時候,消除前面的那個逗号。那麼此時的翻譯過程如下:

myprintf(templt);

被轉化為:

fprintf(stderr,templt);

這樣如果templt合法,将不會産生編譯錯誤。

       宏是如何解釋的

宏在日常程式設計中的常見使用

宏使用中的陷阱

這裡列出了一些宏使用中容易出錯的地方,以及合适的使用方式。

       錯誤的嵌套-Misnesting

宏的定義不一定要有完整的、配對的括号,但是為了避免出錯并且提高可讀性,最好避免這樣使用。

由操作符優先級引起的問題-Operator Precedence Problem

由于宏隻是簡單的替換,宏的參數如果是複合結構,那麼通過替換之後可能由于各個參數之間的操作符優先級高于單個參數内部各部分之間互相作用的操作符優先級,如果我們不用括号保護各個宏參數,可能會産生預想不到的情形。比如:

#define ceil_div(x, y) (x + y - 1) / y

那麼

a = ceil_div( b & c, sizeof(int) );

将被轉化為:

a = ( b & c      + sizeof(int) - 1) / sizeof(int);

// 由于+/-的優先級高于&的優先級,那麼上面式子等同于:

a = ( b & (c + sizeof(int) - 1)) / sizeof(int);

這顯然不是調用者的初衷。為了避免這種情況發生,應當多寫幾個括号:

define ceil_div(x, y) (((x) + (y) - 1) / (y))

消除多餘的分号-Semicolon Swallowing

通常情況下,為了使函數模樣的宏在表面上看起來像一個通常的C語言調用一樣,通常情況下我們在宏的後面加上一個分号,比如下面的帶參宏:

MY_MACRO(x);

但是如果是下面的情況:

#define MY_MACRO(x) { /

      /

      /

      }

      //...

if (condition())

      MY_MACRO(a);

else

      {...}

這樣會由于多出的那個分号産生編譯錯誤。為了避免這種情況出現同時保持MY_MACRO(x);的這種寫法,我們需要把宏定義為這種形式:

#define MY_MACRO(x) do {

      /

      /

      } while(0)

這樣隻要保證總是使用分号,就不會有任何問題。

       Duplication of Side Effects

這裡的Side Effect是指宏在展開的時候對其參數可能進行多次Evaluation(也就是取值),但是如果這個宏參數是一個函數,那麼就有可能被調用多次進而達到不一緻的結果,甚至會發生更嚴重的錯誤。比如:

#define min(X,Y) ((X) > (Y) ? (Y) : (X))

//...

c = min(a,foo(b));

這時foo()函數就被調用了兩次。為了解決這個潛在的問題,我們應當這樣寫min(X,Y)這個宏:

#define min(X,Y) ({ /

      typeof (X) x_ = (X); /

      typeof (Y) y_ = (Y); /

      (x_ < y_) ? x_ : y_; })

({...})的作用是将内部的幾條語句中最後一條的值傳回,它也允許在内部聲明變量(因為它通過大括号組成了一個局部Scope)。

補充:

1、#define

指令#define定義了一個辨別符及一個串。在源程式中每次遇到該辨別符時,均以定義的串代換它。ANSI标準将辨別符定義為宏名,将替換過程稱為宏

替換。指令的一般形式為:

#define identifier string

注意:

? 該語句沒有分号。在辨別符和串之間可以有任意個空格,串一旦開始,僅由一新行結束。

? 宏名定義後,即可成為其它宏名定義中的一部分。

? 宏替換僅僅是以文本串代替宏辨別符,前提是宏辨別符必須獨立的識别出來,否則不進行替換。例如: #define XYZ

this is a test,使用宏printf("XYZ");//該段不列印"this is a test"而列印"XYZ"。因為預編譯器識

别出的是"XYZ"

? 如果串長于一行,可以在該行末尾用一反斜杠' /'續行。

2、#error

處理器指令#error強迫編譯程式停止編譯,主要用于程式調試。

3、#i nclude

指令#i nclude使編譯程式将另一源檔案嵌入帶有#i nclude的源檔案,被讀入的源檔案必須用雙引号或尖括号括起來。例如:

#i nclude"stdio.h"或者#i nclude

這兩行代碼均使用C編譯程式讀入并編譯用于處理磁盤檔案庫的子程式。

将檔案嵌入#i nclude指令中的檔案内是可行的,這種方式稱為嵌套的嵌入檔案,嵌套層次依賴于具體實作。

如果顯式路徑名為檔案辨別符的一部分,則僅在哪些子目錄中搜尋被嵌入檔案。否則,如果檔案名用雙引号括起來,則首先檢索目前工作目錄。如果未發現檔案,

則在指令行中說明的所有目錄中搜尋。如果仍未發現檔案,則搜尋實作時定義的标準目錄。

如果沒有顯式路徑名且檔案名被尖括号括起來,則首先在編譯指令行中的目錄内檢索。

如果檔案沒找到,則檢索标準目錄,不檢索目前工作目錄。

4、條件編譯指令

有幾個指令可對程式源代碼的各部分有選擇地進行編譯,該過程稱為條件編譯。商業軟體公司廣泛應用條件編譯來提供和維護某一程式的許多顧客版本。

#if、#else,#elif及#endif

#if的一般含義是如果#if後面的常量表達式為true,則編譯它與#endif之間的代碼,否則跳過這些代碼。指令#endif辨別一個#if塊的

結束。

#if constant-expression

statement sequence

#endif

跟在#if後面的表達式在編譯時求值,是以它必須僅含常量及已定義過的辨別符,不可使用變量。表達式不許含有操作符sizeof(sizeof也是編譯

時求值)。

#else指令的功能有點象C語言中的else;#else建立另一選擇(在#if失敗的情況下)。

注意,# else屬于# if塊。

#elif指令意義與ELSE IF 相同,它形成一個if else-if階梯狀語句,可進行多種編譯選擇。

#elif 後跟一個常量表達式。如果表達式為true,則編譯其後的代碼塊,不對其它#elif表達式進行測試。否則,順序測試下一塊。

#if expression

statement sequence

#elif expression1

statement sequence

#endif

在嵌套的條件編譯中#endif、#else或#elif與最近#if或#elif比對。

# ifdef 和# ifndef

條件編譯的另一種方法是用#ifdef與#ifndef指令,它們分别表示"如果有定義"及"如果無定義"。

# ifdef的一般形式是:

# ifdef macroname

statement sequence

#endif

#ifdef與#ifndef可以用于#if、#else,#elif語句中,但必須與一個#endif。

5、#undef

指令#undef 取消其後那個前面已定義過有宏名定義。一般形式為:

#undef macroname

6、#line

指令# line改變__LINE__與__FILE__的内容,它們是在編譯程式中預先定義的辨別符。指令的基本形式如下:

# line number["filename"]

其中的數字為任何正整數,可選的檔案名為任意有效檔案辨別符。行号為源程式中目前行号,檔案名為源檔案的名字。指令# line主要用于調試及其它特殊

應用。

注意:在#line後面的數字辨別從下一行開始的數字辨別。

7、預定義的宏名

ANSI标準說明了C中的五個預定義的宏名。它們是:

__LINE__

__FILE__

__DATE__

__TIME__

__STDC__

如果編譯不是标準的,則可能僅支援以上宏名中的幾個,或根本不支援。記住編譯程式也許還提供其它預定義的宏名。

__LINE__及__FILE__宏指令在有關# line的部分中已讨論,這裡讨論其餘的宏名。

__DATE__宏指令含有形式為月/日/年的串,表示源檔案被翻譯到代碼時的日期。

源代碼翻譯到目标代碼的時間作為串包含在__TIME__中。串形式為時:分:秒。

如果實作是标準的,則宏__STDC__含有十進制常量1。如果它含有任何其它數,則實作是非标準的。編譯C++程式時,編譯器自動定義了一個預處理名

字__cplusplus,而編譯标準C時,自動定義名字__STDC__。

注意:宏名的書寫由辨別符與兩邊各二條下劃線構成。

8、C、C++宏體中出現的#,#@,##

宏體中,#的功能是将其後面的宏參數進行字元串化操作(Stringfication),簡單說就是在對它所引用的宏變量通過替換後在其左右各加上一個

雙引号。

而##被稱為連接配接符(concatenator),用來将兩個Token連接配接為一個Token。注意這裡連接配接的對象是Token就行,而不一定是宏的變

量。比如你要做一個菜單項指令名和函數指針組成的結構體的數組,并且希望在函數名和菜單項指令名之間有直覺的、名字上的關系。那就可以使用:宏參數##

固定部分。當然還可以n個##符号連接配接 n+1個Token,這個特性也是#符号所不具備的。

#@的功能是将其後面的宏參數進行字元化。

9、C宏中的變參...

...在C宏中稱為Variadic Macro,也就是變參宏。比如:

#define myprintf(templt,...) fprintf(stderr,templt,__VA_ARGS__)

或者#define myprintf(templt,args...) fprintf(stderr,templt,args)

第一個宏中由于沒有對變參起名,我們用預設的宏__VA_ARGS__來替代它。第二個宏中,我們顯式地命名變參為args,那麼我們在宏定義中就可以

用args來代指變參了。同C語言的stdcall一樣,變參必須作為參數表的最後有一項出現。當上面的宏中我們隻能提供第一個參數templt時,C

标準要求我們必須寫成: myprintf(templt,);的形式。這時的替換過程為:myprintf("Error!/n",);替換為:

fprintf(stderr,"Error!/n",).

這是一個文法錯誤,不能正常編譯。這個問題一般有兩個解決方法。首先,GNU CPP提供的解決方法允許上面的宏調用寫成:

myprintf(templt);而它将會被通過替換變成: fprintf(stderr,"Error!/n",);

很明顯,這裡仍然會産生編譯錯誤(非本例的某些情況下不會産生編譯錯誤)。除了這種方式外,c99和GNU CPP都支援下面的宏定義方式:

#define myprintf(templt, ...) fprintf(stderr,templt, ##__VAR_ARGS__)

這時,##這個連接配接符号充當的作用就是當__VAR_ARGS__為空的時候,消除前面的那個逗号。那麼此時的翻譯過程如下:

myprintf(templt);被轉化為: fprintf(stderr,templt);

這樣如果templt合法,将不會産生編譯錯誤。

10、#pragma的使用【轉載】

在所有的預處理指令中,#Pragma 指令可能是最複雜的了,它的作用是設定編譯器的狀态或者是訓示編譯器完成一些特定的動作。#pragma指令對

每個編譯器給出了一個方法,在保持與C和C ++語言完全相容的情況下,給出主機或作業系統專有的特征。依據定義,編譯訓示是機器或作業系統專有的,且

對于每個編譯器都是不同的。

其格式一般為: #Pragma Para,其中Para 為參數,下面來看一些常用的參數。

(1)message 參數。 Message 參數是我最喜歡的一個參數,它能夠在編譯資訊輸出視窗中輸出相應的資訊,這對于源代碼資訊的控制是非常

重要的。其使用方法為:

#Pragma message("消息文本")

當編譯器遇到這條指令時就在編譯輸出視窗中将消息文本列印出來。

當我們在程式中定義了許多宏來控制源代碼版本的時候,我們自己有可能都會忘記有沒有正确的設定這些宏,此時我們可以用這條指令在編譯的時候就進行檢查。

假設我們希望判斷自己有沒有在源代碼的什麼地方定義了_X86這個宏可以用下面的方法

#ifdef _X86

#Pragma message("_X86 macro activated!")

#endif

當我們定義了_X86這個宏以後,應用程式在編譯時就會在編譯輸出視窗裡顯示"_

X86 macro activated!"。我們就不會因為不記得自己定義的一些特定的宏而抓耳撓腮了。

(2)另一個使用得比較多的pragma參數是code_seg。格式如:

#pragma code_seg( ["section-name"[,"section-class"] ] )

它能夠設定程式中函數代碼存放的代碼段,當我們開發驅動程式的時候就會使用到它。

(3)#pragma once (比較常用)

隻要在頭檔案的最開始加入這條指令就能夠保證頭檔案被編譯一次,這條指令實際上在VC6中就已經有了,但是考慮到相容性并沒有太多的使用它。

(4)#pragma hdrstop表示預編譯頭檔案到此為止,後面的頭檔案不進行預編譯。BCB可以預編譯頭檔案以加快連結的速度,但如果所有頭文

件都進行預編譯又可能占太多磁盤空間,是以使用這個選項排除一些頭檔案。

有時單元之間有依賴關系,比如單元A依賴單元B,是以單元B要先于單元A編譯。你可以用#pragma startup指定編譯優先級,如果使用了

#pragma package(smart_init) ,BCB就會根據優先級的大小先後編譯。

(5)#pragma resource "*.dfm"表示把*.dfm檔案中的資源加入工程。*.dfm中包括窗體、外觀的定義。

(6)#pragma warning( disable : 4507 34; once : 4385; error : 164 )

等價于:

#pragma warning(disable:4507 34) // 不顯示4507和34号警告資訊

#pragma warning(once:4385) // 4385号警告資訊僅報告一次

#pragma warning(error:164) // 把164号警告資訊作為一個錯誤。

同時這個pragma warning 也支援如下格式:

#pragma warning( push [ ,n ] )

#pragma warning( pop )

這裡n代表一個警告等級(1---4)。

#pragma warning( push )儲存所有警告資訊的現有的警告狀态。

#pragma warning( push, n)儲存所有警告資訊的現有的警告狀态,并且把全局警告等級設定為n。

#pragma warning( pop )向棧中彈出最後一個警告資訊,在入棧和出棧之間所作的一切改動取消。例如:

#pragma warning( push )

#pragma warning( disable : 4705 )

#pragma warning( disable : 4706 )

#pragma warning( disable : 4707 )

//.......

#pragma warning( pop )

在這段代碼的最後,重新儲存所有的警告資訊(包括4705,4706和4707)。

(7)pragma comment(...)

該指令将一個注釋記錄放入一個對象檔案或可執行檔案中。

常用的lib關鍵字,可以幫我們連入一個庫檔案。

(8)用pragma導出dll中的函數

傳統的到出 DLL 函數的方法是使用子產品定義檔案 (.def),Visual C++ 提供了更簡潔友善的方法,那就

是"__declspec()"關鍵字後面跟"dllexport",告訴連接配接去要導出這個函數,例如:

__declspec(dllexport) int __stdcall MyExportFunction(int iTest);

把"__declspec(dllexport)"放在函數聲明的最前面,連接配接生成的 DLL 就會導出函

數"[email protected]"。

上面的導出函數的名稱也許不是我的希望的,我們希望導出的是原版的"MyExportFunction"。還好,VC 提供了一個預處理訓示

符"#pragma"來指定連接配接選項 (不僅僅是這一個功能,還有很多訓示功能) ,如下:

#pragma comment(linker,"/EXPORT:[email protected]")

這下就天如人願了:)。如果你想指定導出的順序,或者隻将函數導出為序号,沒有 Entryname,這個預處理訓示符 (确切地說是連接配接器) 都能夠 實作,看看 MSDN 的文法說明:

/EXPORT:entryname[,@ordinal[,NONAME]][,DATA]

@ordinal 指定順序;NONAME 指定隻将函數導出為序号;DATA 關鍵字指定導出項為資料項。

⑨每個編譯程式可以用#pragma指令激活或終止該編譯程式支援的一些編譯功能。例如,對循環優化功能:

#pragma loop_opt(on) // 激活

#pragma loop_opt(off) // 終止

有時,程式中會有些函數會使編譯器發出你熟知而想忽略的警告,如"Parameter xxx is never used in function xxx",可以這樣:

#pragma warn -100 // Turn off the warning message for warning #100

int insert_record(REC *r)

{ }

#pragma warn +100 // Turn the warning message for warning #100 back on

函數會産生一條有唯一特征碼100的警告資訊,如此可暫時終止該警告。

每個編譯器對#pragma的實作不同,在一個編譯器中有效在别的編譯器中幾乎無效。可從編譯器的文檔中檢視。

⑩#pragm pack()的使用

#pragma pack規定的對齊長度,實際使用的規則是:

? 結構,聯合,或者類的資料成員,第一個放在偏移為0的地方,以後每個資料成員的對齊,按照#pragma pack指定的數值和這

個資料成員自身長度中,比較小的那個進行。

? 也就是說,當#pragma pack的值等于或超過所有資料成員長度的時候,這個值的大小将不産生任何效果。

? 而結構整體的對齊,則按照結構體中最大的資料成員 和 #pragma pack指定值之間,較小的那個進行。

注意:檔案使用#pragma pack(n) 改變了預設設定而不恢複,通常可以使用#pragma pack(push, n)和#pragma

pack(pop)進行設定與恢複。

注:關于宏函數的内容在另外的專題。關于宏使用的誤區在描述宏的時候已經在文中提到了,最後再給出一個例子,描述的Side Effect是指宏在展開

的時候對其參數可能進行多次Evaluation(也就是取值)對程式造成的錯誤影響。

假設在一個系統中,有一個32b的寄存器(REG)儲存狀态,其中高16b表示一種含義,低16b表示另一種含義(這在程式中經常出現)。現在要把高低

16b分開,不考慮實際中的特殊要求,将代碼寫成:

#define High16bit(REG) (REG>>16)

#define Low16bit(REG) ((REG<<16)>>16)

對于這種寫法完成的功能在大多數情況是足夠了,這裡不讨論。主要談論這種寫法的負面影響,如果在程式中分别在不同的語句中使用High16bit和

Low16bit,那麼就可能那就是Side effect,特别寄存器REG是狀态寄存器,他的狀态可能随時變化,那麼引起的問題就是高低16b根本

取的不是同一個時刻狀态寄存器。這種錯誤在程式中找出就比較難了。在這裡我把條件弱化了,試想在一個宏體中,如果對參數多次取值也是可能引起問題,那就 更難了。