iOS中宏的入门和实战

最近新加入了项目组,看到了一个让我很费解的宏定义

#define ServiceTypeMake(_cls, _sel) (NO && ((void)[_cls _sel[Service new]], NO),\ [NSString stringWithFormat:@"%s.%s", #_cls, #_sel])

去掉一些敏感信息后的宏就是这个样子。看到这个宏之后,根据我之前的经验只晓得[NSString stringWithFormat:@"%s.%s", #_cls, #_sel]这块是字符串拼接,前半部分目测像写了个废话(NO &&结果明显是NO啊!!!)。

ServiceTypeMake(IDUrlService, sendUrlRequest:)

预编译展开后就是这样的:

(__objc_no && ((void)[IDUrlService sendUrlRequest:[Service new]], __objc_no), [NSString stringWithFormat:@"%s.%s", "IDUrlService", "sendUrlRequest:"])

&&短路与,前面判断为假后面就不执行了,最后相当于就是个字符串拼接,所以这个宏的写法我觉得不是很能理解。通过查阅一些资料和文章后,我对iOS中宏的定义和使用有了一些深入的了解,希望大家指正。


1.宏的入门

 *宏的定义

任何中文的翻译都不够权威,这里摘抄一下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.  Thiscan 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 amacro, and C++’s named operators (see C++ Named Operators) cannot be macros when you are compiling C++.

从定义中可以知道宏分两种对象宏(Object-like macros)和方法宏(function-like macros),接下来一一介绍。

对象宏(Object-like macros)

这里先举一些🌰:

#define BUFFER_SIZE 1024

#define NUMBERS 1, \

                                + 2, \

                                + 3

#define NUMBER_TOTAL NUMBERS

看上面这些例子可以总结出以下东西:

1.在定义宏的时候,宏的名字通常都会使用大写字母,目的是方便大家一眼就能看出这是个宏,引用的GCC原文是:

By convention, macro names are written in uppercase. Programs are

easier to read when it is possible to tell at a glance which names are

macros.

2.如果一个宏的定义中需要换行的话可以用反斜杠“\”结尾来断行

3.在调用宏时,预处理器在替换宏的内容时会继续检查宏的内容本身是否也是宏定义,如果是,会继续替换宏定义的内容,直到全部展开。

这里可能会有一点疑问,就是对于自引用宏的处理,先举个例子:

#define FOO (4 + FOO)

这要是在使用中要一直展开的话就会是一个无限递归的过程,GCC中有对这种情况进行处理,只会在使用FOO的地方替换成(4 + FOO),还有一种情况是:

#define x (4 + y)

#define y (2 * x)

这里会展开为如下形式:

x → (4 + y)

    → (4 + (2 * x))

y    → (2 * x)

    → (2 * (4 + y))

4.宏定义以最后有效的定义为准,例如

#define FOO 4

#undef FOO

#define FOO 5

NSLog(@"%d", FOO);     → 5

方法宏(function-like macros)

方法宏的特点是在定义宏的时候宏的名字后会接着一对括号,如:

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

这里也有一些注意点:

1.重中之重,就是有参数的时候在表达式里尽量多加括号(防御式编程),避免一些操作符优先级问题造成结果错误:

#define MULTIP(a, b) a * b

NSLog(@"%d", MULTIP(1+2, 3+4));     →11

MULTIP(1+2, 3+4)

          →1+2  *  3+4

                   →11

2.如果有字符串内容,即使与参数名称相同也不会被替换

#define func(x) x, @"x"

NSLog(@"%@,%@", func(@"abc")); // 输出结果为 abc,x

3.使用"#"预处理操作符来实现将宏中的参数转化为字符(串),这个操作会将参数中的所有字符都实现字符(串)话,包括引号。(有的文章里面说会把参数里面的多个空格替换为一个空格,亲测不会)

#define XSTR(s) STR(s)

#define STR(x) #x

#define FOO @"    a      b    cc"

NSLog(@"%s", STR(FOO));    //输出FOO

NSLog(@"%s", XSTR(FOO));   //输出@"    a      b    cc"

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

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

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

经过上面这些内容,大家对宏的使用已经有了基本的认识,但在实际使用过程中依然还有很多坑(特别是函数宏),只有多使用和学习,才能慢慢达到掌握的水准,我们要开始踩坑之旅了。

1.MIN

我们很快会得出我们的版本:

#define MIN_(a, b) ((a) < (b) ? (a) : (b))

测试一下:

初级版:

NSLog(@"%d",MIN_(3,5));

//=>NSLog(@"%d", ((3) < (5) ? (3) : (5)));

//=>3

高级版:

 NSLog(@"%d",MIN_(2+6,3*4));

//=>NSLog(@"%d", ((2+6) < (3*4) ? (2+6) : (3*4)));

//=>8

测试通过,发布上线!!!


开心!!!

项目上线了,跑的没问题,直到有一天一个特殊的情况出现了:

 NSInteger a = 5;

 NSInteger b = 2;

NSInteger c = MIN_(a, b++);

NSLog(@"a = %d, b = %d, c = %d", a, b, c); //输出a = 5 , b = 4, c = 3;

一脸懵逼,这不是真的~


what???

我们期望的结果是a = 5,b = 3, c = 2,a是5,b是2,b++先取b的值跟a比,比a小,所以c得值是2,b自增1,最后b的值是3,这才是想要的结果嘛。为啥会出现上面的结果呢,不妨展开一下

NSInteger c = MIN_(a, b++) = ((a) < (b++) ? (a) : (b++));

这里a先跟b比,5比2大,b会自增1,b为3,此时c取b得值为3,接着b会再自增1,为4。问题就出在比较完a,b的值后b进行了自增1的操作。这里小括号已经无法解决这个问题了,我们需要借助GNU C的赋值扩展({...}),使用这样的方法可以计算出一个对象,而且不会浪费变量名,可以在小范围的作用域内计算特殊的值

NSInteger x = ({

        NSInteger y = 2;

        NSInteger z = 3;

        y + z;

    });

    NSInteger y = 4; //继续使用y,z当变量没问题

    NSInteger z = 5;

有了这个扩展,我们就能解决MIN的写法问题了,GNU C中MIN的标准写法是

#define MIN(A,B) ({ __typeof__(A) __a = (A);\

 __typeof__(B) __b = (B);\

 __a < __b ? __a : __b; })

