Outro nome para a mesma coisa
Na página de parâmetros de função, cada argumento era copiado para dentro da função. Essa cópia é o motivo de uma função não conseguir alterar a variável do chamador: ela só enxerga sua própria duplicata. Uma referência quebra essa parede. Ela é um alias: um segundo nome ligado a uma variável existente, compartilhando exatamente a mesma memória.
Você cria uma referência com & na declaração. Uma vez ligada, a referência e o original são indistinguíveis:
Duas regras tornam as referências seguras e previsíveis: uma referência deve ser inicializada no momento em que é declarada (int& r; é um erro de compilação) e nunca pode ser reassociada para apontar para outra coisa depois. Atribuir a uma referência sempre escreve naquilo a que ela foi originalmente ligada.
Passagem por referência: deixe a função alcançar de volta
O verdadeiro ganho está nas funções. Coloque & em um parâmetro e a função recebe um alias do argumento do chamador em vez de uma cópia. Agora as mudanças dentro da função ficam visíveis fora dela:
Tire o & e addBonus incrementaria uma cópia descartável, deixando total em 100. Esse único caractere é toda a diferença. Esta é a forma canônica de escrever uma função que retorna mais de um resultado ou edita sua entrada no lugar. O exemplo clássico é trocar duas variáveis:
Sem referências, swapValues só trocaria cópias locais e x/y continuariam 1 2. (A biblioteca padrão já tem std::swap, mas escrevê-lo você mesmo mostra exatamente o que um parâmetro por referência oferece.)
Referências const: leia rápido, prometa não tocar
A passagem por referência também evita copiar — e para um objeto grande essa cópia pode ser cara. Mas um parâmetro T& simples sinaliza "eu posso modificar isto", o que é enganoso quando você só quer ler. A solução é const T&: você ganha a velocidade sem cópia de uma referência e uma promessa, garantida pelo compilador, de que a função não vai alterar o argumento.
Uma referência não const só pode se ligar a uma variável modificável, mas uma referência const também pode se ligar a literais e temporários — é por isso que greet("literal works too") compila. Uma regra prática para escolher o tipo de um parâmetro:
void f(int x) // tipo barato, somente leitura -> apenas copie
void f(const string& s) // tipo pesado, somente leitura -> referência const
void f(string& s) // você pretende modificar o objeto do chamador
Use const T& por padrão para qualquer tipo de classe que você só lê (string, vector, seus próprios structs), e reserve uma referência não const para quando você realmente quiser escrever de volta.
Retornando uma referência
Uma função também pode retornar uma referência, entregando ao chamador um alias de algo que já existe. Isso é comum em código do tipo container — é o que faz v[i] = 5 funcionar e o que operator[] faz por baixo dos panos:
Como at retorna int&, a expressão de chamada at(data, 1) é, ela mesma, um lvalue ao qual você pode atribuir. Retorne um int simples e at(data, 1) = 42 não compilaria — você estaria atribuindo a uma cópia temporária.
A grande armadilha: referências pendentes
Uma referência não é dona de nada; ela apenas aponta para memória que vive em outro lugar. Se essa memória morre enquanto a referência ainda está em uso, você tem uma referência pendente, e ler através dela é comportamento indefinido — pode imprimir lixo, travar, ou parecer funcionar até arruinar seu dia em produção. O erro clássico é retornar uma referência para uma variável local:
int& broken() {
int local = 42;
return local; // BUG: local é destruído quando broken() retorna
} // a referência retornada fica pendente
int main() {
int& r = broken();
cout << r << "\n"; // COMPORTAMENTO INDEFINIDO - lê memória morta
}
A variável local some no instante em que broken retorna, então a referência aponta para espaço de pilha já recuperado. Retorne apenas uma referência para algo que sobreviva à chamada: um parâmetro passado por referência, um membro de dados ou um static. Se o valor é calculado dentro da função, retorne por valor e deixe o compilador otimizar a cópia. A mesma armadilha atinge os laços baseados em intervalo e qualquer referência ligada a um temporário: nunca mantenha uma referência além do tempo de vida daquilo que ela nomeia.
Próximo: sobrecarga de funções
As referências dão a você um segundo botão em cada parâmetro (cópia versus alias, mutável versus const) e esse botão interage diretamente com o próximo tópico. A seguir, a sobrecarga de funções permite que você defina várias funções com o mesmo nome mas listas de parâmetros diferentes, e o compilador escolhe a correta combinando os tipos dos argumentos — incluindo se eles são passados por valor, por referência ou por referência const.
Perguntas frequentes
O que é uma referência em C++?
Uma referência é um alias para uma variável existente: outro nome para a mesma memória. Você cria uma com & na declaração: int& r = x;. A partir daí, r e x são intercambiáveis; mudar uma muda a outra. Referências precisam ser inicializadas ao serem declaradas e nunca podem ser reassociadas para apontar para outra variável.
Qual é a diferença entre passagem por valor e passagem por referência em C++?
A passagem por valor (void f(int x)) copia o argumento, então a função trabalha sobre sua própria cópia e a variável do chamador permanece intacta. A passagem por referência (void f(int& x)) dá à função acesso direto à variável do chamador, então as mudanças ficam visíveis após a chamada — e nenhuma cópia é feita, o que importa para objetos grandes.
Quando devo usar parâmetros por referência constante em C++?
Use const T& quando uma função só precisa ler um parâmetro mas o tipo é caro de copiar (string, vector, structs grandes). Você ganha a velocidade sem cópia de uma referência mais a garantia do compilador de que a função não vai modificar o valor do chamador. Para tipos baratos como int ou double, a simples passagem por valor é mais fácil e igualmente rápida.