c语言预处理(万字解析)

news2024/11/18 10:48:23

预处理

  • 一.总体概述
    • 1.注释去除
    • 2.宏替换
  • 二.宏定义(宏替换类型)
    • 1.数值宏常量
    • 2.字符串宏常量
    • 3.用宏定义注释符号
    • 4.用宏定义表达式(难点)
      • 1.第一种情况
      • 2.第二种情况
    • 5.#undef(宏的有效范围)
      • 1.两个问题
      • 2.#undef的使用
      • 3.一段代码的理解
  • 三.条件编译
    • 1.#ifdef和#ifndef的用法
    • 2.#if的用法
    • 3.如何用#if来取代#ifdef
    • 4 .裁剪的意义
    • 5.深入理解奇怪的情况
  • 四.头文件展开
    • 1.一种现象
    • 2.什么叫做头文件展开
  • 四.一些好玩的预处理符号
    • 1.预处理符号
    • 2.#运算符
    • 3.##预算符

在这里插入图片描述

一.总体概述

预处理本质是将我们的代码进行预先处理。主要分为四个步骤:1.去注释; 2.宏替换; 3.条件编译; 4.头文件展开(以下主要说明去注释和宏替换部分,条件编译在第三点,文件展开在第四点)

1.注释去除

去掉注释的本质其实是将我们注释的内容全部变为了空格,在我们的gcc下可以很明确的看到(因为VS是不能看到预处理阶段的)

在这里插入图片描述

这里就不再多说啦,如果有其他想法可以在gcc里自己看一看哟

2.宏替换

举个例子
在这里插入图片描述

可以看到预处理过后我们所定义的M已经不见了,相反在打印阶段里的M被替换成了10,这就是所谓的宏替换

二.宏定义(宏替换类型)

1.数值宏常量

在这里插入图片描述

这个概念很简单,无非就是把一些数用一个常量接收罢了。那我们为什么要这样“麻烦”一下呢?

原因当然是为了方便我们“偷懒啦”,首先3.141592这么长的数字写起来当然没有PI这两个字符方便;其次,有可能在你的程序里会出现多个3.141592,而当某一天你想修改这个值时需要把它们挨个挨个的修改,很麻烦(也叫做可维护性差),如果用宏的话,直接修改一个值就可以了。这也是在大型文件里所必须做的事

2.字符串宏常量

如果我用定义数字宏常量的方法去定义一个字符串宏常量,行不行呢?

在这里插入图片描述

很显然是不行的,知识因为字符串本身是必须带上双引号的(如果不太了解可以看看这篇博客双引号和单引号),所以在定义宏时也必须带上双引号

在这里插入图片描述

这里如果字符串太长可以使用 \ 进行续航(如果对这个符号不太熟悉的话可以看看这篇博客反斜杠)

在这里插入图片描述

3.用宏定义注释符号

上文我们说到,预处理会进行去注释和宏替换,那么这就有个好玩的东西,如果我们用宏定义注释符号,那么它是会先被去除还是先被替换呢?

我们使用gcc来观察一下(因为VS不能够观察到预处理)

在这里插入图片描述

如果我们的宏替换先于去注释,那么BSC就会被替换为双斜杠并且hello bit也就不能看到。反之就都能看到

在这里插入图片描述

都能打印出来,看起来是我们的第二种情况,那么我们接下来看看深入看看它的预处理情况

在这里插入图片描述

从这里,我们更直观的看出,编译器确实是先将//看为了注释,然后直接去掉,那么整个宏就变为了#define BSC,这时右边没有替换值也就是空,所以它在打印时实际并没有起作用

结论:预处理阶段先去注释,后进行宏替换

上面我们用的是c++风格的注释,那么我们换成c风格的注释,结果会不会不同呢?

在这里插入图片描述

预处理之后

在这里插入图片描述

这里我们发现了根c++风格注释不同的 点,它预处理后还剩下一个EMC,这为什么没有宏替换完全呢?这是因为#define EMC被注释掉啦,BMC变成了空所以没有显示,但EMC编译器并不认为它是一个空的宏定义,而是一个未进行声明和初始化的变量。当然这个程序是不能编译的

在这里插入图片描述

结论:无论是哪种风格的注释,都是先去注释再进行宏替换

4.用宏定义表达式(难点)

1.第一种情况

在这里插入图片描述

这里可以看出这里的宏跟我们之前写的宏不一样,之前的宏都是一个数直接替换很简单。而这种宏是带参的,而参数在预处理时是等价的,这里的10就等价于x。并且后面的表达式会替换前面的x。

在这里插入图片描述

第一个式子确实如我们所想的那样,10替换了x,x+x替换了前面的x,最终就输出10+10的结果。但第二个为什么没有被替换呢?这是因为在c语言中,双引号括起来的是严格意义上的字符串,故编译器直接将双引号里面的内容认为是字符啦,所以并未发生替换(如果不太理解双引号可以看看这篇博客 双引号 )

我们也可以来看看它预处理后的结果

在这里插入图片描述

以上是不是就很直观了呢

2.第二种情况

在这里插入图片描述

这里我对a和b两个变量进行初始化,初始化很简单在后面直接加就可以了

在这里插入图片描述

这里为了美观,我使用了反斜杠进行续航(如果不太了解这个作用,可以看看这篇博客反斜杠)

在这里插入图片描述

之后我想修改x和y的值,将它们变成0。根据我们上文的经验,#define(a,b)中的a和b首先应该被替换为x和y,然后后面的表达式替换前面的,也就是0会替换x和y。理论上应该是这样的,那究竟行不行呢?

在这里插入图片描述

根据我们的结果,x和y确实都被改为0了,接下来我们继续看看它的预处理结果

在这里插入图片描述

根据预处理结果看出,它确实进行了宏替换,并且修改了x和y的值。这也符合我们的预期,接下来深入理解一下这样的代码会出现什么问题

有什么问题

在这里插入图片描述
在这里插入图片描述

我们来看看它的预处理后的结果

在这里插入图片描述

我们可以看到,首先x和y确实是被替换了(是被完全替换连分号也会被替换)。我们知道if如果不加花括号的话只能处理一条语句,也就是一个分号。而else又需要紧跟if,但是我们的if中间多出来了一个y=0;;这样的语句。相当于是if(flag){x=0;} y=0;;else{x=100,y=100;}。这样写不符合我们的语法规定,自然就报错了

结论:用宏来充当多条语句的时候,在一些较为复杂的场景中可能并不能达到我们想要的结果

怎样修改

我们是不是带上花括号就可以了呢

在这里插入图片描述

以下是预处理后的结果

在这里插入图片描述

上述的带话括号确实是一种解决方案,但是不够好。因为这是在给程序员提要求,要他遵守好的代码规范,但如果这个程序员不遵守呢?所以这种方案是不具备普适性的

接下来我们进行一个尝试,不是缺花括号吗,我们直接在宏定义时就加上花括号行不行呢?

在这里插入图片描述

这样做其实存在两个问题,一是程序员可能自己会写花括号,导致花括号重复。二是程序员自己在写完一条语句后会带上分号,导致花括号后带分号。这样写是不行的

以下是最终解决方案,为了方便看我们先将续航符去掉

在这里插入图片描述

这是预处理后的结果

在这里插入图片描述

这其实就在我们上一个方案中做了改进,修改了上面存在的两个问题。1.do…while是一条语句,所以在外面带不带上花括号都没有影响,这解决了花括号重复问题。2.多余的分号会自动到while(0)后面,这也解决了分号问题

同理,这里再加上我们的续航符,也没有任何影响,并且如果你的宏里有多条语句,也建议如下写

在这里插入图片描述

为什么do里可以容纳多条语句呢?因为它带有花括号。为什么while(0)呢?因为我们并不需要它循环,我们需要的只是这种语法结构。

这种结构被称为do—while—zero结构

5.#undef(宏的有效范围)

1.两个问题

#undef是用来撤销宏定义的,具体是如何做到的呢?

在说明这个问题前先来讨论两个问题。1.宏只能在main上面定义吗? 2.在一个源文件里,宏的有效范围是多少?

下面是第一个问题的探索

在这里插入图片描述

上面的宏是在main函数内定义的并且我们发现它是可以正常使用的

