it-swarm-pt.tech

Diretrizes gerais para evitar vazamentos de memória em C ++

Quais são algumas dicas gerais para garantir que não vaze memória em programas C++? Como faço para descobrir quem deve liberar memória que foi alocada dinamicamente?

124
dulipishi

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

37
Andri Möll

Eu endosso completamente todos os conselhos sobre RAII e ponteiros inteligentes, mas também gostaria de adicionar uma dica de nível ligeiramente superior: a memória mais fácil de gerenciar é a memória 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 eu vi várias pessoas (incluindo o Dr. Stroustrup) apontam, 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 até mesmo

shared_ptr<Object> x(new Object);

quando você pode apenas escrever

Object x;
194
Ross Smith

Use RAII

  • Esqueça a coleta de lixo (use RAII). Note que mesmo o Garbage Collector pode vazar também (se você esquecer de "null" algumas referências em Java/C #), e que Garbage Collector não irá ajudá-lo a descartar recursos (se você tiver um objeto que tenha adquirido um handle para em 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 C para evitar vazamentos, mas está desatualizado em C++ devido ao uso de exceções (use RAII).
  • E enquanto o "Padrão Sandwich" é um bom conselho C, ele está desatualizado em C++ devido ao uso de exceções (use RAII).

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

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

O RAII é a base da segurança de exceções e do descarte de recursos em C++, e nenhum outro padrão (sanduíche, etc.) lhe dará ambos (e na maioria das vezes, não lhe dará 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.
}

Sobre RAII

Para resumir (após o comentário de Ogro Salmo33), RAII conta com três conceitos:

  • Uma vez que o objeto é construído, simplesmente funciona! Adquira recursos no construtor.
  • A destruição de objetos é suficiente! Libere recursos no destruidor.
  • É tudo sobre escopos! Objetos com escopo definido (veja o exemplo do 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á declarada 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 é muito poderoso, já que você não precisa se preocupar com o manuseio manual de recursos (como em C, ou para alguns objetos em Java que faz uso intensivo de try/finally para esse caso ) ...

Editar (2012-02-12)

"objetos com escopo ... serão destruídos ... não importa a saída" isso não é inteiramente verdade. existem maneiras de enganar o RAII. qualquer sabor de finalizar () irá ignorar a limpeza. exit (EXIT_SUCCESS) é um oxímoro a esse respeito.

- wilhelmtell

wilhelmtell está certo sobre isso: Existem excepcionais maneiras de enganar RAII, todas levando ao processo de parada abrupta.

Essas são maneiras excepcionais porque o código C++ não está cheio de terminação, saída, etc., ou no caso de exceções, queremos um 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 devemos saber sobre esses casos porque, embora eles raramente aconteçam, eles ainda podem acontecer.

(quem chama terminate ou exit em código C++ casual? ... Lembro-me de ter que lidar com esse problema quando se joga com GLUT : Esta biblioteca é muito orientada para C, indo tão longe quanto projetando ativamente para tornar as coisas difíceis para desenvolvedores C++ como não se importar com empilhar dados alocados , ou ter decisões "interessantes" sobre nunca retornando de seu loop principal ... eu não vou comentar sobre isso).

100
paercebal

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

Ao invés 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 a minha última nota, "quando a contagem de referência é zero, que é a parte mais legal. Então, se você tiver vários usuários do seu objeto, você não terá que controlar se o objeto ainda está em uso. ponteiro compartilhado, ele é destruído.

Isso não é uma panacéia, no entanto. Embora seja possível acessar o ponteiro base, você não desejaria passá-lo para uma API de terceiros, a menos que estivesse confiante com o que estava fazendo. Muitas vezes, seu material de "postagem" para algum outro segmento para trabalho a ser feito após o escopo de criação é concluído. 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 sua tampa de pensamento com qualquer ferramenta ...

25
Doug T.

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

12
Hank

A maioria dos vazamentos de memória é o resultado de não ser claro sobre a propriedade e a vida útil 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 algum propósito.

Se você precisa 'novo' um objeto, na maioria das vezes ele terá um único proprietário óbvio pelo resto de sua vida útil. Para esta situação, eu costumo usar um monte de modelos de coleções que são projetados para 'possuir' objetos armazenados neles por ponteiro. Eles são implementados com o vetor STL e os contêineres de mapa, mas possuem algumas diferenças:

  • Essas coleções não podem ser copiadas ou atribuídas a. (uma vez que eles 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 destruída e não vazia.)
  • Como eles armazenam ponteiros, você também pode armazenar objetos herdados nesses contêineres.

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

11
Jeroen Dirks

Bah, seus jovens e seus novos coletores de lixo ...

Regras muito fortes sobre "propriedade" - qual objeto ou parte do software tem o direito de excluir o objeto. Limpar comentários e nomes de variáveis ​​inteligentes para torná-lo óbvio se um ponteiro "possui" ou é "apenas olhe, não toque". Para ajudar a decidir quem é o proprietário, siga o máximo possível o padrão "sanduíche" em 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; Eu acho difícil evitar isso.

Em qualquer programa que exija estruturas de dados complexas, eu crio uma árvore clara de objetos contendo outros objetos - usando ponteiros "owner". 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 termina, existe uma maneira clara de destruir tudo.

Muitos outros ponteiros são definidos conforme necessário sempre que uma entidade precisa acessar outra, para fazer a varredura sobre os arays ou o que quer que seja; estes são os "apenas olhando". Para o exemplo de cena 3D - um objeto usa uma textura, mas não possui; outros objetos podem usar essa mesma textura. A destruição de um objeto faz não invocar a destruição de qualquer textura.

Sim, é demorado, mas é o que eu faço. Eu raramente tenho vazamentos de memória ou outros problemas. Mas então eu trabalho na área limitada de alto desempenho científico, aquisição de dados e software gráfico. Não negocio com frequência transações como em transações bancárias e ecommerce, GUIs baseadas em eventos ou caos assíncrono em rede. Talvez os novos caminhos tenham uma vantagem lá!

10
DarenW

Ótima pergunta!

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

Eu acho que o melhor que você pode fazer é mesclar algumas obras interessantes de vários autores, eu posso te dar uma dica:

  • Alocador de tamanho fixo é fortemente discutido, em toda parte na rede

  • Small Object Allocation foi introduzido 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 em Game Programming Gem 7 (2008) chamado "High Performance Heap allocator" escrito por Dimitar Lazarov

  • Uma ótima lista de recursos pode ser encontrada em this article

Não comece a escrever um alocador imprestável por você mesmo ... DOCUMENTO VOCÊ MESMO primeiro.

8
ugasoft

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

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

5
Jason Dagit

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:

5
fabiopedrosa

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

4
DougN

Compartilhe e conheça regras de propriedade de memória em todo o seu projeto. Usando as regras COM faz a melhor consistência ([no] parâmetros são de propriedade do chamador, o candidato deve copiar; [out] params são de propriedade do chamador, callee deve fazer uma cópia se manter uma referência, etc.)

4
Seth Morris

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

Está disponível na maioria dos sabores do Linux (incluindo Android) e no Darwin.

Se você usa para escrever testes de unidade para seus programas, você deve adquirir o hábito de executar sistematicamente valgrind nos testes. Isso evitará potencialmente 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.

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

4
Joseph

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

3
Joseph

Se você não puder/não usar 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 você digita qualquer código no escopo

2
Seth Morris

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 do boost

Dica # 4 Não escreva seu próprio buggy Smartpointers, use boost (em um projeto em que estou agora eu não posso usar boost, e eu sofri tendo que depurar meus próprios ponteiros inteligentes, eu definitivamente não levaria a mesma rota novamente, mas, novamente, agora eu não posso adicionar impulso às nossas dependências)

-Tip # 5 Se for algum trabalho crítico/não-desempenho crítico (como em jogos com milhares de objetos), olhe para o recipiente de ponteiro boost de Thorsten Ottosen

Dica # 6 Encontre um cabeçalho de detecção de vazamento para a sua plataforma de escolha, como o cabeçalho "vld" do Visual Leak Detection

2
Robert Gould

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

Deixe o caso em que a função apropria-se do objeto seja o caso especial. Em todas as situações em que isso acontece, não deixe 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 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 o retorno, aceite uma referência const. A partir da leitura do código do chamador, será óbvio que sua função não aceitou a propriedade do objeto. Você poderia ter tido a mesma função de aceitar 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úvida.

Não use referências não constantes nas listas de argumentos. Não é muito claro quando se lê 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 referências contadas. Isso normalmente funciona bem, mas quando você tem um bug e ele não funciona, especialmente se o seu destruidor faz algo não-trivial, como em um programa multiencadeado. Definitivamente tente ajustar seu design para não precisar de contagem de referência se não for muito difícil.

2
Jonathan

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

Quando você retorna um auto_ptr, você está dizendo ao chamador que você está dando a eles a propriedade da memória.

Quando você retorna um shared_ptr, você está dizendo ao chamador que você tem uma referência a ele e que ele faz parte da propriedade, mas não é de sua exclusiva responsabilidade.

Essas semânticas também se aplicam aos parâmetros. Se o chamador passar a você um auto_ptr, ele estará lhe dando a propriedade.

1
Justin Rudd
  • 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 evite a alocação dinâmica e a desalocação 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 ;-).
  • Quando você passar objetos para outras funções, prefira parâmetros de referência sobre ponteiros. Isso evita alguns erros possíveis.
  • Declare parâmetros const, quando possível, especialmente ponteiros para objetos. Dessa forma, os objetos não podem ser liberados "acidentalmente" (exceto se você lançar a const away ;-))).
  • Minimize o número de locais no programa onde você aloca memória e desalocação. Por exemplo. 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ídas 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 várias 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 $/€/...).
1
mh.

Se você for 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 usa), então eu o libero.
  2. Alguém me deu a referência, então eu não deveria libertá-la.

Se você precisar quebrar alguma dessas regras, por favor, documente.

É tudo sobre propriedade de ponteiro.

1
Null303

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

1
Ronny Brendel

Outros mencionaram maneiras de evitar vazamentos de memória em primeiro lugar (como ponteiros inteligentes). Mas uma ferramenta de análise de perfil e memória é muitas vezes a única maneira de rastrear problemas de memória depois de tê-los.

Valgrind memcheck é um excelente livre.

1
eli

Apenas 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á avisado sobre qualquer vazamento quando o programa sair (ele rastreia novo/delete). É básico, mas me ajudou no passado.

1
Rob

C++ é projetado RAII em mente. Não há realmente nenhuma maneira melhor de gerenciar memória em C++ eu acho. Mas tenha 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 na verificação de limites ao usar esse fragmento, você poderá sobrescrever outras variáveis ​​ou retornar endereços, o que leva a todos os tipos de falhas de segurança.

0
artificialidiot

Um dos únicos exemplos sobre 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 criando um segmento:

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;
}

Pretty easyn 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 encadeamento 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 nós vamos 'dizer' o auto_ptr para ignorar a desalocação. O gerenciamento de memória C++ é fácil, não é? Felicidades,

