你应该掌握的#define宏定义

如果你并不是一个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宏定义_第1张图片
1.png

干的不错,再来一个取两者更小的一个数的宏

#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:

你应该掌握的#define宏定义_第2张图片
2.png

宏的应用

温故了宏的知识后,我们来看看大牛们是怎么高效的使用宏吧。

苹果官方提供的好用的宏

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请点击这里

文章如果有不足之处,还望指教!

参考资料:

GCC_MACROS
宏定义的黑魔法

你可能感兴趣的:(你应该掌握的#define宏定义)