预计阅读本页时间:-
程序员可能需要为不同的工作环境准备不同的C程序或C库包。代码类型的选择会根据环境的不同而各异。预处理器提供一些指令来帮助程序员编写出这样的代码:改变一些#define宏的值后,这些代码就可以被从一个系统移植到另一个系统。#undef指令取消前面的#define定义。#if、#ifdef、#ifndef、#else、#elif,和#endif指令可用于选择什么情况下编译哪些代码。#line指令用于重置行和文件信息,#error指令用于给出错误消息,#pragma指令用于向编译器发出指示。
16.6.1 #undef指令
#undef指令取消定义一个给定的#define。也就是说,假设有如下定义:
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
则指令:
会取消该定义。现在就可重新定义LIMIT,以使它有一个新的值。即使开始没有定义LIMIT,取消LIMIT的定义也是合法的。如果想使用一个特定名字,但又不能确定前面是否已经使用了该名字,为安全起见,就可以取消该名字的定义。
16.6.2 已定义:C预处理器的观点
关于标识符的构成,预处理器和C遵循相同规则:标识符只能包含大写字母、小写字母、数字和下划线字符。预处理器在预处理指令中遇到标识符时,要么把标识符当作已定义的,要么当作未定义的。这里的已定义表示由预处理器定义。如果标识符是该文件前面的#define指令创建的宏名,并且没有用#undef指令关闭该标识符,则标识符是已定义的。如果标识符不是宏,而是(例如)一个具有文件作用域的C变量,那么预处理器把标识符当作未定义的。
已定义宏可以为类对象宏(包括空宏)或类函数宏:
注意,#define宏的作用域从文件中的定义点开始,直到用#undef指令取消宏为止,或直到文件尾为止(由二者中最先满足的那个结束宏的作用域)。还应注意,如果用头文件引入宏,那么,#define在文件中的位置依赖于#include指令的位置。
本章稍后将讨论几个预定义的宏,如__DATE__和__FILE__。这些宏总被认为是已定义的,并且不能被取消定义。
16.6.3 条件编译
可使用已经提到的指令设置条件编译。也就是说,可以使用这些指令告诉编译器:根据编译时的条件接受或忽略信息(或代码)块。
一、#ifdef、#else和#endif指令
一个简短的示例可以阐明条件编译。考虑下面的内容:
这里采用了较新的实现和ANSI标准支持的缩排格式。如果使用旧的编译器,必须要使所有指令,或者至少使#指令符号(参阅下例)左对齐:
#ifdef指令说明:如果预处理器已经定义了后面的标识符(MAVIS),那么执行所有指令并编译C代码,直到下一个#else或#endif出现为止(无论#else和#endif谁先出现)。如果有#else指令,那么,在未定义标识符时会执行#else和#endif之间的所有代码。
#ifdef #else格式非常类似于C中的if else。主要差异为预处理器不能识别标记代码块的花括号({}),因此使用#else(如果需要)和#endif(必须存在)来标记指令块。这些条件结构可以嵌套。如程序清单16.9所示,也可用这些指令标记C语句块。
程序清单16.9 ifdef.c程序
编译并运行该程序,产生下列输出:
如果略去JUSR_CHECKING的定义(或把它置于C注释中,或用#undef取消它的定义)并重新编译程序,那么将会只显示最后一行。可以使用这种方法辅助调试程序。定义JUST_CHECKING并合理使用#ifdef,使编译器包含那些用于打印辅助调试的中间值的程序代码。程序正常工作后,可以删除定义并重新编译。如果以后还需要使用这些信息,可以重新插入定义,从而避免再次输入额外的打印语句。另外一种应用是:使用#ifdef选择适用于不同C实现的大块代码。
二、#ifndef指令
类似于#ifdef指令,#ifndef指令可以与#else、#endif指令一起使用。#ifndef判断后面的标识符是否为未定义的,#ifndef的反义词为#ifdef。#ifndef通常用来定义此前未定义的常量。
(旧的实现可能不允许使用缩排格式的#define指令。)
一般地,当某文件包含几个头文件,而且每个头文件都可能定义了相同的宏时,使用#ifndef可以防止对该宏重复定义。此时,第一个头文件中的定义变成有效定义,而其他头文件中的定义则被忽略。
下面是另一个应用。假设把下面这行代码:
放在一个文件头部,那么SIZE定义为100,但如果把:
放在文件头部,那么SIZE定义为10。在处理arrays.h中的行时,SIZE是已定义的,因此将跳过#define SIZE 100。有时候可能会这样做,例如,可以用较小的数组来测试程序。当程序令人满意后,可以去除#define SIZE 10语句并重新编译。这样就不必担心要修改头文件数组自身了。
#ifndef指令通常用于防止多次包含同一文件。也就是说,头文件可采用类似下面几行的设置:
假设多次包含了该文件。当预处理器第一次遇到该包含文件时,THINGS_H_是未定义的,因此程序接着定义THINGS_H_,并处理文件的其余部分。预处理器下一次遇到该文件时,THINGS_H_已经定义,因此,预处理器跳过该文件的其余部分。
为什么会多次包含同一文件呢?最常见的原因是:许多包含文件自身包含了其他文件,因此可能显式地包含其他文件已经包含的文件。为什么这会成为问题呢?因为头文件中的有些语句在一个文件中只能出现一次(如结构类型的声明)。标准C头文件使用#ifndef技术来避免多次包含。有一个问题是如何确保您使用的标识符在其他任何地方都没有定义过。通常编译器提供商采用下述方法解决这个问题:用文件名做标识符,并在文件名中使用大写字母、用下划线代替文件名中的句点字符、用下划线(可能使用两条下划线)作前缀和后缀。例如,检查头文件stdio.h,可以发现许多类似这样的语句:
您也可这样做。但是,因为C标准保留使用下划线作前缀,所以您应避免这种用法。没有人希望自己定义的宏意外地与标准头文件发生冲突。程序清单16.10使用#ifndef为程序清单16.6中的头文件提供了多次包含保护。
程序清单16.10 names_st.h头文件
可以用程序清单16.11中的程序对该头文件进行测试。使用程序清单16.10所示的头文件时,程序正常工作。但是从程序清单16.10中删除了#ifndef保护后,程序不能通过编译。
程序清单16.11 doubincl.c程序
三、#if和#elif指令
#if指令更像常规的C中的if;#if后跟常量整数表达式。如果表达式为非零值,则表达式为真。在该表达式中可以使用C的关系运算符和逻辑运算符。
可以使用#elif(有些早期的实现不支持#elif)指令扩展if-else序列。例如,可以这样使用:
许多新的实现提供另一种方法来判断一个名字是否已经定义。不需使用:
而是采用下面的形式:
这里,defined是一个预处理器运算符。如果defined的参数已用#define定义过,那么defined返回1;否则返回0。这种新方法的优点在于它可以和#elif一起使用。下面用它重写前面的示例:
如果把这几行用于VAX机上,那么,应在文件前面的某处用下面这行指令定义过VAX:
条件编译的一个用途是可以使程序更易于移植。通过在文件开头部分改变几个关键的定义,就可以为不同系统设置不同值并包含不同文件。
16.6.4 预定义宏
表16.3列举了C标准指定的一些预定义宏。
表16.3 预定义宏
宏 | 意 义 |
---|---|
__DATE__ | 进行预处理的日期(“Mmm dd yyyy”形式的字符串文字) |
__FILE__ | 代表当前源代码文件名的字符串文字 |
__LINE__ | 代表当前源代码文件中的行号的整数常量 |
__STDC__ | 设置为1时,表示该实现遵循C标准 |
__STDC_HOSTED__ | 为本机环境设置为1,否则设为0 |
__STDC_VERSION__ | 为C99时设置为199901L |
__TIME__ | 源文件编译时间,格式为“hh: mm: ss” |
C99标准提供一个名为__func__的预定义标识符。__func__展开为一个代表函数名(该函数包含该标识符)的字符串。该标识符具有函数作用域,而宏本质上具有文件作用域。因而__func__是C语言的预定义标识符,而非预定义宏。
程序清单16.12显示了几个使用这些预定义标识符的示例。注意有些标识符是C99新增的,C99之前的编译器可能不接受它们。
程序清单16.12 predef.c程序
下面是一个运行示例:
16.6.5 #line和#error
#line指令用于重置由__LINE__和__FILE__宏报告的行号和文件名。可以这样使用#line:
#error指令使预处理器发出一条错误消息,该消息包含指令中的文本。可能的话,编译过程应该中断。可以这样使用#error:
16.6.6 #pragma
在现代的编译器中,可用命令行参数或IDE菜单修改编译器的某些设置。也可用#pragma将编译器指令置于源代码中。
例如,在开发C99时,用C9X代表C99。编译器可以使用下面的编译指示(pragma)来启用对C9X的支持:
一般来说,每台编译器都有自己的编译指示集。例如,这些编译指示可能用于控制分配给自动变量的内存大小,或者设置错误检查的严格程度,或者启用非标准语言特征。C99标准提供了3个标准编译指示。它们的技术性很强,我们不进行讨论。
C99还提供了_Pragma预处理器运算符。_Pragma可将字符串转换成常规的编译指示。例如:
等价于下面的指令:
因为该运算符没有使用#符号,所以可将它作为宏展开的一部分:
接着可以使用类似下面的代码:
顺便提一下,虽然下面的定义看上去可以正常运行,但实际并非如此:
问题在于上面的代码依赖于字符串连接功能,但是,直到预处理过程完成后编译器才连接字符串。
_Pragma运算符完成字符串析构(destringizing)工作;也就是说,将字符串中的转义序列转换成它所代表的字符。因而:
变成: