Alocação dinâmica de memória

As declarações abaixo alocam espaço de memória para diversas variáveis. A alocação é estática, ou seja, acontece antes que o programa comece a ser executado:

char c; 
int i; 
int v[10]; 

Às vezes, a quantidade de memória a alocar só se torna conhecida durante a execução do programa. Para lidar com essa situação é preciso recorrer à alocação dinâmica de memória. A alocação dinâmica é gerenciada pelas funções malloc e free, que estão na biblioteca stdlib.  Para usar esta biblioteca, você deve incluir o correspondente arquivo-interface no seu programa por meio de

#include <stdlib.h>

Função malloc

A função  malloc  (o nome é uma abreviatura de memory allocation) aloca um bloco de bytes consecutivos na memória do computador e devolve o endereço desse bloco.  O número de bytes é especificado no argumento da função. No seguinte fragmento de código, malloc aloca 1 byte:

char *ptr;
ptr = malloc (1);
scanf ("%c", ptr);

O endereço devolvido por malloc é do tipo genérico  void *.   O programador armazena esse endereço num ponteiro de tipo apropriado. No exemplo acima, o endereço é armazenado num ponteiro-para-char.

Para alocar um tipo-de-dados que ocupa mais de 1 byte, é preciso recorrer ao operador sizeof, que diz quantos bytes o tipo especificado tem:

typedef struct {
   int dia, mes, ano; 
} data;
data *d;
d = malloc (sizeof (data));
d->dia = 31; d->mes = 12; d->ano = 2014;

As aparências enganam: sizeof não é uma função mas um operador (tal como return, por exemplo).   A propósito, sizeof também pode ser aplicado a variáveis: se var é uma variável então   sizeof var   é o número de bytes ocupado por var[Como var é uma variável, os parênteses na expressão sizeof (var) são redundantes.]

Overhead. Cada invocação de malloc aloca um bloco de bytes consecutivos maior que o solicitado: os bytes adicionais são usados para guardar informações administrativas sobre o bloco de bytes (essas informações permitem que o bloco seja corretamente desalocado, mais tarde, pela função free).  O número de bytes adicionais pode ser grande, e não depende do número de bytes solicitado no argumento de malloc.  Não é ineficiente, portanto, alocar pequenos blocos de bytes;  é preferível alocar um grande bloco e dele retirar pequenas porções na medida do necessário. Felizmente, malloc faz isso de maneira automática, sem que o programador/usuário perceba.

Exercícios 1

  1. Discuta, passo a passo, o efeito do seguinte fragmento de código:
    int *p, *q;
    p = malloc (sizeof (int));
    *p = 123;
    q = malloc (sizeof (int));
    *q = *p;
    q = p;
    free (p);
    free (q); // má ideia...
    q = NULL; // boa ideia
    
  2. Verificação do tamanho.  Compile e execute o seguinte programa:
    typedef struct {
       int dia, mes, ano; 
    } data;
    int main (void) {
       printf ("sizeof (data) = %d\n", sizeof (data));        
       printf ("sizeof (data *) = %d\n", sizeof (data *));        
       return 0;
    }
    

A memória é finita

Se a memória do computador já estiver toda ocupada, malloc não consegue alocar mais espaço e devolve NULL. Convém verificar essa possibilidade antes de prosseguir:

ptr = malloc (sizeof (data));
if (ptr == NULL) {
   printf ("Socorro! malloc devolveu NULL!\n");
   exit (EXIT_FAILURE);
}

A codificação frequente e repetida desse teste é cansativa. Por isso, vamos usar a seguinte versão alternativa de malloc:

void *mallocc (size_t nbytes)
{
   void *ptr;
   ptr = malloc (nbytes);
   if (ptr == NULL) {
      printf ("Socorro! malloc devolveu NULL!\n");
      exit (EXIT_FAILURE);
   }
   return ptr;
}

O parâmetro de mallocc é do tipo size_t.  Em muitos computadores, size_t é equivalente a unsigned int.   A função mallocc não está em nenhuma biblioteca e é desconhecida fora destas notas de aula.

Função free

As variáveis alocadas estaticamente dentro de uma função desaparecem quando a execução da função termina. Já as variáveis alocadas dinâmicamente continuam a existir mesmo depois que a execução da função termina. Se for necessário liberar a memória ocupada por essas variáveis, é preciso recorrer à função free.

A função free libera a porção de memória alocada por malloc. A instrução free (ptr) avisa ao sistema que o bloco de bytes apontado por ptr está livre. A próxima chamada de malloc poderá tomar posse desses bytes.

A função free não deve ser aplicada a uma parte de um bloco de bytes alocado por malloc; aplique free apenas ao bloco todo.

Convém não deixar ponteiros soltos (= dangling pointers) no seu programa, pois isso pode ser explorado por hackers para atacar o seu computador. Portanto, depois de cada free (ptr), atribua NULL a ptr:

free (ptr);
ptr = NULL;

Atribuir um valor a um ponteiro que se tornou inútil é decididamente deselegante, mas não há como lidar com hackers de maneira elegante…

Para não cansar o leitor com detalhes repetitivos, estas notas não seguem a recomendação de segurança acima.

Vetores e matrizes

Eis como um vetor (= array) com n elementos inteiros pode ser alocado (e depois desalocado) durante a execução de um programa:

int *v; 
int n, i;
scanf ("%d", &n);
v = mallocc (n * sizeof (int));
for (i = 0; i < n; ++i) 
   scanf ("%d", &v[i]);
for (i = n; i > 0; --i) 
   printf ("%d ", v[i-1]);
free (v);

(A propósito, veja observação sobre vetores e endereços em outra página.)  Do ponto de vista conceitual (e apenas desse ponto de vista) a instrução

v = mallocc (100 * sizeof (int));

tem efeito análogo ao da alocação estática

int v[100];

A propósito, convém lembrar que a norma ANSI não permite escrever  int v[n],  a menos que n seja uma constante, definida por um #define.

Matrizes bidimensionais são implementadas como vetores de vetores. Uma matriz com m linhas e n colunas é um vetor cada um de cujos m elementos é um vetor de n elementos. O seguinte fragmento de código faz a alocação dinâmica de uma tal matriz:

int **M; 
int i;
M = mallocc (m * sizeof (int *));
for (i = 0; i < m; ++i)
   M[i] = mallocc (n * sizeof (int));

O elemento de M que está no cruzamento da linha i com a coluna j é denotado por M[i][j].

Exercícios 2

  1. Escreva uma função que receba um caractere c e transforme c em uma string (cadeia de caracteres), ou seja, devolva uma string de comprimento 1 tendo c como único elemento.
  2. Escreva uma função que desaloque a matriz bidimensional M alocada acima. Quais devem ser os parâmetros da função?
  3. Suponha dado um arquivo que contém uma sequência de números inteiros. A quantidade de números no arquivo é desconhecida. Escreva uma função que imprima esses números em ordem inversa.  É claro que você terá que ler os números todos (suponha que o arquivo é stdin), armazenar os números na memória, e depois imprimir em ordem inversa.  A dificuldade esté em alocar espaço para uma quantidade de números que só será conhecida quando chegarmos ao fim do arquivo.  Dica: use um vetor periodicamente redimensionado.