【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 66int 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 66int 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##numint 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语言深度解剖》这本书。如果大家有什么解决不了的问题,欢迎大家评论区留言或者私信告诉我。如果感觉对自己有用的话,可以点个赞或关注鼓励一下博主,我会越做越好的,感谢各位的支持。
【C/C++内功心法】剖析预处理过程,详解预处理指令,提升C/C++内功相关推荐
- linux系统启动过程详解-开机加电后发生了什么 --linux内核剖析(零)
本文参考了如下文章 深入理解linux启动过程 mbr (主引导记录(Master Boot Record)) 电脑从开机加电到操作系统main函数之前执行的过程 详解linux系统的启动过程及系统初 ...
- Xposed源码剖析——app_process作用详解
Xposed源码剖析--app_process作用详解 首先吐槽一下CSDN的改版吧,发表这篇文章之前其实我已经将此篇文章写过了两三次了.就是发表不成功.而且CSDN将我的文章草稿也一带>删除掉 ...
- c语言的编译过程详解
c语言的编译过程详解 IDE的使用让很多和我一样的人对C/C++可执行程序的底层生成一知半解,不利于我们深入理解原理.在这里小结一下,望路过的大神指正~ 前言:从一个源文件(.c文件)到可执行程序到底 ...
- 超级超级详细的实体关系抽取数据预处理代码详解
超级超级详细的实体关系抽取数据预处理代码详解 由于本人是代码小白,在学习代码过程中会出现很多的问题,所以需要一直记录自己出现的问题以及解决办法. 废话不多说,直接上代码!!! 一.data_proce ...
- 图像特征提取(VGG和Resnet特征提取卷积过程详解)
图像特征提取(VGG和Resnet算法卷积过程详解) 第一章 图像特征提取认知 1.1常见算法原理和性能 众所周知,计算机不认识图像,只认识数字.为了使计算机能够"理解"图像,从而 ...
- hadoop作业初始化过程详解(源码分析第三篇)
(一)概述 我们在上一篇blog已经详细的分析了一个作业从用户输入提交命令到到达JobTracker之前的各个过程.在作业到达JobTracker之后初始化之前,JobTracker会通过submit ...
- Hadoop学习之Mapreduce执行过程详解
一.MapReduce执行过程 MapReduce运行时,首先通过Map读取HDFS中的数据,然后经过拆分,将每个文件中的每行数据分拆成键值对,最后输出作为Reduce的输入,大体执行流程如下图所示: ...
- python的执行过程_在交互式环境中执行Python程序过程详解
前言 相信接触过Python的伙伴们都知道运行Python脚本程序的方式有多种,目前主要的方式有:交互式环境运行.命令行窗口运行.开发工具上运行等,其中在不同的操作平台上还互不相同.今天,小编讲些Py ...
- 安卓 linux init.rc,[原创]Android init.rc文件解析过程详解(二)
Android init.rc文件解析过程详解(二) 3.parse_new_section代码如下: void parse_new_section(struct parse_state *state ...
- JetBrains DataGrip工具配置数据库过程详解
JetBrains DataGrip工具配置数据库过程详解 DataGrip是一款数据库管理客户端工具,方便连接到数据库服务器,执行sql.创建表.创建索引以及导出数据等. DataGrip 是 Je ...
最新文章
- 准备战争“软测试”之DB基础知识
- 解决IntelliJ无法导入maven包的问题
- javaSE基础04
- Arduino学习笔记07
- break和continue-continue代码演练
- java aop 实例_Spring aop 简单示例
- linux内核配置usb虚拟串口,Linux USB虚拟串口设备
- 计算机用公式找出第一名,用公式查找Excel工作表中重复数据
- EIGRP路由汇总与安全性配置
- “fatal error C1010”错误解决的三种方法
- asp程序ajax怎么写,ASP+AJAX+ACCESS数据库实例讲解三个步骤分享
- InVEST实践与进阶及在生态系统服务供需、固碳、城市热岛、论文写作等实际项目中的具体应用
- Unity 导入原神人物模型
- GUI图形用户接口编写QQ登录界面
- 蓝桥杯历届试题-回文数字
- arduino loar_「雕爷学编程」Arduino动手做(15)---手指侦测心跳传感器
- 在springboot中导入spring-web相关包导致的错误经验(一)
- numpy第三章-索引器、多级索引
- 2021年低压电工免费试题及低压电工考试技巧
- nose 测试框架使用