C++ Destrutores e RAII

Na aula de Construtores você viu a função que roda quando um objeto nasce. Esta aula é sobre a função que roda quando ele morre: o destrutor. Parece um detalhe, mas é uma das ideias mais importantes do C++. É com ela que um arquivo se fecha sozinho, que a memória volta para o sistema sem você pedir e que o C++ consegue viver sem "coletor de lixo". O nome dessa técnica é RAII, e no fim da aula você vai entender por que todo programador C++ fala dela.

Você vai aprender
  • O que é um destrutor e como escrever um: ~Classe().
  • Quando o destrutor roda: no fim do bloco, no fim da função, no delete.
  • A ordem real em que os objetos nascem e morrem (o último a nascer é o primeiro a morrer).
  • O que é RAII: prender um recurso (arquivo, memória, trava) ao tempo de vida de um objeto.
  • Como std::ifstream, std::vector, std::unique_ptr e std::lock_guard usam RAII por você.
  • Por que o C++ não precisa de coletor de lixo.

Todos os programas foram compilados com o g++ 15 na máquina da Ana (usuária ana, computador notebook), na pasta ~/cpp, com o comando padrão do guia: g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa (veja Compilar com g++).


A ideia: quem pega, devolve

Pense numa biblioteca. Você pega um livro emprestado e, quando termina, devolve. Se todo mundo esquecesse de devolver, a biblioteca ficaria vazia em uma semana. Agora imagine uma biblioteca mágica: o livro volta sozinho para a estante no instante em que você sai do prédio. Ninguém precisa lembrar de nada.

Programas também pegam coisas emprestadas do sistema: memória, arquivos abertos, conexões, travas. Isso tudo se chama recurso, e todo recurso precisa ser devolvido. O destrutor é a "biblioteca mágica" do C++: uma função que roda automaticamente quando o objeto deixa de existir, e é o lugar perfeito para devolver o que ele pegou.

Momento da vida do objetoFunção que rodaServe para
Nasceconstrutor: Classe()Preparar o objeto, pegar recursos.
Morredestrutor: ~Classe()Devolver os recursos, fazer a limpeza final.

1. O primeiro destrutor

O destrutor tem o mesmo nome da classe, com um til (~) na frente. Para enxergar quando ele roda, vamos colocar um std::cout dentro do construtor e outro dentro do destrutor:

#include <iostream>
#include <string>

class Aviso {
public:
    Aviso(std::string nome) : nome_{nome} {
        std::cout << "Construtor: " << nome_ << " nasceu\n";
    }
    ~Aviso() {
        std::cout << "Destrutor: " << nome_ << " morreu\n";
    }
private:
    std::string nome_;
};

int main() {
    std::cout << "Início do main\n";
    Aviso a{"A"};
    std::cout << "Fim do main\n";
}
Aviso(std::string nome) : nome_{nome}
O construtor, com a lista de inicialização que guarda o nome no atributo nome_ (visto em Construtores).
~
O til marca o destrutor. Em matemática e em C++, ~ lembra "o contrário": o contrário do construtor.
~Aviso()
O destrutor: mesmo nome da classe, sem parâmetros e sem tipo de retorno (nem void). Cada classe tem um só destrutor.
{ … }
O corpo: o que fazer na hora da morte. Aqui só imprime; num programa de verdade, devolve recursos.
Aviso a{"A"};
Cria o objeto: o construtor roda nesta linha.
a chave } do main
Fim do escopo de a: o destrutor roda aqui. Você não chama o destrutor; o C++ chama por você.
$ g++ -std=c++23 -Wall -Wextra nasce.cpp -o nasce
$ ./nasce
Início do main
Construtor: A nasceu
Fim do main
Destrutor: A morreu      « depois do "Fim do main": na chave que fecha o main

Repare que nenhuma linha do main chama ~Aviso(). Mesmo assim, a mensagem apareceu, e apareceu depois de Fim do main. O destrutor rodou exatamente quando a variável a deixou de existir: na chave } que fecha o main.

E se eu não escrever destrutor?

