目录
- 什么是链接?
- 链接器的工作流程
- 链接的三个阶段
- 编译
- 汇编
- 链接
- 链接解决的核心问题
- 符号解析
- 地址重定位
- 链接的类型
- 静态链接
- 动态链接
- 与链接相关的常见问题
undefined reference to ...multiple definition of ...
- 多文件项目管理
什么是链接?
链接 是一个将一个或多个目标文件(由编译器生成)和库文件(静态库或动态库)组合成一个单一的可执行文件(如 Windows 下的 .exe 或 Linux 下的 a.out)的过程。

你可以把它想象成一个大型项目的总装车间:
- 编译器 是各个生产车间,负责将源代码文件(
.c文件)加工成半成品——目标文件(.o或.obj文件)。 - 链接器 就是总装车间,它接收所有半成品和标准件(库文件),将它们按照“图纸”(符号表和重定位信息)组装成一个完整、可以运行的产品(可执行文件)。
链接的核心目的:
- 合并代码和数据: 将多个分散的目标文件合并成一个整体。
- 解决引用: 找到一个文件中定义的函数或变量,并连接到另一个文件中使用它的地方。
- 分配最终地址: 为程序中的所有代码和数据分配在内存中的最终运行地址。
链接器的工作流程
链接器主要做两件事:
- 符号解析: 链接器会遍历所有的目标文件,收集所有的“符号”(Symbol),符号可以是函数名、全局变量名等,链接器会确保每个符号要么在某个目标文件中被定义,要么从一个库文件中被找到,如果一个符号被引用但从未被定义,链接器就会报错(
undefined reference)。 - 地址重定位: 每个目标文件在被独立编译时,都认为自己是程序的起点,所以它的代码和数据都从地址 0 开始,链接器需要将这些文件“拼接”起来,并为它们分配最终的、不冲突的内存地址,这个过程就是重定位,链接器会修改目标文件中的地址引用,使其指向正确的最终地址。
链接的三个阶段
我们从一个 .c 文件到一个可执行文件,会经历三个主要阶段(以 GCC 为例):

编译
gcc -c hello.c -o hello.o
- 输入:
hello.c源文件。 - 过程: 编译器进行词法分析、语法分析、语义分析,然后生成汇编代码(
.s文件,此步骤由-c跳过)。 - 输出: 目标文件
hello.o。 - 关键信息: 目标文件包含了编译后的机器码、数据(如全局变量)、以及一个符号表,符号表记录了该文件中定义的符号和引用的符号。
hello.c 示例:
// hello.c
#include <stdio.h>
// 这是一个定义的符号
void my_function();
int main() {
printf("Hello, World!\n"); // printf 是一个引用的符号
my_function(); // my_function 是一个引用的符号
return 0;
}
编译后,hello.o 的符号表大致如下:
- 定义的符号:
main - 引用的符号:
printf,my_function
汇编
gcc -S hello.c -o hello.s

