资讯详情

资讯详情

MFC DLL从创建到调用:导出函数、类与避坑指南

简介一份围绕MFC DLL开发与调用的入门示例资源面向刚开始接触动态链接库的MFC开发者。压缩包内包含DLL生成工程与客户端调用工程两套源码清晰演示从编写导出类与导出函数、设置def模块定义文件到编译生成dll和lib再到另一个程序中链接调用的一整套流程非常适合通过实际代码动手学习的初学者。整个压缩包共61个文件大小约4.79MB文件类型以h、cpp源文件dll、lib库文件exe可执行文件为主同时保留rc资源脚本、dsp/dsw工程配置以及pdb调试信息既可完整编译运行也可对比分析构建与调试细节。目前已有406人学习/下载这份资源。通过学习这份资源可以直观理解MFC扩展DLL的导出机制、调用方对头文件与导入库的配置要求以及遇到LNK链接错误或“找不到DLL”运行问题时的大致排查思路是一份结构清晰、可直接复现的DLL双向工程示例。 前几天一个朋友问我MFC DLL到底该怎么写、怎么调用说自己照着教程写了一下午结果编译全过了一运行就崩要不就是找不到函数入口调得头皮发麻。这问题我太有感触了早些年第一次做插件架构的桌面应用时就在MFC DLL的导出和加载上踩了一整天的坑。这两天整理手头的项目代码正好把这个主题完整梳理一遍从创建工程到导出函数、导出类再到调用方怎么加载、运行期怎么找到符号一条线走通顺便把那些网上教程不怎么提的坑都标出来。如果你正在用VS2013或者更新版本写MFC程序又想拆出模块做DLL这篇应该能帮你少走不少弯路。1. 项目整体设计与思路拆解1.1 什么样的需求下该选MFC DLL先明确一个前提不是所有DLL都必须用MFC DLL。如果你的模块只是纯算法、数据结构、或者简单的C接口封装那就老老实实写Win32 DLL别把MFC那套运行时拖进去没必要给自己增加依赖负担。我见过不少人一上来就创建MFC DLL工程结果里面连一个CWnd都没用纯粹是给系统塞了一套MFC运行时进进程吃亏不讨好。那什么情况下MFC DLL是合理的呢如果你的模块里需要用到MFC特有的类或者框架比如模块内部有基于CDialog、CWnd的窗口类需要在DLL里创建窗口模块内部使用了CString、CArray、CList这些MFC集合类且打算直接暴露给外部调用方模块需要共享MFC的资源比如对话框模板、图标、字符串等你的主程序本身就是MFC应用模块也深度依赖MFC的消息循环和文档视图架构这种情况下选MFC DLL就是自然的选择。它的核心价值在于让模块和主程序用同一套MFC运行时和同样的内存管理机制跨模块传递MFC对象时不需要考虑堆不匹配的灾难性问题。需要特别说明的是MFC DLL并不是一个单独的DLL类型它更准确地说是一种“配置方案”。在VS里新建工程时你其实有两种选择一个是“MFC DLL”另一个是“MFC扩展DLL”。这两者的区别我后面会展开简单说就是普通MFC DLL适合导出函数和少量类MFC扩展DLL适合导出从MFC类派生的更多类让这个DLL像MFC功能的一部分一样扩展出去。1.2 静态链接MFC与共享MFC DLL的选择创建MFC DLL工程的时候向导会让你选择“静态链接到MFC”还是“共享MFC DLL”。这个选择直接影响DLL分发时的体积和依赖。共享MFC DLL意味着你的DLL运行时会动态加载MFC的dll比如mfc140u.dll、msvcp140.dll这些通用运行库。好处是DLL文件体积小只有几十KB到几百KB坏处是目标机器上如果没有对应的VC运行库环境直接报“找不到mfc140u.dll”然后程序起不来。很多时候用dll修复工具去解决这个问题就是因为开发机上编译出来自己跑得欢换一台干净机器就傻眼。静态链接MFC则是把MFC代码直接编进你的DLL里不用依赖外部的mfc140u.dll。好处是分发的时候带上这一个DLL就能跑坏处是文件体积会暴涨而且Debug版本体积更加感人。此外如果一个进程里有多个静态链接MFC的DLL每个DLL都带着一份MFC运行时副本跨模块传递MFC对象时因为对象在A DLL的堆里分配、在B模块里释放很容易出现堆损坏这就是网上常说“dll冲突”的根源之一。我的建议是如果是个人工具或者小团队内部使用开发环境统一用共享MFC DLL就行开发省事编译也快如果是做商业软件要分发给未知环境的用户优先考虑静态链接或者干脆配好安装包把运行库一起装了。幸运的是VS工程属性里的MFC使用方式随时可以改不用重建工程只需要注意编译前做一次全量重新生成。2. 环境准备与工程创建要点2.1 VS版本与平台工具集的注意事项我一直用的是VS2013到VS2019之间的版本不同版本之间MFC DLL的使用方式大同小异核心的几个工程选项位置基本一致。但有一个点很关键DLL的“字符集”设置必须和调用方保持一致。现在的VS默认都是Unicode字符集如果你在DLL工程里改成“使用多字节字符集”然后主程序是Unicode两边互相传CString或者字符串指针时内存布局完全对不上轻则乱码重则崩溃。另一个重要的点是平台目标x86还是x64。这决定你的DLL和调用方到底能不能匹配上。x64的进程永远没法加载x86的DLL反过来也一样。VS工具栏上的平台下拉框里可以直接切换但要注意如果项目代码量很大切换平台后做一次干净重建是必须的不然残留在中间目录里的旧目标文件会给你制造莫名其妙的链接错误。从VS2015开始VS还引入了平台工具集Platform Toolset的概念比如v140、v141、v142。MFC DLL和调用方如果使用不同的工具集一般也能正常工作因为二进制兼容性做得还不错但我的经验是坚持使用同一套工具集最稳妥特别是Debug/Release都必须对应上。2.2 创建MFC DLL工程的具体步骤在VS中创建MFC DLL工程其实很简单新建项目选择Visual C下的MFC DLL模板起一个项目名点确定然后会出现一个向导对话框让你选“DLL类型”和“附加功能”。DLL类型有三项使用共享MFC DLL的规则DLL、使用静态MFC库的规则DLL、MFC扩展DLL。前两者都是规则DLL区别只在MFC运行时的链接方式第三种是扩展DLL。如果只是导出普通的类和函数选第一个“使用共享MFC DLL的规则DLL”即可。点击完成之后工程自动生成三个关键文件一个是DLL的主cpp文件里面包含了DllMain和基本的启动代码一个是.def文件用于声明导出的模块定义还有一个Resource.h甚至可能带有一个.rc资源文件。之所以提到这三个文件是因为很多人在实际写导出代码的时候把这些自动生成的东西又重复做了一遍导致导出符号重复或者丢失这是新手常碰见的第一个坑。我个人的习惯是如果要用__declspec(dllexport)的方式导出也就是在函数和类声明前加上这个修饰就把.def文件里自动生成的函数声明清空或干脆从工程里删除避免两套导出机制互相干扰。反过来如果你想用.def文件精确控制导出的符号名比如去掉C修饰名导出成没有任何名字修饰的纯C风格函数那就不要再用__declspec(dllexport)。两种方式二选一切记不要混用。3. 核心代码实现与导出细节3.1 用__declspec(dllexport)导出函数从代码到导出表先看一个最简单的例子。假设我们要在MFC DLL里导出一个计算两个整数和的函数同时导出一个稍微复杂点的、返回CString字符串的函数。// MFCDllDemo.h #pragma once #ifdef MFCDLLDEMO_EXPORTS #define MFCDLLDEMO_API __declspec(dllexport) #else #define MFCDLLDEMO_API __declspec(dllimport) #endif extern C MFCDLLDEMO_API int __stdcall Add(int a, int b); extern C MFCDLLDEMO_API void __stdcall GetGreeting(CString strOut);// MFCDllDemo.cpp #include pch.h #include MFCDllDemo.h int __stdcall Add(int a, int b) { return a b; } void __stdcall GetGreeting(CString strOut) { strOut _T(Hello from MFC DLL); }这里有个很容易踩坑的地方很多从Win32 DLL转过来写MFC DLL的人会忽略extern C和__stdcall的作用。extern C是为了让C编译器不要对函数名做名字修饰这样导出表里的符号名就是朴素的Add而不是?_AddYGHHHZ这种火星文。__stdcall则是调用约定它决定了参数的压栈顺序和栈谁来清理。调用约定必须“导出方”和“导入方”完全一致。如果你在DLL里用__stdcall导出在调用方声明时却忘了写__stdcall链接的时候会提示找到不到符号因为C编译器会把调用约定纳入符号修饰的一部分。反过来如果都用__cdecl这是C/C的默认调用约定那就可以省略但为了清晰和稳妥建议无论是导出还是导入声明都显式写明调用约定。MFCDLLDEMO_EXPORTS这个宏是工程创建时VS自动定义的只有在编译当前DLL工程时才存在。通过这个宏同一份头文件在DLL内部编译时被当作dllexport在被调用方包含时被当作dllimport。这是一个很经典的做法叫“导出导入共享头文件”省得维护两份声明。编译完这个工程用dumpbin或依赖工具查看生成的DLL导出表你就能看到Add和GetGreeting这两个符号。有人在这个阶段会遇到导出表是空的或者导出的仍是修饰名十有八九是.def文件残留和extern C没写的问题。这里还要吐槽一个常见误解网上有说法认为MFC DLL不能导出CString参数因为CString在不同模块间传递会出问题。这种说法不准确在“共享MFC DLL”模式下所有模块共享同一份MFC运行时CString类型是全进程统一的跨DLL传递CString引用完全可行。只有在“静态链接MFC”的模式下每个DLL自己有一份CString实现跨模块传递才会出问题。3.2 导出C类把窗口类或业务类暴露给外部导出整个类是MFC DLL的强项。比如你做了个自定义的彩色控件它继承自CWnd你想让这个控件类直接被主程序使用这时候就适合用一个MFC扩展DLL来导出这个类。普通规则DLL也能导出类但扩展DLL能做更多MFC层面的扩展比如把你的类添加到MFC的运行时类体系中并且允许调用方使用动态创建。对于大多数场景导出类用扩展DLL更合适。写法和函数导出类似在类声明前加__declspec(dllexport)// CustomButton.h #pragma once #include afxwin.h class AFX_EXT_CLASS CMyColorButton : public CWnd { public: CMyColorButton(); virtual ~CMyColorButton(); BOOL Create(const CString strText, const CRect rect, CWnd* pParent, UINT nID); void SetBkColor(COLORREF clr); void SetTextColor(COLORREF clr); protected: afx_msg void OnPaint(); DECLARE_MESSAGE_MAP() private: COLORREF m_clrBk; COLORREF m_clrText; };注意AFX_EXT_CLASS这个宏它是MFC框架专门为扩展DLL提供的导出宏在DLL编译时会展开成__declspec(dllexport)在调用方编译时会展开成__declspec(dllimport)。这个宏比你自己定义MFCDLLDEMO_API更省事因为它还自动处理了从MFC类派生时的很多细节。一个非常容易犯的错误是导出派生自CWnd的类时忘了在类的cpp文件里添加消息映射。像OnPaint这种消息处理函数如果没有DECLARE_MESSAGE_MAP和BEGIN_MESSAGE_MAP之间的映射消息根本不会进入到你的函数里控件画不出来或者行为异常排查起来非常痛苦。这也是我后来养成的习惯凡是从CWnd派生出来的类写完之后第一件事就是检查消息映射宏是否完整。3.3 MFC DLL里的资源加载问题DLL里如果带有自己的对话框资源、图标、字符串表需要特别注意资源句柄的问题。默认情况下资源查找是根据进程的主模块句柄通常是EXE的HINSTANCE来进行的。当DLL里的代码需要加载自己的资源时不切换资源模块句柄就会去EXE的资源里找很可能找不到。典型的例子是DLL里导出了一个打开对话框的函数运行时明明DLL里已经放了这个对话框模板弹出的却是主程序的资源或者直接报资源找不到的错误。解决方法是使用AfxSetResourceHandle在DLL入口处切换资源状态。在规则DLL的CWinApp派生类的InitInstance里或者在DLL的入口点DllMain调用后执行AfxSetResourceHandle(dll的实例句柄)。如果用的是MFC扩展DLL则需要在DllMain里调用new CDynLinkLibrary对象这样MFC会自动在资源查找链上加入当前DLL。我自己在实际项目中遇到的情况是DLL里有多个对话框模板主程序调用DLL的ShowSettingsDialog()时如果没做资源切换直接显示主程序里的同名对话框资源。当时完全莫名其妙后来才知道是“先搜索主模块资源”的机制在作怪。那段排查经历让我对MFC资源查找顺序记忆特别深刻。4. 调用方使用MFC DLL的实操流程4.1 隐式链接方式的正确打开姿势调用MFC DLL最常用的方式是隐式链接也就是编译时链接.lib导入库运行时依赖.dll文件。这种方式需要三样东西DLL对应的头文件里面包含导出函数的声明DLL工程生成的.lib文件包含导入符号信息DLL本身程序运行时加载用第3节的那个MFCDllDemo工程举例调用方项目里做三件事第一在调用方项目中添加头文件路径和lib路径到工程属性的VC目录里第二在链接器的输入设置里加上MFCDllDemo.lib到附加依赖项第三把MFCDllDemo.dll复制到调用方EXE的输出目录。然后在代码里像使用普通函数一样#include MFCDllDemo.h #pragma comment(lib, MFCDllDemo.lib) void TestDll() { int sum Add(3, 5); TRACE(_T(sum %d\n), sum); CString str; GetGreeting(str); AfxMessageBox(str); }#pragma comment(lib, ...)是一种偷懒的写法能少去工程属性界面操作但代码里就有了硬编码的路径多人协作时如果库目录不统一容易出问题。更规范的做法是在工程属性里配置好不写在代码里。不过对于个人项目或小团队pragma方式确实方便各取所需吧。需要注意的一点是头文件里CMyColorButton类声明前加了AFX_EXT_CLASS所以这个头文件即便不加MFCDLLDemo.lib编译器也能正确识别它是从DLL导入的链接时需要lib只是提供符号的具体地址信息。4.2 显式加载方式的实战场景还有一些场景不适合隐式链接插件式架构、动态选择模块、DLL无法确定是否存在等。这时候就得上LoadLibrary和GetProcAddress。显式加载不需要lib也不用头文件只要DLL存在运行时手工把导出函数地址取出来通过函数指针调用。看个例子typedef int (__stdcall *PFN_Add)(int a, int b); HMODULE hDll LoadLibrary(_T(MFCDllDemo.dll)); if (hDll ! NULL) { PFN_Add pfnAdd (PFN_Add)GetProcAddress(hDll, Add); if (pfnAdd ! NULL) { int result pfnAdd(10, 20); TRACE(_T(result %d\n), result); } FreeLibrary(hDll); }这里最坑的是函数指针typedef的声明必须和DLL导出时的调用约定完全一致。DLL里如果导出的是__cdecl你在调用方声明PFN_Add时忘了指定__cdecl默认就是__cdecl那没事但如果DLL里是__stdcall你在调用方必须写__stdcall否则GetProcAddress能拿到地址但调用时会因为栈清理约定不一致直接崩掉。这种崩溃发生在运行时用调试器查堆栈通常也是一脸懵因为栈已经乱了。显式加载方式下如果DLL的导出函数是C修饰过的名字没有加extern CGetProcAddress的第二个参数就得写成修饰名。这非常难看所以插件架构里导出的接口几乎全部是extern C风格。这也是我在设计接口时一直坚持的底线所有对外导出函数必须加extern C除非有充分理由不用。4.3 部署时的DLL搜索顺序与常见迷思Windows加载DLL的时候有一套搜索顺序首先看是否已经加载到内存中然后看EXE所在目录接着是系统目录、Windows目录、当前目录最后才是PATH环境变量里的目录。很多人觉得“把DLL放到和EXE同一个目录下”是天经地义的但这个天经地义其实是因为EXE所在目录在搜索顺序里排得靠前。如果DLL依赖其他DLL比如你的MFC DLL还依赖mfc140u.dll而系统目录里有老版本的mfc140u.dll搜索顺序会优先使用EXE目录之外的系统版本——这种情况下你明明把自己的DLL放对了位置却因为版本不一致出现问题。所以部署的经验是除了主DLL最好把用到的VC运行库、MFC运行库也搞清楚版本要么用安装包带运行库要么全静态链接。另外强烈建议在开发机上从“把DLL复制到System32”这个习惯改掉——这会污染全局环境出了问题排查半天而且换机器就失效。正确做法是用VS的“生成后事件”把DLL自动复制到目标目录或者干脆设置好输出目录让多个项目共享一个统一输出文件夹。我们公司当时的做法是所有的DLL和EXE都配置成输出到同一个bin目录VS的调试工作目录也指向那里。整个解决方案编译完bin目录里的文件就是可发布的最小集合拿去测试机一跑就是真实结果省去了复制粘贴的步骤也杜绝了“本地能跑别人机器跑不了”的问题。5. 常见问题与排查技巧实录5.1 经典错误LNK2019和LNK2005的成因与排查LNK2019无法解析的外部符号是做DLL调用时频率最高的问题。报错信息一般是这样的error LNK2019: unresolved external symbol int __stdcall Add(int,int) (?AddYGHHHZ) referenced in function ...出现LNK2019先别慌按顺序排查四个方向第一有没有把.lib文件加进链接器依赖这是最基础的一步很多人只记得拷贝DLL忘了链接.lib。第二导入库的二进制格式是否匹配Debug版DLL生成的.lib不能拿去给Release版调用方链接x64的.lib也不能给x86的调用方链接。你可以在VS里看到lib文件的打开状态和类型dumpbin /headers能看。第三函数名是否匹配如果你在DLL里没有加extern C导出符号名是C修饰名_AddYGHHHZ而调用方声明的也是同样的C修饰名一般能对上但如果DLL里是extern C的Add调用方却声明成没加extern C两边符号就对不上链接器报LNK2019。第四调用约定差异同样会导致符号不匹配。这个前面说过了DLL内使用__stdcall调用方声明为__cdecl链接器看到的修饰名不同直接报错。LNK2005则是“符号重复定义”常常出现在多模块混合链接的场景。比如你既有静态库又有DLL两者都带了MFC运行时就会在链接阶段报一堆重定义。解决方法是检查工程属性的“MFC使用”设置尽量让所有参与链接的模块一致如果确实需要混用那就要考虑结构化设计是否合理了。5.2 运行时找不到DLL或运行时崩溃的处理流程程序编译链接全过了双击运行却弹窗说找不到MFCDllDemo.dll或者提示缺少mfc140u.dll。这属于典型的部署环境问题。最常用的排查方法是打开系统的“高级系统设置-环境变量”或者更直接一点用Process Explorer看程序启动时尝试加载哪些DLL失败。不过对大多数人不一定非要上这些工具可以先检查DLL是否真的和EXE在同一目录DLL是否在PATH里调用的平台x86/x64是否匹配有一种很隐蔽的情况程序从IDE里运行没事但双击运行就失败。原因是IDE会把DLL所在目录加到调试环境的PATH里所以调试时能加载到双击运行时只走默认搜索顺序找不到DLL自然报错。运行时崩溃的问题则更复杂。如果DLL在加载阶段爆炸多半是DllMain里做了不该做的事比如一个极其常见的错误在DllMain里创建窗口或加载其他DLL。Windows加载器持有加载锁在DllMain里做这些事可能导致死锁或者加载顺序问题这在MFC DLL里尤其容易触发因为MFC的初始化也可能加载其他模块。因此DllMain里尽量只做简单的初始化延迟加载可以在第一次调用时进行。如果DLL在正常调用时崩溃排除代码逻辑后要重点怀疑内存边界是不是被跨模块破坏了。比如DLL内部用new分配了内存调用方用delete释放如果两边MFC或C运行时不是同一套堆管理器就不是同一个释放时直接崩溃。解决方式是要么保证所有模块使用共享DLL方式的运行时要么在DLL里提供独立的释放函数让分配和释放永远在同一个堆管理器下。5.3 MFC DLL版本的调试与发布配置建议MFC DLL的Debug和Release版本是严格区分的不是简单的优化差别。Debug版DLL内部带着大量断言和调试信息Release版会清理掉这些如果调用方是Debug版链接了Release版导入库十有八九会链接失败或者运行异常。更难受的是有些项目Debug版工作正常Release版就崩溃。这种问题在MFC DLL场景下通常是以下原因一是Release版的代码路径不同比如某些断言在Debug版里拦截了错误内存访问二是优化导致未定义行为暴露出来三是资源和数据段的对齐方式不同。排查这种问题最笨也最有效的办法是二分法先在Release版里关掉优化改为/Od测试如果正常了再逐步开启优化定位是哪项优化引发的。配置发布时还有一个容易被忽略的问题版本信息。VC运行库和MFC库有好几个版本混在一起挂靠比如一个程序里既有vc140的模块又有vc142的模块虽然多数情况没问题但难免遇到行为不一致的时候。我的建议是团队统一使用同一个VS版本并确保所有模块的平台工具集一致。除非有第三方强制不了的依赖否则别混用。依赖项查看方面我比较喜欢用Dependencies这个开源工具来检查DLL的依赖树比老牌Dependency Walker对新版VS支持更好一些。它能直观看到到底缺哪个运行库也能看到导出表里的函数有哪些排查问题效率高很多。6. 从简单到复杂MFC DLL的一些延展用法6.1 把MFC DLL和自定义控件结合起来我有一个实际的项目案例给主程序做一个属性面板面板里需要各种自定义控件比如带颜色的按钮、圆角列表框。传统做法是把这些控件的代码全部编译进主程序缺点是一个小小的控件改动整个主程序就要重新编译编译时间十几分钟。后来我把这些控件单独封装成一个MFC扩展DLL导出基类主程序通过头文件和.lib链接。改动控件代码后只需要编译这一个DLL工程替换bin目录里的DLL重新启动程序新UI立即生效。这个开发效率的提升非常明显尤其是需要频繁调整控件的阶段每次改动省下的时间和CPU占用积少成多非常可观。具体的实现方式和我前面说的CMyColorButton类似只是增加了一件事在扩展DLL的cpp文件里需要重写DllMain并创建一个CDynLinkLibrary对象。这个CDynLinkLibrary是MFC扩展DLL的“注册凭证”MFC会通过它把DLL加入资源搜索链同时也能让程序通过MFC的RUNTIME_CLASS机制动态创建你导出的类。// MFCExtDll.cpp #include pch.h #include resource.h static AFX_EXTENSION_MODULE ExtDll { NULL, NULL }; extern C int APIENTRY DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID) { if (dwReason DLL_PROCESS_ATTACH) { if (!AfxInitExtensionModule(ExtDll, hInstance)) return 0; new CDynLinkLibrary(ExtDll); } else if (dwReason DLL_PROCESS_DETACH) { AfxTermExtensionModule(ExtDll); } return 1; }这段代码是MFC扩展DLL的标准骨架模板生成的工程基本都有重点是理解它的职责AfxInitExtensionModule把扩展模块的资源和运行时信息注册好CDynLinkLibrary再把模块挂到MFC的核心链上。如果你把这段代码删了或者改错了DLL也能编译出来但主程序加载的时候要么资源找不到要么动态创建类失败。6.2 多语言与资源隔离DLL的另一个实用场景MFC DLL不仅可以做代码模块化还可以做资源模块化。我做过一个工具软件界面字符串和对话框模板全部放在一个纯资源DLL里主程序根据用户选择加载对应语言的资源DLL实现界面的快速切换。这个做法比直接在代码里写各种语言的字符串数组要干净得多。你只需要把同一个.rc文件分别翻译成不同语言版本分别编译成UI_DLL_ZH.dll、UI_DLL_EN.dll等运行时用LoadLibrary切换到目标语言然后调用AfxSetResourceHandle把资源句柄切过去。MFC自动地会根据当前的资源句柄去找对话框模板和字符串资源。这个方案对MFC DLL的要求其实不高甚至可以说用纯Win32资源DLL都可以实现。但放在MFC DLL里有一个额外的好处资源DLL本身也可以包含一些查找逻辑和辅助函数比如根据key返回翻译后的CString。这样一个DLL把资源和逻辑都包了外部调用只需要一个接口隔离得比较干净。我在实际项目中还踩过一个坑切换资源DLL需要先FreeLibrary旧DLL再LoadLibrary新DLL如果主程序里还有对话框或者字符串的指针在使用旧DLL里的资源切换后可能指向已经被卸载的内存访问就崩。所以切换前要确保所有基于旧资源创建出来的窗口已经销毁或者能重建稳妥一点的做法是做窗口重建不要试图原地换资源。6.3 最后一个建议从设计上最大化降低DLL之间的耦合MFC DLL的坑大部分不是技术难度高而是模块之间的边界没设计好。如果你只是简单地“把代码扔进DLL”那只是物理上的拆分逻辑上依然耦合在一起该出问题还是会出。比较好的设计模式是DLL对外暴露的是纯C风格的函数接口内部才使用MFC类和数据结构。这样DLL的调用方不需要关心内部用的是MFC还是什么其他框架接口稳定A/B切换DLL实现也方便。典型的例子是我们做的一个计算引擎模块外部提供的是Initialize/Process/Release三个函数内部用MFC的CString处理配置用CArray存储临时数据但这些细节完全不暴露给调用方。这样做还有一个额外好处接口是纯C函数不用关心调用方是什么技术栈。将来如果主程序要从MFC迁移到别的UI框架这个DLL可以留着直接用不需要大改。这一点我深有体会公司的老项目从MFC界面层慢慢拆分成逻辑层DLL然后UI层逐步换成了新的框架整个过程逻辑层几乎没动就因为当初接口设计得够干净。写在最后我实际开发中体会最深的一点是MFC DLL本身不难难的是保持模块边界的清晰。导出函数、导出类、资源切换、运行时一致性这些点用点心都能掌控住真正让人头疼的全是跨模块交互时的隐性问题。如果你正准备拆分一个MFC项目希望你从第一行代码开始就想清楚接口的边界而不是把DLL当成一个“代码收纳箱”什么问题都往里塞。毕竟DLL的价值在于让系统更清晰而不是制造新的混乱。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →