Ponteiro

Em programação, um ponteiro ou apontador é um tipo de dado de uma linguagem de programação cujo valor se refere diretamente a um outro valor alocado em outra área da memória, através de seu endereço. Um ponteiro é uma simples implementação do tipo referência da Ciência da computação.

Arquitetura

Ponteiros são uma abstração da capacidade de endereçamento fornecidas pelas arquiteturas modernas. Em termos simples, um endereço de memória, ou índice numérico, é definido para cada unidade de memória no sistema, no qual a unidade é tipicamente um byte ou uma word, o que em termos práticos transforma toda a memória em um grande vetor. Logo, a partir de um endereço, é possível obter do sistema o valor armazenado na unidade de memória de tal endereço. O ponteiro é um tipo de dado que armazena um endereço.

Na maioria das arquiteturas, um ponteiro é grande o suficiente para indexar todas as unidades de memória presentes no sistema. Isso torna possível a um programa tentar acessar um endereço que corresponde a uma área inválida ou desautorizada da memória, o que é chamado de falha de segmentação. Por outro lado, alguns sistemas possuem mais unidades de memória que endereços. Nesse caso, é utilizado um esquema mais complexo para acessar diferentes regiões da memória, como o de segmentação ou paginação.

Para fornecer uma interface consistente, algumas arquiteturas fornecem E/S mapeada em memória, o que permite que enquanto alguns endereços são referenciados como áreas de memória, outros são referenciados como registradores de dispositivos do computador, como equipamentos periféricos.

Usos de Ponteiros

Ponteiros são diretamente suportados sem restrições em C, C++, D e Pascal, entre outras linguagens. São utilizados para construir referências, elemento fundamental da maioria das estruturas de dados, especialmente aquelas não alocadas em um bloco contínuo de memória, como listas encadeadasárvores ou grafos.

Ao lidar com arranjos, uma operação crítica é o cálculo do endereço para o elemento desejado no arranjo, o que é feito através da manipulação de ponteiros. De fato, em algumas linguagens (como C), os conceitos de “arranjo” e “ponteiro” são intercambiáveis. Em outras estruturas de dados, como listas encadeadas, ponteiros são usados como referências para intercalar cada elemento da estrutura com seus vizinhos (seja anterior ou próximo).

Ponteiros também são utilizados para simular a passagem de parâmetros por referência em linguagens que não oferecem essa construção (como o C). Isso é útil se desejamos que uma modificação em um valor feito pela função chamada seja visível pela função que a chamou, ou também para que uma função possa retornar múltiplos valores.

Linguagens como C, C++ e D permitem que ponteiros possam ser utilizados para apontar para funções, de forma que possam ser invocados como uma função qualquer. Essa abordagem é essencial para a implementação de modelos de re-chamada (callback), muito utilizados atualmente em bibliotecas de rotinas para manipulação de interfaces gráficas. Tais ponteiros devem ser tipados de acordo com o tipo de retorno da função o qual apontam. Ponteiros para função se assemelham a functores, ainda que o conceito função objeto seja mais abrangente.

Exemplos

Abaixo é mostrado o exemplo da declaração de uma lista encadeada em C, o que não seria possível sem o uso de ponteiros:

#define LISTA_VAZIA NULL /* a lista encadeada vazia é representada por NULL */
 
struct link {
    void *info; /* conteúdo do nó da lista */
    struct link *prox; /* endereço do próximo nó da lista; LISTA_VAZIA se este é o último nó */
};

Note que essa definição recursiva de ponteiros é a mesma que a definida em Haskell:

data Link a = Nil             {- lista vazia -}
            | Cons a (Link a) {- a cons nó de um valor de tipo a e outro nó -}

A definição com referências, por outro lado, possui checagem de tipo, sem confusão de símbolos. Por essa razão, estruturas de dados em C geralmente são utilizadas com o auxílio de funções de encapsulamento, que são cuidadosamente verificadas contra erros. A mesma definição de lista encadeada pode ser codificada em Fortran utilizando ponteiros, como segue:

type real_list_t
  real :: sample_data(100)
  type (real_list_t), pointer :: next => null ()
end type
 
type (real_list_t), target :: my_real_list
type (real_list_t), pointer :: real_list_temp
 
real_list_temp => my_real_list
do
  read (1,iostat=ioerr) real_list_temp%sample_data
  if (ioerr /= 0) exit
  allocate (real_list_temp%next)
  real_list_temp => real_list_temp%next
end do

Vetores em C são somente ponteiros para áreas consecutivas da memória. Logo:

#include <stdio.h>
 
int main() {
    int arranjo[5] = { 2, 4, 3, 1, 5 };

    printf("%p\n", arranjo);      /* imprime o endereço do arranjo                                                              */
    printf("%d\n", arranjo[0]);   /* imprime o primeiro elemento do arranjo, 2                                                  */
    printf("%d\n", *arranjo);     /* imprime o primeiro inteiro do endereço apontado pelo arranjo, que é o primeiro elemento, 2 */
    printf("%d\n", arranjo[3]);   /* imprime o quarto elemento do arranjo, 1                                                    */
    printf("%p\n", arranjo+3);    /* imprime o terceiro endereço após o início do arranjo                                       */
    printf("%d\n", *(arranjo+3)); /* imprime o valor no terceiro endereço após o início do arranjo, 1                            */

    return 0;
}

Tal operação é chamada aritmética de ponteiros, e é usada em índices de ponteiros. O uso dessa técnica em C e C++ é discutido posteriormente neste mesmo artigo.

Ponteiros podem ser usados para passar variáveis por referência, permitindo que seus valores modificados tenham efeito no escopo anterior do programa, como exemplificado no código C abaixo:

#include <stdio.h>
 
void alter(int *n) {
    *n = 120;
}
 
int main() {
    int x = 24;
    int *endereco= &x; /* o operador '&' (leia-se "referênca") retorna o endereço de uma variável */

    printf("%d\n", x); /* mostra x */
    printf("%p\n", endereco); /* mostra o endereço de x */
    alter(&x); /* passa o endereço de x como referência, para alteração */
    printf("%d\n", x); /* mostra o novo valor de x */
    printf("%p %p\n", endereco, &x); /* note que o endereço de x não foi alterado */

    return 0;
}

Ponteiros podem ser usados para apontar para funções, permitindo, por exemplo, a passagem de funções como parâmetro de outras funções. O código em C abaixo demonstra tal funcionalidade:

#include <stdio.h>
 
int soma = 0;            /* armazena a soma */
int produto = 1;         /* armazena o produto */
 
void fsoma(int valor)
{
    soma += valor;
}
 
void fproduto(int valor)
{
    produto *= valor;
}
 
void mapeamento_funcao_lista(lista *L, void (*funcaoptr)(int))
{
    lista_no *no;
    no = L->inicio;
    while (no != NULL)
    {
        funcaoptr(no->valor); /* invoca o ponteiro de função */
        no = no->proximo;
    }
}
 
int main()
{
    lista *L; 
    
    /* ... preenche a lista com valores ... */
 
    mapeamento_funcao_lista(L, fsoma);    /* calcula o somatório dos elementos da lista */
    mapeamento_funcao_lista(L, fproduto); /* calcula o produtório dos elementos da lista */

    printf("Somatorio: %d\nProdutorio %d\n", soma, produto); /* imprime na tela os resultados */

    return 0; /* retorno bem sucedido */
}

Note que, no exemplo acima, a rotina para a execução de código para cada elemento da lista (técnica conhecida como mapeamento) é implementada somente uma vez. O mapeamento é novamente exemplificado abaixo em uma rotina para determinar se elementos em uma lista são pares ou ímpares, codificado em ML:

fun map(F, nil) = nil
  | map(F, x::xs) = F(x)::map(F, xs);
 
map(fn x => x mod 2, [1,2,3,4,5,6,7,8,9]);

A função map() recebe como entrada um valor e uma lista. Neste caso, o valor passado é uma função (x mod 2, uma implementação utilizando cálculo lambda para retornar se dado valor é par). Tal função é mapeada recursivamente, sendo executada em cada elemento da lista.

Situações de us

Os ponteiros são necessários para a alocação dinâmica de memória, para sequências de dados alocados e para a passagem ou o retorno através referência. Neste último é importante citar a relevância no desempenho, pois é muito mais rápido alocar um ponteiro para um objeto de memória já existente do que alocar o objeto inteiro novamente. Além do mais, alocando o objeto novamente não podemos alterar o original.

Ponteiros tipados e conversão

Em várias linguagens, ponteiros possuem a restrição adicional de apontar para objetos de um tipo específico de dado. Por exemplo, um ponteiro pode ser declarado para apontar para um inteiro. A linguagem tentará prevenir o programador de apontar para objetos que não são inteiros, ou derivados de ponteiros, como números de ponto flutuante, eliminando alguns tipos básicos de erro cometidos por programadores.

Apesar disso, poucas linguagens definem tipagem restrita de ponteiros, pois programadores frequentemente se encontram em situações nas quais desejam tratar um objeto de um tipo como se tivesse outro. Nesses casos, é possível converter o tipo de um ponteiro. Algumas conversões são sempre seguras, enquanto outras são perigosas, possivelmente resultando em comportamento incorreto do sistema. Apesar de geralmente ser impossível determinar em tempo de compilação se tais conversões são seguras, algumas linguagens armazenam informações sobre tipagem em tempo de execução (como o processo RTTI do C++), que podem ser usadas para confirmar se tais conversões perigosas são válidas, em tempo de execução. Outras linguagens simplesmente aceitam uma aproximação conservadora de conversões seguras, ou apenas não aceitam conversões.

Perigos na utilização de ponteiros

Como ponteiros permitem ao programa acessar objetos que não são explicitamente declarados previamente, permitem uma variedade de erros de programação. Apesar disso, o poder fornecido por eles é tão grande que existem tarefas computacionais que são difíceis de ser implementadas sem sua utilização. Para ajudar nesse aspecto, várias linguagens criaram objetos que possuem algumas das funcionalidades úteis de ponteiros, ainda que evitando alguns tipos de erro.

Um grande problema com ponteiros é que enquanto são manipulados como números, podem apontar para endereços não utilizados, ou para dados que estão sendo usados para outros propósitos. Várias linguagens, incluindo a maioria das linguagens funcionais e linguagens recentes, como C++ e Java, trocaram ponteiros por um tipo mais ameno de referência. Tipicamente chamada de “referência”, pode ser usada somente para referenciar objetos sem ser manipulada como número, prevenindo os tipos de erros citados anteriormente. Índices de vetores são lidados como um caso especial. As primeiras versões de Fortran e Basic omitiam completamente o conceito de ponteiros.

Ponteiro selvagem

Um ponteiro selvagem (também chamado de apontador pendente) não possui endereço associado. Qualquer tentativa em usá-lo causa comportamento indefinido, ou porque seu valor não é um endereço válido ou porque sua utilização pode danificar partes diferentes do sistema.

Em sistemas com alocação explícita de memória, é possível tornar um ponteiro inválido ao desalocar a região de memória apontada por ele. Esse tipo de ponteiro é perigoso e sutil, pois um região desalocada de memória pode conter a mesma informação que possuía antes de ser desalocada, mas também pode ser realocada e sobreescrita com informação fora do escopo antigo. Linguagens com gerenciamento automático de memória previnem esse tipo de erro, eliminando a possibilidade de ponteiros inválidos e de vazamentos de memória.

Algumas linguagens, como C++, suportam ponteiros inteligentes (smart pointers), que utilizam uma forma simples de contagem de referências para ajudar no rastreamento de alocação de memória dinâmica, além de atuar como referência.

Ponteiro nulo

Um ponteiro nulo possui um valor reservado, geralmente zero, indicando que ele não se refere a um objeto. São usados frequentemente, particularmente em C e C++, para representar condições especiais como a falta de um sucessor no último elemento de uma lista ligada, mantendo uma estrutura consistente para os nós da lista. Esse uso de ponteiros nulos pode ser comparado ao uso de valores nulos em bancos de dados relacionais e aos valores Nothing e Maybe em mónadas da programação funcional. Em C, ponteiros de tipos diferentes possuem seus próprios valores nulos, isto é, um ponteiro nulo do tipo char é diferente de um ponteiro nulo do tipo int.

Como se referem ao nada, uma tentativa de utilização causa um erro em tempo de execução que geralmente aborta o programa imediatamente (no caso do C com uma falha de segmentação, já que o endereço literalmente aponta para uma região fora da área de alocação do programa). Em Java, o acesso a uma referência nula lança a exceção Java.lang.NullPointerException. Ela pode ser verificada, ainda que a prática comum é tentar se assegurar que tais exceções nunca ocorram.

Um ponteiro nulo não pode ser confundido com um ponteiro não inicializado: ele possui um valor fixo, enquanto um ponteiro não inicializado pode possuir qualquer valor. Uma comparação entre dois ponteiros nulos distintos sempre retorna verdadeiro.

Fonte: https://pt.wikipedia.org/wiki/Ponteiro_(programa%C3%A7%C3%A3o)

Deixe uma resposta