C語言宏高級用法


1、前言

    今天看代碼時候,遇到一些宏,之前沒有見過,感覺挺新鮮。如是上網google一下,順便總結一下,方便以后學習和運用。C語言程序中廣泛的使用宏定義,采用關鍵字define進行定義,宏只是一種簡單的字符串替換,根據是否帶參數分為無參和帶參。宏的簡單應用很容易掌握,今天主要總結一下宏的特殊符號及慣用法。

  (1)宏中包含特殊符號:#、##.

      (2)宏定義用do{ }while(0)

2、特殊符號#、##

(1)#

 When you put a # before an argument in a preprocessor  macro, the preprocessor turns that argument into a character array. 

 在一個宏中的參數前面使用一個#,預處理器會把這個參數轉換為一個字符數組 

 簡化理解:#是“字符串化”的意思,出現在宏定義中的#是把跟在后面的參數轉換成一個字符串

#define ERROR_LOG(module) fprintf(stderr,"error: "#module"\n")

ERROR_LOG("add"); 轉換為 fprintf(stderr,"error: "add"\n");

ERROR_LOG(devied =0); 轉換為 fprintf(stderr,"error: devied=0\n");

(2)##

  “##”是一種分隔連接方式,它的作用是先分隔,然后進行強制連接。

  在普通的宏定義中,預處理器一般把空格解釋成分段標志,對於每一段和前面比較,相同的就被替換。但是這樣做的結果是,被替換段之間存在一些空格。如果我們不希望出現這些空格,就可以通過添加一些##來替代空格。

1 #define TYPE1(type,name) type name_##type##_type 2 #define TYPE2(type,name) type name##_##type##_type

