首页 / 资讯中心 / 文章详情

C++ exe工程改造为DLL/LIB:动态库静态库链接调用全解析

C++ exe工程改造为DLL/LIB:动态库静态库链接调用全解析 ★ FEATURED ARTICLE
能把事情干完的 C exe 工程到了需要复用的边界上几乎都会遇到同一个很现实的问题别人要的只是某个算法模块、某个解析逻辑、某个数据处理流程根本不需要整个程序界面也没兴趣把你的源码全部合到他的工程里。这时候把 exe 工程改造成动态库 dll或者静态库 lib就是最合理的工程化出路。这篇文章会完整记录我从一个能跑的 exe 控制台工程入手一步步改成 dll / lib并新建调用方项目成功跑通的全过程。文中涉及工程配置、符号导出、导入库生成、隐式链接、显式加载、运行时库一致性这几个核心环节适合已经能写出简单 C 工程、但还没有系统碰过“库化改造”的开发者参考。我分成三块来讲第一块讲清楚动态库、静态库和可执行程序的差异以及什么场景才值得动手第二块是完整的 dll 改造和两种调用方式实操第三块是 lib 静态库改造及常见报错速查。整个过程中我会把每一步操作背后的原因都说清楚尽量不给“照着抄也报错”的机会。1. 为什么非要把 EXE 改成 DLL / LIB1.1 动态库和静态库到底差在哪里首先把三个名词摆到一起看。exe 是最终可执行程序双击就能跑通常带 main 或 WinMain 入口dll 是动态库编译后是一个 PE 文件里面装着可执行代码、数据、资源但它不能自己独立运行必须由某个 exe 或其他模块加载它lib 则分成两种一种是静态库本质是多个 .obj 目标文件的打包集合链接时会被整体复制进 exe另一种是导入库它本身基本没有代码只记录 dll 里导出了哪些符号、符号在哪个函数里供链接器在生成调用方 exe 时做“对表”用的。我以表格形式把我们最关心的差异列出来对比项exedll静态库 lib入口点必须有 main/WinMain导出若干函数无独立入口无入口提供一堆目标文件能不能独立运行能不能不能加载时机程序启动启动时或运行中链接阶段直接合入 exe发布形态一个可执行文件dll 导入库 lib 头文件lib 头文件模块升级要重新替换整个 exe只替换 dll 文件必须重新链接整个程序多进程共享代码各自一份操作系统按引用计数共享一份每个进程都复制一份代码链接期依赖需要完整源码或符号库需要导入库 lib 或显式加载需要静态库 lib看到这里你大概能明白改造过程的关键不是“把工程后缀改一下”而是想清楚你希望外界怎么使用这份代码。如果将来可能频繁更新功能dll 优先如果只是把代码稳定地分离出去、不打算单独更新某个模块静态库 lib 更省事。1.2 适合动手改造的几种场景从我个人接触过的项目看这类改造常出现在下面几种场景第一一个大的 exe 里有一部分算法逻辑比较独立比如图像处理、协议解析、数据校验别的项目也要用同一段逻辑又不想复制粘贴因为复制粘贴会导致同一个 bug 修三遍。把独立模块抽出来做成 dll 或 lib所有调用方共用同一份实现是标准解法。第二项目想插件化希望新增功能时不用重新编译整个主程序只要放一个新的 dll 到目录里主程序扫描目录动态加载即可。这是典型的动态库显式调用场景背后靠的就是 LoadLibrary / GetProcAddress 这类运行时加载能力。第三写单元测试时不想把被测代码全编译进测试框架而是把被测模块单独做成 lib 或者 dll测试工程链接它这样被测代码的编译粒度更清晰换编译选项也方便。第四给非 C 语言调用。比如 C# 通过 P/Invoke 调用 C dllPython 通过 ctypes 或 cffi 调用 C dll都需要 dll 提供 C 语言风格导出接口。这类跨语言调用要求导出符号不做过多的 C 名字修饰我们在后面讲导出方案时会反复强调这一点。动手之前先确认自己手里有完整源码以及可用的编译环境。没有源码、只有 exe 的情况下做不了这种改造因为库必须从源码重新编译产物。想直接从别人的 exe 中“抠”出代码那是逆向工程的范畴不在本文讨论范围内。2. 动手前必须搞清楚的三个关键概念2.1 编译期符号、导入库和运行时查找很多刚开始接触库化改造的人会对“库文件里到底有没有代码”感到混乱。其实这里藏着两个完全不同的阶段编译链接期和运行期。在链接阶段调用方 exe 如果需要调用 dll 里的函数它的链接器必须提前知道这个函数叫什么名字、参数列表长什么样、去哪个库文件里找所以你会看到投递 .dll 时通常要附带的导入库 .lib。这个 .lib 不是实现而是“地址簿”它记录了 dll 里导出的符号和重定位信息。调用方链接时拿着地址簿生成了一个桩真正实现代码还在 dll 里只有在程序启动加载 dll 后才会建立最终关联。静态库 .lib 则完全不同链接器会把静态库中需要的目标文件直接复制到最终 exe 里所以拿到静态库编译出的 exe 不需要再额外携带那个 lib。理解这个区别后你就不会在调用 dll 时奇怪“为什么还要链一个 lib”。运行期加载存在两种方式本文后面都会实操隐式链接编译链接调用方时就指定导入库程序启动时系统自动加载 dll。这是常规情况默认首选。显式加载调用方编译时不提供导入库运行到某一行代码时才用 LoadLibrary 加载 dll再用 GetProcAddress 拿函数地址。这种方式灵活适合插件系统但开发时要自己维护函数指针类型比较麻烦。2.2 工具准备和工程结构规划实践中我用得最多的就是 Windows 平台下 VS 开发环境和 MSVC 编译器这里先以 VS 工程为例解释。当然用 CMake 也可以改造成动态库工程本质思路相同只是配置方式从图形界面变成脚本为了不把篇幅无限拉长我会把重点放在 VS 工程上后面也会简要补充 CMake 的等价配置。开始之前需要准备好三样东西一份能正常编译成 exe 的 C 源码工程这是改造的起点一个可用的 C 开发环境版本不必很新能编 32 位和 64 位 Release/Debug 即可一个用于验证调用的新工程小的空控制台程序就行后面所有调用演示都在它上面做。规划结构时我建议不要直接把原有源码全部塞进库工程。除非你的工程天生就是那种“一个文件包含所有逻辑”的小玩具否则更稳的做法是把公共接口单独抽到一个头文件比如 api.h里面只放外部需要使用的函数声明或类声明实现文件留在内部。这样改造之后的发布物就非常干净库文件 头文件 对应的导入库/静态库。2.3 工程的“配置类型”到底是什么VS 工程里有一个很不起眼的设置项叫“配置类型”。默认新建的应用程序项目里这个值被设置为“应用程序(.exe)”链接器输出的是一个带 exe 后缀的 PE 文件。我们要做的第一步就是把这个值改成“动态库(.dll)”或“静态库(.lib)”。很多人会问直接改这个下拉框就能生成 dll 吗答案是可以生成但能不能被正确调用还要看符号导出。原因很简单exe 工程生成可执行文件时不需要输出对外接口清单而 dll 必须明确告诉外面的世界“我这里有哪些函数是可以被调用的”。如果没有导出任何符号生成的 dll 就是个大空壳链接时你会得到一个:LNK4197: 未导出任何内容的警告调用方无论如何也链接不进来。所以配置类型只是第一步真正决定能不能调用的是如何导出符号。下面进入核心实操部分。3. 从 EXE 改造到 DLL 的完整实操3.1 步骤一修改工程配置生成 dll打开原本能编译通过的那个 exe 工程找到工程属性页确认当前活动配置是自己需要的平台比如 Release x64。在左侧依次进入“配置属性” → “常规”把“配置类型”从“应用程序(.exe)”改为“动态库(.dll)”。改完配置后顺手做两件事避免后面编译报不必要的错误检查字符集设置。如果原工程用多字节字符集调用方可能用 Unicode两种情况下字符串处理方式不同建议统一改成“使用 Unicode 字符集”接口里字符串参数尽量使用宽字符类型或者明确约定使用 UTF-8 字节流。检查运行库设置。进入“配置属性” → “C/C” → “代码生成” → “运行库”可以看到 /MT、/MTd、/MD、/MDd 四个选项。dll 工程推荐使用“多线程 DLL(/MD)”这和后面调用方工程保持一致否则容易出现“堆被另一头分配、另一头释放”的经典崩溃。静态库工程则要格外注意调用方也要选同样的运行库方式这个我放到静态库一节详细说。此时直接编译大概率能生成 dll 文件同时你会在输出目录看到三个文件xxx.dll、xxx.lib、xxx.exp。这个自动生成的 xxx.lib 就是导入库xxx.exp 是导出文件用于记录导出信息的中间产物通常我们不需要对外发布它。但注意如果我们不改任何源码这个 dll 通常是空的断言方式是看编译日志里的警告。所以迈入下一步让代码真正导出可用符号。3.2 步骤二导出函数和类的两种主流方式在 Windows 上导出符号最直观的方式是使用__declspec(dllexport)。为了让同一个头文件既能被 dll 内部使用又能被外部调用方使用一般会配合__declspec(dllimport)写一个宏模式如下// api.h #ifdef MYLIB_EXPORTS #define MYLIB_API __declspec(dllexport) #else #define MYLIB_API __declspec(dllimport) #endif MYLIB_API int Add(int a, int b); #ifdef __cplusplus extern C { #endif MYLIB_API int Multiply(int a, int b); #ifdef __cplusplus } #endif这个模式理解起来不难MYLIB_EXPORTS 是工程里定义的预处理器宏。在 dll 工程内部编译时MYLIB_EXPORTS 存在MYLIB_API 被展开为 dllexport告诉编译器“这些函数是向外部公开的”在调用方工程里MYLIB_EXPORTS 不存在MYLIB_API 变成 dllimport告诉链接器“这些函数在外部 dll 里先在导入库里对表”。extern C的作用很多人第一次接触会忽略。C 编译器为了支持函数重载会把函数名和参数类型揉成一个修饰名比如 Add 可能变成?AddYAHHHZV 到调用方这边如果两边编译器版本或调用约定不一致这个修饰名很难对得上。加上 extern C 后函数采用 C 语言命名规则导出名就是干净的 Add调用方通过导出名查找也容易得多。跨语言调用、显式 LoadLibrary 时强烈建议所有导出函数都用 extern C 包起来。如果你的函数要导出的是整个类就不能用 extern C 了类成员函数的修饰名由编译器统一处理。只要调用方和 dll 用同一套编译器体系通常没问题。类导出写法是class MYLIB_API MathHelper { public: int Add(int a, int b); int Subtract(int a, int b); };写成这样类的所有非私有成员函数会被默认导出。要注意的是导出类会连带要求类的内部结构对外可见一旦类内部增减成员变量所有调用方都要重新编译所以导出类通常适合内部使用不适合跨度很大的跨语言场景。第二种导出方式是用模块定义文件.def。如果你有一些历史原因不方便在代码里写导出宏或者希望精确控制导出序号可以新建一个文本文件后缀改成 .def然后在工程属性里把它加进链接器的“模块定义文件”选项内容大致如下EXPORTS Add Multiply采用 .def 文件后是否加 extern C 就不那么关键了因为 .def 明确列出了要导出的符号名。这种方式对大型存量代码改造更友好不需要改头文件里的每个声明。缺点是 .def 文件要求开发者自己维护符号清单漏一个符号就会导致调用方链接失败。我个人的建议是新项目优先用宏导出因为声明和导出动作放在一起天然同步老项目或跨语言场景如果名字修饰让你头疼用 .def 文件直接钉死导出名会更省心。3.3 步骤三验证生成的 dll 确实导出了符号编译通过后在输出目录里找到 dll先别急着调用用一个命令行工具看一眼导出表。VS 环境自带的 dumpbin 可以做到dumpbin /exports xxx.dll输出里会列出 dll 中所有导出符号。如果 Add 和 Multiply 以导出名形式出现在列表中说明导出成功。用 .def 文件时这里会看到 MANUAL 格式的干净名字用默认 C 而非 extern C 时会看到类似?AddYAHHHZ的修饰名。看到修饰名不用慌这说明编译器按 C 规则处理了调用方如果也用 C 链接理论上能找到但显式调用时写函数指针就需要查修饰名非常不直观所以我还是建议大家能加 extern C 就加。到这里一个合格的 dll 发布物已经生成xxx.dll、导入库 xxx.lib、公共头文件 api.h。4. 调用 DLL 的第一种方式隐式链接4.1 什么是隐式链接所谓隐式链接就是在调用方工程编译阶段就告诉链接器“我要用某个 dll 的某个函数”让系统在程序启动时自动加载 dll。调用方代码里不需要出现 LoadLibrary只需要吃两个东西函数的声明头文件以及一个导入库 lib 文件。为什么有了 dll 还要额外用 lib因为链接器在生成 exe 时需要知道函数地址在哪个模块、调用时怎么生成跳转指令。这个信息记录在导入库 lib 里。编译器从 lib 中提取必要信息在 exe 的导入表里注册“这个进程需要 xxx.dll”运行加载器看到导入表条目后自动加载 dll。所以隐式链接的本质是“编译期登记启动期加载”。4.2 最小调用 Demo 完整走一遍新建一个空控制台工程把 api.h 和 xxx.lib、xxx.dll 都拷贝到合适目录。在调用方工程属性里做两个配置“配置属性” → “链接器” → “常规” → “附加库目录”添加 lib 所在文件夹“配置属性” → “链接器” → “输入” → “附加依赖项”填写 xxx.lib。如果不想改工程配置也可以在源码里用#pragma comment(lib, xxx.lib)手动指定这个方法在临时验证时特别方便不用反复打开属性页。现在写调用代码// main.cpp #include cstdio #include windows.h #include api.h int main() { int sum Add(3, 4); int product Multiply(3, 4); printf(Add(3,4) %d\n, sum); printf(Multiply(3,4) %d\n, product); return 0; }编译并运行。如果工程能正常生成 exe运行后输出正确结果说明隐式链接成功。整个过程里你不需要关心 Add 的实现代码它住在 dll 里。写这段代码时要注意 include 的顺序。Windows.h 和 api.h 一般没冲突但在一些大项目里建议先 include 自己的 api.h再 include windows.h否则可能出现个别宏重定义的干扰虽然这个情况不常见。4.3 隐式链接最容易踩的几个坑隐式链接阶段最典型的错误是 LNK2019无法解析的外部符号。这个错误几乎都是“链接器不知道该去哪找符号”导致的。常见原因有三个一是忘了挂导入库 lib或附加依赖项写错名字。这时候链接器在命令行里找不到 xxx.lib自然报无法解析。二是导入库文件确实存在但里面记录的符号名和调用方声明不一致。比如 dll 内部没有加 extern C导出名是?AddYAHHHZ而调用方声明的是 Add链接器找?AddYAHHHZ时找不到对应的符号记录就会报错。解决办法是统一符号处理方式或改用 .def。三是调用约定不一致。函数声明里如果默认是 cdecl但 dll 导出时被某处定义为 stdcall名字修饰规则就变了。查看函数互相匹配的方式保持默认 cdecl或者一方明确用__stdcall、另一方也用一致声明不要一半默认一半显式指定。除了链接错误运行期也可能出现找不到 dll 的黑框弹窗。隐式链接的 dll 必须在程序启动时被加载系统会按“exe 所在目录 → 系统目录 → PATH 环境变量目录”的顺序搜索。调试时如果 dll 没复制到 exe 所在目录就一定会报由于找不到 xxx.dll无法继续执行代码。一开始遇到这个问题别慌把 dll 复制到 exe 同目录或者把工程输出目录改成同一个即可。5. 调用 DLL 的第二种方式显式加载5.1 什么时候需要显式加载隐式链接虽然简单但它有一个天然约束程序只要一启动dll 必须存在、必须能加载否则整个进程直接失败。这对常驻进程、面板系统、插件系统来说太苛刻了。插件系统通常希望主程序先启动然后扫描目录发现有哪个插件 dll 就加载哪个某个插件损坏了最多跳过不该让主程序整体崩溃。这种场景就要用显式加载。显式加载的好处很明显运行时按需加载不用的时候不占内存可以灵活处理加载失败不至于启动即崩溃便于实现插件“热插拔”概念至少加载和释放层面是自主控制的。坏处是代码量增加要手写函数指针类型还要小心保持函数指针签名与 dll 导出完全一致。5.2 LoadLibrary GetProcAddress 的完整写法还是刚才的 xxx.dll假设里面有 Add 函数。显式加载的调用代码长这样#include cstdio #include windows.h typedef int (*AddFunc)(int, int); int main() { HMODULE hDll LoadLibraryW(Lxxx.dll); if (hDll NULL) { printf(加载 dll 失败错误码: %u\n, GetLastError()); return 1; } AddFunc add (AddFunc)GetProcAddress(hDll, Add); if (add NULL) { printf(找到 Add 函数失败\n); FreeLibrary(hDll); return 1; } int sum add(3, 4); printf(Add(3,4) %d\n, sum); FreeLibrary(hDll); return 0; }关键点有三个。第一LoadLibraryW 而不是 LoadLibrary。在 Unicode 工程里宽字符版本是推荐写法避免遇到中文路径时出现 ANSI 转换问题。调用之前确认 dll 路径正确如果 dll 不在当前目录最好传完整绝对路径或者用相对路径仔细核对。第二函数指针类型必须和 dll 内部实现一致。如果 Add 原型是 int Add(int, int)函数指针就定义成 int (*)(int, int)。如果约定不匹配轻则拿回错误结果重则栈不平衡导致崩溃。C 的调用约定默认 cdecl这也是为什么我在导出代码里特意强调 extern C 默认调用约定比较好。第三GetProcAddress 查找字符串是导出名不是 C 修饰名。所以导出时需要注意命名一致性。若你用 .def 文件指定导出名那显式加载时填的字符串也必须是 .def 里的那个名字。5.3 显式加载的常见崩溃点和调试技巧显式加载比隐式链接更容易出现“看似调用成功结果数据全错”的诡异问题根源大多是调用约定。比如 dll 导出时用了 __stdcall却忘了在函数指针类型上标注 __stdcall调用方按 cdecl 方式清理栈系统内存布局就会错乱。所以一旦出现返回值莫名其妙第一件事就是检查组里的调用约定标记是否一致。另一个经常踩的坑是 dll 内部的依赖。假设你的 dll 依赖了另一个第三方 dll磁盘上只有 xxx.dll 而没有它的依赖文件。隐式链接时启动就报缺 dll显式加载时 LoadLibrary 也会失败返回 NULL但很多人会误以为是 xxx.dll 本身有问题。这时候可以用工具查看 xxx.dll 的导入表或者试着把依赖库放到同一目录再试一次 LoadLibrary。调试建议在 LoadLibrary 返回 NULL 后马上调用 GetLastError 看错误码这能大幅缩短排查时间在 GetProcAddress 返回 NULL 后检查导出名拼写以及导出类别必要时再用 dumpbin 查看当前 dll 实际的导出名。我之前就遇到过把“Add”误写成分号后面带空格的情况导出名没有空格但 GetProcAddress 参数里多了空格迟迟找不到函数。这种细节问题肉眼很难发现用 dumpbin 对一遍就清楚了。6. 再从 EXE 改造到静态库 LIB 并调用6.1 动态库和静态库在实际工程里的定位差异讲完 dll再回来看静态库 lib。静态库不像 dll 那样有一个“加载时间”它是在链接期被直接合并进调用方 exe 的。链接完成后的 exe 不再对 lib 有任何运行时依赖这是它最大的优势部署简单、没有路径问题、也不怕缺 dll。代价是发布体积变大、任何静态库内部改动都需要完整重链。如果你的代码是稳定底层逻辑比如公司内部一个公共数学库、公共加密库合进 exe 没问题但如果这个模块本身更新频率高或者外部有多个团队需要独立升级它静态库就不太合适应该考虑 dll。6.2 把 exe 改造成静态库的操作步骤在原有 exe 工程里再次打开工程属性配置类型改成“静态库(.lib)”。这里有一项必须注意链接器阶段不再需要“生成 DLL”相关的设置同时代码里如果还写着__declspec(dllexport)对应宏建议改成空定义因为在生成静态库时 dllexport 没有意义甚至会在个别编译器版本里引发告警。项目如果原先有#ifdef MYLIB_EXPORTS那种宏可以在静态库构建时不定义 MYLIB_EXPORTS让 MYLIB_API 展开为空这样头文件在外部调用方和静态库构建方看到的都是纯声明不涉及导入导出指令。改完后直接编译输出目录只会得到一个 xxx.lib。这个 lib 是真正的静态库里面包含了所有实现目标文件的字节码。可以再用工具查看里面的目标文件列表lib /list xxx.lib看到列表里有若干 .obj 就证明这是静态库而不是导入库。这个验证方法很实用能帮你确认自己拿到的到底是哪种 lib。6.3 静态库调用 Demo调用方工程里只需要以下配置附加库目录指向 lib 所在目录附加依赖项填 xxx.lib。源码里和之前类似#include cstdio #include api.h int main() { int sum Add(5, 6); int diff Subtract(9, 4); printf(Add(5,6) %d\n, sum); printf(Subtract(9,4) %d\n, diff); return 0; }编译运行一切正常的话exe 会完全包含静态库里的实现之后你再删除 xxx.lib这个 exe 照样可以运行。这一点和 dll 调用形成鲜明对比建议读者亲手试一下体会两类库的部署区别。6.4 静态库调用最容易踩的运行库一致性大坑静态库调用有一个比 dll 更敏感的问题运行库一致性。具体来说静态库编译时用的运行库选项必须和调用方 exe 一致。如果库用 /MT多线程静态链接编译调用方却用 /MD多线程 DLL编译链接阶段大概率会报错提示你在一些 runtime 函数上有冲突定义就算侥幸链接通过运行时也可能因为两边各持有一份堆管理数据出现释放问题。这一点和 dll 不同。dll 内部即使使用静态运行库由于它的内存边界相对独立一般不会直接引发调用方的崩溃但静态库的代码直接合并进调用方进程两边运行库不一致就是实打实的隐患。所以关于静态库记住一条铁律库方和调用方的运行库选项必须统一Debug 版配 DebugRelease 版配 Release不要把 Release 版静态库拿给 Debug 调用方用也不要反着来。为保险起见我一般会在调用方工程属性里手动把运行库选项和静态库工程对齐而不是依赖默认值。7. 常见报错与避坑实录下面把我在实际操作里最常见到的错误按表格整理了一遍每条都是曾经真正让我卡壳过的例子现象/报错直接原因解决办法LNK2019 无法解析的外部符号没链导入库或符号名不匹配添加附加依赖项检查导出名和 extern CLNK2001 无法解析的外部符号 __imp_xxx导入库确实没有导出该函数用 dumpbin /exports 确认 dll 导出表LNK4197 未导出任何内容改了配置类型但没导出符号补充 dllexport 或 .def 文件LNK1112 模块计算机类型冲突x86/x64 库混用调用方和库方平台必须一致启动时弹“找不到 xxx.dll”隐式链接加载失败把 dll 复制到 exe 目录或者用显式加载LoadLibrary 返回 NULLdll 文件缺失或依赖缺失检查目录、查看 GetLastError、检查依赖GetProcAddress 返回 NULL导出名拼写错误或调用约定不一致dumpbin 查看导出表校准函数指针运行结果随机崩溃调用约定不一致或运行库不一致统一 __cdecl/__stdcall统一 /MT 与 /MD中文路径下找不到 dll使用 ANSI 字符串传路径改用 LoadLibraryW 和宽字符路径除了报错还有一种比报错更隐蔽的问题符号可见性。Windows 下默认只要用 dllexport 就能导出但在 Linux/Mac 上用 GCC 编译动态库会有隐藏符号默认策略。如果你将来要跨平台建议从一开始就养成“显式导出”的习惯用统一的 API 宏而不是保留默认导出全部符号的老写法。另外调试期间如果用 Release 版 dll 去调试 Debug 版调用方虽然常见运行库不匹配不一定会立即崩溃但排查问题时非常难受。尽量让 Debug 对应 DebugRelease 对应 Release开发周期里把两套配置的构建产物分开能省很多精神消耗。8. 一些实操心得的最后总结最后再分享几条我自己在反复折腾 dll / lib 时沉淀下来的经验。第一所有对外 API 的头文件建议只放声明不放实现。有人为了图方便直接把函数体也写在头文件里结果导入库和静态库都用不上各调用方反而各自编译出一份实现彻底背离了库化复用的目的。保持声明与实现分离既能让发布物干净也能规避一些奇怪的链接冲突。第二启动阶段就把 dll 加载策略想清楚。如果调用方工程是长期维护的主程序我一般优先考虑显式加载并且在启动时对每个依赖模块做加载检测、打印友好的错误提示如果是快速验证的小工具才用隐式链接省事。显式加载虽然代码多一些但换来的是故障隔离能力长期维护时很值得。第三务必在 CI 或构建脚本里把“拷贝 dll 到输出目录”这一步固化下来。很多调用方工程在本地编译成功后换台机器就报缺 dll就是因为没有统一发布流程。在 VS 里可以写后期生成事件命令或干脆用 CMake 的 POST_BUILD 命令复制文件这一步投入很小收益很大。我最早改造 lib 工程时因为运行库配置没对齐折腾了整整一个下午最后把 /MT 改成 /MD 才解决问题。如果你读完这篇后去动手一定要记得先检查配置类型、再检查导出符号、最后检查运行库一致性。只要按这个顺序做大部分坑其实都能避开。
阅读完成 · 觉得有帮助?
咨询建站