【内存】C/C++ 自己实现简单的内存泄漏检测工具

目录

C++语言实现

C语言实现


学习练习用,真正检测内存泄漏最好还是 借助专门的工具。

C++语言实现

思路:

1.重载operator new/new[ ] 与 operator delete/delete[ ], 并借助双向链表结构(带头节点)管理内存,new的时候将 内存信息存入链表,delete的时候将内存信息踢出链表,程序结束后,查看链表剩余多少节点判断内存泄漏。

https://blog.csdn.net/ssopp24/article/details/77278439

new操作符是由C++语言内建的, 就像sizeof那样, 不能改变意义, 总是做相同的事情:


调用operator new (sizeof(A))
调用A:A()
返回指针

第一: 它分配足够的内存, 用来放置某类型的对象.

第二: 它调用一个构造函数, 为刚才分配的内存中的那个对象设定初始值。

第三: 对象被分配了空间并构造完成, 返回一个指向该对象的指针

new operator(即 new 操作符)总是做这两件事,无论如何你是不能改变其行为。

能够改变的是用来容纳对象的那块内存的分配行为, new operator(new)调用某个函数, 执行必要的内存分配动作, 你可以重写或者重载那个函数, 改变其行为. 这个函数名称就叫operator new

函数 operator new 通常声明如下:

void * operator new (size_t size);

其返回类型void*. 即返回一个指针, 指向一块原始的, 未设置初始值的内存。

函数中的size_t参数表示需要分配多少内存, 你可以将operator new 重载, 加上额外的参数, 但第一个参数类型必须总是size_t.


可以这样理解: new int -> new(sizeof(int)) -> operator new(sizeof(int)/*即size_t size*/)->重载. 

不能改变关键字new的行为 但我们能重载operator new( size_t size )

注意: operator new( size_t size )中的参数size 是new 计算的. 不用我们自己计算. 我们重载时, 只需要开辟 size个字节的内存大小即可
 

LeakDetector.h:

// 注意, 我们的头文件是要被包含进被测试的.cpp 的, 所以头文件中不要出现"多余的"代码及库文件, 以免影响被测文件
#ifndef LEAK_DETECTOR_H_
#define LEAK_DETECTOR_H_
// 有个小技巧: C/C++库中标准的头文件宏定义是这种形式: _STDIO_H( 标准规定保留下划线作前缀 )
// 所以平时我们为了避免自己定义的宏意外地与标准头文件定义的宏发生冲突, 我们使用下划线作后缀, 并且不用下划线作前缀



// 重载版本: operator new/new[]( ), operator delete/delete[]( ) 的声明
void* operator new(size_t size, const char* file, size_t line);
void* operator new[](size_t size, const char* file, size_t line);
// 注意到, 上面我们重载的函数中, 第一个参数和第三个参数的类型是size_t
// 其中第一个参数size为 sizeof的返回值, 所以为size_t类型
// 第三个参数的含义为 行号, 是我们重载 operator new/new[]( )后自己加的参数, 此处也可以用
//unsigned int. 但最好用 size_t. 原因是size_t的可移植性好. 理由见上面链接
void operator delete(void* ptr);
void operator delete[](void* ptr);


// 这个宏在LeakDetector.cpp中定义, 使得编译时 源码中的new被替换为 
//new( __FILE__, __LINE__ ),源码就变成了使用我们自己的重载版本
//operator new/new[]( size_t size, const char* file, size_t line )

#ifndef NEW_OVERLOAD_IMPLEMENTATION_
#define new new( __FILE__, __LINE__ )
// 预定义宏: 
// __FILE__(两个下划线): 代表当前源代码文件名
// __LINE__(两个下划线): 代表当前源代码文件中的行号
#endif


class LeakDetector {
public:
	// LeakDetector.cpp和被测试的.cpp都会包 LeakDetector.h头文件
	// 因此两个源文件中会创建两个静态LeakDetector类对象 exitCounter (两个静态类对象名相同,
//但是它们的链接属性均为内链接(只在当前源文件有效), 因此不会重定义), 如果此时两个对析
//构, 会调用两次析构函数, 调用两次内存泄漏检测函数. 而我们的预期是只调用一次内存泄漏
//检测函数. 所以我们声明一个所有类对象共享的静态变量来实现我们的目的

	static size_t _callCount;

	LeakDetector() { ++_callCount; }
	~LeakDetector() { if (0 == --_callCount) _LeakDetector(); }

private:
	void _LeakDetector();
};
//为什么要设计 callCount? callCount 保证了我们的 LeakDetector 只调用了一次。
// 静态对象
static LeakDetector exitCounter;



