Quando um destruidor C ++ é chamado?

118

Pergunta básica: quando um programa chama um método destruidor de classe em C ++? Disseram-me que é chamado sempre que um objeto sai do escopo ou está sujeito a umdelete

Perguntas mais específicas:

1) Se o objeto é criado por meio de um ponteiro e esse ponteiro é excluído posteriormente ou recebe um novo endereço para apontar, o objeto para o qual ele estava apontando chama seu destruidor (presumindo que nada mais esteja apontando para ele)?

2) Continuando a questão 1, o que define quando um objeto sai do escopo (não em relação a quando um objeto sai de um determinado {bloco}). Em outras palavras, quando um destruidor é chamado em um objeto em uma lista vinculada?

3) Você gostaria de chamar um destruidor manualmente?

Pat Murray
fonte
3
Mesmo suas perguntas específicas são muito amplas. "Esse ponteiro é excluído posteriormente" e "recebe um novo endereço para apontar" são bastante diferentes. Pesquise mais (algumas delas foram respondidas) e faça perguntas separadas para as partes que não conseguiu encontrar.
Matthew Flaschen

Respostas:

74

1) Se o objeto é criado por meio de um ponteiro e esse ponteiro é excluído posteriormente ou recebe um novo endereço para apontar, o objeto para o qual ele estava apontando chama seu destruidor (presumindo que nada mais esteja apontando para ele)?

Depende do tipo de ponteiros. Por exemplo, ponteiros inteligentes geralmente excluem seus objetos quando são excluídos. Ponteiros comuns, não. O mesmo é verdade quando um ponteiro é feito para apontar para um objeto diferente. Alguns ponteiros inteligentes destruirão o objeto antigo ou o destruirão se não houver mais referências. Os ponteiros comuns não têm essa inteligência. Eles apenas mantêm um endereço e permitem que você execute operações nos objetos para os quais eles apontam, fazendo isso especificamente.

2) Continuando a questão 1, o que define quando um objeto sai do escopo (não em relação a quando um objeto sai de um determinado {bloco}). Em outras palavras, quando um destruidor é chamado em um objeto em uma lista vinculada?

Depende da implementação da lista vinculada. Coleções típicas destroem todos os seus objetos contidos quando são destruídos.

Portanto, uma lista vinculada de ponteiros normalmente destruiria os ponteiros, mas não os objetos para os quais eles apontam. (O que pode estar correto. Eles podem ser referências de outros ponteiros.) Uma lista vinculada projetada especificamente para conter ponteiros, entretanto, pode excluir os objetos em sua própria destruição.

Uma lista vinculada de ponteiros inteligentes pode excluir automaticamente os objetos quando os ponteiros são excluídos ou pode fazê-lo se eles não tiverem mais referências. Cabe a você escolher as peças que fazem o que você deseja.

3) Você gostaria de chamar um destruidor manualmente?

Certo. Um exemplo seria se você deseja substituir um objeto por outro objeto do mesmo tipo, mas não deseja liberar memória apenas para alocá-lo novamente. Você pode destruir o objeto antigo no lugar e construir um novo no lugar. (No entanto, geralmente é uma má ideia.)

// pointer is destroyed because it goes out of scope,
// but not the object it pointed to. memory leak
if (1) {
 Foo *myfoo = new Foo("foo");
}


// pointer is destroyed because it goes out of scope,
// object it points to is deleted. no memory leak
if(1) {
 Foo *myfoo = new Foo("foo");
 delete myfoo;
}

