一、概述
- 宏定义:
C语言的预处理功能。作定义内容简单的替换,不作为计算,不也作为表达式。在C语言中作为预处理指令包括:宏定义、文件包含、条件编译。 - 条件编译:
其实就是将if…else…的设计思想引入到预处理功能中,给编译器使用的。条件编译时通过增加条件判断的限制,来通知编译器选择性的编译满足条件的代码段,从而减少程序对内存的消耗,同时也可以提高程序的效率。使用条件编译,可以实现:同一套代码,根据不同的编译条件进行编译,产生不同的目标代码文件,以实现大公司中平台和产品线开发的一种形式。 - 优点及好处:
合理的使用宏定义和条件编译,可以大大的优化程序、提高程序的质量、增加程序的可移植性。
二、宏定义
普通的宏定义其实就是我们理解的宏常量。宏定义又称为宏替换,简称“宏”。其定义格式如下:
#define 标识符 字符串 //没有“;”这个注意。
其中这个标识符,就是我们的宏常量,也就字符常量,在宏定义中称之为宏名。预处理的工作就是替换,就是将宏名替换成相对于的字符串。
对于宏的关键就是替换,所以宏定义的一切都是以换为前提的,做任何的事情之前都要先替换。这个工作在预编译完成。
例如:
#define NAME “snake”
#define MAX_NUM 10
在程序中就是把NAME全部替换为“snake”, 把MAX_NUM替换为10.主要只是替换,不增加任何的代码。
对于宏定义一般需要遵循以下几点要求,以增加程序的可读性。
- ⑴ 宏名一般使用全大写字母;
- ⑵ 使用宏可以提高程序的通用性和可移植性;
- ⑶ 宏定义末尾不增加分号;
- ⑷ 宏定义的作用域是宏定义之后一直到文件结束;
- ⑸ 所以一般全局的宏都会在文件开头定义,或者在.h文件中定义,通过文件包含就可以实现多个文件的使用;
- ⑹ 对于函数内部的宏定义,可以使用#undef命令来限定宏定义的作用域;
- ⑺ 宏定义允许嵌套;
- ⑻ 字符串中不能包含宏,即使存在宏,也会将宏名单做字符串处理;
- ⑼ 宏定义不分配内存空间,变量的定义需要分配内存空间。
三、宏定义函数
宏函数,其实还是宏,只是这一类宏带有参数,我们可以看做是宏函数,从本质上说,他还是宏,在预处理的时候,还是简单的替换。
宏函数的替换,除了一般的字符串替换之外,还要做参数的替换。其格式如下:
#define 标识符(参数列表) 表达式
例如:
#define ADD(a,b) a+b
在使用的过程中,其实就可以看做是一个函数的。宏定义的参数列表就可以看做是宏函数的参数。宏函数和普通的函数有以下不同:
- ⑴ 宏函数的调用不需要要用堆空间。因为宏函数不是真正的函数,其也是直接替换,所以本质上不是函数的调用,故不需要消耗程序调用的堆空间;
- ⑵ 函数的调用是在程序运行期间进行的,存在内存的分配。宏函数是预编译时进行的,不需要分配内存;
- ⑶ 宏函数中的函数没有类型限定;
- ⑷ 宏函数的本质是替换,展开会使源程序变长,函数的调用不存在此问题;
- ⑸ 宏展开不占用运行时间,但是会消耗编译时间;
- ⑹ 函数存在递归调用,但是宏函数不允许存在这样的做法;
- ⑺ 宏函数在预编译的时候被处理,编译器是不知道宏函数的存在;
- ⑻ 宏函数用“实参”完全替代“形参”,不需要进行任何的运算。
对宏函数的定义需要特别注意:
- ⑴ 需要注意运算符的优先级。比如:
#define ADD(a,b) a+b
按照宏的展开,运行的结果就不是我们所期望的。所以建议在定义宏的时候,需要考虑到运算的优先级,增加()来限制优先级。
将刚刚的宏改成:
#define ADD(a,b) ((a)+(b))
这样就能保证运行结果的正确性。
- ⑵ 另外可以利用接续符“\”来实现宏函数更好的可读性
#define ADD(a,b)\
{ \
((a) + (b)) \
}
- 这样的书写形式,会使程序员看起来更加清晰。
⚠️注意:在宏定义中,如果要换行,使用"\"符号。 预处理时会认为"\"后面的还是在同一行。
对于正确书写宏定义,需要注意以下几点:
- ⑴ 宏名和参数的括号之间不能存在空格;
- ⑵ 宏定义只是在编译期间做替换,不做计算,也不作为表达式;
- ⑶ 函数的调用是程序运行期间,需要占用系统资源和内存空间;宏替换在预处理阶段,不占用系统的任何资源;
- ⑷ 宏函数的函数,没有任何的类型限定,也不存在类型转换,所以在使用的时候需要注意输入参数的含义;
- ⑸ 宏函数的展开会使程序变长,函数的调用不会;
- ⑹ 宏定义的展开需要消耗编译的时间;但是函数调用会消耗运行的时间和空间。
四、内置宏
在C中存在部分内置的宏定义
内置宏名 | 含义 |
_FILE_ | 被编译函数的名字 |
_LINE_ | 当前行号 |
_DATE_ | 编译时的日期 |
_TIME_ | 编译时的时间 |
_STDC_ | 编译器是否遵循标准C的规范 |
程序员在程序设计的时候可以利用这些内部的宏来输出相关的信息,以便对错误信息进行快速准确的定位。
另外程序员还可以查看各个包含的头文件来查看头文件包含的宏定义,在应用程序中是可以直接使用的。合理的利用内部的宏定义可以减少代码量。
五、高级使用方法
- ⑴ 宏定义中的“#”运算符
“#”运算符的作用是在预编译期间将宏参数转换为字符串
例如:
#define STRING(s) #s
STRING(snake_lp) 就等价于 “snake_lp”
下面代码:
#include<stdio.h>
#define CALL(f,p) (printf("Call function %s\n", #f), f(p))
int square(intn){
return n * n;
}
int f(int x){
return x;
}
int main(void){
printf("1. %d\n", CALL(square,4));
printf("2. %d\n", CALL(f, 10));
return 0;
}
- ⑵ 宏定义中的“##”运算符
##运算符用于在预编译期间粘连两个字符串
例如:
#define STRING(s) name_##s
❗️ STRING(number1) 就等价于 name_number1
此运算符真正的妙用是用来定义结构体,请欣赏下面代码:
#include <stdio.h>
#define STRUCT(type) typedef struct_tag_##type type;\
struct _tag_##type
STRUCT(Student){
char* name;
int id;
};
int main(){
Student s1;
Student s2;
s1.name = "s1";
s1.id = 0;
s2.name = "s2";
s2.id = 1;
printf("%s\n", s1.name);
printf("%d\n", s1.id);
printf("%s\n", s2.name);
printf("%d\n", s2.id);
return 0;
}
在体会以上两段代码之后,你会发现巧妙使用这两种运算符的好处。但是在不是特别了解此运算符的情况下,看这两段代码会摸不着头脑的。
- (3) \ 行继续操作符
当定义的宏不能用一行表达完整时,可以用"\"表示下一行继续此宏的定义。
已下面代码为例:
#define DEFINE_SINGLETON_FOR_CLASS(className) \
\
+ (className *)sharedManager { \
static className *shared##className = nil; \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
@synchronized(self){ \
shared##className = [[self alloc] init]; \
} \
}); \
return shared##className; \
}
六,特殊功能宏
1. NS_ASSUME_NONNULL_BEGIN && NS_ASSUME_NONNULL_END
在Swift
中存在Option
类型,也就是使用?和!声明的变量。但是OC里面没有这个特征,因为在XCODE6.3之后出现新的关键词定义用于OC转SWIFT时候可以区分到底是什么类型
__nullable
&&___nonnull
__nullable
指代对象可以为NULL或者为NIL__nonnull
指代对象不能为null
当我们不遵循这一规则时,编译器就会给出警告。
我们来看看以下的实例,
@interface TestNullabilityClass ()
@property (nonatomic, copy) NSArray * items;
- (id)itemWithName:(NSString * __nonnull)name;
@end
@implementation TestNullabilityClass
...
- (void)testNullability {
[self itemWithName:nil]; // 编译器警告:Null passed to a callee that requires a non-null argument
}
- (id)itemWithName:(NSString * __nonnull)name {
return nil;
}
@end
nullable &&
nonnull
事实上,在任何可以使用const关键字的地方都可以使用__nullable和__nonnull,不过这两个关键字仅限于使用在指针类型上。而在方法的声明中,我们还可以使用不带下划线的nullable和nonnull,如下所示:
- (nullable id)itemWithName:(NSString * nonnull)name
在属性声明中,也增加了两个相应的特性,因此上例中的items属性可以如下声明:
@property (nonatomic, copy, nonnull) NSArray * items;
当然也可以用以下这种方式:
@property (nonatomic, copy) NSArray * __nonnull items;
推荐使用nonnul
l这种方式,这样可以让属性声明看起来更清晰。
Non null
区域设置(Audited Regions)
如果需要每个属性或每个方法都去指定nonnull
和nullable
,是一件非常繁琐的事。苹果为了减轻我们的工作量,专门提供了两个宏:NS_ASSUME_NONNULL_BEGIN
和NS_ASSUME_NONNULL_END
。在这两个宏之间的代码,所有简单指针对象都被假定为nonnull,因此我们只需要去指定那些nullable的指针。如下代码所示:
NS_ASSUME_NONNULL_BEGIN
@interface TestNullabilityClass ()
@property (nonatomic, copy) NSArray * items;
- (id)itemWithName:(nullable NSString *)name;
@end
NS_ASSUME_NONNULL_END
在上面的代码中,items属性默认是non null
的,itemWithName:方法的返回值也是non null
,而参数是指定为nullable
的。
2.NS_ENUM_AVAILABLE_IOS
从单词的字面可以看出使用这个宏说明这个枚举开始IOS的版本
IOS版本如下 7_0 代表7.0的版本.用_替换
参数只有一个NS_ENUM_AVAILABLE_IOS(2_0) 代表>=2.0开始
3.NS_ENUM_DEPRECATED_IOS
* 代表枚举类型已经过时的API 第一个参数是开始的时候,第二个参数是过时的时候
NS_ENUM_DEPRECATED_IOS(2_0,7_0) 代表开始于IOS2.0废弃于IOS7.0 也就是>=2.0 <=7.0
typedef NS_ENUM(NSInteger, UIStatusBarStyle) {
UIStatusBarStyleDefault = 0, // Dark content, for use on light backgrounds
UIStatusBarStyleLightContent NS_ENUM_AVAILABLE_IOS(7_0) = 1, // Light content, for use on dark backgrounds
UIStatusBarStyleBlackTranslucent NS_ENUM_DEPRECATED_IOS(2_0, 7_0, "Use UIStatusBarStyleLightContent") = 1,
UIStatusBarStyleBlackOpaque NS_ENUM_DEPRECATED_IOS(2_0, 7_0, "Use UIStatusBarStyleLightContent") = 2,
} __TVOS_PROHIBITED;
4.UIKIT_EXTERN
extern
这个是定义字符串 变量 比#define
更加的高效 .但是UIKIT_EXTERN是根据是否是C语言宏定义,根据语言区分 ,比extern更加的高效
例子:
UIKIT_EXTERN NSString *const UIApplicationInvalidInterfaceOrientationException NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;
⚠️ 注意:上面的代码一般定义在.H 在.M实现 实现要去掉UIKIT_EXTERN.代表IOS6.0之后可以用,在TVOS系统不可用。
5.NS_CLASS_AVAILABLE_IOS
代表类开始的API 和上面说的类似
例子
NS_CLASS_AVAILABLE_IOS(2_0) @interface UIApplication : UIResponder
6. NS_EXTENSION_UNAVAILABLE_IOS
标记IOS插件不能使用这些API,后面有一个参数,可以作为提示,用什么API替换
例子
+ (UIApplication *)sharedApplication NS_EXTENSION_UNAVAILABLE_IOS("Use view controller based solutions where appropriate instead.");
7. __kindof
__kindof 这修饰符还是很实用的,解决了一个长期以来的小痛点,拿原来的 UITableView 的这个方法来说:
- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier;
使用时前面基本会使用 UITableViewCell 子类型的指针来接收返回值,所以这个 API 为了让开发者不必每次都蛋疼的写显式强转,把返回值定义成了 id 类型,而这个 API 实际上的意思是返回一个 UITableViewCell 或 UITableViewCell 子类的实例,于是新的 __kindof 关键字解决了这个问题:
- (__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier;
既明确表明了返回值,又让使用者不必写强转。
再举个带泛型的例子,UIView 的 subviews 属性被修改成了:
@property (nonatomic, readonly, copy) NSArray<__kindof UIView *> *subviews;
这样,写下面的代码时就没有任何警告了:
UIButton *button = view.subviews.lastObject;
8. __has_include
功能是检测到某个文件,是否在工程中被包含.
#if __has_include(<YYCache/YYCache.h>)
FOUNDATION_EXPORT double YYCacheVersionNumber;
FOUNDATION_EXPORT const unsigned char YYCacheVersionString[];
#import <YYCache/YYMemoryCache.h>
#import <YYCache/YYDiskCache.h>
#import <YYCache/YYKVStorage.h>
#elif __has_include(<YYWebImage/YYCache.h>)
#import <YYWebImage/YYMemoryCache.h>
#import <YYWebImage/YYDiskCache.h>
#import <YYWebImage/YYKVStorage.h>
#else
#import "YYMemoryCache.h"
#import "YYDiskCache.h"
#import "YYKVStorage.h"
#endif
9. UNAVAILABLE_ATTRIBUTE
告知方法失效
- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;
10.NS_DESIGNATED_INITIALIZER
定义初始化方法
- 为什么要用NS_DESIGNATED_INITIALIZER
Objective-C 中主要通过NS_DESIGNATED_INITIALIZER宏来实现指定构造器的。这里之所以要用这个宏,往往是想告诉调用者要用这个方法去初始化(构造)类对象。 - 怎样避免使用NS_DESIGNATED_INITIALIZER产生的警告
如果子类指定了新的初始化器,那么在这个初始化器内部必须调用父类的Designated Initializer。并且需要重写父类的Designated Initializer,将其指向子类新的初始化器。
如下:
// .h
- (instancetype)initWithName:(NSString *)name NS_DESIGNATED_INITIALIZER;
// .m
- (instancetype)init {
return [self initWithName:@""];
}
- (instancetype)initWithName:(NSString *)name {
self = [super init];
if (self) {
// do something
}
return self;
}
- 更好的做法
如果定义NS_DESIGNATED_INITIALIZER,大多是不想让调用者调用父类的初始化函数,只希望通过该类指定的初始化进行初始化,这时候就可以用NS_UNAVAILABLE宏。
如下:
.h
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithName:(NSString *)name NS_DESIGNATED_INITIALIZER;
如果调用者使用init 初始化,编译器就会给出一个编译错误。使用NS_UNAVAILABLE后,就不需要在.m中重写父类初始化函数了。如果要允许调用者使用init就需要在.m中重写父类的初始化函数,如上提到的,否则就会报警告。
- 避免使用new
如果使用new来创建对象的话,即使init被声明为NS_UNAVAILABLE,也不会收到编译器的警告和错误提示了。
七、条件编译
在C语言中,如果需要对程序中的一些代码段进行选择性的编译,就需要用到条件编译的命令,条件编译的格式有以下几种:
- ⑴ #if…#else…格式
#if 判断条件
代码段1
#else
代码段2
#endif
或者
#if 判断条件1
代码段1
#elif 判断条件2
代码段2
#else
代码段3
#endif
功能:和if…else…表达式是一样的。适用的场景是存在真假的判断条件,此条件一般情况下是一个表达式。
- ⑵ #ifdef…#else…或者#ifndef…#else…格式
#ifdef 标识符
代码段1
#else
代码段2
#endif
或者
#ifndef 标识符
代码段1
#else
代码段2
#endif
功能:判断条件主要是查看标识符是否被定义(#define定义)。
总结:
在现实的工程项目中会使用大量的条件编译。比如说通过条件编译来使用各个不同的硬件平台;通过条件编译来实现平台和产品线管理;通过条件编译来区分正式版本和调试版本等等。
条件编译的本质是选择性的编译,其意义在于:
- ⑴ 增加代码的兼容性,一套代码兼容多个硬件平台或者软件平台;
- ⑵ 区分产品的调试版本和正式发布版本;
- ⑶ 不同的产品线共用代码,使用条件编译来产生适用不同产品的目标文件;
- ⑷ 同时也为程序员提供了一种屏蔽代码块的方式 #if 0….#endif。
理论上来说,条件编译是在预编译的时候生效的,但是我们不要就认为编译好了之后,条件编译就是不起作用了。其实对于第一种形式的条件编译在程序运行中也是有效的。即如果在运行中通过某些触发条件来修改条件编译判断条件的运算结果,也是可以完成实际执行代码段的切换。其实这样的做法在很多的产品中运用,即通过某些设置开关来开启和关闭一些功能。