Saltar al contenido
ES EN

Ejercicio resuelto: flota polimórfica con clonación en C++

Ejercicio resuelto en C++ de herencia y polimorfismo: flota de vehículos, clase abstracta, remolque decorador y copia profunda con clonar y unique_ptr.

Código de Examen C++ Programación orientada a objetos Nivel nota Código probado

Código de Examen: Ejercicio resuelto: flota polimórfica con clonación en C++
Programación Orientada a Objetos · Examen finalEjercicio 4 · 2,5 puntos · 40 min

La empresa de reparto Última Parada gestiona una flota heterogénea. Se pide un programa en C++20 que mantenga la flota y responda a consultas de coste. Todos los importes son enteros y se expresan en céntimos. Todas las distancias son enteros y se expresan en kilómetros.

El diseño es obligatorio. Debe existir una clase abstracta Vehiculo con un identificador y destructor virtual. Debe declarar los métodos virtuales puros coste(km), autonomia(), describir(...) y clonar(), este último devolviendo std::unique_ptr<Vehiculo>. Además tendrá un método virtual ajustar(valor) que devuelve false por defecto. La flota se guarda como std::vector<std::unique_ptr<Vehiculo>>. Está prohibido consultar el tipo dinámico (dynamic_cast, typeid o campos de tipo).

Tipos de vehículo. Furgoneta (F id cpk aut): su coste es cpk·km y su autonomía es aut. Eléctrica (E id cpk aut): su coste es cpk·km + 150, por la tarifa fija de conexión, y su autonomía es aut. Ambas comparten el atributo cpk y ajustar lo sustituye. Bicicleta de carga (B id): su coste es 40·km, su autonomía es 25 y no es ajustable.

Remolque (R id base extra): crea un vehículo nuevo que contiene una copia del vehículo base tal como está en ese instante. La base puede ser a su vez un remolque. Su coste es coste_base(km) + extra·km y su autonomía es autonomia_base·8/10 en división entera, calculando primero el producto. Para un remolque, ajustar sustituye su extra. Los cambios posteriores en el vehículo original no afectan a la copia.

Entrada por la entrada estándar. La primera línea contiene N (0 ≤ N ≤ 1000), el número de operaciones. Le siguen N líneas. Cada una es un alta (F, E, B, R) o una de estas órdenes. C id valor llama a ajustar; si el vehículo no es ajustable imprime NO_AJUSTABLE id y en otro caso no imprime nada. Q km busca, entre los vehículos con autonomía ≥ km, el de menor coste; los empates se deshacen por el identificador lexicográficamente menor. L lista la flota.

Salida. Q imprime km: id coste, o bien km: NINGUNO si ningún vehículo llega. L imprime una línea por vehículo, en orden de alta, con el formato id descripcion aut=A, o bien FLOTA VACIA si no hay ninguno. La descripción es F(cpk), E(cpk) o B. Para un remolque es R(extra)+ seguido de la descripción de su base.

Garantías: los identificadores son únicos y tienen como máximo 20 caracteres alfanuméricos. Los vehículos citados en R y C existen. Además, 1 ≤ km ≤ 10000, 0 ≤ cpk, extra, valor ≤ 1000, 1 ≤ aut ≤ 100000, y como mucho hay 50 remolques anidados.

Entrada de ejemplo

11
F F1 30 500
E E1 12 180
B BICI
R R1 F1 10
Q 20
Q 200
C F1 25
L
Q 400
C BICI 50
Q 600

Salida esperada

20: E1 390
200: F1 6000
F1 F(25) aut=500
E1 E(12) aut=180
BICI B aut=25
R1 R(10)+F(30) aut=400
400: F1 10000
NO_AJUSTABLE BICI
600: NINGUNO
Jerarquía correcta: clase abstracta <code>Vehiculo</code> con destructor virtual, clase intermedia para las tarifas por kilómetro y redefinición con <code>override</code> en cada tipo concreto
0,75
<code>Remolque</code> como decorador que posee una copia profunda de su vehículo base mediante <code>clonar</code>, con constructor de copia correcto y descripción recursiva
1,0
Consultas <code>Q</code>, <code>C</code> y <code>L</code> resueltas solo con llamadas virtuales (sin <code>dynamic_cast</code> ni comprobación de tipos), con el desempate y el formato de salida exactos
0,75

Pistas

Pista 1 · ¿Qué clase necesita saber cómo se copia un vehículo?

El remolque recibe un identificador de base, pero solo tiene un Vehiculo&: no conoce el tipo concreto. No puedes escribir Vehiculo copia = original;, porque la clase es abstracta y además cortarías el objeto. La pregunta «cópiate» debe responderla el propio objeto. Para eso existe el método virtual clonar(), que cada clase concreta implementa con su propio constructor de copia.

Pista 2 · ¿Remolque hereda de Vehiculo, contiene un Vehiculo, o ambas cosas?

Ambas cosas. Remolque es un vehículo, porque puede estar en la flota y competir en Q. Y tiene un vehículo como miembro std::unique_ptr<Vehiculo>. Cada método delega en ese miembro y añade su parte: coste suma, autonomia reduce y describir antepone su prefijo. Así los remolques anidados salen solos por recursión, sin bucles especiales.

Pista 3 · ¿Qué pasa al copiar un Remolque?

Un unique_ptr no se puede copiar, así que el constructor de copia implícito de Remolque queda eliminado. Tienes que escribirlo tú. Inicializa explícitamente la parte Vehiculo y llama a o.base_->clonar() para duplicar la base. Comprueba tu diseño con un remolque de un remolque y después ajusta el intermedio: el exterior no debe cambiar.

Solución

Solución explicada

La solución tiene tres niveles. Vehiculo es la interfaz abstracta. ConTarifa es una clase intermedia que factoriza lo común a furgonetas y eléctricas: el precio por kilómetro, la autonomía y el ajuste. Debajo están las clases concretas, todas final. Remolque es un decorador: hereda de Vehiculo y a la vez envuelve otro Vehiculo del que es dueño exclusivo.

El programa principal no sabe nada de tipos concretos salvo en el momento del alta. A partir de ahí solo usa llamadas virtuales a través de std::unique_ptr<Vehiculo>.

#include <iostream>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include <vector>

// Interfaz abstracta: nunca se instancia directamente.
class Vehiculo {
public:
    explicit Vehiculo(std::string id) : id_(std::move(id)) {}
    virtual ~Vehiculo() = default;

    const std::string& id() const { return id_; }

    virtual long long coste(long long km) const = 0;
    virtual long long autonomia() const = 0;
    virtual void describir(std::string& out) const = 0;
    virtual std::unique_ptr<Vehiculo> clonar() const = 0;
    virtual bool ajustar(long long) { return false; }

protected:
    // Copia solo accesible desde las derivadas: evita el corte (slicing).
    Vehiculo(const Vehiculo&) = default;
    Vehiculo& operator=(const Vehiculo&) = default;

private:
    std::string id_;
};

// Clase intermedia: vehiculos con precio por kilometro ajustable.
class ConTarifa : public Vehiculo {
public:
    ConTarifa(std::string id, long long cpk, long long aut)
        : Vehiculo(std::move(id)), cpk_(cpk), aut_(aut) {}

    long long autonomia() const override { return aut_; }
    bool ajustar(long long nuevo) override {
        cpk_ = nuevo;
        return true;
    }

protected:
    long long cpk_;
    long long aut_;
};

class Furgoneta final : public ConTarifa {
public:
    using ConTarifa::ConTarifa;

    long long coste(long long km) const override { return cpk_ * km; }
    void describir(std::string& out) const override {
        out += "F(";
        out += std::to_string(cpk_);
        out += ")";
    }
    std::unique_ptr<Vehiculo> clonar() const override {
        return std::make_unique<Furgoneta>(*this);
    }
};

class Electrica final : public ConTarifa {
public:
    using ConTarifa::ConTarifa;
    static constexpr long long TARIFA_CONEXION = 150;

    long long coste(long long km) const override {
        return cpk_ * km + TARIFA_CONEXION;
    }
    void describir(std::string& out) const override {
        out += "E(";
        out += std::to_string(cpk_);
        out += ")";
    }
    std::unique_ptr<Vehiculo> clonar() const override {
        return std::make_unique<Electrica>(*this);
    }
};

class Bicicleta final : public Vehiculo {
public:
    using Vehiculo::Vehiculo;

    long long coste(long long km) const override { return 40 * km; }
    long long autonomia() const override { return 25; }
    void describir(std::string& out) const override { out += "B"; }
    std::unique_ptr<Vehiculo> clonar() const override {
        return std::make_unique<Bicicleta>(*this);
    }
};

