麻豆小视频在线观看_中文黄色一级片_久久久成人精品_成片免费观看视频大全_午夜精品久久久久久久99热浪潮_成人一区二区三区四区

首頁 > 學院 > 開發設計 > 正文

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

2019-11-14 19:38:05
字體:
來源:轉載
供稿:網友

目錄

  文件包含
    #include
    #include_next
    #import

  宏定義
    #define
    #undef

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

  錯誤、警告處理
    #error
    #warning

  編譯器控制
    #PRagma

  其他
    #line

  結語

 

 

開篇

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

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

1

 

帶你們看計算機程序執行過程之前,我們要嚴肅的了解一點程序的編譯,也就是上圖中的,我們把老婆的命令轉換成電信號的過程。在計算機世界中有些好事者把這個玩意兒稱作編譯器(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

 

各種編譯器的區別還有幾本對比知識可以參看《LLVMGCC的區別http://www.companysz.com/zuopeng/p/4141467.html

 

關于蘋果的和gcc以及LLVM背后激情個故事看以看這個《三好學生Chris LattnerLLVM編譯工具鏈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)雖然是允許的但是這會降低編譯性能。那該怎么辦呢?1、使用#import替代include 2、使用宏判斷(宏判斷下面會詳解),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.14159265358979323846264338327950288double 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

 

 

那么讓我們開始看看另一類宏吧。函數宏顧名思義,就是行為類似函數,可以接受參數的宏。具體來說,在定義的時候,如果我們在宏名字后面跟上一對括號的話,這個宏就變成了函數宏。從最簡單的例子開始,比如下面這個函數宏

 

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

 

 

這個宏做的事情是,在編譯時如果遇到SELF,并且后面帶括號,并且括號中的參數個數與定義的相符,那么就將括號中的參數換到定義的內容里去,然后替換掉原來的內容。 具體到這段代碼中,SELF接受了一個name,然后將整個SELF(name)用name替換掉。嗯..似乎很簡單很沒用,身經百戰閱碼無數的你一定會認為這個宏是寫出來賣萌的。那么接受多個參數的宏肯定也不在話下了,例如這樣的:

 

#define PLUS(x,y) x + yprintf("%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 :Pif (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,可以參看_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)}#elsefunc 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系統做策略

 

#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0//如果iOS版本低于7.0,這里可以干一些事情#endif

 

 

又或者判斷設備類型

 

#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"http://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__為1005     printf("%d/n",__LINE__);6     printf("%d/n",__LINE__);7     };

 

 

 

輸出為:

 

3100101

 

 

 結語

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

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

  還有哦,轉載不注明!BUG一生隨。你自己看著辦

  寫代碼就是在維護世界和平,謝謝 @戴偉來

 

 


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 99视频有精品视频高清 | 亚洲综合视频在线播放 | 狠狠干伊人网 | 国产亚洲精品久久久久久久久 | 精品一区二区三区免费 | 国av在线 | 日韩做爰视频免费 | 九九热精品视频在线免费观看 | 欧美成人se01短视频在线看 | 视频一区二区国产 | 一级成人欧美一区在线观看 | 91一级毛片 | 欧美成a人片在线观看久 | 欧美色淫 | 九九热在线视频观看 | 精品亚洲夜色av98在线观看 | 久久蜜桃香蕉精品一区二区三区 | 黄色毛片视频在线观看 | 激情久久免费视频 | 一区二区三区日韩在线 | 欧美成网 | 国产男人的天堂 | 一级在线视频 | 黄色午夜剧场 | 精品三区视频 | h视频在线播放 | 久久久精品视频网站 | 操碰97 | 一区二区三区在线观看免费 | 在线观看视频毛片 | 亚洲性一区| 羞羞视频一区二区 | 免费国产网站 | 青青国产在线视频 | 毛片免费试看 | 一级成人黄色片 | 在线看免费观看av | 亚洲一级片在线观看 | 香蕉久久久精品 | 久热久操 | 中文字幕免费在线观看视频 |