Ponteiros: o coração do C

1. O que é um ponteiro?

Um ponteiro é uma variável que armazena um endereço de memória. Enquanto uma variável comum guarda um valor (como um número inteiro ou um caractere), um ponteiro guarda a localização onde esse valor está armazenado na memória do computador.

A declaração de um ponteiro segue a sintaxe:

int *p;    // ponteiro para inteiro
char *c;   // ponteiro para caractere
float *f;  // ponteiro para float

Dois operadores fundamentais trabalham com ponteiros:

  • Operador de endereço (&): retorna o endereço de memória de uma variável
  • Operador de desreferência (*): acessa o valor armazenado no endereço apontado
int x = 42;
int *p = &x;  // p recebe o endereço de x

printf("Valor de x: %d\n", x);       // 42
printf("Endereço de x: %p\n", &x);   // 0x7ff...
printf("Valor apontado: %d\n", *p);  // 42

2. Ponteiros e tipos de dados

O tipo do ponteiro determina quantos bytes serão lidos ou escritos ao desreferenciá-lo. Um int* sabe que cada elemento ocupa normalmente 4 bytes, enquanto um char* trabalha com 1 byte.

A aritmética de ponteiros respeita o tamanho do tipo apontado:

int arr[] = {10, 20, 30, 40};
int *p = arr;

printf("%d\n", *p);      // 10 (primeiro elemento)
p++;                     // avança 4 bytes (tamanho de int)
printf("%d\n", *p);      // 20 (segundo elemento)

O ponteiro void * é um ponteiro genérico que pode apontar para qualquer tipo de dado. No entanto, não é possível realizar aritmética ou desreferência diretamente com ele sem um cast explícito:

void *ptr;
int x = 100;
ptr = &x;
printf("%d\n", *(int *)ptr);  // necessário o cast

3. Ponteiros e arrays

Em C, o nome de um array funciona como um ponteiro constante para o primeiro elemento. Isso significa que arr e &arr[0] são equivalentes:

int vetor[5] = {1, 2, 3, 4, 5};

printf("%p\n", vetor);    // endereço do primeiro elemento
printf("%p\n", &vetor[0]);// mesmo endereço

// Acessando elementos com aritmética de ponteiros
for(int i = 0; i < 5; i++) {
    printf("%d ", *(vetor + i));  // 1 2 3 4 5
}

Para arrays multidimensionais, a lógica se estende:

int matriz[2][3] = {{1,2,3}, {4,5,6}};
int *p = &matriz[0][0];

for(int i = 0; i < 6; i++) {
    printf("%d ", *(p + i));  // 1 2 3 4 5 6
}

4. Passagem de parâmetros por referência

C não possui passagem por referência nativamente, mas podemos simulá-la usando ponteiros. Isso permite que funções modifiquem variáveis da função chamadora:

void troca(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 10, y = 20;
    printf("Antes: x=%d y=%d\n", x, y);
    troca(&x, &y);
    printf("Depois: x=%d y=%d\n", x, y);
    return 0;
}
Antes: x=10 y=20
Depois: x=20 y=10

5. Ponteiros para ponteiros

Um ponteiro para ponteiro (int **p) armazena o endereço de outro ponteiro. Isso é útil para alocar matrizes dinâmicas e manipular argumentos de linha de comando:

int x = 42;
int *p = &x;
int **pp = &p;

printf("%d\n", **pp);  // 42

// Alocação dinâmica de matriz
int **matriz = malloc(3 * sizeof(int *));
for(int i = 0; i < 3; i++) {
    matriz[i] = malloc(4 * sizeof(int));
}

Cuidado com múltiplos níveis de indireção: cada nível adicional aumenta a complexidade e o risco de erros.

6. Ponteiros e funções

Ponteiros para funções permitem armazenar e chamar funções dinamicamente. A sintaxe de declaração é:

int (*fp)(int, int);  // ponteiro para função que recebe dois ints e retorna int

Exemplo prático com callbacks:

int soma(int a, int b) { return a + b; }
int subtrai(int a, int b) { return a - b; }
int multiplica(int a, int b) { return a * b; }

int main() {
    int (*operacao[3])(int, int) = {soma, subtrai, multiplica};
    int opcao, a = 10, b = 5;

    printf("0-Soma 1-Subtrai 2-Multiplica: ");
    scanf("%d", &opcao);

    if(opcao >= 0 && opcao < 3) {
        printf("Resultado: %d\n", operacao[opcao](a, b));
    }
    return 0;
}

7. Armadilhas comuns com ponteiros

Ponteiros mal gerenciados são fonte frequente de bugs graves em C:

Ponteiros não inicializados: contêm lixo de memória e podem causar segmentation fault:

int *p;        // não inicializado
*p = 10;       // PERIGO: escreve em local desconhecido

Ponteiros pendurados (dangling pointers): ocorrem quando o bloco de memória apontado é liberado:

int *p = malloc(sizeof(int));
free(p);
*p = 5;        // PERIGO: p agora é dangling pointer

Vazamento de memória: esquecer de liberar memória alocada dinamicamente:

void funcao() {
    int *p = malloc(1000 * sizeof(int));
    // sem free(p) aqui -> vazamento de memória
}

Acesso fora dos limites: ultrapassar os limites de um array ou bloco alocado:

int arr[5];
arr[10] = 42;  // PERIGO: acesso fora dos limites

Conclusão

Ponteiros são o recurso mais poderoso e ao mesmo tempo mais perigoso da linguagem C. Dominá-los é essencial para programar eficientemente, gerenciar memória dinamicamente e construir sistemas complexos. A prática constante com exemplos reais, combinada com ferramentas de análise como Valgrind e sanitizers, ajuda a evitar as armadilhas mais comuns.

Referências