Alocação dinâmica de memória em linguagem C


1 Introdução

Alocação dinâmica de memória é uma técnica de gestão de memória que pervade praticamente todos os programas de maior dimensão. É por isso um tópico importante na educação dum programador a sério. Este documento procura revêr alguns aspectos relacionados com alocação dinãmica de memória e a sua utilização na alocação de buffers para entrada/saída de dados.

2 Alocação dinãmica de memória (revisão)

Em linguagem C, o programador tem que fazer a gestão de memória alocada dinamicamente.

A biblioteca de C oferece 2 funções para gestão de memória alocada dinamicamente:

void *malloc(size_t size)
para alocação duma região de memória de tamanho size bytes.
void free(void *ptr)
para libertar uma região de memória previamente alocada com malloc. Consequentemente, o argumento de free, deve ser um valor previamente retornado por malloc.

Para uma informação mais pormenorizada sobre estas funções pode consultar as suas man pages:

man 3 malloc e man 3 free

O operador sizeof é particularmente útil para especificar o valor do argumento de malloc(). De facto, a sua utilização torna o código C mais portável, i.e. mais independente da arquitectura, e liberta o programador de ter de saber qual o tamanho dos diferentes tipos ou estruturas de dados. O segmento seguinte, típico da alocação de memória para um vector de n inteiros, ilustra a utilização do operador sizeof:

ptr = malloc(n * sizeof(int));

Note que a memória é um recurso limitado, pelo que um programa não pode alocar a memória que bem entender. Assim, é importante que quando invoca a função malloc() teste se o valor retornado é ou não NULL:

ptr = malloc(n * sizeof(int));
if( ptr == NULL ) {
   printf(``Out of memory\n'');
   return NULL;
}

Uma propriedade das regiões de memória alocadas dinamicamente é que persistem para além do bloco da linguagem C onde são alocadas. Por exemplo, o segmento de código acima, poderia fazer parte duma função int *get_arr(int n), a qual aloca memória dinamicamente para um vector de n inteiros e inicializa-a em 0:

int *get_array(int n) {
   int *ptr;
   int i;

   /* Allocate memory for integer array */
   if( (ptr = malloc(n * sizeof(int))) == NULL ) {
      printf(``Out of memory\n'');
      return NULL;
   } 
   /* Initialize the array elements to 0 */
   for( i = 0; i < n; i++) {
      ptr[i] = 0;
   }

   return ptr;
}

Note que neste caso, a função que invoca get_array() deverá testar se o valor retornado é ou não NULL. Se não fôr, poderá aceder aos elementos do vector alocado dinamicamente.

Infelizmente, a gestão de memória alocada dinamicamente não é fácil, sendo uma das razões para a relativa falta de produtividade da linguagem C. Há essencialmente 2 tipos de problemas:

fugas de memória (memory leaks):
resultam da memória não ser libertada quando não é mais necessária: pode conduzir à terminação intempestiva dum programa, como ilustrado no problema 2 abaixo;
dangling pointers:
resultam da utilização de apontadores para blocos de memória já libertados: podem conduzir à terminação dum programa com a "famosa" mensagem Segmentation violation: core dumped.
O grande problema com este tipo de erros é a difícil identificação das suas causas: tipicamente ocorrem em instruções não directamente relacionadas com a causa. A detecção de problemas devido ao uso de dangling pointers pode ser simplificada inicializando os apontadores com o valor NULL quando o bloco de memória para que apontam é libertado: deste modo, a primeira utilização indevida dum apontador conduzirá ao erro acima mencionado.

De qualquer modo, a sua única defesa contra erros deste tipo é ser extremamente cuidadoso e disciplinado quando usa alocação dinãmica de memória. Certas linguagens de programação, por exemplo Java, encarregam-se elas mesmo da gestão da memória alocada dinamicamente, libertando o programador duma tarefa dada a erros.

3 Buffers para entrada/saída de dados

Um uso comum de alocação dinâmica de memória é na alocação de buffers que são usados para guardar dados a ler de dispositivos periféricos de entrada ou a escrever em dispositivos periféricos de saída.

De facto, como se deve recordar de SIF, Linux oferece as seguintes chamadas ao sistema para aceder a ficheiros:

ssize_t read(int fd, void *buf, size_t count)
para ler dados;
ssize_t write(int fd, void *buf, size_t count)
para escrever dados.
Em ambas as funções buf deve ser o endereço dum buffer de memória usado em:
read()
para passar os dados a ler do ficheiro do kernel para a aplicação , sendo count o tamanho deste buffer (por que razão o SO precisa deste argumento?);
write()
para passar os dados a escrever no ficheiro da aplicação para o kernel, sendo count o número de bytes a escrever (qual deve ser a relação entre o tamanho do buffer cujo endereço é passado e o valor de count?)

Tipicamente há duas alternativas na alocação destes buffers de memória, usar:

alocação estática
por exemplo:
   char buf[BUFSIZ];
   [...]
   n = read(fd, buf, BUFSIZ);
alocação dinãmica
por exemplo:
   char *buf;
   [...]
   if( (buf = malloc(BUFSIZ)) == NULL) {
      printf(``Out of memory!\n'');
      exit(1);
   }	
   n = read(fd, buf, BUFSIZ);

IMPORTANTE: Um ERRO relativamente comum é passar a read() (ou a outras funções semelhantes) um apontador não inicializado. Por exemplo:

   char *buf;
   n = read(fd, buf, BUFSIZ);
Embora sintaticamente este segmento de código esteja correcto, e por isso o compilador não se ``queixe'', compilando sem erros nem avisos, semanticamente está INCORRECTO:
O kernel precisa de um buffer de memória onde porá os dados lidos. Contudo, no segmento acima, reserva-se apenas espaço para a variável buf que é um apontador para carácteres (e por isso ocupa apenas 4 bytes), NÃO se reserva espaço para os dados a ler. A variável buf tem um valor que é lixo (``[...] If they (automatic variables) are not set, they will contain garbage'', K&R, pg. 31). Isto é, buf fica a apontar para uma zona de memória ao acaso, a qual pode ou não ser válida. No primeiro caso, o conteúdo daquela zona de memória pode ser alterado inadvertidamente, resultando um comportamento errático da aplicação. No segundo caso, a aplicação terminará quando tentar aceder a essa zona de memória.

\epsfig{file=buf.eps}

Uma alternativa seria o próprio kernel alocar um buffer com o tamanho necessário dentro de read(). (De facto algumas funções da biblioteca de C, seguem essa estratégia.) Neste caso, o kernel teria que retornar à aplicação não só o número de bytes lidos (como acontece com a chamada ao sistema existente), mas também o endereço do buffer de memória que tiver reservado. Para isso, o protótipo da função read() teria que ser diferente.


About this document ...

This document was generated using the LaTeX2HTML translator Version 99.2beta6 (1.42)

Copyright © 1993, 1994, 1995, 1996, Nikos Drakos, Computer Based Learning Unit, University of Leeds.
Copyright © 1997, 1998, 1999, Ross Moore, Mathematics Department, Macquarie University, Sydney.

The command line arguments were:
latex2html -t C-malloc -no_navigation -split 0 -show_section_numbers malloc

The translation was initiated by Pedro Souto on 2001-04-04


Pedro Souto 2001-04-04