Conceptos Avanzados de C++: Polimorfismo, Herencia y Plantillas de Clase

Gestión de Contenidos Publicables

Este segmento explora la aplicación de la herencia y el polimorfismo en C++ para gestionar diferentes tipos de medios publicables. Se demuestra cómo las clases abstractas y las funciones virtuales puras permiten crear una interfaz común para entidades diversas, como libros, películas y música, facilitando el manejo polimórfico mediante punteros.

Definiciones de Clases

A continuación, se presentan las declaraciones de las clases base y derivadas que definen la jerarquía de contenidos publicables.

#pragma once
#include <string>
#include <memory> // Para std::unique_ptr

// Clase base abstracta para cualquier tipo de medio publicable
class MedioPublicable {
public:
    explicit MedioPublicable(const std::string &identificador_ = "");
    virtual ~MedioPublicable() = default; // Destructor virtual para manejo polimórfico
    virtual void difundir() const = 0;   // Función virtual pura: publicar
    virtual void consumir() const = 0;   // Función virtual pura: usar/consumir

protected:
    std::string identificador; // Nombre o título del medio
};

// Clase para representar un Libro
class Libro : public MedioPublicable {
public:
    Libro(const std::string &titulo_, const std::string &autor_ = "");
    void difundir() const override; // Implementación de difundir para Libro
    void consumir() const override; // Implementación de consumir para Libro
private:
    std::string autor;
};

// Clase para representar una Película
class Pelicula : public MedioPublicable {
public:
    Pelicula(const std::string &titulo_, const std::string &director_ = "");
    void difundir() const override; // Implementación de difundir para Pelicula
    void consumir() const override; // Implementación de consumir para Pelicula
private:
    std::string director;
};

// Clase para representar una Grabación Musical
class GrabacionMusical : public MedioPublicable {
public:
    GrabacionMusical(const std::string &titulo_, const std::string &artista_ = "");
    void difundir() const override; // Implementación de difundir para GrabacionMusical
    void consumir() const override; // Implementación de consumir para GrabacionMusical
private:
    std::string artista;
};

Implementación de Métodos

Las implementaciones detalladas de los constructores y las funciones virtuales para cada clase.

#include <iostream>
#include "MedioPublicable.hpp"

// Implementación de la clase base
MedioPublicable::MedioPublicable(const std::string &identificador_) : identificador(identificador_) {}

// Implementación de la clase Libro
Libro::Libro(const std::string &titulo_, const std::string &autor_) : MedioPublicable(titulo_), autor(autor_) {}

void Libro::difundir() const {
    std::cout << "Publicando libro \«" << identificador << "\» por " << autor << '\n';
}
void Libro::consumir() const {
    std::cout << "Leyendo libro \«" << identificador << "\» de " << autor << '\n';
}

// Implementación de la clase Pelicula
Pelicula::Pelicula(const std::string &titulo_, const std::string &director_) : MedioPublicable(titulo_), director(director_) {}

void Pelicula::difundir() const {
    std::cout << "Estrenando película <" << identificador << "> dirigida por " << director << '\n';
}
void Pelicula::consumir() const {
    std::cout << "Viendo película <" << identificador << "> de " << director << '\n';
}

// Implementación de la clase GrabacionMusical
GrabacionMusical::GrabacionMusical(const std::string &titulo_, const std::string &artista_) : MedioPublicable(titulo_), artista(artista_) {}

void GrabacionMusical::difundir() const {
    std::cout << "Lanzando canción <" << identificador << "> interpretada por " << artista << '\n';
}
void GrabacionMusical::consumir() const {
    std::cout << "Escuchando canción <" << identificador << "> de " << artista << '\n';
}

Ejemplo de Uso y Polimorfismo

Este archivo principal demuestra cómo se pueden manejar diferentes tipos de medios de forma polimórfica, tanto con punteros crudos como con punteros inteligentes.

#include <memory>
#include <iostream>
#include <vector>
#include "MedioPublicable.hpp"

