C

C

Tutorial C: Punteros

C: Conoce los fundamentos de direcciones de memoria y variables puntero para optimizar recursos y mejorar la eficiencia en programación.

Aprende C y certifícate

Fundamentos de direcciones de memoria y variables puntero

Las direcciones de memoria identifican de forma única la ubicación de cada valor en un programa. Cada variable se almacena en un espacio de memoria concreto, y esa dirección se puede representar y gestionar dentro del propio código. En C, esta gestión cuidadosa de ubicaciones permite optimizar recursos y construir estructuras de datos con gran flexibilidad.

Un puntero es una variable que guarda la dirección de memoria en la que reside otra variable. Al trabajar con punteros, se debe tener presente el tipo de dato al que apuntan, pues cada tipo puede ocupar distinto espacio en memoria. Este mecanismo facilita la creación de estructuras dinámicas y la implementación de comportamientos que requieren manipular direcciones de forma controlada.

En un programa C, declarar una variable puntero consiste en especificar el tipo al que hace referencia y añadir un asterisco en la definición. Un ejemplo elemental es el siguiente:

#include <stdio.h>

int main() {
    int *punteroAEntero;
    double *punteroADouble;

    /* 
       Estas variables puntero se han declarado pero 
       aún no se ha asignado ninguna dirección concreta.
    */

    return 0;
}

Entre los usos más habituales de una variable puntero figuran:

  • Pasar información entre funciones sin necesidad de copiar grandes porciones de datos.
  • Manipular arreglos y estructuras con un mayor control sobre el acceso a cada posición.
  • Reservar memoria en tiempo de ejecución para estructuras cuyo tamaño no se conoce de antemano.

Estos conceptos proporcionan la base para comprender otras características de C relacionadas con la gestión de direcciones y con la interacción directa con la memoria.

Operadores & (dirección) y * (contenido)

El operador & se emplea para obtener la dirección de una variable y resulta esencial cuando se desea manipular apuntadores. Por ejemplo, al pasar parámetros a ciertas funciones que necesitan modificar una variable, se puede utilizar & para transmitir su dirección en lugar de su valor. Esto permite un mayor control sobre los cambios que ocurren en la memoria.

Cuando se dispone de un puntero, el operador * (denominado operador de contenido o de indirección) permite acceder al valor apuntado. Es frecuente ver este operador al asignar o leer datos a través de un puntero, lo que posibilita separar la ubicación en memoria del contenido que reside ahí. Dicho acceso controlado habilita cambios precisos en determinados lugares de la memoria sin necesidad de copiar toda la información.

Algunos usos habituales de & y * son:

  • Conseguir la dirección de variables y almacenarlas en punteros para procesarlas en funciones.
  • Modificar valores apuntados por un puntero para aplicar cambios directos en la memoria.
  • Pasar apuntadores a funciones donde se requiera actualizar variables originales.

A continuación se muestra un ejemplo conciso que ilustra la relación entre & y *:

#include <stdio.h>

int main() {
    int valor = 10;
    int *puntero = &valor;  /* Se obtiene la dirección de 'valor' */

    printf("Contenido original: %d\n", *puntero);  /* Se accede al contenido */
    *puntero = 20;  /* Se modifica el valor apuntado */
    printf("Contenido modificado: %d\n", valor);

    return 0;
}

En este fragmento, la variable valor se asocia a puntero utilizando el operador &, mientras que el operador * permite leer y cambiar el contenido en la ubicación de memoria referenciada. Con estas operaciones, se facilita un manejo más flexible de los datos y se evitan copias innecesarias de información.

Punteros y arrays (relación e indexación)

Los arrays en C se almacenan en posiciones de memoria contiguas, lo que posibilita tratarlos de forma similar a un puntero que apunta al primer elemento. Aunque el nombre de un array puede usarse en expresiones como si fuera un puntero, su dirección base no es modificable directamente. Por otro lado, una variable puntero sí puede apuntar a distintos lugares, lo que brinda más flexibilidad al manejar secuencias de datos.

Para acceder a los elementos de un array mediante un puntero, suele aprovecharse la correspondencia entre el operador de subíndice y la aritmética de direcciones. La expresión array[i] es equivalente a *(array + i), donde cada incremento sobre el nombre del array (o sobre un puntero) avanza tantas celdas como el tamaño del tipo base. Con esta equivalencia, se pueden realizar recorridos e iteraciones sobre estructuras de datos con un control más detallado sobre los desplazamientos en memoria.

A continuación, se observa un ejemplo que ilustra la relación entre punteros y arrays, junto con la indexación basada en desplazamientos:

#include <stdio.h>

int main() {
    int valores[] = {1, 2, 3, 4};
    int *puntero = valores;    

    for (int i = 0; i < 4; i++) {
        printf("Elemento %d: %d\n", i, *(puntero + i));
    }

    return 0;
}

En este fragmento, se asigna la dirección base de valores a puntero para posteriormente imprimir cada elemento mediante aritmética de punteros. La suma puntero + i proporciona la ubicación del i-ésimo elemento, lo que ejemplifica la similitud conceptual entre usar valores[i] y *(valores + i). No obstante, siempre conviene administrar los límites de un array con cautela para evitar accesos fuera de rango.

Punteros a funciones

Los punteros a funciones permiten referenciar y llamar a una función a través de una variable que contiene su dirección. Esta capacidad introduce flexibilidad al permitir elegir dinámicamente qué función se invocará en tiempo de ejecución. Un ejemplo habitual es definir distintas operaciones que comparten la misma firma y conmutar entre ellas según sea necesario.

Para declarar un puntero a función, se especifica el tipo de retorno y los tipos de parámetros, incluyendo un nombre entre paréntesis precedido de asterisco. Por ejemplo, si se requiere apuntar a una función que reciba dos valores enteros y devuelva un entero, la declaración puede hacerse de la siguiente manera:

int (*operacion)(int, int);

Una vez declarada la variable, es sencillo asignarle una función compatible. En el siguiente fragmento, se define una función suma y otra función resta, para posteriormente almacenar la dirección de cada una en la variable puntero. El asterisco antes del nombre de la variable al momento de llamar indica que se está invocando la función apuntada:

#include <stdio.h>

int sumar(int a, int b) {
    return a + b;
}

int restar(int a, int b) {
    return a - b;
}

int main() {
    int (*operacion)(int, int);

    operacion = sumar;
    printf("Resultado sumar: %d\n", (*operacion)(5, 3));

    operacion = restar;
    printf("Resultado restar: %d\n", (*operacion)(5, 3));

    return 0;
}

En este ejemplo, la variable puntero se reasigna antes de cada llamada, lo que concentra la lógica de selección en un único lugar. Con dicha mecánica, se pueden implementar estructuras de decisión más compactas y extensibles, ya que basta con incluir nuevas funciones que sigan la misma firma.

En muchas bibliotecas se emplean punteros a funciones para recibir como argumento la rutina encargada de procesar datos. Estas rutinas se conocen a veces como funciones de callback y ofrecen un nivel de abstracción que permite cambiar la forma de procesar la información sin alterar el código principal. Este enfoque es útil en situaciones como el filtrado de datos o la clasificación en un orden específico.

Al definir varios punteros que compartan firma, suele ser buena práctica agruparlos en tablas o arrays de punteros a funciones para simplificar el acceso indexado a comportamientos discretos. Este recurso hace viable crear menús de selección de funciones, donde cada posición del array apunta a una acción distinta.

La asignación, llamada y reubicación de punteros a funciones pueden proporcionar gran adaptabilidad al código, siempre que se administren correctamente los tipos implicados y se garanticen correspondencias exactas entre declaración del puntero y definición de la función. Esto evita errores en tiempo de ejecución y confusiones al compilar.

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 la gestión de memoria en C a través de direcciones y punteros.
  • Diferenciar el uso de operadores & y * en el manejo de memoria.
  • Relacionar arrays y punteros, optimizando el acceso a datos.
  • Implementar punteros a funciones para mejorar la flexibilidad y modularidad del código.