程序的编译和链接应该是C语言进阶知识点的最后一更(分两篇来写),后面可能会单独更新些关于C语言的题什么的,接来下就要更新数据结构了。
今天的知识比较多,也比较重要。
(上篇)
1.程序的翻译环境(重要):详解:C语言程序的编译+链接
程序的执行环境(不重要)
2.预处理详解:
关于#define 定义标识符和宏
(下篇)
#define定义宏和函数的对比
预处理操作符#和##的介绍
预处理指令 #undef
命令行定义(不重要)
3.条件编译
4.文件包含:预处理指令 #include
一.程序的翻译环境
在ANSI C的任何一种实现中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代码。
先讲翻译环境
组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
我们这里着重讲从源文件到可执行程序之间的四个阶段发生的事情,也就是下图test.c到test.exe之间编译链接的事儿。再提一嘴,拿VS2019举例子,翻译过程是cl.exe完成,链接是link.exe完成,不同的编译器不太一样。
首先在翻译中,分为预处理(预编译)、编译、汇编,链接就只有一个过程,就叫链接。我们需要重点搞懂的,就是各个阶段分别做了什么事儿
声明:使用Linux来调试,使用以下程序
1.预处理:这个阶段会生成test.i和add.i,头文件相关内容包含到test.i中,比如执行printf,需要包含stdio.h,那么这个阶段就会把stdio.h包含的内容拷贝在程序之前,这一步就叫头文件的包含。然后还会把#define定义的宏完成替换,还会删掉注释
2.编译:生成test.s和add.s,这里讲C语言代码翻译成了汇编代码,也就是语法分析,词法分析,语义分析和符号汇总。如下图
3.汇编:生成test.o和add.o文件,windows环境下目标文件是xxx.obj,而Linux中是xxx.o。汇编文件是二进制文件,所以这个过程是把汇编指令翻译成了二进制指令。linux环境下,test.o可执行程序类型是elf,这些elf文件也叫段表。
这个阶段就是生成符号表,用来给链接用。在main可以看到main Add ,在Add看到Add,表格里面这些符号对应的地址,比如test.o中的main 0x01 Add 0x00 而add.o中的Add 0x1008
4.链接:首先是合并段表,也就是elf文件的合并。还有符号表的合并和重定位,上述的汇编过程中生成的符号表合并,将汇编阶段生成的符号表进行合并,将无效的符号地址删除,有效的符号地址保留。最终生成可执行程序。
接下来,就该进入运行环境了。
程序的执行过程:
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序 的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用main函数
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回 地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程 一直保留他们的值。
4. 终止程序。正常终止main函数;也有可能是意外终止。
举一个程序当例子:
main函数被调用,给它分配一块空间,存放局部变量,当main函数调用Add函数时,又要为Add函数开辟一块空间,这两个函数之间会有一些空隙,这个空隙来存放Add函数的形参,这里会把a的值传给x,b的值传给y,若Add函数里有局部变量,则在给它分配的空间上存储,函数工作完成后,在返回时,将会将这块空间销毁,还给操作系统,main函数在返回时,空间也还给操作系统。
二.预处理详解
1.预定义符号,都是内置的,直接看下图代码吧
预定义符号的作用就是记录信息,可以写个文件记录下来,对于大工程的调试、记录还是有用处的。
2.#define
2.1#define可以定义标识符,直接上代码看一些例子,标准格式如下:
2.2#define可以定义宏,#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义 宏(define macro),下面是宏的格式。
也就是这么定义: #define SQUARE(x) (x) * (x)
是100还是55?
其实是55,因为替换之后执行的是这个:
所以需要改为:#define DOUBLE(x) ((x)+(x))
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中 的操作符或邻近操作符之间不可预料的相互作用。
3.#define替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先 被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上 述处理过程。
注意:
1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。