Este artículo describe la herencia en tres niveles: principiante, intermedio y avanzado. Experto no. Y ni una palabra sobre SOLID. Honestamente
Principiante
¿Qué es la herencia?
La herencia es uno de los principios fundamentales de la POO. Según esto, una clase puede usar las variables y métodos de otra clase como propios.
Una clase que hereda datos se llama una subclase, una clase derivada o una clase secundaria. La clase de la que se heredan los datos o métodos se denomina superclase, clase base o clase primaria. Los términos "padre" e "hijo" son extremadamente útiles para comprender la herencia. Cuando un niño recibe las características de sus padres, la clase derivada recibe los métodos y variables de la clase base.
La herencia es útil porque le permite estructurar y reutilizar el código, que a su vez puede acelerar significativamente el proceso de desarrollo. A pesar de esto, la herencia debe usarse con precaución, ya que la mayoría de los cambios en la superclase afectarán a todas las subclases, lo que puede conducir a consecuencias imprevistas.
En este ejemplo, el método turn_on()
y la variable serial_number
no se declararon ni definieron en la subclase Computer
. Sin embargo, se pueden usar porque se heredan de la clase base.
Nota importante : Las variables y métodos privados no se pueden heredar.
#include <iostream> using namespace std; class Device { public: int serial_number = 12345678; void turn_on() { cout << "Device is on" << endl; } private: int pincode = 87654321; }; class Computer: public Device {}; int main() { Computer Computer_instance; Computer_instance.turn_on(); cout << "Serial number is: " << Computer_instance.serial_number << endl; // cout << "Pin code is: " << Computer_instance.pincode << endl; // will cause compile time error return 0; }
Tipos de herencia
Hay varios tipos de herencia en C ++:
- public: los datos públicos (
public
) y protegidos ( protected
) se heredan sin cambiar el nivel de acceso a ellos; - protegido (
protected
): todos los datos heredados quedan protegidos; - privado: todos los datos heredados se vuelven privados.
Para la clase base Device
, el nivel de acceso a datos no cambia, pero dado que la clase derivada de Computer
hereda los datos como privados, los datos se vuelven privados para la clase Computer
.
#include <iostream> using namespace std; class Device { public: int serial_number = 12345678; void turn_on() { cout << "Device is on" << endl; } }; class Computer: private Device { public: void say_hello() { turn_on(); cout << "Welcome to Windows 95!" << endl; } }; int main() { Device Device_instance; Computer Computer_instance; cout << "\t Device" << endl; cout << "Serial number is: "<< Device_instance.serial_number << endl; Device_instance.turn_on(); // cout << "Serial number is: " << Computer_instance.serial_number << endl; // Computer_instance.turn_on(); // will cause compile time error cout << "\t Computer" << endl; Computer_instance.say_hello(); return 0; }
La clase Computer
ahora usa el método turn_on()
como cualquier método privado: se puede turn_on()
desde la clase, pero intentar llamarlo directamente desde main
dará como resultado un error en el momento de la compilación. Para el Device
clase base, el método turn_on()
se turn_on()
mantenido público y se puede llamar desde main
.
Constructores y destructores
En C ++, los constructores y destructores no se heredan. Sin embargo, se les llama cuando la clase secundaria inicializa su objeto. Los constructores se llaman jerárquicamente uno tras otro, comenzando con la clase base y terminando con la última clase derivada. Los destructores se llaman en orden inverso.
Nota importante: este artículo no cubre los destructores virtuales. Se puede encontrar material adicional sobre este tema, por ejemplo, en este artículo sobre Habr .
#include <iostream> using namespace std; class Device { public: // constructor Device() { cout << "Device constructor called" << endl; } // destructor ~Device() { cout << "Device destructor called" << endl; } }; class Computer: public Device { public: Computer() { cout << "Computer constructor called" << endl; } ~Computer() { cout << "Computer destructor called" << endl; } }; class Laptop: public Computer { public: Laptop() { cout << "Laptop constructor called" << endl; } ~Laptop() { cout << "Laptop destructor called" << endl; } }; int main() { cout << "\tConstructors" << endl; Laptop Laptop_instance; cout << "\tDestructors" << endl; return 0; }
Constructores: Device
-> Computer
-> Laptop
.
Destructores: Laptop
-> Computer
-> Device
.
Herencia múltiple
La herencia múltiple ocurre cuando una subclase tiene dos o más superclases. En este ejemplo, la clase Laptop
hereda tanto Monitor
como Computer
al mismo tiempo.
#include <iostream> using namespace std; class Computer { public: void turn_on() { cout << "Welcome to Windows 95" << endl; } }; class Monitor { public: void show_image() { cout << "Imagine image here" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); Laptop_instance.show_image(); return 0; }
Múltiples problemas de herencia
La herencia múltiple requiere un diseño cuidadoso, ya que puede conducir a consecuencias imprevistas. La mayoría de estas consecuencias son causadas por la ambigüedad en la herencia. En este ejemplo, Laptop
hereda el método turn_on()
de ambos padres y no está claro qué método debería llamarse.
#include <iostream> using namespace std; class Computer { private: void turn_on() { cout << "Computer is on." << endl; } }; class Monitor { public: void turn_on() { cout << "Monitor is on." << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; // Laptop_instance.turn_on(); // will cause compile time error return 0; }
A pesar de que los datos privados no se heredan, es imposible resolver la herencia ambigua cambiando el nivel de acceso a los datos a privados. Al compilar, primero se realiza una búsqueda de un método o variable, y luego se verifica el nivel de acceso a ellos.
Problema rombo

El problema del diamante es un problema clásico en idiomas que admiten herencia múltiple. Este problema ocurre cuando las clases B
y C
heredan A
, y la clase D
hereda B
y C
Por ejemplo, las clases A
, B
y C
definen el método print_letter()
. Si la clase D
llamará a print_letter()
, no está claro qué método debería llamarse: un método de clase A
, B
o C
Diferentes idiomas tienen diferentes enfoques para resolver problemas en forma de diamante. En C ++, la solución al problema se deja al programador.
El problema en forma de diamante es principalmente un problema de diseño, y debe proporcionarse en la etapa de diseño. En la etapa de desarrollo, se puede resolver de la siguiente manera:
- llamar al método de una superclase específica;
- referirse al objeto de la subclase como un objeto de una superclase particular;
- anular el método problemático en la última clase secundaria (en el código,
turn_on()
en la subclase Laptop
).
#include <iostream> using namespace std; class Device { public: void turn_on() { cout << "Device is on." << endl; } }; class Computer: public Device {}; class Monitor: public Device {}; class Laptop: public Computer, public Monitor { /* public: void turn_on() { cout << "Laptop is on." << endl; } // uncommenting this function will resolve diamond problem */ }; int main() { Laptop Laptop_instance; // Laptop_instance.turn_on(); // will produce compile time error // if Laptop.turn_on function is commented out // calling method of specific superclass Laptop_instance.Monitor::turn_on(); // treating Laptop instance as Monitor instance via static cast static_cast<Monitor&>( Laptop_instance ).turn_on(); return 0; }
Si el método turn_on()
no se ha anulado en Laptop, llamar a Laptop_instance.turn_on()
generará un error de compilación. Un objeto Laptop
puede acceder a dos turn_on()
método turn_on()
simultáneamente: Device:Computer:Laptop.turn_on()
y Device:Monitor:Laptop.turn_on()
.
El problema del diamante: constructores y destructores
Como en C ++, cuando se inicializa un objeto de una clase secundaria, se llama a los constructores de todas las clases primarias, surge otro problema: el constructor de la clase base Device
se llamará dos veces.
#include <iostream> using namespace std; class Device { public: Device() { cout << "Device constructor called" << endl; } }; class Computer: public Device { public: Computer() { cout << "Computer constructor called" << endl; } }; class Monitor: public Device { public: Monitor() { cout << "Monitor constructor called" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; return 0; }
Herencia virtual
La herencia virtual evita que aparezcan múltiples objetos de clase base en la jerarquía de herencia. Por lo tanto, el constructor de la clase base Device
se llamará solo una vez, y una llamada al método turn_on()
sin turn_on()
en la clase secundaria no causará un error de compilación.
#include <iostream> using namespace std; class Device { public: Device() { cout << "Device constructor called" << endl; } void turn_on() { cout << "Device is on." << endl; } }; class Computer: virtual public Device { public: Computer() { cout << "Computer constructor called" << endl; } }; class Monitor: virtual public Device { public: Monitor() { cout << "Monitor constructor called" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); return 0; }
Nota : la herencia virtual en las clases Computer
y Monitor
no permitirá la herencia romboidal si la clase secundaria Laptop
no hereda virtualmente la clase Device
( class Laptop: public Computer, public Monitor, public Device {};
).
Clase abstracta
En C ++, una clase en la que existe al menos un método virtual puro se considera abstracta. Si el método virtual no se anula en la clase secundaria, el código no se compilará. Además, en C ++ es imposible crear un objeto de una clase abstracta; un intento también causará un error durante la compilación.
#include <iostream> using namespace std; class Device { public: void turn_on() { cout << "Device is on." << endl; } virtual void say_hello() = 0; }; class Laptop: public Device { public: void say_hello() { cout << "Hello world!" << endl; } }; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); Laptop_instance.say_hello(); // Device Device_instance; // will cause compile time error return 0; }
Interfaz
C ++, a diferencia de algunos lenguajes OOP, no proporciona una palabra clave separada para denotar una interfaz. Sin embargo, la implementación de la interfaz es posible creando una clase abstracta pura, una clase en la que solo hay declaraciones de métodos. Dichas clases también se denominan a menudo Clase base abstracta (ABC).
#include <iostream> using namespace std; class Device { public: virtual void turn_on() = 0; }; class Laptop: public Device { public: void turn_on() { cout << "Device is on." << endl; } }; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); // Device Device_instance; // will cause compile time error return 0; }
Avanzado
Aunque la herencia es un principio fundamental de la POO, debe usarse con precaución. Es importante pensar que cualquier código que se utilizará es probable que se modifique y se pueda usar de una manera que no sea obvia para el desarrollador.
Herencia de una clase implementada o parcialmente implementada
Si la herencia no proviene de una interfaz (una clase abstracta pura en el contexto de C ++), sino de una clase en la que hay implementaciones, vale la pena considerar que el heredero está conectado a la clase padre por la conexión más cercana posible. La mayoría de los cambios en la clase principal pueden afectar al heredero, lo que puede conducir a un comportamiento inesperado. Tales cambios en el comportamiento del heredero no siempre son obvios: puede producirse un error en el código ya probado y en funcionamiento. Esta situación se ve exacerbada por la presencia de una compleja jerarquía de clases. Siempre vale la pena recordar que el código puede ser cambiado no solo por la persona que lo escribió, y sus colegas no pueden tener en cuenta las rutas de herencia obvias para el autor.
Por el contrario, vale la pena señalar que la herencia de clases implementadas parcialmente tiene una ventaja innegable. Las bibliotecas y los marcos a menudo funcionan de la siguiente manera: proporcionan al usuario una clase abstracta con varios métodos virtuales y muchos implementados. Por lo tanto, la mayor cantidad de trabajo ya se ha realizado: la lógica compleja ya se ha escrito y el usuario solo puede personalizar la solución preparada para satisfacer sus necesidades.
Interfaz
La herencia de una interfaz (clase abstracta pura) presenta la herencia como una oportunidad para estructurar el código y proteger al usuario. Dado que la interfaz describe qué trabajo realizará la clase de implementación, pero no describe cómo, cualquier usuario de la interfaz está protegido de los cambios en la clase que implementa esta interfaz.
Interfaz: ejemplo de uso
En primer lugar, vale la pena señalar que el ejemplo está estrechamente relacionado con el concepto de polimorfismo, pero se considerará en el contexto de la herencia de una clase abstracta pura.
Una aplicación que ejecuta lógica de negocio abstracta debe configurarse desde un archivo de configuración separado. En una etapa temprana de desarrollo, el formato de este archivo de configuración no estaba completamente formado. Pasar el análisis de archivos detrás de una interfaz ofrece varias ventajas.
La falta de claridad con respecto al formato del archivo de configuración no ralentiza el proceso de desarrollo del programa principal. Dos desarrolladores pueden trabajar en paralelo: uno en la lógica de negocios y el otro en el analizador. Como interactúan a través de esta interfaz, cada uno de ellos puede trabajar de forma independiente. Este enfoque facilita la codificación de pruebas unitarias con código, ya que las pruebas necesarias se pueden escribir utilizando simulacro para esta interfaz.
Además, al cambiar el formato del archivo de configuración, la lógica empresarial de la aplicación no se ve afectada. Lo único que requiere una transición completa de un formato a otro es escribir una nueva implementación de la clase abstracta ya existente (clase parser). Además, volver al formato de archivo original requiere un trabajo mínimo: reemplazar un analizador existente por otro.
Conclusión
La herencia proporciona muchos beneficios, pero debe diseñarse cuidadosamente para evitar los problemas para los que presenta una oportunidad. En el contexto de la herencia, C ++ proporciona una amplia gama de herramientas que abren un montón de posibilidades para el programador.
Y SOLID es bueno.