#endif

LeakDetector.cpp:

// 这个宏保证 LeakDetector.cpp 中的new 不会被LeakDetector.h中的 宏替换 替换掉
#define NEW_OVERLOAD_IMPLEMENTATION_


#include <iostream>								//cout 
#include <cstring>								//strlen 和 strcpy
#include "LeakDetector.h"
#pragma warning(disable:4996)

// 初始化 LeakDetector类中定义的静态变量
size_t LeakDetector::_callCount = 0;


// 我们使用带头节点的双向链表来手动管理内存申请与释放, 头节点的_prev指向最后一个结点, _next指向第一个结点
// 双向链表结构
typedef struct MemoryList {
	struct MemoryList* _prev;
	struct MemoryList* _next;
	size_t _size;								// operator new( )申请的内存大小
	bool   _isArray;							// 是否为申请数组(即使用operator new[]( ) 而不是 operator new( ))
	char*  _file;								// 如果有, 存储存在内存泄漏文件的文件信息
	size_t _line;								// 存储存在内存泄漏位置的行号
} MemoryList;

// 创建一个头结点, 它的前后指针均初始化为指向自己(插入、删除双向链表中结点 和 _LeakDetector( )函数中遍历双向链表时, 这样初始化的作用就体现出来了)。使用静态变量使其只在本文件内有效
// 我们只使用这个头节点的 _prev 和 _next 成员
static MemoryList memoryListHead = { &memoryListHead, &memoryListHead, 0, false, NULL, 0 };


// 保存未释放的内存大小
static size_t memoryAllocated = 0;


// 对双向链表采用头插法分配内存
void* AllocateMemory(size_t size, bool array, const char* file, size_t line) {
	// 我们需要为我们管理内存分配的 MemoryList结点 也申请内存
	// 计算新的大小
	size_t newSize = size + sizeof(MemoryList);

	// 把接收到的地址强转为 MemoryList*, 以便我们后续操作
	// 由于重载了new, 所以我们使用 malloc 来申请内存
	MemoryList* newElem = (MemoryList*)malloc(newSize);

	// 更新MemoryList结构成员的值
	newElem->_prev = &memoryListHead;
	newElem->_next = memoryListHead._next;
	newElem->_size = size;						// 注意, 此处为size而不是newSize. 因为我们管理记录的是 new申请的内存, 验证它是否未释放, 存在内存泄漏问题. 申请 newSize的内存(为 MemoryList结点多申请出的内存), 只是为了实现手动管理内存所必须, 这个内存我们一定会释放, 不需关注. 所以保存 时用size而不是newSize
	newElem->_isArray = array;

	// 如果有文件信息, 则保存下来
	if (NULL != file) {
		newElem->_file = (char*)malloc(strlen(file) + 1);
		strcpy(newElem->_file, file);
	}
	else
		newElem->_file = NULL;

	// 保存行号
	newElem->_line = line;

	// 更新双向链表结构
	memoryListHead._next->_prev = newElem;
	memoryListHead._next = newElem;

	// 更新未释放的内存数
	// 我们管理的只是 new申请的内存. 为memoryListHead结点多申请的内存,和为保存文件信息多申请内存无关, 这些内存我们一定会释放, 所以这里只记录size
	memoryAllocated += size;

	// 返回new 申请的内存地址
	// 将newElem强转为char* 类型(保证指针+1时每次加的字节数为1) + memoryListHead所占用字节数( 总共申请的newSize字节数 减去memoryListHead结点占用的字节数, 即为new申请的字节数 )
	return (char*)newElem + sizeof(memoryListHead);
}

// 对双向链表采用头删法手动管理释放内存
// 注意: delete/delete[]时 我们并不知道它操作的是双向链表中的哪一个结点
void  DeleteMemory(void* ptr, bool array) {
	// 注意, 堆的空间自底向上增长. 所以此处为减
	MemoryList* curElem = (MemoryList*)((char*)ptr - sizeof(MemoryList));

	// 如果 new/new[] 和 delete/delete[] 不匹配使用. 直接返回
	if (curElem->_isArray != array)
		return;

	// 更新链表结构
	curElem->_next->_prev = curElem->_prev;
	curElem->_prev->_next = curElem->_next;

	// 更新memoryAllocated值
	memoryAllocated -= curElem->_size;

	// 如果curElem->_file不为NULL, 释放保存文件信息时申请的内存
	if (NULL != curElem->_file)
		free(curElem->_file);

	// 释放内存
	free(curElem);
}


