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.[1] 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 uso
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 é sutil e perigoso, 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 sobrescrita 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.
Exemplos
O seguinte exemplo demonstra um ponteiro selvagem:
int main(void)
{
char *p1 = (char *) malloc(sizeof(char)); // aloca memória e inicializa o ponteiro
printf("p1 aponta para: %p\n", p1); // aponta para algum lugar da memória heap
printf("Valor de *p1: %c\n", *p1); // valor (indefinido) de algum lugar na memória heap
char *p2; // ponteiro selvagem
printf("Endereco de p2: %p\n", p2); // valor indefinido, pode não ser um endereço válido
// se você for sortudo, isso irá causar uma exceção de endereçamento
printf("Valor de *p2: %c\n", *p2); // valor aleatório em endereço aleatório
free(p1); //A função free libera o espaço de memória usado pelo ponteiro
return 0;
}
O seguinte exemplo demonstra um ponteiro inválido por mudança de escopo:
#include <stdio.h>
#include <stdlib.h>
int maIdeia(int **p) // p é um ponteiro para um ponteiro de inteiro
{
int x = 1; // aloca um inteiro na pilha
**p = x; // define o valor de x para o inteiro que p aponta
*p = &x; // faz o ponteiro que p aponta apontar para x
return x; // após retornar x estará fora de escopo e indefinido
}
int main(void)
{
int y = 0;
int *p1 = &y; // ponteiro inicializado para y
int *p2 = NULL; // um bom hábito a ser utilizado
printf("Endereco dep1: %p\n", p1); // imprime o endereço de y
printf("Valor de *p1: %d\n", *p1); // imprime o valor de y
y = maIdeia(&p1); // muda y e muda p1
// p1 agora aponta para onde x estava
// O lugar onde x estada será sobreescrito,
// por exemplo, na próxima interupção, ou na
// próxima sub-rotima, como abaixo...
// algum outro código que utiliza a pilha
p2 = (int *)malloc(5*sizeof(int));
// isso não irá abortar, mas o valor impresso é imprevisível
printf("Valor de *p1: %p\n", *p1); // imprime o valor onde x estava
free(p2);
return 0;
}
Um problema muito comum é usar um ponteiro para a memória heap depois que a memória foi desalocada, como no próximo exemplo:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p1 = (int *)malloc(sizeof(int)); // inicializa o ponteiro para a memória heap
int *p2 = p1; // faz uma cópia
*p1 = 0; // inicializa o valor
printf("Endereco de p2: %p\n", p2); // aponta para a heap
printf("Valor de *p2: %d\n", *p2); // deve imprimir 0
free(p1); // desaloca a memória
.... // algum outro código, possivelmente utilizando a heap
// p2 ainda aponta para o local original da alocação, mas é impossível saber o que está lá
printf("Valor de *p2: %d\n", *p2); // uso inválido de p2
return 0;
}
Outra maneira de utilizar incorretamente ponteiros é acessar fora da estrutura de dados a qual eles apontam, como no exemplo a seguir:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int y = 5; // cria a variável
int *p1 = &y; // inicializa o ponteiro para y
printf("Endereco de p1: %p\n", p1); // endereço de y
printf("Valor de *p1: %d\n", *p1); // valor de y
p1 = p1 + y; // aritmética válida de ponteiros
printf("Valor de *p1: %p\n", *p1); // p1 não aponta mais para y
return 0;
}
Se um ponteiro é usado para escrever além do final de uma região local, a pilha de execução pode ser destruída. No caso abaixo, o problema provavelmente se manifestará quando o programa principal retornar:
#include <stdio.h>
#include <stdlib.h>
// copia a origem para o destino, sem checagem de tamanho
void strcopy(char *d, char *s)
{
while (*d++ = *s++) // copia até que '\0' é encontrado
;
}
int main(void)
{
char y[3]; // cria uma região local
char *p1 = (char *)malloc(10*sizeof(char)); // outra região local na memória heap
p1[9] = '\0'; // insere o terminador na maior região
strcopy(y, p1); // sobreescreve a região local
free(p1);
return 0; // agora coisas ruins acontecem
}
Suporte em linguagens de programação
Várias linguagens de programação suportam algum tipo de ponteiro, ainda que algumas restringem sua ação mais que outras. Se um ponteiro é significativamente abstrato de forma a não poder ser manipulado como endereço, a estrutura de dados resultante não é mais considerada um ponteiro, e sim uma referência.
Ada
Ada é uma linguagem fortemente tipada no qual todos os ponteiros são tipados e somente conversões seguras de tipo são permitidas. Todos os ponteiros são por padrão inicializados com null, e qualquer tentativa em acessar informação através de um ponteiro para null causa uma exceção. Ponteiros em Ada são chamados "tipos de acesso". Ada-83 não permitia aritmética em tipos de acesso (ainda que vários compiladores a forneciam como uma funcionalidade fora do padrão), mas Ada-95 suporta aritmética segura em tipos de acesso pelo pacote System.Storage_Elements
.
C e C++
Em C e C++, ponteiros são variáveis que armazenam endereços e podem ser nulas. Cada ponteiro possui um tipo para o qual aponta, mas conversões de tipo podem ser feitas. Um ponteiro especial, chamado ponteiro void, aponta para um objeto de tipo desconhecido e não pode ser utilizado isoladamente sem a conversão para outro tipo de dado. O endereço do ponteiro pode ser diretamente manipulado ao realizar uma conversão para o tipo inteiro.
C++ suporta completamente os ponteiros e as conversões de tipo do C. Também suporta um novo grupo de operadores para conversão de tipos que ajudam a encontrar algumas das mais perigosas conversões em tempo de compilação e execução. A biblioteca padrão do C++ ainda fornece auto ptr, uma espécie de ponteiro inteligente que pode ser utilizado em algumas situações como uma alternativa segura aos ponteiros primitivos do C. A linguagem também suporta referências, uma versão mais abstrata de ponteiros.
Aritmética de ponteiros em C e C++
Em ambas as linguagens a aritmética de ponteiros é quase irrestrita: adicionar ou subtrair de um ponteiro o movimenta tendo como unidade o tamanho do tipo de dados para o qual ele aponta. Por exemplo, somar 2 a um ponteiro de inteiros de 4 bytes irá incrementá-lo em 8. Isso faz com que o incremento de um ponteiro o posicione para o próximo elemento em um arranjo, o que geralmente é o resultado intencionado. Aritmética entre ponteiros não pode ser feita em ponteiros para void.
É possível também fazer a subtração entre dois ponteiros (o resultado será a distância entre ambos). No entanto, não é possível fazer a adição de dois ponteiros.
Aritmética de ponteiros fornece ao programador uma maneira única em lidar com diferentes tipos: adicionar ou subtrair o número de elementos ao invés de lidar com bytes. Em particular, o C define explicitamente que a sintaxe a[n]
, que representa o n-ésimo elemento de um vetor apontado por a
, é equivalente a *(a+n)
, que é o conteúdo do elemento apontado por a+n
.
Ainda que poderosa, a aritmética de ponteiros pode ser fonte de erros. Ela tende a confundir programadores iniciantes, forçando-os em diferentes contextos: um expressão pode ser de aritmética comum ou de ponteiros, e as vezes é fácil confundir uma da outra. Contra esse tipo de situação, várias linguagens de alto nível modernas (como o Java), não permitem acesso direto à memória usando endereços.
C#
Em C#, ponteiros são suportados somente com algumas condições: qualquer bloco de código que inclua ponteiros deve ser marcado com a palavra-chave unsafe
. Tais blocos geralmente requisitam permissões mais altas de segurança para poderem ser executados.
A sintaxe é a mesma do C++, e o endereço apontado pode ser tanto memória gerenciada quanto não gerenciada. Apesar disso, ponteiros para memória gerenciada devem ser declarados usando a palavra-chave fixed
, o que previne que o coletor de lixo mova o objeto apontado enquanto o ponteiro está no escopo, mantendo o endereço do ponteiro válido por toda a sua existência.
Uma exceção a isso é a utilização da estrutura IntPtr
, que é um gerenciado equivalente seguro para int*
, e não necessita de código inseguro. Este tipo é frequentemente devolvido ao usar métodos dos System.Runtime.InteropServices
, por exemplo:
// Pega 16 bytes de memória desde a memória não gerenciada do processo
IntPtr pointer = System.Runtime.InteropServices.Marshal.AllocHGlobal(16);
// Fazer algo com a memória alocada
// Livrar a memória alocada
System.Runtime.InteropServices.Marshal.FreeHGlobal(pointer);
A plataforma .NET inclui várias classes e métodos nos espaços de nome System
e System.Runtime.InteropServices
(como a classe Marshal
) que convertem tipos .NET (como por exemplo System.String
) para vários tipos não gerenciados de ponteiro (como por exemplo LPWSTR
ou ponteiro void), e vice-versa, permitindo comunicação com código não gerenciado.
D
A linguagem D é derivada do C e C++, e suporta completamente os ponteiros e as conversões de tipo de C. Apesar disso, D também oferece várias estruturas como enlace foreach
, parâmetros fora da função, tipos de referência e manipulação avançada de arranjos, o que evita utilizar ponteiros para a maioria das rotinas.
Fortran
A linguagem Fortran-90 introduziu o conceito de ponteiro fortemente tipado, que armazenam mais que somente um endereço de memória. Eles também encapsulam limites inferiores e superiores das dimensão de arranjos, além de outros meta-dados. Um operador de associação (=>
) é usado para associar um ponteiro (POINTER
) a uma variável que possuiu um atributo TARGET
. A instrução de alocação (ALLOCATE
) também pode ser usada para associar um ponteiro a um bloco de memória. Fortran-2003 também suporte ponteiros para procedimentos. Da mesma maneira, como parte da característica de interoperabilidade com o C, suporta conversão de ponteiros do C para ponteiros Fortran, e vice-versa.
Pascal
Pascal implementa ponteiros para serem úteis, limitados e relativamente seguros. Isso ajuda a encontrar erros feitos por pessoas novas em programação, como obter o valor de um ponteiro utilizando o tipo de dado errado. Entretanto, um ponteiro pode ser convertido de um tipo para outro. Aritmética de ponteiros é irrestrita: adicionar ou subtrair um ponteiro o move pela razão do número de bytes, mas usar os procedimentos padrão Inc
e Dec
o move pela razão do tamanho do tipo de dado ao qual o está declarado. Tentar obter o valor de um ponteiro nulo (nil
) ou não inicializado lança uma exceção em modo protegido. Parâmetros de funções podem ser passados utilizando ponteiros (como parâmetros VAR
).
Referências
- ↑ Luiz Lima Jr. (2007). «Ponteiros». PPGIA, PUCPR. Consultado em 14 de outubro de 2007.
A memória do computador pode ser considerada uma sequência de células de memória, cada uma de um tamanho mínimo que o computador é capaz de gerenciar (geralmente um byte). Estas células de memória de 1 byte são numeradas de forma consecutiva. Assim, cada célula pode ser facilmente localizada na memória porque tem um endereço único.
[ligação inativa]