Virtuelle Funktionen erneut betrachtet
Teil des Abschnitts Objektorientierte Programmierung der C++-Journey von Coddy. Lektion 58 von 104.
Nachdem wir nun den Unterschied zwischen Polymorphismus zur Compile-Zeit und zur Laufzeit verstanden haben, werfen wir einen genaueren Blick auf virtuelle Funktionen und das Schlüsselwort override, das ihre Verwendung sicherer macht.
Wenn du eine Funktion in einer Basisklasse als virtual kennzeichnest, können abgeleitete Klassen ihre eigene Implementierung bereitstellen. Der Spezifizierer override teilt dem Compiler ausdrücklich mit, dass du beabsichtigst, eine virtuelle Funktion zu überschreiben:
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;
}
};Die Verwendung von override ist entscheidend, da dadurch Fehler bereits zur Kompilierzeit erkannt werden. Wenn du den Funktionsnamen versehentlich falsch schreibst oder die falschen Parameter verwendest, wird der Compiler dich darauf hinweisen, anstatt stillschweigend eine neue Funktion zu erstellen:
class Cat : public Animal {
public:
void speek() override { // Compilerfehler: keine Funktion zum Überschreiben
std::cout << "Meow!" << std::endl;
}
};Der Spezifizierer final verhindert weiteres Überschreiben. Verwende ihn, wenn eine abgeleitete Klasse die letzte sein soll, die eine bestimmte Funktion überschreibt:
class Bulldog : public Dog {
public:
void speak() override final {
std::cout << "Gruff woof!" << std::endl;
}
};
class TinyBulldog : public Bulldog {
void speak() override {} // Fehler: finale Funktion kann nicht überschrieben werden
};Verwende beim Überschreiben virtueller Funktionen immer override. Dadurch dokumentierst du deine Absicht und ermöglichst es dem Compiler zu überprüfen, ob du tatsächlich eine vorhandene virtuelle Funktion überschreibst.
Aufgabe
EinfachErstellen wir ein Benachrichtigungssystem, das die Leistungsfähigkeit der Schlüsselwörter override und final mit virtuellen Funktionen demonstriert. Du erstellst eine Hierarchie von Benachrichtigungs-Handlern, in der einige Methoden weiter angepasst werden können, während andere gesperrt sind, um Änderungen zu verhindern.
Du organisierst deinen Code auf drei Dateien:
Notifier.h: Definiere eine BasisklasseNotifier, die einen beliebigen Absender von Benachrichtigungen darstellt:- Ein geschütztes Element
std::string recipient - Einen Konstruktor, der den Namen des Empfängers entgegennimmt und speichert
- Eine virtuelle Methode
send(const std::string& message), die Folgendes ausgibt:Notifying <recipient>: <message> - Eine virtuelle Methode
getType(), die den String"Generic"zurückgibt - Einen virtuellen Destruktor
- Ein geschütztes Element
EmailNotifier.h: Definiere eine KlasseEmailNotifier, die vonNotifiererbt:- Ein privates Element
std::string domain - Einen Konstruktor, der Empfänger und Domain entgegennimmt und den Empfänger an die Basisklasse übergibt
- Überschreibe
send(), sodass Folgendes ausgegeben wird:Emailing <recipient>@<domain>: <message> - Überschreibe
getType()und markiere die Methode mitfinal. Sie soll"Email"zurückgeben
Definiere anschließend eine Klasse
UrgentEmailNotifier, die vonEmailNotifiererbt:- Einen Konstruktor, der Empfänger und Domain entgegennimmt und beide an
EmailNotifierübergibt - Überschreibe
send(), sodass Folgendes ausgegeben wird:[URGENT] Emailing <recipient>@<domain>: <message> - Hinweis: Du kannst
getType()hier nicht überschreiben, da die Methode infinalmitEmailNotifiermarkiert wurde
- Ein privates Element
main.cpp: Lies drei Eingaben ein (jeweils in einer eigenen Zeile):- Name des Empfängers
- E-Mail-Domain
- Nachrichtentext
Erstelle dynamisch drei Benachrichtigungsobjekte: ein
Notifier, einEmailNotifierund einUrgentEmailNotifier, alle mit demselben Empfänger (und, falls zutreffend, derselben Domain). Speichere sie in einem Array ausNotifier*-Zeigern.Durchlaufe das Array und gib für jeden Benachrichtigungsdienst seinen Typ mithilfe von
getType()aus. Rufe anschließendsend()mit deiner Nachricht auf. Formatiere jeden Eintrag wie folgt:Type: <type> <send output>Gib zwischen den einzelnen Benachrichtigungsdiensten eine Leerzeile aus. Räume die dynamisch angelegten Objekte anschließend auf.
Beispielsweise mit den Eingaben Alice, company.com und 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 3pmBeachte, wie UrgentEmailNotifier send() überschreiben kann, um das Nachrichtenformat anzupassen, aber den Typ "Email" von EmailNotifier erbt, da getType() mit final markiert wurde. Verwende das Schlüsselwort override für alle überschriebenen Methoden, um Signaturabweichungen bereits zur Kompilierzeit zu erkennen.
Probier es selbst
#include <iostream>
#include <string>
#include "Notifier.h"
#include "EmailNotifier.h"
using namespace std;
int main() {
// Eingaben lesen
string recipient;
string domain;
string message;
getline(cin, recipient);
getline(cin, domain);
getline(cin, message);
// TODO: Erstelle ein Array von Notifier*-Zeigern mit 3 Elementen
// TODO: Dynamisch erstellen:
// - Einen Basis-Notifier mit dem recipient
// - Einen EmailNotifier mit recipient und domain
// - Einen UrgentEmailNotifier mit recipient und domain
// TODO: Durchlaufe das Array und für jeden Notifier:
// - Gib "Type: " gefolgt vom Ergebnis von getType() aus
// - Rufe send() mit der message auf
// - Gib eine Leerzeile zwischen den Notifiern aus (nicht nach dem letzten)
// TODO: Räume dynamisch allokierte Objekte auf
return 0;
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP
Externe DateienC++ Build & KompilierungHeader-Dateien & QuelldateienNamespaces & GültigkeitsbereichEinführung in OOP mit C++Klassen vs. ObjekteDer „this“-ZeigerMethoden (Member-Funktionen)Attribute (Datenmember)Grundlagen zu Ctors & DtorsRückblick – Einfacher Taschenrechner4Klassenattribute
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen und -KlassenZusammenfassung – Bankkontoverwaltung7Vererbung
Grundlagen der VererbungZugriffsebenen bei der VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick – Mitarbeiterhierarchie2Speicherverwaltung
Stack- vs. Heap-SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Intelligente Zeiger in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsbezeichner in C++Zugriffsbezeichner im DetailInformationsverbergungStruct vs. KlasseVerschachtelte und innere KlassenWiederholung – Studierendenverwaltungssystem8Polymorphie
Polymorphie zur Compilezeit vs. LaufzeitFunktionsüberladungVirtuelle Funktionen erneut betrachtetRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIRückblick – Shape-Calculator3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorMove-KonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktor im DetailDrei-/Fünf-/Null-RegelZusammenfassung – String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladung der Operatoren [] und ()TypkonvertierungsoperatorenWiederholung – Matrix-Klasse9Templates
FunktionstemplatesKlassen-TemplatesTemplate-SpezialisierungVariadische TemplatesGrundlagen zu SFINAE und Type TraitsRückblick – Generischer ContainerÜbe selbstständig: Online-C++-Compiler