26 de maio de 2011

Desmistificando o uso de ponteiros

Uma das grandes vantagens de construir programas em linguagens como C e C++ é que elas são muito próximas do funcionamento de baixo nível dos computadores, o que costuma deixar a performance desses programas superior àquela conseguida com outras linguagens. Essa característica provavelmente explica a posição delas no ranking de linguagens mais usadas: respectivamente a 2a e 3a (em maio/2011).

A força das duas linguagens, no entanto, é também seu calcanhar de Aquiles. Muitos programadores temem C/C++ justamente por causa dessa proximidade com a linguagem de máquina, personificada no uso de um recurso-símbolo : os ponteiros. Alguns fatores contribuem para esse temor mas acredito que o principal é a falta de conhecimento sobre a sua real utilidade e funcionamento.

Então, pra começar, o que são exatamente os "ponteiros" e pra que servem ? Um ponteiro é uma variável cujo conteúdo é um endereço na memória do computador. Em outras palavras, é uma posição de memória que "aponta" para outra posição de memória. Com um diagrama fica mais fácil visualizar o conceito:
Ponteiro

No quadro acima, nome é uma variável do tipo "ponteiro para char". Isto é, o conteúdo dela indica um local na memória do computador que será usado para armazenar caracteres. O que está acontecendo no programa retratado é uma alocação dinâmica de memória, onde o sistema operacional faz para nós a reserva de uma posição da memória e coloca o endereço reservado na nossa variável. Podemos, então, usar esse ponteiro para acessar o conteúdo da memória reservada.

Os ponteiros, portanto, flexibilizam o desenho de uma aplicação, permitindo a ela alocar memória dinamicamente para tratar situações nas quais não sabemos de antemão com quantos elementos teremos que lidar. Isso também permite adiar a alocação até que os elementos sejam efetivamente necessários, reduzindo o consumo de memória. Ainda, no caso de classes, a solução para o tratamento de heranças é uma implementação fortemente calcada em ponteiros.

Uma outra característica dos ponteiros é que eles facilitam o fluxo das informações dentro do programa; sem eles, passar estruturas grandes como parâmetro exigiria a movimentação de grande quantidade de bytes enquanto com eles é necessário trafegar apenas os bytes que carregam o endereço de memória (a quantidade exata depende do tipo de arquitetura do sistema mas em Win32 são 4 bytes).

Isso tudo levanta, então, outras questões: quando usar exclusivamente o ponteiro no meu código, quando e como acessar o conteúdo e que sintaxe usar para diferenciar ambos os casos ?

Na figura no início do post aparece o uso mais comum dos ponteiros, que é manter referência a alguma memória alocada dinamicamente. Obviamente, armazenar o endereço alocado é imprescindível para que possamos acessar o seu conteúdo e também para devolver a memória ao sistema operacional quando não formos mais usá-la.
typedef struct {
bool Assigned;
long Id;
char Nome[51];
double Saldo;
} TWPessoa;

TWPessoa *lPessoa = new TWPessoa;
/* ... */
delete lPessoa;
lPessoa = NULL;

O valor NULL é um ponteiro especial para indicar que uma variável não está apontando para lugar algum, isto é, que a variável não está alocada.

Como regra, se o nome da variável aparece sozinho no código, sem decorações (como pontos, setas ou asteriscos) então estamos trabalhando com a variável ponteiro. Nesta situação, o que temos é o endereço de memória para onde nossa variável aponta. É o que está acontecendo no quadro anterior, onde a memória para a estrutura é alocada e o endereço que o sistema operacional reservou para ela é jogado em nossa variável.

A passagem de um ponteiro por parâmetro é outro uso corriqueiro. Formalmente, isso é conhecido como "passagem por referência", o que significa que apenas o endereço é trafegado. É por essa razão que as alterações feitas ao conteúdo de um parâmetro passado por referência são enxergadas também fora da função: ambos os lados apontam exatamente o mesmo endereço na memória.

Quando há alguma decoração anexada à variável no código, então estamos nos referindo ao conteúdo apontado por essa variável. Em C/C++, há várias sintaxes diferentes para acessar o conteúdo apontado, como mostra o trecho abaixo:
/* As 3 linhas abaixo fazem exatamente a mesma coisa :
Vai até a posição de memória "apontada" por lPessoa;
No local reservado para a variável Assigned, coloque o valor true. */

lPessoa->Assigned = true;

lPessoa[0].Assigned = true;

(*lPessoa).Assigned = true;

O C/C++ ainda tem um sintaxe para que você possa recuperar o endereço de uma variável de alocação estática, independentemente se é um tipo atômico (int, double, char, etc.) ou um tipo criado pelo usuário (struct, class). Poder fazer essa recuperação é útil, por exemplo, para evitar trafegar o dado estático. Novamente, aqui você só trafegaria os bytes relativos ao ponteiro:
TWPessoa lPessoaEstatica;
ResetaPessoa (&lPessoaEstatica);

Pelo rumo dessa discussão, percebemos que qualquer que seja a linguagem utilizada por você, com certeza ela utiliza ponteiros internamente - mesmo que de forma disfarçada - já que eles são a base para o acesso à memória do computador. Assim, conhecer e utilizar bem esse recurso é um excelente começo para construir bons programas em qualquer linguagem.

Nenhum comentário :

Postar um comentário

OBS: Os comentários enviados a este Blog são submetidos a moderação. Por isso, eles serão publicados somente após aprovação.

Observação: somente um membro deste blog pode postar um comentário.