VS2019 DLL动态库连接实例:从导出到加载的完整实践
2026/9/18 18:26:28 网站建设 项目流程

简介:一份面向Visual Studio 2019开发者的DLL动态库连接实例图文教程,系统梳理从动态库项目创建到控制台程序调用函数并验证结果的完整链路,适合初学动态链接库或需要快速熟悉VS2019工程配置的读者,也能帮助已有一定经验的开发者快速定位常见连接问题。资源包内仅含一个PDF文档,共446KB,步骤配图清晰,内容紧凑,便于边看边操作。教程以名为mydll的动态库项目为主线,先在预编译头文件pch.h中声明加法函数Add和减法函数Sub,再在pch.cpp中实现函数体,编译后生成lib与dll文件;之后转到新建的控制台应用,说明如何配置工程属性中的附加包含目录、附加库目录和附加依赖项,准备好所需头文件与静态库路径,并将生成的dll复制到可执行文件所在目录,最终借助include预处理命令和pragma comment指令关联头文件与lib,直接调用DLL中的导出函数,形成动态库调用闭环。目前已有5000余人学习浏览,对希望掌握VS2019下DLL生成、连接与调用全流程的开发者具有直接参考价值。

1. 从一张报错截图说起:Visual Studio 2019 DLL动态库连接实例在解决什么

工程师手里的 DLL 通常自带两种状态:要么是别人拷过来的一个绿色小文件,配一份几十行的接口说明,双击程序却弹 0xC0000135;要么是自己刚生成完的产物,换个解决方案就无论如何连不上,链接器报一堆 LNK2019。这个标题,Visual Studio 2019 DLL动态库连接实例,讲的就是这两类人共同卡住的环节:DLL 已经导出了,调用方在 VS 2019 里到底怎么配置工程,才能在链接时找到符号、运行时加载成功。我不会把截图铺满全文,而是把每步操作落到菜单路径、配置项和可直接编译的代码上,再解释为什么这么配。新手可以照着做一遍把工程跑通,老手可以拿它当排查对照清单,少走弯路。

2. DLL 导出与名字修饰:连接的第一步在生成 DLL 那边

很多人以为“连接”是调用方工程的事,其实 80% 的“无法解析的外部符号”在生成 DLL 那侧就已经注定了。调用方通过头文件声明、导入库 lib、以及 DLL 导出表里的符号名,三者名字对不上,VS 2019 连接器直接抛 LNK2019 或 LNK2001。这一章先把生成侧的三个关键点讲清楚,它们是后续连接成功的前提。

2.1__declspec(dllexport)与 .def 文件:两种导出方式怎么选

最常见的做法是直接在头文件里做条件宏,一段代码同时兼顾 DLL 编译方和调用方两种视角。

// MathLibrary.h #pragma once #ifdef MATHLIBRARY_EXPORTS #define MATHLIBRARY_API __declspec(dllexport) #else #define MATHLIBRARY_API __declspec(dllimport) #endif extern "C" MATHLIBRARY_API int add(int a, int b); extern "C" MATHLIBRARY_API int subtract(int a, int b);

MATHLIBRARY_EXPORTS是 VS 2019 创建 DLL 工程时自动加的预处理器定义,只有生成 DLL 那个项目里存在。编译 DLL 时,头文件走dllexport分支,函数被写进 DLL 的导出表;调用方包含同一份头文件时宏未定义,走dllimport分支,链接器就知道这些符号来自外部。这里要注意,不要图省事把两个分支删掉只留dllexport,那会让调用方工程按错误的语义去处理声明,问题更隐蔽。

另一种不常被新手注意的路径是用 .def 文件导出,适合第三方库或需要精确控制导出序号的场景。

LIBRARY "MathLibrary" EXPORTS add subtract

.def 方式不需要在声明上加__declspec(dllexport),但文件名和导出项要单独维护,而且 C++ 修饰名问题依然存在,该写extern "C"还是得写。

方案适用场景维护成本导出名控制
__declspec(dllexport)自己维护源码,日常开发首选低,宏自动切换受调用约定和extern "C"影响
.def 文件旧库改造、按序号导出中,名字单独维护可以在 .def 中固定名字和序号

2.2 extern "C" 与 __stdcall:x86 下函数名会变成什么

C++ 编译函数时默认做名字修饰,int add(int, int)在 x86 下的修饰名形如?add@@YAHH@Z。如果没有extern "C",DLL 导出表里存的就是这串名字;调用方头文件里写的却是普通add,两边对不上,连接必然失败。这就是为什么所有跨模块导出,我都会在声明外加extern "C",把名字修饰拉回 C 风格。