接下来我们进行更多的尝试,如果定义在其他函数里,另一个函数能不能调用它呢

在这里插入图片描述

得出结论:宏可以在任何地方定义,与在函数体内还是函数体外都没有任何关系

下面是第二个问题的探索

我们像下面的方式书写能编过吗?

在这里插入图片描述

答案是不行的,为什么呢?接下来为了更直观的观看,我使用gcc来演示

下面是gcc预处理后的结果

在这里插入图片描述

可以很明显的看到,在宏定义上面的M并没有被替换掉,而在下面的M则被替换了。

得出结论:宏从定义处向下都是有效的,与函数调用无关(因为宏替换在函数调用之前),只是简单的文本替换

2.#undef的使用

上文说到#undef就是用来取消宏定义的,那是如何取消的呢?直接看预处理结果

在这里插入图片描述

我们可以看到在#undef的上面部分,M是被替换掉了而下面则没有。

结论:#undef又可以称为限制宏,在宏定义的下面,#undef的上面才是宏的有效范围

3.一段代码的理解

在这里插入图片描述

以上这段代码最终打印的结果是什么呢?

在这里插入图片描述

废话不多说,直接转到预处理结果

在这里插入图片描述

这是因为第一个宏#define x 3的有效范围只有#define Y x*2这一行,而当代码向下走到Int z=Y时,#define x 3早已失效,故当Y进行宏替换时所看到的宏其实是#define x 2,所以就不难理解最后的答案是4啦

三.条件编译

必须明确的是,1.条件编译是预处理的一个步骤。2.条件编译更多的是为了进行代码裁剪。

1.#ifdef和#ifndef的用法

通常用于检测一个宏是否被定义(主要与宏为真为假区分开来)。这两个通常与#else,#endif一起用(看起来跟if,else类似)

#ifdef是表肯定。如果宏被定义,则该代码保留
#ifndef表示否定。如果没有被定义,则该代码保留

举个例子
在这里插入图片描述

补充一下#endif是结束标志,表示该条件结束。我们可以看到printf这条语句被裁掉了。这是因为我们的宏DEBUG并没有被定义。如果像保留的话需要定义一下

在这里插入图片描述

#define就是定义,这里定义成多少不重要(我这里就没有初识化),重要的是只要定义了,该代码就能被保留

在这里插入图片描述

接下来加上一起用

在这里插入图片描述

