Nesta aula, vamos explorar duas funções fundamentais para alocação dinâmica de memória em C: calloc e realloc. Enquanto malloc aloca um bloco de memória sem inicializá-lo, calloc aloca e zera todos os bytes, sendo útil para estruturas que precisam de valores iniciais nulos. Já realloc permite redimensionar um bloco previamente alocado, preservando seu conteúdo até o novo tamanho. Dominar essas funções é essencial para gerenciar memória de forma eficiente e evitar erros comuns como vazamentos e acesso a memória inválida.

Vamos entender a sintaxe, o comportamento e as boas práticas de cada uma, com exemplos práticos e exercícios para fixação.

calloc (zerado)

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

void *calloc(size_t nmemb, size_t size);

Ela recebe dois argumentos: o número de elementos (nmemb) e o tamanho de cada elemento (size). A memória total alocada é nmemb * size bytes. Diferente de malloc, que não inicializa a memória, calloc garante que todos os bits sejam zero. Isso é útil para arrays de inteiros, estruturas com ponteiros que devem ser NULL, ou qualquer situação onde valores iniciais nulos são desejados.

Exemplo de uso com um array de inteiros:

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

int main() {
    int *arr;
    int n = 5;
    arr = (int *)calloc(n, sizeof(int));
    if (arr == NULL) {
        fprintf(stderr, "Erro ao alocar memória\n");
        return 1;
    }
    // Todos os elementos são 0
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    free(arr);
    return 0;
}

Saída: 0 0 0 0 0

Note que o retorno é convertido para o tipo apropriado (embora em C o casting de void* seja opcional, é uma boa prática para compatibilidade com C++). Sempre verifique se o retorno é NULL, indicando falha na alocação.

realloc (redimensionar)

A função realloc (reallocation) altera o tamanho de 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 desejado (new_size). O conteúdo do bloco é preservado até o mínimo entre o tamanho antigo e o novo. Se o novo tamanho for maior, os bytes extras não são inicializados (diferente de calloc). Se o novo tamanho for menor, os bytes excedentes são descartados.

Exemplo de redimensionamento de um array de inteiros de 5 para 10 elementos:

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

int main() {
    int *arr = (int *)malloc(5 * sizeof(int));
    if (arr == NULL) return 1;
    for (int i = 0; i < 5; i++) arr[i] = i + 1;

    // Redimensiona para 10 elementos
    int *temp = (int *)realloc(arr, 10 * sizeof(int));
    if (temp == NULL) {
        free(arr);
        return 1;
    }
    arr = temp; // atualiza o ponteiro

    // Os primeiros 5 elementos são preservados
    for (int i = 0; i < 10; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    free(arr);
    return 0;
}

Saída: 1 2 3 4 5 ? ? ? ? ? (valores indeterminados nos novos elementos).

Cuidados com realloc

O uso de realloc requer atenção para evitar vazamentos de memória e comportamento indefinido. Os principais cuidados são:

  • Sempre usar um ponteiro temporário: Se realloc falhar, ela retorna NULL e o bloco original permanece alocado. Se você atribuir diretamente ao ponteiro original (arr = realloc(arr, novo_tamanho)), perderá a referência ao bloco original em caso de falha, causando vazamento. Use um ponteiro temporário e verifique antes de atribuir.
  • Não usar realloc com ponteiro não alocado: Se ptr for NULL, realloc se comporta como malloc. Mas se ptr apontar para memória não alocada por malloc/calloc/realloc, o comportamento é indefinido.
  • Redimensionamento para zero: Se new_size for 0, o comportamento é definido pela implementação: pode liberar a memória e retornar NULL, ou retornar um ponteiro não nulo que não pode ser desreferenciado. Evite usar tamanho zero.
  • Desempenho: realloc pode mover o bloco para uma nova posição se não houver espaço contíguo suficiente. Isso envolve uma cópia do conteúdo, que pode ser custosa para blocos grandes. Em sistemas críticos, considere alocar com folga ou usar estruturas de dados que minimizem realocações.

Exemplo de uso seguro com verificação:

int *arr = (int *)malloc(5 * sizeof(int));
if (arr == NULL) { /* erro */ }
int *new_arr = (int *)realloc(arr, 10 * sizeof(int));
if (new_arr == NULL) {
    free(arr); // libera o original
    /* trata erro */
} else {
    arr = new_arr;
}

Exemplos

Vamos consolidar com exemplos mais completos.

Exemplo 1: Uso de calloc para matriz de estruturas

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

typedef struct {
    char nome[50];
    int idade;
} Pessoa;

int main() {
    int n = 3;
    Pessoa *p = (Pessoa *)calloc(n, sizeof(Pessoa));
    if (p == NULL) return 1;
    // Todos os campos estão zerados (nome = "", idade = 0)
    printf("Idade inicial: %d\n", p[0].idade); // 0
    free(p);
    return 0;
}

Exemplo 2: Redimensionamento dinâmico de array com realloc

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

int main() {
    int *arr = NULL;
    int capacidade = 2;
    int tamanho = 0;
    int valor;

    printf("Digite números (negativo para sair):\n");
    while (1) {
        scanf("%d", &valor);
        if (valor < 0) break;
        if (tamanho == capacidade) {
            capacidade *= 2;
            int *temp = (int *)realloc(arr, capacidade * sizeof(int));
            if (temp == NULL) {
                free(arr);
                return 1;
            }
            arr = temp;
        }
        arr[tamanho++] = valor;
    }

    printf("Array lido: ");
    for (int i = 0; i < tamanho; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    free(arr);
    return 0;
}

Exemplo 3: Combinando calloc e realloc

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

int main() {
    // Aloca array de 5 inteiros zerados
    int *arr = (int *)calloc(5, sizeof(int));
    if (arr == NULL) return 1;
    
    // Redimensiona para 10
    int *temp = (int *)realloc(arr, 10 * sizeof(int));
    if (temp == NULL) {
        free(arr);
        return 1;
    }
    arr = temp;
    // Os 5 primeiros são 0, os 5 últimos são lixo
    for (int i = 5; i < 10; i++) arr[i] = i * 10;
    
    for (int i = 0; i < 10; i++) printf("%d ", arr[i]);
    printf("\n");
    free(arr);
    return 0;
}

Boas Práticas

  • Prefira calloc quando precisar de memória zerada, especialmente para estruturas com ponteiros que devem ser NULL.
  • Sempre verifique o retorno das funções de alocação.
  • Use ponteiro temporário com realloc para evitar vazamentos.
  • Documente o código para deixar claro quem é responsável por liberar a memória.
  • Considere usar valgrind ou ferramentas similares para detectar vazamentos.

Referências

Exercícios

  1. Escreva um programa que aloca um array de 10 inteiros usando calloc, preenche os 5 primeiros com números ímpares e imprime todos.
  2. ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
        int *arr = (int *)calloc(10, sizeof(int));
        if (arr == NULL) return 1;
        for (int i = 0; i < 5; i++) arr[i] = 2*i + 1;
        for (int i = 0; i < 10; i++) printf("%d ", arr[i]);
        printf("\n");
        free(arr);
        return 0;
    }
  3. Implemente uma função que recebe um ponteiro para um array de inteiros e seu tamanho, e dobra a capacidade usando realloc com segurança.
  4. ✓ Resposta:
    int* dobrar_capacidade(int *arr, int *capacidade) {
        int nova_cap = (*capacidade) * 2;
        int *temp = (int *)realloc(arr, nova_cap * sizeof(int));
        if (temp == NULL) return NULL;
        *capacidade = nova_cap;
        return temp;
    }
  5. Explique por que o código abaixo é problemático e corrija-o:
    int *p = malloc(10 * sizeof(int));
    p = realloc(p, 20 * sizeof(int)); // se falhar, perde o bloco original
  6. ✓ Resposta:

    O problema é que se realloc falhar, retorna NULL e o bloco original é perdido (vazamento). Correção:

    int *p = malloc(10 * sizeof(int));
    int *temp = realloc(p, 20 * sizeof(int));
    if (temp != NULL) {
        p = temp;
    } else {
        free(p); // opcional, dependendo da estratégia
    }
  7. Escreva um programa que lê números do usuário até que seja digitado 0, armazenando-os em um array alocado dinamicamente com realloc (comece com capacidade 1 e dobre quando necessário).
  8. ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
        int *arr = NULL;
        int capacidade = 0;
        int tamanho = 0;
        int valor;
        
        while (1) {
            scanf("%d", &valor);
            if (valor == 0) break;
            if (tamanho == capacidade) {
                capacidade = (capacidade == 0) ? 1 : capacidade * 2;
                int *temp = realloc(arr, capacidade * sizeof(int));
                if (temp == NULL) {
                    free(arr);
                    return 1;
                }
                arr = temp;
            }
            arr[tamanho++] = valor;
        }
        for (int i = 0; i < tamanho; i++) printf("%d ", arr[i]);
        printf("\n");
        free(arr);
        return 0;
    }
  9. Qual a diferença entre calloc(5, sizeof(int)) e malloc(5 * sizeof(int)) seguido de memset para zerar? Há alguma vantagem de desempenho?
  10. ✓ Resposta:

    Ambos alocam 5 inteiros e zeram. calloc pode ser mais eficiente porque o kernel pode fornecer páginas já zeradas (como páginas de zero na demanda), enquanto malloc+memset sempre escreve explicitamente. Em muitos sistemas, calloc é otimizado para evitar a escrita dupla. Além disso, calloc é mais conciso e menos propenso a erros de tamanho.