TYPE1(int, c); 轉換為:int  name_int_type ; (因為##號將后面分為 name_ 、type 、 _type三組,替換后強制連接)
TYPE2(int, d);轉換為: int  d_int_type ; (因為##號將后面分為 name、_、type 、_type四組,替換后強制連接)

3、宏定義中do{ }while(0)

   第一眼看到這樣的宏時,覺得非常奇怪,為什么要用do……while(0)把宏定義的多條語句括起來?非常想知道這樣定義宏的好處是什么,於是google、百度一下了。

    采用這種方式是為了防范在使用宏過程中出現錯誤,主要有如下幾點:

  (1)空的宏定義避免warning:
  #define foo() do{}while(0)
  (2)存在一個獨立的block,可以用來進行變量定義,進行比較復雜的實現。
  (3)如果出現在判斷語句過后的宏,這樣可以保證作為一個整體來是實現:
      #define foo(x) \
        action1(); \
        action2();
    在以下情況下:
    if(NULL == pPointer)
         foo();
    就會出現action1和action2不會同時被執行的情況,而這顯然不是程序設計的目的。
  (4)以上的第3種情況用單獨的{}也可以實現,但是為什么一定要一個do{}while(0)呢,看以下代碼:
      #define switch(x,y) {int tmp; tmp="x";x=y;y=tmp;}
      if(x>y)
        switch(x,y);
      else       //error, parse error before else
      otheraction();
    在把宏引入代碼中,會多出一個分號,從而會報錯。這對這一點,可以將if和else語句用{}括起來,可以避免分號錯誤。
  使用do{….}while(0) 把它包裹起來,成為一個獨立的語法單元,從而不會與上下文發生混淆。同時因為絕大多數的編譯器都能夠識別do{…}while(0)這種無用的循環並進行優化,所以使用這種方法也不會導致程序的性能降低

4、測試程序

  簡單寫個測試程序,加強練習,熟悉一下宏的高級用法。

復制代碼
 1 #include <stdio.h>
 2  3 #define PRINT1(a,b) \  4  { \  5 printf("print a\n"); \  6 printf("print b\n"); \  7  }  8  9 #define PRINT2(a, b) \ 10 do{ \ 11 printf("print a\n"); \ 12 printf("print b\n"); \ 13 }while(0) 14 15 #define PRINT(a) \ 16 do{\ 17 printf("%s: %d\n",#a,a);\ 18 printf("%d: %d\n",a,a);\ 19 }while(0) 20 21 #define TYPE1(type,name) type name_##type##_type 22 #define TYPE2(type,name) type name##_##type##_type 23 24 #define ERROR_LOG(module) fprintf(stderr,"error: "#module"\n") 25 26  main() 27 { 28 int a = 20; 29 int b = 19; 30 TYPE1(int, c); 31 ERROR_LOG("add"); 32 name_int_type = a; 33 TYPE2(int, d); 34 d_int_type = a; 35 36  PRINT(a); 37 if (a > b) 38  { 39  PRINT1(a, b); 40  } 41 else 42  { 43  PRINT2(a, b); 44  } 45 return 0; 46 }
復制代碼

測試結果如下:

5、參考網址

http://blog.csdn.net/jiangjingui2011/article/details/6706967

http://www.kuqin.com/language/20080721/11906.html

http://www.360doc.com/content/12/0405/16/8302596_201146109.shtml

http://blog.csdn.net/liliangbao/article/details/4163440

 

 

 

宏定義中的#,##

1在一個預處理器宏中的參數前面使用一個#,預處理器會把這個參數轉換為一個字符數組。(原文:When you put a # before an argument in a preprocessor

macro, the preprocessor turns that argument into a character array. This,

combined with the fact that character arrays with no intervening punctuation are concatenated into a single character array, allows you to make a very convenient macro for printing the values of variables during debugging

#include "iostream"

using namespace std;

#define P(A) cout<<#A<<": "<<(A)<<endl;

int main()

{

int a=1,b=2;

P(a);

P(b);

P(a+b);

return 1;

}

http://blog.163.com/zhoumhan_0351/blog/static/39954227201032124942513/

2、#define D(a) cout << #a "=[" << a << "]" << endl;

3、#字符串化的意思。出現在宏定義中的#是把跟在后面的參數轉換成一個字符串

例如:
            > #define  FOO(arg)   my##arg
        則
            > FOO(abc)
        相當於   myabc

例如:
            > #define STRCPY(dst, src)   strcpy(dst, #src)
        則
            > STRCPY(buff, abc)
        相當於   strcpy(buff, "abc")

另外,如果##后的參數本身也是一個宏的話,##會阻止這個宏的展開,也就是只替換一次。

    #define STRCPY(a, b)    strcpy(a ## _p, #b)

    int main()

    {

        char var1_p[20];

        char var2_p[30];

         /* 注意這里 */

        STRCPY(STRCPY(var1,var2),var2);

        /* 這里是否會展開為: strcpy(strcpy(var1_p,"var2")_p,"var2“)?

         * 答案是否定的:

         * 展開結果將是:  strcpy(STRCPY(var1,var2)_p,"var2")

         * ## 阻止了參數的宏展開!

         * 如果宏定義里沒有用到 # ##, 宏將會完全展開

         */

    }  

http://blog.chinaunix.net/u/17855/showart_113663.html

4about ## in common text

1)關於記號粘貼操作符(token paste operator): ##

簡單的說,“##”是一種分隔連接方式,它的作用是先分隔,然后進行強制連接。

其中,分隔的作用類似於空格。我們知道在普通的宏定義中,預處理器一般把空格解釋成分段標志,對於每一段和前面比較,相同的就被替換。但是這樣做的結果是,被替換段之間存在一些空格。如果我們不希望出現這些空格,就可以通過添加一些##來替代空格。

另外一些分隔標志是,包括操作符,比如 +, -, *, /, [,], ...,所以盡管下面的

宏定義沒有空格,但是依然表達有意義的定義: define add(a, b)  a+b

而其強制連接的作用是,去掉和前面的字符串之間的空格,而把兩者連接起來。

2)舉列 -- 試比較下述幾個宏定義的區別

#define A1(name, type)  type name_##type##_type

#define A2(name, type)  type name##_##type##_type

A1(a1, int);  /* 等價於: int name_int_type; */

A2(a1, int);  /* 等價於: int a1_int_type;   */

