您的位置:首页 > 其它

第16章 C预处理器和C库 16.6 其他指令

2017-07-25 00:00 183 查看
程序员可能需要为不同的工作环境准备不同的C程序和C库包。代码类型的选择会根据环境的不同而各异。预处理器提供一些指令来帮助程序员编写出这样的代码:改变一些#define宏的值后,这些代码就可以被从一个系统移植到另一个系统。#undef指令取消前面的#define定义。#if、#ifdef、#ifndef、#else、#elif和#endif指令可用于选择什么情况下编译哪些代码。#line指令用于重置行和文件信息,#error指令用于给出错误消息,#pragma指令用于向编译器发出指示。

16.6.1 #undef指令

#undef指令取消定义一个给定的#define。也就是说,假设有如下定义:

#define LIMIT 400

则指令:

#undef LIMIT

会取消该定义。现在就可以重新定义LIMIT,以使它有一个新的值。即使开始没有定义LIMIT,取消LIMIT的定义也是合法的。如果想使用一个特定的名字,但又不能确定前面是否已经使用了该名字,为安全起见,就可以取消该名字的定义。

16.6.2 已定义:C预处理器的观点

关于标识符的构成,预处理器和C遵循相同规则:标识符只能包含大写字母 、小写字母 、数字和下划线。

这里的已定义表示由预处理器定义。

如果标识符是该文件前面的#define指令创建的宏名,并且没有#undef指令关闭该标识符,则标识符是已定义的如果标识符不是宏,而是(例如)一个具有文件作用域的C变量,那么预处理器把标识符当作未定义的。

已定义的宏可以为类对象宏(包括空宏)或类函数宏:

#define LIMIT 1000    //LIMIT是已定义的
#define GOOD          //GOOD是已定义的
#define A(X) ((-(X))*(X))  //A是已定义的
int q;                //q不是一个宏,因此是未定义的
#undef GOOD           //GOOD是未定义的

注意,#define宏的作用域从文件中的定义点开始,直到用#undef取消宏为止,或直到文件尾为止(由二者中先满足条件的那个结束宏的作用域)。还应注意,如果用头文件引入宏,那么#define在文件中的位置依赖于#include指令的位置。

16.6.3 条件编译

可使用已经提到的指令设置条件编译。也就是说,可以使用这些指令告诉编译器:根据编译时的条件接受或忽略信息(代码)块。

一、#ifdef、#else和#endif指令

一个简短的示例可以阐明条件编译。考虑下面的内容:

#ifdef MAVIS
#include "horse.h"    //如果已经使用#define 定义了MAVIS,则执行这里的指令
#define STABLES 5
#else
#include "cow.h"      //如果没有用#define定义MAVIS,则执行这里的指令
#define STABLES 15
#endif

这里采用了较新的实现和ANSI 标准支持的缩排格式。如果使用旧编译器,必须要使所有指令,或者到少使用#指令符号左对齐:

#ifdef MAVIS
#    include "horse.h"    //如果已经使用#define 定义了MAVIS,则执行这里的指令
#    define STABLES 5
#else
#    include "cow.h"      //如果没有用#define定义MAVIS,则执行这里的指令
#    define STABLES 15
#endif

#ifdef #else格式非常类似于C中的if else。主要差异为预处理器不能识别标记代码块的花括号({ })因此使用#else(如果需要)和#endif(必须存在)来标记指令块。这些条件结构可以嵌套。

也可以使用这些指令标识C语句块,如程序清单16.9所示。

/*ifdef.c  --使用条件编译*/
#include <stdio.h>
#define JUST_CHECKING
#define LIMIT 4

int main(void)
{
int i;
int total = 0;

for (i=1;i<=LIMIT;I++)
{
total += 2*i*i + 1;
#ifdef JUST_CHECKING
printf("i=%d,running total = %d\n",i,total);
#endif
}
printf("Grand total = %d\n",total);
return 0;
}

编译并运行该程序,产生下列输出:

i=1,running total = 3
i=2,running total = 12
i=3,running total = 31
i=4,running total = 64
Grand total = 64

如果省略JUST_CHECKING的定义(或置于注释中,或用#undef取消它的定义)并重新编译程序,那么将会只显示最后一行。可以使用这种方法辅助调试程序 。定义JUST_CHECKING并合理使用#ifdef,使编译器包含那些用于打印辅助调试的中间值的程序代码。程序正常工作后,可以删除定义并重新编译。如果以后还需要使用这些信息,可以重新插入定义,从而避免再次输入额外的打印语句。

另外一种应用是:使用#indef选择使用不同C实现的大的代码块。

二、 #ifndef指令

类似于#indef指令,#ifndef指令可以与#else 、#endif指令一起使用。#ifndef判断后面的标识符是否为未定义的#ifndef的反义词#ifdef。#ifndef通常用来描述此前未定义的常量。

/*arrays.h*/
#ifndef SIZE
#    define SIZE 100
#endif

一般地,当某个文件包含几个头文件,而且每个头文件都可能定义了相同的宏时, 使用#ifndef可以防止对该宏的重复定义。此时 ,第一个头文件中的定义变成有效定义,而其他头文件中的定义则被忽略。

下面是另外一个应用。假设把下面这行代码:

#include "arrays.h"

放在一个文件头部,那么SIZE定义为100,但如果把:

#define SIZE 10

#include "arrays.h"

放在文件头部,那么SIZE定义为10.在处理arrays.h中的行时,SIZE是已定义的,因此将跳过#define SIZE 100.有时候可能会这么做,例如,可以用较小的数组来测试程序。当程序令人满意后,可以去除#define SIZE 10语句并重新编译。这样就不必担心修改头文件数组本身了。

#ifndef指令通常用于防止多次包含同一文件。也就是说,头文件可以采用类似下面几行的设置:

/*things.h*/

#ifndef THINGS_H_

#define THINGS_H_

/*头文件的其余部分*/

#endif

假设多次包含了该文件。当预处理器第一次遇到该文件时,THINGS_H_是未定义的,因此程序接着定义THINGS_H_,并处理文件其余部分。预处理器下次遇到该文件时,THINGS_H_已经定义,因此,预处理器跳过该文件的其余部分。

为什么会多次包含同一文件呢?最常见的原因是许多包含文件自身包含了其他文件,因此可能显式的包含其他文件已经包含的文件。为什么这会成为问题呢?因为头文件中的有些语句在一个文件中只能出现一次(如结构类型的声明)。标准C头文件使用#ifndef技术来避免多次包含。有一个问题是如何确保您使用的标识符在其他任何地方都没有使定义过。通常编译器提供商采用下述方法解决这个问题:用文件名作标识符,并在文件名中使用大写字母、用下划线代替文件名中的句点字符、用下划线(可能使用两条下划线)作前缀和后缀。例如,检查头文件stdio.h,可以发现许多类似这样的语句:

#ifndef _STDIO_H
#define _STDIO_H
//文件内容
#endif

您也可以这样做,但是,因为C标准保留使用下划线作前缀,所以您应避免这种用法。程序清单16.10使用#ifndef为程序清单16.6中的头文件提供了多次包含保护。

程序清单16.10 names_st.h头文件

//names_st.h  --带有多次包含保护的修订版本
#ifndef NAMES_H_
#define NAMES_H_

//常量
#define SLEN 32

//结构声明
struct names_st
{
char first[SLEN];
char last[SLEN];
};

//类型定义
typedef struct names_st names;

//函数原型
void get_names(names *);
void show_names(const names *);

#endif

可以用程序清单16.11中的程序对该头文件进行测试。使用程序清单16.10所示的头文件时,程序正常工作。但是从程序清单16.10中删除了#ifndef保护后,程序不能通过编译。

//doubincl.c  --两次包含同一文件
#include <stdio.h>
#include "names_st.h"
#include "names_st.h"  //不小心两次包含同一头文件

int main(void)
{
names winner = {"Less","Ismoor"};
printf("The winner is %s %s.\n",winner.first,winner.last);
return 0;
}

三、#if和#elif指令

#if指令更像常规的C中的if;#if后跟常量整数表达式。如果表达式为非零值,则表达式为真。在该表达式中可以使用C的关系运算符和逻辑运算符。

#if SYS == 1
#include "ibm.h"
#endif

可以使用#elif(有些早期的实现不支持#elif)指令扩展if-else序列。例如,可以这样使用:

#if SYS == 1
#include "ibmpc.h"
#elif SYS == 2
#include "vax.h"
#elif SYS == 3
#include "mac.h"
#else
#include "general.h"
#endif

许多新的实现提供另外一种方法来判断一个名字是否已经定义。不需要使用:

#ifdef VAX

而是采用下面的形式:

#if defined(VAX)

这里的defined( )是一个预处理器运算符。如果defined的参数已经使用#define定义过,那么defined( )返回1;否则返回0。这种方法的 优点在于它可以和#elif一起使用 。下面用它重写前面的示例:

#if defined(IBMPC)
#include "ibmpc.h"
#elif defined(VAX)
#include "vax.h"
#elif defined(MAC)
#include "mac.h"
#else
#include "general.h"
#endif

如果把这几行用于VAX机上,那么应在文件前面的某处用下面这行指令定义过VAX:

#define VAX

条件编译的一个用途是可以使程序更易于移植。通过在文件开头部分改变几个关键的定义,就可以为不同系统设置不同的值并包含不同文件。

16.6.4 预定义宏

表16.3列举了C 标准指定的一些预定义宏。

意 义
_ _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程序

//predef.c  --预定义标识符
#include <stdio.h>
void why_me( );
int main()
{
printf("The file is %s.\n",_ _FILE_ _);
printf("The date is %s.\n",_ _DATE_ _);
printf("The time is %s.\n",_ _TIME_ _);
printf("The version is %ld.\n",_ _STDC_VERSION_ _);
printf("This is line %d.\n",_ _LINE_ _);
printf("The function is %s\n",_ _func_ _);
why_me( );

return 0;
}

void why_me()
{
printf("This function is %s.\n",_ _func_ _);
printf("This is line %d.\n",_ _LINE_ _);
}

下面是一个运行示例:
The file is predef.c.
The date is Jul 19 2004.
The time is 10:00:30.
The version is 199901.
This is line 11.
The function is main
The function is why_me
This is line 21.

16.6.5 #line和#error

#line指令用于重置由_ _LINE_ _和_ _FILE_ _宏报告的行号和文件名。可这样使用#line:

#line 1000 //把当前行号重置为1000

#line 10 "cool.c" //把行号重置为10,文件名重置为cool.c

#error指令使预处理器发出一条错误消息,该消息包含指令中的文本。可能的话,编译过程应该中断。

可以这样使用#error:

#if _ _ STDC_VERSION_ _ !=199901L

#error Not C99

#endif

16.6.6 #pragma

在现代的编译器中,可用命令行参数或IDE菜单修改编译器的某些设置。也可用#pragma将编译器指令置于源代码中。

例如,在开发C99时,用C9X代表C99。编译器可以使用下面的编译指示来启用对C9X的支持:

#pragma C9X on

C99还提供了_Pragma预处理器运算符。_Pragma可将字符串转换成常规的编译指令。例如:

_Pragma("nonstandardtreatmenttypeB on")

等价于下面的指令:

#pragma nonstandardtreatmenttypeB on

因为该运算符没有使用#符号,所以可以将它作为宏展开的一部分

#define PRAGMA(X) _Pragma(#X)

#define LIMRG(X) PRAGMA(STDC CX_LIMITED_RANG X)

接着可以使用类似下面的代码:

LIMRG(ON)

顺便提一下,虽然下面的定义看上去可以正常运行,但实际上并非如此:

#define LIMRG(X) _Pragma(STDC CX_LIMITED_RAND #X)

问题在于上面的代码依赖于字符串连接功能,但是,直到预处理过程完成后编译器才连接字符串。

_Pragma运算符完成字符串析构(destringizing)工作,也就是说,将字符串中的转义序列转换成它所代表的字符。因而:

_Pragma("use_bool \"true \"false")

变成:

#Pragma use_bool "true "false
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息