// 重载new/new[]运算符
void* operator new(size_t size, const char* file, size_t line) {
	return AllocateMemory(size, false, file, line);
}

void* operator new[](size_t size, const char* file, size_t line) {
	return AllocateMemory(size, true, file, line);
}

// 重载delete/delete[]运算符
void operator delete(void* ptr) {
	DeleteMemory(ptr, false);
}

void operator delete[](void* ptr) {
	DeleteMemory(ptr, true);
}


// 我们定义的最后一个静态对象析构时调用此函数, 判断是否有内存泄漏, 若有, 则打印出内存泄漏信息
void LeakDetector::_LeakDetector() {
	if (0 == memoryAllocated) {
		std::cout << "恭喜, 您的代码不存在内存泄漏!" << std::endl;
		return;
	}

	// 存在内存泄漏
	// 记录内存泄漏次数
	size_t count = 0;

	// 若不存在内存泄漏, 则双向链表中应该只剩下一个头节点
	// 若存在内存泄漏, 则双向链表中除头节点之外的结点都已泄露,个数即内存泄漏次数
	MemoryList* ptr = memoryListHead._next;
	while ((NULL != ptr) && (&memoryListHead != ptr)) {
		if (true == ptr->_isArray)
			std::cout << "new[] 空间未释放, ";
		else
			std::cout << "new 空间未释放, ";

		std::cout << "指针: " << ptr << " 大小: " << ptr->_size;

		if (NULL != ptr->_file)
			std::cout << "kkkk";
			//std::cout << " 位于 " << ptr->_file << " 第 " << ptr->_line << " 行";
		else
			std::cout << " (无文件信息)";

		std::cout << std::endl;

		ptr = ptr->_next;
		++count;
	}

	std::cout << "存在" << count << "处内存泄露, 共包括 " << memoryAllocated << " byte." << std::endl;
	return;
}

test.cpp:

#include "LeakDetector.h"
 
 
int main() {
 
    // 忘记释放指针 b 申请的内存, 从而导致内存泄露
    int *a = new int;
	int *b = new int[12];
 
    delete a;
 
    return 0;
 
}

 

备注:按原文写代码报错

所以将void* operator new( size_t size, char* file, size_t line ); 改成void* operator new( size_t size, const char* file, size_t line );

 

C语言实现

原理:申请一个链表,malloc 一个内存时,将申请的信息 插入到链表中,free 内存时,将内存信息从链表中移除,最后推出程序的时候,打印内存信息。

这个内存泄漏检测工具很简单,只能检测同一个模块,同一个线程中发送的内存泄漏,对于在编写代码过程中的代码调试有一定的帮助。如果要在集成测试或功能测试中检测内存泄漏,还需借助专门的工具。

源码:

memcheck.c

2. 定义保存内存信息的单向链表

/**
 * 定义链表节点,表示一个内存泄漏信息
 */
typedef struct _mem_node
{
	void *ptr;	   // 泄漏内存地址
	size_t block;	// 泄漏内存大小
	size_t line;	// 泄露发生的代码行
	char *filename;	// 泄漏发生的文件名
	struct _mem_node *next;	// 下一个节点指针
} mem_node;
 
// 定义指向头节点的指针
mem_node *head = NULL;

3. 用于将节点加入单项链表的函数

/**
 * 产生一个节点并加入链表
 * @param ptr 分配的内存地址
 * @param block 分配的内存单元大小
 * @param line 代码行号
 * @param filename 文件名称
 */
static void mem_node_add(void *ptr, size_t block, size_t line, char *filename)
{
	// 产生节点
	mem_node *node = malloc(sizeof(mem_node));
	node->ptr = ptr;
	node->block = block;
	node->line = line;
	node->filename = filename;
	node->next = NULL;
 
	// 加入链表头节点
	if (head)
	{
		node->next = head;
		head = node;
	}
	else
		head = node;
}

4. 从单项链表中删除节点的函数

/**
 * 从链表中删除一个节点
 * @param ptr 分配的内存地址
 */
static void mem_node_remove(void *ptr)
{
	// 判断头节点是否存在
	if (head)
	{
		// 处理头节点
		if (head->ptr == ptr)
		{
			// 获取头节点的下一个节点
			mem_node *pn = head->next;
			// 删除头节点
			free(head);
			// 令头节点指针指向下一个节点
			head = pn;
		}
		else	// 判断链表是否为空
		{
			// 指向节点的指针
			mem_node *pn = head->next;
			// 指向前一个节点的指针
			mem_node *pc = head;
			// 遍历所有节点
			while (pn)
			{
				// 获取指向下一个节点的指针
				mem_node *pnext = pn->next;
				if (pn->ptr == ptr)
				{
					pc->next = pnext;	// 删除当前节点
					free(pn);
				}
				else
					pc = pc->next;
				pn = pnext;
			}
		}
	}
}

