Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
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,volatileoustatic. No entanto, eles podem ser invocados para a destruição de objetos declarados comoconst,volatileoustatic. - 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
deletepara desalocar um objeto alocado usandonew. A utilizaçãodelete[]resulta num comportamento indefinido. - Use
delete[]para desalocar um objeto alocado usandonew[]. A utilizaçãodeleteresulta 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:
O destrutor da classe é chamado e o corpo da função do destrutor é executado.
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.
Os destrutores das classes base não virtuais são chamados na ordem inversa da declaração.
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.
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:
- Percorra o gráfico à esquerda, começando no ponto mais profundo do gráfico (neste caso,
E). - Execute travessias para a esquerda até que todos os nós tenham sido visitados. Anote o nome do nó atual.
- Revisite o nó anterior (para baixo e para a direita) para descobrir se o nó que está a ser lembrado é uma classe base virtual.
- 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.
- Se o nó lembrado ainda não estiver na lista, adicione-o à parte inferior da lista.
- Navegue pelo gráfico para cima e ao longo do próximo caminho para a direita.
- Vá para a etapa 2.
- Quando o último caminho ascendente estiver esgotado, anote o nome do nó atual.
- Vá para a etapa 3.
- Continue este processo até que o nó inferior seja novamente o nó atual.
Portanto, para a classe E, a ordem de destruição é:
- A classe base não virtual
E. - A classe base não virtual
D. - A classe base não virtual
C. - A classe base virtual
B. - 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