先想象一个场景:你写了一段 C 代码(比如printf("Hello World!")
),
电脑要怎么「理解」并运行它?
这就需要了解程序的两个关键阶段:翻译环境和运行环境。
作用:
1.把人类能看懂的 C 代码,翻译成计算机能执行的二进制指令(.exe
或.out
等可执行文件)。
就像是美钞只能在美国商店内买东西一样。
类比:
类似「翻译官」把中文说明书翻译成英文,计算机只认识二进制的「机器语言」,翻译环境就是那个「翻译官」。
作用:真正运行可执行文件的环境,负责加载程序、分配内存、执行指令等。
类比:翻译好的英文说明书要交给工人使用,运行环境就是那个「工厂车间」。
任务:
1.处理#include
头文件:把stdio.h
的内容「粘贴」到代码里(类似盖房子前先搬来砖块)
2.处理#define
宏定义:把代码中的MAX=100
替换成100
(像把图纸上的「房间 A」换成具体尺寸)。
3.删除注释:注释是给人看的,计算机不需要,直接删掉(类似擦掉图纸上的备注)。
输入文件:.c
源文件(如main.c
)
输出文件:预处理后的.i
文件(内容变得超级长,都是展开后的代码)。
如果此时你用gcc编译器打开main.i,你就会发现你是能看懂代码的,
其内操作只是将#include所用的库全部搬过来了,并且将#define定义的宏全部放到所用地
例子
预处理前:
#include "stdio.h"
#define ADD(a,b) a+b
int main() {
// 计算1+2
printf("%d", ADD(1,2));
return 0;
}
预处理后(简化版):
[一堆代码,是库内用来定义stdio.h所用的*************用********************进行省略***********************************************
****************************************************************
***********************************************
********************************************************
***************************************************************************
******************
***************************************************************************************************************************************************************************************************************************************************************************************************
*************************************************************************************************************************
***************************************************************************************************************
*****************************************************************************************************************
*************************************************************************************************************************************************************************
**********************
****************************************************************************************************************************************************************************************************************************************************************************************************]
int main() {
printf("%d", 1+2); // 注释被删除,宏被替换
return 0;
}
任务:
将预处理后的 C 代码翻译成汇编语言(人类勉强能看懂的低级语言,类似「施工图」)。
检查语法错误:如果代码写错别字(如prinft
),这一步会报错(类似图纸审核发现尺寸错误)。
优化代码结构:让程序运行更高效(如合并重复指令)。
输入文件:.i
文件
输出文件:汇编.s
文件(如main.s
)。
例子
预处理后的 C 代码 → 编译 → 汇编代码(类似中文说明书 → 翻译成英文简单句):
; 汇编代码片段(简化)
mov edi, offset s ; 加载"Hello World!"的地址
call printf ; 调用printf函数
任务:
将汇编语言翻译成机器语言(二进制指令),生成目标文件(.o
或.obj
)。
每一条汇编指令对应一条或多条机器指令(类似把施工图拆解成螺丝、钉子等零件)。
目标文件是二进制文件,但还不能直接运行,
因为可能缺少某些「零件」(如printf
函数的实现)
输入文件:.s
汇编文件
输出文件:目标.o
文件(如main.o
)。
例子:
汇编代码 .s
→ 汇编 → 二进制 .o
(类似英文简单句 → 拆成单词字母组合):
二进制片段(示例):
11010101 00101010 11100011 ...(计算机能识别的0和1)
任务:
把多个目标文件(.o
)和库文件(如printf
所在的 C 标准库)合并成可执行文件(.exe
或.out
)。
解决「符号引用」:比如代码中调用了printf
,需要找到它在库中的具体地址(类似盖房子时发现缺少窗户,去建材市场买窗户装上)
处理多个文件关联:如果有main.c
和tool.c
,需要把它们的目标文件合并(类似把不同房间的零件组装成完整房子)
例子
链接过程:
main.o中调用printf的地方 ↓
---------------------------
| 符号未定义:需要找到printf的地址 |
---------------------------
↓ 去标准库中找到printf的二进制代码,填入地址
假设你写了一句printf("Hello")
,C 语言本身没有实现printf
,它存放在C 标准库中。
预编译时,#include
只是声明了printf
的存在(告诉编译器「这里有个函数叫 printf」)。
链接时,编译器才会去标准库中找到printf
的实际二进制代码,「粘」到可执行文件里(类似盖房子时需要买现成的门窗装上去)。
库文件就相当于别人已经将该函数方法写好了,并以文件的方式存储在 .h的文件中,比如stdio.h
这就是一个头文件,你用函数的时候所使用的方法就是从里面进行调用。
步骤 | 输入文件 | 输出文件 | 核心作用 | 类比 |
---|---|---|---|---|
预编译 | .c |
.i |
处理宏、头文件、注释 | 搬砖 + 拆备注 |
编译 | .i |
.s |
翻译成汇编语言,查语法错误 | 画施工图 |
汇编 | .s |
.o |
翻译成二进制目标文件 | 生产零件 |
链接 | .o + 库文件 |
可执行文件 | 合并文件,解决符号引用 | 组装零件成房子 |
脑图记忆法(自己动手画更印象深刻!)
翻译环境四步曲
├─ 预编译(杂活小工):宏展开、头文件粘贴、删注释
├─ 编译(翻译官):C代码→汇编代码,查语法
├─ 汇编(零件工厂):汇编→二进制目标文件
└─ 链接(组装师傅):目标文件+库→可执行文件
目标文件的三种类型:
可重定位文件(.o
):可以和其他文件链接(类似可组装的零件)。
可执行文件(.exe
):可以直接运行(组装好的完整房子)。
共享库文件(.so
或.dll
):类似「通用零件库」,多个程序可共用。
链接的两种方式:
静态链接:把库代码直接复制到可执行文件中(房子自带所有零件,体积大)。
动态链接:运行时再去加载库(房子需要时去零件库借,体积小,更灵活)。