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

在这里插入图片描述

一.总体概述

预处理本质是将我们的代码进行预先处理。主要分为四个步骤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也就不能看到。反之就都能看到

在这里插入图片描述

能看到hello bit看起来是我们的第二种情况那么我们接下来看看深入看看它的预处理情况

在这里插入图片描述

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

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

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

在这里插入图片描述

预处理之后

在这里插入图片描述

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

在这里插入图片描述

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

4.用宏定义表达式难点

1.第一种情况

在这里插入图片描述

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

在这里插入图片描述

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

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

在这里插入图片描述

以上是不是就很直观了呢

2.第二种情况

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

有什么问题

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

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

在这里插入图片描述

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

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

怎样修改

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

在这里插入图片描述

以下是预处理后的结果

在这里插入图片描述

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

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

在这里插入图片描述

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

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

在这里插入图片描述

这是预处理后的结果

在这里插入图片描述

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

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

在这里插入图片描述

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

这种结构被称为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这一行因为遇到#undef就失效了而当代码向下走到Int z=Y时#define x 3早已失效故当Y进行宏替换时所看到的宏其实是#define x 2所以就不难理解最后的答案是4啦

三.条件编译

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

1.#ifdef和#ifndef的用法

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

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

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

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

在这里插入图片描述

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

在这里插入图片描述

接下来加上一起用

在这里插入图片描述

这里的意思是如果定义了DEBUG就打印出hello debug否则就打印出hello unkown注意虽然跟ifelse类似但不能加#elif因为这个是用于判断是否被定义的只有两种可能

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

2.#if的用法

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

在这里插入图片描述

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

在这里插入图片描述

接下来定义一下如果定义为0

在这里插入图片描述

在这里插入图片描述

如果定义为1

在这里插入图片描述

在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

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

多条件判断

除此之外我们也可以加上#elif用于多条件判断具体用法就跟ifelse 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依次和123…连接输出了我们所看到的结果

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

例子

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6