Capítulo 8 - Apontadores


Conteúdo  Capítulo anterior  Capítulo seguinte 

Os apontadores são uma parte fundamental da linguagem C.
O C utiliza os apontadores de forma bastante intensa. Algumas razões:

O C utiliza apontadores explicitamente com certas construções:

Notas: Os apontadores são talvez a parte mais difícil de dominar na linguagem C. A implementação no C é algo diferente das outras linguagens.


Tópicos



O que são apontadores ?

Um apontador é uma variável que contém o endereço de memória de outra variável. É possível ter um apontador para qualquer tipo de variável.

O operador unário & dá o "endereço de uma variável". Por sua vez, o operador unário * dá-nos o "conteúdo de um objeto apontado por um apontador". Para definir um apontador para uma variável de um determinado tipo, pode, por exemplo, usar-se a seguinte declaração:

int *pointer;

Nota: É sempre necessário associar um tipo a um apontador. Assim, por exemplo, um apontador para um inteiro fica diferente de um apontador para um longo.

Considere-se o efeito do código seguinte:

int x = 1, y = 2;
int *ip;

ip = &x;
y = *ip;
x = (int) ip;
*ip = 3;

Vale a pena considerar o que passa ao nível da memória da máquina onde é executado. Vamos assumir que a variável x se encontra armazenada na posição de memória 100, y na posição 200 e ip na posição 1000. Note-se que um apontador é uma variável como as outras e os valores que lhe estão associados têm de ser armazenados em algum lugar na memória. É a natureza dos valores que se armazenam nos apontadores que é novo. Veja-se a figura seguinte:

As atribuições x=1 e y=2 obviamente colocam esses valores nas posições de memória das variáveis x e y. ip é declarado como sendo um apontador para um inteiro e aí é colocado o endereço da variável x (&x). Assim ip fica com o valor 100.
A seguir, a variável y fica com o valor da posição de memória cujo endereço se encontra em ip. No exemplo ip aponta para a posição de memória 100 - que é o endereço de x. Assim y fica com o valor de x, que é 1.
Já vimos que o C permite conversões de tipos. Assim é possível converter um apontador num inteiro (se tiverem o mesmo tamanho - em geral 32 bits). O valor de ip na 3ª instrução é transferido para x.
Finalmente transfere-se para o local apontado por ip o valor 3. Como ip contém o endereço de x é para aí que vai parar o valor 3.

IMPORTANTE: Quando se declara um apontador ele não aponta para lugar nenhum. Compete ao programador colocar no apontador um endereço válido, antes de o poder utilizar.

Assim ...

int *ip;

*ip = 100;

... poderá gerar um erro. (Um crash do programa!).

Um uso correto, poderia ser, por exemplo:

int *ip, x;

ip = &x;
*ip = 100;

O conteúdo da posição de memória apontada pelo apontador pode ser usado normalmente em expressões aritméticas:

float *flp, *flq;

*flp = *flq + 10;
++*flp;          /* Incremento de (*flp) */
(*flq)++;        /* Diferente de *flq++ */
flq = flp;       /* Aqui transfere-se o conteúdo de um apontador (endereço de float) para o outro */

Como já se disse um apontador é um endereço de memória, que é representado por um valor inteiro, mas um apontador NÃO é um inteiro (int). Ainda assim são possíveis certas operações aritméticas diretamente com os apontadores. Uma das razões porque é necessário indicar o tipo da variável apontada por um apontador é permitir essas operações. Por exemplo, quando se incrementa um apontador, isso, em geral, não corresponde a somar 1 ao endereço que ele contém, mas sim o número de bytes que ocupa o tipo de valor para onde aponta. O incremento corresponde assim ao endereço na memória do próximo valor do mesmo tipo.

Apenas para um apontador para char corresponde a operação ++char_ptr a incrementar 1 ao valor que está em char_ptr.

Para apontadores para int's (de 32 bits) e float's, ++ptr corresponde a somar 4 ao valor contido em ptr.

Considere-se um float (fl) e um apontador para float (flp), como se mostra na próxima figura.

Se flp apontar para fl, quando se incrementa flp (++flp) ele passa a apontar para uma posição 4 bytes mais à frente (4 bytes é o tamanho de um float). Se se adicionasse 2 (flp + 2) ao valor original de flp (&fl), ele passaria a apontar para um endereço 8 bytes mais à frente (ou seja 2 float's mais à frente).
 

Up

Apontadores e funções

Examinemos agora a relação próxima entre apontadores e outras construções do C.

Quando se passam argumentos para funções em C, estes são passados por valor (exceto os arrays, como veremos); ou seja, é criada uma cópia do argumento no stack do processador e é essa cópia que chega à função; quando a função termina essa cópia desaparece. No entanto há situações em que a passagem de argumentos por valor não é muito conveniente; por exemplo, quando queremos que modificações feitas aos argumentos no interior das funções cheguem a quem as chamou, ou quando necessitamos de passar argumentos de tamanho muito elevado (estruturas com muitos membros). Certas linguagens permitem então a passagem de parâmetros de outra forma - passagem por referência - em que a informação que chega à função é apenas o endereço do local na memória onde se encontra o valor do argumento (p. exemplo, argumentos declarados como var no Pascal). No entanto o C não permite esse mecanismo sendo necessário o uso explícito de apontadores para o implementar. (Nota: o C++ contém já esse mecanismo).

Por exemplo, se quisessemos escrever uma função para trocar o conteúdo de duas variáveis, a forma habitual não funcionaria:

void swap(int a, int b)
{
  int t;

  t = a; a = b; b = t;
}

com a chamada swap(i, j);

Para funcionar teríamos de recorrer a apontadores e ao operador & de obtenção do endereço de uma variável:

void swap(int *a, int *b)
{
  int t;

  t = *a; *a = *b; *b = t;
}

com a chamada swap(&i, &j);

É possível também retornar apontadores como resultado de uma função, usado geralmente quando se pretende retornar informação que ocupe um grande espaço, como as estruturas:

typedef struct { float x, y, z; } coord;

coord *coor_fn(void);

void main(void)
{
  coord p1;

  ...
  p1 = *coor_fn();
  ...
}

coord *coor_fn(void)
{
  coord p;

  p = ... ;
  return &p;
}

Aqui retorna-se um apontador cujo conteúdo é imediatamente de-referenciado para uma variável do tipo correspondente. Deve fazer-se esta transferência de imediato uma vez que a variável p é local à função e desaparece quando a função retorna, podendo a memória que lhe corresponde ser reutilizada.
 

Up

Apontadores e arrays

Os apontadores e os arrays estão numa relação muito próxima na linguagem C. Um array não é mais do que um bloco de memória onde são armazenados em posições sucessivas valores do mesmo tipo (ocupando por isso o mesmo número de bytes). Podemos pensar no nome do array como representando o endereço desse bloco de memória. Realmente o C considera quase equivalentes esses dois pontos de vista. Vejamos o seguinte código:

int a[10], x;
int *pa;

pa = &a[0];       /* pa fica a apontar para a[0] - endereço inicial do array a[] */

x = *pa;          /* x passa a ser igual a a[0] */

Observe-se agora a figura seguinte:

Para se acessar à posição i do array é agora possível escrever:

*(pa + i)    que é equivalente a (<->)    a[i]

Nota importante: O C não testa de maneira alguma a validade de acessos fora do bloco de memória previamente declarado ou alocado. É da inteira responsabilidade do programador assegurar que esses acessos não são feitos.

No entanto a identificação entre apontadores e arrays vai mais longe. Por exemplo, é válido escrever:

pa = a;      em vez de      pa = &a[0];

e como vimos     a[i] <-> *(pa + i) , ou seja     &(a[i]) <-> pa + i

Outra construção equivalente é   pa[i]   e   *(pa + i)

No entanto existe uma diferença entre arrays e apontadores:

A passagem de um array como argumento de uma função faz-se sempre por referência, ou seja o que realmente é passado à função é o endereço da primeira posição do array, o que é equivalente a passar um apontador para essa posição de memória.

Assim a chamada à função   int strlen(char s[]);   pode ser feita de forma equivalente das seguintes maneiras:

strlen(s)   ou   strlen(&s[0])   sendo s um array de caracteres terminado com o carácter de código 0 (string).

Da mesma forma a declaração da função strlen também poderia ser feita como:   int strlen(char *s);

Nota: strlen() é uma função da biblioteca standard do C que retorna o tamanho de uma string.

Esta função poderia ter sido escrita da forma seguinte:

int strlen(char *s)
{
  char *p = s;

  while (*p != '\0')   /* ou mais simplesmente while (*p++); */
    p++;               /* uma vez que qualquer valor != de 0 é considerado true */
  return p-s;          /* número de caracteres que cabem entre os endereços */
}                      /* representados por p e s */

Outro exemplo: uma função que copia uma string para outra;   strcpy() é também uma função da biblioteca standard do C.

void strcpy(char *s, char *t)
{
    while (*t++ = *s++);
}

É de lembrar que o último carácter de uma string em C deve ter o código 0 que faz com que o ciclo while termine.
 

Up

Arrays de apontadores

Uma vez que os apontadores são valores de um tipo definido no C, nada nos impede de declararmos e usarmos arrays de apontadores. Para certas situações eles são até muito úteis.

Imaginemos que queríamos ordenar linhas de texto (strings) que se apresentam com comprimentos muito diferentes. Uma forma bastante eficiente de fazer isso em memória poder ser a que se esquematiza a seguir:

Este esquema elimina:

 

Up

Arrays multidimensionais e apontadores

Podemos pensar num array multidimensional em C como sendo um array unidimensional cujos elementos são arrays com uma dimensão a menos. Assim um array 2D não é mais do que um array simples cujos elementos são também arrays simples. Daí a notação a[n][m] para os arrays bidimensionais. Os arrays bidimensionais são armazenados linha a linha, ou seja, é o último índice que varia mais depressa ao passarmos de um elemento para o seguinte no bloco de memória onde se encontra o array.

Considere-se um array a[5][35] que pretendemos passar como argumento a uma função. Na declaração desta devemos indicar o número de colunas, porque é necessário sempre saber quantos bytes ocupa cada elemento do array simples que tem como elementos arrays com esse número de colunas. Assim a função poderá ser declarada como:    void f(int a[][35]) { ... }   ou alternativamente como   void f(int (*a)[35]) { ... }. Precisamos dos parêntesis em (*a) porque o operador [] tem maior prioridade do que o operador *.

Assim int (*a)[35] representa um apontador para um array de 35 int's, enquanto que int *a[35] representaria um array de 35 apontadores para int.

Vejamos agora algumas diferenças (sutis) entre apontadores e arrays. Tomemos as seguintes strings como exemplo:

char *name[10];
char aname[20][10];

Legalmente é possível escrever name[3][4] e aname[3][4] num programa em C.

Contudo:

A vantagem da primeira declaração é a possibilidade de cada apontador poder apontar para arrays de tamanho diferente, como nas seguintes declarações:

char *name = {"no month", "jan", "feb", ... };
char aname[][15] = {"no month", "jan", "feb", ... };


 

Up

Inicialização de arrays de apontadores

A inicialização de arrays de apontadores constitui uma aplicação ideal para a declaração de variáveis estáticas locais, uma vez que assim só há uma alocação de memória, mantendo-se os endereços de uma chamada para outra. Exemplo:

void some_function(void)
{
  static char *months = { "jan", "feb", "mar", "apr", "may", "jun",
                          "jul", "aug", "sep", "oct", "nov", "dec" };

  ...
}
 

Up

Apontadores e estruturas

A combinação destes 2 tipos é simples e geralmente sem problemas. Considere-se o seguinte exemplo:

struct coord {float x, y, z; } pt;
struct coord *ppt;

ppt = &pt;

Em C existe o operador -> que nos permite acessar diretamente a membros de uma estrutura apontada por um apontador, como nas 2 linha seguintes:

ppt->x = 1.0;
ppt->y = ppt->z - 3.1;

Outro exemplo: listas ligadas

typedef struct {
  int value;
  element *next;
} element;

element n1, n2;

n1.next = &n2;

O código de cima liga o elemento n1 a n2 através do seu membro next, como se mostra na figura:


 

Up

Erros comuns na utilização de apontadores

Apresentam-se a seguir dois dos erros mais comuns cometidos com a utilização de apontadores.

Apontadores não devidamente inicializados

int *x;

*x = 100;

O código de cima está errado (x não inicializado - pode conter qualquer valor aleatório), no entanto não há qualquer mensagem do compilador. É necessário sempre inicializar os apontadores antes de os utilizar, como por exemplo:

int *x, y;

x = &y;
*x = 100;

De-referência ilegal

Existe uma função na biblioteca standard denominada malloc() que faz a alocação dinâmica de memória retornando uma apontador para o bloco requisitado ou o valor NULL (apontador nulo) se não existir memória suficiente - void *malloc(long size); - (void * é um apontador genérico: tem de ser transformado, através de um cast, para um apontador para um tipo concreto, antes de ser utilizado).

Considere-se o seguinte código:

char *p;

*p = (char *) malloc(100);
*p = 'y';

Existe um erro na 1ª atribuição: não se deveria ter utilizado o operador * em p. A função malloc retorna um endereço (ou seja ou apontador), logo dever-se-ia ter escrito: p = (char *) malloc(100);

No entanto, mesmo com o código retificado, pode subsistir um outro problema. Se malloc retornar o valor NULL a instrução seguinte acessa memória inválida e em geral produz um erro.

O código correto deveria então ser escrito da forma:

p = (char *) malloc(100);
if (p == NULL) {
  printf("Error: out of memory\n");
  exit(1);                            /* termina o programa com código 1 */
}
*p = 'y';
 

Up

Exercícios


Veja aqui os exercícios 



Up

[REV 04/2001]