一、今天我们就研究一下什么是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.)所以此时我们用一个例子来说明: