☰
VS2010下DLL与LIB的隐式加载和显示加载详解:原理、配置与实战避坑
2026/10/18 13:19:41 网站建设 项目流程

1. 为什么在VS2010里搞明白DLL加载方式这么重要

如果你现在还在维护一些老项目,尤其是工业控制、医疗设备上位机、或者某些产线测试软件,大概率绕不开VS2010这个老伙计。它虽然年纪大了,但胜在稳定,很多底层库、驱动接口、硬件SDK至今仍然只提供VS2010能直接编译的版本。而一旦涉及到模块化开发,DLL和LIB的加载方式就成了必须迈过去的坎。

我见过太多人在这上面栽跟头:明明代码写对了,编译也过了,一运行就弹窗说找不到某某DLL;或者链接阶段直接报一堆“无法解析的外部符号”,查了半天也不知道是LIB没配对还是函数声明写错了。更麻烦的是,有些场景必须用显示加载,有些场景隐式加载更省事,选错了方案,后期维护成本会成倍增加。

这篇文章就是要把VS2010下DLL和LIB的两种加载方式彻底讲透。显示加载,也就是运行时通过API动态获取函数地址;隐式加载,也就是编译链接期就把LIB绑进去。我会从原理、操作步骤、参数配置、常见报错、排查思路几个维度展开,尽量让刚接触这块的人也能跟着做出来,同时也会分享一些只有实际踩过坑才知道的细节。

适合谁看?如果你正在用VS2010做Windows平台C/C++开发,需要调用第三方库或者自己封装模块,这篇文章基本能覆盖你90%以上的场景。即使你用的是更高版本的VS,底层逻辑也是一样的,只是界面位置略有差异。

2. 先把DLL和LIB的关系理清楚,不然后面全是坑

2.1 DLL是运行时用的,LIB是链接时用的

很多人一开始分不清DLL和LIB到底谁是谁。简单说,DLL是动态链接库,里面装着编译好的机器码,程序运行起来之后才会被加载到内存里执行。LIB是静态库或者导入库,它本身不包含可执行代码(静态库除外),主要作用是告诉链接器:“你要调用的那个函数在哪个DLL里,它的编号是什么”。

在VS2010里,如果你拿到一个第三方库,通常会看到这样的文件组合:一个.h头文件,一个.lib文件,一个.dll文件。头文件负责声明函数原型,LIB负责提供链接信息,DLL负责实际执行。三者缺一不可。

注意:有些库只提供DLL不提供LIB,这时候就只能用显示加载。反过来,如果只提供LIB不提供DLL,那可能是静态库,直接编进exe里,不涉及动态加载。

2.2 隐式加载和显示加载的本质区别

隐式加载,也叫加载时链接。程序启动的时候,Windows加载器会自动把依赖的DLL加载到进程空间里。如果DLL不存在或者导出函数对不上,程序直接启动失败,连main函数都进不去。这种方式的好处是调用起来跟普通函数一样,不需要额外的代码,坏处是灵活性差,DLL必须放在系统能找到的路径下。

显示加载,也叫运行时链接。程序启动的时候不加载DLL,等到真正需要用了,才通过LoadLibrary或者LoadLibraryEx把DLL加载进来,然后用GetProcAddress拿到函数地址,通过函数指针调用。用完还可以FreeLibrary卸载。这种方式灵活,可以控制加载时机、可以处理加载失败、可以做插件式架构,但代码量会多一些,而且函数指针的类型声明必须和DLL里的导出一致,否则调用就会崩溃。

我个人的经验是:如果是自己项目内部的模块,DLL位置固定,用隐式加载最省事;如果是需要热插拔的插件、或者DLL可能不存在需要优雅降级的场景,必须用显示加载。

2.3 VS2010下两种方式的工程配置差异

在VS2010里,隐式加载需要在工程属性里配置两样东西:附加包含目录指向头文件所在文件夹,附加库目录指向LIB所在文件夹,然后在链接器输入里加上LIB文件名。显示加载则完全不需要这些配置,只需要头文件(或者自己声明函数原型),LIB和DLL都不需要在工程里引用。

这里有个细节:隐式加载时,LIB文件必须和DLL的导出符号完全匹配。如果LIB是旧的,DLL是新的,链接可能通过但运行会崩。显示加载则没有这个问题,因为函数地址是运行时动态查的,只要DLL里确实有这个导出函数就行。

3. 隐式加载的完整操作流程与配置细节

3.1 头文件、LIB、DLL三件套的放置策略

假设你拿到一个第三方库,文件结构是这样的:include\mylib.h、lib\mylib.lib、bin\mylib.dll。在VS2010里,我习惯把这三个文件夹放在解决方案目录旁边,然后用相对路径去引用,这样整个工程拷贝到别的机器上也能直接编译。

具体操作:右键工程 -> 属性 -> 配置属性 -> C/C++ -> 常规 -> 附加包含目录,填入$(SolutionDir)..\include。然后链接器 -> 常规 -> 附加库目录,填入$(SolutionDir)..\lib。最后链接器 -> 输入 -> 附加依赖项,填入mylib.lib。

提示:$(SolutionDir)是VS2010的宏,指向解决方案文件所在目录。用宏的好处是路径不会因为工程移动而失效。

DLL文件则要放到exe生成目录下,或者放到系统PATH能找到的目录。我一般会在工程属性里配置生成后事件,用xcopy把DLL自动拷贝到输出目录,省得每次手动复制。

3.2 链接器输入项的填写规则与顺序问题

附加依赖项里填LIB文件名时,多个LIB之间用空格或者分号隔开。这里有个坑:LIB的顺序有时候会影响链接结果。如果LIB之间有依赖关系,被依赖的LIB要放在后面。比如A.lib依赖B.lib,那顺序应该是A.lib B.lib。VS2010的链接器是从左到右扫描的,顺序反了就可能报“无法解析的外部符号”。

另外,如果LIB文件名和系统库重名,比如你自己编了一个kernel32.lib,那链接器可能会优先找到系统的。这种情况极少见,但一旦遇到,排查起来很头疼。解决办法是给LIB改个独特的名字,或者用完整路径。

3.3 导出函数的声明方式:__declspec(dllexport)和.def文件

隐式加载能成功的前提是DLL正确导出了函数。VS2010下有两种导出方式:一种是在函数声明前加__declspec(dllexport),另一种是使用.def模块定义文件。

__declspec(dllexport)写起来简单,直接在头文件里加就行。但有个问题:C++编译器会对函数名进行名称修饰(name mangling),导致导出的函数名变成一堆乱码。如果调用方也是C++并且用同样的头文件,那没问题;但如果调用方是C语言或者别的编译器,就会找不到函数。

这时候.def文件就更可靠。它明确指定导出函数的名称和序号,不受名称修饰影响。我一般推荐对外发布的库用.def,内部使用的库用__declspec(dllexport)。

// mylib.h #ifdef MYLIB_EXPORTS #define MYLIB_API __declspec(dllexport) #else #define MYLIB_API __declspec(dllimport) #endif extern "C" MYLIB_API int Add(int a, int b);

上面这段代码是标准写法。MYLIB_EXPORTS宏在生成DLL的工程里定义,调用方不定义,这样同一个头文件既能用于导出也能用于导入。extern "C"是为了避免C++名称修饰,让函数名保持为Add。

3.4 运行时报“找不到DLL”的排查链路

隐式加载最常见的报错就是程序启动时弹窗:“无法启动此程序,因为计算机中丢失mylib.dll。尝试重新安装该程序以解决此问题。”

排查步骤我一般按这个顺序来:

  1. 确认DLL文件确实存在于exe同级目录下。有时候VS的调试工作目录和输出目录不是同一个,导致你以为拷过去了其实没有。
  2. 用Dependency Walker或者VS自带的dumpbin /dependents查看exe依赖了哪些DLL。如果列表里有mylib.dll,说明链接没问题,纯粹是路径问题。
  3. 检查DLL的位数是否和exe一致。32位exe不能加载64位DLL,反之亦然。VS2010默认生成32位程序,如果第三方DLL是64位的,就会加载失败。
  4. 检查DLL是否依赖了其他缺失的DLL。用dumpbin /dependents mylib.dll可以看到它的依赖树,有时候mylib.dll本身没问题,但它依赖的另一个DLL不在。

注意:在Win7及以上系统,如果DLL放在System32目录下,32位程序实际上会去SysWOW64找。这个重定向机制经常让人迷惑,建议不要把第三方DLL放到系统目录。

4. 显示加载的代码实现与函数指针管理

4.1LoadLibrary和GetProcAddress的基本用法

显示加载的核心就两个API:LoadLibrary负责把DLL加载到内存,返回一个模块句柄;GetProcAddress负责根据函数名找到函数地址,返回一个函数指针。

#include <windows.h> #include <stdio.h> typedef int (*AddFunc)(int, int); int main() { HMODULE hModule = LoadLibraryA("mylib.dll"); if (hModule == NULL) { printf("加载DLL失败,错误码:%d\n", GetLastError()); return -1; } AddFunc add = (AddFunc)GetProcAddress(hModule, "Add"); if (add == NULL) { printf("找不到Add函数,错误码:%d\n", GetLastError()); FreeLibrary(hModule); return -1; } int result = add(3, 5); printf("结果:%d\n", result); FreeLibrary(hModule); return 0; }

这段代码是最基础的显示加载模板。LoadLibraryA是ANSI版本,LoadLibraryW是宽字符版本。VS2010默认工程可能是Unicode字符集,这时候直接写LoadLibrary会映射到LoadLibraryW,传字符串要用L"mylib.dll"或者_T("mylib.dll")。

4.2 函数指针类型定义与调用约定匹配

函数指针的类型必须和DLL里导出的函数签名完全一致,包括调用约定。Windows下常见的调用约定有__cdecl、__stdcall、__fastcall。如果DLL里用的是__stdcall,你在typedef里没写,调用的时候栈就会不平衡,轻则结果错误,重则程序崩溃。

typedef int (__stdcall *AddFunc)(int, int);

怎么知道DLL用的是什么调用约定?看头文件里的声明。如果没有头文件,用dumpbin /exports mylib.dll看导出函数的修饰名。__stdcall的函数名会变成_Add@8这种形式,__cdecl则是_Add或者Add。

提示:64位程序只有一种调用约定,不需要考虑这个问题。但VS2010默认生成32位程序,所以调用约定必须匹配。

4.3 用GetProcAddress按序号导出的特殊情况

有些DLL导出函数时只给了序号,没有给名字。这时候GetProcAddress的第二个参数要传MAKEINTRESOURCE(序号)。

AddFunc add = (AddFunc)GetProcAddress(hModule, MAKEINTRESOURCE(1));

按序号导出的好处是加载速度快一点点,而且不受名称修饰影响。坏处是可读性差,而且不同版本的DLL序号可能变化。我一般只在没有头文件、又不想折腾名称修饰的时候才用这种方式。

4.4 显示加载的异常处理与资源释放

显示加载最大的优势就是可以优雅地处理失败。LoadLibrary返回NULL时,用GetLastError拿到错误码,常见的错误码有:

错误码含义可能原因
126找不到指定的模块DLL文件不存在或路径不对
193不是有效的Win32应用程序DLL位数不对或文件损坏
998内存位置访问无效DLL依赖的其他模块缺失
127找不到指定的程序导出函数名写错或不存在

拿到错误码之后,可以用FormatMessage把它转成可读的字符串,方便日志记录。

资源释放方面,每次LoadLibrary成功之后,引用计数会加一。对应的FreeLibrary会让引用计数减一。如果加载了多次,必须释放相同次数。我习惯用一个RAII风格的封装类来管理,构造时加载,析构时释放,避免忘记。

class DllLoader { public: DllLoader(const char* name) : m_hModule(NULL) { m_hModule = LoadLibraryA(name); } ~DllLoader() { if (m_hModule) FreeLibrary(m_hModule); } bool IsValid() const { return m_hModule != NULL; } HMODULE Handle() const { return m_hModule; } private: HMODULE m_hModule; };

5. 两种加载方式在实际项目中的选型对比

5.1 启动速度与内存占用的权衡

隐式加载在程序启动时就把所有依赖的DLL加载进来了,启动速度会慢一些,尤其是依赖链很长的时候。但好处是运行期间调用函数没有额外开销,直接就是一次跳转。

显示加载把加载时机推迟到了第一次使用的时候,启动速度快。但每次调用函数都要通过函数指针,比直接调用多一次间接寻址。不过在现代CPU上,这点开销基本可以忽略。

内存占用方面,隐式加载的DLL在程序整个生命周期内都驻留在内存里。显示加载可以在不用的时候FreeLibrary卸载掉,释放内存。对于内存紧张的嵌入式环境,这个差异可能很关键。

5.2 部署灵活性与版本管理的差异

隐式加载的部署比较死板:DLL必须放在exe能找到的路径下,而且版本必须和编译时用的LIB匹配。如果用户机器上已经有一个同名但不同版本的DLL,可能会加载到错误的版本,导致崩溃。

显示加载可以指定完整路径,比如LoadLibraryA("C:\\MyApp\\Plugins\\mylib.dll"),这样就不会被系统路径里的同名DLL干扰。而且可以在运行时判断DLL版本,决定加载哪个。

我做过一个项目,需要同时支持两个版本的硬件SDK。用显示加载,根据硬件型号加载对应的DLL,代码里用同一套函数指针类型,切换起来非常干净。如果用隐式加载,就得编译两个exe,维护成本高很多。

5.3 调试难度与错误定位效率

隐式加载的调试相对简单,因为链接器会在编译期检查符号是否存在。如果LIB里没有这个函数,直接编译不过,问题暴露得早。

显示加载的问题往往在运行时才暴露,而且错误信息不如隐式加载直观。比如函数指针类型写错了,编译期不会报错,运行到调用那一行才崩溃,堆栈信息可能指向一个完全无关的地方。

我的经验是:显示加载的代码一定要加详细的日志,记录每次LoadLibrary和GetProcAddress的结果。一旦出问题,看日志就能定位到是哪个DLL、哪个函数出的错。

5.4 插件式架构为什么必须用显示加载

如果你要做插件系统,显示加载是唯一的选择。主程序在启动时扫描插件目录,遍历所有DLL文件,尝试加载并查找约定的入口函数(比如GetPluginInterface)。找到就调用,找不到就跳过。整个过程主程序不需要知道任何插件的具体信息。

隐式加载做不到这一点,因为它在编译期就确定了要加载哪些DLL,没法在运行时动态发现。

// 插件入口函数类型 typedef IPlugin* (*GetPluginFunc)(); // 遍历目录下的所有DLL foreach (dllFile in pluginDir) { HMODULE h = LoadLibraryA(dllFile); if (!h) continue; GetPluginFunc getPlugin = (GetPluginFunc)GetProcAddress(h, "GetPluginInterface"); if (getPlugin) { IPlugin* plugin = getPlugin(); pluginManager.Register(plugin); } }

这种模式在音频处理、图像滤镜、协议解析等场景非常常见。

6. 那些年我踩过的坑与排查经验

6.1 LIB文件与DLL不匹配导致的链接错误

有一次我拿到一个第三方库,头文件、LIB、DLL都有。编译通过了,但链接时报“无法解析的外部符号 _Add@8”。我检查了头文件,函数声明是int Add(int, int),没有__stdcall。但LIB里的符号是_Add@8,说明DLL是用__stdcall编译的。

解决办法是在头文件里加上__stdcall,或者用.def文件重新生成LIB。这个问题的根源是LIB和DLL的编译选项不一致,但第三方库的文档没写清楚。

提示:拿到第三方库时,先用dumpbin /exports看DLL的导出符号,再用dumpbin /symbols看LIB里的符号,两者对不上就说明有问题。

6.2 路径问题:工作目录与exe目录不一致

VS2010调试的时候,默认工作目录是工程目录,而不是exe输出目录。如果你把DLL放在exe旁边,但工作目录是工程目录,隐式加载就会失败。

解决办法:工程属性 -> 调试 -> 工作目录,改成$(OutDir)。或者把DLL也拷贝一份到工程目录。我一般用前者,更干净。

6.3 32位与64位DLL混用的典型报错

32位程序加载64位DLL,LoadLibrary会返回NULL,错误码193。反过来也一样。这个错误信息很明确,但新手容易忽略。

在VS2010里,默认平台是Win32。如果你拿到的DLL是64位的,需要把工程配置管理器里的平台改成x64。但注意,改成x64之后,所有依赖的LIB也必须是64位的,否则链接会报错。

6.4 显示加载时函数指针类型写错的隐蔽后果

函数指针类型写错是最难查的问题之一。比如DLL里是int Add(int, int),你typedef成了int (*AddFunc)(int)。编译没问题,GetProcAddress也能拿到地址,但调用的时候参数传递就乱了,可能返回一个莫名其妙的值,也可能直接崩溃。

我的做法是:typedef之后,用static_assert或者编译期检查确认参数个数和类型。VS2010不支持static_assert,但可以用一些模板技巧做检查。更简单的办法是,把typedef和头文件里的声明放在一起,改的时候一起改。

6.5 依赖DLL缺失导致的加载失败排查

有时候LoadLibrary失败不是因为目标DLL不存在,而是它依赖的某个DLL不存在。比如mylib.dll依赖了msvcr100.dll,而目标机器上没装对应的运行库。

排查方法:用dumpbin /dependents mylib.dll列出所有依赖项,然后逐个检查是否存在。或者用Process Monitor监控文件系统访问,看加载器在找哪个文件时失败了。

注意:VS2010编译的程序默认依赖msvcr100.dll和msvcp100.dll。如果目标机器没装VS2010运行库,需要把这两个DLL一起打包,或者改成静态链接运行库(工程属性 -> C/C++ -> 代码生成 -> 运行库 -> 多线程(/MT))。

7. 工程配置的自动化与可复现实践

7.1 用属性表统一管理包含目录和库目录

如果一个解决方案里有多个工程都要用同一个第三方库,每个工程单独配置一遍很麻烦,而且容易漏。VS2010的属性表(.props文件)可以解决这个问题。

创建一个mylib.props文件,在里面配置好包含目录、库目录、附加依赖项。然后在每个工程里添加这个属性表。这样改一处,所有工程都生效。

属性表的位置:视图 -> 属性管理器 -> 右键工程 -> 添加现有属性表。

7.2 生成后事件自动拷贝DLL到输出目录

隐式加载需要DLL在exe旁边。手动拷贝容易忘,用生成后事件自动处理最靠谱。

工程属性 -> 生成事件 -> 生成后事件 -> 命令行:

xcopy /Y /D "$(SolutionDir)..\bin\mylib.dll" "$(OutDir)"

/Y表示覆盖时不提示,/D表示只拷贝源文件比目标文件新的情况。这样每次编译完,DLL都会自动同步到输出目录。

7.3 用dumpbin和Dependency Walker做依赖检查

dumpbin是VS自带的命令行工具,在VS2010命令提示符下可以直接用。常用命令:

  • dumpbin /exports mylib.dll:查看导出函数列表
  • dumpbin /dependents mylib.dll:查看依赖的DLL列表
  • dumpbin /headers mylib.dll:查看文件头信息,包括位数

Dependency Walker是图形化工具,可以直观地看到依赖树,还能标出缺失的模块。不过它对新版Windows的API集支持不太好,有时候会误报。我一般用dumpbin做初步检查,用Dependency Walker做深入分析。

7.4 版本控制中该提交哪些文件

如果用隐式加载,LIB文件必须提交到版本控制里,因为链接的时候需要它。DLL文件可以提交,也可以不提交,但建议提交,方便新同事拉下代码就能跑。

如果用显示加载,LIB和DLL都不需要提交到代码仓库,但需要在文档里写清楚去哪里获取。我一般会在仓库里放一个third_party目录,里面放一个README.md说明依赖的库版本和下载方式。

头文件无论哪种方式都必须提交,因为编译的时候需要。

8. 从隐式加载迁移到显示加载的实操建议

8.1 迁移的触发条件与收益评估

什么时候该考虑从隐式加载迁移到显示加载?我总结了几种情况:

  • 需要支持多个版本的同一个库,根据运行环境动态选择
  • 需要做插件式架构,主程序不依赖具体实现
  • 需要优雅处理DLL缺失,而不是直接崩溃
  • 需要控制DLL的加载时机,比如延迟到用户点击某个按钮才加载

迁移的收益是灵活性和健壮性提升,代价是代码量增加、调试复杂度上升。如果项目规模不大,DLL位置固定,隐式加载完全够用,没必要为了“看起来高级”而迁移。

8.2 逐步替换的步骤与回归测试要点

迁移的时候不要一次性把所有调用都改掉,容易出乱子。我的做法是:

  1. 先封装一个DLL加载器类,负责LoadLibrary、GetProcAddress、FreeLibrary。
  2. 把原来的直接函数调用改成通过加载器获取函数指针再调用。
  3. 每改一个模块,就跑一遍回归测试,确保功能没受影响。
  4. 全部改完之后,移除工程里的LIB依赖和附加包含目录配置。

回归测试的重点是:DLL加载失败的场景、函数指针为空的场景、多次加载和释放的场景。这些在隐式加载下不会出现,但显示加载必须处理。

8.3 迁移后如何保持代码可读性

显示加载的代码容易写得又臭又长,全是GetProcAddress和强制类型转换。我的经验是:

  • 把函数指针的获取封装到一个结构体或者类里,初始化的时候一次性获取所有需要的函数。
  • 用宏或者模板减少重复代码。
  • 给每个函数指针起一个和原函数一样的名字,调用的时候看起来和普通函数差不多。
struct MyLibApi { AddFunc Add; SubFunc Sub; bool Load(HMODULE h) { Add = (AddFunc)GetProcAddress(h, "Add"); Sub = (SubFunc)GetProcAddress(h, "Sub"); return Add && Sub; } };

这样调用的时候就是api.Add(3, 5),可读性和直接调用差不多。

8.4 兼容旧代码的混合加载策略

有些老代码已经用隐式加载写好了,不想大改,但又想在某些场景下用显示加载。这时候可以混合使用:主程序用隐式加载保证基本功能,插件部分用显示加载做扩展。

需要注意的是,同一个DLL如果既被隐式加载又被显示加载,引用计数会累加。FreeLibrary的时候要确保释放次数和加载次数一致,否则DLL会一直驻留在内存里。

我一般建议:要么全隐式,要么全显示,混合使用只适合过渡期,长期来看还是统一一种方式更好维护。

9. 一些零散但很关键的实操心得

9.1 关于extern "C"和名称修饰的取舍

extern "C"可以避免C++名称修饰,让导出函数名保持简洁。但代价是失去了函数重载能力。如果你的库需要导出重载函数,就不能用extern "C",只能接受名称修饰,或者用.def文件手动指定导出名。

我个人的选择:对外发布的C接口用extern "C",内部C++模块用名称修饰,反正调用方也是C++,用同一个头文件就行。

9.2 DLL入口点DllMain里不要做复杂操作

DllMain是DLL的入口函数,在加载和卸载的时候被调用。这里面能做的事情非常有限,不要在里面调用LoadLibrary、CreateThread、CoInitialize等函数,否则可能导致死锁或者加载失败。

我见过有人在DllMain里初始化网络连接,结果程序启动直接卡死。正确的做法是在DllMain里只做最简单的标记,真正的初始化放到一个单独的Init函数里,由调用方显式调用。

9.3 导出变量和导出类的注意事项

除了导出函数,DLL还可以导出变量和类。导出变量用__declspec(dllexport)修饰变量声明,调用方用__declspec(dllimport)导入。但导出变量有个问题:如果DLL和exe的CRT版本不一致,可能会导致访问违规。

导出类更复杂,因为涉及到虚函数表、构造函数、析构函数的跨模块调用。我一般不建议跨DLL导出C++类,除非你能保证两边用完全相同的编译器和编译选项。更安全的做法是导出纯虚接口,用工厂函数创建对象。

9.4 用#pragma comment(lib, "xxx.lib")简化链接配置

除了在工程属性里配置LIB,还可以在代码里用#pragma comment(lib, "mylib.lib")。这样链接器会自动链接这个LIB,不需要手动配置附加依赖项。

好处是代码自包含,拷贝到别的工程里也能直接编译。坏处是LIB的搜索路径还是要在工程里配置,而且如果LIB不存在,报错信息不如手动配置的直观。

我一般在写示例代码或者小型工具的时候用#pragma comment,大型项目还是用属性表统一管理。

9.5 调试DLL加载问题的几个命令行工具

除了前面提到的dumpbin和Dependency Walker,还有几个工具很有用:

  • where mylib.dll:在PATH里查找DLL的位置,快速确认系统能不能找到
  • tasklist /m mylib.dll:查看哪些进程加载了mylib.dll
  • Process Explorer:图形化工具,可以看进程加载的所有模块,还能直接卸载DLL

这些工具在排查“DLL被谁占用了”“为什么加载的是旧版本”这类问题时特别高效。

9.6 关于VS2010的一些历史遗留问题

VS2010毕竟是2010年的产品,有些行为和新版VS不太一样。比如:

  • 默认字符集是Unicode,但很多老代码是ANSI的,混用的时候要注意L""和""的区别
  • 不支持C++11的大部分特性,写代码的时候不能用auto、nullptr、std::function这些
  • 调试器的表达式求值功能比较弱,复杂一点的STL容器看起来很不方便

如果条件允许,我建议尽量升级到更高版本的VS。但如果项目必须用VS2010,那就接受这些限制,把精力放在代码质量和架构设计上。

9.7 一个真实的排查案例:DLL加载成功但函数调用崩溃

最后分享一个我实际遇到的案例。有个项目用显示加载调用第三方DLL,LoadLibrary成功,GetProcAddress也成功,但一调用函数就崩溃。

排查过程:先用dumpbin /exports确认函数名没错,然后用dumpbin /headers检查DLL位数,发现是32位的,和exe一致。接着检查调用约定,发现DLL用的是__stdcall,但我的typedef里写的是默认的__cdecl。改成__stdcall之后问题解决。

这个问题的隐蔽之处在于:GetProcAddress不检查调用约定,所以能拿到地址。但调用的时候栈不平衡,导致崩溃。如果DLL的导出函数恰好没有参数,可能不会崩溃,只是返回值不对,那就更难查了。

所以我的建议是:显示加载的时候,一定要用dumpbin /exports看清楚导出函数的修饰名,根据修饰名判断调用约定。_Add@8这种形式就是__stdcall,_Add或者Add就是__cdecl。不要凭感觉写typedef,一定要以实际导出符号为准。

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

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

立即咨询