- 输入:
hello.c源文件。 - 过程: 编译器只生成汇编代码。
- 输出: 汇编文件
hello.s。 - 关键信息: 这是连接 C 语言和机器码的桥梁,但通常我们直接使用
-c跳过此步直接生成目标文件。
链接
gcc hello.o -o hello
- 输入: 一个或多个目标文件(
hello.o)。 - 过程: 链接器被调用。
- 符号解析: 链接器在
hello.o中看到它引用了printf和my_function,它会去标准库(如libc.so或libc.a)中查找printf的定义并找到它,但它找不到my_function的定义,于是报错。 - 地址重定位: 假设所有符号都找到了,链接器会为
main函数、printf函数等分配在最终可执行文件中的虚拟地址,并更新hello.o中对这些地址的引用。
- 符号解析: 链接器在
- 输出: 可执行文件
hello。
链接解决的核心问题
符号解析
符号是链接的基石,每个符号都有一个名字和一个类型(函数、变量等),链接器通过名字来匹配“定义”和“引用”。
- 强符号: 函数和已初始化的全局变量。
- 弱符号: 未初始化的全局变量。
规则:
- 多个强符号同名: 错误,链接器会报
multiple definition。 - 一个强符号和多个弱符号同名: 使用强符号。
- 多个弱符号同名: 使用其中任意一个(具体取决于链接器实现,通常是合并或取第一个)。
地址重定位
想象一下,你把两个乐高模型(file1.o 和 file2.o)拼在一起,每个模型都有自己的内部坐标系统(从 0 开始),当你把它们合并成一个大的模型时,你必须调整它们的位置,确保它们不会重叠,并且所有连接点都对得上。
链接器做的就是这个工作,它为整个程序分配一个虚拟地址空间,然后把每个目标文件“放”进去,并更新所有内部地址引用。
链接的类型
静态链接
- 过程: 在链接阶段,链接器会将目标文件所依赖的库代码(如
printf的机器码)完整地复制到最终的可执行文件中。 - 优点:
- 简单: 生成的可执行文件是独立的,不依赖外部库文件,可以直接在任何相同操作系统的机器上运行(只要架构相同)。
- 性能: 程序加载时,所有代码都在内存中,没有额外的动态查找开销。
- 缺点:
- 体积大: 如果多个程序都使用了同一个库,每个可执行文件里都有一份库的副本,浪费磁盘空间。
- 更新麻烦: 如果库发现了一个安全漏洞,你需要重新编译所有依赖它的程序。
静态库文件: 通常以 .a (archive) 在 Linux/Unix 中常见。
动态链接
- 过程: 在链接阶段,链接器不复制库代码,而是在可执行文件中留下一个“桩”(stub),记录了需要哪个库(如
libc.so.6)以及库中哪个函数(printf),程序在运行时,操作系统加载器会找到这些动态库(.so或.dll文件),并将它们加载到内存中,然后将可执行文件中的“桩”与库中真正的函数地址连接起来。 - 优点:
- 体积小: 可执行文件只包含必要的代码,库代码在共享。
- 易于更新: 只需更新系统上的库文件,所有使用该库的程序都能获得新版本,无需重新编译。
- 节省内存: 如果多个正在运行的程序使用同一个库,库的代码在物理内存中只有一份。
- 缺点:
- 依赖性: 程序运行时必须保证所需的动态库存在于系统中。
- 性能开销: 程序启动时需要动态解析符号,可能会有轻微的性能损失。
动态库文件: Linux 下是 .so (Shared Object),Windows 下是 .dll (Dynamic Link Library)。
与链接相关的常见问题
undefined reference to ... (未定义的引用)
- 原因: 你使用了某个函数或变量,但链接器在所有的目标文件和库文件中都没有找到它的定义。
- 解决方案:
- 拼写错误: 最常见的原因,检查函数名是否拼写正确。
- 忘记实现: 你只声明了函数(如在头文件中),但没有在
.c文件中实现它。 - 忘记包含库: 如果你使用了数学库的
sin()函数,需要告诉链接器去链接它。gcc my_program.o -lm -o my_program(-lm表示链接数学库)。 - 文件未编译: 包含函数定义的
.c文件没有被编译成.o文件,或者没有被传递给链接器。
multiple definition of ... (多重定义)
- 原因: 你在多个
.c文件中定义了同名的全局变量或函数(没有使用static关键字),链接器不知道该用哪一个,所以报错。 - 解决方案:
- 使用
extern声明: 在一个头文件(如my_header.h)中使用extern int my_global;进行声明。 - 在单个
.c文件中定义: 只在一个.c文件(如my_data.c)中进行实际定义int my_global;。 - 包含头文件: 在需要使用这个变量的所有
.c文件中#include "my_header.h"。 - 使用
static: 如果一个变量或函数只在一个文件内部使用,使用static关键字将其作用域限制在该文件内,这样它就不会成为全局符号,链接器也看不到它。
- 使用
多文件项目管理
链接是管理多文件项目的基石。
项目结构:
my_project/
├── main.c
├── utils.c
└── utils.h
utils.h (头文件 - 声明)
// 声明函数,供其他文件使用 int add(int a, int b); void print_message();
utils.c (实现文件 - 定义)
#include <stdio.h>
#include "utils.h" // 包含自己的头文件是好习惯
int add(int a, int b) {
return a + b;
}
void print_message() {
printf("This is a message from utils.c\n");
}
main.c (主程序文件)
#include "utils.h" // 包含 utils.h 以使用 add 和 print_message 的声明
int main() {
int result = add(5, 3);
printf("The result is: %d\n", result);
print_message();
return 0;
}
编译和链接步骤:
- 分别编译: 将
.c文件编译成.o文件。gcc -c main.c -o main.o gcc -c utils.c -o utils.o
- 链接: 将
.o文件链接在一起。gcc main.o utils.o -o my_program
my_program 就是一个可以独立运行的可执行文件。
| 特性 | 描述 |
|---|---|
| 核心作用 | 将多个目标文件和库合并成一个可执行文件。 |
| 主要任务 | 符号解析(匹配定义和引用)和 地址重定位(分配最终内存地址)。 |
| 静态链接 | 将库代码复制到可执行文件中,优点是独立,缺点是体积大。 |
| 动态链接 | 在运行时加载库文件,优点是体积小、易于更新,缺点是有依赖性。 |
| 常见错误 | undefined reference (符号未定义) 和 multiple definition (符号重复定义)。 |
| 重要性 | 理解链接是掌握 C 语言、操作系统、程序性能优化的基础。 |
深入理解链接,能帮助你写出更健壮、更高效的代码,并能轻松地调试与编译、链接相关的疑难杂症。
