Por que o uso de 'novo' causa vazamentos de memória?

131

Aprendi C # primeiro e agora estou começando com C ++. Pelo que entendi, o operador newem C ++ não é semelhante ao do C #.

Você pode explicar o motivo do vazamento de memória neste código de exemplo?

class A { ... };
struct B { ... };

A *object1 = new A();
B object2 = *(new B());
Xeo
fonte
Uma quase duplicata: a coleta de lixo é automática no C ++ padrão?
Brent Bradburn

Respostas:

464

O que está acontecendo

Ao escrever, T t;você cria um objeto do tipo Tcom duração de armazenamento automático . Ele será limpo automaticamente quando sair do escopo.

Ao escrever, new T()você cria um objeto do tipo Tcom duração de armazenamento dinâmico . Não será limpo automaticamente.

novo sem limpeza

Você precisa passar um ponteiro para ele deletepara limpá-lo:

novidade com exclusão

No entanto, seu segundo exemplo é pior: você está desreferenciando o ponteiro e fazendo uma cópia do objeto. Dessa forma, você perde o ponteiro para o objeto criado com new, para nunca poder excluí-lo, mesmo que quisesse!

novidade com deref

O que você deveria fazer

Você deve preferir a duração do armazenamento automático. Precisa de um novo objeto, basta escrever:

A a; // a new object of type A
B b; // a new object of type B

Se você precisar de duração de armazenamento dinâmico, armazene o ponteiro no objeto alocado em um objeto de duração de armazenamento automático que o exclua automaticamente.

template <typename T>
class automatic_pointer {
public:
    automatic_pointer(T* pointer) : pointer(pointer) {}

    // destructor: gets called upon cleanup
    // in this case, we want to use delete
    ~automatic_pointer() { delete pointer; }

    // emulate pointers!
    // with this we can write *p
    T& operator*() const { return *pointer; }
    // and with this we can write p->f()
    T* operator->() const { return pointer; }

private:
    T* pointer;

    // for this example, I'll just forbid copies
    // a smarter class could deal with this some other way
    automatic_pointer(automatic_pointer const&);
    automatic_pointer& operator=(automatic_pointer const&);
};

automatic_pointer<A> a(new A()); // acts like a pointer, but deletes automatically
automatic_pointer<B> b(new B()); // acts like a pointer, but deletes automatically

novidades com automatic_pointer

Este é um idioma comum que atende pelo nome de RAII não muito descritivo ( Resource Acquisition Is Initialization ). Quando você adquire um recurso que precisa de limpeza, cole-o em um objeto com duração de armazenamento automático para não precisar se preocupar em limpá-lo. Isso se aplica a qualquer recurso, seja memória, arquivos abertos, conexões de rede ou o que você desejar.

Essa automatic_pointercoisa já existe de várias formas, eu apenas a forneci para dar um exemplo. Uma classe muito semelhante existe na biblioteca padrão chamada std::unique_ptr.

Há também um antigo (pré-C ++ 11) nomeado, auto_ptrmas agora está obsoleto porque tem um comportamento estranho de cópia.

E há alguns exemplos ainda mais inteligentes, como std::shared_ptr, que permitem vários ponteiros para o mesmo objeto e apenas o limpam quando o último ponteiro é destruído.

R. Martinho Fernandes
fonte
4
@ user1131997: feliz por você ter feito outra pergunta. Como você pode ver que não é muito fácil de explicar nos comentários :)
R. Martinho Fernandes
@ R.MartinhoFernandes: excelente resposta. Apenas uma pergunta. Por que você usou retorno por referência na função operador * ()?
Destructor
@ Resposta final do destruidor: D. Retornar por referência permite modificar o apontador, para que você possa, por exemplo *p += 2, fazer como faria com um apontador normal. Se não retornasse por referência, não imitaria o comportamento de um ponteiro normal, que é a intenção aqui.
R. Martinho Fernandes
Muito obrigado por recomendar "armazenar o ponteiro para o objeto alocado em um objeto de duração de armazenamento automático que o exclui automaticamente". Se ao menos houvesse uma maneira de exigir que os codificadores aprendessem esse padrão antes que pudessem compilar qualquer C ++!
20917 Andy
35

Uma explicação passo a passo:

// creates a new object on the heap:
new B()
// dereferences the object
*(new B())
// calls the copy constructor of B on the object
B object2 = *(new B());

Portanto, ao final disso, você tem um objeto no heap sem ponteiro, portanto, é impossível excluir.

A outra amostra:

A *object1 = new A();

é um vazamento de memória apenas se você esquecer a deletememória alocada:

delete object1;

No C ++, existem objetos com armazenamento automático, aqueles criados na pilha, que são descartados automaticamente e objetos com armazenamento dinâmico, no heap, com os quais você aloca newe precisa se libertar delete. (isso é tudo grosso modo)

Pense que você deve ter um deletepara cada objeto alocado new.

EDITAR

