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 el polimorfismo en tiempo de ejecución, veamos más detenidamente las funciones virtuales y la palabra clave override que hace que sean 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 indica explícitamente al compilador que pretendes 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;
}
};Usar override es crucial porque detecta errores en tiempo de compilación. Si escribes accidentalmente mal el nombre de la función o usas parámetros incorrectos, el compilador te avisará en lugar de crear silenciosamente una función nueva:
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 impide sobrescrituras posteriores. Úsalo cuando una clase derivada deba ser la última en sobrescribir una función determinada:
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 la función final
};Usa siempre 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.
Desafío
FácilConstruyamos un sistema de notificaciones que demuestre el poder de las palabras clave override y final con funciones virtuales. Crearás una jerarquía de gestores de notificaciones en la que algunos métodos puedan personalizarse aún más y otros queden bloqueados para evitar modificaciones.
Organizarás tu código en tres archivos:
Notifier.h: Define una clase baseNotifierque represente a cualquier emisor de notificaciones:- Un miembro
std::string recipientprotegido - Un constructor que reciba y almacene el nombre del destinatario
- Un método virtual
send(const std::string& message)que imprima:Notifying <recipient>: <message> - Un método virtual
getType()que devuelva la cadena"Generic" - Un destructor virtual
- Un miembro
EmailNotifier.h: Define una claseEmailNotifierque herede deNotifier:- Un miembro privado
std::string domain - Un constructor que reciba el destinatario y el dominio, pasando el destinatario a la clase base
- Redefine
send()para imprimir:Emailing <recipient>@<domain>: <message> - Redefine
getType()y márcalo comofinal. Debe devolver"Email"
Después define una clase
UrgentEmailNotifierque herede deEmailNotifier:- Un constructor que reciba el destinatario y el dominio, pasando ambos a
EmailNotifier - Redefine
send()para imprimir:[URGENT] Emailing <recipient>@<domain>: <message> - Nota: Aquí no puedes redefinir
getType()porque se marcó comofinalenEmailNotifier
- Un miembro privado
main.cpp: Lee tres entradas, cada una en una línea independiente:- Nombre del destinatario
- Dominio del correo electrónico
- Texto del mensaje
Crea dinámicamente tres objetos de notificación: un
Notifierbase, unEmailNotifiery unUrgentEmailNotifier, todos usando el mismo destinatario (y el dominio cuando corresponda). Almacénalos en un arreglo de punterosNotifier*.Recorre el arreglo y, para cada notificador, imprime su tipo usando
getType()y después llama asend()con tu mensaje. Da a cada entrada el siguiente formato:Type: <type> <send output>Imprime una línea en blanco entre cada notificador. Libera los objetos asignados dinámicamente cuando termines.
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 3pmObserva cómo UrgentEmailNotifier puede redefinir send() para personalizar el formato del mensaje, pero hereda el tipo "Email" de EmailNotifier porque getType() se marcó como final. Usa la palabra clave override en todos los métodos redefinidos para detectar cualquier discrepancia en la firma durante la compilación.
Pruébalo tú mismo
#include <iostream>
#include <string>
#include "Notifier.h"
#include "EmailNotifier.h"
using namespace std;
int main() {
// Lee las entradas
string recipient;
string domain;
string message;
getline(cin, recipient);
getline(cin, domain);
getline(cin, message);
// TODO: Crea un array de punteros Notifier* con 3 elementos
// TODO: Crea dinámicamente:
// - Un Notifier base con el recipient
// - Un EmailNotifier con recipient y domain
// - Un UrgentEmailNotifier con recipient y domain
// TODO: Recorre el array y para cada notifier:
// - Imprime "Type: " seguido del resultado de getType()
// - Llama a send() con el message
// - Imprime una línea en blanco entre notifiers (no después del último)
// TODO: Limpia los objetos asignados dinámicamente
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éricoPractica por tu cuenta: Compilador de C++ online