語法錯誤
當使用參數調用宏時,避免會將參數替換為宏主體,個誤并與其他輸入文件一起檢查結果,宏削以進行更多的鐵泥宏調用,可以將部分來自宏主體和部分自變量的宏調用組合在一起。例如,避免
#define?twice(x)?(2*(x))
#define?call_with_1(x)?x(1)
call_with_1?(twice)
//x=1
?→?twice(1)
?→?(2*(1))
宏定義不必帶有括號,個誤通過在宏主體中編寫不平衡的宏削開放括號,可以創建一個從宏主體內部開始但在宏主體外部結束的鐵泥宏調用。例如,避免
#define?strange(file)?fprintf?(file,?"%s?%d",
…
strange(stderr)?p,?35)
?????→?fprintf?(stderr,?"%s?%d",?p,?35)
組合宏調用的個誤功能可能會很有用,但是宏削在宏主體中使用不平衡的開放括號只會造成混淆,應該避免。
運算符優先級問題
在大多數宏定義示例中,每次出現的宏參數名稱都帶有括號,并且另一對括號通常會包圍整個宏定義,這是編寫宏最好的方式。舉個例子
#define?ceil_div(x,?y)?(x?+?y?-?1)?/?y
假定其用法如下:
a?=?ceil_div(b&c,sizeof(int));
拓展開是
a?=(b&c?+?sizeof(int)-1)/?sizeof(int);
這沒有達到我們的預期,C的運算符優先級規則使其等效于此,而我們想要的是:
a?=(((b&c)+?sizeof(int)-1))/?sizeof(int);
如果我們將宏定義為
#define?ceil_div(x,y)((x)+(y)-1)/(y)
可能導致另一種情況,sizeof ceil_div(1,2)
是一個C表達式,可以計算ceil_div(1,2)
類型的大小,它擴展為:
sizeof((1)+(2)-1)/(2)
這將采用整數的大小并將其除以2,而除法包含在內部的sizeof之外。所以整個宏定義的括號可防止此類問題。那么,下面是定義ceil_div的正確方法如下
#define?ceil_div(x,y)((((x)+(y)-1)/(y))
吞噬分號
通常需要定義一個擴展為復合語句的宏。例如,考慮以下宏,該宏跨空格字符前進一個指針(參數p表示在何處查找):
#define?SKIP_SPACES(p,?limit)??\
{ ?char?*lim?=?(limit);?????????\
??while?(p?????if?(*p++?!=?'?')?{ ?????????\
??????p--;?break;?}}}
該宏定義必須是單個邏輯行,嚴格來說,該調用擴展為復合語句,這是一個完整的語句,不需要用分號結束。
但是,由于它看起來像函數調用,因此,如果可以像使用函數調用一樣使用它,則可以最大程度地減少混亂,然后再寫一個分號,就像在SKIP_SPACES(p,lim)
中一樣。
這可能會在else語句之前出問題,因為分號實際上是空語句。假設你寫
if?(*p?!=?0)
??SKIP_SPACES?(p,?lim);
else?…
在if條件和else條件之間存在兩個語句(復合語句和null語句)使C代碼無效。
怎么解決?我們可以使用do…while語句更改宏SKIP_SPACES的定義以解決此問題。方法如下:
#define?SKIP_SPACES(p,?limit)?????\
do?{ ?char?*lim?=?(limit);?????????\
?????while?(p????????if?(*p++?!=?'?')?{ ?????????\
?????????p--;?break;?}}}??????????\
while?(0)
SKIP_SPACES (p, lim);
擴展為
do?{ …}?while?(0);
這是一個陳述,循環僅執行一次,而且大多數編譯器不會為此生成任何額外的代碼。
重復調用
我們常見的“最小”定義一個宏min,如下所示:
#define?min(X,?Y)??((X)?(Y)???(X)?:?(Y))
當將此宏與包含副作用的參數一起使用時,如此處所示,
next?=?min(x?+?y,foo(z));
它擴展如下:
next?=?((x?+?y)?(foo?(z))???(x?+?y)?:?(foo?(z)));
其中x + y替換了X,而foo(z)替換了Y。
函數foo出現在程序中的語句中僅使用一次,但是表達式foo(z)已兩次替換到宏擴展中。結果,執行該語句時可能會兩次調用foo,所以min是一個不安全的宏。
解決此問題的最佳方法是以僅計算一次foo(z)值的方式定義min。C語言沒有提供執行此操作的標準方法,但是可以使用GNU擴展來完成此操作,如下所示:
#define?min(X,?Y)????????????????\
({ ?typeof?(X)?x_?=?(X);??????????\
???typeof?(Y)?y_?=?(Y);??????????\
???(x_?
“({ { …})”符號產生一個復合表達式,它的值是其最后一條語句的值。
如果不使用GNU C擴展,唯一的解決方案是在使用宏min時要小心。例如計算foo(z)的值時,將其保存在變量中,然后在min中使用該變量:
//假設foo返回int類型
#define?min(X,?Y)??((X)?(Y)???(X)?:?(Y))
…
{
??int?tem?=?foo?(z);
??next?=?min?(x?+?y,?tem);
}
自引用宏
自引用宏是其名稱出現在其定義中的宏。我們知道所有宏定義都將被重新掃描以查找更多要替換的宏,如果自引用被認為是宏的使用,它將產生無限大的擴展。
為防止這種情況,自引用不被視為宏調用。它原樣傳遞到預處理器輸出中。舉個例子
#define?foo?(4?+?foo)
按照普通規則,其宏定義分析如下
對foo的每個引用都將擴展為
(4 + foo)
;然后將對其進行重新掃描,并將其擴展為
(4 +(4 + foo))
;以此類推,直到計算機內存耗盡。
自引用規則將這一過程縮短了一步,即(4 + foo)
,因此此宏定義可能會導致程序在引用foo的任何地方將foo的值加4。
閱讀程序的人看到foo是變量,就難以記得它也是宏,真的會坑爹的。它的一種常見有用用法是創建一個可擴展為其自身的宏。如果你寫
#define?EPERM?EPERM
然后宏EPERM擴展為EPERM。實際上,每當在運行文本中使用預處理器時,預處理器都會將其單獨保留。
如果宏x擴展為使用宏y,而y的擴展引用了宏x,則這是x的間接自引用。在這種情況下,x也不展開,舉個例子
#define?x?(4?+?y)
#define?y?(2?*?x)
然后x和y擴展如下:
x→(4?+?y)
?????→(4?+(2?*?x))
y→(2?*?x)
?????→(2?*(4?+?y))
當每個宏出現在另一個宏的定義中時,它們將被展開,但是當它間接出現在其自己的定義中時,則不會被展開。
參數預掃描處理
宏參數在被替換為宏主體之前必須經過完全宏擴展,替換后,將再次掃描整個宏主體,包括替換的參數,以查找要擴展的宏。
如果參數包含任何宏調用,則它們將在第一次掃描時擴展,那么結果不包含任何宏調用,因此第二次掃描不會更改它。
如果按照給定的方式替換了參數,并且沒有進行預掃描,則剩余的單個掃描將找到相同的宏調用并產生相同的結果。
預掃描處理在以下三種特殊情況下有大的作用。
對宏的嵌套調用
當宏的參數包含對該宏的調用時,就會發生對宏的嵌套調用,舉個例子。
如果f是期望一個參數的宏,則f(f(1))是對f的嵌套調用對。通過擴展f(1)并將其代入f的定義來進行所需的擴展。預掃描會導致發生預期的結果。
如果沒有預掃描,f(1)本身將被替換為參數,并且f的內部使用將在主掃描期間作為間接自引用出現,并且不會擴展。
調用其他可進行字符串化或連接的宏的宏
如果參數是字符串化或串聯的,則不會進行預掃描。
如果要擴展宏,然后對其擴展進行字符串化或串聯,則可以通過使一個宏調用進行該字符串化或串聯的另一宏來實現。舉個例子
#define?AFTERX(x)?X_?##?x
#define?XAFTERX(x)?AFTERX(x)
#define?TABLESIZE?1024
#define?BUFSIZE?TABLESIZE
然后AFTERX(BUFSIZE)
擴展為X_BUFSIZE,而XAFTERX(BUFSIZE)
擴展為X_1024
而不是X_TABLESIZE
,預掃描始終會進行完整的擴展。
參數中使用的宏,其擴展名包含未屏蔽的逗號。
這可能導致使用錯誤數量的參數調用在第二次掃描時擴展的宏。舉個例子
#define?foo??a,b
#define?bar(x)?lose(x)
#define?lose(x)?(1?+?(x))
我們預期的結果是bar(foo)
變成(1 +(foo))
,然后變成(1 +(a,b))
。
然而bar(foo)
擴展為loss(a,b)
會出錯,因為Los需要一個參數。在這種情況下,該問題可以通過使用相同的括號輕松解決,該括號應用于防止算術運算的錯誤嵌套:
#define?foo?(a,b)
or
#define?bar(x)?lose((x))
多余的一對括號可防止foo定義中的逗號被解釋為參數分隔符。
參數中的換行符
類似函數的宏的調用可以擴展到許多邏輯行,但是在本實施方式中,整個擴展是一行完成的。
因此,由編譯器或調試器發出的行號是指調用在其上開始的行,這可能與包含導致問題的參數的行不同,例如:
#define?ignore_second_arg(a,b,c)?a;?c
ignore_second_arg?(foo?(),
???????????????????ignored?(),
???????????????????syntax?error);
由Syntax error on tokens
觸發的語法錯誤會導致錯誤消息引用第三行(ignore_second_arg行),即使有問題的代碼來自第五行。
參考資料:
http://gcc.gnu.org/onlinedocs/cpp/Macros.html
來源 | 技術讓夢想更偉大
作者 | 李肖遙
|?整理文章為傳播相關技術,版權歸原作者所有?|
|?如有侵權,請聯系刪除?|
【1】嵌入式研發10多年,工程師悟出這些道理
【2】當談起嵌入式工程師,究竟在談些什么
【3】嵌入式工程師出路之我見:就業,技術,行業...
【4】為什么嵌入式工程師會對8位MCU有誤解?
【5】嵌入式工程師結合經歷聊硬件工程師和軟件工程師哪個更有前途?
免責聲明:本文內容由21ic獲得授權后發布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯系我們,謝謝!