Your SlideShare is downloading. ×

Apostila de Alocação Dinâmica em C

4,998

Published on

Esta apostila de alocação dinâmica em C foi desenvolvida para dar uma noção inicial das …

Esta apostila de alocação dinâmica em C foi desenvolvida para dar uma noção inicial das
funções de alocação dinâmica na Linguagem de Programação C. Portanto, inicialmente, é
necessário um estudo sobre ponteiros, pois através do conhecimento destes, fica mais
fácil trabalhar com Estruturas de Dados, as quais na maioria dos casos faz uso de
alocação dinâmica.

Published in: Technology
0 Comments
1 Like
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total Views
4,998
On Slideshare
0
From Embeds
0
Number of Embeds
0
Actions
Shares
0
Downloads
191
Comments
0
Likes
1
Embeds 0
No embeds

Report content
Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
No notes for slide

Transcript

  • 1. UNIVERSIDADE FEDERAL DO RIO GRANDE CENTRO DE CIÊNCIAS COMPUTACIONAIS INTRODUÇÃO A CIÊNCIA DA COMPUTAÇÃO Apostila de Alocação Dinâmica na Linguagem de Programação C André Mendes da Rosa Mestrando em Engenharia de Computação Diana AdamattiProfessora Doutora do Centro de Ciências Computacionais Rio Grande, 01 de fevereiro de 2013.
  • 2. IntroduçãoEsta apostila de alocação dinâmica em C foi desenvolvida para dar uma noção inicial dasfunções de alocação dinâmica na Linguagem de Programação C. Portanto, inicialmente, énecessário um estudo sobre ponteiros, pois através do conhecimento destes, fica maisfácil trabalhar com Estruturas de Dados, as quais na maioria dos casos faz uso dealocação dinâmica.A apostila não pretende findar o assunto, é apenas um incentivo para os alunos seprepararem para disciplinas de computação mais avançadas, sendo necessário que osexercícios da apostilas sejam feitos e analisados, seus valores sejam trocados,implementações diferentes sejam feitas, pois isso vai facilitar um maior aprendizado econsolidação do conhecimento em alocação dinâmica.1. PonteirosA linguagem C é altamente dependente dos ponteiros, fazendo com que se torneimprescindível ter um bom domínio de ponteiros para se tornar o bom programador nalinguagem C.Um ponteiro é uma variável que contém um endereço de memória, sendo que esteendereço pode ser a localização de uma ou mais variáveis, ou seja, um ponteiro é umavariável que guarda (aponta) o endereço de outra variável ou variáveis.Abaixo na Figura 1 está a representação das informações de uma variável comum e umavariável ponteiro na memória RAM (Random Access Memory – Memória de AcessoRandômico). Figura 1. Uma variável comum e uma variável ponteiro.Tal como variáveis, os ponteiros também possuem um tipo, sendo este tipo relativo ao tipoda variável que o ponteiro aponta. tipo_ponteiro nome_ponteiro;Um ponteiro que aponta para uma variável do tipo X, deve ser declarada da seguinteforma na linguagem C: • int ponteiro_para_inteiro; /* ponteiro para inteiro */ • float ponteiro_para_float; /* ponteiro para ponto flutuante */ • char ponteiro_para_caractere; /* ponteiro para caractere */ • double ponteiro_para_double; /* ponteiro para ponto flutuante de precisão dupla */
  • 3. Existem dois operadores unários que são utilizados com os ponteiros, o operador * queretorna o valor da variável para a qual o ponteiro aponta e, o operador & que retorna oendereço de memória da variável apontada pelo ponteiro.Exemplo: Para entender o uso dos operadores unários * e &.Saída do Exemplo:Como pode-se verificar na saída do programa acima, os endereços de memória foramimpressos em hexadecimal, os endereços dos ponteiros diferem das variáveis, pois avariável está localizada em um local da memória e o ponteiro em outro local, ou seja,ponteiros tem endereço e guardam endereço e variáveis tem endereço e guardam umvalor. O operador & seguido do ponteiro imprime o endereço da variável apontada e o *imprime o valor da variável apontada.Como ponteiros contém endereços de memória, as vezes até para um programadorexperiente é um pouco difícil entender as construções de ponteiros e, para isso, um boaforma de comunicar informação e entender ponteiros é através de diagramas, como omostrado na Figura 2.
  • 4. Figura 2. Uma ilustração de algumas operações com ponteiros.Para entender a Figura 2 acima, assume-se as declarações: int a, *p1, *p2, *p3;No passo 1, o ponteiro p1 recebe o endereço de a e passa a apontar para essa variável,no passo 2, o ponteiro p2 recebe o endereço apontado por p1 e, também, passa aapontar para a variável a, já no passo 3, o endereço apontado por p2 recebe o valor 100,fazendo com que a variável inteira a tenha o valor 100. No passo 4, o ponteiro p3 recebeo valor NULL (representado por uma linha terminada com uma barra dupla), pois como alinguagem C não faz a verificação de ponteiros, o chamados ponteiros pendentes, ouseja, que não apontam para nada e podem danificar muitas coisas, devido a poderemapontar para endereços inválidos.A linguagem C utiliza ponteiros de três modos diferentes: • A linguagem C usa ponteiros para criar estruturas dinâmicas de dados, que são estruturas de dados criadas a partir de blocos de memória localizados na pilha durante o tempo de execução. • A linguagem C usa ponteiros para manipular parâmetros de variáveis passados para as funções. • Os ponteiros em C oferecem um modo alternativo para acessar informações armazenadas em matrizes. As técnicas de ponteiros são especialmente valiosas quando se trabalha com strings de caracteres. Há uma estreita relação entre matrizes e ponteiros em linguagem C.2. Agregados e Aritmética de PonteirosUm dos mais comuns usos de ponteiros em C é referenciando dados agregados. Dadosagregados são dados compostos de múltiplos elementos agrupados porque possuemalgum tipo de relação. C suporta dois tipos de dados agregados: Structures (estruturas) earrays (vetores e matrizes).
  • 5. 2.1 Structuras (Structs)Structuras são sequencias de elementos diferentes (heterogêneos) agrupados, de modoque possam ser tratadas em conjunto como um único tipo de dados coerente. Ponteirospara struturas são uma importante parte da construção de estruturas de dados. Enquantoas estruturas nos permitem agrupar dados em conjuntos convenientes, ponteiros deixamligar estes conjuntos um ao outro na memória e, pela ligação de estruturas juntas, pode-se organizá-las de modo significativo para ajudar a resolver problemas reais.Como um exemplo, considere o encadeamento de um número de elementos juntos namemória para formar uma lista ligada. Para fazer isto, precisasse usar uma estruturacomo a struct lista no código abaixo: typedef struct lista { void *dado; struct lista *proximo; } lista;A estrutura lista é usada para cada elemento que fará parte da lista para ligar umasequencia de elementos da lista juntos, é definido proximo para cada elemento da listapara apontar para apontar para o elemento que vem a seguir. O proximo do últimoelemento da lista é definido como NULO para marcar o fim da lista.O membro dado de cada elemento da lista aponta para o dado que o elemento contém.Tendo uma lista contendo elementos ligados deste modo, pode-se atravessar a listaseguindo o ponteiro proximo após o outro.Estruturas não podem conter instâncias delas mesmas, mas podem conter ponteiros parainstâncias delas mesmas.2.2 Arrays (vetores e matrizes)São sequencias de elementos do mesmo tipo (homogêneos) arranjadosconsecutivamente na memória. Arrays estão intimamente relacionados com ponteiros. Defato, quando um identificador de array ocorre em uma expressão. C converte o arraytransparentemente em um não modificável ponteiro que aponta para o primeiro elementodo array.Exemplos equivalentes:1) Referente a array int f() { int a[10], *p; p = a; p[0] = 5; return 0; }
  • 6. 2) Referente a ponteiro int f() { int a[10], *p; p = a; *p = 5; return 0; }Como pode ser observado, no exemplo 1, o primeiro elemento do array a recebe o valor5, pois p[0] aponta para o primeiro endereço de a. Já no exemplo 2, como o ponteirosempre aponta para o primeiro elemento do array, o comando *p = 5, guardará o valor 5na primeira posição do array a.Para um melhor entendimento da relação existente entre arrays e ponteiros em C, é sóobservar que para acessar o i-ésimo elemento de um array, usa-se a expressão: a[ i ]E, a razão para está expressão acessar o i-ésimo elemento de a é que C trata a nestaexpressão do mesmo modo como um ponteiro que aponta para o primeiro elemento de a.Esta expressão como um todo, é equivalente a: *(a + i)A qual é avaliada usando as regras da aritmética de ponteiros. Ou seja, quando adiciona-se um inteiro i a um ponteiro, o resultado é o endereço mais i vezes o número de bytes dotipo de dado que o ponteiro faz referência.Exemplo: Se um array ou ponteiro contém o endereço 0x10000000, no qual umasequencia de cinco (5) inteiros de quatro bytes (4 bytes) é armazenada, a[3] irá acessar ointeiro no endereço 0x1000000c. Este endereço é obtido pela soma de 3X4 = 12 10(Decimal) = c16 (Hexadecimal) ao endereço 0x10000000, como na figura abaixo:Quando um array ou ponteiro faz referência a vinte (20) caracteres (string – cadeia decaracteres), a[3] acessa o caractere no endereço 0x10000003, pois um caractere sóocupa um byte (1 byte), então 3X1 = 310 (Decimal) = 316 (Hexadecimal) somado aoendereço 0x10000000, tal como na figura abaixo:
  • 7. A conversão para um array multidimensional (matriz) em C para um ponteiro é análoga aconversão de um array unidimensional e, para isso, uma linha da matriz é armazenadainteira em uma linha na memória depois é armazenada a linha seguinte e, assim,sucessivamente.Em uma matriz, o índice mais a direita varia mais rapidamente, tal como na figura abaixo: Figura 3. Representação conceitual de uma matriz 3x2 e representação da mesma na memória.Onde para acessar o elemento da linha i e da coluna j, usa-se a expressão: a[i][j]A qual tem seu equivalente em ponteiros: *(*(a + i) + j)3. Ponteiros como Parâmetros para FunçõesPonteiros são uma parte essencial da chamada funções em C. Mais importante ainda,porque os ponteiros são usados para dar suporte ao tipo de passagem de parâmetros porreferência. Em uma passagem de parâmetros chamada por referência, quando umafunção muda os parâmetros passados para ela, esta mudança persiste após a funçãoretornar. Ao contrário da passagem de parâmetros em uma chamada por valor, na qual asmudanças nos parâmetros persistem somente dentro da função.3.1 Passagem de Parâmetros por Chamada por ReferênciaFormalmente, C apenas suporta passagem de parâmetros por valor, mas pode-se simulara chamada por referência passando ponteiros como parâmetros para funções. Usando
  • 8. esta aproximação, uma função obtém uma cópia privada de um ponteiro para cadaparâmetro do ambiente chamador.Exemplo: para entender como isto funciona, primeiro considere a função troca_errada nafigura baixo, que ilustra uma implementação incorreta de troca de dois inteiros usandopassagem de parâmetros pela chamada por valor.Saída doprograma que usa a função troca_errada:Como pode ser observado na saída do programa, não houve a troca dos valores, poiscomo a passagem de parâmetros foi por valor, a troca de valores ocorreu apenas dentroda função troca_errada e não foi retornada para o programa chamador, o qual continuoucom os mesmos valores iniciais, ou seja, a função troca_errada recebe os valores davariáveis a e b (argumentos) e não o endereço das mesmas, pois a função troca_erradanão está usando ponteiros como parâmetros. Então, os valores foram trocados apenasnas variáveis locais x e y da função troca_errada e, quando a função troca_erradaretorna, os valores das variáveis a e b são os mesmos que estavam, inicialmente, emsuas posições de memória.
  • 9. Refazendo o exemplo acima com chamada de parâmetros por referência usandoponteiros, tem-se a resposta esperada, o programa e sua saída estão nas figuras abaixo:Saída do porgrama com a função troca, agora usada corretamente com ponteiros.Como pode ser visto na saída do programa acima, a função troca recebeu comoargumentos o endereço de duas variáveis (&a e &b) e, utilizando a variável temporáriatmp fez a troca dos valores, através de uma chamada a função troca com passagem deparâmetros por referência, ou seja, as variáveis não são diretamente passadas comoargumentos para a função troca (isso geraria um erro em tempo de compilação), mas sãopassados os endereços dessas variáveis, os quais são armazenados nos respectivosponteiros, os quais trocam os valores das posições de memória das variáveis a e b.Outro uso de ponteiros é quando passamos arrays (vetores e matrizes) para funções. Ctrata todos nomes de arrays transparentemente como ponteiros não modificáveis. Porexemplo, se passarmos um array de objetos do tipo T para uma função, será equivalente
  • 10. a passar um ponteiro para o objeto T.Exemplo: O programa seguinte tem uma matriz 2x2 e usa a função funcao_array_soma2para somar 2 a cada valor da matriz e, após imprime os novos valores na tela.A saída do exemplo acima fica da forma abaixo:Como pode ser observado, C não exige que seja passado o número de linhas para umafunção quando ela recebe uma matriz de duas dimensões, a mesma coisa acontece paravetores (arrays unidimensionais), ou seja, a função que recebe um vetor como argumento,não precisa de sua dimensão, como mostrado abaixo:
  • 11. A saída do exemplo acima é a seguinte:Como pode ser observado, C não verifica os limites do vetor e isso fica a cargo doprogramador!Agora, veja como fica o exemplo acima da soma dos elementos do vetor, sendo queagora é usado ponteiros.A saída do exemplo com ponteiros é a seguinte:A figura a seguir dá uma explicação ilustrativa de como um array multidimensional (matriz)é alocado na memória em C. O array tem 3 (três) linhas e 2 (duas) colunas. Figura 4. Representação conceitual de uma matriz 3x2 e representação da mesma na memória.Como pode ser observado, a matriz 3x2 é armazenada na memória linha após linha,
  • 12. pode-se verificar isso, através dos índices que apontam cada posição na memória.4. Alocação de Espaço de ArmazenamentoQuando um ponteiro é declarado em C, uma certa quantidade de memória é reservadapara ele e, geralmente ocupam uma palavra (word) de máquina, mas seu tamanho podevariar. Portanto, para casos de portabilidade, nunca devemos assumir que um ponteirotem um tamanho específico, pois frequentemente variam de tamanho devido as definiçõesdo compilador e especificadores de tipo permitidos por certas implementações de C.É importante salientar que quando um ponteiro é declarado, somente espaço (memória)para ele é alocado, mas nenhum espaço é alocado para os dados que o ponteiro aponta.Há dois modos de alocar espaço para os dados: pela declaração de uma variável paraisto ou pela alocação dinâmica de espaço em tempo de execução (usando as funçõescalloc, malloc e realloc).Quando declaramos uma variável, seu tipo diz ao compilador quanto de armazenamentoreservar para ela conforme o programa executa.Armazenamento (espaço de memória) para as variáveis é alocado dinâmicamente.Essas vantagens de C, permitem que o programador trabalhe diretamente com ogerenciamento de memória. Dependendo do projeto, o espaço necessário paraarmazenar uma quantidade de dados pode ser variável. Para isso, o C fornece algumasfunções para alocar/desalocar memória dinamicamente. Muitos programadores ignoram ofato de que o gerenciamento de memória deve ser feito com muito cuidado. E se otamanho da memória for muito limitado, esta pode se esgotar em algum ponto durante aexecução do programa.A alocação dinâmica permite ao programador alocar memória para variáveis quando oprograma está sendo executado. Assim, pode-se definir, por exemplo, um vetor ou umamatriz cujo tamanho descobre-se em tempo de execução.O padrão C ANSI define apenas 4 (quatro) funções para o sistema de alocação dinâmica,disponíveis na biblioteca stdlib.h.4.1 Alocação Dinâmica com mallocA função malloc serve para alocar memória e tem o seguinte protótipo: void *malloc (unsigned int num);A função toma o número de bytes que queremos alocar num, aloca na memória e retornaum ponteiro void * para o primeiro byte alocado. O ponteiro void * pode ser atribuído aqualquer tipo de ponteiro. Se não houver memória suficiente para alocar a memória
  • 13. requisitada a função malloc() retorna um ponteiro nulo. Ou seja, Aloca n bytesconsecutivos na memória e retorna o endereço da região alocada. Se não for possívelalocar, retorna NULL. Como a função malloc retorna um ponteiro void (sem tipo) énecessário tipar o ponteiro alocado dinamicamente usando um cast.Exemplo: Faça um programa que utilize a função malloc(), sendo que deve ser passadoum valor inteiro para que o programa saiba quanto de espaço deve ser alocadodinâmicamente e, após em cada posição do vetor coloque o valor do índice multiplicadopor ele mesmo e apresente na tela.Saída do Exemplo:No exemplo acima, é alocada memória suficiente para se armazenar a números inteiros.O operador sizeof() retorna o número de bytes de um inteiro. Ele é util para se saber otamanho de tipos. O ponteiro void* que malloc() retorna é convertido para um int* pelocast (muda uma variável de um tipo para outro) e é atribuído a p. A declaração seguintetesta se a operação foi bem sucedida. Se não tiver sido, p terá um valor nulo, o que farácom que !p retorne verdadeiro. Se a operação tiver sido bem sucedida, pode-se usar ovetor de inteiros alocados normalmente, por exemplo, indexando-o de p[0] a p[(a-1)].4.2 Alocação Dinâmica com callocA função calloc() também serve para alocar memória, mas possui um protótipo um pouco
  • 14. diferente: void *calloc (unsigned int num, unsigned int size);A função aloca uma quantidade de memória igual a num * size, isto é, aloca memóriasuficiente para um vetor de num objetos de tamanho size. Retorna um ponteiro void * parao primeiro byte alocado. O ponteiro void * pode ser atribuído a qualquer tipo de ponteiro.Se não houver memória suficiente para alocar a memória requisitada, a função calloc()retorna um ponteiro nulo. Em relação a malloc, calloc tem uma diferença (além do fato deter protótipo diferente): calloc inicializa o espaço alocado com 0. Ou seja, Aloca nelementos de t bytes na memória, inicializa todos os bits da região com zero e retorna oendereço da região alocada. Se não for possível alocar, retorna NULL.Exemplo: Refaça o exemplo acima usando calloc().A saída do exemplo acima é:No exemplo acima, é alocada memória suficiente para se colocar a números inteiros. Ooperador sizeof() retorna o número de bytes de um inteiro. Ele é util para se saber otamanho de tipos. O ponteiro void * que calloc() retorna é convertido para um int * pelocast e é atribuído a p. A declaração seguinte testa se a operação foi bem sucedida. Senão tiver sido, p terá um valor nulo, o que fará com que !p retorne verdadeiro. Se aoperação tiver sido bem sucedida, podemos usar o vetor de inteiros alocadosnormalmente, por exemplo, indexando-o de p[0] a p[(a-1)].
  • 15. 4.3 Alocação Dinâmica com reallocA função realloc() serve para realocar memória e tem o seguinte protótipo:void *realloc (void *ptr, unsigned int num);A função modifica o tamanho da memória previamente alocada apontada por *ptr paraaquele especificado por num. O valor de num pode ser maior ou menor que o original. Umponteiro para o bloco é devolvido porque realloc() pode precisar mover o bloco paraaumentar seu tamanho. Se isso ocorrer, o conteúdo do bloco antigo é copiado no novobloco, e nenhuma informação é perdida. Se ptr for nulo, aloca num bytes e devolve umponteiro; se num é zero, a memória apontada por ptr é liberada. Se não houver memóriasuficiente para a alocação, um ponteiro nulo é devolvido e o bloco original é deixadoinalterado. Ou seja, Altera o tamanho do bloco apontado para n bytes e retorna oendereço da região alocada. Se não for possível alocar, retorna NULL e o bloco não éalterado.Exemplo: Faça um programa que aloque 4 (quatro) números inteiros e imprima-os na telae depois realoque 16 números inteiros e utilize a seguinte equação para calcular o valordas posições a x i x ( i – 6).A saída do exemplo acima é:
  • 16. Como pode ser observado, primeiramente, foi usada a função malloc para alocar um vetorde inteiros de tamanho e, após imprimir esse vetor, houve uma realocação dinâmica domesmo vetor, mas agora para um espaço para 16 inteiros, o qual também é mostrado natela.4.3 FreeQuando alocamos memória dinamicamente é necessário que nós a liberemos quando elanão for mais necessária. Para isto existe a função free() cujo protótipo é: void free (void *p);Basta então passar para free() o ponteiro que aponta para o início da memória alocada.Mas você pode se perguntar: como é que o programa vai saber quantos bytes devem serliberados? Ele sabe pois quando você alocou a memória, ele guardou o número de bytesalocados numa tabela de alocação interna. Ou seja, recebe um ponteiro para uma regiãoalocada e desaloca.Exemplo: supondo-se que você queira alocar certa quantidade de memória durante aexecução de seu aplicativo. Você pode chamar a função malloc a qualquer momento e elasolicitará um bloco de memória da pilha. O sistema operacional reservará um bloco dememória para seu programa e você poderá usá-lo da maneira que quiser. Quando vocêtermina de usar o bloco, você o retorna ao sistema operacional para reciclagem,invocando a função free. Depois, os outros aplicativos podem reservá-lo para seu própriouso. O código a seguir demonstra o uso mais simples possível da pilha:
  • 17. Saída do exemplo:A primeira linha neste programa invoca a função malloc. Esta função faz trêscoisas: 1. Primeiro, a instrução malloc analisa a quantidade de memória disponível na pilha e pergunta: “Há memória suficiente disponível para alocar um bloco de memória do tamanho solicitado?". A quantidade de memória necessária para o bloco é conhecida a partir do parâmetro passado em malloc - neste caso, sizeof(int) é 4 bytes. Se não houver memória suficiente disponível, a função malloc retorna o endereço zero para indicar o erro (outro nome para zero é NULL e você verá que ele é usado por todo o código C). Caso contrário, a função malloc prossegue; 2. Se houver memória disponível na pilha, o sistema "aloca" ou "reserva" um bloco da pilha do tamanho especificado. O sistema reserva o bloco de memória de forma que ele não seja usado acidentalmente por mais de uma instrução malloc; 3. O sistema então coloca na variável do ponteiro (neste caso, p) o endereço do bloco reservado. A própria variável do ponteiro contém um endereço. O bloco alocado é capaz de manter um valor do tipo especificado e o ponteiro aponta para ele.O seguinte diagrama mostra o estado de memória depois de executar malloc: Figura 5. O bloco à direita é o bloco de memória malloc alocada.O programa então verifica o ponteiro para garantir que a solicitação de alocação ocorreucom a linha if (p == 0) (que também poderia ter sido escrita como if (p == NULL) oumesmo if (!p). Em caso de falha da alocação (se p for zero) o programa encerra. Em casode êxito na alocação, o programa inicializa o bloco com o valor 5, imprime o valor eexecuta a função free para retornar a memória à pilha antes do programa encerrar. Duas
  • 18. dúvidas comuns: • É mesmo importante verificar se o ponteiro é zero após cada alocação? Sim. Como a pilha varia de tamanho constantemente, dependendo dos programas em execução e quantidade de memória que alocaram, etc., não há garantias de que uma invocação de malloc será bem sucedida. Você deve verificar o ponteiro depois de qualquer chamada a malloc para conferir se o ponteiro é válido. • O que acontece se eu esquecer de apagar um bloco de memória antes que o programa encerre? Quando um programa encerra, o sistema operacional "faz a limpeza" depois do encerramento, liberando o espaço de código executável, pilha, espaço de memória global e qualquer alocação de pilha para reciclagem. Assim, não há conseqüências de longo prazo em deixar as alocações pendentes no término do programa. Todavia, é considerada uma forma inadequada e os "vazamentos de memória" durante a execução de um programa são prejudiciais.Exemplo: Faça um programa que aloque em tempo de execução um vetor de tamanhodigitado pelo usuário e guarde elementos do tipo inteiro, também digitados pelo usuário.Depois, apresente o vetor na tela.
  • 19. A saída do exemplo:Exemplo: refazendo o exemplo anterior usando calloc.
  • 20. A saída do exemplo:Exemplo: Aloque a quantidade de memória suficiente para armazenar e mostrar na tela onome Andre e, depois realoque para armazenar e mostrar na tela o nome Andre Mendesda Rosa. Depois libere a memória, ou seja, desaloque.
  • 21. A saída do exemplo é a seguinte:

×