Venha para pensar sobre isso, object2não precisa ser um vazamento de memória.

O código a seguir é apenas para esclarecer, é uma má idéia, nunca goste de códigos como este:

class B
{
public:
    B() {};   //default constructor
    B(const B& other) //copy constructor, this will be called
                      //on the line B object2 = *(new B())
    {
        delete &other;
    }
}

Nesse caso, como otheré passado por referência, será o objeto exato apontado por new B(). Portanto, obter o endereço &othere excluir o ponteiro liberaria a memória.

Mas não posso enfatizar isso o suficiente, não faça isso. É só aqui para fazer um ponto.

Luchian Grigore
fonte
2
Eu estava pensando o mesmo: podemos hackear para não vazar, mas você não gostaria de fazer isso. O objeto1 também não precisa vazar, pois seu construtor pode se conectar a algum tipo de estrutura de dados que o excluirá em algum momento.
precisa saber é
2
É sempre tentador escrever essas respostas "é possível fazer isso, mas não"! :-) Eu conheço o sentimento #
828
11

Dados dois "objetos":

obj a;
obj b;

Eles não ocuparão o mesmo local na memória. Em outras palavras,&a != &b

Atribuir o valor de um para o outro não mudará sua localização, mas mudará seu conteúdo:

obj a;
obj b = a;
//a == b, but &a != &b

Intuitivamente, os "objetos" ponteiros funcionam da mesma maneira:

obj *a;
obj *b = a;
//a == b, but &a != &b

Agora, vejamos o seu exemplo:

A *object1 = new A();

Isso está atribuindo o valor de new A()a object1. O valor é um ponteiro, significando object1 == new A(), mas &object1 != &(new A()). (Observe que este exemplo não é um código válido, é apenas para explicação)

Como o valor do ponteiro é preservado, podemos liberar a memória para a qual ele aponta: delete object1;Devido à nossa regra, isso se comporta da mesma forma delete (new A());que não tem vazamento.


Para o seu segundo exemplo, você está copiando o objeto apontado. O valor é o conteúdo desse objeto, não o ponteiro real. Como em qualquer outro caso &object2 != &*(new A()),.

B object2 = *(new B());

Perdemos o ponteiro para a memória alocada e, portanto, não podemos liberá-lo. delete &object2;pode parecer que funcionaria, mas porque &object2 != &*(new A())não é equivalente delete (new A())e, portanto, inválido.

Pubby
fonte
9

Em C # e Java, você usa new para criar uma instância de qualquer classe e não precisa se preocupar em destruí-la posteriormente.

O C ++ também possui a palavra-chave "new", que cria um objeto, mas, diferentemente do Java ou C #, não é a única maneira de criar um objeto.

O C ++ possui dois mecanismos para criar um objeto:

  • automático
  • dinâmico

Com a criação automática, você cria o objeto em um ambiente com escopo definido: - em uma função ou - como membro de uma classe (ou estrutura).

Em uma função, você a criaria da seguinte maneira:

int func()
{
   A a;
   B b( 1, 2 );
}

Dentro de uma classe, você normalmente a cria dessa maneira:

class A
{
  B b;
public:
  A();
};    

A::A() :
 b( 1, 2 )
{
}

No primeiro caso, os objetos são destruídos automaticamente quando o bloco de escopo é encerrado. Pode ser uma função ou um bloco de escopo dentro de uma função.

No último caso, o objeto b é destruído juntamente com a instância de A na qual ele é um membro.

Os objetos são alocados com new quando você precisa controlar a vida útil do objeto e, em seguida, ele requer exclusão para destruí-lo. Com a técnica conhecida como RAII, você cuida da exclusão do objeto no momento em que o cria, colocando-o em um objeto automático e aguarda a efetivação do destruidor desse objeto automático.

Um desses objetos é um shared_ptr que invocará uma lógica "deleter", mas somente quando todas as instâncias do shared_ptr que estão compartilhando o objeto forem destruídas.

Em geral, embora seu código possa ter muitas chamadas para novas, você deve ter chamadas limitadas para excluir e sempre certificar-se de que elas sejam chamadas de destruidores ou objetos "deletadores" inseridos em ponteiros inteligentes.

Seus destruidores também nunca devem lançar exceções.

Se você fizer isso, terá poucos vazamentos de memória.

CashCow
fonte
4
Há mais do que automatice dynamic. Há também static.
Mooing Duck
9
B object2 = *(new B());

Essa linha é a causa do vazamento. Vamos separar isso um pouco ..

O objeto2 é uma variável do tipo B, armazenada no endereço 1 (sim, estou escolhendo números arbitrários aqui). No lado direito, você solicitou um novo B ou um ponteiro para um objeto do tipo B. O programa oferece isso de bom grado e atribui seu novo B ao endereço 2 e também cria um ponteiro no endereço 3. Agora, a única maneira de acessar os dados no endereço 2 é através do ponteiro no endereço 3. Em seguida, você desferenciou o ponteiro *para obter os dados que o ponteiro está apontando (os dados no endereço 2). Isso efetivamente cria uma cópia desses dados e os atribui ao objeto2, atribuído no endereço 1. Lembre-se, é uma CÓPIA, não o original.

