尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

VS2017实战:从零构建C++ DLL并实现隐式链接调用

VS2017实战:从零构建C++ DLL并实现隐式链接调用 1. 项目概述为什么我们需要亲手造一个DLL在Windows平台上做C开发动态链接库DLL几乎是一个绕不开的话题。你可能经常在项目里看到一堆.dll文件或者被“找不到xxx.dll”的报错搞得焦头烂额。但你是否想过这些神秘的“零件”到底是怎么来的自己动手创建一个DLL并让另一个程序成功调用它这不仅仅是完成一个教程任务更是深入理解Windows程序模块化、代码复用和运行时链接机制的关键一步。使用Visual Studio 2017VS2017来完成这个任务是一个非常经典且实用的选择。VS2017在C开发工具链上达到了一个成熟的平衡点既不像更早版本那样对现代C标准支持有限也不像某些新版本那样偶尔有“水土不服”的兼容性问题。通过它我们可以清晰地看到从编写代码、配置项目属性到最终生成和调用DLL的完整链路。这个过程会涉及到项目类型选择、导出符号声明、链接器配置、运行时依赖等多个核心概念。掌握它们意味着你不仅能修复“dll文件丢失”的错误更能主动设计出结构清晰、易于维护的软件架构。无论是为了封装核心算法库、制作游戏插件还是为其他语言如C#、Python提供原生性能接口DLL技术都是你的必备技能。2. 核心概念与项目设计思路在动手写代码之前我们必须把几个关键概念理清楚。很多人照着教程一步步做成功了但遇到问题还是一头雾水根本原因就是没明白背后的“为什么”。2.1 静态库 vs. 动态库本质区别是什么你可以把软件构建想象成造一辆汽车。静态链接库.lib好比把发动机、轮胎、座椅的所有零件和图纸都直接焊死在车架里。最终出厂的就是一辆完整的、独立的车。好处是开出去肯定能跑不依赖外部零件缺点是车会特别重生成的可执行文件大而且想升级发动机修复Bug或更新功能就得把整辆车回厂重造重新编译链接整个项目。动态链接库.dll .lib这时的车架主程序是独立的发动机、变速箱DLL是作为可拆卸的模块。车出厂时会附带一份这些模块的“安装说明书”导入库文件.lib很小。当车启动时程序运行时才会根据“说明书”去指定的仓库系统路径或程序目录里找对应的模块DLL装上。好处是车体轻便主程序小多个车型可以共用同一款发动机多个程序共享一个DLL升级发动机时只需要换掉仓库里的模块所有车下次启动就能用上新版便于更新。缺点就是万一仓库里没有对应的发动机或者型号不对车就趴窝了运行时DLL加载失败。在VS2017中创建项目时这个选择就体现在项目类型上。我们会创建两个项目一个生成DLL生产模块一个生成可执行程序调用模块。2.2 显式链接 vs. 隐式链接两种调用方式详解调用DLL里的函数有两种主流方式它们决定了你编程的复杂度和程序的灵活性。隐式链接Implicit Linking这是我们本次教程主要采用的方式也是最常用、最方便的一种。它需要在编译时compile-time就告诉编译器“我要用哪个DLL里的哪些函数”。具体做法是提供头文件.h声明了哪些函数是可以被外部调用的即导出函数。提供导入库文件.lib这个文件不是静态库它体积很小只包含了DLL中导出函数的名字和序号等信息不包含实际代码。链接器Linker在生成EXE时会把这个.lib文件链接进去。程序启动时Windows加载器会自动寻找并加载所需的DLL。如果找不到程序根本启动不了。显式链接Explicit Linking这种方式更动态适合插件系统。它在运行时run-time通过API手动加载DLL。核心步骤是使用LoadLibrary或LoadLibraryEx函数加载DLL文件获取一个模块句柄HMODULE。使用GetProcAddress函数根据函数名字符串从模块句柄中获取函数的内存地址。将获取的地址转换为正确的函数指针类型后进行调用。使用完毕后用FreeLibrary卸载DLL。这种方式不需要头文件和.lib文件参与编译链接但调用起来更繁琐且没有编译期的类型检查容易出错。对于大多数需要紧密集成、固定功能的场景隐式链接是首选。本次教程我们将聚焦于此。2.3 VS2017中的关键配置项解析在VS2017中影响DLL生成和调用的配置主要集中在项目属性的两个地方C/C - 预处理器这里定义的预处理宏如MYDLL_EXPORTS是控制函数导出的关键开关。链接器常规 - 输出文件决定了生成的DLL和配套的LIB文件的名字和路径。输入 - 附加依赖项这里是填写需要链接的.lib文件的地方。对于DLL项目通常为空或链接其他库对于调用方EXE项目这里需要填入DLL项目生成的.lib文件。高级 - 导入库指定DLL项目生成的导入库.lib的路径和名称。理解这些配置的意义比记住步骤更重要。当项目迁移或配置出错时你才能快速定位问题。3. 实战创建并生成一个C DLL理论铺垫完毕我们进入实战环节。请打开你的VS2017跟着步骤一步步操作。3.1 创建DLL项目新建项目选择“文件 - 新建 - 项目”。选择模板在“Visual C”下选择“Windows 桌面”然后选择“动态链接库(DLL)”。给项目起个名字例如MyMathDLL选择好位置点击“确定”。项目结构VS会为你生成一个基本的DLL项目框架。你通常会看到以下几个关键文件dllmain.cpp这是DLL的入口点包含DllMain函数。这个函数在DLL被加载和卸载时被操作系统调用用于进行线程或进程级别的初始化和清理。对于简单的功能库你通常不需要修改这个文件。pch.h,pch.cpp预编译头文件用于加速编译。framework.h包含Windows头文件和一些通用定义。3.2 编写导出函数与头文件现在我们来添加自己的功能。我们创建一个简单的数学运算库。添加头文件在“解决方案资源管理器”中右键点击“头文件”筛选器 - “添加 - 新建项”选择“头文件(.h)”命名为MyMath.h。编辑MyMath.h这是对外暴露的接口声明文件。// MyMath.h #pragma once // 定义一个宏用于简化导出声明 // 当在DLL项目内部编译时MYMATHDLL_EXPORTS会被定义此时函数被声明为导出(__declspec(dllexport)) // 当在外部项目调用方包含此头文件时MYMATHDLL_EXPORTS未定义函数被声明为导入(__declspec(dllimport)) #ifdef MYMATHDLL_EXPORTS #define MYMATH_API __declspec(dllexport) #else #define MYMATH_API __declspec(dllimport) #endif // 声明导出函数 // 注意必须使用 extern C 来防止C编译器进行名称修饰Name Mangling // 这样才能确保其他语言或通过GetProcAddress按名称正确找到函数。 extern C MYMATH_API int Add(int a, int b); extern C MYMATH_API int Subtract(int a, int b); extern C MYMATH_API double Multiply(double a, double b); extern C MYMATH_API double Divide(double a, double b);关键点解释__declspec(dllexport)和__declspec(dllimport)是Microsoft特有的扩展用于指定函数的链接属性。extern “C”确保了函数名在编译后的二进制文件中保持原样如Add而不是被C编译器修饰成像?AddYAHHHZ这样的复杂符号这对于跨语言调用和显式链接至关重要。添加源文件右键点击“源文件”筛选器 - “添加 - 新建项”选择“C文件(.cpp)”命名为MyMath.cpp。编辑MyMath.cpp这里是函数的具体实现。// MyMath.cpp #include pch.h // 必须包含预编译头 #include MyMath.h // 实现加法函数 int Add(int a, int b) { return a b; } // 实现减法函数 int Subtract(int a, int b) { return a - b; } // 实现乘法函数 double Multiply(double a, double b) { return a * b; } // 实现除法函数注意处理除零错误 double Divide(double a, double b) { if (b 0.0) { // 在实际项目中这里应该抛出一个异常或返回一个错误码 return 0.0; // 简单起见返回0 } return a / b; }3.3 配置项目属性以正确导出定义导出宏我们需要在DLL项目中定义MYMATHDLL_EXPORTS宏这样在编译MyMath.cpp时头文件里的MYMATH_API才会被展开为__declspec(dllexport)。右键点击MyMathDLL项目 - “属性”。选择“配置属性 - C/C - 预处理器”。在“预处理器定义”中添加MYMATHDLL_EXPORTS。通常Debug和Release配置都需要添加。检查输出在“链接器 - 常规 - 输出文件”中你可以看到默认的输出路径和文件名例如$(OutDir)$(TargetName)$(TargetExt)这通常会生成MyMathDLL.dll。同时链接器会自动生成一个同名的导入库文件MyMathDLL.lib。3.4 生成DLL点击菜单栏的“生成 - 生成解决方案”或按F7。如果一切顺利你会在项目的输出目录通常是项目路径\x64\Debug\或项目路径\Win32\Debug\取决于你的平台下找到两个关键文件MyMathDLL.dll动态链接库本体。MyMathDLL.lib导入库文件注意这个.lib文件很小它只是“说明书”不是静态库。至此你的DLL就制作完成了。接下来我们要创建另一个程序来使用它。4. 实战创建调用DLL的应用程序现在我们创建一个控制台应用程序来调用刚才生成的DLL。4.1 创建控制台应用程序项目在同一个解决方案中右键解决方案 - “添加 - 新建项目”。选择“Windows 控制台应用程序”命名为MathClient点击“确定”。4.2 配置客户端项目以链接DLL这是最关键的一步目的是让MathClient项目知道去哪里找DLL的“说明书”.lib和“零件”.dll。包含头文件路径我们需要让客户端能找到MyMath.h这个头文件。右键MathClient项目 - “属性”。选择“配置属性 - C/C - 常规”。在“附加包含目录”中添加MyMathDLL项目的头文件所在目录。一个相对路径的写法是$(SolutionDir)MyMathDLL。这样编译器就知道去哪里找#include “MyMath.h”。链接导入库.lib我们需要告诉链接器要链接哪个导入库。在MathClient项目属性中选择“配置属性 - 链接器 - 输入”。在“附加依赖项”中添加MyMathDLL.lib。注意这里只写文件名链接器会去“附加库目录”里找它。接着选择“链接器 - 常规”。在“附加库目录”中添加MyMathDLL项目生成.lib文件的目录。可以使用宏例如$(SolutionDir)$(Configuration)或更精确的$(SolutionDir)MyMathDLL\$(Platform)\$(Configuration)。这确保了Debug版客户端链接Debug版的DLL导入库。4.3 编写客户端调用代码打开MathClient项目下的MathClient.cpp通常是主源文件。编写调用代码// MathClient.cpp #include iostream #include MyMath.h // 包含我们DLL的头文件 int main() { std::cout DLL Client Started. std::endl; int x 10, y 3; double m 7.5, n 2.0; // 调用DLL中的函数就像调用普通函数一样 int sum Add(x, y); int diff Subtract(x, y); double product Multiply(m, n); double quotient Divide(m, n); std::cout x y sum std::endl; std::cout x - y diff std::endl; std::cout m * n product std::endl; std::cout m / n quotient std::endl; // 测试除零在我们的简单实现中返回0 double badDiv Divide(m, 0.0); std::cout m / 0.0 badDiv (Handled) std::endl; std::cout DLL Client Finished. std::endl; std::cin.get(); // 暂停方便查看输出 return 0; }4.4 解决运行时依赖DLL文件的位置编译链接成功后运行客户端程序可能会失败并弹出“无法启动此程序因为计算机中丢失 MyMathDLL.dll”的错误。这是因为Windows在运行时找不到这个DLL文件。Windows搜索DLL的顺序通常是应用程序所在的目录。系统目录如C:\Windows\System32。Windows目录。当前工作目录。PATH环境变量中列出的目录。最可靠的方法是将MyMathDLL.dll复制到客户端可执行文件MathClient.exe所在的目录下。你可以在MathClient项目属性中配置生成后事件来自动完成这个操作。右键MathClient项目 - “属性”。选择“配置属性 - 生成事件 - 后期生成事件”。在“命令行”中输入一条复制命令例如copy $(SolutionDir)MyMathDLL\$(Platform)\$(Configuration)\MyMathDLL.dll $(TargetDir)这条命令会在MathClient项目每次成功生成后自动将对应平台和配置下的DLL文件复制到自己的输出目录。现在再次生成并运行MathClient项目。你应该能在控制台看到正确的计算结果。恭喜你你已经成功创建并调用了一个自定义的C DLL5. 进阶话题与深度避坑指南掌握了基本流程后我们来看看那些容易踩坑的细节和进阶用法。5.1 导出C类与方法上面的例子导出的是C风格函数。如果你想导出一个完整的C类过程类似但有一些陷阱。DLL项目头文件 (MyClass.h):#ifdef MYCLASSDLL_EXPORTS #define MYCLASS_API __declspec(dllexport) #else #define MYCLASS_API __declspec(dllimport) #endif class MYCLASS_API MyExportedClass { public: MyExportedClass(); ~MyExportedClass(); int DoSomething(int value); // ... 其他成员函数 private: int m_data; };注意导出整个类意味着类的所有公共和非内联成员函数、静态成员以及虚函数表如果存在都需要被处理。这要求DLL和客户端使用完全相同的编译器版本和编译设置如运行时库/MTvs/MD否则极易导致内存分配/释放错位、虚表布局不一致等严重问题引发难以调试的崩溃。强烈建议对于需要跨模块边界的C对象考虑使用抽象接口纯虚类并在DLL中提供工厂函数来创建和销毁对象实例。这样可以极大降低二进制兼容性风险。5.2 内存管理边界问题这是一个超级大坑。一个基本原则是谁分配谁释放。如果DLL中的函数返回了一个由new或malloc分配的内存指针那么也必须由DLL提供一个专门的函数如FreeBuffer来释放这块内存。绝对不要在EXE中用delete去释放DLL中new出来的内存反之亦然。因为它们可能链接到不同的堆Heap管理器尤其是当它们使用不同的运行时库如一个用/MT静态链接另一个用/MD动态链接时100%会导致崩溃。安全的做法是// DLL 侧 extern C MYDLL_API char* CreateBuffer(int size) { return new char[size]; } extern C MYDLL_API void FreeBuffer(char* ptr) { delete[] ptr; } // 客户端侧 char* buf CreateBuffer(100); // ... 使用 buf ... FreeBuffer(buf); // 必须用DLL提供的函数释放5.3 运行时库/MT, /MD不匹配这是导致“找不到入口点”或“运行时错误”的常见原因。在项目属性 - “C/C - 代码生成 - 运行时库”中有四个选项/MT多线程静态链接。将运行时库代码静态打包进你的DLL/EXE。/MTd/MT的调试版。/MD多线程DLL动态链接。你的程序将依赖于MSVCRxxx.dll(C运行时库) 和VCRUNTIMExxx.dll等系统DLL。/MDd/MD的调试版。黄金法则一个解决方案内的所有项目DLL和EXE必须使用相同的运行时库设置。通常发布版本用/MD调试版本用/MDd。如果DLL用/MT编译自带运行时库而EXE用/MD编译依赖外部DLL那么它们各自有一套内存管理、异常处理等机制混用极易出错。5.4 处理DLL的加载与卸载DllMainDllMain是DLL的入口函数但在这里面做的事情要尽可能少。微软官方文档明确警告在DllMain中调用某些系统API或进行复杂初始化可能导致死锁或不可预知的行为。特别是不要在DllMain里创建线程或等待线程。不要调用LoadLibrary或FreeLibrary。不要调用需要加载其他DLL的函数。 安全的做法是导出一个显式的Initialize()和Uninitialize()函数让客户端在合适的时机调用。6. 常见问题排查与解决方案实录即使按照教程一步步来也难免会遇到问题。这里记录了我自己踩过或见过的一些典型错误及解决方法。6.1 “无法解析的外部符号” (LNK2001/LNK2019)这是链接阶段最常见的错误。症状编译成功链接失败报错error LNK2001: 无法解析的外部符号 “int __cdecl Add(int,int)”。原因1最常见客户端项目没有正确链接导入库.lib文件。检查“附加依赖项”和“附加库目录”是否配置正确。确保你链接的是DLL项目生成的.lib而不是静态库项目的.lib。原因2DLL项目没有成功导出函数。检查MYMATHDLL_EXPORTS宏是否在DLL项目的预处理器定义中正确添加。头文件中的导出声明MYMATH_API是否正确展开为__declspec(dllexport)。可以尝试在DLL项目的MyMath.cpp文件开头临时添加#pragma comment(linker, “/export:Add”)来强制导出但这只是调试手段。是否使用了extern “C”如果没有C的名称修饰会导致链接器找不到匹配的符号。可以用dumpbin /exports MyMathDLL.dll命令查看DLL实际导出的函数名如果看到像?AddYAHHHZ这样的名字就说明需要extern “C”。6.2 “应用程序无法启动因为找不到 xxx.dll” 或 “0xc000007b” 错误这是运行时错误。症状程序启动时崩溃或弹窗提示。原因1DLL文件不在应用程序的搜索路径下。按照4.4节的方法将DLL复制到EXE同级目录。原因2DLL本身依赖的其他DLL如特定的MSVCP140.dll,VCRUNTIME140.dll找不到。使用dumpbin /dependents MyMathDLL.dll命令查看依赖。确保客户端运行环境有对应版本的Visual C RedistributableVC运行库。对于调试版本这些依赖通常由VS开发环境提供发布版本则需要单独安装或随程序分发。原因30xc000007b常见32位x86程序试图加载64位x64的DLL或者反过来。确保平台目标一致。在VS2017中检查解决方案平台是x86还是x64DLL和EXE项目必须一致。6.3 调试DLL项目调试DLL本身而不是调用它的EXE需要一点设置。将MathClient项目设为“启动项目”右键项目 - “设为启动项目”。右键MyMathDLL项目 - “属性”。选择“配置属性 - 调试”。将“命令”设置为$(TargetDir)$(TargetName).exe但这里要指向客户端EXE的路径例如$(SolutionDir)$(Configuration)\MathClient.exe。将“工作目录”设置为客户端EXE所在的目录。 这样当你从DLL项目启动调试时VS会自动启动客户端程序并附加调试器你可以在DLL的源代码中设置断点。6.4 函数调用约定Calling Convention不匹配虽然我们用了extern “C”解决了名称修饰但C/C还有不同的函数调用约定如__cdecl,__stdcall,__fastcall等。它们决定了参数如何压栈、栈由谁清理。默认情况下VS中的函数使用__cdecl。如果DLL导出函数时用了__stdcallWindows API常用而客户端声明时用了__cdecl调用时栈会被错误清理导致程序崩溃。解决方案在导出和导入声明中显式指定并保持一致的调用约定。对于跨语言兼容性__stdcall更常见。extern “C” MYMATH_API int __stdcall Add(int a, int b);最后分享一个我个人的习惯在发布DLL给他人使用时除了提供.dll,.lib,.h文件外我总会附带一个简单的测试程序就像我们做的MathClient和一份简明的README.txt说明编译环境、运行时依赖和已知限制。这能为使用者省去大量排查时间也显得非常专业。DLL开发的核心在于定义清晰的接口和遵守严格的二进制契约把这几点想明白、做扎实无论是用VS2017还是更新版本的工具链都能得心应手。
返回列表