Cargando...

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.

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 virtual y la palabra clave override permiten 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_cast permite un downcasting seguro (devuelve nullptr si 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+.

Artículos relacionados