C++ Por Valor × Por Referência

Quando você chama aumentar(estoque), a função recebe o quê: a própria variável estoque ou uma cópia do valor dela? Em C++, você escolhe. Por padrão, a função recebe uma cópia (passagem por valor), e nada do que ela fizer muda a variável de fora. Com um & no parâmetro, ela recebe a própria variável (passagem por referência) e pode mudá-la. Nesta aula você vai ver as duas, entender o const&, que evita cópias caras de textos e vetores, e saber qual usar em cada caso.

Você vai aprender
  • O que é passagem por valor, com a prova de que a variável de fora não muda.
  • O que é passagem por referência (int& x), com o exemplo clássico: trocar dois valores.
  • Por que aumentar(5) não compila quando o parâmetro é int&.
  • O const&: receber sem copiar e sem poder mudar, ideal para std::string e std::vector, e quanto tempo uma cópia custa.
  • Uma tabela para decidir entre valor, & e const&.
  • A ideia da passagem por ponteiro, que você vai ver em código antigo e em C.

Todos os programas desta aula foram compilados com o g++ 15 na máquina fictícia da Ana, na pasta ~/cpp, com o comando padrão do guia, g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.


A ideia: a fotocópia e o original

A Ana precisa que o contador revise a lista de compras da loja. Ela pode fazer duas coisas:

  • Entregar uma fotocópia. O contador rabisca, corrige, risca o que quiser na cópia. A lista original da Ana, na gaveta, continua igual. Isso é passar por valor.
  • Entregar a lista original. Tudo o que o contador escrever fica na lista da Ana. Isso é passar por referência.

E existe uma terceira opção: entregar o original numa pasta lacrada com janelinha: o contador lê tudo, mas não consegue escrever. Ninguém gastou papel com cópia, e a lista está protegida. Isso é o const&.


1. Por valor: a função recebe uma cópia

É o jeito que você usou em todas as aulas até aqui. A prova de que é uma cópia:

#include <iostream>

void aumentar(int x) {
    x = x + 10;
    std::cout << "dentro da função: " << x << "\n";
}

int main() {
    int estoque = 5;
    aumentar(estoque);
    std::cout << "no main: " << estoque << "\n";
}
$ g++ -std=c++23 -Wall -Wextra valor.cpp -o valor
$ ./valor
dentro da função: 15
no main: 5                   « o estoque do main não mudou
  1. No main, a caixa estoque guarda 5.
  2. Na chamada aumentar(estoque), o C++ cria uma caixa nova, x, e copia o 5 para ela.
  3. A função muda a sua caixa: x vira 15. A caixa estoque, lá no main, nem fica sabendo.
  4. A função termina e a caixa x é jogada fora. estoque continua 5.

Isso não é um defeito: é uma proteção. Quando você chama uma função passando por valor, tem a garantia de que suas variáveis voltam intactas. Na maior parte do tempo, é isso que você quer.

O exemplo clássico: trocar dois valores (que não troca)

#include <iostream>

void trocar(int a, int b) {
    int guarda = a;
    a = b;
    b = guarda;
}

int main() {
    int x = 1;
    int y = 2;
    trocar(x, y);
    std::cout << "x = " << x << ", y = " << y << "\n";
}
$ g++ -std=c++23 -Wall -Wextra trocar1.cpp -o trocar1
$ ./trocar1
x = 1, y = 2                 « nada foi trocado

A função trocou direitinho… as cópias a e b, que morreram no fim da função. Para trocar as variáveis do main, a função precisa receber as próprias variáveis.


2. Por referência: & e a função mexe no original

Basta um & (lê-se "e comercial") depois do tipo do parâmetro:

#include <iostream>

void aumentar(int& x) {
    x = x + 10;
    std::cout << "dentro da função: " << x << "\n";
}

int main() {
    int estoque = 5;
    aumentar(estoque);
    std::cout << "no main: " << estoque << "\n";
}
int& x
"x é uma referência a um int". Uma referência é um apelido: x não é uma caixa nova, é outro nome para a caixa que veio na chamada.
x = x + 10;
Como x é um apelido de estoque, mudar x é mudar estoque.
aumentar(estoque);
A chamada é igual à da versão por valor. Só olhando a função dá para saber que ela pode mudar o argumento.
$ g++ -std=c++23 -Wall -Wextra referencia.cpp -o referencia
$ ./referencia
dentro da função: 15
no main: 15                  « agora o estoque do main mudou

A única diferença entre valor.cpp e referencia.cpp é o & na linha 3. Referências também existem fora de funções (int& apelido = estoque;); a aula Referências as explica em detalhe.

Onde fica o &?

int& x, int &x e int & x são a mesma coisa para o compilador. Este guia escreve int& x, colado no tipo, porque "referência a int" é o tipo do parâmetro.

Agora a troca funciona

#include <iostream>

void trocar(int& a, int& b) {
    int guarda = a;
    a = b;
    b = guarda;
}

int main() {
    int x = 1;
    int y = 2;
    trocar(x, y);
    std::cout << "x = " << x << ", y = " << y << "\n";
}
$ g++ -std=c++23 -Wall -Wextra trocar2.cpp -o trocar2
$ ./trocar2
x = 2, y = 1

a é apelido de x, b é apelido de y. A variável guarda é necessária pelo mesmo motivo de quando você troca o conteúdo de dois copos: precisa de um terceiro copo vazio. (A biblioteca padrão já tem essa função pronta, std::swap, em <utility>: std::swap(x, y);.)

Com vetores, a diferença aparece do mesmo jeito

#include <iostream>
#include <vector>

void darBonus(std::vector<double> notas) {
    for (double& n : notas) {
        n = n + 1;
    }
}

void darBonusDeVerdade(std::vector<double>& notas) {
    for (double& n : notas) {
        n = n + 1;
    }
}

void mostrar(const std::vector<double>& notas) {
    for (double n : notas) {
        std::cout << n << " ";
    }
    std::cout << "\n";
}

int main() {
    std::vector<double> notas = {7.5, 8, 6.5};
    darBonus(notas);
    mostrar(notas);
    darBonusDeVerdade(notas);
    mostrar(notas);
}
$ g++ -std=c++23 -Wall -Wextra vetor.cpp -o vetor
$ ./vetor
7.5 8 6.5                    « darBonus mexeu numa cópia do vetor inteiro
8.5 9 7.5                    « darBonusDeVerdade mexeu no vetor da Ana

O double& n dentro do for é o mesmo truque, num laço: cada n é um apelido de um elemento do vetor (aula for de Intervalo). O mostrar usa const&, que é a próxima seção.

Missão: a função dobrar deveria dobrar a quantidade da Ana, mas o Depois continua 4. Rode, entenda por quê e conserte com um caractere.

#include <iostream>

void dobrar(int quantidade) {
    quantidade = quantidade * 2;
}

int main() {
    int canetas = 4;
    std::cout << "Antes: " << canetas << "\n";
    dobrar(canetas);
    std::cout << "Depois: " << canetas << "\n";
}
#include <iostream>

void dobrar(int& quantidade) {
    quantidade = quantidade * 2;
}

int main() {
    int canetas = 4;
    std::cout << "Antes: " << canetas << "\n";
    dobrar(canetas);
    std::cout << "Depois: " << canetas << "\n";
}

Uma referência precisa de uma variável

Se a função vai escrever no argumento, o argumento tem de ser um lugar onde se possa escrever: uma variável. Um número solto não serve:

#include <iostream>

void aumentar(int& x) {
    x = x + 10;
}

int main() {
    aumentar(5);
}
$ g++ -std=c++23 -Wall -Wextra literal.cpp -o literal
literal.cpp: In function ‘int main()’:
literal.cpp:8:14: error: cannot bind non-const lvalue reference of type ‘int&’ to an rvalue of type ‘int’
    8 |     aumentar(5);
      |              ^
literal.cpp:3:20: note: initializing argument 1 of ‘void aumentar(int&)’
    3 | void aumentar(int& x) {
      |               ~~~~~^
cannot bind … reference of type ‘int&’
"Não é possível ligar uma referência int&…"
to an rvalue of type ‘int’
"…a um valor temporário do tipo int". Rvalue é o nome técnico de um valor que não mora numa variável: o 5, o resultado de x + 1. Não faz sentido aumentar o número 5 "no original".
non-const lvalue reference
A mensagem já dá a dica: uma referência const aceitaria o 5. É o que vem a seguir.

3. const&: sem cópia e sem mudanças

Copiar um int é instantâneo: são poucos bytes. Mas copiar um std::string com um texto longo, ou um std::vector com milhares de notas, dá trabalho ao computador: ele precisa reservar memória nova e copiar elemento por elemento, a cada chamada. Quando a função só precisa ler, passe por const& (referência constante):

#include <iostream>
#include <string>
#include <vector>

double media(const std::vector<double>& notas) {
    double soma = 0;
    for (double n : notas) {
        soma += n;
    }
    return soma / notas.size();
}

void mostrar(const std::string& texto) {
    std::cout << "[" << texto << "]\n";
}

int main() {
    std::vector<double> notas = {7.5, 8, 6.5, 9};
    std::cout << "Média: " << media(notas) << "\n";
    std::string aviso = "Prova na sexta";
    mostrar(aviso);
    mostrar("Trazer calculadora");
}
const std::vector<double>& notas
Lê-se da direita para a esquerda: notas é uma referência (&) a um vetor de double constante (const). Sem cópia, como no &; sem poder mudar, por causa do const.
mostrar("Trazer calculadora");
Diferente do int&, um const& aceita valores temporários: o C++ cria um std::string temporário com o texto e liga a referência a ele. Já que a função não vai escrever, não há problema.
$ g++ -std=c++23 -Wall -Wextra constref.cpp -o constref
$ ./constref
Média: 7.75
[Prova na sexta]
[Trazer calculadora]

O const é uma promessa, e o compilador cobra

Se a função tentar mudar um parâmetro const&, não compila. Com um int:

void zerar(const int& x) {
    x = 0;
}

int main() {
    int n = 5;
    zerar(n);
}
$ g++ -std=c++23 -Wall -Wextra constint.cpp -o constint
constint.cpp: In function ‘void zerar(const int&)’:
constint.cpp:2:7: error: assignment of read-only reference ‘x’
    2 |     x = 0;
      |     ~~^~~

Assignment of read-only reference ‘x’ = "atribuição a uma referência somente leitura ‘x’". Com um vetor, a mensagem é mais longa, mas o começo diz o mesmo:

$ g++ -std=c++23 -Wall -Wextra constmuda.cpp -o constmuda        « notas.push_back(10); dentro de media
constmuda.cpp: In function ‘double media(const std::vector<double>&)’:
constmuda.cpp:5:20: error: passing ‘const std::vector<double>’ as ‘this’ argument discards qualifiers [-fpermissive]
    5 |     notas.push_back(10);
      |     ~~~~~~~~~~~~~~~^~~~
In file included from /usr/include/c++/15/vector:68,
                 from constmuda.cpp:2:
/usr/include/c++/15/bits/stl_vector.h:1433:7: note:   in call to ‘constexpr void std::vector< <template-parameter-1-1>, <template-parameter-1-2> >::push_back(value_type&&) [with _Tp = double; _Alloc = std::allocator<double>; value_type = double]’
 1433 |       push_back(value_type&& __x)
      |       ^~~~~~~~~

Leia só a primeira linha de erro: passing ‘const std::vector<double>’ … discards qualifiers = "passar um vetor const para push_back descartaria o const". Em outras palavras: push_back muda o vetor, e o vetor é somente leitura. A nota de baixo mostra onde o push_back está escrito dentro da biblioteca padrão; não é seu código. A proteção é justamente o ponto: quem chama media(notas) tem a garantia de que as notas voltam iguais.

Quanto custa uma cópia?

Para sentir a diferença, a Ana escreveu dois programas iguais, com um vetor de um milhão de números passado 500 vezes a uma função que só lê o primeiro elemento. A única diferença é o parâmetro:

#include <iostream>
#include <vector>

int primeiro(const std::vector<int>& v) {      // em copia-valor.cpp: std::vector<int> v
    return v[0];
}

int main() {
    std::vector<int> numeros(1'000'000, 7);
    long long total = 0;
    for (int i = 0; i < 500; i++) {
        total += primeiro(numeros);
    }
    std::cout << total << "\n";
}
$ time ./copia-valor
3500

real	0m0,206s                « 500 cópias de um milhão de números
user	0m0,196s
sys	0m0,010s
$ time ./copia-ref
3500

real	0m0,016s                « nenhuma cópia: mais de 10 vezes mais rápido
user	0m0,013s
sys	0m0,002s

O comando time mede quanto tempo o programa levou (real é o tempo de relógio). Os números mudam um pouco a cada execução e de máquina para máquina, mas a proporção fica. Num programa pequeno, 0,2 segundo não incomoda; num programa que chama a função milhões de vezes, ou com textos enormes, incomoda muito. Por isso a regra do C++ moderno é simples: textos, vetores e objetos grandes que a função só lê: const&.


4. Qual usar

SituaçãoParâmetroExemplo
Tipo pequeno (int, double, char, bool) que a função só lêPor valordouble dobro(double x)
Tipo grande (std::string, std::vector, structs) que a função só lêconst&double media(const std::vector<double>& notas)
A função precisa mudar a variável de quem chama&void trocar(int& a, int& b)
A função precisa de uma cópia própria para mexer à vontadePor valor (mesmo se for grande)std::string maiusculas(std::string s), que altera s e devolve
A função calcula um resultado novoPrefira devolver com return a escrever num &int dobro(int x) em vez de void dobro(int x, int& resultado)

Sobre a última linha: em código antigo você vai ver funções que "devolvem" resultados escrevendo em parâmetros &. Hoje, com retorno de struct e std::pair, isso é raro: int r = dobro(5); deixa claro de onde veio o valor; dobro(5, r); esconde.

O & tem dois significados em C++

No tipo (int& x, num parâmetro ou numa declaração), & quer dizer "referência". Numa expressão (&estoque), quer dizer "o endereço de", e aparece com ponteiros, logo abaixo. E && entre condições é o "e" lógico (Operadores Lógicos). Mesmo símbolo, papéis diferentes.


5. Por ponteiro: só a ideia

Antes das referências existirem (em C, a linguagem "mãe" do C++, não há referências), o jeito de uma função mudar uma variável de fora era receber o endereço dela na memória, um ponteiro:

#include <iostream>

void aumentar(int* x) {
    *x = *x + 10;
}

int main() {
    int estoque = 5;
    aumentar(&estoque);
    std::cout << "no main: " << estoque << "\n";
}
$ g++ -std=c++23 -Wall -Wextra ponteiro.cpp -o ponteiro
$ ./ponteiro
no main: 15
TrechoLeia como
int* xx guarda o endereço de um int
&estoque"o endereço de estoque": a chamada deixa visível que a variável pode mudar
*x"o int que está nesse endereço"

O resultado é o mesmo da referência, com mais símbolos e mais jeitos de errar (um ponteiro pode ser nulo, uma referência não). Em C++ moderno, para "a função muda a variável", prefira &. Ponteiros têm seus usos, explicados com calma em Ponteiros e nullptr; e o const com ponteiros e referências em const com Ponteiros e Referências.


Quando dá errado

A função "não funcionou": a variável de fora não mudou
$ ./trocar1
x = 1, y = 2

O que significa: o parâmetro é por valor; a função mudou só a cópia. Não há erro nem aviso, porque mudar a cópia é permitido.

Como consertar: se a função deve mudar a variável de quem chama, use & no parâmetro: void trocar(int& a, int& b). Se ela deve calcular algo novo, devolva com return e guarde o resultado: estoque = aumentado(estoque);.

error: cannot bind non-const lvalue reference of type ‘int&’ to an rvalue of type ‘int’

O que significa: o parâmetro é int& (a função pode escrever nele) e você mandou um valor que não é variável: aumentar(5), aumentar(x + 1). No editor do guia, o Clang diz candidate function not viable: expects an lvalue for 1st argument ("espera uma variável no 1º argumento").

Como consertar: passe uma variável (int n = 5; aumentar(n);). Se a função não precisa mudar o argumento, troque o parâmetro por valor (int x) ou por const int& x.

error: assignment of read-only reference ou passing ‘const …’ as ‘this’ argument discards qualifiers

O que significa: a função recebeu const& e tentou mudar o parâmetro: atribuir (x = 0) ou chamar algo que muda (push_back, clear, +=).

Como consertar: decida o que a função deve fazer. Se ela deve mudar o original, tire o const (std::vector<double>&). Se ela só precisa de uma versão modificada para uso interno, receba por valor (uma cópia) e mexa na cópia.

O programa ficou lento com textos ou vetores grandes

O que significa: funções que recebem std::string ou std::vector por valor copiam tudo a cada chamada.

Como consertar: se a função só lê, troque std::vector<int> v por const std::vector<int>& v. As chamadas não mudam.


Referência rápida

ParâmetroCopia?A função pode mudar o original?Aceita f(5)?
int xSimNão (muda só a cópia)Sim
int& xNãoSimNão
const int& xNãoNão (erro de compilação)Sim
int* x (chamada f(&n))Copia só o endereçoSim, com *x = …Não

Exercícios

1. Mudar o original

Complete o parâmetro para que a função zere a variável de quem chama:

void zerar() { x = 0; }

2. Ler sem copiar

Complete o parâmetro de uma função que só mostra um texto, sem copiá-lo e sem poder mudá-lo:

void mostrar() { std::cout << texto; }

3. O que aparece?

void f(int x) { x = 99; } e, no main: int n = 1; f(n); std::cout << n;

4. Qual é o melhor parâmetro para uma função que calcula a média de um std::vector<double> sem alterá-lo?

5. A função é void aumentar(int& x). Qual chamada não compila?

6. Para um parâmetro int que a função só lê, o que o guia recomenda?

Missão final: promoção na lojinha

Missão: complete as duas funções escolhendo o tipo de passagem certo para cada uma. aplicarDesconto deve mudar os preços da Ana (cada preço vira preço * (1 - desconto)). total só lê e devolve a soma, sem copiar o vetor. Saída esperada:

Antes: 10 20 50
Depois: 9 18 45
Total: 72
#include <iostream>
#include <vector>

void aplicarDesconto(std::vector<double> precos, double desconto) {
    for (double& p : precos) {
        p = p * (1 - desconto);
    }
}

double total(std::vector<double> precos) {
    double soma = 0;
    for (double p : precos) {
        soma += p;
    }
    return soma;
}

void mostrar(const std::vector<double>& precos) {
    for (double p : precos) {
        std::cout << " " << p;
    }
    std::cout << "\n";
}

int main() {
    std::vector<double> precos = {10, 20, 50};
    std::cout << "Antes:";
    mostrar(precos);
    aplicarDesconto(precos, 0.1);
    std::cout << "Depois:";
    mostrar(precos);
    std::cout << "Total: " << total(precos) << "\n";
}
#include <iostream>
#include <vector>

void aplicarDesconto(std::vector<double>& precos, double desconto) {
    for (double& p : precos) {
        p = p * (1 - desconto);
    }
}

double total(const std::vector<double>& precos) {
    double soma = 0;
    for (double p : precos) {
        soma += p;
    }
    return soma;
}

void mostrar(const std::vector<double>& precos) {
    for (double p : precos) {
        std::cout << " " << p;
    }
    std::cout << "\n";
}

int main() {
    std::vector<double> precos = {10, 20, 50};
    std::cout << "Antes:";
    mostrar(precos);
    aplicarDesconto(precos, 0.1);
    std::cout << "Depois:";
    mostrar(precos);
    std::cout << "Total: " << total(precos) << "\n";
}

Resumo

  • Por valor (int x): a função recebe uma cópia. Mudar o parâmetro não muda a variável de fora. É o padrão e é seguro.
  • Por referência (int& x): o parâmetro é um apelido da variável de quem chama; mudar um muda o outro. Só aceita variáveis, não valores soltos como 5.
  • const&: sem cópia e sem permissão para mudar. É o jeito certo de passar std::string, std::vector e outros tipos grandes que a função só lê. Copiar um vetor de um milhão de números 500 vezes levou 0,2 s; passar por const&, 0,016 s.
  • Tipos pequenos que a função só lê: por valor. Para devolver resultados: prefira return a escrever num &.
  • Por ponteiro (int* x, chamada f(&n)) faz o mesmo que a referência, do jeito do C. Detalhes em Ponteiros.
  • Próxima aula: Sobrecarga de Funções, várias funções com o mesmo nome.