Partilhar via


Destruidores (C++)

Um destrutor é uma função de membro que é invocada automaticamente quando o objeto sai do escopo ou é explicitamente destruído por uma chamada para delete ou delete[]. Um destruidor tem o mesmo nome que a classe e é precedido por um til (~). Por exemplo, o destrutor da classe String é declarado: ~String().

Se você não definir um destruidor, o compilador fornecerá um padrão, e para algumas classes isso é suficiente. Você precisa definir um destruidor personalizado quando a classe mantém recursos que devem ser liberados explicitamente, como identificadores para recursos do sistema ou ponteiros para memória que devem ser liberados quando uma instância da classe é destruída.

Considere a seguinte declaração de uma classe String:

// spec1_destructors.cpp
#include <string> // strlen()

class String
{
    public:
        String(const char* ch);  // Declare the constructor
        ~String();               // Declare the destructor
    private:
        char* _text{nullptr};
};

// Define the constructor
String::String(const char* ch)
{
    size_t sizeOfText = strlen(ch) + 1; // +1 to account for trailing NULL

    // Dynamically allocate the correct amount of memory.
    _text = new char[sizeOfText];

    // If the allocation succeeds, copy the initialization string.
    if (_text)
    {
        strcpy_s(_text, sizeOfText, ch);
    }
}

// Define the destructor.
String::~String()
{
    // Deallocate the memory that was previously reserved for the string.
    delete[] _text;
}

int main()
{
    String str("We love C++");
}

No exemplo anterior, o String::~String destructor usa o operador delete[] para desalocar o espaço alocado dinamicamente para armazenar texto.

Declarando destrutores

Os destruidores são funções com o mesmo nome da classe, mas precedidas por um til (~)

Várias regras regem a declaração de destrutores. Destruidores:

  • Não aceite argumentos.
  • Não retorne um valor (ou void).
  • Não pode ser declarado como const, volatileou static. No entanto, eles podem ser invocados para a destruição de objetos declarados como const, volatileou static.
  • Pode ser declarado como virtual. Usando destruidores virtuais, você pode destruir objetos sem saber seu tipo — o destruidor correto para o objeto é invocado usando o mecanismo de função virtual. Os destruidores também podem ser declarados como funções virtuais puras para classes abstratas.

Usando destrutores

Os destruidores são chamados quando ocorre um dos seguintes eventos:

  • Um objeto local (automático) com escopo de bloco deixa de estar no escopo.
  • Use delete para desalocar um objeto alocado usando new. A utilização delete[] resulta num comportamento indefinido.
  • Use delete[] para desalocar um objeto alocado usando new[]. A utilização delete resulta num comportamento indefinido.
  • A vida útil de um objeto temporário termina.
  • Um programa termina e existem objetos globais ou estáticos.
  • O destruidor é explicitamente chamado usando o nome totalmente qualificado da função destructor.

Os destruidores podem chamar livremente funções de membro de classe e acessar dados de membros de classe.

Existem duas restrições à utilização de destruidores:

  • Você não pode pegar o endereço dele.

  • As classes derivadas não herdam o destrutor da classe base.

Ordem de destruição

Quando um objeto sai do escopo ou é excluído, a sequência de eventos em sua destruição completa é a seguinte:

  1. O destrutor da classe é chamado e o corpo da função do destrutor é executado.

  2. Os destructores para objetos membros não estáticos são chamados na ordem inversa em que aparecem na declaração de classe. A lista de inicialização opcional de membros usada para construir esses membros não afeta a ordem de construção ou destruição.

  3. Os destrutores das classes base não virtuais são chamados na ordem inversa da declaração.

  4. Os destrutores para classes base virtuais são invocados na ordem inversa à da declaração.

// order_of_destruction.cpp
#include <cstdio>

struct A1      { virtual ~A1() { printf("A1 dtor\n"); } };
struct A2 : A1 { virtual ~A2() { printf("A2 dtor\n"); } };
struct A3 : A2 { virtual ~A3() { printf("A3 dtor\n"); } };

struct B1      { ~B1() { printf("B1 dtor\n"); } };
struct B2 : B1 { ~B2() { printf("B2 dtor\n"); } };
struct B3 : B2 { ~B3() { printf("B3 dtor\n"); } };

int main() {
   A1 * a = new A3;
   delete a;
   printf("\n");

   B1 * b = new B3;
   delete b;
   printf("\n");

   B3 * b2 = new B3;
   delete b2;
}
A3 dtor
A2 dtor
A1 dtor

B1 dtor

B3 dtor
B2 dtor
B1 dtor

Classes base virtuais

Os destrutores para classes base virtuais são chamados na ordem inversa de a sua aparência em um grafo acíclico direcionado (percorrimento em profundidade, da esquerda para a direita, ordem pós-fixada). A figura a seguir mostra um gráfico de herança.

Gráfico de herança que mostra classes base virtuais.

Cinco classes, rotuladas de A a E, são organizadas em um gráfico de herança. A classe E é a classe base de B, C e D. As classes C e D são a classe base de A e B.

