C预处理器(preprocessor)在源代码编译前对其进行一些文本性质的操作。它的主要任务包括删除注释、插入被#include指令包含的文件的内容、定义和替换由#define 指令定义的符号,以及确定代码的部分内容是否应根据一些条件编译指令进行编译。
大多数C语言实习在函数调用时都会带来重大的系统开销。我们希望有这样一中程序块,即它看上去像一个函数,但却没有函数调用带来的开销。举例来说:getchar和putchar经常被实现为宏,以避免在每次执行输入或输出一个字符这样简单的操作时,都要调用相应的函数从而造成系统效率的下降。
1.预定义符号
下表总结了由预处理器定义的符号,它们的值或是字符串常量,或者是十进制数字常量。__FILE__
和__LINE__
在确认调试输出的来源方面很有用处。__DATE__
和__TIME__
常常用于在被编译的程序中加入版本信息。__ STDC__
用于在那些ANSI环境和非ANSI环境都必须进行编译的程序中结合条件编译。
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("file:%s\nline:%d\ndate:%s\ntime:%s\n", __FILE__, __LINE__, __DATE__, __TIME__);
system("pause");
return 0;
}
file:d:\学习\c语言\1\1\1.cpp
line:7
date:Jul 2 2021
time:11:56:26
预处理器符号
符号 | 示例值 | 含义 |
---|---|---|
__FILE__ | “name.c” | 进行编译的源文件名 |
__LINE__ | 25 | 文件当前行的行号 |
__DATE__ | “Jan 31 1997” | 文件被编译的日期 |
__TIME__ | “18:04:30” | 文件被编译的时间 |
__ STDC__ | 1 | 如果编译器遵循ANSI C,其值就为1,否则未定义 |
2.#define
2.1 基本概念
#define name staff
预处理器把符号name替换为staff。
#define指令把一个符号与一个任意的字符序列联系在一起。
替换文本不仅限于数值字面值常量。使用#define
可以把任何的文本替换到程序中。
#define reg register
#define CASE break;case
如果定义中的stuff非常长,它可以分成几行,除了最后一行,每行的末尾加上反斜杠\
#define DEBUG_PRINT printf("File %s line %d:"\
"x=%d,y=%d,z=%d",\
__FILE__,__LINE__,\
x,y,z)
这里利用了相邻的字符串常量被自动连接为一个字符串这个特性。
x+=2;
y+=x;
DEBUG_PRINT;
这里在调用时都末尾有一个分号,所以不应该在宏定义的尾部加上分号。
如果相同的代码需要出现在程序的好几个地方,通常更好的方法是把它实现为一个函数。
2.2 宏
#define
定义了一种机制,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(defined macro)。
#define name(parameter_list) stuff
当宏被调用的时候,名字后面是一个由逗号分隔的值的列表,每个值都与宏定义中的一个参数相对应,整个列表用一对括号包围。
#define SQUARE(x) x*x
...
SQUARE(5); //5*5
但是,这个宏存在问题
a=5;printf("%d\n",SQUARE(a+1));
看上去好像打印36。但这段代码最终将打印11这个值。参数x被文本a+1所替换,所以实际上变成了:
printf("%d\n",a + 1 * a + 1 );
在宏定义中加上两个括号就轻易解决:
#define SQUARE(x) (x)*(x)
上面那个例子:
printf("%d\n",(a+1)*(a+1));
另外一个宏定义
#define DOUBLE(x) (x)+(x)
这个宏可能会出现另外一种错误:
#define DOUBLE(x) (x)+(x)...a=5;printf("%d\n",10*DOUBLE(x))
看上去好像打印100,但实际上打印的是55。通过观察宏替换的文本:
printf("%d\n",10*(a)+(a));
在定义宏时,只要在整个表达式的两边加上一堆括号就能够解决
#define DOUBLE(x) ((x)+(x))
!!所有用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时,参数中的操作数或邻近的操作符之间发生不可预料的后果。
2.3 #define替换
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
- 在调用宏时,首先对参数进行检查,看看是否包含了任何由#define定义的符号。如果是,它们首先被替换。
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被它们的值所替代。
- 最后,再次对结果文本进行扫描,看看它是否包含了任何由#define定义的符号。如果是,就重复上述处理过程。
这样,宏参数和#define定义可以包含其他#define定义的符号。但是,宏不可以出现递归。
当预处理器搜索#define定义的符号时,并不检查字符串常量的内容。如果想把宏参数插入配符串常量中,可以使用两个技巧:
第一个技巧是,邻近字符串自动连接的特性使我们很容易把一个字符串分成几段,每段实际上都是一个宏参数。 示例如下:
#define PRINT(FORMAT,VALUE) \ printf("The Value is"FORMAT"\n,VALUE)...PRINT("%d",x+3);
这个技巧只能当字符串常量作为宏参数给出时才能使用
第二个技巧是适用于处理器把一个宏参数转换为一个字符串。#argument这种结构被预处理器翻译为"argument"。
#define PRINT(FORMAT,VALUE) \ printf("The value of" #VALUE " is " FORMAT "\n",VALUE )...PRINT("%d",x+3);
它将产生下面的输出:
The value of x+3 is 25
##结构用于把位于自己两边的符号连接成一个符号。作为用途之一,它允许宏定义从分离的文本片段创建标识符。
#define ADD_TO_SUM (sum_number ,value ) \ sum ## sum_number += value;...ADD_TO_SUM(5,25);
最后一条语句把值25加到变量sum5。
!!注意,这种连接必须产生一个合法的标识符,否则,其结果就是未定义的。
2.4 宏与函数
宏非常频繁地用于执行简单的计算,比如在两个表达式中寻找其中较大(或较小)的一个:
#define MAX( a, b ) ( (a) > (b) ? (a) : (b) )
为什么不用函数来完成这个任务呢?
首先,用于调用和从函数返回的代码很可能比实际执行这个小型计算工作的代码更大,所以使用宏比使用函数在程序的规模和速度方面都更胜一筹;
其次,更为重要的是,函数的参数必须声明为一种特定的类型,所以它只能在类型合适的表达式中使用。反之,上面这个宏可以用于整型、长整型、单浮点型、双浮点数以及其他任何可以用>操作符比较值大小的类型。换句话说,宏是与类型无关的。
和使用函数相比,使用宏的不利之处在于每次使用宏时,一份宏定义代码的副本都将插入到程序中。除非宏非常短,否则使用宏可能会大幅增加程序的长度。
还有一 些任务根本无法用函数实现。观察下面这个宏,这个宏的第2个参数是一种类型,它无法作为函数参数进行传递。
#define MALLOC(n, type) \ ( (type *)malloc( (n) * sizeof( type ) ) )
#include<stdio.h>#include<string.h>#include<stdlib.h>#define MALLOC(n,type) ((type *)malloc(n*sizeof(type)))int main(){ int a; int *p; p=MALLOC(100,int); int i; for(i=0;i<100;i++) { *(p+i) = i; } for(i=0;i<100;i++) { printf("%d\n",*(p+i)); } free(p);}
现在可以观察一下这个宏确切的工作过程。下面这个例子中的第1条语句被预处理器转换为第2条语句:
pi = MALLOC( 25, int );pi=((int *)malloc( (25) * sizeof( int ) ) );
同样这个宏定义中并没有用一个分号结尾。分号出现在调用这个宏的语句中。这里宏定义并没有用一个分号结尾。分号出现在调用这个宏的语句中。
宏和函数的不同之处
属性 | #define宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都被插入到程序中。除了非常小的宏,程序的长度将大幅增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数调用/返回的额外开销 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非它们加上括号,否则邻近操作符的优先级可能会产生不可预料的结果 | 函数参数只在函数调用时求值一次, 它的结果值传递给函数。表达式的求值结果更容易预测 |
参数求值 | 参数每次用于宏定义时,它们都将重新求值。由于多次求值,具有副作用的参数可能会产生不可预料的结果 | 参数在函数被调用前只求值一次。 在函数中多次使用参数并不会导致多个求值过程。参数的副作用并不会造成任何特殊的问题 |
参数类型 | 宏与类型无关。只要对参数的操作是合法的,它可以使用于任何参数类型 | 函数的参数是与类型有关的。如果参数的类型不同,就需要使用不同的函数,即使它们执行的任务是相同的 |
2.5 带副作用的宏参数
当宏参数在宏定义中出现的次数超过一次时,如果这个参数具有副作用,那么在使用这个宏时就可能出现危险。
副作用就是在表达式求值中出现的永久性效果。
x+1; //重复执行几百次结果都一样
x++; //具有副作用,它增加x的值。
#define MAX(a,b) ((a) > (b) ? (a) : (b))...x=5;y=8;z=MAX(x++,y++);printf("x=%d,y=%d,z=%d\n",x,y,z);
z = ((x++) > (y++) ? (x++) : (y++))
它的结果是x=6,y=10,z=9。
副作用并不仅限于修改变量的值。下面这个表达式:
getchar();
也具有副作用。调用这个函数将会消耗掉一个字符,所以该函数的后续调用将得到不同的字符。如果本意不是想"消耗"字符,就不能重复调用这个函数。
#define EVENPARITY(ch) ((count_one_bits(ch) & 1) ? (ch) | PARITYBIT : (ch))
这个宏的目的是产生一个具有偶校验位的字符。它首先计数字符中位1的个数,如果结果是一个奇数,PARITYBIT值(一个值为1的位)与该字符执行OR操作,否则该字符就保留不变。若以以下情况执行时
ch = EVENPARITY( getchar() ) ;
这条语句看上去很合理,但是它实际上读入了两个字符。
奇偶校验(parity)是一种错误检测机制。在数据被存储或通过通信线路传送之前,为一个值计算 (并添加)一个校验位,使数据的二进制模式中1的个数为偶数。以后,数据可以通过计算它的位1的个数来验证其有效性。如果结果是奇数,那么数据就出现了错误。这个技巧被称为偶校验(even parity)。奇校验(odd parity)的工作原理相同,只是计算并添加校验位之后,数据的二进制位模式中1的个数是奇数。
2.6 命名约定
一个常见的约定就是把宏名字全部大写。
value = MAX(a,b);
命名约定使MAX的身份一清二楚。如果宏使用可能具有副作用的参数,这个约定尤为重要,因为它可以提醒程序员在使用宏之前先把参数存储到临时变量中。
2.7 #undef
下面这条预处理指令用于移除一个宏定义:
#undef name
如果一个现存的名字需要被重新定义,那么首先必须用#undef
移除它的旧定义。
2.8 命令行定义
许多C编译器提供了一种能力,允许在命令行中定义符号,用于启动编译过程。当根据同一个源文件编译一个程序的不同版本时,这个特性是很有用的。
假定某个程序中声明了一个某长度的数组,但是一个机器的内存有限,我们需要一个很小的数组,但是另外一个机器的内存很大,我们需要一个较大的数组。
#include<stdio.h>int main(){int array[ARRAY_SIZE];int i=0;for(;i<ARRAY_SIZE;i++){array[i]=i;}for(i=0;i<ARRAY_SIZE;i++){printf("%d",array[i]);}printf("\n");return 0;}
而在linux命令行编译的时候,可以输入:
gcc -DARRAY_SIZE=10 programe.c
一般适用于根据同一个源文件编译出不同的一个程序的不同版本的时候。
2.9不能忽视宏第一中的空格
#define f (x) ((x)-1)
f代表的是(x) ((x)-1)
,因为f和(x)中间多了一个空格。
正确的形式:
#define f(x) ((x)-1)
练习: 表达式(x) ((x)-1)
能否成为一个合法的C表达式?
答:一种可能是, 如果x是类型名,例如x被这样定义:
typedef int x;
在这种情况下,
(x) ((x)-1)
等价于
(int) ( (int)-1)
这个式子的含义是把常数-1转换为int 类型两次。我们也可以通过预处理指令来定义x为一种类型,以达到同样的效果:
#define x int
另一种可能是当x为函数指针时。回忆一下, 如果某个上下文中本应需要函数而实际上用了函数指针,那么该指针所指向的函数将会自动地被取得并替换这个函数指针。因此,本题中的表达式可以被解释为调用x所指向的函数,这个函数的参数是(x)-1.为了保证(x)-1是个合法的表达式,x必须实际指向个函数指针数组中的某个元素。
x的完整类型是什么呢?为了方便讨论问题,我们假定x的类型是T,因此
可以如下声明x:
T x;
显而易见,x必须是一个指针, 所指向的函数的参数类型是T,这一点让T比较难以定义。下面是最容易想到的办法,但却没有用:
typedef void (*T) (T);
因为只有当T已经被声明之后,才能这样定义T!不过,x所指向的函数的参数类型并不一定要是T,它可以是任何T可以被转换成的类型。具体来说,void *
类型就完全可以:
typedef void (*T) (void *) ;
这个练习的用意在于说明,对于那些看上去无从着手、形式“怪异”的结构,我们不应该轻率地一律将其作为错误来处理。
2.10 宏不是类型定义
宏的一个常见用途是,使多个不同变量的类型可在一个地方说明:
#define FOOTYPE struct fooFOOTYPE a;FOOTYPE b,c;
宏的用法有一个优点——可移植性,得到了所有C编译器的支持。
但是,我们最好还是使用类型定义:
typedef struct foo FOOTYPE;
两种方法差不多,但是typedef更加通用一些。
#define T1 struct foo*typedef struct *T2;T1 a,b;T2 c,d;
第一个声明被拓展为了
struct foo * a,b;
在这个语句中,只有a被声明为了一个指向结构的指针,而b却被定义为一个结构(而不是指针)。但是,第二个声明将c,d定义为指向结构的指针。
3. 条件编译
条件编译(conditional compailation),使用条件编译,可以选择代码的一部分是被正常编译还是完全忽略。
#if constant-expression statements#endif
其中,constant-expression(常量表达式)由预处理器进行求值,如果它的值时非零值(真),那么statements部分就被正常编译,否则预处理器就静默的删除它们。
所谓常量表达式, 就是说它或者是字面值常量,或者是一个由#define 定义的符号。如果变量在执行期之前无法获得它们的值,那么它们出现在常量表达式中就是非法的,因为它们的值在编译时是不可预测的。
例如,将所有的调试代码都以下面这种形式出现:
#if DEBUG printf("x=%d,y=%d\n",x,y);#endif
如果想要编译他,只要令:
#define DEBUG 1
条件编译的另一个用途是在编译时选择不同的代码部分。
#if constant-expression statements#elif constant-expression other statements...#else other statements...#endif
#elif子句出现的次数可以不限。每个constant-expression(常量表达式)又有当前面所有的常量表达式的值都为假的时候才会被编译。
#else子句中的语句只有当前面所有的常量表达式的值都为假时才会被编译,在其他情况下则忽略。
3.1 是否被定义
测试一个符号是否被定义也是可能的。在条件编译中完成这个任务往往更为方便,因为如果程序并不需要控制编译的符号所控制的特性,就不需要定义符号。这个测试可以通过下列任何一种方式进行:
#if defined(symbol)#ifdef symbol
#if !defined(symbol)#ifndef symbol
每对定义的两条语句时等价的,但#if形式功能更强。因为常量表达式可能包含额外的条件,如下所示:
# if X > 0 || defined( ABC ) && defined( BCD )
3.2 嵌套指令
前面提到的语句还可以嵌套于另一个指令内部,如下所示:
#if defined(OS_UNIX) #ifdef OPTION1 unix_version_of_option1(); #endif #ifdef OPTION2 unix_version_of_option2(); #endif#elif defined(OS_MSDOS) #ifdef OPTION2 unix_version_of_option2(); #endif#endif
在这个例子中,操作系统的选择将决定不同的选项可以使用那些方案。这个例子说明了预处理器指令可以在他们呢前面添加空白,形成缩进,从而提高可读性。
可以为每个#endif加上一个注释标签,标签的内容就是#if(或#ifdef)后面的那个表达式。
#ifdef OPTION1 lengthy code for option1;#else lengthy code for alternative#endif /* OPTION1 */
4.文件包含
4.1 基本概念
#include指令使另一个文件的内容被编译,就像它实际出现于#include指令出现的位置一样。 这种替换执行的方式很简单:预处理器删除这条指令,并用包含文件的内容取而代之。这样,一个头文件如果被包含到10个源文件中,它实际上被编译了10 次。
这个事实意味着使用#include文件时会涉及一些开销,但基于两个十分充分的理由,我们不必担心这种开销。
-
首先,这种额外开销实际上并不大。如果两个源文件都需要同一组声明,把这些声明复制到每个源文件中所花费的编译时间跟把这些声明放入一个头文件,然后再用#include指令把它包含于每个源文件所花费的编译时间相差无几。
-
同时,这个开销只是在程序被编译时才存在,对运行时效率并无影响。
-
更为重要的是,把这些声明放于一个头文件中具有重要的意义。如果其他源文件还需要这些声明,就不必把这些副本逐一复制到这些源文件中,因此它们的维护任务也变得简单了。
当头文件被包含时,位于头文件内的所有内容都要被编译。这个事实意味着每个头文件只应过包含一组函数或数据的声明。和把一个程序需要的所有声明都放入一个巨大的头文件中相比,使用几个头文件,每个头文件包含用于某个特定函数或模块的声明的做法会更好一些。
程序设计和模块化的原则也支持这种方法。只把必要的声明包含于一个文件中会更好一些“是样文件中的语句就不会意外地访问应该属于私有的函数或变量。同时,这种方法使得我们也不空云在数百行不相关的代码中寻找所需要的那组声明,因此它们的维护工作也更容易一些。
4.2 函数库文件包含
编译器支持两种不同类型的#include文件包含:函数库文件和本地文件。事实上,他们的区别很小。
函数库头文件包含:
#include<filename>
对于filename,并不存在任何的限制,不过根据约定,标准库文件一般以.h后缀结尾。
从技术上来说,函数库头文件并不需要以文件的形式存储,但对于程序员而言,这并非显而易见。
4.3 本地文件包含
下面是#include指令的另一种形式
#include "filename"
标准允许编译器自行决定是否把本地形式的#include和函数库形式的#include区别对待。可以先对本地头文件使用一种特殊的处理方式,如果失败,编译器再按照函数库头文件的处理方式对它们进行处理。处理本地头文件的一种常见策略就是在源文件所在的当前目录进行查找,如果该头文件并未找到,编译器就像查找函数库头文件样在标准位置查找本地头文件。
可以在所有的#include指令中使用""
而不是<>
。但是这样在查找函数库头文件时科恩那个会浪费少许时间。对函数库头文件使用<>
能够提供一些信息。
#include<error.h>
显然引用的是一个函数库头文件。但是使用如下形式:
#include"error.h"
就无法弄清这个和上面相同的文件到底是一个函数库文件还是一个本地文件。想要弄清,只有检查执行编译过程的目录。
4.4 嵌套文件包含
完全可以在一个将被其他文件包含的文件中使用#include指令。
标准要求编译器必须至少支持8层的头文件嵌套,但它并没有限定嵌套的最大深度。事实上,我们并没有很好的理由让#include指令的嵌套深度超过一层或两层。
-
嵌套#include文件的一个不利之处在于它使得我们很难判断源文件之间的真正依赖关系。有些程序,如UNIX的make实用工具,必须知道这些依赖关系以便决定当某些文件被修改之后,哪些文件需要重新编译。
-
嵌套#include文件的另一个不利之处在于一个头文件可能会被多次包含。为了说明这种错误,考虑下面的代码:
#include "x.h"#include "x.h"
显然,这里文件
x.h
被包含了两次。没有人会故意编写这样的代码。但下面的代码:#include "a.h"#include "b.h"
看上设什么问题。如果
a.h
和b.h
都包含一个嵌套的#inlude
文件x.h
,那么x.h
在此处也同样出现了两次,只不过它的形式不是那么明显而已。
多重包含在绝大都属情况下出现于大型程序中,它们往往需要使用很多的头文件。解决办法是使用条件编译。
#ifdef _HEADERNAME_H#define _HEADERNAME_H 1/***All the stuff that you want in the header file*/#endif
当头文件第一次被包含的时候,他被正常处理,符号HEADERNAME_H
被定义为1.如果头文件被再次包含,通过条件编译,它的所有内容被忽略。
预处理器仍将读入整个文件,即使这个文件的所有内容被忽略。由于这种处理将拖慢编译速度,因此如果可能应该精良便面多重包含,不管他是否嵌套的#include文件导致的。
5.其他指令
5.1 #error指令
当程序编译之后,#error指令允许生成错误信息。
#error text of error message
示例:
#if defined(OPTION_A) stuff needed for option A#elif defined(OPTION_B) stuff needed for option B#elif defined(OPTION_C) stuff needed for option C#else #error NO option selected!#endif
5.2 #line指令
#line number "string"
它通知预处理器number是下一行输入的行号。如果给出了可选部分"string",预处理器就把它作为当前文件的名字。
这条语句将修改__LINE__
符号的值,如果加上可选部分,他还将修改__FILE__
符号的值。
5.3 #progma指令
#progma指令是另一种机制,用于支持因编译器而异的特性。它的语法也是因编译器而异。有些环境可能提供一些#progma 指令,允许些编译选项或其他任何方式无法实现的一 些处理方式。
例如,有些编译器使用#progma指令在编译过程中打开或关闭清单显示,或者把汇编代码插入到C程序中。从本质上说,#progma 是不可移植的。预处理器将忽略它不认识的#progma指令,两个不同的编译器可能以两种不同的方式解释同条#progma 指令。
5.4 无效指令
最后,无效指令(null directive) 就是以#符号开头,但后面不跟任何内容的一行。这类指令只是被预处理器简单地删除。下面例子中的无效指令通过把#include与周围的代码分隔开来,凸显它的存在:
##include <stdio.h>#
我们也可以通过插入空行取得相同的效果。
6.总结
- 避免使用#define指令定义可以用函数实现的很长序列的代码。
- 在那些对表达式求值的宏中,每个宏参数出现的地方都应该加上括号,并且在整个宏定义的两边也加上括号。
- 避免使用#define宏创建一种 新语言。
- 采用命名约定,使程序员很容易看出某个标识符是否为#define宏。
- 只要合适就应该使用文件包含,不必担心它的额外开销。
- 头文件只应该包含一组函数和(或)数据的声明。
- 把不同集合的声明分离到不同的头文件中可以改善信息隐蔽性。
- 嵌套的#include文件使我们很难判断源文件之间的依赖关系。
7.习题
-
putchar
函数定义与头文件stdio.h
中,景观他的内容比较长,但他是作为一个宏实现的。你认为他为什么以这种方式定义? 答:因为
putchar
经常被调用,所以调用速度被认为是最重要的。将它实现为宏可以消除函数调用的开销。 -
下面这段代码是否有错?如果有,错在何处?
#if sizeof(int)==2 typedef long int32;#else typrdef int int32;#endif
答:
sizeof
是在预处理器完成其工作后评估的,这意味着这将不起作用。 另一种方法是使用包含文件<limits.h>中定义的值。 -
下面这段代码是否有错误?若有,错在何处?
在文件header1.h中:
#ifndef _HEADER1_H#define _HEADER1_H#include "header2.h"...#endif
在文件header2.h中:
#ifndef _HEADER2_H#define _HEADER2_H#include "header1.h"...#endif
答:没有错误。 它们每个都包含另一个,并且首先看起来编译器将交替读取它们,直到达到其包含嵌套限制。 事实上,由于条件编译指令,这不会发生。 无论包含哪个文件,首先定义它自己的符号,然后导致包含另一个文件。 当它再次尝试包含第一个时,将跳过整个文件。