一, 前绪
C/C++ 程序给某些程序员的几大印象之一就是内存自己管理容易泄漏容易崩, 笔者曾经在一个产品中使用 C 语言开发维护部分模块, 只要产品有内存泄漏和崩溃的问题, 就被甩锅 "我的程序是 C# 开发的内存都是托管的, C++ 那边也没有内存(庇护其好友), 肯定是 C 这边的问题"(话说一个十几年的程序员还停留在语言层面不觉得有点 low 吗), 笔者毕业不到一年, 听到此语心里一万头草泥马奔腾而过, 默默地修改了程序, 注意不是修改 bug(哈哈), 而是把所有 malloc 和 free 都替换成了自定义宏 MALLOC 和 FREE,debug 版本所有内存分配释放都打了日志, 程序结束自动报告类似 "Core Memory Leaks: 字节数", 此后内存泄漏的问题再也没人敢甩过来了. 语言仅仅是个工具, 人心是大道.
二, C 程序内存泄漏检测方案参考
C 语言应用程序一般使用 malloc 和 free 两个函数分配和释放内存, 对它们做内存泄漏检测还是很好想到完美方案的. 所谓的完美: 1)当内存泄漏时能迅速定位到是哪一行代码分配的; 2)使用简单与原先无异; 3)release 时或者不需要调试内存的时候, 仍然使用原生态函数, 不影响效率.
- #ifdef DEBUG_MEMORY
- #define MALLOC MallocDebug(__FILE__, __LINE__, size)
- #define FREE FreeDebug(__FILE__, __LINE__, p)
- #else
- #define MALLOC malloc
- #define FREE free
- #endif
- #ifdef DEBUG_MEMORY
- #define MEM_OP_MALLOC 1
- #define MEM_OP_FREE 0
- void LogMemory(const char* file, int line, void* p, int operation, size_t size);
- void* MallocDebug(const char* file, int line, size_t size)
- {
- void* p = malloc(size);
- LogMemory(file, line, p, MEM_OP_MALLOC, size);
- return p;
- }
- void FreeDebug(const char* file, int line, void* p)
- {
- LogMemory(file, line, p, MEM_OP_FREE, 0);
- free(p);
- }
- void LogMemory(const char* file, int line, void* p, int operation, size_t size)
- {
- // 打印日志(malloc/free, 指针, 文件名, 行号, 指针, 第几次分配的序号), 分配序号可以实现类似与 crtdbg 的 CrtSetBreakAlloc 函数的功能
- // 操作为 malloc 时, 向 map 插入一条记录, 增加内存使用大小;
- // 操作为 free 时, 在 map 中找到记录并删除, 减少内存使用大小.
- }
- void DetectMemoryLeaks()
- {
- // 打印当前内存管理的 map 中剩余的没有释放的内存指针, 文件名, 行号, 大小, 分配序号
- }
- #endif
- void Program()
- {
- int *pArray = MALLOC(sizeof(int) * 10);
- FREE(pArray);
- #ifdef DEBUG_MEMORY
- DetectMemoryLeaks();
- #endif
- }
C 语言应用程序中的上述内存泄漏检测方案至此完美收官, 记录分配序号, 也可以向 CrtSetBreakAlloc 那样调试内存泄漏哦.
三, C++ 程序内存泄漏检测方案参考
近期在跟踪 C++ 项目的内存泄漏, 项目包含多个工程(1 个 exe + 多个自开发 dll + 多个第三方 dll).
1. 首先考虑的第一个方案是利用 crtdbg. 踩得第一个坑是记得看下工程配置运行时库选项用 debug 版本(/MTd 或 / MDd), 否则无效. 非 MFC 程序报不出可疑泄漏内存的文件名及行号, 要在整个程序所有使用 new 的文件中包含 "#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)" 的宏定义. 对于单个工程程序而言调试比较简单方便; 对于多个 dll 尤其是有第三方库时,/MTd 配置下要非常小心,/MDd 配置要好很多, 但实际中使用 crtdbg 调试还是偶尔会崩在系统底层内存分配的地方, 出现的问题不在个人解决能力之内, 放弃了.
2. 其次的第二个方案, 考虑自己重载 operator new 和 operator delete, 当然是要重载 operator new(size_t size, const char* file, int line)这个版本才能在泄漏时定位到行号. 同样也是要所有使用 new 的文件中包含 "#define new new( __FILE__, __LINE__)" 的宏定义. 问题是虽然可以重载 operator delete(void* p, const char* file, int line)这个版本, 但是这个版本只会在 placement new 失败时才会调用, 正常时候还是调用的 operator delete(void* p)版本, 所以还需要重载 operator delete(void* p)版本, 问题是没有重载的系统内置的 operator new(size_t size)版本分配的所有内存也会走用户重载后的 operator delete(void* p)版本, 不配对, 一起把 operator new(size_t size)也重载了.
第二个方案的另外一个问题是程序要包含宏 "#define new new( __FILE__, __LINE__)", 但第三方库头文件中有 placement new 的用法 new(pointer)classA(), 项目大一点头文件顺序不好调, 编译失败. 还有就是这个方案实践中 (多 dll 全部设置的相同的运行时库配置) 也在系统底层分配内存的方法崩溃过, 也可能是个人在哪里的处理有问题, 总之不再考虑前两个方案了, 打算在应用层做处理.
3. 最后确定在最上层想方案, 首先 C++ 不能自定义操作符, 否则就能定义一个操作符 A* pA = debugnew A(1, 2)了. 宏不能有空格只能考虑函数 debugnew(A, 1, 2)了. 下面上方案.
所有要分配或释放内存的文件中包含 DebugMemory.h 头文件(伪代码):
- // 文件名: DebugMemory.h
- #ifdef DEBUG_MEMORY
- #define NEW(T, ...) DebugNew<T>(__FILE__, __LINE__, __VA_ARGS__)
- #define DEL(p) DebugDelete(__FILE__, __LINE__, p)
- #define NEW_ARRAY(T, size) DebugNewArray<T>(__FILE__, __LINE__, size)
- #define DEL_ARRAY(p) DebugDeleteArray(__FILE__, __LINE__, p)
- #else
- #define NEW(T, ...) new T(__VA_ARGS__)
- #define DEL(p) delete(p)
- #define NEW_ARRAY(T, size) new T[size]
- #define DEL_ARRAY(p) delete[] p
- #endif
- #ifdef DEBUG_MEMORY
- template<class T, class... Args>
- T* DebugNew(const char* file, int line, Args&&... args)
- {
- T* p = new T(std::forward<Args>(args)...);
- //todo: 记录操作(new), 指针, 文件, 行号, 分配号
- return p;
- }
- template<class T>
- void DebugDelete(const char* file, int line, T* p)
- {
- //todo: 记录操作(delete), 指针, 文件, 行号
- delete p;
- }
- template<class T>
- T* DebugNewArray(const char* file, int line, size_t size)
- {
- T* p = new T[size];
- //todo: 记录操作(new[]), 指针, 文件, 行号, 分配号
- return p;
- }
- template<class T>
- void DebugDeleteArray(const char* file, int line, T* p)
- {
- //todo: 记录操作(delete), 指针, 文件, 行号
- delete[] p;
- }
- void DetectMemoryLeaks()
- {
- //todo: 统计并打印未释放的内存信息
- }
- #endif
使用 DebugMemory.h 头文件:
- // 文件名: main.cpp
- #include "DebugMemory.h"
- class A
- {
- public:
- A(){}
- A(int a, int b):m_a(a), m_b(b){}
- private:
- int m_a;
- int m_b;
- }
- int main()
- {
- A* pA = NEW(A, 1, 2); //new A(1, 2)
- DEL(pA); //delete pA;
- A* pArray = NEW_ARRAY(A, 10); //new A[10]
- DEL_ARRAY(pArray); //delete[] pArray
- #ifdef DEBUG_MEMORY
- DetectMemoryLeaks(); // 内存泄漏检测
- #endif
- return 0;
- }
四, 方案评价
1.C 语言应用程序的内存泄漏解决方案: 完美.
2.C++ 语言应用程序的内存泄漏解决方案
优点: 没有改变默认的 operator new 和 operator delete 行为, 毕竟危险.
优点: 实用性通用性强, 完全在应用程序员的控制范围内. 因为在应用层, 不管什么版本都可以检测内存泄漏, 不用考虑跨 dll 调用产生的问题.
不足: 写法习惯改变, 原来是 new A(1,2), 要写成 NEW(A, 1, 2), 如果 C++ 能实现自定义操作符, 那么方案就完美了.
来源: https://www.cnblogs.com/arider/p/11143672.html