欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 维修 > C++编译流程

C++编译流程

2025/10/20 10:54:04 来源:https://blog.csdn.net/zhuxixi1031/article/details/146427367  浏览:    关键词:C++编译流程

编译器其实就是一个翻译器,把我们的文件内容翻译成机器能够看懂的指令,但如何合理翻译是核心。

C语言编译

需要经过以下几步:

  1. 词法分析:扫描代码,确定单词类型,比如是变量还是函数,是标识符还是运算符等等,这样操作后每个token都有自己的性质
  2. 语法分析:根据语法规则构建分析树
  3. 语义分析:主要是检查代码里是否有语义错误,比如函数返回值是否一致/是否访问超出作用域的变量/变量函数是否已经声明等
  4. 生成中间代码
  5. 汇编器生成目标机器语言

在这里插入图片描述

C++编译流程

在这里插入图片描述

预处理

将一些预处理指令的东西进行处理,比如#include、#pragma #define
对于include而言,预处理器会直接打开这个文件,然后将其copy进我们的cpp文件里。 (宏是直接进行字符串替换的)
此时生成的文件是.i文件

编译

通过编译器和汇编器将.cpp文件编译成.o文件,这里的.o其实就是一些01码,为了方便理解,可以先让编译器输出汇编指令(其实本质都是差不多的,做过CPU的应该知道汇编转01码怎么做)
换句话说,如果代码没有语法错误,编译是不应该报错的,比如缺少main函数入口等

// MyClass.cppclass MyClass {
public:int value;void setValue(int v) {value = v;helper();  // 调用自己的成员函数}void helper() {// 空函数,只为了演示调用过程}
};

实际编译后.o:

[ MyClass.o ] ←←←←← 编译器输出
┌──────────────────────────────┐
│          .text 段            │←─ 代码段(真正的机器码存放区)
│                              │
│ 0x0000: MyClass::helper()    │←─ helper 的汇编已生成在此位置
│        push rbp              │
│        mov rsp, rbp          │
│        ...                   │
│        ret                   │
│                              │
│ 0x0040: MyClass::setValue()  │←─ setValue 的汇编在这里
│        mov [this], v         │
│        call helper()         │←─ 这条 call 指令地址还没填
└──────────────────────────────┘┌──────────────────────────────┐
│         符号表(.symtab)     │←─ 存储函数名、地址、段类型等元信息
│                              │
│ _ZN7MyClass6helperEv  → 0x0000helper() 的地址
│ _ZN7MyClass8setValueEi → 0x0040setValue() 的地址
└──────────────────────────────┘

附上如何只编译文件+看汇编结果

 g++ -c -O0 -fno-inline -o main.o main.cppobjdump -d main.o  

需要注意的是,每个.o文件独立,函数留的地址也是相对地址,链接器还需要解决相对地址转绝对地址的问题。

链接

那么问题来了,如果我只有一个cpp文件,那么到编译这一步就可以了。但是在大型工程文件里,我们往往是有多个模块的,多个模块之间彼此还有调用关系,如何能生成正确的指令呢?其实就是依赖链接器,将多个cpp的.o文件给链接在一起。
多个模块之间的调用关系有:

  1. 你去访问别人的变量
  2. 调用别人的成员函数:在汇编指令中这一句话是call xxx,这个xxx就是函数签名
    如果你在一个文件里声明并且定义了函数func,在另一个文件里也定义了func,对于include了这两个文件的文件来说,链接器不知道要链接具体哪一个,因此会报错称重复定义xxx。
    举个具体的例子
    MyClass.h代码:
class MyClass {
public:int value;void setValue(int v);void helper();
};

MyClass.cpp代码:

#include "MyClass.h"
void MyClass::setValue(int v) {value = v;helper();
}
void MyClass::helper() { }

main的cpp代码:

#include "MyClass.h"
int main() {MyClass obj;obj.setValue(42);return 0;
}

main的汇编:

0000000000000000 <main>:0:	55                   	push   %rbp1:	48 89 e5             	mov    %rsp,%rbp4:	48 83 ec 30          	sub    $0x30,%rsp8:	e8 00 00 00 00       	callq  d <main+0xd>d:	48 8d 45 fc          	lea    -0x4(%rbp),%rax11:	ba 2a 00 00 00       	mov    $0x2a,%edx16:	48 89 c1             	mov    %rax,%rcx19:	e8 00 00 00 00       	callq  1e <main+0x1e>1e:	b8 00 00 00 00       	mov    $0x0,%eax23:	48 83 c4 30          	add    $0x30,%rsp27:	5d                   	pop    %rbp28:	c3                   	retq   
0x08构造初始化
0x16设置this指针
0x19调用setValue

编译器在生成这段机器码的时候,先写了一条机器指令,但是不知道setValue的地址,因此在e8后面跟的都是00000000。同时在.o的重定位表里添加记录:

偏移地址:0x19
类型:CALL
目标符号:_ZN7MyClass8setValueEi

myclass.o的汇编

0000000000000000 <_ZN7MyClass8setValueEi>:0:	55                   	push   %rbp1:	48 89 e5             	mov    %rsp,%rbp4:	48 83 ec 20          	sub    $0x20,%rsp8:	48 89 4d 10          	mov    %rcx,0x10(%rbp)c:	89 55 18             	mov    %edx,0x18(%rbp)f:	48 8b 45 10          	mov    0x10(%rbp),%rax13:	8b 55 18             	mov    0x18(%rbp),%edx16:	89 10                	mov    %edx,(%rax)18:	48 8b 4d 10          	mov    0x10(%rbp),%rcx1c:	e8 07 00 00 00       	callq  28 <_ZN7MyClass6helperEv>21:	90                   	nop22:	48 83 c4 20          	add    $0x20,%rsp26:	5d                   	pop    %rbp27:	c3                   	retq   0000000000000028 <_ZN7MyClass6helperEv>:28:	55                   	push   %rbp29:	48 89 e5             	mov    %rsp,%rbp2c:	48 89 4d 10          	mov    %rcx,0x10(%rbp)30:	90                   	nop31:	5d                   	pop    %rbp32:	c3                   	retq   33:	90                   	nop34:	90                   	nop35:	90                   	nop36:	90                   	nop37:	90                   	nop38:	90                   	nop39:	90                   	nop3a:	90                   	nop3b:	90                   	nop3c:	90                   	nop3d:	90                   	nop3e:	90                   	nop3f:	90                   	nop

因此,在链接器处理.o时会读取每个.o文件的重定位表,当看到

main.o:offset 0x19 → CALL → _ZN7MyClass8setValueEi

记录时,就会查找所有.o文件中是否有这个定义的,然后获取它的地址,并且计算当前call指令的位置到这个地址的相对偏移。每个.o文件会有符号表,用来存储每个函数的入口地址,方便补地址

动态链接和静态链接

此前提到的都是静态链接,直接打包进可执行文件中。那什么是动态链接呢?动态链接就是在运行的时候将程序用到的函数、库从外部共享库中加载进来。
优势就是节省空间,只需要加载一份,可以共享。支持更新和修复,但是稳定性和独立性差,性能也不好

生成exe文件

链接需要为exe指明程序的入口位置,这也就是main的作用

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词