Menu
Coddy logo textTech

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() {}
};
challenge icon

Desafío

Fácil

Vamos 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 base Media que 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
  • AudioTrack.h: Define una clase AudioTrack que hereda públicamente de Media:
    • 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 clave override para imprimir: Playing audio: <title> by <artist>
    • Un destructor que imprime: AudioTrack [<title>] destroyed
  • main.cpp: Lee tres entradas (cada una en una línea separada):
    1. Título de la pista de audio (string)
    2. Nombre del artista (string)
    3. Título del video (string)

    Define una clase VideoClip directamente en main.cpp que herede públicamente de Media:

    • 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 Media con el título "Generic Media"
    • Un objeto AudioTrack con el título y artista de entrada
    • Un objeto VideoClip con 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;
}
quiz iconPonte a prueba

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