C语言Hello, 宏定义魔法世界

宏,一言以蔽之就是按预定义的平整替换相应的文本内容,被轮换的文书内容可以是目的也足以是函数。既然是替换,这就需要依据一定的规则来实施,这里的平整就是本文要钻探的严重性内容,希望通过浓厚浅出和逐层剖析的情势可以让我们对宏定义有更加淋漓尽致的明白,继而可以在其实项目中动用并表明宏定义的magic.

利用宏定义不仅能够让代码看起来更为从简易读,更着重的是可以举办编译检查。由于宏定义是在预处理的时候被实施的,由此得以在编译在此以前就反省出包括参数类型,参数完整性等息息相关的失实。比如ReactiveCocoa里头的keypath(…)宏,可以承受可变参数:
keypath(self.observerPath)keypath(self, observerPath)
若果self没有observerPath这一个成员变量,那么编译器会一向提交错误提醒,避免等到运行时才发现路径无效而致使程序异常。

宏定义分为对象宏函数宏,对象宏日常是有些简便的靶子替换,比如#define M_PI 3.1415,
函数宏(宏名字后增长括号)可以承受参数如函数调用一样在代码中行使,比如
#define ADD(x,y) x + y. 函数宏有两点需要小心: a).
括号与宏名之间无法有空格,否则就成靶子宏了 b).
函数式宏定义的参数没有项目,预处理器只担负做花样上的替换,而不做参数类型检查,所以传参时要十分小心。

Okay, 介绍完宏的基本概念之后,让大家专业进入宏定义的魔法世界呢!

宏定义符号

在宏定义中,有多少个独特的标记:1)\ 2)# 3)## 4)
...,它们各自代表换行,字符串化,连接运算和可变多参数。

1) 换行符

俺们知晓在字符串里面可以采纳\n来贯彻换行,同样在宏定义里面也可以插入换行符而不影响其意思,只然则在宏里面是用反斜杠\来标识换行。

#define APP_VERSION() \
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]

等价于:

#define APP_VERSION() [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]

如若不加\换行标识符而直接转行,则第二行的内容就不属于宏定义了,
APP_VERSION()会被定义成空,也就是调用APP_VERSION会不起另外效能。

#define SWAP_VALUE(a, b) { \
            typeof(a) _t = a; \
            a =  b; \
            b = _t; \ 
        }

一般宣称带有参数列表宏定义的时候,尽管函数体字符串太长,平常都会使用换行符来增强函数的可读性。

在宏注明中得以行使typeof来引用宏参数的品类。这些示例中typeof会取得参数a的类型,并用这一个类型定义中间变量_t来交流a和b的值。因而,该宏能够交换所有中央数据类型的变量(整数,字符,结构等)

2) 字符串化

单个#号的功力是字符串化,简单的话就是在输入值上丰裕""双引号,使其更换为C字符串。假设是在ObjC环境下,则可在头顶再增长@标志,出来后即是一个NSString类型。

#define STRINGIZE_(x)  #x
#define STRINGIZE2(x)  STRINGIZE_(x)
#define OCNSSTRING(x) @STRINGIZE2(x)

俺们辅导实际值,比如OCNSSTRING(3),一步步进展看一下:

OCNSSTRING(3) => @STRINGIZE2(3) => @STRINGIZE_(3) => @"3" // NSString @"3"
STRINGIZE2(3) =>  STRINGIZE_(3) // 这里多加一层是为了处理宏展开的问题,见后文介绍
STRINGIZE_(3) =>  #3 => "3" // C字符串"3"

字符串化对空格的处理有二种境况:
a). 忽略传入参数名前边和后边的空格 e.g. STRINGIZE_( abc ) => "abc"
b)当传入参数之间存在空格时,忽略其中多于一个的空格 e.g.
STRINGIZE_(abc /*多个空格*/ def) => "abc def"

3) 连接运算

连日来符号##用来将左右两项联合成一个完全。它的实施分为两步,先是分隔,然后它会将统一项之间的空格去除后形成连接操作。

#define metamacro_concat(A, B) A ## B
NSString *str      = @"This is Ryan!";
NSLog(@"%@", metamacro_concat(st, r));  // This is Ryan!

metamacro_concat的职能是将str这两项合并成完全str,而str是字符串@"This is Ryan!"的目的,所以最终会打印出来该字符串内容。刚刚那么些示例仅仅是出现说法了连接本条动作,这上边所说的分隔是何等看头或者说是什么境况呢?大家直接来看上边这多少个示例

