一个小项目 --- C++实现内存泄漏检查器
Posted ssopp24
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一个小项目 --- C++实现内存泄漏检查器相关的知识,希望对你有一定的参考价值。
先贴出代码:
.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, char* file, size_t line );
void* operator new[]( size_t size, 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中定义, 以防止LeakDetector.cpp中, 我们自己重载的 operator new/new[]( ) 被宏替换. 而这个宏在被测试文件中未定义(我们除过在被测试文件包含LeakDetector.h头文件外, 不改变被测试文件的代码), 所以 替换被测试文件new运算符, 传进两个参数 文件名 和 行号 使用我们自己的重载版本operator new/new[]( size_t size, 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( );
;
// 静态对象
static LeakDetector exitCounter;
#endif
.cpp:
// 这个宏保证 LeakDetector.cpp 中的new 不会被LeakDetector.h中的 宏替换 替换掉
#define NEW_OVERLOAD_IMPLEMENTATION_
#include <iostream> //cout
#include <cstring> //strlen 和 strcpy
#include "LeakDetector.h"
// 初始化 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, 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, char* file, size_t line )
return AllocateMemory( size, false, file, line );
void* operator new[]( size_t size, 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 << " 位于 " << 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;
思路:
1.内存泄露产生于 new/new[]
操作进行后没有执行 delete/delete[]
2.最先被创建的对象, 其析构函数是最后执行的
解决方法:
1.重载operator new/new[ ] 与 operator delete/delete[ ], 并借助双向链表结构(带头节点)我们自己手动管理内存
2.创建一个静态对象, 在程序退出时才调用这个静态对象的析构函数( 在析构函数中, 我们调用内存泄漏检测函数 ), 这就得保证, 我们的静态对象必须先于被测文件的静态对象创建(如果有), 这样我们的静态对象才会最后一个析构(必须保证最后一个析构, 以免发生问题( 如: 假设被测文件也有一个静态对象, 且静态对象申请了空间, 被测文件的静态对象在我们的静态对象析构后析构, 这样内存泄漏检测就会不准确 )), 所以被测文件必须第一个包含我们的 LeakDetector.h 头文件保证我们的静态对象第一个创建.
这样两个步骤的好处在于: 无需修改原始代码的情况下, 就能进行内存检查. 这同时也是我们希望看到的
既然我们已经重载了new/new[], delete/delete[]操作符, 那么我们很自然就能想到通过手动管理内存申请和释放, 如果我们delete/delete[]时没有将申请的内存全部释放完毕, 那么一定发生了内存泄露. 接下来一个问题就是,使用什么结构来实现手动管理内存:
不妨使用双向链表来实现内存泄露检查. 原因在于, 对于内存检查器来说, 并不知道实际代码在什么时候会需要申请内存空间, 所以使用线性表并不够合理, 一个动态的结构(链表)是非常便捷的. 而我们在删除内存检查器中的对象时, 需要更新整个结构, 对于单向链表来说, 也是不够便利的
我们的内存泄漏检测函数会在静态对象析构时被调用, 这时候其他所有申请的对象都已经完成析构, 这时, 如果我们的双向链表除头节点外仍有结点, 那么一定是泄露且尚未释放的内存, 所以我们只需要遍历双向链表即可得到我们需要的结果
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个字节的内存大小即可
同样, delete关键字做两件事:
第一: 调用对象析构函数
第二: 调用operator delete( )释放对象所占用的内存
string* pte = new string("KobeBryant");
delete ptr -> ptr->~string -> operator delete( ptr )
void operator delete(void* memoryToBeDeallocated);
同样, 我们能做的也只有重载operator delete( void* ) 来自己手动管理内存释放.
接下来我们来说下new [] 和 delete []
:
AA为4字节: new AA[10] -> 并不是只开40个字节大小, 而要在对象数组的大小上加上一个额外数据,用于编译器区分对象数组大小:
sizeof(size_t) + 4 * sizeof(AA)
这个多出来的内存用来存申请数目, 由编译器在operator new之后, new expression前设置, 你最终拿到的地址实际上和operator new分到的地址并不一样
编译器通过这个数目(对象数组中元素个数), 来确定调用几次对象的构造函数.
当然,这时, 调用几次构造函数已经和operator new[]无关了. operator new[]根本无法判断到底分配了几个对象,它只知道一共有多少字节
所以, 我们重载时不用关注这个步骤.
这是编译器要做的.
在实现层,两者就是一样的,通常new[] 直接调用new实现而operator new 和 operator new[] 的参数 size也是由编译器传给我们的, 我们不用自己去计算要多开的 sizeof(size_t)大小.
总结下: 我们要重载的 operator new 和·operator new[] 只需要做一件事: 调用malloc 开编译器传给我们的 size 个字节即可.
同样delete[ ]:
1. 因为之前保存过对象个数, 它调用对象个数次析构函数
2. 调用 operator delete[ ] 对内存进行释放(释放对象占用的内存和多开的保存对象个数的内存)
operator delete[]( void* ptr ) 不管对象个数的事, 它也不知道这些. 它只需要做一件事: 释放ptr指向内存即可. 通常调用 operator delete()实现
我们重载的operator delete[] 也实现同样功能.
总体总结下:
new做了两件事:
1. 调用operator new分配空间。
2. 调用构造函数初始化对象。
delete也做了两件事:
1. 调用析构函数清理对象
2. 调用operator delete释放空间
new[N]:
1. 调用operator new分配空间。
2. 调用N次构造函数分别初始化每个对象。
delete[]:
1. 调用N次析构函数清理对象。
2. 调用operator delete释放空间。
1. operator new/operator delete operator new[]/operator delete[] 和 malloc/free用法一样。
2. 他们只负责分配空间/释放空间,不会调用对象构造函数/析构函数来初始化/清理对象。
3. 实际operator new和operator delete只是malloc和free的一层封装。
对了, 并不是所有的类型new []都会多开sizeof(size_t)个字节保存对象个数: 为什么要保存对象个数呢? 因为编译器得知道它要调用几次构造函数和析构函数: 问题来了, 内置类型(int, char等)需要调用析构函数吗: 答案是不一定, 参考这篇文章: http://blog.csdn.net/mind_v/article/details/70740354 所以, 自定义类型, 一定会多开. 内置类型: 不一定.
:http://www.cnblogs.com/fly1988happy/archive/2012/04/26/2471099.html
:http://blog.csdn.net/wudaijun/article/details/9273339#t7
:https://www.zhihu.com/question/25497587
通过这个项目我获得的知识点(有些知识点是之前学过但遗忘了进行复习):
0.更深入了解了 C++ new/new[]和delete/delete[]背后所做的事
1.LeakDetector.cpp 和 被测试文件.cpp 都包含了 LeakDetector.h. 会造成我们这个类重定义吗?
不会: 在一个给定的源文件中,一个类只能被定义一次。如果在多个文件中定义一个类,那么每个文件中的定义必须是完全相同的
因为这遵守“单一定义规则”(One-Definition Rule, ODR)。根据此规则, 如果对同一个类的两个定义完全相同且出现在不同编译单位,会被当作同一个定义。
这里头文件分别被两个不同的编译单位(LeakDetector.cpp, test.cpp)包含,满足ODR规则,会被当作同一个定义。 所以不会有冲突。
此外,模板和inline函数也适用此规则。http://blog.csdn.net/baoxiaofeicsdn/article/details/48338515
2.__FILE__, __LINE__两个预定义宏
3.带头节点双向链表结构(头插, 和删除任意节点)
4.变量存储持续性,作用域, 链接性
5.堆栈生长规律
6.头文件中通常包含的内容: 函数原型, 使用#define/const 定义的符号常量, 结构声明, 类声明, 模板声明, 内联函数
7.复习了条件编译
8.类中静态数据成员与静态类方法:
静态数据成员: 属于所有对象而不是特定对象, 为了实现共享数据
静态类方法: 通过类名::FunName( )调用, 不能通过对象调用.
它们不能通过对象调用是因为它们没有隐含的this指针
http://www.cnblogs.com/ppgeneve/p/5091794.html
http://blog.csdn.net/kerry0071/article/details/25741425/
9.复习了多个.cpp源文件 如何编译链接成 一个可执行程序过程
10.typedef struct结构体时一些规则. 还有typedef 和 #define 区别
11.unsigned int / unsigned long 和 size_t 故事:
size_t平台移植性更好:
http://blog.csdn.net/lemoncyb/article/details/12012987
http://jeremybai.github.io/blog/2014/09/10/size-t
12.静态对象:可以调用它的所有成员, 包括非静态成员. 但静态函数智能调用静态成员.
静态对象何时创建, 何时销毁:http://blog.csdn.net/shltsh/article/details/45959493
以上是关于一个小项目 --- C++实现内存泄漏检查器的主要内容,如果未能解决你的问题,请参考以下文章
如何检查 C++ 代码中的内存泄漏。有没有检查内存泄漏的免费工具[重复]