Ponteiros para ponteiros, ou dupla indireção, são variáveis que armazenam o endereço de outro ponteiro. Em C, isso é representado por dois asteriscos (**). Esse conceito permite manipular ponteiros de forma indireta, sendo essencial em alocação dinâmica de matrizes, passagem de ponteiros por referência e estruturas de dados avançadas.

Dominar ponteiros para ponteiros é um passo importante para entender como o C gerencia memória e como criar código mais flexível e eficiente.

Dupla indireção (**)

A declaração de um ponteiro para ponteiro segue o padrão: tipo **nome;. Isso significa que nome é um ponteiro que aponta para uma variável do tipo tipo *. Por exemplo, int **p; declara p como um ponteiro para um ponteiro para inteiro.

Para acessar o valor final, usamos dois operadores de dereferência: **p. O primeiro asterisco (*p) nos dá o ponteiro apontado, e o segundo (**p) nos dá o valor inteiro. Essa cadeia pode se estender para mais níveis, mas na prática raramente se usa além de dois.

#include <stdio.h>

int main() {
    int x = 42;
    int *px = &x;
    int **ppx = &px;

    printf("x = %d\n", x);
    printf("*px = %d\n", *px);
    printf("**ppx = %d\n", **ppx);

    return 0;
}

Casos de uso

Um dos usos mais comuns de ponteiros para ponteiros é na alocação dinâmica de matrizes (arrays bidimensionais). Com malloc, podemos alocar um array de ponteiros e, em seguida, alocar cada linha individualmente. Outro uso é em funções que precisam modificar o ponteiro passado como argumento (por exemplo, para realocar memória).

Além disso, estruturas de dados como listas encadeadas ou árvores frequentemente usam ponteiros para ponteiros para facilitar a inserção e remoção de nós sem precisar de retorno.

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

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

int main() {
    int **matriz;
    aloca_matriz(&matriz, 3, 4);
    // uso...
    // liberação...
    return 0;
}

Exemplos

Exemplo 1: Modificar um ponteiro dentro de uma função. Sem dupla indireção, a função só altera uma cópia. Com **, podemos alterar o ponteiro original.

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

void alocar_int(int **p) {
    *p = (int *)malloc(sizeof(int));
    **p = 100;
}

int main() {
    int *ptr = NULL;
    alocar_int(&ptr);
    printf("%d\n", *ptr); // 100
    free(ptr);
    return 0;
}

Exemplo 2: Percorrer uma lista encadeada usando dupla indireção para inserir no início.

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

typedef struct no {
    int valor;
    struct no *prox;
} No;

void inserir_inicio(No **cabeca, int valor) {
    No *novo = (No *)malloc(sizeof(No));
    novo->valor = valor;
    novo->prox = *cabeca;
    *cabeca = novo;
}

int main() {
    No *lista = NULL;
    inserir_inicio(&lista, 10);
    inserir_inicio(&lista, 20);
    // percorrer...
    return 0;
}

Cuidados

O principal cuidado é sempre verificar se os ponteiros envolvidos são válidos antes de dereferenciar. Um ponteiro para ponteiro nulo ou não inicializado pode causar segmentation fault. Além disso, lembre-se de que alocar memória para um ponteiro para ponteiro requer dois níveis de alocação (primeiro o array de ponteiros, depois cada linha), e a liberação deve ser feita na ordem inversa.

Outro ponto: ao passar um ponteiro para ponteiro para uma função, certifique-se de que o argumento seja o endereço do ponteiro que se deseja modificar. Erros comuns incluem esquecer o & ou usar um ponteiro não inicializado.

// Exemplo de erro: esquecer de alocar o ponteiro intermediário
int **p;
*p = (int *)malloc(sizeof(int)); // ERRO: p não aponta para lugar algum

// Correto:
int *q;
int **p = &q;
*p = (int *)malloc(sizeof(int));

Boas práticas

Use nomes de variáveis que indiquem o nível de indireção (por exemplo, pp para ponteiro para ponteiro). Sempre inicialize ponteiros para ponteiros com NULL e verifique antes de usar. Prefira criar funções auxiliares para alocação/liberação de matrizes dinâmicas para evitar vazamentos de memória.

Referências

Exercícios

  1. Escreva um programa que declare um inteiro, um ponteiro para inteiro e um ponteiro para ponteiro para inteiro. Atribua valores e imprima todos usando indireção adequada.
  2. ✓ Resposta:
    #include <stdio.h>
    
    int main() {
        int x = 10;
        int *p = &x;
        int **pp = &p;
    
        printf("x = %d\n", x);
        printf("*p = %d\n", *p);
        printf("**pp = %d\n", **pp);
    
        return 0;
    }
  3. Implemente uma função que receba um ponteiro para ponteiro de inteiro e aloque dinamicamente um inteiro com valor 42. Teste no main.
  4. ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    
    void aloca_int(int **pp) {
        *pp = (int *)malloc(sizeof(int));
        **pp = 42;
    }
    
    int main() {
        int *p = NULL;
        aloca_int(&p);
        printf("%d\n", *p);
        free(p);
        return 0;
    }
  5. Crie uma função que receba um ponteiro para ponteiro de char (string) e duplique a string passada, alocando nova memória. Teste com "Hello".
  6. ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    void duplica_string(char **dest, const char *src) {
        *dest = (char *)malloc((strlen(src) + 1) * sizeof(char));
        strcpy(*dest, src);
    }
    
    int main() {
        char *copia = NULL;
        duplica_string(&copia, "Hello");
        printf("%s\n", copia);
        free(copia);
        return 0;
    }
  7. Escreva um programa que aloque dinamicamente uma matriz 2x3 de inteiros usando ponteiro para ponteiro, preencha com valores e imprima. Não esqueça de liberar a memória.
  8. ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
        int linhas = 2, colunas = 3;
        int **mat = (int **)malloc(linhas * sizeof(int *));
        for (int i = 0; i < linhas; i++) {
            mat[i] = (int *)malloc(colunas * sizeof(int));
        }
    
        // preencher
        int count = 1;
        for (int i = 0; i < linhas; i++) {
            for (int j = 0; j < colunas; j++) {
                mat[i][j] = count++;
            }
        }
    
        // imprimir
        for (int i = 0; i < linhas; i++) {
            for (int j = 0; j < colunas; j++) {
                printf("%d ", mat[i][j]);
            }
            printf("\n");
        }
    
        // liberar
        for (int i = 0; i < linhas; i++) {
            free(mat[i]);
        }
        free(mat);
    
        return 0;
    }
  9. Explique por que a função abaixo não modifica o ponteiro original e corrija-a usando dupla indireção.
  10. void aloca(int *p) {
        p = (int *)malloc(sizeof(int));
        *p = 10;
    }

    ✓ Resposta:

    A função recebe uma cópia do ponteiro, então a alocação é perdida ao retornar. Corrigida:

    void aloca(int **p) {
        *p = (int *)malloc(sizeof(int));
        **p = 10;
    }