专栏文章

C++底层的编译逻辑和过程

科技·工程参与者 40已保存评论 47

文章操作

快速查看文章及其快照的属性,并进行相关操作。

当前评论
47 条
当前快照
1 份
快照标识符
@mipindb3
此快照首次捕获于
2025/12/03 12:37
3 个月前
此快照最后确认于
2025/12/03 12:37
3 个月前
查看原文

C++ 底层的编译逻辑和过程:

你是否好奇 C++ 是怎么编译的咩,你是否会觉得一个文本就能让代码变成一个个指令运行起来的,这篇文章告诉你 C++ 的底层编译逻辑和过程。

一、预处理

在我们的编译开始前,预处理器会把代码当做文本进行处理,解析所有需要预处理的指令,这个指令就是以 # 开头的代码,然后生成一个中间代码文件(后缀名为 .i 或者 .ii)。

1. 宏展开

在如下代码中:
CPP
#define int long long
#define maxl(a,b) a>b?a:b
这些指令就会替换与前向匹配的指定文本为后项的替换文本,但可能会出现如重复计算的副作用,但是在后面的编译器中会继续优化。

2. 头文件

在如下代码中:
CPP
#include<iostream>
预处理器会搜索标准库路径,将 iostream 的内容替换并复制到这里。

3. 条件指令

在如下代码中:
CPP
# ifdef DEBUG
printf("debug\n");
# endif
如果没有定义 DEBUG 则这段代码会被移出,就消失不见了。

二、编译

注:此部分因过于专业,本人也收集了来自网络的知识,所以会出现专业词汇。
在经过预处理器的代码会进入编译器前段,通过词法、语法、语义分析生成中间表示,然后转换成目标平台的汇编代码(后缀名为 .s)。

1. 词法分析

编译器将字符流(例如 int x=114514;)拆分成各种词素,包括关键字、标识符、运算符、等等。例如:
CPP
[Keyword:int] [Identifier:x] [Operator:=] [Literal:114514] [Separator:;]

2. 语法分析

根据 C++ 的语法规则,将各种词素组织成抽象语法树。例如:int x=114514; 会被解析成一个包含类型、变量名和初始值变量声明节点。

3. 语义分析

检查抽象语法树语义的正确性,例如类型的匹配、作用域的规则。对于 C++ 这特有的特性(例如模版、重载),会进行名称修饰,将函数的签名编码作为唯一符号,例如:
CPP
void foo(int) 
void foo(double) 
会变成:
CPP
_Z3fooi
_Z3food

4. 中间代码的生成与优化

编译器将抽象语法树转换为 LLVM IR 或 GIMPLE(GCC 的中间表示),并进行优化。例如消除冗余计算、循环优化等,就比如如下代码:
CPP
int sum=0;
for(int i=1;i<=100;i++){
    sum+=i;
}
会被优化替换成:
CPP
int sum=5050;

5. 目标代码生成

根据目标平台的指令集架构,将 IR 转换为汇编代码。例如(asm 语言):
CPP
mov eax,0      ;sum=0
mov ecx,0      ;i=0
loop:
inc ecx        ;i++
add eax,ecx    ;sum+=i
cmp ecx,100
jl loop

三、汇编,生成机器指令

汇编器会将汇编代码转换成目标文件(后缀名为 .o.obj),包括机器码和符号表。

1. 二进制编码:

汇编语言被转换成操作码,例如,add eax,ecx 对应二进制编码为 0x01C8

2. 符号表

记录全局变量、函数的地址和类型,例如:
CPP
_main: address 0x100, type TEXT
_sum: address 0x200, type DATA

3. 重定位信息

标记需要链接阶段处理的地址。比如 main 函数调用了外部函数 check,则 call check 的地址会被标记为待填充。

四、链接

链接器(如 ld)会将多个目标文件和库合并成最终的可执行文件或动态库。

1. 符号解析

链接器检查所有的目标文件,确保每个符号(如函数、全局变量)均有定义。

2. 地址分配和重定位

给所有符号分配出最终的内存地址,并修改代码中的引用部分。例如,若 main 函数被分配到地址 0x400000,则所有调用 main 的地方都会更新为这一个地址。

3. 静态库和动态库的处理

  • 静态链接:将库代码直接复制转移到可执行文件中,生成独立的二进制文件。
  • 动态链接:有且仅有在运行时加载共享库(如后缀名为 .so 或者 .dll),通过 PLT 来延迟绑定。

4. 生成可执行文件格式

链接器根据系统格式(如 ELFPE)生成最终文件,包含代码段(后缀名为 .text)、数据段(后缀名为 .data)、BSS 段和调试信息。

五、C++ 编译的特殊性

1. 模版实例化

模板代码在编译过程中会生成多个实例(如 vector<int>vector<string>),可能会导致代码膨胀,但现代编译器会重复合并进行优化。

2. 异常的处理

trycatch 语句会生成额外的异常表,记录代码位置与处理函数之间的映射关系。

六、专业名词解析

注:本部分结合了一部分网络收集内容。

1. 词素(Tokens)

