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

相关文章
相关标签/搜索