Ponteiros e referências em C++ parte 2

Continuando com essa tentativa de série de posts sobre ponteiros e referências, que começou falando sobre Ponteiros, hoje abordaremos Referências.

Uma referência é um nome alternativo para um objeto, um alias. Aliás, quem diz isso não sou eu, mas o próprio Bjarne Stroustrup, lá pelo tópico 5.5 do seu livro, cujo conteúdo é tão bom que só fui reparar hoje que na capa tem uma onda quebrando em forma de C. Gostei dessa capa!

Declarando Referências

Uma referência para um determinado tipo é declarada acrescentando-se o caracter & (ê comercial ou ampersand) após o nome do tipo da referência. Note que da mesma forma que na declaração de ponteiros, estamos tratando o & como um caracter de notação e não um operador. Não há um operador de declaração de ponteiros ou referências. É apenas uma notação da linguagem. Dado um tipo T, a expressão T& significa Referência para T. Por exemplo:

// Declara uma variável inteira original.
// Também poderia ser char, float, etc.
int i;

// Declaram referências para a variável i.
// Todas as formas são equivalentes, mas preferimos a primeira.
// Note que os tipos das referências tem que ser os mesmos
// que os das variáveis referenciadas.
int& ra     = i;
int &rb     = i;
int & rc    = i;

// Diferentemente de ponteiros, a linha abaixo gera um
// erro de compilação. Você saberia dizer por quê?
int &r1, &r2;

Embora em C++ as referências lembrem um pouco os ponteiros, eles são bem diferentes. Ao contrário do que acontece com ponteiros, uma referência precisa ser inicializada durante sua declaração. Tentar declarar uma referência sem inicializá-la gera um erro em tempo de compilação, ou seja, o compilador vai cuspir na sua cara "Perdeu playboy, perdeu!". A única exceção a essa regra é para referências declaradas como extern, pois elas serão inicializadas em algum outro ponto do programa, mas isso é outro papo.

Se você tentar trapacear declarando uma referência extern sem inicializá-la o compilador não vai reclamar. Se ela nunca for utilizada, ok. Se o compilador for esperto e notar que ela não será utilizada, pode até removê-la da lista de símbolos. Mas se ela for utilizada em qualquer parte do programa, o linker vai rir da sua cara gargalhando "undefined reference to 'nome_da_variavel'". Portanto faça as coisas direitinho. Declarou uma referência? Então iniclalize-a. extern? Tem certeza que sabe o que está fazendo?

A lógica por trás disso é que uma referência foi planejada para ser um nome para alguma coisa. Se você não a inicializar, ela não será nome para nada, então não faz sentido. Um detalhe muito importante é que inicializar uma referência não é atribuir um valor a ela. Como o prório Bjarne diz, nenhum operador opera sobre referências, ou seja, não há como atribuir, somar, subtrair, etc com uma referência. Como uma referência é um apelido para um objeto, cada operador atua nese objeto, e não na referência. Uma vez inicializada, uma referência sempre referenciará o mesmo objeto.

A primeira armadilha com referências, assim como ponteiros está na questão do tamanho. Enquanto o tamanho de um ponteiro (e de outras estruturas de dados) pode ser obtido com o operador sizeof, o tamanho de uma referência não pode ser obtido por técnicas convencionais, já que ao aplicar o operador sizeof em uma referência, estamos na verdade, aplicando o operador no objeto ao qual ela se referencia, conforme vimos no parágrafo anterior.

char    c;
int     i;
double  d;

char&   rc = c;
int&    ri = i;
double& rd = d;

// O operador sizeof diz o tamanho do tipo do seu argumento.
cout << "Size of char:    " << sizeof(c) << " bytes" << endl;
cout << "Size of int:     " << sizeof(i) << " bytes" << endl;
cout << "Size of double:  " << sizeof(d) << " bytes" << endl;

cout << endl;

cout << "Size of char&:   " << sizeof(rc) << " bytes" << endl;
cout << "Size of int&:    " << sizeof(ri) << " bytes" << endl;
cout << "Size of double&: " << sizeof(rd) << " bytes" << endl;

As técnicas para obter os tamanhos das referências fogem ao escopo deste texto, mas pretendo falar sobre elas futuramente. Por hora, vou apenas dizer sem demonstrar que, quando aplicável, o tamanho de uma referência é idêntico ao tamanho de um ponteiro.

A parte boa da história é que frequentemente os tamanhos de ponteiros e referências são bem menos relevantes que os tamanhos dos objetos apontados ou referenciados. Eu particularmente, nunca precisei utilizar essa informação, mas para o desenvolvimento em arquiteturas diferentes ou para sistemas embarcados, isso já se torna mais interessante.

Utilizando Referências

O uso de referências já é bem mais simples que o de ponteiros, pois não são necessários operadores de extração de endereços ou de de-referenciação. Variáveis referências são utilizadas como variáveis comuns, lembrando que uma operação feita numa referência sempre afeta o objeto referenciado.

int x, y;
int& rx = x;
int& ry = y;

x = 13;
y = 10;

rx = 42;    // A atribuição é automaticamente aplicada em x.
ry++;       // O incremento é automaticamente aplicado em y.

cout << "x:  " << x << endl;    // Valor de x.
cout << "y:  " << y << endl;    // Valor de y.
cout << "rx: " << rx << endl;   // Valor de rx == x.
cout << "ry: " << ry << endl;   // Valor de ry == y.

Utilizar uma referência é tão transparente para o programador, que ele nem precisa saber que é uma referência. Simplesmente usa como se fosse uma variável ordinária. Com minha imaginação limitada não vejo como alguém consiga ser excomungado por utilizar referências inadequadamente. Embora sempre tenha algum espírito de porco que consiga fazer merda com referências, é muito mais fácil levar rasteira de Saci ao manusear ponteiros.

Endereços e Referências

Do ponto de vista de Murphy, as referências são feias, chatas e bobas, pois existe pouca coisa potencialmente perigosa a se fazer com elas. Nem mesmo quanto a endereçamento há muitas surpresas (será?). Considere o código abaixo:

int a = 10;
int b = 13;

int& ra = a;
int& rb = b;

ra = 42;
rb = 7;

cout << "ra  = " << ra << endl;// O valor de ra == 42.
cout << "rb  = " << rb << endl;// O valor de rb == 7.
cout << "a   = " << a << endl; // O valor de a == ra == 42.
cout << "b   = " << b << endl; // O valor de b == rb == 7.

cout << "&a  = " << &a << endl; // O endereço de a.
cout << "&b  = " << &b << endl; // O endereço de b.
cout << "&ra = " << &ra << endl;// O endereço de ra == &a. 
cout << "&rb = " << &rb << endl;// O endereço de rb == &b.

Nas linhas 17 e 18 eu declaro duas variáveis inteiras (poderia ter feito esse exemplo com uma só...). Nas linhas 20 e 21 eu declaro duas referências, referenciando as duas variáveis anteriores. A partir desse momento as variáveis ra e rb são apenas apelidos para as variáveis a e b. Como vimos anteriormente, qualquer operador aplicado às referências, na verdade atua nos objetos referenciados, portanto cada par variável-referência possui sempre o mesmo valor.

A parte interessante a se notar é que os endereços das referências são exatamente os mesmo endereços das variáveis que elas referenciam, ao contrário dos ponteiros que tinham seus próprios endereços. Por quê? Porque você caiu na Pegadinha do Malandro. Lembra daquele papo de nenhum operador ser aplicável a uma referência? Pois bem, o & utilizado aqui é o operador "endereço de", que também é aplicado diretamente nas variáveis originais e não nas referências. As mesmas técnicas ninja utilizadas para obter os tamanhos das referências são necessárias para se obter os endereços delas.

Novamente a parte divertida disso é que o mecanismo de referências foi planejado para ser o mais transparente possível para o programador. Tamanhos e endereços físicos de referências são informações irrelevantes do ponto de vista da programação "normal" em C++.

Diferenças e semelhanças entre Ponteiros e Referências

Tanto ponteiros como referências são mecanismos de indireção em C++, ou seja, são mecanismos utilizados para que a partir de um dado símbolo (variável) eu seja capaz de manipular outro objeto.

Os principais usos para ponteiros são geralmente relacionados com gerenciamento e manipulação dinâmica de memória, criação e destruição de objetos de formas e em momentos especiais, bem como "passagem de parâmetros por referência" (assim entre aspas mesmo). Já as referências são mais utilizadas em passagem por referência (por que será?) e sobrecarga de operadores.

Basicamente, tudo que podemos fazer com referências pode ser emulado com ponteiros. Já a recíprocra não é sempre verdadeira. A grande vantagem delas no entanto, é o fato de tornarem a indireção completamente transparente ao programador.

Uma alegoria interesante para ajudar a entender melhor as diferenças entre ponteiros e referências é o apelido. Por exemplo: Imagine que um pessoa, digamos, Carlos Caetano Bledorn Verri, seja a nossa variável original, o objeto. Dunga já é uma referência a Carlos Caetano Bledorn Verri, pois é um apelido para o mesmo objeto, são a mesma pessoa. Já a Mãe do Dunga (ou do Carlos Caetano Bledorn Verri, tanto faz), que é contantemente citada pela torcida, pode ser considerada um ponteiro para ele, pois dados determinados operadores de vocabulário, um elogio feito a ela, na verdade é indiretamente direcionado a ele.

Links