Menu
Coddy logo textTech

Funciones virtuales revisadas

Parte de la sección Programación Orientada a Objetos del Journey de C++ de Coddy — lección 58 de 104.

Ahora que entendemos la diferencia entre el polimorfismo en tiempo de compilación y en tiempo de ejecución, echemos un vistazo más profundo a las funciones virtuales y la palabra clave override que las hace más seguras de usar.

Cuando marcas una función como virtual en una clase base, las clases derivadas pueden proporcionar su propia implementación. El especificador override le indica explícitamente al compilador que tienes la intención de sobrescribir una función virtual:

class Animal {
public:
    virtual void speak() {
        std::cout << "Some sound" << std::endl;
    }
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Woof!" << std::endl;
    }
};

El uso de override es crucial porque detecta errores en tiempo de compilación. Si escribes mal accidentalmente el nombre de la función o usas parámetros incorrectos, el compilador te alertará en lugar de crear silenciosamente una nueva función:

class Cat : public Animal {
public:
    void speek() override {  // Error de compilación: no hay función para sobrescribir
        std::cout << "Meow!" << std::endl;
    }
};

El especificador final impide futuras sobrescrituras. Úselo cuando una clase derivada deba ser la última en sobrescribir una función en particular:

class Bulldog : public Dog {
public:
    void speak() override final {
        std::cout << "Gruff woof!" << std::endl;
    }
};

class TinyBulldog : public Bulldog {
    void speak() override {}  // Error: no se puede sobrescribir una función final
};

Siempre usa override al sobrescribir funciones virtuales. Documenta tu intención y permite que el compilador verifique que realmente estás sobrescribiendo una función virtual existente.

challenge icon

Desafío

Fácil

Vamos a construir un sistema de notificación que demuestre el poder de las palabras clave override y final con funciones virtuales. Crearás una jerarquía de controladores de notificación donde algunos métodos pueden personalizarse aún más y otros están bloqueados para evitar modificaciones.

Organizarás tu código en tres archivos:

  • Notifier.h: Define una clase base Notifier que representa a cualquier remitente de notificaciones:
    • Un miembro protegido std::string recipient
    • Un constructor que recibe y almacena el nombre del destinatario
    • Un método virtual send(const std::string& message) que imprime: Notifying <recipient>: <message>
    • Un método virtual getType() que devuelve la cadena "Generic"
    • Un destructor virtual
  • EmailNotifier.h: Define una clase EmailNotifier que hereda de Notifier:
    • Un miembro privado std::string domain
    • Un constructor que recibe el destinatario y el dominio, pasando el destinatario a la clase base
    • Sobrescribe send() para imprimir: Emailing <recipient>@<domain>: <message>
    • Sobrescribe getType() y márcalo como final — debe devolver "Email"

    Luego define una clase UrgentEmailNotifier que hereda de EmailNotifier:

    • Un constructor que recibe el destinatario y el dominio, pasando ambos a EmailNotifier
    • Sobrescribe send() para imprimir: [URGENT] Emailing <recipient>@<domain>: <message>
    • Nota: No puedes sobrescribir getType() aquí porque fue marcado como final en EmailNotifier
  • main.cpp: Lee tres entradas (cada una en una línea separada):
    1. Nombre del destinatario
    2. Dominio de correo electrónico
    3. Texto del mensaje

    Crea tres objetos de notificación dinámicamente: un Notifier base, un EmailNotifier y un UrgentEmailNotifier — todos usando el mismo destinatario (y dominio cuando corresponda). Almacénalos en un arreglo de punteros Notifier*.

    Recorre el arreglo y, para cada notificador, imprime su tipo usando getType(), luego llama a send() con tu mensaje. Formatea cada entrada como:

    Type: <type>
    <send output>

    Imprime una línea en blanco entre cada notificador. Limpia tus objetos asignados dinámicamente cuando hayas terminado.

Por ejemplo, con las entradas Alice, company.com y Meeting at 3pm:

Type: Generic
Notifying Alice: Meeting at 3pm

Type: Email
Emailing Alice@company.com: Meeting at 3pm

Type: Email
[URGENT] Emailing Alice@company.com: Meeting at 3pm

Observa cómo UrgentEmailNotifier puede sobrescribir send() para personalizar el formato del mensaje, pero hereda el tipo "Email" de EmailNotifier porque getType() fue marcado como final. Usa la palabra clave override en todos los métodos sobrescritos para detectar cualquier desajuste de firma en tiempo de compilación.

Hoja de referencia

El especificador override le indica explícitamente al compilador que tienes la intención de sobrescribir una función virtual de una clase base:

class Animal {
public:
    virtual void speak() {
        std::cout << "Some sound" << std::endl;
    }
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Woof!" << std::endl;
    }
};

El uso de override detecta errores en tiempo de compilación. Si escribes mal el nombre de la función o usas parámetros incorrectos, el compilador te alertará:

class Cat : public Animal {
public:
    void speek() override {  // Error del compilador: no hay función para sobrescribir
        std::cout << "Meow!" << std::endl;
    }
};

El especificador final evita que se sigan sobrescribiendo funciones en las clases derivadas:

class Bulldog : public Dog {
public:
    void speak() override final {
        std::cout << "Gruff woof!" << std::endl;
    }
};

class TinyBulldog : public Bulldog {
    void speak() override {}  // Error: no se puede sobrescribir una función final
};

Utiliza siempre override al sobrescribir funciones virtuales para documentar tu intención y permitir la verificación del compilador.

Pruébalo tú mismo

#include <iostream>
#include <string>
#include "Notifier.h"
#include "EmailNotifier.h"

using namespace std;

int main() {
    // Leer entradas
    string recipient;
    string domain;
    string message;
    
    getline(cin, recipient);
    getline(cin, domain);
    getline(cin, message);
    
    // TODO: Crear un arreglo de punteros Notifier* con 3 elementos
    
    // TODO: Crear dinámicamente:
    // - Un Notifier base con el recipient
    // - Un EmailNotifier con recipient y domain
    // - Un UrgentEmailNotifier con recipient y domain
    
    // TODO: Recorrer el arreglo y para cada notificador:
    // - Imprimir "Type: " seguido del resultado de getType()
    // - Llamar a send() con el message
    // - Imprimir una línea en blanco entre notificadores (no después del último)
    
    // TODO: Limpiar los objetos asignados dinámicamente
    
    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