2023- 详解C C++ 代码的预处理、编译、汇编、链接全过程
详解C/C++ 代码的预处理、编译、汇编、链接全过程
1. C/C++ 运行的四个步骤
编写完成一个

2. 名词解释
为了后面过程的介绍更方便,这里对
2.1 GCC、GNU、gcc 与g++
- GNU:一个操作系统,具体内容不重要,感兴趣可以参考:GCC、
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++ 编译器)
简单来说,
- 对于
*.c
和*.cpp
文件,gcc 分别当作c 和cpp 文件编译,而g++ 则统一当作cpp 文件编译。
2.2 代码编译命令
指令选项 | 功能 |
---|---|
-E(注意大写) | 预处理*.i 文本文件 |
-S(注意大写) | 编译指定的源文件,但不进行汇编*.s 汇编文件 |
-c | 编译、汇编指定的源文件,但不进行链接*.o 目标文件 |
-o | 指定生成文件的文件名 |
-I lib | |
-std= | 手动指定编程语言标准,如 |
2.3 GDB(gdb)
GDB(gdb)全称“GNU symbolic debugger”,是-g
,如
g++ -g -o test test.cpp
1
本文中只演示从源代码生成可执行二进制文件的过程,暂不涉及调试过程。调试的配置会在另一篇文章中专门介绍。
3. C++ 编译过程详解
主要参考:
C++ 预编译,编译,汇编,链接[C/C++ 语言编译链接过程]( https://zhuanlan.zhihu.com/p/88255667#:~:text=C/C++语言编译链接过程1 1. 预处理(Preprocessing) 预处理用于将所有的#include 头文件以及宏定义替换成其真正的内容 ,预处理之后得到的仍然是文本文件,但文件体积会大很多。 …, 成最终的 可执行文件(executable file) 。 …5 5. 数据和指令)
本节内容用下面的简单
|—— include
|—— func.h
|—— src
|—— func.cpp
|—— main.cpp
12345
其中,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;
}
12345678910111213
// func.h
#ifndef FUNC_H
#define FUNC_H
int sum(int a, int b);
#endif
1234567
// func.cpp
#include "func.h"
int sum(int a, int b) {
return a + b;
}
123456
3.1 预处理(Preprocess)
预处理,顾名思义就是编译前的一些准备工作。
预编译把一些#define
的宏定义完成文本替换,然后将#include
的文件里的内容复制到.cpp
文件里,如果.h
文件里还有.h
文件,就递归展开。在预处理这一步,代码注释直接被忽略,不会进入到后续的处理中,所以注释在程序中不会执行。
cpp
(应该是g++ -E
,也可以通过cpp
命令对main.cpp
进行预处理:
g++ -E -I include/ main.cpp -o main.i
# 或者直接调用 cpp 命令
cpp -I include/ main.cpp -o main.i
123
上述命令中:
g++ -E
是让编译器在预处理之后就退出,不进行后续编译过程,等价于cpp
指令-I include/
用于指定头文件目录main.cpp
是要预处理的源文件-o main.i
用于指定生成的文件名
预处理之后的程序格式为 *.i
,仍是文本文件,可以用任意文本编辑器打开。
执行完预处理后的文件结构如下:
|—— include
|—— func.h
|—— src
|—— func.cpp
|—— main.cpp
|—— main.i
123456
3.2 编译(Compile)
编译只是把我们写的代码转为汇编代码,它的工作是检查词法和语法规则,所以,如果程序没有词法或则语法错误,那么不管逻辑是怎样错误的,都不会报错。
编译不是指程序从源文件到二进制程序的全部过程,而是指将经过预处理之后的程序转换成特定汇编代码
(assembly code) 的过程。
编译的指令如下:
g++ -S -I include/ main.cpp -o main.s
1
与预处理类似,上述命令中:
g++ -S
是让编译器在编译之后停止,不进行后续过程-I include/
用于指定头文件目录main.cpp
是要编译的源文件-o main.s
用于指定生成的文件名
编译完成后,会生成程序的汇编代码main.s
,这也是文本文件,可以直接用任意文本编辑器查看。
执行完编译后的文件结构如下:
|—— include
|—— func.h
|—— src
|—— func.cpp
|—— main.cpp
|—— main.i
|—— main.s
1234567
3.3 汇编(Assemble)
汇编过程将上一步的汇编代码
( main.s
) 转换成机器码(machine code) ,这一步产生的文件叫做目标文件( main.o
) ,是二进制格式。
as
命令完成,所以我们可以通过g++ -c
或as
命令完成汇编:
g++ -c -I include/ main.cpp -o main.o
# 或者直接调用 as 命令
as main.s -o main.o
123
上述指令中:
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
123
到了这一步,代码的文件结构如下:
|—— include
|—— func.h
|—— src
|—— func.cpp
|—— func.o
|—— main.cpp
|—— main.i
|—— main.s
|—— main.o
123456789
3.4 链接(Link)
\*.o
既然目标文件和可执行文件的格式是一样的(都是二进制格式
) ,为什么还要再链接一次呢?
因为编译只是将我们自己写的代码变成了二进制形式,它还需要和系统组件(比如标准库、动态链接库等)结合起来,这些组件都是程序运行所必须的。
链接(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
1
-o main.out
用于指定生成的可执行二进制文件名- 由于
g++
自动链接了系统组件,所以我们只需要把自定义函数的目标文件与main.o
链接即可。
运行main.out
,结果如下:
./main.out
a + b = 3
12
3.5 小结
从上面的介绍可以看出,从
不过也不必被这么麻烦的编译过程劝退,当我们编译简单.cpp
代码时,
// hello.cpp
#include <iostream>
using namespace std;
int main(){
cout << "Hello, world!" << endl;
return 0;
}
12345678
仍然可以直接使用g++
命令生成可执行文件,而不必考虑中间过程:
g++ hello.cpp -o hello
./hello
Hello, world!