万字【C语言程序的预处理】各种知识点的讲解

发布于:2022-11-16 ⋅ 阅读:(28) ⋅ 点赞:(0) ⋅ 评论:(0)

一、今天我们就研究一下什么是C语言的预处理

1.总的来说就是当我们写了一个代码,当我们想要运行起来的时候,这个代码是如何运行起来的呢?
这个从代码到运行起来的过程就叫C语言的预处理,以下内容就是关于我的预处理的一系列的知识

1.程序的翻译环境

(一、)什么是程序的翻译环境

(1.)首先我们先了解一下一个test.c后缀的文件是如何变成一个test.exe后缀的文件(翻译环境)
此时就会涉及到两个知识点(1.编译 2.链接),这个东西对于目前的我们是比较抽象的,所以我们通过一些图片对比来说明问题

在这里插入图片描述

当我的程序还没有运行的时候,这个就是目前这个代码的存储位置,可以看出此时并没有一个叫test.exe的文件
在这里插入图片描述

但是当我们运行起来(也就是经过了编译和链接过程后)此时我的这个工程就会增加一些文件(这些文件就是跟编译和链接过程有关的文件)
在这里插入图片描述

在这里插入图片描述

此时这个x64文件中就会出现我们的Debug文件(可运行文件)这个Debug文件中就会有我们的test.exe文件(可执行文件,后缀exe就是代表可执行文件),所以此时我就成功的将我的test.c文件经过一系列的处理变成了test.ext文件(并且这个处理过程就叫做编译和链接)

(2.)翻译环境(在这个环境中源代码被转换问可执行的机器指令)(就是从文本文件到二进制文件的一个过程)

我的test.c文件中本来其中放的是我的代码,是一个文本 文件(我看的懂的),但是经过我的编译和链接过程之后就变成了一个二进制文件

2.程序的执行环境(运行环境)

(一、)程序如何运行(内存)

(1.)首先一个程序想要运行,该程序必须载入内存中,并且有两种载入内存中的方法(1.在操作系统中:由操作系统来完成 2.在独立环境中,程序的载入必须由手工操作或者通过可执行代码置入只读内存来完成)

(2.)程序执行载入内存后便开始调用main函数

(3.)开始执行程序代码,这个时候将使用一个运行时堆栈,存储函数的局部变量和返回地址;程序同时也可以使用静态内存,存储在静态内存中的变量会在整个执行过程中一直保留它们的值

(4.)当终止程序时,有两种可能:1.正常终止main函数 2.也可能是异常终止

3.C语言程序的编译和链接(详解)

1.每一个源文件进行编译时都会有一个单独的编译器进行编译,编译完成后生成我的目标文件
2.每一个源文件被生成目标文件之后,此时就会有一个链接器,这个链接器就会帮我们把所有生成的目标文件链接生成我的可执行程序
3.图示如下:
在这里插入图片描述

4.所以我们现在就来讲一讲什么是编译

图示如下:

(一.)预编译
(二.)编译
(三.)汇编
在这里插入图片描述

(一.)预编译

C语言预编译的概念:(就是以下3点)

1.在我们组成一个程序时,这个程序的每一个源文件通过编译过程分别转换成目标代码(object code)

2.此时的每个目标文件都是由连接器捆绑在一起,形成一个单一而完整的可执行程序

3.连接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中

(1.)其实当我们在进行预编译的过程中,此时就会将代码中的 # 所包含的各种头文件给进行编译,会将这个头文件所包含的内容给调用,只有这样才可以实现我的程序,所以不要看我们平时写代码总是直接写 #include<stdio.h>,其实当真正编译起来的时候这个是非常的复杂的,仅仅只是这个头文件就拥有900多行的代码需要被编译(所以我们都是在大树下进行编码)

(2.)并且当我们在进行预编译的过程中,这个时候编译器会自己进行注释的删除(使用空格来替换)

(3.)并且在预编译过程中,我代码中的 #define定义的各种的标识符也会直接被编译器所替换,替换成它定义的值

(4.)此时经过了预处理阶段,此时我的test.c文件就变成了一个test.i 的文件了

(5.)所以所有的在预编译过程中的操作都是文本操作

(二.)编译

(1.)经过编译过程后此时我的**test.i 文件就会变成一个test.s **的文件了,经过这样的一步一步的转换,最终我的test.c文件就会变成一个test.exe(可执行文件)

(2.)按上述所说我的从test.c到test.exe的过程中,其实也是一个从文本文件到二进制文件的过程中
此时从C语言代码翻译成汇编代码的过程就是在编译这一阶段完成的

(3.)并且在编译中从C语言代码翻译成汇编代码这个过程中,还有语法分析、词法分析、语义分析、符号汇总等操作

(4.)语法分析就是对我的代码进行判断,看一下我的语法是否与C语言的标准语法不同,不同则报错,符合语法则正确

(5.)词法分析就涉及到编译原理,就是讲如何对我的代码进行编译(就是讲如何实现一个编译器)

有了这个编译器,我就可以如上述所说对我的源文件进行编译了,所以想要实现这个编译器就非常的复杂了(这边也不做扩展,我也不会),编译原理(已经是属于另一种知识了,所以我也不晓得)

(6.)语义分析与语法分析类似,就是要进行翻译,我就一定要进行代码的语义分析,看这个语义是否符合我的意思

(7.)符号汇总就是将那些全局变量和函数名这些符号进行汇总在一起(这样可以便与编译时将从C语言代码翻译成汇编代码

(8.)各位如果感兴趣就去研究一下编译原理(我也想研究,但是目前没空)

(三.)汇编

(1.)首先汇编可以把我的test.s文件转换成一个 test.o 文件(test.obj)

(2.)所以此时我们去看一下test.o,此时这个文件中放的就是一推的二进制的乱码而已,所以此时的汇编过程,我就把我的C语言代码翻译成了汇编代码(二进制代码)

(3.)并且在汇编过程中,这边会涉及到一个关于符号表的知识点,不管一个程序中有几个源文件,当我们在执行这个文件的过程时,我们总是会将这些源文件中的所有符号给进行一个汇总,包括(全局变量,函数名,不包括局部变量),此时有了这些符号,我就会形成一个符号表,这个符号表中不仅放了我相应的符号,还放了这些符号所对应的地址(所以符号表就是由符号名和地址一起构成的

(4.)所以在我们讲所有的文件都给预编译、编译、汇编完成之后,这下文件就会经过我的链接器进行链接,这样将所有的我需要的文件链接在一起,此时我就构成了我的可执行程序(所以这个可执行程序是来之不易的,不要只以为是一个黑屏的输出,其实整个编译过程是非常的复杂的),我们都是在大树下的来写的代码

5.所以我们现在就来讲一讲什么是链接

(1.)链接主要包括了,合并段表、符号表的合并和符号表的重定位

1.合并段表:

主要就是对我的目标文件(test.o文件)进行合并,因为我的目标文件都是有格式的,所以想要将这些有格式的目标文件进行合并(一个目标文件可以划分为几个段),所以在链接过程中合并段表的意思就是将两个目标文件链接在一起,然后将对应的段上的数据合并在一起(合并时是有规则的),如果觉得抽象不怕,具体看图:

这个就是合并段表的相应图示:
在这里插入图片描述

2.符号表的合并和重定位:

例:在编译过程后,我的每一个源文件此时都会自己形成一个符号表(所以此时就要对这些符号表进行合并),但是在合并过程可能会有符号的名字的相同,所以此时就使用哪个有效的名字的地址来进行合并,如果不将这些符号表进行合并那么在链接期间,就无法准确的找到我相应的符号,此时就无法正常的进行链接,有了这些表格,我就可以很好的进行链接

6.预定义符号的介绍

在这里插入图片描述
(1.)如上图所示,可以得到预定义符号就是__FILE__、 __ LINE __ 、__ DATE __、 __TIME __
这些就是预定义符号,这些预定义符号是有不同的意思的(具体意思如图片上注释所示)

(2.)并且下面的所示代码的意思就是打开一个文件,然后在这个文件中打印这些预定义符号所代表的意思,这样就可以知道我这个程序的执行日期和具体的时间和文件名和行号,所以此时在这个(test.txt文件中的数据就如下图所示:)

在这里插入图片描述

二、这边我们认识一下什么是预编译(各个知识点的详解)

7.关于#define的预处理指令

(1.)预编译指令包括

#define
#include
#pragma pack(4) 设置默认对齐数的预处理指令
#if
#endif
#ifdef
#line//这些都是预处理指令

总结:由 # 定义的就是预处理指令

(2.)#define定义的预编译指令(定义标识符 、定义宏)

1 .#defien定义的标识符

(1.)#define不仅可以定义整形,而且还可以定义字符等,反正任何类型的数据#define都可以定义
(2.)由图可知:
在这里插入图片描述
(3.)此时的#define不仅可以定义整形100,还可以定义字符"haha",还可以定义寄存器的变量,定义函数,定义for循环,反正就是什么都可以定义,但是有一个小知识点,此时的这个#define定义的数据后面不能加上分号,如果加了就有可能出问题的
例1:int a = MAX;#define MAX 100 加了分号 #define MAX 100 ;
此时在预编译的替换过程中,此时的 int a = MAX;;,所以此时打印就出问题
例2:当此时在一个判断条件中 #define MAX 100;if(nume) max=MAX; else max=0;此时我的MAX后面有一个分号,就会导致max=MAX;;所以此时if语句中就有两个语句(但是没加括号),所以此时也会报错

2.#define定义宏

(1.)概念:#define的使用有一个规定,就是允许把参数替换到文本中,这种实现通常称为宏,或定义宏
(2.)宏的申明方式:#define name( parament-list) stuff 其中的parament是一个由逗号隔开的符号表(就是参数列表(里面有我需要的各种参数)),它们可以出现在stuff(内容)中,总的来说就是把这个宏的名字和参数放在了我的代码中,最后在预编译的过程中,它就会自己替换为我stuff(内容)中的相应的表达式,同时参数也会自己代换到我的表达式中,但是这边有一个注意点

就是name后面的那个()必须和name相连在一起,中间不能有隔开,不然就会导致我括号中的内容(参数)和stuff(参数内容)连接在一起,这样就会导致无参数,只有内容的情况

(3.)我们这边就用一个图片来看一下什么是宏的定义
在这里插入图片描述
此时在的这个定义宏的过程中,我们可以明显看出(就是上述的那个意思:就是把我的定义的宏的名字给替换成了定义宏时的stuff(内容),并且将参数给代换到了这个内容之中),这样就实现了一个宏的定义和使用了

(4.)但是当我们在定义宏的时候,我们一定要注意(宏不是函数),它不是进行传参,它是进行替换,所以此时替换就要注意,当我们的参数进行替换的时候,是否是符合我的意思(所以此时就要考虑到是否需要添加上括号,将我的参数进行保护起来,使其替换的时候不会偏离我的意思,保持我想要实现的结果),所以再强调一遍:

宏是替换而不是传参,不要吝啬括号

(5.)所以此时我们用一个例子来说明:
在这里插入图片描述
这个例子就能很好的说明宏是替换不是传参,所以在使用时一定要明确使用的目的

(6.)所以我们在使用宏的时候就一定要记得要加上括号,

这样进行了改进就不会出现问题了,所以一定要注意括号的使用

(7.)这边再进行一个举例:
在这里插入图片描述
此时的这个输出结果是55而不是100,所以这个就是因为替换完参数之后(表达式为 105+5),乘号的优先级高,所以先执行后执行+号,所以这个就不符合我们的意思,所以要加括号来进行改进
在这里插入图片描述
所以只有进行这样的改进,才有可能达到我的期望

(3.)#define 进行替换的规则

上述讲完了#define定义标识符和#define定义宏的概念之后,这边我们来讲一讲什么是#define的替换规则:
1.在调用宏的时候,首先对宏的参数进行检查,查看这个参数是否包含任何由#define定义的符号,如果有,在预编译的过程中,它们首先被替换
意思就是:
在这里插入图片描述
我们应该先看一下宏的参数是否是#define定义的表示符,如果是,首先替换这个参数的值(也就是我们第一步就是应该把图中宏的参数MAX给替换换成100,然后再调用宏)

2.替换文本随后就被插入到程序中原来文件的位置,对于宏,参数名就会被它们所对应的替换

3.最后就是检查,再次对结果文件进行扫描,看是否还包含#define定义的符号,如果有,就重复上述步骤
4.并且当我们在使用宏这个概念的时候有两个注意点
(1.)宏参数和#define定义中可以出现其它#define定义的变量,但是对于宏是没有递归这个概念的

(2.)当预处理器搜索#define定义的符号时,字符串常量是不在搜索范围内的
这个什么意思呢?
在这里插入图片描述
显而易见此时在双引号中的就是我的字符串常量,所以在预编译的过程中,这个字符串常量是不会被替换的,只有在双引号外的那个MAX此时才会被替换

(4.)如何将参数插入到字符串中(宏的方法使用)

1.首先是问题的引出(此时我想要的是我传参时传进去的是什么字符,此时它打印的就是什么字符,可是,此时却不能达到我想像中的景象 )

在这里插入图片描述

上面的输出并不是我想要的,我想要的是:
the a number is 10
the b number is 20

所以为了解决这个问题这边就涉及到了 # 和 ## 的使用

2.预处理操作符 # 和 ## 的介绍
(1.)# 的介绍

例: #X表达的就是X表达的内容的那个字符串(也就是表达的就是替换时的那个替换的数据)
如下图所示:
在这里插入图片描述

这样使用宏的定义,我就可以顺利的使用 # 的使用方法来解决上述的按个问题了

所以 # 真正的作用就是(把宏的参数直接转换为一个字符串,插入到我的代码中)

(2.)## 的介绍

1.此时进行对 ## 的讲解之前,我们先了解一下打印字符串时的小细节,如图:
在这里插入图片描述
显而易见上述的打印效果是相同的,所以此时就可以涉及到 ## 的作用了
在这里插入图片描述
这个就是##的作用(如上图),可以显而易见的知道,我的##的作用就是将##两端的内容连成一个符号,所以此时 a##b##c的意思就是 abc 的意思

2.所以##的作用就是把位于它两边的符号合成一个符号,它允许宏定义中从分离的文本片段创建出一个新的标识符

(5.) 宏和函数的比较

1.所以这边我们讲一下使用宏和使用函数的优点和缺点(对比)

(1.)首先我们这边引入一个新的概念:叫带副作用的参数 (叫参数会起作用,但是会遗留下来一些别的属性)
在这里插入图片描述
有副作用的意思就是此时的b虽然达到了我的目的,但是此时的a也发生了改变,所以这个就是参数a的副作用

( 2.)所以接下来我们再介绍一下什么是宏参数的副作用

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

(3.)所以当我们的宏参数具有副作用了,此时的输出的值就与我们想象中的完全不同了,接下来我们看一下为什么当宏参数具有副作用时,输出的是这些值;

(4.)首先当我的a++,b++开始替换到我的宏当中时(注意是替换不是传参),此时宏中的stuff(内容)就会变成 :

((a++) > (b++) ? (a++) : (b++))
此时当我进行判断的时候因为(后置加加是先使用后加加),所以(a++)>(b++)这个表达式判断完之后,a就变成了11,b就变成了12,并且这个表达式判断完成了,此时因为b是大于a的所以此时就会执行(a++) : (b++)) 中后面这步(b++),然后因为(后置加加是先使用后加加)所以这边是先把b=12返回给了MAX,所以MAX此时就是12,然后b自己在使用完之后实现后置加加,所以b此时就变成了13;

所以以上就是对宏参数副作用的一些简单的题目的讲解

2.这边我们将上述的比较两个值的大小的题目用函数和宏方法再实现一遍,然后区分出它们的区别,从而得出我们的主要目的(比较宏和函数的优缺点)
在这里插入图片描述
如上图所示,我们比较两个数的大小不仅可以用宏的方法也可以使用函数的方法,所以此时我们就可以很好的区别一下这两种方法的区别(文章底部有一个投票哦!参与参与)

(1.)首先第一点可以证明此时这个案例是我的宏的方法好,假如此时我传给函数的参数是别的类型的数据(例:浮点型,字符类型,长整型),但是我的函数却只可以接收 int 类型,所以此时就不能对我的别的类型进行有效的比较了,而此时我的宏的方法并没有限制它的类型,所以此时不管是什么类型的参数(只有可以进行替换)就都可以进行比较,所以就充分体现出了宏的优点和函数的不足;

(2.)第二点证明这个案例宏的使用比函数的使用更好的是(在编译和链接的阶段)(自己如果感兴趣的话自己去调试一下)(通过看汇编代码就可以很好的看出),当我们在使用函数的方法时,如果想要真正的运行起来,在编译过程中,此时是需要大量的调用各种相关程序的汇编代码,才可以真正 的轮到我关键语句(比大小语句)的调用,并且在这句关键语句执行完成之后,并不是直接就结束了,而是还要在后面执行大量的程序的汇编代码,才算是真正的结束,所以在函数的执行过程中,函数的前后是具有大量的汇编代码需要执行(函数的调用和返回的开销)(这样效率就显得很慢了)
,然而当我们使用宏的方法时,就没有这么的麻烦,汇编代码大大降低(因为宏在预处理阶段时,就完成了符号的替换,不仅不需要传参,而且不需要返回参数),所以此时宏在运行的时候就是非常快的(相比于函数的使用),所以这就可以充分的表明出宏的优点和函数的缺点;

(3.)第三点我们讲一下宏相对无函数的缺点

1.每次使用宏的时候,一份宏定义的代码插入到我的程序中时,除非此时这个宏比较短,否则就会大幅度的增加了程序的长度
2.宏是没法调式的
3.宏由于与类型无关,所以此时宏也是不严谨的
4.宏可能会带来运算符优先级的问题,导致程序容易出错

(4.)这边我们为了加深对宏的使用的理解,这边我们再进行两个例子的解释:
1.这边有一个函数不可能完成的操作,就是进行类型的传参,而使用我的宏,就可以有效的解决这个问题在这里插入图片描述
2.为了加深宏的理解,再看一个代码
在这里插入图片描述
3.所以总的来说当我们在使用#define定义宏的时候,此时这个被定义的宏的使用就是将这个宏的名字和替换的参数放在我应该使用的地方,此时在预编译的过程中,这个宏就会自动的被替换为我定义宏时的那个宏中的stuff(内容),此时这个内容也就是我想要的代码,这样就可以使我的程序执行起来

(5.)所以以上就是宏和函数的各种优缺点和宏的使用

(6.)但是在我们以后学到C++的时候我们就会学一个叫 inline(内联函数)这个内联函数就能很好的解决我们的函数和宏的问题,内联函数不仅具有函数的优点,也具有宏的优点,这个就是宏和函数的集合

(7.)并且注意当我们在定义宏的时候(习惯上是用大写字母来定义 宏的名称),参数可用小写,并且在声明写函数的时候,函数名最好是不要全部大写,避免弄混

(6.)命令定义

1. #undef 的使用

如图:在这里插入图片描述
此时我定义了一个MAX为100,然后我可以进行第一步的打印,然后因为我使用 #undef 此时这个定义就被取消掉了,所以在后面第二步我就无法再一次打印这个MAX的值了,因为此时已经取消了定义

2.什么是命令行定义:

这边讲一个新的知识点,什么是命令行定义,在许多的C语言编译器会提供一种能力,就是允许在命令行中定义符号,用于启动编译过程。假如我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,此时这个命令行定义就可以很好的提供作用(比如此时我想打印1到10的数,我就可以把此时的这个命令行定义在小于10 ,如果我想要打印1到100的数,此时我就可以把这个命令行定义在小于100的,所以这个就是命令行的大致使用)

(7.) 条件编译

1.什么是条件编译:在编译一个程序的时候我们如果将一条语句(一组语句)编译或者放弃是很容易的,因为我们有条件编译指令,有了条件编译我就可以使用选择性的编译,如:#ifdef、#if 、#endif

2.如图就可以充分的说明什么是条件编译的使用
在这里插入图片描述
3.此时的这个# ifdef就是条件编译(意思为假如我的DEBUG如果有被定义,此时就会执行# if中的语句,否则就不会执行)

4.所以当我的DEBUG此时被定义了之后,如图:
在这里插入图片描述
5.我# ifdef中的语句就会被执行

6.并且在条件编译中也是有多分支条件编译的,如图:
在这里插入图片描述

7.所以通过这写条件编译(#if #ifdef #endif),我就可以选择我要的语句和我不要的语句(在预处理阶段执行)

8.并且#ifdef DEBUG 和 #if define (DEBUG)这两句的意思是相同的,都是说明如果有定义,而此时如果都没定义的话这两句的意思 #ifndef DEBUG 和 #if !define (DEBUG)就是相同的,都是没有定义的意思

9.所以条件编译讲的就是我的这个代码有没有机会被执行到的意思,符合意思就执行,不符合意思,就跳过(都是在预编译过程中完成的)

10.这边还有一个条件编译的嵌套使用(头文件中使用异常的频繁),例:
在这里插入图片描述
在这里插入图片描述
可以清晰的看出此时我的stdio.h这个头文件中,大量使用到了条件编译的使用和嵌套

(8.) 预处理指令 #include

1.首先就是包括头文件(但是此时有两种头文件的包含)一种是库函数的包含,一种是本地文件的包含,例:#include<stdio.h> 和 #include“add.h”

2.但是此时使用这两种是有区别的,主要区别就在于查找策略的不同
(1.)当我们使用的是库函数头文件时,此时的查找策略就是:直接去头文件的标准路径下去查找,如果找不到就提示编译错误

(2.)当我们使用的是本地文件时,此时的查找策略就是:先在源文件所在的目录下查找,如果未找到该头文件,编译器就会像查找库函数头文件一样在标准位置查找头文件,如果还是找不到,就提示编译错误

3.所以按照以上的说法,此时其实当我在引用库函数头文件的时候(就也可以使用双引号来引用)例:include“stdio.h”,但是如果这样的话,会导致一定的问题(查找效率低的问题)并且这样写,也就不方便我们自己去区分库文件和本地文件了

4.并且这边有一个新的知识点,就是如何防止我的头文件被重复的包含
(1.)因为当我的头文件数量多时,并且此时这些头文件中也都各自包含了<stdio.h>这个头文件,就会导致,我在一个文件中包含这些头文件时,这个文件此时就会包含多个<stdio.h>头文件,这样就会导致出问题了
(2.)所以此时在每一个头文件的前面都会出现一个条件编译的判断代码,来防止这个头文件被同一个文件给重复调用
具体使用如图:
在这里插入图片描述
(3.)上图中的两种方法就可以防止我的一个头文件被重复调用
在这里插入图片描述
(4.)意思就是当我第一次想要调用这个头文件的时候,#ifndef __TEST__ 此时的这个__TEST__确实是没有被定义的,所以判断条件为真,所以此时走下一步#define __TEST__,所以此时这步走完(__TEST__就被定义了),然后走我需要调用的那个头文件中的对应的内容,所以此时当这个文件还想要调用我的这个头文件时,就会导致此时的 __ TEST __已经是被定义过了的,所以此时的第一个判断条件就直接为假,所以此时我的头文件中的内容就不能被调用,这样就非常好的避免了我的一个头文件被同一个文件调用多次的结果

(5.)还有另一种方法就是#pragma once的方法,这个方法也可以很好的避免重复调用的问题

(6.)在C语言中的预处理指令还有很多,所以如果感兴趣的话,就自己再去研究研究,这边我就讲这么多了,谁让我明天又要6点起来晨跑呢?(高级学校,真的会谢)

三、总结:以上就是C语言内容中的最后一个内容,叫我们如何认识C语言程序的编译、链接和执行

所以总的来说就是要知道一个代码跑起来的所以然。