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.
- 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_ptrestd::lock_guardusam 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 objeto | Função que roda | Serve para |
|---|---|---|
| Nasce | construtor: Classe() | Preparar o objeto, pegar recursos. |
| Morre | destrutor: ~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.
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:
- Fim de bloco. Um par de chaves solto
{ … }cria um escopo.Cmorreu na chave que fecha esse bloco, bem antes do fim domain. - Fim de função.
Té variável local detarefa(); morreu quando a função terminou. - Cada volta de laço. O corpo do
foré um bloco. A cada volta,vnasce e morre de novo. - Ordem inversa.
Anasceu antes deB, eBmorreu antes deA. É 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.
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:
- 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";
}
| Linha | O 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
| Classe | Recurso | Construtor pega | Destrutor devolve |
|---|---|---|---|
std::ifstream / std::ofstream | Arquivo aberto | Abre o arquivo | Fecha o arquivo (e grava o que faltava) |
std::string, std::vector | Memória para os itens | Reserva memória | Libera a memória |
std::unique_ptr, std::shared_ptr | Um objeto criado com new | Recebe o objeto | Faz o delete |
std::lock_guard | Uma trava (std::mutex) | Fecha a trava | Abre 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.
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-resourcese Python temwith).
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 libera | Algum momento depois, quando o coletor decidir | Na hora: no fim do escopo |
| O que libera | Só memória | Memória, arquivos, travas, conexões… |
| Custo extra | Um processo de coleta rodando | Nenhum além do próprio destrutor |
| O que você precisa fazer | Nada para memória; fechar o resto à mão | Usar 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
| Regra | Por quê |
|---|---|
Nome: ~ + nome da classe | É assim que o compilador o reconhece. |
| Sem parâmetros e sem tipo de retorno | Ninguém chama o destrutor com argumentos nem usa o resultado: ele roda sozinho. |
| Um só por classe | Sem parâmetros, não há como ter versões diferentes (sem sobrecarga). |
Não chame obj.~Classe() à mão | Ele rodaria de novo no fim do escopo: destruir duas vezes é comportamento indefinido. |
| Não deixe exceção sair do destrutor | Desde 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 virtual | Explicado em Polimorfismo. |
Quando dá errado
$ 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.
$ 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() { … }.
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.
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 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ção | Quando o destrutor roda |
|---|---|
| Variável local | Na } que fecha o bloco em que foi declarada |
| Vários objetos no mesmo bloco | Na ordem inversa da criação |
| Variável dentro de um laço | No fim de cada volta |
| Atributo de outro objeto | Depois do corpo do destrutor do dono, na ordem inversa da declaração |
Objeto criado com new | Só no delete (nunca, se esquecerem) |
Objeto dentro de std::unique_ptr | Quando o unique_ptr morre |
Itens de um std::vector | Quando o vetor morre, ou quando o item é removido (pop_back, clear…) |
| Uma exceção sai da função | Todas 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:
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
newsó morrem nodelete; esquecer dá vazamento. Prefirastd::unique_ptrou variáveis comuns. - RAII: o construtor pega o recurso e o destrutor devolve. Funciona até quando uma exceção ou um
returnsai da função no meio. std::ifstream/ofstream,std::string,std::vector,std::unique_ptrestd::lock_guardjá 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.