解釋:

1) 在第一個宏定義中,"name"和第一個"_"之間,以及第2"_"和第二個"type"之間沒有被分隔,所以預處理器會把name_##type##_type解釋成3段:

name_”、“type”、以及“_type”,這中間只有“type”是在宏前面出現過

的,所以它可以被宏替換。

2) 而在第二個宏定義中,“name”和第一個“_”之間也被分隔了,所以

預處理器會把name##_##type##_type解釋成4段:“name”、“_”、“type

以及“_type”,這其間,就有兩個可以被宏替換了。

3) A1A2的定義也可以如下:

#define A1(name, type)  type name_  ##type ##_type 

<##前面隨意加上一些空格>

#define A2(name, type)  type name ##_ ##type ##_type

結果是## 會把前面的空格去掉完成強連接,得到和上面結果相同的宏定義

3)其他相關 -- 單獨的一個 #

至於單獨一個#,則表示對這個變量替換后,再加雙引號引起來。比

#define  __stringify_1(x)   #x

那么

__stringify_1(linux)   <==>  "linux"

 

5#stringizing)字符串化操作符。其作用是:將宏定義中的傳入參數名轉換成用一對雙引號括起來參數名字符串。其只能用於有傳入參數的宏定義中,且必須置於宏定義體中的參數名前。

如:

#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"

注意:

對空格的處理

a。忽略傳入參數名前面和后面的空格。

如:str=example1(   abc ); 將會被擴展成 str="abc"

b.當傳入參數名間存在空格時,編譯器將會自動連接各個子字符串,用每個子字符串中只以一個空格連接,忽略其中多於一個的空格。

如:str=exapme( abc    def); 將會被擴展成 str="abc def"

其它參考

1http://blog.chinaunix.net/u/17855/showart_113663.html

 

 

 

 

C語言宏的高級應用

 

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

#define WARN_IF(EXP)     
     do{ if (EXP)    
             fprintf(stderr, "Warning: " #EXP " "); }   
     while(0)

那么實際使用中會出現下面所示的替換過程:

WARN_IF (divider == 0);

被替換為

do {
     if (divider == 0)
fprintf(stderr, "Warning" "divider == 0" " ");
} 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!
",);

替換為:

fprintf(stderr,"Error! ",);

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

myprintf(templt);

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

fprintf(stderr,"Error!
",);

很明顯,這里仍然會產生編譯錯誤(非本例的某些情況下不會產生編譯錯誤)。除了這種方式外,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) {
/* line 1 */
/* line 2 */
/* line 3 */ }

//...

if (condition())
MY_MACRO(a);
else
{...}

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

#define MY_MACRO(x) do {
/* line 1 */
/* line 2 */
/* line 3 */ } 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)

 

 

 

宏定義中使用do{}while(0)的好處

#define MACRO_NAME(para) do{macro content}while(0)

的格式,總結了以下幾個原因:
1,空的宏定義避免warning:
#define foo() do{}while(0)
2,存在一個獨立的block,可以用來進行變量定義,進行比較復雜的實現。
3,如果出現在判斷語句過后的宏,這樣可以保證作為一個整體來是實現:
#define foo(x) /

 

action1(); /

 

action2();

 

在以下情況下:

 

if(NULL == pPointer)

 

   foo();

 

就會出現action1和action2不會同時被執行的情況,而這顯然不是程序設計的目的。
 
4,以上的第3種情況用單獨的{}也可以實現,但是為什么一定要一個do{}while(0)呢,看以下代碼:
#define switch(x,y) {int tmp; tmp="x";x=y;y=tmp;}
if(x>y)
switch(x,y);
else       //error, parse error before else
otheraction();
在把宏引入代碼中,會多出一個分號,從而會報錯。
 
//------------------------------------------------
使用 do{….}while(0) 把它包裹起來,成為一個獨立的語法單元,
從而不會與上下文發生混淆。同時因為絕大多數的編譯器都能夠識別do{…}while(0)這種無
用的循環並進行優化,所以使用這種方法也不會導致程序的性能降低