// Función de prueba utilizando punteros crudos
void ejecutarPruebaPunterosCrudos() {
    std::vector<MedioPublicable*> medios;
    medios.push_back(new Libro("Cien años de soledad", "Gabriel García Márquez"));
    medios.push_back(new Pelicula("Parasite", "Bong Joon-ho"));
    medios.push_back(new GrabacionMusical("Bohemian Rhapsody", "Queen"));

    for (MedioPublicable* pMedio : medios) {
        pMedio->difundir();
        pMedio->consumir();
        std::cout << '\n';
        delete pMedio; // Esencial liberar la memoria
    }
}

// Función de prueba utilizando punteros inteligentes (std::unique_ptr)
void ejecutarPruebaPunterosInteligentes() {
    std::vector<std::unique_ptr<MedioPublicable>> medios;
    medios.push_back(std::make_unique<Libro>("Don Quijote de la Mancha", "Miguel de Cervantes"));
    medios.push_back(std::make_unique<Pelicula>("El Padrino", "Francis Ford Coppola"));
    medios.push_back(std::make_unique<GrabacionMusical>("Imagine", "John Lennon"));

    for (const auto& pMedio : medios) {
        pMedio->difundir();
        pMedio->consumir();
        std::cout << '\n';
    }
}

// Función de prueba con instancias directas
void ejecutarPruebaInstanciasDirectas() {
    Libro miLibro("Clean Code", "Robert C. Martin");
    miLibro.difundir();
    miLibro.consumir();
}

int main() {
    std::cout << "Demostración de Polimorfismo en Tiempo de Ejecución con C++\n";
    std::cout << "\n=== Prueba 1: Usando Punteros Crudos ===\n";
    ejecutarPruebaPunterosCrudos();
    std::cout << "\n=== Prueba 2: Usando Punteros Inteligentes (std::unique_ptr) ===\n";
    ejecutarPruebaPunterosInteligentes();
    std::cout << "\n=== Prueba 3: Uso Directo de Instancias ===\n";
    ejecutarPruebaInstanciasDirectas();
}

Análisis de Conceptos Clave

Pregunta 1: Mecanismo de Clase Abstracta

  1. ¿Qué elementos en la clase MedioPublicable la definen como abstracta?
    La presencia de al menos una función virtual pura (= 0) en su declaración la convierte en una clase abstracta. Específicamente, las funciones virtual void difundir() const = 0; y virtual void consumir() const = 0; son las responsables.
  2. ¿Sería posible compilar directamente MedioPublicable miObjeto; en main?
    No, la compilación fallaría. Las clases abstractas no pueden ser instanciadas directamente debido a sus funciones virtuales puras no implementadas.

Pregunta 2: Funciones Virtuales Puras e Herencia de Interfaz

  1. ¿Cuáles son las dos funciones que Libro, Pelicula y GrabacionMusical deben implementar obligatoriamente?
    Deben implementar void difundir() const override; y void consumir() const override;.
  2. ¿Qué sucede si se omite la palabra clave const en la implementación de una función virtual en una clase derivada, por ejemplo, void Libro::difundir() en lugar de void Libro::difundir() const?
    Se produciría un error de compilación. La firma de la función en la clase derivada debe coincidir exactamente con la de la función virtual pura en la clase base, incluyendo calificadores como const. Si no coincide, el compilador interpretará que la clase derivada no ha proporcionado una implementación para la función virtual pura, haciendo que la clase derivada también sea abstracta y, por tanto, no instanciable directamente (a menos que también se declare como abstracta).

Pregunta 3: Polimorfismo en Tiempo de Ejecución y Destructores Virtuales

  1. En la función ejecutarPruebaPunterosCrudos(), ¿cuál es el tipo declarado del puntero pMedio?
    El tipo declarado es MedioPublicable*.
  2. ¿Por qué el destructor de MedioPublicable está declarado como virtual?
    Se declara como virtual para asegurar que, cuando se elimina un objeto derivado a través de un puntero a la clase base (como en delete pMedio;), se llame correctamente al destructor de la clase derivada. Si el destructor no fuera virtual, solo se llamaría al destructor de la clase base, lo que podría llevar a fugas de memoria o comportamiento indefinido para los recursos específicos de la clase derivada.

Estadísticas de Ventas de Obras Literarias

Esta sección ilustra el uso de la composición de clases y la sobrecarga de operadores para gestionar y presentar registros de ventas de libros. Además, se demuestra cómo ordenar colecciones de objetos utilizando funciones de comparación personalizadas.

Definiciones de Clases para Libros y Ventas

Se definen dos clases: ObraLiteraria para los detalles del libro y RegistroVentaLibro para la inofrmación de venta, que compone una ObraLiteraria.

#pragma once
#include <string>
#include <iostream>

// Clase que representa los detalles de una obra literaria
class ObraLiteraria {
public:
    ObraLiteraria(const std::string &titulo_,
                  const std::string &autor_,
                  const std::string &traductor_,
                  const std::string &identificadorISBN_,
                  double costoBase_);

    // Sobrecarga del operador de inserción para imprimir detalles de la obra
    friend std::ostream& operator<<(std::ostream &salida, const ObraLiteraria &obra);

private:
    std::string titulo;          // Título del libro
    std::string autor;           // Autor
    std::string traductor;       // Traductor (puede estar vacío)
    std::string identificadorISBN; // Número ISBN
    double costoBase;            // Precio de catálogo
};

#pragma once
#include <string>
#include <iostream>
#include "ObraLiteraria.hpp" // Incluimos la clase ObraLiteraria

// Clase que representa un registro de venta para una obra literaria
class RegistroVentaLibro {
public:
    RegistroVentaLibro(const ObraLiteraria &obraAsociada_, double precioDeVenta_, int unidadesVendidas_);
    int obtenerCantidadVendida() const;    // Retorna la cantidad de unidades vendidas
    double calcularIngresosTotales() const; // Retorna los ingresos generados por esta venta

    // Sobrecarga del operador de inserción para imprimir detalles del registro de venta
    friend std::ostream& operator<<(std::ostream &salida, const RegistroVentaLibro &item);

private:
    ObraLiteraria obraAsociada;      // Composición: el libro que se vendió
    double precioDeVenta;           // Precio por unidad vendida
    int unidadesVendidas;           // Cantidad de unidades en esta venta
};

Implementaciones de Clases

Implementación de los constructores y la sobrecarga del operador de inserción para ambas clases.

#include <iomanip>
#include <iostream>
#include <string>
#include "ObraLiteraria.hpp"

ObraLiteraria::ObraLiteraria(const std::string &titulo_,
                             const std::string &autor_,
                             const std::string &traductor_,
                             const std::string &identificadorISBN_,
                             double costoBase_)
    : titulo{titulo_}, autor{autor_}, traductor{traductor_}, identificadorISBN{identificadorISBN_}, costoBase{costoBase_} {}

std::ostream& operator<<(std::ostream &salida, const ObraLiteraria &obra) {
    using std::left;
    using std::setw;

    salida << left;
    salida << setw(18) << "Título:" << obra.titulo << '\n'
           << setw(18) << "Autor:" << obra.autor << '\n'
           << setw(18) << "Traductor:" << obra.traductor << '\n'
           << setw(18) << "ISBN:" << obra.identificadorISBN << '\n'
           << setw(18) << "Precio Base:" << std::fixed << std::setprecision(2) << obra.costoBase;

    return salida;
}

#include <iomanip>
#include <iostream>
#include <string>
#include "RegistroVentaLibro.hpp"

RegistroVentaLibro::RegistroVentaLibro(const ObraLiteraria &obraAsociada_,
                                       double precioDeVenta_,
                                       int unidadesVendidas_)
    : obraAsociada{obraAsociada_}, precioDeVenta{precioDeVenta_}, unidadesVendidas{unidadesVendidas_} {}

int RegistroVentaLibro::obtenerCantidadVendida() const {
    return unidadesVendidas;
}

double RegistroVentaLibro::calcularIngresosTotales() const {
    return unidadesVendidas * precioDeVenta;
}

std::ostream& operator<<(std::ostream &salida, const RegistroVentaLibro &item) {
    using std::left;
    using std::setw;

    salida << left;
    salida << item.obraAsociada << '\n'
           << setw(18) << "Precio de Venta:" << std::fixed << std::setprecision(2) << item.precioDeVenta << '\n'
           << setw(18) << "Unidades Vendidas:" << item.unidadesVendidas << '\n'
           << setw(18) << "Ingresos Generados:" << std::fixed << std::setprecision(2) << item.calcularIngresosTotales();

    return salida;
}

Programa Principal para Gestión de Ventas

Este programa interactúa con el usuario para recopilar datos de ventas y luego los ordena e imprime.

#include <algorithm>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>
#include "RegistroVentaLibro.hpp"

// Función de comparación para ordenar por cantidad vendida (descendente)
bool compararPorCantidadVendida(const RegistroVentaLibro &r1, const RegistroVentaLibro &r2) {
    return r1.obtenerCantidadVendida() > r2.obtenerCantidadVendida();
}

void procesarVentas() {
    using std::cin;
    using std::cout;
    using std::getline;
    using std::sort;
    using std::string;
    using std::vector;
    using std::ws; // Para limpiar el buffer de entrada

    vector<RegistroVentaLibro> registrosDeVenta;

    int numeroDeObras;
    cout << "Ingrese el número de obras literarias a registrar: ";
    cin >> numeroDeObras;

    cout << "\nComenzando el registro de ventas de libros...\n";
    for(int i = 0; i < numeroDeObras; ++i) {
        string titulo, autor, traductor, isbn;
        double precioLista;
        cout << string(25, '=') << " Detalles de la Obra " << (i+1) << string(25, '=') << '\n';
        cout << "Título: "; getline(cin>>ws, titulo);
        cout << "Autor: "; getline(cin>>ws, autor);
        cout << "Traductor (si aplica): "; getline(cin>>ws, traductor);
        cout << "ISBN: "; getline(cin>>ws, isbn);
        cout << "Precio de Lista: "; cin >> precioLista;

        ObraLiteraria obra(titulo, autor, traductor, isbn, precioLista);

        double precioVentaUnitario;
        int cantidadTotalVendida;

        cout << "Precio de venta unitario: "; cin >> precioVentaUnitario;
        cout << "Cantidad vendida: "; cin >> cantidadTotalVendida;

        RegistroVentaLibro nuevoRegistro(obra, precioVentaUnitario, cantidadTotalVendida);
        registrosDeVenta.push_back(nuevoRegistro);
    }

    // Ordenar los registros por la cantidad vendida en orden descendente
    sort(registrosDeVenta.begin(), registrosDeVenta.end(), compararPorCantidadVendida);

    cout << string(20, '=') << " Resumen de Ventas de Obras Literarias " << string(20, '=') << '\n';
    for(auto &registro : registrosDeVenta) {
        cout << registro << '\n';
        cout << string(60, '-') << '\n';
    }
}

int main() {
    procesarVentas();
}

Análisis de Características Implementadas

Pregunta 1: Sobrecarga del Operador <<

  1. ¿Cuántas veces se ha sobrecargado el operador << en este conjunto de archivos, y cuáles son sus firmas?
    El operador << se ha sobrecargado dos veces:

    • std::ostream& operator<<(std::ostream&, const ObraLiteraria&)
    • std::ostream& operator<<(std::ostream&, const RegistroVentaLibro&)
  2. Mencione una línea de código del main que haga uso de la sobrecarga del operador << para imprimir un objeto.
    cout << registro << '\n'; (donde registro es un objeto de tipo RegistroVentaLibro).

Pregunta 2: Estadísticas de Ventas de Libros

  1. Describa cómo se logra el ordenamiento descendente de los registros de venta.
    Se utiliza la función std::sort de la biblioteca estándar, proporcionándole los iteradores al inicio y fin del vector registrosDeVenta, y una función de comparación personalizada compararPorCantidadVendida. Esta función de comparación compara los valores retornados por obtenerCantidadVendida() de dos objetos RegistroVentaLibro usando el operador >, lo que resulta en un orden descendente.
  2. Proporcione un ejemplo de cómo se podría realizar el mismo ordenamiento usando una función lambda en lugar de una función regular.
    std::sort(registrosDeVenta.begin(), registrosDeVenta.end(),
             [](const RegistroVentaLibro& rA, const RegistroVentaLibro& rB) {
                 return rA.obtenerCantidadVendida() > rB.obtenerCantidadVendida();
             });
    
    

Simulación de Mascotas Robóticas

Este ejercicio es una demostración concisa de polimorfismo y herencia con clases abstractas, similar al primer ejercicio pero con un dominio más simple: mascotas robóticas que pueden emitir sonidos característicos.

Clases de Mascotas Robóticas

Declaración de la clase base abstracta MascotaRobotica y sus especializaciones GatoRobot y PerroRobot.

#pragma once
#include <string>
#include <memory> // Para std::unique_ptr

// Clase base abstracta para mascotas robóticas
class MascotaRobotica {
public:
    explicit MascotaRobotica(const std::string& apodo_);
    virtual ~MascotaRobotica() = default; // Destructor virtual
    const std::string& obtenerApodo() const { return apodo; } // Getter para el apodo
    virtual std::string emitirSonido() const = 0; // Función virtual pura: el sonido de la mascota
private:
    std::string apodo; // Nombre o apodo de la mascota
};

// Constructor inline
inline MascotaRobotica::MascotaRobotica(const std::string& apodo_) : apodo(apodo_) {}

// Clase para un Gato Robótico
class GatoRobot : public MascotaRobotica {
public:
    // Hereda los constructores de la clase base
    using MascotaRobotica::MascotaRobotica;
    std::string emitirSonido() const override { return "miau guau"; } // Sonido de gato robot
};

// Clase para un Perro Robótico
class PerroRobot : public MascotaRobotica {
public:
    // Hereda los constructores de la clase base
    using MascotaRobotica::MascotaRobotica;
    std::string emitirSonido() const override { return "guau guau"; } // Sonido de perro robot
};

Programa de Prueba para Mascotas Robóticas

Este archivo principal contiene funciones de prueba para demostrar el manejo de las mascotas robóticas utilizando punteros crudos, punteros inteligentes e instancias directas.

#include <iostream>
#include <memory>
#include <vector>
#include "MascotaRobotica.hpp"

// Prueba con punteros crudos
void pruebaConPunterosCrudos() {
    std::vector<MascotaRobotica*> mascotas;
    mascotas.push_back(new GatoRobot("Miko"));
    mascotas.push_back(new PerroRobot("Max"));

    for(MascotaRobotica *pMascota : mascotas) {
        std::cout << pMascota->obtenerApodo() << " dice: " << pMascota->emitirSonido() << '\n';
        delete pMascota; // Liberar memoria asignada
    }
}

// Prueba con punteros inteligentes (std::unique_ptr)
void pruebaConPunterosInteligentes() {
    std::vector<std::unique_ptr<MascotaRobotica>> mascotas;
    mascotas.push_back(std::make_unique<GatoRobot>("Garfield"));
    mascotas.push_back(std::make_unique<PerroRobot>("Pluto"));

    for(auto const &pMascota : mascotas)
        std::cout << pMascota->obtenerApodo() << " dice: " << pMascota->emitirSonido() << '\n';
}

// Prueba con instancias directas de las clases
void pruebaConInstanciasDirectas() {
    const GatoRobot miGato("Luna");
    std::cout << miGato.obtenerApodo() << " dice: " << miGato.emitirSonido() << '\n';

    const PerroRobot miPerro("Buddy");
    std::cout << miPerro.obtenerApodo() << " dice: " << miPerro.emitirSonido() << '\n';
}

int main() {
    std::cout << "=== Pruebas de Mascotas Robóticas ===\n";
    std::cout << "\n-- Prueba 1: Usando punteros crudos --\n";
    pruebaConPunterosCrudos();

    std::cout << "\n-- Prueba 2: Usando punteros inteligentes --\n";
    pruebaConPunterosInteligentes();

    std::cout << "\n-- Prueba 3: Usando instancias directas --\n";
    pruebaConInstanciasDirectas();
}

Implementación de Plantilla de Clase para Números Complejos

Este apartado presenta la creación de una plantilla de clase NumeroComplejo que permite trabajar con números complejos de difreentes tipos numéricos (int, double, etc.). Demuestra la sobrecarga de operadores para aritmética (suma, asignación con suma), comparación y operaciones de entrada/salida.

Plantilla de Clase NumeroComplejo

Definición de la plantilla de clase con constructores, métodos de acceso y sobrecarga de operadores.

#pragma once
#include <iostream> // Para std::ostream y std::istream

// Plantilla de clase para representar un número complejo
template <typename T>
class NumeroComplejo {
public:
    // Constructores
    NumeroComplejo() = default; // Constructor por defecto
    NumeroComplejo(T r, T i) : parteReal(r), parteImaginaria(i) {} // Constructor con partes real e imaginaria
    NumeroComplejo(const NumeroComplejo& otro) : parteReal(otro.parteReal), parteImaginaria(otro.parteImaginaria) {} // Constructor de copia

