C语言程序从编写到运行历经的几个阶段

C语言程序从编写到运行历经的几个阶段

一 前言

  在完成 .c 或 .cpp 文件的编写后,我们通常直接 gcc 或 g++ 后接文件名,就可以在当前文件夹下生成 a.out 可执行文件, 之后输入 ./a.out 即可执行该二进制可执行文件。
  但实际上C语言程序从编写到运行,这期间的经历并不是这么简单,那么现在小编就带领大家探索,这期间具体有哪几个步骤?

一 过程简介

C语言程序从编写到运行历经的几个阶段
  从上图可知从C源码到可执行程序,我们会历经三个步骤,分别是:预处理阶段、编译阶段以及最后的链接阶段。但是如果我们分的更细一点,其实我们可以分成四个步骤:
C语言程序从编写到运行历经的几个阶段
显然由图片我们可以知道经历的四个步骤是:预处理、编译、汇编、链接
通常gcc命令后面不加选项的话,就会默认执行预处理、编译、汇编、链接所有步骤,若程序没有错误的话,我们就可以得到一个可执行文件,默认为 a.out, 这也是小编在前言中说的。
-E选项:编译器执行完预处理阶段就停止执行,后面的编译、汇编等操作就不会执行。
-S选项:编译器执行完编译阶段就会停止。
-c选项:编译器执行完汇编阶段就会停止。

其实,这三个阶段只是限定了编译器执行操作的截止时间,而不是单独的将某一步拎出来执行。

二 预处理阶段

执行 gcc -E hello.c > hello.i 命令后,我们仅仅执行预编译操作,生成一个.i 文件 (这个文件是我们最后还可以读得懂的文件了,我们可以打开这个文件,仔细观察程序出现了哪些变化)

那么预处理阶段都进行了哪些操作呢?

  1. 对所有以 # 开头的语句进行处理,其中包括我们熟知的:#define、#include <xxx.h>、条件编译指令#ifdef等。
  2. 删除所有的“/**/”和“//”注释。
  3. 添加行号和文件名标识,方便编译器产生的调用以及当出现编译错误或者是警告时可以显示行号。
  4. 保留所有的 #pragma 编译指令

这里小编想着重阐述的是第一部分!!!

程序中以“#include”开头的语句都会被替换成相应头文件中的内容 (也就是说,项目中不论是自己写的 被#include ""引用的.h 头文件还是系统自带的#include <> 头文件,在预处理阶段阶段之后都会消失,并且这个过程是递归进行的,因为被包含的文件还有可能包含了其他文件,同时为了避免头文件的重复包含,我们引入了#ifdef,#ifndef等条件编译指令,这里就不细说)。
此外程序中的#define 定义的宏在使用的地方都会进行替换 (大家不要小看宏定义,这决不仅是 #define PI 3.14之后进行使用这么简单)
关于宏定义的其他操作如下:
C语言程序从编写到运行历经的几个阶段
还有系统已经定义好的宏,我们可以直接拿过来使用
C语言程序从编写到运行历经的几个阶段
C语言程序从编写到运行历经的几个阶段
C语言程序从编写到运行历经的几个阶段
在这里小编想强调两点:

  • 宏 只是替换
    例如我们上图中定义: #define S(a, b) a * b,这显然是用来求乘积的,那我们现在在程序中调用它最终的结果是什么呢?
    例如:S(5, 3 + 1),最后的结果会是: 5 * 4 = 20 吗?
    显然不是,这条语句会被替换成:5 * 3 + 1,所以最终的答案是 16 !!!
    同时也因为只是替换,所以宏替换不会占用程序的运行时间。
  • 可以通过宏 定义代码段
    在这里大家可以粗略的理解成 “宏可以产生代码”。
    至于每一行语句的最后需要加上反斜杠,这是因为宏定义只可以出现在一行,所以我们才使用 '\'进行连接。

三 编译阶段

使用 -S选项,编译器执行完编译阶段就结束,最后形成 .s 文件

应该说编译阶段是整个程序从C到机器语言翻译过程的核心,我们之前学习的编译原理这门课中讲到的词法分析、语法分析、语义分析以及之后的优化等其他操作, 其实就是在这个阶段执行的。

四 汇编阶段

使用 -c 选项,编译器执行完汇编阶段就结束,形成 .o (windows下为 .obj ) 对象文件。
其中汇编器将会汇编代码转换为机器可识别的机器代码,之前项目中有几个 source.c 文件,此时就会出现几个对象文件

五 链接阶段

前一个阶段我们得到了若干个对象文件,现在我们要做的就是将这几个对象文件链接起来,形成最后的可执行文件。
(这其中还涉及到静态链接库和动态链接库的概念,若想了解,请点击我

至此,我们的阐述就结束了。加油,路漫漫其修远兮,吾将上下而求索,与君共勉!!!