【C/C++内功心法】剖析预处理过程,详解预处理指令,提升C/C++内功
文章目录
前言
一、预定义符号
二、#define
1 #define 定义标识符
2 #define 定义宏
3 #define 替换规则
4 #和##
5 带副作用的宏参数
总结
前言
大家好啊,我是不一样的烟火a,今天我将会为大家详细讲解预处理指令。虽然本文章读完后不能让大家代码写得飞起,但是预处理这个过程是十分重要的,其中的很多指令也是经常被考到,你了解了它,它将会大幅提升你的C/C++内功,让你学编程更加的容易。
一、预定义符号
注意:下面这些预定义符号都是语言内置的,可以直接拿来用。
__FILE__ // 进行编译的源文件
__LINE__ // 文件当前的行号
__DATE__ // 文件被编译的日期
__TIME__ // 文件被编译的时间
__STDC__ // 如果编译器遵循ANSI C,其值为1,否则未定义
__func__ // 获取当前所在函数
举例:
#include<stdio.h>
int main()
{
printf("name: %s, file: %s, line: %d, date: %s, time: %s\n", __func__, __FILE__, __LINE__, __DATE__, __TIME__);
return 0;
}
预定义符号的作用
- 我们可以将上面这些文件的信息写到日志里面,如果程序出现错误,我们可以很好的定位到是哪个文件的哪个函数出错了,并且知道文件是在什么时候编译的。
举例:
#include<stdio.h>
int main()
{
FILE* pf = fopen("log.txt", "a"); // 打开log.txt这个文件
if (pf == NULL)
{
return 1;
}
for (int i = 0; i < 10; ++i)
{
// 将所有文件信息写入log.txt文件
fprintf(pf, "name: %s, file: %s, line: %d, date: %s, time: %s, i=%d\n", __func__, __FILE__, __LINE__, __DATE__, __TIME__, i);
}
return 0;
}
我们打开log.txt文件,这时就将所有的文件信息都写了进来。
__STDC__ 预定义符号
如果编译器遵循ANSI C,其值为1,否则未定义
举例:
int main()
{
printf("%d\n", __STDC__);
return 0;
}
在Windows的vs2019下
- 编译器报错:未定义标识符“__STDC__”
- 说明vs2019不遵循ANSI C
在Linux的gcc下
- 打印出来__STDC__的值为1
- 说明gcc遵循ANSI C
二、#define
1 #define 定义标识符
语法:
#define name stuff
功能:
- 在预处理阶段,将代码中所有的name替换成stuff。
举例:
#define NUM 666
#define STR "hello"
int main()
{
int num = NUM;
char* str = STR;
return 0;
}
在预处理阶段,上面的代码将会被替换成:
由于我们定义的标识符已经被替换了,所以替换后,#define 定义标识符将会被删除。
int main()
{
int num = 666;
char* str = "hello";
return 0;
}
怎么验证?
- 点击视图,然后打开解决方案资源管理器。
- 右击此处。
- 点击属性。
- 点击C/C++,然后进入预处理器,将预处理到文件这里的选项改成“是”
- 将当前文件编译一下,然后去当前路径下的Debug文件夹里面可以找到一个test.i文件(这就 是预处理完后生成的文件),然后将其打开。
- 现在就可以看到替换前后的区别了。
提问:
- 在define定义标识符的时候,要不要在最后加上分号 " ; " ?
- 答案是不用。
如果我们在刚刚define定义标识符的最后加上" ; " ,那么预处理后的结果将会是下面这样,这就会出现语法错误。
当然define还可以定义其他标识符,可以是个关键字,也可以是一段代码。
#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__ )
2 #define 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
宏的申明方式:
- #define name( parament-list ) stuff
- 其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
- 参数列表的左括号必须与name紧邻。
- 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
举例:
// 用于求两数中的较大值
#define MAX(x, y) (x > y ? x : y)
int main()
{
int a = 10;
int b = 20;
int c = MAX(a, b);
printf("%d\n", c);
return 0;
}
替换后的结果为:
注意:如果你想运行当前代码,需要将刚才的设置改回来才行,因为经过刚才的设置后,编译器将代码预处理完就会停下来。
刚才代码的运行结果。
注意:定义宏的时候必要的括号不能少,因为宏的本质还是替换。
举例:
我们写一个求平方的宏:
#define SQUARE( x ) x * x
这个宏接收一个参数 x ,
如果在上述声明之后,你把
SQUARE( 5 );
置于程序中,预处理器就会用下面这个表达式替换上面的表达式:
5 * 5
注意: 这个宏存其实在一个问题,观察下面的代码段:
int a = 5;
printf("%d\n" ,SQUARE( a + 1) );
乍一看,你可能觉得这段代码将打印36这个值。 事实上,它将打印11,为什么?
替换文本时,参数x被替换成a + 1,所以这条语句实际上变成了:
printf ("%d\n",a + 1 * a + 1 );
这样就比较清晰了,由替换产生的表达式并没有按照预想的次序进行求值。
解决办法:在宏定义上加上两个括号,这个问题便轻松的解决了:
#define SQUARE(x) (x) * (x)
这样预处理之后就产生了预期的效果:
printf ("%d\n",(a + 1) * (a + 1) );
这里还有一个宏定义:
#define DOUBLE(x) (x) + (x)
定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。
int a = 5;
printf("%d\n" ,10 * DOUBLE(a));
这将打印什么值呢?
看上去,好像打印100,但事实上打印的是55,我们发现替换之后:
printf ("%d\n",10 * (5) + (5));
乘法运算先于宏定义的加法,所以出现了55的结果。
解决办法:在宏定义表达式两边加上一对括号就可以了:
#define DOUBLE(x) ( ( x ) + ( x ) )
所以我们这里就可以将上面写的求较大值的宏优化一下。
#define MAX(x, y) ((x) > (y) ? (x) : (y))
提示:
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中 的操作符或邻近操作符之间不可预料的相互作用。
3 #define 替换规则
在程序中扩展#define定义符号和宏时,需要涉及以下几个步骤:
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
举例:
#define DOUBLE(x) ((x) + (x))
#define NUM 66
int main()
{
int a = DOUBLE(NUM);
return 0;
}
这里就会先将:
int a = DOUBLE(NUM);
替换成:
int a = DOUBLE(66);
然后再替换成:
int a = ((66) + (66));
注意:
- 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
举例:
这里的NUM将不会被替换。
#define NUM 66
int main()
{
printf("NUM is a macro\n");
return 0;
}
4 #和##
我们先来看一段代码:
int main()
{
int a = 10;
printf("the value of a is %d\n", a);
int b = 20;
printf("the value of b is %d\n", b);
return 0;
}
运行结果:
- 我们发现上面的两行代码十分是相似,运行的结果也只有两处不同,那么我们可以把刚刚那两行代码(printf那两行)封装成一个函数吗?这样我们就不用每打印一个变量,都要单独写一个printf函数了。
- 答案是可以,但是十分的复杂,使所以这时就需要用我们的宏了。
但是我们知道,宏是不能替换字符串常量的内容的,那么如何把参数插入到字符串中?
使用#
作用:把一个宏参数变成对应的字符串。(比如:N是一个宏参数,我们使用#N,然后传过去a,那么a就会自动变成字符串 "a")
举例:
#include<stdio.h>
#define PRINT(N) printf("the value of " #N " is %d\n", N)
int main()
{
int a = 10;
PRINT(a); // printf("the value of a is %d\n", a);
int b = 20;
PRINT(b); // printf("the value of b is %d\n", b);
return 0;
}
这里的:
PRINT(a);
将会被替换成: (提示:#N被替换成了 "a",N被替换成了 a)
printf("the value of " "a" " is %d\n", a);
运行结果:
和上面用两个printf打印出来的结果一模一样。
额外补充:
C语言规定,打印字符串时可以将一个字符串分成几个子串写入。
举例:
int main()
{
printf("san lian\n");
printf("san" " " "lian\n");
return 0;
}
运行结果:
如果我们想要打印不同类型的变量,可以像下面这样:
#include<stdio.h>
#define PRINT(N, format) printf("the value of " #N " is " #format "\n", N)
int main()
{
int a = 20;
PRINT(a, %d); // printf("the value of a is %d\n", a);
double pai = 3.1415926;
PRINT(pai, %lf); // printf("the value of pai is %lf\n", pai);
return 0;
}
运行结果:
## 的作用:
- ##可以把位于它两边的符号合成一个符号。
- 它允许宏定义从分离的文本片段创建标识符。
举例:
#include<stdio.h>
#define CAT(name1, num) name1##num
int main()
{
int sanlian333 = 666;
printf("%d\n", CAT(sanlian, 333));
return 0;
}
这里的:
printf("%d\n", CAT(sanlian, 333));
将会被替换成:
printf("%d\n", sanlian333);
运行结果:
注意:像上面这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
5 带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
例如:
int x = 1;
int a = x+1; // 不带副作用(x的值没有改变)
int b = ++x; // 带有副作用(x的值被改变)
MAX宏可以证明具有副作用的参数所引起的问题。
#include<stdio.h>
#define MAX(x, y) ((x) > (y) ? (x) : (y))
int main()
{
int a = 5;
int b = 8;
int c = MAX(a++, b++); // 被替换成 int c = MAX((a++) > (b++) ? (a++) : (b++));
// 你能不运行,说出下面的结果吗?
printf("%d\n", a);
printf("%d\n", b);
printf("%d\n", c);
return 0;
}
运行结果:
最后的结果是不是意料之外,却又在情理之中。(其实稍微细心点还是很容易看出答案的,但是稍不留神就有可能出错哦)
提示:所以为了避免出现不可预测的后果,在写宏的时候,参数部分尽量不要随便写这种带副作用的参数。
总结
还是那句话,虽然本文章读完不能让大家代码写得飞起,但是预处理这个过程是十分重要的,其中的很多指令也是经常被考到,只有了解了这个,你学编程才会更加的容易。当然我在这里只为大家讲了所有预处理指令里面十分重要的一些指令,如果大家还想深入的了解更多预处理指令,推荐大家可以去看看《C语言深度解剖》这本书。如果大家有什么解决不了的问题,欢迎大家评论区留言或者私信告诉我。如果感觉对自己有用的话,可以点个赞或关注鼓励一下博主,我会越做越好的,感谢各位的支持。