C語言的宏能夠用來作宏定義、條件編譯和文件包含,本文主要總結宏定義#define
的用法。html
如下例子經過Xcode12.0測試,gnu99標準。git
#
和##
在一個宏參數前面使用#
號,則此參數會變爲字符串:github
#define LOG(X) printf(#X)
LOG(abc); // printf("abc");
複製代碼
##
是鏈接符號,可在宏參數先後使用:markdown
#define DefineValue(NAME, TYPE, VAL) TYPE NAME##_##TYPE = VAL;
DefineValue(aaa, int, 91) // int aaa_int = 91;
DefineValue(aaa, float, 3.26) // float aaa_float = 3.26;
printf("%d--%f\n", aaa_int, aaa_float); // 91--3.260000
複製代碼
__VA_ARGS__
和...
#define PrintStderr(format, ...) fprintf(stderr, format, ##__VA_ARGS__)
int aa = 1;
int bb = 2;
PrintStderr("%d--%d\n", aa, bb); // fprintf(stderr, "%d--%d\n", aa, bb)
PrintStderr("Huimao Chen\n"); // fprintf(stderr, "Huimao Chen\n");
複製代碼
簡單來講,...
表示全部剩下的參數,__VA_ARGS__
被宏定義中的...
參數所替換。框架
須要注意的是,上面例子用##
鏈接逗號和後面的__VA_ARGS__
,這在c語言的GNU擴展語法裏是一個特殊規則:當__VA_ARGS__
爲空時,會消除前面這個逗號。若是上面例子的宏定義去掉##
號,第一個例子無影響,但第二個例子則會替換成fprintf(stderr, "Huimao Chen", );
多出的一個逗號致使編譯失敗。jsp
宏定義裏能夠有多行語句,do{}while(0)
就能保證了這個宏成爲獨立的語法單元。
若是有這樣一個宏定義 #define SWAP(A, B) int tmp = A; A = B; B = tmp;
,雖然以下代碼能正常執行:函數
int aa = 1;
int bb = 2;
SWAP(aa, bb);
printf("%d--%d\n", aa, bb); // 2--1
複製代碼
可是,下面的代碼就有問題了,編譯報錯:oop
int aa = 1;
int bb = 2;
if (aa < bb)
SWAP(aa, bb);
printf("%d--%d\n", aa, bb);
複製代碼
此時把這個宏改爲以下這種形式就能正常運行:
#define SWAP(A, B) do {int tmp = A; A = B; B = tmp;} while(0)
測試
宏定義只是簡單的替換,經過替換可能會致使運算優先級不符合預期,此時須要用括號保護參數。ui
#define MAX(A, B) A > B ? A : B
int aa = 2;
int bb = 3;
printf("%d\n", 2 * MAX(aa, bb));
// printf("%d\n", 2 * aa > bb ? aa : bb);
// printf("%d\n", 2 * 2 > 3 ? 2 : 3);
// printf("%d\n", 4 > 3 ? 2 : 3);
// 2
複製代碼
上面的例子最後輸出的是2,與預期的結果6不符,此時把宏定義改成以下形式就能解決問題:
#define MAX(A, B) ((A) > (B) ? (A) : (B))
int aa = 2;
int bb = 3;
printf("%d\n", 2 * MAX(aa, bb));
// printf("%d\n", 2 * ((aa) > (bb) ? (aa) : (bb)));
// printf("%d\n", 2 * ((2) > (3) ? (2) : (3)));
// printf("%d\n", 2 * (3));
// 6
複製代碼
({})
包裹語句GNU擴展語法。有時候,宏的參數能夠是個複合結構,而參數可能有屢次取值。若是傳入的宏參數是一個函數,則這個函數會有屢次調用:
#define MAX(A, B) ((A) > (B) ? (A) : (B))
int aa = 5;
int bb = MAX(2, foo(aa)); // 函數foo被調用了兩次
複製代碼
爲了防止此類反作用,能夠改寫爲以下形式:
#define MAX(A, B) ({ __typeof__(A) __a = (A); \ __typeof__(B) __b = (B); \ __a > __b ? __a : __b; })
複製代碼
({})
在順序執行語句以後,返回最後一條表達式的值,這也是其區別於do{}while(0)
的地方。
在使用了#
或##
的宏中,若是宏的參數是另外一個宏,則會阻止另外一個宏展開。爲了保證參數優先展開,須要多嵌套一層宏定義。具體能夠看以下例子:
#define Stringify(A) _Stringify(A)
#define _Stringify(A) #A
#define Concat(A, B) _Concat(A, B)
#define _Concat(A, B) A##B
printf("%s\n", Stringify(Concat(Hel, lo))); // 輸出:Hello
// printf("%s\n", Stringify(Hello));
// printf("%s\n", _Stringify(Hello));
// printf("%s\n", "Hello");
// Hello
printf("%s\n", _Stringify(Concat(Hel, lo))); // 輸出:Concat(Hel, lo)
// printf("%s\n", "Concat(Hel, lo)");
// Concat(Hel, lo)
複製代碼
雖然宏定義只是簡單替換,但也有使人眼前一亮的小技巧,如模式匹配、參數檢測、遞歸宏等等。這裏只介紹遞歸宏,只要看懂了這篇文章的遞歸宏,遇到其餘宏理解起來也是小意思。如下例子參考了個人開源框架HMLog,帶上了前綴HM
。
在介紹遞歸宏以前,先來介紹一個獲取宏參數個數的技巧。
這是一個常見的宏,其構建思惟普遍使用於各類宏功能。下面的宏適用於1到10個參數,最後一個例子給出瞭解釋:
#define HMMacroArgCount(...) _HMMacroArgCount(__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)
#define _HMMacroArgCount(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, COUNT, ...) COUNT
HMMacroArgCount(a); // 1;
HMMacroArgCount(a, a); // 2;
HMMacroArgCount(a, b, c, d); // 4;
// MacroArgCount(a, b, c, d); >>> _MacroArgCount(a, b, c, d, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1);
// _MacroArgCount定義裏是固定取第11個參數,這裏命名爲COUNT,而上面第11個參數就是4,故最終展開結果爲4;
複製代碼
舉個實際應用的例子:
double average(int num, ...) {
va_list valist;
double sum = 0.0;
va_start(valist, num);
for (int i = 0; i < num; ++i) {
sum += va_arg(valist, int);
}
va_end(valist);
return sum / num;
}
#define HMAverage(...) average(HMMacroArgCount(__VA_ARGS__), __VA_ARGS__)
double result = average(5, 10, 20, 30, 40, 50);
printf("%f\n", result); // 30.000000
// 能夠少輸入一個總數5,預編譯期間就替換爲double result2 = average(5, 10, 20, 30, 40, 50);
double result2 = HMAverage(10, 20, 30, 40, 50);
printf("%f\n", result2); // 30.000000
複製代碼
average
是個可變參數的函數,計算輸入整數的平均值。直接調用可變參數函數每每須要傳入參數的長度。使用宏HMAverage
,則省略了這個長度參數,在函數調用頻繁的狀況下大大下降了出錯機率,並且是在預編譯期間完成替換,並不影響實際運行速度。
此時HMMacroArgCount
並不支持0個參數的狀況,其實根據前面總結的規律稍做修改就能夠支持0個參數,留給讀者思考。
接下來正式介紹遞歸宏,這裏給出兩種方法。
我須要一個HMPrint
宏,輸入任意個整數(這個例子是5個之內),就能省略格式化參數,按照指定格式打印出來。
#define HMMacroArgCount(...) _HMMacroArgCount(__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)
#define _HMMacroArgCount(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, COUNT, ...) COUNT
#define HMStringify(A) _HMStringify(A)
#define _HMStringify(A) #A
#define HMConcat(A, B) _HMConcat(A, B)
#define _HMConcat(A, B) A##B
#define HMPrint(...) printf(HMStringify(_HMFormat(__VA_ARGS__)), __VA_ARGS__)
#define _HMFormat(...) HMConcat(_HMFormat, HMMacroArgCount(__VA_ARGS__))(__VA_ARGS__)
#define _HMFormat1(_0) _0->%d\n
#define _HMFormat2(_0, _1) _HMFormat1(_0)_1->%d\n
#define _HMFormat3(_0, _1, _2) _HMFormat2(_0, _1)_2->%d\n
#define _HMFormat4(_0, _1, _2, _3) _HMFormat3(_0, _1, _2)_3->%d\n
#define _HMFormat5(_0, _1, _2, _3, _4) _HMFormat4(_0, _1, _2, _3)_4->%d\n
int a = 1991, b = 3, c = 26;
HMPrint(a, b, c); // 預編譯時被替換爲 printf("a->%d\nb->%d\nc->%d\n", a, b, c);
//a->1991
//b->3
//c->26
複製代碼
根據定義,HMPrint
展開後就是printf
函數,後面的參數部分保持不變。前面格式化宏_HMFormat
用鏈接符##
把_HMFormat
和HMMacroArgCount(__VA_ARGS__)
鏈接起來,後者返回參數的個數,若是HMPrint
傳入3個參數,鏈接後變爲_HMFormat3
並傳入原始參數。把_HMFormat3
前兩個參數傳遞給_HMFormat2
,第3個參數替換爲c->%d\n
,繼續就是_HMFormat2
展開,依次類推,直到格式化部分爲HMStringify(a->%d\nb->%d\nc->%d\n)
,最終變爲"a->%d\nb->%d\nc->%d\n"
。
爲了幫助理解,我這裏給出展開的過程,只需讓依次讓參數優先展開,就能獲得想要的結果:
// 依次替換展開宏,參數優先展開
HMPrint(a, b, c);
printf(HMStringify(_HMFormat(a, b, c)), a, b, c);
printf(HMStringify(HMConcat(_HMFormat, HMMacroArgCount(a, b, c))(a, b, c)), a, b, c);
printf(HMStringify(HMConcat(_HMFormat, 3)(a, b, c)), a, b, c);
printf(HMStringify(_HMFormat3(a, b, c)), a, b, c);
printf(HMStringify(_HMFormat2(a, b)c->%d\n), a, b, c);
printf(HMStringify(_HMFormat1(a)b->%d\nc->%d\n), a, b, c);
printf(HMStringify(a->%d\nb->%d\nc->%d\n), a, b, c);
printf(_HMStringify(a->%d\nb->%d\nc->%d\n), a, b, c);
printf("a->%d\nb->%d\nc->%d\n", a, b, c);
複製代碼
_HMFormat
也能夠寫成這種方式,更容易理解:
#define _HMFormat1(_0) _0->%d\n
#define _HMFormat2(_0, _1) _0->%d\n_1->%d\n
#define _HMFormat3(_0, _1, _2) _0->%d\n_1->%d\n_2->%d\n
#define _HMFormat4(_0, _1, _2, _3) _0->%d\n_1->%d\n_2->%d\n_3->%d\n
#define _HMFormat5(_0, _1, _2, _3, _4) _0->%d\n_1->%d\n_2->%d\n_3->%d\n_4->%d\n
// 再次給出這種方式下展開的過程,能夠看到_HMFormat3一次到位替換爲須要的格式
HMPrint(a, b, c);
printf(HMStringify(_HMFormat(a, b, c)), a, b, c);
printf(HMStringify(HMConcat(_HMFormat, HMMacroArgCount(a, b, c))(a, b, c)), a, b, c);
printf(HMStringify(HMConcat(_HMFormat, 3)(a, b, c)), a, b, c);
printf(HMStringify(_HMFormat3(a, b, c)), a, b, c);
printf(HMStringify(a->%d\nb->%d\nc->%d\n), a, b, c);
printf(_HMStringify(a->%d\nb->%d\nc->%d\n), a, b, c);
printf("a->%d\nb->%d\nc->%d\n", a, b, c);
複製代碼
不建議用後面這種方式,一是遞歸寫法更加簡潔統一;二是結合HMMacroArgCount
這個宏一塊兒能夠擴展成支持10個參數的HMPrint
,此時只須要測試最多參數的例子,沒有出錯就幾乎保證了全部這類宏都沒寫錯。再次強調一點,宏的遞歸展開只發生在預編譯期間,這種遞歸併不影響運行時效率。
這種方法較難理解,仍是用HMPrint
的例子:
#define HMStringify(A) _HMStringify(A)
#define _HMStringify(A) #A
#define HMConcat(A, B) _HMConcat(A, B)
#define _HMConcat(A, B) A##B
#define HMMacroArgCheck(...) _HMMacroArgCheck(__VA_ARGS__, N, N, N, N, N, N, N, N, N, 1)
#define _HMMacroArgCheck(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, TARGET, ...) TARGET
#define HMPrint(...) printf(HMStringify(HMExpand(HMForeach(_HMFormat, __VA_ARGS__))), __VA_ARGS__)
#define _HMFormat(A) A->%d\n
#define HMForeach(MACRO, ...) HMConcat(_HMForeach, HMMacroArgCheck(__VA_ARGS__)) (MACRO, __VA_ARGS__)
#define _HMForeach() HMForeach
#define _HMForeach1(MACRO, A) MACRO(A)
#define _HMForeachN(MACRO, A, ...) MACRO(A)HMDefer(_HMForeach)() (MACRO, __VA_ARGS__)
#define HMEmpty()
#define HMDefer(ID) ID HMEmpty()
#define HMExpand(...) _HMExpand1(_HMExpand1(_HMExpand1(__VA_ARGS__)))
#define _HMExpand1(...) _HMExpand2(_HMExpand2(_HMExpand2(__VA_ARGS__)))
#define _HMExpand2(...) _HMExpand3(_HMExpand3(_HMExpand3(__VA_ARGS__)))
#define _HMExpand3(...) __VA_ARGS__
int a = 1991, b = 3, c = 26;
HMPrint(a, b, c); // 預編譯時被替換爲 printf("a->%d\nb->%d\nc->%d\n", a, b, c);
//a->1991
//b->3
//c->26
int a1 = 11, a2 = 22, a3 = 33, a4 = 44, a5 = 55, a6 = 66, a7 = 77, a8 = 88, a9 = 99, a10 = 100;
HMPrint(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10);
//a1->11
//a2->22
//a3->33
//a4->44
//a5->55
//a6->66
//a7->77
//a8->88
//a9->99
//a10->100
複製代碼
HMMacroArgCheck
用於檢測參數數量,若是是1個參數則返回1,當參數大於1個,且小於等於10個的狀況下返回N。HMDefer
用於延遲展開,而HMExpand
是爲了屢次掃描宏,理解這種技巧須要知道宏展開的通常規則,能夠閱讀這個系列的文章,本文再也不贅述。HMForeach(MACRO, ...)
這個宏的用處是每一個參數都會被傳遞給MACRO
宏,爲HMForeach(MACRO, ...)
舉個簡化的例子用於理解用處:
#define Increase(X) X += 1; // 定義一個宏,準備用來處理每個參數。注意最後有分號
int aa = 10, bb = 20, cc = 30;
// 使用HMForeach須要有HMExpand包裹起來,以便屢次掃描順利展開
HMExpand(HMForeach(Increase, aa, bb, cc))
// 至關於:Increase(aa)Increase(bb)Increase(cc)
// 最後變爲:aa += 1;bb += 1;cc += 1;
printf("%d--%d--%d", aa, bb, cc); // 輸出:11--21--31
複製代碼
這種方法不須要去寫_HMFormat1
、_HMFormat2
、_HMFormat3
等這一類類似結構的宏,支持參數個數取決於HMMacroArgCheck
,因此增長支持的參數數量變得垂手可得,當參數比較多的狀況使用這種方式更加方便。不足之處是隻能對每一個參數作相同的處理,而第1種方式是能夠對每一個參數作不一樣處理的。
最後,我一樣給出展開的過程,但這並不是實際展開過程,好比忽略了HMExpand
展開的時機,僅在最後直接消除:
// 這並不是實際展開過程,好比忽略了HMExpand展開的時機,僅在最後直接消除
HMPrint(a, b, c);
printf(HMStringify(HMExpand(HMForeach(_HMFormat, a, b, c))), a, b, c);
printf(HMStringify(HMExpand(HMConcat(_HMForeach, HMMacroArgCheck(a, b, c)) (_HMFormat, a, b, c))), a, b, c);
printf(HMStringify(HMExpand(HMConcat(_HMForeach, N) (_HMFormat, a, b, c))), a, b, c);
printf(HMStringify(HMExpand(_HMForeachN (_HMFormat, a, b, c))), a, b, c);
printf(HMStringify(HMExpand(_HMFormat(a)HMDefer(_HMForeach)() (_HMFormat, b, c))), a, b, c);
printf(HMStringify(HMExpand(a->%d\n_HMForeach() (_HMFormat, b, c))), a, b, c);
printf(HMStringify(HMExpand(a->%d\nHMForeach (_HMFormat, b, c))), a, b, c);
printf(HMStringify(HMExpand(a->%d\nHMConcat(_HMForeach, HMMacroArgCheck(b, c)) (_HMFormat, b, c))), a, b, c);
printf(HMStringify(HMExpand(a->%d\nHMConcat(_HMForeach, N) (_HMFormat, b, c))), a, b, c);
printf(HMStringify(HMExpand(a->%d\n_HMForeachN (_HMFormat, b, c))), a, b, c);
printf(HMStringify(HMExpand(a->%d\n_HMFormat(b)HMDefer(_HMForeach)() (_HMFormat, c))), a, b, c);
printf(HMStringify(HMExpand(a->%d\nb->%d\n_HMForeach() (_HMFormat, c))), a, b, c);
printf(HMStringify(HMExpand(a->%d\nb->%d\nHMForeach (_HMFormat, c))), a, b, c);
printf(HMStringify(HMExpand(a->%d\nb->%d\nHMConcat(_HMForeach, HMMacroArgCheck(c)) (_HMFormat, c))), a, b, c);
printf(HMStringify(HMExpand(a->%d\nb->%d\nHMConcat(_HMForeach, 1) (_HMFormat, c))), a, b, c);
printf(HMStringify(HMExpand(a->%d\nb->%d\n_HMForeach1 (_HMFormat, c))), a, b, c);
printf(HMStringify(HMExpand(a->%d\nb->%d\n_HMFormat(c))), a, b, c);
printf(HMStringify(HMExpand(a->%d\nb->%d\nc->%d\n)), a, b, c);
printf(HMStringify(a->%d\nb->%d\nc->%d\n), a, b, c);
printf(_HMStringify(a->%d\nb->%d\nc->%d\n), a, b, c);
printf("a->%d\nb->%d\nc->%d\n", a, b, c);
複製代碼
利用宏能寫出不少有意思的代碼,若是你是iOS開發者,強烈建議看看我寫的一個最佳實踐HMLog(有且僅有一個HMLog.h
文件),另外也能夠看libextobjc庫對宏的使用。關於宏更多的使用技巧,能夠查看p99或Boost preprocessor。