"C预处理器"在程序执行前查看程序(故有此名).根据程序中的预处理器指令,预处理器把符号缩写替换为其表示的内容(如包含程序所需的其
他文件,或选择让编译器查看哪些代码).预处理器并不知道C,而基本上只是把一些文本替换为另一些文本.预处理器对标识符的要求和C编译
器相同:①由字母/数字/下划线组成 ②只能以字母开头
一.翻译处理
在预处理之前,编译器需要对程序进行一些翻译处理:
①编译器把源码中出现的字符映射到源字符集.该过程处理多字节字符和三字符序列
②编译器定位每个反斜杠(\)后面跟着换行符的实例,并删除它们,也就是说把多个"物理行"(Physical Line)转换成1个"逻辑行"(Logical Line):
printf("That's wond\
erful\n"); → printf("That's wonderful\n")
由于预处理表达式的长度必须是1个逻辑行,所以这1步为预处理器做好了准备工作
③编译器会将文本划分成"预处理记号序列"(记号的概念参见 二.5 部分),"空白序列"和"注释序列".编译器用1个空格字符替换1条注释,故有:
int/*这相当于空格*/fox; → int fox;
而且,实现可以用1个空格替换所有空白字符序列(不包括换行符)
经过翻译处理后,程序已经准备好进入预处理阶段,预处理器会查找1行中以#为起始的预处理指令
二.宏
宏通过#define指令定义.每个#define指令由3个部分组成(见下图):
①#define本身
②选定的缩写,称为"宏"(Macro),其中不允许有空格,且必须遵循C变量的命名规则
有些宏代表值,称为"类对象宏"(Object-Like Macro)
还有些宏代表,称为"类函数宏"(Function-Like Macro)
③指令的其余部分称为"替换列表"或"替换体",是预处理器用于替换宏的文本
这个替换的过程称为"宏展开"(Macro Expansion)
1.类对象宏
(1)语法:
//宏定义:
#define <Macro> <sub>
//文件中的<Macro>会被直接替换为<sub>
//参数说明:
Macro:指定宏名
//其中不能有空格;通常由大写字母构成,尤其是对类对象宏来说
sub:指定替换体
//其中可以有空格
//宏调用:
<Macro>
和其他预处理器指令一样,#define以#作为1行的开始.ANSI C和之后的标准都允许#前有空格/制表符,但#和define之间不能有空格;而旧版本的C要求
该指令从1行的最左侧开始.该指令从#开始,到后面的第1个换行符结束,也就是说,指令的长度仅限于1个逻辑行.在#define指令所在的逻辑行中可以插
入注释,并且每条注释都会被1个空格代替.指令可以出现在源文件中的任何位置,从出现的位置到文件结尾有效
(2)定义明示常量:
"明示常量"(Manifest Constant)又称"符号常量"或"宏常量",通过预处理器指令#define来定义得到:
#include <stdio.h>
#define TWO 2
#define OW "Consistency is the last refuge of the unimagina\
tive. - Oscar Wilde"
int main(void) {
printf("%d\n",TWO);//结果:2
printf("%s\n",OW);//结果:Consistency is the last refuge of the unimaginative. - Oscar Wilde
return 0;
}
(3)其他用法:
#include <stdio.h>
#define TWO 2
#define PX printf("x is %d\n",x);
#define FOUR TWO*TWO
#define FMT "x is %d\n"
#define OW "Consistency is the last refuge of the unimagina\
tive. - Oscar Wilde"//注意:第2个物理行开始处不能有空格,否则替换后也会有空格
#define OW2 "Consistency is the last refuge of the unimagina\
tive. - Oscar Wilde"
int main(void) {
int x=TWO;
PX;//结果:x is 2
x=FOUR;//FOUR→TWO*TWO→2*2
printf(FMT,x);//结果:x is 4//注意:一些编译器不支持
printf("%s\n",OW2);//结果:Consistency is the last refuge of the unimagina tive. - Oscar Wilde
return 0;
}
(4)例外:
通常来说,预处理器会用和宏等价的替换体替换程序中的宏.如果替换后还有宏,则继续替换,直到所有宏都被替换掉.但双引号中的宏是例外:
#define OW "ooooo"
int main(void) {
printf("OW\n");//结果:OW
return 0;
}
也就是说,双引号中的宏不会被替换.对类对象宏和类函数宏来说都是如此
(5)对替换体的解释:
C预处理器"记号"(Token)是宏定义的替换体中由空格/制表符/换行符分隔的项,如:
#define FOUR 2*2
该宏定义有1个记号:2*2.而:
#define FOUR 2 * 2
该宏定义有3个记号:2,*,2
宏定义中的替换体可能被预处理器解释为"记号型字符串"或"字符型字符串".在替换体中有空格时,对记号型字符串和字符型字符串的处理方式不同:对字
符型字符串,包括空格在内的整个替换体会被视为1个记号;而对记号型字符串,空格会被视为替换体中各记号的分隔符,如:
#define FOUR 2 * 2
如果预处理器把该替换体解释为记号型字符串,则用3个记号(2,*,2,分别用空格分隔)来替换FOUR;如果解释为字符型字符串,则用2 * 2替换FOUR.不同
的预处理器会进行不同的解释,但这种区别只在个别复杂的情况下才有实际意义
另外,C编译器处理记号的方式比预处理器复杂.比如说,由于编译器理解C语言的规则,所以不要求代码中用空格来分隔记号.如编译器可以把2*2直接视为3
个记号,引物它可以识别出2是常量而*是运算符
(6)重定义常量:
2.类函数宏
(1)语法:
//宏定义:
#define <Macro>(<param1>,<param2>...) <sub>
//尽管形式上相似,但类函数宏的行为和函数调用完全不同:程序中的宏先被替换体替换,替换体中的形参再被宏调用时传入的实参替换(见 (2) 部分)
//参数说明:其他参数同上
param:指定宏的参数(形参)
//这些参数将出现在<sub>中
//宏调用:
<Macro>(<val1>,<val2>...)
//参数说明:
val:指定传入的参数(实参)
//实例:
#include <stdio.h>
#define SQUARE(X) X*X
#define PR(X) printf("The result is %d\n",X);
int main(void) {
int x=2;
int z=SQUARE(x);
PR(z);//结果:The result is 4//由于替换体结尾已经有引号了,这里结尾没有引号也可以
z=SQUARE(2);
PR(z);//结果:The result is 4
return 0;
}
(2)注意事项:
预处理器不做计算,只替换字符序列,因此有:
#include <stdio.h>
#define SQUARE(X) X*X
#define PR(X) printf("The result is %d\n",X);
int main(void) {
int x=5;
PR(SQUARE(x+2));//结果:The result is 17
PR(100/SQUARE(2));//结果:The result is 100
return 0;
}
替换过程为:SQUARE(x+2)→X*X→x+2*x+2=5+2*5+2=17与100/SQUARE(2)→100/X*X→100/2*2=100
要解决该问题,需使用足够多的括号以保证运算顺序:
#include <stdio.h>
#define SQUARE2(X) ((X)*(X))
#define PR(X) printf("The result is %d\n",X);
int main(void) {
int x=5;
PR(SQUARE2(x+2));//结果:The result is 49
PR(100/SQUARE2(2));//结果:The result is 25
return 0;
}
(3)用宏参数创建字符串:
C允许在字符串中包含宏参数.在类函数宏的替换体中,预处理运算符#可以把记号转换成字符串,该过程称为宏参数的"字符串化"(Stringizing):
#include <stdio.h>
#define PSQR(x) printf("The square of " #x " is %d\n",((x)*(x)));
//#x两侧与引号之间的空格也可以没有,不影响结果
int main(void) {
int y=5;
PSQR(y);//结果:The square of y is 25
PSQR(2+4);//结果:The square of 2+4 is 36
return 0;
}
也就是说,替换体中的#<param>会被替换为"<val>"
3.预处理器连接符:
##运算符把2个记号组合成1个记号:
#include <stdio.h>
#define XNAME(n) x##n
#define PXN(n) printf("x"#n"=%d\n",x##n);
int main(void) {
int XNAME(1)=1;
int XNAME(2)=2;
int XNAME(3)=3;
PXN(1);//结果:x1=1
PXN(2);//结果:x2=2
PXN(3);//结果:x3=3
return 0;
}
##运算符既可用于类对象宏,也可用于类函数宏
4.变参宏:
C99/C11提供了让用户自定义变参宏(即接受数量可变的参数的宏)的工具,即...和__VA_ARGS__.其中...表示接受数量可变的参数(可以是唯一
的参数),__VA_ARGS__表示...对应的实参要替代的部分,如:
#include <stdio.h>
#define PR(X,...) printf("%d\n",X);printf(__VA_ARGS__);
#define PR2(X,...) printf("%d\n",X);__VA_ARGS__;
int main(void) {
int x=1;
PR(x,"aaa\n");
PR(x,"This is %d\n",3);
PR2(x);
PR2(x,printf("%d\n",3));
return 0;
}
//结果:
1
aaa
1
This is 3
1
1
3
注意:
①...必须是最后1个参数
②...可不对应任何实参
5.宏和函数的选择:
宏和函数的比较:
①宏通常比函数复杂,稍有不慎就会产生奇怪的结果
②宏生成内联代码,即在程序中生成语句,所以调用几次就会产生几份代码,而函数不管调用几次都只有1份代码,也就是说函数比宏节省空间;但调用
函数时,程序的控制必须跳至函数内,执行完毕后再跳回主调函数,这要比宏生成的内联代码花费更多的时间
③此外,宏对变量数据类型的限制更少(因为处理的是文本而不是实际的值).如int/float/double类型的数据均可被传入上文中的SQUARE(X)中,
使用函数则可能导致精度丢失等问题(因为需要进行数据类型的转换)
宏和函数的选择:
①通常用宏来替代那些简单的函数,如:
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
#define ABS(X) ((X)<0?-(X):(X))
#define ISSIGN(X) ((X)=='+'||(X)=='-'?1:0)
②如果希望使用宏来提高性能,需要保证函数被使用足够多次(如在循环尤其是嵌套循环中)才会有明显的效果
三.文件包含
1.语法:
#include <<path_or_filename>>//即头文件名在尖括号中
#include "<path_or_filename>"//即头文件名在双引号中
//预处理器会用<path_or_filename>中的内容替换#include指令
//在多数情况下,头文件中的内容是编译器生成最终代码时所需的信息,而不是添加到最终代码中的材料
//参数说明:
path_or_filename:指定头文件(包括扩展名.h)
//使用尖括号表示在标准系统目录(标准库头文件所在的目录)中查找
//使用双引号表示先查找本地目录,如果没有再在标准系统目录中查找
//具体先查找哪个目录取决于编译器,可能是当前工作目录/源代码文件所在的目录/项目文件所在的目录...
//使用双引号并指定路径表示先在指定目录中查找,如果没有再在标准系统目录中查找
//实例:
#include <stdio.h>
#include "F:\1.h"
int main(void) {
struct name n1={
"John",
"Smith"
};
printf("I am %s %s.",n1.first,n1.last);//结果:I am John Smith.
return 0;
}
2.使用头文件:
Ⅰ.常见的内容:
①明示常量 ②类函数宏 ③函数原型声明 ④定义结构体模板 ⑤类型定义
Ⅱ.声明外部变量供其他文件共享
四.其他指令
1.取消宏:
#undef指令用于取消已定义的宏,语法为:
#undef <Macro>
//即使之前没有定义该宏,也可以使用该命令
//参数说明:
Macro:指定要取消的宏
//实例:
#include <stdio.h>
#define AAA 111
int main(void) {
printf("%d\n",AAA);//结果:111
#undef AAA
#define AAA 222
printf("%d\n",AAA);//结果:222
return 0;
}
2.条件编译(Conditional Compilation)
可以使用某些指令创建"条件编译",也就是说,使用这些指令告诉编译器根据编译时的条件执行/忽略某些代码块.这些指令均允许嵌套
(1)#ifdef指令
//语法:
#ifdef <Macro>
<cmd11>
...
[#else
<cmd21>
...]
#endif
//如果已经定义了<Macro>,则执行<cmd1~>,否则执行<cmd2~>
//从ANSI C开始支持缩进<cmd1~>/<cmd2~>(下同)
//参数说明:
Macro:指定宏
cmd:指定要执行的命令
//注意:不一定是预处理指令,也可以是普通的语句如printf()
//实例:
#define AAA 111
#ifdef AAA
#include <stdio.h>
#endif
int main(void) {
printf("%d\n",AAA);//结果:111
return 0;
}
- 可用于调试程序:
#include <stdio.h>
#define JUST_CHECKING//称为空宏,但该宏也是已定义的
#define LIMIT 4
int main(void) {
int i,total=0;
for(i=1;i<=LIMIT;i++) {
total+=2*i*i+1;
#ifdef JUST_CHECKING
printf("%d,%d\n",i,total);
#endif
}
printf("%d",total);
return 0;
}
//结果:
1,3
2,12
3,31
4,64
64
调试完成后,去除JUST_CHECKING的定义即可;如果需要再次调试,再加上JUST_CHECKING的定义即可
(2)#ifndef指令:
//语法:
#ifndef <Mcaro>
<cmd11>
...
[#else
<cmd21>
...]
#endif
//如果尚未定义<Macro>,则执行<cmd1~>,否则执行<cmd2~>
//参数说明:同上
- 用于测试:
//arrays.h中:
#ifndef SIZE
#define SIZE 100
#endif
//main.c中:
#define SIZE 10
#include "arrays.h"
由于在main.c中定义了SIZE,在arrays.h中就不会再定义SIZE.所以可以先在main.c中定义1个较小的数用于测试,测试完毕后取消main.c中的
定义,使用arrays.h中的定义.如果需要再次测试,只需要在main.c中再次定义即可
- 用于防止多次包含同1个头文件:
//things.h中:
#ifndef THINGS_H_
#define THINGS_H_
...
#endif
如果该文件被包含了多次,其中的命令除第1次外都会由于已经定义了THINGS_H_而被跳过.这种多次包含常常是因为头文件中也会包含其他文件,所以
有些文件已经被隐式包含了.为了保证用于该目的的宏没有在其他地方被定义,这类宏的命名常遵循一定的格式
(3)#if指令与defined运算符:
//defined运算符:
defined <Macro>
//如果<Macro>已被定义,返回1;否则,返回0
//#if指令:
#if <cond1>
<cmd11>
...
[#elif <cond2>
<cmd21>
...
#else
<cmdn1>
...]
#endif
//如果满足某个条件,就执行相应的命令;否则执行<cmdn~>
//实例:
#include <stdio.h>
#define IBMPC "IBMPC"
#if defined IBMPC
#define MAC "MAC"
#else
#define IBMPC "IBMPC2"
#endif
int main(void) {
printf("%s\n",IBMPC);//结果:IBMPC
printf("%s",MAC);//结果:MAC
return 0;
}
3.预定义宏:
宏 | 含义 |
---|---|
__ DATE__ | 预处理的日期,"Mmm dd yyyy"形式的字符串字面量 |
__ FILE__ | 表示当前文件名的字符串字面量 |
__ LINE__ | 表示当前源代码行号的整型常量 |
__ STDC__ | 为1时,表示实现遵循C标准 |
__ STDC_HOSTED__(C99) | 本机环境为1,否则为0 |
__ STDC_VERSION__(C99) | 如果支持C99标准,为199901L;如果支持C11标准,为201112L |
__ TIME__ | 翻译代码的时间,"hh:mm:ss"形式的字符串字面量 |
#include <stdio.h>
void f(void) {
printf("%d\n",__LINE__);
}
int main(void) {
printf("%s\n",__FILE__);
printf("%s\n",__DATE__);
printf("%s\n",__TIME__);
printf("%d\n",__STDC__);
printf("%d\n",__LINE__);
f();
return 0;
}
//实例:
E:\Program\C_C++\1.c
Dec 2 2020
21:53:18
1
11
4
C99还提供了1个名为__func__的预定义标识符,为1个代表当前函数名的字符串.由于__func__具有函数作用域,宏则具有文件作用域,因此__func__
不是预定义宏,而只是预定义标识符:
#include <stdio.h>
void f(void) {
printf("%s\n",__func__);
}
int main(void) {
printf("%s\n",__func__);
f();
return 0;
}
//结果:
main
f
4.其他预处理器指令
(1)#line指令:
#line用于重置由__LINE__和__FILE__报告的当前行号和文件名:
#include <stdio.h>
int main(void) {
printf("%s,%d\n",__FILE__,__LINE__);//结果:E:\Program\C_C++\1.c,4
#line 1000//重置行号
printf("%s,%d\n",__FILE__,__LINE__);//结果:E:\Program\C_C++\1.c,1000
#line 2000 "ccc.c"//重置行号和文件名
printf("%s,%d\n",__FILE__,__LINE__);//结果:ccc.c,2000
//#line "ddd.c"//不能只重置文件名
//printf("%s,%d\n",__FILE__,__LINE__);//报错:[Error] ""ddd.c"" after #line is not a positive integer
return 0;
}
(2)#error指令:
#error指令用于让预处理器发出1条错误信息:
#include <stdio.h>
#define AAA 111
#if AAA==111
#error Wrong Standard
#endif
//报错:
[Error] #error Wrong Standard
(3)#pragma:
#pragma指令用于把编译器指令放入源代码中.如在开发C99时,标准被称为C9X,可通过以下指令让编译器支持C9X:
#pragma c9x on
另外,编译器一般都有自己的编译指令集,不过C99提供了3个标准编译指示
C99还提供了_Pragma预处理器运算符,用于把字符串转换成普通的编译指示,如:
_Pragma("nonstandardtreatmenttypeB on")
等价于:
#pragma nonstandardtreatmenttypeB on
由于_Pragma不使用#,可以作为宏的替换体的一部分:
#define PRAGMA(X) _Pragma(#X)
_Pragma运算符还会完成"解字符串"(Destringizing)的工作,即把字符串中的转义序列转换为其代表的字符:
_Pragma("use_bool \"true \"false")
被转换为了:
#pragma use_bool "true "false
5.泛型:
在编程设计中,"泛型编程"(Generic Programming)指那些没有特定类型,但一旦指定1种类型,就可以转换为指定类型的代码.C没有这种功能,但C11
新增了"泛型选择表达式"(Generic Selection Expression),可根据表达式的类型(int/double等)选择1个值.泛型选择表达式不是预处理器指
令,但在一些泛型编程中常用作宏定义的一部分
//语法:
_Generic(<var>,<type1>:<val1>,<type2>:<val2>...)
//如果<var>是<typei>类型的变量,那么整个表达式的值就是<vali>
//参数说明:
var:指定表达式
type:指定数据类型,如int/float
//注意:外面没有双引号,即不是str
//default表示其他<type>中没有的类型
val:指定该数据类型对应的值
//实例:
#include <stdio.h>
#define MYTYPE(x) _Generic(x,int:"int",float:"float",double:"double",default:"other")
int main(void) {
int x=5;
printf("%s\n",MYTYPE(x));//结果:int
printf("%s\n",MYTYPE(2.0*x));//结果:double
printf("%s\n",MYTYPE(3L));//结果:other
printf("%s\n",MYTYPE(&x));//结果:other
printf("%s\n",MYTYPE('a'));//结果:int
printf("%s\n",MYTYPE("aaa"));//结果:other
return 0;
}