C++ Classes e Objetos
Na aula anterior, O que é POO, você viu a ideia: a classe é o molde e o objeto é a coisa feita com o molde. Agora é hora de escrever a sua primeira classe, linha por linha, e criar objetos com ela. Nesta aula a classe só guarda dados; as ações (os métodos) chegam na próxima aula.
- A sintaxe de uma classe:
class Produto { public: … };, peça por peça. - Como criar um objeto e por que ele nasce na pilha, como uma variável comum.
- Como ler e mudar os atributos com o ponto (
.). - Como criar vários objetos e guardá-los num
std::vector. - A única diferença entre
classestruct. - Os erros de usar um membro que não existe e de esquecer o
;depois da classe.
Os programas desta aula foram compilados e executados na máquina fictícia da Ana, na pasta ~/cpp/classes, com o g++ 15.2 e o comando padrão g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.
A ideia: desenhar a ficha antes de preencher
Antes de cadastrar a primeira caneta, a Ana precisa desenhar a ficha de produto: decidir que campos ela tem. Isso é escrever a classe. Depois, ela tira cópias da ficha e preenche cada uma: isso é criar objetos.
Se você fez a aula de Structs, vai se sentir em casa: uma classe que só tem dados é quase igual a uma struct. A diferença está numa palavra só, e você vai vê-la na seção 5.
1. Escrever a classe
#include <iostream>
#include <string>
class Produto {
public:
std::string nome;
int preco;
int estoque;
};
int main() {
Produto caneta;
caneta.nome = "caneta";
caneta.preco = 3;
caneta.estoque = 50;
std::cout << "Produto: " << caneta.nome << "\n";
std::cout << "Preço: R$ " << caneta.preco << "\n";
std::cout << "Estoque: " << caneta.estoque << "\n";
}
$ g++ -std=c++23 -Wall -Wextra produto.cpp -o produto $ ./produto Produto: caneta Preço: R$ 3 Estoque: 50
Primeiro, a classe (linhas 4 a 9):
- class
- Palavra reservada que diz "vou criar um tipo novo". Depois desta linha,
Produtopassa a ser um tipo, comointoustd::string. - Produto
- O nome da classe. Por costume, nomes de classe começam com maiúscula (
Produto,Cliente,ContaBancaria), e nomes de variáveis com minúscula (caneta). O compilador não exige, mas ajuda a bater o olho e saber o que é molde e o que é objeto. - { … }
- O corpo da classe: tudo o que todo produto tem fica entre as chaves.
- public:
- Um especificador de acesso. Quer dizer "o que vem abaixo pode ser usado de fora da classe", por exemplo pelo
main. Termina com dois-pontos, não com ponto e vírgula. Sem ele, omainnão conseguiria mexer nos campos (seção 5). - std::string nome; int preco; int estoque;
- Os atributos (também chamados de membros de dados ou campos). Cada um é declarado como uma variável comum: tipo e nome. Todo objeto
Produtovai ter esses três. - };
- Fecha a classe. O ponto e vírgula depois da chave é obrigatório, ao contrário do
}de uma função. Esquecer é um dos erros mais comuns (veja em Quando dá errado).
A classe sozinha não cria nada na memória. Ela só ensina ao compilador o que é um Produto. Quem cria é o main:
- Produto caneta;
- Cria um objeto do tipo
Produtochamadocaneta. É igual aint x;: primeiro o tipo, depois o nome. - caneta.nome = "caneta";
- O ponto (
.) entra no objeto e pega um atributo dele. Leia como "o nome da caneta recebe"caneta"". - caneta.preco
- Usado numa expressão, o ponto lê o atributo: "o preço da caneta".
public: aqui? Numa classe, tudo começa escondido (privado). Nesta aula deixamos tudo público para aprender a sintaxe sem tropeçar. Esconder os dados é justamente o que você vai fazer em public e private, quando a classe tiver métodos para cuidar deles.2. Onde o objeto vive: na pilha
O Produto caneta; criou o objeto do mesmo jeito que int x; cria um inteiro: na pilha, a região da memória onde ficam as variáveis locais (aula Como a Memória Funciona). Isso tem três consequências:
- Não precisa de
new. O objeto existe a partir da linha da declaração. - Ele some sozinho no
}que fecha o bloco onde foi criado, como qualquer variável local (aula Escopo). Não precisa dedelete. - Os atributos ficam dentro do objeto, um do lado do outro na memória. O
precoda caneta é umintque mora dentro da caneta.
Cuidado: atributo sem valor guarda lixo
Ainda como um int comum: um atributo int que ninguém preencheu fica com o que estava naquele pedaço da memória. Veja a borracha, que ganhou nome mas não ganhou preço (a classe é a mesma de produto.cpp; daqui em diante, quando o arquivo repetir a classe, mostramos só o main):
int main() {
Produto borracha;
borracha.nome = "borracha";
std::cout << borracha.nome << ": R$ " << borracha.preco << "\n";
}
$ g++ -std=c++23 -Wall -Wextra lixo.cpp -o lixo $ ./lixo borracha: R$ 1 « ninguém escreveu esse 1: é lixo da memória
Sem aviso nenhum do compilador, e o número pode mudar de uma máquina para outra (é comportamento indefinido). O nome, por outro lado, começa vazio, porque std::string sabe se inicializar sozinho. Tipos simples (int, double, bool, ponteiros) não sabem.
A defesa mais simples, por enquanto, é criar o objeto já com valores, usando chaves, como na próxima seção. A defesa definitiva é a classe dar um valor inicial a cada atributo, e isso você vai aprender em Construtores.
3. Criar o objeto já preenchido, com chaves
Preencher campo por campo dá trabalho e é fácil esquecer um. Uma classe simples como esta (só atributos públicos, sem construtores) aceita os valores entre chaves, na ordem em que os atributos foram declarados:
int main() {
Produto caderno{"caderno", 12, 8};
Produto lapis{"lápis", 1}; // estoque fica 0
Produto vazio{}; // tudo zerado
std::cout << caderno.nome << " | " << caderno.preco << " | " << caderno.estoque << "\n";
std::cout << lapis.nome << " | " << lapis.preco << " | " << lapis.estoque << "\n";
std::cout << "[" << vazio.nome << "] | " << vazio.preco << " | " << vazio.estoque << "\n";
}
$ g++ -std=c++23 -Wall -Wextra chaves.cpp -o chaves
chaves.cpp: In function ‘int main()’:
chaves.cpp:13:29: warning: missing initializer for member ‘Produto::estoque’ [-Wmissing-field-initializers]
13 | Produto lapis{"lápis", 1}; // estoque fica 0
| ^
$ ./chaves
caderno | 12 | 8
lápis | 1 | 0
[] | 0 | 0
- caderno{"caderno", 12, 8}
- Os três valores vão para
nome,precoeestoque, nessa ordem. A ordem é a da declaração dentro da classe, não a dos nomes. - lapis{"lápis", 1}
- Faltou o último valor: o
estoquefica zerado (não é lixo). Mesmo assim, o-Wextraavisa: "falta o inicializador do membroProduto::estoque", porque pode ser esquecimento. - vazio{}
- Chaves vazias: todos os atributos começam "zerados": números com 0 e a string vazia (por isso
[]na saída). É a forma segura de criar um objeto que você vai preencher depois.
Produto p{}; nunca tem lixo; Produto p; pode ter. Quando a classe ganhar construtores, as chaves continuam sendo o jeito de passar os valores, mas quem decide o que fazer com eles passa a ser a classe.Experimente: mude a ordem dos valores em caderno{12, "caderno", 8} e rode: o compilador não aceita colocar um número no nome. Depois volte à ordem certa, crie um produto estojo que custa R$ 15 e tem 6 unidades, e mostre os três campos dele.
#include <iostream>
#include <string>
class Produto {
public:
std::string nome;
int preco;
int estoque;
};
int main() {
Produto caderno{"caderno", 12, 8};
std::cout << caderno.nome << " | " << caderno.preco << " | " << caderno.estoque << "\n";
}
4. Vários objetos
A classe é uma só; objetos, quantos você quiser. Cada um tem os seus próprios atributos: mudar o preço da caneta não mexe no do caderno. E, como Produto é um tipo como outro qualquer, dá para guardar produtos num std::vector:
#include <iostream>
#include <string>
#include <vector>
class Produto {
public:
std::string nome;
int preco;
int estoque;
};
int main() {
Produto caneta{"caneta", 3, 50};
Produto caderno{"caderno", 12, 8};
caneta.preco = 4; // muda só a caneta
std::cout << caneta.nome << ": R$ " << caneta.preco << "\n";
std::cout << caderno.nome << ": R$ " << caderno.preco << "\n";
std::vector<Produto> vitrine{caneta, caderno, {"régua", 5, 20}};
vitrine.push_back({"cola", 6, 15});
int total = 0;
for (const Produto& p : vitrine) {
std::cout << "- " << p.nome << " (" << p.estoque << " un.)\n";
total += p.preco * p.estoque;
}
std::cout << "Valor do estoque: R$ " << total << "\n";
}
$ g++ -std=c++23 -Wall -Wextra varios.cpp -o varios $ ./varios caneta: R$ 4 caderno: R$ 12 - caneta (50 un.) - caderno (8 un.) - régua (20 un.) - cola (15 un.) Valor do estoque: R$ 486
| Linha | O que faz |
|---|---|
caneta.preco = 4; | Muda o preço da caneta. O caderno continua com R$ 12. |
std::vector<Produto> vitrine{…}; | Um vetor de produtos. Dentro das chaves vão objetos prontos (caneta, caderno) ou valores para criar um novo ({"régua", 5, 20}). O vetor guarda cópias: mudar caneta depois não muda o que está na vitrine. |
vitrine.push_back({"cola", 6, 15}); | Acrescenta mais um produto no fim, criado ali mesmo com chaves. |
for (const Produto& p : vitrine) | Percorre a vitrine (aula for de Intervalo). O const Produto& faz p ser outro nome para cada produto, sem copiar e sem poder mudar (aula const com Referências). |
p.preco * p.estoque | O ponto funciona do mesmo jeito numa referência: p.preco é o preço do produto da vez. |
Conta do valor do estoque: 4×50 + 12×8 + 5×20 + 6×15 = 200 + 96 + 100 + 90 = 486. Repare que a caneta entrou com R$ 4, porque o preço foi mudado antes de ela ser copiada para a vitrine.
Objetos em funções
Um objeto pode ser passado para uma função como qualquer variável. As regras são as da aula Por Valor × Por Referência:
void mostrar(const Produto& p): a função só lê o produto. É o jeito mais comum: não copia e não deixa mudar.void reajustar(Produto& p): a função muda o produto original.void f(Produto p): a função recebe uma cópia. Mudanças lá dentro não voltam, e copiar um objeto grande custa tempo.
Na próxima aula, as funções que trabalham com um produto vão morar dentro da classe, e aí nem vai ser preciso passar o produto como parâmetro: são os métodos.
5. class × struct
Em C++, class e struct criam a mesma coisa. Uma struct pode ter métodos, construtores, herança, tudo. A única diferença é o acesso padrão, isto é, o que vale quando você não escreve public: nem private::
struct | class | |
|---|---|---|
| Membros sem especificador | públicos (qualquer um usa) | privados (só a própria classe usa) |
| Herança sem especificador | pública | privada |
| Todo o resto | igual | |
Veja na prática. Com struct e sem public:, funciona:
#include <iostream>
#include <string>
struct Produto {
std::string nome;
int preco;
int estoque;
};
int main() {
Produto caneta;
caneta.preco = 3;
std::cout << caneta.preco << "\n";
}
$ g++ -std=c++23 -Wall -Wextra estrutura.cpp -o estrutura $ ./estrutura 3
Troque só a palavra struct por class, sem pôr public:, e o compilador recusa:
#include <iostream>
#include <string>
class Produto {
std::string nome;
int preco;
int estoque;
};
int main() {
Produto caneta;
caneta.preco = 3;
std::cout << caneta.preco << "\n";
}
$ g++ -std=c++23 -Wall -Wextra privado.cpp -o privado
privado.cpp: In function ‘int main()’:
privado.cpp:12:12: error: ‘int Produto::preco’ is private within this context
12 | caneta.preco = 3;
| ^~~~~
privado.cpp:6:9: note: declared private here
6 | int preco;
| ^~~~~
privado.cpp:13:25: error: ‘int Produto::preco’ is private within this context
13 | std::cout << caneta.preco << "\n";
| ^~~~~
privado.cpp:6:9: note: declared private here
6 | int preco;
| ^~~~~
"Produto::preco é privado neste contexto": o main está fora da classe, e numa class tudo começa privado. A nota declared private here aponta a linha 6, onde o atributo foi declarado. Isso não é defeito: é a ferramenta do encapsulamento, assunto da aula public e private.
Qual usar?
O compilador não se importa, mas os programadores C++ seguem um costume (está nas C++ Core Guidelines, o guia de boas práticas mantido pelo criador da linguagem):
structpara um pacote de dados sem regras: um ponto{x, y}, uma data, um registro lido de um arquivo. Qualquer combinação de valores é válida.classquando o objeto tem regras a proteger ("preço nunca negativo", "estoque nunca abaixo de zero") e por isso vai esconder os dados atrás de métodos.
Nesta aula usamos class com tudo public só para aprender a sintaxe. A partir de public e private, a classe Produto vai esconder os dados, como deve ser.
Quando dá errado
Nos três casos abaixo, o arquivo começa com a mesma classe Produto (pública) da seção 1; mostramos só o main, e por isso os números de linha das mensagens começam em 11.
int main() {
Produto caneta{"caneta", 3, 50};
caneta.cor = "azul";
std::cout << caneta.prco << "\n";
}
$ g++ -std=c++23 -Wall -Wextra cor.cpp -o cor
cor.cpp: In function ‘int main()’:
cor.cpp:13:12: error: ‘class Produto’ has no member named ‘cor’
13 | caneta.cor = "azul";
| ^~~
cor.cpp:14:25: error: ‘class Produto’ has no member named ‘prco’; did you mean ‘preco’?
14 | std::cout << caneta.prco << "\n";
| ^~~~
| preco
O que aconteceu: "a classe Produto não tem membro chamado cor". O objeto só tem o que a classe declarou: nome, preço e estoque. Não dá para acrescentar um campo novo num objeto na hora, como em Python ou JavaScript. No segundo erro, o g++ percebeu que prco parece preco e sugeriu: did you mean ‘preco’?, mostrando a correção embaixo. No editor do guia (Clang), a mensagem é no member named 'cor' in 'Produto'.
Como resolver: confira o nome na classe (maiúsculas contam: Preco ≠ preco). Se o produto precisa mesmo de uma cor, acrescente std::string cor; dentro da classe: aí todos os produtos passam a ter cor.
; depois da classe
#include <iostream>
#include <string>
class Produto {
public:
std::string nome;
int preco;
int estoque;
}
int main() {
Produto caneta{"caneta", 3, 50};
std::cout << caneta.nome << "\n";
}
$ g++ -std=c++23 -Wall -Wextra pontovirgula.cpp -o pontovirgula
pontovirgula.cpp:9:2: error: expected ‘;’ after class definition
9 | }
| ^
| ;
O que aconteceu: "esperava ; depois da definição da classe". O } de uma classe não termina a instrução, porque o C++ permite criar objetos ali mesmo (class Produto { … } caneta;, um costume antigo herdado do C). Por isso o ponto e vírgula é obrigatório. O g++ aponta a coluna logo depois do } e até desenha o ; que falta. O Clang diz quase igual: expected ';' after class.
Como resolver: escreva };. Um truque: sempre que digitar class Nome {, digite logo o }; e só depois preencha o meio. Em versões antigas de compiladores, esse erro aparecia em lugares estranhos (às vezes em outro arquivo, quando a classe estava num .h); se aparecer um erro esquisito logo depois de uma classe, confira o ; primeiro.
==
int main() {
Produto a{"caneta", 3, 50};
Produto b{"caneta", 3, 50};
if (a == b) {
std::cout << "iguais\n";
}
}
$ g++ -std=c++23 -Wall -Wextra igual.cpp -o igual
igual.cpp: In function ‘int main()’:
igual.cpp:14:11: error: no match for ‘operator==’ (operand types are ‘Produto’ and ‘Produto’)
14 | if (a == b) {
| ~ ^~ ~
| | |
| | Produto
| Produto
O que aconteceu: "não há operator== para os tipos Produto e Produto". O C++ não sabe o que "igual" quer dizer para um produto (mesmo nome? mesmo preço? tudo igual?), então não inventa. O desenho embaixo mostra o tipo de cada lado do ==.
Como resolver: por enquanto, compare os atributos: if (a.nome == b.nome). Ensinar o == à sua classe é assunto de Sobrecarga de Operadores. O mesmo vale para std::cout << caneta: o C++ também não sabe como mostrar um produto inteiro.
Referência rápida
| Escrita | O que faz |
|---|---|
class Produto { public: int preco; }; | Define a classe (o molde). Não esqueça o ; final. |
Produto p; | Cria um objeto na pilha. Atributos int/double ficam com lixo. |
Produto p{}; | Cria um objeto com tudo zerado. |
Produto p{"caneta", 3, 50}; | Cria já preenchido, na ordem dos atributos (classe só com dados públicos). |
p.preco = 4; | Muda um atributo do objeto p. |
p.preco | Lê um atributo. |
std::vector<Produto> v; | Um vetor de objetos (guarda cópias). |
void f(const Produto& p) | Função que recebe um objeto só para ler, sem copiar. |
struct × class | Iguais, exceto o acesso padrão: struct = público; class = privado. |
Exercícios
1. O ponto
Complete para mudar o estoque do objeto caderno para 20:
2. O detalhe que falta
Complete o fim da classe:
3. Criar com chaves
A classe é class Cliente { public: std::string nome; int pontos; };. Complete para criar o cliente bia, de nome "Bia" e 10 pontos:
4. Qual é a diferença entre class e struct em C++?
5. Depois de Produto p; (sem chaves) numa classe com int preco;, quanto vale p.preco?
6. O compilador diz ‘class Produto’ has no member named ‘Preco’, mas a classe tem int preco;. Por quê?
Missão final: a classe Cliente
Missão: escreva a classe Cliente, com os atributos públicos nome (texto) e pontos (inteiro). No main, crie os clientes Bia (10 pontos) e Caio (3 pontos), dê mais 5 pontos à Bia com += e mostre os dois assim:
Bia: 15 pontos Caio: 3 pontos
#include <iostream>
#include <string>
// escreva a classe Cliente aqui
int main() {
// crie a Bia e o Caio, some 5 pontos à Bia e mostre os dois
}
#include <iostream>
#include <string>
class Cliente {
public:
std::string nome;
int pontos;
};
int main() {
Cliente bia{"Bia", 10};
Cliente caio{"Caio", 3};
bia.pontos += 5;
std::cout << bia.nome << ": " << bia.pontos << " pontos\n";
std::cout << caio.nome << ": " << caio.pontos << " pontos\n";
}
Resumo
- Uma classe se escreve
class Nome { public: atributos… };. O;depois da}é obrigatório. Produto caneta;cria um objeto na pilha, como uma variável comum: semnew, e ele some no fim do bloco.- O ponto acessa os atributos:
caneta.preco = 4;muda,caneta.precolê. - Prefira criar com chaves:
Produto p{"caneta", 3, 50};ouProduto p{};. Sem chaves,intfica com lixo. - Cada objeto tem os próprios atributos. Objetos cabem em
std::vectore vão para funções porconst&(ler) ou&(mudar). classestructsó diferem no acesso padrão:struct= público,class= privado (is private within this context).- Membro que não existe:
has no member named. Não dá para inventar atributos fora da classe. - Próxima aula: colocar funções dentro da classe, os Métodos.