Repaso de las funciones virtuales
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, veamos más a fondo 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 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;
}
};Usar override es fundamental porque detecta errores en tiempo de compilación. Si escribes mal el nombre de la función por accidente 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 ninguna función que sobrescribir
std::cout << "Meow!" << std::endl;
}
};El especificador final impide que se siga sobrescribiendo. Ú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 una función final
};Usa siempre override al sobrescribir funciones virtuales. Esto documenta tu intención y permite que el compilador verifique que realmente estás sobrescribiendo una función virtual existente.
Desafío
FácilVamos a crear un sistema de notificaciones que demuestra el poder de las palabras clave override y final con funciones virtuales. Crearás una jerarquía de controladores de notificaciones en la que algunos métodos se pueden personalizar aún más y otros se bloquean para evitar modificaciones.
Organizarás tu código en tres archivos:
Notifier.h: Define una clase baseNotifierque representa cualquier emisor 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
- Un miembro protegido
EmailNotifier.h: Define una claseEmailNotifierque hereda deNotifier:- Un miembro privado
std::string domain - Un constructor que recibe el destinatario y el dominio, y pasa el destinatario a la clase base
- Anula
send()para que imprima:Emailing <recipient>@<domain>: <message> - Anula
getType()y márcalo comofinal. Debe devolver"Email"
Después, define una clase
UrgentEmailNotifierque hereda deEmailNotifier:- Un constructor que recibe el destinatario y el dominio, y pasa ambos a
EmailNotifier - Anula
send()para que imprima:[URGENT] Emailing <recipient>@<domain>: <message> - Nota: aquí no puedes anular
getType()porque se marcó comofinalenEmailNotifier
- Un miembro privado
main.cpp: Lee tres datos de entrada (cada uno en una línea separada):- Nombre del destinatario
- Dominio del correo electrónico
- Texto del mensaje
Crea dinámicamente tres objetos notificadores: un
Notifierbase, unEmailNotifiery unUrgentEmailNotifier: todos con el mismo destinatario (y el mismo dominio cuando corresponda). Guárdalos 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 formato a cada entrada así:Type: <type> <send output>Imprime una línea en blanco entre cada notificador. Cuando termines, libera los objetos asignados dinámicamente.
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 anular 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 anulados para detectar cualquier incompatibilidad de firmas en tiempo de 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 arreglo de punteros Notifier* con 3 elementos
// TODO: Crea dinámicamente:
// - Un Notifier base con recipient
// - Un EmailNotifier con recipient y domain
// - Un UrgentEmailNotifier con recipient y domain
// TODO: Recorre el arreglo y, para cada notificador:
// - Imprime "Type: " seguido del resultado de getType()
// - Llama a send() con message
// - Imprime una línea en blanco entre notificadores (no después del último)
// TODO: Libera 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 la POO
Archivos externosCompilación y construcción en C++Archivos de cabecera y archivos fuenteEspacios de nombres y ámbitoIntroducción a la POO en C++Clases y objetosEl puntero 'this'Métodos (funciones miembro)Atributos (miembros de datos)Fundamentos de constructores y destructoresRepaso: calculadora sencilla4Propiedades de clase
Miembros de instancia vs. estáticosGetters y settersFunciones miembro constPalabra clave mutableMétodos y variables estáticosFunciones y clases friendRepaso: gestor de cuentas bancarias7Herencia
Herencia básicaNiveles de acceso en la herenciaOrden de llamada de constructores y destructoresSobrescritura de métodosFunciones virtuales y VTableHerencia múltipleHerencia virtualRepaso: jerarquía de empleados2Gestión de memoria
Memoria de pila vs. heapPunteros y referenciasMemoria dinámica (new/delete)Punteros inteligentes en C++RAII en C++Repaso: gestor de arreglos dinámicos5Encapsulación
Especificadores de acceso en C++Especificadores de acceso en profundidadOcultación de informaciónStruct vs. classClases anidadas e internasRepaso: sistema de registros de estudiantes8Polimorfismo
Polimorfismo en compilación y en tiempo de ejecuciónSobrecarga de funcionesRepaso de las funciones virtualesFunciones virtuales purasClases abstractasDiseño de interfaces en C++Conversión dinámica y RTTIRepaso: calculadora de formas11Conceptos avanzados de POO
Composición frente a herenciaMixins mediante CRTPIdiom PimplBorrado de tiposClases enum 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 predeterminadoConstructor con parámetrosConstructor de copiaConstructor de movimientoListas de inicialización del constructorConstructores delegadosAnálisis detallado del destructorRegla de tres / cinco / ceroRepaso: clase String6Sobrecarga de operadores
Introducción a la sobrecarga de operadoresSobrecarga de operadores aritméticosSobrecarga de operadores de comparaciónOperadores de flujoSobrecarga del operador de asignaciónSobrecarga de los operadores [] y ()Operadores de conversión de tipoRepaso: clase Matrix9Plantillas
Plantillas de funcionesPlantillas de clasesEspecialización de plantillasPlantillas variádicasFundamentos de SFINAE y rasgos de tiposRepaso: contenedor genéricoPractica por tu cuenta: Compilador de C++ online