「链接」是把程序拼起来的最后一步;搞懂静态库与动态库,你就掌握了 C++ 工程化最核心的一块拼图
核心方法论:第一性原理
「如果你不能把一个概念解释得让大一新生听懂,你就还没有真正理解它。这一章我们不背命令——先从三个最基本的问题推起:可执行文件是怎么拼出来的?库为什么要存在?静态库与动态库,差的究竟是哪一步?把第一性原理想透,ar、ldd、nm 这些工具,不过是水到渠成的细节。」
先回答一个最朴素的问题:什么是库(library)?说人话,库就是"别人写好的、编译好的、可以直接拿来用的代码集合"——它通常由两部分组成:头文件(.h,声明接口:有哪些函数、参数是什么、怎么调用)和编译好的二进制文件(.a 或 .so,装着函数的真实实现)。你的程序靠头文件知道"存在 add 这个函数",真正干活的是那个二进制。第 5 章讲包管理时说过"包"的概念——库就是包的物理形态;而第 3 章讲过的编译四步(预处理、编译、汇编、链接)里,链接(linking)就是把你写好的目标文件与库"拼装"成可执行文件的那一步,也是本章的主角。
为什么 C++ 世界里"库"这么重要?因为 C++ 是编译型语言,复用代码不能像脚本语言那样"把源码拿过来跑",而是要把别人编译好的二进制链接进你的程序。你每天写 #include <iostream>,背后链接的是 C++ 标准库 libstdc++;调用 sqrt() 要加 -lm,链接的是数学库 libm。从第 6 章的 jsoncpp 到第 8 章的 pistache,前面每一章装第三方库,最后都要回答同一个问题:这个库以什么形态进我的程序?答案只有两种——静态库或动态库。这一章就把这件事彻底讲透。
先建立全局视角。一个 C++ 程序从源码到可执行文件要经过两大阶段:编译阶段(g++ -c)把每个 .cpp 变成目标文件(.o)——此时函数调用还是"悬空"的,只记下一个名字,也就是符号(symbol),说人话就是函数和变量在二进制世界里的"门牌号";链接阶段(ld)负责把这些悬空的门牌号一一"对上号"——要么在别的 .o 里找到,要么在库(.a/.so)里找到,最终拼出完整的可执行文件。找不到,就报 undefined reference——含义就是"引用了一个没人实现的东西"。
# 回顾编译四步(第 3 章):-c 表示"只编译不链接",产物是目标文件 .o
g++ -c math.cpp -o math.o
# 完整链接:把 main.o 与 math.o 拼成可执行文件 app
g++ main.o math.o -o app
# 库参与链接的完整姿势(本章主角,后面逐个拆解):
# -I 告诉编译阶段:头文件在 ./include 目录
# -L 告诉链接阶段:库文件在当前目录(.)
# -l 告诉链接阶段:要链接名为 math 的库(自动展开成 libmath.a / libmath.so)
g++ main.cpp -I./include -L. -lmath -o app
第一性原理的第一步:把"链接"想象成拼拼图。目标文件和库就是拼图块,符号就是拼图块上的图案——每块拼图上"声明了但没实现"的缺口(undefined),必须由另一块拼图上的"实现"(defined)填上。本章所有报错,本质上都是"某块拼图上的缺口没人填"。
用一句话记住本质区别:静态库(static library)是"链接时就把代码复制进可执行文件"的库;动态库(shared library)是"链接时只记账、运行时才真正加载"的库。静态库在 Linux/macOS 上以 .a(archive,归档文件)结尾、Windows 上是 .lib;动态库在 Linux 上是 .so(shared object,共享对象)、macOS 是 .dylib、Windows 是 .dll。平台名字不同,道理完全一样。
用第一性原理拆开"静态"和"动态"这两个词:"静态" = 结果在编译期就固定了——链接器把静态库里你用到的函数代码,原样复制进最终的可执行文件,程序拿到任何一台机器上都能独立运行,不依赖那个 .a 还在不在;"动态" = 决定推迟到运行期——链接器只在可执行文件里记下一笔"欠条":需要 libmath.so 里的 add 函数;真正还债的是运行时的动态加载器(ld.so),程序启动时它才按欠条去找 .so、加载进内存。
这一"复制"一"加载"的差别,引出后面一串连锁差异。复制进可执行文件:程序体积大,但启动快(不用再加载别的)、部署简单(一个文件拷走就能跑);坏处是每个进程都带一份拷贝,浪费内存,而且库更新后所有程序都得重新链接。运行期加载:程序体积小,多个进程能共享内存里的同一份 .so;坏处是程序不能独立运行,缺了 .so 就报错——第 4 章柯南断过的案 cannot open shared object file 就是这个。
更新方式差异最大:静态库修了个 bug,所有用了它的程序都必须重新编译链接才能拿到修复;动态库只需把新的 .so 替换上去,只要接口没变,所有依赖它的程序下次启动自动用新版——这正是系统里 libc、libstdc++ 这类基础库都做成动态库的原因。但硬币的另一面是依赖地狱(dependency hell):说人话就是"库的版本互相打架"——程序 A 要 libfoo.so.1,程序 B 要 libfoo.so.2,装 A 的依赖就弄坏 B。第 3 章那个 GLIBCXX_3.4.21 not found,本质也是"运行环境里的 .so 版本比程序编译时用的旧"。
还有一个 GCC 官方文档明确写的规则,很多人栽在这:同一个目录里 .a 和 .so 同时存在时,链接器默认优先选 .so(除非加 -static)。也就是说你以为在链静态库,实际可能链的是同名动态库——想强制静态,用 -static,或者干脆直接给 .a 的完整路径。
| 维度 | 静态库(.a / .lib) | 动态库(.so / .dll) |
|---|---|---|
| 代码进入程序的时机 | 链接时复制进可执行文件 | 运行时才加载进内存 |
| 可执行文件体积 | 大(代码被复制进来) | 小(只有欠条) |
| 启动速度 | 快,无需再加载 | 略慢,启动时要找库 |
| 更新方式 | 改库 → 全部重新链接 | 换 .so 文件即可(接口不变时) |
| 部署 | 单个文件拷走即用 | 必须带上 .so,缺了报错 |
| 典型风险 | 程序臃肿、更新麻烦 | 依赖地狱、版本冲突 |
# 同一份源码,两种链接方式(假设 libmath.a 与 libmath.so 都在当前目录)
g++ main.cpp -L. -lmath -o app_dyn # 默认优先链 .so(GCC 文档规则)
g++ main.cpp -L. -lmath -static -o app_sta # -static 强制静态
# 对比体积:动态版小得多,因为 .so 的代码没有复制进来
ls -lh app_dyn app_sta
# -rwxr-xr-x 16K app_dyn ← 只有"欠条"
# -rwxr-xr-x 1.2M app_sta ← 代码整个复制进来了
# ldd 查看可执行文件运行时依赖哪些动态库(第 4 章柯南的取证工具)
ldd ./app_dyn
# linux-vdso.so.1 (0x00007ffe...)
# libmath.so => ./libmath.so (0x00007f...) ← 我们自己的动态库
# libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f...)
# libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)
# 纯静态链接的可执行文件则干干净净:
ldd ./app_sta
# not a dynamic executable
记不住 .a/.so 的区别时,问自己一个问题:"把程序拷到另一台机器,还带不带这个文件?"静态库的答案是"不用带",动态库的答案是"必须带"。这一问,就把体积、部署、更新三条连锁差异全推出来了。
制作静态库只需要两步:先编译、后打包。第一步用 g++ -c 把每个 .cpp 编译成目标文件 .o(只编译不链接,第 3 章讲过);第二步用 ar(GNU binutils 里的归档工具)把若干 .o 打包成一个 .a 文件。命令是 ar rcs libxxx.a a.o b.o,三个字母各司其职:r(replace)把文件插入归档、同名则替换;c(create)创建归档(否则旧归档存在时会先报一条提示);s(index)为归档生成符号索引——有了索引,链接器才能在库里按符号名快速查找,加了 s 等于同时执行了 ranlib。
命名规则必须死记:静态库文件名必须是 lib + 库名 + .a。库名叫 math,文件就叫 libmath.a——因为链接参数 -lmath 的展开规则就是"去 lib 前缀、去扩展名",这是 GCC 官方文档明确写的约定:链接器按 liblibrary.a / liblibrary.so 的形式去找 -l 指定的库。下面用一个小小的 mathlib 演示全流程,工程结构先看代码块里的注释。
# 示例工程结构
libdemo/
├── include/mathlib.h # 头文件:声明接口
├── src/mathlib.cpp # 实现文件
└── main.cpp # 使用库的程序
// include/mathlib.h —— 头文件只声明,不实现
#ifndef MATHLIB_H
#define MATHLIB_H
int add(int a, int b);
int mul(int a, int b);
#endif
// src/mathlib.cpp —— 真正的实现
#include "mathlib.h"
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
// main.cpp —— 使用方
#include <iostream>
#include "mathlib.h"
int main() {
std::cout << "3 + 5 = " << add(3, 5) << std::endl;
std::cout << "3 * 5 = " << mul(3, 5) << std::endl;
return 0;
}
# 第一步:编译成目标文件(-c 只编译不链接,-I 指明头文件目录)
g++ -c src/mathlib.cpp -Iinclude -o mathlib.o
# 第二步:ar 打包成静态库(r 插入 / c 创建 / s 建符号索引)
ar rcs libmath.a mathlib.o
# 验证一:列出归档里有哪些成员
ar t libmath.a
# mathlib.o
# 验证二:链接使用(-L. 告诉链接器去当前目录找库)
g++ main.cpp -Iinclude -L. -lmath -o app_static
./app_static
# 3 + 5 = 8
# 3 * 5 = 15
# 用 nm 看一眼库里的符号表(13.6 会用它破案):T = 已定义(有实现)
nm libmath.a
# mathlib.o:
# 0000000000000000 T _Z3addii ← C++ 名字改编后的符号
# 0000000000000020 T _Z3mulii
# -C(--demangle)把改编名还原成人类可读的形式
nm -C libmath.a
# 0000000000000000 T add(int, int)
# 0000000000000020 T mul(int, int)
为什么静态库叫 archive(归档)?因为它本质就是"一堆 .o 的打包文件",跟 tar 打包文件是同一类思想。ar t 能列出成员、ar x 还能解包——理解了"打包",就理解了链接器为什么能按需只取用到的 .o,而不是整个库都塞进去。
动态库在"编译"这一步就和静态库分道扬镳:动态库里的代码将来要加载到内存的任意位置运行,所以必须编译成位置无关代码(PIC,Position Independent Code)——说人话就是"代码里不写死绝对地址,搬到内存哪个角落都能跑"。实现方式就两个选项:编译时加 -fPIC,链接时加 -shared。GCC 文档对 -shared 的定义是"生成一个共享对象,可以再与其他目标文件链接成可执行文件"——也就是我们说的动态库。
不写 -fPIC 会怎样?把普通 .o 直接打包成 .so,链接器会报 relocation R_X86_64_32S against `.rodata' can not be used when making a shared object; recompile with -fPIC——翻译过来就是"这段代码里带着写死的地址,没法做成动态库,回去加 -fPIC 重编"。这是新手做 .so 遇到的第一个报错,原因就一句话:忘了位置无关。
生成 .so 之后,链接程序的命令和静态库一模一样(-L. -lmath)。但运行时有一个第 4 章埋下的伏笔要还:链接期找到库 ≠ 运行期找到库。链接器(ld)靠 -L 找到 libmath.so、记下"欠条";程序启动时,动态加载器 ld.so 按自己的搜索路径(系统默认目录 + LD_LIBRARY_PATH)找库——找不到就报 error while loading shared libraries: libmath.so: cannot open shared object file。两种修法:运行时设 LD_LIBRARY_PATH=. ./app,或者链接时用 -Wl,-rpath 把路径"焊死"进可执行文件(-Wl, 表示把后面的参数原样传给链接器)。
还有一个概念值得认识:SONAME(共享对象名)。系统里真实文件往往是 libfoo.so.1(带版本号),另有一个不带版本号的软链接 libfoo.so 专供链接器使用——第 4 章那个 libjsoncpp.so.25 就是 SONAME 的活例子。日常用 g++ -shared 一步生成时不用管它,但看到 .so.1、.so.25 这种名字要认得:那都是动态库的版本化命名。
# 一步生成动态库:-fPIC(位置无关)+ -shared(共享对象)
g++ -fPIC -shared src/mathlib.cpp -Iinclude -o libmath.so
# 等价的两步版:
g++ -c -fPIC src/mathlib.cpp -Iinclude -o mathlib.o
g++ -shared mathlib.o -o libmath.so
# 链接使用:和静态库一模一样的参数,命令都不用改
g++ main.cpp -Iinclude -L. -lmath -o app_shared
# 编译链接都成功了,一运行却报错——这就是"链接期 vs 运行期"的两张脸:
./app_shared
# error while loading shared libraries: libmath.so: cannot open shared object file: No such file or directory
# 修法一:运行时告诉加载器去哪找(第 4 章柯南的方法)
LD_LIBRARY_PATH=. ./app_shared
# 3 + 5 = 8
# 修法二:链接时用 -Wl,-rpath 把目录焊进可执行文件($ORIGIN = 可执行文件所在目录)
g++ main.cpp -Iinclude -L. -lmath -Wl,-rpath,'$ORIGIN' -o app_shared
./app_shared
# 3 + 5 = 8 ← 这次不用设环境变量也能跑
# 没设 LD_LIBRARY_PATH 时,加载器找不到 libmath.so:
ldd ./app_shared
# libmath.so => not found ← 欠条没人还,运行必挂
# libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f...)
# libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)
# 设置之后,欠条还上了:
LD_LIBRARY_PATH=. ldd ./app_shared
# libmath.so => ./libmath.so (0x00007f...)
注意:-Wl,-rpath,'$ORIGIN' 里的 $ORIGIN 必须用单引号包住——它是链接器在运行时展开的特殊变量,不是 shell 变量。写成双引号会被 shell 先展开成空字符串,rpath 就废了。这是做"带着 .so 一起分发"时最实用的一招。
三个最常用的参数,先记住一句话分工:-I 管编译期的头文件搜索,-L 管链接期的库目录搜索,-l 管"要哪个库"。-I 和 -L 只回答"在哪些目录里找",-l 回答"找什么名字"。三者的关系就像点外卖:-l 是你要的菜名(math),-L 是外卖平台的商家列表(去哪找),-I 是菜谱目录(编译时查接口用)。
搜索顺序要分清两层。第一层是目录顺序:-L 指定的目录按命令行出现顺序依次搜索,最后才是系统默认目录(/usr/lib、/usr/local/lib 等)。所以 -L 写错路径,或者库不在 -L 目录里,链接器就报 /usr/bin/ld: cannot find -lmath。第二层是命令行顺序:GCC 文档原文说得很清楚——"链接器按命令行上出现的顺序搜索和处理库与目标文件",而且每个库只扫一遍(单遍扫描,single pass)。
单遍扫描是新手最常踩的坑,值得单独讲透:链接器拿着"缺口清单"从左到右走一遍,走到 main.o 时记下"缺 add",继续往后遇到 libmath.a 才补上。所以 -lmath 必须写在引用它的目标文件之后;写在前面,扫到库时清单还是空的,等扫到 main.o 记下缺口,库已经扫完了——直接报 undefined reference。库与库之间同理:库 A 依赖库 B,必须 -lA -lB,B 排在 A 后面。
| 参数 | 生效阶段 | 作用 | 说人话 |
|---|---|---|---|
-I./include | 编译期 | 加头文件搜索目录 | 编译器去 ./include 找 mathlib.h |
-L./lib | 链接期 | 加库文件搜索目录 | 链接器去 ./lib 目录找库 |
-lmath | 链接期 | 指定库名 | 找 libmath.a 或 libmath.so |
-static | 链接期 | 强制用静态库 | 有 .so 也不许用,只认 .a |
# 一个典型的三件套命令,三个参数各管一段:
g++ main.cpp -I./include -L./lib -lmath -o app
# 多个 -L 按出现顺序搜索:先 ./lib,找不到再去 ./third_party/lib
g++ main.cpp -L./lib -L./third_party/lib -lmath -o app
# 错误示范:-lmath 写在引用它的 main.cpp 之前 —— 单遍扫描先扫完了库
g++ -lmath main.cpp -o app
# /usr/bin/ld: /tmp/ccXXXXXX.o: in function `main':
# main.cpp:(.text+0x1a): undefined reference to `add(int, int)'
# collect2: error: ld returned 1 exit status
# 正确写法:被依赖的库放在依赖它的文件之后
g++ main.cpp -lmath -o app
# 库互相依赖时顺序更讲究:foo 依赖 bar,必须是 -lfoo -lbar
g++ main.cpp -lfoo -lbar -o app
记单遍扫描,费曼有个笨办法:把自己当成链接器,手里一张"欠条清单",从左到右挨个看命令行上的文件——看到"实现"就销账,看到"缺口"就记账。库出现在缺口之前,等于债主先走了。这个比喻能帮你推出所有顺序问题的答案,不用背。
先把报错分类:undefined reference to ... 是链接错误(不是编译错误)——编译已经过了,链接器拼装时发现"缺口没人填"。报错形式非常典型:/usr/bin/ld: main.cpp:(.text+0x1a): undefined reference to `add(int, int)',最后一行永远是 collect2: error: ld returned 1 exit status(collect2 是 g++ 内部调链接器的包装器)。看到"undefined reference + collect2",就知道问题出在链接这一步。
缺口为什么没人填?按第一性原理拆,只有三种可能:
-lmath,或者 -L 指错了目录,链接器根本没见到那个库——最常发生在"刚写完代码,忘了加库参数"的时候。-l 写在引用者之前,扫到库时缺口还没登记。_Z3addii 这种带类型信息的编码;而 C 库的符号不做改编。C++ 里引用 C 库函数必须用 extern "C" 声明,否则编译器按改编后的名字去找,必然 undefined reference。排查工具就是 13.3 预告过的 nm——它列出目标文件和库的符号表(说人话就是"这个文件里所有门牌号的清单")。nm 输出每行是"地址 + 符号类型 + 符号名",类型只需认识两个:T 表示已定义在代码段(有实现),U 表示未定义(缺实现)。破案流程:先 nm -C main.o 看缺口长什么样(add 是 U),再 nm -C libmath.a 看库里有不有对应的 T——有 T 说明库没链对(参数/顺序问题),没有 T 说明库根本不提供这个函数(找错库了)。
另一个高频报错 /usr/bin/ld: cannot find -lfoo 和 undefined reference 有本质区别:前者是"库文件根本没找到"(文件名级),后者是"库找到了但符号对不上"(符号级)。cannot find 的原因通常是:-L 路径写错、库还没编译生成、或者架构不匹配(32 位 .a 链 64 位程序——用 file libfoo.a 一眼就能看出架构)。
| 报错现象 | 本质 | 排查方向 |
|---|---|---|
undefined reference to `add(int, int)' | 符号级:缺口没人填 | nm -C 查符号;检查 -l 与库顺序 |
/usr/bin/ld: cannot find -lmath | 文件名级:库没找到 | find/ls 确认库存在;检查 -L 路径 |
libmath.so: cannot open shared object file | 运行期:加载器找不到 | ldd ./app 看 not found;LD_LIBRARY_PATH 或 -Wl,-rpath |
relocation ... can not be used when making a shared object | 编译期:非 PIC 代码 | 编译 .so 时补 -fPIC |
# 场景一:忘了链接库(或库顺序错了)——最常见
g++ main.cpp -Iinclude -o app
# /usr/bin/ld: /tmp/ccXXXXXX.o: in function `main':
# main.cpp:(.text+0x1a): undefined reference to `add(int, int)'
# collect2: error: ld returned 1 exit status
# 排查第一步:先编译 main.o,看看缺口是不是 U(未定义)
g++ -c main.cpp -Iinclude -o main.o
nm -C main.o
# 0000000000000000 U add(int, int) ← main.o 只有声明,没有实现
# 排查第二步:看库里有不有对应的 T(已定义)
nm -C libmath.a
# 0000000000000000 T add(int, int) ← 库里有实现,说明只是没链库
# 结论:命令补上 -lmath 即可
g++ main.cpp -Iinclude -L. -lmath -o app
# 场景二:-L 路径不对 / 库根本没生成
g++ main.cpp -Iinclude -L./wrong_dir -lmath -o app
# /usr/bin/ld: cannot find -lmath
# collect2: error: ld returned 1 exit status
# 排查:先确认库文件真的存在,再用 -L 指向它的真实目录
ls -l libmath.*
# -rw-r--r-- 1 user user 2088 ... libmath.a
# -rwxr-xr-x 1 user user 15808 ... libmath.so
find . -name "libmath*"
# ./libmath.a
# ./libmath.so
// 场景三:名字改编 —— C 库按 C 规则编译、符号不改编,
// C++ 里必须用 extern "C" 声明,否则编译器去找改编后的 C++ 名字:
extern "C" {
int c_add(int a, int b); // 按 C 规则找符号,而不是 c_add(int, int)
}
破案的顺序永远是从"离报错最近的一层"开始:先看报错是哪一种(编译?链接?运行?),再用对应工具(nm?ldd?)取证,最后才动手改。undefined reference 十有八九是前两类原因(忘链、顺序错),先用 nm -C 花三十秒确认,比瞎试参数快得多。
手写 g++ 命令链接库,参数一多就容易乱;CMake(第 2 章的主角)把这些苦活全包了。核心就两个命令:add_library 负责"建库",target_link_libraries 负责"用库"。add_library(math STATIC src/mathlib.cpp) 生成 libmath.a,把 STATIC 换成 SHARED 就生成 libmath.so——连 -fPIC 都自动加上,其余细节(-L、-l、库顺序)由 CMake 依据 target 依赖关系自动排好。
头文件路径怎么传?用 target_include_directories(math PUBLIC include)——PUBLIC 表示"随库传递":链接 math 的 target 自动获得 include 目录,main.cpp 里 #include "mathlib.h" 直接能找到,不用自己写 -I。这正是第 2 章讲的 CMake 核心思想:一切以 target 为单位声明依赖,属性和依赖关系都挂在 target 上,构建系统负责推导出具体的编译命令。
构建命令和第 2 章完全一样:cmake -B build && cmake --build build。有个省心的细节:CMake 默认会把构建目录写进可执行文件的 rpath,所以 build 树里直接 ./build/app 通常就能跑动态版;万一报 cannot open shared object file(比如手动改过 rpath 配置),用 LD_LIBRARY_PATH=build 兜底。另外 add_library 的类型还有 OBJECT(只编目标文件不打包)和 INTERFACE(纯头文件库)——现阶段认识 STATIC/SHARED 就够用了。
# CMakeLists.txt —— 静态库版
cmake_minimum_required(VERSION 3.16)
project(libdemo CXX)
# 生成静态库 libmath.a(不写类型关键字时默认也是 STATIC)
add_library(math STATIC src/mathlib.cpp)
# PUBLIC:头文件路径随库"传递"给所有链接者,main.cpp 不用自己写 -I
target_include_directories(math PUBLIC include)
add_executable(app main.cpp)
# CMake 自动补 -L、-lmath,并按依赖关系自动排好库顺序
target_link_libraries(app PRIVATE math)
# out-of-source 构建(第 2 章讲过),产物都在 build/ 里
cmake -B build
cmake --build build
./build/app
# 3 + 5 = 8
# 3 * 5 = 15
# 一行之差改成动态库(STATIC → SHARED),其余代码零改动:
# add_library(math SHARED src/mathlib.cpp)
# 构建产物变成 libmath.so。CMake 默认把构建目录写进 rpath,
# 万一报 cannot open shared object file,用 LD_LIBRARY_PATH=build 兜底:
LD_LIBRARY_PATH=build ./build/app
# 验证 CMake 建的动态版 app 依赖什么(rpath 是否生效一眼可见)
ldd ./build/app | grep math
# libmath.so => /path/to/libdemo/build/libmath.so (0x00007f...)
把本章连成一条线再看:手写命令是"知其所以然",CMake 是"知其然"。13.3~13.5 用 g++/ar 亲手做一遍库,你就知道 -I/-L/-l 和库顺序是怎么回事;13.7 再用 CMake,看到 add_library 就能脑补出它背后生成的命令。先手写后封装,这就是第一性原理的学习路径——理解底层,工具才是你的,而不是你的咒语。
用一句话分别解释:库、静态库、动态库、符号。再回答:为什么 g++ -lmath main.cpp -o app 会报 undefined reference,而 g++ main.cpp -lmath -o app 就正常?
四个概念对应"零件包 / 复制 / 欠条 / 门牌号";命令差异想想 13.5 的单遍扫描和债主先走的比喻。
库:别人写好的、编译好的、可直接拿来用的代码集合;静态库:链接时把代码复制进可执行文件的库;动态库:链接时只记依赖、运行期才加载的库;符号:函数和变量在二进制世界里的"门牌号"。因为链接器单遍扫描、从左到右处理命令行:-lmath 写在 main.cpp 之前时,扫到库时 main.o 的缺口还没登记,等记下缺口库已经扫完了,于是 undefined reference;写在后面就能补上。
为每个需求选一条命令:(a) 只把 mathlib.cpp 编译成目标文件(不链接);(b) 把 mathlib.o 打包成 libmath.a;(c) 生成动态库 libmath.so;(d) 查看可执行文件 app 运行时依赖哪些动态库;(e) 查看 libmath.a 里定义了哪些符号。
五个工具/参数:g++ -c、ar rcs、g++ -fPIC -shared、ldd、nm -C。
(a) g++ -c src/mathlib.cpp -Iinclude -o mathlib.o;(b) ar rcs libmath.a mathlib.o;(c) g++ -fPIC -shared src/mathlib.cpp -Iinclude -o libmath.so;(d) ldd ./app;(e) nm -C libmath.a(想看改编名用 nm libmath.a)。
报错 /usr/bin/ld: cannot find -lfoo,列出至少三种可能原因及对应的排查命令。再说明它和 undefined reference to 'bar()' 在本质上有何区别。
一个"找不到文件",一个"找到文件但符号对不上";原因从 -L 路径、库是否存在、架构匹配三个角度想。
cannot find -lfoo 的原因:① -L 路径不对——用 find . -name "libfoo*" 或 ls 确认库真实位置,修正 -L;② 库根本没生成——回去执行 make/ar 先产出 libfoo.a/.so;③ 架构不匹配——32 位 .a 链 64 位程序,用 file libfoo.a 查看架构。本质区别:cannot find 是文件名级(链接器在搜索目录里根本没见到 libfoo 这个文件);undefined reference 是符号级(库找到了,但里面没有你要的符号)——后者还要继续查:没链对库?库顺序错?还是名字改编对不上?
按本章的 libdemo 工程(include/mathlib.h、src/mathlib.cpp、main.cpp)完整做一遍:① 用 g++ -c + ar rcs 做出 libmath.a 并链接运行;② 用 -fPIC -shared 做出 libmath.so 链接运行,先用 ldd 观察到 not found,再用 LD_LIBRARY_PATH 修复;③ 故意把 -lmath 移到 main.cpp 前面制造 undefined reference,用 nm -C 验证符号后再修复;④ 改用 CMake 的 add_library(math SHARED ...) 重建并运行。把每一步的命令、报错原文和修复方法记成实验笔记。
按 13.3 → 13.4 → 13.5/13.6 → 13.7 的顺序走;报错要复制原文,不要凭印象写——报错原文就是最好的笔记。
① g++ -c src/mathlib.cpp -Iinclude -o mathlib.o && ar rcs libmath.a mathlib.o && g++ main.cpp -Iinclude -L. -lmath -o app_static && ./app_static,输出 3 + 5 = 8;② g++ -fPIC -shared src/mathlib.cpp -Iinclude -o libmath.so && g++ main.cpp -Iinclude -L. -lmath -o app_shared,先 ldd ./app_shared 看到 libmath.so => not found,再 LD_LIBRARY_PATH=. ./app_shared 修复;③ g++ -lmath main.cpp -o app 复现 undefined reference to 'add(int, int)',nm -C libmath.a 看到 T add(int, int) 确认库没问题,把 -lmath 移到 main.cpp 后修复;④ 用 13.7 的 CMakeLists.txt 把 STATIC 改 SHARED,cmake -B build && cmake --build build 后 ./build/app 直接可跑(CMake 默认 rpath)。