iOS中的預編譯指令的初步探究

目錄

  文件包含
    #include
    #include_next
    #importhtml

  宏定義
    #define 
    #undef前端

  條件編譯
    #if #else #endif
    #if define #ifdef #ifndef #elifjava

  錯誤、警告處理
    #error
    #warningios

  編譯器控制
    #pragmac++

  其餘
    #linegit

  結語程序員

 

 

開篇

咱們人類創造東西的時候有個詞叫作」仿生學「!人類創造什麼東西都會模仿本身來創造,因此上帝沒有長成樹的樣子而和人長得同樣,科幻片裏面外星人也像人同樣有眼睛有鼻子……可是人類本身創造的東西若是太像本身,本身又會嚇尿(恐怖谷效應),人類真是奇葩;奇葩的咱們在20世紀創造了改變世界的東西——計算機(電腦),不用懷疑,這貨固然也是仿生學!這貨哪裏長得像人了??別不服,先聽我說完,先把你的磚頭放下。狹義的仿生學是外形上仿生嘛,其實廣義上仿生學還能夠原理的仿生,構造的仿生,性能的仿生阿拉巴拉……,計算機(這裏我狹義的使用我的PC來舉例)咱們常說的有輸入設備(鍵盤呀鼠標呀攝像頭呀……)、處理設備(CPU、GPU……)和輸出設備(顯示器、音響……);而後你自個兒瞅瞅你本身的眼睛耳朵(輸入),大腦(處理),四肢(輸出) 當初設計電腦必需要這種構造的人難道不是瞅着本身來設計計算機的麼?^_^github

因此上計算機組成原理的時候有什麼地方晦澀難以理解的時候,我就馬上解禁我高中的生物知識,而後就迎刃而解了~可是今天我這篇博客是要講程序的呀,這把犢子扯的那麼遠看客們也不免心有憤懣,你切勿急躁,我立刻就帶大家飛!跟着我用仿生學的角度去理解計算機,那麼計算機程序是神馬呢?教科書上怎麼說?能夠被計算機執行,那神馬東西會被人執行的呢?老婆的命令、老爸的呵斥、項目經理的需求變動……咱們都會執行,貌似這就是人的程序了,這確實就是人的程序!下面我具體拿老婆的命令來詳解一下人得程序的執行過程;好比老婆說了一句」你給我滾出去睡沙發!「,首先這句話的處理流程是這樣的:面試

圖1objective-c

 

 

帶大家看計算機程序執行過程以前,咱們要嚴肅的瞭解一點程序的編譯,也就是上圖中的,咱們把老婆的命令轉換成電信號的過程。在計算機世界中有些好事者把這個玩意兒稱做編譯器(compiler),什麼gcc呀clang呀阿拉巴拉,說的編譯器這名字逼格好高~其實說白了就是個翻譯的東西,如咱們人執行程序過程當中,把老婆的話(也是人類的話)翻譯成大腦懂的話(電波),在計算機中就是把各類編程語言(c、c++、oc……)翻譯成0101011……讓計算機懂。編譯器的工做原理基本上都是三段式的,能夠分爲前端(Frontend)、優化器(Optimizer)、後端(Backend)。前端負責解析源代碼,檢查語法錯誤,並將其翻譯爲抽象的語法樹(Abstract Syntax Tree)。優化器對這一中間代碼進行優化,試圖使代碼更高效。後端則負責將優化器優化後的中間代碼轉換爲目標機器的代碼,這一過程後端會最大化的利用目標機器的特殊指令,以提升代碼的性能。

圖2

 

 

爲何要弄成這三段式的呢?我確定不會從什麼框架、結構啊優化……角度提及,由於我也不懂呀,哈哈 不過我能夠講一個過去的故事給你們,你們試想一下編譯器是怎麼開發出來的呀,好傢伙,上網一搜LLVM編譯器是C++寫的,那c++的編譯器呢?其實不用那麼麻煩,如今把你的手借給我,讓我牽着你回到上個世紀70年代,裏奇正在爲他新發明的C語言在寫編譯器呢,他在用匯編語言!彙編語言怎麼編譯變成二進制流呢?答案是使用01011機器碼編寫的編譯器;因此編譯器和計算機語言的進步就像這樣迭代發展的,再以後是用高級語言寫更高級的編譯器,高級的編譯器能編譯更高級的計算機語言……,雖然藍翔的挖掘機技術強,但問題仍是來了,世界上計算機那麼多,各類不一樣的架構,人還好基本架構都同樣,可是計算機有Intel架構的又有ARM架構,怎麼能讓編程語言經過編譯分別產生不一樣架構的執行碼呢?因此這就是編譯器三段式這種模型的好處了,當咱們要支持多種語言時,只須要添加多個前端就能夠了。當須要支持多種目標機器時,只須要添加多個後端就能夠了。對於中間的優化器,咱們能夠使用通用的中間代碼。gcc能夠支持c、c++、java……等語言的編譯。

圖3

 

 

那麼一個HelloWord的程序的編譯和執行過程你們就按照圖1自行腦補吧

 

說了這麼多終於正片開始了~ 原來個人囉嗦,由於我就是叫作話癆戴^_^,本人從沒有開發過Mac os的應用因此本文主要示例代碼和框架都是iOS下的,可是是由於C系語言的預編譯指令,因此基本都能通用。雖然這篇文章有個宏大的開端,可是本文主要就是想探究一下編譯過程當中的預處理部分的部分預處理指令,但願本文可以作到的就是拋磚引玉,給比我菜的廣大猿友指引一條學習的方向。

 

在好久好久之前的Xcode不知道什麼版本,Build settings裏面還能夠選擇不一樣的編譯器。

如圖4

 

不一樣的編譯器,是否對於預處理指令有差別,我也沒辦法考究了。還有其實、其實人家接觸iOS也只有3個月,我開發iOS使用的第一個IDE就是XCode6,若是坑了你們,那就索瑞~~

如今Xcode6裏面默認使用了Apple LLVM(Low Level Virtual Machine) 6.0的編譯器

圖5

 

各類編譯器的區別還有幾本對比知識能夠參看《LLVM和GCC的區別http://www.cnblogs.com/zuopeng/p/4141467.html

 

關於蘋果的和gcc以及LLVM背後激情個故事看以看這個《三好學生Chris Lattner的LLVM編譯工具鏈http://www.programmer.com.cn/9436/

 

那麼接下來就是正片的高潮啦——預處理指令

 

高潮以前再加一個預高潮^_^,幹嗎要預處理呢?回去看圖一,老婆說「你給我滾出去睡沙發!」 若是你沒有預處理,你按照順序運行,先滾出去了你可能還不想睡覺,你在沙發上看電視看了幾個小時後纔打算睡覺,這時候你發現你居然忘了從房間拿枕頭和被子出來了,你這時候就去敲老婆的門,又是一頓臭罵,以後你才能睡覺……折騰不? 若是你進行了預處理,當老婆說完指令,其中你獲取到關鍵字「睡沙發」,無論我滾出去以後睡不睡覺,我都先從房間把被子枕頭拿到沙發,這樣是否是效率高了不少?一樣對於C系的語言的開發,預處理可謂舉足輕重,若是你閱讀過優秀的C源代碼,你必定看到了不少 #define #if #error ……  預編譯對程序以後的編譯提供了不少方便以及優化,對於錯誤處理、包引用、跨平臺……有着極大的幫助。並且開發中使用預編譯指令完成一些事情也是很屌的事情,而且你既然走上了一條改變世界的道路那麼當一個有逼格的程序猿的覺悟也須要覺醒呀

 

文件包含

#include

