大家是否想过从我们编写的代码到最后在屏幕上打印的结果,这中间都经历了些什么了?
其实,在ANSI C(标准C)的任何一种实现中,都存在两个不同的环境,即翻译环境和执行环境。翻译环境,在这个环境中,代码被转换为可执行的机器指令。执行环境,它用于实际执行代码。我们运行代码时都将经历这两个过程,最后才将运行结果呈现在屏幕上。
针对整个程序环境,首先先给出一个大体的图,这样读者可以先理解大概。
翻译环境
翻译环境由编译和链接组成
接下来我们了解下编译的各个组成部分
编译
编译=预编译+编译+汇编,下面来看看每一步都做了些什么?
预编译
在预编译这个环节,编译器主要会对C代码做这三件事:
1.头文件的包含。 在预编译阶段,编译器会将代码中所包含的头文件都替换成头文件的内容。例如,#include <stdio.h>这句代码会被替换成stdio.h这个头文件中的全部代码。
2.注释的删除。 在预编译阶段,编译器会将代码中所有的注释都删去,这样可以减少代码量,毕竟运行代码的时候编译器也不用看你的注释。
3.#define定义符号的替换。 我们都知道#define定义的标识符也好,定义的宏也罢,都是起到替换的作用,而真正进行替换的时刻便是预编译阶段。
这些都是一些文本操作
编译
编译阶段的主要任务就是将C代码翻译成汇编指令,这个过程主要包括这四个步骤:
1.词法分析。 词法分析的任务是对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。
2.语法分析。 编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,最.后看是否构成一个符合要求的程序,按该语言使用的语法规则分析检查每条语句是否有正确的逻辑结构,程序是最终的一个语法单位。
3.语义分析。 语义分析是编译过程的一个逻辑阶段, 语义分析的任务是对结构上正确的源程序进行上下文有关性质的审查,进行类型审查。语义分析是审查源程序有无语义错误,为代码生成阶段收集类型信息。
4.符文汇总。 在这个环节中,会将每个源文件的全局范围的变量符号进行汇总.
汇编
注:因为test.c文件中提取的符号Add只是Add函数的声明,并不是定义,无法判断Add函数是否真正存在,所以test.c生成符号表时分配给Add符号的地址是一个无意义(非法)的地址。
链接
1.合并段表。 其实,汇编结束后所生成的obj文件内部会被划分为几个段,在链接过程中就会把每个obj文件对应的段通过某种规则合并起来,最后形成可执行程序(.exe为后缀)。
2.符号表的合并和重定位。 在链接期间会将每个源文件的符号表进行合并,若不同源文件的符号表中出现了相同的符号,则取合法的地址为合并后的地址(重定位)。
注:符号表并非无意义。例如,当要调用某一函数时,编译器会在符号表中查找该符号,若有,则调用成功,否则调用失败。
执行环境(运行环境)
1.程序首先载入内存中。 在有操作系统的环境中,该操作一般由操作系统来完成。在独立的环境中,程序的载入可以由手工完成,也可以通过可执行代码置入只读内存来完成。
2.程序的执行开始。 接着便调用main函数。
3.开始执行程序代码。 这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留它们的值。
4.终止程序。 正常终止main函数,也可能是以外终止。
针对第3步,程序运行时使用一个运行时堆栈,下面画了一幅图,这个知识涉及到函数栈帧的创建和销毁。
预编译(预处理)
预定义符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
FUNCTION //进行编译的函数
STDC //如果编译器遵循ANSI C,其值为1,否则未定义
这些预定义符号是已经用#define定义好的,在代码运行后的预处理阶段会被替换为相应的内容。
我们也可以利用这些预处理符号来记录写程序时的各种信息
#define定义标识符
这些被#define定义的标识符都将在预处理阶段被编译器替换成对应的内容。
#define定义宏
#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
注意: 参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部
分。
例如,用宏实现求一个数的平方:
#include <stdio.h>
#define SQUARE(x) x*x//求x的平方
int main()
{
int ret = SQUARE(5);
//相当于int ret = 5*5;
printf(“%d\n”, ret);//结果为25
return 0;
}
下面我们来看看这样插入参数
这时你就会发现,哦,原来在宏的定义是括号是那么的重要,所以以后写宏时不要吝啬括号。
#define定义的规则
1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
#include <stdio.h>
#define MAX 100
#define SQUARE(x) ((x)*(x)*MAX)
int main()
{
int ret = SQUARE(5);
printf(“%d\n”, ret);
return 0;
}
例如,#define定义的宏中含有#define定义的符号MAX,则调用该宏时,首先将MAX替换。
2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
例如,上例中经过该步骤后,代码等价于:
#include <stdio.h>
int main()
{
int ret = ((5)*(5)*100);
printf(“%d\n”, ret);
return 0;
}
3.最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
1.宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
举个反面例子
#define FAC(x) (x)*FAC(x-1)//error
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
#和##的用法
相信通过这幅图,你应该可以理解#和##的用法了
宏参数的副作用
在介绍带副作用的宏参数之前,我们先看看带有副作用是什么意思。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数同学面临毕业设计项目选题时,很多人都会感到无从下手,尤其是对于计算机专业的学生来说,选择一个合适的题目尤为重要。因为毕业设计不仅是我们在大学四年学习的一个总结,更是展示自己能力的重要机会。
因此收集整理了一份《2024年计算机毕业设计项目大全》,初衷也很简单,就是希望能够帮助提高效率,同时减轻大家的负担。
既有Java、Web、PHP、也有C、小程序、Python等项目供你选择,真正体系化!
由于项目比较多,这里只是将部分目录截图出来,每个节点里面都包含素材文档、项目源码、讲解视频
如果你觉得这些内容对你有帮助,可以添加VX:vip1024c (备注项目大全获取)
希望能够帮助提高效率,同时减轻大家的负担。**
[外链图片转存中…(img-gDtvIeWd-1712514294838)]
[外链图片转存中…(img-OW9ltnGF-1712514294839)]
[外链图片转存中…(img-FcAZf1tn-1712514294839)]
既有Java、Web、PHP、也有C、小程序、Python等项目供你选择,真正体系化!
由于项目比较多,这里只是将部分目录截图出来,每个节点里面都包含素材文档、项目源码、讲解视频
如果你觉得这些内容对你有帮助,可以添加VX:vip1024c (备注项目大全获取)
[外链图片转存中…(img-ybviRe9K-1712514294839)]