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

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 600Salida 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.
Casos de prueba
| Caso | Entrada | Salida esperada | Salida real | Resultado |
|---|---|---|---|---|
| Ejemplo del enunciado | 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 | 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 | 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 | OK |
| Remolques anidados y desempate | 8
F Z9 20 1000
R M1 Z9 5
R M2 M1 5
L
Q 100
C M1 0
L
Q 640 | Z9 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 12800 | Z9 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 12800 | OK |
| Flota vacía | 2
L
Q 5 | FLOTA VACIA
5: NINGUNO | FLOTA VACIA
5: NINGUNO | OK |
| Bicicleta en el límite de autonomía | 5
B CARGO
Q 25
Q 26
R TR CARGO 3
Q 20 | 25: CARGO 1000
26: NINGUNO
20: CARGO 800 | 25: CARGO 1000
26: NINGUNO
20: CARGO 800 | OK |
| Tarifa fija de la eléctrica | 6
E VE 10 300
F VF 11 300
Q 100
Q 200
C VE 9
Q 100 | 100: VF 1100
200: VE 2150
100: VE 1050 | 100: VF 1100
200: VE 2150
100: VE 1050 | OK |
| Sin operaciones | 0 | (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
virtualen el destructor deVehiculo. 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
Remolqueun puntero o referencia al vehículo de la flota en vez de un clon. TrasC F1 25, el listado del ejemplo mostraríaR(10)+F(25)y la consulta de 400 km cambiaría de coste. - Escribir
autonomia()/10*8o usar0.8condouble. 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
Z9en vez deM1. - Resolver
Ccondynamic_casto con un campotipoy unswitch. Funciona, pero rompe el diseño exigido: cada tipo nuevo obliga a tocarmain. El método virtualajustarcon implementación por defecto lo evita. - Implementar
clonarsolo en la base o constd::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



Comentarios
Publicar un comentario