Aritmética de ponteiros

1. Conceitos Fundamentais da Aritmética de Ponteiros

A aritmética de ponteiros é um dos recursos mais poderosos e ao mesmo tempo mais perigosos da linguagem C. Diferentemente da aritmética comum, que opera sobre valores numéricos, a aritmética de ponteiros opera sobre endereços de memória, levando em consideração o tipo de dado apontado.

A base de todo o conceito está na relação entre endereços de memória e tipos de dados. Quando declaramos um ponteiro int *p, o compilador sabe que cada deslocamento de uma unidade corresponde a sizeof(int) bytes na memória. Isso significa que p + 1 não avança um byte, mas sim o tamanho completo de um inteiro (tipicamente 4 bytes).

A unidade fundamental de avanço é determinada por sizeof(tipo). Se temos um ponteiro para double, cada incremento avançará 8 bytes (em sistemas comuns). Essa característica torna a aritmética de ponteiros ideal para navegação em arrays, pois cada operação nos move exatamente para o próximo elemento.

2. Operações Básicas: Incremento e Decremento

As operações mais simples são ptr++ e ptr--, que deslocam o ponteiro para o próximo ou anterior elemento do array.

#include <stdio.h>

int main() {
    int numeros[] = {10, 20, 30, 40, 50};
    int *ptr = numeros;  // aponta para numeros[0]

    printf("Valor inicial: %d (endereco: %p)\n", *ptr, (void*)ptr);

    ptr++;  // avança para numeros[1]
    printf("Apos ptr++: %d (endereco: %p)\n", *ptr, (void*)ptr);

    ptr--;  // volta para numeros[0]
    printf("Apos ptr--: %d (endereco: %p)\n", *ptr, (void*)ptr);

    return 0;
}

Com diferentes tipos de dados, o comportamento ilustra a importância do sizeof:

#include <stdio.h>

int main() {
    int    int_arr[] = {1, 2, 3};
    char   char_arr[] = {'A', 'B', 'C'};
    double dbl_arr[] = {1.1, 2.2, 3.3};

    int    *iptr = int_arr;
    char   *cptr = char_arr;
    double *dptr = dbl_arr;

    printf("int:    %p -> %p (diferenca: %ld bytes)\n",
           (void*)iptr, (void*)(iptr + 1),
           (long)((char*)(iptr + 1) - (char*)iptr));

    printf("char:   %p -> %p (diferenca: %ld bytes)\n",
           (void*)cptr, (void*)(cptr + 1),
           (long)((char*)(cptr + 1) - (char*)cptr));

    printf("double: %p -> %p (diferenca: %ld bytes)\n",
           (void*)dptr, (void*)(dptr + 1),
           (long)((char*)(dptr + 1) - (char*)dptr));

    return 0;
}

Cuidado fundamental: a aritmética de ponteiros não verifica limites. Avançar além do array ou recuar antes do início é comportamento indefinido.

3. Soma e Subtração de Inteiros com Ponteiros

As expressões ptr + n e ptr - n permitem avançar ou recuar múltiplos elementos de uma só vez:

#include <stdio.h>

int main() {
    int arr[] = {100, 200, 300, 400, 500};
    int *ptr = arr;  // aponta para arr[0]

    // ptr + 2 equivale a &arr[2]
    printf("ptr + 2 = %d (esperado: 300)\n", *(ptr + 2));

    // ptr + 4 equivale a &arr[4]
    printf("ptr + 4 = %d (esperado: 500)\n", *(ptr + 4));

    // Navegação em loop sem índice
    for (int i = 0; i < 5; i++) {
        printf("Elemento %d: %d\n", i, *(ptr + i));
    }

    // Diferença crucial: ptr + 1 vs (int*)ptr + 1
    // Ambos são equivalentes quando ptr já é int*
    printf("ptr + 1 = %p\n", (void*)(ptr + 1));

    return 0;
}

A diferença entre ptr + 1 e (int*)ptr + 1 só se manifesta quando o ponteiro original é de um tipo diferente. Se ptr é char*, ptr + 1 avança 1 byte, enquanto (int*)ptr + 1 avançaria 4 bytes (tratando o endereço como se apontasse para inteiros).

4. Subtração entre Ponteiros

A subtração de dois ponteiros que apontam para elementos do mesmo array retorna a distância entre eles em número de elementos, não em bytes:

