Pular para conteúdo

Semana 13 — Alocação dinâmica de memória (parte 2)

Nesta semana

Unidade 3 · Seg 02/11 · Qua 04/11 · Sex 06/11 · ⚠️ feriado: 02/11 (Finados)

Na Semana 12 você aprendeu a pedir memória em tempo de execução com malloc e a devolvê-la com free, criando vetores cujo tamanho só é conhecido quando o programa roda. Esta semana damos o passo seguinte: redimensionar um bloco já alocado com realloc, construir matrizes dinâmicas com ponteiro-para-ponteiro (int **) e — o mais importante de tudo — aprender a gerenciar a memória sem deixar rastros: sem vazamentos, sem ponteiros pendentes, sem liberações duplas.

Como a segunda-feira (02/11) é feriado de Finados, o estudo do conteúdo desta semana pode ser feito com calma ao longo dos primeiros dias; chegue ao encontro de quarta tendo lido e executado os exemplos. Este é um tópico difícil — talvez o mais sutil da disciplina — mas é exatamente aqui que você deixa de "usar" a memória e passa a controlá-la. Vá devagar, desenhe os ponteiros no papel e execute cada trecho.

🎯 Objetivos de aprendizagem

Ao final desta semana, você deverá ser capaz de:

  • usar realloc para crescer ou encolher um bloco alocado preservando o conteúdo, e aplicar o padrão seguro com ponteiro temporário;
  • alocar, usar e liberar corretamente uma matriz dinâmica linhas x colunas usando int ** (um vetor de ponteiros para as linhas);
  • liberar memória na ordem inversa da alocação (primeiro as linhas, depois o vetor de ponteiros) e explicar por quê;
  • explicar o que é um vazamento de memória (memory leak), por que ele é um problema e como evitá-lo com a regra "todo malloc tem seu free";
  • reconhecer e evitar os erros clássicos de ponteiros: uso após free (ponteiro pendente), free duplo, esquecer de checar NULL, ponteiro não inicializado e perda de referência;
  • adotar um conjunto de boas práticas que tornam o gerenciamento de memória seguro e previsível.

📖 Segunda — Estudo do conteúdo

0. Retomando a Semana 12 em 90 segundos

Antes de avançar, relembre o essencial da alocação dinâmica:

  • malloc(n) pede ao sistema n bytes de memória e devolve um ponteiro para o início do bloco — ou NULL se não houver memória disponível.
  • Usamos sizeof para calcular quantos bytes queremos, e sempre verificamos o retorno.
  • free(p) devolve ao sistema o bloco que p aponta. Todo malloc precisa de um free.
int *v = malloc(5 * sizeof(int));   // pede espaço para 5 inteiros
if (v == NULL) {                    // sempre verifique!
    printf("Sem memoria!\n");
    return 1;
}
// ... usa v[0], v[1], ..., v[4] como um vetor normal ...
free(v);                            // devolve a memoria ao sistema

Tudo isso mora em <stdlib.h>. Guarde a imagem central: malloc reserva, free devolve. Esta semana é sobre o que fazer entre os dois — e sobre nunca esquecer o segundo.

A memória dinâmica vive no heap

As variáveis locais que você declara dentro de funções vivem na pilha (stack), vista na Semana 04, e somem sozinhas quando a função termina. Já a memória de malloc vive numa região chamada heap (monte) e não some sozinha: ela fica reservada até você chamar free. É justamente por isso que gerenciá-la é responsabilidade sua — e o tema desta semana.

1. realloc: redimensionar um bloco já alocado

Muitas vezes você aloca um vetor, começa a preenchê-lo e descobre que precisa de mais espaço — ou de menos. Alocar um bloco novo, copiar tudo à mão e liberar o antigo seria trabalhoso e fácil de errar. Para isso existe o realloc ("re-allocate", realocar):

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

