iOS开发:从零基础到精通
上QQ阅读APP看书,第一时间看更新

6.3 预编译指令

6.3.1 宏定义

在对源代码的编译过程中,需要一些机制来完成一些功能,如在编译时包含其他源文件、定义宏、根据条件决定编译时是否包含某些代码。要完成这些工作,就需要使用预处理程序。尽管目前绝大多数编译器都包含了预处理程序,但通常认为它们是独立于编译器的。预处理过程通过读入源代码,检查包含预处理指令的语句和宏定义,对源代码进行相应的转换,并产生新的源代码提供给编译器。

预处理指令是以#号开头的代码行。#号必须是该行除了任何空白字符外的第一个字符。#号后是指令关键字,在关键字和#号之间允许存在任意个数的空白字符。整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。预处理过程先于编译器对源代码进行处理,还会删除程序中的注释和多余的空白字符。

宏(Macros),定义了一个代表特定内容的标识符。预处理过程会把源代码中出现的宏标识符替换成宏定义时的值。宏最常见的用法是定义代表某个值的全局符号。宏的第二种用法是定义带参数的宏,这样的宏可以像函数一样被调用,但它是在调用语句处展开宏,并用调用时的实际参数来代替定义中的形式参数。#define预处理指令是用来定义宏的,宏的作用范围是从宏定义的那一行开始,直到文件尾。

1.无参宏

无参数的宏定义,简称为无参宏,其定义格式如下所示。

作为一种约定,习惯上总是全部用大写字母来定义宏,这样易于把程序宏的宏标识符和一般变量标识符区别开来。另一种常见的宏标识符习惯以k开头,如#define kLength 20。宏定义结尾不能使用分号,因为宏是一种替换机制,是用宏体部分所有的字符串替换宏名,如果加了分号,会将分号也替换进去。下面是一些常见的无参宏的定义方法。

  • 定义符号常量。用PI这个符号代表常量3.14,在后面代码中要用到3.14的时候都可以用PI代替。
  • 定义表达式。用LARGE代表(100+100),宏体中常量多于一个时需要用一对()括起来。
  • 定义字符串常量,用WEBNAME代表了一个字符串。
  • 定义符号。用AND代表了&&符号。
  • 在宏定义中,可以使用另一个宏定义的值。

2.有参宏

除了无参宏之外,还可以定义有参数的宏,当使用有参宏时,需要传入必要的参数参与运算。有参宏的定义格式为:

例如,可以定义如下一些有参宏。

  • 定义一个又一个参数的宏,求参数的平方值。
  • 使用续行符定义多行的宏。\被称为续行符,表示下一行是本行的延续。在\符号所在行之后不能加任何空白字符。

3.有参宏使用注意事项

宏体中的参数要加上括号,因为宏体中传入的参数可以是一个表达式,如果不将参数加括号,那么遇到混合运算时可能会出现错误,例如下面的求平方的例子。

若参数不加括号则计算的值会变成如下的情况:(3+4*3+4)。这样的情况不是程序中想要的,所以在使用有参数的宏时注意给参数加括号。

4.运算符“#”

出现在宏定义中的#运算符把跟在其后的参数转换成一个字符串。有时把这种用法的#称为字符串化运算符。

运行结果如图6-15所示。

图6-15 运行结果

5.运算符“##”

##运算符用于把多个参数连接到一起。预处理程序把出现在##两侧的参数合并成一个符号。很少有程序员会知道##运算符,绝大多数程序员从来没用过它。

运行结果如图6-16所示。

图6-16 运行结果

6.3.2 #include、# import与@class

1.#include

#include预处理指令的作用是在指令处展开被包含的文件,包含可以是多重的,也就是说一个被包含的文件中还可以包含其他文件。标准C编译器至少支持八重嵌套包含,但是在一个文件中写两个一样的#include "类名.h"会报错,编译器会认为对同一个文件重复的引用了。

在程序中包含头文件有两种格式:

第一种方法是用尖括号把头文件括起来,这种格式告诉预处理程序在编译器自带的或外部库的头文件中搜索被包含的头文件。第二种方法是用双引号把头文件括起来,这种格式告诉预处理程序在当前被编译的应用程序的源代码文件中搜索被包含的头文件,如果找不到,再搜索编译器自带的头文件。

采用两种不同包含格式的理由在于编译器是安装在公共子目录下的,而被编译的应用程序是在其私有子目录下的。一个应用程序既包含编译器提供的公共头文件,也包含自定义的私有头文件。采用两种不同的包含格式使得编译器能够在很多头文件中区别出一组公共的头文件。

2.#import

#import大部分功能和#include是一样的,但是其解决了重复引用的问题,在引用文件的时候不用再去自己处理重复引用的错误了。

3.@class

使用@class指令,也可以引入一个类,在声明中与#import的功能类似。但使用@class指令提高了效率,因为编译器不需要引入和处理整个引入的类。如果需要用到引入类中的属性和方法,使用@class是不够的,必须要使用#import。

如下面例子所示,创建一个新的类ClassC,分别用#import和@class引入两个其他的类ClassA和ClassB。在ClassC.h声明文件中,添加两个属性classA和classB,此时用#import和@class没有区别。

在ClassC.m文件中,新增一个方法print,在该方法中,打印classA和classB对象中的属性,就可以看到用#import和@class的区别。使用@class是不可以引用类中定义的属性和方法的,而使用#import则可以。

报错提示信息如图6-17所示。

图6-17 报错提示

6.3.3 条件编译

条件编译指令决定哪些代码将被编译,而哪些是不被编译的。根据表达式的值或者某个特定的宏是否被定义来确定编译条件。条件编译在实际的开发中使用还是比较普遍的,例如针对NSLog()函数,可以定义只有在版本调试debug状态时才打印日志,在版本发布release状态时不打印日志,从而提升应用的执行效率。

1.#if、#elif、#else

  • #if指令检测跟在关键字后的宏或者常量表达式的值,如果值为真,则编译后面的代码,直到出现#else#elif#endif为止,反之则不执行。
  • #elif预处理指令综合了#else和#if指令的作用,类似于else if。
  • #else指令用于某个#if指令之后,当前面的#if指令的条件不为真时,就编译#else后面的代码。

下方的示例代码中,如果iOS 10值为真时,输出“这是一个运行iOS 10的设备!”;若iOS 10值为假且iOS 9值为真时,输出“这是一个运行iOS 9的设备!”;否则输出“这个设备既不运行iOS 10,也不运行iOS 9!”

运行结果如图6-18所示。大家可以自行修改宏定义中iOS 10和iOS 9的值,看一下运行结果的变化情况。

图6-18 运行结果

2.#ifdef与#ifndef

#ifdef等价于#if defined,如果后面跟的宏被定义过,则执行下面的代码。

运行结果如图6-19所示。

图6-19 运行结果

#ifndef和#ifdef相反,如果后面跟的宏没有被定义过,则执行下面的代码。

运行结果如图6-20所示。

图6-20 运行结果