Menu

Aritmética de punteros en C: sumar, restar y recorrer memoria

Sumar 1 a un puntero no suma 1 byte: lo mueve al siguiente objeto de ese tipo. Esta página cubre ptr+1, el incremento y el decremento, la diferencia entre punteros, las comparaciones, recorrer un array con un puntero y la regla del uno más allá del final.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Los punteros guardan direcciones, y las direcciones son números, así que es razonable esperar que la aritmética funcione con ellos. Funciona, pero no exactamente como la aritmética entera corriente. C escala cada operación con punteros según el tamaño de aquello a lo que apuntan, lo que convierte "suma uno" en el mucho más útil "muévete al siguiente elemento".

Esa única decisión de diseño es la razón por la que recorrer un array con un puntero es tan natural en C como recorrerlo con un índice.

Sumar 1 mueve un objeto, no un byte

Toma un puntero, súmale 1 e imprime ambas direcciones:

El int * salta 4 bytes, el double * salta 8, el char * salta 1: cada uno aterriza exactamente donde empezaría el siguiente objeto de su tipo. La regla es simple:

ptr + n  ==  (direccion en ptr) + n * sizeof(*ptr)

Esto es justamente por lo que un puntero tiene tipo. Una dirección desnuda sin tipo asociado no tendría idea de a qué distancia está "el siguiente".

Recorrer un array con un puntero

Ahora la recompensa. Los elementos de un array están en memoria consecutiva, así que avanzar un puntero los visita en orden:

Ambos bucles imprimen los mismos cinco números. En el segundo, scores decae a un puntero a su primer elemento, p++ avanza un int y *p lee el elemento que está ahí. Dispuesto en memoria:

 scores[0] scores[1] scores[2] scores[3] scores[4]
+---------+---------+---------+---------+---------+
|   88    |   92    |   75    |   60    |   100   |
+---------+---------+---------+---------+---------+
 ^         ^                                       ^
 p         p+1                                     scores+5
 (inicio)  (4 bytes despues)                       (uno mas alla del final)

La relación entre las dos formas es exacta y vale la pena memorizarla: scores[i] está definido como *(scores + i). Más sobre eso en punteros y arrays.

Incremento, decremento y la trampa de *p++

++ y -- funcionan sobre punteros con el mismo escalado.

La expresión que confunde a todo el mundo es *p++. El ++ sufijo se asocia más fuerte que el *, así que significa "toma el p actual, avanza p y luego desreferencia el valor viejo":

Tres expresiones, tres significados distintos:

ExpresiónPuntero despuésValor producido
*p++avanzadoel elemento anterior al movimiento
*++pavanzadoel elemento posterior al movimiento
(*p)++sin cambiosel valor viejo, y el elemento queda incrementado

*p++ es C idiomático —es como se escriben las copias de cadenas y búferes— pero pon los paréntesis cuando quieras decir cualquier otra cosa.

Restar dos punteros

Restar un puntero de otro da la cantidad de elementos entre ellos, no la cantidad de bytes:

El tipo del resultado es ptrdiff_t, un tipo entero con signo de <stddef.h>, que se imprime con %td. Restar punteros que apuntan a dos arrays distintos produce un resultado indefinido: el estándar solo define la operación dentro de un mismo objeto.

También puedes restarle un entero a un puntero (p - 3 retrocede tres elementos), pero no puedes sumar dos punteros. "La suma de dos direcciones" no nombra nada.

Comparar punteros

Los operadores relacionales funcionan con punteros dentro del mismo array, y significan exactamente lo que sugiere la disposición en memoria: p < q es verdadero cuando p direcciona un elemento anterior.

front < back es toda la historia de terminación del bucle, y es correcta porque ambos punteros se quedan dentro de un mismo array. == y != son también la forma natural de contrastar un bucle con su marca de fin, lo que nos lleva a la regla que hace legal todo esto.

La regla del uno más allá del final

