Modelos de
Linguagem de
Programação I
Aula 06
Prof. Silvestri
www.eduardosilvestri.com.br
Tipos de Dados - Ponteiros
No campo da 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.
Ponteiros - 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.
Ponteiros - Arquitetura
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.
Ponteiros - Arquitetura
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.
Ponteiros - Uso
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, um 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 - Uso
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 - Uso
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ó */
};
Ponteiros - Uso
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 tercero endereço após o início do arranjo, 1 */
return 0; }
Ponteiros - Uso
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:
Ponteiros - Uso
#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 - Uso
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;
}
Ponteiros - Uso
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 */
}
Ponteiros Tipados e conversões
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.
Ponteiros Tipados e conversões
Apesar disso, poucas linguagens definem tipagem restrita de
ponteiros, pois programadores freqüentemente 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, 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.
Perigos na utilização de Ponteiros
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.
Ponteiros 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.
Ponteiros Selvagem
Algumas linguagens, como C++, suportam ponteiros inteligentes
(smart pointers), que utilizam um 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
freqüentemente, 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 valors 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 e diferente de um ponteiro nulo do tipo int.
Ponteiro Nulo
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.
Ponteiro Nulo
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
return 0;
}
Ponteiro Nulo
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
}
Ponteiro Nulo
O seguinte exemplo demonstra um ponteiro inválido por
mudança de escopo:
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
return 0;
}
Dúvidas
www.eduardosilvestri.com.br
Eduardo Silvestri
silvestri@eduardosilvestri.com.br
Questões
Publicação
1. Pesquisa sobre linguagens que suportam
ponteiros.
Download

Ponteiros - Professor Eduardo Silvestri