赋值扩展里包含3个语句,前两个定义了两个变量__a和__b其类型为输出参数的类型,再进行取小值得运算。用这个写法试了下完美解决问题,但依然会有坑。

NSInteger __a = 5;

NSInteger __b =2;

NSInteger __c =MIN_(__a, __b++);

NSLog(@"__a =%d, __b = %d, __c = %d", __a, __b, __c); //输出__a = 5, __b = 2, __c = 0

因为赋值函数内变量名与外部变量名重名而造成无法被初始化,造成宏的行为不可预知。Apple的工程师彻底解决了这个问题,官方的写法是

#define __NSX_PASTE__(A,B) A##B

#if !defined(MIN)

    #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__)

#endif

__NSX_PASTE__这个宏是用来做字符串拼接, __NSMIN_IMPL__这个宏除了字符串拼接之外跟之前的宏写法完全一样,比较费解的就是__NSMIN_IMPL__(A, B, __COUNTER__)这里第三个参数__COUNTER__,__COUNTER__是预定义的宏,在编译阶段开始时从0开始计数,每次被使用时加1,这样就大大避免了变量名重名的可能性。如果一定要任性的定义变量名为__a234这样的名字,后果只会是"no zuo, no die"。

2.NSLog

通过打印来调试是我们经常使用的一种方式,我们希望测试的时候多输出一些信息,比如所在行、方法名等,在release环境下不打印信息,通常我们的写法会是

#ifdef DEBUG

#defineNSLog(format, ...) do {                                                                          \

fprintf(stderr,"<%s : %d> %s\n",                                          \

[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],  \

__LINE__, __func__);                                                        \

(NSLog)((format), ##__VA_ARGS__);                                          \

fprintf(stderr,"-------\n");                                              \

} while (0)

#else

#define NSLog(...);

#endif

这个宏定义比较复杂了,首先判断是否定义了DEBUG,就是说是否在DEBUG模式下,我们按住command点击一下并找不到定义的位置,那DEBUG在哪里定义的呢? 在 "Target > Build Settings > Preprocessor Macros > Debug" 里有一个"DEBUG=1"。在没打包的情况下,就属于DEBUG模式。再看宏定义,先抛开do{}while(0),看内部。首先会打印文件路径的最后一个路径,还有行数和调用的方法。这里出现了__FILE__, __LINE__, __func__三个预定义宏,具体的作用大家可以参考预定义宏。接着是一个标准的NSLog调用,输出参数信息,最后在打印一排“------------”并换行。这里比较费解的就是外层的这个do{}while(0)的作用了,貌似没啥用,就是让代码执行一次,那我们可不可以去掉do()while(0)把NSLog写成这样

#defineNSLog(format, ...)                                                                          \

fprintf(stderr,"<%s : %d> %s\n",                                          \

[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],  \

__LINE__, __func__);                                                        \

(NSLog)((format), ##__VA_ARGS__);                                          \

fprintf(stderr,"-------\n");                                              \

NSLog(@"aaa");

这个例子里我们可以正常输出了,但如果我们碰到这样的呢

if(1)

        NSLog(@"aaa");

这里首先出现了一种反面的写法,就是if条件语句判断后不带大括号。我们先看看程序预编译之后的结果

if (1)

        fprintf(__stderrp,"<%s : %d> %s\n", [[[NSString stringWithUTF8String:"/Users/baidu/Desktop/test/TestMacro/TestMacro/ViewController.m"] lastPathComponent] UTF8String],98, __func__); (NSLog)((@"aaa")); fprintf(__stderrp,"-------\n");;

我们知道if判断不带大括号的写法是只会执行判断条件后面的一个语句,虽然这里执行完后好像对结果没什么影响,我们还是觉得是不是加个大括号更好一点,就有了一个高级点的版本

#defineNSLog(format, ...) {                                                                          \

fprintf(stderr,"<%s : %d> %s\n",                                          \

[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],  \

__LINE__, __func__);                                                        \

(NSLog)((format), ##__VA_ARGS__);                                          \

fprintf(stderr,"-------\n");                                              \

}

这样看着就没什么毛病了,还减少了do()while(0)的执行,减少了点点CPU的消耗,很开森!很明显,事情不会这么简单,总有童鞋会有“新奇”的写法

if(1)

        NSLog(@"aaa");

    else

        NSLog(@"bbb");

这么写了一下,编译都会报错,wtf???看下预编译的结果

if (1)

        { fprintf(__stderrp,"<%s : %d> %s\n", [[[NSString stringWithUTF8String:"/Users/baidu/Desktop/test/TestMacro/TestMacro/ViewController.m"] lastPathComponent] UTF8String],98, __func__); (NSLog)((@"aaa")); fprintf(__stderrp,"-------\n"); };

    else

        { fprintf(__stderrp,"<%s : %d> %s\n", [[[NSString stringWithUTF8String:"/Users/baidu/Desktop/test/TestMacro/TestMacro/ViewController.m"] lastPathComponent] UTF8String],100, __func__); (NSLog)((@"bbb")); fprintf(__stderrp,"-------\n"); };

细心的同学应该会发现if判断的大括号后居然多了一个“;”,这个“;”哪里来的呢,是NSLog(@"aaa");这里出来的,就报错了。在试试加了do{}while(0)的版本呢,编译一下看看区别

if (1)

        do{ fprintf(__stderrp,"<%s : %d> %s\n", [[[NSString stringWithUTF8String:"/Users/baidu/Desktop/test/TestMacro/TestMacro/ViewController.m"] lastPathComponent] UTF8String],98, __func__); (NSLog)((@"aaa")); fprintf(__stderrp,"-------\n"); }while(0);

    else

        do{ fprintf(__stderrp,"<%s : %d> %s\n", [[[NSString stringWithUTF8String:"/Users/baidu/Desktop/test/TestMacro/TestMacro/ViewController.m"] lastPathComponent] UTF8String],100, __func__); (NSLog)((@"bbb")); fprintf(__stderrp,"-------\n"); }while(0);

咦,一下就不一样了,do{}while(0)完美的利用上了最后的分号。事实上,在编译器编译阶段,遇到do{}while(0)时会做一些优化,执行的时间并不会变多。从这里我们总结出来两点

(1)在宏展开有多个语句时,可以包裹上do{}while(0)来吃掉“;”

(2)不要出现if判断不带{}的写法,貌似简化了一点,事实上往往会害人害己,出现一些莫名其妙的异常。

在我们平时遇到的宏中还有很多更复杂、更有趣的宏。首先不要谎,可以先将其展开来慢慢分析,通过一点点琢磨,会理解其精髓的。最后提一下iOS中怎么进行预编译查看结果

1.点击辅助编辑按钮


2.点击出现的这个按钮

3.选择下拉中出现的Preprocess选项,就会出现编译结果。

《宏的入门和理解》就到这里了,之后有时间还会在分享一些更复杂的宏。😊


参考文章:

1.GCC文档:https://gcc.gnu.org/onlinedocs/cpp/Operator-Precedence-Problems.html#Operator-Precedence-Problems

2.宏菜鸟起飞手册:https://blog.csdn.net/hopedark/article/details/20699723

3.京东宏分享:https://mp.weixin.qq.com/s/qTFLZFL2IAz1ScrV1u313Q

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • http://www.open-open.com/lib/view/open1390651437117.html ...
    Xtuphe阅读 1,255评论 0 10
  • 宏定义在C系开发中可以说占有举足轻重的作用。底层框架自不必说,为了编译优化和方便,以及跨平台能力,宏被大量使用,可...
    你好自己阅读 1,054评论 0 5
  • /**获取屏幕宽度与高度 导航,tabbar高度*/ #define SCREEN_WIDTH [UIScree...
    MUYO_echo阅读 685评论 0 3
  • 喜欢读一些开源项目源码的人,总是会发现,大神的代码中总是有那么一些简短而高效的宏定义,点击进去一看,发现晦涩难懂,...
    我是强强阅读 288评论 0 0
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,339评论 8 265