Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

本页目录

静态链接 vs 动态链接:原理对比、部署选择与常见陷阱 / Static and Dynamic Linking: Principles, Deployment, and Pitfalls ​

📅 创建时间:2026-07-13 🏷️ 标签:#C++ #静态链接 #动态链接 #GOT/PLT #部署 📚 前置知识:windows build outputs, linux build outputs


📋 本章目标 ​

  • 深入理解静态链接和动态链接的内部工作机制
  • 理解 GOT(全局偏移表)和 PLT(过程链接表)的工作原理——延迟绑定是怎么实现的
  • 理解 Windows 侧 IAT(导入地址表)的工作方式
  • 掌握符号可见性控制:-fvisibility=hidden 的作用
  • 能够根据实际场景做出静态/动态链接的决策

第1部分:两种链接方式的核心差异 ​

1.1 静态链接——所有代码融为一体 ​

┌─────────────────────────────────────────────────────────────────────────────┐
│                    静态链接                                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  链接时:                                                                    │
│  ┌───────────┐   ┌──────────┐   ┌──────────┐                               │
│  │ main.o    │   │ libA.a   │   │ libB.a   │                               │
│  │           │   │          │   │          │                               │
│  │ call foo ─┼───┤ foo: ... │   │          │                               │
│  │ call bar ─┼───┼──────────┼───┤ bar: ... │                               │
│  └───────────┘   └──────────┘   └──────────┘                               │
│       │               │               │                                     │
│       └───────────────┴───────────────┘                                     │
│                       │  链接器复制代码                                      │
│                       ▼                                                     │
│              ┌───────────────────┐                                          │
│              │   main.exe       │  一个完整的二进制                          │
│              │   (包含 foo 和   │                                           │
│              │    bar 的全部    │                                           │
│              │    机器码)       │                                           │
│              └───────────────────┘                                          │
│                                                                             │
│  运行时:                                                                    │
│  • 只加载一个文件                                                            │
│  • 所有符号地址在编译时就确定了                                              │
│  • 不需要动态链接器参与                                                      │
│                                                                             │
│  优点:                                                                      │
│  ✅ 单文件部署:发给用户一个 exe 就能跑                                      │
│  ✅ 无版本依赖:不依赖系统上有没有 foo.so.2                                  │
│  ✅ 启动稍快:不需要加载多个 .so 和符号重定位                                │
│  ✅ 链接时优化 (LTO):可以跨 .o 内联优化                                     │
│                                                                             │
│  缺点:                                                                      │
│  ❌ 体积大:每个程序都自带一份 libc/libstdc++ 的代码                         │
│  ❌ 安全更新麻烦:libssl 有漏洞 → 所有静态链接的程序都要重编重发             │
│  ❌ 无法共享内存:每个进程各自加载一份代码                                    │
│  ❌ 不支持插件:运行时没法加载新的代码                                       │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

1.2 动态链接——代码在运行时结合 ​

┌─────────────────────────────────────────────────────────────────────────────┐
│                    动态链接                                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  链接时:                                                                    │
│  ┌───────────┐                         ┌──────────┐                         │
│  │ main.o    │                         │ libfoo.so│                         │
│  │           │   链接器只记录依赖        │          │                         │
│  │ call foo ─┼──→ "此符号来自 libfoo.so" │ foo: ... │                         │
│  └───────────┘                         └──────────┘                         │
│       │                                                                     │
│       ▼                                                                     │
│  ┌───────────────────┐                                                      │
│  │   main.exe        │  ← 不含 foo 的代码                                   │
│  │   (只有调用 stub) │                                                     │
│  │   依赖:libfoo.so  │                                                     │
│  └───────────────────┘                                                      │
│                                                                             │
│  运行时:动态链接器介入                                                      │
│  ┌───────────────────┐     ┌──────────┐                                    │
│  │   main.exe        │     │ libfoo.so│                                    │
│  │   ┌───────────┐   │     │          │                                    │
│  │   │ call foo  │───┼────→│ foo()    │                                    │
│  │   │ (PLT stub)│   │     │          │                                    │
│  │   └───────────┘   │     └──────────┘                                    │
│  └───────────────────┘                                                      │
│                                                                             │
│  优点:                                                                      │
│  ✅ 共享内存:多个进程加载同一份 .so,物理内存只有一份                        │
│  ✅ 热更新:替换 .so 文件,重启程序即可(甚至不重启)                         │
│  ✅ 安全补丁:系统 libssl.so 打补丁,所有程序自动受益                       │
│  ✅ 插件系统:运行时 dlopen/LoadLibrary 加载新功能                           │
│                                                                             │
│  缺点:                                                                      │
│  ❌ 依赖地狱 (DLL Hell):找不到 .so、版本不匹配 → 程序跑不起来               │
│  ❌ 启动稍慢:加载时要做符号重定位(特别是 LD_BIND_NOW 时)                   │
│  ❌ 符号冲突:两个 .so 导出了同名的不同函数                                  │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

第2部分:动态链接的底层机制——GOT 和 PLT ​

2.1 问题:动态库的地址是不确定的 ​

┌─────────────────────────────────────────────────────────────────────────────┐
│         为什么需要 GOT/PLT?                                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  你的代码里调用 printf():                                                   │
│                                                                             │
│  call printf    ← 这条指令应该跳转到哪个地址?                               │
│                                                                             │
│  在静态链接时,链接器知道 printf 的确切地址(比如 0x401000),                │
│  可以直接把地址写死在 call 指令里。                                          │
│                                                                             │
│  在动态链接时,printf 在 libc.so 里,而 libc.so 可能被加载到                 │
│  任何地址(因为 ASLR)。你编译时根本不知道运行时 printf 在哪。               │
│                                                                             │
│  解决方法:不要直接 call printf 的地址,而是通过一个"中转站"。               │
│  这个"中转站"由两部分组成:PLT(过程链接表)和 GOT(全局偏移表)。           │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

2.2 GOT 和 PLT 的工作原理 ​

┌─────────────────────────────────────────────────────────────────────────────┐
│                GOT/PLT 延迟绑定流程                                           │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  你的代码:                                                                  │
│  call printf@plt        ← 你调用的是 PLT 表项,不是 printf 本身              │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                           PLT (代码段)                               │   │
│  │                                                                     │   │
│  │  printf@plt:                                                        │   │
│  │    jmp *printf@GOT     ──────────┐                                  │   │
│  │    push <index>                  │  首次调用时 GOT 里是              │   │
│  │    jmp PLT[0]   (解析器)         │  下一条指令的地址                 │   │
│  │                                  │  ↓ 跳到 push <index>             │   │
│  └──────────────────────────────────┼──────────────────────────────────┘   │
│                                     │                                       │
│                                     ▼                                       │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                      GOT (数据段,可写)                               │   │
│  │                                                                     │   │
│  │  printf@GOT:    0x401026  ← 首次:指向 PLT 中的下一条指令(push idx)  │   │
│  │                 0x7f...0a  ← 解析后:被动态链接器填为 printf 真实地址 │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  首次调用 printf 的流程:                                                    │
│  1. call printf@plt                                                        │
│  2. jmp *printf@GOT → GOT 里是 0x401026(回到 PLT)                        │
│  3. push <index>    → 告诉解析器:"我要找 printf"                          │
│  4. jmp PLT[0]      → 跳转到动态链接器(ld.so)的解析函数                   │
│  5. ld.so 找到 printf 在 libc.so 中的真实地址                               │
│  6. 把真实地址写回 printf@GOT                                                 │
│  7. 跳转到 printf 的真实地址执行                                             │
│                                                                             │
│  后续调用 printf 的流程:                                                    │
│  1. call printf@plt                                                        │
│  2. jmp *printf@GOT → GOT 里已经是真实地址 → 直接跳转到 printf!            │
│                                                                             │
│  这就是"延迟绑定"(Lazy Binding):第一次调用时解析,之后直接跳转。             │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

2.3 LD_BIND_NOW 和安全性 ​

bash
# 默认:延迟绑定(LD_BIND_NOW=0),首次调用时才解析
./my_app

# 立即绑定:启动时就解析所有符号(更安全,稍慢)
LD_BIND_NOW=1 ./my_app

# 编译时也可以指定:
g++ -Wl,-z,now main.o -o my_app   # 在 ELF 中标记要求立即绑定
1
2
3
4
5
6
7
8

立即绑定在安全敏感场景下很有意义——一旦程序启动,GOT 就是只读的,攻击者无法通过修改 GOT 来劫持函数调用(一种常见的漏洞利用技术)。


第3部分:Windows 侧的对应机制——IAT ​

┌─────────────────────────────────────────────────────────────────────────────┐
│              Windows 的 IAT(Import Address Table)                           │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  Windows 没有 GOT/PLT,但有类似机制——IAT:                                   │
│                                                                             │
│  你的代码:                                                                  │
│  call [__imp_MessageBoxA]    ← 间接调用,通过 IAT 中的地址                   │
│                                                                             │
│  链接时:                                                                     │
│  • .exe 中有一个 IAT(Import Address Table)                                  │
│  • IAT 最初存储的是函数名(或序号)                                           │
│                                                                             │
│  加载时(PE Loader):                                                        │
│  • 加载 .exe,发现它依赖 user32.dll                                          │
│  • 加载 user32.dll,找到 MessageBoxA 的导出地址                               │
│  • 把 MessageBoxA 的真实地址写入 .exe 的 IAT 中                              │
│                                                                             │
│  ⚠️ Windows 默认是"加载时绑定"(load-time binding),不是延迟绑定!           │
│  所有导入函数在程序启动时一次性解析完成。                                     │
│  如果某个 DLL 找不到或某个函数不存在 → 程序直接无法启动。                      │
│                                                                             │
│  但 Windows 也支持延迟加载(Delay Load):                                    │
│  /DELAYLOAD:user32.dll  → 首次调用时才加载和解析,类似 Linux 的延迟绑定       │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

第4部分:符号可见性——控制哪些符号对外暴露 ​

4.1 为什么需要隐藏符号? ​

cpp
// 假设你的 .so 库有 200 个函数,但只有 5 个是对外 API
// 如果不加控制,200 个函数全部导出!

// bad_mylib.cpp
int add(int a, int b) { return a + b; }          // ← 要导出
int internal_helper(int x) { return x * 2; }      // ← 内部用的,不该导出
int validate_input(const char* s) { ... }          // ← 内部用的
// ... 197 more
1
2
3
4
5
6
7
8

全部导出会带来什么问题?

  1. .so 体积更大(符号表膨胀)
  2. 加载更慢(更多符号需要重定位)
  3. 符号冲突:如果另一个 .so 也导出了 validate_input,行为未定义
  4. 暴露实现细节(增加被逆向工程的风险)

4.2 Linux 侧:-fvisibility=hidden ​

cmake
# CMake 中设置(推荐做法)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)     # 默认所有符号隐藏
set(CMAKE_VISIBILITY_INLINES_HIDDEN YES)    # inline 函数也隐藏
1
2
3
cpp
// 在代码中显式标记哪些要导出
#define MYLIB_EXPORT __attribute__((visibility("default")))

class MYLIB_EXPORT PublicAPI {   // ← 只有标记了的才导出
public:
    void doWork();
};

// 没有标记 → 默认 hidden → 不导出
class InternalHelper {           // ← 外部看不到
    void doInternalWork();
};
1
2
3
4
5
6
7
8
9
10
11
12

4.3 Windows 侧:dllexport 显式导出 ​

cpp
// Windows 的哲学相反:默认不导出,必须显式声明
#define MYLIB_EXPORT __declspec(dllexport)

class MYLIB_EXPORT PublicAPI {   // 只有显式声明的才导出
    void doWork();
};
1
2
3
4
5
6
┌─────────────────────────────────────────────────────────────────────────────┐
│              符号可见性策略对比                                               │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  Linux:默认导出所有 → 需要 opt-out(-fvisibility=hidden)                    │
│  Windows:默认不导出 → 需要 opt-in(__declspec(dllexport))                   │
│                                                                             │
│  CMake 推荐做法(跨平台统一):                                               │
│  set(CMAKE_CXX_VISIBILITY_PRESET hidden)    # Linux 默认隐藏                 │
│  # Windows 还是需要显式 dllexport,但这个宏在 Linux 上也生效                  │
│                                                                             │
│  生成的宏:                                                                  │
│  #ifdef _WIN32                                                              │
│    #define MYLIB_API __declspec(dllexport)   // Windows 导出               │
│  #else                                                                      │
│    #define MYLIB_API __attribute__((visibility("default"))) // Linux 导出  │
│  #endif                                                                     │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

第5部分:部署选择决策树 ​

