Gerenciar memória manualmente em C é uma das maiores responsabilidades do programador. Erros como vazamentos, liberações duplicadas e acesso após liberação podem causar falhas, comportamento indefinido e vulnerabilidades de segurança. Nesta aula, vamos explorar cada um desses problemas, suas causas e consequências, e aprender a usar o Valgrind para identificá-los.

Dominar esses conceitos é crucial para escrever código robusto e eficiente. Vamos começar com o memory leak, o mais comum dos erros.

Memory leaks

Um memory leak ocorre quando a memória alocada dinamicamente não é liberada após o uso, fazendo com que o programa consuma cada vez mais memória. Com o tempo, isso pode levar à exaustão de memória e queda do programa. Em C, isso acontece quando o ponteiro que aponta para a memória alocada é perdido ou sobrescrito sem chamar free().

Exemplo clássico: alocar memória e retornar sem liberar, ou alocar em um loop sem liberar entre as iterações.

#include <stdlib.h>

void func() {
    int *p = malloc(sizeof(int) * 100);
    // esqueceu de free(p);
}

int main() {
    func();
    // p não existe mais, mas a memória continua alocada
    return 0;
}

Para evitar leaks, sempre pareie cada malloc/calloc/realloc com um free correspondente. Use ferramentas como Valgrind para detectar leaks.

Double free

Double free ocorre quando a função free() é chamada duas vezes para o mesmo bloco de memória. Isso corrompe as estruturas internas do gerenciador de memória, podendo causar falhas ou comportamento imprevisível. O C não protege contra isso; é responsabilidade do programador.

Exemplo:

#include <stdlib.h>

int main() {
    int *p = malloc(sizeof(int));
    free(p);
    free(p); // ERRO: double free
    return 0;
}

Para evitar, após liberar um ponteiro, atribua NULL a ele. Assim, chamar free(NULL) é seguro (não faz nada).

free(p);
p = NULL;
// se free(p) for chamado novamente, é seguro

Use-after-free

Use-after-free acontece quando acessamos um ponteiro que já foi liberado. O endereço pode ainda conter dados, mas eles podem ser sobrescritos ou realocados. Isso é um comportamento indefinido e pode ser explorado por atacantes.

Exemplo:

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

int main() {
    int *p = malloc(sizeof(int));
    *p = 42;
    free(p);
    printf("%d\n", *p); // ERRO: use-after-free
    return 0;
}

Para evitar, nunca use um ponteiro após liberá-lo. Atribuir NULL após free ajuda a detectar o erro, pois acessar NULL causa uma falha imediata (segmentation fault), que é mais fácil de depurar.

Valgrind (visão geral)

Valgrind é uma ferramenta de instrumentação que ajuda a detectar erros de memória. Ele executa o programa em um ambiente controlado e monitora todas as alocações e liberações. O Valgrind pode detectar memory leaks, double free, use-after-free, acesso a memória não inicializada, entre outros.

Para usar, compile o programa com informações de depuração (-g) e execute:

gcc -g -o meu_prog meu_prog.c
valgrind --leak-check=full ./meu_prog

A saída mostrará erros como:

==12345== Invalid write of size 4
==12345==    at 0x...: main (exemplo.c:5)
==12345==  Address 0x... is 0 bytes inside a block of size 4 free'd
==12345==    by 0x...: free (exemplo.c:7)

Para memory leaks, Valgrind exibe um resumo de blocos alocados e não liberados. É uma ferramenta indispensável no desenvolvimento de programas em C.

Boas práticas e observações finais

Sempre inicialize ponteiros com NULL, libere memória no mesmo nível de abstração em que foi alocada, e use ferramentas automáticas como Valgrind. Em projetos grandes, considere usar um padrão de alocação consistente e revisões de código focadas em gerenciamento de memória.

Referências

Exercícios

  1. Escreva um programa que aloca memória para um array de 10 inteiros, preenche com valores de 0 a 9 e depois libera a memória. Em seguida, tente imprimir o primeiro elemento. O que acontece? Corrija o código.

    ✓ Resposta: O código original causa use-after-free. A correção é não acessar o ponteiro após free. Exemplo corrigido:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
        int *arr = malloc(10 * sizeof(int));
        if (arr == NULL) return 1;
        for (int i = 0; i < 10; i++) arr[i] = i;
        free(arr);
        // arr = NULL; // boa prática
        // não acessar arr agora
        return 0;
    }
    
  2. O que é um memory leak? Dê um exemplo e explique como Valgrind pode detectá-lo.

    ✓ Resposta: Memory leak é quando memória alocada não é liberada. Exemplo:
    void leak() {
        int *p = malloc(100);
        // sem free
    }
    
    Valgrind com --leak-check=full mostra blocos definitivamente perdidos.
  3. Explique o que é double free e como evitá-lo.

    ✓ Resposta: Double free é chamar free duas vezes no mesmo ponteiro. Evita-se atribuindo NULL ao ponteiro após free, pois free(NULL) é seguro.
  4. Considere o código: int *p = malloc(sizeof(int)); free(p); *p = 10; Que tipo de erro é esse? Qual a consequência?

    ✓ Resposta: Use-after-free. A consequência é comportamento indefinido: pode corromper dados, causar falha ou ser explorado.
  5. Como você usaria Valgrind para verificar um programa? Escreva o comando e explique o que a opção --leak-check=full faz.

    ✓ Resposta: Comando: valgrind --leak-check=full ./programa. A opção --leak-check=full ativa a detecção detalhada de memory leaks, mostrando exatamente onde a memória foi alocada e não liberada.