__stdcall在 x86 上会额外把参数总字节数追加到导出名后面。比如两个 int 参数是 8 字节,导出名会变成_add@8。这时候调用方如果用默认的__cdecl去声明同一个函数,链接器会找_add,同样报错。

声明写法(x86 下)导出名
extern "C" int add(int,int),默认__cdecl_add
extern "C" int __stdcall add(int,int)_add@8
不加extern "C"的普通 C++ 函数形如?add@@YAHH@Z

x64 上只有一种调用约定,__stdcall会被忽略,名字也不再带下划线和@8后缀。所以同一份代码在 x64 下往往能过,切到 x86 就翻车。遇到 Debug 配置默认跑 x86、链接报 LNK2019 时,先把平台切到 x64 验证,再回头对着这张表查声明,比乱调项目属性高效得多。

2.3 dumpbin /exports 查看导出表,确认符号可被外部连接

VS 2019 自带 dumpbin,不需要额外安装。打开“x64 Native Tools Command Prompt for VS 2019”,或者直接从开始菜单启动“Developer Command Prompt”,执行:

dumpbin /exports MathLibrary.dll

输出里会有一个name列,列出 DLL 实际导出的全部符号。你在这里看到的函数名,才是调用方最终能连上的名字。如果看到的是?add@@...这种修饰名,回头给声明补extern "C";如果看到_add@8,说明调用约定确实是__stdcall,调用方声明也得跟着改。

提示:dumpbin 支持全路径,直接写dumpbin /exports D:\build\MathLibrary.dll也一样。从这一步开始,你就拥有了一套不依赖 IDE 界面的验证方法。

3. 在 VS 2019 中创建 DLL 工程并拿到 h/lib/dll 三件套

理论立住之后,现在从建工程开始走一遍。很多人卡在源头上:项目模板选错了,或者预编译头设置不对,导致编译都过不去。这一章把 VS 2019 的操作路径和生成产物的来龙去脉说清楚。

3.1 用“动态链接库(DLL)”模板建工程,模板自带哪些文件

菜单路径:文件 -> 新建 -> 项目 -> 语言选 C++ -> 搜索“动态链接库”,选择“动态链接库(DLL)”模板,工程名我习惯叫 MathLibrary。这里要和“Windows 桌面应用程序”区分开,那个模板生成的是 exe,后面所有配置都不成立。

VS 2019 会默认生成四个文件:dllmain.cpppch.hpch.cppframework.hdllmain.cpp里的DllMain是 DLL 的入口点,处理进程和线程的附着、分离事件,平时不用写任何业务代码;pch.hpch.cpp是预编译头的一部分,新加的源文件里必须include "pch.h",否则编译会报 C1010。你可以把预编译头整个关掉,但工程默认开启时,不要跟编译器对着干。

3.2 写导出头文件与实现,注意自动生成的项目宏

在解决方案资源管理器里右键项目 -> 添加 -> 新建项 -> 头文件,把 2.1 那段MathLibrary.h加进去。再添加一个MathLibrary.cpp写实现:

// MathLibrary.cpp #include "pch.h" #include "MathLibrary.h" int add(int a, int b) { return a + b; } int subtract(int a, int b) { return a - b; }

#include "pch.h"必须放在第一行,这是预编译头的硬性要求。MathLibrary.h里的函数现在已经被MATHLIBRARY_EXPORTS宏标记成dllexport,编译后就会写进 DLL 导出表。如果你改过工程名,宏名也会跟着变,比如工程叫 Foo,宏就是FOO_EXPORTS;自己手写头文件时别把宏名记错。

3.3 生成后拿到 .dll、.lib、.h 三个文件,别混淆导入库与静态库

按 Ctrl+Shift+B 生成解决方案,默认输出到解决方案目录下的x64\Debugx64\Release(取决于你配的解决方案平台)。里面出现三个关键文件:MathLibrary.dllMathLibrary.libMathLibrary.pdb,再加上工程里的MathLibrary.h,就是完整的“三件套”。

MathLibrary.lib是导入库,不是静态库。它里面没有 add 的代码实现,只有一段桩信息,告诉连接器“add 这个符号在 MathLibrary.dll 里,偏移是多少”。连接器生成 exe 时读 .lib,程序运行时 Windows 加载器读 .dll,两个文件缺哪个都不行。很多新手把 .lib 当成静态库删了,或者只拷贝 .dll 去别的机器,运行时报错后一脸茫然,原因就在这里。

