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.
- 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 parastd::stringestd::vector, e quanto tempo uma cópia custa. - Uma tabela para decidir entre valor,
&econst&. - 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
- No
main, a caixaestoqueguarda 5. - Na chamada
aumentar(estoque), o C++ cria uma caixa nova,x, e copia o 5 para ela. - A função muda a sua caixa:
xvira 15. A caixaestoque, lá nomain, nem fica sabendo. - A função termina e a caixa
xé jogada fora.estoquecontinua 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 umint". Uma referência é um apelido:xnão é uma caixa nova, é outro nome para a caixa que veio na chamada. - x = x + 10;
- Como
xé um apelido deestoque, mudarxé mudarestoque. - 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.
&?
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: o5, o resultado dex + 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 dedoubleconstante (const). Sem cópia, como no&; sem poder mudar, por causa doconst. - mostrar("Trazer calculadora");
- Diferente do
int&, umconst&aceita valores temporários: o C++ cria umstd::stringtemporá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ção | Parâmetro | Exemplo |
|---|---|---|
Tipo pequeno (int, double, char, bool) que a função só lê | Por valor | double 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 à vontade | Por valor (mesmo se for grande) | std::string maiusculas(std::string s), que altera s e devolve |
| A função calcula um resultado novo | Prefira 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.
& 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
| Trecho | Leia como |
|---|---|
int* x | x 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
$ ./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 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âmetro | Copia? | A função pode mudar o original? | Aceita f(5)? |
|---|---|---|---|
int x | Sim | Não (muda só a cópia) | Sim |
int& x | Não | Sim | Não |
const int& x | Não | Não (erro de compilação) | Sim |
int* x (chamada f(&n)) | Copia só o endereço | Sim, 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:
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:
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 como5. const&: sem cópia e sem permissão para mudar. É o jeito certo de passarstd::string,std::vectore 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 porconst&, 0,016 s.- Tipos pequenos que a função só lê: por valor. Para devolver resultados: prefira
returna escrever num&. - Por ponteiro (
int* x, chamadaf(&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.