C permite explícitamente formar un puntero a la posición una más allá del último elemento de un array. Puedes calcularlo, guardarlo y compararlo. No puedes desreferenciarlo.

int arr[4];

 arr[0]   arr[1]   arr[2]   arr[3]   (sin elemento)
+------+ +------+ +------+ +------+  +- - - -+
|      | |      | |      | |      |  |       |
+------+ +------+ +------+ +------+  +- - - -+
 ^                                    ^
 arr                                  arr + 4
 legal de usar                        legal de FORMAR y COMPARAR
                                      nunca legal de desreferenciar

Esa regla es lo que hace válida la forma estándar de bucle:

for (int *p = arr; p != arr + n; p++) {
    /* ... */
}

En la última iteración p pasa a ser arr + n, la comparación falla y el bucle sale, sin haber leído nunca esa posición.

Aun así, dos cosas siguen indefinidas. Formar un puntero dos posiciones más allá del final, o una antes del inicio, es comportamiento indefinido aunque nunca lo desreferencies:

int *bad1 = arr + n + 1;   // indefinido: demasiado lejos del final
int *bad2 = arr - 1;       // indefinido: antes del comienzo

Ese segundo caso importa en la práctica: un bucle hacia atrás escrito como for (int *p = arr + n - 1; p >= arr; p--) calcula arr - 1 en su último decremento. Funciona en todos los compiladores comunes y aun así es formalmente indefinido. El bucle hacia atrás limpio lo evita:

Punteros a void y la aritmética que no puedes hacer

Un void * guarda una dirección sin ningún tipo asociado, que es como malloc y qsort se mantienen genéricos. Como no hay tamaño de elemento, el estándar no permite aritmética de punteros sobre un void *: convierte antes a un tipo concreto (o a char * para trabajo a nivel de bytes).

void *v = buffer;
// v + 1;              // no es C estandar: no hay tamano por el que escalar
char *b = v;
b + 1;                 // bien: un byte hacia adelante

GCC y Clang aceptan la aritmética sobre void * como una extensión que lo trata como char *, así que el código que se apoya en ella compila hasta el día en que se construye en un lugar más estricto. Compila con -std=c17 -pedantic si quieres que te avisen.

Qué te da todo esto

La aritmética de punteros no es un truco de optimización al que recurres de vez en cuando: es el mecanismo que hay debajo de la indexación de arrays, del manejo de cadenas y de cada recorrido de búfer de la biblioteca estándar. strlen es un puntero avanzado hasta el '\0' terminador y restado del inicio. memcpy son dos punteros avanzando al mismo paso.

A continuación, mira cómo funciona realmente la equivalencia entre arrays y punteros, incluida la trampa de sizeof que atrapa a todos la primera vez que pasan un array a una función: punteros y arrays.

Preguntas frecuentes

¿Qué hace ptr + 1 en C?

Mueve el puntero hacia adelante un objeto, no un byte. Para un int * en una máquina típica eso son 4 bytes; para un double * son 8. El compilador multiplica por sizeof(*ptr) por ti, y por eso importa el tipo del puntero.

¿Se pueden restar dos punteros en C?

Sí, cuando ambos apuntan dentro del mismo array. end - start da la cantidad de elementos entre ellos, con tipo ptrdiff_t (se imprime con %td). Restar punteros a objetos sin relación es comportamiento indefinido.

¿Es legal apuntar uno más allá del final de un array?

Sí: formar un puntero a la posición siguiente al último elemento está permitido explícitamente para que bucles como for (int *p = arr; p != arr + n; p++) funcionen. Puedes calcular ese puntero y compararlo, pero desreferenciarlo es comportamiento indefinido.

¿Por qué *p++ no es lo mismo que (*p)++?

*p++ incrementa el puntero y desreferencia el valor viejo: lee el elemento actual y luego avanza. (*p)++ deja el puntero en paz e incrementa el valor al que apunta. El ++ sufijo se asocia más fuerte que el *, así que los paréntesis son lo que cambia el significado.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR