Encerramos a Fase 1 no artigo Adeus NULL, Bem-vindo à Dedução — nullptr e auto e vimos, de relance, o std::string se limpar sozinho ao fim do escopo. Agora começa a Fase 2 — o coração do C++ — e o objetivo é transformar aquele truque em técnica sua. Ao fim desta aula você terá escrito uma classe que adquire um recurso quando nasce e o devolve quando morre, automaticamente, sem um free ou um fclose à vista. Esse padrão tem nome, RAII, e uma vez que ele entra na sua cabeça, você passa a enxergar quase todo o C++ moderno como variações dele. É a aula mais importante do curso até aqui; leia com calma.
Structs que ganham comportamento: a classe
Você conhece struct do C — um agrupamento de dados. Em C++, struct e class são quase a mesma coisa (a única diferença técnica é que membros de struct são públicos por padrão e de class, privados — voltaremos a isso na Fase 3). O que muda de verdade é que, em C++, um tipo pode ter funções especiais que o compilador chama automaticamente em momentos-chave da vida do objeto. Duas nos interessam hoje: o construtor, chamado quando o objeto nasce, e o destrutor, chamado quando ele morre.
#include <iostream>
class Sensor {
public:
// CONSTRUTOR: mesmo nome da classe, sem tipo de retorno.
// Roda automaticamente quando um Sensor é criado.
Sensor() {
std::cout << "Sensor ligado\n";
}
// DESTRUTOR: nome da classe com ~ na frente, sem parâmetros.
// Roda automaticamente quando o Sensor é destruído.
~Sensor() {
std::cout << "Sensor desligado\n";
}
};
int main() {
std::cout << "antes\n";
{
Sensor s; // aqui o construtor roda: "Sensor ligado"
std::cout << "usando o sensor\n";
} // aqui 's' morre: o destrutor roda: "Sensor desligado"
std::cout << "depois\n";
return 0;
}
A saída é:
antes
Sensor ligado
usando o sensor
Sensor desligado
depois
Observe o ponto crucial: você não chamou o construtor nem o destrutor. O compilador os inseriu — o construtor no ponto onde s foi criado, o destrutor no } onde s saiu de escopo. Esse "no fim do escopo, automaticamente" é o motor do RAII.
RAII de verdade: amarrando um recurso ao objeto
O exemplo do sensor só imprime. O poder aparece quando o construtor adquire um recurso e o destrutor o libera. Vamos envolver um arquivo — algo que em C você abre com fopen e é obrigado a fechar com fclose, sob pena de vazar o descritor:
#include <cstdio>
#include <iostream>
// Uma classe que "possui" um arquivo: abre no construtor, fecha no destrutor.
class ArquivoTexto {
public:
// O construtor ADQUIRE o recurso: abre o arquivo.
ArquivoTexto(const char* caminho) {
f_ = std::fopen(caminho, "w");
if (f_ == nullptr)
std::cout << "falha ao abrir " << caminho << '\n';
else
std::cout << "arquivo aberto\n";
}
// O destrutor LIBERA o recurso: fecha o arquivo, se aberto.
~ArquivoTexto() {
if (f_ != nullptr) {
std::fclose(f_);
std::cout << "arquivo fechado\n";
}
}
void escreve(const char* texto) {
if (f_ != nullptr)
std::fputs(texto, f_);
}
private:
std::FILE* f_ = nullptr; // o recurso que a classe possui
};
int main() {
{
ArquivoTexto arq("saida.txt"); // abre
arq.escreve("linha 1\n");
arq.escreve("linha 2\n");
} // ← ao sair do escopo, o destrutor fecha o arquivo AUTOMATICAMENTE
std::cout << "trabalho concluído\n";
return 0;
}
Compare com o C que você escreveria: fopen, checar NULL, usar, e nunca, jamais, esquecer o fclose — inclusive nos caminhos de erro, onde é tão fácil esquecer. Aqui, o fclose está amarrado ao fim de vida do objeto. Não importa como o escopo termine — chegando ao }, um return no meio, ou até uma exceção sendo lançada (Fase 6) —, o destrutor roda e o arquivo fecha. Essa garantia é o que torna o RAII tão robusto: a limpeza não depende da sua disciplina, depende da linguagem.
Construtores com parâmetros e a lista de inicialização
O construtor de ArquivoTexto recebeu um argumento (caminho). Construtores podem ter parâmetros como qualquer função, e é assim que você configura o objeto no nascimento. Há uma sintaxe especial e preferida para inicializar membros, a lista de inicialização de membros, que vem depois de dois-pontos:
#include <iostream>
#include <string>
class Ponto {
public:
// Inicializa x_ e y_ ANTES do corpo do construtor rodar.
// Esta é a forma idiomática e mais eficiente.
Ponto(int x, int y) : x_(x), y_(y) {
std::cout << "Ponto(" << x_ << ", " << y_ << ") criado\n";
}
void mostra() const { // 'const' aqui promete não alterar o objeto (Artigo A Disciplina do const)
std::cout << "(" << x_ << ", " << y_ << ")\n";
}
private:
int x_;
int y_;
};
int main() {
Ponto p(3, 4); // chama Ponto(int, int)
p.mostra(); // (3, 4)
return 0;
}
A lista : x_(x), y_(y) inicializa os membros diretamente, antes de o corpo { } rodar. É melhor do que atribuir dentro do corpo (x_ = x;) porque inicializa de uma vez, em vez de criar o membro vazio e depois atribuir — uma diferença que fica importante quando os membros são tipos caros de construir. Adote a lista de inicialização como padrão. E note o const no método mostra(): ele promete, no espírito do artigo A Disciplina do const — Promessas que o Compilador Cobra, que olhar o ponto não o modifica — permitindo chamá-lo até em objetos const.
A honestidade sobre o que ainda falta
Você acabou de escrever uma classe que gerencia um recurso, e isso é enorme. Mas há uma armadilha que preciso plantar agora para colher no próximo artigo. O que acontece se você copiar um ArquivoTexto? Por padrão, o compilador copia o membro f_ — o ponteiro — e agora dois objetos acham que possuem o mesmo arquivo. Quando ambos forem destruídos, ambos chamarão fclose no mesmo descritor: fechar duas vezes é um bug clássico e sério. Esse perigo — o de classes que possuem recursos e são copiadas ingenuamente — é exatamente o que a Regra dos Três/Cinco resolve, e é o tema do artigo Quando Copiar Dá Errado — a Regra dos Três e dos Cinco. Por ora, guarde a pergunta: quem possui o recurso, e o que acontece na cópia? Ela será a chave da próxima aula.
O que faz o RAII funcionar não é o destrutor em si: é a garantia de que ele será chamado — pelo fim do escopo, pela exceção que sobe, pelo caminho de erro que ninguém lembrou de escrever. É essa certeza que transforma liberar recurso de tarefa sua em consequência da linguagem, e é o que o free no fim da função nunca deu, porque bastava um return no meio para pulá-lo. Quanto à lista de inicialização, ela não é preferência de estilo: membros const e referências só podem ser inicializados ali, e o resto pagaria por uma construção seguida de atribuição.
Fontes e leituras recomendadas
- Bjarne Stroustrup, A Tour of C++ (3ª ed.), capítulo sobre classes e gerenciamento de recursos: a introdução canônica a construtores, destrutores e RAII pelo criador da linguagem.
- cppreference.com/w/cpp/language/raii: a página que define formalmente o idioma RAII e lista os tipos da biblioteca que o empregam.
- cppreference.com/w/cpp/language/constructor e /w/cpp/language/destructor: as referências técnicas das duas funções especiais desta aula.
- ISO C++ Core Guidelines, seção "R" (Resource management), regras R.1 ("Manage resources automatically using RAII") e C.40/C.41 sobre construtores: as diretrizes que sustentam tudo o que vimos.
- Scott Meyers, Effective C++ (3ª ed.), Item 13 ("Use objects to manage resources"): o argumento clássico a favor de embrulhar recursos em objetos RAII.
Exercícios
Exercício 1
Escreva uma classe Cronometro cujo construtor imprime "iniciado" e cujo destrutor imprime "parado". No main, crie um objeto dentro de um bloco { } e observe quando cada mensagem aparece.
Ver resposta
✓ Resposta: A classe:
#include <iostream>
class Cronometro {
public:
Cronometro() { std::cout << "iniciado\n"; }
~Cronometro() { std::cout << "parado\n"; }
};
int main() {
std::cout << "antes\n";
{
Cronometro c; // "iniciado"
std::cout << "medindo\n";
} // "parado" — no fim do bloco
std::cout << "depois\n";
return 0;
}
A mensagem "parado" aparece exatamente no }, provando que o destrutor roda no fim do escopo, sem chamada explícita.
Exercício 2
Dê à classe Ponto da aula um segundo construtor, sem parâmetros, que inicialize x_ e y_ com zero (usando lista de inicialização). Demonstre os dois construtores no main.
Ver resposta
✓ Resposta: Dois construtores (uma sobrecarga, no espírito do artigo Um Nome, Vários Sentidos — Sobrecarga de Funções):
#include <iostream>
class Ponto {
public:
Ponto(int x, int y) : x_(x), y_(y) {}
Ponto() : x_(0), y_(0) {} // construtor padrão, sem parâmetros
void mostra() const { std::cout << "(" << x_ << ", " << y_ << ")\n"; }
private:
int x_, y_;
};
int main() {
Ponto origem; // usa Ponto()
Ponto p(3, 4); // usa Ponto(int, int)
origem.mostra(); // (0, 0)
p.mostra(); // (3, 4)
return 0;
}
Exercício 3
Explique, em suas palavras, por que o RAII garante a liberação do recurso mesmo quando a função retorna no meio do caminho — algo que o fopen/fclose manual do C não garante sem cuidado extra.
Ver resposta
✓ Resposta: Porque a chamada do destrutor não está escrita em nenhum ponto específico do seu código — ela é inserida pelo compilador em todos os caminhos pelos quais o objeto pode sair de escopo. Se a função retorna cedo, o objeto sai de escopo ali, e o destrutor roda ali. Com fopen/fclose manual, o fclose está escrito num lugar fixo; se um return antecipado pula esse lugar, o arquivo vaza — e é preciso lembrar de fechar antes de cada return, ou recorrer a goto cleanup, o velho padrão do C. O RAII elimina essa disciplina manual: a liberação segue o objeto, não uma linha específica.
Exercício 4
A classe abaixo tem um vazamento potencial. Identifique-o e reescreva-a no estilo RAII, movendo a liberação para o destrutor.
#include <cstdlib>
class Buffer {
public:
Buffer(int n) { dados_ = (int*) std::malloc(n * sizeof(int)); }
int* dados_;
// ... e o free? quem chama?
};
Ver resposta
✓ Resposta: O vazamento: Buffer aloca com malloc no construtor mas nunca libera — quem criar um Buffer vaza a memória. A versão RAII adiciona um destrutor que libera:
#include <cstdlib>
#include <iostream>
class Buffer {
public:
Buffer(int n) : dados_(static_cast<int*>(std::malloc(n * sizeof(int)))) {}
~Buffer() {
std::free(dados_); // libera automaticamente ao fim de vida
std::cout << "buffer liberado\n";
}
private:
int* dados_ = nullptr;
};
int main() {
{ Buffer b(100); } // aloca no {, libera no }
return 0;
}
(Nota honesta: esta classe ainda é vulnerável à cópia, exatamente o tema do próximo artigo — e, na prática, usaríamos std::vector ou um ponteiro inteligente em vez de malloc. Aqui o malloc serve só para ilustrar o par aquisição/liberação.)
Exercício 5
Na classe ArquivoTexto da aula, o que aconteceria se, no main, você escrevesse ArquivoTexto b = arq; (copiando um objeto já criado)? Descreva o problema concreto que surge quando ambos os objetos forem destruídos, ligando-o à pergunta que a aula deixou para o próximo artigo.
Ver resposta
✓ Resposta: ArquivoTexto b = arq; cria b como uma cópia de arq. Como não definimos como copiar, o compilador copia membro a membro: o ponteiro f_ de b recebe o mesmo valor de f_ de arq — os dois objetos passam a apontar para o mesmo FILE. Quando o escopo termina, os dois destrutores rodam, e cada um chama std::fclose(f_) sobre o mesmo descritor. Fechar o mesmo arquivo duas vezes é comportamento indefinido (pode corromper o estado da biblioteca de C ou travar). É exatamente a pergunta que a aula plantou — "quem possui o recurso, e o que acontece na cópia?" — e a Regra dos Três/Cinco, no artigo Quando Copiar Dá Errado — a Regra dos Três e dos Cinco, é a resposta: quando sua classe possui um recurso, você precisa dizer explicitamente como ela se copia (ou proibir a cópia), para que dois donos jamais tentem liberar a mesma coisa.