Herencia y derivación de clases en C++

Conceptos fundamentales

En programación orientada a objetos, la herencia permite que una clase adquiera propiedades y comportamientos de otra. La clase original se denomina clase base o superclase, mientras que la nueva clase creada se llama clase derivada o subclase.

La declaración de una clase derivada sigue esta snitaxis:

class NombreClaseDerivada : EspecificadorHerencia NombreClaseBase1, EspecificadorHerencia NombreClaseBase2, ...
{
    // miembros de la clase derivada
};

Existen dos tipos principales de herencia:

  • Herencia simple: la clase derivada tiane una sola clase base.
  • Herencia múltiple: la clase derivada puede tener múltiples clases base.

Especificadores de herencia

El especificador de herencia determina cómo los miembros de la clase base son accesibles dentro de la clase derivada y desde el exterior. Si no se especifica, se asume private por defecto.

Herencia pública (public)

Los miembros públicos y protegidos de la clase base conservan su nivel de acceso en la clase derivada. Los miembros privados no son accesibles.

Herencia privada (private)

Todos los miembros heredados (públicos y protegidos) se convierten en privados en la clase derivada. Los miembros privados de la base no son accesibles. Esta forma es menos común.

Herencia protegida (protected)

Los miembros públicos y protegidos heredados se convierten en protegidos. Los miembros priavdos siguen sin ser accesibles.

Generación de la clase derivada

Una clase derivada se crea mediante tres procesos:

  1. Absorción: Hereda todos los miembros de la clase base excepto constructores y destructores.
  2. Modificación: Ajusta el acceso de los miembros heredados según el especificador y puede ocultar miembros o funciones virtuales.
  3. Extensión: Agrega nuevos miembros propios.

Constructores y destructores en clases derivadas

Los miembros heredados de la clase base son inicializados por los constructores de la base, mientras que los nuevos miembros de la derivada se inicializan en su propio constructor.

Sintaxis del constructor de una clase derivada:

NombreDerivada::NombreDerivada(Parametros) : NombreBase1(args1), NombreBase2(args2), ..., ObjetoMiembro1(args), ...
{
    // inicialización de miembros nuevos
}

Orden de ejecución del constructor:

  1. Constructores de las clases base (en el orden de declaración de herencia).
  2. Constructores de los miembros objetos (en el orden de declaración en la clase).
  3. Cuerpo del constructor de la clase derivada.

Orden de ejecución del destructor: Inverso al de los constructores.

Ejemplo de orden de construcción

#include <iostream>

class BaseA {
public:
    BaseA(int valor) {
        std::cout << "Constructor BaseA " << valor << std::endl;
    }
};

class BaseB {
public:
    BaseB(int valor) {
        std::cout << "Constructor BaseB " << valor << std::endl;
    }
};

class BaseC {
public:
    BaseC() {
        std::cout << "Constructor BaseC" << std::endl;
    }
};

class Derivada : public BaseB, public BaseA, public BaseC {
    BaseA miembroA;
    BaseB miembroB;
    BaseC miembroC;

public:
    Derivada(int a, int b, int c, int d)
        : BaseA(a), miembroB(d), miembroA(c), BaseB(b) {}
};

int main() {
    Derivada obj(1, 2, 3, 4);
    return 0;
}

Salida esperada:

Constructor BaseB 2
Constructor BaseA 1
Constructor BaseC
Constructor BaseA 3
Constructor BaseB 4
Constructor BaseC

Resolución de nombres y acceso a miembros

Cuando una clase derivada y sus bases poseen miembros con el mismo nombre, se produce ocultamiento. Para acceder a un miembro de una base específica, se utiliza el operador de resolución de alcance:

NombreClaseBase::miembro

Ejemplo de resolución de nombres

#include <iostream>

class Base1 {
public:
    int dato;
    void mostrar() { std::cout << "Base1::dato = " << dato << std::endl; }
};

class Base2 {
public:
    int dato;
    void mostrar() { std::cout << "Base2::dato = " << dato << std::endl; }
};

class Derivada : public Base1, public Base2 {
public:
    int dato;
    void mostrar() { std::cout << "Derivada::dato = " << dato << std::endl; }
};

int main() {
    Derivada d;
    d.dato = 10;
    d.mostrar();               // Llama a Derivada::mostrar
    d.Base1::dato = 20;
    d.Base1::mostrar();        // Accede al miembro de Base1
    d.Base2::dato = 30;
    d.Base2::mostrar();        // Accede al miembro de Base2
    return 0;
}

Herencia diamante y clases base virtuales

En herencia múltiple, si varias clases derivan de una misma base, la clase final puede contener múltiples copias de los miembros de esa base común. Para evitar esto y garantizar una sola copia, se utiliza la herencia virtual.

#include <iostream>

class BaseComun {
public:
    int valor;
    void metodo() { std::cout << "BaseComun::valor = " << valor << std::endl; }
};

class Intermedia1 : virtual public BaseComun {
    // ...
};

class Intermedia2 : virtual public BaseComun {
    // ...
};

class Final : public Intermedia1, public Intermedia2 {
    // Solo hay una copia de BaseComun
};

int main() {
    Final obj;
    obj.valor = 42;   // Acceso directo, sin ambigüedad
    obj.metodo();
    return 0;
}

Constructores con herencia virtual

Todas las clases derivadas (directa o indirectamente) de una base virtual deben incluir la inicialización de dicha base en sus listas de inicialización.

#include <iostream>

class BaseVirtual {
    int x;
public:
    BaseVirtual(int val) : x(val) {
        std::cout << "Constructor BaseVirtual(" << val << ")" << std::endl;
    }
    void mostrar() { std::cout << "Valor: " << x << std::endl; }
};

class DerivaA : virtual public BaseVirtual {
public:
    DerivaA(int a) : BaseVirtual(a) {}
};

class DerivaB : virtual public BaseVirtual {
public:
    DerivaB(int b) : BaseVirtual(b) {}
};

class Final : public DerivaA, public DerivaB {
public:
    Final(int val) : BaseVirtual(val), DerivaA(val), DerivaB(val) {}
};

int main() {
    Final obj(100);
    obj.mostrar(); // Llama a BaseVirtual::mostrar
    return 0;
}

Regla de compatibilidad de asignación

Cuando se utiliza herencia pública, un objeto de clase derivada puede usarse en cualquier contexto donde se espere un objeto de la clase base (como argumento, asignación o puntero). Sin embargo, solo se podrán acceder a los miembros definidos en la clase base.

Ejemplo de compatibilidad

#include <iostream>

class Base {
public:
    virtual void mostrar() { std::cout << "Base::mostrar()" << std::endl; }
};

class Derivada : public Base {
public:
    void mostrar() override { std::cout << "Derivada::mostrar()" << std::endl; }
};

void procesar(Base* ptr) {
    ptr->mostrar(); // Sin 'virtual', siempre llama a Base::mostrar
}

int main() {
    Base b;
    Derivada d;

    procesar(&b);   // Imprime Base::mostrar()
    procesar(&d);   // También imprime Base::mostrar() si mostrar no es virtual

    return 0;
}

Para invocar funciones de la clase derivada a través de un puntero o referencia de la base, las funciones deben ser declaradas virtual en la clase base.

Etiquetas: C++ herencia clases virtual herencia-multiple

Publicado el 7-9 17:26