Fechamos a Fase 2 sabendo gerenciar recursos com maestria. Mas até aqui nossas classes foram embrulhos modestos — um arquivo, um buffer, um ponteiro inteligente por dentro. A partir de agora subimos um degrau: vamos projetar abstrações de verdade, tipos que não apenas guardam dados, mas garantem que esses dados façam sempre sentido. Essa é a promessa central da orientação a objetos em C++, e ela começa com uma palavra que você conhece de struct no C mas que ganha músculos aqui: encapsulamento. Ao fim desta aula, você entenderá por que esconder dados é um ato de projeto, não de paranoia, e conhecerá o ponteiro this, que responde à pergunta "de qual objeto estamos falando?".
O que muda de struct para class
No C, uma struct é transparente: qualquer um mexe em qualquer campo, a qualquer hora. Isso é liberdade e é perigo. Considere uma fração — numerador e denominador. Nada em C impede alguém de zerar o denominador e detonar a próxima divisão. Em C++, uma class pode proteger seus dados, expondo só o que é seguro. A diferença técnica entre struct e class é mínima (visibilidade padrão), mas a intenção idiomática é clara: use class quando o tipo tem invariantes a proteger.
#include <iostream>
class Fracao {
public:
// Construtor: única porta de entrada. Garante o invariante desde o nascimento.
Fracao(int numerador, int denominador)
: num_(numerador), den_(denominador) {
if (den_ == 0) {
den_ = 1; // política simples: denominador nulo vira 1 (evita divisão por zero)
std::cout << "aviso: denominador zero corrigido para 1\n";
}
}
double valor() const { return static_cast<double>(num_) / den_; }
void mostra() const {
std::cout << num_ << "/" << den_ << '\n';
}
private:
int num_; // escondido: ninguém de fora mexe direto
int den_; // escondido: protegido contra virar zero
};
int main() {
Fracao f(3, 4);
f.mostra(); // 3/4
std::cout << f.valor() << '\n'; // 0.75
// f.den_ = 0; // ERRO de compilação: den_ é privado. O invariante está a salvo.
Fracao ruim(1, 0); // o construtor corrige para 1
ruim.mostra(); // 1/1
return 0;
}
Os membros sob private: são invisíveis fora da classe. Toda manipulação passa pela interface public: — o construtor e os métodos —, e é aí que a classe impõe suas regras. den_ nunca será zero, porque o único caminho para defini-lo (o construtor) checa. Essa garantia que a classe mantém sobre seu próprio estado tem um nome que você vai ouvir muito: invariante.
Invariantes: a razão de ser do encapsulamento
Um invariante é uma verdade que sempre vale sobre um objeto, do nascimento à morte. "O denominador nunca é zero", "o tamanho nunca é negativo", "o saldo nunca fica abaixo do limite". Encapsular não é esconder por esconder; é criar uma fronteira onde essas verdades são estabelecidas (no construtor) e mantidas (nos métodos). Se qualquer código pudesse alterar os campos diretamente, nenhum invariante seria confiável — bastaria um deslize em qualquer canto do programa para quebrá-lo. Ao trancar os dados como private e obrigar todo acesso a passar por métodos que respeitam as regras, você concentra a responsabilidade num só lugar. É a diferença entre "espero que ninguém estrague isto" e "é impossível estragar isto de fora".
Getters, setters e a tentação de expor tudo
O padrão comum de dar acesso controlado é o par getter/setter: métodos que leem e escrevem um campo com validação. Mas há uma armadilha honesta aqui, e vou apontá-la de saída: um setter que simplesmente atribui sem checar nada é apenas um campo público disfarçado — deu trabalho e não protegeu invariante nenhum. O bom setter valida:
#include <iostream>
class Termostato {
public:
Termostato() : temp_(20) {} // começa numa temperatura segura
int temperatura() const { return temp_; } // getter: só lê
// Setter COM validação: mantém o invariante 10 <= temp <= 30.
void ajustar(int nova) {
if (nova < 10) temp_ = 10;
else if (nova > 30) temp_ = 30;
else temp_ = nova;
}
private:
int temp_; // invariante: sempre entre 10 e 30
};
int main() {
Termostato t;
t.ajustar(100); // clampeado para 30
std::cout << t.temperatura() << '\n'; // 30
t.ajustar(22);
std::cout << t.temperatura() << '\n'; // 22
return 0;
}
Repare no const do getter — a disciplina do artigo A Disciplina do const — Promessas que o Compilador Cobra aplicada a métodos: temperatura() const promete não modificar o objeto, então pode ser chamado até sobre um Termostato constante. Adote isso como reflexo: todo método que só lê o objeto leva const.
O ponteiro this: quem sou eu?
Quando você chama t.ajustar(22), o método precisa saber sobre qual objeto agir — o t, e não outro Termostato qualquer. O C++ passa, invisível, um ponteiro para o objeto atual chamado this. Dentro de qualquer método, this aponta para o objeto sobre o qual o método foi invocado, e temp_ é, na verdade, this->temp_. Quase sempre o this fica implícito, mas ele aparece explicitamente em situações reais. Uma clássica: quando o parâmetro tem o mesmo nome do membro.
#include <iostream>
class Contador {
public:
Contador(int valor) {
// Parâmetro 'valor' e membro 'valor_' — sem ambiguidade aqui por causa do _.
// Mas se o membro se chamasse 'valor', precisaríamos de this->valor = valor;
valor_ = valor;
}
// Retornar *this permite ENCADEAR chamadas: c.incrementa().incrementa();
Contador& incrementa() {
++valor_;
return *this; // devolve referência ao próprio objeto
}
int atual() const { return valor_; }
private:
int valor_;
};
int main() {
Contador c(0);
c.incrementa().incrementa().incrementa(); // encadeamento graças ao *this
std::cout << c.atual() << '\n'; // 3
return 0;
}
Dois usos de this que você verá o tempo todo: desambiguar nomes (this->membro = membro;) e devolver *this para permitir encadeamento de métodos — o mesmo mecanismo que fez operator= retornar *this no artigo Quando Copiar Dá Errado — a Regra dos Três e dos Cinco. Você já tinha visto this de relance ali; agora sabe o que ele é. Por convenção, este curso usa o sufixo _ nos membros (temp_, num_) justamente para evitar a colisão de nomes e tornar o this-> quase nunca necessário — mas é bom reconhecê-lo, porque muito código por aí não segue essa convenção.
Juntando tudo: uma abstração completa
Reunindo encapsulamento, invariante, validação, const e this, eis uma pequena classe ContaBancaria que protege um invariante real — o saldo nunca fica negativo — no espírito de "cada artigo é o curso em miniatura":
#include <iostream>
#include <string>
class ContaBancaria {
public:
ContaBancaria(std::string titular, int saldo_inicial)
: titular_(std::move(titular)), saldo_(saldo_inicial < 0 ? 0 : saldo_inicial) {}
// Deposita: só aceita valores positivos (mantém a coerência).
ContaBancaria& depositar(int valor) {
if (valor > 0) saldo_ += valor;
return *this; // permite encadear: conta.depositar(10).depositar(5);
}
// Saca: respeita o invariante "saldo nunca negativo".
bool sacar(int valor) {
if (valor > 0 && valor <= saldo_) {
saldo_ -= valor;
return true;
}
return false; // recusa saque inválido em vez de quebrar o invariante
}
int saldo() const { return saldo_; } // getter só-leitura
const std::string& titular() const { return titular_; }
private:
std::string titular_; // std::string membro → Regra do Zero (Artigo Quando Copiar Dá Errado)
int saldo_; // invariante: saldo_ >= 0
};
int main() {
ContaBancaria conta("Ana", 100);
conta.depositar(50).depositar(25); // encadeado
std::cout << conta.titular() << ": " << conta.saldo() << '\n'; // Ana: 175
if (!conta.sacar(1000))
std::cout << "saque recusado; saldo preservado: " << conta.saldo() << '\n';
return 0;
}
Note como todas as lições convivem: titular_ é um std::string, então a classe herda cópia/movimento corretos de graça (Regra do Zero); os getters são const; depositar devolve *this para encadear; e o invariante "saldo não-negativo" é impossível de violar de fora. Isso é projetar uma abstração: a classe é a guardiã das suas próprias regras.
O que separa uma class de um agregado de dados não é a palavra-chave, é a existência de um invariante — uma afirmação que precisa valer durante toda a vida do objeto e que ninguém de fora pode quebrar. É isso que dá sentido ao encapsulamento: o construtor estabelece o invariante, os métodos o preservam, e o privado impede que alguém o viole por engano. Onde não há invariante a defender, getter e setter para cada campo apenas reescrevem uma struct com mais cerimônia.
Fontes e leituras recomendadas
- Bjarne Stroustrup, A Tour of C++ (3ª ed.), capítulo sobre classes: a apresentação do encapsulamento e da distinção
struct/classpelo criador da linguagem. - cppreference.com/w/cpp/language/access: a referência técnica sobre
public,privateeprotected, com as regras precisas de visibilidade. - cppreference.com/w/cpp/language/this: a especificação do ponteiro
this, seus usos e seu tipo em métodosconst. - ISO C++ Core Guidelines, seção "C" (Classes), regras C.2 ("Use
classif the class has an invariant") e C.9 ("Minimize exposure of members"): as diretrizes que justificam tudo o que vimos hoje. - Scott Meyers, Effective C++ (3ª ed.), Item 22 ("Declare data members
private"): o argumento clássico a favor do encapsulamento estrito.
Exercícios
Exercício 1
Escreva uma classe Retangulo com largura e altura privadas, um construtor que rejeite valores negativos (tratando-os como zero), e métodos const area() e perimetro(). Demonstre no main.
Ver resposta
✓ Resposta: A classe:
#include <iostream>
class Retangulo {
public:
Retangulo(int largura, int altura)
: largura_(largura < 0 ? 0 : largura),
altura_(altura < 0 ? 0 : altura) {}
int area() const { return largura_ * altura_; }
int perimetro() const { return 2 * (largura_ + altura_); }
private:
int largura_;
int altura_;
};
int main() {
Retangulo r(3, 4);
std::cout << "área: " << r.area() << ", perímetro: " << r.perimetro() << '\n';
Retangulo degenerado(-5, 10); // largura vira 0
std::cout << "área degenerada: " << degenerado.area() << '\n'; // 0
return 0;
}
O construtor é a porta única: os valores negativos são corrigidos ali, e os getters const só leem.
Exercício 2
A classe abaixo expõe um setter inútil. Explique por que ele não protege invariante algum e reescreva-o para garantir que idade_ fique entre 0 e 150.
class Pessoa {
public:
void set_idade(int i) { idade_ = i; } // por que isto é só um campo público disfarçado?
int idade() const { return idade_; }
private:
int idade_;
};
Ver resposta
✓ Resposta: O setter set_idade(int i) { idade_ = i; } não valida nada: aceita idades negativas ou absurdas, exatamente como se idade_ fosse público — deu o trabalho de escrever um método sem ganhar proteção nenhuma. A versão que protege o invariante:
class Pessoa {
public:
void set_idade(int i) {
if (i < 0) idade_ = 0;
else if (i > 150) idade_ = 150;
else idade_ = i;
}
int idade() const { return idade_; }
private:
int idade_ = 0; // invariante: 0 <= idade_ <= 150
};
Agora o método faz jus à sua existência: nenhuma idade fora da faixa entra no objeto.
Exercício 3
Explique o papel do return *this; no método depositar da classe ContaBancaria. O que deixaria de funcionar se o método retornasse void?
Ver resposta
✓ Resposta: return *this; devolve uma referência ao próprio objeto sobre o qual depositar foi chamado, o que permite encadear chamadas: conta.depositar(50).depositar(25) — o resultado da primeira depositar é a própria conta, sobre a qual a segunda opera. Se o método retornasse void, conta.depositar(50).depositar(25) não compilaria, porque void não tem método depositar; você teria de escrever duas instruções separadas. O *this é o que viabiliza a interface fluente.
Exercício 4
Dada a classe Contador da aula, adicione um método decrementa() que também encadeia e que respeite o invariante "o valor nunca fica abaixo de zero". Demonstre um encadeamento misto de incrementa() e decrementa().
Ver resposta
✓ Resposta: Com o invariante de não ficar negativo:
#include <iostream>
class Contador {
public:
Contador(int valor) : valor_(valor < 0 ? 0 : valor) {}
Contador& incrementa() { ++valor_; return *this; }
Contador& decrementa() {
if (valor_ > 0) --valor_; // respeita "nunca abaixo de zero"
return *this;
}
int atual() const { return valor_; }
private:
int valor_;
};
int main() {
Contador c(1);
c.incrementa().incrementa().decrementa().decrementa().decrementa();
// 1 → 2 → 3 → 2 → 1 → 0 (o último decrementa não passa de 0)
std::cout << c.atual() << '\n'; // 0
return 0;
}
Ambos retornam Contador&, então encadeiam livremente, e decrementa protege o piso zero.
Exercício 5
Projete uma classe Estoque que mantenha o invariante "quantidade sempre entre 0 e uma capacidade máxima fixada na construção". Ofereça adicionar(int) e remover(int) que respeitem os limites (sem estourar nem ficar negativo) e um getter const. Explique qual é o invariante e onde exatamente ele é protegido.
Ver resposta
✓ Resposta: A classe:
#include <iostream>
class Estoque {
public:
Estoque(int capacidade, int inicial = 0)
: capacidade_(capacidade < 0 ? 0 : capacidade),
quantidade_(0) {
adicionar(inicial); // reusa a lógica validada em vez de atribuir cru
}
void adicionar(int n) {
if (n <= 0) return;
if (quantidade_ + n > capacidade_) quantidade_ = capacidade_; // não estoura
else quantidade_ += n;
}
void remover(int n) {
if (n <= 0) return;
if (n > quantidade_) quantidade_ = 0; // não fica negativo
else quantidade_ -= n;
}
int quantidade() const { return quantidade_; }
int capacidade() const { return capacidade_; }
private:
int capacidade_; // fixada na construção
int quantidade_; // invariante: 0 <= quantidade_ <= capacidade_
};
int main() {
Estoque e(100, 30);
e.adicionar(90); // 30+90=120 > 100 → 100
std::cout << e.quantidade() << '\n'; // 100
e.remover(150); // não fica negativo → 0
std::cout << e.quantidade() << '\n'; // 0
return 0;
}
O invariante é 0 <= quantidade_ <= capacidade_. Ele é protegido em três lugares que constituem toda a superfície de escrita do objeto: o construtor (que delega para adicionar, reaproveitando a validação em vez de duplicá-la), o adicionar (que impede ultrapassar a capacidade) e o remover (que impede ficar negativo). Como não há como escrever em quantidade_ de fora, o invariante é inquebrável.