【C语言】预处理(预编译)详解(上)(C语言最终篇)

在这里插入图片描述

文章目录

  • 一、预定义符号
  • 二、#define定义常量
  • 三.、#define定义宏
  • 四、带有副作用的宏参数
  • 五、宏替换的规则
  • 六、宏和函数的对比
    • 1.宏的优势
    • 2.函数的优势
    • 3.宏和函数的命名约定

一、预定义符号

   学习本篇文章的内容推荐先去看前面的编译和链接,才能更好地理解和吸收,文章链接:【C语言】编译和链接(编译环境和运行环境)
   C语⾔设置了⼀些预定义符号,可以直接使⽤,预定义符号也是在预处理期间处理的,如下:

_ _FILE_ _ 
_ _LINE_ _ 
_ _DATE_ _ 
_ _TIME_ _ 
_ _STDC_ _ 

   我们需要注意的是,使用这些预定义符号的时候,下面的两个短下划不能少,并且两个短下划线之间是没有间隙的,由于写文章时两个短下划线会进行合并,所以这里我加上了空格,但是在实际使用时,下面的两个短下划线之间没有间隙
   接下来我们来再详细介绍一下它们(在描述时我会省略短下划线,但是我们要注意,那些下划线也属于预定义符号的一部分,使用时必须加上):

  1. FILE代表当前进行编译的源文件,在打印时,需要使用占位符%s,它不仅会打印文件名,还会打印文件的完整路径
  2. LINE代表出现了这个预定义符号的行号,比如这个预定义符号出现在第6行时,那么它就代表6,所以需要使用%d进行打印
  3. DATE代表文件被编译时的日期,打印时需要使用占位符%s
  4. TIME代表文件被编译时的具体时间,具体到时分秒,打印时也是使用占位符%s
  5. STDC就与编译文件的编译器有关了,如果编译当前文件的编译器完全遵守了ANSI C标准,那么它将会被定义,并且值为1,打印时需要使用%d,如果该编译器不完全遵守ANSI C标准,那么STDC这个预定义符号就没有被定义过,如果使用它就会报错

   接着我们就来使用一下这几个预定义符号,首先我们来使用前4个预定义符号,来打印我们源文件在编译时的各种信息,如下:

#include <stdio.h>int main()
{printf("FILE: %s\n", __FILE__);printf("LINE: %d\n", __LINE__);printf("DATE: %s\n", __DATE__);printf("TIME: %s\n", __TIME__);return 0;
}

   我们来看看代码运行结果:
在这里插入图片描述
   我们来看看运行结果是否如同我们上面说的那样,首先FILE会打印源文件的完整路径,LINE会打印它出现时的行号,可以看到LINE确实是在第8行出现,DATE打印的就是文件被编译的日期,是2024年10月26日,也没有问题,最后就是TIME,也确实打印了文件编译时的时分秒
   接着我们就可以使用STDC这个预定义符号,来判断我们的编译器是否完全遵循ANSI C,如下:

#include <stdio.h>int main()
{printf("STDC: %d", __STDC__);return 0;
}

   接着我们在VS2022这个IDE上面运行一下,结果如图:
在这里插入图片描述
   可以看到VS2022在运行时报错了,不认识这个标识符,说明我们的VS2022并没有严格遵守ANSI C标准,可能遵守了%99,但是就是没有完全遵守

二、#define定义常量

   #define定义常量的基本语法如下:

#define name stuff

   其中的name就是我们定义的常量的名称,stuff就是我们定义的常量的值,可以是整型,可以是字符串,也可以是字符等等
   接着我们就使用#define来定义各种类型的常量,我们要注意的一点是,在取名时我们的常量名最好全部大写,这是我们编程的一种习惯,如下:

#include <stdio.h>#define MAX 100
#define STR "I am Sam!"
#define CH 'x'int main()
{printf("MAX: %d\n",MAX);printf("STR: %s\n",STR);printf("CH : %c\n", CH);return 0;
}

   我们来看看运行结果:
在这里插入图片描述   可以看到这些常量都可以正常使用
   接着我们思考一个问题,在#define定义常量时,后面是否要加分号?比如:

 #define MAX 100;#define MAX 100

   我们首先要知道#define定义常量时是怎么工作的,它会直接把常量名替换为对应的值,在第一条语句中,MAX就代表了100; ,而在第二条语句中,MAX就只代表100
   所以很明显我们在使用#define定义常量时,最好不要在后面加上分号,那么为什么有时候加上分号也没有问题呢?如下:

#define MAX 100;int a = MAX;

   当我们运行这条语句时,发现不会出错,这是为什么呢?我们只需要把它替换一下就知道了:

int a = 100;;

   现在相当于就是语句后多了一条分号,前一个分号就是这条语句的结束标志了,第二个分号相当于是一个空语句,什么也没有做,所以这句话就相当于了两条语句,第二条语句是空语句,什么也没有做,所以执行起来没有问题
   但是这种情况也不是我们使用#define定义常量的初衷,我们只是想要使用MAX表示100而已,并不想要带上那个分号,并且加上分号后,有很多情况会出错,所以我们使用时就最好不要在#define定义常量时在后面加上分号

三.、#define定义宏

   #define 机制包括了⼀个规定,允许把参数替换到⽂本中,这种实现通常称为宏(macro)或定义宏(definemacro),下面是宏的声明方式:

#define name( parament-list ) stuff

   其中的parament-list 是⼀个由逗号隔开的符号表,它们可能出现在stuff中,要注意的是:参数列表的左括号必须与name紧邻,如果两者之间有任何空⽩存在,参数列表就会被解释为stuff的⼀部分
   是不是有点难懂,我们可以看如下的例子:

#define SQUARE( x )  x * x

   它的形式有点类似于函数,前面就相当于函数名,括号中就是宏的参数,后面是这个宏的计算方式,比如使用SQUARE(5),那么预处理后,就会把这条语句转化成5*5
   其中SQUARE和第一个小括号要紧紧贴在一起,如果两者之间有任何空⽩存在,那么(x)就会成为后面的一部分,就会出错
   那么我们上面写的这个宏是否就完全正确了呢?其实它还存在一个问题,比如我们来看一个例子:

#include <stdio.h>#define SQUARE( x ) x * xint main()
{int a = 5;printf("%d\n", SQUARE(a + 1));return 0;
}

   我们预期的结果是它帮我们算出6的平方36,那么它最后能否得到这个结果呢?我们来看看它的运行结果:
在这里插入图片描述
   我们可以惊奇的发现,程序运行的结果不应该是a+1,也就是6的平方36吗?为什么结果变成了11?这就要涉及到我们上面谈到过的,#define定义的内容是直接替换的,不会有任何的变化
   其中的x会直接被a+1替换,那么SQUARE(x)经过替换过后应该是如下的样子:

a + 1 * a + 1
//带入a=5
5 + 1 * 5 + 1

   这个时候就可以发现问题了,由于运算符的优先级,中间的1 * 5会优先计算,变成5,然后就是5+5+1,最后结果为11
   那么怎么解决运算符导致的错误呢?我们可以在定义宏的时候,把参数使用小括号括起来,让每个参数成为一个整体,无论怎么样都是参数内部先计算,最后再进行宏定义的运算,如下:

#define SQUARE( x ) (x) * (x)

我们将宏定义改成这样再来看看代码运行结果:
在这里插入图片描述
   那么这样是否就一定不会出错了呢?这里就不卖关子了,这样还是不能确保得到我们预期的结果,为什么呢?我们接着看一个例子:

#include <stdio.h>#define DOUBLE( x ) (x) + (x)int main()
{int a = 5;printf("%d\n", 10 * DOUBLE(a));return 0;
}

   按照我们的预期,宏DOUBLE会帮我们计算出一个数的2倍,那么这里5的2倍是10,乘以10过后就变成了100,那么我们来看最后的结果是否是100:
在这里插入图片描述
   可以看到结果又与我们预期的不一样了,这还是我们在预处理阶段出现的问题,还是因为#define使用宏的时候,会直接替换内容,上面的那条语句经过替换后如下:

10 * (a) + (a)
//将a替换成5之后
10 * (5) + (5)

   这个时候就可以看出来,由于*的优先级更高,所以10和前面那个5结合变成了50,然后+5变成了55,这就是55的由来,所以我们可以看出,光给每个参数加上()还不够,我们还最好把整个式子括起来,表示它们是一个整体,如下:

#define DOUBLE( x ) ((x) + (x))

   接着我们就拿这个宏定义来试试答案是否会变成我们预想的100,如图:
在这里插入图片描述
   可以看到最后结果就正确了,所以总结一下,在我们使用宏定义的时候,我们要使用()将每个参数括起来,保证每个参数是一个整体,最后我们还要使用()将整个式子括起来,保证整个式子是一个整体

