Ponteiros, de novo?!

Um querido amigo me pede para escrever um pouco mais sobre “ponteiros” e eu penso “de novo?!”. Aparentemente esse cara (o ponteiro) é mais difícil de compreender do que mecânica quântica!! Assim, quero tentar resolver esse troço de uma vez por todas aqui… Aqui vai um resumão do que tentei explicar no capítulo 3 de meu livro (disponível gratuitamente aqui).

Antes, quero que você pense na memória de sua máquina como se fosse um gigantesco array de bytes. Uma sequência gigantesta ca bytes, cujo primeiro byte pode ser localizado no índice zero e o último, no índice mais alto possível, de acordo com a arquitetura que está usando (na arquitetura de 32 bits, será a posição 232-1, ou 4 GB menos 1 byte… Na arquitetura de 64 bits seria, em teoria, a posição 264-1 – na prática, 252-1). Essa é a lição nº 1: Memória é um array gigante de bytes!

Quando você faz uma declaração do tipo:

int x;

Você diz duas coisas ao compilador:

  1. Pede para reservar espaço para caber 4 bytes (32 bits), que é o tamanho de um ‘int’;
  2. Pede para apelidar de ‘x’ esse espaço, na memória.

Daí por diante todo o código gerado pelo compilador que faça referência à variável ‘x’ substituirá esse apelido por um endereço no array gigante que chamamos de memória… Ao fazer:

x = 1000;

O compilador fará algo assim:

mov dword ptr [0x604b8a],0x3e8

Ou seja, colocará o valor 0x000003e8 na posição 0x604b8a do array gigante que chamamos de memória! (0x3e8 = 1000, em decimal).

É claro que você, ao desenvolver em C, usa uma abstração chamada “variável”, mas o compilador a enxerga pelo que ela é: Um endereço na memória! Assim, você está usando ponteiros o tempo todo sem saber: Toda variável é, na verdade, um ponteiro e um ponteiro é um índice, um endereço, no grande array chamado memória.

Observação: Para os puristas: É claro que existem variáveis que não são alocadas na memória! Variáveis locais de funções, por exemplo, tendem a serem alocadas em registradores sempre que possível, deixemos esse ponto de lado por um momento, tá bom?

Acontece que, às vezes, você pode querer acessar uma variável ou uma estrutura de dados de forma indireta, ou seja, apenas através do endereço dessa variável ou estrutura… Para isso, a linguagem C define um “tipo” especial chamado “ponteiro”. Variáveis do tipo ponteiro são variáveis comuns que armazenam um endereço (do grande array chamado… well… você entendeu!). É isso! Acabou a explicação! Zé fini! Fui!

Declarando ponteiros:

Nah… Achou que eu ia acabar ai em cima?! Bem… Da mesma forma que você declara uma variável, especificando o tamanho (int) e o apelido (x), fazemos o mesmo com ponteiros. Ao declarar:

int *p;

Você diz ao compilador que este reserve um espaço suficiente para caber um endereço (do grande array… chega né?) em algum lugar da memória e apelide esse lugar de ‘p’. Note que o “tipo” aqui é “int *”, com o asterisco…  Endereços de memória têm sempre o mesmo tamanho (32 bits para sistemas de 32 bits, 64 bits para sistemas de 64 bits), tanto faz se temos ponteiros para “char”, “int”, “float” ou qualquer outro tipo… Então, pra que serve o tipo (int) associado à variável ‘p’, acima?

Se ‘p’ contém um endereço de memória e a finalidade de uma variável ponteiro é permitir o acesso indireto (‘p’ contém um endereço e neste endereço temos alguma coisa) então precisamos saber o tamanho do dado contido no endereço fornecido por ‘p’! Ou seja, de posse de um endereço, devo gravar ou ler quantos bytes? O ‘int’ nos diz que qualquer operação envolvendo ‘p’ será feita em 4 bytes de uma só vez (o tamanho de um ‘int’).

O operador de indireção (ou “derreferência”) *:

Uma coisa é declarar uma variável, que significa reservar espaço para ela, outra coisa diferente é realizar operações usando-a. Para tanto existem “operadores” como +, -, *, /, %, ++, — etc. O operador ‘*’ tem duplo uso: Ele pode ser usado como operador de multiplicação ou como “operador de indireção”. O segundo caso só é possível se a variável ao qual o operador se aplica seja um ponteiro.

