AGRUPAMENTO DE ESCOLAS DE SANTA COMBA DÃO CURSO PROFISSIONAL DE TÉCNICO DE GESTÃO E PROGRAMAÇÃO DE SISTEMAS INFORMÁTICOS 2012-2015 PROGRAMAÇÃO E SISTEMAS DE INFORMAÇÃO MÓDULO 6 – Estruturas de Dados Dinâmicas (48 aulas/45min) Objetivos do módulo:
- Compreender o conceito de apontador; - Conhecer as regras de declaração de apontadores; - Identificar as operações para manipulação m anipulação de apontadores; - Utilizar estruturas dinâmicas lineares; - Distinguir apontador de estrutura dinâmica; - Identificar os tipos de estrutura dinâmica – Pilha e Fila de Espera; - Adquirir a noção de lista bidirecional; - Dominar as operações básicas sobre listas; - Criar programas com estruturas dinâmicas; Conteúdos do módulo:
- Conceitos de estruturas dinâmicas; - Regras de declaração de estruturas dinâmicas; - Técnicas de manipulação de informação em estruturas dinâmicas; - Noções de pilha e fila; - Operações básicas sobre listas unidirecionais e bidirecionais. Definição
Nos módulos anteriores aprendemos a utilizar grandes quantidades de memória através de vetores e matrizes bem como com a implementação de estruturas. Estas estruturas de dados, apesar de muito uteis, têm uma limitação devido à sua natureza estática, ou seja, depois de declarados os vetores não podem “crescer” para acomodar mais dados, por um lado esta realidade propicia o desperdício de espaço na memória do computador bem como estabelece limites ao volume de dados que o programa pode tratar. Para contornar estas limitações existem estruturas dinâmicas de armazenamento de dados que se caracterizam por alocar memória somente quando é necessário e sempre que é necessário, sendo mais eficientes são também mais exigentes do ponto de vista do programador pois são mais difíceis de implementar.
[email protected] – alunosnet.pt.vu
Página 1 de 10
Ponteiros
Um ponteiro (ou apontador) é uma variável que, em vez de armazenar informação, guarda uma posição de memória, ou seja um endereço. Por exemplo a seguinte linha de código define uma variável para um inteiro: int x;
Memória do computador
x=10;
Variável x
Posição 0x100
Conteúdo 10
Um ponteiro declara-se utilizando o * (asterisco) antes do nome da variável, assim: int *p; p=NULL; Neste exemplo o ponteiro p não pode guardar um int, o que está declarado é um ponteiro para um valor inteiro, por tanto o que p pode guardar é a posição de memória de um valor inteiro. Quando declaramos um ponteiro é uma boa prática atribuir o valor NULL que corresponde a indicar que este ainda não tem uma posição de memória válida, é o equivalente ao 0 das variáveis numéricas. Assim temos o seguinte esquema na memória: int *p=NULL;
Memória do computador
int x=5; p=&x;
Variável x p
Posição 0x100 0x101
Conteúdo 10 0x100
O carater &, como já sabemos, indica que p recebe o endereço de memória da variável x. Para indicar endereços de memória do computador é comum utilizar-se valores em base hexadecimal, daí a notação 0x100. Como podemos verificar a variável p, que é um ponteiro, tem o endereço de memória de x pois a instrução p=&x indica que p recebe o endereço de x. Depois de o ponteiro ter um endereço de memória válido podemos utilizar esse valor para mostrar o conteúdo dessa posição para isso utilizamos o * (asterisco), assim: int *p=NULL; int x=5; p=&x; printf(“%d \n %d\n”,x,*p);
Com a linha do printf podemos verificar que *p refere-se ao valor da posição de memória para a qual p aponta.
[email protected] – alunosnet.pt.vu
Página 2 de 10
Em resumo com o & referenciamos o endereço de memória de uma variável e com o * o conteúdo de um endereço ou ponteiro. Outro exemplo: int *p=NULL; int x; p=&x; printf (“introduza um valor:”); scanf(“%d”,p);
Neste exemplo, na linha do scanf, não é colocado o & porque p já é um endereço, p “aponta” para x devido à linha p=&x, sem esta linha p não podia ser utilizado no scanf. Ver exemplos: m6_exemplo_1.cpp m6_exemplo_1a.cpp m6_exemplo_2.cpp m6_exemplo_2a.cpp Aritmética de ponteiros
Os operadores de aritmética têm um efeito diferente do esperado quando utilizados em ponteiros. Os operadores ++ (incremento) e -- (decremento) quando utilizados em ponteiros não somam o valor 1 (um), na realidade fazem o ponteiro movimentar-se na memória para o próximo elemento do mesmo tipo, ou melhor, o ponteiro salta um valor correspondente ao espaço ocupado na memória pelo tipo de dados deste. Assim com o seguinte código o ponteiro avança 4 bytes porque um int ocupa 4 bytes na memória. int *p=NULL; int x=10; p=&x; x++; p++; Neste exemplo a linha x++ aumenta o valor de x em 1 (era 10 passa a 11) e a linha p++ faz que p avance 4 bytes (ver exemplo 3ª). Esta aritmética é muito útil quando temos de percorrer um vetor, uma vez que os elementos do vetor estão organizados num espaço contínuo de memória podemos percorrer um vetor assim: int *p=NULL; int vetor[10],indice; p=vetor; for(indice=0;índice<10;indice++){ scanf(“%d”,p);
p++; }
[email protected] – alunosnet.pt.vu
Página 3 de 10
Neste exemplo começamos por colocar o p a apontar para o primeiro elemento do vetor e depois percorremos todos os elementos até ao último com o ciclo fazendo a introdução dos dados no scanf através do p. Ver exemplos: m6_exemplo_3.cpp m6_exemplo_3a.cpp Alocação de memória
Como foi referido no início deste documento um dos problemas que os ponteiros ajudam a resolver está associado a uma melhor gestão da memória do computador, ao invés de termos de ter vetores de dimensões pré-definidas podemos alocar a memória verdadeiramente necessária para o progr ama. Para alocar memória o C/C++ tem várias funções: Função void *malloc(size_t size) void *calloc(size_t n,size_t size) void *realloc(void *ptr,size_t size)
Aloca size bytes de memória e devolve o endereço ou NULL (ver exemplo 4a) Aloca n*size bytes de memória que inicializa a 0 Altera o espaço alocado para o ponteiro ptr
Com estas funções podemos declarar um ponteiro e posteriormente alocar memória para que o ponteiro possa apontar para um espaço de memória com informação. int *valor; valor=(int *)malloc(sizeof(int)); *valor=5; printf(“%d\n”,*valor); free(valor); Como podemos ver no exemplo apresentado a função malloc devolve o endereço de memória do primeiro byte reservado, como a função é genérica (void) temos de utilizar o type cast para o tipo de dados do ponteiro, neste exemplo int. O malloc funciona independentemente do tipo de dados ao utilizarmos o type cast informamos como esse espaço de memória será tratado pelo ponteiro que recebe o endereço devolvido pelo malloc. Caso a função não consiga alocar memória devolve NULL e assim podemos informar o utilizador do erro. Para alocar memória para vários elementos, como num vetor, podemos declarar um ponteiro e depois associar um bloco de memória a esse ponteiro para vários elementos, não podemos esquecer de libertar a memória alocada dinamicamente uma vez que o C não faz isso por nós. int *vetor,indice; vetor=(int *)malloc(sizeof(int)*10); …
free(vetor); Ao alocar memória devemos, também verificar se a operação terminou com sucesso, ou seja, se existia memória livre suficiente para alocar, assim comparar o vetor com NULL. int *vetor,indice; vetor=(int *)malloc(sizeof(int)*10); if(vetor==NULL){ printf(“ERRO\n”); return; }
[email protected] – alunosnet.pt.vu
Página 4 de 10
O vetor alocado dinamicamente pode ser utilizado tal como os restantes vetores: int *vetor,indice; vetor=(int *)malloc(sizeof(int)*10); scanf(“%d”,&vetor[0]); …
for(indice=0;indice<10;indice++) scanf(“%d”,&vetor[indice]); …
free(vetor); Nunca esquecer de libertar a memória alocada! A função calloc é muito semelhante à função malloc. A principal diferença é que inicializa o espaço alocado a 0, para além de receber dois parâmetros, o primeiro para indicar o número de elementos e o segundo o tamanho de cada elemento. O seguinte exemplo cria um vetor de 10 inteiros com o calloc: int *vetor,indice; vetor=(int *)calloc(10,sizeof(int)); if(vetor==NULL) return; scanf(“%d”,&vetor[0]); …
for(indice=0;indice<10;indice++) scanf(“%d”,&vetor[indice]); …
free(vetor);
A função realloc, tal como o nome indica, altera o tamanho da memória aloca de um ponteiro, preservando os dados e se necessário movendo os dados para outra área. Se a função receber um ponteiro com NULL funciona tal como a função malloc, fazendo a alocação da memória pela primeira vez. Caso a função consiga alocar a memória corretamente o ponteiro passado é libertado, caso contrário a função devolve NULL e o ponteiro mantém o espaço que tinha reservado anteriormente. Ver exemplos: m6_exemplo_4.cpp m6_exemplo_4d.cpp
m6_exemplo_4a.cpp
m6_exemplo_4b.cpp
m6_exemplo_4c.cpp
Todas estas funções podem também ser utilizadas para alocar memória para um vetor de char (string) ou de estruturas. Ver exemplos: m6_exemplo_5.cpp m6_exemplo_6.cpp Estas funções estão definidas no header file stdlib.h.
[email protected] – alunosnet.pt.vu
Página 5 de 10
Gestão da memória de um programa
Quando um programa é executado o sistema operativo atribui-lhe um segmento de memória com a seguinte organização: Memória do computador Texto Initialized data Uninitialized data Heap
Este segmento contém o código do programa Estes segmentos contêm as variáveis globais e as constantes O heap é utilizado para alocar memória que só é libertada por ordem do programa. É nesta memória que o malloc trabalha. A stack é o espaço onde existem as variáveis locais e os parâmetros das funções, para cada função é criada uma stack frame. Esta é automaticamente libertada quando uma função termina.
Stack
Environment variables
Estruturas de dados dinâmicas
Com as funções de gestão dinâmica de memória podemos implementar várias estruturas de armazenamento de dados, diversas dos vetores. Pilha Uma pilha é uma estrutura que organiza os dados com base no princípio do último a entrar é o primeiro a sair (Last In First Out). Esta estrutura implementa duas funções: Push (empurra) – função que insere o elemento no topo da pilha; Pop (retira) – função que retira o último elemento inserido. Inicial a pilha está vazia. Pilha vazia
Pilha
Topo Depois executando um push(10) Pilha
Topo 10
[email protected] – alunosnet.pt.vu
Página 6 de 10
Depois executando um push(20) Pilha
Topo 20 10
Agora um pop() Pilha
Topo 10
Ver exemplos m6_exemplo_pilha_1.cpp m6_exemplo_pilha_2.cpp Fila Uma fila é uma estrutura que organiza os dados com base no princípio do primeiro a entrar é o primeiro a sair (First In First Out). Esta estrutura implementa duas funções: Push (empurra) – função que insere o elemento no fim da fila; Pop (retira) – função que retira o primeiro elemento inserido. Inicialmente a fila está vazia Fila vazia
Fila
Último
Primeiro
Depois de um push(10): Fila
Último 10
[email protected] – alunosnet.pt.vu
Primeiro
Página 7 de 10
Depois de mais um push(20) Fila
Último 20 10
Primeiro
20
Primeiro
Depois de um pop() Fila
Último
Depois de mais um pop() Fila
Último
Primeiro
Ver exemplos m6_exemplo_fila_1.cpp m6_exemplo_fila_1a.cpp m6_exemplo_fila_2.cpp
Lista Uma lista organiza os elementos numa estrutura em cadeia em que cada elemento aponta para o próximo (listas ligadas) ou aponta para o próximo e para o anterior (li stas duplamente ligadas). Ao contrário dos vetores e das estruturas anteriores os elementos da lista podem não estar em espaços contíguos, ou seja, podem estar espalhados pela memória do computador não sendo possível utilizar um simples índice para os referenciar como nos vetores. As listas podem implementar diferentes funções de inserção de dados bem como remoção de dados, incluindo funções de pesquisa e ordenação. Exemplo de uma lista ligada:
[email protected] – alunosnet.pt.vu
Página 8 de 10
Lista
Dados
Dados
Dados
Dados
*Se uinte
*Se uinte
*Se uinte
*Se uinte NULL
Neste exemplo podemos verificar que: 1. A lista começa por apresentar um ponteiro para o primeiro elemento; 2. Cada elemento da lista, também conhecido por nó, é constituído por duas partes: os dados e um ponteiro para o elemento seguinte; 3. O último elemento da lista aponta para NULL. Lista vazia: Lista
NULL
A lista está vazia quando aponta para NULL. Inserir elementos: Lista
Dados
Dados
*Se uinte
*Se uinte
NULL
Para inserir um elemento na lista podemos implementar diferentes funções mas os passos são mais ou menos os mesmos: a. Criar um novo nó; b. Procurar a posição onde inserir o nó na lista e ajustar os ponteiros. Exemplo de inserção no início: a. Criar o novo nó Lista
Novo
Dados
Dados
*Seguinte
*Se uinte
NULL
Dados *Seguinte
[email protected] – alunosnet.pt.vu
Página 9 de 10
b. Ponto de inserção é o início por isso o novo nó passa a ser o primeiro:
Lista 2
Novo
Dados
Dados
*Seguinte
*Se uinte
Dados
NULL
1
*Seguinte Ver exemplo m6_exemplo_listas_1.cpp Para inserir no final devemos percorrer a lista até chegarmos ao NULL: Lista
Dados
Dados
*Seguinte
*Se uinte
NULL 1
Novo
Dados
2
*Seguinte Ver exemplo m6_exemplo_listas_2.cpp m6_exemplo_listas_3.cpp Muito Importante: Nestas estruturas temos de libertar os nós individualmente!
Exemplo de lista duplamente ligada:
Lista
Dados
Dados
Dados
Dados
*Seguinte
*Seguinte
*Seguinte
*Seguinte
*Anterior
*Anterior
*Anterior
*Anterior
NULL
NULL
Para além de estas estruturas existem outras mais complexas de implementar como as árvores (Trees) ou as Hash tables. Fontes: C dynamic memory allocation - http://en.wikipedia.org/wiki/C_dynamic_memory_allocation C Programming Expert.com http://www.cprogrammingexpert.com/C/Tutorial/dynamic_memmory_allocation/calloc.aspx http://www.cplusplus.com/doc/tutorial/dynamic/
[email protected] – alunosnet.pt.vu
Página 10 de 10