Как обнаружить утечку памяти
Введение
При разработке больших приложений, оперирующих большими объемами информации
на первое место при отладке встает проблема обнаружения неправильного распределения
памяти. Суть проблемы состоит в том, что если мы выделили участок памяти, а
затем освободили не весь выделенный объем, то образуются блоки памяти, которые
помечены как занятые, но на самом деле они не используются. При длительной работе
программы такие блоки могут накапливаться, приводя к значительному расходу памяти.
Для обнаружения подобных ошибок создано специализированное программное обеспечение
(типа BoundsChecker от Numega), однако чаще бывает удобнее встроить механизм
обнаружения утечки в свои проекты. Поэтому метод должен быть простым, и в то
же время как можно более универсальным. Кроме того, не хотелось бы переписывать
годами накопленные мегабайты кода, написанного и отлаженного задолго до того,
как вам пришло в голову оградить себя от ошибок. Так что к списку требований
добавляется стандартизация, т.е. нужно каким-то образом встроить защиту от ошибок
в стандартный код.
Предлагаемое решение основывается на перегрузке стандартных операторов распределения
памяти new и delete. Причем перегружать мы будем глобальные операторы
new|delete,
т.к. переписать эти операторы для каждого разработанного ранее класса было бы
очень трудоемким процессом. Т.о. после перегрузки нам нужно будет только отследить
распределение памяти и, соответственно, освобождение ее в момент завершения
программы. Все несоответствия - ошибка.
Реализация
Проект написан на Visual C++, но переписать его на любой другой диалект С++
не будет слишком сложной задачей. Во-первых, нужно переопределить стандартные
операторы new и delete так, чтобы это работало во всех проектах. Поэтому в stdafx.h
добавляем следующий фрагмент:
#ifdef _DEBUG
inline void * __cdecl operator new(unsigned int size,
const char *file, int line)
{
};
inline void __cdecl operator delete(void *p)
{
};
#endif
Как видите, переопределение операторов происходит в блоке
#ifdef/#endif. Это
ограждает наш код от влияния на релиз компилируемой программы. Вы, наверное,
заметили, что теперь оператор new имеет три параметра вместо одного. Два дополнительных
параметра содержат имя файла и номер строки, в которой выделяется память. Это
удобно для обнаружения конкретного места, где происходит ошибка. Однако код
наших проектов по-прежнему ссылается на оператор
new, принимающий один параметр.
Для исправления этого несоответствия нужно добавиить следующий фрагмент
#ifdef _DEBUG
#define DEBUG_NEW new(__FILE__, __LINE__)
#else
#define DEBUG_NEW new
#endif
#define new DEBUG_NEW
Теперь все наши операторы new будут вызываться с тремя параметрами, причем
недостающие параметры подставит препроцессор. Конечно, пустые переопределенные
функции ни в чем нам не помогут, так что давайте добавим в них какой-нибудь
код:
#ifdef _DEBUG
inline void * __cdecl operator new(unsigned int size,
const char *file, int line)
{
void *ptr = (void *)malloc(size);
AddTrack((DWORD)ptr, size, file, line);
return(ptr);
};
inline void __cdecl operator delete(void *p)
{
RemoveTrack((DWORD)p);
free(p);
};
#endif
Для полноты картины нужно переопределить операторы
new[] и delete[], однако
никаких существенных отличий здесь нет - творите!
Последний штрих - пишем функции
AddTrack() и RemoveTrack(). Для создания списка
используемых блоков памяти будем использовать стандартные средства
STL:
typedef struct {
DWORD address;
DWORD size;
char file[64];
DWORD line;
} ALLOC_INFO;
typedef list<ALLOC_INFO*> AllocList;
AllocList *allocList;
void AddTrack(DWORD addr, DWORD asize, const char *fname, DWORD lnum)
{
ALLOC_INFO *info;
if(!allocList) {
allocList = new(AllocList);
}
info = new(ALLOC_INFO);
info->address = addr;
strncpy(info->file, fname, 63);
info->line = lnum;
info->size = asize;
allocList->insert(allocList->begin(), info);
};
void RemoveTrack(DWORD addr)
{
AllocList::iterator i;
if(!allocList)
return;
for(i = allocList->begin(); i != allocList->end(); i++)
{
if((*i)->address == addr)
{
allocList->remove((*i));
break;
}
}
};
Перед самым завершением программы наш список allocList содержит ссылки на блоки
памяти, котороые не были освобождены. Все, что нужно сделать - вывести эту информацию
куда-нибудь. В нашем проекте мы выведем список неосвобожденных участков памяти
в окно вывода отладочных сообщений Visual C++:
void DumpUnfreed()
{
AllocList::iterator i;
DWORD totalSize = 0;
char buf[1024];
if(!allocList)
return;
for(i = allocList->begin(); i != allocList->end(); i++) {
sprintf(buf, "%-50s:\t\tLINE %d,\t\tADDRESS %d\t%d unfreed\n",
(*i)->file, (*i)->line, (*i)->address, (*i)->size);
OutputDebugString(buf);
totalSize += (*i)->size;
}
sprintf(buf, "--------------------------------------------------\n");
OutputDebugString(buf);
sprintf(buf, "Total Unfreed: %d bytes\n", totalSize);
OutputDebugString(buf);
};
Надеюсь, этот проект сделает ваши баг-листы короче, а программы устойчивее.
Удачи!
|