O compilador cria um sozinho, vazio por fora. Ele não imprime nada, mas faz uma coisa importante: chama o destrutor de cada atributo. É por isso que o std::string nome_ devolve a memória do texto mesmo sem você escrever nada. A maioria das classes não precisa de destrutor próprio, como você vai ver no fim da aula.


2. Quando o destrutor roda

Uma variável comum (criada sem new) vive do ponto em que foi declarada até o fim do bloco em que ela está, ou seja, até a próxima } que fecha o seu bloco. Esse trecho é o escopo dela (aula Escopo), e o tempo entre nascer e morrer é o seu tempo de vida. Vamos ver vários casos de uma vez. Para a saída ficar curta, o Aviso agora imprime + quando nasce e - quando morre:

#include <iostream>
#include <string>

class Aviso {
public:
    Aviso(std::string nome) : nome_{nome} {
        std::cout << "  + " << nome_ << '\n';
    }
    ~Aviso() {
        std::cout << "  - " << nome_ << '\n';
    }
private:
    std::string nome_;
};

void tarefa() {
    std::cout << "Entrando em tarefa()\n";
    Aviso t{"T (da função)"};
    std::cout << "Saindo de tarefa()\n";
}

int main() {
    Aviso a{"A"};
    Aviso b{"B"};
    {
        std::cout << "Abrindo o bloco\n";
        Aviso c{"C (do bloco)"};
        std::cout << "Fechando o bloco\n";
    }
    tarefa();
    for (int i = 1; i <= 2; ++i) {
        Aviso v{"volta " + std::to_string(i)};
    }
    std::cout << "Fim do main\n";
}
$ g++ -std=c++23 -Wall -Wextra ordem.cpp -o ordem
$ ./ordem
  + A
  + B
Abrindo o bloco
  + C (do bloco)
Fechando o bloco
  - C (do bloco)         « a chave } do bloco: C morre aqui
Entrando em tarefa()
  + T (da função)
Saindo de tarefa()
  - T (da função)        « fim da função: T morre aqui
  + volta 1
  - volta 1              « cada volta do for cria e destrói um objeto novo
  + volta 2
  - volta 2
Fim do main
  - B                    « B nasceu depois de A, então morre antes
  - A

Leia a saída de cima para baixo e compare com o código. Quatro regras aparecem:

  1. Fim de bloco. Um par de chaves solto { … } cria um escopo. C morreu na chave que fecha esse bloco, bem antes do fim do main.
  2. Fim de função. T é variável local de tarefa(); morreu quando a função terminou.
  3. Cada volta de laço. O corpo do for é um bloco. A cada volta, v nasce e morre de novo.
  4. Ordem inversa. A nasceu antes de B, e B morreu antes de A. É como uma pilha de pratos: o último que você colocou é o primeiro que você tira.

A ordem inversa tem um motivo: um objeto criado depois pode depender de um criado antes (por exemplo, usar um arquivo que outro objeto abriu). Destruindo de trás para frente, ninguém fica usando algo que já morreu.

Experimente: rode como está. Depois crie um Aviso d{"D"}; logo antes de Fim do main e descubra em que ponto ele morre. Tente também mover Aviso b{"B"}; para dentro do bloco.

#include <iostream>
#include <string>

class Aviso {
public:
    Aviso(std::string nome) : nome_{nome} {
        std::cout << "  + " << nome_ << '\n';
    }
    ~Aviso() {
        std::cout << "  - " << nome_ << '\n';
    }
private:
    std::string nome_;
};

int main() {
    Aviso a{"A"};
    Aviso b{"B"};
    {
        std::cout << "Abrindo o bloco\n";
        Aviso c{"C"};
        std::cout << "Fechando o bloco\n";
    }
    std::cout << "Fim do main\n";
}

Os atributos morrem depois do dono

Quando um objeto tem outros objetos como atributos, a ordem é: primeiro roda o corpo do destrutor do dono, depois os atributos morrem, na ordem inversa em que foram declarados. Assim, dentro do destrutor, você ainda pode usar todos os atributos:

class Mochila {
public:
    Mochila() : caderno_{"caderno"}, estojo_{"estojo"} {
        std::cout << "Mochila pronta\n";
    }
    ~Mochila() {
        std::cout << "Esvaziando a mochila\n";
    }
private:
    Aviso caderno_;
    Aviso estojo_;
};

int main() {
    Mochila m;
    std::cout << "Fim do main\n";
}
$ ./membros
  + caderno              « os atributos nascem primeiro, na ordem da declaração
  + estojo
Mochila pronta           « depois roda o corpo do construtor
Fim do main
Esvaziando a mochila     « na morte, primeiro o corpo do destrutor...
  - estojo               « ...depois os atributos, de trás para frente
  - caderno

Objetos criados com new: só morrem no delete

Um objeto criado com new (aula Memória Dinâmica) não pertence a nenhum bloco. Ele vive até alguém escrever delete. Se ninguém escrever, ele nunca morre, e o destrutor nunca roda. Já o std::unique_ptr (aula Ponteiros Inteligentes) faz o delete sozinho:

int main() {
    Aviso* p = new Aviso{"P (com new)"};
    std::cout << "Antes do delete\n";
    delete p;
    std::cout << "Depois do delete\n";

    Aviso* esquecido = new Aviso{"E (ninguém deu delete)"};
    (void)esquecido;

    auto u = std::make_unique<Aviso>("U (unique_ptr)");
    std::cout << "Fim do main\n";
}
$ g++ -std=c++23 -Wall -Wextra heap.cpp -o heap
$ ./heap
  + P (com new)
Antes do delete
  - P (com new)          « o delete chamou o destrutor
Depois do delete
  + E (ninguém deu delete)
  + U (unique_ptr)
Fim do main
  - U (unique_ptr)       « o unique_ptr morreu no fim do main e levou o objeto junto

O E nasceu e nunca morreu: não existe a linha - E. A memória dele ficou presa até o programa terminar. Isso se chama vazamento de memória (memory leak). O (void)esquecido; só serve para o compilador não avisar que a variável não foi usada.

Dá para pegar o vazamento

Compilando com -fsanitize=address (aula Sanitizers), o programa avisa no final ERROR: LeakSanitizer: detected memory leaks ("detectados vazamentos de memória") e aponta a linha do new esquecido. Mas o melhor é nem precisar: com RAII, que vem a seguir, não existe delete para esquecer.


3. RAII: o recurso preso ao tempo de vida do objeto

RAII é a sigla de Resource Acquisition Is Initialization, "adquirir o recurso é inicializar". O nome é estranho (até o criador do C++, Bjarne Stroustrup, admite), mas a regra é simples:

A regra do RAII
  • O construtor pega o recurso (abre o arquivo, reserva a memória, fecha a trava).
  • O destrutor devolve o recurso (fecha o arquivo, libera a memória, abre a trava).
  • Você usa o objeto como uma variável comum. Quando ele sai do escopo, o recurso volta sozinho.

O problema: o jeito manual

O C++ herdou da linguagem C um jeito antigo de abrir arquivos: std::fopen ("file open", abrir arquivo) devolve um ponteiro do tipo std::FILE*, e std::fclose ("file close") fecha. Quem abre precisa lembrar de fechar. Veja uma função que salva a lista de compras e, na segunda chamada, encontra um problema no meio do caminho (uma exceção, o jeito do C++ de interromper tudo e avisar que deu errado):

#include <cstdio>
#include <iostream>
#include <stdexcept>

void salvar_lista(bool falhar) {
    std::FILE* arq = std::fopen("compras.txt", "w");
    std::cout << "[abriu compras.txt]\n";
    std::fputs("arroz\n", arq);
    if (falhar) throw std::runtime_error{"acabou o espaço"};
    std::fputs("feijão\n", arq);
    std::fclose(arq);
    std::cout << "[fechou]\n";
}

