C/C++ 程序最简单的 “Hello World” 几乎是每个程序员闭着眼睛就能写出的,编译运行过程一气呵成,基本成了程序入门和开发环境测试的默认标准。
例如,一个最基本的 “Hello World” 的 C 语言程序像下面这样:
1 |
|
在 Linux 中,当我们使用 GCC 来编译此程序时,只需要使用最简单的命令(假设源文件名为 hello.c
):
1 | gcc hello.c |
事实上,上述过程可以分解为 4 个步骤,分别是预处理(Prepressing) 、编译(Compilation) 、汇编(Assembly) 和链接(Linking),如下图所示:
1. 预编译
首先是源代码文件 hello.c
和相关的头文件,如 stdio.h
等被预编译器 cpp
(这里预编译器的名字就叫 “cpp”,下面示例中也使用了这个命令)预编译成一个 .i
文件。对于 C++ 程序来说,它的源代码文件的扩展名可能是 .cpp
或..cxx
,头文件的扩展名可能是 .hpp
,而预编译后的文件扩展名是 .i
。第一步预编译的过程相当于如下命令 (-E
表示只进行预编译):
1 | gcc -E hello.c -o hello.i |
或者:
1 | cpp hello.c > hello.i |
预编译过程主要处理那些源代码文件中的以 #
开始的预编译指令。比如 #include
、#define
等,主要处理规则如下:
- 将所有的
#define
删除,并且展开所有的宏定义。 - 处理所有条件预编译指令,比如
#if
、#ifdef
、#elif
、#else
、#endif
。 - 处理
#include
预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。 - 删除所有的注释
//
和/**/
。 - 添加行号和文件名标识,比如
#2“hello.c”2
,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。 - 保留所有的
#pragma
编译器指令,因为编译器须要使用它们。
经过预编译后的 .i
文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到 .i
文件中。所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。
2. 编译
编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生c成相应的汇编代码文件。
这个过程往往是我们所说的整个程序构建的核心部分,也是最复杂的部分之一。本文不会深入讨论编译过程,仅简单介绍。
汇编输出文件后缀为 .s
。
上面的编译过程相当于如下命令:
1 | gcc -s hello.i -o hello.s |
现在版本的 GCC 把预编译和编译两个步骤合并成一个步骤,使用一个叫做 cc1
的程序来完成这两个步骤。这个程序位于 “/usr/lib/gcc/i486-linux-gnu/4.1/”,我们也可以直接调用 ccI
来完成它:
1 | /usr/lib/gcc/i486-linux-gnu/4.1/cc1 hello.c |
或者使用如下命令:
1 | gcc -S hello.c -o hello.s |
都可以得到汇编输出文件 hello.s
。
对于 C 语言的代码来说,这个预编译和编译的程序是 cc1
,对于 C++ 来说,有对应的程序叫做 cc1plus
;Objective-C 是 cc1obj
;fortran
是 f771
;Java 是 jc1
。所以实际上 gcc
这个命令只是这些后台程序的包装,它会根据不同的参数要求去调用预编译编译程序 cc1
、汇编器 as
、链接器 ld
。
3. 汇编
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没有复杂的语法,也没有语义也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了,“汇编”这个名字也来源于此。上面的汇编过程我们可以调用汇编器 as
来完成,其输出的文件称为目标文件(Object File),后缀为.o
:
1 | as hello.s -o hel1o.o |
或者:
1 | gcc -c hello.s -o hello.o |
或者使用 gcc
命令从 C 源代码文件开始,经过预编译、编译和汇编直接输出目标文件:
1 | gcc -c hel1o.c -o hello.o |
4. 链接
链接的具体细节相对复杂,这里仅仅简单介绍什么是链接。
链接就是将程序运行需要的外部资源和程序二进制代码链接在一起,保证程序能正确运行。链接以后才能输出 .out
可执行文件。
链接分为两种:
- 静态链接:代码从其所在的静态链接库中拷贝到最终的可执行程序中,在该程序被执行时,这些代码会被装入到该进程的虚拟地址空间中。
- 动态链接:代码被放到动态链接库或共享对象的某个目标文件中,链接程序只是在最终的可执行程序中记录了共享对象的名字等一些信息。在程序执行时,动态链接库的全部内容会被映射到运行时相应的虚拟地址的空间。
两种链接方式的优缺点:
- 静态链接:浪费空间,每个可执行程序都会有目标文件的一个副本,这样如果目标文件进行了更新操作,就需要重新进行编译链接生成可执行程序(更新困难),优点就是执行的时候运行速度快,因为可执行程序具备了程序运行的所有内容。
- 动态链接:节省内存、更新方便,但是动态链接是在程序运行时,每次执行都需要链接,相比静态链接会有一定的性能损失。