// no memory leak, object goes out of scope
if(1) {
 Foo myfoo("foo");
}
David Schwartz
fonte
2
Achei que o último de seus exemplos declarasse uma função? É um exemplo da "análise mais irritante". (O outro ponto mais trivial é que eu acho que você quis dizer new Foo()com 'F' maiúsculo.)
Stuart Golodetz
1
Eu acho que Foo myfoo("foo")não é a análise mais irritante, mas char * foo = "foo"; Foo myfoo(foo);é.
Cosine
Pode ser uma pergunta estúpida, mas não deveria delete myFooser chamado antes Foo *myFoo = new Foo("foo");? Ou então você excluiria o objeto recém-criado, não?
Matheus Rocha
Não há myFooantes da Foo *myFoo = new Foo("foo");linha. Essa linha cria uma nova variável chamada myFoo, sombreando qualquer uma existente. Embora, neste caso, não haja nenhum, já que o myFooanterior está no escopo do if, que terminou.
David Schwartz
1
@galactikuh Um "ponteiro inteligente" é algo que atua como um ponteiro para um objeto, mas que também possui recursos que facilitam o gerenciamento do tempo de vida desse objeto.
David Schwartz
20

Outros já trataram dos outros problemas, então vou apenas observar um ponto: você já quis excluir manualmente um objeto.

A resposta é sim. @DavidSchwartz deu um exemplo, mas é bastante incomum. Vou dar um exemplo que está por trás do que muitos programadores de C ++ usam o tempo todo: std::vector(e std::deque, embora não seja usado tanto).

Como a maioria das pessoas sabe, std::vectoralocará um bloco maior de memória quando / se você adicionar mais itens do que sua alocação atual pode conter. Quando ele faz isso, no entanto, ele tem um bloco de memória que é capaz de conter mais objetos do que atualmente no vetor.

Para gerenciar isso, o vectorque nos oculta é alocar memória bruta por meio do Allocatorobjeto (o que, a menos que você especifique de outra forma, significa que ele usa ::operator new). Então, quando você usa (por exemplo) push_backpara adicionar um item ao vector, internamente o vetor usa a placement newpara criar um item na parte (anteriormente) não utilizada de seu espaço de memória.

Agora, o que acontece quando / se você eraseum item do vetor? Ele não pode simplesmente usar delete- isso liberaria todo o seu bloco de memória; ele precisa destruir um objeto naquela memória sem destruir nenhum outro, ou liberar qualquer bloco de memória que ele controla (por exemplo, se você erase5 itens de um vetor, então imediatamente push_back5 mais itens, é garantido que o vetor não será realocado memória quando você faz isso.

Para fazer isso, o vetor destrói diretamente os objetos na memória chamando explicitamente o destruidor, não usando delete.

Se, por acaso, outra pessoa fosse escrever um contêiner usando armazenamento contíguo mais ou menos como um vectorfaz (ou alguma variante disso, como std::dequerealmente faz), você quase certamente desejaria usar a mesma técnica.

Apenas por exemplo, vamos considerar como você pode escrever código para um buffer circular.

#ifndef CBUFFER_H_INC
#define CBUFFER_H_INC

template <class T>
class circular_buffer {
    T *data;
    unsigned read_pos;
    unsigned write_pos;
    unsigned in_use;
    const unsigned capacity;
public:
    circular_buffer(unsigned size) :
        data((T *)operator new(size * sizeof(T))),
        read_pos(0),
        write_pos(0),
        in_use(0),
        capacity(size)
    {}

    void push(T const &t) {
        // ensure there's room in buffer:
        if (in_use == capacity) 
            pop();

        // construct copy of object in-place into buffer
        new(&data[write_pos++]) T(t);
        // keep pointer in bounds.
        write_pos %= capacity;
        ++in_use;
    }

    // return oldest object in queue:
    T front() {
        return data[read_pos];
    }

    // remove oldest object from queue:
    void pop() { 
        // destroy the object:
        data[read_pos++].~T();

        // keep pointer in bounds.
        read_pos %= capacity;
        --in_use;
    }
  
~circular_buffer() {
    // first destroy any content
    while (in_use != 0)
        pop();

    // then release the buffer.
    operator delete(data); 
}

};

#endif

Ao contrário dos contêineres padrão, este usa operator newe operator deletediretamente. Para uso real, você provavelmente deseja usar uma classe alocadora, mas no momento faria mais para distrair do que contribuir (IMO, de qualquer maneira).

Jerry Coffin
fonte
9
  1. Ao criar um objeto com new, você é responsável por chamar delete. Quando você cria um objeto com make_shared, o resultante shared_ptré responsável por manter a contagem e chamar deletequando a contagem de uso chegar a zero.
  2. Sair do escopo significa deixar um bloco. É quando o destruidor é chamado, assumindo que o objeto não foi alocado com new(ou seja, é um objeto de pilha).
  3. Praticamente o único momento em que você precisa chamar um destruidor explicitamente é quando você aloca o objeto com um posicionamentonew .
dasblinkenlight
fonte
1
Há contagem de referência (shared_ptr), embora obviamente não para ponteiros simples.
Pubby de
1
@Pubby: Bom ponto, vamos promover boas práticas. Resposta editada.
MSalters de
6

1) Os objetos não são criados 'por meio de ponteiros'. Existe um ponteiro que é atribuído a qualquer objeto que você 'novo'. Assumindo que é isso que você quer dizer, se você chamar 'delete' no ponteiro, ele irá realmente excluir (e chamar o destruidor) o objeto que o ponteiro desreferenciou. Se você atribuir o ponteiro a outro objeto, haverá um vazamento de memória; nada em C ++ irá coletar seu lixo para você.