5. 显示内存泄露信息报告

/**
 * 显示内存泄漏信息
 */
void show_block()
{
    if (head)
    {
        // 保存总内存泄漏数量
        size_t total = 0;
        // 指向头节点的指针
        mem_node *pn = head;
 
        // 输出标题
        puts("\n\n-------------------------------内存泄漏报告------------------------------------\n");
 
        // 遍历链表
        while (pn)
        {
            mem_node *pnext = pn->next;
            // 处理文件名
            char *pfile = pn->filename, *plast = pn->filename;
            while (*pfile)
            {
                // 找到\字符
                if (*pfile == '\\')
                    plast = pfile + 1;    // 获取\字符的位置
                pfile++;
            }
            // 输出内存泄漏信息
            printf("位置:%s(%d), 地址:%p(%dbyte)\n", plast, pn->line, pn->ptr, pn->block);
            // 累加内存泄漏总量
            total += pn->block;
            // 删除链表节点
            free(pn);
            // 指向下一个节点
            pn = pnext;
        }
        printf("总计内存泄漏:%dbyte\n", total);
    }
}

6. 定义调试用malloc函数

/**
 * 用于调试的malloc函数
 * @param elem_size 分配内存大小
 * @param filename 文件名称
 * @param line 代码行号
 */
void *dbg_malloc(size_t elem_size, char *filename, size_t line)
{
    void *ptr = malloc(elem_size);
    // 将分配内存的地址加入链表
    mem_node_add(ptr, elem_size, line, filename);
    return ptr;
}

7. 定义调试用的calloc函数

/**
 * 用于调试的calloc函数
 * @param count 分配内存单元数量
 * @param elem_size 每单元内存大小
 * @param filename 文件名称
 * @param line 代码行号
 */
void *dbg_calloc(size_t count, size_t elem_size, char *filename, size_t line)
{
    void *ptr = calloc(count, elem_size);
    // 将分配内存的地址加入链表
    mem_node_add(ptr, elem_size * count, line, filename);
    return ptr;
}

8. 定义调试用的free函数

/**
 * 用于调试的free函数
 * @param ptr 要释放的内存地址
 */
void dbg_free(void *ptr)
{
    free(ptr);
    // 从链表中删除节点
    mem_node_remove(ptr);
}

  

 上述代码应包含在一个C文件中(例如memcheck.c),完成上述步骤,就可以利用这一组函数来检测内存泄露了,需要定义如下头文件memcheck.h,

1. 先取消malloc,free和calloc这几个标识符的定义

2.然后#define 重定义 malloc、free和calloc 为我们前面定义的函数

然后工程内其他.c代码文件包含该头文件即可。

memcheck.h

#ifndef _MEM_CHECK_H
#define _MEM_CHECK_H
 
#include <stdlib.h>



// 取消malloc, calloc, free的宏定义
#undef malloc
#undef calloc
#undef free


 
// instead of malloc
#define malloc(s) dbg_malloc(s, __FILE__, __LINE__)
 
// instead of calloc
#define calloc(c, s) dbg_calloc(c, s, __FILE__, __LINE__)
 
// instead of free
#define free(p) dbg_free(p)
 
/**
 * allocation memory
 */
void *dbg_malloc(size_t elem_size, char *filename, size_t line);
 
/**
 * allocation and zero memory
 */
void *dbg_calloc(size_t count, size_t elem_size, char *filename, size_t line);
 
/**
 * deallocate memory
 */
void dbg_free(void *ptr);
 
/**
 * show memory leake report
 */
void show_block();
 
#endif // _MEM_CHECK_H

  使用的时候只需要包含上述头文件(例如命名为memcheck.h),并将上述C文件引入到项目中即可。测试代码如下:

#ifdef DEBUG
#include "memcheck.h"
#endif
 
int main()
{
    int* p;
 
#ifdef DEBUG
    atexit(show_block); // 在程序结束后显示内存泄漏报告
#endif // DEBUG
    // 分配内存并不回收,显示内存泄漏报告
    p = (int*)malloc(1000);
 
    return 0;
}


原文链接:https://blog.csdn.net/mousebaby808/article/details/17212111

猜你喜欢

转载自blog.csdn.net/bandaoyu/article/details/106954009