宏定义在C类语言中非常重要,因为宏是一种预编译时的功能,因此其可以比运行时更高层面的对程序流程进行控制。在初学宏定义的时候,大家可能都会有这样一种感觉:就是完全替换么,太简单了。但如果你真这么想,那你就太天真了,不说自己编写宏,在Foundation框架中内置定义的许多宏要看明白也要费一番脑筋。本篇博客,总结了前辈的经验,同时收集了一些编写非常巧妙的宏进行分析,希望可以帮助大家对宏定义有更加深刻的理解,并且可以将心得应用于实际开发中。
一、准备
宏的本质是预编译时的替换,在开始正文之前,我们需要先介绍一种观察宏替换后结果的方法,这样帮助我们更方便的对宏最终的结果进行验证与测试。Xcode开发工具自带查看预编译结果的功能,首先需要对工程编译一遍,之后选择工具栏中的Assistant选项,打开助手窗口,如下图所示:
之后选择窗口的Preprocess选项,即可打开预编译结果窗口,可以看到,宏被替换后的最终结果,如下图所示:
后面,我们将使用这种方式来对编写的宏进行验证。
二、关于“宏定义”
宏使用#define来进行定义,宏定义分为两种,一种是对象式宏,一种是函数式宏。对象式宏通常对来定义量值,在预编译时,直接将宏名替换成对应的量值,函数式宏在定义时可以设置参数,其作用与函数很类似。
例如,我们可以将π的值定义成一个对象式宏,在使用的时候,用有意义的宏名要比直接使用π的字面值方便很多,例如:
#import <Foundation/Foundation.h>
#define PI 3.1415926
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
CGFloat res = PI * 3;
NSLog(@"%f", res);
}
return 0;
}
函数式宏要更加灵活一些,例如对圆面积计算的方法,我们就可以将其定义成一个宏:
#define PI 3.1415926
#define CircleArea(r) PI * r * r
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
CGFloat res = CircleArea(1);
NSLog(@"%f", res);
}
return 0;
}
现在,有了这个面积计算宏我们可以更加方便的计算圆的面积了,看上去很完美,后面我们就使用这个函数式宏为例,来深入理解宏的原理。
三、从一个简单的函数式宏说起
再来看下上面我们编写的计算面积的宏,正常情况下好像没什么问题,但是需要注意,归根结底宏并不是函数,如果完全把其作为函数使用,我们就可能会陷入一系列的陷阱中,比如这样使用:
#define PI 3.1415926
#define CircleArea(r) PI * r * r
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
CGFloat res = CircleArea(1 + 1);
NSLog(@"%f", res);
}
return 0;
}
运行代码,运算的结果并不是半径为2个圆的面积,哪里出了问题呢,我们还是先看下宏预编译后的结果:
CGFloat res = 3.1415926 * 1 + 1 * 1 + 1;
一目了然了,由于运算符的优先级问题导致了运算顺序错误,在编程中,所有运算符优先级产生的问题都可以使用一种方式解决:用小括号。对CircleArea宏进行一下改造,如下:
#define CircleArea(r) (PI * (r) * (r))
对执行顺序进行了强制的控制,代码执行又恢复了正常,看上去好像是没有问题了,现在就满意了还为时过早,例如下面这样使用这个宏:
#import <Foundation/Foundation.h>
#define PI 3.1415926
#define CircleArea(r) PI * (r) * (r)
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
int r = 1;
CGFloat res = CircleArea(r++);
NSLog(@"%f, %d", res, r);
}
return 0;
}
运行,发现结果又错了,不仅计算结果与我们的预期不符,变量自加的的结果也不对了,我们检查其展开的结果:
CGFloat res = 3.1415926 * (r++) * (r++);
原来问题出在这里,宏在展开的时候,将参数替换了两次,由于参数本身是一个自加表达式,所以被自加了两次,产生了问题,那么这个问题怎么解决呢,C语言中有一种很有用的语法,即使用大括号定义代码块,代码块会将最后一条语句的执行结果返回,修改上面宏定义如下:
#import <Foundation/Foundation.h>
#define PI 3.1415926
#define CircleArea(r) \
({ \
typeof(r) _r = r; \
(PI * (_r) * (_r)); \
})
int main(int argc, const char * argv[]) {
@autoreleasepool {
int r = 1;
CGFloat res = CircleArea(r++);
NSLog(@"%f, %d", res, r);
}
return 0;
}
这次程序又恢复的了正常。但是,如果如果在调用宏是变量的名字与宏内的临时变量产生了重名,灾难就又发生了,例如:
#import <Foundation/Foundation.h>
#define PI 3.1415926
#define CircleArea(r) \
({ \
typeof(r) _r = r; \
(PI * (_r) * (_r)); \
})
int main(int argc, const char * argv[]) {
@autoreleasepool {
int _r = 1;
CGFloat res = CircleArea(_r);
NSLog(@"%f, %d", res, _r);
}
return 0;
}
运行上面代码,会发现宏内的临时变量没有被初始化成功。这确实难受,我们在进一步,比如对临时变量的名字做一些手脚,将其命名为极其不容易重复的名字,其实系统内置的一个宏就是专门用来构造唯一性变量名的:__COUNTER__,这个宏是一个计数器,在编译的时候会自动进行累加,再次对我们编写的宏进行改造,如下:
#import <Foundation/Foundation.h>
#define PI 3.1415926
#define PAST(A, B) A##B
#define CircleArea(r) __CircleArea(r, __COUNTER__)
#define __CircleArea(r, v) \
({ \
typeof(r) PAST(_r, v) = r; \
(PI * PAST(_r, v) * PAST(_r, v)); \
})
int main(int argc, const char * argv[]) {
@autoreleasepool {
int _r = 1;
CGFloat res = CircleArea(_r);
CGFloat res2 = CircleArea(_r);
NSLog(@"%f, %f", res, res2);
}
return 0;
}
这里改造后,我们的宏就没有那么容易理解了,首先__COUNTER__在每次宏替换时都会进行自增,##是一种宏中专用的特殊符号,用来将参数拼接到一起,但是需要注意,使用##符号拼接的如果是另外一个宏,则其会阻止宏的展开,因此我们定义了一个转换宏PAST(A, B)来处理拼接。如果你一下子不能理解为什么这样就可以解决宏展开的问题,你只需要记住这样一条宏展开的原则:如果形参有使用#或##这种处理符号,则不会进行宏参数的展开,否则先展开宏参数,在展开当前宏。上面代码最终预编译的结果如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int _r = 1;
CGFloat res = ({ typeof(_r) _r0 = _r; (3.1415926 * _r0 * _r0); });
CGFloat res2 = ({ typeof(_r) _r1 = _r; (3.1415926 * _r1 * _r1); });
NSLog(@"%f, %f", res, res2);
}
return 0;
}
一个简单的计算圆面积的宏,为了安全,我们就进行了这么多的处理,看来要用好宏,的确不容易。