四、带有副作用的宏参数

   宏参数还有副作用,是不是基本上没有听过这种说法,为什么会这么说呢?我们一起来学习一下:
   带有副作用的宏参数就是:当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可能出现危险,导致不可预测的后果,其中副作⽤就是表达式求值的时候出现的永久性效果
   我们可以举一个例子,如下:

//不带副作⽤
x+1;
//带有副作⽤
x++;

   乍一看这两者不是一样的吗?但其实并不一样,因为x++对x造成了永久性的效果,就是对x自增了一个1,而x+1这个表达式对x并没有影响
   接着我们来看一个例子来更好的理解,我们来定义一个宏,它的功能就是帮我们找到两个数中的最大数:

#include <stdio.h>#define MAX(x,y) ( (x) > (y) ? (x) : (y) ) int main()
{int a = 5;int b = 2;int ret = MAX(a++, b++);printf("a = %d b = %d ret = %d\n", a, b, ret);return 0;
}  

   这个例子的运行结果是什么呢?我们预期的结果是a变成6,b变成3,ret则是5,因为传参的时候使用的是后置++,所以是先使用a和b的值,也就是把5和2作为参数传过去后,然后a和b再++,所以a变成了6,b变成了3,ret还是5
   那么最后结果是否是我们预期的结果呢?如图:
在这里插入图片描述
   可以看到,最后结果和我们的预期又不一样,而且还相差的很远,这是为什么呢?这其实就是我们所说的带副作用的宏参数,那么引起它的本质是什么呢?没错,还是因为宏定义时的#define替换规则
   由于在预处理阶段,会将宏直接替换过来,所以上面的语句就变成如下语句:

( (a++) > (b++) ? (a++) : (b++) ) 

   在执行这条语句时,首先会执行(a++) > (b++),此时这里是后置++,所以a和b先使用再自增1,由于a是5,b是2,a>b成立了,然后对a和b进行自增1,a就变成了6,b就变成了3
   由于(a++) > (b++)的结果为真,所以最后整个三目表达式返回的就是a++的结果,由于这里还是后置++,所以返回的就是6,然后对a自增1变成7,所以最后ret的值就是6,a的值为7,b的值为3
   所以我们在使用宏的时候最好不要使用带副作用的宏参数,也就是使用后会对原本的参数造成永久性效果的表达式,例如++和- -操作

五、宏替换的规则

   在程序中扩展#define定义符号和宏时,需要涉及以下⼏个步骤,我们简单地了解一下:

  1. 在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先被替换
  2. 替换⽂本随后被插⼊到程序中原来⽂本的位置,不做任何更改,而对于宏,参数名被它们的值所替换
  3. 最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程

注意:

  1. 宏参数和#define定义中可以出现其他#define定义的符号,比如先使用#define定义一个常量N,值为100,那么这个N就可以在另一个#define中出现,但是对于宏,不能出现递归
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索

六、宏和函数的对比

1.宏的优势

   宏通常被应⽤于执⾏简单的运算,而函数则可以应用于较为复杂的场面,⽐如在两个数中找出较⼤的⼀个时,写成下⾯的宏,更有优势⼀些

#define MAX(x,y) ( (x) > (y) ? (x) : (y) ) 

   那为什么不⽤函数来完成这个任务?原因有2点:

  1. ⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多,因为函数还要开辟自己的栈帧,进行返回等等操作,所以宏⽐函数在程序的规模和速度⽅⾯更胜⼀筹
  2. 更为重要的是函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使⽤,反之这个宏怎可以适⽤于整形、⻓整型、浮点型等可以⽤于>来比较的类型,宏的参数是类型⽆关的,比如上面我们定义的MAX宏,不仅可以比较整型,同时也可以比较浮点型和长整型等等,而一个函数只能比较单个数据类型

2.函数的优势

   对于宏来说,函数也有它的优势,它们没有一定的哪一个好,只有哪一个更适合我们的需求,那么对比宏,函数的优势如下:

  1. 每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中,除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度
  2. 宏是没法调试的,而函数可以一步一步调试,查看bug出现的原因
  3. 宏由于类型⽆关,也就不够严谨,这在上面成为了它的优势,但是在某些场景导致它的不够严谨,这个时候就要使用函数
  4. 宏可能会带来运算符优先级的问题,导致程序容易出错,比如忘记对参数加上(),或者忘了给整个式子加上()都可能出现预期以外的结果