這個我真的不想多說,只要你大學C語言課程不是體育老師教得話,他們確定跟你說過#include 「」、#include <>的區別,他們確定說過#include「xxx」包含和使用#include <xxx>包含的不一樣之處就是使用<>包含時,預處理器會搜索C函數庫頭文件路徑下的文件,而使用「」包含時首先搜索程序所在目錄,其次搜索系統Path定義目錄,若是仍是找不到纔會搜索C函數庫頭文件所在目錄。

因此我不想爲了彌補你老師犯下的錯,我就不想重複了,有一點須要注意使用#include的時候包含文件的時候是不能遞歸包含的,例如a.h文件包含b.h,而b.h就不能再包含a.h了;還有就是重複包含(好比a.h包含了b.h,而後main.c中又包含了a.h和b.h)雖然是容許的可是這會下降編譯性能。那該怎麼辦呢?一、使用#import替代include 二、使用宏判斷(宏判斷下面會詳解),xcode很聰明,只要新建一個頭文件a.h 裏面就自動就生成了 圖6

 

這個看不懂?你能夠等看完#ifndef和#define以後就明白了,大概的原理就是,用宏定義判斷一個宏是否認義了,若是沒有定義則會定義這個宏,這樣以來若是已經包含過則這個宏定義確定已經定義過了,即便再包含也不會從新定義了,下面的代碼也就不會包含進去。

 

#include_next

這個是非C標準庫裏面的預處理指令,可是Xcode中容許使用,因此也就介紹一下吧。#include_next是GNU(一羣牛逼的人瘋狂開源的組織,能夠說是Linux的靈魂)的一個擴展,並非標準C中的指令 例若有個搜索路徑鏈,在#include中,它們的搜索順序依次是A,B,C,D和E。在B目錄中有個頭文件叫a.h,在D目錄中也有個頭文件叫a.h,若是在咱們的源代碼中這樣寫#include <a.h>,那麼咱們就會包含的是B目錄中的a.h頭文件,若是咱們這樣寫#include_next <a.h>那麼咱們就會包含的是D目錄中的a.h頭文件。#include_next <a.h>的意思按咱們上面的引號包含中的解釋來講就是「在B目錄中的a.h頭文件後面的目錄路徑(即C,D和E)中搜索a.h頭文件幷包含進來)。#include_next <a.h>的操做會是這樣的,它將在A,B,C,D和E目錄中依次搜索a.h頭文件,那麼首先它會在B目錄中搜索到a.h頭文件,那它就會以B目錄做爲分割點,搜索B目錄後面的目錄(C,D和E),而後在這後面的目錄中搜索a.h頭文件,並把在這以後搜索到的a.h頭文件包含進來。這樣說的話你們應該清楚了吧。

#import

OC特有的就是一個智能的#include,解決了#include的重複包含的問題。

 

宏定義

#define 

這個使用的就太多了,我的認爲是全部預處理指令中最酷的!必需要學習!這裏我厚顏無恥的轉載OneV’s Den的文章,他寫的很是的棒!省得同窗們連接跳來跳去我就直接粘貼他的文章吧,請叫我快樂的搬運工!

 

宏定義的黑魔法 - 宏菜鳥起飛手冊

宏定義在C系開發中能夠說佔有舉足輕重的做用。底層框架自沒必要說,爲了編譯優化和方便,以及跨平臺能力,宏被大量使用,能夠說底層開發離開define將步履維艱。而在更高層級進行開發時,咱們會將更多的重心放在業務邏輯上,彷佛對宏的使用和依賴並很少。可是使用宏定義的好處是不言自明的,在節省工做量的同時,代碼可讀性大大增長。若是想成爲一個能寫出漂亮優雅代碼的開發者,宏定義絕對是必不可少的技能(雖然宏自己可能並不漂亮優雅XD)。可是由於宏定義對於不少人來講,並不像業務邏輯那樣是天天會接觸的東西。即便是能偶爾使用到一些宏,也更多的僅僅只停留在使用的層級,卻並不會去探尋背後發生的事情。有一些開發者確實也有探尋的動力和意願,但卻在點開一個定義以後發現還有宏定義中還有其餘無數定義,再加上滿屏幕都是不一樣於平時的代碼,既看不懂又不變色,因而乎心生煩惱,怒而回退。本文但願經過按部就班的方式,經過幾個例子來表述C系語言宏定義世界中的一些基本規則和技巧,從0開始,但願最後能讓你們至少能看懂和還原一些相對複雜的宏。考慮到我本身如今objc使用的比較多,這個站點的讀者應該也大可能是使用objc的,因此有部分例子是選自objc,可是本文的大部份內容將是C系語言通用。

入門

若是您徹底不知道宏是什麼的話,能夠先來熱個身。不少人在介紹宏的時候會說,宏嘛很簡單,就是簡單的查找替換嘛。嗯,只說對了的一半。C中的宏分爲兩類,對象宏(object-like macro)和函數宏(function-like macro)。對於對象宏來講確實相對簡單,但卻也不是那麼簡單的查找替換。對象宏通常用來定義一些常數,舉個例子:

 

//This defines PI
#define M_PI        3.14159265358979323846264338327950288

 

 

#define關鍵字代表即將開始定義一個宏,緊接着的M_PI是宏的名字,空格以後的數字是內容。相似這樣的#define X A的宏是比較簡單的,在編譯時編譯器會在語義分析認定是宏後,將X替換爲A,這個過程稱爲宏的展開。好比對於上面的M_PI

 

#define M_PI        3.14159265358979323846264338327950288

double r = 10.0;  
double circlePerimeter = 2 * M_PI * r;  
// => double circlePerimeter = 2 * 3.14159265358979323846264338327950288 * r;

printf("Pi is %0.7f",M_PI);  
//Pi is 3.1415927

 

 

那麼讓咱們開始看看另外一類宏吧。函數宏顧名思義,就是行爲相似函數,能夠接受參數的宏。具體來講,在定義的時候,若是咱們在宏名字後面跟上一對括號的話,這個宏就變成了函數宏。從最簡單的例子開始,好比下面這個函數宏

 

   => Hello Macro Rookie//A simple function-like macro
#define SELF(x)      x
NSString *name = @"Macro Rookie";  
NSLog(@"Hello %@",SELF(name));  
// => NSLog(@"Hello %@",name);
//

 

 

這個宏作的事情是,在編譯時若是遇到SELF,而且後面帶括號,而且括號中的參數個數與定義的相符,那麼就將括號中的參數換到定義的內容裏去,而後替換掉原來的內容。 具體到這段代碼中,SELF接受了一個name,而後將整個SELF(name)用name替換掉。嗯..彷佛很簡單很沒用,身經百戰閱碼無數的你必定會認爲這個宏是寫出來賣萌的。那麼接受多個參數的宏確定也不在話下了,例如這樣的:

 

#define PLUS(x,y) x + y
printf("%d",PLUS(3,2));  
// => printf("%d",3 + 2);
//  => 5

 

 

相比對象宏來講,函數宏要複雜一些,可是看起來也至關簡單吧?嗯,那麼如今熱身結束,讓咱們正式開啓宏的大門吧。

宏的世界,小有乾坤

由於宏展開實際上是編輯器的預處理,所以它能夠在更高層級上控制程序源碼自己和編譯流程。而正是這個特色,賦予了宏很強大的功能和靈活度。可是凡事都有兩面性,在獲取靈活的背後,是以須要大量時間投入以對各類邊界狀況進行考慮來做爲代價的。可能這麼說並非很能讓人理解,可是大部分宏(特別是函數宏)背後都有一些本身的故事,挖掘這些故事和設計的思想會是一件頗有意思的事情。另外,我一直相信在實踐中學習纔是真正掌握知識的惟一途徑,雖然可能正在看這篇博文的您可能最初並非打算親自動手寫一些宏,可是這咱們不妨開始動手從實際的書寫和犯錯中進行學習和挖掘,由於只有肌肉記憶和大腦記憶協同起來,才能說達到掌握的水準。能夠說,寫宏和用宏的過程,必定是在在犯錯中學習和深刻思考的過程,咱們接下來要作的,就是重現這一系列過程從而提升進步。