词素是在编译过程中词法的分析阶段,它会将源代码字符流分解成的最小的有意义的单元。它们是构建抽象语法树的基础。

(1) 词素的分类

词素的类型示例说明
关键字intfor语言预定义的保留字,具有固定的含义
标识符mainanscnt用户自己定义的名称,但要符合命名规则
字面量114514"abcdefg"3.1415926false直接表示的常量值(不一定是整型)
运算符+>!=new由单字符或多字符组合,表示运算和操作(算术、逻辑、内存操作)
分隔符([;{标记代码块、参数列表或语句结束
预处理指令#include#define仅在预处理阶段处理的指令
注释///* */通常被词法分析器忽略的语句,不生成有效词素

(2) 词素的生成过程

词素由语法分析器生成,核心步骤如下:
输入(字符流):
源代码被读取为字符序列。
正则表达式匹配:
通过预定义的正则表达式规则识别词素类型:
  • 关键字:\b(int|return|if|else)\b
  • 标识符:[a-zA-Z_][a-zA-Z0-9_]*
  • 整数字面量:\d+
  • 运算符:(\+|\-|\*|/|=|==|&&)
有限自动机处理:
词法分析器实现为有限状态机,逐字符处理输入:
  • 状态转移:根据当前字符切换状态(如从“初始状态” 进入“数字识别状态”)。
  • 最长匹配原则:优先匹配更长的有效的词素(如 ++ 视为单个运算符,而非两个 +)。
生成词素流:
输出格式化的词素序列,每个词素包含:
  • 类型:表示词素类型。
  • 值:原始字符串内容。
  • 位置(行、列):用于错误提示。

示例:int main() { return 0; } 生成的词素流:
PLAINTEXT
[KEYWORD:int] (Line 1, Col 1)
[IDENTIFIER:main] (Line 1, Col 5)
[LPAREN:] (Line 1, Col 9)
[RPAREN:] (Line 1, Col 10)
[LBRACE:] (Line 1, Col 12)
[KEYWORD:return] (Line 1, Col 14)
[LITERAL:0] (Line 1, Col 21)
[SEMICOLON:] (Line 1, Col 22)
[RBRACE:] (Line 1, Col 24)

(3) 词素处理的问题

消除歧义:
  • 最长匹配:如:a+++++b 会被解析为 a ++ ++ + b 而不是 a ++ + ++ b
  • 优先级规则:某些符号需要根据上下文确定类型(一字多义)。
上下文相关词素:
  • C++ 模版:vector<pair<int,int>>g 中的 >> 应识别的是两个 > 而不是右移运算符
  • C/C++ 类型修饰符:const 在变量声明和函数参数中有着不同的作用。

2. 抽象语法树(Abstract Syntax Tree)

抽象语法树是编译器和解释器之中的核心数据结构,它将源代码的结构呈树状形式抽象表示,是高级语言与机器代码之间的过渡。

(1) 抽象语法树于解析树的区别

  • 解析树:完全保留了所有语法的细节(分号、括号等),严格按照对应语法规则推导过程。
  • 抽象语法树:仅仅保留程序逻辑结构的必要元素,会过滤掉冗余语法符号部分。
举个例子,在表达式 1*((1+1-4)/5) 中,解析树会包含括号结点,而抽象语法树是直接以树的形式体现运算优先级。

(2) 抽象语法树如何生成

我们以 int main(){return 2*3+4;} 为例。
词法分析:
PLAINTEXT
[Keyword:int] [Identifier:main] [LParen] [RParen] [LBrace]
[Keyword:return] [Literal:2] [Operator:*] [Literal:3] 
[Operator:+] [Literal:4] [Semicolon] [RBrace]
语法分析:
  • 递归下降解析器根据 C++ 语法规则构建解析树。
  • 关键语法规则:
BNF
FunctionDecl → Type Identifier Params CompoundStmt
CompoundStmt → '{' Stmt* '}'
Stmt → ReturnStmt Expr ';'
Expr → Expr '+' Expr | Expr '*' Expr | Literal
抽象语法树转换:
简化后如下:
CPP
FunctionDecl
├─ Type: int
├─ Name: main
└─ CompoundStmt
   └─ ReturnStmt
      └─ BinaryOperator(op=+)
         ├─ BinaryOperator(op=*)
         │  ├─ IntegerLiteral(2)
         │  └─ IntegerLiteral(3)
         └─ IntegerLiteral(4)

(3) 抽象语法树的优化

常量直接赋值:
提前计算所有常量表达式。
如:2*3+4 会直接计算赋值为 10
优化前:
CPP
ReturnStmt
└─ BinaryOperator(+)
   ├─ BinaryOperator(*) 
   │  ├─ 2
   │  └─ 3
   └─ 4
优化后:
CPP
ReturnStmt
└─ IntegerLiteral(10)
消除无作用代码:
删除无作用的分支(if(false){})等无效结构。

到这里,此文章就结束了,感谢观看到最后的朋友们咩,可以给东东羊一个赞,给个关注吗,谢谢咩!

评论

47 条评论,欢迎与作者交流。

正在加载评论...