编译一个C程序涉及很多步骤,其中第一个步骤被称为处理阶段(preprocessing)。C预处理器在源代码编译之前对其进行一些文本类性质的操作,它的主要任务包括删除注释、插入被#include包含的文件的内容、定义和替换由替换由#define指令定义的符号以及确定代码的部分内容是否应该根据一些条件编译指令进行编译。
下表总结了预处理器定义的符号。他们的值可以是,字符串常量,或者是十进制数字常量。
define的简单用法就是为数值明明为一个符号。下面观察一下它更为正式的用途。
#define name stuff
有了这条指令之后,每当有name出现在这条指令后面时,预处理器就会把它替换成功stuff。
替换文本并不仅限于数值字面值常量,使用#define指令可以把任何文本替换到程序中,下面有几个例子:
#define reg register
#define do_forever for(;;)
#define CASE break;case
第一个定义为关键字register创建了一个更简短的别名,这个较短的名字使各个声明更容易通过制表符进行排列。第二个声明用一个更具有描述性的符号来代替一种用于实现无限循环的for语句类型。最后一个#define**定义了一种简短记法,以便在switch语句中使用**。它自动的把一个break放在每个case之前。
如果定义的stuff非常长,他可以分成几行,除了最后一行之外,每行的末尾都要加一个反斜杠,如下面的例子:
#define DEBUG_PRINT printf("File %s line %d:"\
" x = %d, y = %d, z = %d:,\ __FILE__, __LINE__,\ x,y,z);
注意,不要在宏定义的尾部加上分号,这样的话就会产生两条语句,而有些场合只能出现一条语句,例如if语句之类的语句。
不要滥用#define指令,如果相同的代码需要出现在程序的几个地方,最好的方法是把它实现为一个函数。
2.1宏
#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或者宏定义,下面是宏的声明方式:
#define nname(parameter-list) stuff
其中,parameter-list(参数列表)是一个由逗号分号分隔的符号列表,他们可能出现在stuff中,参数列表的左括号必须与name紧邻,如果有空白存在就被解释为stuff的一部分
例子:
#define SQUARE(x) x * x
上述声明之后,如果有这样一句:SQUARE(5),预处理器就用下面这个表达式替换:5*5
注意,观察下面的代码段:
a = 5;
printf("%d\n",SQUARE( a + 1));
这个代码段打印的值是11,因为参数x被文本a+1替换了:
printf("%d\n", a + 1 * a + 1 );
如果把宏定义改为下=下面这样就解决了:
#define DOUBLE(x) (x) + (x)
a = 5;
printf("%d\n", 10 * DOUBLE ( a ));
上面这个代码段,结果是55,替换的代码段是:
printf("%d\n", 10 * (a) + (a));
所以在定义宏的时候,在整个表达式两边加上括号就行:
#define DOUBLE(x) ((x) + (x))
所有用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时,由于参数中的操作符或邻近的操作符之间不可预料的相互作用
2.2宏替换
在程序中扩展#define定义符号和宏时,需要涉及几个步骤:
1.调用宏时,首先对参数进行检查,看看是否包含了任何由#define定义的符号,如果是,它们首先被替换。
2.替换文本随后被插入到程序中原来文本的位置,对于宏,参数名被它们的值所替代
3.最后,再次对结果文本进行扫描,看看它是否包含了任何由#define定义的符号,如果是,就重复上面的步骤。
注:
当预处理器搜索#define定义可以包含其他#define定义的符号宏,不可以出现递归
##结构执行一种不同的任务,它把位于它两边的符号连接成一个符号。下面这个例子用这种连接把一个值添加到几个变量之一:
#define ADD_TO_SUM(sum_number, value ) \
sum##sum_number += value
ADD_TO_SUM(5, 25)
上面这条语句把值25加到变量sum5,注意这种连接必须需产生一个合法的标识符,否则其结果是未定义的
2.3宏与函数
宏非常频繁的用于执行简单的计算,比如在两个表达式中寻找其中较大(或较小)的一个:
#define MAX( a, b ) ((a) > (b) ? (a) : (b))
为什么上述操作不用函数呢,首先函数调用和从函数返回的代码可能比世纪执行这个计算工作的代码更大,所以宏比使用函数在程序的规模和速度方面都更胜一筹。
更重要的是函数的参数声明必须为一种特定的类型,反之宏可以用于整型、长整型、单浮点数、双浮点数以及其他任何可以用操作符比较值大小的类型,宏是与类型无关的。
2.4带副作用的宏参数
当宏参数在宏定义中出现的次数超过一次时,如果这个参数具有副作用,那么当你使用这个宏时就可能出现危险。副作用就是在表达式求值时出现永久性效果,例如:
x++;
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);
上面这个表达式的最终结果是:
x = 6, y = 10, z = 9;
虽然那个较小的值只自增了一次,但那个较大的值却增值了两次——第一次是在比较时,第二次是在执行?符号后面的表达式时出现。
2.5命名约定
宏一般都用大写字母。
下表示宏和函数的不同之处:
2.6#undef
这条预处理指令用于移除一个宏定义。
#undef name
如果一个现存的名字需要被重新定义,那么它的旧定义I首先必须用#undef移除。
2.7命令行定义
许多C编译器都允许我们在命令行中定义符号,用于启动编译过程,当我们根据同一个源文件编译一个程序的不同版本时,这个特性就很有用。
如果某个数组类似下面的形式进行声明:
int array[ARRAY_SIZE];
那么在编译程序时,ARRAY_SIZE的值可以在命令行中指定
在UNIX中,-D可以玩车工这项任务
-Dname
-Dname = stuff
在需要引用数组长度时,使用符号常量
使用条件编译,可以在调试程序时,可以选择代码的一部分是被正常编译还是被完全忽略。
用于支持条件编译的基本结构是#if指令和与其匹配的#endif指令,下面是语法形式:
#if constant-expression
statements
#endif
常量表达式由预处理器进行求值,如果它的值是非0值,那么statements部分就被正常编译,否则预处理器就删除这部分代码。
#if指令还具有可选的#elif和#else字句,完整的语法如下所示:
#if constant-expression
statementss
#elif constant-expression
other statements...
#else
other statements
#endif
#elif语句可以有许多个。
3.1是否被定义
代码示意:
#if define(symbol)
#ifdef symbol
#if !defined(symbol)
#define symbol
每对定义的两条语句是等价的但是#if的形式功能更强,因为常量表达式可能包含额外的条件
3.2嵌套指令
前面提到的这些指令可以嵌套于另一个指令内部,如下面的代码所示:
#if define(ps_UNIX)
#ifdef OPTION1
unix_version_of_option1();
#enif
#ifdef OPTION2
unix_version_of_option2();
#enif
#elif define(OS_MSDOS)
#define OPTION2
msdos_version_of_option2();
#endif
#endif
这就是嵌套使用的一个例子
#include指令使另一个文件的内容被编译,这种替换方式很简单:预处理器删除这条指令,并用于包含文件的内容取而代之。
4.1函数库文件包含和本地文件包含
编译器支持两种不同类型的#include文件包含:函数库文件和本地文件。事实上,它们之间的区别很小
#include
#include"filename.h"
4.2嵌套文件包含
在一个将被其他文件包含的文件中使用#include指令是可能的,标准要求编译器必须支持至少八层的头文件嵌套,但它并没有限定嵌套深度的最大值。
多重包含在绝大多数情况下出现于大型程序中,解决这个问题时往往使用条件编译,如果所有的头文件都像下面这样编写:
#ifndef _HEADERNAME_H
#define _HEADERNAME_H 1
#endif
如果可能,应该避免出现多重包含,不管是否由于嵌套头文件导致
预处理器还支持其他一些指令,例如程序编译之后,#error指令允许生成错误信息,例如:
#error text of error message
还有一种用途较小的#line指令,它的形式如下:
#line number"string"
它通知预处理器number是下一行是输入的行号,如果给出了可选部分“string”,预处理器就把它作为当前文件的名字。这条指令将修改_LINE_ 符号的值。
#pragma指令是另一种机制,用于支持因编译器而异的特性,语法也因为编译器而异。
最后,无效指令(null directive)就是一个#符号开头,但后面的不跟任何内容的一行。这类指令只是被预处理器简单的删除
1.不要在一个宏定义的末尾加上分号,使其成为一条完整的语句
2.在宏定义中使用参数,但忘了在他们周围加上括号
3.忘了在整个宏定义的两边加上括号
4.避免用#define指令定义可以用于函数实现的很长序列的代码
5.避免使用#define宏创建一种新语言
6.只要合适就应该使用文件包含,不必担心额外开销
7.头文件应该只包含一组函数或者数据的声明
8.采用命名约定,避免程序员看不出某个标识符是否为#define宏