该系列文章系个人读书笔记及总结性内容,任何组织和个人不得转载进行商业活动!
第5章 预处理器
OC语言中含有一个预处理器,用于在编译前处理源文件;
其中有些处理操作是自动执行的;
另一些处理操作是根据源文件中包含的预处理器语言元素执行的;
预处理器语言用法丰富:那预处理器有什么作用,以及如何使用呢这种语言呢?
5.1 概述
OC编译器编译源代码的一般流程:
接收源文件,转换为能够在目标平台上执行的文件,大致分为几个阶段;
1)输入源文件;
2)预处理器进行词法分析;
3)语法分析器进行语法分析;
4)经AST生成代码;
5)经过优化、汇编与链接;
6)最后输入可执行的二进制文件;
词法分析阶段:
源代码被拆分成多个记号;
每个记号(token)都是一个独立的语言元素,如关键字、操作符、标识符和在其语法环境中的符号名;
语法分析阶段:
会检查正确语法的记号,并检查他们所构成表达式的合法性;
用以通过记号创建抽象语法树(AST)或层次分析树;
生成代码和优化阶段:
AST用于生成输出语言代码,输出的语言可能是机器语言;也可能是中间语言表示(IR);
优化后,代码功能不会改变,但性能更好,体积更小;
汇编阶段:
接收上一处理阶段生成的代码,并将它们转换为目标平台上可执行的机器代码;
链接阶段:
汇编输出的一段或多段代码会被合并为一个独立的可执行程序;
这节介绍的预处理器是在语法分析阶段前的词法分析阶段发挥作用的;
接下来详细说下预处理器的操作;
操作:
预处理器根据一系列预定义规则,使用一些字符序列替换输入的字符序列;
这些操作会按如下顺序执行:
输入源文件到预处理器;
经过:
1)执行文本发翻译:
2)将输入的源文件拆分成多个记号;
3)将输入代码转换为预处理语言;
文本翻译:
预处理器会现将输入的源文件拆分成代码行、使用单个字符替换三字母组合、将被断开的连续行合并为较长的代码行和使用单个空格替换注释;
三字母组合是指C语言中用来代表单个字符的三字符序列;
记号转换:
预处理器将上衣步骤处理过的代码装换为记号序列;
基于预处理器语言的转换:
如果记号序列中含有预处理语言元素,就会根据这些记号进行转换;
前两个操作是自动执行的;
最后一个操作是由添加到源文件中的预处理器语言函数执行的;
5.2 预处理器语言
预处理器语言是一门独立语言;
预处理器语言对源文件进行的转换主要包括源文件的内容、条件编译和宏展开;
预处理器语言元素会在程序编译前处理源文件;
预处理器不能识别OC代码;
预处理器语言定义了预处理器指令和宏展开;
预处理器执行是指由预处理器(不是编译器)执行的指令;
宏指令是指具有名称的一段代码;当该名称在源代码中使用时,就会被替换为它代表的那段代码;
预处理器还定义了多个操作符和关键字;
5.2.1 指令
OC中源文件中的预处理器指令会使用独特的语法,使他们由预处理器而不是编译器处理;
预处理器指令具有以下形式:
#指令名 指令参数
例如:#import "Elements.h"
这句代码我们经常写,他就是一条预处理器指令;
预处理器指令以#号开头,后面紧跟指令名,之后是相应的参数;
预处理器指令会将换行符号用作结束符号;
要使预处理器指令扩展为多行,可以使用反斜杠\连接两行代码;
例如:#define A(x) \
(x * x * 3.1415926)
成套的预处理器指令及其作用:
1)头文件包含:#import、#include;
2)条件编译:#if、#elif、#else、#endif、#ifdef和#ifndef;
3)诊断:#error、#warning和#line;
4)#pragma指令;
接下来我们一个一个介绍;
1.头文件包含:
通过两条头文件包含指令启用头文件的包含功能;
作用是使预处理器获得被包含文件的文本,并将其插入到当前文件中;
这样做,促进了代码重用,因为源文件可以使用外部类的接口和宏指令,而无需直接复制它们;
#include包含头文件的两种形式:
#include "头文件名称"
#include <头文件名称>
头文件的名称是要插入的头文件的名称;两种方式的区别在于编译器寻找文件的方式:
1)使用双引号封装头文件的名称时:
编译器会先从存储源文件的目录中搜索被包含的头文件;
如果没找到,编译器会在默认目录中搜索头文件,默认目录是预先配置的用于搜索系统标准头文件的目录;
2)使用尖括号封装头文件名称时:
编译器会在默认目录中搜索被包含的头文件;
依照惯例:
使用<>封装标准头文件,因为它们通常会保存在默认目录中;
其他头文件应使用双引号封装;
#import和#include类似,区别在于:
他可以确保头文件仅在源文件中被包含一次,因此能够防止递归/重复包含;
在没有使用#import指令的情况下,必须在头文件中添加包含警卫,以防止出现递归包含;
就是说使用的是#include进行头文件包含,那么在被包含的头文件中(即某个类的.h文件)需要在顶部添加包含警卫;
包含警卫:
是一系列用于防止重复包含头文件的预处理器语句;
通常由#ifndef指令的条件表达式和#define指令构成;
例如:类Atom,在其他类中没有使用#import进行头文件包含,那么在Atom.h中定义接口前需要添加包含警卫;
#ifndef ATOM_H
#define ATOM_H
@interface ATOM :NSObject
...
@end
#endif
2.条件编译:
条件编译指令(#if、#elif、#else、#endif、#ifdef和#ifndef)可以根据条件是否成立,确定包含或不包含 部分或全部源文本;
#if:
使用#if指令可以测试表达式的值,并根据测试结果确定是否包含部分源文本;
使用示例:
#if 条件表达式
...
#endif:
#if指令与#endif指令配对使用,封装根据条件包含的文本;
这里的条件表达式指整型运算,可以包含一下元素:
1)整型和字符型常量;
2)算术运算符、位操作、转换、比较和逻辑运算操作;
3)预处理器宏指令;在计算表达式前,宏指令会被展开;
4)用于检查宏是否被定义的已定义操作符;
5)非宏指令标识符,在求值时它们都会被赋予0;
预处理器器会根据下边的规则计算表达式:
如果计算结果小于等于0,那木根据条件包含的文本(通常是源代码,也有可能是其他预处理器指令)就会被包含以进行编译或进一步预处理;
否则就不会被包含;
示例:
#define INPUT_ARGS 0
#if INPUT_ARGS <= 0
NSLog(@"hahah");
#warning todo
#endif
预处理器会按照以下步骤进行处理:
1)展开INPUT_ARGS标识符;
如果该标识符是一条指令,他会被相应的值替换;
如果不是或者这个宏没值,该标识符就会被替换为0;
2)计算表达式值,如果小于等于0,则#if和#endif语句封装的文本就会被包含,不会被预编译器过滤掉;
这类指令通常用在指定目标环境中,对平台专用代码进行条件编译;
比如,如果要使用多种编译器编译含有专用代码的OC程序,应使用#if指令和对应的编译器专有标识符封装这些代码;
#elif:
用于条件判断为否的情况,他增强了#if指令,是你可以在两个或多个条件之间进行选择;
#else:
该指令增强了#if和#else指令;
示例:
#if condition1
...
#elif condation2
...
#else
...
#endif
值得注意的是:#if #elif #else 和 #endif指令是可以嵌套的;此时,#endif指令会和同一嵌套层的#if指令配对;
#ifdef:
判断宏是否定义;
#ifdef指令也要与#endif指令配对使用,一起来封装条件文本;
示例:
#ifdef 宏名
...
#endif
#ifndef:
该指令可以补充#ifdef指令;
该指令中的宏未定义时,该指令会使预处理器将条件文本包含到源文件中;
在#if和#elif条件表达式中使用defined操作符,可以获得与使用#ifdef和#ifndef指令相同的效果;
defined操作符用于测试一个名称是否被宏定义,等同于#ifdef;
示例:#if defined 宏名 #if defined 宏名1 || defined 宏名2
在使用宏时,也可以将宏名放在括号中(宏名);
3.诊断:
在编译程序的过程中,可以使用多条预处理指令诊断问题;
#error:
使用#error指令可以使预处理器生成一条错误消息,并终止编译过程;
#error "error info"
使用双引号封装错误消息;
这条指令通常用于处理条件编译错误;
示例:
#ifndef INPUT_ARGS
#error "NO INPUT_ARGS ARGUMENT PROVIDED"
#endif
#warning:
与#error类似;
生成一条警告消息,但编译器会继续进行编译;
#line:
使用#line可以在编译器消息中添加行号;
当编译出错时,编译器会显示含有出错文件名称和相应行号的错误信息,从而使定位错误更加容易;
使用:#line 行号 "文件名"
该行号将被赋予下一行代码;而后续的代码行都会拥有每行加1的行号;
OC编译器中使用#line类似这种:
#line 100 "HUAHUA.m"
int ?number;
这段代码会产生一条错误信息,表示某文件中第101行有问题;
Xcode会自动显示源文件行号以及错误和警告消息,#line这条指令用到的情况极少;
4.#pragma指令:
使用#pragma指令可以设置超出OC语言范畴的、额外的编译器选项;
这些选项专门用于特定的平台和编译器;
语法:
#pragma pragma选项
pragma选项:
是对应特定编译器指令(及参数,如果存在)的一系列字符,这些字符可以实现预定义操作;
如果编译器不支持#pragma指令中的特定参数,则会忽略,不会产生错误;
Xcode支持多条#pragma mark指令:用以区分方法类别;
#pragma mark - :创建分割线;
#pragma mark -标签名称 :创建区分方法类别的标签;
示例:
#pragma mark -
#pragma mark -标签名称
...
#pragma mark -
在含有大量方法的大型工程和类中,#pragma mark 指令提供的区分类别和设置类方法显示方式的高效机制尤为有效;
5.2.2 宏
宏(macro):是指有名称的代码段;
当在代码中使用某个名称时,其代表的代码段就会替换他;
使用预处理器宏指令可以定义常量值;还可以配合输入参数值提供类似函数的功能;
使用预处理器指令#define定义宏;
使用#undef指令移除宏;
对象型宏定义常量值:#define 宏名 [值]
函数型宏:#define 宏名(参数) 代码
宏名后可以跟一个或多个参数(放入括号中且以逗号分隔),以及他所代表的代码段;
示例:#define SQUARE(x) ((x)*(x))
在被替换进宏体之前,所有参数都会进行彻底展开;参数替换后,为了进行宏展开预处理器会再次扫描全部内容,包括参数;
预处理器无法识别OC语句,因此它仅是将宏名替换为相应的值和代码;(宏只是简单的替换,这也是宏的定义应该使用括号的原因)
值得注意的是:宏不会受OC作用范围规则的影响;被定义后,会一直存在且在整个文件中起作用,直到被#undef指令解除定义为止;
另外,应使用花括号封装用于执行计算而非返回值的多行函数型宏;
#和##:函数型宏定义在替换字符串时接收的两种特殊操作符;
#:字符串化操作符,将宏的输入参数替换为相应的字符串(由双引号封装);
##:连接操作符,可以将两个记号(token)连接起来,使他们之间不含空格;
不要过度使用宏:
由于预处理器的操作是在对源文件进行语法分析之前执行的,所以定义一个能够使用所有情况的宏很难;
另外,向宏传递待副作用的参数(即参数的值被改变了)通常会引起问题;
最后,依赖宏的代码难以维护;
5.3 小结
1)预处理器指令在编译过程中的词法分析阶段(语法分析之前)起作用;
根据一系列预定义的规则,使用其他字符序列替换源文件中的某些字符序列;这些操作实现了文本,记号和基于预处理器语言的转换;
2)预处理器语言是一门独立的编程语言;
使用该语言对源文件执行装换的主要目的是包括源文件的内容、进行条件编译和宏展开;
3)使用预处理器语言的语法可以定义预处理器指令和宏;
预处理器指令是指由预处理器(而不是编译器)执行的命令;
预处理器宏是指有名称的代码段;被用于源代码中时,预处理器会将该名称替换为其代表的代码段;
4)预处理器指令能够执行头文件包含、条件处理、诊断和特定平台操作(#pragma);
5)使用宏可以定义常量和通过输入的参数值提供函数型替换功能;
6)不要过度使用宏;