You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Visual Studio单元测试中检测Windows DLL内存泄漏

针对Visual Studio CppUnitTest框架的DLL内存泄漏验证方案

作为一名经常在跨平台环境下做测试的开发者,我完全理解你从Linux的CppUnit切换到Windows的CppUnitTest时遇到的内存检测痛点——Windows的内存管理机制和Linux差异很大,进程级的内存指标确实没法精准反映堆内的实际泄漏情况。下面给你几个适配当前场景的可行方案,按实现复杂度和侵入性排序:

1. 用CRT内存快照工具(推荐,低侵入)

Visual Studio自带的CRT内存检测工具完美适配CppUnitTest框架,只要你的DLL使用标准CRT的malloc/free或new/delete(默认情况下都是),就能直接用它来对比测试前后的内存状态,自动验证泄漏。

核心思路是用_CrtMemCheckpoint记录测试前的内存快照,执行DLL代码后再拍一次快照,用_CrtMemDifference对比两者的差异:

#include <crtdbg.h>
#include "CppUnitTest.h"

using namespace Microsoft::VisualStudio::CppUnitTestFramework;

TEST_CLASS(DLLMemoryLeakTests) {
private:
    _CrtMemState m_initialState;

public:
    TEST_METHOD_INITIALIZE(SetUp) {
        // 初始化时记录内存基准状态
        _CrtMemCheckpoint(&m_initialState);
    }

    TEST_METHOD(TestNoMemoryLeakAfterFree) {
        // 调用你的DLL分配逻辑
        YourDLLAllocFunction();
        // 调用你的DLL释放逻辑
        YourDLLFreeFunction();

        _CrtMemState finalState, diffState;
        _CrtMemCheckpoint(&finalState);

        // 对比前后内存状态
        if (_CrtMemDifference(&diffState, &m_initialState, &finalState)) {
            // 如果有差异,输出泄漏详情到VS调试窗口
            _CrtMemDumpStatistics(&diffState);
            _CrtMemDumpAllObjectsSince(&m_initialState);
            Assert::Fail(L"Memory leak detected: allocated memory not fully freed!");
        }
    }
};

这个方案的优势:

  • 完全不需要修改DLL代码,只需要在测试项目里引入crtdbg.h
  • 和CppUnitTest的生命周期(TEST_METHOD_INITIALIZE/TEST_METHOD)完美契合
  • 能精准定位泄漏的内存块和调用栈(调试时非常有用)

唯一的限制是:如果你的DLL用了完全自定义的内存分配器(不依赖CRT),这个方法就失效了。

2. 遍历进程堆统计已分配内存(中度侵入,无依赖)

如果CRT的方法不适用,你可以直接遍历进程的所有堆,统计当前处于"占用"状态的内存块总大小。这种方法不依赖CRT,能覆盖所有基于Windows堆的分配(包括CRT的malloc,因为CRT默认用进程堆)。

示例代码:

#include <windows.h>
#include "CppUnitTest.h"

using namespace Microsoft::VisualStudio::CppUnitTestFramework;

size_t GetTotalAllocatedHeapMemory() {
    HANDLE heaps[1024];
    DWORD heapCount = GetProcessHeaps(_countof(heaps), heaps);
    if (heapCount == 0) {
        Assert::Fail(L"Failed to retrieve process heaps");
    }

    size_t totalAllocated = 0;
    for (DWORD i = 0; i < heapCount; ++i) {
        PROCESS_HEAP_ENTRY entry{};
        while (HeapWalk(heaps[i], &entry)) {
            // 只统计正在使用的内存块
            if (entry.wFlags & PROCESS_HEAP_ENTRY_BUSY) {
                totalAllocated += entry.cbData;
            }
        }
        // 忽略"没有更多条目"的正常结束错误
        if (GetLastError() != ERROR_NO_MORE_ITEMS) {
            Assert::Fail(L"Failed to walk process heap");
        }
    }
    return totalAllocated;
}