O realloc redimensiona o bloco apontado por ptr para novo_tamanho bytes, preservando o conteúdo que couber no novo tamanho:

  • se o bloco cresce, os dados antigos são mantidos e o espaço extra fica sem inicializar (contém lixo, como qualquer memória nova);
  • se o bloco encolhe, os dados até o novo tamanho são mantidos e o excedente é descartado;
  • devolve um ponteiro para o bloco redimensionado — que pode ser diferente do original!

Esse último ponto é a grande sutileza. Para crescer, o sistema pode não ter espaço logo após o bloco atual; então ele aloca um bloco novo em outro lugar, copia os dados para lá e libera o antigo. Ou seja, o endereço pode mudar. E, como qualquer alocação, realloc pode falhar e devolver NULL.

O erro fatal do realloc (nunca faça assim)

v = realloc(v, novo_tam);   // PERIGOSO!
Se o realloc falhar, ele devolve NULL sem liberar o bloco antigo. Ao escrever v = realloc(v, ...), você sobrescreveu v com NULL e perdeu para sempre o endereço do bloco antigo — que continua reservado. Isso é, ao mesmo tempo, um vazamento de memória e a perda dos seus dados. Nunca atribua o resultado do realloc de volta à mesma variável diretamente.

1.1 O padrão seguro do realloc

A forma correta usa um ponteiro temporário. Só substituímos o ponteiro original se o realloc deu certo:

int *tmp = realloc(v, novo_tam * sizeof(int));
if (tmp != NULL) {
    v = tmp;                 // deu certo: agora v aponta para o bloco (talvez novo)
} else {
    // deu errado: v AINDA aponta para o bloco antigo, valido, intacto.
    printf("Falha ao redimensionar; mantendo o vetor atual.\n");
}

Assim, se a realocação falhar, v continua válido apontando para os dados originais, e você pode tratar o erro com calma (avisar o usuário, salvar o que já tem, encerrar de forma limpa) em vez de perder tudo.

Curiosidade 💡 — os dois casos especiais do realloc

O realloc é um "canivete suíço" da alocação:

  • realloc(NULL, n) comporta-se exatamente como malloc(n) — aloca um bloco novo.
  • realloc(p, 0) libera o bloco (comportamento parecido com free), embora o retorno nesse caso seja definido pela implementação; prefira sempre o free explícito.

Isso permite escrever um laço que "cresce" um vetor começando de NULL, sem tratar o primeiro caso de forma diferente — padrão que você usará no estudo dirigido de sexta.

1.2 Um exemplo completo

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

int main(void) {
    int capacidade = 2;
    int *v = malloc(capacidade * sizeof(int));
    if (v == NULL) { return 1; }

    v[0] = 10;
    v[1] = 20;

    // Precisamos de espaco para 4 inteiros: crescemos o bloco.
    int novo_tam = 4;
    int *tmp = realloc(v, novo_tam * sizeof(int));
    if (tmp == NULL) {
        printf("Sem memoria para crescer.\n");
        free(v);            // v antigo ainda e valido; liberamos antes de sair
        return 1;
    }
    v = tmp;                // padrao seguro: so agora atualizamos v
    capacidade = novo_tam;

    v[2] = 30;              // as duas novas posicoes estavam com lixo; preenchemos
    v[3] = 40;

    for (int i = 0; i < capacidade; i++) {
        printf("v[%d] = %d\n", i, v[i]);   // 10 20 30 40 (os antigos foram preservados)
    }

    free(v);
    return 0;
}

Curiosidade 💡 — realloc pode ser caro

Quando o realloc precisa mover o bloco, ele copia todos os bytes para o novo local. Se você cresce um vetor de milhões de elementos de um em um, pode acabar copiando o conteúdo inteiro repetidas vezes — muito lento. A estratégia usual é crescer em saltos (por exemplo, dobrando a capacidade quando ela se esgota), o que dilui o custo das cópias. É exatamente assim que estruturas de "vetor que cresce" (as dynamic arrays de várias linguagens) funcionam por dentro.

2. Matrizes dinâmicas com ponteiro-para-ponteiro (int **)

Um vetor dinâmico é um bloco linear de memória. E uma matriz (tabela com linhas e colunas), quando as dimensões só são conhecidas em tempo de execução? A técnica clássica em C é usar um ponteiro-para-ponteiro, int **.

A ideia tem dois níveis:

  1. um vetor de ponteiros (o int **): cada posição guarda o endereço de uma linha;
  2. para cada linha, um vetor de int com o número de colunas.

Visualmente, é um vetor que aponta para vários vetores:

flowchart LR
    subgraph nivel1["Vetor de ponteiros (int **m)"]
    L0["m[0]"]
    L1["m[1]"]
    L2["m[2]"]
    end
    subgraph nivel2["As linhas (cada uma um int* de 'colunas' elementos)"]
    R0["[ m[0][0] | m[0][1] | m[0][2] | m[0][3] ]"]
    R1["[ m[1][0] | m[1][1] | m[1][2] | m[1][3] ]"]
    R2["[ m[2][0] | m[2][1] | m[2][2] | m[2][3] ]"]
    end
    L0 --> R0
    L1 --> R1
    L2 --> R2

Repare que, para uma matriz 3 x 4, fazemos 1 alocação para o vetor de ponteiros e 3 alocações (uma por linha) — 4 alocações no total. Guarde esse número: teremos que fazer 4 liberações também.

2.1 Alocando a matriz

int linhas = 3, colunas = 4;

// 1) Aloca o vetor de ponteiros (as "linhas"):
int **m = malloc(linhas * sizeof(int *));   // atencao: sizeof(int *)!
if (m == NULL) { /* trata erro */ }

// 2) Para cada linha, aloca um vetor de 'colunas' inteiros:
for (int i = 0; i < linhas; i++) {
    m[i] = malloc(colunas * sizeof(int));   // agora sizeof(int)
    if (m[i] == NULL) { /* trata erro */ }
}

Duas coisas merecem sua atenção:

  • No passo 1, o sizeof é de int * (o tamanho de um ponteiro), porque cada posição de m guarda um endereço, não um inteiro.
  • No passo 2, o sizeof é de int, porque agora cada posição da linha guarda um inteiro de verdade.

Depois disso, você usa m[i][j] exatamente como usaria uma matriz estática — a sintaxe de colchetes duplos funciona igualzinho.

2.2 Programa completo: alocar, usar e liberar

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

int main(void) {
    int linhas = 3, colunas = 4;

    // --- ALOCACAO ---
    int **m = malloc(linhas * sizeof(int *));   // vetor de ponteiros
    if (m == NULL) {
        printf("Sem memoria.\n");
        return 1;
    }
    for (int i = 0; i < linhas; i++) {
        m[i] = malloc(colunas * sizeof(int));   // cada linha
        if (m[i] == NULL) {
            printf("Sem memoria na linha %d.\n", i);
            // libera o que ja foi alocado antes de sair (linhas 0..i-1):
            for (int k = 0; k < i; k++) {
                free(m[k]);
            }
            free(m);
            return 1;
        }
    }

    // --- USO: preenche m[i][j] = i * colunas + j ---
    for (int i = 0; i < linhas; i++) {
        for (int j = 0; j < colunas; j++) {
            m[i][j] = i * colunas + j;
        }
    }

    // --- USO: imprime a matriz ---
    for (int i = 0; i < linhas; i++) {
        for (int j = 0; j < colunas; j++) {
            printf("%4d", m[i][j]);
        }
        printf("\n");
    }

    // --- LIBERACAO (ordem inversa da alocacao) ---
    for (int i = 0; i < linhas; i++) {
        free(m[i]);   // 1o: libera cada linha
    }
    free(m);          // 2o: libera o vetor de ponteiros

    return 0;
}

Por que checar NULL também dentro do laço?

No exemplo acima, se a alocação de uma linha falhar no meio, precisamos liberar as linhas já alocadas e o vetor de ponteiros antes de sair — senão elas vazam. Em programas didáticos pequenos você pode simplificar, mas é importante ver o padrão correto: o tratamento de erro também precisa limpar a bagunça parcial.

