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

Você vai aprender
  • 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 do public:, o main pode 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; em caderno.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 soltaMétodo
Onde moraFora da classeDentro da classe
Como recebe o produtoPor parâmetro: void mostrar(const Produto& p)Automaticamente: é o objeto antes do ponto
Como usa os atributosp.nomenome
Como chamamostrar(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 chamada caderno.repor(4), quantidade vale 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.

Dois tipos de método. Alguns só olham o objeto (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 quantidade unidades?
if (!temEstoque(quantidade))
Chama outro método do mesmo objeto, sem ponto. Quando o caderno executa vender, o temEstoque chamado 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 main pode ignorar o resultado (como nas duas primeiras chamadas) ou usá-lo num if (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 é o mostrar que pertence à classe Produto". O :: é o operador de escopo, o mesmo de std::cout (o cout que pertence a std, 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.

Isso abre a porta para os arquivos .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çãoOnde 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 arquivosDeclaração no .h, definição no .cpp (aula Arquivos .h e .cpp).
Exemplos curtos deste guiaDentro, 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.

Esquecer os parênteses na chamada
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.

Chamar o método sem o objeto
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();.

Definir fora e esquecer o 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() { … }.

Definir fora um método que a classe não declarou
#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.

Declarar e esquecer de definir
#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).

Um método com o mesmo nome de um atributo
#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

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

void (int quantidade) {

3. Dentro do método

Complete o corpo do método int dobroDoPreco() para devolver o dobro do preço do objeto:

return ;

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 .h e .cpp.
  • Erros: sem parênteses → invalid use of non-static member function; sem Produto:: → ‘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.