Aula 21 — Aritmética de Ponteiros

Avançar, recuar e comparar ponteiros — navegando pela memória com precisão

1. O que é Aritmética de Ponteiros?

Você já sabe que um ponteiro armazena um endereço de memória. O que torna os ponteiros em C especialmente poderosos é que você pode fazer contas com esses endereços — somar, subtrair e comparar — de forma inteligente e segura.

Aritmética de ponteiros é a capacidade de realizar operações aritméticas em ponteiros para navegar entre posições de memória. Ao contrário de somar bytes brutos, o compilador automaticamente escala o resultado pelo tamanho do tipo apontado — somar 1 a um int * avança 4 bytes, não 1.

Por que o compilador escala automaticamente?

Se um int ocupa 4 bytes e você tem um array int v[5], os elementos estão nos endereços:

Elemento Índice Endereço (exemplo) Offset em bytes
v[0]00x10000
v[1]10x10044
v[2]20x10088
v[3]30x100C12
v[4]40x101016

Ao escrever p + 1, o compilador sabe que deve avançar 1 × sizeof(int) = 4 bytes, e não apenas 1 byte. Isso é a escala automática — você pensa em elementos, não em bytes.

Operações válidas com ponteiros: p + n, p - n, p++, p--, p += n, p1 - p2 (distância entre dois ponteiros), e comparações p1 == p2, p1 < p2. Não são válidas: adição de dois ponteiros (p1 + p2), multiplicação ou divisão de ponteiros.

2. Incremento e Decremento: p++ e p--

As operações mais comuns em aritmética de ponteiros são o incremento (++) e o decremento (--), que avançam ou recuam o ponteiro por um elemento:

#include <stdio.h>

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

    printf("*p    = %d  (endereço %p)\n", *p, p);   // 10

    p++;                 // avança 4 bytes → aponta para v[1]
    printf("*p++  = %d  (endereço %p)\n", *p, p);   // 20

    p++;                 // avança mais 4 bytes → v[2]
    printf("*p++  = %d  (endereço %p)\n", *p, p);   // 30

    p--;                 // recua 4 bytes → v[1]
    printf("*p--  = %d  (endereço %p)\n", *p, p);   // 20

    return 0;
}
*p = 10 (endereço 0x7ffd1000)
*p++ = 20 (endereço 0x7ffd1004)
*p++ = 30 (endereço 0x7ffd1008)
*p-- = 20 (endereço 0x7ffd1004)

O tamanho do passo depende do tipo

Tipo do ponteiro Tamanho do tipo Passo de p++
char *1 byteavança 1 byte
short *2 bytesavança 2 bytes
int *4 bytesavança 4 bytes
double *8 bytesavança 8 bytes
struct Aluno *sizeof(Aluno)avança o tamanho da struct

3. Soma e Subtração com Inteiro

p + n produz um novo ponteiro que aponta n elementos à frente de p, sem mover p:

#include <stdio.h>

int main() {
    float notas[] = {6.5f, 7.0f, 8.5f, 9.0f, 5.5f};
    float *inicio = notas;             // aponta para notas[0]

    printf("notas[0] = %.1f\n", *inicio);
    printf("notas[2] = %.1f\n", *(inicio + 2));  // avança 2×4 = 8 bytes
    printf("notas[4] = %.1f\n", *(inicio + 4));  // avança 4×4 = 16 bytes

    /* inicio ainda aponta para notas[0] — não foi modificado */
    printf("inicio ainda = %.1f\n", *inicio);

    /* Navegando com += */
    float *p = notas;
    p += 3;                            // agora aponta para notas[3]
    printf("após p+=3: %.1f\n", *p);

    return 0;
}
notas[0] = 6.5
notas[2] = 8.5
notas[4] = 5.5
inicio ainda = 6.5
após p+=3: 9.0
Equivalência fundamental: *(p + n) é exatamente equivalente a p[n]. O compilador gera o mesmo código para ambas as notações. A notação de colchetes é apenas açúcar sintático — por baixo, é sempre aritmética de ponteiros.
int v[] = {10, 20, 30, 40, 50};
int *p = v;

