Visual Studio调试器无法显示GDI对象数量,因GDI句柄(如HBRUSH、HPEN)不暴露给调试符号系统;需借助任务管理器勾选“GDI对象”列实时监控趋势,或用Deleaker快照比对定位多线程泄漏源头。

调试器里看不到GDI泄漏,得靠外部监控
Visual Studio 调试器本身不显示 GDI 对象数量,$handles 只统计系统句柄(如 HANDLE),不包含 HBRUSH、HPEN、HFONT 这类 GDI 句柄。你 breakpoint 停下来查变量、看调用栈,都看不到当前有多少画刷没释放——这不是调试器的缺陷,是 Windows GDI 的设计限制:GDI 对象不暴露给调试器符号系统。
用任务管理器实时看 GDI 对象数
这是最轻量、最直接的办法,尤其适合多线程场景下观察泄漏趋势:
- 打开任务管理器 → “详细信息”页 → 右键列标题 → “选择列” → 勾选
GDI objects - 找到你的进程,盯住这一列数值:正常界面操作后应短暂上升、很快回落;持续单向增长(比如从 200 → 800 → 1500)就是泄漏信号
- 多线程特别要注意:不是所有线程都能创建 GDI 对象。只有 UI 线程(通常为主线程)能安全调用
CreatePen、CreateFont等函数。如果工作线程误调用了这些 API(比如通过GetDC获取了 DC 后没及时ReleaseDC),GDI 对象会挂在 UI 线程上下文里,但泄漏源头在子线程代码中
Deleaker 快照比对定位多线程泄漏点
单纯看数字只能确认“有泄漏”,不能知道“谁漏的”。Deleaker 的快照功能才是关键:
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
- 启动程序后立即点 Deleaker 面板上的
Take Snapshot(记为 S1) - 触发可疑操作(比如打开一个含自绘控件的对话框,或启动一个后台绘图任务)
- 等几秒让线程跑完,再拍一次快照(S2)
- 右键 S2 →
Compare with S1→ 切换到GDI objects标签页 - 重点看
Stack trace列:它会显示每个新增 GDI 对象是在哪条线程、哪个函数、哪行代码创建的。即使创建发生在子线程,只要该线程调用了 GDI API,Deleaker 就能捕获调用栈 - 常见陷阱:
CBrush或CPen在OnCtlColor中临时创建却未缓存复用,或在线程函数里调用GetDC后忘记ReleaseDC—— 这些都会在比对结果里高亮出来
为什么多线程 GDI 泄漏更难发现
根本原因在于 GDI 对象生命周期和线程绑定的模糊性:
立即学习“C++免费学习笔记(深入)”;
-
CreatePen等函数必须在 UI 线程调用,但一旦创建成功,对象句柄可跨线程传递(虽然不推荐);而GetDC/ReleaseDC必须成对出现在同一线程中,否则ReleaseDC失效 - 线程退出时,Windows 不自动清理该线程创建的 GDI 对象——它们会一直留在进程 GDI 表里,直到进程结束
- Deleaker 默认只监控主线程的 GDI 分配?错。它注入的是进程级钩子,所有线程的 GDI 调用都会被捕获,但你需要确保子线程确实执行了 GDI 操作(比如绘图、字体测量),否则快照里不会出现相关记录
真正容易被忽略的,不是“哪里没删”,而是“哪个线程不该碰 GDI”。多线程环境下,把绘图逻辑挪到 UI 线程(比如用 PostMessage 触发重绘),比在线程里硬扛 GetDC 安全得多。

















