Menu

Aritmética de ponteiros em C: somar, subtrair e caminhar pela memória

Somar 1 a um ponteiro não soma 1 byte - move para o próximo objeto daquele tipo. Esta página cobre ptr+1, incremento e decremento, diferenças entre ponteiros, comparações, percorrer um array com um ponteiro e a regra do um-além-do-fim.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

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ãoPonteiro depoisValor produzido
*p++avançadoo elemento antes do movimento
*++pavançadoo elemento depois do movimento
(*p)++inalteradoo 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.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR