编译链接详解(c语言 )

目录

引入

正题

概况

翻译环境

从.c到.obj——编译

从.obj到.exe——链接

运行环境

回到引入


引入

  • 我们拥有的一些良好的编程习惯,你有想过为什么要这样吗?

  1. 为什么一般把声明写在头文件里,而定义写在源文件里?

  2. 定义可以放头文件里吗?

  • 我们再看这个问题:

    这是头文件“example.h”的内容,它能编译过吗?为什么?

#include
​
int a;
​
struct example
{
  int b;
};

想要知道这些问题的答案,那么就需要了解程序编译、链接的相关知识。

正题

概况

我们平时接触最多的,双击即可打开运行的叫可执行文件(.exe),可执行文件的内容实际上是机器语言,也即二级制指令,是由0、1组成的。

我们写的代码,叫源文件(.c),阅读者是人类,计算机是看不懂源文件的,它只认得0和1。 因此,编译器需要把源文件翻译成机器能看懂的二进制文件。

这个过程首先需要把多个源文件一一编译成目标文件(.obj文件,内容也是01码),然后多个独立的目标文件经过链接,合成一体,生成可执行文件,至此,翻译环境结束。经过编译器和链接器的努力,我们写的程序实现了从给人看的到给机器看的转变,这就是翻译环境要干的事。

之后,可执行文件进入运行环境。

编译链接详解(c语言 )_第1张图片

注:只有机器语言是01码,可以被计算机直接执行。我们所写的c语言,或者后面提到的汇编语言,都不是01码,它们都需要被转化成01码,才能被执行。

翻译环境

从.c到.obj——编译

思考(解答在下面):

  1. 源文件是编译的基本单位,那编译时头文件去哪了呢?

  2. 定义可以放进头文件里吗?

  3. 哪些声明应该放在头文件,哪些声明应该放在源文件?

编译包括三个步骤:预编译 (即预处理) 、编译、汇编。

  1. 预编译

    • 头文件的索引、替换。比如你写了一个#include,那么编译器首先会去系统类库目录下找到stdio.h这个文件,找到以后,把头文件的内容都引进程序里。所以说,引头文件相当于复制了头文件的内容进去。

      引头文件的方式有#include< >和#include" "两种。

      补充两者区别:我们一般用#include< >来引库文件,而用include" "来引自定义的头文件。两者的查找策略不同:前者直接从系统类库目录里查找,找不到就报错;后者先去源文件目录下查找,若找不到,编译器就会像查找库函数头文件一样去标准位置查找头文件。找到以后,预处理器会删除头文件中的这条指令,用包含文件的内容替换。

      Q:#include可以写成#include"stdio.h"吗? answer:可以,但因为会先去源文件目录下做无用功的查找,所以效率要低一些。

    • 宏的查找替换

    • 删除注释。使用空格来替换注释。

  2. 编译:编译期间,通过语法分析、词法分析、语义分析、符号汇总等方式,将c语言代码翻译成汇编代码 。汇编代码并不是01码,它是一种较为底层的语言。编译的过程很复杂,我们先不做赘述。

  3. 汇编:把汇编代码转换01码,并且形成了符号表。

    关于符号表的补充:符号表是一个记录了程序中符号(如变量、函数名)与其对应地址or值的表格。在汇编过程中,符号表能帮助正确地解析和处理符号。运行时,符号表能保证程序正确地访问各个符号。当我们调试程序时,输入符号名称就可以查到它的地址or输入地址就能查到对应的符号名称,这都依赖于符号表。

当有多个源文件时,每个源文件会依次编译形成对应的目标文件,这些目标文件彼此间还没有什么关联。但经过链接之后,它们就会合成统一的整体。

解答:

  1. 头文件相当于被复制粘贴进源文件中,再编译的。所以说,编译的基本单位是源文件。

  2. 不要放在头文件里。当这个头文件被多个源文件包含时,你的定义就相当于被复制粘贴多次,导致重复定义,会报错。(声明可以多次,但定义只能有一次。)

  3. 其实声明的位置没有“应该”一说,因为并没有标准的规定。但一般把声明都放进头文件中,在源文件中定义,这是比较好的一种实现。

从.obj到.exe——链接

既然经过编译的目标文件已经是二进制指令、可以让计算机读懂了,那为什么不能直接运行呢?

这是因为当前编译单元调用到的函数,它的实现可能不在当前编译单元中,可能在其他编译单元中。比如,我在Add.c中实现函数Add,那么Add的函数名称以及相应地址将会被保存在Add.c的符号表中。当我在test.c中调用Add时,编译器会根据函数名在test.c的符号表上寻找它,然而,test.c中并没有函数Add的定义,也就没有它的有效地址,所以是调用不了Add的。只有将各个目标文件的符号表合并,才能打通各个目标文件之间的调用关系。这就需要链接的过程。

链接:合并段表;进行符号表的合并和重定位。

这个动图演示了符号表的合并和重定位:

编译链接详解(c语言 )_第2张图片

运行环境

这里对运行环境做一个简要的介绍。

  1. 程序必须先载入内存中。

  2. 开始执行,首先调用main函数。此时程序将使用一个运行的堆栈,储存函数的局部变量、返回地址等。程序同时也可使用静态内存,储存在静态内存中的变量在程序的整个执行过程中一直保留它们的值。

  3. 开始执行程序代码。

  4. 终止程序。

回到引入

  • 回看引入部分我们留下的问题,我们发现,其实没有标准规定声明和定义一定要放在哪。

    声明也可以写进源文件,只要保证声明在它的调用之前。定义也可以写进头文件,我们学C++时,常常把内联函数的定义写在头文件里,(这并不冲突,因为C++做出了改进,当调用内联函数时,会把函数体的代码拷过去替换掉原本的指令)。但是,将声明写在头文件,而定义写在源文件里,是一种良好的编程习惯。

  • 头文件“example.h”中,未必能编译过。因为它把变量a的定义放在头文件里了,引用时可能会出现重复包含的错误。

    同时说明一下: ”int a;“是变量a的定义,“int b;”是变量的声明。需注意:不像函数的那么好判断,变量的声明与定义,得看是否开了空间。 int a;开了空间,所以是定义,这很好理解。那么b为什么没开空间呢? 因为这是在结构体里。C语言中的结构体和C++中的类,都是像模型一样的存在,本身并不占空间,只有照着模型创建出了结构体变量,系统才会为结构体类型的变量分配空间。所以在这里,b并没有空间,”int b;“是声明。

你可能感兴趣的:(c语言,c++)