Estruturas de dados dinâmicas: Malloc e Free

Vamos supor que você queira alocar certa quantidade de memória durante a execução de seu aplicativo. Você pode chamar a função malloc a qualquer momento e ela solicitará um bloco de memória da pilha. O sistema operacional reservará um bloco de memó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óprio uso.

Por exemplo, o código a seguir demonstra o uso mais simples possível da pilha:

  int main()
  {
  	int *p;  
  	p = (int *)malloc(sizeof(int));  
  	if (p == 0)
    {
        printf("ERRO: Sem memória\n"); 
 		return 1;  
	
    }
    *p = 5;  
    printf("&d\n", *p);  
    free(p);  
    return 0;  
   }  

A primeira linha neste programa invoca a função malloc. Esta função faz três coisas:

  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:


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 ocorreu com a linha if (p == 0) (que também poderia ter sido escrita como if (p == NULL) ou mesmo if (!p) Em caso de falha da alocação (se p for zero) o programa encerra. Em caso de êxito na alocação, o programa inicializa o bloco com o valor 5, imprime o valor e executa a função free para retornar a memória à pilha antes do programa encerrar.

Não existe diferença entre este código e o código anterior que determine p igual ao endereço de um inteiro existente i. A única distinção é que, no caso da variável i, a memória existiu como parte do espaço de memória pré-alocado do programa e tem dois nomes: i e *p. No caso da memória alocada da pilha, o bloco tem o único nome de *p e é alocado durante a execução do programa. Duas 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, como discutido abaixo.
Os dois programas a seguir mostram dois usos válidos e distintos de ponteiros. Tente distingüi-los usando um ponteiro e um valor de ponteiro:
  void main()
  {
  	int *p, *q;


    	p = (int *)malloc(sizeof(int));
  	q = p;  
 	*p = 10;  
 	priprintf("%d\n", *q);  
 	*q = 20;  	pr
     printf("%d\n", *q);  
  } 

O resultado final deste código seria 10 n linha 4 e 20 na linha 6. Eis um diagrama:


O seguinte código é um pouco diferente:

  void main()
  {
  	int *p, *q;   
 	p = (int *)malloc(sizeof(int));  
 	q = (int *)malloc(sizeof(int));  
        *p = 10;
  	*q = 20;  
 	*p = *q;  
  	prprintf("%d\n", *p);  
  } 

O resultado final deste código seria 20 na linha 6. Eis um diagrama:


Observe que o compilador permitirá *p = *q pois *p e *q são ambos inteiros. Esta instrução diz: "Mova o valor inteiro apontado por q em um valor inteiro apontado por p". A instrução move os valores. O compilador também permitirá p = q, pois p e q são ambos ponteiros e apontam para o mesmo tipo (se s for um ponteiro para um caractere, p = s não é permitido pois eles apontam para tipos diferentes). A instrução p = q diz: "Aponte p para o mesmo bloco para o qual q aponta". Em outras palavras, o endereço apontado por q é transferido para p, assim ambos apontam para o mesmo bloco. Esta instrução transfere os endereços.

De todos estes exemplos, você pode ver que há quatro modos diferentes para inicializar um ponteiro. Quando um ponteiro é declarado, como em int *p, ele inicia no programa em um estado não inicializado. Como ele pode apontar para qualquer lugar, remover sua referência é um erro. A inicialização de uma variável de ponteiro envolve apontá-la para um local conhecido na memória.

  1. Um modo, como já visto, é usar a instrução malloc. Esta instrução aloca um bloco de memória da pilha e aponta o ponteiro para o bloco. Isso inicializa o ponteiro, pois ele agora aponta para um local conhecido. O ponteiro é inicializado pois foi preenchido com um endereço válido: o endereço do novo bloco;

  2. O segundo modo, como visto recentemente, é usar uma instrução como p = q para que p aponte para o mesmo lugar que q. Se q estiver apontando para um bloco válido, então p é inicializado. O ponteiro p é carregado com o endereço válido contido em q. Entretanto, se q não for inicializado ou for inválido, p obterá o mesmo endereço inútil;

  3. O terceiro modo é apontar o ponteiro para um endereço conhecido, como o endereço de uma variável global. Por exemplo, se i é um inteiro e p é um ponteiro de um inteiro, então a instrução p=&i inicializa p apontando-o para i;

  4. A quarta forma de inicialização de um ponteiro é usar um valor zero. Zero é um valor especial usado com ponteiros, como mostrado aqui:
      p = 0;  

    ou:

      p = NULL;  

    O que isto faz fisicamente é colocar um zero em p. O endereço do ponteiro p é zero. Isso é geralmente diagramado como:


Qualquer ponteiro pode ser definido para apontar para zero. Quando p aponta para zero, ele não aponta para um bloco. O ponteiro simplesmente contém o endereço zero e este valor é útil como uma tag. Você pode usá-lo em declarações como:
  if (p == 0)
  {
  	...  
  }  

ou:

  while (p != 0)
  {
  	...  
  }

 

O sistema também reconhece o valor zero e gerará mensagens de erro se você remover a referência de um ponteiro zero. Por exemplo, no seguinte código:

  p = 0;
  *p = 5;  

O programa geralmente travará. O ponteiro p não aponta para um bloco, aponta para zero, assim um valor não pode ser atribuído a *p. O ponteiro zero será usado como sinalizador quando chegarmos às listas vinculadas.

O comando malloc é usado para alocar um bloco de memória. Também é possível desalocar um bloco de memória quando ele não é mais necessário. Quando um bloco é desalocado, ele pode ser reutilizado por um comando malloc subseqüente que permite ao sistema reciclar a memória. O comando usado para desalocar a memória é chamado free e aceita um ponteiro como seu parâmetro. O comando free faz duas coisas:

  1. O bloco de memória apontado pelo ponteiro não está reservado e é devolvido à memória livre na pilha. Ele pode ser reutilizado posteriormente por novas declarações;
  2. O ponteiro permanece em estado não inicializado e deve ser reinicializado antes que possa ser novamente utilizado.
A instrução free simplesmente retorna um ponteiro para seu estado original não-inicializado e devolve o bloco para a pilha.

O exemplo a seguir mostra como usar a pilha: ele aloca um bloco de inteiros, preenche, escreve e o descarta:

  #include  <stdio.h>
  
  int main()
  {
      int *p;
      p = (int *)malloc(sizeof(int));
      *p=10;
      printf("%d\n",*p);  
      free(p);
      return 0;
  } 

Este código só é útil para demonstrar o processo de alocação, desalocação e como usar um bloco em C. A linha malloc aloca um bloco de memória do tamanho especificado - neste caso, sizeof(int) bytes (4 bytes). O comando sizeof em C retorna o tamanho, em bytes, de qualquer tipo. A codificação poderia ter dito malloc(4), uma vez que sizeof(int) é igual a 4 bytes na maioria das máquinas. Todavia, o uso de sizeof torna a codificação mais portátil e legível.

A função malloc retorna um ponteiro para o bloco alocado. Este ponteiro é genérico. O uso do ponteiro sem conversão de tipo geralmente produz um tipo de aviso do compilador. O (int *) converte o ponteiro genérico retornado por malloc em um "ponteiro para um número inteiro", que é esperado por p. A instrução free em C retorna um bloco para a pilha para reutilização.

O segundo exemplo ilustra as mesmas funções que o exemplo anterior, mas usa uma estrutura em vez de um inteiro. Em C, o código se parece com:

  #include <stdio.h>
 
  struct rec
  {
      int i;
      float f;
      char c;
  };    
int main()
  {
      struct rec *p;
      p=(struct rec *) malloc (sizeof(struct rec));
      (*p).i=10;
      (*p).f=3.14;
      (*p).c='a'; 
      printf("%d %f %c\n",(*p).i,(*p).f,(*p).c);  
      free(p);
      return 0;
  }  

Observe a seguinte linha:

  (*p).i=10;  

Muitos se perguntam por que não funciona:

  *p.i=10; 

A resposta está relacionada com a precedência de operadores em C. O resultado do cálculo 5+3*4 é 17, e não 32, pois o operador * tem maior precedência do que + na maioria das linguagens de computação. Em C, o operador . tem precedência maior que *, portanto os parênteses forçam a devida precedência.

A maioria das pessoas se cansa de digitar (*p).i o tempo todo, de forma que o C oferece uma anotação de taquigrafia. As duas instruções a seguir são equivalentes, porém a segunda é mais fácil de digitar:

  (*p).i=10;
  p->i=10;  

Você verá a segunda instrução mais freqüentemente do que a primeira ao ler a codificação de outras pessoas.