2) Estas são duas questões distintas. Uma variável sai do escopo quando o quadro de pilha em que é declarada é retirado da pilha. Normalmente, é quando você sai de um bloco. Objetos em um heap nunca saem do escopo, embora seus ponteiros na pilha possam. Nada em particular garante que um destruidor de um objeto em uma lista vinculada seja chamado.

3) Na verdade não. Pode haver Deep Magic sugerindo o contrário, mas normalmente você deseja combinar suas 'novas' palavras-chave com suas palavras-chave 'deletar' e colocar tudo o que for necessário em seu destruidor para ter certeza de que ele se limpa de maneira adequada. Se você não fizer isso, certifique-se de comentar o destruidor com instruções específicas para qualquer pessoa que esteja usando a classe sobre como eles devem limpar os recursos desse objeto manualmente.

Nathaniel Ford
fonte
3

Para dar uma resposta detalhada à pergunta 3: sim, há (raras) ocasiões em que você pode chamar o destruidor explicitamente, em particular como a contraparte de um novo posicionamento, como observa dasblinkenlight.

Para dar um exemplo concreto disso:

#include <iostream>
#include <new>

struct Foo
{
    Foo(int i_) : i(i_) {}
    int i;
};

int main()
{
    // Allocate a chunk of memory large enough to hold 5 Foo objects.
    int n = 5;
    char *chunk = static_cast<char*>(::operator new(sizeof(Foo) * n));

    // Use placement new to construct Foo instances at the right places in the chunk.
    for(int i=0; i<n; ++i)
    {
        new (chunk + i*sizeof(Foo)) Foo(i);
    }

    // Output the contents of each Foo instance and use an explicit destructor call to destroy it.
    for(int i=0; i<n; ++i)
    {
        Foo *foo = reinterpret_cast<Foo*>(chunk + i*sizeof(Foo));
        std::cout << foo->i << '\n';
        foo->~Foo();
    }

    // Deallocate the original chunk of memory.
    ::operator delete(chunk);

    return 0;
}

O objetivo desse tipo de coisa é desacoplar a alocação de memória da construção de objetos.

Stuart Golodetz
fonte
2
  1. Ponteiros - ponteiros regulares não suportam RAII. Sem um explícito delete, haverá lixo. Felizmente C ++ tem ponteiros automáticos que cuidam disso para você!

  2. Escopo - pense em quando uma variável se torna invisível para o seu programa. Normalmente, isso é no final de {block}, como você observou.

  3. Destruição manual - nunca tente fazer isso. Deixe o osciloscópio e o RAII fazerem a mágica por você.