文件谁在哪个阶段用缺失时现象
MathLibrary.h编译器编译调用方源码时C2065 未声明的标识符
MathLibrary.lib连接器生成 exe 时LNK2019 或找不到 lib
MathLibrary.dll运行时加载0xC0000135 或系统弹窗

3.4 顺手改掉源码编码:把 VS 2019 默认代码页调成 UTF-8

如果你在 DLL 源码里写了中文注释或者中文字符串,VS 2019 在简体中文系统上默认按本地代码页读取源文件,经常报警告 C4819,或者编出来的字符串在别的机器上显示成乱码。运行时字符集设置(项目属性 -> 常规 -> 字符集)管的是宽窄字符转换,跟源码文件本身的编码是两码事。

真正要改的是文件编码:菜单栏 文件 -> 另存为 -> 保存按钮旁边的箭头 -> 编码保存 -> 选“UTF-8 带签名”。如果“编码保存”选项没有直接出现,去 工具 -> 自定义 -> 命令 -> 文件 里把“高级保存选项”命令拖到菜单上。UTF-8 带 BOM 是 Windows 生态下最稳的源码编码,VS 2019 能正确识别,MSVC 编译器也不会再纠结代码页。

4. 调用方连接 DLL 的两种写法:隐式连接与 LoadLibrary 显式加载

这是标题里“连接”二字的核心。DLL 造出来了,调用方怎么连上去,业界通行的做法分两种:隐式连接(编译期通过 lib 绑定)和显式连接(运行期 LoadLibrary)。两种都值得掌握,因为它们应对的场景不同。

4.1 调用方新建空项目,先把位数和配置与 DLL 对齐

菜单路径:文件 -> 新建 -> 项目 -> C++ -> 空项目,或者“控制台应用”,然后添加一个main.cpp。建好后第一件事不是写代码,而是看工具栏上的解决方案平台是不是 x64,和 DLL 生成时的平台保持一致。Windows 一个进程只能有一种位数,exe 是 x64 就加载不了 x86 的 DLL,运行时直接报“模块映像格式不正确”。Debug/Release 也尽量一致,混用虽然偶尔能运行,但调试时会踩到代码优化不一致的坑。

4.2 隐式连接:附加包含目录、附加库目录、附加依赖项三步配置

MathLibrary.h放到调用方工程的 include 目录,然后在main.cpp里写调用代码:

#include <iostream> #include "MathLibrary.h" int main() { int r = add(10, 5); std::cout << r << std::endl; return 0; }

编译能过,但连接会报找不到add符号,因为还没告诉链接器去哪找。VS 2019 需要配置三处:

  1. 项目属性 -> C/C++ -> 常规 -> 附加包含目录:填MathLibrary.h所在目录,让编译器找得到声明。
  2. 项目属性 -> 链接器 -> 常规 -> 附加库目录:填MathLibrary.lib所在目录,让连接器找得到导入库。
  3. 项目属性 -> 链接器 -> 输入 -> 附加依赖项:填MathLibrary.lib,显式声明需要链接这个库。

不想每次开属性页折腾的话,第三条可以换成代码里的指令:

#pragma comment(lib, "MathLibrary.lib")

#pragma comment(lib, ...)是写给连接器看的,等效于在附加依赖项里填库名。但注意,第二条的“附加库目录”仍然要配,否则连接器根本找不到这个 .lib 文件。生成 exe 后,再把MathLibrary.dll复制到 exe 同级目录。VS 2019 调试时默认工作目录是项目目录(.vcxproj所在目录),它也会在那找 DLL。很多人代码明明没问题、一运行就报找不到 DLL,多半是把 dll 放在了别处。

4.3 显式连接:LoadLibrary + GetProcAddress 最小调用代码

当你想在程序启动时不强制依赖 DLL、运行时按需加载,或者做插件系统时,用显式连接。它不需要 .lib,不需要附加依赖项配置,全部在代码里完成:

#include <windows.h> #include <iostream> typedef int (*AddFunc)(int, int); int main() { HMODULE hMod = LoadLibraryW(L"MathLibrary.dll"); if (!hMod) { std::cerr << "LoadLibrary failed, code=" << GetLastError() << std::endl; return -1; } AddFunc add = reinterpret_cast<AddFunc>(GetProcAddress(hMod, "add")); if (!add) { FreeLibrary(hMod); std::cerr << "GetProcAddress failed, code=" << GetLastError() << std::endl; return -2; } std::cout << "result: " << add(3, 4) << std::endl; FreeLibrary(hMod); return 0; }

