C++ O que é POO
Até agora, nos seus programas, os dados ficavam de um lado (variáveis, structs) e as ações ficavam do outro (funções). Funciona, mas, quando o programa cresce, fica difícil saber qual função mexe em qual dado. A Programação Orientada a Objetos (POO) junta as duas coisas: cada "coisa" do programa carrega os seus dados e as ações que sabe fazer. Esta aula é a porta de entrada do Nível 3. Ela explica a ideia, sem pressa, antes de você escrever a sua primeira classe na próxima aula.
- O que é uma classe (o molde) e o que é um objeto (a coisa feita com o molde).
- Como o mesmo programa da lojinha da Ana fica escrito sem POO e com POO.
- Por que tanta gente organiza programas assim.
- A ideia dos quatro pilares: encapsulamento, herança, polimorfismo e abstração.
- O que muda na POO do C++ em relação a Java e Python: objetos na pilha, cópias de verdade e nada de coletor de lixo.
- Dois erros de quem está começando: usar o molde como se fosse objeto e escrever
newcomo em Java.
Todos os programas desta aula foram compilados e executados na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/cpp/poo, com o g++ 15.2 e o comando padrão do guia, g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa (veja Compilar com g++).
A ideia: a ficha de cadastro da lojinha
A Ana tem uma lojinha de papelaria. Para não se perder, ela imprimiu uma ficha de cadastro de produto, com três campos em branco: Nome, Preço e Estoque. No verso, a ficha traz as instruções do que se pode fazer com um produto: "mostrar na vitrine", "vender", "repor".
A ficha em branco não é um produto. Ninguém compra uma ficha em branco. Ela é só o molde: diz quais informações todo produto tem e o que todo produto sabe fazer. Quando a Ana preenche uma cópia da ficha com "caneta, R$ 3, 50 unidades", aí sim ela tem um produto de verdade. Outra cópia, preenchida com "caderno, R$ 12, 8 unidades", é outro produto.
| Na lojinha | Em POO | Em C++ |
|---|---|---|
| A ficha em branco (o molde) | classe | class Produto { … }; |
| Uma ficha preenchida (a caneta, o caderno) | objeto (ou instância) | Produto caneta{…}; |
| Os campos: nome, preço, estoque | atributos (dados do objeto) | variáveis dentro da classe, os membros de dados |
| As instruções do verso: mostrar, vender | métodos (ações do objeto) | funções dentro da classe, as funções-membro |
Guarde a frase: a classe é o molde; o objeto é a coisa feita com o molde. Uma classe, muitos objetos. Outra comparação que ajuda: a receita de bolo (classe) e os bolos que saem do forno (objetos). A receita diz "tem farinha, ovos e açúcar, e vai ao forno"; cada bolo tem a sua própria farinha, e comer um bolo não some com o outro.
"Instância" é só outro nome para objeto: "a caneta é uma instância da classe Produto" quer dizer "a caneta é um objeto feito com o molde Produto". Você vai encontrar as duas palavras em livros e mensagens de erro.
1. O jeito que você já conhece: dados de um lado, funções do outro
Na aula de Structs, você aprendeu a juntar dados num tipo só. Um produto da lojinha, escrito assim, fica com os dados numa struct e as ações em funções soltas, que recebem o produto como parâmetro:
#include <iostream>
#include <string>
struct Produto {
std::string nome;
int preco;
int estoque;
};
void mostrar(const Produto& p) {
std::cout << p.nome << ": R$ " << p.preco << " (" << p.estoque << " no estoque)\n";
}
void vender(Produto& p, int quantidade) {
p.estoque -= quantidade;
}
int main() {
Produto caneta{"caneta", 3, 50};
vender(caneta, 2);
mostrar(caneta);
}
$ g++ -std=c++23 -Wall -Wextra solto.cpp -o solto $ ./solto caneta: R$ 3 (48 no estoque)
Nada de novo aqui: a struct guarda os dados, mostrar recebe o produto por referência constante (só lê) e vender recebe por referência (para poder mudar o estoque). Os preços desta parte do guia são em reais inteiros, para não desviar a atenção com casas decimais.
O problema aparece quando o programa cresce. Numa loja de verdade haveria mostrar, vender, repor, aplicarDesconto, mudarPreco… todas soltas, espalhadas pelos arquivos, e nada liga essas funções ao Produto além do parâmetro. Pior: qualquer parte do programa pode mexer em p.estoque diretamente, inclusive deixando o estoque negativo.
2. O mesmo programa com POO
Agora o mesmo produto, escrito como uma classe. Não se preocupe em decorar a sintaxe: a próxima aula, Classes e Objetos, explica cada pedaço. Aqui, repare só em onde as coisas estão.
#include <iostream>
#include <string>
class Produto {
public:
std::string nome;
int preco;
int estoque;
void mostrar() {
std::cout << nome << ": R$ " << preco << " (" << estoque << " no estoque)\n";
}
void vender(int quantidade) {
estoque -= quantidade;
}
};
int main() {
Produto caneta{"caneta", 3, 50};
Produto caderno{"caderno", 12, 8};
caneta.vender(2);
caderno.vender(1);
caneta.mostrar();
caderno.mostrar();
}
$ g++ -std=c++23 -Wall -Wextra loja.cpp -o loja $ ./loja caneta: R$ 3 (48 no estoque) caderno: R$ 12 (7 no estoque)
- class Produto { … };
- A classe: o molde. Diz que todo produto tem nome, preço e estoque e sabe se mostrar e ser vendido. Ela sozinha não cria produto nenhum.
- public:
- "O que vem a seguir pode ser usado de fora da classe." A aula public e private explica por que às vezes queremos o contrário.
- std::string nome; int preco; int estoque;
- Os atributos: os campos da ficha. Cada objeto tem os seus.
- void mostrar() { … }
- Um método: uma função que mora dentro da classe. Repare que ela não recebe o produto como parâmetro: usa
nome,precoeestoquedireto, porque sempre trabalha sobre o objeto que a chamou. - Produto caneta{"caneta", 3, 50};
- Cria um objeto: uma ficha preenchida, chamada
caneta. - caneta.vender(2);
- Pede à caneta: "venda 2 de você". O ponto (
.) liga o objeto à ação. Compare comvender(caneta, 2)da versão sem POO.
A grande mudança está na última linha da tabela: em vez de "função, faça isto com este produto", você diz "produto, faça isto". O objeto vem primeiro, e a ação pertence a ele. E caneta.vender(2) mexeu só no estoque da caneta: o caderno continua com o dele. Cada objeto tem os próprios dados.
Experimente: crie um terceiro produto, borracha, que custa R$ 2 e tem 30 unidades. Venda 5 borrachas e mostre. Depois tente vender mais borrachas do que existem e veja o que acontece com o estoque.
#include <iostream>
#include <string>
class Produto {
public:
std::string nome;
int preco;
int estoque;
void mostrar() {
std::cout << nome << ": R$ " << preco << " (" << estoque << " no estoque)\n";
}
void vender(int quantidade) {
estoque -= quantidade;
}
};
int main() {
Produto caneta{"caneta", 3, 50};
Produto caderno{"caderno", 12, 8};
caneta.vender(2);
caderno.vender(1);
caneta.mostrar();
caderno.mostrar();
}
3. Por que usar POO
Ninguém é obrigado a usar POO em C++. Mas ela resolve problemas reais que aparecem quando o programa cresce:
| Problema | Como a POO ajuda |
|---|---|
| "Onde está a função que mexe no estoque?" | Tudo o que é de Produto está dentro de Produto. Para entender um produto, você lê uma classe. |
| Alguém deixou o estoque negativo | A classe pode proibir que mexam nos dados direto e obrigar todo mundo a passar pelos métodos, que conferem as regras. |
| A loja agora vende livros, que têm autor | Uma classe nova pode reaproveitar o Produto e acrescentar só o que falta. |
| Mostrar uma lista misturada de livros e canetas, cada um do seu jeito | Objetos diferentes podem responder à mesma ordem (mostrar()), cada um à sua maneira. |
| O programa ficou grande demais para caber na cabeça | Cada classe vira uma "caixa preta": você usa caneta.vender(2) sem pensar em como ela funciona por dentro. |
Veja o segundo problema acontecendo. Com os dados abertos, nada impede isto (a classe Produto é a mesma de loja.cpp; mostramos só o main):
int main() {
Produto caneta{"caneta", 3, 50};
caneta.estoque -= 60; // vendeu mais do que tinha!
caneta.preco = -3; // preço negativo?!
caneta.mostrar();
}
$ g++ -std=c++23 -Wall -Wextra furo.cpp -o furo $ ./furo caneta: R$ -3 (-10 no estoque)
O compilador não reclamou de nada, e o programa obedeceu: estoque −10, preço −3. Para o C++, são só números. Quem sabe que estoque negativo não existe é você, e a POO dá as ferramentas para colocar essa regra dentro da classe. É o primeiro pilar.
4. Os quatro pilares (só a ideia)
Livros e entrevistas de emprego falam dos "quatro pilares da POO". São quatro ideias. Cada uma tem a sua própria aula mais adiante; aqui você só precisa reconhecer o nome e o que ele quer dizer.
Encapsulamento: proteger os dados
A classe esconde os seus dados e só deixa mexer neles pelos métodos. Quem quer mudar o preço chama caneta.mudarPreco(4), e o método recusa preço negativo. É o que teria evitado o "R$ −3" acima. É como o caixa da loja: o cliente não abre a gaveta do dinheiro, ele pede ao caixa. Em C++, isso se faz com private e public. Aula: public e private.
Herança: aproveitar um molde para fazer outro
A Ana passou a vender livros. Um livro é um produto (tem nome, preço e estoque) e mais um autor. Em vez de copiar tudo, a classe Livro herda de Produto e acrescenta só o autor. Diz-se que "Livro é um Produto". Aula: Herança.
Polimorfismo: a mesma ordem, respostas diferentes
"Poli" = muitas, "morfo" = formas. A Ana tem uma lista com canetas e livros e manda cada um se mostrar(). A caneta mostra nome e preço; o livro mostra também o autor. Quem chama não precisa saber o que cada um é: dá a mesma ordem e cada objeto responde do seu jeito. Em C++, isso usa a palavra virtual. Aula: Polimorfismo (virtual).
Abstração: mostrar só o que importa
Quem vende uma caneta não precisa saber como o estoque é guardado: precisa de "vender" e "repor". Abstrair é pensar no que uma coisa faz, deixando o como escondido. Em C++, isso chega ao extremo nas classes abstratas: moldes que só dizem "todo produto sabe calcular o frete", sem dizer como, deixando cada tipo de produto completar. Aula: Classes Abstratas.
| Pilar | Em uma frase | Ferramenta em C++ | Aula |
|---|---|---|---|
| Encapsulamento | Os dados ficam protegidos; só os métodos mexem neles. | private, public | public e private |
| Herança | Uma classe nova aproveita outra e acrescenta coisas. | class Livro : public Produto | Herança |
| Polimorfismo | A mesma chamada faz coisas diferentes conforme o objeto. | virtual, override | Polimorfismo |
| Abstração | Mostrar o que o objeto faz, esconder como faz. | classes e métodos abstratos (= 0) | Classes Abstratas |
5. POO em C++ × Java e Python
Se você já ouviu falar de POO em Java ou Python, algumas coisas em C++ vão parecer estranhas. Se nunca ouviu, esta seção também vale: são as características que tornam o C++ diferente.
Objetos nascem na pilha, sem new
Em Java, todo objeto é criado com new. Em C++, o jeito normal é declarar o objeto como uma variável comum: Produto caneta{"caneta", 3, 50};. Ele fica na pilha (stack), a mesma região de memória das variáveis locais (aula Como a Memória Funciona), nasce na linha da declaração e some sozinho no fim do bloco { }, igual a um int.
O new existe em C++, mas faz outra coisa: cria o objeto em outra região (o heap) e devolve um ponteiro para ele (aula new e delete). Por isso, o hábito de Java dá erro. Veja na seção Quando dá errado.
Não existe coletor de lixo
Java, Python, C# e JavaScript têm um coletor de lixo (garbage collector): de tempos em tempos, um programa auxiliar procura objetos que ninguém usa mais e libera a memória deles. Você não sabe exatamente quando isso acontece.
O C++ não tem coletor de lixo. Em compensação, ele sabe exatamente quando cada objeto morre: um objeto na pilha morre no } que fecha o bloco dele. Nesse momento, o C++ chama uma função especial da classe, o destrutor, que pode fechar arquivos, devolver memória e fazer qualquer limpeza. Essa garantia é a base de uma técnica muito importante do C++, o RAII, que você vai ver em Destrutores e RAII. Para objetos criados com new, quem libera é você, ou melhor, um ponteiro inteligente.
b = a faz uma cópia de verdade
Esta é a diferença que mais surpreende quem vem de outras linguagens. Em C++, um objeto se comporta como um int: atribuir copia. (A classe é a mesma de loja.cpp, só com o método mostrar.)
int main() {
Produto a{"caneta", 3, 50};
Produto b = a; // b é uma CÓPIA de a
b.nome = "caneta azul";
b.preco = 4;
a.mostrar();
b.mostrar();
}
$ g++ -std=c++23 -Wall -Wextra copia.cpp -o copia $ ./copia caneta: R$ 3 (50 no estoque) caneta azul: R$ 4 (50 no estoque)
Mudar b não mexeu em a: são duas fichas, cada uma com os seus dados. Em Python (e em Java), a mesma ideia dá outro resultado, porque b = a não copia o objeto: só cria outro nome para o mesmo objeto.
class Produto:
def __init__(self, nome, preco):
self.nome = nome
self.preco = preco
a = Produto("caneta", 3)
b = a # b é OUTRO NOME para o mesmo objeto
b.nome = "caneta azul"
print(a.nome)
$ python3 copia.py caneta azul « mudou o b, e o a mudou junto: é o mesmo objeto
Em C++, quando você quer "outro nome para o mesmo objeto", usa uma referência (Produto& b = a;) ou um ponteiro, e isso fica escrito no código, à vista. Como as cópias podem ser caras (copiar um objeto grande copia tudo o que está dentro), o C++ tem regras próprias para copiar e mover objetos: aula Cópia e Movimento.
Nem tudo precisa estar numa classe
Em Java, todo código mora dentro de uma classe, até o main. Em C++, não: o main é uma função solta, e você pode misturar classes, funções soltas e templates à vontade. O C++ é uma linguagem multiparadigma: a POO é uma das ferramentas, não a única.
| Assunto | C++ | Java / Python |
|---|---|---|
| Onde o objeto nasce | Normalmente na pilha: Produto p{…}; | Sempre no heap (Java: new Produto(…); Python: Produto(…)) |
| Quem libera a memória | O próprio C++, no fim do bloco (destrutor); com new, você ou um ponteiro inteligente | O coletor de lixo, quando ele quiser |
b = a | Copia o objeto inteiro | Os dois nomes apontam para o mesmo objeto |
| Código fora de classes | Pode (o main é solto) | Java: não; Python: pode |
| Proteção dos dados | private conferido pelo compilador | Java: private; Python: só por convenção (_nome) |
Quando dá errado
#include <iostream>
#include <string>
class Produto {
public:
std::string nome;
int preco;
int estoque;
};
int main() {
Produto.preco = 5;
}
$ g++ -std=c++23 -Wall -Wextra molde.cpp -o molde
molde.cpp: In function ‘int main()’:
molde.cpp:12:12: error: expected unqualified-id before ‘.’ token
12 | Produto.preco = 5;
| ^
O que aconteceu: Produto é a classe, a ficha em branco. Não existe "o preço da ficha em branco": existe o preço desta caneta, daquele caderno. O g++ diz que, antes do ponto, esperava o nome de um objeto (um unqualified-id, um nome simples), não de um tipo. O Clang, compilador do editor do guia, é mais direto: error: cannot use dot operator on a type ("não dá para usar o operador ponto num tipo").
Como resolver: crie um objeto e use o ponto nele: Produto caneta{"caneta", 3, 50}; e depois caneta.preco = 5;.
new, como em Java
#include <iostream>
#include <string>
class Produto {
public:
std::string nome;
int preco;
int estoque;
};
int main() {
Produto caneta = new Produto();
}
$ g++ -std=c++23 -Wall -Wextra novo.cpp -o novo
novo.cpp: In function ‘int main()’:
novo.cpp:12:22: error: conversion from ‘Produto*’ to non-scalar type ‘Produto’ requested
12 | Produto caneta = new Produto();
| ^~~~~~~~~~~~~
O que aconteceu: em C++, new Produto() não devolve um Produto: devolve um Produto*, um ponteiro para um produto criado no heap. A mensagem diz: "foi pedida uma conversão de Produto* para o tipo Produto", e essa conversão não existe. O Clang diz o mesmo com outras palavras: no viable conversion from 'Produto *' to 'Produto'.
Como resolver: no C++, você quase nunca precisa de new. Escreva só Produto caneta{}; (ou com os valores dentro das chaves). Se um dia precisar mesmo do objeto no heap, use std::make_unique<Produto>() (aula Ponteiros Inteligentes), nunca um new solto.
Referência rápida
| Termo | Significado | Exemplo |
|---|---|---|
| Classe | O molde: diz que dados e ações um tipo de coisa tem | class Produto { … }; |
| Objeto / instância | Uma coisa feita com o molde, com os próprios dados | Produto caneta{"caneta", 3, 50}; |
| Atributo (membro de dados) | Uma variável que cada objeto carrega | int estoque; |
| Método (função-membro) | Uma função que trabalha sobre o objeto que a chamou | caneta.vender(2); |
| Encapsulamento | Esconder os dados e expor métodos | private: / public: |
| Herança | Uma classe aproveita outra | class Livro : public Produto |
| Polimorfismo | Mesma chamada, comportamentos diferentes | virtual void mostrar(); |
| Abstração | O que faz, não como faz | virtual int frete() = 0; |
Exercícios
1. Molde ou objeto?
Na linha Produto lapis{"lápis", 1, 100};, qual palavra é a classe?
2. Quem é o objeto?
Na mesma linha, qual é o nome do objeto criado?
3. Chamar a ação
Complete para pedir ao objeto lapis que venda 10 unidades, no estilo POO:
4. Qual pilar da POO impediria alguém de escrever caneta.preco = -3;?
5. Em C++, depois de Produto b = a; e b.preco = 9;, quanto vale a.preco, se antes valia 3?
6. Quando um objeto criado como Produto caneta{…}; dentro de uma função deixa de existir?
7. A Ana quer uma classe Livro que tenha tudo de Produto e mais o autor. Que pilar ela vai usar?
Missão final: o estilo POO
Missão: crie um objeto mochila que custa R$ 80 e tem 5 unidades. Venda 1 mochila e 5 canetas chamando o método de cada objeto (objeto.vender(…)) e mostre os dois. A saída deve ter:
caneta: R$ 3 (45 no estoque) mochila: R$ 80 (4 no estoque)
#include <iostream>
#include <string>
class Produto {
public:
std::string nome;
int preco;
int estoque;
void mostrar() {
std::cout << nome << ": R$ " << preco << " (" << estoque << " no estoque)\n";
}
void vender(int quantidade) {
estoque -= quantidade;
}
};
int main() {
Produto caneta{"caneta", 3, 50};
// crie a mochila aqui
// venda e mostre aqui
}
#include <iostream>
#include <string>
class Produto {
public:
std::string nome;
int preco;
int estoque;
void mostrar() {
std::cout << nome << ": R$ " << preco << " (" << estoque << " no estoque)\n";
}
void vender(int quantidade) {
estoque -= quantidade;
}
};
int main() {
Produto caneta{"caneta", 3, 50};
Produto mochila{"mochila", 80, 5};
caneta.vender(5);
mochila.vender(1);
caneta.mostrar();
mochila.mostrar();
}
Resumo
- Classe é o molde (a ficha em branco); objeto (ou instância) é a coisa feita com o molde (a ficha preenchida). Uma classe, muitos objetos, cada um com os próprios dados.
- Na POO, dados (atributos) e ações (métodos) moram juntos. Em vez de
vender(caneta, 2), você escrevecaneta.vender(2). - POO organiza o código; não deixa o programa mais rápido. Ela ajuda a achar as coisas, proteger os dados e reaproveitar código.
- Os quatro pilares: encapsulamento (proteger os dados), herança (aproveitar um molde), polimorfismo (mesma ordem, respostas diferentes) e abstração (o que faz, não como faz).
- Em C++, objetos nascem normalmente na pilha, sem
new, e morrem no fim do bloco. Não há coletor de lixo: o destrutor roda na hora certa. - Em C++,
b = acopia o objeto. Para "outro nome do mesmo objeto", use referência ou ponteiro. - Próxima aula: escrever a sua primeira classe, em Classes e Objetos.