C++ 编译过程 简要分析
在一些远古时代的ide中,代码总是需要编译、链接、运行。为什么需要这样做?
在学习 CMake 的前置 Makefile 时总是见到main.o
。这又是什么?
因此我决定学习一下 C++ 是如何把代码编译为可执行程序的。
首先我写了如下代码
main.cpp
#include <iostream>
#include "MyMath.hpp"
int main(){
int l = 10, r = 20;
int sum = MyMath::sum(l, r);
std::cout << "sum = " << sum << std::endl;
}
MyMath.hpp
#ifndef MY_MATH_HPP
#define MY_MATH_HPP
namespace MyMath{
int sum(int l, int r);
}
#endif
MaMath.hpp
#include "MyMath.hpp"
int MyMath::sum(int l, int r) {
int ans = 0;
for(int i = l; i <= r; i ++)
ans += i;
return ans;
}
然后在终端输入如下命令
g++ main.cpp MyMath.cpp -o main.exe ./main.exe
然后得到如下结果
sum = 165
可以看到我们代码成功的编译并运行了。接下来我们来学习一下编译器在这个过程中做了什么。
1|0步骤1:预处理 Preprocessing
首先输入如下命令
g++ -E main.cpp -o main.i g++ -E MyMath.cpp -o MyMath.i
然后我们得到了main.i
和MyMath.i
两个文件,首先我们先看MyMath.i
# 0 "MyMath.cpp"
# 0 "<built-in>"
# 0 "<命令行>"
# 1 "MyMath.cpp"
# 1 "MyMath.hpp" 1
namespace MyMath{
int sum(int l, int r);
}
# 2 "MyMath.cpp" 2
int MyMath::sum(int l, int r) {
int ans = 0;
for(int i = l; i <= r; i ++)
ans += i;
return ans;
}
与MyMath.cpp
对比一下就能发现,这一步基本没有对代码进行修改,只是把代码#include
部分做了一个简单的替换。
再看main.i
文件,很长。但我们直接看最下面
# 4 "MyMath.hpp"
namespace MyMath{
int sum(int l, int r);
}
# 3 "main.cpp" 2
int main(){
int l = 10, r = 20;
int sum = MyMath::sum(l, r);
std::cout << "sum = " << sum << std::endl;
}
可以看到,这里没有修改main
函数的内容,只是把MyMath.hpp
的内容替换进来了。
可以推测的是上面的部分就是替换<iostream>
的内容。
2|0步骤2:编译 Compilation
执行命令
g++ -S main.i -o main.s g++ -S MyMath.i -o MyMath.s
我们依旧先看MyMath.s
.file "MyMath.cpp" .text .globl _ZN6MyMath3sumEii .def _ZN6MyMath3sumEii; .scl 2; .type 32; .endef .seh_proc _ZN6MyMath3sumEii _ZN6MyMath3sumEii: .LFB0: pushq %rbp .seh_pushreg %rbp movq %rsp, %rbp .seh_setframe %rbp, 0 subq $16, %rsp .seh_stackalloc 16 .seh_endprologue movl %ecx, 16(%rbp) movl %edx, 24(%rbp) movl $0, -4(%rbp) movl 16(%rbp), %eax movl %eax, -8(%rbp) jmp .L2 .L3: movl -8(%rbp), %eax addl %eax, -4(%rbp) addl $1, -8(%rbp) .L2: movl -8(%rbp), %eax cmpl 24(%rbp), %eax jle .L3 movl -4(%rbp), %eax addq $16, %rsp popq %rbp ret .seh_endproc .ident "GCC: (GNU) 11.3.0"
这个代码不算很长,但是应该是汇编语言。因为我对汇编语言的了解不多。因此我把这个代码交给DeepSeek让他分析一下。
这段代码是GCC编译器生成的x86-64汇编代码,源自C++源文件"MyMath.cpp"。它实现了
MyMath
类中的一个成员函数sum
,功能是计算两个整数之间的累加和。功能分析
函数
int MyMath::sum(int a, int b)
通过循环累加从a
到b
(包含两端)的所有整数,并返回总和。其逻辑等价于以下C++代码:
int MyMath::sum(int a, int b) { int sum = 0; for (int i = a; i <= b; ++i) { sum += i; } return sum; }
可以看到,这个代码基本与原始的代码相同。因此我们也可以倒退出这一步其实就是把C++翻译成能再对应平台运行的汇编语言。
再看main.s
发现短了不少,不难推测在生成汇编时,编译器并不是逐个翻译,而是只翻译了需要用到的函数。
3|0步骤3:汇编 Assemble
运行下面的命令
g++ -c main.s -o main.o g++ -c MyMath.s -o MyMath.o
得到了两个.o
文件,用vscode已经无法打开了,提示是二进制文件。
问了DeepSeek 得到了如下回复。
在 C++(以及一般的 C/C++ 开发中),
.o
文件是目标文件(Object File),它是编译器将源代码(.cpp
或.c
)编译后生成的中间文件,包含机器码、符号表和调试信息等,但尚未完成最终的链接(Linking)过程。
4|0步骤4:链接 Linking
g++ main.o MyMath.o -o main.exe ./main.exe
这一步就是把刚才生成的两个目标文件拼接成了一个可执行文件。并且是可以成功运行的。
5|0总结
其实不难发现,编译器在处理时是单独的对每一个cpp文件做预处理、编译、汇编三个步骤,然后将所有的目标文件链接为一个可执行文件。
以下回答依旧来着DeepSeek
早期C++的编译步骤
在早期的C/C++开发中,编译过程确实需要手动分步执行预处理、编译、汇编、链接四个阶段。开发者需逐步调用不同工具完成每个步骤:
预处理:
使用预处理器(如cpp
)处理宏、头文件包含等,生成预处理后的源码(.i
文件):cpp main.cpp > main.i 编译:
将预处理后的代码转换为汇编代码(.s
文件),例如使用cc1plus
(GCC的C++编译器前端):cc1plus main.i -o main.s 汇编:
将汇编代码转换为目标文件(.o
文件),使用汇编器(如as
):as main.s -o main.o 链接:
将目标文件与库链接生成可执行文件,使用链接器(如ld
):ld main.o -o main -lc 这一过程繁琐且容易出错,尤其是在处理多文件项目时。
一条命令完成编译的时代
随着编译器前端(如
g++
、clang++
)的成熟,开发者可以通过单条命令直接完成所有步骤。以GCC为例:g++ main.cpp -o main 编译器自动按顺序处理预处理、编译、汇编、链接,无需手动干预。
这一改进从编译器工具链的早期版本就已支持(如GCC在1987年首次发布时已集成),但实际普及依赖于编译器的易用性优化和开发者习惯的转变。现代编译器(如GCC 3.x+、Clang)进一步简化了流程,并支持优化选项(如
-O2
)和调试信息(如-g
)的直接集成。
简化编译步骤的意义
提升开发效率:
减少手动输入多步骤命令的时间,尤其适合快速迭代和调试。降低错误风险:
自动处理依赖关系和步骤顺序,避免遗漏或误操作(如忘记链接库)。统一构建流程:
便于与构建工具(如Make
、CMake
)和IDE集成,实现跨平台一致性。优化编译过程:
编译器可以全局优化代码(如内联展开、跨文件优化),而分步操作可能限制优化空间。降低学习门槛:
新手无需深入理解底层工具链即可快速编译程序。
实际案例对比
假设有一个多文件项目:
main.cpp
和utils.cpp
。
早期分步流程:
# 预处理、编译、汇编每个文件 cpp main.cpp > main.i cc1plus main.i -o main.s as main.s -o main.o cpp utils.cpp > utils.i cc1plus utils.i -o utils.s as utils.s -o utils.o # 链接所有目标文件 ld main.o utils.o -o app -lc 现代单命令流程:
g++ main.cpp utils.cpp -o app 后者显著简化操作,尤其在大规模项目中优势明显。
总结
从早期手动分步到现代单命令编译的演进,体现了工具链对开发者体验的重视。这种简化不仅提升了效率,还推动了C++生态的发展(如依赖管理、包管理器、自动化构建系统),使复杂项目的维护更加可行。
例子保证可以在 gcc 版本 11.3.0 (GCC) 下正常运行。
__EOF__

本文链接:https://www.cnblogs.com/PHarr/p/18725608.html
关于博主:前OIer,SMUer
版权声明:CC BY-NC 4.0
声援博主:如果这篇文章对您有帮助,不妨给我点个赞
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库