Ponteiros para ponteiros: o que são e quando usar

1. Introdução aos Ponteiros para Ponteiros

Em Linguagem C, um ponteiro para ponteiro (também chamado de ponteiro duplo ou double pointer) é uma variável que armazena o endereço de memória de outro ponteiro. Enquanto um ponteiro simples (int *ptr) aponta para uma variável do tipo base, um ponteiro para ponteiro (int **ptr) aponta para um ponteiro que, por sua vez, aponta para a variável final.

A sintaxe básica é:

int valor = 42;
int *ptr = &valor;    // ponteiro simples
int **ptr2 = &ptr;    // ponteiro para ponteiro

Na memória, temos três níveis: ptr2 contém o endereço de ptr, que contém o endereço de valor, que armazena o inteiro 42. Isso cria uma cadeia de dois níveis de indireção.

A diferença fundamental é que um ponteiro simples permite acessar e modificar um valor, enquanto um ponteiro duplo permite acessar e modificar o próprio ponteiro que aponta para o valor.

2. Declaração e Inicialização

A declaração de um ponteiro duplo segue o padrão tipo **nome;. Para inicializá-lo, precisamos do endereço de um ponteiro existente:

#include <stdio.h>

int main() {
    int numero = 100;
    int *p1 = &numero;       // p1 aponta para numero
    int **p2 = &p1;          // p2 aponta para p1

    printf("Valor de numero: %d\n", numero);
    printf("Valor via *p1: %d\n", *p1);
    printf("Valor via **p2: %d\n", **p2);

    return 0;
}

A saída será:

Valor de numero: 100
Valor via *p1: 100
Valor via **p2: 100

3. Operações com Ponteiros Duplos

Com ponteiros duplos, podemos realizar três operações principais:

  • ptr2 → acessa o endereço armazenado em ptr2 (que é o endereço de ptr)
  • *ptr2 → acessa o valor apontado por ptr2 (que é o endereço armazenado em ptr)
  • **ptr2 → acessa o valor final (o inteiro)

Podemos também modificar o ponteiro apontado:

#include <stdio.h>

int main() {
    int a = 10, b = 20;
    int *p = &a;
    int **pp = &p;

    printf("Antes: *p = %d\n", *p);   // 10

    *pp = &b;   // modifica p para apontar para b
    printf("Depois: *p = %d\n", *p);  // 20

    return 0;
}

Uma armadilha comum é confundir os níveis de indireção. Usar *pp quando se deseja **pp (ou vice-versa) pode causar erros de segmentação ou valores incorretos.

4. Uso em Funções: Modificar Ponteiros Passados como Argumento

Em C, argumentos são passados por valor. Isso significa que uma função não pode modificar o ponteiro original passado a ela. Para contornar essa limitação, passamos o endereço do ponteiro (um ponteiro para ponteiro).

O exemplo clássico é a alocação dinâmica de uma matriz dentro de uma função:

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

void alocar_matriz(int ***mat, int linhas, int colunas) {
    // Aloca o array de ponteiros para as linhas
    *mat = (int **)malloc(linhas * sizeof(int *));
    if (*mat == NULL) {
        printf("Erro de alocacao!\n");
        return;
    }

    // Aloca cada linha
    for (int i = 0; i < linhas; i++) {
        (*mat)[i] = (int *)malloc(colunas * sizeof(int));
        if ((*mat)[i] == NULL) {
            printf("Erro de alocacao na linha %d!\n", i);
            // Libera memória já alocada
            for (int j = 0; j < i; j++) free((*mat)[j]);
            free(*mat);
            *mat = NULL;
            return;
        }
    }
}

void preencher_matriz(int **mat, int linhas, int colunas) {
    for (int i = 0; i < linhas; i++) {
        for (int j = 0; j < colunas; j++) {
            mat[i][j] = i * colunas + j;
        }
    }
}

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

