O que é um destrutor
Na página anterior você viu os construtores - funções especiais que rodam quando um objeto nasce para configurar seu estado inicial. Um destrutor é a imagem espelhada: uma função especial que roda quando um objeto morre, para fazer a limpeza depois dele.
Você o declara com o nome da classe precedido por um til (~). Ele não recebe parâmetros, não retorna nada, e uma classe pode ter exatamente um. Você quase nunca o chama manualmente - o C++ o chama por você no momento certo.
Repare que a mensagem do destrutor é impressa depois que main termina o corpo da função, mas antes de o programa encerrar. Quando log sai de escopo na chave de fechamento, o C++ roda ~Logger() por você.
Quando os destrutores rodam
O momento exato depende de onde o objeto vive:
- Objetos na pilha (locais) são destruídos quando saem de escopo - na chave de fechamento
}do bloco. - Objetos no heap (criados com
new) são destruídos quando você chamadelete. Se esquecer odelete, o destrutor nunca roda e você causa um vazamento.
Este exemplo torna a diferença visível:
Os objetos são destruídos na ordem inversa à da construção. a foi construído primeiro, então morre por último. Essa ordem LIFO (last-in, first-out - último a entrar, primeiro a sair) importa quando os objetos dependem uns dos outros.
Por que os destrutores importam: RAII
O verdadeiro poder dos destrutores é que eles tornam a limpeza automática e segura contra exceções. Em vez de lembrar de liberar um recurso em cada caminho do código, você coloca a liberação em um destrutor e deixa a linguagem garantir que ela rode. Esse padrão se chama RAII - Resource Acquisition Is Initialization (aquisição de recurso é inicialização) - e é a espinha dorsal do C++ moderno.
Aqui uma classe detém um buffer no heap: ela aloca no construtor e libera no destrutor, de modo que quem a usa nunca toca em new/delete por conta própria.
A ideia central: mesmo que uma exceção fosse lançada depois que squares foi criado, a pilha seria desempilhada e ~IntArray() ainda rodaria. Essa garantia é o que torna o RAII tão confiável - e o motivo pelo qual você raramente escreve um delete solto em bom código C++.
A Regra de Três (e de Cinco)
Uma classe com um destrutor personalizado quase sempre detém um recurso bruto, e isso cria um perigo oculto. O construtor de cópia e a atribuição de cópia gerados pelo compilador fazem uma cópia rasa - copiam o ponteiro, não o buffer para o qual ele aponta. Agora dois objetos detêm o mesmo ponteiro, e ambos os destrutores vão fazer delete nele, causando uma falha por dupla liberação.
IntArray a(5);
IntArray b = a; // cópia rasa: a.data e b.data são o MESMO ponteiro
// no fim do escopo: o destrutor de b libera o buffer,
// depois o destrutor de a o libera DE NOVO -> comportamento indefinido (dupla liberação)
Isso leva à Regra de Três: se você escreve qualquer um dos três - destrutor, construtor de cópia ou operador de atribuição de cópia - quase certamente precisa dos três. No C++11 e versões posteriores, ela se estende à Regra de Cinco, acrescentando o construtor de movimento e a atribuição de movimento.
Existe, porém, uma regra ainda melhor - a Regra de Zero: projete classes de forma que você não gerencie recursos brutos de modo algum. Use um std::vector, um std::string ou um ponteiro inteligente, e o destrutor gerado pelo compilador faz a coisa certa de graça.
Recorra à Regra de Zero por padrão. Escreva um destrutor personalizado apenas quando você realmente detiver um recurso bruto que nenhum tipo padrão encapsule por você.
Destrutores virtuais
Quando você apaga um objeto por meio de um ponteiro para a classe base, o destrutor precisa ser virtual - caso contrário apenas a parte base é destruída e a parte derivada vaza. Esse é um dos bugs mais comuns em código polimórfico, e o compilador não avisa sobre isso por padrão.
Sem virtual em ~Base, delete p chamaria apenas ~Base() - comportamento indefinido, e a parte Derived do objeto nunca é limpa. Regra prática: qualquer classe com funções virtuais (uma classe base polimórfica) precisa de um destrutor virtual. Você verá exatamente por que isso importa assim que começar a derivar classes.
Erros e armadilhas comuns
Algumas armadilhas pegam quase todo mundo:
new/delete incompatíveis. Se você alocar com new[], libere com delete[]. Misturar new[] com um delete simples (ou vice-versa) é comportamento indefinido.
Esquecer o virtual no destrutor de uma base. Como visto acima, apagar um objeto derivado por meio de um ponteiro para a base sem um destrutor virtual vaza a parte derivada. Se você está escrevendo uma classe destinada a ser herdada, torne o destrutor virtual.
Deixar exceções escaparem de um destrutor. Um destrutor que lança uma exceção durante o desempilhamento da pilha encerra seu programa. No C++ moderno, os destrutores são implicitamente noexcept - impeça que o código de limpeza lance exceções, ou engula a exceção dentro do destrutor.
Escrever um destrutor de que você não precisa. Se seus membros já se limpam sozinhos, um ~NomeClasse() {} vazio adiciona ruído e pode desabilitar silenciosamente as operações de movimento. Quando não há nada a limpar, não escreva destrutor algum.
Próximo: Herança
Você já viu o ciclo de vida completo de um objeto - os construtores o trazem à vida, os destrutores fazem sua limpeza, e os destrutores virtual mantêm essa limpeza correta quando uma classe é construída sobre outra. Esse último ponto é uma prévia da próxima grande ideia: a herança, na qual uma classe reutiliza e estende os dados e o comportamento de outra. A próxima página mostra como derivar uma classe de outra, como a construção e a destruição se encadeiam pela hierarquia, e como as peças que você acabou de aprender se encaixam.
Perguntas frequentes
O que é um destrutor em C++?
Um destrutor é uma função-membro especial chamada ~NomeClasse() que roda automaticamente quando um objeto é destruído - quando ele sai de escopo ou quando você faz delete. Sua função é a limpeza: liberar memória, fechar arquivos ou liberar qualquer recurso que o objeto detenha. Ele não recebe parâmetros nem tem tipo de retorno, e uma classe pode ter apenas um.
Quando um destrutor é executado em C++?
Para um objeto local (na pilha), o destrutor roda quando ele sai de escopo, na chave de fechamento }. Para um objeto no heap criado com new, ele roda quando você chama delete. Os membros e as classes base são destruídos automaticamente depois, na ordem inversa à da construção.
Sempre preciso escrever um destrutor em C++?
Não. Se sua classe contém apenas membros que se limpam sozinhos (como std::string, std::vector ou ponteiros inteligentes), o destrutor gerado pelo compilador é suficiente - não escreva um. Você só precisa de um destrutor personalizado quando sua classe detém um recurso bruto, como memória vinda de new ou um descritor de arquivo aberto.