3. Liberar na ORDEM CORRETA (a inversa da alocação)

Este ponto é tão importante que merece seção própria. Ao liberar a matriz, a ordem é obrigatoriamente:

  1. primeiro, free de cada linha (m[0], m[1], ...);
  2. depois, free do vetor de ponteiros (m).
flowchart LR
    A["free(m[0])"] --> B["free(m[1])"] --> C["free(m[2])"] --> D["free(m)"]

Nunca libere m antes das linhas!

Se você fizer free(m) primeiro, o vetor de ponteiros deixa de existir — e com ele os endereços m[0], m[1], ... Você perdeu os endereços das linhas e não tem mais como liberá-las: elas vazam. Pior: tentar acessar m[i] depois de free(m) é uso após free (veja a seção 5). A regra é simples e mecânica: libere na ordem inversa da que você alocou. Você alocou o vetor de ponteiros primeiro e as linhas depois; então libere as linhas primeiro e o vetor de ponteiros por último.

Pense na matriz como uma árvore: m é o tronco, m[i] são os galhos. Para desmontar sem perder nada, corte os galhos antes do tronco.

4. Vazamentos de memória (memory leaks)

Um vazamento de memória acontece quando você aloca um bloco e nunca o libera — e, além disso, perde o ponteiro que apontava para ele. A memória fica reservada, mas inacessível: o programa não pode mais usá-la nem devolvê-la.

vazamento.c
void faz_algo(void) {
    int *v = malloc(1000 * sizeof(int));
    // ... usa v ...
    // ESQUECEU o free(v)!
}   // ao sair da funcao, a variavel local 'v' morre.
    // O BLOCO de 1000 inteiros continua reservado, mas ninguem mais tem seu endereco.

Quando faz_algo termina, a variável local v (que estava na pilha) desaparece — mas o bloco de 1000 inteiros continua ocupando o heap, agora sem nenhum ponteiro que chegue até ele. Vazou.

Por que isso é um problema? Um vazamento pontual, num programa que roda por 1 segundo, raramente causa dano visível — o sistema operacional recupera toda a memória do processo quando ele termina. O problema aparece em programas que rodam por muito tempo (um servidor, um jogo, um serviço) e vazam repetidamente, por exemplo dentro de um laço ou a cada requisição: o consumo de memória cresce sem parar até o programa ficar lento, travar ou ser encerrado pelo sistema. É o clássico "por que meu programa está comendo 4 GB de RAM depois de 3 horas?".

Como evitar? A regra de ouro, que você já viu na Semana 12, é:

A regra de ouro do gerenciamento de memória

Todo malloc (ou calloc, ou realloc) tem, em algum lugar, o seu free correspondente.

Ao escrever um malloc, pergunte-se imediatamente: "quem, e quando, vai liberar isto?". Se você não sabe responder, ainda não terminou de projetar o código.

Curiosidade 💡 — a Valgrind, sua detetive de memória

Como saber se o seu programa vaza? No Linux, a ferramenta Valgrind executa seu programa e rastreia cada alocação e liberação. Ao final, ela relata quantos bytes ficaram "definitely lost" (vazaram), aponta em qual linha foram alocados e ainda detecta usos inválidos (ler além do bloco, usar memória já liberada, etc.). Roda-se assim:

gcc -g -Wall programa.c -o programa
valgrind --leak-check=full ./programa
A linha mágica que você quer ver é: "All heap blocks were freed -- no leaks are possible". Vale ligar a Valgrind nos seus exercícios desta semana — ela transforma um tópico abstrato em algo concreto e verificável.

5. Erros clássicos de ponteiros e alocação

Esta é a "galeria de armadilhas" da semana. Reconhecê-las é metade do caminho para evitá-las.

5.1 Uso após free (ponteiro pendente / dangling pointer)

