Destruidores (C++)

Um destruidor é 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 destruidor 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.

Observe a 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 destruidor String::~String usa o operador delete[] para desalocar o espaço alocado dinamicamente para o armazenamento de texto.

Declarando destruidores

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

Várias regras controlam a declaração de destruidores. 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 chamados para destruir objetos declarados como const, volatile ou static.
  • Deve 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. Destruidores também podem ser declarados como funções virtuais puras para classes abstratas.

Usando destruidores

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

  • Um objeto local (automático) com escopo de bloco sai do escopo.
  • Use delete para desalocar um objeto alocado usando new. O uso delete[] resulta em comportamento indefinido.
  • Use delete[] para desalocar um objeto alocado usando new[]. O uso delete resulta em comportamento indefinido.
  • O tempo de vida de um objeto temporário termina.
  • Um programa é encerrado e existem objetos globais ou estáticos.
  • O destruidor é chamado explicitamente usando o nome totalmente qualificado da função de destruidor.

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

Há duas restrições no uso de destruidores:

  • Você não pode tomar seu endereço.

  • As classes derivadas não herdam o destruidor de sua 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 destruidor da classe é chamado e o corpo da função de destruidor é executado.

  2. Os destruidores para objetos dos membros não estáticos são chamados na ordem inversa em que aparecem na declaração da classe. A lista de inicialização de membros opcionais usada na construção desses membros não afeta a ordem de construção ou destruição.

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

  4. Os destruidores de classes base virtuais são chamados 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 destruidores para classes base virtuais são chamados na ordem inversa de sua ocorrência em um grafo direcionado acíclico (primeiro em profundidade, da esquerda para a direita, ordem de publicação transversal). a figura a seguir ilustra um grafo de herança.

Inheritance graph that shows virtual base classes.

Cinco classes, rotuladas de A a E, são organizadas em um gráfico de heranç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. Percorrer o gráfico à esquerda, a partir do ponto mais profundo do gráfico (nesse caso, E).
  2. Executar percurso à esquerda até que todos os nós sejam visitados. Tomar nota do nome do nó atual.
  3. Retornar ao nó anterior (para baixo e à direita) para descobrir se o nó que está sendo recordado é uma classe base virtual.
  4. Se o nó recordado for uma classe base virtual, verificar 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. Percorrer o gráfico acima e ao longo do caminho seguinte à direita.
  7. Vá para a etapa 2.
  8. Quando o último caminho acima tiver sido é esgotado, tomar nota o nome do nó atual.
  9. Vá para a etapa 3.
  10. Continuar esse processo até que o nó final 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.

Esse processo gera uma lista ordenada de entradas exclusivas. Nenhum nome de classe aparece duas vezes. Uma vez que a lista é construída, ela é caminhada em ordem inversa, e o destruidor para cada uma das classes na lista da última para a primeira é chamado.

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

Essas interdependências entre classes em um grafo de herança são inerentemente perigosas, pois 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 destruidores de classes base não virtuais são chamados na ordem inversa da qual os nomes da classe base são declarados. Considere a seguinte declaração de classe:

class MultInherit : public Base1, public Base2
...

No exemplo anterior, o destruidor de Base2 é chamado antes de destruidor de Base1.

Chamadas do destruidor explícitas

Chamar um destruidor de forma explícita raramente é necessário. No entanto, pode ser útil realizar a limpeza dos objetos colocados em endereços absolutos. Esses objetos são normalmente alocados usando um operador new definido pelo usuário que usa um argumento de posicionamento. O delete operador não pode desalocar essa memória porque ela não está alocada do repositório gratuito (para obter mais informações, consulte Os operadores novos e excluídos). Entretanto, uma chamada para o destruidor pode realizar uma limpeza apropriada. Para chamar explicitamente o destruidor 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 de chamadas explícitas aos destruidores, mostrada anteriormente, pode ser usada independentemente de o tipo definir um destruidor. Isso permite fazer essas chamadas explícitas sem saber se há um destruidor definido para o tipo. Uma chamada explícita para um destruidor que não tem nenhum 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 precisará implementar um construtor de cópia e uma atribuição de cópia.

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

No próximo exemplo, o construtor de cópia gerado implicitamente fará os ponteiros str1.text e str2.text se referirá à 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

Definir explicitamente um destruidor, um construtor de cópia ou um operador de atribuição de cópia impede a definição implícita do construtor de movimentação e do operador de atribuição de movimentação. Nesse caso, não fornecer operações de movimentação geralmente será, se a cópia for cara, uma oportunidade de otimização perdida.

Confira também

Operadores de construtores de cópia e de atribuição de cópia
Operadores de construtores de movimento e de atribuição de movimento