Windows 编译产物详解:exe、DLL、lib —— 彻底分清两种 lib / Windows Build Artifacts: EXE, DLL, and the Two Kinds of LIB
📅 创建时间:2026-07-13 🏷️ 标签:#Windows #C++ #DLL #静态库 #导入库 #MSVC 📚 前置知识:cpp project fundamentals
📋 本章目标
- 彻底分清 Windows 上两种
.lib文件的本质区别——这是 Windows C++ 开发最大的认知陷阱 - 理解 .exe、.dll、.obj、.lib、.pdb、.exp 等所有编译产物的身份和用途
- 掌握 DLL 导出/导入机制:
__declspec(dllexport)和__declspec(dllimport) - 理解运行时库链接:
/MTvs/MD的选择和混用的灾难性后果 - 熟悉 MSVC 工具链中的关键工具:cl.exe、link.exe、lib.exe、dumpbin.exe
第1部分:Windows 编译产物全景图
1.1 一次完整编译产生了什么?
┌─────────────────────────────────────────────────────────────────────────────┐
│ Windows C++ 编译产物全景 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 源码 编译过程 最终产物 │
│ │
│ mylib.cpp ──→ cl.exe ──→ mylib.obj ──→ link.exe ──→ mylib.dll │
│ │ │
│ mylib.h ├──→ mylib.lib ← 导入库! │
│ │ │
│ (如果有 .def) ├──→ mylib.exp │
│ │ │
│ (如果 /DEBUG) └──→ mylib.pdb │
│ │
│ main.cpp ────→ cl.exe ──→ main.obj ──→ link.exe ──→ main.exe │
│ mylib.lib ──┘ main.pdb │
│ │
│ 产生的文件列表: │
│ ┌───────────┬──────────────────────────────────────────────────────────┐ │
│ │ 文件 │ 身份 │ │
│ ├───────────┼──────────────────────────────────────────────────────────┤ │
│ │ .obj │ 目标文件——编译产生,一个 .cpp 对应一个 .obj │ │
│ │ .exe │ PE 可执行文件——有 main/WinMain 入口点,可以直接运行 │ │
│ │ .dll │ 动态链接库——没有 main,被 .exe 或其他 .dll 加载 │ │
│ │ .lib(1) │ 静态库——.obj 的集合,链接时嵌入到 .exe/.dll 中 │ │
│ │ .lib(2) │ 导入库——DLL 的"目录",链接时告诉链接器符号在 DLL 里 │ │
│ │ .pdb │ 调试符号文件——存函数名、文件名、行号映射,给调试器用的 │ │
│ │ .exp │ 导出文件——链接器生成的中间产物,通常可以删除 │ │
│ │ .ilk │ 增量链接文件——加速增量链接,可以删除 │ │
│ │ .res │ 编译后的资源文件(图标、对话框、菜单等) │ │
│ └───────────┴──────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘第2部分:彻底分清两种 .lib —— 本章最核心的内容
2.1 你的困惑是完全正常的
在 Windows 上,.lib 这个后缀名被两种完全不同的东西共用。这就像两个人都叫"张三",但一个是会计一个是建筑工人——同名但干的活完全不同。这是 Windows C++ 开发中最大的认知陷阱。
┌─────────────────────────────────────────────────────────────────────────────┐
│ 两种 .lib 的本质区别 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 静态库 .lib(Static Library) │ │
│ │ │ │
│ │ 本质:一堆 .obj 文件打包在一起(类似 ZIP 或 tar 归档) │ │
│ │ 大小:比较大(包含实际机器码) │ │
│ │ 生成方式:lib.exe /OUT:mylib.lib obj1.obj obj2.obj ... │ │
│ │ 使用时机:链接时把代码嵌入到最终 .exe/.dll 中 │ │
│ │ 运行时:不需要额外文件(代码已经在 .exe 里了) │ │
│ │ 查看:dumpbin /symbols mylib.lib(可以看到函数名和代码) │ │
│ │ │ │
│ │ .cpp → cl.exe → .obj ─→ lib.exe → .lib(静态库) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 导入库 .lib(Import Library) │ │
│ │ │ │
│ │ 本质:DLL 的"目录"——只含 stub 代码,不含实际实现 │ │
│ │ 大小:比较小(只有跳转 stub,没有实际代码) │ │
│ │ 生成方式:link.exe 生成 .dll 时自动生成(/IMPLIB 选项) │ │
│ │ 使用时机:链接时告诉链接器"这些符号在 .dll 里" │ │
│ │ 运行时:必须有对应的 .dll 存在(否则程序启动报错) │ │
│ │ 查看:dumpbin /exports mylib.lib(只有导出符号列表) │ │
│ │ │ │
│ │ .cpp → cl.exe → .obj ─→ link.exe → .dll + .lib(导入库) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ⚠️ 怎么区分? │
│ • 看大小:几 MB 以上多半是静态库;几 KB 多半是导入库 │
│ • dumpbin /symbols:静态库能看到函数名+实现;导入库只有 __imp_ 标记 │
│ • 场景判断:有对应 .dll 的 .lib 是导入库;单独的 .lib 是静态库 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘2.2 深入理解导入库的工作机制
导入库是怎么"偷梁换柱"的?看这个例子:
// 你的代码里写了:
add(3, 5); // add 函数在 mylib.dll 里编译和链接时发生了什么:
┌─────────────────────────────────────────────────────────────────────────────┐
│ 导入库的 stub 机制 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 你在代码中调用 add(3, 5) │
│ │ │
│ ▼ 编译 │
│ main.obj 中有一条 call 指令:call ???(还不知道 add 在哪) │
│ │ │
│ ▼ 链接(链接器读入 mylib.lib 导入库) │
│ 导入库告诉链接器: │
│ "add 这个符号在 mylib.dll 里,我给你一个 stub: │
│ jmp [__imp_add] ← 间接跳转,实际地址在 IAT 表中 │
│ " │
│ │ │
│ ▼ 生成 │
│ main.exe 的导入表里记录了:需要从 mylib.dll 加载 add │
│ │ │
│ ▼ 运行时(PE 加载器) │
│ 1. 加载 main.exe │
│ 2. 发现 main.exe 依赖 mylib.dll │
│ 3. 找到并加载 mylib.dll │
│ 4. 找到 mylib.dll 中 add 的实际地址 │
│ 5. 把地址填入 main.exe 的 IAT(Import Address Table) │
│ 6. __imp_add → 指向 add 的真实地址 │
│ │ │
│ ▼ │
│ 你的 call add → jmp [__imp_add] → DLL 中 add 的真实代码 │
│ │
│ 结论:导入库不含 add 的代码实现,只含一条"去 DLL 里找 add"的指令 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘2.3 实战场景:甲方给你文件,怎么用?
┌─────────────────────────────────────────────────────────────────────────────┐
│ 甲方给你 你需要做什么 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 场景A:.h + .lib(静态库) │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 1. #include 他们的 .h │ │
│ │ 2. 链接时添加 .lib │ │
│ │ 3. 编译完成,不需要额外文件 │ │
│ │ 4. 发布你的程序时:只需要你的 .exe │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 场景B:.h + .lib(导入库) + .dll │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 1. #include 他们的 .h │ │
│ │ 2. 链接时添加 .lib(导入库) │ │
│ │ 3. 编译完成 │ │
│ │ 4. 运行时 .dll 必须在搜索路径中——这步最容易忘! │ │
│ │ 5. 发布你的程序时:你的 .exe + 他们的 .dll │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 场景C:.h + .dll(没有 .lib!) │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 这通常是甲方漏给了导入库,或者 DLL 本来就是给运行时加载用的 │ │
│ │ 方案1:生成导入库:dumpbin /exports foo.dll → 写 .def → lib.exe │ │
│ │ 方案2:运行时动态加载:LoadLibrary + GetProcAddress │ │
│ │ (这就是第 06 篇要讲的内容) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘第3部分:DLL 的符号导出机制
3.1 __declspec(dllexport) 和 __declspec(dllimport)
这是 Windows DLL 最核心的两个宏:
// ====== 编译 DLL 时(MYLIB_EXPORTS 被定义)======
#ifdef MYLIB_EXPORTS
#define MYLIB_API __declspec(dllexport)
#else
#define MYLIB_API __declspec(dllimport)
#endif
// 在 DLL 内部:dllexport → 告诉链接器"这个函数要放到导出表里"
class MYLIB_API Calculator {
public:
int add(int a, int b);
int multiply(int a, int b);
};
MYLIB_API int global_compute(int x);┌─────────────────────────────────────────────────────────────────────────────┐
│ dllexport vs dllimport 的工作原理 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ __declspec(dllexport) —— 编译 DLL 时用 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ • 告诉链接器:把这个符号放入 DLL 的导出表 │ │
│ │ • 导出表 = DLL 提供给外界的符号目录 │ │
│ │ • dumpbin /exports mylib.dll 可以看到所有导出符号 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ __declspec(dllimport) —— 使用 DLL 时用(链接 .exe 或其他 .dll) │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ • 告诉编译器:这个符号来自外部 DLL,不要生成直接调用 │ │
│ │ • 改为通过 __imp_ 间接调用(走 IAT) │ │
│ │ • 优化:编译器知道是从 DLL 导入,可能生成更高效的代码 │ │
│ │ • 不写 dllimport 通常也能工作,但会产生一次不必要的间接跳转 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 典型模式(几乎所有 Windows DLL 项目都用): │
│ #ifdef MYLIB_EXPORTS // 只在编译 DLL 时定义 │
│ #define MYLIB_API __declspec(dllexport) │
│ #else // 使用者 include 这个头文件时 │
│ #define MYLIB_API __declspec(dllimport) │
│ #endif │
│ │
└─────────────────────────────────────────────────────────────────────────────┘3.2 .def 文件——显式控制导出
除了 __declspec(dllexport),还有另一种方式控制 DLL 导出——.def 文件。
// mylib.def
LIBRARY mylib
EXPORTS
add @1 // @1 是序号(ordinal)
multiply @2
compute @3 NONAME // 只按序号导出,隐藏函数名
internal_fn @4 PRIVATE // 不对外可见.def 文件的优势:
- 可以按序号导出(不用函数名,更难逆向)
- 可以导出没有 dllexport 标记的函数
- 精确控制哪些符号对外可见
- 可以重命名导出符号
实际项目中的选择:大多数项目用 __declspec(dllexport) + 宏方式,因为更简单、和代码在一起。.def 用于需要精确控制的场景(如系统级 DLL)。
3.3 从 DLL 的角度看符号可见性
用 dumpbin 查看 DLL 的导出表:
dumpbin /exports mylib.dll
# 输出示例:
# ordinal hint RVA name
# 1 0 00001200 ?add@@YAHHH@Z ← C++ name mangling
# 2 1 00001250 multiply ← extern "C" 的干净名字
# 3 2 00001300 compute注意:如果不用 extern "C",C++ 函数在 DLL 里的名字会是 mangled 形式(如 ?add@@YAHHH@Z)。调用方如果也是同版本编译器编译的,这个不是问题。但如果跨编译器或跨语言调用,就要用 extern "C" 保平安。
第4部分:/MT 和 /MD —— 运行时库链接方式
4.1 这个问题有多重要?
混用 /MT 和 /MD 会导致程序崩溃,而且错误信息完全看不出原因。 这是 Windows C++ 开发中排名前 3 的坑。
┌─────────────────────────────────────────────────────────────────────────────┐
│ 运行时库链接选项 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ MSVC 提供了四种运行时库链接方式: │
│ │
│ ┌──────────┬────────────────────┬──────────────┬──────────────────────┐ │
│ │ 选项 │ 含义 │ CRT 位置 │ 场景 │ │
│ ├──────────┼────────────────────┼──────────────┼──────────────────────┤ │
│ │ /MT │ 静态链接CRT(Release)│ 嵌入 .exe │ 单文件分发 │ │
│ │ /MTd │ 静态链接CRT(Debug) │ 嵌入 .exe │ Debug 开发 │ │
│ │ /MD │ 动态链接CRT(Release)│ vcruntime.dll │ 多个模块共享CRT │ │
│ │ /MDd │ 动态链接CRT(Debug) │ vcruntimed.dll│ Debug 开发 │ │
│ └──────────┴────────────────────┴──────────────┴──────────────────────┘ │
│ │
│ CRT = C Runtime Library = 包含 malloc/free/printf/new/delete 等的库 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘4.2 混用 /MT 和 /MD 为什么会崩溃?
┌─────────────────────────────────────────────────────────────────────────────┐
│ /MT 和 /MD 混用的灾难 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 假设你的程序用 /MD 编译,链接了一个用 /MT 编译的静态库: │
│ │
│ main.exe (/MD) mylib.lib (/MT) │
│ ┌──────────────────┐ ┌───────────────────┐ │
│ │ 调用库函数 │ │ │ │
│ │ create_object(){ │──调用────→│ Object* create() │ │
│ │ p = create(); │ │ { │ │
│ │ delete p; ←崩溃!│ │ return new Obj; │ ← 这个 new 用了 │
│ │ } │ │ } │ /MT 的 CRT │
│ │ // 这个 delete 用 │ └───────────────────┘ │
│ │ // /MD 的 CRT │ │
│ └──────────────────┘ │
│ │
│ /MT 的 .lib 有自己的 CRT 副本(有自己的 malloc 堆) │
│ /MD 的 .exe 有共享的 CRT(另一个 malloc 堆) │
│ │
│ 结果:库函数在堆A上 new,程序在堆B上 delete → 堆管理器不认识 → 崩溃 │
│ │
│ 更隐蔽的问题: │
│ • 一个模块的 fopen() 返回的 FILE* 给另一个模块用 → 可能崩溃 │
│ • 一个模块 setlocale() 不影响另一个 → 行为不一致 │
│ • 跨模块传递 STL 对象(如 std::string)→ Debug/Release 布局不同 → 崩溃 │
│ │
│ 铁律:一个进程中的所有 .exe 和 .dll 必须使用相同的 CRT 链接方式 │
│ (特别是 /MT vs /MD 不能混用) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘4.3 实际建议
┌─────────────────────────────────────────────────────────────────────────────┐
│ 运行时库选择建议 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ✅ 推荐 /MD(动态链接 CRT): │
│ • 多个模块(.exe + 多个 .dll)共享一份 CRT │
│ • .dll 和 .exe 之间可以安全传递堆对象 │
│ • 安全更新:Windows Update 可以修补 vcruntime.dll 的安全漏洞 │
│ • 缺点:部署时必须带 vcruntime140.dll 或要求用户安装 VC Redist │
│ │
│ ✅ 推荐 /MT(静态链接 CRT): │
│ • 单一 .exe 无需安装任何运行时 │
│ • 零依赖单文件部署 │
│ • 缺点:多个模块不能混用 /MT;exe 体积增大 │
│ │
│ 插件开发场景:必须统一用 /MD! │
│ • 宿主 .exe 和所有插件 .dll 必须用相同的 CRT 链接方式 │
│ • 否则宿主 new 的对象给插件 delete → 崩溃 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘第5部分:MSVC 工具链速览
5.1 核心工具
| 工具 | 全名 | 做什么 |
|---|---|---|
cl.exe | Microsoft C/C++ Compiler | 编译 .cpp → .obj |
link.exe | Microsoft Linker | 链接 .obj + .lib → .exe / .dll |
lib.exe | Microsoft Library Manager | 创建/管理 .lib(静态库或导入库) |
dumpbin.exe | Dump Binary | 查看 PE/COFF 文件的内部信息 |
ml.exe | Microsoft Macro Assembler | 汇编 .asm → .obj |
rc.exe | Resource Compiler | 编译 .rc(资源文件) → .res |
editbin.exe | Edit Binary | 修改已有 PE/COFF 文件的属性 |
5.2 dumpbin——你的二进制侦探
dumpbin /headers mylib.dll # 查看 PE 头信息
dumpbin /exports mylib.dll # 查看 DLL 导出了哪些函数(最重要!)
dumpbin /imports main.exe # 查看 exe 依赖哪些 DLL 的哪些函数
dumpbin /symbols mylib.lib # 查看 .lib 里的符号
dumpbin /dependents main.exe # 列出依赖的 DLL(类似 ldd)
dumpbin /disasm mylib.dll # 反汇编最常用的场景:甲方给的 .lib 是静态库还是导入库?用 dumpbin /symbols 看,如果符号是 __imp_ 开头 → 导入库;如果是普通函数名 → 静态库。
核心总结
┌─────────────────────────────────────────────────────────────────────────────┐
│ Windows 编译产物核心速查 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ .obj 编译产物(= Linux 的 .o) │
│ .exe PE 可执行文件,有入口点 │
│ .dll 动态链接库,没有 main,被加载 │
│ │
│ ⚠️ .lib 有两种,完全不同: │
│ 静态库 = .obj 的 tar 包,链接时嵌入代码 │
│ 导入库 = DLL 的目录,链接时只记录"这个符号去 XXX.dll 里找" │
│ 区分方法:dumpbin /symbols → 有 __imp_ = 导入库 │
│ │
│ dllexport / dllimport 宏模式 → 控制符号导出/导入 │
│ .def 文件 → 精确控制导出(序号导出、隐藏函数名) │
│ .pdb → 调试信息(调试时必须,发布时可不带) │
│ │
│ /MT = 静态链接 CRT → 每个模块有自己的 CRT │
│ /MD = 动态链接 CRT → 所有模块共享 CRT │
│ 绝对不能混用 /MT 和 /MD!跨模块 new/delete 会崩溃 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘章节测试
测试1:两种 .lib
Windows 上的 .lib 文件有哪两种?它们的本质区别是什么?你怎么区分一个 .lib 文件是哪种?
测试2:导入库的工作原理
导入库(Import Library)里有 add 函数的实际代码吗?程序运行时 add 的实际代码是怎么被找到的?
测试3:dllexport/dllimport 宏模式
下面这段代码有什么作用?MYLIB_EXPORTS 应该在什么时候被定义?
#ifdef MYLIB_EXPORTS
#define MYLIB_API __declspec(dllexport)
#else
#define MYLIB_API __declspec(dllimport)
#endif测试4:甲方给文件场景
甲方给了你 engine.h、engine.lib、engine.dll 三个文件。请说明:
- 编译你的程序时,需要哪些文件?
- 运行你的程序时,需要哪些文件?
engine.lib是哪一种 .lib?
测试5:/MT vs /MD
为什么 /MT 编译的 .exe 和 /MD 编译的 .dll 一起使用可能会导致崩溃?具体举一个会崩溃的场景。
测试6:工具使用
你拿到一个 .dll 文件,想确认它导出了哪些函数,用什么命令?
参考答案
测试1答案
答案:
- 静态库:.obj 文件的集合(类似 tar 包),链接时把实际代码嵌入到目标 .exe/.dll 中。
- 导入库:DLL 的"符号目录",只包含 stub 代码和导入信息,不含实际实现。
区分方法:用 dumpbin /symbols foo.lib 查看——如果有 __imp_ 前缀的符号,是导入库;如果看到完整函数名(非 __imp_),是静态库。也可以根据大小和是否有伴随的 .dll 来判断。
测试2答案
答案:不包含。导入库只包含指向 IAT 的跳转 stub。运行时:
- PE 加载器加载 .exe,发现依赖 .dll
- 加载 .dll 到进程地址空间
- 从 .dll 的导出表获取
add的实际地址 - 将地址填入 .exe 的 IAT 表
- 程序调用
add时通过__imp_add间接跳转到 IAT 中记录的地址
测试3答案
答案:这是 Windows DLL 开发的经典宏模式。MYLIB_EXPORTS 应该在编译 DLL 本身时定义(通过 CMake 的 PRIVATE 宏定义或编译器命令行 -DMYLIB_EXPORTS)。编译 DLL 时,MYLIB_API 展开为 __declspec(dllexport),把符号放入导出表。使用者 include 头文件时(没有定义 MYLIB_EXPORTS),MYLIB_API 展开为 __declspec(dllimport),告诉编译器符号来自外部 DLL。
测试4答案
答案:
- 编译时需要:
engine.h(声明) +engine.lib(导入库,确认符号存在) - 运行需要:你的 .exe +
engine.dll(实际代码) engine.lib是导入库(因为有伴随的 .dll)
如果运行时报"找不到 engine.dll",需要把 engine.dll 放到 .exe 同目录或 PATH 中。
测试5答案
答案:崩溃场景:.dll(/MT 编译)内部用 new 在它自己的 CRT 堆上分配了一个对象,然后返回指针给 .exe。.exe(/MD 编译)用完后 delete 这个指针,但它的 delete 调用的是共享 CRT 的堆管理器。共享 CRT 的堆不认识这个地址(它不在共享堆上),导致 heap corruption 或直接崩溃。铁律:所有模块(.exe + .dll)必须统一 CRT 链接方式。
测试6答案
答案:dumpbin /exports foo.dll,这会列出所有导出函数的名字、序号(ordinal)和 RVA(相对虚拟地址)。
相关笔记
- cpp project fundamentals - C++ 项目工作机制全景
- linux build outputs - Linux 编译产物详解
- static vs dynamic linking - 静态链接 vs 动态链接深度对比
- cpp abi compatibility - C++ ABI 兼容性
下一步学习
- [ ] 阅读 02 - Linux 编译产物详解 —— 对应理解 Linux 侧的 .o/.a/.so
学习状态:🟡 开始学习
工程深化:把本篇知识落到项目里
这一节不是为了凑篇幅,而是把《Windows 编译产物详解:exe、DLL、lib —— 彻底分清两种 lib / Windows Build Artifacts: EXE, DLL, and the Two Kinds of LIB》从“知道概念”推进到“能在项目里稳定使用”。 阅读时可以把每个知识点都追问成四件事:它保护什么边界,失败时有什么现象,怎样最小复现,怎样写进团队流程。
concept -> boundary -> failure signal -> minimal proof -> project rule工程切片 1:最小可复现样例
**场景。**围绕 DLL 建一个很小的案例,不要一开始就放进完整业务系统。把问题压缩到一个可以提交给同事的目录,保留源码、构建命令、版本输出和预期现象。
**边界。**先判断这里讨论的是 DLL、导入库 还是 PDB 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> DLL -> 导入库 -> PDB -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
本篇收束
掌握《Windows 编译产物详解:exe、DLL、lib —— 彻底分清两种 lib / Windows Build Artifacts: EXE, DLL, and the Two Kinds of LIB》的标志,不是记住所有名词,而是能把 DLL、导入库、PDB、运行库、导出符号、部署目录 放进一条可验证的工程链路。 先用最小样例建立判断,再用测试和脚本固定判断,最后把失败证据留给未来的自己和团队。