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());

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.


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.


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.


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.


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ê.


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.


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.


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.


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.


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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.