Fundamentos de la herencia en C++
Aprende los fundamentos de la herencia en C++, incluyendo clases base, derivadas y reutilización de código.
La herencia (inheritance) permite que una clase herede las propiedades y comportamientos de otra. De este modo se reduce la duplicación de código, las clases pueden organizarse jerárquicamente y se hace posible el polimorfismo. En este artículo veremos una clase base Book y sus clases derivadas EBook y PrintedBook, junto con los conceptos de cadena de constructores, funciones virtuales, override, destructor virtual, upcasting/downcasting y clase base abstracta.
1. Clase base: Book
Reunimos los campos y comportamientos comunes en la clase Book. Las clases derivadas heredan estos elementos y pueden personalizarlos si es necesario.
#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
class Book {
protected: // accesible desde clases derivadas
string title;
string author;
double basePrice;
int pageCount;
public:
Book(string t, string a, double price, int pages)
: title(t), author(a), basePrice(price), pageCount(pages) {
// inicialización común
}
// Destructor virtual: necesario para eliminar objetos derivados correctamente
virtual ~Book() = default;
// Método virtual: las subclases pueden personalizar el cálculo del precio
virtual double GetFinalPrice() const {
return basePrice;
}
// Método virtual: las subclases pueden mostrar información adicional
virtual void PrintInfo() const {
cout << "[Book] " << title
<< " | Autor: " << author
<< " | Páginas: " << pageCount
<< " | Precio: " << GetFinalPrice() << " €" << endl;
}
};
Los miembros protected no son accesibles desde fuera de la clase,
pero sí desde las subclases.
El destructor virtual es esencial para la eliminación polimórfica.
2. Clase derivada: EBook
Los libros electrónicos no tienen costo de impresión, pero pueden incluir IVA, descuentos o tarifas de licencia. Personalicemos el cálculo del precio.
class EBook : public Book {
double vatRate; // IVA (ej. 0.10 = 10%)
bool drmProtected; // Protección DRM
public:
EBook(string t, string a, double price, int pages,
double vat, bool drm)
: Book(t, a, price, pages), vatRate(vat), drmProtected(drm) {}
double GetFinalPrice() const override {
double priceWithVat = basePrice * (1.0 + vatRate);
// Descuento del 5% para libros de 500 páginas o más
if (pageCount >= 500) priceWithVat *= 0.95;
return priceWithVat;
}
void PrintInfo() const override {
cout << "[EBook] " << title
<< " | DRM: " << (drmProtected ? "Sí" : "No")
<< " | Precio final: " << GetFinalPrice() << " €" << endl;
}
};
El uso de override permite al compilador detectar errores de firma en tiempo de compilación.
3. Clase derivada: PrintedBook
Los libros impresos tienen costos adicionales, como papel y envío. Apliquemos diferentes multiplicadores según el tipo de encuadernación (blanda o dura).
enum class Cover { Soft, Hard };
class PrintedBook : public Book {
Cover coverType;
double shipping; // Costo de envío
double printCost; // Costo de impresión por página
public:
PrintedBook(string t, string a, double price, int pages,
Cover cover, double shippingCost, double printPerPage)
: Book(t, a, price, pages),
coverType(cover), shipping(shippingCost), printCost(printPerPage) {}
double GetFinalPrice() const override {
double cost = basePrice + shipping + (printCost * pageCount);
if (coverType == Cover::Hard) {
cost *= 1.10; // +10% para tapa dura
}
return cost;
}
void PrintInfo() const override {
cout << "[PrintedBook] " << title
<< " | Cubierta: " << (coverType == Cover::Hard ? "Dura" : "Blanda")
<< " | Precio final: " << GetFinalPrice() << " €" << endl;
}
};
La cadena de constructores primero llama al constructor de la clase base (Book),
y luego inicializa los campos específicos de PrintedBook.
4. Polimorfismo: Upcasting y llamadas virtuales
Los objetos derivados pueden manejarse mediante punteros o referencias al tipo base y las funciones virtuales permiten llamadas dinámicas.
int main() {
vector<unique_ptr<Book>> library;
library.push_back(make_unique<EBook>(
"C++ 20 Guide", "J. Doe", 100.0, 520, 0.10, true));
library.push_back(make_unique<PrintedBook>(
"Clean Code", "R. Martin", 150.0, 464, Cover::Hard, 20.0, 0.05));
for (const auto &bk : library) {
// Llamada virtual mediante un puntero base
bk->PrintInfo();
}
}
Con unique_ptr<Book>, la gestión de memoria es automática;
no es necesario usar delete.
5. Downcasting: uso de dynamic_cast
A veces es necesario convertir de un tipo base a su tipo derivado.
Para realizar conversiones seguras, se utiliza dynamic_cast.
void TryPrintDRM(const Book* b) {
if (auto eb = dynamic_cast<const EBook*>(b)) {
cout << "Estado DRM del EBook verificado." << endl;
} else {
cout << "No es un EBook." << endl;
}
}
Si dynamic_cast falla, devuelve nullptr (al usar punteros).
Esto mantiene la seguridad de tipos.
Requiere que RTTI esté habilitado y una tabla virtual disponible.
6. Clase base abstracta
Una clase que contiene al menos una función virtual pura es abstracta
y no puede ser instanciada directamente.
Al hacer que Book sea abstracta, definimos un contrato claro en la arquitectura.
class AbstractBook {
protected:
string title, author;
int pageCount;
public:
AbstractBook(string t, string a, int p)
: title(t), author(a), pageCount(p) {}
virtual ~AbstractBook() = default;
virtual double GetFinalPrice() const = 0; // virtual pura
virtual void PrintInfo() const = 0;
};
// La clase derivada debe redefinir todas las funciones virtuales puras
class SimpleEBook : public AbstractBook {
double basePrice, vat;
public:
SimpleEBook(string t, string a, int p, double price, double v)
: AbstractBook(t,a,p), basePrice(price), vat(v) {}
double GetFinalPrice() const override { return basePrice*(1+vat); }
void PrintInfo() const override {
cout << "[SimpleEBook] " << title
<< " | Precio: " << GetFinalPrice() << endl;
}
};
Una clase base abstracta promueve un diseño orientado a contratos (similar a una interfaz).
7. Modificadores de acceso y tipos de herencia
En la herencia existen tres tipos: public, protected y private.
- Herencia pública: los miembros públicos de la clase base permanecen públicos en la clase derivada (la más común).
- Herencia protegida: los miembros públicos de la base se convierten en
protecteden la clase derivada (ya no accesibles externamente). - Herencia privada: todos los miembros públicos/protegidos de la base se convierten en privados en la clase derivada.
En la práctica, generalmente se utiliza la herencia pública (relación “es-un” / is-a).
8. Palabras clave final y override
La palabra clave override indica al compilador que un método en la clase derivada realmente está sobrescribiendo un método virtual.
La palabra clave final evita que una función o clase sea sobrescrita en el futuro.
class FinalEBook : public EBook {
public:
using EBook::EBook;
double GetFinalPrice() const override final {
return EBook::GetFinalPrice(); // no se puede sobrescribir más
}
};
9. Ejemplo completo de aplicación
#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
enum class Cover { Soft, Hard };
class Book {
protected:
string title, author;
double basePrice;
int pageCount;
public:
Book(string t, string a, double price, int pages)
: title(t), author(a), basePrice(price), pageCount(pages) {}
virtual ~Book() = default;
virtual double GetFinalPrice() const { return basePrice; }
virtual void PrintInfo() const {
cout << "[Book] " << title
<< " | " << author
<< " | " << pageCount << " páginas"
<< " | " << GetFinalPrice() << " €" << endl;
}
};
class EBook : public Book {
double vatRate; bool drmProtected;
public:
EBook(string t, string a, double price, int pages, double vat, bool drm)
: Book(t,a,price,pages), vatRate(vat), drmProtected(drm) {}
double GetFinalPrice() const override {
double x = basePrice*(1+vatRate);
if (pageCount >= 500) x *= 0.95;
return x;
}
void PrintInfo() const override {
cout << "[EBook] " << title
<< " | DRM: " << (drmProtected? "Sí":"No")
<< " | " << GetFinalPrice() << " €" << endl;
}
};
class PrintedBook : public Book {
Cover coverType; double shipping; double printCost;
public:
PrintedBook(string t, string a, double price, int pages,
Cover cover, double ship, double perPage)
: Book(t,a,price,pages), coverType(cover),
shipping(ship), printCost(perPage) {}
double GetFinalPrice() const override {
double c = basePrice + shipping + printCost*pageCount;
if (coverType == Cover::Hard) c *= 1.10;
return c;
}
void PrintInfo() const override {
cout << "[PrintedBook] " << title
<< " | Cubierta: " << (coverType==Cover::Hard?"Dura":"Blanda")
<< " | " << GetFinalPrice() << " €" << endl;
}
};
int main() {
vector<unique_ptr<Book>> books;
books.push_back(make_unique<EBook>("Modern C++", "A. Dev", 120.0, 520, 0.10, true));
books.push_back(make_unique<PrintedBook>("Clean Code", "R. Martin", 150.0, 464, Cover::Hard, 25.0, 0.04));
for (const auto& b : books) b->PrintInfo(); // llamada polimórfica
// Intento de downcast seguro
if (auto pb = dynamic_cast<PrintedBook*>(books[1].get())) {
cout << "Precio del PrintedBook: " << pb->GetFinalPrice() << endl;
}
}
El código se puede compilar y ejecutar en Visual Studio 2022 o GCC 11+.
10. TL;DR
- Con la herencia, centralizamos el comportamiento común en la clase base y lo personalizamos en las clases derivadas.
- Los métodos
virtualy la palabra claveoverridepermiten el polimorfismo dinámico. - Sin un destructor virtual, eliminar objetos a través de punteros base puede ser peligroso.
- Cadena de constructores: primero se ejecuta el constructor base, luego el de la clase derivada.
dynamic_castpermite un downcasting seguro (devuelvenullptrsi falla).- Una clase base abstracta (virtual pura) no puede instanciarse y define un contrato obligatorio.
- Los ejemplos son compatibles con Visual Studio 2022 y GCC 11+.