Programação de Computadores – 2o Sem- 2013 – Prof. André Amarante Luiz – LAB10 Esse material foi preparado pelos professores Aníbal Tavares de Azevedo e Cassilda Maria Ribeiro para o curso de Programação de Computadores I – FEG/UNESP Pense duas vezes e faça uma vez. Provérbio Chinês. A sintaxe para se declarar uma variável do tipo ponteiro é dada por: tipo *ptr; PONTEIROS Um ponteiro nada mais é que uma variável capaz de armazenar um número hexadecimal que corresponde a um endereço de memória de outra variável. É importante lembrar que a declaração de uma variável do tipo caractere, por exemplo, implica em reservar um espaço de memória para armazenar a informação correspondente: char ch; ch |||| 100 101 102 ... 5000 5001 ... Quando um valor é atribuído à variável caractere, o que ocorre, na verdade, é que o espaço de memória, cujo endereço1 é 5000, passa a ser disponível para ocupar um caractere: char ch = ‘a’; ch ‘a’ 100 101 102 ... 5000 5001 ... 1 O endereço de memória de uma variável é um número hexadecimal como, por exemplo, 0022FF77. Mas, para manter a clareza da discussão e sem perda de generalidade serão utilizados números na base 10. onde: tipo – é o tipo da variável cujo endereço será armazenado pelo ponteiro, * indica que a variável é do tipo ponteiro e ptr – é o nome da variável. Observe que as três declarações a seguir são equivalentes: int * p, i; int* p, i; int *p, i; Porém, a mais utilizada é a última, pois indica claramente que apenas a variável p é do tipo ponteiro para variável inteira e i é variável do tipo inteiro. Como dito anteriormente, uma variável tipo ponteiro é responsável por armazenar um endereço de memória. Para se obter um endereço de memória de uma variável basta utilizar o operador &. PT1: Implemente o programa a seguir e verifique o valor de i, o endereço da variável i, o valor armazenado em p e o endereço de p. main() { int *p, i =4; p = &i; printf(“i = %d \n”,i); printf(“&i = %x \n”,&i); printf(“p = %p \n”,p); printf(“&p = %X \n”,&p); } Para se inicializar um ponteiro com valor nulo, basta fazer: int *p = NULL;. Observe que no programa anterior foi utilizada a tag %p para se imprimir o conteúdo de uma variável do tipo ponteiro e %x e %X para endereço de memória. Na realidade como a variável ponteiro armazena endereço de memória, tanto faz utilizar %p ou %x ou ainda %X. A única diferença fica por conta da formatação da impressão. Um outro operador útil é o operador dereferência *. Com ele é possível saber qual o valor contido no endereço armazenado (apontado) por um ponteiro como *p, por exemplo. PT2: Verifique os resultados do programa: main() { int i, *p, *q; i = 9; p = &i; // p aponta para i. q = p; // q também aponta para i. printf(“i = %d \n”,i); // valor de i printf(“&i = %X\n”, &i); // end de i printf(“p = %p \n”, p); // end apontado printf(“q = %p \n”, q); // end apontado // conteudo do end apontado por p. printf(“p = %d\n”, *p); } Para auxiliar na compreensão do que faz o programa anterior, sem precisar rodar o mesmo, é útil o seguinte esquema de representação de armazenamento de informações na memória: Variável Conteúdo i 9 p 5000 q 5000 1 Programação de Computadores – 2o Sem- 2013 – Prof. André Amarante Luiz – LAB10 Esse material foi preparado pelos professores Aníbal Tavares de Azevedo e Cassilda Maria Ribeiro para o curso de Programação de Computadores I – FEG/UNESP Endereço 5000 ... 7001 ... 7502 É útil, também, verificar quais valores serão obtidos em cada uma das expressões dadas a seguir para a representação de armazenamento anterior: Expressão Valor i 9 &i 5000 p 5000 *p 9 &p 7001 q 5000 *q 9 &q 7502 PONTEIROS DE PONTEIROS Uma vez que os ponteiros ocupam espaço em memória, é possível obter a sua posição através do operador endereço &. É importante lembrar que a função de um ponteiro é armazenar o endereço de uma variável de um dado tipo (int, etc). Ou seja: int x; int *ptr_x= &x; Para criar uma variável que armazene o endereço de uma variável do tipo ponteiro para inteiro (int) é necessário fazer: int x; int *ptr_x = &x; int **ptr_ptr_x = &ptr_x; A utilização de asteriscos e portanto, a criação de ponteiros para ponteiros (indireção múltipla) não tem limites. PT3: Teste o programa abaixo. #include <stdio.h> main() { int x = 5; int *p_x = NULL; // Ponteiro de x. // Ponteiro para ponteiro de x. int **p_p_x = NULL; // Carga inicial dos ponteiros p_x = &x; p_p_x = &p_x; printf(“x= %d -&x = %x \n”,x,&x); printf(“x= %d -p_x = %p \n”,*p_x,p_x); printf(“x= %d -*p_x = %d”,**p_p_x,*p_x); } PE1: A partir dos resultados do PT3 e das informações do esquema E1, indique os resultados a serem mostrados na Tabela T1. x p_x p_p_x 5 ... 1000 ... ... 1002 1000 1001 1002 1003 1004 1005 Esquema E1: Usando ponteiros. Expressão Valor x &x p_x *p_x &p_x p_p_x *p_p_x **p_p_x Tabela T1: Expressões de ponteiros. Uma observação importante é que não se deve inicializar um ponteiro com valores numéricos e sim com endereços de variáveis. Exemplo: // p contém lixo, ou seja, aponta para um // lugar qualquer. int *p; // Incorreto ! *p = 234; Portanto, sempre inicialize ponteiros com endereços. Caso não tenha um endereço inicial especificado, use NULL e sempre atribua endereços para ponteiros. Exemplo: int a; int *p = NULL; p = &x; ARITMÉTICA DE PONTEIROS Observe que uma variável do tipo ponteiro deve possuir um determinado tipo. Por exemplo: char a = ‘Z’; int n = 1234; float pi = 3.1415; char *ptr_a = &a; int *ptr_n = &n; float *ptr_pi = &pi; Ou seja, um ponteiro para um dado tipo t endereça sempre o número de bytes que esse tipo ocupa em memória, i.e., endereça sizeof(t) bytes. Os ponteiros são números que representam posições de memória e 2 Programação de Computadores – 2o Sem- 2013 – Prof. André Amarante Luiz – LAB10 Esse material foi preparado pelos professores Aníbal Tavares de Azevedo e Cassilda Maria Ribeiro para o curso de Programação de Computadores I – FEG/UNESP com eles podem ser realizadas as operações aritméticas dadas na Tabela OP1: Operação Exemplo Observações Atribuição p=&x Atribuição de p=NULL endereço. Incremento p = p + 2 Incremento de 2*sizeof(tipo) de p. Decremento p = p - 1 Decremento de 10*sizeof(tipo) de p. Apontado *p O asterisco por permite obter o valor existente na posição cujo endereço está em p. Endereço de &p O ponteiro ocupa espaço em memória e é possível saber também o seu endereço. Diferença p1– p2 Permite saber qual o número de elementos entre p1 e p2. Comparação pt1 > pt2 Verificação da ordem de dois elementos através dos endereços. OP1: Operações aritméticas com ponteiros. A precedência do operador * é maior que os operadores aritméticos e menor que os operadores de incremento. O acréscimo ou decréscimo é sempre realizado em função do tamanho de armazenamento do tipo do ponteiro. Assim, se o tipo for um inteiro e o mesmo for incrementado em uma unidade, o mesmo apontará para o próximo endereço compatível com o tipo inteiro. Alguns exemplos são: strings. No caso de vetores, seu nome corresponde ao endereço do seu primeiro elemento, isto é: v == &v[0]. Assim, existem duas formas de se colocar o ponteiro ptr apontado para o primeiro elemento de v: int *pi = 3000; // inteiro ocupa 4 bytes. char *pc = 4000; // caractere ocupa 1 byte. double *pt = 5000; // real ocupa 8 bytes. ptr = &v[0]; ptr = v; pi++; // pi apontará para o endereço 3004. pc++; // pi apontará para o endereço 4001. pf++; // pf apontará para o endereço 5008. Como os elementos de um vetor ocupam posições consecutivas de memória, então, é possível utilizar a aritmética de ponteiros para acessar os elementos do vetor. PT4: Teste o seguinte programa: #include <stdio.h> main() { int x=5, *px = &x; double y=5.0, *py = &y; printf(“%d %ld\n”,x,(long) px); printf(“%d %ld\n”,x+1,(long) (px+1)); printf(“%f %ld\n”,y,(long) py); printf(“%f %ld\n”,y+1,(long) (py+1)); } O que significa o resultado apresentado? Os conceitos de aritmética de ponteiros são particularmente úteis para acessar elementos de vetores e matrizes. PONTEIROS E VETORES Os ponteiros são normalmente utilizados no tratamento e manipulação de vetores e PT5: Teste o seguinte programa: int v[3] = {10, 20, 30}; int *ptr = NULL; ptr = v; printf(“v[0] = %d \n”, *(ptr)); printf(“v[1] = %d \n”, *(++ptr)); printf(“v[2] = %d \n”, *(++ptr)); PE2: O que ocorreria se (++ptr) fosse trocado por (ptr++)? Teste e discuta os resultados obtidos. PT6: Escreva um programa que mostre um vetor na tela pela ordem original e pela ordem contrária. main() { int s[100], i, n; // Aponta para o primeiro elemento de s. int *ptr = s; 3 Programação de Computadores – 2o Sem- 2013 – Prof. André Amarante Luiz – LAB10 Esse material foi preparado pelos professores Aníbal Tavares de Azevedo e Cassilda Maria Ribeiro para o curso de Programação de Computadores I – FEG/UNESP i = 0; printf("O tamanho do vetor: "); scanf("%d",&n); // Leitura. for (i=0; i < n; i++) { scanf("%d",ptr); ptr++; } // Impressão ao contrário. for (i=0; i < n; i++) { ptr = ptr - 1; printf("%d |",*ptr); } printf("\n"); // Impressão original. for (i=0; i < n; i++) { printf("%d |",*ptr); ptr = ptr + 1; } printf("\n");} PE3: Observe que no PT6, o incremento do primeiro laço for de impressão dos elementos foi diferente do segundo for. Teste e visualize o que ocorreria caso isto não fosse feito. Uma observação muito importante é que se v é um vetor ou um ponteiro para o primeiro elemento de um vetor, então, para obter o elemento cujo índice é i deste vetor, basta fazer: v[i] *(v+i) PE4: Crie um programa que lê e imprime os elementos de um vetor de inteiros utilizando a expressão *(v+i). Dica: Quando for usar scanf, lembre-se: &(*(v+i)) é igual à (v+i). ALOCAÇÃO DINÂMICA Para trabalhar com vetores ou matrizes é necessário saber a priori o número exato de elementos que serão utilizados. Como isso nem sempre é possível, o que ocorre, em geral, é que um vetor tem que ser declarado com um tamanho muito maior do que será efetivamente utilizado. Por exemplo: float x[8]; ou const int tamanho = 8; float x[tamanho]; A alocação dinâmica de memória, através de ponteiros, define em tempo de execução a quantidade de memória usada por um vetor: #include <stdlib.h> main() {float *x; int i, n; printf(“Tamanho do vetor: ”); scanf(“%d”,&n); x = (float *) calloc(n, sizeof(float)); A função calloc permite criar n elementos, cada um deles com o mesmo número de bytes (especificado por sizeof(float)). Todos os bytes são alocados com valor 0 e ou o endereço da área criada é retornado ou NULL para x(retorno de (float *)). Outra função de alocação dinâmica é malloc que aloca o número de bytes indicados (ou seja, malloc(n* sizeof(float))) e devolve um ponteiro para o bloco de bytes ou NULL. Todas estas funções são da biblioteca <stdlib.h>. PE5: Refazer o PE4 utilizando o código de alocação dinâmica discutida anteriormente, usando calloc e malloc. A alocação dinâmica pode ser utilizada para construir matriz: PT7: Construir um programa que aloca dinamicamente memória para uma matriz e preenche os seus elementos. float **a; int m,n, i, j; printf(“Numero de linhas e colunas: ”); scanf(“%d %d”,&m,&n); a = (float **) calloc(m, sizeof(float *)); for (i = 0; i < m; i++) a[i] = (float *) calloc(n,sizeof(float)); for (i = 0; i < m; i++) for (j = 0; j <n; j++) a[i][j] = (float) (i+j); PE6: Complete o PT7, imprindo os elementos da matriz usando *(*(a+i)+j). Verifique porque este comando funciona. A memória dinâmica alocada deve ser disponibilizada após o uso de outros programas. Para tanto, antes de terminar a função main() deve-se executar a função free() para cada estrutura dinâmica: PT8: Código que usa o comando free(). float *x; int **a; free(x); for (i = 0; i < m; i++) free(a[i]); 4 Programação de Computadores – 2o Sem- 2013 – Prof. André Amarante Luiz – LAB10 Esse material foi preparado pelos professores Aníbal Tavares de Azevedo e Cassilda Maria Ribeiro para o curso de Programação de Computadores I – FEG/UNESP free(a); PE7: Inserir código no PE6 que libera a memória dinâmica, usando o free(). 5