预处理器是编译过程中单独执行的第一个步骤。最常用的预处理器是:#include指令(用于在编译期间把指定文件的内容包含进当前文件中)和#define指令(用任意字符 序列替代一个标记)。
1. 文件包含
文件包含指令(即#include指令)使得处理大量的#define指令以及声明更加方便。在源文件中,任何形如:
#include“文件名”
#include <文件名>
的行都将被替换为由文件名指定的文件的内容。如果文件名用引号引起来,则在源文件所在的位置查找该文件;如果在该位置没有找到文件,或者如果文件名是用尖括号<与>括起来的,则将根据相应的规则查找该文件。
2. 宏定义
宏定义的形式如下:
#define 名字 替换文本
这是一种最简单的宏替换----后续所有出现名字记号的地方都将被替换为替换文本。
#define指令中的名字与变量名的命名方式相同,替换文本可以是任意字符串。通常情况下,#define指令占一行,替换文本是#define指令行尾部的所有剩余部分内容,但也可以把一个较长的宏定义分成若干行,这时需要在待续的行末尾加上一个反斜杠符\。
如果宏里有多过一个语句,就需要用 do{/*...*/} while(0) 包裹成单个语句,否则会有以下问题:
#define M() a(); b()
if (cond)
M();
else
c();
/* 预处理后 */
if (cond)
a(); b();
else /* <- else 缺乏对应 if */
c();
只用 {} 也不行:
#define M() { a(); b(); }
/* 预处理后 */
if (cond)
{ a(); b(); }; /* 最后的分号代表 if 语句结束 */
else /* else 缺乏对应 if */
c();
用 do while 就行了:
#define M() do { a(); b(); } while(0)
/* 预处理后 */
if (cond)
do { a(); b(); } while(0);
else
c();
作用域
#define指令定义的名字的作用域从其定义点开始,到被编译的源文件的末尾处结束。宏定义中也可以使用前面出现的宏定义。替换只对记号进行,对括在引号中的字符串不起作用。例如,如果YES是一个通过#define指令定义过的名字,则在printf("YES")或YESMAN中将不执行替换。
宏定义的优缺点
替换文本可以是任意的,例如:
#define forever for(;;) //无限循环
该语句为无限循环定义了一个新的名字forever。
宏定义也可以带参数,这样就可以对不同的宏调用使用不同的替换文本。例如,下列宏定义定义了一个宏max:
#define max(A+B) ((A) > (B) ? (A) : (B))
使用宏max看起来像是函数调用,但宏调用直接将替换文本插入到代码中。形式参数(在此为A或B)的每次出现都将被替换成对应的实际参数。因此,语句:
x=max(p+q,r+s);
将被替换为下列形式:
x=((p+q) > (r+s) ? (p+q) : (r+s));
如果对各种类型的参数的处理是一致的,则可以将同一个宏定义应用于任何数据类型,则无需针对不同的数据类型需要定义不同的max函数。
但是max存在一定的缺陷。作为参数的表达式需要计算两次,如果表达式存在副作用(比如含自增运算符或输入/输出),则会出现不正确的情况。例如:
max(i++, j++) //错误
它将对每个参数执行两次自增操作。同时还需要注意,要适当使用圆括号以保证计算次序的正确性。下列宏定义:
#define square(x) x*x //错误
如果square(z+1)调用该宏定义则会出现 z+1*z+1,而不是 (z+1)*(z+1)
3. 使用宏定义的函数
<stdio.h>头文件中有一个很实用的例子:getchar与putchar函数在实际中常常被定义为宏,这样可以避免处理字符时调用函数所需的运行时开销。<ctype.h>头文件中定义的函数也常常通过宏实现的。
可以通过#undef指令取消名字的宏定义,这样做可以保证后续的调用是函数调用,而不是宏调用:
#undef getchar
int getchar(void){...}
宏定义中形式参数不能带引号的字符串替换。但是,如果在替换文本中,参数名以#作为前缀则结果将被扩展为由实际参数替换该参数的带引号的字符串。例如,可以将它与字符串连接运算结合起来编写一个调试打印宏:
#define dprint(expr) printf(#expr " = %g\n", expr)
使用语句dprint(x/y);
调用该宏时,该宏将被扩展为:
printf("x/y" " = %g\n",x/y);
其中的字符串被连接起来了,这样,该宏调用的效果等价于
printf("x/y = %g\n",x/y);
在实际参数中,每个双引号"将被替换为\",反斜杠\将被替换为\\,因此替换后的字符串是合法的字符串常量。
4. 条件包含
还可以使用条件语句对预处理本身进行控制,这种条件语句的值是在预处理执行的过程中进行计算。这种方式为在编译过程中根据计算所得的条件值选择性地包含不同代码提供了一种手段。
#if 语句对其中的常量整型表达式(其中不能包含sizeof、类型转换运算符或enum常量)进行求值。若该表达式的值不等于0,则包含其后的各行,直到遇到#endif、#elif 或#else 语句为止(预处理器语句#elif 类似于else if)。在#if 语句中可以使用表达式defined(名字),该表达式的值遵循以下规则:当名字已经定义时,其值为1;否则,其值为0。
例如,为了保证hdr.h 文件的内容只被包含一次,可以将该文件的内容包含在下列形式的条件语句中:
#if !defined(HDR)
#define HDR
/* hdr.h 文件的内容放在这里 */
#endif
第一次包含头文件hdr.h 时,将定义名字HDR;此后再次包含该头文件时,会发现该名字已经定义,这样将直接跳转到#ifndef 处。类似的方式也可以用来避免多次重复包含同一文件。如果多个头文件能够一致的使用这种方式,那么,每个头文件都可以将它所依赖的任何头文件包含进来,用户不必考虑和处理头文件之间的各种依赖关系。
下面的这段预处理代码首先测试系统变量SYSTEM,然后根据该变量的值确定包含哪个版本的头文件:
#if SYSTEM == SYSV
#define HDR "sysv.h"
#elif SYSTEM == BSD
#define HDR "bsd.h"
#elif SYSTEM == MSDOS
#define HDR "msdos.h"
#else
#define HDR "default.h"
#endif
#include HDR
C语言专门定义了两个预处理语句#ifdef 与#ifndef,它们用来测试某个名字是否已经定义。上面有关#if 的第一个例子可以改写为下列形式
#ifndef HDR
#define HDR
/* hdr.h 文件的内容放在这里 */
#endif