Destrutores virtuais em C++: por que isso ainda é relevante (e quando pode ser ignorado)
\n\nSe você já programou em C++ e lidou com herança, certamente já viu esta pergunta no Stack Overflow com dezenas de milhares de visualizações: «When to use virtual destructors?». Em 2025, o tema não perdeu relevância — vazamentos de memória devido a um virtual esquecido continuam causando estragos em produção. Mas há também o outro lado: adicionar virtual cegamente em todos os lugares possíveis também não é uma boa prática.
Vamos entender de uma vez por todas: quando o destrutor virtual é obrigatório, quando é apenas uma boa prática, e quando sua presença indica uma arquitetura ruim. Vamos nessa.
\n\nResposta rápida (para quem está com pressa)
\n\nO destrutor virtual é necessário se, e somente se, você pretende excluir um objeto de uma classe derivada através de um ponteiro para a classe base. Se esse cenário não ocorre no código — virtual não é necessário. Mas existem nuances que abordaremos a seguir.
Por que isso é um problema? Anatomia de um vazamento
\n\nVejamos o antipadrão clássico que quebra o código de iniciantes (e não só).
\n\n#include <iostream>\n\nclass Base {\npublic:\n ~Base() { \n std::cout << "Base destructor\\n"; \n }\n};\n\nclass Derived : public Base {\nprivate:\n int* big_data = new int[1000]; // recursos que precisam ser liberados\npublic:\n Derived() { std::cout << "Derived created\\n"; }\n ~Derived() {\n delete[] big_data;\n std::cout << "Derived destructor\\n";\n }\n};\n\nint main() {\n Base* ptr = new Derived();\n delete ptr; // Opa! Apenas ~Base() será chamado\n return 0;\n}\n\nSaída:
\nDerived created\nBase destructor\n\nO array big_data vazou. O destrutor de Derived não foi chamado porque o compilador, vendo o tipo Base*, chamou o destrutor exatamente dessa classe. E ele não é virtual.
Solução — adicionar virtual antes do destrutor de Base:
class Base {\npublic:\n virtual ~Base() = default; // ou corpo vazio\n};\n\nAgora, ao executar delete ptr, primeiro será executado ~Derived(), depois ~Base(). Os recursos são liberados, a memória não vaza, o analisador estático fica satisfeito.
Quando o destrutor virtual é obrigatório (regra de ferro)
\n\nExiste apenas um caso incondicional: se a classe for projetada para uso polimórfico através de ponteiro ou referência para a classe base, e os objetos das classes derivadas forem excluídos através desse ponteiro.
\n\nNa prática, isso significa:
\n- \n
- A classe tem pelo menos uma função virtual (pura ou não) \n
- Você planeja usar
Base*ouBase¶ trabalhar comDerived\n - Os objetos são criados no heap (
new) e excluídos viadelete\n
Exemplo — hierarquia clássica de formas:
\n\nclass Shape {\npublic:\n virtual double area() const = 0;\n virtual ~Shape() = default; // obrigatório para classe abstrata\n};\n\nclass Circle : public Shape {\n double radius;\npublic:\n explicit Circle(double r) : radius(r) {}\n double area() const override { return 3.14159 * radius * radius; }\n};\n\nint main() {\n std::vector<std::unique_ptr<Shape>> shapes;\n shapes.push_back(std::make_unique<Circle>(5.0));\n // unique_ptr cuidará do delete, mas o destrutor de Shape deve ser virtual\n return 0;\n}\n\nObserve: mesmo usando std::unique_ptr (que é a abordagem recomendada em 2025), o destrutor virtual ainda é necessário. O unique_ptr chama delete através do ponteiro para a classe base —