O que são as referências em C++?
Agora que já tratamos melhor sobre os tipos primitivos em C++ e sobre os fundamentos da utilização de variáveis (declaração e definição, inicialização e o uso de variáveis const), chegou a hora de falar de um assunto muito importante: as referências em C++. O que são, então, as tais referências?
Definição
Uma referência em C++ é um “apelido” (ou um nome alternativo) para um objeto já existente.
Como se declaram as referências em C++?
As referências são tipos que se definem em função de um tipo de base. Logo, para se declarar uma referência, utiliza-se o caractere & após o nome do tipo do objeto ao qual se deseja ligá-la.
int variavelNormal = 10; // Declaração de uma variável referência para int. int& variavelReferencia = variavelNormal; int &outraReferencia = variavelReferencia;
Como é possível ver no exemplo anterior, pouco importa se colocamos o símbolo & colado ao nome da variável ou colado ao tipo da variável: as duas formas são válidas. Além disso, vemos também que pode-se criar mais de uma referência para um único objeto (tantas referências quanto se quiser para um mesmo objeto), e que é possível inicializar referências a partir de outras referências (isto tem o mesmo efeito de inicializar as referências diretamente a partir do objeto original).
Para reter:
As referências em C++ pertencem à categoria de tipos compostos, pois elas são definidas em função de tipos já existentes (acrescentando-lhes o &).
Declarações múltiplas em uma mesma linha
Uma outra maneira de se declarar múltiplas referências de um mesmo tipo é fazê-lo em uma única linha de código (apesar de que eu não goste desse método, pois ele torna o código menos legível). Ao se fazer uma declaração múltipla deste tipo, é preciso tomar cuidado para não esquecer de se colocar o & antes de cada nome que se deseja declarar como uma referência.
int var = 10, outraVar = 20; // Duas variáveis int int& ref = var, &outraRef = outraVar; // Duas refs. p/ int. int ultimaVar = 30, &ultimaRef = ultimaVar; // Um int e uma ref. p/ int.
Best practices
Uma boa prática de programação é manter a consistência no uso do & para se declarar referências: ou sempre colado ao início do nome da variável, ou sempre colado ao fim do tipo da variável. Todavia, a maioria dos programadores me parece preferir colocar o & ao lado do tipo, pois assim fica claro que a referência faz parte da informação do tipo, e não parte do nome.
Como funcionam as referências em C++?
Ao criar-se uma referência, ela se “liga” ao objeto utilizado para inicializá-la e torna-se então impossível modificar a referência para ligá-la a um outro objeto. O que acontece quando se tenta “religar” uma referência a um novo objeto é apenas a modificação do valor do objeto original, como acontece no exemplo abaixo (lembre-se que a referência é apenas um nome alternativo para o objeto usado durante sua inicialização).
int mundiaisDoPalmeiras = 0; // Variável int normal /* refMundiais torna-se um outro nome para a variável * mundiaisDoPalmeiras, e não pode se ligar a outra * variável no futuro. */ int& refMundiais = mundiaisDoPalmeiras; // inicialização da referência int mundiaisFakesDoPalmeiras = 1; /* Não é possível "religar" uma referência a um novo objeto * após sua inicialização. O que acontece na linha abaixo é * a modificação do valor armazenado em mundiaisDoPalmeiras * para o valor de mundiaisFakesDoPalmeiras, que é 1. */ refMundiais = mundiaisFakesDoPalmeiras;
Assim sendo, quando tentamos modificar a referência para ligá-la à variável mundiaisFakesDoPalmeiras
, o que estamos fazendo na verdade é modificar o valor da variável mundiaisDoPalmeiras
, como se houvéssemos escrito na verdade a seguinte expressão:
// A linha a seguir é equivalente a escrever // refMundiais = mundiaisFakesDoPalmeiras; mundiaisDoPalmeiras = mundiaisFakesDoPalmeiras;
Como as referências em C++ não podem ser associadas a outros objetos após sua declaração, é obrigatório inicializar as referências declaradas no programa. Uma variável de tipo referência declarada, mas não inicializada resulta em um erro de compilação.
int& ref; // Erro – referência não inicializada
Operações sobre referências em C++
O que se pode deduzir a partir do exemplo anterior é o seguinte: qualquer operação realizada sobre uma referência é, na verdade, realizada sobre o objeto ao qual a referência está ligada. Onde quer que apareça o nome da referência no código, o comportamento do programa será de enxergar tal ocorrência como o nome da variável original. Isto acontence porque a referência não é um objeto, mas apenas um outro nome para a variável à qual ela se refere.
No exemplo a seguir, por exemplo, na linha 13 a inicialização da variável copiaVariavelOriginal
é feita utilizando o valor modificado da variável original variavelOriginal
, à qual está ligada a referência refVar
. Em seguida, modifica-se o valor da cópia da variável original, o que não altera o valor de variavelOriginal
.
Exemplo 1 – Impacto de operações sobre referências na variável original
#include <iostream> #include <string> using namespace std; int main() { string variavelOriginal= "Versão original"; string& refVar = variavelOriginal; cout << "1) Variável original: " << refVar << endl; refVar += " modificada"; cout << "2) Variável original após primeira modificação: " << variavelOriginal << endl; string copiaVariavelOriginal = variavelOriginal; cout << "3) Valor da cópia: " << copiaVariavelOriginal << endl; copiaVariavelOriginal += " de novo"; cout << "4) Valor da cópia modificada mais uma vez: " << copiaVariavelOriginal << endl; cout << "5) Valor final da variável original: " << variavelOriginal<< endl; return 0; }
Para refletir: qual seria o resultado do cout número 5) do exemplo anterior se a variável copiaVariavelOriginal fosse uma referência, e não uma cópia da variável original?
Limitações das referências em C++
Referências a referências
Algumas coisas estão fora do alcance das referências, das quais a primeira é a capacidade de se referir a uma outra referência. Como uma referência não é um objeto, é impossível criar uma referência para outra referência: tentar fazê-lo resultará em uma referência para o objeto ao qual se refere a outra referência, como no exemplo a seguir.
double temperatura = 37.7; double& refTemperatura = temperatura; double& segundaRefTemperatura = refTemperatura // Apenas cria uma nova ref para temperatura, e não uma ref. para refTemperatura.
Referências para tipos diferentes
Por via de regra, as referências em C++ devem ser do mesmo tipo dos objetos aos quais elas se referem (com exceção de tipos modificados por const e no uso com tipos definidos pelo usuário: as classes); ou seja, não podemos definir uma referência de tipo int para uma variável de tipo double.
double temperatura = 38.5; int& refTemperatura = temperatura; // Erro - impossível associar uma referência de tipo int& a um valor de tipo double
Referências para literais
As referências das quais falei até aqui, conhecidas também como lvalue references (referências a valores-L, em tradução livre), não podem ser associadas a valores literais – apenas rvalue references (referências a valores-R) podem ser ligadas a literais. Falaremos mais sobre esse assunto em um artigo posterior, mas aos curiosos que lêem em inglês, há um bom artigo no site da referência em inglês do C++.
char& caractere = 'x'; // Erro - impossível associar uma referência de tipo char& a um rvalue de tipo char.
Por que utilizar referências?
Uma das principais perguntas que as pessoas se fazem ao se depararem com as referências em C++ é “por que utilizá-las?”. Portanto, darei dois motivos simples para a utilização de referências:
- Modificar o valor de uma variável dentro de uma função;
- Evitar a criação de uma cópia da variável original sem que haja necessidade.
Modificação de uma variável dentro de uma função
Para poder alterar o valor de uma variável desde dentro de uma função, é preciso que o parâmetro dessa função seja uma referência para o tipo da variável que se deseja modificar. Caso contrário, será criada uma cópia da variável dentro da função, e qualquer alteração feita na cópia não alterará o valor da variável original.
Exemplo 2 – Modificar variável dentro de função com referências em C++
#include <iostream> using namespace std; // A função abaixo realmente modifica o valor da variável // passada como argumento. void duplicaValor(int& oValorOriginal) { oValorOriginal *= 2; } // a função a seguir modifica o valor de uma cópia local // da variável original, deixando esta última inalterada. void pensaQueDuplicaValor(int iValorOriginal) { iValorOriginal *= 2; } int main() { int salarioDevMensalBruto = 5000; pensaQueDuplicaValor(salarioDevMensalBruto); cout << "1) Valor após chamada da função sem referência: " << salarioDevMensalBruto << endl; duplicaValor(salarioDevMensalBruto); cout << "1) Valor após chamada da função com referência: " << salarioDevMensalBruto << endl; }
Evitar a criação de cópias desnecessárias
Este motivo é especialmente importante para sistemas onde os recursos de memória são limitados, ou nos quais se deseja obter a melhor performance possível do programa. Como vimos na seção de operações sobre referências, as referências em C++ não são objetos, mas sim apenas um outro nome para o objeto original. Assim sendo, essa característica das referências é muito útil para evitar a cópia de objetos muito grandes desnecessariamente.
Tomemos o exemplo a seguir de base. Nele há um tipo definido pelo usuário (uma classe) chamado TipoEspecial; digamos que esse tipo requer muita memória para cada objeto pois ele possui vários dados membros (veremos mais sobre isso nos artigos futuros dedicados às Classes), então queremos evitar a todo custo criar cópias de objetos do TipoEspecial. Logo, quando criamos nossa função manipulaSemModificarPorReferencia que recebe um objeto do tipo TipoEspecial, usamos uma refêrencia no parâmetro da função, permitindo usar os dados contidos no objeto recebido como argumento da função sem nenhum custo adicional de memória. Em contrapartida, na função manipulaSemModificarPorCopia, as mesmas operações são realizadas sobre o objeto, mas com o custo adicional de criar-lhe uma cópia local à função.
#include "TipoEspecial.hpp" void manipulaSemModificarPorReferencia(TipoEspecial& iObjetoEspecial) { // realiza operacoes sobre o objeto iObjetoEspecial diretamente, // sem criar uma cópia local. ... } void manipulaSemModificarPorCopia(TipoEspecial iObjetoEspecial) { // realiza as mesmas operacoes sobre o objeto recebido na entrada, // mas dessa vez criando uma cópia local que se chama iObjetoEspecial. ... } // a função a seguir modifica o valor de uma cópia local // da variável original, deixando-a inalterada. int main() { TipoEspecial objetoEspecial; manipulaSemModificarPorReferencia(objetoEspecial); manipulaSemModificarPorCopia(objetoEspecial); }
Créditos: figuras obtidas em Freepik e FlatIcon.
Próximos passos
Agora que sabemos o que são e como funcionam as referências em C++, é hora de passar aos temidos ponteiros!
Ah, quase esqueci: se quiserem ficar por dentro das novidades aqui do site e se não quiserem perder nenhum artigo, se inscrevam na nossa newsletter usando o campo abaixo. Vejo vocês no próximo artigo!
Sou apaixonado por tecnologia, literatura e também filosofia. O cultivo dessas paixões ao longo da minha trajetória me inspiraram a compartilhar aquilo que aprendo com os outros da maneira mais clara que eu possa. Sou formado em Engenharia Elétrica no Brasil, e também sou engenheiro formado na França. Trabalho atualmente como programador C++ em uma multinacional francesa, uma das maiores empresas de TI do mundo.
Qual a resposta do “Para refletir”?? Seria “Valor final da variável original: Versão original modificada de novo” ?
Uma observação: Os std:: deixam a leitura do código no celular meio ruim. Talvez usar um using namespace deixasse o código mais limpo.
No mais, muito boa a explicação 😀
Isso mesmo, Júlia. Boa! Ficou feliz que você tenha gostado 🙂
Muito obrigado pelo feedback. Acabei de modificar o bloco de código para remover os namespaces std::, assim como você sugeriu.