C++ Polimorfismo

PolimorfismoLiteralmente significa múltiples formas.

Cuando existe una jerarquía entre clases y las clases se relacionan mediante herencia, se utiliza el polimorfismo.

En C++, el polimorfismo (Polymorphism) es una de las características importantes de la programación orientada a objetos.

El polimorfismo de C++ permite utilizar punteros o referencias de la clase base para llamar a los métodos reescritos de la subclase, de modo que la misma interfaz pueda presentar diferentes comportamientos.

El polimorfismo hace que el código sea más flexible y genérico. El programa puede operar sobre objetos de diferentes tipos a través de punteros o referencias de la clase base, sin necesidad de distinguir explícitamente el tipo de objeto. Esto hace que el código sea más extensible: al añadir una nueva clase de forma, no es necesario modificar el programa principal.

A continuación se presentan varios puntos clave del polimorfismo:

Funciones virtuales (Virtual Functions):

  • En la clase base se declara una función como virtual, utilizando la palabra clavevirtual。
  • La clase derivada puede reescribir (override) esta función virtual.
  • Al llamar a una función virtual, se decide qué versión de la función invocar según el tipo real del objeto.

Enlace dinámico (Dynamic Binding):

  • También llamado enlace tardío (Late Binding); determina la implementación específica de la llamada a la función en tiempo de ejecución.
  • Se necesita utilizar un puntero o una referencia a la clase base para llamar a la función virtual; el compilador decide en tiempo de ejecución qué función invocar según el tipo real del objeto.

Funciones virtuales puras (Pure Virtual Functions):

  • Una clase que contiene funciones virtuales puras se denomina clase abstracta (Abstract Class) y no puede instanciarse directamente.
  • Una función virtual pura no tiene cuerpo de función; al declararla se utiliza= 0。
  • Obliga a la clase derivada a proporcionar una implementación concreta.

Mecanismo de implementación del polimorfismo:

  • Tabla de funciones virtuales (V-Table): C++ en tiempo de ejecución utiliza la tabla de funciones virtuales para implementar el polimorfismo. Cada clase que contiene funciones virtuales tiene una tabla de funciones virtuales, en la que se almacenan punteros a todas las funciones virtuales de la clase.
  • Puntero de función virtual (V-Ptr): el objeto contiene un puntero a la tabla de funciones virtuales de su clase.

Ventajas de utilizar el polimorfismo:

  • Reutilización de código: a través de punteros o referencias de la clase base, se pueden operar objetos de diferentes clases derivadas, logrando la reutilización del código.
  • Extensibilidad: al añadir una clase derivada nueva, no es necesario modificar el código que depende de la clase base; solo hay que asegurarse de que la nueva clase reescriba correctamente las funciones virtuales.
  • Desacoplamiento: el polimorfismo permite que el diseño del programa sea más modular, reduciendo el acoplamiento entre clases.

Consideraciones:

  • Solo se produce polimorfismo cuando se llama a una función virtual a través de un puntero o referencia de la clase base.
  • Si se utiliza directamente un objeto de la clase derivada para llamar a la función, se invoca la versión de la clase derivada, no la de la clase base.
  • El polimorfismo requiere información de tipo en tiempo de ejecución (RTTI), lo que puede aumentar la sobrecarga del programa.

Ejemplo 1

Veamos la aplicación del polimorfismo a través de un ejemplo sencillo:

Ejemplo 1

#include <iostream>
using namespace std;

// Clase base Animal
class Animal {
public:
    // Función virtual sound, proporciona una interfaz para el sonido de diferentes animales
    virtual void sound() const {
        cout << "Animal makes a sound" << endl;
    }
   
    // Destructor virtual asegura que los objetos de las subclases se destruyan correctamente
    virtual ~Animal() {
        cout << "Animal destroyed" << endl;
    }
};

// Clase derivada Dog, hereda de Animal
class Dog : public Animal {
public:
    // Reescribe el método sound
    void sound() const override {
        cout << "Dog barks" << endl;
    }
   
    ~Dog() {
        cout << "Dog destroyed" << endl;
    }
};

// Clase derivada Cat, hereda de Animal
class Cat : public Animal {
public:
    // Reescribe el método sound
    void sound() const override {
        cout << "Cat meows" << endl;
    }
   
    ~Cat() {
        cout << "Cat destroyed" << endl;
    }
};