Lembre-se que um ponteiro é uma variável que contém um endereço… No exemplo de ‘p’, acima, temos que colocar um endereço em ‘p’ e, usando o operador ‘*’, acessar o inteiro “apontado” ou “endereçado” por ele:

int x = 10;
int y;
int *p;

p = &y;      // Coloca o "endereço de" (opeador &) y na variável 'p'.
*p = x + 1;  // 'y' agora será 11.

Ahhh… sim… o operador ‘&’ tamém tem duplo sentido… Ele é a operação lógica AND ou o “endereço de”, dependendo do uso…

Repare que o ‘*’ antes de ‘p’ não é o operador de multiplicação, mas o de indireção… Aqui você está dizendo ao compilador: “Pegue o endereço contido na variável ‘p’ e acesse o inteiro contido neste endereço”. O detalhe é que VOCÊ está dando a ordem para o compilador fazer isso diretamente… O mesmo efeito pode ser obtido, no exemplo acima, com:

y = x + 1;

O compilador entenderá a referência a ‘y’, neste caso, como “gravar no endereço de memória cujo apelido é ‘y'”. Ambos os casos envolvem endereços de memória. Quando você usa ponteiros a indireção é explícita, está sob o seu controle, não do compilador…

Utilidade dos ponteiros:

Se usar uma variável comum é a mesma coisa que usar um ponteiro, mas de forma escondida, então qual é a utilidade dos ponteiros? Bem… com ponteiros você precisa, por exemplo, ficar fazendo cópias de arrays sempre que chamar funções, por exemplo… A função de exemplo abaixo exige um array de ints…

int f(int a[1000])
{
  int i, sum = 0;

  for (i = 0; i < 1000; i++)
    sum += a[i];

  return sum;
}

Sem o uso de ponteiros, teríamos que copiar 4000 bytes de um array para a pilha antes de chamar a função (1000 ‘ints’). Com ponteiros poderíamos escrever o protótipo acima dessa maneira:

int f(int *p)
{
  int i, sum = 0;

  for (i = 0; i < 1000; i++, p++)
    sum += *p;

  return sum;
}

A função agora recebe o endereço do primeiro ‘int’ de um array e pode usar o ponteiro para acessar todos os 1000 ‘int’s. Note que, no loop, ao incrementar ‘p’, estamos adicionando 4 ao valor do endereço contido no ponteiro…. Isso acontece porque ‘p’ é definida como um ponteiro para ‘ints’.

Observação: Para os puristas: Ambas as declarações ‘int f(int [])’ e ‘int f(int *)’ são a mesma coisa. As operações com arrays, em C, são apelidos para “ponteiro para a primeira posição do array”… Por isso, ao declarar o parâmetro argv, em main, podemos tanto usar ‘char *argv[]” quanto “char **argv”. Ou seja, as duas funções acima recebem apenas o ponteiro da primeira posição de um array… Não é feita cópia de 1000 itens, no primeiro caso! Se quiser um exemplo mais explícito, substitua o ‘int’ por uma estrutura qualquer, por exemplo: ‘struct S { int x[1000]; }’ e os protótipos ‘int f(struct S s)’ e ‘int f(struct S *p)’ serão muito diferentes…

Ponteiros para ponteiros… para ponteiros… etc

Se ‘int *p’ é um ponteiro para ‘ints’, o que seria ‘int **pp’? Isso é um um ponteiro para ponteiros para ints, ora bolas!

Neste caso a variável ‘pp’ contém um endereço e, neste endereço, temos outro endereço que aponta para um inteiro. Outra forma de ver: ‘pp’ pode conter o endereço de um array de endereços para arrays de ints. Eis um exemplo com a declaração ‘int main(int args, char **pp)’:

Ponteiro para pointeiros.
Ponteiro para pointeiros.

Podemos usar tantas indireçõs quanto quisermos… Se você declarar ‘int ***ppp’ terá um ponteiro (ppp) para um array de ponteiros onde, cada um apontará para um array de ponteiros em que, cada um apontará para um array de ints. Esse tipo de uso é raro, mas você pode encontrar algo assim algum dia (eu encontrei, por exemplo, na API do Ogg Vorbis).

Em resumo:

Ponteiros são endereços e endereços são índices no grande array que chamamos de memória (de novo?!)… Como eu disse lá em cima: “É isso! Acabou a explicação! Zé fini! Fui!”.

Anúncios

2 comentários sobre “Ponteiros, de novo?!

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s