3.宏和函数的命名约定

   ⼀般来讲函数的宏的使⽤语法很相似,并且语⾔本⾝没法帮我们区分⼆者,所以我们平时就通过命名来简单区分它们,接下来我们来看看它们的命名约定:

  1. 宏名全部大写
  2. 函数名不要全部大写,一般是多个单词中,每个单词的首字母大写

   今天的C语言知识分享就到这里啦,也是我们的最终篇(上),下一篇我们会讲到条件编译,又是一个硬知识,最后希望大家能在我的博客能够学习到知识,如果有疑问欢迎提出来
   bye~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/457811.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于springboot+vue的高校就业管理系统,

基于springbootvue的高校就业管理系统, 分为管理员&#xff1a;测试账号:10086/123 学生&#xff1a;测试账号:10087/123 包含个人信息、查看企业岗位信息、简历信息管理、我的应聘企业&#xff1a;测试账号:10070/123 包含企业信息、岗位企业信息管理、查看学生简历信息…

颠覆级AI:10秒生成超清视频

颠覆级AI&#xff1a;10秒生成超清视频 Pyramid-Flow 是一款开源 AI 视频生成神器&#x1f4bb;&#xff0c;只需文字或图片即可极速生成高清视频&#x1f3a5;&#xff01;高效、高清、资源需求低&#xff0c;适合创作广告、教学视频等多种用途&#x1f680;&#xff0c;快来…

VIVO售后真好:屏幕绿线,4年免费换屏

只要亮屏就有。这也太影响使用了。 本来想换趁机换手机&#xff0c;看了VIVO发布的X200&#xff0c;决定等明年的X200 ULTRA。手头这个就准备修。 查了一下价格&#xff0c;换屏1600&#xff0c;优惠1100。咸鱼上X70 PRO也就800。能不能简单维修就解决呢&#xff1f;于是联系…

4款免费恢复工具,一键拯救你的重要资料

不管是学习的资料、工作的文件&#xff0c;还是重要的照片和视频&#xff0c;要是丢了或者不小心删了&#xff0c;我们肯定急得像热锅上的蚂蚁。不过好在科技发达了&#xff0c;出现了一些能找回数据的神奇工具。今天&#xff0c;我就带你去看看四款免费数据恢复的工具&#xf…

【无人机设计与控制】改进人工势场法,引入模糊控制实现无人机路径规划和避障

摘要 本文提出了一种基于改进人工势场法并结合模糊控制的无人机路径规划和避障方法。传统的人工势场法在处理障碍物时易出现局部极小值问题&#xff0c;且对动态障碍物的应对能力有限。为了解决这些问题&#xff0c;我们引入了模糊控制来调整势场参数&#xff0c;从而使无人机…

Mybatis中的参数占位符:${...} 、#{...}的区别

Mybatis中的参数占位符&#xff1a;${...} 、#{...}的区别 在Mybatis中提供的参数占位符有两种&#xff1a;${…} 、#{…} #{…} 执行SQL时&#xff0c;会将#{…}替换为?&#xff0c;生成预编译SQL&#xff0c;会自动设置参数值使用时机&#xff1a;参数传递&#xff0c;都使…

Java面试题——微服务篇

1.微服务的拆分原则/怎么样才算一个有效拆分 单一职责原则&#xff1a;每个微服务应该具有单一的责任。这意味着每个服务只关注于完成一项功能&#xff0c;并且该功能应该是独立且完整的。最小化通信&#xff1a;尽量减少服务之间的通信&#xff0c;服务间通信越少&#xff0c…

C++11实践指北

C11&#xff1a;书、在线工具、库。 书 1. 《现代C语言核心特性解析》 覆盖 C11~C20 特性的讲解。 视频跟读&#xff1a;https://www.bilibili.com/video/BV1nN4y1j7fv 现代CPP随笔_0CCh - 每天5分钟了解现代C新特性 2. 《C Primer》第五版 基于 C11 的 C 入门书。 正在看…

