您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   Code  
会员   
   
 
     
   
 
 订阅
详解C/C++代码的预处理、编译、汇编、链接全过程
 
 
  375  次浏览      7 次
 2024-1-24
 
编辑推荐:
本文主要介绍了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,如

g++ -g -o test test.cpp

 

本文中只演示从源代码生成可执行二进制文件的过程,暂不涉及调试过程。调试的配置会在另一篇文章中专门介绍。

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,结果如下:

./main.out
a + b = 3

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!
   
375 次浏览       7
相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程

最新活动计划
MBSE(基于模型的系统工程)4-18[北京]
自然语言处理(NLP) 4-25[北京]
基于 UML 和EA进行分析设计 4-29[北京]
以用户为中心的软件界面设计 5-16[北京]
DoDAF规范、模型与实例 5-23[北京]
信息架构建模(基于UML+EA)5-29[北京]
 
 
最新文章
.NET Core 3.0 正式公布:新特性详细解读
.NET Core部署中你不了解的框架依赖与独立部署
C# event线程安全
简析 .NET Core 构成体系
C#技术漫谈之垃圾回收机制(GC)
最新课程
.Net应用开发
C#高级开发技术
.NET 架构设计与调试优化
ASP.NET Core Web 开发
ASP.Net MVC框架原理与应用开发
成功案例
航天科工集团子公司 DotNet企业级应用设计与开发
日照港集 .NET Framewor
神华信 .NET单元测试
台达电子 .NET程序设计与开发
神华信息 .NET单元测试