TEST_CLASS(DLLHeapMemoryTests) {
private:
    size_t m_initialHeapSize;

public:
    TEST_METHOD_INITIALIZE(SetUp) {
        // 记录测试前的堆内存基准
        m_initialHeapSize = GetTotalAllocatedHeapMemory();
    }

    TEST_METHOD(TestHeapSizeUnchangedAfterFree) {
        YourDLLAllocFunction();
        YourDLLFreeFunction();

        size_t finalHeapSize = GetTotalAllocatedHeapMemory();
        // 允许微小误差(比如CppUnitTest自身的小分配),或者调整基准时机
        Assert::AreEqual(m_initialHeapSize, finalHeapSize, L"Heap memory changed after free - possible leak!");
    }
};

注意事项:

  • 要在测试初始化完成后再记录基准值,避免把CppUnitTest自身的初始化分配算进去
  • 堆遍历的速度可能较慢,不适合频繁调用的性能敏感测试

3. Hook标准分配函数(类似Linux的LD_PRELOAD,中度复杂度)

如果你的DLL用的是标准malloc/free/new/delete,但CRT快照和堆遍历都满足不了需求,你可以用Windows的Hook技术(比如微软的Detours库,或者开源的MinHook)来拦截这些分配函数,精准统计每个分配和释放的内存量,类似Linux下的LD_PRELOAD替换malloc。

以Detours为例,核心代码如下:

#include <detours.h>
#include <malloc.h>
#include "CppUnitTest.h"

using namespace Microsoft::VisualStudio::CppUnitTestFramework;

// 统计变量
static size_t s_totalAllocated = 0;
static size_t s_totalFreed = 0;

// 保存原始函数指针
void* (WINAPI* OriginalMalloc)(size_t) = malloc;
void (WINAPI* OriginalFree)(void*) = free;

// Hook后的malloc
void* WINAPI HookedMalloc(size_t size) {
    void* ptr = OriginalMalloc(size);
    if (ptr) s_totalAllocated += size;
    return ptr;
}

// Hook后的free
void WINAPI HookedFree(void* ptr) {
    if (ptr) {
        // 获取malloc分配的内存大小
        size_t size = _msize(ptr);
        s_totalFreed += size;
    }
    OriginalFree(ptr);
}

TEST_CLASS(DLLHookedMemoryTests) {
public:
    TEST_METHOD_INITIALIZE(SetUp) {
        // 重置统计
        s_totalAllocated = 0;
        s_totalFreed = 0;

        // 安装Hook
        DetourTransactionBegin();
        DetourUpdateThread(GetCurrentThread());
        DetourAttach(&(PVOID&)OriginalMalloc, HookedMalloc);
        DetourAttach(&(PVOID&)OriginalFree, HookedFree);
        DetourTransactionCommit();
    }

    TEST_METHOD_CLEANUP(TearDown) {
        // 卸载Hook
        DetourTransactionBegin();
        DetourUpdateThread(GetCurrentThread());
        DetourDetach(&(PVOID&)OriginalMalloc, HookedMalloc);
        DetourDetach(&(PVOID&)OriginalFree, HookedFree);
        DetourTransactionCommit();
    }

    TEST_METHOD(TestAllocatedMatchesFreed) {
        YourDLLAllocFunction();
        YourDLLFreeFunction();

        Assert::AreEqual(s_totalAllocated, s_totalFreed, L"Allocated memory does not match freed memory!");
    }
};

这个方案的优势是完全不修改DLL代码,能精准统计每一次分配和释放;缺点是需要引入第三方Hook库,且只能针对标准分配函数生效。

4. 修改DLL添加分配器接口(最终方案,高侵入)

如果你的DLL用了完全自定义的内存池(不依赖Windows堆或CRT),上面的方法都无效,那只能给DLL添加可替换的分配器接口。比如:

  • 给DLL的初始化函数传入自定义的malloc/free回调
  • 用模板或抽象类封装分配逻辑,测试时注入统计用的分配器

这个方案是最可靠的,但需要修改DLL的核心代码,适合长期维护的项目。


内容的提问来源于stack exchange,提问作者Bruce Adams

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 09:17:40