/* Estas quatro formas são idênticas — geram o mesmo código */
printf("%d\n", v[2]);       // notação de array com nome
printf("%d\n", p[2]);       // notação de array com ponteiro
printf("%d\n", *(v + 2));   // aritmética com nome do array
printf("%d\n", *(p + 2));   // aritmética com ponteiro

4. Subtração entre Ponteiros — Distância em Elementos

Quando você subtrai dois ponteiros do mesmo tipo que apontam para posições dentro do mesmo array, o resultado é a distância em número de elementos (não em bytes) entre eles:

#include <stdio.h>

int main() {
    int v[] = {10, 20, 30, 40, 50};
    int *inicio = &v[0];
    int *fim    = &v[4];

    printf("fim - inicio = %ld elementos\n", fim - inicio);  // 4
    printf("distância em bytes: %ld\n",
           (char*)fim - (char*)inicio);                       // 16

    /* Encontrar posição de um elemento */
    int *p = &v[2];
    printf("v[2] está na posição %ld do array\n", p - v);    // 2

    return 0;
}
fim - inicio = 4 elementos
distância em bytes: 16
v[2] está na posição 2 do array
⚠️ Subtrair ponteiros de arrays diferentes é comportamento indefinido. A subtração p1 - p2 só é válida e significativa quando os dois ponteiros apontam para elementos do mesmo array (ou para uma posição além do último elemento). Subtrair ponteiros de variáveis não relacionadas pode produzir qualquer resultado.

5. Comparação de Ponteiros

Ponteiros do mesmo array podem ser comparados com ==, !=, <, >, <=, >=. Um ponteiro menor aponta para uma posição anterior na memória:

#include <stdio.h>

int main() {
    int v[] = {10, 20, 30, 40, 50};
    int *p = &v[1];
    int *q = &v[3];

    printf("p < q? %s\n",  p < q  ? "sim" : "não");   // sim
    printf("p == q? %s\n", p == q ? "sim" : "não");   // não

    /* Percorrer array com dois ponteiros convergindo */
    int *esq = v;
    int *dir = v + 4;

    printf("\nPercurso convergente: ");
    while (esq <= dir) {
        printf("%d ", *esq);
        esq++;
    }
    printf("\n");

    return 0;
}
p < q? sim
p == q? não

Percurso convergente: 10 20 30 40 50

Comparar com NULL — o teste mais importante

int *p = NULL;

if (p == NULL)
    printf("ponteiro inválido — não use!\n");
else
    printf("valor: %d\n", *p);    // só desreferencia se não for NULL

6. Percorrendo Arrays com Ponteiros

Uma das aplicações mais comuns de aritmética de ponteiros é percorrer arrays de forma eficiente — especialmente em funções que recebem arrays como parâmetros:

Estilo clássico com índice vs. estilo com ponteiro

Com índice
void imprimirV1(int v[], int n) {
    for (int i = 0; i < n; i++)
        printf("%d ", v[i]);
    printf("\n");
}
Com ponteiro
void imprimirV2(int *p, int n) {
    int *fim = p + n;
    while (p < fim)
        printf("%d ", *p++);
    printf("\n");
}

Ambas produzem o mesmo resultado. A versão com ponteiro usa *p++: primeiro lê *p (desreferencia) e depois incrementa o ponteiro — tudo numa única expressão.

Sobre *p++ em contexto de loop: Aqui o comportamento é bem definido. *p++ lê o valor em p e avança p para o próximo elemento. É o idioma canônico para percorrer arrays em C e é amplamente usado em código C de alto desempenho.

Exemplo completo: operações com ponteiros em array

#include <stdio.h>

/* Soma todos os elementos usando aritmética de ponteiros */
int somarArray(const int *p, int n) {
    int soma = 0;
    const int *fim = p + n;
    while (p < fim)
        soma += *p++;
    return soma;
}

/* Encontra o maior elemento, retorna ponteiro para ele */
int *encontrarMaior(int *p, int n) {
    int *maior = p;
    int *fim   = p + n;
    p++;
    while (p < fim) {
        if (*p > *maior)
            maior = p;
        p++;
    }
    return maior;
}

