目录
引言
预定义符号
define 定义常量
#define 定义宏
带有副作用的宏参数
宏替换的规则
宏和函数的对比
引言
在C语言编程中,预处理是编译前的关键步骤,它负责处理如宏定义、条件编译和文件包含等指令。今天我们来学习一下有关C语言——预处理的内容。
预处理部分分两篇。
预定义符号
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;
}
运行结果:
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__ )
1.定义了一个常量 MAX ,值为100。
2.为 register 关键字创建别名 reg 。
3.定义了一个无限循环的do_forever。
4.在 switch 语句的 case 前自动添加 break ,但是最好不要这样子使用。
5.续行符可以防止分行后出现问题。
接下来我们来思考一下:
在define定义标识符的时候,要不要在最后加上 ; ?
比如这样:
#define MAX 1000;
#define MAX 1000
来看看如下场景:
#define MAX 1000;
int main()
{
int max = 0;
if (1)
max = MAX;
else
max = 1;
return 0;
}
像在这段代码中,MAX相当于是 1000; ,代码就相当于变成了这种:
#define MAX 1000;
int main()
{
int max = 0;
if (1)
max = MAX;
;
else
max = 1;
return 0;
}
这样子最终导致了 else 缺少 if 与其配对。
因此,建议在define定义标识符的时候,最好不要在最后加上 ; 。
#define 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或定义宏。
下面是宏的声明方式:
#define name(parameters) stuff
这里的 name 是你定义的宏的名称, parameters 是宏的参数列表(可以是空的,也可以包含一个或多个参数),而 stuff 是宏展开时将要替换成的文本。如果 stuff 中包含了 parameters 中的参数,那么这些参数在宏展开时会被实际的参数值所替换。
注意:
参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
举个例子:
//实现一个宏,计算一个数的平方
#define SQUARE(x) x*x
int main()
{
int a = 5;
printf("%d\n", SQUARE(a + 1));
return 0;
}
按照我们的思考逻辑,答案显然是36,但是我们运行之后会发现:
答案居然是11?!
为什么会出现这种情况呢?问题就出在宏上。
我们知道宏是直接替换的,上面的代码经过替换后就变成了如下形式:
printf("%d\n", a+1*a+1);
这样子算出来的结果就符合运算结果了。
为了避免出现这种情况,我们可以对宏进行如下修改:
#define SQUARE(x) (x)*(x)
这样子就符合预期了
我们接下来再来看一个示例:
#define DOUBLE(x) (x) + (x)
int main()
{
int a = 5;
printf("%d\n", 5 * DOUBLE(a));
return 0;
}
这个的运算结果为:30
这显然也是不符合我们的预期的。上面的代码经过替换后变成:
printf("%d\n", 5 * 5 + 5);
解决方法如下:
#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++))
1.首先判断:x++ 与 y++ ,由于是后置++,判断时 x 为 5,y 为 8,8 > 5。
2.判断完后x自增1变为6,y自增为9。
3.接着执行 y++,由于是后置++,返回结果为9。
4.再接着 y 进行自增,y 最终结果为10。
当我们向宏中传递有副作用的参数,而并且参数在宏中出现了不止一次,那么该参数的副作用也不止一次。
为了避免这种问题,最好不要在宏的参数中使用带有副作用的表达式(如递增或递减操作符)。
宏替换的规则
在程序中扩展 #define 定义符号和宏时,需要涉及几个步骤:
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
来看个简单的例子:
#include<stdio.h>
#define a 100
int main()
{
printf("%d\n",a);
return 0;
}
在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
注意:
宏参数 和 #define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
宏里面是可以嵌套宏的,例如这样:
MAX(x, MAX(2, 3))
递归宏在宏定义中通常不推荐使用,且容易出错
当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索。
举个例子:
#include <stdio.h>
#define HELLO "Hello, "
#define WORLD "world!"
int main()
{
// 字符串常量中的HELLO和WORLD不会被预处理器替换,因为它们不是作为printf的参数传递的
printf("这是一个测试:HELLO WORLD\n"); // 输出: 这是一个测试:HELLO WORLD
// 正确的做法是使用格式化字符串和多个参数,此时HELLO和WORLD会被替换
printf("这是一个测试:%s%s\n", HELLO, WORLD); // 输出: 这是一个测试:Hello, world!
return 0;
}
输出结果如下:
宏和函数的对比
宏通常被应用于执行简单的运算。
比如在两个数中找出较大的⼀个时,写成下面的宏,更有优势⼀些。
例如这样:
#define MAX(a, b) ((a)>(b)?(a):(b))
宏和函数都能完成需求,为什么说在执行简单的运算这一需要时,宏更加有优势呢?
原因如下:
1.用于 调用函数 和 从函数返回的代码 可能比实际执行这个小型计算工作所需要的时间更多。所以 宏 比 函数 在程序的规模和速度方面更胜⼀筹。
2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏的参数是类型无关的。
似乎宏相较于函数更加厉害,然而,宏并不适合做复杂、大的运算。
相较于函数,宏也有很多劣势:
1.每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是没法调试的。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程序容易出现错误。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
例如这样:
int* p = (int*)malloc(5 * sizeof(int));
我们觉得这样子使用malloc函数太麻烦了,我们可以这样子:
Malloc(5, int);
函数显然是不可以做到的:函数不能传递类型。
宏是可以做到的,宏能接收类型。
#define MALLOC(num, type) ((type*)malloc(num * sizeof(type)))
int main()
{
int* p = MALLOC(5, int);
if (p == NULL)
{
perror("malloc fail:");
return 1;
}
free(p);
return 0;
}
宏和函数的对比表格:
属性 | #define 定义宏 | 函数 |
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长。 | 函数代码值出现于一个地方,每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,相对慢点 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则临近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多加括号 | 函数参数只有在函数调用的时候求值一次,它的结果值传递给函数,表达式的求值结果更容易预测 |
带有副作用的参数 | 参数可能被替换到宏中的多个位置,如果宏的参数被多次计算,带有副作用的参数求值可能会产生不可预估的结果 | 函数参数只有在传参时调用一次,结果更容易预测 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,他就可以使用于任何参数类型 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是相同的 |
调试 | 宏不方便调试 | 函数可以逐语句调试 |
递归 | 宏不能递归 | 函数可以递归 |
———————————————————————————————————————————
以上是预处理的部分内容,由于篇幅原因,欢迎大家去查看预处理的下部分——C语言——预处理详解(下)
希望大家能点赞收藏支持下!!!