C++ Referências
Este é o começo do grupo Memória, a parte do C++ que mais assusta quem está aprendendo. Vamos devagar, com desenhos. A primeira peça é a mais simples: a referência, que é só um segundo nome para uma variável que já existe. Você escreve int& placar = pontos; e, a partir daí, placar e pontos são a mesma caixa. Mexeu num, mexeu no outro.
- O que é uma referência e como criar uma:
int& r = x;. - A diferença entre copiar uma variável e criar um apelido para ela.
- Por que toda referência precisa nascer ligada a alguém (o erro real do g++).
- Por que uma referência nunca "troca de alvo".
- A referência para
const: um apelido que só deixa ler. - A referência pendurada, que aponta para uma variável que já morreu, e o aviso
-Wreturn-local-addr.
Todos os programas foram compilados na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/cpp, com o comando padrão do guia, g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa (veja Compilar com g++). As saídas e mensagens são as reais do g++ 15.2.
A ideia: um apelido
A Ana tem um primo chamado Roberto. Em casa, todo mundo chama ele de Beto. "Roberto" e "Beto" não são duas pessoas: são dois nomes para a mesma pessoa. Se o Beto corta o cabelo, o Roberto está de cabelo cortado. Não tem como ser diferente.
Na aula de Variáveis, você aprendeu que uma variável é uma caixa com etiqueta. Uma referência é uma segunda etiqueta colada na mesma caixa. Não existe caixa nova, não existe cópia: só mais um nome.
| Etiquetas (nomes) | A caixa (o valor guardado) |
|---|---|
pontos e placar | 10 |
int pontos = 10; e int& placar = pontos;: uma caixa só, com duas etiquetas.1. Criar uma referência
Para criar uma referência, escreva o tipo, um & (o "e comercial") e o nome do apelido, e ligue-o a uma variável que já existe:
#include <iostream>
int main() {
int pontos = 10;
int& placar = pontos; // placar é outro nome para pontos
std::cout << "pontos = " << pontos << "\n";
std::cout << "placar = " << placar << "\n";
placar = 25; // muda pelo apelido
std::cout << "Depois de placar = 25:\n";
std::cout << "pontos = " << pontos << "\n";
pontos = pontos + 5; // muda pelo nome original
std::cout << "Depois de pontos + 5:\n";
std::cout << "placar = " << placar << "\n";
}
- int pontos = 10;
- Uma variável comum: a caixa
pontos, guardando 10. - int&
- Leia como "referência para int". O
&colado no tipo quer dizer: "isto não é uma caixa nova, é um apelido de uma caixaintque já existe". - placar
- O nome do apelido. Escolha como qualquer nome de variável (veja Nomes de Variáveis).
- = pontos;
- Aqui o
=não copia: ele diz de quemplacaré apelido. Isso acontece uma vez, na criação, e nunca mais muda. - placar = 25;
- Depois de criada, a referência se usa exatamente como a variável original. Escrever em
placarescreve na caixa depontos.
$ g++ -std=c++23 -Wall -Wextra apelido.cpp -o apelido $ ./apelido pontos = 10 placar = 10 Depois de placar = 25: pontos = 25 « mudou pelo apelido, o original mudou Depois de pontos + 5: placar = 30 « mudou pelo original, o apelido "vê" a mudança
Repare que nunca existiram dois números guardados. Existe um só, e dois jeitos de chamá-lo. O desenho, passo a passo:
| Linha | A caixa pontos / placar |
|---|---|
int pontos = 10; | 10 (só a etiqueta pontos) |
int& placar = pontos; | 10 (agora com as duas etiquetas) |
placar = 25; | 25 |
pontos = pontos + 5; | 30 |
Cópia × apelido
A diferença entre int copia = original; e int& apelido = original; é um único &, e muda tudo:
#include <iostream>
int main() {
int original = 10;
int copia = original; // caixa nova, com o valor copiado
int& apelido = original; // a MESMA caixa, com outro nome
original = 99;
std::cout << "original = " << original << "\n";
std::cout << "copia = " << copia << "\n";
std::cout << "apelido = " << apelido << "\n";
}
$ g++ -std=c++23 -Wall -Wextra copia.cpp -o copia $ ./copia original = 99 copia = 10 « a cópia ficou com o valor antigo apelido = 99 « o apelido é a própria caixa
| Caixa | Etiquetas | Valor no fim |
|---|---|---|
| caixa 1 | original, apelido | 99 |
| caixa 2 | copia | 10 |
Experimente: rode o programa. Depois troque int& vidas_jogo por int vidas_jogo (sem o &) e rode de novo. O que mudou na primeira linha da saída?
#include <iostream>
int main() {
int vidas = 3;
int& vidas_jogo = vidas;
vidas_jogo = vidas_jogo - 1; // o jogador perdeu uma vida
std::cout << "vidas = " << vidas << "\n";
std::cout << "vidas_jogo = " << vidas_jogo << "\n";
}
Onde fica o &?
Os espaços em volta do & não importam. As três linhas abaixo são idênticas para o compilador:
int& r = x; // o estilo deste guia: o & "faz parte do tipo" int &r = x; // estilo comum em código herdado do C int & r = x; // também vale
Este guia usa int& r, porque lê melhor: "r é um int&". Mas cuidado com uma armadilha: numa linha com vírgula, o & vale só para o primeiro nome.
#include <iostream>
int main() {
int x = 1;
int& a = x, b = x; // cuidado: só o a é referência!
a = 50;
b = 99;
std::cout << "x = " << x << "\n";
}
$ g++ -std=c++23 -Wall -Wextra virgula.cpp -o virgula
virgula.cpp: In function ‘int main()’:
virgula.cpp:5:17: warning: variable ‘b’ set but not used [-Wunused-but-set-variable]
5 | int& a = x, b = x; // cuidado: só o a é referência!
| ^
$ ./virgula
x = 50 « o b = 99 não chegou no x: b é uma cópia
O aviso entrega o problema: "a variável b recebeu valor, mas nunca foi usada". Se b fosse apelido de x, escrever nele seria usar x. Como b é uma caixa separada, o 99 foi parar numa caixa que ninguém lê. Regra simples: uma referência por linha.
2. Toda referência precisa nascer ligada
Um apelido sem dono não faz sentido: "Beto" de quem? Por isso o C++ exige que a referência seja ligada a uma variável na mesma linha em que é criada. Esquecer dá erro de compilação:
#include <iostream>
int main() {
int& r;
std::cout << r << "\n";
}
$ g++ -std=c++23 -Wall -Wextra sem-alvo.cpp -o sem-alvo
sem-alvo.cpp: In function ‘int main()’:
sem-alvo.cpp:4:10: error: ‘r’ declared as reference but not initialized
4 | int& r;
| ^
Tradução: "r foi declarada como referência, mas não foi inicializada". Inicializar é dar o primeiro valor, na criação. Para uma referência, o "primeiro valor" é quem ela apelida. Conserto: int& r = alguma_variavel;.
Isso é uma vantagem, não um castigo. Como toda referência nasce ligada, você nunca encontra uma referência "vazia" num programa que compilou. Os ponteiros, que você vai ver daqui a duas aulas, não têm essa garantia, e por isso dão mais trabalho.
3. A referência nunca troca de alvo
Uma vez apelido da Ana, sempre apelido da Ana. Depois da criação, todo = escrito na referência muda o valor da caixa, nunca a etiqueta de lugar. Veja o que acontece quando alguém tenta "apontar" a referência para outra variável:
#include <iostream>
int main() {
int ana = 7;
int bruno = 3;
int& r = ana; // r é apelido da ana, para sempre
r = bruno; // NÃO troca o alvo: copia o valor de bruno para ana
std::cout << "ana = " << ana << "\n";
std::cout << "bruno = " << bruno << "\n";
r = 100;
std::cout << "Depois de r = 100:\n";
std::cout << "ana = " << ana << "\n";
std::cout << "bruno = " << bruno << "\n";
}
$ g++ -std=c++23 -Wall -Wextra trocar.cpp -o trocar $ ./trocar ana = 3 « o 7 da ana foi trocado pelo 3 do bruno bruno = 3 Depois de r = 100: ana = 100 « r continua sendo a ana bruno = 3 « o bruno nunca foi tocado
int& r = ana;→ a etiquetaré colada na caixa daana. Esta é a única vez em que o="liga".r = bruno;→ lê o valor dobruno(3) e guarda na caixa da ana. A etiqueta não se mexe.r = 100;→ mais uma vez, a caixa daanarecebe 100.
Não existe comando para "descolar" a etiqueta. Se você precisa de algo que aponte ora para uma variável, ora para outra, isso é um ponteiro.
4. Referência para const: o apelido que só lê
Às vezes você quer dar um nome a uma variável sem dar permissão para mudá-la. Pense no extrato do banco: a Ana pode ver o saldo pelo aplicativo, mas não pode digitar um saldo novo. Para isso existe const int&, a "referência para const" (veja const em Constantes):
#include <iostream>
int main() {
int saldo = 500;
const int& so_leitura = saldo;
std::cout << "Saldo: " << so_leitura << "\n";
so_leitura = 0;
}
$ g++ -std=c++23 -Wall -Wextra const-ref.cpp -o const-ref
const-ref.cpp: In function ‘int main()’:
const-ref.cpp:8:16: error: assignment of read-only reference ‘so_leitura’
8 | so_leitura = 0;
| ~~~~~~~~~~~^~~
Tradução: "atribuição numa referência somente-leitura so_leitura". O compilador barrou a linha 8 antes de o programa existir. Ler pelo apelido pode; escrever, não.
Atenção: o const vale para o apelido, não para a caixa. A variável original continua podendo mudar, e o apelido vê a mudança:
#include <iostream>
int main() {
int saldo = 500;
const int& so_leitura = saldo;
std::cout << "Saldo: " << so_leitura << "\n";
saldo = saldo + 100; // o dono continua podendo mudar
std::cout << "Saldo: " << so_leitura << "\n";
}
$ g++ -std=c++23 -Wall -Wextra const-ref-ok.cpp -o const-ref-ok $ ./const-ref-ok Saldo: 500 Saldo: 600
Apelido de um número solto
Uma referência comum (int&) só pode apelidar uma variável, algo que tem caixa. Um número solto como 10 não tem caixa: o g++ recusa.
#include <iostream>
int main() {
int& r = 10;
std::cout << r << "\n";
}
$ g++ -std=c++23 -Wall -Wextra literal.cpp -o literal
literal.cpp: In function ‘int main()’:
literal.cpp:4:14: error: cannot bind non-const lvalue reference of type ‘int&’ to an rvalue of type ‘int’
4 | int& r = 10;
| ^~
A mensagem assusta, mas leia por partes:
- cannot bind = "não dá para ligar";
- non-const lvalue reference of type ‘int&’ = "uma referência comum (sem
const) do tipoint&"; - 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, como10ou o resultado dea + b. lvalue é o contrário: algo com caixa, que pode aparecer à esquerda (left) de um=.
Por que proibir? Se int& r = 10; fosse aceito, r = 11; estaria tentando mudar o número 10. Já com const não há risco, porque ninguém vai escrever pelo apelido. Então o C++ deixa, e cria uma caixinha escondida para guardar o 10:
#include <iostream>
int main() {
const int& r = 10;
std::cout << r << "\n";
}
$ g++ -std=c++23 -Wall -Wextra literal-const.cpp -o literal-const $ ./literal-const 10
Essa regra parece detalhe, mas é por causa dela que funções com parâmetro const std::string& aceitam tanto uma variável quanto um texto escrito direto na chamada. A aula const com Ponteiros e Referências volta a esse assunto.
5. Para que serve, na prática
Ninguém cria um apelido no meio do main só para ter dois nomes. As referências brilham em dois lugares, que têm aulas próprias:
Em parâmetros de função
Uma função normal recebe cópias dos valores. Com & no parâmetro, ela recebe um apelido da variável de quem chamou e pode mudá-la:
#include <iostream>
void dar_bonus(int& nota) { // nota é um apelido da variável de quem chamou
nota = nota + 1;
}
int main() {
int nota_ana = 8;
dar_bonus(nota_ana);
std::cout << "Nota da Ana: " << nota_ana << "\n";
}
$ g++ -std=c++23 -Wall -Wextra funcao.cpp -o funcao $ ./funcao Nota da Ana: 9
Durante a chamada, nota é o "Beto" da nota_ana. Os detalhes (quando usar cópia, & ou const&) estão em Por Valor × Por Referência.
No for de intervalo
No for (int n : notas), cada n é uma cópia. Com int& n, cada n é um apelido do elemento, e mudar n muda o vetor:
#include <iostream>
#include <vector>
int main() {
std::vector<int> notas = {6, 7, 8};
for (int n : notas) { // n é uma CÓPIA de cada nota
n = n + 1;
}
std::cout << "Sem &: " << notas[0] << " " << notas[1] << " " << notas[2] << "\n";
for (int& n : notas) { // n é um APELIDO de cada nota
n = n + 1;
}
std::cout << "Com &: " << notas[0] << " " << notas[1] << " " << notas[2] << "\n";
}
$ g++ -std=c++23 -Wall -Wextra for-ref.cpp -o for-ref $ ./for-ref Sem &: 6 7 8 « as cópias mudaram e foram jogadas fora Com &: 7 8 9 « os apelidos mudaram o vetor de verdade
Mais sobre isso em for de Intervalo.
6. A referência pendurada
Este é o perigo de verdade. Um apelido só vale enquanto a pessoa existe. Se a caixa some, a etiqueta fica pendurada no ar, colada em lugar nenhum. Usar uma referência assim é um erro grave, que o compilador nem sempre consegue barrar.
O jeito mais comum de criar uma referência pendurada é uma função devolver um apelido para uma variável local dela. Variáveis locais morrem quando a função termina (você vai ver o porquê na próxima aula, Como a Memória Funciona):
#include <iostream>
const int& dobro(int n) {
int resultado = n * 2;
return resultado; // devolve um apelido para uma variável local
}
int main() {
const int& r = dobro(21);
std::cout << "Dobro: " << r << "\n";
}
$ g++ -std=c++23 -Wall -Wextra pendurada.cpp -o pendurada
pendurada.cpp: In function ‘const int& dobro(int)’:
pendurada.cpp:5:12: warning: reference to local variable ‘resultado’ returned [-Wreturn-local-addr]
5 | return resultado; // devolve um apelido para uma variável local
| ^~~~~~~~~
pendurada.cpp:4:9: note: declared here
4 | int resultado = n * 2;
| ^~~~~~~~~
$ ./pendurada
Falha de segmentação (imagem do núcleo gravada) ./pendurada
$ echo $?
139
- const int& dobro(int n)
- O tipo de retorno tem
&: a função promete devolver um apelido, não um número. - warning: reference to local variable ‘resultado’ returned
- "Foi devolvida uma referência para a variável local
resultado". É só um aviso (warning): o programa foi gerado mesmo assim. Por isso o guia sempre compila com-Wall(veja Avisos). - [-Wreturn-local-addr]
- O nome do aviso: "devolver o endereço de algo local".
- note: declared here
- Uma nota que mostra onde a vítima nasceu: na linha 4, dentro da função.
- Falha de segmentação
- O programa tentou ler uma memória que não podia e o sistema o matou. Em inglês, a mesma mensagem é
Segmentation fault (core dumped). O código de saída139(128 + 11, o sinal 11,SIGSEGV) confirma. A aula nullptr mostra esse erro com calma.
Quando a dobro termina, a caixa resultado é desmontada. O main recebe uma etiqueta colada no vazio. Desta vez o programa caiu; em outra máquina, ou com outras opções, ele poderia mostrar um número qualquer, ou até o 42 certo por sorte. Isso se chama comportamento indefinido: o C++ não promete nada, e "funcionou no meu computador" não prova nada.
Se a função acima devolvesse int& (sem const), o g++ 15 com -std=c++23 nem compilaria: ele mostra error: cannot bind non-const lvalue reference of type ‘int&’ to an rvalue of type ‘int’ na linha do return. Com -std=c++20, o mesmo código passava só com o aviso -Wreturn-local-addr. O padrão novo fechou uma das portas, mas não todas: como você viu, com const int& o problema continua.
Conserto: devolva o valor, sem &. A função entrega uma cópia do resultado, que sobrevive ao fim dela:
#include <iostream>
int dobro(int n) {
int resultado = n * 2;
return resultado; // devolve uma CÓPIA do valor
}
int main() {
int r = dobro(21);
std::cout << "Dobro: " << r << "\n";
}
$ g++ -std=c++23 -Wall -Wextra pendurada-ok.cpp -o pendurada-ok $ ./pendurada-ok Dobro: 42
Nunca devolva & para algo que nasceu dentro da função. Devolver referência só faz sentido para algo que continua vivo depois que a função termina, como um elemento de um vetor que a própria função recebeu por referência. Na dúvida, devolva por valor: copiar um int não custa nada.
Cópia, & ou const&: qual usar
| Declaração | Cria caixa nova? | Pode mudar o original? | Use quando… |
|---|---|---|---|
int c = x; | Sim (cópia) | Não: mexe só na cópia | Você quer um valor independente. É o padrão para tipos pequenos (int, double, char, bool). |
int& r = x; | Não | Sim | Você quer mudar o original (parâmetro de saída, for que altera o vetor). |
const int& r = x; | Não | Não | Você só quer ler algo grande (uma std::string, um std::vector) sem pagar a cópia. |
Quando dá errado
$ g++ -std=c++23 -Wall -Wextra sem-alvo.cpp -o sem-alvo
sem-alvo.cpp: In function ‘int main()’:
sem-alvo.cpp:4:10: error: ‘r’ declared as reference but not initialized
4 | int& r;
| ^
O que aconteceu: você criou um apelido sem dizer de quem. No editor do guia (Clang), a mesma coisa aparece como declaration of reference variable 'r' requires an initializer.
Como resolver: ligue na criação: int& r = x;. Se não existe ainda uma variável para ligar, talvez você não queira uma referência, e sim uma variável comum.
$ g++ -std=c++23 -Wall -Wextra const-ref.cpp -o const-ref
const-ref.cpp: In function ‘int main()’:
const-ref.cpp:8:16: error: assignment of read-only reference ‘so_leitura’
8 | so_leitura = 0;
| ~~~~~~~~~~~^~~
O que aconteceu: você tentou escrever por uma referência const. O Clang diz cannot assign to variable 'so_leitura' with const-qualified type 'const int &'.
Como resolver: se a mudança é intencional, mude pelo nome original ou tire o const da referência. Se não é, o compilador acabou de salvar você de um bug.
$ g++ -std=c++23 -Wall -Wextra literal.cpp -o literal
literal.cpp: In function ‘int main()’:
literal.cpp:4:14: error: cannot bind non-const lvalue reference of type ‘int&’ to an rvalue of type ‘int’
4 | int& r = 10;
| ^~
O que aconteceu: você tentou ligar uma int& a algo que não é variável: um número, uma conta (a + b) ou o retorno de uma função que devolve por valor. A mesma mensagem aparece no return de uma função int& que devolve uma variável local, no C++23.
Como resolver: se é só para ler, use const int&. Se é para guardar o valor, use uma variável comum: int r = 10;.
$ ./pendurada Falha de segmentação (imagem do núcleo gravada) ./pendurada $ echo $? 139
O que aconteceu: a função devolveu um apelido de uma variável local, que morreu no fim da função. O aviso -Wreturn-local-addr tinha avisado na compilação. Em outras execuções, o sintoma pode ser um número sem sentido em vez da queda.
Como resolver: devolva por valor (int dobro(int n)). Trate esse aviso como erro: ele nunca é alarme falso.
Referência rápida
| Código | O que faz |
|---|---|
int& r = x; | Cria r, um apelido de x. Ler ou escrever r é ler ou escrever x. |
r = 5; | Guarda 5 na caixa de x (não muda de quem r é apelido). |
const int& r = x; | Apelido só de leitura. r = 5; dá erro; x = 5; continua valendo. |
const int& r = 10; | Permitido: o C++ cria uma caixa escondida para o 10. |
int& r = 10; | Erro: referência comum só apelida variáveis. |
int& r; | Erro: referência sem dono. |
void f(int& n) | Parâmetro por referência: a função pode mudar a variável de quem chamou. |
for (int& n : v) | Cada n é apelido de um elemento de v. |
int& f() { int x; return x; } | Nunca: referência pendurada (erro no C++23; com const int&, aviso -Wreturn-local-addr). |
Exercícios
1. Criar o apelido
Complete para que t seja um apelido (e não uma cópia) de total:
2. Só leitura
Complete para criar um apelido de nome que não deixa mudar o texto:
3. Depois de int a = 1; int b = 2; int& r = a; r = b; b = 9;, quanto valem a e b?
4. Qual destas linhas não compila?
5. Uma função devolve const int& para uma variável criada dentro dela. O que o g++ faz com -Wall?
Missão final: o caixa da lojinha
Missão: a lojinha da Ana já vendeu 50 reais (variável vendas). Crie uma referência chamada caixa que seja apelido de vendas. Depois, pelo apelido, some 30 (uma venda nova) e mostre as duas linhas abaixo:
Caixa: 80 Vendas: 80
#include <iostream>
int main() {
int vendas = 50;
// crie a referência caixa aqui
// some 30 pelo apelido
// mostre "Caixa: " e "Vendas: "
}
#include <iostream>
int main() {
int vendas = 50;
int& caixa = vendas;
caixa = caixa + 30;
std::cout << "Caixa: " << caixa << "\n";
std::cout << "Vendas: " << vendas << "\n";
}
Missão extra (provoque o erro): apague o = preco da linha da referência, deixando só int& valor;, e clique em Rodar. Leia a mensagem: ela diz que toda referência precisa nascer ligada.
#include <iostream>
int main() {
int preco = 12;
int& valor = preco;
std::cout << valor << "\n";
}
#include <iostream>
int main() {
int preco = 12;
int& valor;
std::cout << valor << "\n";
}
Resumo
- Uma referência é um apelido:
int& r = x;cola uma segunda etiqueta na caixa dex. Não cria caixa nova. - Mudar pelo apelido muda o original, e vice-versa.
- Toda referência precisa ser ligada na criação (
declared as reference but not initialized). - Ela nunca troca de alvo:
r = y;copia o valor deypara a caixa dex. const int&é um apelido só de leitura; pode até apelidar um número solto como10.- Nunca devolva referência para uma variável local: ela morre no fim da função e a referência fica pendurada (
-Wreturn-local-addr, comportamento indefinido). - Próxima aula: Como a Memória Funciona, para entender onde moram as caixas e por que as locais morrem.