1. 程式人生 > >iOS 7:漫談#define 宏定義

iOS 7:漫談#define 宏定義

們的 知識 深入 最簡 連接符 都是 inline 疑惑 處理

#define宏定義在C系開發中可以說占有舉足輕重的作用。底層框架自不必說,為了編譯優化和方便,以及跨平臺能力,宏被大量使用,可以說底層開發離開define將寸步難行。而在更高層級進行開發時,我們會將更多的重心放在業務邏輯上,似乎對宏的使用和依賴並不多。但是使用宏定義的好處是不言自明的,在節省工作量的同時,代碼可讀性大大增加。 如果想成為一個能寫出漂亮優雅代碼的開發者,宏定義絕對是必不可少的技能(雖然宏本身可能並不漂亮優雅XD)。但是因為宏定義對於很多人來說,並不像業務邏輯那樣是每天會接觸的東西。即使是能偶爾使用到一些宏,也更多的僅僅只停留在使用的層級,卻並不會去探尋背後發生的事情。 有一些開發者確實也有探尋的動力和意願,但卻在點開一個定義之後發現還有宏定義中還有其他無數定義,再加上滿屏幕都是不同於平時的代碼,既看不懂又不變色,於是乎心生煩惱,怒而回退。本文希望通過循序漸進的方式,通過幾個例子來表述C系語言宏定義世界中的一些基本規則和技巧,從0開始,希望最後能讓大家至少能看懂和還原一些相對復雜的宏。考慮到我自己現在objc使用的比較多,這個站點的讀者應該也大多是使用objc的,所以有部分例子是選自objc,但是本文的大部分內容將是C系語言通用。 入門
如果您完全不知道宏是什麽的話,可以先來熱個身。很多人在介紹宏的時候會說,宏嘛很簡單,就是簡單的查找替換嘛。嗯,只說對了的一半。C中的宏分為兩類,對象宏(object-like macro)和函數宏(function-like macro)。對於對象宏來說確實相對簡單,但卻也不是那麽簡單的查找替換。對象宏一般用來定義一些常數,舉個例子:

  1. //This defines PI
  2. #define M_PI 3.14159265358979323846264338327950288
#define關鍵字表明即將開始定義一個宏,緊接著的M_PI是宏的名字,空格之後的數字是內容。類似這樣的#define X A的宏是比較簡單的,在編譯時編譯器會在語義分析認定是宏後,將X替換為A,這個過程稱為宏的展開。比如對於上面的M_PI
  1. #define M_PI 3.14159265358979323846264338327950288
  2. double r = 10.0;
  3. double circlePerimeter = 2 * M_PI * r;
  4. // => double circlePerimeter = 2 * 3.14159265358979323846264338327950288 * r;
  5. printf("Pi is %0.7f",M_PI);
  6. //Pi is 3.1415927
那麽讓我們開始看看另一類宏吧。函數宏顧名思義,就是行為類似函數,可以接受參數的宏。具體來說,在定義的時候,如果我們在宏名字後面跟上一對括號的話,這個宏就變成了函數宏。從最簡單的例子開始,比如下面這個函數宏
  1. //A simple function-like macro
  2. #define SELF(x) x
  3. NSString *name = @"Macro Rookie";
  4. NSLog(@"Hello %@",SELF(name));
  5. // => NSLog(@"Hello %@",name);
  6. // => Hello Macro Rookie
這個宏做的事情是,在編譯時如果遇到SELF,並且後面帶括號,並且括號中的參數個數與定義的相符,那麽就將括號中的參數換到定義的內容裏去,然後替換掉原來的內容。 具體到這段代碼中,SELF接受了一個name,然後將整個SELF(name)用name替換掉。嗯..似乎很簡單很沒用,身經百戰閱碼無數的你一定會認為這個宏是寫出來賣萌的。那麽接受多個參數的宏肯定也不在話下了,例如這樣的:

  1. #define PLUS(x,y) x + y
  2. printf("%d",PLUS(3,2));
  3. // => printf("%d",3 + 2);
  4. // => 5
相比對象宏來說,函數宏要復雜一些,但是看起來也相當簡單吧?嗯,那麽現在熱身結束,讓我們正式開啟宏的大門吧。 宏的世界,小有乾坤 因為宏展開其實是編輯器的預處理,因此它可以在更高層級上控制程序源碼本身和編譯流程。而正是這個特點,賦予了宏很強大的功能和靈活度。但是凡事都有兩面性,在獲取靈活的背後,是以需要大量時間投入以對各種邊界情況進行考慮來作為代價的。可能這麽說並不是很能讓人理解,但是大部分宏(特別是函數宏)背後都有一些自己的故事,挖掘這些故事和設計的思想會是一件很有意思的事情。另外,我一直相信在實踐中學習才是真正掌握知識的唯一途徑,雖然可能正在看這篇博文的您可能最初並不是打算親自動手寫一些宏,但是這我們不妨開始動手從實際的書寫和犯錯中進行學習和挖掘,因為只有肌肉記憶和大腦記憶協同起來,才能說達到掌握的水準。可以說,寫宏和用宏的過程,一定是在在犯錯中學習和深入思考的過程,我們接下來要做的,就是重現這一系列過程從而提高進步。 第一個題目是,讓我們一起來實現一個MIN宏吧:實現一個函數宏,給定兩個數字輸入,將其替換為較小的那個數。比如MIN(1,2)出來的值是1。嗯哼,simple enough?定義宏,寫好名字,兩個輸入,然後換成比較取值。比較取值嘛,任何一本入門級別的C程序設計上都會有講啊,於是我們可以很快寫出我們的第一個版本:

  1. //Version 1.0
  2. #define MIN(A,B) A < B ? A : B
Try一下

  1. int a = MIN(1,2);
  2. // => int a = 1 < 2 ? 1 : 2;
  3. printf("%d",a);
  4. // => 1
輸出正確,打包發布! 瀟灑走一回 但是在實際使用中,我們很快就遇到了這樣的情況

  1. int a = 2 * MIN(3, 4);
  2. printf("%d",a);
  3. // => 4
看起來似乎不可思議,但是我們將宏展開就知道發生什麽了

  1. int a = 2 * MIN(3, 4);
  2. // => int a = 2 * 3 < 4 ? 3 : 4;
  3. // => int a = 6 < 4 ? 3 : 4;
  4. // => int a = 4;
嘛,寫程序這個東西,bug出來了,原因知道了,事後大家就都是諸葛亮了。因為小於和比較符號的優先級是較低的,所以乘法先被運算了,修正非常簡單嘛,加括號就好了。

  1. //Version 2.0
  2. #define MIN(A,B) (A < B ? A : B)
這次2 * MIN(3, 4)這樣的式子就輕松愉快地拿下了。經過了這次修改,我們對自己的宏信心大增了…直到,某一天一個怒氣沖沖的同事跑來摔鍵盤,然後給出了一個這樣的例子:

  1. int a = MIN(3, 4 < 5 ? 4 : 5);
  2. printf("%d",a);
  3. // => 4
簡單的相比較三個數字並找到最小的一個而已,要怪就怪你沒有提供三個數字比大小的宏,可憐的同事只好自己實現4和5的比較。在你開始著手解決這個問題的時候,你首先想到的也許是既然都是求最小值,那寫成MIN(3, MIN(4, 5))是不是也可以。於是你就隨手這樣一改,發現結果變成了3,正是你想要的..接下來,開始懷疑之前自己是不是看錯結果了,改回原樣,一個4赫然出現在屏幕上。你終於意識到事情並不是你想像中那樣簡單,於是還是回到最原始直接的手段,展開宏。

  1. int a = MIN(3, 4 < 5 ? 4 : 5);
  2. // => int a = (3 < 4 < 5 ? 4 : 5 ? 3 : 4 < 5 ? 4 : 5); //希望你還記得運算符優先級
  3. // => int a = ((3 < (4 < 5 ? 4 : 5) ? 3 : 4) < 5 ? 4 : 5); //為了您不太糾結,我給這個式子加上了括號
  4. // => int a = ((3 < 4 ? 3 : 4) < 5 ? 4 : 5)
  5. // => int a = (3 < 5 ? 4 : 5)
  6. // => int a = 4
找到問題所在了,由於展開時連接符號和被展開式子中的運算符號優先級相同,導致了計算順序發生了變化,實質上和我們的1.0版遇到的問題是差不多的,還是考慮不周。那麽就再嚴格一點吧,3.0版!

  1. //Version 3.0
  2. #define MIN(A,B) ((A) < (B) ? (A) : (B))
至於為什麽2.0版本中的MIN(3, MIN(4, 5))沒有出問題,可以正確使用,這裏作為練習,大家可以試著自己展開一下,來看看發生了什麽。 經過兩次悲劇,你現在對這個簡單的宏充滿了疑惑。於是你跑了無數的測試用例而且它們都通過了,我們似乎徹底解決了括號問題,你也認為從此這個宏就妥妥兒的哦了。不過如果你真的這麽想,那你就圖樣圖森破了。生活總是殘酷的,該來的bug也一定是會來的。不出意外地,在一個霧霾陰沈的下午,我們又收到了一個出問題的例子。

  1. float a = 1.0f;
  2. float b = MIN(a++, 1.5f);
  3. printf("a=%f, b=%f",a,b);
  4. // => a=3.000000, b=2.000000
拿到這個出問題的例子你的第一反應可能和我一樣,這TM的誰這麽二貨還在比較的時候搞++,這簡直亂套了!但是這樣的人就是會存在,這樣的事就是會發生,你也不能說人家邏輯有錯誤。a是1,a++表示先使用a的值進行計算,然後再加1。那麽其實這個式子想要計算的是取a和b的最小值,然後a等於a加1:所以正確的輸出a為2,b為1才對!嘛,滿眼都是淚,讓我們這些久經摧殘的程序員淡定地展開這個式子,來看看這次又發生了些什麽吧:

  1. float a = 1.0f;
  2. float b = MIN(a++, 1.5f);
  3. // => float b = ((a++) < (1.5f) ? (a++) : (1.5f))
其實只要展開一步就很明白了,在比較a++和1.5f的時候,先取1和1.5比較,然後a自增1。接下來條件比較得到真以後又觸發了一次a++,此時a已經是2,於是b得到2,最後a再次自增後值為3。出錯的根源就在於我們預想的是a++只執行一次,但是由於宏展開導致了a++被多執行了,改變了預想的邏輯。解決這個問題並不是一件很簡單的事情,使用的方式也很巧妙。我們需要用到一個GNU C的賦值擴展,即使用({...})的形式。這種形式的語句可以類似很多腳本語言,在順次執行之後,會將最後一次的表達式的賦值作為返回。舉個簡單的例子,下面的代碼執行完畢後a的值為3,而且b和c只存在於大括號限定的代碼域中

  1. int a = ({
  2. int b = 1;
  3. int c = 2;
  4. b + c;
  5. });
  6. // => a is 3
有了這個擴展,我們就能做到之前很多做不到的事情了。比如徹底解決MIN宏定義的問題,而也正是GNU C中MIN的標準寫法

  1. //GNUC MIN
  2. #define MIN(A,B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; })
