这篇博客和大家分享一下C语言中的预处理操作。
1. 预定义符号
C语言设置了⼀些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的。
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATA__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
一起看下面的代码,我们来举个栗子:
#include<stdio.h>
int main()
{
printf("%s\n",__FILE__);
return 0;
}
2. #define定义常量
基本语法:
#define name stuff
看下面几个例子:
#define MAX 100 //为MAX定义一个值
#define reg register //为register关键字创建一个简短的名字
#define CASE break;case //再写case语句的时候自动把break补上
//如果定义的 stuff过⻓,可以分成⼏⾏写,除了最后⼀⾏外,每⾏的后⾯都加⼀个反斜杠(续⾏符)。
在定义标识符的时候要不要加上“ ; ”?建议不要加上,容易导致问题,比如下面的场景:
if(a==1)
max=MAX;
else
max=0;
如果是这种情况下加了分号的话,那么等替换的时候,if和else之间就是两条语句,没有括号的情况下,if后面只能有1条语句,会出现语法错误。
3. #define定义宏
#define机制包括一个规定,允许把参数替换到文本中,这种实现通常叫做宏或者定义宏,宏的申明方式如下:
#define name(parament-list) stuff
其中parament-list是一个用逗号隔开的符号表,他们可能出现在stuff中在。注意:参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的⼀部分。看下面的例子:
#define SQUARE(x) x*x
这是一个宏接收参数x,如果在上述声明之后,会将SQUARE(5)置于程序中,预处理器就会用下面的式子来替换掉上面的式子:x*x。这个宏存在一个问题,观察下面的代码:
int a=4;
printf("%d",SQUARE(a+1));
运行一下我们看一下结果:
这时候是不是在想不应该是16吗?为什么是9呢? 在预处理器替换文本时,x变成了a+1;所以这条语句就变成了:
printf("%d",a+1*a+1);
在宏定义上加上两个括号问题就解决了:
#define SQUARE(x) (x)*(x)
这样预处理之后就解决这个问题了,那么我们看一下下面这宏:
#define ADD(x) (x)+(x)
在定义时我们使用了括号避免了刚刚的问题,但同时又带了新的问题,我们看下面的代码:
int a=5;
printf("%d",10*ADD(a));
运行一下看看结果如何:
这里我们已经使用了上面的方法加括号了,结果不应该是100吗?为什么是55呢?乘法咸鱼宏的定义计算,在预处理时这条语句就变成了:
printf("%d",10*(5)+(5));
注意:所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
4. 带有副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。例如:
x+1;//没有副作用
x++;//有副作用
我们可以定义一个MAX宏来证明具有副作用的参数所引起的问题。
#define MAX(a,b) ((a)>(b)?(a):(b))
....//省略一些常规代码
a=5;
b=9;
c=MAX(a++,b++);
printf("%d %d %d",a,b,c);
输出结果是什么呢?我们调试一下看看结果:
为什么不是5 9 11呢?其实在c=MAX(a++,b++)这条语句被替换成了下面这条语句:
c=(a++)>(b++)?(a++):(b++);
在这段代码中第一个括号内的a++是先使用a的值再++,同理下一个括号的b也是先使用b的值再++,在这个三目运算符中,就变成了5和9的比较来决定返回的结果,再后面的括号中的a++,a已经经过前面的计算变成了6,同理b也就变成了10,然后再看返回的结果也是b++,那就是先使用再++,返回的是10,返回了一个值也就是使用了b,此时b还要再++一次,所以b的值就变成了11。
在这里大家有没有发现如果宏的参数是个表达式的话,宏的参数是不计算的,直接替换,把该替换的位置全部替换掉。
5. 宏替换的规则
在上面我们一直提到宏的替换,那么宏的替换规则是什么呢?
在程序中扩展#define定义符号和宏时,需要涉及几个步骤:
1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先
被替换。
2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3.最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上
述处理过程。
注意:1.宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2.当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
6. 宏和函数的对比
宏通常被应用于执行简单的运算。比如在两个数中找到较大的那一个,我们可以写:
#define MAX(a,b) ((a)>(b)?(a):(b))
那为什么不用函数来完成这个任务呢?主要原因是:1.用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜⼀筹。2.更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之
这个宏怎可以适用于整形、长整型、浮点型等可以用于 > 来比较的类型。宏是类型无关的。
这事就有人好奇了,你这一直说宏的好处,那我们为什么还要用函数呢?其实宏和函数对比还是有很大的劣势的:
1.每次使用宏的时候,⼀份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2.宏是无法调试的。
3.宏由于与类型无关,也就不够严谨。
4.宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。下面做一个宏和函数的对比:
属性 | #define定义宏 | 函数 |
代码长度 | 每次使用时,宏代码都会被插入到程序中,除了非常小的宏之外,代码长度会大幅增长 | 函数代码只出现在一个地方,每次使用函数时,都会调用那里的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对较慢一些 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则临近操作符的优先级可能会产生不可预料的后果,所以建议在定义宏时多些括号 | 函数参数只在函数调用的时候求一次值,它的结果值传递给函数,表达式的求值更容易预测 |
带有副作用的参数 | 参数可能被替换到宏体内多个位置,如果宏的参数被多次计算,带有副作用的参数求值会产生不可预料的结果 | 函数参数只在传参时求值一次,结果更容易控制 |
参数类型 | 宏的参数与类型无关,只要是对参数的操作是合法的,他就可以适用于任何类型的参数 | 函数的参数与类型有关,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的 |
调试 | 宏是不方便去调试的 | 函数是可以逐语句调试的 |
递归 | 宏不能递归 | 函数是可以递归的 |
7.#和##
7.1 #运算符
#运算符将宏的⼀个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。#运算符所执行的操作可以理解为“字符串化”。当我们有一个变量int a=10时,我们想打印出the value of a is 10时该怎么办呢?我们可以这样写:
#define PRINT printf("the value of #n is "%d"",n)
当我们按照下面的方式调用时:PRINT(a) 当我们把a替换到宏的体内时,就出现了#a,而#a就是转换为(a)时,代码就会被处理为:
printf("the value of ""a"" is "%d"",a);
从而达到我们想要的结果
7.2 ##运算符
## 可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本片段创建标识符。 ## 被称
为记号粘合这样的连接必须产生⼀个合法的标识符。否则其结果就是未定义的。这里我们想想,写⼀个函数求2个数的较大值的时候,不同的数据类型就得写不同的函数。例如:
int int_max(int a,int b)
{
return a>b?a:b;
}
float float_max(float a,float b)
{
return a>b?a:b;
}
这样写起来太繁琐了,我们可以这样写:
#define GENERIC_MAX(type)
type type##_max(type x,type y)
{
return (x>y?x:y);
}
使用宏定义函数,我们一起看下面的代码来感受一下:
GENERIC_MAX(int)
GENERIC_MAX(float)
int main()
{
int m=int_max(2,3);
printf("%d\n",m);
float n=float_max(3.5f,4.5f);
printf("%f\n",n);
return 0;
}
8. 命名约定
⼀般来讲函数的宏的使用语法很相似。所以语言本⾝没法帮我们区分⼆者。我们平时的一个习惯就是:把宏的名字全部大写,函数名不要全部大写。
9. #undef
这条指令用来移除一个宏定义,用法如下:
#undef NAME
本篇到这里就结束了,感谢大家的观看,有问题可以评论或者私信我。