编辑推荐: |
本文主要介绍了C/C++代码的预处理、编译、汇编、链接全过程。 希望能为大家提供一些参考或帮助。
文章来自于知乎,由火龙果Linda编辑推荐。 |
|
1. C/C++运行的四个步骤
编写完成一个C/C++程序后,想要运行起来,必须要经过四个步骤:预处理、编译、汇编和链接。每个步骤都会生成对应的文件,如下图所示(注意后缀名):
C/C++代码编译全过程
第3节将通过一个简易C++工程演示图中的全过程,并解释细节。
2.名词解释
为了后面过程的介绍更方便,这里对C++编译过程中涉及的一些常用名词进行解释。
2.1 GCC、GNU、gcc与g++
GNU:一个操作系统。
GCC:GNU Compiler Collection(GNU编译器集合)的缩写,可以理解为一组GNU操作系统中的编译器集合,可以用于编译C、C++、Java、Go、Fortan、Pascal、Objective-C等语言。
gcc:GCC(编译器集合)中的GNU C Compiler(C 编译器)
g++:GCC(编译器集合)中的GNU C++ Compiler(C++ 编译器)
简单来说,gcc调用了GCC中的C Compiler,而g++调用了GCC中的C++ Compiler。
- 对于 *.c 和 *.cpp 文件,gcc分别当作 c 和 cpp文件编译,而g++则统一当作cpp文件编译。
2.2 代码编译命令
gcc/g++常用指令选项:
gcc/g++常用指令选项
2.3 GDB(gdb)
GDB(gdb)全称“GNU symbolic debugger”,是 Linux 下常用的程序调试器。
为了能够使用 gdb 调试,需要在代码编译的时候加上-g,如
本文中只演示从源代码生成可执行二进制文件的过程,暂不涉及调试过程。调试的配置会在另一篇文章中专门介绍。
3. C++编译过程详解
本节内容用下面的简单C++工程做演示。示例的文件结构如下:
|—— include
|—— func.h
|—— src
|—— func.cpp
|—— main.cpp
|
其中,main.cpp是主要代码,include/func.h是自定义函数的头文件,src/func.cpp是函数的具体实现
各个文件的内容如下:
// main.cpp
#include <iostream>
#include "func.h"
using namespace std;
int main(){
int a = 1;
int b = 2;
cout << "a + b = " << sum(a, b) << endl;;
return 0;
}
// func.h
#ifndef FUNC_H
#define FUNC_H
int sum(int a, int b);
#endif
// func.cpp
#include "func.h"
int sum(int a, int b) {
return a + b;
}
|
3.1 预处理(Preprocess)
预处理,顾名思义就是编译前的一些准备工作。
预编译把一些#define的宏定义完成文本替换,然后将#include的文件里的内容复制到.cpp文件里,如果.h文件里还有.h文件,就递归展开。在预处理这一步,代码注释直接被忽略,不会进入到后续的处理中,所以注释在程序中不会执行。
gcc/g++的预处理实质上是通过预处理器cpp(应该是c preprocess的缩写?)来完成的,所以我们既可以通过g++
-E,也可以通过cpp命令对main.cpp进行预处理:
g++ -E -I include/ main.cpp -o main.i
# 或者直接调用 cpp 命令
cpp -I include/ main.cpp -o main.i
|
上述命令中: - g++ -E 是让编译器在预处理之后就退出,不进行后续编译过程,等价于cpp指令
- -I include/用于指定头文件目录 - main.cpp是要预处理的源文件 - -o main.i用于指定生成的文件名
预处理之后的程序格式为 *.i,仍是文本文件,可以用任意文本编辑器打开。
执行完预处理后的文件结构如下:
|—— include
|—— func.h
|—— src
|—— func.cpp
|—— main.cpp
|—— main.i
|
3.2 编译(Compile)
编译只是把我们写的代码转为汇编代码,它的工作是检查词法和语法规则,所以,如果程序没有词法或则语法错误,那么不管逻辑是怎样错误的,都不会报错。
编译不是指程序从源文件到二进制程序的全部过程,而是指将经过预处理之后的程序转换成特定汇编代码(assembly
code)的过程。
编译的指令如下:
g++ -S -I include/ main.cpp -o main.s
|
与预处理类似,上述命令中: - g++ -S是让编译器在编译之后停止,不进行后续过程 - -I include/用于指定头文件目录
- main.cpp是要编译的源文件 - -o main.s用于指定生成的文件名
编译完成后,会生成程序的汇编代码main.s,这也是文本文件,可以直接用任意文本编辑器查看。
执行完编译后的文件结构如下:
|—— include
|—— func.h
|—— src
|—— func.cpp
|—— main.cpp
|—— main.i
|—— main.s
|
3.3 汇编(Assemble)
汇编过程将上一步的汇编代码(main.s)转换成机器码(machine code),这一步产生的文件叫做目标文件(main.o),是二进制格式。
gcc/g++的汇编过程通过 as 命令完成,所以我们可以通过g++ -c或as命令完成汇编:
g++ -c -I include/ main.cpp -o main.o
# 或者直接调用 as 命令
as main.s -o main.o
|
上述指令中: - g++ -c让编译器在汇编之后退出,等价于 as - -I include/仍是用于指定头文件目录
- main.cpp是要汇编的源文件 - -o main.o用于指定生成的文件名
汇编这一步需要为每一个源文件(本文示例代码中为main.cpp、func.cpp)产生一个目标文件。因此func.cpp也需要执行一次这个汇编过程产生一个func.o文件:
# 可以用 g++ -c 命令一步生成 func.o
g++ -c -I include/ src/func.cpp -o src/func.o
# 当然也可以按照上面的预处理、编译、汇编三个步骤生成func.o
|
到了这一步,代码的文件结构如下:
|—— include
|—— func.h
|—— src
|—— func.cpp
|—— func.o
|—— main.cpp
|—— main.i
|—— main.s
|—— main.o
|
3.4 链接(Link)
C/C++代码经过汇编之后生成的目标文件(*.o)并不是最终的可执行二进制文件,而仍是一种中间文件(或称临时文件),目标文件仍然需要经过链接(Link)才能变成可执行文件。
既然目标文件和可执行文件的格式是一样的(都是二进制格式),为什么还要再链接一次呢?
因为编译只是将我们自己写的代码变成了二进制形式,它还需要和系统组件(比如标准库、动态链接库等)结合起来,这些组件都是程序运行所必须的。
链接(Link)其实就是一个“打包”的过程,它将所有二进制形式的目标文件(.o)和系统组件组合成一个可执行文件。完成链接的过程也需要一个特殊的软件,叫做链接器(Linker)。
此外需要注意的是:C++程序编译的时候其实只识别.cpp文件,每个cpp文件都会分别编译一次,生成一个.o文件。这个时候,链接器除了将目标文件和系统组件组合起来,还需要将编译器生成的多个.o或者.obj文件组合起来,生成最终的可执行文件(Executable
file)。
以本文中的代码为例,将func.o和main.o链接成可执行文件main.out,指令如下:
g++ src/func.o main.o -o main.out
|
-o main.out用于指定生成的可执行二进制文件名
由于g++自动链接了系统组件,所以我们只需要把自定义函数的目标文件与main.o链接即可。
运行main.out,结果如下:
3.5 小结
从上面的介绍可以看出,从C++源代码到最终的可执行文件的中间过程并不简单。了解预处理、编译、汇编、链接各个步骤的作用有助于我们处理更加复杂的项目工程。
不过也不必被这么麻烦的编译过程劝退,当我们编译简单.cpp代码时,
// hello.cpp
#include <iostream>
using namespace std;
int main(){
cout << "Hello, world!" << endl;
return 0;
}
|
仍然可以直接使用g++命令生成可执行文件,而不必考虑中间过程:
g++ hello.cpp -o hello
./hello
Hello, world!
|
|