// Prueba del polimorfismo
int main() {
    Animal* animalPtr;  // Puntero de la clase base

    // Crea un objeto Dog y lo asigna al puntero Animal
    animalPtr = new Dog();
    animalPtr->sound();  // Llama al método sound de Dog
    delete animalPtr;    // Libera memoria, llama a los destructores de Dog y Animal

    // Crea un objeto Cat y lo asigna al puntero Animal
    animalPtr = new Cat();
    animalPtr->sound();  // Llama al método sound de Cat
    delete animalPtr;    // Libera memoria, llama a los destructores de Cat y Animal

    return 0;
}

La salida de la ejecución del programa es:

Dog barks
Dog destroyed
Animal destroyed
Cat meows
Cat destroyed
Animal destroyed

Explicación del código

Clase baseAnimal:

  • AnimalLa clase define una función virtualsound(), es una función virtual (virtual), utilizada para representar el comportamiento de sonido de los animales.
  • ~Animal()Es un destructor virtual, asegura que al liberar el objeto de la clase derivada apuntado por el puntero de la clase base se llame correctamente al destructor de la clase derivada, evitando fugas de memoria.

Clases derivadasDogyCat:

  • DogyCatLas clases derivan deAnimalla clase, y cada una implementasound()el método.
  • Dogdesound()Imprime "Dog barks";Catdesound()Imprime "Cat meows". Esto hace que el mismo método (sound()) se comporte de manera diferente en distintas clases.

Función principalmain():

  • Crear un puntero de la clase baseanimalPtr。
  • Usandonew Dog()crearDogun objeto y asignar su dirección aanimalPtr. En este momento, llamar aanimalPtr->sound()producirá "Dog barks", porqueanimalPtrrealmente apunta aDogobjeto.
  • Al liberarDogel objeto, primero se llamaDogal destructor de, y luego se llamaAnimalal destructor de.
  • Usandonew Cat()crearCatun objeto y asignarlo aanimalPtr, luego llamar aanimalPtr->sound(), producirá "Cat meows", mostrando el comportamiento polimórfico.

Conceptos clave

  • Función virtual: Al usar en la clase basevirtualla palabra clave para declarar la función virtual, la clase derivada puede reescribir esta función, logrando así que en tiempo de ejecución se llame a la función correcta según el tipo de objeto.

  • Enlace dinámico: El polimorfismo de C++ se implementa mediante enlace dinámico. En tiempo de ejecución, el puntero de la clase baseanimalPtrselecciona según el tipo de objeto al que apunta realmente (DogoCat) llamando al correspondientesound()método.

  • Destructor virtual: En una clase base con comportamiento polimórfico, el destructor debe declararse comovirtual, para asegurar que al eliminar un objeto de la clase derivada se llame al destructor de la clase derivada, evitando fugas de recursos.


Ejemplo 2

En el siguiente ejemplo, implementamos mediante polimorfismo una clase base genérica Shape y dos clases derivadas Rectangle y Triangle.

Al llamar a métodos de diferentes clases derivadas a través de un puntero de la clase base, se muestra la característica de enlace dinámico del polimorfismo.

Ejemplo 2

#include <iostream> using namespace std; // Clase base Shape, representa una forma class Shape { protected: int width, height; // Ancho y alto public: // Constructor, con parámetros por defecto Shape(int a = 0, int b = 0) : width(a), height(b) { } // Función virtual area, utilizada para calcular el área // Se utiliza la palabra clave virtual para implementar el polimorfismo virtual int area() { cout << "Shape class area: " << endl; return 0; } }; // Clase derivada Rectangle, representa un rectángulo class Rectangle : public Shape { public: // Constructor, usa el constructor de la clase base para inicializar width y height Rectangle(int a = 0, int b = 0) : Shape(a, b) { } // Reescribe la función area, calcula el área del rectángulo int area() override { cout << "Rectangle class area: " << endl; return width * height; } }; // Clase derivada Triangle, representa un triángulo class Triangle : public Shape { public: // Constructor, usa el constructor de la clase base para inicializar width y height Triangle(int a = 0, int b = 0) : Shape(a, b) { } // Reescribe la función area, calcula el área del triángulo int area() override { cout << "Triangle class area: " << endl; return (width * height / 2); } }; // Función principal int main() { Shape *shape; // Puntero de la clase base Rectangle rec(10, 7); // Objeto rectángulo Triangle tri(10, 5); // Objeto triángulo // Apunta el puntero de la clase base al objeto rectángulo y llama a la función area shape = &rec; cout << "Rectangle Area: " << shape->area() << endl; // Apunta el puntero de la clase base al objeto triángulo y llama a la función area shape = &tri; cout << "Triangle Area: " << shape->area() << endl; return 0; }

Cuando el código anterior se compila y ejecuta, produce los siguientes resultados:

Rectangle Area: Rectangle class area: 
70
Triangle Area: Triangle class area: 
25

Análisis del código

Definición de la clase Shape:

  • Shapees una clase base abstracta que define una función virtualarea()。area()es la función virtual para calcular el área, y se utiliza lavirtualpalabra clave, de modo que en la clase derivada se pueda reescribir esta función y así implementar el polimorfismo.
  • widthyheightSíprotectedatributos, solo se puedenShapeacceder en la clase y en sus clases derivadas.
// 基类 Shape,表示形状
class Shape {
   protected:
      int width, height; // 宽度和高度

   public:
      // 构造函数,带有默认参数
      Shape(int a = 0, int b = 0) : width(a), height(b) { }

      // 虚函数 area,用于计算面积
      virtual int area() {
         cout << "Shape class area: " << endl;
         return 0;
      }
};

Definición de la clase Rectangle:

  • Rectanglehereda deShapela clase, y reescribearea()el método, calculando el área del rectángulo.
  • area()El método utilizaoverridela palabra clave, lo que indica que esto es una reescritura delShapedearea()método de la clase base.
  • Rectangle::area()Devuelvewidth * height, es decir, el área del rectángulo.
// 派生类 Rectangle,表示矩形
class Rectangle : public Shape {
   public:
      // 构造函数,使用基类构造函数初始化 width 和 height
      Rectangle(int a = 0, int b = 0) : Shape(a, b) { }

      // 重写 area 函数,计算矩形面积
      int area() override { 
         cout << "Rectangle class area: " << endl;
         return width * height;
      }
};

Definición de la clase Triangle:

  • TriangleLa clase también hereda deShape, y sobrescribe elarea()método, utilizado para calcular el área del triángulo.
  • Triangle::area()Devuelvewidth * height / 2, que es la fórmula del área del triángulo.
// 派生类 Triangle,表示三角形
class Triangle : public Shape {
   public:
      // 构造函数,使用基类构造函数初始化 width 和 height
      Triangle(int a = 0, int b = 0) : Shape(a, b) { }

      // 重写 area 函数,计算三角形面积
      int area() override { 
         cout << "Triangle class area: " << endl;
         return (width * height / 2); 
      }
};

Comportamiento polimórfico en la función principal:

  • Se define un puntero de clase baseshape, este puntero puede apuntar a cualquierShapeobjeto de la clase o de sus clases derivadas.
  • Primero se hace que elshapepuntero apunte alRectangleobjetorec, y luego se llama ashape->area(). Dado quearea()es una función virtual, en este momento se enlaza dinámicamente aRectangle::area(), y se muestra el área del rectángulo.
  • A continuación, se hace que elshapepuntero apunte alTriangleobjetotri, y al llamar ashape->area()se enlazará dinámicamente aTriangle::area(), y se mostrará el área del triángulo.
// 主函数
int main() {
   Shape *shape;           // 基类指针
   Rectangle rec(10, 7);   // 矩形对象
   Triangle tri(10, 5);    // 三角形对象

   // 将基类指针指向矩形对象,并调用 area 函数
   shape = &rec;
   cout << "Rectangle Area: " << shape->area() << endl;

   // 将基类指针指向三角形对象,并调用 area 函数
   shape = &tri;
   cout << "Triangle Area: " << shape->area() << endl;

   return 0;
}

Conceptos clave

  • Función virtual: En la clase baseShapese define la función virtualarea(). La función virtual permite que la clase derivada pueda sobrescribir esta función y, en tiempo de ejecución, llamar a la implementación adecuada según el tipo real del objeto al que apunta el puntero.

  • Enlace dinámico: Dado quearea()es una función virtual,shape->area()al llamarla, en tiempo de ejecución, segúnshapeel tipo real del objeto al que apunta (RectangleoTriangle), se llama a la correspondientearea()implementación. Este mecanismo de decidir en tiempo de ejecución qué función llamar se denomina enlace dinámico, y es el núcleo del polimorfismo.

  • Polimorfismo del puntero de clase base: El puntero de clase baseshapepuede apuntar a cualquier objeto derivado deShape. Cuandoshapeapunta a diferentes objetos de clases derivadas, la llamada ashape->area()produce comportamientos diferentes, lo que refleja las características del polimorfismo.

  • Funciones virtuales

    Una función virtuales una función declarada en la clase base con la palabra clavevirtualvirtual.

    La función virtualpermite que la subclase la sobrescriba, de modo que en tiempo de ejecución, a través de un puntero o referencia de la clase base, se llame a la versión sobrescrita de la subclase, logrando así el enlace dinámico.

    Lo que queremos es que en cualquier punto del programa se pueda elegir la función a llamar según el tipo de objeto al que se llama. Esta operación se conoce comoenlace dinámico, oenlace tardío.

    Características:

    • En la clase base puede haber una implementación. Normalmente, la función virtual proporciona una implementación predeterminada en la clase base, pero la subclase puede optar por sobrescribirla.
    • Enlace dinámico: En tiempo de ejecución, se llama a la versión correspondiente de la función según el tipo real del objeto.
    • Sobrescritura opcional: La clase derivada puede sobrescribir la función virtual de forma opcional, pero no es obligatorio.

    Ejemplo

    #include <iostream>
    using namespace std;

    class Animal {
    public:
        virtual void sound() {  // Función virtual
            cout << "Animal makes a sound" << endl;
        }
    };

    class Dog : public Animal {
    public:
        void sound() override {  // Sobrescribir la función virtual
            cout << "Dog barks" << endl;
        }
    };

    int main() {
        Animal *animal = new Dog();
        animal->sound();  // Salida: Dog barks
        delete animal;
    }

    En el código anterior, sound es una función virtual de la clase Animal. Al llamar a sound() mediante el puntero Animal* animal, el programa elige llamar a Dog::sound() según el tipo real del objeto (Dog).

    Funciones virtuales puras

    Es posible que desee definir una función virtual en la clase base para redefinirla en la clase derivada y que se adapte mejor al objeto, pero si no puede dar una implementación significativa de la función virtual en la clase base, entonces se utiliza una función virtual pura.

    Una función virtual pura es una función virtual sin implementación, que se declara en la clase base con= 0para declararla.

    Una función virtual pura indica que la clase base define una interfaz, pero la implementación concreta corre a cargo de la clase derivada.

    Una función virtual pura convierte a la clase base en una clase abstracta (abstract class), que no se puede instanciar.

    Características:

    • Debe declararse en la clase base como= 0, lo que indica que no tiene implementación y la subclase debe sobrescribirla.
    • Clase abstracta: Una clase que contiene funciones virtuales puras no se puede instanciar directamente; solo se puede crear un objeto cuando la clase derivada implementa todas las funciones virtuales puras.
    • Definición de interfaz: Las funciones virtuales puras se utilizan generalmente para definir interfaces y permitir que las clases derivadas implementen comportamientos concretos.

    Podemos reescribir la función virtual area() de la clase base de la siguiente manera:

    #include <iostream> using namespace std; class Shape { public: virtual int area() = 0; // Función virtual pura, obliga a la subclase a implementar este método }; class Rectangle : public Shape { private: int width, height; public: Rectangle(int w, int h) : width(w), height(h) { } int area() override { // Implementación de la función virtual pura return width * height; } }; int main() { Shape *shape = new Rectangle(10, 5); cout << "Rectangle Area: " << shape->area() << endl; // Salida: Rectangle Area: 50 delete shape; }

    = 0Le dice al compilador que la función no tiene cuerpo; la función virtual anterior es unafunción virtual pura。

    Comparación entre funciones virtuales y funciones virtuales puras

    CaracterísticaFunción virtual (Virtual Function)Función virtual pura (Pure Virtual Function)
    DefiniciónEn la clase base se usavirtualvirtual, y tiene implementaciónEn la clase base se usa= 0= 0, y no tiene implementación
    Sobrescritura en subclaseLa subclase puede elegir sobrescribirlaLa subclase debe implementarla
    AbstracciónLa clase se puede instanciarConvierte la clase en abstracta, no se puede instanciar
    UsoProporciona un comportamiento predeterminado y permite que la subclase lo sobrescribaDefine una interfaz y obliga a la subclase a implementar un comportamiento concreto
    Otras extensiones