第一個題目是,讓咱們一塊兒來實現一個MIN宏吧:實現一個函數宏,給定兩個數字輸入,將其替換爲較小的那個數。好比MIN(1,2)出來的值是1。嗯哼,simple enough?定義宏,寫好名字,兩個輸入,而後換成比較取值。比較取值嘛,任何一本入門級別的C程序設計上都會有講啊,因而咱們能夠很快寫出咱們的第一個版本:

 

//Version 1.0
#define MIN(A,B) A < B ? A : B

 

 

Try一下

 

int a = MIN(1,2);  
// => int a = 1 < 2 ? 1 : 2;
printf("%d",a);  
// => 1

 

 

輸出正確,打包發佈!圖7

瀟灑走一回

可是在實際使用中,咱們很快就遇到了這樣的狀況

 

int a = 2 * MIN(3, 4);  
printf("%d",a);  
// => 4

 

 

看起來彷佛難以想象,可是咱們將宏展開就知道發生什麼了

 

int a = 2 * MIN(3, 4);  
// => int a = 2 * 3 < 4 ? 3 : 4;
// => int a = 6 < 4 ? 3 : 4;
// => int a = 4;

 

 

嘛,寫程序這個東西,bug出來了,緣由知道了,過後你們就都是諸葛亮了。由於小於和比較符號的優先級是較低的,因此乘法先被運算了,修正很是簡單嘛,加括號就行了。

 

//Version 2.0
#define MIN(A,B) (A < B ? A : B)

 

 

此次2 * MIN(3, 4)這樣的式子就輕鬆愉快地拿下了。通過了此次修改,咱們對本身的宏信心大增了...直到,某一天一個怒氣衝衝的同事跑來摔鍵盤,而後給出了一個這樣的例子:

 

int a = MIN(3, 4 < 5 ? 4 : 5);  
printf("%d",a);  
// => 4

 

 

簡單的相比較三個數字並找到最小的一個而已,要怪就怪你沒有提供三個數字比大小的宏,可憐的同事只好本身實現4和5的比較。在你開始着手解決這個問題的時候,你首先想到的也許是既然都是求最小值,那寫成MIN(3, MIN(4, 5))是否是也能夠。因而你就隨手這樣一改,發現結果變成了3,正是你想要的..接下來,開始懷疑以前本身是否是看錯結果了,改回原樣,一個4赫然出如今屏幕上。你終於意識到事情並非你想像中那樣簡單,因而仍是回到最原始直接的手段,展開宏。

 

int a = MIN(3, 4 < 5 ? 4 : 5);  
// => int a = (3 < 4 < 5 ? 4 : 5 ? 3 : 4 < 5 ? 4 : 5);  //但願你還記得運算符優先級
//  => int a = ((3 < (4 < 5 ? 4 : 5) ? 3 : 4) < 5 ? 4 : 5);  //爲了您不太糾結,我給這個式子加上了括號
//   => int a = ((3 < 4 ? 3 : 4) < 5 ? 4 : 5)
//    => int a = (3 < 5 ? 4 : 5)
//     => int a = 4

 

 

找到問題所在了,因爲展開時鏈接符號和被展開式子中的運算符號優先級相同,致使了計算順序發生了變化,實質上和咱們的1.0版遇到的問題是差很少的,仍是考慮不周。那麼就再嚴格一點吧,3.0版!

 

//Version 3.0
#define MIN(A,B) ((A) < (B) ? (A) : (B))

 

 

至於爲何2.0版本中的MIN(3, MIN(4, 5))沒有出問題,能夠正確使用,這裏做爲練習,你們能夠試着本身展開一下,來看看發生了什麼。

通過兩次悲劇,你如今對這個簡單的宏充滿了疑惑。因而你跑了無數的測試用例並且它們都經過了,咱們彷佛完全解決了括號問題,你也認爲今後這個宏就妥妥兒的哦了。不過若是你真的這麼想,那你就圖樣圖森破了。生活老是殘酷的,該來的bug也必定是會來的。不出意外地,在一個霧霾陰沉的下午,咱們又收到了一個出問題的例子。

 

float a = 1.0f;  
float b = MIN(a++, 1.5f);  
printf("a=%f, b=%f",a,b);  
// => a=3.000000, b=2.000000

 

 

拿到這個出問題的例子你的第一反應可能和我同樣,這TM的誰這麼二貨還在比較的時候搞++,這簡直亂套了!可是這樣的人就是會存在,這樣的事就是會發生,你也不能說人家邏輯有錯誤。a是1,a++表示先使用a的值進行計算,而後再加1。那麼其實這個式子想要計算的是取a和b的最小值,而後a等於a加1:因此正確的輸出a爲2,b爲1纔對!嘛,滿眼都是淚,讓咱們這些久經摧殘的程序員淡定地展開這個式子,來看看此次又發生了些什麼吧:

 

float a = 1.0f;  
float b = MIN(a++, 1.5f);  
// => 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只存在於大括號限定的代碼域中

 

int a = ({  
    int b = 1;
    int c = 2;
    b + c;
});
// => a is 3

 

 

有了這個擴展,咱們就能作到以前不少作不到的事情了。好比完全解決MIN宏定義的問題,而也正是GNU C中MIN的標準寫法

 

//GNUC MIN
#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的寫法。爲了方便說明,我直接把相關的部分抄錄以下:

 

//CLANG MIN
#define __NSX_PASTE__(A,B) A##B

#define MIN(A,B) __NSMIN_IMPL__(A,B,__COUNTER__)

#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__內容實在是太長了。咱們知道代碼的話是能夠插入換行而不影響含義的,宏是否也能夠呢?答案是確定的,只不過咱們不能使用一個單一的回車來完成,而必須在回車前加上一個反斜槓\。改寫一下,爲其加上換行好看些:

 

#define __NSX_PASTE__(A,B) A##B

#define MIN(A,B) __NSMIN_IMPL__(A,B,__COUNTER__)

#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); \
                              })

 

 

 

但能夠看出MIN一共由三個宏定義組合而成。第一個__NSX_PASTE__裏出現的兩個連着的井號##在宏中是一個特殊符號,它表示將兩個參數鏈接起來這種運算。注意函數宏必須是有意義的運算,所以你不能直接寫AB來鏈接兩個參數,而須要寫成例子中的A##B。宏中還有一切其餘的自成一脈的運算符號,咱們稍後還會介紹幾個。接下來是咱們調用的兩個參數的MIN,它作的事是調用了另外一個三個參數的宏__NSMIN_IMPL__,其中前兩個參數就是咱們的輸入,而第三個__COUNTER__咱們彷佛不認識,也不知道其從何而來。其實__COUNTER__是一個預約義的宏,這個值在編譯過程當中將從0開始計數,每次被調用時加1。由於惟一性,因此不少時候被用來構造獨立的變量名稱。有了上面的基礎,再來看最後的實現宏就很簡單了。總體思路和前面的實現和以前的GNUC MIN是同樣的,區別在於爲變量名__a__b添加了一個計數後綴,這樣大大避免了變量名相同而致使問題的可能性(固然若是你固執地把變量叫作__a9527而且出問題了的話,就只能說不做死就不會死了)。

花了好多功夫,咱們終於把一個簡單的MIN宏完全搞清楚了。宏就是這樣一類東西,簡單的表面之下隱藏了不少玄機,可謂小有乾坤。做爲練習你們能夠本身嘗試一下實現一個SQUARE(A),給一個數字輸入,輸出它的平方的宏。雖然通常這個計算如今都是用inline來作了,可是經過和MIN相似的思路咱們是能夠很好地實現它的,動手試一試吧 :)

Log,永恆的主題

Log人人愛,它爲咱們指明前進方向,它爲咱們抓蟲提供幫助。在objc中,咱們最多使用的log方法就是NSLog輸出信息到控制檯了,可是NSLog的標準輸出可謂殘廢,有用信息徹底不夠,好比下面這段代碼:

 

NSArray *array = @[@"Hello", @"My", @"Macro"];  
NSLog (@"The array is %@", array);

 

 

打印到控制檯裏的結果是相似這樣的

 

2014-01-20 11:22:11.835 TestProject[23061:70b] The arr

ay is ( Hello, My, Macro )

 

 

咱們在輸出的時候關心什麼?除告終果之外,不少狀況下咱們會對這行log的所在的文件位置方法什麼的會比較關心。在每次NSLog裏都手動加上方法名字和位置信息什麼的無疑是個笨辦法,而若是一個工程裏已經有不少NSLog的調用了,一個一個手動去改的話無疑也是噩夢。咱們經過宏,能夠很簡單地完成對NSLog原生行爲的改進,優雅,高效。只須要在預編譯的pch文件中加上

 

//A better version of NSLog
#define NSLog(format, ...) do {                                                                          \
                             fprintf(stderr, "<%s : %d> %s\n",                                           \
                             [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],  \
                             __LINE__, __func__);                                                        \
                             (NSLog)((format), ##__VA_ARGS__);                                           \
                             fprintf(stderr, "-------\n");                                               \
                           } while (0)

 

 

 

嘛,這是咱們到如今爲止見到的最長的一個宏了吧...不要緊,一點一點來分析就好。首先是定義部分,第2行的NSLog(format, ...)。咱們看到的是一個函數宏,可是它的參數比較奇怪,第二個參數是...,在宏定義(其實也包括函數定義)的時候,寫爲...的參數被叫作可變參數(variadic)。可變參數的個數不作限定。在這個宏定義中,除了第一個參數format將被單獨處理外,接下來輸入的參數將做爲總體一併看待。回想一下NSLog的用法,咱們在使用NSLog時,每每是先給一個format字符串做爲第一個參數,而後根據定義的格式在後面的參數裏跟上寫要輸出的變量之類的。這裏第一個格式化字符串即對應宏裏的format,後面的變量所有映射爲...做爲總體處理。

接下來宏的內容部分。上來就是一個下馬威,咱們遇到了一個do while語句...想一想看你上次使用do while是何時吧?也許是C程序設計課的大做業?或者是某次早已被遺忘的算法面試上?總之雖然你們都是明白這個語句的,可是實際中可能用到它的機會少之又少。乍一看彷佛這個do while什麼都沒作,由於while是0,因此do確定只會被執行一次。那麼它存在的意義是什麼呢,咱們是否是能夠直接簡化一下這個宏,把它給去掉,變成這個樣子呢?

 

//A wrong version of NSLog
#define NSLog(format, ...)   fprintf(stderr, "<%s : %d> %s\n",                                           \
                             [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],  \
                             __LINE__, __func__);                                                        \
                             (NSLog)((format), ##__VA_ARGS__);                                           \
                             fprintf(stderr, "-------\n");

 

 

 

答案固然是否認的,也許簡單的測試裏你沒有遇到問題,可是在生產環境中這個宏顯然悲劇了。考慮下面的常見狀況

 

if (errorHappend)  
    NSLog(@"Oops, error happened");

 

 

 

展開之後將會變成

 

if (errorHappend)  
    fprintf((stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__);
(NSLog)((format), ##__VA_ARGS__); //I wi

ll expand this later fprintf(stderr, "-------\n");

 

 

注意..C系語言可不是靠縮進來控制代碼塊和邏輯關係的。因此說若是使用這個宏的人沒有在條件判斷後加大括號的話,你的宏就會一直調用真正的NSLog輸出東西,這顯然不是咱們想要的邏輯。固然在這裏仍是須要從新批評一下認爲if後的單條執行語句不加大括號也沒問題的同窗,這是陋習,無需理由,請改正。不管是不是一條語句,也不管是if後仍是else後,都加上大括號,是對別人和本身的一種尊重。

好了知道咱們的宏是如何失效的,也就知道了修改的方法。做爲宏的開發者,應該力求使用者在最大限度的狀況下也不會出錯,因而咱們想到直接用一對大括號把宏內容括起來,大概就萬事大吉了?像這樣:

 

//Another wrong version of NSLog
#define NSLog(format, ...)   {
                               fprintf(stderr, "<%s : %d> %s\n",                                           \
                               [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],  \
                               __LINE__, __func__);                                                        \
                               (NSLog)((format), ##__VA_ARGS__);                                           \
                               fprintf(stderr, "-------\n");                                               \
                             }

 

 

 

展開剛纔的那個式子,結果是

 

//I am sorry if you don't like { in the same like. But I am a fan of this style :P
if (errorHappend) {  
    fprintf((stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__);
    (NSLog)((format), ##__VA_ARGS__);
    fprintf(stderr, "-------\n");
};

 

 

 

編譯,執行,正確!由於用大括號標識代碼塊是不會嫌多的,因此這樣一來的話咱們的宏在不論if後面有沒有大括號的狀況下都能工做了!這麼看來,前面例子中的do while果真是多餘的?因而咱們又能夠愉快地發佈了?若是你夠細心的話,可能已經發現問題了,那就是上面最後的一個分號。雖然編譯運行測試沒什麼問題,可是始終稍微有些刺眼有木有?沒錯,由於咱們在寫NSLog自己的時候,是將其看成一條語句來處理的,後面跟了一個分號,在宏展開後,這個分號就如同噩夢通常的多出來了。什麼,你還沒看出哪兒有問題?試試看展開這個例子吧:

 

if (errorHappend)  
    NSLog(@"Oops, error happened");
else  
    //Yep, no error, I am happy~ :)

 

 

 

No! I am not haapy at all! 由於編譯錯誤了!實際上這個宏展開以

後變成了這個樣子:

 

 

if (errorHappend) {  
    fprintf((stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__);
    (NSLog)((format), ##__VA_ARGS__);
    fprintf(stderr, "-------\n");
}; else {
    //Yep, no error, I am happy~ :)
}

 

 

由於else前面多了一個分號,致使了編譯錯誤,很惱火..要是寫代碼的人乖乖寫大括號不就啥事兒沒有了麼?可是咱們仍是有巧妙的解決方法的,那就是上面的do while。把宏的代碼塊添加到do中,而後以後while(0),在行爲上沒有任何改變,可是能夠巧妙地吃掉那個悲劇的分號,使用do while的版本展

開之後是這個樣子的

 

 

if (errorHappend)  
    do {
        fprintf((stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__);
        (NSLog)((format), ##__VA_ARGS__);
        fprintf(stderr, "-------\n");
    } while (0);
else {  
    //Yep, no error, I am really happy~ :)
}

 

 

這個吃掉分號的方法被大量運用在代碼塊宏中,幾乎已經成爲了標準寫法。並且while(0)的好處在於,在編譯的時候,編譯器基本都會爲你作好優化,把這部份內容去掉,最終編譯的結果不會由於這個do while而致使運行效率上的差別。在終於弄明白了這個奇怪的do while以後,咱們終於能夠繼續深刻到這個宏裏面了。宏本體內容的第一行沒有什麼值得多說的fprintf(stderr, "<%s : %d> %s\n",,簡單的格式化輸出而已。注意咱們使用了\將這個宏分紅了好幾行來寫,實際在最後展開時會被合併到同一行內,咱們在剛纔MIN最後也用到了反斜槓,但願你還能記得。接下來一行咱們填寫這個格式輸出中

的三個token,

 

 

[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__);

 

 

這裏用到了三個預約義宏,和剛纔的__COUNTER__相似,預約義宏的行爲是由編譯器指定的。__FILE__返回當前文件的絕對路徑,__LINE__返回展開該宏時在文件中的行數,__func__是改宏所在scope的函數名稱。咱們在作Log輸出時若是帶上這這三個參數,即可以加快解讀Log,迅速定位。關於編譯器預約義的Log以及它們的一些實現機制,感興趣的同窗能夠移步到gcc文檔的PreDefine頁面和clang的Builtin Macro進行查看。在這裏咱們將格式化輸出的三個參數分別設定爲文件名的最後一個部分(由於絕對路徑太長很難看),行數,以及方法名稱。

接下來是還原原始的NSLog,(NSLog)((format), ##__VA_ARGS__);中出現了另外一個預約義的宏__VA_ARGS__(咱們彷佛已經找出規律了,先後雙下槓的通常都是預約義)。__VA_ARGS__表示的是宏定義中的...中的全部剩餘參數。咱們以前說過可變參數將被統一處理,在這裏展開的時候編譯器會將__VA_ARGS__直接替換爲輸入中從第二個參數開始的剩餘參數。另一個懸疑點是在它前面出現了兩個井號##。還記得咱們上面在MIN中的兩個井號麼,在那裏兩個井號的意思是將先後兩項合併,在這裏作的事情比較相似,將前面的格式化字符串和後面的參數列表合併,這樣咱們就獲得了一個完整的NSLog方法了。以後的幾行相信你們本身看懂也沒有問題了,最後輸出一下試試看,大概

看起來會是這樣的。

 

 

-------
<AppDelegate.m : 46> -[AppDelegate application:didFinishLaunchingWithOptions:]  
2014-01-20 16:44:25.480 TestProject[30466:70b] The array is (  
    Hello,
    My,
    Macro
)
-------

 

帶有文件,行號和方法的輸出,而且用橫槓隔開了(請原諒我沒有質感的設計,也許我應該畫一隻牛,好比這樣?),debug的時候也許會輕鬆一些吧 :)圖8

hello cowsay

這個Log有三個懸念點,首先是爲何咱們要把format單獨寫出來,而後吧其餘參數做爲可變參數傳遞呢?若是咱們不要那個format,而直接寫成NSLog(...)會不會有問題?對於咱們這裏這個例子來講的話是沒有變化的,可是咱們須要記住的是...是可變參數列表,它能夠表明一個、兩個,或者是不少個參數,但同時它也能表明零個參數。若是咱們在申明這個宏的時候沒有指定format參數,而直接使用參數列表,那麼在使用中不寫參數的NSLog()也將被匹配到這個宏中,致使編譯沒法經過。若是你手邊有Xcode,也能夠看看Cocoa中真正的NSLog方法的實現,能夠看到它也是接收一個格式參數和一個參數列表的形式,咱們在宏裏這麼定義,正是爲了其傳入正確合適的參數,從而保證使用者能夠按照原來的方式正確使用這個宏。

第二點是既然咱們的可變參數能夠接受任意個輸入,那麼在只有一個format輸入,而可變參數個數爲零的時候會發生什麼呢?不妨展開看一看,記住##的做用是拼接先後,而如今##

後的可變參數是空:

 

 

NSLog(@"Hello");  
=> do {
       fprintf((stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__);
       (NSLog)((@"Hello"), );
       fprintf(stderr, "-------\n");
   } while (0);

 

中間的一行(NSLog)(@"Hello", );彷佛是存在問題的,你必定會有疑惑,這種方式怎麼可能編譯經過呢?!原來大神們其實早已想到這個問題,而且進行了一點特殊的處理。這裏有個特殊的規則,在逗號__VA_ARGS__之間的雙井號,除了拼接先後文本以外,還有一個功能,那就是若是後方文本爲空,那麼它會將前面一個逗號吃掉。這個特性當且僅當上面說的條件成立時纔會生效,所以能夠說是特例。加上這條規則後,咱們就能夠將剛纔的式子展開爲正確的(NSLog)((@"Hello"));了。

最後一個值得討論的地方是(NSLog)((format), ##__VA_ARGS__);的括號使用。把看起來能去掉的括號去掉,寫成NSLog(format, ##__VA_ARGS__);是否能夠呢?在這裏的話應該是沒有什麼大問題的,首先format不會被調用屢次也不太存在誤用的可能性(由於最後編譯器會檢查NSLog的輸入是否正確)。另外你也不用擔憂展開之後式子裏的NSLog會再次被本身展開,雖然展開式中NSLog也知足了咱們的宏定義,可是宏的展開很是聰明,展開後會自身無限循環的狀況,就不會再次被展開了。

做爲一個您讀到了這裏的小獎勵,附送三個debug輸出rect,size和point的宏,但願您能用上(嗯..想一想曾經有多少次你須要打印這些結構體的某個數字而被折磨致死,讓它們玩兒蛋去吧!固然請

先加油看懂它們吧)

 

 

#define NSLogRect(rect) NSLog(@"%s x:%.4f, y:%.4f, w:%.4f, h:%.4f", #rect, rect.origin.x, rect.origin.y, rect.size.width, rect.size.height)
#define NSLogSize(size) NSLog(@"%s w:%.4f, h:%.4f", #size, size.width, size.height)
#define NSLogPoint(point) NSLog(@"%s x:%.4f, y:%.4f", #point, point.x, point.y)

 

兩個實際應用的例子

固然不是說上面介紹的宏實際中不能用。它們相對簡單,可是裏面坑很多,因此顯得頗有特色,很是適合做爲入門用。而實際上在平常中不少咱們經常使用的宏並無那麼多奇怪的問題,不少時候咱們按照想法去實現,再稍微注意一下上述介紹的可能存在的共通問題,一個高質量的宏就能夠誕生。若是能寫出一些有意義價值的宏,小了從對你的代碼的使用者來講,大了從整個社區整個世界和減小碳排放來講,你都作出了至關的貢獻。咱們經過幾個實際的例子來看看,宏是如何改變咱們的生活,和寫代碼的習慣的吧。

先來看看這兩個宏

 

#define XCTAssertTrue(expression, format...) \
    _XCTPrimitiveAssertTrue(expression, ## format)

#define _XCTPrimitiveAssertTrue(expression, format...) \
({ \
    @try { \
        BOOL _evaluatedExpression = !!(expression); \
        if (!_evaluatedExpression) { \
            _XCTRegisterFailure(_XCTFailureDescription(_XCTAssertion_True, 0, @#expression),format); \
        } \
    } \
    @catch (id exception) { \
        _XCTRegisterFailure(_XCTFailureDescription(_XCTAssertion_True, 1, @#expression, [exception reason]),format); \
    }\
})

 

 

 

若是您常年作蘋果開發,卻沒有見過或者徹底不知道XCTAssertTrue是什麼的話,強烈建議補習一下測試驅動開發的相關知識,我想應該會對您以後的道路頗有幫助。若是你已經很熟悉這個命令了,那咱們一塊兒開始來看看幕後發生了什麼。

有了上面的基礎,相信您大致上應該能夠自行解讀這個宏了。({...})的語法和##都很熟悉了,這裏有三個值得注意的地方,在這個宏的一開始,咱們後面的的參數是format...,這其實也是可變參數的一種寫法,和...__VA_ARGS__配對相似,{NAME}...將於{NAME}配對使用。也就是說,在這裏宏內容的format指代的其實就是定義的先對expression取了兩次反?我不是科班出身,可是我還能依稀記得這在大學程序課上講過,兩次取反的操做能夠確保結果是BOOL值,這在objc中仍是比較重要的(關於objc中BOOL的討論已經有不少,若是您還沒能分清BOOL, bool和Boolean,能夠參看NSHisper的這篇文章)。而後就是@#expression這個式子。咱們接觸過雙井號##,而這裏咱們看到的操做符是單井號#,注意井號前面的@是objc的編譯符號,不屬於宏操做的對象。單個井號的做用是字符串化,簡單來講就是將替換後在兩頭加上"",轉爲一個C字符串。這裏使用@而後緊跟#expression,出來後就是一個內容是expression的內容的NSString。而後這個NSString再做爲參數傳遞給_XCTRegisterFailure_XCTFailureDescription等,繼續進行展開,這些是後話。簡單一瞥,咱們大概就能夠想象宏幫助咱們省了多少事兒了,若是各位看官要是寫個斷言還要來個十多行的話,想象都會瘋掉的吧。

另一個例子,找了人民羣衆喜聞樂見的ReactiveCocoa(RAC)中的一個宏定義。對於RAC不熟悉或者沒聽過的朋友,能夠簡單地看看Limboy的一系列相關博文(搜索ReactiveCocoa),介紹的很棒。若是以爲「哇哦這個好酷我很想學」的話,不妨能夠跟隨raywenderlich上這個系列的教程作一些實踐,裏面簡單地用到了RAC,可是都已經包含了RAC的基本用法了。RAC中有幾個很重要的宏,它們是保證RAC簡潔好用的基本,能夠說要是沒有這幾個宏的話,是不會有人喜歡RAC的。其中RACObserve就是其中一個,它經過KVC來爲對象的某個屬性建立一個信號返回(若是你看不懂這句話,不要擔憂,這對你理解這個宏的寫法和展開沒有任何影響)。對於這個宏,我決定再也不像上面那樣展開和講解,我會在最後把相關的宏都貼出來,你們不妨拿它練練手,看看能不能將其展開到代碼的狀態,而且明白其中都發生了些什麼。若是你遇到什麼問題或者在展開過程當中有所心得,歡迎在評論裏留言分享和交流 :)

好了,這篇文章已經夠長了。但願在看過之後您在看到宏的時候再也不發怵,而是能夠很開心地說這個我會這個我會這個我也會。最終目標固然是寫出漂亮高效簡潔的宏,這不論對於提升生產力仍是~震懾你的同事~提高本身實力都會頗有幫助。

另外,在這裏必定要宣傳一下關注了好久的@hangcom 吳航前輩的新書《iOS應用逆向工程》。很榮幸可以在發佈以前獲得前輩的容許拜讀了整本書,能夠說看的暢快淋漓。我以前並無越獄開發的任何基礎,也對相關領域知之甚少,在這樣的前提下跟隨書中的教程和例子進行探索的過程能夠說是十分有趣。我也得以可以用不一樣的眼光和高度來審視這幾年所從事的iOS開發行業,獲益良多。能夠說《iOS應用逆向工程》是我近期所愉快閱讀到的很cool的一本好書。如今這本書還在預售中,可是距離1月28日的正式發售已經很近,有興趣的同窗能夠前往亞馬遜或者ChinaPub的相關頁面預約,相信這本書將會是iOS技術人員很是棒的春節讀物。

最後是咱們說好的留給你們玩的練習,我加了一點註釋幫助你們稍微理解每一個宏是作什麼的,在文章後面留了一塊試驗田,你們能夠隨便填寫玩弄。總之,加油!

 

//調用 RACSignal是類的名字
RACSignal *signal = RACObserve(self, currentLocation);

//如下開始是宏定義
//rac_valuesForKeyPath:observer:是方法名
#define RACObserve(TARGET, KEYPATH) \
    [(id)(TARGET) rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self]

#define keypath(...) \
    metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__))(keypath1(__VA_ARGS__))(keypath2(__VA_ARGS__))

//這個宏在取得keypath的同時在編譯期間判斷keypath是否存在,避免誤寫
//您能夠先不用介意這裏面的巫術..
#define keypath1(PATH) \
    (((void)(NO && ((void)PATH, NO)), strchr(# PATH, '.') + 1))

#define keypath2(OBJ, PATH) \
    (((void)(NO && ((void)OBJ.PATH, NO)), # PATH))

//A和B是否相等,若相等則展開爲後面的第一項,不然展開爲後面的第二項
//eg. metamacro_if_eq(0, 0)(true)(false) => true
//    metamacro_if_eq(0, 1)(true)(false) => false
#define metamacro_if_eq(A, B) \
        metamacro_concat(metamacro_if_eq, A)(B)

#define metamacro_if_eq1(VALUE) metamacro_if_eq0(metamacro_dec(VALUE))

#define metamacro_if_eq0(VALUE) \
    metamacro_concat(metamacro_if_eq0_, VALUE)

#define metamacro_if_eq0_1(...) metamacro_expand_

#define metamacro_expand_(...) __VA_ARGS__

#define metamacro_argcount(...) \
        metamacro_at(20, __VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)

#define metamacro_at(N, ...) \
        metamacro_concat(metamacro_at, N)(__VA_ARGS__)

#define metamacro_concat(A, B) \
        metamacro_concat_(A, B)

#define metamacro_concat_(A, B) A ## B

#define metamacro_at2(_0, _1, ...) metamacro_head(__VA_ARGS__)

#define metamacro_at20(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, ...) metamacro_head(__VA_ARGS__)

#define metamacro_head(...) \
        metamacro_head_(__VA_ARGS__, 0)

#define metamacro_head_(FIRST, ...) FIRST

#define metamacro_dec(VAL) \
        metamacro_at(VAL, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
//調用 RACSignal是類的名字 RACSignal *signal = RACObserve(self, currentLocation);

 

 

源地址是http://onevcat.com/2014/01/black-magic-in-macro/

OneV’s Den大大還有不少很棒的文章喲~~~

附上那頭小牛

 

#define NSLog(format, ...)   fprintf(stderr, "<%s : %d> %s\n",                                           \

[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],  \

__LINE__, __func__);                                                        \

(NSLog)((format), ##__VA_ARGS__);                                           \

fprintf(stderr, "\n ------------------\n/ Hello David Day! \\\n\\ my Macro Log ~   /\n ------------------\n            \\\n             \\   ^__^\n                 (OO)\__________\n                 (__)\\          )\\/\\\n                     ||_______ _)\n                     ||       W |\n       YYy           ww        ww\n");

 

 

圖9

 

#undef

當你使用了#define宏定義後,則在整個程序的運行週期內這個宏都是有效的,但有時候咱們在某個邏輯裏但願這個宏失效不想使用,則會使用

 

#define NetworkOn //定義一個宏,若是該宏定義了,則在應用裏使用網絡

-(void)closeNetwork{//忽然發生意外的狀況,網絡沒法使用了,調用該方法,取消NetworkOn的宏定義
#undef NetworkOn
}

 

 

 

 

 

條件編譯

#if #else #endif

#if就和咱們經常使用的條件語句的if使用方式同樣,#if的後面跟上條件表達式,後面跟上一個#endif表示結束#if,雖然說這玩意兒簡單,可是用的好,對於某些取巧的工做特別容易實現。好比你如今有這樣的需求,個人程序平時調試模式的時候須要打印一些log,可是發佈模式的應用就不用再打印log了,怎麼辦?不少人就說發佈的時候吧log語句一句一句的刪除唄~ 那客戶發爛咋說你寫的東西是狗屎讓你修改,因此你又要回來調試,當你調試的時候你菊花確定一緊,之前的調試語句由於過於自信在發佈的時候全都刪除了,又想不到發佈後又被要求修改~,有基友就說了,那就不刪除log語句唄,反正是打印到控制檯的信息,用戶又看不到~,果真沒有安全意識,企業開發不是學雷鋒,不用把你的全部log都寫在日記本,有時候你的軟件被破解的緣由就是由於你的調試信息出賣了你。安全意識不可無,否則老王替你生孩子~~~~~。

怎麼作呢?

 

//swift語言
#if DEBUG 
func dlog<T>(object: T) { 
    println(object)
}
#else
func dlog<T>(object: T) {}
#endif

 

 

DEBUG是xcode的預約義的宏,這個東西多的很呢,要慢慢挖掘呢。 之後打印log你都只使用dlog()這個函數,若是你是在調試模式的時候就會打印,不然就不會打印了。

其餘例子:

判斷是否開啓ARC,有些庫須要ARC支持,則在編譯以前能夠判斷用戶有沒有開啓ARC

 

#if !__has_feature(objc_arc)
//若是沒有開啓ARC這裏能夠作一些錯誤處理 好比:
#error "啊 啊 啊~ 倫家須要ARC"
#endif

 

 

一樣__has_feature(objc_arc)這玩意兒也是xcode預置的 , 前綴是這個的"__"都是預約宏;

又好比,對不一樣版本的os系統作策略

 

些事情
#endif#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0
//若是iOS版本低於7.0,這裏能夠幹一

 

 

又或者判斷設備類型

 

#define IS_IPAD (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
#if IS_IPAD
//這臺設備是IPAD呀~~~~
#else
//這貨是IPhone
#endif

 

 

這個東西簡單可是很常使用,正所謂IF在手,天下我有 哈哈哈

 #if define  #ifdef  #ifndef  #elif

  #if define = #ifdef

  #if !define = #ifndef

  #elif = "else if"

錯誤、警告處理

#error

若是編譯器遇到這貨,立刻就會罷工。再說Xcode的錯誤糾正功能這麼強大,因此幾乎不可能在編譯過程當中遇到#error了,因此說這貨沒用?非也~,咱們是受太高等教育的高材生,咱們要懂得辯證觀點還要了解價值定理!任何事物都有存在的價值的。雖然說今天的IDE很好很強大,#error彷佛沒什麼用了~可是還有有一羣猿類孤高冷傲,隱居山林,他們鄙視一切IDE,他們堅信Notepad就是他們的屠龍寶刀……

對於這些虛幻飄渺的程序猿們,他們仍是須要#error來給他們預報編譯前的錯誤的。咱們說點有價值的,若是非要用#error,那在咱們當下的開發中怎麼用?

如今#error仍是有用的,尤爲是你在開發一個庫的時候,這個庫的使用須要必定的條件,若是不知足這個條件,你就不讓使用者編譯。這樣不就能夠使用#error啦嘛

 

#if !__has_feature(objc_arc)
#error "個人低調不是你裝逼的資本!這個庫須要開啓ARC,否則你別用!"
#endif

 

 

那麼若是用戶沒有開啓ARC就沒法進行編譯了,由於xcode看到#error就不編譯了,在這裏只有開啓了ARC,#error纔會不見。

#warning

這個用法很簡單,只要後面跟上你想警告的話就OK了,這樣你就可讓編譯器提醒這個警告。圖10

若是你在Xcode中設置了,圖11

若是你設置成Yes,那麼你的waring就等於error,編譯不了的哦。

 

請再次叫我快樂的小搬運工~ 又是他 ---->Onev's Den寫的東西,我就是喜歡他,怎麼樣怎麼樣?

 

談談Objective-C的警告

一個有節操的程序員會在意本身的代碼的警告,就像在意飯碗邊上有隻死蟑螂那樣。 ——@onevcat

重視編譯警告

如今編譯器有時候會很吵,而編譯器給出的警告對開發者來講是頗有用的信息。警告不會阻止繼續編譯和連接,也不會致使程序不能運行,可是不少時候編譯器會先你一步發現問題所在,對於Objective-C來講特別如此。Clang不只對於明顯的錯誤可以提出警告(好比某方法或者接口未實現),也能對不少潛在可能的問題作出提示(好比方法已經廢棄或者有問題的轉換),而這些問題在不少時候均可能成爲潛在的致命錯誤,必須加以重視。

像Ruby或者PHP這樣的動態語言沒有所謂的編譯警告,而C#或者Java這類語言的警告不少都是不得不照顧的廢棄方法什麼的,不少開發者已經習慣於忽略警告進行開發。OC因爲如今由蘋果負責維護,Clang的LLVM也同時是蘋果在作,能夠說從語言到編譯器到SDK全局都在掌握之中,所以作OC開發時的警告每每比其餘語言的警告更有參考價值。打開儘量多的警告提示,而且在程序開發中儘可能避免生成警告,對於構建一個健壯高效的程序來講,是必須的。

在Xcode中開啓額外警告提示

Xcode的工程模板已經爲咱們設置開啓了一些默認和經常使用的警告提示,這些默認設置爲了兼容一些上年頭的項目,並無打開不少,僅是指對最危險和最多見的部分進行了警告。這對於一個新項目來講這是不夠用的(至少對我來講是不夠用的),在無數前輩大牛的教導下,首先要作的事情就是打開儘量多的警告提示。

最簡單的方法是經過UI來打開警告。在Xcode中,Build Setting選項裏爲咱們預留了一些打開警告的開關,找到並直接勾選相應的選項就能夠打開警告。大部分時間裏選項自己已經足夠能描述警告的做用和產生警告的時機,若是不是很明白的話,在右側的Quick Help面板裏有更詳細的說明。對於OC開發來講特有的警告都在Apple LLVM compiler 4.2 - Warnings - Objective C一欄中,無論您是否是決定打開它們,都是值得花時間看一看加以瞭解的,由於它們都是寫OC程序時最應該避免的狀況。另外幾個Apple LLVM compiler 4.2 - Warnings - …(All languages和C++)也包含了大量的選項,以方便控制警告產生。

Xcode設置中的警告選項

固然在UI裏一個一個點擊激活警告雖然簡單,但每次都這樣來一回是一種一點也不有趣的作法,特別是在你已經瞭解它們的內容並決定打開它們的時候。在編譯選項中加入合適的flag可以打開或者關閉警告:在Build Setting中的Other C Flags裏添加形似-W...的編譯標識。你能夠在其中填寫任意多的-W...以開關某些警告,好比,填寫爲-Wall -Wno-unused-variable便可打開「所有」警告(其實並非所有,只是一大部分嚴重警告而已),可是不啓用「未使用變量」的警告。使用-W...的形式,而不是在UI上勾選的一大好處是,在編譯器版本更新時,新加入的警告若是包含在-Wall中的話,不須要對工程作任何修改,新的警告便可以生效。這樣當即能夠察覺到同一個工程因爲編譯器版本更新時可能帶來的隱患。另一個更重要的緣由是..Xcode的UI並無提供全部的警告 =_=||..

剛纔提到的,須要注意的是,-Wall的名字雖然是all,可是這真的只是一個迷惑人的詞語,實際上-Wall涵蓋的僅只是全部警告中的一個子集。在StackExchange上有一個在Google工做的Clang開發者進行的回答,其中解釋了有一些重要的警告組:

    • -Wall 並非全部警告。這一個警告組開啓的是編譯器開發者對於「你所寫的代碼中有問題」這一命題有着很高的自信的那些警告。要是在這一組設定下你的代碼出現了警告,那基本上就是你的代碼真的存在嚴重問題了。可是同時,並非說打開Wall就萬事大吉了,由於Wall所針對的僅僅只是經典代碼庫中的爲數很少的問題,所以有一些致命的警告並不能被其捕捉到。可是不論如何,由於Wall的警告提供的都是可信度和優先級很高的警告,因此爲全部項目(至少是全部新項目)打開這組警告,應該成爲一種良好的習慣。
    • -Wextra 如其所名,-Wextra組提供「額外的」警告。這個組和-Wall組幾乎同樣有用,可是有些狀況下對於代碼相對過於嚴苛。一個很常見的例子是,-Wextra中包含了-Wsign-compare,這個警告標識會開啓比較時候對signed和unsigned的類型檢查,當比較符兩邊一邊是signed一邊是unsigned時,產生警告。其實不少代碼並無特別在乎這樣的比較,並且絕大多數時候,比較signed和unsigned也是沒有太大問題的(固然不排除會有致命錯誤出現的狀況)。須要注意,-Wextra-Wall是相互獨立的兩個警告組,雖然裏面打開的警告標識有個別是重複的,可是兩組並無包含的關係。想要同時使用的話必須在Other C Flags中都加上
    • -Weverything 這個是真正的全部警告。可是通常開發者不會選擇使用這個標識,由於它包含了那些還正在開發中的可能尚存bug的警告提示。這個標識通常是編譯器開發者用來調試時使用的,若是你想在本身的項目裏開啓的話,警告必定會爆棚致使你想開始撞牆..

-Wall和-Wextra下0警告的工程,在-Weverything下的表現,能夠用慘不忍睹來形容

關於某個組開啓了哪些警告的說明,在GCC的手冊中有一個參考。雖然蘋果如今用的都是LLVM了,可是這部份內容應該是繼承了GCC的設定。

控制警告,局部加入或關閉

Clang提供了咱們本身加入警告或者暫時關閉警告的辦法。

強制加入一個警告:

 

//Generate a warning
#pragma message "Warning 1"

//Another way to generate a warning
#warning "Warning 2"

 

 

兩種強制警告的方法在視覺效果上結果是同樣的,可是警告類型略有不一樣,一個是-W#pragma-messages,另外一個是-W#warnings。對於第二種寫法,把warning換成error,能夠強制使編譯失敗。好比在發佈一些須要API Key之類的類庫時,能夠使用這個方法來提示別的開發者別忘了輸入必要的信息。

 

//Generate an error to fail the build.
#error "Something wrong"

 

 

 

對於關閉某個警告,若是須要全局關閉的話,直接在Other C Flags裏寫-Wno-...就好了,好比-Wextra -Wno-sign-compare就是一個常見的組合。若是相對某幾個文件開啓或禁用警告,在Build Phases的Compile Source相應的文件中加入對應的編譯標識便可。若是隻是想在某幾行關閉某個警告的話,能夠經過臨時改變診斷編譯標記來抑制指定類型的警告,具體以下:

 

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-variable"

int a;

#pragma clang diagnostic pop

 

 

 

若是a以後沒有被使用,也不會出未使用變量的警告了。對於想要抑制的警告類型的標識名,能夠在build產生該警告後的build log中看到。Xcode中的話,快捷鍵Cmd+7而後點擊最近的build log中,進入詳細信息中就能看到了。

警告的詳細信息,能夠找到標識符

我應該開啓哪些警告提示

我的喜愛(代碼潔癖)不一樣,會有不一樣的需求。個人建議是對於全部項目,特別是新開的項目,首先開啓-Wall-Wextra,而後在此基礎上構建項目而且避免一切警告。若是在開發過程當中遇到了某些確實沒法解決或者確信本身的作法是正確的話(其實這種狀況,你的作法通常即便不是錯誤的,也會是不那麼正確的),能夠有選擇性地關閉某些警告。通常來講,關閉的警告項目不該該超過一隻手能數出來的數字,不然必定哪兒出問題了..

是否要讓警告等於錯誤

一種很常見的作法和代碼潔癖是將警告標識爲錯誤,從而中斷編譯過程。這讓開發者不得不去修復這些警告,從而保持代碼乾淨整潔。在Xcode中,能夠經過勾選相應的Treat Warnings as Errors來開啓,或者加入-Werror標識。我我的來講不喜歡使用這個設定,由於它老是打斷開發流程。不少時候並不可能把代碼全寫完再編譯調試,相反地,我更喜歡寫一點就編譯運行一下看看結果,這樣在中間debug編譯的時候會出現警告也不足爲奇。另外,若是作TDD開發時,也可能會有大量正常的警告出現,若是有警告就不讓編譯的話,開發效率可能會打折扣。一個比較好的作法是隻在Release Build時將警告視爲錯誤,由於Xcode中是能夠爲Debug和Release分別指定標識的,因此這很容易作到。

另外也能夠只把某些警告看成錯誤,-Werror=...便可,一樣地,也能夠在-Werror被激活時使用-Wno-error=...來使某些警告不成爲錯誤。結合使用這些編譯標識能夠達到很好的控制。

原文地址:http://onevcat.com/2013/05/talk-about-warning/

 

編譯器控制

#pragma

你們都說在全部的預處理指令中,#Pragma 指令多是最複雜的了,它的做用是設定編譯器的狀態或者是指示編譯器完成一些特定的動做。#pragma指令對每一個編譯器給出了一個方法,在保持與C和C++語言徹底兼容的狀況下,給出主機或操做系統專有的特徵。依據定義,編譯指示是機器或操做系統專有的,且對於每一個編譯器都是不一樣的。

其格式通常爲: #pragma Para。其中Para 爲參數

咱們就說說iOS下,經常使用的

#pragma mark

若是一個文件代碼量很大,有時候找某段邏輯不太好找,你就能夠使用#pragma mark!

好比這樣:圖12

圖13

在方法導航哪裏就會出現你的mark了 是否是很方便呀

若是使用了 "#pragma mark -" 如這樣:

 

 

#pragma mark -
#pragma mark 這裏是applicationWillTerminate方法呀~
- (void)applicationWillTerminate:(UIApplication *)application {
    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}

 

就會這樣,圖14

自動分隔開了!!!

 

#pragma message("")

能夠輸出調試信息

控制編譯器行爲不過多解釋了

#pragma clang diagnostic push

#pragma clang diagnostic ignored "clang的參數"

#pragma clang diagnostic pop

自行Clang使用手冊: http://clang.llvm.org/get_started.html

#pragma很是複雜須要你對編譯器底層很是的瞭解,只有當你開發一些比較底層的framework的時候纔可能比較多用的,我是初學者,我不用我怕誰?

 

其餘

#line

在說這個東西的時候咱們先來看一個預約義的宏,__LINE__,咱們在《宏定義的黑魔法 - 宏菜鳥起飛手冊》自定義NSLog中見過吧

C語言中的__LINE__用以指示本行語句在源文件中的位置信息。而#line就是能夠改變當前行的行號在編譯器中的表示,而且以後的行號也會相應的改變,好比

 

1 #include <stdio.h>
2 main(){
3     printf("%d\n",__LINE__);
4 #line 100  //指定下一行的__LINE__爲100
5     printf("%d\n",__LINE__);
6     printf("%d\n",__LINE__);
7     };

 

 

 

輸出爲:

 

3
100
101

 

 

 結語

  這篇文章完了~ 這篇文章既是我學習的筆記也是我思考的感悟和一些技術資料的集合,我很用心的寫,白天上班寫代碼,晚上要準備本科的畢業設計,週末陪女友,因此我只有在拉屎蹲坑的時候一點一點寫出來的,其中必定錯漏百出,因此但願看到文章的朋友盡情的噴!磚頭不要省!反正我都寫代碼了我還怕誰?

  可是最終嘛,我仍是但願能幫到剛剛開始學習的朋友們,畢竟你丫的寫的代碼太差,也是在污染環境呀!!不是開玩笑!不僅僅污染環境,你還破壞世界和平,若是你的代碼效率不好,你想一想若是你的代碼運行在電腦上或者手機上那麼是否是很費電?費電了是否是要燒不少煤炭來發電?大氣中的有害氣體是否是愈來愈多了?溫室效應,臭氧層破壞,土地沙漠化,北京沙塵暴,拿錢治理,錢,貪污,腐敗,革命,美國參戰,朝鮮怒點核武……都怪你!!知道了吧。

  還有哦,轉載不註明!BUG一輩子隨。你本身看着辦

相關文章
相關標籤/搜索