C++底层的编译逻辑和过程
Aurelia_Veil · · 科技·工程
C++ 底层的编译逻辑和过程:
你是否好奇 C++ 是怎么编译的咩,你是否会觉得一个文本就能让代码变成一个个指令运行起来的,这篇文章告诉你 C++ 的底层编译逻辑和过程。
一、预处理
在我们的编译开始前,预处理器会把代码当做文本进行处理,解析所有需要预处理的指令,这个指令就是以 #
开头的代码,然后生成一个中间代码文件(后缀名为 .i
或者 .ii
)。
1. 宏展开
在如下代码中:
#define int long long
#define maxl(a,b) a>b?a:b
这些指令就会替换与前向匹配的指定文本为后项的替换文本,但可能会出现如重复计算的副作用,但是在后面的编译器中会继续优化。
2. 头文件
在如下代码中:
#include<iostream>
预处理器会搜索标准库路径,将 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<int>
和 vector<string>
),可能会导致代码膨胀,但现代编译器会重复合并进行优化。
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<pair<int,int>>g
中的>>
应识别的是两个>
而不是右移运算符 -
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){}
)等无效结构。
到这里,此文章就结束了,感谢观看到最后的朋友们咩,可以给东东羊一个赞吗,谢谢咩!
注:可催更。