A aritmética de ponteiros é um dos recursos mais poderosos e, ao mesmo tempo, perigosos da linguagem C. Ela permite que você manipule endereços de memória diretamente, realizando operações como incremento, decremento, soma, subtração e comparação. Quando usada corretamente, a aritmética de ponteiros pode tornar o código mais eficiente, especialmente ao percorrer arrays ou gerenciar memória dinâmica. No entanto, erros podem levar a acessos inválidos e comportamento indefinido.

Nesta aula, vamos explorar as principais operações de aritmética de ponteiros, entender como o tipo do ponteiro afeta o resultado, aprender a comparar ponteiros e discutir os cuidados essenciais para evitar armadilhas. Ao final, você terá uma compreensão sólida para usar ponteiros de forma segura e eficiente em seus programas.

Incremento e decremento

Operadores de incremento (++) e decremento (--) podem ser aplicados a ponteiros. Quando você incrementa um ponteiro, ele passa a apontar para o próximo elemento do tipo apontado, não para o próximo byte. O deslocamento real em bytes é igual ao tamanho do tipo apontado (sizeof(tipo)).

Por exemplo, se um ponteiro int* p aponta para o endereço 1000, e sizeof(int) é 4, então p++ fará com que p aponte para o endereço 1004. Isso é fundamental para percorrer arrays de forma natural.

#include <stdio.h>

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

    printf("Endereço inicial: %p\n", (void*)p);
    printf("Valor: %d\n", *p);

    p++;  // avança para o próximo int
    printf("Após p++: endereço %p, valor %d\n", (void*)p, *p);

    p += 2;  // avança dois ints
    printf("Após p+=2: endereço %p, valor %d\n", (void*)p, *p);

    p--;  // volta um int
    printf("Após p--: endereço %p, valor %d\n", (void*)p, *p);

    return 0;
}

No código acima, p++ move o ponteiro para o próximo elemento do array (arr[1]), e p += 2 move dois elementos adiante (arr[3]). O decremento funciona de forma análoga. É importante notar que a aritmética de ponteiros só é válida dentro dos limites de um array ou de um bloco de memória alocado. Incrementar além do último elemento ou decrementar antes do primeiro causa comportamento indefinido.

Relação com tipos

O tipo do ponteiro determina o incremento real em bytes. Um ponteiro char* avança de 1 em 1 byte, um int* avança de 4 em 4 (em sistemas comuns), e um double* avança de 8 em 8. Isso permite que a aritmética de ponteiros seja independente do tipo, bastando que o ponteiro seja corretamente tipado.

Considere o exemplo a seguir, que usa um ponteiro void* para ilustrar a necessidade de conversão:

#include <stdio.h>

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

    // Ilegal: aritmética com void* não é permitida (GCC aceita como extensão)
    // vp++;  // erro: incremento de ponteiro para void

    // Correto: converter para o tipo apropriado
    int *ip = (int*)vp;
    ip++;
    printf("Valor: %d\n", *ip);  // imprime 2

    return 0;
}

Em C padrão, não é permitido fazer aritmética com ponteiros para void, pois o compilador não sabe o tamanho do tipo apontado. Por isso, é necessário converter para um tipo completo antes de realizar operações. Além disso, a aritmética de ponteiros funciona com qualquer tipo, incluindo structs:

#include <stdio.h>

struct Point {
    int x, y;
};

int main() {
    struct Point pts[] = {{1,2}, {3,4}, {5,6}};
    struct Point *p = pts;

    printf("Ponto: (%d, %d)\n", p->x, p->y);  // (1,2)
    p++;
    printf("Ponto: (%d, %d)\n", p->x, p->y);  // (3,4)

    return 0;
}

Neste caso, p++ avança o tamanho da struct (provavelmente 8 bytes, dependendo do alinhamento). Isso é útil para percorrer arrays de structs.

Comparação

Ponteiros podem ser comparados usando operadores relacionais (==, !=, <, >, <=, >=). A comparação é útil para verificar se dois ponteiros apontam para o mesmo local, ou para determinar a ordem relativa dentro de um array.

Quando dois ponteiros apontam para elementos do mesmo array, a comparação funciona como esperado: um ponteiro para um índice menor é considerado "menor" que um ponteiro para um índice maior. Comparar ponteiros de arrays diferentes é comportamento indefinido (embora em alguns sistemas funcione, não é portável).

#include <stdio.h>

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

    if (p1 < p2) {
        printf("p1 está antes de p2\n");
    }

    if (p1 == &arr[1]) {
        printf("p1 aponta para arr[1]\n");
    }

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

    return 0;
}

A comparação com NULL é uma prática comum para verificar se um ponteiro foi inicializado ou se uma alocação falhou. Além disso, é possível subtrair dois ponteiros do mesmo array para obter a distância (número de elementos) entre eles:

#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *p1 = &arr[1];
    int *p2 = &arr[4];

    ptrdiff_t diff = p2 - p1;  // resultado: 3
    printf("Diferença: %td\n", diff);  // imprime 3

    return 0;
}

