【C语言】 —— 预处理详解(上)
- 一、预定义符号
- 二、# d e f i n e define define 定义常量(符号)
- 三、# d e f i n e define define 定义宏
- 四、带有副作用的宏参数
- 五、宏替换的规则
- 六、宏和函数的对比
一、预定义符号
C语言中设置了一些预定义符号,可以直接使用,预定义符号也就是在预处理期间处理的
。
__FILE__ //进行编译的源文件 __LINE__ //文件当前的行号 __DATE__ //文件被编译日期 __TIME__ //文件被编译那一瞬的时间 __STDC__ //如果编译器遵循ANSI C(标准C),其值为1,否则未定义(报错)
举例:
#include<stdio.h> int main() { printf("%s\n", __FILE__); printf("%d\n", __LINE__); printf("%s\n", __DATE__); printf("%s\n", __TIME__); return 0; }
运行结果:
这里随便提一下,VS 不完全支持ANSI C(标准C); g c c gcc gcc 支持ANSI C(标准C)
二、# d e f i n e define define 定义常量(符号)
基本语法:
# define name stuff
举个例子:
#define MAX 100 #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__ )
- 第二句就是懒,咋地
- 第三句 f o r for for 循环的
初始化、判断、调整都可以省略
,但是判断如果省略,则意味着判断条件恒为真,即死循环- 第四局最好别这么搞,很容易出事的
- 第五句续行符是防止分行后出现问题,其本质是
转义
后面的回车符
,让回车不再是回车。续行符后面什么都不能有,按下“\”
直接回车,否则续的就不是下面一行的代码了
现在问题来了:用 # d e f i n e define define 定义标识符的时候,要不要在后面加上;
?
比如:
#define MAX 1000 #define MAX 1000; int main() { int n = MAX; return 0; }
上述代码加上;
,好像只是有点多余,但对程序运行并没有什么影响
好像加 ;
或者不加 ;
都可以?
真的是这样的吗?
我们看下面的例子:
//例一 int main() { printf("%d\n", MAX); return 0; } //例二 int mian() { int max = 0; if (1) max = MAX; else max = 1; return 0 }
替换后:
printf("%d\n", 1000;);
打印1000;
是什么意思?
if (1) max = 1000; ; else max = 1;
e l s e else else 匹配谁?
你看,这就出问题了吧,所以用 # d e f i n e define define 定义表示符的时候,后面不要加 ;
三、# d e f i n e define define 定义宏
# d e f i n e define define 机制包括了一个规定,允许把参数替换到文本中
,这种实现通常称为宏 ( m a r c o ) (marco) (marco)或 定义宏 ( d e f i n e m a c r o ) (define macro) (definemacro)
宏和上面宏定义标识符的区别就是:宏有参数
下面是宏的声明方式:
#define name(parament - list) stuff
其中的 p a r a m e n t parament parament - l i s t list list(参数列表)是一个由逗号隔开的符号表,他们可能出现在 s t u f f stuff stuff 中
注: p a r a m e n t parament parament - l i s t list list(参数列表)的左括号必须与 n a m e name name紧邻,如果之间有任何空白存在,参数列表就会被解释为 s t u f f stuff stuff 的一部分。
举例:
//实现一个宏,计算一个数的平方 #define SQUARE(x) x*x int main() { int a = 5; int ret = SQUARE(a); printf("%d\n", ret); return 0; }
运行结果:
可以看到,正确运算出 5 的平方
但其实上述代码是有问题的,请看下面代码段:
int a = 5; printf("%d\n", SQUARE(a + 1));
运行结果:
为什么会是这样呢? 5+1 的结果是 36,而 6 ∗ * ∗ 6 应该是 36 才对,11 是怎么得来的呢?
问题出现在宏上,我们知道宏是直接替换
的,那么上述代码直接替换的结果:
printf("%d\n", a+1*a+1);
5 + 6 + 1,结果自然是 11 了
我们在宏定义两边加上括号,这个问题就轻松解决了
#define SQUARE(x) (x)*(x)
那这么定义就毫无问题了吗?我们来看看下面这个宏定义
#define DOUBLE(X) (X)+(X)
定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。
int a = 5; printf("%d\n", 10 * DOUBLE(a));
运行结果:
输出结果不是100 而是 55,原因和上面类似,依然是 优先级的问题
解决方法:
#define DOUBLE(X) ((X)+(X))
综上,在使用宏的时候千万不要吝啬括号,以避免在使用宏是由于参数中的操作符或者邻近操作符之间不可预料的相互作用。
四、带有副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测
的结果。副作用就是表达式求值的时候出现永久性的效果。
例如:
x + 1; //不带副作用 x++; //带副作用
下面代码的 MAX 宏可以证明具有副作用的参数所引起的问题
#define MAX(a,b) ((a) > (b) ? (a):(b)) int main() { int x = 5; int y = 8; int z = MAX(x++, y++); printf("x=%d, y=%d, z=%d\n", x, y, z); return 0; }
运行结果:
为什么会这样呢?我们一起来分析分析
z = ((X++) > (y++) ? (x++) : (y++))
- 首先先进行判断: x x x++ 与 y y y++ 判断,因为是 后置++,判断时 x x x 为 5, y y y 为 8,8 > 5
- 判断完后 x x x 为 6, y y y 为 9
- 再接着执行 y y y ++,因为是 后置++,返回结果 9
- 再接着 y y y 进行自增, y y y 最终结果为 10
我们将 x x x 和 y y y 传入宏中,出来的结果都已经改变了,特别是 y y y,经过了两次改变,你说可不可怕
当向宏中传递有副作用的参数,而并且参数在宏中出现了不止一次,那么该参数的副作用也不止一次
五、宏替换的规则
在程序中扩展 # d e f i n e define define 定义的符号和宏时,需要涉及几个步骤
- 在调用宏时,先对
参数进行检查
,看看是否包含 # d e f i n e define define 定义的标识符
。如果是,他们首先被替换 - 替换文本随后
被插入
到程序中原来的位置,对于宏参数名被他们的值所替换
#define MAX(a,b) ((a) > (b) ? (a):(b)) #define M 10 int main() { int x = 5; int z = MAX(x, M); return 0; }
- M A X ( x , M ) MAX(x, M) MAX(x,M) 中的 M M M 首先被替换成 10,10 插入到原来 M M M 所在的位置
- 最后,再次对结果进行扫描,看看是否包含任何由 # d e f i n e define define 定义的符号,如果是,就重复上述处理了过程
上述代码中 MAX 也是由 # d e f i n e define define 定义的宏。上一次检验中,它的参数 M 已经完成了替换,这次该替换它了
M A X ( x , 10 ) MAX(x, 10) MAX(x,10) 被替换成 ( ( x ) > ( 10 ) ((x) > (10) ((x)>(10) ? ? ? ( x ) : ( 10 ) ) (x):(10)) (x):(10))
当然,宏里面嵌套宏
也是可以的
MAX(x, MAX(2, 3))
这时,先将参数中的宏进行替换,再对整个宏进行替换
但需要注意的是,这不是递归
,这只是一个宏作为另一个宏的参数。递归是宏内部又调用了宏本身
。
同时,当预处理器搜索 # d e f i n e define define 定义的符号时,字符串常量的内容并不被搜索
什么意思呢?举个栗子就明白了
#define M 10 int main() { int x = 5; int z = MAX(x, MAX(2, 3)); printf("M = %d", x); return 0; }
上述代码中printf("M = %d", x);
中的 M M M 就不被替换成 10
六、宏和函数的对比
上述用宏求两个数的较大值,我们完全可以写成函数
int Max(int x, int y) { return x > y ? x : y; }
我们发现他们都能完成同样的功能。但就对于 “求两个数的较大值” 这个功能而言,写成宏会更有优势
一些
原因有二:
- 用于
调用函数
和函数返回
的代码可能比实际执行这个小型计算工作所花费的时间更多(调用函数时要建立栈帧)。所以宏比函数在程序的规模
和速度
上更胜一筹- 更为重要的是
函数的参数必须声明类型
,这就导致函数只能在类型合适的表达式上使用。反之,这个宏可以适用多种类型:整形、长整形、浮点型等都可以用>
来比较。宏的参数是类型无关的。
那是不是以后都用宏呢?其实宏只使用于简单计算,不适合做那些复杂的、大的运算和函数相比宏的 劣势:
- 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能会
大幅度增加程序的长度
- 宏是
没法调试
的- 宏由于类型无关,也就
不够严谨
- 宏可能会带来运算优先级的问题,导致程序
容易出错
但有些时候,宏可以做到函数做不到的事情
例如:
int* p = (int*)malloc(10 * sizeof * (int));
我们嫌这样写 m a l l o c malloc malloc 函数太麻烦了,我想把大小
和类型
传过去就能开辟空间
Malloc(10, int);
函数能不能做到呢?不行,因为函数是不能传递类型的
而宏可以做到,因为宏压根不检查你参数的类型的
#define Malloc(n, type) (type*)malloc(n * sizeof(type)) int main() { int* p = Malloc(10, int); return 0; }
宏和函数的一个对比:
属性 | # d e f i n e define define定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中,除了非常小的宏外,程序的长度会大幅度增长 | 函数代码值出现于一个地方,每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销(开辟栈帧),先对慢些 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则临近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多加括号 | 函数参数只有在函数调用的时候求值一次,它的结果值传递给函数,表达式的求值结果更容易预测 |
带有副作用的参数 | 参数可能被替换到宏中的多个位置,如果宏的参数被多次计算,带有副作用的参数求值可能会产生不可预估的结果 | 函数参数只有在传参时调用一次,结果更容易预测 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,他就可以使用于任何参数类型 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是相同的 |
调试 | 宏是不方便调试的 | 函数是可以逐句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
那有没有什么办法把他们的优点结合起来呢
在 C++ 中引入了内联函数 i n l i n e inline inline —— 既又宏优点又有函数的优点
它的执行速度和宏一样快,但效果又和函数一样