资讯动态

VS2019静态库与动态库配置全解析:从lib、dll到LNK2019排查

发布时间:2026/9/9 4:03:28 来源:尧图企业网站定制
在VS2019里折腾静态库和动态库这件事我本来以为自己已经很熟了直到前段时间接了一个集成第三方C库的活链接器突然冒出一堆LNK2019才老老实实把整个流程重新捋了一遍。后来发现不管你是用VS2019做上位机、写Qt程序、接ONNX Runtime还是给UG二次开发配环境核心套路都差不多把一堆.lib、.dll、.h文件按VS的脾气塞到正确的位置让它能看见、能链接、能运行。这篇文章就是把我实际操作中总结下来的用法、踩过的坑、排查思路全部整理出来作为一份可以直接照着操作的笔记。适合刚学会写一个main程序的入门者也适合已经接触过库、但一直被各种链接错误折磨的同学。1. 先把“静态库”和“动态库”的真实关系说清楚1.1 “.lib”不一定代表静态库这点最容易搞混很多新手看到.lib文件就以为是静态库其实不是VS生成的动态链接库里也会带一个.lib文件我们叫它“导入库Import Library”。这两个东西虽然都叫.lib内部结构和使用方式完全不一样。静态库的本质是一个“目标文件.obj的压缩包”。你在项目里写的那一堆.cpp文件经过编译后会生成一个个.obj文件链接器把可能用到的.obj挑出来打包成一个.lib。最终做exe时链接器从.lib里找到需要的.obj内容直接复制到你的exe程序里。也就是说静态库的代码是“物理拷贝”进目标程序的之后你把这个.lib删了exe照样能运行。而动态库的.lib导入库里面没有真正的函数实现只有“这个函数在哪个dll里、叫什么名字、参数列表长什么样”这类符号信息。真正干活的是.dll文件里面才有函数对应的机器码。当exe被加载时Windows系统会根据导入库记录的符号信息去加载对应的dll并在内存中把函数地址和你的调用点对接上。这个dll文件和exe是分开的exe运行时必须有它否则直接报“找不到xxx.dll”。理解了这一点后面配置属性就不会再懵了。你说“我想用这个动态库”其实有两件不同的事要做链接期要让VS找到.lib导入库运行期要让Windows找到.dll这俩文件缺一不可。1.2 什么时候选静态库什么时候选动态库选型没有绝对标准但我的经验是看“给谁用”和“怎么发布”。如果库是你自己项目内部用而且生成的是一个单独的可执行程序优先用静态库。静态链接的程序发布时只需要拷贝exe一个文件不担心目标机器上缺dll的问题。缺点是exe体积会膨胀而且如果有多个exe共用同一份静态库每个exe里都有一份代码副本内存占用会大一些。如果是给第三方提供SDK或者想把公共模块拆出来给多个团队动态更新用动态库更合适。动态库可以在不重新编译主程序的情况下单独替换dll前提是你没有改动接口升级bug和加功能都灵活。缺点是部署的时候漏一个dll程序就起不来Windows上还得处理各种“运行库版本不一样”的问题后面我会细说。还有一个比较实际的选择标准看你和别人约定的编译环境。用MSVC编译器生成的静态库换一个编译器比如MinGW去链接通常很痛苦因为C的符号命名规则、标准库ABI都不一致。动态库虽然同样有ABI问题但如果用extern C导出纯C接口被各种语言和工具链调用的兼容性就会好很多。1.3 VS2019环境准备别只装了编辑器就开始既然是VS2019的使用环境本身先谈几句。很多人从网上下载VS2019离线安装包装完发现新建项目里找不到C相关模板大概率是安装时没勾工作负载。要用静态库、动态库这些能力至少需要在“使用C的桌面开发”这个工作负载里把“适用于最新v142生成工具的C生成工具”和“Windows 10 SDK”选上否则编译器和Windows头文件都不存在。另外建议在“单个组件”里勾一下“C MFC”或者“C ATL”——不是所有人都用得上但万一后续接的第三方库依赖这些框架缺了它编译时能给你整出几百行“无法打开包括文件: afxwin.h”的错误。VS2019的安装确实麻烦装好了后面会省心很多。缺组件重装属于浪费时间我第一次离线安装时没注意后来为了一个MFC项目硬是又花了几个小时补环境。2. 动手做一个静态库并在VS2019里调用2.1 新建静态库项目的正确姿势在VS2019里创建静态库我建议的路径是新建项目搜索“静态库”选“静态库项目”模板如果没有单独这个模板也可以选“Windows桌面向导”在向导第二页的“应用程序类型”里勾选“静态库”。这里有一个比较隐蔽的坑如果你用的模板生成了一个pch.h和pch.cpp说明这个项目默认启用了预编译头。预编译头会导致你每次写代码都要记得#include pch.h而且生成第三方静态库时这个pch依赖还挺烦的。如果只是做一个小型库我建议在项目属性 → C/C → 预编译头 中把“预编译头”改成“不使用预编译头”再顺手把pch相关文件删掉。等以后你真正维护大型项目再启用它也不迟。静态库项目本身不要写main函数。它编译出来不是可执行程序而是一个.lib文件。有些新手在一个库工程里写了个int main()然后连接器报“main已经定义”或者“无法解析外部符号main”原因就在这里。库的代码就是一组函数、类的“零件仓库”谁来链接谁负责main。2.2 静态库源码、生成与产出物我创建一个叫MathTools的静态库工程放了两个很简单的函数// MathTools.h #pragma once namespace MathTools { int Add(int a, int b); int Multiply(int a, int b); }// MathTools.cpp #include MathTools.h namespace MathTools { int Add(int a, int b) { return a b; } int Multiply(int a, int b) { return a * b; } }编译之后到工程的输出目录一般是x64/Debug或x64/Release能看到一个MathTools.lib文件。整个静态库的产出物就它一个头文件MathTools.h是给别人看接口用的不算必须要交付的文件但不带头文件的话别人根本不知道这个库里有啥函数。这里顺便说一下配置问题。我强烈建议你一开始就确定好是生成Debug还是Release版并把它跟调用方工程保持一致。用Debug版本的.lib去链接Release的exe编译通常能过但运行时时不时会冒出内存错误、堆损坏之类的诡异问题因为这些库内部链接的是调试版C运行库和发布版的布局不一样。排查起来特别让人头大。2.3 调用静态库的三种方法和头文件组织首先得把库的头文件给调用工程看到。最简单的一种做法把MathTools.h和MathTools.lib拷到调用工程目录下然后在源代码里用#include ../MathTools/MathTools.h这种相对路径。我个人不太推荐一旦目录结构变化所有include路径全得改维护成本高。正规做法是通过项目属性配置目录整个过程分三步第一步C/C → 常规 → 附加包含目录填MathTools.h所在的文件夹。 第二步链接器 → 常规 → 附加库目录填MathTools.lib所在的文件夹。 第三步链接器 → 输入 → 附加依赖项填MathTools.lib写完整文件名。也有人用#pragma comment(lib, MathTools.lib)来代替第三步。这个指示符的本质是告诉链接器“帮我去这个文件名对应的库里找符号”省得你去属性页点那一堆窗口。我平时自己写demo时会用因为快。但要注意一点如果同一个库Debug版和Release版名字不同比如MathTools_d.lib和MathTools.lib你把名字写死在#pragma comment里会有点麻烦这时候还是老老实实用属性页配置用宏来做区分。代码里的调用方式和平常写本地函数没有区别引用了头文件之后直接用#include iostream #include MathTools.h int main() { int sum MathTools::Add(10, 20); int product MathTools::Multiply(3, 4); std::cout sum sum std::endl; std::cout product product std::endl; return 0; }如果你是在同一个解决方案里同时打开“MathTools”和“调用方”两个工程还有更省事的方法右键调用方工程 → 添加 → 引用 → 勾选MathTools项目。VS会自动帮你处理头文件路径和lib依赖。这种方式在调试时特别方便你改了静态库源码按F5生成时会自动先编译库再编译主程序。3. 动态库从生成到调用完整手记3.1 写一个带导出接口的动态库项目动态库的创建其实和静态库类似但关键多了一个“导出”。当别人调用你的dll时系统得知道哪些函数是公开放出去的这就要用到__declspec(dllexport)这个关键字。通常一个库工程内部定义导出宏像这样// StrTools.h #pragma once #ifdef STRTOOLS_EXPORTS #define STRTOOLS_API __declspec(dllexport) #else #define STRTOOLS_API __declspec(dllimport) #endif #include string class STRTOOLS_API StrTools { public: static std::string ToUpper(const std::string input); static std::string ToLower(const std::string input); };注意这个STRTOOLS_EXPORTS宏。VS2019创建动态链接库项目时会自动为项目定义一个项目名_EXPORTS的预处理器宏。在这个dll工程内部编译时STRTOOLS_EXPORTS是存在的所以StrTools.h在编译自身源码看到的是dllexport会把类的所有符号导出到dll中而当外部工程包含这个头文件时STRTOOLS_EXPORTS不存在于是StrTools.h自动变成dllimport告诉编译器“这些符号来自外部的dll”。.cpp实现// StrTools.cpp #include StrTools.h #include algorithm #include cctype std::string StrTools::ToUpper(const std::string input) { std::string result input; std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { return static_castchar(std::toupper(c)); }); return result; } std::string StrTools::ToLower(const std::string input) { std::string result input; std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { return static_castchar(std::tolower(c)); }); return result; }编译之后去输出目录看看里面有三个重要文件StrTools.dll、StrTools.lib、StrTools.exp。其中.dll是运行时真正需要的.lib是链接期需要的导入库.exp是增量链接用的导出文件一般不需要亲自部署。3.2 隐式链接三步配好include、lib、dll调用动态库有两种方式第一种叫“隐式链接”也叫“静态加载”。它和调用静态库的配置很相似工程属性里一样要填三个地方附加包含目录填.h文件目录附加库目录填.lib文件目录附加依赖项填StrTools.lib。头文件和代码中直接调用#include iostream #include StrTools.h int main() { std::string s Hello, VS2019!; std::cout StrTools::ToUpper(s) std::endl; return 0; }但仅仅这样还不够编译能过一运行会报“找不到StrTools.dll”。因为链接器从导入库里知道函数在哪个dll里但系统加载exe时要在exe所在目录、系统PATH环境变量、或Windows系统目录里找这个dll。通常做法是把dll文件复制到exe的输出目录也就是和exe同目录。如果嫌每次手动拷贝麻烦可以在项目属性 → 生成事件 → 后期生成事件命令行里写一条拷贝命令copy /Y $(SolutionDir)x64\$(Configuration)\StrTools.dll $(OutDir)这样每次编译完自动把dll送到exe身边。之后F5运行就顺利了。3.3 显式加载插件式调用跑通LoadLibrary第二种调用方式叫“显式链接”也叫“动态加载”。这种方式根本不用管.lib导入库而是程序运行到某个时刻自己调用Windows API 把dll加载进来然后用函数指针找到函数地址再调用。相当于把“找函数”这个动作从程序启动延迟到了运行期间。代码长这样#include windows.h #include iostream typedef std::string (*ToUpperFunc)(const std::string); int main() { HMODULE hModule LoadLibraryA(StrTools.dll); if (hModule NULL) { std::cerr LoadLibrary failed, code: GetLastError() std::endl; return 1; } auto pFunc reinterpret_castToUpperFunc(GetProcAddress(hModule, ToUpper)); if (pFunc NULL) { std::cerr GetProcAddress failed std::endl; FreeLibrary(hModule); return 1; } std::string result pFunc(hello); std::cout result std::endl; FreeLibrary(hModule); return 0; }这里有个大坑上面的示例代码有一个很微妙的错误。C类和成员函数在编译器眼里符号名会被“改编name mangling”成乱七八糟的名字GetProcAddress(hModule, ToUpper)根本找不到导出符号。所以在设计动态库接口时如果要支持显式加载和跨语言调用我强烈建议用extern C导出一组普通函数作为C接口然后库内部再用C实现extern C { __declspec(dllexport) const char* ToUpperString(const char* input); __declspec(dllexport) const char* ToLowerString(const char* input); }这里还需要一个额外的函数申请缓存用完后释放否则调用者不知道释放谁的内存。其实动态库跨模块内存管理本身就是一大坑最好的做法是导出“创建/销毁句柄”这样的成对函数。示例的话我可以再开一篇文章细讲今天先点到为止。用显式加载的好处是程序启动时不检查dll是否存在可以在运行时根据配置动态切换实现适合插件架构。缺点是代码繁琐每一次函数调用都要GetProcAddress维护成本高。所以实际项目里绝大多数情况用隐式链接就够了。3.4 动态库运行库/MT、/MD与部署需要重点注意新手经常忽略一个属性项目属性 → C/C → 代码生成 → 运行库。常见值有/MT、/MTd、/MD、/Md属性面板显示为“多线程(/MT)”和“多线程DLL(/MD)”几项。静态库通常用/MT也就是把C/C运行库静态链接不依赖“VC运行库.dll”动态库则常用/MD因为dll自身和exe都可以共享系统安装的“VC运行库.dll”。问题来了如果你做出来的动态库用/MT调用方exe却用/MD两者使用的堆分配可能不是同一个跨dll边界new/delete、malloc/free的时候会出问题。所以同项目里发布的所有二进制和它依赖的第三方库运行库设置尽量保持一致。部署动态库还有个现实问题调试版和发布版dll不能混着给别人。一个用Debug模式编译的dll很可能依赖VCRUNTIME140D.dll这个调试版运行库平时开发机上有目标机器上通常没有不装VS的普通用户机器一跑就崩。所以给外部发包必须用Release版并且在配置里把运行库设为“多线程DLL(/MD)”同时把VS2019对应版本的VC运行库安装包vcredist_x64.exe一并给到对方。4. 编译链接期最坑的报错与排查清单4.1 常见错误速查表下面是平时使用静态库和动态库时遇到概率最高的错误信息我这里按“错误现象 → 可能原因 → 解决办法”整理成表。错误现象常见原因解决办法LNK2019: 无法解析的外部符号“xxx”函数只找到声明没找到实现或lib没链接进来确认附加依赖项已添加库名别再纠结#pragma comment是否生效LNK2001: 无法解析的外部符号“main”库项目里出现了main或控制台入口设置不对静态库项目不要写main检查链接器 → 系统 → 子系统LNK1104: 无法打开文件“xxx.lib”链接器找不到指定库文件检查附加库目录是否正确文件名是否大小写一致运行时报0xC000007Bx86/x64架构不匹配确保exe和dll都是同一平台目标不要x64的exe加载x86的dll运行时找不到xxx.dlldll部署了没把dll放到exe同目录、PATH目录或系统目录C4996: ‘strcpy’ was declared deprecated用了旧的C函数不是库本身问题用安全版本或在预处理加 _CRT_SECURE_NO_WARNINGS4.2 为什么“无法解析的外部符号”总是反反复复链接错误里最常见的就是“无法解析的外部符号”。我排查的时候一般按顺序问自己三个问题第一这个函数到底在哪个库里面第二我有没有告诉链接器去这个库里面找第三库的编译方式和我调用方匹配吗经验里“是否告诉链接器”这一项问题最多。有时候你已经加了附加依赖项但还是报错那多半是“附加库目录”和“附加依赖项”写错位了。有人把lib的完整路径直接写进“附加库目录”然后附加依赖项却啥都不写这当然找不到。另外如果同一个函数在多个lib里都有定义链接器还会给你报LNK2005重复定义。“附加依赖项”里的顺序也有讲究较早列出的lib中如果有未解析符号后面的lib可以补反向就不行了。对于C库还有一类特别讨厌的符号解析问题调用方写#include MathTools.h但头文件里的函数没有放在extern C里而库实际上是C语言写的或者反过来。C编译器会生成修饰过的符号名跟.lib里导出的符号名对不上于是报“无法解析的外部符号”。遇到这种情况检查头文件里是否缺少#ifdef __cplusplus extern C { #endif // 函数声明... #ifdef __cplusplus } #endif4.3 VS2019中文注释报错到底是怎么回事之前有段时间我同事一直哀嚎“VS2019中的.cpp文件加入中文注释就报错”比如在文件顶部写了句// 初始化参数编译时就冒出error C2059: 语法错误:“...”之类的。其实问题不在中文字符本身而是文件编码。VS2019默认情况下源码文件如果既包含中文又保存成“无BOM的UTF-8”格式MSVC编译器会自作主张按系统本地代码页中文系统上是GBK/936去解析这个UTF-8文件。某些汉字的UTF-8字节序列恰好撞上GBK里的引号、反斜杠等特殊字符编译器就误会了随即报出迷惑性很强的语法错误。解决办法有几种第一用VS2019菜单里的“文件 → 另存为”点击“保存”按钮旁边的下拉箭头选择“高级保存选项”再把编码改成“Unicode (UTF-8 带签名) - 代码页 65001”也就是给文件加BOM字节序标记。加BOM后MSVC能自动识别文件确实是UTF-8中文注释就稳妥了。第二如果你更愿意用无BOM的UTF-8可以在项目预处理或C/C命令行里加一个编译选项/utf-8它的意思是“源文件是UTF-8执行字符集也设成UTF-8”直接绕开本地代码页猜测。我个人推荐在项目属性 → C/C → 命令行 → 其他选项 中统一加上/utf-8整个项目不用再管单个文件的保存格式。4.4 排查库问题的一个核心顺序只要你的程序牵扯到外部库我的排查习惯永远是这种顺序先确认生成成功 → 再确认链接成功 → 最后确认运行成功。编译报错优先瞪屏幕去翻头文件路径。链接报错优先看附加依赖项和附加库目录。运行报错才轮到dll路径和运行库版本这些事。很多人一开始就在怀疑运行库、系统PATH结果最后发现纯粹是“附加依赖项里忘了填lib文件名”太浪费时间。实际上我还遇到过更离谱的方案平台是“x86”但生成输出目录设置的路径是x64/DebugVS把lib生成到了x64文件夹调用工程却去x86目录找怎么配都找不到。最后我把输出目录改成$(SolutionDir)$(Platform)\$(Configuration)\问题一下就消失了。输出目录这种底层设置不要乱改有时候一次愚蠢的改动会让你怀疑人生。5. 几个高频场景串讲Qt、ONNX Runtime、UG、STM32附近的库用法5.1 在Windows Qt工程里用pro文件指定静态库现在很多人喜欢用Qt VS2019组合开发。在Qt这边除了能直接从VS菜单跑Qt程序也有不少朋友是用.qmake的.pro工程文件维护依赖的。如果要在.pro文件链接一个静态库比如放在项目根目录libs文件夹下的MyStaticLib.lib头文件放在include文件夹通常这样写INCLUDEPATH $$PWD/include win32 { CONFIG(debug, debug|release) { LIBS -L$$PWD/libs/debug -lMyStaticLib } else { LIBS -L$$PWD/libs/release -lMyStaticLib } }注意qmake中-l后面一般不带“lib”前缀也不带“.lib”后缀比如文件名是MyStaticLib.lib写-lMyStaticLib就行。-L后面是库目录的绝对路径。它和VS属性页里的“附加库目录附加依赖项”是同一个逻辑你理解了VS里那套Qt这边只是换个语法。如果你动态库的dll也是放在自己的目录里Qt程序运行时默认不会从$$PWD/libs去找dll。Windows下还是老办法要么把dll复制到exe输出目录要么在起始处用QCoreApplication::addLibraryPath或者在系统环境变量PATH里把dll所在目录加进去。很多人Qt程序双击没反应就是少了这一步。5.2 ONNX Runtime动态库接入的通用打法经常看我文章的朋友可能知道我在博客里分享过ONNX Runtime相关实践。最近问得也多因为ONNX Runtime官方发行版里就是典型的动态库形式解压后是include目录、lib目录、bin目录里面分别是头文件、导入库比如onnxruntime.lib和动态库本体onnxruntime.dll。你要做的就是把它们接到VS2019中去原理和前面完全一致。我的操作是解压到C:\libs\onnxruntime然后在VS工程属性中把C:\libs\onnxruntime\include填入“附加包含目录”把C:\libs\onnxruntime\lib填入“附加库目录”在“附加依赖项”里加上onnxruntime.lib代码里#include onnxruntime_cxx_api.h后正常调用。到最后把onnxruntime.dll从bin目录拷到exe目录。这样跑起来九成没有问题。官方文档里还会特别提醒配置/MD因为ONNX Runtime的动态库是绑定VS运行库的这也呼应我前面说的运行库一致性问题。5.3 UG二次开发和STM32静态库思路是相通的热搜词里还有“UG二次开发环境配置 vs2019”和“stm32制作静态库”。UG现在叫NX的二次开发环境配置本质上就是给VS指定一堆NX提供的.lib、.dll、.h文件路径。NX安装目录里通常有UGOPEN文件夹里面含头文件UGII文件夹里是动态库。你用VS2019建一个C工程把对应.lib和.h路径配置好程序里调用NX Open C API链接之后运行时需要把一些dll路径加进PATH或者拷贝到可访问目录。说到底还是VS引用第三方库那套基本功没跑偏。STM32那边有点不一样STM32单片机工程常用的是ARM编译器生成的静态库后缀是.a或者.lib在Keil、IAR或STM32CubeIDE里操作而不是用VS2019的MSVC编译。但核心理念依然成立把你自己写的驱动、中间件源码编译成库交付时只给头文件和库文件保护源码不被直接看到。所以你在VS2019里学会的静态库知识迁移到嵌入式工程里只需要换一套IDE界面和编译器命令行参数不会白学。5.4 一个小习惯所有第三方库统一放进一个目录管理最后分享一个我个人的工程目录习惯。我一般在项目根目录建一个third_party文件夹下面按库名和版本建子目录third_party/ onnxruntime/ include/ lib/ bin/ nxsdk/ include/ lib/ bin/ spdlog/ include/ lib/VS属性页里的附加目录都指向这套固定的相对路径比如用$(SolutionDir)third_party\onnxruntime\include这样整个工程换电脑、拷贝给同事时不需要改绝对路径。因为别人拿到源码后把third_party目录一放编译路径就全通了。很多新手会在自己的电脑上把库放到C盘乱建目录然后路径写成C:\Users\me\Downloads\...一旦换个环境或者用网盘同步整条路径直接失效排查又特别痛苦。如果有条件我建议每个第三方库附带一个说明文件写下从哪个官网版本下载的、用的编译选项是/MD还是/MT、修复过哪些坑。三个月之后你再回到这个项目会发现这些东西比写文档还管用。我在实际项目中把这套“头文件目录、库目录、附加依赖项、dll部署”的统一流程跑顺之后再接到任何需要集成C/C库的活都变得很机械。静态库和动态库本身不复杂难点在于理解不同文件在哪里生效以及知道编译期、链接期、运行期三个阶段的报错分别该往哪里查。希望这篇整理能把你在VS2019里的库依赖问题一次解决。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价