Depois de free(p), o bloco não é mais seu — mas a variável p ainda contém o endereço antigo. Um ponteiro nessa situação é chamado de ponteiro pendente (dangling pointer). Usá-lo é erro grave:

int *p = malloc(sizeof(int));
*p = 42;
free(p);           // devolvemos o bloco ao sistema
printf("%d\n", *p);  // ERRO: uso apos free! p ainda aponta para memoria que nao e mais nossa

Curiosidade 💡 — por que 'usar após free' é tão perigoso

O pior de tudo é que muitas vezes o programa não trava na hora — o valor antigo ainda pode estar lá "por sorte", e o programa parece funcionar. Mas, assim que o sistema reaproveitar aquele bloco para uma outra alocação, *p passará a ler (ou pior, escrever) sobre dados que agora pertencem a outra parte do programa. O resultado são bugs intermitentes e não reproduzíveis — os mais difíceis de caçar que existem. Em software real, esse tipo de falha (use-after-free) é, inclusive, uma das principais portas de entrada para falhas de segurança.

A defesa: logo após free(p), faça p = NULL;. Um ponteiro NULL usado indevidamente faz o programa falhar na hora (de forma óbvia), em vez de silenciosamente corromper dados.

5.2 free duplo (double free)

Liberar duas vezes o mesmo bloco é comportamento indefinido — costuma corromper as estruturas internas do gerenciador de memória e travar o programa:

free(p);
free(p);   // ERRO: free duplo!

A boa notícia: a mesma defesa da seção anterior resolve. Como free(NULL) é seguro (não faz absolutamente nada), atribuir p = NULL após o primeiro free neutraliza um eventual segundo free:

free(p);
p = NULL;
free(p);   // agora e inofensivo: free(NULL) nao faz nada

5.3 Esquecer de verificar NULL

malloc e realloc podem falhar e devolver NULL. Se você usar o ponteiro sem checar, qualquer acesso p[0] será uma desreferência de NULL — falha imediata (o famoso segmentation fault):

int *v = malloc(n * sizeof(int));
v[0] = 1;   // se malloc falhou, v == NULL -> falha ao acessar v[0]

Sempre: alocou, cheque.

5.4 Usar ponteiro não inicializado

Um ponteiro declarado mas não inicializado contém lixo — um endereço qualquer. Usá-lo é escrever/ler num lugar aleatório da memória:

int *p;      // p contem LIXO (um endereco imprevisivel)
*p = 10;     // ERRO: escreve num lugar desconhecido -> comportamento indefinido

A defesa é inicializar sempre, nem que seja com NULL:

int *p = NULL;   // agora p tem um valor conhecido e seguro

5.5 Perder a referência (sobrescrever o ponteiro antes de liberar)

Se você reatribui um ponteiro sem ter liberado o bloco anterior, perde o endereço do bloco antigo — que vaza:

int *p = malloc(100 * sizeof(int));
p = malloc(200 * sizeof(int));   // ERRO: o primeiro bloco (100 ints) vazou!

O correto é liberar antes de reapontar (free(p); e só então o novo malloc) — ou, no caso de redimensionar, usar realloc com o padrão seguro da seção 1.

6. Boas práticas (o resumo que salva você)

Reunindo tudo, um punhado de hábitos elimina a grande maioria dos bugs de memória:

Prática Por quê
Inicialize ponteiros (int *p = NULL;) Evita usar lixo; NULL é um valor seguro e detectável.
Verifique o retorno de malloc/realloc Eles podem falhar; usar NULL trava o programa.
Todo mallocfree Sem par, é vazamento certo. Planeje o free ao escrever o malloc.
p = NULL após free Neutraliza uso-após-free e free duplo de uma só vez.
Padrão seguro do realloc (via tmp) Preserva os dados se a realocação falhar.
Libere na ordem inversa da alocação Em estruturas aninhadas (matrizes), evita perder referências.

Um esqueleto mental para qualquer código com memória dinâmica

