宏定義分為兩種:一種是對象宏(object-like macro)另一種就是函數宏(function-like macro)
根據名字也可以理解到,對象宏就是用來定義一個量,通過這個宏可以拿到這個變量,比如我們定義一個π值: #define PI 3.1415926
在這裡如果用到π值時,就不需要再寫出一個浮點數了,而直接使用PI
就相當寫入了這個常量浮點數,其本質的意義在於把代碼中的PI
在編譯階段替換為真正的常量,一般用來定義一些常用的常量,比如屏幕的寬高、系統版本號等。但是需要注意的是,但你定義一個表達式為宏的時候,需要透過宏的表面,看到器編譯的本質,例如
#define MARGIN 10 + 20
但你用它來計算一個寬度時,如果用到了MARGIN * 2
,結果將會非你所願,你得到的會是一個50而並非60,展開表達式就可以看到
MARGIN * 2 // 展開可以得到 // 10 + 20 * 2 = 50
我們需要考慮到它的運算優先級,解決的方式很簡單,再它的外層加上一個小括號
#define MARGIN (10 + 20) // MARGIN * 2 // (10 + 20) * 2 = 60
函數宏的作用就類似於一個函數一樣,它可以傳遞參數,通過參數進行一系列的操作,比如我們常用的計算兩個數的最大值,我們可以這樣來定義
#define MAX(A,B) A > B ? A : B
這樣寫看起來是沒有問題的,進行簡單的比較MAX(1,2)
發現也是沒有什麼問題,但是當有人使用你的宏進行更加復雜的計算時就回出現新的問題,比如進行三個數值的計較時,可能會這樣寫
int a = 3;int b = 2;int c = 1; MAX(a, b > c ? b : c) // = 2
結果肯定也不是你想要的,最大值很明顯是3,但是計算的結果確實2,這其中發生了什麼導致計算出錯,我們可以展開宏來一探究竟,下面是宏的展開
MAX(a,b > c ? b : c); //a > b > c ? b : c ? a : b > c ? b : c //(a > (b > c ? b : c) ? a : b) > c ? b : c // 這是運算的優先級 // 帶入值可以看出 //( 3 > (2 > 1 ? 2 : 1 ) ? 3 : 2) > 1 ? 2 : 1 // (3 > 2 ? 3 : 2) > 1 ? 2 : 1 // 3 > 1 ? 2 : 1
想必大家都看出來了問題所在,還是由於優先級的問題,所以在此謹記,反正多寫兩個括號也不會累著,不管會不會出現問題, 寫上小括號終究是保險一些~
可是總有寫奇葩的寫法會出現,而且看開起來還很有道理的樣子~
c = MAX(a++,b); // **我直接展開給你看就得了** // c = a++ > b ? a++ : b // c = 3++ > 2 ? 3++ : 2 // c = 4 // a = 5
不管這樣寫的那個人是有多欠揍,但是畢竟看起來是沒有任何問題的,所有我們要處理這樣的情況,但是使用我們普通的小括號已經無法解決,我們需要使用賦值擴展({...})
相信有朋友已經認出來了這種用法了,我們可以使用這樣的方法來計算出一個對象,而不用浪費變量名,可以形成小范圍的作用域來計算特殊的值
int a = ({ int b = 10; int c = 20; b + c; }) // a = 30; int b; // 繼續使用b和c當變量名也沒有問題 int c;
再回到現在這個問題上,我們該如何改裝這個宏來讓其適應這個坑爹的寫法呢
#define MAX(A,B) ({__typeof(A) __a = (A);__typeof(B) __b = (B); __a > __b ? __a : __b; })
__typeof()
就是轉換為相同類型的變量值,就完美的解決了這個問題,但是還有一個不怎麼會發生的意外,通過上面也可以知道,我們生成了新的變量__a, __b
,如何有人使用了__a,__b
,就會應為變量名重復而編譯錯誤,如果有人這樣用了,你可以拿起你的鍵盤砸他一臉,原因當然不是__a
使你的宏錯誤了,而是__a
到底是什麼意思,變量名的重要性不言而喻,除非你和看代碼的人有仇,否則請使用有意義的變量名,接下來讓我們看一看官方的MAX是如何實現的
#define __NSX_PASTE__(A,B) A##B #if !defined(MAX) #define __NSMAX_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__(__b,L) : __NSX_PASTE__(__a,L); }) #define MAX(A,B) __NSMAX_IMPL__(A,B,__COUNTER__) #endif
這是Function
框架中的MAX定義,我麼來一步一步的解析它,首先看見的是
#define __NSX_PASTE__(A,B) A##B// 將A和B連接到一塊
它的作用是將A
和B
連接到一塊,用來生成一個的字符串,比如A##12
就成了A12
接下來我們看到了一個有三個參數的宏定義__NSMAX_IMPL__(A,B,__COUNTER__)
#if !defined(MAX) #define __NSMAX_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__(__b,L) : __NSX_PASTE__(__a,L); }) #define MAX(A,B) __NSMAX_IMPL__(A,B,__COUNTER__)#endif
我們先來解釋__COUNTER__
是什麼,__COUNTER__
是一個預編譯宏,它將會在每次編譯時加1,這樣的話可以保證__NSX_PASTE__(__b,__CONNTER__)
生成的變量名不易重復,但是這樣還是有那麼點危險,就是你要是起變量名叫__a20
,那就真的真的沒有辦法了~
說起可變參數,我們用的最多的一個方法NSLog(...)
就是可變參數了,可變參數意味著參數的個數是不定的,而NSLog
作為我們調試時一個重要的工具實在時太廢物了,只能打印對應的時間和參數信息,而文件名,行數,方法名等重要的信息都沒有給出,今天我們就借此來實現一個超級版NSLog
宏~~~
#define NSLog(format, ...) do { fprintf(stderr, " %s\n", \ [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__); \ (NSLog)((format), ##__VA_ARGS__); \ fprintf(stderr, "-------\n"); \ } while (0)
首先看這個宏的定義NSLog(format,...)
發現它有...
,這就是可變參數,而__VA__ARGS__
就是除了format
外剩下的所有參數,接下來我們發現使用了一個do{}while(0)
循環,說明這個循環只執行一便就回停止,感覺廢話啊,我們的目的就是只執行一遍啊,但這樣寫又是為了進行防御式編程
,如果有人這樣寫的話
if (100 > 99) NSLog(@"%@",@"Fuck");
就會出現無論如何都會執行後兩個打印,出現的問題想必大家也都知道,那我們直接使用{}
給擴起來不就行了,實際操作後確實是解決了這個問題,但是再擴展一下,當我們使用了if{} else if{}
時又會出現新的問題
if (100 > 99) NSLog(@"%@",@"Fuck"); else { } // 展開後可得 if (100 > 99) { fprintf(stderr, " %s\n", [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__); (NSLog)((format), ##__VA_ARGS__); fprintf(stderr, "-------\n");}; else { }
編譯錯誤,大家也發現了NSLog
後面會跟上;
,如果我麼直接使用了{}
後,會在編譯時在外面加上;
,導致編譯錯誤,而使用了do{} while(0)
循環後就不會出現這個問題了
if (100 > 99) do { fprintf(stderr, " %s\n", [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__); (NSLog)((format), ##__VA_ARGS__); fprintf(stderr, "-------\n");} while(0); else { }
到此位置問題解決的差不多了,看一下內部的結構,__FILE__
是編譯的文件路徑,__LINE__
是行數,__func__
是編譯的方法名,下面我們又看見了
(NSLog)((format), ##__VA_ARGS__);
##
上面已經看見過了,在這裡的作用差不多,也是連接的意思,__VA_ARGS__
是剩下的所有參數,使用##
連接起來後就時NSLog(format,__VA_ARGS__)
了,這就是NSLog
的方法了,但是不知道有沒有人發現一個細節,如果__VA_ARGS__
為空的話,那豈不是成了NSLog(format,)
這樣肯定會編譯報錯的,但是蘋果的大神們早就想到了解決的方法,如果__VA_ARGS__
為空的話,在這裡##
將會吞掉前面的,
,這樣一來就不會出問題了。然後我們就可以使用這個強大的NSLog()
了。
接下說一下多參數函數的使用
- (void)say:(NSString *)code,... { va_list args; va_start(args, code); NSLog(@"%@",code); while (YES) { NSString *string = va_arg(args, NSString *); if (!string) { break; } NSLog(@"%@",string); } va_end(args); }
我們可以要先定義一個va_list args
來定義多參數變量args
,然後通過va_start(args, code)
來開始取值,code
是第一個值,va_arg(args, NSString *)
來定義取出的值類型,取值方式有點像生成器,取完之後調用va_end(args)
來關閉。這就是整個過程,平時很少使用這樣的方法,如果你有什麼好的實用方法請評論指教~~~