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 [email protected] Questões Publicação 1. Pesquisa sobre linguagens que suportam ponteiros.