Ponteiros para ponteiros
Esta aula explora ponteiros para ponteiros em C, também conhecidos como dupla indireção. Você aprenderá a sintaxe, os principais casos de uso (como matrizes dinâmicas e parâmetros de funções) e os cuidados necessários para evitar erros comuns.
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
- cppreference: Pointer declaration
- GNU C Library: Pointer to Pointer
- Tutorialspoint: Pointer to Pointer in C
- Wikipedia: Double pointer
- Stack Overflow: How do pointer to pointers work in C?
Exercícios
- 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.
- Implemente uma função que receba um ponteiro para ponteiro de inteiro e aloque dinamicamente um inteiro com valor 42. Teste no main.
- 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".
- 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.
- Explique por que a função abaixo não modifica o ponteiro original e corrija-a usando dupla indireção.
#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;
}#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;
}#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;
}#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;
}void aloca(int *p) {
p = (int *)malloc(sizeof(int));
*p = 10;
}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;
}