松柏之志,经霜犹茂

松柏之志,经霜犹茂

  一、前绪

  C/C++程序给某些程序员的几大印象之一就是内存自己管理容易泄漏容易崩,笔者曾经在一个产品中使用C语言开发维护部分模块,只要产品有内存泄漏和崩溃的问题,就被甩锅“我的程序是C#开发的内存都是托管的,C++那边也没有内存(庇护其好友),肯定是C这边的问题”(话说一个十几年的程序员还停留在语言层面不觉得有点low吗),笔者毕业不到一年,听到此语心里一万头草泥马奔腾而过,默默地修改了程序,注意不是修改bug(哈哈),而是把所有malloc和free都替换成了自定义宏MALLOC和FREE,debug版本所有内存分配释放都打了日志,程序结束自动报告类似“Core Memory Leaks: 字节数”,此后内存泄漏的问题再也没人敢甩过来了。语言仅仅是个工具,人心是大道。

  二、C程序内存泄漏检测方案参考

  C语言应用程序一般使用malloc和free两个函数分配和释放内存,对它们做内存泄漏检测还是很好想到完美方案的。所谓的完美:1)当内存泄漏时能迅速定位到是哪一行代码分配的;2)使用简单与原先无异;3)release时或者不需要调试内存的时候,仍然使用原生态函数,不影响效率。

 1 #ifdef DEBUG_MEMORY
 2 #define MALLOC MallocDebug(__FILE__, __LINE__, size)
 3 #define FREE FreeDebug(__FILE__, __LINE__, p)
 4 #else
 5 #define MALLOC malloc
 6 #define FREE free
 7 #endif
 8
 9 #ifdef DEBUG_MEMORY
10 #define MEM_OP_MALLOC 1
11 #define MEM_OP_FREE 0
12
13 void LogMemory(const char* file, int line, void* p, int operation, size_t size);
14
15 void* MallocDebug(const char* file, int line, size_t size)
16 {
17     void* p = malloc(size);
18     LogMemory(file, line, p, MEM_OP_MALLOC, size);
19     return p;
20 }
21
22 void FreeDebug(const char* file, int line, void* p)
23 {
24     LogMemory(file, line, p, MEM_OP_FREE, 0);
25      free(p);
26 }
27
28 void LogMemory(const char* file, int line, void* p, int operation, size_t size)
29 {
30     //打印日志(malloc/free、指针、文件名、行号、指针、第几次分配的序号),分配序号可以实现类似与crtdbg的CrtSetBreakAlloc函数的功能
31     //操作为malloc时,向map插入一条记录,增加内存使用大小;
32     //操作为free时,在map中找到记录并删除,减少内存使用大小。
33 }
34
35 void DetectMemoryLeaks()
36 {
37     //打印当前内存管理的map中剩余的没有释放的内存指针、文件名、行号、大小、分配序号
38 }
39
40 #endif
41
42 void Program()
43 {
44     int *pArray = MALLOC(sizeof(int) * 10);
45     FREE(pArray);
46
47 #ifdef DEBUG_MEMORY
48     DetectMemoryLeaks();
49 #endif
50 }

    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头文件(伪代码):

 1 //文件名:DebugMemory.h
 2
 3 #ifdef DEBUG_MEMORY
 4 #define NEW(T, ...) DebugNew<T>(__FILE__, __LINE__, __VA_ARGS__)
 5 #define DEL(p) DebugDelete(__FILE__, __LINE__, p)
 6 #define NEW_ARRAY(T, size) DebugNewArray<T>(__FILE__, __LINE__, size)
 7 #define DEL_ARRAY(p) DebugDeleteArray(__FILE__, __LINE__, p)
 8 #else
 9 #define NEW(T, ...) new T(__VA_ARGS__)
10 #define DEL(p) delete(p)
11 #define NEW_ARRAY(T, size) new T[size]
12 #define DEL_ARRAY(p) delete[] p
13 #endif
14
15 #ifdef DEBUG_MEMORY
16
17 template<class T, class... Args>
18 T* DebugNew(const char* file, int line, Args&&... args)
19 {
20     T* p = new T(std::forward<Args>(args)...);
21     //todo:记录操作(new)、指针、文件、行号、分配号
22     return p;
23 }
24
25 template<class T>
26 void DebugDelete(const char* file, int line, T* p)
27 {
28     //todo:记录操作(delete)、指针、文件、行号
29     delete p;
30 }
31
32 template<class T>
33 T* DebugNewArray(const char* file, int line, size_t size)
34 {
35     T* p = new T[size];
36     //todo:记录操作(new[])、指针、文件、行号、分配号
37     return p;
38 }
39
40 template<class T>
41 void DebugDeleteArray(const char* file, int line, T* p)
42 {
43     //todo:记录操作(delete)、指针、文件、行号        
44     delete[] p;
45 }
46
47 void DetectMemoryLeaks()
48 {
49     //todo:统计并打印未释放的内存信息
50 }
51
52 #endif

    使用DebugMemory.h头文件:

 1 //文件名:main.cpp
 2
 3 #include "DebugMemory.h"
 4
 5 class A
 6 {
 7 public:
 8     A(){}
 9     A(int a, int b):m_a(a), m_b(b){}
10 private:
11     int m_a;
12     int m_b;
13 }
14
15 int main()
16 {
17     A* pA = NEW(A, 1, 2);         //new A(1, 2)
18     DEL(pA);                      //delete pA;
19
20     A* pArray = NEW_ARRAY(A, 10); //new A[10]
21     DEL_ARRAY(pArray);            //delete[] pArray
22
23 #ifdef DEBUG_MEMORY
24     DetectMemoryLeaks();          //内存泄漏检测
25 #endif
26
27     return 0;
28 }

    四、方案评价

1.C语言应用程序的内存泄漏解决方案:完美。

2.C++语言应用程序的内存泄漏解决方案

           优点:没有改变默认的operator new和operator delete行为,毕竟危险。

           优点:实用性通用性强,完全在应用程序员的控制范围内。因为在应用层,不管什么版本都可以检测内存泄漏,不用考虑跨dll调用产生的问题。

           不足:写法习惯改变,原来是new A(1,2),要写成NEW(A, 1, 2),如果C++能实现自定义操作符,那么方案就完美了。

07-06 22:52