Visibilidad en la herencia: Public, Protected y Private

Si intentas realizar un upcasting (convertir un puntero de la clase derivada a uno de la base) en una herencia protected o private, el compilador lo bloqueará porque la relación de “es-un” no es pública. Por el contrario, el downcasting (de base a derivada) requiere cuidado: dynamic_cast es la opción segura cuando trabajas con polimorfismo, ya que utiliza la RTTI (Run-Time Type Information) para verificar el tipo, devolviendo nullptr en punteros o lanzando std::bad_cast en referencias si la conversión falla; static_cast es la opción de alto rendimiento si estás absolutamente seguro del tipo, pero no realiza comprobaciones en tiempo de ejecución y puede causar comportamiento indefinido si te equivocas.

Para aprovechar estas reglas, debes entender el orden de construcción: siempre se construye primero la jerarquía de la base y luego la de la derivada. En la destrucción, el proceso es inverso para asegurar que la derivada limpie sus recursos antes de que la base deje de existir.

#include <iostream>
#include <memory>

// La clase base debe tener al menos un método virtual (comúnmente el destructor)
// para ser considerada un tipo polimórfico y permitir dynamic_cast.
class Dispositivo {
public:
    explicit Dispositivo(int id) : id_(id) {
        std::cout << "[Base] Dispositivo " << id_ << " construido\n";
    }

    virtual ~Dispositivo() {
        std::cout << "[Base] Dispositivo " << id_ << " destruido\n";
    }

    virtual void ejecutar() const {
        std::cout << "[Base] Ejecutando tarea genérica...\n";
    }

    void encender() const {
        std::cout << "[Base] Sistema encendido\n";
    }

protected:
    int id_;
};

// Herencia pública: "es-un" Smartphone es un Dispositivo.
class Smartphone : public Dispositivo {
public:
    explicit Smartphone(int id, int bateria) 
        : Dispositivo(id), bateria_(bateria) {}

    void ejecutar() const override {
        std::cout << "[Public] Smartphone: Ejecutando app. Batería: " << battery_pct() << "%\n";
    }

    void llamar() const {
        std::cout << "[Public] Realizando llamada...\n";
    }

private:
    int batería_;
    int battery_pct() const { return batería_; }
};

// Herencia protegida: Smartphone es un Dispositivo, pero su interfaz 
// de base está oculta para el usuario de RobotIndustrial.
class RobotIndustrial : public protected Dispositivo {
public:
    explicit RobotIndustrial(int id) : Dispositivo(id) {}

    void trabajar() {
        // Puede acceder a miembros públicos y protegidos de la base
        ejecutar(); 
        encender();
    }
};

// Herencia privada: RobotIndustrial implementa su lógica mediante un Dispositivo,
// pero no quiere exponer la interfaz de Dispositivo al exterior.
class MotorInterno : private Dispositivo {
public:
    explicit MotorInterno(int id) : Dispositivo(id) {}

    void arrancar() {
        // Solo puede acceder a miembros de la base si son públicos/protegidos
        encender();
    }
};

int main() {
    std::cout << "--- Escenario 1: Herencia Pública y Polimorfismo ---\n";
    std::unique_ptr<Dispositivo> ptr_base = std::make_unique<Smartphone>(101, 85);
    
    // Upcasting implícito (permitido en herencia pública)
    Dispositivo* raw_base = ptr_base.get();
    raw_base->ejecutar();

    // Downcasting seguro con dynamic_cast
    if (Smartphone* s = dynamic_cast<Smartphone*>(raw_base)) {
        s->llamar();
    }

    // Downcasting rápido con static_cast (si estamos seguros del tipo)
    Smartphone* s_fast = static_cast<Smartphone*>(raw_base);
    s_fast->llamar();

    std::cout << "\n--- Escenario 2: Herencia Protegida ---\n";
    RobotIndustrial robot(202);
    robot.trabajar();
    // Dispositivo* d = &robot; // ERROR DE COMPILACIÓN: La relación no es pública.

    std::cout << "\n--- Escenario 3: Herencia Privada ---\n";
    MotorInterno motor(303);
    motor.arrancar();
    // Dispositivo* d2 = &motor; // ERROR DE COMPILACIÓN: La relación es privada.

    return 0;
}

En el código anterior, observa cómo Smartphone permite la conversión implícita de Smartphone* a Dispositivo* gracias a la herencia public. Esto es lo que permite usar std::unique_ptr<Dispositivo> para gestionar un Smartphone en el primer bloque del main. El uso de dynamic_cast<Smartphone*> es seguro porque Dispositivo tiene un destructor virtual, lo que activa la tabla de métodos virtuales (vtable) necesaria para que el runtime identifique el tipo real en memoria.

En el caso de RobotIndustrial, aunque hereda de Dispositivo, el especificador protected hace que Dispositivo::ejecutar() sea inaccesible desde main. El objeto sigue siendo un Dispositivo internamente, pero el mundo exterior solo puede interactuar con la interfaz que RobotIndustrial decida exponer mediante sus propios métodos públicos, como trabajar().

La clase MotorInterno ejemplifica la reutilización de código mediante herencia private. El MotorInterno utiliza la lógica de encender() de la base para su método arrancar(), pero no permite que nadie fuera de la clase trate a un MotorInterno como un Dispositivo.

Finalmente, la construcción y destrucción siguen una jerarquía estricta: al crear el Smartphone, primero se llama al constructor de Dispositivo para inicializar id_ y luego el de Smartphone para batería_. Al destruirse, el destructor de la clase derivada se ejecuta primero, y este, al terminar, invoca automáticamente el destructor de la clase base.

El error frecuente
Un error crítico en sistemas con jerarquías complejas es olvidar declarar el destructor como virtual en la clase base cuando se planea usar herencia pública.

class Base {
public:
    ~Base() { std::cout << "Destructor Base\n"; } // NO VIRTUAL
};

class Derived : public Base {
    int* data;
public:
    Derived() : data(new int[10]) {}
    ~Derived() override { 
        delete[] data; 
        std::cout << "Destructor Derived\n"; 
    }
};

// Uso peligroso:
Base* ptr = new Derived();
delete ptr; // COMPORTAMIENTO INDEFINIDO: Solo se llamará al destructor de Base.
             // El contenido de 'data' en Derived nunca se liberará (Memory Leak).

Si compilas con -fsanitize=address, AddressSanitizer detectará este error de fuga de memoria inmediatamente. En producción, este bug puede manifestarse como un consumo de RAM creciente que es extremadamente difícil de rastrear.

41

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio