使用 Deleaker 精准定位内存与 GDI 资源泄漏
使用 Deleaker 精准定位内存与 GDI 资源泄漏
在日常 C++ / MFC 桌面应用开发中,内存泄漏与 GDI 资源泄漏常常会导致程序崩溃、界面闪烁、资源耗尽等问题。本文将介绍如何安装并使用 Deleaker 工具,并与 Visual Studio 自带的内存快照工具进行对比,帮助开发者选择最合适的泄漏检测手段。
一、Deleaker 简介
Deleaker 是一款功能强大的内存和资源泄漏检测工具,支持:
- 堆内存(
new
/malloc
)泄漏检测 - GDI 对象泄漏(
CreatePen
/CreateFont
等) - USER 对象泄漏(如窗口、菜单、图标)
- 句柄(
CreateEvent
/CreateFile
等)泄漏 - DC(设备上下文)泄漏
- 与 Visual Studio 无缝集成,支持快照比对、堆栈回溯、符号定位
二、安装与首次配置
1. 下载与安装
前往官网:https://deleaker.com,下载安装包。
安装过程中建议勾选:
- ✅ Visual Studio 插件(建议勾选)
- ✅ 独立 GUI 工具(可用于调试任意 EXE)
2. 第一次启动时的提示说明
① 选择分析模式(Profiler Mode)
启动 Deleaker 后会提示:
你需要根据项目类型选择合适的分析模式:
-
✅ Unmanaged Code Profiling Mode(推荐 C++ 开发者选择)
- 用于:原生 C/C++ 项目、MFC 应用程序、Win32 API 程序
- 检测内容:堆内存泄漏、GDI 对象、USER 对象、系统句柄等
- 特点:无需 CLR 支持,适合桌面软件、驱动工具、传统 C++ 项目
-
✅ .NET Profiling Mode(适用于 C#/.NET 程序)
- 用于:C#、VB.NET、WPF、WinForms 等托管应用程序
- 检测内容:.NET 托管对象、未释放的
System.IO.Stream
、数据库连接等 - 特点:需启用 CLR Profiler 接口,可追踪托管堆
如果你不确定,可以参考以下简表:
项目类型 | 推荐模式 |
---|---|
C++ MFC 桌面应用 | Unmanaged |
C++ 控制台程序 | Unmanaged |
C# WinForms/WPF 应用 | .NET |
.NET Core 控制台应用 | .NET |
混合 C++/CLI 工程 | 建议拆分模块独立调试 |
⚠️ 注意:选择后仍可在 Deleaker 设置中修改,不是永久绑定。
② 是否启用调试监控提示
- 选择
Yes
:允许 Deleaker 注入检测器,记录资源分配情况(建议) - 勾选“Don’t ask me again”,避免每次启动弹出
③ 选择监控的资源类型
默认全选:
- ✅ memory(new/malloc)
- ✅ GDI objects(画笔、字体等)
你也可以只保留感兴趣的部分(例如仅 GDI)提高运行速度。
三、基本用法
1. 拍摄快照(Snapshot)
- 启动调试(F5)
- 打开
Deleaker
面板,点击 Take Snapshot - 多次快照可对比内存变化(使用
Compare with...
)
2. 定位泄漏
- 快照后资源列表中会显示未释放对象
- 支持按照类型(Heap / GDI / Handle)筛选
- 双击条目可跳转至源代码位置
四、常见资源泄漏场景示例
1. 堆内存泄漏(new
/malloc
)
void TestLeak()
{for (int i = 0; i < 10; ++i){int* p = new int[100];// 未 delete[] p}
}
问题:分配了堆内存,但未释放,导致堆空间不断增长。Deleaker 可准确捕捉每个分配点。
2. GDI 资源泄漏(CreateXxx
)
void CMyDialog::OnPaint()
{for (int i = 0; i < 5; ++i){HBRUSH hBrush = CreateSolidBrush(RGB(0, 255, 0));HDC hdc = GetDC()->GetSafeHdc();SelectObject(hdc, hBrush);// 忘记 DeleteObject(hBrush);}
}
问题:GDI 对象是系统资源,如果不释放,会造成绘图失败、UI 闪烁等问题。
❗ Visual Studio 快照工具的表现:
使用 Visual Studio 的“诊断工具 > 内存使用”进行快照时,对上述 GDI 资源分配并不会显示明确的分配位置。
在快照报告中看到类似:
这表示该对象不是通过 CRT 分配,VS 工具无法关联它与代码行。
✅ 结论:CreateSolidBrush 创建的资源不会被 VS2022 自带工具标记为明确泄漏源,也不会显示调用堆栈或源代码位置。
3. GDI 泄漏(new CPen
)
for (int i = 0; i < 10; ++i)
{CPen* pPen = new CPen(PS_SOLID, 2, RGB(0, 128, 255));// 忘记 delete pPen → CPen 析构不会调用 DeleteObject
}
说明:MFC 封装的 GDI 对象(如 CPen、CBrush)如果使用 new
分配,也必须手动 delete
才能释放其关联 GDI 资源。
① Deleaker 检测到泄漏
- 文件:
CRobotTaskDlg.cpp
, line 159 - 类型:
Heap memory
(堆内存) - 条目数量:26 个泄漏
说明这段代码:
CPen* pPen = new CPen(PS_SOLID, 2, RGB(0, 128, 255));
确实在循环里不断分配 CPen
对象,但从未 delete
,堆对象未析构,GDI 对象也未 DeleteObject
,构成双重泄漏。
② 输出窗口 dump_complete
+ atlTraceGeneral
MFC 的内存调试(如 _CrtDumpMemoryLeaks()
或使用 afxMemDF
):
-
输出:
detected memory leaks!
-
并列出了每个泄漏块的地址和大小,例如:
{7456} normal block at 0x000001236F602820, 16 bytes long
这与 Deleaker 的结果形成 双重交叉验证,表示泄漏 确实存在,且可以定位具体分配行数。
③ 总结这次验证意义
工具 | 检测内容 | 优点 |
---|---|---|
Deleaker | 堆泄漏 + GDI 泄漏,精确到源文件 | 支持快照对比、GDI 专项、定位调用栈 |
VS 输出窗口 (_CrtDumpMemoryLeaks ) | 堆泄漏 | 自带,轻量,适合 Debug 阶段 |
五、Deleaker 漏洞验证与修复成功案例
在开发自定义控件 CBlButton
时,使用了多个 GDI 对象(CreatePen
、CreateSolidBrush
、GetStockObject
等),初期由于部分 DeleteObject
遗漏或误释放,导致 Deleaker 检测到如下泄漏:
泄漏位置:BltButton.cpp, line 272
类型:GDI 对象(笔、画刷)
❌ 修复前问题:
CreatePen
创建的对象未释放或错误释放(重复释放旧对象)- 字体(HFONT)使用
SelectObject
设置后未还原旧字体 - 菜单小三角中误将已释放的
hBrush
/hPen
重复释放(应释放局部hbrDrop
/hPenDrop
)
例如以下代码存在典型问题:
// 错误释放菜单绘图中局部资源
SelectObject(hDC, holdBrush);
SelectObject(hDC, holdPen);
DeleteObject(hBrush); // ❌ 已在上一段释放,应为 hbrDrop
DeleteObject(hPen); // ❌ 应为 hPenDrop
✅ 修复操作:
- 所有 GDI 对象遵循
Create → Select → Restore → Delete
模式 - 字体部分使用
hOldFont = SelectObject(...)
并还原 - 修复了
DrawTriangle
逻辑,确保仅释放Create
出来的局部资源
修复后的关键片段:
// 菜单项小三角绘制
HPEN hPenDrop = CreatePen(...);
HBRUSH hbrDrop = CreateSolidBrush(...);
HBRUSH hOldBrush = (HBRUSH)SelectObject(hDC, hbrDrop);
HPEN hOldPen = (HPEN)SelectObject(hDC, hPenDrop);
// Polygon(...)
SelectObject(hDC, hOldBrush);
SelectObject(hDC, hOldPen);
DeleteObject(hbrDrop); // ✅ 正确释放
DeleteObject(hPenDrop); // ✅ 正确释放
字体部分:
HFONT hFont = (HFONT)GetStockObject(DEFAULT_GUI_FONT);
HFONT hOldFont = (HFONT)::SelectObject(hDC, hFont);
// ... 文字绘制
::SelectObject(hDC, hOldFont); // ✅ 恢复原字体
📸 快照对比结果:
最终快照显示:
No leaks found
并提示:
0 of 16 shown(无泄漏对象)
说明所有 DrawItem()
中涉及的资源都已成功释放,Deleaker 报告“干净”状态。
🔍 对比前后改动关键点:
对象类型 | 修复前 | 修复后 |
---|---|---|
HPEN | 未 delete 或重复释放 | delete 与原始对象匹配 |
HBRUSH | 部分遗漏或重复释放 | 局部变量专属释放 |
HFONT | 未还原 | 还原旧字体对象 |
六、Deleaker 与 Visual Studio 自带内存分析对比
功能维度 | Visual Studio 自带内存快照 | Deleaker |
---|---|---|
是否支持堆泄漏检测(new/malloc) | ✅ | ✅ |
是否支持 GDI 对象泄漏检测 | ❌ | ✅ |
是否支持 USER/句柄泄漏检测 | ❌ | ✅ |
是否可对比快照差异 | ✅(有限) | ✅(支持任意对比) |
是否显示调用堆栈 | ✅(摘要) | ✅(完整堆栈) |
是否跳转源代码 | ✅ | ✅ |
是否检测程序退出前泄漏 | ❌ | ✅ |
是否支持 GUI 独立使用 | ❌ | ✅ |
✅ 结论
- Visual Studio 快照工具:适合简单调试
new
内存泄漏,对非托管资源无能为力。 - Deleaker:适合全面检测桌面应用程序,特别适用于 UI/GDI 密集型项目。
七、推荐实践
- 每次版本发布前使用 Deleaker 检查内存与 GDI 泄漏。
- 对于
new
分配,建议使用智能指针(如std::unique_ptr
)管理。 - 所有
CreateXxx
(如CreateFont
,CreatePen
)资源,务必对应调用DeleteObject
。 - 拍摄多个快照(初始化 → 操作后 → 退出前)进行泄漏增长对比分析。
- 使用
Compare with...
找出精确的资源增长点。
八、结语
对于 C++/MFC/Win32 桌面开发者,Deleaker 是一款值得长期集成的调试利器,特别是在 UI、绘图、资源频繁分配的程序中,它能够清晰、精准、实用地帮助我们及时发现并修复潜在问题。
借助 Deleaker,我们不仅可以识别 new
导致的堆泄漏,更重要的是能发现诸如 CreateSolidBrush
、CreateFont
等 系统 GDI 资源未释放的问题,这是 Visual Studio 自带工具无法实现的。