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 basándose en el tipo de objeto real en tiempo de ejecución, no en el tipo de 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 recomendada. Le indica al compilador que tienes la intención de 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 virtual table (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 en tiempo de ejecución. Esto se llama despacho dinámico. Añade una pequeña sobrecarga en comparación con las llamadas a funciones regulares, pero permite un comportamiento polimórfico potente.
Una regla importante: si una clase tiene funciones virtuales y se utilizará como clase base, su destructor también debería ser virtual. Esto asegura una limpieza adecuada al eliminar objetos derivados a través de punteros de la clase base:
class Animal {
public:
virtual ~Animal() {} // Destructor virtual
virtual void speak() {}
};Desafío
FácilVamos a construir un sistema de reproductor de medios 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 donde cada uno se reproduce de manera diferente, y verás cómo las funciones virtuales permiten el comportamiento correcto incluso cuando se accede a través de punteros de 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 (string)
- Nombre del artista (string)
- Título del video (string)
Define una clase
VideoClipdirectamente en main.cpp que herede públicamente deMedia:- Un miembro privado
int duration(en segundos) - Un constructor que recibe un título y una duración (duración por defecto de 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. Asigna dinámicamente:- Un objeto base
Mediacon el título "Generic Media" - Un objeto
AudioTrackcon el título y artista de entrada - Un objeto
VideoClipcon el título de video de entrada
Recorre el arreglo y llama a
play()en cada puntero. Luego 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() a través de punteros Media* invoca el método correcto de la clase derivada gracias a las funciones virtuales. También observa cómo el destructor virtual asegura una limpieza adecuada: tanto el destructor de la clase derivada como el de la clase base se ejecutan al eliminar a través de un puntero base.
Hoja de referencia
La palabra clave virtual permite el polimorfismo en tiempo de ejecución al determinar qué método llamar basándose en el tipo de objeto real, no en el tipo de 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(); // Output: Woof!La palabra clave override es opcional pero recomendada. Le indica al compilador que tienes la intención de sobrescribir una función virtual, detectando errores si las firmas no coinciden.
Cómo funcionan las funciones virtuales: El compilador crea una tabla virtual (vtable) 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. Al llamar a una función virtual a través de un puntero, el programa busca la dirección de función correcta en la vtable en tiempo de ejecución. Esto se denomina despacho dinámico (dynamic dispatch).
Destructores virtuales: Si una clase tiene funciones virtuales y se utilizará como clase base, su destructor también debe ser virtual. Esto asegura una limpieza adecuada al eliminar objetos derivados a través de punteros de la clase base:
class Animal {
public:
virtual ~Animal() {} // Virtual destructor
virtual void speak() {}
};Pruébalo tú mismo
#include <iostream>
#include <string>
#include "Media.h"
#include "AudioTrack.h"
using namespace std;
// TODO: Define la clase VideoClip aquí que herede públicamente de Media
// - Miembro privado int duration (en segundos)
// - El constructor toma title y duration (duration por defecto a 120)
// - Sobrescribe play() para imprimir: Playing video: <title> (<duration>s)
// - El destructor imprime: VideoClip [<title>] destroyed
class VideoClip : public Media {
private:
int duration;
public:
// TODO: Implementar el constructor
VideoClip(const std::string& t, int d = 120) : Media(t) {
// TODO: Almacenar la duration
}
// TODO: Sobrescribir el método play()
void play() override {
// TODO: Implementar este método
}
// TODO: Implementar el destructor
~VideoClip() {
// TODO: Implementar este destructor
}
};
int main() {
// Leer las entradas
string audioTitle;
string artist;
string videoTitle;
getline(cin, audioTitle);
getline(cin, artist);
getline(cin, videoTitle);
// TODO: Crear un ámbito de bloque con llaves
// Dentro del bloque:
// 1. Crear un arreglo de tres punteros Media*
// 2. Asignar dinámicamente:
// - Un objeto base Media con el título "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. Recorrer el arreglo y llamar a play() en cada puntero
// 4. Eliminar todos los objetos en orden inverso
// TODO: Después del bloque, imprimir: 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érico