在C++中,有三種類型的循環語句:for, while, 和do...while, 但是在一般應用中作循環時, 我們可能用for和while要多一些,do...while相對不受重視。
    但是,最近在讀我們項目的代碼時,卻發現了do...while的一些十分聰明的用法,不是用來做循環,而是用作其他來提高代碼的健壯性。

1. do...while(0)消除goto語句。
通常,如果在一個函數中開始要分配一些資源,然后在中途執行過程中如果遇到錯誤則退出函數,當然,退出前先釋放資源,我們的代碼可能是這樣:
version 1

bool Execute()
{
   // 分配資源
   int *= new int;
   bool bOk(true);

   // 執行並進行錯誤處理
   bOk = func1();
   if(!bOk) 
   {
      delete p;   
      p = NULL;
      return false;
   }

   bOk = func2();
   if(!bOk) 
   {
      delete p;   
      p = NULL;
      return false;
   }

   bOk = func3();
   if(!bOk) 
   {
      delete p;   
      p = NULL;
      return false;
   }

   // ..........

   // 執行成功,釋放資源並返回
    delete p;   
    p = NULL;
    return true;
   
}


這里一個最大的問題就是代碼的冗余,而且我每增加一個操作,就需要做相應的錯誤處理,非常不靈活。於是我們想到了goto:
version 2

bool Execute()
{
   // 分配資源
   int *= new int;
   bool bOk(true);

   // 執行並進行錯誤處理
   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;
   
}


代碼冗余是消除了,但是我們引入了C++中身份比較微妙的goto語句,雖然正確的使用goto可以大大提高程序的靈活性與簡潔性,但太靈活的東西往往是很危險的,它會讓我們的程序捉摸不定,那么怎么才能避免使用goto語句,又能消除代碼冗余呢,請看do...while(0)循環:
version3

bool Execute()
{
   // 分配資源
   int *= new int;

   bool bOk(true);
   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)
如果你是C++程序員,我有理由相信你用過,或者接觸過,至少聽說過MFC, 在MFC的afx.h文件里面, 你會發現很多宏定義都是用了do...while(0)或do...while(false), 比如說:
#define AFXASSUME(cond)       do { bool __afx_condVal=!!(cond); ASSERT(__afx_condVal); __analysis_assume(__afx_condVal); } while(0)
粗看我們就會覺得很奇怪,既然循環里面只執行了一次,我要這個看似多余的do...while(0)有什么意義呢?
當然有!
為了看起來更清晰,這里用一個簡單點的宏來演示:
#define SAFE_DELETE(p) do{ delete p; p = NULL} while(0)
假設這里去掉do...while(0),
#define SAFE_DELETE(p) delete p; p = NULL;
那么以下代碼:
if(NULL != p) SAFE_DELETE(p)
else   ...do sth...
就有兩個問題,
1) 因為if分支后有兩個語句,else分支沒有對應的if,編譯失敗
2) 假設沒有else, SAFE_DELETE中的第二個語句無論if測試是否通過,會永遠執行。
你可能發現,為了避免這兩個問題,我不一定要用這個令人費解的do...while,  我直接用{}括起來就可以了
#define SAFE_DELETE(p) { delete p; p = NULL;}
的確,這樣的話上面的問題是不存在了,但是我想對於C++程序員來講,在每個語句后面加分號是一種約定俗成的習慣,這樣的話,以下代碼:
if(NULL != p) SAFE_DELETE(p);
else   ...do sth...
其else分支就無法通過編譯了(原因同上),所以采用do...while(0)是做好的選擇了。

也許你會說,我們代碼的習慣是在每個判斷后面加上{}, 就不會有這種問題了,也就不需要do...while了,如:
if(...)
{
}
else
{
}
誠然,這是一個好的,應該提倡的編程習慣,但一般這樣的宏都是作為library的一部分出現的,而對於一個library的作者,他所要做的就是讓其庫具有通用性,強壯性,因此他不能有任何對庫的使用者的假設,如其編碼規范,技術水平等


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM