CPP常識 04 -- 宏,#號##號,可變參數

文章來自于這里:
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+1Token,這個特性也是#符號所不具備的。比如:

#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__) 
// or
#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",); 

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

一些有意思的問題

  • 下面的代碼:
#define display(name) printf(""#name"") 
int main() { 
    display(name); 
} 

運行結果是name,為什么不是"#name"呢?

#在這里是字符串化的意思,printf(""#name"") 相當于 printf("" "name" "").

printf("" #name "") <1>
相當于printf("" "name" "") <2>
而<2>中的第2,3個“中間時空格 等價于("空+name+空')

## 連接符號由兩個井號組成,其功能是在帶參數的宏定義中將兩個子串(token)聯接起來,從而形成一個新的子串。但它不可以是第一個或者最后一個子串。所謂的子串 (token)就是指編譯器能夠識別的最小語法單元。具體的定義在編譯原理里有詳盡的解釋,但不知道也無所謂。同時值得注意的是#符是把傳遞過來的參數當成字符串進行替代。下面來看看它們是怎樣工作的。這是MSDN上的一個例子。

假設程序中已經定義了這樣一個帶參數的宏:

#define paster( n ) printf( "token" #n " = %d", token##n ) 

同時又定義了一個整形變量:

int token9 = 9; 

現在在主程序中以下面的方式調用這個宏:

paster(9); 

那么在編譯時,上面的這句話被擴展為:

printf( "token" "9" " = %d", token9 );

注意到在這個例子中,paster(9);中的這個”9”被原封不動的當成了一個字符串,與”token”連接在了一起,從而成為了token9。而#n也被”9”所替代。

可想而知,上面程序運行的結果就是在屏幕上打印出token9=9.

#define display(name) printf(""#name"") 
int main() { 
    display(name); 
} 

特殊性就在于它是個宏,宏里面處理#號就如LS所說!
處理后就是一個附加的字符串!

printf(""#name"");就不行了!

#define display(name) printf(""#name"") 

該定義字符串化name,得到結果其實就是printf("name")(前后的空字符串拿掉), 這樣輸出來的自然是 name .

從另外一個角度講, #是一個連接符號, 參與運算了, 自然不會輸出了.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容