/* Inverte o array no próprio lugar usando dois ponteiros */
void inverter(int *esq, int *dir) {
    while (esq < dir) {
        int temp = *esq;
        *esq = *dir;
        *dir = temp;
        esq++;
        dir--;
    }
}

int main() {
    int v[] = {3, 7, 1, 9, 4, 6, 2};
    int n = 7;

    printf("Array:  ");
    for (int i = 0; i < n; i++) printf("%d ", v[i]);
    printf("\n");

    printf("Soma:   %d\n", somarArray(v, n));

    int *pm = encontrarMaior(v, n);
    printf("Maior:  %d (posição %ld)\n", *pm, pm - v);

    inverter(v, v + n - 1);
    printf("Invertido: ");
    for (int i = 0; i < n; i++) printf("%d ", v[i]);
    printf("\n");

    return 0;
}
Array: 3 7 1 9 4 6 2
Soma: 32
Maior: 9 (posição 3)
Invertido: 2 6 4 9 1 7 3

7. Aritmética de Ponteiros em Strings

Strings em C são arrays de char terminados com '\0'. A aritmética de ponteiros é a base de praticamente todas as operações com strings na linguagem:

#include <stdio.h>

/* Calcula o comprimento de uma string sem usar strlen */
int meuStrlen(const char *s) {
    const char *p = s;
    while (*p != '\0')   // avança até o terminador nulo
        p++;
    return p - s;        // distância em caracteres
}

/* Copia uma string sem usar strcpy */
void meuStrcpy(char *dest, const char *src) {
    while ((*dest++ = *src++) != '\0')  // copia e avança os dois ponteiros
        ;
}

int main() {
    char saudacao[] = "Olá, C!";
    printf("String:     %s\n",  saudacao);
    printf("Comprimento: %d\n", meuStrlen(saudacao));

    char copia[20];
    meuStrcpy(copia, saudacao);
    printf("Cópia:      %s\n",  copia);

    /* Avançando manualmente em uma string */
    const char *p = "abcdef";
    printf("\nCaracteres: ");
    while (*p)                    // *p != '\0' pode ser escrito como *p
        printf("%c ", *p++);
    printf("\n");

    /* Pulando para a segunda palavra */
    const char *frase = "Bom dia!";
    const char *q = frase;
    while (*q != ' ') q++;        // avança até o espaço
    q++;                           // pula o espaço
    printf("Segunda palavra: %s\n", q);

    return 0;
}
String: Olá, C!
Comprimento: 7
Cópia: Olá, C!

Caracteres: a b c d e f
Segunda palavra: dia!
O idioma while (*p): Como o terminador nulo '\0' tem valor inteiro 0 (falso), while (*p) é exatamente equivalente a while (*p != '\0'). É o padrão mais compacto para percorrer strings em C e aparece em praticamente toda biblioteca de string.

8. Limites e Cuidados

Aritmética de ponteiros é poderosa, mas silenciosamente perigosa quando usada fora dos limites válidos:

Situação Status Consequência
p aponta para v[0] a v[n-1] ✅ Válido Acesso normal
p = v + n (um além do último) ✅ Válido como endereço Pode ser comparado, mas não desreferenciado
p = v - 1 (antes do início) ❌ Comportamento indefinido Pode corromper dados adjacentes
p = v + n + 1 (dois além do último) ❌ Comportamento indefinido Acessa memória fora do array
Subtrair ponteiros de arrays diferentes ❌ Comportamento indefinido Resultado sem sentido
int v[5] = {10, 20, 30, 40, 50};
int *p;

/* ✅ Válido — percorrer com ponteiro sentinela (v+5) */
for (p = v; p < v + 5; p++)
    printf("%d ", *p);

/* ❌ INVÁLIDO — desreferenciar v+5 é comportamento indefinido */
p = v + 5;
printf("%d\n", *p);   // acessa memória além do array!

