一、预处理器工作原理
预处理器执行预处理指令,并在处理过程中删除这些指令。
二、预处理指令
2.1 指令类型
宏定义 —— #define
定义一个宏,#undef
删除宏
文件包含 —— #include
将指定文件包含进程序中,如各种头文件(***.h
)
条件编译 —— #if
、#ifdef
、#ifndef
、#elif
、#else
、#endif
根据测试条件确定包含还是排除一个文本块。
# //空指令,合法
2.2 指令通用规则:
- 指令以
#
开头。无需位于行首,但要求前面有空白字符。 - 指令符号间空格数量任意。 如
# define N 50
,包括 Tab 符。 - 指令在换行后结束。
//指令续行
#define DISK_CAPACITY (SIDES * \
TRACKS_PER_SIZE * \
SECTORS_PER_TRACK * \
BYTES_PER_SECTOR)
- 指令在程序中的位置任意。通常在文件的开始,也可以放在后面,甚至函数定义的中间。
- 指令行可添加注释
三、宏定义
3.1 对象式宏(无参数宏)
3.1.1 格式
格式: [#define 指令 (对象式宏)]
#define 标识符 替换列表
替换列表是预处理标记,预处理器将程序代码中的标识符用替换列表替换。
替换列表可包括:标识符、关键字、数值常量、字符常量、字面串、运算符、标点符号。
3.1.2 应用举例
主要定义“明示常量”(manifest constant),如给 数值、字符值、字符串值 命名。
#define STE_LEN 80
#define TRUE 1
#define FALSE 0
#define PI 3.14159
#define CR '\r'
#define EOS '\0'
#define MEM_ERR "Error: not enough memory"
#define FORMAT "%f\n"
建议定义为宏的字符或字符串:
- 常量会被多次使用;
- 以后可能需要修改常量的值。
3.1.3 优点
- 提高代码易读性
- 便于修改
- 避免相同内容前后不一致,减少出错
- 小幅修改C语法,提高编写自由度,但可能提高阅读难度
#define BEGIN {
#define END }
#define LOOP for(;;)
- 类型重命名
#define BOOL int
3.1.4 注意事项
-
宏定义不得添加额外符号
-
宏定义句尾不得添加分号或其他无关内容(注释除外)
#define M = 100 //错误
#define N 100; //错误
#define N 100 2 //错误
...
int a[M]; // 等价于 int a[= 100];
int b[N]; // 等价于 int b[100;];
- 替换列表可以为空
#define N
合法 - 宏不会重复替换,只会替换一次(避免陷入死循环)
#define N (2*M)
#define M (N+1)
...
i = N; //结果是i = (2*(N+1));
3.2 函数式宏(含参数宏)
3.2.1 格式
格式: #define 标识符(x1,x2,...,xn) 替换列表
注意:
- 标识符(宏的名称)与
(
之间不得有空格,否则会被识别为对象式宏,(x1,x2,…,xn) 成为替换列表的一部分; - 整个宏的替换列表需要被圆括号括起来;
- 替换列表中的任何变量都需要被圆括号括起来。
- 宏定义的结尾不要加分号。
举例1:
#define MAX(x,y) ((x)>(y)?(x):(y))
#define IS_EVEN(n) ((n)%2==0)
#define PRINT_INT(n) printf("%d\n", n)
...
i = MAX(j+k,m-n); //等价于i = ((j+k)>(m-n)?(j+k):(m-n));
if(IS_EVEN(i)) //等价于if((i)%2==0)
PRINT_INT(i/j); //等价于printf("%d\n", i/j);
举例2:
#define PRINT(s) printf("s\n"); //错误写法,容易出错
...
if(i>1)
PRINTF(yes);
else
PRINTF(no);
/*等价于:
if(i>1)
printf("yes\n"); //程序报错
;
else
printf("no\n");
;
*/
3.2.2 实际参数为空
- 与函数不同,含参数的宏允许实际参数为空
//举例1:
#define ADD(x,y) (x+y)
...
i = ADD(,k); //等价于 i = (+k);
//举例2:
#define MK_STR(x) #x //#不是运算符,不必加圆括号
...
char empty_str[] = MK_STR(); //等价于 empty_str[] = ""; 初始化为空串
//举例3:
#define JOIN(x,y,z) x##y##z
...
int JOIN(a,b,c), JOIN(a,b,), JOIN(a,,c), JOIN(,,c);
//等价于 int abc, ab, ac, c;
3.2.3 参数个数可变的宏
#define TEST(condition, ...) ( (condition)? \
printf("Passed test: %s\n", #condition) : \
printf(__VA_ARGS__) )
...
TEST(voltage <= max_voltage, "Voltage %d exceeds %d\n", voltage, max_voltage);
/*等价于
( (voltage <= max_voltage)?
printf("Passed test: %s\n", "voltage <= max_voltage") :
printf("Voltage %d exceeds %d\n", voltage, max_voltage) );
*/
说明:
__VA_ARGS__
为专用标识符,只能出现在可变参数的宏的替换列表中,代表所有与省略号相同的参数。预处理指令执行后,程序语句中的省略号的内容将替换到__VA_ARGS__
的位置。
3.2.4 带参数宏的优缺点
- 优点:
- 程序运行更快,省去函数调用的额外步骤;
- 更通用,参数不限制类型
- 缺点:
- 编译后代码量变大(使用直接替换的方式);
- 参数没有类型检查,类型出错不会报错,不会类型转换;
- 无法用指针指向一个宏;
- 宏可能会多次计算其参数,且这类错误难于发现,例如:
# define MAX(x,y) ((x)>(y)?(x):(y))
...
n = MAX(i++, j);
//n = ((i++)>(j)?(i++):(j)); i自增了两次
3.3 #
和##
运算符
3.3.1 #
运算符
#
将宏的一个参数转换为字面串(“串化”);- 仅允许出现在带参数的宏的替换列表中;
#a
中,若 a 为宏参数,则将 a 替换为实际参数之后再串化,否则直接串化为"a"。
举例:
#define PRINT_INT(n) printf(#n " = %d\n", n) //将参数n转换为字面串
...
PRINT_INT(i/j);
//等价于: printf("i/j" " = %d\n", i/j);
//相邻字面串合并: printf("i/j = %d\n", i/j);
对于已经包含"
或\
的参数:
"
—替换为—>\"
\
—替换为—>\\
- 在最外侧加上
""
成为字符串。
3.3.2 ##
运算符
##
将两个记号“粘合”在一起,使之成为一个记号;- 若操作数含有宏参数,则在参数被实际参数替换后再“粘合”
#define MK_ID(n) i##n //i与n粘合 --> in (n被替换后)
...
int MK_ID(1), MK_ID(2), MK_ID(3); //等价于:int i1, i2, i3;
3.2.2.1 说明
- 当
##
连接形式参数时,前后可间隔空格,如:
#define MK_ID(n) i ## n //##前后间隔空格,且 n 为参数,因此仍然合法有效
- 替换列表中依赖
##
的宏通常不能嵌套调用。
#define CONCAT(x,y) x##y
...
CONCAT(a,CONCAT(b,c)); //会出错, aCONCAT(b,c)无法识别
3.3.2.2 应用举例
举例1:
//应对未知参数类型的函数
#define MAX(type) \
type type##_max(type x, type y) \
{ \
return x > y ? x : y; \
} \
...
/*等价于:
float float_max(float x, float y)
{
return x > y ? x : y;
}
*/
MAX(float); //此宏的调用
- 宏定义中使用
\
符号续行,对于函数中的语句无需续行符
举例2:
#include <stdio.h>
#include <string.h>
#define STRCPY(a, b) strcpy(a ## _p, #b)
int main(){
char var1_p[30];
char var2_p[30];
strcpy(var1_p, "aaaa");
strcpy(var2_p, "bbbb");
STRCPY(var1, var2); //strcpy(var1_p, "var2");
STRCPY(var2, var1); //strcpy(var2_p, "var1");
printf("var1 = %s\n", var1_p);
printf("var2 = %s\n", var2_p);
//STRCPY(STRCPY(var1, var2), var2);
//宏的嵌套调用,等价于strcpy(STRCPY(var1, var2)_p, "var2"); 编译器无法识别STRCPY(var1, var2)_p,程序出错
return 0;
}
输出结果:
3.4 宏的通用属性
- 宏的替换列表可以包含对其他宏的调用;
- 预处理器会不断重新检查替换列表,直到将所有的宏的名字都替换完为止。
- 预处理只替换完整的记号,而非片段;
- 会忽略嵌在标识符、字符常量、字面串中的宏名,如:
#define SIZE 256
...
int BUFFER_SIZE;
if(BUFFER_SIZE > SIZE){ //BUFFER_SIZE不会被替换为BUFFER_256
puts("ERROR: SIZE exceeded."); //字面串中的SIZE也不会被替换
}
- 宏定义的作用范围通常到出现该宏的文件末尾, 具有文件作用域;
- 宏不可重复定义,除非新旧定义不一致;
- 宏可以使用
#undef
指令来“取消定义”,若所取消的定义不存在,也不影响;- 格式:
#undef 标识符
- 一般用来取消现有定义,以便给出新的定义
- 格式:
#define N 10 //宏定义N
...
#undef N //取消当前N的宏定义
3.5 宏定义中的圆括号
- 宏定义中使用的大量圆括号很有必要;
- 如果宏的替换列表有运算符,则始终要将替换列表放在括号中;
- 如果宏含有参数,各参数都需要放在圆括号中,因为实际参数可能是表达式,会出现不同优先级和结合性的运算符;
- 目的都是为了保证任何时候宏替换列表都能被优先计算。
#define PI2 (2*3.14159) //替换列表含运算符
#define MAX(x,y) ((x)>(y)?(x):(y)) //含参数宏
3.6 创建较长的宏
方法1:续行符\
#define ECHO(s) \
do{ \
gets(s); \
puts(s); \
}while(0) //不加结尾的分号,便于调用时符合编写习惯
...
ECHO(s); //补上分号
注意:
- 续行符
\
只在预处理器中使用,函数中自有;
作为语句的分隔符,无需使用续行符。
方法2:逗号运算符,
(用途有限)
逗号运算符就是将两个表达式连接成一个表达式(逗号表达式),并将逗号右侧的表达式的值作为整个逗号表达式的值。
#define ECHO(s) (gets(s), puts(s)) //gets函数和puts函数调用的都是表达式,因此合法
...
ECHO(str); //调用,等价于 gets(str), puts(str);
缺点:不适用于包含一系列语句,而不仅仅是一些列表达式的宏的情况。
方法3:{}
组成复合语句(不推荐)
#define ECHO(s) {gets(s); puts(s);} //块内语句必须加分号,否则不合法
...
ECHO(str) //调用时,注意末尾不加分号
缺点:不符合代码编写习惯,容易出错。
3.7 预定义宏
3.7.1 预定义宏
- 预定义宏:每个宏表示一个整型常量或字面串。
预定义宏 | 描述 |
---|---|
__LINE__ | 当前宏所在行的行号 |
__FILE__ | 当前文件名 |
__DATE__ | 编译时的日期(格式"mm dd yyyy") |
__TIME__ | 编译时的时间(格式”hh:mm:ss“) |
__STDC__ | 若编译器符合C标准(C89或C99),则其值为1 |
__func__ | 当前语句所在的函数名(C99) |
#include<stdio.h>
int main()
{
printf(" 日期:%s\n", __DATE__);
printf(" 时间:%s\n", __TIME__);
printf("文件名:%s\n", __FILE__);
printf(" 行号:%d\n", __LINE__);
printf(" 标准:%d\n", __STDC__);
return 0;
}
输出结果:
四、条件编译
条件编译 —— 根据预处理器所执行的测试结果来包含或排除程序的手段。
4.1 #if
和#endif
指令
满足条件则保留特定函数的调用,便于调试过程中,显示错误代码处的特定的值。
使用格式“
#if 常量表达式
#endif
举例:
#define DEBUG 1 //声明一个初始为非零值的宏
...
#if DEBUG
printf("Value of i: %d\n", i);
printf("Value of j: %d\n", j);
#endif
在预处理过程中,#if 指令会检查 DEBUG 的值,其值为非0, 则 #if 和 #endif 之间的代码会保留,并将 #if 和 #endif 所在行删去;若其值为0,则删去 #if 和 #endif 及其之间的全部内容,剩余内容留给编译器编译。
说明:
- #if 指令会把未定义过的标识符当作值为0的宏来看待,因此包含代码会被删去,不会产生出错信息。但若标志符 DEBUG 未定义,且
#if !DEBUG
,则条件有效,代码会保留。
4.2 defined
运算符
当defined
运算符作用于标识符时,若标识符是一个定义过的宏,则返回1(即便该宏定义为0,也返回1),否则返回0。常与 #if 和 #endif 搭配使用。
#if defined(DEBUG)
...
#endif
//可不必使用圆括号
#if defined DEBUG
...
#endif
defined
只检测标识符是否有定义,因此不必给标识符赋值。
#define DD //此宏替换列表为空
...
#if defined DD //defined仍返回1
...
#endif
#if
和defined
可测试任意数量的宏
#if defined(FOO) && defined(BAR) && !defined (BAZ)
...
#endif
4.3 #ifdef
和#ifndef
指令
4.3.1 作用
#ifdef
测试一个标识符是否被定义为宏
#ifndef
测试一个标识符是否未定义为宏
4.3.2 使用格式
使用格式和用法与 #if
相同:
#ifdef 标识符 //等价为 #if defined 标识符
... //当标识符定义为宏时需包含的代码
#endif
#ifdef 标识符
等价为 #if defined 标识符
等价为 #if defined(标识符)
#ifndef 标识符 //等价为 #if !defined 标识符
... //当标识符未定义为宏时需包含的代码
#endif
#ifndef 标识符
等价为 #if !defined 标识符
等价为 #if !defined(标识符)
4.4 #elif
和#else
指令
作用:与#if
指令进行嵌套,与if
-elseif
-else
语句类似。
#if 表达式1
当表达式1为非0时需包含的代码
#elif 表达式2
当表达式1为0且表达式2非0时需包含的代码
#else
其他情况需包含的代码
#endif
4.5 使用条件编译
4.5.1 多台设备或多种操作系统之间的可移植程序
在程序的开头定义宏,如Linux宏,表明该程序将运行在Linux操作系统下。
#if defined(WIN32)
...
#elif defined(MAC_OS)
...
#elif defined(Linux)
...
#endif
4.5.2 不同编译器编译的程序
#if __STDC__ //编译器是否支持C89或C99
...
#else
...
#endif
4.5.3 未宏提供默认定义
//检查宏BUFFER_SIZE是否被定义,若未定义,则提供该宏的定义
#ifndef BUFFER_SIZE
#define BUFFER_SIZE 256 //宏定义
#endif
4.5.4 临时屏蔽包含注释的代码(条件屏蔽)
#if 0
包含注释的代码行
#endif
我们无法使用 /…/注释已经包含 /…/ 的代码行,则可使用此方法注释。
错误代码,程序报错:
/*
printf("KKKKKKKK\n"); /*注释*/
*/
修正代码:
#if 0
printf("KKKKKKKK\n"); /*注释*/
#endif
五、其他指令
5.1 #error
指令
5.1.1 使用格式
使用格式:#error 消息
消息
为任意记号序列,当程序遇到#error
指令,会立即显示一条包含消息
的出错信息。出错信息的格式依编译器的而有所不同,类似格式为:
- Error directive: 消息
- #error 消息
遇到 #error 表明程序中出现严重的错误,有些编译器会立即终止编译,不再检查其他错误。
5.1.2 用法举例
//确保程序无法在一台int类型不能存储小于100000的数的机器上编译
#define INT_MAX 10000 //存储允许的最大的int值
...
#if INT_MAX < 100000
#error int type is too small
#endif
#error
通常在 #if - #elif - #else 序列中的 #else 部分出现
#if defined(WIN32)
...
#elif defined(MAC_OS)
...
#elif defined(Linux)
...
#else
#error No operating system specified
#endif
5.2 line
指令
5.2.1 作用
- 改变程序行的编号方法。可用来是编译器认为它正在从一个有不同名字的文件中读取程序。
#line
指令的作用是改变__LINE__
宏或__FILE__
宏的值。
5.2.2 使用方法
使用格式:
#line n
(n的取值范围为 1 ~ 32767(C99为231-1) 的整数),该指令导致后续行的编号为 n、n+1、n+2、…line n "文件"
后续行被认为来自文件,行号从 n 开始。
5.3 #pragma
指令
使用方法:
#pragma 记号
此指令对于大型程序和需要使用特定编译器的特殊功能的程序非常有用,此处暂不展开。
5.4 _Pragma
运算符
使用方法:
_Pragma (字面串)
遇到该指令时,预处理器会移除字符串两端的双引号,并分别用字符"
和\
代替转义序列\"
和\\
。实现对字面串的 “去串化” 。表达式的结果是一系列记号,该记号会被作为 pragma 指令中的记号。
_Pragma("data(heap_size => 1000, stack_size => 2000)")
//上下二者等价
#pragma data(heap_size => 1000, stack_size => 2000)
#define DO_PRAGMA(x) _Pragma(#x)
...
DO_PRAGMA(GCC dependency "parse.y") //#x将x串化为"GCC dependency \"parse.y\""
//扩展后的结果
#pragma GCC dependency "parse.y"