#define A1(name, type) type name_##type##_type 
#define A2(name, type) type name##_##type##_type

引导实参A1(a1, int)A2(a1, int),相信你看来这两个宏定义后,会觉得没关系特其余,精通了##的意思,再逐个替换之后(type替换成intname替换成a1##左右的项连成全部),很快就能查获答案:

A1(a1, int) => int a1_int_int; // 然而并不对!!!
A2(a1, int) => int a1_int_int; // 然而并不对!!!

毋庸置疑,参照前边注释,可是并不对!现在该是探讨##相隔操作的时候了,大家先来发布下正确答案,当然我指出你可以找个编译器实际试一下,看看结果到底是何许。

A1(a1, int) => int name_int_type; // bingo!
A2(a1, int) => int a1_int_type;   // bingo!

观望答案是不是很奇怪,为啥有些替换了,有的没替换,name不应有都替换成a1type不应该都替换成int啊?好了,我们来揭开谜底吧:

预处理器在解析宏的时候会先做分隔操作,就是把##的上下项分隔开。

  1. A1name_##type##_type会被分隔成name_type_type这3段,显然name
    != name_;type !=
    _type,所以首先段name_和第三段_type不会被宏替换,中间段type则被替换成int,按这么些规则带入后就足以赢得最后的结果为int name_int_type
  2. A2name##_##type##_type会被分隔成name_type_type这4段,现在就一目精通了,_type不会被互换,因而带入参数后拿走终极结果为int a1_int_type

相隔的效益类似于空格。在宏定义中,预处理器一般把空格解释成分段标志,对于每一段和后边相比,相同的就被沟通。而##则会把前后项之间的空格都剔除,然后再做连接操作。所以A1A2的定义也可正如:

#define A1(name, type)  type name_     ##    type     ##     _t1ype
#define A2(name, type)  type name   ##   _   ##   type   ##  _t1ype

常见的演算符比如+,-, *, /,
++以及宏定义操作符#...也是相隔标志,e.g.
define add(a, b) a+bdefine add(a, b) a + b结果是千篇一律的,+会把ab中间的空格去掉后再去做相应运算

4) 可变多参数

标识符号...用来标识该宏可以收起可变的参数数量(零个或两个标志)。在宏体中,使用__VA_ARGS__来代表这么些输入的其实参数,也就是__VA_ARGS__在预处理师长为实在的参数集所替换。需要留意的是...不得不放在最终,代替最终面的宏参数。

...__VA_ARGS__配对类似,也得以应用NAME...NAME来配对采纳表示可变多参数。不同于前者,这里的NAME是您轻易的参数名,并不是系统保留名:
e.g. format...format

俺们来看一个贯彻对ObjC的NSLog打印信息补充和界定只在DEBUG环境下输出的可变参数宏定义示例:

#ifdef  DEBUG
#define NSLog(format, ...) NSLog((@"%s [Line %d] " format), __func__, __LINE__, ##__VA_ARGS__);
#else
#define NSLog(format, ...)
#endif

在这一个宏定义中,假设是非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)

深信您应当发现了,其实h(a)在此地担任的就是当中转换宏的角色,目标就是为了让f(1,2)先在h(a)里头被举办,防止放置g(a)内部遇到#而一筹莫展被交换。好了,精晓了宏定义的开展规则,大家再留个小作业给大家:

#define VALUE            2 
#define STRINGIZES_(s)   #s 
#define COMBINATION(a,b) int(a##e##b) 

printf("int max: %s\n", STRINGIZES_(INT_MAX)); // => ?
printf("%s\n", COMBINATION(VALUE, VALUE));     // => ?           

有道是怎么添加转换宏才能分别打印出int max: 0x7fffffff200? P.S.
INT_MAX的十六进制为0x7fffffff; 200则等于2e2,
e为指数表明式,表示2乘以102次方。

宏实例分析

有了下面的介绍,我们得以选一些对峙复杂的宏定义来分析了,这边我们依然选用ReactiveCocoa里面的六个宏。我们只要有趣味,仍旧强烈推荐去GitHub下载这么些库查看下,里面有好多令人叹为观止的宏定义。

1) 总计参数个数

下边这多少个宏metamacro_argcount(...)用来总结在可变参数的情况下,传入的实参数量。e.g.
int num = metamacro_argcount(a, b, c);等价于int num = 3;
作者提到灵感是根源于P99.
这里为了便于分析,我们把帮忙最多参数数量的精打细算改成10个且做了多少简化

#define metamacro_argcount(...) metamacro_at(10, __VA_ARGS__,10, 9, 8, 7, 6, 5, 4, 3, 2, 1)
#define metamacro_at(N,...)     metamacro_concat_at##N(__VA_ARGS__)

#define metamacro_concat_at10(_0,_1,_2,_3,_4,_5,_6,_7,_8,_9,...) metamacro_head(__VA_ARGS__)

#define metamacro_head(...)             metamacro_head_first(__VA_ARGS__,0)
#define metamacro_head_first(first,...) first

看起来是不是深感很复杂?没关系,大家一步步来,逐层带入参数来分析。假若我们传入5个参数metamacro_argcount(a,b,c,d,e)

STEP 1: 带入metamacro_argcount
metamacro_argcount(a,b,c,d,e) => metamacro_at(10, a,b,c,d,e,10,9,8,7,6,5,4,3,2,1)

这里的__VA_ARGS__替换成前面传入的可变实参a,b,c,d,e

STEP 2: 带入metamacro_at
metamacro_at(10, a,b,c,d,e,10,9,8,7,6,5,4,3,2,1) => metamacro_concat_at10 (a,b,c,d,e,10,9,8,7,6,5,4,3,2,1)

首先个参数为N, 之后都定义为可变参数。故而N10, __VA_ARGS__
a, b, c, d, e, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1,
这一步既修改了参数又修改了主意名。

STEP 3: 带入metamacro_concat_at10
metamacro_concat_at10 (a,b,c,d,e,10,9,8,7,6,5,4,3,2,1) => metamacro_head(5,4,3,2,1)

此间把前边十个参数替换成_0,_1,_2,_3,_4,_5,_6,_7,_8,_9,
然后事后的参数,也就是5,4,3,2,1概念为可变参数,并作为实参传给宏metamacro_head.
前10个参数就被drop掉了,所以您绝不关心它是被替换成了_0还是___0,
可想而知它们不需要后续被运用了。

STEP 4: 带入metamacro_head
metamacro_head(5,4,3,2,1) => metamacro_head_first(5,4,3,2,1,0)

缘何在末端加个0啊?还记得前边说过的,可变参数的多寡得以为零的呢,在那一个场地下就改成metamacro_head_first()了,后边再用metamacro_head_first取第一个值就出错了,所以需要非常加个0,
那样可变参数为空的时候就成为metamacro_head_first(0),
再取第一个值就能够收获参数数量为0了。

STEP 5: 带入metamacro_head_first
metamacro_head_first(5,4,3,2,1,0) => 5 // 直接获取第一个值,其他的省略

是不是很cool很magic?
通过多少个宏定义的转换,我们就能随便的汲取传入的实参个数,而且这一个结果在预处理阶段就获得了,不必等到运行阶段再去总括。

2) 参数格式检查

ReactiveCocoa里面还有个要命迷你的宏keypath(...),
可以断定输入的门道参数是否合法,并且付诸代码提醒。比如输入keypath(self.path),
宏会作出判断path是否为selfproperty,
如果该path不存在,则交由警告,防止误写。而且以此宏扶助可变参数,还是可以够输入格式为keypath(self, path),
同样会对path做参数检查。是不是很神奇?让我们来揭秘外衣看看它的魔法来源。

#define keypath(...) \
        metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__))(keypath1(__VA_ARGS__))(keypath2(__VA_ARGS__))