Ema!

0
Emanuele Oriani

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

0
Nemanja Trifunovic

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

Isso também pode ser feito em tempo de compilação, substituindo operadores new e delete e outras funções de alocação de memória.

Por exemplo, verifique este site [Depuração de alocação de memória em C++] Nota: Há um truque para excluir o operador 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á qual foi o local de onde foi chamado. Desta forma, você pode ter o rastreamento de cada exclusão e malloc do seu programa. No final da seqüência de verificação de memória, você deve ser capaz de informar qual bloco de memória alocado não foi 'excluído', identificando-o pelo nome do arquivo e número da linha, o que é o que você deseja.

Você também pode tentar algo como BoundsChecker sob Visual Studio, que é muito interessante e fácil de usar.

0
INS

Nós envolvemos todas as nossas funções de alocação com uma camada que acrescenta uma breve cadeia na frente e uma bandeira sentinela no final. Por exemplo, você teria uma chamada para myalloc (pszSomeString, iSize, iAlignment) ou new ("description", iSize) MyObject (), que aloca internamente o tamanho especificado, além de espaço suficiente para o seu cabeçalho e sentinela. , não se esqueça de comentar isso para compilações que não sejam de depuração. É necessário 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 com facilidade e rapidez o código que está vazando, fazendo pesquisas rápidas por código alocado em determinadas 'zonas', mas não limpas quando essas zonas devem ser 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 essas falhas bem ocultas ou erros de ordem. O terceiro benefício está em rastrear o uso de memória para ver quem são os grandes jogadores - um agrupamento de certas descrições em um MemDump diz quando o "som" está ocupando muito mais espaço do que você esperava, por exemplo.

0
screenglow