chrisaycock
fonte
Uma nota: auto_ptr está obsoleto, como seu link menciona.
tnecniv
std::auto_ptrestá obsoleto no C ++ 11, sim. Se o OP realmente tiver C ++ 11, ele deve usar std::unique_ptrpara proprietários únicos ou std::shared_ptrpara proprietários múltiplos contados por referência.
chrisaycock de
'Destruição manual - nunca tente fazer isso'. Freqüentemente, coloco na fila ponteiros de objeto para um thread diferente usando uma chamada de sistema que o compilador não entende. 'Confiar' em ponteiros de escopo / auto / inteligentes faria com que meus aplicativos falhassem catastroficamente, pois os objetos eram excluídos pelo encadeamento de chamada antes que pudessem ser manipulados pelo encadeamento do consumidor. Esse problema afeta objetos e interfaces de escopo limitado e refCounted. Apenas ponteiros e exclusão explícita servirão.
Martin James,
@MartinJames Você pode postar um exemplo de uma chamada de sistema que o compilador não entende? E como você está implementando a fila? Não. std::queue<std::shared_ptr>?Eu descobri que pipe()entre um produtor e um consumidor encadeado torna a simultaneidade muito mais fácil, se a cópia não for muito cara.
chrisaycock
meuObjeto = new minhaClasse (); PostMessage (aHandle, WM_APP, 0, LPPARAM (meuObjeto));
Martin James
1

Sempre que você usar "novo", ou seja, anexar um endereço a um ponteiro, ou dizer, você reivindicar espaço na pilha, precisará "excluí-lo".
1.sim, quando você exclui algo, o destruidor é chamado.
2.Quando o destruidor da lista encadeada é chamado, seu destruidor de objetos é chamado. Mas se forem ponteiros, você precisará excluí-los manualmente. 3. quando o espaço é reivindicado por "novo".

ganso turvo
fonte
0

Sim, um destruidor (também conhecido como dtor) é chamado quando um objeto sai do escopo se estiver na pilha ou quando você chama deleteum ponteiro para um objeto.

  1. Se o ponteiro for excluído via delete , o dtor será chamado. Se você reatribuir o ponteiro sem chamar deleteprimeiro, terá um vazamento de memória porque o objeto ainda existe em algum lugar na memória. No último caso, o dtor não é chamado.

  2. Uma boa implementação de lista vinculada chamará o dtor de todos os objetos na lista quando a lista estiver sendo destruída (porque você chamou algum método para destruí-la ou ela saiu do próprio escopo). Isso depende da implementação.

  3. Duvido, mas não ficaria surpreso se houvesse alguma circunstância estranha por aí.

tnecniv
fonte
1
"Se você reatribuir o ponteiro sem chamar delete primeiro, você obterá um vazamento de memória porque o objeto ainda existe em algum lugar na memória.". Não necessariamente. Ele pode ter sido excluído por meio de outro ponteiro.
Matthew Flaschen
0

Se o objeto não for criado por meio de um ponteiro (por exemplo, A a1 = A ();), o destruidor será chamado quando o objeto for destruído, sempre que a função onde o objeto se encontra for concluída. Por exemplo:

void func()
{
...
A a1 = A();
...
}//finish


o destruidor é chamado quando o código é executado para a linha "terminar".

Se o objeto for criado por meio de um ponteiro (por exemplo, A * a2 = new A ();), o destruidor será chamado quando o ponteiro for excluído (exclua a2;). Se o ponto não for excluído explicitamente pelo usuário ou dado um novo endereço antes de excluí-lo, ocorreu o vazamento de memória. Isso é um bug.

Em uma lista encadeada, se usarmos std :: list <>, não precisamos nos preocupar com o desctructor ou vazamento de memória porque std :: list <> terminou tudo isso para nós. Em uma lista vinculada escrita por nós mesmos, devemos escrever o desctrutor e excluir o ponteiro explicitamente. Do contrário, isso causará vazamento de memória.

Raramente chamamos um destruidor manualmente. É uma função que provê o sistema.

Desculpe pelo meu pobre inglês!

wyx
fonte
Não é verdade que você não pode chamar um destruidor manualmente - você pode (veja o código em minha resposta, por exemplo). O que é verdade é que na grande maioria das vezes você não deveria :)
Stuart Golodetz
0

Lembre-se de que o Construtor de um objeto é chamado imediatamente depois que a memória é alocada para esse objeto e que o destruidor é chamado imediatamente antes de desalocar a memória desse objeto.

Sunny Khandare
fonte