前言
预处理是C语言的一个重要功能,它由预处理程序负责完成。当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。C语言提供多种预处理功能,主要处理#开始的预编译指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
这篇文章,我们就一起来学习一下C语言的预处理。
1. 预定义符号
首先给大家介绍一下预定义符号。
__FILE__ //进行编译的源文件 __LINE__ //文件当前的行号 __DATE__ //文件被编译的日期 __TIME__ //文件被编译的时间 __STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
这些预定义符号都是C语言内置的,我们可以直接使用。
举个例子:
#include <stdio.h> int main() { printf("%s\n", __FILE__); printf("%d\n", __LINE__); printf("%s\n", __DATE__); printf("%s\n", __TIME__); return 0; }
2. 预处理指令——#define
#define到底是什么东西呢?
#define是 C语言 和 C++ 中的一个预处理指令,其中的“#”表示这是一条预处理命令·。凡是以“#”开头的均为预处理命令,“define”为宏定义命令,“标识符”为所定义的宏名。#define的部分运行效果类似于word中的ctrl+F替换,与常量const相比有着无法替代的优势。
那它有哪些作用呢?
2.1 #define 定义标识符
语法:
#define name stuff
比如我们之前经常这样写:#define MAX 100
MAX的值就是100。
通过上一篇文章的学习,我们知道
#define
定义的符号是在预处理阶段就完成替换的。
当然其它类型的数据也🆗的,不止可以是整型:
#define MAX 1000 #define reg register //为 register这个关键字,创建一个简短的名字 #define do_forever for(;;) //用更形象的符号来替换一种实现 #define CASE break;case //在写case语句的时候自动把 break写上。 // 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。 #define DEBUG_PRINT printf("file:%s\tline:%d\t \ date:%s\ttime:%s\n" ,\ __FILE__,__LINE__ , \ __DATE__,__TIME__ )
可以是关键字,甚至是代码一段代码也可以。
那现在有一个问题:
在define定义标识符的时候,要不要在最后加上分号 ;
?
比如:
#define MAX 1000;
建议不要加上 ;
,这样容易导致问题。
像这种场景:
#define MAX 100; int main() { int max = 0; if (3>5) max = MAX; else max = 0; return 0; }
这段代码看上去好像没什么问题。
但是:
它就是一段有问题的代码。
为什么呢?
就因为
#define MAX 100;
我们加了分号。替换之后的代码就变成了这样:
相当于max = 100;
又跟了一条空语句,但是因为if
后面没跟大括号,只能匹配一条语句,这样就出现语法错误了。
初此之外,#define还有什么作用呢?
2.2 #define 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空格存在,参数列表就会被解释为stuff的一部分。
举个例子:
现在又一个变量a和b,要求出它们的最大值。
当然有很多方式,我们可以直接比较,或者写一个函数。
那可不可以定义一个宏来完成这个功能呢?
当然!
我们可以把要比较的a和b作为参数传给宏,那我们写的宏也要能够来接收这两个数。
那就可以这样写:
#define MAX(x,y) x>y?x:y int main() { int a = 10; int b = 20; int max = MAX(a, b); printf("%d\n", max); return 0; }
能完成任务吗?
大家会不会感觉好像跟函数有点像啊?
其实宏跟函数还是不同的,#define定义的宏本质上还是替换。
我们可以利用Linux环境给大家看一下预处理之后宏替换完的代码:
和函数是不同的,后面我们会详细讲解宏和函数的对比。
2.3 使用宏的注意事项
我们再来看这样一个宏:
#define SQUARE( x ) x * x
大家应该很容易看出这个宏的功能,就是求一个数的平方嘛。
这个宏接收一个参数 x 。
如果在上述声明之后,你把
SQUARE( 5 );
置于程序中,预处理器就会用下面这个表达式替换上面的表达式:
5 * 5
我们来试一下:
#define SQUARE( x ) x * x int main() { int ret = SQUARE(5); printf("%d\n", ret); return 0; }
运行程序:
但是,这个宏写的好不好呢?有没有什么问题呢?
其实这个宏是存在一点问题的。
观察下面的代码段:
#define SQUARE( x ) x * x int main() { int a = 5; printf("%d\n", SQUARE(a+1)); return 0; }
大家思考一下结果是啥?
a = 5,a+1就是6,那6的平方就是36嘛。
乍一看,你可能觉得这段代码将打印36这个值。
可事实是这样吗?
结果是11,为什么?
替换文本时,参数x被替换成a + 1,所以这条语句实际上变成了:
printf ("%d\n",a + 1 * a + 1 );
那就是5+1*5+1,可不就是11嘛。
这样就比较清晰了,由替换产生的表达式由于操作符优先级的问题并没有按照预想的次序进行求值。
如何解决,那也很简单:
在宏定义上加上两个括号,这个问题便轻松的解决了:
#define SQUARE(x) (x) * (x)
这样预处理之后就产生了预期的效果:
printf ("%d\n",(a + 1) * (a + 1) );
接着我们再来看一个宏定义:
#define DOUBLE(x) (x) + (x)
它来计算一个数的二倍,这次我们加上了括号:
#define DOUBLE( x ) (x) + (x) int main() { printf("%d\n", DOUBLE(5)); printf("%d\n", DOUBLE(5 + 1)); return 0; }
运行一下:
定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。
int a = 5; printf("%d\n" ,10 * DOUBLE(a));
这将打印什么值呢?
5的二倍是10,10*10=100。
看上去,好像打印100,但事实上是这样嘛?
为啥是55了?
我们发现替换之后:
printf ("%d\n",10 * (5) + (5));
乘法运算先于宏定义的加法,所以出现了55的结果 。
这又这么解决:
在宏定义表达式的两边再加上一对括号就可以了。
#define DOUBLE( x ) ((x) + (x))
所以我们得出结论:
用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
2.4 #define 替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
- 在调用宏时,首先对其参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
什么意思呢?举个栗子:
#define M 10 #define SUM(x,y) x+y int main() { int ret = SUM(5, M); printf("%d\n", ret); return 0; }
在这段代码中,调用宏SUM时,首先对其参数进行检查,发现它的第二个参数M是由#define定义的符号,就会首先对M进行替换:
int ret = SUM (5, 10);
然后再进行宏的替换:
int ret = 5+10;
替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
1. 宏参数和#define 的定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
就像上面我们定义的宏SUM中的第二个参数M就是其它#define定义的符号。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
举个栗子:
3. #和##
3.1 #的作用
我们先来看这样一个代码:
int main() { printf("hello world\n"); printf("hello ""world\n"); return 0; }
输出结果会是什么呢?
首先第一个肯定是
"hello world"
,那第二个呢,会不会跟第一个输出有不同的地方呢?
并没有不同。
我们发现字符串是有自动连接的特点的。
那现在有这样一个场景:
int main() { int a = 8; printf("the value of a is %d\n", a); float b = 3.5; printf("the value of b is %f\n", b); return 0; }
运行它:
打印出来这样两句话。
那如果我们不想每次都调用printf,想写一个宏来实现这个功能,应该这么搞呢?
那我们肯定要把a,b的不同的值作为参数传给宏,那%d,%f是不是也得传过去啊,要不然不知道一什么形式打印啊。
打印a就传%d,打印b就传%f。
那定义得宏就要有两个参数:
#define PRINT(val,format)
那宏得内容怎么写:
#define PRINT(val,format) printf("the value of val is"format"\n",val)
这样好像不行,这样的话of后面得val就固定死了,打印得时候不会变成对应的a,b。
那这样:
printf("the value of" val "is"format"\n",val)
这样也不行,“the value of”,"is"和format都是字符串,能拼起来,但是val 自己一个数字,没法和它们组成字符串啊。
那这时候,就要用到#
了,那它的作用是什么呢?
使用 # ,可以把一个宏参数变成对应的字符串。
那就可以这样写了:
printf("the value of " #val " is "format"\n",val)
在val
前面加上一个#
,它就会把参数val
转化成字符串。
我们验证一下:
这样就可以了。
代码中的 #val 会被预处理器处理为:“val”
而我们用函数是没法实现这样的功能的。
3.2 ## 的作用
#的作用我们知道了,那##的作用又是啥呢?
##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。
什么意思呢?举个例子大家就明白了:
#define CAT(a,b) a##b int main() { int helloworld = 10; printf("%d", CAT(hello, world)); return 0; }
打印helloworld 这个变量的时候,我们没有直接给变量名,而给的是CAT(hello, world),我们知道宏CAT(hello, world)替换之后就是hello##world。
而##就将hello,world合成符号helloworld
我们看看能不能成功打印:
但是要注意:
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
4. 带副作用的宏参数
什么叫带有副作用呢?
比如:
int main() { int a = 2; int b = a + 1;//不带副作用 int c = ++a;//带有副作用 return 0; }
int b = a + 1;,b的值变成了2,但是a还是1,这就没有副作用;
但是,int c = ++a;,b的值是变成了2,但同时a的值也自增1,这就是带有副作用的。
而对于宏来说:
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。
副作用就是表达式求值的时候出现的永久性效果。
举个例子:
#define MAX(a, b) ( (a) > (b) ? (a) : (b) ) int main() { int x = 3; int y = 4; int max = MAX(++x, ++y); printf("max=%d x=%d y=%d\n", max, x, y);//输出的结果是什么? return 0; }
结果会是什么呢?
首先这个宏的功能我们很容易看出来就是求
a, b
中的最大值。
int max = MAX(++x, ++y);
++x的值是4,++y的值是5,5和4的最大值是5。
但结果会是5嘛?
并不是我们所想的。
这就是宏参数的副作用导致的。
这据代码替换之后是这样的:
int max = ((++x) > (++y) ? (++x) : (++y));
++x的值是4,++y的值是5,4大于5不成立,所以问号❓后面的++x不执行,x还是4,++y执行,所以y又自增了一次,变成6。
这就导致了最终结果与我们预想不同。
所以:
尽量不要使用带有副作用的宏参数,以避免产生一些不可预料的结果。
5. 宏和函数对比
通过前面的学习,不知道大家有没有一种感觉,就是宏和函数好像有一点相似:
好像都是我们给它传一些参数,然后它给我们返回一个结果。
但是,事实上它们是两个不同的东西。
接下来,我们就对宏和函数进行一个对比:
宏通常被应用于执行简单的运算。
比如:求一下两个数的最大值。
当然,要解决这个问题,我们定义一个宏或者写一个函数都是可以的。
用函数:
int Max(int x, int y) { return (x > y ? x : y); } int main() { int a = 10; int b = 20; int max = Max(a, b); printf("%d\n", max); return 0; }
用宏:
#define MAX(x,y) ((x)>(y)?(x):(y)) int main() { int a = 10; int b = 20; int max = MAX(a, b); printf("%d\n", max); return 0; }
当然也是可以的:
5.1 宏的优点
那现在大家思考一下,就对于实现这个小问题来说,函数和宏用哪一个更好?
使用宏更好,优先选择宏的方法。
那为什么呢?
原因有二:
对于这样一个非常简单的运算来说:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
所以对于简单的运算来说,宏比函数在程序的规模和速度方面更胜一筹。
2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。
反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。
宏是类型无关的。
5.2 宏的缺点
当然和函数相比宏也有劣势的地方:
每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
我们知道宏是进行替换的,那我们使用一次宏,就会进行一次替换,如何用的次数比较多,而且宏定义的代码也比较长,可能就会导致程序的长度大幅度增长。
而对于函数来说,我们定义好的函数在程序中只留存一份,我们每次使用,只需要调用一下就行了。
宏是没法调试的。
宏由于类型无关,也就不够严谨。
这是它的一个优点,但同时也是缺点。
4.宏可能会带来运算符优先级的问题,导致程容易出现错。
这个其实我们在讲宏的时候就演示过了,在使用宏的时候,有些地方如果我们不加括号,可能就会导致由替换产生的表达式因为操作符优先级的问题而并没有按照我们预想的次序进行求值。
5.3 宏能做一些函数做不到的事情
宏有时候可以做函数做不到的事情。
比如:宏的参数可以是一种数据类型,但是函数不行。
举个栗子:
之前的文章里我们已经学过了动态内存开辟。
如果我们想开辟40个字节的空间放整型数据,可以这样搞:
int main() { int* p = (int*)malloc(sizeof(int) * 10); return 0; }
而且我们可以实现这样一个宏:
#define MALLOC(num,type) (type*)malloc(num*sizeof(type)) int main() { int* p = MALLOC(10, int); return 0; }
这样是不是更加方便,我们直接把元素个数和类型传给宏,它就帮我们开辟好空间了。
但是:
这件事宏可以完成,函数就不行,因为函数不能传一个数据类型作为参数吧。
5.4 总结
总的来说:
宏和函数各有优劣,没有决对的谁好谁坏,我们在不同的情况下选择适合的就行了。