Quais são algumas dicas gerais para garantir que eu não vaze memória nos programas C ++? Como faço para descobrir quem deve liberar memória que foi alocada dinamicamente?
Quais são algumas dicas gerais para garantir que eu não vaze memória nos programas C ++? Como faço para descobrir quem deve liberar memória que foi alocada dinamicamente?
Respostas:
Em vez de gerenciar a memória manualmente, tente usar ponteiros inteligentes, quando aplicável.
Dê uma olhada no Boost lib , TR1 e ponteiros inteligentes .
Agora, ponteiros inteligentes agora fazem parte do padrão C ++ chamado C ++ 11 .
Endosso completamente todos os conselhos sobre RAII e indicadores inteligentes, mas também gostaria de adicionar uma dica de nível um pouco mais alto: a memória mais fácil de gerenciar é a memória que você nunca alocou. Diferente de linguagens como C # e Java, onde praticamente tudo é uma referência, em C ++ você deve colocar objetos na pilha sempre que puder. Como já vi várias pessoas (incluindo o Dr. Stroustrup), a principal razão pela qual a coleta de lixo nunca foi popular em C ++ é que o C ++ bem escrito não produz muito lixo em primeiro lugar.
Não escreva
Object* x = new Object;
ou mesmo
shared_ptr<Object> x(new Object);
quando você pode escrever
Object x;
Este post parece ser repetitivo, mas em C ++, o padrão mais básico a ser conhecido é o RAII .
Aprenda a usar ponteiros inteligentes, tanto do boost, TR1 quanto do auto_ptr modesto (mas geralmente eficiente o suficiente) (mas você deve conhecer suas limitações).
RAII é a base da segurança de exceção e do descarte de recursos em C ++, e nenhum outro padrão (sanduíche, etc.) fornecerá os dois (e na maioria das vezes, não fornecerá nenhum).
Veja abaixo uma comparação do código RAII e não-RAII:
void doSandwich()
{
T * p = new T() ;
// do something with p
delete p ; // leak if the p processing throws or return
}
void doRAIIDynamic()
{
std::auto_ptr<T> p(new T()) ; // you can use other smart pointers, too
// do something with p
// WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}
void doRAIIStatic()
{
T p ;
// do something with p
// WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}
Para resumir (após o comentário do Ogre Psalm33 ), o RAII se baseia em três conceitos:
Isso significa que, no código C ++ correto, a maioria dos objetos não será construída new
e, em vez disso, será declarada na pilha. E para aqueles construídos usando new
, tudo terá um escopo de alguma forma (por exemplo, anexado a um ponteiro inteligente).
Como desenvolvedor, isso é muito poderoso, pois você não precisará se preocupar com o manuseio manual de recursos (como em C ou em alguns objetos em Java que fazem uso intensivo try
/ finally
para esse caso) ...
"objetos com escopo ... serão destruídos ... não importa a saída", isso não é inteiramente verdade. existem maneiras de enganar RAII. qualquer sabor de terminate () ignorará a limpeza. exit (EXIT_SUCCESS) é um oxímoro a esse respeito.
wilhelmtell está certo sobre isso: existem maneiras excepcionais de enganar a RAII, todas levando à parada abrupta do processo.
Essas são maneiras excepcionais porque o código C ++ não está repleto de terminação, saída etc., ou, no caso de exceções, queremos uma exceção não tratada para travar o processo e o core despejar sua imagem de memória como está, e não após a limpeza.
Mas ainda precisamos saber sobre esses casos, porque, embora raramente aconteçam, ainda podem acontecer.
(quem chama terminate
ou exit
no código C ++ casual? ... Lembro-me de ter que lidar com esse problema ao jogar com o GLUT : essa biblioteca é muito orientada para C, chegando ao ponto de projetá-la ativamente para tornar as coisas difíceis para desenvolvedores de C ++, como não se importarem sobre empilhar dados alocados ou tomar decisões "interessantes" sobre nunca retornar do loop principal ... Não vou comentar sobre isso) .
Você vai querer olhar para ponteiros inteligentes, como os ponteiros inteligentes do boost .
Ao invés de
int main()
{
Object* obj = new Object();
//...
delete obj;
}
O boost :: shared_ptr será excluído automaticamente quando a contagem de referência for zero:
int main()
{
boost::shared_ptr<Object> obj(new Object());
//...
// destructor destroys when reference count is zero
}
Observe minha última observação, "quando a contagem de referência for zero, que é a parte mais legal. Portanto, se você tiver vários usuários do seu objeto, não precisará acompanhar se o objeto ainda está em uso. Depois que ninguém se refere ao seu ponteiro compartilhado, ele é destruído.
Esta não é uma panacéia, no entanto. Embora você possa acessar o ponteiro base, não desejaria passá-lo para uma API de terceiros, a menos que estivesse confiante com o que estava fazendo. Muitas vezes, as coisas de "postagem" em algum outro encadeamento para que o trabalho seja feito APÓS o término da criação. Isso é comum com PostThreadMessage no Win32:
void foo()
{
boost::shared_ptr<Object> obj(new Object());
// Simplified here
PostThreadMessage(...., (LPARAM)ob.get());
// Destructor destroys! pointer sent to PostThreadMessage is invalid! Zohnoes!
}
Como sempre, use seu boné de pensamento com qualquer ferramenta ...
A maioria dos vazamentos de memória resulta da falta de clareza sobre a propriedade e a duração do objeto.
A primeira coisa a fazer é alocar na pilha sempre que puder. Isso lida com a maioria dos casos em que você precisa alocar um único objeto para alguma finalidade.
Se você precisar 'novo' um objeto, na maioria das vezes ele terá um único proprietário óbvio pelo resto de sua vida útil. Para esta situação, costumo usar vários modelos de coleções projetados para 'possuir' objetos armazenados neles pelo ponteiro. Eles são implementados com o vetor STL e os contêineres de mapas, mas têm algumas diferenças:
Meu problema com o STL é que ele é tão focado nos objetos Value, enquanto na maioria dos aplicativos os objetos são entidades únicas que não possuem semântica de cópia significativa necessária para uso nesses contêineres.
Bah, vocês jovens e seus novos colecionadores de lixo ...
Regras muito fortes sobre "propriedade" - qual objeto ou parte do software tem o direito de excluir o objeto. Comentários claros e nomes de variáveis sábios para tornar óbvio se um ponteiro "possui" ou é "apenas olhe, não toque". Para ajudar a decidir quem possui o quê, siga o máximo possível o padrão "sanduíche" em todas as sub-rotinas ou métodos.
create a thing
use that thing
destroy that thing
Às vezes é necessário criar e destruir em lugares amplamente diferentes; Eu acho difícil evitar isso.
Em qualquer programa que exija estruturas de dados complexas, eu crio uma árvore estrita e clara de objetos contendo outros objetos - usando ponteiros "proprietários". Essa árvore modela a hierarquia básica dos conceitos de domínio de aplicativo. Exemplo: uma cena 3D possui objetos, luzes, texturas. No final da renderização, quando o programa é encerrado, existe uma maneira clara de destruir tudo.
Muitos outros ponteiros são definidos conforme necessário sempre que uma entidade precisa acessar outra, para varrer arays ou o que for; estes são os "apenas olhando". Para o exemplo da cena 3D - um objeto usa uma textura, mas não possui; outros objetos podem usar a mesma textura. A destruição de um objeto não invoca a destruição de nenhuma textura.
Sim, é demorado, mas é o que eu faço. Eu raramente tenho vazamentos de memória ou outros problemas. Mas então trabalho na arena limitada de software científico, de aquisição de dados e gráficos de alto desempenho. Não costumo negociar transações como bancos e comércio eletrônico, GUIs orientadas a eventos ou caos assíncrono em rede. Talvez os novos caminhos tenham uma vantagem por lá!
Ótima pergunta!
se você estiver usando c ++ e estiver desenvolvendo um aplicativo alto de CPU e memória em tempo real (como jogos), precisará criar seu próprio Gerenciador de Memória.
Acho que o melhor que você pode fazer é mesclar algumas obras interessantes de vários autores, posso dar uma dica:
Alocador de tamanho fixo é muito discutido, em todos os lugares da rede
A alocação de objetos pequenos foi introduzida por Alexandrescu em 2001 em seu livro perfeito "Design moderno em c ++"
Um grande avanço (com o código-fonte distribuído) pode ser encontrado em um incrível artigo na Game Programming Gem 7 (2008) chamado "High Performance Heap alocador", escrito por Dimitar Lazarov
Uma ótima lista de recursos pode ser encontrada em neste artigo
Não comece a escrever um alocador inútil inútil por si mesmo ... DOCUMENTO-SE primeiro.
Uma técnica que se tornou popular no gerenciamento de memória em C ++ é o RAII . Basicamente, você usa construtores / destruidores para lidar com a alocação de recursos. É claro que existem outros detalhes desagradáveis em C ++ devido à segurança de exceções, mas a idéia básica é bastante simples.
A questão geralmente se resume a uma questão de propriedade. Eu recomendo a leitura das séries Effective C ++ de Scott Meyers e Modern C ++ Design de Andrei Alexandrescu.
Já existe muito sobre como não vazar, mas se você precisar de uma ferramenta para ajudá-lo a rastrear vazamentos, dê uma olhada:
Compartilhe e conheça as regras de propriedade da memória em seu projeto. O uso das regras COM garante a melhor consistência (os parâmetros [de entrada] pertencem ao chamador, o destinatário deve copiar; os parâmetros [out] pertencem ao chamador, o destinatário deve fazer uma cópia se manter uma referência; etc.)
O valgrind é uma boa ferramenta para verificar também os vazamentos de memória dos programas em tempo de execução.
Está disponível na maioria dos tipos de Linux (incluindo Android) e no Darwin.
Se você costuma escrever testes de unidade para seus programas, deve adquirir o hábito de executar sistematicamente o valgrind nos testes. Potencialmente, evitará muitos vazamentos de memória em um estágio inicial. Também é geralmente mais fácil identificá-los em testes simples que em um software completo.
Obviamente, este conselho permanece válido para qualquer outra ferramenta de verificação de memória.
Se você não pode / não usa um ponteiro inteligente para algo (embora isso deva ser uma grande bandeira vermelha), digite seu código com:
allocate
if allocation succeeded:
{ //scope)
deallocate()
}
Isso é óbvio, mas certifique-se de digitá-lo antes de digitar qualquer código no escopo
Uma fonte frequente desses erros é quando você tem um método que aceita uma referência ou ponteiro para um objeto, mas deixa a propriedade pouco clara. Convenções de estilo e comentários podem tornar isso menos provável.
Seja o caso especial em que a função assume a propriedade do objeto. Em todas as situações em que isso acontece, certifique-se de escrever um comentário ao lado da função no arquivo de cabeçalho indicando isso. Você deve se esforçar para garantir que, na maioria dos casos, o módulo ou a classe que aloca um objeto também seja responsável por desalocá-lo.
O uso de const pode ajudar bastante em alguns casos. Se uma função não modificar um objeto e não armazenar uma referência a ele que persista após o retorno, aceite uma referência const. Ao ler o código do chamador, será óbvio que sua função não aceitou a propriedade do objeto. Você poderia ter a mesma função aceitar um ponteiro não-const e o chamador pode ou não ter assumido que o chamado aceitou a propriedade, mas com uma referência const, não há dúvida.
Não use referências não-const nas listas de argumentos. Não é muito claro ao ler o código do chamador que o receptor pode ter mantido uma referência ao parâmetro.
Não concordo com os comentários que recomendam indicadores de referência contados. Isso geralmente funciona bem, mas quando você tem um bug e não funciona, especialmente se o seu destruidor faz algo não trivial, como em um programa multithread. Definitivamente, tente ajustar seu design para não precisar de contagem de referência, se não for muito difícil.
Dicas em ordem de importância:
Dica # 1 Lembre-se sempre de declarar seus destruidores "virtuais".
Dica 2: use RAII
Dica # 3 Use os smartpointers da boost
Dica # 4 Não escreva seus próprios Smartpointers de buggy, use boost (em um projeto em que estou no momento não posso usar o boost, e sofri a necessidade de depurar meus próprios ponteiros inteligentes, eu definitivamente não aceitaria a mesma rota novamente, mas novamente agora não posso adicionar impulso às nossas dependências)
Dica # 5 Se algum trabalho ocasional / crítico para o desempenho (como em jogos com milhares de objetos) funcionar, observe o contêiner de ponteiro de impulso de Thorsten Ottosen
Dica # 6 Encontre um cabeçalho de detecção de vazamento para sua plataforma de escolha, como o cabeçalho "vld" da Visual Leak Detection
Se puder, use boost shared_ptr e C ++ auto_ptr padrão. Eles transmitem semântica de propriedade.
Quando você retorna um auto_ptr, está dizendo ao chamador que está dando a ele propriedade da memória.
Quando você retorna um shared_ptr, está dizendo ao chamador que você tem uma referência a ele e ele faz parte da propriedade, mas não é apenas responsabilidade deles.
Essas semânticas também se aplicam aos parâmetros. Se o chamador passar um auto_ptr, ele estará lhe dando propriedade.
Outros mencionaram maneiras de evitar vazamentos de memória em primeiro lugar (como ponteiros inteligentes). Mas uma ferramenta de análise de perfil e de memória geralmente é a única maneira de rastrear problemas de memória depois que você os tiver.
O memcheck do Valgrind é excelente e gratuito.
Somente para MSVC, adicione o seguinte na parte superior de cada arquivo .cpp:
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
Em seguida, ao depurar com o VS2003 ou superior, você será informado sobre quaisquer vazamentos quando o programa sair (ele rastreia novo / exclui). É básico, mas me ajudou no passado.
valgrind (disponível apenas para plataformas * nix) é um verificador de memória muito bom
Se você deseja gerenciar sua memória manualmente, você tem dois casos:
Se você precisar violar alguma dessas regras, documente-a.
É tudo sobre propriedade de ponteiro.
Você pode interceptar as funções de alocação de memória e verificar se há algumas zonas de memória não liberadas na saída do programa (embora não seja adequado para todos os aplicativos).
Isso também pode ser feito em tempo de compilação, substituindo os operadores new e delete e outras funções de alocação de memória.
Por exemplo, verifique neste site [Depurando alocação de memória em C ++] Nota: Há um truque para o operador de exclusão também algo como isto:
#define DEBUG_DELETE PrepareDelete(__LINE__,__FILE__); delete
#define delete DEBUG_DELETE
Você pode armazenar em algumas variáveis o nome do arquivo e quando o operador de exclusão sobrecarregado saberá de onde foi chamado. Dessa forma, você pode rastrear cada exclusão e malloc do seu programa. No final da sequência de verificação de memória, você deve poder relatar qual bloco de memória alocado não foi 'excluído', identificando-o pelo nome do arquivo e pelo número da linha, que é o que você deseja.
Você também pode tentar algo como o BoundsChecker no Visual Studio, que é bastante interessante e fácil de usar.
Envolvemos todas as nossas funções de alocação com uma camada que acrescenta uma breve sequência na frente e uma bandeira sentinela no final. Assim, por exemplo, você teria uma chamada para "myalloc (pszSomeString, iSize, iAlignment); ou novo (" description ", iSize) MyObject (); que aloca internamente o tamanho especificado, mais espaço suficiente para o cabeçalho e o sentinela. , não esqueça de comentar isso para compilações sem depuração! É preciso um pouco mais de memória para fazer isso, mas os benefícios superam os custos.
Isso tem três benefícios - primeiro, permite rastrear fácil e rapidamente o código que está vazando, fazendo pesquisas rápidas por códigos alocados em determinadas 'zonas', mas não limpos quando essas zonas deveriam ter sido liberadas. Também pode ser útil detectar quando um limite foi substituído, verificando se todos os sentinelas estão intactos. Isso nos salvou várias vezes ao tentar encontrar essas falhas ou erros de matriz bem ocultos. O terceiro benefício é rastrear o uso da memória para ver quem são os grandes jogadores - um agrupamento de determinadas descrições em um MemDump informa quando o 'som' ocupa muito mais espaço do que o esperado, por exemplo.
C ++ é projetado RAII em mente. Não há realmente nenhuma maneira melhor de gerenciar memória em C ++, eu acho. Mas tome cuidado para não alocar pedaços muito grandes (como objetos de buffer) no escopo local. Isso pode causar estouros de pilha e, se houver uma falha nos limites de verificação ao usar esse pedaço, você poderá sobrescrever outras variáveis ou endereços de retorno, o que leva a todos os tipos de falhas de segurança.
Um dos únicos exemplos de alocação e destruição em locais diferentes é a criação de encadeamentos (o parâmetro que você passa). Mas mesmo neste caso é fácil. Aqui está a função / método que cria um thread:
struct myparams {
int x;
std::vector<double> z;
}
std::auto_ptr<myparams> param(new myparams(x, ...));
// Release the ownership in case thread creation is successfull
if (0 == pthread_create(&th, NULL, th_func, param.get()) param.release();
...
Aqui, ao invés, a função thread
extern "C" void* th_func(void* p) {
try {
std::auto_ptr<myparams> param((myparams*)p);
...
} catch(...) {
}
return 0;
}
Muito fácil, não é? Caso a criação do encadeamento falhe, o recurso será liberado (excluído) pelo auto_ptr, caso contrário, a propriedade será passada para o encadeamento. E se o encadeamento for tão rápido que, após a criação, libere o recurso antes do
param.release();
é chamado na principal função / método? Nada! Porque 'informaremos' o auto_ptr para ignorar a desalocação. O gerenciamento de memória C ++ é fácil, não é? Felicidades,
Ema!
Gerencie a memória da mesma maneira que gerencia outros recursos (identificadores, arquivos, conexões db, soquetes ...). A GC também não ajudaria você.
Exatamente um retorno de qualquer função. Dessa forma, você pode fazer a desalocação lá e nunca perder.
Caso contrário, é muito fácil cometer um erro:
new a()
if (Bad()) {delete a; return;}
new b()
if (Bad()) {delete a; delete b; return;}
... // etc.