C++ public e private
Na aula O que é POO, a caneta da Ana terminou com "R$ −3 e −10 no estoque", e o compilador não reclamou de nada. Desde então, a classe Produto ganhou métodos que conferem as regras e construtores que a fazem nascer certinha, mas qualquer linha do programa ainda pode escrever caneta.preco = -3; e passar por cima de tudo. Nesta aula você fecha essa porta: é o encapsulamento, o primeiro pilar da POO.
- O que fazem
public,privateeprotected. - A ler o erro
is private within this context. - Como escrever getters (métodos que leem) e setters (métodos que mudam, conferindo as regras).
- O que é uma invariante e como a classe garante que ela nunca quebre.
- Como validar no construtor, inclusive com exceção.
- Por que esconder os dados vale a pena, mesmo quando "dá mais trabalho".
Os programas desta aula foram compilados e executados na máquina fictícia da Ana, na pasta ~/cpp/encapsulamento, com o g++ 15.2 e o comando padrão g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.
A ideia: o cliente não abre a gaveta do caixa
Na lojinha da Ana, o dinheiro fica numa gaveta, atrás do balcão. O cliente não abre a gaveta e tira o troco sozinho: ele pede ao caixa. O caixa confere ("a nota é verdadeira? o troco está certo?") e só então mexe na gaveta. A gaveta é privada; o balcão, onde se fazem os pedidos, é público.
Uma classe bem feita funciona assim: os dados ficam na gaveta (private), e o resto do programa só conversa com o objeto pelo balcão, os métodos public. Como todo pedido passa pelo balcão, as regras ("preço nunca negativo", "não vende mais do que tem") são conferidas sempre, sem depender da memória de quem usa a classe.
| Especificador | Quem pode usar o membro | Na lojinha |
|---|---|---|
public: | Qualquer código: o main, outras funções, outras classes | O balcão |
private: | Só os métodos da própria classe | A gaveta do caixa |
protected: | Os métodos da própria classe e das classes "filhas" (herança) | O depósito, que só a família da dona acessa |
1. Esconder os dados com private
#include <iostream>
#include <string>
class Produto {
public:
Produto(std::string nome, int preco, int estoque)
: nome_(nome), preco_(preco), estoque_(estoque) {
}
int preco() { return preco_; }
private:
std::string nome_;
int preco_;
int estoque_;
};
int main() {
Produto caneta{"caneta", 3, 50};
caneta.preco_ = -3;
std::cout << caneta.preco() << "\n";
}
$ g++ -std=c++23 -Wall -Wextra invasao.cpp -o invasao
invasao.cpp: In function ‘int main()’:
invasao.cpp:20:12: error: ‘int Produto::preco_’ is private within this context
20 | caneta.preco_ = -3;
| ^~~~~~
invasao.cpp:14:9: note: declared private here
14 | int preco_;
| ^~~~~~
invasao.cpp:20:12: note: field ‘int Produto::preco_’ can be accessed via ‘int Produto::preco()’
20 | caneta.preco_ = -3;
| ^~~~~~
| preco()
Pronto: a invasão agora é um erro de compilação. O programa nem chega a existir. Leia a mensagem:
| Trecho | Significado |
|---|---|
In function ‘int main()’ | O problema está dentro do main, que fica fora da classe. |
‘int Produto::preco_’ is private within this context | "Produto::preco_ é privado neste contexto": daqui (o main), não se pode tocar nele. |
note: declared private here | "Foi declarado privado aqui": aponta a linha 14, dentro da seção private:. |
note: field … can be accessed via ‘int Produto::preco()’ | O g++ 15 ainda dá uma dica: "este campo pode ser acessado pelo método preco()". Ele achou um getter e sugeriu usá-lo (para ler; para mudar, não serviria). |
No editor do guia, o Clang escreve 'preco_' is a private member of 'Produto' ("preco_ é um membro privado de Produto").
- public:
- Tudo o que vem depois, até o próximo especificador, é público: o construtor e o método
preco(). - private:
- Daqui até o fim da classe, tudo é privado: os três atributos. Uma classe pode ter várias seções, em qualquer ordem; o costume é
publicprimeiro (o que os usuários da classe precisam ler) eprivateno fim (detalhes internos). - nome_, preco_, estoque_
- O sublinhado no fim é um costume, não uma regra: marca "este é um dado interno". Ele também resolve um problema da aula Métodos: um método não pode ter o mesmo nome de um atributo, e agora o método pode se chamar simplesmente
preco(). Outro costume comum ém_preco("m" de membro). - int preco() { return preco_; }
- O método está dentro da classe, então pode ler
preco_à vontade.privatenão esconde nada dos métodos da própria classe.
class e struct? Numa class, quem não está em seção nenhuma já é privado. Por isso, em Classes e Objetos, foi preciso escrever public: para o main enxergar os atributos. Agora a gente usa isso a favor.2. Getters e setters: o balcão da classe
Se os dados estão escondidos, como o resto do programa lê o preço ou muda o preço? Por métodos públicos, com nomes que já viraram costume:
- getter (de get, "pegar"): só lê um dado e o devolve. Ex.:
int preco(). - setter (de set, "definir"): muda um dado, mas antes confere se o valor novo faz sentido. Ex.:
bool mudarPreco(int novo).
#include <iostream>
#include <string>
class Produto {
public:
Produto(std::string nome, int preco, int estoque)
: nome_(nome), preco_(0), estoque_(0) {
mudarPreco(preco);
repor(estoque);
}
std::string nome() { return nome_; }
int preco() { return preco_; }
int estoque() { return estoque_; }
bool mudarPreco(int novo) {
if (novo < 0) {
std::cout << "Preço inválido: " << novo << "\n";
return false;
}
preco_ = novo;
return true;
}
bool repor(int quantidade) {
if (quantidade < 0) {
std::cout << "Quantidade inválida: " << quantidade << "\n";
return false;
}
estoque_ += quantidade;
return true;
}
bool vender(int quantidade) {
if (quantidade <= 0 || quantidade > estoque_) {
std::cout << "Venda recusada: " << quantidade << " " << nome_ << "(s)\n";
return false;
}
estoque_ -= quantidade;
return true;
}
private:
std::string nome_;
int preco_;
int estoque_;
};
int main() {
Produto caneta{"caneta", 3, 50};
caneta.mudarPreco(-3);
caneta.vender(60);
caneta.vender(2);
caneta.mudarPreco(4);
std::cout << caneta.nome() << ": R$ " << caneta.preco()
<< " (" << caneta.estoque() << " no estoque)\n";
}
$ g++ -std=c++23 -Wall -Wextra produto.cpp -o produto $ ./produto Preço inválido: -3 Venda recusada: 60 caneta(s) caneta: R$ 4 (48 no estoque)
Compare com o "R$ −3 (−10 no estoque)" da aula O que é POO. As mesmas tentativas erradas agora são recusadas, e a caneta termina num estado que faz sentido.
- std::string nome() { return nome_; }
- Getter: devolve uma cópia do nome. Quem chama pode ler, mas não consegue mudar o
nome_de dentro do objeto. Repare que não existe setter de nome: um produto não muda de nome, e a classe simplesmente não oferece essa porta. - bool mudarPreco(int novo)
- Setter com validação: se o preço for negativo, avisa e devolve
falsesem mexer em nada. Só um preço válido chega aopreco_. - bool repor(int) / bool vender(int)
- Não existe
mudarEstoque. O estoque só muda por ações com sentido na loja: repor e vender, cada uma com a sua regra. Isso é melhor que um setter genérico, que deixaria escrever qualquer número. - : nome_(nome), preco_(0), estoque_(0) { mudarPreco(preco); repor(estoque); }
- O construtor começa com valores seguros (0) e passa os valores recebidos pelos mesmos métodos que conferem as regras. Assim, nem no nascimento entra um preço negativo. (Os parâmetros se chamam
nomeepreco, e não há confusão com os atributosnome_epreco_, graças ao sublinhado.)
const depois dos parênteses, int preco() const. Ela é tão importante que tem aula própria, Métodos const. Até lá, os getters ficam sem ela.Experimente: no main, tente escrever caneta.estoque_ = 1000; e rode: leia o erro. Depois apague essa linha e tente vender 0 canetas e repor -5. Por último, pense: que regra você acrescentaria ao mudarPreco para impedir um preço absurdo, como R$ 10000?
#include <iostream>
#include <string>
class Produto {
public:
Produto(std::string nome, int preco, int estoque)
: nome_(nome), preco_(0), estoque_(0) {
mudarPreco(preco);
repor(estoque);
}
std::string nome() { return nome_; }
int preco() { return preco_; }
int estoque() { return estoque_; }
bool mudarPreco(int novo) {
if (novo < 0) {
std::cout << "Preço inválido: " << novo << "\n";
return false;
}
preco_ = novo;
return true;
}
bool repor(int quantidade) {
if (quantidade < 0) {
std::cout << "Quantidade inválida: " << quantidade << "\n";
return false;
}
estoque_ += quantidade;
return true;
}
bool vender(int quantidade) {
if (quantidade <= 0 || quantidade > estoque_) {
std::cout << "Venda recusada: " << quantidade << " " << nome_ << "(s)\n";
return false;
}
estoque_ -= quantidade;
return true;
}
private:
std::string nome_;
int preco_;
int estoque_;
};
int main() {
Produto caneta{"caneta", 3, 50};
caneta.vender(2);
std::cout << caneta.nome() << ": R$ " << caneta.preco()
<< " (" << caneta.estoque() << " no estoque)\n";
}
3. Invariantes: as regras que nunca quebram
Uma invariante é uma regra sobre os dados que vale o tempo todo, para todo objeto da classe, do nascimento à morte. As invariantes do Produto da Ana são:
preco_ >= 0(preço nunca negativo);estoque_ >= 0(estoque nunca negativo).
Quem garante as invariantes é a classe, e ela consegue garantir porque só os métodos dela mexem nos dados. Basta conferir duas coisas:
- o construtor cria o objeto já respeitando as regras;
- cada método público que muda os dados termina com as regras ainda valendo (ou recusa a mudança).
Se as duas coisas valem, nenhum código no programa inteiro, por mais errado que seja, consegue criar um produto com preço negativo. Sem o private, isso seria impossível de garantir: bastaria uma linha esquecida em algum arquivo.
struct pública resolve.Validar no construtor: devolver false não dá
Um setter pode avisar o erro devolvendo false. Um construtor não devolve nada, então não tem como dizer "não deu". No exemplo acima, a solução foi aceitar o produto com preço 0. Outra solução, mais firme, é recusar a criação com uma exceção:
#include <iostream>
#include <stdexcept>
#include <string>
class Produto {
public:
Produto(std::string nome, int preco) : nome_(nome), preco_(preco) {
if (preco < 0) {
throw std::invalid_argument("preço negativo: " + std::to_string(preco));
}
}
int preco() { return preco_; }
private:
std::string nome_;
int preco_;
};
int main() {
try {
Produto caneta{"caneta", 3};
std::cout << "Caneta criada: R$ " << caneta.preco() << "\n";
Produto errado{"lápis", -1};
std::cout << "Esta linha não aparece\n";
} catch (const std::invalid_argument& e) {
std::cout << "Não criei o produto: " << e.what() << "\n";
}
}
$ g++ -std=c++23 -Wall -Wextra excecao.cpp -o excecao $ ./excecao Caneta criada: R$ 3 Não criei o produto: preço negativo: -1
Quando o construtor lança a exceção, o objeto não chega a existir: não há como usar um errado inválido, porque o programa pula direto para o catch. std::invalid_argument ("argumento inválido", do cabeçalho <stdexcept>) é a exceção padrão para "você me passou um valor que não serve". Se ninguém capturar a exceção, o programa termina na hora (veja em Quando dá errado).
4. Por que esconder os dados
"Mas dá mais trabalho: em vez de caneta.preco, tenho de escrever um método." Dá, e vale a pena por três motivos.
Motivo 1: as regras ficam num lugar só
É o que você viu nas seções anteriores. A regra "preço ≥ 0" está no mudarPreco e em nenhum outro lugar. Se a regra mudar ("preço mínimo R$ 1"), muda-se uma linha.
Motivo 2: dá para mudar o "por dentro" sem quebrar o "por fora"
A Ana resolveu dar descontos de 10%, e reais inteiros não servem mais (10% de R$ 3 = R$ 2,70). Ela muda a classe para guardar o preço em centavos. Como o atributo era privado, ninguém fora da classe usava preco_ direto. Ela troca o atributo, ajusta os métodos, e todo o código que usava caneta.preco() continua funcionando sem mudar uma vírgula:
#include <iostream>
#include <string>
class Produto {
public:
Produto(std::string nome, int preco)
: nome_(nome), centavos_(preco * 100) {
}
int preco() { return centavos_ / 100; }
void aplicarDesconto(int porcento) {
centavos_ = centavos_ * (100 - porcento) / 100;
}
int centavos() { return centavos_; }
private:
std::string nome_;
int centavos_; // antes era: int preco_;
};
int main() {
Produto caneta{"caneta", 3};
std::cout << "Preço: R$ " << caneta.preco() << "\n";
caneta.aplicarDesconto(10);
std::cout << "Com desconto: " << caneta.centavos() << " centavos\n";
}
$ g++ -std=c++23 -Wall -Wextra centavos.cpp -o centavos $ ./centavos Preço: R$ 3 Com desconto: 270 centavos
Se o preco fosse público e espalhado por 30 arquivos, a troca para centavos exigiria caçar e corrigir cada caneta.preco do projeto. Com o atributo privado, a mudança ficou dentro da classe. Essa é a ideia de abstração: quem usa a classe depende do que ela faz (os métodos públicos), não de como ela guarda as coisas.
Motivo 3: fica fácil achar quem mexeu
Se o estoque da caneta aparecer errado, onde procurar? Com o atributo privado, só os métodos repor e vender mexem nele: basta olhar dois métodos (ou pôr um ponto de parada neles no GDB). Com o atributo público, o culpado pode estar em qualquer linha do programa.
5. protected (para a herança)
protected ("protegido") fica no meio do caminho: para o mundo de fora, funciona como private; mas as classes que herdam da classe também podem usar. Herança é assunto da aula Herança; aqui vai só um exemplo para você ver a diferença. A classe Livro é "filha" de Produto:
#include <iostream>
#include <string>
class Produto {
public:
Produto(std::string nome, int preco) : preco_(preco), nome_(nome) {
}
protected:
int preco_;
private:
std::string nome_;
};
class Livro : public Produto {
public:
Livro(std::string nome, int preco) : Produto(nome, preco) {
}
void promocao() {
preco_ = preco_ - 5; // pode: Livro é "filho" de Produto
}
};
int main() {
Livro romance{"romance", 40};
romance.promocao();
romance.preco_ = 0; // não pode: main está fora da família
}
$ g++ -std=c++23 -Wall -Wextra protegido.cpp -o protegido
protegido.cpp: In function ‘int main()’:
protegido.cpp:29:13: error: ‘int Produto::preco_’ is protected within this context
29 | romance.preco_ = 0; // não pode: main está fora da família
| ^~~~~~
protegido.cpp:10:9: note: declared protected here
10 | int preco_;
| ^~~~~~
O promocao(), dentro de Livro, pôde mexer em preco_: nenhum erro na linha 22. Já o main recebeu o mesmo tipo de erro do private, agora com protected. Se preco_ fosse private, nem o Livro conseguiria.
protected com moderação. Um atributo protected pode ser mexido por qualquer classe filha, inclusive as que outras pessoas escreverem no futuro, e aí as invariantes voltam a depender da boa vontade de muita gente. Na dúvida, deixe os atributos private e ofereça métodos protected para as filhas.6. private vale por classe, não por objeto
Um detalhe que surpreende: um método de Produto pode mexer nos dados privados de outro objeto Produto. A proteção separa "código da classe" de "código de fora", não um objeto do outro.
class Produto {
public:
Produto(std::string nome, int preco) : nome_(nome), preco_(preco) {
}
bool maisCaroQue(const Produto& outro) {
return preco_ > outro.preco_;
}
private:
std::string nome_;
int preco_;
};
int main() {
Produto caderno{"caderno", 12};
Produto caneta{"caneta", 3};
std::cout << std::boolalpha;
std::cout << caderno.maisCaroQue(caneta) << "\n";
std::cout << caneta.maisCaroQue(caderno) << "\n";
}
$ g++ -std=c++23 -Wall -Wextra irmaos.cpp -o irmaos $ ./irmaos true false
outro.preco_ é privado, mas o código está dentro de um método de Produto, então pode. Faz sentido: quem escreveu a classe conhece as regras de todos os produtos. (std::boolalpha faz o cout mostrar true/false em vez de 1/0; veja bool.)
Formas diferentes: qual usar
| Decisão | Opções | Recomendação |
|---|---|---|
| Nome do getter | preco() ou getPreco() | Os dois são comuns; a biblioteca padrão usa o estilo size(), sem "get". Escolha um e use em todo o projeto. |
| Nome do setter | setPreco(int) ou um verbo da loja (mudarPreco, vender, repor) | Prefira ações com sentido. setEstoque(qualquer número) é quase um atributo público com outro nome. |
| Nome dos atributos privados | preco_, m_preco ou só preco | Um sinal (_ ou m_) evita o conflito com o getter. Sem sinal, use this-> quando houver ambiguidade. |
| Valor inválido num setter | Devolver false, ignorar, ou lançar exceção | false quando o erro é esperado e quem chama pode reagir; exceção quando o valor errado é um defeito do programa. Ignorar em silêncio, nunca. |
| Valor inválido no construtor | Corrigir para um valor seguro ou lançar exceção | Exceção, se um objeto "corrigido" puder enganar alguém. |
| Getter e setter para tudo? | — | Não. Getter só para o que alguém precisa ler; setter só se a mudança fizer sentido. Sem regra nenhuma? Talvez seja uma struct. |
Quando dá errado
É o erro da seção 1: error: ‘int Produto::preco_’ is private within this context, com a nota declared private here. Como resolver: não "conserte" tornando o atributo público. Use o método que a classe oferece (caneta.preco() para ler, caneta.mudarPreco(4) para mudar). Se não existe método para o que você quer, pergunte-se se a classe deveria deixar fazer isso; se sim, escreva o método, com as regras.
#include <iostream>
#include <string>
class Produto {
Produto(std::string nome, int preco) : nome_(nome), preco_(preco) {
}
public:
int preco() { return preco_; }
private:
std::string nome_;
int preco_;
};
int main() {
Produto caneta{"caneta", 3};
std::cout << caneta.preco() << "\n";
}
$ g++ -std=c++23 -Wall -Wextra construtorprivado.cpp -o construtorprivado
construtorprivado.cpp: In function ‘int main()’:
construtorprivado.cpp:17:31: error: ‘Produto::Produto(std::string, int)’ is private within this context
17 | Produto caneta{"caneta", 3};
| ^
construtorprivado.cpp:5:5: note: declared private here
5 | Produto(std::string nome, int preco) : nome_(nome), preco_(preco) {
| ^~~~~~~
O que aconteceu: o construtor foi escrito antes do public:, e numa class o começo é privado. Ninguém de fora consegue criar um produto. É o mesmo erro, agora apontando para o construtor. O Clang diz calling a private constructor of class 'Produto'.
Como resolver: comece a classe com public: e ponha os construtores logo abaixo. (Construtor privado de propósito existe, em técnicas avançadas, mas não é o que você quer agora.)
int main() {
Produto errado{"lápis", -1};
std::cout << "Esta linha não aparece\n";
}
$ ./semcatch terminate called after throwing an instance of 'std::invalid_argument' what(): preço negativo: -1 Abortado (imagem do núcleo gravada) ./semcatch $ echo $? 134
O que aconteceu: a classe é a mesma de excecao.cpp, mas o main não tem try/catch. A exceção subiu até o fim, e o C++ chamou std::terminate: "terminate foi chamado depois de lançar um std::invalid_argument", seguido da mensagem (what()). O Bash avisa que o programa foi abortado, e o código de saída 134 (128 + 6, o sinal de abortar) confirma. A validação funcionou, nenhum produto inválido existiu, mas o programa inteiro parou.
Como resolver: capture a exceção onde der para reagir (pedir outro preço, pular o produto), como em excecao.cpp. Veja Exceções.
Referência rápida
| Escrita | O que faz |
|---|---|
public: | O que vem depois pode ser usado por qualquer código. |
private: | Só os métodos da própria classe usam. É o padrão numa class. |
protected: | A própria classe e as classes filhas usam; de fora, é como private. |
int preco() { return preco_; } | Getter: devolve um dado privado. |
bool mudarPreco(int n) { if (n < 0) return false; preco_ = n; return true; } | Setter com validação. |
throw std::invalid_argument("…"); | Recusa um valor inválido no construtor (<stdexcept>). |
is private within this context | Erro: código de fora tentou usar um membro privado. |
is protected within this context | Erro: código de fora da família tentou usar um membro protegido. |
Exercícios
1. A gaveta
Complete o especificador que esconde os atributos do resto do programa:
int saldo_;
2. O getter
Complete o getter que devolve o atributo privado saldo_:
3. A regra
No setter bool mudarPreco(int novo), complete a condição que recusa preço negativo:
4. Quem pode ler um atributo private da classe Produto?
5. O que é uma invariante?
6. A Ana trocou o atributo privado preco_ por centavos_ e ajustou os métodos. O que acontece com o código fora da classe que usava caneta.preco()?
Missão final: o caixa da lojinha
Missão: escreva a classe Caixa com o atributo privado saldo_ (começa em 0) e três métodos públicos: depositar(int valor), que recusa valores ≤ 0 mostrando Depósito recusado: VALOR; sacar(int valor), que recusa valores ≤ 0 ou maiores que o saldo mostrando Saque recusado: VALOR; e o getter saldo(). Com o main pronto, a saída deve ser:
Depósito recusado: -50 Saque recusado: 500 Saldo: R$ 70
#include <iostream>
class Caixa {
// escreva a classe aqui
};
int main() {
Caixa caixa;
caixa.depositar(100);
caixa.depositar(-50);
caixa.sacar(500);
caixa.sacar(30);
std::cout << "Saldo: R$ " << caixa.saldo() << "\n";
}
#include <iostream>
class Caixa {
public:
bool depositar(int valor) {
if (valor <= 0) {
std::cout << "Depósito recusado: " << valor << "\n";
return false;
}
saldo_ += valor;
return true;
}
bool sacar(int valor) {
if (valor <= 0 || valor > saldo_) {
std::cout << "Saque recusado: " << valor << "\n";
return false;
}
saldo_ -= valor;
return true;
}
int saldo() { return saldo_; }
private:
int saldo_ = 0;
};
int main() {
Caixa caixa;
caixa.depositar(100);
caixa.depositar(-50);
caixa.sacar(500);
caixa.sacar(30);
std::cout << "Saldo: R$ " << caixa.saldo() << "\n";
}
Resumo
- Encapsulamento: os dados ficam
private; o resto do programa usa os métodospublic. Tentar mexer de fora dáis private within this context. - Getters leem (
int preco()); setters mudam conferindo as regras (bool mudarPreco(int)). Prefira ações com sentido (vender,repor) a setters genéricos, e não crie getter e setter para tudo. - Invariantes são as regras que valem sempre ("preço ≥ 0"). O construtor as estabelece; cada método público as preserva. Só funciona porque os dados são privados.
- Construtor não devolve
false: corrija para um valor seguro ou lancestd::invalid_argument. - Dados escondidos permitem mudar o "por dentro" (reais → centavos) sem quebrar quem usa a classe, e deixam fácil achar quem mexeu.
protected: comoprivate, mas as classes filhas também acessam. Use com moderação.- Próxima aula: o que acontece quando o parâmetro tem o mesmo nome do atributo, e como encadear chamadas, em this.