宏定义为对象宏和函数宏,对象宏通常是对一些简单的对象进行替换,
如#define M_PI 3.1415
,函数宏(在宏名字后面加上()
)可以接受参数如函数调用一样在代码中使用,如#define sum(a,b) a+b
,使用函数宏时候应该注意:-函数名和括号之间不能有空格,如果有空格就会变为对象宏了(因为宏是按照空格进行分割的
);-函数式宏定义的参数没有类型,预处理器只能只能做形式上的替换,而不做参数类型的检查
宏定义的符号
在宏定义中有四个特殊符号:1)\
2)#
3)##
4)...
,他们分别代表换行,字符串化,连接运算,可变参数。
1)换行符
在字符串可以使用\n
进行换行操作,同样在宏中也可以换行符而不影响其含义,只不过在宏使用的是\
来标识换行。
#define sum(a,b)\
a+b
等于:
#define sum(a,b) a+b
字符串化
单个#
号的作用是字符串化,简单来说就是在输入值上加上""
双引号,使其转换为C字符串。若果在objc环境下,可以在头部再加上@
符号,结果为NSString
类型。
#define string(b) #b
NSString
类型
#define string(b) @#b
字符串化对
空格
的处理有两种情况:a).忽略传入参数名前面和后面的空格NSLog(@"%@",string( 112 ));=>@"112"
b)当传入的参数之间有空格时候,忽略其中多于一个的空格NSLog(@"%@",string(1 12));=>@"1 12"
连接运算
连接符号##
用来将前后两项合并成一个整体。执行分为两个步骤,先分割(按照##
号进行分割操作),然后会将分割的部分去掉空格后进行合并。
#define stingCompontent(a,b) a ## b
NSString * str = @"宏 使用 连接运算";
stingCompontent(st,r);
NSLog(@"%@",stingCompontent(st,r));
可变多参数
标识符号...
用来标识该宏可以接收可变的参数数量(零个或多个)。在宏体重,使用__VA_ARGS__
来表示那些输入的实际参数,也就是__VA_ARGS__
在预处理阶段将被实际的参数集替换。需要注意的是...
只能放在末尾,替换最后面的宏参数。
同
...
与__VA_ARGS__
配对类似,也可以使用name...
与name
来配对使用表示可变多参数。不同于前者,这里的name
是你任意的参数名,并不是系统保留名。
#define NSLog(format,...) NSLog((@"%s [line %d]" format),__func__,__LINE__,##__VA_ARGS__)
//这样可以的
#define NSLog(format1,name...) NSLog((@"%s [line %d]" format1),__func__,__LINE__,##name)
在这个宏定义中,如果是非DEBUG环境,那么直接替换为空,也就是NSLog将不起任何作用。我们重点讨论DEBUG环境下的定义,第一个参数format
将被单独处理,接下来输入的参数则作为一个整体被视为可变参数。比如NSLog(@"name = %@, age = %d", @"Ryan", 18)
, 这里的@"name = %@, age = %d"
即对应宏里的format
, 后面的@"Ryan"
和18
则映射为...
指代为统一的可变参数。 因为我们不确定用户格式化字符串时会输入多少个参数,所以我们指定为可变参数,允许用户输入任意数量的参数。带入具体的实参后替换后的结果为:
NSLog((@"%s [Line %d] " "name = %@, age = %d"), __func__, __LINE__, ##@"Ryan", 18);
不知道你有没有注意到__VA_ARGS__
前面的##
标识符,有了上文的介绍,我们知道它是用来做连接操作的,也就是将name = %@, age = %d
和前面的参数连接后打印出来。但是__VA_ARGS__
本来就是顺着__LINE__
后面写的,应该不需要加##
吧?YES! 确实不需要加##
来做"连接"的作用,那为什么还要加呢?
既然是可变多参数,那它是包括一个case的: 参数数量为0,如果我们把##
去掉,替换后宏就变成如下结果:
NSLog((@"%s [Line %d] "), __func__, __LINE__, ); // 注意最后一个逗号
有没有发现,当可变参数的个数为0时,最后面会多一个逗号,显然这个情况下编译器会报错的,那怎么才能支持0参数的情况呢?答案就是##
. 当可变参数的个数为0的时候,##
会把前面多余的逗号去掉,所以定义可变参数宏需要记得加上##
来处理这个情况。
宏定义展开
当宏定义有多层嵌套的情况,即宏定义里面又包含另外的宏定义,这时宏展开(替换)需要遵循一定的规则,总体原则是每次只解开当前层的宏,我们直接来看下面这个示例:
#define _ANONYMOUS1(type, var, line) type var##line
#define _ANONYMOUS0(type, line) _ANONYMOUS1(type, _anonymous, line)
#define ANONYMOUSS(type) _ANONYMOUS0(type, __LINE__)
带入实参ANONYMOUSS(static int);
即: static int _anonymous70;
70表示该行行号。这个宏包含三层,逐一解析:
第一层:ANONYMOUSS(static int) –> _ANONYMOUS0(static int, __LINE__)
第二层: –> _ANONYMOUS1(static int, _anonymous, 70);
第三层: –> static int _anonymous70;
由于每次只能解开当前层的宏,__LINE__
需要等到第二层才能被解开。所以如果我们把中间层_ANONYMOUS0
去掉,直接由_ANONYMOUS1
来定义ANONYMOUSS
:
#define _ANONYMOUS1(type, var, line) type var##line
#define ANONYMOUSS(type) _ANONYMOUS1(type, _anonymous, __LINE__)
再次带入实参ANONYMOUSS(static int);
这个情况下,最终的结果会是static int _anonymous__LINE__
,预定义宏__LINE__
并不会被解开!所以当你看一些有嵌套宏定义的时候(包括系统的宏定义),你会发现它们往往会加多一层中间转换宏,加这层宏的用意是把所有宏的参数在这层里全部展开,这个我们在自己实际项目中定义复杂宏的时候也需要特别留意。
这里用到了预定义宏
__LINE__
,预定义宏的行为是由编译器指定的。__LINE__
返回展开该宏时在文件中的行数,其他类似的有__FILE__
返回当前文件的绝对路径;__func__
是该宏所在scope的函数名称;__COUNTER__
在编译过程中将从0开始计数,每次被调用时加1。因为唯一性,所以很多时候被用来构造独立的变量名称。
宏展开的另外一个规则是,在展开当前宏函数时,如果形参有#
或##
则不进行宏参数的展开,否则先展开宏参数,再展开当前宏。我们来看一道经典的C语言题目:
#include <stdio.h>
#define f(a,b) a##b
#define g(a) #a
#define h(a) g(a)
int main() {
printf("%s\n", h(f(1,2))); // => 12
printf("%s\n", g(f(1,2))); // => f(1,2)
return 0;
}
这道题的正确答案是分别是12
和f(1,2)
,后者宏g
里面的参数f(1,2)
不会被展开。我们对照上面宏展开的规则来分析下:
第一行h(f(1,2))
由于h(a)
非#
或者##
所以先展开参数f(1,2)
即12
再展开当前宏h(12)
=> g(12)
=> 12
第二行g(f(1,2))
由于g(a)
形参带有#
所以里面的f(1,2)
不会被展开,最终结果就是f(1,2)
宏的实例
#define hbgcKeyPath(objc,path) @(((void)objc.path,#path))
这个宏是用于寻找对象中某个属性是否存在的,这个宏整体是一个c语言的逗号表达式,逗号表达式的格式:int a = (b,c);
逗号表达式取后面的值,所以a将被赋值成c,此时b在赋值运算中被忽略了,没有被使用,所以编译器会给出警告,为了消除warning我们需要在b前面加上(void)
做个类型强制转换操作。
在ObjC里面的block为了防止循环引用,我们会使用__weak
关键字,这个宏就是用来实现obj的weak化,调用的时候则是weakObj(self)
, 但是iOS都是习惯加@
符号,比如字符串是@""
, 数组是@[]
, 就连定义协议都是@protocol
, 那怎么让我们的weakObj
也能在前面加上@
符号呢?
iOS开发的同学应该都记得系统的自动释放池@autoreleasepool{}
, 这里面就有个@
符号,所以我们可以在weakObj
的宏定义里面放一个空的autoreleasepool{}
, 并且不加@
符号,让这个@
符号有外面调用的时候加上,也就是这样的:
#define weakObj(obj) autoreleasepool{} __weak typeof(obj) obj##Weak = obj;
调用的时候@weakObj
里的@
符号就被加到autoreleasepool{}
上了,其实这个autoreleasepool{}
是空的,并不起任何实际作用:
@weakObj(obj) => @autoreleasepool{} __weak typeof(obj) obj##Weak = obj;
#define hbgcweak(objc) autoreleasepool{} __weak typeof(objc) weak##objc = objc
//使用
@hbgcweak(self);
[weakself.view setValue:[UIColor redColor] forKey:hbgcKeyPath(self.view, backgroundColor)];
#define hbgcweak(objc) @autoreleasepool{} __weak typeof(objc) weak##objc = objc
//没有@的使用
hbgcweak(self);
[weakself.view setValue:[UIColor redColor] forKey:hbgcKeyPath(self.view, backgroundColor)];
宏知识补充
由于宏定义的实质只是文本替换,所以这个并不智能的替换会在一些环境下发生不可预知的错误。幸运的是,我们的前辈们发现了这些问题,并且提供了很好的解决方案,这也是我们下面要讨论的许多宏定义约定俗成的格式写法。
1) 使用do{}while(0)语句
对于函数宏,我们一般都是推荐使用do{}while(0)
语句,把函数体包到do
后面的{}
内,为什么要这样呢?我们看一个实例:
#difne FOO(a,b) a+b; \
a++;
正常调用是没有问题的,但是如果我们是在if
条件语句里面调用,并且if
语句没有{}
, 像下面这样:
if (...)
FOO(a,b) // 满足了if条件后FOO会被执行
展开之后就会变成(显然就不对了):
if (...)
a+b; // a+b在满足了if条件后会被执行
a++; // a++不管if条件是否满足都会被执行
如果加上do{}while(0)
语句展开后就是:
if (...)
do {
a+b;
a++;
} while (0);
这样就没有问题了,但你肯定会疑惑,这个和直接包一个{}
不是一样的吗,只要把函数体包成一个整体就可以了。是的,在这个情况下是一样的,但是do{}while(0)
还有一个功能,会去除多余的分号,我们还是看实例:
#difne FOO(a,b) { a+b; \
a++; }
if (...)
FOO;
else
...
使用{}
情况下我们展开来看:
if (...) {
a+b;
a++;
}; else // 注意这边多出来的分号,编译直接报错!
...
如果是do{}while(0)
的话会直接吸收掉这个分号:
if (...)
do {
a+b;
a++;
} while(0); // 分号被do{}while(0)吸收了
else {
...
}
#define NSLog(format1,name...) do{ \
NSLog((@"%s [line %d]" format1),__func__,__LINE__,##name);\
} while (0);
这个吸收分号的方法现在已经几乎成了标准写法。而且因为绝大多数的编译器都能够识别do{}while(0)
这种无用的循环并进行优化,所以不会因为这种方法导致运行效率上的差异。
2) 使用({...})语句
GNU C里面有个({...})
形式的赋值扩展。这种形式的语句在顺次执行之后,会将最后一次的表达式的赋值作为返回。
#define MIN(A,B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; })
这个宏用来取输入参数中较小的值,并将该值作为返回值返回。这里就是用到了({...})
语句来实现,函数体中可以做任意的逻辑处理和运算,但最终的返回值则是最后的表达式。所以在定义宏的时候,我们可以用({...})
语句来定义有返回值的函数宏,这个也是函数宏很常见的写法,大家在实际项目中也可以注意参照使用。
网友评论