#define keypath1(PATH) \
        (((void)(NO && ((void)PATH, NO)), strchr(# PATH, '.') + 1))
#define keypath2(OBJ, PATH) \
        (((void)(NO && ((void)OBJ.PATH, NO)), # PATH))
STEP 1: 带入metamacro_if_eq
metamacro_if_eq(1, metamacro_argcount(self,path))(keypath1(__VA_ARGS__))(keypath2(self,path))

这边的metamacro_argcount地点探讨过,是计量可变参数个数,所以metamacro_if_eq的效能就是判断参数个数,即使个数是1就执行后边的keypath1,
若不是1就执行keypath2, 我们来看看metamacro_if_eq的定义:

/**
 * If A is equal to B, the next argument list is expanded; otherwise, the
 * argument list after that is expanded. A and B must be numbers between zero
 * and twenty, inclusive. Additionally, B must be greater than or equal to A.
 *
 * @code

// expands to true
metamacro_if_eq(0, 0)(true)(false)

// expands to false
metamacro_if_eq(0, 1)(true)(false)

 * @endcode
 *
 * This is primarily useful when dealing with indexes and counts in
 * metaprogramming.
 */
#define metamacro_if_eq(A, B) \
        metamacro_concat(metamacro_if_eq, A)(B)

避免篇幅,这边就不开展太多了,只重点分析下keypath(...)宏的实现,至于metamacro_if_eq大家清楚它的意义就足以了,然则自己仍然提出我们去ReactiveCocoa查看下那一个宏的完整定义,并尝试分析下metamacro_if_eq的贯彻原理。我深信,通过本文的牵线再增长一步步的引导替换,应该不难精晓它的实现。

STEP 2: 带入keypath2
keypath2(self,path) (((void)(NO && ((void)self.path, NO)), # path))

以此宏全体是一个C语言的逗号表明式,我们来回顾下逗号表明式的格式: e.g.
int a = (b, c); 逗号表明式取后边的值,故而a将被赋值成c,
此时b在赋值运算中就被忽略了,没有被应用,所以编译器会付出警告,为了祛除这多少个warning我们需要在b面前加上(void)做个类别强转操作。

逗号表明式的前项和NO举行了与操作,这一个根本是为着让编译器忽略首个值,因为大家的确赋值的是表达式前面的值。预编译的时候看见了NO,
就会急速的跳过判断标准。我猜你看到这儿肯定会意外了,既然要不经意,这为啥还要用个逗号表达式呢,直接赋值不就好了?

此间根本是对传播的首先个参数OBJ和第二个正要输入的PATH做了.操作,这也正是为什么输入第二个参数时编辑器会提交正确的代码指示(只如果当做表达式的一有些,
Xcode自动会指示)。尽管传入的path不是self的特性,那么self.path就不是一个合法的表明式,所以本来编译就不会由此了。

STEP 3: 带入keypath1
keypath1(self.path) (((void)(NO && ((void)self.path, NO)), strchr(# self.path, '.') + 1))

keypath1经受1个参数,所以我们直接带走self.path.
宏的前半段和地点是千篇一律的,不同的是逗号表明式的后一段strchr(# self.path, '.') + 1,
函数strchar是C语言中的函数,用来搜寻某字符在字符串中首次面世的地方,这里用来在self.path(注意眼前加了#字符串化)中追寻.并发的地点,再添加1就是回去.后面path的地址了。也就是strchr('self.path', '.')归来的是一个C字符串,这多少个字符串从找到'self.path'中为'.'的字符开首以后,即'path'.

按部就班上边的分析,我们清楚keypath(...)是回去一个透过检查的合法途径。如若在ObjC环境下,大家需要的是一个NSString,
所以大家在调用那些宏的时候,再增长@符号就OK了, e.g.
@keypath(self.path) => @"self.path".

偶尔定义宏我们会有意抬高@标记,但不是为了转移NSString品种,也不是为了某种专门的功力,只是让调用看起来更原生一些。

我们来看下面这么些事例:

#define weakObj(obj) __weak typeof(obj) obj##Weak = obj;

在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;

宏知识补充

出于宏定义的精神只是文本替换,所以这些并不智能的轮换会在部分环境下发生不可预知的荒谬。幸运的是,大家的先辈们发现了这一个问题,并且提供了很好的解决方案,这也是我们下边要研讨的好多宏定义约定俗成的格式写法。

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 {
   ...
}

其一吸收分号的章程现在一度几乎成了专业写法。而且因为大部分的编译器都可以辨识do{}while(0)那种无用的轮回并开展优化,所以不会因为这种艺术导致运行效能上的出入。

2) 使用({…})语句

GNU
C里面有个({...})形式的赋值扩张。这种模式的口舌在挨家挨户执行之后,会将最终一回的表达式的赋值作为重临。

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

本条宏用来取输入参数中较小的值,并将该值作为重返值重返。这里就是接纳了({...})语句来兑现,函数体中可以做任意的逻辑处理和运算,但最终的再次来到值则是最后的表明式。所以在定义宏的时候,我们可以用({...})语句来定义有重返值的函数宏,这么些也是函数宏很普遍的写法,我们在实际项目中也得以小心参照使用。

最后简短提下宏和const怎么区别使用,一般的话定义常量字符串就用const,定义代码就用宏(可以参见iOS的API相关定义)。如果有其他不精通的,欢迎留言钻探。PS.
本文参考了重重长辈们的精彩小说,在文中以超链接的情势做了引用,感谢她们的享用,也冀望本文能给大家带来或多或少声援。