Gerenciamento de Memória Dinâmica em C

O gerenciamento de memória dinâmica em C permite alocar e liberar memória durante a execução do programa, oferecendo maior flexibilidade em comparação com a alocação estática.

  1. Necessidade de Alocação Dinâmica

As formas tradicionais de alocação de memória em C incluem:


int valor = 20; // Aloca 4 bytes na pilha (stack)
char array[10] = {0}; // Aloca 10 bytes contíguos na pilha (stack)

Esses métodos possuem limitações:

  • O tamanho do espaço alocado é fixo.
  • O tamanho de um array deve ser definido em tempo de compilação e não pode ser alterado após a alocação.

Em muitas situações, o tamanho exato da memória necessária só é conhecido durante a execução do programa. Para atender a essa demanda, a linguagem C introduziu a alocação dinâmica de memória, permitindo que os programadores gerenciem a alocação e a liberação de espaço de forma mais flexível.

  1. Funções malloc e free

2.1 malloc

A função malloc (memory allocation) é utilizada para alocar um bloco de memória dinamicamente.


void* malloc (size_t size);
  • Se a alocação for bem-sucedida, malloc retorna um ponteiro para o início do bloco de memória alocado.
  • Caso contrário, retorna um ponteiro NULL. É crucial verificar o valor de retorno de malloc.
  • O tipo de retorno é void*, indicando que malloc não determina o tipo dos dados a serem armazenados; o programador é responsável por fazer o type cast apropriado.
  • Se o parâmetro size for 0, o comportamento de malloc é indefinido pelo padrão C.

Observação: A unidade para o parâmetro size é bytes. A função retorna o endereço inicial do espaço alocado em caso de sucesso e NULL em caso de falha. O cabeçalho necessário é <stdlib.h>.

2.2 Diferenças entre Memória Alocada por malloc e Arrays

  1. A quanitdade de memória alocada por malloc pode ser redimensionada, ao contrário dos arrays, cujo tamanho é fixo após a alocação.
  2. A memória alocada por malloc reside na heap, enquanto arrays declarados em tempo de compilação são alocados na stack.

2.3 free

A função free é utilizada para liberar a memória previamente alocada dinamicamente.


void free (void* ptr);
  • Se o ponteiro ptr não aponta para um bloco de memória alocado dinamicamente, o comportamento de free é indefinido.
  • Se ptr for NULL, a função free não faz nada.

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

int main() {
    int* ptr = NULL;
    ptr = (int*)malloc(40); // Aloca espaço para 10 inteiros

    if (ptr != NULL) {
        for (int i = 0; i < 10; i++) {
            *(ptr + i) = i * 2; // Preenche com valores
            printf("%d ", *(ptr + i));
        }
        printf("\n");
    } else {
        fprintf(stderr, "Falha na alocação de memória.\n");
    }

    free(ptr); // Libera a memória alocada
    ptr = NULL; // Evita ponteiro pendente (dangling pointer)

    return 0;
}

Observações:

  1. Após chamar free(p), o ponteiro p se torna um ponteiro pendente (dangling pointer). Embora a memória não pertença mais ao programa, o ponteiro ainda aponta para aquele endereço. Se a memória não for liberada explicitamente, o sistema operacional a recuperará quando o programa terminar.

  2. É uma boa prática usar malloc e free em pares para garantir que a memória seja corretamente gerenciada.

  3. Funções calloc e realloc


3.1 calloc

A função calloc (contiguous allocation) também é usada para alocação dinâmica de memória.


void* calloc (size_t num, size_t size);
  • Esta função aloca memória para num eelmentos, cada um com tamanho size bytes.
  • A principle diferença em relação a malloc é que calloc inicializa todos os bytes do bloco de memória alocado com zero antes de retornar o ponteiro.

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

int main() {
    int* ptr = NULL;
    // Aloca espaço para 10 inteiros e inicializa com 0
    ptr = (int*)calloc(10, sizeof(int)); 

    if (ptr == NULL) {
        perror("calloc"); // Exibe a mensagem de erro do sistema
        return 1;
    } else {
        // Verifica se a alocação foi bem-sucedida (boa prática)
        for (int i = 0; i < 10; i++) {
            printf("%d ", *(ptr + i)); // Imprime os valores (inicializados com 0)
        }
        printf("\n");
    }

    free(ptr);
    ptr = NULL;

    return 0;
}

Se for necessário que a memória alocada seja inicializada com zero, a função calloc oferece uma maneira conveniente de fazer isso.

3.2 realloc

A função realloc (re-allocation) adiciona flexibilidade ao gerenciamento de memória dinâmica, permitindo redimensionar blocos de memória previamente alocados.


