Virtuelle Funktionen – Wiederholung
Teil des Abschnitts Objektorientierte Programmierung der C++-Journey von Coddy — Lektion 58 von 104.
Nachdem wir nun den Unterschied zwischen Polymorphismus zur Kompilierzeit und zur Laufzeit verstehen, werfen wir einen tieferen Blick auf virtuelle Funktionen und das Schlüsselwort override, das ihre Verwendung sicherer macht.
Wenn Sie eine Funktion in einer Basisklasse als virtual markieren, können abgeleitete Klassen ihre eigene Implementierung bereitstellen. Der Bezeichner override teilt dem Compiler explizit mit, dass Sie beabsichtigen, 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 sie Fehler zur Kompilierzeit abfängt. Wenn Sie versehentlich den Funktionsnamen falsch schreiben oder falsche Parameter verwenden, wird der Compiler Sie warnen, anstatt stillschweigend eine neue Funktion zu erstellen:
class Cat : public Animal {
public:
void speek() override { // Kompilierfehler: keine Funktion zum Überschreiben
std::cout << "Meow!" << std::endl;
}
};Der final-Bezeichner verhindert weiteres Überschreiben. Verwenden Sie 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: Eine finale Funktion kann nicht überschrieben werden
};Verwenden Sie immer override beim Überschreiben virtueller Funktionen. Es dokumentiert Ihre Absicht und lässt den Compiler überprüfen, dass Sie tatsächlich eine bestehende virtuelle Funktion überschreiben.
Aufgabe
EinfachLassen Sie uns ein Benachrichtigungssystem bauen, das die Leistungsfähigkeit der Schlüsselwörter override und final bei virtuellen Funktionen demonstriert. Sie werden eine Hierarchie von Benachrichtigungs-Handlern erstellen, bei denen einige Methoden weiter angepasst werden können und andere gesperrt sind, um Änderungen zu verhindern.
Sie werden Ihren Code über drei Dateien organisieren:
Notifier.h: Definieren Sie eine BasisklasseNotifier, die einen beliebigen Benachrichtigungs-Sender repräsentiert:- Ein geschütztes (protected)
std::string recipientMitglied - Ein Konstruktor, der den Namen des Empfängers entgegennimmt und speichert
- Eine virtuelle
send(const std::string& message)Methode, die Folgendes ausgibt:Notifying <recipient>: <message> - Eine virtuelle
getType()Methode, die den String"Generic"zurückgibt - Ein virtueller Destruktor
- Ein geschütztes (protected)
EmailNotifier.h: Definieren Sie eine KlasseEmailNotifier, die vonNotifiererbt:- Ein privates
std::string domainMitglied - Ein Konstruktor, der Empfänger und Domain entgegennimmt und den Empfänger an die Basisklasse übergibt
- Überschreiben Sie
send(), um Folgendes auszugeben:Emailing <recipient>@<domain>: <message> - Überschreiben Sie
getType()und markieren Sie es alsfinal— es sollte"Email"zurückgeben
Definieren Sie dann eine Klasse
UrgentEmailNotifier, die vonEmailNotifiererbt:- Ein Konstruktor, der Empfänger und Domain entgegennimmt und beides an
EmailNotifierübergibt - Überschreiben Sie
send(), um Folgendes auszugeben:[URGENT] Emailing <recipient>@<domain>: <message> - Hinweis: Sie können
getType()hier nicht überschreiben, da es inEmailNotifieralsfinalmarkiert wurde
- Ein privates
main.cpp: Lesen Sie drei Eingaben ein (jede in einer separaten Zeile):- Name des Empfängers
- E-Mail-Domain
- Nachrichtentext
Erstellen Sie drei Benachrichtigungsobjekte dynamisch: einen Basis-
Notifier, einenEmailNotifierund einenUrgentEmailNotifier— alle unter Verwendung desselben Empfängers (und der Domain, wo zutreffend). Speichern Sie diese in einem Array vonNotifier*-Pointern.Iterieren Sie durch das Array und geben Sie für jeden Notifier seinen Typ mit
getType()aus, und rufen Sie dannsend()mit Ihrer Nachricht auf. Formatieren Sie jeden Eintrag wie folgt:Type: <type> <send output>Geben Sie eine Leerzeile zwischen den einzelnen Notifiern aus. Bereinigen Sie Ihre dynamisch zugewiesenen Objekte, wenn Sie fertig sind.
Zum Beispiel 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 3pmBeachten Sie, wie UrgentEmailNotifier die Methode send() überschreiben kann, um das Nachrichtenformat anzupassen, aber den Typ "Email" von EmailNotifier erbt, weil getType() als final markiert wurde. Verwenden Sie das Schlüsselwort override bei allen überschriebenen Methoden, um etwaige Signatur-Fehler bereits zur Kompilierzeit zu erkennen.
Spickzettel
Der Bezeichner override teilt dem Compiler explizit mit, dass Sie beabsichtigen, eine virtuelle Funktion aus einer Basisklasse 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 fängt Fehler zur Kompilierzeit ab. Wenn Sie den Funktionsnamen falsch schreiben oder falsche Parameter verwenden, wird der Compiler Sie warnen:
class Cat : public Animal {
public:
void speek() override { // Compiler-Fehler: keine Funktion zum Überschreiben
std::cout << "Meow!" << std::endl;
}
};Der Bezeichner final verhindert weiteres Überschreiben in abgeleiteten Klassen:
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
};Verwenden Sie beim Überschreiben virtueller Funktionen immer override, um Ihre Absicht zu dokumentieren und die Überprüfung durch den Compiler zu ermöglichen.
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 aus Notifier* Pointern 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 Notifiers aus (nicht nach dem letzten)
// TODO: Dynamisch zugewiesene Objekte bereinigen
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 & QuelldateienNamensräume & GültigkeitsbereicheEinführung in OOP in C++Klassen vs. ObjekteDer 'this'-PointerMethoden (Elementfunktionen)Attribute (Datenelemente)Grundlagen zu Ctors & DtorsRückblick - Einfacher Taschenrechner4Klasseneigenschaften
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen & KlassenZusammenfassung - Bankkonto-Manager7Vererbung
Grundlagen der VererbungZugriffsstufen bei VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick - Mitarbeiter-Hierarchie2Speicherverwaltung
Stack vs. Heap SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Smart Pointers in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsspezifizierer in C++Zugriffsspezifizierer im DetailInformation HidingStruct vs. ClassVerschachtelte & innere KlassenRückblick - Studentenverwaltungssystem8Polymorphie
Kompilierzeit- vs. Laufzeit-PolymorphieFunktionsüberladungVirtuelle Funktionen – WiederholungRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIZusammenfassung – Formen-Rechner3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorVerschiebekonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktoren im DetailRule of Three / Five / ZeroZusammenfassung - String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladen der [] und () OperatorenTypumwandlungsoperatorenRückblick - Matrix-Klasse9Templates
FunktionstemplatesKlassentemplatesTemplate-SpezialisierungVariadische TemplatesSFINAE & Type Traits GrundlagenRückblick - Generische Container