A seguir estão listadas as definições de classe para as classes mostradas na figura:

class A {};
class B {};
class C : virtual public A, virtual public B {};
class D : virtual public A, virtual public B {};
class E : public C, public D, virtual public B {};

Para determinar a ordem de destruição das classes base virtuais de um objeto do tipo E, o compilador cria uma lista aplicando o seguinte algoritmo:

  1. Percorra o gráfico à esquerda, começando no ponto mais profundo do gráfico (neste caso, E).
  2. Execute travessias para a esquerda até que todos os nós tenham sido visitados. Anote o nome do nó atual.
  3. Revisite o nó anterior (para baixo e para a direita) para descobrir se o nó que está a ser lembrado é uma classe base virtual.
  4. Se o nó lembrado for uma classe base virtual, verifique a lista para ver se ela já foi inserida. Se não for uma classe base virtual, ignore-a.
  5. Se o nó lembrado ainda não estiver na lista, adicione-o à parte inferior da lista.
  6. Navegue pelo gráfico para cima e ao longo do próximo caminho para a direita.
  7. Vá para a etapa 2.
  8. Quando o último caminho ascendente estiver esgotado, anote o nome do nó atual.
  9. Vá para a etapa 3.
  10. Continue este processo até que o nó inferior seja novamente o nó atual.

Portanto, para a classe E, a ordem de destruição é:

  1. A classe base não virtual E.
  2. A classe base não virtual D.
  3. A classe base não virtual C.
  4. A classe base virtual B.
  5. A classe base virtual A.

Este processo produz uma lista ordenada de entradas únicas. Nenhum nome de classe aparece duas vezes. Uma vez que a lista é construída, ela é percorrida na ordem inversa, e o destrutor para cada uma das classes na lista da última para a primeira é chamado.

A ordem de construção ou destruição é principalmente importante quando os construtores ou destrutores de uma classe dependem de que o outro componente seja criado primeiro ou persista por mais tempo — por exemplo, se o destrutor para A (na figura mostrada anteriormente) dependia de B ainda estar presente quando o seu código tenha sido executado, ou vice-versa.

Tais interdependências entre classes em um gráfico de herança são inerentemente perigosas porque as classes derivadas posteriormente podem alterar qual é o caminho mais à esquerda, alterando assim a ordem de construção e destruição.

Classes base não virtuais

Os destrutores para classes base não virtuais são chamados na ordem inversa em que os nomes das classes base são declarados. Considere a seguinte declaração de classe:

class MultInherit : public Base1, public Base2
...

No exemplo anterior, o destrutor para Base2 é chamado antes do destrutor para Base1.

Chamadas explícitas do destruidor

Chamar um destrutor explicitamente é raramente necessário. No entanto, pode ser útil executar a limpeza de objetos colocados em endereços absolutos. Esses objetos geralmente são alocados usando um operador de new definido pelo usuário que usa um argumento de posicionamento. O operador de delete não pode desalocar essa memória porque ela não está alocada do armazenamento gratuito (para obter mais informações, consulte Os operadores novos e excluídos). Uma chamada para o destrutor, no entanto, pode executar a limpeza apropriada. Para chamar explicitamente o destrutor de um objeto, s, da classe String, use uma das seguintes instruções:

s.String::~String();     // non-virtual call
ps->String::~String();   // non-virtual call

s.~String();       // Virtual call
ps->~String();     // Virtual call

A notação para chamadas explícitas para destrutores, mostrada anteriormente, pode ser usada independentemente de o tipo definir um destrutor. Isso permite lhe fazer essas chamadas explícitas sem saber se um destrutor está definido para o tipo. Uma chamada explícita para um destruidor onde nenhum está definido não tem efeito.

Programação robusta

Uma classe precisa de um destruidor se adquirir um recurso e, para gerenciar o recurso com segurança, provavelmente terá que implementar um construtor de cópia e uma atribuição de cópia.

Se essas funções especiais não são definidas pelo usuário, elas são implicitamente definidas pelo compilador. Os construtores e operadores de atribuição gerados implicitamente executam uma cópia superficial e baseada em membros, o que é quase certamente errado se um objeto estiver gerenciando um recurso.

No próximo exemplo, o construtor de cópia gerado implicitamente fará com que os ponteiros str1.text e str2.text se refiram à mesma memória, e quando retornarmos de copy_strings(), essa memória será excluída duas vezes, o que é um comportamento indefinido:

void copy_strings()
{
   String str1("I have a sense of impending disaster...");
   String str2 = str1; // str1.text and str2.text now refer to the same object
} // delete[] _text; deallocates the same memory twice
  // undefined behavior

A definição explícita de um destruidor, construtor de cópia ou operador de atribuição de cópia impede a definição implícita do construtor de movimento e do operador de atribuição de movimento. Neste caso, se a cópia for cara, deixar de fornecer operações de movimentação é geralmente uma oportunidade de otimização perdida.

Ver também

Construtores de cópia e operadores de atribuição de cópia
Construtores de Movimento e Operadores de Atribuição por Movimento