int main() {
    salvar_lista(false);
    try {
        salvar_lista(true);
    } catch (const std::exception& e) {
        std::cout << "Erro: " << e.what() << '\n';
    }
}
$ g++ -std=c++23 -Wall -Wextra manual.cpp -o manual
$ ./manual
[abriu compras.txt]
[fechou]
[abriu compras.txt]      « segunda chamada: abriu...
Erro: acabou o espaço    « ...e nunca fechou: o throw pulou o fclose

Na segunda vez, o throw saiu da função no meio, e a linha do fclose nunca rodou. O arquivo ficou aberto. O mesmo acontece com um return antecipado que alguém acrescenta meses depois. Num programa que roda por dias (um servidor), cada arquivo esquecido é um recurso a menos, até o sistema recusar abrir o próximo.

A solução: uma classe Arquivo que fecha sozinha

Coloque o fopen no construtor e o fclose no destrutor:

#include <cstdio>
#include <iostream>
#include <string>

class Arquivo {
public:
    Arquivo(const std::string& nome) {
        ptr_ = std::fopen(nome.c_str(), "w");
        std::cout << "[abriu " << nome << "]\n";
    }
    ~Arquivo() {
        if (ptr_) std::fclose(ptr_);
        std::cout << "[fechou]\n";
    }
    void escrever(const std::string& texto) {
        if (ptr_) std::fputs(texto.c_str(), ptr_);
    }

    Arquivo(const Arquivo&) = delete;             // proíbe cópias
    Arquivo& operator=(const Arquivo&) = delete;  // (aula Cópia e Movimento)
private:
    std::FILE* ptr_;
};

int main() {
    {
        Arquivo lista{"compras.txt"};
        lista.escrever("arroz\n");
        lista.escrever("feijão\n");
        std::cout << "Escrevi duas linhas\n";
    }
    std::cout << "Já fora do bloco\n";
}
LinhaO que faz
std::FILE* ptr_;O recurso que a classe "toma conta": o arquivo aberto.
ptr_ = std::fopen(nome.c_str(), "w");No construtor: abre o arquivo para escrita ("w" = write). O c_str() entrega o texto no formato antigo que o fopen entende. Se não conseguir abrir, ptr_ fica nulo.
if (ptr_) std::fclose(ptr_);No destrutor: fecha, se estiver aberto. Ninguém precisa lembrar disso.
escrever(…)Um método comum para usar o recurso. fputs = "file put string", grava um texto.
Arquivo(const Arquivo&) = delete;Proíbe copiar um Arquivo. Sem isso, duas cópias tentariam fechar o mesmo arquivo. O porquê completo está na próxima aula, Cópia e Movimento.
$ g++ -std=c++23 -Wall -Wextra arquivo.cpp -o arquivo
$ ./arquivo
[abriu compras.txt]
Escrevi duas linhas
[fechou]                 « fechou sozinho, na chave } do bloco
Já fora do bloco
$ cat compras.txt
arroz
feijão

Mesmo com erro no meio, o destrutor roda

Agora o teste de fogo: a mesma função salvar_lista do jeito manual, mas usando a classe Arquivo (com uma pequena mudança: o construtor lança uma exceção se o fopen falhar):

void salvar_lista(bool falhar) {
    Arquivo arq{"compras.txt"};
    arq.escrever("arroz\n");
    if (falhar) throw std::runtime_error{"acabou o espaço"};
    arq.escrever("feijão\n");
}                       // sem fclose: o destrutor de arq cuida disso
$ g++ -std=c++23 -Wall -Wextra excecao.cpp -o excecao
$ ./excecao
[abriu compras.txt]
[fechou]
[abriu compras.txt]
[fechou]                 « o throw saiu da função, e o destrutor rodou no caminho
Erro: acabou o espaço

Quando uma exceção sai de uma função, o C++ destrói todas as variáveis locais pelo caminho, de trás para frente, antes de chegar ao catch. Isso se chama desempilhar (stack unwinding). Com RAII, não importa como a função termina (no fim, num return no meio ou numa exceção): o recurso sempre volta.


4. RAII que você já usa sem saber

A biblioteca padrão do C++ é feita de classes RAII. Você quase nunca precisa escrever uma classe como a Arquivo: ela já existe, pronta e mais completa.

