A aritmética de ponteiros é um dos recursos mais poderosos e ao mesmo tempo perigosos da linguagem C. Ela permite que você realize operações aritméticas diretamente sobre endereços de memória, possibilitando navegar por arrays, percorrer estruturas de dados e manipular memória de forma eficiente. No entanto, seu uso incorreto pode levar a erros sutis e crashes. Nesta aula, vamos explorar os conceitos fundamentais da aritmética de ponteiros, incluindo incremento, decremento, relação com tipos, comparação e os cuidados necessários para evitar armadilhas.

Incremento e decremento

Quando você incrementa um ponteiro (com ++), ele não simplesmente adiciona 1 ao endereço. Em vez disso, ele avança para o próximo objeto do tipo apontado. O deslocamento real em bytes é igual a sizeof(tipo_apontado). Por exemplo, se você tem um ponteiro para int (que geralmente ocupa 4 bytes), incrementá-lo adiciona 4 ao endereço. Isso permite navegar por 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); // 10

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

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

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

    return 0;
}

O operador de decremento (--) funciona de forma análoga, movendo o ponteiro para o elemento anterior. Tanto a pré-incremento/decremento quanto a pós-incremento/decremento são válidos, com a mesma semântica de precedência que em variáveis comuns.

Relação com tipos

O tamanho do passo na aritmética de ponteiros depende inteiramente do tipo base do ponteiro. Se você tem um ponteiro para char, incrementar adiciona 1 byte; para double, adiciona 8 bytes (em sistemas típicos). Isso é crucial para percorrer arrays de diferentes tipos corretamente.

#include <stdio.h>

int main() {
    char c_arr[] = "abc";
    char *cp = c_arr;
    printf("char: %p -> %p (incremento %ld byte)\n", cp, cp+1, (long)((char*)(cp+1) - (char*)cp));

    int i_arr[] = {1,2,3};
    int *ip = i_arr;
    printf("int: %p -> %p (incremento %ld bytes)\n", ip, ip+1, (long)((char*)(ip+1) - (char*)ip));

    double d_arr[] = {1.0,2.0};
    double *dp = d_arr;
    printf("double: %p -> %p (incremento %ld bytes)\n", dp, dp+1, (long)((char*)(dp+1) - (char*)dp));

    return 0;
}

Outro ponto importante: a aritmética de ponteiros só faz sentido quando o ponteiro aponta para um elemento de um array (ou para uma posição após o último elemento, para fins de comparação). Operar sobre ponteiros que apontam para variáveis isoladas pode resultar em comportamento indefinido.

Comparação

Ponteiros podem ser comparados usando operadores relacionais (==, !=, <, >, <=, >=). A comparação é feita com base nos endereços absolutos (como inteiros sem sinal). Isso é útil para verificar se um ponteiro está dentro dos limites de um array, por exemplo.

#include <stdio.h>

int main() {
    int arr[5] = {0,1,2,3,4};
    int *p = arr;
    int *end = arr + 5; // um após o último

    while (p < end) {
        printf("%d ", *p);
        p++;
    }
    printf("\n");

    // Comparação de igualdade
    int *q = &arr[2];
    if (q == arr + 2) {
        printf("q aponta para arr[2]\n");
    }

    return 0;
}

É importante notar que a comparação de ponteiros só é bem definida se ambos apontarem para o mesmo array (ou para uma posição após o último). Comparar ponteiros de arrays diferentes ou com variáveis isoladas pode levar a comportamento indefinido.

Cuidados

A aritmética de ponteiros oferece grande poder, mas também exige disciplina. Aqui estão os principais cuidados:

  • Nunca desreferencie um ponteiro que não aponte para memória válida. Isso inclui ponteiros nulos, ponteiros que ultrapassaram os limites do array, ou ponteiros que foram liberados (dangling pointers).
  • Evite ultrapassar os limites do array. Incrementar um ponteiro além do último elemento e depois desreferenciá-lo é comportamento indefinido. Você pode, no entanto, manter um ponteiro apontando para “um após o último” para fins de comparação, mas nunca desreferenciá-lo.
  • Cuidado com a precedência de operadores. Por exemplo, *p++ é interpretado como *(p++), ou seja, desreferencia o valor atual e depois incrementa o ponteiro. Use parênteses quando necessário.
  • Não misture tipos de ponteiros diferentes em aritmética. A aritmética depende do tipo base; usar um ponteiro void* não permite aritmética (em C padrão, não é permitido incrementar void*).
  • Lembre-se de que arrays decaem para ponteiros. O nome de um array é um ponteiro constante para o primeiro elemento. Você pode usar aritmética no nome do array (ex.: arr + i), mas não pode atribuir a ele.
