宏定義在C類語言中非常重要,因為宏是一種預編譯時的功能,是以其可以比運作時更高層面的對程式流程進行控制。在初學宏定義的時候,大家可能都會有這樣一種感覺:就是完全替換麼,太簡單了。但如果你真這麼想,那你就太天真了,不說自己編寫宏,在Foundation架構中内置定義的許多宏要看明白也要費一番腦筋。本篇部落格,總結了前輩的經驗,同時收集了一些編寫非常巧妙的宏進行分析,希望可以幫助大家對宏定義有更加深刻的了解,并且可以将心得應用于實際開發中。
一、準備
宏的本質是預編譯時的替換,在開始正文之前,我們需要先介紹一種觀察宏替換後結果的方法,這樣幫助我們更友善的對宏最終的結果進行驗證與測試。Xcode開發工具自帶檢視預編譯結果的功能,首先需要對工程編譯一遍,之後選擇工具欄中的Assistant選項,打開助手視窗,如下圖所示:
之後選擇視窗的Preprocess選項,即可打開預編譯結果視窗,可以看到,宏被替換後的最終結果,如下圖所示:
後面,我們将使用這種方式來對編寫的宏進行驗證。
二、關于“宏定義”
宏使用#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); });
一個簡單的計算圓面積的宏,為了安全,我們就進行了這麼多的處理,看來要用好宏,的确不容易。