简单点,宏定义就是原封不动的替换,要检查自己编写的宏定义是否正确,只需要用宏替换相应的代码(块)即可。宏大致分为两种,一种是对象宏,另一种是函数宏(带参数的宏)。
对象宏:
函数宏:
#define SF_MIN(A, B) A < B ? A : B
如果写成这样:
int a = 3 * SF_MIN(1, 2);
// a -> 2
跟我们预期结果不一样,我们把宏展开看看:
// 3 * 1 < 2 ? 1 : 2
// 3 < 2 ? 1 : 2
// 2
这里面主要涉及到运算符优先级的问题,小于号和比较符号优先级低于乘法运算符,所以先计算了乘法,那就要加上括号。
修正版1:
#define SF_MIN(A, B) (A < B ? A : B)
如果写成这样呢:
int a = SF_MIN(3, 4 < 5 ? 4 : 5);
// a -> 4
跟我们预想的结果还是不一样,我们期望的值是3,结果是4。同样我们展开宏看看:
// (3 < 4 < 5 ? 4 : 5 ? 3 : 4 < 5 ? 4 ; 5)
// ((3 < (4 < 5 ? 4 : 5) ? 3 : 4) < 5 ? 4 ; 5)
// ((3 < 4 ? 3 : 4) < 5 ? 4 : 5)
// (3 < 5 ? 4 : 5)
// 4
#define SF_MIN(A, B) ((A) < (B) ? (A) : (B))
如果写成这样的:
int a = 1;
int b = SF_MIN(a++, 4);
NSLog(@"a = %d, b = %d", a, b);
//预期结果:a = 2, b = 1.
//实际结果:a = 3, b = 3.
展开宏再看看:
//-> int b = ((a++) < (4) ? (a++) : (4))
//-> int b = 1 < 4 ? 1 : 4 , a = 2
//-> int b = 2 < 4 ? 2 : 4 , a = 3
//-> int b = 3 < 4 ? 3 : 4
在比较a++和4的时候,先取a = 1,此时b = 1,a自增1,a = 2。接下来条件比较得到(2 < 4)为真,又触发了一次a++,此时a已经为2,b = 2,最后a再次自增为3, b = 3。我们可以看到,我们期望的是a++只执行一次,但由于宏的展开导致a++被执行了多次。我们要用到GNU C的赋值扩展,即({...})这样的形式,这种形式的语句在顺序执行之后,会将最后一次的表达式的赋值作为返回。先来个简单的例子:
int a = ({
int b = 1;
int c = 2;
b + c;
});
printf("%d", a);
// a -> 3
#define SF_MIN(A, B) ({ \
__typeof__(A) __a = (A); \
__typeof__(B) __b = (B); \
__a < __b ? __a : __b; \
})
说明:typeof是gcc中对C/C++语法的一个扩展,用来静态获取参数类型,比如:
int a = 10;
typeof(a) b = 4; // 相当于int b = 4;
__typeof__("12345") c = "Axe"; //相当于const char c[4] = "Axe";
这里定义了三个语句,分别以输入的类型申明了__a和__b,并使用输入为其赋值,接下来做一个简单的条件比较,得到__a和__b中的较小值,并使用赋值扩展将结果作为返回。这样的实现保证了不改变原来的逻辑,先进行一次赋值,也避免了括号优先级的问题,可以说是一个比较好的解决方案了。如果编译环境支持GNU C的这个扩展,那么毫无疑问我们应该采用这种方式来书写我们的MIN宏,如果不支持这个环境扩展,那我们只有人为地规定参数不带运算或者函数调用,以避免出错。
Apple 在Clang中彻底解决了这个问题,我们可以看下苹果是怎么定义这个宏的:
#define __NSX_PASTE__(A,B) A##B
#define __NSMIN_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__(__a,L) : __NSX_PASTE__(__b,L); })
#define MIN(A,B) __NSMIN_IMPL__(A,B,__COUNTER__)
可能这样看起来比较吃力,我们先美化一下__NSMIN_IMPL__(A,B,L)这个宏。我们知道宏是可以插入换行符而不影响其含义的,改写如下:
#define __NSX_PASTE__(A,B) A##B
#define __NSMIN_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__(__a,L) : __NSX_PASTE__(__b,L); \
})
#define MIN(A,B) __NSMIN_IMPL__(A,B,__COUNTER__)
__typeof__(A) __a__COUNTER__ = (A);
__typeof__(B) __b__COUNTER__ = (B);
(__a__COUNTER__ < __b__COUNTER__) ? __a__COUNTER__ : __b__COUNTER__;
第一个__NSX_PASTE__(A,B)中出现的##,它是一个特殊的符号,表示将两个参数连接起来,函数宏必须是有意义的计算,你不能直接写AB来连接两个参数。
MIN(A,B)其实是调用了__NSMIN_IMPL(A,B,L)__这个宏,前两个就是我们输入的A、B,最后一个参数__COUNTER__,是一个预定义的宏,这个值在编译过程中将从0开始计数,每次被调用时加1。因为它的唯一性,所以很多时候被用来构造独立的变量名称。有了这些基础,再来看最后实现的宏就很简单了。整体思路和前面GUN C MIN是一样的,却别在于为变量名__a和__b添加了一个计数后缀,这样可避免变量名相同而导致问题的可能性。