int main() {
    int **matriz = NULL;
    int linhas = 3, colunas = 4;

    alocar_matriz(&matriz, linhas, colunas);
    if (matriz != NULL) {
        preencher_matriz(matriz, linhas, colunas);
        imprimir_matriz(matriz, linhas, colunas);
    }

    // Liberação (será detalhada na próxima seção)
    for (int i = 0; i < linhas; i++) free(matriz[i]);
    free(matriz);

    return 0;
}

Observe que a função alocar_matriz recebe int ***mat (ponteiro para ponteiro para ponteiro) para poder modificar o ponteiro matriz declarado em main.

5. Matrizes Dinâmicas com Ponteiros Duplos

A alocação de uma matriz 2D usando int ** segue um processo de duas etapas:

  1. Alocar um array de ponteiros (as linhas)
  2. Para cada linha, alocar um array de inteiros (as colunas)
#include <stdio.h>
#include <stdlib.h>

int main() {
    int linhas = 4, colunas = 5;

    // Passo 1: alocar o array de ponteiros para linhas
    int **mat = (int **)malloc(linhas * sizeof(int *));

    // Passo 2: alocar cada linha individualmente
    for (int i = 0; i < linhas; i++) {
        mat[i] = (int *)malloc(colunas * sizeof(int));
    }

    // Uso normal: mat[i][j]
    for (int i = 0; i < linhas; i++) {
        for (int j = 0; j < colunas; j++) {
            mat[i][j] = i * 10 + j;
        }
    }

    // Liberação correta: primeiro as colunas, depois as linhas
    for (int i = 0; i < linhas; i++) {
        free(mat[i]);      // libera cada linha
    }
    free(mat);             // libera o array de ponteiros

    return 0;
}

A ordem de liberação é crucial: primeiro liberamos cada array de colunas, depois o array de ponteiros das linhas. Fazer o contrário causaria vazamento de memória.

6. Vetores de Strings (Array de Ponteiros para char)

Uma aplicação muito comum de ponteiros duplos é na representação de vetores de strings. O parâmetro argv em main(int argc, char **argv) é um exemplo clássico.

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

int main() {
    // Criando um vetor de 3 strings
    char **nomes = (char **)malloc(3 * sizeof(char *));

    nomes[0] = (char *)malloc(20 * sizeof(char));
    nomes[1] = (char *)malloc(20 * sizeof(char));
    nomes[2] = (char *)malloc(20 * sizeof(char));

    strcpy(nomes[0], "Alice");
    strcpy(nomes[1], "Bruno");
    strcpy(nomes[2], "Carlos");

    // Percorrendo o vetor de strings
    for (int i = 0; i < 3; i++) {
        printf("Nome %d: %s\n", i, nomes[i]);
    }

    // Liberação
    for (int i = 0; i < 3; i++) free(nomes[i]);
    free(nomes);

    return 0;
}

O padrão é o mesmo das matrizes: primeiro alocamos o array de ponteiros, depois alocamos cada string individualmente.

7. Quando Evitar Ponteiros Duplos

Apesar de poderosos, ponteiros duplos devem ser usados com critério. Evite-os quando:

  1. O problema pode ser resolvido com structs: Agrupar dados relacionados em uma struct muitas vezes elimina a necessidade de múltiplos níveis de indireção.

  2. Um vetor unidimensional é suficiente: Para matrizes, considere usar um único vetor e calcular índices manualmente (mat[i * colunas + j]).

  3. A complexidade não se justifica: Se você só precisa modificar um ponteiro em uma função, avalie se há alternativas mais simples.

// Alternativa mais simples: struct em vez de ponteiro duplo
typedef struct {
    int *dados;
    int linhas;
    int colunas;
} Matriz;

void inicializar_matriz(Matriz *m, int linhas, int colunas) {
    m->linhas = linhas;
    m->colunas = colunas;
    m->dados = (int *)malloc(linhas * colunas * sizeof(int));
}

int acessar(Matriz *m, int i, int j) {
    return m->dados[i * m->colunas + j];
}

Boas práticas incluem documentar claramente quando um parâmetro é um ponteiro duplo e usar nomes descritivos que indiquem o propósito (como pp_matriz ou p_matriz).

Referências