Aritmética de punteros: escala de tipo y límites

La aritmética de punteros no es aritmética de direcciones de memoria puras, sino aritmética de objetos. Cuando aplicas un operador de suma o resta a un puntero, el resultado no se calcula en bytes, sino en unidades del tipo de dato al que apunta. Esto ocurre porque el compilador aplica un escalado automático: si tienes un puntero a int y le sumas 1, el compilador no suma 1 byte, sino sizeof(int) bytes a la dirección actual. Esta abstracción es la que permite que el programador trabaje con estructuras lógicas de datos sin preocuparse de si un double ocupa 8 bytes o un char ocupa 1, permitiendo que la lógica de navegación sea portable.

Esta mecánica es fundamental cuando necesitas recorrer buffers, implementar iteradores personalizados o manipular subsecciones de una estructura de datos de forma eficiente. Sin embargo, este poder tiene límites estrictos definidos por el estándar. La aritmética está permitida únicamente dentro de los límites de un mismo objeto (un array) o un elemento justo después del final del array (como un centinela para comparaciones), pero el momento en que intentas desreferenciar ese elemento “fuera de rango”, entras en el terreno del comportamiento indefinido (UB). Asimismo, si intentas restar dos punteros que no pertenecen al mismo bloque de memoria, el resultado es matemáticamente irrelevante y el comportamiento es indefinido, ya que la arquitectura no garantiza que la distancia física en bytes refleje la distancia lógica en elementos.

#include <stdio.h>
#include <stddef.h> // Necesario para ptrdiff_t
#include <stdlib.h>

typedef struct {
    int id;
    double valor;
} SensorData;

int main(void) {
    // Definición de un array de estructuras
    SensorData datos[] = {
        {101, 22.5},
        {102, 23.1},
        {103, 21.9},
        {104, 24.0}
    };

    // El tamaño del array es 4 elementos
    size_t n = sizeof(datos) / sizeof(datos[0]);
    SensorData *base = datos;
    SensorData *actual = &datos[2]; // Apunta al tercer elemento (id 103)

    // 1. Suma: avanza el tamaño de 'SensorData'
    // El compilador calcula: dirección_actual + (1 * sizeof(SensorData))
    SensorData *siguiente = actual + 1;

    // 2. Resta: produce un tipo ptrdiff_t (signed)
    // Representa la cantidad de elementos entre los dos punteros
    ptrdiff_t distancia = actual - base;

    // 3. Comparación: permitida porque ambos pertenecen al mismo array
    if (actual > base) {
        printf("El puntero 'actual' está después de 'base'\n");
    }

    // 4. Comparación de igualdad
    // Solo es útil para verificar si un puntero apunta al inicio
    if (actual != base) {
        printf("No estamos en el inicio del array\n");
    }

    printf("Datos en actual: ID=%d, Valor=%.1f\n", actual->id, actual->valor);
    printf("Distancia desde base: %td elementos\n", distancia);
    printf("Dirección base:    %p\n", (void *)base);
    printf("Dirección actual:  %p\n", (void *)actual);
    printf("Dirección siguiente: %p (justo después de la última)\n", (void *)siguiente);

    // El puntero 'siguiente' es legal para comparar, pero NO para desreferenciar
    // ya que apunta al elemento inmediatamente después del final del array.

    return 0;
}

Análisis del funcionamiento

Fíjate en la variable distancia de tipo ptrdiff_t. Es crucial entender que ptrdiff_t es un tipo entero con signo definido en <stddef.h> diseñado específicamente para almacenar la diferencia entre dos punteros. Si usáramos un int estándar, podríamos tener problemas de desbordamiento en sistemas de 64 bits con arrays masivos; ptrdiff_t garantiza la compatibilidad con el modelo de memoria del sistema.

Cuando calculamos actual + 1, el compilador no realiza una simple suma de enteros sobre la dirección de memoria. En la máquina real, si SensorData mide 16 bytes (un int y un double con padding), actual + 1 le suma 16 a la dirección base. Esto es lo que permite que actual[1] sea equivalente a *(actual + 1).

En el ejemplo, siguiente apunta a la posición de memoria inmediatamente posterior al último elemento del array. El estándar C permite que un puntero apunte exactamente a base + n (donde n es el número de elementos) para facilitar comparaciones de bucles como while (p < fin), pero intentar acceder al contenido de siguiente mediante siguiente->id provocaría un error de segmentación o, peor aún, un comportamiento silencioso e impredecible si esa memoria está ocupada por otros datos.

El error frecuente

Un error crítico ocurre al comparar punteros que no pertenecen al mismo objeto usando operadores relacionales (<, >, <=, >=).

// ERROR: Comportamiento indefinido
int *a = malloc(sizeof(int) * 10);
int *b = malloc(sizeof(int) * 10);

if (a < b) { 
    // Esto no tiene significado semántico real.
    // La dirección de memoria de un bloque de malloc no garantiza 
    // orden relativo con otro bloque de malloc.
}

Aunque en muchos sistemas el código “funcione” porque los bloques están contiguos, esto es undefined behavior. El compilador está libre de optimizar asumiendo que esta comparación nunca será verdadera o falsa de la manera que esperas. Las únicas operaciones seguras entre punteros de distinto origen son la igualdad == y la desigualdad !=. Para medir distancias entre bloques de memoria independientes, debes trabajar con direcciones numéricas (uintptr_t) o simplemente con los valores de los punteros, pero nunca asumir que su orden relacional tiene sentido lógico.

45

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio