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.

Você vai aprender
  • O que fazem public, private e protected.
  • 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.

EspecificadorQuem pode usar o membroNa lojinha
public:Qualquer código: o main, outras funções, outras classesO balcão
private:Só os métodos da própria classeA 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:

TrechoSignificado
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 é public primeiro (o que os usuários da classe precisam ler) e private no 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. private não esconde nada dos métodos da própria classe.
Lembra da diferença entre 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 false sem mexer em nada. Só um preço válido chega ao preco_.
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 nome e preco, e não há confusão com os atributos nome_ e preco_, graças ao sublinhado.)
Falta um detalhe nos getters. Getters só leem, e o C++ tem uma forma de deixar isso escrito e conferido pelo compilador: a palavra 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:

  1. preco_ >= 0 (preço nunca negativo);
  2. 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.

Dica de projeto: antes de escrever uma classe, escreva num comentário as invariantes dela. Elas dizem quais dados devem ser privados (os que têm regra) e quais métodos você precisa (os que mudam esses dados respeitando a regra). Um dado sem regra nenhuma (qualquer valor serve) talvez nem precise estar numa classe: uma 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.

Use 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ãoOpçõesRecomendação
Nome do getterpreco() 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 settersetPreco(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 privadospreco_, m_preco ou só precoUm sinal (_ ou m_) evita o conflito com o getter. Sem sinal, use this-> quando houver ambiguidade.
Valor inválido num setterDevolver false, ignorar, ou lançar exceçãofalse 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 construtorCorrigir para um valor seguro ou lançar exceçãoExceçã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

Mexer num atributo privado de fora da classe

É 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.

O construtor ficou privado
#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.)

A exceção do construtor que ninguém capturou
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

EscritaO 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 contextErro: código de fora tentou usar um membro privado.
is protected within this contextErro: 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_:

int saldo() { }

3. A regra

No setter bool mudarPreco(int novo), complete a condição que recusa preço negativo:

if () return false;

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étodos public. 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 lance std::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: como private, 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.