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 = № // 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 emptr2(que é o endereço deptr)*ptr2→ acessa o valor apontado porptr2(que é o endereço armazenado emptr)**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:
- Alocar um array de ponteiros (as linhas)
- 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:
-
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.
-
Um vetor unidimensional é suficiente: Para matrizes, considere usar um único vetor e calcular índices manualmente (
mat[i * colunas + j]). -
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
- Ponteiros para Ponteiros em C - Documentação GNU C — Explicação oficial da Free Software Foundation sobre ponteiros duplos em C
- Double Pointer (Pointer to Pointer) in C - GeeksforGeeks — Tutorial completo com exemplos práticos de declaração, inicialização e uso
- Pointers to Pointers - C Programming Tutorial by Programiz — Guia passo a passo com diagramas de memória para iniciantes
- Dynamic 2D Array Allocation in C - Tutorialspoint — Exemplos detalhados de alocação dinâmica de matrizes 2D com ponteiros duplos
- C Pointer to Pointer (Double Pointer) - Javatpoint — Explicação visual e código para entender a relação entre níveis de indireção
- When to Use Double Pointers in C - Stack Overflow — Discussão da comunidade sobre casos de uso reais e melhores práticas