void* realloc (void* ptr, size_t size);
  • ptr é o ponteiro para o bloco de memória a ser redimensionado.
  • size é o novo tamanho desejado para o bloco de memória.
  • A função retorna um ponteiro para o bloco de memória redimensionado.
  • realloc tenta redimensionar o bloco original. Se possível, mantém os dados existentes e anexa ou remove espaço conforme necessário. Caso contrário, aloca um novo bloco, copia os dados do bloco original para o novo e libera o bloco original.

Existem duas situações principais ao redimensionar a memória:

  • Situação 1: Há espaço contíguo suficiente após o bloco original. Nesse caso, o bloco original é expandido.
  • Situação 2: Não há espaço contíguo suficiente. realloc aloca um novo bloco maior, copia os dados do bloco antigo para o novo e libera o bloco antigo.

É crucial verificar o retorno de realloc, pois ele pode falhar e retornar NULL.


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

int main() {
    int* ptr = NULL;
    ptr = (int*)malloc(40); // Aloca 40 bytes (suficiente para 10 inteiros)

    if (ptr == NULL) {
        perror("malloc");
        return 1;
    } else {
        // Inicializa os primeiros 10 inteiros
        for (int i = 0; i < 10; i++) {
            *(ptr + i) = i + 1;
        }
    }

    // Tenta redimensionar para 80 bytes (suficiente para 20 inteiros)
    int* new_ptr = (int*)realloc(ptr, 80); 

    if (new_ptr != NULL) {
        ptr = new_ptr; // Atualiza o ponteiro se a realocação for bem-sucedida
    } else {
        perror("realloc");
        // Se realloc falhar, ptr ainda aponta para a memória original de 40 bytes
        // É importante liberar essa memória antes de sair ou continuar
        free(ptr); 
        return 1;
    }

    // Inicializa os novos 10 inteiros
    for (int i = 10; i < 20; i++) {
        *(ptr + i) = i + 1;
    }

    // Imprime todos os 20 inteiros
    for (int i = 0; i < 20; i++) {
        printf("%d ", *(ptr + i));
    }
    printf("\n");

    // Libera a memória
    free(ptr);
    ptr = NULL;

    return 0;
}

Recomendação: Sempre utilize uma variável auxiliar para o retorno de realloc. Se a realocação falhar, o ponteiro original permanece válido e a memória que ele aponta ainda precisa ser liberada. Se você atribuir diretamente o retorno de realloc ao ponteiro original e a realocação falhar, você perderá o ponteiro para a memória original, resultando em um vazamento de memória.

3.3 Observações sobre realloc

Em certas condições, realloc pode ser usado para alocar memória, de forma semelhante a malloc:


// Equivalente a malloc(20)
void* memory = realloc(NULL, 20); 

Neste caso, se ptr for NULL, realloc se comporta como malloc.

  1. Erros Comuns em Gerenciamento de Memória Dinâmica

Atenção: Os exemplos de código a seguir contêm erros e não devem ser imitados.

4.1 Desreferenciamento de Ponteiro Nulo

Tentar acessar a memória através de um ponteiro nulo causa a falha do programa.


void test_null_dereference() {
    int* p = (int*)malloc(sizeof(int));
    if (p == NULL) {
        fprintf(stderr, "Falha na alocação.\n");
        return; // Ou exit()
    }
    // Se a alocação falhar e p for NULL, a próxima linha causará crash
    *p = 20; 
    free(p);
}

É essencial verificar se a alocação de memória foi bem-sucedida antes de usá-la.

4.2 Acesso Fora dos Limites da Memória Alocada

Acessar posições de memória além do bloco alocado pode corromper dados ou causar falhas.


void test_out_of_bounds() {
    int* p = (int*)malloc(10 * sizeof(int)); // Aloca espaço para 10 inteiros
    if (p == NULL) {
        exit(EXIT_FAILURE);
    }
    for (int i = 0; i <= 10; i++) { // O loop vai até i=10, que está fora do limite
        *(p + i) = i; // Acesso a *(p + 10) é um acesso fora dos limites
    }
    free(p);
}

4.3 Liberar Memória Não Alocada Dinamicamente com free

Chamar free em um ponteiro que não aponta para memória alocada dinamicamente (e não liberada) resulta em comportamento indefinido.


void test_free_non_dynamic() {
    int a = 10;
    int* p = &a; // p aponta para uma variável na stack
    // free(p); // ERRO: Liberar memória da stack com free é incorreto
}

4.4 Liberar Apenas Parte de um Bloco de Memória Alocado

A função free deve ser chamada com o ponteiro que aponta para o *início* do bloco alocado.