這裏定義了三個語句,分別以輸入的類型申明了__a和__b,並使用輸入為其賦值,接下來做一個簡單的條件比較,得到__a和__b中的較小值,並使用賦值擴展將結果作為返回。這樣的實現保證了不改變原來的邏輯,先進行一次賦值,也避免了括號優先級的問題,可以說是一個比較好的解決方案了。如果編譯環境支持GNU C的這個擴展,那麽毫無疑問我們應該采用這種方式來書寫我們的MIN宏,如果不支持這個環境擴展,那我們只有人為地規定參數不帶運算或者函數調用,以避免出錯。 關於MIN我們討論已經夠多了,但是其實還存留一個懸疑的地方。如果在同一個scope內已經有__a或者__b的定義的話(雖然一般來說不會出現這種悲劇的命名,不過誰知道呢),這個宏可能出現問題。在申明後賦值將因為定義重復而無法被初始化,導致宏的行為不可預知。如果您有興趣,不妨自己動手試試看結果會是什麽。Apple在Clang中徹底解決了這個問題,我們把Xcode打開隨便建一個新工程,在代碼中輸入MIN(1,1),然後Cmd+點擊即可找到clang中 MIN的寫法。為了方便說明,我直接把相關的部分抄錄如下:

  1. //CLANG MIN
  2. #define __NSX_PASTE__(A,B) A##B
  3. #define MIN(A,B) __NSMIN_IMPL__(A,B,__COUNTER__)
  4. #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); })
似乎有點長,看起來也很吃力。我們先美化一下這宏,首先是最後那個__NSMIN_IMPL__內容實在是太長了。我們知道代碼的話是可以插入換行而不影響含義的,宏是否也可以呢?答案是肯定的,只不過我們不能使用一個單一的回車來完成,而必須在回車前加上一個反斜杠\。改寫一下,為其加上換行好看些:

  1. #define __NSX_PASTE__(A,B) A##B
  2. #define MIN(A,B) __NSMIN_IMPL__(A,B,__COUNTER__)
  3. #define __NSMIN_IMPL__(A,B,L) ({ __typeof__(A) __NSX_PASTE__(__a,L) = (A); \
  4. __typeof__(B) __NSX_PASTE__(__b,L) = (B); \
  5. (__NSX_PASTE__(__a,L) < __NSX_PASTE__(__b,L)) ? __NSX_PASTE__(__a,L) : __NSX_PASTE__(__b,L); \
  6. })
但可以看出MIN一共由三個宏定義組合而成。第一個__NSX_PASTE__裏出現的兩個連著的井號##在宏中是一個特殊符號,它表示將兩個參數連接起來這種運算。註意函數宏必須是有意義的運算,因此你不能直接寫AB來連接兩個參數,而需要寫成例子中的A##B。宏中還有一切其他的自成一脈的運算符號,我們稍後還會介紹幾個。接下來是我們調用的兩個參數的MIN,它做的事是調用了另一個三個參數的宏__NSMIN_IMPL__,其中前兩個參數就是我們的輸入,而第三個__COUNTER__我們似乎不認識,也不知道其從何而來。其實__COUNTER__是一個預定義的宏,這個值在編譯過程中將從0開始計數,每次被調用時加1。因為唯一性,所以很多時候被用來構造獨立的變量名稱。有了上面的基礎,再來看最後的實現宏就很簡單了。整體思路和前面的實現和之前的GNUC MIN是一樣的,區別在於為變量名__a和__b添加了一個計數後綴,這樣大大避免了變量名相同而導致問題的可能性(當然如果你執拗地把變量叫做__a9527並且出問題了的話,就只能說不作死就不會死了)。 花了好多功夫,我們終於把一個簡單的MIN宏徹底搞清楚了。宏就是這樣一類東西,簡單的表面之下隱藏了很多玄機,可謂小有乾坤。作為練習大家可以自己嘗試一下實現一個SQUARE(A),給一個數字輸入,輸出它的平方的宏。雖然一般這個計算現在都是用inline來做了,但是通過和MIN類似的思路我們是可以很好地實現它的,動手試一試吧 :)

iOS 7:漫談#define 宏定義