Python实现贝叶斯优化器(Bayes_opt)优化简单循环神经网络分类模型(SimpleRNN分类算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后关注获取。 1.项目背景 贝叶斯优化器 (BayesianOptimization) 是一种黑盒子优化器&#xff0c;用来寻找最优参数。 贝叶斯…

时间序列预测(九)——门控循环单元网络(GRU)

目录 一、GRU结构 二、GRU核心思想 1、更新门&#xff08;Update Gate&#xff09;&#xff1a;决定了当前时刻隐藏状态中旧状态和新候选状态的混合比例。 2、重置门&#xff08;Reset Gate&#xff09;&#xff1a;用于控制前一时刻隐藏状态对当前候选隐藏状态的影响程度。…

质量漫谈一

我知道很多同学看到这类问题&#xff0c;第一反应想要去寻找的就是作为测试角色&#xff0c;应该要如何如何去做&#xff1f;但是今天这里作为质量第一篇&#xff0c;不打算按照这样单角度去写&#xff0c;这类同学可以就此打住&#xff0c;如果在意的话&#xff0c;可关注后续…

python源码编译—Cython隐藏源码(windows)

文章目录 1、前言2、依赖3、操作示例 1、前言 很多时候&#xff0c;我们想提供我们的程序给别人使用&#xff0c;但又不想让别人看到我们的源代码&#xff0c;这样我们就需要对python代码进行编译&#xff0c;然后打包发送给别人使用。 2、依赖 安装Visual Studio Installer。…

uniapp移动端优惠券! 附源码!!!!

本文为常见的移动端uniapp优惠券&#xff0c;共有6种优惠券样式&#xff08;参考了常见的优惠券&#xff09;&#xff0c;文本内容仅为示例&#xff0c;您可在此基础上调整为你想要的文本 预览效果 通过模拟数据&#xff0c;实现点击使用优惠券让其变为灰色的效果&#xff08;模…

手机柔性屏全贴合视觉应用

在高科技日新月异的今天&#xff0c;手机柔性显示屏作为智能手机市场的新宠&#xff0c;以其独特的可弯曲、轻薄及高耐用性特性引领着行业潮流。然而&#xff0c;在利用贴合机加工这些先进显示屏的过程中&#xff0c;仍面临着诸多技术挑战。其中&#xff0c;高精度对位、应力控…

8. 数据结构—排序

目录 一、插入排序 1&#xff09; 直接插入排序 优化&#xff1a; 折半插入排序 2&#xff09;希尔排序 二、 交换排序 1&#xff09;冒泡排序 2&#xff09;快速排序——递归实现 三、选择排序 1&#xff09;简单选择排序 2&#xff09;堆排序 四、归并排序 五. 各…

论文笔记(五十一)Challenges for Monocular 6-D Object Pose Estimation in Robotics

Challenges for Monocular 6-D Object Pose Estimation in Robotics 文章概括摘要I. 介绍II. 正在进行的研究和常见数据集A. 数据集B. 正在进行的研究问题 III. 未来挑战A. 物体本体B. 可变形和关节物体C. 场景级一致性D. 基准现实性E. 环境影响F. 通用物体操控 IV. 结论 Estim…

Telephony中ITelephony的AIDL调用关系

以Android14.0源码讲解 ITelephony来自framework下的com.android.internal.telephony包下 frameworks/base/telephony/java/com/android/internal/telephony/ITelephony.aidl这个接口用于与Phone交互的界面&#xff0c;主要由TelephonyManager类使用&#xff0c;一些地方仍在…

多元线性回归【正规方程/sklearn】

多元线性回归【正规方程/sklearn】 1. 基本概念1.1 线性回归1.2 一元简单线性回归1.3 最优解1.4 多元线性回归 2. 正规方程求最优解2.1 线性回归的损失函数&#xff08;最小二乘法&#xff09;2.2 推导正规方程2.3 正规方程练习2.4 使用sklearn计算多元线性方程2.5 凸函数 3. 线…

InternVL-1.1: Enhance Chinese and OCR Capabilities

Blog:https://internvl.github.io/blog/2024-01-24-InternVL-1.1/ 指南:https://internvl.readthedocs.io/en/latest/internvl1.1/introduction.html InternVL-Chat-V1-1 结构类似于 LLaVA,包括一个 ViT、一个 MLP 投影器和一个 LLM。如上图所示,我们通过一个简单的 MLP …

JAVA篇之类和对象

目录 一. 面向对象 1.1 面向对象和面向过程 二. 类的定义和使用 2.1 什么是类 2.2 类的定义格式 三. 类的实例化 四. this引用 4.1 this引用的作用 五. 构造方法 5.1 构造方法重载 5.2 通过this调用其他构造方法 5.3 默认初始化 结语 一. 面向对象 Java 是一门面向对…