C语言细节 预处理器

"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=17100/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;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值