Herencia virtual
Parte de la sección Programación Orientada a Objetos del Journey de C++ de Coddy — lección 54 de 104.
El problema del diamante ocurre cuando una clase hereda de dos clases que comparten una clase base común. Sin un manejo especial, la clase derivada termina con dos copias de la base común, lo que causa ambigüedad y desperdicio de memoria.
class Animal {
public:
int age;
};
class Mammal : public Animal {};
class Bird : public Animal {};
class Bat : public Mammal, public Bird {};
Bat b;
b.age = 5; // Error: ambiguo - ¿cuál 'age'?La clase Bat contiene dos subobjetos Animal separados: uno a través de Mammal y otro a través de Bird. Esto crea el diagrama de herencia en forma de diamante que le da nombre al problema.
La herencia virtual resuelve esto asegurando que solo exista una copia de la base común. Añada la palabra clave virtual al heredar de la base compartida:
class Animal {
public:
int age;
Animal(int a = 0) : age(a) {}
};
class Mammal : virtual public Animal {
public:
Mammal(int a = 0) : Animal(a) {}
};
class Bird : virtual public Animal {
public:
Bird(int a = 0) : Animal(a) {}
};
class Bat : public Mammal, public Bird {
public:
Bat(int a) : Animal(a), Mammal(a), Bird(a) {}
};
Bat b(5);
b.age = 10; // ¡Funciona! Solo existe un 'age'Observe que Bat debe inicializar directamente a Animal en su constructor. Con la herencia virtual, la clase más derivada es responsable de construir la base virtual, independientemente de las clases intermedias.
Desafío
FácilVamos a construir un sistema de gestión de trabajadores que demuestre cómo la herencia virtual resuelve el problema del diamante. Crearás una jerarquía donde un TeamLead hereda tanto de Developer como de Manager, los cuales comparten una clase base común Employee.
Organizarás tu código en cuatro archivos:
Employee.h: Define la clase base comúnEmployeecon:- Un
std::string namey unint idprotegidos - Un constructor que recibe ambos valores e imprime:
Employee [<name>] hired with ID <id> - Un método público
getInfo()que imprime:Employee: <name> (ID: <id>) - Un destructor virtual que imprime:
Employee [<name>] record closed
- Un
Developer.h: Define una claseDeveloperque utiliza herencia pública virtual deEmployee:- Un miembro protegido
std::string language - Un constructor que recibe name, id y language — pasa name e id a
Employee, almacena el lenguaje e imprime:Developer [<name>] specializes in <language> - Un método público
code()que imprime:<name> is coding in <language> - Un destructor que imprime:
Developer [<name>] signed off
- Un miembro protegido
Manager.h: Define una claseManagerque utiliza herencia pública virtual deEmployee:- Un miembro protegido
int teamSize - Un constructor que recibe name, id y team size — pasa name e id a
Employee, almacena el tamaño del equipo e imprime:Manager [<name>] leads a team of <teamSize> - Un método público
manage()que imprime:<name> is managing <teamSize> people - Un destructor que imprime:
Manager [<name>] stepped down
- Un miembro protegido
main.cpp: Lee cuatro entradas (cada una en una línea separada):- Nombre (string)
- ID de empleado (entero)
- Lenguaje de programación (string)
- Tamaño del equipo (entero)
Define una clase
TeamLeadque hereda públicamente tanto deDevelopercomo deManager:- Un constructor que recibe los cuatro parámetros y debe inicializar directamente a
Employee(la base virtual), luego aDeveloperyManager - El constructor debe imprimir:
TeamLead [<name>] ready to lead and code! - Un método
showRole()que llama agetInfo(),code()ymanage()en ese orden - Un destructor que imprime:
TeamLead [<name>] promoted out
Crea un objeto
TeamLeaddentro de un ámbito de bloque, llama ashowRole()y luego deja que salga de ámbito. Después del bloque, imprime:Organization restructured!
Por ejemplo, con las entradas Alice, 101, C++ y 5:
Employee [Alice] hired with ID 101
Developer [Alice] specializes in C++
Manager [Alice] leads a team of 5
TeamLead [Alice] ready to lead and code!
Employee: Alice (ID: 101)
Alice is coding in C++
Alice is managing 5 people
TeamLead [Alice] promoted out
Manager [Alice] stepped down
Developer [Alice] signed off
Employee [Alice] record closed
Organization restructured!Observa cómo solo hay una llamada al constructor de Employee y una llamada al destructor de Employee — la herencia virtual garantiza que solo exista una copia de la base compartida. El TeamLead debe inicializar directamente a Employee porque, con la herencia virtual, la clase más derivada es responsable de construir la base virtual.
Hoja de referencia
El problema del diamante ocurre cuando una clase hereda de dos clases que comparten una clase base común, lo que resulta en dos copias de la clase base y causa ambigüedad.
class Animal {
public:
int age;
};
class Mammal : public Animal {};
class Bird : public Animal {};
class Bat : public Mammal, public Bird {};
Bat b;
b.age = 5; // Error: ambiguo - ¿cuál 'age'?La herencia virtual resuelve el problema del diamante al asegurar que solo exista una copia de la clase base común. Use la palabra clave virtual al heredar:
class Animal {
public:
int age;
Animal(int a = 0) : age(a) {}
};
class Mammal : virtual public Animal {
public:
Mammal(int a = 0) : Animal(a) {}
};
class Bird : virtual public Animal {
public:
Bird(int a = 0) : Animal(a) {}
};
class Bat : public Mammal, public Bird {
public:
Bat(int a) : Animal(a), Mammal(a), Bird(a) {}
};
Bat b(5);
b.age = 10; // ¡Funciona! Solo existe un 'age'Con la herencia virtual, la clase más derivada debe inicializar directamente la clase base virtual en su constructor, independientemente de las clases intermedias.
Pruébalo tú mismo
#include <iostream>
#include <string>
#include "Developer.h"
#include "Manager.h"
using namespace std;
// TODO: Define la clase TeamLead que hereda públicamente tanto de Developer como de Manager
// Recuerda: Con la herencia virtual, TeamLead debe inicializar directamente a Employee (la base virtual)
class TeamLead : public Developer, public Manager {
public:
// TODO: Implementa el constructor que toma name, id, language y teamSize
// Debe inicializar: primero Employee (base virtual), luego Developer, después Manager
// Debe imprimir: TeamLead [<name>] ready to lead and code!
TeamLead(const std::string& name, int id, const std::string& language, int teamSize)
: Employee(name, id),
Developer(name, id, language),
Manager(name, id, teamSize) {
// TODO: Imprime el mensaje del constructor
}
// TODO: Implementa el método showRole()
// Debe llamar a getInfo(), code() y manage() en ese orden
void showRole() {
// TODO: Llama a los tres métodos
}
// TODO: Implementa el destructor
// Debe imprimir: TeamLead [<name>] promoted out
~TeamLead() {
// TODO: Imprime el mensaje del destructor
}
};
int main() {
// Lee las entradas
string name;
int id;
string language;
int teamSize;
getline(cin, name);
cin >> id;
cin.ignore();
getline(cin, language);
cin >> teamSize;
// TODO: Crea un objeto TeamLead dentro de un ámbito de bloque
// Llama a showRole(), luego deja que salga de ámbito
{
// TODO: Crea TeamLead y llama a showRole()
}
// Imprime el mensaje final después del bloque
cout << "Organization restructured!" << endl;
return 0;
}
Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.
Todas las lecciones de Programación Orientada a Objetos
1Fundamentos de OOP
Archivos externosConstrucción y compilación en C++Archivos de cabecera y archivos fuenteNamespaces y alcanceIntroducción a OOP en C++Clases vs ObjetosEl puntero 'this'Métodos (Funciones miembro)Atributos (Miembros de datos)Conceptos básicos de Ctors y DtorsResumen - Calculadora simple4Propiedades de clase
Miembros de instancia vs. estáticosGetters y SettersFunciones miembro constPalabra clave mutableMétodos y variables estáticosFunciones y clases amigasResumen - Gestor de cuentas bancarias7Herencia
Herencia básicaNiveles de acceso en la herenciaOrden de llamada de Ctor y DtorSobrescritura de métodosFunciones virtuales y VTableHerencia múltipleHerencia virtualResumen - Jerarquía de empleados2Gestión de memoria
Memoria Stack vs HeapPunteros y referenciasMemoria dinámica (new/delete)Punteros inteligentes en C++RAII en C++Resumen - Gestor de arrays dinámicos5Encapsulamiento
Especificadores de acceso en C++Especificadores de acceso en profundidadOcultamiento de informaciónStruct vs ClassClases anidadas e internasResumen - Sistema de registros de estudiantes8Polimorfismo
Polimorfismo: Compilación vs. Tiempo de ejecuciónSobrecarga de funcionesFunciones virtuales revisadasFunciones virtuales purasClases abstractasDiseño de interfaces en C++Dynamic Casting y RTTIResumen: Calculadora de figuras11Conceptos avanzados de POO
Composición vs. HerenciaMixins mediante CRTPIdioma PimplBorrado de tiposEnum Classes y tipado fuerteManejo de excepciones en POOJerarquías de excepciones personalizadas14Patrones de diseño - Parte 2
Patrón CommandPatrón AdapterPatrón DecoratorPatrón Template MethodPatrón StatePatrón CompositeRAII como patrón3Constructores y Destructores
Constructor por defectoConstructor parametrizadoConstructor de copiaConstructor de movimientoListas de inicialización del constructorConstructores delegadosAnálisis profundo del destructorRegla de tres / cinco / ceroResumen - Clase String6Sobrecarga de operadores
Introducción a la sobrecarga de operadoresSobrecarga de operadores aritméticosSobrecarga de operadores de comparaciónOperadores de flujo (Stream)Sobrecarga del operador de asignaciónSobrecarga de los operadores [] y ()Operadores de conversión de tiposResumen - Clase Matrix9Plantillas
Plantillas de funcionesPlantillas de clasesEspecialización de plantillasPlantillas variádicasConceptos básicos de SFINAE y Type TraitsResumen - Contenedor genérico