// Decorador: ES un Vehiculo y TIENE (en exclusiva) otro Vehiculo.
class Remolque final : public Vehiculo {
public:
    Remolque(std::string id, std::unique_ptr<Vehiculo> base, long long extra)
        : Vehiculo(std::move(id)), base_(std::move(base)), extra_(extra) {}

    // Copia profunda: la base se duplica a traves de su propio clonar().
    Remolque(const Remolque& o)
        : Vehiculo(o), base_(o.base_->clonar()), extra_(o.extra_) {}

    long long coste(long long km) const override {
        return base_->coste(km) + extra_ * km;
    }
    long long autonomia() const override {
        return base_->autonomia() * 8 / 10;
    }
    void describir(std::string& out) const override {
        out += "R(";
        out += std::to_string(extra_);
        out += ")+";
        base_->describir(out);
    }
    std::unique_ptr<Vehiculo> clonar() const override {
        return std::make_unique<Remolque>(*this);
    }
    bool ajustar(long long nuevo) override {
        extra_ = nuevo;
        return true;
    }

private:
    std::unique_ptr<Vehiculo> base_;
    long long extra_;
};

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n = 0;
    if (!(std::cin >> n)) return 0;

    std::vector<std::unique_ptr<Vehiculo>> flota;
    std::map<std::string, std::size_t> indice;

    auto alta = [&](std::unique_ptr<Vehiculo> v) {
        indice[v->id()] = flota.size();
        flota.push_back(std::move(v));
    };

    for (int k = 0; k < n; ++k) {
        char op = ' ';
        std::cin >> op;
        if (op == 'F' || op == 'E') {
            std::string id;
            long long cpk = 0, aut = 0;
            std::cin >> id >> cpk >> aut;
            if (op == 'F') alta(std::make_unique<Furgoneta>(id, cpk, aut));
            else alta(std::make_unique<Electrica>(id, cpk, aut));
        } else if (op == 'B') {
            std::string id;
            std::cin >> id;
            alta(std::make_unique<Bicicleta>(id));
        } else if (op == 'R') {
            std::string id, base;
            long long extra = 0;
            std::cin >> id >> base >> extra;
            const Vehiculo& original = *flota[indice.at(base)];
            alta(std::make_unique<Remolque>(id, original.clonar(), extra));
        } else if (op == 'C') {
            std::string id;
            long long valor = 0;
            std::cin >> id >> valor;
            if (!flota[indice.at(id)]->ajustar(valor)) {
                std::cout << "NO_AJUSTABLE " << id << '\n';
            }
        } else if (op == 'Q') {
            long long km = 0;
            std::cin >> km;
            const Vehiculo* mejor = nullptr;
            long long mejorCoste = 0;
            for (const auto& v : flota) {
                if (v->autonomia() < km) continue;
                long long c = v->coste(km);
                if (mejor == nullptr || c < mejorCoste ||
                    (c == mejorCoste && v->id() < mejor->id())) {
                    mejor = v.get();
                    mejorCoste = c;
                }
            }
            std::cout << km << ": ";
            if (mejor != nullptr) std::cout << mejor->id() << ' ' << mejorCoste << '\n';
            else std::cout << "NINGUNO\n";
        } else if (op == 'L') {
            if (flota.empty()) std::cout << "FLOTA VACIA\n";
            for (const auto& v : flota) {
                std::string d;
                v->describir(d);
                std::cout << v->id() << ' ' << d << " aut=" << v->autonomia() << '\n';
            }
        }
    }
    return 0;
}

La base abstracta. Vehiculo declara el destructor virtual porque la flota destruye cada objeto a través de un unique_ptr<Vehiculo>. Sin él, borrar una Furgoneta por un puntero a la base es comportamiento indefinido. El constructor de copia y la asignación son protected. Las derivadas pueden usarlos para copiarse a sí mismas, pero desde fuera nadie puede escribir una copia de Vehiculo que corte el objeto. ajustar no es puro: su implementación por defecto devuelve false, y por eso Bicicleta ni siquiera lo menciona.

