Por que o uso de ‘novo’ causa vazamentos de memory?

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

Você pode explicar o motivo do memory leaks neste código de exemplo?

 class A { ... }; struct B { ... }; A *object1 = new A(); B object2 = *(new B()); 

O que está acontecendo

Quando você escreve T t; você está criando um object do tipo T com duração de armazenamento automático . Ele será limpo automaticamente quando sair do escopo.

Quando você escreve new T() você está criando um object do tipo T com duração de armazenamento dynamic . Não será limpo automaticamente.

novo sem limpeza

Você precisa passar um ponteiro para ele para delete fim de limpá-lo:

newing com delete

No entanto, seu segundo exemplo é pior: você está desreferenciando o ponteiro e fazendo uma cópia do object. Desta forma, você perde o ponteiro para o object criado com o new , então você nunca pode apagá-lo, mesmo se você quiser!

newing com deref

O que você deveria fazer

Você deve preferir a duração do armazenamento automático. Precisa de um novo object, 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 dynamic, armazene o ponteiro no object alocado em um object de duração de armazenamento automático que o exclua automaticamente.

 template  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(new A()); // acts like a pointer, but deletes automatically automatic_pointer b(new B()); // acts like a pointer, but deletes automatically 

newing com automatic_pointer

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

Esta coisa automatic_pointer já existe em várias formas, eu apenas forneci para dar um exemplo. Existe uma class muito semelhante na biblioteca padrão chamada std::unique_ptr .

Há também um antigo (pre-C ++ 11) chamado auto_ptr mas agora está obsoleto porque tem um comportamento de cópia estranho.

E, em seguida, há alguns exemplos ainda mais inteligentes, como std::shared_ptr , que permite vários pointers para o mesmo object e apenas limpa quando o último ponteiro é destruído.

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

Então, ao final disso, você tem um object no heap sem nenhum ponteiro para ele, então é impossível excluir.

A outra amostra:

 A *object1 = new A(); 

é um memory leaks somente se você esquecer de delete a memory alocada:

 delete object1; 

Em C ++, há objects com armazenamento automático, aqueles criados na pilha, que são descartados automaticamente e objects com armazenamento dynamic, no heap, que você aloca com new e precisam se liberar com delete . (isso é tudo mais ou menos)

Pense que você deve ter uma delete para cada object alocado com o new .

EDITAR

Venha para pensar sobre isso, object2 não precisa ser um memory leaks.

O código a seguir é apenas para fazer um ponto, é uma má idéia, nunca gosta de código 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; } } 

Neste caso, como o other é passado por referência, será o object exato apontado pelo new B() . Portanto, obter seu endereço por &other e excluir o ponteiro liberaria a memory.

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

Dados dois “objects”:

 obj a; obj b; 

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

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

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

Intuitivamente, os “objects” apontadores funcionam da mesma maneira:

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

Agora, vamos dar uma olhada no seu exemplo:

 A *object1 = new A(); 

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

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


Para o segundo exemplo, você está copiando o object apontado. O valor é o conteúdo desse object, não o ponteiro real. Como em todos os outros casos, &object2 != &*(new A()) .

 B object2 = *(new B()); 

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

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

C ++ também tem uma palavra-chave “new” que cria um object, mas diferente de Java ou C #, não é a única maneira de criar um object.

C ++ tem dois mecanismos para criar um object:

  • automático
  • dynamic

Com a criação automática, você cria o object em um ambiente com escopo definido: – em uma function ou – como membro de uma class (ou struct).

Em uma function, você criaria desta maneira:

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

Dentro de uma class, você normalmente criaria desta maneira:

 class A { B b; public: A(); }; A::A() : b( 1, 2 ) { } 

No primeiro caso, os objects são destruídos automaticamente quando o bloco de escopo é encerrado. Esta poderia ser uma function ou um bloco de escopo dentro de uma function.

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

Objetos são alocados com o novo quando você precisa controlar a vida útil do object e, em seguida, requer exclusão para destruí-lo. Com a técnica conhecida como RAII, você cuida da exclusão do object no ponto em que foi criado, colocando-o dentro de um object automático e espera que o destrutor desse object automático tenha efeito.

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

Em geral, enquanto seu código pode ter muitas chamadas para novo, você deve ter chamadas limitadas para excluir e deve sempre certificar-se de que elas sejam chamadas de destruidores ou objects “deleter” que são colocados em pointers inteligentes.

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

Se você fizer isso, você terá poucos vazamentos de memory.

 B object2 = *(new B()); 

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

object2 é uma variável do tipo B, armazenada no endereço address 1 (Sim, estou escolhendo números arbitrários aqui). No lado direito, você pediu um novo B, ou um ponteiro para um object do tipo B. O programa dá isso de bom grado a você 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ê desreferencia o ponteiro usando * para obter os dados para os quais o ponteiro está apontando (os dados no endereço 2). Isso efetivamente cria uma cópia desses dados e os atribui ao object2, atribuído no endereço 1. Lembre-se, é uma CÓPIA, não o original.

Agora, aqui está o problema:

Você nunca realmente armazenou aquele ponteiro em qualquer lugar que você possa usá-lo! Uma vez terminada esta atribuição, o ponteiro (memory no endereço 3, que você usou para acessar o endereço 2) está fora do escopo e além de seu alcance! Você não pode mais chamar delete nele e, portanto, não pode limpar a memory no endereço2. O que resta é uma cópia dos dados do endereço 2 no endereço1. Duas das mesmas coisas sentadas na memory. Um que você pode acessar, o outro que você não pode (porque você perdeu o caminho para isso). É por isso que isso é um memory leaks.

Gostaria de sugerir vindo do seu plano de fundo do C # que você leu muito sobre como funcionam os pointers em C ++. Eles são um tópico avançado e podem levar algum tempo para serem compreendidos, mas seu uso será inestimável para você.

Ao criar o object2 você está criando uma cópia do object criado com o novo, mas também está perdendo o ponteiro (nunca atribuído) (portanto, não há como excluí-lo depois). Para evitar isso, você teria que fazer do object2 uma referência.

Bem, você cria um memory leaks se em algum momento não liberar a memory alocada usando o new operador passando um ponteiro para essa memory ao operador de delete .

Nos seus dois casos acima:

 A *object1 = new A(); 

Aqui você não está usando delete para liberar a memory, então se e quando seu ponteiro object1 sair do escopo, você terá um memory leaks, porque você terá perdido o ponteiro e não poderá usar o operador delete em isto.

E aqui

 B object2 = *(new B()); 

você está descartando o ponteiro retornado pelo new B() e, portanto, nunca pode passar esse ponteiro para delete a memory a ser liberada. Daí outro memory leaks.

É essa linha que está vazando imediatamente:

 B object2 = *(new B()); 

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

Esta linha não está imediatamente com vazamento:

 A *object1 = new A(); 

Haveria um vazamento se você nunca delete d object1 embora.

Se isso facilitar, pense na memory do computador como sendo um hotel e os programas são clientes que contratam salas quando precisam.

A maneira que este hotel trabalha é que você registra uma sala e diz o porteiro quando você está saindo.

Se você programar os livros em um quarto e sair sem avisar ao porteiro, o portador achará que o quarto ainda está em uso e não deixará ninguém usá-lo. Nesse caso, há um vazamento na sala.

Se o seu programa aloca memory e não a apaga (simplesmente pára de usá-la), o computador pensa que a memory ainda está em uso e não permitirá que ninguém a use. Isso é um memory leaks.

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