C语言 宏定义

222 阅读3分钟

简单点,宏定义就是原封不动的替换,要检查自己编写的宏定义是否正确,只需要用宏替换相应的代码(块)即可。宏大致分为两种,一种是对象宏,另一种是函数宏(带参数的宏)。

对象宏:

#define SFNavigationBarHeight 44.0

函数宏:

我们着手定义一个两个数中求最小值的宏。

初始版:

#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

修正版2:

#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

修正版3:

#define SF_MIN(A, B) ({  \
    __typeof__(A) __a = (A); \
    __typeof__(B) __b = (B); \
    __a < __b ? __a : __b;   \
})

说明:typeofgcc 中对 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__)

__NSMIN_IMPL__(A,B,L)__ 其实可转化为下列的伪代码:

__typeof__(A) __a__COUNTER__ = (A);
__typeof__(B) __b__COUNTER__ = (B);
(__a__COUNTER__ < __b__COUNTER__) ? __a__COUNTER__ : __b__COUNTER__;

这样看起来就清晰多了,可以看到 MIN 宏共由三个宏组合而成的。

第一个 __NSX_PASTE__(A,B) 中出现的 ##,它是一个特殊的符号,表示将两个参数连接起来,函数宏必须是有意义的计算,你不能直接写 AB 来连接两个参数。

MIN(A,B) 其实是调用了 __NSMIN_IMPL(A,B,L)__ 这个宏,前两个就是我们输入的 A、B,最后一个参数__COUNTER__,是一个预定义的宏,这个值在编译过程中将从 0 开始计数,每次被调用时加 1。因为它的唯一性,所以很多时候被用来构造独立的变量名称。有了这些基础,再来看最后实现的宏就很简单了。整体思路和前面 GUN C MIN 是一样的,区别在于为变量名 __a__b 添加了一个计数后缀,这样可避免变量名相同而导致问题的可能性。