C++底层的编译逻辑和过程
Aurelia_Veil
·
2025-04-23 23:23:32
·
科技·工程
C++ 底层的编译逻辑和过程:
你是否好奇 C++ 是怎么编译的咩,你是否会觉得一个文本就能让代码变成一个个指令运行起来的,这篇文章告诉你 C++ 的底层编译逻辑和过程。
一、预处理
在我们的编译开始前,预处理器会把代码当做文本进行处理,解析所有需要预处理的指令,这个指令就是以 # 开头的代码,然后生成一个中间代码文件(后缀名为 .i 或者 .ii)。
1. 宏展开
在如下代码中:
#define int long long
#define maxl(a,b) a>b?a:b
这些指令就会替换与前向匹配的指定文本为后项的替换文本,但可能会出现如重复计算的副作用,但是在后面的编译器中会继续优化。
2. 头文件
在如下代码中:
#include
预处理器会搜索标准库路径,将 iostream 的内容替换并复制到这里。
3. 条件指令
在如下代码中:
# ifdef DEBUG
printf("debug\n");
# endif
如果没有定义 DEBUG 则这段代码会被移出,就消失不见了。
二、编译
注:此部分因过于专业,本人也收集了来自网络的知识,所以会出现专业词汇。
在经过预处理器的代码会进入编译器前段,通过词法、语法、语义分析生成中间表示,然后转换成目标平台的汇编代码(后缀名为 .s)。
1. 词法分析
编译器将字符流(例如 int x=114514;)拆分成各种词素,包括关键字、标识符、运算符、等等。例如:
[Keyword:int] [Identifier:x] [Operator:=] [Literal:114514] [Separator:;]
2. 语法分析
根据 C++ 的语法规则,将各种词素组织成抽象语法树。例如:int x=114514; 会被解析成一个包含类型、变量名和初始值变量声明节点。
3. 语义分析
检查抽象语法树语义的正确性,例如类型的匹配、作用域的规则。对于 C++ 这特有的特性(例如模版、重载),会进行名称修饰,将函数的签名编码作为唯一符号,例如:
void foo(int)
void foo(double)
会变成:
_Z3fooi
_Z3food
4. 中间代码的生成与优化
编译器将抽象语法树转换为 LLVM IR 或 GIMPLE(GCC 的中间表示),并进行优化。例如消除冗余计算、循环优化等,就比如如下代码:
int sum=0;
for(int i=1;i<=100;i++){
sum+=i;
}
会被优化替换成:
int sum=5050;
5. 目标代码生成
根据目标平台的指令集架构,将 IR 转换为汇编代码。例如(asm 语言):
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. 符号表
记录全局变量、函数的地址和类型,例如:
_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. 生成可执行文件格式
链接器根据系统格式(如 ELF、PE)生成最终文件,包含代码段(后缀名为 .text)、数据段(后缀名为 .data)、BSS 段和调试信息。
五、C++ 编译的特殊性
1. 模版实例化
模板代码在编译过程中会生成多个实例(如 vector
2. 异常的处理
try 和 catch 语句会生成额外的异常表,记录代码位置与处理函数之间的映射关系。
六、专业名词解析
注:本部分结合了一部分网络收集内容。
1. 词素(Tokens)
词素是在编译过程中词法的分析阶段,它会将源代码字符流分解成的最小的有意义的单元。它们是构建抽象语法树的基础。
(1) 词素的分类
词素的类型
示例
说明
关键字
int,for
语言预定义的保留字,具有固定的含义
标识符
main,ans,cnt
用户自己定义的名称,但要符合命名规则
字面量
114514,"abcdefg",3.1415926,false
直接表示的常量值(不一定是整型)
运算符
+,>,!=,new
由单字符或多字符组合,表示运算和操作(算术、逻辑、内存操作)
分隔符
(,[,;,{
标记代码块、参数列表或语句结束
预处理指令
#include,#define
仅在预处理阶段处理的指令
注释
//,/* */
通常被词法分析器忽略的语句,不生成有效词素
(2) 词素的生成过程
词素由语法分析器生成,核心步骤如下:
输入(字符流):
源代码被读取为字符序列。
正则表达式匹配:
通过预定义的正则表达式规则识别词素类型:
关键字:\b(int|return|if|else)\b。
标识符:[a-zA-Z_][a-zA-Z0-9_]*。
整数字面量:\d+。
运算符:(\+|\-|\*|/|=|==|&&)。
有限自动机处理:
词法分析器实现为有限状态机,逐字符处理输入:
状态转移:根据当前字符切换状态(如从“初始状态” 进入“数字识别状态”)。
最长匹配原则:优先匹配更长的有效的词素(如 ++ 视为单个运算符,而非两个 +)。
生成词素流:
输出格式化的词素序列,每个词素包含:
类型:表示词素类型。
值:原始字符串内容。
位置(行、列):用于错误提示。
示例:int main() { return 0; } 生成的词素流:
[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
C/C++ 类型修饰符:const 在变量声明和函数参数中有着不同的作用。
2. 抽象语法树(Abstract Syntax Tree)
抽象语法树是编译器和解释器之中的核心数据结构,它将源代码的结构呈树状形式抽象表示,是高级语言与机器代码之间的过渡。
(1) 抽象语法树于解析树的区别
解析树:完全保留了所有语法的细节(分号、括号等),严格按照对应语法规则推导过程。
抽象语法树:仅仅保留程序逻辑结构的必要元素,会过滤掉冗余语法符号部分。
举个例子,在表达式 1*((1+1-4)/5) 中,解析树会包含括号结点,而抽象语法树是直接以树的形式体现运算优先级。
(2) 抽象语法树如何生成
我们以 int main(){return 2*3+4;} 为例。
词法分析:
[Keyword:int] [Identifier:main] [LParen] [RParen] [LBrace]
[Keyword:return] [Literal:2] [Operator:*] [Literal:3]
[Operator:+] [Literal:4] [Semicolon] [RBrace]
语法分析:
递归下降解析器根据 C++ 语法规则构建解析树。
关键语法规则:
FunctionDecl → Type Identifier Params CompoundStmt
CompoundStmt → '{' Stmt* '}'
Stmt → ReturnStmt Expr ';'
Expr → Expr '+' Expr | Expr '*' Expr | Literal
抽象语法树转换:
简化后如下:
FunctionDecl
├─ Type: int
├─ Name: main
└─ CompoundStmt
└─ ReturnStmt
└─ BinaryOperator(op=+)
├─ BinaryOperator(op=*)
│ ├─ IntegerLiteral(2)
│ └─ IntegerLiteral(3)
└─ IntegerLiteral(4)
(3) 抽象语法树的优化
常量直接赋值:
提前计算所有常量表达式。
如:2*3+4 会直接计算赋值为 10。
优化前:
ReturnStmt
└─ BinaryOperator(+)
├─ BinaryOperator(*)
│ ├─ 2
│ └─ 3
└─ 4
优化后:
ReturnStmt
└─ IntegerLiteral(10)
消除无作用代码:
删除无作用的分支(if(false){})等无效结构。
到这里,此文章就结束了,感谢观看到最后的朋友们咩,可以给东东羊一个赞,给个关注吗,谢谢咩!