#include <stdio.h>
#include <stddef.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50, 60, 70};
    int *p1 = &arr[1];  // aponta para 20
    int *p2 = &arr[5];  // aponta para 60

    // Calculando a distância
    ptrdiff_t distancia = p2 - p1;
    printf("p2 - p1 = %td elementos\n", distancia);
    printf("arr[5] - arr[1] = %d\n", arr[5] - arr[1]);

    // Verificando que p1 + distancia == p2
    printf("p1 + distancia = %p\n", (void*)(p1 + distancia));
    printf("p2 = %p\n", (void*)p2);

    // Número total de elementos no array
    ptrdiff_t total = &arr[6] - arr;
    printf("Total de elementos: %td\n", total + 1);

    return 0;
}

O resultado é do tipo ptrdiff_t, definido em <stddef.h>, que é um tipo inteiro com sinal capaz de armazenar a diferença entre dois ponteiros.

5. Comparação entre Ponteiros

Os operadores relacionais podem ser usados para comparar endereços dentro do mesmo array:

#include <stdio.h>

int main() {
    int arr[] = {5, 10, 15, 20, 25};
    int *inicio = arr;
    int *fim = arr + 4;  // aponta para o último elemento

    // Comparações úteis
    if (inicio < fim) {
        printf("inicio (%p) esta antes de fim (%p)\n",
               (void*)inicio, (void*)fim);
    }

    // Verificando se um ponteiro está dentro dos limites
    int *ptr = arr + 2;  // elemento 15
    if (ptr >= inicio && ptr <= fim) {
        printf("ptr esta dentro do array\n");
    }

    // Comparação com NULL
    int *nulo = NULL;
    if (nulo == NULL) {
        printf("Ponteiro nulo detectado\n");
    }

    // Percorrendo com comparação
    for (int *p = arr; p <= arr + 4; p++) {
        printf("%d ", *p);
    }
    printf("\n");

    return 0;
}

Importante: comparar ponteiros que não pertencem ao mesmo array (ou um elemento após o final) é comportamento indefinido.

6. Aritmética com Ponteiros Void e Ponteiros para Tipos Diferentes

Ponteiros do tipo void* são genéricos, mas não suportam aritmética diretamente porque o compilador não conhece o tamanho do tipo apontado:

#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40};
    void *vptr = arr;

    // Isto NÃO compila:
    // vptr++;  // erro: aritmética com void*

    // Solução: cast para char* para navegação byte a byte
    char *cptr = (char*)vptr;

    printf("Primeiro byte do array: %d\n", (unsigned char)*cptr);

    // Avançando byte a byte
    for (int i = 0; i < sizeof(int) * 4; i++) {
        printf("Byte %d: %02x\n", i, (unsigned char)*(cptr + i));
    }

    // Para voltar à aritmética normal, faça cast de volta
    int *iptr = (int*)cptr;
    printf("Primeiro inteiro: %d\n", *iptr);

    return 0;
}

Cuidados com alinhamento: fazer cast de char* para int* pode resultar em acesso não alinhado, causando comportamento indefinido em algumas arquiteturas.

7. Aplicações Práticas e Armadilhas Comuns

#include <stdio.h>

int main() {
    int matriz[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    // Percorrendo com ponteiro para array de 4 ints
    int (*ptr_linha)[4] = matriz;

    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%2d ", *(*(ptr_linha + i) + j));
        }
        printf("\n");
    }

    return 0;
}

Diferença entre *ptr++ e (*ptr)++

#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30};
    int *p = arr;

    // *ptr++ : primeiro acessa o valor, depois incrementa o ponteiro
    printf("*p++ = %d (p agora aponta para %d)\n", *p++, *p);

    // (*ptr)++ : incrementa o valor apontado
    printf("(*p)++: antes = %d\n", *p);
    (*p)++;
    printf("(*p)++: depois = %d\n", *p);

    return 0;
}

Erros comuns

#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *p = arr;

    // ERRO 1: estouro de buffer
    // for (int i = 0; i <= 5; i++) {  // acessa arr[5], que não existe
    //     printf("%d ", *p++);
    // }

    // ERRO 2: confundir endereço com valor
    printf("Endereco: %p\n", (void*)p);    // correto
    // printf("Endereco: %p\n", p);         // também funciona, mas sem cast

    // ERRO 3: subtração de ponteiros de arrays diferentes
    int outro_arr[3] = {10, 20, 30};
    // ptrdiff_t diff = p - outro_arr;  // comportamento indefinido

    // Forma correta de evitar problemas
    p = arr;
    for (int i = 0; i < 5; i++) {
        printf("%d ", *(p + i));
    }
    printf("\n");

    return 0;
}

A aritmética de ponteiros é uma ferramenta poderosa que permite escrever código eficiente e elegante, mas exige disciplina e compreensão profunda da memória. Dominar esse conceito é essencial para programação em C de alto nível.

Referências