    // Métodos de acceso (getters)
    T obtenerParteReal() const { return parteReal; }
    T obtenerParteImaginaria() const { return parteImaginaria; }

    // Sobrecarga del operador de asignación con suma (+=)
    NumeroComplejo& operator+=(const NumeroComplejo& rhs) {
        parteReal += rhs.parteReal;
        parteImaginaria += rhs.parteImaginaria;
        return *this;
    }

    // Sobrecarga del operador de suma binaria (+)
    // Implementado como función friend para permitir conmutatividad
    friend NumeroComplejo operator+(NumeroComplejo lhs, const NumeroComplejo& rhs) {
        lhs += rhs; // Reutiliza el operador +=
        return lhs;
    }

    // Sobrecarga del operador de igualdad (==)
    friend bool operator==(const NumeroComplejo& a, const NumeroComplejo& b) {
        return a.parteReal == b.parteReal && a.parteImaginaria == b.parteImaginaria;
    }

    // Sobrecarga del operador de inserción (salida <<)
    friend std::ostream& operator<<(std::ostream& os, const NumeroComplejo& c) {
        os << c.parteReal;
        if (c.parteImaginaria >= 0) os << '+';
        os << c.parteImaginaria << 'i';
        return os;
    }

    // Sobrecarga del operador de extracción (entrada >>)
    // Permite leer las partes real e imaginaria directamente
    friend std::istream& operator>>(std::istream& is, NumeroComplejo& c) {
        // Asume que la entrada será en formato "real imaginaria" (ej. 3.5 2.1)
        return is >> c.parteReal >> c.parteImaginaria;
    }

private:
    T parteReal{};     // Parte real del número complejo
    T parteImaginaria{}; // Parte imaginaria del número complejo
};

Programa Principal de Pruebas para NumeroComplejo

Este programa demuestra la funcionalidad de la plantilla de clase NumeroComplejo con diferentes tipos de datos y operaciones.

#include <iostream>
#include "NumeroComplejo.hpp" // Incluye la plantilla de clase

// Función para realizar la primera serie de pruebas con números complejos enteros
void realizarPruebaEnteros() {
    using std::cout;
    using std::boolalpha; // Para imprimir booleanos como "true" o "false"

    NumeroComplejo<int> z1(5, -3); // Un número complejo (5 - 3i)
    NumeroComplejo<int> z2 = z1;   // Constructor de copia

    cout << "z1 = " << z1 << '\n';
    cout << "z2 = " << z2 << '\n';
    cout << "z1 + z2 = " << z1 + z2 << '\n';

    z1 += z2; // z1 ahora es z1 + z2
    cout << "Después de z1 += z2, z1 = " << z1 << '\n';
    cout << "¿z1 es igual a z2? " << boolalpha << (z1 == z2) << '\n';
}

// Función para realizar la segunda serie de pruebas con números complejos de doble precisión
void realizarPruebaDoubles() {
    using std::cin;
    using std::cout;

    NumeroComplejo<double> z3, z4;
    cout << "Introduzca dos números complejos (formato 'real imaginario'):\n";
    cout << "Introduzca z3: ";
    cin >> z3;
    cout << "Introduzca z4: ";
    cin >> z4;

    cout << "z3 = " << z3 << '\n';
    cout << "z4 = " << z4 << '\n';

    const NumeroComplejo<double> z5(z3); // Constructor de copia
    cout << "z5 (copia de z3). Parte real: " << z5.obtenerParteReal() << '\n';
    cout << "z5 (copia de z3). Parte imaginaria: " << z5.obtenerParteImaginaria() << '\n';
}

int main() {
    std::cout << "=== Pruebas de la Plantilla de Clase NumeroComplejo ===\n";
    std::cout << "\n--- Prueba 1: Números Complejos Enteros ---\n";
    realizarPruebaEnteros();

    std::cout << "\n--- Prueba 2: Números Complejos de Doble Precisión ---\n";
    realizarPruebaDoubles();
}

Etiquetas: C++ oop polimorfismo clases abstractas herencia

Publicado el 10-12 17:46