Aritmética de punteros: sumar en unidades de tipo
Por qué sumar 1 a un puntero avanza el tamaño del tipo apuntado, qué significa restar dos punteros y cuáles son las reglas exactas que no puedes violar sin caer en comportamiento indefinido.
La aritmética de punteros es la operación que convierte una dirección aislada en la capacidad de recorrer estructuras enteras. Su regla central desconcierta al principio y es evidente después: sumar 1 no avanza un byte, avanza un elemento. Detrás de esa decisión de diseño hay una idea potente —el puntero mide en unidades de su propio tipo— y un conjunto de reglas estrictas que separan el código correcto del que solo parece funcionar.
- Entender por qué
p + 1avanzasizeofbytes del tipo apuntado. - Calcular y usar la diferencia de dos punteros con
ptrdiff_t. - Conocer el rango legal de un puntero, incluida la posición uno más allá del final.
- Reconocer qué operaciones aritméticas están prohibidas y por qué.
Sumar uno avanza un elemento, no un byte
La regla es que la aritmética de punteros se expresa en unidades del tipo apuntado. Si p es un int * y un int ocupa cuatro bytes, entonces p + 1 es la dirección de p más cuatro. El compilador escala automáticamente por sizeof del tipo apuntado.
#include <stdio.h>
int main(void)
{
int nums[4] = { 10, 20, 30, 40 };
int *pi = nums;
char *pc = (char *)nums;
printf("%p\n", (void *)(pi + 1)); // avanza 4 bytes
printf("%p\n", (void *)(pc + 1)); // avanza 1 byte
}
Que la escala dependa del tipo no es un capricho: es lo que hace que la indexación de arrays funcione. La expresión nums[i] está definida en el estándar como una abreviatura exacta de *(nums + i), y esa suma solo aterriza en el elemento correcto si se escala por el tamaño del elemento.
De esa definición se sigue una curiosidad que suele citarse como broma y que en realidad es una demostración: como la suma es conmutativa, nums[i] y i[nums] son literalmente la misma expresión para el compilador. No lo escribas nunca en código real, pero entiende lo que revela: los corchetes no son una operación de array, son azúcar sobre aritmética de punteros.
La diferencia de punteros y ptrdiff_t
La resta entre dos punteros al mismo array no da bytes: da cuántos elementos los separan. Es la operación inversa de la suma y respeta la misma escala.
#include <stdio.h>
#include <stddef.h>
int main(void)
{
double datos[8];
double *inicio = datos;
double *fin = datos + 8; // uno mas alla del final: legal
ptrdiff_t n = fin - inicio; // 8, no 64
printf("%td elementos\n", n);
}
El tipo del resultado es ptrdiff_t, definido en stddef.h. Es un entero con signo, porque la diferencia puede ser negativa si restas en el otro orden, y se imprime con el especificador %td. Usar int para almacenar una diferencia es un error clásico en cuanto los arrays superan el rango de int.
Hay dos matices que separan al que memoriza del que entiende. El primero: restar punteros que no pertenecen al mismo objeto es comportamiento indefinido, aunque numéricamente el resultado parezca razonable. El segundo: la resta exige que ambos punteros sean del mismo tipo, porque si no la unidad de medida sería ambigua.
Las reglas que hay que respetar
La aritmética de punteros solo está definida dentro de un objeto, y con una única excepción cuidadosamente diseñada. Estas son las fronteras.
Rango legal
Para un array de n elementos, los punteros válidos van del elemento 0 al elemento n. Ese último es la posición uno más allá del final.
Uno más allá, sí; dos, no
Puedes formar y comparar el puntero al final, pero desreferenciarlo es comportamiento indefinido. Formar p + n + 1 ya es ilegal.
Mismo objeto
Restar o comparar con < y > solo está definido entre punteros al mismo array. Entre objetos distintos, ni la resta ni el orden tienen sentido.
Operaciones prohibidas
No existe suma de dos punteros, ni multiplicación, ni división. Tampoco aritmética sobre void * ni sobre punteros a función en C estándar.
La regla del “uno más allá del final” es la más elegante del conjunto, y existe por una razón muy concreta: hace que el idioma de recorrido con condición de parada sea legal. Sin ella, el bucle canónico de C sería comportamiento indefinido en su última iteración.
Merece un apunte la prohibición sobre void *. Como ese tipo no tiene lente, no hay unidad por la que escalar la suma, así que la aritmética sobre él no está definida en C estándar. GCC y Clang la admiten como extensión tratándolo como si fuera char *, y ese es exactamente el problema: compila sin queja, se cuela en el código y deja de compilar en cuanto activas -pedantic o cambias de compilador. La forma portable es convertir explícitamente antes de operar.
void *base = obtener_buffer();
// unsigned char *p = base + 16; // extension, no C estandar
unsigned char *p = (unsigned char *)base + 16; // portable y explicito
El error más frecuente en este terreno no está en el puntero sino en el índice. Mezclar size_t —sin signo— con enteros con signo en la misma expresión provoca conversiones implícitas que convierten un valor negativo en un número enorme. Un p + (i - 1) con i de tipo size_t e i valiendo cero no retrocede una posición: salta a la otra punta del espacio de direcciones. Usa size_t para tamaños e índices, ptrdiff_t para diferencias, y nunca los combines sin pensar en la promoción que aplicará el compilador.
flowchart LR E0[Elemento 0] --> E1[Elemento 1] --> E2[Elemento 2] --> E3[Elemento 3] E3 --> FIN[Uno mas alla: formar y comparar si, desreferenciar no] FIN --> MAL[Dos mas alla: comportamiento indefinido al formarlo] style FIN fill:#f9e2af,color:#11111b style MAL fill:#f38ba8,color:#11111b
Recorrer con punteros frente a índices
Con las reglas claras, el recorrido idiomático de C se lee solo. Compara las dos formas de sumar un array:
#include <stddef.h>
// Version con indices: clara y perfectamente idiomatica.
long suma_indices(const int *v, size_t n)
{
long total = 0;
for (size_t i = 0; i < n; i++)
total += v[i];
return total;
}
// Version con punteros: la condicion usa el puntero uno mas alla.
long suma_punteros(const int *v, size_t n)
{
long total = 0;
for (const int *p = v; p != v + n; p++)
total += *p;
return total;
}
Nótese que el bucle con punteros forma v + n y lo compara, pero nunca lo desreferencia: exactamente lo que la regla permite. Obsérvese también que la condición usa != y no <. Con arrays contiguos ambas funcionan, pero la desigualdad es la forma canónica porque solo exige que los punteros sean comparables por igualdad, no que exista un orden entre ellos. Es el mismo criterio que hace que los iteradores de otros lenguajes se comparen así, y no es casualidad: heredaron el idioma de aquí. Sobre rendimiento conviene bajar las expectativas: con optimizaciones activadas ambos suelen generar el mismo código máquina. La razón para dominar la forma con punteros no es la velocidad, sino que es la única disponible cuando recorres estructuras que no son arrays contiguos y que es el vocabulario con el que está escrita la biblioteca estándar.
La intuición ingenua dice que un puntero es un entero y que sumarle es sumar. Si te quedas ahí, escribirás código que funciona en tu máquina y explota en la del cliente. La verdad del estándar es más restrictiva y más interesante: la aritmética de punteros no está definida sobre el espacio de direcciones plano, sino dentro de un objeto, y el objeto es la frontera absoluta. Fuera de él la operación no es que dé un valor raro: es que no está definida en absoluto, ni siquiera al calcularla sin desreferenciar. Y esto no es purismo académico. El compilador explota esa promesa activamente: si escribes p + i sabe que el resultado sigue dentro del mismo objeto, y sobre esa certeza construye vectorización, reordenación de accesos, eliminación de comprobaciones redundantes y desenrollado de bucles. Cuando violas la regla no obtienes un desbordamiento predecible, obtienes un compilador que optimizó basándose en una premisa falsa; y por eso el síntoma aparece a veces a metros de distancia del pecado, solo con -O2, solo en una versión del compilador. Aquí está la lección que trasciende a los punteros: en C, cada regla que parece una limitación arbitraria es en realidad un contrato de información que le das al optimizador. Respetarlas no te frena, es lo que le permite ir rápido; violarlas no te da libertad, te da un programa que ya no significa nada.
- Imprime
p,p + 1yp + 2para punteros achar,intydoublesobre el mismo búfer y verifica el escalado porsizeof. - Escribe una función que reciba
inicioyfinen vez de puntero y longitud, y súmelos recorriendo conp != fin. - Calcula la posición de un elemento dentro de un array restando punteros y guarda el resultado en un
ptrdiff_timpreso con%td. - Implementa una inversión de array in situ con dos punteros que convergen desde los extremos, sin usar ningún índice.
- Escribe a propósito un bucle que forme
v + n + 1, compílalo con sanitizers activados y documenta qué te reporta la herramienta.