Apuntes de apuntadores

Apuntadores (Punteros) en Programación — Capítulo de apunte

Apuntadores (Punteros) en Programación

Un apuntador (o puntero) es una variable que contiene la dirección de memoria de otro dato o de otra variable. En lugar de almacenar el valor directamente, el puntero “apunta” al lugar de memoria donde ese valor vive. Los punteros permiten manipulación eficiente y flexible de la memoria.

Lenguaje: C y pseudocódigo Conceptos clave: dirección (&), desreferenciación / indirección (*), paso por valor vs referencia, aritmética de punteros.

Declaración de un Apuntador

Para declarar un puntero, se especifica el tipo de dato al que apuntará, seguido de un asterisco (*) y el nombre o identificador del puntero.

En Pseudocódigo

tipo_dato *nombre_puntero

// Ejemplo
entero *puntero;

En Lenguaje C

tipo_dato *nombre_puntero;

// Ejemplo
int *puntero;

Uso de Apuntadores

Los punteros permiten trabajar con memoria de forma directa. Entre sus usos principales están: manipulación indirecta de variables, paso de parámetros por referencia (de salida) y trabajo eficiente con arreglos, así como la asignación dinámica de memoria y las estructuras dinámicas, como listas dinámicas, árboles, etc.

1. Manipulación Indirecta de Variables

Obtener la dirección: usa el operador & delante del nombre de la variable; se puede aplicar a una variable de cualquier tipo de dtado. Devuelve su dirección de memoria. Ej. si la variable es num, entonces &num retorna su dirección.

Desreferenciar: usa el operador * con un puntero para acceder al contenido de la ubicación a la que apunta (o dirección de memoria que contiene dicha variable). Si puntero almacena la dirección de num, entonces *puntero es el valor de num.

Ejemplo de aplicación (pseudocódigo)

// En C, se necesitaría include <stdio.h> para printf
Inicio
  // Declaración de una variable entera y un puntero a entero
  entero num
  entero *puntero

  // Asignar un valor a num
  num ← 100

  // Asignar la dirección de memoria de num al puntero
  puntero ← &num

  // Mostrar la dirección de memoria a la que apunta el puntero
  Escribir("La dirección de memoria de num es: ", puntero)

  // Mostrar el valor de la variable a través del puntero
  Escribir("El valor de num a través del puntero es: ", *puntero)

  // Modificar el valor de num a través del puntero
  *puntero ← 200

  // Mostrar el nuevo valor de num (directamente y a través del puntero)
  Escribir("El nuevo valor de num es: ", num)
  Escribir("El nuevo valor de num a través del puntero es: ", *puntero)
Fin

Ejemplo en lenguaje C

#include <stdio.h> // Necesario para printf

int main(void) {
    // Declaración de una variable entera y un puntero a entero
    int num;
    int *puntero; //

    // Asignar un valor a num
    num = 100;

    // Asignar la dirección de memoria de num al puntero
    puntero = &num; // El operador '&' obtiene la dirección de memoria

    // Mostrar la dirección de memoria a la que apunta el puntero
    printf("La dirección de memoria de num es: %p\n", (void*)puntero); // '%p' para direcciones

    // Mostrar el valor de la variable a través del puntero
    printf("El valor de num a través del puntero es: %d\n", *puntero); // El operador '*' desreferencia

    // Modificar el valor de num a través del puntero
    *puntero = 200;

    // Mostrar el nuevo valor de num (directamente y a través del puntero)
    printf("El nuevo valor de num es: %d\n", num);
    printf("El nuevo valor de num a través del puntero es: %d\n", *puntero);
    return 0;
}

En este ejemplo, puntero primero almacena la dirección de num y luego se utiliza *puntero para ver y cambiar el valor almacenado en num de forma indirecta.

2. Paso de Parámetros a Funciones (Entrada y Salida)

Debido a que las variables se declaran en el interior de cada función se consideran en el ámbito local de la función. Esto resulta en que todas las variables declaradas son visibles (o conocidas) sólo en la función a la que pertencen. Para comounicar información entre las funciones se usan los parámetros. Estos se clasifican en “de entrada” o “paso por valor” y “de salida” o “paso por referencia”.

Paso por valor: la función recibe una copia del valor; modificarlo dentro de la funicón no afecta la variable original.

Paso por referencia: se pasa la dirección de la variable mediante un puntero; la función puede modificar el valor original mediante dicha referencia.

Pseudocódigo

// En C, se necesitaría include <stdio.h>
// Prototipo de la función que suma dos números y devuelve el resultado (paso por valor)
funcion_entero Sumar(entero a, entero b)
  inicio
    regresa (a + b)
  fin

// Prototipo de la función que resta dos números y guarda el resultado (paso por referencia)
// 'resultado' se pasa como un puntero para modificar la variable original fuera de la función
funcion_nada Restar(entero num1, entero num2, entero *resultado)
  inicio
    *resultado ← num1 - num2 // Acceder al valor en la dirección y modificarlo
  fin

// Programa Principal
principal()
  inicio
    entero x, y, z, resta_resultado // Declaración de variables

    // Paso por valor para la suma
    x ← 10
    y ← 5
    z ← Sumar(x, y) // Se pasa una copia de x e y. Z almacena el resultado.
    Escribir("La suma de ", x, " y ", y, " es: ", z)

    // Paso por referencia para la resta
    // La variable 'resta_resultado' se declara en el main.
    // La función 'Restar' modificará su valor directamente.
    Restar(x, y, &resta_resultado) // Se pasa la dirección de memoria de resta_resultado
    Escribir("La resta de ", x, " y ", y, " es: ", resta_resultado)
  fin

Lenguaje C

#include <stdio.h> // Necesario para printf

// Función que suma dos números y devuelve el resultado (paso por valor)
int Sumar(int a, int b) {
    return (a + b);
}

// Función que resta dos números y guarda el resultado (paso por referencia)
// 'resultado' se pasa como un puntero para modificar el valor original
void Restar(int num1, int num2, int *resultado) { // 'resultado' es un puntero
    *resultado = num1 - num2; // Se desreferencia el puntero para acceder y modificar el valor original
}

int main(void) {
    int x, y, z;
    int resta_resultado; // Variable donde se almacenará el resultado de la resta

    // Paso por valor para la suma
    x = 10;
    y = 5;
    z = Sumar(x, y); // Se pasan copias de x e y
    printf("La suma de %d y %d es: %d\n", x, y, z);

    // Paso por referencia para la resta
    // Se pasa la dirección de memoria de 'resta_resultado' usando '&'
    Restar(x, y, &resta_resultado);
    printf("La resta de %d y %d es: %d\n", x, y, resta_resultado);
    return 0;
}

En el ejemplo Restar, el parámetro resultado es un puntero. Al llamar a la función se pasa &resta_resultado; dentro, *resultado modifica el valor en la memoria del main().

3. Apuntadores para Recorrer o Trabajar con Arreglos

Un arreglo almacena elementos contiguos en memoria. El nombre del arreglo ya es la dirección del primer elemento, por lo que los punteros son ideales para recorrer y manipular sus elementos.

  • Declara un puntero y asígnale la dirección del primer elemento de un arreglo.
  • Incrementa (++) o decrementa () el puntero para moverte por el arreglo. Al incrementar, el puntero salta al siguiente elemento del tipo correspondiente.
  • El nombre del arreglo es una constante de dirección; no puede incrementarse como variable.
Nota de seguridad en C: el ejemplo clásico usa gets, pero está desaconsejado. Aquí incluimos una versión con fgets, que limita el tamaño de lectura.

Pseudocódigo (conversión de mayúsculas a minúsculas)

// Se asume la existencia de una función 'toLower' que convierte un carácter a minúscula.
// En C, se necesitaría include <stdio.h> y <string.h> para gets/puts, y <ctype.h> para tolower

