Buscar

Capitulo2-Gestão de Memória Dinâmica

Prévia do material em texto

Estrutura de Dados e Algoritmos em C 
Para uso exclusivo dos alunos da Faculdade de Engenharia de Universidade Católica de Angola Prof. Manuel Menezes 14 
 
 
 
Capítulo 2 
Gestão de Memória Dinâmica 
 
 
“ Qualquer pessoa pode escrever um programa que um computador entende, mas os bons programa-
dores escrevem programas que os humanos entende” 
 
 - Martin Fowler - 
 
Sumário: 
 
2.1 - Introdução 
2.2 - Gestão de Memória Dinâmica 
2.3 - Alocação de Memória Dinâmica 
2.4 - Operações de Leitura e Atribuição 
2.5 - Libertação de Memória 
2.6- Realocação de Memória 
2.7 - Alocação Dinâmica de Vectores 
2.8 - Alocação Dinâmica de Matrizes 
2.9 -Alocação Dinâmica de Estruturas 
 
2.1 - Introdução 
 
Os programas que foram desenvolvidos para uma primeira disciplina de 
programação de computadores, normalmente utilizam a gestão de memória 
estática. Esses programas caracterizam-se por determinar a quantidade de 
memória para sua execução em tempo de compilação. Como consequência, o 
espaço necessário para o programa ser executado não pode ser alterado e fica 
ao dispor do programa de forma exclusiva durante a sua execução. 
 
Neste capítulo, estudaremos os métodos para desenvolver programas que 
utilizam a gestão da memória de dinâmica, ou seja, a quantidade de memória 
para executar um programa, poderá ser aumentada ou diminuída em cada 
instante. Para dominar esses métodos o leitor terá a necessidade de conhecer 
às técnicas de manipulação de ponteiros. 
 
Estrutura de Dados e Algoritmos em C 
Para uso exclusivo dos alunos da Faculdade de Engenharia de Universidade Católica de Angola Prof. Manuel Menezes 15 
 
 
2.2- Gestão de Memória Dinâmica 
 
Para desenvolver programas que utilizam a gestão de memória dinâmica, 
necessitamos de funções especiais para solicitar blocos consecutivos de 
memória. Uma vez obtidos esses blocos, o programa vai armazenar dados, e 
quando essa quantidade de memória não for mais necessária, esses blocos 
serão devolvidos à memória principal. 
 
Se o programa solicitar blocos de memória e não os libertar, e se essa práctica 
for repetida, o programa poderá apropriar-se de forma exclusiva de uma grande 
quantidade de memória e bloquear o funcionamento do sistema operativo. 
 
A gestão de memória dinâmica exige algumas precauções no desenvolvimento 
do software. Por este motivo, muitas linguagens de programação de alto-nível 
mais recentes (Lisp, Java, Scheme, Python, Perl, Mathematica, e outras), 
efectuam a gestão de memória dinâmica de forma automática, permitindo que o 
programador se concentre na implementação do algoritmo e nos modelos de 
dados associados, sem ter a necessidade de preocupar-se com os problemas 
de dimensionamento de variáveis ou a quantidade de memória necessário para 
o armazenamento de dados. 
 
Neste contexto, é necessário perguntar quais as vantagens de programar em C 
e porque o C contínua a ser utilizado em muitas áreas de aplicação. Existem 
algumas respostas para estas questões, que daremos a seguir: 
 
A gestão de memória dinâmica permite a construção de programas mais 
eficientes e com a utilização de menores recursos de hardware. 
 
A linguagem C é a linguagem de alto-nível que possui mais recursos para emular 
a linguagem máquina. Dada essa aproximação, a maioria dos sistemas 
operativos modernos são escritos em C; 
 
A maioria dos compiladores de linguagens de alto nível, incluindo o próprio C, 
são actualmente escritos e desenvolvidos em C. Ou seja, nesta perspectiva, o C 
é hoje uma linguagem indispensável `a geração da maior parte das linguagens 
de programação e por esse motivo é denominado por “linguagem das 
linguagens”. 
 
2.3 - Alocação de Memória Dinâmica 
 
Para solicitar um bloco de memória durante a execução do programa, é 
necessário invocar a função malloc (memory allocation), que possui a seguinte 
sintaxe: 
 
void *malloc (size_t numero_de_bytes); 
 
esta função recebe como parâmetro o tamanho do espaço solicitado em bytes e 
devolve um ponteiro que faz referência ao primeiro byte do bloco disponibilizado, 
ou um ponteiro nulo se o pedido não for satisfeito. 
 
Estrutura de Dados e Algoritmos em C 
Para uso exclusivo dos alunos da Faculdade de Engenharia de Universidade Católica de Angola Prof. Manuel Menezes 16 
 
 
Por exemplo, para solicitar 1000 bytes de memória, e apontar para o endereço 
desse bloco o ponteiro ptr, devemos utilizar as seguintes linhas de código: 
 
void *ptr; 
ptr = malloc(1000); 
 
O ponteiro retornado pela função malloc() é um ponteiro genérico e não possui um 
tipo de dados específico. Por este facto, ele é declarado como void *. 
 
2.4 - Operações de Leitura e Atribuição 
 
Com ponteiros genéricos ou ponteiros do tipo void *, não podemos efectuar 
operações de atribuição e de leitura, porque o programa não consegue 
armazenar dados nos blocos de memória que foram alocadas. 
 
Para tornar essas operações possíveis é necessário converter de forma explicita, 
utilizando o operador cast, o endereço obtido pela função malloc(), para um 
ponteiro com o tipo de dados que iremos manipular. 
 
Por exemplo, para armazenar um número inteiro na memória dinâmica, devemos 
converter o ponteiro genérico para um ponteiro do tipo inteiro. 
 
void *ptr; 
int *pnumero; 
ptr = malloc(1000); 
pnumero = (int *)ptr; 
 
Agora, o ponteiro pnumero e o ponteiro ptr apontam para o início de um bloco de 
memória dinâmica obtido pela função malloc(). Através do ponteiro pnumero, o 
programa poderá manipular esse bloco de memória como se trata-se de um 
vector de números inteiros. 
 
Por exemplo, podemos armazenar no endereço apontado por pnumero um valor 
qualquer como fazemos com as variáveis elementares: 
 
*pnumero = 10; 
 
Mas, muitas vezes temos a necessidade de armazenar o endereço retornado 
pela função malloc() num ponteiro específico. Para isso efectuamos a seguinte 
conversão: 
 
int *pnumero; 
pnumero = (int *)malloc(1000); 
*pnumero = 10; 
 
 
 
 
 
Estrutura de Dados e Algoritmos em C 
Para uso exclusivo dos alunos da Faculdade de Engenharia de Universidade Católica de Angola Prof. Manuel Menezes 17 
 
O ponteiro retornado pela função malloc() é a única forma de aceder à um bloco 
de memória dinâmica disponibilizada. Através desse ponteiro o programa pode 
escrever e ler dados nesse espaço. Mas se durante a execução do programa 
este ponteiro for pedido, esse espaço permanecerá alocado até o fim do 
programa e a sua localização será impossível de detectar. 
 
2.5 - Libertação de Memória Dinâmica 
 
Na primeira disciplina de fundamentos de programação, o leitor certamente viu 
que o espaço de memória para as variáveis locais é reservado em memória 
quando uma função é chamada, e automaticamente libertado quando a função 
termina. 
 
Ao contrário das variáveis locais, os blocos de memória solicitados pela função 
malloc() permanece sob propriedade do programa por tempo indeterminado. 
Quando esse programa não precisar mais dessa porção de memória, é neces-
sário devolve-lo à memória principal. Essa acção deve ser feita pela função free() 
que possui a seguinte sintaxe: 
 
void free (void *ptr); 
 
no qual 
 
*ptr é um ponteiro para um bloco de memória que deve ser liberado. 
 
Uma vez libertado o bloco de memória, é impossível aceder a esse espaço. Após a 
execução dessa função, todos os ponteiros que fazem referencia a esse espaço 
tornam-se inválidos. 
 
Constitui uma boa práctica de programação, executar a chamada de uma função 
free() para cada ponteiro obtido por uma função malloc(). Desta forma temos a 
certeza que toda a memória alocado foi libertada. 
 
2.6 – Realocação de Memória 
 
Para aumentar ou diminuir durante a execução de um programa a quantidadede memória, também podemos utilizar a função realloc, que possui a seguinte 
sintaxe: 
 
void *realloc (void *ptr, size_t total_new_size); 
 
no qual 
 
*ptr é o ponteiro para o bloco de memória previamente reservado; 
total_new_size é a nova dimensão do bloco de memória a reservar. 
 
Devemos salientar que o segundo argumento, size_t total_new_size, tem um 
significado semelhante ao da função malloc (size_t total_size). 
 
 
Estrutura de Dados e Algoritmos em C 
Para uso exclusivo dos alunos da Faculdade de Engenharia de Universidade Católica de Angola Prof. Manuel Menezes 18 
 
Suponhamos que no início de um programa tínhamos reservado um bloco de 
memória para a n inteiros: 
 
x = (int*) malloc(n, sizeof(int)); 
 
e, mais tarde, verificamos que tínhamos a necessidade de acrescentar 1000 po-
sições a este bloco de memória. Este resultado poderia ser obtido com a instru-
ção: 
 
x = (int*) realloc(x, (n+1000)*sizeof(int)); 
 
Para consolidar os conhecimentos, vamos descrever em pormenor o funciona-
mento dessas instruções. Suponhamos sem perda da generalidade que inicial-
mente tínhamos n=2000, e que o valor de x resultante da chamada da função 
malloc() era 10000. Isso quer dizer que reservamos um bloco de memória que 
começa no endereço 10000 e vai até o endereço 11999. Quando executarmos 
à chamada da função realloc(), duas situações podem ocorrer. Se os endereços 
de memória entre 12000 e 12999 estiverem livres, o bloco de memória é auto-
maticamente prolongado. Mas se algum endereço de memória estiver ocupado, 
é necessário deslocar todo o bloco de memória para uma nova região. Essa nova 
região é mapeada, e a função realloc() retorna o endereço da primeira posição 
de memória. 
 
2.7 - Alocação Dinâmica de Vectores 
 
Uma das aplicações mais interessantes na gestão de memória dinâmica é a 
criação e a manipulação de vectores. Nos programas que desenvolvemos, 
declaramos um vector com MAX elementos, onde MAX é uma constante, e esse 
vector é normalmente carregado com um número inferior de elementos. 
 
Durante o processo de compilação é alocado um espaço para armazenar esse 
vector, mas o programa não utiliza normalmente todo o espaço disponibilizado. 
Uma parte desse espaço é desperdiçado e só volta a estar disponível para o 
computador, quando o programa terminar a sua execução. 
 
O nosso objectivo é declarar o tamanho do vector a maior precisão, ou seja, o 
vector vai ser criado em tempo de execução com o tamanho exacto para que 
não se despedisse nenhum bloco de memória. 
 
Mas, para criarmos um vector de forma dinâmica, o compilador calcula o número 
de bytes que vai necessitar, e esse número depende da arquitectura da máquina. 
Por exemplo, para computadores de 16 bits um inteiro é armazenado em dois 
bytes, mas se a arquitectura for de 32 bits, um inteiro é armazenado em quatro 
bytes. 
 
Para tornar o programa genérico e o mais independente da arquitectura, 
devemos utilizar a função biblioteca sizeof(), que possui a seguinte sintaxe: 
 
int sizeof nome da variável; 
int sizeof ( tipo de dados); 
 
Estrutura de Dados e Algoritmos em C 
Para uso exclusivo dos alunos da Faculdade de Engenharia de Universidade Católica de Angola Prof. Manuel Menezes 19 
 
 
onde 
o tipo de dados é qualquer tipo de dados suportado pela linguagem C. 
 
Esta função retorna o número de bytes necessários para armazenar esse tipo de 
dados ou essa variável. 
 
Então, para criar um ponteiro para um vector, devemos seguir a seguinte 
sequencia de passos: 
 
1º- Calcular com a função sizeof() o número de bytes para armazenar a estrutura; 
2º- Invocar a função malloc() para retornar um apontador genérico que aponta 
para o primeiro byte desse bloco de memória; 
3º- Converter o ponteiro genérico retornado pela função malloc(), para um 
ponteiro cujo tipo de dados é compatível com o tipo do vector. 
 
Mas por consistência de código, devemos verificar se o ponteiro retornado pela 
função malloc() é válido. Se ele for igual a NULL, o pedido não foi satisfeito. Isso 
quer dizer que o computador não possui memória disponível e devemos notificar 
tal facto ao utilizador. 
 
Vamos consolidar este conceito com alguns exemplos simples. O primeiro 
consiste em desenvolver um programa para carregar num vector n números 
inteiros digitados pelo utilizador. 
 
A primeira solução consiste em utilizar a gestão de memória estática. 
 
#include <stdio.h> 
main () 
 { 
 int v[3000], n; 
 n = lerQtdNumjeros(); 
 for (i = 0; i < n, i++) 
 { 
 printf (“Insira um inteiro: “); 
 scanf (“%d”, &v[i]); 
 } 
 . . . 
 return 0; 
 } 
 
Agora, vamos reescrever esse programa, com a gestão de memória dinâmica. 
 
#include <stdio.h> 
main () 
 { 
 int *v, n; 
 
Estrutura de Dados e Algoritmos em C 
Para uso exclusivo dos alunos da Faculdade de Engenharia de Universidade Católica de Angola Prof. Manuel Menezes 20 
 
 n = lerQtdNumjeros(); 
 v = (int*) malloc (n*sizeof (int)); 
 if (v == NULL) 
 printf ("Erro: memória insuficiente \n "); 
 else 
 { 
 for (i = 0; i < n, i++) 
 { 
 printf (“Insira um inteiro: “); 
 scanf (“%d”, &v[i]); 
 } 
 . . . 
 free (v); 
 } 
 return 0; 
 } 
 
Na chamada da função malloc(), dois aspectos devem ser considerados. Em 
primeiro lugar o operador sizeof() ´e um operador intrínseco da linguagem C que 
devolve a dimensão em bytes do tipo ou variável indicado no argumento. Por 
outro lado, à esquerda da função malloc() foi acrescentada e expressão (int*). 
Esta expressão funciona como um operador de cast, e obriga à conversão do 
apontador genérico devolvido pela função malloc() para um ponteiro do tipo in-
teiro. Em geral, na operação de cast é indicado o tipo do ponteiro que se encontra 
do lado esquerdo da atribuição. 
 
Após a reserva dinâmica de memória, o ponteiro v pode ser tratado como um 
vector convencional. É evidente que o índice i, não pode ultrapassar a posição 
n-1, porque essas áreas de memória são inválidas. 
 
O segundo exemplo consiste em desenvolver um programa com funções para 
calcular a média e a variância para n números reias. 
 
#include <stdio.h> 
#include <stdlib.h> 
 int main () 
 { 
 int i, n; 
 float med, var; 
 n = lerQtdNumeros(); 
 float *v = (float*) malloc( n *sizeof (float)); 
 if (v == NULL) 
 printf ("Erro: memória insuficiente \n "); 
 else 
 { 
 for ( i = 0; i < n; i++ ) 
 { 
 printf (“Insira um inteiro: “); 
 scanf (“%d”, &v[i]); 
 } 
 
Estrutura de Dados e Algoritmos em C 
Para uso exclusivo dos alunos da Faculdade de Engenharia de Universidade Católica de Angola Prof. Manuel Menezes 21 
 
 med = media(n, v); 
 var = variancia(n, v, med); 
 printf ("Media = %f Variancia = %f \n", med, var); 
 free(v); 
 } 
 return 0; 
 } 
 
onde a função para calcular a média recebe como parâmetro o número de 
elementos do vector e um ponteiro para o primeiro elemento do vector. 
 
float media (int n, float *v) 
 { 
 int i; float s = 0.0; 
 for (i = 0; i < n; i++) 
 s += v[i]; 
 return s/n;} 
 
e a função para calcular a variância recebe como parâmetro o número de 
elementos do vector, um ponteiro para o primeiro elemento do vector e o valor 
da média. 
 
float variancia (int n, float *v, float m) 
 { 
 int i; float s = 0.0; 
 for (i = 0; i < n; i++) 
 s += (v[i] - m) * (v[i] - m); 
 return s/n; 
 } 
 
Para tornar o programa mais elegante e mais fácil de ler, devemos desenvolver 
uma função para alocar uma porção de memória. Essa função retorna um se a 
operação foi executada com sucesso e zero no caso contrário. Uma possível 
solução é dada por: 
 
 boolean aloca ( float *v, int n) 
 { 
 *v = (float*) malloc( n *sizeof (float)); 
 return ( v == NULL ); 
 } 
 
 
 
2.8 - Alocação Dinâmica de Matrizes 
 
Suponhamos que num determinado programa, pretendemos substituir a 
sequência de instruções 
 
 
Estrutura de Dados e Algoritmos em C 
Para uso exclusivo dos alunos da Faculdade de Engenharia de Universidade Católica de Angola Prof. Manuel Menezes 22 
 
#define N ... 
#define M ... 
... 
int x[N][M]; 
 
por uma estrutura dinâmica, em que os limites n e m são variáveis cujo valor 
deverá ser definido na fase de execução do programa. 
 
A solução para este problema consiste em substituir a estrutura x por um vector 
dinâmico de ponteiros, em que cada posição é, por sua vez, um vector dinâmico 
de ponteiros. Considere por exemplo, que se pretende criar uma matriz de di-
mensão n por m. Uma possível solução é descrita pela seguinte função. 
 
boolean aloca ( int ** x , int n , int m) 
 { 
 int k; 
 x = (int**) malloc(n, sizeof(int*)); 
 if (x == NULL) return 0; 
 else 
 { 
 for (k = 0 ; k < n ; k++) 
 { 
 x[k] = (int*) malloc(m,sizeof(int)); 
 if (x == NULL) return 0; 
 } 
 } 
 return 1; 
 } 
 
Agora, vamos criar uma matriz 4 por 2 e inicializa-la com valores inteiros digita-
dos pelo utilizador. Uma possível solução para esse problema é dada pelo se-
guinte programa: 
 
int main() 
 { 
 int k, j , n, m; 
 int **x; 
 n = lerDimensaoLinha(); 
 m = lerDimensaoColuna(); 
 if ( ! aloca ( x , n , m) ) 
 printf (" Erro: memória insuficiente \n "); 
 else 
 { 
 for (k = 0; k < n ; k++) 
 for (j = 0; j < m ; j++) 
 x[k][j] = lerElemento(); 
 } 
 return 0; 
 } 
 
 
Estrutura de Dados e Algoritmos em C 
Para uso exclusivo dos alunos da Faculdade de Engenharia de Universidade Católica de Angola Prof. Manuel Menezes 23 
 
Este exemplo mostra que para criar uma matriz em memória dinâmica, devemos 
declarar um ponteiro de ponteiros para os elementos da matriz e em seguida, 
reservar dinâmicamente um vector de ponteiros e criar dinamicamente os vecto-
res correspondentes a cada linha. 
 
2.9 - Alocação Dinâmica de Estruturas 
 
O procedimento para alocação dinâmica de estruturas (struct) é análogo ao 
procedimento para a alocação dinâmica de vectores. Para mostrar essa 
analogia, veremos a declaração de um ponto no plano cartesiano. 
 
typedef struct 
 { 
 int ordenada; 
 int abcissa; 
 } TPlano; 
 
Para criar um ponteiro para uma estrutura devemos utilizar os mesmos passos 
que utilizamos para criar um ponteiro para um vector. Veremos a seguir uma 
função que atende a essa necessidade. 
 
 boolean aloca ( TPlano *coordenada, int n ) 
 { 
 *Coordenda = (TPlano*)malloc ( n* sizeof (TPlano) ); 
 return (coordenada == NULL) ; 
 } 
 
Suponhamos que pretendemos colocar zeros em todos os elementos de um 
vector do tipo plano. Como o vector será criado em tempo de execução, o 
utilizador deverá definir o número de elementos e, esse número, será 
armazenado numa variável n. 
 
#include <stdlib.h> 
#include <stdio.h> 
int main ( ) 
{ 
 int i, j, n; 
 typedef struct 
 { 
 int ordenada; 
 int abcissa; 
 } TPlano; 
 n = lerQtdElementos(); 
 if ( !aloca (tabPonto, n )) 
 printf (" Erro: memória insuficiente \n "); 
 else 
 { 
 for ( i = 0; i < n; i++ ) 
 { 
 
Estrutura de Dados e Algoritmos em C 
Para uso exclusivo dos alunos da Faculdade de Engenharia de Universidade Católica de Angola Prof. Manuel Menezes 24 
 
 (tabPonto+i)->ordenada = 0; 
 (tabPonto+i)->abcissa = 0 ; 
 } 
 free (tabPontos); 
 return 0; 
 } 
 } 
 
Mas poderíamos realizar essa operação, com a seguinte notação: 
 
 for (i = 0; i < n; i++) 
 { 
 (* (tabPonto+i).ordenada = 0; 
 (tabPonto+i).abcissa = 0 ; 
 } 
 
Contudo, utilizamos uma notação é mais pesada, muito susceptível a erros, e 
facto, raramente utilizada em ambientes profissionais. 
 
Não é por demais salientar que a notação (tabPonto+i)->ordenada é equivalente 
a notação tabPonto[i]->ordenada. A primeira está na notação de ponteiros 
enquanto a segunda na notação vectorial. 
 
 
2.10 - Exercícios

Continue navegando