Introdução aos apontadores em C

Nesta seção apresentaremos o conceito de apontadores na linguagem C.

Lembrete do tratamento padrão para variáveis em praticamente todas as linguagens de programação

Usualmente, em qualquer linguagem de programação, sempre que usamos em um código uma variável, significa que deve-se pegar o valor armazenado nesta variável. Assim, um trecho com 10*b, implica em pegar o valor corrente armazenado na variável b e multiplicá-lo por 10.

Por que precisamos de apontadores

Uma boa razão é que na linguagem C as funções devolvem apenas valores simples (como inteiro ou real, mas nunca uma estrutura). Então imagina precisarmos de uma função que computa dois ou mais valores, sem apontador seria impossível devolver de alguma forma ambos os valores! Mas com apontadores podemos passar para esta função os apontadores para duas variáveis locais (i.e., seus endereços ou suas referências) e, dentro da função registrar as alterações na variável apontada.

Um exemplo onipresente de apontadores na linguagem C é a função de leitura scanf usada para providenciar leitura de dados. Isso é feito via apontadores, por exemplo, o comando para ler e guardar em uma variável inteira é: scanf("%d", &a);.
Cujo significado é: pegar bytes até encontrar um separador ou finalizador ENTER, interpretar os bytes como um número inteiro e guardá-lo no endereço indicado da variável a.
O operador & devolve o endereço de uma variável.

Ideia de apontadores

Assim a ideia básica de um apontador é que a linguagem deve dispor de um recurso de dois passos para pegar um conteúdo, uma indireção. Para ficar mais claro, comparemos com as variáveis "usuais": invocar o nome desta variável "usual" pegamos diretamente seu conteúdo, mas ao tentar pegar o conteúdo apontado por uma variável apontadora, precisamos primeiro pegar o que ele guarda (um endereço) e depois ir para este endereço e dali pegar efetivamente o conteúdo (seria o conteúdo apontado). Existe um operador especial para esta indireção, que é o operador *.
O operador * só pode ser aplicado à variáveis apontadoras (ou para declarar uma) e devolve o conteúdo guardado na variável apontada.
Da mesma forma, existe um operador "inverso" ao *, o operador & que devolve o endereço ocupado por uma variável.
O operador & indica que deve-se pegar o endereço da variável.
Logo, usando a declaração int *a, b=-2, c = 3;, o código a = &c; *a = b; indica, respectivamente, que: a receberá o endereço de c e depois, a variável apontada por a (no caso c) receberá o valor guardado em b. Ou seja, ao final b e c estarão com o mesmo valor -2. Na fig. 1 está a situação após executar a instrução a = &c; e antes de executar a instrução *a = b;.


Fig. 1: Representação da memória RAM com espaços para as variáveia a, b, c, p1 e p2.

Por exemplo, vamos imaginar outra situação que a figura 1 poderia ilustrar. Suponha que precisemos alterar os valores das variáveis inteiras b e de c dentro de uma função f (que nada devolve, logo void).
Desse modo, a declaração da função deveria ser void f (int *p1, int *p2) e sua chamada deveria ser f(&b, &c).
Lembrando: p1 e p2 são parâmetros formais, que receberão valores vindo dos parâmetros efetivos, que são &b e &c.
Isso quer dizer que p1 e p2 serão variáveis apontadoras para b e c, ou seja, deverão guardar os endereços de variáveis inteiras e não valores inteiros!
Suponha ainda que os resultados que b e c deveriam receber dentro da função f sejam, respectivamente, os valores guardados nas variáveis v1 e v2 (variáveis locais declaradas na função f).
Então a função f deveria ter como últimas duas linhas os comandos *p1 = v1; e *p2 = v2;.
Como o operador * indica pegar o valor no endereço que é apontado, então *p1 = v1; *p2 = v2; resulta em: guarde em b o valor que encontra-se em v1 e guarde em c o valor que encontra-se em v2 (pois p1 aponta para b (guarda o endereço de b) e p2 aponta para c).

Exemplo inicial de apontadores

Vamos examinar um exemplo simples: usar um apontador para alterar o valor guardado em outra variável.

#include <stdio.h>
int main (void) {
  int *a, b, c; // a deve guardar endereco de variavel inteira (nao e' o mesmo que guardar inteiro)
  b = 5; c = 11;
  printf("b=%d, c=%d\n", b, c); // deve resultar na tela: b=5, c=11
  a = &c; // a recebe endereco de c
  *a = b; // que e' apontado por a recebe valor guardado em b
  printf("b=%d, c=%d\n", b, c); // deve resultar na tela: b=5, c=5  (por que?)
  }
Algoritmo 1. A variável apontadora a receberá o endereço da variável c.

Antes de continuar a leitura, copie o código acima, cole em seu editor preferido, grave, compile e rode. Verá que o resultado é o indicado nos comentários. Por que?

Resposta: vamos examinar as linhas chaves e traduzir o que elas fazem.
1. a = &c;: nesta linha a variável apontadora a recebe o endereço da variável inteira c;
2. *a = b;: nesta linha existe a "indireção", o conteúdo guardado em b será atribuido à variável apontada por a, que é a variável c, logo c receberá o que está em b.

Portanto, poderíamos trocar os comandos 1 e 2 acima, por um equivalente mais simples: c = b;

Exemplo de apontadores como parâmetros (parâmetros por referência)

Vamos retomar a figura 1 com um exemplo que se adequa bem a ela: uma função para trocar os conteúdos de duas variáveis.

#include <stdio.h>
void troca (int *p1, int *p2) {
  int aux; // para trocar o conteudo e' obrigatorio termos mais uma variavel (senao perdemos um dos valores)
  aux = *p1; // aux recebe o conteudo da variavel apontada por p1
  *p1 = *p2; // a variavel apontada por p1 recebe o conteudo da variavel apontada por p2
  *p2 = aux; // a variavel apontada por p2 recebe o conteudo em aux
  }
int main (void) {
  int a = 5, b = 11;
  printf("a=%d, b=%d\n", a, b); // deve resultar na tela: a=5, b=11
  troca(&a, &b); // passa os parametros por referencia (ou endereco)
  printf("a=%d, b=%d\n", a, b); // deve resultar na tela: a=11, b=5
  }
Algoritmo 2. Uso de passagem de parâmetro por referência (ou endereço).

Exemplo da recursividade do conceito e implementação em C de apontadores

Da mesma forma que pode-se declarar vetor de vetor de vetor... (int vet[D0][D1][D2]...), pode-se fazer a mesma coisa com apontador de apontador de apontador.... Claro que isso só é recomendável em situações muito especiais. Só implemente apontadores níveis mais altos se estiver muito seguro de que isso é necessário.
O código no algoritmo 3 apresenta a possibilidade de um número arbitrário de indireções com 3 níveis de indireção e a figura 2 ilusta esse algoritmo.


Fig. 2: Representação da memória RAM com variáveis apontadoras de até 3 níveis.

#include <stdio.h>
int main (void) {
  int ***a, **b, *c, d = 11, e = 3; // declara apontadores de varios niveis - tem que ser compativeis!
  c = &d; // c aponta para d
  b = &c; // b aponta para c = b aponta para quem aponta para c
  a = &b; // a aponta para b = b aponta para quem aponta para quem aponta para c
  printf("***a=%d, **b=%d, *c=%d, d=%d, e=%d\n", ***a, **b, *c, d, e);
  // printf acima deve resultar em: ***a=11, **b=11, *c=11, d=11, e=3
  ***a = e; // apontado por apontado por apontado por a recebe e => d recebe valor em e
  printf("***a=%d, **b=%d, *c=%d, d=%d, e=%d\n", ***a, **b, *c, d, e);
  // printf acima deve resultar em: ***a=3, **b=3, *c=3, d=3, e=3
  }
Algoritmo 3. Exemplo da recursividade da definição de apontador.

Novamente, experimente o código do algoritmo 2 até estar seguro de compreendê-lo.

Leônidas de Oliveira Brandão
http://line.ime.usp.br

Alterações:
2020/10/16: explicações adicionais e novo exemplo de apontador em diversos níveis
2020/08/20: acerto no formato
2020/05/05: primeira versão do texto