Ponteiros guardam endereços, e endereços são números - então é razoável esperar que a aritmética funcione sobre eles. Ela funciona, mas não exatamente como a aritmética comum de inteiros. C escala toda operação de ponteiro pelo tamanho da coisa apontada, o que transforma "some um" no bem mais útil "vá para o próximo elemento".
Essa única decisão de projeto é por que percorrer um array com um ponteiro é tão natural em C quanto percorrê-lo com um índice.
Somar 1 anda um objeto, não um byte
Pegue um ponteiro, some 1 e imprima os dois endereços:
O int * salta 4 bytes, o double * salta 8, o char * salta 1 - cada um cai exatamente onde o próximo objeto do seu tipo começaria. A regra é simples:
ptr + n == (endereço em ptr) + n * sizeof(*ptr)
É exatamente por isso que um ponteiro tem um tipo. Um endereço nu, sem tipo algum atrelado, não teria ideia de quão longe fica o "próximo".
Percorrendo um array por ponteiro
Agora a recompensa. Os elementos de um array ficam em memória consecutiva, então avançar um ponteiro os visita em ordem:
Os dois laços imprimem os mesmos cinco números. No segundo, scores decai para um ponteiro ao seu primeiro elemento, p++ avança um int, e *p lê o elemento que está lá. Dispostos na memória:
scores[0] scores[1] scores[2] scores[3] scores[4]
+---------+---------+---------+---------+---------+
| 88 | 92 | 75 | 60 | 100 |
+---------+---------+---------+---------+---------+
^ ^ ^
p p+1 scores+5
(início) (4 bytes depois) (um além do fim)
A relação entre as duas formas é exata e vale memorizar: scores[i] é definido como *(scores + i). Mais sobre isso em ponteiros e arrays.
Incremento, decremento e a armadilha do *p++
++ e -- funcionam em ponteiros com a mesma escala.
A expressão que confunde todo mundo é *p++. O ++ pós-fixo se associa mais forte que o *, então ela significa "pegue o p atual, avance p, depois desreferencie o valor antigo":
Três expressões, três significados diferentes:
| Expressão | Ponteiro depois | Valor produzido |
|---|---|---|
*p++ | avançado | o elemento antes do movimento |
*++p | avançado | o elemento depois do movimento |
(*p)++ | inalterado | o valor antigo, e o elemento é incrementado |
*p++ é C idiomático - é assim que cópias de strings e buffers são escritas - mas escreva os parênteses quando quiser dizer qualquer outra coisa.
Subtraindo dois ponteiros
Subtrair um ponteiro de outro dá o número de elementos entre eles, não o número de bytes:
O tipo do resultado é ptrdiff_t, um tipo inteiro com sinal de <stddef.h>, impresso com %td. Subtrair ponteiros de dois arrays diferentes produz um resultado indefinido - o padrão só define a operação dentro de um mesmo objeto.
Você também pode subtrair um inteiro de um ponteiro (p - 3 volta três elementos), mas não pode somar dois ponteiros. "A soma de dois endereços" não nomeia nada.
Comparando ponteiros
Os operadores relacionais funcionam em ponteiros para dentro do mesmo array, e significam exatamente o que o arranjo da memória sugere: p < q é verdadeiro quando p endereça um elemento anterior.
front < back é toda a história do término desse laço, e está correta porque os dois ponteiros permanecem dentro de um mesmo array. == e != também são a forma natural de testar um laço contra seu marcador de fim, o que nos leva à regra que torna tudo isso legal.
A regra do um-além-do-fim
C permite explicitamente que você forme um ponteiro para a posição um além do último elemento de um array. Você pode calculá-lo, guardá-lo e compará-lo. Você não pode desreferenciá-lo.
int arr[4];
arr[0] arr[1] arr[2] arr[3] (sem elemento)
+------+ +------+ +------+ +------+ +- - - -+
| | | | | | | | | |
+------+ +------+ +------+ +------+ +- - - -+
^ ^
arr arr + 4
legal usar legal FORMAR e COMPARAR
nunca legal desreferenciar
Essa regra é o que torna válido o formato padrão de laço:
for (int *p = arr; p != arr + n; p++) {
/* ... */
}
Na última iteração p vira arr + n, a comparação falha e o laço sai - sem nunca ler aquela posição.
Duas coisas continuam indefinidas mesmo assim. Formar um ponteiro dois além do fim, ou um antes do início, é comportamento indefinido mesmo que você nunca o desreferencie:
int *bad1 = arr + n + 1; // indefinido - longe demais além do fim
int *bad2 = arr - 1; // indefinido - antes do começo
Esse segundo importa na prática: um laço para trás escrito como for (int *p = arr + n - 1; p >= arr; p--) calcula arr - 1 no seu decremento final. Funciona em todo compilador comum e ainda assim é formalmente indefinido. O laço para trás limpo evita isso:
Ponteiros void e a aritmética que você não pode fazer
Um void * guarda um endereço sem tipo atrelado, e é assim que malloc e qsort se mantêm genéricos. Como não há tamanho de elemento, aritmética de ponteiros sobre um void * não é permitida pelo padrão - converta para um tipo concreto (ou para char * para trabalho byte a byte) primeiro.
void *v = buffer;
// v + 1; // não é C padrão - não há tamanho para escalar
char *b = v;
b + 1; // ok - um byte adiante
GCC e Clang aceitam aritmética sobre void * como uma extensão que a trata como char *, então código que depende disso compila até o dia em que é construído em algum lugar mais estrito. Compile com -std=c17 -pedantic se quiser ser avisado.
O que isso te dá
Aritmética de ponteiros não é um truque de otimização ao qual você recorre de vez em quando - é o mecanismo por baixo da indexação de arrays, do tratamento de strings e de toda travessia de buffer da biblioteca padrão. strlen é um ponteiro avançado até o '\0' terminador e subtraído do início. memcpy são dois ponteiros avançando em sincronia.
A seguir, veja como a equivalência array/ponteiro realmente funciona, incluindo a armadilha do sizeof que pega todo mundo na primeira vez que passa um array para uma função: ponteiros e arrays.
Perguntas frequentes
O que ptr + 1 faz em C?
Ele avança o ponteiro em um objeto, não um byte. Para um int * em uma máquina típica isso são 4 bytes; para um double * são 8. O compilador multiplica por sizeof(*ptr) para você, e é por isso que o tipo do ponteiro importa.
Dá para subtrair dois ponteiros em C?
Sim, quando os dois apontam para dentro do mesmo array. end - start dá o número de elementos entre eles, com o tipo ptrdiff_t (imprima com %td). Subtrair ponteiros de objetos não relacionados é comportamento indefinido.
É legal apontar para uma posição além do fim de um array?
Sim - formar um ponteiro para um elemento depois do último é explicitamente permitido para que laços como for (int *p = arr; p != arr + n; p++) funcionem. Você pode calcular e comparar esse ponteiro, mas desreferenciá-lo é comportamento indefinido.
Por que *p++ não é o mesmo que (*p)++?
*p++ incrementa o ponteiro e desreferencia o valor antigo - ele lê o elemento atual e depois avança. (*p)++ deixa o ponteiro em paz e incrementa o valor para o qual ele aponta. O ++ pós-fixo se associa mais forte que o *, então os parênteses são o que muda o significado.