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
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
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.
Algoritmo 1. A variável apontadora a receberá o endereço da variável c.#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?) }
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.
Algoritmo 2. Uso de passagem de parâmetro por referência (ou endereço).#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 }
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.
Algoritmo 3. Exemplo da recursividade da definição de apontador.#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 }
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