Ponteiros duplos e matrizes dinâmicas

1. Fundamentos de Ponteiros Duplos

1.1. Declaração e sintaxe

Um ponteiro duplo é declarado com dois asteriscos: int **ptr;. Isso significa que ptr é um ponteiro que armazena o endereço de outro ponteiro do tipo int*. Em outras palavras, é um ponteiro para um ponteiro para um inteiro.

int valor = 42;
int *ptr_simples = &valor;
int **ptr_duplo = &ptr_simples;

1.2. Representação em memória

Na memória, um ponteiro duplo segue a seguinte hierarquia:
- ptr_duplo armazena o endereço de ptr_simples
- ptr_simples armazena o endereço de valor
- valor contém o dado efetivo (42)

Para acessar o valor final, é necessário fazer uma desreferenciação dupla:

printf("%d\n", **ptr_duplo);  // Imprime 42
printf("%d\n", *ptr_simples); // Também imprime 42

1.3. Relação com arrays unidimensionais

Em C, um array de ponteiros int *arr[] é equivalente a um ponteiro duplo int **arr quando passado como argumento para uma função. Isso ocorre porque o nome de um array decai para um ponteiro para seu primeiro elemento.

int *vetor[5];  // Array de 5 ponteiros para int
int **p = vetor; // Válido: vetor decai para int**

2. Alocação Dinâmica de Matrizes com Ponteiro Duplo

2.1. Alocação passo a passo

Para alocar uma matriz dinâmica de linhas x colunas:

int **matriz;
int linhas = 3, colunas = 3;

// Passo 1: alocar um array de ponteiros (as linhas)
matriz = (int**) malloc(linhas * sizeof(int*));

// Passo 2: para cada linha, alocar um array de inteiros (as colunas)
for (int i = 0; i < linhas; i++) {
    matriz[i] = (int*) malloc(colunas * sizeof(int));
}

2.2. Liberação de memória

A liberação deve seguir a ordem inversa da alocação:

// Liberar cada linha primeiro
for (int i = 0; i < linhas; i++) {
    free(matriz[i]);
}
// Depois liberar o ponteiro principal
free(matriz);

2.3. Exemplo prático: matriz 3x3

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

int main() {
    int **mat;
    int lin = 3, col = 3;
    int count = 1;

    // Alocação
    mat = (int**) malloc(lin * sizeof(int*));
    for (int i = 0; i < lin; i++) {
        mat[i] = (int*) malloc(col * sizeof(int));
    }

    // Preenchimento
    for (int i = 0; i < lin; i++) {
        for (int j = 0; j < col; j++) {
            mat[i][j] = count++;
        }
    }

    // Exibição
    for (int i = 0; i < lin; i++) {
        for (int j = 0; j < col; j++) {
            printf("%d ", mat[i][j]);
        }
        printf("\n");
    }

    // Liberação
    for (int i = 0; i < lin; i++) {
        free(mat[i]);
    }
    free(mat);

    return 0;
}

3. Acesso e Manipulação de Elementos

3.1. Indexação

Existem duas formas equivalentes de acessar elementos:

// Forma 1: notação de array (mais legível)
mat[i][j] = 10;

// Forma 2: aritmética de ponteiros (equivalente)
*(*(mat + i) + j) = 10;

3.2. Percorrimento com laços aninhados

for (int i = 0; i < linhas; i++) {
    for (int j = 0; j < colunas; j++) {
        printf("Elemento [%d][%d] = %d\n", i, j, mat[i][j]);
    }
}

3.3. Passagem de matriz dinâmica para funções

void imprime_matriz(int **mat, int lin, int col) {
    for (int i = 0; i < lin; i++) {
        for (int j = 0; j < col; j++) {
            printf("%3d ", mat[i][j]);
        }
        printf("\n");
    }
}

// Chamada
imprime_matriz(matriz, linhas, colunas);

4. Matrizes Irregulares (Jagged Arrays)

4.1. Conceito