#include <stdio.h>

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

    // Cuidado: p pode ultrapassar o array
    p += 5; // agora p aponta para fora do array
    // *p = 10; // ERRO: comportamento indefinido

    // Precedência: *p++ vs (*p)++
    p = arr;
    int x = *p++; // x = 1, p agora aponta para arr[1]
    printf("x = %d, *p = %d\n", x, *p);

    p = arr;
    (*p)++; // incrementa o valor apontado (arr[0] vira 2)
    printf("arr[0] = %d\n", arr[0]);

    return 0;
}

Seguindo essas boas práticas, você poderá usar aritmética de ponteiros com segurança e eficiência.

Referências

Exercícios

  1. Escreva uma função que recebe um array de inteiros e seu tamanho, e imprime todos os elementos usando aritmética de ponteiros (sem usar índices).
  2. ✓ Resposta:
    #include <stdio.h>
    
    void print_array(int *arr, int size) {
        int *end = arr + size;
        for (int *p = arr; p < end; p++) {
            printf("%d ", *p);
        }
        printf("\n");
    }
    
    int main() {
        int nums[] = {10, 20, 30, 40};
        print_array(nums, 4);
        return 0;
    }
    
  3. Explique o que acontece se você incrementar um ponteiro para void em C padrão. Isso é permitido? Justifique.
  4. ✓ Resposta:

    Em C padrão (C89, C99, C11, etc.), não é permitido realizar aritmética em ponteiros para void, pois o compilador não sabe o tamanho do tipo apontado. O operador ++ ou + com void* resulta em erro de compilação. Alguns compiladores (como GCC) permitem como extensão, tratando como tamanho 1 (como se fosse char*), mas isso não é portável.

  5. Dado o código a seguir, qual será a saída? Explique passo a passo.
  6. #include <stdio.h>
    int main() {
        int arr[3] = {1, 2, 3};
        int *p = arr;
        printf("%d\n", *p++);
        printf("%d\n", *p);
        printf("%d\n", (*p)++);
        printf("%d\n", *p);
        return 0;
    }
    

    ✓ Resposta:

    Saída:
    1
    2
    2
    3

    Explicação:
    1. *p++ é *(p++): desreferencia p (que aponta para arr[0] = 1) e depois incrementa p para apontar para arr[1]. Imprime 1.
    2. *p agora imprime o valor em arr[1] = 2.
    3. (*p)++ incrementa o valor apontado por p (arr[1] de 2 para 3), mas retorna o valor original (2). Imprime 2.
    4. *p imprime o novo valor de arr[1] = 3.

  7. Escreva um programa que verifique se um ponteiro está dentro dos limites de um array (inclusive no elemento um após o último) usando comparação de ponteiros.
  8. ✓ Resposta:
    #include <stdio.h>
    #include <stdbool.h>
    
    bool is_within(int *p, int *start, int *end) {
        // start aponta para o primeiro, end para um após o último
        return (p >= start && p < end);
    }
    
    int main() {
        int arr[5] = {0,1,2,3,4};
        int *first = arr;
        int *last = arr + 5;
    
        int *p = &arr[2];
        printf("p dentro? %s\n", is_within(p, first, last) ? "sim" : "não");
    
        p = arr + 5; // um após o último
        printf("p (end) dentro? %s\n", is_within(p, first, last) ? "sim" : "não"); // não
    
        p = arr - 1; // antes do início
        printf("p (antes) dentro? %s\n", is_within(p, first, last) ? "sim" : "não"); // não
    
        return 0;
    }
    
  9. Qual é a diferença entre p++ e p + 1? Dê um exemplo onde a diferença é importante.
  10. ✓ Resposta:

    p++ incrementa o ponteiro e retorna o valor original (efeito colateral). Já p + 1 apenas calcula um novo endereço sem modificar p. Exemplo:
    int arr[] = {10,20}; int *p = arr; int x = *p++; // x=10, p agora aponta para arr[1]
    Se usássemos int x = *(p + 1);, p não seria alterado e x receberia 20.