C预处理器在源代码编译之前对其进行一些文本性质的操作。它的主要任务包括删除注释,插入被#include指令包含的文件的内存,定义和替换由#define指令定义的符号以及确定代码的部分内容是否应该根据一些条件编译指令进行编译。
14.1 预定义符号
__FILE__ 进行编译的源文件名 __LINE__ 文件当前行的行号 __DATE__ 文件被编译的日期 __TIME__ 文件被编译的时间 __STDC__ 如果编译器遵循ANSI C,其值为1,否则未定义14.2 #define
#define name stuff每当有符号name出现在这条指令后面时,预处理器就会把它替换成stuff。
使用#define指令,你可以把任何文本替换到程序中。
#define DEBUG_PRINT printf("File %s line %d:"\ " x = %d, y = %d, z = %d",\ __FILE__, __LINE__, x, y, z )当我们调试一个存在许多设计一组变量的不同计算过程时,这种类型的声明非常有用。
x *= 2; y += x; z = x * y; DEBUG_PRINT;警告:这条语句在DEBUG_PRINT后面加了一个分号,所以你不应该在宏定义的尾部加上分号。如果这样做,就产生了两条语句---一条printf语句后面再加一条空语句。如果有些场合只要求一条语句的话,就会出现问题:
if ( ... ) DEBUG_PRINT; else备注:不要滥用这种技巧。如果相同代码需要出现在程序的几个地方,通常更好的方法是把它实现为一个函数。
14.2.1 宏
#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏。
#define name(parameter-list) stuff来看两个例子:
1.
#define SQUARE(x) x * x则:
a = 5; SQUARE( a + 1 );输出多少?实际上为11--->5 + 1 * 5 + 1 = 11.
所以,我们应该修改宏定义:
#define SQUARE(x) (x) * (x)2.
#define DOUBLE(a) (a) + (a)这个定义貌似正确的,但是下面表达式输出多少?
a = 5; 10 * DOUBLE(a);实际上输出55--->10 * 5 + 5 = 55.
所以我们应该修改宏定义:
#define DOUBLE(a) ((a) + (a))14.2.2 #define替换
在程序中扩展#define定义符号和宏时,需要以下几个步骤:
1. 在调用宏时,首先对参数进行检查,看看是否包含了任何由#define定义的符号。如果是,则它们首先被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被它们的值所替代。
3. 最后,再次对结果文本进行扫描,看看它是否包含了任何由#define定义的符号。如果是,就重复上述过程。
这样,宏参数和#define定义可以包含其他#define定义的符号。但是,宏不可出现递归。
宏定义有两个技巧:
1. 字符串的自动拼接
#include <stdio.h> #define PRINT(FORMAT, VALUE) printf("the value is " FORMAT "\n", VALUE ) int main(void) { PRINT("%d", 3); return 0; }程序输出:
2. 宏参数替换为一个字符串
1) #argument这种结构被预处理器翻译为“argument”:
#include <stdio.h> #define PRINT(FORMAT, VALUE) printf("the value of " #VALUE " is " FORMAT "\n", VALUE) int main(void) { int x = 5; PRINT("%d", x + 3); //这里VALUE应该是个变量,而不是一个常量 return 0; }程序输出:
2) ##把位于它两边的符号连接成一个符号:
#include <stdio.h> #define PRINT(FORMAT, VALUE) printf("the value of " #VALUE " is " FORMAT "\n", VALUE##VALUE) int main(void) { char *xx = "hello world"; PRINT("%s", x ); return 0; }这个例子好丑陋,程序输出:
14.2.3 宏与函数
宏非常频繁的用于执行简单的计算:
#define MAX( a, b ) ( ( a ) > ( b ) ? ( a ) : ( b ) )为什么不用函数来完成这个任务呢?原因有两个:
1. 用于调用和从函数返回的代码很可能比实际执行这个小型计算工作的代码更大,所以使用宏比使用函数在程序的规模和速度方面都更胜一筹。
2. 更重要的是:函数的参数必须声明为一种特定类型,所以它只能在类型合适的表达式上使用。反之,宏可以应用于任何的数据类型。
但是宏相比于函数有以下缺点:
1. 每次使用宏的时候,都是将宏的代码拷贝到程序中去。如果宏的代码量大,则大幅度增加程序的长度。
还有一些用函数无法办到但是用宏可以办到:以下程序的宏的第二个参数是一种类型,它无法作为函数参数进行传递:
#define MALLOC( n, type ) ( ( type * )malloc( ( n ) * sizeof( type ) ) )
则:
pi = MALLOC( 25, int );
被转换为:
pi = ( ( int *) malloc( ( 25 ) * sizeof( int ) ) );14.2.4 带副作用的宏参数
副作用就是在表达式求值时出现的永久性效果。
#include <stdio.h> #define MAX( a, b ) ( ( a ) > ( b ) ? ( a ) : ( b ) ) int main(void) { int x = 5; int y = 8; int z = 0; z = MAX( x++, y++ ); printf("x = %d, y = %d, z = %d\n", x, y, z ); return 0; }程序输出:
输出有点不可思议,但我们用宏定义替换一下便知道答案:
z = ( ( x++ ) > ( y++ ) ? ( x++ ) : ( y++ ) );14.2.5 命名约定
1. 代码长度:
宏定义:每次使用时,宏代码都被插入到程序中,除了非常小的宏之外,程序的长度将大幅度增长
函数:函数代码只出现于一个地方,每次使用这个函数时,都调用那个地方的同一份代码。
2. 执行速度
宏定义:更快
函数:存在函数调用/返回的额外开销
3. 操作符优先级
宏定义:宏参数的求值是在所有周围表达式的上下文环境里,除非它们加上括号,否则邻近操作符的优先级可能会产生不可预料的结果。
函数:函数参数只在函数调用时求值一次,它的结果值传递给函数,表达式的求值结果更容易预测。
4. 参数求值
宏定义:参数每次用于宏定义时,它们都将重新求值。由于多次求值,具有副作用的参数可能会产生不可预料的结果。
函数:参数在函数被调用前只求值一次,在函数中多次使用参数并不会导致多种求值过程,参数的副作用并不会造成任何特殊的问题。
5. 参数类型
宏定义:宏于类型无关,只要对参数的操作时合法的,它可以使用与任何参数类型。
函数:函数的参数是与类型有关的。如果参数的类型不同,就需要使用不同的函数,即使它们执行的任务是相同的。
14.2.6 #undef
这条预处理指令用于移除一个宏定义
#undef name如果一个现存的名字需要被重新定义,那么它的旧定义首先必须用#undef移除。
14.3 条件编译
条件编译的一大作用是:我们可以控制程序中哪些语句需要编译:
#if DEBUG printf("x = %d, y = %d\n", x, y ); #endif如果我们想DEBUG这段代码,则只要:
#define DEBUG 1当然,条件编译支持多种判断:
#if constant-expression statements #elif constant-expression other statements #else other statements #endif14.3.1 是否被定义
#if defined(symbol) #ifdef symbol #if !defined(symbol) #ifndef symbol#if的形式功能更强一些,因为常量表达式可能包含额外的条件:
#if X > 0 || defined( ABC ) && defined( BCD )14.5 其他指令
1. #error
#error指令允许你生成错误信息。
#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! #endif2. #line
#line number "string"它通知预处理器number是下一行输入的行号。如果给出可选部分“string”,预处理器就把它作为当前文件的名字。这条指令修改了__LINE__和__FILE__.
3. #progma
支持编译器而异的特性
4. #
代表无效指令:以#开头,后面不跟任何内容的一行。
总结:
1. 不要在一个宏定义的末尾加上分号,使其成为一条完整的语句。
2. 在宏定义中使用参数,但忘了在它们周围加上括号。
3. 忘了在整个宏定义的两边加上括号。
习题:
1.
void print_ledger( int argument ) { #if defined( OPTION_LONG ) print_ledger_long( argument ); #elif defined( OPTION_DETAILED ) print_ledger_detailed( argument ); #else print_ledger_default( argument ); #endif }
2.
int cpu_type() { int count = 0; #if defined( VAX ) count++; #if defined( M68000 ) count++; #if defined( M68020 ) count++; #if defined( I80386 ) count++; #if defined( X6809 ) count++; #if defined( X6502 ) count++; #if defined( U3B2 ) count++; if ( 0 == count ){ return CPU_UNKNOWN; } else ( 1 != count ){ return NULL; } #if defined( VAX ) return CPU_VAX; #if defined( M68000 ) return CPU_68000; #if defined( M68020 ) return CPU_68020; #if defined( I80386 ) return CPU_80386; #if defined( X6809 ) return CPU_6809; #if defined( X6502 ) return CPU_6502; #if defined( U3B2 ) return CPU_3B2; }