tipo *p = NULL;                 // 1. inicializa
p = malloc(n * sizeof(tipo));   // 2. aloca
if (p == NULL) { /* trata */ }  // 3. verifica
// 4. ... usa p ...
free(p);                        // 5. libera
p = NULL;                       // 6. anula
Decore este ciclo de seis passos. Ele cabe em qualquer situação — de um vetor simples à linha de uma matriz.

7. Erros comuns desta semana (resumão)

Checklist de armadilhas

  • v = realloc(v, ...) direto (sem tmp) — vaza e perde os dados se falhar.
  • sizeof(int) onde deveria ser sizeof(int *) ao alocar o vetor de ponteiros.
  • Liberar m antes das linhas — as linhas vazam.
  • Esquecer o freevazamento de memória.
  • Usar p depois de free(p)ponteiro pendente.
  • free(p) duas vezes — corrompe o heap (defesa: p = NULL após o free).
  • Não checar NULL após malloc/realloc.
  • Usar ponteiro não inicializado (contém lixo).
  • Reatribuir um ponteiro sem liberar o bloco anterior — perda de referência.

👥 Quarta — Encontro

Chegue ao encontro tendo lido o material acima e tendo executado ao menos os exemplos crescer_vetor.c e matriz_dinamica.c. Como a segunda foi feriado, capriche na preparação nos dias anteriores. Roteiro previsto:

  1. Revisão relâmpago: realloc e o padrão seguro; a estrutura de uma matriz int **; a ordem de liberação.
  2. Desenho ao vivo: vamos desenhar, no quadro, o vetor de ponteiros e as linhas, e acompanhar o que acontece na memória a cada malloc e a cada free.
  3. Galeria de erros com a Valgrind: provocamos de propósito um vazamento e um uso-após-free, e usamos a Valgrind para ver o problema aparecer no relatório.

Problema-guia do encontro — vetor dinâmico que cresce sob demanda: ler inteiros do usuário até que ele digite 0 (sentinela), guardando todos em um vetor que cresce com realloc conforme necessário, e ao final imprimir os valores e a soma. Este é o esqueleto do "vetor que cresce" que aparece por toda parte na programação.

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

int main(void) {
    int *v = NULL;          // comeca vazio (realloc(NULL, ...) == malloc)
    int tamanho = 0;        // quantos elementos ja guardamos
    int capacidade = 0;     // quantos cabem no bloco atual

    int x;
    printf("Digite inteiros (0 para terminar):\n");
    while (scanf("%d", &x) == 1 && x != 0) {
        if (tamanho == capacidade) {              // encheu? precisa crescer
            int nova_cap = (capacidade == 0) ? 1 : capacidade * 2;  // dobra
            int *tmp = realloc(v, nova_cap * sizeof(int));
            if (tmp == NULL) {                    // padrao seguro
                printf("Sem memoria; parando a leitura.\n");
                break;
            }
            v = tmp;
            capacidade = nova_cap;
        }
        v[tamanho] = x;
        tamanho++;
    }

    long soma = 0;
    printf("Valores lidos (%d): ", tamanho);
    for (int i = 0; i < tamanho; i++) {
        printf("%d ", v[i]);
        soma += v[i];
    }
    printf("\nSoma = %ld\n", soma);

    free(v);                // um unico free para todo o vetor (mesmo tendo crescido varias vezes)
    return 0;
}

Perguntas para pensar antes do encontro: por que começar com v = NULL e capacidade = 0 funciona sem um caso especial para o primeiro elemento? Por que dobrar a capacidade, em vez de crescer de um em um? Quantas vezes o realloc é chamado ao ler 100 números?


✍️ Sexta — Estudo dirigido

Este é um tema pouco 'testado' no Beecrowd

Alocação dinâmica é difícil de avaliar em juízes online automáticos (que olham só a saída e não enxergam vazamentos). Por isso, esta semana o foco principal são os desafios do zero — onde você constrói e, sobretudo, libera a memória corretamente. Ainda assim, vale revisitar problemas de vetores e matrizes resolvendo-os com memória dinâmica e conferindo com a Valgrind.

