Pergunta

Quais são algumas dicas gerais para garantir que não haja vazamento de memória em programas C++?Como descubro quem deve liberar memória alocada dinamicamente?

Foi útil?

Solução

Em vez de gerenciar a memória manualmente, tente usar ponteiros inteligentes quando aplicável.
Dê uma olhada no Aumentar biblioteca, TR1, e ponteiros inteligentes.
Além disso, os ponteiros inteligentes agora fazem parte do padrão C++ chamado C++11.

Outras dicas

Endosso totalmente todos os conselhos sobre RAII e ponteiros inteligentes, mas também gostaria de adicionar uma dica de nível um pouco superior:a memória mais fácil de gerenciar é aquela que você nunca alocou.Ao contrário de linguagens como C# e Java, onde praticamente tudo é uma referência, em C++ você deve colocar objetos na pilha sempre que puder.Como tenho visto várias pessoas (incluindo o Dr. Stroustrup) apontarem, a principal razão pela qual a coleta de lixo nunca foi popular em C++ é que 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 simplesmente escrever

Object x;

Usar RAII

  • Esqueça a coleta de lixo (Use RAII).Observe que até mesmo o Garbage Collector também pode vazar (se você esquecer de "anular" algumas referências em Java/C#), e que o Garbage Collector não o ajudará a descartar recursos (se você tiver um objeto que adquiriu um identificador para um arquivo, o arquivo não será liberado automaticamente quando o objeto sair do escopo se você não fizer isso manualmente em Java ou usar o padrão "dispose" em C#).
  • Esqueça a regra “um retorno por função”.Este é um bom conselho em C para evitar vazamentos, mas está desatualizado em C++ devido ao uso de exceções (use RAII).
  • E enquanto o "Padrão Sanduíche" é um bom conselho C, é está desatualizado em C++ devido ao uso de exceções (use RAII).

Este post parece repetitivo, mas em C++, o padrão mais básico a saber é RAII.

Aprenda a usar ponteiros inteligentes, tanto do boost, TR1 ou até mesmo do humilde (mas muitas vezes eficiente o suficiente) auto_ptr (mas você deve conhecer suas limitações).

RAII é a base da segurança de exceções e do descarte de recursos em C++, e nenhum outro padrão (sanduíche, etc.) fornecerá ambos (e na maioria das vezes, não fornecerá nenhum).

Veja abaixo uma comparação entre 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.
}

Sobre RAII

Para resumir (após o comentário de Salmo Ogro33), o RAII baseia-se em três conceitos:

  • Depois que o objeto é construído, ele simplesmente funciona! Adquira recursos no construtor.
  • A destruição de objetos é suficiente! Faça recursos gratuitos no destruidor.
  • É tudo uma questão de escopos! Objetos com escopo (veja o exemplo doRAIIStatic acima) serão construídos em sua declaração e serão destruídos no momento em que a execução sair do escopo, não importando como a saída (retorno, quebra, exceção, etc.).

Isso significa que no código C++ correto, a maioria dos objetos não será construída com new, e será declarado na pilha.E para aqueles construídos usando new, tudo será de alguma forma escopo (por exemplo.anexado a um ponteiro inteligente).

Como desenvolvedor, isso é realmente muito poderoso, pois você não precisará se preocupar com o manuseio manual de recursos (como é feito em C, ou com alguns objetos em Java que fazem uso intensivo de try/finally para esse caso)...

Editar (12/02/2012)

"objetos com escopo definido ...será destruído...não importa a saída" isso não é inteiramente verdade.existem maneiras de enganar o RAII.qualquer tipo de terminação() ignorará a limpeza.exit(EXIT_SUCCESS) é um oxímoro nesse sentido.

Guilherme Tell

Guilherme Tell está certo sobre isso:Há excepcional maneiras de enganar o RAII, todas levando à parada abrupta do processo.

Esses são excepcional maneiras porque o código C++ não está repleto de terminação, saída, etc., ou no caso de exceções, queremos um exceção não tratada para travar o processo e fazer o core dump de sua imagem de memória como está, e não após a limpeza.

Mas ainda devemos saber sobre esses casos porque, embora raramente aconteçam, ainda podem acontecer.

(quem liga terminate ou exit em código C++ casual?Lembro-me de ter que lidar com esse problema ao brincar com EXCESSO:Esta biblioteca é muito orientada para C, chegando ao ponto de projetá-la ativamente para dificultar as coisas para os desenvolvedores de C++, como não se importar com empilhar dados alocados, ou tomar decisões "interessantes" sobre nunca retornando do loop principal...não vou comentar sobre isso).

Você vai querer olhar para indicadores inteligentes, como ponteiros inteligentes do boost.

Em vez de

int main()
{ 
    Object* obj = new Object();
    //...
    delete obj;
}

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 é zero, que é a parte mais legal.Portanto, se você tiver vários usuários do seu objeto, não precisará controlar se o objeto ainda está em uso.Quando ninguém se referir ao seu ponteiro compartilhado, ele será destruído.

Isto não é uma panaceia, no entanto.Embora você possa acessar o ponteiro base, você não gostaria de passá-lo para uma API de terceiros, a menos que estivesse confiante com o que ele estava fazendo.Muitas vezes, você "posta" coisas em algum outro tópico para que o trabalho seja feito APÓS a conclusão da criação do escopo.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 raciocínio com qualquer ferramenta...

Leia em RAII e certifique-se de entendê-lo.

A maioria dos vazamentos de memória é o resultado de não haver clareza sobre a propriedade e o tempo de vida do objeto.

A primeira coisa a fazer é alocar na Stack sempre que puder.Isso trata da 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.Para esta situação, costumo usar vários modelos de coleções projetados para 'possuir' objetos armazenados nelas por ponteiro.Eles são implementados com os contêineres de vetor e mapa STL, mas têm algumas diferenças:

  • Essas coleções não podem ser copiadas ou atribuídas.(uma vez que contenham objetos.)
  • Ponteiros para objetos são inseridos neles.
  • Quando a coleção é excluída, o destruidor é chamado primeiro em todos os objetos da coleção.(Eu tenho outra versão onde afirma se está destruído e não vazio.)
  • Como eles armazenam ponteiros, você também pode armazenar objetos herdados nesses contêineres.

Minha vantagem com o STL é que ele é tão focado em objetos Value, enquanto na maioria dos aplicativos os objetos são entidades exclusivas que não possuem semântica de cópia significativa necessária para uso nesses contêineres.

Bah, vocês, crianças e seus novos coletores de lixo...

Regras muito rígidas sobre “propriedade” – qual objeto ou parte do software tem o direito de excluir o objeto.Comentários claros e nomes de variáveis ​​inteligentes para tornar óbvio se um ponteiro "possui" ou é "apenas olhe, não toque".Para ajudar a decidir quem possui o quê, siga tanto quanto possível o padrão "sanduíche" dentro de cada sub-rotina ou método.

create a thing
use that thing
destroy that thing

Às vezes é necessário criar e destruir em lugares muito diferentes;acho difícil evitar isso.

Em qualquer programa que exija estruturas de dados complexas, eu crio uma árvore bem definida de objetos contendo outros objetos - usando ponteiros de "proprietário".Esta árvore modela a hierarquia básica dos conceitos de domínio de aplicação.Exemplo uma cena 3D possui objetos, luzes, texturas.No final da renderização, quando o programa é encerrado, há uma maneira clara de destruir tudo.

Muitos outros ponteiros são definidos conforme necessário sempre que uma entidade precisa acessar outra, para varrer arrays ou qualquer outra coisa;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 não invocar a destruição de quaisquer texturas.

Sim, é demorado, mas é o que eu faço.Raramente tenho vazamentos de memória ou outros problemas.Mas então eu trabalho na área limitada de software científico, de aquisição de dados e gráfico de alto desempenho.Não costumo lidar com transações bancárias e de comércio eletrônico, GUIs orientadas a eventos ou caos assíncrono em rede.Talvez as novas formas tenham uma vantagem nisso!

Ótima pergunta!

se você estiver usando c++ e desenvolvendo aplicativos boud de CPU e memória em tempo real (como jogos), precisará escrever seu próprio gerenciador de memória.

Acho que o melhor que você pode fazer é mesclar alguns trabalhos interessantes de vários autores, posso te dar uma dica:

  • O alocador de tamanho fixo é muito discutido em toda a rede

  • A alocação de objetos pequenos foi introduzida por Alexandrescu em 2001 em seu livro perfeito "Modern c++ design"

  • Um grande avanço (com código-fonte distribuído) pode ser encontrado em um artigo incrível no Game Programming Gem 7 (2008) chamado "High Performance Heap allocator" escrito por Dimitar Lazarov

  • Uma grande lista de recursos pode ser encontrada em esse artigo

Não comece a escrever um alocador inútil e noob sozinho ...DOCUMENTE-SE primeiro.

Uma técnica que se tornou popular com gerenciamento de memória em C++ é RAII.Basicamente você usa construtores/destruidores para lidar com a alocação de recursos.É claro que existem alguns outros detalhes desagradáveis ​​em C++ devido à segurança de exceções, mas a ideia básica é bem simples.

A questão geralmente se resume à propriedade.Eu recomendo fortemente a leitura da série Effective C++ de Scott Meyers e Modern C++ Design de Andrei Alexandrescu.

Já há muito sobre como não vazar, mas se você precisar de uma ferramenta para ajudá-lo a rastrear vazamentos, dê uma olhada em:

Ponteiros inteligentes do usuário em todos os lugares que você puder!Classes inteiras de vazamentos de memória simplesmente desaparecem.

Compartilhe e conheça as regras de propriedade de memória em seu projeto.O uso das regras COM proporciona a melhor consistência ([in] os parâmetros são de propriedade do chamador, o chamador deve copiar;[out] os parâmetros são de propriedade do chamador, o chamador deve fazer uma cópia se mantiver uma referência;etc.)

valgrind também é uma boa ferramenta para verificar vazamentos de memória em seus programas em tempo de execução.

Ele está disponível na maioria das versões do Linux (incluindo Android) e no Darwin.

Se você costuma escrever testes unitários para seus programas, você deve adquirir o hábito de executar sistematicamente o valgrind nos testes.Isso potencialmente evitará muitos vazamentos de memória em um estágio inicial.Também costuma ser mais fácil identificá-los em testes simples do que em um software completo.

É claro que este conselho permanece válido para qualquer outra ferramenta de verificação de memória.

Além disso, não use memória alocada manualmente se houver uma classe de biblioteca std (por exemplovetor).Certifique-se de violar essa regra de que você possui um destruidor virtual.

Se você não pode/não quer usar um ponteiro inteligente para alguma coisa (embora isso deva ser um grande sinal de alerta), digite seu código com:

allocate
if allocation succeeded:
{ //scope)
     deallocate()
}

Isso é óbvio, mas certifique-se de digitá-lo antes você digita qualquer código no escopo

Uma fonte frequente desses bugs é quando você tem um método que aceita uma referência ou ponteiro para um objeto, mas deixa a propriedade incerta.As convenções de estilo e comentários podem tornar isso menos provável.

Seja o caso especial o caso em que a função toma posse do objeto.Em todas as situações em que isso acontecer, certifique-se de escrever um comentário próximo à 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 classe que aloca um objeto também seja responsável por desalocá-lo.

Usar const pode ajudar muito 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 seu retorno, aceite uma referência const.Ao ler o código do chamador, ficará óbvio que sua função não aceitou a propriedade do objeto.Você poderia ter feito com que a mesma função aceitasse um ponteiro não const, e o chamador pode ou não ter assumido que o receptor aceitou a propriedade, mas com uma referência const não há dúvidas.

Não use referências não const em listas de argumentos.Não está 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 ponteiros contados de referência.Isso geralmente funciona bem, mas quando você tem um bug e não funciona, especialmente se o seu destruidor fizer 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 nº 2: use RAII

-Dica nº 3 Use os smartpointers do boost

-Dica nº 4 Não escreva seus próprios Smartpointers com bugs, use boost (em um projeto em que estou agora, não posso usar boost e sofri tendo que depurar meus próprios smart pointers, eu definitivamente não aceitaria a mesma rota novamente, mas agora não consigo adicionar impulso às nossas dependências)

-Dica # 5 Se for algum trabalho casual/não crítico de desempenho (como em jogos com milhares de objetos), observe o contêiner de ponteiro de impulso de Thorsten Ottosen

-Dica nº 6 Encontre um cabeçalho de detecção de vazamento para sua plataforma preferida, como o cabeçalho "vld" do Visual Leak Detection

Se puder, use boost shared_ptr e C++ auto_ptr padrão.Eles transmitem a semântica de propriedade.

Ao retornar um auto_ptr, você está informando ao chamador que está concedendo a ele a propriedade da memória.

Ao retornar um shared_ptr, você está dizendo ao chamador que tem uma referência a ele e que ele assume parte da propriedade, mas não é responsabilidade exclusiva dele.

Essa semântica também se aplica aos parâmetros.Se o chamador passar um auto_ptr para você, ele estará lhe concedendo a propriedade.

Outros mencionaram maneiras de evitar vazamentos de memória (como ponteiros inteligentes).Mas uma ferramenta de criação de perfil e análise de memória costuma ser a única maneira de rastrear problemas de memória, uma vez que você os tenha.

Verificação de memória Valgrind é um excelente gratuito.

Somente para MSVC, adicione o seguinte ao topo de cada arquivo .cpp:

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

Então, ao depurar com o VS2003 ou superior, você será informado sobre quaisquer vazamentos quando o programa for encerrado (ele rastreia novas/exclusões).É básico, mas me ajudou no passado.

valgrind (disponível apenas para plataformas *nix) é um verificador de memória muito bom

Se você pretende gerenciar sua memória manualmente, você tem dois casos:

  1. Eu criei o objeto (talvez indiretamente, chamando uma função que aloca um novo objeto), eu o uso (ou uma função que eu chamo o usa) e depois o libero.
  2. Alguém me deu a referência, então não devo divulgá-la.

Se você precisar quebrar alguma dessas regras, documente.

É tudo uma questão de propriedade do ponteiro.

  • Tente evitar alocar objetos dinamicamente.Contanto que as classes tenham construtores e destruidores apropriados, use uma variável do tipo de classe, não um ponteiro para ela, e você evitará alocação e desalocação dinâmicas porque o compilador fará isso por você.
    Na verdade, esse também é o mecanismo usado pelos "ponteiros inteligentes" e referido como RAII por alguns dos outros escritores ;-).
  • Ao passar objetos para outras funções, prefira parâmetros de referência a ponteiros.Isso evita alguns possíveis erros.
  • Declare parâmetros const, sempre que possível, especialmente ponteiros para objetos.Dessa forma, os objetos não podem ser liberados "acidentalmente" (exceto se você descartar o const ;-))).
  • Minimize o número de locais no programa onde você faz a alocação e desalocação de memória.E.g.se você alocar ou liberar o mesmo tipo várias vezes, escreva uma função para ele (ou um método de fábrica ;-)).
    Dessa forma, você pode criar saída de depuração (quais endereços são alocados e desalocados, ...) facilmente, se necessário.
  • Use uma função de fábrica para alocar objetos de diversas classes relacionadas a partir de uma única função.
  • Se suas classes tiverem uma classe base comum com um destruidor virtual, você poderá liberar todas elas usando a mesma função (ou método estático).
  • Verifique seu programa com ferramentas como purificar (infelizmente muitos $/€/...).

Você pode interceptar as funções de alocação de memória e ver se há algumas zonas de memória não liberadas ao sair do programa (embora não seja adequado para todos as aplicações).

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 isto site Alocação de memória de depuração em C ++] Nota:Existe um truque para o operador delete também mais ou menos assim:

#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 delete sobrecarregado saberá de qual local ele foi chamado.Desta forma você pode ter o rastreamento de cada delete e malloc do seu programa.No final da sequência de verificação de memória, você poderá relatar qual bloco de memória alocado não foi 'excluído', identificando-o pelo nome do arquivo e número da linha, o que eu acho que você deseja.

Você também pode tentar algo como Verificador de limites no Visual Studio, que é bastante interessante e fácil de usar.

Envolvemos todas as nossas funções de alocação com uma camada que anexa uma breve string na frente e um sinalizador sentinela no final.Então, por exemplo, você teria uma chamada para "myalloc( pszSomeString, iSize, iAlignment );ou new( "descrição", iSize ) MyObject();que aloca internamente o tamanho especificado mais espaço suficiente para seu cabeçalho e sentinela.Claro, não se 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 em muito os custos.

Isso tem três benefícios - primeiro, permite rastrear fácil e rapidamente qual código está vazando, fazendo pesquisas rápidas por código alocado em certas 'zonas', mas não limpo quando essas zonas deveriam ter sido liberadas.Também pode ser útil detectar quando um limite foi sobrescrito, verificando se todas as sentinelas estão intactas.Isso nos salvou inúmeras vezes ao tentar encontrar falhas bem escondidas ou erros de matriz.O terceiro benefício é rastrear o uso da memória para ver quem são os grandes jogadores - um agrupamento de certas descrições em um MemDump informa quando o 'som' está ocupando muito mais espaço do que você esperava, por exemplo.

C++ foi projetado em mente para RAII.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 buffer) no escopo local.Isso pode causar estouros de pilha e, se houver uma falha na verificação de limites 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 threads (o parâmetro que você passa).Mas mesmo neste caso é fácil.Aqui está a função/método de criação de 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, em vez disso, 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 thread falhe, o recurso será liberado (excluído) pelo auto_ptr, caso contrário a propriedade será passada para o thread.E se o thread for tão rápido que após a criação ele libera o recurso antes do

param.release();

é chamado na função/método principal?Nada!Porque iremos 'dizer' ao auto_ptr para ignorar a desalocação.O gerenciamento de memória C++ é fácil, não é?Saúde,

Ema!

Gerencie a memória da mesma forma que gerencia outros recursos (identificadores, arquivos, conexões de banco de dados, soquetes...).GC também não ajudaria você com eles.

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.
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top