Ir al contenido

Herencia y Relaciones Es-Un

Aprende la herencia publica, el encadenamiento de constructores, el problema del slicing, la herencia multiple y el problema del diamante — y cuando la herencia es apropiada.

La Herencia Modela Es-Un

La herencia publica modela una relacion "es-un": un Dog es un Animal, una SavingsAccount es una BankAccount. Esto se rige por el Principio de Sustitucion de Liskov (LSP): en cualquier lugar donde se espere una clase base, una clase derivada debe funcionar correctamente sin sorpresas.

La herencia es una de las caracteristicas mas poderosas — y mas mal usadas — de la POO. No toda relacion es "es-un." Un Stack no es un Vector aunque use uno internamente. Usar mal la herencia donde la composicion es apropiada conduce a codigo fragil y dificil de mantener.

Herencia Basica y Encadenamiento de Constructores

Una clase derivada hereda todos los miembros de la clase base. El constructor de la clase base se ejecuta primero, luego el constructor de la clase derivada. La destruccion ocurre en orden inverso.

inheritance_basic.cpp
#include <iostream>
#include <string>

class Animal {
    std::string name_;
    int age_;

public:
    Animal(const std::string& name, int age)
        : name_{name}, age_{age}
    {
        std::cout << "Animal constructed: " << name_ << '\n';
    }

    virtual ~Animal() {
        std::cout << "Animal destroyed: " << name_ << '\n';
    }

    const std::string& name() const { return name_; }
    int age() const { return age_; }

    virtual void speak() const {
        std::cout << name_ << " makes a sound\n";
    }
};

class Dog : public Animal {  // public inheritance = is-a
    std::string breed_;

public:
    Dog(const std::string& name, int age, const std::string& breed)
        : Animal{name, age}  // base class constructor MUST be called first
        , breed_{breed}
    {
        std::cout << "Dog constructed: " << name() << '\n';
    }

    ~Dog() override {
        std::cout << "Dog destroyed: " << name() << '\n';
    }

    void speak() const override {
        std::cout << name() << " barks! (" << breed_ << ")\n";
    }
};

int main() {
    Dog rex{"Rex", 5, "German Shepherd"};
    rex.speak();  // Rex barks! (German Shepherd)
    // Destruction order: Dog destroyed, then Animal destroyed
    return 0;
}

El Problema del Slicing

El object slicing ocurre cuando un objeto derivado se asigna o pasa por valor a una variable de clase base. La parte derivada se "recorta" — solo queda la porcion base. Este es un error sutil y comun.

slicing.cpp
#include <iostream>
#include <string>
#include <vector>
#include <memory>

class Shape {
public:
    virtual ~Shape() = default;
    virtual std::string type() const { return "Shape"; }
};

class Circle : public Shape {
    double radius_;
public:
    explicit Circle(double r) : radius_{r} {}
    std::string type() const override { return "Circle"; }
};

// BUG: passing by value slices the object!
void print_type_bad(Shape s) {
    std::cout << s.type() << '\n';  // always prints "Shape"!
}

// CORRECT: pass by reference to preserve polymorphism
void print_type_good(const Shape& s) {
    std::cout << s.type() << '\n';  // prints actual type
}

int main() {
    Circle c{5.0};

    print_type_bad(c);   // "Shape"  — SLICED!
    print_type_good(c);  // "Circle" — correct

    // Slicing also happens with containers of values:
    // std::vector<Shape> shapes;  // BAD: slicing on insert
    // Use pointers instead:
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(3.0));
    std::cout << shapes[0]->type() << '\n';  // "Circle"

    return 0;
}

Herencia Multiple y el Problema del Diamante

C++ permite que una clase herede de multiples bases. El problema del diamante ocurre cuando dos bases comparten un ancestro comun, causando que la clase derivada contenga dos copias de los datos del ancestro. La herencia virtual resuelve esto.

diamond.cpp
#include <iostream>

// Diamond problem:
//       Device
//      /      \
//  Printer  Scanner
//      \      /
//     AllInOne

class Device {
protected:
    int id_;
public:
    Device(int id) : id_{id} {
        std::cout << "Device(" << id_ << ")\n";
    }
    virtual ~Device() = default;
};

// Virtual inheritance: only ONE copy of Device in AllInOne
class Printer : virtual public Device {
public:
    Printer(int id) : Device{id} {
        std::cout << "Printer\n";
    }
    void print_doc() { std::cout << "Printing on device " << id_ << '\n'; }
};

class Scanner : virtual public Device {
public:
    Scanner(int id) : Device{id} {
        std::cout << "Scanner\n";
    }
    void scan_doc() { std::cout << "Scanning on device " << id_ << '\n'; }
};

class AllInOne : public Printer, public Scanner {
public:
    // With virtual inheritance, the most-derived class
    // must initialize the virtual base directly
    AllInOne(int id) : Device{id}, Printer{id}, Scanner{id} {
        std::cout << "AllInOne\n";
    }
};

int main() {
    AllInOne aio{42};
    aio.print_doc();  // Printing on device 42
    aio.scan_doc();   // Scanning on device 42
    // Only ONE Device sub-object exists — no ambiguity
    return 0;
}

Tipos de Retorno Covariantes y Acceso Protected

Los tipos de retorno covariantes permiten que una clase derivada sobreescriba una funcion virtual y retorne un tipo de puntero o referencia mas especifico (derivado). Por ejemplo, si Base::clone() retorna Base*, Derived::clone() puede retornar Derived*.

El especificador de acceso protected se situa entre public y private: los miembros protegidos son accesibles a la propia clase y sus clases derivadas, pero no al codigo externo. Usa protected para miembros que las clases derivadas legitimamente necesitan — pero ten cuidado, ya que crea un acoplamiento entre base y derivada.

Error común

Un error comun es usar la herencia unicamente para reutilizar codigo. Si un Stack hereda de std::vector, los usuarios pueden llamar a push_back, insert y otras operaciones de vector que violan la abstraccion del stack. La herencia debe modelar una verdadera relacion es-un que satisfaga el Principio de Sustitucion de Liskov. Para reutilizacion de codigo, prefiere la composicion (un Stack que *contiene* un vector).

Buena práctica

Usa siempre herencia public — la herencia private/protected raramente se necesita y es confusa. Declara siempre los destructores de la clase base como virtual. Pasa objetos polimorficos por referencia o puntero, nunca por valor (para evitar el slicing). Prefiere la composicion sobre la herencia a menos que la relacion sea verdaderamente "es-un." Evita jerarquias de herencia profundas (2-3 niveles es usualmente el limite practico).

Puntos clave
  • La herencia publica modela es-un — la clase derivada debe satisfacer el Principio de Sustitucion de Liskov
  • Los constructores de la base se ejecutan primero, los destructores se ejecutan al final (orden inverso de construccion)
  • El object slicing ocurre cuando un objeto derivado se pasa por valor a una base — usa siempre referencias o punteros
  • El problema del diamante se resuelve con herencia virtual, pero prefiere la composicion en su lugar
  • La herencia es para la sustituibilidad, no para la reutilizacion de codigo — usa composicion para lo segundo

Quiz — Pon a prueba tus conocimientos

(15 XP)

1. Que es el object slicing?

2. Como resuelve la herencia virtual el problema del diamante?

3. Por que NO deberias heredar de `std::vector` para crear una clase `Stack`?