Funciones virtuales y VTable
Parte de la sección Programación Orientada a Objetos del Journey de C++ de Coddy. Lección 52 de 104.
La palabra clave virtual resuelve el problema que vimos en la lección anterior. Cuando declaras un método como virtual, C++ determina qué versión llamar según el tipo real del objeto durante la ejecución, no según el tipo del puntero.
class Animal {
public:
virtual void speak() {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
Dog d;
Animal* ptr = &d;
ptr->speak(); // Salida: Woof!¡Ahora se llama al método correcto! La palabra clave override es opcional, pero se recomienda. Le indica al compilador que pretendes sobrescribir una función virtual, detectando errores si las firmas no coinciden.
¿Cómo funciona esto? Cuando una clase tiene funciones virtuales, el compilador crea una tabla virtual (vtable). Esta es una tabla de búsqueda oculta que contiene punteros a las funciones virtuales de la clase. Cada objeto con funciones virtuales almacena un puntero oculto (vptr) a la vtable de su clase.
Cuando llamas a una función virtual a través de un puntero, el programa busca la dirección de función correcta en la vtable durante la ejecución. Esto se denomina despacho dinámico. Añade una pequeña sobrecarga en comparación con las llamadas a funciones normales, pero permite un potente comportamiento polimórfico.
Una regla importante: si una clase tiene funciones virtuales y se utilizará como clase base, su destructor también debería ser virtual. Esto garantiza una limpieza adecuada al eliminar objetos derivados mediante punteros a la clase base:
class Animal {
public:
virtual ~Animal() {} // Destructor virtual
virtual void speak() {}
};Desafío
FácilConstruyamos un sistema de reproducción multimedia que demuestre el poder de las funciones virtuales y el polimorfismo en tiempo de ejecución. Crearás una jerarquía de tipos de medios en la que cada uno se reproduce de forma diferente, y verás cómo las funciones virtuales permiten el comportamiento correcto incluso cuando se accede a ellas mediante punteros a la clase base.
Organizarás tu código en tres archivos:
Media.h: Define una clase baseMediaque representa cualquier medio reproducible:- Un miembro protegido
std::string title - Un constructor que recibe un título y lo almacena
- Un método virtual
play()que imprime:Playing media: <title> - Un destructor virtual que imprime:
Media [<title>] destroyed
- Un miembro protegido
AudioTrack.h: Define una claseAudioTrackque hereda públicamente deMedia:- Un miembro privado
std::string artist - Un constructor que recibe un título y un artista, pasa el título a la clase base y almacena el artista
- Sobrescribe
play()usando la palabra claveoverridepara imprimir:Playing audio: <title> by <artist> - Un destructor que imprime:
AudioTrack [<title>] destroyed
- Un miembro privado
main.cpp: Lee tres entradas (cada una en una línea separada):- Título de la pista de audio (cadena)
- Nombre del artista (cadena)
- Título del video (cadena)
Define una clase
VideoClipdirectamente en main.cpp que herede públicamente deMedia:- Un miembro privado
int duration(en segundos) - Un constructor que reciba un título y una duración (establece la duración predeterminada en 120)
- Sobrescribe
play()para imprimir:Playing video: <title> (<duration>s) - Un destructor que imprime:
VideoClip [<title>] destroyed
Crea un arreglo de tres punteros
Media*dentro de un ámbito de bloque. Reserva dinámicamente:- Un objeto base
Mediacon el título "Generic Media" - Un objeto
AudioTrackcon el título y el artista introducidos - Un objeto
VideoClipcon el título del video introducido
Recorre el arreglo y llama a
play()en cada puntero. Después, elimina todos los objetos en orden inverso. Después del bloque, imprime:Playback complete!
Por ejemplo, con las entradas Bohemian Rhapsody, Queen y Nature Documentary:
Playing media: Generic Media
Playing audio: Bohemian Rhapsody by Queen
Playing video: Nature Documentary (120s)
VideoClip [Nature Documentary] destroyed
Media [Nature Documentary] destroyed
AudioTrack [Bohemian Rhapsody] destroyed
Media [Bohemian Rhapsody] destroyed
Media [Generic Media] destroyed
Playback complete!Observa cómo llamar a play() mediante punteros Media* invoca el método correcto de la clase derivada gracias a las funciones virtuales. Observa también cómo el destructor virtual garantiza una limpieza adecuada: tanto el destructor derivado como el de la clase base se ejecutan al eliminar mediante un puntero a la clase base.
Pruébalo tú mismo
#include <iostream>
#include <string>
#include "Media.h"
#include "AudioTrack.h"
using namespace std;
// TODO: Define la clase VideoClip aquí que hereda públicamente de Media
// - Miembro privado int duration (en segundos)
// - El constructor toma title y duration (duration por defecto a 120)
// - Override play() to print: Playing video: <title> (<duration>s)
// - Destructor prints: VideoClip [<title>] destroyed
class VideoClip : public Media {
private:
int duration;
public:
// TODO: Implementa el constructor
VideoClip(const std::string& t, int d = 120) : Media(t) {
// TODO: Almacena la duration
}
// TODO: Sobrescribe el método play()
void play() override {
// TODO: Implementa este método
}
// TODO: Implementa el destructor
~VideoClip() {
// TODO: Implementa este destructor
}
};
int main() {
// Lee las entradas
string audioTitle;
string artist;
string videoTitle;
getline(cin, audioTitle);
getline(cin, artist);
getline(cin, videoTitle);
// TODO: Crea un ámbito de bloque con llaves
// Dentro del bloque:
// 1. Crea un array de tres punteros Media*
// 2. Asigna dinámicamente:
// - A base Media object with title "Generic Media"
// - Un objeto AudioTrack con el título y el artista de entrada
// - Un objeto VideoClip con el título de video de entrada
// 3. Recorre el array y llama a play() en cada puntero
// 4. Elimina todos los objetos en orden inverso
// TODO: After the block, print: Playback complete!
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éricoPractica por tu cuenta: Compilador de C++ online