英文原版:P315
诸如#define
、#include
等预处理指令都是由预处理指令是由预处理器来处理的。
预处理器是一个在编译前对C程序进行编辑的小型软件。
C语言对预处理器的依赖使其在主流编程语言中显得与众不同。
预处理器虽然是一个功能强大的工具,但也可能产生许多难发现的错误。而且,预处理器很容易被误用来编写几乎读不懂的程序。因此,建议:不要过分依赖预处理器,请适度地使用。
本章的主要内容
- 14.1节描述预处理器的工作原理。
- 14.2节给出一些影响所有预处理指令的通用规则。
- 14.3节介绍预处理的宏定义功能。
- 14.4节介绍预处理器条件编译功能。
- 14.5节介绍较少被使用的预处理指令:
#error
、#line
、#progma
。
14.1 预处理器是如何工作的?
什么是预处理指令?
以
#
开头的指令,末尾不包含分号``;
预处理指令的工作原理
- 输入:C源程序
- 输出:源程序的修改版本,该版本不包含预处理指令
- 功能:执行预处理指令,并移除它们。
注意事项:
- 预处理指令的末尾不包含分号
;
例1 celsius.c
/* Converting a Fahrenheit temperature to Celsius */
#include <stdio.h>
#define FREEZING_PT 32.0f
#define SCALE_FACTOR (5.0f/9.0f)
int main(void)
{
float fahrenheit, celsius;
printf("Enter Fahrenheit temperature: ");
scanf("%f", &fahrenheit);
celsius = (fahrenheit - FREEZING_PT) * SCALE_FACTOR;
printf("Celsius equivalent: %.1f\n", celsius);
return 0;
}
生成.i
文件
gcc -E celsius.c -o celsius.i
文件celsius.i
空行
空行
从stdio.h中引入的行
空行
空行
空行
空行
int main(void)
{
float fahrenheit, celsius;
printf("Enter Fahrenheit temperature: ");
scanf("%f", &fahrenheit);
celsius = (fahrenheit - 32.0f) * (5.0f/9.0f);
printf("Celsius equivalent: %.1f\n", celsius);
return 0;
}
解释:
在这个例子中,预处理器都做了哪些事?
- 预处理器执行
#include
指令:- 引入stdio.h的内容
- 预处理器执行
#define
指令:- 移除
#define
指令; - 将源文件中出现
FREEZING_PT
和SCALE_FACTOR
的地方替换为宏定义的值;
- 移除
- 用空格符来替换注释
14.2 预处理指令都遵循哪些规则?
常见的预处理指令有哪几类?
- 宏定义
可使用#define
来定义一个宏;
可使用#undefine
来移除一个宏的定义; - 文件包含
可使用#include
来让一个程序包含某个具体文件的内容; - 条件编译
基于预处理器测试的条件,可使用#if
、#ifdef
、#ifndef
、#elif
、#else
、#endif
来将一个文本块包含到程序中,或者从程序中移除一个文本块。
所有的预处理指令都遵循的一些规则:
- 指令都以
#
号开头; - 在一个指令的tokens间可放入任意数量的空格符和水平制表符;
- 指令总是在第一个换行符处结束,除非显式地指出
- 指令可出现在程序中的任何地方;
- 注释可以和指令在同一行里;
例1 需要多行的单条指令
#define DISK_CAPACITY (SIDES * \
TRACKS_PER_SIDE * \
SECTORS_PER_TRACK * \
BYTES_PER _SECTORS)
例2 在宏的定义后面加上注释
#define FREZZING_PT 32.0f //水的凝固点
14.3 宏的定义
如何定义宏?
宏定义注意事项:
不要在宏定义里添加额外的字符
错误示例1 在宏定义里放等号=
#define N = 100
...
int a[N];//等价于int a[=100]
错误示例2 在宏定义里放分号;
#define N 100;
...
int a[N];//等价于int a[100;]
不带参数的宏
格式:
#define indentifier replace-list
常用情形:
给数值常量、字符常量、字符串字面量起个别名
解释:
- 替换列表可以包含标识符、关键字、数值常量、字符常量、字符串字面量、操作符及标点符号等。
- 当预处理器碰到一个宏定义时,它会做下记录:标识符代表替换列表。之后,不管在文件哪里出现了标识符,则预处理器就会用替换列表代替。
例1 无参数宏定义
#define STR_LEN 80
#define TRUE 1
#define FALSE 0
#define PI 3.14159
#define CR '\r'
#define EOS '\0'
#define MEM_ERR "Error: not enough memeory"
使用宏来给常量起别名有若干个好处:
- 使程序更易读;
- 使程序更易修改;
- 避免出现不一致错误和输入错误;
- 对C语法做最小的修改;
- 重命名一些类型;
- 控制条件编译;
带参数的宏
格式:
- #define identifier(
,
, ...,
) replace_list
-
,
,...,
是宏的参数,宏的参数可在替换列表中出现多次;
-
在宏名字和左括号之间没有空格;如果有空格,则预处理器假设该宏是不带参数的宏,且将(
,
,...,
)看做是替换列表的一部分;
宏定义、宏调用
预处理器是如何处理带参数宏的?
- 当预处理器遇见一个带参数宏的定义时,它会存储该定义以备后续使用。
- 诸如identifier(
,
,...,
)形式的宏调用,后续无论出现在程序哪里,预处理器都会用替换列表来替换它,比如
替换
,
替换
等。
带参数的宏的用法:
- 充当简单的函数
例1 宏定义
#define MAX(x,y) ((x)>(y):(x),(y))
解释:
- 宏
MAX
没有运行时开销。 - 宏
MAX
支持泛型,即可使用宏MAX
来找出任意类型的两个值的较大者,比如int
、long
、float
、double
等。
例1 宏定义
#include <stdio.h>
#define MAX(x,y) ((x)>(y):(x),(y))
#define IS_EVEN(n) ((n)%2==0)
int main(void)
{
int i;
i = 98;
if(IS_EVEN(i)){
i++;
}
printf("%d\n", i);
MAX(100,34);
return 0;
}
例2 复杂点的带参数宏定义
#define TOUPPER(c) ('a'<(c)&&(c)<='z'?(c)-'a'+'A':(c))
例3 空参数列表的带参数宏定义
#define getchar() getc(stdin)
函数调用都有哪些运行时开销?
- 保存上线文信息;
- 拷贝参数;
如何避免函数调用的运行时开销?
- 使用带参数的宏;
- 声明一个函数是
inline
;
带参数的宏 VS 真正的函数
优点:
- 没有运行时开销;
- 宏是泛型的;
函数调用在程序执行过程中是有开销的,比如保存上下文信息、拷贝参数等。
宏调用没有运行时开销。
宏支持泛型
宏的形参跟函数形参不一样,没有特定的类型。
宏可接受任意类型的实参
带参数宏的缺点有哪些?
导致编译后的代码过长。
每个宏调用都会导致在源代码中插入宏的替换列表,因此会增加源代码的长度。
宏使用的次数越多,这种效果越明显。特别是当嵌套进行宏调用时,效果会加倍。
编译器不对宏调用实参进行类型检查,也不会发生类型转换。
当函数调用发生时,编译器会挨个检查实参来看其是否有合适的类型。如果存在某个实参不是合适的类型,则该实参就会被转换成合适的类型,或者发出一条错误消息。
不存在指向宏的指针
存在指向函数的指针
因为宏在预处理后就被移除了。
宏调用可能对实参求值多次
函数调用只会对实参求值一次
如果实参有副作用的话,则对一个参数求值多次会出现不在期望内的行为。
带参数的宏
- 模拟函数
- 重复代码段
带参数的宏注意事项:
对宏实参多次求值导致的错误是很难被发现的,因为宏调用跟函数调用看起来是一样的。
更糟的是,一个宏在大部分时候是正常工作的,仅对某些有副作用的参数会有错误。
最好不要使用有副作用的实参。
14.4 条件编译指令
-
#if
指令和#endif
指令 -
ifdef
指令和ifndef
指令 -
#elif
指令和#else
指令
知识补充
什么是条件编译?
根据其执行的测试的结果,预处理器可包含或者移除一段程序文本。
#if
指令和#endif
指令
格式:
#if 常量表达式
....
#endif
预处理器是如何处理#if
指令和#endif
指令的?
当遇见#if
指令时,预处理器会对常量表达式求值。
- 如果该表达式的值是0,就移除在
#if
和#endif
之间的代码。 - 否则,就保留在
#if
和#endif
之间的代码,待编译器处理。
注意事项:
如果在#if
中出现未定义的宏,则#if
指令会默认该未定义的宏的值是0。
例1 保留测试代码,且让编译器忽略这些代码
#include <stdio.h>
#define DEBUG 1
int main(void)
{
int i, j;
i = 100;
j = 98;
#if DEBUG
printf("Value of i : %d\n", i);
printf("Value of j : %d\n", j);
#endif /** DEBUG*/
return 0;
}
defined
运算符
defined
运算符是预处理器特有的运算符。
功能:
- 如果该标识符是个当前已被定义的宏,则该defined表达式的值就是1,否则是0。
例2 defined运算符
#include <stdio.h>
//注意这里没有给DEBUG定义一个值,但是预处理后的文件中仍包含了printf语句
#define DEBUG
int main(void)
{
int i, j;
i = 100;
j = 98;
// defined只是测试DEBUG是否已被定义过
#if defined(DEBUG)
printf("Value of i : %d\n", i);
printf("Value of j : %d\n", j);
#endif /** DEBUG*/
return 0;
}
ifdef
指令和ifndef
指令
#ifdef
- 用于测试一个标识符是否已被定义为一个宏
#ifndef
- 用于测试一个标识符是否未被定义为一个宏
例1 等价关系
#ifdef 标识符
等价于
#if defined(标识符)
例2 等价关系2
#ifndef 标识符
等价于
#if !defined(标识符)
#elif
指令和#else
指令
为了提供多一点便利,预处理器还支持:#elif
指令和#else
指令
格式:
#elif 常量表达式
#else
网友评论