C

C

Tutorial C: Punteros y arrays

Aprende la relación entre punteros y arrays en C, cómo acceder a elementos y la equivalencia entre arr[i] y *(arr + i) para programar eficazmente.

Aprende C y certifícate

Nombre del array es puntero al primer elemento

En C, uno de los conceptos más fundamentales pero a veces confusos es la estrecha relación entre arrays y punteros. Cuando declaramos un array en C, el nombre de ese array actúa como un puntero constante que apunta al primer elemento del array.

Para entender esto mejor, veamos qué ocurre en memoria cuando declaramos un array:

int numeros[5] = {10, 20, 30, 40, 50};

Cuando creamos este array, C reserva espacio contiguo en memoria para almacenar los cinco enteros. El nombre numeros por sí solo representa la dirección de memoria donde comienza el array, es decir, la dirección del primer elemento numeros[0].

Podemos comprobar esto imprimiendo la dirección:

#include <stdio.h>

int main() {
    int numeros[5] = {10, 20, 30, 40, 50};
    
    printf("numeros = %p\n", numeros);
    printf("&numeros[0] = %p\n", &numeros[0]);
    
    return 0;
}

Al ejecutar este código, verás que ambos valores son idénticos. Esto confirma que el nombre del array numeros es equivalente a &numeros[0], la dirección del primer elemento.

Esta característica nos permite usar el nombre del array directamente como un puntero. Por ejemplo, podemos acceder al primer elemento de dos formas:

printf("Primer elemento: %d\n", numeros[0]);  // Usando notación de array
printf("Primer elemento: %d\n", *numeros);    // Usando notación de puntero

Ambas líneas producen exactamente el mismo resultado, porque *numeros desreferencia la dirección almacenada en numeros, que es la dirección del primer elemento.

Sin embargo, es importante entender algunas diferencias clave entre un array y un puntero:

  • El nombre del array es un puntero constante - no puedes cambiar a dónde apunta:
int numeros[5] = {10, 20, 30, 40, 50};
int otro_array[5] = {1, 2, 3, 4, 5};

// Esto NO es válido y causará un error de compilación
numeros = otro_array;  // Error: el nombre del array no puede ser modificado
  • Un array conoce su tamaño, mientras que un puntero simple no:
int numeros[5] = {10, 20, 30, 40, 50};
int *ptr = numeros;  // ptr ahora apunta al primer elemento de numeros

// Esto es válido y nos da el tamaño del array en bytes
printf("Tamaño del array: %lu bytes\n", sizeof(numeros));  // 20 bytes (5 ints * 4 bytes)

// Esto solo nos da el tamaño del puntero, no del array al que apunta
printf("Tamaño del puntero: %lu bytes\n", sizeof(ptr));    // 8 bytes (en sistemas de 64 bits)

Veamos un ejemplo práctico que muestra cómo podemos usar el nombre del array como puntero para recorrer sus elementos:

#include <stdio.h>

int main() {
    int numeros[5] = {10, 20, 30, 40, 50};
    int *ptr;
    
    // Asignamos el puntero al inicio del array
    ptr = numeros;  // Equivalente a ptr = &numeros[0]
    
    printf("Recorriendo el array usando el nombre como puntero:\n");
    
    for (int i = 0; i < 5; i++) {
        printf("numeros[%d] = %d (dirección: %p)\n", 
               i, *(numeros + i), numeros + i);
    }
    
    return 0;
}

En este ejemplo, numeros + i calcula la dirección del elemento en la posición i, y *(numeros + i) accede al valor almacenado en esa dirección.

Esta relación entre arrays y punteros es una de las características distintivas del lenguaje C y es fundamental para entender cómo manipular datos en memoria de manera eficiente.

Un último ejemplo para consolidar este concepto:

#include <stdio.h>

void imprimir_array(int *arr, int longitud) {
    printf("Elementos del array: ");
    for (int i = 0; i < longitud; i++) {
        printf("%d ", arr[i]);  // Podemos usar notación de array con punteros
    }
    printf("\n");
}

int main() {
    int numeros[5] = {10, 20, 30, 40, 50};
    
    // Pasamos el nombre del array como un puntero
    imprimir_array(numeros, 5);
    
    return 0;
}

En este ejemplo, la función imprimir_array recibe un puntero a entero y la longitud del array. Podemos pasar directamente el nombre del array numeros como argumento porque, como hemos visto, el nombre del array es un puntero al primer elemento.

Acceder a elementos con *(arr + i)

Una vez que entendemos que el nombre de un array es un puntero al primer elemento, podemos aprovechar esta característica para acceder a los elementos del array utilizando aritmética de punteros. Esta técnica nos permite navegar por los elementos del array de una manera más explícita.

La aritmética de punteros en C se basa en un principio simple: cuando sumamos un número entero a un puntero, el resultado es un puntero que apunta a una posición de memoria desplazada según el tamaño del tipo de dato. Por ejemplo, si tenemos un puntero a int y le sumamos 1, el resultado será un puntero a la siguiente posición de memoria donde cabría otro int.

Veamos cómo funciona esto con un array:

#include <stdio.h>

int main() {
    int numeros[5] = {10, 20, 30, 40, 50};
    
    // Accediendo al primer elemento (índice 0)
    printf("Primer elemento: %d\n", *numeros);
    
    // Accediendo al segundo elemento (índice 1)
    printf("Segundo elemento: %d\n", *(numeros + 1));
    
    // Accediendo al tercer elemento (índice 2)
    printf("Tercer elemento: %d\n", *(numeros + 2));
    
    return 0;
}

En este ejemplo, numeros apunta al primer elemento del array. Cuando escribimos numeros + 1, estamos calculando la dirección del segundo elemento. Luego, al usar el operador de desreferencia *, accedemos al valor almacenado en esa dirección.

Es importante entender qué ocurre exactamente con la expresión numeros + i:

  1. numeros es la dirección base del array
  2. i es el desplazamiento en términos de elementos (no bytes)
  3. C automáticamente multiplica este desplazamiento por el tamaño del tipo de dato

Por ejemplo, si cada int ocupa 4 bytes en memoria:

  • numeros apunta a la dirección base (digamos 1000)
  • numeros + 1 apunta a la dirección 1004 (1000 + 1×4)
  • numeros + 2 apunta a la dirección 1008 (1000 + 2×4)

Podemos visualizar esto con un ejemplo más detallado:

#include <stdio.h>

int main() {
    int numeros[5] = {10, 20, 30, 40, 50};
    
    printf("Tamaño de un int: %lu bytes\n", sizeof(int));
    
    for (int i = 0; i < 5; i++) {
        printf("Elemento %d:\n", i);
        printf("  Dirección: %p\n", numeros + i);
        printf("  Valor: %d\n", *(numeros + i));
    }
    
    return 0;
}

Este código muestra claramente cómo la dirección de memoria se incrementa según el tamaño del tipo de dato, mientras que el valor obtenido corresponde al elemento en esa posición del array.

La notación *(numeros + i) es especialmente útil cuando trabajamos con punteros de manera explícita o cuando necesitamos manipular la dirección antes de acceder al valor. Veamos un ejemplo práctico donde recorremos un array utilizando un puntero y aritmética de punteros:

#include <stdio.h>

int main() {
    int numeros[5] = {10, 20, 30, 40, 50};
    int *ptr = numeros;  // ptr apunta al primer elemento
    
    printf("Recorriendo el array con un puntero:\n");
    
    for (int i = 0; i < 5; i++) {
        printf("Elemento %d: %d\n", i, *(ptr + i));
    }
    
    // Alternativa: incrementando el puntero
    printf("\nRecorriendo el array incrementando el puntero:\n");
    
    ptr = numeros;  // Reiniciamos ptr al inicio del array
    for (int i = 0; i < 5; i++) {
        printf("Elemento %d: %d\n", i, *ptr);
        ptr++;  // Avanzamos el puntero al siguiente elemento
    }
    
    return 0;
}

En este ejemplo mostramos dos formas de recorrer el array:

  1. Usando la expresión *(ptr + i) que calcula la dirección y accede al valor en una sola operación
  2. Incrementando progresivamente el puntero con ptr++ y accediendo al valor con *ptr

Ambos enfoques producen el mismo resultado, pero ilustran diferentes estilos de programación con punteros.

La notación *(arr + i) también es muy útil cuando trabajamos con arrays multidimensionales. Por ejemplo:

#include <stdio.h>

int main() {
    int matriz[3][4] = {
        {11, 12, 13, 14},
        {21, 22, 23, 24},
        {31, 32, 33, 34}
    };
    
    // Accediendo a elementos con aritmética de punteros
    printf("Elemento [1][2]: %d\n", *(*(matriz + 1) + 2));
    
    return 0;
}

En este caso, matriz + 1 nos da un puntero a la segunda fila, y *(matriz + 1) + 2 nos da la dirección del tercer elemento de esa fila. Finalmente, *(*(matriz + 1) + 2) nos da el valor almacenado en esa dirección, que es 23.

La notación con *(arr + i) también es fundamental cuando implementamos funciones que trabajan con arrays:

#include <stdio.h>

// Función que suma todos los elementos de un array
int sumar_array(int *arr, int longitud) {
    int suma = 0;
    for (int i = 0; i < longitud; i++) {
        suma += *(arr + i);  // Equivalente a suma += arr[i]
    }
    return suma;
}

int main() {
    int valores[5] = {10, 20, 30, 40, 50};
    
    printf("La suma de los elementos es: %d\n", sumar_array(valores, 5));
    
    return 0;
}

Esta forma de acceder a los elementos es especialmente valiosa cuando necesitamos manipular arrays de manera genérica o cuando trabajamos con memoria asignada dinámicamente, donde no tenemos una variable de array propiamente dicha, sino solo un puntero al primer elemento de un bloque de memoria.

Notación arr[i] es equivalente

Después de entender que el nombre de un array es un puntero al primer elemento y que podemos acceder a sus elementos mediante la expresión *(arr + i), es momento de revelar algo sorprendente: en C, la notación de corchetes arr[i] que usamos habitualmente es en realidad un azúcar sintáctico (una simplificación) para la expresión *(arr + i).

Cuando escribimos arr[i], el compilador de C lo traduce internamente a *(arr + i). Esto significa que ambas notaciones son completamente equivalentes y pueden usarse indistintamente. Esta equivalencia es una característica fundamental del lenguaje C y explica muchos de sus comportamientos con arrays y punteros.

Veamos un ejemplo sencillo que demuestra esta equivalencia:

#include <stdio.h>

int main() {
    int numeros[5] = {10, 20, 30, 40, 50};
    
    // Ambas formas son equivalentes
    printf("Tercer elemento usando arr[i]: %d\n", numeros[2]);
    printf("Tercer elemento usando *(arr + i): %d\n", *(numeros + 2));
    
    return 0;
}

Al ejecutar este código, verás que ambas líneas imprimen el mismo valor (30), confirmando que son funcionalmente idénticas.

Esta equivalencia tiene algunas consecuencias interesantes. Una de las más sorprendentes es que la expresión i[arr] también es válida y equivalente a arr[i]. Esto ocurre porque:

arr[i] = *(arr + i) = *(i + arr) = i[arr]

La suma es conmutativa (a + b = b + a), por lo que arr + i es lo mismo que i + arr. Veamos un ejemplo que demuestra esta propiedad:

#include <stdio.h>

int main() {
    int numeros[5] = {10, 20, 30, 40, 50};
    
    // Estas tres expresiones son equivalentes
    printf("Usando arr[i]: %d\n", numeros[2]);
    printf("Usando *(arr + i): %d\n", *(numeros + 2));
    printf("Usando i[arr]: %d\n", 2[numeros]);  // ¡Funciona!
    
    return 0;
}

Aunque i[arr] es válido, se considera una práctica poco convencional y podría confundir a otros programadores, por lo que generalmente se evita en código profesional.

La equivalencia entre arr[i] y *(arr + i) también explica por qué podemos usar la notación de corchetes con punteros normales:

#include <stdio.h>

int main() {
    int numeros[5] = {10, 20, 30, 40, 50};
    int *ptr = numeros;  // ptr apunta al primer elemento
    
    // Ambas notaciones funcionan con punteros
    printf("Usando ptr[i]: %d\n", ptr[2]);
    printf("Usando *(ptr + i): %d\n", *(ptr + 2));
    
    return 0;
}

Esta característica es especialmente útil cuando trabajamos con funciones que reciben arrays. Podemos usar la notación de corchetes dentro de la función aunque el parámetro sea declarado como puntero:

#include <stdio.h>

// La función recibe un puntero, pero podemos usar notación de array
void imprimir_elementos(int *arr, int longitud) {
    for (int i = 0; i < longitud; i++) {
        printf("%d ", arr[i]);  // Equivalente a *(arr + i)
    }
    printf("\n");
}

int main() {
    int valores[5] = {10, 20, 30, 40, 50};
    
    imprimir_elementos(valores, 5);
    
    return 0;
}

La equivalencia también nos permite entender mejor cómo funcionan los arrays multidimensionales en C. Por ejemplo, en un array bidimensional:

int matriz[3][4];

// Acceder a un elemento
int valor = matriz[1][2];

Esto es equivalente a:

int valor = *(*(matriz + 1) + 2);

Donde:

  1. matriz + 1 calcula la dirección de la segunda fila
  2. *(matriz + 1) obtiene un puntero a esa fila
  3. *(matriz + 1) + 2 calcula la dirección del tercer elemento de esa fila
  4. *(*(matriz + 1) + 2) obtiene el valor almacenado en esa dirección

Esta equivalencia también explica por qué podemos usar operadores de incremento con punteros para recorrer arrays:

#include <stdio.h>

int main() {
    int numeros[5] = {10, 20, 30, 40, 50};
    int *ptr = numeros;
    
    // Recorriendo el array con ptr++
    for (int i = 0; i < 5; i++) {
        printf("%d ", *ptr);  // Accedemos al valor actual
        ptr++;                // Avanzamos al siguiente elemento
    }
    printf("\n");
    
    // Equivalente usando notación de corchetes
    ptr = numeros;  // Reiniciamos el puntero
    for (int i = 0; i < 5; i++) {
        printf("%d ", ptr[i]);
    }
    printf("\n");
    
    return 0;
}

Un ejemplo práctico donde esta equivalencia resulta útil es cuando implementamos algoritmos de búsqueda:

#include <stdio.h>

// Busca un valor en un array y devuelve su índice (o -1 si no lo encuentra)
int buscar(int *arr, int longitud, int valor) {
    for (int i = 0; i < longitud; i++) {
        // Podemos usar arr[i] o *(arr + i) indistintamente
        if (arr[i] == valor) {
            return i;
        }
    }
    return -1;  // No encontrado
}

int main() {
    int numeros[5] = {10, 20, 30, 40, 50};
    int indice;
    
    indice = buscar(numeros, 5, 30);
    if (indice != -1) {
        printf("Valor encontrado en el índice %d\n", indice);
    } else {
        printf("Valor no encontrado\n");
    }
    
    return 0;
}

La comprensión de esta equivalencia es fundamental para dominar el lenguaje C, ya que nos permite elegir la notación más clara y conveniente según el contexto, mejorando la legibilidad y mantenibilidad de nuestro código.

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 que el nombre de un array en C es un puntero constante al primer elemento.
  • Aprender a acceder a elementos de un array usando aritmética de punteros con la notación *(arr + i).
  • Entender la equivalencia entre la notación arr[i] y *(arr + i) en C.
  • Conocer las diferencias entre arrays y punteros, especialmente en cuanto a tamaño y modificabilidad.
  • Aplicar estos conceptos para recorrer arrays y trabajar con funciones que reciben punteros o arrays.