最近新加入了項目組举塔,看到了一個讓我很費解的宏定義
#define ServiceTypeMake(_cls, _sel) (NO && ((void)[_cls _sel[Service new]], NO),\ [NSString stringWithFormat:@"%s.%s", #_cls, #_sel])
去掉一些敏感信息后的宏就是這個樣子艇炎。看到這個宏之后陵吸,根據(jù)我之前的經(jīng)驗只曉得[NSString stringWithFormat:@"%s.%s", #_cls, #_sel]這塊是字符串拼接民鼓,前半部分目測像寫了個廢話(NO &&結(jié)果明顯是NO啊!5愣睢!)莺琳。
ServiceTypeMake(IDUrlService, sendUrlRequest:)
預(yù)編譯展開后就是這樣的:
(__objc_no && ((void)[IDUrlService sendUrlRequest:[Service new]], __objc_no), [NSString stringWithFormat:@"%s.%s", "IDUrlService", "sendUrlRequest:"])
&&短路與还棱,前面判斷為假后面就不執(zhí)行了,最后相當(dāng)于就是個字符串拼接惭等,所以這個宏的寫法我覺得不是很能理解珍手。通過查閱一些資料和文章后,我對iOS中宏的定義和使用有了一些深入的了解辞做,希望大家指正琳要。
1.宏的入門
?*宏的定義
任何中文的翻譯都不夠權(quán)威,這里摘抄一下GCC對宏的定義(定義鏈接):
A macro is a fragment of code which has been given a name.Whenever the name is used, it is replaced by the contents of the macro.There are two kinds of macros. They differ mostly in what they look like when they are used.Object-like macros resemble data objects when used,function-like macros resemble function calls.
You may define any valid identifier as a macro, even if it is a C keyword.? The preprocessor does not know anything about keywords.? Thiscan be useful if you wish to hide a keyword such as const from an older compiler that does not understand it.? However, the preprocessor operator defined(see Defined) can never be defined as amacro, and C++’s named operators (see C++ Named Operators) cannot be macros when you are compiling C++.
從定義中可以知道宏分兩種對象宏(Object-like?macros)和方法宏(function-like?macros)秤茅,接下來一一介紹稚补。
對象宏(Object-like macros)
這里先舉一些??:
#define BUFFER_SIZE 1024
#define NUMBERS 1, \
??????????????????????????????? + 2, \
??????????????????????????????? + 3
#define NUMBER_TOTAL?NUMBERS
看上面這些例子可以總結(jié)出以下東西:
1.在定義宏的時候,宏的名字通常都會使用大寫字母框喳,目的是方便大家一眼就能看出這是個宏课幕,引用的GCC原文是:
By convention, macro names are written in uppercase. Programs are
easier to read when it is possible to tell at a glance which names are
macros.
2.如果一個宏的定義中需要換行的話可以用反斜杠“\”結(jié)尾來斷行
3.在調(diào)用宏時厦坛,預(yù)處理器在替換宏的內(nèi)容時會繼續(xù)檢查宏的內(nèi)容本身是否也是宏定義,如果是乍惊,會繼續(xù)替換宏定義的內(nèi)容杜秸,直到全部展開。
這里可能會有一點疑問润绎,就是對于自引用宏的處理撬碟,先舉個例子:
#define FOO (4 + FOO)
這要是在使用中要一直展開的話就會是一個無限遞歸的過程,GCC中有對這種情況進行處理凡橱,只會在使用FOO的地方替換成(4 + FOO)小作,還有一種情況是:
#define x (4 + y)
#define y (2 * x)
這里會展開為如下形式:
x → (4 + y)
? ? → (4 + (2 * x))
y? ? → (2 * x)
? ? → (2 * (4 + y))
4.宏定義以最后有效的定義為準,例如
#define FOO 4
#undef FOO
#define FOO 5
NSLog(@"%d", FOO);? ? ?→ 5
方法宏(function-like macros)
方法宏的特點是在定義宏的時候宏的名字后會接著一對括號稼钩,如:
#define MAX(a, b) ((a) > (b))
這里也有一些注意點:
1.重中之重顾稀,就是有參數(shù)的時候在表達式里盡量多加括號(防御式編程),避免一些操作符優(yōu)先級問題造成結(jié)果錯誤:
#define MULTIP(a, b) a * b
NSLog(@"%d", MULTIP(1+2, 3+4));? ? ?→11
MULTIP(1+2, 3+4)
????????? →1+2? *? 3+4
?????????????????? →11
2.如果有字符串內(nèi)容坝撑,即使與參數(shù)名稱相同也不會被替換
#define func(x) x, @"x"
NSLog(@"%@,%@", func(@"abc")); // 輸出結(jié)果為 abc,x
3.使用"#"預(yù)處理操作符來實現(xiàn)將宏中的參數(shù)轉(zhuǎn)化為字符(串)静秆,這個操作會將參數(shù)中的所有字符都實現(xiàn)字符(串)話,包括引號巡李。(有的文章里面說會把參數(shù)里面的多個空格替換為一個空格抚笔,親測不會)
#define XSTR(s) STR(s)
#define STR(x) #x
#define FOO @"? ? a? ? ? b? ? cc"
NSLog(@"%s", STR(FOO));? ? //輸出FOO
NSLog(@"%s", XSTR(FOO));? ?//輸出@"? ? a? ? ? b? ? cc"
4.使用"##"實現(xiàn)宏中token的連接
#define VIEW(prefix/*前綴*/, suffix/*后綴*/) prefix##View##suffix
VIEW(UI,) ? ? ? ? ? ? ? ? // 等價于 UIView
VIEW(UI, Controller) ? ? // 等價于 UIViewController
示例所示表示把prefix和suffix對應(yīng)的內(nèi)容與View連接起來,當(dāng)然連接后的字符(串)必須是有意義的侨拦,否則會出現(xiàn)錯誤或警告殊橙;但不能將 “##” 放在宏的最后,否則會出現(xiàn)錯誤狱从。
預(yù)處理器會將注釋轉(zhuǎn)化成空格膨蛮,因此在宏中間,參數(shù)中間加入注釋都是可以的季研。只能加入/**/這種注釋而不能加入//這種敞葛。
“##”的特殊用法:
#define NSLog(args, ...) NSLog(args, ##__VA_ARGS__);
NSLog(@"abc") ? ? ? ? ? ? ? ?// 輸出結(jié)果為 abc
NSLog(@"123, "@"abc") ? ?// 輸出結(jié)果為 123, abc
將 “##” 放在 "," 和參數(shù)之間,那么如果參數(shù)留空的話与涡,那么 “##” 前面的 "," 就會刪掉惹谐,從而防止編譯錯誤。
上例中使用標識符 __VA_ARGS__ 來表示多個參數(shù)驼卖,在宏的定義中則使用 (...) 表示氨肌。
經(jīng)過上面這些內(nèi)容,大家對宏的使用已經(jīng)有了基本的認識酌畜,但在實際使用過程中依然還有很多坑(特別是函數(shù)宏)怎囚,只有多使用和學(xué)習(xí),才能慢慢達到掌握的水準檩奠,我們要開始踩坑之旅了桩了。
1.MIN
我們很快會得出我們的版本:
#define MIN_(a, b) ((a) < (b) ? (a) : (b))
測試一下:
初級版:
NSLog(@"%d",MIN_(3,5));
//=>NSLog(@"%d", ((3) < (5) ? (3) : (5)));
//=>3
高級版:
?NSLog(@"%d",MIN_(2+6,3*4));
//=>NSLog(@"%d", ((2+6) < (3*4) ? (2+6) : (3*4)));
//=>8
測試通過附帽,發(fā)布上線!>蕉扮!
項目上線了,跑的沒問題在岂,直到有一天一個特殊的情況出現(xiàn)了:
?NSInteger a = 5;
?NSInteger b = 2;
NSInteger c = MIN_(a, b++);
NSLog(@"a = %d, b = %d, c = %d", a, b, c); //輸出a = 5 , b = 4, c = 3;
一臉懵逼奔则,這不是真的~
我們期望的結(jié)果是a = 5,b = 3蔽午, c = 2易茬,a是5,b是2及老,b++先取b的值跟a比抽莱,比a小,所以c得值是2骄恶,b自增1食铐,最后b的值是3,這才是想要的結(jié)果嘛僧鲁。為啥會出現(xiàn)上面的結(jié)果呢虐呻,不妨展開一下
NSInteger c?= MIN_(a, b++) = ((a) < (b++) ? (a) : (b++));
這里a先跟b比,5比2大寞秃,b會自增1斟叼,b為3,此時c取b得值為3蜕该,接著b會再自增1犁柜,為4洲鸠。問題就出在比較完a堂淡,b的值后b進行了自增1的操作。這里小括號已經(jīng)無法解決這個問題了扒腕,我們需要借助GNU C的賦值擴展({...})绢淀,使用這樣的方法可以計算出一個對象,而且不會浪費變量名瘾腰,可以在小范圍的作用域內(nèi)計算特殊的值
NSInteger x = ({
? ? ? ? NSInteger y = 2;
? ? ? ? NSInteger z = 3;
? ? ? ? y + z;
? ? });
? ? NSInteger y = 4; //繼續(xù)使用y皆的,z當(dāng)變量沒問題
? ? NSInteger z = 5;
有了這個擴展,我們就能解決MIN的寫法問題了蹋盆,GNU C中MIN的標準寫法是
#define?MIN(A,B)?({?__typeof__(A)?__a?=?(A);\
?__typeof__(B)?__b?=?(B);\
?__a?<?__b???__a?:?__b;?})
賦值擴展里包含3個語句费薄,前兩個定義了兩個變量__a和__b其類型為輸出參數(shù)的類型硝全,再進行取小值得運算。用這個寫法試了下完美解決問題楞抡,但依然會有坑伟众。
NSInteger __a = 5;
NSInteger __b =2;
NSInteger __c =MIN_(__a, __b++);
NSLog(@"__a =%d, __b = %d, __c = %d", __a, __b, __c); //輸出__a = 5, __b = 2, __c = 0
因為賦值函數(shù)內(nèi)變量名與外部變量名重名而造成無法被初始化,造成宏的行為不可預(yù)知召廷。Apple的工程師徹底解決了這個問題凳厢,官方的寫法是
#define __NSX_PASTE__(A,B) A##B
#if !defined(MIN)
? ? #define __NSMIN_IMPL__(A,B,L) ({ __typeof__(A) __NSX_PASTE__(__a,L) = (A); __typeof__(B) __NSX_PASTE__(__b,L) = (B); (__NSX_PASTE__(__a,L) < __NSX_PASTE__(__b,L)) ? __NSX_PASTE__(__a,L) : __NSX_PASTE__(__b,L); })
? ? #define MIN(A,B)? __NSMIN_IMPL__(A, B, __COUNTER__)
#endif
__NSX_PASTE__這個宏是用來做字符串拼接,?__NSMIN_IMPL__這個宏除了字符串拼接之外跟之前的宏寫法完全一樣竞慢,比較費解的就是__NSMIN_IMPL__(A, B, __COUNTER__)這里第三個參數(shù)__COUNTER__先紫,__COUNTER__是預(yù)定義的宏,在編譯階段開始時從0開始計數(shù)筹煮,每次被使用時加1遮精,這樣就大大避免了變量名重名的可能性。如果一定要任性的定義變量名為__a234這樣的名字败潦,后果只會是"no zuo, no die"仑鸥。
2.NSLog
通過打印來調(diào)試是我們經(jīng)常使用的一種方式,我們希望測試的時候多輸出一些信息变屁,比如所在行眼俊、方法名等,在release環(huán)境下不打印信息粟关,通常我們的寫法會是
#ifdef DEBUG
#defineNSLog(format, ...) do {? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
fprintf(stderr,"<%s : %d> %s\n",? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],? \
__LINE__, __func__);? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
(NSLog)((format), ##__VA_ARGS__);? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
fprintf(stderr,"-------\n");? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
} while (0)
#else
#define NSLog(...);
#endif
這個宏定義比較復(fù)雜了疮胖,首先判斷是否定義了DEBUG,就是說是否在DEBUG模式下闷板,我們按住command點擊一下并找不到定義的位置澎灸,那DEBUG在哪里定義的呢??在 "Target > Build Settings > Preprocessor Macros > Debug" 里有一個"DEBUG=1"遮晚。在沒打包的情況下性昭,就屬于DEBUG模式。再看宏定義县遣,先拋開do{}while(0)糜颠,看內(nèi)部。首先會打印文件路徑的最后一個路徑萧求,還有行數(shù)和調(diào)用的方法其兴。這里出現(xiàn)了__FILE__, __LINE__, __func__三個預(yù)定義宏,具體的作用大家可以參考預(yù)定義宏夸政。接著是一個標準的NSLog調(diào)用元旬,輸出參數(shù)信息,最后在打印一排“------------”并換行。這里比較費解的就是外層的這個do{}while(0)的作用了匀归,貌似沒啥用坑资,就是讓代碼執(zhí)行一次,那我們可不可以去掉do()while(0)把NSLog寫成這樣
#defineNSLog(format, ...)? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
fprintf(stderr,"<%s : %d> %s\n",? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],? \
__LINE__, __func__);? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
(NSLog)((format), ##__VA_ARGS__);? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
fprintf(stderr,"-------\n");? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
NSLog(@"aaa");
這個例子里我們可以正常輸出了穆端,但如果我們碰到這樣的呢
if(1)
? ? ? ? NSLog(@"aaa");
這里首先出現(xiàn)了一種反面的寫法盐茎,就是if條件語句判斷后不帶大括號。我們先看看程序預(yù)編譯之后的結(jié)果
if (1)
? ? ? ? fprintf(__stderrp,"<%s : %d> %s\n", [[[NSString stringWithUTF8String:"/Users/baidu/Desktop/test/TestMacro/TestMacro/ViewController.m"] lastPathComponent] UTF8String],98, __func__); (NSLog)((@"aaa")); fprintf(__stderrp,"-------\n");;
我們知道if判斷不帶大括號的寫法是只會執(zhí)行判斷條件后面的一個語句徙赢,雖然這里執(zhí)行完后好像對結(jié)果沒什么影響字柠,我們還是覺得是不是加個大括號更好一點,就有了一個高級點的版本
#defineNSLog(format, ...) {? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
fprintf(stderr,"<%s : %d> %s\n",? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],? \
__LINE__, __func__);? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
(NSLog)((format), ##__VA_ARGS__);? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
fprintf(stderr,"-------\n");? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
}
這樣看著就沒什么毛病了狡赐,還減少了do()while(0)的執(zhí)行窑业,減少了點點CPU的消耗,很開森枕屉!很明顯常柄,事情不會這么簡單,總有童鞋會有“新奇”的寫法
if(1)
? ? ? ? NSLog(@"aaa");
? ? else
? ? ? ? NSLog(@"bbb");
這么寫了一下搀擂,編譯都會報錯西潘,wtf???看下預(yù)編譯的結(jié)果
if (1)
? ? ? ? { fprintf(__stderrp,"<%s : %d> %s\n", [[[NSString stringWithUTF8String:"/Users/baidu/Desktop/test/TestMacro/TestMacro/ViewController.m"] lastPathComponent] UTF8String],98, __func__); (NSLog)((@"aaa")); fprintf(__stderrp,"-------\n"); };
? ? else
? ? ? ? { fprintf(__stderrp,"<%s : %d> %s\n", [[[NSString stringWithUTF8String:"/Users/baidu/Desktop/test/TestMacro/TestMacro/ViewController.m"] lastPathComponent] UTF8String],100, __func__); (NSLog)((@"bbb")); fprintf(__stderrp,"-------\n"); };
細心的同學(xué)應(yīng)該會發(fā)現(xiàn)if判斷的大括號后居然多了一個“;”哨颂,這個“喷市;”哪里來的呢,是NSLog(@"aaa");這里出來的威恼,就報錯了品姓。在試試加了do{}while(0)的版本呢,編譯一下看看區(qū)別
if (1)
? ? ? ? do{ fprintf(__stderrp,"<%s : %d> %s\n", [[[NSString stringWithUTF8String:"/Users/baidu/Desktop/test/TestMacro/TestMacro/ViewController.m"] lastPathComponent] UTF8String],98, __func__); (NSLog)((@"aaa")); fprintf(__stderrp,"-------\n"); }while(0);
? ? else
? ? ? ? do{ fprintf(__stderrp,"<%s : %d> %s\n", [[[NSString stringWithUTF8String:"/Users/baidu/Desktop/test/TestMacro/TestMacro/ViewController.m"] lastPathComponent] UTF8String],100, __func__); (NSLog)((@"bbb")); fprintf(__stderrp,"-------\n"); }while(0);
咦箫措,一下就不一樣了腹备,do{}while(0)完美的利用上了最后的分號。事實上斤蔓,在編譯器編譯階段植酥,遇到do{}while(0)時會做一些優(yōu)化,執(zhí)行的時間并不會變多弦牡。從這里我們總結(jié)出來兩點
(1)在宏展開有多個語句時友驮,可以包裹上do{}while(0)來吃掉“;”
(2)不要出現(xiàn)if判斷不帶{}的寫法,貌似簡化了一點喇伯,事實上往往會害人害己喊儡,出現(xiàn)一些莫名其妙的異常拨与。
在我們平時遇到的宏中還有很多更復(fù)雜稻据、更有趣的宏。首先不要謊,可以先將其展開來慢慢分析捻悯,通過一點點琢磨匆赃,會理解其精髓的。最后提一下iOS中怎么進行預(yù)編譯查看結(jié)果
3.選擇下拉中出現(xiàn)的Preprocess選項今缚,就會出現(xiàn)編譯結(jié)果算柳。
《宏的入門和理解》就到這里了,之后有時間還會在分享一些更復(fù)雜的宏姓言。??
參考文章:
1.GCC文檔:https://gcc.gnu.org/onlinedocs/cpp/Operator-Precedence-Problems.html#Operator-Precedence-Problems
2.宏菜鳥起飛手冊:https://blog.csdn.net/hopedark/article/details/20699723