C++内存泄漏一直是让软件工程师头痛的问题,因为内存的泄漏可能最终导致系统内存耗尽而崩溃。今天想到一个方法来跟踪与控制对象的分配,原理就是在构造函数中检测该对象是在堆上分配还是在栈上分配的,如果是在栈上分配的当然不用管它,离开作用域会被自动销毁,我们只需要跟踪椎上创建的对象。
最关键的问题是如何判断对象是在堆上还是在栈上创建的呢?我们可以通过对象所在的位置,也就是this指针的值来检测是在堆的区域还是在栈的区域。一般的,操作系统会为静态存储区、堆、栈分别分配几块空间,而且这几块空间都独立而且距离较远。距离this指针最近的是堆,就可以认为this在堆上,距离this指针为栈,则可以认为对象在栈上,类似地如果距离静态存储区最近则可以认为该变量为静态变量。
根据此原理我们可以实现一个父类,在父类的构造函数中检测该对象的位置,以后可以根据该对象位置可以判断是否需要采取一些措施,比如对于堆上创建的对象,我们可以记录、跟踪,并且在退出的时候对其进行检测看是否被全部释放,便于检测内存泄漏。而被监测的类型不需要添加额外代码,只需要从该父类继承即可,该类的一个初步实现为。
#include <iostream>
#include <cstdio>
#include <cmath>
#if defined(_WIN32)
#include<Windows.h>
#else
#include <errno.h>
#include <pthread.h>
#endif
using namespace std;
class Locationable
{
public:
typedef enum { UNKNOWN, HEAP, STACK, STATIC } ObjectLocation;
private:
static int* _spi;
static const char * const nameTable[];
ObjectLocation _location;
public:
Locationable( const char* name = "")
{
_location = Locate( );
}
inline unsigned long long distance(void* a1, void* a2)
{
return a1 > a2 ? ((unsigned long long)a1 - (unsigned long long)a2)
: ((unsigned long long)a2 - (unsigned long long)a1);
}
inline ObjectLocation Location( void ) const { return _location; }
inline const char* LocationStr( void ) const
{
return nameTable[_location];
}
private:
ObjectLocation Locate( void )
{
unsigned long long dStack,dHeap, dStatic;
dHeap = distance(_spi, this);
dStack = distance(&dStack, this);
dStatic = distance(&_spi, this);
if( dHeap < dStack){
return dHeap < dStatic ? HEAP : STATIC;
}else{
return dStack < dStatic ? STACK : STATIC;
}
}
};
int* Locationable::_spi = new int;
const char * const Locationable::nameTable[]={"UNKNOWN","HEAP","STACK","STATIC"};
#if defined(_WIN32)
DWORD WINAPI work_thread_proc(LPVOID)
#else
void* work_thread_proc( void* )
#endif
{
printf("======= work thread ==========/n"); // cout does not work in work thread
Locationable e("e @ stack");
Locationable* d = new Locationable("d @ heap");
char *pm = new char[100*1024*1024];
Locationable a("a @ stack");
Locationable* b = new Locationable("b @ heap");
static Locationable s1("s @ static");
//pthread_exit(NULL);
return NULL;
}
int main( void )
{
Locationable e("e @ stack");
Locationable* d = new Locationable("d @ heap");
char *pm = new char[100*1024*1024];
Locationable a("a @ stack");
Locationable* b = new Locationable("b @ heap");
static Locationable s1("s @ static");
#if defined(_WIN32)
HANDLE htd = CreateThread(NULL, 0, &work_thread_proc,NULL, 0, NULL);
WaitForSingleObject(htd, 1000);
CloseHandle(htd);
#else
pthread_t tid;
int ret = pthread_create(&tid, NULL, &work_thread_proc, NULL);
if( ret != 0){
cout<<"FAILED to create thread:"<<ret<<endl;
perror("pthread_create( )");
}
cout<<"Created work thread:"<<tid<<endl;
void* ec;
pthread_join(tid, &ec);
#endif
char c ;
cin>>c;
}
在Locate函数中,计算局部变量dStack与this指针的距离,因为是在构造函数,而且构造函数中并没有定义太多局部变量,如果对象是在栈上创建,则this与dStack的距离必然很近,所以该方法是可以保证该对象是在栈上的正确性与可靠性。而对于静态存储区和堆的位置的检测暂时还不能确定,这个需要研究各种操作系统进程加载时的内在布局以及多线程时的内存布局来确认。但上例中代码的简单测试是没有问题的,也说明该方法的可行性。
如果我们约定,所以自定义对象都继承自Locationable类,那么我们所有的自定义类都的创建与销毁都可以被监控,可以实现一个监控类,叫作ObjectManager,定义在objmgr.hpp中,全文如下:
// objmgr.hpp
#ifndef OBJECTMANAGER_HPP_H
#define OBJECTMANAGER_HPP_H
#include <map>
namespace noock{
namespace memory{
class Locationable;
typedef enum { OM_NONE=0, OM_HEAP=0x01, OM_STACK=0x02, OM_STATIC=0x04, OM_ALL= 0x07 } ObjectMask;
/// map collection for ObjectManager
typedef std::map<void*, Locationable*> ObjectMap;
/// A Object manager on heap
class ObjectManager
{
private:
ObjectMap _objects;
static ObjectManager* _instance;
static ObjectMask _mask ;
public:
void Add(Locationable* obj);
/// remove the object from manager when the object is to be deleted
/// @return true: if found and removed, false: not found or NULL obj
bool Remove(Locationable* obj);
/// check existence of an object
bool Exists(Locationable* obj) const;
/// destroy the object
void Destroy(Locationable* obj);
/// Get the count of the objects on heap now
inline size_t Count( void ) const{ return _objects.size(); }
static ObjectManager& Instance( void );
/// Get A copy of the objects
const ObjectMap Objects( void ) const { return _objects; }
static inline ObjectMask Mask( void ) { return _mask; }
static inline void Mask(ObjectMask val) { _mask = val; }
private:
ObjectManager( ) { }
ObjectManager(const ObjectManager&);
ObjectManager& operator=(const ObjectManager&);
};
}
}
它负责管理一个对象集,使用MAP数据结构,key就是其地址,而value即对象的指针,实现如objmgr.cpp。ObjectManager使用了单件模式(Singleton),有且仅有一个管理器,通过Instance( )方法获取实例,如果在C#中就方便了,可以使用属性,就不需要那个括号了。
Objects返回一个MAP,包含所有被管理的对象,但那只是一个拷贝,而不能直接操作内部的MAP,因为在Destroy方法中会对内部的map进行操作,如果在外部的通过迭代器的for循环中调用Destroy方法会导致错误的迭代器状态。
通过设置Mask( )可以设置跟踪的对象类型,默认只跟踪堆上的对象。
// objmgr.cpp
#include "objmgr.hpp"
#include <stdexcept>
#include "locationable.hpp"
namespace noock{
namespace memory{
ObjectManager* ObjectManager::_instance = NULL;
ObjectMask ObjectManager::_mask = OM_ALL;
void ObjectManager::Add( Locationable* obj )
{
if( obj == NULL)
throw std::invalid_argument("NULL pointer to remove");
_objects[(void*)obj] = obj;
}
bool ObjectManager::Remove( Locationable* obj )
{
if( obj == NULL)
return false;
ObjectMap::iterator iobj = _objects.find((void*)obj);
if(iobj != _objects.end()){
_objects.erase(iobj);
printf("ObjectManager::Remove() : 0x%08X : %s @ %s/n"
, (unsigned int)obj, typeid(*obj).name(), obj->LocationStr());
return true;
}
return false;
}
bool ObjectManager::Exists( Locationable* obj ) const
{
if( obj == NULL)
return false;
return _objects.find((void*)obj) != _objects.end();
}
void ObjectManager::Destroy( Locationable* obj )
{
if( obj == NULL)
throw std::invalid_argument("NULL pointer to delete");
if( obj->Location() != OL_HEAP)
throw std::invalid_argument("An object not on heap cannot be destroyed manually");
ObjectMap::iterator iobj = _objects.find((void*)obj);
if(iobj != _objects.end())
_objects.erase(iobj);
printf("ObjectManager::Destroy() : 0x%08X : %s @ %s/n"
, (unsigned int)obj, typeid(*obj).name(), obj->LocationStr());
delete obj;
}
ObjectManager& ObjectManager::Instance( void )
{
if(_instance == NULL)
_instance = new ObjectManager();
return *_instance;
}
}// namespace memory
}// namespace noock
在Locationable在构造函数中将this指针添加到ObjectManager中,在其被析构的时候在析构函数中从ObjectManager中删除,这样就可以跟踪堆上的对象,在特定状态就可以检测堆上的对象情况,比如在程序退出之前检测堆上的对象是否为空,如果不为空则证明存在资源泄漏的情况,修改后的Locationable如下:
// locationable.hpp
#ifndef LOCATIONABLE_HPP_H
#define LOCATIONABLE_HPP_H
namespace noock{
namespace memory{
typedef enum { OL_UNKNOWN, OL_HEAP, OL_STACK, OL_STATIC } ObjectLocation;
class Locationable
{
private:
static int* _spi;
static const char * const nameTable[];
ObjectLocation _location;
public:
Locationable();
inline unsigned long long distance(void* a1, void* a2)
{
return a1 > a2 ? ((unsigned long long)a1 - (unsigned long long)a2)
: ((unsigned long long)a2 - (unsigned long long)a1);
}
inline ObjectLocation Location( void ) const { return _location; }
inline const char* LocationStr( void ) const
{
return nameTable[_location];
}
virtual ~Locationable(void);
private:
ObjectLocation Locate( void );
};
}// namespace memory
}// namespace noock
#endif
其实现为locationable.cpp
// locationable.cpp
#include "locationable.hpp"
#include "objmgr.hpp"
namespace noock{
namespace memory{
int* Locationable::_spi = new int;
const char * const Locationable::nameTable[]={"UNKNOWN","HEAP","STACK","STATIC"};
ObjectLocation Locationable::Locate( void )
{
unsigned long long dStack,dHeap, dStatic;
dHeap = distance(_spi, this);
dStack = distance(&dStack, this);
dStatic = distance(&_spi, this);
if( dHeap < dStack){
return dHeap < dStatic ? OL_HEAP : OL_STATIC;
}else{
return dStack < dStatic ? OL_STACK : OL_STATIC;
}
}
Locationable::~Locationable( void )
{
ObjectManager::Instance().Remove(this);
}
Locationable::Locationable()
{
_location = Locate( );
if( ObjectManager::Mask() != OM_NONE){
switch(_location){
case OL_HEAP:
if( ObjectManager::Mask() & OM_HEAP)
ObjectManager::Instance().Add(this);
break;
case OL_STATIC:
if( ObjectManager::Mask() & OM_STATIC)
ObjectManager::Instance().Add(this);
break;
case OL_STACK:
if( ObjectManager::Mask() & OM_STACK)
ObjectManager::Instance().Add(this);
break;
default:
if( ObjectManager::Mask() == OM_ALL)
ObjectManager::Instance().Add(this);
}
}
}
}// namespace memory
}// namespace noock
使用示例:
#include <iostream>
#include <cstdio>
#include <cmath>
#if defined(_WIN32)
#include<Windows.h>
#else
#include <errno.h>
#include <pthread.h>
#endif
#include "locationable.hpp"
#include "objmgr.hpp"
using namespace std;
using namespace noock::memory;
class A : public noock::memory::Locationable
{
};
class B : public noock::memory::Locationable
{
};
class C : public noock::memory::Locationable{ };
Locationable gL1;
#if defined(_WIN32)
DWORD WINAPI work_thread_proc(LPVOID)
#else
void* work_thread_proc( void* )
#endif
{
printf("======= work thread ==========/n"); // cout does not work in work thread
Locationable e;
Locationable* d = new Locationable;
char *pm = new char[100*1024*1024];
Locationable a;
Locationable* b = new Locationable;
static Locationable s1;
return NULL;
}
int main( void )
{
ObjectManager::Instance().Mask(OM_HEAP);
Locationable e;
Locationable* d = new Locationable;
char *pm = new char[100*1024*1024];
Locationable a;
Locationable* b = new Locationable;
static Locationable s1;
#if defined(_WIN32)
HANDLE htd = CreateThread(NULL, 0, &work_thread_proc,NULL, 0, NULL);
WaitForSingleObject(htd, 1000);
CloseHandle(htd);
#else
pthread_t tid;
int ret = pthread_create(&tid, NULL, &work_thread_proc, NULL);
if( ret != 0){
cout<<"FAILED to create thread:"<<ret<<endl;
perror("pthread_create( )");
}
cout<<"Created work thread:"<<tid<<endl;
void* ec;
pthread_join(tid, &ec);
#endif
cout<<"*************************"<<endl;
A a1, a2;
A *a3 = new A;
B b1;
B *b2 = new B, *b3 = new B;
C c1;
static A a5;
C *c2 = new C;
static C c3;
static B b4;
A a4;
// free all objects on heap AT LAST
printf("Object #:%d/n", ObjectManager::Instance().Count());
const ObjectMap objects = ObjectManager::Instance().Objects();
for(ObjectMap::const_iterator it = objects.begin(); it != objects.end(); ++it){
Locationable* pobj = it->second;
const type_info& t = typeid(*it->second);
printf("0x%08lX : %s @ %s/n", (unsigned int)it->first, t.name(), it->second->LocationStr());
if( it->second->Location() == OL_HEAP){
ObjectManager::Instance().Destroy(it->second); // don't call it here
}
}
cout<<"+++++++++++++"<<endl;
printf("Object #:%d/n", ObjectManager::Instance().Count());
ObjectMap objects2 = ObjectManager::Instance().Objects();
for(ObjectMap::const_iterator it = objects2.begin(); it != objects2.end(); ++it){
Locationable* pobj = it->second;
const type_info& t = typeid(*it->second);
printf("0x%08lX : %s @ %s/n", (unsigned int)it->first, t.name(), it->second->LocationStr());
}
char c ;
cin>>c;
}
#define ENABLE_HEAP_TRACE // 开关,也可以在命令行选项中实现
#if defined(ENABLE_HEAP_TRACE)
#define HEAP_TRACE : public noock::memory::Locationable
#else
#define HEAP_TRACE
#endif
class D HEAP_TRACE
{
};
该方法可以将跟踪与业务代码完全隔离。而在程序退出之前可以通过动态类型信息(RTTI),使用typeid运算符获取是哪个类型的对象被泄漏了。例如只对堆内存监控时的一次运行结果:
======= work thread ========== ************************* Object #:9 0x003957E0 : class noock::memory::Locationable @ HEAP ObjectManager::Destroy() : 0x003957E0 : class noock::memory::Locationable @ HEAP 0x00395860 : class noock::memory::Locationable @ HEAP ObjectManager::Destroy() : 0x00395860 : class noock::memory::Locationable @ HEAP 0x003958E0 : class A @ HEAP ObjectManager::Destroy() : 0x003958E0 : class A @ HEAP 0x00395960 : class B @ HEAP ObjectManager::Destroy() : 0x00395960 : class B @ HEAP 0x003959E0 : class B @ HEAP ObjectManager::Destroy() : 0x003959E0 : class B @ HEAP 0x00395A60 : class C @ HEAP ObjectManager::Destroy() : 0x00395A60 : class C @ HEAP 0x00396B50 : class noock::memory::Locationable @ HEAP ObjectManager::Destroy() : 0x00396B50 : class noock::memory::Locationable @ HEAP 0x00396BD0 : class noock::memory::Locationable @ HEAP ObjectManager::Destroy() : 0x00396BD0 : class noock::memory::Locationable @ HEAP 0x0042A298 : class noock::memory::Locationable @ STATIC +++++++++++++ Object #:1 0x0042A298 : class noock::memory::Locationable @ STATIC
因为有一个全局的静态变量,默认的监控范围是所有对象,所以静态全局变量在main函数执行之前,也就是在设置监控范围为OM_HEAP之前已经添加到了被监控对象的列表中,故最后列表中还有一个对象。
注意:
1) 本文中的ObjectManager类不是线程安全的,如果在多线程环境下还要根据操作系统不同使用互斥锁保证内部map列表的完整性。
2) 不同操作系统在多线程环境下堆的分配是不同的,本文中假设多线程共享一个堆,对于不同线程使用独立堆空间的情况可能还不适用,这种情况就比较复杂了,就需要每次判断前检测线程的ID或每次在堆上创建一个对象(这样的性能消耗较大)。