C語言宏定義使用總結與遞歸宏

C語言宏定義使用總結與遞歸宏

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)

宏定義裏能夠有多行語句,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個參數,留給讀者思考。

接下來正式介紹遞歸宏,這裏給出兩種方法。

1. 鏈接宏的參數個數,定義一系列結構類似的宏。

我須要一個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用鏈接符##_HMFormatHMMacroArgCount(__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,此時只須要測試最多參數的例子,沒有出錯就幾乎保證了全部這類宏都沒寫錯。再次強調一點,宏的遞歸展開只發生在預編譯期間,這種遞歸併不影響運行時效率。

2. 利用宏的延遲展開和屢次掃描

這種方法較難理解,仍是用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庫對宏的使用。關於宏更多的使用技巧,能夠查看p99Boost preprocessor

相關文章
相關標籤/搜索