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.

Você vai aprender
  • 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 class e struct.
  • 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, Produto passa a ser um tipo, como int ou std::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, o main nã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 Produto vai 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 Produto chamado caneta. É igual a int 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".
Por que 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 de delete.
  • Os atributos ficam dentro do objeto, um do lado do outro na memória. O preco da caneta é um int que 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, preco e estoque, 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 estoque fica zerado (não é lixo). Mesmo assim, o -Wextra avisa: "falta o inicializador do membro Produto::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.
Prefira criar com chaves. 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
LinhaO 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.estoqueO 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::

structclass
Membros sem especificadorpúblicos (qualquer um usa)privados (só a própria classe usa)
Herança sem especificadorpúblicaprivada
Todo o restoigual

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):

  • struct para 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.
  • class quando 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

Usar um atributo que não existe (ou escrever o nome 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.

Esquecer o ; 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.

Comparar dois objetos com ==
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

EscritaO 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.precoLê 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 × classIguais, 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:

= 20;

2. O detalhe que falta

Complete o fim da classe:

class Cliente { public: std::string nome;

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:

Cliente bia

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: sem new, e ele some no fim do bloco.
  • O ponto acessa os atributos: caneta.preco = 4; muda, caneta.preco lê.
  • Prefira criar com chaves: Produto p{"caneta", 3, 50}; ou Produto p{};. Sem chaves, int fica com lixo.
  • Cada objeto tem os próprios atributos. Objetos cabem em std::vector e vão para funções por const& (ler) ou & (mudar).
  • class e struct só 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.