Cargando...

Polimorfismo en C++: Funciones virtuales y Vtable

Aprende polimorfismo en C++ con funciones virtuales, vtable y enlace dinámico para desarrollar aplicaciones orientadas a objetos.

El polimorfismo permite que los objetos que comparten la misma interfaz (tipo base) muestren comportamientos diferentes en tiempo de ejecución. En C++, el polimorfismo dinámico se logra mediante funciones virtuales, y el compilador implementa este mecanismo utilizando una vtable (tabla virtual) y un vptr (puntero virtual) en cada objeto. En este artículo exploraremos cómo funcionan las funciones virtuales, la estructura vtable/vptr, los impactos en el rendimiento y las mejores prácticas con ejemplos realistas.


1) ¿Qué es el polimorfismo dinámico?

El polimorfismo dinámico significa que qué función se llama se decide en tiempo de ejecución. Esto permite invocar el comportamiento de una clase derivada a través de un puntero o referencia del tipo base.


#include <iostream>
#include <memory>
using namespace std;

struct Book {
    virtual ~Book() = default;           // requerido para una base polimórfica
    virtual void PrintInfo() const {     // función virtual
        cout << "[Book] Información general" << endl;
    }
};

struct EBook : Book {
    void PrintInfo() const override {
        cout << "[EBook] Edición digital" << endl;
    }
};

struct PrintedBook : Book {
    void PrintInfo() const override {
        cout << "[PrintedBook] Edición física" << endl;
    }
};

int main() {
    unique_ptr<Book> a = make_unique<EBook>();
    unique_ptr<Book> b = make_unique<PrintedBook>();

    a->PrintInfo();  // [EBook] ...
    b->PrintInfo();  // [PrintedBook] ...
}

Aunque a y b parecen ser del tipo base (Book), las llamadas se dirigen en tiempo de ejecución al tipo real (EBook/PrintedBook).


2) ¿Cómo funcionan vtable y vptr?

Cuando una clase tiene al menos una función virtual, el compilador genera una vtable para esa clase. Cada objeto contiene un campo oculto vptr que apunta a la vtable de la clase correspondiente.

Durante una llamada: objeto.vptr → vtable → dirección de la función real.


Book (vptr) ──► vtable(Book)       : &Book::PrintInfo
EBook (vptr) ─► vtable(EBook)       : &EBook::PrintInfo
Printed (vptr)► vtable(PrintedBook) : &PrintedBook::PrintInfo

Esta redirección indirecta añade un pequeño costo de llamada, aunque en la mayoría de los programas es insignificante.


3) override, final y destructor virtual

La palabra clave override verifica en tiempo de compilación que realmente se está sobrescribiendo una función virtual de la clase base. final evita que una función o clase sea sobrescrita nuevamente. En clases base polimórficas, un destructor virtual es esencial; de lo contrario, eliminar un objeto derivado mediante un puntero base provoca comportamiento indefinido.


struct Book {
    virtual ~Book() = default;
    virtual double Price() const { return 0.0; }
};

struct EBook final : Book {
    double Price() const override { return 99.0; }
};

// struct SpecialEBook : EBook {}; // ERROR: EBook es final

4) Diferencia con el polimorfismo estático (sobrecarga/plantillas)

Las sobrecargas (mismo nombre, firma diferente) y las plantillas se resuelven en tiempo de compilación y no usan vtable. El polimorfismo dinámico, en cambio, se resuelve en tiempo de ejecución mediante una llamada virtual.


5) Diseño de una interfaz polimórfica (ejemplo: Book)

Coloque el comportamiento común en la clase base y personalícelo en las clases derivadas. Ejemplo de cálculo de precio para libros digitales y físicos:


#include <iostream>
#include <memory>
#include <string>
using namespace std;

class Book {
protected:
    string title, author;
public:
    Book(string t, string a) : title(t), author(a) {}
    virtual ~Book() = default;

    virtual double FinalPrice() const = 0;   // puro virtual: contrato abstracto
    virtual void PrintInfo() const {
        cout << title << " - " << author
             << " | " << FinalPrice() << " €" << endl;
    }
};

class EBook : public Book {
    double base, vat;
public:
    EBook(string t, string a, double b, double v)
        : Book(t,a), base(b), vat(v) {}

    double FinalPrice() const override {
        return base*(1.0+vat);
    }
};

class PrintedBook : public Book {
    double base, ship; int pages; bool hard;
public:
    PrintedBook(string t, string a, double b, double s, int p, bool h)
        : Book(t,a), base(b), ship(s), pages(p), hard(h) {}

    double FinalPrice() const override {
        double cost = base + ship + 0.04*pages;
        if (hard) cost *= 1.1;
        return cost;
    }
};

int main() {
    unique_ptr<Book> b1 = make_unique<EBook>("Modern C++", "A. Dev", 100, 0.10);
    unique_ptr<Book> b2 = make_unique<PrintedBook>("Clean Code", "R. Martin", 120, 20, 464, true);

    b1->PrintInfo(); // comportamiento de EBook
    b2->PrintInfo(); // comportamiento de PrintedBook
}

6) Object Slicing y uso de referencias/punteros

Si copia un objeto derivado por valor en el tipo base, la parte específica del derivado se pierde. Para el polimorfismo, use siempre una referencia o un puntero base.


PrintedBook pb("X","Y",100,20,200,false);
Book base = pb;   // SLICING: solo se copia la parte Book
base.PrintInfo(); // comportamiento de Book (se pierde la parte derivada)

7) dynamic_cast y seguridad de tipo

Use dynamic_cast para conversiones seguras desde un tipo base a un derivado (requiere RTTI). Si falla, devuelve nullptr (para punteros).


void PrintIfPrinted(const Book* b) {
    if (auto pb = dynamic_cast<const PrintedBook*>(b)) {
        cout << "Precio impreso: " << pb->FinalPrice() << endl;
    } else {
        cout << "No es un PrintedBook." << endl;
    }
}

8) Notas sobre el rendimiento


9) Clases/métodos final, errores override y buenas prácticas


10) Visualización del efecto de la vtable (paso a paso)

  1. La clase contiene al menos una función virtual → el compilador crea una vtable.
  2. Al construir un objeto, el vptr apunta a la vtable de su clase.
  3. Durante la llamada, el compilador obtiene la dirección mediante vptr → vtable en lugar de un enlace estático.
  4. Un override reemplaza la entrada correspondiente en la vtable de la clase derivada.

11) Errores comunes


12) Ejemplo completo: colección polimórfica


#include <iostream>
#include <vector>
#include <memory>
using namespace std;

class Book {
public:
    virtual ~Book() = default;
    virtual double FinalPrice() const = 0;
    virtual void Print() const = 0;
};

class EBook : public Book {
    double base, vat;
public:
    EBook(double b, double v) : base(b), vat(v) {}
    double FinalPrice() const override { return base*(1+vat); }
    void Print() const override {
        cout << "[EBook] " << FinalPrice() << " €" << endl;
    }
};

class PrintedBook : public Book {
    double base, ship; int pages;
public:
    PrintedBook(double b, double s, int p) : base(b), ship(s), pages(p) {}
    double FinalPrice() const override { return base + ship + 0.05*pages; }
    void Print() const override {
        cout << "[Printed] " << FinalPrice() << " €" << endl;
    }
};

int main() {
    vector<unique_ptr<Book>> items;
    items.push_back(make_unique<EBook>(100, 0.1));
    items.push_back(make_unique<PrintedBook>(120, 20, 400));

    for (const auto& it : items) it->Print(); // polimorfismo dinámico
}

13) TL;DR

  • El polimorfismo dinámico en C++ se logra mediante funciones virtuales.
  • El compilador implementa las llamadas virtuales con vtable/vptr.
  • Un destructor virtual es obligatorio en clases base polimórficas.
  • override garantiza la corrección de la firma; final evita más sobrescrituras.
  • Use referencias/punteros en lugar de copias por valor para evitar slicing.
  • El costo es generalmente bajo; para rutas críticas considere alternativas (CRTP/sobrecarga).
  • Todos los ejemplos son compatibles con Visual Studio 2022 y GCC 11+.

Artículos relacionados