如果你并不是一个new coder
,那你一定是知道#define
的。是的,它并不完美,而且被很多人所诟病。但是的的确确会让你的代码看起来看简洁,更方便阅读。下面让我们先来温习下宏的基础。
基础宏
定义宏标示 (Object-like Macros)
如果你想定义一个缓存区的大小,又不太想硬编码,你可能会选择下面的方式
#define BUFFER_SIZE 1024
如果你在#define标识符后面,使用了BUFFER_SIZE代码块开辟一块区间
foo = (char *) malloc (BUFFER_SIZE);
C预处理器将会识别BUFFER_SIZE并且把它替换成你上面写的1024:
foo = (char *) malloc (1024);
BUFFER_SIZE
=>1024
按照约定,宏定义需要以大写的形式呈现。这样让人瞟一眼就知道你写的个啥玩意。
定义宏函数 (Function-like Macros)
你也可以定义一个宏,它看起来像一个函数一样。比如:
#import
#define lang_init() c_init()
void c_init();
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello, World!");
lang_init();
}
return 0;
}
void c_init(){
printf("c_init method is called \n");
}
当然你也可以去定义一个A减去B
带参数的函数:
#define SUB(A,B) (A-B)
验证一下: SUB(2, SUB(1,3))
干的不错,再来一个取两者更小的一个数的宏
:
#define MIN(A,B) A
验证一下:
| 表达式 | 结果 | 是否正确 |错误原因|
| -------- | -----: | :----: ||
| MIN(4,5) | 4 | 是 ||
| MIN(-1,5) | -1 | 是 ||
| 2 * MIN(4,5) | 5 | 否 |2*4<5?4:5|
在做乘法的时候我们改变了原先的运算顺序,机智如你肯定很快的给出解决方案,如下:
#define MIN(A,B) (A
信心爆棚的你再次跑起了测试,仿佛一切都在你的掌握之中,直到遇到了那个它:
| 表达式 | 结果 | 是否正确 ||
| -------- | -----: | :----: ||
| MIN(3, 4 < 5 ? 4 : 5) | 4 | 否 ||
我们先来分解一下:
(3<4<5?4:5?3:4<5?4:5)
// =>(1<5?4:5?3:4<5?4:5)
// =>(1?4:5?3:1?4:5)
// => 4
我们不得不又改了解决方案:
#define MIN(A,B) ((A)<(B)?(A):(B))
说了这么多废话,只是想提醒大家能加上括号的就一定加括号。当然加了括号也不是万能了。比如:
float a = 1.0f;
float b = MIN(a++, 1.5f);
不再作展开操作了,更详细的可以去看喵大的宏定义的黑魔法
实在感觉到很无力。默默的打开XCODE
,看看苹果官方的标准写法:
#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
我们先来认识一下几个不太熟悉的大兄弟:
__COUNTER__
是一个由GCC提供用来构造独立变量名的标识符,在预编译过程中从0开始计数,每次被调用的时候+1).也就是连续调用两次MIN(A,B),两次的A在预编译中会以不同的变量名存在。
#import
#import "Header.h"
#define FUNC2(x,y) x##y
#define FUNC1(x,y) FUNC2(x,y)
#define FUNC(x) FUNC1(x,__COUNTER__)
int FUNC(my_unique_prefix);
int FUNC(my_unique_prefix);
int main(int argc, const char * argv[]) {
@autoreleasepool {
printf("__COUNTER__==%d \n",__COUNTER__);
printf("__COUNTER__==%d \n",__COUNTER__);
printf("因为预编译FUNC被调用了两次,所以__COUNTER__打印的值是从2开始");
}
return 0;
}
=>打印:
__COUNTER__==2
__COUNTER__==3
因为预编译FUNC被调用了两次,所以__COUNTER__打印的值是从2开始
__NSX_PASTE__
是预编译的连接符,宏定义中不能直接写 AB 来连接参数,需要写成 A##B。
__typeof__
是gcc对C语言的一个扩展保留字,用于声明变量类型,var可以是数据类型(int, char*..),也可以是变量表达式。
__NSMIN_IMPL__
现在多加了个__COUNTER__
的参数生成独立标示符。
认识了上面的大兄弟,我们现在自己来转换一下,应该就很好读了:
#define __NSMIN_IMPL__(A,B,L) ({\
__typeof__(A) __a##L = (A);\
__typeof__(B) __b##L = (B);\
(__a##L < __b##L) ? __a##L : __b##L;\
})
宏短语 (Stringification)
有时候你可能想把一个宏的参数转成一个字符串常量。可是你并没有办法直接把宏参数硬塞到字符串里面去,这个时候“#”宏预处理符会帮助到你。当一个宏参数前面加上“#”宏预处理符,你就可以成功的把宏参数转成字符串了。
来个例子,我们开发了一个APP,第一个版本的版本号可能是1.0咯
#define VERSION_MAJOR 1
#define VERSION_MINOR 0
#define STRINGSIZE2(s) #s
#define STRINGSIZE(s) STRINGSIZE2(s)
#define VERSION_STRING "v" STRINGSIZE(VERSION_MAJOR) \
"." STRINGSIZE(VERSION_MINOR)
=>打印一下
printf ("%s\n", VERSION_STRING);
v1.0
宏串联(Concatenation)
在上面的例子我们有提到过“##”宏串联符。假如现在你想整理你电脑里面的动漫,分为HYRZ和SQBB系列,后面加上数字代表集数。
#define SERIES1 HYRZ
#define SERIES2 SQBB
#define PPCAT_NX(A, B) A ## B
#define PPCAT(A, B) PPCAT_NX(A, B)
#define STRINGSIZE_NX(A) #A
#define STRINGSIZE(A) STRINGSIZE_NX(A)
=>打印一下
printf("%s \n",STRINGSIZE(PPCAT(SERIES1, 01)));
printf("%s \n",STRINGSIZE(PPCAT(SERIES2, 01)));
HYRZ01
SQBB01
可变宏(Variadic Macros)
宏可以和一个方法一样能接受一系列的参数。这面是个例子:
#define eprintf(...) fprintf (stderr, __VA_ARGS__)
这种宏就叫做variadic
。在宏定义(其实也包括函数定义)的时候,写为...
的参数被叫做可变参数(variadic)。可变参数的个数不做限定。当宏被使用下面的这些参数会替换这个宏里面的__VA_ARGS__
。所有,我们可以有下面的展开:
eprintf ("%s:%d: ", __FILE__, __LINE__)
==> fprintf (stderr, "%s:%d: ", __FILE__, __LINE__)
打印出当前的文件在你电脑上的路径,和该行代码在你当前文件的当前行。
再来个相加的加法的例子:
#define A(a1, a2, a3) ((a1)+(a2)+(a3))
#define ADD(...) A(__VA_ARGS__)
printf("结果为:%d\n", ADD(1, 2, 3));;
=>打印一下
结果为:6
可以看出你可以在A2
宏里面扩展成N个参数。这就证明了__VA_ARGS__
参数的个数不做限定。
标准的预定义宏(Standard Predefined Macros)
表达式 | 含义 |
---|---|
__FILE__ |
当前源文件的路径 |
__LINE__ |
当前代码的在源文件的行数 |
__DATE__ |
当前的日期 |
__TIME__ |
当前的时间 |
__FUNCTION__ |
当前的方法名 |
标准的预定义宏被相关语言标准所定义,所以他们适用于各种编译器。
表达式 | 含义 |
---|---|
__FILE__ |
当前源文件的路径 |
__LINE__ |
当前代码的在源文件的行数 |
__DATE__ |
当前的日期 |
__TIME__ |
当前的时间 |
__FUNCTION__ |
当前的方法名 |
#define log(x) printf(" THIS IS A LOG : x = %d \n LINE %d \n FILE = %s \n DATE = %s \n TIME = %s \n FUNCTION = %s\n ",x,__LINE__,__FILE__,__DATE__,__TIME__,__FUNCTION__)
=>打印一下
THIS IS A LOG : x = 3
LINE 86
FILE = /Users/wyy/Desktop/wyy/code/demo/DefineDemo/DefineDemo/main.m
DATE = Nov 7 2016
TIME = 15:10:51
FUNCTION = main
Program ended with exit code: 0
宏定义取消(Undefining and Redefining Macros)
如果一个宏不再有用,我们可以直接用#undef
来取消它
#define VERSION_NUM 1
int main(int argc, const char * argv[]) {
@autoreleasepool {
#ifdef VERSION_NUM //如果定义了 VERSION_NUM
printf("定义了VERSION_NUM == %d \n",VERSION_NUM);
#endif
#undef VERSION_NUM //取消VERSION_NUM的定义
#ifndef VERSION_NUM //如果没有定义VERSION_NUM
printf("undef取消了VERSION_NUM的定义 \n");
#endif
}
return 0;
}
所以,那些不用和程序生命周期相绑定的宏,在使用完后你应该取消掉。比如YY系列NSAttributedString+YYText.m
中的宏Fail
:
宏的应用
温故了宏的知识后,我们来看看大牛们是怎么高效的使用宏吧。
苹果官方提供的好用的宏
UIKIT_EXTERN //UIKIT_EXTERN是根据是否支持C++环境做的宏定义,大体类似于extern,使用需要引入
NS_REQUIRES_SUPER //对于一些可以被继承的类,需要子类在重某一调用父类的实现以保证正确的行为,通过在头文件方法的声明末尾添加。如果子类没有调用[super method],会有警告提示。
NS_AVAILABLE_IOS(_ios) //你可以规定你自己的方法在iOS版本号适用
NS_DEPRECATED //过期提醒
NS_DESIGNATED_INITIALIZER //Objective-C中主要通过NS_DESIGNATED_INITIALIZER宏来实现指定构造器的。
NSLocalizedString(key, comment) \
[[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:nil] //字符串的本地化
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-相关命令"
// your code
#pragma clang diagnostic pop
比如:
方法弃用告警
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
不兼容指针类型
#pragma clang diagnostic ignored "-Wincompatible-pointer-types"
循环引用
#pragma clang diagnostic ignored "-Warc-retain-cycles"
未使用变量
#pragma clang diagnostic ignored "-Wunused-variable"
第三方框架的应用
#define MJWeakSelf __weak typeof(self) weakSelf = self; //弱引用
// 运行时objc_msgSend 转函数指针
#define MJRefreshMsgSend(...) ((void (*)(void *, SEL, UIView *))objc_msgSend)(__VA_ARGS__)
#define MJRefreshMsgTarget(target) (__bridge void *)(target)
#ifndef kSystemVersion
#define kSystemVersion [UIDevice systemVersion] //版本号
#endif
#define YYAssertMainThread() NSAssert([NSThread isMainThread], @"This method must be called on the main thread") //利用断言实现方法必须在主线程调用
#define YYCAssertNil(condition, description, ...) NSCAssert(!(condition), (description), ##__VA_ARGS__) //利用断言保证对象nil,否则crash
#define YYCAssertNotNil(condition, description, ...) NSCAssert((condition), (description), ##__VA_ARGS__) //利用断言保证对象非nil,否则crash
#define dispatch_main_async_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
const
define
有个优势就是没有类型,所以就算你把下面的ANIMATION_DURATION
赋给int
编译器也不会产生警告。从另外一个角度来说,这也是宏的一个弊端。比如说:
当你写动画效果的时候需要设定一个动画展示的时间。所以代码可能是这样的:
#define ANIMATION_DURATION 0.25
很棒,当我们的动画是组合动画的时候我们也可以通过多次调用ANIMATION_DURATION
来解决硬编码的问题。但问题来了,虽然你的宏命名很规范,我可以大概猜出与时间的有关系,但还是无法确定准确的类型。所以我们有了更好的选择:
static const NSTimeInterval kAnimationDuration = 0.25;
#define
最大的优势是它并不需要在你的工程里面占用内存(比如上面的ANIMATION_DURATION
,但0.25
还是需要分配内存,放在常量区)。当你使用了这个定义的宏,它实际上只是用你的设定的那个值0.25
在编译的时候去替换。这就造成了,你使用了多少次这个宏,就需要进行多次替换,占用运行内存。上面讲到的__COUNTER__
就保证了每一次调用同样的宏,产生的立即数都不一样。
而const
定义的常量储存在数据段,只有一份copy,这样效率更高。
更多信息#define
VS const
请点击这里
文章如果有不足之处,还望指教!