Nesta aula, vamos explorar duas funções essenciais para alocação dinâmica de memória em C: calloc e realloc. Enquanto malloc aloca memória sem inicializar, calloc aloca e zera a memória. Já realloc permite redimensionar um bloco alocado anteriormente, preservando seu conteúdo. Compreender essas funções é crucial para gerenciar memória de forma eficiente e segura.

Vamos ver como usá-las corretamente, quais os cuidados necessários e como evitar erros comuns, como vazamentos de memória e acesso a áreas inválidas.

calloc (zerado)

A função calloc (contiguous allocation) aloca memória para um array de elementos, cada um de tamanho especificado, e inicializa todos os bytes com zero. Sua assinatura é:

void *calloc(size_t num, size_t size);

Ela recebe dois argumentos: o número de elementos (num) e o tamanho de cada elemento (size). Retorna um ponteiro para o início do bloco alocado, ou NULL se a alocação falhar. A memória alocada é garantidamente zerada, o que é útil para estruturas que precisam de valores iniciais nulos, como arrays de contadores ou strings vazias.

Exemplo de uso:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *arr;
    int n = 5;
    arr = (int*) calloc(n, sizeof(int));
    if (arr == NULL) {
        printf("Falha na alocação\n");
        return 1;
    }
    // Todos os elementos são 0
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    free(arr);
    return 0;
}

Neste exemplo, arr[0] a arr[4] são inicializados com 0. Note que calloc é ligeiramente mais lenta que malloc devido à inicialização, mas oferece segurança extra.

realloc (redimensionar)

A função realloc (reallocation) redimensiona um bloco de memória previamente alocado por malloc, calloc ou realloc. Sua assinatura é:

void *realloc(void *ptr, size_t new_size);

Ela recebe um ponteiro para o bloco original (ptr) e o novo tamanho em bytes (new_size). Retorna um ponteiro para o bloco redimensionado (que pode ser o mesmo ou outro endereço), ou NULL se a realocação falhar. Se ptr for NULL, realloc se comporta como malloc. Se new_size for zero, o comportamento é definido pela implementação (geralmente libera a memória e retorna NULL).

Exemplo de redimensionamento:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *arr = (int*) malloc(3 * sizeof(int));
    if (arr == NULL) return 1;
    arr[0] = 10; arr[1] = 20; arr[2] = 30;
    
    // Redimensionar para 5 elementos
    int *temp = (int*) realloc(arr, 5 * sizeof(int));
    if (temp == NULL) {
        free(arr);
        return 1;
    }
    arr = temp;
    arr[3] = 40; arr[4] = 50;
    
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]);
    }
    free(arr);
    return 0;
}

Após realloc, os três primeiros elementos preservam seus valores. Os novos elementos (índices 3 e 4) têm valores indeterminados, pois realloc não inicializa a nova memória.

Cuidados com realloc

realloc é poderosa, mas requer atenção para evitar erros comuns:

  • Não use o ponteiro original após realocação: Se realloc mover o bloco, o ponteiro original se torna inválido. Sempre atribua o retorno a um ponteiro temporário e verifique se é NULL antes de sobrescrever o original.
  • Vazamento de memória se a realocação falhar: Se realloc retornar NULL, o bloco original permanece intacto. Se você sobrescrever o ponteiro original diretamente, perderá a referência para liberá-lo depois. Use um ponteiro temporário.
  • Comportamento com tamanho zero: Evite usar realloc com new_size = 0, pois pode liberar a memória ou retornar NULL. Prefira free explicitamente.
  • Alinhamento e fragmentação: Muitas realocações podem fragmentar a memória. Considere estratégias como alocar blocos maiores e gerenciar manualmente.

Exemplo de uso seguro:

int *arr = malloc(10 * sizeof(int));
if (!arr) { /* erro */ }
// ...
int *new_arr = realloc(arr, 20 * sizeof(int));
if (new_arr == NULL) {
    // arr ainda é válido, podemos continuar usando ou liberar
    free(arr);
    exit(1);
}
arr = new_arr; // agora arr aponta para o bloco redimensionado

Exemplos

Vamos ver um exemplo completo que combina calloc e realloc para gerenciar um array dinâmico de números lidos do usuário:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *numbers = NULL;
    int capacity = 0;
    int count = 0;
    int input;
    
    printf("Digite números (0 para sair):\n");
    while (1) {
        scanf("%d", &input);
        if (input == 0) break;
        
        if (count == capacity) {
            capacity = capacity == 0 ? 4 : capacity * 2;
            int *temp = realloc(numbers, capacity * sizeof(int));
            if (temp == NULL) {
                printf("Erro de alocação\n");
                free(numbers);
                return 1;
            }
            numbers = temp;
        }
        numbers[count++] = input;
    }
    
    printf("Números digitados: ");
    for (int i = 0; i < count; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");
    free(numbers);
    return 0;
}

Neste exemplo, usamos realloc para expandir o array conforme necessário. Inicialmente, numbers é NULL, e a primeira chamada a realloc age como malloc. Note que usamos um ponteiro temporário temp para evitar vazamento.

Boas Práticas

  • Sempre verifique o retorno de calloc e realloc.
  • Use calloc quando precisar de memória zerada, especialmente para estruturas que dependem de valores nulos.
  • Evite realocações frequentes; prefira dobrar a capacidade para amortizar o custo.
  • Libere a memória alocada com free quando não for mais necessária.

Referências

Exercícios

  1. Escreva um programa que aloca um array de 10 inteiros usando calloc, imprime os valores (que devem ser zero), e depois libera a memória.
  2. ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
        int *arr = (int*) calloc(10, sizeof(int));
        if (arr == NULL) {
            printf("Falha na alocação\n");
            return 1;
        }
        for (int i = 0; i < 10; i++) {
            printf("%d ", arr[i]);
        }
        printf("\n");
        free(arr);
        return 0;
    }
  3. Modifique o programa anterior para redimensionar o array para 20 elementos usando realloc, preencha os novos elementos com valores (por exemplo, 100 a 109) e imprima tudo.
  4. ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
        int *arr = (int*) calloc(10, sizeof(int));
        if (arr == NULL) return 1;
        // Preenche os 10 primeiros com 0 (já são) e depois redimensiona
        int *temp = (int*) realloc(arr, 20 * sizeof(int));
        if (temp == NULL) {
            free(arr);
            return 1;
        }
        arr = temp;
        for (int i = 10; i < 20; i++) {
            arr[i] = 100 + (i - 10);
        }
        for (int i = 0; i < 20; i++) {
            printf("%d ", arr[i]);
        }
        printf("\n");
        free(arr);
        return 0;
    }
  5. Crie uma função que recebe um ponteiro para um array de inteiros e seu tamanho, e dobra a capacidade do array usando realloc. A função deve retornar o novo tamanho ou -1 em caso de erro. Teste a função.
  6. ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    
    int expandir_array(int **arr, int *tamanho) {
        int novo_tamanho = (*tamanho) * 2;
        int *temp = (int*) realloc(*arr, novo_tamanho * sizeof(int));
        if (temp == NULL) {
            return -1;
        }
        *arr = temp;
        *tamanho = novo_tamanho;
        return novo_tamanho;
    }
    
    int main() {
        int *arr = (int*) malloc(5 * sizeof(int));
        if (arr == NULL) return 1;
        int tam = 5;
        for (int i = 0; i < tam; i++) arr[i] = i;
        
        int novo_tam = expandir_array(&arr, &tam);
        if (novo_tam == -1) {
            printf("Erro\n");
            free(arr);
            return 1;
        }
        printf("Novo tamanho: %d\n", novo_tam);
        for (int i = 0; i < novo_tam; i++) {
            printf("%d ", arr[i]);
        }
        printf("\n");
        free(arr);
        return 0;
    }
  7. Explique por que é importante usar um ponteiro temporário ao chamar realloc. Dê um exemplo de código que ilustra o problema.
  8. ✓ Resposta:

    Usar um ponteiro temporário evita vazamento de memória se realloc falhar. Se atribuirmos diretamente ao ponteiro original e a realocação falhar, o ponteiro original se torna NULL e perdemos a referência ao bloco original, que não pode ser liberado. Exemplo incorreto:

    int *arr = malloc(10 * sizeof(int));
    // ...
    arr = realloc(arr, 20 * sizeof(int)); // se falhar, arr = NULL e memória original vaza
    if (arr == NULL) { /* erro, mas memória original perdida */ }
  9. Escreva um programa que lê uma sequência de números do usuário (até o usuário digitar -1) e armazena em um array alocado dinamicamente. Use calloc para alocar o array inicial com capacidade 5 e realloc para expandir quando necessário. Imprima os números lidos.
  10. ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
        int capacidade = 5;
        int *arr = (int*) calloc(capacidade, sizeof(int));
        if (arr == NULL) return 1;
        int count = 0;
        int num;
        printf("Digite números (-1 para sair):\n");
        while (1) {
            scanf("%d", &num);
            if (num == -1) break;
            if (count == capacidade) {
                capacidade *= 2;
                int *temp = (int*) realloc(arr, capacidade * sizeof(int));
                if (temp == NULL) {
                    free(arr);
                    return 1;
                }
                arr = temp;
            }
            arr[count++] = num;
        }
        printf("Números lidos: ");
        for (int i = 0; i < count; i++) {
            printf("%d ", arr[i]);
        }
        printf("\n");
        free(arr);
        return 0;
    }