LoadLibraryW用宽字符版本,避免路径里带中文时出现 ANSI 编码问题。GetProcAddress返回的是FARPROC,必须强转成与目标函数签名一致的函数指针,这里就是int (*)(int, int)。签名不一致时,参数会被按错误方式解读,轻则返回垃圾值,重则栈被写坏直接崩溃。显式连接的典型场景就是加载onnxruntime.dll、OpenGL 动态库这类第三方运行时,你不用在连接期绑定它们,但要在调用前检查每个句柄和函数指针是否有效。

GetLastError()的返回值有很强的指引性:

错误码含义优先排查方向
126找不到指定模块DLL 路径、位数、依赖链
127找不到指定过程导出函数名是否被修饰过,回到 2.2 查
1114DLL 初始化例程失败DLL 的DllMain或它依赖的静态库初始化失败

Python 开发者常见的OSError: [WinError 1114] 动态链接库(DLL)初始化例程失败,报错信息里说某个 dll “or one of its dependencies”,本质就是这条链路里某一个依赖初始化没通过。C++ 调用 DLL 时出 1114,排查思路完全一样,去看 DLL 的依赖项,而不是怀疑调用代码。

4.4 运行时报错 126 / 127 / 1114 时的排查次序

按顺序排查,多数问题三步内能定位:

  1. 位数确认:任务管理器看 exe 进程是 64 位还是 32 位,再用后面 5.3 的方法确认 DLL 位数,两者必须一致。
  2. 依赖链检查:在 DLL 所在目录执行dumpbin /dependents MathLibrary.dll,看它还依赖哪些 DLL。缺 VC++ 运行库时,目标机器装对应版本的 Visual C++ Redistributable 就能解决。
  3. 加载路径:Windows 按“exe 目录 -> 系统目录 -> PATH”的顺序找 DLL。只把 dll 放进工程目录然后分发 exe,到没有装 VS 的机器上必然报缺库。

最后提一个高发问题:dll 冲突。系统目录或 PATH 里如果已经存在另一个同名旧版 dll,即使你的 exe 目录里有正确版本,某些加载顺序下也可能拿到旧文件。国产软件安装目录里这种同名覆盖尤其常见,装完某个软件后,原本正常的程序莫名弹 0xC0000135,多半是系统目录被写入了同名旧版。遇到这种情况,先用 Process Explorer 看已加载模块的真实路径,别急着下“DLL 文件损坏”的结论。

5. 用 dumpbin 和模块窗口验证 DLL 连接成功的三个技巧

连接成功不是编译通过就算数,要验证到“运行时真的加载了正确路径的 DLL”这一层。以下三个手段按验证深度递进,也是我每次换机器、换编译器版本后必做的体检项。

5.1 模块窗口:调试时确认 DLL 确实加载进来了

main.cppadd(10, 5)那行设个断点,按 F5 调试运行,断点命中后打开菜单 调试 -> 窗口 -> 模块。列表里找到MathLibrary.dll,看它的“路径”列。如果路径不是你以为的那个目录,说明加载器取的是 PATH 里另一个同名文件,问题立刻暴露。这个窗口同时显示 DLL 的版本号和符号状态,右键 -> 加载符号可以顺手把 PDB 配上,之后 F11 就能直接步进 DLL 内部源码。

5.2 dumpbin /imports 反查 exe,验证编译期的连接链路

隐式连接成功后,用 dumpbin 从另一个方向验证:

dumpbin /imports MyApp.exe

输出里会有一段MathLibrary.dll的说明,下面列出addsubtract两个符号。这表明 exe 的导入表里已经写死了对 MathLibrary 的依赖。它和 2.3 的dumpbin /exports是同一枚硬币的两面:exports 管 DLL 产出什么,imports 管 exe 消费什么。连接期缺 .lib 会在生成时报 LNK,运行期缺 dll 会弹 0xC0000135,这两条命令正好各管一个阶段,能帮你立刻区分是“没连上”还是“没加载到”。

5.3 dumpbin /headers 判断 DLL 位数,终结 32/64 位不匹配

dumpbin /headers MathLibrary.dll | findstr /i "machine"

输出里会出现machine (8664)machine (14C),前者是 x64,后者是 x86。我用这个作为终极验证手段:只要 DLL 产物位数正确、调用方导入表正确、运行时模块窗口路径正确、位数一致,连接实例就已经闭环。之后再遇到“能编译能链接、一运行就崩”的情况,排查方向就该转到函数调用约定和参数传递时序上,而不是继续怀疑 VS 2019 的工程配置。把这里的machine换成subsystem,还能顺带确认目标文件是控制台程序还是 GUI 程序,这条命令在排查连接问题时基本够用。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询