principal()
  inicio
    cadena cadena_original[7]    // Declaración de un arreglo de caracteres (cadena)
    caracter *puntero_cadena   // Declaración de un puntero a carácter

    Escribir("Ingrese una cadena en mayúsculas: ")
    Leer(cadena_original) // Asumimos una instrucción para leer cadena

    // Asignar la dirección del primer elemento de la cadena al puntero
    puntero_cadena ← cadena_original // El nombre del arreglo es la dirección del primer elemento

    Escribir("La cadena en minúsculas es: ")

    // Recorrer la cadena utilizando el puntero hasta el final de la cadena (carácter nulo '\0')
    mientras (*puntero_cadena <> '\0') // Mientras el carácter actual no sea el final de la cadena
      inicio
        // Imprimir el carácter actual después de convertirlo a minúscula
        Escribir(toLower(*puntero_cadena)) // Asumiendo 'toLower' es una función que convierte a minúscula

        // Incrementar el puntero para moverlo al siguiente carácter de la cadena
        puntero_cadena ← puntero_cadena + 1 // O simplemente puntero_cadena++
      fin

    Escribir("\n") // Salto de línea al final
  fin

Lenguaje C (versión segura con fgets)

#include <stdio.h>   // printf, fgets
#include <ctype.h>   // tolower

int main(void) {
    char cadena_original[7];   // Tamaño fijo del buffer
    char *puntero_cadena;      // Puntero a carácter

    printf("Ingrese una cadena en mayúsculas (máx 6 caracteres): ");
    if (fgets(cadena_original, sizeof(cadena_original), stdin) == NULL) {
        return 0; // lectura fallida o EOF
    }

    // Quitar salto de línea si aparece
    for (int i = 0; cadena_original[i] != '\0'; ++i) {
        if (cadena_original[i] == '\n') { cadena_original[i] = '\0'; break; }
    }

    // El nombre del arreglo ya es la dirección del primer elemento
    puntero_cadena = cadena_original;

    printf("La cadena en minúsculas es: ");
    while (*puntero_cadena != '\0') {
        printf("%c", tolower((unsigned char)*puntero_cadena)); // tolower() convierte a minúscula
        puntero_cadena++; // Avanza al siguiente carácter
    }
    printf("\n");
    return 0;
}
Versión original con gets (no recomendada)
Advertencia: gets fue eliminada del estándar C por insegura (no controla longitud). Se muestra solo con fines didácticos.
#include <stdio.h>
#include <ctype.h>

int main(void) {
    char cadena_original[7];
    char *puntero_cadena;

    printf("Ingrese una cadena en mayúsculas: ");
    gets(cadena_original); // NO RECOMENDADO

    puntero_cadena = cadena_original;
    printf("La cadena en minúsculas es: ");
    while (*puntero_cadena != '\0') {
        printf("%c", tolower((unsigned char)*puntero_cadena));
        puntero_cadena++;
    }
    printf("\n");
    return 0;
}

Ejercicios de Práctica Sugeridos

Implementa en pseudocódigo y en C:

1. Funciones con Parámetros por Referencia
  • intercambiar_valores: recibe direcciones de dos enteros y los intercambia. main() lee, llama e imprime.
  • Operaciones básicas: función que reciba dos números (por valor) y devuelva suma, resta, multiplicación y división mediante parámetros por referencia (punteros).
  • obtener_max_min: recibe un arreglo de enteros y dos punteros; guarda en esas direcciones el máximo y el mínimo.
2. Manipulación de Arreglos con Punteros
  • Declara un arreglo de 5 enteros. Usa un puntero para leer los 5 números y otro para imprimirlos en orden inverso.
  • copiar_arreglo(origen, destino, n): copia con aritmética de punteros.
3. Funciones con Punteros para Cálculos Específicos
  • calcular_promedio_y_suma: recibe arreglo de float y dos punteros a float; calcula y almacena promedio y suma.