GCC编译C/C++程序过程
GCC 编译器并未提供给用户可用鼠标点击的界面窗口,要想调用 GCC 编译器编译 C 或者 C++ 程序,只能通过执行相应的 gcc 或者 g++ 指令。实际上,C 或者 C++ 程序从源代码生成可执行程序的过程,需经历 4 个过程,分别是预处理、编译、汇编和链接。
同样,使用 GCC 编译器编译 C 或者 C++ 程序,也必须要经历这 4 个过程。但考虑在实际使用中,用户可能并不关心程序的执行结果,只想快速得到最终的可执行程序,因此 gcc 和 g++ 都对此需求做了支持。可以一步生成可执行程序。
编译一步完成
先以运行 C 语言程序为例,给大家演示如何使用 gcc 快速获得对应的可执行程序。如下就是一段 C 语言程序:
#include <stdio.h> int main(){ puts("hello world"); return 0; }
如上所示,这是一个很简单的输出“Hello,World!”字符串的 C 语言程序,接下来打开命令行窗口(Terminal),编写如下 gcc 指令:
由此 GCC 编译器就帮我们在当前目录下生成了对应的可执行文件,该文件的名称为 a.out。gcc 或者 g++ 指令还支持用户手动指定最终生成的可执行文件的文件名,例如修改前面执行 C、C++ 程序的 gcc 和 g++ 指令:
其中 -o 选项用于指定要生成的文件名,例如 -o demo.exe 即表示将生成的文件名设为 demo.exe。可以看到,GCC 编译器支持使用 gcc(g++)指令“一步编译”指定的(C++)程序。
注意,虽然我们仅编写了一条 gcc 或者 g++ 指令,但其底层依据是按照预处理、编译、汇编、链接的过程将 C 、C++ 程序转变为可执行程序的。而本应在预处理阶段、编译阶段、汇编阶段生成的中间文件,此执行方式默认是不会生成的,只会生成最终的 a.out 可执行文件(除非为 gcc 或者 g++ 额外添加 -save-temps 选项)
分步编译
所谓“分步编译”,即由用户手动调用 GCC 编译器完成对 C、C++源代码的预处理、编译、汇编以及链接过程,每个阶段都会生成对源代码加工后的文件。GCC 编译器除了提供 gcc 和 g++ 这 2 个指令之外,还提供有大量的指令选项,方便用户根据自己的需求自定义编译方式。表 1 罗列出了实际使用 gcc 或者 g++ 指令编译 C/C++ 程序时,常用的一些指令选项:
gcc/g++指令选项 | 功 能 |
---|---|
-E(大写) | 预处理指定的源文件,不进行编译。 |
-S(大写) | 编译指定的源文件,但是不进行汇编。 |
-c | 编译、汇编指定的源文件,但是不进行链接。 |
-o | 指定生成文件的文件名。 |
-llibrary(-I library) | 其中 library 表示要搜索的库文件的名称。该选项用于手动指定链接环节中程序可以调用的库文件。建议 -l 和库文件名之间不使用空格,比如 -lstdc++。 |
-ansi | 对于 C 语言程序来说,其等价于 -std=c90;对于 C++ 程序来说,其等价于 -std=c++98。 |
-std= | 手动指令编程语言所遵循的标准,例如 c89、c90、c++98、c++11 等。 |
1、预处理
通过为 gcc 指令添加 -E 选项,即可控制 GCC 编译器仅对源代码做预处理操作。预处理操作,进行头文件展开,宏定义展开、删除注释等工作。主要是处理那些源文件和头文件中以 # 开头的命令(比如 #include、#define、#ifdef 等),并删除程序中所有的注释 // 和 /* ... */
默认情况下 gcc -E 指令只会将预处理操作的结果输出到屏幕上,并不会自动保存到某个文件。因此该指令往往会和 -o 选项连用,将结果导入到指令的文件中:
Linux 系统中通常用 ".i" 作为 C 语言程序预处理后所得文件的后缀名。由此,就完成了 demo.c 文件的预处理操作,并将其结果导入到了 demo.i 文件中。我们可以为 gcc 指令再添加一个 -C 选项,阻止 GCC 删除源文件和头文件中的注释。除了 -C、-o 以外,根据实际场景的需要,gcc -E 后面还可以添加其它的选项,例如:
选 项 | 功 能 |
---|---|
-D name[=definition] | 在处理源文件之前,先定义宏 name。宏 name 必须是在源文件和头文件中都没有被定义过的。将该选项搭配源代码中的#ifdef name命令使用,可以实现条件式编译。如果没有指定一个替换的值(即省略 =definition),该宏被定义为值 1。 |
-U name | 如果在命令行或 GCC 默认设置中定义过宏 name,则“取消”name 的定义。-D 和 -U 选项会依据在命令行中出现的先后顺序进行处理。 |
-include file | 如同在源代码中添加 #include "file" 一样。 |
-iquote dir | 对于以引号(#include "")导入的头文件中,-iquote 指令可以指定该头文件的搜索路径。当 GCC 在源程序所在目录下找不到此头文件时,就会去 -iquote 指令指定的目录中查找。 |
-I dir | 同时适用于以引号 "" 和 <> 导入的头文件。当 GCC 在 -iquote 指令指定的目录下搜索头文件失败时,会再自动去 -I 指定的目录中查找。该选项在 GCC 10.1 版本中已被弃用,并建议用 -iquote 选项代替。 |
-isystem dir -idirafter dir |
都用于指定搜索头文件的目录,适用于以引号 "" 和 <> 导入的头文件。 |
其中,对于指定 #include 搜索路径的几个选项,作用的先后顺序如下:
- 对于用 #include "" 引号形式引入的头文件,首先搜索当前程序文件所在的目录,其次再前往 -iquote 选项指定的目录中查找;
- 前往 -I 选项指定的目录中搜索;
- 前往 -isystem 选项指定的目录中搜索;
- 前往默认的系统路径下搜索;
- 前往 -idirafter 选项指定的目录中搜索。
2、编译
编译是整个程序构建的核心部分,也是最复杂的部分之一。所谓编译,简单理解就是将预处理得到的程序代码,经过一系列的词法分析、语法分析、语义分析以及优化,加工为当前机器支持的汇编代码。通过给 gcc 指令添加 -S(注意是大写)选项,即可令 GCC 编译器仅将指定文件加工至编译阶段,并生成对应的汇编代码文件:
需要注意的是,gcc -S 指令操作的文件并非必须是经过预处理后得到的 .i 文件,-S 选项的功能是令 GCC 编译器将指定文件处理至编译阶段结束。这也就意味着,gcc -S 指令可以操作预处理后的 .i 文件,也可以操作源代码文件:
- 如果操作对象为 .i 文件,则 GCC 编译器只需编译此文件;
- 如果操作对象为 .c 或者 .cpp 源代码文件,则 GCC 编译器会对其进行预处理和编译这 2 步操作
3、汇编
汇编其实就是将汇编代码转换成可以执行的机器指令。大部分汇编语句对应一条机器指令,有的汇编语句对应多条机器指令。相对于编译操作,汇编过程会简单很多,它并没有复杂的语法,也没有语义,也不需要做指令优化,只需要根据汇编语句和机器指令的对照表一一翻译即可。汇编的作用是生成目标文件:
通过为 gcc 指令添加 -c 选项(注意是小写字母 c),即可让 GCC 编译器将指定文件加工至汇编阶段,并生成相应的目标文件。
可以看到,该指令生成了和 demo.s 同名但后缀名为 .o 的文件,这就是经过汇编操作得到的目标文件。如果必要的话,还可以为 gcc -c 指令在添加一个 -o 选项,用于将汇编操作的结果输入到指定文件中。
需要强调的一点是,和 gcc -S 类似,gcc -c 选项并非只能用于加工 .s 文件。事实上,-c 选项只是令 GCC 编译器将指定文件加工至汇编阶段,但不执行链接操作。这也就意味着:
- 如果指定文件为源程序文件(例如 demo.c),则 gcc -c 指令会对 demo.c 文件执行预处理、编译以及汇编这 3 步操作;
- 如果指定文件为刚刚经过预处理后的文件(例如 demo.i),则 gcc -c 指令对 demo.i 文件执行编译和汇编这 2 步操作;
- 如果指定文件为刚刚经过编译后的文件(例如 demo.s),则 gcc -c 指令只对 demo.s 文件执行汇编这 1 步操作。
4、链接
得到生成目标文件之后,接下来就可以直接使用 gcc 指令继续执行链接操作。链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
gcc 会根据所给文件的后缀名 .o,自行判断出此类文件为目标文件,仅需要进行链接操作。也可以通过 -o 选项指定生成的文件名。