前言
在平时的项目开发过程中,经常遇到内存泄露问题。虽然我们可以借助valgrind工具进行内存使用问题检测,但是这个工具毕竟不是太好用。
那么,有没有什么简单方便的办法可以自动记录哪个内存泄露,以及这个内存申请时所处的代码行呢?
实现思路
以C++为例,C++的内存申请方式有两种,malloc和new。
- new/delete 是C++的操作符,需要编译器支持。它调用的分别为operator new()和operator delete()。
- malloc/free 是C的标准库函数,需要头文件支持。有2个头文件都可以使用,C头文件, .调用时 malloc(x);C++头文件, 注意没有后缀名.调用时要写 std::malloc(x) 注意std前缀。
备注:详细可以参考前面的文章《new与malloc的特点及运行原理的区别》。
我们下面通过替换malloc/free函数,并重写类的new/delete操作符,并生成内存日志文件的形式进行检测。
设计思路如下:
①申请空间时,新建以当前新地址命名的内存日志文件,malloc操作时还可以在文件中记录当前代码所在行数。
②释放空间时,查找并删除以此地址命名的内存日志文件。扩展:如果找不到文件说明重复释放。
③程序运行完毕后,查看是否有未删除的内存日志文件。如果存在,则说明有内存泄露。
注意:operator new 与 operator delete 和 operator new[] 和 operator delete[] 应该成对重载,这四个函数为默认static函数,static可写可不写,编译器默认会给声明成static inline类型。
范例代码
// memory_check.cpp : 内存检查
// 替换malloc和new是为了检查内存泄漏和溢出。虽然有valgrind但是并不好用。
//
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <iostream>
using namespace std;
#define DEBUG 1 //测试宏,临时定义并打开
//宏定义需要放头文件中
//支持c++11的编译器使用constexpr代替宏
#ifdef DEBUG
#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1900)
//C++11 is supported
template<typename T>
constexpr auto malloc_debug(T size) { return _malloc(size, __FILE__, __LINE__); }
template<typename T>
constexpr auto free_debug(T ptr) { return _free(ptr, __FILE__, __LINE__); }
#else
//C++11 is not supported
#define malloc_debug(size) _malloc(size,__FILE__,__LINE__);
#define free_debug(ptr) _free(ptr,__FILE__,__LINE__);
#endif
#else
#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1900)
//C++11 is supported
template<typename T>
constexpr auto malloc_debug(T size) { return malloc(size); }
template<typename T>
constexpr auto free_debug(T ptr) { return free(ptr); }
#else
//C++11 is not supported
#define malloc_debug(size) malloc(size)
#define free_debug(ptr) free(ptr)
#endif
#endif // DEBUG
void* _malloc(size_t size, const char* filename, int line)
{
cout << "my malloc." << endl;
void* p = malloc(size);
char fname[128] = { 0 };
sprintf_s(fname, "%p.mem_malloc", p);
FILE* fp;
int ret = fopen_s(&fp, fname, "a");//最好使用fopen_s,使用fopen提示“not safe”
if (ret == 0)//文件打开成功
{
fprintf(fp, "[+] %s: %d line, addr:%p size:%zd \n", filename, line, p, size);
fflush(fp);
fclose(fp);
}
return p;
}
void _free(void* ptr, const char* filename, int line)
{
cout << "my free." << endl;
char fname[128] = { 0 };
sprintf_s(fname, "%p.mem_malloc", ptr);
if (_unlink(fname) < 0)
{
printf(" double free ! %s: %d line, addr:%p \n", filename, line, ptr);
}
free(ptr);
}
class A
{
public:
A() {};
~A() {};
int x = 0;
#ifdef DEBUG
public:
//static 可写可不写,编译器将 operator new 重载函数默认为 static inline
//在重载函数中做自己的内存管理
static void* operator new(size_t size)
{
cout << "my operator new." << endl;
void* ptr = ::operator new(size);
char fname[128] = { 0 };
sprintf_s(fname, "%p.mem_new", ptr);
FILE* fp;
int ret = fopen_s(&fp, fname, "w");//最好使用fopen_s,使用fopen提示“not safe”
if (ret == 0)//文件打开成功
{
fprintf(fp, "[+] addr:%p size:%zd \n", ptr, size);
fflush(fp);
fclose(fp);
}
return ptr;
}
//size_t 参数可选
static void operator delete(void* ptr)
{
cout << "my operator delete." << endl;
char fname[128] = { 0 };
sprintf_s(fname, "%p.mem_new", ptr);
if (_unlink(fname) < 0)
{
cout << " double free ! addr:" << ptr << endl;
}
return ::operator delete(ptr);
}
void* operator new[](size_t size)
{
cout << "my operator new[]." << endl;
void* ptr = ::operator new[](size);
char fname[128] = { 0 };
sprintf_s(fname, "%p.mem_new_array", ptr);
FILE* fp;
int ret = fopen_s(&fp, fname, "w");//最好使用fopen_s,使用fopen提示“not safe”
if (ret == 0)//文件打开成功
{
fprintf(fp, "[+] addr:%p new[%zd] \n", ptr, size);
fflush(fp);
fclose(fp);
}
return ptr;
}
void operator delete[](void* ptr)
{
cout << "my operator delete[]." << endl;
char fname[128] = { 0 };
sprintf_s(fname, "%p.mem_new_array", ptr);
if (_unlink(fname) < 0)
{
cout << " double free ! addr:" << ptr << endl;
}
return ::operator delete[](ptr);
}
#endif
};
int main_memory_test(void)
{
void* p1 = malloc_debug(3);
free_debug(p1);
A* p2 = new A();
delete p2;
A* p_arry = new A[5];
delete[] p_arry;
return 0;
}
int main()
{
std::cout << "Hello World!"<<endl << endl;
main_memory_test();
}
执行效果
(测试工具vs2022)
malloc_debug执行后的效果:
new执行后的效果:
new[]执行后的效果:
由此可见:
当前IDE在new[]时,多分配了8个字节的cookie用于存储需要执行几次构造函数。
返回给用户的指针的初始地址是cookie后的地址。
delete[]时,自动向前获取8个字节的cookie信息,用于确定需要执行几次析构函数。
内存正确被释放后,内存日志文件以前全部被自动删除:
原创不易,欢迎点赞、关注、转发、收藏!