class="hljs-ln-code"> class="hljs-ln-line">#include class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="3"> class="hljs-ln-code"> class="hljs-ln-line">int main(){ class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="4"> class="hljs-ln-code"> class="hljs-ln-line"> printf("hello world!\n"); class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="5"> class="hljs-ln-code"> class="hljs-ln-line"> return 0; class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="6"> class="hljs-ln-code"> class="hljs-ln-line">} class="hljs-button signin" data-title="登录后复制" data-report-click="{"spm":"1001.2101.3001.4334"}" onclick="hljs.signin(event)">
编译过程只需:
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="1"> class="hljs-ln-code"> class="hljs-ln-line">$ g++ helloworld.cpp
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="2"> class="hljs-ln-code"> class="hljs-ln-line">$ ./a
- class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="3"> class="hljs-ln-code"> class="hljs-ln-line">hello world!
class="hljs-button signin" data-title="登录后复制" data-report-click="{"spm":"1001.2101.3001.4334"}" onclick="hljs.signin(event)">
编译过程看似容易,只需一个"g++",以至于大家觉得编译事件很简单的事。事实真的如此吗?、
上述gcc命令其实依次执行了四步操作:1.预处理(Preprocessing), 2.编译(Compilation), 3.汇编(Assemble), 4.链接(Linking)。
大致流程如图所示:

预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成 预编译文件.i
编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成 汇编文件.s
汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成 可重定位目标文件.o
链接阶段:将多个目标文件及所需要的库连接成最终的 可执行目标文件.exe
展开来讲:
一、预处理阶段(产生.i文件, -E)
首先是源代码文件helloworld.cpp和相关头文件预处理成一个.i文件。命令如下
g++ -E helloworld.cpp -o helloworld.i
class="hljs-button signin" data-title="登录后复制" data-report-click="{"spm":"1001.2101.3001.4334"}" onclick="hljs.signin(event)">
在实际编译工作开始之前,预处理器指令指示编译器对源码进行临时扩充,以为之后的步骤做好准备。
在 C++ 中,预处理器指令以 #
号开头,比如 #include
、#define
和 #if
等。在这一阶段,编译器逐个处理 C++ 源码文件。
- 对于
#define
指令,编译器将源码中的宏替换成宏定义中的内容; - 对于
#if
、#ifdef
和 #ifndef
指令,编译器将有选择地跳过或选中部分源代码; - 而对于
#include
指令,编译器将把对应的库的源码插入到当前源代码中——这通常是一些通用的声明。被 #include
指令引入的头文件( .h
)往往会包含大量的代码,你引入的越多,最后生成的预编译文件就越大。 -
预处理过程还会过滤掉所有注释/**/和//里面的内容。
-
另外还会添加行号和文件名标识,使编译器能分辨出每一行来自哪个文件,以便在调试过程中能生成对应的错误信息。
-
最后会保留#pragma编译器指令,因为编译器需要使用它们。如:#pragma once 是为了防止有文件被重复引用。
总的来说,预编译过的文件会比原来的 C++ 源码更大一些。
问:
1、#ifndef,#ifdef,#endif的作用?
防止重复包含头文件。
2、#include尖括号和双引号的区别?
#include<>
,从标准库中寻找头文件
#include " "
,从当前目录开始寻找头文件
附:
下表是常用的一些预处理命令

还有下列几种预处理宏(是双下划线)
__LINE__ 表示正在编译的文件的行号
__FILE__表示正在编译的文件的名字__DATE__表示编译时刻的日期字符串,例如: "25 Dec 2007"
__TIME__ 表示编译时刻的时间字符串,例如: "12:30:55"
__STDC__ 判断该文件是不是定义成标准 C 程序
二、编译阶段(产生.s文件,-s)
编译阶段是检查语法,将去除了预编处理器指令的纯 C++ 代码生成汇编。将预处理的文件进行一系列的词法分析,语法分析,语义分析,以及优化后产生相应的汇编代码文件,这个过程是程序构建的核心部分,也是最复杂的。在 C++ 中,如果一个对象只声明,不进行定义,编译器仍然可以从源代码产生目标文件,因为这个对象也可以指向某些当前代码中还未定义的标识符。
执行命令(-s)如下:
g++ -S helloworld.i -o helloworld.s
class="hljs-button signin" data-title="登录后复制" data-report-click="{"spm":"1001.2101.3001.4334"}" onclick="hljs.signin(event)">
词法分析:利用类似于“有限状态机”的算法,将源代码程序输入到扫描机中,将其中的字符序列分割成一系列的记号。
语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的语法树是一种以表达式为节点的树。
语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义——在编译期能分期的语义,相对应的动态语义是在运行期才能确定的语义。
优化:源代码级别的一个优化过程。
目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列——汇编语言表示。
目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移来替代乘法运算、删除多余的指令等。


三、汇编阶段(产生.o或.obj文件, -c)
汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程,即生成目标文件。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。目标文件由段组成,通常一个目标文件中至少有两个段:
这一步生成的目标文件可以被放在被称为静态库的包中,以备后续使用——也就是说,如果你只修改了一个文件,你并不需要重新编译整个项目的源代码。
四、链接阶段(产生.out或.exe文件, -o)
链接就是把每个源代码独立的编译,然后按照它们的要求将它们组装起来,链接主要解决的是源代码之间的相互依赖问题,链接的过程包括地址和空间的分配,符号决议,和重定位等这些步骤。
在这一阶段,编译器将把上一阶段中编译器产生的各种目标文件链接起来,将未定义标识符的引用全部替换成它们对应的正确地址。没有把目标文件链接起来,就无法生成能够正常工作的程序,就像一页没有页码的目录一样,没什么用处。完成链接工作之后,链接器根据编译目的不同,把链接的结果生成为一个动态链接库,或是一个可执行文件。
链接的过程也会抛出各种异常,通常是重复定义或者缺失定义等错误。不只是没进行定义的情况,如果你忘记将对某个库或是目标文件的引用导入进来,让链接器能找到定义的话,也会发生这类错误。重复定义则刚好相反,当有两个库或目标文件中含有对同一个标识符的定义时,就可能出现重复定义错误。
根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种:
1、静态链接/库
在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中,因此对应的链接方式称为静态链接。
静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。

静态库的缺点在于:浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。
2、动态链接/库
动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。

总结
本文中,我们介绍了 C++ 编译过程的各个阶段,更加详细地了解了整个过程。通过学习俗如何使用 C++ 编译器,并对各种 C++ 编译器进行概述,你得以一窥编译过程的幕后细节,并对它有了一些深入的了解,希望能给你带来帮助。
参考资料:
1. GCC and Make - A Tutorial on how to compile, link and build C/C++ applications (ntu.edu.sg)
2. 10 分钟看懂 C++ 编译过程 | 坎德人的小包包 (oicebot.github.io)
3. 一个C++源文件从文本到可执行文件经历的过程_青萍之末的博客-CSDN博客_c++源文件从文本到可执行文件经历的过程
上一篇: 从B站 (哔哩哔哩) 泄露的源码里发现了B站视频推荐的秘密
下一篇: 400+条实用C/C++框架、库、工具整理 ,你能想到的都在这里了
如果有什么要补充的,欢迎下方👇评论区留言。
1份赞许 = 100分的认可,如果感觉还不错,点个赞👍 支持一下吧 ~
不定期分享 有趣、有料、有营养内容,欢迎 订阅关注 🤝 我的博客 ,期待在这与你相遇 ~
>>
评论记录:
回复评论: