📄 一句话概括
编译出来的是目标文件,还缺一步链接——把【我需要谁】和【我提供了谁】配起来;未定义符号就是这么报出来的。
📝 本节和前面几节的关系
8.3 讲完了翻译,这一节说明【翻译完还没结束】。
一、目标文件还是半成品
编译一个源文件,得到的是目标文件,不是可执行文件。 原因很实在:一个源文件常常调用了别的文件里的函数。 编译这个文件的时候,编译器还没看到那些函数,自然不知道它们的地址。 于是它只能先留个空位、记下名字——等后面有人来补。
二、两张表:定义了谁、还要谁
目标文件里因此带着两张表:
它定义了谁——自己提供了哪些函数。
它还需要谁——用到了哪些别人提供的函数。
举例:math.o 定义了 add、不需要别人;main.o 定义了 main、但需要 add 和 printf。
注意一个细节:需要 add、需要 printf,这两件事性质不同——前者在别的目标文件里,后者在标准库里。
三、链接器在配对
链接器的工作就是逐条去比对:把每个【还需要谁】找到出处,接上。 找得到就接上;找不到就报错——这就是【未定义符号】的来历。 所以那类报错的意思很直白:你用了某个名字,可谁都没提供它。 常见原因有三种:名字拼错了、忘了把那个目标文件加进来、忘了指定要链接哪个库。
四、完整链条
到这里整条链就齐了: 源码 → 编译 → 目标文件 → 链接 → 可执行文件。 两头都是文件,中间那一步最容易被忽略,也最容易出问题。 记住一句话:编译管【一个文件翻译得对不对】,链接管【这些文件能不能拼到一起】。
五、动手 Demo:仅靠编译无法得到可执行文件
编译出来的是目标文件,还是半成品。 它带着两张表:自己定义了谁、还要谁。链接器负责把这两张表配上。
objs = {"math.o": {"def": {"add"}, "use": set()},
"main.o": {"def": {"main"}, "use": {"add", "printf"}}}
defined = set()
for o in objs.values():
defined |= o["def"]
libs = {"printf"}
for nm in ("add", "printf"):
print(nm, "在目标文件里" if nm in defined else "在库里")
实跑输出:
--------------------------------------------------------------------
仅靠编译无法得到可执行文件
--------------------------------------------------------------------
编译出来的**不是可执行文件**,而是**目标文件**——**它还是半成品。**
因为一个源文件常常**调用了别的文件里的函数**,那些地址编译时还不知道。
所以目标文件里要带两张表:**自己定义了谁、还要谁。**
目标文件 它定义了 它需要别人提供
-----------------------------------------
math.o add (无)
main.o main add、printf
链接器就是来配对这些表:**逐条去找【还需要谁】里的名字,找到就接上。**
需要的名字 在目标文件里找到 在库里找到 结果
----------------------------------------------------------
add 是 否 接上了
printf 否 是 接上了
**这正是【未定义符号】这类链接错误的来历:**
你用了某个名字,可**谁都没提供它**——编译能过,链接会失败。
所以完整链条是:**源码 → 编译 → 目标文件 → 链接 → 可执行文件。** 少一步都不成。
add 在别的目标文件里、printf 在标准库里,两个都接上了。 如果谁都没提供这个名字,链接器就报【未定义符号】。
30 秒实验
- 写两个小程序,一个调用另一个里的函数,先只编译不链接,看报什么错。
- 想想:为什么把函数声明写成另一个名字,编译能过而链接才报错。
常见误区
- 以为编译能过的程序就一定能跑。链接失败同样出不了可执行文件。
- 以为链接只是【把文件拼起来】。它还要修正每个符号的实际地址。
自测三问(点开对答案)
1. 编译一个源文件得到的是什么? —— 目标文件——还带着未解决的外部符号,不是可执行文件。 2. 目标文件里的两张表是什么? —— 自己定义了哪些符号;还需要别人提供哪些符号。 3. 【未定义符号】错误是怎么来的? —— 链接器在【还需要谁】里发现某个名字谁都没有提供。- ☐ 下一节(8.5 启动代码与库文件)
相关笔记
- 编译器负责翻译源代码 — 链条的前半段
- 启动代码与库文件 — 链接的另一半故事
- DLL文件与导入库 — 库的两种接法
- ⌨️ 编程原理 · 索引 — 把编程全貌理一遍
- ⚙️ 程序是怎样跑起来的 · 索引