/* ❌ INVÁLIDO — ponteiro antes do início */
p = v - 1;
printf("%d\n", *p);   // comportamento indefinido
⚠️ C não verifica limites automaticamente. Ao contrário de Java, que lança ArrayIndexOutOfBoundsException, C silenciosamente acessa memória fora do array. O resultado pode ser um valor errado, corrupção de outra variável, ou segmentation fault — dependendo de onde a memória acessada está mapeada.

9. Ponteiro para Ponteiro (**)

Um ponteiro pode apontar para outro ponteiro. Isso é usado em funções que precisam modificar o próprio ponteiro do chamador (não apenas o valor apontado), e em arrays de strings:

#include <stdio.h>
#include <stdlib.h>

/* Aloca um array e faz o ponteiro do chamador apontar para ele */
void alocarArray(int **pp, int n) {
    *pp = (int *) malloc(n * sizeof(int));   // modifica o ponteiro original
    if (*pp == NULL) return;
    for (int i = 0; i < n; i++)
        (*pp)[i] = (i + 1) * 10;
}

int main() {
    int *vetor = NULL;

    alocarArray(&vetor, 5);   // passa &vetor → função recebe int **

    if (vetor != NULL) {
        printf("Array alocado: ");
        for (int i = 0; i < 5; i++)
            printf("%d ", vetor[i]);
        printf("\n");
        free(vetor);
    }

    /* Array de strings — cada elemento é um char* */
    const char *dias[] = {
        "Segunda", "Terça", "Quarta",
        "Quinta",  "Sexta", "Sábado", "Domingo"
    };
    int total = 7;

    printf("\nDias da semana:\n");
    for (int i = 0; i < total; i++)
        printf("  dias[%d] = %s\n", i, dias[i]);

    return 0;
}
Array alocado: 10 20 30 40 50

Dias da semana:
  dias[0] = Segunda
  dias[1] = Terça
  dias[2] = Quarta
  dias[3] = Quinta
  dias[4] = Sexta
  dias[5] = Sábado
  dias[6] = Domingo
Expressão Tipo Significado
int x int Variável inteira
int *p int * Ponteiro para int
int **pp int ** Ponteiro para ponteiro para int
*pp int * O ponteiro apontado por pp
**pp int O int apontado pelo ponteiro apontado por pp

10. C vs. Java — Aritmética de Ponteiros

Aspecto C Java
Aritmética de ponteiros ✅ Totalmente suportada — p++, p+n, p1-p2 ❌ Não existe — Java não expõe endereços de memória
Acesso a array por índice v[i] equivale a *(v+i) v[i] — sem equivalência com ponteiros
Verificação de limites ❌ Nenhuma — o programador é responsável ✅ ArrayIndexOutOfBoundsException automático
Percorrer string while (*p++) com ponteiro de char for (char c : str.toCharArray())
Distância entre elementos p2 - p1 em número de elementos Não aplicável — sem aritmética de referências

Side-by-side: percorrer array

C — com aritmética de ponteiros
int v[] = {1, 2, 3, 4, 5};
int *p = v;
int *fim = v + 5;

while (p < fim)
    printf("%d ", *p++);
Java — sem acesso a endereços
int[] v = {1, 2, 3, 4, 5};

for (int x : v)
    System.out.print(x + " ");

// Java abstrai completamente
// a posição na memória

Quiz — Aula 21

1. Se p é um int * apontando para o endereço 0x1000, qual é o endereço de p + 2?

2. Qual é a saída?

int v[] = {10, 20, 30, 40, 50};
int *p = v + 1;
printf("%d %d", *p, *(p + 2));

3. Dado int v[5] com int *p = &v[1] e int *q = &v[4], qual é o valor de q - p?

4. As expressões v[3] e *(v + 3) para um array int v[] são:

5. Qual é a saída?

int v[] = {5, 10, 15, 20};
int *p = v;
p += 2;
printf("%d", *p);

6. Em C, o que acontece se você desreferenciar v + n onde n é o tamanho do array (ou seja, uma posição além do último elemento)?

7. A função abaixo conta o comprimento de uma string usando aritmética de ponteiros. O que ela retorna para "hello"?

int len(const char *s) {
    const char *p = s;
    while (*p) p++;
    return p - s;
}

8. Por que a aritmética de ponteiros não existe em Java?