Clase 1 C3 Ejercicio de punteros y estructuras C++

Tutorial: Punteros y Estructuras en C++



Los punteros y las estructuras son dos pilares esenciales de C++ que, cuando se combinan, permiten una gestión de memoria más eficiente y la creación de tipos de datos complejos.

1. ¿Qué son las Estructuras (Structs)?

En primer lugar, una estructura (o struct) en C++ es un tipo de dato definido por el usuario que agrupa variables de diferentes tipos de datos bajo un único nombre. Piensa en ella como una plantilla o un "plano" para crear objetos que tienen un conjunto específico de características.

Sintaxis básica de una estructura:

C++
struct NombreDeLaEstructura {
    TipoDeDato miembro1;
    TipoDeDato miembro2;
    // ...
};

Ejemplo: Para representar a una persona, podríamos agrupar su nombre y edad.

C++
struct Persona {
    std::string nombre;
    int edad;
};

Podemos crear una variable de tipo Persona y acceder a sus miembros usando el operador punto (.):

C++
Persona p1;
p1.nombre = "Alice";
p1.edad = 30;
std::cout << p1.nombre << " tiene " << p1.edad << " años." << std::endl;

2. ¿Qué son los Punteros?

En segundo lugar, un puntero es una variable que almacena la dirección de memoria de otra variable. En lugar de contener un valor directamente, un puntero "apunta" a dónde se encuentra ese valor en la memoria RAM. Son cruciales para la gestión dinámica de memoria, pasar argumentos a funciones por referencia y trabajar con estructuras de datos complejas como listas enlazadas o árboles.

  • Declaración de un puntero: Se usa el asterisco (*) para declarar un puntero.

    C++
    TipoDeDato* nombreDelPuntero;
    

    Ejemplo: int* ptr_entero; (un puntero a un entero)

  • Operador de Dirección (&): Este operador devuelve la dirección de memoria de una variable.

    C++
    int num = 10;
    int* ptr_num = &num; // ptr_num ahora almacena la dirección de 'num'
    
  • Operador de Desreferencia (*): Este operador se utiliza para acceder al valor almacenado en la dirección a la que apunta el puntero.

    C++
    std::cout << *ptr_num << std::endl; // Imprime el valor de 'num', que es 10
    

3. Punteros a Estructuras

La combinación de punteros y estructuras es muy potente. Un puntero puede apuntar a una variable de tipo estructura.

Declaración y asignación:

C++
Persona p2;
Persona* ptr_p2 = &p2; // ptr_p2 ahora apunta a la estructura p2

Acceso a miembros de una estructura a través de un puntero:

Cuando se tiene un puntero a una estructura, no se puede usar el operador punto (.) directamente para acceder a sus miembros. En su lugar, se utiliza el operador flecha (->), que es una forma abreviada de desreferenciar el puntero y luego acceder al miembro.

(*ptr_p2).nombre es equivalente a ptr_p2->nombre

Ejemplo:

C++
Persona p2;
Persona* ptr_p2 = &p2;

ptr_p2->nombre = "Bob"; // Accediendo al miembro 'nombre' a través del puntero
ptr_p2->edad = 25;     // Accediendo al miembro 'edad' a través del puntero

std::cout << "Nombre: " << ptr_p2->nombre << ", Edad: " << ptr_p2->edad << std::endl;

Ejercicio Práctico: Gestión Simple de Estudiantes

Vamos a crear un programa que utilice estructuras para representar estudiantes y punteros para manipularlos, incluyendo la asignación dinámica de memoria para un arreglo de estudiantes.

Requerimientos:

  1. Definir una estructura Estudiante con id (entero), nombre (cadena de caracteres) y promedio (flotante).
  2. Crear una función para inicializar los datos de un estudiante, recibiendo un puntero a Estudiante.
  3. Crear una función para mostrar los datos de un estudiante, recibiendo un puntero a Estudiante.
  4. En la función main, crear un arreglo dinámico de punteros a Estudiante para almacenar la información de varios estudiantes.
  5. Permitir al usuario ingresar la cantidad de estudiantes.
  6. Usar un bucle para inicializar y luego mostrar los datos de cada estudiante.
  7. Liberar la memoria asignada dinámicamente al finalizar.

Solución Paso a Paso:

C++
#include <iostream> // Para entrada/salida (cout, cin)
#include <string>   // Para usar std::string
#include <vector>   // No se usa directamente para la solución dinámica con punteros, pero es útil saber que existe

// 1. Definir la estructura Estudiante
struct Estudiante {
    int id;
    std::string nombre;
    float promedio;
};

// 2. Función para inicializar los datos de un estudiante
// Recibe un puntero a Estudiante, permitiendo modificar el objeto original
void inicializarEstudiante(Estudiante* est) {
    std::cout << "--- Ingresar datos del Estudiante ---" << std::endl;
    std::cout << "ID: ";
    std::cin >> est->id; // Acceder al miembro 'id' a través del puntero
    std::cin.ignore();   // Limpiar el buffer de entrada después de leer el entero
    std::cout << "Nombre: ";
    std::getline(std::cin, est->nombre); // Acceder al miembro 'nombre' a través del puntero
    std::cout << "Promedio: ";
    std::cin >> est->promedio; // Acceder al miembro 'promedio' a través del puntero
    std::cin.ignore();   // Limpiar el buffer de entrada
    std::cout << "------------------------------------" << std::endl;
}

// 3. Función para mostrar los datos de un estudiante
// Recibe un puntero constante a Estudiante, indicando que no modificará el objeto
void mostrarEstudiante(const Estudiante* est) {
    std::cout << "ID: " << est->id << std::endl;
    std::cout << "Nombre: " << est->nombre << std::endl;
    std::cout << "Promedio: " << est->promedio << std::endl;
}

int main() {
    int cantidadEstudiantes;

    std::cout << "Ingrese la cantidad de estudiantes a registrar: ";
    std::cin >> cantidadEstudiantes;
    std::cin.ignore(); // Limpiar el buffer de entrada

    // Validar que la cantidad sea positiva
    if (cantidadEstudiantes <= 0) {
        std::cerr << "La cantidad de estudiantes debe ser mayor que cero." << std::endl;
        return 1; // Salir con código de error
    }

    // 4. Crear un arreglo dinámico de punteros a Estudiante
    // Cada elemento del arreglo es un puntero a una estructura Estudiante
    Estudiante** listaEstudiantes = new Estudiante*[cantidadEstudiantes];

    // 5. Bucle para inicializar cada estudiante dinámicamente
    for (int i = 0; i < cantidadEstudiantes; ++i) {
        // Asignar memoria para cada objeto Estudiante individualmente
        listaEstudiantes[i] = new Estudiante;
        std::cout << "\nDatos para el Estudiante " << i + 1 << ":" << std::endl;
        inicializarEstudiante(listaEstudiantes[i]); // Pasar el puntero al estudiante actual
    }

    std::cout << "\n--- Mostrando todos los estudiantes ---" << std::endl;
    // Bucle para mostrar los datos de cada estudiante
    for (int i = 0; i < cantidadEstudiantes; ++i) {
        std::cout << "\nEstudiante " << i + 1 << ":" << std::endl;
        mostrarEstudiante(listaEstudiantes[i]); // Pasar el puntero al estudiante actual
    }

    // 6. Liberar la memoria asignada dinámicamente
    // Primero, liberar la memoria de cada objeto Estudiante
    for (int i = 0; i < cantidadEstudiantes; ++i) {
        delete listaEstudiantes[i];
        listaEstudiantes[i] = nullptr; // Buenas práctica: poner el puntero a nullptr después de liberar
    }
    // Luego, liberar la memoria del arreglo de punteros
    delete[] listaEstudiantes;
    listaEstudiantes = nullptr; // Buenas práctica: poner el puntero del arreglo a nullptr

    std::cout << "\nMemoria liberada exitosamente." << std::endl;

    return 0; // Salida exitosa
}

Explicación del ejercicio:

  • Estudiante** listaEstudiantes = new Estudiante*[cantidadEstudiantes];
    • Aquí estamos creando un arreglo de punteros a Estudiante. Esto significa que listaEstudiantes es un puntero que apunta al inicio de un bloque de memoria que contiene cantidadEstudiantes direcciones de memoria. Cada una de esas direcciones puede (y de hecho, lo hará) apuntar a un objeto Estudiante real.
  • listaEstudiantes[i] = new Estudiante;
    • Dentro del bucle, para cada posición i del arreglo de punteros, asignamos dinámicamente memoria para un nuevo objeto Estudiante y la dirección de esa memoria se almacena en listaEstudiantes[i].
  • inicializarEstudiante(listaEstudiantes[i]); y mostrarEstudiante(listaEstudiantes[i]);
    • Pasamos la dirección de cada objeto Estudiante a las funciones. Dentro de las funciones, se utiliza el operador flecha (->) para acceder y manipular los miembros de la estructura a través del puntero.
  • Gestión de Memoria (new y delete):
    • Cuando asignas memoria con new, debes liberarla con delete para evitar fugas de memoria (memory leaks).
    • Para un arreglo dinámico (new Tipo[]), se usa delete[] para liberar todo el bloque de memoria.
    • Es crucial liberar primero la memoria de cada objeto individual al que apuntan los punteros (delete listaEstudiantes[i];) y luego liberar la memoria del arreglo de punteros en sí (delete[] listaEstudiantes;).

Este ejercicio ilustra cómo los punteros permiten la gestión dinámica de objetos de estructura, lo cual es fundamental en la programación de sistemas y aplicaciones más complejas en C++.


Referencias Bibliográficas:

American Psychological Association. (2020). Publication Manual of the American Psychological Association (7th ed.). American Psychological Association.

Lippman, S. B., Lajoie, J., & Moo, B. E. (2012). C++ Primer (5th ed.). Addison-Wesley Professional.

Stroustrup, B. (2013). The C++ Programming Language (4th ed.). Addison-Wesley Professional. 

Comentarios