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.iMyMath.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)通过循环累加从ab(包含两端)的所有整数,并返回总和。其逻辑等价于以下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++开发中,编译过程确实需要手动分步执行预处理、编译、汇编、链接四个阶段。开发者需逐步调用不同工具完成每个步骤:

  1. 预处理
    使用预处理器(如 cpp)处理宏、头文件包含等,生成预处理后的源码(.i 文件):

    cpp main.cpp > main.i
    
    
  2. 编译
    将预处理后的代码转换为汇编代码(.s 文件),例如使用 cc1plus(GCC的C++编译器前端):

    cc1plus main.i -o main.s
    
    
  3. 汇编
    将汇编代码转换为目标文件(.o 文件),使用汇编器(如 as):

    as main.s -o main.o
    
    
  4. 链接
    将目标文件与库链接生成可执行文件,使用链接器(如 ld):

    ld main.o -o main -lc
    
    

这一过程繁琐且容易出错,尤其是在处理多文件项目时。


一条命令完成编译的时代

随着编译器前端(如 g++clang++)的成熟,开发者可以通过单条命令直接完成所有步骤。以GCC为例:

g++ main.cpp -o main

编译器自动按顺序处理预处理、编译、汇编、链接,无需手动干预。

这一改进从编译器工具链的早期版本就已支持(如GCC在1987年首次发布时已集成),但实际普及依赖于编译器的易用性优化和开发者习惯的转变。现代编译器(如GCC 3.x+、Clang)进一步简化了流程,并支持优化选项(如 -O2)和调试信息(如 -g)的直接集成。


简化编译步骤的意义

  1. 提升开发效率
    减少手动输入多步骤命令的时间,尤其适合快速迭代和调试。

  2. 降低错误风险
    自动处理依赖关系和步骤顺序,避免遗漏或误操作(如忘记链接库)。

  3. 统一构建流程
    便于与构建工具(如 MakeCMake)和IDE集成,实现跨平台一致性。

  4. 优化编译过程
    编译器可以全局优化代码(如内联展开、跨文件优化),而分步操作可能限制优化空间。

  5. 降低学习门槛
    新手无需深入理解底层工具链即可快速编译程序。


实际案例对比

假设有一个多文件项目:main.cpputils.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__

本文作者PHarr
本文链接https://www.cnblogs.com/PHarr/p/18725608.html
关于博主:前OIer,SMUer
版权声明CC BY-NC 4.0
声援博主:如果这篇文章对您有帮助,不妨给我点个赞
posted @   PHarr  阅读(5)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示