┌─────────────────────────────────────────────────────────────────────────────┐
│              静态链接 vs 动态链接 决策树                                       │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  Q1: 你需要插件系统吗?(运行时加载第三方 .dll/.so)                          │
│      YES → 动态链接(插件宿主 + 插件都是动态的)                              │
│      NO  → 继续                                                             │
│                                                                             │
│  Q2: 你的程序是否需要和系统共享库(如OpenGL、CUDA)交互?                    │
│      YES → 这些系统库必须动态链接(它们本身就不是静态的)                     │
│                                                                             │
│  Q3: 你是做 CLI 工具、单文件分发、容器化部署吗?                              │
│      YES → 静态链接优先(零依赖单文件 = 极佳用户体验)                        │
│                                                                             │
│  Q4: 你是做桌面软件、大型系统吗?                                             │
│      YES → 混合策略:                                                        │
│            • 系统依赖(Qt、OpenGL)动态链接                                   │
│            • 自己内部模块可以静态链接                                         │
│            • 插件接口部分动态链接                                             │
│                                                                             │
│  快速参考:                                                                  │
│  ┌────────────────────┬──────────────────────┬──────────────────────────┐  │
│  │ 场景                │ 推荐                  │ 原因                      │  │
│  ├────────────────────┼──────────────────────┼──────────────────────────┤  │
│  │ CLI 小工具          │ 静态链接              │ 单文件分发,零依赖        │  │
│  │ Docker 容器         │ 静态链接              │ 镜像分层简单              │  │
│  │ 桌面软件(无插件)   │ 核心静态 + GUI库动态  │ Qt 等太重了,动态即可    │  │
│  │ 桌面软件(有插件)   │ 核心动态 + 插件动态   │ 插件系统必须动态          │  │
│  │ 系统级库(给别人的) │ 动态链接 + 静态版本   │ 给别人选择的空间          │  │
│  │ AI/ML 推理引擎      │ 核心静态 + CUDA动态   │ CUDA 驱动必须动态         │  │
│  └────────────────────┴──────────────────────┴──────────────────────────┘  │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

核心总结 ​

┌─────────────────────────────────────────────────────────────────────────────┐
│              静态 vs 动态链接 核心速查                                        │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  静态链接:代码在编译时嵌入一个二进制                                         │
│    ✅ 优点:单文件部署、无版本依赖                                           │
│    ❌ 缺点:体积大、安全更新需重编、无法做插件                                │
│                                                                             │
│  动态链接:代码在运行时组装                                                   │
│    ✅ 优点:共享内存、热更新、插件系统                                        │
│    ❌ 缺点:依赖地狱、符号冲突、启动慢                                        │
│                                                                             │
│  Linux GOT/PLT = 延迟绑定中转站                                              │
│    首次调用 → PLT → GOT(未解析) → ld.so → 填GOT → 执行                       │
│    后续调用 → PLT → GOT(已解析) → 直接执行                                   │
│                                                                             │
│  Windows IAT = 加载时绑定(默认全部解析)                                     │
│    /DELAYLOAD 可以做到类似 linux 的延迟绑定                                   │
│                                                                             │
│  符号可见性:-fvisibility=hidden + 显式导出(少导出 = 少问题)                │
│                                                                             │
│  插件开发前提:宿主和插件都必须动态链接!                                     │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

章节测试 ​

测试1:本质区别 ​

静态链接和动态链接最本质的区别是什么?

测试2:GOT/PLT ​

解释为什么 GOT/PLT 采用"延迟绑定"设计。首次调用 printf 时经过了什么流程?

测试3:LD_BIND_NOW ​

LD_BIND_NOW=1 的作用是什么?什么场景下需要它?

测试4:符号可见性 ​

Linux 上编译 .so 时,不加 -fvisibility=hidden 会有什么潜在问题?举两个具体的例子。

测试5:部署决策 ​

你正在开发一个图像处理 CLI 工具,需要 libpng 和 libjpeg。你应该静态链接还是动态链接?说出你的理由。

测试6:Windows vs Linux ​

Windows 和 Linux 在动态库加载时的符号绑定策略有什么不同?


参考答案 ​

测试1答案 ​

答案:最本质的区别是代码结合的时机。静态链接在编译/链接时把所有代码合并为一个独立的二进制文件,运行时不再需要外部库。动态链接在编译时只记录依赖关系,运行时才由动态链接器把 .so/.dll 加载到进程地址空间并解析符号引用。

测试2答案 ​

答案:因为动态链接时不知道 printf 在内存中的确切地址(ASLR 随机化 + .so 加载位置不确定)。延迟绑定的流程:首次调用 printf@PLT → GOT 中存的是 PLT 的下一条指令地址 → 跳回 PLT → push 函数索引 → 调用动态链接器 → 动态链接器找到 printf 真实地址 → 写回 GOT → 执行 printf。后续调用时 GOT 已有真实地址,直接跳转,零额外开销。

测试3答案 ​

答案:LD_BIND_NOW=1 强制在程序启动时就解析所有动态符号(而非首次调用时才解析)。适用场景:(1) 安全敏感程序——启动后 GOT 设为只读,防止 GOT 覆写攻击;(2) 对延迟敏感的实时系统——不想在执行中途因符号解析产生不可预测的延迟;(3) 调试——立即发现符号缺失问题而不等到运行时 random crash。

测试4答案 ​

答案:(1) 符号冲突:两个 .so 都导出了同名的内部辅助函数(如 validate),动态链接器可能把一个 .so 的调用错误绑定到另一个 .so 的实现。(2) 加载性能:所有符号都导出 → 符号表庞大 → 动态链接器要做更多重定位 → 启动变慢;.so 文件也更大。

