Alocação dinâmica de memória

Até aqui, fizemos alguns malabarismos com endereços de memória, aplicando-os em diferentes cenários (variáveis de tipos primitivos, vetores e matrizes). Apesar da variedade, sempre trabalhamos com a alocação estática de memória. Agora, começaremos a estudar a alocação dinâmica de memória.

🧠 Teste seus conhecimentos

Qual é a principal vantagem da alocação dinâmica de memória?

  • A

    Não precisar manipular endereços de memória.

  • B

    Declarar variáveis de diferetes tipos.

  • C

    Criar vetores e matrizes de tamanhos indefinidos.

  • D

    Alocar memória em tempo de execução, conforme a necessidade identificada durante o uso do programa.

  • E

    Nenhuma das alternativas anteriores.

💡 Entendendo o conceito

A linguagem de programação C permite, com o uso de ponteiros, a alocação dinâmica de memória. Essa estratégia, útil quando não se sabe previamente a quantidade de memória a ser alocada, pode ser usada tanto para a alocação de variáveis de tipos primitivos quanto para estruturas de dados homogêneas, como vetores e matrizes.

📙 Funções de apoio

Para executarmos operações de alocação dinâmica de memória, precisamos fazer o uso de algumas funções:

sizeof

A função sizeof, que já discutimos em nossas aulas, retorna o espaço em bytes necessário para armazenar uma variável ou um tipo de dado. Ao lidarmos com variáveis, podemos adotar a seguinte sintaxe:

sizeof variavel;

Ou, ainda, a mesma estratégia usada para analisar o tamanho necessário de um tipo de dado:

sizeof(variavelOuTipo);

Observe o código a seguir:

int x = 10;
int tam_x = sizeof(x); // recebendo uma variável
int tam_x_alternativo = sizeof x; // recebendo uma variável de forma alternativa
int tam_int = sizeof(int); // recebendo um tipo de dado

printf("Tam X => %d\n", tam_x);
printf("Tam X alternativo => %d\n", tam_x_alternativo);
printf("Tam INT => %d\n", tam_int);

malloc

A função malloc (memory allocation) é responsável por solicitar um espaço de memória ao computador. Seu retorno será, como antecipado no início desta página, o endereço de memória da primeira posição desse espaço. Como entrada, a função deve receber o tamanho (em bytes) do espaço a ser alocado.

A função possui o seguinte protótipo:

void *malloc(unsigned int num);

Antes de mais nada, vamos entender detalhamente esse protótipo:

Veja um exemplo de chamada para a função:

/* Para inteiro */
int *a = (int*) malloc(sizeof(int));
/* Para float */
float *b = (float*) malloc(sizeof(float));
/* Para char */
char *c = (char*) malloc(sizeof(char));

Cuidados especiais

Quando usamos a alocação dinâmica de memória, estamos assumindo mais responsabilidades. Nem sempre as solicitações de alocação de espaço serão atendidas, seja por falta de espaço na memória, seja por algum problema ocorrido durante a execução da função.

calloc

Não, não foi erro de digitação: além da função malloc, podemos fazer uso da função calloc, que atua de forma muito parecida com a anterior. A diferença fica evidenciada na protótipo da função:

void *calloc(unsigned int num, unsigned int size);

Se tiver um olhar atento, você deve ter reparado que a diferença consiste na inclusão de um parâmetro a mais: unsigned int size. A diferença agora é pequena, mas interessante.

Imagine que a nossa intenção é alocar um espaço de memória para armazenar três inteiros contíguos. O comando com malloc seria:

int *a = (int*) malloc(sizeof(int) * 3);

Com calloc, ficaria:

int *a = (int*) calloc(3,sizeof(int));

realloc

Para enterdemos a importância da função realloc, precisamos primeiro compreender seu contexto de utilização: imagine que já fizemos a alocação de um espaço de memória, usando a função malloc ou calloc. No entanto, descobrirmos posteriormente que o espaço de memória necessário seria diferente. Para realocar esse espaço de memória, alterando seu tamanho, precisamos fazer usando a função realloc.

Veja o seu protótipo:

void *realloc(void *ptr, unsigned int num);

Vamos entender detalhamente esse protótipo:

Agora, vamos ver um caso de utilização:

int *a = (int*) calloc(1,sizeof(int));
int *realocado = (int*) realloc(a, sizeof(int)*100);
    
printf("A: %p\n", a);
printf("Realocado: %p\n", realocado);

if (realocado != NULL) {
  a = realocado;
}

free

As variáveis armazenadas via alocação estática de memória são gerenciadas automaticamente. Por exemplo: quando uma função termina, as variáveis declaradas nela são excluídas sem qualquer necessidade de nossa intervenção.

Na alocação dinâmica de memória, cabe ao desenvolvedor do softwware fazer a liberação do espaço por ele alocado. Para isso, usamos o procedimento free, que possui o seguinte protótipo:

void free(void *p);

Note que agora trata-se de um procedimento: não retorno de ponteiro. Como parâmetro, esse procedimento recebe o endereço da memória da posição a ser liberada.

Vejamos:

int *a = (int*) calloc(1,sizeof(int));
free(a);

Há, no código acima, um detalhe importante: o procedimento free libera o espaço de memória, mas o ponteiro a continua com o mesmo valor. Acompanhe:

int *a = (int*) calloc(1,sizeof(int));
printf("%p\n", a);
free(a);
printf("%p\n", a);

Desse modo, após a liberação de um espaço de memória, devemos também nos atentar aos ponteiros soltos (dangling pointers). Para tanto, basta atribuir NULL ao ponteiro após fazer a liberação da memória:

int *a = (int*) calloc(1,sizeof(int));
free(a);
a = NULL;

📖 Bibliografia

Livros

  • Backes, A. (2018). Linguagem C - Completa e Descomplicada.