#include <fstream>
#include <iostream>
#include <memory>
#include <mutex>
#include <string>
#include <vector>

std::mutex trava;
int contador = 0;

void somar() {
    std::lock_guard<std::mutex> guarda{trava};  // trava aqui...
    ++contador;
}                                               // ...e destrava aqui

int main() {
    {
        std::ofstream saida{"compras.txt"};
        saida << "arroz\nfeijão\n";
    }   // o ofstream fechou o arquivo aqui

    std::ifstream entrada{"compras.txt"};
    std::string linha;
    while (std::getline(entrada, linha)) {
        std::cout << "Li: " << linha << '\n';
    }

    std::vector<int> notas{7, 8, 10};
    auto nome = std::make_unique<std::string>("Ana");
    somar();
    std::cout << "contador = " << contador << '\n';
}   // entrada, notas e nome liberam o que pegaram aqui
$ g++ -std=c++23 -Wall -Wextra biblio.cpp -o biblio
$ ./biblio
Li: arroz
Li: feijão
contador = 1
ClasseRecursoConstrutor pegaDestrutor devolve
std::ifstream / std::ofstreamArquivo abertoAbre o arquivoFecha o arquivo (e grava o que faltava)
std::string, std::vectorMemória para os itensReserva memóriaLibera a memória
std::unique_ptr, std::shared_ptrUm objeto criado com newRecebe o objetoFaz o delete
std::lock_guardUma trava (std::mutex)Fecha a travaAbre a trava

O std::lock_guard ("guarda da trava") aparece em programas com várias threads, tarefas que rodam ao mesmo tempo. Uma std::mutex é uma trava que deixa só uma tarefa por vez mexer no contador. Esquecer de abrir a trava congela o programa para sempre, e o lock_guard torna esse esquecimento impossível. Você vai usá-lo de verdade na aula de threads; aqui, guarde a ideia: trava presa à vida de um objeto.

Por que o ofstream está dentro de um bloco?

Para ele morrer (e fechar o arquivo, gravando tudo) antes de o ifstream abrir o mesmo arquivo para ler. Esse truque das chaves soltas é a forma mais simples de dizer "o recurso só é necessário até aqui". Arquivos têm aula própria: Ler e Escrever Arquivos.


5. Por que o C++ não precisa de coletor de lixo

Linguagens como Java, Python e JavaScript têm um coletor de lixo (garbage collector): um ajudante que, de tempos em tempos, procura objetos que ninguém mais usa e libera a memória deles. É cômodo, mas tem dois custos:

  • Você não sabe quando acontece. O objeto pode ficar na memória por segundos ou minutos depois de não servir mais.
  • Só cuida de memória. Arquivos, conexões e travas continuam precisando ser fechados à mão (por isso Java tem try-with-resources e Python tem with).

No C++, com RAII, a liberação é determinística: acontece num ponto exato do código, a chave }, e vale para qualquer recurso, não só memória. Não existe um processo extra rodando por trás, o que é um dos motivos para o C++ ser usado em jogos, navegadores e sistemas em que cada milissegundo conta.

Coletor de lixo (Java, Python…)RAII (C++)
Quando liberaAlgum momento depois, quando o coletor decidirNa hora: no fim do escopo
O que liberaSó memóriaMemória, arquivos, travas, conexões…
Custo extraUm processo de coleta rodandoNenhum além do próprio destrutor
O que você precisa fazerNada para memória; fechar o resto à mãoUsar classes RAII (as da biblioteca padrão já são)

O preço do RAII é uma disciplina: em C++ moderno, você quase nunca escreve delete. Use std::string, std::vector, std::unique_ptr e os fluxos de arquivo, e deixe os destrutores deles trabalharem. Quando uma classe só tem atributos assim, ela nem precisa de destrutor próprio. Isso tem nome, regra do zero, e aparece na próxima aula.


6. Regras do destrutor

