美文网首页
ios宏的总结

ios宏的总结

作者: guoguojianshu | 来源:发表于2019-03-12 18:02 被阅读0次

    宏定义为对象宏函数宏,对象宏通常是对一些简单的对象进行替换,
    #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;
    }
    
    

    这道题的正确答案是分别是12f(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; })
    
    

    这个宏用来取输入参数中较小的值,并将该值作为返回值返回。这里就是用到了({...})语句来实现,函数体中可以做任意的逻辑处理和运算,但最终的返回值则是最后的表达式。所以在定义宏的时候,我们可以用({...})语句来定义有返回值的函数宏,这个也是函数宏很常见的写法,大家在实际项目中也可以注意参照使用。

    相关文章

      网友评论

          本文标题:ios宏的总结

          本文链接:https://www.haomeiwen.com/subject/nbowpqtx.html