O vazamento de memória (memory leak) sempre foi um dos problemas mais difíceis, mesmo para programadores experientes. O vazamento de memória ainda persiste mesmo após um certo tempo. Além do fenômeno básico de vazamento de memória alocada, existem muitos tipos diferentes de vazamento de memória, como vazamento em ramificações de exceção, etc. Este projeto guia você na implementação de um detector de vazamento de memória.
Tópicos a Aprender
Como sobrecarregar o operador new
Macros predefinidas __FILE__ e __LINE__
Variáveis estáticas em arquivos de cabeçalho (headfile)
Ponteiro inteligente std::shared_ptr
Teste de Código
Vazamentos de memória geralmente estão relacionados à memória alocada inadvertidamente que permanece não liberada. Em sistemas operacionais modernos, essa memória não liberada ainda será recuperada pelo sistema operacional após a memória regular ter sido usada por uma aplicação que é terminada, portanto, um vazamento de memória causado por uma aplicação transitória não causará consequências sérias.
No entanto, se estivermos escrevendo uma aplicação como um servidor, ela estará sempre em execução. Se houver alguma lógica que leve a um vazamento de memória, há uma alta possibilidade de que ela continue aumentando o vazamento de memória. Eventualmente, o desempenho do sistema será reduzido e pode até causar falha na operação.
Um programa em execução que continuamente continua alocando memória é bastante simples:
int main() {
// allocates memory forever, never release
while(1) int *a = new int;
return 0;
}
Para detectar vazamentos de memória em um programa em execução, geralmente definimos pontos de verificação (checkpoints) na aplicação para analisar se a memória se torna diferente em relação a outros pontos de verificação. Essencialmente, é semelhante a uma inspeção de vazamento de memória de curto prazo do programa. Portanto, o detector de vazamento de memória neste projeto será implementado para um vazamento de memória de curto prazo.
Esperamos que o seguinte código de teste possa detectar um vazamento de memória. Neste código, não liberamos a memória alocada e criamos um vazamento de memória causado por uma ramificação de exceção:
//
// main.cpp
// LeakDetector
//
#include <iostream>
// Implements memory leak inspection here
// #include "LeakDetector.hpp"
// testing exception branch leaking behavior
class Err {
public:
Err(int n) {
if(n == 0) throw 1000;
data = new int[n];
}
~Err() {
delete[] data;
}
private:
int *data;
};
int main() {
// Memory Leak: forget release pointer b
int *a = new int;
int *b = new int[12];
delete a;
// Memory Leak: 0 as constructor parameter causes exception leaking
try {
Err* e = new Err(0);
delete e;
} catch (int &ex) {
std::cout << "Exception: " << ex << std::endl;
};
return 0;
}
Se comentarmos o trecho de código #include "LeakDetector.hpp", então este código pode ser executado normalmente, como mostrado abaixo. No entanto, é claro que sabemos que ele realmente leva a vazamentos de memória:
O Resultado:
Design do Detector de Vazamento de Memória
Para alcançar a detecção de vazamento de memória, vamos considerar os seguintes pontos:
Vazamentos de memória são gerados se delete não foi executado após a operação new.
O destrutor do primeiro objeto criado sempre executa no final.
Então, podemos realizar as seguintes ações correspondentes aos dois pontos acima:
Sobrecarga do operador new.
Criar um objeto estático e chamar o destrutor no final do programa original.
O benefício dessas duas etapas é que a detecção de vazamento de memória pode ser feita sem modificar o código original. Assim, podemos escrever LeakDetector.hpp:
//
// /home/labex/Code/LeakDetector.hpp
//
#ifndef __LEAK_DETECTOR__
#define __LEAK_DETECTOR__
void* operator new(size_t _size, char *_file, unsigned int _line);
void* operator new[](size_t _size, char *_file, unsigned int _line);
// This macro will be described in LeakDetector.cpp
#ifndef __NEW_OVERLOAD_IMPLEMENTATION__
#define new new(__FILE__, __LINE__)
#endif
class _leak_detector
{
public:
static unsigned int callCount;
_leak_detector() noexcept {
++callCount;
}
~_leak_detector() noexcept {
if (--callCount == 0)
LeakDetector();
}
private:
static unsigned int LeakDetector() noexcept;
};
static _leak_detector _exit_counter;
#endif
Por que projetar um callCount? O callCount garante que nosso LeakDetector só possa ser invocado uma vez. Considere o seguinte trecho de código simplificado:
// main.cpp
#include <iostream>
#include "test.h"
int main () {
return 0;
}
// test.hpp
#include <iostream>
class Test {
public:
static unsigned int count;
Test () {
++count;
std::cout << count << ",";
}
};
static Test test;
// test.cpp
#include "test.hpp"
unsigned int Test::count = 0;
A saída final é 1, 2,.
Isso ocorre porque as variáveis estáticas no arquivo de cabeçalho são definidas várias vezes e serão definidas toda vez que forem incluídas em um arquivo .cpp. (Neste caso, será incluído em main.cpp e test.cpp.) No entanto, essencialmente, eles são o mesmo objeto.
Agora, a questão restante é: Como implementar um detector de vazamento de memória?
Implementar um Detector de Vazamento de Memória
Vamos implementar nosso detector de vazamento de memória.
Como já sobrecarregamos o operador new, é fácil pensar em gerenciar a alocação de memória manualmente e liberar a memória naturalmente. Se nosso delete não liberar toda a memória alocada, então ocorrerá um vazamento de memória. Agora, a pergunta é: Que estrutura devemos usar para gerenciar a memória manualmente?
Lista duplamente encadeada é uma boa opção. Para o código real, não sabemos quando precisamos alocar memória, portanto, uma lista linear unidirecional não é boa o suficiente, mas uma estrutura dinâmica (por exemplo, uma lista encadeada) será muito mais conveniente. Além disso, uma lista linear unidirecional também não é conveniente ao excluir o objeto no detector de vazamento de memória.
Assim, a lista duplamente encadeada é nossa melhor opção no momento:
#include <iostream>
#include <cstring>
// Defining __NEW_OVERLOAD_IMPLEMENTATION__ macros,
// it prevents overwrite operator `new`
#define __NEW_OVERLOAD_IMPLEMENTATION__
#include "LeakDetector.hpp"
typedef struct _MemoryList {
struct _MemoryList *next, *prev;
size_t size; // size of allocated memory
bool isArray; // either allocated array or not
char *file; // locating leaking file
unsigned int line; // locating leaking line
} _MemoryList;
static unsigned long _memory_allocated = 0; // saving unreleased memory size
static _MemoryList _root = {
&_root, &_root, // pointers of first element all pointing to itself
0, false, // 0 allocated memory size, not array
NULL, 0 // no file, line 0
};
unsigned int _leak_detector::callCount = 0;
Agora, vamos gerenciar a memória alocada manualmente:
// allocating memory starts from the head of _MemoryList
void* AllocateMemory(size_t _size, bool _array, char *_file, unsigned _line) {
// calculate new size
size_t newSize = sizeof(_MemoryList) + _size;
// we can only use malloc for allocation since new has already been overwited.
_MemoryList *newElem = (_MemoryList*)malloc(newSize);
newElem->next = _root.next;
newElem->prev = &_root;
newElem->size = _size;
newElem->isArray = _array;
newElem->file = NULL;
// save file info if exitsting
if (_file) {
newElem->file = (char *)malloc(strlen(_file)+1);
strcpy(newElem->file, _file);
}
// save line number
newElem->line = _line;
// update list
_root.next->prev = newElem;
_root.next = newElem;
// save to unreleased memory size
_memory_allocated += _size;
// return allocated memory
return (char*)newElem + sizeof(_MemoryList);
}
e o operador delete também:
void DeleteMemory(void* _ptr, bool _array) {
// back to the begining of MemoryList
_MemoryList *currentElem = (_MemoryList *)((char *)_ptr - sizeof(_MemoryList));
if (currentElem->isArray != _array) return;
// update list
currentElem->prev->next = currentElem->next;
currentElem->next->prev = currentElem->prev;
_memory_allocated -= currentElem->size;
// release allocated memory for file information
if (currentElem->file) free(currentElem->file);
free(currentElem);
}
Considerando que temos duas maneiras diferentes de usar new e delete:
Finalmente, precisamos implementar _leak_detector::LeakDetector e ele será chamado no destrutor de static _leak_detector _exit_counter. Então, todos os objetos alocados serão liberados. O vazamento de memória aparece no caso em que static _MemoryList _root não está vazio, e só precisamos iterar _root para encontrar onde está o vazamento de memória:
unsigned int _leak_detector::LeakDetector(void) noexcept {
unsigned int count = 0;
// iterates the whole list, if there is a memory leak,
// then _LeakRoot.next alwasy doesn't pointing to itself
_MemoryList *ptr = _root.next;
while (ptr && ptr != &_root)
{
// output all leaking information
if(ptr->isArray)
std::cout << "Leaking[] ";
else
std::cout << "Leaking ";
std::cout << ptr << ", size of " << ptr->size;
if (ptr->file)
std::cout << " (located in " << ptr->file << ", line " << ptr->line << ")";
else
std::cout << " (no file info)";
std::cout << std::endl;
++count;
ptr = ptr->next;
}
if (count)
std::cout << "Existing " << count << " memory leaking behavior, "<< _memory_allocated << " byte in total." << std::endl;
return count;
}
Em main.cpp, descomente o include "LeakDetector.hpp". Aqui está a saída esperada:
Resumo
Neste laboratório, sobrecarregamos o operador new em C++ e implementamos um detector de vazamento de memória.
Na verdade, o código também pode ser usado para vazamento de referência circular de ponteiro inteligente. Só precisamos mudar main.cpp da seguinte forma:
#include <memory> // use smart pointer
// no changes at the begining as previous in main.cpp
// add two testing class
class A;
class B;
class A {
public:
std::shared_ptr<B> p;
};
class B {
public:
std::shared_ptr<A> p;
};
int main() {
// changes here as previous in main.cpp
// additional cases, use smart pointer
auto smartA = std::make_shared<A>();
auto smartB = std::make_shared<B>();
smartA->p = smartB;
smartB->p = smartA;
return 0;
}
Nossas saídas finais são:
Para alguns de vocês, talvez o resultado seja:
Seu resultado pode ser diferente. Cada um dos dois acima está correto, porque seu ambiente pode ter diferenças na máquina e no compilador.