O tipo ptrdiff_t (definido em <stddef.h>) é o tipo adequado para armazenar a diferença entre dois ponteiros.

Cuidados

A aritmética de ponteiros oferece grande poder, mas também exige atenção redobrada para evitar erros. Aqui estão os principais cuidados:

  • Nunca ultrapasse os limites do array: Incrementar um ponteiro além do último elemento ou decrementar antes do primeiro causa comportamento indefinido. Sempre verifique os limites.
  • Cuidado com ponteiros não inicializados: Usar um ponteiro sem inicialização pode levar a acessos a locais aleatórios. Sempre inicialize ponteiros com um endereço válido ou NULL.
  • Não misture ponteiros de diferentes arrays: Comparar ou subtrair ponteiros de arrays diferentes é indefinido. A aritmética só é válida dentro do mesmo bloco de memória.
  • Evite aritmética com void*: Em C padrão, não é permitido. Se precisar, converta para um tipo completo.
  • Prefira índices quando possível: Embora a aritmética de ponteiros seja eficiente, o uso de índices (arr[i]) é mais legível e seguro. Use ponteiros quando o desempenho for crítico ou quando estiver manipulando alocação dinâmica.

Outro cuidado importante é com o operador sizeof aplicado a ponteiros. sizeof(p) retorna o tamanho do ponteiro (4 ou 8 bytes), não o tamanho do array. Para obter o número de elementos de um array, use sizeof(arr)/sizeof(arr[0]).

#include <stdio.h>

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

    printf("Tamanho do ponteiro: %zu\n", sizeof(p));  // 4 ou 8
    printf("Número de elementos: %zu\n", sizeof(arr)/sizeof(arr[0]));  // 5

    return 0;
}

Boas práticas

Para usar aritmética de ponteiros de forma segura, siga estas recomendações:

  • Sempre mantenha um ponteiro "sentinela" que aponte para o início do array, caso precise reiniciar a navegação.
  • Use const quando o ponteiro não deve modificar os dados apontados.
  • Documente o código que usa aritmética de ponteiros, explicando os limites e a lógica.
  • Teste exaustivamente, especialmente com arrays de tamanho zero ou alocações dinâmicas.

Referências

Exercícios

  1. Escreva um programa que declare um array de inteiros de 5 elementos, inicialize-o com valores de sua escolha e use um ponteiro para imprimir todos os elementos usando aritmética de ponteiros (não use índices).

    ✓ Resposta:
    #include <stdio.h>
    
    int main() {
        int arr[] = {10, 20, 30, 40, 50};
        int *p = arr;
        for (int i = 0; i < 5; i++) {
            printf("%d ", *(p + i));
        }
        printf("\n");
        return 0;
    }
    
  2. Dado um array de doubles de 4 elementos, use um ponteiro para calcular a soma de todos os elementos. Imprima o resultado.

    ✓ Resposta:
    #include <stdio.h>
    
    int main() {
        double arr[] = {1.5, 2.3, 4.7, 0.9};
        double *p = arr;
        double soma = 0.0;
        for (int i = 0; i < 4; i++) {
            soma += *(p + i);
        }
        printf("Soma: %.2f\n", soma);
        return 0;
    }
    
  3. Escreva um programa que declare um array de caracteres (string) e use um ponteiro para percorrê-lo e contar quantas vogais (a, e, i, o, u) existem. Considere apenas minúsculas.

    ✓ Resposta:
    #include <stdio.h>
    
    int main() {
        char str[] = "hello world";
        char *p = str;
        int vogais = 0;
        while (*p != '\0') {
            char c = *p;
            if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u')
                vogais++;
            p++;
        }
        printf("Vogais: %d\n", vogais);
        return 0;
    }
    
  4. Dado um array de inteiros, use dois ponteiros para inverter o array in-place (sem usar um array auxiliar). Dica: um ponteiro aponta para o início, outro para o fim; troque os valores e mova os ponteiros.

    ✓ Resposta:
    #include <stdio.h>
    
    int main() {
        int arr[] = {1, 2, 3, 4, 5};
        int *inicio = arr;
        int *fim = arr + 4;  // aponta para o último elemento
        while (inicio < fim) {
            int temp = *inicio;
            *inicio = *fim;
            *fim = temp;
            inicio++;
            fim--;
        }
        // imprime array invertido
        for (int i = 0; i < 5; i++) {
            printf("%d ", arr[i]);
        }
        printf("\n");
        return 0;
    }
    
  5. Escreva uma função que recebe um array de inteiros e seu tamanho, e retorna a média dos valores usando aritmética de ponteiros (sem usar índices). A função deve ter protótipo: double media(int *arr, int n);

    ✓ Resposta:
    #include <stdio.h>
    
    double media(int *arr, int n) {
        int soma = 0;
        int *p = arr;
        for (int i = 0; i < n; i++) {
            soma += *(p + i);
        }
        return (double)soma / n;
    }
    
    int main() {
        int nums[] = {10, 20, 30, 40, 50};
        printf("Média: %.2f\n", media(nums, 5));
        return 0;
    }