美文网首页工作生活
宏 - 从入门到精通

宏 - 从入门到精通

作者: 杂货铺学徒 | 来源:发表于2019-08-01 11:52 被阅读0次

image
提起宏定义相信所有人都知道是干什么用的,在项目中也经常使用,通常情况下我们用宏定义也就是定义一些”魔鬼常量”和短函数。
#define M_PI 3.14159265358979323846264338327950288
#define MAX(a, b) ((a) > (b) ? (a) : (b))

如果仅仅会使用类似上面的宏定义,那么其实对于宏定义的掌握只能算是刚刚入门,作为一个资深iOS开发者,非常有必要深入研究一下宏定义的用法及其背后的原理

宏的定义

任何版本的翻译都无法满足宏定义的描述,这里直接摘抄GCC网站对于宏的定义描述。

A macro is a fragment of code which has been given a name. Whenever the name is used, it is replaced by the contents of the macro. There are two kinds of macros. They differ mostly in what they look like when they are used. Object-like macros resemble data objects when used, function-like macros resemble function calls.

You may define any valid identifier as a macro, even if it is a C keyword. The preprocessor does not know anything about keywords. This can be useful if you wish to hide a keyword such as const from an older compiler that does not understand it. However, the preprocessor operator defined (see Defined) can never be defined as a macro, and C++’s named operators (see C++ Named Operators) cannot be macros when you are compiling C++.

宏的类型及用法

Object-like 宏

1、在宏定义时,通常宏的名称都是用大写字母表示,如果要换行就在行末使用 \ 断行。
#define SUM(a, b) (a + b) 
#define NSLog(format, ...) NSLog((@"[文件名:%s]" "[函数名:%s]" "[行号:%d]" format), \
 __FILE__, __FUNCTION__, __LINE__, ##__VA_ARGS__);
2、在调用宏时,预处理器在替换宏的内容时会继续检查宏的内容本身是否也是宏定义,如果是,会继续替换宏定义的内容,直到全部展开。
#define A(a1, a2) (a1 + a2)
#define B(b1, b2) (b1 + b2)
#define SUM(a, b) (a + b)
SUM(A(1, 2), B(3, 4)) => SUM((1 + 2), (3 + 4)) => (1 + 2) + (3 + 4) => 10
3、宏定义以最后有效的定义为准。
#define SIZE (1024)
#define MAX_SIZE SIZE
#undef  SIZE  // 取消宏定义
#define SIZE (2048)

最终 SIZEMAX_SIZE 的值都为 2048。

个人建议不要随意使用 #undef

Function-like 宏

function-like宏后面可以加“()”,但是必须要紧随宏名称,否则就变成了object-like宏。

1、在“()”里可以添加参数,以“,”分隔
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SUM(a, b) (a + b)
2、如果有字符串内容,即使与参数名称相同也不会被替换
#define func(x) x, @"x"
NSLog(@"%@,%@", func(@"abc")); // 输出结果为 abc,x
3、使用 “#” 预处理操作符来实现将宏中的参数转化为字符(串),这个操作会将参数中的所有字符都实现字符(串)化,包括引号,如果参数中间有很多空格,字符(串)化之后将会只用一个空格代替。
#define XSTR(s) STR(s)
#define STR(s) #s
#define FOO 4
NSLog(@"%s", STR(FOO));  // 输出结果为 FOO
NSLog(@"%s", XSTR(FOO)); // 输出结果为 4

出现上面的结果是因为在使用 STR(s) 时, s 是被字符(串)化的,所以宏没有扩展开;而使用 XSTR(s)s 作为一个参数,因此先把宏完全扩展然后再放进参数。

4、使用 “##” 操作符可以实现宏中token的连接
#define VIEW(prefix/*前缀*/, suffix/*后缀*/) prefix##View##suffix
VIEW(UI,)                 // 等价于 UIView
VIEW(UI, Controller)     // 等价于 UIViewController

示例所示表示把prefix和suffix对应的内容与View连接起来,当然连接后的字符(串)必须是有意义的,否则会出现错误或警告;但不能将 “##” 放在宏的最后,否则会出现错误。

预处理器会将注释转化成空格,因此在宏中间,参数中间加入注释都是可以的。只能加入 /**/ 这种注释而不能加入 // 这种。

“##”的特殊用法:

#define NSLog(args, ...) NSLog(args, ##__VA_ARGS__);
NSLog(@"abc")                // 输出结果为 abc
NSLog(@"123, "@"abc")    // 输出结果为 123, abc

将 “##” 放在 “,” 和参数之间,那么如果参数留空的话,那么 “##” 前面的 “,” 就会删掉,从而防止编译错误。

5、可变参数宏

使用标识符 __VA_ARGS__ 来表示多个参数,在宏的定义中则使用 (...) 表示。

#define NSLog(...) NSLog(__VA_ARGS__);

宏的示例详细解析

看到这里相信您已经对宏有更深层次的认识了,但是我不得不说上面的一些示例只适应于一般情况下,在某些特殊情况下还是会出错的。

1、MAX

#define MAX(a, b) ((a) > (b) ? (a) : (b))

如上求最大值的宏在下面的例子中就会出现意想不到的结果

NSInteger a = 1;
NSInteger b = 2;
NSInteger c = MAX(a, b++);
NSLog(@"a = %d, b = %d, c = %d", a, b, c); // 输出结果为 a = 1, b = 4, c = 3

我们期望的输出结果为 a = 1, b = 3, c = 2,但是实际输出结果为 a = 1, b = 4, c = 3,为什么呢?让我们一步一步分析看,

表达式 c=MAX(a,b++); 展开为 c=((a)>(b++)?(a):(b++))

我们把值替换上看一下,在比较 1b++ 的时候, b++ 的值为 2 然后 b 自增 1 ,此时条件不成立且 b 的值为 3 ,然后再执行 b++,得到 c的值为 3b 自增 1 得到结果为 4 。我们本以为 b++ 会执行一次,但是由于宏展开导致了 b++ 被多次执行了。

那么对于这种情况是不是就没有彻底的解决方案了呢?答案是否定的,系统有更优雅的解决方案。

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

这里用到GNU C的赋值扩展,形式为 ({...}),这种形式的语句在顺序执行之后,会将最后一次执行的结果返回。

系统的 MAX(A,B) 定义了三个语句,分别以入参的类型声明两个变量 __a__b ,并用入参为其赋值,接下来简单比较两个值中最大的那个并把结果返回。

这样的 MAX(A,B) 看上去很完美了,但是还是存在一定缺陷的,不信您写成如下的代码试试,结果留给读者自行尝试一下。

NSInteger __a = 1;
NSInteger __b = 2;
NSInteger __c = MAX(__a, __b++);
NSLog(@"__a = %d, __b = %d, __c = %d", __a, __b, __c);

Apple的大牛工程师肯定不会到此为止,他们在Clang中彻底解决了这个问题,源代码摘抄如下:

#define __NSX_PASTE__(A,B) A##B
#define __NSMAX_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__(__b,L) : __NSX_PASTE__(__a,L); })
#define MAX(A,B) __NSMAX_IMPL__(A,B,__COUNTER__)

MAX(A,B) 一共有三个宏来实现

第一个 __NSX_PASTE__(A,B) 宏的作用是把两个参数连接起来;

第二个宏才是实现的主体,这个宏也是定义了三个语句,分别以前两个入参的类型声明两个变量 __aL__bL (其中L是可变的,这个在第三个语句的时候再讲)并用入参为其赋值,接下来简单比较两个值中最大的那个并把结果返回;整个过程与上面 MAX(A,B) 的实现一模一样,关键的地方就在于第三个语句的 __COUNTER__ 参数, __COUNTER__ 是一个预定义的宏,编译过程中将从0开始计数,每被调用一次就加1(类似于数据库的主键),具有唯一性,用在这里是来定义独立的变量名称,那么第二条语句中的 __NSX_PASTE__(__a,L)(或__NSX_PASTE__(__a,L)) 就会变成 __a(或__b) 加一个数字后缀来表示一个变量名字,这样就极大的避免了因变量名字相同而导致的问题,但是如果您一定要把变量定义为__a888,那么出了问题您自己负责吧。

2、NSAssert

NSAssert(self != nil, @"self is nil");

断言是我们在Debug调试中经常使用的一个宏,摘抄源码来分析一下其中的技术点。

#define NSAssert(condition, desc, ...)    \ 
 do {                \ 
 __PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \ 
 if (__builtin_expect(!(condition), 0)) {        \ 
 NSString *__assert_file__ = [NSString stringWithUTF8String:__FILE__]; \   
            __assert_file__ = __assert_file__ ? __assert_file__ : @"<Unknown File>"; \  
      [[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd \     
           object:self file:__assert_file__ \ 
 lineNumber:__LINE__ description:(desc), ##__VA_ARGS__]; \ 
 }               \ 
 __PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \ 
 } while(0)
__PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS
__PRAGMA_POP_NO_EXTRA_ARG_WARNINGS

这两个宏是用来屏蔽warnning的,不用过多解释,我们在项目中也可以拿来使用的。

__builtin_expect

这个函数是GCC在2.96版本引入的,函数声明是 long__builtin_expect(longexp,longc);,我们期望 exp 表达式的值等于常量 c,如果 c 的值为0(即期望的函数返回值), 那么 执行 if 分支的的可能性小, 否则执行 else 分支的可能性小(函数的返回值等于第一个参数 exp)。

if判断里的代码就比较简单了;这里重点要说明的一点就是 do{}while() 的使用,这里用的是while(0),这样的话貌似do{}里的内容只是被执行了一次,那这又有什么意义呢?相信自有它存在的道理。

我们把上面的NSAssert展开来看其中的细节问题

do { 
 __PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS 
 if (__builtin_expect(!(self != nil), 0)) { 
     NSString *__assert_file__ = [NSString stringWithUTF8String:__FILE__];    
           __assert_file__ = __assert_file__ ? __assert_file__ : @"<Unknown File>";   
     [[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd                                                             
                                                         object:self                                                                         
                                                           file:__assert_file__                                                        
                                                     lineNumber:__LINE__                                                            
                                                    description:(@"self is nil")]; 
 } 
 __PRAGMA_POP_NO_EXTRA_ARG_WARNINGS
} while(0);

如果您够细心观察,那么相信您已经发现最后面的 ; 分号了,在NSAssert的宏定义中这个分号是没有的,而在宏展开后这里多了一个分号,其实这个分号就是 NSAssert(self!=nil,@"self is nil"); 这里的分号, 那么这个分号就被 do{}while() 给吃掉了,如果我们在项目中使用 do{}while() 后面不加分号还编译不过呢。

这种吃掉 ; 分号的方式被大量宏代码块所使用,而且这种使用 while(0) 的好处方式在编译时,编译器会帮做好优化操作,最终编译的结果不会因为这个 do{}while() 而导致运行效率低下。

3、ReactiveCocoa

如果 ReactiveCocoa 没有那么多有用的宏的话,相信大多数人都不会选择拿来使用了。接下来让我们一一分析几个常用的宏。

weakify(...) strongify(...)

这两个宏在代码中总是配对出现

@weakify(self)
[RACObserve(self.model, name) subscribeNext:^(NSString *name) { 
 @strongify(self) 
 if(self) 
 { 
 self.nameLabel.text = name; 
 }
}];

为什么要配对出现这个是大家肯定都知道的,但是对于这个宏的展开您又了解多少呢?使用时前面需要添加一个 @ 符号,这又是为什么呢?接下来让我们把宏展开来看看它的真面目。

下面我们把 @weakify(self) 展开

#define weakify(...) \ 
 rac_keywordify \ 
 metamacro_foreach_cxt(rac_weakify_,, __weak, __VA_ARGS__)

Step 1: 展开 rac_keywordify

#if DEBUG
#define rac_keywordify autoreleasepool {}
#else#define rac_keywordify try {} @catch (...) {}
#endif

rac_keywordify 也是一个宏定义,这个比较简单,分 DEBUG 和非 DEBUG 环境,这里以 DEBUG环境为例来展开

@autoreleasepool {}
metamacro_foreach_cxt(rac_weakify_,, __weak, self)

到这里已经可以解释前面提到的添加 @ 符号的疑问了,只是这个 @ 符号用在了 autoreleasepool 的前面,生成了一个空的 @autoreleasepool{} 而已,然而并没有什么用,只是在使用时前面添加一个 @ 符号,看上去很高大上。

Step 2: 展开 metamacro_foreach_cxt

#define metamacro_foreach_cxt(MACRO, SEP, CONTEXT, ...) \ 
 metamacro_concat(metamacro_foreach_cxt, metamacro_argcount(__VA_ARGS__))(MACRO, SEP, CONTEXT, __VA_ARGS__)

metamacro_foreach_cxt 也是一个宏,展开这一步的结果如下:

@autoreleasepool {}
metamacro_concat(metamacro_foreach_cxt, metamacro_argcount(self))(rac_weakify_, , __weak, self)

这一步中最强大的就是 metamacro_argcount(...),这个宏可以在预编译阶段计算出可变参数的个数,如 metamacro_argcount(self 得出的结果为1, metamacro_argcount(A,B) 得出的结果为2. 这里对这个宏不做详细说明,后续会另开篇幅单讲这个宏。

Step 3: 展开 metamacro_concat

#define metamacro_concat(A, B) metamacro_concat_(A, B)
#define metamacro_concat_(A, B) A ## B

metamacro_concat 这个宏做的事情就是把入参拼接起来, metamacro_concat(metamacro_foreach_cxt,metamacro_argcount(self)) 展开这部分的结果是 metamacro_foreach_cxt1

展开这一步的结果如下:

@autoreleasepool {}metamacro_foreach_cxt1(rac_weakify_, , __weak, self)

Step 4: 展开 metamacro_foreach_cxt1

#define metamacro_foreach_cxt1(MACRO, SEP, CONTEXT, _0) MACRO(0, CONTEXT, _0)

在这里,ReactiveCocoa的作者竟然用上一步的入参拼接起来的内容重新定义成一个新的宏,不仅仅有 metamacro_foreach_cxt1,还有 metamacro_foreach_cxt0、2、3、4...20 等21个宏。

继续展开这一步的结果如下:

@autoreleasepool {}
rac_weakify_(0, __weak, self)

注意这个的 _0 是一个参数名,用在这里指的是 self

Step 5: 展开 rac_weakify_

#define rac_weakify_(INDEX, CONTEXT, VAR) \ 
 CONTEXT __typeof__(VAR) metamacro_concat(VAR, _weak_) = (VAR);

metamacro_concat(VAR,_weak_) 这部分展开的结果是 self_weak_,到这一步已经比较简单了,继续展开结果如下:

@autoreleasepool {}
__weak __typeof__(self) self_weak_ = (self);

到这里一个宏都没有了,豁然开朗,看到了熟悉的代码,最终揭开了神秘的面纱。

既然这么简单为什么要绕这么大的圈子呢?别着急, weakify(...) 其实是可以传入多个参数的,最多可以支持传入20个呢。

@weakify(objc1,objc2,objc3...objc20),最终展开的结果是:

@autoreleasepool {}
__weak __typeof__(objc0) self_weak_ = (objc0); __weak __typeof__(objc1) self_weak_ = (objc1); __weak __typeof__(objc2) self_weak_ = (objc2); ... __weak __typeof__(objc19) self_weak_ = (objc19);

到此为止,关于 weakify(...) 的展开就算完成了。

下面我们再把 @strongify(self) 展开

#define strongify(...) \ 
 rac_keywordify \ 
 _Pragma("clang diagnostic push") \ 
 _Pragma("clang diagnostic ignored \"-Wshadow\"") \ 
 metamacro_foreach(rac_strongify_,, __VA_ARGS__) \ 
 _Pragma("clang diagnostic pop")

Step 1: 展开 rac_keywordify

@autoreleasepool {}
metamacro_foreach(rac_strongify_,, self)

Step 2: 展开 metamacro_foreach

#define metamacro_foreach(MACRO, SEP, ...) \ 
 metamacro_foreach_cxt(metamacro_foreach_iter, SEP, MACRO, __VA_ARGS__)

把变量替换后展开结果如下

@autoreleasepool {}
metamacro_foreach_cxt(metamacro_foreach_iter,, rac_strongify_, self)

Step 3: 展开 metamacro_foreach_cxt

这里与 weakify(...) 的Step 2一样,展开结果如下:

@autoreleasepool {}
metamacro_concat(metamacro_foreach_cxt, metamacro_argcount(self))(metamacro_foreach_iter, , rac_strongify_, self)

Step 4: 展开 metamacro_concat

这里与 weakify(...) 的Step 3一样,展开结果如下:

@autoreleasepool {}
metamacro_foreach_cxt1(metamacro_foreach_iter, , rac_strongify_, self)

Step 5: 展开 metamacro_foreach_cxt1

得到结果为:

metamacro_foreach_iter(0, rac_strongify_, self)

Step 6: 展开 metamacro_foreach_iter

#define metamacro_foreach_iter(INDEX, MACRO, ARG) MACRO(INDEX, ARG)

得到结果为 rac_strongify_(0,self)

Step 7: 展开 rac_strongify_

#define rac_strongify_(INDEX, VAR) \ 
 __strong __typeof__(VAR) VAR = metamacro_concat(VAR, _weak_);

这里有一点比较特别,那就是等号右边的表达式 metamacro_concat(VAR,_weak_) 展开后的结果为 self_weak_,这里的 self_weak_ 就是 weakify(self)__weak __typeof__(self)self_weak_=(self); )声明的一个变量。

最后展开结果为:

__strong __typeof__(self) self = self_weak_;

到此, strongify(self) 的展开也就完成了;同样的 strongify(...) 最多也可以支持传入20个参数。

那么开始我们编写的示例代码展开后最终结果如下:

@autoreleasepool {}
__weak __typeof__(self) self_weak_ = (self);
[RACObserve(self.model, name) subscribeNext:^(NSString *name) { 
 @autoreleasepool {} 
 __strong __typeof__(self) self = self_weak_; 
 if(self) 
 { 
 self.nameLabel.text = name; 
 }
}];

宏在项目中的应用

1、NSLog

在项目开发中我们希望在Release配置下不做日志输出,这个比较简单

#ifndef __OPTIMIZE__ 
#define NSLog(format, ...) NSLog((@"[文件名:%s]" "[函数名:%s]" "[行号:%d]" format), __FILE__, __FUNCTION__, __LINE__, ##__VA_ARGS__);
#else 
#define NSLog(...){} 
#endif

2、Singleton

单例也是我们项目中经常使用的,如果每个单例都单独实现一遍,那么会有大量的重复代码,后续维护也会是灾难性的,如果把单例写成宏来使用,写起来简单,维护起来也只有一份代码,何乐而不为呢?

// Header
#define SINGLETON_HEADER(singletonClassName) \
+ (singletonClassName *)sharedInstance;

// Implementation
#define SINGLETON_IMPLEMENTATION(singletonClassName) \
\
static singletonClassName *_sharedInstance = nil; \
+ (instancetype)sharedInstance { \ 
 static dispatch_once_t onceToken; \ 
 dispatch_once(&onceToken, ^{ \ 
 _sharedInstance = [[self alloc] init]; \ 
 }); \ 
 return _sharedInstance; \
} \
\
+ (instancetype)allocWithZone:(struct _NSZone *)zone { \ 
 static dispatch_once_t onceToken; \ 
 dispatch_once(&onceToken, ^{ \ 
 _sharedInstance = [super allocWithZone:zone]; \ 
 });\ 
 return _sharedInstance; \
} \
\
- (id)copyWithZone:(NSZone *)zone { \ 
 return _sharedInstance; \
}

如果再项目中想要定义单例,在头文件和实现文件中直接调用 SINGLETON_HEADER()SINGLETON_IMPLEMENTATION() 就可以搞定。

3、Class

在我们的项目中,基本上所有的源文件都会带有我们自己的命名前缀(或者叫命名空间),那么当我们引入别人的开源模块时,如果可以,我们也总是希望加上我们自己的命名前缀来加以区分;那么既然这样,当我们自己在写一些独立模块时并且希望以后可以以源码的方式开源出去给别人使用时,我们是否可以为使用者提供一个便利的解决方案来添加命名前缀呢?当然可以,可以用宏来实现啊。如下代码部分摘抄PLCrashReporter 库中的宏定义如下。

// .h 文件
#define PL_PREFIX JD
#define PLNS_impl2(prefix, symbol) prefix ## symbol
#define PLNS_impl(prefix, symbol) PLNS_impl2(prefix, symbol)
#define PLNS(symbol) PLNS_impl(PL_PREFIX, symbol)

#define SINGLETON_CLASS           PLNS(CartManager)

@interface SINGLETON_CLASS : NSObject
SINGLETON_HEADER(SINGLETON_CLASS)

@end

// .m 文件
@implementation 
SINGLETON_CLASSSINGLETON_IMPLEMENTATION(SINGLETON_CLASS)

@end

使用时和使用普通类没有什么差别

SINGLETON_CLASS *instance = [SINGLETON_CLASS sharedInstance];

在项目中使用时我们可以直接使用 SINGLETON_CLASS,如果需要修改类名直接在.h文件中修改 PLNS 宏的参数即可,如果要修改名字的前缀可以直接修改 PL_PREFIX 的值,那么就会生成一个带有特定 PL_PREFIX 前缀的的新类,直接把文件Copy走就能使用。

还有很多很多的宏在网上都能查到,这里就不一一列出了。

总结

讲了这么多宏的优势,同样宏也存在很明显的弊端

1、宏是直接嵌入的,代码会相对多一点

2、嵌套定义过多会影响程序的可读性,且很容易出错

3、 对带参的宏而言,由于是直接替换,并不会检查参数是否合法,存在安全隐患

学习了宏的用法,详细分析了几个比较常用宏的,也了解了宏的弊端,相信您现在对宏应该有了一个更高层次的认识,看到宏也不会那么模糊了,只需要一步一步展开就能看清其背后的逻辑,项目中也不仅仅可以用宏来替代一些数字,还可以把一些复杂的高频出现的代码用宏来实现。

参考文献


本文非原创

相关文章

网友评论

    本文标题:宏 - 从入门到精通

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