天天看點

玩轉iOS“宏定義”(一)

 宏定義在C類語言中非常重要,因為宏是一種預編譯時的功能,是以其可以比運作時更高層面的對程式流程進行控制。在初學宏定義的時候,大家可能都會有這樣一種感覺:就是完全替換麼,太簡單了。但如果你真這麼想,那你就太天真了,不說自己編寫宏,在Foundation架構中内置定義的許多宏要看明白也要費一番腦筋。本篇部落格,總結了前輩的經驗,同時收集了一些編寫非常巧妙的宏進行分析,希望可以幫助大家對宏定義有更加深刻的了解,并且可以将心得應用于實際開發中。

一、準備

     宏的本質是預編譯時的替換,在開始正文之前,我們需要先介紹一種觀察宏替換後結果的方法,這樣幫助我們更友善的對宏最終的結果進行驗證與測試。Xcode開發工具自帶檢視預編譯結果的功能,首先需要對工程編譯一遍,之後選擇工具欄中的Assistant選項,打開助手視窗,如下圖所示:

玩轉iOS“宏定義”(一)

之後選擇視窗的Preprocess選項,即可打開預編譯結果視窗,可以看到,宏被替換後的最終結果,如下圖所示:

玩轉iOS“宏定義”(一)

後面,我們将使用這種方式來對編寫的宏進行驗證。

二、關于“宏定義”

     宏使用#define來進行定義,宏定義分為兩種,一種是對象式宏,一種是函數式宏。對象式宏通常對來定義量值,在預編譯時,直接将宏名替換成對應的量值,函數式宏在定義時可以設定參數,其作用與函數很類似。

例如,我們可以将π的值定義成一個對象式宏,在使用的時候,用有意義的宏名要比直接使用π的字面值友善很多,例如:

#import <Foundation/Foundation.h>

#define PI 3.1415926

int main(int argc, const char * argv[]) {

   @autoreleasepool {

       // insert code here...

       CGFloat res = PI * 3;

       NSLog(@"%f", res);

   }

   return 0;

}

函數式宏要更加靈活一些,例如對圓面積計算的方法,我們就可以将其定義成一個宏:

#define CircleArea(r) PI * r * r

       CGFloat res = CircleArea(1);

現在,有了這個面積計算宏我們可以更加友善的計算圓的面積了,看上去很完美,後面我們就使用這個函數式宏為例,來深入了解宏的原理。

三、從一個簡單的函數式宏說起

    再來看下上面我們編寫的計算面積的宏,正常情況下好像沒什麼問題,但是需要注意,歸根結底宏并不是函數,如果完全把其作為函數使用,我們就可能會陷入一系列的陷阱中,比如這樣使用:

       CGFloat res = CircleArea(1 + 1);

運作代碼,運算的結果并不是半徑為2個圓的面積,哪裡出了問題呢,我們還是先看下宏預編譯後的結果:

CGFloat res = 3.1415926 * 1 + 1 * 1 + 1;

一目了然了,由于運算符的優先級問題導緻了運算順序錯誤,在程式設計中,所有運算符優先級産生的問題都可以使用一種方式解決:用小括号。對CircleArea宏進行一下改造,如下:

#define CircleArea(r) (PI * (r) * (r))

對執行順序進行了強制的控制,代碼執行又恢複了正常,看上去好像是沒有問題了,現在就滿意了還為時過早,例如下面這樣使用這個宏:

#define CircleArea(r) PI * (r) * (r)

       int r = 1;

       CGFloat res = CircleArea(r++);

       NSLog(@"%f, %d", res, r);

運作,發現結果又錯了,不僅計算結果與我們的預期不符,變量自加的的結果也不對了,我們檢查其展開的結果:

CGFloat res = 3.1415926 * (r++) * (r++);

原來問題出在這裡,宏在展開的時候,将參數替換了兩次,由于參數本身是一個自加表達式,是以被自加了兩次,産生了問題,那麼這個問題怎麼解決呢,C語言中有一種很有用的文法,即使用大括号定義代碼塊,代碼塊會将最後一條語句的執行結果傳回,修改上面宏定義如下:

#define CircleArea(r)   \

({                      \

   typeof(r) _r = r;   \

   (PI * (_r) * (_r)); \

})

這次程式又恢複的了正常。但是,如果如果在調用宏是變量的名字與宏内的臨時變量産生了重名,災難就又發生了,例如:

       int _r = 1;

       CGFloat res = CircleArea(_r);

       NSLog(@"%f, %d", res, _r);

運作上面代碼,會發現宏内的臨時變量沒有被初始化成功。這确實難受,我們在進一步,比如對臨時變量的名字做一些手腳,将其命名為極其不容易重複的名字,其實系統内置的一個宏就是專門用來構造唯一性變量名的:__COUNTER__,這個宏是一個計數器,在編譯的時候會自動進行累加,再次對我們編寫的宏進行改造,如下:

#define PAST(A, B) A##B

#define CircleArea(r)   __CircleArea(r, __COUNTER__)

#define __CircleArea(r, v)      \

({                              \

   typeof(r) PAST(_r, v) = r;         \

   (PI * PAST(_r, v) * PAST(_r, v));     \

       CGFloat res2 = CircleArea(_r);

       NSLog(@"%f, %f", res, res2);

這裡改造後,我們的宏就沒有那麼容易了解了,首先__COUNTER__在每次宏替換時都會進行自增,##是一種宏中專用的特殊符号,用來将參數拼接到一起,但是需要注意,使用##符号拼接的如果是另外一個宏,則其會阻止宏的展開,是以我們定義了一個轉換宏PAST(A, B)來處理拼接。如果你一下子不能了解為什麼這樣就可以解決宏展開的問題,你隻需要記住這樣一條宏展開的原則:如果形參有使用#或##這種處理符号,則不會進行宏參數的展開,否則先展開宏參數,在展開目前宏。上面代碼最終預編譯的結果如下:

       CGFloat res = ({ typeof(_r) _r0 = _r; (3.1415926 * _r0 * _r0); });

       CGFloat res2 = ({ typeof(_r) _r1 = _r; (3.1415926 * _r1 * _r1); });

一個簡單的計算圓面積的宏,為了安全,我們就進行了這麼多的處理,看來要用好宏,的确不容易。

繼續閱讀