C++ Métodos
Na aula Classes e Objetos, a classe Produto só guardava dados, e quem fazia as contas era o main. Agora a classe vai ganhar ações: funções que moram dentro dela e trabalham com os dados do objeto. Essas funções se chamam métodos (ou funções-membro). Você já sabe escrever funções; um método é uma função com um "dono".
- Como escrever um método dentro da classe e chamá-lo com
objeto.metodo(). - Por que o método enxerga os atributos sem receber o objeto como parâmetro.
- Métodos que devolvem um valor, que recebem parâmetros e que mudam o objeto.
- Como um método chama outro método do mesmo objeto.
- Como declarar o método na classe e escrever o corpo do lado de fora, com
Produto::. - Os erros de esquecer os parênteses, esquecer o
Produto::e dar a um método o nome de um atributo.
Os programas desta aula foram compilados e executados na máquina fictícia da Ana, na pasta ~/cpp/metodos, com o g++ 15.2 e o comando padrão g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.
A ideia: as instruções no verso da ficha
A ficha de produto da Ana tem, na frente, os campos (nome, preço, estoque) e, no verso, as instruções: "para mostrar na vitrine, escreva o nome e o preço"; "para repor, some a quantidade ao estoque". As instruções são as mesmas para todo produto, mas cada vez que alguém as segue, segue numa ficha específica: repor canetas mexe na ficha da caneta, não na do caderno.
Métodos são isso: as instruções escritas uma vez, na classe, e executadas sempre sobre um objeto. Quando você escreve caderno.repor(4), está dizendo "siga a instrução repor, na ficha do caderno, com a quantidade 4".
1. O primeiro método
#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";
}
int valorEmEstoque() {
return preco * estoque;
}
void repor(int quantidade) {
estoque += quantidade;
}
};
int main() {
Produto caneta{"caneta", 3, 50};
Produto caderno{"caderno", 12, 8};
caneta.mostrar();
caderno.mostrar();
caderno.repor(4);
caderno.mostrar();
std::cout << "Valor das canetas: R$ " << caneta.valorEmEstoque() << "\n";
int total = caneta.valorEmEstoque() + caderno.valorEmEstoque();
std::cout << "Valor total: R$ " << total << "\n";
}
$ g++ -std=c++23 -Wall -Wextra metodos.cpp -o metodos $ ./metodos caneta: R$ 3 (50 no estoque) caderno: R$ 12 (8 no estoque) caderno: R$ 12 (12 no estoque) Valor das canetas: R$ 150 Valor total: R$ 294
A classe agora tem três métodos. Veja o primeiro em detalhe:
- void mostrar() { … }
- Uma função comum, com tipo de retorno (
void, nada), nome e parênteses, só que escrita dentro da classe. Por estar abaixo dopublic:, omainpode chamá-la. - nome, preco, estoque
- O método usa os atributos direto, sem ponto e sem parâmetro. Ele sempre trabalha sobre o objeto que o chamou: em
caneta.mostrar(),nomeé o nome da caneta; emcaderno.mostrar(), o do caderno. - caneta.mostrar();
- A chamada: objeto, ponto, nome do método e parênteses. Os parênteses são obrigatórios, mesmo vazios: são eles que dizem "execute".
Compare com a versão da aula O que é POO, em que mostrar era uma função solta: lá ela precisava receber const Produto& p e escrever p.nome, p.preco. O método não precisa, porque o objeto vai junto automaticamente, pelo lado esquerdo do ponto. (Como o C++ faz isso por baixo dos panos é o assunto da aula this.)
| Função solta | Método | |
|---|---|---|
| Onde mora | Fora da classe | Dentro da classe |
| Como recebe o produto | Por parâmetro: void mostrar(const Produto& p) | Automaticamente: é o objeto antes do ponto |
| Como usa os atributos | p.nome | nome |
| Como chama | mostrar(caneta); | caneta.mostrar(); |
2. Métodos que devolvem um valor
Um método pode ter qualquer tipo de retorno, como uma função (aula Retorno). O valorEmEstoque devolve um int:
- int valorEmEstoque()
- Devolve um inteiro: quanto dinheiro está parado no estoque deste produto.
- return preco * estoque;
- Usa os atributos do objeto que chamou. Para a caneta: 3 × 50 = 150.
- caneta.valorEmEstoque() + caderno.valorEmEstoque()
- A chamada vira um valor, e você usa esse valor como qualquer número: soma, guarda numa variável, mostra. 150 + 12 × 12 = 150 + 144 = 294.
Repare na diferença entre um atributo e um método que devolve valor: caneta.estoque é um dado guardado; caneta.valorEmEstoque() é uma conta feita na hora, sempre atualizada. Não faz sentido guardar o "valor em estoque" num atributo, porque ele ficaria errado assim que o estoque mudasse.
3. Métodos que recebem parâmetros e mudam o objeto
O repor recebe a quantidade como parâmetro, igual a uma função comum, e muda um atributo:
- void repor(int quantidade)
- Recebe um
int. Na chamadacaderno.repor(4),quantidadevale 4. - estoque += quantidade;
- Muda o estoque do próprio objeto. Não precisa de
&, nem de devolver nada: o método mexe direto no objeto que o chamou. Por isso o caderno passou de 8 para 12 unidades.
Na versão com função solta, era preciso passar o produto por referência (void repor(Produto& p, int q)), senão a mudança se perdia numa cópia. Com o método, o objeto já vem "por referência": o método trabalha no original.
mostrar, valorEmEstoque); outros mudam o objeto (repor). Essa diferença é tão importante que o C++ tem uma forma de marcá-la, a palavra const depois dos parênteses. Você vai vê-la em Métodos const.Experimente: acrescente à classe um método void reajustar(int reais) que soma reais ao preço. No main, reajuste a caneta em R$ 1 e mostre de novo. O valor das canetas no estoque mudou sozinho?
#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";
}
int valorEmEstoque() {
return preco * estoque;
}
void repor(int quantidade) {
estoque += quantidade;
}
};
int main() {
Produto caneta{"caneta", 3, 50};
caneta.mostrar();
std::cout << "Valor das canetas: R$ " << caneta.valorEmEstoque() << "\n";
}
4. Um método chamando outro
Dentro de um método, você chama os outros métodos do mesmo objeto pelo nome, sem ponto, do mesmo jeito que usa os atributos. Aqui, vender confere o estoque com temEstoque e mostra o resultado com mostrar:
#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";
}
bool temEstoque(int quantidade) {
return quantidade <= estoque;
}
bool vender(int quantidade) {
if (!temEstoque(quantidade)) {
std::cout << "Não há " << quantidade << " " << nome << "(s). ";
mostrar();
return false;
}
estoque -= quantidade;
std::cout << "Vendeu " << quantidade << ". ";
mostrar();
return true;
}
};
int main() {
Produto caderno{"caderno", 12, 8};
caderno.vender(3);
caderno.vender(10);
if (caderno.vender(5)) {
std::cout << "Acabaram os cadernos!\n";
}
}
$ g++ -std=c++23 -Wall -Wextra vender.cpp -o vender $ ./vender Vendeu 3. caderno: R$ 12 (5 no estoque) Não há 10 caderno(s). caderno: R$ 12 (5 no estoque) Vendeu 5. caderno: R$ 12 (0 no estoque) Acabaram os cadernos!
- bool temEstoque(int quantidade)
- Responde "sim" ou "não" (aula bool): há pelo menos
quantidadeunidades? - if (!temEstoque(quantidade))
- Chama outro método do mesmo objeto, sem ponto. Quando o caderno executa
vender, otemEstoquechamado aqui também é o do caderno. - mostrar();
- Mesma coisa: mostra o objeto que está vendendo.
- return false; / return true;
- O método avisa a quem chamou se a venda deu certo. O
mainpode ignorar o resultado (como nas duas primeiras chamadas) ou usá-lo numif(como na terceira).
Essa é uma vantagem grande dos métodos: a regra "só vende se tiver estoque" ficou num lugar só. Quem usa a classe chama vender e não precisa lembrar de conferir. Na aula public e private, você vai fechar a última porta: proibir que alguém mexa no estoque sem passar pelo vender.
5. Declarar dentro, definir fora: Produto::metodo
Quando a classe cresce, os corpos dos métodos ocupam espaço e escondem o que importa: quais métodos existem. Por isso, é comum separar em duas partes:
- a declaração, dentro da classe: só o "cabeçalho" do método, terminado em
;; - a definição, fora da classe: o corpo completo, com o nome da classe e
::na frente do nome do método.
#include <iostream>
#include <string>
class Produto {
public:
std::string nome;
int preco;
int estoque;
void mostrar();
void repor(int quantidade);
int valorEmEstoque();
};
void Produto::mostrar() {
std::cout << nome << ": R$ " << preco << " (" << estoque << " no estoque)\n";
}
void Produto::repor(int quantidade) {
estoque += quantidade;
}
int Produto::valorEmEstoque() {
return preco * estoque;
}
int main() {
Produto regua{"régua", 5, 20};
regua.repor(10);
regua.mostrar();
std::cout << "Valor: R$ " << regua.valorEmEstoque() << "\n";
}
$ g++ -std=c++23 -Wall -Wextra fora.cpp -o fora $ ./fora régua: R$ 5 (30 no estoque) Valor: R$ 150
- void mostrar();
- Dentro da classe, só a declaração: "existe um método
mostrar, que não recebe nada e não devolve nada". O;no lugar do corpo diz "o corpo está em outro lugar". - void Produto::mostrar() { … }
- A definição. O
Produto::diz "este é omostrarque pertence à classeProduto". O::é o operador de escopo, o mesmo destd::cout(ocoutque pertence astd, aula Namespaces). - nome, preco, estoque
- Graças ao
Produto::, o corpo continua enxergando os atributos direto, como se estivesse escrito dentro da classe. - int Produto::valorEmEstoque()
- O tipo de retorno vem antes de
Produto::. O cabeçalho tem de ser igual ao da declaração: mesmo retorno, mesmo nome, mesmos tipos de parâmetro.
Para o programa, não muda nada: a saída é a mesma. A vantagem é de leitura. Quem abre a classe vê, em poucas linhas, tudo o que um Produto sabe fazer.
.h e .cpp. Em projetos reais, a classe (com as declarações) vai num arquivo produto.h, e as definições com Produto:: vão em produto.cpp. Quem quer usar a classe só inclui o .h. Como fazer essa separação e compilar os arquivos juntos é assunto das aulas Arquivos .h e .cpp e Compilar Vários Arquivos.Dentro ou fora: qual usar?
| Situação | Onde escrever o corpo |
|---|---|
Método de uma ou duas linhas (valorEmEstoque) | Dentro da classe é comum e fica legível. |
Método longo (vender com várias regras) | Fora, com Produto::, para a classe continuar curta. |
| Classe usada em vários arquivos | Declaração no .h, definição no .cpp (aula Arquivos .h e .cpp). |
| Exemplos curtos deste guia | Dentro, para caber numa caixa só. |
Quando dá errado
Nos casos abaixo, o arquivo começa com a mesma classe Produto pública das seções anteriores; quando ela muda, mostramos o arquivo inteiro.
int main() {
Produto regua{"régua", 5, 20};
regua.mostrar;
}
$ g++ -std=c++23 -Wall -Wextra parenteses.cpp -o parenteses
parenteses.cpp: In function ‘int main()’:
parenteses.cpp:17:11: error: invalid use of non-static member function ‘void Produto::mostrar()’
17 | regua.mostrar;
| ~~~~~~^~~~~~~
parenteses.cpp:10:10: note: declared here
10 | void mostrar() {
| ^~~~~~~
O que aconteceu: sem os parênteses, você só nomeou o método, sem mandar executá-lo. O g++ chama isso de "uso inválido de função-membro não estática" (non-static: que pertence a um objeto; o contrário você vê em Membros static). O Clang, do editor do guia, é mais claro: reference to non-static member function must be called; did you mean to call it with no arguments? ("…precisa ser chamada; você quis chamá-la sem argumentos?").
Como resolver: regua.mostrar();. Os parênteses vão sempre, mesmo sem argumentos.
int main() {
Produto regua{"régua", 5, 20};
mostrar();
}
$ g++ -std=c++23 -Wall -Wextra semobjeto.cpp -o semobjeto
semobjeto.cpp: In function ‘int main()’:
semobjeto.cpp:17:5: error: ‘mostrar’ was not declared in this scope
17 | mostrar();
| ^~~~~~~
O que aconteceu: "mostrar não foi declarado neste escopo". Fora da classe, não existe um mostrar solto: existe o mostrar de cada produto. O compilador não tem como adivinhar qual produto você quer mostrar. Só dentro de outro método da classe é que mostrar() sem objeto funciona (seção 4), porque lá o objeto é o que está executando.
Como resolver: diga o objeto: regua.mostrar();.
Produto::
#include <iostream>
#include <string>
class Produto {
public:
std::string nome;
int preco;
int estoque;
void mostrar();
};
void mostrar() {
std::cout << nome << ": R$ " << preco << "\n";
}
int main() {
Produto regua{"régua", 5, 20};
regua.mostrar();
}
$ g++ -std=c++23 -Wall -Wextra semclasse.cpp -o semclasse
semclasse.cpp: In function ‘void mostrar()’:
semclasse.cpp:14:18: error: ‘nome’ was not declared in this scope
14 | std::cout << nome << ": R$ " << preco << "\n";
| ^~~~
semclasse.cpp:14:37: error: ‘preco’ was not declared in this scope
14 | std::cout << nome << ": R$ " << preco << "\n";
| ^~~~~
O que aconteceu: sem Produto::, a linha 13 criou uma função solta chamada mostrar, que não tem nada a ver com a classe. Repare na primeira linha da mensagem: In function ‘void mostrar()’, e não Produto::mostrar(). Uma função solta não tem objeto, então nome e preco não existem para ela. O Clang chega a sugerir a causa: use of undeclared identifier 'nome'; did you mean 'Produto::nome'?
Como resolver: void Produto::mostrar() { … }.
#include <iostream>
#include <string>
class Produto {
public:
std::string nome;
int preco;
int estoque;
void mostrar();
};
void Produto::mostrar() {
std::cout << nome << ": R$ " << preco << "\n";
}
void Produto::repor(int quantidade) {
estoque += quantidade;
}
int main() {
Produto regua{"régua", 5, 20};
regua.mostrar();
}
$ g++ -std=c++23 -Wall -Wextra naodeclarado.cpp -o naodeclarado
naodeclarado.cpp:17:6: error: no declaration matches ‘void Produto::repor(int)’
17 | void Produto::repor(int quantidade) {
| ^~~~~~~
naodeclarado.cpp:17:6: note: no functions named ‘void Produto::repor(int)’
naodeclarado.cpp:4:7: note: ‘class Produto’ defined here
4 | class Produto {
| ^~~~~~~
O que aconteceu: "nenhuma declaração corresponde a Produto::repor(int)". Todo método precisa estar declarado dentro da classe; fora, você só escreve o corpo de algo que já foi anunciado. A nota aponta para a classe, para você conferir. O mesmo erro aparece quando a declaração existe mas o cabeçalho é diferente (por exemplo, void repor(int) dentro e void Produto::repor(double) fora). No Clang: out-of-line definition of 'repor' does not match any declaration in 'Produto'.
Como resolver: acrescente void repor(int quantidade); dentro da classe, ou acerte o cabeçalho para ficar igual ao da declaração.
#include <iostream>
#include <string>
class Produto {
public:
std::string nome;
int preco;
int estoque;
void mostrar();
};
int main() {
Produto regua{"régua", 5, 20};
regua.mostrar();
}
$ g++ -std=c++23 -Wall -Wextra declarasemdefinir.cpp -o declarasemdefinir /usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccfkcJOb.o: na função "main": declarasemdefinir.cpp:(.text+0x7d): referência não definida para "Produto::mostrar()" collect2: error: ld returned 1 exit status
O que aconteceu: o compilador aceitou o código (a classe promete que mostrar existe), mas quem reclamou foi o ligador (ld, o linker), a última etapa, que junta os pedaços (aula Como o C++ Vira Programa). Ele procurou o corpo de Produto::mostrar() e não achou: "referência não definida". Repare que esta mensagem saiu em português: ela vem do ld, que tem tradução no Ubuntu, e não do g++. Em sistemas em inglês, aparece undefined reference to `Produto::mostrar()'. O nome /tmp/cc….o é um arquivo temporário e muda a cada compilação.
Como resolver: escreva a definição void Produto::mostrar() { … }. Num projeto com vários arquivos, confira se o .cpp que tem a definição entrou no comando de compilação (aula Compilar Vários Arquivos).
#include <iostream>
#include <string>
class Produto {
public:
std::string nome;
int preco;
int estoque;
int preco() {
return preco;
}
};
int main() {
Produto regua{"régua", 5, 20};
std::cout << regua.preco() << "\n";
}
$ g++ -std=c++23 -Wall -Wextra mesmonome.cpp -o mesmonome
mesmonome.cpp:12:5: error: ‘int Produto::preco()’ conflicts with a previous declaration
12 | }
| ^
mesmonome.cpp:7:9: note: previous declaration ‘int Produto::preco’
7 | int preco;
| ^~~~~
mesmonome.cpp: In function ‘int main()’:
mesmonome.cpp:17:29: error: expression cannot be used as a function
17 | std::cout << regua.preco() << "\n";
| ~~~~~~~~~~~^~
O que aconteceu: numa classe, cada nome só pode ser usado uma vez. O método preco() "conflita com uma declaração anterior", o atributo int preco da linha 7. O segundo erro é consequência do primeiro: para o compilador, regua.preco continua sendo o int, e um int não pode ser chamado como função.
Como resolver: dê nomes diferentes. Existem dois costumes, e você vai ver os dois por aí: mudar o método (getPreco()) ou mudar o atributo, com um sinal que o marca como dado interno (preco_ ou m_preco). A aula public e private usa o segundo, que permite o método se chamar simplesmente preco().
Referência rápida
| Escrita | O que faz |
|---|---|
void mostrar() { … } (dentro da classe) | Declara e define um método de uma vez. |
obj.mostrar(); | Chama o método sobre o objeto obj. Parênteses obrigatórios. |
int x = obj.valorEmEstoque(); | Usa o valor devolvido por um método. |
obj.repor(4); | Passa argumentos ao método. |
estoque += q; (dentro de um método) | Muda o atributo do objeto que chamou. |
mostrar(); (dentro de outro método) | Chama outro método do mesmo objeto. |
void mostrar(); (dentro da classe) | Só declara; o corpo fica fora. |
void Produto::mostrar() { … } | Define, fora da classe, o método declarado. |
Exercícios
1. Chamar o método
Complete para mandar o objeto cola se mostrar:
2. Definir fora
A classe Produto declara void repor(int quantidade);. Complete o começo da definição fora da classe:
3. Dentro do método
Complete o corpo do método int dobroDoPreco() para devolver o dobro do preço do objeto:
4. Por que o método mostrar() não precisa receber o produto como parâmetro?
5. Dentro de vender, a linha mostrar(); mostra qual produto?
6. O g++ diz In function ‘void mostrar()’: … ‘nome’ was not declared in this scope. O que você provavelmente esqueceu?
Missão final: o caixa da lojinha
Missão: a classe já declara dois métodos novos. Escreva as definições fora da classe, com Produto::: reajustar(int reais) soma reais ao preço; totalDaVenda(int quantidade) devolve preco * quantidade. A saída deve ser:
3 cadernos: R$ 36 Caderno agora custa R$ 14
#include <iostream>
#include <string>
class Produto {
public:
std::string nome;
int preco;
int estoque;
void reajustar(int reais);
int totalDaVenda(int quantidade);
};
// escreva as duas definições aqui
int main() {
Produto caderno{"caderno", 12, 8};
std::cout << "3 cadernos: R$ " << caderno.totalDaVenda(3) << "\n";
caderno.reajustar(2);
std::cout << "Caderno agora custa R$ " << caderno.preco << "\n";
}
#include <iostream>
#include <string>
class Produto {
public:
std::string nome;
int preco;
int estoque;
void reajustar(int reais);
int totalDaVenda(int quantidade);
};
void Produto::reajustar(int reais) {
preco += reais;
}
int Produto::totalDaVenda(int quantidade) {
return preco * quantidade;
}
int main() {
Produto caderno{"caderno", 12, 8};
std::cout << "3 cadernos: R$ " << caderno.totalDaVenda(3) << "\n";
caderno.reajustar(2);
std::cout << "Caderno agora custa R$ " << caderno.preco << "\n";
}
Resumo
- Método = função que mora dentro da classe. Chama-se com
objeto.metodo(argumentos); os parênteses são obrigatórios. - O método enxerga os atributos direto (
nome,preco), sempre os do objeto que o chamou. - Métodos podem devolver valores (
int valorEmEstoque()), receber parâmetros (repor(int)) e mudar o objeto, sem precisar de&. - Dentro de um método, outro método do mesmo objeto se chama sem ponto:
mostrar();. - Dá para declarar na classe (
void mostrar();) e definir fora (void Produto::mostrar() { … }). Essa é a base dos arquivos.he.cpp. - Erros: sem parênteses →
invalid use of non-static member function; semProduto::→‘nome’ was not declared; sem declaração →no declaration matches; sem definição →referência não definida(do ligador); nome repetido →conflicts with a previous declaration. - Próxima aula: fazer o objeto já nascer preenchido e válido, com Construtores.