Agora, aqui está o problema:

Você nunca realmente armazenou esse ponteiro em qualquer lugar em que possa usá-lo! Depois que essa tarefa é concluída, o ponteiro (memória no endereço3, que você usou para acessar o endereço2) fica fora do escopo e está além do seu alcance! Você não pode mais chamar delete e, portanto, não pode limpar a memória no endereço2. O que resta é uma cópia dos dados do endereço2 no endereço1. Duas das mesmas coisas guardadas na memória. Um que você pode acessar, o outro não (porque você perdeu o caminho). É por isso que isso é um vazamento de memória.

Eu sugiro que, a partir do seu background em C #, você leia muito sobre como os ponteiros em C ++ funcionam. Eles são um tópico avançado e podem levar algum tempo para entender, mas o uso deles será inestimável para você.

MGZero
fonte
8

Se isso facilitar, pense na memória do computador como um hotel e os programas são clientes que contratam quartos quando precisam deles.

A maneira como esse hotel funciona é que você reserve um quarto e informe o porteiro quando sair.

Se você programar uma sala de livros e sair sem avisar o porteiro, o porteiro pensará que a sala ainda está em uso e não permitirá que mais ninguém a use. Nesse caso, há um vazamento na sala.

Se o seu programa alocar memória e não a excluir (ele simplesmente para de usá-la), o computador pensará que a memória ainda está em uso e não permitirá que mais ninguém a utilize. Este é um vazamento de memória.

Essa não é uma analogia exata, mas pode ajudar.

Stefan
fonte
5
Eu gosto bastante dessa analogia, não é perfeita, mas é definitivamente uma boa maneira de explicar vazamentos de memória para pessoas que são novas nela!
314 AdamM
1
Eu usei isso em uma entrevista para um engenheiro sênior da Bloomberg em Londres para explicar vazamentos de memória a uma garota de RH. Eu terminei a entrevista porque fui capaz de realmente explicar vazamentos de memória (e problemas de segmentação) para um não programador da maneira que ela entendeu.
Stefan
7

Ao criar, object2você cria uma cópia do objeto que criou com novo, mas também perde o ponteiro (nunca atribuído) (portanto, não há como excluí-lo mais tarde). Para evitar isso, você teria que fazer object2uma referência.

Mario
fonte
3
É uma prática incrivelmente ruim usar o endereço de uma referência para excluir um objeto. Use um ponteiro inteligente.
Tom Whittock
3
Prática incrivelmente ruim, não é? O que você acha que ponteiros inteligentes usam nos bastidores?
Blindy
3
Ponteiros inteligentes @Blindy (pelo menos decentemente implementados) usam ponteiros diretamente.
Luchian Grigore
2
Bem, para ser perfeitamente honesto, a idéia toda não é tão boa assim, não é? Na verdade, nem tenho certeza de onde o padrão tentado no OP seria realmente útil.
Mario
7

Bem, você cria um vazamento de memória se, em algum momento, não liberar a memória alocada usando o newoperador, passando um ponteiro para essa memória para o deleteoperador.

Nos seus dois casos acima:

A *object1 = new A();

Aqui você não está usando deletepara liberar a memória; portanto, se e quando o object1ponteiro ficar fora do escopo, haverá um vazamento de memória, porque você o perdeu e não poderá usar o deleteoperador nele.

E aqui

B object2 = *(new B());

você está descartando o ponteiro retornado por new B()e, portanto, nunca pode passar esse ponteiro para deleteque a memória seja liberada. Daí outro vazamento de memória.

razlebe
fonte
7

É essa linha que está vazando imediatamente:

B object2 = *(new B());

Aqui você está criando um novo Bobjeto na pilha e, em seguida, criando uma cópia na pilha. O que foi alocado no heap não pode mais ser acessado e, portanto, o vazamento.

Esta linha não está imediatamente vazando:

A *object1 = new A();

Haveria um vazamento se você nunca deleted object1embora.

mattjgalloway
fonte
4
Por favor, não use heap / stack ao explicar o armazenamento dinâmico / automático.
precisa saber é o seguinte
2
@Pubby por que não usar? Por causa do armazenamento dinâmico / automático, é sempre pilha, não pilha? E é por isso que não há necessidade de detalhar a pilha / pilha, estou certo?
4
@ user1131997 Heap / stack são detalhes de implementação. Eles são importantes para conhecer, mas são irrelevantes para esta pergunta.
Pubby
2
Hmm, gostaria de uma resposta separada, ou seja, igual à minha, mas substituindo o heap / stack pelo que você achar melhor. Eu estaria interessado em descobrir como você prefere explicá-lo.
mattjgalloway