int main() {
    int* p = (int*)malloc(40);
    if (p == NULL) return 1;

    int* temp_ptr = p; // Guarda o ponteiro original
    for (int i = 0; i < 5; i++) {
        // *(temp_ptr + i) = i; // Exemplo de uso
        temp_ptr++; // Avança o ponteiro
    }
    // free(temp_ptr); // ERRO: temp_ptr agora aponta para o meio do bloco alocado
    free(p); // Correto: liberar usando o ponteiro original
    p = NULL;
    return 0;
}

Para evitar esse erro, utilize um ponteiro auxiliar para percorrer a memória alocada, mantendo sempre o ponteiro original (que aponta para o início do bloco) para a chamada de free.

4.5 Liberar o Mesmo Bloco de Memória Múltiplas Vezes

Liberar um bloco de memória que já foi liberado causa comportamento indefinido e geralmente leva à falha do programa.


void test_double_free() {
    int* p = (int*)malloc(100);
    if (p == NULL) return;
    free(p);
    // free(p); // ERRO: Liberar a mesma memória duas vezes
}

4.6 Esquecer de Liberar Memória Alocada (Vazamento de Memória)

Se a memória alocada dinamicamente não for liberada após o uso, ela permanece alocada até o fim do programa, o que é conhecido como vazamento de memória (memory leak).


void leaky_function() {
    int* p = (int*)malloc(100);
    if (p != NULL) {
        *p = 20;
        // A memória alocada por malloc(100) não é liberada aqui
    }
}

int main() {
    leaky_function();
    // O programa continua, mas a memória alocada em leaky_function é perdida
    while (1); 
    return 0;
}

É fundamental liberar toda a memória alocada dinamicamente que não é mais necessária.

  1. Arrays Flexíveis (Estruturas com Array de Tamanho Zero)

Uma estrutura em C pode conter um membro de array de tamanho zero, conhecido como array flexível. Isso permite que a estrutura seja alocada dinamicamente com um tamanho variável.


struct ExemploFlexivel {
    int id;
    int dados[0]; // Array flexível
};

Alguns compiladores podem gerar um aviso para int dados\[0\]. Nesses casos, pode-se usar int dados\[\].


struct ExemploFlexivelAlternativo {
    int id;
    int dados[]; // Array flexível
};

5.1 Características de Arrays Flexíveis

  • O membro do array flexível deve ser o último membro da estrutura.
  • A função sizeof retornará o tamanho da estrutura sem incluir a memória do array flexível.
  • Ao alocar memória para uma estrutura com array flexível usando malloc, o tamanho alocado deve ser maior que o tamanho da estrutura para acomodar os elementos do array flexível.

5.2 Uso de Arrays Flexíveis


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

typedef struct {
    int id;
    int elementos[]; // Array flexível
} EstruturaComArrayFlexivel;

int main() {
    int num_elementos = 100;
    // Aloca memória para a estrutura + 100 inteiros
    EstruturaComArrayFlexivel* ptr = (EstruturaComArrayFlexivel*)malloc(sizeof(EstruturaComArrayFlexivel) + num_elementos * sizeof(int));

    if (ptr == NULL) {
        perror("malloc");
        return 1;
    }

    // Inicializa os membros da estrutura
    ptr->id = 100; 
    for (int i = 0; i < num_elementos; i++) {
        ptr->elementos[i] = i; // Acessa o array flexível
    }

    // Usa os dados...

    free(ptr); // Libera toda a memória alocada (estrutura + array flexível)
    return 0;
}

5.3 Vantagens de Arrays Flexíveis

  1. Facilidade na Liberação de Memória: Ao alocar a estrutura e o array flexível como um único bloco, uma única chamada a free libera toda a memória associada. Isso simplifica o gerenciamento para o usuário da estrutura.

  2. Melhor Desempenho de Acesso: Memória contígua pode levar a um acesso mais rápido aos dados e reduzir a fragmentação da memória.

  3. Áreas de Alocação de Memória em Programas C/C++


Os programas C/C++ utilizam diferentes áreas de memória:

  • Stack (Pilha): Usada para variáveis locais, parâmetros de funções e endereços de retorno. A alocação e desalocação são automáticas e eficientes, mas o espaço é limitado.
  • Heap (Monte): Usado para alocação dinâmica de memória (malloc, calloc, realloc). A alocação e liberação são gerenciadas pelo programador.
  • Static/Data Segment (Segmento Estático/Dados): Usado para variáveis globais, variáveis static e literais de string. A memória é alocada em tempo de compilação e liberada pelo sistema operacional ao final do programa.
  • Code Segment (Segmento de Código): Contém o código binário executável do programa.

Tags: C Alocação Dinâmica malloc free calloc

Publicado em 6-12 03:52 por Thomas