Matrizes irregulares permitem que cada linha tenha um número diferente de colunas, otimizando o uso de memória.

4.2. Alocação

int **jagged;
int linhas = 3;
int tamanhos[] = {2, 4, 3}; // Cada linha com tamanho diferente

jagged = (int**) malloc(linhas * sizeof(int*));
for (int i = 0; i < linhas; i++) {
    jagged[i] = (int*) malloc(tamanhos[i] * sizeof(int));
}

4.3. Caso de uso

Útil para armazenar strings de comprimentos variáveis ou dados esparsos:

char **nomes;
nomes = (char**) malloc(3 * sizeof(char*));
nomes[0] = (char*) malloc(5 * sizeof(char));  // "João"
nomes[1] = (char*) malloc(3 * sizeof(char));  // "Ana"
nomes[2] = (char*) malloc(7 * sizeof(char));  // "Mariana"

5. Ponteiros Duplos em Funções

5.1. Modificação de ponteiros dentro de funções

Para modificar um ponteiro dentro de uma função, passamos seu endereço:

void aloca_matriz(int ***mat, int lin, int col) {
    *mat = (int**) malloc(lin * sizeof(int*));
    for (int i = 0; i < lin; i++) {
        (*mat)[i] = (int*) malloc(col * sizeof(int));
    }
}

// Uso
int **minha_matriz;
aloca_matriz(&minha_matriz, 3, 3);

5.2. Retorno de matrizes alocadas

int** cria_matriz(int lin, int col) {
    int **mat = (int**) malloc(lin * sizeof(int*));
    for (int i = 0; i < lin; i++) {
        mat[i] = (int*) malloc(col * sizeof(int));
    }
    return mat;
}

5.3. Exemplo completo

int** preenche_matriz(int lin, int col) {
    int **mat = cria_matriz(lin, col);
    for (int i = 0; i < lin; i++) {
        for (int j = 0; j < col; j++) {
            mat[i][j] = i * col + j;
        }
    }
    return mat;
}

6. Erros Comuns e Boas Práticas

6.1. Vazamentos de memória

Sempre libere cada linha individualmente antes de liberar o ponteiro principal:

// ERRADO: libera apenas o ponteiro principal
free(mat);  // As linhas continuam alocadas!

// CORRETO
for (int i = 0; i < lin; i++) free(mat[i]);
free(mat);

6.2. Acesso inválido

Sempre verifique índices e ponteiros nulos:

if (mat == NULL) {
    printf("Erro: matriz não alocada\n");
    return;
}
if (i >= linhas || j >= colunas) {
    printf("Erro: índice fora dos limites\n");
    return;
}

6.3. Uso correto de free

// Verificar NULL antes de liberar
if (mat != NULL) {
    for (int i = 0; i < lin; i++) {
        if (mat[i] != NULL) {
            free(mat[i]);
            mat[i] = NULL;  // Boa prática
        }
    }
    free(mat);
    mat = NULL;
}

7. Comparação com Matrizes Estáticas

7.1. Diferenças de alocação

Característica Matriz Estática Matriz Dinâmica
Local na memória Pilha (stack) Heap
Tamanho Fixo em compilação Variável em execução
Liberação Automática Manual (free)
Velocidade Mais rápida Mais lenta (indireção)

7.2. Vantagens da matriz dinâmica

  • Tamanho definido em tempo de execução
  • Uso eficiente de memória (aloca apenas o necessário)
  • Possibilidade de redimensionamento
  • Matrizes irregulares

7.3. Desvantagens

  • Overhead de gerenciamento manual de memória
  • Risco de vazamentos de memória
  • Fragmentação da memória heap
  • Acesso mais lento devido à dupla indireção
// Matriz estática (tamanho fixo)
int estatica[3][4];  // Sempre 3x4

// Matriz dinâmica (tamanho variável)
int **dinamica;
int l = obter_tamanho();  // Em tempo de execução
dinamica = (int**) malloc(l * sizeof(int*));

Referências