RegraPor quê
Nome: ~ + nome da classeÉ assim que o compilador o reconhece.
Sem parâmetros e sem tipo de retornoNinguém chama o destrutor com argumentos nem usa o resultado: ele roda sozinho.
Um só por classeSem parâmetros, não há como ter versões diferentes (sem sobrecarga).
Não chame obj.~Classe() à mãoEle rodaria de novo no fim do escopo: destruir duas vezes é comportamento indefinido.
Não deixe exceção sair do destrutorDesde o C++11 o destrutor é noexcept (promete não lançar exceções). Se lançar, o programa é abortado (veja abaixo).
Em classe base de herança, faça-o virtualExplicado em Polimorfismo.

Quando dá errado

"destructors may not have parameters"
$ g++ -std=c++23 -Wall -Wextra e1.cpp -o e1
e1.cpp:5:5: error: destructors may not have parameters
    5 |     ~Aviso(int codigo) {
      |     ^
e1.cpp: In destructor ‘Aviso::~Aviso()’:
e1.cpp:6:35: error: ‘codigo’ was not declared in this scope
    6 |         std::cout << "morreu " << codigo << '\n';
      |                                   ^~~~~~

O que significa: "destrutores não podem ter parâmetros". O compilador ignorou o int codigo, e por isso o segundo erro diz que codigo não existe. O Clang do editor diz destructor cannot have any parameters.

Como consertar: tire o parâmetro. Se o destrutor precisa de uma informação, guarde-a num atributo quando o objeto for construído.

"return type specification for destructor invalid"
$ g++ -std=c++23 -Wall -Wextra e2.cpp -o e2
e2.cpp:5:5: error: return type specification for destructor invalid
    5 |     void ~Aviso() {
      |     ^~~~

O que significa: "especificar tipo de retorno para destrutor é inválido". Destrutor não tem tipo de retorno, nem void. (Clang: destructor cannot have a return type.)

Como consertar: apague o void: ~Aviso() { … }.

Exceção dentro do destrutor: "‘throw’ will always call ‘terminate’"
class Conexao {
public:
    ~Conexao() {
        std::cout << "Fechando a conexão\n";
        throw std::runtime_error{"falhou ao fechar"};
    }
};
$ g++ -std=c++23 -Wall -Wextra e3.cpp -o e3
e3.cpp: In destructor ‘Conexao::~Conexao()’:
e3.cpp:8:9: warning: ‘throw’ will always call ‘terminate’ [-Wterminate]
    8 |         throw std::runtime_error{"falhou ao fechar"};
      |         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
e3.cpp:8:9: note: in C++11 destructors default to ‘noexcept’
$ ./e3
Fechando a conexão
terminate called after throwing an instance of 'std::runtime_error'
  what():  falhou ao fechar
Abortado                   (imagem do núcleo gravada) ./e3
$ echo $?
134

O que significa: o aviso diz "o throw sempre vai chamar terminate", e a nota explica: desde o C++11 destrutores são noexcept. O main tinha um try/catch em volta, mas não adiantou: terminate aborta o programa na hora (código 134).

Como consertar: o destrutor deve terminar sempre em silêncio. Se fechar pode falhar, ofereça um método comum (fechar()) que o usuário chama quando quer saber do erro, e no destrutor apenas tente fechar, sem lançar nada.

Usar um objeto que já morreu: "reference to local variable ‘texto’ returned"
const std::string& saudacao() {
    std::string texto = "Olá, Ana!";
    return texto;
}

int main() {
    std::cout << saudacao() << '\n';
}
$ g++ -std=c++23 -Wall -Wextra e4.cpp -o e4
e4.cpp: In function ‘const std::string& saudacao()’:
e4.cpp:6:12: warning: reference to local variable ‘texto’ returned [-Wreturn-local-addr]
    6 |     return texto;
      |            ^~~~~
e4.cpp:5:17: note: declared here
    5 |     std::string texto = "Olá, Ana!";
      |                 ^~~~~
$ ./e4
Falha de segmentação     (imagem do núcleo gravada) ./e4
$ echo $?
139

O que significa: "devolvida uma referência a uma variável local". texto morre (o destrutor da string libera o texto) na chave que fecha saudacao(). O main recebe uma referência para um objeto que não existe mais, e o programa quebra. É o lado ruim do tempo de vida bem definido: depois da }, o objeto acabou de verdade.

Como consertar: devolva por valor: std::string saudacao(). Isso é barato no C++ moderno (veja Semântica de Movimento).

O destrutor nunca rodou (nenhuma mensagem)

O que aconteceu: o objeto foi criado com new e ninguém deu delete, como o E do exemplo da seção 2. Não há erro de compilação nem mensagem: só o silêncio, e um vazamento de memória.

Como consertar: troque Classe* p = new Classe{…}; por auto p = std::make_unique<Classe>(…);, ou simplesmente por uma variável comum: Classe obj{…};.


Missão

Missão: a classe Luz acende quando nasce. (1) Escreva o destrutor, que mostra Luz apagada. (2) No main, coloque a luz dentro de um bloco { … } de modo que Luz apagada apareça antes de Saí da sala. Rode e confira a ordem.

#include <iostream>

class Luz {
public:
    Luz() {
        std::cout << "Luz acesa\n";
    }
    // escreva o destrutor aqui
};

int main() {
    std::cout << "Entrei na sala\n";
    Luz luz;
    std::cout << "Lendo um livro...\n";
    std::cout << "Saí da sala\n";
}
#include <iostream>

class Luz {
public:
    Luz() {
        std::cout << "Luz acesa\n";
    }
    ~Luz() {
        std::cout << "Luz apagada\n";
    }
};

int main() {
    std::cout << "Entrei na sala\n";
    {
        Luz luz;
        std::cout << "Lendo um livro...\n";
    }
    std::cout << "Saí da sala\n";
}

A saída da solução no laboratório:

$ ./luz
Entrei na sala
Luz acesa
Lendo um livro...
Luz apagada
Saí da sala

Referência

SituaçãoQuando o destrutor roda
Variável localNa } que fecha o bloco em que foi declarada
Vários objetos no mesmo blocoNa ordem inversa da criação
Variável dentro de um laçoNo fim de cada volta
Atributo de outro objetoDepois do corpo do destrutor do dono, na ordem inversa da declaração
Objeto criado com newSó no delete (nunca, se esquecerem)
Objeto dentro de std::unique_ptrQuando o unique_ptr morre
Itens de um std::vectorQuando o vetor morre, ou quando o item é removido (pop_back, clear…)
Uma exceção sai da funçãoTodas as variáveis locais morrem no caminho até o catch

Exercícios

1. O nome do destrutor

Complete a declaração do destrutor da classe Conta:

{ std::cout << "Conta encerrada\n"; }

2. Quem morre primeiro?

No main estão, nesta ordem, Aviso x{"X"}; Aviso y{"Y"}; Aviso z{"Z"};. Qual nome aparece na primeira linha - quando o main termina?

3. A sigla

Qual é a sigla da técnica de prender um recurso ao tempo de vida de um objeto?

4. Uma função cria um std::ofstream, escreve uma linha e lança uma exceção antes de terminar. O que acontece com o arquivo?

5. Qual destes objetos não tem o destrutor chamado automaticamente?

6. Qual a principal diferença entre RAII e um coletor de lixo?


Resumo

  • O destrutor ~Classe() roda sozinho quando o objeto morre. Não tem parâmetros nem tipo de retorno, e cada classe tem um só.
  • Variáveis comuns morrem na } do seu bloco, na ordem inversa em que nasceram. Atributos morrem depois do corpo do destrutor do dono.
  • Objetos de new só morrem no delete; esquecer dá vazamento. Prefira std::unique_ptr ou variáveis comuns.
  • RAII: o construtor pega o recurso e o destrutor devolve. Funciona até quando uma exceção ou um return sai da função no meio.
  • std::ifstream/ofstream, std::string, std::vector, std::unique_ptr e std::lock_guard já são RAII.
  • Por isso o C++ não precisa de coletor de lixo: a liberação é imediata e vale para qualquer recurso.
  • Nunca deixe uma exceção sair de um destrutor e nunca devolva referência para uma variável local.