C

C

Tutorial C: Aritmética de punteros

Aprende la aritmética de punteros en C para manipular arrays y estructuras con ejemplos claros y aplicaciones prácticas.

Aprende C y certifícate

Sumar/restar enteros a punteros

En C, los punteros no solo almacenan direcciones de memoria, sino que también podemos realizar operaciones aritméticas con ellos. Una de las operaciones más comunes es sumar o restar valores enteros a un puntero. Esta operación es fundamental para manipular arrays y estructuras de datos.

Cuando sumamos o restamos un número entero a un puntero, el resultado no es simplemente añadir o quitar ese valor a la dirección de memoria. En su lugar, C ajusta automáticamente el resultado según el tamaño del tipo de dato al que apunta el puntero. Esto facilita enormemente el trabajo con secuencias de elementos.

Funcionamiento básico

La fórmula que C utiliza internamente para calcular la nueva dirección es:

nueva_dirección = dirección_original + (n * sizeof(tipo_de_dato))

Donde n es el número entero que sumamos o restamos.

Veamos un ejemplo sencillo con un array de enteros:

#include <stdio.h>

int main() {
    int numeros[5] = {10, 20, 30, 40, 50};
    int *ptr = numeros;  // ptr apunta al primer elemento
    
    printf("Dirección de ptr: %p, Valor: %d\n", (void*)ptr, *ptr);
    
    // Sumamos 1 al puntero
    ptr = ptr + 1;
    printf("Después de ptr + 1: %p, Valor: %d\n", (void*)ptr, *ptr);
    
    // Sumamos 2 más al puntero
    ptr = ptr + 2;
    printf("Después de ptr + 2: %p, Valor: %d\n", (void*)ptr, *ptr);
    
    // Restamos 1 al puntero
    ptr = ptr - 1;
    printf("Después de ptr - 1: %p, Valor: %d\n", (void*)ptr, *ptr);
    
    return 0;
}

La salida mostrará que cada vez que sumamos 1 al puntero, avanzamos exactamente el tamaño de un int (normalmente 4 bytes) en la memoria, y el valor al que apunta cambia al siguiente elemento del array.

Acceso directo mediante aritmética de punteros

Podemos usar la aritmética de punteros para acceder directamente a cualquier elemento de un array:

#include <stdio.h>

int main() {
    int numeros[5] = {10, 20, 30, 40, 50};
    int *ptr = numeros;  // ptr apunta al primer elemento
    
    // Accedemos a diferentes elementos usando aritmética de punteros
    printf("Primer elemento: %d\n", *ptr);       // Equivale a numeros[0]
    printf("Segundo elemento: %d\n", *(ptr+1));  // Equivale a numeros[1]
    printf("Tercer elemento: %d\n", *(ptr+2));   // Equivale a numeros[2]
    printf("Cuarto elemento: %d\n", *(ptr+3));   // Equivale a numeros[3]
    printf("Quinto elemento: %d\n", *(ptr+4));   // Equivale a numeros[4]
    
    return 0;
}

Observa que usamos *(ptr+n) para acceder al valor. Los paréntesis son importantes porque el operador de desreferencia * tiene mayor precedencia que la suma.

Diferentes tipos de datos

El ajuste automático según el tamaño del tipo de dato es más evidente cuando trabajamos con diferentes tipos:

#include <stdio.h>

int main() {
    char caracteres[5] = {'A', 'B', 'C', 'D', 'E'};
    int numeros[5] = {10, 20, 30, 40, 50};
    double decimales[5] = {1.1, 2.2, 3.3, 4.4, 5.5};
    
    char *pChar = caracteres;
    int *pInt = numeros;
    double *pDouble = decimales;
    
    printf("Tamaños: char=%lu bytes, int=%lu bytes, double=%lu bytes\n", 
           sizeof(char), sizeof(int), sizeof(double));
    
    printf("\nDirecciones originales:\n");
    printf("pChar: %p\n", (void*)pChar);
    printf("pInt: %p\n", (void*)pInt);
    printf("pDouble: %p\n", (void*)pDouble);
    
    printf("\nDespués de sumar 1:\n");
    printf("pChar+1: %p (diferencia: %ld bytes)\n", 
           (void*)(pChar+1), (long)((pChar+1) - pChar));
    printf("pInt+1: %p (diferencia: %ld bytes)\n", 
           (void*)(pInt+1), (long)((pInt+1) - pInt));
    printf("pDouble+1: %p (diferencia: %ld bytes)\n", 
           (void*)(pDouble+1), (long)((pDouble+1) - pDouble));
    
    return 0;
}

Este programa muestra claramente cómo al sumar 1 a cada puntero, la dirección aumenta según el tamaño del tipo de dato correspondiente.

Aplicación práctica: recorrer un array

Una aplicación común de la aritmética de punteros es recorrer arrays de manera eficiente:

#include <stdio.h>

int main() {
    int numeros[5] = {10, 20, 30, 40, 50};
    int *ptr = numeros;
    int *fin = numeros + 5;  // Apunta justo después del último elemento
    
    printf("Recorriendo el array con aritmética de punteros:\n");
    
    // Recorremos el array sumando 1 al puntero en cada iteración
    while (ptr < fin) {
        printf("%d ", *ptr);
        ptr = ptr + 1;  // Avanzamos al siguiente elemento
    }
    printf("\n");
    
    return 0;
}

Notación de corchetes vs. aritmética de punteros

Es importante entender que en C, estas dos notaciones son equivalentes:

#include <stdio.h>

int main() {
    int numeros[5] = {10, 20, 30, 40, 50};
    int *ptr = numeros;
    
    printf("Usando notación de corchetes:\n");
    for (int i = 0; i < 5; i++) {
        printf("numeros[%d] = %d\n", i, numeros[i]);
    }
    
    printf("\nUsando aritmética de punteros:\n");
    for (int i = 0; i < 5; i++) {
        printf("*(ptr+%d) = %d\n", i, *(ptr+i));
    }
    
    // De hecho, esto también funciona (aunque es menos común):
    printf("\nMezclando notaciones:\n");
    for (int i = 0; i < 5; i++) {
        printf("*(numeros+%d) = %d\n", i, *(numeros+i));
        printf("ptr[%d] = %d\n", i, ptr[i]);
    }
    
    return 0;
}

Esto ocurre porque en C, el nombre de un array se convierte automáticamente en un puntero a su primer elemento en la mayoría de los contextos.

Precauciones al usar aritmética de punteros

Al trabajar con aritmética de punteros, debemos tener cuidado con:

  1. No exceder los límites del array: C no verifica si accedemos a memoria fuera de los límites del array.
int numeros[5] = {10, 20, 30, 40, 50};
int *ptr = numeros;

// ¡Peligroso! Accede a memoria fuera del array
int valor = *(ptr + 10);  // Comportamiento indefinido
  1. No restar más allá del inicio del array:
int numeros[5] = {10, 20, 30, 40, 50};
int *ptr = numeros + 2;  // Apunta al tercer elemento

// Esto es válido
ptr = ptr - 2;  // Ahora apunta al primer elemento

// ¡Peligroso! Apunta antes del inicio del array
ptr = ptr - 1;  // Comportamiento indefinido

La aritmética de punteros es una herramienta poderosa en C que nos permite manipular datos en memoria de forma eficiente. Entender cómo funciona la suma y resta de enteros a punteros es fundamental para dominar el lenguaje y trabajar con estructuras de datos complejas.

Incremento mueve al siguiente elemento

Además de sumar o restar valores enteros a los punteros, C proporciona operadores de incremento y decremento que simplifican la manipulación de punteros. Estos operadores (++ y --) son especialmente útiles cuando necesitamos movernos secuencialmente a través de elementos contiguos en memoria.

Cuando aplicamos el operador de incremento a un puntero, este avanza automáticamente a la siguiente posición del tipo de dato al que apunta. Esta característica hace que el recorrido de arrays sea más intuitivo y el código más limpio.

Operadores de incremento y decremento con punteros

Existen dos formas de utilizar estos operadores:

  • Prefijo: ++ptr (incrementa y luego devuelve el valor)
  • Sufijo: ptr++ (devuelve el valor actual y luego incrementa)

Veamos cómo funcionan en la práctica:

#include <stdio.h>

int main() {
    int valores[3] = {10, 20, 30};
    int *p = valores;  // p apunta al primer elemento
    
    printf("Valor inicial: %d\n", *p);  // Muestra 10
    
    // Incremento con sufijo (post-incremento)
    printf("*p++: %d\n", *p++);  // Muestra 10, luego incrementa p
    printf("Ahora p apunta a: %d\n", *p);  // Muestra 20
    
    // Incremento con prefijo (pre-incremento)
    printf("*++p: %d\n", *++p);  // Incrementa p y luego muestra 30
    
    return 0;
}

La diferencia entre *p++ y *++p es crucial para entender:

  • *p++ → Primero obtiene el valor al que apunta p, luego incrementa p
  • *++p → Primero incrementa p, luego obtiene el valor al que apunta

Recorriendo arrays con operadores de incremento

Una de las aplicaciones más comunes de estos operadores es recorrer arrays de manera eficiente:

#include <stdio.h>

int main() {
    int numeros[5] = {10, 20, 30, 40, 50};
    int *ptr = numeros;
    
    printf("Recorrido con ptr++:\n");
    for (int i = 0; i < 5; i++) {
        printf("%d ", *ptr);
        ptr++;  // Avanza al siguiente elemento
    }
    printf("\n");
    
    // Reiniciamos el puntero al inicio del array
    ptr = numeros;
    
    printf("Recorrido más compacto:\n");
    for (int i = 0; i < 5; i++) {
        printf("%d ", *ptr++);  // Usa el valor y luego incrementa
    }
    printf("\n");
    
    return 0;
}

El segundo enfoque es más conciso pero puede resultar confuso para principiantes. Es importante entender que *ptr++ equivale a *(ptr++) debido a las reglas de precedencia de operadores en C.

Comparación con la notación de índices

Comparemos el uso de operadores de incremento con la notación tradicional de índices:

#include <stdio.h>

int main() {
    int numeros[5] = {10, 20, 30, 40, 50};
    
    // Método 1: Notación de índices
    printf("Con índices: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", numeros[i]);
    }
    printf("\n");
    
    // Método 2: Aritmética de punteros con incremento
    printf("Con punteros: ");
    int *ptr = numeros;
    while (ptr < numeros + 5) {
        printf("%d ", *ptr++);
    }
    printf("\n");
    
    return 0;
}

Ambos métodos producen el mismo resultado, pero el enfoque con punteros puede ser más eficiente en ciertos contextos, especialmente cuando trabajamos con estructuras de datos complejas.

Incremento con diferentes tipos de datos

El incremento de punteros respeta el tamaño del tipo de dato, igual que la suma de enteros:

#include <stdio.h>

int main() {
    // Arrays de diferentes tipos
    char letras[3] = {'A', 'B', 'C'};
    int numeros[3] = {100, 200, 300};
    double decimales[3] = {1.1, 2.2, 3.3};
    
    // Punteros a los arrays
    char *pChar = letras;
    int *pInt = numeros;
    double *pDouble = decimales;
    
    printf("Incremento con diferentes tipos:\n");
    
    // Recorremos cada array con incremento
    printf("char: ");
    for (int i = 0; i < 3; i++) {
        printf("%c ", *pChar++);
    }
    printf("\n");
    
    printf("int: ");
    for (int i = 0; i < 3; i++) {
        printf("%d ", *pInt++);
    }
    printf("\n");
    
    printf("double: ");
    for (int i = 0; i < 3; i++) {
        printf("%.1f ", *pDouble++);
    }
    printf("\n");
    
    return 0;
}

Operadores de decremento

De manera similar, podemos usar los operadores de decremento (--) para movernos hacia atrás en un array:

#include <stdio.h>

int main() {
    int valores[5] = {10, 20, 30, 40, 50};
    int *p = valores + 4;  // p apunta al último elemento
    
    printf("Recorrido inverso con p--:\n");
    for (int i = 0; i < 5; i++) {
        printf("%d ", *p);
        p--;  // Retrocede al elemento anterior
    }
    printf("\n");
    
    // Otra forma más compacta
    p = valores + 4;
    printf("Recorrido inverso compacto:\n");
    for (int i = 0; i < 5; i++) {
        printf("%d ", *p--);  // Usa el valor y luego decrementa
    }
    printf("\n");
    
    return 0;
}

Aplicación práctica: copia de cadenas

Un ejemplo práctico del uso de operadores de incremento es la implementación de una función para copiar cadenas:

#include <stdio.h>

void copiar_cadena(char *destino, const char *origen) {
    while (*origen != '\0') {
        *destino++ = *origen++;  // Copia e incrementa ambos punteros
    }
    *destino = '\0';  // Añade el terminador nulo
}

int main() {
    char original[] = "Hola mundo";
    char copia[20];  // Espacio suficiente para la copia
    
    copiar_cadena(copia, original);
    printf("Original: %s\n", original);
    printf("Copia: %s\n", copia);
    
    return 0;
}

En este ejemplo, *destino++ = *origen++ realiza tres operaciones en una sola línea:

  1. Copia el carácter al que apunta origen en la posición a la que apunta destino
  2. Incrementa origen para que apunte al siguiente carácter de la cadena original
  3. Incrementa destino para que apunte a la siguiente posición en la cadena destino

Precauciones al usar operadores de incremento

Al trabajar con estos operadores, debemos tener cuidado con:

  • Límites de memoria: Asegúrate de no incrementar un puntero más allá del espacio asignado.
  • Claridad del código: El uso excesivo de operadores de incremento puede hacer que el código sea difícil de leer.
  • Efectos secundarios: Ten cuidado con expresiones como *p++ = *q++, donde el orden de evaluación puede ser importante.
#include <stdio.h>

int main() {
    int a[3] = {1, 2, 3};
    int *p = a;
    
    // Esto puede ser confuso
    *p++ += 10;  // Incrementa el valor y luego mueve el puntero
    
    printf("a[0]=%d, a[1]=%d, a[2]=%d\n", a[0], a[1], a[2]);
    
    return 0;
}

El operador de incremento es una herramienta poderosa que simplifica la manipulación de punteros en C, especialmente al trabajar con arrays y cadenas. Dominar su uso te permitirá escribir código más conciso y eficiente, aunque siempre priorizando la claridad y la seguridad.

Diferencia entre punteros

Otra operación importante en la aritmética de punteros es calcular la diferencia entre dos punteros. Esta operación nos permite determinar cuántos elementos hay entre dos posiciones de memoria, lo que resulta especialmente útil cuando trabajamos con arrays.

Cuando restamos un puntero de otro (del mismo tipo), el resultado no es simplemente la diferencia entre direcciones de memoria, sino que C automáticamente divide esta diferencia por el tamaño del tipo de dato. Esto nos da como resultado el número de elementos que separan ambas posiciones.

Cómo funciona la diferencia entre punteros

La fórmula que C utiliza para calcular la diferencia es:

diferencia = (ptr2 - ptr1) / sizeof(tipo_de_dato)

Sin embargo, C realiza esta división automáticamente, por lo que simplemente escribimos:

ptrdiff_t diferencia = ptr2 - ptr1;

El tipo ptrdiff_t (definido en <stddef.h>) es el tipo de dato adecuado para almacenar diferencias entre punteros.

Veamos un ejemplo básico:

#include <stdio.h>
#include <stddef.h>

int main() {
    int numeros[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    int *inicio = &numeros[2];  // Apunta al tercer elemento (valor 2)
    int *fin = &numeros[7];     // Apunta al octavo elemento (valor 7)
    
    ptrdiff_t diferencia = fin - inicio;
    
    printf("Inicio apunta a: %d\n", *inicio);
    printf("Fin apunta a: %d\n", *fin);
    printf("Diferencia: %td elementos\n", diferencia);
    
    return 0;
}

En este ejemplo, la diferencia será 5, que es el número de elementos entre las posiciones 2 y 7 del array.

Aplicaciones prácticas

1. Calcular la longitud de un array

Podemos usar la diferencia entre punteros para calcular la longitud de un array cuando conocemos sus límites:

#include <stdio.h>
#include <stddef.h>

int main() {
    int valores[] = {10, 20, 30, 40, 50, 60, 70};
    
    // Calculamos la longitud usando punteros
    int *inicio = valores;
    int *fin = valores + sizeof(valores)/sizeof(valores[0]);
    
    ptrdiff_t longitud = fin - inicio;
    
    printf("Longitud del array: %td elementos\n", longitud);
    
    return 0;
}

2. Determinar la posición de un elemento en un array

Otra aplicación común es encontrar el índice de un elemento dentro de un array:

#include <stdio.h>
#include <stddef.h>

int main() {
    int numeros[] = {15, 23, 8, 42, 16, 4};
    int *base = numeros;
    int *elemento = &numeros[3];  // Apunta al valor 42
    
    ptrdiff_t indice = elemento - base;
    
    printf("El elemento %d está en la posición %td\n", *elemento, indice);
    
    return 0;
}

3. Medir el tamaño de un segmento

Podemos medir cuántos elementos hay en un segmento específico de un array:

#include <stdio.h>
#include <stddef.h>

int main() {
    char texto[] = "Programacion en C";
    char *inicio = &texto[2];  // Apunta a 'o'
    char *fin = &texto[8];     // Apunta a 'a'
    
    ptrdiff_t longitud_segmento = fin - inicio;
    
    printf("Segmento: \"");
    for (char *p = inicio; p <= fin; p++) {
        printf("%c", *p);
    }
    printf("\"\n");
    
    printf("Longitud del segmento: %td caracteres\n", longitud_segmento);
    
    return 0;
}

Restricciones importantes

La diferencia entre punteros solo tiene sentido cuando:

  1. Ambos punteros son del mismo tipo
  2. Ambos punteros apuntan a elementos del mismo array

Intentar calcular la diferencia entre punteros que apuntan a diferentes arrays o a diferentes tipos de datos puede producir resultados inesperados o errores:

#include <stdio.h>

int main() {
    int array1[5] = {1, 2, 3, 4, 5};
    int array2[5] = {6, 7, 8, 9, 10};
    
    int *p1 = &array1[2];
    int *p2 = &array2[3];
    
    // ¡INCORRECTO! Los punteros apuntan a diferentes arrays
    ptrdiff_t diferencia = p2 - p1;  // Resultado indefinido
    
    return 0;
}

Comparación de punteros

Además de calcular la diferencia, también podemos comparar punteros usando los operadores relacionales (<, >, <=, >=, ==, !=). Esto es útil para determinar el orden relativo de elementos en un array:

#include <stdio.h>

int main() {
    int valores[5] = {10, 20, 30, 40, 50};
    int *p1 = &valores[1];  // Apunta a 20
    int *p2 = &valores[3];  // Apunta a 40
    
    if (p1 < p2) {
        printf("p1 apunta a un elemento que está antes que p2\n");
    }
    
    if (p1 == &valores[1]) {
        printf("p1 apunta al segundo elemento del array\n");
    }
    
    // Recorremos el array mientras el puntero sea menor que el final
    for (int *p = valores; p < valores + 5; p++) {
        printf("%d ", *p);
    }
    printf("\n");
    
    return 0;
}

Ejemplo práctico: búsqueda en un array

Podemos usar la diferencia entre punteros para implementar una función de búsqueda que devuelva el índice de un elemento:

#include <stdio.h>
#include <stddef.h>

// Busca un valor en un array y devuelve su índice (-1 si no lo encuentra)
int buscar(int *array, int longitud, int valor) {
    int *final = array + longitud;
    int *encontrado = NULL;
    
    for (int *p = array; p < final; p++) {
        if (*p == valor) {
            encontrado = p;
            break;
        }
    }
    
    if (encontrado) {
        return encontrado - array;  // Calculamos el índice
    } else {
        return -1;  // No encontrado
    }
}

int main() {
    int numeros[] = {15, 7, 23, 9, 42, 8, 16};
    int longitud = sizeof(numeros) / sizeof(numeros[0]);
    
    int indice1 = buscar(numeros, longitud, 42);
    int indice2 = buscar(numeros, longitud, 100);
    
    if (indice1 != -1) {
        printf("El valor 42 está en la posición %d\n", indice1);
    }
    
    if (indice2 == -1) {
        printf("El valor 100 no se encuentra en el array\n");
    }
    
    return 0;
}

Uso con la biblioteca estándar

Muchas funciones de la biblioteca estándar de C utilizan la diferencia entre punteros. Por ejemplo, la función ptrdiff de la biblioteca <string.h> calcula la diferencia entre dos punteros:

#include <stdio.h>
#include <string.h>

int main() {
    char mensaje[] = "Aprendiendo C";
    char *inicio = mensaje;
    char *fin = strchr(mensaje, 'C');  // Busca la primera ocurrencia de 'C'
    
    if (fin) {
        ptrdiff_t posicion = fin - inicio;
        printf("La letra 'C' está en la posición %td\n", posicion);
    }
    
    return 0;
}

La diferencia entre punteros es una herramienta fundamental en C que nos permite realizar operaciones como calcular longitudes, determinar posiciones y recorrer arrays de manera eficiente. Entender cómo funciona esta operación es esencial para manipular estructuras de datos y escribir código más efectivo.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende C online

Todas las lecciones de C

Accede a todas las lecciones de C y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a C y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender cómo sumar y restar enteros a punteros y su relación con el tamaño del tipo de dato.
  • Utilizar operadores de incremento y decremento para recorrer arrays de forma eficiente.
  • Diferenciar entre notación de índices y aritmética de punteros en C.
  • Calcular la diferencia entre dos punteros y sus aplicaciones prácticas.
  • Reconocer las precauciones y restricciones al manipular punteros para evitar comportamientos indefinidos.