9. Classes, encapsulamento e construtores¶
Vídeos
23 - Introdução a POO (conceitos básicos) · 24 - Introdução a POO (exemplo em C++) · 25 - Setters and Getters · 26 - Construtores
Classe e objeto: a planta e a casa¶
Antes de escrever a primeira class, vale fixar o vocabulário — porque ele vai
aparecer em toda mensagem de erro do compilador daqui para frente. E a boa
notícia é que você já usa tudo isso desde o capítulo 1, só não com esses
nomes.
Nessas duas linhas:
std::stringé uma classe — um tipo que alguém escreveu, com dados e operações juntos.nomeé um objeto dessa classe: uma variável concreta, com um valor concreto, ocupando um lugar na memória..size()é um método — uma função que pertence à classe e é chamada através de um objeto.- E
nomeguarda, por dentro, os campos de que astd::stringprecisa para funcionar (um ponteiro para os caracteres, um tamanho, uma capacidade). Você nunca viu esses campos, nunca precisou vê-los, e é exatamente esse o ponto.
A imagem que costuma ajudar: a classe é a planta da casa; o objeto é a casa
construída. A planta é desenhada uma vez e diz quais são os quartos; as casas
são muitas, e cada uma tem os seus próprios móveis. Escrever class Conta { ... }
não cria conta nenhuma — só descreve como uma conta é. Cada
ContaBancaria c("Ana", 100.0); é que constrói uma casa.
| Termo | O que é | No exemplo acima |
|---|---|---|
| Classe | o tipo, a planta | std::string |
| Objeto (ou instância) | uma variável daquele tipo, a casa | nome |
| Membro | qualquer coisa declarada dentro da classe | size, e os campos internos |
| Campo (ou atributo) | membro que guarda dado | o tamanho guardado pela string |
| Método | membro que é função | size() |
Instância é só um sinônimo de objeto: "nome é uma instância de std::string"
quer dizer "nome é um objeto da classe std::string".
De struct para class: o que muda¶
Você já vem usando struct com métodos desde a Unidade 1 — Retangulo, Aluno,
Produto. Então, na prática, você já escreveu classes: tecnicamente, em C++,
struct e class são a mesma coisa, com uma única diferença — os membros de
struct são public por padrão, e os de class são private por padrão.
struct Ponto { // campos publicos por padrao
double x, y;
};
class Ponto2 { // campos privados por padrao
double x, y; // ninguem de fora enxerga x e y
};
public significa "qualquer código pode acessar"; private, "só os métodos da
própria classe podem acessar". São esses dois rótulos que carregam toda a
diferença prática entre struct e class, e é sobre eles que o resto do
capítulo fala.
Convenção do curso (e da maior parte do código C++ real): use struct para
agrupar dados simples, sem nenhuma regra a proteger (como Ponto, ou o Aluno
das listas anteriores). Use class quando o tipo tem um invariante — uma
regra que precisa ser sempre verdadeira, e que código externo não deveria poder
quebrar.
Encapsulamento: por que esconder os campos¶
Considere uma conta bancária representada como struct. A regra do domínio é
simples: saldo nunca é negativo.
Pare e pense
Essa última linha compila? E se compilar, quem no programa vai impedir que ela aconteça?
Compila sem um aviso sequer. Nada — nem o compilador, nem a struct — impede
código em qualquer lugar do programa de atribuir um saldo negativo direto no
campo, mesmo que a regra de negócio diga o contrário. O campo está exposto, e uma
regra que depende da boa vontade de todo mundo que toca no dado não é uma regra:
basta um c.saldo = ... esquecido num canto do sistema para o invariante cair.
Com class, os campos ficam private por padrão, e o acesso passa a ser
controlado por métodos public:
class ContaBancaria {
private:
double saldo_;
public:
void deposita(double valor) {
if (valor > 0) {
saldo_ += valor;
}
}
bool saca(double valor) {
if (valor > 0 && valor <= saldo_) {
saldo_ -= valor;
return true;
}
return false; // saque invalido: nao mexe no saldo
}
double saldo() const {
return saldo_;
}
};
Agora saldo_ é inacessível de fora da classe — a única forma de alterá-lo é
através de deposita/saca, que garantem a regra "saldo nunca fica
negativo". Isso é encapsulamento: a classe é responsável por proteger seu próprio
invariante, em vez de confiar que todo código externo vai se comportar direito.
E repare no ganho de manutenção: se amanhã a regra mudar (passa a existir limite
de crédito, por exemplo), existe um lugar no programa para mexer.
Note também que só há dois rótulos no corpo da classe, e que tudo o que vem
depois de um deles obedece a ele até o próximo: private: vale para saldo_,
public: vale para os três métodos abaixo. Você pode repetir os rótulos quantas
vezes quiser; a convenção é agrupar, como acima.
Convenção de nome: um sufixo _ (saldo_) para campos privados é comum em C++ e
evita colisão com o nome do getter (saldo()).
Invariante não é assunto só de banco
Uma Fracao cujo denominador nunca pode ser zero, uma Data em que o mês
fica entre 1 e 12, um Termometro que só aceita leituras acima do zero
absoluto: todos têm invariante, e todos são bons candidatos a class. A
pergunta que decide é sempre a mesma — existe algum valor dos meus campos
que seria absurdo? Se existe, esconda os campos.
this: o objeto em que o método foi chamado¶
Dentro de deposita, escrevemos saldo_ += valor. Mas de qual objeto é esse
saldo_? Se existem três contas no programa, cada uma com seu próprio saldo_,
qual delas o método altera?
A resposta é: aquela através da qual o método foi chamado. Em
conta_da_ana.deposita(50), o saldo_ dentro do método é o saldo_ da
conta_da_ana. C++ passa esse objeto para o método por baixo dos panos, num
ponteiro chamado this — e saldo_, dentro de um método, é apenas um atalho
para this->saldo_.
void deposita(double valor) {
if (valor > 0) {
this->saldo_ += valor; // exatamente o mesmo que 'saldo_ += valor'
}
}
Na prática você quase nunca escreve this->, porque o atalho é mais limpo. O
caso em que ele se torna obrigatório é quando um parâmetro tem o mesmo nome
do campo — aí o nome sozinho se refere ao parâmetro, e só this-> alcança o
campo:
void define_saldo(double saldo_) {
this->saldo_ = saldo_; // à esquerda o campo, à direita o parametro
}
(É justamente para não precisar desse malabarismo que a convenção do sufixo _
nos campos existe.) Vale conhecer o this por um segundo motivo, bem prático:
ele aparece com esse nome nas mensagens de erro do compilador, como você vai ver
na seção de erros comuns no fim do capítulo.
Getters e setters¶
Um getter lê um campo privado; um setter o altera, geralmente validando.
Na nossa conta, saldo() já é um getter. Faltava o titular:
class ContaBancaria {
private:
std::string titular_;
double saldo_;
public:
std::string titular() const { return titular_; } // getter
double saldo() const { return saldo_; } // getter
void define_titular(const std::string& novo_titular) { // setter, com validacao
if (!novo_titular.empty()) {
titular_ = novo_titular;
}
}
// ... deposita/saca como antes
};
Repare que não existe um define_saldo. Não é esquecimento: o saldo só pode
mudar por depósito ou saque, e expor um setter para ele jogaria fora todo o
encapsulamento que acabamos de construir — seria o c.saldo = -50.0 de volta,
agora com um nome mais respeitável.
É esse o recado da seção: getters e setters não são "obrigatórios para toda
classe", e uma classe que oferece um par deles para cada campo é uma struct com
passos a mais. Ofereça o que o design pedir, com a validação que cada campo
pedir; a ausência de um método também é uma decisão de projeto.
Construtores¶
Um construtor é um método especial, com o mesmo nome da classe e sem tipo de retorno, chamado automaticamente quando um objeto é criado. Ele garante que o objeto nasce já em um estado válido — a primeira linha de defesa do invariante. Sem ele, os métodos protegem a conta a partir do segundo instante de vida, mas o primeiro fica descoberto.
class ContaBancaria {
private:
std::string titular_;
double saldo_;
public:
ContaBancaria(std::string titular, double saldo_inicial) {
titular_ = titular;
saldo_ = (saldo_inicial >= 0) ? saldo_inicial : 0.0;
}
// ... deposita/saca/saldo como antes
};
ContaBancaria c("Ana", 100.0); // chama o construtor automaticamente
Pare e pense
Com esse construtor escrito, a linha ContaBancaria c2; — sem argumento
nenhum — compila?
Não compila. E é bom que não compile.
Se você não escrever construtor nenhum, o compilador gera um construtor
padrão (sem argumentos) que não inicializa campos de tipo primitivo — um
saldo_ assim pode começar com lixo de memória, e o objeto nasce inválido
antes de qualquer método ter chance de opinar. Mas, assim que você escreve
qualquer construtor, esse padrão automático desaparece. É por isso que
ContaBancaria c2; passa a ser erro de compilação: a classe agora exige os dados
que garantem o invariante, e o compilador cobra isso de quem cria a conta.
Lista de inicialização (member initializer list)¶
A forma idiomática de inicializar campos não é atribuir dentro do corpo do
construtor, e sim usar a lista de inicialização, entre : e {:
class ContaBancaria {
private:
std::string titular_;
double saldo_;
public:
ContaBancaria(std::string titular, double saldo_inicial)
: titular_(titular), saldo_(saldo_inicial >= 0 ? saldo_inicial : 0.0) {
// corpo pode ficar vazio, ou conter validacao mais complexa
}
};
Pare e pense
As duas versões do construtor — atribuir no corpo e inicializar na lista — terminam com o objeto no mesmo estado. Por que, então, preferir a segunda?
Por dois motivos. O primeiro é que campos const e campos que são referências
só podem ser inicializados dessa forma: atribuição no corpo não compila para
eles, porque atribuir pressupõe que o campo já existe com algum valor, e
const/referência não admite "existir primeiro e receber valor depois", em dois
passos.
O segundo vale para qualquer campo: a lista de inicialização inicializa
diretamente, com o valor final; atribuir no corpo primeiro constrói o campo
com um valor padrão e depois o sobrescreve. Para um double a diferença é
irrelevante, mas para um std::string titular_ são duas operações onde bastava
uma — a string é criada vazia e em seguida recebe o conteúdo. Um passo
desnecessário, em todo objeto criado.
Regra prática: inicialize na lista, sempre; use o corpo só para validação que não cabe numa expressão.
Múltiplos construtores (sobrecarga)¶
Assim como funções (capítulo 2), construtores podem ser sobrecarregados — vários construtores com o mesmo nome, distinguidos pelos parâmetros:
class Retangulo {
private:
double largura_;
double altura_;
public:
Retangulo(double largura, double altura)
: largura_(largura), altura_(altura) {}
explicit Retangulo(double lado) // construtor para quadrado
: largura_(lado), altura_(lado) {}
double area() const { return largura_ * altura_; }
};
Retangulo r1(4.0, 3.0); // usa o primeiro construtor
Retangulo r2(5.0); // usa o segundo -- um quadrado 5x5
A palavra explicit no segundo construtor impede que o compilador use esse
construtor para conversões implícitas indesejadas — sem ela, um double solto
passado a uma função que espera Retangulo viraria silenciosamente um quadrado,
o que quase nunca é a intenção de quem escreveu a chamada. Regra prática: marque
explicit todo construtor de um argumento só, a menos que você queira mesmo
permitir a conversão implícita.
Método const (revisão)¶
Igual ao que você já viu com struct na Unidade 1: um método marcado const
promete não alterar os campos do objeto, e o compilador garante isso. Todo getter
deveria ser const.
double saldo() const { return saldo_; } // so le -- const
void deposita(double valor) { ... } // altera saldo_ -- sem const
Erros comuns¶
"error: passing ‘const ContaBancaria’ as ‘this’ argument discards qualifiers" —
você tentou chamar um método não-const (deposita) através de uma referência
const ContaBancaria&. Agora que você sabe o que é o this, a mensagem se lê
inteira: o objeto que chegou ao método é const, e um método que altera campos
não pode receber um objeto const — é exatamente a garantia que const promete.
"error: ‘saldo_’ is private within this context" — você tentou ler ou
escrever um campo privado de fora da classe (algo como c.saldo_). Não é um bug
a contornar: é o encapsulamento funcionando. Use o getter, ou pergunte-se se
falta um método na classe que faça o que você queria fazer na mão.
"error: use of deleted function ‘ContaBancaria::ContaBancaria()’" — você
tentou criar um objeto sem argumentos (ContaBancaria c;), mas a classe só tem
construtor(es) que exigem argumentos. Forneça os argumentos, ou adicione um
construtor sem argumentos se fizer sentido para o domínio.
Esquecer const no getter e o compilador reclamar em outro lugar — mesmo
padrão do capítulo 3: se você tentar chamar um getter não-const a partir de um
objeto const T&, o compilador barra. Marque getters como const por padrão.
O que vem a seguir¶
O construtor resolveu uma das pontas do ciclo de vida do objeto: o
nascimento. Ele garante que nenhuma ContaBancaria existe em estado inválido,
nem por um instante.
Falta a outra ponta. O que acontece quando o objeto morre — quando a variável
sai de escopo e o programa segue adiante? Enquanto a classe só guarda double e
std::string, a resposta é "nada de especial". Mas assim que ela passa a ser
dona de um recurso — memória alocada, um arquivo aberto, uma conexão —, alguém
precisa devolvê-lo, e esquecer disso é a fonte clássica de bugs em C. É o assunto
do capítulo 10, o mais importante do curso.
Resumo¶
- Classe é a planta, objeto (ou instância) é a casa construída. Campo é membro que guarda dado; método é membro que é função.
classtem membrosprivatepor padrão;struct,public. Useclassquando há um invariante a proteger.- Encapsulamento: esconder os campos e expor só métodos que garantem as regras do domínio — assim a regra vive num lugar só.
thisé o objeto em que o método foi chamado;campo_dentro de um método é atalho parathis->campo_.- Getters/setters: nem todo campo precisa dos dois — a ausência de um setter também é decisão de projeto.
- Construtor: garante que o objeto nasce em estado válido; sem construtor nenhum escrito, campos primitivos podem começar com lixo.
- Lista de inicialização (
: campo(valor)) é a forma idiomática de inicializar — obrigatória paraconst/referência, mais eficiente para o resto. explicitem construtores de um argumento evita conversões implícitas surpreendentes.