Exercícios guiados (Beecrowd)

Reforço opcional: refaça, agora com malloc/realloc/free, problemas de vetores/matrizes já conhecidos.

Problema O que exercita
1180 Menor e Posição vetor dinâmico do tamanho lido
1181 Linha na Matriz leitura por linha (base para int **)
1182 Coluna na Matriz percorrer colunas de uma matriz dinâmica
1435 Square Matrix I montar/imprimir matriz n x n dinâmica

Para todos: aloque com base no tamanho lido, use e libere tudo ao final. Rode com valgrind --leak-check=full.

Desafios para escrever do zero

  1. Vetor que cresce com realloc: parta do vetor_sob_demanda.c do encontro. Leia valores até uma sentinela (por exemplo, um número negativo), guardando todos com crescimento por realloc. Ao final, imprima o maior, o menor e a média.
  2. Matriz dinâmica N x M do usuário: leia N e M, aloque uma matriz int **, leia os N * M valores, imprima a soma de cada linha e a soma de cada coluna, e libere tudo na ordem correta.
  3. Função que dobra um vetor: escreva int *dobra_capacidade(int *v, int *capacidade) que duplica o tamanho do bloco apontado por v (usando o padrão seguro do realloc), atualiza *capacidade e devolve o novo ponteiro. Teste-a chamando-a algumas vezes e imprimindo o conteúdo preservado.
  4. Caça-vazamentos: dado o trecho abaixo, encontre e corrija todos os problemas de memória (há mais de um!). Depois confirme com a Valgrind que não sobrou nenhum vazamento.
    int **m = malloc(3 * sizeof(int));      // erro 1: sizeof errado
    for (int i = 0; i < 3; i++)
        m[i] = malloc(3 * sizeof(int));
    // ... usa m ...
    free(m);                                 // erro 2: liberou so o vetor de ponteiros
    

Para investigar (curiosidade prática)

  • Uso após free, ao vivo: aloque um int, atribua *p = 123, dê free(p) e imprima *p logo em seguida. Rode algumas vezes. O valor "sobrevive"? Agora faça outra alocação entre o free e o printf e observe. Explique com suas palavras por que isso é perigoso, ligando com a curiosidade da seção 5.1.
  • Testando o padrão seguro: modifique o realloc para pedir um tamanho absurdamente grande (por exemplo, realloc(v, 1000000000000UL * sizeof(int))) e confirme que o padrão com tmp faz o programa avisar o erro sem perder os dados que v já tinha.

✅ Checklist de autoavaliação

Marque com sinceridade — se algum item não estiver ✅, volte à seção correspondente.

  • Sei usar realloc para crescer/encolher um bloco preservando o conteúdo.
  • Uso o padrão seguro do realloc (ponteiro tmp) e explico por que ele importa.
  • Aloco uma matriz int ** com sizeof(int *) no vetor de ponteiros e sizeof(int) nas linhas.
  • Libero a matriz na ordem correta: primeiro as linhas, depois o vetor de ponteiros.
  • Explico o que é um vazamento de memória, por que é problema e como evitá-lo.
  • Reconheço e evito: uso após free, free duplo, NULL não checado, ponteiro não inicializado e perda de referência.
  • Aplico as boas práticas (inicializar, checar, parear malloc/free, p = NULL após free).

🔗 Referências e para se aprofundar

Antes de seguir para a Semana 14

Você agora sabe reservar, redimensionar e devolver memória com segurança — a base de toda estrutura de dados séria. Na Semana 14 vamos dar nome e forma aos dados: os registros (struct), que agrupam vários campos de tipos diferentes sob um mesmo nome, e as enumerações (enum). Junte isso ao que você aprendeu sobre ponteiros e alocação e você terá tudo para construir suas próprias estruturas dinâmicas — o grande salto que fecha a Unidade 3. Antes de avançar, garanta que seus programas desta semana passam limpos na Valgrind: memória bem gerenciada é um hábito, e o momento de formá-lo é agora.