17-条件编译
16.6.3 条件编译
可以使用其他指令创建条件编译(conditinal compilation)。也就是说,可以使用这些指令告诉编译器根据编译时的条件执行或忽略信息(或代码)块。
1. #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 指令说明,如果预处理器已定义了后面的标识符( MAVIS ),则执行 #else 或 #endif 指令之前的所有指令并编译所有C代码(先出现哪个指令就执行到哪里)。如果预处理器未定义 MAVIS ,且有 #else 指令,则执行 #else 和 #endif 指令之间的所有代码。
#ifdef #else 很像C的 if else 。两者的主要区别是,预处理器不识别用于标记块的花括号( {} ),因此它使用 #else (如果需要)和 #endif (必须存在)来标记指令块。这些指令结构可以嵌套。也可以用这些指令标记C语句块,如程序清单16.9所示。
程序清单16.9 ifdef.c 程序
/* 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 定义(把它放在C注释中,或者使用 #undef 指令取消它的定义)并重新编译该程序,只会输出最后一行。可以用这种方法来调试程序。定义 JUST_CHECKING 并合理使用 #ifdef ,编译器将执行用于调试的程序代码,打印中间值。调试结束后,可移除 JUST_CHECKING 定义并重新编译。如果以后还需要使用这些信息,重新插入定义即可。这样做省去了再次输入额外打印语句的麻烦。 #ifdef 还可用于根据不同的C实现选择合适的代码块。
2. #ifndef 指令
#ifndef 指令与 #ifdef 指令的用法类似,也可以和 #else 、 #endif 一起使用,但是它们的逻辑相反。 #ifndef 指令判断后面的标识符是否是未定义的,常用于定义之前未定义的常量。如下所示:
/* arrays.h */
#ifndef SIZE
#define SIZE 100
#endif
(旧的实现可能不允许使用缩进的 #define )
通常,包含多个头文件时,其中的文件可能包含了相同宏定义。 #ifndef 指令可以防止相同的宏被重复定义。在首次定义一个宏的头文件中用 #ifndef 指令激活定义,随后在其他头文件中的定义都被忽略。
#ifndef 指令还有另一种用法。假设有上面的 arrays.h 头文件,然后把下面一行代码放入一个头文件中:
#include "arrays.h"
SIZE 被定义为 100 。但是,如果把下面的代码放入该头文件:
#define SIZE 10
#include "arrays.h"
SIZE 则被设置为 10 。这里,当执行到 #include "arrays.h" 这行,处理 array.h 中的代码时,由于 SIZE 是已定义的,所以跳过了 #define SIZE 100 这行代码。鉴于此,可以利用这种方法,用一个较小的数组测试程序。测试完毕后,移除 #define SIZE 10 并重新编译。这样,就不用修改头文件数组本身了。
#ifndef 指令通常用于防止多次包含一个文件。也就是说,应该像下面这样设置头文件:
/* things.h */
#ifndef THINGS_H_
#define THINGS_H_
/* 省略了头文件中的其他内容*/
#endif
假设该文件被包含了多次。当预处理器首次发现该文件被包含时, THINGS_H_ 是未定义的,所以定义了 THINGS_H_ ,并接着处理该文件的其他部分。当预处理器第2次发现该文件被包含时, THINGS_H_ 是已定义的,所以预处理器跳过了该文件的其他部分。
为何要多次包含一个文件?最常见的原因是,许多被包含的文件中都包含着其他文件,所以显式包含的文件中可能包含着已经包含的其他文件。这有什么问题?在被包含的文件中有某些项(如,一些结构类型的声明)只能在一个文件中出现一次。C标准头文件使用 #ifndef 技巧避免重复包含。但是,这存在一个问题:如何确保待测试的标识符没有在别处定义。通常,实现的供应商使用这些方法解决这个问题:用文件名作为标识符、使用大写字母、用下划线字符代替文件名中的点字符、用下划线字符做前缀或后缀(可能使用两条下划线)。例如,查看 stdio.h 头文件,可以发现许多类似的代码:
#ifndef _STDIO_H
#define _STDIO_H
// 省略了文件的内容
#endif
你也可以这样做。但是,由于标准保留使用下划线作为前缀,所以在自己的代码中不要这样写,避免与标准头文件中的宏发生冲突。程序清单16.10修改了程序清单16.6中的头文件,使用 #ifndef 避免文件被重复包含。
程序清单16.10 names.h 程序
// names.h --修订后的 names_st 头文件,避免重复包含
#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 *);
char * s_gets(char * st, int n);
#endif
用程序清单16.11的程序测试该头文件没问题,但是如果把清单16.10中的 #ifndef 保护删除后,程序就无法通过编译。
程序清单16.11 doubincl.c 程序
// doubincl.c -- 包含头文件两次
#include <stdio.h>
#include "names.h"
#include "names.h" // 不小心第2次包含头文件
int main()
{
names winner = { "Less", "Ismoor" };
printf("The winner is %s %s.\n", winner.first,
winner.last);
return 0;
}
3. #if 和 #elif 指令
#if 指令很像C语言中的 if 。 #if 后面跟整型常量表达式,如果表达式为非零,则表达式为真。可以在指令中使用C的关系运算符和逻辑运算符:
#if SYS == 1
#include "ibm.h"
#endif
可以按照 if else 的形式使用 #elif (早期的实现不支持 #elif )。例如,可以这样写:
#if SYS == 1
#include "ibmpc.h"
#elif SYS == 2
#include "vax.h"
#elif SYS == 3
#include "mac.h"
#else
#include "general.h"
#endif
较新的编译器提供另一种方法测试名称是否已定义,即用 #if defined (VAX) 代替 #ifdef VAX 。
这里, defined 是一个预处理运算符,如果它的参数是用 #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
条件编译还有一个用途是让程序更容易移植。改变文件开头部分的几个关键的定义,即可根据不同的系统设置不同的值和包含不同的文件。