测试5答案 ​

答案:建议静态链接。理由:(1) CLI 工具追求零依赖单文件部署,用户下载一个 exe 就能跑;(2) 不需要插件系统;(3) libpng/libjpeg 体积适中,静态嵌入不会过分膨胀。

如果选动态链接也有理由(安全更新),但 CLI 工具的场景下静态链接的用户体验更好。

测试6答案 ​

答案:Windows 默认是加载时绑定(load-time binding),PE 加载器在启动时一次性解析所有导入符号,如果某个 DLL 或函数不存在,程序直接无法启动。Linux 默认是延迟绑定(lazy binding),首次调用时才解析,如果某个符号从未被用到就不会报错。Windows 的 /DELAYLOAD 链接器选项可模拟延迟绑定。


相关笔记 ​

  • windows build outputs - Windows 编译产物详解
  • linux build outputs - Linux 编译产物详解
  • cpp abi compatibility - C++ ABI 兼容性
  • runtime dynamic loading - LoadLibrary 与 dlopen

下一步学习 ​

  • [ ] 阅读 05 - C++ ABI 兼容性

学习状态:🟡 开始学习

工程深化:把本篇知识落到项目里 ​

这一节不是为了凑篇幅,而是把《静态链接 vs 动态链接:原理对比、部署选择与常见陷阱 / Static and Dynamic Linking: Principles, Deployment, and Pitfalls》从“知道概念”推进到“能在项目里稳定使用”。 阅读时可以把每个知识点都追问成四件事:它保护什么边界,失败时有什么现象,怎样最小复现,怎样写进团队流程。

text
concept -> boundary -> failure signal -> minimal proof -> project rule
1

工程切片 1:最小可复现样例 ​

**场景。**围绕 load/unload 建一个很小的案例,不要一开始就放进完整业务系统。把问题压缩到一个可以提交给同事的目录,保留源码、构建命令、版本输出和预期现象。

**边界。**先判断这里讨论的是 load/unload、句柄 还是 入口函数 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> load/unload -> 句柄 -> 入口函数 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 2:接口边界 ​

**场景。**围绕 句柄 建一个很小的案例,不要一开始就放进完整业务系统。写清调用方能依赖什么、不能依赖什么,并把隐式假设转换成命名函数、配置项或测试。

**边界。**先判断这里讨论的是 句柄、入口函数 还是 引用计数 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 句柄 -> 入口函数 -> 引用计数 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 3:失败注入 ​

**场景。**围绕 入口函数 建一个很小的案例,不要一开始就放进完整业务系统。故意制造一个常见错误,让日志、断言或测试先失败,再用修复后的证据说明规则生效。

**边界。**先判断这里讨论的是 入口函数、引用计数 还是 错误码 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 入口函数 -> 引用计数 -> 错误码 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 4:跨平台差异 ​

**场景。**围绕 引用计数 建一个很小的案例,不要一开始就放进完整业务系统。至少比较 Windows、Linux 或不同编译器下的一个差异,记录差异属于标准、实现还是平台约定。

**边界。**先判断这里讨论的是 引用计数、错误码 还是 生命周期 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 引用计数 -> 错误码 -> 生命周期 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 5:性能观察 ​

**场景。**围绕 错误码 建一个很小的案例,不要一开始就放进完整业务系统。用小规模和压力规模各跑一次,区分算法成本、同步成本、I/O 成本和工具链配置成本。

**边界。**先判断这里讨论的是 错误码、生命周期 还是 load/unload 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 错误码 -> 生命周期 -> load/unload -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 6:生命周期 ​

**场景。**围绕 生命周期 建一个很小的案例,不要一开始就放进完整业务系统。标出资源创建、转移、共享、停止和销毁的顺序,特别关注错误返回和提前退出路径。

**边界。**先判断这里讨论的是 生命周期、load/unload 还是 句柄 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 生命周期 -> load/unload -> 句柄 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

本篇收束 ​

掌握《静态链接 vs 动态链接:原理对比、部署选择与常见陷阱 / Static and Dynamic Linking: Principles, Deployment, and Pitfalls》的标志,不是记住所有名词,而是能把 load/unload、句柄、入口函数、引用计数、错误码、生命周期 放进一条可验证的工程链路。 先用最小样例建立判断,再用测试和脚本固定判断,最后把失败证据留给未来的自己和团队。

最后更新于:

Pager
下一篇← C++ 编程 / C++ Programming

持续记录,持续成长

Copyright © Tidenflow