这里的意思是,如果定义了DEBUG,就打印出hello debug;否则就打印出hello unkown(注意不能加#elif,这个是用于判断真假的)

具体的结果就不再演示了,大家有兴趣的话可以自己打印一下(#ifndef的用法与#ifndef相同)

2.#if的用法

#if是用来判断我们的宏的真假(与#ifdef区分开来),如果为真就保留该代码,否则就裁剪(这里的用法其实与if从句类似)

在这里插入图片描述

这里我没有定义c,那么c就被默认认为是假了

在这里插入图片描述

接下来我们来定义一下,如果定义为0

在这里插入图片描述

在这里插入图片描述

如果定义为1

在这里插入图片描述

在这里插入图片描述

如果我们只定义不初始化呢?

在这里插入图片描述

在这里插入图片描述

这里就直接报错了,这是因为c被替换后什么也没有,所以编译器会报错说#if后没有表达式

多条件判断

除此之外我们也可以加上#elif,用于多条件判断,具体用法就跟if,else if,类似。

在这里插入图片描述

在这里插入图片描述

上述的所有代码均可在VS里实现并且没有差别。

3.如何用#if来取代#ifdef

其实只需要在#if后面加上defined(),括号里就是要判断的元素。
在这里插入图片描述
在这里插入图片描述

因为我们的VERSION并未被定义,所以输出的就是hello other。其实仔细观察,#ifdef就是#if defined()的缩写。如果想模拟实现#ifndef呢?

在这里插入图片描述

直接带个感叹号,取反就可以啦。

结论
1.#ifdef等价于#if defined()
2.#ifndef等价于#if !defined()
3.不管是哪一种写法必须以#endif 结尾

4 .裁剪的意义

对于程序员来说,我们如果不需要某行代码,直接删除或者注释掉就可以了,为什么需要这些语句呢?

本质上就是通过代码裁剪,快速实现某种目的(版本维护,功能裁剪,跨平台性)

举个例子

现在很多软件都分为免费版和收费版。毫无疑问,收费版的功能更多,开发商做这两个版本的时候难道用的是两份不同的代码吗?当然不是,这样的话维护成本太高了,如果一个版本出现了问题我们不仅需要改这个bug还需要相应修改另一个版本的bug。所以其实他们使用的就是代码裁剪,如果不需要哪个功能,直接剪掉就可以了。并且这样只需要维护一份代码,成本较低。

5.深入理解奇怪的情况

第一种:同时检测两个定义

在这里插入图片描述

在这里插入图片描述

与平常我们所使用的语句相同,直接用&&就可以啦。同时这里推荐最外面加上圆括号,这样会更加规范

同理既然可以判断“和”,那也可以判断“或”

在这里插入图片描述

在这里插入图片描述

嵌套情况

在这里插入图片描述

在这里插入图片描述

这个与我们的if从句里的嵌套类似,我们可以类似的看为下面的代码

在这里插入图片描述

当然上面的代码并不够准确,只是为了方便我们理解。

多条件检测宏定义

在这里插入图片描述

在这里插入图片描述

这种情况也是符号if …else…语句的顺序的。也就是如果#if条件成立就不会判断#elif里的内容

四.头文件展开

在gcc里创建了两个文件,一个是test.h,用于包含所有头文件;一个是test.c,用于我们代码的实现

1.一种现象

在这里插入图片描述

这里的意思是如果没有定义TEST_H_,那么就定义TEST_H_。为什么我们经常在头文件项目里看到这样写呢?

这是为了防止头文件被重复包含。那么是如何做到的呢?

第一次包含时,我们的TEST_H_没有被定义,那么它下面的就会被保留。

在这里插入图片描述

当我们第二次,第三次想要包含该文件时,由于_TEST_H_已经被定义,那么它之下的就不会被保留,从而做到避免被重复包含。

2.什么叫做头文件展开

在这里插入图片描述
在这里插入图片描述

其实每次在进行预处理结果查看时会出现很多我并没有写并且不认识的函数(上文我们所看的预处理结果都在最下面),我们直接只写了六行代码,但展开后却有八百多行代码。这是为什么呢?

我们仔细观察,其实它的第11行就是我们所写的头文件stdio.h

在这里插入图片描述

它之后的代码可以简单理解成收stdio.h自己所包含的内容。

结论:头文件展开就是把头文件内容拷贝到目标源文件(当然这种拷贝是进行过优化的)

一个小问题:重复包含一定是错误的吗?

并不是的。甚至可以不算一种错误。因为头文件里的很多内容并不是定义,而是声明,声明是可以重复进行的。但也可能引起一些定义类的错误,但特别特别少。重复包含主要会引起重复拷贝,影响运行效率

四.一些好玩的预处理符号

1.预处理符号

以下内容简单了解一下,所以我还是回到熟悉的VS
在这里插入图片描述

#error

作用是只要遇到#error,就会生成一个编译错误提示信息并停止编译

在这里插入图片描述

这就相当于你自定义了一个错误。

#line

作用是改变当前行数和文件名称

在这里插入图片描述

可以看到前面打印的就是我的文件名,后面打印的就是行号。那么#line的作用是什么呢?

在这里插入图片描述

可以看到的是,就是强制改变了我的文件名和行数

#pragma

作用是用来对代码中特定的符号进行是否存在编译时消息提醒
在这里插入图片描述

主要用途就是来检测某些宏是否存在。补充一点#pragma message是让编译器编译时在输出窗口输出相应的信息

2.#运算符

在这里插入图片描述

我们可以看到这三种打印都能通过。在c语言中一对双引号看着一个字符串,但如果两对双引号连在一起编译器也会认为这是一个字符串。

这种特性被叫做:相邻字符串具有连接特性

一个例子

在这里插入图片描述

结论是:在宏中直接使用单井号是将参数符号s对应的文本内容转义成字符串

其实就是拿3.1415926这个值来充当s,那么s就被替换为#3.1415926。而在c语言中,碰到单井号就会被解释为字符串。也就是3.1415926不再是数字了,而是一个字符串

为了更直观,再次使用gcc

在这里插入图片描述

之后再根据字符串的连接性,打印出来就是一个字符串啦

一个应用

我想要把1234这一串数字转换为字符串,以前的话我们是需要写算法的,但现在只需要使用#就可以了

在这里插入图片描述

3.##预算符

和#一样,##也可以运用在宏替换部分。这个运算符将两个语言符号组合成单个语言符号(俗称粘合剂)

在这里插入图片描述

这里的student是一个符号,n是一个符号。而student##n就又是另一种新的符号啦。通过预处理结果我们可以直观的看到student和n被连接到一起了。后面n的值发生改变,所以student依次和1,2,3…连接,输出了我们所看到的结果

需要格外注意的是:这里的student1…之类的既不是整数也不是字符串,它是一个符号于int,char类似

例子

在这里插入图片描述

这里的e2就是科学计数法,如果我们模拟实现这它呢?当然我们可以使用pow函数,但是我们今天也可以使用我们的粘合剂

在这里插入图片描述

由于后面的是粘在一起的,所以我们可以直接看为(base)(e)(n),如果我们令base=3.14,n=2那其实就相当于3.14e2。当然,我们也可以看看它的预处理结果

在这里插入图片描述

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FLStudio2023水果软件哪个版本好用?功能区别对比

FL Studio是一款功能非常强大的音乐创作编辑软件它就是FL Studio(水果软件)。使用FL Studio中文版可以轻松帮我们制作自己的音乐唱片,拥有强大且专业的编曲混音创作工具,有需要的朋友不要错过。 水果,全称Fruity Loop Studio,简称…

量子计算(十六):其他类型体系的量子计算体系

文章目录 其他类型体系的量子计算体系 一、离子阴量子计算 二、原子量子计算 三、核自旋量子计算 四、拓扑量子计算 其他类型体系的量子计算体系 一、离子阴量子计算 离子研量子计算在影响范围方面仅次于超导量子计算。早在2003年,基于离子阴就可以演示两比特…

“转行做程序员”很难?这里有4个重要建议

近几年来,传统行业多处于经济下行,加上互联网行业的赚钱效应,想要转行到这一行的人越来越多,其中程序员这个行业更是很多人梦寐以求的。 但另一方面,我们也发现,这些想要转行的同学们往往会遇到很多困扰。…

推荐今日 火火火火 的开源项目

本期推荐开源项目目录:1. coding-interview-university2. 前端后台管理模板3. 钉钉聊天机器人4. 基于 ChatGPT 的 Neovim 插件5. 开源的分布式社交网络平台6. 分析社交媒体内容7. 用于绘制流程图的库01coding-interview-university这是一个汇集了软件工程师面试中常…

2023跨年代码(烟花+背景音乐)

文章目录前言效果展示使用方法源码学习HTML代码CSS代码前言 时光荏苒,白驹过隙。2022这一年又在忙碌中度过了,过去的一年,我们同努力,我们共欢笑,每一次成功都蕴藏着我们辛勤的劳动。 新的一年即将来到,我…

【linuxgdb】动态链接和静态链接的区别,gdb的基础使用

目录 1.gcc/c动态链接和静态链接的区别 2.gdb的基础使用 2.2使用gdb调试 1.gcc/c动态链接和静态链接的区别 1.1查看可执行文件是动态还是静态的 格式:file文件名 1.2动态链接和静态链接 静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的…

ARM S5PV210 串行通信接口详解

一、串行通信接口详解 1 0. 串口的名称 S5PV210 的数据手册中串口控制器在 section8.1串口的官方名称叫:universal asynchronous reciver and transmitter,通用异步收发器 英文缩写是uart,中文简称串口。 1. S5PV210 的串口控制器工作原理…

CSS -- 2D转换各属性讲解(translate,rotate,scale)

文章目录2D转换1 二维坐标系2 2D转换之移动 translate3 2D转换之旋转 rotate4 2D转换中心点 transform-origin5 2D转换之缩放scale6 2D转换综合写法7 2D转换总结2D转换 转换(transform)是CSS3中具有颠覆性的特征之一,可以实现元素的位移、旋转、缩放等效果 转换(t…

[ 常用工具篇 ] 使用 kali 实现网络钓鱼 -- setoolkit 详解实战(一)

🍬 博主介绍 👨‍🎓 博主介绍:大家好,我是 _PowerShell ,很高兴认识大家~ ✨主攻领域:【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 🎉点赞➕评论➕收藏 养成习…

密码学讲座PPT

来自山大密码学讲座的PPT。 一些密码学领域常用名词术语 Diffie-Hellman 密钥交换;Elgamal 加密和签名;DSA 签名;因相应的离散对数问题难解,大素数的原根可用于密钥交换;RSA加密和签名: 因大整数因子分解难算,合数可成…

JetPack 组件总结

文章目录JetPackLifecycle使用Lifecycle解耦页面和组件使用Lifecycle解耦Service与组件使用ProcessLifecycleOwner监听应用程序生命周期ViewModel 与 LiveDataViewModelLiveDataViewModel LiveData 实现Fragment间通信DataBinding 的意义与应用意义使用前的配置import标签事件…

SWPUCTF 2021 新生赛

😋大家好,我是YAy_17,是一枚爱好网安的小白,自学ing。 本人水平有限,欢迎各位大佬指点,一起学习💗,一起进步⭐️。 ⭐️此后如竟没有炬火,我便是唯一的光。⭐️ 目录 g…

Kotlin + Jpa + Querydsl

Kotlin Jpa Querydsl 本篇主要介绍一下 kotlin jpa querydsl . jpa querydsl 是我很喜欢的一种搭配,它能够让你写sql语句一样的去写代码 , 以前我也写过关于 java版本的, 本篇就来看看kotlin中如何去使用 1.引入插件 主要引入jpa插件 和 kapt Kotlin annotation processi…

【剧前爆米花--爪哇岛寻宝】Cloneable接口和深浅拷贝

作者:困了电视剧 专栏:《JavaSE语法与底层详解》 文章分布:这是一篇关于接口的文章,在本篇文章中我会分享Cloneable接口的用法和机制,同时从底层分析深拷贝和浅拷贝的区别。 Cloneable接口 Cloneable这类接口可以说是…

ZigBee硬件平台小记(CC2530)

文章目录1.CC2530射频模块特征2.CC2530射频模块主要技术指标3.调试器输出接口特点输出引脚排列1.CC2530射频模块特征 基于CC2530F256单芯片ZigBee SOC(System On Chip),集成8051内核模块尺寸:36mm x 26mmSMA底座,外接…

代码随想录训练营第56天|LeetCode 647. 回文子串、516.最长回文子序列

参考 代码随想录 题目一:LeetCode 647. 回文子串 如果用暴力求解,两层for循环加一层判断,两个遍历指针i和j构成一个区间,每次判断这个区间内的字符串是否为回文串,这样的求法时间复杂度为O(n^3)。这里使用动态规划可…

Jetpack Compose中的手势操作

点击事件 监听点击事件非常简单,使用 clickable 和 combinedClickable 修饰符即可满足需求: OptIn(ExperimentalFoundationApi::class) Composable fun ClickableExample() {Column{Box(Modifier.clickable { println("clickable") }.size(3…

异常检测原理及其在计算机视觉中的应用

点击上方“小白学视觉”,选择加"星标"或“置顶”重磅干货,第一时间送达这篇文章涵盖了三件事,首先什么是视觉角度的异常检测?用于异常检测的技术有哪些?它在哪里使用?异常检测是什么?…

微服务架构下的配置治理模式

微服务被滥用是不争的事实。被滥用的同时,很少人留意到它所带来的配置治理的问题。本文我们介绍两种常见的治理模式。基于common的配置治理模式当微服务数量多时,开发人员倾向于创建这样的配置文件:common-redis.jsoncommon-mysql.jsoncommon…

〖产品思维训练白宝书 - 产品思维认知篇③〗- 产品思维 VS 技术思维

大家好,我是 哈士奇 ,一位工作了十年的"技术混子", 致力于为开发者赋能的UP主, 目前正在运营着 TFS_CLUB社区。 💬 人生格言:优于别人,并不高贵,真正的高贵应该是优于过去的自己。💬 &#x1f4e…