La clase intermedia. ConTarifa no implementa coste ni describir, así que sigue siendo abstracta. Sí resuelve autonomia y ajustar, que son idénticos en furgonetas y eléctricas. Furgoneta y Electrica heredan su constructor con using ConTarifa::ConTarifa;. Solo aportan la fórmula de coste, su letra en la descripción y su clonar, que es una línea: std::make_unique<Furgoneta>(*this). Ahí el tipo concreto sí se conoce, porque el código está dentro de la propia clase. Ese es el «constructor virtual».

El decorador. Remolque guarda base_ como unique_ptr<Vehiculo>, es decir, es su único dueño. En main, la orden R localiza el original con indice.at(base) y entrega a Remolque el resultado de original.clonar(), nunca el original. Su constructor de copia escrito a mano hace dos cosas. Inicializa Vehiculo(o), que copia el identificador; omitirlo no compila, porque Vehiculo no tiene constructor por defecto. Y llama a o.base_->clonar(), que es recursivo si la base es otro remolque.

Comprobación con las pruebas. En el ejemplo, R1 se crea sobre F1 con tarifa 30. Después C F1 25 cambia F1, y el listado muestra F1 F(25) mientras R1 sigue como R(10)+F(30). La consulta de 400 km lo confirma: R1 costaría 16000 frente a los 10000 de F1. En la prueba de remolques anidados, M2 se construye clonando M1, que a su vez lleva un clon de Z9. Tras C M1 0, M2 conserva R(5)+R(5)+F(20).

Consultas. Q recorre la flota descartando a quien no llega (v->autonomia() < km) y compara por coste y luego por identificador. La prueba de anidados lo pone a prueba con 640 km: Z9 y M1 empatan a 12800, y gana M1 aunque se dio de alta después. La autonomía del remolque se calcula como autonomia*8/10. En la prueba de la bicicleta, 25·8/10 = 20, y por eso TR entra en la consulta de 20 km. Por último, describir recibe un std::string& y añade al final en lugar de concatenar y devolver copias. Así describir una cadena de remolques cuesta lineal en su profundidad.

Código y tests en GitLab

Casos de prueba

CasoEntradaSalida esperadaSalida realResultado
Ejemplo del enunciado11 F F1 30 500 E E1 12 180 B BICI R R1 F1 10 Q 20 Q 200 C F1 25 L Q 400 C BICI 50 Q 60020: E1 390 200: F1 6000 F1 F(25) aut=500 E1 E(12) aut=180 BICI B aut=25 R1 R(10)+F(30) aut=400 400: F1 10000 NO_AJUSTABLE BICI 600: NINGUNO20: E1 390 200: F1 6000 F1 F(25) aut=500 E1 E(12) aut=180 BICI B aut=25 R1 R(10)+F(30) aut=400 400: F1 10000 NO_AJUSTABLE BICI 600: NINGUNOOK
Remolques anidados y desempate8 F Z9 20 1000 R M1 Z9 5 R M2 M1 5 L Q 100 C M1 0 L Q 640Z9 F(20) aut=1000 M1 R(5)+F(20) aut=800 M2 R(5)+R(5)+F(20) aut=640 100: Z9 2000 Z9 F(20) aut=1000 M1 R(0)+F(20) aut=800 M2 R(5)+R(5)+F(20) aut=640 640: M1 12800Z9 F(20) aut=1000 M1 R(5)+F(20) aut=800 M2 R(5)+R(5)+F(20) aut=640 100: Z9 2000 Z9 F(20) aut=1000 M1 R(0)+F(20) aut=800 M2 R(5)+R(5)+F(20) aut=640 640: M1 12800OK
Flota vacía2 L Q 5FLOTA VACIA 5: NINGUNOFLOTA VACIA 5: NINGUNOOK
Bicicleta en el límite de autonomía5 B CARGO Q 25 Q 26 R TR CARGO 3 Q 2025: CARGO 1000 26: NINGUNO 20: CARGO 80025: CARGO 1000 26: NINGUNO 20: CARGO 800OK
Tarifa fija de la eléctrica6 E VE 10 300 F VF 11 300 Q 100 Q 200 C VE 9 Q 100100: VF 1100 200: VE 2150 100: VE 1050100: VF 1100 200: VE 2150 100: VE 1050OK
Sin operaciones0(vacía)(vacía)OK

Salidas reales: código compilado con g++ 14.4.0 (C++20) y ejecutado en un contenedor aislado el 25 de octubre de 2026.

