说明:预处理器是一个小软件,它可以在编译前编辑c程序。
弊端:
- 可能是许多难以发现的错误的根源
- 经常被错误地用来编写一些几乎不可能读懂的程序
技巧:适度使用预处理功能,减少对于处理器的依赖。
扩展:c++中可以进一步限制预处理器的使用。
14.1 预处理器的工作方式
说明:预处理器的输入是一个c语言程序,程序中可能会包含指令。预处理器会执行这些指令,并在处理过程中删除这些指令。
特点:
- 不检查错误
- 不删除包含指令的行,而是简单地将它们替换为空
- 将每一处注视替换为空格字符(有些与编译器会进一步删除不必要的空白字符,并在每一行开始使用缩进的空格符和制表符)
注意:预处理器仅知道少量的c语言规则,因此,它在执行指令时非常有可能产生非法的程序。
14.2 预处理指令
分类:
指令 | 包括 |
---|---|
宏定义 | #define #undef |
文件包含 | #include |
条件编译 | #if ifdef ifndef elif #else #endif |
其他 | #error #line #pragma |
语法:
- 指令都以
#
开始:空白符 #指令名 指令所需要的其他信息
- 在指令的符号之间可以插入任意数量的空格或横向制表符
1 |
- 指令总是在第一个换行符处结束,除非明确地指明要继续(通过在行末尾使用
\
)
1 |
|
- 指令可以出现在程序种任何地方(
#define
和#include
通常放在文件开始) - 注释可以和指令放在同一行
1 |
14.3 宏定义
说明:除了简单的宏,与编译器也支持带参数的宏。
14.3.1 简单的宏
语法:#define 标识符 替换列表
替换列表:一系列c语言记号,包括标识符、关键字、数、字符常量、字符串字面量、运算符和标点符号。
原理:当预处理器遇到一个宏定义时,会做一个标识符
代表替换列表
的记录。在文件后面的内容中,不管标识符在任何位置出现,预处理器都会用替换列表代替它。
注意(常见错误):
- 不要在宏定义中放置任何额外的符号
1 | /* 不能使用 = */ |
- 不能在宏定义的末尾添加分号
1 | /* 末尾添加的分号会被作为替换列表的一部分 */ |
优点:简单的宏主要用来定义那些被Kernighan
和Ritchie
称为 明示常量(manifest constant
)的东西。优点如下
- 程序更易读
- 易于修改
- 帮助避免前后不一致或键盘输入错误
- 可以对c语法做小的修改
- 对类型重命名
- 控制条件编译
1 |
14.3.2 带参数的宏
语法:#define 标识符(x1, x2, ..., xn) 替换列表
- 宏的
(
和标识符
之间必须没有空格(否则会被当作简单宏处理)
原理:当预处理器遇到一个带参数的宏,会将定义存储起来以便后面使用。在后面的程序中,宏调用标识符(y1,y2,...,yn)
会被替换列表
替换,且参数也会依据宏定义
对应到替换列表中。
用途:
- 经常被用来作为一些简单的函数使用(模拟函数调用)
- 经常被作为模版,替换经常重复书写的代码段(替换语句)
优点:相比实际的函数
- 宏可能会稍快些(没有存储上下文、复制参数等的开销)
- 宏会更“通用”(没有对参数类型的限制)
缺点:
- 编译后的代码通常会变大
- 宏参数没有类型检查
- 无法用一个指针指向一个宏
- 宏可能会不止一次地计算它的参数
注意: 如果宏使用带有副作用的参数,多次进行宏调用带来的副作用可能导致不易察觉的错误。
技巧: 避免使用带有副作用的宏。
1 | //模拟函数 |
14.3.3 #
运算符
说明:将带参数的宏的参数转换为字符串字面量
语法:#define 标识符(x1...) 替换列表
#参数
仅允许出现在带参数的宏的替换列表中
1 |
|
14.3.4 ##
运算符
说明:可以将两个记号(例如标识符)“粘”在一起,成为一个记号。
1 | #define MK_ID(n) i##n |
求最大值的函数模版(针对不同类型)
1 |
|
14.3.5 宏的通用属性
- 宏的替换列表可以包含对另一个宏的调用
- 预处理器只会替换完整的记号,而不会替换记号的片段
- 一个宏定义的作用范围通常到出现这个宏的文件末尾
- 宏不可以被定义两遍,除非新的定义和旧的定义是一样的
- 宏可以使用
#undef
指令取消定义
1 |
14.3.6 宏定义中的圆括号
说明:在宏定义中缺少圆括号会导致c语言最让人讨厌的错误(比如优先级问题)。
哪里要添加圆括号:
- 如果宏的替换列表中有运算符,那么始终要将替换列表放在括号中
- 当宏有参数时,仅给替换列表添加圆括号是不够的,参数的每一次出现都要添加圆括号
1 |
|
14.3.7 创建较长的宏
14.3.7.1 逗号运算符
说明:创建较长的宏的一个办法是使用逗号运算符
,特别是可以使用逗号运算符来使替换列表包含一系列表达式。
限制:逗号运算符
只能连接表达式
,不能连接语句
。
1 |
|
14.4.7.2 复合语句
说明:除了使用逗号表达式,还可以将语句
或表达式
放在{}
内形成复合语句。
缺点:不能在替换列表
为复合语句的宏调用的末尾使用分号结尾,因为在if
语句中调用会导致错误。
1 |
|
14.4.7.3 在只循环一次的do-while中包含语句和表达式
说明:加入一个宏需要包含一系列的语句,而不仅仅是一系列的表达式,可以将语句放在do
循环中,并将条件设置为假。
1 |
|
14.3.8 预定义宏
说明:在c语言中预定义了一些有用的宏,这些宏主要是提供当前编译的信息。
扩展:c语言提供了一个通用的、用于错误检测的宏—assert宏。
名字 | 描述 |
---|---|
LINE | 被编译的文件的行数 |
FILE | 被编译的文件的名字 |
DATE | 编译的日期(格式”Mmm dd yyyy”) |
TIME | 编译的时间(格式”hh:mm:ss”) |
STDC | 如果编译器接受标准c,那么值为1 |
1 | //检测被零除的错误 |
14.4 条件编译
说明:条件编译是指根据预处理器所执行的测试结果来包含或排除程序的片段。
14.4.1 #if指令和#endif指令
语法:当预处理器遇到if
指令时,会计算常量表达式。如果表达式的值为0,那么#if
与#endif
之间的行将在预处理过程中删除。
注意:对于没有定义过的标识符,#if
指令会把它当作是值为0的宏对待。
1 |
|
1 |
|
14.4.2 defined运算符
说明:如果标识符是一个定义过的宏返回1,否则返回0。
用途:判断宏某个标识符是否被定义的宏,通常和#if
指令结合使用。
1 |
|
14.4.3 #ifdef指令和#ifndef指令
说明:严格说来,这里要介绍的两种指令都不是必须的,因为都可以用其他指令模拟。
14.4.3.1 #ifdef指令
语法:等价于if defined(标识符)
1 |
|
14.4.3.2 #ifndef指令
语法:等价于#if !defined(标识符)
1 |
|
14.4.4 #elif指令和#else指令
1 |
|
14.4.5 使用条件编译
常见应用:
- 编写在多台机器或多种操作系统之间可移植的程序
1 |
|
- 编写可以使用不同的编译器进行编译的程序
1 |
|
- 为宏提供默认定义
1 |
- 临时屏蔽包含注释的代码
1 |
|
14.5 其他指令
14.5.1 #error指令
语法:#error 消息
说明:如果预处理器遇到一个#error
指令,它会显示一个出错消息,这个消息一定会包含消息
,然后大多数编译器会立即终止编译而不去找出其他错误。
用途:通常与条件编译指令一起用于检测正常编译过程中不应出现的情况。
1 |
14.5.2 #line指令
说明:用来改变程序行编号的方式以及使编译器认为所在文件是另一个文件。
语法:#line 行号 [文件名]
行号
是大小介于1-32767之间的整数行号
会影响__LINE__
宏的值,文件名
影响__FILE__
的值
用途:主要用于那些产生c代码作为输入的程序,因为出错信息都指向程序员编写的文件,而不是(更复杂)由一些工具生成的文件。
14.5.3 #pragma指令
语法:#pragma 记号
- #pragma指令通常只跟着一个记号,这个记号表示了一条编译器需要服从的命令
- 一些编译器允许#pragma指令所包含的不仅是简单的命令(特别是有些编译器允许#pragma指令带参数)
- 如果#pragma指令包含了无法识别的命令,编译器必须忽略这些#pragma指令,不允许产生出错信息
注意:#pragma指令中出现的命令集在不同的编译器上是不一样的,需要查阅相关编译器的文档。