Complejidad

Sea V el número de vehículos y d la profundidad máxima de anidamiento (≤ 50). coste, autonomia y describir recorren la cadena de bases, así que cuestan O(d). Una orden Q o L cuesta O(V·d). Un alta R clona la base entera, O(d), y además inserta en el std::map, O(log V). C cuesta O(log V) por la búsqueda más O(1) del ajuste. Con N ≤ 1000 el peor caso ronda las 2,5·107 llamadas virtuales, muy por debajo del límite.

En memoria, cada remolque posee su propia copia completa de la cadena que envuelve, por lo que el total es O(V·d) objetos. Es el precio de la semántica de valor que exige el enunciado. La alternativa compartida, comentada en las variantes, ahorraría memoria pero cambiaría el resultado de las pruebas.

Errores típicos

  • Olvidar virtual en el destructor de Vehiculo. El programa puede imprimir lo correcto en todas las pruebas y, aun así, tener comportamiento indefinido al destruir la flota, con fugas de la base de cada remolque.
  • Guardar en Remolque un puntero o referencia al vehículo de la flota en vez de un clon. Tras C F1 25, el listado del ejemplo mostraría R(10)+F(25) y la consulta de 400 km cambiaría de coste.
  • Escribir autonomia()/10*8 o usar 0.8 con double. En el primer caso la bicicleta remolcada daría 16 en vez de 20 y la prueba correspondiente fallaría. En el segundo caso aparecen errores de redondeo al truncar.
  • Deshacer los empates por orden de alta en lugar de por identificador. En la prueba de remolques anidados, la consulta de 640 km devolvería Z9 en vez de M1.
  • Resolver C con dynamic_cast o con un campo tipo y un switch. Funciona, pero rompe el diseño exigido: cada tipo nuevo obliga a tocar main. El método virtual ajustar con implementación por defecto lo evita.
  • Implementar clonar solo en la base o con std::make_unique<Vehiculo>(*this). No compila porque la clase es abstracta, y aunque no lo fuera, cortaría el objeto.

Variantes

Remolques que siguen al original

Si el enunciado pidiera que el remolque refleje los cambios posteriores de su base, la propiedad dejaría de ser exclusiva. Habría que pasar a std::shared_ptr<Vehiculo> tanto en la flota como en base_, y en la orden R se compartiría el puntero en vez de clonar. Las salidas de las pruebas cambiarían: en el ejemplo, R1 pasaría a describirse como R(10)+F(25). Es justo la diferencia entre semántica de valor y semántica de referencia que suele repasarse al estudiar polimorfismo y enlace dinámico en Java, donde todo objeto se maneja por referencia.

Tipo de retorno covariante

Con punteros crudos, cada derivada puede declarar Furgoneta* clonar() const override. C++ admite tipos de retorno covariantes para punteros y referencias, pero no para unique_ptr. El precio es gestionar el delete a mano. Una solución habitual es un método privado virtual que devuelve el puntero crudo covariante y un clonar() público no virtual que lo envuelve en unique_ptr.

Añadir un nuevo tipo sin tocar main

Se pide añadir una moto con coste 25·km + 50 y autonomía 120, no ajustable. Basta una clase final que herede de Vehiculo e implemente los cuatro métodos puros, más una línea de alta en el lector. Ni Q, ni L, ni C cambian. Así se demuestra que el diseño está abierto a extensión.

En los temarios

La reutilización de clases mediante herencia y la definición de contratos con interfaces forman parte del temario de ISIS1209 APO II Honores de la Universidad de los Andes. En el programa de IIC2233 Programación Avanzada de la Pontificia Universidad Católica de Chile figura aplicar herencia, polimorfismo e interfaces al diseño de programas complejos. Este ejercicio trabaja esos mismos conceptos en C++, con la clase abstracta y el decorador.

La otra mitad del ejercicio, decidir quién es dueño de cada objeto dinámico, conecta con el manejo de la memoria dinámica mediante punteros y referencias. Ese contenido aparece en el programa de Algoritmos y Programación II (75.41) de la Universidad de Buenos Aires.

Artículo generado mediante AI.larebelion

Kernel

· Dirección de tecnología · España

«La jerarquía la dibuja cualquiera; lo difícil es decidir quién es dueño de cada objeto y cuándo deja de existir.»

Comentarios

Publicar un comentario